wsb-github 0.251.6311

GitHub issues, pull requests, gists and Projects boards, mirrored once per machine for every project under $PRJ
Documentation
//! Where the GitHub token comes from.
//!
//! The GitHub CLI's own login first — `gh auth token`, which reads the token
//! `gh auth login` stored in the operating system's credential store — and
//! then `GITHUB_TOKEN` in the environment. Nothing else is consulted, and no
//! token is ever written anywhere by this crate: one place to log in, and one
//! answer to "which token is this using".

use std::fmt;
use std::process::Command;

/// Which of the two places answered.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
    /// `gh auth token`.
    GhLogin,
    /// The `GITHUB_TOKEN` environment variable.
    Environment,
}

impl fmt::Display for Source {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::GhLogin => "the GitHub CLI's login (gh auth token)",
            Self::Environment => "GITHUB_TOKEN",
        })
    }
}

/// A token, and where it came from. Its value is never printed.
#[derive(Clone)]
pub struct Token {
    value: String,
    pub source: Source,
}

impl Token {
    pub fn value(&self) -> &str {
        &self.value
    }
}

impl fmt::Debug for Token {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Token").field("source", &self.source).finish_non_exhaustive()
    }
}

/// Why no token could be had: both places, and what each said.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoToken {
    pub gh: String,
}

impl fmt::Display for NoToken {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "no GitHub token: {}, and GITHUB_TOKEN is not set. Log in with `gh auth login` \
             (with the `project` scope for boards: `gh auth refresh -s project`), or set GITHUB_TOKEN",
            self.gh
        )
    }
}

impl std::error::Error for NoToken {}

/// The token to use, from the GitHub CLI's login or else the environment.
pub fn resolve() -> Result<Token, NoToken> {
    let gh = match Command::new("gh").args(["auth", "token"]).output() {
        Ok(out) if out.status.success() => {
            let value = String::from_utf8_lossy(&out.stdout).trim().to_string();
            if !value.is_empty() {
                return Ok(Token {
                    value,
                    source: Source::GhLogin,
                });
            }
            "the GitHub CLI answered with no token".to_string()
        }
        Ok(out) => format!(
            "the GitHub CLI has no login ({})",
            String::from_utf8_lossy(&out.stderr).trim()
        ),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            "the GitHub CLI (gh) is not installed".to_string()
        }
        Err(error) => format!("the GitHub CLI could not be run ({error})"),
    };
    match std::env::var("GITHUB_TOKEN") {
        Ok(value) if !value.trim().is_empty() => Ok(Token {
            value: value.trim().to_string(),
            source: Source::Environment,
        }),
        _ => Err(NoToken { gh }),
    }
}