git format-patch: Email-Ready Patches in CI
git format-patch <range> writes one numbered .patch file per commit (with full author and message), the mailbox format that git am replays exactly.
When a pipeline moves commits between repos or stores them as artifacts, format-patch produces self-contained patch files that preserve authorship and message, unlike a plain git diff. The range and --stdout forms are the ones CI uses.
What it does
git format-patch generates a patch file per commit in mbox format, including author, date, subject, body, and the diff, so git am can recreate the commits faithfully. You select commits by range (origin/main..HEAD), by count (-3 for the last three), or stream them to one stream with --stdout.
Common usage
# one file per commit since origin/main
git format-patch origin/main..HEAD
# the last 3 commits
git format-patch -3
# a single combined stream (pipe into git am)
git format-patch origin/main..HEAD --stdout > series.patch
# into a directory, with a cover letter
git format-patch -o patches/ --cover-letter origin/main..HEADOptions
| Flag | What it does |
|---|---|
| <since>..<until> | Format commits in the range |
| -<n> | Format the last n commits |
| --stdout | Write all patches to stdout as one stream |
| -o <dir> | Write the .patch files into a directory |
| --cover-letter | Add a 0000 cover letter for the series |
| --signoff | Add a Signed-off-by trailer |
In CI
format-patch against a base needs that base present, so a shallow clone can silently produce too few patches or none; fetch enough history. The output of format-patch is exactly what git am consumes, so the round trip format-patch | git am preserves SHAs of content and authorship. Use --stdout to keep it to one artifact.
Common errors in CI
No files produced usually means the range was empty (base equals HEAD, or the base ref is missing on a shallow clone), not an error. "fatal: ambiguous argument 'origin/main..HEAD'" means the base ref was not fetched. On git am later, "patch does not apply" means the target tree diverged from where the patches were generated.
Using this in CI
CI checkouts are shallow and detached by default, which changes the answer this command gives you. Commands that read history, branch names, or tags need the checkout configured for it.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # history, tags, and git describe all need this
- run: |
git rev-parse --is-shallow-repository # expect false
git rev-parse --abbrev-ref HEAD # prints HEAD when detached