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.

gitGraph commit id: "C1" commit id: "C2" commit id: "C3" commit id: "C4" tag: "main (moved forward)"

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.

gitGraph commit id: "C1" commit id: "C2" branch feature checkout feature commit id: "C3" checkout main commit id: "C4" checkout feature commit id: "C5" checkout main merge feature id: "merge commit: two parents"

--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).

gitGraph commit id: "C1" commit id: "C2" branch feature checkout feature commit id: "C3" checkout main merge feature id: "--no-ff: merge commit even though fast-forward was possible"

--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.

gitGraph commit id: "C1" commit id: "C2" branch feature checkout feature commit id: "C3" commit id: "C4" checkout main commit id: "squash commit: C3+C4 as one, no link to feature"

--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:

  • <<<<<<< HEAD marks the start of your side (the branch that was checked out).

  • ======= separates the two sides.

  • >>>>>>> feature marks 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.

gitGraph commit id: "C1" commit id: "C2" branch feature checkout feature commit id: "C3" checkout main commit id: "C4" checkout feature commit id: "C5"

Before rebasing: feature (C3, C5) diverged from main after C2, and main since gained C4.

gitGraph commit id: "C1" commit id: "C2" commit id: "C4" branch feature checkout feature commit id: "C3'" commit id: "C5'"

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 main, or on any branch others also use

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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 merge or git rebase with uncommitted local changes: commit, stash, or discard them first (see Checkout, Switch and Restore). git rebase refuses to start with uncommitted changes (unless --autostash is given); git merge aborts if those changes would be overwritten by the merge or if anything is already staged.

  • Confusing git rebase --continue with git merge --continue: the flag exists on both commands but must match the operation currently in progress; git status always names which one is underway.

  • Leaving stray conflict markers in a file: git commit does 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 --squash on 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.