kkm mistrusts SE
kkm mistrusts SE

Reputation: 5548

Git: see changes on their branch since last merge on merge conflict

I thought that my scenario should be fairly common, and there must be a simpler way of doing what I do. Suppose there are two branches, current and next, for two lines of development. next at this stage receives all changes from the current branch. Being maintainer of the next branch I am syncing the changes (as necessary for alpha releases) like this:

$ git co origin/next -b next
$ git merge origin/current

Some files changed their format on the next, and every change to these few files made since the last sync-merge results in a conflict. To resolve this conflict, I need to see the changes on the current branch since the previous merge. Usually many files have changed, but only 1 or 2 files like those I mentioned are conflicting.

Example

Suppose that the file baz.dat on the current branch contains words in square brackets, like

[real]
[programmers]
[use]
[pascal]

On the next branch, the syntax change demands that the words be surrounded with colons instead

:real:
:programmers:
:use:
:pascal:

A change on the current branch has added a line to the file:

[real]
[programmers]
[don't]
[use]
[pascal]

Every time a merge conflict results in the following diff merge marks:

<<<<<<< ours
:real:
:programmers:
:use:
:pascal:
=======
[real]
[programmers]
[don't]
[use]
[pascal]
>>>>>>>> theirs

Whole file content deleted and replaced, because it, in a sense, is.

Similarly, just git diff shows all incoming, "their" lines as deleted and "our" lines as added:

$ git diff
<usual unified diff preamble>
+  :real:
... more lines with "+ " (our new) or " -" (their deleted) mark, 
 - [pascal]

What I am looking for

A way to see what changed on the current ("their") branch since I last merged, so I can manually incorporate the change into the next ("our") branch

$ git diff --magically-from-merge-point --theirs
<usual unified diff preamble>
+ [don't]

In other words, I want to see in this example case that only one line was added, so I can insert it in the new format as well.

(My real case is a change in a domain-specific language, but it essentially very similar to this trivial example).

Current solution

What I am resorting to do is a rather unwieldy series of commands:

$ git status 
. . . .
      both modified:   foo/bar/baz.dat <== copy/paste every conflict filename
$ git diff `git merge-base HEAD origin/current`..origin/current -- foo/bar/baz.dat

This shows what I want, but rather complex, and I construct and type it every time. It is easy enough to script in bash, but before I do that I want to ask is there a simpler way to see the conflicting changes from merge base? I. e.

(next)$ git merge origin/current
. . . 
CONFLICT (content): foo/bar/baz.dat
(next|MERGING)$ git diff --magic-switch

and diff would show changes only for conflicting files as a delta between the merge base and the point of merge (which I would, in the ideal case, further restrict with e. g. --magic-switch --theirs)

Does such a magic switch exist? Looks like I am missing something obvious!

Upvotes: 4

Views: 1727

Answers (2)

LeGEC
LeGEC

Reputation: 52081

git mergetool will open the conflicting files, one by one, in your diff editor of choice (meld, vimdiff, kdiff3, winmerge ...), as a 3 way merge between the 3 versions :

  • local : version in the current branch (in your case : next's version)
  • base : version in the merge-base commit
  • remote : version in the merged branch (in your case : origin/current's version)

If you edit + save the central file, git will mark this conflict as solved, an stage what you saved in the index.


If your merge halted due to conflict, git stores a ref to the merged commit in .git/MERGE_HEAD. This means that you can use the string "MERGE_HEAD" as a valid reference in git commands :

git log -1 MERGE_HEAD            # view last commit on the merged branch
git merge-base MERGE_HEAD HEAD   # no need for the name of the branch

You can then build a simpler alias :

theirs = 'git diff $(git merge-base MERGE_HEAD HEAD) MERGE_HEAD'
# usage :
git theirs                  # complete diff between 'base' and 'theirs'
git theirs -w -- this/file  # you can add any option you would pass to 'git diff'

Upvotes: 1

torek
torek

Reputation: 489628

Ah, with the example we can get somewhere.

This is not built in to Git because it's a very hard problem in general. But if we constrain it somewhat, we can write a script or tool or procedure to deal with it (and that part, it turns out, is built into Git! ... well, sort of). Let's see if we can describe the constraints:

  • This is a standard three-way merge, with a standard modify conflict, so there is a base version (stage 1), a local/HEAD/--ours version (stage 2), and a remote/other/--theirs version (stage 3).

  • One "side" of the merge touches every line, but in a repeating and identifiable pattern, which we could back out, perhaps temporarily. (Let's give this change a name: let's call it a "systematic delta" or SD for short, and +SD means add or keep this delta, while -SD means undo / remove it. The SD may be an irreversible change, i.e., -SD might not undo it perfectly; if so, we may still be able to handle it automatically.) It may or may not also have some additional changes, vs the base.

  • The other "side" of the merge only touches a few lines, or even no lines, and lacks the repeating-and-identifiable pattern change, which we could add to it, perhaps temporarily.

We have a few additional questions and decisions to consider here, perhaps on an ad-hoc case-by-case basis or perhaps systematically. They will affect how we write our script, tool, or procedure. These are:

  • Is the SD actually reversible? That is, once we discover the SD, does base + SD - SD reproduce the original base?
  • Do we want the SD in the result?
  • Can we detect the presence of the SD? (We may not need to, but we will see that it's convenient if we can.)

If the SD is reversible, we are in good shape because we can get any result we like. If not, we'll have to apply the SD to whichever side lacks it: this is OK if and only if we want the SD in the result.

Let's assume for the moment that we want +SD in the result. In this case, we're in great shape: let's just call the process of applying the SD "normalizing" the file. Or, if we want -SD in the result, and the SD is reversible, let's call doing -SD "normalizing". Furthermore, if we can tell whether a file has SD applied, we can just write a "normalizer filter" and run everything through that filter.

This is what is built in to Git: it has "clean" and "smudge" filters, as described in the gitattributes documentation. Furthermore, we can instruct git merge to run the two filters (both of them—but we can make them both be "normalize", or leave one unset, for our particular purpose here) on each file before doing the merge. Since "normalizing" gives us the final form we want, it makes Git do the merge we want done.

If the SD is not reversible, we need to be more clever, but as long as we want +SD in the result, we are still OK. Instead of using the merge.renormalize setting described in gitattributes, we can write our own merge driver. This is also documented (further down) in the same manual. I won't go into detail here since the clean/smudge filter method is easier and likely to work for your case, but the essence is that we extract the three stages, apply +SD to whichever ones need it (selecting "files that need it" by test or by prior knowledge), and then use git-merge-file to achieve the desired merge on the three (now all +SD) inputs.

Note that the .gitattributes file that is present in the work-tree at the time you run git merge, plus any configuration items you select in .gitconfig or with a -c command line option to git, control the filtering and renormalization at the time you run git merge. This .gitattributes file need not even be checked-in anywhere. This means you can have different .gitattributes files in effect at different times, either by making it an ordinary tracked file and switching commits and/or branches (so that Git updates it for you), or just by updating it manually as needed.

Upvotes: 1

Related Questions