use std::ffi::{OsStr, OsString};
use clap::Subcommand;
use super::config::ApprovalsCommand;
const HOOK_SUBCOMMANDS_WITH_VARS: &[&str] = &[
"pre-switch",
"post-switch",
"pre-start",
"post-start",
"post-create",
"pre-commit",
"post-commit",
"pre-merge",
"post-merge",
"pre-remove",
"post-remove",
];
const KNOWN_HOOK_LONG_FLAGS: &[&str] = &[
"--yes",
"--dry-run",
"--foreground",
"--var",
"--help",
"--config",
"--verbose",
];
pub(crate) fn rewrite_var_shorthand(args: Vec<OsString>) -> Vec<OsString> {
let Some(hook_idx) = args.iter().enumerate().find_map(|(i, arg)| {
if i == 0 || arg.as_os_str() != OsStr::new("hook") {
return None;
}
let prev = args[i - 1].as_os_str();
if prev == OsStr::new("-C") || prev == OsStr::new("--config") {
return None;
}
Some(i)
}) else {
return args;
};
let Some(sub_str) = args.get(hook_idx + 1).and_then(|s| s.to_str()) else {
return args;
};
if !HOOK_SUBCOMMANDS_WITH_VARS.contains(&sub_str) {
return args;
}
let rewrite_start = hook_idx + 2;
let mut out: Vec<OsString> = Vec::with_capacity(args.len());
out.extend(args[..rewrite_start].iter().cloned());
let mut past_double_dash = false;
for token in &args[rewrite_start..] {
if token == "--" {
past_double_dash = true;
out.push(token.clone());
continue;
}
if !past_double_dash
&& let Some(s) = token.to_str()
&& let Some(rest) = s.strip_prefix("--")
&& let Some((key, value)) = rest.split_once('=')
&& !key.is_empty()
{
let flag_name = format!("--{key}");
if !KNOWN_HOOK_LONG_FLAGS.contains(&flag_name.as_str()) {
out.push(OsString::from("--var"));
out.push(OsString::from(format!("{key}={value}")));
continue;
}
}
out.push(token.clone());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn rewrite(args: &[&str]) -> Vec<String> {
rewrite_var_shorthand(args.iter().map(OsString::from).collect())
.into_iter()
.map(|s| s.into_string().unwrap())
.collect()
}
#[test]
fn test_rewrite_var_shorthand() {
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--branch=feature/test"]),
vec!["wt", "hook", "pre-start", "--var", "branch=feature/test"]
);
assert_eq!(
rewrite(&[
"wt",
"hook",
"post-merge",
"--branch=main",
"--target=develop"
]),
vec![
"wt",
"hook",
"post-merge",
"--var",
"branch=main",
"--var",
"target=develop"
]
);
assert_eq!(
rewrite(&[
"wt",
"hook",
"pre-merge",
"--yes",
"--branch=feature",
"--dry-run"
]),
vec![
"wt",
"hook",
"pre-merge",
"--yes",
"--var",
"branch=feature",
"--dry-run"
]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-merge", "test", "--branch=feature"]),
vec!["wt", "hook", "pre-merge", "test", "--var", "branch=feature"]
);
assert_eq!(
rewrite(&["wt", "hook", "post-create", "--branch=x"]),
vec!["wt", "hook", "post-create", "--var", "branch=x"]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--url=http://host?a=1"]),
vec!["wt", "hook", "pre-start", "--var", "url=http://host?a=1"]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--branch="]),
vec!["wt", "hook", "pre-start", "--var", "branch="]
);
}
#[test]
fn test_rewrite_preserves_known_flags() {
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--var=branch=feature"]),
vec!["wt", "hook", "pre-start", "--var=branch=feature"]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--config=/tmp/config.toml"]),
vec!["wt", "hook", "pre-start", "--config=/tmp/config.toml"]
);
assert_eq!(
rewrite(&[
"wt",
"hook",
"post-merge",
"--yes",
"--dry-run",
"--foreground"
]),
vec![
"wt",
"hook",
"post-merge",
"--yes",
"--dry-run",
"--foreground"
]
);
}
#[test]
fn test_rewrite_leaves_non_hook_subcommands_alone() {
assert_eq!(
rewrite(&["wt", "hook", "show", "--expanded"]),
vec!["wt", "hook", "show", "--expanded"]
);
assert_eq!(
rewrite(&["wt", "hook", "approvals", "add", "--all"]),
vec!["wt", "hook", "approvals", "add", "--all"]
);
assert_eq!(
rewrite(&["wt", "switch", "--foo=bar"]),
vec!["wt", "switch", "--foo=bar"]
);
assert_eq!(rewrite(&["wt"]), vec!["wt"]);
}
#[test]
fn test_rewrite_skips_hook_in_flag_value_position() {
assert_eq!(
rewrite(&["wt", "-C", "hook", "pre-start", "--branch=x"]),
vec!["wt", "-C", "hook", "pre-start", "--branch=x"]
);
assert_eq!(
rewrite(&["wt", "--config", "hook", "pre-start", "--branch=x"]),
vec!["wt", "--config", "hook", "pre-start", "--branch=x"]
);
}
#[test]
fn test_rewrite_handles_global_flags_before_hook() {
assert_eq!(
rewrite(&["wt", "-v", "hook", "pre-start", "--branch=x"]),
vec!["wt", "-v", "hook", "pre-start", "--var", "branch=x"]
);
}
#[test]
fn test_rewrite_ignores_bare_hyphen_args() {
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "-y", "--branch=x"]),
vec!["wt", "hook", "pre-start", "-y", "--var", "branch=x"]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--=val"]),
vec!["wt", "hook", "pre-start", "--=val"]
);
assert_eq!(
rewrite(&["wt", "hook", "pre-start", "--", "--branch=x"]),
vec!["wt", "hook", "pre-start", "--", "--branch=x"]
);
}
#[test]
fn test_hook_subcommands_with_vars_matches_clap() {
use crate::cli::Cli;
use clap::CommandFactory;
let app = Cli::command();
let hook_cmd = app
.get_subcommands()
.find(|c| c.get_name() == "hook")
.expect("hook subcommand exists");
let subs_with_var: Vec<&str> = hook_cmd
.get_subcommands()
.filter(|c| c.get_arguments().any(|a| a.get_id() == "vars"))
.map(|c| c.get_name())
.collect();
for name in &subs_with_var {
assert!(
HOOK_SUBCOMMANDS_WITH_VARS.contains(name),
"Hook subcommand '{name}' accepts --var but is missing from \
HOOK_SUBCOMMANDS_WITH_VARS. Add it so --KEY=VALUE shorthand works."
);
}
let deprecated_aliases: &[&str] = &["post-create"];
for name in HOOK_SUBCOMMANDS_WITH_VARS {
if deprecated_aliases.contains(name) {
continue;
}
assert!(
subs_with_var.contains(name),
"HOOK_SUBCOMMANDS_WITH_VARS contains '{name}' but that subcommand \
doesn't accept --var. Remove it from the list."
);
}
}
#[test]
fn test_known_hook_long_flags_matches_clap() {
use crate::cli::Cli;
use clap::CommandFactory;
let app = Cli::command();
let hook_cmd = app
.get_subcommands()
.find(|c| c.get_name() == "hook")
.expect("hook subcommand exists");
let mut clap_flags: std::collections::HashSet<String> = std::collections::HashSet::new();
for sub in hook_cmd.get_subcommands() {
if !sub.get_arguments().any(|a| a.get_id() == "vars") {
continue;
}
for arg in sub.get_arguments() {
if let Some(long) = arg.get_long() {
clap_flags.insert(format!("--{long}"));
}
}
}
for arg in app.get_arguments() {
if let Some(long) = arg.get_long() {
clap_flags.insert(format!("--{long}"));
}
}
for flag in &clap_flags {
assert!(
KNOWN_HOOK_LONG_FLAGS.contains(&flag.as_str()),
"Hook subcommand flag '{flag}' is missing from KNOWN_HOOK_LONG_FLAGS. \
Add it so --KEY=VALUE shorthand doesn't rewrite it."
);
}
}
}
#[derive(Subcommand)]
pub enum HookCommand {
Show {
#[arg(value_parser = ["pre-switch", "post-switch", "pre-start", "post-start", "pre-commit", "post-commit", "pre-merge", "post-merge", "pre-remove", "post-remove"])]
hook_type: Option<String>,
#[arg(long)]
expanded: bool,
},
PreSwitch {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PostSwitch {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
foreground: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
#[command(alias = "post-create")]
PreStart {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PostStart {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
foreground: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PreCommit {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PostCommit {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
foreground: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PreMerge {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PostMerge {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
foreground: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PreRemove {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
PostRemove {
#[arg(add = crate::completion::hook_command_name_completer())]
name: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
foreground: bool,
#[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)]
vars: Vec<(String, String)>,
},
#[command(hide = true, name = "run-pipeline")]
RunPipeline,
#[command(hide = true)]
Approvals {
#[command(subcommand)]
action: ApprovalsCommand,
},
}