1use colored::Colorize;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5use crate::error::{Error, Result};
6
7pub 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
30pub 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
51pub 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
57pub fn get_default_branch(repo_path: &Path) -> Result<String> {
59 execute_capture(&["symbolic-ref", "--short", "HEAD"], Some(repo_path))
60}
61
62pub 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
69pub fn prune_worktrees(git_dir: &Path) -> Result<()> {
73 execute_streaming(&["worktree", "prune"], Some(git_dir))
74}
75
76pub 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
91pub 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 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}