From e16d7d8c07a20f5ee0b4111808680bbcfd7df9ac Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 28 Jul 2026 15:34:13 +0300 Subject: [PATCH] fix: preserve linked worktree workspace labels --- src/app/actions.rs | 2 +- src/app/api/worktrees.rs | 6 +-- src/app/worktrees.rs | 6 +-- src/ui/sidebar.rs | 2 +- src/workspace.rs | 18 ++++++++- src/workspace/git/discovery.rs | 49 ++++++++++++++++++------ src/workspace/git/mod.rs | 4 +- src/workspace/git/status.rs | 25 +++++++++++-- src/workspace/git/test_support.rs | 62 +++++++++++++++++++++++++++++++ 9 files changed, 149 insertions(+), 25 deletions(-) diff --git a/src/app/actions.rs b/src/app/actions.rs index 91572765..c92a91e5 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -4136,7 +4136,7 @@ mod tests { space: Some(crate::workspace::GitSpaceMetadata { key: "other-repo-key".into(), checkout_key: "/other/checkout".into(), - label: "other".into(), + repo_name: "other".into(), repo_root: "/other/repo".into(), is_linked_worktree: false, }), diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs index 91a19673..7aa8c00f 100644 --- a/src/app/api/worktrees.rs +++ b/src/app/api/worktrees.rs @@ -212,7 +212,7 @@ impl App { source_checkout_path: space.repo_root.clone(), source_repo_root: space.repo_root, repo_key: space.key, - repo_name: space.label, + repo_name: space.repo_name, }; return Ok(source); } @@ -324,7 +324,7 @@ impl App { source_checkout_path: space.repo_root.clone(), source_repo_root: space.repo_root, repo_key: space.key, - repo_name: space.label, + repo_name: space.repo_name, }) } @@ -678,7 +678,7 @@ fn worktree_source_from_space( source_checkout_path: source_checkout_path.clone(), source_repo_root: source_checkout_path, repo_key: space.key, - repo_name: space.label, + repo_name: space.repo_name, } } diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs index ca537bb8..c839c04a 100644 --- a/src/app/worktrees.rs +++ b/src/app/worktrees.rs @@ -57,7 +57,7 @@ impl App { Some(crate::workspace::GitSpaceMetadata { key: membership.key.clone(), checkout_key: membership.checkout_path.display().to_string(), - label: membership.label.clone(), + repo_name: membership.label.clone(), repo_root: membership.repo_root.clone(), is_linked_worktree: membership.is_linked_worktree, }) @@ -88,7 +88,7 @@ impl App { } }; - let repo_name = space.label.clone(); + let repo_name = space.repo_name.clone(); let seed = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_micros().min(u128::from(u64::MAX)) as u64) @@ -224,7 +224,7 @@ impl App { source_checkout_path, source_repo_root: space.repo_root, repo_key: space.key, - repo_name: space.label, + repo_name: space.repo_name, entries, selected: 0, query: String::new(), diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index c7ba659a..69193158 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -2386,7 +2386,7 @@ rows = [[{ token = "git_status", fg = "#123456" }]] ws.cached_git_space = Some(crate::workspace::GitSpaceMetadata { key: key.into(), checkout_key: format!("/repo/{name}"), - label: "herdr".into(), + repo_name: "herdr".into(), repo_root: std::path::PathBuf::from(format!("/repo/{name}")), is_linked_worktree: false, }); diff --git a/src/workspace.rs b/src/workspace.rs index 73a46d25..bb079b96 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -65,7 +65,7 @@ pub(crate) fn discover_workspace_git_identity( let space = git_space_metadata(cwd); let auto_label = space .as_ref() - .map(|space| space.label.clone()) + .map(|space| self::git::automatic_workspace_label(cwd, &space.repo_root)) .unwrap_or_else(|| fallback_label_from_cwd(cwd)); let status_cache_key = space .as_ref() @@ -1677,6 +1677,22 @@ mod tests { std::fs::remove_dir_all(root).expect("remove test repo"); } + #[test] + fn linked_worktree_auto_label_uses_checkout_name_not_repo_name() { + let (base, repo, checkout) = + self::git::test_support::create_repo_with_linked_worktree("linked-auto-label"); + + let (space, auto_label, _) = discover_workspace_git_identity(&checkout); + + assert_eq!( + space.unwrap().repo_name, + repo.file_name().unwrap().to_str().unwrap() + ); + assert_eq!(auto_label, checkout.file_name().unwrap().to_str().unwrap()); + + std::fs::remove_dir_all(base).unwrap(); + } + #[test] fn display_name_reads_cached_identity_without_rechecking_filesystem() { let stamp = std::time::SystemTime::now() diff --git a/src/workspace/git/discovery.rs b/src/workspace/git/discovery.rs index 9e1be74d..3e867443 100644 --- a/src/workspace/git/discovery.rs +++ b/src/workspace/git/discovery.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; pub struct GitSpaceMetadata { pub key: String, pub checkout_key: String, - pub label: String, + pub repo_name: String, pub repo_root: PathBuf, pub is_linked_worktree: bool, } @@ -19,13 +19,9 @@ pub struct GitWorktreeInfo { } pub fn derive_label_from_cwd(cwd: &Path) -> String { - if let Some(repo_root) = git_repo_root(cwd) { - if let Some(name) = repo_root.file_name().and_then(|n| n.to_str()) { - return name.to_string(); - } - } - - fallback_label_from_cwd(cwd) + git_repo_root(cwd) + .map(|repo_root| automatic_workspace_label(cwd, &repo_root)) + .unwrap_or_else(|| fallback_label_from_cwd(cwd)) } pub fn fallback_label_from_cwd(cwd: &Path) -> String { @@ -64,6 +60,14 @@ pub fn git_space_metadata(cwd: &Path) -> Option { Some(git_space_metadata_from_info(&info)) } +pub(crate) fn automatic_workspace_label(cwd: &Path, repo_root: &Path) -> String { + repo_root + .file_name() + .and_then(|name| name.to_str()) + .map(str::to_string) + .unwrap_or_else(|| fallback_label_from_cwd(cwd)) +} + pub(super) fn git_space_metadata_from_info(info: &GitWorktreeInfo) -> GitSpaceMetadata { let key = canonicalize_best_effort_path(&info.git_common_dir) .display() @@ -79,9 +83,9 @@ pub(super) fn git_space_metadata_from_info(info: &GitWorktreeInfo) -> GitSpaceMe { info.git_common_dir.parent().unwrap_or(&info.repo_root) } else { - &info.repo_root + &info.git_common_dir }; - let label = label_path + let repo_name = label_path .file_name() .and_then(|name| name.to_str()) .unwrap_or("repo") @@ -89,7 +93,7 @@ pub(super) fn git_space_metadata_from_info(info: &GitWorktreeInfo) -> GitSpaceMe GitSpaceMetadata { key, checkout_key, - label, + repo_name, repo_root: info.repo_root.clone(), is_linked_worktree: info.is_linked_worktree, } @@ -404,6 +408,29 @@ mod tests { std::fs::remove_dir_all(bare).unwrap(); } + #[test] + fn bare_source_and_linked_checkout_share_repo_name_but_not_auto_label() { + let (base, bare, checkout) = + crate::workspace::git::test_support::create_bare_repo_with_linked_worktree( + "bare-linked-labels", + ); + + let bare_space = git_space_metadata(&bare).unwrap(); + let checkout_space = git_space_metadata(&checkout).unwrap(); + let bare_auto_label = automatic_workspace_label(&bare, &bare_space.repo_root); + let checkout_auto_label = automatic_workspace_label(&checkout, &checkout_space.repo_root); + + assert_eq!(bare_space.key, checkout_space.key); + assert_eq!(bare_space.repo_name, checkout_space.repo_name); + assert_eq!(bare_auto_label, bare.file_name().unwrap().to_str().unwrap()); + assert_eq!( + checkout_auto_label, + checkout.file_name().unwrap().to_str().unwrap() + ); + + std::fs::remove_dir_all(base).unwrap(); + } + #[test] fn git_space_metadata_marks_bare_dot_git_repo() { let root = temp_test_dir("bare-dot-git"); diff --git a/src/workspace/git/mod.rs b/src/workspace/git/mod.rs index 87a6b06e..b00cf3b3 100644 --- a/src/workspace/git/mod.rs +++ b/src/workspace/git/mod.rs @@ -4,7 +4,9 @@ mod config_tests; mod discovery; mod status; #[cfg(test)] -mod test_support; +pub(super) mod test_support; + +pub(crate) use self::discovery::automatic_workspace_label; pub use self::{ discovery::{ diff --git a/src/workspace/git/status.rs b/src/workspace/git/status.rs index fe873e29..dcfc1aff 100644 --- a/src/workspace/git/status.rs +++ b/src/workspace/git/status.rs @@ -6,9 +6,9 @@ use crate::workspace::{GitSpaceMetadata, WorkspaceGitStatusSnapshot}; use super::{ config::{read_branch_config, upstream_full_ref}, discovery::{ - 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, + 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, + git_symbolic_head_full, git_worktree_info, read_ref_oid, GitWorktreeInfo, }, }; @@ -111,8 +111,8 @@ 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 = space.label.clone(); if !demand.ahead_behind { let branch = demand @@ -529,6 +529,23 @@ mod tests { std::fs::remove_dir_all(root).unwrap(); } + #[test] + fn linked_worktree_refresh_keeps_checkout_name_as_auto_label() { + let (base, _, checkout) = + crate::workspace::git::test_support::create_repo_with_linked_worktree( + "linked-refresh-label", + ); + + let (snapshot, _) = git_status_snapshot_for_cwd(&checkout, None); + + assert_eq!( + snapshot.auto_label, + checkout.file_name().unwrap().to_str().unwrap() + ); + + std::fs::remove_dir_all(base).unwrap(); + } + #[test] fn git_status_cache_key_is_per_linked_worktree_checkout() { let base = temp_test_dir("linked-worktree-keys"); diff --git a/src/workspace/git/test_support.rs b/src/workspace/git/test_support.rs index feba4b3a..01996782 100644 --- a/src/workspace/git/test_support.rs +++ b/src/workspace/git/test_support.rs @@ -16,6 +16,68 @@ pub(super) fn temp_test_dir(name: &str) -> PathBuf { path } +fn init_repo_with_commit(repo: &Path) { + std::fs::create_dir_all(repo).unwrap(); + run_git(repo, &["init", "--quiet"]); + run_git(repo, &["config", "user.email", "herdr@example.invalid"]); + run_git(repo, &["config", "user.name", "Herdr Test"]); + run_git( + repo, + &["commit", "--quiet", "--allow-empty", "-m", "initial"], + ); +} + +pub(crate) fn create_repo_with_linked_worktree(name: &str) -> (PathBuf, PathBuf, PathBuf) { + let base = temp_test_dir(name); + let repo = base.join("herdr"); + let checkout = base.join("testr56"); + init_repo_with_commit(&repo); + run_git( + &repo, + &[ + "worktree", + "add", + "--quiet", + "-b", + "testr56", + checkout.to_str().unwrap(), + "HEAD", + ], + ); + (base, repo, checkout) +} + +pub(crate) fn create_bare_repo_with_linked_worktree(name: &str) -> (PathBuf, PathBuf, PathBuf) { + let base = temp_test_dir(name); + let seed = base.join("seed"); + let bare = base.join("herdr.git"); + let checkout = base.join("feature"); + init_repo_with_commit(&seed); + run_git( + &base, + &[ + "clone", + "--quiet", + "--bare", + seed.to_str().unwrap(), + bare.to_str().unwrap(), + ], + ); + run_git( + &bare, + &[ + "worktree", + "add", + "--quiet", + "-b", + "feature", + checkout.to_str().unwrap(), + "HEAD", + ], + ); + (base, bare, checkout) +} + pub(super) fn write_fake_tracked_repo(root: &Path) { let head_oid = "1111111111111111111111111111111111111111"; let upstream_oid = "2222222222222222222222222222222222222222";