fix(sync): purge orphan base entries when both sides deleted
`compute_sync_actions` emits no action for files that are missing from both local and remote but still tracked in the sync base (the `(None, None, Some(_))` arm). Nothing else cleaned those entries, so `.syncstate.json` grew forever every time a file was deleted both locally and remotely — and on each subsequent sync the same no-op match fired again. Add a `prune_orphan_bases` pass that runs before `compute_sync_actions` in `sync_workspace_inner`, dropping any base entry whose path is in neither the local nor remote scan. Unit-tested in isolation.
This commit is contained in:
parent
970210b647
commit
1fcc6e7f6d
|
|
@ -230,6 +230,22 @@ pub fn compute_sync_actions(
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove base entries for files that are gone from both local and remote.
|
||||||
|
/// `compute_sync_actions` emits no action for the both-deleted case, so without
|
||||||
|
/// this pass those entries would persist in `.syncstate.json` indefinitely.
|
||||||
|
fn prune_orphan_bases(
|
||||||
|
sync_state: &mut SyncState,
|
||||||
|
local_files: &[LocalFileInfo],
|
||||||
|
remote_files: &[RemoteFileSnapshot],
|
||||||
|
) {
|
||||||
|
let live_paths: std::collections::HashSet<&str> = local_files
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.path.as_str())
|
||||||
|
.chain(remote_files.iter().map(|f| f.path.as_str()))
|
||||||
|
.collect();
|
||||||
|
sync_state.files.retain(|p, _| live_paths.contains(p.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
/// Compare two timestamps for equality by parsing both, tolerating format differences.
|
/// Compare two timestamps for equality by parsing both, tolerating format differences.
|
||||||
fn timestamps_equal(a: Option<&str>, b: Option<&str>) -> bool {
|
fn timestamps_equal(a: Option<&str>, b: Option<&str>) -> bool {
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
|
|
@ -605,6 +621,12 @@ async fn sync_workspace_inner(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Purge orphan base entries: files we previously tracked that are now gone
|
||||||
|
// from both local and remote. Without this, `.syncstate.json` accumulates
|
||||||
|
// ghost entries forever because the both-deleted diff case emits no action
|
||||||
|
// and so nothing else would clean them.
|
||||||
|
prune_orphan_bases(&mut sync_state, &local_files, &remote_files);
|
||||||
|
|
||||||
// Compute actions from three-way diff
|
// Compute actions from three-way diff
|
||||||
let fresh_actions = compute_sync_actions(&local_files, &remote_files, &sync_state);
|
let fresh_actions = compute_sync_actions(&local_files, &remote_files, &sync_state);
|
||||||
|
|
||||||
|
|
@ -1107,6 +1129,22 @@ mod tests {
|
||||||
assert!(actions.is_empty());
|
assert!(actions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prune_orphan_bases() {
|
||||||
|
let mut state = SyncState::default();
|
||||||
|
state.files.insert("kept_local.md".to_string(), make_base("a"));
|
||||||
|
state.files.insert("kept_remote.md".to_string(), make_base("b"));
|
||||||
|
state.files.insert("orphan.md".to_string(), make_base("c"));
|
||||||
|
|
||||||
|
let local = vec![make_local("kept_local.md", "a")];
|
||||||
|
let remote = vec![make_remote("kept_remote.md")];
|
||||||
|
prune_orphan_bases(&mut state, &local, &remote);
|
||||||
|
|
||||||
|
assert!(state.files.contains_key("kept_local.md"));
|
||||||
|
assert!(state.files.contains_key("kept_remote.md"));
|
||||||
|
assert!(!state.files.contains_key("orphan.md"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_files_mixed() {
|
fn test_multiple_files_mixed() {
|
||||||
let local = vec![
|
let local = vec![
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue