aboutsummaryrefslogtreecommitdiff
path: root/src/workspace/git/status.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-25 04:26:55 +0300
committerGitHub <noreply@github.com>2026-07-25 04:26:55 +0300
commitd4e0dd3d903c50d2edb8c3cec71952a83989b310 (patch)
treec90ec43a08fab1fc79e1d6db865c723877a051af /src/workspace/git/status.rs
parentb33e5f973fc89381aa4ac72b3a31fa087ec66e23 (diff)
perf: eliminate repeated workspace git discovery (#1842)
* perf: eliminate repeated workspace git discovery refs #1838 * fix: keep workspace labels current without extra redraws refs #1838 * fix: retain discovered workspace git metadata refs #1838
Diffstat (limited to 'src/workspace/git/status.rs')
-rw-r--r--src/workspace/git/status.rs212
1 files changed, 192 insertions, 20 deletions
diff --git a/src/workspace/git/status.rs b/src/workspace/git/status.rs
index d9c3123a..fe873e29 100644
--- a/src/workspace/git/status.rs
+++ b/src/workspace/git/status.rs
@@ -1,19 +1,39 @@
use std::path::{Path, PathBuf};
+use std::time::{Duration, Instant};
-use crate::workspace::WorkspaceGitStatusSnapshot;
+use crate::workspace::{GitSpaceMetadata, WorkspaceGitStatusSnapshot};
use super::{
config::{read_branch_config, upstream_full_ref},
discovery::{
- canonicalize_best_effort_path, git_branch, git_ref_storage_is_reftable,
- git_rev_parse_verify, git_space_metadata, git_symbolic_head_full, git_worktree_info,
- read_ref_oid, GitWorktreeInfo,
+ canonicalize_best_effort_path, fallback_label_from_cwd, git_ref_storage_is_reftable,
+ git_rev_parse_verify, git_space_metadata_from_info, git_symbolic_head_full,
+ git_worktree_info, read_ref_oid, GitWorktreeInfo,
},
};
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub struct GitStatusRefreshDemand {
+ pub branch: bool,
+ pub ahead_behind: bool,
+}
+
+impl GitStatusRefreshDemand {
+ #[cfg(test)]
+ pub const ALL: Self = Self {
+ branch: true,
+ ahead_behind: true,
+ };
+
+ pub fn is_empty(self) -> bool {
+ !self.branch && !self.ahead_behind
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitStatusCacheEntry {
- pub fingerprint: GitStatusFingerprint,
+ pub fingerprint: Option<GitStatusFingerprint>,
+ pub retry_after: Option<Instant>,
pub snapshot: WorkspaceGitStatusSnapshot,
}
@@ -49,33 +69,97 @@ pub fn git_status_cache_key(cwd: &Path) -> Option<PathBuf> {
git_worktree_info(cwd).map(|info| canonicalize_best_effort_path(&info.repo_root))
}
+pub fn git_status_cache_key_for_space(space: &GitSpaceMetadata) -> PathBuf {
+ canonicalize_best_effort_path(&space.repo_root)
+}
+
+#[cfg(test)]
pub fn git_status_snapshot_for_cwd(
cwd: &Path,
cached: Option<&GitStatusCacheEntry>,
) -> (WorkspaceGitStatusSnapshot, Option<GitStatusCacheEntry>) {
- let space = git_space_metadata(cwd);
- let Some(fingerprint) = git_status_fingerprint(cwd) else {
+ git_status_snapshot_for_cwd_with_demand(cwd, cached, GitStatusRefreshDemand::ALL)
+}
+
+pub fn git_status_snapshot_for_cwd_with_demand(
+ cwd: &Path,
+ cached: Option<&GitStatusCacheEntry>,
+ demand: GitStatusRefreshDemand,
+) -> (WorkspaceGitStatusSnapshot, Option<GitStatusCacheEntry>) {
+ if let Some(cached) = cached.filter(|entry| {
+ entry.fingerprint.is_none()
+ && entry
+ .retry_after
+ .is_some_and(|retry_after| retry_after > Instant::now())
+ }) {
+ return (cached.snapshot.clone(), Some(cached.clone()));
+ }
+
+ let Some(info) = git_worktree_info(cwd) else {
+ let snapshot = WorkspaceGitStatusSnapshot {
+ auto_label: fallback_label_from_cwd(cwd),
+ branch: None,
+ ahead_behind: None,
+ space: None,
+ };
+ return (
+ snapshot.clone(),
+ Some(GitStatusCacheEntry {
+ fingerprint: None,
+ retry_after: Some(Instant::now() + Duration::from_secs(30)),
+ snapshot,
+ }),
+ );
+ };
+ let space = git_space_metadata_from_info(&info);
+ let auto_label = space.label.clone();
+
+ if !demand.ahead_behind {
+ 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();
return (
WorkspaceGitStatusSnapshot {
- branch: git_branch(cwd),
+ auto_label,
+ branch,
ahead_behind: None,
- space,
+ space: Some(space),
+ },
+ None,
+ );
+ }
+
+ let Some(fingerprint) = git_status_fingerprint_from_info(&info) else {
+ return (
+ WorkspaceGitStatusSnapshot {
+ auto_label,
+ branch: None,
+ ahead_behind: None,
+ space: Some(space),
},
None,
);
};
let branch = fingerprint.branch_name().map(str::to_string);
- if let Some(cached) = cached.filter(|entry| entry.fingerprint == fingerprint) {
+ if let Some(cached) = cached.filter(|entry| entry.fingerprint.as_ref() == Some(&fingerprint)) {
let snapshot = WorkspaceGitStatusSnapshot {
+ auto_label,
branch,
ahead_behind: cached.snapshot.ahead_behind,
- space,
+ space: Some(space),
};
return (
snapshot.clone(),
Some(GitStatusCacheEntry {
- fingerprint,
+ fingerprint: Some(fingerprint),
+ retry_after: None,
snapshot,
}),
);
@@ -86,24 +170,31 @@ pub fn git_status_snapshot_for_cwd(
.zip(fingerprint.upstream_oid())
.and_then(|(head_oid, upstream_oid)| git_ahead_behind_between(cwd, head_oid, upstream_oid));
let snapshot = WorkspaceGitStatusSnapshot {
+ auto_label,
branch,
ahead_behind,
- space,
+ space: Some(space),
};
(
snapshot.clone(),
Some(GitStatusCacheEntry {
- fingerprint,
+ fingerprint: Some(fingerprint),
+ retry_after: None,
snapshot,
}),
)
}
+#[cfg(test)]
pub(super) fn git_status_fingerprint(cwd: &Path) -> Option<GitStatusFingerprint> {
let info = git_worktree_info(cwd)?;
- let head = read_head_identity(&info)?;
+ git_status_fingerprint_from_info(&info)
+}
+
+fn git_status_fingerprint_from_info(info: &GitWorktreeInfo) -> Option<GitStatusFingerprint> {
+ let head = read_head_identity(info)?;
let upstream = match &head {
- GitHeadIdentity::Branch { short_name, .. } => read_upstream_identity(&info, short_name),
+ GitHeadIdentity::Branch { short_name, .. } => read_upstream_identity(info, short_name),
GitHeadIdentity::Detached { .. } => None,
};
@@ -243,7 +334,30 @@ fn parse_git_ahead_behind_output(stdout: &str) -> Option<(usize, usize)> {
#[cfg(test)]
mod tests {
use super::*;
- use crate::workspace::git::test_support::{run_git, temp_test_dir, write_fake_tracked_repo};
+ use crate::workspace::git::{
+ git_space_metadata,
+ test_support::{run_git, temp_test_dir, write_fake_tracked_repo},
+ };
+
+ #[cfg(target_os = "linux")]
+ #[test]
+ fn cache_key_from_space_preserves_non_utf8_checkout_path() {
+ use std::os::unix::ffi::OsStringExt;
+
+ let base = temp_test_dir("non-utf8-key");
+ let root = base.join(std::ffi::OsString::from_vec(vec![
+ b'r', b'e', b'p', b'o', 0x80,
+ ]));
+ write_fake_tracked_repo(&root);
+ let space = git_space_metadata(&root).expect("Git metadata");
+
+ assert_eq!(
+ git_status_cache_key_for_space(&space),
+ std::fs::canonicalize(&root).unwrap()
+ );
+
+ std::fs::remove_dir_all(base).unwrap();
+ }
#[test]
fn git_status_cache_key_ignores_invalid_git_marker() {
@@ -258,13 +372,67 @@ mod tests {
}
#[test]
+ fn non_git_refresh_reuses_cached_miss_without_rechecking_filesystem() {
+ let root = temp_test_dir("cached-miss");
+ let cwd = root.join("deep/nested");
+ std::fs::create_dir_all(&cwd).unwrap();
+
+ let (initial, cache_entry) = git_status_snapshot_for_cwd(&cwd, None);
+ let cache_entry = cache_entry.expect("non-Git result should be cached");
+ std::fs::remove_dir_all(&root).unwrap();
+
+ let (cached, update) = git_status_snapshot_for_cwd(&cwd, Some(&cache_entry));
+
+ assert_eq!(cached, initial);
+ assert_eq!(update, Some(cache_entry));
+ }
+
+ #[test]
+ fn expired_non_git_cache_detects_repository_created_in_place() {
+ let root = temp_test_dir("expired-miss");
+ let (_, cache_entry) = git_status_snapshot_for_cwd(&root, None);
+ let mut cache_entry = cache_entry.expect("non-Git result should be cached");
+ cache_entry.retry_after = Some(Instant::now() - Duration::from_secs(1));
+ write_fake_tracked_repo(&root);
+
+ let (snapshot, update) = git_status_snapshot_for_cwd(&root, Some(&cache_entry));
+
+ assert_eq!(snapshot.branch.as_deref(), Some("main"));
+ assert!(update.is_some_and(|entry| entry.fingerprint.is_some()));
+ 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);
+
+ let (snapshot, update) = git_status_snapshot_for_cwd_with_demand(
+ &root,
+ None,
+ GitStatusRefreshDemand {
+ branch: true,
+ ahead_behind: false,
+ },
+ );
+
+ assert_eq!(snapshot.branch.as_deref(), Some("main"));
+ assert_eq!(snapshot.ahead_behind, None);
+ assert_eq!(update, None);
+
+ std::fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn git_status_reuses_cached_ahead_behind_when_fingerprint_matches() {
let root = temp_test_dir("cache-hit");
write_fake_tracked_repo(&root);
let fingerprint = git_status_fingerprint(&root).unwrap();
let cached = GitStatusCacheEntry {
- fingerprint,
+ fingerprint: Some(fingerprint),
+ retry_after: None,
snapshot: WorkspaceGitStatusSnapshot {
+ auto_label: "repo".into(),
branch: Some("main".into()),
ahead_behind: Some((2, 1)),
space: git_space_metadata(&root),
@@ -286,8 +454,10 @@ mod tests {
write_fake_tracked_repo(&root);
let fingerprint = git_status_fingerprint(&root).unwrap();
let cached = GitStatusCacheEntry {
- fingerprint,
+ fingerprint: Some(fingerprint),
+ retry_after: None,
snapshot: WorkspaceGitStatusSnapshot {
+ auto_label: "repo".into(),
branch: Some("main".into()),
ahead_behind: Some((4, 0)),
space: git_space_metadata(&root),
@@ -319,8 +489,10 @@ mod tests {
write_fake_tracked_repo(&root);
let fingerprint = git_status_fingerprint(&root).unwrap();
let cached = GitStatusCacheEntry {
- fingerprint,
+ fingerprint: Some(fingerprint),
+ retry_after: None,
snapshot: WorkspaceGitStatusSnapshot {
+ auto_label: "repo".into(),
branch: Some("main".into()),
ahead_behind: Some((0, 3)),
space: git_space_metadata(&root),