The two previous lessons settled how a change is proposed and how it is reviewed. What remains is the question above them: which branches exist in the repository and what each one is for.

Up to now, task-manager has lived with an implicit model: a main branch and working branches with the prefixes the team agreed on in lesson 03-06. It worked because the team deployed the web application and that was that. But the situation has changed: the company has started selling task-manager as a product installed on the client's servers. There is now a version 1.4 running at three clients who do not want to upgrade, a version 2.0 in preparation, and an obligation to publish security fixes for 1.4 without dragging in anything from 2.0.

That scenario — explicit versions, several alive at once, planned releases — is exactly the one Git Flow was designed for: the branching model Vincent Driessen published in January 2010 and which for years was the default answer to the question "how do we organise our branches?".

This lesson explains the complete model, without caricaturing it and without selling it. It is a model with impeccable internal logic and with real costs, and both things have to be understood before deciding whether it suits you.

Contents

  1. The problem Git Flow solves
  2. The five classes of branch
  3. The complete cycle at a glance
  4. develop and main: the two permanent branches
  5. Feature branches: opening and closing
  6. Release branches: stabilising without blocking
  7. Hotfix branches: why they merge in two places
  8. Where the tags fit in
  9. The git flow tool
  10. When Git Flow makes sense
  11. The criticisms, and the author's own note

  1. The problem Git Flow solves

Before the model, the problem. Picture the task-manager team with no branch convention at all, about to release version 2.0:

  • Ana has finished the CSV export and wants to integrate it.
  • Carla is halfway through the calendar sync, which will not go into 2.0.
  • Bruno is fixing the last bugs found in the 2.0 testing.
  • And a client has just reported a serious bug in 1.4 that has to be fixed today.

If everybody works against a single branch, this is impossible to manage. Carla's work cannot go in yet, but neither can it sit unintegrated for three weeks. The 1.4 fix cannot be built on top of the 2.0 code, because the client does not want 2.0.

Git Flow answers with a simple and consistent idea: each type of work has its own class of branch, with explicit rules about where it is born and where it ends up. Nothing is improvised.

  1. The five classes of branch

The model defines five, split into two groups.

Permanent branches (they always exist, they are never deleted):

  • main (called master in the original article): contains released code and nothing else. Every commit on main is a version that went out into the world, and it carries its tag. Nobody commits directly here.
  • develop: the integration line. It contains everything finished and accepted that will go out in the next version. It is the "next release" state.

