pawan/tools/git/
status.rs1use super::run_git;
2use super::super::Tool;
3use async_trait::async_trait;
4use serde_json::{json, Value};
5use std::path::PathBuf;
6
7pub 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 }
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 let (_, branch_output, _) =
79 run_git(&self.workspace_root, &["branch", "--show-current"]).await?;
80 let branch = branch_output.trim().to_string();
81
82 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 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 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 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 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}