Skip to main content

xbp_cli/commands/secrets/
github.rs

1use crate::config::resolve_github_oauth2_key;
2use crate::utils::{
3    command_exists, git_remote_url_from_metadata, parse_github_repo_from_remote_url,
4};
5use std::path::Path;
6use tokio::process::Command;
7
8pub async fn resolve_repository(
9    project_root: &Path,
10    repo_override: Option<&str>,
11) -> Result<(String, String), String> {
12    if let Some(value) = repo_override {
13        return parse_repo_override(value);
14    }
15
16    if let Some(url) = git_remote_url_from_metadata(project_root, "origin")? {
17        return parse_detected_repo_url(&url);
18    }
19
20    if !command_exists("git") {
21        return Err(
22            "Failed to detect GitHub repository. Is 'origin' remote configured?".to_string(),
23        );
24    }
25
26    let output = Command::new("git")
27        .arg("remote")
28        .arg("get-url")
29        .arg("origin")
30        .current_dir(project_root)
31        .output()
32        .await
33        .map_err(|error| format!("Failed to run git: {}", error))?;
34
35    if !output.status.success() {
36        return Err(
37            "Failed to detect GitHub repository. Is 'origin' remote configured?".to_string(),
38        );
39    }
40
41    let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
42    parse_detected_repo_url(&url)
43}
44
45pub fn needs_repo_setup(error: &str) -> bool {
46    error.contains("origin' remote configured")
47        || error.contains("Git remote URL is empty")
48        || error.contains("Origin remote is not a GitHub repository URL")
49        || error.contains("Unexpected GitHub remote format")
50}
51
52pub async fn resolve_token(token_override: Option<&str>) -> Result<String, String> {
53    if let Some(token) = token_override {
54        let token = token.trim();
55        if !token.is_empty() {
56            return Ok(token.to_string());
57        }
58    }
59
60    if let Some(token) = resolve_github_oauth2_key() {
61        return Ok(token);
62    }
63
64    if command_exists("gh") {
65        let output = Command::new("gh")
66            .arg("auth")
67            .arg("token")
68            .output()
69            .await
70            .map_err(|error| format!("Failed to run `gh auth token`: {}", error))?;
71        if output.status.success() {
72            let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
73            if !token.is_empty() {
74                return Ok(token);
75            }
76        }
77    }
78
79    Err("No GitHub token found. Use `--token`, export `GITHUB_TOKEN`, run `xbp config github set-key`, or authenticate `gh`.".to_string())
80}
81
82fn parse_repo_override(value: &str) -> Result<(String, String), String> {
83    let trimmed = value.trim().trim_end_matches(".git");
84    let parts = trimmed
85        .split('/')
86        .filter(|segment| !segment.is_empty())
87        .collect::<Vec<_>>();
88    if parts.len() != 2 {
89        return Err(format!(
90            "Invalid repository override `{}`; expected owner/repo",
91            value
92        ));
93    }
94    Ok((parts[0].to_string(), parts[1].to_string()))
95}
96
97fn parse_detected_repo_url(url: &str) -> Result<(String, String), String> {
98    let trimmed = url.trim();
99    if trimmed.is_empty() {
100        return Err("Git remote URL is empty.".to_string());
101    }
102
103    if let Some((owner, repo)) = parse_github_repo_from_remote_url(trimmed) {
104        return Ok((owner, repo));
105    }
106
107    if trimmed.contains("github.com/") || trimmed.contains("github.com:") {
108        return Err(format!("Unexpected GitHub remote format: {}", trimmed));
109    }
110
111    Err("Origin remote is not a GitHub repository URL.".to_string())
112}