Skip to main content

sparrow/tools/
git.rs

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        // Update risk based on action
47        let mut git_args = vec![action.to_string()];
48        git_args.extend(extra_args);
49
50        // Special handling for commit
51        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        // Cleanup temp file
67        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}