A patch handoff can mean two very different things: deliver the final file changes, or replay a sequence of commits with their explanations and authorship. Git diff vs format-patch is therefore a question about the information being transferred, not a contest between filename extensions. Choosing the wrong contract can produce correct-looking files while losing the history that a reviewer expected to receive.
The practical rule is straightforward. Use diff and apply when the deliverable is a content change. Use format-patch and am when the deliverable is a replayable series of non-merge commits. Then verify the base, the resulting files, and the history separately. Matching file trees do not prove matching commit IDs, and a command that accepts a patch does not prove that the resulting application is correct.
Start with the information the recipient needs
Choose a content patch when the recipient only needs a file delta. Choose a commit series when individual messages, authorship, and change boundaries matter. Make that requirement explicit before selecting commands, because the filename alone cannot establish what information a patch contains.
The local source note distinguishes a diff containing file changes from a format-patch output carrying commit information. That is a useful starting model, but it needs qualifications. A diff between two commits describes their endpoint differences, not every explanation recorded along the way. Format-patch prepares messages for selected non-merge commits; it is not a complete backup of arbitrary repository history. Empty commits can also be omitted by default.
A file ending in .patch is not necessarily a mailbox-style commit message. Renaming a plain diff does not add an author or a commit description. Conversely, format-patch with --stdout can place multiple messages in one mailbox file rather than creating one output file per commit. Inspect the generation command and the actual structure instead of choosing an importer from the extension.
Consider a documentation change made in two stages: first add a missing example, then explain its limitations. A recipient who only needs the final document may prefer a single delta. A reviewer evaluating the reasoning and sequence may need both commits. Neither choice is universally better. The important issue is whether the artifact preserves the information promised by the handoff.
This article covers ordinary text and a linear sequence. It does not establish behavior for merge topology, submodules, or large binary assets. Those cases need their own acceptance criteria rather than an assumption that a small text experiment covers everything Git can represent.
A diff compares endpoints, not the story between them
A diff between two commits shows the content change from one endpoint to the other. Intermediate edits and their explanations are not automatically preserved. Record both endpoints when reviewing a patch so that the recipient knows which starting state the comparison assumes.
There are also different kinds of local diff. With no commit argument, git diff compares the working tree with the index. With --cached, it compares the index with the specified commit, usually HEAD when no commit is given. A staged edit can disappear from the first view while remaining visible in the second. Treat these as separate questions rather than interchangeable ways to inspect all pending work.
The experiment below begins with a file containing alpha. Two subsequent commits add beta and gamma. Comparing the base with the final commit produces a delta that can reproduce the final file, but that delta does not contain the two commit-message boundaries. Applying it at the base leaves HEAD at the base. That is the expected responsibility of the chosen command, not a sign that Git malfunctioned.
Range-looking syntax can be another source of confusion. The official diff documentation states that git diff A..B is synonymous with comparing the two endpoints. Do not import the revision-selection meaning of a range from another command and assume that diff will enumerate each commit. To verify the history that was transferred, inspect commits and messages explicitly.
A file-level summary also cannot explain why a change was made. It may help a reviewer establish scope, but it does not replace the commit descriptions or the application tests. The handoff should distinguish evidence of content, evidence of history, and evidence of behavior rather than collapsing all three into a single success flag.
apply and am change different parts of repository state
Plain apply changes files without creating commits, while am creates commits from mailbox messages on the current branch. Verify the working tree, the index, and history independently. A successful exit code is necessary for the operation, but it is not a complete acceptance test.
The apply documentation separates its default behavior from --index and --cached. The default applies to files. The first option also handles the index; the second handles only the index. Adding --check checks applicability instead of applying the patch. This article tests plain apply and a separate check invocation, not every combination of those switches.
Am extracts authorship information, the author date, the message, and the patch to construct commits. Replaying that information does not promise the same commit object. A different committer date, parent, or other commit content can change the identifier even when the final file tree matches. The lab deliberately changes the receiving committer date so that this distinction becomes observable rather than merely theoretical.
This matters when defining review evidence. If the requirement is equal file content, compare the trees. If it is a two-step change with preserved explanations, also count commits and check their ordered subjects. If it requires original object identity or topology, do not assume a mailbox replay meets that requirement. State a different delivery contract and test it directly.
A recipient who only wants to review should not immediately run am in an existing working repository. Read the artifact, establish its intended base, and use an isolated test directory first. When applicability fails, investigate the cause rather than treating options that relax checks as the default response. A base mismatch can be useful evidence that the proposed handoff is incomplete.
Verification: three reproducible checks in an offline sandbox
Build synthetic commits in a new local directory, then verify that checking does not write, applying does not commit, and mailbox replay preserves commit boundaries. Each expectation is an assertion. Any unexpected state change or command failure must stop the experiment rather than being counted as partial success.
The fixed baseline is Windows with Git for Windows 2.38.1.windows.1 and Python 3.11.15. These identify the actual experiment, not an upgrade recommendation or a claim of current security suitability. Do not downgrade everyday tools to follow this article. Test other versions separately and label those results; the supplied script intentionally rejects a different version rather than silently treating it as the same run.
Prerequisites are both executables on the command path, a writable local temporary directory, and the complete script below saved as git_patch_lab.py. It creates a new repository and a separate worktree, retains the generated files, and does not delete existing data. All author identifiers are non-deliverable demonstration values, not real accounts. The script isolates inherited Git settings, uses empty template and hooks directories, and pins line-ending behavior.
- Run
git --versionandpython --version, then runpython git_patch_lab.pyafter confirming the fixed versions. The first check callsgit apply --checkand requires unchanged file bytes, index, and HEAD. A command failure or any mutation fails this check. - The same script next calls plain
git apply. It requires exactly the alpha, beta, and gamma lines, an unchanged HEAD, and an empty staged diff. Correct-looking text accompanied by an unexpected commit or staged change is a failure. - In another new worktree, the script runs
git amand requires two commits in the expected order, matching subjects, retained authors and author dates, an equal final tree, and a different final commit ID. Every assertion must pass before accepting the run.
"""Offline, synthetic Git patch semantics verification. No existing repositories touched."""
import os
from pathlib import Path
import subprocess
import tempfile
import sys
assert sys.version_info[:3] == (3, 11, 15), 'Use Python 3.11.15'
assert subprocess.check_output(['git', '--version'], text=True).strip() == 'git version 2.38.1.windows.1', 'Use Git for Windows 2.38.1.windows.1'
root = Path(tempfile.mkdtemp(prefix='patch-semantics-'))
(root / 'empty.config').touch()
(root / 'hooks').mkdir()
(root / 'template').mkdir()
env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')}
env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=str(root / 'empty.config'), GIT_TERMINAL_PROMPT='0', GIT_AUTHOR_NAME='Example Author', GIT_AUTHOR_EMAIL='author@example.invalid', GIT_COMMITTER_NAME='Example Integrator', GIT_COMMITTER_EMAIL='integrator@example.invalid', GIT_AUTHOR_DATE='2020-01-01T00:00:00+00:00', GIT_COMMITTER_DATE='2020-01-02T00:00:00+00:00')
repo = root / 'repo'
repo.mkdir()
def git(*args, ok=True):
result = subprocess.run(['git', '-c', 'core.autocrlf=false', '-c', 'commit.gpgsign=false', '-c', 'core.hooksPath=' + str(root / 'hooks'), *args], cwd=repo, env=env, capture_output=True)
if ok and result.returncode:
raise RuntimeError(result.stderr.decode('utf-8', 'replace'))
return result
git('init', '--template=' + str(root / 'template'))
f = repo / 'sample.txt'
f.write_bytes(b'alpha\n')
git('add', 'sample.txt')
git('commit', '-m', 'Base')
base = git('rev-parse', 'HEAD').stdout.strip().decode()
f.write_bytes(b'alpha\nbeta\n')
git('add', 'sample.txt')
git('commit', '-m', 'Add beta')
f.write_bytes(b'alpha\nbeta\ngamma\n')
git('add', 'sample.txt')
git('commit', '-m', 'Add gamma')
tip = git('rev-parse', 'HEAD').stdout.strip().decode()
diff = root / 'change.diff'
mbox = root / 'series.mbox'
diff.write_bytes(git('diff', base, tip, '--', 'sample.txt').stdout)
mbox.write_bytes(git('format-patch', '--stdout', '--no-signature', base + '..' + tip).stdout)
git('checkout', '-b', 'apply-check', base)
before = f.read_bytes()
git('apply', '--check', str(diff))
assert f.read_bytes() == before
assert git('status', '--porcelain').stdout == b''
assert git('rev-parse', 'HEAD').stdout.strip().decode() == base
print('PASS check: working tree, index, and HEAD unchanged')
git('apply', str(diff))
assert f.read_bytes() == b'alpha\nbeta\ngamma\n'
assert git('rev-parse', 'HEAD').stdout.strip().decode() == base
assert git('diff', '--cached').stdout == b''
print('PASS apply: target content, unchanged HEAD, empty staged diff')
am_repo = root / 'am-repo'
git('worktree', 'add', '-b', 'mail-check', str(am_repo), base)
repo = am_repo
env['GIT_COMMITTER_DATE'] = '2020-01-03T00:00:00+00:00'
git('am', str(mbox))
assert git('rev-list', '--count', base + '..HEAD').stdout.strip() == b'2'
assert git('log', '--reverse', '--format=%s', base + '..HEAD').stdout.splitlines() == [b'Add beta', b'Add gamma']
assert git('log', '--format=%an', base + '..HEAD').stdout.splitlines() == [b'Example Author', b'Example Author']
assert git('log', '--format=%aI', base + '..HEAD').stdout.splitlines() == [b'2020-01-01T00:00:00+00:00'] * 2
assert git('diff', tip, 'HEAD').stdout == b''
assert git('rev-parse', 'HEAD').stdout.strip().decode() != tip
print('PASS am: two commits, subjects/authors/dates retained, equal tree, different commit ID')
print('PASS all: offline synthetic repositories only')
The scheduler executed this complete script and received exit code zero. The no-write check, the plain-apply content and HEAD checks, and the mailbox checks for count, subjects, authors, dates, tree equality, and different commit identity all passed. These are actual local semantic checks, not a benchmark. No latency, throughput, or success-rate estimate was collected or inferred.
The script prints a short PASS line only after the relevant assertions succeed. If it fails before those lines, retain the error and the generated test data for diagnosis. Running it again creates a separate experiment rather than overwriting an existing repository. This makes the evidence easy to interpret without relying on a person's memory of the previous working state.
Investigate compatibility failures in a deliberate order
Applicability depends on the receiving content, paths, and surrounding context, not merely on the installed Git version. Establish the base and affected files first, then inspect line endings and patch structure. Only discuss specialized options after identifying the condition that made the original operation fail.
The three-line experiment does not validate binary patches, rename detection, submodules, or merge commits. In particular, the format-patch documentation explains that merge commits are omitted from its ordinary output because a simple patch cannot reproduce the same merge commit. If preserving branch topology is a requirement, reconsider the transport rather than presenting a successful text replay as proof of equivalence.
Three-way fallback also has prerequisites. The apply and am documentation describe the need for recorded blob identities and corresponding objects available locally. A switch cannot manufacture missing history or guarantee conflict resolution. This lab does not execute three-way merging and makes no claim that it will rescue a patch generated against an unknown base.
Keep the response to a failed check narrow. Preserve the diagnostic output, identify the intended base, and reproduce the problem in another fresh test directory. Do not forcibly repair unrelated working files just to make a tutorial command finish. A failure is useful when it exposes an unstated dependency or an assumption about the recipient's state.
Applicability is also separate from code review. Accepting a patch's structure and context says nothing about whether its behavior should be executed. The experiment downloads no external patches and runs no code introduced by a patch. Only the reviewed harness and self-generated text are used. Preserve that boundary when extending the test to additional cases.
Use an acceptance checklist and keep evidence in scope
Before handing off a patch, confirm its format, starting point, expected file result, and required commit boundaries. Keep the reproduction evidence alongside those expectations. Official documentation supports the command semantics, while this experiment supports only the synthetic cases it actually exercises.
- Specify whether the recipient needs final content or separately replayed commits.
- Record the generation command, endpoints, and exact tested tool versions.
- Inspect the working tree, index, and HEAD separately in a new sandbox.
- Distinguish equal file trees from equal commit identifiers.
- Mark untested cases explicitly and stop to investigate conflicts.
This topic is intentionally narrower than a version-control governance guide. Governance addresses accountability and change traceability across a team; this article answers a concrete patch-exchange question with executable state checks. It does not retell a generic collaboration policy or turn a personal-name search opportunity into irrelevant command-page targeting.
BlogPosting is the only recommended structured-data type. There is no visible FAQ section requiring FAQPage, and the article does not promise a special search presentation. The numbered lab remains useful instructional content without adding unnecessary schema claims.
The local note supplied the initial distinction between a content delta and commit-bearing patches. The fixed-version official project documents refine that distinction with endpoint semantics, apply modes, mailbox replay, and the non-merge limitation. The lab is a synthetic editorial verification example, not a record of Jerry's work, personal outcomes, or measured business improvements.