1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
//! # gh-config
//! Loads config and hosts for gh CLI.
//!
//! ## Getting started
//! ```toml
//! [dependencies]
//! gh-config = "0.3"
//! ```
//!
//! ## Usage
//! ```rust
//! use std::error::Error;
//! use gh_config::*;
//!
//! fn main() -> Result<(), Box<dyn Error>> {
//! let config = Config::load()?;
//! let hosts = Hosts::load()?;
//!
//! match hosts.get(GITHUB_COM) {
//! Some(host) => println!("Token for github.com: {}", hosts.retrieve_token(GITHUB_COM)?.unwrap()),
//! _ => eprintln!("Token not found."),
//! }
//!
//! Ok(())
//! }
//! ```
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";
/// Hostname of github.com.
pub const GITHUB_COM: &str = "github.com";
/// An error occurred in this crate.
#[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,
}
/// What protocol to use when performing git operations.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GitProtocol {
Https,
Ssh,
}
/// When to interactively prompt.
#[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)
}
}
/// Config representation for gh CLI.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
/// What protocol to use when performing git operations.
pub git_protocol: GitProtocol,
/// What editor gh should run when creating issues, pull requests, etc.
/// If blank, will refer to environment.
pub editor: Option<String>,
/// When to interactively prompt.
/// This is a global config that cannot be overridden by hostname.
pub prompt: Prompt,
/// A pager program to send command output to, e.g. "less".
/// Set the value to "cat" to disable the pager.
pub pager: Option<String>,
/// Aliases allow you to create nicknames for gh commands.
#[serde(default)]
pub aliases: HashMap<String, String>,
/// The path to a unix socket through which send HTTP connections.
/// If blank, HTTP traffic will be handled by default transport.
pub http_unix_socket: Option<String>,
/// What web browser gh should use when opening URLs.
/// If blank, will refer to environment.
pub browser: Option<String>,
}
impl Config {
/// Loads a config from the default path.
pub fn load() -> Result<Self, Error> {
Self::load_from(CONFIG_FILE_NAME)
}
/// Loads all host configs from the specified path.
pub fn load_from<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
load(path)
}
}
/// Host config representation for gh CLI.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Host {
pub user: Option<String>,
#[serde(default)]
oauth_token: String,
pub git_protocol: Option<GitProtocol>,
}
/// Mapped host configs by their hostname.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Hosts(HashMap<String, Host>);
impl Hosts {
/// Loads all host configs from the default path.
pub fn load() -> Result<Self, Error> {
Self::load_from(HOSTS_FILE_NAME)
}
/// Loads all host configs from the specified path.
pub fn load_from<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
load(path).map(Self)
}
/// Gets a host config by the hostname.
pub fn get(&self, hostname: &str) -> Option<&Host> {
self.0.get(hostname)
}
/// Sets a host config and returns the current value.
/// If no values present currently, returns `None` .
pub fn set(&mut self, hostname: impl Into<String>, host: Host) -> Option<Host> {
self.0.insert(hostname.into(), host)
}
/// Retrieves a token from the secure storage or insecure storage.
/// User interaction may be required to unlock the keychain, depending on the OS.
/// If any token found for the hostname, returns None.
#[allow(deprecated)]
pub fn retrieve_token(&self, hostname: &str) -> Result<Option<String>, Error> {
Ok(self.retrieve_token_secure(hostname)?.or_else(|| {
self.get(hostname)
.and_then(|h| match h.oauth_token.is_empty() {
true => None,
_ => Some(h.oauth_token.to_owned()),
})
}))
}
/// Retrieves a token from the secure storage only.
/// User interaction may be required to unlock the keychain, depending on the OS.
/// If any token found for the hostname, returns None.
pub fn retrieve_token_secure(&self, hostname: &str) -> Result<Option<String>, Error> {
Ok(Keyring
.get(hostname)?
.map(|t| String::from_utf8(t).unwrap()))
}
}
/// Finds the default config directory effected by the environment.
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"))
}
/// Loads a file in the config directory as `T` type.
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)
}