Reputation: 17802
I have an old, out-of-sync project code in the master branch. Several files have been deleted, added, re-written and thoroughly edited. I want to force merge stable latest code from development branch into master. I am trying to force merge, but still get merge conflicts.
Here are the steps I followed:
➜ project git:(development) git checkout master
➜ project git:(master) git pull
➜ project git:(master) git merge -s recursive -X theirs development
CONFLICT (modify/delete): vendors/popup/magnific-popup.css deleted in HEAD and modified in development. Version development of vendors/popup/magnific-popup.css left in tree.
CONFLICT (modify/delete): vendors/popup/jquery.magnific-popup.min.js deleted in HEAD and modified in development. Version development of vendors/popup/jquery.magnific-popup.min.js left in tree.
CONFLICT (modify/delete): vendors/owl-carousel/assets/animated.css deleted in HEAD and modified in development. Version development of vendors/owl-carousel/assets/animated.css left in tree.
... several similar conflicts
I tried doing a rm
of all files in my master branch
➜ project git:(master) git reset --hard HEAD
➜ project git:(master) rm -rf *
➜ project git:(master) git merge -s recursive -X theirs development
Still get the same conflicts. What am I doing wrong?
Edit:
Here is a graph for better understanding about what is going on. At some point, unrelated histories were also pushed.
Upvotes: 2
Views: 5219
Reputation: 488193
I think you don't want this kind of merge at all. I think what you want is a "theirs strategy" merge, which Git doesn't offer. It can still be done: see VonC's answer here. But read on to be sure that this is what you want, before you use it.
There are two important things going on here, both sort-of captured by your command sequence:
git checkout master git merge -s recursive -X theirs development
(I left out the step that didn't do anything).
The first is that most merges—including that from the default -s recursive
merge strategy—work by finding a common starting point: a shared merge base commit. To draw this as a rather simple case, consider the following commit graph:
C--D--E <-- master (HEAD)
/
...--o--B
\
F--G--H <-- development
Obviously your own graph will be much hairier, but in the end, this all works out the same: Git takes the current commit, as indicated by your current branch master
, which is commit E
, as one of the three inputs to the merge process. Git takes the commit you specify, in this case commit H
because development
is the one you said to look at, as the second of the three inputs to the merge process.
The third input is computed from the graph. We start at each of the two tip commits and begin walking backwards, the way Git always does. Commit E
leads back to D
, which leads to C
and then to B
. Meanwhile commit H
leads to G
to F
to B
. Commit B
is on both branches, and it is, in graph terms, the lowest common ancestor.1 (Informally, it's the one closest to both branch tips. "Closest" breaks down in complex graphs, but for simple ones like this, that's what lowest common ancestor means.) That makes it the best shared commit, and hence the merge base.
What happens now is pretty simple in general terms, but gets sticky in some details. Git starts with the merge base commit in terms of "desired result". That is, the contents Git starts with are those from the snapshot in commit B
. To these contents, Git needs to apply the combination of any changes that you made and any changes that they made. So Git needs to run two git diff
commands:
git diff --find-renames hash-of-B hash-of-E
: what we changed on master
git diff --find-renames hash-of-B hash-of-H
: what they changed on development
If the merge base is close to both tips, as it is here, probably each of these two diffs doesn't show all that much. Combining the two sets of changes will be easy and straightforward. If we changed line 12 of README.md
, and they didn't touch README.md
at all, Git takes our change, giving our version of README.md
to go in the new merge commit. If they touched a different line of README.md
, Git puts our two changes together: both get applied to B:README.md
to produce the README.md
for the new commit. This process repeats for all the files.
If we and they both touched the same line(s) of the same files, though, Git would normally declare a merge conflict and stop, leaving us to clean up the mess. That's where the -X theirs
option comes in: this is an eXtended option, passed on to the -s
strategy.2 In this case, the recursive
strategy treats the theirs
extended-option as meaning: in the case of a conflict, throw away my change and use theirs.
Note that if there isn't a conflict, Git will use our change! Sometimes that's what we want. If it is what we want, -X theirs
is the right idea. If it's not what we want, -X theirs
is the wrong idea. So that's where you need to decide: do we use -X theirs
, or do we use a -s theirs
that we have to construct as in VonC's answer?
1It's the lowest common ancestor because computer scientists draw their trees upside down:
A
|
B
/ \
C D
/ \ \
E F G
The common ancestors of E and F here are C, B, and A, but C is the lowest one. The common ancestors of F and G are B and A; B is lowest. Git's commit graph is a directed acyclic graph or DAG, rather than a tree, so "LCA" is not as simple as it is in a tree, and there can be multiple LCAs (or no LCA at all) given two nodes in a graph. Git handles all this sensibly, for some definition of sensible.
2Git calls -X
a strategy option, but that sounds exactly like the -s strategy
argument. It's literally an option to the strategy, hence Git's poorly-chosen name. I think eXtended option is a better name, in part because it explains the -X
.
-X theirs
: see the other answer I linkedThere's not much more to say: -s theirs
was once a Git merge strategy. It isn't any more. You can still synthesize it, in any number of ways. My favorite is actually the plumbing command variety in Michal Soltys' answer.
-X theirs
: why is it complaining of conflicts?I mentioned above that the merge process—the to merge part of git merge
—is "pretty simple ... but gets sticky in some details." You have just hit one of those details.
The massive git diff
s that Git gets from diffing the merge base it found, vs each of the two tip commits you have specified, has a lot of cases of files that were completely deleted by one side, but modified by the other side:
...--B--o--o--...--o--o <-- master (HEAD)
\
o--o--...--o--o <-- development
Somewhere in that massive chain of anonymous o
commits along the top, "we" (base vs master) deleted some file. "They" (base vs development) changed that file. Git doesn't know how to combine "deleted" with "changed".
I call these high level conflicts, because they are changes to the very nature or existence of a file, rather than to the individual lines within a file. Well, I also call it that because Git itself calls the part of the code that combines individual lines the "low level merge driver", so these must be the opposite, high level. These kinds of conflicts—whether add/add, modify/delete, rename/delete, or what—always cause a -s recursive
git merge
to stop and let you fix up the mess. The extended options are only given to the low level merge driver invoked by the strategy.
In fact, the only strategy that doesn't stop is the -s ours
strategy. When you use the -s ours
strategy, that one totally ignores what's in "their" commit. Git doesn't even bother looking at the merge base at all—it just uses what's in our commit as the merge result.
Of course, -s ours
takes ours. You wanted something that took theirs, at least for this particular case, maybe for all cases. But if you really don't want the equivalent of -s theirs
, but rather -X theirs
with taking their entire file in these cases, you're stuck with resolving each of these conflicts after-the-fact.
You can do it with a script (which you must write yourself). That's a little tricky though. The trick is to use git ls-files --stage
: you'll see that, for each file for which Git complained:
CONFLICT (modify/delete): vendors/popup/magnific-popup.css deleted in HEAD and modified in development. Version development of vendors/popup/magnific-popup.css left in tree.
Git will have an entry at stage #1 for the name (vendors/popup/magnific-popup.css
), and another entry at stage #3 (theirs), but no entry at stage #2 (ours). To resolve this, use git add
to write the stage-3 entry from the work-tree into the index at stage zero: git add
will remove the stage 1 and 3 copies. You can just collect up all such file names and pass them all to git add
.
Upvotes: 3
Reputation: 7610
A branch is just a pointer, and master
is just another branch. If you haven't been merging changes as you go and dev
represents the current stable state of the project, one option that would save you from going through all the conflicts would be:
git branch -m master <new-name>
git branch -m dev master
There may be a need to tweak tracking relationships, depending on how you have any remotes set up.
Upvotes: 0
Reputation: 23091
This will just set master
to be the same as your development
branch - anything in master committed after the last merge of development
will be lost.
git checkout -B master development
Upvotes: 2