Skip to content

Commit efdb809

Browse files
committed
cli: Split jj squash into jj squash and jj movediff
As has been discussed in several places, `jj squash` is a very complicated command that can do many things. `jj movediff` is much simpler to explain ("moves diffs from one commit to another"). It is implemented in terms of squash because the difference between the two is more semantic than programatic. In the future, we *may* consider removing the ability for squash to operate on individual files, but that is still hotly debated.
1 parent f4b5460 commit efdb809

File tree

5 files changed

+197
-76
lines changed

5 files changed

+197
-76
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6666
tags. These keywords may be useful in non-colocated Git repositories where
6767
local and exported `@git` tags can point to different revisions.
6868

69+
* `jj amend` moves diffs between revisions, (similar to squash)
70+
6971
### Fixed bugs
7072

7173
* `jj metaedit --author-timestamp` twice with the same value no longer

cli/src/commands/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ enum Command {
108108
Describe(describe::DescribeArgs),
109109
Diff(diff::DiffArgs),
110110
Diffedit(diffedit::DiffeditArgs),
111+
Amend(squash::AmendArgs),
111112
Duplicate(duplicate::DuplicateArgs),
112113
Edit(edit::EditArgs),
113114
#[command(alias = "obslog", visible_alias = "evolution-log")]
@@ -180,6 +181,11 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
180181
Command::Describe(args) => describe::cmd_describe(ui, command_helper, args),
181182
Command::Diff(args) => diff::cmd_diff(ui, command_helper, args),
182183
Command::Diffedit(args) => diffedit::cmd_diffedit(ui, command_helper, args),
184+
Command::Amend(args) => squash::cmd_squash_or_amend(
185+
ui,
186+
command_helper,
187+
squash::SquashOrAmendArgs::AmendArgs(args),
188+
),
183189
Command::Duplicate(args) => duplicate::cmd_duplicate(ui, command_helper, args),
184190
Command::Edit(args) => edit::cmd_edit(ui, command_helper, args),
185191
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
212218
Command::Sign(args) => sign::cmd_sign(ui, command_helper, args),
213219
Command::Sparse(args) => sparse::cmd_sparse(ui, command_helper, args),
214220
Command::Split(args) => split::cmd_split(ui, command_helper, args),
215-
Command::Squash(args) => squash::cmd_squash(ui, command_helper, args),
221+
Command::Squash(args) => squash::cmd_squash_or_amend(
222+
ui,
223+
command_helper,
224+
squash::SquashOrAmendArgs::SquashArgs(args),
225+
),
216226
Command::Status(args) => status::cmd_status(ui, command_helper, args),
217227
Command::Tag(args) => tag::cmd_tag(ui, command_helper, args),
218228
Command::Undo(args) => undo::cmd_undo(ui, command_helper, args),

cli/src/commands/squash.rs

Lines changed: 132 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ use crate::description_util::join_message_paragraphs;
4747
use crate::description_util::try_combine_messages;
4848
use crate::ui::Ui;
4949

50-
/// Move changes from a revision into another revision
50+
/// Combine revisions by moving changes from a revision into another revision.
51+
///
52+
/// Note: If you aren't trying to merge the revision metadata
53+
/// (eg. description, bookmarks), or if you want to do a partial squash,
54+
/// you probably want `jj amend`.
5155
///
5256
/// With the `-r` option, moves the changes from the specified revision to the
5357
/// parent revision. Fails if there are several parent revisions (i.e., the
@@ -59,8 +63,8 @@ use crate::ui::Ui;
5963
/// commit to the grandparent.
6064
///
6165
/// If, after moving changes out, the source revision is empty compared to its
62-
/// parent(s), and `--keep-emptied` is not set, it will be abandoned. Without
63-
/// `--interactive` or paths, the source revision will always be empty.
66+
/// parent(s), it will be abandoned. Without `--interactive` or paths, the
67+
/// source revision will always be empty.
6468
///
6569
/// If the source was abandoned and both the source and destination had a
6670
/// non-empty description, you will be asked for the combined description. If
@@ -76,34 +80,17 @@ use crate::ui::Ui;
7680
/// (if no `--from` is specified, `--from @` is assumed).
7781
#[derive(clap::Args, Clone, Debug)]
7882
pub(crate) struct SquashArgs {
79-
/// Revision to squash into its parent (default: @). Incompatible with the
80-
/// experimental `-d`/`-A`/`-B` options.
81-
#[arg(
82-
long,
83-
short,
84-
value_name = "REVSET",
85-
add = ArgValueCompleter::new(complete::revset_expression_mutable),
86-
)]
87-
revision: Option<RevisionArg>,
83+
#[clap(flatten)]
84+
pub(crate) common: CommonArgs,
8885

