From 94b429386bd876b4315a6c70a7023fa7070c3dfb Mon Sep 17 00:00:00 2001 From: mhohlios Date: Mon, 2 Nov 2026 14:10:46 -0500 Subject: [PATCH 1/2] Wait for a kept clone's fetch before scanning from its mark A kept clone's first pull scan could run before its first fetch, when the mark it joined at is not in the clone yet. The scan then moved the mark back to the clone's old head, and the next pull rescanned commits pushed while the Mac was away. A mark the clone does not have now waits. --- crates/workspace-host/src/alerts.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/workspace-host/src/alerts.rs b/crates/workspace-host/src/alerts.rs index 32424668..0f8bb93d 100644 --- a/crates/workspace-host/src/alerts.rs +++ b/crates/workspace-host/src/alerts.rs @@ -205,7 +205,8 @@ impl PullScan { } /// Scans the next commit on the head's first-parent line after `mark`, with any commits a /// merge brought, oldest first: where the mark goes, `None` when it is at the head. Walking - /// that line keeps the mark an ancestor of what is left, so it always moves forward. + /// that line keeps the mark an ancestor of what is left, so it always moves forward. A mark + /// the clone does not have yet, such as a kept clone's before its first fetch, waits for it. fn advance( &self, check: &str, @@ -214,6 +215,11 @@ impl PullScan { ) -> std::result::Result, String> { let failed = |error: anyhow::Error| format!("{error:#}"); let clone = self.clone.as_path(); + let known = format!("{mark}^{{commit}}"); + let answer = runtime::git_answer(clone, &["cat-file", "-e", &known]).map_err(failed)?; + if !answer.status.success() { + return Ok(None); + } let start = self.start(mark); let range = format!("{start}..{}", self.head); let walk = ["rev-list", "--first-parent", "--reverse", &range]; @@ -1050,6 +1056,21 @@ mod tests { }); } + #[test] + fn a_kept_clones_first_scan_waits_for_its_fetch_and_scans_no_old_history() { + let (_folder, github) = fixture(); + let mut mac = Mac::join(&github); + let sage = crate::storage::tests::space_named(&mac.host, "Sage"); + mac.host.remove_space(&sage.id).unwrap(); + github.commit(&in_folder(NOTE), PHONE); + join_sage(&mut mac.host, &github).unwrap(); + mac.scan(); + assert_eq!(mac.mark(SENSITIVE_INFO), head(&github), "the mark waits"); + mac.pull(&github); + assert_eq!(mac.alerts(), [], "what it missed is old history"); + assert_eq!(mac.mark(SENSITIVE_INFO), head(&github)); + } + #[test] fn a_rewrite_restarts_at_the_merge_base() { let (_folder, github) = fixture(); From 57a01bfba9fa501a6a8193e8f95ff9a7f3dc96a1 Mon Sep 17 00:00:00 2001 From: mhohlios Date: Mon, 2 Nov 2026 15:10:00 -0500 Subject: [PATCH 2/2] Give a kept clone its join head so a rewrite cannot stall its scan A kept clone rejoining a Space now fetches the head it joined at from the staging clone, so its pull scan's mark is always a commit it has. The scan waits while the head is at or behind the mark, instead of waiting on a missing mark, which a rewrite before the first pull left missing for good. --- crates/workspace-host/src/alerts.rs | 30 +++++++++++++++++++++++----- crates/workspace-host/src/storage.rs | 5 +++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/workspace-host/src/alerts.rs b/crates/workspace-host/src/alerts.rs index 0f8bb93d..932e58b5 100644 --- a/crates/workspace-host/src/alerts.rs +++ b/crates/workspace-host/src/alerts.rs @@ -205,8 +205,8 @@ impl PullScan { } /// Scans the next commit on the head's first-parent line after `mark`, with any commits a /// merge brought, oldest first: where the mark goes, `None` when it is at the head. Walking - /// that line keeps the mark an ancestor of what is left, so it always moves forward. A mark - /// the clone does not have yet, such as a kept clone's before its first fetch, waits for it. + /// that line keeps the mark an ancestor of what is left, so it always moves forward. A head + /// behind the mark, such as a kept clone's before its first fetch, has nothing new yet. fn advance( &self, check: &str, @@ -215,9 +215,7 @@ impl PullScan { ) -> std::result::Result, String> { let failed = |error: anyhow::Error| format!("{error:#}"); let clone = self.clone.as_path(); - let known = format!("{mark}^{{commit}}"); - let answer = runtime::git_answer(clone, &["cat-file", "-e", &known]).map_err(failed)?; - if !answer.status.success() { + if publish::is_ancestor(clone, &self.head, mark) { return Ok(None); } let start = self.start(mark); @@ -1071,6 +1069,28 @@ mod tests { assert_eq!(mac.mark(SENSITIVE_INFO), head(&github)); } + #[test] + fn a_rewrite_before_a_kept_clones_first_fetch_scans_what_follows() { + let (_folder, github) = fixture(); + let mut mac = Mac::join(&github); + let base = head(&github); + let sage = crate::storage::tests::space_named(&mac.host, "Sage"); + mac.host.remove_space(&sage.id).unwrap(); + github.commit(&in_folder(NOTE), PHONE); + join_sage(&mut mac.host, &github).unwrap(); + git(&github.bare, &["update-ref", "refs/heads/main", &base]); + let rewritten = in_folder("second-brain/notes/Rewritten.md"); + github.commit(&rewritten, PHONE); + mac.pull(&github); + let alerts = mac.alerts(); + assert_eq!(alerts.len(), 1, "{alerts:?}"); + assert_eq!( + alerts[0].path, rewritten, + "the dropped commit is not scanned" + ); + assert_eq!(mac.mark(SENSITIVE_INFO), head(&github)); + } + #[test] fn a_rewrite_restarts_at_the_merge_base() { let (_folder, github) = fixture(); diff --git a/crates/workspace-host/src/storage.rs b/crates/workspace-host/src/storage.rs index c5bec366..fdf0d79a 100644 --- a/crates/workspace-host/src/storage.rs +++ b/crates/workspace-host/src/storage.rs @@ -1639,6 +1639,11 @@ impl Host { if !clone.exists() { fs::create_dir_all(clone.parent().context("A clone is in a folder")?)?; fs::rename(staged.staging.keep(), &clone)?; + } else if joined_at.is_some() { + // A kept clone takes the head it joined at, where its pull scan starts. + let staging = (staged.staging.path().to_str()).context("A staging path is UTF-8")?; + let fetch = ["fetch", "--quiet", "--no-tags", "--recurse-submodules=no"]; + runtime::git_output(&clone, &[&fetch[..], &[staging, "HEAD"]].concat())?; } let head = runtime::git_output(&clone, &["rev-parse", "HEAD"])?; let head = head.trim();