changeset 49640:37bc3edef76f

rhg: upgrade `clap` dependency This one is the worst one to upgrade since v2 -> v4 broke a ton of API, which thankfully seems saner now. Contrary to what was done in the `hg-core/src/examples/nodemap` rewrite, we're not switching from the "builder" pattern to the "derive" pattern, since that would imply a much larger diff. It can be done incrementally.
author Raphaël Gomès <rgomes@octobus.net>
date Tue, 15 Nov 2022 00:02:43 +0100
parents 5844cd8e81ca
children ab6151e1f468
files rust/Cargo.lock rust/rhg/Cargo.toml rust/rhg/src/commands/cat.rs rust/rhg/src/commands/config.rs rust/rhg/src/commands/debugdata.rs rust/rhg/src/commands/debugignorerhg.rs rust/rhg/src/commands/debugrequirements.rs rust/rhg/src/commands/debugrhgsparse.rs rust/rhg/src/commands/files.rs rust/rhg/src/commands/root.rs rust/rhg/src/commands/status.rs rust/rhg/src/main.rs tests/test-rhg.t
diffstat 13 files changed, 178 insertions(+), 213 deletions(-) [+]
line wrap: on
line diff
--- a/rust/Cargo.lock	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/Cargo.lock	Tue Nov 15 00:02:43 2022 +0100
@@ -50,15 +50,6 @@
 ]
 
 [[package]]
-name = "ansi_term"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "atty"
 version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -172,21 +163,6 @@
 
 [[package]]
 name = "clap"
-version = "2.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
-dependencies = [
- "ansi_term",
- "atty",
- "bitflags",
- "strsim 0.8.0",
- "textwrap",
- "unicode-width",
- "vec_map",
-]
-
-[[package]]
-name = "clap"
 version = "4.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "60494cedb60cb47462c0ff7be53de32c0e42a6fc2c772184554fa12bd9489c03"
@@ -196,7 +172,7 @@
  "clap_derive",
  "clap_lex",
  "once_cell",
- "strsim 0.10.0",
+ "strsim",
  "termcolor",
 ]
 
@@ -548,7 +524,7 @@
  "bitflags",
  "byteorder",
  "bytes-cast",
- "clap 4.0.24",
+ "clap",
  "crossbeam-channel",
  "derive_more",
  "flate2",
@@ -1110,7 +1086,7 @@
 dependencies = [
  "atty",
  "chrono",
- "clap 2.34.0",
+ "clap",
  "derive_more",
  "env_logger",
  "format-bytes",
@@ -1209,12 +1185,6 @@
 
 [[package]]
 name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
-[[package]]
-name = "strsim"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
@@ -1254,15 +1224,6 @@
 ]
 
 [[package]]
