use clap::Parser;
use std::path::PathBuf;
use crate::{OutputFormat, RootArgs, parse_kind_filter_arg};
#[derive(Debug, Clone, Parser)]
pub(crate) struct DiffArgs {
#[command(flatten)]
pub(super) root: RootArgs,
#[arg(long)]
pub(super) config: Option<PathBuf>,
#[arg(long, value_parser = parse_kind_filter_arg)]
pub(super) kind: Option<String>,
#[arg(long)]
pub(super) include_untracked: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Human)]
pub(super) format: OutputFormat,
#[arg(long)]
pub(super) output: Option<PathBuf>,
#[arg(
long,
value_parser = parse_revision_arg,
allow_hyphen_values = true
)]
pub(super) base: String,
#[arg(
long,
value_parser = parse_revision_arg,
allow_hyphen_values = true
)]
pub(super) head: Option<String>,
#[arg(long)]
pub(super) require_change_note: bool,
#[arg(long, default_value = ".allow/revisions")]
pub(super) revisions_dir: PathBuf,
#[arg(long)]
pub(super) write_change_note_template: Option<PathBuf>,
}
fn parse_revision_arg(value: &str) -> Result<String, String> {
if value.is_empty() {
return Err("revision must not be empty".to_string());
}
if value.trim() != value {
return Err("revision must not have leading or trailing whitespace".to_string());
}
if value.starts_with('-') {
return Err("revision must not start with `-`".to_string());
}
if value.chars().any(char::is_control) {
return Err("revision must not contain control characters".to_string());
}
Ok(value.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diff_args_accept_common_revision_forms() {
for revision in [
"HEAD",
"HEAD~1",
"origin/main",
"refs/tags/v0.1.10",
"0123456789abcdef0123456789abcdef01234567",
] {
let args =
DiffArgs::try_parse_from(["diff", "--base", revision]).unwrap_or_else(|err| {
std::panic::panic_any(format!("revision `{revision}` should parse: {err}"))
});
assert_eq!(args.base, revision);
}
}
#[test]
fn diff_args_reject_option_like_and_malformed_revisions() {
for revision in ["--output=owned", "-O", "", " HEAD", "HEAD ", "HEAD\nmain"] {
let result = DiffArgs::try_parse_from(["diff", &format!("--base={revision}")]);
assert!(result.is_err(), "revision `{revision}` should be rejected");
}
}
#[test]
fn diff_args_accept_change_note_template_path() {
let args = DiffArgs::try_parse_from([
"diff",
"--base",
"HEAD~1",
"--write-change-note-template",
".allow/revisions/next.toml",
])
.unwrap_or_else(|err| std::panic::panic_any(format!("template path should parse: {err}")));
assert_eq!(
args.write_change_note_template,
Some(PathBuf::from(".allow/revisions/next.toml"))
);
}
}