Skip to main content

ararajuba_tools_coding/git/
diff.rs

1//! `git_diff` tool — show diffs (staged or unstaged).
2
3use ararajuba_core::tools::tool::{tool, ToolDef};
4use git2::{DiffOptions, Repository};
5use serde_json::json;
6
7/// Create the `git_diff` tool.
8///
9/// - `staged: false` (default) → working tree vs index
10/// - `staged: true` → index vs HEAD
11/// - `file` → diff a single file
12pub fn git_diff_tool() -> ToolDef {
13    tool("git_diff")
14        .description("Show git diff. Use staged=true for index vs HEAD.")
15        .input_schema(json!({
16            "type": "object",
17            "properties": {
18                "path":   { "type": "string", "description": "Repository path (default: current dir)" },
19                "staged": { "type": "boolean", "description": "Diff staged changes vs HEAD (default false)" },
20                "file":   { "type": "string", "description": "Diff a single file" }
21            }
22        }))
23        .execute(|input| async move {
24            let path = input["path"].as_str().unwrap_or(".");
25            let staged = input["staged"].as_bool().unwrap_or(false);
26            let file_filter = input["file"].as_str();
27
28            let repo = Repository::discover(path)
29                .map_err(|e| format!("failed to open repository: {e}"))?;
30
31            let mut diff_opts = DiffOptions::new();
32            if let Some(f) = file_filter {
33                diff_opts.pathspec(f);
34            }
35
36            let diff = if staged {
37                let head_tree = repo
38                    .head()
39                    .and_then(|h| h.peel_to_tree())
40                    .map_err(|e| format!("failed to get HEAD tree: {e}"))?;
41                repo.diff_tree_to_index(Some(&head_tree), None, Some(&mut diff_opts))
42            } else {
43                repo.diff_index_to_workdir(None, Some(&mut diff_opts))
44            }
45            .map_err(|e| format!("failed to compute diff: {e}"))?;
46
47            let mut diff_text = String::new();
48            diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
49                let origin = line.origin();
50                if origin == '+' || origin == '-' || origin == ' ' {
51                    diff_text.push(origin);
52                }
53                diff_text.push_str(
54                    std::str::from_utf8(line.content()).unwrap_or(""),
55                );
56                true
57            })
58            .map_err(|e| format!("failed to format diff: {e}"))?;
59
60            Ok(json!({ "diff": diff_text }))
61        })
62        .build()
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn tool_metadata() {
71        let t = git_diff_tool();
72        assert_eq!(t.name, "git_diff");
73        assert!(t.execute.is_some());
74        assert!(t.needs_approval.is_none());
75    }
76}