Skip to main content

ararajuba_tools_coding/git/
status.rs

1//! `git_status` tool — show repository status.
2
3use ararajuba_core::tools::tool::{tool, ToolDef};
4use git2::{Repository, StatusOptions};
5use serde_json::json;
6
7/// Create the `git_status` tool.
8///
9/// Returns the current branch, staged/unstaged changes, and untracked files.
10pub fn git_status_tool() -> ToolDef {
11    tool("git_status")
12        .description("Show git repository status: branch, staged, unstaged, and untracked files.")
13        .input_schema(json!({
14            "type": "object",
15            "properties": {
16                "path": { "type": "string", "description": "Repository path (default: current dir)" }
17            }
18        }))
19        .execute(|input| async move {
20            let path = input["path"].as_str().unwrap_or(".");
21
22            let repo = Repository::discover(path)
23                .map_err(|e| format!("failed to open repository: {e}"))?;
24
25            // Current branch
26            let branch = repo
27                .head()
28                .ok()
29                .and_then(|h| h.shorthand().map(String::from))
30                .unwrap_or_else(|| "HEAD (detached)".into());
31
32            let mut opts = StatusOptions::new();
33            opts.include_untracked(true)
34                .recurse_untracked_dirs(true);
35
36            let statuses = repo
37                .statuses(Some(&mut opts))
38                .map_err(|e| format!("failed to get status: {e}"))?;
39
40            let mut staged = Vec::new();
41            let mut unstaged = Vec::new();
42            let mut untracked = Vec::new();
43
44            for entry in statuses.iter() {
45                let path_str = entry.path().unwrap_or("").to_string();
46                let s = entry.status();
47
48                if s.is_index_new() {
49                    staged.push(json!({"path": path_str, "status": "added"}));
50                } else if s.is_index_modified() {
51                    staged.push(json!({"path": path_str, "status": "modified"}));
52                } else if s.is_index_deleted() {
53                    staged.push(json!({"path": path_str, "status": "deleted"}));
54                }
55
56                if s.is_wt_modified() {
57                    unstaged.push(json!({"path": path_str, "status": "modified"}));
58                } else if s.is_wt_deleted() {
59                    unstaged.push(json!({"path": path_str, "status": "deleted"}));
60                }
61
62                if s.is_wt_new() {
63                    untracked.push(json!(path_str));
64                }
65            }
66
67            Ok(json!({
68                "branch": branch,
69                "staged": staged,
70                "unstaged": unstaged,
71                "untracked": untracked
72            }))
73        })
74        .build()
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn tool_metadata() {
83        let t = git_status_tool();
84        assert_eq!(t.name, "git_status");
85        assert!(t.execute.is_some());
86        assert!(t.needs_approval.is_none());
87    }
88}