89-
/// Revision(s) to squash from (default: @)
90-
#[arg(
91-
long, short,
92-
conflicts_with = "revision",
93-
value_name = "REVSETS",
94-
add = ArgValueCompleter::new(complete::revset_expression_mutable),
95-
)]
96-
from: Vec<RevisionArg>,
86+
/// The description to use for squashed revision (don't open editor)
87+
#[arg(long = "message", short, value_name = "MESSAGE")]
88+
pub(crate) message_paragraphs: Vec<String>,
9789

98-
/// Revision to squash into (default: @)
99-
#[arg(
100-
long, short = 't',
101-
conflicts_with = "revision",
102-
visible_alias = "to",
103-
value_name = "REVSET",
104-
add = ArgValueCompleter::new(complete::revset_expression_mutable),
105-
)]
106-
into: Option<RevisionArg>,
90+
/// Use the description of the destination revision and discard the
91+
/// description(s) of the source revision(s)
92+
#[arg(long, short, conflicts_with = "message_paragraphs")]
93+
pub(crate) use_destination_message: bool,
10794

10895
/// (Experimental) The revision(s) to use as parent for the new commit (can
10996
/// be repeated to create a merge commit)
@@ -145,14 +132,67 @@ pub(crate) struct SquashArgs {
145132
)]
146133
insert_before: Option<Vec<RevisionArg>>,
147134

148-
/// The description to use for squashed revision (don't open editor)
135+
/// The source revision will not be abandoned
136+
#[arg(long, short)]
137+
keep_emptied: bool,
138+
}
139+
140+
/// Move changes from a revision into another revision
141+
///
142+
/// With the `-r` option, moves the changes from the specified revision to the
143+
/// parent revision. Fails if there are several parent revisions (i.e., the
144+
/// given revision is a merge).
145+
///
146+
/// With the `--from` and/or `--into` options, moves changes from/to the given
147+
/// revisions. If either is left out, it defaults to the working-copy commit.
148+
/// For example, `jj squash --into @--` moves changes from the working-copy
149+
/// commit to the grandparent.
150+
///
151+
/// EXPERIMENTAL FEATURES
152+
///
153+
/// An alternative squashing UI is available via the `-d`, `-A`, and `-B`
154+
/// options. They can be used together with one or more `--from` options
155+
/// (if no `--from` is specified, `--from @` is assumed).
156+
#[derive(clap::Args, Clone, Debug)]
157+
pub(crate) struct AmendArgs {
158+
#[clap(flatten)]
159+
common: CommonArgs,
160+
161+
/// The description to overwrite for the destination revision.
149162
#[arg(long = "message", short, value_name = "MESSAGE")]
150-
message_paragraphs: Vec<String>,
163+
pub(crate) message_paragraphs: Vec<String>,
164+
}
151165

152-
/// Use the description of the destination revision and discard the
153-
/// description(s) of the source revision(s)
154-
#[arg(long, short, conflicts_with = "message_paragraphs")]
155-
use_destination_message: bool,
166+
#[derive(clap::Args, Clone, Debug)]
167+
pub(crate) struct CommonArgs {
168+
/// Revision to squash into its parent (default: @). Incompatible with the
169+
/// experimental `-d`/`-A`/`-B` options.
170+
#[arg(
171+
long,
172+
short,
173+
value_name = "REVSET",
174+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
175+
)]
176+
revision: Option<RevisionArg>,
177+
178+
/// Revision(s) to squash from (default: @)
179+
#[arg(
180+
long, short,
181+
conflicts_with = "revision",
182+
value_name = "REVSETS",
183+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
184+
)]
185+
from: Vec<RevisionArg>,
186+
187+
/// Revision to squash into (default: @)
188+
#[arg(
189+
long, short = 't',
190+
conflicts_with = "revision",
191+
visible_alias = "to",
192+
value_name = "REVSET",
193+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
194+
)]
195+
into: Option<RevisionArg>,
156196

157197
/// Interactively choose which parts to squash
158198
#[arg(long, short)]
@@ -173,20 +213,59 @@ pub(crate) struct SquashArgs {
173213
add = ArgValueCompleter::new(complete::squash_revision_files),
174214
)]
175215
paths: Vec<String>,
216+
}
176217

