use anyhow::{bail, Context, Result};
use std::path::Path;
use std::process::Command;
pub trait ForgeWriter {
fn is_authenticated(&self) -> Result<bool>;
fn current_user(&self) -> Result<String>;
fn repo_exists(&self, owner: &str, repo: &str) -> Result<bool>;
fn fork(&self, repo_path: &Path) -> Result<String>;
fn create_repo(&self, name: &str, private: bool, repo_path: &Path) -> Result<String>;
}
pub struct GitHubWriter;
impl ForgeWriter for GitHubWriter {
fn is_authenticated(&self) -> Result<bool> {
let output = Command::new("gh")
.args(["auth", "status"])
.output()
.context("Failed to run `gh auth status`. Is `gh` CLI installed?")?;
Ok(output.status.success())
}
fn current_user(&self) -> Result<String> {
let output = Command::new("gh")
.args(["api", "user", "--jq", ".login"])
.output()
.context("Failed to get current GitHub user. Is 'gh' installed and authenticated?")?;
if !output.status.success() {
bail!(
"Failed to get GitHub user: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn repo_exists(&self, owner: &str, repo: &str) -> Result<bool> {
let full_name = format!("{}/{}", owner, repo);
let output = Command::new("gh")
.args(["repo", "view", &full_name])
.output()
.context("Failed to check if repository exists")?;
Ok(output.status.success())
}
fn fork(&self, repo_path: &Path) -> Result<String> {
let output = Command::new("gh")
.args(["repo", "fork", "--remote=false"])
.current_dir(repo_path)
.output()
.context("Failed to execute `gh repo fork`. Is GitHub CLI installed?")?;
if !output.status.success() {
bail!(
"gh repo fork failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let user = self.current_user()?;
let repo_name = get_repo_name_from_git(repo_path)?;
Ok(format!("git@github.com:{}/{}.git", user, repo_name))
}
fn create_repo(&self, name: &str, private: bool, repo_path: &Path) -> Result<String> {
let mut args = vec!["repo", "create", name, "--source=.", "--push"];
if private {
args.push("--private");
} else {
args.push("--public");
}
let output = Command::new("gh")
.args(&args)
.current_dir(repo_path)
.output()
.context("Failed to create GitHub repository")?;
if !output.status.success() {
bail!(
"Failed to create repository: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let user = self.current_user()?;
Ok(format!("git@github.com:{}/{}.git", user, name))
}
}
fn get_repo_name_from_git(repo_path: &Path) -> Result<String> {
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(repo_path)
.output()
.context("Failed to get git remote URL")?;
if !output.status.success() {
bail!("No origin remote found");
}
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
let repo_name = if let Some(rest) = url.strip_prefix("git@") {
rest.split(':')
.nth(1)
.and_then(|p| p.strip_suffix(".git").or(Some(p)))
.and_then(|p| p.split('/').next_back())
} else if url.starts_with("https://") || url.starts_with("http://") {
url.trim_end_matches(".git").split('/').next_back()
} else {
None
};
repo_name
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("Could not parse repository name from URL: {}", url))
}
pub struct NoneWriter;
impl ForgeWriter for NoneWriter {
fn is_authenticated(&self) -> Result<bool> {
Ok(false)
}
fn current_user(&self) -> Result<String> {
bail!("No forge configured - cannot get current user")
}
fn repo_exists(&self, _owner: &str, _repo: &str) -> Result<bool> {
Ok(false)
}
fn fork(&self, _repo_path: &Path) -> Result<String> {
bail!("No forge configured - cannot create fork")
}
fn create_repo(&self, _name: &str, _private: bool, _repo_path: &Path) -> Result<String> {
bail!("No forge configured - cannot create repository")
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_get_repo_name_ssh() {
let url = "git@github.com:owner/myrepo.git";
let name = url
.strip_prefix("git@")
.and_then(|r| r.split(':').nth(1))
.and_then(|p| p.strip_suffix(".git"))
.and_then(|p| p.split('/').next_back());
assert_eq!(name, Some("myrepo"));
}
#[test]
fn test_get_repo_name_https() {
let url = "https://github.com/owner/myrepo";
let name = url.trim_end_matches(".git").split('/').next_back();
assert_eq!(name, Some("myrepo"));
}
}