Skip to main content

ararajuba_tools_coding/git/
log.rs

1//! `git_log` tool — show commit history.
2
3use ararajuba_core::tools::tool::{tool, ToolDef};
4use git2::Repository;
5use serde_json::json;
6
7/// Create the `git_log` tool.
8///
9/// Returns recent commits. Use `limit` to control how many (default 20).
10/// Use `file` to show history of a specific file.
11pub fn git_log_tool() -> ToolDef {
12    tool("git_log")
13        .description("Show git commit log. Use limit to control count, file for history of one file.")
14        .input_schema(json!({
15            "type": "object",
16            "properties": {
17                "path":  { "type": "string",  "description": "Repository path (default: current dir)" },
18                "limit": { "type": "integer", "description": "Max commits to return (default 20)" },
19                "file":  { "type": "string",  "description": "Show history for a specific file" }
20            }
21        }))
22        .execute(|input| async move {
23            let path = input["path"].as_str().unwrap_or(".");
24            let limit = input["limit"].as_u64().unwrap_or(20) as usize;
25            let file_filter = input["file"].as_str();
26
27            let repo = Repository::discover(path)
28                .map_err(|e| format!("failed to open repository: {e}"))?;
29
30            let mut revwalk = repo
31                .revwalk()
32                .map_err(|e| format!("failed to create revwalk: {e}"))?;
33            revwalk.push_head().map_err(|e| format!("failed to push HEAD: {e}"))?;
34            revwalk
35                .set_sorting(git2::Sort::TIME)
36                .map_err(|e| format!("failed to set sorting: {e}"))?;
37
38            let mut commits = Vec::new();
39
40            for oid_result in revwalk {
41                if commits.len() >= limit {
42                    break;
43                }
44                let oid = oid_result.map_err(|e| format!("revwalk error: {e}"))?;
45                let commit = repo
46                    .find_commit(oid)
47                    .map_err(|e| format!("failed to find commit: {e}"))?;
48
49                // File filter: check if any diff entry touches the file
50                if let Some(file_path) = file_filter {
51                    let dominated = commit_touches_file(&repo, &commit, file_path);
52                    if !dominated {
53                        continue;
54                    }
55                }
56
57                let author = commit.author();
58                let hash = oid.to_string();
59                let short = &hash[..7.min(hash.len())];
60
61                commits.push(json!({
62                    "hash": hash,
63                    "short_hash": short,
64                    "author": author.name().unwrap_or("unknown"),
65                    "date": commit.time().seconds().to_string(),
66                    "message": commit.message().unwrap_or("").trim()
67                }));
68            }
69
70            Ok(json!({ "commits": commits }))
71        })
72        .build()
73}
74
75/// Check if a commit modifies a given file path.
76fn commit_touches_file(
77    repo: &Repository,
78    commit: &git2::Commit,
79    file_path: &str,
80) -> bool {
81    let tree = match commit.tree() {
82        Ok(t) => t,
83        Err(_) => return false,
84    };
85
86    let parent_tree = commit
87        .parent(0)
88        .ok()
89        .and_then(|p| p.tree().ok());
90
91    let diff = repo
92        .diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None)
93        .ok();
94
95    if let Some(d) = diff {
96        for delta in d.deltas() {
97            let old = delta.old_file().path().and_then(|p| p.to_str());
98            let new = delta.new_file().path().and_then(|p| p.to_str());
99            if old == Some(file_path) || new == Some(file_path) {
100                return true;
101            }
102        }
103    }
104    false
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn tool_metadata() {
113        let t = git_log_tool();
114        assert_eq!(t.name, "git_log");
115        assert!(t.execute.is_some());
116        assert!(t.needs_approval.is_none());
117    }
118}