pawan/tools/git/
status.rs1use super::super::Tool;
2use super::run_git;
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(
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 let (_, branch_output, _) =
84 run_git(&self.workspace_root, &["branch", "--show-current"]).await?;
85 let branch = branch_output.trim().to_string();
86
87 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 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 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 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 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}