ok/jj
1
0
Fork 0
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:
Yuya Nishihara 2024-05-30 10:42:42 +09:00
parent 00ae8603db
commit 5e7cb3435e
3 changed files with 89 additions and 23 deletions

View file

@ -570,12 +570,28 @@ fn test_git_init_colocated_via_flag_git_dir_exists() {
#[test]
fn test_git_init_colocated_via_flag_git_dir_not_exists() {
let test_env = TestEnvironment::default();
let workspace_root = test_env.env_root().join("repo");
let (stdout, stderr) =
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
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]

View file

@ -670,23 +670,22 @@ pub fn export_some_refs(
.and_then(parse_git_ref)
{
let old_target = head_ref.inner.target.clone();
if let Ok(current_git_commit_id) = head_ref.into_fully_peeled_id() {
let detach_head =
if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) {
*new_oid != current_git_commit_id
} else {
branches_to_delete.contains_key(&parsed_ref)
};
if detach_head {
git_repo
.reference(
"HEAD",
current_git_commit_id,
gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
"export from jj",
)
.map_err(GitExportError::from_git)?;
}
let current_oid = match head_ref.into_fully_peeled_id() {
Ok(id) => Some(id.detach()),
Err(gix::reference::peel::Error::ToId(gix::refs::peel::to_id::Error::Follow(
gix::refs::file::find::existing::Error::NotFound { .. },
))) => None, // Unborn ref should be considered absent
Err(err) => return Err(GitExportError::from_git(err)),
};
let new_oid = if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) {
Some(new_oid)
} else if branches_to_delete.contains_key(&parsed_ref) {
None
} else {
current_oid.as_ref()
};
if new_oid != current_oid.as_ref() {
update_git_head(&git_repo, old_target, current_oid)?;
}
}
}
@ -921,6 +920,49 @@ fn update_git_ref(
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
/// the Git index.
pub fn reset_head(

View file

@ -40,6 +40,7 @@ use jj_lib::str_util::StringPattern;
use jj_lib::workspace::Workspace;
use maplit::{btreemap, hashset};
use tempfile::TempDir;
use test_case::test_case;
use testutils::{
commit_transactions, create_random_commit, load_repo_at_head, write_random_commit, TestRepo,
TestRepoBackend,
@ -1543,8 +1544,9 @@ fn test_export_refs_current_branch_changed() {
assert!(git_repo.head_detached().unwrap());
}
#[test]
fn test_export_refs_unborn_git_branch() {
#[test_case(false; "without moved placeholder ref")]
#[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)
let test_data = GitRepoData::create();
let git_settings = GitSettings::default();
@ -1556,9 +1558,15 @@ fn test_export_refs_unborn_git_branch() {
git::import_refs(mut_repo, &git_settings).unwrap();
mut_repo.rebase_descendants(&test_data.settings).unwrap();
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);
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_eq!(
mut_repo.get_git_ref("refs/heads/main"),
@ -1573,10 +1581,10 @@ fn test_export_refs_unborn_git_branch() {
.id(),
git_id(&new_commit)
);
// It's weird that the head is still pointing to refs/heads/main, but
// it doesn't seem that Git lets you be on an "unborn branch" while
// also being in a "detached HEAD" state.
assert!(!git_repo.head_detached().unwrap());
// HEAD should no longer point to refs/heads/main
assert!(git_repo.head().is_err(), "HEAD is unborn");
// The placeholder ref should be deleted if any
assert!(git_repo.find_reference("refs/jj/root").is_err());
}
#[test]