A branching strategy is a team's agreement about where work happens, how long it may stay apart from everyone else's work, and how it reaches users. Git itself does not care: a branch costs forty-one bytes. What costs real money is the time between writing a change and integrating it with everyone else's, because every hour of separation is an hour in which conflicts, broken assumptions and untested combinations pile up.
This article explains the common strategies from first principles rather than as rival brands. It starts with what Git actually stores, names the forces that should drive the choice, walks through trunk-based development, GitHub flow, Gitflow and release branches, compares merge, squash and rebase, then works through a real migration with commands. It ends with guardrails, failure modes and a checklist you can apply this week.
What a branch actually is
In Git a branch is a file containing a commit hash. Commits form a directed acyclic graph in which every commit points at its parents; a branch name is a movable label on one node, and HEAD says which label you are working on. Creating a branch copies nothing. Committing on it moves the label forward. Merging creates a commit with two parents, or simply slides the label forward when no divergence exists, which Git calls a fast-forward.
Two consequences matter for strategy. First, branches are cheap to create and delete, so the cost of a strategy is never the number of branches but how long they live. Second, the amount of work in a merge depends on how far the two sides have moved since their merge base, the most recent common ancestor. You can see that distance directly:
git merge-base main feature/search-ranking # the common ancestor
git rev-list --count main..feature/search-ranking # commits only on the feature
git rev-list --count feature/search-ranking..main # commits it has not seen yet
git diff --stat $(git merge-base main HEAD) HEAD # what this branch changesIf the third number is in the hundreds, the branch is integrating with a codebase that no longer exists. That number, not the name of the workflow, is the thing to watch.
The forces that decide a strategy
Every strategy is an answer to a few questions. Teams that pick a workflow by fashion usually discover they answered them wrongly.
- How often do you release? A web service deployed many times a day needs one always-releasable line. An app that ships to a store every few weeks, or firmware that ships quarterly, needs a way to stabilise a release while new work continues.
- How many versions do you support at once? If customers run 3.x and 4.x and both get security fixes, you need maintenance lines. If everyone runs the latest deploy, you do not.
- How fast and trustworthy is CI? Frequent integration only works when the build and tests on the main line run in minutes and fail for real reasons. Slow or flaky CI pushes teams towards batching, which pushes them towards long branches.
- Can unfinished work be hidden? Feature flags, branch by abstraction and dark launches let incomplete code live on the main line safely. Without them, the only place to hide unfinished work is a branch.
- Who must approve what? Regulated changes may need review records and release sign-off, which shapes where gates sit, not necessarily how many branches exist.
Trunk-based development
In trunk-based development everybody integrates into one branch, usually main, at least daily. Small teams may commit directly; most teams use short-lived branches that exist for hours or a day or two and merge through a pull request with required checks. The main line is kept releasable at all times, and releases are either deployed straight from it or cut as tags or short release branches.
The discipline it demands is in the code, not the repository. Large changes are split into small, independently safe steps. Work that is not ready for users ships dark behind a flag, which is why feature flags and trunk-based development are usually adopted together. Interfaces are changed with expand-and-contract: add the new path, migrate callers, remove the old path, each as its own merge.
The payoff is that conflicts are small because nobody is far from main, CI tests the combination that will actually ship, and the path from commit to production is one straight line that a CI/CD pipeline can automate end to end. The cost is that a broken main blocks everyone, so the team must invest in fast pre-merge checks, a merge queue where traffic is high, and a habit of reverting first and debugging second.
GitHub flow
GitHub flow is trunk-based development described in pull-request terms: branch from main, commit, open a pull request, discuss and review, deploy or test from the branch if needed, merge, and delete the branch. There is one permanent branch. Everything else is a topic branch whose whole life is one reviewable change.
It fits teams that deploy continuously and support one version. Its weak spot: if pull requests grow for weeks, GitHub flow quietly becomes long-lived feature branching. A review rule of thumb such as "small enough to review in one sitting" does more for integration speed than any naming convention.
Gitflow and why it is less common now
Gitflow, described by Vincent Driessen in his 2010 post "A successful Git branching model", keeps two permanent branches. main holds only released code, one commit per release, tagged. develop collects finished features. Feature branches start from develop and merge back with --no-ff so the feature stays visible as a unit. When develop is ready, a release/* branch is cut for stabilisation; only fixes go there, and when it ships it is merged into both main and develop. Urgent production fixes start from main as hotfix/* and are also merged into both.
The model was designed for software with explicit, versioned releases, and for that it still works: it separates "what is in production" from "what is next" and gives stabilisation its own space. Its costs are the double merges, the extra permanent line where integration problems hide, and feature branches that naturally live for weeks. In a note added to the post in 2020, Driessen himself recommended a simpler workflow such as GitHub flow for software that is delivered continuously, and kept Gitflow for explicitly versioned software or several versions in the wild.
Release branches without Gitflow
You do not need Gitflow to stabilise releases or maintain old versions. The common pattern is release branches cut from trunk: development continues on main; at release time you cut release/2.4 from a known-good commit; fixes land on main first and are cherry-picked onto the release branch; the branch is tagged for each patch release and abandoned when the version reaches end of life.
# cut the release from a green commit on main
git switch -c release/2.4 3f9c2ab
git push -u origin release/2.4
git tag -a v2.4.0 -m "2.4.0" && git push origin v2.4.0
# a bug is fixed on main first, then backported with provenance
git switch release/2.4
git cherry-pick -x 8d41e07 # -x records "(cherry picked from commit 8d41e07...)"
git tag -a v2.4.1 -m "2.4.1" && git push origin release/2.4 v2.4.1Fix on main first, then backport. The opposite order, fixing on the release branch and promising to merge it back later, is the most common way a fixed bug comes back in the next release. The -x trailer makes backports auditable: git log --grep 'cherry picked from' on the release branch lists them. For a fixed cadence of such branches, see release trains.
Merge, squash or rebase
Separate from where branches live is how their commits arrive on main. All three options produce the same final tree; they differ in the history they leave.
| Method | History on main | Good for | Watch out for |
|---|---|---|---|
Merge commit (--no-ff) | Branch commits plus a merge commit with two parents | Keeping a feature visible as a unit; Gitflow | Noisy graph; work-in-progress commits reach main |
| Squash merge | One new commit per pull request | Small PRs, readable linear history, easy revert | Loses internal commits; branch must be deleted, not reused |
| Rebase and fast-forward | Branch commits replayed on top, linear | Curated commit series where each commit builds | Rewrites hashes; never rebase a branch others have pulled |
For trunk-based teams with small pull requests, squash merging is the simplest default: one commit per reviewed change, and git revert undoes exactly one change. Teams that write careful commit series prefer rebase-and-merge. Whatever you choose, enforce it in repository settings rather than by convention, so history stays consistent.
Rebasing your own branch onto fresh main before review is a different matter and is usually good practice: git fetch origin && git rebase origin/main keeps the diff honest. Push the result with git push --force-with-lease, which refuses to overwrite commits on the remote that you have not seen, rather than a bare --force.
Worked example: moving a team off Gitflow
A team of nine runs Gitflow for a web service that deploys weekly. Symptoms: feature branches average eleven days, the Thursday merge into develop routinely breaks the build, the release branch needs two days of fixes, and hotfixes sometimes never reach develop. They want to deploy daily.
Step 1: make main the only integration line. Merge develop into main one last time, protect main, and stop creating feature branches from develop. Keep develop read-only for two weeks so nothing is lost, then delete it.
git switch main && git pull
git merge --no-ff origin/develop -m "Retire develop: main is now the integration branch"
git push origin main
# later, once nothing references it
git push origin --delete developStep 2: shrink the work, not just the branches. Open features are split into flagged slices. The search-ranking feature, eleven days old, becomes three pull requests: a new scoring interface with the old implementation behind it, the new implementation behind a flag defaulting to off, and the flag flip, which is a configuration change rather than a merge.
Step 3: add guardrails. Branch protection on main requires one review and the CI checks; a merge queue tests each pull request against the latest main plus anything ahead of it; squash merge is the only allowed method; branches are deleted automatically on merge.
Step 4: release from main. Each green commit on main is deployable; the pipeline tags deploys. A release branch is cut only if a deploy must be patched without taking newer changes, and fixes reach it with cherry-pick -x.
Step 5: measure. The team tracks branch age at merge, time from first commit to production, how often main is red and for how long, and change failure rate. A month in, the sign of success is branch age measured in hours and a red main reverted within a session.
Guardrails that make any strategy work
- Protected branches. Block direct pushes and force-pushes to main and release branches; require status checks and review.
- Merge queue. With many merges a day, two pull requests can each pass against an old main and break it together. A queue tests them in order against the real future state.
- Fast, reliable checks. Keep the required pre-merge suite to minutes; move slow suites to post-merge with automatic revert or alerting. Quarantine flaky tests rather than retrying them silently.
- Code ownership. A CODEOWNERS file routes reviews to the right people without a separate integration branch per team.
- Flags with expiry. Every flag gets an owner and a removal date, or trunk fills with dead branches of a different kind. The feature flag guide covers lifecycle.
- Naming and cleanup. Prefix branches by type and owner, delete on merge, and run a weekly report of branches older than a few days.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Merge day breaks the build | Long-lived branches integrate rarely | Daily integration; flags for unfinished work |
| Fixed bug reappears next release | Fix made on a release branch, never merged back | Fix on main first, cherry-pick -x to the release |
| Main is red for hours | Slow or flaky checks; no revert habit | Merge queue, quarantine flakes, revert first |
| Teammate's commits vanish | Force-push over a shared branch | Protect branches; use --force-with-lease on own branches only |
| Environment branches drift | Branches named dev, staging, prod promoted by merge | One artifact promoted through environments by the pipeline |
| Flag graveyard | Flags never removed after launch | Owner and expiry per flag; removal as part of done |
| Unreviewable PRs | Branch scope set by feature, not by change | Split into safe steps; expand and contract |
The environment-branch anti-pattern deserves emphasis. Promoting code by merging dev into staging into prod means each environment runs a build that was never tested anywhere else. Build once from main and promote the same artifact; environments differ in configuration, not in source.
Choosing
| Situation | Recommended | Why |
|---|---|---|
| Web service, continuous deploy, one version | Trunk-based or GitHub flow | Shortest path from commit to production |
| Mobile or desktop app with store releases | Trunk plus short release branches | Stabilise each release while main moves on |
| Several supported major versions | Trunk plus long-lived maintenance branches | Backports with cherry-pick -x |
| Explicitly versioned product, infrequent releases, mature team on Gitflow | Gitflow is acceptable | Its structure matches the release process |
| Open source with outside contributors | Fork plus pull request into main | Contributors cannot push; maintainers integrate |
Branching for data and models follows the same logic with different storage; data versioning shows how branches over an object store behave.
What to do next
- Measure your current median branch age at merge and the commits-behind count for open branches; that is your real strategy, whatever the wiki says.
- Write down release cadence and number of supported versions, then pick from the choosing table.
- Protect main: required review, required checks, no force-push, one allowed merge method, auto-delete merged branches.
- Get the required pre-merge checks under ten minutes and quarantine flaky tests.
- Introduce feature flags with owners and expiry dates before shortening branches, so unfinished work has somewhere safe to live.
- Adopt the rule that fixes land on main first and are backported with cherry-pick -x.
- Review the numbers after a month and shorten again.