6214 lines · 216.1 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;use std::collections::HashMap;use std::collections::HashSet;use std::fs;use std::io;use std::io::Write as _;use std::iter;use std::path::Path;use std::path::PathBuf;use std::sync::Arc;use std::sync::Barrier;use std::sync::mpsc;use std::thread; use assert_matches::assert_matches;use gix::remote::Direction;use itertools::Itertools as _;use jj_lib::backend::BackendError;use jj_lib::backend::ChangeId;use jj_lib::backend::CommitId;use jj_lib::backend::MillisSinceEpoch;use jj_lib::backend::Signature;use jj_lib::backend::Timestamp;use jj_lib::commit::Commit;use jj_lib::commit_builder::CommitBuilder;use jj_lib::config::ConfigLayer;use jj_lib::config::ConfigSource;use jj_lib::git;use jj_lib::git::FailedRefExportReason;use jj_lib::git::FetchTagsOverride;use jj_lib::git::GitBranchPushTargets;use jj_lib::git::GitFetch;use jj_lib::git::GitFetchError;use jj_lib::git::GitFetchRefExpression;use jj_lib::git::GitImportError;use jj_lib::git::GitImportOptions;use jj_lib::git::GitImportStats;use jj_lib::git::GitPushError;use jj_lib::git::GitPushOptions;use jj_lib::git::GitPushStats;use jj_lib::git::GitRefKind;use jj_lib::git::GitRefUpdate;use jj_lib::git::GitResetHeadError;use jj_lib::git::GitSettings;use jj_lib::git::GitSidebandLineTerminator;use jj_lib::git::GitSubprocessCallback;use jj_lib::git::GitSubprocessOptions;use jj_lib::git::IgnoredRefspec;use jj_lib::git::IgnoredRefspecs;use jj_lib::git::expand_fetch_refspecs;use jj_lib::git::load_default_fetch_bookmarks;use jj_lib::git_backend::GitBackend;use jj_lib::hex_util;use jj_lib::index::ResolvedChangeTargets;use jj_lib::merge::Merge;use jj_lib::object_id::ObjectId as _;use jj_lib::op_store::LocalRemoteRefTarget;use jj_lib::op_store::RefTarget;use jj_lib::op_store::RemoteRef;use jj_lib::op_store::RemoteRefState;use jj_lib::ref_name::GitRefNameBuf;use jj_lib::ref_name::RefName;use jj_lib::ref_name::RemoteName;use jj_lib::ref_name::RemoteRefSymbol;use jj_lib::refs::BookmarkPushUpdate;use jj_lib::repo::MutableRepo;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo as _;use jj_lib::settings::UserSettings;use jj_lib::signing::Signer;use jj_lib::str_util::StringExpression;use jj_lib::str_util::StringMatcher;use jj_lib::str_util::StringPattern;use jj_lib::workspace::Workspace;use maplit::btreemap;use maplit::hashset;use pollster::FutureExt as _;use tempfile::TempDir;use test_case::test_case;use testutils::CommitBuilderExt as _;use testutils::TestRepo;use testutils::TestRepoBackend;use testutils::base_user_config;use testutils::commit_transactions;use testutils::create_random_commit;use testutils::repo_path;use testutils::write_random_commit;use testutils::write_random_commit_with_parents; #[derive(Debug)]struct NullCallback; impl GitSubprocessCallback for NullCallback {    fn needs_progress(&self) -> bool {        false    }     fn progress(&mut self, _progress: &git::GitProgress) -> io::Result<()> {        Ok(())    }     fn local_sideband(        &mut self,        _message: &[u8],        _term: Option<GitSidebandLineTerminator>,    ) -> io::Result<()> {        Ok(())    }     fn remote_sideband(        &mut self,        _message: &[u8],        _term: Option<GitSidebandLineTerminator>,    ) -> io::Result<()> {        Ok(())    }} fn empty_git_commit(    git_repo: &gix::Repository,    ref_name: &str,    parents: &[gix::ObjectId],) -> gix::ObjectId {    let empty_tree_id = git_repo.empty_tree().id().detach();    testutils::git::write_commit(        git_repo,        ref_name,        empty_tree_id,        &format!("random commit {}", rand::random::<u32>()),        parents,    )} fn jj_id(id: gix::ObjectId) -> CommitId {    CommitId::from_bytes(id.as_bytes())} fn git_id(commit: &Commit) -> gix::ObjectId {    gix::ObjectId::from_bytes_or_panic(commit.id().as_bytes())} fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>where    N: AsRef<RefName> + ?Sized,    M: AsRef<RemoteName> + ?Sized,{    RemoteRefSymbol {        name: name.as_ref(),        remote: remote.as_ref(),    }} fn get_git_backend(repo: &Arc<ReadonlyRepo>) -> &GitBackend {    repo.store().backend_impl().unwrap()} fn get_git_repo(repo: &Arc<ReadonlyRepo>) -> gix::Repository {    get_git_backend(repo).git_repo()} /// Fetches and imports all refs with the default configuration.fn fetch_import_all(mut_repo: &mut MutableRepo, remote: &RemoteName) -> GitImportStats {    let git_settings = GitSettings::from_settings(mut_repo.base_repo().settings()).unwrap();    let import_options = default_import_options();    let mut fetcher = GitFetch::new(        mut_repo,        git_settings.to_subprocess_options(),        &import_options,    )    .unwrap();    fetch_all_with(&mut fetcher, remote).unwrap();    fetcher.import_refs().unwrap()} /// Fetches all refs without importing.fn fetch_all_with(fetcher: &mut GitFetch, remote: &RemoteName) -> Result<(), GitFetchError> {    let ref_expr = GitFetchRefExpression {        bookmark: StringExpression::all(),        // TODO: disable implicit fetching and set this to "all" (#7528)        tag: StringExpression::none(),    };    fetch_with(fetcher, remote, ref_expr)} /// Fetches the specified refs without importing.fn fetch_with(    fetcher: &mut GitFetch,    remote: &RemoteName,    ref_expr: GitFetchRefExpression,) -> Result<(), GitFetchError> {    let refspecs = expand_fetch_refspecs(remote, ref_expr).expect("ref patterns should be valid");    let depth = None;    let fetch_tags = None;    fetcher.fetch(remote, refspecs, &mut NullCallback, depth, fetch_tags)} fn push_status_rejected_references(push_stats: GitPushStats) -> Vec<GitRefNameBuf> {    assert!(push_stats.pushed.is_empty());    assert!(push_stats.remote_rejected.is_empty());    push_stats        .rejected        .into_iter()        .map(|(reference, _)| reference)        .collect()} #[test]fn test_import_refs() {    let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);    let repo = &test_repo.repo;    let git_repo = get_git_repo(repo);    let import_options = default_import_options();     let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);    git_ref(&git_repo, "refs/remotes/origin/main", commit1);    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]);     testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");     let mut tx = repo.start_transaction();    git::import_head(tx.repo_mut()).unwrap();    let stats = git::import_refs(tx.repo_mut(), &import_options).unwrap();    tx.repo_mut().rebase_descendants().block_on().unwrap();    let repo = tx.commit("test").block_on().unwrap();    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.bookmarks().count(), 4);    assert_eq!(        view.get_local_bookmark("main".as_ref()),        &RefTarget::normal(jj_id(commit2))    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("main", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit2)),            state: RemoteRefState::Tracked,        },    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("main", "origin")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit1)),            state: RemoteRefState::New,        },    );    assert_eq!(        view.get_local_bookmark("feature1".as_ref()),        &RefTarget::normal(jj_id(commit3))    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature1", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit3)),            state: RemoteRefState::Tracked,        },    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature1", "origin")),        RemoteRef::absent_ref()    );    assert_eq!(        view.get_local_bookmark("feature2".as_ref()),        &RefTarget::normal(jj_id(commit4))    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature2", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit4)),            state: RemoteRefState::Tracked,        },    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature2", "origin")),        RemoteRef::absent_ref()    );    assert_eq!(        view.get_local_bookmark("feature3".as_ref()),        RefTarget::absent_ref()    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature3", "git")),        RemoteRef::absent_ref()    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature3", "origin")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit6)),            state: RemoteRefState::New,        },    );     assert_eq!(        view.get_local_tag("v1.0".as_ref()),        &RefTarget::normal(jj_id(commit5))    );    assert_eq!(        view.get_remote_tag(remote_symbol("v1.0", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit5)),            state: RemoteRefState::Tracked,        },    );     assert_eq!(view.git_refs().len(), 6);    assert_eq!(        view.get_git_ref("refs/heads/main".as_ref()),        &RefTarget::normal(jj_id(commit2))    );    assert_eq!(        view.get_git_ref("refs/heads/feature1".as_ref()),        &RefTarget::normal(jj_id(commit3))    );    assert_eq!(        view.get_git_ref("refs/heads/feature2".as_ref()),        &RefTarget::normal(jj_id(commit4))    );    assert_eq!(        view.get_git_ref("refs/remotes/origin/main".as_ref()),        &RefTarget::normal(jj_id(commit1))    );    assert_eq!(        view.get_git_ref("refs/remotes/origin/feature3".as_ref()),        &RefTarget::normal(jj_id(commit6))    );    assert_eq!(        view.get_git_ref("refs/tags/v1.0".as_ref()),        &RefTarget::normal(jj_id(commit5))    );    assert_eq!(view.git_head(), &RefTarget::normal(jj_id(commit2)));} #[test]fn test_import_refs_reimport() {    let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git);    let repo = &test_workspace.repo;    let git_repo = get_git_repo(repo);    let import_options = default_import_options();     let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);    git_ref(&git_repo, "refs/remotes/origin/main", commit1);    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.write_blob(b"my PGP key").unwrap().detach();    git_repo        .reference(            "refs/tags/my-gpg-key",            pgp_key_oid,            gix::refs::transaction::PreviousValue::MustNotExist,            "",        )        .unwrap();     let mut tx = repo.start_transaction();    let stats = git::import_refs(tx.repo_mut(), &import_options).unwrap();    tx.repo_mut().rebase_descendants().block_on().unwrap();    let repo = tx.commit("test").block_on().unwrap();     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();    let commit6 = create_random_commit(tx.repo_mut())        .set_parents(vec![jj_id(commit2)])        .write_unwrap();    tx.repo_mut()        .set_local_bookmark_target("feature2".as_ref(), RefTarget::normal(commit6.id().clone()));    let repo = tx.commit("test").block_on().unwrap();     let mut tx = repo.start_transaction();    let stats = git::import_refs(tx.repo_mut(), &import_options).unwrap();    tx.repo_mut().rebase_descendants().block_on().unwrap();    let repo = tx.commit("test").block_on().unwrap();     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.bookmarks().count(), 2);    let commit1_target = RefTarget::normal(jj_id(commit1));    let commit2_target = RefTarget::normal(jj_id(commit2));    assert_eq!(        view.get_local_bookmark("main".as_ref()),        &RefTarget::normal(jj_id(commit2))    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("main", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit2)),            state: RemoteRefState::Tracked,        },    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("main", "origin")),        &RemoteRef {            target: commit1_target.clone(),            state: RemoteRefState::New,        },    );    assert_eq!(        view.get_local_bookmark("feature2".as_ref()),        &RefTarget::from_legacy_form([jj_id(commit4)], [commit6.id().clone(), jj_id(commit5)])    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature2", "git")),        &RemoteRef {            target: RefTarget::normal(jj_id(commit5)),            state: RemoteRefState::Tracked,        },    );    assert_eq!(        view.get_remote_bookmark(remote_symbol("feature2", "origin")),        RemoteRef::absent_ref()    );     assert_eq!(view.local_tags().count(), 0);     assert_eq!(view.git_refs().len(), 3);    assert_eq!(        view.get_git_ref("refs/heads/main".as_ref()),        &commit2_target    );    assert_eq!(        view.get_git_ref("refs/remotes/origin/main".as_ref()),        &commit1_target    );    let commit5_target = RefTarget::normal(jj_id(commit5));    assert_eq!(        view.get_git_ref("refs/heads/feature2".as_ref()),        &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 test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);    let repo = &test_repo.repo;    let git_repo = get_git_repo(repo);    let import_options = default_import_options();     let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);    let mut tx = repo.start_transaction();    git::import_refs(tx.repo_mut(), &import_options).unwrap();    tx.repo_mut().rebase_descendants().block_on().unwrap();    let commit_id = jj_id(commit);    // Test the setup    assert!(tx.repo().view().heads().contains(&commit_id));

Lines 1–500 of 6214.