Merging, Rebasing and Conflicts
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Once two branches have diverged, bringing their changes back together is either a merge, which records how the
histories came together, or a rebase, which rewrites one branch’s commits so the histories look as if they had
never diverged. Both operations can produce conflicts when the same lines were changed on both sides, and this
page covers how to read and resolve them. It is written against Git 2.55.x and GitHub CLI 2.101.x on the command
line, IntelliJ IDEA 2026.2, the current stable release of VS Code with the GitHub Pull Requests and Issues
extension, and github.com as of September 2026; every example assumes a repository whose default branch is
main. For the branches being merged or rebased themselves, see
Branches; for opening and merging a pull request on GitHub,
see Pull Requests.
Concept: kinds of merge
git merge <branch> integrates the tip of <branch> into the branch currently checked out. Which of the
following happens depends on whether the two branches have diverged and which flags are passed.
Fast-forward merge
If the current branch’s tip is a direct ancestor of the branch being merged — nothing new happened on the current branch since it branched off — Git has no divergent work to reconcile. It simply moves the current branch’s pointer forward to the other branch’s tip. No new commit is created.
After git merge feature on main: feature had added C3 and C4 on top of C2, so main simply moves forward
to C4 — the history stays a straight line and no merge commit is added.
Three-way merge (merge commit)
If both branches have new commits since they diverged, Git combines the two tips and their common ancestor (a three-way comparison) and creates a merge commit with two parents, one on each branch. History still shows both lines of development.
--no-ff
git merge --no-ff feature forces a merge commit even when a fast-forward is possible, so the fact that
feature existed as a separate line of work stays visible in the log. Many teams require this on shared branches
so every feature leaves a single, easy-to-find merge commit (and a single point to git revert if the whole
feature must be undone).
--ff-only
git merge --ff-only feature does the opposite: it succeeds only if a fast-forward is possible and refuses
(fails without changing anything) if the branches have diverged. It is a safety check, useful in scripts and in
git pull --ff-only, to guarantee no surprise merge commit is created.
--squash
git merge --squash feature applies all of feature’s changes to the working tree and index as a single set of
staged changes, but does not commit and does not record `feature as a parent. A follow-up git commit
creates one ordinary commit with the combined diff. The result looks like a single-commit fast-forward, but the
branch relationship is not recorded, so git log --merges and git branch --merged will not show feature as
merged.
--squash is convenient for a feature branch with a messy, exploratory commit history that should not pollute
`main’s log, at the cost of losing the individual commits and the merge relationship. Squashing a pull request
on GitHub.com uses the same idea server-side; see
Pull Requests for the three merge methods GitHub offers
(merge commit, squash and merge, rebase and merge) and when to pick each one.
Resolving conflicts
A conflict happens when Git cannot automatically combine the two sides because the same region of a file (or, for some conflict types, the same file path) was changed differently on both branches. Git stops in the middle of the merge, leaves the non-conflicting files already merged, and marks the conflicting files with conflict markers so a human can decide the outcome:
-
<<<<<<< HEADmarks the start of your side (the branch that was checked out). -
=======separates the two sides. -
>>>>>>> featuremarks the end of the incoming side, labelled with the branch or commit being merged.
Worked example
$ git merge feature
Auto-merging src/config.yaml
CONFLICT (content): Merge conflict in src/config.yaml
Automatic merge failed; fix conflicts and then commit the result.
Opening src/config.yaml shows the conflicting region:
timeout:
<<<<<<< HEAD
seconds: 30
=======
seconds: 60
>>>>>>> feature
retries: 3
Resolve it by editing the file to the wanted final content and removing all three marker lines:
timeout:
seconds: 60
retries: 3
Then stage the resolved file and continue:
$ git add src/config.yaml
$ git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: src/config.yaml
$ git commit
git commit with no message argument opens the editor pre-filled with a Merge branch 'feature' message; saving
it completes the merge commit. git merge --continue is equivalent to git commit here; git rebase and git
cherry-pick have their own --continue flags that serve the same purpose for those operations.
If a conflict turns out to be more than a few lines, or the wrong branch was merged by mistake, abandon the merge entirely and return to the pre-merge state:
$ git merge --abort
git merge --abort only works while the merge is still unresolved (before the final commit); once the merge
commit exists, undoing it is a git revert of that commit instead.
Rebasing
git rebase <base> takes every commit unique to the current branch, sets the branch pointer back to <base>,
and replays each of those commits, one at a time, on top of the new base — producing new commits with new
hashes that contain the same changes. The result is a straight line of history with no merge commit, as if the
work had been written starting from the latest <base> all along.
Before rebasing: feature (C3, C5) diverged from main after C2, and main since gained C4.
After git rebase main run on feature: C3 and C5 were replayed as the new commits C3' and C5' on top
of C4.
Git replays commits one by one and can hit the same kind of conflict a merge would; when it does, it pauses on
that one commit, leaves the same <<<<<<</=======/>>>>>>> markers in the affected files, and waits:
$ git rebase main
Auto-merging src/config.yaml
CONFLICT (content): Merge conflict in src/config.yaml
error: could not apply a1b2c3d... Adjust request timeout
Resolve the conflict the same way as for a merge (edit the file, remove the markers), then git add the
resolved file and tell the rebase to move on to the next commit:
$ git add src/config.yaml
$ git rebase --continue
git rebase --continue replays the remaining commits, which may pause again on a later conflict. git rebase
--skip drops the commit currently being replayed instead of resolving it, and git rebase --abort cancels the
whole operation and restores the branch to exactly where it was before the rebase started.
Interactive rebase (git rebase -i), which lets each replayed commit be reordered, reworded, squashed or
dropped, is a separate, more advanced tool and is out of scope for this page; see the
git rebase reference for its -i mode.
The golden rule of rebasing
Never rebase commits that other people already have — that is, commits that have been pushed to a branch others may have fetched, pulled or built work on top of. Rebasing replaces old commits with new ones that have different hashes; anyone who already has the old commits now has a history that has diverged from the rewritten one, and reconciling the two normally means every commit they based on the old ones reappears as a duplicate, or they must force-fetch and re-base their own work. A local feature branch that has never been pushed, or one that only its author has ever pulled, is safe to rebase freely; a shared branch that teammates are also committing to is not.
Merge vs. rebase
| Aspect | Merge | Rebase |
|---|---|---|
History shape |
Preserves both lines of development; branch points and merge commits stay visible |
Produces a single straight line; the branch appears to have started from the new base |
New commits created |
One merge commit (unless fast-forward); original commits on both branches are untouched |
Every replayed commit is a new commit with a new hash |
Safe on shared branches |
Yes — never rewrites existing commits |
Only for commits nobody else has pulled — see the golden rule above |
Conflict resolution |
Once, in a single merge step (or once per file for the whole merge) |
Potentially once per replayed commit that conflicts |
Bisectability / blame |
Coarser — a single merge commit can hide which original commit introduced a change |
Finer — history reads as if written serially against the latest base |
Typical use |
Integrating a finished feature branch into |
Cleaning up or catching up a purely local/personal branch before it is shared |
On GitHub.com
GitHub’s own merge methods for a pull request (merge commit, squash and merge, rebase and merge) are covered in Pull Requests; this section only covers resolving conflicts and updating a pull request’s branch.
-
Open the pull request; if its base branch has changed in a way that conflicts with the pull request’s changes, GitHub shows This branch has conflicts that must be resolved above the merge box.
-
For conflicts simple enough to fix in the browser, click Resolve conflicts to open GitHub’s web-based conflict editor, which lists every conflicting file and shows each conflict framed by
<<<<<<<,=======and>>>>>>>markers, exactly as they would appear locally. -
Edit each conflicting section directly in the editor to the wanted final content and delete the marker lines, then click Mark as resolved for that file; repeat for every file listed on the left.
-
Once every file is marked resolved, click Commit merge to commit the resolution directly to the pull request’s branch. GitHub declines to offer this editor for conflicts it judges too complex (binary files, conflicts touching many files, or ones needing more context); those must be resolved locally on the command line or in an editor and pushed as an ordinary commit.
-
Separately from conflicts, when the base branch has moved ahead and the pull request should pick up those changes before it is merged, use Update branch on the pull request page: the plain button performs a traditional merge (recording a merge commit on the pull request’s branch), while its dropdown arrow offers Update with rebase, which replays the pull request’s commits onto the current base branch instead, keeping a linear history but rewriting the branch’s commit hashes — anyone with a local copy of that branch will need to fetch and hard-reset to match afterwards.
In IntelliJ IDEA
-
Open the branches popup (Git widget in the status bar, or Git → Branches…), select the branch to bring in, and choose Merge 'feature' into 'main' to perform a merge, or Rebase 'main' onto 'feature' to rebase the checked-out branch onto it instead.
-
If the operation completes without conflicts, IntelliJ reports success in the notification balloon; if conflicts are found, the Conflicts dialog lists every conflicting file with Accept Yours (keep the current branch’s version), Accept Theirs (keep the incoming version) and Merge (resolve that file manually) for each one.
-
Choosing Merge opens the three-pane merge window: the left pane is the current branch’s version, the right pane is the incoming version, and the centre pane is the editable result, seeded from the common base. Use the per-change Accept Left / Accept Right arrows (or the toolbar’s Apply Non-Conflicting Changes) to pull in each side, edit the centre pane directly for anything that needs hand-merging, then click Apply to save the resolved file.
-
Once every conflicted file has been resolved, IntelliJ finishes the merge commit (or, for a rebase, continues replaying the remaining commits automatically); a rebase that cannot go further shows the same three-pane window per conflicting commit, and can be abandoned by choosing Abort Rebase from the Git widget’s branches popup (or Git → Abort Rebase in the main menu).
In VS Code
-
Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and run Git: Merge… to merge another branch into the current one, or Git: Rebase Branch… to rebase the current branch onto another.
-
If the operation produces conflicts, the affected files show a ! badge under Merge Changes in the Source Control view, and opening one shows inline CodeLens actions above each conflicting block: Accept Current Change, Accept Incoming Change, Accept Both Changes, or Compare Changes.
-
For conflicts that need more context, use Resolve in Merge Editor (from the same CodeLens row, or the file’s context menu) to open the 3-way merge editor: Incoming and Current panes at the top show each side’s version with per-change checkboxes to include or exclude it, and the Result pane at the bottom is the editable merged file, which can also be edited directly.
-
After every conflict in a file is resolved, click Complete Merge in the merge editor (or stage the file from the Source Control view) to mark it resolved; once every conflicting file is staged, commit to finish a merge, or continue a rebase from the Source Control view’s … menu — Continue to move to the next commit, or Abort Rebase to cancel and restore the branch’s previous state.
Common mistakes and tips
-
Running
git mergeorgit rebasewith uncommitted local changes: commit, stash, or discard them first (see Checkout, Switch and Restore).git rebaserefuses to start with uncommitted changes (unless--autostashis given);git mergeaborts if those changes would be overwritten by the merge or if anything is already staged. -
Confusing
git rebase --continuewithgit merge --continue: the flag exists on both commands but must match the operation currently in progress;git statusalways names which one is underway. -
Leaving stray conflict markers in a file:
git commitdoes not detect a leftover<<<<<<<or>>>>>>>line as an error, so always search the diff before committing a conflict resolution. -
Force-pushing after a rebase on a branch that a teammate has already pulled: this is exactly the case the golden rule warns against; coordinate first, or avoid rebasing that branch at all.
-
Reaching for
--squashon a long-lived shared branch: it discards the branch relationship, so tools that walk merge history (git log --graph,git branch --merged) will no longer show it as merged.