From 66fe23b4e97cee80b74f109412b9ea9bf67a3ea2 Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Tue, 22 Mar 2022 15:11:28 -0700 Subject: [PATCH] cli: switch from `clap`'s Builder API to its Derive API The Derive API is easier to work with, less error-prone, and less verbose, so it seems like a good improvement. It was quite a bit of work to make the switch, and I'll be surprised if I didn't make any mistakes in the translation. We unfortunately don't have enough e2e tests to be very confident, so we'll have to fix any problems as we discover them. I've at least verified that the output of `jj debug completion --fish` is unchanged, except for a few help texts I changed for different reasons (consistency, clarity, avoiding redundancy). --- Cargo.lock | 20 + Cargo.toml | 2 +- src/commands.rs | 2446 +++++++++++++++++++++++------------------------ 3 files changed, 1202 insertions(+), 1266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93460de06..1905bb67c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,6 +219,7 @@ checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" dependencies = [ "atty", "bitflags", + "clap_derive", "indexmap", "lazy_static", "os_str_bytes", @@ -236,6 +237,19 @@ dependencies = [ "clap 3.1.6", ] +[[package]] +name = "clap_derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_mangen" version = "0.1.2" @@ -573,6 +587,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" diff --git a/Cargo.toml b/Cargo.toml index 4404f2abe..55a100fd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = ["lib"] assert_cmd = "2.0.4" atty = "0.2.14" chrono = "0.4.19" -clap = { version = "3.1.6", features = ["cargo"] } +clap = { version = "3.1.6", features = ["derive"] } clap_complete = "3.1.1" clap_mangen = "0.1" config = "0.12.0" diff --git a/src/commands.rs b/src/commands.rs index b3d03f92d..86e311e76 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -29,7 +29,7 @@ use std::sync::Arc; use std::time::Instant; use std::{fs, io}; -use clap::{crate_version, Arg, ArgMatches, Command}; +use clap::{ArgGroup, CommandFactory, Subcommand}; use criterion::Criterion; use git2::{Oid, Repository}; use itertools::Itertools; @@ -168,24 +168,24 @@ impl From for CommandError { struct CommandHelper<'help> { app: clap::Command<'help>, string_args: Vec, - root_args: ArgMatches, + args: Args, } impl<'help> CommandHelper<'help> { - fn new(app: clap::Command<'help>, string_args: Vec, root_args: ArgMatches) -> Self { + fn new(app: clap::Command<'help>, string_args: Vec, root_args: Args) -> Self { Self { app, string_args, - root_args, + args: root_args, } } - fn root_args(&self) -> &ArgMatches { - &self.root_args + fn args(&self) -> &Args { + &self.args } fn workspace_helper(&self, ui: &Ui) -> Result { - let wc_path_str = self.root_args.value_of("repository").unwrap(); + let wc_path_str = self.args.repository.as_deref().unwrap_or("."); let wc_path = ui.cwd().join(wc_path_str); let workspace = match Workspace::load(ui.settings(), wc_path) { Ok(workspace) => workspace, @@ -209,7 +209,7 @@ jj init --git-repo=."; } }; let repo_loader = workspace.repo_loader(); - let op_str = self.root_args.value_of("at_op").unwrap(); + let op_str = &self.args.at_operation; let repo = if op_str == "@" { repo_loader.load_at_head() } else { @@ -233,7 +233,7 @@ jj init --git-repo=."; ui, workspace, self.string_args.clone(), - &self.root_args, + &self.args, repo, ) } @@ -259,12 +259,11 @@ impl WorkspaceCommandHelper { ui: &Ui, workspace: Workspace, string_args: Vec, - root_args: &ArgMatches, + root_args: &Args, repo: Arc, ) -> Result { - let loaded_at_head = root_args.value_of("at_op").unwrap() == "@"; - let may_update_working_copy = - loaded_at_head && !root_args.is_present("no_commit_working_copy"); + let loaded_at_head = &root_args.at_operation == "@"; + let may_update_working_copy = loaded_at_head && !root_args.no_commit_working_copy; let mut working_copy_shared_with_git = false; let maybe_git_repo = repo.store().git_repo(); if let Some(git_repo) = &maybe_git_repo { @@ -406,14 +405,6 @@ impl WorkspaceCommandHelper { git_ignores } - fn resolve_revision_arg( - &mut self, - ui: &mut Ui, - args: &ArgMatches, - ) -> Result { - self.resolve_single_rev(ui, args.value_of("revision").unwrap()) - } - fn resolve_single_rev( &mut self, ui: &mut Ui, @@ -717,34 +708,6 @@ fn expand_git_path(path_str: String) -> PathBuf { PathBuf::from(path_str) } -fn rev_arg<'help>() -> Arg<'help> { - Arg::new("revision") - .long("revision") - .short('r') - .takes_value(true) - .default_value("@") -} - -fn paths_arg<'help>() -> Arg<'help> { - Arg::new("paths").index(1).multiple_occurrences(true) -} - -fn message_arg<'help>() -> Arg<'help> { - Arg::new("message") - .long("message") - .short('m') - .takes_value(true) -} - -fn op_arg<'help>() -> Arg<'help> { - Arg::new("operation") - .long("operation") - .alias("op") - .short('o') - .takes_value(true) - .default_value("@") -} - fn resolve_single_op(repo: &ReadonlyRepo, op_str: &str) -> Result { if op_str == "@" { // Get it from the repo to make sure that it refers to the operation the repo @@ -820,9 +783,9 @@ fn resolve_single_op_from_store( fn matcher_from_values( ui: &Ui, wc_path: &Path, - values: Option, + values: &[String], ) -> Result, CommandError> { - if let Some(values) = values { + if !values.is_empty() { // TODO: Add support for globs and other formats let mut paths = vec![]; for value in values { @@ -879,848 +842,819 @@ fn update_working_copy( Ok(stats) } -fn get_app<'help>() -> Command<'help> { - let init_command = Command::new("init") - .about("Create a new repo in the given directory") - .long_about( - "Create a new repo in the given directory. If the given directory does not exist, it \ - will be created. If no directory is given, the current directory is used.", - ) - .arg( - Arg::new("destination") - .index(1) - .default_value(".") - .help("The destination directory"), - ) - .arg( - Arg::new("git") - .long("git") - .help("Use the Git backend, creating a jj repo backed by a Git repo"), - ) - .arg( - Arg::new("git-repo") - .long("git-repo") - .takes_value(true) - .conflicts_with("git") - .help("Path to a git repo the jj repo will be backed by"), - ); - let checkout_command = Command::new("checkout") - .alias("co") - .about("Update the working copy to another revision") - .long_about( - "Update the working copy to another revision. If the revision is closed or has \ - conflicts, then a new, open revision will be created on top, and that will be checked \ - out. For more information, see \ - https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.", - ) - .arg( - Arg::new("revision") - .index(1) - .required(true) - .help("The revision to update to"), - ); - let untrack_command = Command::new("untrack") - .about("Stop tracking specified paths in the working copy") - .arg(paths_arg()); - let files_command = Command::new("files") - .about("List files in a revision") - .arg(rev_arg().help("The revision to list files in")) - .arg(paths_arg()); - let diff_command = Command::new("diff") - .about("Show changes in a revision") - .long_about( - "Show changes in a revision. +/// Jujutsu (An experimental VCS) +/// +/// To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/docs/tutorial.md. +#[derive(clap::Parser, Clone, Debug)] +#[clap(author = "Martin von Zweigbergk ", version)] +#[clap(mut_arg("help", |arg| { arg.help("Print help information, more help with --help than with -h")}))] +struct Args { + #[clap(subcommand)] + command: Commands, + /// Path to repository to operate on + /// + /// By default, Jujutsu searches for the closest .jj/ directory in an + /// ancestor of the current working directory. + #[clap(long, short = 'R', global = true)] + repository: Option, + /// Don't commit the working copy + /// + /// By default, Jujutsu commits the working copy on every command, unless + /// you load the repo at a specific operation with `--at-operation`. If + /// you want to avoid committing the working and instead see a possibly + /// stale working copy commit, you can use `--no-commit-working-copy`. + /// This may be useful e.g. in a command prompt, especially if you have + /// another process that commits the working copy. + #[clap(long, global = true)] + no_commit_working_copy: bool, + /// Operation to load the repo at + /// + /// Operation to load the repo at. By default, Jujutsu loads the repo at the + /// most recent operation. You can use `--at-op=` to see what + /// the repo looked like at an earlier operation. For example `jj + /// --at-op= st` will show you what `jj st` would have + /// shown you when the given operation had just + /// finished. + /// + /// Use `jj op log` to find the operation ID you want. Any unambiguous + /// prefix of the operation ID is enough. + /// + /// When loading the repo at an earlier operation, the working copy will not + /// be automatically committed. + /// + /// It is possible to mutating commands when loading the repo at an earlier + /// operation. Doing that is equivalent to having run concurrent commands + /// starting at the earlier operation. There's rarely a reason to do that, + /// but it is possible. + #[clap(long, alias = "at-op", global = true, default_value = "@")] + at_operation: String, +} -With the `-r` option, which is the default, shows the changes compared to the parent revision. If \ - there are several parent revisions (i.e., the given revision is a merge), then they \ - will be merged and the changes from the result to the given revision will be shown. +#[derive(Subcommand, Clone, Debug)] +enum Commands { + Init(InitArgs), + Checkout(CheckoutArgs), + Untrack(UntrackArgs), + Files(FilesArgs), + Diff(DiffArgs), + Show(ShowArgs), + Status(StatusArgs), + Log(LogArgs), + Obslog(ObslogArgs), + Describe(DescribeArgs), + Close(CloseArgs), + Open(OpenArgs), + Duplicate(DuplicateArgs), + Abandon(AbandonArgs), + New(NewArgs), + Move(MoveArgs), + Squash(SquashArgs), + Unsquash(UnsquashArgs), + Restore(RestoreArgs), + Edit(EditArgs), + Split(SplitArgs), + Merge(MergeArgs), + Rebase(RebaseArgs), + Backout(BackoutArgs), + Branch(BranchArgs), + Branches(BranchesArgs), + /// Undo an operation (shortcut for `jj op undo`) + Undo(OperationUndoArgs), + Operation(OperationArgs), + Workspace(WorkspaceArgs), + Git(GitArgs), + Bench(BenchArgs), + Debug(DebugArgs), +} -With the `--from` and/or `--to` options, shows the difference from/to the given revisions. If \ - either is left out, it defaults to the current checkout. For example, `jj diff \ - --from main` shows the changes from \"main\" (perhaps a branch name) to the current \ - checkout.", - ) - .arg( - Arg::new("summary") - .long("summary") - .short('s') - .help("For each path, show only whether it was modified, added, or removed"), - ) - .arg( - Arg::new("git") - .long("git") - .conflicts_with("summary") - .help("Show a Git-format diff"), - ) - .arg( - Arg::new("color-words") - .long("color-words") - .conflicts_with("summary") - .conflicts_with("git") - .help("Show a word-level diff with changes indicated only by color"), - ) - .arg( - Arg::new("revision") - .long("revision") - .short('r') - .takes_value(true) - .help("Show changes changes in this revision, compared to its parent(s)"), - ) - .arg( - Arg::new("from") - .long("from") - .conflicts_with("revision") - .takes_value(true) - .help("Show changes from this revision"), - ) - .arg( - Arg::new("to") - .long("to") - .conflicts_with("revision") - .takes_value(true) - .help("Show changes to this revision"), - ) - .arg(paths_arg()); - let show_command = Command::new("show") - .about("Show commit description and changes in a revision") - .long_about("Show commit description and changes in a revision") - .arg( - Arg::new("summary") - .long("summary") - .short('s') - .help("For each path, show only whether it was modified, added, or removed"), - ) - .arg( - Arg::new("git") - .long("git") - .conflicts_with("summary") - .help("Show a Git-format diff"), - ) - .arg( - Arg::new("color-words") - .long("color-words") - .conflicts_with("summary") - .conflicts_with("git") - .help("Show a word-level diff with changes indicated only by color"), - ) - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("Show changes changes in this revision, compared to its parent(s)"), - ); - let status_command = Command::new("status") - .alias("st") - .about("Show high-level repo status") - .long_about( - "Show high-level repo status. This includes: +/// Create a new repo in the given directory +/// +/// If the given directory does not exist, it will be created. If no directory +/// is given, the current directory is used. +#[derive(clap::Args, Clone, Debug)] +#[clap(group(ArgGroup::new("backend").args(&["git", "git-repo"])))] +struct InitArgs { + /// The destination directory + #[clap(index = 1, default_value = ".")] + destination: String, + /// Use the Git backend, creating a jj repo backed by a Git repo + #[clap(long)] + git: bool, + /// Path to a git repo the jj repo will be backed by + #[clap(long)] + git_repo: Option, +} - * The working copy commit and its (first) \ - parent, and a summary of the changes between them +/// Update the working copy to another revision +/// +/// If the revision is closed or has conflicts, then a new, open +/// revision will be created on top, and that will be checked out. +/// For more information, see https://github.com/martinvonz/jj/blob/main/docs/working-copy.md. +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "co")] +struct CheckoutArgs { + /// The revision to update to + #[clap(index = 1)] + revision: String, +} - * Conflicted branches (see https://github.com/martinvonz/jj/blob/main/docs/branches.md)\ - ", - ); - let log_command = Command::new("log") - .about("Show commit history") - .arg( - Arg::new("template") - .long("template") - .short('T') - .takes_value(true) - .help( - "Render each revision using the given template (the syntax is not yet \ - documented and is likely to change)", - ), - ) - .arg( - Arg::new("revisions") - .long("revisions") - .short('r') - .takes_value(true) - .default_value(":heads()") - .help("Which revisions to show"), - ) - .arg( - Arg::new("no-graph") - .long("no-graph") - .help("Don't show the graph, show a flat list of revisions"), - ); - let obslog_command = Command::new("obslog") - .about("Show how a change has evolved") - .long_about("Show how a change has evolved as it's been updated, rebased, etc.") - .arg(rev_arg()) - .arg( - Arg::new("template") - .long("template") - .short('T') - .takes_value(true) - .help( - "Render each revision using the given template (the syntax is not yet \ - documented)", - ), - ) - .arg( - Arg::new("no-graph") - .long("no-graph") - .help("Don't show the graph, show a flat list of revisions"), - ); - let describe_command = Command::new("describe") - .about("Edit the change description") - .about("Edit the description of a change") - .long_about( - "Starts an editor to let you edit the description of a change. The editor will be \ - $EDITOR, or `pico` if that's not defined.", - ) - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("The revision whose description to edit"), - ) - .arg(message_arg().help("The change description to use (don't open editor)")) - .arg( - Arg::new("stdin") - .long("stdin") - .help("Read the change description from stdin"), - ); - let close_command = Command::new("close") - .alias("commit") - .about("Mark a revision closed") - .long_about( - "Mark a revision closed. For information about open/closed revisions, see \ - https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.", - ) - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("The revision to close"), - ) - .arg( - Arg::new("edit") - .long("edit") - .short('e') - .help("Also edit the description"), - ) - .arg(message_arg().help("The change description to use (don't open editor)")); - let open_command = Command::new("open") - .about("Mark a revision open") - .alias("uncommit") - .long_about( - "Mark a revision open. For information about open/closed revisions, see \ - https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.", - ) - .arg( - Arg::new("revision") - .index(1) - .required(true) - .help("The revision to open"), - ); - let duplicate_command = Command::new("duplicate") - .about("Create a new change with the same content as an existing one") - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("The revision to duplicate"), - ); - let abandon_command = Command::new("abandon") - .about("Abandon a revision") - .long_about( - "Abandon a revision, rebasing descendants onto its parent(s). The behavior is similar \ - to `jj restore`; the difference is that `jj abandon` gives you a new change, while \ - `jj restore` updates the existing change.", - ) - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("The revision(s) to abandon"), - ); - let new_command = Command::new("new") - .about("Create a new, empty change") - .long_about( - "Create a new, empty change. This may be useful if you want to make some changes \ - you're unsure of on top of the working copy. If the changes turned out to useful, \ - you can `jj squash` them into the previous working copy. If they turned out to be \ - unsuccessful, you can `jj abandon` them and `jj co @-` the previous working copy.", - ) - .arg( - Arg::new("revision") - .index(1) - .default_value("@") - .help("Parent of the new change") - .long_help( - "Parent of the new change. If the parent is the working copy, then the new \ - change will be checked out.", - ), - ); - let move_command = Command::new("move") - .about("Move changes from one revision into another") - .long_about( - "Move changes from a revision into another revision. Use `--interactive` to move only \ - part of the source revision into the destination. The selected changes (or all the \ - changes in the source revision if not using `--interactive`) will be moved into the \ - destination. The changes will be removed from the source. If that means that the \ - source is now empty compared to its parent, it will be abandoned.", - ) - .arg( - Arg::new("from") - .long("from") - .takes_value(true) - .default_value("@") - .help("Move part of this change into the destination"), - ) - .arg( - Arg::new("to") - .long("to") - .takes_value(true) - .default_value("@") - .help("Move part of the source into this change"), - ) - .arg( - Arg::new("interactive") - .long("interactive") - .short('i') - .help("Interactively choose which parts to move"), - ); - let squash_command = Command::new("squash") - .alias("amend") - .about("Move changes from a revision into its parent") - .long_about( - "Move changes from a revision into its parent. After moving the changes into the \ - parent, the child revision will have the same content state as before. If that means \ - that the change is now empty compared to its parent, it will be abandoned. This will \ - always be the case without `--interactive`.", - ) - .arg(rev_arg()) - .arg( - Arg::new("interactive") - .long("interactive") - .short('i') - .help("Interactively choose which parts to squash"), - ); +/// Stop tracking specified paths in the working copy +#[derive(clap::Args, Clone, Debug)] +struct UntrackArgs { + #[clap(index = 1)] + paths: Vec, +} + +/// List files in a revision +#[derive(clap::Args, Clone, Debug)] +struct FilesArgs { + /// The revision to list files in + #[clap(long, short, default_value = "@")] + revision: String, + #[clap(index = 1)] + paths: Vec, +} + +#[derive(clap::Args, Clone, Debug)] +#[clap(group(ArgGroup::new("format").args(&["summary", "git", "color-words"])))] +struct DiffFormat { + /// For each path, show only whether it was modified, added, or removed + #[clap(long, short)] + summary: bool, + /// Show a Git-format diff + #[clap(long)] + git: bool, + /// Show a word-level diff with changes indicated only by color + #[clap(long)] + color_words: bool, +} + +/// Show changes in a revision +/// +/// With the `-r` option, which is the default, shows the changes compared to +/// the parent revision. If there are several parent revisions (i.e., the given +/// revision is a merge), then they will be merged and the changes from the +/// result to the given revision will be shown. +/// +/// With the `--from` and/or `--to` options, shows the difference from/to the +/// given revisions. If either is left out, it defaults to the current checkout. +/// For example, `jj diff --from main` shows the changes from "main" (perhaps a +/// branch name) to the current checkout. +#[derive(clap::Args, Clone, Debug)] +struct DiffArgs { + /// Show changes changes in this revision, compared to its parent(s) + #[clap(long, short)] + revision: Option, + /// Show changes from this revision + #[clap(long, conflicts_with = "revision")] + from: Option, + /// Show changes to this revision + #[clap(long, conflicts_with = "revision")] + to: Option, + /// Restrict the diff to these paths + #[clap(index = 1)] + paths: Vec, + #[clap(flatten)] + format: DiffFormat, +} + +/// Show commit description and changes in a revision +#[derive(clap::Args, Clone, Debug)] +struct ShowArgs { + /// Show changes changes in this revision, compared to its parent(s) + #[clap(index = 1, default_value = "@")] + revision: String, + #[clap(flatten)] + format: DiffFormat, +} + +/// Show high-level repo status +/// +/// This includes: +/// +/// * The working copy commit and its (first) parent, and a summary of the +/// changes between them +/// +/// * Conflicted branches (see https://github.com/martinvonz/jj/blob/main/docs/branches.md) +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "st")] +struct StatusArgs {} + +/// Show commit history +#[derive(clap::Args, Clone, Debug)] +struct LogArgs { + /// Which revisions to show + #[clap(long, short, default_value = ":heads()")] + revisions: String, + /// Don't show the graph, show a flat list of revisions + #[clap(long)] + no_graph: bool, + /// Render each revision using the given template (the syntax is not yet + /// documented and is likely to change) + #[clap(long, short = 'T')] + template: Option, +} + +/// Show how a change has evolved +/// +/// Show how a change has evolved as it's been updated, rebased, etc. +#[derive(clap::Args, Clone, Debug)] +struct ObslogArgs { + #[clap(long, short, default_value = "@")] + revision: String, + /// Don't show the graph, show a flat list of revisions + #[clap(long)] + no_graph: bool, + /// Render each revision using the given template (the syntax is not yet + /// documented and is likely to change) + #[clap(long, short = 'T')] + template: Option, +} + +/// Edit the change description +/// +/// Starts an editor to let you edit the description of a change. The editor +/// will be $EDITOR, or `pico` if that's not defined. +#[derive(clap::Args, Clone, Debug)] +struct DescribeArgs { + /// The revision whose description to edit + #[clap(index = 1, default_value = "@")] + revision: String, + /// The change description to use (don't open editor) + #[clap(long, short)] + message: Option, + /// Read the change description from stdin + #[clap(long)] + stdin: bool, +} + +/// Mark a revision closed +/// +/// For information about open/closed revisions, see https://github.com/martinvonz/jj/blob/main/docs/working-copy.md. +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "commit")] +struct CloseArgs { + /// The revision to close + #[clap(index = 1, default_value = "@")] + revision: String, + /// The change description to use (don't open editor) + #[clap(long, short)] + message: Option, + /// Also edit the description + #[clap(long, short)] + edit: bool, +} + +/// Mark a revision open +/// +/// For information about open/closed revisions, see https://github.com/martinvonz/jj/blob/main/docs/working-copy.md. +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "uncommit")] +struct OpenArgs { + /// The revision to open + #[clap(index = 1)] + revision: String, +} + +/// Create a new change with the same content as an existing one +/// +/// For information about open/closed revisions, see https://github.com/martinvonz/jj/blob/main/docs/working-copy.md. +#[derive(clap::Args, Clone, Debug)] +struct DuplicateArgs { + /// The revision to duplicate + #[clap(index = 1, default_value = "@")] + revision: String, +} + +/// Abandon a revision +/// +/// Abandon a revision, rebasing descendants onto its parent(s). The behavior is +/// similar to `jj restore`; the difference is that `jj abandon` gives you a new +/// change, while `jj restore` updates the existing change. +#[derive(clap::Args, Clone, Debug)] +struct AbandonArgs { + /// The revision(s) to abandon + #[clap(index = 1, default_value = "@")] + revisions: String, +} + +/// Create a new, empty change +/// +/// This may be useful if you want to make some changes +/// you're unsure of on top of the working copy. If the changes turned out to +/// useful, you can `jj squash` them into the previous working copy. If they +/// turned out to be unsuccessful, you can `jj abandon` them and `jj co @-` the +/// previous working copy. +#[derive(clap::Args, Clone, Debug)] +struct NewArgs { + /// Parent of the new change + /// + /// If the parent is the working copy, then the new change will be checked + /// out. + #[clap(index = 1, default_value = "@")] + revision: String, +} + +/// Move changes from one revision into another +/// +/// Use `--interactive` to move only +/// part of the source revision into the destination. The selected changes (or +/// all the changes in the source revision if not using `--interactive`) will be +/// moved into the destination. The changes will be removed from the source. If +/// that means that the source is now empty compared to its parent, it will be +/// abandoned. +#[derive(clap::Args, Clone, Debug)] +struct MoveArgs { + /// Move part of this change into the destination + #[clap(long, default_value = "@")] + from: String, + /// Move part of the source into this change + #[clap(long, default_value = "@")] + to: String, + /// Interactively choose which parts to move + #[clap(long, short)] + interactive: bool, +} + +/// Move changes from a revision into its parent +/// +/// After moving the changes into the parent, the child revision will have the +/// same content state as before. If that means that the change is now empty +/// compared to its parent, it will be abandoned. This will always be the case +/// without `--interactive`. +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "amend")] +struct SquashArgs { + #[clap(long, short, default_value = "@")] + revision: String, + /// Interactively choose which parts to squash + #[clap(long, short)] + interactive: bool, +} + +/// Move changes from a revision's parent into the revision +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "unamend")] +struct UnsquashArgs { + #[clap(long, short, default_value = "@")] + revision: String, + /// Interactively choose which parts to unsquash // TODO: It doesn't make much sense to run this without -i. We should make that // the default. We should also abandon the parent commit if that becomes empty. - let unsquash_command = Command::new("unsquash") - .alias("unamend") - .about("Move changes from a revision's parent into the revision") - .arg(rev_arg()) - .arg( - Arg::new("interactive") - .long("interactive") - .short('i') - .help("Interactively choose which parts to unsquash"), - ); - let restore_command = Command::new("restore") - .about("Restore paths from another revision") - .long_about( - "Restore paths from another revision. That means that the paths get the same content \ - in the destination (`--to`) as they had in the source (`--from`). This is typically \ - used for undoing changes to some paths in the working copy (`jj restore `). - - If you restore from a revision where the path has conflicts, then the destination revision will \ - have the same conflict. If the destination is the working copy, then a new commit \ - will be created on top for resolving the conflict (as if you had run `jj checkout` \ - on the new revision). Taken together, that means that if you're already resolving \ - conflicts and you want to restart the resolution of some file, you may want to run \ - `jj restore ; jj squash`.", - ) - .arg( - Arg::new("from") - .long("from") - .takes_value(true) - .default_value("@-") - .help("Revision to restore from (source)"), - ) - .arg( - Arg::new("to") - .long("to") - .takes_value(true) - .default_value("@") - .help("Revision to restore into (destination)"), - ) - .arg( - Arg::new("interactive") - .long("interactive") - .short('i') - .help("Interactively choose which parts to restore"), - ) - .arg(paths_arg()); - let edit_command = Command::new("edit") - .about("Edit the content changes in a revision") - .long_about( - "Lets you interactively edit the content changes in a revision. - -Starts a diff editor (`meld` by default) on the changes in the revision. Edit the right side of \ - the diff until it looks the way you want. Once you close the editor, the revision \ - will be updated. Descendants will be rebased on top as usual, which may result in \ - conflicts. See `jj squash -i` or `jj unsquash -i` if you instead want to move \ - changes into or out of the parent revision.", - ) - .arg(rev_arg().help("The revision to edit")); - let split_command = Command::new("split") - .about("Split a revision in two") - .long_about( - "Lets you interactively split a revision in two. - -Starts a diff editor (`meld` by default) on the changes in the revision. Edit the right side of \ - the diff until it has the content you want in the first revision. Once you close the \ - editor, your edited content will replace the previous revision. The remaining \ - changes will be put in a new revision on top. You will be asked to enter a change \ - description for each.", - ) - .arg(rev_arg().help("The revision to split")); - let merge_command = Command::new("merge") - .about("Merge work from multiple branches") - .long_about( - "Merge work from multiple branches. - -Unlike most other VCSs, `jj merge` does not implicitly include the working copy revision's parent \ - as one of the parents of the merge; you need to explicitly list all revisions that \ - should become parents of the merge. Also, you need to explicitly check out the \ - resulting revision if you want to.", - ) - .arg( - Arg::new("revisions") - .index(1) - .required(true) - .multiple_occurrences(true), - ) - .arg(message_arg().help("The change description to use (don't open editor)")); - let rebase_command = Command::new("rebase") - .about("Move a revision to a different parent") - .long_about( - "Move a revision to a different parent. - -With `-s`, rebases the specified revision and its descendants onto the destination. For example, -`jj rebase -s B -d D` would transform your history like this: - -D C' -| | -| C B' -| | => | -| B D -|/ | -A A - -With `-r`, rebases only the specified revision onto the destination. Any \"hole\" left behind will \ - be filled by rebasing descendants onto the specified revision's parent(s). For \ - example, `jj rebase -r B -d D` would transform your history like this: - -D B' -| | -| C D -| | => | -| B | C' -|/ |/ -A A", - ) - .arg( - Arg::new("revision") - .long("revision") - .short('r') - .takes_value(true) - .help( - "Rebase only this revision, rebasing descendants onto this revision's \ - parent(s)", - ), - ) - .arg( - Arg::new("source") - .long("source") - .short('s') - .conflicts_with("revision") - .takes_value(true) - .required(false) - .multiple_occurrences(false) - .help("Rebase this revision and its descendants"), - ) - .arg( - Arg::new("destination") - .long("destination") - .short('d') - .takes_value(true) - .required(true) - .multiple_occurrences(true) - .help("The revision to rebase onto"), - ); - // TODO: It seems better to default the destination to `@-`. Maybe the working - // copy should be rebased on top? - let backout_command = Command::new("backout") - .about("Apply the reverse of a revision on top of another revision") - .arg(rev_arg().help("The revision to apply the reverse of")) - .arg( - Arg::new("destination") - .long("destination") - .short('d') - .takes_value(true) - .default_value("@") - .multiple_occurrences(true) - .help("The revision to apply the reverse changes on top of"), - ); - let branch_command = Command::new("branch") - .about("Create, update, or delete a branch") - .long_about( - "Create, update, or delete a branch. For information about branches, see \ - https://github.com/martinvonz/jj/blob/main/docs/branches.md.", - ) - .arg(rev_arg().help("The branch's target revision")) - .arg( - Arg::new("allow-backwards") - .long("allow-backwards") - .help("Allow moving the branch backwards or sideways"), - ) - .arg( - Arg::new("delete") - .long("delete") - .help("Delete the branch locally") - .long_help( - "Delete the branch locally. The deletion will be propagated to remotes on \ - push.", - ), - ) - .arg( - Arg::new("forget") - .long("forget") - .help("Forget the branch") - .long_help( - "Forget everything about the branch. There will be no record of its position \ - on remotes (no branchname@remotename in log output). Pushing will not affect \ - the branch on the remote. Pulling will bring the branch back if it still \ - exists on the remote.", - ), - ) - .arg( - Arg::new("name") - .index(1) - .required(true) - .help("The name of the branch to move or delete"), - ); - let branches_command = Command::new("branches").about("List branches").long_about( - "\ -List branches and their targets. A remote branch will be included only if its target is different \ - from the local target. For a conflicted branch (both local and remote), old target \ - revisions are preceded by a \"-\" and new target revisions are preceded by a \"+\". - -For information about branches, see https://github.com/martinvonz/jj/blob/main/docs/branches.md", - ); - let undo_command = Command::new("undo") - .about("Undo an operation") - .arg(op_arg().help("The operation to undo")); - let operation_command = Command::new("operation") - .alias("op") - .about("Commands for working with the operation log") - .long_about( - "Commands for working with the operation log. For information about the \ - operation log, see https://github.com/martinvonz/jj/blob/main/docs/operation-log.md.", - ) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand(Command::new("log").about("Show the operation log")) - .subcommand(undo_command.clone()) - .subcommand( - Command::new("restore") - .about("Restore to the state at an operation") - .arg(op_arg().help("The operation to restore to")), - ); - let undo_command = undo_command.about("Undo an operation (shortcut for `jj op undo`)"); - let workspace_command = Command::new("workspace") - .about("Commands for working with workspaces") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("add") - .about("Add a workspace") - .arg( - Arg::new("destination") - .index(1) - .required(true) - .help("Where to create the new workspace"), - ) - .arg( - Arg::new("name") - .long("name") - .takes_value(true) - .help("A name for the workspace") - .long_help( - "A name for the workspace, to override the default, which is the \ - basename of the destination directory.", - ), - ), - ) - .subcommand( - Command::new("forget") - .about("Stop tracking a workspace's checkout") - .long_about( - "Stop tracking a workspace's checkout in the repo. The workspace will not be \ - touched on disk. It can be deleted from disk before or after running this \ - command.", - ) - .arg(Arg::new("workspace").index(1)), - ) - .subcommand(Command::new("list").about("List workspaces")); - let git_command = Command::new("git") - .about("Commands for working with the underlying Git repo") - .long_about( - "Commands for working with the underlying Git repo. - -For a comparison with Git, including a table of commands, see \ -https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md.\ - ", - ) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("remote") - .about("Manage Git remotes") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("add") - .about("Add a Git remote") - .arg( - Arg::new("remote") - .index(1) - .required(true) - .help("The remote's name"), - ) - .arg( - Arg::new("url") - .index(2) - .required(true) - .help("The remote's URL"), - ), - ) - .subcommand( - Command::new("remove") - .about("Remove a Git remote and forget its branches") - .arg( - Arg::new("remote") - .index(1) - .required(true) - .help("The remote's name"), - ), - ), - ) - .subcommand( - Command::new("fetch").about("Fetch from a Git remote").arg( - Arg::new("remote") - .long("remote") - .takes_value(true) - .default_value("origin") - .help("The remote to fetch from (only named remotes are supported)"), - ), - ) - .subcommand( - Command::new("clone") - .about("Create a new repo backed by a clone of a Git repo") - .long_about( - "Create a new repo backed by a clone of a Git repo. The Git repo will be a \ - bare git repo stored inside the `.jj/` directory.", - ) - .arg( - Arg::new("source") - .index(1) - .required(true) - .help("URL or path of the Git repo to clone"), - ) - .arg( - Arg::new("destination") - .index(2) - .help("The directory to write the Jujutsu repo to"), - ), - ) - .subcommand( - Command::new("push") - .about("Push to a Git remote") - .long_about( - "Push to a Git remote - -By default, all branches are pushed. Use `--branch` if you want to push only one branch.", - ) - .arg( - Arg::new("branch") - .long("branch") - .takes_value(true) - .help("Push only this branch"), - ) - .arg( - Arg::new("remote") - .long("remote") - .takes_value(true) - .default_value("origin") - .help("The remote to push to (only named remotes are supported)"), - ), - ) - .subcommand( - Command::new("import") - .about("Update repo with changes made in the underlying Git repo"), - ) - .subcommand( - Command::new("export") - .about("Update the underlying Git repo with changes made in the repo"), - ); - let bench_command = Command::new("bench") - .about("Commands for benchmarking internal operations") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("commonancestors") - .about("Find the common ancestor(s) of a set of commits") - .arg(Arg::new("revision1").index(1).required(true)) - .arg(Arg::new("revision2").index(2).required(true)), - ) - .subcommand( - Command::new("isancestor") - .about("Checks if the first commit is an ancestor of the second commit") - .arg(Arg::new("ancestor").index(1).required(true)) - .arg(Arg::new("descendant").index(2).required(true)), - ) - .subcommand( - Command::new("walkrevs") - .about( - "Walk revisions that are ancestors of the second argument but not ancestors \ - of the first", - ) - .arg(Arg::new("unwanted").index(1).required(true)) - .arg(Arg::new("wanted").index(2).required(true)), - ) - .subcommand( - Command::new("resolveprefix") - .about("Resolve a commit ID prefix") - .arg(Arg::new("prefix").index(1).required(true)), - ); - let debug_command = Command::new("debug") - .about("Low-level commands not intended for users") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("completion") - .about("Print a command-line-completion script") - .arg(Arg::new("bash").long("bash")) - .arg(Arg::new("fish").long("fish")) - .arg(Arg::new("zsh").long("zsh")), - ) - .subcommand(Command::new("mangen").about("Print a ROFF (manpage)")) - .subcommand( - Command::new("resolverev") - .about("Resolve a revision identifier to its full ID") - .arg(rev_arg()), - ) - .subcommand( - Command::new("workingcopy").about("Show information about the working copy state"), - ) - .subcommand( - Command::new("template") - .about("Parse a template") - .arg(Arg::new("template").index(1).required(true)), - ) - .subcommand(Command::new("index").about("Show commit index stats")) - .subcommand(Command::new("reindex").about("Rebuild commit index")); - Command::new("jj") - .subcommand_required(true) - .arg_required_else_help(true) - .version(crate_version!()) - .author("Martin von Zweigbergk ") - .about( - "Jujutsu (An experimental VCS) - -To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/docs/tutorial.md.\ - ", - ) - .mut_arg("help", |arg| { - arg.help("Print help information, more help with --help than with -h") - }) - .arg( - Arg::new("repository") - .long("repository") - .short('R') - .global(true) - .takes_value(true) - .default_value(".") - .help("Path to repository to operate on") - .long_help( - "Path to repository to operate on. By default, Jujutsu searches for the \ - closest .jj/ directory in an ancestor of the current working directory.", - ), - ) - .arg( - Arg::new("no_commit_working_copy") - .long("no-commit-working-copy") - .global(true) - .help("Don't commit the working copy") - .long_help( - "Don't commit the working copy. By default, Jujutsu commits the working copy \ - on every command, unless you load the repo at a specific operation with \ - `--at-operation`. If you want to avoid committing the working and instead \ - see a possibly stale working copy commit, you can use \ - `--no-commit-working-copy`. This may be useful e.g. in a command prompt, \ - especially if you have another process that commits the working copy.", - ), - ) - .arg( - Arg::new("at_op") - .long("at-operation") - .alias("at-op") - .global(true) - .takes_value(true) - .default_value("@") - .help("Operation to load the repo at") - .long_help( - "Operation to load the repo at. By default, Jujutsu loads the repo at the \ - most recent operation. You can use `--at-op=` to see what the \ - repo looked like at an earlier operation. For example `jj --at-op= st` will show you what `jj st` would have shown you when the given \ - operation had just finished. - -Use `jj op log` to find the operation ID you want. Any unambiguous prefix of the operation ID is \ - enough. - -When loading the repo at an earlier operation, the working copy will not be automatically \ - committed. - -It is possible to mutating commands when loading the repo at an earlier operation. Doing that is \ - equivalent to having run concurrent commands starting at the earlier \ - operation. There's rarely a reason to do that, but it is possible. -", - ), - ) - .subcommand(init_command) - .subcommand(checkout_command) - .subcommand(untrack_command) - .subcommand(files_command) - .subcommand(diff_command) - .subcommand(show_command) - .subcommand(status_command) - .subcommand(log_command) - .subcommand(obslog_command) - .subcommand(describe_command) - .subcommand(close_command) - .subcommand(open_command) - .subcommand(duplicate_command) - .subcommand(abandon_command) - .subcommand(new_command) - .subcommand(move_command) - .subcommand(squash_command) - .subcommand(unsquash_command) - .subcommand(restore_command) - .subcommand(edit_command) - .subcommand(split_command) - .subcommand(merge_command) - .subcommand(rebase_command) - .subcommand(backout_command) - .subcommand(branch_command) - .subcommand(branches_command) - .subcommand(operation_command) - .subcommand(undo_command) - .subcommand(workspace_command) - .subcommand(git_command) - .subcommand(bench_command) - .subcommand(debug_command) + #[clap(long, short)] + interactive: bool, } +/// Restore paths from another revision +/// +/// That means that the paths get the same content in the destination (`--to`) +/// as they had in the source (`--from`). This is typically used for undoing +/// changes to some paths in the working copy (`jj restore `). +/// +/// If you restore from a revision where the path has conflicts, then the +/// destination revision will have the same conflict. If the destination is the +/// working copy, then a new commit will be created on top for resolving the +/// conflict (as if you had run `jj checkout` on the new revision). Taken +/// together, that means that if you're already resolving conflicts and you want +/// to restart the resolution of some file, you may want to run `jj restore +/// ; jj squash`. +#[derive(clap::Args, Clone, Debug)] +struct RestoreArgs { + /// Revision to restore from (source) + #[clap(long, default_value = "@-")] + from: String, + /// Revision to restore into (destination) + #[clap(long, default_value = "@")] + to: String, + /// Interactively choose which parts to restore + #[clap(long, short)] + interactive: bool, + #[clap(index = 1)] + paths: Vec, +} + +/// Edit the content changes in a revision +/// +/// Starts a diff editor (`meld` by default) on the changes in the revision. +/// Edit the right side of the diff until it looks the way you want. Once you +/// close the editor, the revision will be updated. Descendants will be rebased +/// on top as usual, which may result in conflicts. See `jj squash -i` or `jj +/// unsquash -i` if you instead want to move changes into or out of the parent +/// revision. +#[derive(clap::Args, Clone, Debug)] +struct EditArgs { + /// The revision to edit + #[clap(long, short, default_value = "@")] + revision: String, +} + +/// Split a revision in two +/// +/// Starts a diff editor (`meld` by default) on the changes in the revision. +/// Edit the right side of the diff until it has the content you want in the +/// first revision. Once you close the editor, your edited content will replace +/// the previous revision. The remaining changes will be put in a new revision +/// on top. You will be asked to enter a change description for each. +#[derive(clap::Args, Clone, Debug)] +struct SplitArgs { + /// The revision to split + #[clap(long, short, default_value = "@")] + revision: String, +} + +/// Merge work from multiple branches +/// +/// Unlike most other VCSs, `jj merge` does not implicitly include the working +/// copy revision's parent as one of the parents of the merge; you need to +/// explicitly list all revisions that should become parents of the merge. Also, +/// you need to explicitly check out the resulting revision if you want to. +#[derive(clap::Args, Clone, Debug)] +struct MergeArgs { + #[clap(index = 1)] + revisions: Vec, + /// The change description to use (don't open editor) + #[clap(long, short)] + message: Option, +} + +/// Move a revision to a different parent +/// +/// With `-s`, rebases the specified revision and its descendants onto the +/// destination. For example, `jj rebase -s B -d D` would transform your history +/// like this: +/// +/// D C' +/// | | +/// | C B' +/// | | => | +/// | B D +/// |/ | +/// A A +/// +/// With `-r`, rebases only the specified revision onto the destination. Any +/// "hole" left behind will be filled by rebasing descendants onto +/// the specified revision's parent(s). For example, `jj rebase -r +/// B -d D` would transform your history like this: +/// +/// D B' +/// | | +/// | C D +/// | | => | +/// | B | C' +/// |/ |/ +/// A A +#[derive(clap::Args, Clone, Debug)] +#[clap(group(ArgGroup::new("to_rebase").args(&["revision", "source"])))] +struct RebaseArgs { + /// Rebase only this revision, rebasing descendants onto this revision's + /// parent(s) + #[clap(long, short)] + revision: Option, + /// Rebase this revision and its descendants + #[clap(long, short)] + source: Option, + /// The revision to rebase onto + #[clap(long, short)] + destination: Vec, +} + +/// Apply the reverse of a revision on top of another revision +#[derive(clap::Args, Clone, Debug)] +struct BackoutArgs { + /// The revision to apply the reverse of + #[clap(long, short, default_value = "@")] + revision: String, + /// The revision to apply the reverse changes on top of + // TODO: It seems better to default this to `@-`. Maybe the working + // copy should be rebased on top? + #[clap(long, short, default_value = "@")] + destination: Vec, +} + +/// Create, update, or delete a branch +/// +/// For information about branches, see https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Args, Clone, Debug)] +struct BranchArgs { + /// The branch's target revision + #[clap(long, short, default_value = "@")] + revision: String, + /// Allow moving the branch backwards or sideways + #[clap(long)] + allow_backwards: bool, + /// Delete the branch locally + /// + /// The deletion will be propagated to remotes on push. + #[clap(long)] + delete: bool, + /// The name of the branch to move or delete + #[clap(long)] + forget: bool, + #[clap(index = 1)] + name: String, +} + +/// List branches and their targets +/// +/// A remote branch will be included only if its target is different from the +/// local target. For a conflicted branch (both local and remote), old target +/// revisions are preceded by a "-" and new target revisions are preceded by a +/// "+". For information about branches, see https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Args, Clone, Debug)] +struct BranchesArgs {} + +/// Commands for working with the operation log +/// +/// Commands for working with the operation log. For information about the +/// operation log, see https://github.com/martinvonz/jj/blob/main/docs/operation-log.md. +#[derive(clap::Args, Clone, Debug)] +#[clap(alias = "op")] +struct OperationArgs { + #[clap(subcommand)] + command: OperationCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum OperationCommands { + Log(OperationLogArgs), + Undo(OperationUndoArgs), + Restore(OperationRestoreArgs), +} + +/// Show the operation log +#[derive(clap::Args, Clone, Debug)] +struct OperationLogArgs {} + +/// Restore to the state at an operation +#[derive(clap::Args, Clone, Debug)] +struct OperationRestoreArgs { + /// The operation to restore to + #[clap(long, alias = "op", short, default_value = "@")] + operation: String, +} + +/// Undo an operation +#[derive(clap::Args, Clone, Debug)] +struct OperationUndoArgs { + /// The operation to undo + #[clap(long, alias = "op", short, default_value = "@")] + operation: String, +} + +/// Commands for working with workspaces +#[derive(clap::Args, Clone, Debug)] +struct WorkspaceArgs { + #[clap(subcommand)] + command: WorkspaceCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum WorkspaceCommands { + Add(WorkspaceAddArgs), + Forget(WorkspaceForgetArgs), + List(WorkspaceListArgs), +} + +/// Add a workspace +#[derive(clap::Args, Clone, Debug)] +struct WorkspaceAddArgs { + /// Where to create the new workspace + #[clap(index = 1)] + destination: String, + /// A name for the workspace + /// + /// To override the default, which is the basename of the destination + /// directory. + #[clap(long)] + name: Option, +} + +/// Stop tracking a workspace's checkout in the repo +/// +/// The workspace will not be touched on disk. It can be deleted from disk +/// before or after running this command. +#[derive(clap::Args, Clone, Debug)] +struct WorkspaceForgetArgs { + #[clap(index = 1)] + workspace: Option, +} + +/// List workspaces +#[derive(clap::Args, Clone, Debug)] +struct WorkspaceListArgs {} + +/// Commands for working with the underlying Git repo +/// +/// For a comparison with Git, including a table of commands, see https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md. +#[derive(clap::Args, Clone, Debug)] +struct GitArgs { + #[clap(subcommand)] + command: GitCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum GitCommands { + Remote(GitRemoteArgs), + Fetch(GitFetchArgs), + Clone(GitCloneArgs), + Push(GitPushArgs), + Import(GitImportArgs), + Export(GitExportArgs), +} + +/// Manage Git remotes +/// +/// The Git repo will be a bare git repo stored inside the `.jj/` directory. +#[derive(clap::Args, Clone, Debug)] +struct GitRemoteArgs { + #[clap(subcommand)] + command: GitRemoteCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum GitRemoteCommands { + Add(GitRemoteAddArgs), + Remove(GitRemoteRemoveArgs), +} + +/// Add a Git remote +#[derive(clap::Args, Clone, Debug)] +struct GitRemoteAddArgs { + /// The remote's name + #[clap(index = 1)] + remote: String, + /// The remote's URL + #[clap(index = 1)] + url: String, +} + +/// Remove a Git remote and forget its branches +#[derive(clap::Args, Clone, Debug)] +struct GitRemoteRemoveArgs { + /// The remote's name + #[clap(index = 1)] + remote: String, +} + +/// Fetch from a Git remote +#[derive(clap::Args, Clone, Debug)] +struct GitFetchArgs { + /// The remote to fetch from (only named remotes are supported) + #[clap(long, default_value = "origin")] + remote: String, +} + +/// Create a new repo backed by a clone of a Git repo +/// +/// The Git repo will be a bare git repo stored inside the `.jj/` directory. +#[derive(clap::Args, Clone, Debug)] +struct GitCloneArgs { + /// URL or path of the Git repo to clone + #[clap(index = 1)] + source: String, + /// The directory to write the Jujutsu repo to + #[clap(index = 2)] + destination: Option, +} + +/// Push to a Git remote +/// +/// By default, all branches are pushed. Use `--branch` if you want to push only +/// one branch. +#[derive(clap::Args, Clone, Debug)] +struct GitPushArgs { + /// The remote to push to (only named remotes are supported) + #[clap(long, default_value = "origin")] + remote: String, + /// Push only this branch + #[clap(long)] + branch: Option, +} + +/// Update repo with changes made in the underlying Git repo +#[derive(clap::Args, Clone, Debug)] +struct GitImportArgs {} + +/// Update the underlying Git repo with changes made in the repo +#[derive(clap::Args, Clone, Debug)] +struct GitExportArgs {} + +/// Commands for benchmarking internal operations +#[derive(clap::Args, Clone, Debug)] +struct BenchArgs { + #[clap(subcommand)] + command: BenchCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum BenchCommands { + #[clap(name = "commonancestors")] + CommonAncestors(BenchCommonAncestorsArgs), + #[clap(name = "isancestor")] + IsAncestor(BenchIsAncestorArgs), + #[clap(name = "walkrevs")] + WalkRevs(BenchWalkRevsArgs), + #[clap(name = "resolveprefix")] + ResolvePrefix(BenchResolvePrefixArgs), +} + +/// Find the common ancestor(s) of a set of commits +#[derive(clap::Args, Clone, Debug)] +struct BenchCommonAncestorsArgs { + #[clap(index = 1)] + revision1: String, + #[clap(index = 2)] + revision2: String, +} + +/// Checks if the first commit is an ancestor of the second commit +#[derive(clap::Args, Clone, Debug)] +struct BenchIsAncestorArgs { + #[clap(index = 1)] + ancestor: String, + #[clap(index = 2)] + descendant: String, +} + +/// Walk revisions that are ancestors of the second argument but not ancestors +/// of the first +#[derive(clap::Args, Clone, Debug)] +struct BenchWalkRevsArgs { + #[clap(index = 1)] + unwanted: String, + #[clap(index = 2)] + wanted: String, +} + +/// Resolve a commit ID prefix +#[derive(clap::Args, Clone, Debug)] +struct BenchResolvePrefixArgs { + #[clap(index = 1)] + prefix: String, +} + +/// Low-level commands not intended for users +#[derive(clap::Args, Clone, Debug)] +struct DebugArgs { + #[clap(subcommand)] + command: DebugCommands, +} + +#[derive(Subcommand, Clone, Debug)] +enum DebugCommands { + Completion(DebugCompletionArgs), + Mangen(DebugMangenArgs), + #[clap(name = "resolverev")] + ResolveRev(DebugResolveRevArgs), + #[clap(name = "workingcopy")] + WorkingCopy(DebugWorkingCopyArgs), + Template(DebugTemplateArgs), + Index(DebugIndexArgs), + #[clap(name = "reindex")] + ReIndex(DebugReIndexArgs), +} + +/// Print a command-line-completion script +#[derive(clap::Args, Clone, Debug)] +struct DebugCompletionArgs { + #[clap(long)] + bash: bool, + #[clap(long)] + fish: bool, + #[clap(long)] + zsh: bool, +} + +/// Print a ROFF (manpage) +#[derive(clap::Args, Clone, Debug)] +struct DebugMangenArgs {} + +/// Resolve a revision identifier to its full ID +#[derive(clap::Args, Clone, Debug)] +struct DebugResolveRevArgs { + #[clap(long, short, default_value = "@")] + revision: String, +} + +/// Show information about the working copy state +#[derive(clap::Args, Clone, Debug)] +struct DebugWorkingCopyArgs {} + +/// Parse a template +#[derive(clap::Args, Clone, Debug)] +struct DebugTemplateArgs { + #[clap(index = 1)] + template: String, +} + +/// Show commit index stats +#[derive(clap::Args, Clone, Debug)] +struct DebugIndexArgs {} + +/// Rebuild commit index +#[derive(clap::Args, Clone, Debug)] +struct DebugReIndexArgs {} + fn short_commit_description(commit: &Commit) -> String { let first_line = commit.description().split('\n').next().unwrap(); format!("{} ({})", short_commit_hash(commit.id()), first_line) @@ -1767,14 +1701,13 @@ fn add_to_git_exclude(ui: &mut Ui, git_repo: &git2::Repository) -> Result<(), Co Ok(()) } -fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { - if command.root_args.occurrences_of("repository") > 0 { +fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &InitArgs) -> Result<(), CommandError> { + if command.args().repository.is_some() { return Err(CommandError::UserError( "'--repository' cannot be used with 'init'".to_string(), )); } - let wc_path_str = args.value_of("destination").unwrap(); - let wc_path = ui.cwd().join(wc_path_str); + let wc_path = ui.cwd().join(&args.destination); if wc_path.exists() { assert!(wc_path.is_dir()); } else { @@ -1782,7 +1715,7 @@ fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( } let wc_path = std::fs::canonicalize(&wc_path).unwrap(); - if let Some(git_store_str) = args.value_of("git-repo") { + if let Some(git_store_str) = &args.git_repo { let mut git_store_path = ui.cwd().join(git_store_str); if !git_store_path.ends_with(".git") { git_store_path = git_store_path.join(".git"); @@ -1818,7 +1751,7 @@ fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( if tx.mut_repo().has_changes() { workspace_command.finish_transaction(ui, tx)?; } - } else if args.is_present("git") { + } else if args.git { Workspace::init_internal_git(ui.settings(), wc_path.clone())?; } else { Workspace::init_local(ui.settings(), wc_path.clone())?; @@ -1832,10 +1765,10 @@ fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( fn cmd_checkout( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &CheckoutArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let new_commit = workspace_command.resolve_revision_arg(ui, args)?; + let new_commit = workspace_command.resolve_single_rev(ui, &args.revision)?; let workspace_id = workspace_command.workspace_id(); if workspace_command.repo().view().get_checkout(&workspace_id) == Some(new_commit.id()) { ui.write("Already on that commit\n")?; @@ -1853,17 +1786,13 @@ fn cmd_checkout( fn cmd_untrack( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &UntrackArgs, ) -> Result<(), CommandError> { // TODO: We should probably check that the repo was loaded at head. let mut workspace_command = command.workspace_helper(ui)?; workspace_command.maybe_commit_working_copy(ui)?; let store = workspace_command.repo().store().clone(); - let matcher = matcher_from_values( - ui, - workspace_command.workspace_root(), - args.values_of("paths"), - )?; + let matcher = matcher_from_values(ui, workspace_command.workspace_root(), &args.paths)?; let current_checkout_id = workspace_command .repo @@ -1936,14 +1865,10 @@ fn cmd_untrack( Ok(()) } -fn cmd_files(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_files(ui: &mut Ui, command: &CommandHelper, args: &FilesArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; - let matcher = matcher_from_values( - ui, - workspace_command.workspace_root(), - args.values_of("paths"), - )?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; + let matcher = matcher_from_values(ui, workspace_command.workspace_root(), &args.paths)?; for (name, _value) in commit.tree().entries_matching(matcher.as_ref()) { writeln!( ui, @@ -2050,34 +1975,33 @@ fn show_color_words_diff_line( Ok(()) } -fn cmd_diff(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_diff(ui: &mut Ui, command: &CommandHelper, args: &DiffArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let from_tree; let to_tree; - if args.is_present("from") || args.is_present("to") { - let from = - workspace_command.resolve_single_rev(ui, args.value_of("from").unwrap_or("@"))?; + if args.from.is_some() || args.to.is_some() { + let from = workspace_command.resolve_single_rev(ui, args.from.as_deref().unwrap_or("@"))?; from_tree = from.tree(); - let to = workspace_command.resolve_single_rev(ui, args.value_of("to").unwrap_or("@"))?; + let to = workspace_command.resolve_single_rev(ui, args.to.as_deref().unwrap_or("@"))?; to_tree = to.tree(); } else { let commit = - workspace_command.resolve_single_rev(ui, args.value_of("revision").unwrap_or("@"))?; + workspace_command.resolve_single_rev(ui, args.revision.as_deref().unwrap_or("@"))?; let parents = commit.parents(); from_tree = merge_commit_trees(workspace_command.repo().as_repo_ref(), &parents); to_tree = commit.tree() } let repo = workspace_command.repo(); let workspace_root = workspace_command.workspace_root(); - let matcher = matcher_from_values(ui, workspace_root, args.values_of("paths"))?; + let matcher = matcher_from_values(ui, workspace_root, &args.paths)?; let diff_iterator = from_tree.diff(&to_tree, matcher.as_ref()); - show_diff(ui, repo, workspace_root, args, diff_iterator)?; + show_diff(ui, repo, workspace_root, &args.format, diff_iterator)?; Ok(()) } -fn cmd_show(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_show(ui: &mut Ui, command: &CommandHelper, args: &ShowArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_single_rev(ui, args.value_of("revision").unwrap())?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; let parents = commit.parents(); let from_tree = merge_commit_trees(workspace_command.repo().as_repo_ref(), &parents); let to_tree = commit.tree(); @@ -2102,7 +2026,7 @@ fn cmd_show(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( template_string, ); template.format(&commit, ui.stdout_formatter().as_mut())?; - show_diff(ui, repo, workspace_root, args, diff_iterator)?; + show_diff(ui, repo, workspace_root, &args.format, diff_iterator)?; Ok(()) } @@ -2110,7 +2034,7 @@ fn show_diff( ui: &mut Ui, repo: &Arc, workspace_root: &Path, - args: &ArgMatches, + args: &DiffFormat, tree_diff: TreeDiffIterator, ) -> Result<(), CommandError> { enum Format { @@ -2119,11 +2043,11 @@ fn show_diff( ColorWords, } let format = { - if args.is_present("summary") { + if args.summary { Format::Summary - } else if args.is_present("git") { + } else if args.git { Format::Git - } else if args.is_present("color-words") { + } else if args.color_words { Format::ColorWords } else { match ui.settings().config().get_string("diff.format") { @@ -2585,7 +2509,7 @@ fn show_diff_summary( fn cmd_status( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &StatusArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; workspace_command.maybe_commit_working_copy(ui)?; @@ -2710,18 +2634,17 @@ fn log_template(settings: &UserSettings) -> String { .unwrap_or_else(|_| String::from(default_template)) } -fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let revset_expression = - workspace_command.parse_revset(ui, args.value_of("revisions").unwrap())?; + let revset_expression = workspace_command.parse_revset(ui, &args.revisions)?; let repo = workspace_command.repo(); let workspace_id = workspace_command.workspace_id(); let checkout_id = repo.view().get_checkout(&workspace_id); let revset = revset_expression.evaluate(repo.as_repo_ref(), Some(&workspace_id))?; let store = repo.store(); - let template_string = match args.value_of("template") { + let template_string = match &args.template { Some(value) => value.to_string(), None => log_template(ui.settings()), }; @@ -2735,7 +2658,7 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<() let mut formatter = formatter.as_mut(); formatter.add_label(String::from("log"))?; - if !args.is_present("no-graph") { + if !args.no_graph { let mut graph = AsciiGraphDrawer::new(&mut formatter); for (index_entry, edges) in revset.iter().graph() { let mut graphlog_edges = vec![]; @@ -2797,14 +2720,14 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<() Ok(()) } -fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let start_commit = workspace_command.resolve_revision_arg(ui, args)?; + let start_commit = workspace_command.resolve_single_rev(ui, &args.revision)?; let workspace_id = workspace_command.workspace_id(); let checkout_id = workspace_command.repo().view().get_checkout(&workspace_id); - let template_string = match args.value_of("template") { + let template_string = match &args.template { Some(value) => value.to_string(), None => log_template(ui.settings()), }; @@ -2823,7 +2746,7 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result Box::new(|commit: &Commit| commit.id().clone()), Box::new(|commit: &Commit| commit.predecessors()), ); - if !args.is_present("no-graph") { + if !args.no_graph { let mut graph = AsciiGraphDrawer::new(&mut formatter); for commit in commits { let mut edges = vec![]; @@ -2906,19 +2829,19 @@ fn edit_description(repo: &ReadonlyRepo, description: &str) -> String { fn cmd_describe( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &DescribeArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let description; - if args.is_present("stdin") { + if args.stdin { let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer).unwrap(); description = buffer; - } else if args.is_present("message") { - description = args.value_of("message").unwrap().to_owned() + } else if let Some(message) = &args.message { + description = message.to_owned() } else { description = edit_description(repo, commit.description()); } @@ -2935,9 +2858,9 @@ fn cmd_describe( Ok(()) } -fn cmd_open(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_open(ui: &mut Ui, command: &CommandHelper, args: &OpenArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let mut tx = workspace_command.start_transaction(&format!("open commit {}", commit.id().hex())); @@ -2948,18 +2871,18 @@ fn cmd_open(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( Ok(()) } -fn cmd_close(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_close(ui: &mut Ui, command: &CommandHelper, args: &CloseArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let mut commit_builder = CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &commit).set_open(false); - let description = if args.is_present("message") { - args.value_of("message").unwrap().to_string() + let description = if let Some(message) = &args.message { + message.to_string() } else if commit.description().is_empty() { edit_description(repo, "\n\nJJ: Enter commit description.\n") - } else if args.is_present("edit") { + } else if args.edit { edit_description(repo, commit.description()) } else { commit.description().to_string() @@ -2975,10 +2898,10 @@ fn cmd_close(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result< fn cmd_duplicate( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &DuplicateArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let predecessor = workspace_command.resolve_revision_arg(ui, args)?; + let predecessor = workspace_command.resolve_single_rev(ui, &args.revision)?; let repo = workspace_command.repo(); let mut tx = workspace_command .start_transaction(&format!("duplicate commit {}", predecessor.id().hex())); @@ -3000,10 +2923,10 @@ fn cmd_duplicate( fn cmd_abandon( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &AbandonArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let to_abandon = workspace_command.resolve_revset(ui, args.value_of("revision").unwrap())?; + let to_abandon = workspace_command.resolve_revset(ui, &args.revisions)?; workspace_command.check_non_empty(&to_abandon)?; for commit in &to_abandon { workspace_command.check_rewriteable(commit)?; @@ -3033,9 +2956,9 @@ fn cmd_abandon( Ok(()) } -fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let parent = workspace_command.resolve_revision_arg(ui, args)?; + let parent = workspace_command.resolve_single_rev(ui, &args.revision)?; let repo = workspace_command.repo(); let commit_builder = CommitBuilder::for_open_commit( ui.settings(), @@ -3054,10 +2977,10 @@ fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<() Ok(()) } -fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &MoveArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let source = workspace_command.resolve_single_rev(ui, args.value_of("from").unwrap())?; - let mut destination = workspace_command.resolve_single_rev(ui, args.value_of("to").unwrap())?; + let source = workspace_command.resolve_single_rev(ui, &args.from)?; + let mut destination = workspace_command.resolve_single_rev(ui, &args.to)?; if source.id() == destination.id() { return Err(CommandError::UserError(String::from( "Source and destination cannot be the same.", @@ -3074,7 +2997,7 @@ fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<( let repo = workspace_command.repo(); let parent_tree = merge_commit_trees(repo.as_repo_ref(), &source.parents()); let source_tree = source.tree(); - let new_parent_tree_id = if args.is_present("interactive") { + let new_parent_tree_id = if args.interactive { let instructions = format!( "\ You are moving changes from: {} @@ -3132,9 +3055,9 @@ from the source will be moved into the destination. Ok(()) } -fn cmd_squash(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_squash(ui: &mut Ui, command: &CommandHelper, args: &SquashArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let parents = commit.parents(); @@ -3149,7 +3072,7 @@ fn cmd_squash(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result workspace_command.start_transaction(&format!("squash commit {}", commit.id().hex())); let mut_repo = tx.mut_repo(); let new_parent_tree_id; - if args.is_present("interactive") { + if args.interactive { let instructions = format!( "\ You are moving changes from: {} @@ -3196,10 +3119,10 @@ from the source will be moved into the parent. fn cmd_unsquash( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &UnsquashArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let parents = commit.parents(); @@ -3215,7 +3138,7 @@ fn cmd_unsquash( let mut_repo = tx.mut_repo(); let parent_base_tree = merge_commit_trees(repo.as_repo_ref(), &parent.parents()); let new_parent_tree_id; - if args.is_present("interactive") { + if args.interactive { let instructions = format!( "\ You are moving changes from: {} @@ -3264,16 +3187,16 @@ aborted. fn cmd_restore( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &RestoreArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let from_commit = workspace_command.resolve_single_rev(ui, args.value_of("from").unwrap())?; - let to_commit = workspace_command.resolve_single_rev(ui, args.value_of("to").unwrap())?; + let from_commit = workspace_command.resolve_single_rev(ui, &args.from)?; + let to_commit = workspace_command.resolve_single_rev(ui, &args.to)?; workspace_command.check_rewriteable(&to_commit)?; let repo = workspace_command.repo(); let tree_id; - if args.is_present("interactive") { - if args.is_present("paths") { + if args.interactive { + if !args.paths.is_empty() { return Err(UserError( "restore with --interactive and path is not yet supported".to_string(), )); @@ -3295,12 +3218,8 @@ side. If you don't make any changes, then the operation will be aborted. ); tree_id = workspace_command.edit_diff(&from_commit.tree(), &to_commit.tree(), &instructions)?; - } else if args.is_present("paths") { - let matcher = matcher_from_values( - ui, - workspace_command.workspace_root(), - args.values_of("paths"), - )?; + } else if !args.paths.is_empty() { + let matcher = matcher_from_values(ui, workspace_command.workspace_root(), &args.paths)?; let mut tree_builder = repo.store().tree_builder(to_commit.tree().id().clone()); for (repo_path, diff) in from_commit.tree().diff(&to_commit.tree(), matcher.as_ref()) { match diff.into_options().0 { @@ -3337,9 +3256,9 @@ side. If you don't make any changes, then the operation will be aborted. Ok(()) } -fn cmd_edit(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_edit(ui: &mut Ui, command: &CommandHelper, args: &EditArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let base_tree = merge_commit_trees(repo.as_repo_ref(), &commit.parents()); @@ -3375,9 +3294,9 @@ don't make any changes, then the operation will be aborted.", Ok(()) } -fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &SplitArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?.rebase_descendants(false); - let commit = workspace_command.resolve_revision_arg(ui, args)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; workspace_command.check_rewriteable(&commit)?; let repo = workspace_command.repo(); let base_tree = merge_commit_trees(repo.as_repo_ref(), &commit.parents()); @@ -3449,9 +3368,9 @@ any changes, then the operation will be aborted. Ok(()) } -fn cmd_merge(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_merge(ui: &mut Ui, command: &CommandHelper, args: &MergeArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let revision_args = args.values_of("revisions").unwrap(); + let revision_args = &args.revisions; if revision_args.len() < 2 { return Err(CommandError::UserError(String::from( "Merge requires at least two revisions", @@ -3468,8 +3387,8 @@ fn cmd_merge(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result< commits.push(commit); } let repo = workspace_command.repo(); - let description = if args.is_present("message") { - args.value_of("message").unwrap().to_string() + let description = if let Some(message) = &args.message { + message.to_string() } else { edit_description( repo, @@ -3488,10 +3407,10 @@ fn cmd_merge(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result< Ok(()) } -fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?.rebase_descendants(false); let mut new_parents = vec![]; - for revision_str in args.values_of("destination").unwrap() { + for revision_str in &args.destination { let destination = workspace_command.resolve_single_rev(ui, revision_str)?; new_parents.push(destination); } @@ -3499,13 +3418,13 @@ fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result // replace --source by --rebase-descendants? let old_commit; let rebase_descendants; - if let Some(source_str) = args.value_of("source") { + if let Some(source_str) = &args.source { rebase_descendants = true; old_commit = workspace_command.resolve_single_rev(ui, source_str)?; } else { rebase_descendants = false; old_commit = - workspace_command.resolve_single_rev(ui, args.value_of("revision").unwrap_or("@"))?; + workspace_command.resolve_single_rev(ui, args.revision.as_deref().unwrap_or("@"))?; } workspace_command.check_rewriteable(&old_commit)?; for parent in &new_parents { @@ -3575,12 +3494,12 @@ fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result fn cmd_backout( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &BackoutArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let commit_to_back_out = workspace_command.resolve_revision_arg(ui, args)?; + let commit_to_back_out = workspace_command.resolve_single_rev(ui, &args.revision)?; let mut parents = vec![]; - for revision_str in args.values_of("destination").unwrap() { + for revision_str in &args.destination { let destination = workspace_command.resolve_single_rev(ui, revision_str)?; parents.push(destination); } @@ -3605,10 +3524,10 @@ fn is_fast_forward(repo: RepoRef, branch_name: &str, new_target_id: &CommitId) - } } -fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { +fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &BranchArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?.rebase_descendants(false); - let branch_name = args.value_of("name").unwrap(); - if args.is_present("delete") { + let branch_name = &args.name; + if args.delete { if workspace_command .repo() .view() @@ -3620,7 +3539,7 @@ fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result let mut tx = workspace_command.start_transaction(&format!("delete branch {}", branch_name)); tx.mut_repo().remove_local_branch(branch_name); workspace_command.finish_transaction(ui, tx)?; - } else if args.is_present("forget") { + } else if args.forget { if workspace_command .repo() .view() @@ -3633,8 +3552,8 @@ fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result tx.mut_repo().remove_branch(branch_name); workspace_command.finish_transaction(ui, tx)?; } else { - let target_commit = workspace_command.resolve_revision_arg(ui, args)?; - if !args.is_present("allow-backwards") + let target_commit = workspace_command.resolve_single_rev(ui, &args.revision)?; + if !args.allow_backwards && !is_fast_forward( workspace_command.repo().as_repo_ref(), branch_name, @@ -3663,7 +3582,7 @@ fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result fn cmd_branches( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &BranchesArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); @@ -3749,67 +3668,73 @@ fn cmd_branches( Ok(()) } -fn cmd_debug(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { - if let Some(completion_matches) = args.subcommand_matches("completion") { - let mut app = command.app.clone(); - let mut buf = vec![]; - let shell = if completion_matches.is_present("zsh") { - clap_complete::Shell::Zsh - } else if completion_matches.is_present("fish") { - clap_complete::Shell::Fish - } else { - clap_complete::Shell::Bash - }; - clap_complete::generate(shell, &mut app, "jj", &mut buf); - ui.stdout_formatter().write_all(&buf)?; - } else if let Some(_mangen_matches) = args.subcommand_matches("mangen") { - let mut buf = vec![]; - let man = clap_mangen::Man::new(command.app.clone()); - man.render(&mut buf)?; - ui.stdout_formatter().write_all(&buf)?; - } else if let Some(resolve_matches) = args.subcommand_matches("resolverev") { - let mut workspace_command = command.workspace_helper(ui)?; - let commit = workspace_command.resolve_revision_arg(ui, resolve_matches)?; - writeln!(ui, "{}", commit.id().hex())?; - } else if let Some(_wc_matches) = args.subcommand_matches("workingcopy") { - let workspace_command = command.workspace_helper(ui)?; - let wc = workspace_command.working_copy(); - writeln!(ui, "Current operation: {:?}", wc.operation_id())?; - writeln!(ui, "Current tree: {:?}", wc.current_tree_id())?; - for (file, state) in wc.file_states().iter() { - writeln!( - ui, - "{:?} {:13?} {:10?} {:?}", - state.file_type, state.size, state.mtime.0, file - )?; +fn cmd_debug(ui: &mut Ui, command: &CommandHelper, args: &DebugArgs) -> Result<(), CommandError> { + match &args.command { + DebugCommands::Completion(completion_matches) => { + let mut app = command.app.clone(); + let mut buf = vec![]; + let shell = if completion_matches.zsh { + clap_complete::Shell::Zsh + } else if completion_matches.fish { + clap_complete::Shell::Fish + } else { + clap_complete::Shell::Bash + }; + clap_complete::generate(shell, &mut app, "jj", &mut buf); + ui.stdout_formatter().write_all(&buf)?; } - } else if let Some(template_matches) = args.subcommand_matches("template") { - let parse = TemplateParser::parse( - crate::template_parser::Rule::template, - template_matches.value_of("template").unwrap(), - ); - writeln!(ui, "{:?}", parse)?; - } else if let Some(_reindex_matches) = args.subcommand_matches("index") { - let workspace_command = command.workspace_helper(ui)?; - let stats = workspace_command.repo().index().stats(); - writeln!(ui, "Number of commits: {}", stats.num_commits)?; - writeln!(ui, "Number of merges: {}", stats.num_merges)?; - writeln!(ui, "Max generation number: {}", stats.max_generation_number)?; - writeln!(ui, "Number of heads: {}", stats.num_heads)?; - writeln!(ui, "Number of changes: {}", stats.num_changes)?; - writeln!(ui, "Stats per level:")?; - for (i, level) in stats.levels.iter().enumerate() { - writeln!(ui, " Level {}:", i)?; - writeln!(ui, " Number of commits: {}", level.num_commits)?; - writeln!(ui, " Name: {}", level.name.as_ref().unwrap())?; + DebugCommands::Mangen(_mangen_matches) => { + let mut buf = vec![]; + let man = clap_mangen::Man::new(command.app.clone()); + man.render(&mut buf)?; + ui.stdout_formatter().write_all(&buf)?; + } + DebugCommands::ResolveRev(resolve_matches) => { + let mut workspace_command = command.workspace_helper(ui)?; + let commit = workspace_command.resolve_single_rev(ui, &resolve_matches.revision)?; + writeln!(ui, "{}", commit.id().hex())?; + } + DebugCommands::WorkingCopy(_wc_matches) => { + let workspace_command = command.workspace_helper(ui)?; + let wc = workspace_command.working_copy(); + writeln!(ui, "Current operation: {:?}", wc.operation_id())?; + writeln!(ui, "Current tree: {:?}", wc.current_tree_id())?; + for (file, state) in wc.file_states().iter() { + writeln!( + ui, + "{:?} {:13?} {:10?} {:?}", + state.file_type, state.size, state.mtime.0, file + )?; + } + } + DebugCommands::Template(template_matches) => { + let parse = TemplateParser::parse( + crate::template_parser::Rule::template, + &template_matches.template, + ); + writeln!(ui, "{:?}", parse)?; + } + DebugCommands::Index(_index_matches) => { + let workspace_command = command.workspace_helper(ui)?; + let stats = workspace_command.repo().index().stats(); + writeln!(ui, "Number of commits: {}", stats.num_commits)?; + writeln!(ui, "Number of merges: {}", stats.num_merges)?; + writeln!(ui, "Max generation number: {}", stats.max_generation_number)?; + writeln!(ui, "Number of heads: {}", stats.num_heads)?; + writeln!(ui, "Number of changes: {}", stats.num_changes)?; + writeln!(ui, "Stats per level:")?; + for (i, level) in stats.levels.iter().enumerate() { + writeln!(ui, " Level {}:", i)?; + writeln!(ui, " Number of commits: {}", level.num_commits)?; + writeln!(ui, " Name: {}", level.name.as_ref().unwrap())?; + } + } + DebugCommands::ReIndex(_reindex_matches) => { + let mut workspace_command = command.workspace_helper(ui)?; + let mut_repo = Arc::get_mut(workspace_command.repo_mut()).unwrap(); + let index = mut_repo.reindex(); + writeln!(ui, "Finished indexing {:?} commits.", index.num_commits())?; } - } else if let Some(_reindex_matches) = args.subcommand_matches("reindex") { - let mut workspace_command = command.workspace_helper(ui)?; - let mut_repo = Arc::get_mut(workspace_command.repo_mut()).unwrap(); - let index = mut_repo.reindex(); - writeln!(ui, "Finished indexing {:?} commits.", index.num_commits())?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); } Ok(()) } @@ -3835,63 +3760,73 @@ where Ok(()) } -fn cmd_bench(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { - if let Some(command_matches) = args.subcommand_matches("commonancestors") { - let mut workspace_command = command.workspace_helper(ui)?; - let revision1_str = command_matches.value_of("revision1").unwrap(); - let commit1 = workspace_command.resolve_single_rev(ui, revision1_str)?; - let revision2_str = command_matches.value_of("revision2").unwrap(); - let commit2 = workspace_command.resolve_single_rev(ui, revision2_str)?; - let index = workspace_command.repo().index(); - let routine = || index.common_ancestors(&[commit1.id().clone()], &[commit2.id().clone()]); - run_bench( - ui, - &format!("commonancestors-{}-{}", revision1_str, revision2_str), - routine, - )?; - } else if let Some(command_matches) = args.subcommand_matches("isancestor") { - let mut workspace_command = command.workspace_helper(ui)?; - let ancestor_str = command_matches.value_of("ancestor").unwrap(); - let ancestor_commit = workspace_command.resolve_single_rev(ui, ancestor_str)?; - let descendants_str = command_matches.value_of("descendant").unwrap(); - let descendant_commit = workspace_command.resolve_single_rev(ui, descendants_str)?; - let index = workspace_command.repo().index(); - let routine = || index.is_ancestor(ancestor_commit.id(), descendant_commit.id()); - run_bench( - ui, - &format!("isancestor-{}-{}", ancestor_str, descendants_str), - routine, - )?; - } else if let Some(command_matches) = args.subcommand_matches("walkrevs") { - let mut workspace_command = command.workspace_helper(ui)?; - let unwanted_str = command_matches.value_of("unwanted").unwrap(); - let unwanted_commit = workspace_command.resolve_single_rev(ui, unwanted_str)?; - let wanted_str = command_matches.value_of("wanted"); - let wanted_commit = workspace_command.resolve_single_rev(ui, wanted_str.unwrap())?; - let index = workspace_command.repo().index(); - let routine = || { - index - .walk_revs( - &[wanted_commit.id().clone()], - &[unwanted_commit.id().clone()], - ) - .count() - }; - run_bench( - ui, - &format!("walkrevs-{}-{}", unwanted_str, wanted_str.unwrap()), - routine, - )?; - } else if let Some(command_matches) = args.subcommand_matches("resolveprefix") { - let workspace_command = command.workspace_helper(ui)?; - let prefix = - HexPrefix::new(command_matches.value_of("prefix").unwrap().to_string()).unwrap(); - let index = workspace_command.repo().index(); - let routine = || index.resolve_prefix(&prefix); - run_bench(ui, &format!("resolveprefix-{}", prefix.hex()), routine)?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); - }; +fn cmd_bench(ui: &mut Ui, command: &CommandHelper, args: &BenchArgs) -> Result<(), CommandError> { + match &args.command { + BenchCommands::CommonAncestors(command_matches) => { + let mut workspace_command = command.workspace_helper(ui)?; + let commit1 = workspace_command.resolve_single_rev(ui, &command_matches.revision1)?; + let commit2 = workspace_command.resolve_single_rev(ui, &command_matches.revision2)?; + let index = workspace_command.repo().index(); + let routine = + || index.common_ancestors(&[commit1.id().clone()], &[commit2.id().clone()]); + run_bench( + ui, + &format!( + "commonancestors-{}-{}", + &command_matches.revision1, &command_matches.revision2 + ), + routine, + )?; + } + BenchCommands::IsAncestor(command_matches) => { + let mut workspace_command = command.workspace_helper(ui)?; + let ancestor_commit = + workspace_command.resolve_single_rev(ui, &command_matches.ancestor)?; + let descendant_commit = + workspace_command.resolve_single_rev(ui, &command_matches.descendant)?; + let index = workspace_command.repo().index(); + let routine = || index.is_ancestor(ancestor_commit.id(), descendant_commit.id()); + run_bench( + ui, + &format!( + "isancestor-{}-{}", + &command_matches.ancestor, &command_matches.descendant + ), + routine, + )?; + } + BenchCommands::WalkRevs(command_matches) => { + let mut workspace_command = command.workspace_helper(ui)?; + let unwanted_commit = + workspace_command.resolve_single_rev(ui, &command_matches.unwanted)?; + let wanted_commit = + workspace_command.resolve_single_rev(ui, &command_matches.wanted)?; + let index = workspace_command.repo().index(); + let routine = || { + index + .walk_revs( + &[wanted_commit.id().clone()], + &[unwanted_commit.id().clone()], + ) + .count() + }; + run_bench( + ui, + &format!( + "walkrevs-{}-{}", + &command_matches.unwanted, &command_matches.wanted + ), + routine, + )?; + } + BenchCommands::ResolvePrefix(command_matches) => { + let workspace_command = command.workspace_helper(ui)?; + let prefix = HexPrefix::new(command_matches.prefix.clone()).unwrap(); + let index = workspace_command.repo().index(); + let routine = || index.resolve_prefix(&prefix); + run_bench(ui, &format!("resolveprefix-{}", prefix.hex()), routine)?; + } + } Ok(()) } @@ -3908,7 +3843,7 @@ fn format_timestamp(timestamp: &Timestamp) -> String { fn cmd_op_log( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &OperationLogArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); @@ -3988,11 +3923,11 @@ fn cmd_op_log( fn cmd_op_undo( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &OperationUndoArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); - let bad_op = resolve_single_op(repo, args.value_of("operation").unwrap())?; + let bad_op = resolve_single_op(repo, &args.operation)?; let parent_ops = bad_op.parents(); if parent_ops.len() > 1 { return Err(CommandError::UserError( @@ -4014,14 +3949,15 @@ fn cmd_op_undo( Ok(()) } + fn cmd_op_restore( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &OperationRestoreArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); - let target_op = resolve_single_op(repo, args.value_of("operation").unwrap())?; + let target_op = resolve_single_op(repo, &args.operation)?; let mut tx = workspace_command .start_transaction(&format!("restore to operation {}", target_op.id().hex())); tx.mut_repo().set_view(target_op.view().take_store_view()); @@ -4033,37 +3969,37 @@ fn cmd_op_restore( fn cmd_operation( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &OperationArgs, ) -> Result<(), CommandError> { - if let Some(command_matches) = args.subcommand_matches("log") { - cmd_op_log(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("undo") { - cmd_op_undo(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("restore") { - cmd_op_restore(ui, command, command_matches)?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); + match &args.command { + OperationCommands::Log(command_matches) => { + cmd_op_log(ui, command, command_matches)?; + } + OperationCommands::Restore(command_matches) => { + cmd_op_restore(ui, command, command_matches)?; + } + OperationCommands::Undo(command_matches) => { + cmd_op_undo(ui, command, command_matches)?; + } } Ok(()) } -fn cmd_undo(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { - cmd_op_undo(ui, command, args) -} - fn cmd_workspace( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &WorkspaceArgs, ) -> Result<(), CommandError> { - if let Some(command_matches) = args.subcommand_matches("add") { - cmd_workspace_add(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("forget") { - cmd_workspace_forget(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("list") { - cmd_workspace_list(ui, command, command_matches)?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); + match &args.command { + WorkspaceCommands::Add(command_matches) => { + cmd_workspace_add(ui, command, command_matches)?; + } + WorkspaceCommands::Forget(command_matches) => { + cmd_workspace_forget(ui, command, command_matches)?; + } + WorkspaceCommands::List(command_matches) => { + cmd_workspace_list(ui, command, command_matches)?; + } } Ok(()) } @@ -4071,11 +4007,10 @@ fn cmd_workspace( fn cmd_workspace_add( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &WorkspaceAddArgs, ) -> Result<(), CommandError> { let old_workspace_command = command.workspace_helper(ui)?; - let destination_str = args.value_of("destination").unwrap(); - let destination_path = ui.cwd().join(destination_str); + let destination_path = ui.cwd().join(&args.destination); if destination_path.exists() { return Err(CommandError::UserError( "Workspace already exists".to_string(), @@ -4083,7 +4018,7 @@ fn cmd_workspace_add( } else { fs::create_dir(&destination_path).unwrap(); } - let name = if let Some(name) = args.value_of("name") { + let name = if let Some(name) = &args.name { name.to_string() } else { destination_path @@ -4117,7 +4052,7 @@ fn cmd_workspace_add( ui, new_workspace, command.string_args.clone(), - &command.root_args, + command.args(), repo, )?; let mut tx = new_workspace_command @@ -4151,11 +4086,11 @@ fn cmd_workspace_add( fn cmd_workspace_forget( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &WorkspaceForgetArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let workspace_id = if let Some(workspace_str) = args.value_of("workspace") { + let workspace_id = if let Some(workspace_str) = &args.workspace { WorkspaceId::new(workspace_str.to_string()) } else { workspace_command.workspace_id() @@ -4179,7 +4114,7 @@ fn cmd_workspace_forget( fn cmd_workspace_list( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &WorkspaceListArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); @@ -4204,14 +4139,15 @@ fn get_git_repo(store: &Store) -> Result { fn cmd_git_remote( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitRemoteArgs, ) -> Result<(), CommandError> { - if let Some(command_matches) = args.subcommand_matches("add") { - cmd_git_remote_add(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("remove") { - cmd_git_remote_remove(ui, command, command_matches)?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); + match &args.command { + GitRemoteCommands::Add(command_matches) => { + cmd_git_remote_add(ui, command, command_matches)?; + } + GitRemoteCommands::Remove(command_matches) => { + cmd_git_remote_remove(ui, command, command_matches)?; + } } Ok(()) } @@ -4219,18 +4155,16 @@ fn cmd_git_remote( fn cmd_git_remote_add( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitRemoteAddArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - let remote_name = args.value_of("remote").unwrap(); - let url = args.value_of("url").unwrap(); - if git_repo.find_remote(remote_name).is_ok() { + if git_repo.find_remote(&args.remote).is_ok() { return Err(CommandError::UserError("Remote already exists".to_string())); } git_repo - .remote(remote_name, url) + .remote(&args.remote, &args.url) .map_err(|err| CommandError::UserError(err.to_string()))?; Ok(()) } @@ -4238,29 +4172,28 @@ fn cmd_git_remote_add( fn cmd_git_remote_remove( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitRemoteRemoveArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - let remote_name = args.value_of("remote").unwrap(); - if git_repo.find_remote(remote_name).is_err() { + if git_repo.find_remote(&args.remote).is_err() { return Err(CommandError::UserError("Remote doesn't exists".to_string())); } git_repo - .remote_delete(remote_name) + .remote_delete(&args.remote) .map_err(|err| CommandError::UserError(err.to_string()))?; let mut branches_to_delete = vec![]; for (branch, target) in repo.view().branches() { - if target.remote_targets.contains_key(remote_name) { + if target.remote_targets.contains_key(&args.remote) { branches_to_delete.push(branch.clone()); } } if !branches_to_delete.is_empty() { let mut tx = - workspace_command.start_transaction(&format!("remove git remote {}", remote_name)); + workspace_command.start_transaction(&format!("remove git remote {}", &args.remote)); for branch in branches_to_delete { - tx.mut_repo().remove_remote_branch(&branch, remote_name); + tx.mut_repo().remove_remote_branch(&branch, &args.remote); } workspace_command.finish_transaction(ui, tx)?; } @@ -4270,15 +4203,14 @@ fn cmd_git_remote_remove( fn cmd_git_fetch( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitFetchArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - let remote_name = args.value_of("remote").unwrap(); let mut tx = - workspace_command.start_transaction(&format!("fetch from git remote {}", remote_name)); - git::fetch(tx.mut_repo(), &git_repo, remote_name) + workspace_command.start_transaction(&format!("fetch from git remote {}", &args.remote)); + git::fetch(tx.mut_repo(), &git_repo, &args.remote) .map_err(|err| CommandError::UserError(err.to_string()))?; workspace_command.finish_transaction(ui, tx)?; Ok(()) @@ -4295,16 +4227,17 @@ fn clone_destination_for_source(source: &str) -> Option<&str> { fn cmd_git_clone( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitCloneArgs, ) -> Result<(), CommandError> { - if command.root_args.occurrences_of("repository") > 0 { + if command.args().repository.is_some() { return Err(CommandError::UserError( "'--repository' cannot be used with 'git clone'".to_string(), )); } - let source = args.value_of("source").unwrap(); + let source = &args.source; let wc_path_str = args - .value_of("destination") + .destination + .as_deref() .or_else(|| clone_destination_for_source(source)) .ok_or_else(|| { CommandError::UserError( @@ -4353,14 +4286,13 @@ fn cmd_git_clone( fn cmd_git_push( ui: &mut Ui, command: &CommandHelper, - args: &ArgMatches, + args: &GitPushArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); - let remote_name = args.value_of("remote").unwrap(); let mut branch_updates = HashMap::new(); - if let Some(branch_name) = args.value_of("branch") { + if let Some(branch_name) = &args.branch { let maybe_branch_target = repo.view().get_branch(branch_name); if maybe_branch_target.is_none() { return Err(CommandError::UserError(format!( @@ -4369,14 +4301,14 @@ fn cmd_git_push( ))); } let branch_target = maybe_branch_target.unwrap(); - let push_action = classify_branch_push_action(branch_target, remote_name); + let push_action = classify_branch_push_action(branch_target, &args.remote); match push_action { BranchPushAction::AlreadyMatches => { writeln!( ui, "Branch {}@{} already matches {}", - branch_name, remote_name, branch_name + branch_name, &args.remote, branch_name )?; return Ok(()); } @@ -4389,7 +4321,7 @@ fn cmd_git_push( BranchPushAction::RemoteConflicted => { return Err(CommandError::UserError(format!( "Branch {}@{} is conflicted", - branch_name, remote_name + branch_name, &args.remote ))); } BranchPushAction::Update(update) => { @@ -4407,7 +4339,7 @@ fn cmd_git_push( } else { // TODO: Is it useful to warn about conflicted branches? for (branch_name, branch_target) in repo.view().branches() { - let push_action = classify_branch_push_action(branch_target, remote_name); + let push_action = classify_branch_push_action(branch_target, &args.remote); match push_action { BranchPushAction::AlreadyMatches => {} BranchPushAction::LocalConflicted => {} @@ -4464,7 +4396,7 @@ fn cmd_git_push( // already been pushed. let mut old_heads = vec![]; for branch_target in repo.view().branches().values() { - if let Some(old_head) = branch_target.remote_targets.get(remote_name) { + if let Some(old_head) = branch_target.remote_targets.get(&args.remote) { old_heads.extend(old_head.adds()); } } @@ -4479,7 +4411,7 @@ fn cmd_git_push( } let git_repo = get_git_repo(repo.store())?; - git::push_updates(&git_repo, remote_name, &ref_updates) + git::push_updates(&git_repo, &args.remote, &ref_updates) .map_err(|err| CommandError::UserError(err.to_string()))?; let mut tx = workspace_command.start_transaction("import git refs"); git::import_refs(tx.mut_repo(), &git_repo)?; @@ -4490,7 +4422,7 @@ fn cmd_git_push( fn cmd_git_import( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &GitImportArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); @@ -4504,7 +4436,7 @@ fn cmd_git_import( fn cmd_git_export( ui: &mut Ui, command: &CommandHelper, - _args: &ArgMatches, + _args: &GitExportArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); @@ -4513,21 +4445,26 @@ fn cmd_git_export( Ok(()) } -fn cmd_git(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> { - if let Some(command_matches) = args.subcommand_matches("remote") { - cmd_git_remote(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("fetch") { - cmd_git_fetch(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("clone") { - cmd_git_clone(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("push") { - cmd_git_push(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("import") { - cmd_git_import(ui, command, command_matches)?; - } else if let Some(command_matches) = args.subcommand_matches("export") { - cmd_git_export(ui, command, command_matches)?; - } else { - panic!("unhandled command: {:#?}", command.root_args()); +fn cmd_git(ui: &mut Ui, command: &CommandHelper, args: &GitArgs) -> Result<(), CommandError> { + match &args.command { + GitCommands::Fetch(command_matches) => { + cmd_git_fetch(ui, command, command_matches)?; + } + GitCommands::Clone(command_matches) => { + cmd_git_clone(ui, command, command_matches)?; + } + GitCommands::Remote(command_matches) => { + cmd_git_remote(ui, command, command_matches)?; + } + GitCommands::Push(command_matches) => { + cmd_git_push(ui, command, command_matches)?; + } + GitCommands::Import(command_matches) => { + cmd_git_import(ui, command, command_matches)?; + } + GitCommands::Export(command_matches) => { + cmd_git_export(ui, command, command_matches)?; + } } Ok(()) } @@ -4576,77 +4513,46 @@ where return 1; } } + let string_args = resolve_alias(&mut ui, string_args); - let app = get_app(); - let matches = app.clone().get_matches_from(&string_args); - let command_helper = CommandHelper::new(app, string_args, matches.clone()); - let result = if let Some(sub_args) = command_helper.root_args.subcommand_matches("init") { - cmd_init(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("checkout") { - cmd_checkout(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("untrack") { - cmd_untrack(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("files") { - cmd_files(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("diff") { - cmd_diff(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("show") { - cmd_show(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("status") { - cmd_status(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("log") { - cmd_log(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("obslog") { - cmd_obslog(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("describe") { - cmd_describe(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("close") { - cmd_close(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("open") { - cmd_open(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("duplicate") { - cmd_duplicate(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("abandon") { - cmd_abandon(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("new") { - cmd_new(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("move") { - cmd_move(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("squash") { - cmd_squash(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("unsquash") { - cmd_unsquash(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("restore") { - cmd_restore(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("edit") { - cmd_edit(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("split") { - cmd_split(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("merge") { - cmd_merge(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("rebase") { - cmd_rebase(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("backout") { - cmd_backout(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("branch") { - cmd_branch(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("branches") { - cmd_branches(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("operation") { - cmd_operation(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("undo") { - cmd_undo(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("workspace") { - cmd_workspace(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("git") { - cmd_git(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("bench") { - cmd_bench(&mut ui, &command_helper, sub_args) - } else if let Some(sub_args) = matches.subcommand_matches("debug") { - cmd_debug(&mut ui, &command_helper, sub_args) - } else { - panic!("unhandled command: {:#?}", matches); + let app = Args::command(); + let args: Args = clap::Parser::parse_from(&string_args); + let command_helper = CommandHelper::new(app, string_args, args.clone()); + let result = match &args.command { + Commands::Init(sub_args) => cmd_init(&mut ui, &command_helper, sub_args), + Commands::Checkout(sub_args) => cmd_checkout(&mut ui, &command_helper, sub_args), + Commands::Untrack(sub_args) => cmd_untrack(&mut ui, &command_helper, sub_args), + Commands::Files(sub_args) => cmd_files(&mut ui, &command_helper, sub_args), + Commands::Diff(sub_args) => cmd_diff(&mut ui, &command_helper, sub_args), + Commands::Show(sub_args) => cmd_show(&mut ui, &command_helper, sub_args), + Commands::Status(sub_args) => cmd_status(&mut ui, &command_helper, sub_args), + Commands::Log(sub_args) => cmd_log(&mut ui, &command_helper, sub_args), + Commands::Obslog(sub_args) => cmd_obslog(&mut ui, &command_helper, sub_args), + Commands::Describe(sub_args) => cmd_describe(&mut ui, &command_helper, sub_args), + Commands::Close(sub_args) => cmd_close(&mut ui, &command_helper, sub_args), + Commands::Open(sub_args) => cmd_open(&mut ui, &command_helper, sub_args), + Commands::Duplicate(sub_args) => cmd_duplicate(&mut ui, &command_helper, sub_args), + Commands::Abandon(sub_args) => cmd_abandon(&mut ui, &command_helper, sub_args), + Commands::New(sub_args) => cmd_new(&mut ui, &command_helper, sub_args), + Commands::Move(sub_args) => cmd_move(&mut ui, &command_helper, sub_args), + Commands::Squash(sub_args) => cmd_squash(&mut ui, &command_helper, sub_args), + Commands::Unsquash(sub_args) => cmd_unsquash(&mut ui, &command_helper, sub_args), + Commands::Restore(sub_args) => cmd_restore(&mut ui, &command_helper, sub_args), + Commands::Edit(sub_args) => cmd_edit(&mut ui, &command_helper, sub_args), + Commands::Split(sub_args) => cmd_split(&mut ui, &command_helper, sub_args), + Commands::Merge(sub_args) => cmd_merge(&mut ui, &command_helper, sub_args), + Commands::Rebase(sub_args) => cmd_rebase(&mut ui, &command_helper, sub_args), + Commands::Backout(sub_args) => cmd_backout(&mut ui, &command_helper, sub_args), + Commands::Branch(sub_args) => cmd_branch(&mut ui, &command_helper, sub_args), + Commands::Branches(sub_args) => cmd_branches(&mut ui, &command_helper, sub_args), + Commands::Undo(sub_args) => cmd_op_undo(&mut ui, &command_helper, sub_args), + Commands::Operation(sub_args) => cmd_operation(&mut ui, &command_helper, sub_args), + Commands::Workspace(sub_args) => cmd_workspace(&mut ui, &command_helper, sub_args), + Commands::Git(sub_args) => cmd_git(&mut ui, &command_helper, sub_args), + Commands::Bench(sub_args) => cmd_bench(&mut ui, &command_helper, sub_args), + Commands::Debug(sub_args) => cmd_debug(&mut ui, &command_helper, sub_args), }; + match result { Ok(()) => 0, Err(CommandError::UserError(message)) => { @@ -4661,3 +4567,13 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_app() { + Args::command(); + } +}