Most teams do not need Git Flow. Short-lived branches off main, squash merges, and branch protection cover almost everything, and the elaborate branching models exist for a release cadence that shipping continuously makes irrelevant.
Five practices below, in the order they pay off. None of them require the team to agree on anything philosophical.
1. GitHub Flow (For Most Teams)
One main branch. All work in short-lived feature branches. Merge via pull request.
git checkout -b feature/user-authentication
# ... work ...
git push origin feature/user-authentication
# Open a PR → review → merge → delete branch
Rules:
mainis always deployable- Branches live for days, not weeks
- Every merge goes through a PR with at least one reviewer
- Delete branches after merge
[!TIP] Enforce "Require branches to be up to date before merging" in GitHub branch protection settings. This prevents the class of bugs where a feature branch doesn't have recent changes from
main.
2. Conventional Commits
Structure your commit messages. Unlocks automated changelogs and semantic versioning.
feat: add QR code WiFi mode support
fix: correct JSON parser crash on empty arrays
docs: update README with environment variables
chore: upgrade next.js to 16.0.10
perf: lazy-load tool components on route change
refactor: extract heading parser to server utility
Format: type(optional-scope): description
Types: feat, fix, docs, style, refactor, perf, test, chore, ci
Why it matters: tools like semantic-release and standard-version read commit messages and automatically bump version numbers and generate CHANGELOG.md entries.
npm install -g commitizen
# Then use 'git cz' instead of 'git commit' for a guided prompt
3. Squash Merging for Clean History
Instead of merging every commit from a feature branch, squash them into one:
# In GitHub: "Squash and merge" button
# On CLI:
git checkout main
git merge --squash feature/my-feature
git commit -m "feat: add email signature templates"
Before squash:
* wip
* trying again
* fix typo
* oops
* working now
After squash:
* feat: add email signature templates
[!NOTE] Squash merging means you lose granular commit history on the feature branch. Keep branches short so this isn't a problem; if a branch has 40 commits, something went wrong earlier.
4. Interactive Rebase Before PR
Clean up your commits before asking for review:
git rebase -i HEAD~5 # Interactively edit last 5 commits
In the editor:
pick a1b2c3 feat: add email template base
squash d4e5f6 fix typo in template
squash g7h8i9 wip
pick j0k1l2 feat: add template preview
squash merges that commit into the previous one. The result: a clean, logical history that's easy to review and bisect.
5. Branch Protection Rules
Set these in GitHub → Settings → Branches → Branch protection rules for main:
✅ Require a pull request before merging
✅ Require approvals: 1
✅ Dismiss stale pull request approvals when new commits are pushed
✅ Require status checks to pass before merging
→ Your CI pipeline: lint, typecheck, test
✅ Require branches to be up to date before merging
✅ Restrict who can push to matching branches
✅ Do not allow bypassing the above settings
This prevents accidental direct pushes to main and ensures every change is reviewed and CI-green before landing.
Why not Git Flow
Git Flow gives you main, develop, feature/*, release/* and hotfix/*. It was designed in 2010 for software with versioned releases shipped to users who install them, and for that it works well.
It fits continuous deployment badly. develop becomes a second integration branch that drifts from main, every change merges twice, and release branches spend days accumulating conflicts. The overhead buys you the ability to stage a release, which a team deploying on merge does not need.
Use Git Flow if you ship versioned artefacts: a mobile app going through review, a library with supported major versions, on-premise software. Use GitHub Flow if deploying is a merge.
| GitHub Flow | Git Flow | |
|---|---|---|
| Long-lived branches | main | main and develop |
| Merges per change | one | two or more |
| Suits | continuous deployment | versioned releases |
| Release staging | feature flags | release branches |
| Overhead | almost none | significant |
The middle path most teams actually land on: GitHub Flow, plus feature flags for anything that needs to merge before it is ready to be seen. That gets you the ability to stage a release without a second branch to maintain.
Trunk-based development, and the branch lifetime that matters
The most valuable variable is not the branching model, it is how long a branch lives. A branch open for two hours conflicts with nothing. A branch open for two weeks conflicts with everything, and the conflict resolution is where bugs get introduced.
Trunk-based development takes this to its conclusion: branches measured in hours, merged behind a flag if incomplete. The practices in this post support that, and the flow chosen matters far less than keeping branches short.
If your team argues about branching models, the productive question is usually not which model, it is why changes take a fortnight to merge.
The recovery commands worth knowing before you need them
Git makes almost everything reversible, and knowing that changes how confidently people work. These four cover the situations that cause panic.
You committed to the wrong branch. Move the commit without losing it:
git reset HEAD~1 --soft # undo the commit, keep the changes staged
git stash
git switch correct-branch
git stash pop
git commit -m "feat: the thing"
You force-pushed over someone's work, or lost a branch in a rebase. The reflog records where every branch pointer has been, for ninety days by default:
git reflog # find the sha the branch used to point at
git reset --hard abc1234
Almost nothing committed is genuinely lost. The exception is uncommitted work destroyed by git checkout . or git reset --hard, which the reflog cannot help with, because it was never committed.
You need one commit from another branch, not the whole thing:
git cherry-pick abc1234
You want to undo a commit that is already pushed. Do not rewrite shared history, add an inverse commit instead:
git revert abc1234
revert is the safe one on any branch other people have pulled. reset is for history that is still yours alone.
Where these practices actually conflict
Worth being honest that the five above are not independent, and two combinations bite.
Squash merging destroys the individual commits, which makes the conventional-commit messages on your feature branch irrelevant. Only the squash message survives, so that is the one that has to follow the convention. Some teams write careful atomic commits and then squash them all away, which is wasted effort.
Interactive rebase after pushing means a force push. Use --force-with-lease rather than --force, because it refuses when someone else has pushed to the branch since you last fetched:
git push --force-with-lease
Plain --force overwrites their work without asking. The longer flag is worth the typing, and worth aliasing.
Putting It Together
A workflow that scales:
# Start work
git checkout main && git pull
git checkout -b feat/new-feature
# Work in small logical commits
git add -p # Add hunks interactively, not entire files
git commit -m "feat: initial structure for new feature"
# Before opening PR
git rebase -i HEAD~4 # Clean up commits
git push origin feat/new-feature
# After review
# Use "Squash and merge" on GitHub
# Delete the branch
The goal is a main branch history that reads like a table of contents: each entry is a meaningful unit of work, described in plain English, that any team member can understand.
None of this works if reviews sit for days. The single highest-leverage change most teams can make is not a branching model, it is agreeing that reviewing an open pull request comes before starting new work.
Tools in this post
Related Tool
Focus Pomodoro Timer
Boost your productivity with a visual Pomodoro timer and relaxing ambient soundscapes. Keep focused and track work cycles.
Try it freeWritten by
Jamith NimanthaSoftware developer. Builds the DebuggerMe tools and writes about the things he runs into shipping them.
Related Articles
All articles →Docker for Developers: From Zero to Production-Ready in One Guide
Docker is non-negotiable in modern development. This guide takes you from installing Docker to running a full multi-service production stack with zero fluff and working code at every step.
One Week in the AI Money Machine: $25B in Bonds, a Record IPO, and a Moratorium
Amazon raised $25 billion in bonds, SK Hynix pulled off the largest foreign US listing ever, Meta committed to doubling compute, and New York hit pause. All in the same two weeks.
GPT-5.6: What Sol, Terra, and Luna Actually Mean for Developers
OpenAI shipped GPT-5.6 as a three-tier family: Sol, Terra, and Luna. Here's the pricing, the new caching rules, and which tier your workload actually needs.