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 p1.canonicalize().ok() == p2.canonicalize().ok()
36}
37
38pub fn list_worktrees(repo: &GitRepo) -> Result<Vec<Worktree>> {
39 let git_repo = repo.repo.lock().unwrap();
40 let mut worktrees = Vec::new();
41
42 let worktree_names = git_repo.worktrees().context("Failed to list worktrees")?;
44 for name in worktree_names.iter() {
45 let name = name.unwrap();
46 let wt = git_repo.find_worktree(name)?;
48 let path = wt.path().to_path_buf();
49
50 let should_prune = wt.is_prunable(None).unwrap_or(false);
51 let is_locked = matches!(wt.is_locked(), Ok(git2::WorktreeLockStatus::Locked(_)));
52
53 if should_prune {
55 worktrees.push(Worktree {
56 path,
57 head: String::new(),
58 branch: None,
59 branch_short: None,
60 detached: false,
61 locked: is_locked,
62 prunable: true,
63 });
64 continue;
65 }
66
67 match git2::Repository::open(&path) {
70 Ok(wt_repo) => {
71 let (head, branch, branch_short, detached) = get_repo_head_info(&wt_repo);
72 worktrees.push(Worktree {
73 path,
74 head,
75 branch,
76 branch_short,
77 detached,
78 locked: is_locked,
79 prunable: false,
80 });
81 }
82 Err(_) => {
83 worktrees.push(Worktree {
85 path,
86 head: String::new(),
87 branch: None,
88 branch_short: None,
89 detached: false,
90 locked: is_locked,
91 prunable: true, });
93 }
94 }
95 }
96
97 let common_dir = if git_repo.is_worktree() {
101 git_repo
103 .path()
104 .parent()
105 .and_then(|p| p.parent())
106 .unwrap_or(git_repo.path())
107 } else {
108 git_repo.path()
110 };
111
112 let main_path = common_dir.parent().unwrap_or(common_dir);
113
114 if let Ok(main_repo) = git2::Repository::open(main_path) {
117 if !worktrees
118 .iter()
119 .any(|w| check_same_path(&w.path, main_path))
120 {
121 let (head, branch, branch_short, detached) = get_repo_head_info(&main_repo);
122 worktrees.push(Worktree {
123 path: main_path.to_path_buf(),
124 head,
125 branch,
126 branch_short,
127 detached,
128 locked: false,
129 prunable: false,
130 });
131 }
132 }
133
134 Ok(worktrees)
135}
136
137fn get_repo_head_info(repo: &git2::Repository) -> (String, Option<String>, Option<String>, bool) {
138 let head_ref = repo.head();
139 match head_ref {
140 Ok(r) => {
141 let head_oid = r.target().map(|o| o.to_string()).unwrap_or_default();
142 let detached = repo.head_detached().unwrap_or(false);
143 let name = r.name().map(|s| s.to_string());
144
145 if detached {
146 (head_oid, None, None, true)
147 } else {
148 let shorthand = r.shorthand().map(|s| s.to_string());
149 (head_oid, name, shorthand, false)
150 }
151 }
152 Err(_) => (String::new(), None, None, false), }
154}
155
156pub fn find_worktree<'a>(worktrees: &'a [Worktree], name: &str) -> Option<&'a Worktree> {
157 worktrees.iter().find(|worktree| {
158 worktree.branch_short.as_deref() == Some(name)
159 || worktree.path.file_name().and_then(|s| s.to_str()) == Some(name)
160 })
161}
162
163pub fn slug_from_branch(branch: &str) -> String {
164 branch
165 .chars()
166 .map(|c| {
167 if c.is_alphanumeric() || c == '-' || c == '_' {
168 c
169 } else {
170 '-'
171 }
172 })
173 .collect::<String>()
174 .trim_matches('-')
175 .to_string()
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_slug_from_branch() {
184 assert_eq!(slug_from_branch("feat/login"), "feat-login");
185 assert_eq!(slug_from_branch("fix/bug-123"), "fix-bug-123");
186 assert_eq!(
187 slug_from_branch("feature/add user auth"),
188 "feature-add-user-auth"
189 );
190 }
191}