Monday morning. Two developers on the same team start their week by pulling the latest code from the main branch. Alice is fixing a performance bug in the billing module. Bob is refactoring that same module to use a new logging framework. Neither knows the other is working there. They are on separate branches, heads down, making good progress.
Alice finishes Tuesday afternoon. She has changed forty files, mostly small tweaks, but the heart of her fix is a rewritten function in the billing calculator. She renamed variables, restructured the logic, and shaved off two hundred milliseconds per transaction. She runs the tests. Everything passes. She pushes her branch and opens a pull request.
Bob finishes Wednesday morning. He has touched thirty files, swapping out the old logging calls for the new framework. Several of those files overlap with Alice's changes. He runs the tests. Everything passes. He pushes his branch.
Now someone has to bring these two streams of work back together. And when Git tries, it hits a wall. The same function Alice rewrote is the same function Bob rewired for logging. Git looks at both versions, looks at what the function used to be, and delivers its famous verdict: merge conflict. Automatic merge failed. Fix conflicts and then commit the result.
This is the moment every developer dreads. Not because it is hard, though sometimes it is. But because it forces you to stop being a programmer and start being a detective. You have to read both versions, understand what each person intended, and figure out how to combine those intentions into something that works. Git can do a lot of things automatically. Understanding human intent is not one of them.
Last episode, we talked about how Git made branching free. A forty-one-byte pointer file, microseconds to create, zero overhead. Teams everywhere took that gift and ran with it. Feature branches. Release branches. Hotfix branches. Experimental branches nobody ever planned to merge. The branching revolution changed how the entire industry works.
But every branch, eventually, has to come home. And this is where the drama lives.
At its simplest, merging is combining two parallel lines of development into one. You branched off from the main line to work on something. While you were gone, the main line moved forward. Other people added features, fixed bugs, reorganized code. Now you want your work to become part of that main line again. That is a merge.
When you run git merge and name a branch, Git does something that looks simple but involves a surprising amount of machinery. It takes the snapshot at the tip of your current branch and the snapshot at the tip of the branch you are merging in, and it tries to produce a new snapshot that contains both sets of changes.
Most of the time, this just works. If Alice changed the billing module and Bob changed the authentication module, there is no conflict. Git sees that each person touched different parts of the codebase and combines both changes cleanly. No drama. No human intervention needed. You might not even notice it happened.
This is the common case, and it is worth pausing to appreciate it. In the CVS era we talked about back in episode two, merging branches was so painful that teams avoided branching entirely. Developers worked on the same trunk, stepping on each other's code, because the alternative was worse. The fact that Git can silently, correctly, automatically combine independent changes from dozens of developers is something the industry takes for granted now. It should not. That machinery is a small miracle of computer science, and it has a history worth knowing.
Here is the key insight that makes Git's merging work as well as it does. Git does not just compare two versions of a file and try to mash them together. It finds their common ancestor, the point where the two branches diverged, and compares both versions against that shared starting point.
This is called a three-way merge. It uses three inputs: the version on your branch, the version on the other branch, and the version they both started from. That third input, the common ancestor, is what makes the difference.
Think of it like this. You and a friend are both editing copies of the same document. Without the original, if you see a difference between your two copies, you have no idea who changed what. Maybe you added a paragraph. Maybe your friend deleted one. Maybe you both changed the same sentence in different ways. All you can see is that your copies are different.
But if you also have the original document, everything becomes clear. You can compare your version against the original to see exactly what you changed. You can compare your friend's version against the original to see exactly what they changed. Now you know who did what. And if you both changed different parts, you can combine both sets of changes confidently. The original gives you context. It gives you a baseline.
This is what the three-way merge does. The common ancestor is the baseline. Git compares your branch's changes against it and the other branch's changes against it. If only one side changed a particular section, Git takes that change. If neither side changed a section, Git keeps the original. And if both sides changed the same section in different ways, that is a conflict, and Git asks you to sort it out.
The technique predates Git by decades. Three-way merge was used by diff3, a Unix utility that has been around since the early nineteen eighties. CVS used three-way merge too. But CVS often struggled with it because finding the correct common ancestor in a centralized system with limited branch tracking was unreliable. Git's content-addressed storage and its careful tracking of commit parentage mean it can almost always find the right ancestor. More information leads to better merges.
The merge strategy Git used for most of its life was called recursive. When the history between two branches is complicated, when there are multiple possible common ancestors because of earlier merges creating a tangled graph, the recursive strategy handles it by merging the common ancestors themselves first to create a synthetic ancestor. It sounds like an edge case, but in active projects with lots of cross-branch merging, it comes up constantly. The recursive strategy was tested against real merge commits from the Linux kernel development history, and it produced fewer merge conflicts and fewer silent errors than simpler approaches.
In two thousand twenty-one, a Git developer named Elijah Newren replaced the recursive strategy with something he called ort, which stands for Ostensibly Recursive's Twin. It was a complete rewrite of the merge engine, same concepts but cleaner implementation, better correctness, and dramatically better performance. In one benchmark involving a merge with many renamed files, ort was over five hundred times faster than recursive. In a series of merges during a rebase operation, it was over nine thousand times faster.
Merge-ort was written to be a from-scratch rewrite, using the same concepts as the recursive merge strategy, but solving many of the long-standing correctness and performance problems.
Ort became the default merge strategy in Git two point thirty-four, released in November two thousand twenty-one. Most developers never noticed the switch. Their merges just got faster and more correct, silently, in the background. Which is exactly how good infrastructure should work.
So Git finds the common ancestor, compares both branches against it, and automatically combines changes that do not overlap. Beautiful. But what happens when two people change the same lines of the same file in different ways?
Git stops. It marks the file with conflict markers, ugly angle brackets and equal signs that show you both versions side by side, and it says: I cannot figure this out. You decide.
This is a merge conflict. And despite being one of the most feared experiences in software development, it is actually Git doing exactly the right thing. The alternative would be worse. The alternative would be Git guessing. And Git guessing wrong would mean silent data loss, your changes or your colleague's changes quietly disappearing without anyone noticing.
When you hit a merge conflict, you have three options. You can open the conflicted files, read both versions, manually combine them into something correct, and tell Git to continue. You can run git merge with the dash-dash-abort flag, which backs out the entire merge and returns your branch to the state it was in before you tried. Or you can use a visual merge tool, software that shows you the three versions side by side, your version, their version, and the ancestor, and lets you build the result interactively.
The first option is what most developers do. The third option is what experienced developers recommend. The second option is what panicking developers reach for at four in the afternoon on a Friday.
But here is the thing about merge conflicts that the textual conflict markers do not tell you. The conflicts Git catches are the easy ones. Two people changed the same line. Obvious. Flagged. Resolved by a human who reads both versions.
The dangerous merges are the ones Git does not catch.
Martin Fowler, the British software engineer who has spent decades writing about development practices, calls these semantic conflicts. A semantic conflict is when two changes can be merged cleanly on a textual level, no overlapping lines, no conflict markers, but the combined result is broken.
However powerful your tooling is, it will only protect you from textual conflicts.
Here is a classic example. Alice renames a function from calculate to calculateBill across the entire codebase. Every caller updated. Tests pass. Meanwhile, Bob on his branch adds three new calls to the old function name, calculate, because that is what it was called when he branched. Git merges their work cleanly. No conflicts. But the code is broken. Bob's new calls point to a function name that no longer exists. The tests might catch it. They might not. Research has found that code associated with semantic merge conflicts is twenty-six times more likely to contain a bug compared to code without conflicts.
Git does not understand code. It understands text. It can tell you that two people changed the same line of text in different ways. It cannot tell you that two people changed different lines in ways that are logically incompatible. That requires understanding what the code means, what it does, what it intends. And that is a problem no version control system has solved.
The practical defense against semantic conflicts is the same defense that trunk-based development teams have been preaching for years: merge often. The longer a branch lives in isolation, the more the world changes underneath it, and the more likely a clean merge hides a broken program. Merge after hours, not after weeks.
Every developer who has been in the industry long enough has a merge horror story. The details change but the shape is always the same. A branch lived too long. It drifted too far from the main line. And when someone finally tried to bring it home, they walked into a nightmare.
The classic version goes like this. A team creates a feature branch for a major new capability. It will take about six weeks, the estimate says. The team works heads-down on their branch, making progress, writing tests, feeling good. Meanwhile, the main branch keeps moving. Other teams are shipping features, refactoring modules, updating dependencies. Nobody is merging between the feature branch and the main line because the team wants to stay focused and avoid distractions.
Six weeks turns into eight. Then ten. The feature is almost done, they keep saying. Just a few more things.
When the day finally comes to merge, someone runs git merge and watches the output scroll. Forty-seven files with conflicts. Not small conflicts. Structural conflicts. An entire module was reorganized on the main branch while the feature team was building on the old structure. A shared library was upgraded to a new major version with a different interface. Configuration files were reformatted.
The team spends three days resolving conflicts. Not three days of focused merge work. Three days of reading code they did not write, understanding changes they did not make, testing combinations that nobody ever intended. By the end, the merge commit touches more files than the entire feature branch did. And nobody is confident it is correct.
This is the three-day merge. It has convinced more teams to adopt trunk-based development than any conference talk or blog post ever could. Not because of an argument about best practices. Because of the visceral experience of spending half a week doing work that produced no new value, only the absence of disaster.
The irony is that Git handled the mechanics flawlessly. It found the common ancestor. It automatically merged everything it could. It flagged the genuine conflicts accurately. The problem was not the tool. The problem was the process. A branch that lives for ten weeks will diverge from the main line in ways that no merge algorithm can reconcile automatically. The longer you wait, the harder the merge, not linearly but exponentially. Two weeks of drift might produce five conflicts. Ten weeks might produce fifty.
Linus Torvalds has been characteristically blunt about this. On the Linux kernel mailing list, he laid out his philosophy on merging with the directness his community has come to expect. A merge, he wrote, should still be a big deal, something you think about. Merging on its own is not wrong or evil. It is a very good operation. But mindless merging is bad.
He went further. The kinds of merges he really dislikes, he said, are the ones that are basically "let us do a regular merge every day to keep up to date." Not because frequent integration is bad, but because merging without thinking about what you are merging and why is bad. Every merge should be a conscious decision with a clear purpose, not an automatic reflex. If you cannot explain what and why you merged, Linus wrote, you probably should not be merging. That is a good rule of thumb.
When a merge completes successfully, Git creates something called a merge commit. It looks like any other commit in your history, a snapshot of the entire project at one particular moment. But there is one crucial difference. A normal commit has one parent, the commit that came directly before it. A merge commit has two parents. One pointing back along your branch, and one pointing back along the branch you just merged in.
This is how Git records the fact that two lines of development came together. The merge commit is a permanent record: at this point in time, these two histories were combined into one. Follow one parent and you see the work that happened on your branch. Follow the other and you see the work that happened on the merged branch.
The commit graph, that chain of snapshots we talked about in episode five, is no longer a straight line. It is a graph with branches that diverge and converge, a map of how the project actually evolved.
Some teams love merge commits. They preserve the true topology of development. You can look at the history and see exactly when a feature branch was created, how long it lived, and when it was integrated. The merge commit itself carries a message explaining why the merge happened. For large projects with many contributors, this topological history is valuable. It tells the story of how the project was built, not just what was built.
Some teams hate merge commits. They clutter the log. A history full of "Merge branch feature-seventeen into main" commits makes it harder to read the actual changes. When you are trying to track down when a bug was introduced, wading through merge commits that add no information of their own is noise.
Linus himself has strong opinions here, at least about how merge commit messages should be written. In two thousand twenty-one, he called out merge commits created through GitHub's web interface as "absolutely useless garbage" because they carry generic, auto-generated messages that explain nothing about why the merge was made.
GitHub creates absolutely useless garbage merges, and you should never ever use the GitHub interface to merge anything.
Strong words, but his point is a real one. A merge commit that just says "Merge pull request number four seven three from feature-branch" tells you nothing. A merge commit that says "Integrate the new billing pipeline, replacing the batch processor with streaming events, tested against three months of production data" tells you everything. The merge commit is a narrative moment in your project's history. Wasting it on boilerplate is wasting a chance to communicate.
This love-it-or-hate-it quality of merge commits is not just an aesthetic preference. It drives one of the most heated debates in the Git world: the question of whether you should merge at all, or whether there is a cleaner way to bring branches back together. That debate has a name. It is called rebase. And it is the subject of the next episode.
But for now, the important thing is this: the merge commit, a snapshot with two parents, is Git's way of recording truth. Two streams of work existed in parallel and were brought together here. Whether you find that record useful or noisy depends on your team, your workflow, and your philosophy about what version control history is for.
Here is the thing about merging that most tutorials miss. They focus on the mechanics. Run this command. Resolve conflicts with this tool. Push the result. But the mechanics are the easy part. The hard part, and the interesting part, is the design philosophy underneath.
Git automates everything it can. Different files changed? Merged automatically. Same file, different sections? Merged automatically. Same file, different lines? Merged automatically. Git's three-way merge, powered by twenty years of refinement from the recursive strategy to ort, handles the vast majority of cases without any human involvement. Most merges, in most projects, resolve silently and correctly.
But when Git hits the boundary of what automation can handle, when two humans made incompatible decisions about the same piece of code, it stops. It does not guess. It does not pick a winner. It does not silently discard one version. It presents both options and asks a human to decide.
This is good design. Not just good version control design, but good tool design in general. Automate the routine. Escalate the exceptional. Make the boundary between automated and manual decisions explicit so the human knows exactly what they need to pay attention to.
The merge conflict is not a failure of Git. It is Git succeeding at the thing it was designed to do: handle the easy cases so humans can focus on the hard ones. Every minute Git saves you on automatic merges is a minute you can spend thinking carefully about the conflicts that actually require judgment.
Junio Hamano, who has maintained Git for twenty years and has reviewed more merge-related code than perhaps anyone alive, approaches the merge machinery with the quiet precision that characterizes all his work. When Elijah Newren proposed replacing recursive with ort, the change touched some of the most critical code in Git. Junio reviewed it with his characteristic thoroughness on the mailing list, asking questions that exposed edge cases Newren had not considered, taking weeks before he was satisfied. He does not rush. He never rushes. Under his stewardship, the merge engine has grown from Linus's initial implementation to the sophisticated ort strategy, always improving correctness, always getting faster, but never overstepping. Git will never try to be smarter than the developer. It will do what it can prove is correct, and leave the rest to you.
This is the philosophy that makes Git's merging trustworthy. In a world of software that tries to do everything automatically, that autocorrects your spelling and autocompletes your sentences and autoformats your code, Git's willingness to stop and say "I do not know what you intended here" is refreshingly honest. It respects the boundary between what a machine can determine and what only a human can decide.
The merge is where all those cheap branches from last episode come back to reality. Branching is the easy part, a forty-one-byte pointer, microseconds, zero risk. Merging is where parallel universes collide and someone has to decide which version of reality survives. Git handles most of those collisions silently and well. For the rest, it asks you. And that asking, that moment where the tool steps back and the human steps forward, is not a limitation. It is the entire point.
Next episode, we look at the other side of this coin. Because some teams looked at merge commits and said: there has to be a better way. A way to bring branches together without the two-parent snapshots, without the tangled history graphs, without the merge commits cluttering the log. They wanted a clean, linear history that reads like a story instead of a map. What they wanted is called a rebase. And the argument about whether that is a good idea has been raging for fifteen years.
git merge, followed by a branch name. That is the moment of truth. Most of the time, nothing dramatic happens. Git quietly combines both sets of changes and moves on. You might not even notice it worked. But sometimes Git stops and says, in its polite, mechanical way, that it cannot figure out what you intended. That is not a failure. That is the tool being honest about the limits of what automation can decide. The merge is where the human earns their keep.