/* * 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 "diff.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; /* Consultation counters; see diff_hunks_read_stats(). */ unsigned long read_hits; unsigned long read_misses; }; 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 diff_hunks_read_stats(struct repository *r, unsigned long *hits, unsigned long *misses) { struct diff_hunks_store *s = repo_diff_hunks_store(r); *hits = s ? s->read_hits : 0; *misses = s ? s->read_misses : 0; } 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 (!s) return 0; if (!diff_hunks_store_get(s, old_oid, new_oid, xdl_opts, &e) || !replayable_hunks(&e)) { s->read_misses++; 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); } s->read_hits++; return 1; } /* * The store's consult implementation. The store is not * authoritative, so it serves a recorded pair or passes; what the * recording key cannot express, it excludes here with the * stop-no-record disposition. None of those legs reaches * diff_hunks_replay(), so none of them counts as a miss. */ static enum diff_provider_disposition diff_hunks_store_consult(struct diff_provider *provider UNUSED, const struct diff_provider_request *req, diff_provider_fill_fn fill UNUSED, void *fill_data UNUSED, xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data) { /* * xpparam_t is the consult's parameter input. Its flags are * the store key's xdl_opts; ignore_regex (-I) and anchors * (--anchored) shape the diff outside the key, so such a * request is neither served nor recorded. * * Adding an xpparam_t field fires this assert (its size no * longer matches the reference struct). To clear it: (1) add * the field to the reference struct below; then (2) decide how * it affects the key: make it part of the key, or exclude * diffs that use it here with the disposition below. The * assert only tracks size: a same-size reorder or a changed * field meaning slips past, so re-read the fields when it * fires. */ (void)BUILD_ASSERT_OR_ZERO(sizeof(xpparam_t) == sizeof(struct { unsigned long flags; regex_t **ignore_regex; size_t ignore_regex_nr; char **anchors; size_t anchors_nr; })); if (req->xpp->ignore_regex_nr || req->xpp->anchors_nr) return DIFF_PROVIDER_DISP_STOP_NO_RECORD; /* * Break detection (-B) rescores the pair outside xpparam_t, so * it is outside the key for the same reason. */ if (req->diffopt && req->diffopt->break_opt != -1) return DIFF_PROVIDER_DISP_STOP_NO_RECORD; if (!req->old_oid || !req->new_oid) return DIFF_PROVIDER_DISP_PASS; if (diff_hunks_replay(repo_diff_hunks_store(req->repo), req->old_oid, req->new_oid, req->xpp->flags, hunk_cb, cb_data)) return DIFF_PROVIDER_DISP_ANSWERED; return DIFF_PROVIDER_DISP_PASS; } /* * The provider borrows the repository's store through * repo_diff_hunks_store() per request; the object database owns the * file and tears it down, so there is nothing to release here. */ struct diff_provider *diff_hunks_store_provider_new(void) { struct diff_provider *p = xcalloc(1, sizeof(*p)); p->consult = diff_hunks_store_consult; return p; } /* 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; } void diff_hunks_writer_record_stable(struct diff_hunks_writer *w, const struct object_id *old_oid, const struct object_id *new_oid, int xdl_opts, const struct precomputed_hunk *trimmed, size_t nr_trimmed, const struct precomputed_hunk *full, size_t nr_full) { size_t i; if (!w) return; /* * Record only a trim-stable pair, one whose trimmed and * untrimmed diffs are identical, so the single entry answers * any consumer at any context (see the top of this file). A * pair where the two diffs differ is never recorded and every * consumer computes it. */ if (nr_trimmed != nr_full) return; for (i = 0; i < nr_trimmed; i++) if (trimmed[i].old_start != full[i].old_start || trimmed[i].old_count != full[i].old_count || trimmed[i].new_start != full[i].new_start || trimmed[i].new_count != full[i].new_count) return; diff_hunks_writer_add(w, old_oid, new_oid, xdl_opts, trimmed, nr_trimmed); } /* * 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); }