Skip to main content

enact_core/tool/
git.rs

1//! Git operations tool
2
3use crate::tool::Tool;
4use async_trait::async_trait;
5use serde_json::json;
6use tokio::process::Command;
7
8/// Git operations tool
9pub 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 // Git often requires network access
66    }
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        // Handle specific operations with special parameters
89        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        // Add additional args
109        for arg in operation_args {
110            cmd.arg(arg);
111        }
112
113        // Set working directory if specified
114        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        // Should succeed if we're in a git repo
149        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}