squash: add -A/-B/-d options

For example, with a commit tree like this one:

@  kkrvspxk (empty) (no description set)
○  lpllnppl file4
│  A file4
○  xynqvmyt file3
│  A file3
○  wkrztqwl file2
│  A file2
○  mzuowqnz file1
│  A file1
♦  zzzzzzzz root()

we can jj squash -f x:: -d m to squash xynqvmyt and all its descendant
as a new commit onto mzuowqnz.

@  youptqqn (empty) (no description set)
○  wkrztqwl file2
│  A file2
│ ○  vsonsouy file3 file4
├─╯  A file3
│    A file4
○  mzuowqnz file1
│  A file1
♦  zzzzzzzz root()

On the implementation side, I've chosen to prepare the destination
commit before hand, and keep the squash algorithm mostly unmodified.
This commit is contained in:
Gaëtan Lehmann 2025-08-03 17:49:57 +02:00 committed by Gaëtan Lehmann
parent e8f91696fe
commit 1e58ca2253
6 changed files with 806 additions and 23 deletions

View file

@ -74,6 +74,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
output using template expressions, similar to `jj op log`. Also added
`--no-op-diff` flag to suppress the operation diff.
* `jj squash` has gained `--insert-before`, `--insert-after`, and `--destination`
options.
### Fixed bugs
* `jj git clone` now correctly fetches all tags, unless `--fetch-tags` is

