From ab50a08ddd8df5d45671062dfb1997237c74a6bf Mon Sep 17 00:00:00 2001 From: Arthur Grillo Date: Sat, 21 Sep 2024 01:42:50 -0300 Subject: [PATCH] cli: Explicitly add a Help command to accept the early args after it The default clap's help command doesn't have the ability to accept flags (e.g --no-pager). The recommended way[1] to solve this is to manually implement it. [1]: https://github.com/clap-rs/clap/discussions/5332 Fixes: #4501 --- cli/src/commands/help.rs | 62 +++++++++++++ cli/src/commands/mod.rs | 66 ++++++++++++- cli/tests/cli-reference@.md.snap | 154 +++++++++++++++++++++++++++++++ cli/tests/runner.rs | 1 + cli/tests/test_global_opts.rs | 4 + cli/tests/test_help_command.rs | 50 ++++++++++ 6 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 cli/src/commands/help.rs create mode 100644 cli/tests/test_help_command.rs diff --git a/cli/src/commands/help.rs b/cli/src/commands/help.rs new file mode 100644 index 0000000000..8b6bee544b --- /dev/null +++ b/cli/src/commands/help.rs @@ -0,0 +1,62 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use tracing::instrument; + +use crate::cli_util::CommandHelper; +use crate::command_error; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Print this message or the help of the given subcommand(s) +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct HelpArgs { + /// Print help for the subcommand(s) + pub(crate) command: Vec, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_help( + _ui: &mut Ui, + command: &CommandHelper, + args: &HelpArgs, +) -> Result<(), CommandError> { + let mut cmd_names = vec![]; + let mut curr_command = command.app(); + + for cmd in &args.command { + curr_command = + curr_command + .find_subcommand(cmd.clone()) + .ok_or(command.app().clone().error( + clap::error::ErrorKind::InvalidValue, + format!("No subcomand with name {}", cmd), + ))?; + + cmd_names.push(curr_command.get_name()); + } + + let mut args_to_show_help = vec![command.app().get_name()]; + args_to_show_help.extend(cmd_names); + args_to_show_help.push("--help"); + + let help_err = command + .app() + .clone() + .subcommand_required(true) + .try_get_matches_from(args_to_show_help) + .expect_err("Clap library should return a DisplayHelp error in this context"); + + Err(command_error::cli_error(help_err)) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 52c03c1743..1b29b8d34a 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -30,6 +30,7 @@ mod evolog; mod file; mod fix; mod git; +mod help; mod init; mod interdiff; mod log; @@ -109,6 +110,7 @@ enum Command { Fix(fix::FixArgs), #[command(subcommand)] Git(git::GitCommand), + Help(help::HelpArgs), Init(init::InitArgs), Interdiff(interdiff::InterdiffArgs), Log(log::LogArgs), @@ -172,13 +174,72 @@ struct DummyCommandArgs { _args: Vec, } +fn add_help_cmd_to_subcommands(command: clap::Command, help_cmd: &clap::Command) -> clap::Command { + let mut ret = command.clone(); + + for subcmd in command.get_subcommands() { + ret = ret.mut_subcommand(subcmd.get_name(), |mut subcmd| { + if subcmd.has_subcommands() { + subcmd = add_help_cmd_to_subcommands(subcmd.clone(), help_cmd); + subcmd.disable_help_subcommand(true).subcommand(help_cmd) + } else { + subcmd + } + }); + } + + ret +} + pub fn default_app() -> clap::Command { - Command::augment_subcommands(Args::command()) + let app = Command::augment_subcommands(Args::command()).disable_help_subcommand(true); + add_help_cmd_to_subcommands( + app.clone(), + app.find_subcommand("help") + .expect("JJ has the help command"), + ) +} + +fn get_help_arg_cmd(matches: &clap::ArgMatches) -> Option> { + let mut curr_matches = matches; + + let mut ret = vec![]; + let mut has_help = false; + + while let Some((subcmd_name, matches)) = curr_matches.subcommand() { + if subcmd_name == "help" { + has_help = true; + + let cmd = Command::from_arg_matches(curr_matches).unwrap(); + if let Command::Help(args) = cmd { + ret.extend(args.command); + } else { + unreachable!() + } + + break; + } else { + ret.push(subcmd_name.to_string()); + } + + curr_matches = matches; + } + + if has_help { + Some(ret) + } else { + None + } } #[instrument(skip_all)] pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), CommandError> { - let subcommand = Command::from_arg_matches(command_helper.matches()).unwrap(); + let subcommand = if let Some(command) = get_help_arg_cmd(command_helper.matches()) { + Command::Help(help::HelpArgs { command }) + } else { + Command::from_arg_matches(command_helper.matches()).unwrap() + }; + match &subcommand { Command::Abandon(args) => abandon::cmd_abandon(ui, command_helper, args), Command::Backout(args) => backout::cmd_backout(ui, command_helper, args), @@ -213,6 +274,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co } Command::Fix(args) => fix::cmd_fix(ui, command_helper, args), Command::Git(args) => git::cmd_git(ui, command_helper, args), + Command::Help(args) => help::cmd_help(ui, command_helper, args), Command::Init(args) => init::cmd_init(ui, command_helper, args), Command::Interdiff(args) => interdiff::cmd_interdiff(ui, command_helper, args), Command::Log(args) => log::cmd_log(ui, command_helper, args), diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index d2a4d709aa..5d7feb31cc 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -23,6 +23,7 @@ This document contains the help content for the `jj` command-line program. * [`jj bookmark set`↴](#jj-bookmark-set) * [`jj bookmark track`↴](#jj-bookmark-track) * [`jj bookmark untrack`↴](#jj-bookmark-untrack) +* [`jj bookmark help`↴](#jj-bookmark-help) * [`jj commit`↴](#jj-commit) * [`jj config`↴](#jj-config) * [`jj config edit`↴](#jj-config-edit) @@ -30,6 +31,7 @@ This document contains the help content for the `jj` command-line program. * [`jj config list`↴](#jj-config-list) * [`jj config path`↴](#jj-config-path) * [`jj config set`↴](#jj-config-set) +* [`jj config help`↴](#jj-config-help) * [`jj describe`↴](#jj-describe) * [`jj diff`↴](#jj-diff) * [`jj diffedit`↴](#jj-diffedit) @@ -42,6 +44,7 @@ This document contains the help content for the `jj` command-line program. * [`jj file show`↴](#jj-file-show) * [`jj file track`↴](#jj-file-track) * [`jj file untrack`↴](#jj-file-untrack) +* [`jj file help`↴](#jj-file-help) * [`jj fix`↴](#jj-fix) * [`jj git`↴](#jj-git) * [`jj git clone`↴](#jj-git-clone) @@ -56,6 +59,9 @@ This document contains the help content for the `jj` command-line program. * [`jj git remote remove`↴](#jj-git-remote-remove) * [`jj git remote rename`↴](#jj-git-remote-rename) * [`jj git remote set-url`↴](#jj-git-remote-set-url) +* [`jj git remote help`↴](#jj-git-remote-help) +* [`jj git help`↴](#jj-git-help) +* [`jj help`↴](#jj-help) * [`jj init`↴](#jj-init) * [`jj interdiff`↴](#jj-interdiff) * [`jj log`↴](#jj-log) @@ -68,6 +74,7 @@ This document contains the help content for the `jj` command-line program. * [`jj operation restore`↴](#jj-operation-restore) * [`jj operation show`↴](#jj-operation-show) * [`jj operation undo`↴](#jj-operation-undo) +* [`jj operation help`↴](#jj-operation-help) * [`jj parallelize`↴](#jj-parallelize) * [`jj prev`↴](#jj-prev) * [`jj rebase`↴](#jj-rebase) @@ -80,17 +87,20 @@ This document contains the help content for the `jj` command-line program. * [`jj sparse list`↴](#jj-sparse-list) * [`jj sparse reset`↴](#jj-sparse-reset) * [`jj sparse set`↴](#jj-sparse-set) +* [`jj sparse help`↴](#jj-sparse-help) * [`jj split`↴](#jj-split) * [`jj squash`↴](#jj-squash) * [`jj status`↴](#jj-status) * [`jj tag`↴](#jj-tag) * [`jj tag list`↴](#jj-tag-list) +* [`jj tag help`↴](#jj-tag-help) * [`jj util`↴](#jj-util) * [`jj util completion`↴](#jj-util-completion) * [`jj util gc`↴](#jj-util-gc) * [`jj util mangen`↴](#jj-util-mangen) * [`jj util markdown-help`↴](#jj-util-markdown-help) * [`jj util config-schema`↴](#jj-util-config-schema) +* [`jj util help`↴](#jj-util-help) * [`jj undo`↴](#jj-undo) * [`jj unsquash`↴](#jj-unsquash) * [`jj version`↴](#jj-version) @@ -101,6 +111,7 @@ This document contains the help content for the `jj` command-line program. * [`jj workspace rename`↴](#jj-workspace-rename) * [`jj workspace root`↴](#jj-workspace-root) * [`jj workspace update-stale`↴](#jj-workspace-update-stale) +* [`jj workspace help`↴](#jj-workspace-help) ## `jj` @@ -126,6 +137,7 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor * `file` — File operations * `fix` — Update files with formatting fixes or other changes * `git` — Commands for working with Git remotes and the underlying Git repo +* `help` — Print this message or the help of the given subcommand(s) * `init` — Create a new repo in the given directory * `interdiff` — Compare the changes of two commits * `log` — Show revision history @@ -246,6 +258,7 @@ For information about bookmarks, see https://martinvonz.github.io/jj/latest/docs * `set` — Create or update a bookmark to point to a certain commit * `track` — Start tracking given remote bookmarks * `untrack` — Stop tracking given remote bookmarks +* `help` — Print this message or the help of the given subcommand(s) @@ -425,6 +438,18 @@ A non-tracking remote bookmark is just a pointer to the last-fetched remote book +## `jj bookmark help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj bookmark help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj commit` Update the description and create a new change on top @@ -467,6 +492,7 @@ For file locations, supported config options, and other details about jj config, * `list` — List variables set in config file, along with their values * `path` — Print the path to the config file * `set` — Update config file to set the given option to a given value +* `help` — Print this message or the help of the given subcommand(s) @@ -568,6 +594,18 @@ Update config file to set the given option to a given value +## `jj config help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj config help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj describe` Update the change description or other metadata @@ -749,6 +787,7 @@ File operations * `show` — Print contents of files in a revision * `track` — Start tracking specified paths in the working copy * `untrack` — Stop tracking specified paths in the working copy +* `help` — Print this message or the help of the given subcommand(s) @@ -848,6 +887,18 @@ Stop tracking specified paths in the working copy +## `jj file help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj file help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj fix` Update files with formatting fixes or other changes @@ -946,6 +997,7 @@ For a comparison with Git, including a table of commands, see https://martinvonz * `init` — Create a new Git backed repo * `push` — Push to a Git remote * `remote` — Manage Git remotes +* `help` — Print this message or the help of the given subcommand(s) @@ -1088,6 +1140,7 @@ The Git repo will be a bare git repo stored inside the `.jj/` directory. * `remove` — Remove a Git remote and forget its bookmarks * `rename` — Rename a Git remote * `set-url` — Set the URL of a Git remote +* `help` — Print this message or the help of the given subcommand(s) @@ -1150,6 +1203,42 @@ Set the URL of a Git remote +## `jj git remote help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj git remote help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + +## `jj git help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj git help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + +## `jj help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj init` Create a new repo in the given directory @@ -1334,6 +1423,7 @@ For information about the operation log, see https://martinvonz.github.io/jj/lat * `restore` — Create a new operation that restores the repo to an earlier state * `show` — Show changes to the repository in an operation * `undo` — Create a new operation that undoes an earlier operation +* `help` — Print this message or the help of the given subcommand(s) @@ -1516,6 +1606,18 @@ This undoes an individual operation by applying the inverse of the operation. +## `jj operation help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj operation help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj parallelize` Parallelize revisions by making them siblings @@ -1829,6 +1931,7 @@ Manage which paths from the working-copy commit are present in the working copy * `list` — List the patterns that are currently present in the working copy * `reset` — Reset the patterns to include all files in the working copy * `set` — Update the patterns that are present in the working copy +* `help` — Print this message or the help of the given subcommand(s) @@ -1874,6 +1977,18 @@ For example, if all you need is the `README.md` and the `lib/` directory, use `j +## `jj sparse help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj sparse help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj split` Split a revision in two @@ -1961,6 +2076,7 @@ Manage tags ###### **Subcommands:** * `list` — List tags +* `help` — Print this message or the help of the given subcommand(s) @@ -1986,6 +2102,18 @@ List tags +## `jj tag help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj tag help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj util` Infrequently used commands such as for generating shell completions @@ -1999,6 +2127,7 @@ Infrequently used commands such as for generating shell completions * `mangen` — Print a ROFF (manpage) * `markdown-help` — Print the CLI help for all subcommands in Markdown * `config-schema` — Print the JSON schema for the jj TOML config format +* `help` — Print this message or the help of the given subcommand(s) @@ -2073,6 +2202,18 @@ Print the JSON schema for the jj TOML config format +## `jj util help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj util help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + + ## `jj undo` Undo an operation (shortcut for `jj op undo`) @@ -2154,6 +2295,7 @@ Each workspace also has own sparse patterns. * `rename` — Renames the current workspace * `root` — Show the current workspace root directory * `update-stale` — Update a workspace that has become stale +* `help` — Print this message or the help of the given subcommand(s) @@ -2246,6 +2388,18 @@ For information about stale working copies, see https://martinvonz.github.io/jj/ +## `jj workspace help` + +Print this message or the help of the given subcommand(s) + +**Usage:** `jj workspace help [COMMAND]...` + +###### **Arguments:** + +* `` — Print help for the subcommand(s) + + +
diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs index 1420e30cd0..0b2c7b4f87 100644 --- a/cli/tests/runner.rs +++ b/cli/tests/runner.rs @@ -44,6 +44,7 @@ mod test_git_remotes; mod test_git_submodule; mod test_gitignores; mod test_global_opts; +mod test_help_command; mod test_immutable_commits; mod test_init_command; mod test_interdiff_command; diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index b275f7109f..8fe2097295 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -530,6 +530,10 @@ fn test_early_args() { let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--color=always", "help"]); insta::assert_snapshot!(stdout.lines().find(|l| l.contains("Commands:")).unwrap(), @"Commands:"); + // Check that early args are accepted after the help command + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["help", "--color=always"]); + insta::assert_snapshot!(stdout.lines().find(|l| l.contains("Commands:")).unwrap(), @"Commands:"); + // Early args are parsed with clap's ignore_errors(), but there is a known // bug that causes defaults to be unpopulated. Test that the early args are // tolerant of this bug and don't cause a crash. diff --git a/cli/tests/test_help_command.rs b/cli/tests/test_help_command.rs new file mode 100644 index 0000000000..e4c4fb1e59 --- /dev/null +++ b/cli/tests/test_help_command.rs @@ -0,0 +1,50 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::TestEnvironment; + +#[test] +fn test_help() { + let test_env = TestEnvironment::default(); + + let help_cmd_stdout = test_env.jj_cmd_success(test_env.env_root(), &["help"]); + insta::assert_snapshot!(help_cmd_stdout); + // The help command output should be equal to the long --help flag + let help_flag_stdout = test_env.jj_cmd_success(test_env.env_root(), &["--help"]); + assert_eq!(help_cmd_stdout, help_flag_stdout); + + // Help command should work with commands + let help_cmd_stdout = test_env.jj_cmd_success(test_env.env_root(), &["help", "log"]); + insta::assert_snapshot!(help_cmd_stdout); + let help_flag_stdout = test_env.jj_cmd_success(test_env.env_root(), &["log", "--help"]); + assert_eq!(help_cmd_stdout, help_flag_stdout); + + // Help command should work with subcommands + let help_cmd_stdout = + test_env.jj_cmd_success(test_env.env_root(), &["help", "workspace", "add"]); + insta::assert_snapshot!(help_cmd_stdout); + let help_flag_stdout = + test_env.jj_cmd_success(test_env.env_root(), &["workspace", "add", "--help"]); + assert_eq!(help_cmd_stdout, help_flag_stdout); + + // Help command should work recursively + let help_cmd_stdout = + test_env.jj_cmd_success(test_env.env_root(), &["workspace", "help", "add"]); + insta::assert_snapshot!(help_cmd_stdout); + let help_flag_stdout = + test_env.jj_cmd_success(test_env.env_root(), &["workspace", "add", "--help"]); + assert_eq!(help_cmd_stdout, help_flag_stdout); + + let _ = test_env.jj_cmd_failure(test_env.env_root(), &["workspace", "add", "help"]); +}