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::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::GitPushRefTargets;
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::Diff;
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::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::TestResult;
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()
}
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().block_on().unwrap()
}
fn fetch_all_with(fetcher: &mut GitFetch, remote: &RemoteName) -> Result<(), GitFetchError> {
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::all(),
tag: StringExpression::none(),
};
fetch_with(fetcher, remote, ref_expr)
}
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() -> TestResult {
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]);
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()).block_on()?;
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
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)));
Ok(())
}
#[test]
fn test_import_refs_reimport() -> TestResult {
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")?.detach();
git_repo.reference(
"refs/tags/my-gpg-key",
pgp_key_oid,
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
let mut tx = repo.start_transaction();
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
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_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]);
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()?;
let mut tx = repo.start_transaction();
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
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
);
Ok(())
}
#[test]
fn test_import_refs_reimport_head_removed() -> TestResult {
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).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let commit_id = jj_id(commit);
assert!(tx.repo().view().heads().contains(&commit_id));
tx.repo_mut().remove_head(&commit_id);
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(!tx.repo().view().heads().contains(&commit_id));
Ok(())
}
#[test]
fn test_import_refs_reimport_git_head_does_not_count() -> TestResult {
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", &[]);
testutils::git::set_head_to_id(&git_repo, commit);
let mut tx = repo.start_transaction();
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
git_repo.find_reference("refs/heads/main")?.delete()?;
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(!tx.repo().view().heads().contains(&jj_id(commit)));
Ok(())
}
#[test]
fn test_import_refs_reimport_git_head_without_ref() -> TestResult {
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 mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
testutils::git::set_head_to_id(&git_repo, git_id(&commit1));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
testutils::git::set_head_to_id(&git_repo, git_id(&commit2));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
Ok(())
}
#[test]
fn test_import_refs_reimport_git_head_with_moved_ref() -> TestResult {
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 mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
git_repo.reference(
"refs/heads/main",
git_id(&commit1),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
testutils::git::set_head_to_id(&git_repo, git_id(&commit1));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
git_repo.reference(
"refs/heads/main",
git_id(&commit2),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
testutils::git::set_head_to_id(&git_repo, git_id(&commit2));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(!tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(!tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
Ok(())
}
#[test]
fn test_import_refs_reimport_with_deleted_remote_ref() -> TestResult {
let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_workspace.repo;
let git_repo = get_git_repo(repo);
let import_options = auto_track_import_options();
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,
);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
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.bookmarks().count(), 3);
assert_eq!(
view.get_local_bookmark("feature-remote-only".as_ref()),
&RefTarget::normal(jj_id(commit_remote_only))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_only)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_local_bookmark("feature-remote-and-local".as_ref()),
&RefTarget::normal(jj_id(commit_remote_and_local))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "git")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert!(view.get_local_bookmark("main".as_ref()).is_present());
delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-only");
delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-and-local");
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 2);
assert!(view.get_local_bookmark("main".as_ref()).is_present());
assert!(
view.get_local_bookmark("feature-remote-only".as_ref())
.is_absent()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "origin")),
RemoteRef::absent_ref()
);
assert!(
view.get_local_bookmark("feature-remote-and-local".as_ref())
.is_absent()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "git")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "origin")),
RemoteRef::absent_ref()
);
let expected_heads = hashset! {
jj_id(commit_main),
};
assert_eq!(*view.heads(), expected_heads);
Ok(())
}
#[test]
fn test_import_refs_reimport_with_moved_remote_ref() -> TestResult {
let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_workspace.repo;
let git_repo = get_git_repo(repo);
let import_options = auto_track_import_options();
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,
);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let expected_heads = hashset! {
jj_id(commit_main),
jj_id(dbg!(commit_remote_only)),
jj_id(dbg!(commit_remote_and_local)),
};
let view = repo.view();
assert_eq!(*view.heads(), expected_heads);
assert_eq!(view.bookmarks().count(), 3);
assert_eq!(
view.get_local_bookmark("feature-remote-only".as_ref()),
&RefTarget::normal(jj_id(commit_remote_only))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_only)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_local_bookmark("feature-remote-and-local".as_ref()),
&RefTarget::normal(jj_id(commit_remote_and_local))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "git")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert!(view.get_local_bookmark("main".as_ref()).is_present());
delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-only");
delete_git_ref(&git_repo, "refs/remotes/origin/feature-remote-and-local");
let new_commit_remote_only = empty_git_commit(
&git_repo,
"refs/remotes/origin/feature-remote-only",
&[commit_base],
);
let new_commit_remote_and_local = empty_git_commit(
&git_repo,
"refs/remotes/origin/feature-remote-and-local",
&[commit_base],
);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 3);
assert_eq!(
view.get_local_bookmark("feature-remote-only".as_ref()),
&RefTarget::normal(jj_id(new_commit_remote_only))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-only", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(new_commit_remote_only)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_local_bookmark("feature-remote-and-local".as_ref()),
&RefTarget::normal(jj_id(new_commit_remote_and_local))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "git")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-remote-and-local", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(new_commit_remote_and_local)),
state: RemoteRefState::Tracked,
},
);
assert!(view.get_local_bookmark("main".as_ref()).is_present()); let expected_heads = hashset! {
jj_id(commit_main),
jj_id(new_commit_remote_and_local),
jj_id(new_commit_remote_only),
};
assert_eq!(*view.heads(), expected_heads);
Ok(())
}
#[test]
fn test_import_refs_reimport_with_moved_untracked_remote_ref() -> TestResult {
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 remote_ref_name = "refs/remotes/origin/feature";
let commit_base = empty_git_commit(&git_repo, remote_ref_name, &[]);
let commit_remote_t0 = empty_git_commit(&git_repo, remote_ref_name, &[commit_base]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_t0) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 1);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_t0)),
state: RemoteRefState::New,
},
);
delete_git_ref(&git_repo, remote_ref_name);
let commit_remote_t1 = empty_git_commit(&git_repo, remote_ref_name, &[commit_base]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_t1) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 1);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_t1)),
state: RemoteRefState::New,
},
);
Ok(())
}
#[test]
fn test_import_refs_reimport_with_deleted_untracked_intermediate_remote_ref() -> TestResult {
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 remote_ref_name_a = "refs/remotes/origin/feature-a";
let remote_ref_name_b = "refs/remotes/origin/feature-b";
let commit_remote_a = empty_git_commit(&git_repo, remote_ref_name_a, &[]);
let commit_remote_b = empty_git_commit(&git_repo, remote_ref_name_b, &[commit_remote_a]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_b) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 2);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-a", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_a)),
state: RemoteRefState::New,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-b", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_b)),
state: RemoteRefState::New,
},
);
delete_git_ref(&git_repo, remote_ref_name_a);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_b) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 1);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-b", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_b)),
state: RemoteRefState::New,
},
);
Ok(())
}
#[test]
fn test_import_refs_reimport_with_deleted_abandoned_untracked_remote_ref() -> TestResult {
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 remote_ref_name_a = "refs/remotes/origin/feature-a";
let remote_ref_name_b = "refs/remotes/origin/feature-b";
let commit_remote_a = empty_git_commit(&git_repo, remote_ref_name_a, &[]);
let commit_remote_b = empty_git_commit(&git_repo, remote_ref_name_b, &[commit_remote_a]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_b) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 2);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-a", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_a)),
state: RemoteRefState::New,
},
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-b", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_b)),
state: RemoteRefState::New,
},
);
let mut tx = repo.start_transaction();
let jj_commit_remote_b = tx.repo().store().get_commit(&jj_id(commit_remote_b))?;
tx.repo_mut().record_abandoned_commit(&jj_commit_remote_b);
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(*view.heads(), hashset! { jj_id(commit_remote_a) });
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 2);
delete_git_ref(&git_repo, remote_ref_name_a);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(
*view.heads(),
hashset! { repo.store().root_commit_id().clone() }
);
assert_eq!(view.local_bookmarks().count(), 0);
assert_eq!(view.all_remote_bookmarks().count(), 1);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature-b", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_remote_b)),
state: RemoteRefState::New,
},
);
Ok(())
}
#[test]
fn test_import_refs_reimport_absent_tracked_remote_bookmarks() -> TestResult {
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 absent_tracked_ref = RemoteRef {
target: RefTarget::absent(),
state: RemoteRefState::Tracked,
};
let mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
tx.repo_mut()
.set_local_bookmark_target("foo".as_ref(), RefTarget::normal(commit1.id().clone()));
tx.repo_mut()
.set_remote_bookmark(remote_symbol("foo", "origin"), absent_tracked_ref.clone());
tx.repo_mut()
.set_remote_bookmark(remote_symbol("foo", "upstream"), absent_tracked_ref.clone());
let repo = tx.commit("test").block_on()?;
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
repo.view().all_remote_bookmarks().collect_vec(),
vec![
(remote_symbol("foo", "origin"), &absent_tracked_ref),
(remote_symbol("foo", "upstream"), &absent_tracked_ref),
]
);
git_repo.reference(
"refs/remotes/origin/foo",
git_id(&commit2),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
repo.view().get_local_bookmark("foo".as_ref()),
&RefTarget::normal(commit2.id().clone())
);
assert_eq!(
repo.view()
.get_remote_bookmark(remote_symbol("foo", "origin")),
&RemoteRef {
target: RefTarget::normal(commit2.id().clone()),
state: RemoteRefState::Tracked,
}
);
assert_eq!(
repo.view()
.get_remote_bookmark(remote_symbol("foo", "upstream")),
&absent_tracked_ref
);
Ok(())
}
#[test]
fn test_import_refs_reimport_absent_tracked_remote_tags() -> TestResult {
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 absent_tracked_ref = RemoteRef {
target: RefTarget::absent(),
state: RemoteRefState::Tracked,
};
let mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
let commit3 = write_random_commit(tx.repo_mut());
tx.repo_mut()
.set_local_tag_target("bar".as_ref(), RefTarget::normal(commit1.id().clone()));
tx.repo_mut()
.set_local_tag_target("foo".as_ref(), RefTarget::normal(commit2.id().clone()));
tx.repo_mut()
.set_remote_tag(remote_symbol("bar", "git"), absent_tracked_ref.clone());
tx.repo_mut()
.set_remote_tag(remote_symbol("foo", "git"), absent_tracked_ref.clone());
let repo = tx.commit("test").block_on()?;
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
repo.view().all_remote_tags().collect_vec(),
vec![
(remote_symbol("bar", "git"), &absent_tracked_ref),
(remote_symbol("foo", "git"), &absent_tracked_ref),
]
);
git_repo.reference(
"refs/tags/foo",
git_id(&commit3),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
repo.view().get_local_tag("foo".as_ref()),
&RefTarget::from_merge(Merge::from_vec(vec![
Some(commit2.id().clone()),
None,
Some(commit3.id().clone()),
])),
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("bar", "git")),
&absent_tracked_ref
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("foo", "git")),
&RemoteRef {
target: RefTarget::normal(commit3.id().clone()),
state: RemoteRefState::Tracked,
}
);
Ok(())
}
#[test]
fn test_import_refs_reimport_remote_tags_deleted() -> TestResult {
let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_workspace.repo;
let import_options = default_import_options();
let mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let target1 = RefTarget::normal(commit1.id().clone());
let remote_ref1 = RemoteRef {
target: target1.clone(),
state: RemoteRefState::Tracked,
};
tx.repo_mut()
.set_local_tag_target("tag1".as_ref(), target1.clone());
tx.repo_mut()
.set_remote_tag(remote_symbol("tag1", "git"), remote_ref1.clone());
tx.repo_mut()
.set_remote_tag(remote_symbol("tag1", "origin"), remote_ref1.clone());
let repo = tx.commit("test").block_on()?;
let mut tx = repo.start_transaction();
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(stats.changed_remote_tags.len(), 1);
assert_eq!(stats.changed_remote_tags[0].0, remote_symbol("tag1", "git"));
assert!(repo.view().get_local_tag("tag1".as_ref()).is_absent());
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "origin")),
&remote_ref1
);
Ok(())
}
#[test]
fn test_import_refs_reimport_git_head_with_fixed_ref() -> TestResult {
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 mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit(tx.repo_mut());
git_repo.reference(
"refs/heads/main",
git_id(&commit1),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
testutils::git::set_head_to_id(&git_repo, git_id(&commit1));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
testutils::git::set_head_to_id(&git_repo, git_id(&commit2));
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(commit1.id()));
assert!(tx.repo().view().heads().contains(commit2.id()));
Ok(())
}
#[test]
fn test_import_refs_reimport_all_from_root_removed() -> TestResult {
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).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(&jj_id(commit)));
git_repo.find_reference("refs/heads/main")?.delete()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(!tx.repo().view().heads().contains(&jj_id(commit)));
Ok(())
}
#[test]
fn test_import_refs_reimport_abandoning_disabled() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_repo.repo;
let git_repo = get_git_repo(repo);
let import_options = GitImportOptions {
abandon_unreachable_commits: false,
..default_import_options()
};
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
let commit2 = empty_git_commit(&git_repo, "refs/heads/delete-me", &[commit1]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(&jj_id(commit2)));
git_repo.find_reference("refs/heads/delete-me")?.delete()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
assert!(tx.repo().view().heads().contains(&jj_id(commit2)));
Ok(())
}
#[test]
fn test_import_refs_reimport_conflicted_remote_bookmark() -> TestResult {
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/commit1", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit1);
let mut tx1 = repo.start_transaction();
git::import_refs(tx1.repo_mut(), &import_options).block_on()?;
let commit2 = empty_git_commit(&git_repo, "refs/heads/commit2", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit2);
let mut tx2 = repo.start_transaction();
git::import_refs(tx2.repo_mut(), &import_options).block_on()?;
let repo = commit_transactions(vec![tx1, tx2]);
assert_eq!(
repo.view().get_git_ref("refs/remotes/origin/main".as_ref()),
&RefTarget::from_legacy_form([], [jj_id(commit1), jj_id(commit2)]),
);
assert_eq!(
repo.view()
.get_remote_bookmark(remote_symbol("main", "origin")),
&RemoteRef {
target: RefTarget::from_legacy_form([], [jj_id(commit1), jj_id(commit2)]),
state: RemoteRefState::New,
},
);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(
repo.view().get_git_ref("refs/remotes/origin/main".as_ref()),
&RefTarget::normal(jj_id(commit2)),
);
assert_eq!(
repo.view()
.get_remote_bookmark(remote_symbol("main", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit2)),
state: RemoteRefState::New,
},
);
Ok(())
}
#[test]
fn test_import_refs_reserved_remote_name() -> TestResult {
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();
empty_git_commit(&git_repo, "refs/remotes/git/main", &[]);
empty_git_commit(&git_repo, "refs/remotes/gita/main", &[]);
let mut tx = repo.start_transaction();
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
assert_eq!(stats.failed_ref_names, ["refs/remotes/git/main"]);
let view = tx.repo().view();
assert_eq!(
view.git_refs().keys().collect_vec(),
["refs/remotes/gita/main"]
);
assert_eq!(
view.all_remote_bookmarks()
.map(|(symbol, _)| symbol)
.collect_vec(),
[remote_symbol("main", "gita")]
);
Ok(())
}
#[test]
fn test_import_some_refs() -> TestResult {
let test_workspace = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_workspace.repo;
let git_repo = get_git_repo(repo);
let import_options = auto_track_import_options();
let commit_main = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
let commit_feat1 = empty_git_commit(&git_repo, "refs/remotes/origin/feature1", &[commit_main]);
let commit_feat2 = empty_git_commit(&git_repo, "refs/remotes/origin/feature2", &[commit_feat1]);
let commit_feat3 = empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[commit_feat1]);
let commit_feat4 = empty_git_commit(&git_repo, "refs/remotes/origin/feature4", &[commit_feat3]);
let commit_ign = empty_git_commit(&git_repo, "refs/remotes/origin/ignored", &[]);
let mut tx = repo.start_transaction();
git::import_some_refs(tx.repo_mut(), &import_options, |kind, symbol| {
kind == GitRefKind::Bookmark
&& symbol.remote == "origin"
&& symbol.name.as_str().starts_with("feature")
})
.block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
let expected_heads = hashset! {
jj_id(commit_feat2),
jj_id(commit_feat4),
};
assert_eq!(*view.heads(), expected_heads);
assert_eq!(view.bookmarks().count(), 4);
let commit_feat1_remote_ref = RemoteRef {
target: RefTarget::normal(jj_id(commit_feat1)),
state: RemoteRefState::Tracked,
};
let commit_feat2_remote_ref = RemoteRef {
target: RefTarget::normal(jj_id(commit_feat2)),
state: RemoteRefState::Tracked,
};
let commit_feat3_remote_ref = RemoteRef {
target: RefTarget::normal(jj_id(commit_feat3)),
state: RemoteRefState::Tracked,
};
let commit_feat4_remote_ref = RemoteRef {
target: RefTarget::normal(jj_id(commit_feat4)),
state: RemoteRefState::Tracked,
};
assert_eq!(
view.get_local_bookmark("feature1".as_ref()),
&RefTarget::normal(jj_id(commit_feat1))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature1", "origin")),
&commit_feat1_remote_ref
);
assert_eq!(
view.get_local_bookmark("feature2".as_ref()),
&RefTarget::normal(jj_id(commit_feat2))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature2", "origin")),
&commit_feat2_remote_ref
);
assert_eq!(
view.get_local_bookmark("feature3".as_ref()),
&RefTarget::normal(jj_id(commit_feat3))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature3", "origin")),
&commit_feat3_remote_ref
);
assert_eq!(
view.get_local_bookmark("feature4".as_ref()),
&RefTarget::normal(jj_id(commit_feat4))
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("feature4", "origin")),
&commit_feat4_remote_ref
);
assert!(view.get_local_bookmark("main".as_ref()).is_absent());
assert_eq!(
view.get_remote_bookmark(remote_symbol("main", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent_ref()
);
assert!(!view.heads().contains(&jj_id(commit_main)));
assert!(view.get_local_bookmark("ignored".as_ref()).is_absent());
assert_eq!(
view.get_remote_bookmark(remote_symbol("ignored", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("ignored", "origin")),
RemoteRef::absent_ref()
);
assert!(!view.heads().contains(&jj_id(commit_ign)));
delete_git_ref(&git_repo, "refs/remotes/origin/feature1");
delete_git_ref(&git_repo, "refs/remotes/origin/feature3");
delete_git_ref(&git_repo, "refs/remotes/origin/feature4");
let mut tx = repo.start_transaction();
git::import_some_refs(tx.repo_mut(), &import_options, |kind, symbol| {
kind == GitRefKind::Bookmark && symbol.remote == "origin" && symbol.name == "feature2"
})
.block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 4);
assert_eq!(*view.heads(), expected_heads);
let mut tx = repo.start_transaction();
git::import_some_refs(tx.repo_mut(), &import_options, |kind, symbol| {
kind == GitRefKind::Bookmark && symbol.remote == "origin" && symbol.name == "feature1"
})
.block_on()?;
assert_eq!(tx.repo_mut().rebase_descendants().block_on()?, 0);
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 3);
assert_eq!(*view.heads(), expected_heads);
let mut tx = repo.start_transaction();
git::import_some_refs(tx.repo_mut(), &import_options, |kind, symbol| {
kind == GitRefKind::Bookmark && symbol.remote == "origin" && symbol.name == "feature3"
})
.block_on()?;
assert_eq!(tx.repo_mut().rebase_descendants().block_on()?, 0);
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 2);
assert_eq!(*view.heads(), expected_heads);
let mut tx = repo.start_transaction();
git::import_some_refs(tx.repo_mut(), &import_options, |kind, symbol| {
kind == GitRefKind::Bookmark && symbol.remote == "origin" && symbol.name == "feature4"
})
.block_on()?;
assert_eq!(tx.repo_mut().rebase_descendants().block_on()?, 0);
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert_eq!(view.bookmarks().count(), 1);
let expected_heads = hashset! {
jj_id(commit_feat2),
};
assert_eq!(*view.heads(), expected_heads);
Ok(())
}
fn git_ref(git_repo: &gix::Repository, name: &str, target: gix::ObjectId) {
git_repo
.reference(name, target, gix::refs::transaction::PreviousValue::Any, "")
.unwrap();
}
fn delete_git_ref(git_repo: &gix::Repository, name: &str) {
git_repo.find_reference(name).unwrap().delete().unwrap();
}
struct GitRepoData {
_temp_dir: TempDir,
origin_repo: gix::Repository,
git_repo: gix::Repository,
repo: Arc<ReadonlyRepo>,
}
impl GitRepoData {
fn create() -> Self {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let origin_repo_dir = temp_dir.path().join("source");
let origin_repo = testutils::git::init_bare(&origin_repo_dir);
let git_repo_dir = temp_dir.path().join("git");
let git_repo =
testutils::git::clone(&git_repo_dir, origin_repo_dir.to_str().unwrap(), None);
let jj_repo_dir = temp_dir.path().join("jj");
std::fs::create_dir(&jj_repo_dir).unwrap();
let repo = ReadonlyRepo::init(
&settings,
&jj_repo_dir,
&|settings, store_path| {
Ok(Box::new(GitBackend::init_external(
settings,
store_path,
git_repo.path(),
)?))
},
Signer::from_settings(&settings).unwrap(),
ReadonlyRepo::default_op_store_initializer(),
ReadonlyRepo::default_op_heads_store_initializer(),
ReadonlyRepo::default_index_store_initializer(),
ReadonlyRepo::default_submodule_store_initializer(),
)
.block_on()
.unwrap();
Self {
_temp_dir: temp_dir,
origin_repo,
git_repo,
repo,
}
}
}
#[test]
fn test_import_refs_empty_git_repo() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let heads_before = test_data.repo.view().heads().clone();
let mut tx = test_data.repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(*repo.view().heads(), heads_before);
assert_eq!(repo.view().bookmarks().count(), 0);
assert_eq!(repo.view().local_tags().count(), 0);
assert_eq!(repo.view().git_refs().len(), 0);
assert_eq!(repo.view().git_head(), RefTarget::absent_ref());
Ok(())
}
#[test]
fn test_import_refs_missing_git_commit() -> TestResult {
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", &[]);
let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[commit1]);
let shard = hex_util::encode_hex(&commit1.as_bytes()[..1]);
let object_basename = hex_util::encode_hex(&commit1.as_bytes()[1..]);
let object_store_path = git_repo.path().join("objects");
let object_file = object_store_path.join(&shard).join(object_basename);
let backup_object_file = object_store_path.join(&shard).join("backup");
assert!(object_file.exists());
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/unborn");
fs::rename(&object_file, &backup_object_file)?;
let mut tx = repo.start_transaction();
let result = git::import_refs(tx.repo_mut(), &import_options).block_on();
assert_matches!(
result,
Err(GitImportError::MissingRefAncestor {
symbol,
err: BackendError::ObjectNotFound { .. }
}) if symbol == remote_symbol("main", "git")
);
git_repo.find_reference("refs/heads/main")?.delete()?;
testutils::git::set_head_to_id(&git_repo, commit2);
let mut tx = repo.start_transaction();
let result = git::import_head(tx.repo_mut()).block_on();
assert_matches!(
result,
Err(GitImportError::MissingHeadTarget {
id,
err: BackendError::ObjectNotFound { .. }
}) if id == jj_id(commit2)
);
fs::rename(&backup_object_file, &object_file)?;
git_repo.reference(
"refs/heads/main",
commit1,
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/unborn");
fs::rename(&object_file, &backup_object_file)?;
let mut tx = repo.start_transaction();
let result = git::import_refs(tx.repo_mut(), &import_options).block_on();
assert!(result.is_ok());
fs::rename(&backup_object_file, &object_file)?;
git_repo.find_reference("refs/heads/main")?.delete()?;
testutils::git::set_head_to_id(&git_repo, commit1);
fs::rename(&object_file, &backup_object_file)?;
let mut tx = repo.start_transaction();
let result = git::import_head(tx.repo_mut()).block_on();
assert!(result.is_ok());
Ok(())
}
#[test]
fn test_import_refs_detached_head() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let commit1 = empty_git_commit(&test_data.git_repo, "refs/heads/main", &[]);
test_data
.git_repo
.find_reference("refs/heads/main")?
.delete()?;
testutils::git::set_head_to_id(&test_data.git_repo, commit1);
let mut tx = test_data.repo.start_transaction();
git::import_head(tx.repo_mut()).block_on()?;
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let expected_heads = hashset! { jj_id(commit1) };
assert_eq!(*repo.view().heads(), expected_heads);
assert_eq!(repo.view().git_refs().len(), 0);
assert_eq!(repo.view().git_head(), &RefTarget::normal(jj_id(commit1)));
Ok(())
}
#[test]
fn test_export_refs_no_detach() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(jj_id(commit1))
);
assert_eq!(git_repo.head_name()?.unwrap().as_bstr(), b"refs/heads/main");
assert_eq!(
git_repo.find_reference("refs/heads/main")?.target().id(),
commit1
);
Ok(())
}
#[test]
fn test_export_refs_bookmark_changed() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_repo.reference(
"refs/heads/feature",
commit,
gix::refs::transaction::PreviousValue::MustNotExist,
"test",
)?;
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/feature");
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit)])
.write_unwrap();
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(new_commit.id().clone()));
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(new_commit.id().clone())
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")?
.peel_to_commit()?
.id(),
git_id(&new_commit)
);
assert_eq!(
git_repo.head_name()?.unwrap().as_bstr(),
b"refs/heads/feature"
);
Ok(())
}
#[test]
fn test_export_refs_tag_changed() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit = empty_git_commit(&git_repo, "refs/tags/lightweight-change", &[]);
let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
git_repo.tag_reference("lightweight-delete", commit, constraint)?;
for name in ["annotated-change", "annotated-delete"] {
let kind = gix::object::Kind::Commit;
let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
git_repo.tag(name, commit, kind, None, "", constraint)?;
}
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
let stats = git::import_refs(mut_repo, &import_options).block_on()?;
assert_eq!(stats.changed_remote_tags.len(), 4);
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit)])
.write_unwrap();
let new_target = RefTarget::normal(new_commit.id().clone());
mut_repo.set_local_tag_target("lightweight-change".as_ref(), new_target.clone());
mut_repo.set_local_tag_target("lightweight-delete".as_ref(), RefTarget::absent());
mut_repo.set_local_tag_target("annotated-change".as_ref(), new_target.clone());
mut_repo.set_local_tag_target("annotated-delete".as_ref(), RefTarget::absent());
mut_repo.set_local_tag_target("new".as_ref(), new_target.clone());
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/tags/lightweight-change".as_ref()),
new_target
);
assert_eq!(
mut_repo.get_git_ref("refs/tags/lightweight-delete".as_ref()),
RefTarget::absent()
);
assert_eq!(
mut_repo.get_git_ref("refs/tags/annotated-change".as_ref()),
new_target
);
assert_eq!(
mut_repo.get_git_ref("refs/tags/annotated-delete".as_ref()),
RefTarget::absent()
);
assert_eq!(mut_repo.get_git_ref("refs/tags/new".as_ref()), new_target);
assert_eq!(
git_repo
.find_reference("refs/tags/lightweight-change")?
.peel_to_commit()?
.id(),
git_id(&new_commit)
);
assert!(
git_repo
.try_find_reference("refs/tags/lightweight-delete")?
.is_none()
);
assert_eq!(
git_repo
.find_reference("refs/tags/annotated-change")?
.peel_to_commit()?
.id(),
git_id(&new_commit)
);
assert!(
git_repo
.try_find_reference("refs/tags/annotated-delete")?
.is_none()
);
assert_eq!(
git_repo
.find_reference("refs/tags/new")?
.peel_to_commit()?
.id(),
git_id(&new_commit)
);
Ok(())
}
#[test]
fn test_export_refs_current_bookmark_changed() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit1)])
.write_unwrap();
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(new_commit.id().clone()));
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(new_commit.id().clone())
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")?
.peel_to_commit()?
.id()
.detach(),
git_id(&new_commit)
);
assert!(git_repo.head()?.is_detached(), "HEAD is detached");
Ok(())
}
#[test]
fn test_export_refs_worktree_head_changed() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let worktree_dir = test_data._temp_dir.path().join("git-wt");
let git_workdir = git_repo.workdir().expect("git repo must have workdir");
let output = std::process::Command::new("git")
.args(["worktree", "add", "-b", "wt-branch"])
.arg(&worktree_dir)
.current_dir(git_workdir)
.output()?;
assert!(
output.status.success(),
"Failed to create worktree: {}",
String::from_utf8_lossy(&output.stderr)
);
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit1)])
.write_unwrap();
mut_repo.set_local_bookmark_target(
"wt-branch".as_ref(),
RefTarget::normal(new_commit.id().clone()),
);
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let git_repo_wt = gix::open(&worktree_dir)?;
assert!(git_repo_wt.head()?.is_detached());
Ok(())
}
#[test]
fn test_export_refs_worktree_no_detach() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let worktree_dir = test_data._temp_dir.path().join("git-wt");
let git_workdir = git_repo.workdir().expect("git repo must have workdir");
let output = std::process::Command::new("git")
.args(["worktree", "add", "-b", "wt-branch"])
.arg(&worktree_dir)
.current_dir(git_workdir)
.output()?;
assert!(
output.status.success(),
"Failed to create worktree: {}",
String::from_utf8_lossy(&output.stderr)
);
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit1)])
.write_unwrap();
mut_repo.set_local_bookmark_target(
"other-branch".as_ref(),
RefTarget::normal(new_commit.id().clone()),
);
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let git_repo_wt = gix::open(&worktree_dir)?;
assert!(!git_repo_wt.head()?.is_detached());
assert_eq!(
git_repo_wt.head_name()?.unwrap().as_bstr(),
b"refs/heads/wt-branch"
);
Ok(())
}
#[test]
fn test_export_refs_current_tag_changed() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/tags/v1.0");
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
let new_commit = create_random_commit(mut_repo)
.set_parents(vec![jj_id(commit1)])
.write_unwrap();
mut_repo.set_local_tag_target("v1.0".as_ref(), RefTarget::normal(new_commit.id().clone()));
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/tags/v1.0".as_ref()),
RefTarget::normal(new_commit.id().clone())
);
assert_eq!(
git_repo
.find_reference("refs/tags/v1.0")?
.peel_to_commit()?
.id()
.detach(),
git_id(&new_commit)
);
assert!(git_repo.head()?.is_detached());
Ok(())
}
#[test_case(false; "without moved placeholder ref")]
#[test_case(true; "with moved placeholder ref")]
fn test_export_refs_unborn_git_bookmark(move_placeholder_ref: bool) -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_head(mut_repo).block_on()?;
git::import_refs(mut_repo, &import_options).block_on()?;
mut_repo.rebase_descendants().block_on()?;
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert!(git_repo.head()?.is_unborn(), "HEAD is unborn");
let new_commit = write_random_commit(mut_repo);
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(new_commit.id().clone()));
if move_placeholder_ref {
git_repo.reference(
"refs/jj/root",
git_id(&new_commit),
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
}
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(new_commit.id().clone())
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")?
.peel_to_commit()?
.id(),
git_id(&new_commit)
);
assert!(git_repo.head()?.is_unborn(), "HEAD is unborn");
assert!(git_repo.find_reference("refs/jj/root").is_err());
Ok(())
}
#[test]
fn test_export_import_sequence() -> TestResult {
let test_data = GitRepoData::create();
let import_options = default_import_options();
let git_repo = test_data.git_repo;
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
let commit_a = write_random_commit(mut_repo);
let commit_b = write_random_commit(mut_repo);
let commit_c = write_random_commit(mut_repo);
git_repo.reference(
"refs/heads/main",
git_id(&commit_a),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
git::import_refs(mut_repo, &import_options).block_on()?;
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(commit_a.id().clone())
);
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(commit_b.id().clone()));
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(commit_b.id().clone())
);
git_repo.reference(
"refs/heads/main",
git_id(&commit_c),
gix::refs::transaction::PreviousValue::Any,
"test",
)?;
git::import_refs(mut_repo, &import_options).block_on()?;
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::normal(commit_c.id().clone())
);
assert_eq!(
mut_repo.view().get_local_bookmark("main".as_ref()),
&RefTarget::normal(commit_c.id().clone())
);
Ok(())
}
#[test]
fn test_import_export_non_tracking_bookmark() -> TestResult {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let commit_main_t0 = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
git::import_refs(mut_repo, &default_import_options()).block_on()?;
assert!(
mut_repo
.view()
.get_local_bookmark("main".as_ref())
.is_absent()
);
assert_eq!(
mut_repo
.view()
.get_remote_bookmark(remote_symbol("main", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_main_t0)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo.get_git_ref("refs/remotes/origin/main".as_ref()),
RefTarget::normal(jj_id(commit_main_t0))
);
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
mut_repo.get_git_ref("refs/heads/main".as_ref()),
RefTarget::absent()
);
let commit_main_t1 = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[commit_main_t0]);
let commit_feat_t1 = empty_git_commit(&git_repo, "refs/remotes/origin/feat", &[]);
git::import_refs(mut_repo, &auto_track_import_options()).block_on()?;
assert!(
mut_repo
.view()
.get_local_bookmark("main".as_ref())
.is_absent()
);
assert_eq!(
mut_repo.view().get_local_bookmark("feat".as_ref()),
&RefTarget::normal(jj_id(commit_feat_t1))
);
assert_eq!(
mut_repo
.view()
.get_remote_bookmark(remote_symbol("main", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_main_t1)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo
.view()
.get_remote_bookmark(remote_symbol("feat", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_feat_t1)),
state: RemoteRefState::Tracked,
},
);
let commit_main_t2 = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[commit_main_t1]);
let commit_feat_t2 = empty_git_commit(&git_repo, "refs/remotes/origin/feat", &[commit_feat_t1]);
git::import_refs(mut_repo, &default_import_options()).block_on()?;
assert!(
mut_repo
.view()
.get_local_bookmark("main".as_ref())
.is_absent()
);
assert_eq!(
mut_repo.view().get_local_bookmark("feat".as_ref()),
&RefTarget::normal(jj_id(commit_feat_t2))
);
assert_eq!(
mut_repo
.view()
.get_remote_bookmark(remote_symbol("main", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_main_t2)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo
.view()
.get_remote_bookmark(remote_symbol("feat", "origin")),
&RemoteRef {
target: RefTarget::normal(jj_id(commit_feat_t2)),
state: RemoteRefState::Tracked,
},
);
Ok(())
}
#[test]
fn test_export_conflicts() -> TestResult {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
let commit_a = write_random_commit(mut_repo);
let commit_b = write_random_commit(mut_repo);
let commit_c = write_random_commit(mut_repo);
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(commit_a.id().clone()));
mut_repo
.set_local_bookmark_target("feature".as_ref(), RefTarget::normal(commit_a.id().clone()));
mut_repo.set_local_tag_target("v1.0".as_ref(), RefTarget::normal(commit_a.id().clone()));
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::normal(commit_b.id().clone()));
let conflict_target = RefTarget::from_legacy_form(
[commit_a.id().clone()],
[commit_b.id().clone(), commit_c.id().clone()],
);
mut_repo.set_local_bookmark_target("feature".as_ref(), conflict_target.clone());
mut_repo.set_local_tag_target("v1.0".as_ref(), conflict_target.clone());
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
git_repo.find_reference("refs/heads/feature")?.target().id(),
git_id(&commit_a)
);
assert_eq!(
git_repo.find_reference("refs/heads/main")?.target().id(),
git_id(&commit_b)
);
assert_eq!(
git_repo.find_reference("refs/tags/v1.0")?.target().id(),
git_id(&commit_a)
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("feature", "git")),
RemoteRef {
target: RefTarget::normal(commit_a.id().clone()),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main", "git")),
RemoteRef {
target: RefTarget::normal(commit_b.id().clone()),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
mut_repo.get_remote_tag(remote_symbol("v1.0", "git")),
RemoteRef {
target: RefTarget::normal(commit_a.id().clone()),
state: RemoteRefState::Tracked,
},
);
Ok(())
}
#[test]
fn test_export_bookmark_on_root_commit() -> TestResult {
let test_data = GitRepoData::create();
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
mut_repo.set_local_bookmark_target(
"on_root".as_ref(),
RefTarget::normal(mut_repo.store().root_commit_id().clone()),
);
let stats = git::export_refs(mut_repo)?;
assert_eq!(stats.failed_bookmarks.len(), 1);
assert_eq!(
stats.failed_bookmarks[0].0.as_ref(),
remote_symbol("on_root", "git")
);
assert_matches!(
stats.failed_bookmarks[0].1,
FailedRefExportReason::OnRootCommit
);
assert!(stats.failed_tags.is_empty());
Ok(())
}
#[test]
fn test_export_partial_failure() -> TestResult {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
let commit_a = write_random_commit(mut_repo);
let target = RefTarget::normal(commit_a.id().clone());
mut_repo.set_local_bookmark_target("".as_ref(), target.clone());
mut_repo.set_local_tag_target("".as_ref(), target.clone());
mut_repo.set_local_bookmark_target("HEAD".as_ref(), target.clone());
mut_repo.set_local_bookmark_target("main".as_ref(), target.clone());
mut_repo.set_local_bookmark_target("main/sub".as_ref(), target.clone());
mut_repo.set_remote_tag(
remote_symbol("v1.0", "origin"),
RemoteRef {
target: target.clone(),
state: RemoteRefState::Tracked,
},
);
let stats = git::export_refs(mut_repo)?;
assert_eq!(stats.failed_bookmarks.len(), 3);
assert_eq!(
stats.failed_bookmarks[0].0.as_ref(),
remote_symbol("", "git")
);
assert_matches!(
stats.failed_bookmarks[0].1,
FailedRefExportReason::InvalidGitName
);
assert_eq!(
stats.failed_bookmarks[1].0.as_ref(),
remote_symbol("HEAD", "git")
);
assert_matches!(
stats.failed_bookmarks[1].1,
FailedRefExportReason::InvalidGitName
);
assert_eq!(
stats.failed_bookmarks[2].0.as_ref(),
remote_symbol("main/sub", "git")
);
assert_matches!(
stats.failed_bookmarks[2].1,
FailedRefExportReason::FailedToSet(_)
);
assert_eq!(stats.failed_tags.len(), 1);
assert_eq!(stats.failed_tags[0].0.as_ref(), remote_symbol("", "git"));
assert_matches!(
stats.failed_tags[0].1,
FailedRefExportReason::InvalidGitName
);
assert!(git_repo.find_reference("refs/heads/").is_err());
assert!(git_repo.find_reference("refs/heads/HEAD").is_err());
assert_eq!(
git_repo.find_reference("refs/heads/main")?.target().id(),
git_id(&commit_a)
);
assert!(git_repo.find_reference("refs/heads/main/sub").is_err());
assert!(git_repo.find_reference("refs/tags/").is_err());
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("HEAD", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main", "git")),
RemoteRef {
target: target.clone(),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main/sub", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_tag(remote_symbol("", "git")),
RemoteRef::absent()
);
mut_repo.set_local_bookmark_target("main".as_ref(), RefTarget::absent());
let stats = git::export_refs(mut_repo)?;
assert_eq!(stats.failed_bookmarks.len(), 2);
assert_eq!(
stats.failed_bookmarks[0].0.as_ref(),
remote_symbol("", "git")
);
assert_matches!(
stats.failed_bookmarks[0].1,
FailedRefExportReason::InvalidGitName
);
assert_eq!(
stats.failed_bookmarks[1].0.as_ref(),
remote_symbol("HEAD", "git")
);
assert_matches!(
stats.failed_bookmarks[1].1,
FailedRefExportReason::InvalidGitName
);
assert_eq!(stats.failed_tags.len(), 1);
assert_eq!(stats.failed_tags[0].0.as_ref(), remote_symbol("", "git"));
assert_matches!(
stats.failed_tags[0].1,
FailedRefExportReason::InvalidGitName
);
assert!(git_repo.find_reference("refs/heads/").is_err());
assert!(git_repo.find_reference("refs/heads/HEAD").is_err());
assert!(git_repo.find_reference("refs/heads/main").is_err());
assert_eq!(
git_repo
.find_reference("refs/heads/main/sub")?
.target()
.id(),
git_id(&commit_a)
);
assert!(git_repo.find_reference("refs/tags/").is_err());
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("HEAD", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main", "git")),
RemoteRef::absent()
);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main/sub", "git")),
RemoteRef {
target: target.clone(),
state: RemoteRefState::Tracked,
},
);
assert_eq!(
mut_repo.get_remote_tag(remote_symbol("", "git")),
RemoteRef::absent()
);
Ok(())
}
#[test]
fn test_export_reexport_transitions() -> TestResult {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
let commit_a = write_random_commit(mut_repo);
let commit_b = write_random_commit(mut_repo);
let commit_c = write_random_commit(mut_repo);
for bookmark in [
"AAB", "AAX", "ABA", "ABB", "ABC", "ABX", "AXA", "AXB", "AXX",
] {
mut_repo
.set_local_bookmark_target(bookmark.as_ref(), RefTarget::normal(commit_a.id().clone()));
}
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
for bookmark in ["AXA", "AXB", "AXX"] {
mut_repo.set_local_bookmark_target(bookmark.as_ref(), RefTarget::absent());
}
for bookmark in ["XAA", "XAB", "XAX"] {
mut_repo
.set_local_bookmark_target(bookmark.as_ref(), RefTarget::normal(commit_a.id().clone()));
}
for bookmark in ["ABA", "ABB", "ABC", "ABX"] {
mut_repo
.set_local_bookmark_target(bookmark.as_ref(), RefTarget::normal(commit_b.id().clone()));
}
for bookmark in ["AAX", "ABX", "AXX"] {
git_repo
.find_reference(&format!("refs/heads/{bookmark}"))?
.delete()?;
}
for bookmark in ["XAA", "XXA"] {
git_repo.reference(
format!("refs/heads/{bookmark}"),
git_id(&commit_a),
gix::refs::transaction::PreviousValue::Any,
"",
)?;
}
for bookmark in ["AAB", "ABB", "AXB", "XAB"] {
git_repo.reference(
format!("refs/heads/{bookmark}"),
git_id(&commit_b),
gix::refs::transaction::PreviousValue::Any,
"",
)?;
}
let bookmark = "ABC";
git_repo.reference(
format!("refs/heads/{bookmark}"),
git_id(&commit_c),
gix::refs::transaction::PreviousValue::Any,
"",
)?;
let stats = git::export_refs(mut_repo)?;
assert_eq!(
stats
.failed_bookmarks
.into_iter()
.map(|(symbol, _)| symbol)
.collect_vec(),
vec!["ABC", "ABX", "AXB", "XAB"]
.into_iter()
.map(|s| remote_symbol(s, "git").to_owned())
.collect_vec()
);
for bookmark in ["AAX", "ABX", "AXA", "AXX"] {
assert!(
git_repo
.find_reference(&format!("refs/heads/{bookmark}"))
.is_err(),
"{bookmark} should not exist"
);
}
for bookmark in ["XAA", "XAX", "XXA"] {
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{bookmark}"))?
.target()
.id(),
git_id(&commit_a),
"{bookmark} should point to commit A"
);
}
for bookmark in ["AAB", "ABA", "AAB", "ABB", "AXB", "XAB"] {
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{bookmark}"))?
.target()
.id(),
git_id(&commit_b),
"{bookmark} should point to commit B"
);
}
let bookmark = "ABC";
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{bookmark}"))?
.target()
.id(),
git_id(&commit_c),
"{bookmark} should point to commit C"
);
assert_eq!(
*mut_repo.view().git_refs(),
btreemap! {
"refs/heads/AAX".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/AAB".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/ABA".into() => RefTarget::normal(commit_b.id().clone()),
"refs/heads/ABB".into() => RefTarget::normal(commit_b.id().clone()),
"refs/heads/ABC".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/ABX".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/AXB".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/XAA".into() => RefTarget::normal(commit_a.id().clone()),
"refs/heads/XAX".into() => RefTarget::normal(commit_a.id().clone()),
}
);
Ok(())
}
#[test]
fn test_export_undo_reexport() -> TestResult {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data.repo.start_transaction();
let mut_repo = tx.repo_mut();
let commit_a = write_random_commit(mut_repo);
let target_a = RefTarget::normal(commit_a.id().clone());
let remote_ref_a = RemoteRef {
target: target_a.clone(),
state: RemoteRefState::Tracked,
};
mut_repo.set_local_bookmark_target("main".as_ref(), target_a.clone());
mut_repo.set_local_tag_target("v1.0".as_ref(), target_a.clone());
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
git_repo.find_reference("refs/heads/main")?.target().id(),
git_id(&commit_a)
);
assert_eq!(
git_repo.find_reference("refs/tags/v1.0")?.target().id(),
git_id(&commit_a)
);
assert_eq!(mut_repo.get_git_ref("refs/heads/main".as_ref()), target_a);
assert_eq!(mut_repo.get_git_ref("refs/tags/v1.0".as_ref()), target_a);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main", "git")),
remote_ref_a
);
assert_eq!(
mut_repo.get_remote_tag(remote_symbol("v1.0", "git")),
remote_ref_a
);
mut_repo.set_remote_bookmark(remote_symbol("main", "git"), RemoteRef::absent());
mut_repo.set_remote_tag(remote_symbol("v1.0", "git"), RemoteRef::absent());
let stats = git::export_refs(mut_repo)?;
assert!(stats.failed_bookmarks.is_empty());
assert!(stats.failed_tags.is_empty());
assert_eq!(
git_repo.find_reference("refs/heads/main")?.target().id(),
git_id(&commit_a)
);
assert_eq!(
git_repo.find_reference("refs/tags/v1.0")?.target().id(),
git_id(&commit_a)
);
assert_eq!(mut_repo.get_git_ref("refs/heads/main".as_ref()), target_a);
assert_eq!(mut_repo.get_git_ref("refs/tags/v1.0".as_ref()), target_a);
assert_eq!(
mut_repo.get_remote_bookmark(remote_symbol("main", "git")),
remote_ref_a
);
assert_eq!(
mut_repo.get_remote_tag(remote_symbol("v1.0", "git")),
remote_ref_a
);
Ok(())
}
#[test]
fn test_reset_head_to_root() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
let git_repo = testutils::git::init(&workspace_root);
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let mut_repo = tx.repo_mut();
let root_commit_id = repo.store().root_commit_id();
let tree = repo.store().empty_merged_tree();
let commit1 = mut_repo
.new_commit(vec![root_commit_id.clone()], tree.clone())
.write_unwrap();
let commit2 = mut_repo
.new_commit(vec![commit1.id().clone()], tree.clone())
.write_unwrap();
git::reset_head(tx.repo_mut(), &commit2).block_on()?;
assert!(git_repo.head()?.is_detached(), "HEAD is detached");
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit1.id().clone())
);
git::reset_head(tx.repo_mut(), &commit1).block_on()?;
assert!(git_repo.head()?.is_unborn(), "HEAD is unborn");
assert!(tx.repo().git_head().is_absent());
git_repo.reference(
"refs/jj/root",
git_id(&commit1),
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
git::reset_head(tx.repo_mut(), &commit2).block_on()?;
assert!(git_repo.head_id().is_ok());
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit1.id().clone())
);
assert!(git_repo.find_reference("refs/jj/root").is_ok());
git::reset_head(tx.repo_mut(), &commit1).block_on()?;
assert!(git_repo.head()?.is_unborn(), "HEAD is unborn");
assert!(tx.repo().git_head().is_absent());
assert!(git_repo.find_reference("refs/jj/root").is_err());
Ok(())
}
#[test]
fn test_reset_head_detached_out_of_sync() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
let git_repo = testutils::git::init(&workspace_root);
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);
let commit5 = write_random_commit(tx.repo_mut());
git::reset_head(tx.repo_mut(), &commit2).block_on()?;
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit1.id().clone())
);
testutils::git::set_head_to_id(
&git_repo,
gix::ObjectId::from_bytes_or_panic(commit5.id().as_bytes()),
);
git::reset_head(tx.repo_mut(), &commit3).block_on()?;
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit1.id().clone())
);
assert_matches!(
git::reset_head(tx.repo_mut(), &commit4).block_on(),
Err(GitResetHeadError::UpdateHeadRef(_))
);
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit1.id().clone()),
"view shouldn't be updated on failed export"
);
git::import_head(tx.repo_mut()).block_on()?;
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit5.id().clone())
);
git::reset_head(tx.repo_mut(), &commit4).block_on()?;
assert_eq!(
tx.repo().git_head(),
RefTarget::normal(commit3.id().clone())
);
Ok(())
}
fn get_index_state(workspace_root: &Path) -> String {
let git_repo = gix::open(workspace_root).unwrap();
let index = git_repo.index().unwrap();
index
.entries()
.iter()
.map(|entry| {
format!(
"{:?} {} {:?}\n",
entry.flags.stage(),
entry.path_in(index.path_backing()),
entry.mode
)
})
.join("")
}
#[test]
fn test_reset_head_with_index() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
let git_repo = testutils::git::init(&workspace_root);
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let mut_repo = tx.repo_mut();
let root_commit_id = repo.store().root_commit_id();
let tree = repo.store().empty_merged_tree();
let commit1 = mut_repo
.new_commit(vec![root_commit_id.clone()], tree.clone())
.write_unwrap();
let commit2 = mut_repo
.new_commit(vec![commit1.id().clone()], tree.clone())
.write_unwrap();
git::reset_head(tx.repo_mut(), &commit2).block_on()?;
insta::assert_snapshot!(get_index_state(&workspace_root), @"");
{
let mut index_manager = testutils::git::IndexManager::new(&git_repo);
index_manager.add_file("file.txt", b"i am a file\n");
index_manager.sync_index();
}
insta::assert_snapshot!(get_index_state(&workspace_root), @"Unconflicted file.txt Mode(FILE)");
git::reset_head(tx.repo_mut(), &commit2).block_on()?;
insta::assert_snapshot!(get_index_state(&workspace_root), @"");
Ok(())
}
#[test]
fn test_reset_head_with_index_no_conflict() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
gix::init(&workspace_root)?;
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let mut_repo = tx.repo_mut();
let tree = testutils::create_tree_with(&repo, |builder| {
builder
.file(repo_path("some/dir/normal-file"), "file\n")
.executable(false);
builder
.file(repo_path("some/dir/executable-file"), "file\n")
.executable(true);
builder.symlink(repo_path("some/dir/symlink"), "./normal-file");
builder.submodule(
repo_path("some/dir/commit"),
testutils::write_random_commit(mut_repo).id().clone(),
);
});
let parent_commit = mut_repo
.new_commit(vec![repo.store().root_commit_id().clone()], tree.clone())
.write_unwrap();
let wc_commit = mut_repo
.new_commit(vec![parent_commit.id().clone()], tree.clone())
.write_unwrap();
git::reset_head(mut_repo, &wc_commit).block_on()?;
insta::assert_snapshot!(get_index_state(&workspace_root), @"
Unconflicted some/dir/commit Mode(DIR | SYMLINK)
Unconflicted some/dir/executable-file Mode(FILE | FILE_EXECUTABLE)
Unconflicted some/dir/normal-file Mode(FILE)
Unconflicted some/dir/symlink Mode(SYMLINK)
");
Ok(())
}
#[test]
fn test_reset_head_with_index_merge_conflict() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
gix::init(&workspace_root)?;
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let mut_repo = tx.repo_mut();
let base_tree = testutils::create_tree_with(&repo, |builder| {
builder
.file(repo_path("some/dir/normal-file"), "base\n")
.executable(false);
builder
.file(repo_path("some/dir/executable-file"), "base\n")
.executable(true);
builder.symlink(repo_path("some/dir/symlink"), "./normal-file");
builder.submodule(
repo_path("some/dir/commit"),
testutils::write_random_commit(mut_repo).id().clone(),
);
});
let left_tree = testutils::create_tree_with(&repo, |builder| {
builder
.file(repo_path("some/dir/normal-file"), "left\n")
.executable(false);
builder
.file(repo_path("some/dir/executable-file"), "left\n")
.executable(true);
builder.symlink(repo_path("some/dir/symlink"), "./executable-file");
builder.submodule(
repo_path("some/dir/commit"),
testutils::write_random_commit(mut_repo).id().clone(),
);
});
let right_tree = testutils::create_tree_with(&repo, |builder| {
builder
.file(repo_path("some/dir/normal-file"), "right\n")
.executable(false);
builder
.file(repo_path("some/dir/executable-file"), "right\n")
.executable(true);
builder.symlink(repo_path("some/dir/symlink"), "./commit");
builder.submodule(
repo_path("some/dir/commit"),
testutils::write_random_commit(mut_repo).id().clone(),
);
});
let base_commit = mut_repo
.new_commit(
vec![repo.store().root_commit_id().clone()],
base_tree.clone(),
)
.write_unwrap();
let left_commit = mut_repo
.new_commit(vec![base_commit.id().clone()], left_tree.clone())
.write_unwrap();
let right_commit = mut_repo
.new_commit(vec![base_commit.id().clone()], right_tree.clone())
.write_unwrap();
let wc_commit = mut_repo
.new_commit(
vec![left_commit.id().clone(), right_commit.id().clone()],
right_tree.clone(),
)
.write_unwrap();
git::reset_head(mut_repo, &wc_commit).block_on()?;
insta::assert_snapshot!(get_index_state(&workspace_root), @"
Base some/dir/commit Mode(DIR | SYMLINK)
Ours some/dir/commit Mode(DIR | SYMLINK)
Theirs some/dir/commit Mode(DIR | SYMLINK)
Base some/dir/executable-file Mode(FILE | FILE_EXECUTABLE)
Ours some/dir/executable-file Mode(FILE | FILE_EXECUTABLE)
Theirs some/dir/executable-file Mode(FILE | FILE_EXECUTABLE)
Base some/dir/normal-file Mode(FILE)
Ours some/dir/normal-file Mode(FILE)
Theirs some/dir/normal-file Mode(FILE)
Base some/dir/symlink Mode(SYMLINK)
Ours some/dir/symlink Mode(SYMLINK)
Theirs some/dir/symlink Mode(SYMLINK)
");
Ok(())
}
#[test]
fn test_reset_head_with_index_file_directory_conflict() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let workspace_root = temp_dir.path().join("repo");
gix::init(&workspace_root)?;
let (_workspace, repo) =
Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git"))
.block_on()?;
let mut tx = repo.start_transaction();
let mut_repo = tx.repo_mut();
let left_tree = testutils::create_tree_with(&repo, |builder| {
builder.file(repo_path("test/dir/file"), "dir\n");
});
let right_tree = testutils::create_tree_with(&repo, |builder| {
builder.file(repo_path("test"), "file\n");
});
let left_commit = mut_repo
.new_commit(
vec![repo.store().root_commit_id().clone()],
left_tree.clone(),
)
.write_unwrap();
let right_commit = mut_repo
.new_commit(
vec![repo.store().root_commit_id().clone()],
right_tree.clone(),
)
.write_unwrap();
let wc_commit = mut_repo
.new_commit(
vec![left_commit.id().clone(), right_commit.id().clone()],
repo.store().empty_merged_tree().clone(),
)
.write_unwrap();
git::reset_head(mut_repo, &wc_commit).block_on()?;
insta::assert_snapshot!(get_index_state(&workspace_root), @"Theirs test Mode(FILE)");
Ok(())
}
#[test]
fn test_init() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let git_repo_dir = temp_dir.path().join("git");
let jj_repo_dir = temp_dir.path().join("jj");
let git_repo = testutils::git::init_bare(git_repo_dir);
let initial_git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
std::fs::create_dir(&jj_repo_dir)?;
let repo = &ReadonlyRepo::init(
&settings,
&jj_repo_dir,
&|settings, store_path| {
Ok(Box::new(GitBackend::init_external(
settings,
store_path,
git_repo.path(),
)?))
},
Signer::from_settings(&settings)?,
ReadonlyRepo::default_op_store_initializer(),
ReadonlyRepo::default_op_heads_store_initializer(),
ReadonlyRepo::default_index_store_initializer(),
ReadonlyRepo::default_submodule_store_initializer(),
)
.block_on()?;
assert!(!repo.view().heads().contains(&jj_id(initial_git_commit)));
Ok(())
}
#[test]
fn test_fetch_empty_repo() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
let default_branch = fetcher.get_default_branch("origin".as_ref())?;
let stats = fetcher.import_refs().block_on()?;
assert_eq!(default_branch, None);
assert!(stats.abandoned_commits.is_empty());
assert_eq!(*tx.repo().view().git_refs(), btreemap! {});
assert_eq!(tx.repo().view().bookmarks().count(), 0);
Ok(())
}
#[test]
fn test_fetch_initial_commit_head_is_not_set() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
let default_branch = fetcher.get_default_branch("origin".as_ref())?;
let stats = fetcher.import_refs().block_on()?;
assert_eq!(default_branch, None);
assert!(stats.abandoned_commits.is_empty());
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert!(view.heads().contains(&jj_id(initial_git_commit)));
let initial_commit_target = RefTarget::normal(jj_id(initial_git_commit));
let initial_commit_remote_ref = RemoteRef {
target: initial_commit_target.clone(),
state: RemoteRefState::New,
};
assert_eq!(
*view.git_refs(),
btreemap! {
"refs/remotes/origin/main".into() => initial_commit_target.clone(),
}
);
assert_eq!(
view.bookmarks().collect::<BTreeMap<_, _>>(),
btreemap! {
"main".as_ref() => LocalRemoteRefTarget {
local_target: RefTarget::absent_ref(),
remote_refs: vec![
("origin".as_ref(), &initial_commit_remote_ref),
],
},
}
);
Ok(())
}
#[test]
fn test_fetch_initial_commit_head_is_set() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
testutils::git::set_symbolic_reference(&test_data.origin_repo, "HEAD", "refs/heads/main");
let new_git_commit = empty_git_commit(
&test_data.origin_repo,
"refs/heads/main",
&[initial_git_commit],
);
test_data.origin_repo.reference(
"refs/tags/v1.0",
new_git_commit,
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
let default_branch = fetcher.get_default_branch("origin".as_ref())?;
let stats = fetcher.import_refs().block_on()?;
assert_eq!(default_branch, Some("main".into()));
assert!(stats.abandoned_commits.is_empty());
Ok(())
}
#[test]
fn test_fetch_success() -> TestResult {
let mut test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = auto_track_import_options();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options.clone(), &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
fetcher.import_refs().block_on()?;
test_data.repo = tx.commit("test").block_on()?;
testutils::git::set_symbolic_reference(&test_data.origin_repo, "HEAD", "refs/heads/main");
let new_git_commit = empty_git_commit(
&test_data.origin_repo,
"refs/heads/main",
&[initial_git_commit],
);
test_data.origin_repo.reference(
"refs/tags/v1.0",
new_git_commit,
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
let default_branch = fetcher.get_default_branch("origin".as_ref())?;
let stats = fetcher.import_refs().block_on()?;
assert_eq!(default_branch, Some("main".into()));
assert!(stats.abandoned_commits.is_empty());
let repo = tx.commit("test").block_on()?;
let view = repo.view();
assert!(view.heads().contains(&jj_id(new_git_commit)));
let new_commit_target = RefTarget::normal(jj_id(new_git_commit));
let new_commit_remote_ref = RemoteRef {
target: new_commit_target.clone(),
state: RemoteRefState::Tracked,
};
assert_eq!(
*view.git_refs(),
btreemap! {
"refs/remotes/origin/main".into() => new_commit_target.clone(),
"refs/tags/v1.0".into() => new_commit_target.clone(),
}
);
assert_eq!(
view.bookmarks().collect::<BTreeMap<_, _>>(),
btreemap! {
"main".as_ref() => LocalRemoteRefTarget {
local_target: &new_commit_target,
remote_refs: vec![
("origin".as_ref(), &new_commit_remote_ref),
],
},
}
);
assert_eq!(
view.local_tags().collect_vec(),
vec![("v1.0".as_ref(), &new_commit_target)],
);
assert_eq!(
view.all_remote_tags().collect_vec(),
vec![(remote_symbol("v1.0", "git"), &new_commit_remote_ref)]
);
Ok(())
}
#[test]
fn test_fetch_prune_deleted_ref() -> TestResult {
let test_data = GitRepoData::create();
let commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data.repo.start_transaction();
fetch_import_all(tx.repo_mut(), "origin".as_ref());
tx.repo_mut()
.track_remote_bookmark(remote_symbol("main", "origin"))?;
assert!(tx.repo().get_local_bookmark("main".as_ref()).is_present());
assert!(
tx.repo()
.get_remote_bookmark(remote_symbol("main", "origin"))
.is_present()
);
test_data
.origin_repo
.find_reference("refs/heads/main")?
.delete()?;
let stats = fetch_import_all(tx.repo_mut(), "origin".as_ref());
assert_eq!(stats.abandoned_commits, vec![jj_id(commit)]);
assert!(tx.repo().get_local_bookmark("main".as_ref()).is_absent());
assert_eq!(
tx.repo_mut()
.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent()
);
Ok(())
}
#[test]
fn test_fetch_no_default_branch() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options.clone(), &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
fetcher.import_refs().block_on()?;
empty_git_commit(
&test_data.origin_repo,
"refs/heads/main",
&[initial_git_commit],
);
testutils::git::set_head_to_id(&test_data.origin_repo, initial_git_commit);
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
let default_branch = fetcher.get_default_branch("origin".as_ref())?;
fetcher.import_refs().block_on()?;
assert_eq!(default_branch, None);
Ok(())
}
#[test]
fn test_fetch_empty_refspecs() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::none(),
tag: StringExpression::none(),
};
fetch_with(&mut fetcher, "origin".as_ref(), ref_expr)?;
fetcher.import_refs().block_on()?;
assert_eq!(
tx.repo_mut()
.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent()
);
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
assert_eq!(
tx.repo_mut()
.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent()
);
Ok(())
}
#[test]
fn test_fetch_environment_options() -> TestResult {
let temp_dir = testutils::new_temp_dir();
let test_data = GitRepoData::create();
let import_options = default_import_options();
let mut subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let trace_path = temp_dir.path().join("git-trace.log");
subprocess_options
.environment
.insert("GIT_TRACE".into(), trace_path.clone().into());
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
fetch_all_with(&mut fetcher, "origin".as_ref())?;
assert!(trace_path.exists());
Ok(())
}
#[test]
fn test_load_default_fetch_bookmarks() -> TestResult {
let mut test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let git_repo = get_git_repo(&test_repo.repo);
let config = git_repo.config_snapshot().clone();
std::fs::OpenOptions::new()
.append(true)
.open(
config
.meta()
.path
.as_ref()
.expect("failed to find config file"),
)
.expect("failed to open config file")
.write_all(
br#"
[remote "origin"]
url = /dev/null
# Valid
fetch = +refs/heads/main:refs/remotes/origin/main
fetch = +refs/heads/foo*:refs/remotes/origin/foo*
fetch = ^refs/heads/excluded
fetch = ^refs/heads/fooqux
# Invalid
fetch = +refs/heads/src-only
fetch = refs/heads/non-forced
fetch = refs/heads/non-forced:refs/remotes/origin/non-forced
fetch = +refs/heads/wrong-dst:refs/remotes/tags/wrong-dst
fetch = +refs/heads/wrong-remote:refs/remotes/origin2/wrong-remote
fetch = +refs/tags/wrong-src:refs/remotes/origin/wrong-src
fetch = ^refs/tags/unsupported
[remote "positive-only"]
url = /dev/null
fetch = +refs/heads/*:refs/remotes/positive-only/*
"#,
)
.expect("failed to update config file");
test_repo.repo = test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let git_repo = get_git_repo(&test_repo.repo);
let (IgnoredRefspecs(ignored_refspecs), bookmark_expr) =
load_default_fetch_bookmarks("origin".as_ref(), &git_repo)
.expect("failed to load refspecs");
let mut warnings = String::new();
for IgnoredRefspec { refspec, reason } in ignored_refspecs {
warnings.push_str(reason);
warnings.push_str(": ");
warnings.push_str(&String::from_utf8_lossy(&refspec));
warnings.push('\n');
}
insta::assert_snapshot!(warnings, @"
fetch-only refspecs are not supported: refs/heads/non-forced
fetch-only refspecs are not supported: refs/heads/src-only
only refs/heads/ is supported for refspec sources: ^refs/tags/unsupported
non-forced refspecs are not supported: refs/heads/non-forced:refs/remotes/origin/non-forced
remote renaming not supported: +refs/heads/wrong-dst:refs/remotes/tags/wrong-dst
remote renaming not supported: +refs/heads/wrong-remote:refs/remotes/origin2/wrong-remote
only refs/heads/ is supported for refspec sources: +refs/tags/wrong-src:refs/remotes/origin/wrong-src
");
insta::assert_debug_snapshot!(bookmark_expr, @r#"
Intersection(
Union(
Pattern(
Glob(
GlobPattern(
"foo*",
),
),
),
Pattern(
Exact(
"main",
),
),
),
NotIn(
Union(
Pattern(
Exact(
"excluded",
),
),
Pattern(
Exact(
"fooqux",
),
),
),
),
)
"#);
let (IgnoredRefspecs(ignored_refspecs), bookmark_expr) =
load_default_fetch_bookmarks("positive-only".as_ref(), &git_repo)
.expect("failed to load refspecs");
assert!(ignored_refspecs.is_empty());
insta::assert_debug_snapshot!(bookmark_expr, @r#"
Pattern(
Glob(
GlobPattern(
"*",
),
),
)
"#);
Ok(())
}
#[test]
fn test_load_default_fetch_bookmarks_invalid_configuration() -> TestResult {
let mut test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let git_repo = get_git_repo(&test_repo.repo);
let config = git_repo.config_snapshot().clone();
std::fs::OpenOptions::new()
.append(true)
.open(
config
.meta()
.path
.as_ref()
.expect("failed to find config file"),
)
.expect("failed to open config file")
.write_all(
br#"
[remote "first"]
url = /dev/null
fetch = +refs/heads/bad*pattern*:refs/remotes/heads/bad*pattern*
[remote "second"]
url = /dev/null
fetch = +refs/heads/badpattern?:refs/remotes/heads/badpattern?
[remote "third"]
url = /dev/null
fetch = +refs/heads/bad[pat]:refs/remotes/heads/bad[pat]
"#,
)
.expect("failed to update config file");
test_repo.repo = test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let git_repo = get_git_repo(&test_repo.repo);
let first_err = load_default_fetch_bookmarks("first".as_ref(), &git_repo).unwrap_err();
let second_err = load_default_fetch_bookmarks("second".as_ref(), &git_repo).unwrap_err();
let third_err = load_default_fetch_bookmarks("third".as_ref(), &git_repo).unwrap_err();
insta::assert_snapshot!(format!("{first_err:#?}\n{second_err:#?}\n{third_err:#?}"), @r#"
InvalidRemoteConfiguration(
RemoteNameBuf(
"first",
),
RefSpec {
kind: "fetch",
remote_name: "first",
source: Error {
key: "remote.<name>.fetch",
value: Some(
"+refs/heads/bad*pattern*:refs/remotes/heads/bad*pattern*",
),
environment_override: None,
source: Some(
PatternUnsupported {
pattern: "refs/heads/bad*pattern*",
},
),
},
},
)
InvalidRemoteConfiguration(
RemoteNameBuf(
"second",
),
RefSpec {
kind: "fetch",
remote_name: "second",
source: Error {
key: "remote.<name>.fetch",
value: Some(
"+refs/heads/badpattern?:refs/remotes/heads/badpattern?",
),
environment_override: None,
source: Some(
ReferenceName(
InvalidByte {
byte: "?",
},
),
),
},
},
)
InvalidRemoteConfiguration(
RemoteNameBuf(
"third",
),
RefSpec {
kind: "fetch",
remote_name: "third",
source: Error {
key: "remote.<name>.fetch",
value: Some(
"+refs/heads/bad[pat]:refs/remotes/heads/bad[pat]",
),
environment_override: None,
source: Some(
ReferenceName(
InvalidByte {
byte: "[",
},
),
),
},
},
)
"#);
Ok(())
}
#[test]
fn test_fetch_no_such_remote() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
let result = fetch_all_with(&mut fetcher, "invalid-remote".as_ref());
assert!(matches!(result, Err(GitFetchError::NoSuchRemote(_))));
Ok(())
}
#[test]
fn test_fetch_multiple_branches() -> TestResult {
let test_data = GitRepoData::create();
let _initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let mut tx = test_data.repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::union_all(vec![
StringExpression::exact("main"),
StringExpression::exact("noexist1"),
StringExpression::exact("noexist2"),
]),
tag: StringExpression::none(),
};
fetch_with(&mut fetcher, "origin".as_ref(), ref_expr)?;
let stats = fetcher.import_refs().block_on()?;
assert_eq!(
stats
.changed_remote_bookmarks
.iter()
.map(|(symbol, _)| symbol)
.collect_vec(),
[remote_symbol("main", "origin")]
);
Ok(())
}
#[test]
fn test_fetch_local_remote_conflicts() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = auto_track_import_options();
let fetch_import = |mut_repo: &mut MutableRepo| {
let mut fetcher =
GitFetch::new(mut_repo, subprocess_options.clone(), &import_options).unwrap();
let remote = RemoteName::new("origin");
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::all(),
tag: StringExpression::all(),
};
let refspecs = expand_fetch_refspecs(remote, ref_expr).unwrap();
let depth = None;
let fetch_tags = Some(FetchTagsOverride::NoTags);
fetcher
.fetch(remote, refspecs, &mut NullCallback, depth, fetch_tags)
.unwrap();
fetcher.import_refs().block_on().unwrap()
};
let commit1 = empty_git_commit(&test_data.origin_repo, "refs/heads/bookmark", &[]);
git_ref(&test_data.origin_repo, "refs/tags/tag", commit1);
let mut tx = test_data.repo.start_transaction();
let commit2 = write_random_commit(tx.repo_mut());
let target2 = RefTarget::normal(commit2.id().clone());
let commit3 = write_random_commit(tx.repo_mut());
let target3 = RefTarget::normal(commit3.id().clone());
tx.repo_mut()
.set_local_bookmark_target("bookmark".as_ref(), target2.clone());
tx.repo_mut()
.set_local_tag_target("tag".as_ref(), target3.clone());
let stats = fetch_import(tx.repo_mut());
let repo = tx.commit("test").block_on()?;
assert_eq!(stats.changed_remote_bookmarks.len(), 1);
assert_eq!(stats.changed_remote_tags.len(), 1);
let conflicted_target2 = RefTarget::from_merge(Merge::from_vec(vec![
Some(commit2.id().clone()),
None,
Some(jj_id(commit1)),
]));
let conflicted_target3 = RefTarget::from_merge(Merge::from_vec(vec![
Some(commit3.id().clone()),
None,
Some(jj_id(commit1)),
]));
assert_eq!(
repo.view().get_local_bookmark("bookmark".as_ref()),
&conflicted_target2
);
assert_eq!(
repo.view().get_local_tag("tag".as_ref()),
&conflicted_target3
);
Ok(())
}
#[test]
fn test_fetch_with_tag_changes() -> TestResult {
let test_data = GitRepoData::create();
let commit1 = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
git_ref(&test_data.origin_repo, "refs/tags/tag1", commit1);
let target1 = RefTarget::normal(jj_id(commit1));
let remote_ref1 = RemoteRef {
target: target1.clone(),
state: RemoteRefState::Tracked,
};
let mut tx = test_data.repo.start_transaction();
let commit2 = write_random_commit(tx.repo_mut());
let target2 = RefTarget::normal(commit2.id().clone());
let remote_ref2 = RemoteRef {
target: target2.clone(),
state: RemoteRefState::Tracked,
};
tx.repo_mut()
.set_local_tag_target("tag2".as_ref(), target2.clone());
tx.repo_mut()
.set_remote_tag(remote_symbol("tag2", "git"), remote_ref2.clone());
tx.repo_mut()
.set_remote_tag(remote_symbol("tag2", "origin"), remote_ref2.clone());
let repo = tx.commit("test").block_on()?;
let mut tx = repo.start_transaction();
let stats = fetch_import_all(tx.repo_mut(), "origin".as_ref());
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
assert_eq!(stats.changed_remote_tags.len(), 2);
assert_eq!(stats.changed_remote_tags[0].0, remote_symbol("tag1", "git"));
assert_eq!(stats.changed_remote_tags[1].0, remote_symbol("tag2", "git"));
assert_eq!(repo.view().get_local_tag("tag1".as_ref()), &target1);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "git")),
&remote_ref1
);
assert!(repo.view().get_local_tag("tag2".as_ref()).is_absent());
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "git")),
RemoteRef::absent_ref()
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "origin")),
&remote_ref2
);
Ok(())
}
#[test]
fn test_fetch_with_explicit_tag_patterns() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let fetch_import = |mut_repo: &mut MutableRepo, tag: StringExpression| {
let mut fetcher =
GitFetch::new(mut_repo, subprocess_options.clone(), &import_options).unwrap();
let remote = RemoteName::new("origin");
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::all(),
tag,
};
let refspecs = expand_fetch_refspecs(remote, ref_expr).unwrap();
let depth = None;
let fetch_tags = Some(FetchTagsOverride::NoTags);
fetcher
.fetch(remote, refspecs, &mut NullCallback, depth, fetch_tags)
.unwrap();
fetcher.import_refs().block_on().unwrap()
};
let commit1 = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
git_ref(&test_data.origin_repo, "refs/tags/tag1", commit1);
let commit2 = empty_git_commit(&test_data.origin_repo, "refs/tags/tag2", &[]);
let target1 = RefTarget::normal(jj_id(commit1));
let target2 = RefTarget::normal(jj_id(commit2));
let remote_ref1 = RemoteRef {
target: target1.clone(),
state: RemoteRefState::Tracked,
};
let remote_ref2 = RemoteRef {
target: target2.clone(),
state: RemoteRefState::Tracked,
};
let mut tx = test_data.repo.start_transaction();
let stats = fetch_import(tx.repo_mut(), StringExpression::exact("tag2"));
let repo = tx.commit("test").block_on()?;
assert_eq!(stats.changed_remote_tags.len(), 1);
assert_eq!(
stats.changed_remote_tags[0].0,
remote_symbol("tag2", "origin")
);
assert!(repo.view().get_local_tag("tag1".as_ref()).is_absent());
assert_eq!(repo.view().get_local_tag("tag2".as_ref()), &target2);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "origin")),
&remote_ref2
);
assert_eq!(
*repo.view().heads(),
hashset! { jj_id(commit1), jj_id(commit2) }
);
let mut tx = repo.start_transaction();
let stats = fetch_import(tx.repo_mut(), StringExpression::exact("tag1"));
let repo = tx.commit("test").block_on()?;
assert_eq!(stats.changed_remote_tags.len(), 1);
assert_eq!(
stats.changed_remote_tags[0].0,
remote_symbol("tag1", "origin")
);
assert_eq!(repo.view().get_local_tag("tag1".as_ref()), &target1);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "origin")),
&remote_ref1
);
assert_eq!(repo.view().get_local_tag("tag2".as_ref()), &target2);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "origin")),
&remote_ref2
);
assert_eq!(
*repo.view().heads(),
hashset! { jj_id(commit1), jj_id(commit2) }
);
Ok(())
}
#[test]
fn test_fetch_export_annotated_tags() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let fetch_import = |mut_repo: &mut MutableRepo| {
let mut fetcher =
GitFetch::new(mut_repo, subprocess_options.clone(), &import_options).unwrap();
let remote = RemoteName::new("origin");
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::none(),
tag: StringExpression::all(),
};
let refspecs = expand_fetch_refspecs(remote, ref_expr).unwrap();
let depth = None;
let fetch_tags = Some(FetchTagsOverride::NoTags);
fetcher
.fetch(remote, refspecs, &mut NullCallback, depth, fetch_tags)
.unwrap();
fetcher.import_refs().block_on().unwrap()
};
let commit1 = empty_git_commit(&test_data.origin_repo, "refs/tags/tag1", &[]);
let commit2 = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let commit3 = empty_git_commit(&test_data.origin_repo, "refs/tags/tag3.4", &[]);
let kind = gix::object::Kind::Commit;
let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
let tag2_oid = test_data
.origin_repo
.tag("tag2", commit2, kind, None, "", constraint)?
.id();
let target1 = RefTarget::normal(jj_id(commit1));
let target2 = RefTarget::normal(jj_id(commit2));
let target3 = RefTarget::normal(jj_id(commit3));
let remote_ref1 = RemoteRef {
target: target1.clone(),
state: RemoteRefState::Tracked,
};
let remote_ref2 = RemoteRef {
target: target2.clone(),
state: RemoteRefState::Tracked,
};
let remote_ref3 = RemoteRef {
target: target3.clone(),
state: RemoteRefState::Tracked,
};
let mut tx = test_data.repo.start_transaction();
fetch_import(tx.repo_mut());
let commit4 = write_random_commit(tx.repo_mut());
let target4 = RefTarget::normal(commit4.id().clone());
let remote_ref4 = RemoteRef {
target: target4.clone(),
state: RemoteRefState::Tracked,
};
tx.repo_mut()
.set_local_tag_target("tag3.4".as_ref(), target4.clone());
git::export_refs(tx.repo_mut())?;
let repo = tx.commit("test").block_on()?;
assert_eq!(repo.view().get_local_tag("tag1".as_ref()), &target1);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "git")),
&remote_ref1
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag1", "origin")),
&remote_ref1
);
assert_eq!(repo.view().get_local_tag("tag2".as_ref()), &target2);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "git")),
&remote_ref2
);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag2", "origin")),
&remote_ref2
);
assert_eq!(repo.view().get_local_tag("tag3.4".as_ref()), &target4);
assert_eq!(
repo.view().get_remote_tag(remote_symbol("tag3.4", "git")),
&remote_ref4
);
assert_eq!(
repo.view()
.get_remote_tag(remote_symbol("tag3.4", "origin")),
&remote_ref3
);
assert_eq!(
test_data.git_repo.find_reference("refs/tags/tag1")?.id(),
commit1
);
assert_eq!(
test_data.git_repo.find_reference("refs/tags/tag2")?.id(),
tag2_oid
);
assert_eq!(
test_data.git_repo.find_reference("refs/tags/tag3.4")?.id(),
git_id(&commit4)
);
Ok(())
}
#[test]
fn test_fetch_with_fetch_tags_override() -> TestResult {
let source_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let source_repo = &source_repo.repo;
let source_git_repo = get_git_repo(source_repo);
let git_settings = GitSettings::from_settings(source_repo.settings())?;
let import_options = default_import_options();
let commit1 = empty_git_commit(&source_git_repo, "refs/heads/main", &[]);
git_ref(&source_git_repo, "refs/remotes/origin/main", commit1);
let commit2 = empty_git_commit(&source_git_repo, "refs/heads/disjoint", &[]);
git_ref(&source_git_repo, "refs/remotes/origin/disjoint", commit2);
let commit3 = empty_git_commit(&source_git_repo, "refs/tags/v1.0", &[commit1]);
let commit4 = empty_git_commit(&source_git_repo, "refs/tags/v2.0", &[commit2]);
testutils::git::set_symbolic_reference(&source_git_repo, "HEAD", "refs/heads/main");
let fetch_import =
|mut_repo: &mut MutableRepo, remote: &RemoteName, fetch_tags: Option<FetchTagsOverride>| {
let mut fetcher = GitFetch::new(
mut_repo,
git_settings.to_subprocess_options(),
&import_options,
)
.unwrap();
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::all(),
tag: StringExpression::none(),
};
let refspecs = expand_fetch_refspecs(remote, ref_expr).unwrap();
let depth = None;
fetcher
.fetch(remote, refspecs, &mut NullCallback, depth, fetch_tags)
.unwrap();
fetcher.import_refs().block_on().unwrap()
};
let changed_tags = |stats: &GitImportStats| {
stats
.changed_remote_tags
.iter()
.filter_map(|(remote_symbol, (_, target))| {
target
.as_resolved()?
.as_ref()
.map(|resolved| (remote_symbol.name.as_str().to_owned(), resolved.hex()))
})
.collect::<BTreeMap<_, _>>()
};
let expected_changed_tags = BTreeMap::from([
("v1.0".to_owned(), commit3.to_hex().to_string()),
("v2.0".to_owned(), commit4.to_hex().to_string()),
]);
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
git::add_remote(
tx.repo_mut(),
"origin".as_ref(),
&source_git_repo.path().display().to_string(),
None,
gix::remote::fetch::Tags::None,
&StringExpression::all(),
)?;
let _repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let mut tx = repo.start_transaction();
let stats = fetch_import(tx.repo_mut(), "origin".as_ref(), None);
assert_eq!(stats.changed_remote_tags, vec![]);
let mut tx = repo.start_transaction();
let stats = fetch_import(
tx.repo_mut(),
"origin".as_ref(),
Some(FetchTagsOverride::AllTags),
);
assert_eq!(changed_tags(&stats), expected_changed_tags);
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
git::add_remote(
tx.repo_mut(),
"originAllTags".as_ref(),
&source_git_repo.path().display().to_string(),
None,
gix::remote::fetch::Tags::All,
&StringExpression::all(),
)?;
let _repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let mut tx = repo.start_transaction();
let stats = fetch_import(
tx.repo_mut(),
"originAllTags".as_ref(),
Some(FetchTagsOverride::NoTags),
);
assert_eq!(stats.changed_remote_tags, vec![]);
let mut tx = repo.start_transaction();
let stats = fetch_import(tx.repo_mut(), "originAllTags".as_ref(), None);
assert_eq!(changed_tags(&stats), expected_changed_tags);
Ok(())
}
#[test]
fn test_fetch_rejected_tag_updates() -> TestResult {
let test_data = GitRepoData::create();
let subprocess_options = GitSubprocessOptions::from_settings(test_data.repo.settings())?;
let import_options = default_import_options();
let commit1 = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
git_ref(&test_data.origin_repo, "refs/tags/tag", commit1);
let mut tx = test_data.repo.start_transaction();
let commit2 = write_random_commit(tx.repo_mut());
let target2 = RefTarget::normal(commit2.id().clone());
tx.repo_mut()
.set_local_tag_target("tag".as_ref(), target2.clone());
git::export_refs(tx.repo_mut())?;
let repo = tx.commit("test").block_on()?;
let mut tx = repo.start_transaction();
let mut fetcher = GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
let ref_expr = GitFetchRefExpression {
bookmark: StringExpression::all(),
tag: StringExpression::none(),
};
assert_matches!(
fetcher.fetch(
"origin".as_ref(),
expand_fetch_refspecs("origin".as_ref(), ref_expr)?,
&mut NullCallback,
None,
Some(FetchTagsOverride::AllTags),
),
Err(GitFetchError::RejectedUpdates(refs)) if refs == ["refs/tags/tag"]
);
Ok(())
}
struct PushTestSetup {
source_repo_dir: PathBuf,
jj_repo: Arc<ReadonlyRepo>,
main_commit: Commit,
child_of_main_commit: Commit,
parent_of_main_commit: Commit,
sideways_commit: Commit,
}
fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSetup {
let source_repo_dir = temp_dir.path().join("source");
let clone_repo_dir = temp_dir.path().join("clone");
let jj_repo_dir = temp_dir.path().join("jj");
let source_repo = testutils::git::init_bare(&source_repo_dir);
let parent_of_initial_git_commit = empty_git_commit(&source_repo, "refs/heads/main", &[]);
let initial_git_commit = empty_git_commit(
&source_repo,
"refs/heads/main",
&[parent_of_initial_git_commit],
);
let clone_repo =
testutils::git::clone(&clone_repo_dir, source_repo_dir.to_str().unwrap(), None);
std::fs::create_dir(&jj_repo_dir).unwrap();
let jj_repo = ReadonlyRepo::init(
settings,
&jj_repo_dir,
&|settings, store_path| {
Ok(Box::new(GitBackend::init_external(
settings,
store_path,
clone_repo.path(),
)?))
},
Signer::from_settings(settings).unwrap(),
ReadonlyRepo::default_op_store_initializer(),
ReadonlyRepo::default_op_heads_store_initializer(),
ReadonlyRepo::default_index_store_initializer(),
ReadonlyRepo::default_submodule_store_initializer(),
)
.block_on()
.unwrap();
get_git_backend(&jj_repo)
.import_head_commits(&[jj_id(initial_git_commit)])
.unwrap();
let main_commit = jj_repo
.store()
.get_commit(&jj_id(initial_git_commit))
.unwrap();
let parent_of_main_commit = jj_repo
.store()
.get_commit(&jj_id(parent_of_initial_git_commit))
.unwrap();
let mut tx = jj_repo.start_transaction();
let sideways_commit = write_random_commit(tx.repo_mut());
let child_of_main_commit = write_random_commit_with_parents(tx.repo_mut(), &[&main_commit]);
tx.repo_mut().set_git_ref_target(
"refs/remotes/origin/main".as_ref(),
RefTarget::normal(main_commit.id().clone()),
);
tx.repo_mut().set_remote_bookmark(
remote_symbol("main", "origin"),
RemoteRef {
target: RefTarget::normal(main_commit.id().clone()),
state: RemoteRefState::Tracked,
},
);
let jj_repo = tx.commit("test").block_on().unwrap();
PushTestSetup {
source_repo_dir,
jj_repo,
main_commit,
child_of_main_commit,
parent_of_main_commit,
sideways_commit,
}
}
#[test]
fn test_push_bookmarks_success() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let mut setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = get_git_repo(&setup.jj_repo);
let mut tx = setup.jj_repo.start_transaction();
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let import_options = default_import_options();
let targets = GitPushRefTargets {
bookmarks: vec![(
"main".into(),
Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
)],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
let source_repo = testutils::git::open(&setup.source_repo_dir);
let new_target = source_repo.find_reference("refs/heads/main")?;
let new_oid = git_id(&setup.child_of_main_commit);
assert_eq!(new_target.target().id(), new_oid);
let new_target = clone_repo.find_reference("refs/remotes/origin/main")?;
assert_eq!(new_target.target().id(), new_oid);
let view = tx.repo().view();
assert_eq!(
*view.get_git_ref("refs/remotes/origin/main".as_ref()),
RefTarget::normal(setup.child_of_main_commit.id().clone()),
);
assert_eq!(
*view.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef {
target: RefTarget::normal(setup.child_of_main_commit.id().clone()),
state: RemoteRefState::Tracked,
},
);
setup.jj_repo = tx.commit("test").block_on()?;
let mut tx = setup.jj_repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
assert!(!tx.repo().has_changes());
Ok(())
}
#[test]
fn test_push_bookmarks_deletion() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let mut setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = get_git_repo(&setup.jj_repo);
let mut tx = setup.jj_repo.start_transaction();
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let import_options = default_import_options();
let source_repo = testutils::git::open(&setup.source_repo_dir);
assert!(source_repo.find_reference("refs/heads/main").is_ok());
let targets = GitPushRefTargets {
bookmarks: vec![(
"main".into(),
Diff::new(Some(setup.main_commit.id().clone()), None),
)],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
assert!(source_repo.find_reference("refs/heads/main").is_err());
assert!(
clone_repo
.find_reference("refs/remotes/origin/main")
.is_err()
);
let view = tx.repo().view();
assert!(
view.get_git_ref("refs/remotes/origin/main".as_ref())
.is_absent()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent_ref()
);
setup.jj_repo = tx.commit("test").block_on()?;
let mut tx = setup.jj_repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
assert!(!tx.repo().has_changes());
Ok(())
}
#[test]
fn test_push_bookmarks_mixed_deletion_and_addition() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let mut setup = set_up_push_repos(&settings, &temp_dir);
let mut tx = setup.jj_repo.start_transaction();
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let import_options = default_import_options();
let targets = GitPushRefTargets {
bookmarks: vec![
(
"main".into(),
Diff::new(Some(setup.main_commit.id().clone()), None),
),
(
"topic".into(),
Diff::new(None, Some(setup.child_of_main_commit.id().clone())),
),
],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
GitRefNameBuf(
"refs/heads/topic",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
let source_repo = testutils::git::open(&setup.source_repo_dir);
let new_target = source_repo.find_reference("refs/heads/topic")?;
assert_eq!(
new_target.target().id(),
git_id(&setup.child_of_main_commit)
);
assert!(source_repo.find_reference("refs/heads/main").is_err());
let view = tx.repo().view();
assert!(
view.get_git_ref("refs/remotes/origin/main".as_ref())
.is_absent()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef::absent_ref()
);
assert_eq!(
*view.get_git_ref("refs/remotes/origin/topic".as_ref()),
RefTarget::normal(setup.child_of_main_commit.id().clone()),
);
assert_eq!(
*view.get_remote_bookmark(remote_symbol("topic", "origin")),
RemoteRef {
target: RefTarget::normal(setup.child_of_main_commit.id().clone()),
state: RemoteRefState::Tracked,
},
);
setup.jj_repo = tx.commit("test").block_on()?;
let mut tx = setup.jj_repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
assert!(!tx.repo().has_changes());
Ok(())
}
#[test]
fn test_push_bookmarks_not_fast_forward() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let mut tx = setup.jj_repo.start_transaction();
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let targets = GitPushRefTargets {
bookmarks: vec![(
"main".into(),
Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.sideways_commit.id().clone()),
),
)],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
let source_repo = testutils::git::open(&setup.source_repo_dir);
let new_target = source_repo.find_reference("refs/heads/main")?;
assert_eq!(new_target.target().id(), git_id(&setup.sideways_commit));
Ok(())
}
#[test]
fn test_push_bookmarks_partial_success() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let mut tx = setup.jj_repo.start_transaction();
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let targets = GitPushRefTargets {
bookmarks: vec![
(
"main".into(),
Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
),
(
"other".into(),
Diff::new(
Some(setup.main_commit.id().clone()), Some(setup.child_of_main_commit.id().clone()),
),
),
],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [
(
GitRefNameBuf(
"refs/heads/other",
),
Some(
"stale info",
),
),
],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
let view = tx.repo().view();
assert_eq!(
*view.get_git_ref("refs/remotes/origin/main".as_ref()),
RefTarget::normal(setup.child_of_main_commit.id().clone())
);
assert_eq!(
*view.get_remote_bookmark(remote_symbol("main", "origin")),
RemoteRef {
target: RefTarget::normal(setup.child_of_main_commit.id().clone()),
state: RemoteRefState::Tracked,
}
);
assert_eq!(
view.get_git_ref("refs/remotes/origin/other".as_ref()),
RefTarget::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("other", "origin")),
RemoteRef::absent_ref()
);
Ok(())
}
#[test]
fn test_push_bookmarks_unmapped_refs() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let subprocess_options = GitSubprocessOptions::from_settings(test_repo.repo.settings())?;
let remote_git_repo = testutils::git::init_bare(test_repo.env.root().join("remote"));
let mut tx = test_repo.repo.start_transaction();
git::add_remote(
tx.repo_mut(),
"origin".as_ref(),
remote_git_repo.path().to_str().unwrap(),
None,
gix::remote::fetch::Tags::default(),
&StringExpression::exact("dummy"),
)?;
let repo = tx.commit("set up remote").block_on()?;
let repo = test_repo
.env
.load_repo_at_head(repo.settings(), test_repo.repo_path());
let git_repo = get_git_repo(&repo);
let mut tx = repo.start_transaction();
let commit1 = write_random_commit(tx.repo_mut());
let commit2a = write_random_commit(tx.repo_mut());
let commit2b = write_random_commit(tx.repo_mut());
git_repo.reference(
"refs/remotes/origin/bookmark2",
git_id(&commit2a),
gix::refs::transaction::PreviousValue::MustNotExist,
"",
)?;
let targets = GitPushRefTargets {
bookmarks: vec![
(
"bookmark1".into(),
Diff::new(None, Some(commit1.id().clone())),
),
(
"bookmark2".into(),
Diff::new(None, Some(commit2b.id().clone())),
),
],
};
let stats = git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/bookmark1",
),
GitRefNameBuf(
"refs/heads/bookmark2",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [
(
RemoteRefSymbolBuf {
name: RefNameBuf(
"bookmark2",
),
remote: RemoteNameBuf(
"origin",
),
},
AddedInJjAddedInGit,
),
],
}
"#);
assert_eq!(
git_repo
.find_reference("refs/remotes/origin/bookmark1")?
.into_fully_peeled_id()?,
git_id(&commit1)
);
let view = tx.repo().view();
assert_eq!(
*view.get_git_ref("refs/remotes/origin/bookmark1".as_ref()),
RefTarget::normal(commit1.id().clone())
);
assert_eq!(
*view.get_remote_bookmark(remote_symbol("bookmark1", "origin")),
RemoteRef {
target: RefTarget::normal(commit1.id().clone()),
state: RemoteRefState::Tracked,
}
);
assert_eq!(
view.get_git_ref("refs/remotes/origin/bookmark2".as_ref()),
RefTarget::absent_ref()
);
assert_eq!(
view.get_remote_bookmark(remote_symbol("bookmark2", "origin")),
RemoteRef::absent_ref()
);
Ok(())
}
#[test]
fn test_push_updates_unexpectedly_moved_sideways_on_remote() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let attempt_push_expecting_sideways = |target: Option<CommitId>| {
let subprocess_options = GitSubprocessOptions::from_settings(&settings).unwrap();
let targets = [GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(Some(setup.sideways_commit.id().clone()), target),
}];
git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)
};
assert_eq!(
push_status_rejected_references(attempt_push_expecting_sideways(None)?),
vec!["refs/heads/main".to_owned()],
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_sideways(Some(
setup.child_of_main_commit.id().clone()
))?),
vec!["refs/heads/main".to_owned()]
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_sideways(Some(
setup.sideways_commit.id().clone()
))?),
vec!["refs/heads/main".to_owned()]
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_sideways(Some(
setup.parent_of_main_commit.id().clone()
))?),
vec!["refs/heads/main".to_owned()]
);
let stats = attempt_push_expecting_sideways(Some(setup.main_commit.id().clone()))?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
Ok(())
}
#[test]
fn test_push_updates_unexpectedly_moved_forward_on_remote() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let attempt_push_expecting_parent = |target: Option<CommitId>| {
let subprocess_options = GitSubprocessOptions::from_settings(&settings).unwrap();
let targets = [GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(Some(setup.parent_of_main_commit.id().clone()), target),
}];
git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)
};
assert_eq!(
push_status_rejected_references(attempt_push_expecting_parent(None)?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_parent(Some(
setup.sideways_commit.id().clone()
))?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_parent(Some(
setup.parent_of_main_commit.id().clone()
))?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_parent(Some(
setup.child_of_main_commit.id().clone()
))?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
Ok(())
}
#[test]
fn test_push_updates_unexpectedly_exists_on_remote() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let attempt_push_expecting_absence = |target: Option<CommitId>| {
let subprocess_options = GitSubprocessOptions::from_settings(&settings).unwrap();
let targets = [GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(None, target),
}];
git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)
};
assert_eq!(
push_status_rejected_references(attempt_push_expecting_absence(Some(
setup.parent_of_main_commit.id().clone()
))?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
assert_eq!(
push_status_rejected_references(attempt_push_expecting_absence(Some(
setup.child_of_main_commit.id().clone()
))?),
["refs/heads/main"].map(GitRefNameBuf::from)
);
Ok(())
}
#[test]
fn test_push_updates_success() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let clone_repo = get_git_repo(&setup.jj_repo);
let stats = git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"origin".as_ref(),
&[GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
}],
&mut NullCallback,
&GitPushOptions::default(),
)?;
insta::assert_debug_snapshot!(stats, @r#"
GitPushStats {
pushed: [
GitRefNameBuf(
"refs/heads/main",
),
],
rejected: [],
remote_rejected: [],
unexported_bookmarks: [],
}
"#);
let source_repo = testutils::git::open(&setup.source_repo_dir);
let new_target = source_repo.find_reference("refs/heads/main")?;
let new_oid = git_id(&setup.child_of_main_commit);
assert_eq!(new_target.target().id(), new_oid);
let new_target = clone_repo.find_reference("refs/remotes/origin/main")?;
assert_eq!(new_target.target().id(), new_oid);
Ok(())
}
#[test]
fn test_push_updates_no_such_remote() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let result = git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"invalid-remote".as_ref(),
&[GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
}],
&mut NullCallback,
&GitPushOptions::default(),
);
assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
Ok(())
}
#[test]
fn test_push_updates_invalid_remote() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let result = git::push_updates(
setup.jj_repo.as_ref(),
subprocess_options,
"http://invalid-remote".as_ref(),
&[GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
}],
&mut NullCallback,
&GitPushOptions::default(),
);
assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
Ok(())
}
#[test]
fn test_push_environment_options() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let mut tx = setup.jj_repo.start_transaction();
let mut subprocess_options = GitSubprocessOptions::from_settings(&settings)?;
let trace_path = temp_dir.path().join("git-trace.log");
subprocess_options
.environment
.insert("GIT_TRACE".into(), trace_path.clone().into());
let targets = GitPushRefTargets {
bookmarks: vec![(
"main".into(),
Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
)],
};
git::push_refs(
tx.repo_mut(),
subprocess_options,
"origin".as_ref(),
&targets,
&mut NullCallback,
&GitPushOptions::default(),
)?;
assert!(trace_path.exists());
Ok(())
}
#[test]
fn test_bulk_update_extra_on_import_refs() -> TestResult {
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 count_extra_tables = || {
let extra_dir = test_repo.repo_path().join("store").join("extra");
extra_dir
.read_dir()
.unwrap()
.filter(|entry| entry.as_ref().unwrap().metadata().unwrap().is_file())
.count()
};
let import_refs = |repo: &Arc<ReadonlyRepo>| {
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options)
.block_on()
.unwrap();
tx.repo_mut().rebase_descendants().block_on().unwrap();
tx.commit("test").block_on().unwrap()
};
let mut commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
for _ in 1..10 {
commit = empty_git_commit(&git_repo, "refs/heads/main", &[commit]);
}
let repo = import_refs(repo);
assert_eq!(count_extra_tables(), 2);
let repo = import_refs(&repo);
assert_eq!(count_extra_tables(), 2);
for _ in 0..10 {
commit = empty_git_commit(&git_repo, "refs/heads/main", &[commit]);
}
let repo = import_refs(&repo);
assert_eq!(count_extra_tables(), 3);
drop(repo); Ok(())
}
#[test]
fn test_rewrite_imported_commit() -> TestResult {
let test_repo = TestRepo::init_with_backend_and_settings(
TestRepoBackend::Git,
&user_settings_without_change_id(),
);
let repo = &test_repo.repo;
let git_repo = get_git_repo(repo);
let import_options = default_import_options();
let git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
tx.repo_mut().rebase_descendants().block_on()?;
let repo = tx.commit("test").block_on()?;
let imported_commit = repo.store().get_commit(&jj_id(git_commit))?;
let mut tx = repo.start_transaction();
let authored_commit = tx
.repo_mut()
.new_commit(
imported_commit.parent_ids().to_vec(),
imported_commit.tree(),
)
.set_author(imported_commit.author().clone())
.set_committer(imported_commit.committer().clone())
.set_description(imported_commit.description())
.write_unwrap();
let repo = tx.commit("test").block_on()?;
assert_ne!(imported_commit.id(), authored_commit.id());
assert_ne!(
imported_commit.committer().timestamp,
authored_commit.committer().timestamp,
);
assert_eq!(
repo.resolve_change_id(imported_commit.change_id())?
.and_then(ResolvedChangeTargets::into_visible),
Some(vec![imported_commit.id().clone()]),
);
assert_eq!(
repo.resolve_change_id(authored_commit.change_id())?
.and_then(ResolvedChangeTargets::into_visible),
Some(vec![authored_commit.id().clone()]),
);
Ok(())
}
#[test]
fn test_concurrent_write_commit() -> TestResult {
let settings = &testutils::user_settings();
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let test_env = &test_repo.env;
let repo = &test_repo.repo;
let num_thread = 8;
let (sender, receiver) = mpsc::channel();
thread::scope(|s| {
let barrier = Arc::new(Barrier::new(num_thread));
for i in 0..num_thread {
let repo = test_env.load_repo_at_head(settings, test_repo.repo_path()); let barrier = barrier.clone();
let sender = sender.clone();
s.spawn(move || {
barrier.wait();
let mut tx = repo.start_transaction();
let commit = create_rooted_commit(tx.repo_mut())
.set_description("racy commit")
.write_unwrap();
tx.commit(format!("writer {i}")).block_on().unwrap();
sender
.send((commit.id().clone(), commit.change_id().clone()))
.unwrap();
});
}
});
drop(sender);
let mut commit_change_ids: BTreeMap<CommitId, HashSet<ChangeId>> = BTreeMap::new();
for (commit_id, change_id) in receiver {
commit_change_ids
.entry(commit_id)
.or_default()
.insert(change_id);
}
assert_eq!(commit_change_ids.len(), num_thread);
let repo = repo.reload_at_head().block_on()?;
for (commit_id, change_ids) in &commit_change_ids {
let commit = repo.store().get_commit(commit_id)?;
assert_eq!(commit.id(), commit_id);
assert!(change_ids.contains(commit.change_id()));
}
for commit_id in commit_change_ids.keys() {
assert!(repo.index().has_id(commit_id)?);
let commit = repo.store().get_commit(commit_id)?;
assert_eq!(
repo.resolve_change_id(commit.change_id())?
.and_then(ResolvedChangeTargets::into_visible),
Some(vec![commit_id.clone()]),
);
}
Ok(())
}
#[test]
#[cfg_attr(windows, ignore)]
fn test_concurrent_read_write_commit() -> TestResult {
let settings = user_settings_without_change_id();
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let test_env = &test_repo.env;
let repo = &test_repo.repo;
let commit_ids = [
"c5c6efd6ac240102e7f047234c3cade55eedd621",
"9f7a96a6c9d044b228f3321a365bdd3514e6033a",
"aa7867ad0c566df5bbb708d8d6ddc88eefeea0ff",
"930a76e333d5cc17f40a649c3470cb99aae24a0c",
"88e9a719df4f0cc3daa740b814e271341f6ea9f4",
"4883bdc57448a53b4eef1af85e34b85b9ee31aee",
"308345f8d058848e83beed166704faac2ecd4541",
"9e35ff61ea8d1d4ef7f01edc5fd23873cc301b30",
"8010ac8c65548dd619e7c83551d983d724dda216",
"bbe593d556ea31acf778465227f340af7e627b2b",
"2f6800f4b8e8fc4c42dc0e417896463d13673654",
"a3a7e4fcddeaa11bb84f66f3428f107f65eb3268",
"96e17ff3a7ee1b67ddfa5619b2bf5380b80f619a",
"34613f7609524c54cc990ada1bdef3dcad0fd29f",
"95867e5aed6b62abc2cd6258da9fee8873accfd3",
"7635ce107ae7ba71821b8cd74a1405ca6d9e49ac",
]
.into_iter()
.map(CommitId::from_hex)
.collect_vec();
let num_reader_thread = 8;
thread::scope(|s| {
let barrier = Arc::new(Barrier::new(commit_ids.len() + num_reader_thread));
for (i, commit_id) in commit_ids.iter().enumerate() {
let repo = test_env.load_repo_at_head(&settings, test_repo.repo_path()); let barrier = barrier.clone();
s.spawn(move || {
barrier.wait();
let mut tx = repo.start_transaction();
let commit = create_rooted_commit(tx.repo_mut())
.set_description(format!("commit {i}"))
.write_unwrap();
tx.commit(format!("writer {i}")).block_on().unwrap();
assert_eq!(commit.id(), commit_id);
});
}
for i in 0..num_reader_thread {
let mut repo = test_env.load_repo_at_head(&settings, test_repo.repo_path()); let barrier = barrier.clone();
let mut pending_commit_ids = commit_ids.clone();
pending_commit_ids.rotate_left(i); s.spawn(move || {
barrier.wait();
for _ in 0..100 {
if pending_commit_ids.is_empty() {
break;
}
repo = repo.reload_at_head().block_on().unwrap();
let git_backend = get_git_backend(&repo);
let mut tx = repo.start_transaction();
pending_commit_ids = pending_commit_ids
.into_iter()
.filter_map(|commit_id| {
match git_backend.import_head_commits([&commit_id]) {
Ok(()) => {
let commit = repo.store().get_commit(&commit_id).unwrap();
tx.repo_mut().add_head(&commit).block_on().unwrap();
None
}
Err(BackendError::ObjectNotFound { .. }) => Some(commit_id),
Err(err) => {
eprintln!(
"import error in reader {i} (maybe lock contention?): {}",
iter::successors(
Some(&err as &dyn std::error::Error),
|e| e.source(),
)
.join(": ")
);
Some(commit_id)
}
}
})
.collect_vec();
if tx.repo().has_changes() {
tx.commit(format!("reader {i}")).block_on().unwrap();
}
thread::yield_now();
}
if !pending_commit_ids.is_empty() {
eprintln!(
"reader {i} couldn't observe the following commits: \
{pending_commit_ids:#?}"
);
}
});
}
});
let repo = repo.reload_at_head().block_on()?;
for commit_id in &commit_ids {
assert!(repo.index().has_id(commit_id)?);
let commit = repo.store().get_commit(commit_id)?;
assert_eq!(
repo.resolve_change_id(commit.change_id())?
.and_then(ResolvedChangeTargets::into_visible),
Some(vec![commit_id.clone()]),
);
}
Ok(())
}
fn create_rooted_commit(mut_repo: &mut MutableRepo) -> CommitBuilder<'_> {
let signature = Signature {
name: "Test User".to_owned(),
email: "test.user@example.com".to_owned(),
timestamp: Timestamp {
timestamp: MillisSinceEpoch(1_000_000),
tz_offset: 0,
},
};
mut_repo
.new_commit(
vec![mut_repo.store().root_commit_id().clone()],
mut_repo.store().empty_merged_tree(),
)
.set_author(signature.clone())
.set_committer(signature)
}
#[test]
fn test_shallow_commits_lack_parents() -> TestResult {
let settings = testutils::user_settings();
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let test_env = &test_repo.env;
let repo = &test_repo.repo;
let git_repo = get_git_repo(repo);
let import_options = default_import_options();
let git_root = empty_git_commit(&git_repo, "refs/heads/main", &[]);
let a = empty_git_commit(&git_repo, "refs/heads/main", &[git_root]);
let b = empty_git_commit(&git_repo, "refs/heads/feature", &[a]);
let c = empty_git_commit(&git_repo, "refs/heads/main", &[a]);
let d = empty_git_commit(&git_repo, "refs/heads/feature", &[b]);
let e = empty_git_commit(&git_repo, "refs/heads/main", &[c]);
testutils::git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/main");
let make_shallow = |repo, mut shallow_commits: Vec<_>| {
let shallow_file = get_git_backend(repo).git_repo().shallow_file();
shallow_commits.sort();
let mut buf = Vec::<u8>::new();
for commit in shallow_commits {
writeln!(buf, "{commit}").unwrap();
}
fs::write(shallow_file, buf).unwrap();
test_env.load_repo_at_head(&settings, test_repo.repo_path())
};
let repo = make_shallow(repo, vec![b, c]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("import").block_on()?;
let store = repo.store();
let root = store.root_commit_id();
let expected_heads = hashset! {
jj_id(d),
jj_id(e),
};
assert_eq!(*repo.view().heads(), expected_heads);
let parents = |store: &Arc<jj_lib::store::Store>, commit| {
let commit = store.get_commit(&jj_id(commit)).unwrap();
commit.parent_ids().to_vec()
};
assert_eq!(
parents(store, b),
vec![root.clone()],
"shallow commits have the root commit as a parent"
);
assert_eq!(
parents(store, c),
vec![root.clone()],
"shallow commits have the root commit as a parent"
);
let repo = make_shallow(&repo, vec![a]);
let mut tx = repo.start_transaction();
git::import_refs(tx.repo_mut(), &import_options).block_on()?;
let repo = tx.commit("import").block_on()?;
let store = repo.store();
let root = store.root_commit_id();
assert_eq!(
parents(store, a),
vec![root.clone()],
"shallow commits have the root commit as a parent"
);
assert_eq!(
parents(store, b),
vec![jj_id(a)],
"unshallowed commits have parents"
);
assert_eq!(
parents(store, c),
vec![jj_id(a)],
"unshallowed commits have correct parents"
);
assert!(!repo.index().has_id(&jj_id(a))?);
Ok(())
}
#[test]
fn test_remote_remove_refs() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
git::add_remote(
tx.repo_mut(),
"foo".as_ref(),
"https://example.com/",
None,
Default::default(),
&StringExpression::all(),
)?;
let _repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let git_repo = get_git_repo(repo);
empty_git_commit(&git_repo, "refs/remotes/foo/a", &[]);
empty_git_commit(&git_repo, "refs/remotes/foo/x/y", &[]);
let commit_foobar_a = empty_git_commit(&git_repo, "refs/remotes/foobar/a", &[]);
empty_git_commit(&git_repo, "refs/jj/remote-tags/foo/x/y", &[]);
let commit_tag_foobar_a = empty_git_commit(&git_repo, "refs/jj/remote-tags/foobar/a", &[]);
let mut tx = repo.start_transaction();
git::remove_remote(tx.repo_mut(), "foo".as_ref())?;
let repo = &tx.commit("remove").block_on()?;
let git_repo = get_git_repo(repo);
assert!(git_repo.try_find_reference("refs/remotes/foo/a")?.is_none());
assert!(
git_repo
.try_find_reference("refs/remotes/foo/x/y")?
.is_none()
);
assert_eq!(
git_repo.find_reference("refs/remotes/foobar/a")?.id(),
commit_foobar_a,
);
assert!(
git_repo
.try_find_reference("refs/jj/remote-tags/foo/x/y")?
.is_none()
);
assert_eq!(
git_repo
.find_reference("refs/jj/remote-tags/foobar/a")?
.id(),
commit_tag_foobar_a,
);
Ok(())
}
#[test]
fn test_remote_rename_refs() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
git::add_remote(
tx.repo_mut(),
"foo".as_ref(),
"https://example.com/",
None,
Default::default(),
&StringExpression::all(),
)?;
let _repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let git_repo = get_git_repo(repo);
let commit_foo_a = empty_git_commit(&git_repo, "refs/remotes/foo/a", &[]);
let commit_foo_x_y = empty_git_commit(&git_repo, "refs/remotes/foo/x/y", &[]);
let commit_foobar_a = empty_git_commit(&git_repo, "refs/remotes/foobar/a", &[]);
let commit_tag_foo_x_y = empty_git_commit(&git_repo, "refs/jj/remote-tags/foo/x/y", &[]);
let commit_tag_foobar_a = empty_git_commit(&git_repo, "refs/jj/remote-tags/foobar/a", &[]);
let mut tx = repo.start_transaction();
git::rename_remote(tx.repo_mut(), "foo".as_ref(), "bar".as_ref())?;
let repo = &tx.commit("rename").block_on()?;
let git_repo = get_git_repo(repo);
assert!(git_repo.try_find_reference("refs/remotes/foo/a")?.is_none());
assert!(
git_repo
.try_find_reference("refs/remotes/foo/x/y")?
.is_none()
);
assert_eq!(
git_repo.find_reference("refs/remotes/bar/a")?.id(),
commit_foo_a,
);
assert_eq!(
git_repo.find_reference("refs/remotes/bar/x/y")?.id(),
commit_foo_x_y,
);
assert_eq!(
git_repo.find_reference("refs/remotes/foobar/a")?.id(),
commit_foobar_a,
);
assert!(
git_repo
.try_find_reference("refs/jj/remote-tags/foo/x/y")?
.is_none()
);
assert_eq!(
git_repo.find_reference("refs/jj/remote-tags/bar/x/y")?.id(),
commit_tag_foo_x_y,
);
assert_eq!(
git_repo
.find_reference("refs/jj/remote-tags/foobar/a")?
.id(),
commit_tag_foobar_a,
);
Ok(())
}
fn user_settings_without_change_id() -> UserSettings {
let mut config = base_user_config();
let mut layer = ConfigLayer::empty(ConfigSource::Default);
layer
.set_value("git.write-change-id-header", false)
.unwrap();
config.add_layer(layer);
UserSettings::from_config(config).unwrap()
}
#[test_case(gix::remote::fetch::Tags::All; "all")]
#[test_case(gix::remote::fetch::Tags::Included; "included")]
#[test_case(gix::remote::fetch::Tags::None; "none")]
fn test_remote_add_with_tags_specification(fetch_tags: gix::remote::fetch::Tags) -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
let remote_name = "foo";
git::add_remote(
tx.repo_mut(),
remote_name.as_ref(),
"https://example.com/",
None,
fetch_tags,
&StringExpression::all(),
)?;
let _repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
let git_repo = get_git_repo(repo);
assert_eq!(
fetch_tags,
git_repo
.find_remote(remote_name)
.expect("unable to find remote")
.fetch_tags()
);
Ok(())
}
#[test]
fn test_push_updates_with_options() -> TestResult {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let git_settings = GitSettings::from_settings(&settings)?;
std::process::Command::new("git")
.arg("--git-dir")
.arg(&setup.source_repo_dir)
.args(["config", "receive.advertisePushOptions", "true"])
.output()?;
let hooks_dir = setup.source_repo_dir.join("hooks");
fs::create_dir_all(&hooks_dir)?;
let hook_path = hooks_dir.join("pre-receive");
let hook_content = r#"#!/bin/sh
if [ -n "$GIT_PUSH_OPTION_COUNT" ] && [ "$GIT_PUSH_OPTION_COUNT" -gt 0 ]; then
i=0
while [ $i -lt "$GIT_PUSH_OPTION_COUNT" ]; do
eval "option_value=\$GIT_PUSH_OPTION_$i"
echo "Push-Option: $option_value"
i=$((i + 1))
done
fi
"#;
fs::write(&hook_path, hook_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o700))?;
}
let remote_output = Arc::new(std::sync::Mutex::new(Vec::new()));
struct CapturingCallback {
output: Arc<std::sync::Mutex<Vec<u8>>>,
}
impl GitSubprocessCallback for CapturingCallback {
fn needs_progress(&self) -> bool {
false
}
fn progress(&mut self, _progress: &git::GitProgress) -> std::io::Result<()> {
Ok(())
}
fn local_sideband(
&mut self,
_message: &[u8],
_term: Option<GitSidebandLineTerminator>,
) -> std::io::Result<()> {
Ok(())
}
fn remote_sideband(
&mut self,
message: &[u8],
_term: Option<GitSidebandLineTerminator>,
) -> std::io::Result<()> {
if let Ok(mut output) = self.output.lock() {
output.extend_from_slice(message);
}
Ok(())
}
}
let mut callback = CapturingCallback {
output: remote_output.clone(),
};
let result = git::push_updates(
setup.jj_repo.as_ref(),
git_settings.to_subprocess_options(),
"origin".as_ref(),
&[GitRefUpdate {
qualified_name: "refs/heads/main".into(),
targets: Diff::new(
Some(setup.main_commit.id().clone()),
Some(setup.child_of_main_commit.id().clone()),
),
}],
&mut callback,
&GitPushOptions {
extra_args: vec![],
remote_push_options: vec![
"merge_request.create".to_owned(),
"merge_request.draft".to_owned(),
],
},
)?;
let stats = result;
assert_eq!(
stats.pushed,
vec![jj_lib::ref_name::GitRefNameBuf::from("refs/heads/main")]
);
assert!(stats.rejected.is_empty());
assert!(stats.remote_rejected.is_empty());
assert!(stats.unexported_bookmarks.is_empty());
let captured_bytes = remote_output.lock().unwrap();
let captured_string = String::from_utf8_lossy(&captured_bytes);
assert!(captured_string.contains("Push-Option: merge_request.create"));
assert!(captured_string.contains("Push-Option: merge_request.draft"));
Ok(())
}
#[test]
fn test_remote_add_with_refspecs() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let mut tx = test_repo.repo.start_transaction();
let bookmark_expr = StringExpression::union_all(vec![
StringExpression::exact("bar"),
StringExpression::pattern(StringPattern::glob("foo*")?),
])
.intersection(StringExpression::exact("foobar").negated());
git::add_remote(
tx.repo_mut(),
"origin".as_ref(),
"https://example.com/",
None,
gix::remote::fetch::Tags::default(),
&bookmark_expr,
)?;
let repo = tx.commit("test").block_on()?;
let repo = &test_repo
.env
.load_repo_at_head(repo.settings(), test_repo.repo_path());
let git_repo = get_git_repo(repo);
let remote = git_repo.find_remote("origin")?;
insta::assert_debug_snapshot!(remote.refspecs(gix::remote::Direction::Fetch), @r#"
[
RefSpec {
mode: Negative,
op: Fetch,
src: Some(
"refs/heads/foobar",
),
dst: None,
},
RefSpec {
mode: Force,
op: Fetch,
src: Some(
"refs/heads/bar",
),
dst: Some(
"refs/remotes/origin/bar",
),
},
RefSpec {
mode: Force,
op: Fetch,
src: Some(
"refs/heads/foo*",
),
dst: Some(
"refs/remotes/origin/foo*",
),
},
]
"#);
Ok(())
}
fn auto_track_import_options() -> GitImportOptions {
let remotes_used_in_tests = ["origin", "upstream"];
let auto_track_bookmarks = remotes_used_in_tests
.into_iter()
.map(|name| (name.into(), StringMatcher::all()))
.collect();
GitImportOptions {
remote_auto_track_bookmarks: auto_track_bookmarks,
..default_import_options()
}
}
fn default_import_options() -> GitImportOptions {
GitImportOptions {
auto_local_bookmark: false,
abandon_unreachable_commits: true,
remote_auto_track_bookmarks: HashMap::new(),
}
}
#[track_caller]
fn assert_fetch_and_push_urls(
repo: &Arc<ReadonlyRepo>,
remote_name: &str,
expected_fetch_url: Option<&str>,
expected_push_url: Option<&str>,
) {
let git_repo = get_git_repo(repo);
let remote = git_repo
.find_remote(remote_name)
.expect("unable to find remote");
let actual_fetch_url = remote.url(Direction::Fetch);
let actual_push_url = remote.url(Direction::Push);
let expected_fetch_url = expected_fetch_url
.map(|u| gix::Url::try_from(u).expect("failed to parse the expected fetch url"));
let expected_push_url = expected_push_url
.map(|u| gix::Url::try_from(u).expect("failed to parse the expected push url"));
assert_eq!(actual_fetch_url, expected_fetch_url.as_ref());
assert_eq!(actual_push_url, expected_push_url.as_ref());
}
#[test]
fn test_set_remote_urls() -> TestResult {
let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);
let repo = &test_repo.repo;
let mut tx = repo.start_transaction();
let remote_name = "foo";
git::add_remote(
tx.repo_mut(),
remote_name.as_ref(),
"https://example.com/repo/path",
None,
gix::remote::fetch::Tags::None,
&StringExpression::all(),
)?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
assert_fetch_and_push_urls(
repo,
remote_name,
Some("https://example.com/repo/path"),
Some("https://example.com/repo/path"),
);
git::set_remote_urls(
repo.store(),
remote_name.as_ref(),
None,
Some("git@example.com:repo/path"),
)?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
assert_fetch_and_push_urls(
repo,
remote_name,
Some("https://example.com/repo/path"),
Some("git@example.com:repo/path"),
);
git::set_remote_urls(
repo.store(),
remote_name.as_ref(),
Some("https://example.com/repo/path2"),
None,
)?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
assert_fetch_and_push_urls(
repo,
remote_name,
Some("https://example.com/repo/path2"),
Some("git@example.com:repo/path"),
);
git::set_remote_urls(
repo.store(),
remote_name.as_ref(),
Some("https://example.com/repo/path3"),
Some("git@example.com:repo/path3"),
)?;
let repo = &test_repo
.env
.load_repo_at_head(&testutils::user_settings(), test_repo.repo_path());
assert_fetch_and_push_urls(
repo,
remote_name,
Some("https://example.com/repo/path3"),
Some("git@example.com:repo/path3"),
);
Ok(())
}