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(); + } +}