Supporting branches (they are born, they serve their purpose and they are deleted):

  • feature/*: a feature under development.
  • release/*: the preparation and stabilisation of a specific version.
  • hotfix/*: an urgent fix on top of what is in production.

The complete table, which is the model's practical reference:

Branch Born from Merged into Permanent? Usual name
main Yes main / master
develop main (once, at the start) Yes develop
feature/* develop develop No feature/csv-export
release/* develop main and develop No release/2.0.0
hotfix/* main main and develop No hotfix/2.0.1

There are two rows that concentrate almost all the model's complexity, and it is worth noting them right away:

  1. hotfix/* is born from main, not from develop. It is the only class that does so, and it is the model's whole reason for existing: to fix what is in production you have to start from what is in production, not from development code that has fifteen half-finished features in it.
  2. release/* and hotfix/* merge in two places. That double integration is what guarantees nothing gets lost, and it is also the source of the model's most expensive mistakes (section 7).

In task-manager, the team already uses the prefixes feature/, fix/, docs/ and hotfix/ (lesson 03-06). Git Flow proposes feature/, release/ and hotfix/. The specific name does not matter: what matters is that each class has a role and that everybody knows it. We shall use the model's canonical names so that you recognise the terminology when you meet it elsewhere.

  1. The complete cycle at a glance

This graph captures the entire cycle: two features, one release, and a later hotfix.

gitGraph
   commit id: "initial" tag: "v1.4.0"
   branch develop
   checkout develop
   commit id: "start develop"
   branch feature/csv-export
   checkout feature/csv-export
   commit id: "F1"
   commit id: "F2"
   checkout develop
   merge feature/csv-export
   branch feature/alphabetical-order
   checkout feature/alphabetical-order
   commit id: "G1"
   checkout develop
   merge feature/alphabetical-order
   branch release/2.0.0
   checkout release/2.0.0
   commit id: "bump version to 2.0.0"
   commit id: "fix QA bug"
   checkout main
   merge release/2.0.0 tag: "v2.0.0"
   checkout develop
   merge release/2.0.0
   checkout main
   branch hotfix/2.0.1
   checkout hotfix/2.0.1
   commit id: "fix bulk delete"
   checkout main
   merge hotfix/2.0.1 tag: "v2.0.1"
   checkout develop
   merge hotfix/2.0.1

Read it from top to bottom and take away the general shape:

  • main is a short line of tagged milestones. Five commits a year, perhaps. Each one a version.
  • develop is the line of continuous work, where everything finished flows in.
  • Feature branches leave and return to develop.
  • Release branches leave develop and flow into both permanent branches.
  • The hotfix branches leave main and also flow into both.

That "twin-track with bridges" shape is Git Flow's visual signature. If you see a graph like that, you already know which model the project follows.

  1. develop and main: the two permanent branches

main is a record, not a place to work

The rule is absolute: only merges from release/* and from hotfix/* go into main. No direct commits, no features, no fix that does not come by one of those two routes.

The practical consequence is very valuable: git log --oneline main is literally the product's version history, and git checkout v1.4.0 reconstructs exactly what the client has installed. That is what makes it possible to debug a bug reported by a specific client without guesswork.

git log --oneline --first-parent main
8f3c2a1 Merge branch hotfix/2.0.1
4d9e7b3 Merge branch release/2.0.0
b1a6f28 Merge branch hotfix/1.4.1
7c2d5e9 Merge branch release/1.4.0

The --first-parent (lesson 06-04) shows only the mainline, ignoring what came in through each merge.

develop is the waiting room for the next version

Everything finished gets integrated here. And here is the model's first important restriction, which is worth saying out loud:

develop is not guaranteed to be deployable. It is guaranteed to be integrated.

It is a substantial difference from the models we shall see later. In Git Flow, the quality guarantee is not applied on develop: it is applied on the release branch, which is where stabilisation happens. develop may have half-polished features, changes that have not been tested together and regressions nobody has spotted yet. It is an intermediate state, and the model takes that as given.

Initial creation, in a project starting from main:

git switch main
git switch -c develop
git push -u origin develop

And an important operational detail: on the platform, the repository's default branch should be develop, not main. That way pull requests point by default at the right place and new clones land where the work happens. It is a one-minute setting that avoids dozens of PRs opened against the wrong branch.

  1. Feature branches: opening and closing

This is the most frequent cycle and the simplest.

Opening

git switch develop
git pull                                  # start from the latest integrated work
git switch -c feature/csv-export

It is born from develop, always. Being born from main is the classic beginner's mistake in this model: you take a base without the latest features and you end up with conflicts when integrating.

Working

Ordinary commits, as many as needed. Push to origin if the work lasts more than a day or if you need to collaborate:

git push -u origin feature/csv-export

Closing

When the feature is finished and reviewed (lessons 07-01 and 07-02), it is merged into develop:

git switch develop
git pull
git merge --no-ff feature/csv-export
git push origin develop
git branch -d feature/csv-export
git push origin --delete feature/csv-export

The --no-ff is not optional in Git Flow, it is part of the model. Remember from lesson 03-03 that without it, if develop has not moved on, Git would do a fast-forward and the feature's commits would be diluted into the mainline leaving no trace of the fact that they formed a set. With --no-ff a merge commit is always created, and that gives you three things:

  • The graph shows which commits belonged to which feature.
  • The feature can be reverted in full with git revert -m 1 <merge> (lesson 05-06).
  • git log --first-parent develop gives the list of integrated features, one line each.

If the team works with pull requests, the platform does this merge --no-ff for you when you press the merge button.

Keeping up to date during development

If develop moves on a lot while you are working, update your branch so as not to accumulate conflicts:

git switch feature/csv-export
git rebase develop           # linear history; only if the branch is not shared
# or else
git merge develop            # no rewriting; always safe

The choice between the two is project policy and it is dealt with in lesson 08-02. What you must not do is leave the branch unupdated for weeks: that is the mechanism by which Git Flow accumulates large conflicts, which we shall talk about in section 11.

  1. Release branches: stabilising without blocking

This is the class of branch most people do not understand, and the one that justifies the whole model.

The problem it solves: the moment to release 2.0 arrives. The version number has to be bumped in the files, the README.md and the changelog have to be updated, the manual test suite has to be run through, whatever comes up has to be fixed. That takes a week. During that week, what does the rest of the team do? If everybody waits, a week of three people's work is lost. If they carry on integrating into develop, the version never freezes because something new is always coming in.

The solution: a release/2.0.0 branch is opened from develop. From that instant on:

  • Version 2.0 is exactly what is in that branch. It is frozen.
  • develop is freed immediately to receive work for 2.1.
  • Only bug fixes and release adjustments go into the release branch. No new features. None.
# Open the release branch
git switch develop
git pull
git switch -c release/2.0.0

# Release-specific adjustments
# (edit the version number in the project's files)
git commit -am "Bump the version to 2.0.0"

# (update CHANGELOG.md with what goes into this version)
git commit -am "Update the changelog for 2.0.0"

git push -u origin release/2.0.0

Over the following days, testing reveals bugs and they are fixed in this branch:

git switch release/2.0.0
git commit -am "Fix the long title overflowing in the mobile view"
git push origin release/2.0.0

The closing: the double integration

When the version is ready, it is merged into main and into develop:

# 1. Into main, which is what gets released
git switch main
git pull
git merge --no-ff release/2.0.0 -m "Merge branch release/2.0.0"

# 2. Tag the version (lesson 05-05): annotated and signed
git tag -a v2.0.0 -m "Version 2.0.0

CSV export, configurable alphabetical ordering and
calendar synchronisation."

git push origin main --follow-tags
# 3. And back into develop, so as not to lose the stabilisation fixes
git switch develop
git pull
git merge --no-ff release/2.0.0 -m "Merge branch release/2.0.0 into develop"
git push origin develop

# 4. Delete the branch
git branch -d release/2.0.0
git push origin --delete release/2.0.0

Step 3 is the one people forget, and the consequence is unpleasant: the five fixes made during stabilisation exist in main but not in develop. In other words, version 2.1 will reintroduce the five bugs you have just fixed. They will turn up as "mysterious regressions" and will cost a whole day of git bisect (lesson 06-02) until somebody works out what happened.

How to check that it has not happened:

# Is there anything in main that is not in develop?
git log --oneline develop..main

If that command returns anything other than main's own merge commits, you have work to do. It is worth turning it into an automatic CI check (lesson 07-06).

When should the release branch be opened?

When develop contains the scope planned for that version. The criterion is not about time, it is about content. And one clarification: opening the branch early is better than late, because it frees develop sooner. Many teams open it as soon as the last planned feature is integrated, even though the whole testing phase is still to come.

  1. Hotfix branches: why they merge in two places

The scenario: 2.0.0 has been in production for three days and a client discovers that the bulk delete button also removes other users' completed tasks. It is serious. It has to be fixed today.

And here is the model's critical point: you cannot release what is in develop. develop already carries four untested 2.1 features. Releasing that in order to fix one bug would be swapping one problem for five.

Git Flow's solution: start from main, which is exactly what the client has installed.

# 1. Born from main, at the tag of the affected version
git switch main
git pull
git switch -c hotfix/2.0.1

# 2. The minimal fix. Nothing else.
git commit -am "Fix the bulk delete that affected other users' tasks

The filter by user was not being applied in deleteCompleted().
Refs GT-318."

# 3. Bump the patch version number
git commit -am "Bump the version to 2.0.1"
git push -u origin hotfix/2.0.1

And the closing, also double:

# Into main, with its tag
git switch main
git merge --no-ff hotfix/2.0.1 -m "Merge branch hotfix/2.0.1"
git tag -a v2.0.1 -m "Version 2.0.1: fix the bulk delete"
git push origin main --follow-tags

# Into develop, so that the fix is not lost
git switch develop
git merge --no-ff hotfix/2.0.1 -m "Merge branch hotfix/2.0.1 into develop"
git push origin develop

git branch -d hotfix/2.0.1
git push origin --delete hotfix/2.0.1

The reason for the double integration, said explicitly: if the fix only went into main, the next version would come out of develop, which does not have it, and the bug would come back. The client would see the same problem you fixed for them in 2.0.1 reappear in 2.1. It is the most expensive mistake you can make with this model, because it destroys the client's trust.

A special case: a hotfix while a release is open

If at that moment a release/2.1.0 exists in stabilisation, the fix has to reach all three: main, release/2.1.0 and develop. The practical rule: it is merged into main and into the open release branch; develop will receive it when that release is closed. And if you would rather not reason it out every time, use git cherry-pick (lesson 05-03) to take the fix commit wherever it is missing, and check afterwards with git log --oneline <branch>..main.

If an old version has to be fixed

The client still on 1.4 also needs the fix, and main is on 2.0.1. Here the canonical model falls short and the usual practice is to have a long-lived support branch:

# A support branch created from the corresponding tag
git switch -c support/1.4 v1.4.0

# Bring over the fix that already exists in main
git cherry-pick <fix-hash>
git tag -a v1.4.2 -m "Version 1.4.2: fix the bulk delete"
git push -u origin support/1.4 --follow-tags

This ability to keep several versions alive at once is the main reason an installable product chooses Git Flow. No other model does it as well.

  1. Where the tags fit in

The tags from lesson 05-05 have an exact, unambiguous place in Git Flow:

Every merge commit on main receives an annotated tag with the version number. No other branch gets tagged.

Source of the merge into main Tag SemVer component that changes
release/2.0.0 with new features v2.0.0 or v2.1.0 major or minor
hotfix/2.0.1 v2.0.1 patch

And the fit with SemVer is almost automatic: release/* bumps minor (or major if it breaks compatibility), hotfix/* always bumps patch. That is why the branch name includes the version number: release/2.1.0 tells you which tag it is going to produce before you even open it.

Remember from 05-05 the two rules that matter here:

  • Annotated tags (git tag -a), not lightweight ones: they carry an author, a date and a message, and they are real objects in the repository.
  • Tags are not pushed on their own. git push origin main does not take them. Use --follow-tags (it pushes annotated tags reachable from what you are pushing) or git push origin v2.0.1.

Useful checks on the version history:

# All the published versions, in order
git tag -l 'v*' --sort=-v:refname

# What changed between two versions
git log --oneline v2.0.0..v2.0.1

# Which version a commit was first released in
git describe --contains <hash>

# Current readable version, useful for the build number
git describe --tags

  1. The git flow tool

There is a command-line extension, git-flow (originally by Vincent Driessen, today maintained mainly in the AVH variant), that automates the sequences of the previous sections.

# Installation
sudo apt install git-flow          # Ubuntu — Ana
brew install git-flow-avh          # macOS — Bruno
# Windows — Carla: included in Git for Windows, or via Chocolatey
# Initialise it in the repository (it asks for the branch names and prefixes)
git flow init

The commands and their exact translation into what you already know:

git flow command What it actually does
git flow feature start csv-export git switch -c feature/csv-export develop
git flow feature publish csv-export git push -u origin feature/csv-export
git flow feature finish csv-export switch develop + merge --no-ff + branch -d
git flow release start 2.0.0 git switch -c release/2.0.0 develop
git flow release finish 2.0.0 merge into main + tag + merge into develop + delete branch
git flow hotfix start 2.0.1 git switch -c hotfix/2.0.1 main
git flow hotfix finish 2.0.1 merge into main + tag + merge into develop + delete branch

We insist on something important: git flow adds no capability to Git. It is syntactic sugar over switch, merge, tag, push and branch -d. There is no hidden state and no magic; the only thing it stores is the branch-name configuration in .git/config:

[gitflow "branch"]
    master = main
    develop = develop
[gitflow "prefix"]
    feature = feature/
    release = release/
    hotfix = hotfix/
    versiontag = v

Advantages: it removes the risk of forgetting the second merge, which is the model's expensive mistake, and it standardises how the team works.

Real drawbacks, and they are not minor ones:

  • It fits badly with pull requests. git flow feature finish merges locally and pushes. If the project requires review before merging (lessons 07-01 and 07-02), that command skips the entire process. In teams with mandatory review, git flow feature publish is used and then an ordinary PR, ignoring finish.
  • It hides what is going on. Anyone who learns Git Flow only through the tool does not understand the graph they are building, and the day something goes wrong they do not know where to start.
  • It is one more dependency to install on three different operating systems.

A practical tip: learn the manual commands first, and adopt the tool afterwards if the team wants it. Not the other way round.

  1. When Git Flow makes sense

Git Flow has a bad reputation today, and a good part of that bad reputation comes from having been applied to projects it was not designed for. The model is good when these conditions hold:

Condition Why it matters
Explicit, numbered versions The whole model revolves around release/* and the tags. Without versions, main and develop are redundant
Several versions maintained at once The hotfix/* + support branches combination is the best answer to this problem
Planned, not continuous, releases The release branch makes sense if there is a real stabilisation phase
The user decides when to upgrade Installable software, mobile, embedded, libraries
A manual QA phase before releasing The release branch is exactly where that phase lives
A medium or large team with defined roles Somebody "manages the release" as a job of their own

Concrete cases where it fits well:

  • Software installed at the client's site, like the task-manager the company now sells.
  • Mobile applications, where the store imposes a review cycle and the user decides when to update.
  • Libraries and SDKs with a compatibility commitment and several maintained branches (1.x, 2.x).
  • Embedded systems and firmware, where a bad version cannot be withdrawn.
  • Regulated environments where every version requires documentation and formal approval.

And where it fits badly, which is where it has been used most:

  • Web applications with continuous deployment. If you release five times a day, the release branch is an empty formality and develop is a copy of main one day behind.
  • SaaS with a single live version. There is nothing to maintain in parallel; hotfix/* adds nothing that an ordinary branch does not.
  • Small teams. The model's coordination cost is spread across few people and weighs too heavily.

  1. The criticisms, and the author's own note

The objections to Git Flow are serious and worth knowing before adopting it.

Criticism 1: complexity

Five classes of branch, two permanent ones, different rules of origin and destination for each, and two merges to remember. It is a lot of protocol to keep in your head. In practice, every team that uses Git Flow ends up accumulating an internal "how we do things" document and even so somebody gets it wrong every few weeks.

Criticism 2: long-lived branches and late integration

This is the fundamental criticism, and it goes beyond convenience.

A feature can live for weeks in its branch before touching develop. And the relationship between the time a branch spends apart and the cost of integrating it is not linear: it is explosive. With every day that passes there are more commits in develop, a higher probability that somebody has touched the same files, more conflicts and harder ones.

flowchart LR
    A["Branch open<br/>for longer"] --> B["More divergence<br/>from develop"]
    B --> C["Bigger, harder<br/>conflicts"]
    C --> D["Fear of integrating"]
    D --> A

That loop feeds itself: the worse integrating is, the more it gets postponed, and the more it gets postponed, the worse it is. The model does not cause it on its own, but it does not push against it either: by not requiring frequent integration, it allows it.

There is a second knock-on effect: finished work takes a long time to reach the user. A feature can be finished in March, integrated into develop in April, go into a release in May and be published in June. Three months of value sitting idle in a repository.

Criticism 3: develop and main overlap

In projects with a single live version and continuous deployment, develop is simply "main in a little while". Two branches representing almost the same thing generate synchronisation work without adding information. It is the criticism you hear most, and in that context it is on the mark.

Criticism 4: friction with continuous integration

CI (lesson 07-06) wants to run everything against the mainline on every push. With two permanent branches and ephemeral release branches, you have to decide what gets tested where, and it usually ends in configurations that are complicated to maintain.

Vincent Driessen's own note

In March 2020, ten years after the original article, Driessen added a reflective note at the head of his own text. Its content, in summary:

  • The model was written in 2010, when the software world was different and the web was still delivered in versions.
  • Git Flow is still suitable for software with explicit versions, which is what it was conceived for.
  • For continuous web development, where you deploy constantly and there is only one version in production, he recommends not adopting Git Flow and using a simpler model, and he explicitly mentions GitHub Flow as a reasonable alternative.
  • And a general warning that holds for this whole module: do not adopt any methodology dogmatically. Choose according to your project.

For the author of a model to publish a note like that ten years later is rare and honest, and it is the best way to close this section. Git Flow is not a bad idea to be avoided: it is a specific tool that became popular well beyond its scope.

Common Mistakes and Tips

Mistake 1: creating a feature/* from main. You start from a base without the features already integrated and you pile up conflicts. It is born from develop, always.

Mistake 2: forgetting to merge the release back into develop. The stabilisation fixes stay only in main and the bugs reappear in the next version. Check it with git log --oneline develop..main.

Mistake 3: forgetting to merge the hotfix into develop. The same problem, and worse, because it is a bug the client has already reported to you once.

Mistake 4: putting new features into a release branch. The release branch exists in order to stabilise. If new code goes in, it never stabilises and the cycle stretches out indefinitely.

Mistake 5: merging with a fast-forward. You lose the grouping of commits by feature and the possibility of reverting it in one go. --no-ff always.

Mistake 6: tagging on develop or on the release branch. The tags go on main, on the merge commit. There and nowhere else.

Mistake 7: using lightweight tags. git tag v2.0.0 creates a reference with no metadata. Use git tag -a.

Mistake 8: not pushing the tags. git push origin main does not take them. --follow-tags.

Mistake 9: leaving the repository's default branch as main. Every PR gets opened against the wrong branch. In Git Flow, the default branch is develop.

Mistake 10: adopting Git Flow because it is "the standard". It no longer is, and it probably never should have been for web applications. Adopt it if your product has explicit versions.

Tip 1: automate the double-integration check. A CI job that runs git log --oneline develop..main and fails if there are unpropagated commits eliminates the model's most expensive mistake.

Tip 2: put the version number in the branch name. release/2.1.0 tells you which tag it is going to produce.

Tip 3: open the release branch early. It frees develop sooner and reduces the pressure on the team.

Tip 4: update your feature/* branches from develop often. It is the direct antidote to criticism 2, and it is in your hands.

Tip 5: if you adopt git flow, start with the manual commands. Understanding the graph you are building is worth more than saving keystrokes.

Tip 6: support branches (support/1.4) for old versions. The canonical model does not include them and almost every real product needs them.

Exercises

Exercise 1: setting up the complete cycle

Build a task-manager repository from scratch with the entire Git Flow cycle:

  1. Initialise the repository with index.html, app.js and README.md, and tag that state as v1.4.0 on main.
  2. Create develop from main.
  3. Develop feature/csv-export with two commits and integrate it into develop with --no-ff.
  4. Develop feature/alphabetical-order with one commit and integrate it the same way.
  5. Open release/2.0.0, bump the version number in README.md and fix a bug.
  6. Close the release: merge into main, tag v2.0.0 as an annotated tag, merge into develop as well and delete the branch.
  7. Show the complete graph and check that it resembles the one in section 3.

Exercise 2: hotfix and verifying the double integration

Carrying on with the previous repository:

  1. Add one more feature to develop (simulating the 2.1 work).
  2. Open hotfix/2.0.1 from main, fix a bug and bump the patch version.
  3. Close it correctly: main with the tag v2.0.1, and develop.
  4. Write a command that checks nothing is left in main unpropagated to develop.
  5. Repeat the exercise deliberately wrongly (without merging into develop) on a copy of the repository and observe what that check detects.
  6. Fix the situation with git cherry-pick.

Exercise 3: maintaining an old version

  1. Create a support branch support/1.4 from the v1.4.0 tag.
  2. Bring the hotfix from the previous exercise onto that branch with git cherry-pick.
  3. Tag the result as v1.4.1.
  4. List all the tags sorted by version and check with git describe --contains which version each fix went into.
  5. Show the complete topology with git log --graph --oneline --all --decorate, with the three live lines.

Solutions

Solution 1:

mkdir /tmp/gitflow && cd /tmp/gitflow
git init -qb main
printf '<h1>Task Manager</h1>\n' > index.html
printf 'const tasks = [];\n' > app.js
printf '# task-manager\n\nVersion: 1.4.0\n' > README.md
git add . && git commit -q -m "Initial version of the application"
git tag -a v1.4.0 -m "Version 1.4.0"
git switch -qc develop
# 3. First feature
git switch -qc feature/csv-export
echo "function exportCSV() { /* ... */ }" >> app.js
git commit -qam "Add CSV export"
echo "function download(name, data) { /* ... */ }" >> app.js
git commit -qam "Add download of the generated file"
git switch -q develop
git merge -q --no-ff feature/csv-export -m "Merge branch feature/csv-export"
git branch -qd feature/csv-export
# 4. Second feature
git switch -qc feature/alphabetical-order
echo "function sortTasks() { tasks.sort(); }" >> app.js
git commit -qam "Add alphabetical ordering"
git switch -q develop
git merge -q --no-ff feature/alphabetical-order -m "Merge branch feature/alphabetical-order"
git branch -qd feature/alphabetical-order
# 5. Release branch
git switch -qc release/2.0.0
sed -i 's/Version: 1.4.0/Version: 2.0.0/' README.md
git commit -qam "Bump the version to 2.0.0"
echo "// fixed: quotes in the title when exporting" >> app.js
git commit -qam "Fix the escaping of quotes in the export"
# 6. Closing with the double integration
git switch -q main
git merge -q --no-ff release/2.0.0 -m "Merge branch release/2.0.0"
git tag -a v2.0.0 -m "Version 2.0.0: CSV export and alphabetical ordering"
git switch -q develop
git merge -q --no-ff release/2.0.0 -m "Merge branch release/2.0.0 into develop"
git branch -qd release/2.0.0
# 7. The graph
git log --graph --oneline --all --decorate