177-
/// The source revision will not be abandoned
178-
#[arg(long, short)]
179-
keep_emptied: bool,
218+
pub(crate) enum SquashOrAmendArgs<'a> {
219+
SquashArgs(&'a SquashArgs),
220+
AmendArgs(&'a AmendArgs),
180221
}
181222

182223
#[instrument(skip_all)]
183-
pub(crate) fn cmd_squash(
224+
pub(crate) fn cmd_squash_or_amend(
184225
ui: &mut Ui,
185226
command: &CommandHelper,
186-
args: &SquashArgs,
227+
args: SquashOrAmendArgs,
187228
) -> Result<(), CommandError> {
229+
let keep_emptied = match args {
230+
SquashOrAmendArgs::SquashArgs(args) => {
231+
if args.keep_emptied {
232+
writeln!(
233+
ui.warning_default(),
234+
"`jj squash --keep-emptied` has been deprecated and replaced with `jj amend`",
235+
)?;
236+
}
237+
args.keep_emptied
238+
}
239+
SquashOrAmendArgs::AmendArgs(_) => true,
240+
};
241+
let message_paragraphs = match args {
242+
SquashOrAmendArgs::SquashArgs(args) => &args.message_paragraphs,
243+
SquashOrAmendArgs::AmendArgs(args) => &args.message_paragraphs,
244+
};
245+
let use_destination_message = match args {
246+
SquashOrAmendArgs::SquashArgs(args) => args.use_destination_message,
247+
SquashOrAmendArgs::AmendArgs(_) => Default::default(),
248+
};
249+
let insert_before = match args {
250+
SquashOrAmendArgs::SquashArgs(args) => &args.insert_before,
251+
SquashOrAmendArgs::AmendArgs(_) => &None,
252+
};
253+
let insert_after = match args {
254+
SquashOrAmendArgs::SquashArgs(args) => &args.insert_after,
255+
SquashOrAmendArgs::AmendArgs(_) => &None,
256+
};
257+
let destination = match args {
258+
SquashOrAmendArgs::SquashArgs(args) => &args.destination,
259+
SquashOrAmendArgs::AmendArgs(_) => &None,
260+
};
261+
262+
let args = match args {
263+
SquashOrAmendArgs::SquashArgs(args) => &args.common,
264+
SquashOrAmendArgs::AmendArgs(args) => &args.common,
265+
};
266+
188267
let insert_destination_commit =
189-
args.destination.is_some() || args.insert_after.is_some() || args.insert_before.is_some();
268+
destination.is_some() || insert_after.is_some() || insert_before.is_some();
190269

191270
let mut workspace_command = command.workspace_helper(ui)?;
192271

@@ -253,9 +332,9 @@ pub(crate) fn cmd_squash(
253332
let (parent_ids, child_ids) = compute_commit_location(
254333
ui,
255334
tx.base_workspace_helper(),
256-
args.destination.as_deref(),
257-
args.insert_after.as_deref(),
258-
args.insert_before.as_deref(),
335+
destination.as_deref(),
336+
insert_after.as_deref(),
337+
insert_before.as_deref(),
259338
"squashed commit",
260339
)?;
261340
let parent_commits: Vec<_> = parent_ids
@@ -310,15 +389,12 @@ pub(crate) fn cmd_squash(
310389
tx.base_workspace_helper()
311390
.diff_selector(ui, args.tool.as_deref(), args.interactive)?;
312391
let text_editor = tx.base_workspace_helper().text_editor()?;
313-
let description = SquashedDescription::from_args(args);
392+
let description = SquashedDescription::from_args(message_paragraphs, use_destination_message);
314393

315394
let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
316-
if let Some(squashed) = rewrite::squash_commits(
317-
tx.repo_mut(),
318-
&source_commits,
319-
&destination,
320-
args.keep_emptied,
321-
)? {
395+
if let Some(squashed) =
396+
rewrite::squash_commits(tx.repo_mut(), &source_commits, &destination, keep_emptied)?
397+
{
322398
let mut commit_builder = squashed.commit_builder.detach();
323399
let new_description = match description {
324400
SquashedDescription::Exact(description) => {
@@ -434,14 +510,14 @@ enum SquashedDescription {
434510
}
435511

436512
impl SquashedDescription {
437-
fn from_args(args: &SquashArgs) -> Self {
513+
fn from_args(message_paragraphs: &[String], use_destination_message: bool) -> Self {
438514
// These options are incompatible and Clap is configured to prevent this.
439-
assert!(args.message_paragraphs.is_empty() || !args.use_destination_message);
515+
assert!(message_paragraphs.is_empty() || !use_destination_message);
440516

441-
if !args.message_paragraphs.is_empty() {
442-
let desc = join_message_paragraphs(&args.message_paragraphs);
517+
if !message_paragraphs.is_empty() {
518+
let desc = join_message_paragraphs(message_paragraphs);
443519
Self::Exact(desc)
444-
} else if args.use_destination_message {
520+
} else if use_destination_message {
445521
Self::UseDestination
446522
} else {
447523
Self::Combine

cli/tests/[email protected]

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ This document contains the help content for the `jj` command-line program.
3636
* [`jj describe`↴](#jj-describe)
3737
* [`jj diff`↴](#jj-diff)
3838
* [`jj diffedit`↴](#jj-diffedit)
39+
* [`jj amend`↴](#jj-amend)
3940
* [`jj duplicate`↴](#jj-duplicate)
4041
* [`jj edit`↴](#jj-edit)
4142
* [`jj evolog`↴](#jj-evolog)
@@ -141,6 +142,7 @@ To get started, see the tutorial [`jj help -k tutorial`].
141142
* `describe` — Update the change description or other metadata [default alias: desc]
142143
* `diff` — Compare file contents between two revisions
143144
* `diffedit` — Touch up the content changes in a revision with a diff editor
145+
* `amend` — Move changes from a revision into another revision
144146
* `duplicate` — Create new changes with the same content as existing ones
145147
* `edit` — Sets the specified revision as the working-copy revision
146148
* `evolog` — Show how a change has evolved over time
@@ -168,7 +170,7 @@ To get started, see the tutorial [`jj help -k tutorial`].
168170
* `simplify-parents` — Simplify parent edges for the specified revision(s)
169171
* `sparse` — Manage which paths from the working-copy commit are present in the working copy
170172
* `split` — Split a revision in two
171-
* `squash` — Move changes from a revision into another revision
173+
* `squash` — Combine revisions by moving changes from a revision into another revision
172174
* `status` — Show high-level repo status [default alias: st]
173175
* `tag` — Manage tags
174176
* `undo` — Undo the last operation
@@ -909,6 +911,35 @@ See `jj restore` if you want to move entire files from one revision to another.
909911

910912

911913

914+
## `jj amend`
915+
916+
Move changes from a revision into another revision
917+
918+
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).
919+
920+
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.
921+
922+
EXPERIMENTAL FEATURES
923+
924+
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).
925+
926+
**Usage:** `jj amend [OPTIONS] [FILESETS]...`
927+
928+
###### **Arguments:**
929+
930+
* `<FILESETS>` — Move only changes to these paths (instead of all paths)
931+
932+
###### **Options:**
933+
934+
* `-r`, `--revision <REVSET>` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options
935+
* `-f`, `--from <REVSETS>` — Revision(s) to squash from (default: @)
936+
* `-t`, `--into <REVSET>` [alias: `to`] — Revision to squash into (default: @)
937+
* `-i`, `--interactive` — Interactively choose which parts to squash
938+
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
939+
* `-m`, `--message <MESSAGE>` — The description to overwrite for the destination revision
940+
941+
942+
912943
## `jj duplicate`
913944

914945
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
27152746

27162747
## `jj squash`
27172748

2718-
Move changes from a revision into another revision
2749+
Combine revisions by moving changes from a revision into another revision.
2750+
2751+
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`.
27192752

27202753
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).
27212754

27222755
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.
27232756

2724-
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.
2757+
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.
27252758

27262759
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.
27272760

@@ -2742,13 +2775,13 @@ An alternative squashing UI is available via the `-d`, `-A`, and `-B` options. T
27422775
* `-r`, `--revision <REVSET>` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options
27432776
* `-f`, `--from <REVSETS>` — Revision(s) to squash from (default: @)
27442777
* `-t`, `--into <REVSET>` [alias: `to`] — Revision to squash into (default: @)
2778+
* `-i`, `--interactive` — Interactively choose which parts to squash
2779+
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
2780+
* `-m`, `--message <MESSAGE>` — The description to use for squashed revision (don't open editor)
2781+
* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s)
27452782
* `-d`, `--destination <REVSETS>` — (Experimental) The revision(s) to use as parent for the new commit (can be repeated to create a merge commit)
27462783
* `-A`, `--insert-after <REVSETS>` [alias: `after`] — (Experimental) The revision(s) to insert the new commit after (can be repeated to create a merge commit)
27472784
* `-B`, `--insert-before <REVSETS>` [alias: `before`] — (Experimental) The revision(s) to insert the new commit before (can be repeated to create a merge commit)
2748-
* `-m`, `--message <MESSAGE>` — The description to use for squashed revision (don't open editor)
2749-
* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s)
2750-
* `-i`, `--interactive` — Interactively choose which parts to squash
2751-
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
27522785
* `-k`, `--keep-emptied` — The source revision will not be abandoned
27532786

27542787

0 commit comments

Comments
 (0)