Resolving Pull Request Conflicts
When two branches change the same lines of a file — or one branch edits a file that another branch deletes — Git cannot automatically combine the changes, and GitHub marks the pull request as having a conflict that must be resolved before it can merge. This lesson covers exactly what a conflict is, how to resolve one from the command line (with both git merge and git rebase), when GitHub’s web-based conflict editor is good enough, and the mistakes that turn a routine conflict into a painful one.
Overview: How Conflicts Happen
A pull request asks GitHub to combine your feature branch into a base branch (usually main). Git resolves this with a three-way merge: it looks at your branch tip, the base branch tip, and their common ancestor commit, then applies the changes each side made relative to that ancestor. If both sides changed the same lines of the same file, Git has no way to know which version — or what combination — you want, so it stops and asks you to decide. This is a conflict.
Conflicts are not a sign you did something wrong. They happen naturally when a project has more than one active contributor, especially on long-lived branches that drift far from main before opening a pull request. GitHub detects this the same way Git does locally: it performs a trial merge of your branch against the base branch and reports whether it succeeds cleanly. When it can’t, the pull request page shows "This branch has conflicts that must be resolved" and lists the affected files.
Two kinds of conflicts exist. A textual conflict is what you’ll see most often: overlapping line edits in the same file. A content conflict can also happen without overlapping lines — for example, one branch deletes a file while the other modifies it, or both branches rename the same file differently. GitHub’s web editor can only fix simple, single-file textual conflicts; anything more complex (multiple files, renames, deletions, binary files) must be resolved locally with Git.
Under the hood, when a conflict occurs Git writes conflict markers directly into the working-tree copy of the affected file, and marks that file’s entry in the index as unmerged rather than resolved. The index temporarily stores three versions of the conflicted file at once — stage 1 (the common ancestor), stage 2 ("ours", your current branch), and stage 3 ("theirs", the branch being merged in). Resolving the conflict means editing the working-tree file to the content you actually want, then running git add, which collapses those three stages back down to a single resolved stage 0 entry.
Syntax
There is no single "resolve conflict" command — resolving a conflict is a short workflow. The general shape when updating a feature branch with the latest base branch is:
git fetch origin
git switch feature/login-page
git merge origin/main
git status
git fetch origin— downloads the latest commits from GitHub without touching your working tree (see the fetch-vs-pull distinction below).git switch feature/login-page— makes sure you’re on your feature branch, the one behind the pull request.git merge origin/main— merges the remote-tracking branchorigin/maininto your current branch, triggering a conflict if one exists.git status— after a conflict, lists files under "Unmerged paths" so you know exactly what to fix.
To resolve, you then edit each conflicted file, stage it, and commit:
| Command | Purpose |
|---|---|
git diff |
Shows conflict markers and the competing changes inline. |
git add <file> |
Marks a conflicted file as resolved once you’ve edited it. |
git commit |
Completes a merge conflict resolution (Git pre-fills a merge commit message). |
git rebase --continue |
Completes one step of a rebase conflict resolution (used instead of commit when rebasing). |
git merge --abort |
Bails out of a conflicted merge and restores the pre-merge state. |
git rebase --abort |
Bails out of a conflicted rebase and restores the pre-rebase state. |
Examples
Example 1: Resolving a conflict with git merge
Your pull request branch feature/login-page is behind main, and both branches edited the same function in src/login.js.
git fetch origin
git switch feature/login-page
git merge origin/main
Output:
Auto-merging src/login.js
CONFLICT (content): Merge conflict in src/login.js
Automatic merge failed; fix conflicts and then commit the result.
Opening src/login.js shows conflict markers around the disputed lines:
<<<<<<< HEAD
function login(username, password) {
return authenticate(username, password);
}
=======
function login(email, password) {
return authenticate(email, password, { rememberMe: true });
}
>>>>>>> origin/main
Everything between <<<<<<< HEAD and ======= is your branch’s version; everything between ======= and >>>>>>> origin/main is the incoming version. You edit the file down to the version you actually want (often a combination of both), delete all three marker lines, save, then finish the merge:
git add src/login.js
git commit
git push origin feature/login-page
Git pre-fills a commit message like Merge branch 'main' into feature/login-page; you can accept it as-is. Pushing updates the pull request, and GitHub re-runs its mergeability check — the conflict banner disappears once the check passes.
Example 2: Resolving directly in GitHub’s web editor
For a small, single-file conflict, GitHub lets you resolve without touching a terminal. On the pull request page, click Resolve conflicts. GitHub shows the same <<<<<<< / ======= / >>>>>>> markers in an inline editor; you edit the text, remove the markers, and click Mark as resolved, then Commit merge. GitHub creates the merge commit directly on your branch. Afterwards, run git fetch origin and git switch feature/login-page followed by git merge origin/feature/login-page locally (or just git pull) so your local branch matches what’s now on GitHub — otherwise your next push will itself conflict with the commit GitHub just made.
Example 3: Resolving a conflict during git rebase
Instead of merging main into your branch, you can rebase your branch onto main, which replays your commits one at a time on top of the latest base — producing a linear history instead of a merge commit.
git fetch origin
git switch feature/payment-api
git rebase origin/main
Output:
Auto-merging src/payment.js
CONFLICT (content): Merge conflict in src/payment.js
error: could not apply a1b2c3d... Add retry logic to payment charge
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You resolve the markers in src/payment.js exactly as before, but the next step differs from a merge:
git add src/payment.js
git rebase --continue
git push --force-with-lease origin feature/payment-api
Because rebase rewrites your branch’s commits (they get new SHA-1 hashes), the remote copy of feature/payment-api no longer matches — a plain git push will be rejected, so a force push is required. Always prefer --force-with-lease over bare --force: it aborts if someone else pushed to the branch since you last fetched, preventing you from silently discarding their work.
How It Works Step by Step
When git merge or git rebase hits a conflict, here’s what actually happens internally:
- Git identifies the merge base — the most recent commit both branches share — and computes what each branch changed relative to it.
- For each file, Git tries to apply both sets of changes. If the changed regions don’t overlap, it merges automatically and the file needs no attention.
- If regions overlap, Git writes all three versions (base, ours, theirs) into the working-tree file, separated by conflict markers, and records the file as unmerged (multiple stages) in the index instead of a single resolved entry.
- Git pauses the operation. For a merge,
MERGE_HEADis written to.git/, recording the commit being merged in. For a rebase, Git is mid-replay, working through commits one at a time from a temporary state. - You edit the working-tree file to the final, correct content and delete the markers. This is manual — Git never guesses which side is "right."
git add <file>re-reads the file into the index as a single resolved stage 0 entry, telling Git this file’s conflict is settled.git commit(merge) creates a new commit with two parents — your previous tip and the branch you merged in — recording the merge in history.git rebase --continueinstead creates a new commit with the resolved tree and moves to the next commit being replayed, with no merge commit at all.
Common Mistakes
Mistake 1: Committing with conflict markers still in the file
It’s easy to stage a file after only partially editing it, leaving stray <<<<<<< or ======= lines in the committed code — this compiles as garbage or breaks the build.
# WRONG: staged without removing markers
git add src/login.js
git commit -m "fix: resolve conflict"
Run git diff --check before committing — it flags leftover conflict markers automatically. Always re-open the file and confirm no <<<<<<<, =======, or >>>>>>> lines remain.
Mistake 2: Force-pushing a rebased branch without --force-with-lease
After rebasing, a bare force push overwrites whatever is on the remote — including a teammate’s commits pushed since your last fetch.
# RISKY: silently discards anything you haven't fetched
git push --force origin feature/payment-api
Use git push --force-with-lease origin feature/payment-api instead; it refuses to push if the remote has moved since you last fetched it.
Mistake 3: Blindly picking "ours" or "theirs" without reading the code
Using git checkout --ours <file> or --theirs <file> to skip reading the diff resolves the textual conflict but can silently reintroduce a bug the other branch just fixed, or drop a feature entirely. Treat every conflict as a real code-review decision, not a formality to click through.
Mistake 4: Rebasing a branch other people are also working on
If a feature branch is shared and someone else has already pulled it, rebasing rewrites commit hashes and diverges their copy from yours, producing duplicate commits and confusing conflicts for everyone. This is Git’s golden rule of rebasing: never rebase commits that have been pushed and pulled by others — merge instead, or coordinate first.
Best Practices
- Merge or rebase from the base branch into your feature branch early and often, rather than letting a pull request drift for weeks — small, frequent conflicts are far easier to resolve than one enormous one.
- Prefer
git mergefor branches other people also commit to; reservegit rebasefor branches only you are working on. - Run your test suite after resolving conflicts, before pushing — a conflict can resolve cleanly at the text level while still being logically wrong.
- Use
git diff --checkor a quick visual scan for leftover<<<<<<<markers before every commit that follows a conflict resolution. - Always use
git push --force-with-lease, never bare--force, after rewriting history with rebase. - For a conflict touching more than one file, resolve locally rather than in GitHub’s web editor, which only handles single-file, non-binary conflicts.
- Communicate before rebasing a branch that teammates have already pulled.
Practice Exercises
Exercise 1: Create a new repository with a single file. On main, change line 1 and commit. Create a branch feature/edit from the commit before that change, edit the same line differently, and commit. Merge main into feature/edit and resolve the resulting conflict, verifying with git status that the file shows as resolved before committing.
Exercise 2: Open a pull request on GitHub where the feature branch is deliberately behind main with an overlapping change. Try GitHub’s Resolve conflicts web editor first, then undo it locally and instead resolve the same conflict with git merge origin/main from the command line. Compare the resulting commit history in git log --graph --oneline for each approach.
Exercise 3: Repeat exercise 1, but use git rebase origin/main instead of git merge. After resolving, confirm with git log --graph --oneline that history is linear (no merge commit), then push with git push --force-with-lease.
Summary
- A pull request conflict happens when the same lines (or same file existence) were changed differently on both branches, and Git’s three-way merge can’t combine them automatically.
- Conflicted files get literal
<<<<<<</=======/>>>>>>>markers written into the working tree; you edit them out manually, thengit addto mark the file resolved. git mergeresolution finishes withgit commit(creates a two-parent merge commit);git rebaseresolution finishes withgit rebase --continue(rewrites commits, keeps history linear, but requires a force push).- GitHub’s web conflict editor works only for simple, single-file, non-binary conflicts — anything more complex needs a local resolution.
- Never bare force-push after a rebase; use
--force-with-lease, and never rebase a branch others already have local copies of. - Always re-check for leftover conflict markers and re-run tests before pushing a resolved conflict.
