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] #[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]

View file

@ -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(

View file

@ -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]