A comprehensive reference for kernel-style and suckless-style patch workflows. Covers the full stack from git config to mailing list tooling to stacked branch management.
These settings are used by active kernel maintainers and make the diff/merge/rebase experience substantially better.
[diff]
algorithm = histogram # better than myers for code changes
colorMoved = default
indentHeuristic = true
[merge]
conflictstyle = zdiff3 # shows base alongside ours/theirs, not just ours+theirs
[rerere]
enabled = true
autoupdate = true # auto-stage rerere resolutions (you still commit manually)
[rebase]
autosquash = true # honor fixup!/squash! prefixes automatically
updateRefs = true # update all intermediate refs when rebasing a stack
autoStash = true
[sendemail]
smtpserver = smtp.example.com
smtpserverport = 587
smtpencryption = tls
smtpuser = you@example.com
confirm = always # ask before sending
chainreplyto = false # thread under cover letter, not last patch
suppresscc = self # don't CC yourself on your own patches
annotate = false # don't open editor for every patch
[format]
coverLetter = auto # generate cover letter when >1 patch
notes = true # include git notes in format-patch output
signatureFile = ~/.git-signature
[log]
date = format:%Y-%m-%d
Critical line: chainreplyto = false. Without it, each patch threads under the previous patch instead of under the cover letter, which breaks list threading.
b4 is the current standard for kernel patch submission. It handles threading, versioning, trailers, and sending.
pip install b4
# or
pipx install b4
# Start a new series on a branch
git checkout -b feature/my-driver
# After commits are ready, prep the branch
b4 prep --new v1 --trailers --cover-from-branch main
# Edit cover letter
b4 prep --edit-cover
# Check what will be sent
b4 prep --show-revision
# Dry run first
b4 send --dry-run
# Send to the list
b4 send
# Send a re-roll (v2)
b4 send --revision 2 --in-reply-to <message-id-of-v1>
b4 automatically:
[PATCH v2 0/N] prefix# Apply from a message ID (pulls from lore.kernel.org)
b4 am <message-id>
# Apply and create a local branch
b4 am --create-branch <message-id>
# Apply with 3-way merge fallback
b4 am -3 <message-id>
# Apply a patch series and add your Reviewed-by / Tested-by
b4 shazam <message-id>
# Add a specific trailer
b4 shazam --add-trailer "Tested-by: You <you@example.com>" <message-id>
# Compare v1 vs v2 of a series
b4 diff <message-id-of-v1>
# Compare against a local branch
b4 diff --local-branch feature/my-driver <message-id>
lei is part of public-inbox and gives you a local index of any mailing list you care about, without actually subscribing.
# Install (often packaged as public-inbox-lei)
# Arch: yay -S public-inbox
# Add a list to watch
lei add-watch https://lore.kernel.org/linux-mm/
# Search across all indexed lists
lei q "drm/i915 cursor regression" --no-remote
# Search with field filters
lei q 's:RFC patch series' --no-remote
# Get a specific message by ID
lei q "mid:20230601120000.12345-1-author@kernel.org"
# Convert results to mbox for reading in mutt/neomutt
lei q "from:torvalds" --format=mboxrd -o ~/mail/torvalds-search.mbox
# Watch for new patches matching a query
lei watch-add "s:drm/nouveau v2"
The key advantage: no inbox noise. You search when you need to, and the index updates in the background.
# Format last 3 commits as patches
git format-patch -3
# Format a range
git format-patch origin/main..HEAD
# Output to a directory
git format-patch -o /tmp/patches origin/main..HEAD
# Add a cover letter
git format-patch --cover-letter -o /tmp/patches origin/main..HEAD
# Set the subject prefix for a re-roll
git format-patch --subject-prefix="PATCH v3" -o /tmp/patches origin/main..HEAD
# Automatically detect base commit
git format-patch --base=auto origin/main..HEAD
--base=auto records which upstream commit the series is based on. Reviewers can use this to check out the exact base and apply cleanly.
When sending a v2+, include a range-diff so reviewers can see what changed:
# Generate range-diff and pipe it into the cover letter manually
git range-diff origin/main..v1-branch origin/main..v2-branch
# With b4, this is automatic. Without b4, do it manually:
git format-patch --cover-letter --range-diff=v1-branch -o /tmp/patches origin/main..HEAD
The --range-diff flag makes git insert the range-diff output into the cover letter body, which is the convention on LKML.
# Compare two versions of a 5-patch series
git range-diff \
base..old-branch \
base..new-branch
# Limit to specific patches
git range-diff base..old~2 base..new~2
Output reads like a diff of diffs. Lines prefixed with > are new content inside a changed patch.
# Send all patches in a directory
git send-email /tmp/patches/*.patch
# Send a formatted range directly
git send-email origin/main..HEAD
# Reply to the v1 cover letter so the thread stays together
git send-email \
--in-reply-to="<20230601120000.12345-0-author@kernel.org>" \
/tmp/patches-v2/*.patch
The message ID comes from the original [PATCH v1 0/N] cover letter. Get it from lore.kernel.org or from the email headers.
# ~/.gitconfig
[sendemail "kernel"]
smtpserver = smtp.kernel.org
smtpuser = me@kernel.org
from = Me <me@kernel.org>
[sendemail "personal"]
smtpserver = smtp.fastmail.com
smtpuser = me@fastmail.com
from = Me <me@fastmail.com>
# Use a specific identity
git send-email --identity=kernel /tmp/patches/*.patch
# Dry run: print what would be sent without sending
git send-email --dry-run /tmp/patches/*.patch
# Validate patches before sending
git send-email --validate /tmp/patches/*.patch
# Suppress CC to people already on To:
git send-email --suppress-cc=bodycc /tmp/patches/*.patch
# Suppress all auto-detected CCs
git send-email --suppress-cc=all /tmp/patches/*.patch
# Control CC handling explicitly
git send-email \
--to=linux-kernel@vger.kernel.org \
--cc=maintainer@example.com \
--suppress-cc=self \
/tmp/patches/*.patch
--validate runs git am --dry-run on the patches first. Catches malformed patches before they hit the list.
Josef Bacik (btrfs maintainer) uses a workflow that lets him walk a patch series commit by commit, opening vimdiff at each step.
# In ~/.gitconfig
[alias]
# Show the series log with short stat
series-log = log --oneline --stat origin/main..HEAD
# Review each commit interactively
series-review = "!f() { \
git log --reverse --format='%H' origin/main..HEAD | \
while read sha; do \
git show $sha | vim -R -; \
echo 'Next? [y/n]'; \
read ans; \
[ \"$ans\" = 'n' ] && break; \
done; \
}; f"
# Vimdiff a single commit against its parent
show-diff = "!f() { \
git difftool -t vimdiff \"$1\"^ \"$1\"; \
}; f"
# Check each patch applies to a clean base
check-series = "!f() { \
git format-patch -o /tmp/review-patches origin/main..HEAD && \
git stash && \
git checkout origin/main && \
git am /tmp/review-patches/*.patch; \
}; f"
# See what's in the series
git series-log
# Walk through each patch, reading in vim
git series-review
# Vimdiff a specific commit
git show-diff a3f4c2b
# Review from a mailing list message ID via b4
b4 am <message-id> --create-branch review/feature
git checkout review/feature
git series-review
Greg Kroah-Hartman uses quilt to manage the stable kernel patch queue. The workflow applies to any project where you maintain patches on top of a moving upstream.
# Install quilt
# Arch: sudo pacman -S quilt
# Configure
cat >> ~/.quiltrc << 'EOF'
QUILT_DIFF_ARGS="--no-timestamps --no-index -p ab --color=auto"
QUILT_REFRESH_ARGS="--no-timestamps --no-index -p ab"
QUILT_SERIES_ARGS="--color=auto"
QUILT_PATCH_OPTS="--unified"
QUILT_DIFF_OPTS="-p"
EDITOR=nvim
EOF
# Import patches from upstream stable queue
b4 am <message-id> --output-dir stable-queue/patches/5.15/
# Navigate the queue with quilt
quilt series # list all patches
quilt next # what's the next unapplied patch
quilt push # apply next patch
quilt push -a # apply all remaining
quilt pop # unapply last patch
quilt pop -a # unapply all
# When a patch fails to apply
quilt push # fails
quilt refresh # after resolving conflicts manually
quilt header -e # edit the patch header/description
# Export for re-submission
quilt diff # diff of current patch
quilt export # export to directory
# Start a new patch
quilt new drivers-my-fix.patch
# Add files you'll modify to the patch
quilt add drivers/gpu/drm/i915/display.c
# Edit the file
nvim drivers/gpu/drm/i915/display.c
# Refresh the patch with your changes
quilt refresh
# Check the result
quilt diff
quilt header -e
Junio (git maintainer) maintains git.git with a formal branch graduation model. The same pattern works for any project with a long-running stable history.
| Branch | Purpose |
|---|---|
maint |
Bug fixes for the last stable release |
master |
Current stable release |
next |
Integration branch; topics proposed for master |
seen |
Everything received; experimental / unstable |
topic branch -> seen -> next -> master -> maint (backports)
A topic lands in seen first. If it looks good after review, Junio merges it into next. Once the next release cycle closes, next is merged into master. Only regression fixes ever go into maint.
# Merge a topic into seen for initial integration
git checkout seen
git merge --no-ff pu/feature-branch -m "Merge branch 'pu/feature-branch' into seen"
# Graduate to next after positive review
git checkout next
git merge --no-ff pu/feature-branch
# At release time, merge next into master
git checkout master
git merge --no-ff next
# Backport a critical fix to maint
git checkout maint
git cherry-pick <fix-sha>
git tag v2.41.1
# Mark a topic as "cook more"
# (just don't merge it to next yet, leave in seen)
Junio sends a periodic "What's cooking in git.git" email. You can replicate this:
# Show topics not yet in next
git log next..seen --oneline --no-merges
# Show topics in next not yet in master
git log master..next --oneline --no-merges
The bare repo pattern lets you work on multiple branches simultaneously without stashing or switching. Each branch gets its own directory.
# Clone as bare
git clone --bare https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git linux.git
# Or convert an existing clone
cd linux && git config core.bare true
# Add worktrees for each branch you're working on
git -C linux.git worktree add ../linux-main main
git -C linux.git worktree add ../linux-drm drm-fixes
git -C linux.git worktree add ../linux-review review/incoming-series
# List all worktrees
git -C linux.git worktree list
# Work on the drm branch without affecting main
cd ../linux-drm
vim drivers/gpu/drm/i915/display.c
git add -p
git commit
# Simultaneously test something in the main tree
cd ../linux-main
git log --oneline -10
# Remove a worktree when done
git -C linux.git worktree remove ../linux-review
A regular clone has a checked-out working tree tied to the .git directory. A bare clone has no working tree of its own, so every checkout lives in a separate worktree add directory. You never have a "main" checkout competing with your worktrees.
rerere (reuse recorded resolution) records how you resolved a conflict and replays it automatically next time the same conflict appears. Essential when rebasing a long-lived branch repeatedly onto a moving upstream.
[rerere]
enabled = true
autoupdate = true # stage the resolved file automatically
# Enable if not in config
git config --global rerere.enabled true
# Conflict occurs during rebase
git rebase upstream/main
# ... conflict in drivers/net/foo.c ...
# Resolve it manually
nvim drivers/net/foo.c
git add drivers/net/foo.c
git rebase --continue
# rerere records this resolution. Next time the same conflict appears,
# it resolves automatically.
If you have old merge commits where conflicts were already resolved, you can train rerere from them:
# Walk through all merge commits and extract resolutions
git log --merges --format="%H" | \
while read sha; do
git rerere train "$sha"
done
# List all recorded resolutions
git rerere list
# Show the rerere directory
ls .git/rr-cache/
# Forget a bad resolution (before applying it to commits)
git rerere forget path/to/file.c
# Forget by conflict ID
git rerere forget # forgets all resolutions in the current conflict
Typical scenario: you have a topic branch with a conflict against upstream. You rebase weekly. Without rerere you resolve the same conflict every time. With rerere you resolve it once, and all subsequent rebases apply the resolution automatically.
rebase --update-refs is the native git answer to stacked diffs. It keeps all the intermediate branch pointers correct when you rebase the bottom of a stack.
# Enable globally (git >= 2.38)
git config --global rebase.updateRefs true
# Create a base feature branch
git checkout -b feature/base main
# ... make commits A, B ...
# Build on top of it
git checkout -b feature/part2 feature/base
# ... make commits C, D ...
# Build further
git checkout -b feature/part3 feature/part2
# ... make commits E, F ...
# Without updateRefs, rebasing feature/base orphans part2 and part3.
# With updateRefs, all three branches move together.
git checkout feature/base
git rebase --update-refs main
# feature/base, feature/part2, feature/part3 all point to their
# rebased equivalents. No manual branch moving required.
# Turn on for a single rebase if you haven't set it globally
git rebase --update-refs main
# Turn off for a single rebase even if globally enabled
git rebase --no-update-refs main
# See the stack visually
git log --oneline --graph feature/part3
# See which refs are on which commits
git log --oneline --decorate feature/part3
git-autofixup generates fixup commits targeting the right commit automatically, based on the blame of changed lines.
# Install git-autofixup
cpan App::GitAutofixup
# or
pip install git-autofixup # some distributions package it here
# You have a series: commits A (add function), B (use it), C (tests)
# You find a bug in A while reviewing B
# Fix the bug in the working tree (don't commit yet)
nvim drivers/net/foo.c
# git-autofixup looks at the blame of changed lines and
# creates a fixup! commit targeting the right parent
git autofixup
# Result: a new commit "fixup! <subject of commit A>"
git log --oneline
# C tests
# B use function
# fixup! A add function
# A add function
# Now interactively rebase to collapse the fixup
git rebase -i main # autosquash=true handles it automatically
# or explicitly:
git rebase --autosquash main
Without git-autofixup, you create fixup commits manually:
# Create a fixup targeting commit A
git commit --fixup=<sha-of-A>
# Create a squash (opens editor during rebase to merge messages)
git commit --squash=<sha-of-A>
# Apply
git rebase --autosquash main
# Modify a specific commit without rebase -i
git commit --fixup=<sha>
GIT_SEQUENCE_EDITOR=true git rebase --autosquash -i main
GIT_SEQUENCE_EDITOR=true skips the editor entirely, letting autosquash apply automatically.
Suckless projects (dwm, st, dmenu, etc.) release minimal C programs meant to be patched per-user. The recommended workflow uses a branch per patch, merges them to a local build branch, and uses rerere to survive upstream updates.
cd ~/dev/dwm
# Upstream tracking branch (never commit local patches here)
git remote add upstream https://git.suckless.org/dwm
git fetch upstream
git checkout -b upstream upstream/master
# Your clean base for building
git checkout -b build upstream
# Download a patch from suckless.org
curl -O https://dwm.suckless.org/patches/systray/dwm-systray-6.4.diff
# Apply it on its own branch
git checkout -b patch/systray upstream
git apply dwm-systray-6.4.diff
git add -A
git commit -m "patch: systray"
# Add another
git checkout -b patch/fullgaps upstream
curl -O https://dwm.suckless.org/patches/fullgaps/dwm-fullgaps-6.5.diff
git apply dwm-fullgaps-6.5.diff
git add -A
git commit -m "patch: fullgaps"
# Reset build to upstream
git checkout build
git reset --hard upstream
# Merge patches one at a time
git merge --no-ff patch/systray
# resolve conflicts, git rerere saves them
git merge --no-ff patch/fullgaps
# resolve conflicts, rerere saves them
# build branch is now upstream + all your patches
# New upstream release
git fetch upstream
git checkout upstream
git reset --hard upstream/master
# Rebuild the combined branch
git checkout build
git reset --hard upstream
# Re-merge each patch branch. rerere replays saved resolutions.
git merge --no-ff patch/systray # probably auto-resolves via rerere
git merge --no-ff patch/fullgaps # same
# If a patch no longer applies cleanly, rebase it first
git checkout patch/systray
git rebase upstream
git checkout build
git reset --hard upstream
git merge --no-ff patch/systray
# Push all branches
git push origin upstream build patch/systray patch/fullgaps
patchutils provides tools for manipulating patch files directly, without applying them.
# Arch: sudo pacman -S patchutils
Shows what changed between v1 and v2 of a patch, similar to range-diff but operates on raw patch files:
interdiff v1.patch v2.patch
interdiff v1.patch v2.patch | colordiff | less -R
Useful when you received patches by email and don't have the git history.
lsdiff big-series.patch
# drivers/net/ethernet/intel/igb/igb_main.c
# drivers/net/ethernet/intel/igb/igb_ethtool.c
# With line numbers showing where each hunk starts
lsdiff -s big-series.patch
# Extract only patches touching drivers/net/
filterdiff -i 'drivers/net/*' big-series.patch > net-only.patch
# Exclude generated files
filterdiff -x '*.pb.go' big-series.patch > no-generated.patch
# Extract a specific file
filterdiff -i 'Makefile' big-series.patch
splitdiff -a big-series.patch
# Creates 001-drivers_net_foo.patch, 002-include_linux_bar.patch, ...
splitdiff -D /tmp/split-patches big-series.patch
# Create a single patch that is the net effect of applying p1 then p2
combinediff p1.patch p2.patch > combined.patch
Useful when you've iterated on a fix and want to send a single clean patch to stable@ instead of a series.
git notes attach arbitrary text to commits without changing the commit SHA. Useful for recording review status, test results, or stable-queue tags.
# Add a note to a commit
git notes add -m "Reviewed-by: You <you@example.com>" <sha>
# Append to existing note
git notes append -m "Tested-by: CI <ci@example.com>" <sha>
# Read notes in log
git log --notes --oneline
# Read a specific note
git notes show <sha>
# Edit a note
git notes edit <sha>
# Remove a note
git notes remove <sha>
By default notes go in refs/notes/commits. You can have multiple note tracks:
# Add a review note in its own namespace
git notes --ref=refs/notes/review add -m "Acked-by: Maintainer" <sha>
# Add stable-queue metadata
git notes --ref=refs/notes/stable add -m "stable: 6.1+" <sha>
# Show notes from a specific ref
git log --notes=refs/notes/review
Notes are not pushed/fetched by default. Configure it:
# Push notes
git push origin refs/notes/commits
# Fetch notes from upstream
git fetch origin refs/notes/commits:refs/notes/commits
# Configure automatic fetch
git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*'
git replace creates a redirect: references to the original SHA transparently use the replacement instead. The original object is unchanged.
# Replace commit A with a corrected commit A' (same content, fixed message)
git replace <sha-of-A> <sha-of-A-prime>
# Now git log, git show, git diff all see A' when they encounter A
git log --oneline
# List replacements
git replace -l
# Remove a replacement
git replace -d <sha-of-A>
Scenario: your project's git history only goes back to 2019, but you have an older repo with history to 2015. You want to connect them without a destructive rebase.
# In the newer repo, create a replacement for the root commit
# that points to the last commit in the old repo
git replace --graft <new-root-sha> <old-repo-last-sha>
# Now git log shows continuous history back to 2015
# Anyone who fetches gets the replacement if you push it
git push origin 'refs/replace/*'
git push origin 'refs/replace/*'
git fetch origin '+refs/replace/*:refs/replace/*'
git-branchless provides a Mercurial-style smartlog, stack-aware navigation, and safe history rewriting. Particularly useful for anonymous branch workflows.
# Arch: yay -S git-branchless
# or
cargo install git-branchless
cd ~/dev/linux
git branchless init
git sl
# or
git branchless smartlog
# Output shows a DAG of your local commits relative to main:
# O abc1234 main
# |
# o def5678 (HEAD) feat: add drm helper
# |
# o ghi9012 feat: add drm structure
# Move to the parent commit
git prev
# Move to the child commit (when unambiguous)
git next
# Move multiple steps
git prev 3
git next 2
# When there are multiple children, pick one
git next --interactive
# After main advances, rebase your whole stack onto it
git restack
# Equivalent to: rebase each commit in topological order,
# respecting parent relationships, updating all refs.
# Move a commit to be on top of a different base
git move --base <sha> --onto main
# Move a range
git move --base <old-base-sha> --onto <new-base-sha>
# Fetch upstream and restack all local work on top
git sync
# Equivalent to: git fetch + git restack
# Hide a commit from smartlog without deleting it
git hide <sha>
# Bring it back
git unhide <sha>
# Undo the last operation
git undo
Ranked by leverage: how much workflow pain each tool removes relative to the time to learn and configure it.
| Rank | Tool / Setting | Pain Removed | Time to Learn |
|---|---|---|---|
| 1 | rerere.enabled + autoupdate |
Eliminates repeated conflict resolution on long-lived branches | 5 minutes |
| 2 | merge.conflictstyle = zdiff3 |
Makes conflicts readable; you see base + both sides | 1 minute |
| 3 | rebase.updateRefs = true |
Stacked branches just work after rebase | 1 minute |
| 4 | diff.algorithm = histogram |
Better diffs, fewer false matches in code | 1 minute |
| 5 | b4 send / am | Full patch series management with automatic range-diff embedding | 30 minutes |
| 6 | git worktree (bare pattern) | Parallel branch work with no stashing | 20 minutes |
| 7 | git-branchless smartlog + restack | Visual stack + safe automated rebase | 45 minutes |
| 8 | git-autofixup + autosquash | Automatic fixup commit targeting; eliminates manual rebase -i surgery | 20 minutes |
| 9 | patchutils (filterdiff / interdiff) | Patch surgery without git history; useful for stable backports | 30 minutes |
| 10 | git format-patch --range-diff + --base=auto | Clean re-roll submission with embedded diff; reviewers thank you | 15 minutes |
| 11 | lei | Searchable mailing list archive locally without subscribing | 45 minutes |
| 12 | quilt (Greg KH model) | Patch queue management for stable/backport trees | 1 hour |
| 13 | Junio's seen/next/master/maint model | Formal topic graduation; only needed if you maintain a public tree | 1 hour |
| 14 | git notes | Review metadata without commit mutation | 15 minutes |
| 15 | git replace | Non-destructive history surgery; niche but irreplaceable when needed | 20 minutes |
The first four are pure config with no UX change other than better behavior. Set them globally, forget about them, and reap the benefit forever. Everything else requires a workflow shift, but each one has a concrete break-even point within the first week of use.