1use crate::tool::Tool;
4use async_trait::async_trait;
5use serde_json::json;
6use tokio::process::Command;
7
8pub struct GitTool;
10
11impl GitTool {
12 pub fn new() -> Self {
13 Self
14 }
15}
16
17impl Default for GitTool {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23#[async_trait]
24impl Tool for GitTool {
25 fn name(&self) -> &str {
26 "git"
27 }
28
29 fn description(&self) -> &str {
30 "Execute git commands (clone, status, add, commit, push, pull, etc.)"
31 }
32
33 fn parameters_schema(&self) -> serde_json::Value {
34 json!({
35 "type": "object",
36 "properties": {
37 "operation": {
38 "type": "string",
39 "enum": ["clone", "status", "add", "commit", "push", "pull", "branch", "checkout", "log"],
40 "description": "Git operation to perform"
41 },
42 "args": {
43 "type": "array",
44 "items": { "type": "string" },
45 "description": "Additional arguments for the git command"
46 },
47 "message": {
48 "type": "string",
49 "description": "Commit message (for commit operation)"
50 },
51 "repo_url": {
52 "type": "string",
53 "description": "Repository URL (for clone operation)"
54 },
55 "directory": {
56 "type": "string",
57 "description": "Target directory (for clone) or repository directory"
58 }
59 },
60 "required": ["operation"]
61 })
62 }
63
64 fn requires_network(&self) -> bool {
65 true }
67
68 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
69 let operation = args
70 .get("operation")
71 .and_then(|v| v.as_str())
72 .ok_or_else(|| anyhow::anyhow!("Missing 'operation' parameter"))?;
73
74 let operation_args = args
75 .get("args")
76 .and_then(|v| v.as_array())
77 .map(|arr| {
78 arr.iter()
79 .filter_map(|v| v.as_str())
80 .map(|s| s.to_string())
81 .collect::<Vec<_>>()
82 })
83 .unwrap_or_default();
84
85 let mut cmd = Command::new("git");
86 cmd.arg(operation);
87
88 match operation {
90 "clone" => {
91 if let Some(url) = args.get("repo_url").and_then(|v| v.as_str()) {
92 cmd.arg(url);
93 } else {
94 anyhow::bail!("clone operation requires 'repo_url' parameter");
95 }
96 if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
97 cmd.arg(dir);
98 }
99 }
100 "commit" => {
101 if let Some(msg) = args.get("message").and_then(|v| v.as_str()) {
102 cmd.arg("-m").arg(msg);
103 }
104 }
105 _ => {}
106 }
107
108 for arg in operation_args {
110 cmd.arg(arg);
111 }
112
113 if let Some(dir) = args.get("directory").and_then(|v| v.as_str()) {
115 cmd.current_dir(dir);
116 }
117
118 let output = cmd.output().await?;
119
120 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
121 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
122
123 Ok(json!({
124 "success": output.status.success(),
125 "stdout": stdout,
126 "stderr": stderr,
127 "exit_code": output.status.code(),
128 "operation": operation
129 }))
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[tokio::test]
138 async fn test_git_status() {
139 let tool = GitTool::new();
140 let result = tool
141 .execute(json!({
142 "operation": "status",
143 "directory": "."
144 }))
145 .await
146 .unwrap();
147
148 if result["success"].as_bool().unwrap_or(false) {
150 assert!(result["stdout"].as_str().is_some());
151 }
152 }
153
154 #[tokio::test]
155 async fn test_git_version() {
156 let tool = GitTool::new();
157 let result = tool
158 .execute(json!({
159 "operation": "version"
160 }))
161 .await
162 .unwrap();
163
164 assert_eq!(result["success"], true);
165 assert!(result["stdout"].as_str().unwrap().contains("git version"));
166 }
167}