Skip to main content

battlecommand_forge/
github.rs

1//! GitHub integration via `gh` CLI.
2//! Ported from battleclaw-v2.
3
4use anyhow::{bail, Result};
5use std::process::Command;
6
7pub struct GitHubOps {
8    workspace: String,
9}
10
11#[derive(Debug)]
12pub struct PrResult {
13    pub url: String,
14    pub number: u32,
15}
16
17impl GitHubOps {
18    pub fn new(workspace: &str) -> Self {
19        Self {
20            workspace: workspace.to_string(),
21        }
22    }
23
24    pub fn is_available() -> bool {
25        Command::new("gh")
26            .args(["auth", "status"])
27            .output()
28            .map(|o| o.status.success())
29            .unwrap_or(false)
30    }
31
32    pub fn create_repo(&self, name: &str, private: bool) -> Result<String> {
33        let vis = if private { "--private" } else { "--public" };
34        let output = Command::new("gh")
35            .args([
36                "repo",
37                "create",
38                name,
39                vis,
40                "--source",
41                &self.workspace,
42                "--push",
43            ])
44            .output()?;
45        if output.status.success() {
46            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
47        } else {
48            bail!(
49                "Failed to create repo: {}",
50                String::from_utf8_lossy(&output.stderr)
51            )
52        }
53    }
54
55    pub fn add_remote(&self, repo_url: &str) -> Result<()> {
56        let output = Command::new("git")
57            .args(["remote", "add", "origin", repo_url])
58            .current_dir(&self.workspace)
59            .output()?;
60        if !output.status.success() {
61            Command::new("git")
62                .args(["remote", "set-url", "origin", repo_url])
63                .current_dir(&self.workspace)
64                .output()?;
65        }
66        Ok(())
67    }
68
69    pub fn push(&self, branch: &str) -> Result<()> {
70        let output = Command::new("git")
71            .args(["push", "-u", "origin", branch])
72            .current_dir(&self.workspace)
73            .output()?;
74        if output.status.success() {
75            Ok(())
76        } else {
77            bail!("Push failed: {}", String::from_utf8_lossy(&output.stderr))
78        }
79    }
80
81    pub fn create_pr(&self, title: &str, body: &str, base: &str) -> Result<PrResult> {
82        let output = Command::new("gh")
83            .args([
84                "pr", "create", "--title", title, "--body", body, "--base", base,
85            ])
86            .current_dir(&self.workspace)
87            .output()?;
88        if output.status.success() {
89            let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
90            let number = url
91                .rsplit('/')
92                .next()
93                .and_then(|n| n.parse().ok())
94                .unwrap_or(0);
95            Ok(PrResult { url, number })
96        } else {
97            bail!(
98                "PR creation failed: {}",
99                String::from_utf8_lossy(&output.stderr)
100            )
101        }
102    }
103
104    pub fn status(&self) -> Result<String> {
105        let branch = run_git(&self.workspace, &["branch", "--show-current"])?;
106        let remote = run_git(&self.workspace, &["remote", "-v"]).unwrap_or_default();
107        Ok(format!(
108            "Branch: {}\nRemote:\n{}",
109            branch.trim(),
110            remote.trim()
111        ))
112    }
113}
114
115fn run_git(cwd: &str, args: &[&str]) -> Result<String> {
116    let output = Command::new("git").args(args).current_dir(cwd).output()?;
117    Ok(String::from_utf8_lossy(&output.stdout).to_string())
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_gh_availability() {
126        // Just verify it doesn't panic
127        let _ = GitHubOps::is_available();
128    }
129}