git_worktree_cli/
git.rs

1use colored::Colorize;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5use crate::error::{Error, Result};
6
7/// Execute a git command with real-time output streaming
8pub fn execute_streaming(args: &[&str], cwd: Option<&Path>) -> Result<()> {
9    let mut cmd = Command::new("git");
10    cmd.args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit());
11
12    if let Some(dir) = cwd {
13        cmd.current_dir(dir);
14    }
15
16    let status = cmd
17        .status()
18        .map_err(|e| Error::git(format!("Failed to execute git command: {}", e)))?;
19
20    if !status.success() {
21        return Err(Error::git(format!(
22            "Git command failed with exit code: {:?}",
23            status.code()
24        )));
25    }
26
27    Ok(())
28}
29
30/// Execute a git command and capture output
31pub fn execute_capture(args: &[&str], cwd: Option<&Path>) -> Result<String> {
32    let mut cmd = Command::new("git");
33    cmd.args(args);
34
35    if let Some(dir) = cwd {
36        cmd.current_dir(dir);
37    }
38
39    let output = cmd
40        .output()
41        .map_err(|e| Error::git(format!("Failed to execute git command: {}", e)))?;
42
43    if !output.status.success() {
44        let stderr = String::from_utf8_lossy(&output.stderr);
45        return Err(Error::git(format!("Git command failed: {}", stderr)));
46    }
47
48    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
49}
50
51/// Clone a repository with streaming output
52pub fn clone(repo_url: &str, target_dir: &str) -> Result<()> {
53    println!("{}", format!("Cloning {}...", repo_url).cyan());
54    execute_streaming(&["clone", repo_url, target_dir], None)
55}
56
57/// Get the default branch name of a repository
58pub fn get_default_branch(repo_path: &Path) -> Result<String> {
59    execute_capture(&["symbolic-ref", "--short", "HEAD"], Some(repo_path))
60}
61
62/// Add a new worktree
63/// List all worktrees
64pub fn list_worktrees(git_dir: Option<&Path>) -> Result<Vec<Worktree>> {
65    let output = execute_capture(&["worktree", "list", "--porcelain"], git_dir)?;
66    parse_worktree_list(&output)
67}
68
69/// Prune worktree administrative files
70///
71/// Removes worktree references from .git/worktrees that are no longer valid
72pub fn prune_worktrees(git_dir: &Path) -> Result<()> {
73    execute_streaming(&["worktree", "prune"], Some(git_dir))
74}
75
76/// Remove a worktree
77/// Delete a branch
78/// Check if a branch exists
79pub fn branch_exists(git_dir: &Path, branch_name: &str) -> Result<(bool, bool)> {
80    let local = execute_capture(&["branch", "--list", branch_name], Some(git_dir)).unwrap_or_default();
81
82    let remote = execute_capture(
83        &["branch", "-r", "--list", &format!("origin/{}", branch_name)],
84        Some(git_dir),
85    )
86    .unwrap_or_default();
87
88    Ok((!local.is_empty(), !remote.is_empty()))
89}
90
91/// Get the current git root directory
92pub fn get_git_root() -> Result<Option<PathBuf>> {
93    match execute_capture(&["rev-parse", "--show-toplevel"], None) {
94        Ok(path) => Ok(Some(PathBuf::from(path))),
95        Err(_) => Ok(None),
96    }
97}
98
99#[derive(Debug, Clone)]
100pub struct Worktree {
101    pub path: PathBuf,
102    pub head: String,
103    pub branch: Option<String>,
104    pub bare: bool,
105}
106
107fn parse_worktree_list(output: &str) -> Result<Vec<Worktree>> {
108    let mut worktrees = Vec::new();
109    let mut current_worktree: Option<PartialWorktree> = None;
110
111    #[derive(Default)]
112    struct PartialWorktree {
113        path: Option<PathBuf>,
114        head: Option<String>,
115        branch: Option<String>,
116        bare: bool,
117    }
118
119    impl PartialWorktree {
120        fn into_worktree(self) -> Option<Worktree> {
121            match (self.path, self.head) {
122                (Some(path), Some(head)) => Some(Worktree {
123                    path,
124                    head,
125                    branch: self.branch,
126                    bare: self.bare,
127                }),
128                _ => None,
129            }
130        }
131    }
132
133    for line in output.lines() {
134        match parse_worktree_line(line) {
135            WorktreeLine::New(path) => {
136                if let Some(wt) = current_worktree.take() {
137                    if let Some(worktree) = wt.into_worktree() {
138                        worktrees.push(worktree);
139                    }
140                }
141                current_worktree = Some(PartialWorktree {
142                    path: Some(path),
143                    ..Default::default()
144                });
145            }
146            WorktreeLine::Head(head) => {
147                if let Some(ref mut wt) = current_worktree {
148                    wt.head = Some(head);
149                }
150            }
151            WorktreeLine::Branch(branch) => {
152                if let Some(ref mut wt) = current_worktree {
153                    wt.branch = Some(branch);
154                }
155            }
156            WorktreeLine::Bare => {
157                if let Some(ref mut wt) = current_worktree {
158                    wt.bare = true;
159                }
160            }
161            WorktreeLine::Other => {}
162        }
163    }
164
165    // Don't forget the last worktree
166    if let Some(wt) = current_worktree {
167        if let Some(worktree) = wt.into_worktree() {
168            worktrees.push(worktree);
169        }
170    }
171
172    Ok(worktrees)
173}
174
175enum WorktreeLine {
176    New(PathBuf),
177    Head(String),
178    Branch(String),
179    Bare,
180    Other,
181}
182
183fn parse_worktree_line(line: &str) -> WorktreeLine {
184    if let Some(path) = line.strip_prefix("worktree ") {
185        WorktreeLine::New(PathBuf::from(path))
186    } else if let Some(head) = line.strip_prefix("HEAD ") {
187        WorktreeLine::Head(head.to_string())
188    } else if let Some(branch) = line.strip_prefix("branch ") {
189        WorktreeLine::Branch(branch.to_string())
190    } else if line == "bare" {
191        WorktreeLine::Bare
192    } else {
193        WorktreeLine::Other
194    }
195}