Skip to main content

sh_layer3/builtin_tools/
git_tools.rs

1//! # Git Tools
2//!
3//! Git 版本控制工具集。
4
5use crate::builtin_tools::BuiltinTool;
6use crate::types::{Layer3Result, ToolCategory};
7use async_trait::async_trait;
8use std::process::Command;
9
10/// Execute a git command and return the output
11fn run_git(args: &[&str], cwd: Option<&str>) -> Layer3Result<String> {
12    let mut cmd = Command::new("git");
13    cmd.args(args);
14
15    if let Some(dir) = cwd {
16        cmd.current_dir(dir);
17    }
18
19    let output = cmd
20        .output()
21        .map_err(|e| anyhow::anyhow!("Failed to execute git: {}", e))?;
22
23    if output.status.success() {
24        String::from_utf8(output.stdout).map_err(|e| anyhow::anyhow!("Invalid UTF-8 output: {}", e))
25    } else {
26        let stderr = String::from_utf8_lossy(&output.stderr);
27        Err(anyhow::anyhow!("Git command failed: {}", stderr))
28    }
29}
30
31// ============================================================================
32// Git Status Tool
33// ============================================================================
34
35/// Git 状态工具
36pub struct GitStatusTool;
37
38#[async_trait]
39impl BuiltinTool for GitStatusTool {
40    fn name(&self) -> &str {
41        "git_status"
42    }
43
44    fn description(&self) -> &str {
45        "Show the working tree status. Lists modified, staged, and untracked files."
46    }
47
48    fn parameters_schema(&self) -> serde_json::Value {
49        serde_json::json!({
50            "type": "object",
51            "properties": {
52                "path": {
53                    "type": "string",
54                    "description": "Repository path (default: current directory)"
55                },
56                "short": {
57                    "type": "boolean",
58                    "description": "Use short format (default: false)"
59                }
60            }
61        })
62    }
63
64    fn category(&self) -> ToolCategory {
65        ToolCategory::VersionControl
66    }
67
68    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
69        let path = args["path"].as_str();
70        let short = args["short"].as_bool().unwrap_or(false);
71
72        let mut git_args = vec!["status"];
73        if short {
74            git_args.push("--short");
75        }
76
77        run_git(&git_args, path)
78    }
79}
80
81// ============================================================================
82// Git Log Tool
83// ============================================================================
84
85/// Git 日志工具
86pub struct GitLogTool;
87
88#[async_trait]
89impl BuiltinTool for GitLogTool {
90    fn name(&self) -> &str {
91        "git_log"
92    }
93
94    fn description(&self) -> &str {
95        "Show commit logs. Supports various format options."
96    }
97
98    fn parameters_schema(&self) -> serde_json::Value {
99        serde_json::json!({
100            "type": "object",
101            "properties": {
102                "path": {
103                    "type": "string",
104                    "description": "Repository path (default: current directory)"
105                },
106                "count": {
107                    "type": "integer",
108                    "description": "Number of commits to show (default: 10)"
109                },
110                "oneline": {
111                    "type": "boolean",
112                    "description": "Use one-line format (default: true)"
113                },
114                "branch": {
115                    "type": "string",
116                    "description": "Branch name (default: current branch)"
117                }
118            }
119        })
120    }
121
122    fn category(&self) -> ToolCategory {
123        ToolCategory::VersionControl
124    }
125
126    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
127        let path = args["path"].as_str();
128        let count = args["count"].as_u64().unwrap_or(10);
129        let oneline = args["oneline"].as_bool().unwrap_or(true);
130        let branch = args["branch"].as_str();
131
132        let count_arg = format!("-{}", count);
133        let mut git_args = vec!["log", &count_arg];
134        if oneline {
135            git_args.push("--oneline");
136        }
137        if let Some(b) = branch {
138            git_args.push(b);
139        }
140
141        run_git(&git_args, path)
142    }
143}
144
145// ============================================================================
146// Git Diff Tool
147// ============================================================================
148
149/// Git Diff 工具
150pub struct GitDiffTool;
151
152#[async_trait]
153impl BuiltinTool for GitDiffTool {
154    fn name(&self) -> &str {
155        "git_diff"
156    }
157
158    fn description(&self) -> &str {
159        "Show changes between commits, commit and working tree, etc."
160    }
161
162    fn parameters_schema(&self) -> serde_json::Value {
163        serde_json::json!({
164            "type": "object",
165            "properties": {
166                "path": {
167                    "type": "string",
168                    "description": "Repository path (default: current directory)"
169                },
170                "file": {
171                    "type": "string",
172                    "description": "Specific file to diff"
173                },
174                "staged": {
175                    "type": "boolean",
176                    "description": "Show staged changes (--cached)"
177                },
178                "commit": {
179                    "type": "string",
180                    "description": "Commit hash or branch to compare"
181                }
182            }
183        })
184    }
185
186    fn category(&self) -> ToolCategory {
187        ToolCategory::VersionControl
188    }
189
190    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
191        let path = args["path"].as_str();
192        let file = args["file"].as_str();
193        let staged = args["staged"].as_bool().unwrap_or(false);
194        let commit = args["commit"].as_str();
195
196        let mut git_args = vec!["diff"];
197        if staged {
198            git_args.push("--cached");
199        }
200        if let Some(c) = commit {
201            git_args.push(c);
202        }
203        if let Some(f) = file {
204            git_args.push("--");
205            git_args.push(f);
206        }
207
208        run_git(&git_args, path)
209    }
210}
211
212// ============================================================================
213// Git Branch Tool
214// ============================================================================
215
216/// Git 分支工具
217pub struct GitBranchTool;
218
219#[async_trait]
220impl BuiltinTool for GitBranchTool {
221    fn name(&self) -> &str {
222        "git_branch"
223    }
224
225    fn description(&self) -> &str {
226        "List, create, or delete branches."
227    }
228
229    fn parameters_schema(&self) -> serde_json::Value {
230        serde_json::json!({
231            "type": "object",
232            "properties": {
233                "path": {
234                    "type": "string",
235                    "description": "Repository path (default: current directory)"
236                },
237                "action": {
238                    "type": "string",
239                    "enum": ["list", "create", "delete"],
240                    "description": "Action to perform (default: list)"
241                },
242                "branch_name": {
243                    "type": "string",
244                    "description": "Branch name for create/delete"
245                },
246                "all": {
247                    "type": "boolean",
248                    "description": "List all branches including remote (default: false)"
249                }
250            }
251        })
252    }
253
254    fn category(&self) -> ToolCategory {
255        ToolCategory::VersionControl
256    }
257
258    fn requires_confirmation(&self) -> bool {
259        true // Creating/deleting branches is a significant action
260    }
261
262    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
263        let path = args["path"].as_str();
264        let action = args["action"].as_str().unwrap_or("list");
265        let branch_name = args["branch_name"].as_str();
266        let all = args["all"].as_bool().unwrap_or(false);
267
268        let git_args = match action {
269            "list" => {
270                let mut args = vec!["branch"];
271                if all {
272                    args.push("-a");
273                }
274                args
275            }
276            "create" => {
277                let name = branch_name
278                    .ok_or_else(|| anyhow::anyhow!("branch_name required for create"))?;
279                vec!["branch", name]
280            }
281            "delete" => {
282                let name = branch_name
283                    .ok_or_else(|| anyhow::anyhow!("branch_name required for delete"))?;
284                vec!["branch", "-D", name]
285            }
286            _ => return Err(anyhow::anyhow!("Invalid action: {}", action)),
287        };
288
289        run_git(&git_args, path)
290    }
291}
292
293// ============================================================================
294// Git Add Tool
295// ============================================================================
296
297/// Git Add 工具
298pub struct GitAddTool;
299
300#[async_trait]
301impl BuiltinTool for GitAddTool {
302    fn name(&self) -> &str {
303        "git_add"
304    }
305
306    fn description(&self) -> &str {
307        "Add file contents to the index."
308    }
309
310    fn parameters_schema(&self) -> serde_json::Value {
311        serde_json::json!({
312            "type": "object",
313            "properties": {
314                "path": {
315                    "type": "string",
316                    "description": "Repository path (default: current directory)"
317                },
318                "files": {
319                    "type": "array",
320                    "items": {"type": "string"},
321                    "description": "Files to add (default: ['.'])"
322                }
323            }
324        })
325    }
326
327    fn category(&self) -> ToolCategory {
328        ToolCategory::VersionControl
329    }
330
331    fn requires_confirmation(&self) -> bool {
332        true
333    }
334
335    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
336        let path = args["path"].as_str();
337        let files: Vec<&str> = if let Some(arr) = args["files"].as_array() {
338            arr.iter().filter_map(|v| v.as_str()).collect()
339        } else {
340            vec!["."]
341        };
342
343        let mut git_args = vec!["add", "--"];
344        git_args.extend(files);
345
346        run_git(&git_args, path)
347    }
348}
349
350// ============================================================================
351// Git Commit Tool
352// ============================================================================
353
354/// Git Commit 工具
355pub struct GitCommitTool;
356
357#[async_trait]
358impl BuiltinTool for GitCommitTool {
359    fn name(&self) -> &str {
360        "git_commit"
361    }
362
363    fn description(&self) -> &str {
364        "Record changes to the repository."
365    }
366
367    fn parameters_schema(&self) -> serde_json::Value {
368        serde_json::json!({
369            "type": "object",
370            "properties": {
371                "path": {
372                    "type": "string",
373                    "description": "Repository path (default: current directory)"
374                },
375                "message": {
376                    "type": "string",
377                    "description": "Commit message"
378                }
379            },
380            "required": ["message"]
381        })
382    }
383
384    fn category(&self) -> ToolCategory {
385        ToolCategory::VersionControl
386    }
387
388    fn requires_confirmation(&self) -> bool {
389        true
390    }
391
392    fn is_dangerous(&self) -> bool {
393        true
394    }
395
396    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
397        let path = args["path"].as_str();
398        let message = args["message"]
399            .as_str()
400            .ok_or_else(|| anyhow::anyhow!("Missing message parameter"))?;
401
402        run_git(&["commit", "-m", message], path)
403    }
404}
405
406// ============================================================================
407// Git Show Tool
408// ============================================================================
409
410/// Git Show 工具
411pub struct GitShowTool;
412
413#[async_trait]
414impl BuiltinTool for GitShowTool {
415    fn name(&self) -> &str {
416        "git_show"
417    }
418
419    fn description(&self) -> &str {
420        "Show various types of objects (commits, tags, trees)."
421    }
422
423    fn parameters_schema(&self) -> serde_json::Value {
424        serde_json::json!({
425            "type": "object",
426            "properties": {
427                "path": {
428                    "type": "string",
429                    "description": "Repository path (default: current directory)"
430                },
431                "object": {
432                    "type": "string",
433                    "description": "Object to show (commit hash, tag, etc.)"
434                },
435                "stat": {
436                    "type": "boolean",
437                    "description": "Show diffstat instead of full diff (default: true)"
438                }
439            },
440            "required": ["object"]
441        })
442    }
443
444    fn category(&self) -> ToolCategory {
445        ToolCategory::VersionControl
446    }
447
448    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
449        let path = args["path"].as_str();
450        let object = args["object"]
451            .as_str()
452            .ok_or_else(|| anyhow::anyhow!("Missing object parameter"))?;
453        let stat = args["stat"].as_bool().unwrap_or(true);
454
455        let mut git_args = vec!["show"];
456        if stat {
457            git_args.push("--stat");
458        }
459        git_args.push(object);
460
461        run_git(&git_args, path)
462    }
463}
464
465// ============================================================================
466// Git Stash Tool
467// ============================================================================
468
469/// Git Stash 工具
470pub struct GitStashTool;
471
472#[async_trait]
473impl BuiltinTool for GitStashTool {
474    fn name(&self) -> &str {
475        "git_stash"
476    }
477
478    fn description(&self) -> &str {
479        "Stash the changes in a dirty working directory."
480    }
481
482    fn parameters_schema(&self) -> serde_json::Value {
483        serde_json::json!({
484            "type": "object",
485            "properties": {
486                "path": {
487                    "type": "string",
488                    "description": "Repository path (default: current directory)"
489                },
490                "action": {
491                    "type": "string",
492                    "enum": ["push", "pop", "list", "drop"],
493                    "description": "Action to perform (default: list)"
494                },
495                "message": {
496                    "type": "string",
497                    "description": "Stash message (for push)"
498                }
499            }
500        })
501    }
502
503    fn category(&self) -> ToolCategory {
504        ToolCategory::VersionControl
505    }
506
507    fn requires_confirmation(&self) -> bool {
508        true
509    }
510
511    async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
512        let path = args["path"].as_str();
513        let action = args["action"].as_str().unwrap_or("list");
514        let message = args["message"].as_str();
515
516        let git_args = match action {
517            "push" => {
518                let mut args = vec!["stash", "push"];
519                if let Some(msg) = message {
520                    args.push("-m");
521                    args.push(msg);
522                }
523                args
524            }
525            "pop" => vec!["stash", "pop"],
526            "list" => vec!["stash", "list"],
527            "drop" => vec!["stash", "drop"],
528            _ => return Err(anyhow::anyhow!("Invalid action: {}", action)),
529        };
530
531        run_git(&git_args, path)
532    }
533}
534
535// ============================================================================
536// Tests
537// ============================================================================
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use serde_json::json;
543
544    #[test]
545    fn test_git_status_category() {
546        let tool = GitStatusTool;
547        assert_eq!(tool.category(), ToolCategory::VersionControl);
548    }
549
550    #[test]
551    fn test_git_commit_is_dangerous() {
552        let tool = GitCommitTool;
553        assert!(tool.is_dangerous());
554        assert!(tool.requires_confirmation());
555    }
556
557    #[test]
558    fn test_git_add_requires_confirmation() {
559        let tool = GitAddTool;
560        assert!(tool.requires_confirmation());
561    }
562}