Add whole-diff hash option to GetPatchID

Closes #7350

GetPatchID pipes git diff --full-index into git patch-id --verbatim. --verbatim keeps whitespace, which we need, but it also implies --stable, which hashes each file diff separately and adds the digests together. That sum only makes the ID independent of the order the file diffs appear in, and Gitaly generates the diff itself, so the order never varies. --unstable hashes the whole stream instead, but git refuses to combine it with --verbatim.

This adds a whole_diff_hash option to GetPatchIDRequest. When set, git patch-id is dropped from the pipeline and the diff is normalized in-process the way builtin/patch-id.c:get_one_patchid() does with --verbatim, then hashed once with SHA256. patch_id is then 64 hex characters instead of 40. The option is opt-in, so nothing changes for existing callers.

Hunk headers and index lines stay out of the hash, so neither line offsets nor blob IDs affect the result — that is what lets a patch ID survive a rebase onto a base that moved.

Divergence from git

Our normalization matches git's except that we also hash the file header that terminates a binary diff, which git drops. It matters for a mode-only change directly after a binary diff: such a file diff has no index/---/+++ lines, so git hashes nothing identifying it, and two patches that touch the same binary but chmod different files get the same ID. Reproduced with git itself.

Relatedly, a binary diff without a usable index line is treated as an error rather than hashing nothing, since with --full-index it should always be present.

Performance

The diff is read with ReadSlice(3) rather than ReadBytes(3) to avoid allocating per line. On a synthetic ~40MB diff that roughly halves normalization time (5.2ms vs 11ms) and drops allocations from ~205k to 4.

Testing

TestNormalizeDiff_gitParity checks the normalization against git rather than against our reading of git: for a single-file patch git's per-file sum is the identity, so git patch-id --verbatim is an exact oracle. TestNormalizeDiff_gitParityMultiFile extends that to multi-file patches by splitting per file and reproducing git's carry-sum. Plus end-to-end coverage for binary patches and hard-error cases.

Passes under both sha1 and sha256 object formats.

Note for callers

context_free: true is not implied by this option and should be passed as well where a patch ID needs to survive a rebase onto a new merge base. The result does not match git cherry, which uses the patch-ids.c implementation (whitespace stripped, three lines of context, additive combine).

Merge request reports

Loading
Loading