aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-08-12 03:14:59 +0200
committerGitHub <noreply@github.com>2026-08-12 04:14:59 +0300
commit5600197f00e871764465d4e3d9ba5e6aa6fd9547 (patch)
tree26fd7ddb5f784f39aa4bec98735c316331cef211 /src
parent3f752a72fdd073d875cf941e3049f9860e30d841 (diff)
perf: avoid repeated Git config reads (#2688)
refs #2643 Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/app/git_refresh.rs18
-rw-r--r--src/workspace/git/config.rs117
-rw-r--r--src/workspace/git/config_tests.rs55
-rw-r--r--src/workspace/git/status.rs139
4 files changed, 270 insertions, 59 deletions
diff --git a/src/app/git_refresh.rs b/src/app/git_refresh.rs
index a868a0ed..348b1556 100644
--- a/src/app/git_refresh.rs
+++ b/src/app/git_refresh.rs
@@ -141,6 +141,7 @@ fn deduplicate_git_refresh_items(
let mut jobs = Vec::<WorkspaceGitRefreshJob>::new();
for item in items {
+ let reconcile = item.cache_key_hint.is_none();
let cache_key = item.cache_key_hint.unwrap_or_else(|| {
crate::workspace::git_status_cache_key(&item.resolved_identity_cwd)
.unwrap_or_else(|| item.resolved_identity_cwd.clone())
@@ -150,11 +151,12 @@ fn deduplicate_git_refresh_items(
resolved_identity_cwd: item.resolved_identity_cwd,
};
if let Some(&index) = indexes.get(&cache_key) {
+ jobs[index].cached = jobs[index].cached.take().filter(|_| !reconcile);
jobs[index].targets.push(target);
continue;
}
- let cached = cache.get(&cache_key).cloned();
+ let cached = cache.get(&cache_key).filter(|_| !reconcile).cloned();
indexes.insert(cache_key.clone(), jobs.len());
jobs.push(WorkspaceGitRefreshJob {
cache_key,
@@ -299,6 +301,20 @@ mod tests {
assert_eq!(items.len(), 1);
assert_eq!(items[0].cache_key_hint, None);
+ let cache_key = items[0].resolved_identity_cwd.clone();
+ let cached = GitStatusCacheEntry {
+ fingerprint: None,
+ retry_after: None,
+ snapshot: crate::workspace::WorkspaceGitStatusSnapshot {
+ auto_label: "stale".into(),
+ branch: None,
+ ahead_behind: None,
+ space: None,
+ },
+ };
+ let jobs = deduplicate_git_refresh_items(items, &HashMap::from([(cache_key, cached)]));
+ assert_eq!(jobs.len(), 1);
+ assert_eq!(jobs[0].cached, None);
}
#[test]
diff --git a/src/workspace/git/config.rs b/src/workspace/git/config.rs
index 9783387c..9e386096 100644
--- a/src/workspace/git/config.rs
+++ b/src/workspace/git/config.rs
@@ -1,4 +1,7 @@
+use std::collections::HashMap;
+use std::io::ErrorKind;
use std::path::{Path, PathBuf};
+use std::time::SystemTime;
use super::discovery::{canonicalize_best_effort_path, GitWorktreeInfo};
@@ -10,17 +13,68 @@ pub(super) struct BranchConfig {
remote_urls: Vec<(String, String)>,
}
-pub(super) fn read_branch_config(info: &GitWorktreeInfo, branch: &str) -> Option<BranchConfig> {
- read_branch_config_with_user_paths(info, branch, git_user_config_paths())
+type FileStamp = Option<(Option<SystemTime>, u64)>;
+pub(super) type FileDep = (PathBuf, FileStamp, bool, Option<PathBuf>);
+
+pub(super) fn stamp(path: PathBuf, target: Option<PathBuf>) -> FileDep {
+ let meta = std::fs::metadata(&path);
+ let reusable = meta.is_ok() || matches!(&meta, Err(e) if e.kind() == ErrorKind::NotFound);
+ let stamp = meta.ok().map(|m| (m.modified().ok(), m.len()));
+ (path, stamp, reusable, target)
+}
+
+pub(super) fn deps_current(deps: &[FileDep]) -> bool {
+ deps.iter().all(|dep| {
+ let target = dep
+ .3
+ .as_ref()
+ .map(|_| canonicalize_best_effort_path(&dep.0));
+ dep.2 && stamp(dep.0.clone(), target) == *dep
+ })
+}
+
+pub(super) type ConfigCtx = (String, Option<BranchConfig>, Vec<FileDep>);
+
+#[derive(Default)]
+struct ConfigReader {
+ files: HashMap<PathBuf, Option<String>>,
+ deps: Vec<FileDep>,
+}
+
+impl ConfigReader {
+ fn read(&mut self, path: &Path) -> (PathBuf, Option<String>) {
+ let logical = path.to_path_buf();
+ let path = canonicalize_best_effort_path(path);
+ self.deps
+ .extend((logical != path).then(|| stamp(logical, Some(path.clone()))));
+ let deps = &mut self.deps;
+ let contents = self
+ .files
+ .entry(path.clone())
+ .or_insert_with(|| {
+ let mut dep = stamp(path.clone(), None);
+ let r = std::fs::read_to_string(&path);
+ dep.2 &= r.is_ok() || matches!(&r, Err(e) if e.kind() == ErrorKind::NotFound);
+ deps.push(dep);
+ r.ok()
+ })
+ .clone();
+ (path, contents)
+ }
+}
+
+pub(super) fn read_config(info: &GitWorktreeInfo, branch: &str) -> ConfigCtx {
+ read_config_with_user_paths(info, branch, git_user_config_paths())
}
-pub(super) fn read_branch_config_with_user_paths(
+pub(super) fn read_config_with_user_paths(
info: &GitWorktreeInfo,
branch: &str,
user_config_paths: Vec<PathBuf>,
-) -> Option<BranchConfig> {
+) -> ConfigCtx {
+ let mut reader = ConfigReader::default();
let worktree_config_enabled =
- worktree_config_enabled(&info.git_common_dir.join("config"), info);
+ worktree_config_enabled(&info.git_common_dir.join("config"), info, &mut reader);
let config_paths = user_config_paths
.into_iter()
.chain(std::iter::once(info.git_common_dir.join("config")))
@@ -28,7 +82,14 @@ pub(super) fn read_branch_config_with_user_paths(
let mut remote_urls = Vec::new();
for path in &config_paths {
let mut include_stack = Vec::new();
- collect_remote_urls(path, info, branch, &mut remote_urls, &mut include_stack);
+ collect_remote_urls(
+ path,
+ info,
+ branch,
+ &mut remote_urls,
+ &mut include_stack,
+ &mut reader,
+ );
}
let mut config = BranchConfig {
remote: String::new(),
@@ -38,7 +99,15 @@ pub(super) fn read_branch_config_with_user_paths(
};
for path in config_paths {
let mut include_stack = Vec::new();
- merge_git_config(&mut config, &path, branch, info, true, &mut include_stack);
+ merge_git_config(
+ &mut config,
+ &path,
+ branch,
+ info,
+ true,
+ &mut include_stack,
+ &mut reader,
+ );
}
if worktree_config_enabled {
let mut include_stack = Vec::new();
@@ -49,9 +118,14 @@ pub(super) fn read_branch_config_with_user_paths(
info,
false,
&mut include_stack,
+ &mut reader,
);
}
- (!config.remote.is_empty() && !config.merge_ref.is_empty()).then_some(config)
+ (
+ branch.to_string(),
+ (!config.remote.is_empty() && !config.merge_ref.is_empty()).then_some(config),
+ reader.deps,
+ )
}
fn git_user_config_paths() -> Vec<PathBuf> {
@@ -67,8 +141,8 @@ fn git_user_config_paths() -> Vec<PathBuf> {
paths
}
-fn worktree_config_enabled(path: &Path, info: &GitWorktreeInfo) -> bool {
- let Ok(contents) = std::fs::read_to_string(path) else {
+fn worktree_config_enabled(path: &Path, info: &GitWorktreeInfo, reader: &mut ConfigReader) -> bool {
+ let Some(contents) = reader.read(path).1 else {
return false;
};
let mut section = ConfigSection::Other;
@@ -124,13 +198,14 @@ fn collect_remote_urls(
branch: &str,
remote_urls: &mut Vec<(String, String)>,
include_stack: &mut Vec<PathBuf>,
+ reader: &mut ConfigReader,
) {
- let path = canonicalize_best_effort_path(path);
+ let (path, contents) = reader.read(path);
if include_stack.contains(&path) {
return;
}
include_stack.push(path.clone());
- let Ok(contents) = std::fs::read_to_string(&path) else {
+ let Some(contents) = contents else {
include_stack.pop();
return;
};
@@ -163,6 +238,7 @@ fn collect_remote_urls(
branch,
remote_urls,
include_stack,
+ reader,
);
}
ConfigSection::IncludeIf(IncludeIfMode::Enabled)
@@ -174,6 +250,7 @@ fn collect_remote_urls(
branch,
remote_urls,
include_stack,
+ reader,
);
}
_ => {}
@@ -189,13 +266,14 @@ fn merge_git_config(
info: &GitWorktreeInfo,
collect_hasconfig_urls: bool,
include_stack: &mut Vec<PathBuf>,
+ reader: &mut ConfigReader,
) {
- let path = canonicalize_best_effort_path(path);
+ let (path, contents) = reader.read(path);
if include_stack.contains(&path) {
return;
}
include_stack.push(path.clone());
- let Ok(contents) = std::fs::read_to_string(&path) else {
+ let Some(contents) = contents else {
include_stack.pop();
return;
};
@@ -232,6 +310,7 @@ fn merge_git_config(
info,
collect_hasconfig_urls,
include_stack,
+ reader,
);
}
ConfigSection::IncludeIf(IncludeIfMode::Enabled)
@@ -245,6 +324,7 @@ fn merge_git_config(
info,
collect_hasconfig_urls,
include_stack,
+ reader,
);
}
ConfigSection::IncludeIf(IncludeIfMode::HasConfig)
@@ -257,6 +337,7 @@ fn merge_git_config(
info,
config,
include_stack,
+ reader,
) {
merge_git_config(
config,
@@ -265,6 +346,7 @@ fn merge_git_config(
info,
collect_hasconfig_urls,
include_stack,
+ reader,
);
}
}
@@ -402,13 +484,14 @@ fn included_config_defines_remote_url(
info: &GitWorktreeInfo,
config: &BranchConfig,
include_stack: &mut Vec<PathBuf>,
+ reader: &mut ConfigReader,
) -> bool {
- let path = canonicalize_best_effort_path(path);
+ let (path, contents) = reader.read(path);
if include_stack.contains(&path) {
return false;
}
include_stack.push(path.clone());
- let Ok(contents) = std::fs::read_to_string(&path) else {
+ let Some(contents) = contents else {
include_stack.pop();
return false;
};
@@ -438,6 +521,7 @@ fn included_config_defines_remote_url(
info,
config,
include_stack,
+ reader,
) {
continue;
}
@@ -454,6 +538,7 @@ fn included_config_defines_remote_url(
info,
config,
include_stack,
+ reader,
) {
continue;
}
diff --git a/src/workspace/git/config_tests.rs b/src/workspace/git/config_tests.rs
index e789e1db..d098f664 100644
--- a/src/workspace/git/config_tests.rs
+++ b/src/workspace/git/config_tests.rs
@@ -5,6 +5,41 @@ use crate::workspace::git::{
test_support::{temp_test_dir, write_fake_tracked_repo},
};
+#[cfg(unix)]
+#[test]
+fn config_symlink_retarget_invalidates_context() {
+ use std::os::unix::fs::symlink;
+
+ let root = temp_test_dir("config-symlink-retarget");
+ write_fake_tracked_repo(&root);
+ let alias = root.join("branch.cfg");
+ let first = root.join("first.cfg");
+ let second = root.join("second.cfg");
+ std::fs::write(&first, "").unwrap();
+ std::fs::write(&second, "").unwrap();
+ symlink(&first, &alias).unwrap();
+ let context = read_config_with_user_paths(
+ &git_worktree_info(&root).unwrap(),
+ "main",
+ vec![alias.clone()],
+ );
+ std::fs::remove_file(&alias).unwrap();
+ symlink(&second, &alias).unwrap();
+
+ assert!(!deps_current(&context.2));
+ std::fs::remove_dir_all(root).unwrap();
+}
+
+#[test]
+fn config_read_error_retries_next_refresh() {
+ let root = temp_test_dir("config-read-error");
+ write_fake_tracked_repo(&root);
+ std::fs::write(root.join(".git/config"), [0xff]).unwrap();
+ let context = read_config(&git_worktree_info(&root).unwrap(), "main");
+ assert!(!deps_current(&context.2));
+ std::fs::remove_dir_all(root).unwrap();
+}
+
#[test]
fn git_status_fingerprint_honors_remote_fetch_refspec() {
let root = temp_test_dir("custom-fetch-refspec");
@@ -74,7 +109,9 @@ fn git_status_branch_config_reads_user_config_before_repo_config() {
.unwrap();
let info = git_worktree_info(&root).unwrap();
- let config = read_branch_config_with_user_paths(&info, "main", vec![user_config]).unwrap();
+ let config = read_config_with_user_paths(&info, "main", vec![user_config])
+ .1
+ .unwrap();
assert_eq!(config.remote, "global");
assert_eq!(
@@ -97,7 +134,9 @@ fn git_status_branch_config_repo_config_overrides_user_config() {
.unwrap();
let info = git_worktree_info(&root).unwrap();
- let config = read_branch_config_with_user_paths(&info, "main", vec![user_config]).unwrap();
+ let config = read_config_with_user_paths(&info, "main", vec![user_config])
+ .1
+ .unwrap();
assert_eq!(config.remote, "origin");
assert_eq!(
@@ -475,7 +514,9 @@ fn git_status_fingerprint_matches_user_hasconfig_against_repo_remote_url() {
.unwrap();
let info = git_worktree_info(&root).unwrap();
- let config = read_branch_config_with_user_paths(&info, "main", vec![user_config]).unwrap();
+ let config = read_config_with_user_paths(&info, "main", vec![user_config])
+ .1
+ .unwrap();
assert_eq!(config.remote, "included");
assert_eq!(config.merge_ref, "refs/heads/main");
@@ -511,7 +552,9 @@ fn git_status_fingerprint_skips_hasconfig_include_that_defines_remote_url() {
.unwrap();
let info = git_worktree_info(&root).unwrap();
- let config = read_branch_config_with_user_paths(&info, "main", vec![user_config]).unwrap();
+ let config = read_config_with_user_paths(&info, "main", vec![user_config])
+ .1
+ .unwrap();
assert_eq!(config.remote, "origin");
assert_eq!(config.merge_ref, "refs/heads/main");
@@ -552,7 +595,9 @@ fn git_status_fingerprint_skips_hasconfig_include_chain_that_defines_remote_url(
.unwrap();
let info = git_worktree_info(&root).unwrap();
- let config = read_branch_config_with_user_paths(&info, "main", vec![user_config]).unwrap();
+ let config = read_config_with_user_paths(&info, "main", vec![user_config])
+ .1
+ .unwrap();
assert_eq!(config.remote, "origin");
assert_eq!(config.merge_ref, "refs/heads/main");
diff --git a/src/workspace/git/status.rs b/src/workspace/git/status.rs
index dcfc1aff..4fb2b887 100644
--- a/src/workspace/git/status.rs
+++ b/src/workspace/git/status.rs
@@ -4,7 +4,7 @@ use std::time::{Duration, Instant};
use crate::workspace::{GitSpaceMetadata, WorkspaceGitStatusSnapshot};
use super::{
- config::{read_branch_config, upstream_full_ref},
+ config::{deps_current, read_config, stamp, upstream_full_ref, ConfigCtx, FileDep},
discovery::{
automatic_workspace_label, canonicalize_best_effort_path, fallback_label_from_cwd,
git_ref_storage_is_reftable, git_rev_parse_verify, git_space_metadata_from_info,
@@ -39,10 +39,25 @@ pub struct GitStatusCacheEntry {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitStatusFingerprint {
- pub git_dir: PathBuf,
- pub git_common_dir: PathBuf,
pub head: GitHeadIdentity,
pub upstream: Option<GitUpstreamIdentity>,
+ repository_context: RepoContext,
+}
+
+type RepoContext = (GitWorktreeInfo, bool, Vec<FileDep>, Option<ConfigCtx>);
+
+fn repo_context(cwd: &Path) -> Option<RepoContext> {
+ let info = git_worktree_info(cwd)?;
+ let reftable = git_ref_storage_is_reftable(&info.git_common_dir);
+ let mut paths = vec![info.repo_root.join(".git"), info.git_dir.join("commondir")];
+ paths.push(info.git_dir.join("HEAD"));
+ paths.push(info.git_common_dir.join("config"));
+ paths.extend((info.git_dir != info.git_common_dir).then(|| info.git_dir.join("config")));
+ let mut deps: Vec<_> = paths.into_iter().map(|path| stamp(path, None)).collect();
+ deps[0].2 &= git_worktree_info(cwd).as_ref() == Some(&info)
+ && git_ref_storage_is_reftable(&info.git_common_dir) == reftable
+ && deps_current(&deps);
+ Some((info, reftable, deps, None))
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -95,7 +110,12 @@ pub fn git_status_snapshot_for_cwd_with_demand(
return (cached.snapshot.clone(), Some(cached.clone()));
}
- let Some(info) = git_worktree_info(cwd) else {
+ let repository_context = cached
+ .and_then(|entry| entry.fingerprint.as_ref())
+ .map(|fingerprint| fingerprint.repository_context.clone())
+ .filter(|context| deps_current(&context.2))
+ .or_else(|| repo_context(cwd));
+ let Some(repository_context) = repository_context else {
let snapshot = WorkspaceGitStatusSnapshot {
auto_label: fallback_label_from_cwd(cwd),
branch: None,
@@ -111,31 +131,33 @@ pub fn git_status_snapshot_for_cwd_with_demand(
}),
);
};
- let auto_label = automatic_workspace_label(cwd, &info.repo_root);
- let space = git_space_metadata_from_info(&info);
+ let auto_label = automatic_workspace_label(cwd, &repository_context.0.repo_root);
+ let space = git_space_metadata_from_info(&repository_context.0);
if !demand.ahead_behind {
+ let fingerprint = fingerprint(repository_context, false);
let branch = demand
.branch
- .then(|| {
- read_head_identity(&info).and_then(|head| match head {
- GitHeadIdentity::Branch { short_name, .. } => Some(short_name),
- GitHeadIdentity::Detached { .. } => None,
- })
- })
- .flatten();
+ .then(|| fingerprint.as_ref()?.branch_name())
+ .flatten()
+ .map(str::to_string);
+ let snapshot = WorkspaceGitStatusSnapshot {
+ auto_label,
+ branch,
+ ahead_behind: None,
+ space: Some(space),
+ };
return (
- WorkspaceGitStatusSnapshot {
- auto_label,
- branch,
- ahead_behind: None,
- space: Some(space),
- },
- None,
+ snapshot.clone(),
+ fingerprint.map(|fingerprint| GitStatusCacheEntry {
+ fingerprint: Some(fingerprint),
+ retry_after: None,
+ snapshot,
+ }),
);
}
- let Some(fingerprint) = git_status_fingerprint_from_info(&info) else {
+ let Some(fingerprint) = fingerprint(repository_context, true) else {
return (
WorkspaceGitStatusSnapshot {
auto_label,
@@ -187,22 +209,22 @@ pub fn git_status_snapshot_for_cwd_with_demand(
#[cfg(test)]
pub(super) fn git_status_fingerprint(cwd: &Path) -> Option<GitStatusFingerprint> {
- let info = git_worktree_info(cwd)?;
- git_status_fingerprint_from_info(&info)
+ fingerprint(repo_context(cwd)?, true)
}
-fn git_status_fingerprint_from_info(info: &GitWorktreeInfo) -> Option<GitStatusFingerprint> {
- let head = read_head_identity(info)?;
+fn fingerprint(mut repo: RepoContext, include_upstream: bool) -> Option<GitStatusFingerprint> {
+ let head = read_head_identity(&repo.0, repo.1)?;
let upstream = match &head {
- GitHeadIdentity::Branch { short_name, .. } => read_upstream_identity(info, short_name),
- GitHeadIdentity::Detached { .. } => None,
+ GitHeadIdentity::Branch { short_name, .. } if include_upstream => {
+ read_upstream(&mut repo, short_name)
+ }
+ _ => None,
};
Some(GitStatusFingerprint {
- git_dir: canonicalize_best_effort_path(&info.git_dir),
- git_common_dir: canonicalize_best_effort_path(&info.git_common_dir),
head,
upstream,
+ repository_context: repo,
})
}
@@ -228,8 +250,8 @@ impl GitStatusFingerprint {
}
}
-fn read_head_identity(info: &GitWorktreeInfo) -> Option<GitHeadIdentity> {
- if git_ref_storage_is_reftable(&info.git_common_dir) {
+fn read_head_identity(info: &GitWorktreeInfo, reftable: bool) -> Option<GitHeadIdentity> {
+ if reftable {
return read_head_identity_from_git(info);
}
@@ -268,13 +290,20 @@ fn read_head_identity_from_files(info: &GitWorktreeInfo) -> Option<GitHeadIdenti
})
}
-fn read_upstream_identity(info: &GitWorktreeInfo, branch: &str) -> Option<GitUpstreamIdentity> {
- let config = read_branch_config(info, branch)?;
+fn read_upstream(repo: &mut RepoContext, branch: &str) -> Option<GitUpstreamIdentity> {
+ if repo
+ .3
+ .as_ref()
+ .is_none_or(|context| context.0 != branch || !deps_current(&context.2))
+ {
+ repo.3 = Some(read_config(&repo.0, branch));
+ }
+ let config = repo.3.as_ref()?.1.clone()?;
let full_ref = upstream_full_ref(&config)?;
- let oid = if git_ref_storage_is_reftable(&info.git_common_dir) {
- git_rev_parse_verify(&info.repo_root, &full_ref)
+ let oid = if repo.1 {
+ git_rev_parse_verify(&repo.0.repo_root, &full_ref)
} else {
- read_ref_oid(&info.git_common_dir, &full_ref)
+ read_ref_oid(&repo.0.git_common_dir, &full_ref)
};
Some(GitUpstreamIdentity {
remote: config.remote,
@@ -403,6 +432,19 @@ mod tests {
}
#[test]
+ fn cached_repo_identity_clears_when_head_disappears() {
+ let root = temp_test_dir("missing-head");
+ write_fake_tracked_repo(&root);
+ let (_, cached) = git_status_snapshot_for_cwd(&root, None);
+ std::fs::remove_file(root.join(".git/HEAD")).unwrap();
+
+ let (snapshot, _) = git_status_snapshot_for_cwd(&root, cached.as_ref());
+
+ assert_eq!(snapshot.space, None);
+ std::fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn branch_only_refresh_skips_ahead_behind_cache_work() {
let root = temp_test_dir("branch-only");
write_fake_tracked_repo(&root);
@@ -418,7 +460,7 @@ mod tests {
assert_eq!(snapshot.branch.as_deref(), Some("main"));
assert_eq!(snapshot.ahead_behind, None);
- assert_eq!(update, None);
+ assert!(update.is_some_and(|entry| entry.fingerprint.is_some()));
std::fs::remove_dir_all(root).unwrap();
}
@@ -509,6 +551,29 @@ mod tests {
}
#[test]
+ fn git_status_rebuilds_config_when_missing_include_appears() {
+ let root = temp_test_dir("include-appears");
+ write_fake_tracked_repo(&root);
+ std::fs::write(
+ root.join(".git/config"),
+ "[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n[include]\n\tpath = branch.cfg\n",
+ )
+ .unwrap();
+ let (_, cached) = git_status_snapshot_for_cwd(&root, None);
+ std::fs::write(
+ root.join(".git/branch.cfg"),
+ "[branch \"main\"]\n\tremote = fork\n\tmerge = refs/heads/main\n",
+ )
+ .unwrap();
+
+ let (_, updated) = git_status_snapshot_for_cwd(&root, cached.as_ref());
+
+ let upstream = updated.unwrap().fingerprint.unwrap().upstream.unwrap();
+ assert_eq!(upstream.remote, "fork");
+ std::fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn git_status_fingerprint_reads_packed_refs() {
let root = temp_test_dir("packed-refs");
write_fake_tracked_repo(&root);