Skip to main content

xbp_cli/commands/github_cmd/
mod.rs

1//! User-facing GitHub issues management (`xbp github`).
2
3mod client;
4mod interactive;
5
6use crate::cli::commands::{GithubCmd, GithubSubCommand};
7use crate::commands::cloudflare_config::require_interactive_terminal;
8use crate::config::resolve_github_oauth2_key;
9use crate::utils::{
10    find_xbp_config_upwards, git_remote_url_from_metadata, parse_github_repo_from_remote_url,
11};
12use std::env;
13use std::path::PathBuf;
14
15pub use client::{
16    create_comment, create_issue, ensure_label, get_issue, list_comments, list_issues, list_labels,
17    update_issue, CreateIssueInput, GithubComment, GithubIssue, ListIssuesFilter, UpdateIssueInput,
18};
19
20pub async fn run_github(cmd: GithubCmd) -> Result<(), String> {
21    let token = resolve_github_token()?;
22    let (owner, repo) = resolve_repo(cmd.owner.as_deref(), cmd.repo.as_deref())?;
23    match cmd.command {
24        None => interactive::run_hub(&token, &owner, &repo).await,
25        Some(GithubSubCommand::List(args)) => client::run_list(&token, &owner, &repo, args).await,
26        Some(GithubSubCommand::Show(args)) => client::run_show(&token, &owner, &repo, args).await,
27        Some(GithubSubCommand::Create(args)) => {
28            client::run_create(&token, &owner, &repo, args).await
29        }
30        Some(GithubSubCommand::Edit(args)) => client::run_edit(&token, &owner, &repo, args).await,
31        Some(GithubSubCommand::Status(args)) => {
32            client::run_status(&token, &owner, &repo, args).await
33        }
34        Some(GithubSubCommand::Labels(args)) => {
35            client::run_labels(&token, &owner, &repo, args).await
36        }
37        Some(GithubSubCommand::Assign(args)) => {
38            client::run_assign(&token, &owner, &repo, args).await
39        }
40        Some(GithubSubCommand::Comment(args)) => {
41            client::run_comment(&token, &owner, &repo, args).await
42        }
43        Some(GithubSubCommand::Comments(args)) => {
44            client::run_comments(&token, &owner, &repo, args).await
45        }
46    }
47}
48
49pub(crate) fn resolve_github_token() -> Result<String, String> {
50    resolve_github_oauth2_key().ok_or_else(|| {
51        "No GitHub token found. Configure one with `xbp config github set-key` (or set `github_oauth2_key` in global config).".to_string()
52    })
53}
54
55pub(crate) fn resolve_repo(
56    owner_override: Option<&str>,
57    repo_override: Option<&str>,
58) -> Result<(String, String), String> {
59    if let (Some(owner), Some(repo)) = (owner_override, repo_override) {
60        let owner = owner.trim();
61        let repo = repo.trim();
62        if !owner.is_empty() && !repo.is_empty() {
63            return Ok((owner.to_string(), repo.to_string()));
64        }
65    }
66    let project_root = detect_project_root();
67    let remote = git_remote_url_from_metadata(&project_root, "origin")?.ok_or_else(|| {
68        format!(
69            "Could not resolve git remote `origin` under {}. Pass `--owner` and `--repo`.",
70            project_root.display()
71        )
72    })?;
73    parse_github_repo_from_remote_url(&remote).ok_or_else(|| {
74        format!("Remote is not a GitHub URL (`{remote}`). Pass `--owner` and `--repo`.")
75    })
76}
77
78fn detect_project_root() -> PathBuf {
79    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
80    // Prefer the enclosing git work tree so monorepo package `.xbp` configs still
81    // resolve `origin` from the repository root (not the package subfolder).
82    for dir in cwd.ancestors() {
83        if dir.join(".git").exists() {
84            return dir.to_path_buf();
85        }
86    }
87    if let Some(found) = find_xbp_config_upwards(&cwd) {
88        return found.project_root;
89    }
90    cwd
91}
92
93pub(crate) fn require_interactive(context: &str) -> Result<(), String> {
94    require_interactive_terminal(context)
95}