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 test_case::test_case;
19
20use crate::common::CommandOutput;
21use crate::common::TestEnvironment;
22
23#[must_use]
24fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> CommandOutput {
25 let template = r#"separate(" ", change_id.short(), empty, local_bookmarks, description)"#;
26 test_env.run_jj_in(cwd, ["log", "-T", template])
27}
28
29#[must_use]
30fn get_workspace_log_output(test_env: &TestEnvironment, cwd: &Path) -> CommandOutput {
31 let template = r#"separate(" ", change_id.short(), working_copies, description)"#;
32 test_env.run_jj_in(cwd, ["log", "-T", template, "-r", "all()"])
33}
34
35#[must_use]
36fn get_recorded_dates(test_env: &TestEnvironment, cwd: &Path, revset: &str) -> CommandOutput {
37 let template = r#"separate("\n", "Author date: " ++ author.timestamp(), "Committer date: " ++ committer.timestamp())"#;
38 test_env.run_jj_in(cwd, ["log", "--no-graph", "-T", template, "-r", revset])
39}
40
41#[test]
42fn test_split_by_paths() {
43 let mut test_env = TestEnvironment::default();
44 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
45 let repo_path = test_env.env_root().join("repo");
46
47 std::fs::write(repo_path.join("file1"), "foo").unwrap();
48 std::fs::write(repo_path.join("file2"), "foo").unwrap();
49 std::fs::write(repo_path.join("file3"), "foo").unwrap();
50
51 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
52 @ qpvuntsmwlqt false
53 ◆ zzzzzzzzzzzz true
54 [EOF]
55 ");
56 insta::assert_snapshot!(get_recorded_dates(&test_env, &repo_path,"@"), @r"
57 Author date: 2001-02-03 04:05:08.000 +07:00
58 Committer date: 2001-02-03 04:05:08.000 +07:00[EOF]
59 ");
60
61 let edit_script = test_env.set_up_fake_editor();
62 std::fs::write(
63 edit_script,
64 ["dump editor0", "next invocation\n", "dump editor1"].join("\0"),
65 )
66 .unwrap();
67 let output = test_env.run_jj_in(&repo_path, ["split", "file2"]);
68 insta::assert_snapshot!(output, @r"
69 ------- stderr -------
70 First part: qpvuntsm 65569ca7 (no description set)
71 Second part: zsuskuln 709756f0 (no description set)
72 Working copy (@) now at: zsuskuln 709756f0 (no description set)
73 Parent commit (@-) : qpvuntsm 65569ca7 (no description set)
74 [EOF]
75 ");
76 insta::assert_snapshot!(
77 std::fs::read_to_string(test_env.env_root().join("editor0")).unwrap(), @r#"
78 JJ: Enter a description for the first commit.
79
80
81 JJ: This commit contains the following changes:
82 JJ: A file2
83 JJ:
84 JJ: Lines starting with "JJ:" (like this one) will be removed.
85 "#);
86 assert!(!test_env.env_root().join("editor1").exists());
87
88 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
89 @ zsuskulnrvyr false
90 ○ qpvuntsmwlqt false
91 ◆ zzzzzzzzzzzz true
92 [EOF]
93 ");
94
95 // The author dates of the new commits should be inherited from the commit being
96 // split. The committer dates should be newer.
97 insta::assert_snapshot!(get_recorded_dates(&test_env, &repo_path,"@"), @r"
98 Author date: 2001-02-03 04:05:08.000 +07:00
99 Committer date: 2001-02-03 04:05:10.000 +07:00[EOF]
100 ");
101 insta::assert_snapshot!(get_recorded_dates(&test_env, &repo_path,"@-"), @r"
102 Author date: 2001-02-03 04:05:08.000 +07:00
103 Committer date: 2001-02-03 04:05:10.000 +07:00[EOF]
104 ");
105
106 let output = test_env.run_jj_in(&repo_path, ["diff", "-s", "-r", "@-"]);
107 insta::assert_snapshot!(output, @r"
108 A file2
109 [EOF]
110 ");
111 let output = test_env.run_jj_in(&repo_path, ["diff", "-s"]);
112 insta::assert_snapshot!(output, @r"
113 A file1
114 A file3
115 [EOF]
116 ");
117
118 // Insert an empty commit after @- with "split ."
119 test_env.set_up_fake_editor();
120 let output = test_env.run_jj_in(&repo_path, ["split", "-r", "@-", "."]);
121 insta::assert_snapshot!(output, @r"
122 ------- stderr -------
123 Warning: All changes have been selected, so the second commit will be empty
124 Rebased 1 descendant commits
125 First part: qpvuntsm 9da0eea0 (no description set)
126 Second part: znkkpsqq 5b5714a3 (empty) (no description set)
127 Working copy (@) now at: zsuskuln 0c798ee7 (no description set)
128 Parent commit (@-) : znkkpsqq 5b5714a3 (empty) (no description set)
129 [EOF]
130 ");
131
132 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
133 @ zsuskulnrvyr false
134 ○ znkkpsqqskkl true
135 ○ qpvuntsmwlqt false
136 ◆ zzzzzzzzzzzz true
137 [EOF]
138 ");
139
140 let output = test_env.run_jj_in(&repo_path, ["diff", "-s", "-r", "@--"]);
141 insta::assert_snapshot!(output, @r"
142 A file2
143 [EOF]
144 ");
145
146 // Remove newly created empty commit
147 test_env.run_jj_in(&repo_path, ["abandon", "@-"]).success();
148
149 // Insert an empty commit before @- with "split nonexistent"
150 test_env.set_up_fake_editor();
151 let output = test_env.run_jj_in(&repo_path, ["split", "-r", "@-", "nonexistent"]);
152 insta::assert_snapshot!(output, @r"
153 ------- stderr -------
154 Warning: No changes have been selected, so the first commit will be empty
155 Rebased 1 descendant commits
156 First part: qpvuntsm bd42f95a (empty) (no description set)
157 Second part: lylxulpl ed55c86b (no description set)
158 Working copy (@) now at: zsuskuln 1e1ed741 (no description set)
159 Parent commit (@-) : lylxulpl ed55c86b (no description set)
160 [EOF]
161 ");
162
163 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r"
164 @ zsuskulnrvyr false
165 ○ lylxulplsnyw false
166 ○ qpvuntsmwlqt true
167 ◆ zzzzzzzzzzzz true
168 [EOF]
169 ");
170
171 let output = test_env.run_jj_in(&repo_path, ["diff", "-s", "-r", "@-"]);
172 insta::assert_snapshot!(output, @r"
173 A file2
174 [EOF]
175 ");
176}
177
178#[test]
179fn test_split_with_non_empty_description() {
180 let mut test_env = TestEnvironment::default();
181 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
182 test_env.add_config(r#"ui.default-description = "\n\nTESTED=TODO""#);
183 let workspace_path = test_env.env_root().join("repo");
184
185 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
186 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
187 test_env
188 .run_jj_in(&workspace_path, ["describe", "-m", "test"])
189 .success();
190 let edit_script = test_env.set_up_fake_editor();
191 std::fs::write(
192 edit_script,
193 [
194 "dump editor1",
195 "write\npart 1",
196 "next invocation\n",
197 "dump editor2",
198 "write\npart 2",
199 ]
200 .join("\0"),
201 )
202 .unwrap();
203 let output = test_env.run_jj_in(&workspace_path, ["split", "file1"]);
204 insta::assert_snapshot!(output, @r"
205 ------- stderr -------
206 First part: qpvuntsm 231a3c00 part 1
207 Second part: kkmpptxz e96291aa part 2
208 Working copy (@) now at: kkmpptxz e96291aa part 2
209 Parent commit (@-) : qpvuntsm 231a3c00 part 1
210 [EOF]
211 ");
212
213 insta::assert_snapshot!(
214 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
215 JJ: Enter a description for the first commit.
216 test
217
218 JJ: This commit contains the following changes:
219 JJ: A file1
220 JJ:
221 JJ: Lines starting with "JJ:" (like this one) will be removed.
222 "#);
223 insta::assert_snapshot!(
224 std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r#"
225 JJ: Enter a description for the second commit.
226 test
227
228 JJ: This commit contains the following changes:
229 JJ: A file2
230 JJ:
231 JJ: Lines starting with "JJ:" (like this one) will be removed.
232 "#);
233 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
234 @ kkmpptxzrspx false part 2
235 ○ qpvuntsmwlqt false part 1
236 ◆ zzzzzzzzzzzz true
237 [EOF]
238 ");
239}
240
241#[test]
242fn test_split_with_default_description() {
243 let mut test_env = TestEnvironment::default();
244 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
245 test_env.add_config(r#"ui.default-description = "\n\nTESTED=TODO""#);
246 let workspace_path = test_env.env_root().join("repo");
247
248 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
249 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
250
251 let edit_script = test_env.set_up_fake_editor();
252 std::fs::write(
253 edit_script,
254 ["dump editor1", "next invocation\n", "dump editor2"].join("\0"),
255 )
256 .unwrap();
257 let output = test_env.run_jj_in(&workspace_path, ["split", "file1"]);
258 insta::assert_snapshot!(output, @r"
259 ------- stderr -------
260 First part: qpvuntsm 02ee5d60 TESTED=TODO
261 Second part: rlvkpnrz 33cd046b (no description set)
262 Working copy (@) now at: rlvkpnrz 33cd046b (no description set)
263 Parent commit (@-) : qpvuntsm 02ee5d60 TESTED=TODO
264 [EOF]
265 ");
266
267 // Since the commit being split has no description, the user will only be
268 // prompted to add a description to the first commit, which will use the
269 // default value we set. The second commit will inherit the empty
270 // description from the commit being split.
271 insta::assert_snapshot!(
272 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
273 JJ: Enter a description for the first commit.
274
275
276 TESTED=TODO
277
278 JJ: This commit contains the following changes:
279 JJ: A file1
280 JJ:
281 JJ: Lines starting with "JJ:" (like this one) will be removed.
282 "#);
283 assert!(!test_env.env_root().join("editor2").exists());
284 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
285 @ rlvkpnrzqnoo false
286 ○ qpvuntsmwlqt false TESTED=TODO
287 ◆ zzzzzzzzzzzz true
288 [EOF]
289 ");
290}
291
292#[test]
293fn test_split_with_descendants() {
294 // Configure the environment and make the initial commits.
295 let mut test_env = TestEnvironment::default();
296 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
297 let workspace_path = test_env.env_root().join("repo");
298
299 // First commit. This is the one we will split later.
300 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
301 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
302 test_env
303 .run_jj_in(&workspace_path, ["commit", "-m", "Add file1 & file2"])
304 .success();
305 // Second commit.
306 std::fs::write(workspace_path.join("file3"), "baz\n").unwrap();
307 test_env
308 .run_jj_in(&workspace_path, ["commit", "-m", "Add file3"])
309 .success();
310 // Third commit.
311 std::fs::write(workspace_path.join("file4"), "foobarbaz\n").unwrap();
312 test_env
313 .run_jj_in(&workspace_path, ["describe", "-m", "Add file4"])
314 .success();
315 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
316 @ kkmpptxzrspx false Add file4
317 ○ rlvkpnrzqnoo false Add file3
318 ○ qpvuntsmwlqt false Add file1 & file2
319 ◆ zzzzzzzzzzzz true
320 [EOF]
321 "###);
322
323 // Set up the editor and do the split.
324 let edit_script = test_env.set_up_fake_editor();
325 std::fs::write(
326 edit_script,
327 [
328 "dump editor1",
329 "write\nAdd file1",
330 "next invocation\n",
331 "dump editor2",
332 "write\nAdd file2",
333 ]
334 .join("\0"),
335 )
336 .unwrap();
337 let output = test_env.run_jj_in(&workspace_path, ["split", "file1", "-r", "qpvu"]);
338 insta::assert_snapshot!(output, @r"
339 ------- stderr -------
340 Rebased 2 descendant commits
341 First part: qpvuntsm 34dd141b Add file1
342 Second part: royxmykx 465e03d0 Add file2
343 Working copy (@) now at: kkmpptxz 2d5d641f Add file4
344 Parent commit (@-) : rlvkpnrz b3bd9eb7 Add file3
345 [EOF]
346 ");
347 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
348 @ kkmpptxzrspx false Add file4
349 ○ rlvkpnrzqnoo false Add file3
350 ○ royxmykxtrkr false Add file2
351 ○ qpvuntsmwlqt false Add file1
352 ◆ zzzzzzzzzzzz true
353 [EOF]
354 "###);
355
356 // The commit we're splitting has a description, so the user will be
357 // prompted to enter a description for each of the commits.
358 insta::assert_snapshot!(
359 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
360 JJ: Enter a description for the first commit.
361 Add file1 & file2
362
363 JJ: This commit contains the following changes:
364 JJ: A file1
365 JJ:
366 JJ: Lines starting with "JJ:" (like this one) will be removed.
367 "#);
368 insta::assert_snapshot!(
369 std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r#"
370 JJ: Enter a description for the second commit.
371 Add file1 & file2
372
373 JJ: This commit contains the following changes:
374 JJ: A file2
375 JJ:
376 JJ: Lines starting with "JJ:" (like this one) will be removed.
377 "#);
378
379 // Check the evolog for the first commit. It shows four entries:
380 // - The initial empty commit.
381 // - The rewritten commit from the snapshot after the files were added.
382 // - The rewritten commit once the description is added during `jj commit`.
383 // - The rewritten commit after the split.
384 let evolog_1 = test_env.run_jj_in(&workspace_path, ["evolog", "-r", "qpvun"]);
385 insta::assert_snapshot!(evolog_1, @r###"
386 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:12 34dd141b
387 │ Add file1
388 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 764d46f1
389 │ Add file1 & file2
390 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 44af2155
391 │ (no description set)
392 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
393 (empty) (no description set)
394 [EOF]
395 "###);
396
397 // The evolog for the second commit is the same, except that the change id
398 // changes after the split.
399 let evolog_2 = test_env.run_jj_in(&workspace_path, ["evolog", "-r", "royxm"]);
400 insta::assert_snapshot!(evolog_2, @r###"
401 ○ royxmykx test.user@example.com 2001-02-03 08:05:12 465e03d0
402 │ Add file2
403 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 764d46f1
404 │ Add file1 & file2
405 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 44af2155
406 │ (no description set)
407 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
408 (empty) (no description set)
409 [EOF]
410 "###);
411}
412
413// This test makes sure that the children of the commit being split retain any
414// other parents which weren't involved in the split.
415#[test]
416fn test_split_with_merge_child() {
417 let mut test_env = TestEnvironment::default();
418 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
419 let workspace_path = test_env.env_root().join("repo");
420 test_env
421 .run_jj_in(&workspace_path, ["describe", "-m=1"])
422 .success();
423 test_env
424 .run_jj_in(&workspace_path, ["new", "root()", "-m=a"])
425 .success();
426 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
427 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
428 test_env
429 .run_jj_in(
430 &workspace_path,
431 ["new", "description(1)", "description(a)", "-m=2"],
432 )
433 .success();
434 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
435 @ zsuskulnrvyr true 2
436 ├─╮
437 │ ○ kkmpptxzrspx false a
438 ○ │ qpvuntsmwlqt true 1
439 ├─╯
440 ◆ zzzzzzzzzzzz true
441 [EOF]
442 ");
443
444 // Set up the editor and do the split.
445 let edit_script = test_env.set_up_fake_editor();
446 std::fs::write(
447 edit_script,
448 ["write\nAdd file1", "next invocation\n", "write\nAdd file2"].join("\0"),
449 )
450 .unwrap();
451 let output = test_env.run_jj_in(&workspace_path, ["split", "-r", "description(a)", "file1"]);
452 insta::assert_snapshot!(output, @r"
453 ------- stderr -------
454 Rebased 1 descendant commits
455 First part: kkmpptxz e8006b47 Add file1
456 Second part: royxmykx 5e1b793d Add file2
457 Working copy (@) now at: zsuskuln 696935af (empty) 2
458 Parent commit (@-) : qpvuntsm 8b64ddff (empty) 1
459 Parent commit (@-) : royxmykx 5e1b793d Add file2
460 [EOF]
461 ");
462 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
463 @ zsuskulnrvyr true 2
464 ├─╮
465 │ ○ royxmykxtrkr false Add file2
466 │ ○ kkmpptxzrspx false Add file1
467 ○ │ qpvuntsmwlqt true 1
468 ├─╯
469 ◆ zzzzzzzzzzzz true
470 [EOF]
471 ");
472}
473
474#[test]
475// Split a commit with no descendants into siblings. Also tests that the default
476// description is set correctly on the first commit.
477fn test_split_parallel_no_descendants() {
478 let mut test_env = TestEnvironment::default();
479 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
480 test_env.add_config(r#"ui.default-description = "\n\nTESTED=TODO""#);
481 let workspace_path = test_env.env_root().join("repo");
482
483 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
484 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
485
486 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
487 @ qpvuntsmwlqt false
488 ◆ zzzzzzzzzzzz true
489 [EOF]
490 ");
491
492 let edit_script = test_env.set_up_fake_editor();
493 std::fs::write(
494 edit_script,
495 ["dump editor1", "next invocation\n", "dump editor2"].join("\0"),
496 )
497 .unwrap();
498 let output = test_env.run_jj_in(&workspace_path, ["split", "--parallel", "file1"]);
499 insta::assert_snapshot!(output, @r"
500 ------- stderr -------
501 First part: qpvuntsm 48018df6 TESTED=TODO
502 Second part: kkmpptxz 7eddbf93 (no description set)
503 Working copy (@) now at: kkmpptxz 7eddbf93 (no description set)
504 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
505 Added 0 files, modified 0 files, removed 1 files
506 [EOF]
507 ");
508 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
509 @ kkmpptxzrspx false
510 │ ○ qpvuntsmwlqt false TESTED=TODO
511 ├─╯
512 ◆ zzzzzzzzzzzz true
513 [EOF]
514 ");
515
516 // Since the commit being split has no description, the user will only be
517 // prompted to add a description to the first commit, which will use the
518 // default value we set. The second commit will inherit the empty
519 // description from the commit being split.
520 insta::assert_snapshot!(
521 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
522 JJ: Enter a description for the first commit.
523
524
525 TESTED=TODO
526
527 JJ: This commit contains the following changes:
528 JJ: A file1
529 JJ:
530 JJ: Lines starting with "JJ:" (like this one) will be removed.
531 "#);
532 assert!(!test_env.env_root().join("editor2").exists());
533
534 // Check the evolog for the first commit. It shows three entries:
535 // - The initial empty commit.
536 // - The rewritten commit from the snapshot after the files were added.
537 // - The rewritten commit after the split.
538 let evolog_1 = test_env.run_jj_in(&workspace_path, ["evolog", "-r", "qpvun"]);
539 insta::assert_snapshot!(evolog_1, @r###"
540 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:09 48018df6
541 │ TESTED=TODO
542 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 44af2155
543 │ (no description set)
544 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
545 (empty) (no description set)
546 [EOF]
547 "###);
548
549 // The evolog for the second commit is the same, except that the change id
550 // changes after the split.
551 let evolog_2 = test_env.run_jj_in(&workspace_path, ["evolog", "-r", "kkmpp"]);
552 insta::assert_snapshot!(evolog_2, @r###"
553 @ kkmpptxz test.user@example.com 2001-02-03 08:05:09 7eddbf93
554 │ (no description set)
555 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 44af2155
556 │ (no description set)
557 ○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
558 (empty) (no description set)
559 [EOF]
560 "###);
561}
562
563#[test]
564fn test_split_parallel_with_descendants() {
565 // Configure the environment and make the initial commits.
566 let mut test_env = TestEnvironment::default();
567 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
568 let workspace_path = test_env.env_root().join("repo");
569
570 // First commit. This is the one we will split later.
571 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
572 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
573 test_env
574 .run_jj_in(&workspace_path, ["commit", "-m", "Add file1 & file2"])
575 .success();
576 // Second commit. This will be the child of the sibling commits after the split.
577 std::fs::write(workspace_path.join("file3"), "baz\n").unwrap();
578 test_env
579 .run_jj_in(&workspace_path, ["commit", "-m", "Add file3"])
580 .success();
581 // Third commit.
582 std::fs::write(workspace_path.join("file4"), "foobarbaz\n").unwrap();
583 test_env
584 .run_jj_in(&workspace_path, ["describe", "-m", "Add file4"])
585 .success();
586 // Move back to the previous commit so that we don't have to pass a revision
587 // to the split command.
588 test_env
589 .run_jj_in(&workspace_path, ["prev", "--edit"])
590 .success();
591 test_env
592 .run_jj_in(&workspace_path, ["prev", "--edit"])
593 .success();
594 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
595 ○ kkmpptxzrspx false Add file4
596 ○ rlvkpnrzqnoo false Add file3
597 @ qpvuntsmwlqt false Add file1 & file2
598 ◆ zzzzzzzzzzzz true
599 [EOF]
600 ");
601
602 // Set up the editor and do the split.
603 let edit_script = test_env.set_up_fake_editor();
604 std::fs::write(
605 edit_script,
606 [
607 "dump editor1",
608 "write\nAdd file1",
609 "next invocation\n",
610 "dump editor2",
611 "write\nAdd file2",
612 ]
613 .join("\0"),
614 )
615 .unwrap();
616 let output = test_env.run_jj_in(&workspace_path, ["split", "--parallel", "file1"]);
617 insta::assert_snapshot!(output, @r"
618 ------- stderr -------
619 Rebased 2 descendant commits
620 First part: qpvuntsm 84df941d Add file1
621 Second part: vruxwmqv 94753be3 Add file2
622 Working copy (@) now at: vruxwmqv 94753be3 Add file2
623 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
624 Added 0 files, modified 0 files, removed 1 files
625 [EOF]
626 ");
627 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
628 ○ kkmpptxzrspx false Add file4
629 ○ rlvkpnrzqnoo false Add file3
630 ├─╮
631 │ @ vruxwmqvtpmx false Add file2
632 ○ │ qpvuntsmwlqt false Add file1
633 ├─╯
634 ◆ zzzzzzzzzzzz true
635 [EOF]
636 ");
637
638 // The commit we're splitting has a description, so the user will be
639 // prompted to enter a description for each of the sibling commits.
640 insta::assert_snapshot!(
641 std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r#"
642 JJ: Enter a description for the first commit.
643 Add file1 & file2
644
645 JJ: This commit contains the following changes:
646 JJ: A file1
647 JJ:
648 JJ: Lines starting with "JJ:" (like this one) will be removed.
649 "#);
650 insta::assert_snapshot!(
651 std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r#"
652 JJ: Enter a description for the second commit.
653 Add file1 & file2
654
655 JJ: This commit contains the following changes:
656 JJ: A file2
657 JJ:
658 JJ: Lines starting with "JJ:" (like this one) will be removed.
659 "#);
660}
661
662// This test makes sure that the children of the commit being split retain any
663// other parents which weren't involved in the split.
664#[test]
665fn test_split_parallel_with_merge_child() {
666 let mut test_env = TestEnvironment::default();
667 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
668 let workspace_path = test_env.env_root().join("repo");
669 test_env
670 .run_jj_in(&workspace_path, ["describe", "-m=1"])
671 .success();
672 test_env
673 .run_jj_in(&workspace_path, ["new", "root()", "-m=a"])
674 .success();
675 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
676 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
677 test_env
678 .run_jj_in(
679 &workspace_path,
680 ["new", "description(1)", "description(a)", "-m=2"],
681 )
682 .success();
683 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
684 @ zsuskulnrvyr true 2
685 ├─╮
686 │ ○ kkmpptxzrspx false a
687 ○ │ qpvuntsmwlqt true 1
688 ├─╯
689 ◆ zzzzzzzzzzzz true
690 [EOF]
691 ");
692
693 // Set up the editor and do the split.
694 let edit_script = test_env.set_up_fake_editor();
695 std::fs::write(
696 edit_script,
697 ["write\nAdd file1", "next invocation\n", "write\nAdd file2"].join("\0"),
698 )
699 .unwrap();
700 let output = test_env.run_jj_in(
701 &workspace_path,
702 ["split", "-r", "description(a)", "--parallel", "file1"],
703 );
704 insta::assert_snapshot!(output, @r"
705 ------- stderr -------
706 Rebased 1 descendant commits
707 First part: kkmpptxz e8006b47 Add file1
708 Second part: royxmykx 2cc60f3d Add file2
709 Working copy (@) now at: zsuskuln 35b5d7eb (empty) 2
710 Parent commit (@-) : qpvuntsm 8b64ddff (empty) 1
711 Parent commit (@-) : kkmpptxz e8006b47 Add file1
712 Parent commit (@-) : royxmykx 2cc60f3d Add file2
713 [EOF]
714 ");
715 insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r"
716 @ zsuskulnrvyr true 2
717 ├─┬─╮
718 │ │ ○ royxmykxtrkr false Add file2
719 │ ○ │ kkmpptxzrspx false Add file1
720 │ ├─╯
721 ○ │ qpvuntsmwlqt true 1
722 ├─╯
723 ◆ zzzzzzzzzzzz true
724 [EOF]
725 ");
726}
727
728// Make sure `jj split` would refuse to split an empty commit.
729#[test]
730fn test_split_empty() {
731 let test_env = TestEnvironment::default();
732 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
733 let workspace_path = test_env.env_root().join("repo");
734 test_env
735 .run_jj_in(&workspace_path, ["describe", "--message", "abc"])
736 .success();
737
738 let output = test_env.run_jj_in(&workspace_path, ["split"]);
739 insta::assert_snapshot!(output, @r"
740 ------- stderr -------
741 Error: Refusing to split empty commit 2ab033062e9fdf7fad2ded8e89c1f145e3698190.
742 Hint: Use `jj new` if you want to create another empty commit.
743 [EOF]
744 [exit status: 1]
745 ");
746}
747
748#[test]
749fn test_split_message_editor_avoids_unc() {
750 let mut test_env = TestEnvironment::default();
751 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
752 let repo_path = test_env.env_root().join("repo");
753
754 std::fs::write(repo_path.join("file1"), "foo").unwrap();
755 std::fs::write(repo_path.join("file2"), "foo").unwrap();
756
757 let edit_script = test_env.set_up_fake_editor();
758 std::fs::write(edit_script, "dump-path path").unwrap();
759 test_env.run_jj_in(&repo_path, ["split", "file2"]).success();
760
761 let edited_path =
762 PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap());
763 // While `assert!(!edited_path.starts_with("//?/"))` could work here in most
764 // cases, it fails when it is not safe to strip the prefix, such as paths
765 // over 260 chars.
766 assert_eq!(edited_path, dunce::simplified(&edited_path));
767}
768
769#[test]
770fn test_split_interactive() {
771 let mut test_env = TestEnvironment::default();
772 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
773 let workspace_path = test_env.env_root().join("repo");
774
775 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
776 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
777 let edit_script = test_env.set_up_fake_editor();
778 std::fs::write(edit_script, ["dump editor"].join("\0")).unwrap();
779
780 let diff_editor = test_env.set_up_fake_diff_editor();
781 let diff_script = ["rm file2", "dump JJ-INSTRUCTIONS instrs"].join("\0");
782 std::fs::write(diff_editor, diff_script).unwrap();
783
784 // Split the working commit interactively and select only file1
785 let output = test_env.run_jj_in(&workspace_path, ["split"]);
786 insta::assert_snapshot!(output, @r"
787 ------- stderr -------
788 First part: qpvuntsm 0e15949e (no description set)
789 Second part: rlvkpnrz 9ed12e4c (no description set)
790 Working copy (@) now at: rlvkpnrz 9ed12e4c (no description set)
791 Parent commit (@-) : qpvuntsm 0e15949e (no description set)
792 [EOF]
793 ");
794
795 insta::assert_snapshot!(
796 std::fs::read_to_string(test_env.env_root().join("instrs")).unwrap(), @r"
797 You are splitting a commit into two: qpvuntsm 44af2155 (no description set)
798
799 The diff initially shows the changes in the commit you're splitting.
800
801 Adjust the right side until it shows the contents you want for the first commit.
802 The remainder will be in the second commit.
803 ");
804
805 insta::assert_snapshot!(
806 std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r#"
807 JJ: Enter a description for the first commit.
808
809
810 JJ: This commit contains the following changes:
811 JJ: A file1
812 JJ:
813 JJ: Lines starting with "JJ:" (like this one) will be removed.
814 "#);
815
816 let output = test_env.run_jj_in(&workspace_path, ["log", "--summary"]);
817 insta::assert_snapshot!(output, @r"
818 @ rlvkpnrz test.user@example.com 2001-02-03 08:05:08 9ed12e4c
819 │ (no description set)
820 │ A file2
821 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 0e15949e
822 │ (no description set)
823 │ A file1
824 ◆ zzzzzzzz root() 00000000
825 [EOF]
826 ");
827}
828
829#[test]
830fn test_split_interactive_with_paths() {
831 let mut test_env = TestEnvironment::default();
832 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
833 let workspace_path = test_env.env_root().join("repo");
834
835 std::fs::write(workspace_path.join("file2"), "").unwrap();
836 std::fs::write(workspace_path.join("file3"), "").unwrap();
837 test_env.run_jj_in(&workspace_path, ["new"]).success();
838 std::fs::write(workspace_path.join("file1"), "foo\n").unwrap();
839 std::fs::write(workspace_path.join("file2"), "bar\n").unwrap();
840 std::fs::write(workspace_path.join("file3"), "baz\n").unwrap();
841
842 let edit_script = test_env.set_up_fake_editor();
843 std::fs::write(edit_script, ["dump editor"].join("\0")).unwrap();
844 let diff_editor = test_env.set_up_fake_diff_editor();
845 // On the before side, file2 is empty. On the after side, it contains "bar".
846 // The "reset file2" copies the empty version from the before side to the
847 // after side, effectively "unselecting" the changes and leaving only the
848 // changes made to file1. file3 doesn't appear on either side since it isn't
849 // in the filesets passed to `jj split`.
850 let diff_script = [
851 "files-before file2",
852 "files-after JJ-INSTRUCTIONS file1 file2",
853 "reset file2",
854 ]
855 .join("\0");
856 std::fs::write(diff_editor, diff_script).unwrap();
857
858 // Select file1 and file2 by args, then select file1 interactively via the diff
859 // script.
860 let output = test_env.run_jj_in(&workspace_path, ["split", "-i", "file1", "file2"]);
861 insta::assert_snapshot!(output, @r"
862 ------- stderr -------
863 First part: rlvkpnrz e3d766b8 (no description set)
864 Second part: kkmpptxz 4cf22d3b (no description set)
865 Working copy (@) now at: kkmpptxz 4cf22d3b (no description set)
866 Parent commit (@-) : rlvkpnrz e3d766b8 (no description set)
867 [EOF]
868 ");
869
870 insta::assert_snapshot!(
871 std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r#"
872 JJ: Enter a description for the first commit.
873
874
875 JJ: This commit contains the following changes:
876 JJ: A file1
877 JJ:
878 JJ: Lines starting with "JJ:" (like this one) will be removed.
879 "#);
880
881 let output = test_env.run_jj_in(&workspace_path, ["log", "--summary"]);
882 insta::assert_snapshot!(output, @r"
883 @ kkmpptxz test.user@example.com 2001-02-03 08:05:09 4cf22d3b
884 │ (no description set)
885 │ M file2
886 │ M file3
887 ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 e3d766b8
888 │ (no description set)
889 │ A file1
890 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 497ed465
891 │ (no description set)
892 │ A file2
893 │ A file3
894 ◆ zzzzzzzz root() 00000000
895 [EOF]
896 ");
897}
898
899// When a commit is split, the second commit produced by the split becomes the
900// working copy commit for all workspaces whose working copy commit was the
901// target of the split. This test does a split where the target commit is the
902// working copy commit for two different workspaces.
903#[test]
904fn test_split_with_multiple_workspaces_same_working_copy() {
905 let mut test_env = TestEnvironment::default();
906 test_env.run_jj_in(".", ["git", "init", "main"]).success();
907 let main_path = test_env.env_root().join("main");
908 let secondary_path = test_env.env_root().join("secondary");
909
910 test_env
911 .run_jj_in(&main_path, ["desc", "-m", "first-commit"])
912 .success();
913 std::fs::write(main_path.join("file1"), "foo").unwrap();
914 std::fs::write(main_path.join("file2"), "foo").unwrap();
915
916 // Create the second workspace and change its working copy commit to match
917 // the default workspace.
918 test_env
919 .run_jj_in(
920 &main_path,
921 ["workspace", "add", "--name", "second", "../secondary"],
922 )
923 .success();
924 // Change the working copy in the second workspace.
925 test_env
926 .run_jj_in(&secondary_path, ["edit", "-r", "description(first-commit)"])
927 .success();
928 // Check the working-copy commit in each workspace in the log output. The "@"
929 // node in the graph indicates the current workspace's working-copy commit.
930 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
931 @ qpvuntsmwlqt default@ second@ first-commit
932 ◆ zzzzzzzzzzzz
933 [EOF]
934 ");
935
936 // Do the split in the default workspace.
937 std::fs::write(
938 test_env.set_up_fake_editor(),
939 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
940 )
941 .unwrap();
942 test_env.run_jj_in(&main_path, ["split", "file2"]).success();
943 // The working copy for both workspaces will be the second split commit.
944 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
945 @ royxmykxtrkr default@ second@ second-commit
946 ○ qpvuntsmwlqt first-commit
947 ◆ zzzzzzzzzzzz
948 [EOF]
949 ");
950
951 // Test again with a --parallel split.
952 test_env.run_jj_in(&main_path, ["undo"]).success();
953 std::fs::write(
954 test_env.set_up_fake_editor(),
955 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
956 )
957 .unwrap();
958 test_env
959 .run_jj_in(&main_path, ["split", "file2", "--parallel"])
960 .success();
961 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
962 @ yostqsxwqrlt default@ second@ second-commit
963 │ ○ qpvuntsmwlqt first-commit
964 ├─╯
965 ◆ zzzzzzzzzzzz
966 [EOF]
967 ");
968}
969
970// A workspace should only have its working copy commit updated if the target
971// commit is the working copy commit.
972#[test]
973fn test_split_with_multiple_workspaces_different_working_copy() {
974 let mut test_env = TestEnvironment::default();
975 test_env.run_jj_in(".", ["git", "init", "main"]).success();
976 let main_path = test_env.env_root().join("main");
977
978 test_env
979 .run_jj_in(&main_path, ["desc", "-m", "first-commit"])
980 .success();
981 std::fs::write(main_path.join("file1"), "foo").unwrap();
982 std::fs::write(main_path.join("file2"), "foo").unwrap();
983
984 // Create the second workspace with a different working copy commit.
985 test_env
986 .run_jj_in(
987 &main_path,
988 ["workspace", "add", "--name", "second", "../secondary"],
989 )
990 .success();
991 // Check the working-copy commit in each workspace in the log output. The "@"
992 // node in the graph indicates the current workspace's working-copy commit.
993 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
994 @ qpvuntsmwlqt default@ first-commit
995 │ ○ pmmvwywvzvvn second@
996 ├─╯
997 ◆ zzzzzzzzzzzz
998 [EOF]
999 ");
1000
1001 // Do the split in the default workspace.
1002 std::fs::write(
1003 test_env.set_up_fake_editor(),
1004 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
1005 )
1006 .unwrap();
1007 test_env.run_jj_in(&main_path, ["split", "file2"]).success();
1008 // Only the working copy commit for the default workspace changes.
1009 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
1010 @ mzvwutvlkqwt default@ second-commit
1011 ○ qpvuntsmwlqt first-commit
1012 │ ○ pmmvwywvzvvn second@
1013 ├─╯
1014 ◆ zzzzzzzzzzzz
1015 [EOF]
1016 ");
1017
1018 // Test again with a --parallel split.
1019 test_env.run_jj_in(&main_path, ["undo"]).success();
1020 std::fs::write(
1021 test_env.set_up_fake_editor(),
1022 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
1023 )
1024 .unwrap();
1025 test_env
1026 .run_jj_in(&main_path, ["split", "file2", "--parallel"])
1027 .success();
1028 insta::assert_snapshot!(get_workspace_log_output(&test_env, &main_path), @r"
1029 @ vruxwmqvtpmx default@ second-commit
1030 │ ○ qpvuntsmwlqt first-commit
1031 ├─╯
1032 │ ○ pmmvwywvzvvn second@
1033 ├─╯
1034 ◆ zzzzzzzzzzzz
1035 [EOF]
1036 ");
1037}
1038
1039enum BookmarkBehavior {
1040 Default,
1041 MoveBookmarkToChild,
1042 LeaveBookmarkWithTarget,
1043}
1044
1045// TODO: https://github.com/jj-vcs/jj/issues/3419 - Delete params when the config is removed.
1046#[test_case(BookmarkBehavior::Default; "default_behavior")]
1047#[test_case(BookmarkBehavior::MoveBookmarkToChild; "move_bookmark_to_child")]
1048#[test_case(BookmarkBehavior::LeaveBookmarkWithTarget; "leave_bookmark_with_target")]
1049fn test_split_with_bookmarks(bookmark_behavior: BookmarkBehavior) {
1050 let mut test_env = TestEnvironment::default();
1051 test_env.run_jj_in(".", ["git", "init", "main"]).success();
1052 let main_path = test_env.env_root().join("main");
1053
1054 match bookmark_behavior {
1055 BookmarkBehavior::LeaveBookmarkWithTarget => {
1056 test_env.add_config("split.legacy-bookmark-behavior=false");
1057 }
1058 BookmarkBehavior::MoveBookmarkToChild => {
1059 test_env.add_config("split.legacy-bookmark-behavior=true");
1060 }
1061 BookmarkBehavior::Default => (),
1062 }
1063
1064 // Setup.
1065 test_env
1066 .run_jj_in(&main_path, ["desc", "-m", "first-commit"])
1067 .success();
1068 std::fs::write(main_path.join("file1"), "foo").unwrap();
1069 std::fs::write(main_path.join("file2"), "foo").unwrap();
1070 test_env
1071 .run_jj_in(&main_path, ["bookmark", "set", "'*le-signet*'", "-r", "@"])
1072 .success();
1073 insta::allow_duplicates! {
1074 insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
1075 @ qpvuntsmwlqt false *le-signet* first-commit
1076 ◆ zzzzzzzzzzzz true
1077 [EOF]
1078 ");
1079 }
1080
1081 // Do the split.
1082 std::fs::write(
1083 test_env.set_up_fake_editor(),
1084 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
1085 )
1086 .unwrap();
1087 let output = test_env.run_jj_in(&main_path, ["split", "file2"]);
1088 match bookmark_behavior {
1089 BookmarkBehavior::LeaveBookmarkWithTarget => {
1090 insta::allow_duplicates! {
1091 insta::assert_snapshot!(output, @r"
1092 ------- stderr -------
1093 First part: qpvuntsm 63d0c5ed *le-signet* | first-commit
1094 Second part: mzvwutvl a9f5665f second-commit
1095 Working copy (@) now at: mzvwutvl a9f5665f second-commit
1096 Parent commit (@-) : qpvuntsm 63d0c5ed *le-signet* | first-commit
1097 [EOF]
1098 ");
1099 }
1100 insta::allow_duplicates! {
1101 insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
1102 @ mzvwutvlkqwt false second-commit
1103 ○ qpvuntsmwlqt false *le-signet* first-commit
1104 ◆ zzzzzzzzzzzz true
1105 [EOF]
1106 ");
1107 }
1108 }
1109 BookmarkBehavior::Default | BookmarkBehavior::MoveBookmarkToChild => {
1110 insta::allow_duplicates! {
1111 insta::assert_snapshot!(output, @r"
1112 ------- stderr -------
1113 First part: qpvuntsm 63d0c5ed first-commit
1114 Second part: mzvwutvl a9f5665f *le-signet* | second-commit
1115 Working copy (@) now at: mzvwutvl a9f5665f *le-signet* | second-commit
1116 Parent commit (@-) : qpvuntsm 63d0c5ed first-commit
1117 [EOF]
1118 ");
1119 }
1120 insta::allow_duplicates! {
1121 insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
1122 @ mzvwutvlkqwt false *le-signet* second-commit
1123 ○ qpvuntsmwlqt false first-commit
1124 ◆ zzzzzzzzzzzz true
1125 [EOF]
1126 ");
1127 }
1128 }
1129 }
1130
1131 // Test again with a --parallel split.
1132 test_env.run_jj_in(&main_path, ["undo"]).success();
1133 std::fs::write(
1134 test_env.set_up_fake_editor(),
1135 ["", "next invocation\n", "write\nsecond-commit"].join("\0"),
1136 )
1137 .unwrap();
1138 test_env
1139 .run_jj_in(&main_path, ["split", "file2", "--parallel"])
1140 .success();
1141 match bookmark_behavior {
1142 BookmarkBehavior::LeaveBookmarkWithTarget => {
1143 insta::allow_duplicates! {
1144 insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
1145 @ vruxwmqvtpmx false second-commit
1146 │ ○ qpvuntsmwlqt false *le-signet* first-commit
1147 ├─╯
1148 ◆ zzzzzzzzzzzz true
1149 [EOF]
1150 ");
1151 }
1152 }
1153 BookmarkBehavior::Default | BookmarkBehavior::MoveBookmarkToChild => {
1154 insta::allow_duplicates! {
1155 insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r"
1156 @ vruxwmqvtpmx false *le-signet* second-commit
1157 │ ○ qpvuntsmwlqt false first-commit
1158 ├─╯
1159 ◆ zzzzzzzzzzzz true
1160 [EOF]
1161 ");
1162 }
1163 }
1164 }
1165}