fix(vcs): reopen the repository when the pack directory changes (#1920)

Repo::gix() reopened the cached gitoxide handle only when config or packed-refs changed. An external git fetch/pull/repack writes new packs under objects/pack and moves refs, but never touches those files — so the ODB kept serving the snapshot taken at open, and refs whose tip lived only in the new pack failed to peel: branches vanished and status warned "Could not peel reference". Watch the pack directory's mtime too and reopen when it changes, so externally added objects become visible (issue #1919).

Fixes #1919
This commit is contained in:
arookieofc authored and GitHub committed 2026-11-07 13:32:05 +08:00
1 parent 8ca6e4bc01
commit 6efff739dd
2 files changed
+40 -2

No files matched your search

+16 -2
View File
@@ -15,6 +15,11 @@ pub(crate) struct Cache {
repo: gix::ThreadSafeRepository,
files: [PathBuf; 4],
stamp: Vec<Option<(SystemTime, u64)>>,
/// `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<SystemTime>,
}
fn cache_files(git_dir: &Path, common_dir: &Path) -> [PathBuf; 4] {
@@ -36,6 +41,12 @@ fn stamp(files: &[PathBuf]) -> Vec<Option<(SystemTime, u64)>> {
.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<SystemTime> {
std::fs::metadata(pack_dir).ok()?.modified().ok()
}
pub(crate) fn discover(path: &Path) -> IdeResult<Option<Repo>> {
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<Option<Repo>> {
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}"),
}
+24
View File
@@ -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<String> = 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();