Skip to content

Git patch-based collaboration: format-patch, am, apply, send-email

Patch-based collaboration ships changes as email-style patch files instead of pushing to a shared remote. It predates pull requests and still dominates kernel-style development, but it earns its keep in DevOps for a different reason: you can move a change between environments that have no network path between them, no shared remote, or no write access to the target repo.

A patch file is a complete commit in text form. It carries the diff, the commit message, authorship, and timestamps. git am can turn it back into a real commit with the original author intact, which makes it useful for air-gapped promotion, cross-org contributions, and reviewing exactly what a CI pipeline will apply.


When to reach for patches instead of a remote

Push-based flows need a reachable server and write credentials on both ends. Patches remove both constraints:

  • Air-gapped or isolated networks. A pipeline in a restricted VPC can consume a patch bundle produced by a build on the public internet, transferred as a single file artifact.
  • No write access to the target. Upstream projects, vendor forks, or a read-only mirror accept contribution as patches.
  • Change-set review before merge. git apply --check and git am -3 validate a change against a specific tree without touching a branch.
  • Backport pipelines. Generate a patch from a commit on main, apply it onto a release branch, commit, and push. The patch file is the reviewed artifact that moves through environments.
  • Audit trails. Patch files are immutable text. They can be hashed, signed, and archived as evidence of exactly what changed.

Patches complement, not replace, the bundle workflow. git bundle moves whole object graphs (see git-bundle-shallow-clone). Patches move one or more commits as human-readable text.


Generating patches: git format-patch

git format-patch exports commits as numbered .patch files, one per commit, in the selected range.

# Export the last 3 commits as 0001-*.patch, 0002-*.patch, 0003-*.patch
git format-patch -3

# Export a commit range into a specific directory
git format-patch main..feature -o /tmp/patches

# Export a single commit
git format-patch -1 <sha>

# Include a cover letter for the series
git format-patch main..feature --cover-letter -o /tmp/patches

Each patch embeds the full commit metadata, so git am on the receiving side creates an identical commit including author name, email, and date.

Flags that matter in automation:

Flag Effect
--stdout Print the whole series as one stream. Pipe it into git am in a pipeline or store as a single .mbox file.
--signoff Append a Signed-off-by trailer to each patch. Useful when the sender must certify authorship.
--rfc / --subject-prefix Rewrite the subject prefix, e.g. [RFC PATCH], [PATCH v2], or a custom [CI REVIEW].
--minimal Omit the diffstat and summary headers for compact patches.
--binary Include full binary diffs so binary files survive the round trip.
--no-renames Emit full deltas instead of rename records. Matters when the consumer runs an older Git without rename detection.
--base=<sha> Record the base commit in the patch trailer so git am --3way can find a merge base.

Series structure

/tmp/patches/
  0001-feat-ci-add-promote-binary-step.patch
  0002-fix-ci-escape-version-string.patch
  0003-chore-ci-pin-runner-image.patch

Numbering preserves order. git am applies them in order, and each becomes its own commit.


Applying patches: git am

git am is the mail-format apply. It reads the commit metadata from the patch and creates a commit per patch.

# Apply a single patch file
git am /tmp/patches/0001-feat-ci.patch

