mirror of
https://github.com/jj-vcs/jj.git
synced 2025-12-23 06:01:01 +00:00
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:
parent
e8f91696fe
commit
1e58ca2253
6 changed files with 806 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue