just playing with tangled
at tmp-tutorial 2240 lines 82 kB view raw
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}