ararajuba_tools_coding/git/
log.rs1use ararajuba_core::tools::tool::{tool, ToolDef};
4use git2::Repository;
5use serde_json::json;
6
7pub 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 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
75fn 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}