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