Git Merge and Rebase: Resolve Merge Conflicts
Merge Git branches (fast-forward, merge commit, squash), resolve merge conflicts step by step, rebase onto main, and pick merge or rebase.
A branch is only useful once its work gets back into main. Git has two ways to do that: merging, which joins two lines of history, and rebasing, which replays your commits on top of the other line. This part shows both on the project, with the real output, including the part people dread: a merge conflict, where both sides changed the same lines and Git needs you to decide.
Prerequisites
- The
status-pagerepository from Part 4:mainhas a README fix andfeature/dark-modehas two commits. - Branching basics:
git switch -c,git log --graph(Part 4).
Before merging anything, start one more branch from the current main. It adds a service and documents the build; you come back to it in the rebase section:
How Git Merges
To merge branch B into your current branch A, Git finds the merge base: the newest commit both branches share. What happens next depends on whether A has moved since then.
- Fast-forward. If
Ahas no new commits since the merge base,Bsimply contains everything inA. Git movesA's pointer forward toB's commit. No new commit is made and the history stays a straight line. - Three-way merge. If both branches have new commits, Git compares the merge base with each side. Changes made on only one side are taken as they are. Changes to the same lines on both sides are a conflict. If there are no conflicts, Git records the result as a merge commit with two parents.
Fast-Forward Merge
A small fix on its own branch, then a merge while main has not moved:
"Fast-forward" means main now points to the same commit as fix/footer-text. git branch -d deleted the branch without complaint because its work is in main.
Merge Commit (Three-Way Merge)
feature/dark-mode and main have both moved since the branch started, so merging it makes a merge commit:
Without --no-edit, Git opens your editor with the message Merge branch 'feature/dark-mode' so you can add why the merge happened; saving the file accepts it. The branch's work is now in main, so git branch -d deletes it without complaint. "ort" is Git's default merge strategy since Git 2.34; it handles renames, and -s recursive is now only another name for it.
Choose How a Merge Is Recorded
| Option | Result |
|---|---|
(none), --ff |
Fast-forward when possible, otherwise a merge commit. The default |
--no-ff |
Always a merge commit, even when a fast-forward is possible, so the branch stays visible in the history |
--ff-only |
Fast-forward or nothing: refuse if a merge commit would be needed |
--squash |
Put all of the branch's changes into the staging area as one change, for you to commit as a single new commit |
Hosting platforms offer the same choice as buttons on a pull request ("Create a merge commit", "Squash and merge", "Rebase and merge"); Part 7 compares them. To make one behavior the default, set merge.ff (false for always --no-ff, only for always --ff-only).
Resolve a Merge Conflict
A conflict happens when both branches changed the same lines (or one changed a file the other deleted). Set one up: on a branch, put the team name in the page heading; on main, reword the same heading:
Now merge:
The merge stopped halfway. Files without conflicts are already merged and staged; template.html is listed under "Unmerged paths". Git wrote both versions into the file, between conflict markers:
<<<<<<< HEADstarts your side: the branch you are on (main).=======separates the two sides.>>>>>>> feature/headingends their side: the branch you are merging in.
Abort, or see the original too
If you are not ready to deal with it, git merge --abort puts everything back to how it was before the merge:
The default markers show the two results but not what the line looked like before either change. The zdiff3 conflict style adds the merge base between ||||||| and =======, which often makes the right answer obvious. It is worth turning on for good:
Now you can see that the original was Service status: one side added the team name, the other reworded it. The right result combines both. (git checkout --conflict=zdiff3 <file> rewrites the markers of a file that is already in conflict, if you turn the setting on in the middle of a merge.)
Fix the file, stage it, commit
Edit template.html: keep the line you want and delete all marker lines. Git 2.56 added git add --resolved, which stages the files that were in conflict and refuses while markers are still in them. Try it before editing:
git diff during a conflict shows a combined diff with two columns of + and -, one per parent; here it shows the new line replacing both versions. git add <file> also marks a file as resolved; --resolved is safer because it checks for leftover markers and leaves your other unstaged changes alone. git merge --continue works as well as git commit to finish.
Some shortcuts for whole files:
git checkout --ours template.html # keep your version of the whole file
git checkout --theirs template.html # take their version of the whole file
git log --merge -p template.html # the commits on both sides that touched the file
Use a merge tool
A merge tool shows the two sides and the base next to each other. Git knows many tools by name; set one and run git mergetool during a conflict:
git mergetool --tool-help lists the tools Git found on your system. Most editors (VS Code, JetBrains IDEs) also detect conflict markers on their own and offer "Accept current", "Accept incoming" and "Accept both" buttons. To practice without breaking anything, try the Git merge conflict simulator.
Rebase: Replay Your Commits on Top of main
feature/status-service started before the merges above, so main has moved on. A..B means "commits in B that are not in A" (Part 8 has more on ranges). A fast-forward is no longer possible:
Instead of a merge commit, you can rebase the branch: Git takes the branch's commits, sets them aside, moves the branch to the tip of main, and applies the commits again one by one:
The two commits have new hashes: they are new commits with the same changes and messages, but a different parent. The old ones still exist for a while: right after a rebase, ORIG_HEAD points to where the branch was before it (the next merge or reset overwrites ORIG_HEAD), and the reflog keeps the old tip for weeks, which is how you undo a rebase.
If a commit does not apply cleanly, the rebase stops at that commit with conflict markers, just like a merge. Fix the files, git add them, and run git rebase --continue. git rebase --skip drops the commit that stopped, and git rebase --abort returns everything to how it was before.
Rebase only commits that nobody else has. Rebasing a branch that others have pulled gives it new commits with new hashes; their copies still have the old ones, and the next pull mixes both. For your own branch that you pushed only for review, rebasing and force-pushing with --force-with-lease is normal (Part 6).
Squash Merge: One Commit for the Whole Branch
A branch with several small "wip" commits can be merged as one commit with --squash:
The squash commit has one parent and no link to the branch, so Git cannot tell that the branch's commits are in main, and git branch -d refuses. That is expected after a squash merge. The empty git diff right before it confirmed that main has exactly the branch's files, so the work landed and git branch -D feature/more-services is safe. The same happens after "Squash and merge" on GitHub or GitLab.
Clean up the merged branches and tag the second release:
Merge or Rebase?
Both end with the same files. They differ in the history they leave behind:
| Merge | Rebase | |
|---|---|---|
| History | Keeps what really happened, with merge commits | A straight line, as if you started from the newest main |
| Commits | Unchanged, same hashes | Rewritten, new hashes |
| Conflicts | Resolved once, in the merge commit | Possibly once per replayed commit |
| Safe on shared branches | Yes | No: only on branches only you use |
| Good for | Integrating finished work into main |
Updating your own branch before review, tidying commits |
A common combination: rebase your own branch onto the latest main while you work on it (git fetch, then git rebase origin/main; Part 6 sets up origin), and let the team's pull request merge method decide how it lands. Rebase vs merge is also a classic interview question.
Merge Strategy Options
You rarely need these, but they are worth knowing:
| Command | Effect |
|---|---|
git merge -X ours <branch> |
On conflicting hunks only, keep your side; other changes from the branch still merge in |
git merge -X theirs <branch> |
On conflicting hunks only, take their side |
git merge -X ignore-space-change <branch> |
Treat whitespace-only changes as unchanged, which avoids conflicts from reformatting |
git merge -s ours <branch> |
Record a merge but keep your tree completely; used to mark a branch as merged without its changes |
git merge a b c |
An "octopus" merge of several branches at once; only works without conflicts |
-X ours and -s ours are easy to mix up: the first is an option of the normal strategy, the second is a different strategy that ignores the other branch entirely. If you resolve the same conflict again and again (for example while rebasing a long-lived branch), rerere can record and replay your resolution (Part 10).
To copy a single commit from one branch to another without merging the branch, use git cherry-pick; Part 7 uses it to backport a fix.
Common Mistakes
- Committing conflict markers. Search for
<<<<<<<before you commit, or usegit add --resolved, which refuses. - Resolving by deleting the other side without reading it. The other change was made for a reason; zdiff3 markers show what the line looked like before both changes.
- Rebasing a shared branch. Merge
maininto it instead, or agree with everyone using it first. git pullmerges you did not mean to make. Choose a pull strategy once (Part 6).- Deleting a squash-merged branch with
-Dwithout checking.-Ddeletes unmerged work too. Compare the snapshots first, as above; ifmainhas moved on since the squash merge, review what the diff still shows. - "refusing to merge unrelated histories". Two repositories with no common commit; see this post before adding
--allow-unrelated-histories.
Summary
- A merge finds the merge base; it fast-forwards when only one side moved and makes a merge commit when both did.
--no-ff,--ff-onlyand--squashchoose how the result is recorded.- In a conflict, read both sides (zdiff3 also shows the base), edit the file, stage it with
git add --resolved, and commit.git merge --abortgets you out. git rebase mainreplays your commits on top ofmainwith new hashes; never rebase commits others already have.
Next: Working with Remote Repositories, where the project gets a server and a second developer.
Keep Learning
Tags
Found an issue?