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;
16
17use git2::Oid;
18
19use crate::common::TestEnvironment;
20
21#[test]
22fn test_git_colocated() {
23 let test_env = TestEnvironment::default();
24 let workspace_root = test_env.env_root().join("repo");
25 let git_repo = git2::Repository::init(&workspace_root).unwrap();
26
27 // Create an initial commit in Git
28 std::fs::write(workspace_root.join("file"), "contents").unwrap();
29 git_repo
30 .index()
31 .unwrap()
32 .add_path(Path::new("file"))
33 .unwrap();
34 let tree1_oid = git_repo.index().unwrap().write_tree().unwrap();
35 let tree1 = git_repo.find_tree(tree1_oid).unwrap();
36 let signature = git2::Signature::new(
37 "Someone",
38 "someone@example.com",
39 &git2::Time::new(1234567890, 60),
40 )
41 .unwrap();
42 git_repo
43 .commit(
44 Some("refs/heads/master"),
45 &signature,
46 &signature,
47 "initial",
48 &tree1,
49 &[],
50 )
51 .unwrap();
52 insta::assert_snapshot!(
53 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
54 @"e61b6729ff4292870702f2f72b2a60165679ef37"
55 );
56
57 // Import the repo
58 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
59 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
60 @ 3e9369cd54227eb88455e1834dbc08aad6a16ac4
61 ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial
62 ◉ 0000000000000000000000000000000000000000
63 "###);
64 insta::assert_snapshot!(
65 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
66 @"e61b6729ff4292870702f2f72b2a60165679ef37"
67 );
68
69 // Modify the working copy. The working-copy commit should changed, but the Git
70 // HEAD commit should not
71 std::fs::write(workspace_root.join("file"), "modified").unwrap();
72 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
73 @ b26951a9c6f5c270e4d039880208952fd5faae5e
74 ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial
75 ◉ 0000000000000000000000000000000000000000
76 "###);
77 insta::assert_snapshot!(
78 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
79 @"e61b6729ff4292870702f2f72b2a60165679ef37"
80 );
81
82 // Create a new change from jj and check that it's reflected in Git
83 test_env.jj_cmd_ok(&workspace_root, &["new"]);
84 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
85 @ 9dbb23ff2ff5e66c43880f1042369d704f7a321e
86 ◉ b26951a9c6f5c270e4d039880208952fd5faae5e HEAD@git
87 ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master initial
88 ◉ 0000000000000000000000000000000000000000
89 "###);
90 insta::assert_snapshot!(
91 git_repo.head().unwrap().target().unwrap().to_string(),
92 @"b26951a9c6f5c270e4d039880208952fd5faae5e"
93 );
94}
95
96#[test]
97fn test_git_colocated_unborn_branch() {
98 let test_env = TestEnvironment::default();
99 let workspace_root = test_env.env_root().join("repo");
100 let git_repo = git2::Repository::init(&workspace_root).unwrap();
101
102 let add_file_to_index = |name: &str, data: &str| {
103 std::fs::write(workspace_root.join(name), data).unwrap();
104 let mut index = git_repo.index().unwrap();
105 index.add_path(Path::new(name)).unwrap();
106 index.write().unwrap();
107 };
108 let checkout_index = || {
109 let mut index = git_repo.index().unwrap();
110 index.read(true).unwrap(); // discard in-memory cache
111 git_repo.checkout_index(Some(&mut index), None).unwrap();
112 };
113
114 // Initially, HEAD isn't set.
115 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
116 assert!(git_repo.head().is_err());
117 assert_eq!(
118 git_repo.find_reference("HEAD").unwrap().symbolic_target(),
119 Some("refs/heads/master")
120 );
121 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
122 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
123 ◉ 0000000000000000000000000000000000000000
124 "###);
125
126 // Stage some change, and check out root. This shouldn't clobber the HEAD.
127 add_file_to_index("file0", "");
128 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new", "root()"]);
129 insta::assert_snapshot!(stdout, @"");
130 insta::assert_snapshot!(stderr, @r###"
131 Working copy now at: kkmpptxz fcdbbd73 (empty) (no description set)
132 Parent commit : zzzzzzzz 00000000 (empty) (no description set)
133 Added 0 files, modified 0 files, removed 1 files
134 "###);
135 assert!(git_repo.head().is_err());
136 assert_eq!(
137 git_repo.find_reference("HEAD").unwrap().symbolic_target(),
138 Some("refs/heads/master")
139 );
140 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
141 @ fcdbbd731496cae17161cd6be9b6cf1f759655a8
142 │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09
143 ├─╯
144 ◉ 0000000000000000000000000000000000000000
145 "###);
146 // Staged change shouldn't persist.
147 checkout_index();
148 insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###"
149 The working copy is clean
150 Working copy : kkmpptxz fcdbbd73 (empty) (no description set)
151 Parent commit: zzzzzzzz 00000000 (empty) (no description set)
152 "###);
153
154 // Stage some change, and create new HEAD. This shouldn't move the default
155 // branch.
156 add_file_to_index("file1", "");
157 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new"]);
158 insta::assert_snapshot!(stdout, @"");
159 insta::assert_snapshot!(stderr, @r###"
160 Working copy now at: royxmykx 76c60bf0 (empty) (no description set)
161 Parent commit : kkmpptxz f8d5bc77 (no description set)
162 "###);
163 assert!(git_repo.head().unwrap().symbolic_target().is_none());
164 insta::assert_snapshot!(
165 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
166 @"f8d5bc772d1147351fd6e8cea52a4f935d3b31e7"
167 );
168 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
169 @ 76c60bf0a66dcbe74d74d58c23848d96f9e86e84
170 ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 HEAD@git
171 │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09
172 ├─╯
173 ◉ 0000000000000000000000000000000000000000
174 "###);
175 // Staged change shouldn't persist.
176 checkout_index();
177 insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###"
178 The working copy is clean
179 Working copy : royxmykx 76c60bf0 (empty) (no description set)
180 Parent commit: kkmpptxz f8d5bc77 (no description set)
181 "###);
182
183 // Assign the default branch. The branch is no longer "unborn".
184 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "-r@-", "master"]);
185
186 // Stage some change, and check out root again. This should unset the HEAD.
187 // https://github.com/martinvonz/jj/issues/1495
188 add_file_to_index("file2", "");
189 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new", "root()"]);
190 insta::assert_snapshot!(stdout, @"");
191 insta::assert_snapshot!(stderr, @r###"
192 Working copy now at: znkkpsqq 10dd328b (empty) (no description set)
193 Parent commit : zzzzzzzz 00000000 (empty) (no description set)
194 Added 0 files, modified 0 files, removed 2 files
195 "###);
196 assert!(git_repo.head().is_err());
197 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
198 @ 10dd328bb906e15890e55047740eab2812a3b2f7
199 │ ◉ 2c576a57d2e6e8494616629cfdbb8fe5e3fea73b
200 │ ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 master
201 ├─╯
202 │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09
203 ├─╯
204 ◉ 0000000000000000000000000000000000000000
205 "###);
206 // Staged change shouldn't persist.
207 checkout_index();
208 insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###"
209 The working copy is clean
210 Working copy : znkkpsqq 10dd328b (empty) (no description set)
211 Parent commit: zzzzzzzz 00000000 (empty) (no description set)
212 "###);
213
214 // New snapshot and commit can be created after the HEAD got unset.
215 std::fs::write(workspace_root.join("file3"), "").unwrap();
216 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new"]);
217 insta::assert_snapshot!(stdout, @"");
218 insta::assert_snapshot!(stderr, @r###"
219 Working copy now at: wqnwkozp cab23370 (empty) (no description set)
220 Parent commit : znkkpsqq 8f5b2638 (no description set)
221 "###);
222 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
223 @ cab233704a5c0b21bde070943055f22142fb2043
224 ◉ 8f5b263819457712a2937428b9c58a2a84afbb1c HEAD@git
225 │ ◉ 2c576a57d2e6e8494616629cfdbb8fe5e3fea73b
226 │ ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 master
227 ├─╯
228 │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09
229 ├─╯
230 ◉ 0000000000000000000000000000000000000000
231 "###);
232}
233
234#[test]
235fn test_git_colocated_export_branches_on_snapshot() {
236 // Checks that we export branches that were changed only because the working
237 // copy was snapshotted
238
239 let test_env = TestEnvironment::default();
240 let workspace_root = test_env.env_root().join("repo");
241 let git_repo = git2::Repository::init(&workspace_root).unwrap();
242 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
243
244 // Create branch pointing to the initial commit
245 std::fs::write(workspace_root.join("file"), "initial").unwrap();
246 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "foo"]);
247 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
248 @ 438471f3fbf1004298d8fb01eeb13663a051a643 foo
249 ◉ 0000000000000000000000000000000000000000
250 "###);
251
252 // The branch gets updated when we modify the working copy, and it should get
253 // exported to Git without requiring any other changes
254 std::fs::write(workspace_root.join("file"), "modified").unwrap();
255 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
256 @ fab22d1acf5bb9c5aa48cb2c3dd2132072a359ca foo
257 ◉ 0000000000000000000000000000000000000000
258 "###);
259 insta::assert_snapshot!(git_repo
260 .find_reference("refs/heads/foo")
261 .unwrap()
262 .target()
263 .unwrap()
264 .to_string(), @"fab22d1acf5bb9c5aa48cb2c3dd2132072a359ca");
265}
266
267#[test]
268fn test_git_colocated_rebase_on_import() {
269 let test_env = TestEnvironment::default();
270 let workspace_root = test_env.env_root().join("repo");
271 let git_repo = git2::Repository::init(&workspace_root).unwrap();
272 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
273
274 // Make some changes in jj and check that they're reflected in git
275 std::fs::write(workspace_root.join("file"), "contents").unwrap();
276 test_env.jj_cmd_ok(&workspace_root, &["commit", "-m", "add a file"]);
277 std::fs::write(workspace_root.join("file"), "modified").unwrap();
278 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "master"]);
279 test_env.jj_cmd_ok(&workspace_root, &["commit", "-m", "modify a file"]);
280 // TODO: We shouldn't need this command here to trigger an import of the
281 // refs/heads/master we just exported
282 test_env.jj_cmd_ok(&workspace_root, &["st"]);
283
284 // Move `master` and HEAD backwards, which should result in commit2 getting
285 // hidden, and a new working-copy commit at the new position.
286 let commit2_oid = git_repo
287 .find_branch("master", git2::BranchType::Local)
288 .unwrap()
289 .get()
290 .target()
291 .unwrap();
292 let commit2 = git_repo.find_commit(commit2_oid).unwrap();
293 let commit1 = commit2.parents().next().unwrap();
294 git_repo.branch("master", &commit1, true).unwrap();
295 git_repo.set_head("refs/heads/master").unwrap();
296 let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root);
297 insta::assert_snapshot!(stdout, @r###"
298 @ 7f96185cfbe36341d0f9a86ebfaeab67a5922c7e
299 ◉ 4bcbeaba9a4b309c5f45a8807fbf5499b9714315 master HEAD@git add a file
300 ◉ 0000000000000000000000000000000000000000
301 "###);
302 insta::assert_snapshot!(stderr, @r###"
303 Reset the working copy parent to the new Git HEAD.
304 Abandoned 1 commits that are no longer reachable.
305 Done importing changes from the underlying Git repo.
306 "###);
307}
308
309#[test]
310fn test_git_colocated_branches() {
311 let test_env = TestEnvironment::default();
312 let workspace_root = test_env.env_root().join("repo");
313 let git_repo = git2::Repository::init(&workspace_root).unwrap();
314 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
315 test_env.jj_cmd_ok(&workspace_root, &["new", "-m", "foo"]);
316 test_env.jj_cmd_ok(&workspace_root, &["new", "@-", "-m", "bar"]);
317 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
318 @ 3560559274ab431feea00b7b7e0b9250ecce951f bar
319 │ ◉ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda foo
320 ├─╯
321 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git
322 ◉ 0000000000000000000000000000000000000000
323 "###);
324
325 // Create a branch in jj. It should be exported to Git even though it points to
326 // the working- copy commit.
327 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "master"]);
328 insta::assert_snapshot!(
329 git_repo.find_reference("refs/heads/master").unwrap().target().unwrap().to_string(),
330 @"3560559274ab431feea00b7b7e0b9250ecce951f"
331 );
332 insta::assert_snapshot!(
333 git_repo.head().unwrap().target().unwrap().to_string(),
334 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22"
335 );
336
337 // Update the branch in Git
338 let target_id = test_env.jj_cmd_success(
339 &workspace_root,
340 &["log", "--no-graph", "-T=commit_id", "-r=description(foo)"],
341 );
342 git_repo
343 .reference(
344 "refs/heads/master",
345 Oid::from_str(&target_id).unwrap(),
346 true,
347 "test",
348 )
349 .unwrap();
350 let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root);
351 insta::assert_snapshot!(stdout, @r###"
352 @ 096dc80da67094fbaa6683e2a205dddffa31f9a8
353 │ ◉ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda master foo
354 ├─╯
355 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git
356 ◉ 0000000000000000000000000000000000000000
357 "###);
358 insta::assert_snapshot!(stderr, @r###"
359 Abandoned 1 commits that are no longer reachable.
360 Working copy now at: yqosqzyt 096dc80d (empty) (no description set)
361 Parent commit : qpvuntsm 230dd059 (empty) (no description set)
362 Done importing changes from the underlying Git repo.
363 "###);
364}
365
366#[test]
367fn test_git_colocated_branch_forget() {
368 let test_env = TestEnvironment::default();
369 let workspace_root = test_env.env_root().join("repo");
370 let _git_repo = git2::Repository::init(&workspace_root).unwrap();
371 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
372 test_env.jj_cmd_ok(&workspace_root, &["new"]);
373 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "foo"]);
374 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
375 @ 65b6b74e08973b88d38404430f119c8c79465250 foo
376 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git
377 ◉ 0000000000000000000000000000000000000000
378 "###);
379 let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all-remotes"]);
380 insta::assert_snapshot!(stdout, @r###"
381 foo: rlvkpnrz 65b6b74e (empty) (no description set)
382 @git: rlvkpnrz 65b6b74e (empty) (no description set)
383 "###);
384
385 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["branch", "forget", "foo"]);
386 insta::assert_snapshot!(stdout, @"");
387 insta::assert_snapshot!(stderr, @"");
388 // A forgotten branch is deleted in the git repo. For a detailed demo explaining
389 // this, see `test_branch_forget_export` in `test_branch_command.rs`.
390 let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all-remotes"]);
391 insta::assert_snapshot!(stdout, @"");
392}
393
394#[test]
395fn test_git_colocated_conflicting_git_refs() {
396 let test_env = TestEnvironment::default();
397 let workspace_root = test_env.env_root().join("repo");
398 git2::Repository::init(&workspace_root).unwrap();
399 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
400 test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "main"]);
401 let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "main/sub"]);
402 insta::assert_snapshot!(stdout, @"");
403 insta::with_settings!({filters => vec![(": The lock for resource.*", ": ...")]}, {
404 insta::assert_snapshot!(stderr, @r###"
405 Warning: Failed to export some branches:
406 main/sub: Failed to set: A lock could not be obtained for reference "refs/heads/main/sub": ...
407 Hint: Git doesn't allow a branch name that looks like a parent directory of
408 another (e.g. `foo` and `foo/bar`). Try to rename the branches that failed to
409 export or their "parent" branches.
410 "###);
411 });
412}
413
414#[test]
415fn test_git_colocated_checkout_non_empty_working_copy() {
416 let test_env = TestEnvironment::default();
417 let workspace_root = test_env.env_root().join("repo");
418 let git_repo = git2::Repository::init(&workspace_root).unwrap();
419 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
420
421 // Create an initial commit in Git
422 // We use this to set HEAD to master
423 std::fs::write(workspace_root.join("file"), "contents").unwrap();
424 git_repo
425 .index()
426 .unwrap()
427 .add_path(Path::new("file"))
428 .unwrap();
429 let tree1_oid = git_repo.index().unwrap().write_tree().unwrap();
430 let tree1 = git_repo.find_tree(tree1_oid).unwrap();
431 let signature = git2::Signature::new(
432 "Someone",
433 "someone@example.com",
434 &git2::Time::new(1234567890, 60),
435 )
436 .unwrap();
437 git_repo
438 .commit(
439 Some("refs/heads/master"),
440 &signature,
441 &signature,
442 "initial",
443 &tree1,
444 &[],
445 )
446 .unwrap();
447 insta::assert_snapshot!(
448 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
449 @"e61b6729ff4292870702f2f72b2a60165679ef37"
450 );
451
452 std::fs::write(workspace_root.join("two"), "y").unwrap();
453
454 test_env.jj_cmd_ok(&workspace_root, &["describe", "-m", "two"]);
455 test_env.jj_cmd_ok(&workspace_root, &["new", "@-"]);
456 let (_, stderr) = test_env.jj_cmd_ok(&workspace_root, &["describe", "-m", "new"]);
457 insta::assert_snapshot!(stderr, @r###"
458 Working copy now at: kkmpptxz 4c049607 (empty) new
459 Parent commit : lnksqltp e61b6729 master | initial
460 "###);
461
462 let git_head = git_repo.find_reference("HEAD").unwrap();
463 let git_head_target = git_head.symbolic_target().unwrap();
464
465 assert_eq!(git_head_target, "refs/heads/master");
466
467 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
468 @ 4c04960765ca906d0cb25b15a946be4c0dd71b8e new
469 │ ◉ 4ec6f6506bd1903410f15b80058a7f0d8f62deea two
470 ├─╯
471 ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial
472 ◉ 0000000000000000000000000000000000000000
473 "###);
474}
475
476#[test]
477fn test_git_colocated_fetch_deleted_or_moved_branch() {
478 let test_env = TestEnvironment::default();
479 test_env.add_config("git.auto-local-branch = true");
480 let origin_path = test_env.env_root().join("origin");
481 git2::Repository::init(&origin_path).unwrap();
482 test_env.jj_cmd_ok(&origin_path, &["init", "--git-repo=."]);
483 test_env.jj_cmd_ok(&origin_path, &["describe", "-m=A"]);
484 test_env.jj_cmd_ok(&origin_path, &["branch", "create", "A"]);
485 test_env.jj_cmd_ok(&origin_path, &["new", "-m=B_to_delete"]);
486 test_env.jj_cmd_ok(&origin_path, &["branch", "create", "B_to_delete"]);
487 test_env.jj_cmd_ok(&origin_path, &["new", "-m=original C", "@-"]);
488 test_env.jj_cmd_ok(&origin_path, &["branch", "create", "C_to_move"]);
489
490 let clone_path = test_env.env_root().join("clone");
491 git2::Repository::clone(origin_path.to_str().unwrap(), &clone_path).unwrap();
492 test_env.jj_cmd_ok(&clone_path, &["init", "--git-repo=."]);
493 test_env.jj_cmd_ok(&clone_path, &["new", "A"]);
494 insta::assert_snapshot!(get_log_output(&test_env, &clone_path), @r###"
495 @ 0335878796213c3a701f1c9c34dcae242bee4131
496 │ ◉ 8d4e006fd63547965fbc3a26556a9aa531076d32 C_to_move original C
497 ├─╯
498 │ ◉ 929e298ae9edf969b405a304c75c10457c47d52c B_to_delete B_to_delete
499 ├─╯
500 ◉ a86754f975f953fa25da4265764adc0c62e9ce6b A HEAD@git A
501 ◉ 0000000000000000000000000000000000000000
502 "###);
503
504 test_env.jj_cmd_ok(&origin_path, &["branch", "delete", "B_to_delete"]);
505 // Move branch C sideways
506 test_env.jj_cmd_ok(&origin_path, &["describe", "C_to_move", "-m", "moved C"]);
507 let (stdout, stderr) = test_env.jj_cmd_ok(&clone_path, &["git", "fetch"]);
508 insta::assert_snapshot!(stdout, @"");
509 insta::assert_snapshot!(stderr, @r###"
510 branch: B_to_delete@origin [deleted] untracked
511 branch: C_to_move@origin [updated] tracked
512 Abandoned 2 commits that are no longer reachable.
513 "###);
514 // "original C" and "B_to_delete" are abandoned, as the corresponding branches
515 // were deleted or moved on the remote (#864)
516 insta::assert_snapshot!(get_log_output(&test_env, &clone_path), @r###"
517 ◉ 04fd29df05638156b20044b3b6136b42abcb09ab C_to_move moved C
518 │ @ 0335878796213c3a701f1c9c34dcae242bee4131
519 ├─╯
520 ◉ a86754f975f953fa25da4265764adc0c62e9ce6b A HEAD@git A
521 ◉ 0000000000000000000000000000000000000000
522 "###);
523}
524
525#[test]
526fn test_git_colocated_rebase_dirty_working_copy() {
527 let test_env = TestEnvironment::default();
528 let repo_path = test_env.env_root().join("repo");
529 let git_repo = git2::Repository::init(&repo_path).unwrap();
530 test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]);
531
532 std::fs::write(repo_path.join("file"), "base").unwrap();
533 test_env.jj_cmd_ok(&repo_path, &["new"]);
534 std::fs::write(repo_path.join("file"), "old").unwrap();
535 test_env.jj_cmd_ok(&repo_path, &["branch", "create", "feature"]);
536
537 // Make the working-copy dirty, delete the checked out branch.
538 std::fs::write(repo_path.join("file"), "new").unwrap();
539 git_repo
540 .find_reference("refs/heads/feature")
541 .unwrap()
542 .delete()
543 .unwrap();
544
545 // Because the working copy is dirty, the new working-copy commit will be
546 // diverged. Therefore, the feature branch has change-delete conflict.
547 let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["status"]);
548 insta::assert_snapshot!(stdout, @r###"
549 Working copy changes:
550 M file
551 Working copy : rlvkpnrz d6c5e664 feature?? | (no description set)
552 Parent commit: qpvuntsm 5973d373 (no description set)
553 These branches have conflicts:
554 feature
555 Use `jj branch list` to see details. Use `jj branch set <name> -r <rev>` to resolve.
556 "###);
557 insta::assert_snapshot!(stderr, @r###"
558 Warning: Failed to export some branches:
559 feature: Modified ref had been deleted in Git
560 Done importing changes from the underlying Git repo.
561 "###);
562 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
563 @ d6c5e66473426f5ed3a24ecce8ce8b44ff23cf81 feature??
564 ◉ 5973d3731aba9dd86c00b4a765fbc4cc13f1e14b HEAD@git
565 ◉ 0000000000000000000000000000000000000000
566 "###);
567
568 // The working-copy content shouldn't be lost.
569 insta::assert_snapshot!(
570 std::fs::read_to_string(repo_path.join("file")).unwrap(), @"new");
571}
572
573#[test]
574fn test_git_colocated_external_checkout() {
575 let test_env = TestEnvironment::default();
576 let repo_path = test_env.env_root().join("repo");
577 let git_repo = git2::Repository::init(&repo_path).unwrap();
578 test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]);
579 test_env.jj_cmd_ok(&repo_path, &["ci", "-m=A"]);
580 test_env.jj_cmd_ok(&repo_path, &["branch", "create", "-r@-", "master"]);
581 test_env.jj_cmd_ok(&repo_path, &["new", "-m=B", "root()"]);
582 test_env.jj_cmd_ok(&repo_path, &["new"]);
583
584 // Checked out anonymous branch
585 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
586 @ f8a23336e41840ed1757ef323402a770427dc89a
587 ◉ eccedddfa5152d99fc8ddd1081b375387a8a382a HEAD@git B
588 │ ◉ a86754f975f953fa25da4265764adc0c62e9ce6b master A
589 ├─╯
590 ◉ 0000000000000000000000000000000000000000
591 "###);
592
593 // Check out another branch by external command
594 git_repo
595 .set_head_detached(
596 git_repo
597 .find_reference("refs/heads/master")
598 .unwrap()
599 .target()
600 .unwrap(),
601 )
602 .unwrap();
603
604 // The old working-copy commit gets abandoned, but the whole branch should not
605 // be abandoned. (#1042)
606 let (stdout, stderr) = get_log_output_with_stderr(&test_env, &repo_path);
607 insta::assert_snapshot!(stdout, @r###"
608 @ adadbd65a794e2294962b3c3da9aada09fe1b472
609 ◉ a86754f975f953fa25da4265764adc0c62e9ce6b master HEAD@git A
610 │ ◉ eccedddfa5152d99fc8ddd1081b375387a8a382a B
611 ├─╯
612 ◉ 0000000000000000000000000000000000000000
613 "###);
614 insta::assert_snapshot!(stderr, @r###"
615 Reset the working copy parent to the new Git HEAD.
616 "###);
617}
618
619#[test]
620fn test_git_colocated_squash_undo() {
621 let test_env = TestEnvironment::default();
622 let repo_path = test_env.env_root().join("repo");
623 git2::Repository::init(&repo_path).unwrap();
624 test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]);
625 test_env.jj_cmd_ok(&repo_path, &["ci", "-m=A"]);
626 // Test the setup
627 insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###"
628 @ rlvkpnrzqnoo 8f71e3b6a3be
629 ◉ qpvuntsmwlqt a86754f975f9 A HEAD@git
630 ◉ zzzzzzzzzzzz 000000000000
631 "###);
632
633 test_env.jj_cmd_ok(&repo_path, &["squash"]);
634 insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###"
635 @ zsuskulnrvyr f0c12b0396d9
636 ◉ qpvuntsmwlqt 2f376ea1478c A HEAD@git
637 ◉ zzzzzzzzzzzz 000000000000
638 "###);
639 test_env.jj_cmd_ok(&repo_path, &["undo"]);
640 // TODO: There should be no divergence here; 2f376ea1478c should be hidden
641 // (#922)
642 insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###"
643 @ rlvkpnrzqnoo 8f71e3b6a3be
644 ◉ qpvuntsmwlqt a86754f975f9 A HEAD@git
645 ◉ zzzzzzzzzzzz 000000000000
646 "###);
647}
648
649#[test]
650fn test_git_colocated_undo_head_move() {
651 let test_env = TestEnvironment::default();
652 let repo_path = test_env.env_root().join("repo");
653 let git_repo = git2::Repository::init(&repo_path).unwrap();
654 test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]);
655
656 // Create new HEAD
657 test_env.jj_cmd_ok(&repo_path, &["new"]);
658 insta::assert_snapshot!(
659 git_repo.head().unwrap().target().unwrap().to_string(),
660 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22");
661 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
662 @ 65b6b74e08973b88d38404430f119c8c79465250
663 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git
664 ◉ 0000000000000000000000000000000000000000
665 "###);
666
667 // HEAD should be unset
668 test_env.jj_cmd_ok(&repo_path, &["undo"]);
669 assert!(git_repo.head().is_err());
670 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
671 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
672 ◉ 0000000000000000000000000000000000000000
673 "###);
674
675 // Create commit on non-root commit
676 test_env.jj_cmd_ok(&repo_path, &["new"]);
677 test_env.jj_cmd_ok(&repo_path, &["new"]);
678 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
679 @ 69b19f73cf584f162f078fb0d91c55ca39d10bc7
680 ◉ eb08b363bb5ef8ee549314260488980d7bbe8f63 HEAD@git
681 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
682 ◉ 0000000000000000000000000000000000000000
683 "###);
684 insta::assert_snapshot!(
685 git_repo.head().unwrap().target().unwrap().to_string(),
686 @"eb08b363bb5ef8ee549314260488980d7bbe8f63");
687
688 // HEAD should be moved back
689 let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["undo"]);
690 insta::assert_snapshot!(stdout, @"");
691 insta::assert_snapshot!(stderr, @r###"
692 Working copy now at: royxmykx eb08b363 (empty) (no description set)
693 Parent commit : qpvuntsm 230dd059 (empty) (no description set)
694 "###);
695 insta::assert_snapshot!(
696 git_repo.head().unwrap().target().unwrap().to_string(),
697 @"230dd059e1b059aefc0da06a2e5a7dbf22362f22");
698 insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
699 @ eb08b363bb5ef8ee549314260488980d7bbe8f63
700 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git
701 ◉ 0000000000000000000000000000000000000000
702 "###);
703}
704
705fn get_log_output_divergence(test_env: &TestEnvironment, repo_path: &Path) -> String {
706 let template = r#"
707 separate(" ",
708 change_id.short(),
709 commit_id.short(),
710 description.first_line(),
711 branches,
712 git_head,
713 if(divergent, "!divergence!"),
714 )
715 "#;
716 test_env.jj_cmd_success(repo_path, &["log", "-T", template])
717}
718
719fn get_log_output(test_env: &TestEnvironment, workspace_root: &Path) -> String {
720 let template = r#"separate(" ", commit_id, branches, git_head, description)"#;
721 test_env.jj_cmd_success(workspace_root, &["log", "-T", template, "-r=all()"])
722}
723
724fn get_log_output_with_stderr(
725 test_env: &TestEnvironment,
726 workspace_root: &Path,
727) -> (String, String) {
728 let template = r#"separate(" ", commit_id, branches, git_head, description)"#;
729 test_env.jj_cmd_ok(workspace_root, &["log", "-T", template, "-r=all()"])
730}
731
732#[test]
733fn test_git_colocated_unreachable_commits() {
734 let test_env = TestEnvironment::default();
735 let workspace_root = test_env.env_root().join("repo");
736 let git_repo = git2::Repository::init(&workspace_root).unwrap();
737
738 // Create an initial commit in Git
739 let empty_tree_oid = git_repo.treebuilder(None).unwrap().write().unwrap();
740 let tree1 = git_repo.find_tree(empty_tree_oid).unwrap();
741 let signature = git2::Signature::new(
742 "Someone",
743 "someone@example.com",
744 &git2::Time::new(1234567890, 60),
745 )
746 .unwrap();
747 let oid1 = git_repo
748 .commit(
749 Some("refs/heads/master"),
750 &signature,
751 &signature,
752 "initial",
753 &tree1,
754 &[],
755 )
756 .unwrap();
757 insta::assert_snapshot!(
758 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
759 @"2ee37513d2b5e549f7478c671a780053614bff19"
760 );
761
762 // Add a second commit in Git
763 let tree2 = git_repo.find_tree(empty_tree_oid).unwrap();
764 let signature = git2::Signature::new(
765 "Someone",
766 "someone@example.com",
767 &git2::Time::new(1234567890, 62),
768 )
769 .unwrap();
770 let oid2 = git_repo
771 .commit(
772 None,
773 &signature,
774 &signature,
775 "next",
776 &tree2,
777 &[&git_repo.find_commit(oid1).unwrap()],
778 )
779 .unwrap();
780 insta::assert_snapshot!(
781 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
782 @"2ee37513d2b5e549f7478c671a780053614bff19"
783 );
784
785 // Import the repo while there is no path to the second commit
786 test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]);
787 insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
788 @ 66ae47cee4f8c28ee8d7e4f5d9401b03c07e22f2
789 ◉ 2ee37513d2b5e549f7478c671a780053614bff19 master HEAD@git initial
790 ◉ 0000000000000000000000000000000000000000
791 "###);
792 insta::assert_snapshot!(
793 git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(),
794 @"2ee37513d2b5e549f7478c671a780053614bff19"
795 );
796
797 // Check that trying to look up the second commit fails gracefully
798 let stderr = test_env.jj_cmd_failure(&workspace_root, &["show", &oid2.to_string()]);
799 insta::assert_snapshot!(stderr, @r###"
800 Error: Revision "8e713ff77b54928dd4a82aaabeca44b1ae91722c" doesn't exist
801 "###);
802}