Skip to content

Commit

Permalink
git: add dummy conflict to index if necessary
Browse files Browse the repository at this point in the history
We want to prevent the user from committing with `git commit` whenever
the working copy commit still contains conflicts, since that would
result in conflict markers being committed as text. Git will prevent the
user from committing if there are conflicts present in the index, so
there are two main cases where this can happen:

1. If the working copy commit contains conflicted files which weren't
   conflicted in the merged parent tree, then those files won't appear
   as conflicts in the index (since the index is based on the parents).

2. If there are conflicts with more than 2 sides, those conflicts cannot
   be represented in the index (since Git only allows 2-sided conflicts
   in the index).

To detect these two cases, we start with a set of all paths which are
conflicted in the working copy commit, and whenever we add a conflict to
the index, we remove that path from the set. If there are any paths
remaining in the set after populating the index, that means that there
is a conflicted file in the working copy which is not present in the
index. If this happens, we add a ".jj-do-not-resolve-this-conflict" file
as a dummy conflict in the index. This will prevent the user from
accidentally committing without resolving the conflicts first.

This approach means that in the most common cases (resolving merge
conflicts after running `jj new A B` or resolving rebase conflicts by
running `jj new` on the first conflicted commit), it won't be necessary
to add any dummy conflict to the index since in these cases, the index
already contains all of the conflicts.
  • Loading branch information
scott2000 committed Dec 28, 2024
1 parent 63159ab commit a498758
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 2 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* In colocated repos, the Git index now contains the changes from all parents
of the working copy instead of just the first parent (`HEAD`). 2-sided
conflicts from the merged parents are now added to the Git index as conflicts
as well.
as well. If there are any other conflicts in the working copy, a dummy
conflict is added to the Git index to prevent accidentally committing the
materialized conflict markers.

### Deprecations

Expand Down
37 changes: 36 additions & 1 deletion lib/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use crate::refs::BookmarkPushUpdate;
use crate::repo::MutableRepo;
use crate::repo::Repo;
use crate::repo_path::RepoPath;
use crate::repo_path::RepoPathBuf;
use crate::revset::RevsetExpression;
use crate::settings::GitSettings;
use crate::store::Store;
Expand All @@ -57,6 +58,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 {
Expand Down Expand Up @@ -1053,23 +1057,54 @@ pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(),
);
};

let mut conflicts_not_added_to_index: HashSet<RepoPathBuf> = wc_commit
.tree()?
.conflicts()
.map(|(path, _)| path)
.collect();

// 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.
for (path, entry) in wc_commit.parent_tree(mut_repo)?.entries() {
// We use this path as a dummy file, so we have to ignore it here.
if path.as_internal_file_string() == INDEX_DUMMY_CONFLICT_FILE {
continue;
}
let entry = entry?.simplify();
if let [left, base, right] = entry.as_slice() {
// 2-sided conflicts can be represented in the Git index
push_index_entry(&path, left, Stage::Ours);
push_index_entry(&path, base, Stage::Base);
push_index_entry(&path, right, Stage::Theirs);
conflicts_not_added_to_index.remove(&path);
} else {
// We can't represent many-sided conflicts in the Git index, so just add the
// first side as staged even if there are conflicts.
// first side as staged even if there are conflicts. If the file is conflicted
// in the working copy, we will add a dummy ".jj-do-not-resolve-this-conflict"
// file in the index later.
push_index_entry(&path, entry.first(), Stage::Unconflicted);
}
}

// If any conflicted paths in the working copy weren't added to the index as
// conflicts, the user might forget to resolve them. To prevent this, we add a
// dummy conflicted file to the index.
if !conflicts_not_added_to_index.is_empty() {
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(Stage::Ours),
Mode::FILE,
INDEX_DUMMY_CONFLICT_FILE.into(),
);
}

// Required after `dangerously_push_entry` for correctness
index.sort_entries();

Expand Down

0 comments on commit a498758

Please sign in to comment.