use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Worktree {
pub path: String,
pub branch: String,
pub is_main: bool,
pub is_locked: bool,
pub is_detached: bool,
}
pub fn parse(porcelain: &str) -> Vec<Worktree> {
let mut out: Vec<Worktree> = Vec::new();
let mut current: Option<Worktree> = None;
let mut head = String::new();
for line in porcelain.lines() {
if let Some(path) = line.strip_prefix("worktree ") {
if let Some(w) = current.take() {
out.push(w);
}
head.clear();
current = Some(Worktree {
path: path.to_string(),
branch: String::new(),
is_main: out.is_empty(),
is_locked: false,
is_detached: false,
});
} else if let Some(sha) = line.strip_prefix("HEAD ") {
head = sha.to_string();
} else if let Some(b) = line.strip_prefix("branch ") {
if let Some(w) = current.as_mut() {
w.branch = b.trim_start_matches("refs/heads/").to_string();
}
} else if line == "detached" {
if let Some(w) = current.as_mut() {
w.is_detached = true;
w.branch = head.chars().take(7).collect();
}
} else if line == "locked" || line.starts_with("locked ") {
if let Some(w) = current.as_mut() {
w.is_locked = true;
}
}
}
if let Some(w) = current.take() {
out.push(w);
}
out
}
pub fn list(repo_path: &str) -> Result<Vec<Worktree>, String> {
let out = Command::new("git")
.args(["-C", repo_path, "worktree", "list", "--porcelain"])
.output()
.map_err(|e| format!("could not run git: {e}"))?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
}
Ok(parse(&String::from_utf8_lossy(&out.stdout)))
}
#[cfg(test)]
mod tests {
use super::*;
const PORCELAIN: &str = "\
worktree /Users/x/Developer/TaxCommHub
HEAD edaad18c1111111111111111111111111111111
branch refs/heads/main
worktree /Users/x/Developer/TaxCommHub-561
HEAD 65416862222222222222222222222222222222b
branch refs/heads/561-enrollment-window-support
locked
worktree /Users/x/Developer/TaxCommHub-detached
HEAD 06dd22433333333333333333333333333333333
detached
worktree /Users/x/Developer/TaxCommHub-email
HEAD 5db559e64444444444444444444444444444444
branch refs/heads/email-project-prototype
locked
";
#[test]
fn the_first_worktree_is_the_main_checkout() {
let w = parse(PORCELAIN);
assert_eq!(w.len(), 4);
assert!(w[0].is_main, "porcelain lists the main checkout first");
assert!(!w[1].is_main);
}
#[test]
fn branch_names_lose_their_refs_prefix() {
let w = parse(PORCELAIN);
assert_eq!(w[0].branch, "main");
assert_eq!(w[1].branch, "561-enrollment-window-support");
}
#[test]
fn locked_worktrees_are_marked_not_skipped() {
let w = parse(PORCELAIN);
assert!(w[1].is_locked);
assert!(!w[0].is_locked, "the main checkout is not locked");
assert_eq!(w.len(), 4, "a locked worktree is still a worktree");
}
#[test]
fn a_detached_worktree_shows_its_sha_rather_than_nothing() {
let w = parse(PORCELAIN);
assert!(w[2].is_detached);
assert_eq!(w[2].branch, "06dd224");
assert!(!w[2].branch.is_empty());
}
#[test]
fn empty_input_is_no_worktrees_not_a_panic() {
assert_eq!(parse(""), vec![]);
assert_eq!(parse("\n\n"), vec![]);
}
}