View file

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use indoc::formatdoc;
@ -23,12 +25,15 @@ use jj_lib::object_id::ObjectId as _;
use jj_lib::repo::Repo as _;
use jj_lib::rewrite;
use jj_lib::rewrite::CommitWithSelection;
use jj_lib::rewrite::merge_commit_trees;
use pollster::FutureExt as _;
use tracing::instrument;
use crate::cli_util::CommandHelper;
use crate::cli_util::DiffSelector;
use crate::cli_util::RevisionArg;
use crate::cli_util::WorkspaceCommandTransaction;
use crate::cli_util::compute_commit_location;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::command_error::user_error_with_hint;
@ -72,6 +77,7 @@ pub(crate) struct SquashArgs {
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
revision: Option<RevisionArg>,
/// Revision(s) to squash from (default: @)
#[arg(
long, short,
@ -80,6 +86,7 @@ pub(crate) struct SquashArgs {
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
from: Vec<RevisionArg>,
/// Revision to squash into (default: @)
#[arg(
long, short = 't',
@ -89,16 +96,60 @@ pub(crate) struct SquashArgs {
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
into: Option<RevisionArg>,
/// The revision(s) to use as parent for the new commit (can be repeated
/// to create a merge commit)
#[arg(
long,
short,
conflicts_with = "into",
conflicts_with = "revision",
value_name = "REVSETS",
add = ArgValueCompleter::new(complete::revset_expression_all),
)]
destination: Option<Vec<RevisionArg>>,
/// The revision(s) to insert the new commit after (can be repeated to
/// create a merge commit)
#[arg(
long,
short = 'A',
visible_alias = "after",
conflicts_with = "destination",
conflicts_with = "into",
conflicts_with = "revision",
value_name = "REVSETS",
add = ArgValueCompleter::new(complete::revset_expression_all),
)]
insert_after: Option<Vec<RevisionArg>>,
/// The revision(s) to insert the new commit before (can be repeated to
/// create a merge commit)
#[arg(
long,
short = 'B',
visible_alias = "before",
conflicts_with = "destination",
conflicts_with = "into",
conflicts_with = "revision",
value_name = "REVSETS",
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
insert_before: Option<Vec<RevisionArg>>,
/// The description to use for squashed revision (don't open editor)
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// 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,
/// Interactively choose which parts to squash
#[arg(long, short)]
interactive: bool,
/// Specify diff editor to be used (implies --interactive)
#[arg(
long,
@ -106,6 +157,7 @@ pub(crate) struct SquashArgs {
add = ArgValueCandidates::new(complete::diff_editors),
)]
tool: Option<String>,
/// Move only changes to these paths (instead of all paths)
#[arg(
value_name = "FILESETS",
@ -113,6 +165,7 @@ pub(crate) struct SquashArgs {
add = ArgValueCompleter::new(complete::squash_revision_files),
)]
paths: Vec<String>,
/// The source revision will not be abandoned
#[arg(long, short)]
keep_emptied: bool,
@ -124,11 +177,15 @@ pub(crate) fn cmd_squash(
command: &CommandHelper,
args: &SquashArgs,
) -> Result<(), CommandError> {
let insert_destination_commit =
args.destination.is_some() || args.insert_after.is_some() || args.insert_before.is_some();
let mut workspace_command = command.workspace_helper(ui)?;
let mut sources: Vec<Commit>;
let destination;
if !args.from.is_empty() || args.into.is_some() {
let pre_existing_destination;
if !args.from.is_empty() || args.into.is_some() || insert_destination_commit {
sources = if args.from.is_empty() {
workspace_command.parse_revset(ui, &RevisionArg::AT)?
} else {
@ -136,10 +193,15 @@ pub(crate) fn cmd_squash(
}
.evaluate_to_commits()?
.try_collect()?;
destination = workspace_command
.resolve_single_rev(ui, args.into.as_ref().unwrap_or(&RevisionArg::AT))?;
// remove the destination from the sources
sources.retain(|source| source.id() != destination.id());
if insert_destination_commit {
pre_existing_destination = None;
} else {
let destination = workspace_command
.resolve_single_rev(ui, args.into.as_ref().unwrap_or(&RevisionArg::AT))?;
// remove the destination from the sources
sources.retain(|source| source.id() != destination.id());
pre_existing_destination = Some(destination);
}
// Reverse the set so we apply the oldest commits first. It shouldn't affect the
// result, but it avoids creating transient conflicts and is therefore probably
// a little faster.
@ -155,21 +217,83 @@ pub(crate) fn cmd_squash(
));
}
sources = vec![source];
destination = parents.pop().unwrap();
}
pre_existing_destination = Some(parents.pop().unwrap());
};
let matcher = workspace_command
workspace_command.check_rewritable(sources.iter().chain(&pre_existing_destination).ids())?;
// prepare the tx description before possibly rebasing the source commits
let source_ids: Vec<_> = sources.iter().ids().collect();
let tx_description = if let Some(destination) = &pre_existing_destination {
format!("squash commits into {}", destination.id().hex())
} else {
match &source_ids[..] {
[] => format!("squash {} commits", source_ids.len()),
[id] => format!("squash commit {}", id.hex()),
[first, others @ ..] => {
format!("squash commit {} and {} more", first.hex(), others.len())
}
}
};
let mut tx = workspace_command.start_transaction();
let mut num_rebased = 0;
let destination = if let Some(commit) = pre_existing_destination {
commit
} else {
// create the new destination commit
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(),
"squashed commit",
)?;
let parent_commits: Vec<_> = parent_ids
.iter()
.map(|commit_id| {
tx.base_workspace_helper()
.repo()
.store()
.get_commit(commit_id)
})
.try_collect()?;
let merged_tree = merge_commit_trees(tx.repo(), &parent_commits).block_on()?;
let commit = tx
.repo_mut()
.new_commit(parent_ids.clone(), merged_tree.id())
.write()?;
let mut rewritten = HashMap::new();
tx.repo_mut()
.transform_descendants(child_ids, async |mut rewriter| {
let old_commit_id = rewriter.old_commit().id().clone();
for parent_id in &parent_ids {
rewriter.replace_parent(parent_id, [commit.id()]);
}
let new_commit = rewriter.rebase().await?.write()?;
rewritten.insert(old_commit_id, new_commit);
num_rebased += 1;
Ok(())
})?;
for source in &mut *sources {
if let Some(rewritten_source) = rewritten.remove(source.id()) {
*source = rewritten_source;
}
}
commit
};
let matcher = tx
.base_workspace_helper()
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
let diff_selector =
workspace_command.diff_selector(ui, args.tool.as_deref(), args.interactive)?;
let text_editor = workspace_command.text_editor()?;
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);
workspace_command
.check_rewritable(sources.iter().chain(std::iter::once(&destination)).ids())?;
let mut tx = workspace_command.start_transaction();
let tx_description = format!("squash commits into {}", destination.id().hex());
let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
if let Some(squashed) = rewrite::squash_commits(
tx.repo_mut(),
@ -209,7 +333,7 @@ pub(crate) fn cmd_squash(
ui,
&tx,
abandoned_commits,
&destination,
(!insert_destination_commit).then_some(&destination),
&commit_builder,
)?;
// It's weird that commit.description() contains "JJ: " lines, but works.
@ -222,12 +346,45 @@ pub(crate) fn cmd_squash(
}
};
commit_builder.set_description(new_description);
commit_builder.write(tx.repo_mut())?;
if insert_destination_commit {
// forget about the intermediate commit
commit_builder.set_predecessors(
commit_builder
.predecessors()
.iter()
.filter(|p| p != &destination.id())
.cloned()
.collect(),
);
}
let commit = commit_builder.write(tx.repo_mut())?;
let num_rebased = tx.repo_mut().rebase_descendants()?;
if let Some(mut formatter) = ui.status_formatter() {
if insert_destination_commit {
write!(formatter, "Created new commit ")?;
tx.write_commit_summary(formatter.as_mut(), &commit)?;
writeln!(formatter)?;
}
if num_rebased > 0 {
writeln!(formatter, "Rebased {num_rebased} descendant commits")?;
}
}
} else {
if diff_selector.is_interactive() {
return Err(user_error("No changes selected"));
}
if let Some(mut formatter) = ui.status_formatter() {
if insert_destination_commit {
write!(formatter, "Created new commit ")?;
tx.write_commit_summary(formatter.as_mut(), &destination)?;
writeln!(formatter)?;
}
if num_rebased > 0 {
writeln!(formatter, "Rebased {num_rebased} descendant commits")?;
}
}
if let [only_path] = &*args.paths {
let no_rev_arg = args.revision.is_none() && args.from.is_empty() && args.into.is_none();
if no_rev_arg

View file

@ -323,12 +323,14 @@ pub fn combine_messages_for_editing(
ui: &Ui,
tx: &WorkspaceCommandTransaction,
sources: &[Commit],
destination: &Commit,
destination: Option<&Commit>,
commit_builder: &DetachedCommitBuilder,
) -> Result<String, CommandError> {
let mut combined = String::new();
combined.push_str("JJ: Description from the destination commit:\n");
combined.push_str(destination.description());
if let Some(destination) = destination {
combined.push_str("JJ: Description from the destination commit:\n");
combined.push_str(destination.description());
}
for commit in sources {
combined.push_str("\nJJ: Description from source commit:\n");
combined.push_str(commit.description());
@ -338,7 +340,7 @@ pub fn combine_messages_for_editing(
// show the user only trailers that were not in one of the squashed commits
let old_trailers: Vec<_> = sources
.iter()
.chain(std::iter::once(destination))
.chain(destination)
.flat_map(|commit| parse_description_trailers(commit.description()))
.collect();
let commit = commit_builder.write_hidden()?;

View file

@ -2588,6 +2588,9 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T
* `-r`, `--revision <REVSET>` — Revision to squash into its parent (default: @)
* `-f`, `--from <REVSETS>` — Revision(s) to squash from (default: @)
* `-t`, `--into <REVSET>` [alias: `to`] — Revision to squash into (default: @)
* `-d`, `--destination <REVSETS>` — The revision(s) to use as parent for the new commit (can be repeated to create a merge commit)
* `-A`, `--insert-after <REVSETS>` [alias: `after`] — The revision(s) to insert the new commit after (can be repeated to create a merge commit)
* `-B`, `--insert-before <REVSETS>` [alias: `before`] — The revision(s) to insert the new commit before (can be repeated to create a merge commit)
* `-m`, `--message <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

View file

@ -538,6 +538,34 @@ fn test_rewrite_immutable_commands() {
[EOF]
[exit status: 1]
"#);
// squash --after
let output = work_dir.run_jj(["squash", "--after=main-"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Commit 4397373a0991 is immutable
Hint: Could not modify commit: mzvwutvl 4397373a main | (conflict) merge
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
[exit status: 1]
"#);
// squash --before
let output = work_dir.run_jj(["squash", "--before=main"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Commit 4397373a0991 is immutable
Hint: Could not modify commit: mzvwutvl 4397373a main | (conflict) merge
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
[EOF]
[exit status: 1]
"#);
// sign
let output = work_dir.run_jj(["sign", "-r=main", "--config=signing.backend=test"]);
insta::assert_snapshot!(output, @r#"

View file

@ -1592,14 +1592,14 @@ fn test_squash_use_destination_message() {
");
}
// The --use-destination-message and --message options are incompatible.
#[test]
fn test_squash_use_destination_message_and_message_mutual_exclusion() {
fn test_squash_option_exclusion() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["commit", "-m=a"]).success();
work_dir.run_jj(["describe", "-m=b"]).success();
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"--message=123",
@ -1614,6 +1614,590 @@ fn test_squash_use_destination_message_and_message_mutual_exclusion() {
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"-r@",
"--into=@-"
]), @r"
------- stderr -------
error: the argument '--revision <REVSET>' cannot be used with '--into <REVSET>'
Usage: jj squash --revision <REVSET> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"-r@",
"--destination=@-"
]), @r"
------- stderr -------
error: the argument '--revision <REVSET>' cannot be used with '--destination <REVSETS>'
Usage: jj squash --revision <REVSET> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"-r@",
"--after=@-"
]), @r"
------- stderr -------
error: the argument '--revision <REVSET>' cannot be used with '--insert-after <REVSETS>'
Usage: jj squash --revision <REVSET> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"-r@",
"--before=@-"
]), @r"
------- stderr -------
error: the argument '--revision <REVSET>' cannot be used with '--insert-before <REVSETS>'
Usage: jj squash --revision <REVSET> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"--destination=@",
"--into=@-"
]), @r"
------- stderr -------
error: the argument '--destination <REVSETS>' cannot be used with '--into <REVSET>'
Usage: jj squash --destination <REVSETS> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"--after=@",
"--into=@-"
]), @r"
------- stderr -------
error: the argument '--insert-after <REVSETS>' cannot be used with '--into <REVSET>'
Usage: jj squash --insert-after <REVSETS> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
insta::assert_snapshot!(work_dir.run_jj([
"squash",
"--before=@-",
"--into=@-"
]), @r"
------- stderr -------
error: the argument '--insert-before <REVSETS>' cannot be used with '--into <REVSET>'
Usage: jj squash --insert-before <REVSETS> [FILESETS]...
For more information, try '--help'.
[EOF]
[exit status: 2]
");
}
#[test]
fn test_squash_to_new_commit() {
let mut test_env = TestEnvironment::default();
let edit_script = test_env.set_up_fake_editor();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file1", "file1\n");
work_dir.run_jj(["commit", "-m", "file1"]).success();
work_dir.write_file("file2", "file2\n");
work_dir.run_jj(["commit", "-m", "file2"]).success();
work_dir.write_file("file3", "file3\n");
work_dir.run_jj(["commit", "-m", "file3"]).success();
work_dir.write_file("file4", "file4\n");
work_dir.run_jj(["commit", "-m", "file4"]).success();
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
zsuskulnrvyr file4
A file4
kkmpptxzrspx file3
A file3
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// insert the commit before the source commit
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--insert-before",
"qpvuntsmwlqt",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit yqosqzyt 12bb5aa1 file 3&4
Rebased 2 descendant commits
Working copy (@) now at: spxsnpux 5b7a2ac3 (empty) (no description set)
Parent commit (@-) : rlvkpnrz 269d92e4 file2
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ spxsnpuxtvxq
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
yqosqzytrlsw file 3&4
A file3
A file4
zzzzzzzzzzzz
[EOF]
");
// insert the commit after a commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--insert-after",
"qpvuntsmwlqt",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit znkkpsqq 71efbc99 file 3&4
Rebased 1 descendant commits
Working copy (@) now at: uuzqqzqu 4a07118a (empty) (no description set)
Parent commit (@-) : rlvkpnrz 800cd9f9 file2
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ uuzqqzquvwzn
rlvkpnrzqnoo file2
A file2
znkkpsqqskkl file 3&4
A file3
A file4
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// insert the commit onto another
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--destination",
"qpvuntsmwlqt",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit wqnwkozp e70a59b7 file 3&4
Working copy (@) now at: mouksmqu ecd9569d (empty) (no description set)
Parent commit (@-) : rlvkpnrz 27974c44 file2
Added 0 files, modified 0 files, removed 2 files
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mouksmquosnp
rlvkpnrzqnoo file2
A file2
wqnwkozpkust file 3&4
A file3
A file4
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// insert the commit after the source commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--insert-after",
"zsuskulnrvyr",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit nkmrtpmo dc2faadd file 3&4
Rebased 1 descendant commits
Working copy (@) now at: ruktrxxu 6d045de7 (empty) (no description set)
Parent commit (@-) : nkmrtpmo dc2faadd file 3&4
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ ruktrxxusqqp
nkmrtpmomlro file 3&4
A file3
A file4
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// insert the commit before the source commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--insert-before",
"zsuskulnrvyr",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit xtnwkqum 342eb9be file 3&4
Rebased 1 descendant commits
Working copy (@) now at: pqrnrkux 4f456097 (empty) (no description set)
Parent commit (@-) : xtnwkqum 342eb9be file 3&4
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ pqrnrkuxnrvz
xtnwkqumpolk file 3&4
A file3
A file4
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// double destination with a commit that will disappear
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-m",
"file 3&4",
"-f",
"kkmpptxzrspx::",
"--destination",
"rlvkpnrzqnoo",
"--destination",
"kkmpptxzrspx",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit wvuyspvk 8a940ae6 file 3&4
Working copy (@) now at: pkynqtxp 09bb6d70 (empty) (no description set)
Parent commit (@-) : rlvkpnrz 27974c44 file2
Added 0 files, modified 0 files, removed 2 files
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ pkynqtxptmyn
wvuyspvkupzz file 3&4
A file3
A file4
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
zzzzzzzzzzzz
[EOF]
");
// creating a new commit should open the editor to write the commit message
work_dir.run_jj(["undo"]).success();
std::fs::write(edit_script, ["dump editor1", "write\nfile 3&4"].join("\0")).unwrap();
let output = work_dir.run_jj([
"squash",
"-f",
"kkmpptxzrspx::zsuskulnrvyr",
"--insert-before",
"qpvuntsmwlqt",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit xlzxqlsl 8ceb6c68 file 3&4
Rebased 3 descendant commits
Working copy (@) now at: mzvwutvl af73b227 (empty) (no description set)
Parent commit (@-) : rlvkpnrz e72015b0 file2
[EOF]
");
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
JJ: Enter a description for the combined commit.
JJ: Description from source commit:
file3
JJ: Description from source commit:
file4
JJ: This commit contains the following changes:
JJ: A file3
JJ: A file4
JJ:
JJ: Lines starting with "JJ:" (like this one) will be removed.
"#);
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
xlzxqlslvuyv file 3&4
A file3
A file4
zzzzzzzzzzzz
[EOF]
");
let output = work_dir.run_jj(["evolog", "-r", "xlzxqlslvuyv"]);
insta::assert_snapshot!(output, @r"
xlzxqlsl test.user@example.com 2001-02-03 08:05:31 8ceb6c68
file 3&4
-- operation bce6018e7161 (2001-02-03 08:05:31) squash commit 0d254956d33ed5bb11d93eb795c5e514aadc81b5 and 1 more
zsuskuln hidden test.user@example.com 2001-02-03 08:05:31 c7946a56
file4
-- operation bce6018e7161 (2001-02-03 08:05:31) squash commit 0d254956d33ed5bb11d93eb795c5e514aadc81b5 and 1 more
zsuskuln hidden test.user@example.com 2001-02-03 08:05:11 38778966
file4
-- operation 83489d186f66 (2001-02-03 08:05:11) commit 89a30a7539466ed176c1ef122a020fd9cb15848e
zsuskuln hidden test.user@example.com 2001-02-03 08:05:11 89a30a75
(no description set)
-- operation e23fd04aab50 (2001-02-03 08:05:11) snapshot working copy
zsuskuln hidden test.user@example.com 2001-02-03 08:05:10 bbf04d26
(empty) (no description set)
-- operation 19d57874b952 (2001-02-03 08:05:10) commit c23c424826221bc4fdee9487926595324e50ee95
kkmpptxz hidden test.user@example.com 2001-02-03 08:05:31 3ab8a4a5
file3
-- operation bce6018e7161 (2001-02-03 08:05:31) squash commit 0d254956d33ed5bb11d93eb795c5e514aadc81b5 and 1 more
kkmpptxz hidden test.user@example.com 2001-02-03 08:05:10 0d254956
file3
-- operation 19d57874b952 (2001-02-03 08:05:10) commit c23c424826221bc4fdee9487926595324e50ee95
kkmpptxz hidden test.user@example.com 2001-02-03 08:05:10 c23c4248
(no description set)
-- operation d19ad3734aa6 (2001-02-03 08:05:10) snapshot working copy
kkmpptxz hidden test.user@example.com 2001-02-03 08:05:09 c1272e87
(empty) (no description set)
-- operation fdee458ae5f2 (2001-02-03 08:05:09) commit cb58ff1c6f1af92f827661e7275941ceb4d910c5
[EOF]
");
// creating a new commit with --use-destination-message shouldn't open the
// editor
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-f",
"kkmpptxzrspx::zsuskulnrvyr",
"--insert-before",
"qpvuntsmwlqt",
"--use-destination-message",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit pkstwlsy 3e0cd203 (no description set)
Rebased 3 descendant commits
Working copy (@) now at: mzvwutvl f52f8d55 (empty) (no description set)
Parent commit (@-) : rlvkpnrz 8cfd575a file2
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
pkstwlsyuyku
A file3
A file4
zzzzzzzzzzzz
[EOF]
");
// squashing 0 sources should create an empty commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-f",
"none()",
"--insert-before",
"qpvuntsmwlqt",
"--use-destination-message",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit zowrlwsv 5feda7c2 (empty) (no description set)
Rebased 5 descendant commits
Working copy (@) now at: mzvwutvl 95edec8e (empty) (no description set)
Parent commit (@-) : zsuskuln 5abf0a51 file4
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
zsuskulnrvyr file4
A file4
kkmpptxzrspx file3
A file3
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
zowrlwsvrkvk
zzzzzzzzzzzz
[EOF]
");
let output = work_dir.run_jj(["evolog", "-r", "zowrlwsvrkvk"]);
insta::assert_snapshot!(output, @r"
zowrlwsv test.user@example.com 2001-02-03 08:05:38 5feda7c2
(empty) (no description set)
-- operation 1b5b7f9b27ba (2001-02-03 08:05:38) squash 0 commits
[EOF]
");
// squashing empty changes should create an empty commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj([
"squash",
"-f",
"kkmpptxzrspx::zsuskulnrvyr",
"--insert-before",
"qpvuntsmwlqt",
"--use-destination-message",
"no file",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit nsrwusvy c2183685 (empty) (no description set)
Rebased 5 descendant commits
Working copy (@) now at: mzvwutvl cb96ecf9 (empty) (no description set)
Parent commit (@-) : zsuskuln 97edce13 file4
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
zsuskulnrvyr file4
A file4
kkmpptxzrspx file3
A file3
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
nsrwusvynpoy
zzzzzzzzzzzz
[EOF]
");
let output = work_dir.run_jj(["evolog", "-r", "nsrwusvynpoy"]);
insta::assert_snapshot!(output, @r"
nsrwusvy test.user@example.com 2001-02-03 08:05:42 c2183685
(empty) (no description set)
-- operation a8d1b9aee58e (2001-02-03 08:05:42) squash commit 0d254956d33ed5bb11d93eb795c5e514aadc81b5 and 1 more
[EOF]
");
// squashing from an empty commit should produce an empty commit
work_dir.run_jj(["undo"]).success();
let output = work_dir.run_jj(["new", "--no-edit", "root()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit wtlqussy 7eff41c8 (empty) (no description set)
[EOF]
");
let output = work_dir.run_jj([
"squash",
"-f",
"wtlqussy",
"--destination",
"root()",
"--use-destination-message",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit ukwxllxp 43a4b8e0 (empty) (no description set)
[EOF]
");
insta::assert_snapshot!(get_log_with_summary(&work_dir), @r"
@ mzvwutvlkqwt
zsuskulnrvyr file4
A file4
kkmpptxzrspx file3
A file3
rlvkpnrzqnoo file2
A file2
qpvuntsmwlqt file1
A file1
ukwxllxpysvw
zzzzzzzzzzzz
[EOF]
");
let output = work_dir.run_jj(["evolog", "-r", "ukwxllxpysvw"]);
insta::assert_snapshot!(output, @r"
ukwxllxp test.user@example.com 2001-02-03 08:05:46 43a4b8e0
(empty) (no description set)
-- operation 11bdcad3854e (2001-02-03 08:05:47) squash commit 7eff41c8d17b8b4d2e7110402719e9d245dba975
wtlqussy hidden test.user@example.com 2001-02-03 08:05:46 7eff41c8
(empty) (no description set)
-- operation b2ee55ac8c70 (2001-02-03 08:05:46) new empty commit
[EOF]
");
}
#[must_use]
@ -1626,3 +2210,9 @@ fn get_log_output_with_description(work_dir: &TestWorkDir) -> CommandOutput {
let template = r#"separate(" ", commit_id.short(), description)"#;
work_dir.run_jj(["log", "-T", template])
}
#[must_use]
fn get_log_with_summary(work_dir: &TestWorkDir) -> CommandOutput {
let template = r#"separate(" ", change_id.short(), local_bookmarks, description)"#;
work_dir.run_jj(["log", "-T", template, "--summary"])
}