use crate::tool::Tool;
use async_trait::async_trait;
use serde_json::json;
use tokio::process::Command;
pub struct GitTool;
impl GitTool {
pub fn new() -> Self {
Self
}
}
impl Default for GitTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for GitTool {
fn name(&self) -> &str {
"git"
}
fn description(&self) -> &str {
"Execute git commands (clone, status, add, commit, push, pull, etc.)"
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["clone", "status", "add", "commit", "push", "pull", "branch", "checkout", "log"],
"description": "Git operation to perform"
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Additional arguments for the git command"
},
"message": {
"type": "string",
"description": "Commit message (for commit operation)"
},
"repo_url": {
"type": "string",
"description": "Repository URL (for clone operation)"
},
"directory": {
"type": "string",
"description": "Target directory (for clone) or repository directory"
}
},
"required": ["operation"]
})
}
fn requires_network(&self) -> bool {
true }
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
let operation = args
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'operation' parameter"))?;
let operation_args = args
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let mut cmd = Command::new("git");
cmd.arg(operation);
match operation {
"clone" => {
if let Some(url) = args.get("repo_url").and_then(|v| v.as_str()) {
cmd.arg(url);
} else {
anyhow::bail!("clone operation requires 'repo_url' parameter");
}
if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
cmd.arg(dir);
}
}
"commit" => {
if let Some(msg) = args.get("message").and_then(|v| v.as_str()) {
cmd.arg("-m").arg(msg);
}
}
_ => {}
}
for arg in operation_args {
cmd.arg(arg);
}
if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
cmd.current_dir(dir);
}
let output = cmd.output().await?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(json!({
"success": output.status.success(),
"stdout": stdout,
"stderr": stderr,
"exit_code": output.status.code(),
"operation": operation
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_git_status() {
let tool = GitTool::new();
let result = tool
.execute(json!({
"operation": "status",
"directory": "."
}))
.await
.unwrap();
if result["success"].as_bool().unwrap_or(false) {
assert!(result["stdout"].as_str().is_some());
}
}
#[tokio::test]
async fn test_git_version() {
let tool = GitTool::new();
let result = tool
.execute(json!({
"operation": "version"
}))
.await
.unwrap();
assert_eq!(result["success"], true);
assert!(result["stdout"].as_str().unwrap().contains("git version"));
}
}