Git branching strategies all argue about the same number: how long your code sits apart from main. Start there and the rest follows.
Highlights
- Git branching strategies differ far less than the debate suggests, because nearly every argument between them is really an argument about how long code sits unmerged.
- A branch is not a copy of your code, it is a file containing a single commit hash, which is why creating one is instant and why deleting one loses nothing.
- DORA research puts the ceiling at a single day, since the organisations delivering best are the ones whose branches rarely survive longer than that, and every additional day correlates with worse delivery outcomes.
- Merge conflict probability is a function of branch age rather than developer skill, since the codebase drifts underneath you the whole time your branch is open.
- GitHub Flow is the sensible default for most teams, trunk based development is where high performing teams end up, and GitFlow now suits a narrow set of versioned release cases.
- Feature flags are what make short lived branches possible for large features, because they let you merge incomplete work without exposing it.
- Branch protection rules and a naming convention cost an afternoon to set up and prevent most of the accidents that make teams distrust their process.
Vincent Driessen published GitFlow in 2010, and it became the default answer to how teams should organise their branches for the better part of a decade. He has since added a note to that original post recommending against it for teams practising continuous delivery, and Atlassian now labels it a legacy workflow. The model did not get worse. The world it was designed for, shipping boxed software on a scheduled cadence, stopped being the world most teams live in.
That history is useful because it points at what actually matters. DORA research across thousands of engineering organisations keeps surfacing the same correlation, where the strongest performers almost never let a branch survive a full day, and where teams practising trunk based development deploy considerably more often with lower change failure rates. The strategies have different names and different diagrams, and underneath they are all making one trade: how long a piece of work is allowed to live apart from everyone else's. This guide starts with what a branch actually is, then works outward to how you choose.
What a Git branching strategy is
A Git branching strategy is an agreed set of rules for when your team creates branches, what they are named, how long they live, how they get reviewed, how they merge back, and which branches are allowed to deploy, so that many people can work on one codebase without constantly breaking each other's work.
The phrase to notice is how long they live. That single variable drives merge difficulty, review quality, and how quickly work reaches users, and every named strategy is really a different answer to it.
First, what a branch actually is
Almost everything about branching becomes intuitive once you know how little a branch is.
Git stores your project as a chain of commits, each one a snapshot with a pointer to its parent. A branch is not a copy of that chain and it is not a folder of files. It is a small file containing the hash of a single commit, and Git works out everything else by following parent pointers backwards from there.
cat .git/refs/heads/main
git rev-parse main
git log --oneline --graph --all --decorate -8The first command prints the contents of the branch file itself, which is a 40 character hash and a newline. The second asks Git the same question through its proper interface and returns the identical hash. The third draws the commit graph with every branch marked, which is the fastest way to see the shape of your repository rather than imagining it.
Did you know
A branch in Git is a file containing 41 bytes, meaning a 40 character commit hash plus a newline. That is the entire branch. When you run git switch -c feature/login, Git writes one small file and updates where HEAD points, which is why creating a branch is instantaneous even in a repository with a million commits. It also explains why deleting a branch feels scary and usually is not, since you are removing a pointer rather than any commits. The commits remain reachable through the reflog for a while afterwards, which is how most accidental deletions get recovered.
That mental model matters because it reframes the whole topic. Branching is cheap. Merging is where the cost lives, and merging gets more expensive the longer two lines of history have been apart.
The one variable that decides everything
Here is the idea the rest of this guide rests on.
While your branch is open, everyone else keeps committing to main. Their changes touch files, rename functions, move directories, and change interfaces. Your branch knows nothing about any of it. When you finally merge, Git has to reconcile two versions of history that have been drifting apart the entire time.
The probability of a painful merge is therefore a function of elapsed time rather than of anyone's skill. A branch open for three hours has to reconcile three hours of other people's work. A branch open for three weeks has to reconcile three weeks of it, and the conflicts are not only more numerous but harder to resolve, because by then nobody remembers why either side changed what it changed.
In the wild
Teams often describe a difficult merge as a skill problem, saying somebody should have kept their branch up to date. It is more useful to treat it as a scheduling problem. If the codebase changed for three weeks while a branch stayed open, that conflict was created by the calendar rather than by the person resolving it. The fix is not more care during the merge, it is a shorter branch, which is exactly why the strategy conversation keeps returning to lifetime rather than to technique.
The four Git branching strategies, honestly compared
Now the named models. Read each one as a different answer to how long code stays apart.
GitHub Flow
One permanent branch, called main, which is always deployable. You branch from it, make your change, open a pull request, get review, merge, and deploy. Branches live hours or days.
git switch main
git pull
git switch -c feature/checkout-validation
git push -u origin feature/checkout-validationThose four commands are the whole workflow. Start from an up to date main, create a branch named for the work, and push it so a pull request can be opened against it. The -u flag sets the upstream so later pushes need no arguments, which is a small convenience that saves a surprising amount of friction.
GitHub Flow suits most teams shipping continuously to a web service, and it is the right starting point if you have no strategy today. Its weakness is that it has no built in answer for supporting multiple released versions at once.
Trunk based development
Everyone commits to main, either directly or through branches that live hours rather than days. Incomplete work is hidden behind feature flags rather than isolated on a branch. Releases are cut from main, usually by tagging.
This is where DORA research points, with elite performers considerably more likely to work this way. The requirements are real, though. You need good automated tests, because there is no long stabilisation period to catch problems. You need feature flags for anything that cannot ship in a day. And you need the team discipline to break work into pieces that are individually safe to merge.
GitFlow
Two permanent branches, main for production and develop for integration, plus three kinds of supporting branch for features, releases, and hotfixes. Features merge into develop, a release branch stabilises before merging into main, and hotfixes branch directly from main.
GitFlow was designed for scheduled releases of installed software, and it does that job well. In a continuous delivery setting the ceremony becomes cost, since every extra branch is another place for code to wait. Its own author now recommends against it for teams deploying continuously.
GitLab Flow
A middle path. Main stays deployable as in GitHub Flow, and long lived environment branches such as staging and production track what is deployed where, with changes flowing downstream in one direction.
This suits teams that genuinely deploy to distinct environments on distinct schedules, and it adds moving parts you do not need if you deploy straight from main.
Watch out
The most common mistake is adopting GitFlow because it looks the most professional. It has the most boxes in its diagram, which reads as thoroughness, and for a team deploying to a web service several times a week it mostly adds waiting. Choose the simplest model that covers how you actually release, then add structure only when something specific forces you to. If you cannot name the concrete problem an extra branch solves, you do not need it yet.
Learn the Git fundamentals underneath all of this
Branching strategy makes far more sense once you are comfortable with what Git is doing under the hood, including commits, refs, and how history is actually stored. The Git for Beginners course on KodeKloud covers setup, everyday usage, and a deeper look at what Git really is, across nineteen sections with hands on labs.
Merge, rebase, or squash
Once a branch is ready, you have three ways to bring it back, and teams argue about this more than the topic deserves.
A merge commit preserves the real history including the fact that the work happened on a branch. It is honest and it makes the graph busier.
Rebasing instead replays your commits on top of the current main, producing a straight line as though you had written them just now. It is tidy, and it rewrites commit hashes, which is why the golden rule exists.
A squash merge collapses your whole branch into one commit on main. The history becomes one clean entry per change, and the individual steps of your work are discarded.
git switch feature/checkout-validation
git fetch origin
git rebase origin/main
git push --force-with-leaseThat sequence updates your branch with the latest main before review. Fetching first gets the current state without touching your working tree, the rebase replays your commits on top of it, and the force push is required because rebasing rewrote your commit hashes. Using --force-with-lease rather than plain --force is the important detail, since it refuses to overwrite the remote if somebody else has pushed to your branch in the meantime, which turns a potential disaster into a harmless error message.
Watch out
Never rebase a branch that other people have already pulled. Rebasing creates new commits with new hashes, so anyone who based work on the old commits now has a diverged history and a genuinely confusing mess to untangle. The workable rule is simple: rebase freely on branches only you are using, and never on shared branches such as main. If you must reverse a change on a shared branch, use git revert, which adds a new commit undoing the old one rather than rewriting anything.
Squash merging is the pragmatic default for a lot of teams, because it makes main's history a clean list of changes and it means nobody has to write beautiful intermediate commit messages. The cost is that you lose the fine grained steps, which matters if you rely on bisecting to find where a bug entered.
Feature flags, the thing that makes short branches possible
Here is the obvious objection to short lived branches. Some features take three weeks. How can the branch live one day?
The answer is that the branch does not hold the incomplete feature. A flag does.
A feature flag is a condition in your code that decides whether new behaviour is active, controlled by configuration rather than by deployment. You merge the new code to main in small pieces while the flag is off, so the code ships continuously and the feature stays invisible. When it is complete and tested, you turn the flag on, which is a configuration change rather than a release.
if flags.enabled("new_checkout", user=current_user):
return new_checkout_flow(cart)
return legacy_checkout_flow(cart)That small pattern is what decouples deploying from releasing, which is the idea underneath trunk based development. Both code paths exist in production, the flag decides which runs, and you can enable it for your own account first, then a small percentage of users, then everyone. If something goes wrong you turn the flag off, which is far faster and safer than reverting a deployment.
Flags come with their own discipline. Each one is a branch in your code rather than in your repository, so leaving them in place forever produces a codebase full of dead paths nobody dares remove. Give every flag an owner and a removal date, and delete it once the feature is fully rolled out.
Naming, protection, and the boring rules that help most
The conventions in this section take an afternoon to set up and prevent a large share of the accidents that make teams distrust their process.
A naming convention makes branches scannable and lets automation act on them. A simple prefix plus a short description works well, such as feature/checkout-validation, fix/session-timeout, or chore/upgrade-node. Adding a ticket reference helps traceability. Keep names lowercase with hyphens, since branch names are used in shell commands and paths where spaces and capitals cause friction.
Branch protection rules on main are the highest value configuration change available to a new team. Require a pull request rather than direct pushes, require at least one approving review, require status checks to pass, and require the branch to be up to date before merging. Those four settings prevent the majority of accidental damage.
A merge queue solves a subtler problem that appears as teams grow. Two pull requests can each pass their checks independently and still break main when combined, because neither was tested against the other. A merge queue tests each change against the actual state main will be in when it lands, and merges only if it passes.
Quick tip
Turn on the setting that deletes a branch automatically once its pull request merges. Stale branches accumulate quickly, and a repository with two hundred dead branches makes it genuinely hard to see what is in flight. The commits are safe in main after the merge, so nothing is lost. This one checkbox does more for repository hygiene than any amount of periodic cleanup, because it prevents the mess rather than requiring somebody to remember to tidy it.
How to choose your Git branching strategy
Work down these questions in order and stop at the first one that gives you a clear answer.
Do you support multiple released versions at once? If customers run version 2.4 while you develop 3.0, and you must patch both, you need release branches. That is the case GitFlow was built for, and it is a real one for installed software, libraries, and firmware.
Do you deploy to production continuously? If changes reach users within hours of merging, use GitHub Flow, and move toward trunk based development as your test coverage improves.
Do you have strong automated tests and feature flag infrastructure? If yes, trunk based development is available to you and the research suggests it is where you want to end up. If not, GitHub Flow with short branches gets most of the benefit while you build those foundations.
Do you genuinely deploy to distinct environments on distinct schedules? If staging and production really do run different versions for meaningful periods, GitLab Flow's environment branches earn their keep. If they do not, skip them.
The honest summary is that most teams reading this want GitHub Flow with a rule that branches merge within a day or two, and want to grow into trunk based development rather than adopting it on day one.
Practise the workflow on real repositories
Branching becomes second nature only once you have made the mistakes and recovered from them. The KodeKloud playgrounds give you environments where you can create branches, force conflicts on purpose, rebase, and recover from a bad merge without any of it mattering.
The commands that carry you day to day
Most branching work runs on about a dozen commands. Here they are grouped by the moment you reach for them.
Starting a piece of work means getting current first, because branching from a stale main starts you behind before you have written anything.
git switch main
git pull --ff-only
git switch -c feature/order-historyUsing --ff-only on the pull is a small habit worth forming. It refuses to create a merge commit if your local main has diverged from the remote, which turns a silent surprise into a clear error telling you something unexpected happened. The switch command is the modern replacement for using checkout to move between branches, and it exists precisely because checkout did too many unrelated jobs and confused people for years.
While you work, keeping the branch current is what prevents the merge from becoming an event.
git fetch origin
git rebase origin/main
git status
git diff origin/main...HEAD --statFetching updates your knowledge of the remote without touching your working tree, which makes it safe to run at any time. The rebase then moves your commits on top of the latest main. The last command is worth knowing well, since the three dot syntax shows what your branch changed relative to where it diverged from main rather than every difference between the two, and adding --stat gives you a file summary that tells you in one line whether your change has grown beyond what you intended.
Finishing up means cleaning after yourself, which matters more in a shared repository than people expect.
git push -u origin feature/order-history
git switch main
git pull --ff-only
git branch -d feature/order-history
git remote prune originThe lowercase -d deletes the branch only if it has been merged, which makes it a safety check rather than just a deletion. Pruning removes your local references to remote branches that no longer exist, which is what stops git branch -a becoming an unreadable list of long deleted work.
Quick tip
Two aliases pay for themselves within a week. Set git config --global alias.st "status -sb" for a compact status that fits on one screen, and git config --global alias.lg "log --oneline --graph --decorate -20" for a readable history. The second one in particular changes how people relate to Git, because seeing the actual shape of the commit graph rather than imagining it removes most of the mystery about what merging and rebasing are doing.
Finding when a bug appeared
This is the payoff for a clean history, and it is the reason the merge and squash debate has practical stakes rather than being purely aesthetic.
When a bug exists now and did not exist last month, Git can find the exact commit that introduced it by binary search, testing the middle commit of the range, then the middle of whichever half still has the bug, until one commit remains.
git bisect start
git bisect bad
git bisect good v2.3.0
git bisect run ./scripts/reproduce-bug.sh
git bisect resetYou mark the current state as bad and a known good point in the past, and Git checks out a commit halfway between them. The run form automates the whole process by having a script decide each step, where an exit code of zero means good and anything else means bad, so Git works through the range without you touching anything. Across a thousand commits this takes about ten steps rather than a thousand, which is the difference between an afternoon and a coffee break. Running reset at the end returns you to where you started.
Two things make bisect work well. Your history needs commits that individually build and run, which is an argument for squash merging since a squashed commit is a complete change while a mid branch commit may not even compile. And you need a reliable way to test whether the bug is present, which is usually a small script rather than a manual check.
For narrower questions, blame answers who changed a specific line and when.
git log -S "calculateShipping" --oneline
git blame -L 40,60 src/checkout.js
git log --follow --oneline src/checkout.jsThe first searches history for commits that added or removed a particular string, which is far more useful than searching commit messages because it finds the change itself regardless of how it was described. Blame with a line range shows who last touched those specific lines, and following a file traces its history through renames, which plain log will not do.
In the wild
Blame gets used as an accusation and it is far more useful as an archaeology tool. The question worth asking is rarely who wrote this, it is what else changed in that commit and what the message said about why. Opening the full commit that a blame points at usually explains a line that looked arbitrary, because it was written alongside something else that gives it context. Teams that write meaningful commit messages get a great deal of value back from this months later, which is the practical argument for caring about commit messages at all.
Common mistakes
Every row here is something that costs teams time repeatedly.
In the wild
Large pull requests get approved faster than small ones, which is exactly backwards and is worth naming openly with your team. A forty line change invites real scrutiny because a reviewer can hold all of it in their head. A two thousand line change invites a quick scroll and an approval, because reading it properly would take an hour that nobody has budgeted. If your review process is producing rubber stamps, the size of the changes is usually the cause rather than the diligence of the reviewers.
Branching when the team gets bigger
Everything above holds at any size, and two patterns appear once a team outgrows a handful of people sharing one repository.
Stacked pull requests solve the problem where a change is genuinely too large for one review but its parts depend on each other. Rather than one branch with two thousand lines, you open a chain of small branches, each based on the one before, and review them in order. Each piece is small enough to read properly, and the dependency between them is explicit rather than hidden. Meta and Uber popularised this at scale, and tooling now exists to manage the chain, since rebasing the bottom of a stack by hand is tedious.
Merge queues solve the problem where two pull requests each pass their checks and break main when combined, because neither was ever tested against the other. A queue serialises merging, testing each change against the exact state main will be in when it lands, and rejecting it if that combination fails. The larger the team, the more often this case occurs, since it scales with the number of changes in flight at once.
There is also a scaling failure worth naming. As repositories grow, teams often respond to merge pain by creating more branches, which is precisely backwards. More branches means more code sitting apart for longer, which is the cause rather than the cure. The scaling answer is smaller changes merged more often, supported by better automation, and both patterns above exist to make that possible rather than to add structure.
Recovering when something goes wrong
Knowing these three commands turns most branching accidents from crises into inconveniences.
git reflog
git switch -c recovered abc1234
git revert a1b2c3d
git merge --abortThe reflog records every position HEAD has occupied, including commits that are no longer reachable from any branch, which means a branch you deleted by mistake or a reset you regret is almost always recoverable by finding its hash there. Creating a branch at that hash brings the work back. The revert command undoes a specific commit by adding a new commit that reverses it, which is the safe way to undo something on a shared branch since it rewrites no history. Aborting a merge returns you to the state before you started, which is the right move the moment a merge turns out to be bigger than you expected.
Quick tip
When a merge or rebase goes badly, stop and abort rather than pushing through. There is a strong instinct to resolve conflicts as they appear because you have already started, and that instinct produces the worst outcomes, since a half resolved merge with mistakes in it is far harder to diagnose than a clean restart. Abort, update your branch from main, and try again with fresh conflicts. It usually takes less time than finishing the mess you were in.
Where to start
- Run the graph command on your main repository and look at the actual shape of your history, since most teams have never seen it drawn.
- Find your longest lived open branch and ask what it would take to merge it this week.
- Turn on branch protection for main, requiring a pull request, one review, and passing status checks.
- Enable automatic deletion of merged branches, which is one checkbox and permanently improves hygiene.
- Agree a naming convention and write it in your repository's contributing guide, so it survives people joining.
- Set a team norm that branches merge within two days, and treat anything older as a signal to split the work.
- If you have features that cannot ship in two days, start the conversation about feature flags, because that is the unlock for everything else.
Conclusion
Git branching strategies get discussed as though the choice between GitFlow and trunk based development were a matter of philosophy. It is closer to a matter of arithmetic. Every day a branch stays open, the codebase underneath it changes, the eventual merge grows harder, the review gets less careful, and the work reaches users later. The named strategies are just different levels of tolerance for that.
So the useful question is not which model has the best diagram, it is how quickly your team can get a change from an idea into main. Most teams reading this should run GitHub Flow with a two day rule, protect main properly, and grow toward trunk based development as their tests and feature flag infrastructure improve.
Start by looking at your oldest open branch this week. If it has been alive longer than a couple of days, that branch is telling you something more useful about your process than any strategy diagram will.
Ready to Get Genuinely Comfortable With Git?
Branching strategy is the easy part once the fundamentals are solid, and the fundamentals reward proper study rather than accumulated habit. The Git for Beginners course on KodeKloud takes you from setup through what Git is actually doing under the hood, the GitHub Actions course covers automating the checks that make branch protection meaningful, and the KodeKloud playgrounds give you repositories to break and recover safely. Start with one today.
FAQs
Q1: What is the difference between GitFlow, GitHub Flow, and trunk based development?
They differ mainly in how many permanent branches they use and therefore in how long work stays unmerged. GitHub Flow has one permanent branch, main, which is always deployable, and everything else is a short lived branch that merges through a pull request within hours or days. Trunk based development goes further by having everyone integrate into main continuously, with branches living hours at most and incomplete features hidden behind flags rather than isolated on branches. GitFlow uses two permanent branches, main and develop, plus separate feature, release, and hotfix branches, which suits scheduled releases of installed software and adds meaningful delay for teams deploying continuously. The practical guidance is that GitHub Flow is the right default for most teams, trunk based development is where high performing teams tend to end up once their automated testing is strong, and GitFlow is worth the ceremony only when you genuinely support multiple released versions at once.
Q2: How long should a Git branch live?
Under a day is the target, and under two days is a reasonable working ceiling for most teams. DORA research consistently points to the same ceiling, with the strongest performing organisations rarely letting a branch survive past one day and treating anything older as a signal worth investigating. The reason is mechanical rather than cultural: while your branch is open, everyone else keeps changing main, so the eventual merge has to reconcile all of that drift, and the difficulty grows with the elapsed time rather than with anyone's ability. Long branches also degrade review quality, because a large diff invites skimming while a small one invites genuine scrutiny. When a piece of work is genuinely too big for a day, the answer is to break it into smaller pieces that are individually safe to merge, using a feature flag to keep the incomplete behaviour switched off in production until the whole thing is ready.
Q3: What do I need to know before choosing a branching strategy?
Less about Git than about how your team actually ships. You need to know whether you support multiple released versions at once, since that is the single condition that genuinely justifies release branches. You need to know your deployment cadence, because a team deploying several times a day and a team deploying monthly want different amounts of structure. You need an honest assessment of your automated test coverage, since trunk based development depends on tests catching problems that a stabilisation period would otherwise catch. And you need to know whether you have feature flag infrastructure, because that determines whether large features can be built on short branches. On the Git side, being comfortable with branches, merges, rebasing, and pull requests is enough. If you want to shore up the fundamentals, the Git for Beginners course on KodeKloud covers what Git is doing underneath, and the KodeKloud playgrounds let you practise recovery safely.
Q4: Should I merge, rebase, or squash?
All three are defensible, and the choice matters less than being consistent about it. A merge commit preserves the true history including the branch structure, which is honest and produces a busier graph. Rebasing replays your commits on top of current main so history reads as a straight line, which is tidy but rewrites commit hashes, meaning you must never do it to a branch other people have pulled. Squash merging collapses the branch into a single commit on main, which gives you one clean entry per change and discards the intermediate steps. Most teams land on squash merging for pull requests, because branch commits are usually working notes rather than a curated narrative, and a clean main history is easier to read and revert. If you rely on bisecting to find when a bug appeared, the finer granularity of merge or rebase may be worth keeping. Whatever you choose, configure it as the repository default so it is not decided per pull request.
Q5: How do feature flags relate to branching strategy?
Feature flags are what make short lived branches possible for work that takes weeks, which makes them the enabler for trunk based development rather than an unrelated technique. The problem they solve is direct: a feature that takes three weeks cannot live on a one day branch, so either the branch grows long and accumulates conflicts, or the incomplete code must be safe to have in main. A flag makes the second option workable by wrapping new behaviour in a condition controlled by configuration, so the code ships continuously while the feature stays invisible until you switch it on. That decouples deploying from releasing, which also means a problem can be turned off in seconds rather than requiring a rollback. The discipline they demand is cleanup, since every flag is a branch in your code and a codebase full of stale flags becomes hard to reason about. Give each one an owner and a removal date, and delete it once the rollout is complete.
Q6: What is branch protection and what should I turn on?
Branch protection is a set of rules your hosting platform enforces on important branches, and it is the highest value configuration change a new team can make. Four settings cover most of the benefit: require a pull request rather than allowing direct pushes to main, require at least one approving review, require your status checks to pass before merging, and require the branch to be up to date with main first so changes are tested against what they will actually land on top of. Together those prevent unreviewed code reaching production, catch broken changes before they merge, and stop the accidental direct push that everyone makes eventually. Two additions are worth considering as the team grows: a merge queue, which tests each change against the real state main will be in when it lands and catches the case where two individually passing pull requests break when combined, and automatic deletion of merged branches, which keeps the branch list readable without anyone having to remember to tidy up.
Discussion