Resolving Git Merge Conflicts With LLMs in October 2026: Show the Base Version, Make the Model Explain Both Sides, and Let the Tests Decide
Merge conflicts are one of the places where an LLM can genuinely save time, and also one of the places where it can quietly do real damage. A model is good at reading two versions of a function and explaining what each side was trying to do. It is bad at knowing which side your team actually wants, and it will happily produce a tidy-looking resolution that drops one branch's change without a word. This guide covers a workflow that keeps the speed and removes most of the risk: give the model the full picture, make it explain both intents before it writes anything, and let your tests and a final diff decide whether the result is right.
Why conflicts are tricky for a model
A conflict happens when two branches changed the same lines (or nearby lines) since their common ancestor. Git stops and leaves markers in the file because it cannot tell which change should win. The important detail is that there are really three versions involved, not two:
- Base: the file as it was at the common ancestor commit.
- Ours: the file on the branch you are currently on.
- Theirs: the file on the branch you are bringing in.
Git's default conflict markers only show "ours" and "theirs". Without the base, neither you nor the model can tell whether a line was added on one side or deleted on the other. Those two situations look identical in a two-way view and need opposite resolutions. Most bad LLM resolutions come from this missing context, so the first step is to fix it.
Two streams meeting is easy; deciding what the combined flow should carry is the hard part. Image: John Day River mouth.jpg by Sam Beebe, CC BY 2.0, via Wikimedia Commons.
Step 1: Turn on three-way conflict markers
Tell Git to include the base version inside every conflict hunk:
git config --global merge.conflictStyle zdiff3
zdiff3 is available in Git 2.35 and later; on older versions use diff3. A conflict now looks like this:
<<<<<<< HEAD
def total(items, tax_rate=0.08):
return sum(i.price * i.qty for i in items) * (1 + tax_rate)
||||||| base
def total(items):
return sum(i.price for i in items)
=======
def total(items):
return sum(i.price * i.qty for i in items if not i.refunded)
>>>>>>> feature/refunds
With the base visible, the story is clear: one branch added quantity and tax, the other branch added quantity and a refund filter. The correct answer keeps all three changes. A model shown only the top and bottom sections often picks one and loses the other.
If you are already in the middle of a merge with two-way markers, you can regenerate a file's markers in the new style:
git checkout --conflict=zdiff3 path/to/file.py
That overwrites any manual edits you made to that file, so run it before you start resolving, not halfway through.
Step 2: Collect the context the model actually needs
Pasting one conflict hunk is rarely enough. Gather a small packet before you open the chat:
- The list of conflicted files.
git diff --name-only --diff-filter=Uprints only the unmerged paths. - The three full versions of each file, if the file is short enough. Git keeps them in the index during a conflict:
git show :1:path/to/file.pyis the base,:2:is ours, and:3:is theirs. - Why each side changed.
git log --merge -p -- path/to/file.pyshows the commits on both branches that touched the file, with their messages. Commit messages and linked ticket titles are often the only record of intent. - Callers and tests for any function whose signature changed. If one side added a parameter, the model needs to see who calls it.
Strip secrets and customer data from anything you paste, the same as you would for any other prompt.
Step 3: Ask for intent first, code second
The most useful single change to your prompt is to forbid the model from writing a resolution until it has described what each side did. A prompt along these lines works well:
I'm resolving a Git merge conflict. Below are the base, ours (main),
and theirs (feature/refunds) versions of the conflicted region, plus
the commit messages from both branches.
1. In plain words, describe what changed from base to ours, and what
changed from base to theirs.
2. Say whether the two changes are compatible, overlapping, or
genuinely contradictory.
3. Only then propose a resolved version that keeps every intended
change. If something must be dropped, say exactly what and why.
4. List any other places in the codebase that may need updating
because of the combined change.
Read step 1 carefully before looking at the code. If the model's description of either side is wrong, its resolution will be wrong too, and it is much easier to spot a mistake in a two-sentence summary than in a forty-line function. Step 2 matters because some conflicts are not mechanical at all: if one branch removed a feature flag and the other added a new use of it, no amount of clever merging fixes that. That is a decision for a person, and the model should say so instead of guessing.
Step 4: Watch out for "ours" and "theirs" during a rebase
This trips up people and models alike. During a git merge, "ours" is your current branch and "theirs" is the branch being merged in. During a git rebase, it is reversed: Git replays your commits on top of the upstream branch, so "ours" is the upstream you are rebasing onto and "theirs" is your own commit being replayed.
So if you tell a model "keep my changes, which are ours" during a rebase, you may be asking it to throw away your own work. Label the sides by branch name in your prompt instead of using the words ours and theirs, and check the marker labels in the file (the text after <<<<<<< and >>>>>>>) to confirm which is which.
Step 5: Don't hand-merge generated files
Some files should never be resolved line by line, by you or by a model:
- Lockfiles such as
package-lock.json,yarn.lock,poetry.lock, orCargo.lock. Resolve the manifest (package.json,pyproject.toml, and so on) first, then take either side of the lockfile and regenerate it with your package manager's install command. - Build output and compiled assets. Rebuild them from the merged source.
- Auto-generated code such as protobuf stubs, ORM migration snapshots, or API clients. Re-run the generator.
A model asked to merge two halves of a lockfile will produce something that looks plausible and may reference package versions that were never resolved together. Regenerating is faster and correct.
Step 6: Apply the resolution and check for leftovers
Paste the proposed resolution into the file yourself, hunk by hunk, rather than replacing the whole file with model output. Whole-file replacement is how unrelated lines outside the conflict quietly change. Then run two quick checks:
# Flags leftover conflict markers and whitespace problems
git diff --check
# Search explicitly for any marker the check might miss
grep -rnE '^(<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|)' -- path/to/file.py
Only mark the file resolved with git add path/to/file.py when both come back clean.
At a busy junction, every track has to end up somewhere on purpose. Check where each branch's changes landed. Image: Aerial photograph of Clapham Junction railway station.jpg by Mertbiol, CC0, via Wikimedia Commons.
Step 7: Let the tests and the combined diff have the last word
A resolution that compiles is not the same as a correct one. Before you commit the merge:
- Run the build and the test suite. Pay special attention to tests from both branches. If the feature branch added a test for refunds and it now fails, the resolution dropped something.
- Review the merge against each parent.
git diff HEADbefore committing shows what the merge changes relative to your branch. After committing,git show --remerge-diff(Git 2.36 and later) shows how your resolution differs from Git's automatic attempt, which is a compact way to review exactly the decisions you or the model made. - Look for semantic conflicts. Git only reports overlapping text. If one branch renamed a function and the other added a new call to the old name in a different file, the merge succeeds silently and the code breaks. Ask the model to list call sites of anything renamed or re-signatured, then confirm with a search.
If things go badly, you can always back out and start over: git merge --abort or git rebase --abort returns you to where you were before the operation began.
Step 8: Stop resolving the same conflict twice
Long-lived branches that are rebased repeatedly tend to hit the same conflicts again and again. Git has a built-in feature for this called rerere ("reuse recorded resolution"):
git config --global rerere.enabled true
Once enabled, Git records how you resolved each conflict and reapplies that resolution automatically if the identical conflict appears again. It still leaves the file for you to review and stage, so you keep control. Combined with a careful first resolution, this means a tricky conflict only costs you model time and review time once.
A worked example
Take the total() conflict from Step 1. A good model response would describe the sides like this: main changed the function to multiply by quantity and apply a configurable tax rate; feature/refunds changed it to multiply by quantity and skip refunded items. Both added quantity, so that part agrees. Tax and the refund filter are independent and compatible. The proposed resolution:
def total(items, tax_rate=0.08):
subtotal = sum(i.price * i.qty for i in items if not i.refunded)
return subtotal * (1 + tax_rate)
It should then flag that callers relying on the old one-argument signature still work because tax_rate has a default, and that any test asserting the untaxed total from the refunds branch will now fail and needs its expected value updated, which is a decision a person should confirm rather than an automatic fix.
That last point is the whole approach in miniature. The model does the reading and drafting quickly, you confirm it understood both sides, and the tests tell you whether the combined behavior is what your team actually wants.
Quick checklist
- Set
merge.conflictStyletozdiff3so the base is always visible. - Give the model all three versions, the commit messages, and relevant callers.
- Require a plain-language description of both sides before any code.
- Name branches explicitly; don't trust "ours" and "theirs" during a rebase.
- Regenerate lockfiles and generated code instead of merging them.
- Apply changes hunk by hunk, then run
git diff --check. - Run tests from both branches and review the merge diff before committing.
- Enable rerere so a resolved conflict stays resolved.
Comments
Post a Comment