use std::process::Command;
use anyhow::Result;
use super::ForgeKind;
pub fn resolve_token(kind: ForgeKind, config_env: Option<&str>) -> Result<String> {
if let Some(env_name) = config_env
&& let Ok(val) = std::env::var(env_name)
&& !val.is_empty()
{
return Ok(val);
}
for var in &default_env_vars(kind) {
if let Ok(val) = std::env::var(var)
&& !val.is_empty()
{
return Ok(val);
}
}
if let Some(token) = cli_fallback(kind) {
return Ok(token);
}
let primary_var = config_env.unwrap_or(kind.token_env_var());
match kind {
ForgeKind::GitHub => anyhow::bail!(
"GitHub token not found. Either:\n \
- Run `gh auth login`, or\n \
- Set {primary_var} environment variable"
),
ForgeKind::GitLab => anyhow::bail!(
"GitLab token not found. Either:\n \
- Run `glab auth login`, or\n \
- Set {primary_var} environment variable"
),
ForgeKind::Forgejo => anyhow::bail!(
"{primary_var} not set. Generate a token from your Forgejo/Codeberg \
account settings and export it."
),
}
}
fn default_env_vars(kind: ForgeKind) -> Vec<&'static str> {
match kind {
ForgeKind::GitHub => vec!["GITHUB_TOKEN", "GH_TOKEN"],
ForgeKind::GitLab => vec!["GITLAB_TOKEN"],
ForgeKind::Forgejo => vec!["FORGEJO_TOKEN"],
}
}
fn cli_fallback(kind: ForgeKind) -> Option<String> {
match kind {
ForgeKind::GitHub => gh_auth_token(),
ForgeKind::GitLab => glab_auth_token(),
ForgeKind::Forgejo => None,
}
}
fn gh_auth_token() -> Option<String> {
let output = Command::new("gh")
.args(["auth", "token"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if token.is_empty() { None } else { Some(token) }
}
fn glab_auth_token() -> Option<String> {
let output = Command::new("glab")
.args(["auth", "status", "-t"])
.output()
.ok()?;
let stderr = String::from_utf8_lossy(&output.stderr);
for line in stderr.lines() {
let trimmed = line.trim().trim_start_matches('✓').trim();
let rest = trimmed
.strip_prefix("Token found:")
.or_else(|| trimmed.strip_prefix("Token:"));
if let Some(rest) = rest {
let token = rest.trim().to_string();
if !token.is_empty() {
return Some(token);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_env_vars_github() {
let vars = default_env_vars(ForgeKind::GitHub);
assert_eq!(vars, vec!["GITHUB_TOKEN", "GH_TOKEN"]);
}
#[test]
fn test_default_env_vars_gitlab() {
let vars = default_env_vars(ForgeKind::GitLab);
assert_eq!(vars, vec!["GITLAB_TOKEN"]);
}
#[test]
fn test_default_env_vars_forgejo() {
let vars = default_env_vars(ForgeKind::Forgejo);
assert_eq!(vars, vec!["FORGEJO_TOKEN"]);
}
#[test]
fn test_resolve_token_error_mentions_custom_env() {
let var_name = "JJPR_TEST_NONEXISTENT_TOKEN_42_ZZZZZ";
let result = resolve_token(ForgeKind::Forgejo, Some(var_name));
let err = result.expect_err("should fail");
assert!(
err.to_string().contains(var_name),
"error should mention {var_name}: {err}"
);
}
#[test]
fn test_cli_fallback_forgejo_returns_none() {
assert!(cli_fallback(ForgeKind::Forgejo).is_none());
}
}