- CTO Teachings
- Posts
- The 3-Point Merge
The 3-Point Merge
Why Your IDE's Merge Tool Is Lying to You (And How to Fix It)
👋 Welcome to CTO Teachings’s blog. While we are a recruiting company, we happen to provide a free blog for people just like you. It’s packed with hard-won lessons from CTO—who’s helped managers rise to Engineering Director and CTO roles.
Introduction: The Merge Conflict That Made Me Question Everything
Picture this: It's Friday at 4:47 PM. You've spent three days on a beautiful feature branch. The code is clean, the tests pass, and you're ready to merge to main. You click the merge button and... 47 conflicts detected. Your IDE helpfully shows you two versions of each conflicted file, and you spend the next two hours playing detective, trying to figure out what actually changed and why.
Sound familiar? Here's the dirty secret the tooling vendors don't want you to know: standard merge tools are fundamentally broken. They're showing you the wrong information, and that's why resolving conflicts feels like trying to solve a murder mystery where someone already cleaned up the crime scene.
The problem isn't you. The problem is that Git's default merge—and the tools built on top of it—performs what's called a 2-point merge. What you actually need is a 3-point merge. The difference isn't just academic—it's the difference between understanding intent and blindly guessing.
The Problem with 2-Point Merges
What You See vs. What You Need
When Git encounters a merge conflict, it dumps something like this into your file:
<<<<<<< HEAD
function calculateTotal(items, taxRate) {
return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}
=======
function calculateTotal(items, taxRate, discount) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return (subtotal - discount) * (1 + taxRate);
}
>>>>>>> feature-branchYour IDE shows you these two versions—let's call them B (your HEAD) and C (the incoming branch). It might even try to highlight the differences between B and C. Very helpful, right?
Wrong. Here's the critical information that's missing: what was the original code before either branch touched it?
Without knowing the original (let's call it A, the fork point), you're forced to guess at intent. Did main add the taxRate calculation, or was it always there and feature-branch modified it? Did feature-branch add the discount parameter, or did main remove it? You literally cannot know from a 2-point comparison.
The Proof Is in the Conflict Markers
Here's an easy way to verify this yourself: look at any merge conflict. You'll see the HEAD version and the incoming version. That's it. Two points. The original code—the common ancestor that would tell you what each side actually changed—is nowhere to be found in Git's default output.
Yes, Git internally knows about the merge base. Yes, some IDEs try to simulate a 3-way merge view. But they're building that view on top of Git's 2-point output, and the information loss has already occurred. It's like trying to reconstruct a deleted file from the recycle bin—possible in theory, but you've already lost fidelity.
Why This Makes Conflict Resolution a Nightmare
When you're staring at two different versions of code with no shared context, you're essentially playing a guessing game. Here's what goes wrong:
You lose intent. Knowing that main added logging is very different from knowing that feature-branch removed logging. The end state might look the same, but the correct resolution is completely different.
You can't identify true conflicts. Some 'conflicts' are actually compatible changes that just happened to touch nearby lines. Without seeing what changed, you might manually resolve things that could have been auto-merged.
You risk introducing bugs. When you're guessing, you're gambling. Maybe you keep the wrong version. Maybe you accidentally drop a critical change because it looked like 'the old way.'
It takes forever. Every conflict becomes a mini-investigation. You end up running git log, checking commit messages, and asking teammates 'hey, did you change this function?'
The 3-Point Merge: Finally, Some Sanity
Understanding the Three Points
A proper 3-point merge involves three versions of each file:
A (Fork Point) ─── The common ancestor, where the branches diverged
/ \
/ \
B (Feature) C (Main)
The magic happens when you compute two diffs:
B - A = What changed on the feature branch (the developer's intent)
C - A = What changed on main since the branch was created
Now, instead of looking at two arbitrary states and guessing, you can see exactly what each side intended to change. This transforms conflict resolution from archaeology into engineering.
A Real Example
Let's revisit that calculateTotal function. With 3-point merge info, you'd see:
Original (A - Fork Point):
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}B - A Diff (Feature Branch Changes):
+ Added 'discount' parameter
+ Added discount subtraction before returnC - A Diff (Main Branch Changes):
+ Added 'taxRate' parameter
+ Added tax multiplication to returnNow the resolution is obvious: both branches added features to the original simple function. Main added tax calculation, feature added discount handling. The correct merge keeps both additions:
function calculateTotal(items, taxRate, discount) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return (subtotal - discount) * (1 + taxRate);
}No guessing. No archaeology. Just clear, obvious intent from both sides.
Why This Works Better for AI-Assisted Merging
If you're using AI tools to help resolve conflicts (and if you're not, you're missing out), the 3-point approach becomes even more valuable. AI models are excellent at understanding intent when given the right context. Feed an AI two random code snippets and ask 'which one is right?'—you'll get a coin flip. Feed it the original plus two diffs showing what each side changed, and it can reason about the actual problem.
Think of it this way: would you rather ask a new developer to 'pick the right version' or 'here's what existed before, here's what team A changed and why, here's what team B changed and why—now combine them intelligently'? The second question has an answer. The first is a trap.
Implementing the 3-Point Merge Workflow
Prerequisites and Philosophy
Before we dive into the mechanics, there's an important philosophical point: all developers should be using squash merges (technically 'git merge --squash' which is really a rebase that replays all your commits as a single commit). This keeps your history clean and, critically, makes 3-point merges reliable.
For 3-point merges to work well, you need one of these to be true:
Any previous merges from main to your feature branch were 3-point merges, OR
A squash merge back to main is clean (no conflicts)
If neither is true, you'll need to deal with the main merge first. It's turtles all the way down, but at least they're well-organized turtles.
The Complete Workflow
Step 1: Find the Fork Point and Record A, B, C
First, identify your three points. You'll want to save these somewhere (a simple JSON file works well):
FORK_POINT (A): git merge-base feature-branch main
FEATURE_HEAD (B): The current HEAD of your feature branch
MAIN_HEAD (C): The current HEAD of main
Step 2: Create a Backup Branch
Always. Always create a backup. Future you will thank present you.
git checkout -b username/myFeatureBackup1
git checkout username/myFeatureStep 3: Update Main and Create a Squash Branch
git checkout main && git pull origin main
git checkout -b username/myFeatureSquashStep 4: Attempt the Squash Merge
git merge --squash username/myFeatureIf this succeeds cleanly: commit and skip to Step 11. Pop the champagne. If it fails with conflicts: continue to Step 5. Put down the champagne.
Step 5: Get the Conflicted Files List
git diff --name-only --diff-filter=U > conflicted-files.txtStep 6-8: Extract the Three Versions and Diffs
For each conflicted file, create a directory and extract:
# Full files at each point
git show $FORK_POINT:$file > A-forkpoint.txt
git show $FEATURE_HEAD:$file > B-feature.txt
git show $MAIN_HEAD:$file > C-main.txt
# The magic diffs that show intent
git diff $FORK_POINT $FEATURE_HEAD -- $file > B-A.diff
git diff $FORK_POINT $MAIN_HEAD -- $file > C-A.diffStep 9: Resolve Conflicts
Now you have everything you need. You can resolve manually using the diffs as your guide, or feed the full context to an AI assistant. Either way, you're working with complete information instead of incomplete fragments.
Steps 10-11: Clean Up and Finalize
git branch -D username/myFeature # Delete old feature branch
git branch -m username/myFeature # Rename squash branch
git push -u --force-with-lease origin # If PR existsFile Structure Reference
When you're done extracting, your merge directory should look like this:
merge-myFeature/
├── hashes.json # A, B, C commit hashes
├── conflicted-files.txt # List of conflicted files
└── src__app__component.ts/ # Per conflicted file
├── A-forkpoint.txt # Original code
├── B-feature.txt # Feature branch version
├── C-main.txt # Main branch version
├── B-A.diff # What feature changed
└── C-A.diff # What main changedConclusion: Stop Guessing, Start Merging
The next time you're staring at a merge conflict, remember: you're not bad at merging. You've just been given incomplete information. Standard merge tools show you two points when you need three.
By extracting the fork point and computing the B-A and C-A diffs, you transform merge conflicts from 'guess which code is right' into 'combine two well-understood changes.' It's the difference between stumbling in the dark and working with the lights on.
Key Takeaways:
2-point merges hide intent by only showing end states
3-point merges reveal intent by showing what each side changed from the common ancestor
The B-A diff shows feature branch intent; the C-A diff shows main branch intent
This approach works better for both human and AI-assisted resolution
Use squash merges to keep your history clean and 3-point merges reliable
Your Friday afternoons will thank you.
If you are ever looking for a job or expanding your team, connect with Shelly, Yuliia, or Harold on LinkedIn.
Want to dive deeper into hiring strategies and level up your leadership? Subscribe to our Beehiiv’s blog for more insights!
Thanks for reading,
CTO Teachings
Reply