1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum WorktreeError {
6 #[error("Git error: {0}")]
7 Git(String),
8 #[error("IO error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("Core error: {0}")]
11 Core(#[from] flow_core::FlowError),
12}
13
14#[derive(Debug, Clone)]
15pub struct Worktree {
16 pub name: String,
17 pub path: PathBuf,
18 pub branch: String,
19}
20
21pub fn create(name: &str, base: &str) -> Result<PathBuf, WorktreeError> {
27 let config = flow_core::Config::load()?;
28 let project_name = std::env::current_dir()
29 .ok()
30 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
31 .unwrap_or_else(|| "project".to_string());
32
33 let worktree_path = config
34 .projects_dir
35 .join(format!("{project_name}-worktrees"))
36 .join(name);
37
38 let output = std::process::Command::new("git")
39 .args([
40 "worktree",
41 "add",
42 "-b",
43 name,
44 worktree_path.to_str().unwrap_or("."),
45 base,
46 ])
47 .output()?;
48
49 if !output.status.success() {
50 return Err(WorktreeError::Git(
51 String::from_utf8_lossy(&output.stderr).to_string(),
52 ));
53 }
54
55 tracing::info!("Created worktree at {:?}", worktree_path);
56 Ok(worktree_path)
57}
58
59pub fn list() -> Result<Vec<Worktree>, WorktreeError> {
65 let output = std::process::Command::new("git")
66 .args(["worktree", "list", "--porcelain"])
67 .output()?;
68
69 if !output.status.success() {
70 return Err(WorktreeError::Git(
71 String::from_utf8_lossy(&output.stderr).to_string(),
72 ));
73 }
74
75 let mut worktrees = Vec::new();
76 let stdout = String::from_utf8_lossy(&output.stdout);
77 let mut current_path: Option<PathBuf> = None;
78 let mut current_branch: Option<String> = None;
79
80 for line in stdout.lines() {
81 if let Some(path) = line.strip_prefix("worktree ") {
82 current_path = Some(PathBuf::from(path));
83 } else if let Some(branch) = line.strip_prefix("branch refs/heads/") {
84 current_branch = Some(branch.to_string());
85 } else if line.is_empty() {
86 if let (Some(path), Some(branch)) = (current_path.take(), current_branch.take()) {
87 let name = path
88 .file_name()
89 .and_then(|n| n.to_str())
90 .unwrap_or("unknown")
91 .to_string();
92 worktrees.push(Worktree { name, path, branch });
93 }
94 }
95 }
96
97 Ok(worktrees)
98}
99
100pub fn remove(name: &str) -> Result<(), WorktreeError> {
106 let output = std::process::Command::new("git")
107 .args(["worktree", "remove", name])
108 .output()?;
109
110 if !output.status.success() {
111 return Err(WorktreeError::Git(
112 String::from_utf8_lossy(&output.stderr).to_string(),
113 ));
114 }
115
116 tracing::info!("Removed worktree: {}", name);
117 Ok(())
118}