Solution 2:

# 1. The 2.1 work on develop
git switch -q develop
git switch -qc feature/task-labels
echo "function addLabel(id, label) { /* ... */ }" >> app.js
git commit -qam "Add labels to tasks"
git switch -q develop
git merge -q --no-ff feature/task-labels -m "Merge branch feature/task-labels"
git branch -qd feature/task-labels
# 2. Hotfix from main
git switch -q main
git switch -qc hotfix/2.0.1
echo "// fixed: filter by user in deleteCompleted()" >> app.js
git commit -qam "Fix the bulk delete that affected other users' tasks

The filter by user was not being applied in deleteCompleted().
Refs GT-318."
sed -i 's/Version: 2.0.0/Version: 2.0.1/' README.md
git commit -qam "Bump the version to 2.0.1"
# 3. Double closing
git switch -q main
git merge -q --no-ff hotfix/2.0.1 -m "Merge branch hotfix/2.0.1"
git tag -a v2.0.1 -m "Version 2.0.1: fix the bulk delete"
git switch -q develop
git merge -q --no-ff hotfix/2.0.1 -m "Merge branch hotfix/2.0.1 into develop"
git branch -qd hotfix/2.0.1
# 4. The check
git log --oneline develop..main
(empty: everything in main is in develop)
# 5. The incorrect version, on a copy
cp -r /tmp/gitflow /tmp/gitflow-wrong && cd /tmp/gitflow-wrong
git switch -q main
git switch -qc hotfix/2.0.2
echo "// fixed: due date with time zone" >> app.js
git commit -qam "Fix the time zone of the due date"
git switch -q main
git merge -q --no-ff hotfix/2.0.2 -m "Merge branch hotfix/2.0.2"
git tag -a v2.0.2 -m "Version 2.0.2"
git branch -qD hotfix/2.0.2          # without merging into develop!
git log --oneline develop..main
d4c1f8a Merge branch hotfix/2.0.2
9b2e7a3 Fix the time zone of the due date

