// Copyright 2020 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::{BTreeMap, HashSet};use std::path::PathBuf;use std::sync::{mpsc, Arc, Barrier};use std::{fs, thread}; use assert_matches::assert_matches;use git2::Oid;use itertools::Itertools;use jj_lib::backend::{BackendError, ChangeId, CommitId, MillisSinceEpoch, Signature, Timestamp};use jj_lib::commit::Commit;use jj_lib::commit_builder::CommitBuilder;use jj_lib::git;use jj_lib::git::{ FailedRefExportReason, GitBranchPushTargets, GitFetchError, GitImportError, GitPushError, GitRefUpdate, RefName, SubmoduleConfig,};use jj_lib::git_backend::GitBackend;use jj_lib::object_id::ObjectId;use jj_lib::op_store::{BranchTarget, RefTarget, RemoteRef, RemoteRefState};use jj_lib::refs::BranchPushUpdate;use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo};use jj_lib::settings::{GitSettings, UserSettings};use jj_lib::signing::Signer;use jj_lib::str_util::StringPattern;use jj_lib::workspace::Workspace;use maplit::{btreemap, hashset};use tempfile::TempDir;use testutils::{ commit_transactions, create_random_commit, load_repo_at_head, write_random_commit, TestRepo, TestRepoBackend,}; fn empty_git_commit<'r>( git_repo: &'r git2::Repository, ref_name: &str, parents: &[&git2::Commit],) -> git2::Commit<'r> { let signature = git2::Signature::now("Someone", "someone@example.com").unwrap(); let empty_tree_id = Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(); let empty_tree = git_repo.find_tree(empty_tree_id).unwrap(); let oid = git_repo .commit( Some(ref_name), &signature, &signature, &format!("random commit {}", rand::random::<u32>()), &empty_tree, parents, ) .unwrap(); git_repo.find_commit(oid).unwrap()} fn jj_id(commit: &git2::Commit) -> CommitId { CommitId::from_bytes(commit.id().as_bytes())} fn git_id(commit: &Commit) -> Oid { Oid::from_bytes(commit.id().as_bytes()).unwrap()} fn get_git_backend(repo: &Arc<ReadonlyRepo>) -> &GitBackend { repo.store() .backend_impl() .downcast_ref::<GitBackend>() .unwrap()} fn get_git_repo(repo: &Arc<ReadonlyRepo>) -> git2::Repository { get_git_backend(repo).open_git_repo().unwrap()} #[test]fn test_import_refs() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_ref(&git_repo, "refs/remotes/origin/main", commit1.id()); let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]); let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]); let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); let commit5 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[&commit1]); let commit6 = empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[&commit1]); // Should not be imported empty_git_commit(&git_repo, "refs/notes/x", &[&commit2]); empty_git_commit(&git_repo, "refs/remotes/origin/HEAD", &[&commit2]); git_repo.set_head("refs/heads/main").unwrap(); let mut tx = repo.start_transaction(&settings); git::import_head(tx.mut_repo()).unwrap(); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); let view = repo.view(); assert!(stats.abandoned_commits.is_empty()); let expected_heads = hashset! { jj_id(&commit3), jj_id(&commit4), jj_id(&commit5), jj_id(&commit6) }; assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 4); assert_eq!( view.get_local_branch("main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_remote_branch("main", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit2)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_remote_branch("main", "origin"), &RemoteRef { target: RefTarget::normal(jj_id(&commit1)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_local_branch("feature1"), &RefTarget::normal(jj_id(&commit3)) ); assert_eq!( view.get_remote_branch("feature1", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit3)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature1", "origin").is_absent()); assert_eq!( view.get_local_branch("feature2"), &RefTarget::normal(jj_id(&commit4)) ); assert_eq!( view.get_remote_branch("feature2", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit4)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature2", "origin").is_absent()); assert_eq!( view.get_local_branch("feature3"), &RefTarget::normal(jj_id(&commit6)) ); assert!(view.get_remote_branch("feature3", "git").is_absent()); assert_eq!( view.get_remote_branch("feature3", "origin"), &RemoteRef { target: RefTarget::normal(jj_id(&commit6)), state: RemoteRefState::Tracking, }, ); assert_eq!(view.get_tag("v1.0"), &RefTarget::normal(jj_id(&commit5))); assert_eq!(view.git_refs().len(), 6); assert_eq!( view.get_git_ref("refs/heads/main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_git_ref("refs/heads/feature1"), &RefTarget::normal(jj_id(&commit3)) ); assert_eq!( view.get_git_ref("refs/heads/feature2"), &RefTarget::normal(jj_id(&commit4)) ); assert_eq!( view.get_git_ref("refs/remotes/origin/main"), &RefTarget::normal(jj_id(&commit1)) ); assert_eq!( view.get_git_ref("refs/remotes/origin/feature3"), &RefTarget::normal(jj_id(&commit6)) ); assert_eq!( view.get_git_ref("refs/tags/v1.0"), &RefTarget::normal(jj_id(&commit5)) ); assert_eq!(view.git_head(), &RefTarget::normal(jj_id(&commit2)));} #[test]fn test_import_refs_reimport() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_workspace.repo; let git_repo = get_git_repo(repo); let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_ref(&git_repo, "refs/remotes/origin/main", commit1.id()); let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]); let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]); let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); let pgp_key_oid = git_repo.blob(b"my PGP key").unwrap(); git_repo .reference("refs/tags/my-gpg-key", pgp_key_oid, false, "") .unwrap(); let mut tx = repo.start_transaction(&settings); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); assert!(stats.abandoned_commits.is_empty()); let expected_heads = hashset! { jj_id(&commit3), jj_id(&commit4), }; let view = repo.view(); assert_eq!(*view.heads(), expected_heads); // Delete feature1 and rewrite feature2 delete_git_ref(&git_repo, "refs/heads/feature1"); delete_git_ref(&git_repo, "refs/heads/feature2"); let commit5 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); // Also modify feature2 on the jj side let mut tx = repo.start_transaction(&settings); let commit6 = create_random_commit(tx.mut_repo(), &settings) .set_parents(vec![jj_id(&commit2)]) .write() .unwrap(); tx.mut_repo() .set_local_branch_target("feature2", RefTarget::normal(commit6.id().clone())); let repo = tx.commit("test"); let mut tx = repo.start_transaction(&settings); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); assert_eq!( // The order is unstable just because we import heads from Git repo. HashSet::from_iter(stats.abandoned_commits), hashset! { jj_id(&commit4), jj_id(&commit3), }, ); let view = repo.view(); let expected_heads = hashset! { jj_id(&commit5), commit6.id().clone(), }; assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 2); let commit1_target = RefTarget::normal(jj_id(&commit1)); let commit2_target = RefTarget::normal(jj_id(&commit2)); assert_eq!( view.get_local_branch("main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_remote_branch("main", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit2)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_remote_branch("main", "origin"), &RemoteRef { target: commit1_target.clone(), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_local_branch("feature2"), &RefTarget::from_legacy_form([jj_id(&commit4)], [commit6.id().clone(), jj_id(&commit5)]) ); assert_eq!( view.get_remote_branch("feature2", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit5)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature2", "origin").is_absent()); assert!(view.tags().is_empty()); assert_eq!(view.git_refs().len(), 3); assert_eq!(view.get_git_ref("refs/heads/main"), &commit2_target); assert_eq!( view.get_git_ref("refs/remotes/origin/main"), &commit1_target ); let commit5_target = RefTarget::normal(jj_id(&commit5)); assert_eq!(view.get_git_ref("refs/heads/feature2"), &commit5_target);} #[test]fn test_import_refs_reimport_head_removed() { // Test that re-importing refs doesn't cause a deleted head to come back let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]); let mut tx = repo.start_transaction(&settings); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let commit_id = jj_id(&commit); // Test the setup assert!(tx.mut_repo().view().heads().contains(&commit_id)); // Remove the head and re-import tx.mut_repo().remove_head(&commit_id); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(!tx.mut_repo().view().heads().contains(&commit_id));} #[test]fn test_import_refs_reimport_git_head_counts() { // Test that if a branch is removed but the Git HEAD points to the commit (or a // descendant of it), we still keep it alive. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_repo.set_head_detached(commit.id()).unwrap(); let mut tx = repo.start_transaction(&settings); git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); // Delete the branch and re-import. The commit should still be there since HEAD // points to it git_repo .find_reference("refs/heads/main") .unwrap() .delete() .unwrap(); git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit)));} #[test]fn test_import_refs_reimport_git_head_without_ref() { // Simulate external `git checkout` in colocated repo, from anonymous branch. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); // First, HEAD points to commit1. let mut tx = repo.start_transaction(&settings); let commit1 = write_random_commit(tx.mut_repo(), &settings); let commit2 = write_random_commit(tx.mut_repo(), &settings); git_repo.set_head_detached(git_id(&commit1)).unwrap(); // Import HEAD. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); // Move HEAD to commit2 (by e.g. `git checkout` command) git_repo.set_head_detached(git_id(&commit2)).unwrap(); // Reimport HEAD, which doesn't abandon the old HEAD branch because jj thinks it // would be moved by `git checkout` command. This isn't always true because the // detached HEAD commit could be rewritten by e.g. `git commit --amend` command, // but it should be safer than abandoning old checkout branch. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id()));} #[test]fn test_import_refs_reimport_git_head_with_moved_ref() { // Simulate external history rewriting in colocated repo. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); // First, both HEAD and main point to commit1. let mut tx = repo.start_transaction(&settings); let commit1 = write_random_commit(tx.mut_repo(), &settings); let commit2 = write_random_commit(tx.mut_repo(), &settings); git_repo .reference("refs/heads/main", git_id(&commit1), true, "test") .unwrap(); git_repo.set_head_detached(git_id(&commit1)).unwrap(); // Import HEAD and main. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); // Move both HEAD and main to commit2 (by e.g. `git commit --amend` command) git_repo .reference("refs/heads/main", git_id(&commit2), true, "test") .unwrap(); git_repo.set_head_detached(git_id(&commit2)).unwrap(); // Reimport HEAD and main, which abandons the old main branch. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(!tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id()));} #[test]fn test_import_refs_reimport_with_deleted_remote_ref() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_workspace.repo; let git_repo = get_git_repo(repo); let commit_base = empty_git_commit(&git_repo, "refs/heads/main", &[]); let commit_main = empty_git_commit(&git_repo, "refs/heads/main", &[&commit_base]); let commit_remote_only = empty_git_commit( &git_repo, "refs/remotes/origin/feature-remote-only", &[&commit_base], ); let commit_remote_and_local = empty_git_commit( &git_repo, "refs/remotes/origin/feature-remote-and-local", &[&commit_base], ); git_ref( &git_repo, "refs/heads/feature-remote-and-local", commit_remote_and_local.id(), ); let mut tx = repo.start_transaction(&settings); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); let expected_heads = hashset! { jj_id(&commit_main), jj_id(&commit_remote_only), jj_id(&commit_remote_and_local), }; let view = repo.view(); assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 3); // Even though the git repo does not have a local branch forLines 1–500 of 3039. Load more
// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::{BTreeMap, HashSet}; use std::path::PathBuf; use std::sync::{mpsc, Arc, Barrier}; use std::{fs, thread}; use assert_matches::assert_matches; use git2::Oid; use itertools::Itertools; use jj_lib::backend::{BackendError, ChangeId, CommitId, MillisSinceEpoch, Signature, Timestamp}; use jj_lib::commit::Commit; use jj_lib::commit_builder::CommitBuilder; use jj_lib::git; use jj_lib::git::{ FailedRefExportReason, GitBranchPushTargets, GitFetchError, GitImportError, GitPushError, GitRefUpdate, RefName, SubmoduleConfig, }; use jj_lib::git_backend::GitBackend; use jj_lib::object_id::ObjectId; use jj_lib::op_store::{BranchTarget, RefTarget, RemoteRef, RemoteRefState}; use jj_lib::refs::BranchPushUpdate; use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo}; use jj_lib::settings::{GitSettings, UserSettings}; use jj_lib::signing::Signer; use jj_lib::str_util::StringPattern; use jj_lib::workspace::Workspace; use maplit::{btreemap, hashset}; use tempfile::TempDir; use testutils::{ commit_transactions, create_random_commit, load_repo_at_head, write_random_commit, TestRepo, TestRepoBackend, }; fn empty_git_commit<'r>( git_repo: &'r git2::Repository, ref_name: &str, parents: &[&git2::Commit], ) -> git2::Commit<'r> { let signature = git2::Signature::now("Someone", "someone@example.com").unwrap(); let empty_tree_id = Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(); let empty_tree = git_repo.find_tree(empty_tree_id).unwrap(); let oid = git_repo .commit( Some(ref_name), &signature, &signature, &format!("random commit {}", rand::random::<u32>()), &empty_tree, parents, ) .unwrap(); git_repo.find_commit(oid).unwrap() } fn jj_id(commit: &git2::Commit) -> CommitId { CommitId::from_bytes(commit.id().as_bytes()) } fn git_id(commit: &Commit) -> Oid { Oid::from_bytes(commit.id().as_bytes()).unwrap() } fn get_git_backend(repo: &Arc<ReadonlyRepo>) -> &GitBackend { repo.store() .backend_impl() .downcast_ref::<GitBackend>() .unwrap() } fn get_git_repo(repo: &Arc<ReadonlyRepo>) -> git2::Repository { get_git_backend(repo).open_git_repo().unwrap() } #[test] fn test_import_refs() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_ref(&git_repo, "refs/remotes/origin/main", commit1.id()); let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]); let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]); let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); let commit5 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[&commit1]); let commit6 = empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[&commit1]); // Should not be imported empty_git_commit(&git_repo, "refs/notes/x", &[&commit2]); empty_git_commit(&git_repo, "refs/remotes/origin/HEAD", &[&commit2]); git_repo.set_head("refs/heads/main").unwrap(); let mut tx = repo.start_transaction(&settings); git::import_head(tx.mut_repo()).unwrap(); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); let view = repo.view(); assert!(stats.abandoned_commits.is_empty()); let expected_heads = hashset! { jj_id(&commit3), jj_id(&commit4), jj_id(&commit5), jj_id(&commit6) }; assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 4); assert_eq!( view.get_local_branch("main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_remote_branch("main", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit2)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_remote_branch("main", "origin"), &RemoteRef { target: RefTarget::normal(jj_id(&commit1)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_local_branch("feature1"), &RefTarget::normal(jj_id(&commit3)) ); assert_eq!( view.get_remote_branch("feature1", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit3)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature1", "origin").is_absent()); assert_eq!( view.get_local_branch("feature2"), &RefTarget::normal(jj_id(&commit4)) ); assert_eq!( view.get_remote_branch("feature2", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit4)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature2", "origin").is_absent()); assert_eq!( view.get_local_branch("feature3"), &RefTarget::normal(jj_id(&commit6)) ); assert!(view.get_remote_branch("feature3", "git").is_absent()); assert_eq!( view.get_remote_branch("feature3", "origin"), &RemoteRef { target: RefTarget::normal(jj_id(&commit6)), state: RemoteRefState::Tracking, }, ); assert_eq!(view.get_tag("v1.0"), &RefTarget::normal(jj_id(&commit5))); assert_eq!(view.git_refs().len(), 6); assert_eq!( view.get_git_ref("refs/heads/main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_git_ref("refs/heads/feature1"), &RefTarget::normal(jj_id(&commit3)) ); assert_eq!( view.get_git_ref("refs/heads/feature2"), &RefTarget::normal(jj_id(&commit4)) ); assert_eq!( view.get_git_ref("refs/remotes/origin/main"), &RefTarget::normal(jj_id(&commit1)) ); assert_eq!( view.get_git_ref("refs/remotes/origin/feature3"), &RefTarget::normal(jj_id(&commit6)) ); assert_eq!( view.get_git_ref("refs/tags/v1.0"), &RefTarget::normal(jj_id(&commit5)) ); assert_eq!(view.git_head(), &RefTarget::normal(jj_id(&commit2))); } #[test] fn test_import_refs_reimport() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_workspace.repo; let git_repo = get_git_repo(repo); let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_ref(&git_repo, "refs/remotes/origin/main", commit1.id()); let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]); let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]); let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); let pgp_key_oid = git_repo.blob(b"my PGP key").unwrap(); git_repo .reference("refs/tags/my-gpg-key", pgp_key_oid, false, "") .unwrap(); let mut tx = repo.start_transaction(&settings); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); assert!(stats.abandoned_commits.is_empty()); let expected_heads = hashset! { jj_id(&commit3), jj_id(&commit4), }; let view = repo.view(); assert_eq!(*view.heads(), expected_heads); // Delete feature1 and rewrite feature2 delete_git_ref(&git_repo, "refs/heads/feature1"); delete_git_ref(&git_repo, "refs/heads/feature2"); let commit5 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]); // Also modify feature2 on the jj side let mut tx = repo.start_transaction(&settings); let commit6 = create_random_commit(tx.mut_repo(), &settings) .set_parents(vec![jj_id(&commit2)]) .write() .unwrap(); tx.mut_repo() .set_local_branch_target("feature2", RefTarget::normal(commit6.id().clone())); let repo = tx.commit("test"); let mut tx = repo.start_transaction(&settings); let stats = git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); assert_eq!( // The order is unstable just because we import heads from Git repo. HashSet::from_iter(stats.abandoned_commits), hashset! { jj_id(&commit4), jj_id(&commit3), }, ); let view = repo.view(); let expected_heads = hashset! { jj_id(&commit5), commit6.id().clone(), }; assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 2); let commit1_target = RefTarget::normal(jj_id(&commit1)); let commit2_target = RefTarget::normal(jj_id(&commit2)); assert_eq!( view.get_local_branch("main"), &RefTarget::normal(jj_id(&commit2)) ); assert_eq!( view.get_remote_branch("main", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit2)), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_remote_branch("main", "origin"), &RemoteRef { target: commit1_target.clone(), state: RemoteRefState::Tracking, }, ); assert_eq!( view.get_local_branch("feature2"), &RefTarget::from_legacy_form([jj_id(&commit4)], [commit6.id().clone(), jj_id(&commit5)]) ); assert_eq!( view.get_remote_branch("feature2", "git"), &RemoteRef { target: RefTarget::normal(jj_id(&commit5)), state: RemoteRefState::Tracking, }, ); assert!(view.get_remote_branch("feature2", "origin").is_absent()); assert!(view.tags().is_empty()); assert_eq!(view.git_refs().len(), 3); assert_eq!(view.get_git_ref("refs/heads/main"), &commit2_target); assert_eq!( view.get_git_ref("refs/remotes/origin/main"), &commit1_target ); let commit5_target = RefTarget::normal(jj_id(&commit5)); assert_eq!(view.get_git_ref("refs/heads/feature2"), &commit5_target); } #[test] fn test_import_refs_reimport_head_removed() { // Test that re-importing refs doesn't cause a deleted head to come back let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]); let mut tx = repo.start_transaction(&settings); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let commit_id = jj_id(&commit); // Test the setup assert!(tx.mut_repo().view().heads().contains(&commit_id)); // Remove the head and re-import tx.mut_repo().remove_head(&commit_id); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(!tx.mut_repo().view().heads().contains(&commit_id)); } #[test] fn test_import_refs_reimport_git_head_counts() { // Test that if a branch is removed but the Git HEAD points to the commit (or a // descendant of it), we still keep it alive. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]); git_repo.set_head_detached(commit.id()).unwrap(); let mut tx = repo.start_transaction(&settings); git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); // Delete the branch and re-import. The commit should still be there since HEAD // points to it git_repo .find_reference("refs/heads/main") .unwrap() .delete() .unwrap(); git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit))); } #[test] fn test_import_refs_reimport_git_head_without_ref() { // Simulate external `git checkout` in colocated repo, from anonymous branch. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); // First, HEAD points to commit1. let mut tx = repo.start_transaction(&settings); let commit1 = write_random_commit(tx.mut_repo(), &settings); let commit2 = write_random_commit(tx.mut_repo(), &settings); git_repo.set_head_detached(git_id(&commit1)).unwrap(); // Import HEAD. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); // Move HEAD to commit2 (by e.g. `git checkout` command) git_repo.set_head_detached(git_id(&commit2)).unwrap(); // Reimport HEAD, which doesn't abandon the old HEAD branch because jj thinks it // would be moved by `git checkout` command. This isn't always true because the // detached HEAD commit could be rewritten by e.g. `git commit --amend` command, // but it should be safer than abandoning old checkout branch. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); } #[test] fn test_import_refs_reimport_git_head_with_moved_ref() { // Simulate external history rewriting in colocated repo. let settings = testutils::user_settings(); let git_settings = GitSettings::default(); let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let git_repo = get_git_repo(repo); // First, both HEAD and main point to commit1. let mut tx = repo.start_transaction(&settings); let commit1 = write_random_commit(tx.mut_repo(), &settings); let commit2 = write_random_commit(tx.mut_repo(), &settings); git_repo .reference("refs/heads/main", git_id(&commit1), true, "test") .unwrap(); git_repo.set_head_detached(git_id(&commit1)).unwrap(); // Import HEAD and main. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); // Move both HEAD and main to commit2 (by e.g. `git commit --amend` command) git_repo .reference("refs/heads/main", git_id(&commit2), true, "test") .unwrap(); git_repo.set_head_detached(git_id(&commit2)).unwrap(); // Reimport HEAD and main, which abandons the old main branch. git::import_head(tx.mut_repo()).unwrap(); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); assert!(!tx.mut_repo().view().heads().contains(commit1.id())); assert!(tx.mut_repo().view().heads().contains(commit2.id())); } #[test] fn test_import_refs_reimport_with_deleted_remote_ref() { let settings = testutils::user_settings(); let git_settings = GitSettings { auto_local_branch: true, ..Default::default() }; let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_workspace.repo; let git_repo = get_git_repo(repo); let commit_base = empty_git_commit(&git_repo, "refs/heads/main", &[]); let commit_main = empty_git_commit(&git_repo, "refs/heads/main", &[&commit_base]); let commit_remote_only = empty_git_commit( &git_repo, "refs/remotes/origin/feature-remote-only", &[&commit_base], ); let commit_remote_and_local = empty_git_commit( &git_repo, "refs/remotes/origin/feature-remote-and-local", &[&commit_base], ); git_ref( &git_repo, "refs/heads/feature-remote-and-local", commit_remote_and_local.id(), ); let mut tx = repo.start_transaction(&settings); git::import_refs(tx.mut_repo(), &git_settings).unwrap(); tx.mut_repo().rebase_descendants(&settings).unwrap(); let repo = tx.commit("test"); let expected_heads = hashset! { jj_id(&commit_main), jj_id(&commit_remote_only), jj_id(&commit_remote_and_local), }; let view = repo.view(); assert_eq!(*view.heads(), expected_heads); assert_eq!(view.branches().count(), 3); // Even though the git repo does not have a local branch for