1use crate::builtin_tools::BuiltinTool;
6use crate::types::{Layer3Result, ToolCategory};
7use async_trait::async_trait;
8use std::process::Command;
9
10fn run_git(args: &[&str], cwd: Option<&str>) -> Layer3Result<String> {
12 let mut cmd = Command::new("git");
13 cmd.args(args);
14
15 if let Some(dir) = cwd {
16 cmd.current_dir(dir);
17 }
18
19 let output = cmd
20 .output()
21 .map_err(|e| anyhow::anyhow!("Failed to execute git: {}", e))?;
22
23 if output.status.success() {
24 String::from_utf8(output.stdout).map_err(|e| anyhow::anyhow!("Invalid UTF-8 output: {}", e))
25 } else {
26 let stderr = String::from_utf8_lossy(&output.stderr);
27 Err(anyhow::anyhow!("Git command failed: {}", stderr))
28 }
29}
30
31pub struct GitStatusTool;
37
38#[async_trait]
39impl BuiltinTool for GitStatusTool {
40 fn name(&self) -> &str {
41 "git_status"
42 }
43
44 fn description(&self) -> &str {
45 "Show the working tree status. Lists modified, staged, and untracked files."
46 }
47
48 fn parameters_schema(&self) -> serde_json::Value {
49 serde_json::json!({
50 "type": "object",
51 "properties": {
52 "path": {
53 "type": "string",
54 "description": "Repository path (default: current directory)"
55 },
56 "short": {
57 "type": "boolean",
58 "description": "Use short format (default: false)"
59 }
60 }
61 })
62 }
63
64 fn category(&self) -> ToolCategory {
65 ToolCategory::VersionControl
66 }
67
68 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
69 let path = args["path"].as_str();
70 let short = args["short"].as_bool().unwrap_or(false);
71
72 let mut git_args = vec!["status"];
73 if short {
74 git_args.push("--short");
75 }
76
77 run_git(&git_args, path)
78 }
79}
80
81pub struct GitLogTool;
87
88#[async_trait]
89impl BuiltinTool for GitLogTool {
90 fn name(&self) -> &str {
91 "git_log"
92 }
93
94 fn description(&self) -> &str {
95 "Show commit logs. Supports various format options."
96 }
97
98 fn parameters_schema(&self) -> serde_json::Value {
99 serde_json::json!({
100 "type": "object",
101 "properties": {
102 "path": {
103 "type": "string",
104 "description": "Repository path (default: current directory)"
105 },
106 "count": {
107 "type": "integer",
108 "description": "Number of commits to show (default: 10)"
109 },
110 "oneline": {
111 "type": "boolean",
112 "description": "Use one-line format (default: true)"
113 },
114 "branch": {
115 "type": "string",
116 "description": "Branch name (default: current branch)"
117 }
118 }
119 })
120 }
121
122 fn category(&self) -> ToolCategory {
123 ToolCategory::VersionControl
124 }
125
126 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
127 let path = args["path"].as_str();
128 let count = args["count"].as_u64().unwrap_or(10);
129 let oneline = args["oneline"].as_bool().unwrap_or(true);
130 let branch = args["branch"].as_str();
131
132 let count_arg = format!("-{}", count);
133 let mut git_args = vec!["log", &count_arg];
134 if oneline {
135 git_args.push("--oneline");
136 }
137 if let Some(b) = branch {
138 git_args.push(b);
139 }
140
141 run_git(&git_args, path)
142 }
143}
144
145pub struct GitDiffTool;
151
152#[async_trait]
153impl BuiltinTool for GitDiffTool {
154 fn name(&self) -> &str {
155 "git_diff"
156 }
157
158 fn description(&self) -> &str {
159 "Show changes between commits, commit and working tree, etc."
160 }
161
162 fn parameters_schema(&self) -> serde_json::Value {
163 serde_json::json!({
164 "type": "object",
165 "properties": {
166 "path": {
167 "type": "string",
168 "description": "Repository path (default: current directory)"
169 },
170 "file": {
171 "type": "string",
172 "description": "Specific file to diff"
173 },
174 "staged": {
175 "type": "boolean",
176 "description": "Show staged changes (--cached)"
177 },
178 "commit": {
179 "type": "string",
180 "description": "Commit hash or branch to compare"
181 }
182 }
183 })
184 }
185
186 fn category(&self) -> ToolCategory {
187 ToolCategory::VersionControl
188 }
189
190 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
191 let path = args["path"].as_str();
192 let file = args["file"].as_str();
193 let staged = args["staged"].as_bool().unwrap_or(false);
194 let commit = args["commit"].as_str();
195
196 let mut git_args = vec!["diff"];
197 if staged {
198 git_args.push("--cached");
199 }
200 if let Some(c) = commit {
201 git_args.push(c);
202 }
203 if let Some(f) = file {
204 git_args.push("--");
205 git_args.push(f);
206 }
207
208 run_git(&git_args, path)
209 }
210}
211
212pub struct GitBranchTool;
218
219#[async_trait]
220impl BuiltinTool for GitBranchTool {
221 fn name(&self) -> &str {
222 "git_branch"
223 }
224
225 fn description(&self) -> &str {
226 "List, create, or delete branches."
227 }
228
229 fn parameters_schema(&self) -> serde_json::Value {
230 serde_json::json!({
231 "type": "object",
232 "properties": {
233 "path": {
234 "type": "string",
235 "description": "Repository path (default: current directory)"
236 },
237 "action": {
238 "type": "string",
239 "enum": ["list", "create", "delete"],
240 "description": "Action to perform (default: list)"
241 },
242 "branch_name": {
243 "type": "string",
244 "description": "Branch name for create/delete"
245 },
246 "all": {
247 "type": "boolean",
248 "description": "List all branches including remote (default: false)"
249 }
250 }
251 })
252 }
253
254 fn category(&self) -> ToolCategory {
255 ToolCategory::VersionControl
256 }
257
258 fn requires_confirmation(&self) -> bool {
259 true }
261
262 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
263 let path = args["path"].as_str();
264 let action = args["action"].as_str().unwrap_or("list");
265 let branch_name = args["branch_name"].as_str();
266 let all = args["all"].as_bool().unwrap_or(false);
267
268 let git_args = match action {
269 "list" => {
270 let mut args = vec!["branch"];
271 if all {
272 args.push("-a");
273 }
274 args
275 }
276 "create" => {
277 let name = branch_name
278 .ok_or_else(|| anyhow::anyhow!("branch_name required for create"))?;
279 vec!["branch", name]
280 }
281 "delete" => {
282 let name = branch_name
283 .ok_or_else(|| anyhow::anyhow!("branch_name required for delete"))?;
284 vec!["branch", "-D", name]
285 }
286 _ => return Err(anyhow::anyhow!("Invalid action: {}", action)),
287 };
288
289 run_git(&git_args, path)
290 }
291}
292
293pub struct GitAddTool;
299
300#[async_trait]
301impl BuiltinTool for GitAddTool {
302 fn name(&self) -> &str {
303 "git_add"
304 }
305
306 fn description(&self) -> &str {
307 "Add file contents to the index."
308 }
309
310 fn parameters_schema(&self) -> serde_json::Value {
311 serde_json::json!({
312 "type": "object",
313 "properties": {
314 "path": {
315 "type": "string",
316 "description": "Repository path (default: current directory)"
317 },
318 "files": {
319 "type": "array",
320 "items": {"type": "string"},
321 "description": "Files to add (default: ['.'])"
322 }
323 }
324 })
325 }
326
327 fn category(&self) -> ToolCategory {
328 ToolCategory::VersionControl
329 }
330
331 fn requires_confirmation(&self) -> bool {
332 true
333 }
334
335 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
336 let path = args["path"].as_str();
337 let files: Vec<&str> = if let Some(arr) = args["files"].as_array() {
338 arr.iter().filter_map(|v| v.as_str()).collect()
339 } else {
340 vec!["."]
341 };
342
343 let mut git_args = vec!["add", "--"];
344 git_args.extend(files);
345
346 run_git(&git_args, path)
347 }
348}
349
350pub struct GitCommitTool;
356
357#[async_trait]
358impl BuiltinTool for GitCommitTool {
359 fn name(&self) -> &str {
360 "git_commit"
361 }
362
363 fn description(&self) -> &str {
364 "Record changes to the repository."
365 }
366
367 fn parameters_schema(&self) -> serde_json::Value {
368 serde_json::json!({
369 "type": "object",
370 "properties": {
371 "path": {
372 "type": "string",
373 "description": "Repository path (default: current directory)"
374 },
375 "message": {
376 "type": "string",
377 "description": "Commit message"
378 }
379 },
380 "required": ["message"]
381 })
382 }
383
384 fn category(&self) -> ToolCategory {
385 ToolCategory::VersionControl
386 }
387
388 fn requires_confirmation(&self) -> bool {
389 true
390 }
391
392 fn is_dangerous(&self) -> bool {
393 true
394 }
395
396 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
397 let path = args["path"].as_str();
398 let message = args["message"]
399 .as_str()
400 .ok_or_else(|| anyhow::anyhow!("Missing message parameter"))?;
401
402 run_git(&["commit", "-m", message], path)
403 }
404}
405
406pub struct GitShowTool;
412
413#[async_trait]
414impl BuiltinTool for GitShowTool {
415 fn name(&self) -> &str {
416 "git_show"
417 }
418
419 fn description(&self) -> &str {
420 "Show various types of objects (commits, tags, trees)."
421 }
422
423 fn parameters_schema(&self) -> serde_json::Value {
424 serde_json::json!({
425 "type": "object",
426 "properties": {
427 "path": {
428 "type": "string",
429 "description": "Repository path (default: current directory)"
430 },
431 "object": {
432 "type": "string",
433 "description": "Object to show (commit hash, tag, etc.)"
434 },
435 "stat": {
436 "type": "boolean",
437 "description": "Show diffstat instead of full diff (default: true)"
438 }
439 },
440 "required": ["object"]
441 })
442 }
443
444 fn category(&self) -> ToolCategory {
445 ToolCategory::VersionControl
446 }
447
448 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
449 let path = args["path"].as_str();
450 let object = args["object"]
451 .as_str()
452 .ok_or_else(|| anyhow::anyhow!("Missing object parameter"))?;
453 let stat = args["stat"].as_bool().unwrap_or(true);
454
455 let mut git_args = vec!["show"];
456 if stat {
457 git_args.push("--stat");
458 }
459 git_args.push(object);
460
461 run_git(&git_args, path)
462 }
463}
464
465pub struct GitStashTool;
471
472#[async_trait]
473impl BuiltinTool for GitStashTool {
474 fn name(&self) -> &str {
475 "git_stash"
476 }
477
478 fn description(&self) -> &str {
479 "Stash the changes in a dirty working directory."
480 }
481
482 fn parameters_schema(&self) -> serde_json::Value {
483 serde_json::json!({
484 "type": "object",
485 "properties": {
486 "path": {
487 "type": "string",
488 "description": "Repository path (default: current directory)"
489 },
490 "action": {
491 "type": "string",
492 "enum": ["push", "pop", "list", "drop"],
493 "description": "Action to perform (default: list)"
494 },
495 "message": {
496 "type": "string",
497 "description": "Stash message (for push)"
498 }
499 }
500 })
501 }
502
503 fn category(&self) -> ToolCategory {
504 ToolCategory::VersionControl
505 }
506
507 fn requires_confirmation(&self) -> bool {
508 true
509 }
510
511 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
512 let path = args["path"].as_str();
513 let action = args["action"].as_str().unwrap_or("list");
514 let message = args["message"].as_str();
515
516 let git_args = match action {
517 "push" => {
518 let mut args = vec!["stash", "push"];
519 if let Some(msg) = message {
520 args.push("-m");
521 args.push(msg);
522 }
523 args
524 }
525 "pop" => vec!["stash", "pop"],
526 "list" => vec!["stash", "list"],
527 "drop" => vec!["stash", "drop"],
528 _ => return Err(anyhow::anyhow!("Invalid action: {}", action)),
529 };
530
531 run_git(&git_args, path)
532 }
533}
534
535#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_git_status_category() {
545 let tool = GitStatusTool;
546 assert_eq!(tool.category(), ToolCategory::VersionControl);
547 }
548
549 #[test]
550 fn test_git_commit_is_dangerous() {
551 let tool = GitCommitTool;
552 assert!(tool.is_dangerous());
553 assert!(tool.requires_confirmation());
554 }
555
556 #[test]
557 fn test_git_add_requires_confirmation() {
558 let tool = GitAddTool;
559 assert!(tool.requires_confirmation());
560 }
561}