use clap::{
builder::{
TypedValueParser,
ValueParserFactory,
},
error::{
ContextKind,
ContextValue,
ErrorKind,
},
ArgAction,
Parser,
ValueEnum,
};
use git_cliff_core::{
config::Remote,
DEFAULT_CONFIG,
DEFAULT_OUTPUT,
};
use glob::Pattern;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Strip {
Header,
Footer,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Sort {
Oldest,
Newest,
}
#[derive(Debug, Parser)]
#[command(
version,
author = clap::crate_authors!("\n"),
about,
rename_all_env = "screaming-snake",
help_template = "\
{before-help}{name} {version}
{author-with-newline}{about-with-newline}
{usage-heading}
{usage}
{all-args}{after-help}
",
override_usage = "git-cliff [FLAGS] [OPTIONS] [--] [RANGE]",
next_help_heading = Some("OPTIONS"),
disable_help_flag = true,
disable_version_flag = true,
)]
pub struct Opt {
#[arg(
short,
long,
action = ArgAction::Help,
global = true,
help = "Prints help information",
help_heading = "FLAGS"
)]
pub help: Option<bool>,
#[arg(
short = 'V',
long,
action = ArgAction::Version,
global = true,
help = "Prints version information",
help_heading = "FLAGS"
)]
pub version: Option<bool>,
#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
pub verbose: u8,
#[arg(
short,
long,
value_name = "CONFIG",
num_args = 0..=1,
required = false
)]
pub init: Option<Option<String>>,
#[arg(
short,
long,
env = "GIT_CLIFF_CONFIG",
value_name = "PATH",
default_value = DEFAULT_CONFIG,
value_parser = Opt::parse_dir
)]
pub config: PathBuf,
#[arg(
short,
long,
env = "GIT_CLIFF_WORKDIR",
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub workdir: Option<PathBuf>,
#[arg(
short,
long,
env = "GIT_CLIFF_REPOSITORY",
value_name = "PATH",
num_args(1..),
value_parser = Opt::parse_dir
)]
pub repository: Option<Vec<PathBuf>>,
#[arg(
long,
env = "GIT_CLIFF_INCLUDE_PATH",
value_name = "PATTERN",
num_args(1..)
)]
pub include_path: Option<Vec<Pattern>>,
#[arg(
long,
env = "GIT_CLIFF_EXCLUDE_PATH",
value_name = "PATTERN",
num_args(1..)
)]
pub exclude_path: Option<Vec<Pattern>>,
#[arg(
long,
env = "GIT_CLIFF_WITH_COMMIT",
value_name = "MSG",
num_args(1..)
)]
pub with_commit: Option<Vec<String>>,
#[arg(
long,
env = "GIT_CLIFF_SKIP_COMMIT",
value_name = "SHA1",
num_args(1..)
)]
pub skip_commit: Option<Vec<String>>,
#[arg(
short,
long,
env = "GIT_CLIFF_PREPEND",
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub prepend: Option<PathBuf>,
#[arg(
short,
long,
env = "GIT_CLIFF_OUTPUT",
value_name = "PATH",
value_parser = Opt::parse_dir,
num_args = 0..=1,
default_missing_value = DEFAULT_OUTPUT
)]
pub output: Option<PathBuf>,
#[arg(
short,
long,
env = "GIT_CLIFF_TAG",
value_name = "TAG",
allow_hyphen_values = true
)]
pub tag: Option<String>,
#[arg(long, help_heading = Some("FLAGS"))]
pub bump: bool,
#[arg(long, help_heading = Some("FLAGS"))]
pub bumped_version: bool,
#[arg(
short,
long,
env = "GIT_CLIFF_TEMPLATE",
value_name = "TEMPLATE",
allow_hyphen_values = true
)]
pub body: Option<String>,
#[arg(short, long, help_heading = Some("FLAGS"))]
pub latest: bool,
#[arg(long, help_heading = Some("FLAGS"))]
pub current: bool,
#[arg(short, long, help_heading = Some("FLAGS"))]
pub unreleased: bool,
#[arg(long, help_heading = Some("FLAGS"))]
pub topo_order: bool,
#[arg(long, help_heading = Some("FLAGS"))]
pub no_exec: bool,
#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
pub context: bool,
#[arg(short, long, value_name = "PART", value_enum)]
pub strip: Option<Strip>,
#[arg(
long,
value_enum,
default_value_t = Sort::Oldest
)]
pub sort: Sort,
#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
pub range: Option<String>,
#[arg(
long,
env = "GITHUB_TOKEN",
value_name = "TOKEN",
hide_env_values = true
)]
pub github_token: Option<String>,
#[arg(
long,
env = "GITHUB_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO"
)]
pub github_repo: Option<RemoteValue>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RemoteValue(pub Remote);
impl ValueParserFactory for RemoteValue {
type Parser = RemoteValueParser;
fn value_parser() -> Self::Parser {
RemoteValueParser
}
}
#[derive(Clone, Debug)]
pub struct RemoteValueParser;
impl TypedValueParser for RemoteValueParser {
type Value = RemoteValue;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = clap::builder::StringValueParser::new();
let value = inner.parse_ref(cmd, arg, value)?;
let parts = value.split('/').rev().collect::<Vec<&str>>();
if let (Some(owner), Some(repo)) = (parts.get(1), parts.first()) {
Ok(RemoteValue(Remote::new(*owner, *repo)))
} else {
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(ContextKind::InvalidValue, ContextValue::String(value));
Err(err)
}
}
}
impl Opt {
fn parse_dir(dir: &str) -> Result<PathBuf, String> {
Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
use std::ffi::OsStr;
#[test]
fn verify_cli() {
Opt::command().debug_assert()
}
#[test]
fn path_tilde_expansion() {
let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
assert_eq!(home_dir, dir);
}
#[test]
fn remote_value_parser() -> Result<(), clap::Error> {
let remote_value_parser = RemoteValueParser;
assert_eq!(
RemoteValue(Remote::new("test", "repo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("test/repo")
)?
);
assert!(remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new("test"))
.is_err());
assert_eq!(
RemoteValue(Remote::new("test", "testrepo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("https://github.com/test/testrepo")
)?
);
assert!(remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new(""))
.is_err());
Ok(())
}
}