forked from mirrors/jj
git: unset unborn HEAD ref on export
Otherwise, newly created default branch would be re-imported as a new Git HEAD. This could be addressed by cmd_git_init(), but the same situation can be crafted by using "git checkout -b".
This commit is contained in:
parent
00ae8603db
commit
5e7cb3435e
3 changed files with 89 additions and 23 deletions
|
@ -570,12 +570,28 @@ fn test_git_init_colocated_via_flag_git_dir_exists() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_git_init_colocated_via_flag_git_dir_not_exists() {
|
fn test_git_init_colocated_via_flag_git_dir_not_exists() {
|
||||||
let test_env = TestEnvironment::default();
|
let test_env = TestEnvironment::default();
|
||||||
|
let workspace_root = test_env.env_root().join("repo");
|
||||||
let (stdout, stderr) =
|
let (stdout, stderr) =
|
||||||
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]);
|
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]);
|
||||||
insta::assert_snapshot!(stdout, @"");
|
insta::assert_snapshot!(stdout, @"");
|
||||||
insta::assert_snapshot!(stderr, @r###"
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
Initialized repo in "repo"
|
Initialized repo in "repo"
|
||||||
"###);
|
"###);
|
||||||
|
// No HEAD@git ref is available yet
|
||||||
|
insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
|
||||||
|
@ 230dd059e1b0
|
||||||
|
◉ 000000000000
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Create the default branch (create both in case we change the default)
|
||||||
|
test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "main", "master"]);
|
||||||
|
|
||||||
|
// If .git/HEAD pointed to the default branch, new working-copy commit would
|
||||||
|
// be created on top.
|
||||||
|
insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###"
|
||||||
|
@ 230dd059e1b0 main master
|
||||||
|
◉ 000000000000
|
||||||
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -670,23 +670,22 @@ pub fn export_some_refs(
|
||||||
.and_then(parse_git_ref)
|
.and_then(parse_git_ref)
|
||||||
{
|
{
|
||||||
let old_target = head_ref.inner.target.clone();
|
let old_target = head_ref.inner.target.clone();
|
||||||
if let Ok(current_git_commit_id) = head_ref.into_fully_peeled_id() {
|
let current_oid = match head_ref.into_fully_peeled_id() {
|
||||||
let detach_head =
|
Ok(id) => Some(id.detach()),
|
||||||
if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) {
|
Err(gix::reference::peel::Error::ToId(gix::refs::peel::to_id::Error::Follow(
|
||||||
*new_oid != current_git_commit_id
|
gix::refs::file::find::existing::Error::NotFound { .. },
|
||||||
} else {
|
))) => None, // Unborn ref should be considered absent
|
||||||
branches_to_delete.contains_key(&parsed_ref)
|
Err(err) => return Err(GitExportError::from_git(err)),
|
||||||
};
|
};
|
||||||
if detach_head {
|
let new_oid = if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) {
|
||||||
git_repo
|
Some(new_oid)
|
||||||
.reference(
|
} else if branches_to_delete.contains_key(&parsed_ref) {
|
||||||
"HEAD",
|
None
|
||||||
current_git_commit_id,
|
} else {
|
||||||
gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
|
current_oid.as_ref()
|
||||||
"export from jj",
|
};
|
||||||
)
|
if new_oid != current_oid.as_ref() {
|
||||||
.map_err(GitExportError::from_git)?;
|
update_git_head(&git_repo, old_target, current_oid)?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -921,6 +920,49 @@ fn update_git_ref(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures `HEAD@git` is detached and pointing to the `new_oid`. If `new_oid`
|
||||||
|
/// is `None` (meaning absent), dummy placeholder ref will be set.
|
||||||
|
fn update_git_head(
|
||||||
|
git_repo: &gix::Repository,
|
||||||
|
old_target: gix::refs::Target,
|
||||||
|
new_oid: Option<gix::ObjectId>,
|
||||||
|
) -> Result<(), GitExportError> {
|
||||||
|
let mut ref_edits = Vec::new();
|
||||||
|
let new_target = if let Some(oid) = new_oid {
|
||||||
|
gix::refs::Target::Peeled(oid)
|
||||||
|
} else {
|
||||||
|
// Can't detach HEAD without a commit. Use placeholder ref to nullify
|
||||||
|
// the HEAD. The placeholder ref isn't a normal branch ref. Git CLI
|
||||||
|
// appears to deal with that, and can move the placeholder ref. So we
|
||||||
|
// need to ensure that the ref doesn't exist.
|
||||||
|
ref_edits.push(gix::refs::transaction::RefEdit {
|
||||||
|
change: gix::refs::transaction::Change::Delete {
|
||||||
|
expected: gix::refs::transaction::PreviousValue::Any,
|
||||||
|
log: gix::refs::transaction::RefLog::AndReference,
|
||||||
|
},
|
||||||
|
name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
|
||||||
|
deref: false,
|
||||||
|
});
|
||||||
|
gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
|
||||||
|
};
|
||||||
|
ref_edits.push(gix::refs::transaction::RefEdit {
|
||||||
|
change: gix::refs::transaction::Change::Update {
|
||||||
|
log: gix::refs::transaction::LogChange {
|
||||||
|
message: "export from jj".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
|
||||||
|
new: new_target,
|
||||||
|
},
|
||||||
|
name: "HEAD".try_into().unwrap(),
|
||||||
|
deref: false,
|
||||||
|
});
|
||||||
|
git_repo
|
||||||
|
.edit_references(ref_edits)
|
||||||
|
.map_err(GitExportError::from_git)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets `HEAD@git` to the parent of the given working-copy commit and resets
|
/// Sets `HEAD@git` to the parent of the given working-copy commit and resets
|
||||||
/// the Git index.
|
/// the Git index.
|
||||||
pub fn reset_head(
|
pub fn reset_head(
|
||||||
|
|
|
@ -40,6 +40,7 @@ use jj_lib::str_util::StringPattern;
|
||||||
use jj_lib::workspace::Workspace;
|
use jj_lib::workspace::Workspace;
|
||||||
use maplit::{btreemap, hashset};
|
use maplit::{btreemap, hashset};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use test_case::test_case;
|
||||||
use testutils::{
|
use testutils::{
|
||||||
commit_transactions, create_random_commit, load_repo_at_head, write_random_commit, TestRepo,
|
commit_transactions, create_random_commit, load_repo_at_head, write_random_commit, TestRepo,
|
||||||
TestRepoBackend,
|
TestRepoBackend,
|
||||||
|
@ -1543,8 +1544,9 @@ fn test_export_refs_current_branch_changed() {
|
||||||
assert!(git_repo.head_detached().unwrap());
|
assert!(git_repo.head_detached().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_case(false; "without moved placeholder ref")]
|
||||||
fn test_export_refs_unborn_git_branch() {
|
#[test_case(true; "with moved placeholder ref")]
|
||||||
|
fn test_export_refs_unborn_git_branch(move_placeholder_ref: bool) {
|
||||||
// Can export to an empty Git repo (we can handle Git's "unborn branch" state)
|
// Can export to an empty Git repo (we can handle Git's "unborn branch" state)
|
||||||
let test_data = GitRepoData::create();
|
let test_data = GitRepoData::create();
|
||||||
let git_settings = GitSettings::default();
|
let git_settings = GitSettings::default();
|
||||||
|
@ -1556,9 +1558,15 @@ fn test_export_refs_unborn_git_branch() {
|
||||||
git::import_refs(mut_repo, &git_settings).unwrap();
|
git::import_refs(mut_repo, &git_settings).unwrap();
|
||||||
mut_repo.rebase_descendants(&test_data.settings).unwrap();
|
mut_repo.rebase_descendants(&test_data.settings).unwrap();
|
||||||
assert!(git::export_refs(mut_repo).unwrap().is_empty());
|
assert!(git::export_refs(mut_repo).unwrap().is_empty());
|
||||||
|
assert!(git_repo.head().is_err(), "HEAD is unborn");
|
||||||
|
|
||||||
let new_commit = write_random_commit(mut_repo, &test_data.settings);
|
let new_commit = write_random_commit(mut_repo, &test_data.settings);
|
||||||
mut_repo.set_local_branch_target("main", RefTarget::normal(new_commit.id().clone()));
|
mut_repo.set_local_branch_target("main", RefTarget::normal(new_commit.id().clone()));
|
||||||
|
if move_placeholder_ref {
|
||||||
|
git_repo
|
||||||
|
.reference("refs/jj/root", git_id(&new_commit), false, "")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
assert!(git::export_refs(mut_repo).unwrap().is_empty());
|
assert!(git::export_refs(mut_repo).unwrap().is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mut_repo.get_git_ref("refs/heads/main"),
|
mut_repo.get_git_ref("refs/heads/main"),
|
||||||
|
@ -1573,10 +1581,10 @@ fn test_export_refs_unborn_git_branch() {
|
||||||
.id(),
|
.id(),
|
||||||
git_id(&new_commit)
|
git_id(&new_commit)
|
||||||
);
|
);
|
||||||
// It's weird that the head is still pointing to refs/heads/main, but
|
// HEAD should no longer point to refs/heads/main
|
||||||
// it doesn't seem that Git lets you be on an "unborn branch" while
|
assert!(git_repo.head().is_err(), "HEAD is unborn");
|
||||||
// also being in a "detached HEAD" state.
|
// The placeholder ref should be deleted if any
|
||||||
assert!(!git_repo.head_detached().unwrap());
|
assert!(git_repo.find_reference("refs/jj/root").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue