mod types;
pub mod writer;
pub mod github;
mod none;
pub mod sync;
pub use types::*;
pub use writer::{ForgeWriter, GitHubWriter, NoneWriter};
use anyhow::Result;
pub trait ForgeReader {
fn get_issue_count(&self) -> Result<usize>;
fn get_pr_count(&self) -> Result<usize>;
fn list_issues(&self, limit: usize, since: Option<&str>) -> Result<Vec<Issue>>;
fn list_pull_requests(&self, limit: usize, since: Option<&str>) -> Result<Vec<PullRequest>>;
fn get_pull_request(&self, number: i64) -> Result<PullRequest>;
fn get_issue(&self, number: i64) -> Result<Issue>;
fn get_max_issue_number(&self) -> Result<i64>;
}
pub fn detect(remote_url: &str) -> Forge {
let (host, owner, repo) = parse_remote_url(remote_url);
let kind = if host.contains("github.com") {
ForgeKind::GitHub
} else if is_gitea_host(&host) {
ForgeKind::Gitea
} else {
ForgeKind::None
};
Forge {
kind,
owner,
repo,
host,
}
}
pub fn reader(forge: &Forge) -> Box<dyn ForgeReader> {
match forge.kind {
ForgeKind::GitHub => Box::new(github::GitHubReader::new(forge)),
ForgeKind::Gitea => Box::new(none::NoneReader), ForgeKind::None => Box::new(none::NoneReader),
}
}
fn parse_remote_url(url: &str) -> (String, String, String) {
if let Some(rest) = url.strip_prefix("git@") {
if let Some((host, path)) = rest.split_once(':') {
let path = path.trim_end_matches(".git");
if let Some((owner, repo)) = path.split_once('/') {
return (host.to_string(), owner.to_string(), repo.to_string());
}
}
}
if url.starts_with("https://") || url.starts_with("http://") {
let without_proto = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
.unwrap_or(url);
let without_git = without_proto.trim_end_matches(".git");
let parts: Vec<&str> = without_git.splitn(3, '/').collect();
if parts.len() >= 3 {
return (
parts[0].to_string(),
parts[1].to_string(),
parts[2].to_string(),
);
}
}
(String::new(), String::new(), String::new())
}
fn is_gitea_host(host: &str) -> bool {
host.contains("codeberg.org")
|| host.contains("gitea.")
|| host.contains("forgejo.")
|| host.contains("gitea.io")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_github_ssh() {
let forge = detect("git@github.com:anthropics/claude-code.git");
assert_eq!(forge.kind, ForgeKind::GitHub);
assert_eq!(forge.owner, "anthropics");
assert_eq!(forge.repo, "claude-code");
assert_eq!(forge.host, "github.com");
}
#[test]
fn test_detect_github_https() {
let forge = detect("https://github.com/anthropics/claude-code");
assert_eq!(forge.kind, ForgeKind::GitHub);
assert_eq!(forge.owner, "anthropics");
assert_eq!(forge.repo, "claude-code");
}
#[test]
fn test_detect_codeberg() {
let forge = detect("https://codeberg.org/owner/repo");
assert_eq!(forge.kind, ForgeKind::Gitea);
assert_eq!(forge.owner, "owner");
assert_eq!(forge.repo, "repo");
}
#[test]
fn test_detect_unknown() {
let forge = detect("https://gitlab.com/owner/repo");
assert_eq!(forge.kind, ForgeKind::None);
}
#[test]
fn test_detect_local() {
let forge = detect("/path/to/local/repo");
assert_eq!(forge.kind, ForgeKind::None);
}
}