Git Workflows
Branching models are delivery constraints, not team preferences — and there is no universal winner, because they optimise for different release realities.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
Which branching model fits, given what you ship, to whom, and how often?
Branching models are argued about as if one were correct. They are not interchangeable and they are not equally good: each one makes a specific bet about how many versions must be supported at once, how quickly a fix must reach users, and whether release timing is your decision or someone else's.
Pick the workflow the team knows, or the one in the most popular blog post, and standardise on it across the organisation.
A model designed for versioned desktop software, applied to a continuously-deployed web service, produces long-lived branches, painful merges and a release cadence nobody wanted (Long-Lived Branches).
- A model designed for versioned desktop software, applied to a continuously-deployed web service, produces long-lived branches, painful merges and a release cadence nobody wanted (Long-Lived Branches).
- Trunk-based development applied without the testing, flags and review latency it requires produces a broken main branch and a team that stops trusting it (Trunk-Based Development).
- Standardising one model across an organisation that ships both a SaaS platform and an on-premise product forces at least one of them into a bad fit.
- Models are usually adopted for their diagram and not their preconditions, so the part that makes them work — fast CI, feature flags, or a genuine release manager — is the part that does not get adopted.
- The argument is had at the level of aesthetics ("clean history") rather than at the level of what breaks: how long two versions must coexist, and how fast a security fix can reach a customer on an old release.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- Every workflow is answering one question: how long may divergent work exist before it must be integrated? Everything else — naming, merge versus rebase, how many branches — is downstream of that.
- Divergence cost grows non-linearly with time. Textual conflicts grow roughly with the amount of overlapping change; semantic conflicts — where both sides merge cleanly and the combination is wrong — grow with the amount of *related* change, and no tool detects them (Long-Lived Branches).
- The compensating mechanism for short-lived branches is a strong automated check plus a way to hide unfinished work at runtime. Without both, integrating early means shipping unfinished work (Feature Flags: Deploy Is Not Release).
- The compensating mechanism for long-lived release branches is a backport discipline: every fix must be applied to main and to each supported release line, which is real, ongoing work that someone has to own.
- Release branches exist to decouple "what we are building" from "what a specific customer is running". That decoupling is unnecessary when you control every deployment and mandatory when you do not.
- Merge versus rebase changes what the history looks like, not what ships. It matters for bisecting and for reviewing, not for delivery risk — which is why it absorbs more argument than it deserves.
The models, and what each one is buying
No winner here. Each option is a coherent answer to a different release reality, and the cost column is what you sign up for by choosing it.
You are choosing how work integrates in a repository. What is the constraint you are actually solving for?
when You deploy the service yourself, continuously, and only one version is ever live.
cost Requires fast reliable CI, feature flags for unfinished work, and review turnaround measured in hours (Trunk-Based Development).
when The common default: same as above, but review happens on a branch rather than in a pair.
cost Review latency becomes the integration cadence; if review takes two days, your branches are two days long whatever you call the model.
when Customers run versions they choose to upgrade — on-premise software, mobile apps, SDKs, regulated release trains.
cost Backporting every fix to every supported line, forever, plus an end-of-life policy someone owns.
when Versioned products with scheduled releases and a stabilisation period before each one.
cost Several long-lived branches diverging simultaneously; a poor fit for deploying several times a day, and generally the wrong choice for a SaaS backend.
when Contributors are not trusted with write access — open source, or an internal repository with a wide contributor base.
cost Every contribution needs maintainer action, and CI on untrusted code needs its own permission model (CI Security).
When release branches are the right answer
Trunk-based advocacy sometimes implies that any long-lived branch is a mistake. That is wrong in a specific, identifiable set of cases: whenever you do not control when a user runs your new code.
The test is simple. If you can move every user to a new version by deciding to, you do not need release branches. If a user can be on version 4.2 for eighteen months and still expects security fixes, you do — and no amount of feature-flag discipline substitutes for that.
| Situation | Who decides the version in use | Release branches? | Why |
|---|---|---|---|
| SaaS backend you deploy | You | No | One version live; a fix ships forward in minutes |
| Mobile app in an app store | The user, plus review latency | Usually yes | Old versions keep calling your API for months (Backward Compatibility: The Real Rules) |
| On-premise or air-gapped install | The customer | Yes | Several versions in the field simultaneously, each needing fixes |
| Published library or SDK | Every consumer independently | Yes, per major line | Consumers upgrade on their own schedule (Semantic Versioning, and Where It Stops Applying) |
| Regulated release train | A change board, on a calendar | Yes | Release content is frozen and validated before a scheduled date |
| Internal tool used by one team | You | No | The cost of parallel maintenance buys nothing |
What all the models agree on
The interesting result of comparing them is how much is invariant. These properties appear in every workflow that works, and their absence predicts pain regardless of which diagram is on the wall.
- Main is releasable. Whatever else branches do, the mainline is a state you could ship. A model where main is routinely broken has no safe integration point.
- The deploying branch is protected. No direct pushes, no force pushes, no deletion (Protected Branches).
- Integration is frequent enough that conflicts stay small. The threshold differs; the direction never does.
- Every change is reviewed by someone with context, whatever the mechanism — proposal, pairing, or maintainer review (Review as a Gate).
- Automated checks run on the merge result, not only on the branch in isolation (Required Checks).
- Unfinished work is hidden at runtime, not by being kept out of main. Flags, or an unreferenced code path — anything except a branch that waits (Feature Flags: Deploy Is Not Release).
- Fixes reach every supported line. In trunk-based that is trivial; with release branches it is an explicit, owned process.
How to do it properly
Most important first.
- Start from the release reality: how many versions must be supported simultaneously, and who decides when a user upgrades. The workflow follows from the answer.
- For a service you deploy yourself, continuously: short-lived branches off main, integrated at least daily, with flags for anything unfinished (Trunk-Based Development).
- For software customers install and upgrade on their own schedule: main plus release branches, with an explicit backport policy and an owner for it.
- For open-source projects taking contributions from people without write access: forks and change proposals, because the trust boundary, not the integration cadence, is the constraint.
- Allow different repositories in the same organisation to use different models where their release realities genuinely differ. Consistency is worth less than fit here.
- Whatever the model, make main releasable and make the deploying branch protected. Those two hold across all of them (Protected Branches).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
Nothing inside Git contains a workflow mismatch; it appears as chronic integration pain and slow fixes rather than a single event. Deployment safety mechanisms are downstream and independent.
What can go wrong
- Cargo-culted workflow: the branch names from a diagram, none of the preconditions, and a team wondering why integration is painful.
- Release branches with no backport policy, so a fix lands on main and a customer on the previous release never receives it.
- Release branches accumulating without an end-of-life policy, until you are supporting five lines with the capacity for two.
- Trunk-based in name only: short branch names, integration once a week, and all the divergence cost of long-lived branches.
- A workflow enforced organisation-wide by policy, which is easy to audit and guarantees a bad fit somewhere.
- "Trunk-based development is the modern best practice." It is the right default for continuously-deployed services with strong testing. It is not a fit for software with supported release lines, and saying so is not nostalgia (Trunk-Based Development).
- "Git flow is obsolete." It is a poor fit for continuous deployment and a reasonable fit for versioned products with parallel supported releases — which is what it was designed for.
- "More branches means more control." Each branch is a divergence you have to reconcile later. Branches are debt with a diagram.
- "Rebase versus merge is an important delivery decision." It changes history readability. Delivery risk is decided by how long the branch lived, not by how it was joined.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- Median branch lifetime is measured, and it matches the model you claim to be running.
- For every supported release line you can name its end-of-life date and the last fix backported to it.
- Main is deployable right now — not "after a stabilisation pass".
- Merges that required manual conflict resolution are rare enough to be notable.
- Changing branching model is reversible but disruptive: it touches muscle memory, tooling and protection rules together. Migrate one repository at a time.
- Moving toward shorter-lived branches is nearly always safe to try incrementally — reduce the maximum branch age and see what breaks first, which is usually CI duration (CI Is a Feedback System).
- Moving toward release branches is harder to reverse, because once customers are on a release line you have committed to supporting it for as long as you told them you would.
- Automate the enforcement of whichever model you chose: protection rules, required checks, and merge queues where merge order matters (Required Checks).
- Automate backport mechanics — opening the equivalent change against each supported release line — but not the decision of what deserves a backport.
- Keep human: choosing the model, deciding when a release line ends, and deciding whether a risky fix goes to a stable line (Change Management).
- Short-lived branches reduce integration risk and demand fast, reliable CI plus flag discipline. If CI takes an hour and is flaky, the model fights the team every day.
- Release branches give customers stability and give you permanent parallel maintenance work, which scales with the number of supported lines.
- Fork-based contribution gives you a real trust boundary and slows every contribution down, including trusted ones.
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- ORG-SPECIFICThis depends on your release model, your team size and your compliance obligations. It flips whenever you do not control when a user upgrades: a continuously-deployed SaaS should default to trunk-based, and the same team shipping an on-premise appliance needs release branches for it, in the same organisation and often in the same week.
- SCALE-SPECIFICBelow roughly ten engineers on one deployable, workflow choice barely matters — divergence is small and everyone is in the same conversation. Above that, merge order and integration cadence start producing real incidents, and above roughly a hundred contributors on one repository, merge queues and ownership routing stop being optional (Required Checks).
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.
- — Testing & Reliability Engineering — the automated test suite is the precondition that decides which of these workflows is even available to you.