1use anyhow::Result;
2use std::path::{Path, PathBuf};
3use crate::config::Config;
4use crate::git_util::run;
5use crate::ticket::{Ticket, load_all_from_git};
6
7pub fn find_worktree_for_branch(root: &Path, branch: &str) -> Option<PathBuf> {
10 let out = run(root, &["worktree", "list", "--porcelain"]).ok()?;
11 let mut current_path: Option<PathBuf> = None;
12 for line in out.lines() {
13 if let Some(p) = line.strip_prefix("worktree ") {
14 current_path = Some(PathBuf::from(p));
15 } else if let Some(b) = line.strip_prefix("branch refs/heads/") {
16 if b == branch {
17 return current_path;
18 }
19 }
20 }
21 None
22}
23
24pub fn list_ticket_worktrees(root: &Path) -> Result<Vec<(PathBuf, String)>> {
27 let out = run(root, &["worktree", "list", "--porcelain"])?;
28 let main = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
29
30 let mut result = Vec::new();
31 let mut current_path: Option<PathBuf> = None;
32 for line in out.lines() {
33 if let Some(p) = line.strip_prefix("worktree ") {
34 current_path = Some(PathBuf::from(p));
35 } else if let Some(b) = line.strip_prefix("branch refs/heads/") {
36 if b.starts_with("ticket/") {
37 if let Some(p) = ¤t_path {
38 if p.canonicalize().unwrap_or_else(|_| p.clone()) != main {
39 result.push((p.clone(), b.to_string()));
40 }
41 }
42 }
43 }
44 }
45 Ok(result)
46}
47
48pub fn ensure_worktree(root: &Path, worktrees_base: &Path, branch: &str) -> Result<PathBuf> {
51 if let Some(existing) = find_worktree_for_branch(root, branch) {
52 return Ok(existing);
53 }
54 let wt_name = branch.replace('/', "-");
55 std::fs::create_dir_all(worktrees_base)?;
56 let wt_path = worktrees_base.join(&wt_name);
57 add_worktree(root, &wt_path, branch)?;
58 Ok(find_worktree_for_branch(root, branch).unwrap_or(wt_path))
59}
60
61pub fn add_worktree(root: &Path, wt_path: &Path, branch: &str) -> Result<()> {
64 let has_local = run(root, &["rev-parse", "--verify", &format!("refs/heads/{branch}")]).is_ok();
65 if !has_local {
66 let _ = run(root, &["fetch", "origin", branch]);
67 }
68 run(root, &["worktree", "add", &wt_path.to_string_lossy(), branch])?;
69 crate::logger::log("add_worktree", &format!("{}", wt_path.display()));
70 Ok(())
71}
72
73pub fn remove_worktree(root: &Path, wt_path: &Path, force: bool) -> Result<()> {
75 clean_agent_dirs(root, wt_path);
76 let path_str = wt_path.to_string_lossy();
77 if force {
78 run(root, &["worktree", "remove", "--force", &path_str]).map(|_| ())
79 } else {
80 run(root, &["worktree", "remove", &path_str]).map(|_| ())
81 }
82}
83
84pub fn sync_agent_dirs(root: &Path, wt_path: &Path, agent_dirs: &[String], warnings: &mut Vec<String>) {
87 for dir_name in agent_dirs {
88 let src = root.join(dir_name);
89 if !src.is_dir() {
90 continue;
91 }
92 if is_tracked(root, dir_name) {
93 continue;
94 }
95 let dst = wt_path.join(dir_name);
96 if let Err(e) = copy_dir_recursive(&src, &dst) {
97 warnings.push(format!("warning: could not copy {dir_name} to worktree: {e}"));
98 }
99 }
100}
101
102fn clean_agent_dirs(root: &Path, wt_path: &Path) {
105 let config = match Config::load(root) {
106 Ok(c) => c,
107 Err(_) => return,
108 };
109 for dir_name in &config.worktrees.agent_dirs {
110 let dir = wt_path.join(dir_name);
111 if !dir.is_dir() {
112 continue;
113 }
114 if is_tracked(root, dir_name) {
115 continue;
116 }
117 let _ = std::fs::remove_dir_all(&dir);
118 }
119}
120
121fn is_tracked(root: &Path, path: &str) -> bool {
122 crate::git_util::is_file_tracked(root, path)
123}
124
125fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
126 if dst.exists() {
127 std::fs::remove_dir_all(dst)?;
128 }
129 std::fs::create_dir_all(dst)?;
130 for entry in std::fs::read_dir(src)? {
131 let entry = entry?;
132 let src_path = entry.path();
133 let dst_path = dst.join(entry.file_name());
134 if src_path.is_dir() {
135 copy_dir_recursive(&src_path, &dst_path)?;
136 } else {
137 std::fs::copy(&src_path, &dst_path)?;
138 }
139 }
140 Ok(())
141}
142
143pub fn provision_worktree(root: &Path, config: &Config, branch: &str, warnings: &mut Vec<String>) -> Result<PathBuf> {
144 let main_root = crate::git_util::main_worktree_root(root).unwrap_or_else(|| root.to_path_buf());
145 let worktrees_base = main_root.join(&config.worktrees.dir);
146 let wt = ensure_worktree(root, &worktrees_base, branch)?;
147 sync_agent_dirs(root, &wt, &config.worktrees.agent_dirs, warnings);
148 Ok(wt)
149}
150
151pub fn list_worktrees_with_tickets(
152 root: &Path,
153 tickets_dir: &Path,
154) -> Result<Vec<(PathBuf, String, Option<Ticket>)>> {
155 let worktrees = list_ticket_worktrees(root)?;
156 let tickets = load_all_from_git(root, tickets_dir).unwrap_or_default();
157 let result = worktrees.into_iter().map(|(wt_path, branch)| {
158 let ticket = tickets.iter().find(|t| {
159 t.frontmatter.branch.as_deref() == Some(branch.as_str())
160 || crate::ticket_fmt::branch_name_from_path(&t.path).as_deref() == Some(branch.as_str())
161 }).cloned();
162 (wt_path, branch, ticket)
163 }).collect();
164 Ok(result)
165}