Skip to main content

pawan/tools/git/
status.rs

1use super::super::Tool;
2use super::run_git;
3use async_trait::async_trait;
4use serde_json::{json, Value};
5use std::path::PathBuf;
6
7/// Tool for checking git status
8///
9/// This tool provides information about the current git repository status,
10/// including modified files, untracked files, and branch information.
11///
12/// # Fields
13/// - `workspace_root`: The root directory of the workspace
14pub struct GitStatusTool {
15    workspace_root: PathBuf,
16}
17
18impl GitStatusTool {
19    pub fn new(workspace_root: PathBuf) -> Self {
20        Self { workspace_root }
21    }
22}
23
24#[async_trait]
25impl Tool for GitStatusTool {
26    fn name(&self) -> &str {
27        "git_status"
28    }
29
30    fn description(&self) -> &str {
31        "Get the current git status showing staged, unstaged, and untracked files."
32    }
33
34    fn mutating(&self) -> bool {
35        false // Git status is read-only
36    }
37
38    fn parameters_schema(&self) -> Value {
39        json!({
40            "type": "object",
41            "properties": {
42                "short": {
43                    "type": "boolean",
44                    "description": "Use short format output (default: false)"
45                }
46            },
47            "required": []
48        })
49    }
50
51    fn thulp_definition(&self) -> thulp_core::ToolDefinition {
52        use thulp_core::{Parameter, ParameterType};
53        thulp_core::ToolDefinition::builder("git_status")
54            .description(self.description())
55            .parameter(
56                Parameter::builder("short")
57                    .param_type(ParameterType::Boolean)
58                    .required(false)
59                    .description("Use short format output (default: false)")
60                    .build(),
61            )
62            .build()
63    }
64
65    async fn execute(&self, args: Value) -> crate::Result<Value> {
66        let short = args["short"].as_bool().unwrap_or(false);
67
68        let mut git_args = vec!["status"];
69        if short {
70            git_args.push("-s");
71        }
72
73        let (success, stdout, stderr) = run_git(&self.workspace_root, &git_args).await?;
74
75        if !success {
76            return Err(crate::PawanError::Git(format!(
77                "git status failed: {}",
78                stderr
79            )));
80        }
81
82        // Also get branch info
83        let (_, branch_output, _) =
84            run_git(&self.workspace_root, &["branch", "--show-current"]).await?;
85        let branch = branch_output.trim().to_string();
86
87        // Check if repo is clean
88        let (_, porcelain, _) = run_git(&self.workspace_root, &["status", "--porcelain"]).await?;
89        let is_clean = porcelain.trim().is_empty();
90
91        Ok(json!({
92            "status": stdout.trim(),
93            "branch": branch,
94            "is_clean": is_clean,
95            "success": true
96        }))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use serde_json::json;
104    use tempfile::TempDir;
105    use tokio::process::Command;
106
107    async fn setup_git_repo() -> TempDir {
108        let temp_dir = TempDir::new().unwrap();
109
110        Command::new("git")
111            .args(["init"])
112            .current_dir(temp_dir.path())
113            .output()
114            .await
115            .unwrap();
116
117        Command::new("git")
118            .args(["config", "user.email", "test@test.com"])
119            .current_dir(temp_dir.path())
120            .output()
121            .await
122            .unwrap();
123
124        Command::new("git")
125            .args(["config", "user.name", "Test User"])
126            .current_dir(temp_dir.path())
127            .output()
128            .await
129            .unwrap();
130
131        temp_dir
132    }
133
134    #[tokio::test]
135    async fn test_git_status_empty_repo() {
136        let temp_dir = setup_git_repo().await;
137
138        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
139        let result = tool.execute(json!({})).await.unwrap();
140
141        assert!(result["success"].as_bool().unwrap());
142        assert!(result["is_clean"].as_bool().unwrap());
143    }
144
145    #[tokio::test]
146    async fn test_git_status_with_untracked() {
147        let temp_dir = setup_git_repo().await;
148
149        // Create an untracked file
150        std::fs::write(temp_dir.path().join("test.txt"), "hello").unwrap();
151
152        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
153        let result = tool.execute(json!({})).await.unwrap();
154
155        assert!(result["success"].as_bool().unwrap());
156        assert!(!result["is_clean"].as_bool().unwrap());
157    }
158
159    #[tokio::test]
160    async fn test_git_status_tool_exists() {
161        let temp_dir = setup_git_repo().await;
162        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
163        assert_eq!(tool.name(), "git_status");
164    }
165
166    #[tokio::test]
167    async fn test_git_status_detects_modified_file() {
168        // GitStatusTool should report modified files that were previously committed
169        let temp_dir = setup_git_repo().await;
170        std::fs::write(temp_dir.path().join("tracked.txt"), "v1").unwrap();
171        Command::new("git")
172            .args(["add", "."])
173            .current_dir(temp_dir.path())
174            .output()
175            .await
176            .unwrap();
177        Command::new("git")
178            .args(["commit", "-m", "init tracked"])
179            .current_dir(temp_dir.path())
180            .output()
181            .await
182            .unwrap();
183
184        // Modify the tracked file
185        std::fs::write(temp_dir.path().join("tracked.txt"), "v2").unwrap();
186
187        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
188        let result = tool.execute(json!({})).await.unwrap();
189        // Verify the status includes the modified file
190        let serialized = result.to_string();
191        assert!(
192            serialized.contains("tracked.txt"),
193            "status must mention modified tracked.txt, got: {}",
194            serialized
195        );
196    }
197}