aboutsummaryrefslogtreecommitdiff
path: root/src/workspace.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.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.rs')
-rw-r--r--src/workspace.rs181
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");