diff --git a/crates/ide-vcs/src/repo.rs b/crates/ide-vcs/src/repo.rs index 4f6cc2eff..9c99c5ee2 100644 --- a/crates/ide-vcs/src/repo.rs +++ b/crates/ide-vcs/src/repo.rs @@ -15,6 +15,11 @@ pub(crate) struct Cache { repo: gix::ThreadSafeRepository, files: [PathBuf; 4], stamp: Vec>, + /// `objects/pack` directory and its modification time. An external + /// `git fetch`/`pull`/`repack` adds packs without touching the `files` + /// above — watching the directory catches them (issue #1919). + pack_dir: PathBuf, + pack_stamp: Option, } fn cache_files(git_dir: &Path, common_dir: &Path) -> [PathBuf; 4] { @@ -36,6 +41,12 @@ fn stamp(files: &[PathBuf]) -> Vec> { .collect() } +/// The pack directory's modification time: adding or removing a pack file +/// updates it. `None` when the directory can't be read. +fn pack_stamp(pack_dir: &Path) -> Option { + std::fs::metadata(pack_dir).ok()?.modified().ok() +} + pub(crate) fn discover(path: &Path) -> IdeResult> { let start = if path.is_file() { path.parent().unwrap_or(path) } else { path }; if let Err(e) = gix::discover::upwards(start) { @@ -57,7 +68,8 @@ pub(crate) fn discover(path: &Path) -> IdeResult> { let git_dir = absolute(repo.git_dir())?; let common_dir = absolute(repo.common_dir())?; let files = cache_files(&git_dir, &common_dir); - let cache = Cache { stamp: stamp(&files), files, repo: ts }; + let pack_dir = common_dir.join("objects").join("pack"); + let cache = Cache { stamp: stamp(&files), files, repo: ts, pack_stamp: pack_stamp(&pack_dir), pack_dir }; Ok(Some(Repo { execution_allowed: std::sync::atomic::AtomicBool::new(true), root, @@ -96,11 +108,13 @@ impl Repo { } let mut cache = self.inner.lock().unwrap_or_else(|p| p.into_inner()); let now = stamp(&cache.files); - if now != cache.stamp { + let pack_now = pack_stamp(&cache.pack_dir); + if now != cache.stamp || pack_now != cache.pack_stamp { match gix::ThreadSafeRepository::open(&self.git_dir) { Ok(fresh) => { cache.repo = fresh; cache.stamp = now; + cache.pack_stamp = pack_now; } Err(e) => tracing::warn!("reopening repository failed: {e}"), } diff --git a/crates/ide-vcs/tests/repo.rs b/crates/ide-vcs/tests/repo.rs index ba3fe7fa8..c691f2ace 100644 --- a/crates/ide-vcs/tests/repo.rs +++ b/crates/ide-vcs/tests/repo.rs @@ -1366,6 +1366,30 @@ fn status_reflects_external_changes_without_reopen() { assert_eq!(repo.branch_info().unwrap().head.as_deref(), Some("x")); } +#[test] +fn branches_in_newly_written_packs_stay_visible() { + let fx = Fixture::new(); + fx.write("a", "1\n"); + fx.commit_all("a"); + let repo = fx.repo(); + assert_eq!(repo.branches().unwrap().len(), 1); + + // An external pull: objects move into a pack the IDE has never seen, then a + // new branch points at a commit living only in that pack. + fx.write("a", "2\n"); + fx.commit_all("b"); + fx.git(&["repack", "-adq"]); + fx.git(&["checkout", "-q", "-b", "uat"]); + let uat = fx.git(&["rev-parse", "uat"]); + assert!(std::fs::read_dir(fx.path(".git/objects/pack")).unwrap().count() >= 2); + + let names: Vec = repo.branches().unwrap().into_iter().map(|b| b.name).collect(); + assert!(names.contains(&"uat".to_string()), "branches: {names:?}, want uat at {uat}"); + + // Status peels HEAD for the tree-index diff: the same failure drops it too. + assert!(repo.status().is_ok(), "status failed: {:?}", repo.status().unwrap_err()); +} + #[test] fn linked_worktree_and_submodule() { let fx = Fixture::new();