diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c796b5a98f..d2a663950aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). tags. These keywords may be useful in non-colocated Git repositories where local and exported `@git` tags can point to different revisions. +* `jj amend` moves diffs between revisions, (similar to squash) + ### Fixed bugs * `jj metaedit --author-timestamp` twice with the same value no longer diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 1b03b6dbb5e..95f1aac2da0 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -108,6 +108,7 @@ enum Command { Describe(describe::DescribeArgs), Diff(diff::DiffArgs), Diffedit(diffedit::DiffeditArgs), + Amend(squash::AmendArgs), Duplicate(duplicate::DuplicateArgs), Edit(edit::EditArgs), #[command(alias = "obslog", visible_alias = "evolution-log")] @@ -180,6 +181,11 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Describe(args) => describe::cmd_describe(ui, command_helper, args), Command::Diff(args) => diff::cmd_diff(ui, command_helper, args), Command::Diffedit(args) => diffedit::cmd_diffedit(ui, command_helper, args), + Command::Amend(args) => squash::cmd_squash_or_amend( + ui, + command_helper, + squash::SquashOrAmendArgs::AmendArgs(args), + ), Command::Duplicate(args) => duplicate::cmd_duplicate(ui, command_helper, args), Command::Edit(args) => edit::cmd_edit(ui, command_helper, args), Command::Evolog(args) => evolog::cmd_evolog(ui, command_helper, args), @@ -212,7 +218,11 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Sign(args) => sign::cmd_sign(ui, command_helper, args), Command::Sparse(args) => sparse::cmd_sparse(ui, command_helper, args), Command::Split(args) => split::cmd_split(ui, command_helper, args), - Command::Squash(args) => squash::cmd_squash(ui, command_helper, args), + Command::Squash(args) => squash::cmd_squash_or_amend( + ui, + command_helper, + squash::SquashOrAmendArgs::SquashArgs(args), + ), Command::Status(args) => status::cmd_status(ui, command_helper, args), Command::Tag(args) => tag::cmd_tag(ui, command_helper, args), Command::Undo(args) => undo::cmd_undo(ui, command_helper, args), diff --git a/cli/src/commands/squash.rs b/cli/src/commands/squash.rs index e6011a288b7..aae57019321 100644 --- a/cli/src/commands/squash.rs +++ b/cli/src/commands/squash.rs @@ -47,7 +47,11 @@ use crate::description_util::join_message_paragraphs; use crate::description_util::try_combine_messages; use crate::ui::Ui; -/// Move changes from a revision into another revision +/// Combine revisions by moving changes from a revision into another revision. +/// +/// Note: If you aren't trying to merge the revision metadata +/// (eg. description, bookmarks), or if you want to do a partial squash, +/// you probably want `jj amend`. /// /// With the `-r` option, moves the changes from the specified revision to the /// parent revision. Fails if there are several parent revisions (i.e., the @@ -59,8 +63,8 @@ use crate::ui::Ui; /// commit to the grandparent. /// /// If, after moving changes out, the source revision is empty compared to its -/// parent(s), and `--keep-emptied` is not set, it will be abandoned. Without -/// `--interactive` or paths, the source revision will always be empty. +/// parent(s), it will be abandoned. Without `--interactive` or paths, the +/// source revision will always be empty. /// /// If the source was abandoned and both the source and destination had a /// non-empty description, you will be asked for the combined description. If @@ -76,34 +80,17 @@ use crate::ui::Ui; /// (if no `--from` is specified, `--from @` is assumed). #[derive(clap::Args, Clone, Debug)] pub(crate) struct SquashArgs { - /// Revision to squash into its parent (default: @). Incompatible with the - /// experimental `-d`/`-A`/`-B` options. - #[arg( - long, - short, - value_name = "REVSET", - add = ArgValueCompleter::new(complete::revset_expression_mutable), - )] - revision: Option, + #[clap(flatten)] + pub(crate) common: CommonArgs, - /// Revision(s) to squash from (default: @) - #[arg( - long, short, - conflicts_with = "revision", - value_name = "REVSETS", - add = ArgValueCompleter::new(complete::revset_expression_mutable), - )] - from: Vec, + /// The description to use for squashed revision (don't open editor) + #[arg(long = "message", short, value_name = "MESSAGE")] + pub(crate) message_paragraphs: Vec, - /// Revision to squash into (default: @) - #[arg( - long, short = 't', - conflicts_with = "revision", - visible_alias = "to", - value_name = "REVSET", - add = ArgValueCompleter::new(complete::revset_expression_mutable), - )] - into: Option, + /// Use the description of the destination revision and discard the + /// description(s) of the source revision(s) + #[arg(long, short, conflicts_with = "message_paragraphs")] + pub(crate) use_destination_message: bool, /// (Experimental) The revision(s) to use as parent for the new commit (can /// be repeated to create a merge commit) @@ -145,14 +132,67 @@ pub(crate) struct SquashArgs { )] insert_before: Option>, - /// The description to use for squashed revision (don't open editor) + /// The source revision will not be abandoned + #[arg(long, short)] + keep_emptied: bool, +} + +/// Move changes from a revision into another revision +/// +/// With the `-r` option, moves the changes from the specified revision to the +/// parent revision. Fails if there are several parent revisions (i.e., the +/// given revision is a merge). +/// +/// With the `--from` and/or `--into` options, moves changes from/to the given +/// revisions. If either is left out, it defaults to the working-copy commit. +/// For example, `jj squash --into @--` moves changes from the working-copy +/// commit to the grandparent. +/// +/// EXPERIMENTAL FEATURES +/// +/// An alternative squashing UI is available via the `-d`, `-A`, and `-B` +/// options. They can be used together with one or more `--from` options +/// (if no `--from` is specified, `--from @` is assumed). +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct AmendArgs { + #[clap(flatten)] + common: CommonArgs, + + /// The description to overwrite for the destination revision. #[arg(long = "message", short, value_name = "MESSAGE")] - message_paragraphs: Vec, + pub(crate) message_paragraphs: Vec, +} - /// Use the description of the destination revision and discard the - /// description(s) of the source revision(s) - #[arg(long, short, conflicts_with = "message_paragraphs")] - use_destination_message: bool, +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct CommonArgs { + /// Revision to squash into its parent (default: @). Incompatible with the + /// experimental `-d`/`-A`/`-B` options. + #[arg( + long, + short, + value_name = "REVSET", + add = ArgValueCompleter::new(complete::revset_expression_mutable), + )] + revision: Option, + + /// Revision(s) to squash from (default: @) + #[arg( + long, short, + conflicts_with = "revision", + value_name = "REVSETS", + add = ArgValueCompleter::new(complete::revset_expression_mutable), + )] + from: Vec, + + /// Revision to squash into (default: @) + #[arg( + long, short = 't', + conflicts_with = "revision", + visible_alias = "to", + value_name = "REVSET", + add = ArgValueCompleter::new(complete::revset_expression_mutable), + )] + into: Option, /// Interactively choose which parts to squash #[arg(long, short)] @@ -173,20 +213,59 @@ pub(crate) struct SquashArgs { add = ArgValueCompleter::new(complete::squash_revision_files), )] paths: Vec, +} - /// The source revision will not be abandoned - #[arg(long, short)] - keep_emptied: bool, +pub(crate) enum SquashOrAmendArgs<'a> { + SquashArgs(&'a SquashArgs), + AmendArgs(&'a AmendArgs), } #[instrument(skip_all)] -pub(crate) fn cmd_squash( +pub(crate) fn cmd_squash_or_amend( ui: &mut Ui, command: &CommandHelper, - args: &SquashArgs, + args: SquashOrAmendArgs, ) -> Result<(), CommandError> { + let keep_emptied = match args { + SquashOrAmendArgs::SquashArgs(args) => { + if args.keep_emptied { + writeln!( + ui.warning_default(), + "`jj squash --keep-emptied` has been deprecated and replaced with `jj amend`", + )?; + } + args.keep_emptied + } + SquashOrAmendArgs::AmendArgs(_) => true, + }; + let message_paragraphs = match args { + SquashOrAmendArgs::SquashArgs(args) => &args.message_paragraphs, + SquashOrAmendArgs::AmendArgs(args) => &args.message_paragraphs, + }; + let use_destination_message = match args { + SquashOrAmendArgs::SquashArgs(args) => args.use_destination_message, + SquashOrAmendArgs::AmendArgs(_) => Default::default(), + }; + let insert_before = match args { + SquashOrAmendArgs::SquashArgs(args) => &args.insert_before, + SquashOrAmendArgs::AmendArgs(_) => &None, + }; + let insert_after = match args { + SquashOrAmendArgs::SquashArgs(args) => &args.insert_after, + SquashOrAmendArgs::AmendArgs(_) => &None, + }; + let destination = match args { + SquashOrAmendArgs::SquashArgs(args) => &args.destination, + SquashOrAmendArgs::AmendArgs(_) => &None, + }; + + let args = match args { + SquashOrAmendArgs::SquashArgs(args) => &args.common, + SquashOrAmendArgs::AmendArgs(args) => &args.common, + }; + let insert_destination_commit = - args.destination.is_some() || args.insert_after.is_some() || args.insert_before.is_some(); + destination.is_some() || insert_after.is_some() || insert_before.is_some(); let mut workspace_command = command.workspace_helper(ui)?; @@ -253,9 +332,9 @@ pub(crate) fn cmd_squash( let (parent_ids, child_ids) = compute_commit_location( ui, tx.base_workspace_helper(), - args.destination.as_deref(), - args.insert_after.as_deref(), - args.insert_before.as_deref(), + destination.as_deref(), + insert_after.as_deref(), + insert_before.as_deref(), "squashed commit", )?; let parent_commits: Vec<_> = parent_ids @@ -310,15 +389,12 @@ pub(crate) fn cmd_squash( tx.base_workspace_helper() .diff_selector(ui, args.tool.as_deref(), args.interactive)?; let text_editor = tx.base_workspace_helper().text_editor()?; - let description = SquashedDescription::from_args(args); + let description = SquashedDescription::from_args(message_paragraphs, use_destination_message); let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?; - if let Some(squashed) = rewrite::squash_commits( - tx.repo_mut(), - &source_commits, - &destination, - args.keep_emptied, - )? { + if let Some(squashed) = + rewrite::squash_commits(tx.repo_mut(), &source_commits, &destination, keep_emptied)? + { let mut commit_builder = squashed.commit_builder.detach(); let new_description = match description { SquashedDescription::Exact(description) => { @@ -434,14 +510,14 @@ enum SquashedDescription { } impl SquashedDescription { - fn from_args(args: &SquashArgs) -> Self { + fn from_args(message_paragraphs: &[String], use_destination_message: bool) -> Self { // These options are incompatible and Clap is configured to prevent this. - assert!(args.message_paragraphs.is_empty() || !args.use_destination_message); + assert!(message_paragraphs.is_empty() || !use_destination_message); - if !args.message_paragraphs.is_empty() { - let desc = join_message_paragraphs(&args.message_paragraphs); + if !message_paragraphs.is_empty() { + let desc = join_message_paragraphs(message_paragraphs); Self::Exact(desc) - } else if args.use_destination_message { + } else if use_destination_message { Self::UseDestination } else { Self::Combine diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index d85c583d0b8..334a7e0946a 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -36,6 +36,7 @@ This document contains the help content for the `jj` command-line program. * [`jj describe`↴](#jj-describe) * [`jj diff`↴](#jj-diff) * [`jj diffedit`↴](#jj-diffedit) +* [`jj amend`↴](#jj-amend) * [`jj duplicate`↴](#jj-duplicate) * [`jj edit`↴](#jj-edit) * [`jj evolog`↴](#jj-evolog) @@ -141,6 +142,7 @@ To get started, see the tutorial [`jj help -k tutorial`]. * `describe` — Update the change description or other metadata [default alias: desc] * `diff` — Compare file contents between two revisions * `diffedit` — Touch up the content changes in a revision with a diff editor +* `amend` — Move changes from a revision into another revision * `duplicate` — Create new changes with the same content as existing ones * `edit` — Sets the specified revision as the working-copy revision * `evolog` — Show how a change has evolved over time @@ -168,7 +170,7 @@ To get started, see the tutorial [`jj help -k tutorial`]. * `simplify-parents` — Simplify parent edges for the specified revision(s) * `sparse` — Manage which paths from the working-copy commit are present in the working copy * `split` — Split a revision in two -* `squash` — Move changes from a revision into another revision +* `squash` — Combine revisions by moving changes from a revision into another revision * `status` — Show high-level repo status [default alias: st] * `tag` — Manage tags * `undo` — Undo the last operation @@ -909,6 +911,35 @@ See `jj restore` if you want to move entire files from one revision to another. +## `jj amend` + +Move changes from a revision into another revision + +With the `-r` option, moves the changes from the specified revision to the parent revision. Fails if there are several parent revisions (i.e., the given revision is a merge). + +With the `--from` and/or `--into` options, moves changes from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, `jj squash --into @--` moves changes from the working-copy commit to the grandparent. + +EXPERIMENTAL FEATURES + +An alternative squashing UI is available via the `-d`, `-A`, and `-B` options. They can be used together with one or more `--from` options (if no `--from` is specified, `--from @` is assumed). + +**Usage:** `jj amend [OPTIONS] [FILESETS]...` + +###### **Arguments:** + +* `` — Move only changes to these paths (instead of all paths) + +###### **Options:** + +* `-r`, `--revision ` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options +* `-f`, `--from ` — Revision(s) to squash from (default: @) +* `-t`, `--into ` [alias: `to`] — Revision to squash into (default: @) +* `-i`, `--interactive` — Interactively choose which parts to squash +* `--tool ` — Specify diff editor to be used (implies --interactive) +* `-m`, `--message ` — The description to overwrite for the destination revision + + + ## `jj duplicate` Create new changes with the same content as existing ones @@ -2715,13 +2746,15 @@ Splitting an empty commit is not supported because the same effect can be achiev ## `jj squash` -Move changes from a revision into another revision +Combine revisions by moving changes from a revision into another revision. + +Note: If you aren't trying to merge the revision metadata (eg. description, bookmarks), or if you want to do a partial squash, you probably want `jj amend`. With the `-r` option, moves the changes from the specified revision to the parent revision. Fails if there are several parent revisions (i.e., the given revision is a merge). With the `--from` and/or `--into` options, moves changes from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, `jj squash --into @--` moves changes from the working-copy commit to the grandparent. -If, after moving changes out, the source revision is empty compared to its parent(s), and `--keep-emptied` is not set, it will be abandoned. Without `--interactive` or paths, the source revision will always be empty. +If, after moving changes out, the source revision is empty compared to its parent(s), it will be abandoned. Without `--interactive` or paths, the source revision will always be empty. If the source was abandoned and both the source and destination had a non-empty description, you will be asked for the combined description. If either was empty, then the other one will be used. @@ -2742,13 +2775,13 @@ An alternative squashing UI is available via the `-d`, `-A`, and `-B` options. T * `-r`, `--revision ` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options * `-f`, `--from ` — Revision(s) to squash from (default: @) * `-t`, `--into ` [alias: `to`] — Revision to squash into (default: @) +* `-i`, `--interactive` — Interactively choose which parts to squash +* `--tool ` — Specify diff editor to be used (implies --interactive) +* `-m`, `--message ` — The description to use for squashed revision (don't open editor) +* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s) * `-d`, `--destination ` — (Experimental) The revision(s) to use as parent for the new commit (can be repeated to create a merge commit) * `-A`, `--insert-after ` [alias: `after`] — (Experimental) The revision(s) to insert the new commit after (can be repeated to create a merge commit) * `-B`, `--insert-before ` [alias: `before`] — (Experimental) The revision(s) to insert the new commit before (can be repeated to create a merge commit) -* `-m`, `--message ` — The description to use for squashed revision (don't open editor) -* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s) -* `-i`, `--interactive` — Interactively choose which parts to squash -* `--tool ` — Specify diff editor to be used (implies --interactive) * `-k`, `--keep-emptied` — The source revision will not be abandoned diff --git a/cli/tests/test_squash_command.rs b/cli/tests/test_squash_command.rs index 17aaefa1101..b08ef590ed2 100644 --- a/cli/tests/test_squash_command.rs +++ b/cli/tests/test_squash_command.rs @@ -362,7 +362,7 @@ fn test_squash_partial() { } #[test] -fn test_squash_keep_emptied() { +fn test_amend() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); @@ -391,22 +391,22 @@ fn test_squash_keep_emptied() { [EOF] "); - let output = work_dir.run_jj(["squash", "-r", "b", "--keep-emptied"]); - insta::assert_snapshot!(output, @r" + let output = work_dir.run_jj(["amend", "-r", "b", "-m", "foo"]); + insta::assert_snapshot!(output, @r###" ------- stderr ------- Rebased 2 descendant commits - Working copy (@) now at: mzvwutvl 093590e0 c | (no description set) - Parent commit (@-) : kkmpptxz 357946cf b | (empty) (no description set) + Working copy (@) now at: mzvwutvl b34c7e04 c | (no description set) + Parent commit (@-) : kkmpptxz 4d5527a5 b | (empty) (no description set) [EOF] - "); - // With --keep-emptied, b remains even though it is now empty. - insta::assert_snapshot!(get_log_output(&work_dir), @r" - @ 093590e044bd c - ○ 357946cf85df b (empty) - ○ 2269fb3b12f5 a + "###); + // With amend b remains even though it is now empty. + insta::assert_snapshot!(get_log_output(&work_dir), @r###" + @ b34c7e040136 c + ○ 4d5527a53208 b (empty) + ○ f2c844473038 a foo ◆ 000000000000 (empty) [EOF] - "); + "###); let output = work_dir.run_jj(["file", "show", "file1", "-r", "a"]); insta::assert_snapshot!(output, @r" b