diff-hunks: add the store format, library, and command
Blame and "git log --stat" recover hunk coordinates by diffing blob
pairs, and recompute them on every run. Add a cache of those
coordinates at $GIT_DIR/objects/info/diff-hunks, beside the
commit-graph, so a later run can look them up instead of decompressing
the blobs and running xdiff again.
The store is a single chunk-format file (see gitformat-chunk(5)): an
8-byte header, a DHIX index of fixed-size entries sorted by key, a DHDT
segment of hunk records, and a trailing hash checksum. An entry is
keyed by the two blob object ids and the xdl_opts the pair was diffed
under, so a stored result is served only where that exact key recurs,
independent of path. A zero-context diff trims unchanged lines from
hunk edges and can pick a different but equally valid set of hunks than
an untrimmed diff, so a recording caller stores a pair only when its
trimmed and untrimmed diffs are identical; such an entry answers any
consumer at any context, and the rare divergent pair is always
computed. Identical hunk blocks are interned once and shared across
keys.
The library provides a reader (repo_diff_hunks_store and _replay, gated
by core.diffHunks), loaded once and cached on the object database as the
commit-graph is, and a writer that accumulates entries and flushes them
in one atomic pass. An absent, corrupt, or disabled store reads as all
misses. A record with no hunks is invalid too: replaying it would claim
the pair equivalent, which the store never asserts, so it reads as a
miss.
Ordinary reads are diagnostic-free. Loading parses the chunk table
through read_table_of_contents_quiet(), new in chunk-format, which
prints nothing on a malformed table and takes the repository's hash
algorithm rather than the_hash_algo, so the file is bounds-checked under
the algorithm it is keyed by.
The flush closes the repository's mmapped store and forgets that loading
was attempted before committing the lockfile. A warming run that also
reads may hold the file it is replacing mapped, and the rename must not
land on a live mapping, which Windows refuses; a read after the flush
then observes the committed file. commit-graph closes its graph before
committing for the same reason.
Writing is off by default, enabled per run by GIT_DIFF_HUNKS_WRITE or
persistently by diffHunks.write, the environment winning. A writer
seeds from the existing store, so a flush merges rather than replaces.
The seed's checksum is verified first: a corrupt store is discarded, not
rewritten with a fresh checksum verify could no longer catch. An entry
that fails the shared diff_provider_check_hunk() or names no blob is
dropped with a warning, since it would only ever read as a miss. A seed
that discarded or dropped anything forces the flush even when the
warming run computed nothing new. The writer fsyncs through a new
diff-hunks core.fsync component.
"git diff-hunks" inspects and manages the file: "verify" checks the
checksum, chunk table, sort order, entry bounds, and every entry's hunk
sequence against that shared check, so a store whose entries could only
read as misses fails verify; "clear" removes the file. Later patches
wire the readers and the writer into the diff and blame paths.
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Michael Montalbo committedAug 1, 2026 at 10:41 UTC42824db1138dbe0e64d5c49b52202f0cd30fa967
index a0ebf03e2e..9595619c61 100644--- a/Documentation/config/core.adoc+++ b/Documentation/config/core.adoc@@ -670,12 +670,13 @@ but risks losing recent work in the event of an unclean system shutdown. * `pack` hardens objects added to the repo in packfile form. * `pack-metadata` hardens packfile bitmaps and indexes. * `commit-graph` hardens the commit-graph file.+* `diff-hunks` hardens the diff-hunks store. * `index` hardens the index when it is modified. * `objects` is an aggregate option that is equivalent to `loose-object,pack`. * `reference` hardens references modified in the repo. * `derived-metadata` is an aggregate option that is equivalent to- `pack-metadata,commit-graph`.+ `pack-metadata,commit-graph,diff-hunks`. * `committed` is an aggregate option that is currently equivalent to `objects`. This mode sacrifices some performance to ensure that work that is committed to the repository with `git commit` or similar commands@@ -750,6 +751,13 @@ core.commitGraph:: to parse the graph structure of commits. Defaults to true. See linkgit:git-commit-graph[1] for more information.+core.diffHunks::+ If true, then Git will consult the diff-hunks store (if it+ exists) to skip recomputing diff hunk coordinates in commands+ such as `git log --stat` and linkgit:git-blame[1]. This controls+ only reading; writing the store is controlled by `diffHunks.write`.+ See linkgit:git-diff-hunks[1] for more information. Defaults to true.+ core.useReplaceRefs:: If set to `false`, behave as if the `--no-replace-objects` option was given on the command line. See linkgit:git[1] and
Documentation/config/diff-hunks.adoc
+8
new file mode 100644index 0000000000..ad76d1c6a9--- /dev/null+++ b/Documentation/config/diff-hunks.adoc@@ -0,0 +1,8 @@+diffHunks.write::+ If true, diff-producing commands (`git diff`, `git log`,+ `git show`, or `git diff-tree` with a `--stat`, `--numstat`, or+ `--shortstat` format) write the hunks+ they compute to the diff-hunks store, filling it as a side effect.+ The `GIT_DIFF_HUNKS_WRITE` environment variable overrides this for+ a single invocation. Reading the store is controlled separately by+ `core.diffHunks`. See linkgit:git-diff-hunks[1]. Defaults to false.
Documentation/git-diff-hunks.adoc
+146
new file mode 100644index 0000000000..25cab2ea7d--- /dev/null+++ b/Documentation/git-diff-hunks.adoc@@ -0,0 +1,146 @@+git-diff-hunks(1)+=================++NAME+----+git-diff-hunks - Inspect and manage the diff-hunks store++SYNOPSIS+--------+[synopsis]+git diff-hunks verify+git diff-hunks clear++DESCRIPTION+-----------++The diff-hunks store is a cache of diff hunk coordinates, the line+ranges that changed between two blobs, so that commands+which need them, such as linkgit:git-blame[1] and `git log` and `git diff`+with the `--stat`, `--numstat`, and `--shortstat` formats, can skip+running the diff algorithm, and blame can skip loading the blob+content. (The summary formats still test each pair for binariness,+which can load the blobs.)++The store is a single file, `$GIT_DIR/objects/info/diff-hunks`. Reading is+enabled by default; writing is off by default. A `git diff`, `git log`,+`git show`, or `git diff-tree` that produces one of the stat formats+fills the store as a side effect, but only when writing is enabled for+that run (see "WARMING THE STORE" below), so ordinary reads never+modify the repository. When the store does not have the pair, holds a+different object hash, the file is unreadable, or an object replacement+redirects one of the blobs, the consumer falls back to computing the+diff. A store only speeds up these commands; it never changes their+output.++`git diff-hunks` itself only inspects and manages the file. See+linkgit:gitformat-diff-hunks[5] for the file format.++WARMING THE STORE+-----------------++The store is filled by running ordinary commands with writing enabled.+Turn writing on for a single invocation with the `GIT_DIFF_HUNKS_WRITE`+environment variable, or persistently with the `diffHunks.write`+configuration; the environment variable takes precedence. A repository+owner warms the store by running the diff-producing commands they care+about with writing on, for example:++ GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null++A `--stat` walk records one entry per blob pair;+linkgit:git-blame[1] replays the coordinates and the summary formats+sum the counts, so a single warming walk serves both.+A warming run seeds from the existing store and rewrites the file+with the newly computed pairs merged in, so a later run adds to what+earlier runs recorded rather than discarding it.++A walk records only the pairs it diffs. `git log --all --stat` diffs+each commit against its first parent, so a blame that follows a+merge's second parent computes those pairs itself: blame coverage is+partial on history with merges. Warming with a walk that also diffs+the other parents, for example `git log --all -m --stat`, raises+blame coverage at the cost of a larger store and a longer warming+run.++COMMANDS+--------++`verify`::+ Check the integrity of the store: the trailing hash checksum, the+ chunk table of contents, the sort order of the index, and the+ bounds of every entry. Exits with non-zero status if the store is+ corrupt. An absent store is valid.++`clear`::+ Remove the store file.++CORRECTNESS+-----------++A stored result is interchangeable with a freshly computed one because an+entry is keyed by the inputs that determine the diff:++* the object IDs of the old and new blob, so a result is used only for+ the exact contents it was computed from; and+* the diff algorithm and ignore flags (`xdl_opts`) the hunks were+ computed under. A lookup whose `xdl_opts` differ from a stored entry+ misses. This is why, for example, `blame -w` and+ `--diff-algorithm=<algorithm>` (including a per-path+ `diff.<driver>.algorithm`) do not reuse entries recorded under the+ default settings: they change `xdl_opts`.++The context length is not part of the key because only trim-stable+pairs are recorded: pairs whose zero-context trimmed diff and untrimmed+diff are identical, so one entry answers blame (zero context) and the+summary formats (any context) alike. The rare pair where+the zero-context trimming optimization picks a different but+equally valid set of hunks is+never recorded and is always computed.++Some options shape the hunks in ways the key does not express, so a+diff that uses them is excluded from the store in both directions:+break detection (`-B`), `--ignore-matching-lines` (`-I`), and+`--anchored`. `--ignore-blank-lines` is different: it is an ignore+flag and therefore part of the key, but the summary formats exclude+it anyway, because it coalesces hunks differently between the code+path that emits text and the one that replays coordinates, so a+served answer would not match a store-less run.+linkgit:git-blame[1] additionally does not+consult the store for reverse blame, ignored revisions, or paths with a+textconv driver.++The store carries a trailing hash checksum, but readers do not+re-checksum it on every load. As with the commit-graph and+multi-pack-index, the writer fsyncs the file (honoring `core.fsync`) and+commits it atomically, so a committed store is intact; every offset and+count is still bounds-checked as it is read. The checksum is verified by+`git diff-hunks verify`, not on the read path, so structural corruption+that fails a bounds check is read as an absent entry, while a record+that stays within bounds but whose bytes were altered is served until+`verify` detects the mismatch.++CONFIGURATION+-------------++`core.diffHunks`::+ Whether commands read the store. Defaults to true. See+ linkgit:git-config[1].++`diffHunks.write`::+ Whether diff-producing commands write to the store. Defaults to+ false. The `GIT_DIFF_HUNKS_WRITE` environment variable overrides it+ for a single invocation. See linkgit:git-config[1].++Writing the store honors the `core.fsync` configuration through the+`diff-hunks` component; see linkgit:git-config[1].++SEE ALSO+--------+linkgit:git-blame[1],+linkgit:git-log[1],+linkgit:gitformat-diff-hunks[5]++GIT+---+Part of the linkgit:git[1] suite
Documentation/gitformat-diff-hunks.adoc
+129
new file mode 100644index 0000000000..f75ab73dd9--- /dev/null+++ b/Documentation/gitformat-diff-hunks.adoc@@ -0,0 +1,129 @@+gitformat-diff-hunks(5)+=======================++NAME+----+gitformat-diff-hunks - Precomputed diff hunk store format++SYNOPSIS+--------+[verse]+$GIT_DIR/objects/info/diff-hunks++DESCRIPTION+-----------++The diff-hunks store memoizes diff hunk coordinates so that commands+that need them, such as `git log --stat` and linkgit:git-blame[1], can+skip running the diff algorithm (and, for blame, loading the blob+content; the summary formats still test each pair for binariness,+which can load the blobs). See+linkgit:git-diff-hunks[1] for how the store is filled and managed and the+configuration that controls it.++The store is a single file, `$GIT_DIR/objects/info/diff-hunks`, written+in one pass and replaced atomically, so a reader sees either the old+file or the complete new one.++Entries are keyed by the object IDs of the blob pair that was diffed+and by the diff algorithm and ignore flags (`xdl_opts`) the pair was+diffed under. A blob pair fully determines the diff input, so an entry+is valid regardless of which commits, branches, or index states the+pair was encountered in, and identical diffs performed in different+contexts share one entry. A reader whose `xdl_opts` differ from an+entry does not match it and falls back to computing the diff.++FILE FORMAT+-----------++All multi-byte integers are stored in network byte order. The file is an+8-byte header, the chunk table of contents and chunk data described in+linkgit:gitformat-chunk[5], and a trailing checksum.++HEADER+~~~~~~++- 4-byte signature: `DHPF` (diff-hunks precomputed format)+- 1-byte version number: currently 1+- 1-byte hash version: 1 for SHA-1, 2 for SHA-256. A store whose hash+ function differs from the repository's is ignored.+- 1-byte number of chunks+- 1-byte reserved++CHUNK LOOKUP+~~~~~~~~~~~~++A table of contents in the format of linkgit:gitformat-chunk[5], listing+the offset of each chunk. Both chunks below are required; a file missing+either is treated as corrupt.++CHUNK DATA+~~~~~~~~~~++DHIX (index)::+ A sorted sequence of fixed-size entries. Each entry is the old+ blob object ID, the new blob object ID, a 4-byte `xdl_opts`+ value, and a 4-byte offset into the DHDT chunk. Entries are+ sorted by old object ID, then new object ID, then `xdl_opts`,+ so lookups can use binary search on the full key.++DHDT (hunk data)::+ For each index entry, at its offset: a 4-byte hunk count followed+ by that many 16-byte hunk records. A hunk record is four 4-byte+ values: old start, old count, new start, new count.+ Starts are 0-based line numbers in the old and new blob; counts+ are numbers of lines. The hunk count is at least 1: a record with+ no hunks would claim the blob pair equivalent, which the store+ never records, so readers treat such a record as invalid.+ Identical hunk blocks are stored once:+ distinct index entries whose recorded hunks are byte-for-byte+ equal point at the same offset.++TRAILER+~~~~~~~++A checksum of all preceding bytes, computed with the repository hash+function.++CORRECTNESS+-----------++Serving hunks from a valid store produces the same output as recomputing+the diff. The diff of a blob pair is not unique: a zero context length+triggers xdiff's common-tail trimming, which can pick a different but+equally valid set of hunks than an untrimmed diff does. A pair is+therefore recorded only when its trimmed and untrimmed diffs are+identical, which is the common case. Such an entry answers any consumer+at any context: git-blame replays its coordinates directly (it diffs at+zero context), and diffstat sums its per-hunk line counts, which the+context length does not change. The rare pair whose two diffs differ is+never recorded, so every consumer computes it.++A store that cannot be used is ignored, and the consumer falls back to+computing the diff. Every offset and count read from the file is+bounds-checked, so a store that is missing, truncated, of an unknown+version, or of a different object hash does not change the diff output+and does not produce a diagnostic; `git diff-hunks verify` is what+reports corruption.++The store is not re-checksummed on the read path. The writer fsyncs the+file (honoring `core.fsync`) and commits it atomically, so a+committed store is intact, the same trust model the commit-graph and+multi-pack-index use. The trailing checksum is recomputed by+`git diff-hunks verify` to detect corruption.++The checksum detects corruption but does not prove who wrote the file. A+reader trusts the coordinates in a store that passes its checks, so+anything able to write a checksum-valid file at the store path can+influence output, the same as it could by writing objects directly.++LIMITATIONS+-----------++- Hunk counts, offsets, and line coordinates are 32-bit, capping the+ hunk data at 4 GiB and a single entry at roughly 268 million hunks.+ A result whose coordinates cannot be represented is not recorded.++GIT+---+Part of the linkgit:git[1] suite
index 51b5a2c959..34ab2750f7 100644--- a/chunk-format.c+++ b/chunk-format.c@@ -101,12 +101,14 @@ cleanup: return result; }-int read_table_of_contents(struct chunkfile *cf,- const unsigned char *mfile,- size_t mfile_size,- uint64_t toc_offset,- int toc_length,- unsigned expected_alignment)+static int read_table_of_contents_1(struct chunkfile *cf,+ const unsigned char *mfile,+ size_t mfile_size,+ uint64_t toc_offset,+ int toc_length,+ unsigned expected_alignment,+ const struct git_hash_algo *algo,+ int quiet) { int i; uint32_t chunk_id;@@ -121,12 +123,14 @@ int read_table_of_contents(struct chunkfile *cf, chunk_offset = get_be64(table_of_contents + 4); if (!chunk_id) {- error(_("terminating chunk id appears earlier than expected"));+ if (!quiet)+ error(_("terminating chunk id appears earlier than expected")); return 1; } if (chunk_offset % expected_alignment != 0) {- error(_("chunk id %"PRIx32" not %d-byte aligned"),- chunk_id, expected_alignment);+ if (!quiet)+ error(_("chunk id %"PRIx32" not %d-byte aligned"),+ chunk_id, expected_alignment); return 1; }@@ -134,16 +138,18 @@ int read_table_of_contents(struct chunkfile *cf, next_chunk_offset = get_be64(table_of_contents + 4); if (next_chunk_offset < chunk_offset ||- next_chunk_offset > mfile_size - the_hash_algo->rawsz) {- error(_("improper chunk offset(s) %"PRIx64" and %"PRIx64""),- chunk_offset, next_chunk_offset);+ next_chunk_offset > mfile_size - algo->rawsz) {+ if (!quiet)+ error(_("improper chunk offset(s) %"PRIx64" and %"PRIx64""),+ chunk_offset, next_chunk_offset); return -1; } for (i = 0; i < cf->chunks_nr; i++) { if (cf->chunks[i].id == chunk_id) {- error(_("duplicate chunk ID %"PRIx32" found"),- chunk_id);+ if (!quiet)+ error(_("duplicate chunk ID %"PRIx32" found"),+ chunk_id); return -1; } }@@ -156,13 +162,39 @@ int read_table_of_contents(struct chunkfile *cf, chunk_id = get_be32(table_of_contents); if (chunk_id) {- error(_("final chunk has non-zero id %"PRIx32""), chunk_id);+ if (!quiet)+ error(_("final chunk has non-zero id %"PRIx32""), chunk_id); return -1; } return 0; }+int read_table_of_contents(struct chunkfile *cf,+ const unsigned char *mfile,+ size_t mfile_size,+ uint64_t toc_offset,+ int toc_length,+ unsigned expected_alignment)+{+ return read_table_of_contents_1(cf, mfile, mfile_size, toc_offset,+ toc_length, expected_alignment,+ the_hash_algo, 0);+}++int read_table_of_contents_quiet(struct chunkfile *cf,+ const unsigned char *mfile,+ size_t mfile_size,+ uint64_t toc_offset,+ int toc_length,+ unsigned expected_alignment,+ const struct git_hash_algo *algo)+{+ return read_table_of_contents_1(cf, mfile, mfile_size, toc_offset,+ toc_length, expected_alignment,+ algo, 1);+}+ struct pair_chunk_data { const unsigned char **p; size_t *size;
chunk-format.h
+14
index 212a0a6af1..bc31302ed0 100644--- a/chunk-format.h+++ b/chunk-format.h@@ -39,6 +39,20 @@ int read_table_of_contents(struct chunkfile *cf, int toc_length, unsigned expected_alignment);+/*+ * Like read_table_of_contents(), for a reader that treats a malformed+ * table as an absent file rather than reporting it: nothing is printed+ * on failure, and the trailing-checksum bound is computed with the+ * given hash algorithm instead of the_hash_algo.+ */+int read_table_of_contents_quiet(struct chunkfile *cf,+ const unsigned char *mfile,+ size_t mfile_size,+ uint64_t toc_offset,+ int toc_length,+ unsigned expected_alignment,+ const struct git_hash_algo *algo);+ #define CHUNK_NOT_FOUND (-2) /*
new file mode 100644index 0000000000..eeaaa2466a--- /dev/null+++ b/diff-hunks.c@@ -0,0 +1,916 @@+/*+ * Precomputed diff hunks, keyed by diff input.+ *+ * A single store at .git/objects/info/diff-hunks maps an (old blob,+ * new blob, xdl_opts) key to the hunk coordinates of diffing the pair.+ * The key determines the diff result (only trim-stable pairs are+ * recorded; see diff-hunks.h), so an entry is valid in any context it+ * recurs in, independent of path. Reading is on by default+ * (core.diffHunks); writing is off by default and enabled per run or+ * by configuration (see diff_hunks_write_enabled), so an ordinary+ * command populates the store only during a warming run the+ * repository owner opts into.+ *+ * File layout:+ * Header: "DHPF"(4) + version(1) + hash_version(1)+ * + num_chunks(1) + reserved(1)+ * Table of contents (chunk-format)+ * DHIX chunk: sorted entries, each+ * old_blob_oid, new_blob_oid, xdl_opts(4), hdat_offset(4)+ * DHDT chunk: per entry, num_hunks(4) followed by that many 16-byte hunks+ * Trailing hash checksum+ */+#include "git-compat-util.h"+#include "chunk-format.h"+#include "config.h"+#include "csum-file.h"+#include "diff-hunks.h"+#include "diff-provider-internal.h"+#include "gettext.h"+#include "hash.h"+#include "hashmap.h"+#include "lockfile.h"+#include "odb.h"+#include "path.h"+#include "repo-settings.h"+#include "repository.h"+#include "strbuf.h"+#include "wrapper.h"++#define DIFF_HUNKS_SIGNATURE 0x44485046 /* "DHPF" */+/*+ * Bump when the on-disk format changes, or when xdiff's emitted hunk+ * coordinates change for a fixed (blobs, xdl_opts) key: an old store+ * would otherwise serve stale hunks and change command output.+ */+#define DIFF_HUNKS_VERSION 1+#define DIFF_HUNKS_HEADER_SIZE 8++#define DIFF_HUNKS_CHUNKID_INDEX 0x44484958 /* "DHIX" */+#define DIFF_HUNKS_CHUNKID_DATA 0x44484454 /* "DHDT" */++/*+ * Each hunk is 16 bytes on disk:+ * old_start(4) old_count(4) new_start(4) new_count(4)+ */+#define DIFF_HUNKS_HUNK_SIZE (4 * sizeof(uint32_t))++/*+ * Result of a store lookup: num_hunks records encoded in the store's mmap,+ * valid until the store is freed. Read them with nth_precomputed_hunk().+ */+struct precomputed_entry {+ uint32_t num_hunks;+ const unsigned char *hunk_data;+};++/* Decode a single hunk from the raw on-disk format. */+static inline void decode_precomputed_hunk(const unsigned char *data,+ struct precomputed_hunk *h)+{+ h->old_start = get_be32(data);+ h->old_count = get_be32(data + 4);+ h->new_start = get_be32(data + 8);+ h->new_count = get_be32(data + 12);+}++/* Decode the nth hunk of a lookup result into *h. */+static inline void nth_precomputed_hunk(const struct precomputed_entry *e,+ uint32_t n, struct precomputed_hunk *h)+{+ decode_precomputed_hunk(e->hunk_data + (size_t)n * DIFF_HUNKS_HUNK_SIZE, h);+}++/* Byte length of the (old_oid, new_oid, xdl_opts) lookup key. */+static size_t store_index_key_size(const struct git_hash_algo *algo)+{+ return 2 * algo->rawsz + sizeof(uint32_t);+}++/* Index entry: the lookup key followed by the 4-byte offset into DHDT. */+static size_t store_index_entry_size(const struct git_hash_algo *algo)+{+ return store_index_key_size(algo) + sizeof(uint32_t);+}++/*+ * The smallest a valid store file can be: the header, a table of contents+ * with one entry per chunk plus a terminating entry, and the trailing+ * checksum.+ */+static size_t store_min_size(const struct git_hash_algo *algo,+ uint8_t num_chunks)+{+ size_t toc_size = (num_chunks + 1) * CHUNK_TOC_ENTRY_SIZE;++ return DIFF_HUNKS_HEADER_SIZE + toc_size + algo->rawsz;+}++/*+ * Decode an index entry's key into pointers to the two oids and the+ * xdl_opts value (on-disk: old_oid, new_oid, then xdl_opts as a+ * big-endian uint32).+ */+static void decode_store_index_key(const unsigned char *entry, unsigned int rawsz,+ const unsigned char **old_hash,+ const unsigned char **new_hash,+ uint32_t *xdl_opts)+{+ *old_hash = entry;+ *new_hash = entry + rawsz;+ *xdl_opts = get_be32(entry + 2 * rawsz);+}++/* The DHDT offset stored in an index entry, in the field after its key. */+static uint32_t index_entry_hdat_offset(const unsigned char *entry, size_t keysz)+{+ return get_be32(entry + keysz);+}++static char *diff_hunks_store_path(struct repository *r)+{+ return xstrfmt("%s/info/diff-hunks", repo_get_object_directory(r));+}++struct diff_hunks_store {+ const unsigned char *data;+ size_t data_len;+ const struct git_hash_algo *hash_algo;+ const unsigned char *index;+ uint32_t num_entries;+ const unsigned char *hdat;+ size_t hdat_size;+};++static void free_store(struct diff_hunks_store *s)+{+ if (!s)+ return;+ if (s->data)+ munmap((void *)s->data, s->data_len);+ free(s);+}++/*+ * Open, mmap, and parse the store at fname. Returns the parsed store+ * or NULL on any error. The diff output is unaffected either way;+ * corruption is reported by verify, not treated as fatal here.+ */+static struct diff_hunks_store *load_store_at(+ const struct git_hash_algo *repo_algo, const char *fname)+{+ struct diff_hunks_store *s;+ struct chunkfile *cf;+ int fd;+ struct stat st;+ void *data;+ const unsigned char *p;+ uint8_t num_chunks;+ size_t index_size, entry_size, data_len;++ fd = git_open(fname);+ if (fd < 0)+ return NULL;+ if (fstat(fd, &st) || st.st_size < DIFF_HUNKS_HEADER_SIZE) {+ close(fd);+ return NULL;+ }+ data_len = xsize_t(st.st_size);+ data = xmmap(NULL, data_len, PROT_READ, MAP_PRIVATE, fd, 0);+ close(fd);+ p = data;++ num_chunks = p[6];++ /*+ * Reject a file that is not a readable store: wrong signature,+ * version, or object hash, or too small to hold the table of+ * contents that read_table_of_contents() walks (it dereferences+ * each entry before range-checking its offset).+ */+ if (get_be32(p) != DIFF_HUNKS_SIGNATURE ||+ p[4] != DIFF_HUNKS_VERSION ||+ p[5] != oid_version(repo_algo) ||+ data_len < store_min_size(repo_algo, num_chunks)) {+ munmap(data, data_len);+ return NULL;+ }++ /*+ * The trailing checksum is not verified here: the writer fsyncs+ * and commits atomically, so a committed file is intact, and+ * every record is bounds-checked at read (see precomputed_entry_at).+ * The checksum is checked separately, by diff_hunks_verify().+ */++ CALLOC_ARRAY(s, 1);+ s->data = data;+ s->data_len = data_len;+ s->hash_algo = repo_algo;++ cf = init_chunkfile(NULL);+ if (read_table_of_contents_quiet(cf, p, data_len,+ DIFF_HUNKS_HEADER_SIZE, num_chunks, 1,+ repo_algo) ||+ pair_chunk(cf, DIFF_HUNKS_CHUNKID_INDEX, &s->index, &index_size) ||+ pair_chunk(cf, DIFF_HUNKS_CHUNKID_DATA, &s->hdat, &s->hdat_size)) {+ free_chunkfile(cf);+ goto corrupt;+ }+ free_chunkfile(cf);++ entry_size = store_index_entry_size(s->hash_algo);+ if (index_size % entry_size)+ goto corrupt;+ s->num_entries = index_size / entry_size;+ return s;++corrupt:+ free_store(s);+ return NULL;+}++static struct diff_hunks_store *diff_hunks_store_load(struct repository *r)+{+ struct diff_hunks_store *s;+ char *fname;++ prepare_repo_settings(r);+ if (!r->settings.core_diff_hunks)+ return NULL;++ fname = diff_hunks_store_path(r);+ s = load_store_at(r->hash_algo, fname);+ free(fname);+ return s;+}++struct diff_hunks_store *repo_diff_hunks_store(struct repository *r)+{+ if (!r->objects)+ return NULL;+ if (r->objects->diff_hunks_store_attempted)+ return r->objects->diff_hunks_store;+ r->objects->diff_hunks_store_attempted = 1;+ r->objects->diff_hunks_store = diff_hunks_store_load(r);+ return r->objects->diff_hunks_store;+}++void close_diff_hunks_store(struct object_database *o)+{+ if (!o->diff_hunks_store)+ return;+ free_store(o->diff_hunks_store);+ o->diff_hunks_store = NULL;+}++/*+ * Fill *out with the hunk record at offset in the data chunk, and return+ * 1 if the record is in bounds, 0 otherwise. The read path does not+ * re-verify the checksum, and a valid checksum would not bound the count+ * anyway, so a read must call this and use *out only when it returns+ * non-zero.+ *+ * A record is a be32 hunk count followed by that many DIFF_HUNKS_HUNK_SIZE+ * hunks. "remaining" tracks the bytes from offset to the end of the data+ * chunk: it must hold the count, and after the count is consumed it must+ * hold every hunk. The bounds are written as subtraction and division+ * (never addition or multiplication) so a crafted offset or count cannot+ * overflow them.+ */+static int precomputed_entry_at(const struct diff_hunks_store *s,+ uint32_t offset, struct precomputed_entry *out)+{+ size_t remaining;+ uint32_t num_hunks;++ if (offset >= s->hdat_size)+ return 0;+ remaining = s->hdat_size - offset;+ if (remaining < sizeof(uint32_t))+ return 0;++ num_hunks = get_be32(s->hdat + offset);+ remaining -= sizeof(uint32_t);+ if (num_hunks > remaining / DIFF_HUNKS_HUNK_SIZE)+ return 0;++ out->num_hunks = num_hunks;+ out->hunk_data = s->hdat + offset + sizeof(uint32_t);+ return 1;+}++struct lookup_key {+ const struct object_id *old_oid;+ const struct object_id *new_oid;+ int xdl_opts;+ unsigned int rawsz;+};++/*+ * The store's total order over (old_oid, new_oid, xdl_opts), defined+ * once so the write-side sort (writer_entry_cmp) and the read-side+ * search (store_bsearch_cmp) order the keys identically.+ */+static int cmp_store_index_key(const unsigned char *old_a, const unsigned char *new_a,+ uint32_t opts_a,+ const unsigned char *old_b, const unsigned char *new_b,+ uint32_t opts_b, unsigned int rawsz)+{+ int cmp = memcmp(old_a, old_b, rawsz);+ if (!cmp)+ cmp = memcmp(new_a, new_b, rawsz);+ if (!cmp)+ cmp = (opts_a > opts_b) - (opts_a < opts_b);+ return cmp;+}++static int store_bsearch_cmp(const void *key, const void *entry_ptr)+{+ const struct lookup_key *k = key;+ const unsigned char *old_hash, *new_hash;+ uint32_t xdl_opts;++ decode_store_index_key(entry_ptr, k->rawsz, &old_hash, &new_hash,+ &xdl_opts);+ return cmp_store_index_key(k->old_oid->hash, k->new_oid->hash,+ (uint32_t)k->xdl_opts,+ old_hash, new_hash, xdl_opts, k->rawsz);+}++static int store_get_one(struct diff_hunks_store *s, const struct lookup_key *key,+ struct precomputed_entry *out)+{+ size_t entry_size = store_index_entry_size(s->hash_algo);+ const unsigned char *found;++ found = bsearch(key, s->index, s->num_entries, entry_size,+ store_bsearch_cmp);+ if (!found)+ return 0;+ return precomputed_entry_at(s,+ index_entry_hdat_offset(found, store_index_key_size(s->hash_algo)),+ out);+}++static int diff_hunks_store_get(struct diff_hunks_store *s,+ const struct object_id *old_oid,+ const struct object_id *new_oid,+ int xdl_opts,+ struct precomputed_entry *out)+{+ struct lookup_key key;++ if (!s)+ return 0;+ /* The null OID names no blob and cannot key an entry. */+ if (is_null_oid(old_oid) || is_null_oid(new_oid))+ return 0;++ key.old_oid = old_oid;+ key.new_oid = new_oid;+ key.xdl_opts = xdl_opts;+ key.rawsz = s->hash_algo->rawsz;++ return store_get_one(s, &key, out);+}++/*+ * A recorded hunk sequence must satisfy the provider interface's+ * shared check (diff_provider_check_hunk()) before it may be replayed:+ * coordinates decode from be32 into long, which is 32-bit on some+ * platforms, so a crafted value can decode negative or out of order.+ * An entry that fails reads as a miss, so the caller recomputes.+ */+static int replayable_hunks(const struct precomputed_entry *e)+{+ struct diff_provider_hunks_check c = { 0 };+ uint32_t i;++ /*+ * Replaying a record with no hunks would assert the blob pair+ * equivalent, a claim the store must never make (the writer+ * refuses to record one), so such a record is invalid.+ */+ if (!e->num_hunks)+ return 0;+ for (i = 0; i < e->num_hunks; i++) {+ struct precomputed_hunk h;+ nth_precomputed_hunk(e, i, &h);+ if (diff_provider_check_hunk(&c, h.old_start, h.old_count,+ h.new_start, h.new_count))+ return 0;+ }+ return 1;+}++int diff_hunks_replay(struct diff_hunks_store *s,+ const struct object_id *old_oid,+ const struct object_id *new_oid,+ int xdl_opts,+ xdl_emit_hunk_consume_func_t hunk_func, void *cb_data)+{+ struct precomputed_entry e;+ uint32_t i;++ if (!diff_hunks_store_get(s, old_oid, new_oid, xdl_opts, &e) ||+ !replayable_hunks(&e))+ return 0;+ for (i = 0; i < e.num_hunks; i++) {+ struct precomputed_hunk h;+ nth_precomputed_hunk(&e, i, &h);+ hunk_func(h.old_start, h.old_count,+ h.new_start, h.new_count, cb_data);+ }+ return 1;+}++/* Validate one store file. Returns 0 if valid or absent, -1 on any error. */+static int verify_store_at(struct repository *r, const char *fname)+{+ struct diff_hunks_store *s;+ size_t entry_size;+ uint32_t i;+ int fd;+ int ret = 0;++ /*+ * A file that cannot be opened is not evidence of corruption:+ * report the open error, and reserve the corruption diagnostics+ * below for a file that was read and failed to parse.+ */+ fd = git_open(fname);+ if (fd < 0) {+ if (errno == ENOENT)+ return 0; /* absent is valid */+ return error_errno(_("unable to open diff-hunks store %s"),+ fname);+ }+ close(fd);+ s = load_store_at(r->hash_algo, fname);+ if (!s)+ return error(_("diff-hunks store failed to load (corrupt "+ "header or hash mismatch): %s"), fname);+ if (!hashfile_checksum_valid(r->hash_algo, s->data, s->data_len)) {+ error(_("diff-hunks store has incorrect checksum and is "+ "likely corrupt: %s"), fname);+ free_store(s);+ return -1;+ }++ entry_size = store_index_entry_size(s->hash_algo);+ for (i = 0; i < s->num_entries; i++) {+ const unsigned char *ep = s->index + st_mult(entry_size, i);+ size_t keysz = store_index_key_size(s->hash_algo);+ uint32_t offset = index_entry_hdat_offset(ep, keysz);+ struct precomputed_entry pe;++ /*+ * Keyed by (old_oid, new_oid, xdl_opts), increasing. memcmp+ * matches cmp_store_index_key's integer comparison of+ * xdl_opts because it is non-negative, so its big-endian+ * bytes order the same as its value.+ */+ if (i > 0 && memcmp(ep - entry_size, ep, keysz) >= 0) {+ error(_("diff-hunks entry %u not in sorted order"), i);+ ret = -1;+ }+ if (!precomputed_entry_at(s, offset, &pe)) {+ error(_("diff-hunks entry %u has out-of-bounds hunk "+ "data"), i);+ ret = -1;+ } else if (!replayable_hunks(&pe)) {+ error(_("diff-hunks entry %u holds an invalid hunk "+ "sequence"), i);+ ret = -1;+ }+ }++ free_store(s);+ return ret;+}++int diff_hunks_verify(struct repository *r)+{+ char *fname = diff_hunks_store_path(r);+ int ret = 0;++ if (verify_store_at(r, fname))+ ret = -1;+ free(fname);+ return ret;+}++int diff_hunks_clear(struct repository *r)+{+ char *fname = diff_hunks_store_path(r);+ int ret = 0;++ if (unlink(fname) && errno != ENOENT)+ ret = error_errno(_("unable to remove %s"), fname);+ free(fname);+ return ret;+}++struct writer_entry {+ struct object_id old_oid;+ struct object_id new_oid;+ int xdl_opts;+ uint32_t hdat_offset;+};++struct diff_hunks_writer {+ struct repository *r;+ struct writer_entry *entries;+ size_t nr, alloc;+ size_t seed_nr; /* nr after seeding; finish skips a no-op flush */+ unsigned force_flush : 1; /* seed pruned: rewrite even a no-op warm */+ struct strbuf hdat;+ struct hashmap dedup; /* hunk block content -> offset in hdat */+};++/* A record of one distinct hunk block already present in hdat. */+struct dedup_entry {+ struct hashmap_entry ent;+ uint32_t offset;+ uint32_t len;+};++static int dedup_cmp(const void *cmp_data,+ const struct hashmap_entry *a,+ const struct hashmap_entry *b,+ const void *keydata UNUSED)+{+ const struct diff_hunks_writer *writer = cmp_data;+ const struct dedup_entry *ea = container_of(a, const struct dedup_entry, ent);+ const struct dedup_entry *eb = container_of(b, const struct dedup_entry, ent);++ if (ea->len != eb->len)+ return 1;+ return memcmp(writer->hdat.buf + ea->offset,+ writer->hdat.buf + eb->offset, ea->len);+}++static struct diff_hunks_writer *diff_hunks_writer_new(struct repository *r)+{+ struct diff_hunks_writer *w;++ CALLOC_ARRAY(w, 1);+ w->r = r;+ strbuf_init(&w->hdat, 0);+ hashmap_init(&w->dedup, dedup_cmp, w, 0);+ return w;+}++static void strbuf_put_be32(struct strbuf *sb, uint32_t val)+{+ unsigned char buf[4];+ put_be32(buf, val);+ strbuf_add(sb, buf, 4);+}++/*+ * The hunk block just appended at `start` is deduplicated: if an+ * identical block is already in hdat, this copy is dropped and the+ * earlier offset returned; otherwise it is kept and remembered.+ * Distinct keys that diff to the same hunks then share one block.+ */+static uint32_t intern_block(struct diff_hunks_writer *w, size_t start)+{+ size_t len = w->hdat.len - start;+ struct dedup_entry key, *found, *added;++ hashmap_entry_init(&key.ent, memhash(w->hdat.buf + start, len));+ key.offset = (uint32_t)start;+ key.len = (uint32_t)len;++ found = hashmap_get_entry(&w->dedup, &key, ent, NULL);+ if (found) {+ strbuf_setlen(&w->hdat, start);+ return found->offset;+ }++ added = xmalloc(sizeof(*added));+ hashmap_entry_init(&added->ent, key.ent.hash);+ added->offset = key.offset;+ added->len = key.len;+ hashmap_add(&w->dedup, &added->ent);+ return key.offset;+}++int diff_hunks_writer_add(struct diff_hunks_writer *w,+ const struct object_id *old_oid,+ const struct object_id *new_oid,+ int xdl_opts,+ const struct precomputed_hunk *hunks,+ size_t nr_hunks)+{+ struct writer_entry *e;+ size_t i, block_start;++ if (!w)+ return 0;+ /*+ * The block appended for this entry is sizeof(uint32_t) ++ * nr_hunks * DIFF_HUNKS_HUNK_SIZE bytes. Bound nr_hunks so that+ * length fits the uint32_t the dedup index records (and so the+ * count itself fits the uint32_t written to the store).+ */+ if (!nr_hunks ||+ nr_hunks > (UINT32_MAX - sizeof(uint32_t)) / DIFF_HUNKS_HUNK_SIZE ||+ is_null_oid(old_oid) || is_null_oid(new_oid))+ return 0;+ if (w->hdat.len > UINT32_MAX)+ return 0;+ /*+ * Coordinates are stored as 32-bit values; a result that cannot+ * round-trip is dropped rather than silently truncated.+ */+ for (i = 0; i < nr_hunks; i++)+ if ((uintmax_t)hunks[i].old_start > (uintmax_t)INT32_MAX ||+ (uintmax_t)hunks[i].old_count > (uintmax_t)INT32_MAX ||+ (uintmax_t)hunks[i].new_start > (uintmax_t)INT32_MAX ||+ (uintmax_t)hunks[i].new_count > (uintmax_t)INT32_MAX)+ return 0;++ ALLOC_GROW(w->entries, w->nr + 1, w->alloc);+ e = &w->entries[w->nr++];+ oidcpy(&e->old_oid, old_oid);+ oidcpy(&e->new_oid, new_oid);+ e->xdl_opts = xdl_opts;++ block_start = w->hdat.len;+ strbuf_put_be32(&w->hdat, (uint32_t)nr_hunks);+ for (i = 0; i < nr_hunks; i++) {+ strbuf_put_be32(&w->hdat, hunks[i].old_start);+ strbuf_put_be32(&w->hdat, hunks[i].old_count);+ strbuf_put_be32(&w->hdat, hunks[i].new_start);+ strbuf_put_be32(&w->hdat, hunks[i].new_count);+ }+ e->hdat_offset = intern_block(w, block_start);+ return 1;+}++/*+ * Seed the writer with fname's entries so a rewrite preserves them,+ * setting *pruned when the rewrite will not carry the whole file+ * forward: the file failed its checksum and was discarded outright, or+ * individual entries were dropped because they failed the replayable+ * check or the writer refused them (a key naming no blob). A+ * rewrite re-checksums, so corruption must not be carried forward:+ * that would launder it into a checksum-valid file that verify can no+ * longer catch. This path already reads the whole file, so verify the+ * checksum here (the reader keeps trusting committed files, without+ * re-checksumming); an invalid+ * entry reads as a miss anyway, so dropping it heals the store rather+ * than losing anything a reader could use.+ */+static void diff_hunks_writer_seed(struct diff_hunks_writer *w,+ const char *fname, int *pruned)+{+ struct diff_hunks_store *s = load_store_at(w->r->hash_algo, fname);+ unsigned int rawsz;+ size_t entry_size, keysz;+ struct precomputed_hunk *hunks = NULL;+ size_t hunks_alloc = 0;+ uint32_t i, dropped = 0;++ if (!s)+ return;+ if (!hashfile_checksum_valid(w->r->hash_algo, s->data, s->data_len)) {+ warning(_("diff-hunks store %s failed its checksum; "+ "discarding it"), fname);+ free_store(s);+ *pruned = 1;+ return;+ }+ rawsz = s->hash_algo->rawsz;+ entry_size = store_index_entry_size(s->hash_algo);+ keysz = store_index_key_size(s->hash_algo);++ for (i = 0; i < s->num_entries; i++) {+ const unsigned char *ep = s->index + st_mult(entry_size, i);+ const unsigned char *old_hash, *new_hash;+ struct object_id old_oid, new_oid;+ uint32_t xdl_opts, j;+ struct precomputed_entry pe;++ decode_store_index_key(ep, rawsz, &old_hash, &new_hash,+ &xdl_opts);+ oidread(&old_oid, old_hash, s->hash_algo);+ oidread(&new_oid, new_hash, s->hash_algo);+ if (!precomputed_entry_at(s, index_entry_hdat_offset(ep, keysz), &pe) ||+ !replayable_hunks(&pe)) {+ dropped++;+ continue;+ }+ ALLOC_GROW(hunks, pe.num_hunks, hunks_alloc);+ for (j = 0; j < pe.num_hunks; j++)+ nth_precomputed_hunk(&pe, j, &hunks[j]);+ if (!diff_hunks_writer_add(w, &old_oid, &new_oid,+ (int)xdl_opts, hunks, pe.num_hunks))+ dropped++;+ }+ if (dropped) {+ warning(Q_("diff-hunks store %s: dropping %u invalid entry",+ "diff-hunks store %s: dropping %u invalid entries",+ dropped), fname, dropped);+ *pruned = 1;+ }+ free(hunks);+ free_store(s);+}++/*+ * Writing is off by default. It is enabled per invocation by the+ * GIT_DIFF_HUNKS_WRITE environment variable, or persistently by the+ * diffHunks.write config, with the environment variable winning when+ * set. Only a warming run (a diff or log the repository owner chooses+ * to run with writing on) enables it, so ordinary reads never mutate+ * the store.+ */+static int diff_hunks_write_enabled(struct repository *r)+{+ const char *env = getenv("GIT_DIFF_HUNKS_WRITE");+ int val;++ if (env) {+ /*+ * This is a warming opt-in, so an unparseable value must not+ * abort an ordinary read command: treat it as disabled.+ */+ val = git_parse_maybe_bool(env);+ return val < 0 ? 0 : val;+ }+ if (!repo_config_get_bool(r, "diffhunks.write", &val))+ return val;+ return 0;+}++struct diff_hunks_writer *diff_hunks_writer_maybe_new(struct repository *r)+{+ struct diff_hunks_writer *w;+ char *fname;+ int pruned;++ if (!diff_hunks_write_enabled(r))+ return NULL;+ /*+ * Seed from the existing store so a flush merges with it rather+ * than replacing it: a later warm adds newly computed pairs+ * without discarding what earlier warms recorded.+ */+ w = diff_hunks_writer_new(r);+ fname = diff_hunks_store_path(r);+ pruned = 0;+ diff_hunks_writer_seed(w, fname, &pruned);+ free(fname);+ w->seed_nr = w->nr;+ /*+ * A pruning seed means the file on disk holds material the+ * rewrite must not preserve; flush even if this warm computes+ * nothing new, so the store on disk is repaired rather than+ * left serving what the seed refused.+ */+ w->force_flush = !!pruned;+ return w;+}++static int writer_entry_cmp(const void *va, const void *vb, void *ctx)+{+ const struct writer_entry *a = va, *b = vb;+ unsigned int rawsz = *(const unsigned int *)ctx;+ return cmp_store_index_key(a->old_oid.hash, a->new_oid.hash,+ (uint32_t)a->xdl_opts,+ b->old_oid.hash, b->new_oid.hash,+ (uint32_t)b->xdl_opts,+ rawsz);+}++struct write_ctx {+ struct diff_hunks_writer *w;+ unsigned int rawsz;+};++static int write_index_chunk(struct hashfile *f, void *data)+{+ struct write_ctx *ctx = data;+ size_t i;++ for (i = 0; i < ctx->w->nr; i++) {+ hashwrite(f, ctx->w->entries[i].old_oid.hash, ctx->rawsz);+ hashwrite(f, ctx->w->entries[i].new_oid.hash, ctx->rawsz);+ hashwrite_be32(f, ctx->w->entries[i].xdl_opts);+ hashwrite_be32(f, ctx->w->entries[i].hdat_offset);+ }+ return 0;+}++static int write_data_chunk(struct hashfile *f, void *data)+{+ struct write_ctx *ctx = data;+ hashwrite(f, ctx->w->hdat.buf, ctx->w->hdat.len);+ return 0;+}++/* Sort, dedup, and write the accumulated entries to the file at fname. */+static int diff_hunks_writer_flush(struct diff_hunks_writer *w, char *fname)+{+ struct lock_file lk = LOCK_INIT;+ struct hashfile *f;+ struct chunkfile *cf;+ unsigned int rawsz = w->r->hash_algo->rawsz;+ struct write_ctx ctx = { w, rawsz };+ size_t entry_size;++ QSORT_S(w->entries, w->nr, writer_entry_cmp, &rawsz);++ /*+ * The same blob pair recurs across history (reverts, cherry-+ * picks); identical keys carry identical hunks, so keep one of+ * each. The index must stay duplicate-free for binary search.+ */+ if (w->nr > 1) {+ size_t kept = 1, i;+ for (i = 1; i < w->nr; i++)+ if (writer_entry_cmp(&w->entries[kept - 1],+ &w->entries[i], &rawsz))+ w->entries[kept++] = w->entries[i];+ w->nr = kept;+ }++ if (safe_create_leading_directories(w->r, fname)) {+ error(_("unable to create directory for %s"), fname);+ return -1;+ }+ if (hold_lock_file_for_update(&lk, fname, 0) < 0) {+ error_errno(_("unable to lock %s"), fname);+ return -1;+ }+ adjust_shared_perm(w->r, get_lock_file_path(&lk));+ f = hashfd(w->r->hash_algo, get_lock_file_fd(&lk),+ get_lock_file_path(&lk));++ entry_size = store_index_entry_size(w->r->hash_algo);+ cf = init_chunkfile(f);+ add_chunk(cf, DIFF_HUNKS_CHUNKID_INDEX, w->nr * entry_size,+ write_index_chunk);+ add_chunk(cf, DIFF_HUNKS_CHUNKID_DATA, w->hdat.len, write_data_chunk);++ hashwrite_be32(f, DIFF_HUNKS_SIGNATURE);+ hashwrite_u8(f, DIFF_HUNKS_VERSION);+ hashwrite_u8(f, oid_version(w->r->hash_algo));+ hashwrite_u8(f, get_num_chunks(cf));+ hashwrite_u8(f, 0); /* reserved */++ write_chunkfile(cf, &ctx);+ free_chunkfile(cf);++ /*+ * fsync per the user's configuration (like commit-graph and the+ * multi-pack-index), then commit atomically. Readers trust the+ * committed file rather than re-checksumming it; diff_hunks_verify()+ * checks the checksum separately.+ */+ finalize_hashfile(f, NULL, FSYNC_COMPONENT_DIFF_HUNKS,+ CSUM_HASH_IN_STREAM | CSUM_FSYNC);+ /*+ * This same process may hold the current store mmapped (a warm+ * that also reads); the commit below renames over it, which must+ * never land on a live mapping (Windows refuses it). Close the+ * store and clear the load-attempted flag first, so the next+ * read loads the committed file.+ */+ if (w->r->objects) {+ close_diff_hunks_store(w->r->objects);+ w->r->objects->diff_hunks_store_attempted = 0;+ }+ if (commit_lock_file(&lk)) {+ error_errno(_("unable to write %s"), fname);+ return -1;+ }+ return 0;+}++static void diff_hunks_writer_free(struct diff_hunks_writer *w)+{+ if (!w)+ return;+ hashmap_clear_and_free(&w->dedup, struct dedup_entry, ent);+ free(w->entries);+ strbuf_release(&w->hdat);+ free(w);+}++void diff_hunks_writer_finish(struct diff_hunks_writer *w)+{+ if (!w)+ return;+ /* Skip the flush when the warm recorded nothing beyond its seed. */+ if (w->nr != w->seed_nr || w->force_flush) {+ char *fname = diff_hunks_store_path(w->r);+ diff_hunks_writer_flush(w, fname);+ free(fname);+ }+ diff_hunks_writer_free(w);+}
diff-hunks.h
+117
new file mode 100644index 0000000000..ef9ee3f417--- /dev/null+++ b/diff-hunks.h@@ -0,0 +1,117 @@+#ifndef DIFF_HUNKS_H+#define DIFF_HUNKS_H++#include "hash.h"+#include "xdiff-interface.h" /* xdl_emit_hunk_consume_func_t */++struct object_id;+struct repository;+struct object_database;++/*+ * A persistent store of precomputed diff hunk coordinates, at+ * .git/objects/info/diff-hunks. Entries are keyed by the two blobs diffed+ * and the xdl_opts they were diffed under, so a cached result is valid+ * in any context that key recurs in, independent of path. The xdl_opts+ * key component mirrors the (always non-negative) diff_options field it+ * projects from, and is serialized and compared as a 4-byte big-endian+ * integer.+ *+ * The hunks a pair produces are not unique. They vary with the xdiff+ * algorithm and ignore flags (xdl_opts, part of the key), and with+ * whether the diff was trimmed: a zero-context diff runs+ * trim_common_tail, which can pick a different but equally valid set of+ * hunks than an untrimmed diff. The store holds one entry per key, so a+ * pair is recorded only when its trimmed and untrimmed diffs are+ * identical (the recording caller checks); such an entry serves a+ * consumer at any context. The rare pair where the two diffs differ is+ * never recorded and is always computed.+ *+ * The store is a cache: ordinary commands read it and fall back to+ * computing the diff when it is absent, stale, or corrupt. It is filled+ * as a side effect of diff and log runs, but only when writing is+ * enabled (such a write-enabled run is a warming run); writing is off+ * by default, so an ordinary command reads the store without recording+ * into it.+ */++/*+ * A hunk's coordinates. The type is long to match the xdiff emit+ * callback; the values are a diff's line numbers and counts, always+ * within the int32 range the on-disk format stores (see+ * diff_hunks_writer_add()).+ */+struct precomputed_hunk {+ long old_start;+ long old_count;+ long new_start;+ long new_count;+};++/*+ * The repository's store, loaded once on first use and cached on the+ * object database. Returns NULL when reading is disabled+ * (core.diffHunks=false), the store is absent, or it fails to parse+ * (wrong signature, version, or object hash, or a corrupt structure).+ * The lookup functions below accept a NULL store and treat it as+ * empty (every lookup misses), so callers need not check for NULL.+ * The object database owns the store; callers must not free it.+ */+struct diff_hunks_store *repo_diff_hunks_store(struct repository *r);++/* Free the repository's cached store, at object-database teardown. */+void close_diff_hunks_store(struct object_database *o);++/*+ * Replay the recorded hunks of an (old blob, new blob) pair diffed+ * under xdl_opts through hunk_func. The sequence is validated before+ * any callback runs: on a hit (return 1) every hunk is emitted, on a+ * miss (return 0: absent pair, xdl_opts mismatch, or an entry that+ * fails validation) nothing is emitted, so a caller may accumulate+ * directly into its result.+ */+int diff_hunks_replay(struct diff_hunks_store *s,+ const struct object_id *old_oid,+ const struct object_id *new_oid,+ int xdl_opts,+ xdl_emit_hunk_consume_func_t hunk_func, void *cb_data);++/*+ * A warming run's writer: it accumulates the hunks it computes in memory+ * and flushes them to the store in one pass at finish.+ */+struct diff_hunks_writer;++/*+ * Return a writer for a warming run, or NULL when writing is disabled+ * (the default). diff_hunks_writer_add() tolerates a NULL writer, so a+ * caller may attach the result unconditionally. Pair with+ * diff_hunks_writer_finish().+ */+struct diff_hunks_writer *diff_hunks_writer_maybe_new(struct repository *r);++/*+ * Record a blob pair's hunks as computed under xdl_opts; a later lookup+ * with a matching key is served these hunks. The caller must have+ * checked that the pair's trimmed and untrimmed diffs are identical+ * (see the top of this file), so the entry answers at any context.+ * NULL-safe. Returns 1 when the entry was recorded, 0 when the writer+ * refused it (no hunks, a null object id, or values the on-disk+ * 32-bit fields cannot hold).+ */+int diff_hunks_writer_add(struct diff_hunks_writer *w,+ const struct object_id *old_oid,+ const struct object_id *new_oid,+ int xdl_opts,+ const struct precomputed_hunk *hunks,+ size_t nr_hunks);++/* Flush the accumulated entries to the store and free the writer. NULL-safe. */+void diff_hunks_writer_finish(struct diff_hunks_writer *w);++/* Remove the store file. Returns 0 (incl. absent) or -1. */+int diff_hunks_clear(struct repository *r);+/* Validate the store. Returns 0 if valid/absent, -1 if corrupt. */+int diff_hunks_verify(struct repository *r);++#endif /* DIFF_HUNKS_H */
index 7995bed97b..949d55668f 100644--- a/odb.h+++ b/odb.h@@ -8,6 +8,7 @@ #include "thread-utils.h" struct cached_object_entry;+struct diff_hunks_store; struct list_objects_filter_options; struct odb_source_inmemory; struct packed_git;@@ -76,6 +77,9 @@ struct object_database { struct commit_graph *commit_graph; unsigned commit_graph_attempted : 1; /* if loading has been attempted */+ struct diff_hunks_store *diff_hunks_store;+ unsigned diff_hunks_store_attempted : 1; /* if loading has been attempted */+ /* * This is meant to hold a *small* number of objects that you would * want odb_read_object() to be able to return, but yet you do not want
index e5253ead02..615a55cac4 100644--- a/repo-settings.h+++ b/repo-settings.h@@ -22,6 +22,7 @@ struct repo_settings { int core_commit_graph; int commit_graph_generation_version; int commit_graph_changed_paths_version;+ int core_diff_hunks; int gc_write_commit_graph; int fetch_write_commit_graph; int command_requires_full_index;