1use super::git_cmd;
5use crate::model::workspace::WorktreeInfo;
6use anyhow::{bail, Context, Result};
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9
10#[derive(Debug, Clone)]
11pub struct WorktreeEntry {
12 pub name: String,
13 pub path: PathBuf,
14 pub branch: String,
15 pub is_main: bool,
16}
17
18pub fn list_worktrees(repo_path: &Path) -> Result<Vec<WorktreeEntry>> {
20 let output = super::output_with_timeout(
21 git_cmd(repo_path).args(["worktree", "list", "--porcelain"]),
22 std::time::Duration::from_secs(5),
23 )
24 .context("git worktree list")?;
25 parse_porcelain_output(&String::from_utf8_lossy(&output.stdout), repo_path)
26}
27
28fn parse_porcelain_output(output: &str, repo_path: &Path) -> Result<Vec<WorktreeEntry>> {
29 let mut entries = Vec::new();
30 let mut current_path: Option<PathBuf> = None;
31 let mut current_branch: Option<String> = None;
32 let mut first = true;
33
34 for line in output.lines() {
35 if line.is_empty() {
36 if let Some(path) = current_path.take() {
37 let branch = current_branch.take().unwrap_or_else(|| "HEAD".to_string());
38 let name = derive_name(&path, &branch, first);
39 entries.push(WorktreeEntry {
40 name,
41 path,
42 branch,
43 is_main: first,
44 });
45 first = false;
46 }
47 } else if let Some(p) = line.strip_prefix("worktree ") {
48 current_path = Some(PathBuf::from(p.trim()));
49 } else if let Some(b) = line.strip_prefix("branch ") {
50 let b = b.trim().strip_prefix("refs/heads/").unwrap_or(b.trim());
51 current_branch = Some(b.to_string());
52 }
53 }
54
55 if let Some(path) = current_path {
57 let branch = current_branch.unwrap_or_else(|| "HEAD".to_string());
58 let name = derive_name(&path, &branch, first);
59 entries.push(WorktreeEntry {
60 name,
61 path,
62 branch,
63 is_main: first,
64 });
65 }
66
67 if entries.is_empty() {
68 entries.push(WorktreeEntry {
69 name: "main".to_string(),
70 path: repo_path.to_path_buf(),
71 branch: "main".to_string(),
72 is_main: true,
73 });
74 }
75
76 Ok(entries)
77}
78
79fn derive_name(path: &Path, branch: &str, is_main: bool) -> String {
80 if is_main {
81 return "main".to_string();
82 }
83 path.file_name()
84 .map(|n| n.to_string_lossy().to_string())
85 .unwrap_or_else(|| branch.replace('/', "-"))
86}
87
88pub fn to_worktree_infos(
90 entries: Vec<WorktreeEntry>,
91 aliases: &std::collections::HashMap<String, String>,
92) -> Vec<WorktreeInfo> {
93 entries
94 .into_iter()
95 .map(|e| {
96 let alias = aliases.get(&e.branch).cloned();
97 WorktreeInfo {
98 name: e.name,
99 branch: e.branch,
100 path: e.path,
101 is_main: e.is_main,
102 alias,
103 sessions: Vec::new(),
104 expanded: true,
105 git_info: None,
106 fetch_failed: false,
107 fetch_fail_count: 0,
108 fetch_fail_reason: None,
109 last_fetched: None,
110 git_info_fetched_at: None,
111 }
112 })
113 .collect()
114}
115
116pub fn create_worktree(repo_path: &Path, branch: &str, base_branch: &str) -> Result<PathBuf> {
118 let parent = repo_path.parent().context("repo has no parent dir")?;
119 let repo_name = repo_path
120 .file_name()
121 .context("repo has no name")?
122 .to_string_lossy();
123 let slug = branch.replace('/', "-").replace(
124 |c: char| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.',
125 "-",
126 );
127 let wt_path = parent.join(format!("{}-{}", repo_name, slug));
128
129 let status = git_cmd(repo_path)
130 .args([
131 "worktree",
132 "add",
133 "-b",
134 branch,
135 &wt_path.to_string_lossy(),
136 base_branch,
137 ])
138 .stdout(Stdio::null())
139 .stderr(Stdio::null())
140 .status()
141 .context("git worktree add failed")?;
142
143 if !status.success() {
144 bail!("git worktree add exited {}", status);
145 }
146 Ok(wt_path)
147}
148
149pub fn remove_worktree(repo_path: &Path, worktree_path: &Path, branch: &str) -> Result<()> {
151 let status = git_cmd(repo_path)
152 .args([
153 "worktree",
154 "remove",
155 "--force",
156 &worktree_path.to_string_lossy(),
157 ])
158 .stdout(Stdio::null())
159 .stderr(Stdio::null())
160 .status()
161 .context("git worktree remove failed")?;
162
163 if !status.success() {
164 bail!("git worktree remove exited {}", status);
165 }
166
167 let _ = git_cmd(repo_path)
169 .args(["branch", "-d", branch])
170 .stdout(Stdio::null())
171 .stderr(Stdio::null())
172 .status();
173
174 Ok(())
175}