4

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!

  • 1
    Running `git diff` with no arguments when in a conflicted merge shows combined diffs. I find them difficult to read though: I prefer to just set `merge.conflictstyle` to `diff3`, which leaves the base (stage 1) conflict text *in* the conflicted file, along with the stage 2 (HEAD) and stage 3 (`--theirs`) text. – torek Nov 09 '16 at 03:44
  • @torek: This is not what I am looking for. On every merge, the diff for the file (due to its changed format) shows all old lines deleted and all new lines added. This is great, except I need to find one or two files added to the part marked as being deleted since the last merge. The default diff behavior on merge is incomprehensible to me, and diff3 does not help at all (there are no common, identical lines in the 2 versions, so I see essentially the same thing only with a row of ||||||| added in the middle :( ). – kkm inactive - support strike Nov 09 '16 at 21:58
  • That kind of diff3 output would imply that the conflict you are seeing is an add/add conflict. This means there is no base version of the file! But then you would not get a "content" conflict, but an "add/add" conflict, and diffing the base version would not work either since it does not exist. I'd have to see an actual example to say more. (One stumbling block: I don't understand what you mean by "every point being merged.") – torek Nov 09 '16 at 22:49
  • @torek: I added an "Example" section to the question to elaborate. Could you please have another look? – kkm inactive - support strike Nov 09 '16 at 23:51
  • https://stackoverflow.com/a/5818279/2303202 – max630 Nov 10 '16 at 11:24

2 Answers2

1

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'
LeGEC
  • 46,477
  • 5
  • 57
  • 104
  • Thank you, but this does not solve the problem, only changes the tool. Is it likely I did not describe the problem well? Let me rework the question a bit, and I would appreciate any feedback on its readability! – kkm inactive - support strike Nov 09 '16 at 23:23
  • about readability : the first version was already readable, the rewritten version states the goal more clearly, and is very readable. +1 for that – LeGEC Nov 10 '16 at 10:16
  • about `mergetool` : I use `meld` as a graphical mergetool, using it in 3-way merge mode, I can see "base" vs "theirs" by looking at panel 2 vs panel 3. – LeGEC Nov 10 '16 at 10:18
  • Thanks, that is what I am going to use! ( I did not know about `MERGE_HEAD`; this simplified the script significantly. I tucked `-- $(git diff --name-only --diff-filter=U)` to the end of command to get exactly what I wanted, namely changes in conflicted files only. Not as flexible as your alias, but need same exact thing too often. – kkm inactive - support strike Nov 17 '16 at 23:55
  • Note that `git merge-base MERGE_HEAD HEAD` will pick one merge base at seemingly-random from the set of all merge bases, if there are multiple merge bases. This is pretty much the best you can do for a full comparison, since there's no easy way to access the virtual merge base made by the `recursive` strategy. For individual files, though, the virtual merge base version is what is in the base (slot 1) index entry, so you might want to use `git diff [options] :1:path/to/file :3:path/to/file` here. – torek Nov 18 '16 at 00:57
  • @kkm : I always relied on `git status` to see conflicting files, your extra `git diff ...` makes a nice shortcut. Thanks :) – LeGEC Nov 21 '16 at 08:31
  • @torek: Thanks, that makes sense! – kkm inactive - support strike Nov 21 '16 at 23:31
1

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thank you for a good explanation! I understand that my question probably has no general solution for all merge types, and this is one of the reason that it such a diff option is not available. As for the second part, I know about clean and smudge, but they won't quite help in my case (the changes in the DSL between the branches are actually a bit too complex to script reliably). From the practical standpoint, I am accepting the (LeGEC's answer)[http://stackoverflow.com/a/40502751/1149924]. – kkm inactive - support strike Nov 18 '16 at 00:00