1use async_trait::async_trait;
2use serde_json::json;
3use std::path::PathBuf;
4use std::process::Command as StdCommand;
5
6use super::{Tool, ToolCtx, ToolResult};
7use crate::event::RiskLevel;
8
9pub struct Git;
10
11#[async_trait]
12impl Tool for Git {
13 fn name(&self) -> &str {
14 "git"
15 }
16 fn description(&self) -> &str {
17 "Git operations: status, diff, log, branch, checkout"
18 }
19 fn schema(&self) -> serde_json::Value {
20 json!({
21 "type": "object",
22 "properties": {
23 "action": {
24 "type": "string",
25 "enum": ["status", "diff", "log", "branch", "checkout", "add", "commit"]
26 },
27 "args": { "type": "array", "items": { "type": "string" } }
28 },
29 "required": ["action"]
30 })
31 }
32 fn risk(&self) -> RiskLevel {
33 RiskLevel::ReadOnly
34 }
35 async fn call(&self, args: serde_json::Value, ctx: &ToolCtx) -> anyhow::Result<ToolResult> {
36 let action = args["action"].as_str().unwrap_or("status");
37 let extra_args: Vec<String> = args["args"]
38 .as_array()
39 .map(|a| {
40 a.iter()
41 .filter_map(|v| v.as_str().map(String::from))
42 .collect()
43 })
44 .unwrap_or_default();
45
46 let mut git_args = vec![action.to_string()];
48 git_args.extend(extra_args);
49
50 let mut message_file: Option<PathBuf> = None;
52 if action == "commit" {
53 let msg = args["message"].as_str().unwrap_or("auto-commit");
54 let msg_file = ctx.workspace_root.join(".git").join("SPARROW_COMMIT_MSG");
55 std::fs::write(&msg_file, msg)?;
56 git_args.push("-F".into());
57 git_args.push(msg_file.to_string_lossy().to_string());
58 message_file = Some(msg_file);
59 }
60
61 let output = StdCommand::new("git")
62 .args(&git_args)
63 .current_dir(&ctx.workspace_root)
64 .output()?;
65
66 if let Some(f) = message_file {
68 let _ = std::fs::remove_file(f);
69 }
70
71 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
72 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
73
74 let result = if stdout.is_empty() { stderr } else { stdout };
75
76 Ok(ToolResult::text(result))
77 }
78}