Skip to main content

🔀 Merging, Rebasing, and Conflict Resolution

Branching splits work apart; sooner or later you must bring it back together. Git gives you two philosophies for that — merging preserves exactly what happened, while rebasing rewrites history into a clean straight line. This lesson teaches both, when to reach for each, and how to calmly resolve the conflicts that appear when two people edit the same lines.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Distinguish fast-forward, three-way, and squash merges and choose between them
  • Explain how rebasing replays commits onto a new base and creates a linear history
  • State and justify the golden rule of rebasing
  • Read the anatomy of a conflict and resolve one step by step
  • Use --abort, --continue, --ours/--theirs, and merge tools to handle conflicts safely

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Deliberately create a merge conflict and resolve it to keep both changes.

In This Lesson

Two Ways to Integrate

In the last lesson you spun off branches and did independent work. Integration is the reverse: folding one branch's changes into another. Git offers two fundamentally different tools for this.

💡 The one-sentence difference: Merging writes a new commit that ties two histories together and keeps the full record of what really happened. Rebasing lifts your commits off their old base and re-plays them on top of another branch, producing a tidy straight line — at the cost of rewriting history.

Neither is "better." They're tools with different trade-offs, and mature teams use both — often on the same feature. Let's take them one at a time.

Merging Fundamentals

Merging integrates the changes from one branch into another. Which kind of merge Git performs depends on how the branches relate.

Fast-forward merge

If the target branch has not moved since the feature branched off, there's no divergence to reconcile. Git simply slides the target pointer forward — no new commit needed. This is a fast-forward.

gitGraph commit id: "C1" commit id: "C2" branch feature checkout feature commit id: "C3" commit id: "C4" checkout main merge feature
git switch main
git merge feature   # main just moves forward to C4

Three-way merge

If both branches gained commits since they split, Git can't just slide a pointer. It performs a three-way merge — comparing both tips against their common ancestor — and records the result in a new merge commit with two parents.

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

Commit C5 above is the merge commit; it's the visible record that these two lines of work were joined here.

Squash merge

A squash merge takes all the feature's changes and lands them as a single new commit on the target, discarding the feature's individual commits. Great for collapsing a noisy "WIP / fix typo / oops" history into one clean entry.

git switch main
git merge --squash feature
git commit -m "Add user profile page"   # --squash does NOT auto-commit

Handy merge options

OptionEffect
--ff (default)Fast-forward when possible
--no-ffAlways create a merge commit, even if a fast-forward was possible — preserves the fact a feature existed
--ff-onlyAbort unless a fast-forward is possible — enforces linear history
--squashCollapse all changes into one commit you author separately

Rebasing Fundamentals

Where merging joins two histories with a new commit, rebasing moves a branch to a new base. Conceptually Git: finds the common ancestor, saves each of your commits as a patch, resets your branch to the tip of the target, then re-applies those patches one by one as brand-new commits.

Before and after rebasing feature onto main Before: main has commits C1 C2 C5 C6 and feature branches at C2 with C3 C4. After: feature's commits are re-created as C3-prime and C4-prime on top of C6. Before C1 C2 C5 C6 C3 C4 After rebase C1 C2 C5 C6 C3' C4'
Figure 1 — Rebasing feature onto main re-creates C3 and C4 as brand-new commits C3' and C4' on top of C6. Same changes, different hashes, straight line.
git switch feature
git rebase main   # replay feature's commits on top of main's tip

Rebasing shines for three jobs: keeping a long-running feature branch current with main, cleaning up messy local commits before you share them (with interactive rebase), and producing a linear history that some teams prefer.

⚠️ The Golden Rule of Rebasing

Never rebase commits that you've already pushed and others may have based work on. Rebasing replaces commits with new ones that carry the same changes but different hashes. If a teammate built on the originals, your rebase orphans their base — leading to duplicated commits and brutal conflicts. Rebase private, local history freely; treat shared history as immutable.

Interactive rebase

The most powerful form. It opens an editor listing your commits so you can reorder, combine, or rewrite them:

git rebase -i HEAD~5   # curate the last 5 commits
pick   36d1535 Add login form
squash 7bc563a Style login form
squash 08e4e17 Add form validation
reword 2bc4f3c Fix validation bug
drop   0bec652 Debug console.log

# p, pick   = keep the commit
# r, reword = keep, but edit the message
# e, edit   = stop to amend the commit
# s, squash = meld into previous commit
# f, fixup  = like squash, but discard this message
# d, drop   = remove the commit

Interactive rebase is a time machine for your commits: use it to turn a scratch-pad of experimental commits into a clean, reviewable story before opening a pull request.

Merge vs. Rebase

flowchart TD A[Integrate feature into main] --> B{Choose a method} B -->|Merge| C[New merge commit combines changes] B -->|Rebase| D[Replay feature commits onto main] C --> E[Preserves true history & shows where branches joined] D --> F[Linear history, as if work was sequential]
CharacteristicMergeRebase
History shapePreserves branches with merge commitsRewrites into a straight line
Commit identityOriginal commits kept exactlyNew commits, new hashes
TraceabilityShows when branches diverged and joinedLooks as if all work was sequential
Conflict handlingResolve once, during the mergeMay resolve per replayed commit
Safety on shared branchesSafeOnly on private/local branches

When to merge

  • Integrating a finished feature into a shared branch
  • When the branch is already pushed and others may depend on it
  • When you want an explicit record of when integration happened (GitFlow style)

When to rebase

  • Updating your private feature branch with the latest main
  • Cleaning up local commits before sharing them
  • Keeping a linear history (Trunk-Based style)

✅ A popular hybrid: rebase, then merge with --no-ff

