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.
Choosing a branching strategy
Section titled “Choosing a branching strategy”| 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.
Merge, rebase or squash
Section titled “Merge, rebase or squash”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.
The golden rule of rebasing
Section titled “The golden rule of rebasing”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 |
--force-with-lease instead of --force
Section titled “--force-with-lease instead of --force”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:
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.
git add src/order/pricing.tsgit 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 exitgit push --force-with-leasePer the docs:
--autosquashrecognizes messages starting withfixup!,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.