pub mod github;
pub mod gitlab;
pub mod server_registry;
pub mod types;
use crate::acquire::sources::RepoSource;
use crate::auth::SecureString;
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use std::path::Path;
use types::*;
#[derive(Debug, Clone)]
pub struct RemoteRepo {
pub host: PlatformHost,
pub owner: String,
pub repo: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PlatformHost {
GitHub,
GitLab,
}
impl std::fmt::Display for PlatformHost {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlatformHost::GitHub => write!(f, "GitHub"),
PlatformHost::GitLab => write!(f, "GitLab"),
}
}
}
#[async_trait]
pub trait Platform: Send + Sync {
fn host(&self) -> PlatformHost;
async fn create_pull_request(&self, pr: &CreatePR) -> Result<PullRequest>;
async fn list_pull_requests(&self, state: &str) -> Result<Vec<PullRequest>>;
async fn get_pull_request(&self, number: u64) -> Result<PullRequest>;
async fn create_issue(&self, issue: &CreateIssue) -> Result<Issue>;
async fn search_issues(&self, query: &str) -> Result<Vec<Issue>>;
async fn add_labels(&self, number: u64, labels: &[String]) -> Result<()>;
async fn create_release(&self, release: &CreateRelease) -> Result<Release>;
async fn list_releases(&self, count: usize) -> Result<Vec<Release>>;
async fn upload_release_asset(
&self,
upload_url: &str,
name: &str,
content_type: &str,
data: Vec<u8>,
) -> Result<()>;
async fn get_check_runs(&self, ref_name: &str) -> Result<CombinedStatus>;
async fn get_authenticated_user(&self) -> Result<String>;
async fn create_repo(&self, repo: &CreateRepo) -> Result<Repository>;
}
pub fn detect_remote(path: &Path) -> Result<RemoteRepo> {
let repo = git2::Repository::open(path).context("Not in a git repository")?;
for remote_name in &["origin", "upstream", "gitlab"] {
if let Ok(remote) = repo.find_remote(remote_name) {
if let Some(url) = remote.url() {
if let Ok(info) = parse_remote_url(url) {
return Ok(info);
}
}
}
}
if let Ok(remotes) = repo.remotes() {
for name in remotes.iter().flatten() {
if let Ok(remote) = repo.find_remote(name) {
if let Some(url) = remote.url() {
if let Ok(info) = parse_remote_url(url) {
return Ok(info);
}
}
}
}
}
bail!("Could not detect a GitHub or GitLab remote. Add one with: securegit remote add origin <url>")
}
fn parse_remote_url(url: &str) -> Result<RemoteRepo> {
if let Ok(source) = RepoSource::parse(url) {
match source {
RepoSource::GitHub { owner, repo, .. } => {
return Ok(RemoteRepo {
host: PlatformHost::GitHub,
owner,
repo,
});
}
RepoSource::GitLab { owner, repo, .. } => {
return Ok(RemoteRepo {
host: PlatformHost::GitLab,
owner,
repo,
});
}
_ => {}
}
}
if url.starts_with("git@github.com:") {
let path = url
.trim_start_matches("git@github.com:")
.trim_end_matches(".git");
let parts: Vec<&str> = path.split('/').collect();
if parts.len() == 2 {
return Ok(RemoteRepo {
host: PlatformHost::GitHub,
owner: parts[0].to_string(),
repo: parts[1].to_string(),
});
}
}
if url.starts_with("git@gitlab.com:") {
let path = url
.trim_start_matches("git@gitlab.com:")
.trim_end_matches(".git");
let parts: Vec<&str> = path.split('/').collect();
if parts.len() == 2 {
return Ok(RemoteRepo {
host: PlatformHost::GitLab,
owner: parts[0].to_string(),
repo: parts[1].to_string(),
});
}
}
let cleaned = url.trim_end_matches(".git").trim_end_matches('/');
if cleaned.contains("github") {
let parts: Vec<&str> = cleaned.split('/').collect();
if parts.len() >= 2 {
let repo = parts[parts.len() - 1].to_string();
let owner = parts[parts.len() - 2].to_string();
return Ok(RemoteRepo {
host: PlatformHost::GitHub,
owner,
repo,
});
}
}
if cleaned.contains("gitlab") {
let parts: Vec<&str> = cleaned.split('/').collect();
if parts.len() >= 2 {
let repo = parts[parts.len() - 1].to_string();
let owner = parts[parts.len() - 2].to_string();
return Ok(RemoteRepo {
host: PlatformHost::GitLab,
owner,
repo,
});
}
}
bail!("Could not parse remote URL as GitHub or GitLab: {}", url)
}
pub fn create_client(remote: &RemoteRepo, token: SecureString) -> Box<dyn Platform> {
match remote.host {
PlatformHost::GitHub => Box::new(github::GitHubClient::new(
token,
remote.owner.clone(),
remote.repo.clone(),
)),
PlatformHost::GitLab => Box::new(gitlab::GitLabClient::new(
token,
remote.owner.clone(),
remote.repo.clone(),
)),
}
}
pub fn create_client_for_server(
server: &server_registry::ServerConfig,
token: SecureString,
owner: &str,
repo: &str,
) -> Box<dyn Platform> {
match server.platform {
server_registry::ServerPlatform::GitHub => Box::new(github::GitHubClient::new_with_base(
token,
owner.to_string(),
repo.to_string(),
server.api_url.clone(),
)),
server_registry::ServerPlatform::GitLab => Box::new(gitlab::GitLabClient::new_with_base(
token,
owner.to_string(),
repo.to_string(),
server.api_url.clone(),
)),
}
}
pub fn resolve_token(host: &PlatformHost) -> Option<SecureString> {
let hostname = match host {
PlatformHost::GitHub => "github.com",
PlatformHost::GitLab => "gitlab.com",
};
crate::auth::token_for_host(hostname)
}