xbp 10.30.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::config::resolve_github_oauth2_key;
use crate::utils::{
    command_exists, git_remote_url_from_metadata, parse_github_repo_from_remote_url,
};
use std::path::Path;
use tokio::process::Command;

pub async fn resolve_repository(
    project_root: &Path,
    repo_override: Option<&str>,
) -> Result<(String, String), String> {
    if let Some(value) = repo_override {
        return parse_repo_override(value);
    }

    if let Some(url) = git_remote_url_from_metadata(project_root, "origin")? {
        return parse_detected_repo_url(&url);
    }

    if !command_exists("git") {
        return Err(
            "Failed to detect GitHub repository. Is 'origin' remote configured?".to_string(),
        );
    }

    let output = Command::new("git")
        .arg("remote")
        .arg("get-url")
        .arg("origin")
        .current_dir(project_root)
        .output()
        .await
        .map_err(|error| format!("Failed to run git: {}", error))?;

    if !output.status.success() {
        return Err(
            "Failed to detect GitHub repository. Is 'origin' remote configured?".to_string(),
        );
    }

    let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
    parse_detected_repo_url(&url)
}

pub fn needs_repo_setup(error: &str) -> bool {
    error.contains("origin' remote configured")
        || error.contains("Git remote URL is empty")
        || error.contains("Origin remote is not a GitHub repository URL")
        || error.contains("Unexpected GitHub remote format")
}

pub async fn resolve_token(token_override: Option<&str>) -> Result<String, String> {
    if let Some(token) = token_override {
        let token = token.trim();
        if !token.is_empty() {
            return Ok(token.to_string());
        }
    }

    if let Some(token) = resolve_github_oauth2_key() {
        return Ok(token);
    }

    if command_exists("gh") {
        let output = Command::new("gh")
            .arg("auth")
            .arg("token")
            .output()
            .await
            .map_err(|error| format!("Failed to run `gh auth token`: {}", error))?;
        if output.status.success() {
            let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !token.is_empty() {
                return Ok(token);
            }
        }
    }

    Err("No GitHub token found. Use `--token`, export `GITHUB_TOKEN`, run `xbp config github set-key`, or authenticate `gh`.".to_string())
}

fn parse_repo_override(value: &str) -> Result<(String, String), String> {
    let trimmed = value.trim().trim_end_matches(".git");
    let parts = trimmed
        .split('/')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if parts.len() != 2 {
        return Err(format!(
            "Invalid repository override `{}`; expected owner/repo",
            value
        ));
    }
    Ok((parts[0].to_string(), parts[1].to_string()))
}

fn parse_detected_repo_url(url: &str) -> Result<(String, String), String> {
    let trimmed = url.trim();
    if trimmed.is_empty() {
        return Err("Git remote URL is empty.".to_string());
    }

    if let Some((owner, repo)) = parse_github_repo_from_remote_url(trimmed) {
        return Ok((owner, repo));
    }

    if trimmed.contains("github.com/") || trimmed.contains("github.com:") {
        return Err(format!("Unexpected GitHub remote format: {}", trimmed));
    }

    Err("Origin remote is not a GitHub repository URL.".to_string())
}