git switch feature
git rebase main            # linear, up-to-date feature history
git switch main
git merge --no-ff feature  # one merge commit records the integration

You get a clean feature history and a visible marker of where the feature landed. Many "Squash and merge" buttons on GitHub combine the same idea.

Understanding Conflicts

A conflict happens when Git can't decide how to combine two changes automatically — almost always because the same lines of a file were edited differently on each side. Conflicts aren't errors or failures; they're Git asking a human to make a judgement call.

When a conflict occurs, Git rewrites the affected region of the file with markers showing both versions:

<<<<<<< HEAD
Users can log in with email
=======
Users can log in with username
>>>>>>> feature-branch
  • <<<<<<< HEAD — start of your current branch's version
  • ======= — divider between the two versions
  • >>>>>>> feature-branch — end, and the name of the incoming branch

git status during a conflict tells you exactly where you stand:

Terminal output

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   README.md

Resolving Conflicts

The process is always the same six steps:

  1. Run git status to see which files conflict.
  2. Open each conflicting file and find the marker blocks.
  3. Decide the correct result — keep one side, combine both, or write something new.
  4. Delete the <<<<<<<, =======, and >>>>>>> lines.
  5. git add <file> to mark it resolved.
  6. Finish: git commit for a merge, or git rebase --continue for a rebase.

Combining both changes

Often the right answer takes something from each side. Given the conflict from the previous section, a good resolution merges the intent:

Users can log in with email or username

Useful conflict commands

# Give up and go back to the pre-merge state
git merge --abort
git rebase --abort

# Take one whole side for a file, then mark resolved
git checkout --ours   path/to/file    # keep current branch's version
git checkout --theirs path/to/file    # keep the incoming version
git add path/to/file

# Launch a configured visual merge tool
git mergetool

💡 Careful: --ours and --theirs flip during a rebase

Because a rebase replays your commits onto the other branch, "ours" refers to the branch you're rebasing onto and "theirs" to your own commits — the opposite of a merge. When in doubt, read the marker labels rather than trusting the flag names.

Most editors help too. VS Code shows conflicts inline with one-click "Accept Current / Incoming / Both" buttons and a three-way merge editor, which is far easier than editing markers by hand for anything complex.

Preventing conflicts in the first place

  • Integrate frequently — the longer branches diverge, the worse conflicts get.
  • Keep changes small and focused so overlaps are rare.
  • Communicate when you're editing shared files.
  • Standardize formatting with tools like Prettier/ESLint so whitespace and style don't cause phantom conflicts.

Hands-on Exercise

🏋️ Create and Resolve a Conflict

Objective: Deliberately produce a merge conflict on a single line, then resolve it to keep both ideas.

Instructions:

  1. Create a repo with a README.md whose first line is Welcome.
  2. On branch-a, change line 1 to Welcome — installation guide and commit.
  3. Back on main, create branch-b, change line 1 to Welcome — usage guide and commit.
  4. Merge branch-a into main (clean), then merge branch-b — this conflicts.
  5. Resolve so line 1 reads Welcome — installation and usage guide, mark resolved, and commit.
💡 Hint

After the failing merge, run git status to confirm README.md is "both modified", open it, and edit the marker block into a single combined line before git add.

✅ Solution
mkdir conflict-demo && cd conflict-demo
git init -b main
echo "Welcome" > README.md
git add README.md && git commit -m "Add README"

git switch -c branch-a
echo "Welcome — installation guide" > README.md
git commit -am "Document installation"

git switch main
git switch -c branch-b
echo "Welcome — usage guide" > README.md
git commit -am "Document usage"

git switch main
git merge branch-a          # fast-forward, no conflict
git merge branch-b          # CONFLICT in README.md
# Edit README.md to the single line:
#   Welcome — installation and usage guide
git add README.md
git commit                  # completes the merge
git log --graph --oneline

🎯 Quick Quiz

Question 1: Git can move a branch pointer straight forward with no new commit when the branches haven't diverged. What is this called?

Question 2: Why does the golden rule say never to rebase shared, already-pushed commits?

Question 3: You're midway through a painful merge and want to bail out completely. Which command restores the pre-merge state?

Best Practices

✅ Do

  • Integrate frequently and keep feature branches short-lived so conflicts stay small.
  • Rebase private branches to tidy them before opening a pull request.
  • Test after resolving conflicts — a clean merge can still be logically wrong.
  • Write descriptive merge-commit messages summarizing what was integrated.
  • When updating a rebased branch upstream, push with --force-with-lease, never bare --force.

⚠️ Don't

  • Rebase shared history (the golden rule).
  • Blindly accept one side of a conflict without understanding why both changes were made.
  • Leave conflict markers (<<<<<<<) in committed code — always search for them before finishing.
  • Resolve a giant conflict alone when several developers' work is tangled — pull them in.

Summary & Quiz

🎉 Key Takeaways

  • Merging preserves history: fast-forward when there's no divergence, a three-way merge commit when both sides moved, or a squash to collapse many commits into one.
  • Rebasing replays your commits onto a new base for a linear history — powerful for cleanup, but it rewrites history.
  • The golden rule: never rebase commits others may have built on.
  • Conflicts are Git asking for a decision; resolve them with the six-step process and git add + commit/rebase --continue.
  • --abort, --ours/--theirs, and visual merge tools give you an escape hatch and shortcuts when things get hairy.

📚 Further Reading

🚀 What's Next?

You can now integrate branches and resolve conflicts. Next we turn Git's complete history into a tool you can search, navigate, and — when necessary — safely rewrite and recover from.

🎉 Nice work!

Conflicts no longer scare you — they're just a conversation Git needs you to have.