The check detects the orphaned commit: it exists in main and not in develop. The next version would reintroduce the bug.

# 6. The fix
git switch -q develop
git cherry-pick 9b2e7a3
git log --oneline develop..main

Now only the merge commit is left, which belongs to main and does not need propagating.

Solution 3:

cd /tmp/gitflow

# 1. Support branch from the old tag
git switch -qc support/1.4 v1.4.0

# 2. Bring over the fix (the hash of the hotfix's fix commit)
FIX=$(git log --format=%h --all --grep='bulk delete that affected' -1)
git cherry-pick "$FIX"
# 3. Tag it
sed -i 's/Version: 1.4.0/Version: 1.4.1/' README.md
git commit -qam "Bump the version to 1.4.1"
git tag -a v1.4.1 -m "Version 1.4.1: fix the bulk delete"
# 4. Inventory of versions
git tag -l 'v*' --sort=-v:refname
git describe --contains "$FIX"
v2.0.1
v1.4.1
v2.0.0
v1.4.0
# 5. Complete topology
git log --graph --oneline --all --decorate

You can see the three live lines: main with its tagged versions, develop with the 2.1 work, and support/1.4 hanging off the old tag.

Conclusion

Git Flow is the most structured branching model of those we shall see, and its internal logic is impeccable for the problem it solves. The essentials:

  • Five classes of branch: main (released, tagged versions only), develop (integration of what is finished), feature/* (from develop to develop), release/* (from develop to main and develop) and hotfix/* (from main to main and develop).
  • develop is integrated, not guaranteed deployable. Stabilisation happens on the release branch, and that is the fundamental difference from the models we shall see next.
  • The release branch is the key piece: it freezes the scope and frees develop immediately, making it possible to stabilise without stopping the team.
  • The double integration of release/* and hotfix/* is not a whim: without it, the fixes exist in main but not in develop and the bugs reappear in the next version. Automate the check with git log --oneline develop..main.
  • Hotfixes are born from main because what has to be fixed is what is in production, not what is half-finished in development.
  • Annotated tags go exclusively on the merge commits in main, and they fit naturally with SemVer: release/* bumps minor/major, hotfix/* bumps patch.
  • git flow is syntactic sugar, not new capability. Handy for not forgetting the double integration, awkward with pull requests.
  • It fits software with explicit versions, several maintained versions, planned releases and manual QA: installable products, mobile, libraries, embedded.
  • It does not fit continuously deployed web software with a single live version, as Driessen himself acknowledged in his 2020 note, where he recommends simpler models for that case.

That note is precisely the bridge to what comes next. If your product is a web application deployed several times a day, do you really need two permanent branches, release branches and a stabilisation phase? The answer from the model we are about to see is a resounding no: a single long-lived branch, always deployable, and short branches that come in by pull request. That is the content of lesson 07-04: GitHub Flow.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved