Software Development

Git Branching Strategies

A branch is a movable pointer to a commit. That’s it. That’s the whole feature.

Everyone expects something heavier - a copy of the project, a folder somewhere, some kind of storage cost. Nope. refs/heads/main is a text file containing 40 characters. Creating a branch writes a new 40-character file. Deleting one removes it. This is why branching in Git is instant while the same operation in older systems (looking at you, SVN) was something you planned ahead for.

[!NOTE]

Branching is cheap, merging is where the work happens. The strategies on this page are all basically answers to one question: “how do we keep merges boring?”

See Git for the object model underneath all of this.

The mechanics first

git switch -c feature/login     # create branch + switch to it
git switch main                 # switch back
git branch                      # list local branches
git branch -d feature/login     # delete (safe, refuses if unmerged)
git push origin feature/login   # publish it to the remote

git switch and git restore replaced the overloaded git checkout back in Git 2.23 (2019). checkout still works and half the internet still uses it, but switch says what it does - worth retraining your fingers.

HEAD points at the branch you’re currently on. When you commit, Git moves that branch pointer forward one commit. Nothing else in the repository changes.

graph LR
  C0[commit A] --> C1[commit B]
  C1 --> C2[commit C]
  C1 --> C3[commit D]
  main[main] -.-> C2
  feat["feature/login"] -.-> C3
  HEAD[HEAD] -.-> feat

Two branches, four commits, zero copies of your source tree.

History Lesson

Branching models didn’t come from Git itself - Git ships the mechanism and stays deliberately opinion-free. The opinions came from the platforms and the people using them.

Year What happened
2005 Git is released. Branches are cheap for the first time, and nobody is quite sure what to do with that yet.
2008 GitHub launches, and makes the Pull Request the unit of collaboration. Branch-per-change becomes the norm.
2010 Vincent Driessen publishes “A successful Git branching model” - the post the world renamed Git Flow.
2011 GitLab launches (self-hostable, later the “everything DevOps” platform). Merge Requests are the same idea under a different name.
2011 Scott Chacon describes GitHub Flow - essentially “Git Flow, but delete most of it”.
2014 GitLab Flow adds environment branches for teams that can’t ship to production on every merge.
2018 Microsoft publishes Release Flow, the model behind Azure DevOps - trunk plus release branches, developed at a genuinely large scale.
2020 Driessen adds a note to his own Git Flow post saying it’s probably not what you want for continuous delivery. Respect for that one 🙇
2020 Default branch naming moves from master to main across GitHub, GitLab and Git itself (init.defaultBranch).

Worth knowing: GitHub and GitLab are Git hosting platforms, not Git. They add the social layer - pull/merge requests, reviews, CI, permissions, protected branches. Every flow below is really a combination of Git branches plus platform rules enforcing them. More at Pull Requests / Merge Requests.


Trunk-Based Development

One long-lived branch (main, the “trunk”). Branches exist, but they live for hours, not weeks. Everything merges back fast, behind feature flags if it isn’t finished.

gitGraph
   commit id: "feat: search"
   branch short-lived
   commit id: "fix: pagination"
   checkout main
   merge short-lived tag: "deploy"
   commit id: "feat: filters" tag: "deploy"
   commit id: "chore: bump deps" tag: "deploy"

Good: simplest possible history, merge conflicts barely happen, every commit is a release candidate. Bad: demands real test coverage and a working CI pipeline. Without those it’s just pushing to main and hoping.

Use it when you genuinely do continuous delivery. It’s the model with the best evidence behind it (the DORA research keeps pointing this direction) - and the one most teams aren’t honest enough about their test suite to pull off.


GitHub Flow

The lightweight one. main is always deployable. Any change gets a branch, a Pull Request, review and CI, then merges back and ships.

gitGraph
   commit id: "main is deployable"
   branch feature/login
   commit id: "feat: login form"
   commit id: "test: login"
   checkout main
   merge feature/login tag: "deployed"
   branch fix/button-label
   commit id: "fix: button label"
   checkout main
   merge fix/button-label tag: "deployed"

Good: almost no ceremony, obvious review point, easy to teach. Bad: no concept of a release. If you need to support version 2.3 while 2.5 is in development, this model has no answer for you.

This is the sane default for web apps, services and most internal tooling. Start here, add process only when something actually hurts.


Git Flow

