mod keyring;
use std::collections::HashMap;
use std::env::var;
use std::path::{Path, PathBuf};
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use crate::keyring::{GhKeyring, Keyring};
#[cfg(target_os = "windows")]
const APP_DATA: &str = "AppData";
const GH_CONFIG_DIR: &str = "GH_CONFIG_DIR";
const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME";
const CONFIG_FILE_NAME: &str = "config.yml";
const HOSTS_FILE_NAME: &str = "hosts.yml";
pub const GITHUB_COM: &str = "github.com";
pub const GHE_COM: &str = "ghe.com";
pub const LOCALHOST: &str = "github.localhost";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to deserialize config from YAML: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Secure storage error: {0}")]
Keyring(#[from] keyring::Error),
#[error("Config file not found.")]
ConfigNotFound,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GitProtocol {
Https,
Ssh,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Prompt {
Enabled,
Disabled,
}
impl From<Prompt> for bool {
fn from(p: Prompt) -> Self {
matches!(p, Prompt::Enabled)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub git_protocol: GitProtocol,
pub editor: Option<String>,
pub prompt: Prompt,
pub pager: Option<String>,
#[serde(default)]
pub aliases: HashMap<String, String>,
pub http_unix_socket: Option<String>,
pub browser: Option<String>,
}
impl Config {
pub fn load() -> Result<Self, Error> {
Self::load_from(CONFIG_FILE_NAME)
}
pub fn load_from<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
load(path)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Host {
pub user: Option<String>,
#[serde(default)]
oauth_token: String,
pub git_protocol: Option<GitProtocol>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Hosts(HashMap<String, Host>);
impl Hosts {
pub fn load() -> Result<Self, Error> {
Self::load_from(HOSTS_FILE_NAME)
}
pub fn load_from<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
load(path).map(Self)
}
pub fn get(&self, hostname: &str) -> Option<&Host> {
self.0.get(hostname)
}
pub fn set(&mut self, hostname: impl Into<String>, host: Host) -> Option<Host> {
self.0.insert(hostname.into(), host)
}
pub fn retrieve_token(&self, hostname: &str) -> Result<Option<String>, Error> {
if let Some(token) = retrieve_token_from_env(is_enterprise(hostname)) {
return Ok(Some(token));
}
if let Some(token) = self
.get(hostname)
.and_then(|h| match h.oauth_token.is_empty() {
true => None,
_ => Some(h.oauth_token.to_owned()),
})
{
return Ok(Some(token));
}
retrieve_token_secure(hostname)
}
#[deprecated(
since = "0.4.0",
note = "Use `retrieve_token_secure` without `Hosts` struct instead."
)]
pub fn retrieve_token_secure(&self, hostname: &str) -> Result<Option<String>, Error> {
retrieve_token_secure(hostname)
}
}
pub fn is_enterprise(host: &str) -> bool {
host != GITHUB_COM && host != LOCALHOST && !host.ends_with(&format!(".{}", GHE_COM))
}
pub fn retrieve_token_from_env(enterprise: bool) -> Option<String> {
if enterprise {
if let Ok(token) = var("GH_ENTERPRISE_TOKEN").or_else(|_| var("GITHUB_ENTERPRISE_TOKEN")) {
return Some(token);
}
}
var("GH_TOKEN").or_else(|_| var("GITHUB_TOKEN")).ok()
}
pub fn retrieve_token_secure(hostname: &str) -> Result<Option<String>, Error> {
Ok(Keyring
.get(hostname)?
.map(|t| String::from_utf8(t).unwrap()))
}
pub fn find_config_directory() -> Option<PathBuf> {
let gh_config_dir = var(GH_CONFIG_DIR).unwrap_or_default();
if !gh_config_dir.is_empty() {
return Some(PathBuf::from(gh_config_dir));
}
let xdg_config_home = var(XDG_CONFIG_HOME).unwrap_or_default();
if !xdg_config_home.is_empty() {
return Some(PathBuf::from(xdg_config_home).join("gh"));
}
#[cfg(target_os = "windows")]
{
let app_data = var(APP_DATA).unwrap_or_default();
if !app_data.is_empty() {
return Some(PathBuf::from(app_data).join("GitHub CLI"));
}
}
home_dir().map(|p| p.join(".config").join("gh"))
}
pub fn load<T, P>(path: P) -> Result<T, Error>
where
T: for<'de> Deserialize<'de>,
P: AsRef<Path>,
{
serde_yaml::from_slice(
std::fs::read(
find_config_directory()
.ok_or(Error::ConfigNotFound)?
.join(path),
)
.map_err(Error::Io)?
.as_ref(),
)
.map_err(Error::Yaml)
}