Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI config file #557

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 34 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::{path::PathBuf, str::FromStr};

use clap::{Parser, Subcommand, ValueEnum};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use tracing::level_filters::LevelFilter;

use crate::format::{CriteriaName, ImportName, PackageName, VersionReq, VetVersion};

pub use self::config::Config;

mod config;

#[derive(Parser)]
#[clap(version, about, long_about = None)]
#[clap(propagate_version = true)]
Expand All @@ -13,6 +17,33 @@ pub enum FakeCli {
Vet(Cli),
}

impl FakeCli {
pub fn parse_with_config(config: &Config) -> Self {
let mut command = Self::command_for_update().mut_subcommand("vet", |vet| {
vet.mut_subcommand("inspect", |inspect| {
inspect.mut_arg("mode", |mode| {
if let Some(default_mode) = config.inspect.mode {
// TODO: clap v4 doesn't require leaking here
mode.default_value(String::leak(
default_mode
.to_possible_value()
.unwrap()
.get_name()
.to_owned(),
))
} else {
mode
}
})
})
});
match Self::from_arg_matches_mut(&mut command.get_matches_mut()) {
Ok(cli) => cli,
Err(err) => err.format(&mut command).exit(),
}
}
}

#[derive(clap::Args)]
#[clap(version)]
#[clap(bin_name = "cargo vet")]
Expand Down Expand Up @@ -789,7 +820,8 @@ impl FromStr for DependencyCriteriaArg {
}
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FetchMode {
Local,
Sourcegraph,
Expand Down
88 changes: 88 additions & 0 deletions src/cli/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::errors::{LoadTomlError, SourceFile, TomlParseError};
use miette::SourceOffset;

use std::path::PathBuf;

#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(default)]
pub inspect: Inspect,
#[serde(default)]
pub(crate) user: Option<crate::UserInfo>,
}

// Can't use types from `errors` because this may error before `miette` is
// configured. We need to collect the data here then transform it into a report
// once the logger is configured.
pub enum LoadConfigError {
TomlParse {
path: PathBuf,
content: String,
line: usize,
col: usize,
error: toml::de::Error,
},

IoError {
path: PathBuf,
error: std::io::Error,
},
}

impl From<LoadConfigError> for miette::Report {
fn from(err: LoadConfigError) -> Self {
match err {
LoadConfigError::TomlParse {
path,
content,
line,
col,
error,
} => TomlParseError {
span: SourceOffset::from_location(&content, line + 1, col + 1),
source_code: SourceFile::new(&path.display().to_string(), content),
error,
}
.into(),

LoadConfigError::IoError { path, error } => {
miette::Report::from(LoadTomlError::from(error))
.context(format!("reading '{}'", path.display()))
}
}
}
}

impl Config {
pub fn load() -> Result<Self, LoadConfigError> {
let Some(config_dir) = dirs::config_dir() else {
return Ok(Self::default());
};
let path = config_dir.join("cargo-vet").join("config.toml");
match std::fs::read_to_string(&path) {
Ok(content) => {
let config = toml::de::from_str(&content).map_err(|error| {
let (line, col) = error.line_col().unwrap_or((0, 0));
LoadConfigError::TomlParse {
path,
content,
line,
col,
error,
}
})?;
Ok(config)
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(error) => Err(LoadConfigError::IoError { path, error }),
}
}
}

#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Inspect {
#[serde(default)]
pub mode: Option<super::FetchMode>,
}
44 changes: 32 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::time::{Duration, SystemTime};
use std::{fs::File, io, panic, path::PathBuf};

use cargo_metadata::{Metadata, Package};
use clap::{CommandFactory, Parser};
use clap::CommandFactory;
use console::Term;
use errors::{
AggregateCriteriaDescription, AggregateCriteriaDescriptionMismatchError,
Expand Down Expand Up @@ -76,6 +76,8 @@ pub struct Config {
pub struct PartialConfig {
/// Details of the CLI invocation (args)
pub cli: Cli,
/// Global config file
pub config: cli::Config,
/// The date and time to use as the current time.
pub now: chrono::DateTime<chrono::Utc>,
/// Path to the cache directory we're using
Expand Down Expand Up @@ -230,7 +232,14 @@ fn main() -> Result<(), ()> {
fn real_main() -> Result<(), miette::Report> {
use cli::Commands::*;

let fake_cli = cli::FakeCli::parse();
let config = cli::Config::load();
// We can't return if this errored here as error logging isn't configured
// yet, instead use a default and then check the error later
let (config, config_err) = match config {
Ok(config) => (config, None),
Err(err) => (cli::Config::default(), Some(err)),
};
let fake_cli = cli::FakeCli::parse_with_config(&config);
let cli::FakeCli::Vet(cli) = fake_cli;

//////////////////////////////////////////////////////
Expand Down Expand Up @@ -327,6 +336,10 @@ fn real_main() -> Result<(), miette::Report> {
set_report_errors_as_json(out.clone());
}

if let Some(err) = config_err {
return Err(miette::Report::from(err).context("failed to load global config"));
}

////////////////////////////////////////////////////
// Potentially handle freestanding commands
////////////////////////////////////////////////////
Expand All @@ -341,6 +354,7 @@ fn real_main() -> Result<(), miette::Report> {
.unwrap_or_else(|| chrono::DateTime::from(SystemTime::now()));
let partial_cfg = PartialConfig {
cli,
config,
now,
cache_dir,
mock_cache: false,
Expand Down Expand Up @@ -771,9 +785,9 @@ fn do_cmd_certify(
};

let (username, who) = if sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
let user_info = get_user_info(&cfg)?;
let who = format!("{} <{}>", user_info.name, user_info.email);
(user_info.name, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
Expand Down Expand Up @@ -1519,9 +1533,9 @@ fn cmd_record_violation(
};

let (_username, who) = if sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
let user_info = get_user_info(&cfg)?;
let who = format!("{} <{}>", user_info.name, user_info.email);
(user_info.name, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
Expand Down Expand Up @@ -2662,12 +2676,14 @@ fn cmd_gc(

// Utils

#[derive(Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct UserInfo {
username: String,
name: String,
email: String,
}

fn get_user_info() -> Result<UserInfo, UserInfoError> {
fn get_user_info(cfg: &Config) -> Result<UserInfo, UserInfoError> {
fn get_git_config(value_name: &str) -> Result<String, CommandError> {
let out = std::process::Command::new("git")
.arg("config")
Expand All @@ -2684,10 +2700,14 @@ fn get_user_info() -> Result<UserInfo, UserInfoError> {
.map_err(CommandError::BadOutput)
}

let username = get_git_config("user.name").map_err(UserInfoError::UserCommandFailed)?;
if let Some(user_info) = &cfg.config.user {
return Ok(user_info.clone());
}

let name = get_git_config("user.name").map_err(UserInfoError::UserCommandFailed)?;
let email = get_git_config("user.email").map_err(UserInfoError::EmailCommandFailed)?;

Ok(UserInfo { username, email })
Ok(UserInfo { name, email })
}

async fn eula_for_criteria(
Expand Down