Skip to content
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
132 changes: 115 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ fn get_base_command(color_choice: Option<termcolor::ColorChoice>) -> clap::Comma
],
))
.help_expected(true)
.infer_subcommands(true)
.max_term_width(88)
.disable_version_flag(true)
.arg(
Expand Down Expand Up @@ -140,6 +141,44 @@ pub(crate) fn get_full_command(
.subcommands(aliases.values().map(alias::Alias::make))
}

enum Subcommand<'a> {
Command(&'static cmd::StGitCommand),
Alias(&'a alias::Alias),
Help,
}

/// Find a command, alias, or `"help"` matching `sub_name` exactly or as an
/// unambiguous prefix.
fn find_subcommand<'a>(sub_name: &str, aliases: &'a alias::Aliases) -> Option<Subcommand<'a>> {
if let Some(command) = STGIT_COMMANDS
.iter()
.find(|command| command.name == sub_name)
{
return Some(Subcommand::Command(command));
} else if sub_name == "help" {
return Some(Subcommand::Help);
} else if let Some(alias) = aliases.get(sub_name) {
return Some(Subcommand::Alias(alias));
}

let mut candidates = STGIT_COMMANDS
.iter()
.filter(|command| command.name.starts_with(sub_name))
.map(Subcommand::Command)
.chain(
aliases
.values()
.filter(|alias| alias.name.starts_with(sub_name))
.map(Subcommand::Alias),
)
.chain("help".starts_with(sub_name).then_some(Subcommand::Help));

match (candidates.next(), candidates.next()) {
(Some(subcommand), None) => Some(subcommand),
_ => None,
}
}

