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]
|
||||
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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue