1use crate::agent::git::is_git_repo;
2use serde_json::Value;
3use std::path::Path;
4use std::process::Command;
5
6pub async fn execute(args: &Value) -> Result<String, String> {
10 let message = args
11 .get("message")
12 .and_then(|v| v.as_str())
13 .ok_or_else(|| "Missing required argument: 'message'".to_string())?;
14
15 let repo_path = Path::new(".");
16 if !is_git_repo(repo_path) {
17 return Err("Current directory is not a Git repository".to_string());
18 }
19
20 let add_status = std::process::Command::new("git")
22 .arg("add")
23 .arg("-A")
24 .stdout(std::process::Stdio::null())
25 .stderr(std::process::Stdio::null())
26 .status()
27 .map_err(|e| format!("Failed to run git add: {e}"))?;
28
29 if !add_status.success() {
30 return Err("Git 'add' failed".to_string());
31 }
32
33 let commit_status = std::process::Command::new("git")
35 .arg("commit")
36 .arg("-m")
37 .arg(message)
38 .stdout(std::process::Stdio::null())
39 .stderr(std::process::Stdio::null())
40 .status()
41 .map_err(|e| format!("Failed to run git commit: {e}"))?;
42
43 if commit_status.success() {
44 Ok(format!("Successfully committed changes: '{message}'"))
45 } else {
46 Err("Git 'commit' failed (maybe nothing to commit or malformed message?)".to_string())
47 }
48}
49
50pub async fn execute_push(_args: &Value) -> Result<String, String> {
52 let repo_path = Path::new(".");
53 if !is_git_repo(repo_path) {
54 return Err("Current directory is not a Git repository".to_string());
55 }
56
57 let output = Command::new("git")
58 .args(["push", "origin", "HEAD"])
59 .output()
60 .map_err(|e| format!("Failed to execution git push: {e}"))?;
61
62 if output.status.success() {
63 Ok("Changes successfully pushed to remote origin.".to_string())
64 } else {
65 let stderr = String::from_utf8_lossy(&output.stderr);
66 Err(format!("Git push failed: {}", stderr))
67 }
68}
69
70pub async fn execute_remote(args: &Value) -> Result<String, String> {
72 let action = args
73 .get("action")
74 .and_then(|v| v.as_str())
75 .unwrap_or("list");
76 let repo_path = Path::new(".");
77 if !is_git_repo(repo_path) {
78 return Err("Current directory is not a Git repository".to_string());
79 }
80
81 match action {
82 "list" => {
83 let output = Command::new("git")
84 .arg("remote")
85 .arg("-v")
86 .output()
87 .map_err(|e| format!("Failed to list remotes: {e}"))?;
88 Ok(String::from_utf8_lossy(&output.stdout).to_string())
89 }
90 "add" => {
91 let name = args
92 .get("name")
93 .and_then(|v| v.as_str())
94 .ok_or("Missing name for add")?;
95 let url = args
96 .get("url")
97 .and_then(|v| v.as_str())
98 .ok_or("Missing url for add")?;
99 let status = std::process::Command::new("git")
100 .args(["remote", "add", name, url])
101 .stdout(std::process::Stdio::null())
102 .stderr(std::process::Stdio::null())
103 .status()
104 .map_err(|e| format!("Failed to add remote: {e}"))?;
105 if status.success() {
106 Ok(format!("Successfully added remote '{}' -> {}", name, url))
107 } else {
108 Err("Failed to add remote (it might already exist)".to_string())
109 }
110 }
111 "remove" => {
112 let name = args
113 .get("name")
114 .and_then(|v| v.as_str())
115 .ok_or("Missing name for remove")?;
116 let status = std::process::Command::new("git")
117 .args(["remote", "remove", name])
118 .stdout(std::process::Stdio::null())
119 .stderr(std::process::Stdio::null())
120 .status()
121 .map_err(|e| format!("Failed to remove remote: {e}"))?;
122 if status.success() {
123 Ok(format!("Successfully removed remote '{}'", name))
124 } else {
125 Err("Failed to remove remote".to_string())
126 }
127 }
128 _ => Err(format!("Unknown action: {}", action)),
129 }
130}
131
132pub async fn execute_worktree(args: &Value) -> Result<String, String> {
137 let action = args
138 .get("action")
139 .and_then(|v| v.as_str())
140 .ok_or_else(|| "Missing required argument: 'action' (list|add|remove|prune)".to_string())?;
141
142 let repo_path = Path::new(".");
143 if !is_git_repo(repo_path) {
144 return Err("Current directory is not a Git repository".to_string());
145 }
146
147 match action {
148 "list" => {
149 let output = Command::new("git")
150 .args(["worktree", "list"])
151 .output()
152 .map_err(|e| format!("Failed to list worktrees: {e}"))?;
153 let out = String::from_utf8_lossy(&output.stdout).to_string();
154 if out.trim().is_empty() {
155 Ok("No worktrees (only main working tree)".to_string())
156 } else {
157 Ok(out)
158 }
159 }
160
161 "add" => {
162 let path = args
163 .get("path")
164 .and_then(|v| v.as_str())
165 .ok_or_else(|| "Missing 'path' for worktree add".to_string())?;
166
167 let branch_arg = args.get("branch").and_then(|v| v.as_str());
169 let branch = branch_arg.unwrap_or_else(|| {
170 std::path::Path::new(path)
171 .file_name()
172 .and_then(|s| s.to_str())
173 .unwrap_or(path)
174 });
175
176 let branch_check = Command::new("git")
178 .args(["branch", "--list", branch])
179 .output()
180 .map_err(|e| format!("Failed to check branch: {e}"))?;
181 let branch_exists = !String::from_utf8_lossy(&branch_check.stdout)
182 .trim()
183 .is_empty();
184
185 let output = if branch_exists {
186 Command::new("git")
188 .args(["worktree", "add", path, branch])
189 .output()
190 .map_err(|e| format!("Failed to add worktree: {e}"))?
191 } else {
192 Command::new("git")
194 .args(["worktree", "add", path, "-b", branch])
195 .output()
196 .map_err(|e| format!("Failed to add worktree: {e}"))?
197 };
198
199 if output.status.success() {
200 Ok(format!(
201 "Worktree created at '{path}' on branch '{branch}'.\n\
202 Work there independently, then commit and merge back when ready."
203 ))
204 } else {
205 let stderr = String::from_utf8_lossy(&output.stderr);
206 Err(format!("Failed to create worktree: {}", stderr.trim()))
207 }
208 }
209
210 "remove" => {
211 let path = args
212 .get("path")
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| "Missing 'path' for worktree remove".to_string())?;
215
216 let output = Command::new("git")
217 .args(["worktree", "remove", path])
218 .output()
219 .map_err(|e| format!("Failed to remove worktree: {e}"))?;
220
221 if output.status.success() {
222 Ok(format!("Worktree '{path}' removed."))
223 } else {
224 let stderr = String::from_utf8_lossy(&output.stderr);
225 if stderr.contains("contains modified or untracked files") {
227 Err(format!(
228 "Worktree '{path}' has uncommitted changes. \
229 Commit or stash them first, or use action=remove with force=true."
230 ))
231 } else {
232 Err(format!("Failed to remove worktree: {}", stderr.trim()))
233 }
234 }
235 }
236
237 "prune" => {
238 let output = Command::new("git")
239 .args(["worktree", "prune", "-v"])
240 .output()
241 .map_err(|e| format!("Failed to prune worktrees: {e}"))?;
242 let out = String::from_utf8_lossy(&output.stdout).to_string();
243 Ok(if out.trim().is_empty() {
244 "Nothing to prune.".to_string()
245 } else {
246 out
247 })
248 }
249
250 _ => Err(format!(
251 "Unknown worktree action '{action}'. Use: list | add | remove | prune"
252 )),
253 }
254}