diff --git a/Cargo.lock b/Cargo.lock index c78e08663..6514754ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,7 @@ dependencies = [ "embed-resource", "file-format", "fs-lock", + "gh-token", "log", "miette", "mimalloc", @@ -803,6 +804,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "gh-token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc623f67e9004eac91ad251d7d4c0b1fa7ac53646c09d9c8ef6e27a21b2d02fd" +dependencies = [ + "home", + "serde", + "serde_derive", + "serde_yaml", +] + [[package]] name = "gimli" version = "0.27.2" @@ -1964,6 +1977,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -2541,6 +2567,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unsafe-libyaml" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 2c717027a..805de46ec 100644 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -29,6 +29,7 @@ compact_str = "0.7.0" dirs = "4.0.0" file-format = { version = "0.14.0", default-features = false } fs-lock = { version = "0.1.0", path = "../fs-lock" } +gh-token = "0.1.0" log = { version = "0.4.17", features = ["std"] } miette = "5.5.0" mimalloc = { version = "0.1.34", default-features = false, optional = true } diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs index 0668c0935..f24012886 100644 --- a/crates/bin/src/args.rs +++ b/crates/bin/src/args.rs @@ -1,7 +1,7 @@ use std::{ env, ffi::OsString, - fmt, + fmt, fs, iter, num::{NonZeroU64, ParseIntError}, path::PathBuf, str::FromStr, @@ -14,6 +14,7 @@ use binstalk::{ }; use clap::{error::ErrorKind, CommandFactory, Parser, ValueEnum}; use compact_str::CompactString; +use dirs::home_dir; use log::LevelFilter; use semver::VersionReq; use strum::EnumCount; @@ -145,6 +146,14 @@ pub struct Args { #[clap(help_heading = "Overrides", long, value_delimiter(','))] pub disable_strategies: Vec, + /// If `--github-token` or environment variable `GITHUB_TOKEN` is not + /// specified, then cargo-binstall will try to extract github token from + /// `$HOME/.git-credentials` or `$HOME/.config/gh/hosts.yml` by default. + /// + /// This option can be used to disable that behavior. + #[clap(help_heading = "Overrides", long)] + pub no_discover_github_token: bool, + /// Disable symlinking / versioned updates. /// /// By default, Binstall will install a binary named `-` in the install path, and @@ -452,9 +461,52 @@ You cannot use --{option} and specify multiple packages at the same time. Do one .exit() } + if opts.github_token.is_none() && !opts.no_discover_github_token { + if let Some(github_token) = try_extract_from_git_credentials() { + opts.github_token = Some(github_token); + } else if let Ok(github_token) = gh_token::get() { + opts.github_token = Some(github_token.into()); + } + } + opts } +fn try_extract_from_git_credentials() -> Option { + home_dir() + .map(|mut home| { + home.push(".git-credentials"); + home + }) + .into_iter() + .chain(iter::from_fn(|| { + let home = env::var_os("XDG_CONFIG_HOME")?; + (!home.is_empty()).then(|| { + let mut path = PathBuf::from(home); + path.push("git/credentials"); + path + }) + })) + .find_map(try_extract_from_git_credentials_from) +} + +fn try_extract_from_git_credentials_from(path: PathBuf) -> Option { + fs::read_to_string(path) + .ok()? + .lines() + .find_map(extract_github_token_from_git_credentials_line) + .map(CompactString::from) +} + +fn extract_github_token_from_git_credentials_line(line: &str) -> Option<&str> { + let cred = line + .trim() + .strip_prefix("https://")? + .strip_suffix("@github.com")?; + + Some(cred.split_once(':')?.1) +} + #[cfg(test)] mod test { use super::*; @@ -463,4 +515,24 @@ mod test { fn verify_cli() { Args::command().debug_assert() } + + const GIT_CREDENTIALS_TEST_CASES: &[(&str, Option<&str>)] = &[ + // Success + ("https://NobodyXu:gho_asdc@github.com", Some("gho_asdc")), + ( + "https://NobodyXu:gho_asdc12dz@github.com", + Some("gho_asdc12dz"), + ), + // Failure + ("http://NobodyXu:gho_asdc@github.com", None), + ("https://NobodyXu:gho_asdc@gitlab.com", None), + ("https://NobodyXugho_asdc@github.com", None), + ]; + + #[test] + fn test_extract_github_token_from_git_credentials_line() { + GIT_CREDENTIALS_TEST_CASES.iter().for_each(|(line, res)| { + assert_eq!(extract_github_token_from_git_credentials_line(line), *res); + }) + } }