1035 lines
30 KiB
C
1035 lines
30 KiB
C
/*
|
|
* 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);
|
|
}
|