| /* |
| * 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); |
| } |