-name = "textwrap"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
 name = "thread_local"
 version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1339,12 +1300,6 @@
 ]
 
 [[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
-[[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/rust/rhg/Cargo.toml	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/Cargo.toml	Tue Nov 15 00:02:43 2022 +0100
@@ -11,7 +11,7 @@
 atty = "0.2.14"
 hg-core = { path = "../hg-core"}
 chrono = "0.4.19"
-clap = "2.34.0"
+clap = { version = "4.0.24", features = ["cargo"] }
 derive_more = "0.99.17"
 home = "0.5.3"
 lazy_static = "1.4.0"
--- a/rust/rhg/src/commands/cat.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/cat.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -4,27 +4,28 @@
 use hg::operations::cat;
 use hg::utils::hg_path::HgPathBuf;
 use micro_timer::timed;
+use std::ffi::OsString;
+use std::os::unix::prelude::OsStrExt;
 
 pub const HELP_TEXT: &str = "
 Output the current or given revision of files
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("cat")
+pub fn args() -> clap::Command {
+    clap::command!("cat")
         .arg(
-            Arg::with_name("rev")
+            Arg::new("rev")
                 .help("search the repository as it is in REV")
-                .short("-r")
-                .long("--rev")
-                .value_name("REV")
-                .takes_value(true),
+                .short('r')
+                .long("rev")
+                .value_name("REV"),
         )
         .arg(
-            clap::Arg::with_name("files")
+            clap::Arg::new("files")
                 .required(true)
-                .multiple(true)
-                .empty_values(false)
+                .num_args(1..)
                 .value_name("FILE")
+                .value_parser(clap::value_parser!(std::ffi::OsString))
                 .help("Files to output"),
         )
         .about(HELP_TEXT)
@@ -41,11 +42,15 @@
         ));
     }
 
-    let rev = invocation.subcommand_args.value_of("rev");
-    let file_args = match invocation.subcommand_args.values_of("files") {
-        Some(files) => files.collect(),
-        None => vec![],
-    };
+    let rev = invocation.subcommand_args.get_one::<String>("rev");
+    let file_args =
+        match invocation.subcommand_args.get_many::<OsString>("files") {
+            Some(files) => files
+                .filter(|s| !s.is_empty())
+                .map(|s| s.as_os_str())
+                .collect(),
+            None => vec![],
+        };
 
     let repo = invocation.repo?;
     let cwd = hg::utils::current_dir()?;
@@ -53,8 +58,8 @@
     let working_directory = cwd.join(working_directory); // Make it absolute
 
     let mut files = vec![];
-    for file in file_args.iter() {
-        if file.starts_with("set:") {
+    for file in file_args {
+        if file.as_bytes().starts_with(b"set:") {
             let message = "fileset";
             return Err(CommandError::unsupported(message));
         }
@@ -62,7 +67,7 @@
         let normalized = cwd.join(&file);
         // TODO: actually normalize `..` path segments etc?
         let dotted = normalized.components().any(|c| c.as_os_str() == "..");
-        if file == &"." || dotted {
+        if file.as_bytes() == b"." || dotted {
             let message = "`..` or `.` path segment";
             return Err(CommandError::unsupported(message));
         }
@@ -74,7 +79,7 @@
             .map_err(|_| {
                 CommandError::abort(format!(
                     "abort: {} not under root '{}'\n(consider using '--cwd {}')",
-                    file,
+                    String::from_utf8_lossy(file.as_bytes()),
                     working_directory.display(),
                     relative_path.display(),
                 ))
--- a/rust/rhg/src/commands/config.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/config.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -8,14 +8,13 @@
 With one argument of the form section.name, print just the value of that config item.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("config")
+pub fn args() -> clap::Command {
+    clap::command!("config")
         .arg(
-            Arg::with_name("name")
+            Arg::new("name")
                 .help("the section.name to print")
                 .value_name("NAME")
-                .required(true)
-                .takes_value(true),
+                .required(true),
         )
         .about(HELP_TEXT)
 }
@@ -23,7 +22,7 @@
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
     let (section, name) = invocation
         .subcommand_args
-        .value_of("name")
+        .get_one::<String>("name")
         .expect("missing required CLI argument")
         .as_bytes()
         .split_2(b'.')
--- a/rust/rhg/src/commands/debugdata.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/debugdata.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -8,27 +8,27 @@
 Dump the contents of a data file revision
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("debugdata")
+pub fn args() -> clap::Command {
+    clap::command!("debugdata")
         .arg(
-            Arg::with_name("changelog")
+            Arg::new("changelog")
                 .help("open changelog")
-                .short("-c")
-                .long("--changelog"),
+                .short('c')
+                .action(clap::ArgAction::SetTrue),
         )
         .arg(
-            Arg::with_name("manifest")
+            Arg::new("manifest")
                 .help("open manifest")
-                .short("-m")
-                .long("--manifest"),
+                .short('m')
+                .action(clap::ArgAction::SetTrue),
         )
         .group(
-            ArgGroup::with_name("")
+            ArgGroup::new("revlog")
                 .args(&["changelog", "manifest"])
                 .required(true),
         )
         .arg(
-            Arg::with_name("rev")
+            Arg::new("rev")
                 .help("revision")
                 .required(true)
                 .value_name("REV"),
@@ -40,19 +40,21 @@
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
     let args = invocation.subcommand_args;
     let rev = args
-        .value_of("rev")
+        .get_one::<String>("rev")
         .expect("rev should be a required argument");
-    let kind =
-        match (args.is_present("changelog"), args.is_present("manifest")) {
-            (true, false) => DebugDataKind::Changelog,
-            (false, true) => DebugDataKind::Manifest,
-            (true, true) => {
-                unreachable!("Should not happen since options are exclusive")
-            }
-            (false, false) => {
-                unreachable!("Should not happen since options are required")
-            }
-        };
+    let kind = match (
+        args.get_one::<bool>("changelog").unwrap(),
+        args.get_one::<bool>("manifest").unwrap(),
+    ) {
+        (true, false) => DebugDataKind::Changelog,
+        (false, true) => DebugDataKind::Manifest,
+        (true, true) => {
+            unreachable!("Should not happen since options are exclusive")
+        }
+        (false, false) => {
+            unreachable!("Should not happen since options are required")
+        }
+    };
 
     let repo = invocation.repo?;
     if repo.has_narrow() {
@@ -60,7 +62,7 @@
             "support for ellipsis nodes is missing and repo has narrow enabled",
         ));
     }
-    let data = debug_data(repo, rev, kind).map_err(|e| (e, rev))?;
+    let data = debug_data(repo, rev, kind).map_err(|e| (e, rev.as_ref()))?;
 
     let mut stdout = invocation.ui.stdout_buffer();
     stdout.write_all(&data)?;
--- a/rust/rhg/src/commands/debugignorerhg.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/debugignorerhg.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -1,5 +1,4 @@
 use crate::error::CommandError;
-use clap::SubCommand;
 use hg;
 use hg::matchers::get_ignore_matcher;
 use hg::StatusError;
@@ -13,8 +12,8 @@
 Some options might be missing, check the list below.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    SubCommand::with_name("debugignorerhg").about(HELP_TEXT)
+pub fn args() -> clap::Command {
+    clap::command!("debugignorerhg").about(HELP_TEXT)
 }
 
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
--- a/rust/rhg/src/commands/debugrequirements.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/debugrequirements.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -4,8 +4,8 @@
 Print the current repo requirements.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("debugrequirements").about(HELP_TEXT)
+pub fn args() -> clap::Command {
+    clap::command!("debugrequirements").about(HELP_TEXT)
 }
 
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
--- a/rust/rhg/src/commands/debugrhgsparse.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/debugrhgsparse.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -1,19 +1,21 @@
-use std::os::unix::prelude::OsStrExt;
+use std::{
+    ffi::{OsStr, OsString},
+    os::unix::prelude::OsStrExt,
+};
 
 use crate::error::CommandError;
-use clap::SubCommand;
 use hg::{self, utils::hg_path::HgPath};
 
 pub const HELP_TEXT: &str = "";
 
-pub fn args() -> clap::App<'static, 'static> {
-    SubCommand::with_name("debugrhgsparse")
+pub fn args() -> clap::Command {
+    clap::command!("debugrhgsparse")
         .arg(
-            clap::Arg::with_name("files")
+            clap::Arg::new("files")
+                .value_name("FILES")
                 .required(true)
-                .multiple(true)
-                .empty_values(false)
-                .value_name("FILES")
+                .num_args(1..)
+                .value_parser(clap::value_parser!(std::ffi::OsString))
                 .help("Files to check against sparse profile"),
         )
         .about(HELP_TEXT)
@@ -23,8 +25,12 @@
     let repo = invocation.repo?;
 
     let (matcher, _warnings) = hg::sparse::matcher(&repo).unwrap();
-    let files = invocation.subcommand_args.values_of_os("files");
+    let files = invocation.subcommand_args.get_many::<OsString>("files");
     if let Some(files) = files {
+        let files: Vec<&OsStr> = files
+            .filter(|s| !s.is_empty())
+            .map(|s| s.as_os_str())
+            .collect();
         for file in files {
             invocation.ui.write_stdout(b"matches: ")?;
             invocation.ui.write_stdout(
--- a/rust/rhg/src/commands/files.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/files.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -14,15 +14,14 @@
 Returns 0 on success.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("files")
+pub fn args() -> clap::Command {
+    clap::command!("files")
         .arg(
-            Arg::with_name("rev")
+            Arg::new("rev")
                 .help("search the repository as it is in REV")
-                .short("-r")
-                .long("--revision")
-                .value_name("REV")
-                .takes_value(true),
+                .short('r')
+                .long("revision")
+                .value_name("REV"),
         )
         .about(HELP_TEXT)
 }
@@ -35,7 +34,7 @@
         ));
     }
 
-    let rev = invocation.subcommand_args.value_of("rev");
+    let rev = invocation.subcommand_args.get_one::<String>("rev");
 
     let repo = invocation.repo?;
 
@@ -57,7 +56,8 @@
                 "rhg files -r <rev> is not supported in narrow clones",
             ));
         }
-        let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
+        let files = list_rev_tracked_files(repo, rev)
+            .map_err(|e| (e, rev.as_ref()))?;
         display_files(invocation.ui, repo, files.iter())
     } else {
         // The dirstate always reflects the sparse narrowspec, so if
--- a/rust/rhg/src/commands/root.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/root.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -9,8 +9,8 @@
 Returns 0 on success.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    clap::SubCommand::with_name("root").about(HELP_TEXT)
+pub fn args() -> clap::Command {
+    clap::command!("root").about(HELP_TEXT)
 }
 
 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
--- a/rust/rhg/src/commands/status.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/commands/status.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -8,7 +8,7 @@
 use crate::error::CommandError;
 use crate::ui::Ui;
 use crate::utils::path_utils::RelativizePaths;
-use clap::{Arg, SubCommand};
+use clap::Arg;
 use format_bytes::format_bytes;
 use hg::config::Config;
 use hg::dirstate::has_exec_bit;
@@ -41,75 +41,86 @@
 Some options might be missing, check the list below.
 ";
 
-pub fn args() -> clap::App<'static, 'static> {
-    SubCommand::with_name("status")
+pub fn args() -> clap::Command {
+    clap::command!("status")
         .alias("st")
         .about(HELP_TEXT)
         .arg(
-            Arg::with_name("all")
+            Arg::new("all")
                 .help("show status of all files")
-                .short("-A")
-                .long("--all"),
+                .short('A')
+                .action(clap::ArgAction::SetTrue)
+                .long("all"),
         )
         .arg(
-            Arg::with_name("modified")
+            Arg::new("modified")
                 .help("show only modified files")
-                .short("-m")
-                .long("--modified"),
+                .short('m')
+                .action(clap::ArgAction::SetTrue)
+                .long("modified"),
         )
         .arg(
-            Arg::with_name("added")
+            Arg::new("added")
                 .help("show only added files")
-                .short("-a")
-                .long("--added"),
+                .short('a')
+                .action(clap::ArgAction::SetTrue)
+                .long("added"),
         )
         .arg(
-            Arg::with_name("removed")
+            Arg::new("removed")
                 .help("show only removed files")
-                .short("-r")
-                .long("--removed"),
+                .short('r')
+                .action(clap::ArgAction::SetTrue)
+                .long("removed"),
         )
         .arg(
-            Arg::with_name("clean")
+            Arg::new("clean")
                 .help("show only clean files")
-                .short("-c")
-                .long("--clean"),
+                .short('c')
+                .action(clap::ArgAction::SetTrue)
+                .long("clean"),
         )
         .arg(
-            Arg::with_name("deleted")
+            Arg::new("deleted")
                 .help("show only deleted files")
-                .short("-d")
-                .long("--deleted"),
+                .short('d')
+                .action(clap::ArgAction::SetTrue)
+                .long("deleted"),
         )
         .arg(
-            Arg::with_name("unknown")
+            Arg::new("unknown")
                 .help("show only unknown (not tracked) files")
-                .short("-u")
-                .long("--unknown"),
+                .short('u')
+                .action(clap::ArgAction::SetTrue)
+                .long("unknown"),
         )
         .arg(
-            Arg::with_name("ignored")
+            Arg::new("ignored")
                 .help("show only ignored files")
-                .short("-i")
-                .long("--ignored"),
+                .short('i')
+                .action(clap::ArgAction::SetTrue)
+                .long("ignored"),
         )
         .arg(
-            Arg::with_name("copies")
+            Arg::new("copies")
                 .help("show source of copied files (DEFAULT: ui.statuscopies)")
-                .short("-C")
-                .long("--copies"),
+                .short('C')
+                .action(clap::ArgAction::SetTrue)
+                .long("copies"),
         )
         .arg(
-            Arg::with_name("no-status")
+            Arg::new("no-status")
                 .help("hide status prefix")
-                .short("-n")
-                .long("--no-status"),
+                .short('n')
+                .action(clap::ArgAction::SetTrue)
+                .long("no-status"),
         )
         .arg(
-            Arg::with_name("verbose")
+            Arg::new("verbose")
                 .help("enable additional output")
-                .short("-v")
-                .long("--verbose"),
+                .short('v')
+                .action(clap::ArgAction::SetTrue)
+                .long("verbose"),
         )
 }
 
@@ -200,25 +211,25 @@
     let config = invocation.config;
     let args = invocation.subcommand_args;
 
-    let verbose = !args.is_present("print0")
-        && (args.is_present("verbose")
-            || config.get_bool(b"ui", b"verbose")?
-            || config.get_bool(b"commands", b"status.verbose")?);
+    // TODO add `!args.get_flag("print0") &&` when we support `print0`
+    let verbose = args.get_flag("verbose")
+        || config.get_bool(b"ui", b"verbose")?
+        || config.get_bool(b"commands", b"status.verbose")?;
 
-    let all = args.is_present("all");
+    let all = args.get_flag("all");
     let display_states = if all {
         // TODO when implementing `--quiet`: it excludes clean files
         // from `--all`
         ALL_DISPLAY_STATES
     } else {
         let requested = DisplayStates {
-            modified: args.is_present("modified"),
-            added: args.is_present("added"),
-            removed: args.is_present("removed"),
-            clean: args.is_present("clean"),
-            deleted: args.is_present("deleted"),
-            unknown: args.is_present("unknown"),
-            ignored: args.is_present("ignored"),
+            modified: args.get_flag("modified"),
+            added: args.get_flag("added"),
+            removed: args.get_flag("removed"),
+            clean: args.get_flag("clean"),
+            deleted: args.get_flag("deleted"),
+            unknown: args.get_flag("unknown"),
+            ignored: args.get_flag("ignored"),
         };
         if requested.is_empty() {
             DEFAULT_DISPLAY_STATES
@@ -226,9 +237,9 @@
             requested
         }
     };
-    let no_status = args.is_present("no-status");
+    let no_status = args.get_flag("no-status");
     let list_copies = all
-        || args.is_present("copies")
+        || args.get_flag("copies")
         || config.get_bool(b"ui", b"statuscopies")?;
 
     let repo = invocation.repo?;
--- a/rust/rhg/src/main.rs	Mon Nov 14 17:18:56 2022 +0100
+++ b/rust/rhg/src/main.rs	Tue Nov 15 00:02:43 2022 +0100
@@ -1,10 +1,7 @@
 extern crate log;
 use crate::error::CommandError;
 use crate::ui::{local_to_utf8, Ui};
-use clap::App;
-use clap::AppSettings;
-use clap::Arg;
-use clap::ArgMatches;
+use clap::{command, Arg, ArgMatches};
 use format_bytes::{format_bytes, join};
 use hg::config::{Config, ConfigSource, PlainInfo};
 use hg::repo::{Repo, RepoError};
@@ -35,55 +32,47 @@
 ) -> Result<(), CommandError> {
     check_unsupported(config, repo)?;
 
-    let app = App::new("rhg")
-        .global_setting(AppSettings::AllowInvalidUtf8)
-        .global_setting(AppSettings::DisableVersion)
-        .setting(AppSettings::SubcommandRequired)
-        .setting(AppSettings::VersionlessSubcommands)
+    let app = command!()
+        .subcommand_required(true)
         .arg(
-            Arg::with_name("repository")
+            Arg::new("repository")
                 .help("repository root directory")
-                .short("-R")
-                .long("--repository")
+                .short('R')
                 .value_name("REPO")
-                .takes_value(true)
                 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
                 .global(true),
         )
         .arg(
-            Arg::with_name("config")
+            Arg::new("config")
                 .help("set/override config option (use 'section.name=value')")
-                .long("--config")
                 .value_name("CONFIG")
-                .takes_value(true)
                 .global(true)
+                .long("config")
                 // Ok: `--config section.key1=val --config section.key2=val2`
-                .multiple(true)
                 // Not ok: `--config section.key1=val section.key2=val2`
-                .number_of_values(1),
+                .action(clap::ArgAction::Append),
         )
         .arg(
-            Arg::with_name("cwd")
+            Arg::new("cwd")
                 .help("change working directory")
-                .long("--cwd")
                 .value_name("DIR")
-                .takes_value(true)
+                .long("cwd")
                 .global(true),
         )
         .arg(
-            Arg::with_name("color")
+            Arg::new("color")
                 .help("when to colorize (boolean, always, auto, never, or debug)")
-                .long("--color")
                 .value_name("TYPE")
-                .takes_value(true)
+                .long("color")
                 .global(true),
         )
         .version("0.0.1");
     let app = add_subcommand_args(app);
 
-    let matches = app.clone().get_matches_from_safe(argv.iter())?;
+    let matches = app.clone().try_get_matches_from(argv.iter())?;
 
-    let (subcommand_name, subcommand_matches) = matches.subcommand();
+    let (subcommand_name, subcommand_args) =
+        matches.subcommand().expect("subcommand required");
 
     // Mercurial allows users to define "defaults" for commands, fallback
     // if a default is detected for the current command
@@ -104,9 +93,7 @@
         }
     }
     let run = subcommand_run_fn(subcommand_name)
-        .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
-    let subcommand_args = subcommand_matches
-        .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
+        .expect("unknown subcommand name from clap despite Command::subcommand_required");
 
     let invocation = CliInvocation {
         ui,
@@ -535,7 +522,7 @@
             )+
         }
 
-        fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
+        fn add_subcommand_args(app: clap::Command) -> clap::Command {
             app
             $(
                 .subcommand(commands::$command::args())
@@ -569,7 +556,7 @@
 
 pub struct CliInvocation<'a> {
     ui: &'a Ui,
-    subcommand_args: &'a ArgMatches<'a>,
+    subcommand_args: &'a ArgMatches,
     config: &'a Config,
     /// References inside `Result` is a bit peculiar but allow
     /// `invocation.repo?` to work out with `&CliInvocation` since this
--- a/tests/test-rhg.t	Mon Nov 14 17:18:56 2022 +0100
+++ b/tests/test-rhg.t	Tue Nov 15 00:02:43 2022 +0100
@@ -4,12 +4,11 @@
 
 Unimplemented command
   $ $NO_FALLBACK rhg unimplemented-command
-  unsupported feature: error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
+  unsupported feature: error: The subcommand 'unimplemented-command' wasn't recognized
   
-  USAGE:
-      rhg [OPTIONS] <SUBCOMMAND>
+  Usage: rhg [OPTIONS] <COMMAND>
   
-  For more information try --help
+  For more information try '--help'
   
   [252]
   $ rhg unimplemented-command --config rhg.on-unsupported=abort-silent
@@ -159,10 +158,11 @@
   $ $NO_FALLBACK rhg cat original --exclude="*.rs"
   unsupported feature: error: Found argument '--exclude' which wasn't expected, or isn't valid in this context
   
-  USAGE:
-      rhg cat [OPTIONS] <FILE>...
+    If you tried to supply '--exclude' as a value rather than a flag, use '-- --exclude'
   
-  For more information try --help
+  Usage: rhg cat <FILE>...
+  
+  For more information try '--help'
   
   [252]
   $ rhg cat original --exclude="*.rs"
@@ -190,10 +190,11 @@
   Blocking recursive fallback. The 'rhg.fallback-executable = rhg' config points to `rhg` itself.
   unsupported feature: error: Found argument '--exclude' which wasn't expected, or isn't valid in this context
   
-  USAGE:
-      rhg cat [OPTIONS] <FILE>...
+    If you tried to supply '--exclude' as a value rather than a flag, use '-- --exclude'
   
-  For more information try --help
+  Usage: rhg cat <FILE>...
+  
+  For more information try '--help'
   
   [252]