just playing with tangled
1// Copyright 2020 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::collections::{BTreeMap, HashSet};
16use std::path::PathBuf;
17use std::sync::{mpsc, Arc, Barrier};
18use std::thread;
19
20use git2::Oid;
21use itertools::Itertools;
22use jj_lib::backend::{
23 BackendError, ChangeId, CommitId, MillisSinceEpoch, ObjectId, Signature, Timestamp,
24};
25use jj_lib::commit::Commit;
26use jj_lib::commit_builder::CommitBuilder;
27use jj_lib::git;
28use jj_lib::git::{GitFetchError, GitPushError, GitRefUpdate, SubmoduleConfig};
29use jj_lib::git_backend::GitBackend;
30use jj_lib::op_store::{BranchTarget, RefTarget};
31use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo};
32use jj_lib::settings::{GitSettings, UserSettings};
33use jj_lib::view::RefName;
34use maplit::{btreemap, hashset};
35use tempfile::TempDir;
36use testutils::{create_random_commit, load_repo_at_head, write_random_commit, TestRepo};
37
38fn empty_git_commit<'r>(
39 git_repo: &'r git2::Repository,
40 ref_name: &str,
41 parents: &[&git2::Commit],
42) -> git2::Commit<'r> {
43 let signature = git2::Signature::now("Someone", "someone@example.com").unwrap();
44 let empty_tree_id = Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
45 let empty_tree = git_repo.find_tree(empty_tree_id).unwrap();
46 let oid = git_repo
47 .commit(
48 Some(ref_name),
49 &signature,
50 &signature,
51 &format!("random commit {}", rand::random::<u32>()),
52 &empty_tree,
53 parents,
54 )
55 .unwrap();
56 git_repo.find_commit(oid).unwrap()
57}
58
59fn jj_id(commit: &git2::Commit) -> CommitId {
60 CommitId::from_bytes(commit.id().as_bytes())
61}
62
63fn git_id(commit: &Commit) -> Oid {
64 Oid::from_bytes(commit.id().as_bytes()).unwrap()
65}
66
67fn get_git_repo(repo: &Arc<ReadonlyRepo>) -> git2::Repository {
68 repo.store()
69 .backend_impl()
70 .downcast_ref::<GitBackend>()
71 .unwrap()
72 .git_repo_clone()
73}
74
75#[test]
76fn test_import_refs() {
77 let settings = testutils::user_settings();
78 let git_settings = GitSettings::default();
79 let test_repo = TestRepo::init(true);
80 let repo = &test_repo.repo;
81 let git_repo = get_git_repo(repo);
82
83 let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
84 git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
85 let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
86 let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
87 let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
88 let commit5 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[&commit1]);
89 let commit6 = empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[&commit1]);
90 // Should not be imported
91 empty_git_commit(&git_repo, "refs/notes/x", &[&commit2]);
92 empty_git_commit(&git_repo, "refs/remotes/origin/HEAD", &[&commit2]);
93
94 git_repo.set_head("refs/heads/main").unwrap();
95
96 let git_repo = get_git_repo(repo);
97 let mut tx = repo.start_transaction(&settings, "test");
98 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
99 tx.mut_repo().rebase_descendants(&settings).unwrap();
100 let repo = tx.commit();
101 let view = repo.view();
102
103 let expected_heads = hashset! {
104 jj_id(&commit3),
105 jj_id(&commit4),
106 jj_id(&commit5),
107 jj_id(&commit6)
108 };
109 assert_eq!(*view.heads(), expected_heads);
110
111 let expected_main_branch = BranchTarget {
112 local_target: RefTarget::normal(jj_id(&commit2)),
113 remote_targets: btreemap! {
114 "origin".to_string() => RefTarget::normal(jj_id(&commit1)),
115 },
116 };
117 assert_eq!(view.get_branch("main"), Some(expected_main_branch).as_ref());
118 let expected_feature1_branch = BranchTarget {
119 local_target: RefTarget::normal(jj_id(&commit3)),
120 remote_targets: btreemap! {},
121 };
122 assert_eq!(
123 view.get_branch("feature1"),
124 Some(expected_feature1_branch).as_ref()
125 );
126 let expected_feature2_branch = BranchTarget {
127 local_target: RefTarget::normal(jj_id(&commit4)),
128 remote_targets: btreemap! {},
129 };
130 assert_eq!(
131 view.get_branch("feature2"),
132 Some(expected_feature2_branch).as_ref()
133 );
134 let expected_feature3_branch = BranchTarget {
135 local_target: RefTarget::normal(jj_id(&commit6)),
136 remote_targets: btreemap! {
137 "origin".to_string() => RefTarget::normal(jj_id(&commit6)),
138 },
139 };
140 assert_eq!(
141 view.get_branch("feature3"),
142 Some(expected_feature3_branch).as_ref()
143 );
144
145 assert_eq!(view.get_tag("v1.0"), &RefTarget::normal(jj_id(&commit5)));
146
147 assert_eq!(view.git_refs().len(), 6);
148 assert_eq!(
149 view.get_git_ref("refs/heads/main"),
150 &RefTarget::normal(jj_id(&commit2))
151 );
152 assert_eq!(
153 view.get_git_ref("refs/heads/feature1"),
154 &RefTarget::normal(jj_id(&commit3))
155 );
156 assert_eq!(
157 view.get_git_ref("refs/heads/feature2"),
158 &RefTarget::normal(jj_id(&commit4))
159 );
160 assert_eq!(
161 view.get_git_ref("refs/remotes/origin/main"),
162 &RefTarget::normal(jj_id(&commit1))
163 );
164 assert_eq!(
165 view.get_git_ref("refs/remotes/origin/feature3"),
166 &RefTarget::normal(jj_id(&commit6))
167 );
168 assert_eq!(
169 view.get_git_ref("refs/tags/v1.0"),
170 &RefTarget::normal(jj_id(&commit5))
171 );
172 assert_eq!(view.git_head(), &RefTarget::normal(jj_id(&commit2)));
173}
174
175#[test]
176fn test_import_refs_reimport() {
177 let settings = testutils::user_settings();
178 let git_settings = GitSettings::default();
179 let test_workspace = TestRepo::init(true);
180 let repo = &test_workspace.repo;
181 let git_repo = get_git_repo(repo);
182
183 let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
184 git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
185 let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
186 let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
187 let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
188 let pgp_key_oid = git_repo.blob(b"my PGP key").unwrap();
189 git_repo
190 .reference("refs/tags/my-gpg-key", pgp_key_oid, false, "")
191 .unwrap();
192
193 let mut tx = repo.start_transaction(&settings, "test");
194 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
195 tx.mut_repo().rebase_descendants(&settings).unwrap();
196 let repo = tx.commit();
197
198 let expected_heads = hashset! {
199 jj_id(&commit3),
200 jj_id(&commit4),
201 };
202 let view = repo.view();
203 assert_eq!(*view.heads(), expected_heads);
204
205 // Delete feature1 and rewrite feature2
206 delete_git_ref(&git_repo, "refs/heads/feature1");
207 delete_git_ref(&git_repo, "refs/heads/feature2");
208 let commit5 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
209
210 // Also modify feature2 on the jj side
211 let mut tx = repo.start_transaction(&settings, "test");
212 let commit6 = create_random_commit(tx.mut_repo(), &settings)
213 .set_parents(vec![jj_id(&commit2)])
214 .write()
215 .unwrap();
216 tx.mut_repo()
217 .set_local_branch_target("feature2", RefTarget::normal(commit6.id().clone()));
218 let repo = tx.commit();
219
220 let mut tx = repo.start_transaction(&settings, "test");
221 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
222 tx.mut_repo().rebase_descendants(&settings).unwrap();
223 let repo = tx.commit();
224
225 let view = repo.view();
226 let expected_heads = hashset! {
227 jj_id(&commit5),
228 commit6.id().clone(),
229 };
230 assert_eq!(*view.heads(), expected_heads);
231
232 assert_eq!(view.branches().len(), 2);
233 let commit1_target = RefTarget::normal(jj_id(&commit1));
234 let commit2_target = RefTarget::normal(jj_id(&commit2));
235 let expected_main_branch = BranchTarget {
236 local_target: RefTarget::normal(jj_id(&commit2)),
237 remote_targets: btreemap! {
238 "origin".to_string() => commit1_target.clone(),
239 },
240 };
241 assert_eq!(view.get_branch("main"), Some(expected_main_branch).as_ref());
242 let expected_feature2_branch = BranchTarget {
243 local_target: RefTarget::from_legacy_form(
244 [jj_id(&commit4)],
245 [commit6.id().clone(), jj_id(&commit5)],
246 ),
247 remote_targets: btreemap! {},
248 };
249 assert_eq!(
250 view.get_branch("feature2"),
251 Some(expected_feature2_branch).as_ref()
252 );
253
254 assert!(view.tags().is_empty());
255
256 assert_eq!(view.git_refs().len(), 3);
257 assert_eq!(view.get_git_ref("refs/heads/main"), &commit2_target);
258 assert_eq!(
259 view.get_git_ref("refs/remotes/origin/main"),
260 &commit1_target
261 );
262 let commit5_target = RefTarget::normal(jj_id(&commit5));
263 assert_eq!(view.get_git_ref("refs/heads/feature2"), &commit5_target);
264}
265
266#[test]
267fn test_import_refs_reimport_head_removed() {
268 // Test that re-importing refs doesn't cause a deleted head to come back
269 let settings = testutils::user_settings();
270 let git_settings = GitSettings::default();
271 let test_repo = TestRepo::init(true);
272 let repo = &test_repo.repo;
273 let git_repo = get_git_repo(repo);
274
275 let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
276 let mut tx = repo.start_transaction(&settings, "test");
277 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
278 tx.mut_repo().rebase_descendants(&settings).unwrap();
279 let commit_id = jj_id(&commit);
280 // Test the setup
281 assert!(tx.mut_repo().view().heads().contains(&commit_id));
282
283 // Remove the head and re-import
284 tx.mut_repo().remove_head(&commit_id);
285 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
286 tx.mut_repo().rebase_descendants(&settings).unwrap();
287 assert!(!tx.mut_repo().view().heads().contains(&commit_id));
288}
289
290#[test]
291fn test_import_refs_reimport_git_head_counts() {
292 // Test that if a branch is removed but the Git HEAD points to the commit (or a
293 // descendant of it), we still keep it alive.
294 let settings = testutils::user_settings();
295 let git_settings = GitSettings::default();
296 let test_repo = TestRepo::init(true);
297 let repo = &test_repo.repo;
298 let git_repo = get_git_repo(repo);
299
300 let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
301 git_repo.set_head_detached(commit.id()).unwrap();
302
303 let mut tx = repo.start_transaction(&settings, "test");
304 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
305 tx.mut_repo().rebase_descendants(&settings).unwrap();
306
307 // Delete the branch and re-import. The commit should still be there since HEAD
308 // points to it
309 git_repo
310 .find_reference("refs/heads/main")
311 .unwrap()
312 .delete()
313 .unwrap();
314 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
315 tx.mut_repo().rebase_descendants(&settings).unwrap();
316 assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit)));
317}
318
319#[test]
320fn test_import_refs_reimport_git_head_without_ref() {
321 // Simulate external `git checkout` in colocated repo, from anonymous branch.
322 let settings = testutils::user_settings();
323 let git_settings = GitSettings::default();
324 let test_repo = TestRepo::init(true);
325 let repo = &test_repo.repo;
326 let git_repo = get_git_repo(repo);
327
328 // First, HEAD points to commit1.
329 let mut tx = repo.start_transaction(&settings, "test");
330 let commit1 = write_random_commit(tx.mut_repo(), &settings);
331 let commit2 = write_random_commit(tx.mut_repo(), &settings);
332 git_repo.set_head_detached(git_id(&commit1)).unwrap();
333
334 // Import HEAD.
335 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
336 tx.mut_repo().rebase_descendants(&settings).unwrap();
337 assert!(tx.mut_repo().view().heads().contains(commit1.id()));
338 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
339
340 // Move HEAD to commit2 (by e.g. `git checkout` command)
341 git_repo.set_head_detached(git_id(&commit2)).unwrap();
342
343 // Reimport HEAD, which doesn't abandon the old HEAD branch because jj thinks it
344 // would be moved by `git checkout` command. This isn't always true because the
345 // detached HEAD commit could be rewritten by e.g. `git commit --amend` command,
346 // but it should be safer than abandoning old checkout branch.
347 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
348 tx.mut_repo().rebase_descendants(&settings).unwrap();
349 assert!(tx.mut_repo().view().heads().contains(commit1.id()));
350 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
351}
352
353#[test]
354fn test_import_refs_reimport_git_head_with_moved_ref() {
355 // Simulate external history rewriting in colocated repo.
356 let settings = testutils::user_settings();
357 let git_settings = GitSettings::default();
358 let test_repo = TestRepo::init(true);
359 let repo = &test_repo.repo;
360 let git_repo = get_git_repo(repo);
361
362 // First, both HEAD and main point to commit1.
363 let mut tx = repo.start_transaction(&settings, "test");
364 let commit1 = write_random_commit(tx.mut_repo(), &settings);
365 let commit2 = write_random_commit(tx.mut_repo(), &settings);
366 git_repo
367 .reference("refs/heads/main", git_id(&commit1), true, "test")
368 .unwrap();
369 git_repo.set_head_detached(git_id(&commit1)).unwrap();
370
371 // Import HEAD and main.
372 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
373 tx.mut_repo().rebase_descendants(&settings).unwrap();
374 assert!(tx.mut_repo().view().heads().contains(commit1.id()));
375 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
376
377 // Move both HEAD and main to commit2 (by e.g. `git commit --amend` command)
378 git_repo
379 .reference("refs/heads/main", git_id(&commit2), true, "test")
380 .unwrap();
381 git_repo.set_head_detached(git_id(&commit2)).unwrap();
382
383 // Reimport HEAD and main, which abandons the old main branch.
384 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
385 tx.mut_repo().rebase_descendants(&settings).unwrap();
386 assert!(!tx.mut_repo().view().heads().contains(commit1.id()));
387 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
388}
389
390#[test]
391fn test_import_refs_reimport_with_deleted_remote_ref() {
392 let settings = testutils::user_settings();
393 let git_settings = GitSettings::default();
394 let test_workspace = TestRepo::init(true);
395 let repo = &test_workspace.repo;
396 let git_repo = get_git_repo(repo);
397
398 let commit_base = empty_git_commit(&git_repo, "refs/heads/main", &[]);
399 let commit_main = empty_git_commit(&git_repo, "refs/heads/main", &[&commit_base]);
400 let commit_remote_only = empty_git_commit(
401 &git_repo,
402 "refs/remotes/origin/feature-remote-only",
403 &[&commit_base],
404 );
405 let commit_remote_and_local = empty_git_commit(
406 &git_repo,
407 "refs/remotes/origin/feature-remote-and-local",
408 &[&commit_base],
409 );
410 git_ref(
411 &git_repo,
412 "refs/heads/feature-remote-and-local",
413 commit_remote_and_local.id(),
414 );
415
416 let mut tx = repo.start_transaction(&settings, "test");
417 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
418 tx.mut_repo().rebase_descendants(&settings).unwrap();
419 let repo = tx.commit();
420
421 let expected_heads = hashset! {
422 jj_id(&commit_main),
423 jj_id(&commit_remote_only),
424 jj_id(&commit_remote_and_local),
425 };
426 let view = repo.view();
427 assert_eq!(*view.heads(), expected_heads);
428 assert_eq!(view.branches().len(), 3);
429 assert_eq!(
430 view.get_branch("feature-remote-only"),
431 Some(&BranchTarget {
432 // Even though the git repo does not have a local branch for `feature-remote-only`, jj
433 // creates one. This follows the model explained in docs/branches.md.
434 local_target: RefTarget::normal(jj_id(&commit_remote_only)),
435 remote_targets: btreemap! {
436 "origin".to_string() => RefTarget::normal(jj_id(&commit_remote_only)),
437 },
438 }),
439 );
440 assert_eq!(
441 view.get_branch("feature-remote-and-local"),
442 Some(&BranchTarget {
443 local_target: RefTarget::normal(jj_id(&commit_remote_and_local)),
444 remote_targets: btreemap! {
445 "origin".to_string() => RefTarget::normal(jj_id(&commit_remote_and_local)),
446 },
447 }),
448 );
449 view.get_branch("main").unwrap(); // branch #3 of 3
450
451 // Simulate fetching from a remote where feature-remote-only and
452 // feature-remote-and-local branches were deleted. This leads to the
453 // following import deleting the corresponding local branches.
454 delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-only");
455 delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-and-local");
456
457 let mut tx = repo.start_transaction(&settings, "test");
458 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
459 tx.mut_repo().rebase_descendants(&settings).unwrap();
460 let repo = tx.commit();
461
462 let view = repo.view();
463 // The local branches were indeed deleted
464 assert_eq!(view.branches().len(), 1);
465 view.get_branch("main").unwrap(); // branch #1 of 1
466 assert_eq!(view.get_branch("feature-remote-local"), None);
467 assert_eq!(view.get_branch("feature-remote-and-local"), None);
468 let expected_heads = hashset! {
469 jj_id(&commit_main),
470 // Neither commit_remote_only nor commit_remote_and_local should be
471 // listed as a head. commit_remote_only was never affected by #864,
472 // but commit_remote_and_local was.
473 };
474 assert_eq!(*view.heads(), expected_heads);
475}
476
477/// This test is nearly identical to the previous one, except the branches are
478/// moved sideways instead of being deleted.
479#[test]
480fn test_import_refs_reimport_with_moved_remote_ref() {
481 let settings = testutils::user_settings();
482 let git_settings = GitSettings::default();
483 let test_workspace = TestRepo::init(true);
484 let repo = &test_workspace.repo;
485 let git_repo = get_git_repo(repo);
486
487 let commit_base = empty_git_commit(&git_repo, "refs/heads/main", &[]);
488 let commit_main = empty_git_commit(&git_repo, "refs/heads/main", &[&commit_base]);
489 let commit_remote_only = empty_git_commit(
490 &git_repo,
491 "refs/remotes/origin/feature-remote-only",
492 &[&commit_base],
493 );
494 let commit_remote_and_local = empty_git_commit(
495 &git_repo,
496 "refs/remotes/origin/feature-remote-and-local",
497 &[&commit_base],
498 );
499 git_ref(
500 &git_repo,
501 "refs/heads/feature-remote-and-local",
502 commit_remote_and_local.id(),
503 );
504
505 let mut tx = repo.start_transaction(&settings, "test");
506 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
507 tx.mut_repo().rebase_descendants(&settings).unwrap();
508 let repo = tx.commit();
509
510 let expected_heads = hashset! {
511 jj_id(&commit_main),
512 jj_id(dbg!(&commit_remote_only)),
513 jj_id(dbg!(&commit_remote_and_local)),
514 };
515 let view = repo.view();
516 assert_eq!(*view.heads(), expected_heads);
517 assert_eq!(view.branches().len(), 3);
518 assert_eq!(
519 view.get_branch("feature-remote-only"),
520 Some(&BranchTarget {
521 // Even though the git repo does not have a local branch for `feature-remote-only`, jj
522 // creates one. This follows the model explained in docs/branches.md.
523 local_target: RefTarget::normal(jj_id(&commit_remote_only)),
524 remote_targets: btreemap! {
525 "origin".to_string() => RefTarget::normal(jj_id(&commit_remote_only)),
526 },
527 }),
528 );
529 assert_eq!(
530 view.get_branch("feature-remote-and-local"),
531 Some(&BranchTarget {
532 local_target: RefTarget::normal(jj_id(&commit_remote_and_local)),
533 remote_targets: btreemap! {
534 "origin".to_string() => RefTarget::normal(jj_id(&commit_remote_and_local)),
535 },
536 }),
537 );
538 view.get_branch("main").unwrap(); // branch #3 of 3
539
540 // Simulate fetching from a remote where feature-remote-only and
541 // feature-remote-and-local branches were moved. This leads to the
542 // following import moving the corresponding local branches.
543 delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-only");
544 delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-and-local");
545 let new_commit_remote_only = empty_git_commit(
546 &git_repo,
547 "refs/remotes/origin/feature-remote-only",
548 &[&commit_base],
549 );
550 let new_commit_remote_and_local = empty_git_commit(
551 &git_repo,
552 "refs/remotes/origin/feature-remote-and-local",
553 &[&commit_base],
554 );
555
556 let mut tx = repo.start_transaction(&settings, "test");
557 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
558 tx.mut_repo().rebase_descendants(&settings).unwrap();
559 let repo = tx.commit();
560
561 let view = repo.view();
562 assert_eq!(view.branches().len(), 3);
563 // The local branches are moved
564 assert_eq!(
565 view.get_branch("feature-remote-only"),
566 Some(&BranchTarget {
567 local_target: RefTarget::normal(jj_id(&new_commit_remote_only)),
568 remote_targets: btreemap! {
569 "origin".to_string() => RefTarget::normal(jj_id(&new_commit_remote_only)),
570 },
571 }),
572 );
573 assert_eq!(
574 view.get_branch("feature-remote-and-local"),
575 Some(&BranchTarget {
576 local_target: RefTarget::normal(jj_id(&new_commit_remote_and_local)),
577 remote_targets: btreemap! {
578 "origin".to_string() => RefTarget::normal(jj_id(&new_commit_remote_and_local)),
579 },
580 }),
581 );
582 view.get_branch("main").unwrap(); // branch #3 of 3
583 let expected_heads = hashset! {
584 jj_id(&commit_main),
585 jj_id(&new_commit_remote_and_local),
586 jj_id(&new_commit_remote_only),
587 // Neither commit_remote_only nor commit_remote_and_local should be
588 // listed as a head. commit_remote_only was never affected by #864,
589 // but commit_remote_and_local was.
590 };
591 assert_eq!(*view.heads(), expected_heads);
592}
593
594#[test]
595fn test_import_refs_reimport_git_head_with_fixed_ref() {
596 // Simulate external `git checkout` in colocated repo, from named branch.
597 let settings = testutils::user_settings();
598 let git_settings = GitSettings::default();
599 let test_repo = TestRepo::init(true);
600 let repo = &test_repo.repo;
601 let git_repo = get_git_repo(repo);
602
603 // First, both HEAD and main point to commit1.
604 let mut tx = repo.start_transaction(&settings, "test");
605 let commit1 = write_random_commit(tx.mut_repo(), &settings);
606 let commit2 = write_random_commit(tx.mut_repo(), &settings);
607 git_repo
608 .reference("refs/heads/main", git_id(&commit1), true, "test")
609 .unwrap();
610 git_repo.set_head_detached(git_id(&commit1)).unwrap();
611
612 // Import HEAD and main.
613 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
614 tx.mut_repo().rebase_descendants(&settings).unwrap();
615 assert!(tx.mut_repo().view().heads().contains(commit1.id()));
616 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
617
618 // Move only HEAD to commit2 (by e.g. `git checkout` command)
619 git_repo.set_head_detached(git_id(&commit2)).unwrap();
620
621 // Reimport HEAD, which shouldn't abandon the old HEAD branch.
622 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
623 tx.mut_repo().rebase_descendants(&settings).unwrap();
624 assert!(tx.mut_repo().view().heads().contains(commit1.id()));
625 assert!(tx.mut_repo().view().heads().contains(commit2.id()));
626}
627
628#[test]
629fn test_import_refs_reimport_all_from_root_removed() {
630 // Test that if a chain of commits all the way from the root gets unreferenced,
631 // we abandon the whole stack, but not including the root commit.
632 let settings = testutils::user_settings();
633 let git_settings = GitSettings::default();
634 let test_repo = TestRepo::init(true);
635 let repo = &test_repo.repo;
636 let git_repo = get_git_repo(repo);
637
638 let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
639 let mut tx = repo.start_transaction(&settings, "test");
640 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
641 tx.mut_repo().rebase_descendants(&settings).unwrap();
642 // Test the setup
643 assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit)));
644
645 // Remove all git refs and re-import
646 git_repo
647 .find_reference("refs/heads/main")
648 .unwrap()
649 .delete()
650 .unwrap();
651 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
652 tx.mut_repo().rebase_descendants(&settings).unwrap();
653 assert!(!tx.mut_repo().view().heads().contains(&jj_id(&commit)));
654}
655
656#[test]
657fn test_import_some_refs() {
658 let settings = testutils::user_settings();
659 let git_settings = GitSettings::default();
660 let test_workspace = TestRepo::init(true);
661 let repo = &test_workspace.repo;
662 let git_repo = get_git_repo(repo);
663
664 let commit_main = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
665 let commit_feat1 = empty_git_commit(&git_repo, "refs/remotes/origin/feature1", &[&commit_main]);
666 let commit_feat2 =
667 empty_git_commit(&git_repo, "refs/remotes/origin/feature2", &[&commit_feat1]);
668 let commit_feat3 =
669 empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[&commit_feat1]);
670 let commit_feat4 =
671 empty_git_commit(&git_repo, "refs/remotes/origin/feature4", &[&commit_feat3]);
672 let commit_ign = empty_git_commit(&git_repo, "refs/remotes/origin/ignored", &[]);
673
674 fn get_remote_branch(ref_name: &RefName) -> Option<&str> {
675 match ref_name {
676 RefName::RemoteBranch { branch, remote } if remote == "origin" => Some(branch),
677 _ => None,
678 }
679 }
680
681 // Import branches feature1, feature2, and feature3.
682 let mut tx = repo.start_transaction(&settings, "test");
683 git::import_some_refs(tx.mut_repo(), &git_repo, &git_settings, |ref_name| {
684 get_remote_branch(ref_name)
685 .map(|branch| branch.starts_with("feature"))
686 .unwrap_or(false)
687 })
688 .unwrap();
689 tx.mut_repo().rebase_descendants(&settings).unwrap();
690 let repo = tx.commit();
691
692 // There are two heads, feature2 and feature4.
693 let view = repo.view();
694 let expected_heads = hashset! {
695 jj_id(&commit_feat2),
696 jj_id(&commit_feat4),
697 };
698 assert_eq!(*view.heads(), expected_heads);
699
700 // Check that branches feature[1-4] have been locally imported and are known to
701 // be present on origin as well.
702 assert_eq!(view.branches().len(), 4);
703 let commit_feat1_target = RefTarget::normal(jj_id(&commit_feat1));
704 let commit_feat2_target = RefTarget::normal(jj_id(&commit_feat2));
705 let commit_feat3_target = RefTarget::normal(jj_id(&commit_feat3));
706 let commit_feat4_target = RefTarget::normal(jj_id(&commit_feat4));
707 let expected_feature1_branch = BranchTarget {
708 local_target: RefTarget::normal(jj_id(&commit_feat1)),
709 remote_targets: btreemap! {
710 "origin".to_string() => commit_feat1_target,
711 },
712 };
713 assert_eq!(
714 view.get_branch("feature1"),
715 Some(expected_feature1_branch).as_ref()
716 );
717 let expected_feature2_branch = BranchTarget {
718 local_target: RefTarget::normal(jj_id(&commit_feat2)),
719 remote_targets: btreemap! {
720 "origin".to_string() => commit_feat2_target,
721 },
722 };
723 assert_eq!(
724 view.get_branch("feature2"),
725 Some(expected_feature2_branch).as_ref()
726 );
727 let expected_feature3_branch = BranchTarget {
728 local_target: RefTarget::normal(jj_id(&commit_feat3)),
729 remote_targets: btreemap! {
730 "origin".to_string() => commit_feat3_target,
731 },
732 };
733 assert_eq!(
734 view.get_branch("feature3"),
735 Some(expected_feature3_branch).as_ref()
736 );
737 let expected_feature4_branch = BranchTarget {
738 local_target: RefTarget::normal(jj_id(&commit_feat4)),
739 remote_targets: btreemap! {
740 "origin".to_string() => commit_feat4_target,
741 },
742 };
743 assert_eq!(
744 view.get_branch("feature4"),
745 Some(expected_feature4_branch).as_ref()
746 );
747 assert_eq!(view.get_branch("main"), None,);
748 assert!(!view.heads().contains(&jj_id(&commit_main)));
749 assert_eq!(view.get_branch("ignored"), None,);
750 assert!(!view.heads().contains(&jj_id(&commit_ign)));
751
752 // Delete branch feature1, feature3 and feature4 in git repository and import
753 // branch feature2 only. That should have no impact on the jj repository.
754 delete_git_ref(&git_repo, "refs/remotes/origin/feature1");
755 delete_git_ref(&git_repo, "refs/remotes/origin/feature3");
756 delete_git_ref(&git_repo, "refs/remotes/origin/feature4");
757 let mut tx = repo.start_transaction(&settings, "test");
758 git::import_some_refs(tx.mut_repo(), &git_repo, &git_settings, |ref_name| {
759 get_remote_branch(ref_name) == Some("feature2")
760 })
761 .unwrap();
762 tx.mut_repo().rebase_descendants(&settings).unwrap();
763 let repo = tx.commit();
764
765 // feature2 and feature4 will still be heads, and all four branches should be
766 // present.
767 let view = repo.view();
768 assert_eq!(view.branches().len(), 4);
769 assert_eq!(*view.heads(), expected_heads);
770
771 // Import feature1: this should cause the branch to be deleted, but the
772 // corresponding commit should stay because it is reachable from feature2.
773 let mut tx = repo.start_transaction(&settings, "test");
774 git::import_some_refs(tx.mut_repo(), &git_repo, &git_settings, |ref_name| {
775 get_remote_branch(ref_name) == Some("feature1")
776 })
777 .unwrap();
778 // No descendant should be rewritten.
779 assert_eq!(tx.mut_repo().rebase_descendants(&settings).unwrap(), 0);
780 let repo = tx.commit();
781
782 // feature2 and feature4 should still be the heads, and all three branches
783 // feature2, feature3, and feature3 should exist.
784 let view = repo.view();
785 assert_eq!(view.branches().len(), 3);
786 assert_eq!(*view.heads(), expected_heads);
787
788 // Import feature3: this should cause the branch to be deleted, but
789 // feature4 should be left alone even though it is no longer in git.
790 let mut tx = repo.start_transaction(&settings, "test");
791 git::import_some_refs(tx.mut_repo(), &git_repo, &git_settings, |ref_name| {
792 get_remote_branch(ref_name) == Some("feature3")
793 })
794 .unwrap();
795 // No descendant should be rewritten
796 assert_eq!(tx.mut_repo().rebase_descendants(&settings).unwrap(), 0);
797 let repo = tx.commit();
798
799 // feature2 and feature4 should still be the heads, and both branches
800 // should exist.
801 let view = repo.view();
802 assert_eq!(view.branches().len(), 2);
803 assert_eq!(*view.heads(), expected_heads);
804
805 // Import feature4: both the head and the branch will disappear.
806 let mut tx = repo.start_transaction(&settings, "test");
807 git::import_some_refs(tx.mut_repo(), &git_repo, &git_settings, |ref_name| {
808 get_remote_branch(ref_name) == Some("feature4")
809 })
810 .unwrap();
811 // No descendant should be rewritten
812 assert_eq!(tx.mut_repo().rebase_descendants(&settings).unwrap(), 0);
813 let repo = tx.commit();
814
815 // feature2 should now be the only head and only branch.
816 let view = repo.view();
817 assert_eq!(view.branches().len(), 1);
818 let expected_heads = hashset! {
819 jj_id(&commit_feat2),
820 };
821 assert_eq!(*view.heads(), expected_heads);
822}
823
824fn git_ref(git_repo: &git2::Repository, name: &str, target: Oid) {
825 git_repo.reference(name, target, true, "").unwrap();
826}
827
828fn delete_git_ref(git_repo: &git2::Repository, name: &str) {
829 git_repo.find_reference(name).unwrap().delete().unwrap();
830}
831
832struct GitRepoData {
833 settings: UserSettings,
834 _temp_dir: TempDir,
835 origin_repo: git2::Repository,
836 git_repo: git2::Repository,
837 repo: Arc<ReadonlyRepo>,
838}
839
840impl GitRepoData {
841 fn create() -> Self {
842 let settings = testutils::user_settings();
843 let temp_dir = testutils::new_temp_dir();
844 let origin_repo_dir = temp_dir.path().join("source");
845 let origin_repo = git2::Repository::init_bare(&origin_repo_dir).unwrap();
846 let git_repo_dir = temp_dir.path().join("git");
847 let git_repo =
848 git2::Repository::clone(origin_repo_dir.to_str().unwrap(), &git_repo_dir).unwrap();
849 let jj_repo_dir = temp_dir.path().join("jj");
850 std::fs::create_dir(&jj_repo_dir).unwrap();
851 let repo = ReadonlyRepo::init(
852 &settings,
853 &jj_repo_dir,
854 |store_path| {
855 Ok(Box::new(GitBackend::init_external(
856 store_path,
857 &git_repo_dir,
858 )?))
859 },
860 ReadonlyRepo::default_op_store_factory(),
861 ReadonlyRepo::default_op_heads_store_factory(),
862 ReadonlyRepo::default_index_store_factory(),
863 ReadonlyRepo::default_submodule_store_factory(),
864 )
865 .unwrap();
866 Self {
867 settings,
868 _temp_dir: temp_dir,
869 origin_repo,
870 git_repo,
871 repo,
872 }
873 }
874}
875
876#[test]
877fn test_import_refs_empty_git_repo() {
878 let test_data = GitRepoData::create();
879 let git_settings = GitSettings::default();
880 let heads_before = test_data.repo.view().heads().clone();
881 let mut tx = test_data
882 .repo
883 .start_transaction(&test_data.settings, "test");
884 git::import_refs(tx.mut_repo(), &test_data.git_repo, &git_settings).unwrap();
885 tx.mut_repo()
886 .rebase_descendants(&test_data.settings)
887 .unwrap();
888 let repo = tx.commit();
889 assert_eq!(*repo.view().heads(), heads_before);
890 assert_eq!(repo.view().branches().len(), 0);
891 assert_eq!(repo.view().tags().len(), 0);
892 assert_eq!(repo.view().git_refs().len(), 0);
893 assert_eq!(repo.view().git_head(), RefTarget::absent_ref());
894}
895
896#[test]
897fn test_import_refs_detached_head() {
898 let test_data = GitRepoData::create();
899 let git_settings = GitSettings::default();
900 let commit1 = empty_git_commit(&test_data.git_repo, "refs/heads/main", &[]);
901 // Delete the reference. Check that the detached HEAD commit still gets added to
902 // the set of heads
903 test_data
904 .git_repo
905 .find_reference("refs/heads/main")
906 .unwrap()
907 .delete()
908 .unwrap();
909 test_data.git_repo.set_head_detached(commit1.id()).unwrap();
910
911 let mut tx = test_data
912 .repo
913 .start_transaction(&test_data.settings, "test");
914 git::import_refs(tx.mut_repo(), &test_data.git_repo, &git_settings).unwrap();
915 tx.mut_repo()
916 .rebase_descendants(&test_data.settings)
917 .unwrap();
918 let repo = tx.commit();
919
920 let expected_heads = hashset! { jj_id(&commit1) };
921 assert_eq!(*repo.view().heads(), expected_heads);
922 assert_eq!(repo.view().git_refs().len(), 0);
923 assert_eq!(repo.view().git_head(), &RefTarget::normal(jj_id(&commit1)));
924}
925
926#[test]
927fn test_export_refs_no_detach() {
928 // When exporting the branch that's current checked out, don't detach HEAD if
929 // the target already matches
930 let test_data = GitRepoData::create();
931 let git_settings = GitSettings::default();
932 let git_repo = test_data.git_repo;
933 let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
934 git_repo.set_head("refs/heads/main").unwrap();
935 let mut tx = test_data
936 .repo
937 .start_transaction(&test_data.settings, "test");
938 let mut_repo = tx.mut_repo();
939 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
940 mut_repo.rebase_descendants(&test_data.settings).unwrap();
941
942 // Do an initial export to make sure `main` is considered
943 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
944 assert_eq!(
945 mut_repo.get_git_ref("refs/heads/main"),
946 RefTarget::normal(jj_id(&commit1))
947 );
948 assert_eq!(git_repo.head().unwrap().name(), Some("refs/heads/main"));
949 assert_eq!(
950 git_repo.find_reference("refs/heads/main").unwrap().target(),
951 Some(commit1.id())
952 );
953}
954
955#[test]
956fn test_export_refs_branch_changed() {
957 // We can export a change to a branch
958 let test_data = GitRepoData::create();
959 let git_settings = GitSettings::default();
960 let git_repo = test_data.git_repo;
961 let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
962 git_repo
963 .reference("refs/heads/feature", commit.id(), false, "test")
964 .unwrap();
965 git_repo.set_head("refs/heads/feature").unwrap();
966
967 let mut tx = test_data
968 .repo
969 .start_transaction(&test_data.settings, "test");
970 let mut_repo = tx.mut_repo();
971 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
972 mut_repo.rebase_descendants(&test_data.settings).unwrap();
973 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
974
975 let new_commit = create_random_commit(mut_repo, &test_data.settings)
976 .set_parents(vec![jj_id(&commit)])
977 .write()
978 .unwrap();
979 mut_repo.set_local_branch_target("main", RefTarget::normal(new_commit.id().clone()));
980 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
981 assert_eq!(
982 mut_repo.get_git_ref("refs/heads/main"),
983 RefTarget::normal(new_commit.id().clone())
984 );
985 assert_eq!(
986 git_repo
987 .find_reference("refs/heads/main")
988 .unwrap()
989 .peel_to_commit()
990 .unwrap()
991 .id(),
992 git_id(&new_commit)
993 );
994 // HEAD should be unchanged since its target branch didn't change
995 assert_eq!(git_repo.head().unwrap().name(), Some("refs/heads/feature"));
996}
997
998#[test]
999fn test_export_refs_current_branch_changed() {
1000 // If we update a branch that is checked out in the git repo, HEAD gets detached
1001 let test_data = GitRepoData::create();
1002 let git_settings = GitSettings::default();
1003 let git_repo = test_data.git_repo;
1004 let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
1005 git_repo.set_head("refs/heads/main").unwrap();
1006 let mut tx = test_data
1007 .repo
1008 .start_transaction(&test_data.settings, "test");
1009 let mut_repo = tx.mut_repo();
1010 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
1011 mut_repo.rebase_descendants(&test_data.settings).unwrap();
1012 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1013
1014 let new_commit = create_random_commit(mut_repo, &test_data.settings)
1015 .set_parents(vec![jj_id(&commit1)])
1016 .write()
1017 .unwrap();
1018 mut_repo.set_local_branch_target("main", RefTarget::normal(new_commit.id().clone()));
1019 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1020 assert_eq!(
1021 mut_repo.get_git_ref("refs/heads/main"),
1022 RefTarget::normal(new_commit.id().clone())
1023 );
1024 assert_eq!(
1025 git_repo
1026 .find_reference("refs/heads/main")
1027 .unwrap()
1028 .peel_to_commit()
1029 .unwrap()
1030 .id(),
1031 git_id(&new_commit)
1032 );
1033 assert!(git_repo.head_detached().unwrap());
1034}
1035
1036#[test]
1037fn test_export_refs_unborn_git_branch() {
1038 // Can export to an empty Git repo (we can handle Git's "unborn branch" state)
1039 let test_data = GitRepoData::create();
1040 let git_settings = GitSettings::default();
1041 let git_repo = test_data.git_repo;
1042 git_repo.set_head("refs/heads/main").unwrap();
1043 let mut tx = test_data
1044 .repo
1045 .start_transaction(&test_data.settings, "test");
1046 let mut_repo = tx.mut_repo();
1047 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
1048 mut_repo.rebase_descendants(&test_data.settings).unwrap();
1049 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1050
1051 let new_commit = write_random_commit(mut_repo, &test_data.settings);
1052 mut_repo.set_local_branch_target("main", RefTarget::normal(new_commit.id().clone()));
1053 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1054 assert_eq!(
1055 mut_repo.get_git_ref("refs/heads/main"),
1056 RefTarget::normal(new_commit.id().clone())
1057 );
1058 assert_eq!(
1059 git_repo
1060 .find_reference("refs/heads/main")
1061 .unwrap()
1062 .peel_to_commit()
1063 .unwrap()
1064 .id(),
1065 git_id(&new_commit)
1066 );
1067 // It's weird that the head is still pointing to refs/heads/main, but
1068 // it doesn't seem that Git lets you be on an "unborn branch" while
1069 // also being in a "detached HEAD" state.
1070 assert!(!git_repo.head_detached().unwrap());
1071}
1072
1073#[test]
1074fn test_export_import_sequence() {
1075 // Import a branch pointing to A, modify it in jj to point to B, export it,
1076 // modify it in git to point to C, then import it again. There should be no
1077 // conflict.
1078 let test_data = GitRepoData::create();
1079 let git_settings = GitSettings::default();
1080 let git_repo = test_data.git_repo;
1081 let mut tx = test_data
1082 .repo
1083 .start_transaction(&test_data.settings, "test");
1084 let mut_repo = tx.mut_repo();
1085 let commit_a = write_random_commit(mut_repo, &test_data.settings);
1086 let commit_b = write_random_commit(mut_repo, &test_data.settings);
1087 let commit_c = write_random_commit(mut_repo, &test_data.settings);
1088
1089 // Import the branch pointing to A
1090 git_repo
1091 .reference("refs/heads/main", git_id(&commit_a), true, "test")
1092 .unwrap();
1093 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
1094 assert_eq!(
1095 mut_repo.get_git_ref("refs/heads/main"),
1096 RefTarget::normal(commit_a.id().clone())
1097 );
1098
1099 // Modify the branch in jj to point to B
1100 mut_repo.set_local_branch_target("main", RefTarget::normal(commit_b.id().clone()));
1101
1102 // Export the branch to git
1103 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1104 assert_eq!(
1105 mut_repo.get_git_ref("refs/heads/main"),
1106 RefTarget::normal(commit_b.id().clone())
1107 );
1108
1109 // Modify the branch in git to point to C
1110 git_repo
1111 .reference("refs/heads/main", git_id(&commit_c), true, "test")
1112 .unwrap();
1113
1114 // Import from git
1115 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
1116 assert_eq!(
1117 mut_repo.get_git_ref("refs/heads/main"),
1118 RefTarget::normal(commit_c.id().clone())
1119 );
1120 assert_eq!(
1121 mut_repo.view().get_local_branch("main"),
1122 &RefTarget::normal(commit_c.id().clone())
1123 );
1124}
1125
1126#[test]
1127fn test_import_export_no_auto_local_branch() {
1128 // Import a remote tracking branch and export it. We should not create a git
1129 // branch.
1130 let test_data = GitRepoData::create();
1131 let git_settings = GitSettings {
1132 auto_local_branch: false,
1133 };
1134 let git_repo = test_data.git_repo;
1135 let git_commit = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
1136
1137 let mut tx = test_data
1138 .repo
1139 .start_transaction(&test_data.settings, "test");
1140 let mut_repo = tx.mut_repo();
1141
1142 git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
1143
1144 let expected_branch = BranchTarget {
1145 local_target: RefTarget::absent(),
1146 remote_targets: btreemap! {
1147 "origin".to_string() => RefTarget::normal(jj_id(&git_commit)),
1148 },
1149 };
1150 assert_eq!(
1151 mut_repo.view().get_branch("main"),
1152 Some(expected_branch).as_ref()
1153 );
1154 assert_eq!(
1155 mut_repo.get_git_ref("refs/remotes/origin/main"),
1156 RefTarget::normal(jj_id(&git_commit))
1157 );
1158
1159 // Export the branch to git
1160 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1161 assert_eq!(mut_repo.get_git_ref("refs/heads/main"), RefTarget::absent());
1162}
1163
1164#[test]
1165fn test_export_conflicts() {
1166 // We skip export of conflicted branches
1167 let test_data = GitRepoData::create();
1168 let git_repo = test_data.git_repo;
1169 let mut tx = test_data
1170 .repo
1171 .start_transaction(&test_data.settings, "test");
1172 let mut_repo = tx.mut_repo();
1173 let commit_a = write_random_commit(mut_repo, &test_data.settings);
1174 let commit_b = write_random_commit(mut_repo, &test_data.settings);
1175 let commit_c = write_random_commit(mut_repo, &test_data.settings);
1176 mut_repo.set_local_branch_target("main", RefTarget::normal(commit_a.id().clone()));
1177 mut_repo.set_local_branch_target("feature", RefTarget::normal(commit_a.id().clone()));
1178 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1179
1180 // Create a conflict and export. It should not be exported, but other changes
1181 // should be.
1182 mut_repo.set_local_branch_target("main", RefTarget::normal(commit_b.id().clone()));
1183 mut_repo.set_local_branch_target(
1184 "feature",
1185 RefTarget::from_legacy_form(
1186 [commit_a.id().clone()],
1187 [commit_b.id().clone(), commit_c.id().clone()],
1188 ),
1189 );
1190 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1191 assert_eq!(
1192 git_repo
1193 .find_reference("refs/heads/feature")
1194 .unwrap()
1195 .target()
1196 .unwrap(),
1197 git_id(&commit_a)
1198 );
1199 assert_eq!(
1200 git_repo
1201 .find_reference("refs/heads/main")
1202 .unwrap()
1203 .target()
1204 .unwrap(),
1205 git_id(&commit_b)
1206 );
1207}
1208
1209#[test]
1210fn test_export_partial_failure() {
1211 // Check that we skip branches that fail to export
1212 let test_data = GitRepoData::create();
1213 let git_repo = test_data.git_repo;
1214 let mut tx = test_data
1215 .repo
1216 .start_transaction(&test_data.settings, "test");
1217 let mut_repo = tx.mut_repo();
1218 let commit_a = write_random_commit(mut_repo, &test_data.settings);
1219 let target = RefTarget::normal(commit_a.id().clone());
1220 // Empty string is disallowed by Git
1221 mut_repo.set_local_branch_target("", target.clone());
1222 // Branch named HEAD is disallowed by Git CLI
1223 mut_repo.set_local_branch_target("HEAD", target.clone());
1224 mut_repo.set_local_branch_target("main", target.clone());
1225 // `main/sub` will conflict with `main` in Git, at least when using loose ref
1226 // storage
1227 mut_repo.set_local_branch_target("main/sub", target);
1228 assert_eq!(
1229 git::export_refs(mut_repo, &git_repo),
1230 Ok(vec![
1231 RefName::LocalBranch("HEAD".to_string()),
1232 RefName::LocalBranch("".to_string()),
1233 RefName::LocalBranch("main/sub".to_string())
1234 ])
1235 );
1236
1237 // The `main` branch should have succeeded but the other should have failed
1238 assert!(git_repo.find_reference("refs/heads/").is_err());
1239 assert!(git_repo.find_reference("refs/heads/HEAD").is_err());
1240 assert_eq!(
1241 git_repo
1242 .find_reference("refs/heads/main")
1243 .unwrap()
1244 .target()
1245 .unwrap(),
1246 git_id(&commit_a)
1247 );
1248 assert!(git_repo.find_reference("refs/heads/main/sub").is_err());
1249
1250 // Now remove the `main` branch and make sure that the `main/sub` gets exported
1251 // even though it didn't change
1252 mut_repo.set_local_branch_target("main", RefTarget::absent());
1253 assert_eq!(
1254 git::export_refs(mut_repo, &git_repo),
1255 Ok(vec![
1256 RefName::LocalBranch("HEAD".to_string()),
1257 RefName::LocalBranch("".to_string())
1258 ])
1259 );
1260 assert!(git_repo.find_reference("refs/heads/").is_err());
1261 assert!(git_repo.find_reference("refs/heads/HEAD").is_err());
1262 assert!(git_repo.find_reference("refs/heads/main").is_err());
1263 assert_eq!(
1264 git_repo
1265 .find_reference("refs/heads/main/sub")
1266 .unwrap()
1267 .target()
1268 .unwrap(),
1269 git_id(&commit_a)
1270 );
1271}
1272
1273#[test]
1274fn test_export_reexport_transitions() {
1275 // Test exporting after making changes on the jj side, or the git side, or both
1276 let test_data = GitRepoData::create();
1277 let git_repo = test_data.git_repo;
1278 let mut tx = test_data
1279 .repo
1280 .start_transaction(&test_data.settings, "test");
1281 let mut_repo = tx.mut_repo();
1282 let commit_a = write_random_commit(mut_repo, &test_data.settings);
1283 let commit_b = write_random_commit(mut_repo, &test_data.settings);
1284 let commit_c = write_random_commit(mut_repo, &test_data.settings);
1285 // Create a few branches whose names indicate how they change in jj in git. The
1286 // first letter represents the branch's target in the last export. The second
1287 // letter represents the branch's target in jj. The third letter represents the
1288 // branch's target in git. "X" means that the branch doesn't exist. "A", "B", or
1289 // "C" means that the branch points to that commit.
1290 //
1291 // AAB: Branch modified in git
1292 // AAX: Branch deleted in git
1293 // ABA: Branch modified in jj
1294 // ABB: Branch modified in both jj and git, pointing to same target
1295 // ABC: Branch modified in both jj and git, pointing to different targets
1296 // ABX: Branch modified in jj, deleted in git
1297 // AXA: Branch deleted in jj
1298 // AXB: Branch deleted in jj, modified in git
1299 // AXX: Branch deleted in both jj and git
1300 // XAA: Branch added in both jj and git, pointing to same target
1301 // XAB: Branch added in both jj and git, pointing to different targets
1302 // XAX: Branch added in jj
1303 // XXA: Branch added in git
1304
1305 // Create initial state and export it
1306 for branch in [
1307 "AAB", "AAX", "ABA", "ABB", "ABC", "ABX", "AXA", "AXB", "AXX",
1308 ] {
1309 mut_repo.set_local_branch_target(branch, RefTarget::normal(commit_a.id().clone()));
1310 }
1311 assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
1312
1313 // Make changes on the jj side
1314 for branch in ["AXA", "AXB", "AXX"] {
1315 mut_repo.set_local_branch_target(branch, RefTarget::absent());
1316 }
1317 for branch in ["XAA", "XAB", "XAX"] {
1318 mut_repo.set_local_branch_target(branch, RefTarget::normal(commit_a.id().clone()));
1319 }
1320 for branch in ["ABA", "ABB", "ABC", "ABX"] {
1321 mut_repo.set_local_branch_target(branch, RefTarget::normal(commit_b.id().clone()));
1322 }
1323
1324 // Make changes on the git side
1325 for branch in ["AAX", "ABX", "AXX"] {
1326 git_repo
1327 .find_reference(&format!("refs/heads/{branch}"))
1328 .unwrap()
1329 .delete()
1330 .unwrap();
1331 }
1332 for branch in ["XAA", "XXA"] {
1333 git_repo
1334 .reference(&format!("refs/heads/{branch}"), git_id(&commit_a), true, "")
1335 .unwrap();
1336 }
1337 for branch in ["AAB", "ABB", "AXB", "XAB"] {
1338 git_repo
1339 .reference(&format!("refs/heads/{branch}"), git_id(&commit_b), true, "")
1340 .unwrap();
1341 }
1342 let branch = "ABC";
1343 git_repo
1344 .reference(&format!("refs/heads/{branch}"), git_id(&commit_c), true, "")
1345 .unwrap();
1346
1347 // TODO: The branches that we made conflicting changes to should have failed to
1348 // export. They should have been unchanged in git and in
1349 // mut_repo.view().git_refs().
1350 assert_eq!(
1351 git::export_refs(mut_repo, &git_repo),
1352 Ok(["AXB", "ABC", "ABX", "XAB"]
1353 .into_iter()
1354 .map(|s| RefName::LocalBranch(s.to_string()))
1355 .collect_vec())
1356 );
1357 for branch in ["AAX", "ABX", "AXA", "AXX"] {
1358 assert!(
1359 git_repo
1360 .find_reference(&format!("refs/heads/{branch}"))
1361 .is_err(),
1362 "{branch} should not exist"
1363 );
1364 }
1365 for branch in ["XAA", "XAX", "XXA"] {
1366 assert_eq!(
1367 git_repo
1368 .find_reference(&format!("refs/heads/{branch}"))
1369 .unwrap()
1370 .target(),
1371 Some(git_id(&commit_a)),
1372 "{branch} should point to commit A"
1373 );
1374 }
1375 for branch in ["AAB", "ABA", "AAB", "ABB", "AXB", "XAB"] {
1376 assert_eq!(
1377 git_repo
1378 .find_reference(&format!("refs/heads/{branch}"))
1379 .unwrap()
1380 .target(),
1381 Some(git_id(&commit_b)),
1382 "{branch} should point to commit B"
1383 );
1384 }
1385 let branch = "ABC";
1386 assert_eq!(
1387 git_repo
1388 .find_reference(&format!("refs/heads/{branch}"))
1389 .unwrap()
1390 .target(),
1391 Some(git_id(&commit_c)),
1392 "{branch} should point to commit C"
1393 );
1394 assert_eq!(
1395 *mut_repo.view().git_refs(),
1396 btreemap! {
1397 "refs/heads/AAX".to_string() => RefTarget::normal(commit_a.id().clone()),
1398 "refs/heads/AAB".to_string() => RefTarget::normal(commit_a.id().clone()),
1399 "refs/heads/ABA".to_string() => RefTarget::normal(commit_b.id().clone()),
1400 "refs/heads/ABB".to_string() => RefTarget::normal(commit_b.id().clone()),
1401 "refs/heads/ABC".to_string() => RefTarget::normal(commit_a.id().clone()),
1402 "refs/heads/ABX".to_string() => RefTarget::normal(commit_a.id().clone()),
1403 "refs/heads/AXB".to_string() => RefTarget::normal(commit_a.id().clone()),
1404 "refs/heads/XAA".to_string() => RefTarget::normal(commit_a.id().clone()),
1405 "refs/heads/XAX".to_string() => RefTarget::normal(commit_a.id().clone()),
1406 }
1407 );
1408}
1409
1410#[test]
1411fn test_init() {
1412 let settings = testutils::user_settings();
1413 let temp_dir = testutils::new_temp_dir();
1414 let git_repo_dir = temp_dir.path().join("git");
1415 let jj_repo_dir = temp_dir.path().join("jj");
1416 let git_repo = git2::Repository::init_bare(&git_repo_dir).unwrap();
1417 let initial_git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
1418 std::fs::create_dir(&jj_repo_dir).unwrap();
1419 let repo = ReadonlyRepo::init(
1420 &settings,
1421 &jj_repo_dir,
1422 |store_path| {
1423 Ok(Box::new(GitBackend::init_external(
1424 store_path,
1425 &git_repo_dir,
1426 )?))
1427 },
1428 ReadonlyRepo::default_op_store_factory(),
1429 ReadonlyRepo::default_op_heads_store_factory(),
1430 ReadonlyRepo::default_index_store_factory(),
1431 ReadonlyRepo::default_submodule_store_factory(),
1432 )
1433 .unwrap();
1434 // The refs were *not* imported -- it's the caller's responsibility to import
1435 // any refs they care about.
1436 assert!(!repo.view().heads().contains(&jj_id(&initial_git_commit)));
1437}
1438
1439#[test]
1440fn test_fetch_empty_repo() {
1441 let test_data = GitRepoData::create();
1442 let git_settings = GitSettings::default();
1443
1444 let mut tx = test_data
1445 .repo
1446 .start_transaction(&test_data.settings, "test");
1447 let default_branch = git::fetch(
1448 tx.mut_repo(),
1449 &test_data.git_repo,
1450 "origin",
1451 None,
1452 git::RemoteCallbacks::default(),
1453 &git_settings,
1454 )
1455 .unwrap();
1456 // No default branch and no refs
1457 assert_eq!(default_branch, None);
1458 assert_eq!(*tx.mut_repo().view().git_refs(), btreemap! {});
1459 assert_eq!(*tx.mut_repo().view().branches(), btreemap! {});
1460}
1461
1462#[test]
1463fn test_fetch_initial_commit() {
1464 let test_data = GitRepoData::create();
1465 let git_settings = GitSettings::default();
1466 let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
1467
1468 let mut tx = test_data
1469 .repo
1470 .start_transaction(&test_data.settings, "test");
1471 let default_branch = git::fetch(
1472 tx.mut_repo(),
1473 &test_data.git_repo,
1474 "origin",
1475 None,
1476 git::RemoteCallbacks::default(),
1477 &git_settings,
1478 )
1479 .unwrap();
1480 // No default branch because the origin repo's HEAD wasn't set
1481 assert_eq!(default_branch, None);
1482 let repo = tx.commit();
1483 // The initial commit is visible after git::fetch().
1484 let view = repo.view();
1485 assert!(view.heads().contains(&jj_id(&initial_git_commit)));
1486 let initial_commit_target = RefTarget::normal(jj_id(&initial_git_commit));
1487 assert_eq!(
1488 *view.git_refs(),
1489 btreemap! {
1490 "refs/remotes/origin/main".to_string() => initial_commit_target.clone(),
1491 }
1492 );
1493 assert_eq!(
1494 *view.branches(),
1495 btreemap! {
1496 "main".to_string() => BranchTarget {
1497 local_target: initial_commit_target.clone(),
1498 remote_targets: btreemap! {
1499 "origin".to_string() => initial_commit_target,
1500 },
1501 },
1502 }
1503 );
1504}
1505
1506#[test]
1507fn test_fetch_success() {
1508 let mut test_data = GitRepoData::create();
1509 let git_settings = GitSettings::default();
1510 let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
1511
1512 let mut tx = test_data
1513 .repo
1514 .start_transaction(&test_data.settings, "test");
1515 git::fetch(
1516 tx.mut_repo(),
1517 &test_data.git_repo,
1518 "origin",
1519 None,
1520 git::RemoteCallbacks::default(),
1521 &git_settings,
1522 )
1523 .unwrap();
1524 test_data.repo = tx.commit();
1525
1526 test_data.origin_repo.set_head("refs/heads/main").unwrap();
1527 let new_git_commit = empty_git_commit(
1528 &test_data.origin_repo,
1529 "refs/heads/main",
1530 &[&initial_git_commit],
1531 );
1532
1533 let mut tx = test_data
1534 .repo
1535 .start_transaction(&test_data.settings, "test");
1536 let default_branch = git::fetch(
1537 tx.mut_repo(),
1538 &test_data.git_repo,
1539 "origin",
1540 None,
1541 git::RemoteCallbacks::default(),
1542 &git_settings,
1543 )
1544 .unwrap();
1545 // The default branch is "main"
1546 assert_eq!(default_branch, Some("main".to_string()));
1547 let repo = tx.commit();
1548 // The new commit is visible after we fetch again
1549 let view = repo.view();
1550 assert!(view.heads().contains(&jj_id(&new_git_commit)));
1551 let new_commit_target = RefTarget::normal(jj_id(&new_git_commit));
1552 assert_eq!(
1553 *view.git_refs(),
1554 btreemap! {
1555 "refs/remotes/origin/main".to_string() => new_commit_target.clone(),
1556 }
1557 );
1558 assert_eq!(
1559 *view.branches(),
1560 btreemap! {
1561 "main".to_string() => BranchTarget {
1562 local_target: new_commit_target.clone(),
1563 remote_targets: btreemap! {
1564 "origin".to_string() => new_commit_target,
1565 },
1566 },
1567 }
1568 );
1569}
1570
1571#[test]
1572fn test_fetch_prune_deleted_ref() {
1573 let test_data = GitRepoData::create();
1574 let git_settings = GitSettings::default();
1575 empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
1576
1577 let mut tx = test_data
1578 .repo
1579 .start_transaction(&test_data.settings, "test");
1580 git::fetch(
1581 tx.mut_repo(),
1582 &test_data.git_repo,
1583 "origin",
1584 None,
1585 git::RemoteCallbacks::default(),
1586 &git_settings,
1587 )
1588 .unwrap();
1589 // Test the setup
1590 assert!(tx.mut_repo().get_branch("main").is_some());
1591
1592 test_data
1593 .origin_repo
1594 .find_reference("refs/heads/main")
1595 .unwrap()
1596 .delete()
1597 .unwrap();
1598 // After re-fetching, the branch should be deleted
1599 git::fetch(
1600 tx.mut_repo(),
1601 &test_data.git_repo,
1602 "origin",
1603 None,
1604 git::RemoteCallbacks::default(),
1605 &git_settings,
1606 )
1607 .unwrap();
1608 assert!(tx.mut_repo().get_branch("main").is_none());
1609}
1610
1611#[test]
1612fn test_fetch_no_default_branch() {
1613 let test_data = GitRepoData::create();
1614 let git_settings = GitSettings::default();
1615 let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
1616
1617 let mut tx = test_data
1618 .repo
1619 .start_transaction(&test_data.settings, "test");
1620 git::fetch(
1621 tx.mut_repo(),
1622 &test_data.git_repo,
1623 "origin",
1624 None,
1625 git::RemoteCallbacks::default(),
1626 &git_settings,
1627 )
1628 .unwrap();
1629
1630 empty_git_commit(
1631 &test_data.origin_repo,
1632 "refs/heads/main",
1633 &[&initial_git_commit],
1634 );
1635 // It's actually not enough to have a detached HEAD, it also needs to point to a
1636 // commit without a branch (that's possibly a bug in Git *and* libgit2), so
1637 // we point it to initial_git_commit.
1638 test_data
1639 .origin_repo
1640 .set_head_detached(initial_git_commit.id())
1641 .unwrap();
1642
1643 let default_branch = git::fetch(
1644 tx.mut_repo(),
1645 &test_data.git_repo,
1646 "origin",
1647 None,
1648 git::RemoteCallbacks::default(),
1649 &git_settings,
1650 )
1651 .unwrap();
1652 // There is no default branch
1653 assert_eq!(default_branch, None);
1654}
1655
1656#[test]
1657fn test_fetch_no_such_remote() {
1658 let test_data = GitRepoData::create();
1659 let git_settings = GitSettings::default();
1660 let mut tx = test_data
1661 .repo
1662 .start_transaction(&test_data.settings, "test");
1663 let result = git::fetch(
1664 tx.mut_repo(),
1665 &test_data.git_repo,
1666 "invalid-remote",
1667 None,
1668 git::RemoteCallbacks::default(),
1669 &git_settings,
1670 );
1671 assert!(matches!(result, Err(GitFetchError::NoSuchRemote(_))));
1672}
1673
1674struct PushTestSetup {
1675 source_repo_dir: PathBuf,
1676 jj_repo: Arc<ReadonlyRepo>,
1677 new_commit: Commit,
1678}
1679
1680fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSetup {
1681 let source_repo_dir = temp_dir.path().join("source");
1682 let clone_repo_dir = temp_dir.path().join("clone");
1683 let jj_repo_dir = temp_dir.path().join("jj");
1684 let source_repo = git2::Repository::init_bare(&source_repo_dir).unwrap();
1685 let initial_git_commit = empty_git_commit(&source_repo, "refs/heads/main", &[]);
1686 git2::Repository::clone(source_repo_dir.to_str().unwrap(), &clone_repo_dir).unwrap();
1687 std::fs::create_dir(&jj_repo_dir).unwrap();
1688 let jj_repo = ReadonlyRepo::init(
1689 settings,
1690 &jj_repo_dir,
1691 |store_path| {
1692 Ok(Box::new(GitBackend::init_external(
1693 store_path,
1694 &clone_repo_dir,
1695 )?))
1696 },
1697 ReadonlyRepo::default_op_store_factory(),
1698 ReadonlyRepo::default_op_heads_store_factory(),
1699 ReadonlyRepo::default_index_store_factory(),
1700 ReadonlyRepo::default_submodule_store_factory(),
1701 )
1702 .unwrap();
1703 let mut tx = jj_repo.start_transaction(settings, "test");
1704 let new_commit = create_random_commit(tx.mut_repo(), settings)
1705 .set_parents(vec![jj_id(&initial_git_commit)])
1706 .write()
1707 .unwrap();
1708 let jj_repo = tx.commit();
1709 PushTestSetup {
1710 source_repo_dir,
1711 jj_repo,
1712 new_commit,
1713 }
1714}
1715
1716#[test]
1717fn test_push_updates_success() {
1718 let settings = testutils::user_settings();
1719 let temp_dir = testutils::new_temp_dir();
1720 let setup = set_up_push_repos(&settings, &temp_dir);
1721 let clone_repo = get_git_repo(&setup.jj_repo);
1722 let result = git::push_updates(
1723 &clone_repo,
1724 "origin",
1725 &[GitRefUpdate {
1726 qualified_name: "refs/heads/main".to_string(),
1727 force: false,
1728 new_target: Some(setup.new_commit.id().clone()),
1729 }],
1730 git::RemoteCallbacks::default(),
1731 );
1732 assert_eq!(result, Ok(()));
1733
1734 // Check that the ref got updated in the source repo
1735 let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
1736 let new_target = source_repo
1737 .find_reference("refs/heads/main")
1738 .unwrap()
1739 .target();
1740 let new_oid = git_id(&setup.new_commit);
1741 assert_eq!(new_target, Some(new_oid));
1742
1743 // Check that the ref got updated in the cloned repo. This just tests our
1744 // assumptions about libgit2 because we want the refs/remotes/origin/main
1745 // branch to be updated.
1746 let new_target = clone_repo
1747 .find_reference("refs/remotes/origin/main")
1748 .unwrap()
1749 .target();
1750 assert_eq!(new_target, Some(new_oid));
1751}
1752
1753#[test]
1754fn test_push_updates_deletion() {
1755 let settings = testutils::user_settings();
1756 let temp_dir = testutils::new_temp_dir();
1757 let setup = set_up_push_repos(&settings, &temp_dir);
1758 let clone_repo = get_git_repo(&setup.jj_repo);
1759
1760 let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
1761 // Test the setup
1762 assert!(source_repo.find_reference("refs/heads/main").is_ok());
1763
1764 let result = git::push_updates(
1765 &get_git_repo(&setup.jj_repo),
1766 "origin",
1767 &[GitRefUpdate {
1768 qualified_name: "refs/heads/main".to_string(),
1769 force: false,
1770 new_target: None,
1771 }],
1772 git::RemoteCallbacks::default(),
1773 );
1774 assert_eq!(result, Ok(()));
1775
1776 // Check that the ref got deleted in the source repo
1777 assert!(source_repo.find_reference("refs/heads/main").is_err());
1778
1779 // Check that the ref got deleted in the cloned repo. This just tests our
1780 // assumptions about libgit2 because we want the refs/remotes/origin/main
1781 // branch to be deleted.
1782 assert!(clone_repo
1783 .find_reference("refs/remotes/origin/main")
1784 .is_err());
1785}
1786
1787#[test]
1788fn test_push_updates_mixed_deletion_and_addition() {
1789 let settings = testutils::user_settings();
1790 let temp_dir = testutils::new_temp_dir();
1791 let setup = set_up_push_repos(&settings, &temp_dir);
1792 let clone_repo = get_git_repo(&setup.jj_repo);
1793 let result = git::push_updates(
1794 &clone_repo,
1795 "origin",
1796 &[
1797 GitRefUpdate {
1798 qualified_name: "refs/heads/main".to_string(),
1799 force: false,
1800 new_target: None,
1801 },
1802 GitRefUpdate {
1803 qualified_name: "refs/heads/topic".to_string(),
1804 force: false,
1805 new_target: Some(setup.new_commit.id().clone()),
1806 },
1807 ],
1808 git::RemoteCallbacks::default(),
1809 );
1810 assert_eq!(result, Ok(()));
1811
1812 // Check that the topic ref got updated in the source repo
1813 let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
1814 let new_target = source_repo
1815 .find_reference("refs/heads/topic")
1816 .unwrap()
1817 .target();
1818 assert_eq!(new_target, Some(git_id(&setup.new_commit)));
1819
1820 // Check that the main ref got deleted in the source repo
1821 assert!(source_repo.find_reference("refs/heads/main").is_err());
1822}
1823
1824#[test]
1825fn test_push_updates_not_fast_forward() {
1826 let settings = testutils::user_settings();
1827 let temp_dir = testutils::new_temp_dir();
1828 let mut setup = set_up_push_repos(&settings, &temp_dir);
1829 let mut tx = setup.jj_repo.start_transaction(&settings, "test");
1830 let new_commit = write_random_commit(tx.mut_repo(), &settings);
1831 setup.jj_repo = tx.commit();
1832 let result = git::push_updates(
1833 &get_git_repo(&setup.jj_repo),
1834 "origin",
1835 &[GitRefUpdate {
1836 qualified_name: "refs/heads/main".to_string(),
1837 force: false,
1838 new_target: Some(new_commit.id().clone()),
1839 }],
1840 git::RemoteCallbacks::default(),
1841 );
1842 assert_eq!(result, Err(GitPushError::NotFastForward));
1843}
1844
1845#[test]
1846fn test_push_updates_not_fast_forward_with_force() {
1847 let settings = testutils::user_settings();
1848 let temp_dir = testutils::new_temp_dir();
1849 let mut setup = set_up_push_repos(&settings, &temp_dir);
1850 let mut tx = setup.jj_repo.start_transaction(&settings, "test");
1851 let new_commit = write_random_commit(tx.mut_repo(), &settings);
1852 setup.jj_repo = tx.commit();
1853 let result = git::push_updates(
1854 &get_git_repo(&setup.jj_repo),
1855 "origin",
1856 &[GitRefUpdate {
1857 qualified_name: "refs/heads/main".to_string(),
1858 force: true,
1859 new_target: Some(new_commit.id().clone()),
1860 }],
1861 git::RemoteCallbacks::default(),
1862 );
1863 assert_eq!(result, Ok(()));
1864
1865 // Check that the ref got updated in the source repo
1866 let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
1867 let new_target = source_repo
1868 .find_reference("refs/heads/main")
1869 .unwrap()
1870 .target();
1871 assert_eq!(new_target, Some(git_id(&new_commit)));
1872}
1873
1874#[test]
1875fn test_push_updates_no_such_remote() {
1876 let settings = testutils::user_settings();
1877 let temp_dir = testutils::new_temp_dir();
1878 let setup = set_up_push_repos(&settings, &temp_dir);
1879 let result = git::push_updates(
1880 &get_git_repo(&setup.jj_repo),
1881 "invalid-remote",
1882 &[GitRefUpdate {
1883 qualified_name: "refs/heads/main".to_string(),
1884 force: false,
1885 new_target: Some(setup.new_commit.id().clone()),
1886 }],
1887 git::RemoteCallbacks::default(),
1888 );
1889 assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
1890}
1891
1892#[test]
1893fn test_push_updates_invalid_remote() {
1894 let settings = testutils::user_settings();
1895 let temp_dir = testutils::new_temp_dir();
1896 let setup = set_up_push_repos(&settings, &temp_dir);
1897 let result = git::push_updates(
1898 &get_git_repo(&setup.jj_repo),
1899 "http://invalid-remote",
1900 &[GitRefUpdate {
1901 qualified_name: "refs/heads/main".to_string(),
1902 force: false,
1903 new_target: Some(setup.new_commit.id().clone()),
1904 }],
1905 git::RemoteCallbacks::default(),
1906 );
1907 assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
1908}
1909
1910#[test]
1911fn test_bulk_update_extra_on_import_refs() {
1912 let settings = testutils::user_settings();
1913 let git_settings = GitSettings::default();
1914 let test_repo = TestRepo::init(true);
1915 let repo = &test_repo.repo;
1916 let git_repo = get_git_repo(repo);
1917
1918 let count_extra_tables = || {
1919 let extra_dir = repo.repo_path().join("store").join("extra");
1920 extra_dir
1921 .read_dir()
1922 .unwrap()
1923 .filter(|entry| entry.as_ref().unwrap().metadata().unwrap().is_file())
1924 .count()
1925 };
1926 let import_refs = |repo: &Arc<ReadonlyRepo>| {
1927 let mut tx = repo.start_transaction(&settings, "test");
1928 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
1929 tx.mut_repo().rebase_descendants(&settings).unwrap();
1930 tx.commit()
1931 };
1932
1933 // Extra metadata table shouldn't be created per read_commit() call. The number
1934 // of the table files should be way smaller than the number of the heads.
1935 let mut commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
1936 for _ in 1..10 {
1937 commit = empty_git_commit(&git_repo, "refs/heads/main", &[&commit]);
1938 }
1939 let repo = import_refs(repo);
1940 assert_eq!(count_extra_tables(), 2); // empty + imported_heads == 2
1941
1942 // Noop import shouldn't create a table file.
1943 let repo = import_refs(&repo);
1944 assert_eq!(count_extra_tables(), 2);
1945
1946 // Importing new head should add exactly one table file.
1947 for _ in 0..10 {
1948 commit = empty_git_commit(&git_repo, "refs/heads/main", &[&commit]);
1949 }
1950 let repo = import_refs(&repo);
1951 assert_eq!(count_extra_tables(), 3);
1952
1953 drop(repo); // silence clippy
1954}
1955
1956#[test]
1957fn test_rewrite_imported_commit() {
1958 let settings = testutils::user_settings();
1959 let git_settings = GitSettings::default();
1960 let test_repo = TestRepo::init(true);
1961 let repo = &test_repo.repo;
1962 let git_repo = get_git_repo(repo);
1963
1964 // Import git commit, which generates change id from the commit id.
1965 let git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
1966 let mut tx = repo.start_transaction(&settings, "test");
1967 git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
1968 tx.mut_repo().rebase_descendants(&settings).unwrap();
1969 let repo = tx.commit();
1970 let imported_commit = repo.store().get_commit(&jj_id(&git_commit)).unwrap();
1971
1972 // Try to create identical commit with different change id.
1973 let mut tx = repo.start_transaction(&settings, "test");
1974 let authored_commit = tx
1975 .mut_repo()
1976 .new_commit(
1977 &settings,
1978 imported_commit.parent_ids().to_vec(),
1979 imported_commit.tree_id().clone(),
1980 )
1981 .set_author(imported_commit.author().clone())
1982 .set_committer(imported_commit.committer().clone())
1983 .set_description(imported_commit.description())
1984 .write()
1985 .unwrap();
1986 let repo = tx.commit();
1987
1988 // Imported commit shouldn't be reused, and the timestamp of the authored
1989 // commit should be adjusted to create new commit.
1990 assert_ne!(imported_commit.id(), authored_commit.id());
1991 assert_ne!(
1992 imported_commit.committer().timestamp,
1993 authored_commit.committer().timestamp,
1994 );
1995
1996 // The index should be consistent with the store.
1997 assert_eq!(
1998 repo.resolve_change_id(imported_commit.change_id()),
1999 Some(vec![imported_commit.id().clone()]),
2000 );
2001 assert_eq!(
2002 repo.resolve_change_id(authored_commit.change_id()),
2003 Some(vec![authored_commit.id().clone()]),
2004 );
2005}
2006
2007#[test]
2008fn test_concurrent_write_commit() {
2009 let settings = &testutils::user_settings();
2010 let test_repo = TestRepo::init(true);
2011 let repo = &test_repo.repo;
2012
2013 // Try to create identical commits with different change ids. Timestamp of the
2014 // commits should be adjusted such that each commit has a unique commit id.
2015 let num_thread = 8;
2016 let (sender, receiver) = mpsc::channel();
2017 thread::scope(|s| {
2018 let barrier = Arc::new(Barrier::new(num_thread));
2019 for i in 0..num_thread {
2020 let repo = load_repo_at_head(settings, repo.repo_path()); // unshare loader
2021 let barrier = barrier.clone();
2022 let sender = sender.clone();
2023 s.spawn(move || {
2024 barrier.wait();
2025 let mut tx = repo.start_transaction(settings, &format!("writer {i}"));
2026 let commit = create_rooted_commit(tx.mut_repo(), settings)
2027 .set_description("racy commit")
2028 .write()
2029 .unwrap();
2030 tx.commit();
2031 sender
2032 .send((commit.id().clone(), commit.change_id().clone()))
2033 .unwrap();
2034 });
2035 }
2036 });
2037
2038 drop(sender);
2039 let mut commit_change_ids: BTreeMap<CommitId, HashSet<ChangeId>> = BTreeMap::new();
2040 for (commit_id, change_id) in receiver {
2041 commit_change_ids
2042 .entry(commit_id)
2043 .or_default()
2044 .insert(change_id);
2045 }
2046
2047 // Ideally, each commit should have unique commit/change ids.
2048 assert_eq!(commit_change_ids.len(), num_thread);
2049
2050 // All unique commits should be preserved.
2051 let repo = repo.reload_at_head(settings).unwrap();
2052 for (commit_id, change_ids) in &commit_change_ids {
2053 let commit = repo.store().get_commit(commit_id).unwrap();
2054 assert_eq!(commit.id(), commit_id);
2055 assert!(change_ids.contains(commit.change_id()));
2056 }
2057
2058 // The index should be consistent with the store.
2059 for commit_id in commit_change_ids.keys() {
2060 assert!(repo.index().has_id(commit_id));
2061 let commit = repo.store().get_commit(commit_id).unwrap();
2062 assert_eq!(
2063 repo.resolve_change_id(commit.change_id()),
2064 Some(vec![commit_id.clone()]),
2065 );
2066 }
2067}
2068
2069#[test]
2070fn test_concurrent_read_write_commit() {
2071 let settings = &testutils::user_settings();
2072 let test_repo = TestRepo::init(true);
2073 let repo = &test_repo.repo;
2074
2075 // Create unique commits and load them concurrently. In this test, we assume
2076 // that writer doesn't fall back to timestamp adjustment, so the expected
2077 // commit ids are static. If reader could interrupt in the timestamp
2078 // adjustment loop, this assumption wouldn't apply.
2079 let commit_ids = [
2080 "c5c6efd6ac240102e7f047234c3cade55eedd621",
2081 "9f7a96a6c9d044b228f3321a365bdd3514e6033a",
2082 "aa7867ad0c566df5bbb708d8d6ddc88eefeea0ff",
2083 "930a76e333d5cc17f40a649c3470cb99aae24a0c",
2084 "88e9a719df4f0cc3daa740b814e271341f6ea9f4",
2085 "4883bdc57448a53b4eef1af85e34b85b9ee31aee",
2086 "308345f8d058848e83beed166704faac2ecd4541",
2087 "9e35ff61ea8d1d4ef7f01edc5fd23873cc301b30",
2088 "8010ac8c65548dd619e7c83551d983d724dda216",
2089 "bbe593d556ea31acf778465227f340af7e627b2b",
2090 "2f6800f4b8e8fc4c42dc0e417896463d13673654",
2091 "a3a7e4fcddeaa11bb84f66f3428f107f65eb3268",
2092 "96e17ff3a7ee1b67ddfa5619b2bf5380b80f619a",
2093 "34613f7609524c54cc990ada1bdef3dcad0fd29f",
2094 "95867e5aed6b62abc2cd6258da9fee8873accfd3",
2095 "7635ce107ae7ba71821b8cd74a1405ca6d9e49ac",
2096 ]
2097 .into_iter()
2098 .map(CommitId::from_hex)
2099 .collect_vec();
2100 let num_reader_thread = 8;
2101 thread::scope(|s| {
2102 let barrier = Arc::new(Barrier::new(commit_ids.len() + num_reader_thread));
2103
2104 // Writer assigns random change id
2105 for (i, commit_id) in commit_ids.iter().enumerate() {
2106 let repo = load_repo_at_head(settings, repo.repo_path()); // unshare loader
2107 let barrier = barrier.clone();
2108 s.spawn(move || {
2109 barrier.wait();
2110 let mut tx = repo.start_transaction(settings, &format!("writer {i}"));
2111 let commit = create_rooted_commit(tx.mut_repo(), settings)
2112 .set_description(format!("commit {i}"))
2113 .write()
2114 .unwrap();
2115 tx.commit();
2116 assert_eq!(commit.id(), commit_id);
2117 });
2118 }
2119
2120 // Reader may generate change id (if not yet assigned by the writer)
2121 for i in 0..num_reader_thread {
2122 let mut repo = load_repo_at_head(settings, repo.repo_path()); // unshare loader
2123 let barrier = barrier.clone();
2124 let mut pending_commit_ids = commit_ids.clone();
2125 pending_commit_ids.rotate_left(i); // start lookup from different place
2126 s.spawn(move || {
2127 barrier.wait();
2128 while !pending_commit_ids.is_empty() {
2129 repo = repo.reload_at_head(settings).unwrap();
2130 let mut tx = repo.start_transaction(settings, &format!("reader {i}"));
2131 pending_commit_ids = pending_commit_ids
2132 .into_iter()
2133 .filter_map(|commit_id| {
2134 match repo.store().get_commit(&commit_id) {
2135 Ok(commit) => {
2136 // update index as git::import_refs() would do
2137 tx.mut_repo().add_head(&commit);
2138 None
2139 }
2140 Err(BackendError::ObjectNotFound { .. }) => Some(commit_id),
2141 Err(err) => panic!("unexpected error: {err}"),
2142 }
2143 })
2144 .collect_vec();
2145 if tx.mut_repo().has_changes() {
2146 tx.commit();
2147 }
2148 thread::yield_now();
2149 }
2150 });
2151 }
2152 });
2153
2154 // The index should be consistent with the store.
2155 let repo = repo.reload_at_head(settings).unwrap();
2156 for commit_id in &commit_ids {
2157 assert!(repo.index().has_id(commit_id));
2158 let commit = repo.store().get_commit(commit_id).unwrap();
2159 assert_eq!(
2160 repo.resolve_change_id(commit.change_id()),
2161 Some(vec![commit_id.clone()]),
2162 );
2163 }
2164}
2165
2166fn create_rooted_commit<'repo>(
2167 mut_repo: &'repo mut MutableRepo,
2168 settings: &UserSettings,
2169) -> CommitBuilder<'repo> {
2170 let signature = Signature {
2171 name: "Test User".to_owned(),
2172 email: "test.user@example.com".to_owned(),
2173 timestamp: Timestamp {
2174 // avoid underflow during timestamp adjustment
2175 timestamp: MillisSinceEpoch(1_000_000),
2176 tz_offset: 0,
2177 },
2178 };
2179 mut_repo
2180 .new_commit(
2181 settings,
2182 vec![mut_repo.store().root_commit_id().clone()],
2183 mut_repo.store().empty_tree_id().clone(),
2184 )
2185 .set_author(signature.clone())
2186 .set_committer(signature)
2187}
2188
2189#[test]
2190fn test_parse_gitmodules() {
2191 let result = git::parse_gitmodules(
2192 &mut r#"
2193[submodule "wellformed"]
2194url = https://github.com/martinvonz/jj
2195path = mod
2196update = checkout # Extraneous config
2197
2198[submodule "uppercase"]
2199URL = https://github.com/martinvonz/jj
2200PATH = mod2
2201
2202[submodule "repeated_keys"]
2203url = https://github.com/martinvonz/jj
2204path = mod3
2205url = https://github.com/chooglen/jj
2206path = mod4
2207
2208# The following entries aren't expected in a well-formed .gitmodules
2209[submodule "missing_url"]
2210path = mod
2211
2212[submodule]
2213ignoreThisSection = foo
2214
2215[randomConfig]
2216ignoreThisSection = foo
2217"#
2218 .as_bytes(),
2219 )
2220 .unwrap();
2221 let expected = btreemap! {
2222 "wellformed".to_string() => SubmoduleConfig {
2223 name: "wellformed".to_string(),
2224 url: "https://github.com/martinvonz/jj".to_string(),
2225 path: "mod".to_string(),
2226 },
2227 "uppercase".to_string() => SubmoduleConfig {
2228 name: "uppercase".to_string(),
2229 url: "https://github.com/martinvonz/jj".to_string(),
2230 path: "mod2".to_string(),
2231 },
2232 "repeated_keys".to_string() => SubmoduleConfig {
2233 name: "repeated_keys".to_string(),
2234 url: "https://github.com/martinvonz/jj".to_string(),
2235 path: "mod3".to_string(),
2236 },
2237 };
2238
2239 assert_eq!(result, expected);
2240}