gitster (Junio C Hamano) (gitster) wrote,
gitster (Junio C Hamano)
gitster

Fun with undoing conflict resolution with "git checkout -m file"

We deal with conflicts in many situations while integrating changes from different sources:
  • We may be merging two branches that independently worked on different theme that happened to touch overlapping parts of the code ("git merge");
  • We may be transplanting the change made by a commit in an unrelated part of the history on top of our current state ("git cherry-pick");
  • We may be recording our wish to reverse an earlier commit that introduced a wrong change in our current history ("git revert");
  • We may be forward-porting an earlier work of ours on top of the upstream that has been updated ("git rebase");
  • We may be accepting a patch that is based on the source on a wrong branch ("git am -3"); or
  • We may have started local changes on a wrong branch, and are switching to the correct branch on which we should have started the local changes ("git checkout -m branch").
These mergy operations are handled exactly in the same way in all of these situations. Our task is to replay the change one side made on top of the state of the other side.  Three versions of files are involved: the base version, our version (i.e. the version to which the changes from the other side is replayed), and their version (i.e. the version the changes to be replayed is taken from).

If there is no overlap between our changes (since base) and their changes (again, since base), the merge is automatically resolved cleanly, and you don't have to deal with manual resolution. The merge results are already recorded in the index, and are waiting for you to make a commit out of them.

When there are overlaps, however, these operations record the three versions for each conflicted file in the index, and you will see changes from both our side and their side in the file in your work tree, marked with conflict markers, to resolve manually.

The workflow to resolve these is the same in all of these situations:
  1. Run a mergy operation (e.g. "git merge", "git cherry-pick") that results in conflicts;
  2. Check with "git diff" to examine conflicts;
  3. Edit the conflicted files;
  4. "git add files" to mark the result resolved; and
  5. Conclude the mergy operation (e.g. "git commit" after "git merge" conflicted; "git rebase --continue" after one step of "git rebase" conflicted).
One issue is that we are not perfect. We often make mistakes and end up with a wrong conflict resolution in the "Edit the conflicted files" step. Even if we wish to redo the resolution from scratch, once edited away, the originally conflicted state is not visible in the files in the work tree anymore.

With recent enough git (v1.6.1 or newer), we can use:

git checkout -m file

to reproduce the automerged result (with conflict markers) of a conflicted path file, using the three versions (base, ours and theirs) recorded in the index. With versions of git up to v1.6.6 (released recently), however, this is only possible until we "git add file" to mark the path as resolved, at which point the three versions are discarded from the index and replaced with the result of the merge. Hence, the workflow to use "git checkout -m file" becomes like this:
  1. Run a mergy operation (e.g. "git merge", "git cherry-pick") that results in conflicts;
  2. Check with "git diff" to examine conflicts;
  3. Edit the conflicted files, go back to "git diff" step as necessary;
  4. If you made a mistake in resolving conflicts in file, "git checkout -m file" to recover, and go back to "Edit the conflicted files" step;
  5. Make sure everything is correct;
  6. "git add files" to mark the result resolved; and
  7. Conclude the mergy operation (e.g. "git commit" after "git merge" conflicted; "git rebase --continue" after one step of "git rebase" conflicted).
In other words, "git add" step becomes the point of no return if you want to be able to fix a botched resolution. This is very unfortunate, as it inevitably leads to a rather awkward workflow of editing each and every conflict away, making sure all of them are correct and then finally "git add" to mark all of them as resolved, as outlined above.

When dealing with a large merge, which is where we need a better tool support the most, we would rather wish to be able to work incrementally.

Today, because everybody is eating his or her ham (or whatever one celebrates one's end-of-year holidays with) and not hacking, I managed to spend some quality time addressing this issue. The main idea is to record the conflicted state in a hidden part of the index as an "undo" information, every time a conflicted path is marked as resolved by using "git add". With this information, even after a path is marked as resolved with "git add file", "git checkout -m file" can first resurrect the conflicts in the index from this "undo" information, and then reproduce the conflicted state in the work tree. I've written minimum test and queued the result in the 'pu' branch, and hopefully we can perfect the series before v1.7.0 ships. With this change, the workflow will become:
  1. Run a mergy operation (e.g. "git merge", "git cherry-pick") that results in conflicts;
  2. Check with "git diff" to examine conflicts;
  3. Edit some conflicted files, go back to "git diff" step as necessary;
  4. "git add file" to mark a path as dealt with (in other words, you do not have to have total confidence in the result at this point), go back to "git diff" step to review the result, or to deal with other paths;
  5. If you made a mistake in resolving conflicts in file, "git checkout -m file" to recover, and go back to "Edit some conflicted files" step;
  6. Make sure everything is dealt with and is correct; and
  7. Conclude the mergy operation (e.g. "git commit" after "git merge" conflicted; "git rebase --continue" after one step of "git rebase" conflicted).
This is much nicer as it does not have the point of no return anymore, except for the obvious and inevitable "Conclude" step.


 
Tags: git
Subscribe

Recent Posts from This Journal

  • For the last time...

    I was asked by a few people where my blog moved to, so here it is.

  • Sorry, LiveJournal, git bye and thanks for all the spam

    I've been blogging elsewhere without announcing where it is to the public. The primary reason I left here is because the pages seem to get too…

  • Git 1.7.5

    The cycle took a bit longer than I would have liked (83 days), but finally I tagged the latest release v1.7.5 with 547 changes from 77…

  • Post a new comment

    Error

    Comments allowed for friends only

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 3 comments