diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-07-25 04:26:55 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-25 04:26:55 +0300 |
| commit | d4e0dd3d903c50d2edb8c3cec71952a83989b310 (patch) | |
| tree | c90ec43a08fab1fc79e1d6db865c723877a051af /src/workspace.rs | |
| parent | b33e5f973fc89381aa4ac72b3a31fa087ec66e23 (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.rs')
| -rw-r--r-- | src/workspace.rs | 181 |
1 files changed, 165 insertions, 16 deletions
diff --git a/src/workspace.rs b/src/workspace.rs index e90bd359..73a46d25 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -20,11 +20,12 @@ mod tab; #[cfg(test)] use self::git::git_ahead_behind; -pub(crate) use self::tab::MovedPane; +use self::git::git_status_cache_key_for_space; +pub(crate) use self::{git::git_status_snapshot_for_cwd_with_demand, tab::MovedPane}; pub use self::{ git::{ - derive_label_from_cwd, git_branch, git_space_metadata, git_status_cache_key, - GitSpaceMetadata, GitStatusCacheEntry, + derive_label_from_cwd, fallback_label_from_cwd, git_branch, git_space_metadata, + git_status_cache_key, GitSpaceMetadata, GitStatusCacheEntry, GitStatusRefreshDemand, }, tab::{NewPane, Tab}, }; @@ -42,6 +43,9 @@ pub struct WorktreeSpaceMembership { pub struct WorkspaceGitStatus { pub workspace_id: String, pub resolved_identity_cwd: PathBuf, + pub status_cache_key: PathBuf, + pub demand: GitStatusRefreshDemand, + pub auto_label: String, pub branch: Option<String>, pub ahead_behind: Option<(usize, usize)>, pub space: Option<GitSpaceMetadata>, @@ -49,20 +53,41 @@ pub struct WorkspaceGitStatus { #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceGitStatusSnapshot { + pub auto_label: String, pub branch: Option<String>, pub ahead_behind: Option<(usize, usize)>, pub space: Option<GitSpaceMetadata>, } +pub(crate) fn discover_workspace_git_identity( + cwd: &std::path::Path, +) -> (Option<GitSpaceMetadata>, String, PathBuf) { + let space = git_space_metadata(cwd); + let auto_label = space + .as_ref() + .map(|space| space.label.clone()) + .unwrap_or_else(|| fallback_label_from_cwd(cwd)); + let status_cache_key = space + .as_ref() + .map(git_status_cache_key_for_space) + .unwrap_or_else(|| cwd.to_path_buf()); + (space, auto_label, status_cache_key) +} + impl WorkspaceGitStatusSnapshot { pub fn into_workspace_status( self, workspace_id: String, resolved_identity_cwd: PathBuf, + status_cache_key: PathBuf, + demand: GitStatusRefreshDemand, ) -> WorkspaceGitStatus { WorkspaceGitStatus { workspace_id, resolved_identity_cwd, + status_cache_key, + demand, + auto_label: self.auto_label, branch: self.branch, ahead_behind: self.ahead_behind, space: self.space, @@ -149,6 +174,12 @@ pub struct Workspace { pub custom_name: Option<String>, /// Fallback workspace identity source for tests, old snapshots, or missing runtimes. pub identity_cwd: PathBuf, + /// CWD from which the cached automatic label and Git metadata were derived. + pub(crate) cached_identity_cwd: PathBuf, + /// Automatic workspace label cached outside the render path. + pub(crate) cached_auto_label: String, + /// Cache key for periodic Git status associated with `cached_identity_cwd`. + pub(crate) cached_git_status_key: PathBuf, /// Cached current git branch for the workspace repo. pub(crate) cached_git_branch: Option<String>, /// Cached ahead/behind counts for the workspace repo's current branch upstream. @@ -210,13 +241,18 @@ impl Workspace { let tab = Tab::from_existing_pane(1, tab_label, moved, events, render_notify, render_dirty); let mut public_pane_numbers = HashMap::new(); public_pane_numbers.insert(root_pane, 1); + let (cached_git_space, cached_auto_label, cached_git_status_key) = + discover_workspace_git_identity(&identity_cwd); Self { id, custom_name: label, identity_cwd: identity_cwd.clone(), + cached_identity_cwd: identity_cwd.clone(), + cached_auto_label, + cached_git_status_key, cached_git_branch: git_branch(&identity_cwd), cached_git_ahead_behind: None, - cached_git_space: git_space_metadata(&identity_cwd), + cached_git_space, worktree_space: None, metadata_tokens: crate::metadata_tokens::MetadataTokens::default(), metadata_token_sequences: HashMap::new(), @@ -392,14 +428,19 @@ impl Workspace { }; let mut public_pane_numbers = HashMap::new(); public_pane_numbers.insert(tab.root_pane, 1); + let (cached_git_space, cached_auto_label, cached_git_status_key) = + discover_workspace_git_identity(&initial_cwd); Ok(( Self { id, custom_name: None, identity_cwd: initial_cwd.clone(), + cached_identity_cwd: initial_cwd.clone(), + cached_auto_label, + cached_git_status_key, cached_git_branch: git_branch(&initial_cwd), cached_git_ahead_behind: None, - cached_git_space: None, + cached_git_space, worktree_space: None, metadata_tokens: crate::metadata_tokens::MetadataTokens::default(), metadata_token_sequences: HashMap::new(), @@ -1036,6 +1077,7 @@ impl Workspace { self.custom_name = Some(name); } + #[cfg(test)] pub fn resolved_identity_cwd(&self) -> Option<PathBuf> { Some(self.identity_cwd.clone()) } @@ -1051,14 +1093,31 @@ impl Workspace { .or_else(|| Some(self.identity_cwd.clone())) } + #[cfg(test)] pub fn display_name(&self) -> String { if let Some(name) = &self.custom_name { return name.clone(); } - self.resolved_identity_cwd() - .map(|cwd| derive_label_from_cwd(&cwd)) - .unwrap_or_else(|| "workspace".into()) + self.automatic_display_name_for_cwd(&self.identity_cwd) + } + + pub(crate) fn display_name_from_terminals( + &self, + terminals: &HashMap<TerminalId, TerminalState>, + ) -> String { + if let Some(name) = &self.custom_name { + return name.clone(); + } + + let cwd = self + .tabs + .first() + .and_then(|tab| tab.terminal_id(tab.root_pane)) + .and_then(|terminal_id| terminals.get(terminal_id)) + .map(|terminal| &terminal.cwd) + .unwrap_or(&self.identity_cwd); + self.automatic_display_name_for_cwd(cwd) } pub fn display_name_from( @@ -1071,10 +1130,18 @@ impl Workspace { } self.resolved_identity_cwd_from(terminals, terminal_runtimes) - .map(|cwd| derive_label_from_cwd(&cwd)) + .map(|cwd| self.automatic_display_name_for_cwd(&cwd)) .unwrap_or_else(|| "workspace".into()) } + fn automatic_display_name_for_cwd(&self, cwd: &std::path::Path) -> String { + if cwd == self.cached_identity_cwd { + self.cached_auto_label.clone() + } else { + fallback_label_from_cwd(cwd) + } + } + pub fn branch(&self) -> Option<String> { self.cached_git_branch.clone() } @@ -1099,13 +1166,6 @@ impl Workspace { self.cached_git_space = cwd.as_deref().and_then(git_space_metadata); } - pub fn git_status_snapshot_for_cwd_with_cache( - resolved_identity_cwd: &std::path::Path, - cached: Option<&GitStatusCacheEntry>, - ) -> (WorkspaceGitStatusSnapshot, Option<GitStatusCacheEntry>) { - self::git::git_status_snapshot_for_cwd(resolved_identity_cwd, cached) - } - pub fn find_tab_index_for_pane(&self, pane_id: PaneId) -> Option<usize> { self.tabs .iter() @@ -1210,6 +1270,9 @@ impl Workspace { id: generate_workspace_id(), custom_name: Some(name.to_string()), identity_cwd: identity_cwd.clone(), + cached_identity_cwd: identity_cwd.clone(), + cached_auto_label: fallback_label_from_cwd(&identity_cwd), + cached_git_status_key: identity_cwd.clone(), cached_git_branch: git_branch(&identity_cwd), cached_git_ahead_behind: None, cached_git_space: None, @@ -1571,6 +1634,92 @@ mod tests { assert!(!target.tabs[0].panes.contains_key(&source_pane)); } + #[tokio::test] + async fn new_workspace_retains_discovered_git_metadata() { + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock should be after unix epoch") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "herdr-workspace-git-metadata-{}-{stamp}", + std::process::id() + )); + std::fs::create_dir_all(root.join(".git")).expect("create git directory"); + std::fs::write(root.join(".git/HEAD"), "ref: refs/heads/main\n").expect("write git head"); + #[cfg(windows)] + let command = "C:\\Windows\\System32\\whoami.exe"; + #[cfg(not(windows))] + let command = "/usr/bin/true"; + let argv = vec![command.to_string()]; + let (events, _) = mpsc::channel(64); + let render_notify = Arc::new(Notify::new()); + let render_dirty = Arc::new(AtomicBool::new(false)); + + let (workspace, _terminal, runtime) = Workspace::new_argv_command( + root.clone(), + 24, + 80, + &argv, + 1024, + crate::terminal_theme::TerminalTheme::default(), + events, + render_notify, + render_dirty, + ) + .expect("create workspace"); + + let space = workspace + .git_space() + .expect("workspace should retain discovered git metadata"); + assert_eq!(space.repo_root, root); + + runtime.shutdown(); + std::fs::remove_dir_all(root).expect("remove test repo"); + } + + #[test] + fn display_name_reads_cached_identity_without_rechecking_filesystem() { + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock should be after unix epoch") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "herdr-workspace-label-cache-{}-{stamp}", + std::process::id() + )); + let cwd = root.join("deep/nested"); + std::fs::create_dir_all(&cwd).expect("create nested cwd"); + + let mut ws = Workspace::test_new("ignored"); + ws.custom_name = None; + ws.identity_cwd = cwd.clone(); + ws.tabs.clear(); + ws.cached_identity_cwd = cwd; + ws.cached_auto_label = "cached-repo".into(); + + std::fs::remove_dir_all(root).expect("remove cwd after cache admission"); + + assert_eq!(ws.display_name(), "cached-repo"); + } + + #[test] + fn terminal_aware_display_name_uses_latest_admitted_identity_cache() { + let mut ws = Workspace::test_new("ignored"); + let root_pane = ws.tabs[0].root_pane; + let terminal_id = ws.tabs[0].terminal_id(root_pane).unwrap().clone(); + ws.custom_name = None; + ws.identity_cwd = PathBuf::from("/old/workspace"); + ws.cached_identity_cwd = PathBuf::from("/new/repo/deep"); + ws.cached_auto_label = "repo".into(); + let terminals = HashMap::from([( + terminal_id.clone(), + TerminalState::new(terminal_id, PathBuf::from("/new/repo/deep")), + )]); + + assert_eq!(ws.display_name_from_terminals(&terminals), "repo"); + assert_eq!(ws.display_name(), "workspace"); + } + #[test] fn workspace_identity_follows_first_tab_root_pane_cwd() { let mut ws = Workspace::test_new("ignored"); |
