ararajuba_tools_coding/git/
pull.rs1use ararajuba_core::tools::tool::{tool, ToolDef};
4use git2::Repository;
5use serde_json::json;
6
7pub fn git_pull_tool() -> ToolDef {
12 tool("git_pull")
13 .description("Pull (fetch + merge) from a remote. Requires approval.")
14 .input_schema(json!({
15 "type": "object",
16 "properties": {
17 "remote": { "type": "string", "description": "Remote name (default: origin)" },
18 "branch": { "type": "string", "description": "Branch to pull (default: current)" }
19 }
20 }))
21 .execute(|input| async move {
22 let remote_name = input["remote"].as_str().unwrap_or("origin");
23
24 let repo = Repository::discover(".")
25 .map_err(|e| format!("failed to open repository: {e}"))?;
26
27 let branch_name = if let Some(b) = input["branch"].as_str() {
28 b.to_string()
29 } else {
30 repo.head()
31 .ok()
32 .and_then(|h| h.shorthand().map(String::from))
33 .ok_or_else(|| "cannot determine current branch".to_string())?
34 };
35
36 let mut remote = repo
38 .find_remote(remote_name)
39 .map_err(|e| format!("remote not found: {e}"))?;
40
41 remote
42 .fetch(&[&branch_name], None, None)
43 .map_err(|e| format!("fetch failed: {e}"))?;
44
45 let fetch_head = repo
47 .find_reference(&format!("refs/remotes/{remote_name}/{branch_name}"))
48 .map_err(|e| format!("failed to find fetched ref: {e}"))?;
49
50 let fetch_commit = repo
51 .reference_to_annotated_commit(&fetch_head)
52 .map_err(|e| format!("failed to get annotated commit: {e}"))?;
53
54 let (merge_analysis, _) = repo
56 .merge_analysis(&[&fetch_commit])
57 .map_err(|e| format!("merge analysis failed: {e}"))?;
58
59 if merge_analysis.is_up_to_date() {
60 return Ok(json!({ "ok": true, "commits_pulled": 0 }));
61 }
62
63 if merge_analysis.is_fast_forward() {
64 let refname = format!("refs/heads/{branch_name}");
66 let mut reference = repo
67 .find_reference(&refname)
68 .map_err(|e| format!("failed to find reference: {e}"))?;
69 reference
70 .set_target(fetch_commit.id(), "fast-forward pull")
71 .map_err(|e| format!("failed to fast-forward: {e}"))?;
72 repo.set_head(&refname)
73 .map_err(|e| format!("failed to set HEAD: {e}"))?;
74 repo.checkout_head(Some(
75 git2::build::CheckoutBuilder::new().force(),
76 ))
77 .map_err(|e| format!("failed to checkout: {e}"))?;
78
79 Ok(json!({ "ok": true, "commits_pulled": 1 }))
80 } else {
81 repo.merge(&[&fetch_commit], None, None)
83 .map_err(|e| format!("merge failed: {e}"))?;
84
85 Ok(json!({ "ok": true, "commits_pulled": 1 }))
86 }
87 })
88 .needs_approval(|_input| true)
89 .build()
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn tool_metadata() {
98 let t = git_pull_tool();
99 assert_eq!(t.name, "git_pull");
100 assert!(t.execute.is_some());
101 assert!(t.needs_approval.is_some());
102 }
103}