git: add dummy conflict to index if necessary
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
nix / flake check (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Run doctests (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with latest Python and uv (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

If the parent tree contains conflicts, we want the index to also contain
a conflict to ensure that the use can't accidentally commit conflict
markers using `git commit`. Since we can't represent conflicts with more
than 2 sides in the Git index, we need to add a dummy conflict in this
case. We use ".jj-do-not-resolve-this-conflict" as the dummy conflict to
indicate to the user that they should not attempt to resolve this
conflict using Git.
This commit is contained in:
Scott Taylor 2024-12-28 10:33:27 -06:00 committed by Scott Taylor
parent 42b390bbc4
commit e6a51d6637
2 changed files with 54 additions and 6 deletions

View file

@ -1072,9 +1072,10 @@ fn test_git_colocated_update_index_3_sided_conflict() {
0000000000000000000000000000000000000000
"#);
// We can't add conflicts with more than 2 sides to the index, so they should
// show as unconflicted. The stat for base.txt should not change.
// We can't add conflicts with more than 2 sides to the index, so we add a dummy
// conflict instead. The stat for base.txt should not change.
insta::assert_snapshot!(get_index_state(&repo_path), @r#"
Ours Mode(FILE) eb8299123d2a ctime=0:0 mtime=0:0 size=0 .jj-do-not-resolve-this-conflict
Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 conflict.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 side-1.txt
@ -1099,6 +1100,20 @@ fn test_git_colocated_update_index_3_sided_conflict() {
// Index should be the same after `jj new`.
insta::assert_snapshot!(get_index_state(&repo_path), @r#"
Ours Mode(FILE) eb8299123d2a ctime=0:0 mtime=0:0 size=0 .jj-do-not-resolve-this-conflict
Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 conflict.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 side-1.txt
Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 side-2.txt
Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 side-3.txt
"#);
// If we add a file named ".jj-do-not-resolve-this-conflict", it should take
// precedence over the dummy conflict.
std::fs::write(repo_path.join(".jj-do-not-resolve-this-conflict"), "file\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["new"]);
insta::assert_snapshot!(get_index_state(&repo_path), @r#"
Unconflicted Mode(FILE) f73f3093ff86 ctime=0:0 mtime=0:0 size=0 .jj-do-not-resolve-this-conflict
Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 conflict.txt
Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 side-1.txt

View file

@ -57,6 +57,9 @@ use crate::view::View;
pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &str = "git";
/// Ref name used as a placeholder to unset HEAD without a commit.
const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
/// Dummy file to be added to the index to indicate that the user is editing a
/// commit with a conflict that isn't represented in the Git index.
const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)]
pub enum RefName {
@ -1030,8 +1033,8 @@ pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(),
let parent_tree = wc_commit.parent_tree(mut_repo)?;
// Use the merged parent tree as the Git index, allowing `git diff` to show the
// same changes as `jj diff`. If the merged parent tree has 2-sided conflicts,
// then the Git index will also be conflicted.
// same changes as `jj diff`. If the merged parent tree has conflicts, then the
// Git index will also be conflicted.
let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
if tree.id() == mut_repo.store().empty_tree_id() {
// If the tree is empty, gix can fail to load the object (since Git doesn't
@ -1124,6 +1127,8 @@ fn build_index_from_merged_tree(
);
};
let mut has_many_sided_conflict = false;
for (path, entry) in merged_tree.entries() {
let entry = entry?;
if let Some(resolved) = entry.as_resolved() {
@ -1142,7 +1147,10 @@ fn build_index_from_merged_tree(
// first side as staged. This is preferable to adding the first 2 sides as a
// conflict, since some tools rely on being able to resolve conflicts using the
// index, which could lead to an incorrect conflict resolution if the index
// didn't contain all of the conflict sides.
// didn't contain all of the conflict sides. Instead, we add a dummy conflict of
// a file named ".jj-do-not-resolve-this-conflict" to prevent the user from
// accidentally committing the conflict markers.
has_many_sided_conflict = true;
push_index_entry(
&path,
conflict.first(),
@ -1151,9 +1159,34 @@ fn build_index_from_merged_tree(
}
}
// Required after `dangerously_push_entry` for correctness
// Required after `dangerously_push_entry` for correctness. We use do a lookup
// in the index after this, so it must be sorted before we do the lookup.
index.sort_entries();
// If the conflict had an unrepresentable conflict and the dummy file path isn't
// already added in the index, add a dummy file as a conflict.
if has_many_sided_conflict
&& index
.entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
.is_err()
{
let file_blob = git_repo
.write_blob(
b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
)
.map_err(GitExportError::from_git)?;
index.dangerously_push_entry(
gix::index::entry::Stat::default(),
file_blob.detach(),
gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
gix::index::entry::Mode::FILE,
INDEX_DUMMY_CONFLICT_FILE.into(),
);
// We need to sort again for correctness before writing the index file since we
// added a new entry.
index.sort_entries();
}
Ok(index)
}