1mod keyring;
29
30use std::collections::HashMap;
31use std::env::var;
32use std::path::{Path, PathBuf};
33
34use dirs::home_dir;
35use serde::{Deserialize, Serialize};
36
37use crate::keyring::{GhKeyring, Keyring};
38
39#[cfg(target_os = "windows")]
40const APP_DATA: &str = "AppData";
41const GH_CONFIG_DIR: &str = "GH_CONFIG_DIR";
42const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME";
43
44const CONFIG_FILE_NAME: &str = "config.yml";
45const HOSTS_FILE_NAME: &str = "hosts.yml";
46
47pub const GITHUB_COM: &str = "github.com";
49pub const GHE_COM: &str = "ghe.com";
50pub const LOCALHOST: &str = "github.localhost";
51
52#[derive(Debug, thiserror::Error)]
54pub enum Error {
55 #[error("Failed to deserialize config from YAML: {0}")]
56 Yaml(#[from] serde_yaml::Error),
57
58 #[error("I/O error: {0}")]
59 Io(#[from] std::io::Error),
60
61 #[error("Secure storage error: {0}")]
62 Keyring(#[from] keyring::Error),
63
64 #[error("Config file not found.")]
65 ConfigNotFound,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70#[serde(rename_all = "snake_case")]
71pub enum GitProtocol {
72 Https,
73 Ssh,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
78#[serde(rename_all = "snake_case")]
79pub enum Prompt {
80 Enabled,
81 Disabled,
82}
83
84impl From<Prompt> for bool {
85 fn from(p: Prompt) -> Self {
86 matches!(p, Prompt::Enabled)
87 }
88}
89
90#[derive(Debug, Clone, Deserialize, Serialize)]
92pub struct Config {
93 pub git_protocol: GitProtocol,
95
96 pub editor: Option<String>,
99
100 pub prompt: Prompt,
103
104 pub pager: Option<String>,
107
108 #[serde(default)]
110 pub aliases: HashMap<String, String>,
111
112 pub http_unix_socket: Option<String>,
115
116 pub browser: Option<String>,
119}
120
121impl Config {
122 pub fn load() -> Result<Self, Error> {
124 Self::load_from(CONFIG_FILE_NAME)
125 }
126
127 pub fn load_from<P>(path: P) -> Result<Self, Error>
129 where
130 P: AsRef<Path>,
131 {
132 load(path)
133 }
134}
135
136#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct Host {
139 pub user: Option<String>,
140 #[serde(default)]
141 oauth_token: String,
142 pub git_protocol: Option<GitProtocol>,
143}
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
147pub struct Hosts(HashMap<String, Host>);
148
149impl Hosts {
150 pub fn load() -> Result<Self, Error> {
152 Self::load_from(HOSTS_FILE_NAME)
153 }
154
155 pub fn load_from<P>(path: P) -> Result<Self, Error>
157 where
158 P: AsRef<Path>,
159 {
160 load(path).map(Self)
161 }
162
163 pub fn get(&self, hostname: &str) -> Option<&Host> {
165 self.0.get(hostname)
166 }
167
168 pub fn set(&mut self, hostname: impl Into<String>, host: Host) -> Option<Host> {
171 self.0.insert(hostname.into(), host)
172 }
173
174 pub fn retrieve_token(&self, hostname: &str) -> Result<Option<String>, Error> {
178 if let Some(token) = retrieve_token_from_env(is_enterprise(hostname)) {
179 return Ok(Some(token));
180 }
181
182 if let Some(token) = self
183 .get(hostname)
184 .and_then(|h| match h.oauth_token.is_empty() {
185 true => None,
186 _ => Some(h.oauth_token.to_owned()),
187 })
188 {
189 return Ok(Some(token));
190 }
191
192 retrieve_token_secure(hostname)
193 }
194
195 #[deprecated(
199 since = "0.4.0",
200 note = "Use `retrieve_token_secure` without `Hosts` struct instead."
201 )]
202 pub fn retrieve_token_secure(&self, hostname: &str) -> Result<Option<String>, Error> {
203 retrieve_token_secure(hostname)
204 }
205}
206
207pub fn is_enterprise(host: &str) -> bool {
209 host != GITHUB_COM && host != LOCALHOST && !host.ends_with(&format!(".{}", GHE_COM))
210}
211
212pub fn retrieve_token_from_env(enterprise: bool) -> Option<String> {
216 if enterprise {
217 if let Ok(token) = var("GH_ENTERPRISE_TOKEN").or_else(|_| var("GITHUB_ENTERPRISE_TOKEN")) {
218 return Some(token);
219 }
220 }
221
222 var("GH_TOKEN").or_else(|_| var("GITHUB_TOKEN")).ok()
223}
224
225pub fn retrieve_token_secure(hostname: &str) -> Result<Option<String>, Error> {
229 Ok(Keyring
230 .get(hostname)?
231 .map(|t| String::from_utf8(t).unwrap()))
232}
233
234pub fn find_config_directory() -> Option<PathBuf> {
236 let gh_config_dir = var(GH_CONFIG_DIR).unwrap_or_default();
237 if !gh_config_dir.is_empty() {
238 return Some(PathBuf::from(gh_config_dir));
239 }
240
241 let xdg_config_home = var(XDG_CONFIG_HOME).unwrap_or_default();
242 if !xdg_config_home.is_empty() {
243 return Some(PathBuf::from(xdg_config_home).join("gh"));
244 }
245
246 #[cfg(target_os = "windows")]
247 {
248 let app_data = var(APP_DATA).unwrap_or_default();
249 if !app_data.is_empty() {
250 return Some(PathBuf::from(app_data).join("GitHub CLI"));
251 }
252 }
253
254 home_dir().map(|p| p.join(".config").join("gh"))
255}
256
257pub fn load<T, P>(path: P) -> Result<T, Error>
259where
260 T: for<'de> Deserialize<'de>,
261 P: AsRef<Path>,
262{
263 serde_yaml::from_slice(
264 std::fs::read(
265 find_config_directory()
266 .ok_or(Error::ConfigNotFound)?
267 .join(path),
268 )
269 .map_err(Error::Io)?
270 .as_ref(),
271 )
272 .map_err(Error::Yaml)
273}