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:

  1. Where do I start my work?
  2. How do I share it with my team?
  3. How do I get it reviewed?
  4. 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
2
3
4
5
main        ──●────────────────────●────────
\ /
develop ●──●──●────●──●──●──
\ /
feature/foo ●──●──●

main

  • Always reflects the latest production release.
  • Every commit on main should 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 main for 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 main for critical bug fixes.
  • Merged back into both main and develop to prevent regressions.

3. The Daily Flow

Here’s what a typical day looks like for a developer:

Step 1: Sync & Branch

1
2
3
4
5
6
# Start from an up-to-date develop
git checkout develop
git pull --rebase origin develop

# Create a feature branch
git checkout -b feature/add-login-api

Step 2: Work & Commit

Make small, logical commits. Each commit should represent one atomic change:

1
2
git add src/api/login.go
git commit -m "feat(api): add login endpoint with JWT validation"

Good commit messages follow the Conventional Commits format

1
2
3
4
5
<type>(<scope>): <description>

[optional body]

[optional footer]
  • feat: a new feature
  • fix: a bug fix
  • refactor: code change that neither fixes a bug nor adds a feature
  • docs: documentation only
  • test: adding or fixing tests
  • chore: 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
2
3
4
git checkout develop
git pull --rebase origin develop
git checkout feature/add-login-api
git rebase 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
2
3
4
5
# On GitHub, select "Squash and merge"
# Or locally:
git checkout develop
git merge --squash feature/add-login-api
git commit -m "feat(api): add login endpoint"

Then delete the feature branch:

1
2
git branch -d feature/add-login-api
git push origin --delete 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 developmain (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
2
3
4
5
6
7
8
9
10
# After rebase/merge, Git tells you which files conflict
# Edit the files to resolve conflicts, then:

git add <resolved-files>

# If rebasing:
git rebase --continue

# If merging:
git commit

Pro tip: Use a visual diff tool:

1
2
git config --global diff.tool vimdiff
git mergetool

6. Hotfix Flow

When production is on fire:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
git checkout main
git checkout -b hotfix/critical-security-patch

# Fix the bug, commit
git add .
git commit -m "fix(auth): patch OAuth token validation"

# Merge into main (with a merge commit for visibility)
git checkout main
git merge --no-ff hotfix/critical-security-patch
git tag v1.2.1

# Also merge into develop so the fix isn't lost
git checkout develop
git merge --no-ff hotfix/critical-security-patch

# Clean up
git branch -d hotfix/critical-security-patch

7. Release Flow

When develop has enough features for a release:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
git checkout develop
git checkout -b release/v1.3.0

# Bump version numbers, update changelog, final testing
# Commit release prep
git commit -m "chore(release): v1.3.0"

# Merge into main
git checkout main
git merge --no-ff release/v1.3.0
git tag v1.3.0

# Merge back into develop (in case of release-specific fixes)
git checkout develop
git merge --no-ff release/v1.3.0

git branch -d release/v1.3.0

8. Git Aliases to Speed You Up

Add these to your ~/.gitconfig:

1
2
3
4
5
6
7
8
9
10
[alias]
co = checkout
br = branch
ci = commit
st = status
unstage = reset HEAD --
last = log -1 HEAD
lg = log --graph --oneline --all --decorate
amend = commit --amend --no-edit
undo = reset --soft HEAD~1

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Start a feature
git checkout develop && git pull --rebase
git checkout -b feature/my-thing

# Save work
git add -p # interactive staging
git commit -m "feat: add my thing"

# Sync with upstream
git fetch origin
git rebase origin/develop

# Share
git push origin feature/my-thing # first push
git push # subsequent pushes

# Polish history before PR
git rebase -i HEAD~3 # interactive rebase to squash/fixup

# Finish
git checkout develop
git merge --squash feature/my-thing
git commit -m "feat: add my thing"
git branch -d feature/my-thing

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
2
3
4
5
6
7
Linus's Tree (torvalds/linux)
↑ pull requests (merge tags)
Subsystem Maintainer Trees (e.g., netdev, drm, kbuild)
↑ pull requests
Driver / Feature Trees
↑ patches (email)
Individual Contributors

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
2
# Make your changes, then create formatted patches
git format-patch -v2 origin/master..HEAD

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
2
# The kernel's MAINTAINERS file tells you who to send to
./scripts/get_maintainer.pl my-patch.patch

Step 3: Send with git send-email

1
2
3
4
5
git send-email --to="maintainer@kernel.org" \
--cc="linux-kernel@vger.kernel.org" \
--cover-letter \
--annotate \
*.patch

Your patch lands on the mailing list (vger.kernel.org), where it’s reviewed by maintainers and peers.

💡 git send-email requires extra setup (IMAP/SMTP). Many newcomers use git format-patch + mutt or aerc instead.

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
2
3
4
5
6
7
8
9
10
11
commit 1234abcd...
Author: Jane Developer <jane@example.com>
Date: Mon Jul 10 14:22:00 2026 +0800

drm/foo: fix framebuffer pitch calculation

When the hardware requires 256-byte alignment, the existing code
under-calculates the pitch for non-power-of-two widths. Align up.

Signed-off-by: Jane Developer <jane@example.com>
Reviewed-by: Senior Hacker <senior@example.com>

The Signed-off-by is a chain — each person who handles the patch adds theirs:

1
2
3
Signed-off-by: Jane Developer <jane@example.com>
Signed-off-by: Subsystem Maintainer <maintainer@kernel.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
drm/foo: fix framebuffer pitch calculation

When the hardware requires 256-byte alignment... (full description)

Signed-off-by: Jane Developer <jane@example.com>
---
Changes in v2:
- Use ALIGN() macro instead of open-coding (suggested by Reviewer)
- Fix whitespace in comment

Changes in v1:
- Initial patch
---
drivers/gpu/drm/foo/bar.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)

11.6 The linux-next Tree

Between cycles, Stephen Rothwell maintains linux-next — a integration test tree that merges all subsystem trees together:

1
2
3
4
git remote add linux-next \
git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
git fetch linux-next
git log --oneline linux-next/master | head -5

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
2
3
4
git checkout -b linux-6.10.y v6.10
git cherry-pick <fix-commit-id>
git commit -s -m "fix: backport framebuffer pitch fix"
# Send to stable@vger.kernel.org with Cc: stable@vger.kernel.org in the original commit

Patches destined for stable are tagged with a Cc: stable@vger.kernel.org in the commit message:

1
2
3
Cc: stable@vger.kernel.org
Fixes: a1b2c3d4 ("drm/foo: add initial driver")
Signed-off-by: Jane Developer <jane@example.com>

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
2
3
# Maintainer side:
git tag drm-fixes-2026-07-10
git push origin drm-fixes-2026-07-10

Then email Linus a signed pull request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Subject: [GIT PULL] drm fixes for 6.11-rc5

Hi Linus,

A few amdgpu and i915 fixes this week.

The following changes since commit abc123...:

...

are available in the git repository at:

git://git.kernel.org/pub/scm/linux/kernel/git/foo/drm.git drm-fixes-2026-07-10

for you to fetch changes up to def456...:

...
--------------------------------------------------
Alex Smith (2):
drm/amdgpu: fix power management on RX 7900 XT
drm/amdgpu: add PCI ID for new Navi variant

Jane Developer (1):
drm/foo: fix framebuffer pitch calculation

Linus then does:

1
2
git pull --verify-signatures \
git://git.kernel.org/.../drm.git drm-fixes-2026-07-10

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!