xbp_cli/commands/secrets/
github.rs1use 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}