This is by analogy with rebase_descendants_with_options, except
we don't need the options.
+19
-4
lib/src/repo.rs
+19
-4
lib/src/repo.rs
···
1406
1406
/// The content of those descendants will remain untouched.
1407
1407
/// Returns the number of reparented descendants.
1408
1408
pub fn reparent_descendants(&mut self) -> BackendResult<usize> {
1409
-
let roots = self.parent_mapping.keys().cloned().collect_vec();
1410
1409
let mut num_reparented = 0;
1410
+
self.reparent_descendants_with_progress(|_, _| {
1411
+
num_reparented += 1;
1412
+
})?;
1413
+
Ok(num_reparented)
1414
+
}
1415
+
1416
+
/// Reparent descendants, and call the provided function for each moved
1417
+
/// commit
1418
+
///
1419
+
/// The function takes the old commit and the reparented commit.
1420
+
pub fn reparent_descendants_with_progress(
1421
+
&mut self,
1422
+
mut progress: impl FnMut(Commit, Commit),
1423
+
) -> BackendResult<()> {
1424
+
let roots = self.parent_mapping.keys().cloned().collect_vec();
1411
1425
self.transform_descendants(roots, |rewriter| {
1412
1426
if rewriter.parents_changed() {
1427
+
let old_commit = rewriter.old_commit().clone();
1413
1428
let builder = rewriter.reparent();
1414
-
builder.write()?;
1415
-
num_reparented += 1;
1429
+
let reparented_commit = builder.write()?;
1430
+
progress(old_commit, reparented_commit);
1416
1431
}
1417
1432
Ok(())
1418
1433
})?;
1419
1434
self.parent_mapping.clear();
1420
-
Ok(num_reparented)
1435
+
Ok(())
1421
1436
}
1422
1437
1423
1438
pub fn set_wc_commit(
+3
CHANGELOG.md
+3
CHANGELOG.md
···
324
324
* The 'how to resolve conflicts' hint that is shown when conflicts appear can
325
325
be hidden by setting `hints.resolving-conflicts = false`.
326
326
327
+
* `jj squash` now has a `--restore-descendants` option to preserve the snapshots
328
+
of the children of the modified commits.
329
+
327
330
* `jj op diff` and `jj op log --op-diff` now show changes to which commits
328
331
correspond to working copies.
329
332
+156
-2
cli/src/commands/squash.rs
+156
-2
cli/src/commands/squash.rs
···
22
22
use jj_lib::repo::Repo as _;
23
23
use jj_lib::rewrite;
24
24
use jj_lib::rewrite::CommitWithSelection;
25
+
use jj_lib::rewrite::SquashOptions;
25
26
use tracing::instrument;
26
27
27
28
use crate::cli_util::CommandHelper;
···
112
113
/// The source revision will not be abandoned
113
114
#[arg(long, short)]
114
115
keep_emptied: bool,
116
+
/// Preserve the content (not the diff) when rebasing descendants of the
117
+
/// source and target commits
118
+
///
119
+
/// Only the snapshots of the `--from` and the `--into` commits will be
120
+
/// modified.
121
+
///
122
+
/// If you'd like to preserve the content of *only* the target's descendants
123
+
/// (or *only* the source's), consider using `jj rebase -r` or `jj
124
+
/// duplicate` before squashing.
125
+
//
126
+
// See "NOTE: Not implementing `--restore-{target,source}-descendants`" in
127
+
// squash.rs.
128
+
//
129
+
// TODO: Once it's implemented, we should recommend `jj rebase -r
130
+
// --restore-descendants` instead of `jj duplicate`, since you actually
131
+
// would need to `squash` twice with `duplicate`.
132
+
#[arg(long)]
133
+
restore_descendants: bool,
115
134
}
116
135
136
+
// NOTE: Not implementing `--restore-{target,source}-descendants`
137
+
// --------------------------------------------------------------
138
+
//
139
+
// We have `jj squash --restore-descendants --from X --into Y` preserve the
140
+
// snapshots of both the descendants of `X` and those of the descendants of `Y`.
141
+
// This behavior makes it simple to understand; it does the same thing to the
142
+
// child of any commit `jj squash` rewrites. As @yuja pointed out it could even
143
+
// be a global flag that would apply to any command that rewrites commits.
144
+
//
145
+
// In this note, we explain why we choose not to have a flag for `jj squash`
146
+
// that preserves *only* the descendants of the source (call it
147
+
// `--restore-source-descendants`) or a similar `--restore-target-descendants`
148
+
// flag, even though they might seem easy to implement at a glance.
149
+
//
150
+
// (The same argument applies to `jj rebase --restore-???-descendants`.)
151
+
//
152
+
// Firstly, such extra flags seem to only be useful in rare cases. If needed,
153
+
// they can be simulated. Instead of `squash --restore-target-descendants`, you
154
+
// could do `jj rebase -r X -d all:X-; jj squash --restore-descendants --from X
155
+
// --into Y`. Instead of `squash --restore-source-descendants`, you could do `jj
156
+
// duplicate -r X; jj squash --restore-descendants --from copy_of_X --into Y; jj
157
+
// abandon --restore-descendants X`. (TODO: When `jj rebase -r
158
+
// --restore-descendants` is implemented, this will become 2 commands instead of
159
+
// 3).
160
+
//
161
+
// Secondly, the behavior of these flags would get confusing in corner cases,
162
+
// when the target is an ancestor or descendant of the source, or for ancestors
163
+
// of merge commits. For example, consider this commit graph with merge commit
164
+
// `Z` where `A` is *not* empty (thanks to @lilyball for suggesting the merge
165
+
// commit example):
166
+
//
167
+
// ```
168
+
// A -> X -
169
+
// \ (Example I)
170
+
// B -> Y --->Z
171
+
// ```
172
+
//
173
+
// The behavior of `jj squash --from A --into B --restore-descendants` is easy
174
+
// to understand: the snapshots of `X` and `Y` remain the same, and all of their
175
+
// descendants also remain the same by normal rebasing rules.
176
+
//
177
+
// If we allowed `jj squash --from A --into B --restore-target-descendants`,
178
+
// what should it mean? It seems clear that `X`'s snapshot should remain the
179
+
// same, and `X`'s will change. However, should `Z`'s snapshot change? If we
180
+
// follow the logic that Z had one of its parents change and the other stay the
181
+
// same, it seems that yes, it should. This is also what the equivalence with
182
+
// `jj rebase -r A -d A-; jj squash --from A --into B --restore-descendants`
183
+
// would imply.
184
+
//
185
+
// (A contrarian mind could argue that `Z`'s snapshot should be preserved since
186
+
// `Z` is a descendant of the target `B`. We'll put this thought aside for a
187
+
// moment and keep going, to see how things get even more confusing.)
188
+
//
189
+
// Now, let's pretend we squashed `X` and `Y` into `Z` and ask the same
190
+
// question. Our graph is now:
191
+
//
192
+
// ```
193
+
// A -
194
+
// \ (Example II)
195
+
// B --->Z
196
+
// ```
197
+
//
198
+
// By the logic above, the snapshot of `Z` will again change after `jj squash
199
+
// --from A --into B --restore-target-descendants`. This is unsatisfying and
200
+
// would probably be unexpected, since `Z` is a direct child of the target
201
+
// commit `B`, so the user might expect its snapshot to be preserved.
202
+
//
203
+
// Now, there are a few options:
204
+
//
205
+
// 1. Allow the confusing but seemingly correct definition of
206
+
// `--restore-target-descendants` as above.
207
+
// 2. Allow `--restore-target-descendants`, but forbid it in some set of
208
+
// situations we deem too confusing.
209
+
// 3. Have the effect of `jj squash --from A --into B
210
+
// --restore-target-descendants` on `Z`'s snapshot differ between Example I
211
+
// and Example II. In other words, the behavior will depend on whether there
212
+
// are commits (even if they are empty commits!) between `A` and `Z`, or
213
+
// between `B` and `Z`.
214
+
// 4. Declare that in both Example I and Example II above, the snapshot of `Z`
215
+
// should be preserved.
216
+
//
217
+
// The first problem with this (and with option 3 above) would be that
218
+
// `--restore-target-descendants` would now be equivalent to a rebase
219
+
// followed by `squash --restore-descendants` *almost* always, but would
220
+
// differ in corner cases.
221
+
//
222
+
// Perhaps more importantly, this would break the important property of `jj
223
+
// squash --restore-target-descendants` that its difference from the
224
+
// behavior of normal `jj squash` is local; affects only the direct children
225
+
// of the modified commits. All others can normally be rebased by normal
226
+
// `jj` rules.
227
+
//
228
+
// If `jj squash --restore-target-descendants` preserved the snapshot of `Z`
229
+
// even if there are 100 commit between it and `A`, this would change its
230
+
// diff relative to its parents, possibly without any awareness from the
231
+
// user that this happened or that `Z` even existed.
232
+
// 5. Do not provide `--restore-target-descendants` ourselves, and recommend
233
+
// that the user manually does `jj rebase -r X -d all:X-; jj squash
234
+
// --restore-descendants --from X --into Y` if they really need it.
235
+
//
236
+
// The last option seems easiest. It also has the advantage of requiring fewer
237
+
// tests and being the simplest to maintain.
238
+
//
239
+
// Aside: the merge example is probably the easiest to understand and the most
240
+
// problematic, but for `X -> A -> B -> C -> D`, both `jj squash --from C
241
+
// --into A --restore-target-descendants` and `jj squash --from A --into C
242
+
// --restore-source-descendants` have similar problems.
243
+
117
244
#[instrument(skip_all)]
118
245
pub(crate) fn cmd_squash(
119
246
ui: &mut Ui,
···
166
293
.check_rewritable(sources.iter().chain(std::iter::once(&destination)).ids())?;
167
294
168
295
let mut tx = workspace_command.start_transaction();
169
-
let tx_description = format!("squash commits into {}", destination.id().hex());
296
+
let tx_description = format!(
297
+
"squash commits into {}{}",
298
+
destination.id().hex(),
299
+
if args.restore_descendants {
300
+
" while preserving descendant contents"
301
+
} else {
302
+
""
303
+
}
304
+
);
170
305
let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
171
306
if let Some(squashed) = rewrite::squash_commits(
172
307
tx.repo_mut(),
173
308
&source_commits,
174
309
&destination,
175
-
args.keep_emptied,
310
+
SquashOptions {
311
+
keep_emptied: args.keep_emptied,
312
+
// See "NOTE: Not implementing `--restore-{target,source}-descendants`" in
313
+
// squash.rs.
314
+
restore_descendants: args.restore_descendants,
315
+
},
176
316
)? {
177
317
let mut commit_builder = squashed.commit_builder.detach();
178
318
let new_description = match description {
···
220
360
};
221
361
commit_builder.set_description(new_description);
222
362
commit_builder.write(tx.repo_mut())?;
363
+
364
+
if args.restore_descendants {
365
+
// If !args.restore_descendants, the corresponding steps are done inside
366
+
// tx.finish()
367
+
let num_reparented = tx.repo_mut().reparent_descendants()?;
368
+
if let Some(mut formatter) = ui.status_formatter() {
369
+
writeln!(
370
+
formatter,
371
+
"Rebased {num_reparented} descendant commits (while preserving their content)",
372
+
)?;
373
+
}
374
+
}
223
375
} else {
224
376
if diff_selector.is_interactive() {
225
377
return Err(user_error("No changes selected"));
···
241
393
}
242
394
}
243
395
}
396
+
// TODO: Show the "Rebase NNN descendant commits message", add " (while
397
+
// preserving their content)" in the --restore-descendants mode
244
398
tx.finish(ui, tx_description)?;
245
399
Ok(())
246
400
}
+5
cli/tests/cli-reference@.md.snap
+5
cli/tests/cli-reference@.md.snap
···
2524
2524
* `-i`, `--interactive` — Interactively choose which parts to squash
2525
2525
* `--tool <NAME>` — Specify diff editor to be used (implies --interactive)
2526
2526
* `-k`, `--keep-emptied` — The source revision will not be abandoned
2527
+
* `--restore-descendants` — Preserve the content (not the diff) when rebasing descendants of the source and target commits
2528
+
2529
+
Only the snapshots of the `--from` and the `--into` commits will be modified.
2530
+
2531
+
If you'd like to preserve the content of *only* the target's descendants (or *only* the source's), consider using `jj rebase -r` or `jj duplicate` before squashing.
2527
2532
2528
2533
2529
2534
+775
cli/tests/test_squash_command.rs
+775
cli/tests/test_squash_command.rs
···
746
746
}
747
747
748
748
#[test]
749
+
fn test_squash_working_copy_restore_descendants() {
750
+
let test_env = TestEnvironment::default();
751
+
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
752
+
let work_dir = test_env.work_dir("repo");
753
+
754
+
// Create history like this:
755
+
// Y
756
+
// |
757
+
// B X@
758
+
// |/
759
+
// A
760
+
//
761
+
// Each commit adds a file named the same as the commit
762
+
let create_commit = |name: &str| {
763
+
work_dir
764
+
.run_jj(["bookmark", "create", "-r@", name])
765
+
.success();
766
+
work_dir.write_file(name, format!("test {name}\n"));
767
+
};
768
+
769
+
create_commit("a");
770
+
work_dir.run_jj(["new"]).success();
771
+
create_commit("b");
772
+
work_dir.run_jj(["new", "a"]).success();
773
+
create_commit("x");
774
+
work_dir.run_jj(["new"]).success();
775
+
create_commit("y");
776
+
work_dir.run_jj(["edit", "x"]).success();
777
+
778
+
let template = r#"separate(
779
+
" ",
780
+
commit_id.short(),
781
+
bookmarks,
782
+
description,
783
+
if(empty, "(empty)")
784
+
)"#;
785
+
let run_log = || work_dir.run_jj(["log", "-r=::", "--summary", "-T", template]);
786
+
787
+
// Verify the setup
788
+
insta::assert_snapshot!(run_log(), @r"
789
+
○ 3f45d7a3ae69 y
790
+
│ A y
791
+
@ 5b4046443e64 x
792
+
│ A x
793
+
│ ○ b1e1eea2f666 b
794
+
├─╯ A b
795
+
○ 7468364c89fc a
796
+
│ A a
797
+
◆ 000000000000 (empty)
798
+
[EOF]
799
+
");
800
+
let output = work_dir.run_jj(["file", "list", "-r=a"]);
801
+
insta::assert_snapshot!(output, @r"
802
+
a
803
+
[EOF]
804
+
");
805
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
806
+
insta::assert_snapshot!(output, @r"
807
+
a
808
+
b
809
+
[EOF]
810
+
");
811
+
let output = work_dir.run_jj(["file", "list"]);
812
+
insta::assert_snapshot!(output, @r"
813
+
a
814
+
x
815
+
[EOF]
816
+
");
817
+
let output = work_dir.run_jj(["file", "list", "-r=y"]);
818
+
insta::assert_snapshot!(output, @r"
819
+
a
820
+
x
821
+
y
822
+
[EOF]
823
+
");
824
+
825
+
let output = work_dir.run_jj(["squash", "--restore-descendants"]);
826
+
insta::assert_snapshot!(output, @r"
827
+
------- stderr -------
828
+
Rebased 2 descendant commits (while preserving their content)
829
+
Working copy (@) now at: kxryzmor 7ec5499d (empty) (no description set)
830
+
Parent commit (@-) : qpvuntsm 1c6a069e a x | (no description set)
831
+
[EOF]
832
+
");
833
+
insta::assert_snapshot!(run_log(), @r"
834
+
@ 7ec5499d9141 (empty)
835
+
│ ○ ddfef0b279f8 y
836
+
├─╯ A y
837
+
│ ○ 640ba5e85507 b
838
+
├─╯ A b
839
+
│ D x
840
+
○ 1c6a069ec7e3 a x
841
+
│ A a
842
+
│ A x
843
+
◆ 000000000000 (empty)
844
+
[EOF]
845
+
");
846
+
847
+
let output = work_dir.run_jj(["diff", "--summary"]);
848
+
// The current commit becomes empty.
849
+
insta::assert_snapshot!(output, @"");
850
+
// Should coincide with the working copy commit before
851
+
let output = work_dir.run_jj(["file", "list", "-r=a"]);
852
+
insta::assert_snapshot!(output, @r"
853
+
a
854
+
x
855
+
[EOF]
856
+
");
857
+
// Commit b should be the same as before
858
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
859
+
insta::assert_snapshot!(output, @r"
860
+
a
861
+
b
862
+
[EOF]
863
+
");
864
+
let output = work_dir.run_jj(["file", "list", "-r=y"]);
865
+
insta::assert_snapshot!(output, @r"
866
+
a
867
+
x
868
+
y
869
+
[EOF]
870
+
");
871
+
}
872
+
873
+
#[test]
874
+
fn test_squash_from_to_restore_descendants() {
875
+
let test_env = TestEnvironment::default();
876
+
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
877
+
let work_dir = test_env.work_dir("repo");
878
+
879
+
// Create history like this:
880
+
// F
881
+
// |\
882
+
// E C
883
+
// | |
884
+
// D B
885
+
// |/
886
+
// A
887
+
//
888
+
// Each commit adds a file named the same as the commit
889
+
let create_commit = |name: &str| {
890
+
work_dir
891
+
.run_jj(["bookmark", "create", "-r@", name])
892
+
.success();
893
+
work_dir.write_file(name, format!("test {name}\n"));
894
+
};
895
+
896
+
create_commit("a");
897
+
work_dir.run_jj(["new"]).success();
898
+
create_commit("b");
899
+
work_dir.run_jj(["new"]).success();
900
+
create_commit("c");
901
+
work_dir.run_jj(["new", "a"]).success();
902
+
create_commit("d");
903
+
work_dir.run_jj(["new"]).success();
904
+
create_commit("e");
905
+
work_dir.run_jj(["new", "e", "c"]).success();
906
+
create_commit("f");
907
+
908
+
let template = r#"separate(
909
+
" ",
910
+
commit_id.short(),
911
+
bookmarks,
912
+
description,
913
+
if(empty, "(empty)")
914
+
)"#;
915
+
let run_log = || work_dir.run_jj(["log", "-r=::", "--summary", "-T", template]);
916
+
917
+
// ========== Part 1 =========
918
+
// Verify the setup
919
+
insta::assert_snapshot!(run_log(), @r"
920
+
@ 42acd0537c88 f
921
+
├─╮ A f
922
+
│ ○ 4fb9706b0f47 c
923
+
│ │ A c
924
+
│ ○ b1e1eea2f666 b
925
+
│ │ A b
926
+
○ │ b4e3197108ba e
927
+
│ │ A e
928
+
○ │ d707102f499f d
929
+
├─╯ A d
930
+
○ 7468364c89fc a
931
+
│ A a
932
+
◆ 000000000000 (empty)
933
+
[EOF]
934
+
");
935
+
let beginning = work_dir.current_operation_id();
936
+
test_env.advance_test_rng_seed_to_multiple_of(200_000);
937
+
938
+
// Squash without --restore-descendants for comparison
939
+
work_dir
940
+
.run_jj(["operation", "restore", &beginning])
941
+
.success();
942
+
let output = work_dir.run_jj(["squash", "--from=b", "--into=d"]);
943
+
insta::assert_snapshot!(output, @r"
944
+
------- stderr -------
945
+
Rebased 3 descendant commits
946
+
Working copy (@) now at: kpqxywon e462100a f | (no description set)
947
+
Parent commit (@-) : yostqsxw 6944fd03 e | (no description set)
948
+
Parent commit (@-) : mzvwutvl 6cd5d5c1 c | (no description set)
949
+
[EOF]
950
+
");
951
+
insta::assert_snapshot!(run_log(), @r"
952
+
@ e462100ae7c3 f
953
+
├─╮ A f
954
+
│ ○ 6cd5d5c1daf7 c
955
+
│ │ A c
956
+
○ │ 6944fd03dc5d e
957
+
│ │ A e
958
+
○ │ 1befcf027d1b d
959
+
├─╯ A b
960
+
│ A d
961
+
○ 7468364c89fc a b
962
+
│ A a
963
+
◆ 000000000000 (empty)
964
+
[EOF]
965
+
");
966
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
967
+
insta::assert_snapshot!(output, @r"
968
+
a
969
+
b
970
+
d
971
+
[EOF]
972
+
");
973
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
974
+
insta::assert_snapshot!(output, @r"
975
+
a
976
+
c
977
+
[EOF]
978
+
");
979
+
let output = work_dir.run_jj(["file", "list", "-r=e"]);
980
+
insta::assert_snapshot!(output, @r"
981
+
a
982
+
b
983
+
d
984
+
e
985
+
[EOF]
986
+
");
987
+
let output = work_dir.run_jj(["file", "list", "-r=f"]);
988
+
insta::assert_snapshot!(output, @r"
989
+
a
990
+
b
991
+
c
992
+
d
993
+
e
994
+
f
995
+
[EOF]
996
+
");
997
+
998
+
// --restore-descendants
999
+
work_dir
1000
+
.run_jj(["operation", "restore", &beginning])
1001
+
.success();
1002
+
let output = work_dir.run_jj(["squash", "--from=b", "--into=d", "--restore-descendants"]);
1003
+
insta::assert_snapshot!(output, @r"
1004
+
------- stderr -------
1005
+
Rebased 3 descendant commits (while preserving their content)
1006
+
Working copy (@) now at: kpqxywon 1d64ccbf f | (no description set)
1007
+
Parent commit (@-) : yostqsxw cb90d752 e | (no description set)
1008
+
Parent commit (@-) : mzvwutvl 4e6702ae c | (no description set)
1009
+
[EOF]
1010
+
");
1011
+
// `d`` becomes the same as in the above example,
1012
+
// but `c` does not lose file `b` and `e` still does not contain file `b`
1013
+
// regardless of what happened to their parents.
1014
+
insta::assert_snapshot!(run_log(), @r"
1015
+
@ 1d64ccbf4608 f
1016
+
├─╮ A f
1017
+
│ ○ 4e6702ae494c c
1018
+
│ │ A b
1019
+
│ │ A c
1020
+
○ │ cb90d75271b4 e
1021
+
│ │ D b
1022
+
│ │ A e
1023
+
○ │ 853ea07451aa d
1024
+
├─╯ A b
1025
+
│ A d
1026
+
○ 7468364c89fc a b
1027
+
│ A a
1028
+
◆ 000000000000 (empty)
1029
+
[EOF]
1030
+
");
1031
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1032
+
insta::assert_snapshot!(output, @r"
1033
+
a
1034
+
b
1035
+
d
1036
+
[EOF]
1037
+
");
1038
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1039
+
insta::assert_snapshot!(output, @r"
1040
+
a
1041
+
b
1042
+
c
1043
+
[EOF]
1044
+
");
1045
+
let output = work_dir.run_jj(["file", "list", "-r=e"]);
1046
+
insta::assert_snapshot!(output, @r"
1047
+
a
1048
+
d
1049
+
e
1050
+
[EOF]
1051
+
");
1052
+
let output = work_dir.run_jj(["file", "list", "-r=f"]);
1053
+
insta::assert_snapshot!(output, @r"
1054
+
a
1055
+
b
1056
+
c
1057
+
d
1058
+
e
1059
+
f
1060
+
[EOF]
1061
+
");
1062
+
1063
+
// --restore-descendants works with --keep-emptied, same result except for
1064
+
// leaving an empty commit
1065
+
work_dir
1066
+
.run_jj(["operation", "restore", &beginning])
1067
+
.success();
1068
+
let output = work_dir.run_jj([
1069
+
"squash",
1070
+
"--from=b",
1071
+
"--into=d",
1072
+
"--restore-descendants",
1073
+
"--keep-emptied",
1074
+
]);
1075
+
insta::assert_snapshot!(output, @r"
1076
+
------- stderr -------
1077
+
Rebased 3 descendant commits (while preserving their content)
1078
+
Working copy (@) now at: kpqxywon 3c13920f f | (no description set)
1079
+
Parent commit (@-) : yostqsxw aa73012d e | (no description set)
1080
+
Parent commit (@-) : mzvwutvl d323deaa c | (no description set)
1081
+
[EOF]
1082
+
");
1083
+
// `d`` becomes the same as in the above example,
1084
+
// but `c` does not lose file `b` and `e` still does not contain file `b`
1085
+
// regardless of what happened to their parents.
1086
+
insta::assert_snapshot!(run_log(), @r"
1087
+
@ 3c13920f1e9a f
1088
+
├─╮ A f
1089
+
│ ○ d323deaa04c2 c
1090
+
│ │ A b
1091
+
│ │ A c
1092
+
│ ○ a55451e8808f b (empty)
1093
+
○ │ aa73012df9cd e
1094
+
│ │ D b
1095
+
│ │ A e
1096
+
○ │ d00e73142243 d
1097
+
├─╯ A b
1098
+
│ A d
1099
+
○ 7468364c89fc a
1100
+
│ A a
1101
+
◆ 000000000000 (empty)
1102
+
[EOF]
1103
+
");
1104
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1105
+
insta::assert_snapshot!(output, @r"
1106
+
a
1107
+
b
1108
+
d
1109
+
[EOF]
1110
+
");
1111
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1112
+
insta::assert_snapshot!(output, @r"
1113
+
a
1114
+
b
1115
+
c
1116
+
[EOF]
1117
+
");
1118
+
let output = work_dir.run_jj(["file", "list", "-r=e"]);
1119
+
insta::assert_snapshot!(output, @r"
1120
+
a
1121
+
d
1122
+
e
1123
+
[EOF]
1124
+
");
1125
+
1126
+
// ========== Part 2 =========
1127
+
// Reminder of the setup
1128
+
test_env.advance_test_rng_seed_to_multiple_of(200_000);
1129
+
work_dir
1130
+
.run_jj(["operation", "restore", &beginning])
1131
+
.success();
1132
+
insta::assert_snapshot!(run_log(), @r"
1133
+
@ 42acd0537c88 f
1134
+
├─╮ A f
1135
+
│ ○ 4fb9706b0f47 c
1136
+
│ │ A c
1137
+
│ ○ b1e1eea2f666 b
1138
+
│ │ A b
1139
+
○ │ b4e3197108ba e
1140
+
│ │ A e
1141
+
○ │ d707102f499f d
1142
+
├─╯ A d
1143
+
○ 7468364c89fc a
1144
+
│ A a
1145
+
◆ 000000000000 (empty)
1146
+
[EOF]
1147
+
");
1148
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1149
+
insta::assert_snapshot!(output, @r"
1150
+
a
1151
+
b
1152
+
c
1153
+
[EOF]
1154
+
");
1155
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1156
+
insta::assert_snapshot!(output, @r"
1157
+
a
1158
+
d
1159
+
[EOF]
1160
+
");
1161
+
1162
+
// --restore-descendants works when squashing from parent to child
1163
+
work_dir
1164
+
.run_jj(["operation", "restore", &beginning])
1165
+
.success();
1166
+
let output = work_dir.run_jj(["squash", "--from=a", "--into=b", "--restore-descendants"]);
1167
+
insta::assert_snapshot!(output, @r"
1168
+
------- stderr -------
1169
+
Rebased 5 descendant commits (while preserving their content)
1170
+
Working copy (@) now at: kpqxywon 27d75f43 f | (no description set)
1171
+
Parent commit (@-) : yostqsxw 102e6106 e | (no description set)
1172
+
Parent commit (@-) : mzvwutvl 86d2ecde c | (no description set)
1173
+
[EOF]
1174
+
");
1175
+
insta::assert_snapshot!(run_log(), @r"
1176
+
@ 27d75f43e860 f
1177
+
├─╮ A f
1178
+
│ ○ 86d2ecdec2d7 c
1179
+
│ │ A c
1180
+
│ ○ 7c3b32b0545d b
1181
+
│ │ A a
1182
+
│ │ A b
1183
+
○ │ 102e61065eb2 e
1184
+
│ │ A e
1185
+
○ │ 7b1493a2027e d
1186
+
├─╯ A a
1187
+
│ A d
1188
+
◆ 000000000000 a (empty)
1189
+
[EOF]
1190
+
");
1191
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1192
+
insta::assert_snapshot!(output, @r"
1193
+
a
1194
+
b
1195
+
[EOF]
1196
+
");
1197
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1198
+
insta::assert_snapshot!(output, @r"
1199
+
a
1200
+
b
1201
+
c
1202
+
[EOF]
1203
+
");
1204
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1205
+
insta::assert_snapshot!(output, @r"
1206
+
a
1207
+
d
1208
+
[EOF]
1209
+
");
1210
+
1211
+
// --restore-descendants --keep-emptied works when squashing from parent to
1212
+
// child
1213
+
work_dir
1214
+
.run_jj(["operation", "restore", &beginning])
1215
+
.success();
1216
+
let output = work_dir.run_jj([
1217
+
"squash",
1218
+
"--from=a",
1219
+
"--into=b",
1220
+
"--restore-descendants",
1221
+
"--keep-emptied",
1222
+
]);
1223
+
insta::assert_snapshot!(output, @r"
1224
+
------- stderr -------
1225
+
Rebased 5 descendant commits (while preserving their content)
1226
+
Working copy (@) now at: kpqxywon a6c6eeb5 f | (no description set)
1227
+
Parent commit (@-) : yostqsxw c20a2a7a e | (no description set)
1228
+
Parent commit (@-) : mzvwutvl 5230f5a0 c | (no description set)
1229
+
[EOF]
1230
+
");
1231
+
insta::assert_snapshot!(run_log(), @r"
1232
+
@ a6c6eeb5767f f
1233
+
├─╮ A f
1234
+
│ ○ 5230f5a06e69 c
1235
+
│ │ A c
1236
+
│ ○ 5d6fef1e0e34 b
1237
+
│ │ A a
1238
+
│ │ A b
1239
+
○ │ c20a2a7a24ba e
1240
+
│ │ A e
1241
+
○ │ a224ba6ebde8 d
1242
+
├─╯ A a
1243
+
│ A d
1244
+
○ 367fe826e43e a (empty)
1245
+
◆ 000000000000 (empty)
1246
+
[EOF]
1247
+
");
1248
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1249
+
insta::assert_snapshot!(output, @r"
1250
+
a
1251
+
b
1252
+
[EOF]
1253
+
");
1254
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1255
+
insta::assert_snapshot!(output, @r"
1256
+
a
1257
+
b
1258
+
c
1259
+
[EOF]
1260
+
");
1261
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1262
+
insta::assert_snapshot!(output, @r"
1263
+
a
1264
+
d
1265
+
[EOF]
1266
+
");
1267
+
1268
+
// --restore-descendants works when squashing from child to parent
1269
+
work_dir
1270
+
.run_jj(["operation", "restore", &beginning])
1271
+
.success();
1272
+
let output = work_dir.run_jj(["squash", "--from=b", "--into=a", "--restore-descendants"]);
1273
+
insta::assert_snapshot!(output, @r"
1274
+
------- stderr -------
1275
+
Rebased 4 descendant commits (while preserving their content)
1276
+
Working copy (@) now at: kpqxywon 6ad1c62a f | (no description set)
1277
+
Parent commit (@-) : yostqsxw e259f026 e | (no description set)
1278
+
Parent commit (@-) : mzvwutvl 36192c59 c | (no description set)
1279
+
[EOF]
1280
+
");
1281
+
insta::assert_snapshot!(run_log(), @r"
1282
+
@ 6ad1c62aec5b f
1283
+
├─╮ A b
1284
+
│ │ A f
1285
+
│ ○ 36192c59f1e9 c
1286
+
│ │ A c
1287
+
○ │ e259f02633ca e
1288
+
│ │ A e
1289
+
○ │ 92943f1c8204 d
1290
+
├─╯ D b
1291
+
│ A d
1292
+
○ 59aac8514774 a b
1293
+
│ A a
1294
+
│ A b
1295
+
◆ 000000000000 (empty)
1296
+
[EOF]
1297
+
");
1298
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1299
+
insta::assert_snapshot!(output, @r"
1300
+
a
1301
+
b
1302
+
[EOF]
1303
+
");
1304
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1305
+
insta::assert_snapshot!(output, @r"
1306
+
a
1307
+
b
1308
+
c
1309
+
[EOF]
1310
+
");
1311
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1312
+
insta::assert_snapshot!(output, @r"
1313
+
a
1314
+
d
1315
+
[EOF]
1316
+
");
1317
+
1318
+
// same test, but with --keep-emptied
1319
+
work_dir
1320
+
.run_jj(["operation", "restore", &beginning])
1321
+
.success();
1322
+
let output = work_dir.run_jj([
1323
+
"squash",
1324
+
"--from=b",
1325
+
"--into=a",
1326
+
"--keep-emptied",
1327
+
"--restore-descendants",
1328
+
]);
1329
+
insta::assert_snapshot!(output, @r"
1330
+
------- stderr -------
1331
+
Rebased 5 descendant commits (while preserving their content)
1332
+
Working copy (@) now at: kpqxywon 6eadede0 f | (no description set)
1333
+
Parent commit (@-) : yostqsxw 97233b50 e | (no description set)
1334
+
Parent commit (@-) : mzvwutvl 5b2d6858 c | (no description set)
1335
+
[EOF]
1336
+
");
1337
+
// BUG! b should now be empty!
1338
+
insta::assert_snapshot!(run_log(), @r"
1339
+
@ 6eadede086b1 f
1340
+
├─╮ A b
1341
+
│ │ A f
1342
+
│ ○ 5b2d685868b7 c
1343
+
│ │ A b
1344
+
│ │ A c
1345
+
│ ○ 904dac9cd09e b
1346
+
│ │ D b
1347
+
○ │ 97233b506c11 e
1348
+
│ │ A e
1349
+
○ │ 8cbe1a629aed d
1350
+
├─╯ D b
1351
+
│ A d
1352
+
○ c1fbbbe74a28 a
1353
+
│ A a
1354
+
│ A b
1355
+
◆ 000000000000 (empty)
1356
+
[EOF]
1357
+
");
1358
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1359
+
insta::assert_snapshot!(output, @r"
1360
+
a
1361
+
[EOF]
1362
+
");
1363
+
let output = work_dir.run_jj(["file", "list", "-r=c"]);
1364
+
insta::assert_snapshot!(output, @r"
1365
+
a
1366
+
b
1367
+
c
1368
+
[EOF]
1369
+
");
1370
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1371
+
insta::assert_snapshot!(output, @r"
1372
+
a
1373
+
d
1374
+
[EOF]
1375
+
");
1376
+
1377
+
// ========== Part 3 =========
1378
+
// Reminder of the setup
1379
+
test_env.advance_test_rng_seed_to_multiple_of(200_000);
1380
+
work_dir
1381
+
.run_jj(["operation", "restore", &beginning])
1382
+
.success();
1383
+
insta::assert_snapshot!(run_log(), @r"
1384
+
@ 42acd0537c88 f
1385
+
├─╮ A f
1386
+
│ ○ 4fb9706b0f47 c
1387
+
│ │ A c
1388
+
│ ○ b1e1eea2f666 b
1389
+
│ │ A b
1390
+
○ │ b4e3197108ba e
1391
+
│ │ A e
1392
+
○ │ d707102f499f d
1393
+
├─╯ A d
1394
+
○ 7468364c89fc a
1395
+
│ A a
1396
+
◆ 000000000000 (empty)
1397
+
[EOF]
1398
+
");
1399
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1400
+
insta::assert_snapshot!(output, @r"
1401
+
a
1402
+
d
1403
+
[EOF]
1404
+
");
1405
+
let output = work_dir.run_jj(["file", "list", "-r=f"]);
1406
+
insta::assert_snapshot!(output, @r"
1407
+
a
1408
+
b
1409
+
c
1410
+
d
1411
+
e
1412
+
f
1413
+
[EOF]
1414
+
");
1415
+
1416
+
// --restore-descendants works when squashing from grandchild to grandparent
1417
+
work_dir
1418
+
.run_jj(["operation", "restore", &beginning])
1419
+
.success();
1420
+
let output = work_dir.run_jj(["squash", "--from=e", "--into=a", "--restore-descendants"]);
1421
+
insta::assert_snapshot!(output, @r"
1422
+
------- stderr -------
1423
+
Rebased 4 descendant commits (while preserving their content)
1424
+
Working copy (@) now at: kpqxywon 6d14c928 f | (no description set)
1425
+
Parent commit (@-) : yqosqzyt ab775412 d e | (no description set)
1426
+
Parent commit (@-) : mzvwutvl 175aa1f2 c | (no description set)
1427
+
[EOF]
1428
+
");
1429
+
insta::assert_snapshot!(run_log(), @r"
1430
+
@ 6d14c928f32e f
1431
+
├─╮ A e
1432
+
│ │ A f
1433
+
│ ○ 175aa1f28a05 c
1434
+
│ │ A c
1435
+
│ ○ d1076aeca3e6 b
1436
+
│ │ A b
1437
+
│ │ D e
1438
+
○ │ ab7754126332 d e
1439
+
├─╯ A d
1440
+
│ D e
1441
+
○ 4644e0c16443 a
1442
+
│ A a
1443
+
│ A e
1444
+
◆ 000000000000 (empty)
1445
+
[EOF]
1446
+
");
1447
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1448
+
insta::assert_snapshot!(output, @r"
1449
+
a
1450
+
b
1451
+
[EOF]
1452
+
");
1453
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1454
+
insta::assert_snapshot!(output, @r"
1455
+
a
1456
+
d
1457
+
[EOF]
1458
+
");
1459
+
let output = work_dir.run_jj(["file", "list", "-r=f"]);
1460
+
insta::assert_snapshot!(output, @r"
1461
+
a
1462
+
b
1463
+
c
1464
+
d
1465
+
e
1466
+
f
1467
+
[EOF]
1468
+
");
1469
+
1470
+
// --restore-descendants works when squashing from grandparent to grandchild
1471
+
work_dir
1472
+
.run_jj(["operation", "restore", &beginning])
1473
+
.success();
1474
+
let output = work_dir.run_jj(["squash", "--from=a", "--into=e", "--restore-descendants"]);
1475
+
insta::assert_snapshot!(output, @r"
1476
+
------- stderr -------
1477
+
Rebased 5 descendant commits (while preserving their content)
1478
+
Working copy (@) now at: kpqxywon e92b3f0f f | (no description set)
1479
+
Parent commit (@-) : yostqsxw 78651b37 e | (no description set)
1480
+
Parent commit (@-) : mzvwutvl 2214436c c | (no description set)
1481
+
[EOF]
1482
+
");
1483
+
insta::assert_snapshot!(run_log(), @r"
1484
+
@ e92b3f0fb9fe f
1485
+
├─╮ A f
1486
+
│ ○ 2214436c3fa7 c
1487
+
│ │ A c
1488
+
│ ○ a469c893f362 b
1489
+
│ │ A a
1490
+
│ │ A b
1491
+
○ │ 78651b37e114 e
1492
+
│ │ A e
1493
+
○ │ 93671eb30330 d
1494
+
├─╯ A a
1495
+
│ A d
1496
+
◆ 000000000000 a (empty)
1497
+
[EOF]
1498
+
");
1499
+
let output = work_dir.run_jj(["file", "list", "-r=b"]);
1500
+
insta::assert_snapshot!(output, @r"
1501
+
a
1502
+
b
1503
+
[EOF]
1504
+
");
1505
+
let output = work_dir.run_jj(["file", "list", "-r=d"]);
1506
+
insta::assert_snapshot!(output, @r"
1507
+
a
1508
+
d
1509
+
[EOF]
1510
+
");
1511
+
let output = work_dir.run_jj(["file", "list", "-r=f"]);
1512
+
insta::assert_snapshot!(output, @r"
1513
+
a
1514
+
b
1515
+
c
1516
+
d
1517
+
e
1518
+
f
1519
+
[EOF]
1520
+
");
1521
+
}
1522
+
1523
+
#[test]
749
1524
fn test_squash_from_multiple() {
750
1525
let test_env = TestEnvironment::default();
+19
-8
lib/src/rewrite.rs
+19
-8
lib/src/rewrite.rs
···
1109
1109
pub abandoned_commits: Vec<Commit>,
1110
1110
}
1111
1111
1112
+
#[derive(Clone, Debug)]
1113
+
pub struct SquashOptions {
1114
+
pub keep_emptied: bool,
1115
+
pub restore_descendants: bool,
1116
+
}
1117
+
1112
1118
/// Squash `sources` into `destination` and return a [`SquashedCommit`] for the
1113
1119
/// resulting commit. Caller is responsible for setting the description and
1114
1120
/// finishing the commit.
···
1116
1122
repo: &'repo mut MutableRepo,
1117
1123
sources: &[CommitWithSelection],
1118
1124
destination: &Commit,
1119
-
keep_emptied: bool,
1125
+
SquashOptions {
1126
+
keep_emptied,
1127
+
restore_descendants,
1128
+
}: SquashOptions,
1120
1129
) -> BackendResult<Option<SquashedCommit<'repo>>> {
1121
1130
struct SourceCommit<'a> {
1122
1131
commit: &'a CommitWithSelection,
···
1158
1167
1159
1168
1160
1169
1170
+
}
1161
1171
1162
-
1163
-
1164
-
1165
-
1166
-
1167
-
1168
-
1172
+
let mut rewritten_destination = destination.clone();
1173
+
if !restore_descendants
1174
+
&& sources.iter().any(|source| {
1175
+
repo.index()
1176
+
.is_ancestor(source.commit.id(), destination.id())
1177
+
})
1178
+
{
1179
+
// If we're moving changes to a descendant, first rebase descendants onto the
1169
1180
// rewritten sources. Otherwise it will likely already have the content
1170
1181
// changes we're moving, so applying them will have no effect and the
1171
1182
// changes will disappear.
History
1 round
0 comments
ilyagr.bsky.social
submitted
#0
3 commits
expand
collapse
repo: create a reparent_descendants_with_progress helper (no-op)
This is by analogy with `rebase_descendants_with_options`, except
we don't need the options.
cli
squash: new --restore-descendants option
This option is already implemented for `abandon`, `restore`, `diffedit`.
Includes [GHC-style note] explaining why a single option for restoring *both*
source and target descendants is sufficient and simplest to understand in corner
cases. Currently, this note is only relevant to `squash.rs`, but it
would also be referenced in `jj rebase` and `jj duplicate` once those
gain `--restore-descendants`.
[GHC-style note]: https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/coding-style#2-using-notes
Fixes #6000
no-restore-descendants
no conflicts, ready to merge