Skip to content

Commit dd8e02e

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 a979755 commit dd8e02e

File tree

5 files changed

+185
-68
lines changed

5 files changed

+185
-68
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1919
* `jj metaedit` now accepts `-m`/`--message` option to non-interactively update
2020
the change description.
2121

22+
* `jj movediff` moves diffs between revisions, (similar to squash)
23+
2224
### Fixed bugs
2325

2426
* `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
@@ -111,6 +111,7 @@ enum Command {
111111
Describe(describe::DescribeArgs),
112112
Diff(diff::DiffArgs),
113113
Diffedit(diffedit::DiffeditArgs),
114+
Diffmove(squash::Diffmove),
114115
Duplicate(duplicate::DuplicateArgs),
115116
Edit(edit::EditArgs),
116117
#[command(alias = "obslog", visible_alias = "evolution-log")]
@@ -184,6 +185,11 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
184185
Command::Describe(args) => describe::cmd_describe(ui, command_helper, args),
185186
Command::Diff(args) => diff::cmd_diff(ui, command_helper, args),
186187
Command::Diffedit(args) => diffedit::cmd_diffedit(ui, command_helper, args),
188+
Command::Diffmove(args) => squash::cmd_squash_or_diffmove(
189+
ui,
190+
command_helper,
191+
squash::SquashOrDiffmoveArgs::DiffmoveArgs(args),
192+
),
187193
Command::Duplicate(args) => duplicate::cmd_duplicate(ui, command_helper, args),
188194
Command::Edit(args) => edit::cmd_edit(ui, command_helper, args),
189195
Command::Evolog(args) => evolog::cmd_evolog(ui, command_helper, args),
@@ -216,7 +222,11 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
216222
Command::Sign(args) => sign::cmd_sign(ui, command_helper, args),
217223
Command::Sparse(args) => sparse::cmd_sparse(ui, command_helper, args),
218224
Command::Split(args) => split::cmd_split(ui, command_helper, args),
219-
Command::Squash(args) => squash::cmd_squash(ui, command_helper, args),
225+
Command::Squash(args) => squash::cmd_squash_or_diffmove(
226+
ui,
227+
command_helper,
228+
squash::SquashOrDiffmoveArgs::SquashArgs(args),
229+
),
220230
Command::Status(args) => status::cmd_status(ui, command_helper, args),
221231
Command::Tag(args) => tag::cmd_tag(ui, command_helper, args),
222232
Command::Undo(args) => undo::cmd_undo(ui, command_helper, args),

cli/src/commands/squash.rs

Lines changed: 130 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ use crate::description_util::join_message_paragraphs;
4646
use crate::description_util::try_combine_messages;
4747
use crate::ui::Ui;
4848

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

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

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

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

147-
/// The description to use for squashed revision (don't open editor)
148-
#[arg(long = "message", short, value_name = "MESSAGE")]
149-
message_paragraphs: Vec<String>,
134+
/// The source revision will not be abandoned
135+
#[arg(long, short)]
136+
keep_emptied: bool,
137+
}
150138

151-
/// Use the description of the destination revision and discard the
152-
/// description(s) of the source revision(s)
153-
#[arg(long, short, conflicts_with = "message_paragraphs")]
154-
use_destination_message: bool,
139+
/// Move changes from a revision into another revision
140+
///
141+
/// With the `-r` option, moves the changes from the specified revision to the
142+
/// parent revision. Fails if there are several parent revisions (i.e., the
143+
/// given revision is a merge).
144+
///
145+
/// With the `--from` and/or `--into` options, moves changes from/to the given
146+
/// revisions. If either is left out, it defaults to the working-copy commit.
147+
/// For example, `jj squash --into @--` moves changes from the working-copy
148+
/// commit to the grandparent.
149+
///
150+
/// EXPERIMENTAL FEATURES
151+
///
152+
/// An alternative squashing UI is available via the `-d`, `-A`, and `-B`
153+
/// options. They can be used together with one or more `--from` options
154+
/// (if no `--from` is specified, `--from @` is assumed).
155+
#[derive(clap::Args, Clone, Debug)]
156+
pub(crate) struct Diffmove {
157+
#[clap(flatten)]
158+
common: CommonArgs,
159+
}
160+
161+
#[derive(clap::Args, Clone, Debug)]
162+
pub(crate) struct CommonArgs {
163+
/// Revision to squash into its parent (default: @). Incompatible with the
164+
/// experimental `-d`/`-A`/`-B` options.
165+
#[arg(
166+
long,
167+
short,
168+
value_name = "REVSET",
169+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
170+
)]
171+
revision: Option<RevisionArg>,
172+
173+
/// Revision(s) to squash from (default: @)
174+
#[arg(
175+
long, short,
176+
conflicts_with = "revision",
177+
value_name = "REVSETS",
178+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
179+
)]
180+
from: Vec<RevisionArg>,
181+
182+
/// Revision to squash into (default: @)
183+
#[arg(
184+
long, short = 't',
185+
conflicts_with = "revision",
186+
visible_alias = "to",
187+
value_name = "REVSET",
188+
add = ArgValueCompleter::new(complete::revset_expression_mutable),
189+
)]
190+
into: Option<RevisionArg>,
155191

156192
/// Interactively choose which parts to squash
157193
#[arg(long, short)]
@@ -172,20 +208,60 @@ pub(crate) struct SquashArgs {
172208
add = ArgValueCompleter::new(complete::squash_revision_files),
173209
)]
174210
paths: Vec<String>,
211+
}
175212

176-
/// The source revision will not be abandoned
177-
#[arg(long, short)]
178-
keep_emptied: bool,
213+
pub(crate) enum SquashOrDiffmoveArgs<'a> {
214+
SquashArgs(&'a SquashArgs),
215+
DiffmoveArgs(&'a Diffmove),
179216
}
180217

181218
#[instrument(skip_all)]
182-
pub(crate) fn cmd_squash(
219+
pub(crate) fn cmd_squash_or_diffmove(
183220
ui: &mut Ui,
184221
command: &CommandHelper,
185-
args: &SquashArgs,
222+
args: SquashOrDiffmoveArgs,
186223
) -> Result<(), CommandError> {
224+
let keep_emptied = match args {
225+
SquashOrDiffmoveArgs::SquashArgs(args) => {
226+
if args.keep_emptied {
227+
writeln!(
228+
ui.warning_default(),
229+
"`jj squash --keep-emptied` has been deprecated and replaced with `jj \
230+
diffmove`",
231+
)?;
232+
}
233+
args.keep_emptied
234+
}
235+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => true,
236+
};
237+
let message_paragraphs = match args {
238+
SquashOrDiffmoveArgs::SquashArgs(args) => &args.message_paragraphs,
239+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => &Default::default(),
240+
};
241+
let use_destination_message = match args {
242+
SquashOrDiffmoveArgs::SquashArgs(args) => args.use_destination_message,
243+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => Default::default(),
244+
};
245+
let insert_before = match args {
246+
SquashOrDiffmoveArgs::SquashArgs(args) => &args.insert_before,
247+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => &None,
248+
};
249+
let insert_after = match args {
250+
SquashOrDiffmoveArgs::SquashArgs(args) => &args.insert_after,
251+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => &None,
252+
};
253+
let destination = match args {
254+
SquashOrDiffmoveArgs::SquashArgs(args) => &args.destination,
255+
SquashOrDiffmoveArgs::DiffmoveArgs(_) => &None,
256+
};
257+
258+
let args = match args {
259+
SquashOrDiffmoveArgs::SquashArgs(args) => &args.common,
260+
SquashOrDiffmoveArgs::DiffmoveArgs(args) => &args.common,
261+
};
262+
187263
let insert_destination_commit =
188-
args.destination.is_some() || args.insert_after.is_some() || args.insert_before.is_some();
264+
destination.is_some() || insert_after.is_some() || insert_before.is_some();
189265

190266
let mut workspace_command = command.workspace_helper(ui)?;
191267

@@ -252,9 +328,9 @@ pub(crate) fn cmd_squash(
252328
let (parent_ids, child_ids) = compute_commit_location(
253329
ui,
254330
tx.base_workspace_helper(),
255-
args.destination.as_deref(),
256-
args.insert_after.as_deref(),
257-
args.insert_before.as_deref(),
331+
destination.as_deref(),
332+
insert_after.as_deref(),
333+
insert_before.as_deref(),
258334
"squashed commit",
259335
)?;
260336
let parent_commits: Vec<_> = parent_ids
@@ -299,15 +375,12 @@ pub(crate) fn cmd_squash(
299375
tx.base_workspace_helper()
300376
.diff_selector(ui, args.tool.as_deref(), args.interactive)?;
301377
let text_editor = tx.base_workspace_helper().text_editor()?;
302-
let description = SquashedDescription::from_args(args);
378+
let description = SquashedDescription::from_args(message_paragraphs, use_destination_message);
303379

304380
let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
305-
if let Some(squashed) = rewrite::squash_commits(
306-
tx.repo_mut(),
307-
&source_commits,
308-
&destination,
309-
args.keep_emptied,
310-
)? {
381+
if let Some(squashed) =
382+
rewrite::squash_commits(tx.repo_mut(), &source_commits, &destination, keep_emptied)?
383+
{
311384
let mut commit_builder = squashed.commit_builder.detach();
312385
let new_description = match description {
313386
SquashedDescription::Exact(description) => {
@@ -423,14 +496,14 @@ enum SquashedDescription {
423496
}
424497

425498
impl SquashedDescription {
426-
fn from_args(args: &SquashArgs) -> Self {
499+
fn from_args(message_paragraphs: &[String], use_destination_message: bool) -> Self {
427500
// These options are incompatible and Clap is configured to prevent this.
428-
assert!(args.message_paragraphs.is_empty() || !args.use_destination_message);
501+
assert!(message_paragraphs.is_empty() || !use_destination_message);
429502

430-
if !args.message_paragraphs.is_empty() {
431-
let desc = join_message_paragraphs(&args.message_paragraphs);
503+
if !message_paragraphs.is_empty() {
504+
let desc = join_message_paragraphs(message_paragraphs);
432505
Self::Exact(desc)
433-
} else if args.use_destination_message {
506+
} else if use_destination_message {
434507
Self::UseDestination
435508
} else {
436509
Self::Combine

cli/tests/[email protected]

Lines changed: 39 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 diffmove`↴](#jj-diffmove)
3940
* [`jj duplicate`↴](#jj-duplicate)
4041
* [`jj edit`↴](#jj-edit)
4142
* [`jj evolog`↴](#jj-evolog)
@@ -139,6 +140,7 @@ To get started, see the tutorial [`jj help -k tutorial`].
139140
* `describe` — Update the change description or other metadata [default alias: desc]
140141
* `diff` — Compare file contents between two revisions
141142
* `diffedit` — Touch up the content changes in a revision with a diff editor
143+
* `diffmove` — Move changes from a revision into another revision
142144
* `duplicate` — Create new changes with the same content as existing ones
143145
* `edit` — Sets the specified revision as the working-copy revision
144146
* `evolog` — Show how a change has evolved over time
@@ -166,7 +168,7 @@ To get started, see the tutorial [`jj help -k tutorial`].
166168
* `simplify-parents` — Simplify parent edges for the specified revision(s)
167169
* `sparse` — Manage which paths from the working-copy commit are present in the working copy
168170
* `split` — Split a revision in two
169-
* `squash` — Move changes from a revision into another revision
171+
* `squash` — Combine revisions by moving changes from a revision into another revision
170172
* `status` — Show high-level repo status [default alias: st]
171173
* `tag` — Manage tags
172174
* `undo` — Undo the last operation
@@ -892,6 +894,34 @@ See `jj restore` if you want to move entire files from one revision to another.
892894

893895

894896

897+
## `jj diffmove`
898+
899+
Move changes from a revision into another revision
900+
901+
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).
902+
903+
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.
904+
905+
EXPERIMENTAL FEATURES
906+
907+
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).
908+
909+
**Usage:** `jj diffmove [OPTIONS] [FILESETS]...`
910+
911+
###### **Arguments:**
912+
913+
* `<FILESETS>` — Move only changes to these paths (instead of all paths)
914+
915+
###### **Options:**
916+
917+
* `-r`, `--revision <REVSET>` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options
918+
* `-f`, `--from <REVSETS>` — Revision(s) to squash from (default: @)
919+
* `-t`, `--into <REVSET>` [alias: `to`] — Revision to squash into (default: @)
920+
* `-i`, `--interactive` — Interactively choose which parts to squash
921+
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
922+
923+
924+
895925
## `jj duplicate`
896926

897927
Create new changes with the same content as existing ones
@@ -2698,13 +2728,15 @@ Splitting an empty commit is not supported because the same effect can be achiev
26982728

26992729
## `jj squash`
27002730

2701-
Move changes from a revision into another revision
2731+
Combine revisions by moving changes from a revision into another revision.
2732+
2733+
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 movediff`.
27022734

27032735
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).
27042736

27052737
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.
27062738

2707-
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.
2739+
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.
27082740

27092741
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.
27102742

@@ -2725,13 +2757,13 @@ An alternative squashing UI is available via the `-d`, `-A`, and `-B` options. T
27252757
* `-r`, `--revision <REVSET>` — Revision to squash into its parent (default: @). Incompatible with the experimental `-d`/`-A`/`-B` options
27262758
* `-f`, `--from <REVSETS>` — Revision(s) to squash from (default: @)
27272759
* `-t`, `--into <REVSET>` [alias: `to`] — Revision to squash into (default: @)
2760+
* `-i`, `--interactive` — Interactively choose which parts to squash
2761+
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
2762+
* `-m`, `--message <MESSAGE>` — The description to use for squashed revision (don't open editor)
2763+
* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s)
27282764
* `-d`, `--destination <REVSETS>` — (Experimental) The revision(s) to use as parent for the new commit (can be repeated to create a merge commit)
27292765
* `-A`, `--insert-after <REVSETS>` [alias: `after`] — (Experimental) The revision(s) to insert the new commit after (can be repeated to create a merge commit)
27302766
* `-B`, `--insert-before <REVSETS>` [alias: `before`] — (Experimental) The revision(s) to insert the new commit before (can be repeated to create a merge commit)
2731-
* `-m`, `--message <MESSAGE>` — The description to use for squashed revision (don't open editor)
2732-
* `-u`, `--use-destination-message` — Use the description of the destination revision and discard the description(s) of the source revision(s)
2733-
* `-i`, `--interactive` — Interactively choose which parts to squash
2734-
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
27352767
* `-k`, `--keep-emptied` — The source revision will not be abandoned
27362768

27372769

0 commit comments

Comments
 (0)