1use crate::git::GitRepo;
2use anyhow::{Context, Result};
3use serde::Serialize;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize)]
7pub struct Worktree {
8 pub path: PathBuf,
9 pub head: String,
10 pub branch: Option<String>,
11 pub branch_short: Option<String>,
12 pub detached: bool,
13 pub locked: bool,
14 pub prunable: bool,
15}
16
17impl Worktree {
18 pub fn name(&self) -> &str {
19 self.branch_short
20 .as_deref()
21 .or_else(|| self.path.file_name().and_then(|s| s.to_str()))
22 .unwrap_or("unknown")
23 }
24
25 pub fn is_main_worktree(&self, repo: &GitRepo) -> bool {
26 repo.common_dir.parent() == Some(&self.path) || check_same_path(&self.path, &repo.root)
30 }
32}
33
34fn check_same_path(p1: &Path, p2: &Path) -> bool {
35 match (p1.canonicalize(), p2.canonicalize()) {
36 (Ok(c1), Ok(c2)) => c1 == c2,
37 _ => false, }
39}
40
41pub fn list_worktrees(repo: &GitRepo) -> Result<Vec<Worktree>> {
42 let git_repo = repo
43 .repo
44 .lock()
45 .map_err(|_| anyhow::anyhow!("Failed to lock repository"))?;
46 let mut worktrees = Vec::new();
47
48 let worktree_names = git_repo.worktrees().context("Failed to list worktrees")?;
50 for name in worktree_names.iter().flatten() {
51 let wt = git_repo.find_worktree(name)?;
53 let path = wt.path().to_path_buf();
54
55 let should_prune = wt.is_prunable(None).unwrap_or(false);
56 let is_locked = matches!(wt.is_locked(), Ok(git2::WorktreeLockStatus::Locked(_)));
57
58 if should_prune {
60 worktrees.push(Worktree {
61 path,
62 head: String::new(),
63 branch: None,
64 branch_short: None,
65 detached: false,
66 locked: is_locked,
67 prunable: true,
68 });
69 continue;
70 }
71
72 match git2::Repository::open(&path) {
75 Ok(wt_repo) => {
76 let (head, branch, branch_short, detached) = get_repo_head_info(&wt_repo);
77 worktrees.push(Worktree {
78 path,
79 head,
80 branch,
81 branch_short,
82 detached,
83 locked: is_locked,
84 prunable: false,
85 });
86 }
87 Err(_) => {
88 worktrees.push(Worktree {
90 path,
91 head: String::new(),
92 branch: None,
93 branch_short: None,
94 detached: false,
95 locked: is_locked,
96 prunable: true, });
98 }
99 }
100 }
101
102 let common_dir = if git_repo.is_worktree() {
106 git_repo
108 .path()
109 .parent()
110 .and_then(|p| p.parent())
111 .unwrap_or(git_repo.path())
112 } else {
113 git_repo.path()
115 };
116
117 let main_path = common_dir.parent().unwrap_or(common_dir);
118
119 if let Ok(main_repo) = git2::Repository::open(main_path) {
122 if !worktrees
123 .iter()
124 .any(|w| check_same_path(&w.path, main_path))
125 {
126 let (head, branch, branch_short, detached) = get_repo_head_info(&main_repo);
127 worktrees.push(Worktree {
128 path: main_path.to_path_buf(),
129 head,
130 branch,
131 branch_short,
132 detached,
133 locked: false,
134 prunable: false,
135 });
136 }
137 }
138
139 Ok(worktrees)
140}
141
142fn get_repo_head_info(repo: &git2::Repository) -> (String, Option<String>, Option<String>, bool) {
143 let head_ref = repo.head();
144 match head_ref {
145 Ok(r) => {
146 let head_oid = r.target().map(|o| o.to_string()).unwrap_or_default();
147 let detached = repo.head_detached().unwrap_or(false);
148 let name = r.name().map(|s| s.to_string());
149
150 if detached {
151 (head_oid, None, None, true)
152 } else {
153 let shorthand = r.shorthand().map(|s| s.to_string());
154 (head_oid, name, shorthand, false)
155 }
156 }
157 Err(_) => (String::new(), None, None, false), }
159}
160
161pub fn find_worktree<'a>(worktrees: &'a [Worktree], name: &str) -> Option<&'a Worktree> {
162 worktrees.iter().find(|worktree| {
163 worktree.branch_short.as_deref() == Some(name)
164 || worktree.path.file_name().and_then(|s| s.to_str()) == Some(name)
165 })
166}
167
168pub fn slug_from_branch(branch: &str) -> String {
169 branch
170 .chars()
171 .map(|c| {
172 if c.is_alphanumeric() || c == '-' || c == '_' {
173 c
174 } else {
175 '-'
176 }
177 })
178 .collect::<String>()
179 .trim_matches('-')
180 .to_string()
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_slug_from_branch() {
189 assert_eq!(slug_from_branch("feat/login"), "feat-login");
190 assert_eq!(slug_from_branch("fix/bug-123"), "fix-bug-123");
191 assert_eq!(
192 slug_from_branch("feature/add user auth"),
193 "feature-add-user-auth"
194 );
195 }
196}