~kris/dots

srice

ref: 178c9a25a484ead715392d50e21d03df486b8b41 srice/doc/patch-workflow.md -rw-r--r-- 27.0 KiB
178c9a25 — Kris Yotam conky: sync proper config from moirai (was the greenred variant) 2 months ago

#Patch-Based Git Workflows

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.


#1. Kernel Maintainer Git Config

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.


#2. b4 Toolchain

b4 is the current standard for kernel patch submission. It handles threading, versioning, trailers, and sending.

#Installation

pip install b4
# or
pipx install b4

#Prep: Setting Up a Series

# 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

#Send: Submitting to a List

# 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:

  • embeds a range-diff against the previous revision
  • threads the re-roll under the v1 cover letter
  • handles the [PATCH v2 0/N] prefix

#am: Applying a Series from the List

# 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>

#shazam: Apply + Sign-off

# 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>

#diff: Compare Series Versions

# 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>

#3. lei: Searching Mailing Lists Without Subscribing

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.


#4. git format-patch + range-diff

#Basic format-patch

# 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.

#Embedding range-diff in the Cover Letter

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.

#range-diff standalone

# 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.


#5. git send-email Power Usage

#Basic Invocation

# Send all patches in a directory
git send-email /tmp/patches/*.patch

# Send a formatted range directly
git send-email origin/main..HEAD

#Re-rolls: Threading Under the Original Cover Letter

# 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.

#Identity Profiles

# ~/.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

#Safety Options

# 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.


#6. Josef Bacik's Review Aliases

Josef Bacik (btrfs maintainer) uses a workflow that lets him walk a patch series commit by commit, opening vimdiff at each step.

#Aliases

# 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"

#Usage Walkthrough

# 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

#7. Greg KH's Stable Kernel Workflow with Quilt

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.

#Setup

# 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

#Daily Workflow

# 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

#Backporting a Fix to Stable

# 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

#8. Junio Hamano's Integration Branch Model

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.

#The Four Branches

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

#Graduation Flow

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.

#Maintaining This Model

# 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)

#What-Cooking Document

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

#9. git worktree Bare Repo Pattern

The bare repo pattern lets you work on multiple branches simultaneously without stashing or switching. Each branch gets its own directory.

#Setup

# 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

#Daily Use

# 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

#Why Bare

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.


#10. git rerere for Repeated Conflicts

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.

#Configuration

[rerere]
    enabled = true
    autoupdate = true   # stage the resolved file automatically

#Basic Usage

# 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.

#Training from Existing Merges

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

#Inspecting and Forgetting

# 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

#When rerere Saves You

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.


#11. Stacked Branches with --update-refs

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.

#Setup

# Enable globally (git >= 2.38)
git config --global rebase.updateRefs true

#Building a Stack

# 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 ...

#Rebasing the Whole Stack

# 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.

#Explicit Per-Rebase Control

# 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

#Checking the Stack

# See the stack visually
git log --oneline --graph feature/part3

# See which refs are on which commits
git log --oneline --decorate feature/part3

#12. git-autofixup + autosquash Workflow

git-autofixup generates fixup commits targeting the right commit automatically, based on the blame of changed lines.

#Installation

# Install git-autofixup
cpan App::GitAutofixup
# or
pip install git-autofixup   # some distributions package it here

#Workflow

# 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

#Manual autosquash

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

#Amending Specific Commits Without Interactive Rebase

# 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.


#13. Suckless Patch Management

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.

#Initial Setup

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

#Adding a Patch

# 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"

#Building Your Combined Branch

# 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

#Surviving Upstream Updates

# 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

#Pushing to Your Remote

# Push all branches
git push origin upstream build patch/systray patch/fullgaps

#14. patchutils: interdiff, filterdiff, lsdiff, splitdiff, combinediff

patchutils provides tools for manipulating patch files directly, without applying them.

# Arch: sudo pacman -S patchutils

#interdiff: Diff Between Two Patches

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: List Files Affected by a Patch

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

#filterdiff: Extract or Exclude Hunks

# 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: Split a Multi-file Patch Into Per-file Patches

splitdiff -a big-series.patch
# Creates 001-drivers_net_foo.patch, 002-include_linux_bar.patch, ...

splitdiff -D /tmp/split-patches big-series.patch

#combinediff: Combine Two Patches into One

# 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.


#15. git notes for Review Metadata

git notes attach arbitrary text to commits without changing the commit SHA. Useful for recording review status, test results, or stable-queue tags.

#Basic Usage

# 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>

#Custom Note Namespaces

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

#Sharing Notes

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/*'

#16. git replace for Non-Destructive History

git replace creates a redirect: references to the original SHA transparently use the replacement instead. The original object is unchanged.

#Use Cases

  • Grafting a project's history onto a different parent
  • Replacing a commit with a fixed version without rewriting history
  • Splicing two repos together

#Basic Replace

# 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>

#Grafting History

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/*'

#Sharing Replacements

git push origin 'refs/replace/*'
git fetch origin '+refs/replace/*:refs/replace/*'

#17. git-branchless

git-branchless provides a Mercurial-style smartlog, stack-aware navigation, and safe history rewriting. Particularly useful for anonymous branch workflows.

#Installation

# Arch: yay -S git-branchless
# or
cargo install git-branchless

#Setup

cd ~/dev/linux
git branchless init

#smartlog: Visual Stack Overview

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

#restack: Rebase an Entire Stack

# 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: Relocate Commits

# 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>

#sync: Pull and Restack

# Fetch upstream and restack all local work on top
git sync

# Equivalent to: git fetch + git restack

#hide/unhide

# Hide a commit from smartlog without deleting it
git hide <sha>

# Bring it back
git unhide <sha>

# Undo the last operation
git undo

#Speed Stack Ranking

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.