fix: preserve linked worktree workspace labels

This commit is contained in:
Ogulcan Celik 2026-07-28 15:34:13 +03:00
parent 81f355fada
commit e16d7d8c07
9 changed files with 149 additions and 25 deletions

View File

@ -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,
}),

View File

@ -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,
}
}

View File

@ -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(),

View File

@ -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,
});

View File

@ -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()

View File

@ -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,
}
@ -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");

View File

@ -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::{

View File

@ -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");

View File

@ -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";