Git Development Workflow & the Linux Kernel Model
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler
Git is the de facto standard for version control in modern software development. But knowing Git commands is only half the battle — having a solid workflow is what makes a team productive. This post walks through a practical, real-world Git development workflow that scales from solo projects to large teams.
1. Why a Workflow Matters
Without a consistent workflow, a shared repository quickly becomes a nightmare:
- Merge conflicts pile up
- No one knows which branch is stable
- Hotfixes get tangled with feature work
- Code review becomes impossible to track
A good workflow answers four questions:
- Where do I start my work?
- How do I share it with my team?
- How do I get it reviewed?
- How does it reach production?
2. The Branches
Most teams adopt a variation of GitHub Flow or Git Flow. Here’s the simplified version I recommend as a starting point:
| Branch | Purpose | Who pushes |
|---|---|---|
main (or master) |
Production-ready code | Maintainers only |
develop |
Integration branch for features | Developers (via PR) |
feature/* |
Individual feature work | Feature authors |
hotfix/* |
Urgent production fixes | Anyone |
release/* |
Release preparation | Maintainers |
1 | main ──●────────────────────●──────── |
main
- Always reflects the latest production release.
- Every commit on
mainshould be deployable. - Protected — no direct pushes; only merges via pull request.
develop
- The integration branch where features come together.
- Must also be kept in a green (passing CI) state.
- Periodically merged into
mainfor releases.
feature/*
- One branch per feature, branched off
develop. - Naming convention:
feature/short-description(e.g.,feature/add-user-auth). - Deleted after merging.
hotfix/*
- Branched directly off
mainfor critical bug fixes. - Merged back into both
mainanddevelopto prevent regressions.
3. The Daily Flow
Here’s what a typical day looks like for a developer:
Step 1: Sync & Branch
1 | # Start from an up-to-date develop |
Step 2: Work & Commit
Make small, logical commits. Each commit should represent one atomic change:
1 | git add src/api/login.go |
Good commit messages follow the Conventional Commits format
1 | <type>(<scope>): <description> |
feat: a new featurefix: a bug fixrefactor: code change that neither fixes a bug nor adds a featuredocs: documentation onlytest: adding or fixing testschore: build process, dependencies, etc.
Step 3: Stay Updated
While you work, others may merge into develop. Keep your branch up to date with rebase, not merge:
1 | git checkout develop |
Why rebase? It keeps a linear history — no unnecessary merge commits cluttering the log.
⚠️ Never rebase a branch that others are working on. Rebasing rewrites history. Only rebase your own feature branches.
Step 4: Push & Open a Pull Request
1 | git push origin feature/add-login-api |
Then open a Pull Request (PR) on GitHub/GitLab/Bitbucket targeting develop.
A good PR includes:
- Title that summarizes the change
- Description explaining why (not just what)
- Linked issue (e.g., “Closes #42”)
- Screenshots for UI changes
- Checklist of what was done
Step 5: Code Review
- Keep PRs small — ideally under 400 lines. Large PRs are hard to review and hide bugs.
- Respond to comments quickly.
- Address feedback in new commits — no need to squash until the end.
Step 6: Merge
Once approved and CI passes, merge with squash merge (to keep a clean history on develop):
1 | # On GitHub, select "Squash and merge" |
Then delete the feature branch:
1 | git branch -d feature/add-login-api |
4. Merge vs. Rebase vs. Squash
This is the most common source of Git confusion. Here’s a cheat sheet:
| Strategy | Use when | History result |
|---|---|---|
| Merge commit | Merging develop → main (preserves topology) |
Non-linear, preserves all branches |
| Rebase | Updating a feature branch with upstream changes | Linear, clean |
| Squash merge | Merging a completed feature into develop |
Single commit, clean |
Golden rule:
- Public/stable branches (
main,develop,release) → merge commit - Private feature branches → rebase to sync, squash to finish
5. Handling Conflicts
Conflicts happen. Don’t panic.
1 | # After rebase/merge, Git tells you which files conflict |
Pro tip: Use a visual diff tool:
1 | git config --global diff.tool vimdiff |
6. Hotfix Flow
When production is on fire:
1 | git checkout main |
7. Release Flow
When develop has enough features for a release:
1 | git checkout develop |
8. Git Aliases to Speed You Up
Add these to your ~/.gitconfig:
1 | [alias] |
9. Common Pitfalls & How to Avoid Them
| Pitfall | Solution |
|---|---|
Committing to main by accident |
Protect main in repo settings (no direct push) |
| Large, unreviewable PRs | Enforce a line limit; require atomic commits |
| Merge commits everywhere | Use rebase for feature branches, squash for merges |
| “I lost my work!” | Use git reflog — Git keeps a log of HEAD movements for ~30 days |
| Forgetting to pull before pushing | git pull --rebase before every git push |
10. A Complete Cheat Sheet
1 | # Start a feature |
11. The Linux Kernel Development Model
Git wasn’t created in a vacuum — Linus Torvalds built it specifically to support Linux kernel development. The kernel’s workflow is fascinatingly different from the corporate GitHub flow above. It’s a hierarchical, mailing-list-driven model that has scaled to thousands of contributors across dozens of subsystems for over 20 years.
11.1 The Big Picture
Unlike a central origin on GitHub, the kernel uses a tiered tree model:
1 | Linus's Tree (torvalds/linux) |
Every layer communicates via email patches, not GitHub PRs. The key insight: trust is delegated. Linus only pulls from a few dozen trusted subsystem maintainers. Those maintainers pull from sub-maintainers. Everyone else submits patches via email.
11.2 The Release Cycle
Each kernel release follows a rigid ~9–10 week cadence:
| Phase | Duration | What happens |
|---|---|---|
| Merge Window | ~2 weeks | Linus accepts new features. Subsystem maintainers send pull requests. High churn. |
| rc1 | First release candidate | Merge window closes. Linus tags v6.x-rc1. |
| rc2 – rc7/rc8 | ~7 weeks | Bug fixes only. Each week a new -rcN tag. Stricter criteria each week. |
| Final release | — | Linus tags v6.x. The cycle resets. |
“We do not add new features after the merge window closes, period.” — Linus Torvalds
This time-based (not feature-based) release model is the opposite of “ship when it’s done.” It forces discipline: if your feature misses the merge window, it waits for the next cycle.
11.3 Patch Submission via Email
This is the most alien part for GitHub-raised developers. Here’s how it works:
Step 1: Prepare your patches
1 | # Make your changes, then create formatted patches |
This generates .patch files with a cover letter (for series > 1 patch). Each patch includes:
- A
[PATCH v2 1/3]subject line - A changelog since the previous version (
---below---) Signed-off-by: Your Name <email>
Step 2: Find the right maintainer
1 | # The kernel's MAINTAINERS file tells you who to send to |
Step 3: Send with git send-email
1 | git send-email --to="maintainer@kernel.org" \ |
Your patch lands on the mailing list (vger.kernel.org), where it’s reviewed by maintainers and peers.
💡
git send-emailrequires extra setup (IMAP/SMTP). Many newcomers usegit format-patch+muttoraercinstead.
11.4 The Developer Certificate of Origin (DCO)
Instead of a CLA, the kernel uses the DCO — a lightweight certification that you wrote the code or have the right to submit it.
Every commit must carry a Signed-off-by: trailer:
1 | commit 1234abcd... |
The Signed-off-by is a chain — each person who handles the patch adds theirs:
1 | Signed-off-by: Jane Developer <jane@example.com> |
11.5 Review Tags
During review, reviewers and maintainers add tags to patch replies:
| Tag | Meaning |
|---|---|
Reviewed-by: |
Patch looks correct |
Acked-by: |
I’m OK with this going through another tree |
Tested-by: |
I ran it on real hardware |
Reported-by: |
I found this bug |
Suggested-by: |
I proposed this idea |
Fixes: abc123 |
This patch fixes the bug introduced in commit abc123 |
Closes: https://bugzilla.kernel.org/12345 |
Links to the bug report |
When you send a new version (v2, v3…), include a changelog after the --- separator:
1 | drm/foo: fix framebuffer pitch calculation |
11.6 The linux-next Tree
Between cycles, Stephen Rothwell maintains linux-next — a integration test tree that merges all subsystem trees together:
1 | git remote add linux-next \ |
linux-next exists to catch cross-subsystem conflicts before the merge window. If your subsystem tree has a conflict with another tree, you’ll find out here, not when Linus tries to pull.
11.7 Stable & Longterm Kernels
After a mainline release, a stable team (led by Greg Kroah-Hartman) maintains it:
| Series | Support | Example |
|---|---|---|
| Stable | ~3 months (until next release) | 6.10.1, 6.10.2 |
| Longterm (LTS) | 2–6 years | 6.6.y, 5.15.y, 5.10.y, 5.4.y |
Fixes are backported and tagged. The workflow:
1 | git checkout -b linux-6.10.y v6.10 |
Patches destined for stable are tagged with a Cc: stable@vger.kernel.org in the commit message:
1 | Cc: stable@vger.kernel.org |
11.8 How Linus Pulls
When a subsystem maintainer is ready to send code to Linus, they create a tag and send a pull request via email:
1 | # Maintainer side: |
Then email Linus a signed pull request:
1 | Subject: [GIT PULL] drm fixes for 6.11-rc5 |
Linus then does:
1 | git pull --verify-signatures \ |
He verifies the tag’s PGP signature — every kernel tag is cryptographically signed. This is how trust works in a distributed, pull-based model.
11.9 Key Takeaways for Kernel Contributors
| Do | Don’t |
|---|---|
| Split changes into logical per-patch commits | Submit a single mega-patch |
Run scripts/checkpatch.pl before sending |
Ignore coding style (Documentation/process/coding-style.rst) |
Find the right maintainer with get_maintainer.pl |
Blindly CC everyone |
Use git format-patch -vN for new versions |
Edit patches by hand |
| Respond to reviewer comments on the mailing list | Get defensive — technical discussion is expected |
Add Cc: stable@vger.kernel.org for fixes that should be backported |
Forget to tag fixes for stable |
| Be patient — review can take weeks | Resend every few days (that’s considered rude) |
11.10 Linux vs. Corporate Workflow at a Glance
| Aspect | Corporate (GitHub Flow) | Linux Kernel |
|---|---|---|
| Code submission | Pull request on GitHub | Email patch via git send-email |
| Review platform | GitHub UI / GitLab MR | Mailing list (public inbox, lore.kernel.org) |
| Branching model | Feature branch → PR → squash | Maintainer trees → signed tags → pull |
| Release model | Continuous deployment or feature-based | Fixed ~10-week time-based cycle |
| Legal sign-off | CLA (often) | DCO (Signed-off-by: trailer) |
| History | Squash-merged, may be rewritten | Never rewritten — history is sacred |
| Communication | PR comments, Slack/Discord | Mailing lists, IRC, conference hallway track |
Final Thoughts
A Git workflow is like a coding convention — it’s less about right and wrong and more about consistency. Pick a workflow that fits your team’s size, release cadence, and risk tolerance. Document it. Follow it. Revisit it when it hurts.
The workflow above has served teams of 1 to 100+ developers. Adapt it to your needs — the most important rule is: automate what you can, document what you can’t.
Have a different workflow that works well for your team? I’d love to hear about it. Drop a comment below!