Skip to main content

pawan/tools/git/
status.rs

1use super::run_git;
2use super::super::Tool;
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(Parameter::builder("short").param_type(ParameterType::Boolean).required(false)
56                .description("Use short format output (default: false)").build())
57            .build()
58    }
59
60    async fn execute(&self, args: Value) -> crate::Result<Value> {
61        let short = args["short"].as_bool().unwrap_or(false);
62
63        let mut git_args = vec!["status"];
64        if short {
65            git_args.push("-s");
66        }
67
68        let (success, stdout, stderr) = run_git(&self.workspace_root, &git_args).await?;
69
70        if !success {
71            return Err(crate::PawanError::Git(format!(
72                "git status failed: {}",
73                stderr
74            )));
75        }
76
77        // Also get branch info
78        let (_, branch_output, _) =
79            run_git(&self.workspace_root, &["branch", "--show-current"]).await?;
80        let branch = branch_output.trim().to_string();
81
82        // Check if repo is clean
83        let (_, porcelain, _) = run_git(&self.workspace_root, &["status", "--porcelain"]).await?;
84        let is_clean = porcelain.trim().is_empty();
85
86        Ok(json!({
87            "status": stdout.trim(),
88            "branch": branch,
89            "is_clean": is_clean,
90            "success": true
91        }))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use serde_json::json;
99    use tempfile::TempDir;
100    use tokio::process::Command;
101
102    async fn setup_git_repo() -> TempDir {
103        let temp_dir = TempDir::new().unwrap();
104
105        Command::new("git")
106            .args(["init"])
107            .current_dir(temp_dir.path())
108            .output()
109            .await
110            .unwrap();
111
112        Command::new("git")
113            .args(["config", "user.email", "test@test.com"])
114            .current_dir(temp_dir.path())
115            .output()
116            .await
117            .unwrap();
118
119        Command::new("git")
120            .args(["config", "user.name", "Test User"])
121            .current_dir(temp_dir.path())
122            .output()
123            .await
124            .unwrap();
125
126        temp_dir
127    }
128
129    #[tokio::test]
130    async fn test_git_status_empty_repo() {
131        let temp_dir = setup_git_repo().await;
132
133        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
134        let result = tool.execute(json!({})).await.unwrap();
135
136        assert!(result["success"].as_bool().unwrap());
137        assert!(result["is_clean"].as_bool().unwrap());
138    }
139
140    #[tokio::test]
141    async fn test_git_status_with_untracked() {
142        let temp_dir = setup_git_repo().await;
143
144        // Create an untracked file
145        std::fs::write(temp_dir.path().join("test.txt"), "hello").unwrap();
146
147        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
148        let result = tool.execute(json!({})).await.unwrap();
149
150        assert!(result["success"].as_bool().unwrap());
151        assert!(!result["is_clean"].as_bool().unwrap());
152    }
153
154    #[tokio::test]
155    async fn test_git_status_tool_exists() {
156        let temp_dir = setup_git_repo().await;
157        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
158        assert_eq!(tool.name(), "git_status");
159    }
160
161    #[tokio::test]
162    async fn test_git_status_detects_modified_file() {
163        // GitStatusTool should report modified files that were previously committed
164        let temp_dir = setup_git_repo().await;
165        std::fs::write(temp_dir.path().join("tracked.txt"), "v1").unwrap();
166        Command::new("git")
167            .args(["add", "."])
168            .current_dir(temp_dir.path())
169            .output()
170            .await
171            .unwrap();
172        Command::new("git")
173            .args(["commit", "-m", "init tracked"])
174            .current_dir(temp_dir.path())
175            .output()
176            .await
177            .unwrap();
178
179        // Modify the tracked file
180        std::fs::write(temp_dir.path().join("tracked.txt"), "v2").unwrap();
181
182        let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
183        let result = tool.execute(json!({})).await.unwrap();
184        // Verify the status includes the modified file
185        let serialized = result.to_string();
186        assert!(
187            serialized.contains("tracked.txt"),
188            "status must mention modified tracked.txt, got: {}",
189            serialized
190        );
191    }
192}