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
- The problem Git Flow solves
- The five classes of branch
- The complete cycle at a glance
developandmain: the two permanent branches- Feature branches: opening and closing
- Release branches: stabilising without blocking
- Hotfix branches: why they merge in two places
- Where the tags fit in
- The
git flowtool - When Git Flow makes sense
- The criticisms, and the author's own note
- 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.
- The five classes of branch
The model defines five, split into two groups.
Permanent branches (they always exist, they are never deleted):
main(calledmasterin the original article): contains released code and nothing else. Every commit onmainis 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:
hotfix/*is born frommain, not fromdevelop. 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.release/*andhotfix/*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 prefixesfeature/,fix/,docs/andhotfix/(lesson 03-06). Git Flow proposesfeature/,release/andhotfix/. 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.
- 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:
mainis a short line of tagged milestones. Five commits a year, perhaps. Each one a version.developis the line of continuous work, where everything finished flows in.- Feature branches leave and return to
develop. - Release branches leave
developand flow into both permanent branches. - The hotfix branches leave
mainand 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.
develop and main: the two permanent branches
develop and main: the two permanent branchesmain 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.
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:
developis 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:
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.
- 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-exportIt 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:
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-exportThe --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 developgives 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 safeThe 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.
- 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.
developis 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.0Over 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.0The 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.0Step 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:
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.
- 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.1And 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.1The 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-tagsThis 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.
- Where the tags fit in
The tags from lesson 05-05 have an exact, unambiguous place in Git Flow:
Every merge commit on
mainreceives 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 maindoes not take them. Use--follow-tags(it pushes annotated tags reachable from what you are pushing) orgit 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
- The
git flow tool
git flow toolThere 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 ChocolateyThe 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 = vAdvantages: 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 finishmerges 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 publishis used and then an ordinary PR, ignoringfinish. - 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.
- 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-managerthe 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
developis a copy ofmainone 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.
- 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:
- Initialise the repository with
index.html,app.jsandREADME.md, and tag that state asv1.4.0onmain. - Create
developfrommain. - Develop
feature/csv-exportwith two commits and integrate it intodevelopwith--no-ff. - Develop
feature/alphabetical-orderwith one commit and integrate it the same way. - Open
release/2.0.0, bump the version number inREADME.mdand fix a bug. - Close the release: merge into
main, tagv2.0.0as an annotated tag, merge intodevelopas well and delete the branch. - 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:
- Add one more feature to
develop(simulating the 2.1 work). - Open
hotfix/2.0.1frommain, fix a bug and bump the patch version. - Close it correctly:
mainwith the tagv2.0.1, anddevelop. - Write a command that checks nothing is left in
mainunpropagated todevelop. - Repeat the exercise deliberately wrongly (without merging into
develop) on a copy of the repository and observe what that check detects. - Fix the situation with
git cherry-pick.
Exercise 3: maintaining an old version
- Create a support branch
support/1.4from thev1.4.0tag. - Bring the hotfix from the previous exercise onto that branch with
git cherry-pick. - Tag the result as
v1.4.1. - List all the tags sorted by version and check with
git describe --containswhich version each fix went into. - 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.0Solution 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# 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..mainThe check detects the orphaned commit: it exists in main and not in develop. The next version would reintroduce the bug.
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"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/*(fromdeveloptodevelop),release/*(fromdeveloptomainanddevelop) andhotfix/*(frommaintomainanddevelop). developis 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
developimmediately, making it possible to stabilise without stopping the team. - The double integration of
release/*andhotfix/*is not a whim: without it, the fixes exist inmainbut not indevelopand the bugs reappear in the next version. Automate the check withgit log --oneline develop..main. - Hotfixes are born from
mainbecause 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 flowis 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
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
