use std::path::{Path, PathBuf};
use super::{GitError, Repo, anchor_git_path};
#[derive(Debug, Clone)]
pub struct BranchInfo {
pub name: String,
pub head_sha: String,
}
#[derive(Debug, Clone)]
pub struct WorktreeInfo {
pub name: String,
pub path: PathBuf,
pub branch: Option<String>,
pub head_sha: Option<String>,
pub detached: bool,
}
impl Repo {
pub fn list_local_branches(&self) -> Result<Vec<BranchInfo>, GitError> {
let local = self.local();
let platform = local.references().map_err(|e| GitError::Read {
what: "references".to_string(),
msg: e.to_string(),
})?;
let branches = platform.local_branches().map_err(|e| GitError::Read {
what: "local branches".to_string(),
msg: e.to_string(),
})?;
let mut out = Vec::new();
for reference in branches {
let Ok(mut reference) = reference else { continue };
let name = String::from_utf8_lossy(reference.name().shorten()).into_owned();
let Ok(id) = reference.peel_to_id() else {
continue;
};
out.push(BranchInfo {
name,
head_sha: id.to_string(),
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
pub fn list_worktrees(&self) -> Result<Vec<WorktreeInfo>, GitError> {
let mut out = vec![self.main_worktree_info()?];
let common = anchor_git_path(&self.workdir, self.local().common_dir());
let worktrees_dir = common.join("worktrees");
if let Ok(entries) = std::fs::read_dir(&worktrees_dir) {
let mut linked: Vec<WorktreeInfo> = entries
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.filter_map(|e| self.linked_worktree_info(&e.path()))
.collect();
linked.sort_by(|a, b| a.name.cmp(&b.name));
out.extend(linked);
}
Ok(out)
}
fn main_worktree_info(&self) -> Result<WorktreeInfo, GitError> {
let info = self.info()?;
Ok(WorktreeInfo {
name: "(main)".to_string(),
path: self.main_worktree_root(),
detached: info.head_sha.is_some() && info.branch.is_none(),
branch: info.branch,
head_sha: info.head_sha,
})
}
fn linked_worktree_info(&self, plumbing_dir: &Path) -> Option<WorktreeInfo> {
let name = plumbing_dir.file_name()?.to_string_lossy().into_owned();
let root = self.worktree_root_from_gitdir(plumbing_dir)?;
let (branch, head_sha, detached) = self.read_worktree_head(plumbing_dir);
Some(WorktreeInfo {
name,
path: root,
branch,
head_sha,
detached,
})
}
fn worktree_root_from_gitdir(&self, plumbing_dir: &Path) -> Option<PathBuf> {
let raw = std::fs::read_to_string(plumbing_dir.join("gitdir")).ok()?;
let dot_git = Path::new(raw.trim());
let anchored = anchor_git_path(&self.workdir, dot_git);
anchored.parent().map(Path::to_path_buf)
}
fn read_worktree_head(&self, plumbing_dir: &Path) -> (Option<String>, Option<String>, bool) {
let Ok(head) = std::fs::read_to_string(plumbing_dir.join("HEAD")) else {
return (None, None, false);
};
let head = head.trim();
if let Some(refname) = head.strip_prefix("ref: ") {
let branch = refname.strip_prefix("refs/heads/").unwrap_or(refname).to_string();
let sha = self.resolve_rev(refname).ok();
(Some(branch), sha, false)
} else if !head.is_empty() {
(None, Some(head.to_string()), true)
} else {
(None, None, false)
}
}
}