/// Main entry point for `stg` executable.
///
/// The name of the game is to dispatch to the appropriate subcommand or alias as
Expand Down Expand Up @@ -187,16 +226,19 @@ fn main() -> ! {
// If the subcommand name does not match a builtin subcommand, the
// aliases are located, which involves finding the Git repo and parsing
// the various levels of config files. If the subcommand name matches an
// alias, it is executed and the cost of constructing all subcommands'
// Command instances is still avoided.
// alias or an unambiguous prefix of a subcommand or alias, it is
// executed and the cost of constructing all subcommands' Command
// instances is still avoided.
match get_aliases() {
Err(e) => exit_with_result(Err(e), color_choice),
Ok((aliases, maybe_repo)) => {
if let Some(alias) = aliases.get(sub_name) {
Ok((aliases, maybe_repo)) => match find_subcommand(sub_name, &aliases) {
Some(Subcommand::Command(command)) => {
execute_command(command, argv, color_choice)
}
Some(Subcommand::Alias(alias)) => {
let user_args: Vec<OsString> = sub_matches
.get_many::<OsString>("")
.map_or_else(Vec::new, |vals| vals.cloned().collect());

match alias.kind {
alias::AliasKind::Shell => execute_shell_alias(
alias,
Expand All @@ -212,7 +254,8 @@ fn main() -> ! {
&aliases,
),
}
} else {
}
Some(Subcommand::Help) | None => {
// If no command or alias matches can be determined from the
// above process, then a complete clap::Command instance is
// constructed with all subcommand Command instances for
Expand All @@ -221,7 +264,7 @@ fn main() -> ! {
// expected to terminate with an appropriate help message.
full_app_help(argv, Some(aliases), color_choice)
}
}
},
}
}
} else {
Expand Down Expand Up @@ -502,18 +545,13 @@ fn execute_stgit_alias(
.expect("empty aliases are filtered in get_aliases()")
.as_str();

if let Some(command) = STGIT_COMMANDS
.iter()
.find(|command| command.name == resolved_cmd_name)
{
execute_command(command, argv, color_choice)
} else if aliases.contains_key(resolved_cmd_name) {
Err(anyhow!("recursive alias `{}`", alias.name))
} else {
Err(anyhow!(
match find_subcommand(resolved_cmd_name, aliases) {
Some(Subcommand::Command(command)) => execute_command(command, argv, color_choice),
Some(Subcommand::Alias(_)) => Err(anyhow!("recursive alias `{}`", alias.name)),
Some(Subcommand::Help) | None => Err(anyhow!(
"bad alias for `{}`: `{resolved_cmd_name}` is not a stg command",
alias.name,
))
)),
}
}
Err(reason) => Err(anyhow!("bad alias for `{}`: {reason}", alias.name)),
Expand Down Expand Up @@ -643,3 +681,63 @@ fn print_merge_conflicts() {
};
stupid.status_short(Some(pathspecs)).unwrap_or_default();
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn subcommand_shorthand_resolution() {
let mut aliases = alias::get_default_aliases();
assert!(matches!(
find_subcommand("del", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "delete"
));
assert!(matches!(
find_subcommand("de", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "delete"
));
assert!(matches!(
find_subcommand("di", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "diff"
));
assert!(find_subcommand("d", &aliases).is_none());
assert!(find_subcommand("pu", &aliases).is_none());
assert!(find_subcommand("res", &aliases).is_none());
assert!(matches!(
find_subcommand("rese", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "reset"
));
assert!(matches!(
find_subcommand("reso", &aliases),
Some(Subcommand::Alias(alias)) if alias.name == "resolved"
));
assert!(matches!(
find_subcommand("st", &aliases),
Some(Subcommand::Alias(alias)) if alias.name == "status"
));
assert!(find_subcommand("h", &aliases).is_none());
assert!(matches!(
find_subcommand("he", &aliases),
Some(Subcommand::Help)
));
assert!(matches!(
find_subcommand("hi", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "hide"
));

aliases.insert(
"show-stat".to_string(),
alias::Alias::new("show-stat", "!git show --stat"),
);
assert!(find_subcommand("sho", &aliases).is_none());
assert!(matches!(
find_subcommand("show", &aliases),
Some(Subcommand::Command(cmd)) if cmd.name == "show"
));
assert!(matches!(
find_subcommand("show-", &aliases),
Some(Subcommand::Alias(alias)) if alias.name == "show-stat"
));
}
}
29 changes: 28 additions & 1 deletion t/t0004-main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,39 @@ test_expect_success 'Test help on regular command' '
stg help init | grep -e "stg init"
'

test_expect_success 'Test help on command shorthand' '
stg in -h >h-in.txt &&
stg init -h >h-init.txt &&
test_cmp h-init.txt h-in.txt &&
stg he delete >h-del.txt &&
stg delete --help >h-delete.txt &&
test_cmp h-delete.txt h-del.txt
'

test_expect_success 'Test unambiguous command shorthand' '
stg ver >v0.txt &&
stg version >v1.txt &&
test_cmp v1.txt v0.txt &&
stg in &&
stg new -m p0 &&
stg ser --noprefix >s.txt &&
echo p0 >expected.txt &&
test_cmp expected.txt s.txt &&
stg del p0 &&
stg ser --noprefix >s.txt &&
test_must_be_empty s.txt
'

test_expect_success 'Test help on ambiguous command' '
general_error stg pu 2>err &&
grep -e "unrecognized subcommand .pu." err &&
grep -e "some similar subcommands exist: .pull., .push." err &&
general_error stg help pu 2>err &&
grep -e "unrecognized subcommand .pu." err
grep -e "unrecognized subcommand .pu." err &&
general_error stg d 2>err &&
grep -e "unrecognized subcommand .d." err &&
general_error stg h 2>err &&
grep -e "unrecognized subcommand .h." err
'

test_expect_success 'Test version/--version equivalence' '
Expand Down
5 changes: 4 additions & 1 deletion t/t0010-alias.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ test_expect_success 'Test help on builtin alias command' '
test_expect_success 'Test ambiguous alias' '
test_config stgit.alias.show-stat "!git show --stat" &&
stg show-stat &&
stg show-s &&
stg init &&
stg show &&
general_error stg sho 2>err &&
grep -e "tip: some similar subcommands exist: .show-stat., .show." err
grep -e "tip: some similar subcommands exist: .show-stat., .show." err &&
general_error stg res 2>err &&
grep -e "unrecognized subcommand .res." err
'

test_expect_success 'Setup top-level and nested aliases' '
Expand Down
4 changes: 3 additions & 1 deletion t/t1901-email.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ test_expect_success 'Check that email command requires subcommand' '

test_expect_success 'Check email subcommand help' '
stg email help format &&
stg email help send
stg email help send &&
stg em f -h &&
stg em s --help
'

test_done
Loading