3039 lines · 108.9 KB · Rust
RawDownload
// 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

Lines 1–500 of 3039.