The heavy one, and the one everybody’s seen a diagram of. Two permanent branches (main = released, develop = integration) plus three supporting types: feature/*, release/*, hotfix/*.

gitGraph
   commit id: "initial" tag: "v1.0.0"
   branch develop
   commit id: "chore: open 1.1"
   branch feature/search
   commit id: "feat: search"
   checkout develop
   merge feature/search
   branch release/1.1.0
   commit id: "chore: bump version"
   checkout main
   merge release/1.1.0 tag: "v1.1.0"
   checkout develop
   merge release/1.1.0
   checkout main
   branch hotfix/1.1.1
   commit id: "fix: crash on startup"
   checkout main
   merge hotfix/1.1.1 tag: "v1.1.1"
   checkout develop
   merge hotfix/1.1.1

Good: genuinely solves parallel release maintenance, versioned products, scheduled releases. Bad: a lot of branches, a lot of merging, and develop drifting from main is a recurring tax.

[!WARNING]

Git Flow gets cargo-culted onto web apps that deploy twelve times a day, where it adds pure overhead. If you ship continuously and only ever support one version, you do not need develop. The author himself says so.

Use it for installable/versioned software: desktop apps, libraries with long support windows, firmware, anything where “just deploy the fix” isn’t a thing.


Release Flow

Microsoft’s model, and the pragmatic middle ground. Trunk-based day to day, but each release cuts a release branch off main. Fixes always land on main first, then get cherry-picked into the release branch - never the other way around.

gitGraph
   commit id: "base"
   branch topic/search
   commit id: "feat: search"
   checkout main
   merge topic/search
   commit id: "feat: filters"
   branch release/2026.04
   commit id: "chore: release prep" tag: "v2026.04.0"
   checkout main
   commit id: "feat: next thing"
   commit id: "nullfix"
   checkout release/2026.04
   cherry-pick id: "nullfix" tag: "v2026.04.1"

Good: one integration branch, releases are still supportable, no develop drift. Bad: cherry-picking is manual effort and needs discipline about direction.

The “fix forward on main, then cherry-pick back” rule is the whole trick. It guarantees a fix can never exist on a release branch but be missing from the next release - which is the classic Git Flow footgun.


GitLab Flow

GitHub Flow plus environment branches for teams that can’t deploy straight to production on merge. main flows into staging, then production - downstream only, never back up.

gitGraph
   commit id: "main"
   branch staging
   commit id: "deploy to staging"
   branch production
   commit id: "deploy to prod" tag: "live"
   checkout main
   commit id: "feat: next"
   checkout staging
   merge main
   checkout production
   merge staging tag: "live"

Deployment becomes “merge into the next branch down”, which gives you an audit trail of what’s running where. Useful with regulated release gates. If your CD pipeline already tracks environments properly, this is largely redundant.


Picking one

Strategy Use when Pros Cons
Trunk-Based True continuous delivery Simplest history, minimal merging Needs strong tests + CI
GitHub Flow Web apps, services, one live version Low ceremony, clear review point No release concept
Release Flow Shipping on a cadence, supporting releases Trunk-ish, releases stay fixable Cherry-picking is manual
Git Flow Versioned/installable products Handles parallel versions Heavy process, develop drift
GitLab Flow Explicit environment gates Visible audit trail per env Often duplicates what CD does

Honest shortcut: GitHub Flow unless you have a concrete reason not to, Release Flow the moment you need to support a shipped version. Git Flow only if you really do maintain multiple versions in parallel.

Whichever you pick, the thing that actually determines whether it works:

  • Short-lived branches. A branch open for three weeks will not merge cleanly. It never does.
  • Protected main. No direct pushes, require review and green CI. See Pull Requests / Merge Requests.
  • Consistent names. feature/, fix/, release/ - see Naming Conventions.
  • Automated tests. Every strategy above degrades into “merge and pray” without them.

[!WARNING]

The strategy matters far less than the discipline. A team doing boring GitHub Flow properly beats a team doing Git Flow badly, every single time.

Keeping a branch current

Your branch gets stale the moment someone else merges. Two ways to catch up:

# Merge main into your branch - keeps true history, adds a merge commit
git switch feature/login
git merge main

# Rebase onto main - replays your commits on top, linear history
git switch feature/login
git fetch origin
git rebase origin/main

Rebase your own unshared branch freely. Don’t rebase anything others have already pulled - rewriting shared history is how you become that person on the team.

References and further reading

#git#scm#branching
100%