From 4ab146012d0af1c27b2352a78fcc2bb65d5c870c Mon Sep 17 00:00:00 2001 From: shvvkz Date: Thu, 21 May 2026 19:56:57 +0200 Subject: [PATCH 1/8] fix: use main repo when listing worktrees --- gitoxide-core/src/repository/worktree.rs | 64 ++++++++++++++++++------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index e3a373eb80c..ae40f5656ed 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -6,24 +6,58 @@ pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputF if format != OutputFormat::Human { bail!("JSON output isn't implemented yet"); } + let main_repo = repo.main_repo()?; + let mut worktrees = Vec::new(); - if let Some(worktree) = repo.worktree() { - writeln!( - out, - "{base} [{branch}]", - base = gix::path::realpath(worktree.base())?.display(), - branch = repo - .head_name()? - .map_or("".into(), |name| name.shorten().to_owned()), - )?; + if let Some(worktree) = main_repo.worktree() { + worktrees.push(create_worktree_info(&main_repo, gix::path::realpath(worktree.base())?)?); + } + + for proxy in main_repo.worktrees()? { + let base = proxy.base()?; + let worktree_repo = proxy.into_repo()?; + + worktrees.push(create_worktree_info(&worktree_repo, base)?); } - for proxy in repo.worktrees()? { + + let path_width = worktrees.iter().map(|worktree| worktree.base.len()).max().unwrap_or(0); + + for worktree in worktrees { + worktree.write(out, path_width)?; + } + + Ok(()) +} + +struct WorktreeInfo { + base: String, + head: String, + branch: String, +} + +impl WorktreeInfo { + fn write(&self, out: &mut dyn std::io::Write, path_width: usize) -> std::io::Result<()> { writeln!( out, - "{base} [{name}]", - base = proxy.base()?.display(), - name = proxy.id() - )?; + "{: anyhow::Result { + let head = repo.head_id()?.to_hex_with_len(9).to_string(); + let branch = repo + .head_name()? + .map_or("".into(), |name| name.shorten().to_owned()) + .to_string(); + + Ok(WorktreeInfo { + base: base.display().to_string(), + head, + branch, + }) } From b8c81091a7e69898cbfcb275ef24c83c5fa7a0dd Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:26:08 +0200 Subject: [PATCH 2/8] fix: use real_path for linked worktrees --- gitoxide-core/src/repository/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index ae40f5656ed..27382ea6db3 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -14,7 +14,7 @@ pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputF } for proxy in main_repo.worktrees()? { - let base = proxy.base()?; + let base = gix::path::realpath(proxy.base()?)?; let worktree_repo = proxy.into_repo()?; worktrees.push(create_worktree_info(&worktree_repo, base)?); From 1c21212b92635e1360808e775e74351228bfe8ef Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:30:32 +0200 Subject: [PATCH 3/8] fix: branch computation return a string --- gitoxide-core/src/repository/worktree.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index 27382ea6db3..4daa6e76042 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -50,10 +50,10 @@ impl WorktreeInfo { fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> anyhow::Result { let head = repo.head_id()?.to_hex_with_len(9).to_string(); - let branch = repo - .head_name()? - .map_or("".into(), |name| name.shorten().to_owned()) - .to_string(); + let branch = repo.head_name()?.map_or_else( + || "".to_string(), + |name| name.shorten().to_owned().to_string(), + ); Ok(WorktreeInfo { base: base.display().to_string(), From 33e2e24b51997d87d757f92992d5fdc4fed362ea Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:33:21 +0200 Subject: [PATCH 4/8] fix: use a const for head length --- gitoxide-core/src/repository/worktree.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index 4daa6e76042..59d5b91cebe 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -2,6 +2,8 @@ use anyhow::bail; use crate::OutputFormat; +const HEAD_LENGTH: usize = 9; + pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputFormat) -> anyhow::Result<()> { if format != OutputFormat::Human { bail!("JSON output isn't implemented yet"); @@ -49,7 +51,7 @@ impl WorktreeInfo { } fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> anyhow::Result { - let head = repo.head_id()?.to_hex_with_len(9).to_string(); + let head = repo.head_id()?.to_hex_with_len(HEAD_LENGTH).to_string(); let branch = repo.head_name()?.map_or_else( || "".to_string(), |name| name.shorten().to_owned().to_string(), From 1e2dfd40fb84f24768018f4880f0d7648dd4631e Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:39:44 +0200 Subject: [PATCH 5/8] fix: fall back on unborn HEADs using a null OID --- gitoxide-core/src/repository/worktree.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index 59d5b91cebe..f47186726fd 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -3,7 +3,7 @@ use anyhow::bail; use crate::OutputFormat; const HEAD_LENGTH: usize = 9; - +const ZERO_HEAD: &str = "000000000"; pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputFormat) -> anyhow::Result<()> { if format != OutputFormat::Human { bail!("JSON output isn't implemented yet"); @@ -51,7 +51,11 @@ impl WorktreeInfo { } fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> anyhow::Result { - let head = repo.head_id()?.to_hex_with_len(HEAD_LENGTH).to_string(); + let head = repo + .head_id() + .map(|id| id.to_hex_with_len(HEAD_LENGTH).to_string()) + .unwrap_or_else(|_| ZERO_HEAD.to_string()); + let branch = repo.head_name()?.map_or_else( || "".to_string(), |name| name.shorten().to_owned().to_string(), From 0b745cc35c15536209406363a48ed28da13bb30e Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:54:55 +0200 Subject: [PATCH 6/8] fix: fall back for inaccessible worktree --- gitoxide-core/src/repository/worktree.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index f47186726fd..e00fc313910 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -17,9 +17,15 @@ pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputF for proxy in main_repo.worktrees()? { let base = gix::path::realpath(proxy.base()?)?; - let worktree_repo = proxy.into_repo()?; - worktrees.push(create_worktree_info(&worktree_repo, base)?); + match proxy.into_repo() { + Ok(worktree_repo) => { + worktrees.push(create_worktree_info(&worktree_repo, base)?); + } + Err(_) => { + worktrees.push(create_inaccessible_worktree_info(base)); + } + } } let path_width = worktrees.iter().map(|worktree| worktree.base.len()).max().unwrap_or(0); @@ -67,3 +73,11 @@ fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> any branch, }) } + +fn create_inaccessible_worktree_info(base: std::path::PathBuf) -> WorktreeInfo { + WorktreeInfo { + base: base.display().to_string(), + head: ZERO_HEAD.to_string(), + branch: "".to_string(), + } +} From 08219d4ddaa4f0fb77afead8189836cc9ea23945 Mon Sep 17 00:00:00 2001 From: shvvkz Date: Fri, 22 May 2026 20:57:04 +0200 Subject: [PATCH 7/8] clippy happy --- gitoxide-core/src/repository/worktree.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index e00fc313910..5a52519a62e 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -57,10 +57,10 @@ impl WorktreeInfo { } fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> anyhow::Result { - let head = repo - .head_id() - .map(|id| id.to_hex_with_len(HEAD_LENGTH).to_string()) - .unwrap_or_else(|_| ZERO_HEAD.to_string()); + let head = repo.head_id().map_or_else( + |_| ZERO_HEAD.to_string(), + |id| id.to_hex_with_len(HEAD_LENGTH).to_string(), + ); let branch = repo.head_name()?.map_or_else( || "".to_string(), From 41f3e6d720c9149640241e3e817481a64b7c3e62 Mon Sep 17 00:00:00 2001 From: shvvkz Date: Tue, 26 May 2026 09:23:20 +0200 Subject: [PATCH 8/8] add documentation to functions and struct --- gitoxide-core/src/repository/worktree.rs | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/gitoxide-core/src/repository/worktree.rs b/gitoxide-core/src/repository/worktree.rs index 5a52519a62e..47c31cf7950 100644 --- a/gitoxide-core/src/repository/worktree.rs +++ b/gitoxide-core/src/repository/worktree.rs @@ -4,6 +4,23 @@ use crate::OutputFormat; const HEAD_LENGTH: usize = 9; const ZERO_HEAD: &str = "000000000"; + +/// list: List all worktrees associated with the main repository. +/// +/// This function collects information about the main worktree and any linked +/// worktrees, then writes them to the provided output stream in a human-readable +/// format. +/// +/// # Parameters +/// +/// - `repo`: The Git repository from which the worktrees are retrieved. +/// - `out`: The output stream where the worktree list is written. +/// - `format`: The output format to use. Currently, only `OutputFormat::Human` +/// is supported. +/// +/// # Returns +/// +/// Returns `Ok(())` if the worktrees are successfully listed. pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputFormat) -> anyhow::Result<()> { if format != OutputFormat::Human { bail!("JSON output isn't implemented yet"); @@ -37,6 +54,9 @@ pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write, format: OutputF Ok(()) } +/// WorktreeInfo +/// +/// Stores display worktree information struct WorktreeInfo { base: String, head: String, @@ -44,6 +64,19 @@ struct WorktreeInfo { } impl WorktreeInfo { + /// write: Writes the worktree information to the given output stream. + /// + /// The output contains the worktree path, the shortened HEAD commit hash, + /// and the current branch name. + /// + /// # Parameters + /// + /// - `out`: The output stream where the worktree information is written. + /// - `path_width`: The width used to align the worktree paths. + /// + /// # Returns + /// + /// Returns `Ok(())` if the information is successfully written. fn write(&self, out: &mut dyn std::io::Write, path_width: usize) -> std::io::Result<()> { writeln!( out, @@ -56,6 +89,24 @@ impl WorktreeInfo { } } +/// create_worktree_info: Creates display information for an accessible worktree. +/// +/// This function reads the worktree HEAD and branch name from the given +/// repository. If the HEAD commit cannot be read, a zero hash is used instead. +/// If the repository is in a detached HEAD state, the branch name is displayed +/// as ``. +/// +/// # Parameters +/// +/// - `repo`: The repository associated with the worktree. +/// - `base`: The resolved base path of the worktree. +/// +/// # Returns +/// +/// Returns a `WorktreeInfo` value containing: +/// - the worktree base path, +/// - the shortened HEAD commit hash, +/// - the current branch name or ``. fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> anyhow::Result { let head = repo.head_id().map_or_else( |_| ZERO_HEAD.to_string(), @@ -74,6 +125,23 @@ fn create_worktree_info(repo: &gix::Repository, base: std::path::PathBuf) -> any }) } +/// create_inaccessible_worktree_info: Creates display information for +/// an inaccessible worktree. +/// +/// This function is used when a linked worktree exists but its repository +/// cannot be opened. In that case, the HEAD is displayed as a zero hash and +/// the branch is displayed as ``. +/// +/// # Parameters +/// +/// - `base`: The resolved base path of the inaccessible worktree. +/// +/// # Returns +/// +/// Returns a `WorktreeInfo` value containing: +/// - the worktree base path, +/// - a zero hash as the HEAD value, +/// - `` as the branch name. fn create_inaccessible_worktree_info(base: std::path::PathBuf) -> WorktreeInfo { WorktreeInfo { base: base.display().to_string(),