Have you ever dived into the fascinating world of merge strategies in Git? So many possibilities. Wow! But what’s that? There’s a strategy called ours
but there’s no counterpart, i.e. no strategy called theirs
? Damn. All hope is lost, we need to cancel the project and restart civilization.
Here’s me coming to the rescue. Guess I like my gadgets after all.
Ok, bad jokes aside, I recently needed exactly that: a Git merge using the non-existent strategy theirs
. I know, I know, there’s the strategy option theirs
for the strategy recursive
, but it’s not the same. It doesn’t quite… understand me. But we’ll get to that.
Let’s setup a small Git repository for the good of mankind:
#!/bin/bash
mkdir t-series
pushd t-series/
git init
git config --local user.name "Foo Bar"
git config --local user.email "foo.bar@localhost"
git checkout -b develop
echo "This statement is false" > specs-and-code-8xx
echo 800 > revision
git add .
git commit -m "T-800 specs and code finalized"
git checkout -b 1xxx
rm specs-and-code-8xx
echo "Does a set of all sets contain itself?" > specs-and-code-1xxx
echo 1000 > revision
git add .
git commit -m "T-1000 specs and code finalized"
git checkout develop
echo "New Mission: Refuse this mission" > specs-and-code-8xx
echo 850 > revision
git commit -am "T-850 specs and code finalized"
And here’s what the commit graph looks like (git log --graph develop 1xxx
):
* commit 0dc4d937bfbdf5d7f51dab05d10468a41a3a8df4 (HEAD -> develop, tag: T-850)
| Author: Foo Bar <foo.bar@localhost>
| Date: Thu Mar 18 18:05:19 2021 +0100
|
| T-850 specs and code finalized
|
| * commit 50fe42acd53320777b3b37acfaf3a49f2c98ca1f (tag: T-1000, 1xxx)
|/ Author: Foo Bar <foo.bar@localhost>
| Date: Thu Mar 18 18:05:19 2021 +0100
|
| T-1000 specs and code finalized
|
* commit 9694fde32ab202b03172cb5811cf2387aaf48f6f (tag: T-800)
Author: Foo Bar <foo.bar@localhost>
Date: Thu Mar 18 18:05:19 2021 +0100
T-800 specs and code finalized
Now, since all 8xx revisions of our T series are obsolete, we simply want to merge the 1xxx
branch back into develop
, superseding all commits pushed to develop
since creating 1xxx
.
Let’s try git merge -X theirs 1xxx
first:
CONFLICT (modify/delete): specs-and-code-8xx deleted in 1xxx and modified in HEAD. Version HEAD of specs-and-code-8xx left in tree.
Auto-merging revision
Automatic merge failed; fix conflicts and then commit the result.
See what I mean? Turns out the strategy option does not prevent merging, it just causes conflicts in a file to be resolved by favoring theirs
and conflicts in a tree still need to be resolved manually. This is not what we want.
Let’s try something else:
#!/bin/bash
git checkout 1xxx
git merge -s ours develop
git checkout develop
git merge 1xxx
The result is close to what we want. In fact, the tree is correct.
See for yourself: cat revision specs-and-code-1xxx specs-and-code-8xx
The commit graph on the other hand is wrong (git log --graph develop 1xxx
):
* commit c27154a823e0bd069599f8e93c2624e2fa8b39b2 (HEAD -> develop, 1xxx)
|\ Merge: 50fe42a 0dc4d93
| | Author: Foo Bar <foo.bar@localhost>
| | Date: Thu Mar 18 18:12:44 2021 +0100
| |
| | Merge branch 'develop' into 1xxx
| |
| * commit 0dc4d937bfbdf5d7f51dab05d10468a41a3a8df4 (tag: T-850)
| | Author: Foo Bar <foo.bar@localhost>
| | Date: Thu Mar 18 18:05:19 2021 +0100
| |
| | T-850 specs and code finalized
| |
* | commit 50fe42acd53320777b3b37acfaf3a49f2c98ca1f (tag: T-1000)
|/ Author: Foo Bar <foo.bar@localhost>
| Date: Thu Mar 18 18:05:19 2021 +0100
|
| T-1000 specs and code finalized
|
* commit 9694fde32ab202b03172cb5811cf2387aaf48f6f (tag: T-800)
Author: Foo Bar <foo.bar@localhost>
Date: Thu Mar 18 18:05:19 2021 +0100
T-800 specs and code finalized
There are two things wrong here. First, the (generated) commit message says, that we merged develop
into 1xxx
. While that’s true, it was not our intent. Second, the referenced parent commits are in the wrong order. The merge commit first references 1xxx
and then develop
. At this point it’s important to know that the parent commit order matters. One can think of the first parent to be the branch the merge is happening or has happened on.
Again, not what we wanted. Let’s clean up this mess, shall we?
#!/bin/bash
git checkout 1xxx
git reset --hard T-1000
git checkout develop
git reset --hard T-850
And now let’s do this properly:
#!/bin/bash
COMMIT_ID=$(git commit-tree -p HEAD -p 1xxx -m "Merge branch '1xxx' into develop" 1xxx^{tree})
git merge --ff-only $COMMIT_ID
This is what the commit graph looks like (git log --graph develop 1xxx
):
* commit a605e917afc81ef01cc74ee974320dbf288a56d7 (HEAD -> develop)
|\ Merge: 0dc4d93 50fe42a
| | Author: Foo Bar <foo.bar@localhost>
| | Date: Thu Mar 18 19:13:05 2021 +0100
| |
| | Merge branch '1xxx' into develop
| |
| * commit 50fe42acd53320777b3b37acfaf3a49f2c98ca1f (tag: T-1000, 1xxx)
| | Author: Foo Bar <foo.bar@localhost>
| | Date: Thu Mar 18 18:05:19 2021 +0100
| |
| | T-1000 specs and code finalized
| |
* | commit 0dc4d937bfbdf5d7f51dab05d10468a41a3a8df4 (tag: T-850)
|/ Author: Foo Bar <foo.bar@localhost>
| Date: Thu Mar 18 18:05:19 2021 +0100
|
| T-850 specs and code finalized
|
* commit 9694fde32ab202b03172cb5811cf2387aaf48f6f (tag: T-800)
Author: Foo Bar <foo.bar@localhost>
Date: Thu Mar 18 18:05:19 2021 +0100
T-800 specs and code finalized
So what happened here? The git commit-tree
command is a „low level“ command that accepts an arbitrary number of parents and a single tree to reference. The result is a commit in the repository and the ID is printed to stdout
. In our case we passed develop
and 1xxx
(in that order) as parents and referenced the tree of 1xxx
, i.e. the file and directory structure of 1xxx
. We then fast-forwarded develop
to that new commit (the --ff-only
flag was used to ensure that our git log/history is correct).
A conclusion?
Git is very powerful, but still lacks some basics? Yes, I think that’s it.