The previous lesson ended with the note Vincent Driessen himself added to his article ten years later: for continuous web development, Git Flow is too much. This lesson explains what lies on the other side of that recommendation.
GitHub Flow is the minimalist model. It grew out of GitHub's internal practice and was formalised by Scott Chacon in 2011 in an article titled, without ceremony, "GitHub Flow", with a premise that was almost a provocation at the time: for many projects, a single long-lived branch is enough.
And it is worth understanding where that claim comes from. It is not simplifying for the sake of convenience. It is that Git Flow solves a problem — coordinating several released versions and a stabilisation phase — that many projects do not have. If your application is deployed from a server you control, if there is only ever one version in production at a time, and if you can release a fix twenty minutes after writing it, then develop, release/* and hotfix/* are solving nothing for you: they are charging you complexity in exchange for nothing.
In task-manager the two realities coexist. The company sells the installable version — and for that one they keep Git Flow, as we saw — but it also runs a cloud version, app.example.com, which they deploy themselves several times a week. For that second line, the team has decided to try GitHub Flow.
Contents
- The idea in one sentence
- The six rules
- The complete cycle, with a graph
- The constraint that holds everything up:
mainalways deployable - What it demands in return
- The merge method: merge commit, squash or rebase
- Versions without release branches: tags on
main - Protected branches: the mechanism that enforces the flow
- When GitHub Flow fits
- Its limits
- The idea in one sentence
There is a single long-lived branch,
main, which is always in a deployable state. Everything else is short branches that come offmain, get reviewed in a pull request and go back intomain.
That is the entire model. There is no develop, no release/*, no hotfix/*. An urgent fix does not need a class of branch of its own because every branch is equally fast: it comes off main, gets reviewed, goes in and gets deployed. The distinction between "urgent fix" and "ordinary feature" disappears from the branching model and becomes, at most, a label on the ticket.
The comparison of topologies, without yet going into the full comparison we shall make in lesson 07-05:
| Git Flow | GitHub Flow | |
|---|---|---|
| Permanent branches | 2 (main, develop) |
1 (main) |
| Classes of supporting branch | 3 (feature, release, hotfix) |
1 (a working branch) |
| Rules of origin and destination | 5 different combinations | 1: from main to main |
| Double integrations | Yes (release, hotfix) |
None |
And a consequence you notice from day one: it is impossible to forget the second merge, because it does not exist.
- The six rules
Chacon stated them like this. We reproduce them with what each one means in practice.
Rule 1: anything in main is deployable
Not "it compiles". Not "it's nearly there". Deployable: if somebody deploys main to production right now, the result is correct. It is the rule the other five depend on, and we devote the whole of section 4 to it.
Rule 2: create descriptively named branches off main
Always from main, always up to date. The name should explain the work to somebody who only sees the list of branches: follow the conventions of lesson 03-06 — feature/, fix/, docs/ — and avoid tests, temp or anas-branch.
Rule 3: push to the named branch constantly
Not once at the end: often. Three reasons, and all three are practical:
- Backup. If Bruno's laptop dies this afternoon, the work is on the server.
- Visibility. The rest of the team sees what is being worked on and does not duplicate effort.
- CI runs on every push (lesson 07-06). A failure found after ten minutes costs minutes; the same failure found three days later costs hours of rebuilding the context.
Rule 4: open a pull request at any time
At any time, including the first day and the first commit. This rule baffles a lot of people: why open a PR for something unfinished?
Because the PR is not just a request to merge: it is where the conversation lives (lesson 07-01). Opening it early lets you ask for an opinion on the approach before investing three days, leave a record of what you are on, have CI test it, and let somebody tell you "Carla already did that somewhere else" before it is too late. That is what draft PRs are for.
Rule 5: merge only after review
Nothing goes into main without another person having looked at it (lesson 07-02). And without CI being green. These two requirements do not rely on goodwill: they are imposed technically with protected branches (section 8).
Rule 6: deploy immediately after merging
Merging into main and deploying are, ideally, the same act. The shorter the gap between the two, the better: if something goes wrong, you know exactly which change caused it, because only one has gone in.
A frequent variant in practice inverts the order: deploy the branch first and merge afterwards, once you have checked that it works in production. It is what GitHub does internally. It has the advantage that
mainnever comes to contain anything broken, and the cost of needing infrastructure to deploy arbitrary branches. The mechanics of that deployment — environments, promotion, rollback — are the subject of lesson 10-05; here we stay with the part that concerns Git.
- The complete cycle, with a graph
gitGraph commit id: "C1" tag: "deploy" branch feature/filter-by-label checkout feature/filter-by-label commit id: "F1" commit id: "F2" checkout main merge feature/filter-by-label tag: "deploy" branch fix/focus-after-delete checkout fix/focus-after-delete commit id: "B1" checkout main merge fix/focus-after-delete tag: "deploy" branch feature/csv-export checkout feature/csv-export commit id: "E1" commit id: "E2" checkout main merge feature/csv-export tag: "deploy"
Compare it mentally with the graph in the previous lesson. There is no twin track here, no bridges between permanent branches, no stabilisation phase. A straight line with short branches hanging off it, and a deployment at every meeting point.
Ana's complete cycle, from start to finish:
# 1. Start from an up-to-date main
git switch main
git pull
# 2. A descriptively named branch
git switch -c feature/filter-by-label
# 3. Work and push often
git commit -am "Add the label selector to the top bar"
git push -u origin feature/filter-by-label
# ... more commits, more pushes
# 4. Open the PR (as a draft if it is not ready yet)
# 5. Review: apply the comments on the same branch
git commit -am "Keep the filter when the page is reloaded"
git push
# 6. CI green + approval -> merge from the platform
# 7. Clean up
git switch main
git pull
git branch -d feature/filter-by-labelOne detail that matters more than it seems: the branch should last days, not weeks. If a GitHub Flow branch has been open for three weeks, the model has stopped working: you have rebuilt a long-lived branch under another name, and with it all the late-integration problems we criticised in the previous lesson. Lesson 07-05 will take this idea to its extreme.
Keeping the branch up to date
Since main moves on while you work, and the branch is short, this is usually trivial:
git switch feature/filter-by-label
git fetch origin
git rebase origin/main # linear history
# or
git merge origin/main # no rewritingMany teams configure the platform to require the branch to be up to date with main before it can be merged. It is an additional guarantee (CI will have tested the real combination) in exchange for having to update the branch whenever somebody gets in ahead of you. In teams with a lot of activity, that requirement creates a continuous race that is solved with a merge queue, which we shall see in lesson 07-06.
- The constraint that holds everything up:
main always deployable
main always deployableThis is the section to really understand. The rest is procedure.
GitHub Flow looks simpler than Git Flow because it has fewer branches. But it is not free: it has swapped process complexity for a very strong constraint. Git Flow let you have develop broken for three days because stabilisation happened afterwards, on the release branch. GitHub Flow removes that safety net. There is no release branch. There is no subsequent QA phase. main is what gets deployed, full stop.
Think about what that implies:
- If
mainis broken, nobody can deploy. Neither the person who broke it nor anybody else. The whole team is blocked. - If
mainis broken and it goes unnoticed, it gets deployed broken. - And if
mainis broken, you cannot release an urgent fix either, because the fix would go out accompanied by whatever is broken.
Hence this model's most important informal rule: fixing main is the team's top priority, ahead of any feature. If main's CI goes red, whatever is being done stops until it is green again. Many teams apply an automatic revert policy: if main breaks and is not fixed within ten minutes, the guilty commit is reverted (git revert, lesson 05-06) and it gets investigated calmly on a branch. Revert first, understand later.
And another, less obvious consequence: the size of each deployment drops a great deal. If you deploy after every PR, each deployment contains one change. When something fails in production, the list of suspects has one item on it. Compare that with deploying a quarterly release with a hundred and twenty changes inside it: when something goes wrong, the archaeology begins. That reduction of risk per deployment is the model's real benefit, and it is what compensates for the constraint.
- What it demands in return
The constraint of rule 1 does not hold itself up. It demands three things, and if any of them is missing the model does not work: it degrades into "we push to main and pray".
Demand 1: reliable automated tests
This is the indispensable condition. Nobody can manually guarantee that main is deployable after every merge: you need a test suite that runs on every push and every PR (lesson 07-06).
And reliable is the key word. A suite that fails randomly one run in ten — what is called a flaky test — is worse than having no tests, because it teaches the team to ignore failures. The moment somebody says "run it again, it fails sometimes", the signal is dead and with it the guarantee that main is deployable.
Demand 2: small pull requests
We already quantified this in lesson 07-02: above a few hundred lines, review stops finding defects. In a model where review is the last barrier before production, that is risk, plain and simple.
Besides, a small PR gets reviewed the same day, merged the same day and deployed the same day. A thousand-line PR waits three days for somebody to have a gap, and in those three days it accumulates conflicts.
Demand 3: frequent integration
Branches of days, not weeks. It is the same idea as the previous demand seen from the angle of time rather than size: the longer a branch lives apart, the more it diverges, the more conflicts it generates and the longer the value takes to reach the user.
And if the work is genuinely large and cannot be broken up into small PRs? There is an answer — feature flags and branch by abstraction — and it is one of the central topics of lesson 07-05.
The trap of adopting the model without the demands
It is worth saying plainly, because it is the most common failure: a team reads that GitHub Flow is "simpler", removes develop and the release branches, and does not invest in automated tests. The result is not simplicity: it is that main is broken half the time and nobody dares to deploy. They have taken away Git Flow's safety net without building GitHub Flow's.
GitHub Flow is not Git Flow minus branches. It is Git Flow minus branches plus automation.
- The merge method: merge commit, squash or rebase
When closing a pull request you have to decide how those commits go into main. The platforms offer three options, and the choice determines what the project's history will look like for ever.
You already know all of this from modules 3 and 5: here we just look at what each option produces in the context of a PR.
Option A: an ordinary merge (Create a merge commit)
Equivalent to git merge --no-ff (lesson 03-03). All the branch's commits are preserved and a merge commit is added.
gitGraph commit id: "C1" branch branch checkout branch commit id: "F1" commit id: "F2" commit id: "F3" checkout main merge branch id: "M"
Option B: Squash and merge
Equivalent to the merge --squash of lesson 03-04. All the branch's commits are condensed into a single one on main, and the original branch disappears from the history.
gitGraph commit id: "C1" commit id: "F (all squashed)"
Option C: Rebase and merge
Equivalent to a git rebase (lesson 05-01) followed by a fast-forward. The commits are rewritten on top of the tip of main, in a straight line and with no merge commit.
gitGraph commit id: "C1" commit id: "F1'" commit id: "F2'" commit id: "F3'"
The comparison
| Aspect | Merge commit | Squash | Rebase and merge |
|---|---|---|---|
Commits in main per PR |
All of them + 1 merge | Exactly 1 | All of them |
| History | With branching | Linear | Linear |
| Can you see which commits were one PR? | Yes, from the merge commit | Yes, it is a single commit | No, they blend in |
git log --first-parent |
One line per PR: an excellent summary | The same as git log |
Adds nothing |
git bisect (06-02) |
May land on broken intermediate commits | Optimal: each step is a complete PR | May land on broken intermediate commits |
| Reverting the whole PR | git revert -m 1 <merge> |
git revert <commit>, trivial |
You have to revert N commits |
git blame (06-03) |
Points at the original commit, with its context | Points at the squashed commit: the detail is lost | Points at the original commit |
| Historical detail | Maximum | Minimum: the step by step is lost | Maximum |
| Rewrites hashes | No | Yes | Yes |
| Demands discipline in the commits | Yes: they stay on show | No: the tidying is automatic | Yes |
Practical observations on each one:
Squash is the most popular in teams using GitHub Flow, and for a very specific reason: it absorbs untidy commits. The branch's wip, fixed and now it works disappear and what is left in main is one clean line per integrated change. The price is real: you lose the detail of how the change was built, and in a large PR the resulting commit is an enormous block that git blame cannot break down. It is the right choice if the PRs are small; if they are large, squashing makes things worse.
The ordinary merge preserves everything, but it only adds value if the branch's commits were well built (lesson 05-02). If they were not, it pollutes main with permanent noise. Its great advantage is git log --first-parent main: a perfect summary of the project, one line per PR.
Rebase and merge gives a linear history with no merge commits, but it completely loses the grouping: there is no way to know which commits arrived together, and reverting "that PR" means reverting N commits by hand. It is the least used of the three.
And now the important point:
Choose one and apply it always. A repository where every PR is merged with a different method has a history that cannot be read consistently:
--first-parentmeans nothing,bisectgives uneven results and nobody knows what to expect. The specific decision and its consequences for the history are the content of lesson 08-02: Keeping a Clean History, where we shall deal with history policy in full. Here it is enough to know that the decision exists, that it is taken once, and that the platforms let you disable the methods you do not want so that nobody presses the wrong button.
- Versions without release branches: tags on
main
mainWith no release/*, how do you know what was released and when? With tags on main (lesson 05-05).
The idea is simple: every deployment to production gets tagged. The tag does not change what is in the repository; it adds a stable name to a commit so that you can come back to it.
# An annotated tag on the deployed commit
git switch main
git pull
git tag -a v2.4.0 -m "Version 2.4.0: filter by label and CSV export"
git push origin v2.4.0Since there is no stabilisation phase, many teams that deploy frequently abandon SemVer for the application — there is not much point deciding whether a deployment is a minor or a patch when there are four a day — and use date-and-sequence tags:
| Scheme | Example | When to use it |
|---|---|---|
| SemVer | v2.4.0 |
Libraries, public APIs, anything with a compatibility contract |
| Date + sequence | deploy-2026-08-01.1 |
Web applications with several deployments a day |
| Incrementing number | build-1482 |
When the number is generated by CI itself |
What is worth keeping is SemVer for libraries, and ui-components is exactly that case: other projects depend on it and need to know whether a version breaks compatibility.
Tags are also the natural mechanism for three everyday things:
# What went in between the last two deployments?
git log --oneline v2.3.0..v2.4.0
# Which deployment did this commit go out in?
git describe --contains <hash>
# Go back to a deployment's exact state to debug it
git switch --detach v2.3.0And a third piece the platforms offer: deployment environments, a record of which commit is live in each environment. It is useful, but managing it is the subject of lesson 10-05; from Git's point of view, what there is is a tag and a reference.
And if something urgent has to be fixed?
This is where GitHub Flow shines, because it needs no special mechanism. The urgent fix follows exactly the same path as any other change:
git switch main && git pull
git switch -c fix/task-loss-when-filtering
# fix it
git commit -am "Fix the loss of tasks when two filters are applied"
git push -u origin fix/task-loss-when-filtering
# PR, quick review, CI, merge, deployTwenty minutes from start to finish. In Git Flow that same thing required a class of branch of its own, two merges and a patch tag. Deployment speed makes hotfix/* unnecessary: when deploying costs minutes, every fix is a hotfix.
- Protected branches: the mechanism that enforces the flow
Everything above is agreement. And as we saw in lesson 06-01 when talking about client-side hooks, agreements that depend on human discipline fail on a Friday at seven in the evening.
The piece that turns GitHub Flow into something real is the protected branch: a set of rules the server applies to a specific branch and that nobody can bypass from their laptop.
The usual protections on main:
| Protection | What it prevents | Which rule of the flow it upholds |
|---|---|---|
| Forbid direct pushing | git push origin main from a laptop |
Rules 4 and 5: everything goes through a PR |
| Require N approvals | Merging without review | Rule 5 |
| Require green checks | Merging with CI red | Rule 1: main deployable |
Require the branch to be up to date with main |
Merging without having tested the real combination | Rule 1 |
| Dismiss approvals when new commits arrive | Approving one version and merging another | Rule 5 |
| Forbid force pushes | git push --force onto main |
The golden rule (05-06) |
| Forbid deleting the branch | Deleting main by accident |
Common sense |
| Require conversations to be resolved | Merging with blocking comments still open | Rule 5 |
When Ana tries to push directly to a protected main, the rejection comes from the server:
remote: error: GH006: Protected branch update failed for refs/heads/main. remote: error: Required status check "tests" is expected. Changes must be remote: made through a pull request. To git@git.example.com:team/task-manager.git ! [remote rejected] main -> main (protected branch hook declined) error: failed to push some refs to 'git.example.com:team/task-manager.git'
Look at the last line in brackets: protected branch hook declined. That is literally a server hook rejecting the update of the reference. It is the pre-receive/update family that lesson 06-01 announced, and it is also the answer to the question left open there: this cannot be bypassed with --no-verify, because --no-verify only affects the hooks that run on your machine. Here the server decides.
We shall develop it in full in lesson 07-06, including how a status check becomes a mandatory requirement. For now, the idea to hold on to: GitHub Flow with protected branches is a process; GitHub Flow without them is a recommendation.
A note on organisational design: almost every platform allows certain roles to bypass the protections. It is tempting to leave that door open "just in case", but it is worth thinking twice. The exception always gets used at the worst possible moment — a production incident, in a hurry and short of sleep — which is exactly when the checks are most needed.
- When GitHub Flow fits
| Condition | Why it fits |
|---|---|
| A web application or SaaS | You control the deployment; there are no versions in the user's hands |
| A single version in production | There is nothing to maintain in parallel, so develop is surplus |
| Frequent, cheap deployment | It makes hotfix/* unnecessary: every fix arrives in minutes |
| Solid automated tests | It is the indispensable condition of rule 1 |
| A small or medium team | Few simultaneous branches, few integration conflicts |
| An established review culture | The PR is the only barrier before production |
Typical examples: internal web applications, SaaS, APIs deployed by the team itself, static sites, internal tools. And the specific case in this course: app.example.com, the cloud version of task-manager.
It is also, by a distance, the most used model today, and it is the one you will find by default in most open source projects: an outside contributor makes a fork (lesson 07-01), opens a branch, sends a PR against main and that is that. No develop, no release branches, no more protocol than necessary.
- Its limits
Being honest about the limitations is what allows you to choose well.
Limit 1: it is no good for maintaining old versions
This is the fundamental limit. If you have clients on 1.4 and on 2.0 and both need security fixes, a single long-lived branch cannot represent two simultaneous production states. There is no way to fix 1.4 from main, which is already on 2.0.
The practical solution is a hybrid: GitHub Flow for day-to-day development plus maintenance branches created from the tags whenever they are needed:
# Only when a client needs it
git switch -c support/1.4 v1.4.0
git cherry-pick <security-fix>
git tag -a v1.4.3 -m "Version 1.4.3: security patch"
git push -u origin support/1.4 --follow-tagsIt is exactly what the task-manager team does for the installable version. And note the nuance: it is not "switching to Git Flow", it is adding the specific missing piece when it is needed, which is always better than adopting a whole model up front.
Limit 2: it demands technical maturity that not every team has
Without reliable tests and without fast CI, the model degrades quickly. A team without that foundation is safer with Git Flow, where the release branch gives a margin for manual stabilisation. It is an uncomfortable but true conclusion: the simpler model demands more maturity, not less.
Limit 3: it says nothing about what to do with large work
A database migration, a rewrite of the interface, an architectural change. They do not fit into a three-day branch and they cannot be integrated half-finished into a main that has to be deployable. GitHub Flow offers no answer; it has to be brought in from outside, and that answer — feature flags and branch by abstraction — is one of the topics of the next lesson.
Limit 4: regulated environments, or ones with formal approval
If every release requires a quality manager's sign-off, formal documentation or an authorised deployment window, the "merge and deploy" of rule 6 is not applicable as it stands. It can be adapted (integrate continuously, deploy in windows), but then main accumulates undeployed changes and part of the benefit is lost.
Common Mistakes and Tips
Mistake 1: adopting GitHub Flow without automated tests. Git Flow's net is taken away without building your own. main ends up broken half the time.
Mistake 2: three-week branches. That is a long-lived branch under another name, with all the late-integration problems the model was meant to avoid.
Mistake 3: leaving main broken and carrying on working. It blocks the whole team, urgent fixes included. Fixing main is the absolute priority; if it takes more than a few minutes, revert.
Mistake 4: not protecting main. Without protected branches the model is a recommendation that somebody will bypass. With them it is a process.
Mistake 5: mixing merge methods. Some PRs with squash, others with a merge commit, others with rebase. The resulting history cannot be read consistently.
Mistake 6: squashing enormous PRs. Squash works with small PRs. Squashing a thousand lines produces a commit that git blame cannot break down and that nobody will be able to understand a year from now.
Mistake 7: not tagging the deployments. Without tags there is no way to know what was in production last Tuesday, nor to go back to it to debug.
Mistake 8: creating the branch without updating main first. git switch main && git pull first, always. Starting from a week-old main generates avoidable conflicts.
Mistake 9: living with unstable tests. A test that fails one time in ten teaches the team to ignore the red, and with that the guarantee of rule 1 dies. Fix it or remove it, but do not ignore it.
Tip 1: PRs of under 200 lines. It is what has the biggest impact on the speed and quality of the flow (lesson 07-02).
Tip 2: open the PR on day one, as a draft. CI starts testing sooner and somebody can correct your course in time.
Tip 3: delete the branch when merging. Turn it on in the platform and use git fetch --prune locally. A repository with two hundred dead branches is unusable.
Tip 4: disable in the platform the merge methods you do not use. It is more reliable than trusting that nobody presses the wrong button.
Tip 5: measure the lifetime of your branches. If the average is over a week, you are not doing GitHub Flow, whatever you call it.
Tip 6: add maintenance branches only when a real client needs them. Do not adopt the whole of Git Flow for a problem you may never have.
Exercises
Exercise 1: the GitHub Flow cycle
On a new repository with index.html, app.js and styles.css:
- Create
mainwith three initial commits and tag it asv2.3.0. - Simulate three complete cycles: a branch from
main, two commits, back tomainwith--no-ff, delete the branch and add a deployment tag. - Use branch names following the project's conventions: one
feature/, onefix/and onedocs/. - Show the graph and check that it resembles the one in section 3.
- Run
git log --oneline --first-parent mainand explain what information it gives you.
Exercise 2: comparing the three merge methods
- Create a branch
feature/exportwith four commits, two of them clearly noise (wip,fixed). - Make three copies of the repository at
/tmp/method-merge,/tmp/method-squashand/tmp/method-rebase. - In each copy, integrate the branch with the corresponding method (
merge --no-ff,merge --squash+ commit,rebase+merge --ff-only). - Compare across the three:
git log --oneline,git log --oneline --first-parentand the number of commits inmain. - In each copy, try to revert the whole feature with a single command. In which is it possible?
- Run
git blame app.jsin all three and comment on the differences.
Exercise 3: the hotfix simulation
- Starting from the exercise 1 repository, simulate a production bug: create
fix/task-lossfrommain, fix it, merge it and tag the deployment. - Count how many commands were needed from the initial
switchto the tag. - Compare that number with the equivalent
hotfix/*procedure in Git Flow (lesson 07-03, section 7). How many steps have you saved, and which Git Flow step simply does not exist here? - Now the other side: simulate that a client is still on
v2.3.0and needs that same fix without receiving anything else. Createsupport/2.3from the tag, bring the fix over withcherry-pickand tagv2.3.1. - Explain why step 4 is not part of GitHub Flow and what that tells you about when the model falls short.
Solutions
Solution 1:
mkdir /tmp/github-flow && cd /tmp/github-flow
git init -qb main
printf '<h1>Task Manager</h1>\n' > index.html
git add . && git commit -q -m "Add the main page"
printf 'const tasks = [];\n' > app.js
git add . && git commit -q -m "Add the initial application state"
printf 'body { margin: 0; }\n' > styles.css
git add . && git commit -q -m "Add the base stylesheet"
git tag -a v2.3.0 -m "Version 2.3.0"# Cycle 1
git switch -qc feature/filter-by-label
echo "function filterByLabel(l) { /* ... */ }" >> app.js
git commit -qam "Add filtering by label"
echo ".filter { display: flex; }" >> styles.css
git commit -qam "Style the filter bar"
git switch -q main
git merge -q --no-ff feature/filter-by-label -m "Merge feature/filter-by-label (#101)"
git branch -qd feature/filter-by-label
git tag -a deploy-2026-08-01.1 -m "Deployment to production"# Cycle 2
git switch -qc fix/focus-after-delete
echo "// fixed: return the focus to the field after deleting" >> app.js
git commit -qam "Return the focus to the input field after deleting"
git switch -q main
git merge -q --no-ff fix/focus-after-delete -m "Merge fix/focus-after-delete (#102)"
git branch -qd fix/focus-after-delete
git tag -a deploy-2026-08-01.2 -m "Deployment to production"# Cycle 3
git switch -qc docs/installation
printf '# task-manager\n\n## Installation\n\nOpen index.html.\n' > README.md
git add . && git commit -q -m "Document the installation in the README"
git switch -q main
git merge -q --no-ff docs/installation -m "Merge docs/installation (#103)"
git branch -qd docs/installation
git tag -a deploy-2026-08-02.1 -m "Deployment to production"c8a2f1e Merge docs/installation (#103) 9d4b7c3 Merge fix/focus-after-delete (#102) 2f8e1a6 Merge feature/filter-by-label (#101) 7b3c9d2 Add the base stylesheet 4a1e6f8 Add the initial application state 1c5d2b9 Add the main page
--first-parent gives one line per merged PR: it is the project's readable summary. That is the main benefit of merging with an ordinary merge commit.
Solution 2:
cd /tmp/github-flow
git switch -qc feature/export
echo "function exportTasks() { /* ... */ }" >> app.js
git commit -qam "Add the skeleton of the export"
echo "// wip" >> app.js
git commit -qam "wip"
echo "function generateCSV() { /* ... */ }" >> app.js
git commit -qam "Generate the CSV content"
echo "// separator fixed" >> app.js
git commit -qam "fixed"
git switch -q main# Ordinary merge
cd /tmp/method-merge
git merge --no-ff feature/export -m "Merge feature/export (#104)"
git log --oneline main | head -6# Squash
cd /tmp/method-squash
git merge --squash feature/export
git commit -q -m "Add the CSV export (#104)"
git log --oneline main | head -6# Rebase
cd /tmp/method-rebase
git switch -q feature/export
git rebase -q main
git switch -q main
git merge -q --ff-only feature/export
git log --oneline main | head -6# 4. Comparison
for m in merge squash rebase; do
echo "== $m: $(git -C /tmp/method-$m rev-list --count main) commits in main"
git -C /tmp/method-$m log --oneline --first-parent -3 main
done# 5. Reverting the whole feature
git -C /tmp/method-merge revert -m 1 HEAD --no-edit # works
git -C /tmp/method-squash revert HEAD --no-edit # works
git -C /tmp/method-rebase revert HEAD~3..HEAD --no-edit # 4 reverts neededWith a merge commit and with squash, one command is enough. With rebase you have to revert each commit, because nothing indicates that they were a set.
# 6. blame
for m in merge squash rebase; do
echo "== $m"; git -C /tmp/method-$m blame --date=short -- app.js | tail -4
doneIn squash, every line of the feature points at the same commit and the same message: the information about which part was done at which step has been lost.
Solution 3:
cd /tmp/github-flow
git switch -q main
git switch -qc fix/task-loss
echo "// fixed: do not empty the array when combining two filters" >> app.js
git commit -qam "Fix the loss of tasks when two filters are applied
Refs GT-341."
git switch -q main
git merge -q --no-ff fix/task-loss -m "Merge fix/task-loss (#105)"
git branch -qd fix/task-loss
git tag -a deploy-2026-08-02.2 -m "Urgent deployment to production"Six commands. In Git Flow: create from main, fix, bump the patch version, merge into main, tag, merge into develop as well, delete in two places. The step that does not exist here is the second merge, and it is precisely the one most often forgotten and the most expensive when it is.
# 4. Maintaining an old version: the model's limit
FIX=$(git log --format=%h --grep='loss of tasks when two filters' -1)
git switch -qc support/2.3 v2.3.0
git cherry-pick "$FIX"
git tag -a v2.3.1 -m "Version 2.3.1: patch for clients on 2.3"
git switch -q main
git log --graph --oneline --all --decorate | head -20# 5.
# support/2.3 is a SECOND long-lived branch: it contradicts the premise
# of GitHub Flow. The model assumes a single version in production.
# As soon as there are two live production states, a piece is needed
# that the model does not have, and it has to be borrowed from Git Flow.Conclusion
GitHub Flow is the dominant model today, and its simplicity is real but conditional. The essentials:
- A single long-lived branch,
main, always deployable. Everything else is short branches that come offmain, get reviewed in a PR and go back intomain. There is nodevelop, norelease/*, nohotfix/*, and no double integrations to forget. - The six rules:
maindeployable; descriptive branches offmain; push constantly; open the PR at any time; merge only after review; deploy immediately afterwards. - "
mainalways deployable" is the constraint that holds the model up, and it is not free: ifmainbreaks, the whole team is blocked, urgent fixes included. Fixingmainis the absolute priority; if it drags on, revert. - In exchange for removing branches, the model demands reliable automated tests, small PRs and frequent integration. GitHub Flow is not Git Flow minus branches: it is Git Flow minus branches plus automation. Without that foundation, it degrades.
- The merge method — merge commit, squash or rebase — determines the history for ever: squash absorbs untidy commits and is optimal for
bisect, the merge commit preserves the detail and enables--first-parent, rebase loses the grouping. Choose one and apply it always; the complete policy is in lesson 08-02. - With no release branches, tags on
mainrecord what was deployed and when: SemVer for libraries such asui-components, date and sequence for applications with several deployments a day. - Urgent fixes need no mechanism of their own: when deploying costs minutes, every fix is a hotfix.
- Protected branches turn the agreement into a process: forbid direct pushing, require approvals and green checks. And that
protected branch hook declinedis a server hook, precisely what--no-verifycannot touch. - It fits web and SaaS with a single version in production, frequent deployment and good tests. Its limit is maintaining old versions: for that you need to add support branches from the tags.
There is one question this model leaves open. GitHub Flow asks for short branches, but how short? And what do you do with work that does not fit into three days? There is a school of thought that answers by taking the idea to its extreme: integrate into the trunk at least once a day, every person, always, and decouple deployment from release with feature flags so that this is possible even with half-finished work. That is the approach of lesson 07-05: Trunk Based Development, where we shall also finally compare the three flows in a single table and build a decision tree for choosing between them.
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
