Catch Production merge conflicts before they occur with git-merge-tree.


Ahh…the dreaded merge conflict. No developer likes to see those red squiggly lines after working hard on their feature branch and merging to their target branch. Because it means there’s more work to do, and you have to go through the painful process of rolling back the merge if the target branch is your production branch.
What if I told you git already had this covered? Introducing git-merge-tree.
git-merge-tree helps you run a merge without touching the index(staging area) or working tree(files touched in the current working branch). You’re telling git, “Yo, simulate a merge between these two branches for me, and report back in my terminal, but make no real changes”.
Let’s demonstrate with an example.
I’ve prepped a very simple git repo to help you follow along. Open up your editor, and in your terminal run:
git clone https://github.com/Prodigy00/git-merge-tree-sample.git
The repository has two remote branches, which will help demonstrate the usefulness of the git-merge-tree feature: feat/conflict-log
, feat/no-conflict-log
. Kindly fetch them using either of the following commands:
git fetch --all //fetches all remote branches
OR (if that doesn’t work)
git checkout -b feat/conflict-log origin/feat/conflict-log
git checkout -b feat/no-conflict-log origin/feat/no-conflict-log
Once you’re all set up, kindly switch between the branches to see their content.
The main
branch contains a single log:
console.log("Log file loaded, with some updates");
which was previously:
console.log("Log file loaded");
I made the update to main
after branching out to feat/conflict-log
so there would be some content that the conflict feature branch didn’t have…leading to a merge conflict.
The contents of the other branches should be as follows:feat/conflict-log
console.log("Log conflict file loaded");
feat/no-conflict-log
console.log("Log file loaded, with some updates");
Next, we run the git-merge-tree command, the command signature goes like this:
git merge-tree target-branch source-branch
This means your next command will be something like this:
for feat/conflict-log
:
git merge-tree main feat/conflict-log
Your output, if there’s no conflict, would be a single Object ID(OID), which you can inspect with commands like git ls-tree
, git show
etc. However, if there’s a conflict, you’ll get a multi-line output like so:
3482116f061296a37acf8ed257cdf882ebefab38
100644 1dac12776fb1fa4a119078ea71982a138d7c1fb0 1 log.js
100644 84b09124265c6b020361f539293281a8864133e6 2 log.js
100644 9c20d2759a784810ab82f4dcb0e9a8462cbd8357 3 log.js
Now…that’s not super helpful, is it? I found a more user-friendly way to view the conflict between the files.
Introducing….git-merge-base. It’s a command that finds the common tree ancestor between the target branch and the source branch. For more info, kindly check the documentation.
We can use a combination of the two commands like so:
git merge-tree $(git merge-base main feat/conflict-log) main feat/conflict-log
This gives us a visual diff between the target-branch’s changes(our
) and the changes in the source/feature branch(their
); this way, you can see the conflicts:
changed in both
base 100644 1dac12776fb1fa4a119078ea71982a138d7c1fb0 log.js
our 100644 84b09124265c6b020361f539293281a8864133e6 log.js
their 100644 9c20d2759a784810ab82f4dcb0e9a8462cbd8357 log.js
@@ -1,4 +1,8 @@
-console.log("Log file loaded, with some updates");
\ No newline at end of file
+<<<<<<< .our
+console.log("Log file loaded, with some updates");
+=======
+console.log("Log conflict file loaded");
+>>>>>>> .their
Isn’t that great? Just from the combination of these two powerful git commands, you can catch issues before breaking prod. From the above, we see that there are some changes in main
that feat/conflict-log
doesn’t have, we can quickly fix this before making a merge request.
In contrast, if we run the same command between main
and feat/no-conflict-log
, what would we get?
Kindly switch to the feat/no-conflict-log
branch and add a line beneath the first log:
console.log("added from no conflict log");
Add it to the staging area and commit with a message of your choice.
Now let’s run our newly learned command, this should cause no conflict since the feat/no-conflict-log
has the changes from main
:
git merge-tree $(git merge-base main feat/no-conflict-log) main feat/no-conflict-log
Our output:
merged
result 100644 30731894c0b216764f730b1c70867fd2fa68f4f2 log.js
our 100644 84b09124265c6b020361f539293281a8864133e6 log.js
@@ -1,4 +1,5 @@
-console.log("Log file loaded, with some updates");
\ No newline at end of file
+console.log("Log file loaded, with some updates");
+console.log("added from no conflict log");
\ No newline at end of file
Tip: If you see the merged
keyword as part of the output, it means there was no conflict!
We get a nice visual showing the before and after merge changes, and the best part? None of this is in your working tree or index…the magic all happens in the terminal!
I hope this has been helpful. Kindly comment and share this with other developers who may need it. As always, your feedback is welcome!
Subscribe to my newsletter
Read articles from Gideon Idowu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Gideon Idowu
Gideon Idowu
I’m passionate about technical writing, backend api services, and building tools.