use super::super::Tool;
use super::run_git;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::path::PathBuf;
pub struct GitStatusTool {
workspace_root: PathBuf,
}
impl GitStatusTool {
pub fn new(workspace_root: PathBuf) -> Self {
Self { workspace_root }
}
}
#[async_trait]
impl Tool for GitStatusTool {
fn name(&self) -> &str {
"git_status"
}
fn description(&self) -> &str {
"Get the current git status showing staged, unstaged, and untracked files."
}
fn mutating(&self) -> bool {
false }
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"short": {
"type": "boolean",
"description": "Use short format output (default: false)"
}
},
"required": []
})
}
fn thulp_definition(&self) -> thulp_core::ToolDefinition {
use thulp_core::{Parameter, ParameterType};
thulp_core::ToolDefinition::builder("git_status")
.description(self.description())
.parameter(
Parameter::builder("short")
.param_type(ParameterType::Boolean)
.required(false)
.description("Use short format output (default: false)")
.build(),
)
.build()
}
async fn execute(&self, args: Value) -> crate::Result<Value> {
let short = args["short"].as_bool().unwrap_or(false);
let mut git_args = vec!["status"];
if short {
git_args.push("-s");
}
let (success, stdout, stderr) = run_git(&self.workspace_root, &git_args).await?;
if !success {
return Err(crate::PawanError::Git(format!(
"git status failed: {}",
stderr
)));
}
let (_, branch_output, _) =
run_git(&self.workspace_root, &["branch", "--show-current"]).await?;
let branch = branch_output.trim().to_string();
let (_, porcelain, _) = run_git(&self.workspace_root, &["status", "--porcelain"]).await?;
let is_clean = porcelain.trim().is_empty();
Ok(json!({
"status": stdout.trim(),
"branch": branch,
"is_clean": is_clean,
"success": true
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
use tokio::process::Command;
async fn setup_git_repo() -> TempDir {
let temp_dir = TempDir::new().unwrap();
Command::new("git")
.args(["init"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
temp_dir
}
#[tokio::test]
async fn test_git_status_empty_repo() {
let temp_dir = setup_git_repo().await;
let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
let result = tool.execute(json!({})).await.unwrap();
assert!(result["success"].as_bool().unwrap());
assert!(result["is_clean"].as_bool().unwrap());
}
#[tokio::test]
async fn test_git_status_with_untracked() {
let temp_dir = setup_git_repo().await;
std::fs::write(temp_dir.path().join("test.txt"), "hello").unwrap();
let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
let result = tool.execute(json!({})).await.unwrap();
assert!(result["success"].as_bool().unwrap());
assert!(!result["is_clean"].as_bool().unwrap());
}
#[tokio::test]
async fn test_git_status_tool_exists() {
let temp_dir = setup_git_repo().await;
let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
assert_eq!(tool.name(), "git_status");
}
#[tokio::test]
async fn test_git_status_detects_modified_file() {
let temp_dir = setup_git_repo().await;
std::fs::write(temp_dir.path().join("tracked.txt"), "v1").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
Command::new("git")
.args(["commit", "-m", "init tracked"])
.current_dir(temp_dir.path())
.output()
.await
.unwrap();
std::fs::write(temp_dir.path().join("tracked.txt"), "v2").unwrap();
let tool = GitStatusTool::new(temp_dir.path().to_path_buf());
let result = tool.execute(json!({})).await.unwrap();
let serialized = result.to_string();
assert!(
serialized.contains("tracked.txt"),
"status must mention modified tracked.txt, got: {}",
serialized
);
}
}