Skip to content
Knowledge Hub

Branching strategy, merge/rebase/squash and safe force pushes Intermediate

Estimated reading time: 5 min read

This page answers: which branching model a team should use, whether to integrate PRs by merge, rebase or squash, and how to force push without losing teammates’ commits.

Trunk-based (short branches) GitHub flow GitFlow
Long-lived branches Only main (trunk) Only main main + develop, plus release/*, hotfix/*
Feature branches Very short-lived, merged into trunk at least daily Branch → PR → review → merge → delete branch feature/* from develop, may live long
Unfinished features Feature flags, branch by abstraction Feature flags or keep the PR open Kept on the feature branch
Release From trunk, or cut a release branch “just in time” Deploy from main Via release/*, merged back to main and develop
Fits Web services, continuous delivery Small to mid-sized teams, continuous deployment Software with several versions in parallel (installed apps, SDKs, firmware)

The basis for the choice:

  • trunkbaseddevelopment.com: developers collaborate on one “trunk” branch and avoid long-lived branches. Feature branches must be short, only for review and CI; each developer integrates into trunk at least every 24 hours.
  • The author of GitFlow, Vincent Driessen, added a “note of reflection” (March 5, 2020): web apps are usually continuously delivered and do not support multiple versions. For such teams he recommends a simpler workflow like GitHub flow.

Default for backend services: trunk-based / GitHub flow, small PRs, feature flags for long work. Only use GitFlow when you really have to patch several versions running out in the wild.

Branch feature has 3 commits (A, B, C) and branched off main at M1; meanwhile main gained M2.

Before integrating:
main: M1 ── M2
\
feature: A ── B ── C
1) Merge commit (git merge --no-ff / "Create a merge commit")
main: M1 ── M2 ─────────── X X has 2 parents: M2 and C
\ /
A ──── B ──── C ─ keeps A, B, C and their old hashes
2) Rebase then fast-forward (git rebase main / "Rebase and merge")
main: M1 ── M2 ── A' ── B' ── C' A', B', C' are NEW commits (new hashes)
3) Squash merge ("Squash and merge")
main: M1 ── M2 ── S S = A+B+C combined into one commit
Merge commit Rebase + fast-forward Squash merge
main history Branched, complete Linear, keeps each commit Linear, 1 commit per PR
Original commit hashes Kept Changed Lost (originals only remain on the branch)
Reverting a whole PR git revert -m 1 X Revert each commit git revert S
Fits when The PR’s commits are all deliberate and worth keeping Commits are already cleaned up, atomic Small PRs whose intermediate commits are “wip”

The GitHub docs note: squash merge loses the original SHAs. If you keep opening new PRs from the same branch after squashing, old commits may reappear and cause repeated conflicts. After a squash merge: delete the branch and create a new one from main.

Pro Git: “Do not rebase commits that exist outside your repository and that people may have based work on.”

Rebase does not “move” commits. It creates new commits (A') and abandons the old ones (A). Anyone who built on A has to merge again, and history ends up with two copies of A (see the demo in Common mistakes).

Branch Rebase onto origin/main?
Your own branch, even if pushed to open a PR Yes — as long as nobody has based work on it
main, develop, branches several people push to Never — use git merge origin/main

Rebasing a pushed branch requires a force push. The difference:

Command Behavior
git push --force Disables all checks; overwrites the remote even if it has commits you have not seen. Docs: “It can cause the remote repository to lose commits”
git push --force-with-lease Only overwrites if the remote ref still equals your remote-tracking ref (origin/feature)
git push --force-with-lease --force-if-includes Adds a check: the tip of the remote-tracking ref must be reachable from an entry in the local branch’s reflog — guards against an IDE/cron silently running git fetch and defeating the lease

Loading diagram…

An alias worth having in ~/.gitconfig:

Terminal window
git config --global alias.pushf "push --force-with-lease --force-if-includes"

Fixing an old commit in a PR: --fixup + --autosquash

Section titled “Fixing an old commit in a PR: --fixup + --autosquash”

A reviewer points out a mistake in the refactor commit (not the last commit). Do not create a stray “fix review” commit.

Terminal window
git add src/order/pricing.ts
git commit --fixup=3f2a9c1 # creates commit "fixup! refactor(order): extract pricing..."
git rebase -i --autosquash origin/main
# the todo list automatically places "fixup 7b1e..." right after 3f2a9c1 — save and exit
git push --force-with-lease

Per the docs:

  • --autosquash recognizes messages starting with fixup! , squash! , amend! and moves that commit right after the target commit.
  • --fixup=amend:<commit> also replaces the message; --fixup=reword:<commit> only changes the message.
  • Enable it by default: git config --global rebase.autoSquash true.

Tip: if you want the reviewer to see the fix on its own, push the fixup! commit first, wait for approval, then autosquash.

Next: Recovery.