use std::path::PathBuf;
use std::sync::Arc;
use git2::Oid;
use itertools::Itertools;
use jujutsu_lib::backend::{CommitId, ObjectId};
use jujutsu_lib::commit::Commit;
use jujutsu_lib::git;
use jujutsu_lib::git::{GitFetchError, GitPushError, GitRefUpdate};
use jujutsu_lib::git_backend::GitBackend;
use jujutsu_lib::op_store::{BranchTarget, RefTarget};
use jujutsu_lib::repo::{ReadonlyRepo, Repo};
use jujutsu_lib::settings::{GitSettings, UserSettings};
use maplit::{btreemap, hashset};
use tempfile::TempDir;
use testutils::{create_random_commit, write_random_commit, TestRepo};
fn empty_git_commit<'r>(
git_repo: &'r git2::Repository,
ref_name: &str,
parents: &[&git2::Commit],
) -> git2::Commit<'r> {
let signature = git2::Signature::now("Someone", "someone@example.com").unwrap();
let empty_tree_id = Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
let empty_tree = git_repo.find_tree(empty_tree_id).unwrap();
let oid = git_repo
.commit(
Some(ref_name),
&signature,
&signature,
&format!("random commit {}", rand::random::<u32>()),
&empty_tree,
parents,
)
.unwrap();
git_repo.find_commit(oid).unwrap()
}
fn jj_id(commit: &git2::Commit) -> CommitId {
CommitId::from_bytes(commit.id().as_bytes())
}
fn git_id(commit: &Commit) -> Oid {
Oid::from_bytes(commit.id().as_bytes()).unwrap()
}
#[test]
fn test_import_refs() {
let settings = testutils::user_settings();
let git_settings = GitSettings::default();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let git_repo = repo.store().git_repo().unwrap();
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
let commit5 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[&commit1]);
let commit6 = empty_git_commit(&git_repo, "refs/remotes/origin/feature3", &[&commit1]);
empty_git_commit(&git_repo, "refs/notes/x", &[&commit2]);
empty_git_commit(&git_repo, "refs/remotes/origin/HEAD", &[&commit2]);
git_repo.set_head("refs/heads/main").unwrap();
let git_repo = repo.store().git_repo().unwrap();
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
let repo = tx.commit();
let view = repo.view();
let expected_heads = hashset! {
jj_id(&commit3),
jj_id(&commit4),
jj_id(&commit5),
jj_id(&commit6)
};
assert_eq!(*view.heads(), expected_heads);
let expected_main_branch = BranchTarget {
local_target: Some(RefTarget::Normal(jj_id(&commit2))),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::Normal(jj_id(&commit1)),
},
};
assert_eq!(
view.branches().get("main"),
Some(expected_main_branch).as_ref()
);
let expected_feature1_branch = BranchTarget {
local_target: Some(RefTarget::Normal(jj_id(&commit3))),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature1"),
Some(expected_feature1_branch).as_ref()
);
let expected_feature2_branch = BranchTarget {
local_target: Some(RefTarget::Normal(jj_id(&commit4))),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature2"),
Some(expected_feature2_branch).as_ref()
);
let expected_feature3_branch = BranchTarget {
local_target: Some(RefTarget::Normal(jj_id(&commit6))),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::Normal(jj_id(&commit6)),
},
};
assert_eq!(
view.branches().get("feature3"),
Some(expected_feature3_branch).as_ref()
);
assert_eq!(
view.tags().get("v1.0"),
Some(RefTarget::Normal(jj_id(&commit5))).as_ref()
);
assert_eq!(view.git_refs().len(), 6);
assert_eq!(
view.git_refs().get("refs/heads/main"),
Some(RefTarget::Normal(jj_id(&commit2))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/heads/feature1"),
Some(RefTarget::Normal(jj_id(&commit3))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/heads/feature2"),
Some(RefTarget::Normal(jj_id(&commit4))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/remotes/origin/main"),
Some(RefTarget::Normal(jj_id(&commit1))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/remotes/origin/feature3"),
Some(RefTarget::Normal(jj_id(&commit6))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/tags/v1.0"),
Some(RefTarget::Normal(jj_id(&commit5))).as_ref()
);
assert_eq!(view.git_head(), Some(&RefTarget::Normal(jj_id(&commit2))));
}
#[test]
fn test_import_refs_reimport() {
let settings = testutils::user_settings();
let git_settings = GitSettings::default();
let test_workspace = TestRepo::init(true);
let repo = &test_workspace.repo;
let git_repo = repo.store().git_repo().unwrap();
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
let _commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
let pgp_key_oid = git_repo.blob(b"my PGP key").unwrap();
git_repo
.reference("refs/tags/my-gpg-key", pgp_key_oid, false, "")
.unwrap();
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
let repo = tx.commit();
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(&settings, "test");
let commit6 = create_random_commit(tx.mut_repo(), &settings)
.set_parents(vec![jj_id(&commit2)])
.write()
.unwrap();
tx.mut_repo().set_local_branch(
"feature2".to_string(),
RefTarget::Normal(commit6.id().clone()),
);
let repo = tx.commit();
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
let repo = tx.commit();
let view = repo.view();
let expected_heads = hashset! {
jj_id(&commit5),
commit6.id().clone(),
};
assert_eq!(*view.heads(), expected_heads);
assert_eq!(view.branches().len(), 2);
let commit1_target = RefTarget::Normal(jj_id(&commit1));
let commit2_target = RefTarget::Normal(jj_id(&commit2));
let expected_main_branch = BranchTarget {
local_target: Some(RefTarget::Normal(jj_id(&commit2))),
remote_targets: btreemap! {
"origin".to_string() => commit1_target.clone(),
},
};
assert_eq!(
view.branches().get("main"),
Some(expected_main_branch).as_ref()
);
let expected_feature2_branch = BranchTarget {
local_target: Some(RefTarget::Conflict {
removes: vec![jj_id(&commit4)],
adds: vec![commit6.id().clone(), jj_id(&commit5)],
}),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature2"),
Some(expected_feature2_branch).as_ref()
);
assert!(view.tags().is_empty());
assert_eq!(view.git_refs().len(), 3);
assert_eq!(
view.git_refs().get("refs/heads/main"),
Some(commit2_target).as_ref()
);
assert_eq!(
view.git_refs().get("refs/remotes/origin/main"),
Some(commit1_target).as_ref()
);
let commit5_target = RefTarget::Normal(jj_id(&commit5));
assert_eq!(
view.git_refs().get("refs/heads/feature2"),
Some(commit5_target).as_ref()
);
}
#[test]
fn test_import_refs_reimport_head_removed() {
let settings = testutils::user_settings();
let git_settings = GitSettings::default();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let git_repo = repo.store().git_repo().unwrap();
let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
let commit_id = jj_id(&commit);
assert!(tx.mut_repo().view().heads().contains(&commit_id));
tx.mut_repo().remove_head(&commit_id);
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
assert!(!tx.mut_repo().view().heads().contains(&commit_id));
}
#[test]
fn test_import_refs_reimport_git_head_counts() {
let settings = testutils::user_settings();
let git_settings = GitSettings::default();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let git_repo = repo.store().git_repo().unwrap();
let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_repo.set_head_detached(commit.id()).unwrap();
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
git_repo
.find_reference("refs/heads/main")
.unwrap()
.delete()
.unwrap();
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit)));
}
#[test]
fn test_import_refs_reimport_all_from_root_removed() {
let settings = testutils::user_settings();
let git_settings = GitSettings::default();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
let git_repo = repo.store().git_repo().unwrap();
let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
let mut tx = repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
assert!(tx.mut_repo().view().heads().contains(&jj_id(&commit)));
git_repo
.find_reference("refs/heads/main")
.unwrap()
.delete()
.unwrap();
git::import_refs(tx.mut_repo(), &git_repo, &git_settings).unwrap();
tx.mut_repo().rebase_descendants(&settings).unwrap();
assert!(!tx.mut_repo().view().heads().contains(&jj_id(&commit)));
}
fn git_ref(git_repo: &git2::Repository, name: &str, target: Oid) {
git_repo.reference(name, target, true, "").unwrap();
}
fn delete_git_ref(git_repo: &git2::Repository, name: &str) {
git_repo.find_reference(name).unwrap().delete().unwrap();
}
struct GitRepoData {
settings: UserSettings,
_temp_dir: TempDir,
origin_repo: git2::Repository,
git_repo: git2::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 = git2::Repository::init_bare(&origin_repo_dir).unwrap();
let git_repo_dir = temp_dir.path().join("git");
let git_repo =
git2::Repository::clone(origin_repo_dir.to_str().unwrap(), &git_repo_dir).unwrap();
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,
|store_path| Box::new(GitBackend::init_external(store_path, &git_repo_dir)),
ReadonlyRepo::default_op_store_factory(),
ReadonlyRepo::default_op_heads_store_factory(),
)
.unwrap();
Self {
settings,
_temp_dir: temp_dir,
origin_repo,
git_repo,
repo,
}
}
}
#[test]
fn test_import_refs_empty_git_repo() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let heads_before = test_data.repo.view().heads().clone();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
git::import_refs(tx.mut_repo(), &test_data.git_repo, &git_settings).unwrap();
tx.mut_repo()
.rebase_descendants(&test_data.settings)
.unwrap();
let repo = tx.commit();
assert_eq!(*repo.view().heads(), heads_before);
assert_eq!(repo.view().branches().len(), 0);
assert_eq!(repo.view().tags().len(), 0);
assert_eq!(repo.view().git_refs().len(), 0);
assert_eq!(repo.view().git_head(), None);
}
#[test]
fn test_import_refs_detached_head() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let commit1 = empty_git_commit(&test_data.git_repo, "refs/heads/main", &[]);
test_data
.git_repo
.find_reference("refs/heads/main")
.unwrap()
.delete()
.unwrap();
test_data.git_repo.set_head_detached(commit1.id()).unwrap();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
git::import_refs(tx.mut_repo(), &test_data.git_repo, &git_settings).unwrap();
tx.mut_repo()
.rebase_descendants(&test_data.settings)
.unwrap();
let repo = tx.commit();
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(),
Some(&RefTarget::Normal(jj_id(&commit1)))
);
}
#[test]
fn test_export_refs_no_detach() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_repo.set_head("refs/heads/main").unwrap();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
mut_repo.rebase_descendants(&test_data.settings).unwrap();
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(jj_id(&commit1)))
);
assert_eq!(git_repo.head().unwrap().name(), Some("refs/heads/main"));
assert_eq!(
git_repo.find_reference("refs/heads/main").unwrap().target(),
Some(commit1.id())
);
}
#[test]
fn test_export_refs_branch_changed() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let git_repo = test_data.git_repo;
let commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_repo
.reference("refs/heads/feature", commit.id(), false, "test")
.unwrap();
git_repo.set_head("refs/heads/feature").unwrap();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
mut_repo.rebase_descendants(&test_data.settings).unwrap();
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
let new_commit = create_random_commit(mut_repo, &test_data.settings)
.set_parents(vec![jj_id(&commit)])
.write()
.unwrap();
mut_repo.set_local_branch(
"main".to_string(),
RefTarget::Normal(new_commit.id().clone()),
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(new_commit.id().clone()))
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")
.unwrap()
.peel_to_commit()
.unwrap()
.id(),
git_id(&new_commit)
);
assert_eq!(git_repo.head().unwrap().name(), Some("refs/heads/feature"));
}
#[test]
fn test_export_refs_current_branch_changed() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let git_repo = test_data.git_repo;
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_repo.set_head("refs/heads/main").unwrap();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
mut_repo.rebase_descendants(&test_data.settings).unwrap();
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
let new_commit = create_random_commit(mut_repo, &test_data.settings)
.set_parents(vec![jj_id(&commit1)])
.write()
.unwrap();
mut_repo.set_local_branch(
"main".to_string(),
RefTarget::Normal(new_commit.id().clone()),
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(new_commit.id().clone()))
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")
.unwrap()
.peel_to_commit()
.unwrap()
.id(),
git_id(&new_commit)
);
assert!(git_repo.head_detached().unwrap());
}
#[test]
fn test_export_refs_unborn_git_branch() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let git_repo = test_data.git_repo;
git_repo.set_head("refs/heads/main").unwrap();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
mut_repo.rebase_descendants(&test_data.settings).unwrap();
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
let new_commit = write_random_commit(mut_repo, &test_data.settings);
mut_repo.set_local_branch(
"main".to_string(),
RefTarget::Normal(new_commit.id().clone()),
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(new_commit.id().clone()))
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")
.unwrap()
.peel_to_commit()
.unwrap()
.id(),
git_id(&new_commit)
);
assert!(!git_repo.head_detached().unwrap());
}
#[test]
fn test_export_import_sequence() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let git_repo = test_data.git_repo;
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
let commit_a = write_random_commit(mut_repo, &test_data.settings);
let commit_b = write_random_commit(mut_repo, &test_data.settings);
let commit_c = write_random_commit(mut_repo, &test_data.settings);
git_repo
.reference("refs/heads/main", git_id(&commit_a), true, "test")
.unwrap();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(commit_a.id().clone()))
);
mut_repo.set_local_branch("main".to_string(), RefTarget::Normal(commit_b.id().clone()));
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(commit_b.id().clone()))
);
git_repo
.reference("refs/heads/main", git_id(&commit_c), true, "test")
.unwrap();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
assert_eq!(
mut_repo.get_git_ref("refs/heads/main"),
Some(RefTarget::Normal(commit_c.id().clone()))
);
assert_eq!(
mut_repo.view().get_local_branch("main"),
Some(RefTarget::Normal(commit_c.id().clone()))
);
}
#[test]
fn test_import_export_no_auto_local_branch() {
let test_data = GitRepoData::create();
let git_settings = GitSettings {
auto_local_branch: false,
};
let git_repo = test_data.git_repo;
let git_commit = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
let expected_branch = BranchTarget {
local_target: None,
remote_targets: btreemap! {
"origin".to_string() => RefTarget::Normal(jj_id(&git_commit))
},
};
assert_eq!(
mut_repo.view().branches().get("main"),
Some(expected_branch).as_ref()
);
assert_eq!(
mut_repo.get_git_ref("refs/remotes/origin/main"),
Some(RefTarget::Normal(jj_id(&git_commit)))
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(mut_repo.get_git_ref("refs/heads/main"), None);
}
#[test]
fn test_export_conflicts() {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
let commit_a = write_random_commit(mut_repo, &test_data.settings);
let commit_b = write_random_commit(mut_repo, &test_data.settings);
let commit_c = write_random_commit(mut_repo, &test_data.settings);
mut_repo.set_local_branch("main".to_string(), RefTarget::Normal(commit_a.id().clone()));
mut_repo.set_local_branch(
"feature".to_string(),
RefTarget::Normal(commit_a.id().clone()),
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
mut_repo.set_local_branch("main".to_string(), RefTarget::Normal(commit_b.id().clone()));
mut_repo.set_local_branch(
"feature".to_string(),
RefTarget::Conflict {
removes: vec![commit_a.id().clone()],
adds: vec![commit_b.id().clone(), commit_c.id().clone()],
},
);
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(
git_repo
.find_reference("refs/heads/feature")
.unwrap()
.target()
.unwrap(),
git_id(&commit_a)
);
assert_eq!(
git_repo
.find_reference("refs/heads/main")
.unwrap()
.target()
.unwrap(),
git_id(&commit_b)
);
}
#[test]
fn test_export_partial_failure() {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
let commit_a = write_random_commit(mut_repo, &test_data.settings);
let target = RefTarget::Normal(commit_a.id().clone());
mut_repo.set_local_branch("".to_string(), target.clone());
mut_repo.set_local_branch("main".to_string(), target.clone());
mut_repo.set_local_branch("main/sub".to_string(), target);
assert_eq!(
git::export_refs(mut_repo, &git_repo),
Ok(vec!["".to_string(), "main/sub".to_string()])
);
assert!(git_repo.find_reference("refs/heads/").is_err());
assert_eq!(
git_repo
.find_reference("refs/heads/main")
.unwrap()
.target()
.unwrap(),
git_id(&commit_a)
);
assert!(git_repo.find_reference("refs/heads/main/sub").is_err());
mut_repo.remove_local_branch("main");
assert_eq!(
git::export_refs(mut_repo, &git_repo),
Ok(vec!["".to_string()])
);
assert!(git_repo.find_reference("refs/heads/").is_err());
assert!(git_repo.find_reference("refs/heads/main").is_err());
assert_eq!(
git_repo
.find_reference("refs/heads/main/sub")
.unwrap()
.target()
.unwrap(),
git_id(&commit_a)
);
}
#[test]
fn test_export_reexport_transitions() {
let test_data = GitRepoData::create();
let git_repo = test_data.git_repo;
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let mut_repo = tx.mut_repo();
let commit_a = write_random_commit(mut_repo, &test_data.settings);
let commit_b = write_random_commit(mut_repo, &test_data.settings);
let commit_c = write_random_commit(mut_repo, &test_data.settings);
for branch in [
"AAB", "AAX", "ABA", "ABB", "ABC", "ABX", "AXA", "AXB", "AXX",
] {
mut_repo.set_local_branch(branch.to_string(), RefTarget::Normal(commit_a.id().clone()));
}
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
for branch in ["AXA", "AXB", "AXX"] {
mut_repo.remove_local_branch(branch);
}
for branch in ["XAA", "XAB", "XAX"] {
mut_repo.set_local_branch(branch.to_string(), RefTarget::Normal(commit_a.id().clone()));
}
for branch in ["ABA", "ABB", "ABC", "ABX"] {
mut_repo.set_local_branch(branch.to_string(), RefTarget::Normal(commit_b.id().clone()));
}
for branch in ["AAX", "ABX", "AXX"] {
git_repo
.find_reference(&format!("refs/heads/{branch}"))
.unwrap()
.delete()
.unwrap();
}
for branch in ["XAA", "XXA"] {
git_repo
.reference(&format!("refs/heads/{branch}"), git_id(&commit_a), true, "")
.unwrap();
}
for branch in ["AAB", "ABB", "AXB", "XAB"] {
git_repo
.reference(&format!("refs/heads/{branch}"), git_id(&commit_b), true, "")
.unwrap();
}
let branch = "ABC";
git_repo
.reference(&format!("refs/heads/{branch}"), git_id(&commit_c), true, "")
.unwrap();
assert_eq!(
git::export_refs(mut_repo, &git_repo),
Ok(["AXB", "ABC", "ABX", "XAB"]
.into_iter()
.map(String::from)
.collect_vec())
);
for branch in ["AAX", "ABX", "AXA", "AXX"] {
assert!(
git_repo
.find_reference(&format!("refs/heads/{branch}"))
.is_err(),
"{branch} should not exist"
);
}
for branch in ["XAA", "XAX", "XXA"] {
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{branch}"))
.unwrap()
.target(),
Some(git_id(&commit_a)),
"{branch} should point to commit A"
);
}
for branch in ["AAB", "ABA", "AAB", "ABB", "AXB", "XAB"] {
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{branch}"))
.unwrap()
.target(),
Some(git_id(&commit_b)),
"{branch} should point to commit B"
);
}
let branch = "ABC";
assert_eq!(
git_repo
.find_reference(&format!("refs/heads/{branch}"))
.unwrap()
.target(),
Some(git_id(&commit_c)),
"{branch} should point to commit C"
);
assert_eq!(
*mut_repo.view().git_refs(),
btreemap! {
"refs/heads/AAX".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/AAB".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/ABA".to_string() => RefTarget::Normal(commit_b.id().clone()),
"refs/heads/ABB".to_string() => RefTarget::Normal(commit_b.id().clone()),
"refs/heads/ABC".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/ABX".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/AXB".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/XAA".to_string() => RefTarget::Normal(commit_a.id().clone()),
"refs/heads/XAX".to_string() => RefTarget::Normal(commit_a.id().clone()),
}
);
}
#[test]
fn test_init() {
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 = git2::Repository::init_bare(&git_repo_dir).unwrap();
let initial_git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
std::fs::create_dir(&jj_repo_dir).unwrap();
let repo = ReadonlyRepo::init(
&settings,
&jj_repo_dir,
|store_path| Box::new(GitBackend::init_external(store_path, &git_repo_dir)),
ReadonlyRepo::default_op_store_factory(),
ReadonlyRepo::default_op_heads_store_factory(),
)
.unwrap();
assert!(!repo.view().heads().contains(&jj_id(&initial_git_commit)));
}
#[test]
fn test_fetch_empty_repo() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let default_branch = git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert_eq!(default_branch, None);
assert_eq!(*tx.mut_repo().view().git_refs(), btreemap! {});
assert_eq!(*tx.mut_repo().view().branches(), btreemap! {});
}
#[test]
fn test_fetch_initial_commit() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let default_branch = git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert_eq!(default_branch, None);
let repo = tx.commit();
let view = repo.view();
assert!(view.heads().contains(&jj_id(&initial_git_commit)));
let initial_commit_target = RefTarget::Normal(jj_id(&initial_git_commit));
assert_eq!(
*view.git_refs(),
btreemap! {
"refs/remotes/origin/main".to_string() => initial_commit_target.clone(),
}
);
assert_eq!(
*view.branches(),
btreemap! {
"main".to_string() => BranchTarget {
local_target: Some(initial_commit_target.clone()),
remote_targets: btreemap! {"origin".to_string() => initial_commit_target}
},
}
);
}
#[test]
fn test_fetch_success() {
let mut test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
test_data.repo = tx.commit();
test_data.origin_repo.set_head("refs/heads/main").unwrap();
let new_git_commit = empty_git_commit(
&test_data.origin_repo,
"refs/heads/main",
&[&initial_git_commit],
);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let default_branch = git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert_eq!(default_branch, Some("main".to_string()));
let repo = tx.commit();
let view = repo.view();
assert!(view.heads().contains(&jj_id(&new_git_commit)));
let new_commit_target = RefTarget::Normal(jj_id(&new_git_commit));
assert_eq!(
*view.git_refs(),
btreemap! {
"refs/remotes/origin/main".to_string() => new_commit_target.clone(),
}
);
assert_eq!(
*view.branches(),
btreemap! {
"main".to_string() => BranchTarget {
local_target: Some(new_commit_target.clone()),
remote_targets: btreemap! {"origin".to_string() => new_commit_target}
},
}
);
}
#[test]
fn test_fetch_prune_deleted_ref() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
empty_git_commit(&test_data.git_repo, "refs/heads/main", &[]);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert!(tx.mut_repo().get_branch("main").is_some());
test_data
.git_repo
.find_reference("refs/heads/main")
.unwrap()
.delete()
.unwrap();
git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert!(tx.mut_repo().get_branch("main").is_none());
}
#[test]
fn test_fetch_no_default_branch() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
empty_git_commit(
&test_data.origin_repo,
"refs/heads/main",
&[&initial_git_commit],
);
test_data
.origin_repo
.set_head_detached(initial_git_commit.id())
.unwrap();
let default_branch = git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"origin",
git::RemoteCallbacks::default(),
&git_settings,
)
.unwrap();
assert_eq!(default_branch, None);
}
#[test]
fn test_fetch_no_such_remote() {
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
let mut tx = test_data
.repo
.start_transaction(&test_data.settings, "test");
let result = git::fetch(
tx.mut_repo(),
&test_data.git_repo,
"invalid-remote",
git::RemoteCallbacks::default(),
&git_settings,
);
assert!(matches!(result, Err(GitFetchError::NoSuchRemote(_))));
}
struct PushTestSetup {
source_repo_dir: PathBuf,
jj_repo: Arc<ReadonlyRepo>,
new_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 = git2::Repository::init_bare(&source_repo_dir).unwrap();
let initial_git_commit = empty_git_commit(&source_repo, "refs/heads/main", &[]);
git2::Repository::clone(source_repo_dir.to_str().unwrap(), &clone_repo_dir).unwrap();
std::fs::create_dir(&jj_repo_dir).unwrap();
let jj_repo = ReadonlyRepo::init(
settings,
&jj_repo_dir,
|store_path| Box::new(GitBackend::init_external(store_path, &clone_repo_dir)),
ReadonlyRepo::default_op_store_factory(),
ReadonlyRepo::default_op_heads_store_factory(),
)
.unwrap();
let mut tx = jj_repo.start_transaction(settings, "test");
let new_commit = create_random_commit(tx.mut_repo(), settings)
.set_parents(vec![jj_id(&initial_git_commit)])
.write()
.unwrap();
let jj_repo = tx.commit();
PushTestSetup {
source_repo_dir,
jj_repo,
new_commit,
}
}
#[test]
fn test_push_updates_success() {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = setup.jj_repo.store().git_repo().unwrap();
let result = git::push_updates(
&clone_repo,
"origin",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: Some(setup.new_commit.id().clone()),
}],
git::RemoteCallbacks::default(),
);
assert_eq!(result, Ok(()));
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
let new_target = source_repo
.find_reference("refs/heads/main")
.unwrap()
.target();
let new_oid = git_id(&setup.new_commit);
assert_eq!(new_target, Some(new_oid));
let new_target = clone_repo
.find_reference("refs/remotes/origin/main")
.unwrap()
.target();
assert_eq!(new_target, Some(new_oid));
}
#[test]
fn test_push_updates_deletion() {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = setup.jj_repo.store().git_repo().unwrap();
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
assert!(source_repo.find_reference("refs/heads/main").is_ok());
let result = git::push_updates(
&setup.jj_repo.store().git_repo().unwrap(),
"origin",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: None,
}],
git::RemoteCallbacks::default(),
);
assert_eq!(result, Ok(()));
assert!(source_repo.find_reference("refs/heads/main").is_err());
assert!(clone_repo
.find_reference("refs/remotes/origin/main")
.is_err());
}
#[test]
fn test_push_updates_mixed_deletion_and_addition() {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = setup.jj_repo.store().git_repo().unwrap();
let result = git::push_updates(
&clone_repo,
"origin",
&[
GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: None,
},
GitRefUpdate {
qualified_name: "refs/heads/topic".to_string(),
force: false,
new_target: Some(setup.new_commit.id().clone()),
},
],
git::RemoteCallbacks::default(),
);
assert_eq!(result, Ok(()));
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
let new_target = source_repo
.find_reference("refs/heads/topic")
.unwrap()
.target();
assert_eq!(new_target, Some(git_id(&setup.new_commit)));
assert!(source_repo.find_reference("refs/heads/main").is_err());
}
#[test]
fn test_push_updates_not_fast_forward() {
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(&settings, "test");
let new_commit = write_random_commit(tx.mut_repo(), &settings);
setup.jj_repo = tx.commit();
let result = git::push_updates(
&setup.jj_repo.store().git_repo().unwrap(),
"origin",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: Some(new_commit.id().clone()),
}],
git::RemoteCallbacks::default(),
);
assert_eq!(result, Err(GitPushError::NotFastForward));
}
#[test]
fn test_push_updates_not_fast_forward_with_force() {
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(&settings, "test");
let new_commit = write_random_commit(tx.mut_repo(), &settings);
setup.jj_repo = tx.commit();
let result = git::push_updates(
&setup.jj_repo.store().git_repo().unwrap(),
"origin",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: true,
new_target: Some(new_commit.id().clone()),
}],
git::RemoteCallbacks::default(),
);
assert_eq!(result, Ok(()));
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
let new_target = source_repo
.find_reference("refs/heads/main")
.unwrap()
.target();
assert_eq!(new_target, Some(git_id(&new_commit)));
}
#[test]
fn test_push_updates_no_such_remote() {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let result = git::push_updates(
&setup.jj_repo.store().git_repo().unwrap(),
"invalid-remote",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: Some(setup.new_commit.id().clone()),
}],
git::RemoteCallbacks::default(),
);
assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
}
#[test]
fn test_push_updates_invalid_remote() {
let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir);
let result = git::push_updates(
&setup.jj_repo.store().git_repo().unwrap(),
"http://invalid-remote",
&[GitRefUpdate {
qualified_name: "refs/heads/main".to_string(),
force: false,
new_target: Some(setup.new_commit.id().clone()),
}],
git::RemoteCallbacks::default(),
);
assert!(matches!(result, Err(GitPushError::NoSuchRemote(_))));
}