Skip to main content

git_same/auth/
gh_cli.rs

1//! GitHub CLI authentication integration.
2//!
3//! Uses the `gh` CLI tool to obtain authentication tokens securely.
4
5use crate::errors::AppError;
6use std::process::Command;
7
8/// Check if the GitHub CLI is installed.
9pub fn is_installed() -> bool {
10    Command::new("gh")
11        .arg("--version")
12        .output()
13        .map(|o| o.status.success())
14        .unwrap_or(false)
15}
16
17/// Check if the user is authenticated with the GitHub CLI.
18pub fn is_authenticated() -> bool {
19    Command::new("gh")
20        .args(["auth", "status"])
21        .output()
22        .map(|o| o.status.success())
23        .unwrap_or(false)
24}
25
26/// Get the authentication token from the GitHub CLI.
27pub fn get_token() -> Result<String, AppError> {
28    let output = Command::new("gh")
29        .args(["auth", "token"])
30        .output()
31        .map_err(|e| AppError::auth(format!("Failed to run 'gh auth token': {}", e)))?;
32
33    if !output.status.success() {
34        let stderr = String::from_utf8_lossy(&output.stderr);
35        return Err(AppError::auth(format!(
36            "gh auth token failed: {}",
37            stderr.trim()
38        )));
39    }
40
41    let token = String::from_utf8(output.stdout)
42        .map_err(|_| AppError::auth("Invalid UTF-8 in token output"))?
43        .trim()
44        .to_string();
45
46    if token.is_empty() {
47        return Err(AppError::auth("gh auth token returned empty token"));
48    }
49
50    Ok(token)
51}
52
53/// Get the authenticated GitHub username.
54pub fn get_username() -> Result<String, AppError> {
55    let output = Command::new("gh")
56        .args(["api", "user", "--jq", ".login"])
57        .output()
58        .map_err(|e| AppError::auth(format!("Failed to get username from gh: {}", e)))?;
59
60    if !output.status.success() {
61        let stderr = String::from_utf8_lossy(&output.stderr);
62        return Err(AppError::auth(format!(
63            "Failed to get username: {}",
64            stderr.trim()
65        )));
66    }
67
68    let username = String::from_utf8(output.stdout)
69        .map_err(|_| AppError::auth("Invalid UTF-8 in username output"))?
70        .trim()
71        .to_string();
72
73    if username.is_empty() {
74        return Err(AppError::auth("gh returned empty username"));
75    }
76
77    Ok(username)
78}
79
80/// Get token for a specific GitHub host (for GitHub Enterprise).
81pub fn get_token_for_host(host: &str) -> Result<String, AppError> {
82    let output = Command::new("gh")
83        .args(["auth", "token", "--hostname", host])
84        .output()
85        .map_err(|e| {
86            AppError::auth(format!(
87                "Failed to run 'gh auth token --hostname {}': {}",
88                host, e
89            ))
90        })?;
91
92    if !output.status.success() {
93        let stderr = String::from_utf8_lossy(&output.stderr);
94        return Err(AppError::auth(format!(
95            "gh auth token for {} failed: {}",
96            host,
97            stderr.trim()
98        )));
99    }
100
101    let token = String::from_utf8(output.stdout)
102        .map_err(|_| AppError::auth("Invalid UTF-8 in token output"))?
103        .trim()
104        .to_string();
105
106    if token.is_empty() {
107        return Err(AppError::auth(format!(
108            "gh auth token for {} returned empty token",
109            host
110        )));
111    }
112
113    Ok(token)
114}
115
116#[cfg(test)]
117#[path = "gh_cli_tests.rs"]
118mod tests;