diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-08-12 03:14:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-12 04:14:59 +0300 |
| commit | 5600197f00e871764465d4e3d9ba5e6aa6fd9547 (patch) | |
| tree | 26fd7ddb5f784f39aa4bec98735c316331cef211 /src/workspace/git/status.rs | |
| parent | 3f752a72fdd073d875cf941e3049f9860e30d841 (diff) | |
perf: avoid repeated Git config reads (#2688)
refs #2643
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/workspace/git/status.rs')
| -rw-r--r-- | src/workspace/git/status.rs | 139 |
1 files changed, 102 insertions, 37 deletions
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); |
