Skip to main content

flow_git/
worktree.rs

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
21/// Create a new git worktree.
22///
23/// # Errors
24///
25/// Returns an error if the worktree cannot be created.
26pub 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
59/// List all git worktrees in the current repository.
60///
61/// # Errors
62///
63/// Returns an error if the worktrees cannot be listed.
64pub 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
100/// Remove a git worktree.
101///
102/// # Errors
103///
104/// Returns an error if the worktree cannot be removed.
105pub 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}