Artyom
Artyom

Reputation: 3571

Git re-merge a file using another merge strategy option?

I'm merging two branches with common ancestor:

> [branch1]
git merge branch2 --no-commit
# ... did merging for several files with conflicts
git merge branch2 -srecersive -Xignore-space-at-eol -- myfile.txt  # not possible

And I've resolved several merge conflicts in files which I wish to keep. Now there is a file with conflicts that are because of newline chars different. How can I make git to ignore newline chars conflicts and make another file with conflicts, with option ignore-space-at-eol of recursive strategy, ie. something like that git merge branch2 -srecursive -Xignore-space-at-eol -- myfilewithconflicts.txt. See picture. I can undo my merging of a file (git checkout -m myfilewithconflicts.txt) but how I apply another strategy to this file?

enter image description here

Upvotes: 2

Views: 138

Answers (1)

torek
torek

Reputation: 488203

Unfortunately, you can't. Once you are in the middle of a conflicted merge, you must either finish that merge, or abort it entirely, to start another merge. Neither is what you want here ("finish and start a new one" can get you there, but it is convoluted and error-prone).

Git really should have a "suspend merge" tool to let you do this in a cleaner way. It might be possible to write one, but I don't have the time to do it.

Why this is so (long)

Let's start with some background. The problem at this point has been reduced to doing three-way merges on individual files—but how did we get here?

A typical Git merge—or more precisely, what I have taken to calling a true merge, as opposed to the not-really-a-merge "fast forward" operation that's mainly just done as if via git checkout or git reset --hard—uses three inputs. You specify one of these inputs, the --theirs commit. The other two are implicit: they are the current or --ours commit, and the merge base as computed by your merge strategy. The default strategy for this kind of merge is the one you chose in both example commands, -s recursive.

The goal of this kind of merge is to combine work. The merge base commit serves as the common starting point from which you (on your branch, --ours) did some work, and they (on their branch, --theirs) did some work, which Git should now combine. Since you both started from the same commit, this makes the combining easier: Git can run a git diff from the merge base to your commit to see what you did, and a second git diff from the merge base to their commit to see what they did.

Having run these two diff commands (or the internal equivalent of them), Git can then pair up every base file with one file in your commit, and with one file in their commit. Doing the diff and pairing the files is all done as part of the merge strategy, but it's not yet finished: this process simply reduces the problem, down to the point of now having to combine work for each group of files.

Ignoring tricky cases like new files, renamed files, and deleted files, we now have the merge strategy, for each path name P:

  • look at the content of P in the merge base;
  • look at the content of P in the --ours commit; and
  • look at the content of P in the --theirs commit.

As a short-cut, if the content is the same in two or three out of the three versions, there's nothing to merge and Git can just pick the --ours version that's already in the index or put the --theirs version into the index as appropriate.

If not, though—if all three versions differ—then this particular file requires actual merging. Git copies all three versions into the index, in slots 1, 2, and 3 which are reserved for the merge process, and calls the low level, one-file-at-a-time merge code. This code merges one file, using three versions. The three versions are those now stored in the index (we can extract all three into the work-tree ourselves later, if we like, but that's where they are).

The low-level merge code is what we think of when we think of merge conflicts. In fact, though, if the low-level code is able to merge without conflicts, it goes on to remove the three versions from the index, add the merged file as a stage-zero entry instead, and proceed as if there had not been any problem. It's only when this fails that the low-level merge writes, to the work-tree, the conflicted merge version of the file. It leaves all three inputs in the index.

You cannot start a new merge if you have an incomplete merge

Note that in the above sequence, Git uses the index / staging-area to store the three versions of each conflicted file, and the work-tree to store the combined version when stopping. To do so, Git must begin with a clean index and work-tree. If the index and work-tree were not clean to start with, this process could destroy unsaved work.

Git therefore says "no" if you try to run git merge with an unresolved merge conflict in place, or an uncommitted index, or a dirty work-tree. If you abort the existing, ongoing merge, that wipes everything out that you have done so far, and re-sets your index and work-tree so that they are clean—but that wipes out everything that you have done so far.

Your only other option is to finish the merge—mark everything resolved and commit. But that creates a new commit on the current branch. If you then start a second merge, the implied merge base is the commit you just made, rather than the original merge base you had during the earlier commit. This means you must save the merge commit, but then undo it with git reset or equivalent. This is the tricky and error-prone part.

If only we could repeat the low-level merge with different options

The low-level code uses most of the -X options, like -X ours or -X theirs, or in your case, -X ignore-space-at-eol. If we had direct access to this low-level code, we could just run it ourselves. That would allow us to add -X ignore-space-at-eol for one specific file that did have conflicts.

Unfortunately, while there is a git merge-file command, that's not the code that git merge-recursive uses (the merge-recursive code is in ll-merge.c, which implements the low level merging, not merge-file.c which implements git merge-file). The merge-file command does not accept -X options. It does take --ours, --theirs, and even --union. It calls into code that could take -X ignore-space-at-eol, but it has no command-line option to set that. If it did take -X ignore-space-at-eol, you would be able to achieve what you wanted directly.

Upvotes: 3

Related Questions