use std::path::Path;
use std::process::Command;
use crate::RhoResult;
pub const PROVIDER: &str = "github";
#[derive(Debug, Clone, Copy)]
pub struct GithubIdentityProvider;
impl super::IdentityProvider for GithubIdentityProvider {
fn provider(&self) -> &'static str {
PROVIDER
}
fn validate_handle(&self, handle: &str) -> RhoResult<()> {
validate_handle(handle)
}
}
pub fn identity_id(handle: &str) -> RhoResult<String> {
<GithubIdentityProvider as super::IdentityProvider>::identity_id(
&GithubIdentityProvider,
handle,
)
}
pub fn handle_from_identity_id(identity_id: &str) -> RhoResult<String> {
<GithubIdentityProvider as super::IdentityProvider>::handle_from_identity_id(
&GithubIdentityProvider,
identity_id,
)
}
pub fn provider_url(handle: &str) -> RhoResult<String> {
validate_handle(handle)?;
Ok(format!("https://github.com/{handle}"))
}
pub fn handle_from_provider_url(provider_url: &str) -> RhoResult<String> {
let Some(handle) = provider_url.strip_prefix("https://github.com/") else {
return Err(format!("unsupported github provider_url: {provider_url}").into());
};
let handle = handle.trim_end_matches('/');
if handle.contains('/') || handle.contains('#') || handle.contains('?') {
return Err(format!("unsupported github provider_url: {provider_url}").into());
}
validate_handle(handle)?;
Ok(handle.to_string())
}
pub fn validate_handle(handle: &str) -> RhoResult<()> {
if handle.is_empty() || handle.len() > 39 {
return Err("github handle must be 1-39 characters".into());
}
if handle.starts_with('-') || handle.ends_with('-') {
return Err(format!("invalid github handle: {handle}").into());
}
let valid = handle
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-');
if !valid || handle.contains("--") {
return Err(format!("invalid github handle: {handle}").into());
}
Ok(())
}
pub fn repo_candidate_from_remote(remote: &str) -> Option<String> {
repo_candidate_from_remote_with_host_resolver(remote, ssh_host_is_github)
}
pub fn repo_candidate_from_remote_with_host_resolver<F>(
remote: &str,
host_is_github: F,
) -> Option<String>
where
F: Fn(&str) -> bool,
{
if let Some(path) = remote.strip_prefix("https://github.com/") {
return slug_from_remote_path(path);
}
if let Some(path) = remote.strip_prefix("git@github.com:") {
return slug_from_remote_path(path);
}
if let Some(rest) = remote.strip_prefix("git@")
&& let Some((host, path)) = rest.split_once(':')
&& (host == "github.com" || host_is_github(host))
{
return slug_from_remote_path(path);
}
if let Some(rest) = remote.strip_prefix("ssh://git@")
&& let Some((host, path)) = rest.split_once('/')
&& (host == "github.com" || host_is_github(host))
{
return slug_from_remote_path(path);
}
None
}
fn slug_from_remote_path(path: &str) -> Option<String> {
let path = path.trim_end_matches(".git").trim_matches('/');
let mut parts = path.split('/');
let owner = parts.next()?;
let repo = parts.next()?;
if parts.next().is_some() || owner.is_empty() || repo.is_empty() {
return None;
}
Some(format!("{owner}/{repo}"))
}
fn ssh_host_is_github(host: &str) -> bool {
let output = Command::new("ssh").args(["-G", host]).output();
let Ok(output) = output else {
return false;
};
if !output.status.success() {
return false;
}
let config = String::from_utf8_lossy(&output.stdout);
config.lines().any(|line| {
let mut fields = line.split_whitespace();
matches!(
(fields.next(), fields.next(), fields.next()),
(Some("hostname"), Some("github.com"), None)
)
})
}
pub fn create_pull_request(
root: &Path,
title: &str,
body: &str,
open_browser: bool,
) -> RhoResult<String> {
let existing = Command::new("gh")
.current_dir(root)
.args(["pr", "view", "--json", "url", "--jq", ".url"])
.output();
if let Ok(existing) = existing
&& existing.status.success()
{
let url = String::from_utf8(existing.stdout)?.trim().to_string();
if !url.is_empty() {
return Ok(url);
}
}
let mut command = Command::new("gh");
command
.current_dir(root)
.args(["pr", "create", "--title", title, "--body", body]);
if open_browser {
command.arg("--web");
}
let output = command.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("gh pr create failed: {}", stderr.trim()).into());
}
Ok(String::from_utf8(output.stdout)?.trim().to_string())
}