gitrebaseworkflowversioning

Git rebase vs merge: which to use and the branching flows that work

When to merge and when to rebase, resolving conflicts without panic, interactive rebase for history cleanup and which branch flows fit your team.

August 27, 2026·9 min read

Merge or rebase is Git's eternal debate, and most discussions get lost mixing two different questions: how do I integrate others' changes? and what history do I want to tell? Separating them turns the dilemma into a ten-second technical decision. This article covers both plus the cleanup techniques that keep history readable.

What each one does, exactly

Starting from the typical scenario: main advanced while you worked on feature.

Merge creates a new commit with TWO parents, joining the timelines:

      A---B---C  feature
     /         \
D---E---F---G---M   main (M = merge commit)

History stays literal as it happened: fork and reunion visible. The graph tangles with every feature, but no commit ever changes.

Rebase rewrites your commits as if born on top of the current tip:

D---E---F---A'---B'---C'  feature (rewritten)
              ↑
main still at F

Git copies your commits one by one applying them onto the new base. Resulting history is linear — but they're NEW commits with distinct hashes. Originals stay orphaned until garbage collection.

The golden rule ending debates

Rebase what's private; merge what's public.

  • Commits only YOU have (local feature, personal unpushed branch): rebase freely. Rewriting your own draft.
  • Already-shared commits (pushed and used by others, or integrated): never rebase. Rewriting public history breaks everyone's work — their clones point at commits that no longer exist.

Applied to the standard flow: keep your feature updated via git fetch && git rebase origin/main (clean history), then when ready, integrate with merge (or fast-forward if not diverged) into main.

Fast-forward: merge without a commit

If main didn't advance since your fork, Git doesn't even need a merge commit: it just moves the pointer (--ff). The happy case — perfectly linear history rewriting nothing.

With --no-ff you ALWAYS generate the merge commit, preserving each feature's visual grouping (handy for reverting whole features with one revert). Project choice; both legitimate:

git merge --ff-only origin/main    # fails if fast-forward impossible: safe
git merge --no-ff feature          # always generates merge commit

Conflicts without panic

A conflict means: both sides touched the same lines and Git can't decide. The file gets marked:

<<<<<<< HEAD
current branch text
=======
incoming text
>>>>>>> feature

Calm protocol:

  1. git status lists conflicted files — only those.
  2. Edit each marker deciding the FINAL content (sometimes combining both, sometimes one).
  3. Markers <<<<<<</=======/>>>>>>> must NEVER survive your edit.
  4. git add file marks resolved; after all of them: continue (git rebase --continue or git merge --continue).

Two emergency exits: git merge --abort / git rebase --abort return everything pre-operation — no damage, no loss. Using them fearlessly is what kills the panic.

Pro trick: git config merge.conflictstyle diff3 adds a third block with the common ancestor, showing WHAT each side changed from the original — the difference between guessing and understanding a conflict.

Interactive rebase: history surgery

git rebase -i HEAD~5 opens the editor with your last 5 commits and per-line actions:

pick a1b2c3 feat: add form
pick d4e5f6 fix: typo
pick g7h8i9 fix: another typo
pick j0k1l2 wip

Swap pick for the verb and save:

  • squash/s: fuse into previous (the three fix/wips → one coherent commit).
  • reword/r: edit message only.
  • edit/e: stop there to modify content (add forgotten bits with git commit --amend).
  • drop/d: delete the commit.
  • fixup/f: squash discarding the absorbed commit's message.

Result: "feat: add form" + a single fused "fix" — history where every commit builds and makes sense. It's the difference between a navigable log and a landfill of "wip", "asdf" and "fix2-final-REAL".

Safety warnings: never interactive-rebase over shared commits; backup first with git branch backup-pre-rebase (deleting branches is free); if lost mid-way, git reflog shows where HEAD was — git reset --hard HEAD@{n} restores any recent point.

Branch flows by team size

Trunk-based (1 dev or mature teams with strong CI): branches live hours, not days. Everything integrates fast to main; feature flags hide incompleteness. Maximum operational simplicity, requires test discipline.

Lightweight feature branches (most teams' sweet spot): one branch per feature/task, days-long life, PR with review, merge to main, delete. What makes GitHub/GitLab comfortable and auditable.

GitFlow (main + develop + release + hotfix): born for versioned software with planned releases (packaged apps, on-premise). For web with continuous deploy it's dead ceremony — too many permanent branches for zero benefit.

Industry reality converges on trunk-based or light-feature-branches + strict CI.

Compare before deciding

Before a delicate merge, look at EXACTLY what it brings:

git diff main...feature        # what feature adds vs common ancestor
git log main..feature --oneline  # incoming commits

Our text comparison tool helps review pasted diffs outside the terminal (chat code reviews, teammate snippets), and for mastering individual commands the Git cheatsheet has the essentials on one page.

FAQ

Lost commits after a rebase? Almost surely not: git reflog stores every HEAD movement for months. Find the hash before the disaster and git branch recovered <hash>.

Should I force-push? Only --force-with-lease (fails if someone pushed meanwhile) and only on YOUR branches. Naked force on shared branches is how others' work gets deleted.

Squash on merge or normal merge? Squash (whole PR = 1 commit in main) gives clean main at the cost of revert granularity. Normal merge keeps everything. Teams with good commit hygiene: normal merge; teams full of "wip": squash without hesitation.


Compare text versions outside the terminal with our Diff Checker, free and right in your browser.

Try it without code

Diff Checker

Line-by-line text differences.

Open Diff Checker

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me