At the close of module 9 we left Ana, Bruno, Carla and Diego with a firm command of Git on task-manager: they know how to build history, rewrite it with judgement, collaborate with a process and get out of trouble. But task-manager is four files and four people. The real world is bigger and stranger.
This lesson does not teach any new commands. It teaches something that at this point is worth more: judgement. We are going to look at how four very different kinds of project use Git — from the largest and oldest one to one exactly like yours — and to draw out of each what problem its workflow solves, what price it pays for it and what it takes to make it work.
Let me give away the conclusion we are after, because it is the only one worth taking home: there is no such thing as "the correct Git workflow". There is the workflow that fits the size of the team, the trust between its members, the release rhythm and the cost of getting things wrong. Copying the Linux kernel's workflow into a team of four is as absurd as coordinating the kernel through the pull requests of a web platform.
Everything described here is public and well documented: the kernel's workflows are written up in its own documentation, open source projects publish their contribution guides, and enterprise monorepo practices have been described in articles and technical talks for years. When I quote a magnitude, I will give it as an approximate order of magnitude, never as an exact figure: what matters is the scale, not the number.
Contents
- How to read a case study
- Case 1: the Linux kernel, the email workflow
- The maintainer hierarchy and the "tree of trees"
- Why the kernel does not use platform pull requests
- What the kernel model demands of a contributor
- Case 2: a medium-sized open source project
- The social infrastructure of an open project
- Case 3: a company with a monorepo
- Case 4: a small product team (ours)
- The four models, compared
- The cross-cutting lesson: a workflow is chosen, not inherited
- How to read a case study
Before looking at any project it is worth settling on the questions. Every Git workflow, however complex it may look, answers five questions:
| Question | What it determines |
|---|---|
| How does someone outside the core propose a change? | The proposal mechanism: email, pull request, direct branch |
| Who decides that a change goes in? | The authority structure: one maintainer, two reviewers, a directory owner |
| Which branch is the work done on, and for how long? | The branching model and its half-life |
| What does the resulting history look like? | Whether it is linear, whether it has merges, whether every commit builds |
| What does it take to make that work? | The infrastructure and the discipline required |
The fifth is the one that is almost always ignored, and the one that explains why a workflow copied from somewhere else fails. A workflow is not just a sequence of commands: it is a sequence of commands plus the tools, the rules and the habits that hold it up.
We are going to answer those five questions for each case.
- Case 1: the Linux kernel, the email workflow
The Linux kernel is the project Git was written for, in 2005, when the team lost access to the tool it had been using until then. It is also the project that still uses the oldest workflow and, for almost everybody, the most surprising one: changes are proposed by email, on public lists, in the form of text patches.
The magnitudes, as orders of magnitude: tens of millions of lines of code, tens of thousands of files, more than twenty years of history in Git (and more than thirty as a project), and on the order of thousands of people contributing each year. No small team looks anything like this, and that is precisely why the case is instructive: it shows what happens when you take Git collaboration to its limit.
The life of a patch
flowchart TD
A["Developer<br/>makes local commits"] --> B["git format-patch<br/>generates .patch files"]
B --> C["git send-email<br/>to the subsystem list"]
C --> D["Public review<br/>on the mailing list"]
D -->|"changes requested"| E["New version: v2, v3…"]
E --> C
D -->|"accepted"| F["Maintainer:<br/>git am applies the patch"]
F --> G["Subsystem tree"]
G --> H["git request-pull<br/>to the maintainer above"]
H --> I["Main tree (Linus)"]
Notice one important detail: the patch travels as text, not as a reference to a repository. Whoever receives it does not need to add a remote, or have access to the sender's repository, or even know whether that repository exists. They receive an email, and that email contains the entire change.
git format-patch: turning commits into emails
Each file is a complete email: headers, a subject taken from the first line of the commit message, the body of the message, and the diff at the end. If we apply this to our own project to see it from the inside:
From 8a1f6c3d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c Mon Sep 17 00:00:00 2001 From: Ana Ferrer <[email protected]> Date: Fri, 31 Jul 2026 13:11:58 +0200 Subject: [PATCH 1/2] GT-142 show the pending counter A counter is added in the header with the number of tasks that are not completed, recalculated whenever a task is ticked or unticked. Signed-off-by: Ana Ferrer <[email protected]> --- app.js | 12 ++++++++++++ index.html | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-)
That From 8a1f6c3d... on the first line is not a real email header: it is the hash of the original commit, which Git stores there so that whoever applies it can tell where it came from. The date Mon Sep 17 00:00:00 2001 is a historical sentinel value that Git always writes; it means nothing.
Options that genuinely get used:
# Number them and add a cover letter (0000-cover-letter.patch)
git format-patch main --cover-letter --numbered
# Mark this as the second version of the series after review
git format-patch main -v2
# Only the last three commits
git format-patch -3
# With the branch name in the subject, useful for long series
git format-patch main --subject-prefix="PATCH task-manager"The cover letter (--cover-letter) is the 0/N email of the series: it explains the set as a whole, not each individual patch. In the kernel it is where the case for the complete change is argued, and where what has changed since the previous version of the series is summarised.
git send-email: sending them
git send-email [email protected] \
[email protected] \
0001-*.patch 0002-*.patchgit send-email sends them as an email thread: the cover letter is the root message and the patches hang off it as replies. It requires an SMTP server to be configured:
git config --global sendemail.smtpServer smtp.example.com
git config --global sendemail.smtpUser [email protected]
git config --global sendemail.smtpEncryption tls
git config --global sendemail.smtpServerPort 587It is the command more people have installed and never actually used. But it is worth knowing: when somebody tells you "send me the patch", this is what they mean.
git am: applying the patch you receive
At the other end, the maintainer saves the email and applies it:
# Apply a series of patches in order
git am 0001-*.patch 0002-*.patch
# Apply straight from a mailbox
git am /path/to/mailbox.mbox
# Add my own sign-off when applying it (standard practice in the kernel)
git am --signoff 0001-*.patcham stands for apply mailbox. It creates one commit per patch preserving the original author and putting the maintainer down as committer. Here, for the first time, the author/committer distinction that appeared in the data model (lesson 01-04) makes practical sense: the kernel uses it constantly, because whoever writes the code and whoever integrates it are almost never the same person.
If a patch does not apply cleanly:
# See where it failed
git am --show-current-patch=diff
# Try applying with more leeway using the blob information
git am -3
# Fix by hand, mark as resolved and carry on
git add .
git am --continue
# Or abandon the whole series and go back to the previous state
git am --abortgit am -3 (three-way merge) is the one that rescues most cases: it uses the blob hashes the patch carries embedded in it to reconstruct the context and perform a real merge instead of a textual application. It is exactly the three-way mechanism of lesson 03-03, applied to a patch.
git request-pull: asking someone to integrate your tree
When a subsystem maintainer already has the patches in their repository and wants the level above to integrate them, they do not send patches: they send a pull request by email, generated by Git:
The following changes since commit 3f2a1b9c...:
Linux 6.10 (2026-06-14 18:02:11 -0700)
are available in the Git repository at:
https://git.example.com/network-subsystem.git for-6.11
for you to fetch changes up to 9d4e7f2a...:
net: add regression test (2026-07-28 11:40:03 +0200)
----------------------------------------------------------------
Ana Ferrer (2):
net: fix memory leak in the driver
net: add regression test
drivers/net/example.c | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)This is the original pull request, the real one: an email that says "in this repository, on this branch, from this base commit, there are these changes; come and get them". The web platforms we use every day did not invent the concept: they put an interface on top of it. We will come back to request-pull in lesson 10-02, as a bridge between the two worlds.
- The maintainer hierarchy and the "tree of trees"
The kernel does not have one repository: it has hundreds. Each subsystem — networking, filesystems, graphics, a particular processor architecture — has its own public repository and its own maintainer.
flowchart BT
D1["Contributor"] --> S1["A driver's tree"]
D2["Contributor"] --> S1
D3["Contributor"] --> S2["Another driver's tree"]
S1 --> M1["Subsystem tree<br/>(networking)"]
S2 --> M1
S3["Subsystem tree<br/>(filesystems)"] --> T["Main tree"]
M1 --> T
S4["Subsystem tree<br/>(graphics)"] --> T
T --> R["Release<br/>vX.Y"]
Information flows from the bottom upwards, on trust. Each level trusts the one below because it knows those people and has been reviewing their work for years, and answers to the one above for what it integrates. The main tree does not review every patch: it reviews pull requests from maintainers it trusts.
This has three technical consequences that are very visible in the history:
- The main history is full of merge commits between subsystem trees. It is not disorder: each merge documents "on this date I integrated this subsystem's work". The message of those merges is usually the text of the corresponding
request-pull. - Individual patches arrive already clean, because they have been discussed and rewritten on the list before entering any tree. Cleanliness is not achieved by rewriting afterwards: it is achieved by not letting the dirty stuff in.
- The rhythm is one of windows. There are periods in which the main tree accepts new changes and periods in which it only accepts fixes, closing with a release. That rhythm makes the work of every level below predictable.
Signed-off-by and the certificate of origin
Every kernel commit carries one or more lines at the end:
Signed-off-by: Ana Ferrer <[email protected]> Signed-off-by: Bruno Salas <[email protected]>
It is not a cryptographic signature (that is git commit -S, lesson 08-05). It is a legal declaration: whoever adds it certifies that they have the right to contribute that code under the project's licence. It is called the Developer Certificate of Origin, it is a short public text, and it is added with:
Each level of the hierarchy adds its own on integrating, so the commit ends up carrying the complete chain of custody: who wrote it and whose hands it passed through on its way to the main tree. It is social traceability written into the commit object itself.
- Why the kernel does not use platform pull requests
It is the obvious question. The answer is not "out of habit" or "out of resistance to change", although inertia does play its part. There are structural reasons:
| Reason | Explanation |
|---|---|
| Vendor independence | Depending on one particular platform means its policy, its availability and its price constrain the project. Email can be served by anybody. |
| Review is the product | On a list, the technical discussion is archived, it is quotable, it can be answered inline on the code itself, and it is read by people who were not following that change. It is review that is public by default, not private by default. |
| Scale of participants | Thousands of people reviewing hundreds of simultaneous series in parallel works better with email filters than with the notifications of a web interface. |
| Nobody needs permissions | To propose a change you do not need an account, or a fork, or anybody to grant you access to anything. Knowing the list address is enough. |
| The patch is self-contained | It can be applied, saved, forwarded, quoted and archived without depending on any server still existing fifteen years from now. |
And there are costs too, which are worth stating just as plainly:
- The barrier to entry is high. Configuring outgoing email so that it does not mangle patches is a rite of passage famous for how painful it is.
- There is no centralised state for "where has this proposal got to". External patch-tracking tools exist to soften that, but it is not the same as a dashboard.
- Continuous integration is harder to tie to a specific proposal, because the proposal is an email, not a branch on a server.
In recent years tools have appeared that automate the tedious parts (sending series, retrieving them from the public archives, managing successive versions) without abandoning email. The trend is not to replace the workflow, but to make it less abrasive.
- What the kernel model demands of a contributor
This is the point in the lesson where the case stops being a curiosity and starts being useful to you, even if you never send a patch to the kernel.
Demand 1: every commit must be correct on its own. Not "the set works": each commit builds and passes the tests, because git bisect (lesson 06-02) has to be able to stop at any one of them. This forces you to split the work into coherent steps, and it is what turns a series of patches into something reviewable.
Demand 2: the message reads on its own. A maintainer receives the patch in an email, with no access to your ticket, no context from you and no way to ask you in the corridor. The message is the documentation of the change. Everything we saw in lesson 08-01 — the why before the what, the imperative, the body that explains the alternative that was discarded — is not a good practice here: it is a functional requirement.
Demand 3: a series is a narrative. Patches are ordered so that each one prepares the next: first the refactoring that does not change behaviour, then the behaviour change, then the tests. It is exactly the use of interactive rebase from lesson 05-02, with an editorial purpose.
Demand 4: you accept rewriting. Version 5 of a series is normal. It is redone with rebase -i, resent with -v5, and the cover letter explains what changed relative to v4. That is what git range-diff (lesson 07-02) is for, which compares two versions of the same series.
Applied to our project, this is how Ana would prepare to send a series in this style:
# 1. Clean up the series: reorder, squash the "fix typo" commits, rewrite messages
git rebase -i main
# 2. Check that every commit builds, one by one
git rebase main --exec "npm test"
# 3. Generate v2 and compare it with the v1 I already sent
git format-patch main -v2 --cover-letter
git range-diff main v1-final mainThe --exec in the second step is a little-known gem: it runs that command after applying each commit of the rebase, and stops at the first one that fails. It is the mechanical way of guaranteeing demand 1.
- Case 2: a medium-sized open source project
Let us drop down a scale. We are talking about a project with hundreds of contributors, dozens of them regulars, a handful of maintainers, and a hosting platform (git.example.com in our fictional universe). A popular library, a framework, a well-known command line tool.
This is exactly Diego's model, the external contributor from lesson 07-01, multiplied by two hundred.
The workflow
flowchart LR
A["Personal fork"] --> B["Topic branch"]
B --> C["Pull request<br/>to the main repository"]
C --> D["Automated CI"]
C --> E["Human review"]
D --> F{"All green?"}
E --> F
F -->|yes| G["Squash and merge<br/>into main"]
F -->|no| B
None of this is new to you: it is the whole of module 7. What changes with scale is not the mechanics, but how much work the infrastructure does so that the maintainers do not drown.
The maintainer's arithmetic
The fact that explains everything else: in a project like this, review is the scarce resource. There are hundreds of people capable of writing a proposal and perhaps five with the right and the time to integrate it. Every minute a maintainer spends explaining how to run the tests, asking for a branch to be rebased or closing duplicate proposals is a minute not spent reviewing code.
Hence why the whole design of the process pursues one objective: that the proposal should arrive already in good shape.
- The social infrastructure of an open project
These are the concrete pieces, and they deserve attention because they are directly applicable to any repository, yours included.
CONTRIBUTING.md
A file at the root of the repository that platforms show automatically to anybody about to open a proposal. It contains what a maintainer is tired of repeating:
# How to contribute to task-manager
## Before you start
- Open an issue before writing code for a large change.
- Check whether an open proposal about the same thing already exists.
## Setting up the environment
git clone https://git.example.com/team/task-manager.git
npm install
npm test
## Conventions
- Commit messages: Conventional Commits (feat:, fix:, docs:…).
- One proposal, one topic. If your branch touches two things, split it in two.
- Rebase onto `main` before asking for review; do not merge `main` into your branch.
## What to expect
- CI must be green before we review.
- A maintainer will reply within a few working days.
- We integrate with squash: your branch becomes a single commit on `main`.Notice the last line. Saying up front which integration policy applies (merge, squash or rebase, lesson 08-02) heads off the most recurrent argument in proposals from newcomers.
Proposal and issue templates
Files inside a platform configuration directory that pre-fill the form. The proposal template is usually a checklist:
## What changes
<!-- Brief description of the change and the problem it solves -->
## Related ticket
Closes GT-
## Checks
- [ ] I have run `npm test` locally and it passes
- [ ] I have added tests for the new behaviour
- [ ] I have updated the README if the change affects usage
- [ ] My commits follow Conventional CommitsTheir real effect is not documentary, it is psychological: whoever opens the proposal sees the empty boxes and it occurs to them to go through them before being asked.
Labels
A well-thought-out label system turns an unmanageable list of issues into a navigable map:
| Label family | Examples | What it is for |
|---|---|---|
| Type | bug, enhancement, documentation |
Classifying at a glance |
| State | needs-review, awaiting-response, blocked |
Knowing whose turn it is |
| Difficulty | good-first-issue, help-wanted |
Channelling newcomers |
| Area | ui, storage, ci |
Routing to the right maintainer |
The good-first-issue label (the name most platforms have standardised on) is the most profitable of the lot: it is how a project turns readers into contributors.
Automating the repetitive parts
In a project on this scale, robots do the work that in task-manager a person does:
name: Proposal checks
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check the format of the commit messages
run: |
git log --format=%s origin/main..HEAD | while read -r subject; do
echo "$subject" | grep -qE '^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .+' \
|| { echo "Non-conforming message: $subject"; exit 1; }
done
- name: Verify that the branch is up to date with main
run: |
git merge-base --is-ancestor origin/main HEAD \
|| echo "::warning::Rebase onto main before asking for review"
- name: Tests
run: npm ci && npm testThat git merge-base --is-ancestor is a plumbing command (lesson 09-06) that answers with an exit code: is origin/main an ancestor of HEAD? If it is, the branch is up to date.
What history this model produces
Almost always linear on main, with one commit per integrated proposal, a normalised message and a reference to the proposal number. It is the result of the squash policy, and it has an enormous virtue at this scale: git log --oneline on main reads like a changelog for the project, not like the diary of two hundred people.
The price, already discussed in lesson 08-02, is that the internal detail of each proposal is lost. In an open project that is accepted quite happily, because that detail still exists in the proposal itself, archived on the platform.
- Case 3: a company with a monorepo
A third model, radically different: one company, one repository, many products inside it. The web application, the data service, the mobile app, the shared libraries, the infrastructure definitions: all in the same directory tree and the same history.
Several large companies have publicly described this model over the years. The magnitudes, always as an order of magnitude: hundreds of thousands to millions of files, millions of commits, and thousands of people working on the same repository. At that scale, Git out of the box is not enough, and that is the subject of lesson 10-04. What interests us here is the why, not the how.
Why anybody chooses this
Reason 1: the atomic change across projects. If the shared library changes its interface and there are thirty consumers, in a monorepo that is a single commit that updates the library and all thirty at once. There is never an instant in which the repository is inconsistent. With separate repositories, that same change is thirty-one coordinated proposals, intermediate compatibility versions and weeks of work.
This is exactly the problem we left open in lesson 06-05 with ui-components: when the submodule changes, the pointer has to be updated in the parent repository, and for a while the two things do not line up. In a monorepo that problem does not exist because there are not two repositories.
Reason 2: global refactoring is possible. Renaming a function used across the whole company is a search and replace plus a commit. With separate repositories, it is a project.
Reason 3: a single version of everything. There is no "which version of the library does the billing service use". It uses the one that is on main, like everybody else.
Reason 4: visibility and reuse. Anybody can read any code, find out who maintains it and propose a change.
The price
| Cost | How it is paid |
|---|---|
| Git out of the box drowns | Partial clones, sparse-checkout, specialised servers, build caches (10-04) |
| Everybody sees everything | Can be unacceptable if there is code with confidentiality requirements |
| CI cannot run everything | You need selective CI: working out what the change affects |
| In-house tooling | Build system with a dependency graph, code ownership tools, migration automation |
| Nobody can just clone and start | Onboarding newcomers requires training in the environment |
Code ownership by directory
The key organisational piece. A text file in the repository declares who has to approve changes in each zone of the tree:
# CODEOWNERS # Default rule: the platform team * @platform # Each team owns its directory /services/billing/ @billing-team /services/tasks/ @tasks-team /libraries/ui-components/ @design-team @tasks-team # Critical areas require additional approval /infra/production/ @platform @security /libraries/authentication/ @security
The platform reads that file, works out which directories each proposal touches and automatically assigns the mandatory reviewers. Combined with the protected branches of lesson 07-06, the result is that autonomy is recovered without giving up the single repository: each team is in charge of its own patch, but touching the authentication library requires security's sign-off.
It is the answer to the most frequent objection against the monorepo. "If everything is together, can anybody change anything?" Technically yes; in practice, not without the approval of whoever owns that zone.
Selective continuous integration
If the repository has a thousand projects, running the tests for all thousand on every proposal is impossible. The solution is to work out the affected set:
name: Selective CI
on: [pull_request]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
affected: ${{ steps.calc.outputs.list }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: calc
name: Which projects this proposal touches
run: |
# Files modified relative to the divergence point with main
CHANGES=$(git diff --name-only origin/main...HEAD)
# First directory level = project
PROJECTS=$(echo "$CHANGES" | cut -d/ -f1-2 | sort -u | tr '\n' ' ')
echo "list=$PROJECTS" >> "$GITHUB_OUTPUT"
test:
needs: detect
if: needs.detect.outputs.affected != ''
runs-on: ubuntu-latest
steps:
- run: echo "Testing only ${{ needs.detect.outputs.affected }}"That origin/main...HEAD with three dots is the same one from the code review of lesson 07-02: the branch's changes since it diverged, not the differences with the current state of main. It is a detail that matters a great deal here: with two dots, any advance in main would make the pipeline believe the proposal touches projects it has not touched.
In practice, a serious system does not use directories but the build system's dependency graph: if ui-components changes, every project that depends on it is tested as well. But the principle is the same, and it always starts with a git diff --name-only.
- Case 4: a small product team (ours)
A fourth case: four people, one product, a release every few weeks. That is, task-manager. It is also, by a wide margin, the most common case: the vast majority of teams in the world look far more like this one than like the previous three.
Recapping what this team has built over the course, without repeating the detail:
- One repository on
git.example.com, plusui-componentsas a submodule (06-05). - Short topic branches named after the ticket (
GT-231-hidden-filter), integrated intomainby way of a proposal and review (07-01, 07-02), with a workflow close to GitHub Flow (07-04). - Continuous integration on every proposal and a protected branch on
main(07-06). - Conventional Commits and an agreed integration policy (08-01, 08-02).
- Annotated tags with SemVer for releasing (05-05).
- Diego, an external contributor, works by fork and proposal (07-01).
What is interesting about this case is what it does NOT have, and does not need:
| Does not have | Why it does not need it |
|---|---|
| A maintainer hierarchy | Four people talk to each other; the hierarchy would be theatre |
| An email workflow | They all have write access to the same server |
CODEOWNERS |
Anybody can review any file, and it is healthy that they should |
| Selective CI | The tests for the whole project take two minutes |
Partial clones or sparse-checkout |
The repository weighs less than a photograph |
| Git LFS | There are no large binaries… yet (10-03) |
| Long-lived release branches | There is only ever one version in production at a time |
Each of those absences is a design decision, not a shortcoming. Adding any of them "because serious projects do it" would make the workflow worse: more steps, more ceremony, more places to get things wrong, and zero problems solved.
This is the most expensive professional bias there is in this field: confusing the complexity of the process with the maturity of the team.
- The four models, compared
| Linux kernel | Medium open project | Company monorepo | Product team | |
|---|---|---|---|---|
| Size | Thousands of people, hundreds of subsystems | Hundreds of contributors, few maintainers | Hundreds or thousands of people, one repository | 3–15 people |
| Proposal mechanism | Patch by email (format-patch/send-email/am) |
Fork + pull request on a platform | Branch in the central repository + proposal | Branch in the central repository + proposal |
| Who decides | Maintainer hierarchy based on trust | The project's maintainers | Directory owners (CODEOWNERS) |
Any teammate |
| Branching flow | Independent trees merged upwards; integration windows | Short topic branches on main |
Very short topic branches; main always deployable |
Short topic branches on main |
| Resulting history | Many merges between trees; every commit valid on its own | Linear, one commit per proposal (squash) | Linear and extremely high volume | Linear or nearly so, readable by eye |
| Rewriting | Constant and expected (v2, v3…) before integrating |
Frequent before integrating | Little: integration is fast and small | Occasional, to tidy up the branch |
| Releasing | Windows and a release candidate cycle | SemVer tags | Continuous, with no global version | SemVer tags |
| What it takes | Extreme discipline in commits and messages; a culture of public review; tolerance for rewriting | CONTRIBUTING.md, templates, labels, reliable CI, maintainers with time |
In-house tooling, selective CI, Git scaling, code ownership | Mutual trust, basic CI, agreed conventions |
| Fails when | Somebody sends a patch with no context or without splitting it | The maintainers get saturated or CI is slow and flaky | The tooling cannot keep up with the repository | Ceremony nobody needs gets added |
Read it vertically, not horizontally: each column is a coherent system, in which every cell holds up the others. The kernel's extreme discipline is possible because there is a hierarchy of trust that demands it; the hierarchy is sustainable because the patches arrive impeccable. Taking a cell out of its column and putting it into another usually produces a disaster.
- The cross-cutting lesson: a workflow is chosen, not inherited
If I had to reduce this lesson to one sentence it would be this: a workflow is an answer to a specific set of constraints. Change the constraints and the correct answer changes.
The constraints that genuinely matter are four:
1. How much trust is there between the contributors? With high trust (an internal team), direct write access and light review work. With low or unknown trust (anybody on the internet), you need a mechanism where a proposal can arrive without permissions and approval is explicit: fork and proposal, or patch by email.
2. How much does a mistake in production cost? If redeploying costs thirty seconds, you can afford to integrate quickly and fix afterwards (07-05). If the mistake means recalling firmware from devices, you need windows, release candidates and stabilisation branches (07-03).
3. How many versions do you have alive at once? A single version in production allows for a single main. Maintaining three simultaneous versions with security fixes forces you into long-lived branches and systematic cherry-pick (05-03).
4. How big is the repository and how many people touch it? That is what decides whether you need the techniques of lesson 10-04 or whether Git out of the box will be more than enough for the next ten years.
The anti-pattern
The classic mistake always follows the same script:
Somebody reads an article about how an enormous company works. They propose adopting it. Five tools get installed, seven branch types get defined and a three-tier approval policy comes in. Two months later, in a team of five, integrating a two-line change takes three days and nobody remembers why.
The process of a five-thousand-person organisation is the scar left by problems that organisation had. If you do not have those problems, what you are copying is not a solution: it is somebody else's scar.
The healthy recipe
- Start with the simplest workflow that works. For almost everybody: short topic branches, proposals with review, a protected
main, CI that passes in minutes. - Add process only when something specific hurts. And let the conversation start with the pain, not with the tool: "production broke three times this month" is a reason; "we ought to use Git Flow" is not.
- Write the decisions down. In
CONTRIBUTING.mdor in theREADME.md. A workflow that lives only in two people's heads does not survive the third hire. - Review it now and then. A process that suits four people may be insufficient for twelve and absurd when they are back down to four.
Common Mistakes and Tips
Mistake 1: copying the workflow of a project that does not look like yours. This is the central mistake of this lesson. Before adopting anything, ask what problem it solved in its original context and whether you have that problem.
Mistake 2: believing format-patch/am belongs in a museum. It is the most portable way there is of moving a change between two repositories without connecting them. It works for sending a fix to a colleague with no shared network, for moving a commit between two repositories that know nothing about each other, or for archiving a change as text. We pick it up again in lesson 10-02.
Mistake 3: confusing git request-pull with a platform's pull request. The first generates a text that you send by whatever means you like; the second is an object managed by a server, with state, comments and checks. Conceptually they are the same thing; operationally, they are not.
Mistake 4: thinking a monorepo is "everything thrown in together". A monorepo that works has more structure than a set of separate repositories: ownership by directory, an explicit dependency graph and selective CI. What it eliminates is not the order, but the coordination between repositories.
Mistake 5: using a project's prestige as a technical argument. "The kernel does it this way" is not a reason. The reason is why it does it that way, and whether that applies to your case.
Tip 1: learn to read another project's history. It is the fastest way of understanding how a team works before contributing:
git clone --filter=blob:none https://git.example.com/team/project.git
cd project
# Are there merges, or is the history linear?
git log --oneline --graph -40
# What proportion of the commits are merges?
git rev-list --count --merges HEAD
git rev-list --count HEAD
# What message convention do they use?
git log --format=%s -40
# Who really integrates? (committer, not author)
git log --format='%cn' -300 | sort | uniq -c | sort -rn | head
# Do they use Signed-off-by?
git log --format=%b -60 | grep -c 'Signed-off-by'That --filter=blob:none on the clone is a partial clone: it brings down the complete graph but not the contents of the files. For inspecting a history it is perfect and vastly faster. It is explained in lesson 10-04.
Tip 2: before contributing to a project, read its CONTRIBUTING.md from top to bottom. It sounds obvious and hardly anybody does it. It is the difference between a proposal that gets integrated and one that sits waiting for six months.
Tip 3: if your team does not have its workflow written down, write it yourself. Half a page in the README.md: which branches exist, how they are named, how things are integrated, how releases are made. It is the contribution with the best effort-to-benefit ratio you can make.
Exercises
Exercise 1: moving a change with no shared remote
Ana and Bruno are on a train with no connectivity, sharing a local network. Ana has three commits on the branch GT-244-sort-by-priority that Bruno needs in his repository. There is no server within reach.
Describe two ways of achieving it with what you have learnt in the course, and explain when you would prefer each one.
Exercise 2: diagnosing a workflow from its history
You have cloned an unknown project and you get this data:
$ git rev-list --count HEAD 18452 $ git rev-list --count --merges HEAD 6127 $ git log --format=%s -8 Merge tag 'network-for-6.11' of git.example.com/network-subsystem Merge tag 'filesystems-for-6.11' of git.example.com/filesystem-subsystem net: fix memory leak in the driver net: add regression test Merge tag 'graphics-for-6.11' of git.example.com/graphics-subsystem docs: update the contribution guide Merge tag 'usb-for-6.11' of git.example.com/usb-subsystem Merge tag 'audio-for-6.11' of git.example.com/audio-subsystem $ git log --format='%an|%cn' -6 Ana Ferrer|Bruno Salas Carla Vidal|Bruno Salas Diego Rueda|Ana Ferrer Ana Ferrer|Bruno Salas
Which of the four models does it correspond to? Justify your answer with three different pieces of evidence.
Exercise 3: choosing a workflow for three teams
For each situation, decide the working model and justify it in three or four lines. Say also what you would not add.
A. Five people, an internal web application, deploying several times a day, everybody in the same office, the repository weighs 40 MB.
B. An open source library with twenty regular contributors and around two hundred occasional ones a year. Two maintainers with little time. It is used in production in many places, so a broken version is a serious problem.
C. A company of 400 people, twelve products sharing four internal libraries. Today they have sixteen repositories and they spend their lives coordinating versions between them. Every change to a shared library takes weeks to reach all its consumers.
Solutions
Solution 1
Way A: git bundle (lesson 09-05).
# Ana: package the three commits into a file
git bundle create /tmp/GT-244.bundle main..GT-244-sort-by-priority
# (copy the file to Bruno's machine over the local network or on a USB stick)
# Bruno: check what the bundle requires and what it contains
git bundle verify /tmp/GT-244.bundle
git bundle list-heads /tmp/GT-244.bundle
# Bruno: fetch from it as though it were a remote
git fetch /tmp/GT-244.bundle GT-244-sort-by-priority:GT-244-sort-by-priorityWay B: format-patch + am (this lesson).
# Ana
git format-patch main -o /tmp/gt244-patches/
# Bruno
git checkout -b GT-244-sort-by-priority main
git am /tmp/gt244-patches/*.patchWhen to use each:
bundle |
format-patch/am |
|
|---|---|---|
| What it carries | Real Git objects, with their hashes | The content of the changes, as text |
| Resulting hashes | Identical to Ana's | Different: the committer and the date change |
| Readable by eye | No, it is binary | Yes, it is reviewable text |
| Applicable on a different base | No: it requires having the base commit | Yes, and with -3 even with divergences |
| Volume | Efficient for many commits | One file per commit |
Choose bundle when you want the exact history (same hashes, same signature, same parents). Choose format-patch when you want the change, to review it, discuss it or apply it on a different base. And there is a third route if the two machines can see each other on the network: add your colleague's repository as a remote via a filesystem path or over SSH and do a normal fetch (lesson 04-02) — Git is distributed, and any clone can be another's remote.
Solution 2
It is case 1: the kernel model, or any project organised as a tree of trees. Three independent pieces of evidence:
-
The proportion of merges is 33% (6,127 out of 18,452). A history with a third of its commits being merges does not come out of a topic-branch workflow with squash, which produces an almost linear history. It comes out of a model in which complete trees are repeatedly integrated.
-
The merge messages say
Merge tag '...-for-6.11' of git.example.com/...-subsystem. They are integrating tags from different repositories, each one from a subsystem, all prepared for the same version. It is literally the result of a series ofgit request-pullfollowed bygit mergeof independent trees. -
Author and committer differ systematically, and the committers are a small, repeated set (Bruno, Ana) while the authors are varied (Carla, Diego). That is the fingerprint of
git am: there are people who write patches and different people who apply them. In a platform pull request workflow with squash, the committer is usually the server itself or the author.
A fourth, reinforcing piece of evidence: the messages of the ordinary commits carry a subsystem prefix (net:, docs:), the usual convention when the tree is organised by area.
Solution 3
A. Small product team (case 4).
Short topic branches on main, a proposal with one review, CI that runs the tests, a protected main, integration with squash or rebase, deployment from main. That is GitHub Flow (07-04) or even Trunk Based (07-05) if they have good test coverage and use feature flags.
I would not add: Git Flow with develop and release branches (there are no multiple live versions), CODEOWNERS (five people who sit together), selective CI, or any scaling technique. 40 MB is not a performance problem: it is a small repository.
B. Medium open project (case 2).
Fork and proposal, an explicit CONTRIBUTING.md, proposal and issue templates, labels for triage and for attracting newcomers, mandatory and fast CI, integration with squash, releases with an annotated tag and SemVer.
Since "a broken version is a serious problem", I would add one maintenance branch per live major version (1.x, 2.x) onto which fixes are taken with cherry-pick (05-03), and I would publish release candidates before a major version. That is the only point where I borrow a piece of Git Flow, and for a specific reason.
I would not add: an email workflow (the barrier to entry would sink the occasional contributions, which are valuable here), or develop (a stable main plus maintenance branches is enough), or a maintainer hierarchy (with two people there is no hierarchy worth the name).
C. A clear monorepo candidate (case 3).
The symptom they describe — weeks to propagate a change in a shared library to twelve consumers — is exactly the problem the monorepo eliminates at the root: an atomic commit updates the library and all its consumers at once.
Before deciding, I would check three things:
- That there is no code with confidentiality requirements that would rule out total visibility.
- That they are willing to invest in tooling: selective CI and a build system with a dependency graph. Without that, the monorepo turns into a two-hour pipeline per change.
- That the resulting volume is manageable, or that they accept the techniques of lesson 10-04.
As an intermediate and reversible step, I would start by merging only the four shared libraries into a single repository with CODEOWNERS by directory, measure the effect, and consolidate the products afterwards if the experience is good.
I would not add to begin with: submodules (06-05) to tie the libraries to the products. They would solve traceability but not the real problem, which is coordination: they would still need twelve pointer updates, one per product.
Conclusion
Four projects, four different answers to the same five questions.
- The Linux kernel works with patches by email:
git format-patchturns commits into emails,git send-emailsends them to public lists,git amapplies them preserving the original author, andgit request-pullasks a higher level to integrate an entire tree. On top of that stands a maintainer hierarchy based on trust, withSigned-off-byas the chain of custody. The price of admission is high: every commit must stand on its own and every message must read without context. - A medium open project uses fork and proposal, and devotes its whole design to protecting the scarce resource, which is the maintainers' time:
CONTRIBUTING.md, templates, labels and automation of the repetitive parts. The resulting history is linear and readable. - A company with a monorepo chooses the single repository for the atomic change across projects and global refactoring, and pays for that benefit with in-house tooling, code ownership by directory and selective CI computed from
git diff --name-only origin/main...HEAD. - A small product team — ours — runs on short branches, proposals and a protected
main, and its greatest virtue is everything it does not have.
And the cross-cutting lesson, which is the only one worth memorising:
A workflow is chosen on the basis of four constraints — trust between contributors, the cost of a mistake in production, the number of live versions and the size of the repository — and not on the basis of the prestige of whoever uses it.
The process of an enormous organisation is the scar left by the problems that organisation had. If you do not have those problems, copying it means adopting the cost without the benefit.
What is coming
In all four cases something appeared that is not Git: mailing lists, proposal platforms, build systems, patch-tracking tools, integration pipelines. Git is almost never used on its own. It lives surrounded by editors that show differences in colour, by graphical clients, by ticketing systems that want to know which commit closed issue GT-231, by linters that run before every commit and by platforms that turn a history into a conversation.
That integration layer completely changes the daily experience and, badly understood, it is also where most people lose control of what they are doing: an editor's "Sync" button hides at least two commands, and not always the ones you would expect.
In lesson 10-02: Integrating Git with Other Tools we will look at what each piece of the ecosystem genuinely contributes, what is worth carrying on doing in the terminal and why the underlying advice is always the same: automate the repetitive, but always understand which command is underneath.
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
