just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::path::Path;
16use std::path::PathBuf;
17
18use crate::common::CommandOutput;
19use crate::common::TestEnvironment;
20
21#[test]
22fn test_squash() {
23 let test_env = TestEnvironment::default();
24 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
25 let repo_path = test_env.env_root().join("repo");
26
27 test_env
28 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
29 .success();
30 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
31 test_env.run_jj_in(&repo_path, ["new"]).success();
32 test_env
33 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
34 .success();
35 std::fs::write(repo_path.join("file1"), "b\n").unwrap();
36 test_env.run_jj_in(&repo_path, ["new"]).success();
37 test_env
38 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
39 .success();
40 std::fs::write(repo_path.join("file1"), "c\n").unwrap();
41 // Test the setup
42 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
43 @ 382c9bad7d42 c
44 ○ d5d59175b481 b
45 ○ 184ddbcce5a9 a
46 ◆ 000000000000 (empty)
47 [EOF]
48 ");
49
50 // Squashes the working copy into the parent by default
51 let output = test_env.run_jj_in(&repo_path, ["squash"]);
52 insta::assert_snapshot!(output, @r"
53 ------- stderr -------
54 Working copy (@) now at: vruxwmqv f7bb78d8 (empty) (no description set)
55 Parent commit (@-) : kkmpptxz 59f44460 b c | (no description set)
56 [EOF]
57 ");
58 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
59 @ f7bb78d8da62 (empty)
60 ○ 59f4446070a0 b c
61 ○ 184ddbcce5a9 a
62 ◆ 000000000000 (empty)
63 [EOF]
64 ");
65 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
66 insta::assert_snapshot!(output, @r"
67 c
68 [EOF]
69 ");
70
71 // Can squash a given commit into its parent
72 test_env.run_jj_in(&repo_path, ["undo"]).success();
73 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b"]);
74 insta::assert_snapshot!(output, @r"
75 ------- stderr -------
76 Rebased 1 descendant commits
77 Working copy (@) now at: mzvwutvl 1d70f50a c | (no description set)
78 Parent commit (@-) : qpvuntsm 9146bcc8 a b | (no description set)
79 [EOF]
80 ");
81 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
82 @ 1d70f50afa6d c
83 ○ 9146bcc8d996 a b
84 ◆ 000000000000 (empty)
85 [EOF]
86 ");
87 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "b"]);
88 insta::assert_snapshot!(output, @r"
89 b
90 [EOF]
91 ");
92 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
93 insta::assert_snapshot!(output, @r"
94 c
95 [EOF]
96 ");
97
98 // Cannot squash a merge commit (because it's unclear which parent it should go
99 // into)
100 test_env.run_jj_in(&repo_path, ["undo"]).success();
101 test_env.run_jj_in(&repo_path, ["edit", "b"]).success();
102 test_env.run_jj_in(&repo_path, ["new"]).success();
103 test_env
104 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "d"])
105 .success();
106 std::fs::write(repo_path.join("file2"), "d\n").unwrap();
107 test_env.run_jj_in(&repo_path, ["new", "c", "d"]).success();
108 test_env
109 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "e"])
110 .success();
111 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
112 @ 41219719ab5f e (empty)
113 ├─╮
114 │ ○ f86e2b3af3e3 d
115 ○ │ 382c9bad7d42 c
116 ├─╯
117 ○ d5d59175b481 b
118 ○ 184ddbcce5a9 a
119 ◆ 000000000000 (empty)
120 [EOF]
121 ");
122 let output = test_env.run_jj_in(&repo_path, ["squash"]);
123 insta::assert_snapshot!(output, @r"
124 ------- stderr -------
125 Error: Cannot squash merge commits without a specified destination
126 Hint: Use `--into` to specify which parent to squash into
127 [EOF]
128 [exit status: 1]
129 ");
130
131 // Can squash into a merge commit
132 test_env.run_jj_in(&repo_path, ["new", "e"]).success();
133 std::fs::write(repo_path.join("file1"), "e\n").unwrap();
134 let output = test_env.run_jj_in(&repo_path, ["squash"]);
135 insta::assert_snapshot!(output, @r"
136 ------- stderr -------
137 Working copy (@) now at: xlzxqlsl b50b843d (empty) (no description set)
138 Parent commit (@-) : nmzmmopx 338cbc05 e | (no description set)
139 [EOF]
140 ");
141 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
142 @ b50b843d8555 (empty)
143 ○ 338cbc05e4e6 e
144 ├─╮
145 │ ○ f86e2b3af3e3 d
146 ○ │ 382c9bad7d42 c
147 ├─╯
148 ○ d5d59175b481 b
149 ○ 184ddbcce5a9 a
150 ◆ 000000000000 (empty)
151 [EOF]
152 ");
153 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "e"]);
154 insta::assert_snapshot!(output, @r"
155 e
156 [EOF]
157 ");
158}
159
160#[test]
161fn test_squash_partial() {
162 let mut test_env = TestEnvironment::default();
163 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
164 let repo_path = test_env.env_root().join("repo");
165
166 test_env
167 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
168 .success();
169 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
170 std::fs::write(repo_path.join("file2"), "a\n").unwrap();
171 test_env.run_jj_in(&repo_path, ["new"]).success();
172 test_env
173 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
174 .success();
175 std::fs::write(repo_path.join("file1"), "b\n").unwrap();
176 std::fs::write(repo_path.join("file2"), "b\n").unwrap();
177 test_env.run_jj_in(&repo_path, ["new"]).success();
178 test_env
179 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
180 .success();
181 std::fs::write(repo_path.join("file1"), "c\n").unwrap();
182 std::fs::write(repo_path.join("file2"), "c\n").unwrap();
183 // Test the setup
184 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
185 @ a0b1a272ebc4 c
186 ○ d117da276a0f b
187 ○ 54d3c1c0e9fd a
188 ◆ 000000000000 (empty)
189 [EOF]
190 ");
191
192 // If we don't make any changes in the diff-editor, the whole change is moved
193 // into the parent
194 let edit_script = test_env.set_up_fake_diff_editor();
195 std::fs::write(&edit_script, "dump JJ-INSTRUCTIONS instrs").unwrap();
196 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b", "-i"]);
197 insta::assert_snapshot!(output, @r"
198 ------- stderr -------
199 Rebased 1 descendant commits
200 Working copy (@) now at: mzvwutvl 3c633226 c | (no description set)
201 Parent commit (@-) : qpvuntsm 38ffd8b9 a b | (no description set)
202 [EOF]
203 ");
204
205 insta::assert_snapshot!(
206 std::fs::read_to_string(test_env.env_root().join("instrs")).unwrap(), @r"
207 You are moving changes from: kkmpptxz d117da27 b | (no description set)
208 into commit: qpvuntsm 54d3c1c0 a | (no description set)
209
210 The left side of the diff shows the contents of the parent commit. The
211 right side initially shows the contents of the commit you're moving
212 changes from.
213
214 Adjust the right side until the diff shows the changes you want to move
215 to the destination. If you don't make any changes, then all the changes
216 from the source will be moved into the destination.
217 ");
218
219 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
220 @ 3c6332267ea8 c
221 ○ 38ffd8b98578 a b
222 ◆ 000000000000 (empty)
223 [EOF]
224 ");
225 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "a"]);
226 insta::assert_snapshot!(output, @r"
227 b
228 [EOF]
229 ");
230
231 // Can squash only some changes in interactive mode
232 test_env.run_jj_in(&repo_path, ["undo"]).success();
233 std::fs::write(&edit_script, "reset file1").unwrap();
234 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b", "-i"]);
235 insta::assert_snapshot!(output, @r"
236 ------- stderr -------
237 Rebased 2 descendant commits
238 Working copy (@) now at: mzvwutvl 57c3cf20 c | (no description set)
239 Parent commit (@-) : kkmpptxz c4925e01 b | (no description set)
240 [EOF]
241 ");
242 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
243 @ 57c3cf20d0b1 c
244 ○ c4925e01d298 b
245 ○ 1fc159063ed3 a
246 ◆ 000000000000 (empty)
247 [EOF]
248 ");
249 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "a"]);
250 insta::assert_snapshot!(output, @r"
251 a
252 [EOF]
253 ");
254 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "a"]);
255 insta::assert_snapshot!(output, @r"
256 b
257 [EOF]
258 ");
259 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "b"]);
260 insta::assert_snapshot!(output, @r"
261 b
262 [EOF]
263 ");
264 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "b"]);
265 insta::assert_snapshot!(output, @r"
266 b
267 [EOF]
268 ");
269
270 // Can squash only some changes in non-interactive mode
271 test_env.run_jj_in(&repo_path, ["undo"]).success();
272 // Clear the script so we know it won't be used even without -i
273 std::fs::write(&edit_script, "").unwrap();
274 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b", "file2"]);
275 insta::assert_snapshot!(output, @r"
276 ------- stderr -------
277 Rebased 2 descendant commits
278 Working copy (@) now at: mzvwutvl 64d7ad7c c | (no description set)
279 Parent commit (@-) : kkmpptxz 60a26452 b | (no description set)
280 [EOF]
281 ");
282 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
283 @ 64d7ad7c43c1 c
284 ○ 60a264527aee b
285 ○ 7314692d32e3 a
286 ◆ 000000000000 (empty)
287 [EOF]
288 ");
289 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "a"]);
290 insta::assert_snapshot!(output, @r"
291 a
292 [EOF]
293 ");
294 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "a"]);
295 insta::assert_snapshot!(output, @r"
296 b
297 [EOF]
298 ");
299 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "b"]);
300 insta::assert_snapshot!(output, @r"
301 b
302 [EOF]
303 ");
304 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "b"]);
305 insta::assert_snapshot!(output, @r"
306 b
307 [EOF]
308 ");
309
310 // If we specify only a non-existent file, then nothing changes.
311 test_env.run_jj_in(&repo_path, ["undo"]).success();
312 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b", "nonexistent"]);
313 insta::assert_snapshot!(output, @r"
314 ------- stderr -------
315 Nothing changed.
316 [EOF]
317 ");
318
319 // We get a warning if we pass a positional argument that looks like a revset
320 test_env.run_jj_in(&repo_path, ["undo"]).success();
321 let output = test_env.run_jj_in(&repo_path, ["squash", "b"]);
322 insta::assert_snapshot!(output, @r#"
323 ------- stderr -------
324 Warning: The argument "b" is being interpreted as a fileset expression. To specify a revset, pass -r "b" instead.
325 Nothing changed.
326 [EOF]
327 "#);
328}
329
330#[test]
331fn test_squash_keep_emptied() {
332 let test_env = TestEnvironment::default();
333 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
334 let repo_path = test_env.env_root().join("repo");
335
336 test_env
337 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
338 .success();
339 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
340 test_env.run_jj_in(&repo_path, ["new"]).success();
341 test_env
342 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
343 .success();
344 std::fs::write(repo_path.join("file1"), "b\n").unwrap();
345 test_env.run_jj_in(&repo_path, ["new"]).success();
346 test_env
347 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
348 .success();
349 std::fs::write(repo_path.join("file1"), "c\n").unwrap();
350 // Test the setup
351
352 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
353 @ 382c9bad7d42 c
354 ○ d5d59175b481 b
355 ○ 184ddbcce5a9 a
356 ◆ 000000000000 (empty)
357 [EOF]
358 ");
359
360 let output = test_env.run_jj_in(&repo_path, ["squash", "-r", "b", "--keep-emptied"]);
361 insta::assert_snapshot!(output, @r"
362 ------- stderr -------
363 Rebased 2 descendant commits
364 Working copy (@) now at: mzvwutvl 7ee7f18a c | (no description set)
365 Parent commit (@-) : kkmpptxz 9490bd7f b | (empty) (no description set)
366 [EOF]
367 ");
368 // With --keep-emptied, b remains even though it is now empty.
369 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
370 @ 7ee7f18a5223 c
371 ○ 9490bd7f1e6a b (empty)
372 ○ 53bf93080518 a
373 ◆ 000000000000 (empty)
374 [EOF]
375 ");
376 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "a"]);
377 insta::assert_snapshot!(output, @r"
378 b
379 [EOF]
380 ");
381}
382
383#[test]
384fn test_squash_from_to() {
385 let test_env = TestEnvironment::default();
386 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
387 let repo_path = test_env.env_root().join("repo");
388
389 // Create history like this:
390 // F
391 // |
392 // E C
393 // | |
394 // D B
395 // |/
396 // A
397 //
398 // When moving changes between e.g. C and F, we should not get unrelated changes
399 // from B and D.
400 test_env
401 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
402 .success();
403 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
404 std::fs::write(repo_path.join("file2"), "a\n").unwrap();
405 std::fs::write(repo_path.join("file3"), "a\n").unwrap();
406 test_env.run_jj_in(&repo_path, ["new"]).success();
407 test_env
408 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
409 .success();
410 std::fs::write(repo_path.join("file3"), "b\n").unwrap();
411 test_env.run_jj_in(&repo_path, ["new"]).success();
412 test_env
413 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
414 .success();
415 std::fs::write(repo_path.join("file1"), "c\n").unwrap();
416 test_env.run_jj_in(&repo_path, ["edit", "a"]).success();
417 test_env.run_jj_in(&repo_path, ["new"]).success();
418 test_env
419 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "d"])
420 .success();
421 std::fs::write(repo_path.join("file3"), "d\n").unwrap();
422 test_env.run_jj_in(&repo_path, ["new"]).success();
423 test_env
424 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "e"])
425 .success();
426 std::fs::write(repo_path.join("file2"), "e\n").unwrap();
427 test_env.run_jj_in(&repo_path, ["new"]).success();
428 test_env
429 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "f"])
430 .success();
431 std::fs::write(repo_path.join("file2"), "f\n").unwrap();
432 // Test the setup
433 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
434 @ a847ab4967fe f
435 ○ c2f9de87325d e
436 ○ e0dac715116f d
437 │ ○ 59597b34a0d8 c
438 │ ○ 12d6103dc0c8 b
439 ├─╯
440 ○ b7b767179c44 a
441 ◆ 000000000000 (empty)
442 [EOF]
443 ");
444
445 // Errors out if source and destination are the same
446 let output = test_env.run_jj_in(&repo_path, ["squash", "--into", "@"]);
447 insta::assert_snapshot!(output, @r"
448 ------- stderr -------
449 Error: Source and destination cannot be the same
450 [EOF]
451 [exit status: 1]
452 ");
453
454 // Can squash from sibling, which results in the source being abandoned
455 let output = test_env.run_jj_in(&repo_path, ["squash", "--from", "c"]);
456 insta::assert_snapshot!(output, @r"
457 ------- stderr -------
458 Working copy (@) now at: kmkuslsw b902d1dd f | (no description set)
459 Parent commit (@-) : znkkpsqq c2f9de87 e | (no description set)
460 Added 0 files, modified 1 files, removed 0 files
461 [EOF]
462 ");
463 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
464 @ b902d1dd59d9 f
465 ○ c2f9de87325d e
466 ○ e0dac715116f d
467 │ ○ 12d6103dc0c8 b c
468 ├─╯
469 ○ b7b767179c44 a
470 ◆ 000000000000 (empty)
471 [EOF]
472 ");
473 // The change from the source has been applied
474 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
475 insta::assert_snapshot!(output, @r"
476 c
477 [EOF]
478 ");
479 // File `file2`, which was not changed in source, is unchanged
480 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2"]);
481 insta::assert_snapshot!(output, @r"
482 f
483 [EOF]
484 ");
485
486 // Can squash from ancestor
487 test_env.run_jj_in(&repo_path, ["undo"]).success();
488 let output = test_env.run_jj_in(&repo_path, ["squash", "--from", "@--"]);
489 insta::assert_snapshot!(output, @r"
490 ------- stderr -------
491 Working copy (@) now at: kmkuslsw cfc5eb87 f | (no description set)
492 Parent commit (@-) : znkkpsqq 4dc7c279 e | (no description set)
493 [EOF]
494 ");
495 // The change has been removed from the source (the change pointed to by 'd'
496 // became empty and was abandoned)
497 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
498 @ cfc5eb876eb1 f
499 ○ 4dc7c27994bd e
500 │ ○ 59597b34a0d8 c
501 │ ○ 12d6103dc0c8 b
502 ├─╯
503 ○ b7b767179c44 a d
504 ◆ 000000000000 (empty)
505 [EOF]
506 ");
507 // The change from the source has been applied (the file contents were already
508 // "f", as is typically the case when moving changes from an ancestor)
509 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2"]);
510 insta::assert_snapshot!(output, @r"
511 f
512 [EOF]
513 ");
514
515 // Can squash from descendant
516 test_env.run_jj_in(&repo_path, ["undo"]).success();
517 let output = test_env.run_jj_in(&repo_path, ["squash", "--from", "e", "--into", "d"]);
518 insta::assert_snapshot!(output, @r"
519 ------- stderr -------
520 Rebased 1 descendant commits
521 Working copy (@) now at: kmkuslsw 6de62c22 f | (no description set)
522 Parent commit (@-) : vruxwmqv 32196a11 d e | (no description set)
523 [EOF]
524 ");
525 // The change has been removed from the source (the change pointed to by 'e'
526 // became empty and was abandoned)
527 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
528 @ 6de62c22fa07 f
529 ○ 32196a117ee3 d e
530 │ ○ 59597b34a0d8 c
531 │ ○ 12d6103dc0c8 b
532 ├─╯
533 ○ b7b767179c44 a
534 ◆ 000000000000 (empty)
535 [EOF]
536 ");
537 // The change from the source has been applied
538 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "d"]);
539 insta::assert_snapshot!(output, @r"
540 e
541 [EOF]
542 ");
543}
544
545#[test]
546fn test_squash_from_to_partial() {
547 let mut test_env = TestEnvironment::default();
548 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
549 let repo_path = test_env.env_root().join("repo");
550
551 // Create history like this:
552 // C
553 // |
554 // D B
555 // |/
556 // A
557 test_env
558 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
559 .success();
560 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
561 std::fs::write(repo_path.join("file2"), "a\n").unwrap();
562 std::fs::write(repo_path.join("file3"), "a\n").unwrap();
563 test_env.run_jj_in(&repo_path, ["new"]).success();
564 test_env
565 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
566 .success();
567 std::fs::write(repo_path.join("file3"), "b\n").unwrap();
568 test_env.run_jj_in(&repo_path, ["new"]).success();
569 test_env
570 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
571 .success();
572 std::fs::write(repo_path.join("file1"), "c\n").unwrap();
573 std::fs::write(repo_path.join("file2"), "c\n").unwrap();
574 test_env.run_jj_in(&repo_path, ["edit", "a"]).success();
575 test_env.run_jj_in(&repo_path, ["new"]).success();
576 test_env
577 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "d"])
578 .success();
579 std::fs::write(repo_path.join("file3"), "d\n").unwrap();
580 // Test the setup
581 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
582 @ e0dac715116f d
583 │ ○ 087591be5a01 c
584 │ ○ 12d6103dc0c8 b
585 ├─╯
586 ○ b7b767179c44 a
587 ◆ 000000000000 (empty)
588 [EOF]
589 ");
590
591 let edit_script = test_env.set_up_fake_diff_editor();
592
593 // If we don't make any changes in the diff-editor, the whole change is moved
594 let output = test_env.run_jj_in(&repo_path, ["squash", "-i", "--from", "c"]);
595 insta::assert_snapshot!(output, @r"
596 ------- stderr -------
597 Working copy (@) now at: vruxwmqv 987bcfb2 d | (no description set)
598 Parent commit (@-) : qpvuntsm b7b76717 a | (no description set)
599 Added 0 files, modified 2 files, removed 0 files
600 [EOF]
601 ");
602 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
603 @ 987bcfb2eb62 d
604 │ ○ 12d6103dc0c8 b c
605 ├─╯
606 ○ b7b767179c44 a
607 ◆ 000000000000 (empty)
608 [EOF]
609 ");
610 // The changes from the source has been applied
611 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
612 insta::assert_snapshot!(output, @r"
613 c
614 [EOF]
615 ");
616 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2"]);
617 insta::assert_snapshot!(output, @r"
618 c
619 [EOF]
620 ");
621 // File `file3`, which was not changed in source, is unchanged
622 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file3"]);
623 insta::assert_snapshot!(output, @r"
624 d
625 [EOF]
626 ");
627
628 // Can squash only part of the change in interactive mode
629 test_env.run_jj_in(&repo_path, ["undo"]).success();
630 std::fs::write(&edit_script, "reset file2").unwrap();
631 let output = test_env.run_jj_in(&repo_path, ["squash", "-i", "--from", "c"]);
632 insta::assert_snapshot!(output, @r"
633 ------- stderr -------
634 Working copy (@) now at: vruxwmqv 576244e8 d | (no description set)
635 Parent commit (@-) : qpvuntsm b7b76717 a | (no description set)
636 Added 0 files, modified 1 files, removed 0 files
637 [EOF]
638 ");
639 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
640 @ 576244e87883 d
641 │ ○ 6f486f2f4539 c
642 │ ○ 12d6103dc0c8 b
643 ├─╯
644 ○ b7b767179c44 a
645 ◆ 000000000000 (empty)
646 [EOF]
647 ");
648 // The selected change from the source has been applied
649 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
650 insta::assert_snapshot!(output, @r"
651 c
652 [EOF]
653 ");
654 // The unselected change from the source has not been applied
655 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2"]);
656 insta::assert_snapshot!(output, @r"
657 a
658 [EOF]
659 ");
660 // File `file3`, which was changed in source's parent, is unchanged
661 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file3"]);
662 insta::assert_snapshot!(output, @r"
663 d
664 [EOF]
665 ");
666
667 // Can squash only part of the change from a sibling in non-interactive mode
668 test_env.run_jj_in(&repo_path, ["undo"]).success();
669 // Clear the script so we know it won't be used
670 std::fs::write(&edit_script, "").unwrap();
671 let output = test_env.run_jj_in(&repo_path, ["squash", "--from", "c", "file1"]);
672 insta::assert_snapshot!(output, @r"
673 ------- stderr -------
674 Working copy (@) now at: vruxwmqv 5b407c24 d | (no description set)
675 Parent commit (@-) : qpvuntsm b7b76717 a | (no description set)
676 Added 0 files, modified 1 files, removed 0 files
677 [EOF]
678 ");
679 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
680 @ 5b407c249fa7 d
681 │ ○ 724d64da1487 c
682 │ ○ 12d6103dc0c8 b
683 ├─╯
684 ○ b7b767179c44 a
685 ◆ 000000000000 (empty)
686 [EOF]
687 ");
688 // The selected change from the source has been applied
689 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1"]);
690 insta::assert_snapshot!(output, @r"
691 c
692 [EOF]
693 ");
694 // The unselected change from the source has not been applied
695 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2"]);
696 insta::assert_snapshot!(output, @r"
697 a
698 [EOF]
699 ");
700 // File `file3`, which was changed in source's parent, is unchanged
701 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file3"]);
702 insta::assert_snapshot!(output, @r"
703 d
704 [EOF]
705 ");
706
707 // Can squash only part of the change from a descendant in non-interactive mode
708 test_env.run_jj_in(&repo_path, ["undo"]).success();
709 // Clear the script so we know it won't be used
710 std::fs::write(&edit_script, "").unwrap();
711 let output = test_env.run_jj_in(
712 &repo_path,
713 ["squash", "--from", "c", "--into", "b", "file1"],
714 );
715 insta::assert_snapshot!(output, @r"
716 ------- stderr -------
717 Rebased 1 descendant commits
718 [EOF]
719 ");
720 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
721 @ e0dac715116f d
722 │ ○ d2a587ae205d c
723 │ ○ a53394306362 b
724 ├─╯
725 ○ b7b767179c44 a
726 ◆ 000000000000 (empty)
727 [EOF]
728 ");
729 // The selected change from the source has been applied
730 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file1", "-r", "b"]);
731 insta::assert_snapshot!(output, @r"
732 c
733 [EOF]
734 ");
735 // The unselected change from the source has not been applied
736 let output = test_env.run_jj_in(&repo_path, ["file", "show", "file2", "-r", "b"]);
737 insta::assert_snapshot!(output, @r"
738 a
739 [EOF]
740 ");
741
742 // If we specify only a non-existent file, then nothing changes.
743 test_env.run_jj_in(&repo_path, ["undo"]).success();
744 let output = test_env.run_jj_in(&repo_path, ["squash", "--from", "c", "nonexistent"]);
745 insta::assert_snapshot!(output, @r"
746 ------- stderr -------
747 Nothing changed.
748 [EOF]
749 ");
750}
751
752#[test]
753fn test_squash_from_multiple() {
754 let test_env = TestEnvironment::default();
755 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
756 let repo_path = test_env.env_root().join("repo");
757
758 // Create history like this:
759 // F
760 // |
761 // E
762 // /|\
763 // B C D
764 // \|/
765 // A
766 let file = repo_path.join("file");
767 test_env
768 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
769 .success();
770 std::fs::write(&file, "a\n").unwrap();
771 test_env.run_jj_in(&repo_path, ["new"]).success();
772 test_env
773 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
774 .success();
775 std::fs::write(&file, "b\n").unwrap();
776 test_env.run_jj_in(&repo_path, ["new", "@-"]).success();
777 test_env
778 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
779 .success();
780 std::fs::write(&file, "c\n").unwrap();
781 test_env.run_jj_in(&repo_path, ["new", "@-"]).success();
782 test_env
783 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "d"])
784 .success();
785 std::fs::write(&file, "d\n").unwrap();
786 test_env
787 .run_jj_in(&repo_path, ["new", "all:visible_heads()"])
788 .success();
789 test_env
790 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "e"])
791 .success();
792 std::fs::write(&file, "e\n").unwrap();
793 test_env.run_jj_in(&repo_path, ["new"]).success();
794 test_env
795 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "f"])
796 .success();
797 std::fs::write(&file, "f\n").unwrap();
798 // Test the setup
799 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
800 @ 94e57ecb8d4f f
801 ○ 78ed28eb87b8 e
802 ├─┬─╮
803 │ │ ○ 35e764e4357c b
804 │ ○ │ 02a128cd4344 c
805 │ ├─╯
806 ○ │ aaf7b53a1b64 d
807 ├─╯
808 ○ 3b1673b6370c a
809 ◆ 000000000000 (empty)
810 [EOF]
811 ");
812
813 // Squash a few commits sideways
814 let output = test_env.run_jj_in(&repo_path, ["squash", "--from=b", "--from=c", "--into=d"]);
815 insta::assert_snapshot!(output, @r"
816 ------- stderr -------
817 Rebased 2 descendant commits
818 Working copy (@) now at: kpqxywon 7ea39167 f | (no description set)
819 Parent commit (@-) : yostqsxw acfbf2a0 e | (no description set)
820 New conflicts appeared in 1 commits:
821 yqosqzyt 4df3b215 d | (conflict) (no description set)
822 Hint: To resolve the conflicts, start by updating to it:
823 jj new yqosqzyt
824 Then use `jj resolve`, or edit the conflict markers in the file directly.
825 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
826 Then run `jj squash` to move the resolution into the conflicted commit.
827 [EOF]
828 ");
829 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
830 @ 7ea391676d52 f
831 ○ acfbf2a0600d e
832 ├─╮
833 × │ 4df3b2156c3d d
834 ├─╯
835 ○ 3b1673b6370c a b c
836 ◆ 000000000000 (empty)
837 [EOF]
838 ");
839 // The changes from the sources have been applied
840 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=d", "file"]);
841 insta::assert_snapshot!(output, @r"
842 <<<<<<< Conflict 1 of 1
843 %%%%%%% Changes from base #1 to side #1
844 -a
845 +d
846 %%%%%%% Changes from base #2 to side #2
847 -a
848 +b
849 +++++++ Contents of side #3
850 c
851 >>>>>>> Conflict 1 of 1 ends
852 [EOF]
853 ");
854
855 // Squash a few commits up an down
856 test_env.run_jj_in(&repo_path, ["undo"]).success();
857 let output = test_env.run_jj_in(&repo_path, ["squash", "--from=b|c|f", "--into=e"]);
858 insta::assert_snapshot!(output, @r"
859 ------- stderr -------
860 Rebased 1 descendant commits
861 Working copy (@) now at: xznxytkn 6a670d1a (empty) (no description set)
862 Parent commit (@-) : yostqsxw c1293ff7 e f | (no description set)
863 [EOF]
864 ");
865 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
866 @ 6a670d1ac76e (empty)
867 ○ c1293ff7be51 e f
868 ├─╮
869 ○ │ aaf7b53a1b64 d
870 ├─╯
871 ○ 3b1673b6370c a b c
872 ◆ 000000000000 (empty)
873 [EOF]
874 ");
875 // The changes from the sources have been applied to the destination
876 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=e", "file"]);
877 insta::assert_snapshot!(output, @r"
878 f
879 [EOF]
880 ");
881
882 // Empty squash shouldn't crash
883 let output = test_env.run_jj_in(&repo_path, ["squash", "--from=none()"]);
884 insta::assert_snapshot!(output, @r"
885 ------- stderr -------
886 Nothing changed.
887 [EOF]
888 ");
889}
890
891#[test]
892fn test_squash_from_multiple_partial() {
893 let test_env = TestEnvironment::default();
894 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
895 let repo_path = test_env.env_root().join("repo");
896
897 // Create history like this:
898 // F
899 // |
900 // E
901 // /|\
902 // B C D
903 // \|/
904 // A
905 let file1 = repo_path.join("file1");
906 let file2 = repo_path.join("file2");
907 test_env
908 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "a"])
909 .success();
910 std::fs::write(&file1, "a\n").unwrap();
911 std::fs::write(&file2, "a\n").unwrap();
912 test_env.run_jj_in(&repo_path, ["new"]).success();
913 test_env
914 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "b"])
915 .success();
916 std::fs::write(&file1, "b\n").unwrap();
917 std::fs::write(&file2, "b\n").unwrap();
918 test_env.run_jj_in(&repo_path, ["new", "@-"]).success();
919 test_env
920 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "c"])
921 .success();
922 std::fs::write(&file1, "c\n").unwrap();
923 std::fs::write(&file2, "c\n").unwrap();
924 test_env.run_jj_in(&repo_path, ["new", "@-"]).success();
925 test_env
926 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "d"])
927 .success();
928 std::fs::write(&file1, "d\n").unwrap();
929 std::fs::write(&file2, "d\n").unwrap();
930 test_env
931 .run_jj_in(&repo_path, ["new", "all:visible_heads()"])
932 .success();
933 test_env
934 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "e"])
935 .success();
936 std::fs::write(&file1, "e\n").unwrap();
937 std::fs::write(&file2, "e\n").unwrap();
938 test_env.run_jj_in(&repo_path, ["new"]).success();
939 test_env
940 .run_jj_in(&repo_path, ["bookmark", "create", "-r@", "f"])
941 .success();
942 std::fs::write(&file1, "f\n").unwrap();
943 std::fs::write(&file2, "f\n").unwrap();
944 // Test the setup
945 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
946 @ 30980b9045f7 f
947 ○ 5326a04aac1f e
948 ├─┬─╮
949 │ │ ○ d117da276a0f b
950 │ ○ │ 93a7bfff61e7 c
951 │ ├─╯
952 ○ │ 763809ca0131 d
953 ├─╯
954 ○ 54d3c1c0e9fd a
955 ◆ 000000000000 (empty)
956 [EOF]
957 ");
958
959 // Partially squash a few commits sideways
960 let output = test_env.run_jj_in(&repo_path, ["squash", "--from=b|c", "--into=d", "file1"]);
961 insta::assert_snapshot!(output, @r"
962 ------- stderr -------
963 Rebased 2 descendant commits
964 Working copy (@) now at: kpqxywon a8530305 f | (no description set)
965 Parent commit (@-) : yostqsxw 0a3637fc e | (no description set)
966 New conflicts appeared in 1 commits:
967 yqosqzyt 05a3ab3d d | (conflict) (no description set)
968 Hint: To resolve the conflicts, start by updating to it:
969 jj new yqosqzyt
970 Then use `jj resolve`, or edit the conflict markers in the file directly.
971 Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
972 Then run `jj squash` to move the resolution into the conflicted commit.
973 [EOF]
974 ");
975 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
976 @ a8530305127c f
977 ○ 0a3637fca632 e
978 ├─┬─╮
979 │ │ ○ 450d1499c1ae b
980 │ ○ │ 14b44bf0473c c
981 │ ├─╯
982 × │ 05a3ab3dffc8 d
983 ├─╯
984 ○ 54d3c1c0e9fd a
985 ◆ 000000000000 (empty)
986 [EOF]
987 ");
988 // The selected changes have been removed from the sources
989 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=b", "file1"]);
990 insta::assert_snapshot!(output, @r"
991 a
992 [EOF]
993 ");
994 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=c", "file1"]);
995 insta::assert_snapshot!(output, @r"
996 a
997 [EOF]
998 ");
999 // The selected changes from the sources have been applied
1000 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=d", "file1"]);
1001 insta::assert_snapshot!(output, @r"
1002 <<<<<<< Conflict 1 of 1
1003 %%%%%%% Changes from base #1 to side #1
1004 -a
1005 +d
1006 %%%%%%% Changes from base #2 to side #2
1007 -a
1008 +b
1009 +++++++ Contents of side #3
1010 c
1011 >>>>>>> Conflict 1 of 1 ends
1012 [EOF]
1013 ");
1014 // The unselected change from the sources have not been applied to the
1015 // destination
1016 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=d", "file2"]);
1017 insta::assert_snapshot!(output, @r"
1018 d
1019 [EOF]
1020 ");
1021
1022 // Partially squash a few commits up an down
1023 test_env.run_jj_in(&repo_path, ["undo"]).success();
1024 let output = test_env.run_jj_in(&repo_path, ["squash", "--from=b|c|f", "--into=e", "file1"]);
1025 insta::assert_snapshot!(output, @r"
1026 ------- stderr -------
1027 Rebased 1 descendant commits
1028 Working copy (@) now at: kpqxywon 3b7559b8 f | (no description set)
1029 Parent commit (@-) : yostqsxw a3b1714c e | (no description set)
1030 [EOF]
1031 ");
1032 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
1033 @ 3b7559b89a57 f
1034 ○ a3b1714cdfb2 e
1035 ├─┬─╮
1036 │ │ ○ 867efb38e801 b
1037 │ ○ │ 84dcb3d4b3eb c
1038 │ ├─╯
1039 ○ │ 763809ca0131 d
1040 ├─╯
1041 ○ 54d3c1c0e9fd a
1042 ◆ 000000000000 (empty)
1043 [EOF]
1044 ");
1045 // The selected changes have been removed from the sources
1046 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=b", "file1"]);
1047 insta::assert_snapshot!(output, @r"
1048 a
1049 [EOF]
1050 ");
1051 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=c", "file1"]);
1052 insta::assert_snapshot!(output, @r"
1053 a
1054 [EOF]
1055 ");
1056 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=f", "file1"]);
1057 insta::assert_snapshot!(output, @r"
1058 f
1059 [EOF]
1060 ");
1061 // The selected changes from the sources have been applied to the destination
1062 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=e", "file1"]);
1063 insta::assert_snapshot!(output, @r"
1064 f
1065 [EOF]
1066 ");
1067 // The unselected changes from the sources have not been applied
1068 let output = test_env.run_jj_in(&repo_path, ["file", "show", "-r=d", "file2"]);
1069 insta::assert_snapshot!(output, @r"
1070 d
1071 [EOF]
1072 ");
1073}
1074
1075#[test]
1076fn test_squash_from_multiple_partial_no_op() {
1077 let test_env = TestEnvironment::default();
1078 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1079 let repo_path = test_env.env_root().join("repo");
1080
1081 // Create history like this:
1082 // B C D
1083 // \|/
1084 // A
1085 let file_a = repo_path.join("a");
1086 let file_b = repo_path.join("b");
1087 let file_c = repo_path.join("c");
1088 let file_d = repo_path.join("d");
1089 test_env
1090 .run_jj_in(&repo_path, ["describe", "-m=a"])
1091 .success();
1092 std::fs::write(file_a, "a\n").unwrap();
1093 test_env.run_jj_in(&repo_path, ["new", "-m=b"]).success();
1094 std::fs::write(file_b, "b\n").unwrap();
1095 test_env
1096 .run_jj_in(&repo_path, ["new", "@-", "-m=c"])
1097 .success();
1098 std::fs::write(file_c, "c\n").unwrap();
1099 test_env
1100 .run_jj_in(&repo_path, ["new", "@-", "-m=d"])
1101 .success();
1102 std::fs::write(file_d, "d\n").unwrap();
1103 // Test the setup
1104 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
1105 @ b37ca1ee3306 d
1106 │ ○ f40b442af3e8 c
1107 ├─╯
1108 │ ○ b73077b08c59 b
1109 ├─╯
1110 ○ 2443ea76b0b1 a
1111 ◆ 000000000000 (empty)
1112 [EOF]
1113 ");
1114
1115 // Source commits that didn't match the paths are not rewritten
1116 let output = test_env.run_jj_in(
1117 &repo_path,
1118 ["squash", "--from=@-+ ~ @", "--into=@", "-m=d", "b"],
1119 );
1120 insta::assert_snapshot!(output, @r"
1121 ------- stderr -------
1122 Working copy (@) now at: mzvwutvl e178068a d
1123 Parent commit (@-) : qpvuntsm 2443ea76 a
1124 Added 1 files, modified 0 files, removed 0 files
1125 [EOF]
1126 ");
1127 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
1128 @ e178068add8c d
1129 │ ○ f40b442af3e8 c
1130 ├─╯
1131 ○ 2443ea76b0b1 a
1132 ◆ 000000000000 (empty)
1133 [EOF]
1134 ");
1135 let output = test_env.run_jj_in(
1136 &repo_path,
1137 [
1138 "evolog",
1139 "-T",
1140 r#"separate(" ", commit_id.short(), description)"#,
1141 ],
1142 );
1143 insta::assert_snapshot!(output, @r"
1144 @ e178068add8c d
1145 ├─╮
1146 │ ○ b73077b08c59 b
1147 │ ○ a786561e909f b
1148 ○ b37ca1ee3306 d
1149 ○ 1d9eb34614c9 d
1150 [EOF]
1151 ");
1152
1153 // If no source commits match the paths, then the whole operation is a no-op
1154 test_env.run_jj_in(&repo_path, ["undo"]).success();
1155 let output = test_env.run_jj_in(
1156 &repo_path,
1157 ["squash", "--from=@-+ ~ @", "--into=@", "-m=d", "a"],
1158 );
1159 insta::assert_snapshot!(output, @r"
1160 ------- stderr -------
1161 Nothing changed.
1162 [EOF]
1163 ");
1164 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
1165 @ b37ca1ee3306 d
1166 │ ○ f40b442af3e8 c
1167 ├─╯
1168 │ ○ b73077b08c59 b
1169 ├─╯
1170 ○ 2443ea76b0b1 a
1171 ◆ 000000000000 (empty)
1172 [EOF]
1173 ");
1174}
1175
1176#[must_use]
1177fn get_log_output(test_env: &TestEnvironment, repo_path: &Path) -> CommandOutput {
1178 let template = r#"separate(
1179 " ",
1180 commit_id.short(),
1181 bookmarks,
1182 description,
1183 if(empty, "(empty)")
1184 )"#;
1185 test_env.run_jj_in(repo_path, ["log", "-T", template])
1186}
1187
1188#[test]
1189fn test_squash_description() {
1190 let mut test_env = TestEnvironment::default();
1191 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1192 let repo_path = test_env.env_root().join("repo");
1193
1194 let edit_script = test_env.set_up_fake_editor();
1195 std::fs::write(&edit_script, r#"fail"#).unwrap();
1196
1197 // If both descriptions are empty, the resulting description is empty
1198 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
1199 std::fs::write(repo_path.join("file2"), "a\n").unwrap();
1200 test_env.run_jj_in(&repo_path, ["new"]).success();
1201 std::fs::write(repo_path.join("file1"), "b\n").unwrap();
1202 std::fs::write(repo_path.join("file2"), "b\n").unwrap();
1203 test_env.run_jj_in(&repo_path, ["squash"]).success();
1204 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @"");
1205
1206 // If the destination's description is empty and the source's description is
1207 // non-empty, the resulting description is from the source
1208 test_env.run_jj_in(&repo_path, ["undo"]).success();
1209 test_env
1210 .run_jj_in(&repo_path, ["describe", "-m", "source"])
1211 .success();
1212 test_env.run_jj_in(&repo_path, ["squash"]).success();
1213 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1214 source
1215 [EOF]
1216 ");
1217
1218 // If the destination description is non-empty and the source's description is
1219 // empty, the resulting description is from the destination
1220 test_env
1221 .run_jj_in(&repo_path, ["op", "restore", "@--"])
1222 .success();
1223 test_env
1224 .run_jj_in(&repo_path, ["describe", "@-", "-m", "destination"])
1225 .success();
1226 test_env.run_jj_in(&repo_path, ["squash"]).success();
1227 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1228 destination
1229 [EOF]
1230 ");
1231
1232 // An explicit description on the command-line overrides this
1233 test_env.run_jj_in(&repo_path, ["undo"]).success();
1234 test_env
1235 .run_jj_in(&repo_path, ["squash", "-m", "custom"])
1236 .success();
1237 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1238 custom
1239 [EOF]
1240 ");
1241
1242 // If both descriptions were non-empty, we get asked for a combined description
1243 test_env.run_jj_in(&repo_path, ["undo"]).success();
1244 test_env
1245 .run_jj_in(&repo_path, ["describe", "-m", "source"])
1246 .success();
1247 std::fs::write(&edit_script, "dump editor0").unwrap();
1248 test_env.run_jj_in(&repo_path, ["squash"]).success();
1249 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1250 destination
1251
1252 source
1253 [EOF]
1254 ");
1255 insta::assert_snapshot!(
1256 std::fs::read_to_string(test_env.env_root().join("editor0")).unwrap(), @r#"
1257 JJ: Enter a description for the combined commit.
1258 JJ: Description from the destination commit:
1259 destination
1260
1261 JJ: Description from source commit:
1262 source
1263
1264 JJ: This commit contains the following changes:
1265 JJ: A file1
1266 JJ: A file2
1267 JJ:
1268 JJ: Lines starting with "JJ:" (like this one) will be removed.
1269 "#);
1270
1271 // An explicit description on the command-line overrides prevents launching an
1272 // editor
1273 test_env.run_jj_in(&repo_path, ["undo"]).success();
1274 test_env
1275 .run_jj_in(&repo_path, ["squash", "-m", "custom"])
1276 .success();
1277 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1278 custom
1279 [EOF]
1280 ");
1281
1282 // If the source's *content* doesn't become empty, then the source remains and
1283 // both descriptions are unchanged
1284 test_env.run_jj_in(&repo_path, ["undo"]).success();
1285 test_env
1286 .run_jj_in(&repo_path, ["squash", "file1"])
1287 .success();
1288 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1289 destination
1290 [EOF]
1291 ");
1292 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@"), @r"
1293 source
1294 [EOF]
1295 ");
1296}
1297
1298#[test]
1299fn test_squash_description_editor_avoids_unc() {
1300 let mut test_env = TestEnvironment::default();
1301 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1302 let repo_path = test_env.env_root().join("repo");
1303
1304 let edit_script = test_env.set_up_fake_editor();
1305 std::fs::write(repo_path.join("file1"), "a\n").unwrap();
1306 std::fs::write(repo_path.join("file2"), "a\n").unwrap();
1307 test_env.run_jj_in(&repo_path, ["new"]).success();
1308 std::fs::write(repo_path.join("file1"), "b\n").unwrap();
1309 std::fs::write(repo_path.join("file2"), "b\n").unwrap();
1310 test_env
1311 .run_jj_in(&repo_path, ["describe", "@-", "-m", "destination"])
1312 .success();
1313 test_env
1314 .run_jj_in(&repo_path, ["describe", "-m", "source"])
1315 .success();
1316
1317 std::fs::write(edit_script, "dump-path path").unwrap();
1318 test_env.run_jj_in(&repo_path, ["squash"]).success();
1319
1320 let edited_path =
1321 PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap());
1322 // While `assert!(!edited_path.starts_with("//?/"))` could work here in most
1323 // cases, it fails when it is not safe to strip the prefix, such as paths
1324 // over 260 chars.
1325 assert_eq!(edited_path, dunce::simplified(&edited_path));
1326}
1327
1328#[test]
1329fn test_squash_empty() {
1330 let mut test_env = TestEnvironment::default();
1331 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1332 let repo_path = test_env.env_root().join("repo");
1333
1334 test_env
1335 .run_jj_in(&repo_path, ["commit", "-m", "parent"])
1336 .success();
1337
1338 let output = test_env.run_jj_in(&repo_path, ["squash"]);
1339 insta::assert_snapshot!(output, @r"
1340 ------- stderr -------
1341 Working copy (@) now at: kkmpptxz adece6e8 (empty) (no description set)
1342 Parent commit (@-) : qpvuntsm 5076fc41 (empty) parent
1343 [EOF]
1344 ");
1345 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1346 parent
1347 [EOF]
1348 ");
1349
1350 test_env
1351 .run_jj_in(&repo_path, ["describe", "-m", "child"])
1352 .success();
1353 test_env.set_up_fake_editor();
1354 test_env.run_jj_in(&repo_path, ["squash"]).success();
1355 insta::assert_snapshot!(get_description(&test_env, &repo_path, "@-"), @r"
1356 parent
1357
1358 child
1359 [EOF]
1360 ");
1361}
1362
1363#[test]
1364fn test_squash_use_destination_message() {
1365 let test_env = TestEnvironment::default();
1366 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1367 let repo_path = test_env.env_root().join("repo");
1368
1369 test_env.run_jj_in(&repo_path, ["commit", "-m=a"]).success();
1370 test_env.run_jj_in(&repo_path, ["commit", "-m=b"]).success();
1371 test_env
1372 .run_jj_in(&repo_path, ["describe", "-m=c"])
1373 .success();
1374 // Test the setup
1375 insta::assert_snapshot!(get_log_output_with_description(&test_env, &repo_path), @r"
1376 @ 8aac283daeac c
1377 ○ 017c7f689ed7 b
1378 ○ d8d5f980a897 a
1379 ◆ 000000000000
1380 [EOF]
1381 ");
1382
1383 // Squash the current revision using the short name for the option.
1384 test_env.run_jj_in(&repo_path, ["squash", "-u"]).success();
1385 insta::assert_snapshot!(get_log_output_with_description(&test_env, &repo_path), @r"
1386 @ fd33e4bc332b
1387 ○ 3a17aa5dcce9 b
1388 ○ d8d5f980a897 a
1389 ◆ 000000000000
1390 [EOF]
1391 ");
1392
1393 // Undo and squash again, but this time squash both "b" and "c" into "a".
1394 test_env.run_jj_in(&repo_path, ["undo"]).success();
1395 test_env
1396 .run_jj_in(
1397 &repo_path,
1398 [
1399 "squash",
1400 "--use-destination-message",
1401 "--from",
1402 "description(b)::",
1403 "--into",
1404 "description(a)",
1405 ],
1406 )
1407 .success();
1408 insta::assert_snapshot!(get_log_output_with_description(&test_env, &repo_path), @r"
1409 @ 7c832accbf60
1410 ○ 688660377651 a
1411 ◆ 000000000000
1412 [EOF]
1413 ");
1414}
1415
1416// The --use-destination-message and --message options are incompatible.
1417#[test]
1418fn test_squash_use_destination_message_and_message_mutual_exclusion() {
1419 let test_env = TestEnvironment::default();
1420 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1421 let repo_path = test_env.env_root().join("repo");
1422 test_env.run_jj_in(&repo_path, ["commit", "-m=a"]).success();
1423 test_env
1424 .run_jj_in(&repo_path, ["describe", "-m=b"])
1425 .success();
1426 insta::assert_snapshot!(test_env.run_jj_in(
1427 &repo_path,
1428 [
1429 "squash",
1430 "--message=123",
1431 "--use-destination-message",
1432 ],
1433 ), @r"
1434 ------- stderr -------
1435 error: the argument '--message <MESSAGE>' cannot be used with '--use-destination-message'
1436
1437 Usage: jj squash --message <MESSAGE> [FILESETS]...
1438
1439 For more information, try '--help'.
1440 [EOF]
1441 [exit status: 2]
1442 ");
1443}
1444
1445#[must_use]
1446fn get_description(test_env: &TestEnvironment, repo_path: &Path, rev: &str) -> CommandOutput {
1447 test_env.run_jj_in(
1448 repo_path,
1449 ["log", "--no-graph", "-T", "description", "-r", rev],
1450 )
1451}
1452
1453#[must_use]
1454fn get_log_output_with_description(test_env: &TestEnvironment, repo_path: &Path) -> CommandOutput {
1455 let template = r#"separate(" ", commit_id.short(), description)"#;
1456 test_env.run_jj_in(repo_path, ["log", "-T", template])
1457}