Skip to main content

pawan/tools/git/
mod.rs

1//! Git operation tools
2//!
3//! Tools for git operations: status, diff, add, commit, log, blame, branch.
4
5use std::path::PathBuf;
6use std::process::Stdio;
7use tokio::io::AsyncReadExt;
8use tokio::process::Command;
9
10pub mod branch;
11pub mod diff;
12pub mod log;
13pub mod staging;
14pub mod status;
15
16pub use branch::{GitBranchTool, GitCheckoutTool, GitStashTool};
17pub use diff::GitDiffTool;
18pub use log::{GitBlameTool, GitLogTool};
19pub use staging::{GitAddTool, GitCommitTool};
20pub use status::GitStatusTool;
21
22/// Run a git command in a workspace directory
23pub(crate) async fn run_git(
24    workspace: &PathBuf,
25    args: &[&str],
26) -> crate::Result<(bool, String, String)> {
27    let mut cmd = Command::new("git");
28    cmd.args(args)
29        .current_dir(workspace)
30        .stdout(Stdio::piped())
31        .stderr(Stdio::piped())
32        .stdin(Stdio::null());
33
34    let mut child = cmd.spawn().map_err(crate::PawanError::Io)?;
35
36    let mut stdout = String::new();
37    let mut stderr = String::new();
38
39    if let Some(mut handle) = child.stdout.take() {
40        handle.read_to_string(&mut stdout).await.ok();
41    }
42    if let Some(mut handle) = child.stderr.take() {
43        handle.read_to_string(&mut stderr).await.ok();
44    }
45
46    let status = child.wait().await.map_err(crate::PawanError::Io)?;
47    Ok((status.success(), stdout, stderr))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::super::Tool;
53    use super::*;
54    use tempfile::TempDir;
55
56    #[tokio::test]
57    async fn test_git_tool_schemas() {
58        let tmp = TempDir::new().unwrap();
59        // Verify all git tools have correct names and non-empty schemas
60        let tools: Vec<(&str, Box<dyn Tool>)> = vec![
61            (
62                "git_status",
63                Box::new(status::GitStatusTool::new(tmp.path().into())),
64            ),
65            (
66                "git_diff",
67                Box::new(diff::GitDiffTool::new(tmp.path().into())),
68            ),
69            (
70                "git_add",
71                Box::new(staging::GitAddTool::new(tmp.path().into())),
72            ),
73            (
74                "git_commit",
75                Box::new(staging::GitCommitTool::new(tmp.path().into())),
76            ),
77            ("git_log", Box::new(log::GitLogTool::new(tmp.path().into()))),
78            (
79                "git_blame",
80                Box::new(log::GitBlameTool::new(tmp.path().into())),
81            ),
82            (
83                "git_branch",
84                Box::new(branch::GitBranchTool::new(tmp.path().into())),
85            ),
86            (
87                "git_checkout",
88                Box::new(branch::GitCheckoutTool::new(tmp.path().into())),
89            ),
90            (
91                "git_stash",
92                Box::new(branch::GitStashTool::new(tmp.path().into())),
93            ),
94        ];
95        for (expected_name, tool) in &tools {
96            assert_eq!(tool.name(), *expected_name, "Tool name mismatch");
97            assert!(
98                !tool.description().is_empty(),
99                "Missing description for {}",
100                expected_name
101            );
102            let schema = tool.parameters_schema();
103            assert!(
104                schema.is_object(),
105                "Schema should be object for {}",
106                expected_name
107            );
108        }
109    }
110}