aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 15:34:13 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 15:34:13 +0300
commite16d7d8c07a20f5ee0b4111808680bbcfd7df9ac (patch)
tree2c5c338c6096deaad1487b4b77e71d671e30b842 /src
parent81f355fadac7d0d45b077dfc28f9f679add6bbb6 (diff)
fix: preserve linked worktree workspace labels
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs2
-rw-r--r--src/app/api/worktrees.rs6
-rw-r--r--src/app/worktrees.rs6
-rw-r--r--src/ui/sidebar.rs2
-rw-r--r--src/workspace.rs18
-rw-r--r--src/workspace/git/discovery.rs49
-rw-r--r--src/workspace/git/mod.rs4
-rw-r--r--src/workspace/git/status.rs25
-rw-r--r--src/workspace/git/test_support.rs62
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()
@@ -1678,6 +1678,22 @@ mod tests {
}
#[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()
.duration_since(std::time::UNIX_EPOCH)
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<GitSpaceMetadata> {
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,
}
@@ -405,6 +409,29 @@ mod tests {
}
#[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");
run_git(&root, &["init", "--bare", ".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
@@ -530,6 +530,23 @@ mod tests {
}
#[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");
let common_dir = base.join("repo/.git");
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";