Git Workflows & Branching
Key Points
- Trunk-Based Development (TBD) is the default for high-performing teams (DORA). Branches live hours, not weeks.
- Feature flags > long-lived branches. Hide unfinished work in
main, don't isolate it in a branch. - CI is the integration point, not a "release branch". If you can't merge to trunk daily, fix CI first.
- GitFlow still earns its keep for versioned/regulated/shipped-to-customer products. Don't cargo-cult it onto SaaS.
- Master the porcelain AND the plumbing:
rebase -i,bisect,reflog,worktree,cherry-pickare senior table stakes. - Conventional Commits are not aesthetic — they drive changelogs, semver bumps, and release automation.
Concepts (deep dive)
The four mainstream workflows
| Workflow | Branches | Best for | Pain points |
|---|---|---|---|
| Trunk-Based | main + short-lived (<1 day) | SaaS, CD, high-velocity teams | Requires strong CI + flags |
| GitHub Flow | main + feature branches via PR | Mid-size teams, web products | Branch lifetime creep |
| GitFlow | main, develop, feature/*, release/*, hotfix/* | Versioned products, regulated shops | Merge hell, ceremony |
| Release Flow (MS) | main + release/x.y cut at ship | Long-lived products with multiple supported versions | Cherry-pick discipline |
✅ TBD wins for continuous delivery. GitHub Flow is "TBD with PRs" and is fine for most teams. GitFlow is for products where v1.4 and v1.5 both ship and both need patches.
Why TBD at staff/senior level
- Smaller diffs → faster review, fewer conflicts, lower defect rate.
- Real continuous integration: integrating once a sprint is not CI.
- Forces the painful conversations early (API breaks, schema migrations) instead of at merge time.
- Composable with feature flags + branch-by-abstraction for risky/long work.
When GitFlow still makes sense
- Multiple supported releases in the wild (v2.x and v3.x both patched).
- Regulatory release gating (signed builds, change advisory boards).
- Hotfix isolation matters more than integration speed.
- Mobile / desktop apps shipped through stores (not your servers).
If you're a SaaS team using GitFlow because someone read a 2010 blog post — that's cargo cult.
Branching anti-patterns
- ❌ The 3-month feature branch. Renames, refactors upstream, schema drift — merge becomes a re-implementation.
- ❌
develop+mainfor SaaS. Two branches that always converge add ceremony, not safety. - ❌ Per-environment branches (
dev,qa,staging,prod). Promote artifacts, not commits. - ❌ Squash-only with no commit hygiene. "Fix tests" ×40 in PR history is fine; in
mainhistory it's noise. - ❌ Reviewer-blocking long branches. A PR that grew past 800 LOC will not get a real review.
Senior-level git ops cheat sheet
# Interactive rebase + autosquash (clean up PR history)
git commit --fixup <sha>
git rebase -i --autosquash main
# Find the commit that broke something
git bisect start
git bisect bad
git bisect good v1.4.0
# git runs through the history; mark each as good/bad
# Recover from "I just deleted/reset something"
git reflog # every HEAD move, ~90 days
git reset --hard HEAD@{2}
# Two checkouts, no stash dance
git worktree add ../proj-hotfix hotfix/critical
# Big monorepo: clone only what you need
git clone --filter=blob:none --sparse <url>
git sparse-checkout set src/billing src/shared
# Move work between branches
git stash push -m "wip" --keep-index
git cherry-pick -x <sha> # -x records origin commit in message
The reflog is the safety net seniors quietly rely on. New engineers think git reset --hard deleted their work; you know it's recoverable for ~90 days.
Conflict resolution strategy
- Pull/rebase often — conflicts grow superlinearly with branch age.
- Resolve in the IDE with 3-way view, not by hand-editing
<<<<<<<markers. git rerere(reuse recorded resolution) for repeated conflicts during long rebases.- When in doubt, keep both, then delete. Lost code is worse than ugly diffs.
- Run tests after the resolution, not just after the merge button.
Commit message conventions
Conventional Commits format:
Types: feat, fix, docs, refactor, test, chore, perf, ci, build.
Why it's worth the discipline: - Auto-generated changelogs (semantic-release, release-please). - Auto semver bumps: feat → minor, fix → patch, BREAKING CHANGE → major. - Searchable history: git log --grep="^fix(auth)".
Rebase vs merge for PRs
| Strategy | Use when |
|---|---|
| Squash & merge | Default for feature PRs; keeps main linear and readable |
| Rebase & merge | When individual commits tell a story (refactor + behavior change separated) |
| Merge commit | Long-lived release branches, preserving topology |
Pick one team-wide. Mixed strategies make git log --graph unreadable.
Patterns
- Branch by abstraction — Intent: ship a large change incrementally on
mainby routing through an abstraction layer; flip the implementation when ready. Replaces long-lived branches. - Feature flag + dark launch — Intent: merge unfinished code; gate behavior by config or per-user flag. Decouples deploy from release.
- Expand / contract migration — Intent: schema or API changes via additive step → dual-write → cutover → cleanup. Each step ships independently to trunk.
- Stacked PRs — Intent: chain dependent small PRs (Graphite, Sapling,
git-spr) instead of one giant PR. Reviewer-friendly substitute for long branches. - Release train — Intent: cut
release/x.yfrommainon a schedule; cherry-pick fixes, never new features. Combines TBD onmainwith controlled release branches.
Pros & cons / trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Trunk-Based | Fast feedback, small diffs, real CI | Demands flags + strong tests |
| GitHub Flow | Simple, PR-driven, familiar | Branch creep without discipline |
| GitFlow | Clean release isolation, hotfix path | Heavyweight, merge hell |
| Release Flow | Multiple shipped versions supportable | Cherry-pick discipline overhead |
| Squash merge | Linear, readable history | Loses intermediate commits |
| Rebase merge | Preserves authored intent | Force-push hazards on shared branches |
When to use / when to avoid
✅ TBD when: SaaS, CD pipeline exists, team can write tests, feature flags available. ✅ GitFlow when: shipping versioned artifacts, multiple releases supported in production, regulated change control. ✅ Release Flow when: you ship to enterprises that pin versions and you patch each minor.
❌ Avoid TBD if CI is broken or flaky (fix CI first; don't paper over with branches). ❌ Avoid GitFlow for a SaaS where main deploys hourly — the ceremony buys nothing. ❌ Avoid per-env branches universally — promote artifacts, not commits.
Senior-level tips
- 💡 Branch lifetime is a leading indicator of team health. Track median branch age; rising trend = process smell.
- 💡 Teach
bisectandreflogto your team. Two skills that turn 4-hour debugging into 10 minutes. - 💡 PR template > prose discipline. A required checklist (tests, migration plan, flag, rollback) does more than nagging.
- 💡 Pair "rewrite history" sessions for juniors learning rebase. The fear of breaking things is the bigger blocker than the syntax.
- 💡 Forbid force-push on shared branches at the server level (
receive.denyNonFastForwards); allow it only on the author's PR branch. - 💡 Auto-delete merged branches in repo settings. The cruft compounds.
- 💡 Tags are immutable, branches are not. Sign release tags (
git tag -s) when supply chain matters. - 💡
git logis a debugging tool, not a vanity metric. Optimize history forgit blame's reader, who is future you at 2 AM.
Common pitfalls
- ⚠️ Long-lived feature branches that "just need one more week" — they never do.
- ⚠️ Rebasing a shared branch and force-pushing — strands collaborators' commits.
- ⚠️ Merging without rebasing/updating first — CI passed against stale
main. - ⚠️ Squash-merging a PR that contained an actual refactor — lost reviewable history.
- ⚠️ Using
git push --forceinstead of--force-with-lease— overwrites teammates' work. - ⚠️ Cherry-picking a hotfix to
mainand forgettingrelease/*(or vice versa) — silent regression next release. - ⚠️ Storing secrets in commits then "rebasing them out" — they're still in the reflog and on every clone.
- ⚠️ Adopting Conventional Commits without the tooling that consumes them — pure ceremony.
Further reading
- Paul Hammant — Trunk Based Development
- Jez Humble & David Farley — Continuous Delivery (the canonical case for TBD)
- Martin Fowler — Branching Patterns
- Vincent Driessen — A successful Git branching model (the original GitFlow; read his 2020 update too)
- Atlassian — Git Workflows guide
- GitHub — Understanding the GitHub flow
- Microsoft — Release Flow at Microsoft
- DORA / Accelerate (Forsgren, Humble, Kim) — branching as a delivery performance predictor
- Conventional Commits 1.0.0
- Scott Chacon — Pro Git, chapters 6–7 (rebasing, reset demystified)