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::fmt::Write as _;
16use std::path::Path;
17
18use testutils::git;
19
20use crate::common::CommandOutput;
21use crate::common::TestEnvironment;
22use crate::common::TestWorkDir;
23
24#[test]
25fn test_git_colocated() {
26 let test_env = TestEnvironment::default();
27 let work_dir = test_env.work_dir("repo");
28 let git_repo = git::init(work_dir.root());
29
30 // Create an initial commit in Git
31 let tree_id = git::add_commit(
32 &git_repo,
33 "refs/heads/master",
34 "file",
35 b"contents",
36 "initial",
37 &[],
38 )
39 .tree_id;
40 git::checkout_tree_index(&git_repo, tree_id);
41 assert_eq!(work_dir.read_file("file"), b"contents");
42 insta::assert_snapshot!(
43 git_repo.head_id().unwrap().to_string(),
44 @"97358f54806c7cd005ed5ade68a779595efbae7e"
45 );
46
47 // Import the repo
48 work_dir
49 .run_jj(["git", "init", "--git-repo", "."])
50 .success();
51 insta::assert_snapshot!(get_log_output(&work_dir), @r"
52 @ c3a12656d5027825bc69f40e11dc0bb381d7c277
53 ○ 97358f54806c7cd005ed5ade68a779595efbae7e master git_head() initial
54 ◆ 0000000000000000000000000000000000000000
55 [EOF]
56 ");
57 insta::assert_snapshot!(
58 git_repo.head_id().unwrap().to_string(),
59 @"97358f54806c7cd005ed5ade68a779595efbae7e"
60 );
61
62 // Modify the working copy. The working-copy commit should changed, but the Git
63 // HEAD commit should not
64 work_dir.write_file("file", "modified");
65 insta::assert_snapshot!(get_log_output(&work_dir), @r"
66 @ 59642000f061cf23eb37ab6eecce428afe7824da
67 ○ 97358f54806c7cd005ed5ade68a779595efbae7e master git_head() initial
68 ◆ 0000000000000000000000000000000000000000
69 [EOF]
70 ");
71 insta::assert_snapshot!(
72 git_repo.head_id().unwrap().to_string(),
73 @"97358f54806c7cd005ed5ade68a779595efbae7e"
74 );
75
76 // Create a new change from jj and check that it's reflected in Git
77 work_dir.run_jj(["new"]).success();
78 insta::assert_snapshot!(get_log_output(&work_dir), @r"
79 @ fd4e130e43068d76ec6eb2c6df01653ea12eccb4
80 ○ 59642000f061cf23eb37ab6eecce428afe7824da git_head()
81 ○ 97358f54806c7cd005ed5ade68a779595efbae7e master initial
82 ◆ 0000000000000000000000000000000000000000
83 [EOF]
84 ");
85 assert!(git_repo.head().unwrap().is_detached());
86 insta::assert_snapshot!(
87 git_repo.head_id().unwrap().to_string(),
88 @"59642000f061cf23eb37ab6eecce428afe7824da"
89 );
90}
91
92#[test]
93fn test_git_colocated_intent_to_add() {
94 let test_env = TestEnvironment::default();
95 test_env
96 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
97 .success();
98 let work_dir = test_env.work_dir("repo");
99
100 // A file added directly on top of the root commit should be marked as
101 // intent-to-add
102 work_dir.write_file("file1.txt", "contents");
103 work_dir.run_jj(["status"]).success();
104 insta::assert_snapshot!(get_index_state(work_dir.root()), @"Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 file1.txt");
105
106 // Another new file should be marked as intent-to-add
107 work_dir.run_jj(["new"]).success();
108 work_dir.write_file("file2.txt", "contents");
109 work_dir.run_jj(["status"]).success();
110 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
111 Unconflicted Mode(FILE) 0839b2e9412b ctime=0:0 mtime=0:0 size=0 flags=0 file1.txt
112 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 file2.txt
113 ");
114
115 // After creating a new commit, it should not longer be marked as intent-to-add
116 work_dir.run_jj(["new"]).success();
117 work_dir.write_file("file2.txt", "contents");
118 work_dir.run_jj(["status"]).success();
119 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
120 Unconflicted Mode(FILE) 0839b2e9412b ctime=0:0 mtime=0:0 size=0 flags=0 file1.txt
121 Unconflicted Mode(FILE) 0839b2e9412b ctime=0:0 mtime=0:0 size=0 flags=0 file2.txt
122 ");
123
124 // If we edit an existing commit, new files are marked as intent-to-add
125 work_dir.run_jj(["edit", "@-"]).success();
126 work_dir.run_jj(["status"]).success();
127 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
128 Unconflicted Mode(FILE) 0839b2e9412b ctime=0:0 mtime=0:0 size=0 flags=0 file1.txt
129 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 file2.txt
130 ");
131
132 // If we remove the added file, it's removed from the index
133 work_dir.remove_file("file2.txt");
134 work_dir.run_jj(["status"]).success();
135 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
136 Unconflicted Mode(FILE) 0839b2e9412b ctime=0:0 mtime=0:0 size=0 flags=0 file1.txt
137 ");
138}
139
140#[test]
141fn test_git_colocated_unborn_bookmark() {
142 let test_env = TestEnvironment::default();
143 let work_dir = test_env.work_dir("repo");
144 let git_repo = git::init(work_dir.root());
145
146 // add a file to an (in memory) index
147 let add_file_to_index = |name: &str, data: &str| {
148 let mut index_manager = git::IndexManager::new(&git_repo);
149 index_manager.add_file(name, data.as_bytes());
150 index_manager.sync_index();
151 };
152
153 // checkout index (i.e., drop the in-memory changes)
154 let checkout_index = || {
155 let mut index = git_repo.open_index().unwrap();
156 let objects = git_repo.objects.clone();
157 gix::worktree::state::checkout(
158 &mut index,
159 git_repo.work_dir().unwrap(),
160 objects,
161 &gix::progress::Discard,
162 &gix::progress::Discard,
163 &gix::interrupt::IS_INTERRUPTED,
164 gix::worktree::state::checkout::Options::default(),
165 )
166 .unwrap();
167 };
168
169 // Initially, HEAD isn't set.
170 work_dir
171 .run_jj(["git", "init", "--git-repo", "."])
172 .success();
173 assert!(git_repo.head().unwrap().is_unborn());
174 assert_eq!(
175 git_repo.head_name().unwrap().unwrap().as_bstr(),
176 b"refs/heads/master"
177 );
178 insta::assert_snapshot!(get_log_output(&work_dir), @r"
179 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
180 ◆ 0000000000000000000000000000000000000000
181 [EOF]
182 ");
183
184 // Stage some change, and check out root. This shouldn't clobber the HEAD.
185 add_file_to_index("file0", "");
186 let output = work_dir.run_jj(["new", "root()"]);
187 insta::assert_snapshot!(output, @r"
188 ------- stderr -------
189 Working copy (@) now at: kkmpptxz fcdbbd73 (empty) (no description set)
190 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
191 Added 0 files, modified 0 files, removed 1 files
192 [EOF]
193 ");
194 assert!(git_repo.head().unwrap().is_unborn());
195 assert_eq!(
196 git_repo.head_name().unwrap().unwrap().as_bstr(),
197 b"refs/heads/master"
198 );
199 insta::assert_snapshot!(get_log_output(&work_dir), @r"
200 @ fcdbbd731496cae17161cd6be9b6cf1f759655a8
201 │ ○ 993600f1189571af5bbeb492cf657dc7d0fde48a
202 ├─╯
203 ◆ 0000000000000000000000000000000000000000
204 [EOF]
205 ");
206 // Staged change shouldn't persist.
207 checkout_index();
208 insta::assert_snapshot!(work_dir.run_jj(["status"]), @r"
209 The working copy has no changes.
210 Working copy (@) : kkmpptxz fcdbbd73 (empty) (no description set)
211 Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
212 [EOF]
213 ");
214
215 // Stage some change, and create new HEAD. This shouldn't move the default
216 // bookmark.
217 add_file_to_index("file1", "");
218 let output = work_dir.run_jj(["new"]);
219 insta::assert_snapshot!(output, @r"
220 ------- stderr -------
221 Working copy (@) now at: royxmykx 0e146103 (empty) (no description set)
222 Parent commit (@-) : kkmpptxz e3e01407 (no description set)
223 [EOF]
224 ");
225 assert!(git_repo.head().unwrap().is_detached());
226 insta::assert_snapshot!(
227 git_repo.head_id().unwrap().to_string(),
228 @"e3e01407bd3539722ae4ffff077700d97c60cb11"
229 );
230 insta::assert_snapshot!(get_log_output(&work_dir), @r"
231 @ 0e14610343ef50775f5c44db5aeef19aee45d9ad
232 ○ e3e01407bd3539722ae4ffff077700d97c60cb11 git_head()
233 │ ○ 993600f1189571af5bbeb492cf657dc7d0fde48a
234 ├─╯
235 ◆ 0000000000000000000000000000000000000000
236 [EOF]
237 ");
238 // Staged change shouldn't persist.
239 checkout_index();
240 insta::assert_snapshot!(work_dir.run_jj(["status"]), @r"
241 The working copy has no changes.
242 Working copy (@) : royxmykx 0e146103 (empty) (no description set)
243 Parent commit (@-): kkmpptxz e3e01407 (no description set)
244 [EOF]
245 ");
246
247 // Assign the default bookmark. The bookmark is no longer "unborn".
248 work_dir
249 .run_jj(["bookmark", "create", "-r@-", "master"])
250 .success();
251
252 // Stage some change, and check out root again. This should unset the HEAD.
253 // https://github.com/jj-vcs/jj/issues/1495
254 add_file_to_index("file2", "");
255 let output = work_dir.run_jj(["new", "root()"]);
256 insta::assert_snapshot!(output, @r"
257 ------- stderr -------
258 Working copy (@) now at: znkkpsqq 10dd328b (empty) (no description set)
259 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
260 Added 0 files, modified 0 files, removed 2 files
261 [EOF]
262 ");
263 assert!(git_repo.head().unwrap().is_unborn());
264 insta::assert_snapshot!(get_log_output(&work_dir), @r"
265 @ 10dd328bb906e15890e55047740eab2812a3b2f7
266 │ ○ ef75c0b0dcc9b080e00226908c21316acaa84dc6
267 │ ○ e3e01407bd3539722ae4ffff077700d97c60cb11 master
268 ├─╯
269 │ ○ 993600f1189571af5bbeb492cf657dc7d0fde48a
270 ├─╯
271 ◆ 0000000000000000000000000000000000000000
272 [EOF]
273 ");
274 // Staged change shouldn't persist.
275 checkout_index();
276 insta::assert_snapshot!(work_dir.run_jj(["status"]), @r"
277 The working copy has no changes.
278 Working copy (@) : znkkpsqq 10dd328b (empty) (no description set)
279 Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
280 [EOF]
281 ");
282
283 // New snapshot and commit can be created after the HEAD got unset.
284 work_dir.write_file("file3", "");
285 let output = work_dir.run_jj(["new"]);
286 insta::assert_snapshot!(output, @r"
287 ------- stderr -------
288 Working copy (@) now at: wqnwkozp 101e2723 (empty) (no description set)
289 Parent commit (@-) : znkkpsqq fc8af934 (no description set)
290 [EOF]
291 ");
292 insta::assert_snapshot!(get_log_output(&work_dir), @r"
293 @ 101e272377a9daff75358f10dbd078df922fe68c
294 ○ fc8af9345b0830dcb14716e04cd2af26e2d19f63 git_head()
295 │ ○ ef75c0b0dcc9b080e00226908c21316acaa84dc6
296 │ ○ e3e01407bd3539722ae4ffff077700d97c60cb11 master
297 ├─╯
298 │ ○ 993600f1189571af5bbeb492cf657dc7d0fde48a
299 ├─╯
300 ◆ 0000000000000000000000000000000000000000
301 [EOF]
302 ");
303}
304
305#[test]
306fn test_git_colocated_export_bookmarks_on_snapshot() {
307 // Checks that we export bookmarks that were changed only because the working
308 // copy was snapshotted
309
310 let test_env = TestEnvironment::default();
311 let work_dir = test_env.work_dir("repo");
312 let git_repo = git::init(work_dir.root());
313 work_dir
314 .run_jj(["git", "init", "--git-repo", "."])
315 .success();
316
317 // Create bookmark pointing to the initial commit
318 work_dir.write_file("file", "initial");
319 work_dir
320 .run_jj(["bookmark", "create", "-r@", "foo"])
321 .success();
322 insta::assert_snapshot!(get_log_output(&work_dir), @r"
323 @ b15ef4cdd277d2c63cce6d67c1916f53a36141f7 foo
324 ◆ 0000000000000000000000000000000000000000
325 [EOF]
326 ");
327
328 // The bookmark gets updated when we modify the working copy, and it should get
329 // exported to Git without requiring any other changes
330 work_dir.write_file("file", "modified");
331 insta::assert_snapshot!(get_log_output(&work_dir), @r"
332 @ 4d2c49a8f8e2f1ba61f48ba79e5f4a5faa6512cf foo
333 ◆ 0000000000000000000000000000000000000000
334 [EOF]
335 ");
336 insta::assert_snapshot!(git_repo
337 .find_reference("refs/heads/foo")
338 .unwrap()
339 .id()
340 .to_string(), @"4d2c49a8f8e2f1ba61f48ba79e5f4a5faa6512cf");
341}
342
343#[test]
344fn test_git_colocated_rebase_on_import() {
345 let test_env = TestEnvironment::default();
346 let work_dir = test_env.work_dir("repo");
347 let git_repo = git::init(work_dir.root());
348 work_dir
349 .run_jj(["git", "init", "--git-repo", "."])
350 .success();
351
352 // Make some changes in jj and check that they're reflected in git
353 work_dir.write_file("file", "contents");
354 work_dir.run_jj(["commit", "-m", "add a file"]).success();
355 work_dir.write_file("file", "modified");
356 work_dir
357 .run_jj(["bookmark", "create", "-r@", "master"])
358 .success();
359 work_dir.run_jj(["commit", "-m", "modify a file"]).success();
360 // TODO: We shouldn't need this command here to trigger an import of the
361 // refs/heads/master we just exported
362 work_dir.run_jj(["st"]).success();
363
364 // Move `master` backwards, which should result in commit2 getting hidden,
365 // and the working-copy commit rebased.
366 let parent_commit = git_repo
367 .find_reference("refs/heads/master")
368 .unwrap()
369 .peel_to_commit()
370 .unwrap()
371 .parent_ids()
372 .next()
373 .unwrap()
374 .detach();
375 git_repo
376 .reference(
377 "refs/heads/master",
378 parent_commit,
379 gix::refs::transaction::PreviousValue::Any,
380 "update ref",
381 )
382 .unwrap();
383 insta::assert_snapshot!(get_log_output(&work_dir), @r"
384 @ 15b1d70c5e33b5d2b18383292b85324d5153ffed
385 ○ 47fe984daf66f7bf3ebf31b9cb3513c995afb857 master git_head() add a file
386 ◆ 0000000000000000000000000000000000000000
387 [EOF]
388 ------- stderr -------
389 Abandoned 1 commits that are no longer reachable.
390 Rebased 1 descendant commits off of commits rewritten from git
391 Working copy (@) now at: zsuskuln 15b1d70c (empty) (no description set)
392 Parent commit (@-) : qpvuntsm 47fe984d master | add a file
393 Added 0 files, modified 1 files, removed 0 files
394 Done importing changes from the underlying Git repo.
395 [EOF]
396 ");
397}
398
399#[test]
400fn test_git_colocated_bookmarks() {
401 let test_env = TestEnvironment::default();
402 let work_dir = test_env.work_dir("repo");
403 let git_repo = git::init(work_dir.root());
404 work_dir
405 .run_jj(["git", "init", "--git-repo", "."])
406 .success();
407 work_dir.run_jj(["new", "-m", "foo"]).success();
408 work_dir.run_jj(["new", "@-", "-m", "bar"]).success();
409 insta::assert_snapshot!(get_log_output(&work_dir), @r"
410 @ 3560559274ab431feea00b7b7e0b9250ecce951f bar
411 │ ○ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda foo
412 ├─╯
413 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 git_head()
414 ◆ 0000000000000000000000000000000000000000
415 [EOF]
416 ");
417
418 // Create a bookmark in jj. It should be exported to Git even though it points
419 // to the working- copy commit.
420 work_dir
421 .run_jj(["bookmark", "create", "-r@", "master"])
422 .success();
423 insta::assert_snapshot!(
424 git_repo.find_reference("refs/heads/master").unwrap().target().id().to_string(),
425 @"3560559274ab431feea00b7b7e0b9250ecce951f"
426 );
427 assert!(git_repo.head().unwrap().is_detached());
428 insta::assert_snapshot!(
429 git_repo.head_id().unwrap().to_string(),
430 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22"
431 );
432
433 // Update the bookmark in Git
434 let target_id = work_dir
435 .run_jj(["log", "--no-graph", "-T=commit_id", "-r=description(foo)"])
436 .success()
437 .stdout
438 .into_raw();
439 git_repo
440 .reference(
441 "refs/heads/master",
442 gix::ObjectId::from_hex(target_id.as_bytes()).unwrap(),
443 gix::refs::transaction::PreviousValue::Any,
444 "test",
445 )
446 .unwrap();
447 insta::assert_snapshot!(get_log_output(&work_dir), @r"
448 @ 096dc80da67094fbaa6683e2a205dddffa31f9a8
449 │ ○ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda master foo
450 ├─╯
451 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 git_head()
452 ◆ 0000000000000000000000000000000000000000
453 [EOF]
454 ------- stderr -------
455 Abandoned 1 commits that are no longer reachable.
456 Working copy (@) now at: yqosqzyt 096dc80d (empty) (no description set)
457 Parent commit (@-) : qpvuntsm 230dd059 (empty) (no description set)
458 Done importing changes from the underlying Git repo.
459 [EOF]
460 ");
461}
462
463#[test]
464fn test_git_colocated_bookmark_forget() {
465 let test_env = TestEnvironment::default();
466 let work_dir = test_env.work_dir("repo");
467 git::init(work_dir.root());
468 work_dir
469 .run_jj(["git", "init", "--git-repo", "."])
470 .success();
471 work_dir.run_jj(["new"]).success();
472 work_dir
473 .run_jj(["bookmark", "create", "-r@", "foo"])
474 .success();
475 insta::assert_snapshot!(get_log_output(&work_dir), @r"
476 @ 65b6b74e08973b88d38404430f119c8c79465250 foo
477 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 git_head()
478 ◆ 0000000000000000000000000000000000000000
479 [EOF]
480 ");
481 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
482 foo: rlvkpnrz 65b6b74e (empty) (no description set)
483 @git: rlvkpnrz 65b6b74e (empty) (no description set)
484 [EOF]
485 ");
486
487 let output = work_dir.run_jj(["bookmark", "forget", "--include-remotes", "foo"]);
488 insta::assert_snapshot!(output, @r"
489 ------- stderr -------
490 Forgot 1 local bookmarks.
491 Forgot 1 remote bookmarks.
492 [EOF]
493 ");
494 // A forgotten bookmark is deleted in the git repo. For a detailed demo
495 // explaining this, see `test_bookmark_forget_export` in
496 // `test_bookmark_command.rs`.
497 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
498}
499
500#[test]
501fn test_git_colocated_bookmark_at_root() {
502 let test_env = TestEnvironment::default();
503 test_env
504 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
505 .success();
506 let work_dir = test_env.work_dir("repo");
507
508 let output = work_dir.run_jj(["bookmark", "create", "foo", "-r=root()"]);
509 insta::assert_snapshot!(output, @r"
510 ------- stderr -------
511 Created 1 bookmarks pointing to zzzzzzzz 00000000 foo | (empty) (no description set)
512 Warning: Failed to export some bookmarks:
513 foo@git: Ref cannot point to the root commit in Git
514 [EOF]
515 ");
516
517 let output = work_dir.run_jj(["bookmark", "move", "foo", "--to=@"]);
518 insta::assert_snapshot!(output, @r"
519 ------- stderr -------
520 Moved 1 bookmarks to qpvuntsm 230dd059 foo | (empty) (no description set)
521 [EOF]
522 ");
523
524 let output = work_dir.run_jj([
525 "bookmark",
526 "move",
527 "foo",
528 "--allow-backwards",
529 "--to=root()",
530 ]);
531 insta::assert_snapshot!(output, @r"
532 ------- stderr -------
533 Moved 1 bookmarks to zzzzzzzz 00000000 foo* | (empty) (no description set)
534 Warning: Failed to export some bookmarks:
535 foo@git: Ref cannot point to the root commit in Git
536 [EOF]
537 ");
538}
539
540#[test]
541fn test_git_colocated_conflicting_git_refs() {
542 let test_env = TestEnvironment::default();
543 let work_dir = test_env.work_dir("repo");
544 git::init(work_dir.root());
545 work_dir
546 .run_jj(["git", "init", "--git-repo", "."])
547 .success();
548 work_dir
549 .run_jj(["bookmark", "create", "-r@", "main"])
550 .success();
551 let output = work_dir.run_jj(["bookmark", "create", "-r@", "main/sub"]);
552 insta::with_settings!({filters => vec![("Failed to set: .*", "Failed to set: ...")]}, {
553 insta::assert_snapshot!(output, @r#"
554 ------- stderr -------
555 Created 1 bookmarks pointing to qpvuntsm 230dd059 main main/sub | (empty) (no description set)
556 Warning: Failed to export some bookmarks:
557 main/sub@git: Failed to set: ...
558 Hint: Git doesn't allow a branch name that looks like a parent directory of
559 another (e.g. `foo` and `foo/bar`). Try to rename the bookmarks that failed to
560 export or their "parent" bookmarks.
561 [EOF]
562 "#);
563 });
564}
565
566#[test]
567fn test_git_colocated_checkout_non_empty_working_copy() {
568 let test_env = TestEnvironment::default();
569 let work_dir = test_env.work_dir("repo");
570 let git_repo = git::init(work_dir.root());
571 work_dir
572 .run_jj(["git", "init", "--git-repo", "."])
573 .success();
574
575 // Create an initial commit in Git
576 // We use this to set HEAD to master
577 let tree_id = git::add_commit(
578 &git_repo,
579 "refs/heads/master",
580 "file",
581 b"contents",
582 "initial",
583 &[],
584 )
585 .tree_id;
586 git::checkout_tree_index(&git_repo, tree_id);
587 assert_eq!(work_dir.read_file("file"), b"contents");
588 insta::assert_snapshot!(
589 git_repo.head_id().unwrap().to_string(),
590 @"97358f54806c7cd005ed5ade68a779595efbae7e"
591 );
592
593 work_dir.write_file("two", "y");
594
595 work_dir.run_jj(["describe", "-m", "two"]).success();
596 work_dir.run_jj(["new", "@-"]).success();
597 let output = work_dir.run_jj(["describe", "-m", "new"]);
598 insta::assert_snapshot!(output, @r"
599 ------- stderr -------
600 Working copy (@) now at: kkmpptxz acea3383 (empty) new
601 Parent commit (@-) : slsumksp 97358f54 master | initial
602 [EOF]
603 ");
604
605 assert_eq!(
606 git_repo.head_name().unwrap().unwrap().as_bstr(),
607 b"refs/heads/master"
608 );
609
610 insta::assert_snapshot!(get_log_output(&work_dir), @r"
611 @ acea3383e7a9e4e0df035ee0e83d04cac44a3a14 new
612 │ ○ a99003c2d01be21a82b33c0946c30c596e900287 two
613 ├─╯
614 ○ 97358f54806c7cd005ed5ade68a779595efbae7e master git_head() initial
615 ◆ 0000000000000000000000000000000000000000
616 [EOF]
617 ");
618}
619
620#[test]
621fn test_git_colocated_fetch_deleted_or_moved_bookmark() {
622 let test_env = TestEnvironment::default();
623 test_env.add_config("git.auto-local-bookmark = true");
624 let origin_dir = test_env.work_dir("origin");
625 git::init(origin_dir.root());
626 origin_dir.run_jj(["git", "init", "--git-repo=."]).success();
627 origin_dir.run_jj(["describe", "-m=A"]).success();
628 origin_dir
629 .run_jj(["bookmark", "create", "-r@", "A"])
630 .success();
631 origin_dir.run_jj(["new", "-m=B_to_delete"]).success();
632 origin_dir
633 .run_jj(["bookmark", "create", "-r@", "B_to_delete"])
634 .success();
635 origin_dir.run_jj(["new", "-m=original C", "@-"]).success();
636 origin_dir
637 .run_jj(["bookmark", "create", "-r@", "C_to_move"])
638 .success();
639
640 let clone_dir = test_env.work_dir("clone");
641 git::clone(clone_dir.root(), origin_dir.root().to_str().unwrap(), None);
642 clone_dir.run_jj(["git", "init", "--git-repo=."]).success();
643 clone_dir.run_jj(["new", "A"]).success();
644 insta::assert_snapshot!(get_log_output(&clone_dir), @r"
645 @ 9c2de797c3c299a40173c5af724329012b77cbdd
646 │ ○ 4a191a9013d3f3398ccf5e172792a61439dbcf3a C_to_move original C
647 ├─╯
648 │ ○ c49ec4fb50844d0e693f1609da970b11878772ee B_to_delete B_to_delete
649 ├─╯
650 ◆ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 A git_head() A
651 ◆ 0000000000000000000000000000000000000000
652 [EOF]
653 ");
654
655 origin_dir
656 .run_jj(["bookmark", "delete", "B_to_delete"])
657 .success();
658 // Move bookmark C sideways
659 origin_dir
660 .run_jj(["describe", "C_to_move", "-m", "moved C"])
661 .success();
662 let output = clone_dir.run_jj(["git", "fetch"]);
663 insta::assert_snapshot!(output, @r"
664 ------- stderr -------
665 bookmark: B_to_delete@origin [deleted] untracked
666 bookmark: C_to_move@origin [updated] tracked
667 Abandoned 2 commits that are no longer reachable.
668 [EOF]
669 ");
670 // "original C" and "B_to_delete" are abandoned, as the corresponding bookmarks
671 // were deleted or moved on the remote (#864)
672 insta::assert_snapshot!(get_log_output(&clone_dir), @r"
673 @ 9c2de797c3c299a40173c5af724329012b77cbdd
674 │ ○ 4f3d13296f978cbc351c46a43b4619c91b888475 C_to_move moved C
675 ├─╯
676 ◆ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 A git_head() A
677 ◆ 0000000000000000000000000000000000000000
678 [EOF]
679 ");
680}
681
682#[test]
683fn test_git_colocated_rebase_dirty_working_copy() {
684 let test_env = TestEnvironment::default();
685 let work_dir = test_env.work_dir("repo");
686 let git_repo = git::init(work_dir.root());
687 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
688
689 work_dir.write_file("file", "base");
690 work_dir.run_jj(["new"]).success();
691 work_dir.write_file("file", "old");
692 work_dir
693 .run_jj(["bookmark", "create", "-r@", "feature"])
694 .success();
695
696 // Make the working-copy dirty, delete the checked out bookmark.
697 work_dir.write_file("file", "new");
698 git_repo
699 .find_reference("refs/heads/feature")
700 .unwrap()
701 .delete()
702 .unwrap();
703
704 // Because the working copy is dirty, the new working-copy commit will be
705 // diverged. Therefore, the feature bookmark has change-delete conflict.
706 let output = work_dir.run_jj(["status"]);
707 insta::assert_snapshot!(output, @r"
708 Working copy changes:
709 M file
710 Working copy (@) : rlvkpnrz 6bad94b1 feature?? | (no description set)
711 Parent commit (@-): qpvuntsm 3230d522 (no description set)
712 Warning: These bookmarks have conflicts:
713 feature
714 Hint: Use `jj bookmark list` to see details. Use `jj bookmark set <name> -r <rev>` to resolve.
715 [EOF]
716 ------- stderr -------
717 Warning: Failed to export some bookmarks:
718 feature@git: Modified ref had been deleted in Git
719 Done importing changes from the underlying Git repo.
720 [EOF]
721 ");
722 insta::assert_snapshot!(get_log_output(&work_dir), @r"
723 @ 6bad94b10401f5fafc8a91064661224650d10d1b feature??
724 ○ 3230d52258f6de7e9afbd10da8d64503cc7cdca5 git_head()
725 ◆ 0000000000000000000000000000000000000000
726 [EOF]
727 ");
728
729 // The working-copy content shouldn't be lost.
730 insta::assert_snapshot!(work_dir.read_file("file"), @"new");
731}
732
733#[test]
734fn test_git_colocated_external_checkout() {
735 let test_env = TestEnvironment::default();
736 let work_dir = test_env.work_dir("repo");
737 let git_repo = git::init(work_dir.root());
738 let git_check_out_ref = |name| {
739 let target = git_repo
740 .find_reference(name)
741 .unwrap()
742 .into_fully_peeled_id()
743 .unwrap()
744 .detach();
745 git::set_head_to_id(&git_repo, target);
746 };
747
748 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
749 work_dir.run_jj(["ci", "-m=A"]).success();
750 work_dir
751 .run_jj(["bookmark", "create", "-r@-", "master"])
752 .success();
753 work_dir.run_jj(["new", "-m=B", "root()"]).success();
754 work_dir.run_jj(["new"]).success();
755
756 // Checked out anonymous bookmark
757 insta::assert_snapshot!(get_log_output(&work_dir), @r"
758 @ f8a23336e41840ed1757ef323402a770427dc89a
759 ○ eccedddfa5152d99fc8ddd1081b375387a8a382a git_head() B
760 │ ○ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 master A
761 ├─╯
762 ◆ 0000000000000000000000000000000000000000
763 [EOF]
764 ");
765
766 // Check out another bookmark by external command
767 git_check_out_ref("refs/heads/master");
768
769 // The old working-copy commit gets abandoned, but the whole bookmark should not
770 // be abandoned. (#1042)
771 insta::assert_snapshot!(get_log_output(&work_dir), @r"
772 @ 8bb9e8d42a37c2a4e8dcfad97fce0b8f49bc7afa
773 ○ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 master git_head() A
774 │ ○ eccedddfa5152d99fc8ddd1081b375387a8a382a B
775 ├─╯
776 ◆ 0000000000000000000000000000000000000000
777 [EOF]
778 ------- stderr -------
779 Reset the working copy parent to the new Git HEAD.
780 [EOF]
781 ");
782
783 // Edit non-head commit
784 work_dir.run_jj(["new", "description(B)"]).success();
785 work_dir.run_jj(["new", "-m=C", "--no-edit"]).success();
786 insta::assert_snapshot!(get_log_output(&work_dir), @r"
787 ○ 99a813753d6db988d8fc436b0d6b30a54d6b2707 C
788 @ 81e086b7f9b1dd7fde252e28bdcf4ba4abd86ce5
789 ○ eccedddfa5152d99fc8ddd1081b375387a8a382a git_head() B
790 │ ○ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 master A
791 ├─╯
792 ◆ 0000000000000000000000000000000000000000
793 [EOF]
794 ");
795
796 // Check out another bookmark by external command
797 git_check_out_ref("refs/heads/master");
798
799 // The old working-copy commit shouldn't be abandoned. (#3747)
800 insta::assert_snapshot!(get_log_output(&work_dir), @r"
801 @ ca2a4e32f08688c6fb795c4c034a0a7e09c0d804
802 ○ a7e4cec4256b7995129b9d1e1bda7e1df6e60678 master git_head() A
803 │ ○ 99a813753d6db988d8fc436b0d6b30a54d6b2707 C
804 │ ○ 81e086b7f9b1dd7fde252e28bdcf4ba4abd86ce5
805 │ ○ eccedddfa5152d99fc8ddd1081b375387a8a382a B
806 ├─╯
807 ◆ 0000000000000000000000000000000000000000
808 [EOF]
809 ------- stderr -------
810 Reset the working copy parent to the new Git HEAD.
811 [EOF]
812 ");
813}
814
815#[test]
816#[cfg_attr(windows, ignore = "uses POSIX sh")]
817fn test_git_colocated_concurrent_checkout() {
818 let test_env = TestEnvironment::default();
819 test_env
820 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
821 .success();
822 let work_dir = test_env.work_dir("repo");
823
824 work_dir.run_jj(["new", "-mcommit1"]).success();
825 work_dir.write_file("file1", "");
826 work_dir.run_jj(["new", "-mcommit2"]).success();
827 work_dir.write_file("file2", "");
828 work_dir.run_jj(["new", "-mcommit3"]).success();
829
830 // Run "jj commit" and "git checkout" concurrently
831 let output = work_dir.run_jj([
832 "commit",
833 "--config=ui.editor=['sh', '-c', 'git checkout -q HEAD^']",
834 ]);
835 insta::assert_snapshot!(output, @r#"
836 ------- stderr -------
837 Warning: Failed to update Git HEAD ref
838 Caused by: The reference "HEAD" should have content 58a6206c70b53dfc30dc2f8c9e3713034cfc323e, actual content was 363a08cf5e683485227336e24a006e0deac341bc
839 Working copy (@) now at: mzvwutvl 6b3bc9c8 (empty) (no description set)
840 Parent commit (@-) : zsuskuln 7d358222 (empty) commit3
841 [EOF]
842 "#);
843
844 // git_head() isn't updated because the export failed
845 insta::assert_snapshot!(work_dir.run_jj(["log", "--summary", "--ignore-working-copy"]), @r"
846 @ mzvwutvl test.user@example.com 2001-02-03 08:05:11 6b3bc9c8
847 │ (empty) (no description set)
848 ○ zsuskuln test.user@example.com 2001-02-03 08:05:11 7d358222
849 │ (empty) commit3
850 ○ kkmpptxz test.user@example.com 2001-02-03 08:05:10 git_head() 58a6206c
851 │ commit2
852 │ A file2
853 ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 363a08cf
854 │ commit1
855 │ A file1
856 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
857 │ (empty) (no description set)
858 ◆ zzzzzzzz root() 00000000
859 [EOF]
860 ");
861
862 // The current Git HEAD is imported on the next jj invocation
863 insta::assert_snapshot!(work_dir.run_jj(["log", "--summary"]), @r"
864 @ yqosqzyt test.user@example.com 2001-02-03 08:05:13 690bd924
865 │ (empty) (no description set)
866 │ ○ zsuskuln test.user@example.com 2001-02-03 08:05:11 7d358222
867 │ │ (empty) commit3
868 │ ○ kkmpptxz test.user@example.com 2001-02-03 08:05:10 58a6206c
869 ├─╯ commit2
870 │ A file2
871 ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 git_head() 363a08cf
872 │ commit1
873 │ A file1
874 ○ qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
875 │ (empty) (no description set)
876 ◆ zzzzzzzz root() 00000000
877 [EOF]
878 ------- stderr -------
879 Reset the working copy parent to the new Git HEAD.
880 [EOF]
881 ");
882}
883
884#[test]
885fn test_git_colocated_squash_undo() {
886 let test_env = TestEnvironment::default();
887 let work_dir = test_env.work_dir("repo");
888 git::init(work_dir.root());
889 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
890 work_dir.run_jj(["ci", "-m=A"]).success();
891 // Test the setup
892 insta::assert_snapshot!(get_log_output_divergence(&work_dir), @r"
893 @ rlvkpnrzqnoo 9670380ac379
894 ○ qpvuntsmwlqt a7e4cec4256b A git_head()
895 ◆ zzzzzzzzzzzz 000000000000
896 [EOF]
897 ");
898
899 work_dir.run_jj(["squash"]).success();
900 insta::assert_snapshot!(get_log_output_divergence(&work_dir), @r"
901 @ zsuskulnrvyr 6ee662324e5a
902 ○ qpvuntsmwlqt 13ab6b96d82e A git_head()
903 ◆ zzzzzzzzzzzz 000000000000
904 [EOF]
905 ");
906 work_dir.run_jj(["undo"]).success();
907 // TODO: There should be no divergence here; 2f376ea1478c should be hidden
908 // (#922)
909 insta::assert_snapshot!(get_log_output_divergence(&work_dir), @r"
910 @ rlvkpnrzqnoo 9670380ac379
911 ○ qpvuntsmwlqt a7e4cec4256b A git_head()
912 ◆ zzzzzzzzzzzz 000000000000
913 [EOF]
914 ");
915}
916
917#[test]
918fn test_git_colocated_undo_head_move() {
919 let test_env = TestEnvironment::default();
920 let work_dir = test_env.work_dir("repo");
921 let git_repo = git::init(work_dir.root());
922 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
923
924 // Create new HEAD
925 work_dir.run_jj(["new"]).success();
926 assert!(git_repo.head().unwrap().is_detached());
927 insta::assert_snapshot!(
928 git_repo.head_id().unwrap().to_string(),
929 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22");
930 insta::assert_snapshot!(get_log_output(&work_dir), @r"
931 @ 65b6b74e08973b88d38404430f119c8c79465250
932 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 git_head()
933 ◆ 0000000000000000000000000000000000000000
934 [EOF]
935 ");
936
937 // HEAD should be unset
938 work_dir.run_jj(["undo"]).success();
939 assert!(git_repo.head().unwrap().is_unborn());
940 insta::assert_snapshot!(get_log_output(&work_dir), @r"
941 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
942 ◆ 0000000000000000000000000000000000000000
943 [EOF]
944 ");
945
946 // Create commit on non-root commit
947 work_dir.run_jj(["new"]).success();
948 work_dir.run_jj(["new"]).success();
949 insta::assert_snapshot!(get_log_output(&work_dir), @r"
950 @ 69b19f73cf584f162f078fb0d91c55ca39d10bc7
951 ○ eb08b363bb5ef8ee549314260488980d7bbe8f63 git_head()
952 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
953 ◆ 0000000000000000000000000000000000000000
954 [EOF]
955 ");
956 assert!(git_repo.head().unwrap().is_detached());
957 insta::assert_snapshot!(
958 git_repo.head_id().unwrap().to_string(),
959 @"eb08b363bb5ef8ee549314260488980d7bbe8f63");
960
961 // HEAD should be moved back
962 let output = work_dir.run_jj(["undo"]);
963 insta::assert_snapshot!(output, @r"
964 ------- stderr -------
965 Undid operation: b50ec983d1c1 (2001-02-03 08:05:13) new empty commit
966 Working copy (@) now at: royxmykx eb08b363 (empty) (no description set)
967 Parent commit (@-) : qpvuntsm 230dd059 (empty) (no description set)
968 [EOF]
969 ");
970 assert!(git_repo.head().unwrap().is_detached());
971 insta::assert_snapshot!(
972 git_repo.head_id().unwrap().to_string(),
973 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22");
974 insta::assert_snapshot!(get_log_output(&work_dir), @r"
975 @ eb08b363bb5ef8ee549314260488980d7bbe8f63
976 ○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 git_head()
977 ◆ 0000000000000000000000000000000000000000
978 [EOF]
979 ");
980}
981
982#[test]
983fn test_git_colocated_update_index_preserves_timestamps() {
984 let test_env = TestEnvironment::default();
985 test_env
986 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
987 .success();
988 let work_dir = test_env.work_dir("repo");
989
990 // Create a commit with some files
991 work_dir.write_file("file1.txt", "will be unchanged\n");
992 work_dir.write_file("file2.txt", "will be modified\n");
993 work_dir.write_file("file3.txt", "will be deleted\n");
994 work_dir
995 .run_jj(["bookmark", "create", "-r@", "commit1"])
996 .success();
997 work_dir.run_jj(["new"]).success();
998
999 // Create a commit with some changes to the files
1000 work_dir.write_file("file2.txt", "modified\n");
1001 work_dir.remove_file("file3.txt");
1002 work_dir.write_file("file4.txt", "added\n");
1003 work_dir
1004 .run_jj(["bookmark", "create", "-r@", "commit2"])
1005 .success();
1006 work_dir.run_jj(["new"]).success();
1007
1008 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1009 @ 051508d190ffd04fe2d79367ad8e9c3713ac2375
1010 ○ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2 git_head()
1011 ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1
1012 ◆ 0000000000000000000000000000000000000000
1013 [EOF]
1014 ");
1015
1016 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1017 Unconflicted Mode(FILE) ed48318d9bf4 ctime=0:0 mtime=0:0 size=0 flags=0 file1.txt
1018 Unconflicted Mode(FILE) 2e0996000b7e ctime=0:0 mtime=0:0 size=0 flags=0 file2.txt
1019 Unconflicted Mode(FILE) d5f7fc3f74f7 ctime=0:0 mtime=0:0 size=0 flags=0 file4.txt
1020 ");
1021
1022 // Update index with stats for all files. We may want to do this automatically
1023 // in the future after we update the index in `git::reset_head` (#3786), but for
1024 // now, we at least want to preserve existing stat information when possible.
1025 update_git_index(work_dir.root());
1026
1027 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1028 Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 flags=0 file1.txt
1029 Unconflicted Mode(FILE) 2e0996000b7e ctime=[nonzero] mtime=[nonzero] size=9 flags=0 file2.txt
1030 Unconflicted Mode(FILE) d5f7fc3f74f7 ctime=[nonzero] mtime=[nonzero] size=6 flags=0 file4.txt
1031 ");
1032
1033 // Edit parent commit, causing the changes to be removed from the index without
1034 // touching the working copy
1035 work_dir.run_jj(["edit", "commit2"]).success();
1036
1037 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1038 @ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2
1039 ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1 git_head()
1040 ◆ 0000000000000000000000000000000000000000
1041 [EOF]
1042 ");
1043
1044 // Index should contain stat for unchanged file still.
1045 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1046 Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 flags=0 file1.txt
1047 Unconflicted Mode(FILE) 28d2718c947b ctime=0:0 mtime=0:0 size=0 flags=0 file2.txt
1048 Unconflicted Mode(FILE) 528557ab3a42 ctime=0:0 mtime=0:0 size=0 flags=0 file3.txt
1049 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 file4.txt
1050 ");
1051
1052 // Create sibling commit, causing working copy to match index
1053 work_dir.run_jj(["new", "commit1"]).success();
1054
1055 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1056 @ ccb1b1807383dba5ff4d335fd9fb92aa540f4632
1057 │ ○ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2
1058 ├─╯
1059 ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1 git_head()
1060 ◆ 0000000000000000000000000000000000000000
1061 [EOF]
1062 ");
1063
1064 // Index should contain stat for unchanged file still.
1065 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1066 Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 flags=0 file1.txt
1067 Unconflicted Mode(FILE) 28d2718c947b ctime=0:0 mtime=0:0 size=0 flags=0 file2.txt
1068 Unconflicted Mode(FILE) 528557ab3a42 ctime=0:0 mtime=0:0 size=0 flags=0 file3.txt
1069 ");
1070}
1071
1072#[test]
1073fn test_git_colocated_update_index_merge_conflict() {
1074 let test_env = TestEnvironment::default();
1075 test_env
1076 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
1077 .success();
1078 let work_dir = test_env.work_dir("repo");
1079
1080 // Set up conflict files
1081 work_dir.write_file("conflict.txt", "base\n");
1082 work_dir.write_file("base.txt", "base\n");
1083 work_dir
1084 .run_jj(["bookmark", "create", "-r@", "base"])
1085 .success();
1086
1087 work_dir.run_jj(["new", "base"]).success();
1088 work_dir.write_file("conflict.txt", "left\n");
1089 work_dir.write_file("left.txt", "left\n");
1090 work_dir
1091 .run_jj(["bookmark", "create", "-r@", "left"])
1092 .success();
1093
1094 work_dir.run_jj(["new", "base"]).success();
1095 work_dir.write_file("conflict.txt", "right\n");
1096 work_dir.write_file("right.txt", "right\n");
1097 work_dir
1098 .run_jj(["bookmark", "create", "-r@", "right"])
1099 .success();
1100
1101 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1102 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 base.txt
1103 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1104 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 right.txt
1105 ");
1106
1107 // Update index with stat for base.txt
1108 update_git_index(work_dir.root());
1109
1110 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1111 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1112 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1113 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 right.txt
1114 ");
1115
1116 // Create merge conflict
1117 work_dir.run_jj(["new", "left", "right"]).success();
1118
1119 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1120 @ aea7acd77752c3f74914de1fe327075a579bf7c6
1121 ├─╮
1122 │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right
1123 ○ │ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left git_head()
1124 ├─╯
1125 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1126 ◆ 0000000000000000000000000000000000000000
1127 [EOF]
1128 ");
1129
1130 // Conflict should be added in index with correct blob IDs. The stat for
1131 // base.txt should not change.
1132 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1133 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1134 Base Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=1000 conflict.txt
1135 Ours Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=2000 conflict.txt
1136 Theirs Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=3000 conflict.txt
1137 Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=0 left.txt
1138 Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=0 right.txt
1139 ");
1140
1141 work_dir.run_jj(["new"]).success();
1142
1143 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1144 @ cae33b49a8a514996983caaf171c5edbf0d70e78
1145 × aea7acd77752c3f74914de1fe327075a579bf7c6 git_head()
1146 ├─╮
1147 │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right
1148 ○ │ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left
1149 ├─╯
1150 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1151 ◆ 0000000000000000000000000000000000000000
1152 [EOF]
1153 ");
1154
1155 // Index should be the same after `jj new`.
1156 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1157 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1158 Base Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=1000 conflict.txt
1159 Ours Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=2000 conflict.txt
1160 Theirs Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=3000 conflict.txt
1161 Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=0 left.txt
1162 Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=0 right.txt
1163 ");
1164}
1165
1166#[test]
1167fn test_git_colocated_update_index_rebase_conflict() {
1168 let test_env = TestEnvironment::default();
1169 test_env
1170 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
1171 .success();
1172 let work_dir = test_env.work_dir("repo");
1173
1174 // Set up conflict files
1175 work_dir.write_file("conflict.txt", "base\n");
1176 work_dir.write_file("base.txt", "base\n");
1177 work_dir
1178 .run_jj(["bookmark", "create", "-r@", "base"])
1179 .success();
1180
1181 work_dir.run_jj(["new", "base"]).success();
1182 work_dir.write_file("conflict.txt", "left\n");
1183 work_dir.write_file("left.txt", "left\n");
1184 work_dir
1185 .run_jj(["bookmark", "create", "-r@", "left"])
1186 .success();
1187
1188 work_dir.run_jj(["new", "base"]).success();
1189 work_dir.write_file("conflict.txt", "right\n");
1190 work_dir.write_file("right.txt", "right\n");
1191 work_dir
1192 .run_jj(["bookmark", "create", "-r@", "right"])
1193 .success();
1194
1195 work_dir.run_jj(["edit", "left"]).success();
1196
1197 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1198 @ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left
1199 │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right
1200 ├─╯
1201 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base git_head()
1202 ◆ 0000000000000000000000000000000000000000
1203 [EOF]
1204 ");
1205
1206 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1207 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 base.txt
1208 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1209 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 left.txt
1210 ");
1211
1212 // Update index with stat for base.txt
1213 update_git_index(work_dir.root());
1214
1215 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1216 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1217 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1218 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 left.txt
1219 ");
1220
1221 // Create rebase conflict
1222 work_dir
1223 .run_jj(["rebase", "-r", "left", "-d", "right"])
1224 .success();
1225
1226 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1227 @ 233cb41e128e74aa2fcbf01c85d69b33a118faa8 left
1228 ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right git_head()
1229 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1230 ◆ 0000000000000000000000000000000000000000
1231 [EOF]
1232 ");
1233
1234 // Index should contain files from parent commit, so there should be no conflict
1235 // in conflict.txt yet. The stat for base.txt should not change.
1236 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1237 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1238 Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1239 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 left.txt
1240 Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=0 right.txt
1241 ");
1242
1243 work_dir.run_jj(["new"]).success();
1244
1245 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1246 @ 6d84b9021f9e07b69770687071c4e8e71113e688
1247 × 233cb41e128e74aa2fcbf01c85d69b33a118faa8 left git_head()
1248 ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right
1249 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1250 ◆ 0000000000000000000000000000000000000000
1251 [EOF]
1252 ");
1253
1254 // Now the working copy commit's parent is conflicted, so the index should have
1255 // a conflict with correct blob IDs.
1256 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1257 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1258 Base Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=1000 conflict.txt
1259 Ours Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=2000 conflict.txt
1260 Theirs Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=3000 conflict.txt
1261 Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 flags=0 left.txt
1262 Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 flags=0 right.txt
1263 ");
1264}
1265
1266#[test]
1267fn test_git_colocated_update_index_3_sided_conflict() {
1268 let test_env = TestEnvironment::default();
1269 test_env
1270 .run_jj_in(".", ["git", "init", "--colocate", "repo"])
1271 .success();
1272 let work_dir = test_env.work_dir("repo");
1273
1274 // Set up conflict files
1275 work_dir.write_file("conflict.txt", "base\n");
1276 work_dir.write_file("base.txt", "base\n");
1277 work_dir
1278 .run_jj(["bookmark", "create", "-r@", "base"])
1279 .success();
1280
1281 work_dir.run_jj(["new", "base"]).success();
1282 work_dir.write_file("conflict.txt", "side-1\n");
1283 work_dir.write_file("side-1.txt", "side-1\n");
1284 work_dir
1285 .run_jj(["bookmark", "create", "-r@", "side-1"])
1286 .success();
1287
1288 work_dir.run_jj(["new", "base"]).success();
1289 work_dir.write_file("conflict.txt", "side-2\n");
1290 work_dir.write_file("side-2.txt", "side-2\n");
1291 work_dir
1292 .run_jj(["bookmark", "create", "-r@", "side-2"])
1293 .success();
1294
1295 work_dir.run_jj(["new", "base"]).success();
1296 work_dir.write_file("conflict.txt", "side-3\n");
1297 work_dir.write_file("side-3.txt", "side-3\n");
1298 work_dir
1299 .run_jj(["bookmark", "create", "-r@", "side-3"])
1300 .success();
1301
1302 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1303 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 base.txt
1304 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1305 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 side-3.txt
1306 ");
1307
1308 // Update index with stat for base.txt
1309 update_git_index(work_dir.root());
1310
1311 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1312 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1313 Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1314 Unconflicted Mode(FILE) e69de29bb2d1 ctime=0:0 mtime=0:0 size=0 flags=20004000 side-3.txt
1315 ");
1316
1317 // Create 3-sided merge conflict
1318 work_dir
1319 .run_jj(["new", "side-1", "side-2", "side-3"])
1320 .success();
1321
1322 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1323 @ faee07ad76218d193f2784f4988daa2ac46db30c
1324 ├─┬─╮
1325 │ │ ○ 86e722ea6a9da2551f1e05bc9aa914acd1cb2304 side-3
1326 │ ○ │ b8b9ca2d8178c4ba727a61e2258603f30ac7c6d3 side-2
1327 │ ├─╯
1328 ○ │ a4b3ce25ef4857172e7777567afd497a917a0486 side-1 git_head()
1329 ├─╯
1330 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1331 ◆ 0000000000000000000000000000000000000000
1332 [EOF]
1333 ");
1334
1335 // We can't add conflicts with more than 2 sides to the index, so we add a dummy
1336 // conflict instead. The stat for base.txt should not change.
1337 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1338 Ours Mode(FILE) eb8299123d2a ctime=0:0 mtime=0:0 size=0 flags=2000 .jj-do-not-resolve-this-conflict
1339 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1340 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1341 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 side-1.txt
1342 Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 flags=0 side-2.txt
1343 Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 flags=0 side-3.txt
1344 ");
1345
1346 work_dir.run_jj(["new"]).success();
1347
1348 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1349 @ b0e5644063c2a12fb265e5f65cd88c6a2e1cf865
1350 × faee07ad76218d193f2784f4988daa2ac46db30c git_head()
1351 ├─┬─╮
1352 │ │ ○ 86e722ea6a9da2551f1e05bc9aa914acd1cb2304 side-3
1353 │ ○ │ b8b9ca2d8178c4ba727a61e2258603f30ac7c6d3 side-2
1354 │ ├─╯
1355 ○ │ a4b3ce25ef4857172e7777567afd497a917a0486 side-1
1356 ├─╯
1357 ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base
1358 ◆ 0000000000000000000000000000000000000000
1359 [EOF]
1360 ");
1361
1362 // Index should be the same after `jj new`.
1363 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1364 Ours Mode(FILE) eb8299123d2a ctime=0:0 mtime=0:0 size=0 flags=2000 .jj-do-not-resolve-this-conflict
1365 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1366 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1367 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 side-1.txt
1368 Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 flags=0 side-2.txt
1369 Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 flags=0 side-3.txt
1370 ");
1371
1372 // If we add a file named ".jj-do-not-resolve-this-conflict", it should take
1373 // precedence over the dummy conflict.
1374 work_dir.write_file(".jj-do-not-resolve-this-conflict", "file\n");
1375 work_dir.run_jj(["new"]).success();
1376 insta::assert_snapshot!(get_index_state(work_dir.root()), @r"
1377 Unconflicted Mode(FILE) f73f3093ff86 ctime=0:0 mtime=0:0 size=0 flags=0 .jj-do-not-resolve-this-conflict
1378 Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 flags=0 base.txt
1379 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 conflict.txt
1380 Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 flags=0 side-1.txt
1381 Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 flags=0 side-2.txt
1382 Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 flags=0 side-3.txt
1383 ");
1384}
1385
1386#[must_use]
1387fn get_log_output_divergence(work_dir: &TestWorkDir) -> CommandOutput {
1388 let template = r#"
1389 separate(" ",
1390 change_id.short(),
1391 commit_id.short(),
1392 description.first_line(),
1393 bookmarks,
1394 if(git_head, "git_head()"),
1395 if(divergent, "!divergence!"),
1396 )
1397 "#;
1398 work_dir.run_jj(["log", "-T", template])
1399}
1400
1401#[must_use]
1402fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
1403 let template = r#"
1404 separate(" ",
1405 commit_id,
1406 bookmarks,
1407 if(git_head, "git_head()"),
1408 description,
1409 )
1410 "#;
1411 work_dir.run_jj(["log", "-T", template, "-r=all()"])
1412}
1413
1414fn update_git_index(repo_path: &Path) {
1415 let mut iter = git::open(repo_path)
1416 .status(gix::progress::Discard)
1417 .unwrap()
1418 .into_index_worktree_iter(None)
1419 .unwrap();
1420
1421 // need to explicitly iterate over the changes to recreate the index
1422
1423 for item in iter.by_ref() {
1424 item.unwrap();
1425 }
1426
1427 iter.outcome_mut()
1428 .unwrap()
1429 .write_changes()
1430 .unwrap()
1431 .unwrap();
1432}
1433
1434fn get_index_state(repo_path: &Path) -> String {
1435 let git_repo = gix::open(repo_path).expect("git repo should exist");
1436 let mut buffer = String::new();
1437 // We can't use the real time from disk, since it would change each time the
1438 // tests are run. Instead, we just show whether it's zero or nonzero.
1439 let format_time = |time: gix::index::entry::stat::Time| {
1440 if time.secs == 0 && time.nsecs == 0 {
1441 "0:0"
1442 } else {
1443 "[nonzero]"
1444 }
1445 };
1446 let index = git_repo.index_or_empty().unwrap();
1447 for entry in index.entries() {
1448 writeln!(
1449 &mut buffer,
1450 "{:12} {:?} {} ctime={} mtime={} size={} flags={:x} {}",
1451 format!("{:?}", entry.stage()),
1452 entry.mode,
1453 entry.id.to_hex_with_len(12),
1454 format_time(entry.stat.ctime),
1455 format_time(entry.stat.mtime),
1456 entry.stat.size,
1457 entry.flags.bits(),
1458 entry.path_in(index.path_backing()),
1459 )
1460 .unwrap();
1461 }
1462 buffer
1463}
1464
1465#[test]
1466fn test_git_colocated_unreachable_commits() {
1467 let test_env = TestEnvironment::default();
1468 let work_dir = test_env.work_dir("repo");
1469 let git_repo = git::init(work_dir.root());
1470
1471 // Create an initial commit in Git
1472 let commit1 = git::add_commit(
1473 &git_repo,
1474 "refs/heads/master",
1475 "some-file",
1476 b"some content",
1477 "initial",
1478 &[],
1479 )
1480 .commit_id;
1481 insta::assert_snapshot!(
1482 git_repo.head_id().unwrap().to_string(),
1483 @"cd740e230992f334de13a0bd0b35709b3f7a89af"
1484 );
1485
1486 // Add a second commit in Git
1487 let commit2 = git::add_commit(
1488 &git_repo,
1489 "refs/heads/dummy",
1490 "next-file",
1491 b"more content",
1492 "next",
1493 &[commit1],
1494 )
1495 .commit_id;
1496 git_repo
1497 .find_reference("refs/heads/dummy")
1498 .unwrap()
1499 .delete()
1500 .unwrap();
1501 insta::assert_snapshot!(
1502 git_repo.head_id().unwrap().to_string(),
1503 @"cd740e230992f334de13a0bd0b35709b3f7a89af"
1504 );
1505
1506 // Import the repo while there is no path to the second commit
1507 work_dir
1508 .run_jj(["git", "init", "--git-repo", "."])
1509 .success();
1510 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1511 @ 9ff88424a06a94d04738847733e68e510b906345
1512 ○ cd740e230992f334de13a0bd0b35709b3f7a89af master git_head() initial
1513 ◆ 0000000000000000000000000000000000000000
1514 [EOF]
1515 ");
1516 insta::assert_snapshot!(
1517 git_repo.head_id().unwrap().to_string(),
1518 @"cd740e230992f334de13a0bd0b35709b3f7a89af"
1519 );
1520
1521 // Check that trying to look up the second commit fails gracefully
1522 let output = work_dir.run_jj(["show", &commit2.to_string()]);
1523 insta::assert_snapshot!(output, @r"
1524 ------- stderr -------
1525 Error: Revision `b23bb53bdce25f0e03ff9e484eadb77626256041` doesn't exist
1526 [EOF]
1527 [exit status: 1]
1528 ");
1529}
1530
1531#[test]
1532fn test_git_colocated_operation_cleanup() {
1533 let test_env = TestEnvironment::default();
1534 let output = test_env.run_jj_in(".", ["git", "init", "--colocate", "repo"]);
1535 insta::assert_snapshot!(output, @r#"
1536 ------- stderr -------
1537 Initialized repo in "repo"
1538 [EOF]
1539 "#);
1540
1541 let work_dir = test_env.work_dir("repo");
1542
1543 work_dir.write_file("file", "1");
1544 work_dir.run_jj(["describe", "-m1"]).success();
1545 work_dir.run_jj(["new"]).success();
1546
1547 work_dir.write_file("file", "2");
1548 work_dir.run_jj(["describe", "-m2"]).success();
1549 work_dir
1550 .run_jj(["bookmark", "create", "-r@", "main"])
1551 .success();
1552 work_dir.run_jj(["new", "root()+"]).success();
1553
1554 work_dir.write_file("file", "3");
1555 work_dir.run_jj(["describe", "-m3"]).success();
1556 work_dir
1557 .run_jj(["bookmark", "create", "-r@", "feature"])
1558 .success();
1559 work_dir.run_jj(["new"]).success();
1560
1561 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1562 @ e3feb4fda7b5e1d458a460ce76cb840b8f3cae34
1563 ○ e810c2ff6f3287a27e5d08aa3f429e284d99fea0 feature git_head() 3
1564 │ ○ 52fef888179abf5819a0a0d4f7907fcc025cb2a1 main 2
1565 ├─╯
1566 ○ 61c11921948922575504d7b9f2df236543d0cec9 1
1567 ◆ 0000000000000000000000000000000000000000
1568 [EOF]
1569 "#);
1570
1571 // Start a rebase in Git and expect a merge conflict.
1572 let output = std::process::Command::new("git")
1573 .current_dir(work_dir.root())
1574 .args(["rebase", "main"])
1575 .output()
1576 .unwrap();
1577 assert!(!output.status.success());
1578
1579 // Check that we’re in the middle of a conflicted rebase.
1580 assert!(std::fs::exists(work_dir.root().join(".git").join("rebase-merge")).unwrap());
1581 let output = std::process::Command::new("git")
1582 .current_dir(work_dir.root())
1583 .args(["status", "--porcelain=v1"])
1584 .output()
1585 .unwrap();
1586 assert!(output.status.success());
1587 insta::assert_snapshot!(String::from_utf8(output.stdout).unwrap(), @r#"
1588 UU file
1589 "#);
1590 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1591 @ fbb4e341d1e7e1d3b87377c075bd8a407305ba3a
1592 ○ 52fef888179abf5819a0a0d4f7907fcc025cb2a1 main git_head() 2
1593 │ ○ e810c2ff6f3287a27e5d08aa3f429e284d99fea0 feature 3
1594 ├─╯
1595 ○ 61c11921948922575504d7b9f2df236543d0cec9 1
1596 ◆ 0000000000000000000000000000000000000000
1597 [EOF]
1598 ------- stderr -------
1599 Reset the working copy parent to the new Git HEAD.
1600 [EOF]
1601 "#);
1602
1603 // Reset the Git HEAD with Jujutsu.
1604 let output = work_dir.run_jj(["new", "main"]);
1605 insta::assert_snapshot!(output, @r"
1606 ------- stderr -------
1607 Working copy (@) now at: kmkuslsw 92667528 (empty) (no description set)
1608 Parent commit (@-) : kkmpptxz 52fef888 main | 2
1609 Added 0 files, modified 1 files, removed 0 files
1610 [EOF]
1611 ");
1612 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1613 @ 926675286938f585d83b3646a95df96206968e8c
1614 │ ○ fbb4e341d1e7e1d3b87377c075bd8a407305ba3a
1615 ├─╯
1616 ○ 52fef888179abf5819a0a0d4f7907fcc025cb2a1 main git_head() 2
1617 │ ○ e810c2ff6f3287a27e5d08aa3f429e284d99fea0 feature 3
1618 ├─╯
1619 ○ 61c11921948922575504d7b9f2df236543d0cec9 1
1620 ◆ 0000000000000000000000000000000000000000
1621 [EOF]
1622 "#);
1623
1624 // Check that the operation was correctly aborted.
1625 assert!(!std::fs::exists(work_dir.root().join(".git").join("rebase-merge")).unwrap());
1626 let output = std::process::Command::new("git")
1627 .current_dir(work_dir.root())
1628 .args(["status", "--porcelain=v1"])
1629 .output()
1630 .unwrap();
1631 assert!(output.status.success());
1632 insta::assert_snapshot!(String::from_utf8(output.stdout).unwrap(), @"");
1633}
1634
1635#[must_use]
1636fn get_bookmark_output(work_dir: &TestWorkDir) -> CommandOutput {
1637 // --quiet to suppress deleted bookmarks hint
1638 work_dir.run_jj(["bookmark", "list", "--all-remotes", "--quiet"])
1639}