# Apply a whole series from a directory
git am /tmp/patches/*.patch

# Apply a stream from stdin (pairs with format-patch --stdout)
git format-patch main..feature --stdout | git am

# Apply without committing; inspect the result first
git am --no-commit /tmp/patches/0001-feat-ci.patch

The critical flag for CI and release branches is --3way. When the patch context does not match the current tree exactly, git am falls back to a three-way merge using the blob IDs embedded in the patch. Without it, an out-of-context patch fails outright.

git am --3way /tmp/patches/*.patch

Handling a failed apply

git am records the failure state in .git/rebase-apply. You have three recovery paths:

# Inspect what failed
git am --show-current-patch

# Fix the conflict in the working tree, stage it, then continue the series
git add .
git am --continue

# Skip this patch and move to the next
git am --skip

# Abort the whole am session and restore the pre-apply state
git am --abort

--continue opens your editor for the commit message unless the patch already contains it. To skip the editor and keep the patch's message:

git am -3 --resolved

--resolved continues after a conflict using the message embedded in the patch, no editor prompt. That keeps automation non-interactive.

Whitespace policy

Patches carrying accidental whitespace changes can silently corrupt a clean tree. am applies the apply.whitespace config. The safe default for pipelines:

git config apply.whitespace nowarn   # accept, do not warn
git config apply.whitespace warn      # report but proceed
git config apply.whitespace error     # fail on whitespace errors

In a CI gate, use error so a sloppy patch cannot land silently.


Applying raw diffs: git apply

git apply applies a plain diff to the working tree and index without creating commits and without requiring mailbox format. Use it when you want the change applied but committed later under the pipeline's own identity.

# Dry-run check against the current tree
git apply --check /tmp/change.diff

# Apply to working tree only
git apply /tmp/change.diff

# Apply to index and working tree
git apply --index /tmp/change.diff

# Three-way fallback when context is stale
git apply --3way /tmp/change.diff

# Reverse a previously applied patch (safer than hand-editing)
git apply -R /tmp/change.diff

The --check gate is the CI pattern. Run it before any state mutation:

if ! git apply --check --3way /tmp/change.diff; then
  echo "patch does not apply cleanly"
  exit 1
fi

Unlike git am, git apply ignores commit metadata entirely. A raw diff has none. If authorship matters, use format-patch/am instead.


Sending patches by email: git send-email

The kernel workflow sends patch series as email. git send-email formats outgoing mail from format-patch output and delivers it via SMTP.

# Configure once
git config --global sendemail.smtpserver smtp.example.com
git config --global sendemail.smtpencryption tls
git config --global sendemail.smtpserverport 587
git config --global sendemail.from "ops@example.com"

# Send the series to a list
git send-email --to=dev@example.com /tmp/patches/*.patch

For most DevOps teams this is the least-used path today. The format matters more than the transport: patch mailboxes travel over any medium once generated, including artifact stores and S3 buckets.


Patch lifecycle in a DevOps pipeline

Pattern 1: promote a reviewed change across environments

A change validated in staging moves to production as a patch artifact, not a branch push.

# On the validating side: export the exact commits that were tested
git format-patch -1 <sha> --stdout > change-$(git rev-parse --short <sha>).patch

# Hash it for the audit trail
sha256sum change-*.patch > change-*.patch.sha256

The production pipeline fetches the artifact, checks the hash against the signed manifest, applies the patch, runs its own tests, and commits:

git am --3way --signoff change-*.patch

Pattern 2: backport pipeline

Cherry-picking across long-lived branches works, but a patch-based backport keeps the reviewed artifact explicit and works when the source and target repos have no shared remote.

# Generate from the source repo
git format-patch -1 <sha> -o /out/backport/

# On the release branch in the target repo
git am --3way /out/backport/0001-*.patch

When the release branch has drifted, --3way produces a conflict. Resolve it, run the release tests, and git am --continue. The resulting commit keeps the original author and message.

Pattern 3: consume vendor or partner patches

Trusted third parties hand over a patch series instead of a fork. The ingestion job is the write gate:

git apply --check --3way vendor-change.diff || exit 1
git apply --3way vendor-change.diff
git add -A
git commit -m "apply vendor change: $(head -1 vendor-change.diff)"

Pattern 4: rebase a series without a remote branch

Mutating a patch series in place (reorder, drop, squash) is an interactive rebase over the am state:

git am /tmp/patches/*.patch
git rebase -i <base>
git format-patch <base> -o /tmp/patches-v2/

This gives you a clean v2 artifact without ever pushing an intermediate state.


Verifying and inspecting patches

Before applying anything from outside, inspect it. git apply --check validates the diff; git mailinfo and git mailsplit dissect the mailbox structure.

# Show what the patch claims to change without applying
git apply --stat /tmp/change.diff

# Show the commit message and authorship embedded in a mail-format patch
git mailinfo /tmp/msg.txt /tmp/patch.diff < 0001-fix-ci.patch
cat /tmp/msg.txt

# Split an mbox stream into individual patches
git mailsplit -o /tmp/parts < series.mbox

For a signed series, verify who authored it:

git am --show-current-patch > current.patch
grep -E "^(From:|Subject:|Signed-off-by:)" current.patch

Interop with other workflows in this repo

  • Pair with git-range-diff to compare patch series revisions: git range-diff a re-rolled branch against the previous export to show exactly what changed between v1 and v2 before send-email or artifact sync.
  • Pair with git-bundle-shallow-clone when you need full object graph transport across an air gap; patches cover change review and atomic application, bundles cover history.
  • Pair with git-commit-signing to sign the patch mailboxes you ship, so consumers can verify content integrity before git am.
  • Pair with git-notes-ci-annotations if you want to attach the promotion metadata to the created commit after git am without rewriting it.
  • git am and git apply respect the same clean/smudge filter drivers documented in git-attributes-filter-drivers, so secret scrubbing in the export path stays consistent.

Pitfalls

  • Patch context goes stale fast. The more the target tree drifts, the more --3way conflicts you will resolve. Regenerate patches near the moment of application, or pin the base with --base=<sha>.
  • Binary files break plain patches. Without --binary, Git replaces binary content with a placeholder and the apply fails. Always pass --binary when the range may contain binaries.
  • git am demands a clean tree. It refuses to run with uncommitted changes. Apply onto a fresh worktree (git worktree add) when in doubt.
  • Line endings. A patch generated on Linux applied on a Windows consumer tree without core.autocrlf agreement produces whitespace noise. Normalize with .gitattributes first, or the eol attribute will fight the patch.
  • git apply vs git am confusion. git apply never creates commits. If your pipeline needs a commit with authorship, use am, not apply plus a synthetic commit-tree.
  • Non-interactive environment. git am --continue opens an editor when the patch lacks a message. Use --resolved or --no-edit-style flows in scripts, or set GIT_EDITOR=true.

Verification

Confirm both directions of the round trip before trusting the flow in automation:

# Export one commit, reset it away, apply it back, compare trees
git format-patch -1 HEAD --stdout > verify.patch
git reset --hard HEAD~1
git am verify.patch
git diff HEAD^ HEAD --stat           # the change is back

The patch round-trip is lossless when the resulting commit's tree matches the original:

git diff <original-sha> HEAD --stat   # empty output means identical trees