Build and Check a Git Patch Series with format-patch
Sending a pull request is not always an option, and that is what git format-patch is for. It turns a known revision range into numbered patch files a reviewer can read or apply with git am. This guide also checks the range first, because a single-commit argument does not quietly mean what you think it means. Allow about fifteen minutes. You need Git and a repository with at least two commits, no elevated privileges required.
The route
Jump straight to the step you need, or tick off Done means at the end.
The examples here use Git 2.43.0 from Ubuntu package git-man 1:2.43.0-1ubuntu7.3. Git configuration can silently change defaults such as the output directory, subject prefix, threading and sign-off behaviour, so the explicit options below are the contract worth reviewing.
1. Check the repository and choose the range
First confirm you are in the repository you actually mean to publish. This step is read-only:
$ git rev-parse --show-toplevel
/path/to/project
$ git status --short
$ git log --oneline --decorate -n 5
For a series sitting on your current branch but not on origin/main, use origin/main..HEAD. That two-dot range means commits reachable from HEAD but not from origin/main. Preview exactly what it will select:
$ git log --oneline origin/main..HEAD
7c91f40 Add input validation
3a8e1b2 Document the configuration file
$ git diff --stat origin/main..HEAD
config/app.conf | 8 ++++++++
1 file changed, 8 insertions(+)
Checkpoint: if this log includes a commit that has no business in the series, stop and fix the range before generating anything. A clean working tree is not required for format-patch, but checking it first avoids confusion about changes that never got committed and so cannot appear in the series.
2. Write the series to a new directory
Create an output directory and pass it explicitly. Git will create missing directory components for --output-directory, and a fresh directory keeps generated files well away from source files.
$ mkdir -p /tmp/project-patches-v1
$ git format-patch --output-directory /tmp/project-patches-v1 origin/main..HEAD
/tmp/project-patches-v1/0001-Document-the-configuration-file.patch
/tmp/project-patches-v1/0002-Add-input-validation.patch
Each non-merge commit becomes one mbox-style message: a From <commit> marker, the author and subject, the commit message, then a three-dash separator before the diff. Merge commits are left out entirely, a simple patch cannot carry enough information to recreate one.
Do not use shell redirection to build these files yourself. git format-patch already picks safe names from the commit subjects and prints each path. If a directory already holds patches you care about, pick a different one, the command will not rewrite history but rerunning it can happily overwrite files with matching names.
3. Inspect the first patch before sending it
Read the headers and the separator, then compare the diff against the preview from step 1:
$ sed -n '1,30p' /tmp/project-patches-v1/0001-*.patch
From 3a8e1b2... Mon Sep 17 00:00:00 2001
From: Developer Name <[email protected]>
Subject: [PATCH] Document the configuration file
...commit message...
---
... files changed ...
diff --git a/config/app.conf b/config/app.conf
That fixed date in the first From line is a format marker, not the actual commit date. The subject usually comes straight from the first paragraph of the commit message. For a multi-patch series Git uses subjects like [PATCH 1/2]; for one patch it just says [PATCH].
Use git apply --check when you want to test the diff mechanics without creating a commit:
$ git apply --check /tmp/project-patches-v1/0001-Document-the-configuration-file.patch
$ printf 'check status: %s\n' "$?"
check status: 0
This only checks whether the diff applies to the current files, not whether the chosen history or commit message makes sense. Hang on to the original patch directory until the recipient confirms the series is good.
4. Make numbering and output unambiguous
Use --numbered-files when some other tool expects names containing nothing but sequence numbers:
$ mkdir -p /tmp/project-patches-v1-numbered
$ git format-patch --numbered-files --output-directory /tmp/project-patches-v1-numbered origin/main..HEAD
/tmp/project-patches-v1-numbered/1
/tmp/project-patches-v1-numbered/2
For a single-commit fix, -1 HEAD means exactly that commit. Without -1, though, a lone commit name is read as a <since> boundary, and Git formats everything leading up to it. Formatting from the very beginning of history needs --root.
For a pipeline or a mail tool reading from standard input, use --stdout. You get one mbox stream instead of separate files:
$ git format-patch --stdout origin/main..HEAD > /tmp/project-series.mbox
$ file /tmp/project-series.mbox
/tmp/project-series.mbox: Mailbox file, ASCII text
Redirection truncates an existing destination before Git even runs. Pick a new path, or write to a temporary file first and inspect it before it replaces a previous mailbox. If the command then fails, that old file is gone unless you had a backup.
5. Prepare a reviewable series
Add a cover letter for a multi-commit series when reviewers need context beyond the individual commit messages:
$ git format-patch --cover-letter --subject-prefix='PATCH project' \
--output-directory /tmp/project-patches-v1 origin/main..HEAD
/tmp/project-patches-v1/0000-cover-letter.patch
/tmp/project-patches-v1/0001-Document-the-configuration-file.patch
/tmp/project-patches-v1/0002-Add-input-validation.patch
The cover letter comes with a branch description, a shortlog and an overall diffstat. Edit its description before sending it anywhere. The subject prefix becomes [PATCH project], which helps a shared mailing list tell this series apart from patches for other projects.
For a second revision, use --reroll-count=2, which adds v2 to names and subjects. If you are updating a series reviewers have already seen, a range-diff usually beats asking them to rediscover every change themselves:
$ git format-patch --cover-letter --reroll-count=2 \
--range-diff=feature-v1 --output-directory /tmp/project-patches-v2 \
origin/main..HEAD
feature-v1 has to name the tip of the earlier series and share its base with the new one. Check the cover letter afterwards: the range-diff is reviewer commentary, it is not part of the commit messages themselves.
6. Set only the mail metadata you actually intend
Use --to and --cc to add recipient headers. Neither one sends mail:
$ git format-patch [email protected] \
[email protected] --output-directory /tmp/project-patches-v1 \
origin/main..HEAD
These can also be configured under [format], alongside subjectPrefix, suffix, numbered, signOff, outputDirectory and coverLetter. Check inherited configuration before trusting a short command at face value:
$ git config --show-origin --get-regexp '^format\.'
file:/home/me/.gitconfig format.subjectPrefix PATCH project
A repository or global setting can silently add headers or change where files land. An explicit --output-directory wins over format.outputDirectory. Treat --signoff as a statement you are actually authorised to make: it adds your committer identity as a Signed-off-by trailer, and it is not a cryptographic signature.
7. Apply a received series safely
On a disposable test branch, inspect the files first, then let git am turn the mbox messages back into commits:
$ git switch -c review-project-series origin/main
$ git am --3way /tmp/project-patches-v1/*.patch
Applying: Document the configuration file
Applying: Add input validation
Do this only after checking the sender and the patch content yourself. Applying a patch changes both repository history and the working tree. If it stops on a conflict, resolve it, stage the fix, and run git am --continue. To abandon the in-progress application entirely and return to the pre-git am state, run:
$ git am --abort
Do not reach for git am --abort after resolving a conflict unless you genuinely want to throw away that in-progress application. It leaves the original patch files alone, it just removes the temporary application state and restores the branch.
Done means
- The range was right:
git log origin/main..HEADshowed exactly the commits you meant to publish. - The series exists: each selected non-merge commit has a numbered patch in a deliberate output directory.
- You checked the content: the patch headers, subjects, three-dash separators and diffs were inspected.
- Metadata is intentional: any cover letter, recipients, subject prefix, sign-off and reroll number reflect a real decision.
- Nothing was deleted early: the generated files are kept until the recipient confirms the series applies cleanly.