summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTaylor Blau <me@ttaylorr.com>2022-11-18 20:04:57 -0500
committerTaylor Blau <me@ttaylorr.com>2022-11-18 20:04:57 -0500
commitcd9aeb40a73b32ab898f1d32b08599522e6abb4a (patch)
tree33ae71c0b9cbdaf207b52cbaf5652e4c62099b46
parentcc065bd85e568d19ceffcf1e3c141f6fab770518 (diff)
parent119947034800e6aba8abaa7ebec7b1be72ed643d (diff)
downloadgit-cd9aeb40a73b32ab898f1d32b08599522e6abb4a.tar.gz
Merge branch 'ds/packed-refs-v2' into jch
* ds/packed-refs-v2: (30 commits) refs: skip hashing when writing packed-refs v2 p1401: create performance test for ref operations ci: run GIT_TEST_PACKED_REFS_VERSION=2 in some builds t*: skip packed-refs v2 over http tests t3210: require packed-refs v1 for some tests t5502: add PACKED_REFS_V1 prerequisite t5312: allow packed-refs v2 format t1409: test with packed-refs v2 packed-backend: create GIT_TEST_PACKED_REFS_VERSION packed-refs: write prefix chunks packed-refs: read optional prefix chunks packed-refs: read file format v2 packed-refs: write file format version 2 packed-backend: create shell of v2 writes config: add config values for packed-refs v2 packed-backend: create abstraction for writing refs packed-backend: extract iterator/updates merge packed-backend: extract add_write_error() refs: extract packfile format to new file chunk-format: parse trailing table of contents ...
-rw-r--r--Documentation/config.txt2
-rw-r--r--Documentation/config/extensions.txt76
-rw-r--r--Documentation/config/index.txt8
-rw-r--r--Documentation/config/refs.txt13
-rw-r--r--Documentation/gitformat-chunk.txt26
-rw-r--r--Makefile2
-rw-r--r--cache.h2
-rw-r--r--chunk-format.c109
-rw-r--r--chunk-format.h18
-rwxr-xr-xci/run-build-and-tests.sh1
-rw-r--r--commit-graph.c2
-rw-r--r--csum-file.c14
-rw-r--r--csum-file.h7
-rw-r--r--midx.c2
-rw-r--r--read-cache.c22
-rw-r--r--refs.c24
-rw-r--r--refs/files-backend.c8
-rw-r--r--refs/packed-backend.c880
-rw-r--r--refs/packed-backend.h281
-rw-r--r--refs/packed-format-v1.c456
-rw-r--r--refs/packed-format-v2.c624
-rw-r--r--refs/refs-internal.h9
-rw-r--r--repository.c2
-rw-r--r--repository.h7
-rw-r--r--setup.c26
-rwxr-xr-xt/perf/p1401-ref-operations.sh52
-rwxr-xr-xt/t1409-avoid-packing-refs.sh22
-rwxr-xr-xt/t1600-index.sh8
-rwxr-xr-xt/t3210-pack-refs.sh8
-rwxr-xr-xt/t3212-ref-formats.sh100
-rwxr-xr-xt/t5502-quickfetch.sh2
-rwxr-xr-xt/t5539-fetch-http-shallow.sh7
-rwxr-xr-xt/t5541-http-push-smart.sh7
-rwxr-xr-xt/t5542-push-http-shallow.sh7
-rwxr-xr-xt/t5551-http-fetch-smart.sh7
-rwxr-xr-xt/t5558-clone-bundle-uri.sh7
-rw-r--r--t/test-lib.sh4
37 files changed, 2157 insertions, 695 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e93aef862..e480f99c3e 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -493,6 +493,8 @@ include::config/rebase.txt[]
include::config/receive.txt[]
+include::config/refs.txt[]
+
include::config/remote.txt[]
include::config/remotes.txt[]
diff --git a/Documentation/config/extensions.txt b/Documentation/config/extensions.txt
index bccaec7a96..05abb821e0 100644
--- a/Documentation/config/extensions.txt
+++ b/Documentation/config/extensions.txt
@@ -7,6 +7,69 @@ Note that this setting should only be set by linkgit:git-init[1] or
linkgit:git-clone[1]. Trying to change it after initialization will not
work and will produce hard-to-diagnose issues.
+extensions.refFormat::
+ Specify the reference storage mechanisms used by the repoitory as a
+ multi-valued list. The acceptable values are `files` and `packed`.
+ If not specified, the list of `files` and `packed` is assumed. It
+ is an error to specify this key unless `core.repositoryFormatVersion`
+ is 1.
++
+As new ref formats are added, Git commands may modify this list before and
+after upgrading the on-disk reference storage files. The specific values
+indicate the existence of different layers:
++
+--
+`files`;;
+ When present, references may be stored as "loose" reference files
+ in the `$GIT_DIR/refs/` directory. The name of the reference
+ corresponds to the filename after `$GIT_DIR` and the file contains
+ an object ID as a hexadecimal string. If a loose reference file
+ exists, then its value takes precedence over all other formats.
+
+`packed`;;
+ When present, references may be stored as a group in a
+ `packed-refs` file in its version 1 format. When grouped with
+ `"files"` or provided on its own, this file is located at
+ `$GIT_DIR/packed-refs`. This file contains a list of distinct
+ reference names, paired with their object IDs. When combined with
+ `files`, the `packed` format will only be used to group multiple
+ loose object files upon request via the `git pack-refs` command or
+ via the `pack-refs` maintenance task.
+
+`packed-v2`;;
+ When present, references may be stored as a group in a
+ `packed-refs` file in its version 2 format. This file is in the
+ same position and interacts with loose refs the same as when the
+ `packed` value exists. Both `packed` and `packed-v2` must exist to
+ upgrade an existing `packed-refs` file from version 1 to version 2
+ or to downgrade from version 2 to version 1. When both are
+ present, the `refs.packedRefsVersion` config value indicates which
+ file format version is used during writes, but both versions are
+ understood when reading the file.
+--
++
+The following combinations are supported by this version of Git:
++
+--
+`files` and (`packed` and/or `packed-v2`);;
+ This set of values indicates that references are stored both as
+ loose reference files and in the `packed-refs` file. Loose
+ references are preferred, and the `packed-refs` file is updated
+ only when deleting a reference that is stored in the `packed-refs`
+ file or during a `git pack-refs` command.
++
+The presence of `packed` and `packed-v2` specifies whether the `packed-refs`
+file is allowed to be in its v1 or v2 formats, respectively. When only one
+is present, Git will refuse to read the `packed-refs` file that do not
+match the expected format. When both are present, the `refs.packedRefsVersion`
+config option indicates which file format is used during writes.
+
+`files`;;
+ When only this value is present, Git will ignore the `packed-refs`
+ file and refuse to write one during `git pack-refs`. All references
+ will be read from and written to loose reference files.
+--
+
extensions.worktreeConfig::
If enabled, then worktrees will load config settings from the
`$GIT_DIR/config.worktree` file in addition to the
@@ -21,10 +84,15 @@ When enabling `extensions.worktreeConfig`, you must be careful to move
certain values from the common config file to the main working tree's
`config.worktree` file, if present:
+
-* `core.worktree` must be moved from `$GIT_COMMON_DIR/config` to
- `$GIT_COMMON_DIR/config.worktree`.
-* If `core.bare` is true, then it must be moved from `$GIT_COMMON_DIR/config`
- to `$GIT_COMMON_DIR/config.worktree`.
+--
+`core.worktree`;;
+ This config value must be moved from `$GIT_COMMON_DIR/config` to
+ `$GIT_COMMON_DIR/config.worktree`.
+
+`core.bare`;;
+ If true, then this value must be moved from
+ `$GIT_COMMON_DIR/config` to `$GIT_COMMON_DIR/config.worktree`.
+--
+
It may also be beneficial to adjust the locations of `core.sparseCheckout`
and `core.sparseCheckoutCone` depending on your desire for customizable
diff --git a/Documentation/config/index.txt b/Documentation/config/index.txt
index 75f3a2d105..709ba72f62 100644
--- a/Documentation/config/index.txt
+++ b/Documentation/config/index.txt
@@ -30,3 +30,11 @@ index.version::
Specify the version with which new index files should be
initialized. This does not affect existing repositories.
If `feature.manyFiles` is enabled, then the default is 4.
+
+index.computeHash::
+ When enabled, compute the hash of the index file as it is written
+ and store the hash at the end of the content. This is enabled by
+ default.
++
+If you disable `index.computHash`, then older Git clients may report that
+your index is corrupt during `git fsck`.
diff --git a/Documentation/config/refs.txt b/Documentation/config/refs.txt
new file mode 100644
index 0000000000..b2fdb2923f
--- /dev/null
+++ b/Documentation/config/refs.txt
@@ -0,0 +1,13 @@
+refs.packedRefsVersion::
+ Specifies the file format version to use when writing a `packed-refs`
+ file. Defaults to `1`.
++
+The only other value currently allowed is `2`, which uses a structured file
+format to result in a smaller `packed-refs` file. In order to write this
+file format version, the repository must also have the `packed-v2` extension
+enabled. The most typical setup will include the
+`core.repositoryFormatVersion=1` config value and the `extensions.refFormat`
+key will have three values: `files`, `packed`, and `packed-v2`.
++
+If `extensions.refFormat` has the value `packed-v2` and not `packed`, then
+`refs.packedRefsVersion` defaults to `2`.
diff --git a/Documentation/gitformat-chunk.txt b/Documentation/gitformat-chunk.txt
index 57202ede27..ee3718c430 100644
--- a/Documentation/gitformat-chunk.txt
+++ b/Documentation/gitformat-chunk.txt
@@ -24,8 +24,9 @@ how they use the chunks to describe structured data.
A chunk-based file format begins with some header information custom to
that format. That header should include enough information to identify
-the file type, format version, and number of chunks in the file. From this
-information, that file can determine the start of the chunk-based region.
+the file type, format version, and (optionally) the number of chunks in
+the file. From this information, that file can determine the start of the
+chunk-based region.
The chunk-based region starts with a table of contents describing where
each chunk starts and ends. This consists of (C+1) rows of 12 bytes each,
@@ -51,8 +52,27 @@ The final entry in the table of contents must be four zero bytes. This
confirms that the table of contents is ending and provides the offset for
the end of the chunk-based data.
+The default chunk format assumes the table of contents appears at the
+beginning of the file (after the header information) and the chunks are
+ordered by increasing offset. Alternatively, the chunk format allows a
+table of contents that is placed at the end of the file (before the
+trailing hash) and the offsets are in descending order. In this trailing
+table of contents case, the data in order looks instead like the following
+table:
+
+ | Chunk ID (4 bytes) | Chunk Offset (8 bytes) |
+ |--------------------|------------------------|
+ | 0x0000 | OFFSET[C+1] |
+ | ID[C] | OFFSET[C] |
+ | ... | ... |
+ | ID[0] | OFFSET[0] |
+
+The concrete file format that uses the chunk format will mention that it
+uses a trailing table of contents if it uses it. By default, the table of
+contents is in ascending order before all chunk data.
+
Note: The chunk-based format expects that the file contains _at least_ a
-trailing hash after `OFFSET[C+1]`.
+trailing hash after either `OFFSET[C+1]` or the trailing table of contents.
Functions for working with chunk-based file formats are declared in
`chunk-format.h`. Using these methods provide extra checks that assist
diff --git a/Makefile b/Makefile
index 8863106507..54f3bdc6a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1121,6 +1121,8 @@ LIB_OBJS += refs/debug.o
LIB_OBJS += refs/files-backend.o
LIB_OBJS += refs/iterator.o
LIB_OBJS += refs/packed-backend.o
+LIB_OBJS += refs/packed-format-v1.o
+LIB_OBJS += refs/packed-format-v2.o
LIB_OBJS += refs/ref-cache.o
LIB_OBJS += refspec.o
LIB_OBJS += remote.o
diff --git a/cache.h b/cache.h
index 26ed03bd6d..13e9c251ac 100644
--- a/cache.h
+++ b/cache.h
@@ -1155,6 +1155,8 @@ struct repository_format {
int hash_algo;
int sparse_index;
char *work_tree;
+ int ref_format_count;
+ enum ref_format_flags ref_format;
struct string_list unknown_extensions;
struct string_list v1_only_extensions;
};
diff --git a/chunk-format.c b/chunk-format.c
index 0275b74a89..e836a121c5 100644
--- a/chunk-format.c
+++ b/chunk-format.c
@@ -13,6 +13,7 @@ struct chunk_info {
chunk_write_fn write_fn;
const void *start;
+ off_t offset;
};
struct chunkfile {
@@ -56,38 +57,59 @@ void add_chunk(struct chunkfile *cf,
cf->chunks_nr++;
}
-int write_chunkfile(struct chunkfile *cf, void *data)
+int write_chunkfile(struct chunkfile *cf,
+ enum chunkfile_flags flags,
+ void *data)
{
int i, result = 0;
- uint64_t cur_offset = hashfile_total(cf->f);
trace2_region_enter("chunkfile", "write", the_repository);
- /* Add the table of contents to the current offset */
- cur_offset += (cf->chunks_nr + 1) * CHUNK_TOC_ENTRY_SIZE;
+ if (!(flags & CHUNKFILE_TRAILING_TOC)) {
+ uint64_t cur_offset = hashfile_total(cf->f);
- for (i = 0; i < cf->chunks_nr; i++) {
- hashwrite_be32(cf->f, cf->chunks[i].id);
- hashwrite_be64(cf->f, cur_offset);
+ /* Add the table of contents to the current offset */
+ cur_offset += (cf->chunks_nr + 1) * CHUNK_TOC_ENTRY_SIZE;
- cur_offset += cf->chunks[i].size;
- }
+ for (i = 0; i < cf->chunks_nr; i++) {
+ hashwrite_be32(cf->f, cf->chunks[i].id);
+ hashwrite_be64(cf->f, cur_offset);
+
+ cur_offset += cf->chunks[i].size;
+ }
- /* Trailing entry marks the end of the chunks */
- hashwrite_be32(cf->f, 0);
- hashwrite_be64(cf->f, cur_offset);
+ /* Trailing entry marks the end of the chunks */
+ hashwrite_be32(cf->f, 0);
+ hashwrite_be64(cf->f, cur_offset);
+ }
for (i = 0; i < cf->chunks_nr; i++) {
- off_t start_offset = hashfile_total(cf->f);
+ cf->chunks[i].offset = hashfile_total(cf->f);
result = cf->chunks[i].write_fn(cf->f, data);
if (result)
goto cleanup;
- if (hashfile_total(cf->f) - start_offset != cf->chunks[i].size)
- BUG("expected to write %"PRId64" bytes to chunk %"PRIx32", but wrote %"PRId64" instead",
- cf->chunks[i].size, cf->chunks[i].id,
- hashfile_total(cf->f) - start_offset);
+ if (!(flags & CHUNKFILE_TRAILING_TOC)) {
+ if (hashfile_total(cf->f) - cf->chunks[i].offset != cf->chunks[i].size)
+ BUG("expected to write %"PRId64" bytes to chunk %"PRIx32", but wrote %"PRId64" instead",
+ cf->chunks[i].size, cf->chunks[i].id,
+ hashfile_total(cf->f) - cf->chunks[i].offset);
+ }
+
+ cf->chunks[i].size = hashfile_total(cf->f) - cf->chunks[i].offset;
+ }
+
+ if (flags & CHUNKFILE_TRAILING_TOC) {
+ size_t last_chunk_tail = hashfile_total(cf->f);
+ /* First entry marks the end of the chunks */
+ hashwrite_be32(cf->f, 0);
+ hashwrite_be64(cf->f, last_chunk_tail);
+
+ for (i = cf->chunks_nr - 1; i >= 0; i--) {
+ hashwrite_be32(cf->f, cf->chunks[i].id);
+ hashwrite_be64(cf->f, cf->chunks[i].offset);
+ }
}
cleanup:
@@ -151,6 +173,59 @@ int read_table_of_contents(struct chunkfile *cf,
return 0;
}
+int read_trailing_table_of_contents(struct chunkfile *cf,
+ const unsigned char *mfile,
+ size_t mfile_size)
+{
+ int i;
+ uint32_t chunk_id;
+ const unsigned char *table_of_contents = mfile + mfile_size - the_hash_algo->rawsz;
+
+ while (1) {
+ uint64_t chunk_offset;
+
+ table_of_contents -= CHUNK_TOC_ENTRY_SIZE;
+
+ chunk_id = get_be32(table_of_contents);
+ chunk_offset = get_be64(table_of_contents + 4);
+
+ /* Calculate the previous chunk size, if it exists. */
+ if (cf->chunks_nr) {
+ off_t previous_offset = cf->chunks[cf->chunks_nr - 1].offset;
+
+ if (chunk_offset < previous_offset ||
+ chunk_offset > table_of_contents - mfile) {
+ error(_("improper chunk offset(s) %"PRIx64" and %"PRIx64""),
+ previous_offset, chunk_offset);
+ return -1;
+ }
+
+ cf->chunks[cf->chunks_nr - 1].size = chunk_offset - previous_offset;
+ }
+
+ /* Stop at the null chunk. We only need it for the last size. */
+ if (!chunk_id)
+ break;
+
+ for (i = 0; i < cf->chunks_nr; i++) {
+ if (cf->chunks[i].id == chunk_id) {
+ error(_("duplicate chunk ID %"PRIx32" found"),
+ chunk_id);
+ return -1;
+ }
+ }
+
+ ALLOC_GROW(cf->chunks, cf->chunks_nr + 1, cf->chunks_alloc);
+
+ cf->chunks[cf->chunks_nr].id = chunk_id;
+ cf->chunks[cf->chunks_nr].start = mfile + chunk_offset;
+ cf->chunks[cf->chunks_nr].offset = chunk_offset;
+ cf->chunks_nr++;
+ }
+
+ return 0;
+}
+
static int pair_chunk_fn(const unsigned char *chunk_start,
size_t chunk_size,
void *data)
diff --git a/chunk-format.h b/chunk-format.h
index 7885aa0848..acb8dfbce8 100644
--- a/chunk-format.h
+++ b/chunk-format.h
@@ -31,7 +31,14 @@ void add_chunk(struct chunkfile *cf,
uint32_t id,
size_t size,
chunk_write_fn fn);
-int write_chunkfile(struct chunkfile *cf, void *data);
+
+enum chunkfile_flags {
+ CHUNKFILE_TRAILING_TOC = (1 << 0),
+};
+
+int write_chunkfile(struct chunkfile *cf,
+ enum chunkfile_flags flags,
+ void *data);
int read_table_of_contents(struct chunkfile *cf,
const unsigned char *mfile,
@@ -39,6 +46,15 @@ int read_table_of_contents(struct chunkfile *cf,
uint64_t toc_offset,
int toc_length);
+/**
+ * Read the given chunkfile, but read the table of contents from the
+ * end of the given mfile. The file is expected to be a hashfile with
+ * the_hash_file->rawsz bytes at the end storing the hash.
+ */
+int read_trailing_table_of_contents(struct chunkfile *cf,
+ const unsigned char *mfile,
+ size_t mfile_size);
+
#define CHUNK_NOT_FOUND (-2)
/*
diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh
index a3ae5ff397..32eb280da0 100755
--- a/ci/run-build-and-tests.sh
+++ b/ci/run-build-and-tests.sh
@@ -30,6 +30,7 @@ linux-TEST-vars)
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master
export GIT_TEST_WRITE_REV_INDEX=1
export GIT_TEST_CHECKOUT_WORKERS=2
+ export GIT_TEST_PACKED_REFS_VERSION=2
;;
linux-clang)
export GIT_TEST_DEFAULT_HASH=sha1
diff --git a/commit-graph.c b/commit-graph.c
index a7d8755932..c927b81250 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -1932,7 +1932,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
get_num_chunks(cf) * ctx->commits.nr);
}
- write_chunkfile(cf, ctx);
+ write_chunkfile(cf, 0, ctx);
stop_progress(&ctx->progress);
strbuf_release(&progress_title);
diff --git a/csum-file.c b/csum-file.c
index 59ef3398ca..3243473c3d 100644
--- a/csum-file.c
+++ b/csum-file.c
@@ -45,7 +45,8 @@ void hashflush(struct hashfile *f)
unsigned offset = f->offset;
if (offset) {
- the_hash_algo->update_fn(&f->ctx, f->buffer, offset);
+ if (!f->skip_hash)
+ the_hash_algo->update_fn(&f->ctx, f->buffer, offset);
flush(f, f->buffer, offset);
f->offset = 0;
}
@@ -64,7 +65,12 @@ int finalize_hashfile(struct hashfile *f, unsigned char *result,
int fd;
hashflush(f);
- the_hash_algo->final_fn(f->buffer, &f->ctx);
+
+ if (f->skip_hash)
+ memset(f->buffer, 0, the_hash_algo->rawsz);
+ else
+ the_hash_algo->final_fn(f->buffer, &f->ctx);
+
if (result)
hashcpy(result, f->buffer);
if (flags & CSUM_HASH_IN_STREAM)
@@ -108,7 +114,8 @@ void hashwrite(struct hashfile *f, const void *buf, unsigned int count)
* the hashfile's buffer. In this block,
* f->offset is necessarily zero.
*/
- the_hash_algo->update_fn(&f->ctx, buf, nr);
+ if (!f->skip_hash)
+ the_hash_algo->update_fn(&f->ctx, buf, nr);
flush(f, buf, nr);
} else {
/*
@@ -153,6 +160,7 @@ static struct hashfile *hashfd_internal(int fd, const char *name,
f->tp = tp;
f->name = name;
f->do_crc = 0;
+ f->skip_hash = 0;
the_hash_algo->init_fn(&f->ctx);
f->buffer_len = buffer_len;
diff --git a/csum-file.h b/csum-file.h
index 0d29f528fb..29468067f8 100644
--- a/csum-file.h
+++ b/csum-file.h
@@ -20,6 +20,13 @@ struct hashfile {
size_t buffer_len;
unsigned char *buffer;
unsigned char *check_buffer;
+
+ /**
+ * If set to 1, skip_hash indicates that we should
+ * not actually compute the hash for this hashfile and
+ * instead only use it as a buffered write.
+ */
+ unsigned int skip_hash;
};
/* Checkpoint */
diff --git a/midx.c b/midx.c
index 7cfad04a24..03d947a5d3 100644
--- a/midx.c
+++ b/midx.c
@@ -1510,7 +1510,7 @@ static int write_midx_internal(const char *object_dir,
}
write_midx_header(f, get_num_chunks(cf), ctx.nr - dropped_packs);
- write_chunkfile(cf, &ctx);
+ write_chunkfile(cf, 0, &ctx);
finalize_hashfile(f, midx_hash, FSYNC_COMPONENT_PACK_METADATA,
CSUM_FSYNC | CSUM_HASH_IN_STREAM);
diff --git a/read-cache.c b/read-cache.c
index 7c6477487a..ac9f8122e5 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1817,6 +1817,8 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
git_hash_ctx c;
unsigned char hash[GIT_MAX_RAWSZ];
int hdr_version;
+ int all_zeroes = 1;
+ unsigned char *start, *end;
if (hdr->hdr_signature != htonl(CACHE_SIGNATURE))
return error(_("bad signature 0x%08x"), hdr->hdr_signature);
@@ -1827,10 +1829,23 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
if (!verify_index_checksum)
return 0;
+ end = (unsigned char *)hdr + size;
+ start = end - the_hash_algo->rawsz;
+ while (start < end) {
+ if (*start != 0) {
+ all_zeroes = 0;
+ break;
+ }
+ start++;
+ }
+
+ if (all_zeroes)
+ return 0;
+
the_hash_algo->init_fn(&c);
the_hash_algo->update_fn(&c, hdr, size - the_hash_algo->rawsz);
the_hash_algo->final_fn(hash, &c);
- if (!hasheq(hash, (unsigned char *)hdr + size - the_hash_algo->rawsz))
+ if (!hasheq(hash, end - the_hash_algo->rawsz))
return error(_("bad index file sha1 signature"));
return 0;
}
@@ -2922,9 +2937,14 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
int ieot_entries = 1;
struct index_entry_offset_table *ieot = NULL;
int nr, nr_threads;
+ int compute_hash;
f = hashfd(tempfile->fd, tempfile->filename.buf);
+ if (!git_config_get_maybe_bool("index.computehash", &compute_hash) &&
+ !compute_hash)
+ f->skip_hash = 1;
+
for (i = removed = extended = 0; i < entries; i++) {
if (cache[i]->ce_flags & CE_REMOVE)
removed++;
diff --git a/refs.c b/refs.c
index 2c7e88b190..4cc3b16db9 100644
--- a/refs.c
+++ b/refs.c
@@ -1976,6 +1976,17 @@ static struct ref_store *lookup_ref_store_map(struct hashmap *map,
return entry ? entry->refs : NULL;
}
+static int add_ref_format_flags(enum ref_format_flags flags, int caps) {
+ if (flags & REF_FORMAT_FILES)
+ caps |= REF_STORE_FORMAT_FILES;
+ if (flags & REF_FORMAT_PACKED)
+ caps |= REF_STORE_FORMAT_PACKED;
+ if (flags & REF_FORMAT_PACKED_V2)
+ caps |= REF_STORE_FORMAT_PACKED_V2;
+
+ return caps;
+}
+
/*
* Create, record, and return a ref_store instance for the specified
* gitdir.
@@ -1985,9 +1996,17 @@ static struct ref_store *ref_store_init(struct repository *repo,
unsigned int flags)
{
const char *be_name = "files";
- struct ref_storage_be *be = find_ref_storage_backend(be_name);
+ struct ref_storage_be *be;
struct ref_store *refs;
+ flags = add_ref_format_flags(repo->ref_format, flags);
+
+ if (!(flags & REF_STORE_FORMAT_FILES) &&
+ packed_refs_enabled(flags))
+ be_name = "packed";
+
+ be = find_ref_storage_backend(be_name);
+
if (!be)
BUG("reference backend %s is unknown", be_name);
@@ -2003,7 +2022,8 @@ struct ref_store *get_main_ref_store(struct repository *r)
if (!r->gitdir)
BUG("attempting to get main_ref_store outside of repository");
- r->refs_private = ref_store_init(r, r->gitdir, REF_STORE_ALL_CAPS);
+ r->refs_private = ref_store_init(r, r->gitdir,
+ REF_STORE_ALL_CAPS);
r->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);
return r->refs_private;
}
diff --git a/refs/files-backend.c b/refs/files-backend.c
index b89954355d..4a18aed620 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -1198,6 +1198,12 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)
struct strbuf err = STRBUF_INIT;
struct ref_transaction *transaction;
+ if (!packed_refs_enabled(refs->store_flags)) {
+ warning(_("refusing to create '%s' file because '%s' is not set"),
+ "packed-refs", "extensions.refFormat=packed");
+ return -1;
+ }
+
transaction = ref_store_transaction_begin(refs->packed_ref_store, &err);
if (!transaction)
return -1;
@@ -3274,7 +3280,7 @@ static int files_init_db(struct ref_store *ref_store, struct strbuf *err UNUSED)
}
struct ref_storage_be refs_be_files = {
- .next = NULL,
+ .next = &refs_be_packed,
.name = "files",
.init = files_ref_store_create,
.init_db = files_init_db,
diff --git a/refs/packed-backend.c b/refs/packed-backend.c
index c1c71d183e..e84f669c42 100644
--- a/refs/packed-backend.c
+++ b/refs/packed-backend.c
@@ -36,121 +36,6 @@ static enum mmap_strategy mmap_strategy = MMAP_TEMPORARY;
static enum mmap_strategy mmap_strategy = MMAP_OK;
#endif
-struct packed_ref_store;
-
-/*
- * A `snapshot` represents one snapshot of a `packed-refs` file.
- *
- * Normally, this will be a mmapped view of the contents of the
- * `packed-refs` file at the time the snapshot was created. However,
- * if the `packed-refs` file was not sorted, this might point at heap
- * memory holding the contents of the `packed-refs` file with its
- * records sorted by refname.
- *
- * `snapshot` instances are reference counted (via
- * `acquire_snapshot()` and `release_snapshot()`). This is to prevent
- * an instance from disappearing while an iterator is still iterating
- * over it. Instances are garbage collected when their `referrers`
- * count goes to zero.
- *
- * The most recent `snapshot`, if available, is referenced by the
- * `packed_ref_store`. Its freshness is checked whenever
- * `get_snapshot()` is called; if the existing snapshot is obsolete, a
- * new snapshot is taken.
- */
-struct snapshot {
- /*
- * A back-pointer to the packed_ref_store with which this
- * snapshot is associated:
- */
- struct packed_ref_store *refs;
-
- /* Is the `packed-refs` file currently mmapped? */
- int mmapped;
-
- /*
- * The contents of the `packed-refs` file:
- *
- * - buf -- a pointer to the start of the memory
- * - start -- a pointer to the first byte of actual references
- * (i.e., after the header line, if one is present)
- * - eof -- a pointer just past the end of the reference
- * contents
- *
- * If the `packed-refs` file was already sorted, `buf` points
- * at the mmapped contents of the file. If not, it points at
- * heap-allocated memory containing the contents, sorted. If
- * there were no contents (e.g., because the file didn't
- * exist), `buf`, `start`, and `eof` are all NULL.
- */
- char *buf, *start, *eof;
-
- /*
- * What is the peeled state of the `packed-refs` file that
- * this snapshot represents? (This is usually determined from
- * the file's header.)
- */
- enum { PEELED_NONE, PEELED_TAGS, PEELED_FULLY } peeled;
-
- /*
- * Count of references to this instance, including the pointer
- * from `packed_ref_store::snapshot`, if any. The instance
- * will not be freed as long as the reference count is
- * nonzero.
- */
- unsigned int referrers;
-
- /*
- * The metadata of the `packed-refs` file from which this
- * snapshot was created, used to tell if the file has been
- * replaced since we read it.
- */
- struct stat_validity validity;
-};
-
-/*
- * A `ref_store` representing references stored in a `packed-refs`
- * file. It implements the `ref_store` interface, though it has some
- * limitations:
- *
- * - It cannot store symbolic references.
- *
- * - It cannot store reflogs.
- *
- * - It does not support reference renaming (though it could).
- *
- * On the other hand, it can be locked outside of a reference
- * transaction. In that case, it remains locked even after the
- * transaction is done and the new `packed-refs` file is activated.
- */
-struct packed_ref_store {
- struct ref_store base;
-
- unsigned int store_flags;
-
- /* The path of the "packed-refs" file: */
- char *path;
-
- /*
- * A snapshot of the values read from the `packed-refs` file,
- * if it might still be current; otherwise, NULL.
- */
- struct snapshot *snapshot;
-
- /*
- * Lock used for the "packed-refs" file. Note that this (and
- * thus the enclosing `packed_ref_store`) must not be freed.
- */
- struct lock_file lock;
-
- /*
- * Temporary file used when rewriting new contents to the
- * "packed-refs" file. Note that this (and thus the enclosing
- * `packed_ref_store`) must not be freed.
- */
- struct tempfile *tempfile;
-};
-
/*
* Increment the reference count of `*snapshot`.
*/
@@ -164,7 +49,7 @@ static void acquire_snapshot(struct snapshot *snapshot)
* memory and close the file, or free the memory. Then set the buffer
* pointers to NULL.
*/
-static void clear_snapshot_buffer(struct snapshot *snapshot)
+void clear_snapshot_buffer(struct snapshot *snapshot)
{
if (snapshot->mmapped) {
if (munmap(snapshot->buf, snapshot->eof - snapshot->buf))
@@ -181,7 +66,7 @@ static void clear_snapshot_buffer(struct snapshot *snapshot)
* Decrease the reference count of `*snapshot`. If it goes to zero,
* free `*snapshot` and return true; otherwise return false.
*/
-static int release_snapshot(struct snapshot *snapshot)
+int release_snapshot(struct snapshot *snapshot)
{
if (!--snapshot->referrers) {
stat_validity_clear(&snapshot->validity);
@@ -245,224 +130,6 @@ static void clear_snapshot(struct packed_ref_store *refs)
}
}
-static NORETURN void die_unterminated_line(const char *path,
- const char *p, size_t len)
-{
- if (len < 80)
- die("unterminated line in %s: %.*s", path, (int)len, p);
- else
- die("unterminated line in %s: %.75s...", path, p);
-}
-
-static NORETURN void die_invalid_line(const char *path,
- const char *p, size_t len)
-{
- const char *eol = memchr(p, '\n', len);
-
- if (!eol)
- die_unterminated_line(path, p, len);
- else if (eol - p < 80)
- die("unexpected line in %s: %.*s", path, (int)(eol - p), p);
- else
- die("unexpected line in %s: %.75s...", path, p);
-
-}
-
-struct snapshot_record {
- const char *start;
- size_t len;
-};
-
-static int cmp_packed_ref_records(const void *v1, const void *v2)
-{
- const struct snapshot_record *e1 = v1, *e2 = v2;
- const char *r1 = e1->start + the_hash_algo->hexsz + 1;
- const char *r2 = e2->start + the_hash_algo->hexsz + 1;
-
- while (1) {
- if (*r1 == '\n')
- return *r2 == '\n' ? 0 : -1;
- if (*r1 != *r2) {
- if (*r2 == '\n')
- return 1;
- else
- return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
- }
- r1++;
- r2++;
- }
-}
-
-/*
- * Compare a snapshot record at `rec` to the specified NUL-terminated
- * refname.
- */
-static int cmp_record_to_refname(const char *rec, const char *refname)
-{
- const char *r1 = rec + the_hash_algo->hexsz + 1;
- const char *r2 = refname;
-
- while (1) {
- if (*r1 == '\n')
- return *r2 ? -1 : 0;
- if (!*r2)
- return 1;
- if (*r1 != *r2)
- return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
- r1++;
- r2++;
- }
-}
-
-/*
- * `snapshot->buf` is not known to be sorted. Check whether it is, and
- * if not, sort it into new memory and munmap/free the old storage.
- */
-static void sort_snapshot(struct snapshot *snapshot)
-{
- struct snapshot_record *records = NULL;
- size_t alloc = 0, nr = 0;
- int sorted = 1;
- const char *pos, *eof, *eol;
- size_t len, i;
- char *new_buffer, *dst;
-
- pos = snapshot->start;
- eof = snapshot->eof;
-
- if (pos == eof)
- return;
-
- len = eof - pos;
-
- /*
- * Initialize records based on a crude estimate of the number
- * of references in the file (we'll grow it below if needed):
- */
- ALLOC_GROW(records, len / 80 + 20, alloc);
-
- while (pos < eof) {
- eol = memchr(pos, '\n', eof - pos);
- if (!eol)
- /* The safety check should prevent this. */
- BUG("unterminated line found in packed-refs");
- if (eol - pos < the_hash_algo->hexsz + 2)
- die_invalid_line(snapshot->refs->path,
- pos, eof - pos);
- eol++;
- if (eol < eof && *eol == '^') {
- /*
- * Keep any peeled line together with its
- * reference:
- */
- const char *peeled_start = eol;
-
- eol = memchr(peeled_start, '\n', eof - peeled_start);
- if (!eol)
- /* The safety check should prevent this. */
- BUG("unterminated peeled line found in packed-refs");
- eol++;
- }
-
- ALLOC_GROW(records, nr + 1, alloc);
- records[nr].start = pos;
- records[nr].len = eol - pos;
- nr++;
-
- if (sorted &&
- nr > 1 &&
- cmp_packed_ref_records(&records[nr - 2],
- &records[nr - 1]) >= 0)
- sorted = 0;
-
- pos = eol;
- }
-
- if (sorted)
- goto cleanup;
-
- /* We need to sort the memory. First we sort the records array: */
- QSORT(records, nr, cmp_packed_ref_records);
-
- /*
- * Allocate a new chunk of memory, and copy the old memory to
- * the new in the order indicated by `records` (not bothering
- * with the header line):
- */
- new_buffer = xmalloc(len);
- for (dst = new_buffer, i = 0; i < nr; i++) {
- memcpy(dst, records[i].start, records[i].len);
- dst += records[i].len;
- }
-
- /*
- * Now munmap the old buffer and use the sorted buffer in its
- * place:
- */
- clear_snapshot_buffer(snapshot);
- snapshot->buf = snapshot->start = new_buffer;
- snapshot->eof = new_buffer + len;
-
-cleanup:
- free(records);
-}
-
-/*
- * Return a pointer to the start of the record that contains the
- * character `*p` (which must be within the buffer). If no other
- * record start is found, return `buf`.
- */
-static const char *find_start_of_record(const char *buf, const char *p)
-{
- while (p > buf && (p[-1] != '\n' || p[0] == '^'))
- p--;
- return p;
-}
-
-/*
- * Return a pointer to the start of the record following the record
- * that contains `*p`. If none is found before `end`, return `end`.
- */
-static const char *find_end_of_record(const char *p, const char *end)
-{
- while (++p < end && (p[-1] != '\n' || p[0] == '^'))
- ;
- return p;
-}
-
-/*
- * We want to be able to compare mmapped reference records quickly,
- * without totally parsing them. We can do so because the records are
- * LF-terminated, and the refname should start exactly (GIT_SHA1_HEXSZ
- * + 1) bytes past the beginning of the record.
- *
- * But what if the `packed-refs` file contains garbage? We're willing
- * to tolerate not detecting the problem, as long as we don't produce
- * totally garbled output (we can't afford to check the integrity of
- * the whole file during every Git invocation). But we do want to be
- * sure that we never read past the end of the buffer in memory and
- * perform an illegal memory access.
- *
- * Guarantee that minimum level of safety by verifying that the last
- * record in the file is LF-terminated, and that it has at least
- * (GIT_SHA1_HEXSZ + 1) characters before the LF. Die if either of
- * these checks fails.
- */
-static void verify_buffer_safe(struct snapshot *snapshot)
-{
- const char *start = snapshot->start;
- const char *eof = snapshot->eof;
- const char *last_line;
-
- if (start == eof)
- return;
-
- last_line = find_start_of_record(start, eof - 1);
- if (*(eof - 1) != '\n' || eof - last_line < the_hash_algo->hexsz + 2)
- die_invalid_line(snapshot->refs->path,
- last_line, eof - last_line);
-}
-
#define SMALL_FILE_SIZE (32*1024)
/*
@@ -475,9 +142,11 @@ static int load_contents(struct snapshot *snapshot)
{
int fd;
struct stat st;
- size_t size;
ssize_t bytes_read;
+ if (!packed_refs_enabled(snapshot->refs->store_flags))
+ return 0;
+
fd = open(snapshot->refs->path, O_RDONLY);
if (fd < 0) {
if (errno == ENOENT) {
@@ -498,91 +167,30 @@ static int load_contents(struct snapshot *snapshot)
if (fstat(fd, &st) < 0)
die_errno("couldn't stat %s", snapshot->refs->path);
- size = xsize_t(st.st_size);
+ snapshot->buflen = xsize_t(st.st_size);
- if (!size) {
+ if (!snapshot->buflen) {
close(fd);
return 0;
- } else if (mmap_strategy == MMAP_NONE || size <= SMALL_FILE_SIZE) {
- snapshot->buf = xmalloc(size);
- bytes_read = read_in_full(fd, snapshot->buf, size);
- if (bytes_read < 0 || bytes_read != size)
+ } else if (mmap_strategy == MMAP_NONE || snapshot->buflen <= SMALL_FILE_SIZE) {
+ snapshot->buf = xmalloc(snapshot->buflen);
+ bytes_read = read_in_full(fd, snapshot->buf, snapshot->buflen);
+ if (bytes_read < 0 || bytes_read != snapshot->buflen)
die_errno("couldn't read %s", snapshot->refs->path);
snapshot->mmapped = 0;
} else {
- snapshot->buf = xmmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ snapshot->buf = xmmap(NULL, snapshot->buflen, PROT_READ, MAP_PRIVATE, fd, 0);
snapshot->mmapped = 1;
}
close(fd);
snapshot->start = snapshot->buf;
- snapshot->eof = snapshot->buf + size;
+ snapshot->eof = snapshot->buf + snapshot->buflen;
return 1;
}
/*
- * Find the place in `snapshot->buf` where the start of the record for
- * `refname` starts. If `mustexist` is true and the reference doesn't
- * exist, then return NULL. If `mustexist` is false and the reference
- * doesn't exist, then return the point where that reference would be
- * inserted, or `snapshot->eof` (which might be NULL) if it would be
- * inserted at the end of the file. In the latter mode, `refname`
- * doesn't have to be a proper reference name; for example, one could
- * search for "refs/replace/" to find the start of any replace
- * references.
- *
- * The record is sought using a binary search, so `snapshot->buf` must
- * be sorted.
- */
-static const char *find_reference_location(struct snapshot *snapshot,
- const char *refname, int mustexist)
-{
- /*
- * This is not *quite* a garden-variety binary search, because
- * the data we're searching is made up of records, and we
- * always need to find the beginning of a record to do a
- * comparison. A "record" here is one line for the reference
- * itself and zero or one peel lines that start with '^'. Our
- * loop invariant is described in the next two comments.
- */
-
- /*
- * A pointer to the character at the start of a record whose
- * preceding records all have reference names that come
- * *before* `refname`.
- */
- const char *lo = snapshot->start;
-
- /*
- * A pointer to a the first character of a record whose
- * reference name comes *after* `refname`.
- */
- const char *hi = snapshot->eof;
-
- while (lo != hi) {
- const char *mid, *rec;
- int cmp;
-
- mid = lo + (hi - lo) / 2;
- rec = find_start_of_record(lo, mid);
- cmp = cmp_record_to_refname(rec, refname);
- if (cmp < 0) {
- lo = find_end_of_record(mid, hi);
- } else if (cmp > 0) {
- hi = rec;
- } else {
- return rec;
- }
- }
-
- if (mustexist)
- return NULL;
- else
- return lo;
-}
-
-/*
* Create a newly-allocated `snapshot` of the `packed-refs` file in
* its current state and return it. The return value will already have
* its reference count incremented.
@@ -623,72 +231,52 @@ static struct snapshot *create_snapshot(struct packed_ref_store *refs)
snapshot->refs = refs;
acquire_snapshot(snapshot);
snapshot->peeled = PEELED_NONE;
+ snapshot->version = 1;
if (!load_contents(snapshot))
return snapshot;
- /* If the file has a header line, process it: */
- if (snapshot->buf < snapshot->eof && *snapshot->buf == '#') {
- char *tmp, *p, *eol;
- struct string_list traits = STRING_LIST_INIT_NODUP;
-
- eol = memchr(snapshot->buf, '\n',
- snapshot->eof - snapshot->buf);
- if (!eol)
- die_unterminated_line(refs->path,
- snapshot->buf,
- snapshot->eof - snapshot->buf);
-
- tmp = xmemdupz(snapshot->buf, eol - snapshot->buf);
-
- if (!skip_prefix(tmp, "# pack-refs with:", (const char **)&p))
- die_invalid_line(refs->path,
- snapshot->buf,
- snapshot->eof - snapshot->buf);
-
- string_list_split_in_place(&traits, p, ' ', -1);
-
- if (unsorted_string_list_has_string(&traits, "fully-peeled"))
- snapshot->peeled = PEELED_FULLY;
- else if (unsorted_string_list_has_string(&traits, "peeled"))
- snapshot->peeled = PEELED_TAGS;
+ if ((refs->store_flags & REF_STORE_FORMAT_PACKED) &&
+ !detect_packed_format_v2_header(refs, snapshot)) {
+ parse_packed_format_v1_header(refs, snapshot, &sorted);
+ snapshot->version = 1;
+ verify_buffer_safe_v1(snapshot);
- sorted = unsorted_string_list_has_string(&traits, "sorted");
+ if (!sorted) {
+ sort_snapshot_v1(snapshot);
- /* perhaps other traits later as well */
-
- /* The "+ 1" is for the LF character. */
- snapshot->start = eol + 1;
-
- string_list_clear(&traits, 0);
- free(tmp);
- }
-
- verify_buffer_safe(snapshot);
+ /*
+ * Reordering the records might have moved a short one
+ * to the end of the buffer, so verify the buffer's
+ * safety again:
+ */
+ verify_buffer_safe_v1(snapshot);
+ }
- if (!sorted) {
- sort_snapshot(snapshot);
+ if (mmap_strategy != MMAP_OK && snapshot->mmapped) {
+ /*
+ * We don't want to leave the file mmapped, so we are
+ * forced to make a copy now:
+ */
+ char *buf_copy = xmalloc(snapshot->buflen);
+
+ memcpy(buf_copy, snapshot->start, snapshot->buflen);
+ clear_snapshot_buffer(snapshot);
+ snapshot->buf = snapshot->start = buf_copy;
+ snapshot->eof = buf_copy + snapshot->buflen;
+ }
- /*
- * Reordering the records might have moved a short one
- * to the end of the buffer, so verify the buffer's
- * safety again:
- */
- verify_buffer_safe(snapshot);
+ return snapshot;
}
- if (mmap_strategy != MMAP_OK && snapshot->mmapped) {
+ if (refs->store_flags & REF_STORE_FORMAT_PACKED_V2) {
/*
- * We don't want to leave the file mmapped, so we are
- * forced to make a copy now:
+ * Assume we are in v2 format mode, now.
+ *
+ * fill_snapshot_v2() will die() if parsing fails.
*/
- size_t size = snapshot->eof - snapshot->start;
- char *buf_copy = xmalloc(size);
-
- memcpy(buf_copy, snapshot->start, size);
- clear_snapshot_buffer(snapshot);
- snapshot->buf = snapshot->start = buf_copy;
- snapshot->eof = buf_copy + size;
+ fill_snapshot_v2(snapshot);
+ snapshot->version = 2;
}
return snapshot;
@@ -732,54 +320,26 @@ static int packed_read_raw_ref(struct ref_store *ref_store, const char *refname,
struct packed_ref_store *refs =
packed_downcast(ref_store, REF_STORE_READ, "read_raw_ref");
struct snapshot *snapshot = get_snapshot(refs);
- const char *rec;
-
- *type = 0;
- rec = find_reference_location(snapshot, refname, 1);
-
- if (!rec) {
+ if (!snapshot) {
/* refname is not a packed reference. */
*failure_errno = ENOENT;
return -1;
}
- if (get_oid_hex(rec, oid))
- die_invalid_line(refs->path, rec, snapshot->eof - rec);
-
- *type = REF_ISPACKED;
- return 0;
-}
-
-/*
- * This value is set in `base.flags` if the peeled value of the
- * current reference is known. In that case, `peeled` contains the
- * correct peeled value for the reference, which might be `null_oid`
- * if the reference is not a tag or if it is broken.
- */
-#define REF_KNOWS_PEELED 0x40
-
-/*
- * An iterator over a snapshot of a `packed-refs` file.
- */
-struct packed_ref_iterator {
- struct ref_iterator base;
-
- struct snapshot *snapshot;
-
- /* The current position in the snapshot's buffer: */
- const char *pos;
+ switch (snapshot->version) {
+ case 1:
+ return packed_read_raw_ref_v1(refs, snapshot, refname,
+ oid, type, failure_errno);
- /* The end of the part of the buffer that will be iterated over: */
- const char *eof;
+ case 2:
+ return packed_read_raw_ref_v2(refs, snapshot, refname,
+ oid, type, failure_errno);
- /* Scratch space for current values: */
- struct object_id oid, peeled;
- struct strbuf refname_buf;
-
- struct repository *repo;
- unsigned int flags;
-};
+ default:
+ return -1;
+ }
+}
/*
* Move the iterator to the next record in the snapshot, without
@@ -790,68 +350,16 @@ struct packed_ref_iterator {
*/
static int next_record(struct packed_ref_iterator *iter)
{
- const char *p = iter->pos, *eol;
-
- strbuf_reset(&iter->refname_buf);
+ switch (iter->version) {
+ case 1:
+ return next_record_v1(iter);
- if (iter->pos == iter->eof)
- return ITER_DONE;
-
- iter->base.flags = REF_ISPACKED;
-
- if (iter->eof - p < the_hash_algo->hexsz + 2 ||
- parse_oid_hex(p, &iter->oid, &p) ||
- !isspace(*p++))
- die_invalid_line(iter->snapshot->refs->path,
- iter->pos, iter->eof - iter->pos);
-
- eol = memchr(p, '\n', iter->eof - p);
- if (!eol)
- die_unterminated_line(iter->snapshot->refs->path,
- iter->pos, iter->eof - iter->pos);
-
- strbuf_add(&iter->refname_buf, p, eol - p);
- iter->base.refname = iter->refname_buf.buf;
-
- if (check_refname_format(iter->base.refname, REFNAME_ALLOW_ONELEVEL)) {
- if (!refname_is_safe(iter->base.refname))
- die("packed refname is dangerous: %s",
- iter->base.refname);
- oidclr(&iter->oid);
- iter->base.flags |= REF_BAD_NAME | REF_ISBROKEN;
- }
- if (iter->snapshot->peeled == PEELED_FULLY ||
- (iter->snapshot->peeled == PEELED_TAGS &&
- starts_with(iter->base.refname, "refs/tags/")))
- iter->base.flags |= REF_KNOWS_PEELED;
-
- iter->pos = eol + 1;
-
- if (iter->pos < iter->eof && *iter->pos == '^') {
- p = iter->pos + 1;
- if (iter->eof - p < the_hash_algo->hexsz + 1 ||
- parse_oid_hex(p, &iter->peeled, &p) ||
- *p++ != '\n')
- die_invalid_line(iter->snapshot->refs->path,
- iter->pos, iter->eof - iter->pos);
- iter->pos = p;
+ case 2:
+ return next_record_v2(iter);
- /*
- * Regardless of what the file header said, we
- * definitely know the value of *this* reference. But
- * we suppress it if the reference is broken:
- */
- if ((iter->base.flags & REF_ISBROKEN)) {
- oidclr(&iter->peeled);
- iter->base.flags &= ~REF_KNOWS_PEELED;
- } else {
- iter->base.flags |= REF_KNOWS_PEELED;
- }
- } else {
- oidclr(&iter->peeled);
+ default:
+ return -1;
}
-
- return ITER_OK;
}
static int packed_ref_iterator_advance(struct ref_iterator *ref_iterator)
@@ -926,6 +434,7 @@ static struct ref_iterator *packed_ref_iterator_begin(
struct packed_ref_iterator *iter;
struct ref_iterator *ref_iterator;
unsigned int required_flags = REF_STORE_READ;
+ size_t v2_row = 0;
if (!(flags & DO_FOR_EACH_INCLUDE_BROKEN))
required_flags |= REF_STORE_ODB;
@@ -938,10 +447,21 @@ static struct ref_iterator *packed_ref_iterator_begin(
*/
snapshot = get_snapshot(refs);
- if (prefix && *prefix)
- start = find_reference_location(snapshot, prefix, 0);
- else
- start = snapshot->start;
+ if (!snapshot || snapshot->version < 0 || snapshot->version > 2)
+ return empty_ref_iterator_begin();
+
+ if (prefix && *prefix) {
+ if (snapshot->version == 1)
+ start = find_reference_location_v1(snapshot, prefix, 0);
+ else
+ start = find_reference_location_v2(snapshot, prefix, 0,
+ &v2_row);
+ } else {
+ if (snapshot->version == 1)
+ start = snapshot->start;
+ else
+ start = snapshot->refs_chunk;
+ }
if (start == snapshot->eof)
return empty_ref_iterator_begin();
@@ -952,6 +472,10 @@ static struct ref_iterator *packed_ref_iterator_begin(
iter->snapshot = snapshot;
acquire_snapshot(snapshot);
+ iter->version = snapshot->version;
+ iter->row = v2_row;
+
+ init_iterator_prefix_info(prefix, iter);
iter->pos = start;
iter->eof = snapshot->eof;
@@ -969,23 +493,6 @@ static struct ref_iterator *packed_ref_iterator_begin(
return ref_iterator;
}
-/*
- * Write an entry to the packed-refs file for the specified refname.
- * If peeled is non-NULL, write it as the entry's peeled value. On
- * error, return a nonzero value and leave errno set at the value left
- * by the failing call to `fprintf()`.
- */
-static int write_packed_entry(FILE *fh, const char *refname,
- const struct object_id *oid,
- const struct object_id *peeled)
-{
- if (fprintf(fh, "%s %s\n", oid_to_hex(oid), refname) < 0 ||
- (peeled && fprintf(fh, "^%s\n", oid_to_hex(peeled)) < 0))
- return -1;
-
- return 0;
-}
-
int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)
{
struct packed_ref_store *refs =
@@ -1067,17 +574,6 @@ int packed_refs_is_locked(struct ref_store *ref_store)
return is_lock_file_locked(&refs->lock);
}
-/*
- * The packed-refs header line that we write out. Perhaps other traits
- * will be added later.
- *
- * Note that earlier versions of Git used to parse these traits by
- * looking for " trait " in the line. For this reason, the space after
- * the colon and the trailing space are required.
- */
-static const char PACKED_REFS_HEADER[] =
- "# pack-refs with: peeled fully-peeled sorted \n";
-
static int packed_init_db(struct ref_store *ref_store UNUSED,
struct strbuf *err UNUSED)
{
@@ -1085,56 +581,20 @@ static int packed_init_db(struct ref_store *ref_store UNUSED,
return 0;
}
-/*
- * Write the packed refs from the current snapshot to the packed-refs
- * tempfile, incorporating any changes from `updates`. `updates` must
- * be a sorted string list whose keys are the refnames and whose util
- * values are `struct ref_update *`. On error, rollback the tempfile,
- * write an error message to `err`, and return a nonzero value.
- *
- * The packfile must be locked before calling this function and will
- * remain locked when it is done.
- */
-static int write_with_updates(struct packed_ref_store *refs,
- struct string_list *updates,
- struct strbuf *err)
+static void add_write_error(struct packed_ref_store *refs, struct strbuf *err)
{
- struct ref_iterator *iter = NULL;
- size_t i;
- int ok;
- FILE *out;
- struct strbuf sb = STRBUF_INIT;
- char *packed_refs_path;
-
- if (!is_lock_file_locked(&refs->lock))
- BUG("write_with_updates() called while unlocked");
-
- /*
- * If packed-refs is a symlink, we want to overwrite the
- * symlinked-to file, not the symlink itself. Also, put the
- * staging file next to it:
- */
- packed_refs_path = get_locked_file_path(&refs->lock);
- strbuf_addf(&sb, "%s.new", packed_refs_path);
- free(packed_refs_path);
- refs->tempfile = create_tempfile(sb.buf);
- if (!refs->tempfile) {
- strbuf_addf(err, "unable to create file %s: %s",
- sb.buf, strerror(errno));
- strbuf_release(&sb);
- return -1;
- }
- strbuf_release(&sb);
-
- out = fdopen_tempfile(refs->tempfile, "w");
- if (!out) {
- strbuf_addf(err, "unable to fdopen packed-refs tempfile: %s",
- strerror(errno));
- goto error;
- }
+ strbuf_addf(err, "error writing to %s: %s",
+ get_tempfile_path(refs->tempfile), strerror(errno));
+}
- if (fprintf(out, "%s", PACKED_REFS_HEADER) < 0)
- goto write_error;
+int merge_iterator_and_updates(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err,
+ write_ref_fn write_fn,
+ void *write_data)
+{
+ struct ref_iterator *iter = NULL;
+ int ok, i;
/*
* We iterate in parallel through the current list of refs and
@@ -1227,10 +687,13 @@ static int write_with_updates(struct packed_ref_store *refs,
struct object_id peeled;
int peel_error = ref_iterator_peel(iter, &peeled);
- if (write_packed_entry(out, iter->refname,
- iter->oid,
- peel_error ? NULL : &peeled))
- goto write_error;
+ if (write_fn(iter->refname,
+ iter->oid,
+ peel_error ? NULL : &peeled,
+ write_data)) {
+ add_write_error(refs, err);
+ goto error;
+ }
if ((ok = ref_iterator_advance(iter)) != ITER_OK)
iter = NULL;
@@ -1248,15 +711,133 @@ static int write_with_updates(struct packed_ref_store *refs,
int peel_error = peel_object(&update->new_oid,
&peeled);
- if (write_packed_entry(out, update->refname,
- &update->new_oid,
- peel_error ? NULL : &peeled))
- goto write_error;
+ if (write_fn(update->refname,
+ &update->new_oid,
+ peel_error ? NULL : &peeled,
+ write_data)) {
+ add_write_error(refs, err);
+ goto error;
+ }
i++;
}
}
+error:
+ if (iter)
+ ref_iterator_abort(iter);
+ return ok;
+}
+
+static int write_with_updates_v1(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err)
+{
+ FILE *out;
+
+ out = fdopen_tempfile(refs->tempfile, "w");
+ if (!out) {
+ strbuf_addf(err, "unable to fdopen packed-refs tempfile: %s",
+ strerror(errno));
+ goto error;
+ }
+
+ if (write_packed_file_header_v1(out) < 0) {
+ add_write_error(refs, err);
+ goto error;
+ }
+
+ return merge_iterator_and_updates(refs, updates, err,
+ write_packed_entry_v1, out);
+
+error:
+ return -1;
+}
+
+static int write_with_updates_v2(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err)
+{
+ struct write_packed_refs_v2_context *ctx = create_v2_context(refs, updates, err);
+ int ok = -1;
+
+ if ((ok = write_packed_refs_v2(ctx)) < 0)
+ add_write_error(refs, err);
+
+ free_v2_context(ctx);
+ return ok;
+}
+
+/*
+ * Write the packed refs from the current snapshot to the packed-refs
+ * tempfile, incorporating any changes from `updates`. `updates` must
+ * be a sorted string list whose keys are the refnames and whose util
+ * values are `struct ref_update *`. On error, rollback the tempfile,
+ * write an error message to `err`, and return a nonzero value.
+ *
+ * The packfile must be locked before calling this function and will
+ * remain locked when it is done.
+ */
+static int write_with_updates(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err)
+{
+ int ok;
+ struct strbuf sb = STRBUF_INIT;
+ char *packed_refs_path;
+ int version;
+
+ if (!is_lock_file_locked(&refs->lock))
+ BUG("write_with_updates() called while unlocked");
+
+ /*
+ * If packed-refs is a symlink, we want to overwrite the
+ * symlinked-to file, not the symlink itself. Also, put the
+ * staging file next to it:
+ */
+ packed_refs_path = get_locked_file_path(&refs->lock);
+ strbuf_addf(&sb, "%s.new", packed_refs_path);
+ free(packed_refs_path);
+ refs->tempfile = create_tempfile(sb.buf);
+ if (!refs->tempfile) {
+ strbuf_addf(err, "unable to create file %s: %s",
+ sb.buf, strerror(errno));
+ strbuf_release(&sb);
+ return -1;
+ }
+ strbuf_release(&sb);
+
+ if (!(version = git_env_ulong("GIT_TEST_PACKED_REFS_VERSION", 0)) &&
+ git_config_get_int("refs.packedrefsversion", &version)) {
+ /*
+ * Set the default depending on the current extension
+ * list. Default to version 1 if available, but allow a
+ * default of 2 if only "packed-v2" exists.
+ */
+ if (refs->store_flags & REF_STORE_FORMAT_PACKED)
+ version = 1;
+ else if (refs->store_flags & REF_STORE_FORMAT_PACKED_V2)
+ version = 2;
+ else
+ BUG("writing a packed-refs file without an extension");
+ }
+
+ switch (version) {
+ case 1:
+ ok = write_with_updates_v1(refs, updates, err);
+ break;
+
+ case 2:
+ /* Convert the normal error codes to ITER_DONE. */
+ ok = write_with_updates_v2(refs, updates, err) ? -2 : ITER_DONE;
+ break;
+
+ default:
+ strbuf_addf(err, "unknown packed-refs version: %d",
+ version);
+ goto error;
+ }
+
if (ok != ITER_DONE) {
strbuf_addstr(err, "unable to write packed-refs file: "
"error iterating over old contents");
@@ -1275,14 +856,7 @@ static int write_with_updates(struct packed_ref_store *refs,
return 0;
-write_error:
- strbuf_addf(err, "error writing to %s: %s",
- get_tempfile_path(refs->tempfile), strerror(errno));
-
error:
- if (iter)
- ref_iterator_abort(iter);
-
delete_tempfile(&refs->tempfile);
return -1;
}
diff --git a/refs/packed-backend.h b/refs/packed-backend.h
index 9dd8a344c3..1936bb5c76 100644
--- a/refs/packed-backend.h
+++ b/refs/packed-backend.h
@@ -1,6 +1,10 @@
#ifndef REFS_PACKED_BACKEND_H
#define REFS_PACKED_BACKEND_H
+#include "../cache.h"
+#include "refs-internal.h"
+#include "../lockfile.h"
+
struct repository;
struct ref_transaction;
@@ -36,4 +40,281 @@ int packed_refs_is_locked(struct ref_store *ref_store);
int is_packed_transaction_needed(struct ref_store *ref_store,
struct ref_transaction *transaction);
+struct packed_ref_store;
+
+/*
+ * A `snapshot` represents one snapshot of a `packed-refs` file.
+ *
+ * Normally, this will be a mmapped view of the contents of the
+ * `packed-refs` file at the time the snapshot was created. However,
+ * if the `packed-refs` file was not sorted, this might point at heap
+ * memory holding the contents of the `packed-refs` file with its
+ * records sorted by refname.
+ *
+ * `snapshot` instances are reference counted (via
+ * `acquire_snapshot()` and `release_snapshot()`). This is to prevent
+ * an instance from disappearing while an iterator is still iterating
+ * over it. Instances are garbage collected when their `referrers`
+ * count goes to zero.
+ *
+ * The most recent `snapshot`, if available, is referenced by the
+ * `packed_ref_store`. Its freshness is checked whenever
+ * `get_snapshot()` is called; if the existing snapshot is obsolete, a
+ * new snapshot is taken.
+ */
+struct snapshot {
+ /*
+ * A back-pointer to the packed_ref_store with which this
+ * snapshot is associated:
+ */
+ struct packed_ref_store *refs;
+
+ /* Is the `packed-refs` file currently mmapped? */
+ int mmapped;
+
+ /* which file format version is this file? */
+ int version;
+
+ /*
+ * The contents of the `packed-refs` file:
+ *
+ * - buf -- a pointer to the start of the memory
+ * - start -- a pointer to the first byte of actual references
+ * (i.e., after the header line, if one is present)
+ * - eof -- a pointer just past the end of the reference
+ * contents
+ *
+ * If the `packed-refs` file was already sorted, `buf` points
+ * at the mmapped contents of the file. If not, it points at
+ * heap-allocated memory containing the contents, sorted. If
+ * there were no contents (e.g., because the file didn't
+ * exist), `buf`, `start`, and `eof` are all NULL.
+ */
+ char *buf, *start, *eof;
+
+ /*
+ * What is the peeled state of the `packed-refs` file that
+ * this snapshot represents? (This is usually determined from
+ * the file's header.)
+ */
+ enum { PEELED_NONE, PEELED_TAGS, PEELED_FULLY } peeled;
+
+ /*************************
+ * packed-refs v2 values *
+ *************************/
+ size_t nr;
+ size_t prefixes_nr;
+ size_t buflen;
+ const unsigned char *offset_chunk;
+ const char *refs_chunk;
+ const unsigned char *prefix_offsets_chunk;
+ const char *prefix_chunk;
+
+ /*
+ * Count of references to this instance, including the pointer
+ * from `packed_ref_store::snapshot`, if any. The instance
+ * will not be freed as long as the reference count is
+ * nonzero.
+ */
+ unsigned int referrers;
+
+ /*
+ * The metadata of the `packed-refs` file from which this
+ * snapshot was created, used to tell if the file has been
+ * replaced since we read it.
+ */
+ struct stat_validity validity;
+};
+
+int release_snapshot(struct snapshot *snapshot);
+
+/*
+ * If the buffer in `snapshot` is active, then either munmap the
+ * memory and close the file, or free the memory. Then set the buffer
+ * pointers to NULL.
+ */
+void clear_snapshot_buffer(struct snapshot *snapshot);
+
+/*
+ * A `ref_store` representing references stored in a `packed-refs`
+ * file. It implements the `ref_store` interface, though it has some
+ * limitations:
+ *
+ * - It cannot store symbolic references.
+ *
+ * - It cannot store reflogs.
+ *
+ * - It does not support reference renaming (though it could).
+ *
+ * On the other hand, it can be locked outside of a reference
+ * transaction. In that case, it remains locked even after the
+ * transaction is done and the new `packed-refs` file is activated.
+ */
+struct packed_ref_store {
+ struct ref_store base;
+
+ unsigned int store_flags;
+
+ /* The path of the "packed-refs" file: */
+ char *path;
+
+ /*
+ * A snapshot of the values read from the `packed-refs` file,
+ * if it might still be current; otherwise, NULL.
+ */
+ struct snapshot *snapshot;
+
+ /*
+ * Lock used for the "packed-refs" file. Note that this (and
+ * thus the enclosing `packed_ref_store`) must not be freed.
+ */
+ struct lock_file lock;
+
+ /*
+ * Temporary file used when rewriting new contents to the
+ * "packed-refs" file. Note that this (and thus the enclosing
+ * `packed_ref_store`) must not be freed.
+ */
+ struct tempfile *tempfile;
+};
+
+/*
+ * This value is set in `base.flags` if the peeled value of the
+ * current reference is known. In that case, `peeled` contains the
+ * correct peeled value for the reference, which might be `null_oid`
+ * if the reference is not a tag or if it is broken.
+ */
+#define REF_KNOWS_PEELED 0x40
+
+/*
+ * An iterator over a snapshot of a `packed-refs` file.
+ */
+struct packed_ref_iterator {
+ struct ref_iterator base;
+ struct snapshot *snapshot;
+ struct repository *repo;
+ unsigned int flags;
+ int version;
+
+ /* Scratch space for current values: */
+ struct object_id oid, peeled;
+ struct strbuf refname_buf;
+
+ /* The current position in the snapshot's buffer: */
+ const char *pos;
+
+ /***********************************
+ * packed-refs v1 iterator values. *
+ ***********************************/
+
+ /* The end of the part of the buffer that will be iterated over: */
+ const char *eof;
+
+ /***********************************
+ * packed-refs v2 iterator values. *
+ ***********************************/
+ size_t nr;
+ size_t row;
+ size_t prefix_row_end;
+ size_t prefix_i;
+ const char *cur_prefix;
+};
+
+typedef int (*write_ref_fn)(const char *refname,
+ const struct object_id *oid,
+ const struct object_id *peeled,
+ void *write_data);
+
+int merge_iterator_and_updates(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err,
+ write_ref_fn write_fn,
+ void *write_data);
+
+/**
+ * Parse the buffer at the given snapshot to verify that it is a
+ * packed-refs file in version 1 format. Update the snapshot->peeled
+ * value according to the header information. Update the given
+ * 'sorted' value with whether or not the packed-refs file is sorted.
+ */
+int parse_packed_format_v1_header(struct packed_ref_store *refs,
+ struct snapshot *snapshot,
+ int *sorted);
+
+/*
+ * Find the place in `snapshot->buf` where the start of the record for
+ * `refname` starts. If `mustexist` is true and the reference doesn't
+ * exist, then return NULL. If `mustexist` is false and the reference
+ * doesn't exist, then return the point where that reference would be
+ * inserted, or `snapshot->eof` (which might be NULL) if it would be
+ * inserted at the end of the file. In the latter mode, `refname`
+ * doesn't have to be a proper reference name; for example, one could
+ * search for "refs/replace/" to find the start of any replace
+ * references.
+ *
+ * The record is sought using a binary search, so `snapshot->buf` must
+ * be sorted.
+ */
+const char *find_reference_location_v1(struct snapshot *snapshot,
+ const char *refname, int mustexist);
+
+int packed_read_raw_ref_v1(struct packed_ref_store *refs, struct snapshot *snapshot,
+ const char *refname, struct object_id *oid,
+ unsigned int *type, int *failure_errno);
+
+void verify_buffer_safe_v1(struct snapshot *snapshot);
+void sort_snapshot_v1(struct snapshot *snapshot);
+int write_packed_file_header_v1(FILE *out);
+int next_record_v1(struct packed_ref_iterator *iter);
+int write_packed_entry_v1(const char *refname,
+ const struct object_id *oid,
+ const struct object_id *peeled,
+ void *write_data);
+
+/**
+ * Parse the buffer at the given snapshot to verify that it is a
+ * packed-refs file in version 1 format. Update the snapshot->peeled
+ * value according to the header information. Update the given
+ * 'sorted' value with whether or not the packed-refs file is sorted.
+ */
+int parse_packed_format_v1_header(struct packed_ref_store *refs,
+ struct snapshot *snapshot,
+ int *sorted);
+
+int detect_packed_format_v2_header(struct packed_ref_store *refs,
+ struct snapshot *snapshot);
+/*
+ * Find the place in `snapshot->buf` where the start of the record for
+ * `refname` starts. If `mustexist` is true and the reference doesn't
+ * exist, then return NULL. If `mustexist` is false and the reference
+ * doesn't exist, then return the point where that reference would be
+ * inserted, or `snapshot->eof` (which might be NULL) if it would be
+ * inserted at the end of the file. In the latter mode, `refname`
+ * doesn't have to be a proper reference name; for example, one could
+ * search for "refs/replace/" to find the start of any replace
+ * references.
+ *
+ * The record is sought using a binary search, so `snapshot->buf` must
+ * be sorted.
+ */
+const char *find_reference_location_v2(struct snapshot *snapshot,
+ const char *refname, int mustexist,
+ size_t *pos);
+
+int packed_read_raw_ref_v2(struct packed_ref_store *refs, struct snapshot *snapshot,
+ const char *refname, struct object_id *oid,
+ unsigned int *type, int *failure_errno);
+int next_record_v2(struct packed_ref_iterator *iter);
+void fill_snapshot_v2(struct snapshot *snapshot);
+
+struct write_packed_refs_v2_context;
+struct write_packed_refs_v2_context *create_v2_context(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err);
+int write_packed_refs_v2(struct write_packed_refs_v2_context *ctx);
+void free_v2_context(struct write_packed_refs_v2_context *ctx);
+
+void init_iterator_prefix_info(const char *prefix,
+ struct packed_ref_iterator *iter);
+
#endif /* REFS_PACKED_BACKEND_H */
diff --git a/refs/packed-format-v1.c b/refs/packed-format-v1.c
new file mode 100644
index 0000000000..2d071567c0
--- /dev/null
+++ b/refs/packed-format-v1.c
@@ -0,0 +1,456 @@
+#include "../cache.h"
+#include "../config.h"
+#include "../refs.h"
+#include "refs-internal.h"
+#include "packed-backend.h"
+#include "../iterator.h"
+#include "../lockfile.h"
+#include "../chdir-notify.h"
+
+static NORETURN void die_unterminated_line(const char *path,
+ const char *p, size_t len)
+{
+ if (len < 80)
+ die("unterminated line in %s: %.*s", path, (int)len, p);
+ else
+ die("unterminated line in %s: %.75s...", path, p);
+}
+
+static NORETURN void die_invalid_line(const char *path,
+ const char *p, size_t len)
+{
+ const char *eol = memchr(p, '\n', len);
+
+ if (!eol)
+ die_unterminated_line(path, p, len);
+ else if (eol - p < 80)
+ die("unexpected line in %s: %.*s", path, (int)(eol - p), p);
+ else
+ die("unexpected line in %s: %.75s...", path, p);
+}
+
+struct snapshot_record {
+ const char *start;
+ size_t len;
+};
+
+static int cmp_packed_ref_records(const void *v1, const void *v2)
+{
+ const struct snapshot_record *e1 = v1, *e2 = v2;
+ const char *r1 = e1->start + the_hash_algo->hexsz + 1;
+ const char *r2 = e2->start + the_hash_algo->hexsz + 1;
+
+ while (1) {
+ if (*r1 == '\n')
+ return *r2 == '\n' ? 0 : -1;
+ if (*r1 != *r2) {
+ if (*r2 == '\n')
+ return 1;
+ else
+ return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
+ }
+ r1++;
+ r2++;
+ }
+}
+
+/*
+ * Compare a snapshot record at `rec` to the specified NUL-terminated
+ * refname.
+ */
+static int cmp_record_to_refname(const char *rec, const char *refname)
+{
+ const char *r1 = rec + the_hash_algo->hexsz + 1;
+ const char *r2 = refname;
+
+ while (1) {
+ if (*r1 == '\n')
+ return *r2 ? -1 : 0;
+ if (!*r2)
+ return 1;
+ if (*r1 != *r2)
+ return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
+ r1++;
+ r2++;
+ }
+}
+
+/*
+ * `snapshot->buf` is not known to be sorted. Check whether it is, and
+ * if not, sort it into new memory and munmap/free the old storage.
+ */
+void sort_snapshot_v1(struct snapshot *snapshot)
+{
+ struct snapshot_record *records = NULL;
+ size_t alloc = 0, nr = 0;
+ int sorted = 1;
+ const char *pos, *eof, *eol;
+ size_t len, i;
+ char *new_buffer, *dst;
+
+ pos = snapshot->start;
+ eof = snapshot->eof;
+
+ if (pos == eof)
+ return;
+
+ len = eof - pos;
+
+ /*
+ * Initialize records based on a crude estimate of the number
+ * of references in the file (we'll grow it below if needed):
+ */
+ ALLOC_GROW(records, len / 80 + 20, alloc);
+
+ while (pos < eof) {
+ eol = memchr(pos, '\n', eof - pos);
+ if (!eol)
+ /* The safety check should prevent this. */
+ BUG("unterminated line found in packed-refs");
+ if (eol - pos < the_hash_algo->hexsz + 2)
+ die_invalid_line(snapshot->refs->path,
+ pos, eof - pos);
+ eol++;
+ if (eol < eof && *eol == '^') {
+ /*
+ * Keep any peeled line together with its
+ * reference:
+ */
+ const char *peeled_start = eol;
+
+ eol = memchr(peeled_start, '\n', eof - peeled_start);
+ if (!eol)
+ /* The safety check should prevent this. */
+ BUG("unterminated peeled line found in packed-refs");
+ eol++;
+ }
+
+ ALLOC_GROW(records, nr + 1, alloc);
+ records[nr].start = pos;
+ records[nr].len = eol - pos;
+ nr++;
+
+ if (sorted &&
+ nr > 1 &&
+ cmp_packed_ref_records(&records[nr - 2],
+ &records[nr - 1]) >= 0)
+ sorted = 0;
+
+ pos = eol;
+ }
+
+ if (sorted)
+ goto cleanup;
+
+ /* We need to sort the memory. First we sort the records array: */
+ QSORT(records, nr, cmp_packed_ref_records);
+
+ /*
+ * Allocate a new chunk of memory, and copy the old memory to
+ * the new in the order indicated by `records` (not bothering
+ * with the header line):
+ */
+ new_buffer = xmalloc(len);
+ for (dst = new_buffer, i = 0; i < nr; i++) {
+ memcpy(dst, records[i].start, records[i].len);
+ dst += records[i].len;
+ }
+
+ /*
+ * Now munmap the old buffer and use the sorted buffer in its
+ * place:
+ */
+ clear_snapshot_buffer(snapshot);
+ snapshot->buf = snapshot->start = new_buffer;
+ snapshot->eof = new_buffer + len;
+
+cleanup:
+ free(records);
+}
+
+/*
+ * Return a pointer to the start of the record that contains the
+ * character `*p` (which must be within the buffer). If no other
+ * record start is found, return `buf`.
+ */
+static const char *find_start_of_record(const char *buf, const char *p)
+{
+ while (p > buf && (p[-1] != '\n' || p[0] == '^'))
+ p--;
+ return p;
+}
+
+/*
+ * Return a pointer to the start of the record following the record
+ * that contains `*p`. If none is found before `end`, return `end`.
+ */
+static const char *find_end_of_record(const char *p, const char *end)
+{
+ while (++p < end && (p[-1] != '\n' || p[0] == '^'))
+ ;
+ return p;
+}
+
+/*
+ * We want to be able to compare mmapped reference records quickly,
+ * without totally parsing them. We can do so because the records are
+ * LF-terminated, and the refname should start exactly (GIT_SHA1_HEXSZ
+ * + 1) bytes past the beginning of the record.
+ *
+ * But what if the `packed-refs` file contains garbage? We're willing
+ * to tolerate not detecting the problem, as long as we don't produce
+ * totally garbled output (we can't afford to check the integrity of
+ * the whole file during every Git invocation). But we do want to be
+ * sure that we never read past the end of the buffer in memory and
+ * perform an illegal memory access.
+ *
+ * Guarantee that minimum level of safety by verifying that the last
+ * record in the file is LF-terminated, and that it has at least
+ * (GIT_SHA1_HEXSZ + 1) characters before the LF. Die if either of
+ * these checks fails.
+ */
+void verify_buffer_safe_v1(struct snapshot *snapshot)
+{
+ const char *start = snapshot->start;
+ const char *eof = snapshot->eof;
+ const char *last_line;
+
+ if (start == eof)
+ return;
+
+ last_line = find_start_of_record(start, eof - 1);
+ if (*(eof - 1) != '\n' || eof - last_line < the_hash_algo->hexsz + 2)
+ die_invalid_line(snapshot->refs->path,
+ last_line, eof - last_line);
+}
+
+/*
+ * Find the place in `snapshot->buf` where the start of the record for
+ * `refname` starts. If `mustexist` is true and the reference doesn't
+ * exist, then return NULL. If `mustexist` is false and the reference
+ * doesn't exist, then return the point where that reference would be
+ * inserted, or `snapshot->eof` (which might be NULL) if it would be
+ * inserted at the end of the file. In the latter mode, `refname`
+ * doesn't have to be a proper reference name; for example, one could
+ * search for "refs/replace/" to find the start of any replace
+ * references.
+ *
+ * The record is sought using a binary search, so `snapshot->buf` must
+ * be sorted.
+ */
+const char *find_reference_location_v1(struct snapshot *snapshot,
+ const char *refname, int mustexist)
+{
+ /*
+ * This is not *quite* a garden-variety binary search, because
+ * the data we're searching is made up of records, and we
+ * always need to find the beginning of a record to do a
+ * comparison. A "record" here is one line for the reference
+ * itself and zero or one peel lines that start with '^'. Our
+ * loop invariant is described in the next two comments.
+ */
+
+ /*
+ * A pointer to the character at the start of a record whose
+ * preceding records all have reference names that come
+ * *before* `refname`.
+ */
+ const char *lo = snapshot->start;
+
+ /*
+ * A pointer to a the first character of a record whose
+ * reference name comes *after* `refname`.
+ */
+ const char *hi = snapshot->eof;
+
+ while (lo != hi) {
+ const char *mid, *rec;
+ int cmp;
+
+ mid = lo + (hi - lo) / 2;
+ rec = find_start_of_record(lo, mid);
+ cmp = cmp_record_to_refname(rec, refname);
+ if (cmp < 0) {
+ lo = find_end_of_record(mid, hi);
+ } else if (cmp > 0) {
+ hi = rec;
+ } else {
+ return rec;
+ }
+ }
+
+ if (mustexist)
+ return NULL;
+ else
+ return lo;
+}
+
+int parse_packed_format_v1_header(struct packed_ref_store *refs,
+ struct snapshot *snapshot,
+ int *sorted)
+{
+ *sorted = 0;
+ /* If the file has a header line, process it: */
+ if (snapshot->buf < snapshot->eof && *snapshot->buf == '#') {
+ char *tmp, *p, *eol;
+ struct string_list traits = STRING_LIST_INIT_NODUP;
+
+ eol = memchr(snapshot->buf, '\n',
+ snapshot->eof - snapshot->buf);
+ if (!eol)
+ die_unterminated_line(refs->path,
+ snapshot->buf,
+ snapshot->eof - snapshot->buf);
+
+ tmp = xmemdupz(snapshot->buf, eol - snapshot->buf);
+
+ if (!skip_prefix(tmp, "# pack-refs with:", (const char **)&p))
+ die_invalid_line(refs->path,
+ snapshot->buf,
+ snapshot->eof - snapshot->buf);
+
+ string_list_split_in_place(&traits, p, ' ', -1);
+
+ if (unsorted_string_list_has_string(&traits, "fully-peeled"))
+ snapshot->peeled = PEELED_FULLY;
+ else if (unsorted_string_list_has_string(&traits, "peeled"))
+ snapshot->peeled = PEELED_TAGS;
+
+ *sorted = unsorted_string_list_has_string(&traits, "sorted");
+
+ /* perhaps other traits later as well */
+
+ /* The "+ 1" is for the LF character. */
+ snapshot->start = eol + 1;
+
+ string_list_clear(&traits, 0);
+ free(tmp);
+ }
+
+ return 0;
+}
+
+int packed_read_raw_ref_v1(struct packed_ref_store *refs, struct snapshot *snapshot,
+ const char *refname, struct object_id *oid,
+ unsigned int *type, int *failure_errno)
+{
+ const char *rec;
+
+ *type = 0;
+
+ rec = find_reference_location_v1(snapshot, refname, 1);
+
+ if (!rec) {
+ /* refname is not a packed reference. */
+ *failure_errno = ENOENT;
+ return -1;
+ }
+
+ if (get_oid_hex(rec, oid))
+ die_invalid_line(refs->path, rec, snapshot->eof - rec);
+
+ *type = REF_ISPACKED;
+ return 0;
+}
+
+int next_record_v1(struct packed_ref_iterator *iter)
+{
+ const char *p = iter->pos, *eol;
+
+ strbuf_reset(&iter->refname_buf);
+
+ if (iter->pos == iter->eof)
+ return ITER_DONE;
+
+ iter->base.flags = REF_ISPACKED;
+
+ if (iter->eof - p < the_hash_algo->hexsz + 2 ||
+ parse_oid_hex(p, &iter->oid, &p) ||
+ !isspace(*p++))
+ die_invalid_line(iter->snapshot->refs->path,
+ iter->pos, iter->eof - iter->pos);
+
+ eol = memchr(p, '\n', iter->eof - p);
+ if (!eol)
+ die_unterminated_line(iter->snapshot->refs->path,
+ iter->pos, iter->eof - iter->pos);
+
+ strbuf_add(&iter->refname_buf, p, eol - p);
+ iter->base.refname = iter->refname_buf.buf;
+
+ if (check_refname_format(iter->base.refname, REFNAME_ALLOW_ONELEVEL)) {
+ if (!refname_is_safe(iter->base.refname))
+ die("packed refname is dangerous: %s",
+ iter->base.refname);
+ oidclr(&iter->oid);
+ iter->base.flags |= REF_BAD_NAME | REF_ISBROKEN;
+ }
+ if (iter->snapshot->peeled == PEELED_FULLY ||
+ (iter->snapshot->peeled == PEELED_TAGS &&
+ starts_with(iter->base.refname, "refs/tags/")))
+ iter->base.flags |= REF_KNOWS_PEELED;
+
+ iter->pos = eol + 1;
+
+ if (iter->pos < iter->eof && *iter->pos == '^') {
+ p = iter->pos + 1;
+ if (iter->eof - p < the_hash_algo->hexsz + 1 ||
+ parse_oid_hex(p, &iter->peeled, &p) ||
+ *p++ != '\n')
+ die_invalid_line(iter->snapshot->refs->path,
+ iter->pos, iter->eof - iter->pos);
+ iter->pos = p;
+
+ /*
+ * Regardless of what the file header said, we
+ * definitely know the value of *this* reference. But
+ * we suppress it if the reference is broken:
+ */
+ if ((iter->base.flags & REF_ISBROKEN)) {
+ oidclr(&iter->peeled);
+ iter->base.flags &= ~REF_KNOWS_PEELED;
+ } else {
+ iter->base.flags |= REF_KNOWS_PEELED;
+ }
+ } else {
+ oidclr(&iter->peeled);
+ }
+
+ return ITER_OK;
+}
+
+/*
+ * The packed-refs header line that we write out. Perhaps other traits
+ * will be added later.
+ *
+ * Note that earlier versions of Git used to parse these traits by
+ * looking for " trait " in the line. For this reason, the space after
+ * the colon and the trailing space are required.
+ */
+static const char PACKED_REFS_HEADER[] =
+ "# pack-refs with: peeled fully-peeled sorted \n";
+
+int write_packed_file_header_v1(FILE *out)
+{
+ return fprintf(out, "%s", PACKED_REFS_HEADER);
+}
+
+/*
+ * Write an entry to the packed-refs file for the specified refname.
+ * If peeled is non-NULL, write it as the entry's peeled value. On
+ * error, return a nonzero value and leave errno set at the value left
+ * by the failing call to `fprintf()`.
+ */
+int write_packed_entry_v1(const char *refname,
+ const struct object_id *oid,
+ const struct object_id *peeled,
+ void *write_data)
+{
+ FILE *fh = write_data;
+
+ if (fprintf(fh, "%s %s\n", oid_to_hex(oid), refname) < 0 ||
+ (peeled && fprintf(fh, "^%s\n", oid_to_hex(peeled)) < 0))
+ return -1;
+
+ return 0;
+}
diff --git a/refs/packed-format-v2.c b/refs/packed-format-v2.c
new file mode 100644
index 0000000000..ada34bf9bf
--- /dev/null
+++ b/refs/packed-format-v2.c
@@ -0,0 +1,624 @@
+#include "../cache.h"
+#include "../config.h"
+#include "../refs.h"
+#include "refs-internal.h"
+#include "packed-backend.h"
+#include "../iterator.h"
+#include "../lockfile.h"
+#include "../chdir-notify.h"
+#include "../chunk-format.h"
+#include "../csum-file.h"
+
+#define OFFSET_IS_PEELED (((uint64_t)1) << 63)
+
+#define PACKED_REFS_SIGNATURE 0x50524546 /* "PREF" */
+#define CHREFS_CHUNKID_OFFSETS 0x524F4646 /* "ROFF" */
+#define CHREFS_CHUNKID_REFS 0x52454653 /* "REFS" */
+#define CHREFS_CHUNKID_PREFIX_DATA 0x50465844 /* "PFXD" */
+#define CHREFS_CHUNKID_PREFIX_OFFSETS 0x5046584F /* "PFXO" */
+
+static const char *get_nth_prefix(struct snapshot *snapshot,
+ size_t n, size_t *len)
+{
+ uint64_t offset, next_offset;
+
+ if (n >= snapshot->prefixes_nr)
+ BUG("asking for prefix %"PRIu64" outside of bounds (%"PRIu64")",
+ (uint64_t)n, (uint64_t)snapshot->prefixes_nr);
+
+ if (n)
+ offset = get_be32(snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * (n - 1));
+ else
+ offset = 0;
+
+ if (len) {
+ next_offset = get_be32(snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * n);
+
+ /* Prefix includes null terminator. */
+ *len = next_offset - offset - 1;
+ }
+
+ return snapshot->prefix_chunk + offset;
+}
+
+/*
+ * Find the place in `snapshot->buf` where the start of the record for
+ * `refname` starts. If `mustexist` is true and the reference doesn't
+ * exist, then return NULL. If `mustexist` is false and the reference
+ * doesn't exist, then return the point where that reference would be
+ * inserted, or `snapshot->eof` (which might be NULL) if it would be
+ * inserted at the end of the file. In the latter mode, `refname`
+ * doesn't have to be a proper reference name; for example, one could
+ * search for "refs/replace/" to find the start of any replace
+ * references.
+ *
+ * The record is sought using a binary search, so `snapshot->buf` must
+ * be sorted.
+ */
+static const char *find_prefix_location(struct snapshot *snapshot,
+ const char *refname, size_t *pos)
+{
+ size_t lo = 0, hi = snapshot->prefixes_nr;
+
+ while (lo != hi) {
+ const char *rec;
+ int cmp;
+ size_t len;
+ size_t mid = lo + (hi - lo) / 2;
+
+ rec = get_nth_prefix(snapshot, mid, &len);
+ cmp = strncmp(rec, refname, len);
+ if (cmp < 0) {
+ lo = mid + 1;
+ } else if (cmp > 0) {
+ hi = mid;
+ } else {
+ /* we have a prefix match! */
+ *pos = mid;
+ return rec;
+ }
+ }
+
+ *pos = lo;
+ if (lo < snapshot->prefixes_nr)
+ return get_nth_prefix(snapshot, lo, NULL);
+ else
+ return NULL;
+}
+
+int detect_packed_format_v2_header(struct packed_ref_store *refs,
+ struct snapshot *snapshot)
+{
+ /*
+ * packed-refs v1 might not have a header, so check instead
+ * that the v2 signature is not present.
+ */
+ return get_be32(snapshot->buf) == PACKED_REFS_SIGNATURE;
+}
+
+static const char *get_nth_ref(struct snapshot *snapshot,
+ size_t n)
+{
+ uint64_t offset;
+
+ if (n >= snapshot->nr)
+ BUG("asking for position %"PRIu64" outside of bounds (%"PRIu64")",
+ (uint64_t)n, (uint64_t)snapshot->nr);
+
+ if (n)
+ offset = get_be64(snapshot->offset_chunk + (n-1) * sizeof(uint64_t))
+ & ~OFFSET_IS_PEELED;
+ else
+ offset = 0;
+
+ return snapshot->refs_chunk + offset;
+}
+
+/*
+ * Find the place in `snapshot->buf` where the start of the record for
+ * `refname` starts. If `mustexist` is true and the reference doesn't
+ * exist, then return NULL. If `mustexist` is false and the reference
+ * doesn't exist, then return the point where that reference would be
+ * inserted, or `snapshot->eof` (which might be NULL) if it would be
+ * inserted at the end of the file. In the latter mode, `refname`
+ * doesn't have to be a proper reference name; for example, one could
+ * search for "refs/replace/" to find the start of any replace
+ * references.
+ *
+ * The record is sought using a binary search, so `snapshot->buf` must
+ * be sorted.
+ */
+const char *find_reference_location_v2(struct snapshot *snapshot,
+ const char *refname, int mustexist,
+ size_t *pos)
+{
+ size_t lo = 0, hi = snapshot->nr;
+
+ if (snapshot->prefix_chunk) {
+ size_t prefix_row;
+ const char *prefix;
+ int found = 1;
+
+ prefix = find_prefix_location(snapshot, refname, &prefix_row);
+
+ if (!prefix || !starts_with(refname, prefix)) {
+ if (mustexist)
+ return NULL;
+ found = 0;
+ }
+
+ /* The second 4-byte column of the prefix offsets */
+ if (prefix_row) {
+ /* if prefix_row == 0, then lo = 0, which is already true. */
+ lo = get_be32(snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * (prefix_row - 1) + sizeof(uint32_t));
+ }
+
+ if (!found) {
+ const char *ret;
+ /* Terminate early with this lo position as the insertion point. */
+ if (pos)
+ *pos = lo;
+
+ if (lo >= snapshot->nr)
+ return NULL;
+
+ ret = get_nth_ref(snapshot, lo);
+ return ret;
+ }
+
+ hi = get_be32(snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * prefix_row + sizeof(uint32_t));
+
+ if (prefix)
+ refname += strlen(prefix);
+ }
+
+ while (lo != hi) {
+ const char *rec;
+ int cmp;
+ size_t mid = lo + (hi - lo) / 2;
+
+ rec = get_nth_ref(snapshot, mid);
+ cmp = strcmp(rec, refname);
+ if (cmp < 0) {
+ lo = mid + 1;
+ } else if (cmp > 0) {
+ hi = mid;
+ } else {
+ if (pos)
+ *pos = mid;
+ return rec;
+ }
+ }
+
+ if (mustexist) {
+ return NULL;
+ } else {
+ const char *ret;
+ /*
+ * We are likely doing a prefix match, so use the current
+ * 'lo' position as the indicator.
+ */
+ if (pos)
+ *pos = lo;
+ if (lo >= snapshot->nr)
+ return NULL;
+
+ ret = get_nth_ref(snapshot, lo);
+ return ret;
+ }
+}
+
+int packed_read_raw_ref_v2(struct packed_ref_store *refs, struct snapshot *snapshot,
+ const char *refname, struct object_id *oid,
+ unsigned int *type, int *failure_errno)
+{
+ const char *rec;
+
+ *type = 0;
+
+ rec = find_reference_location_v2(snapshot, refname, 1, NULL);
+
+ if (!rec) {
+ /* refname is not a packed reference. */
+ *failure_errno = ENOENT;
+ return -1;
+ }
+
+ hashcpy(oid->hash, (const unsigned char *)rec + strlen(rec) + 1);
+ oid->algo = hash_algo_by_ptr(the_hash_algo);
+
+ *type = REF_ISPACKED;
+ return 0;
+}
+
+static int packed_refs_read_offsets(const unsigned char *chunk_start,
+ size_t chunk_size, void *data)
+{
+ struct snapshot *snapshot = data;
+
+ snapshot->offset_chunk = chunk_start;
+ snapshot->nr = chunk_size / sizeof(uint64_t);
+ return 0;
+}
+
+static int packed_refs_read_prefix_offsets(const unsigned char *chunk_start,
+ size_t chunk_size, void *data)
+{
+ struct snapshot *snapshot = data;
+
+ snapshot->prefix_offsets_chunk = chunk_start;
+ snapshot->prefixes_nr = chunk_size / sizeof(uint64_t);
+ return 0;
+}
+
+void fill_snapshot_v2(struct snapshot *snapshot)
+{
+ uint32_t file_signature, file_version, hash_version;
+ struct chunkfile *cf;
+
+ file_signature = get_be32(snapshot->buf);
+ if (file_signature != PACKED_REFS_SIGNATURE)
+ die(_("%s file signature %X does not match signature %X"),
+ "packed-ref", file_signature, PACKED_REFS_SIGNATURE);
+
+ file_version = get_be32(snapshot->buf + sizeof(uint32_t));
+ if (file_version != 2)
+ die(_("format version %u does not match expected file version %u"),
+ file_version, 2);
+
+ hash_version = get_be32(snapshot->buf + 2 * sizeof(uint32_t));
+ if (hash_version != the_hash_algo->format_id)
+ die(_("hash version %X does not match expected hash version %X"),
+ hash_version, the_hash_algo->format_id);
+
+ cf = init_chunkfile(NULL);
+
+ if (read_trailing_table_of_contents(cf, (const unsigned char *)snapshot->buf, snapshot->buflen)) {
+ release_snapshot(snapshot);
+ snapshot = NULL;
+ goto cleanup;
+ }
+
+ read_chunk(cf, CHREFS_CHUNKID_OFFSETS, packed_refs_read_offsets, snapshot);
+ pair_chunk(cf, CHREFS_CHUNKID_REFS, (const unsigned char**)&snapshot->refs_chunk);
+
+ read_chunk(cf, CHREFS_CHUNKID_PREFIX_OFFSETS, packed_refs_read_prefix_offsets, snapshot);
+ pair_chunk(cf, CHREFS_CHUNKID_PREFIX_DATA, (const unsigned char**)&snapshot->prefix_chunk);
+
+ /* TODO: add error checks for invalid chunk combinations. */
+
+cleanup:
+ free_chunkfile(cf);
+}
+
+/*
+ * Move the iterator to the next record in the snapshot, without
+ * respect for whether the record is actually required by the current
+ * iteration. Adjust the fields in `iter` and return `ITER_OK` or
+ * `ITER_DONE`. This function does not free the iterator in the case
+ * of `ITER_DONE`.
+ */
+int next_record_v2(struct packed_ref_iterator *iter)
+{
+ uint64_t offset;
+ const char *pos = iter->pos;
+ strbuf_reset(&iter->refname_buf);
+
+ if (iter->row == iter->snapshot->nr)
+ return ITER_DONE;
+
+ iter->base.flags = REF_ISPACKED;
+
+ if (iter->cur_prefix)
+ strbuf_addstr(&iter->refname_buf, iter->cur_prefix);
+ strbuf_addstr(&iter->refname_buf, pos);
+ iter->base.refname = iter->refname_buf.buf;
+ pos += strlen(pos) + 1;
+
+ hashcpy(iter->oid.hash, (const unsigned char *)pos);
+ iter->oid.algo = hash_algo_by_ptr(the_hash_algo);
+ pos += the_hash_algo->rawsz;
+
+ if (check_refname_format(iter->base.refname, REFNAME_ALLOW_ONELEVEL)) {
+ if (!refname_is_safe(iter->base.refname))
+ die("packed refname is dangerous: %s",
+ iter->base.refname);
+ oidclr(&iter->oid);
+ iter->base.flags |= REF_BAD_NAME | REF_ISBROKEN;
+ }
+
+ /* We always know the peeled value! */
+ iter->base.flags |= REF_KNOWS_PEELED;
+
+ offset = get_be64(iter->snapshot->offset_chunk + sizeof(uint64_t) * iter->row);
+ if (offset & OFFSET_IS_PEELED) {
+ hashcpy(iter->peeled.hash, (const unsigned char *)pos);
+ iter->peeled.algo = hash_algo_by_ptr(the_hash_algo);
+ } else {
+ oidclr(&iter->peeled);
+ }
+
+ /* TODO: somehow all tags are getting OFFSET_IS_PEELED even though
+ * some are not annotated tags.
+ */
+ iter->pos = iter->snapshot->refs_chunk + (offset & (~OFFSET_IS_PEELED));
+
+ iter->row++;
+
+ if (iter->row == iter->prefix_row_end && iter->snapshot->prefix_chunk) {
+ size_t prefix_pos = get_be32(iter->snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * iter->prefix_i);
+ iter->cur_prefix = iter->snapshot->prefix_chunk + prefix_pos;
+ iter->prefix_i++;
+ iter->prefix_row_end = get_be32(iter->snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * iter->prefix_i + sizeof(uint32_t));
+ }
+
+ return ITER_OK;
+}
+
+void init_iterator_prefix_info(const char *prefix,
+ struct packed_ref_iterator *iter)
+{
+ struct snapshot *snapshot = iter->snapshot;
+
+ if (snapshot->version != 2 || !snapshot->prefix_chunk) {
+ iter->prefix_row_end = snapshot->nr;
+ return;
+ }
+
+ if (prefix)
+ iter->cur_prefix = find_prefix_location(snapshot, prefix, &iter->prefix_i);
+ else {
+ iter->cur_prefix = snapshot->prefix_chunk;
+ iter->prefix_i = 0;
+ }
+
+ iter->prefix_row_end = get_be32(snapshot->prefix_offsets_chunk +
+ 2 * sizeof(uint32_t) * iter->prefix_i +
+ sizeof(uint32_t));
+}
+
+struct write_packed_refs_v2_context {
+ struct packed_ref_store *refs;
+ struct string_list *updates;
+ struct strbuf *err;
+
+ struct hashfile *f;
+ struct chunkfile *cf;
+
+ /*
+ * As we stream the ref names to the refs chunk, store these
+ * values in-memory. These arrays are populated one for every ref.
+ */
+ uint64_t *offsets;
+ size_t nr;
+ size_t offsets_alloc;
+
+ int write_prefixes;
+ const char *cur_prefix;
+ size_t cur_prefix_len;
+
+ char **prefixes;
+ uint32_t *prefix_offsets;
+ uint32_t *prefix_rows;
+ size_t prefix_nr;
+ size_t prefixes_alloc;
+ size_t prefix_offsets_alloc;
+ size_t prefix_rows_alloc;
+};
+
+struct write_packed_refs_v2_context *create_v2_context(struct packed_ref_store *refs,
+ struct string_list *updates,
+ struct strbuf *err)
+{
+ struct write_packed_refs_v2_context *ctx;
+ int do_skip_hash;
+ CALLOC_ARRAY(ctx, 1);
+
+ ctx->refs = refs;
+ ctx->updates = updates;
+ ctx->err = err;
+
+ if (!fdopen_tempfile(refs->tempfile, "w")) {
+ strbuf_addf(err, "unable to fdopen packed-refs tempfile: %s",
+ strerror(errno));
+ return ctx;
+ }
+
+ ctx->f = hashfd(refs->tempfile->fd, refs->tempfile->filename.buf);
+
+ /* Default to true, so skip_hash if not set. */
+ if (git_config_get_maybe_bool("refs.hashpackedrefs", &do_skip_hash) ||
+ do_skip_hash)
+ ctx->f->skip_hash = 1;
+
+ ctx->cf = init_chunkfile(ctx->f);
+
+ return ctx;
+}
+
+static int write_packed_entry_v2(const char *refname,
+ const struct object_id *oid,
+ const struct object_id *peeled,
+ void *write_data)
+{
+ struct write_packed_refs_v2_context *ctx = write_data;
+ size_t reflen = strlen(refname) + 1;
+ size_t i = ctx->nr;
+
+ ALLOC_GROW(ctx->offsets, i + 1, ctx->offsets_alloc);
+
+ if (ctx->write_prefixes) {
+ if (ctx->cur_prefix && starts_with(refname, ctx->cur_prefix)) {
+ /* skip ahead! */
+ refname += ctx->cur_prefix_len;
+ reflen -= ctx->cur_prefix_len;
+ } else {
+ size_t len;
+ const char *slash, *slashslash = NULL;
+ if (ctx->prefix_nr) {
+ /* close out the old prefix. */
+ ctx->prefix_rows[ctx->prefix_nr - 1] = ctx->nr;
+ }
+
+ /* Find the new prefix. */
+ slash = strchr(refname, '/');
+ if (slash)
+ slashslash = strchr(slash + 1, '/');
+ /* If there are two slashes, use that. */
+ slash = slashslash ? slashslash : slash;
+ /*
+ * If there is at least one slash, use that,
+ * and include the slash in the string.
+ * Otherwise, use the end of the ref.
+ */
+ slash = slash ? slash + 1 : refname + strlen(refname);
+
+ len = slash - refname;
+ ALLOC_GROW(ctx->prefixes, ctx->prefix_nr + 1, ctx->prefixes_alloc);
+ ALLOC_GROW(ctx->prefix_offsets, ctx->prefix_nr + 1, ctx->prefix_offsets_alloc);
+ ALLOC_GROW(ctx->prefix_rows, ctx->prefix_nr + 1, ctx->prefix_rows_alloc);
+
+ if (ctx->prefix_nr)
+ ctx->prefix_offsets[ctx->prefix_nr] = ctx->prefix_offsets[ctx->prefix_nr - 1] + len + 1;
+ else
+ ctx->prefix_offsets[ctx->prefix_nr] = len + 1;
+
+ ctx->prefixes[ctx->prefix_nr] = xstrndup(refname, len);
+ ctx->cur_prefix = ctx->prefixes[ctx->prefix_nr];
+ ctx->prefix_nr++;
+
+ refname += len;
+ reflen -= len;
+ ctx->cur_prefix_len = len;
+ }
+
+ /* Update the last row continually. */
+ ctx->prefix_rows[ctx->prefix_nr - 1] = i + 1;
+ }
+
+
+ /* Write entire ref, including null terminator. */
+ hashwrite(ctx->f, refname, reflen);
+ hashwrite(ctx->f, oid->hash, the_hash_algo->rawsz);
+ if (peeled)
+ hashwrite(ctx->f, peeled->hash, the_hash_algo->rawsz);
+
+ if (i)
+ ctx->offsets[i] = (ctx->offsets[i - 1] & (~OFFSET_IS_PEELED));
+ else
+ ctx->offsets[i] = 0;
+ ctx->offsets[i] += reflen + the_hash_algo->rawsz;
+
+ if (peeled) {
+ ctx->offsets[i] += the_hash_algo->rawsz;
+ ctx->offsets[i] |= OFFSET_IS_PEELED;
+ }
+
+ ctx->nr++;
+ return 0;
+}
+
+static int write_refs_chunk_refs(struct hashfile *f,
+ void *data)
+{
+ struct write_packed_refs_v2_context *ctx = data;
+ int ok;
+
+ trace2_region_enter("refs", "refs-chunk", the_repository);
+ ok = merge_iterator_and_updates(ctx->refs, ctx->updates, ctx->err,
+ write_packed_entry_v2, ctx);
+ trace2_region_leave("refs", "refs-chunk", the_repository);
+
+ return ok != ITER_DONE;
+}
+
+static int write_refs_chunk_offsets(struct hashfile *f,
+ void *data)
+{
+ struct write_packed_refs_v2_context *ctx = data;
+ size_t i;
+
+ trace2_region_enter("refs", "offsets", the_repository);
+ for (i = 0; i < ctx->nr; i++)
+ hashwrite_be64(f, ctx->offsets[i]);
+
+ trace2_region_leave("refs", "offsets", the_repository);
+ return 0;
+}
+
+static int write_refs_chunk_prefix_data(struct hashfile *f,
+ void *data)
+{
+ struct write_packed_refs_v2_context *ctx = data;
+ size_t i;
+
+ trace2_region_enter("refs", "prefix-data", the_repository);
+ for (i = 0; i < ctx->prefix_nr; i++) {
+ size_t len = strlen(ctx->prefixes[i]) + 1;
+ hashwrite(f, ctx->prefixes[i], len);
+
+ /* TODO: assert the prefix lengths match the stored offsets? */
+ }
+
+ trace2_region_leave("refs", "prefix-data", the_repository);
+ return 0;
+}
+
+static int write_refs_chunk_prefix_offsets(struct hashfile *f,
+ void *data)
+{
+ struct write_packed_refs_v2_context *ctx = data;
+ size_t i;
+
+ trace2_region_enter("refs", "prefix-offsets", the_repository);
+ for (i = 0; i < ctx->prefix_nr; i++) {
+ hashwrite_be32(f, ctx->prefix_offsets[i]);
+ hashwrite_be32(f, ctx->prefix_rows[i]);
+ }
+
+ trace2_region_leave("refs", "prefix-offsets", the_repository);
+ return 0;
+}
+
+int write_packed_refs_v2(struct write_packed_refs_v2_context *ctx)
+{
+ unsigned char file_hash[GIT_MAX_RAWSZ];
+
+ ctx->write_prefixes = git_env_bool("GIT_TEST_WRITE_PACKED_REFS_PREFIXES", 1);
+
+ add_chunk(ctx->cf, CHREFS_CHUNKID_REFS, 0, write_refs_chunk_refs);
+ add_chunk(ctx->cf, CHREFS_CHUNKID_OFFSETS, 0, write_refs_chunk_offsets);
+
+ if (ctx->write_prefixes) {
+ add_chunk(ctx->cf, CHREFS_CHUNKID_PREFIX_DATA, 0, write_refs_chunk_prefix_data);
+ add_chunk(ctx->cf, CHREFS_CHUNKID_PREFIX_OFFSETS, 0, write_refs_chunk_prefix_offsets);
+ }
+
+ hashwrite_be32(ctx->f, PACKED_REFS_SIGNATURE);
+ hashwrite_be32(ctx->f, 2);
+ hashwrite_be32(ctx->f, the_hash_algo->format_id);
+
+ if (write_chunkfile(ctx->cf, CHUNKFILE_TRAILING_TOC, ctx))
+ goto failure;
+
+ finalize_hashfile(ctx->f, file_hash, FSYNC_COMPONENT_REFERENCE,
+ CSUM_HASH_IN_STREAM | CSUM_FSYNC);
+
+ return 0;
+
+failure:
+ return -1;
+}
+
+void free_v2_context(struct write_packed_refs_v2_context *ctx)
+{
+ if (ctx->cf)
+ free_chunkfile(ctx->cf);
+ free(ctx);
+}
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 69f93b0e2a..39b93fce97 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -521,6 +521,15 @@ struct ref_store;
REF_STORE_ODB | \
REF_STORE_MAIN)
+#define REF_STORE_FORMAT_FILES (1 << 8) /* can use loose ref files */
+#define REF_STORE_FORMAT_PACKED (1 << 9) /* can use v1 packed-refs file */
+#define REF_STORE_FORMAT_PACKED_V2 (1 << 10) /* can use v2 packed-refs file */
+
+static inline int packed_refs_enabled(int flags)
+{
+ return flags & (REF_STORE_FORMAT_PACKED | REF_STORE_FORMAT_PACKED_V2);
+}
+
/*
* Initialize the ref_store for the specified gitdir. These functions
* should call base_ref_store_init() to initialize the shared part of
diff --git a/repository.c b/repository.c
index 5d166b692c..96533fc76b 100644
--- a/repository.c
+++ b/repository.c
@@ -182,6 +182,8 @@ int repo_init(struct repository *repo,
repo->repository_format_partial_clone = format.partial_clone;
format.partial_clone = NULL;
+ repo->ref_format = format.ref_format;
+
if (worktree)
repo_set_worktree(repo, worktree);
diff --git a/repository.h b/repository.h
index 6c461c5b9d..7ce4b8a962 100644
--- a/repository.h
+++ b/repository.h
@@ -62,6 +62,12 @@ struct repo_path_cache {
char *shallow;
};
+enum ref_format_flags {
+ REF_FORMAT_FILES = (1 << 0),
+ REF_FORMAT_PACKED = (1 << 1),
+ REF_FORMAT_PACKED_V2 = (1 << 2),
+};
+
struct repository {
/* Environment */
/*
@@ -96,6 +102,7 @@ struct repository {
* the ref object.
*/
struct ref_store *refs_private;
+ enum ref_format_flags ref_format;
/*
* Contains path to often used file names.
diff --git a/setup.c b/setup.c
index cefd5f63c4..a4525732fe 100644
--- a/setup.c
+++ b/setup.c
@@ -577,6 +577,18 @@ static enum extension_result handle_extension(const char *var,
"extensions.objectformat", value);
data->hash_algo = format;
return EXTENSION_OK;
+ } else if (!strcmp(ext, "refformat")) {
+ if (!strcmp(value, "files"))
+ data->ref_format |= REF_FORMAT_FILES;
+ else if (!strcmp(value, "packed"))
+ data->ref_format |= REF_FORMAT_PACKED;
+ else if (!strcmp(value, "packed-v2"))
+ data->ref_format |= REF_FORMAT_PACKED_V2;
+ else
+ return error(_("invalid value for '%s': '%s'"),
+ "extensions.refFormat", value);
+ data->ref_format_count++;
+ return EXTENSION_OK;
}
return EXTENSION_UNKNOWN;
}
@@ -718,6 +730,14 @@ int read_repository_format(struct repository_format *format, const char *path)
git_config_from_file(check_repo_format, path, format);
if (format->version == -1)
clear_repository_format(format);
+
+ /* Set default ref_format if no extensions.refFormat exists. */
+ if (!format->ref_format_count) {
+ format->ref_format = REF_FORMAT_FILES | REF_FORMAT_PACKED;
+ if (git_env_ulong("GIT_TEST_PACKED_REFS_VERSION", 0) == 2)
+ format->ref_format |= REF_FORMAT_PACKED_V2;
+ }
+
return format->version;
}
@@ -1420,6 +1440,9 @@ int discover_git_directory(struct strbuf *commondir,
candidate.partial_clone;
candidate.partial_clone = NULL;
+ /* take ownership of candidate.ref_format */
+ the_repository->ref_format = candidate.ref_format;
+
clear_repository_format(&candidate);
return 0;
}
@@ -1556,6 +1579,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
the_repository->repository_format_partial_clone =
repo_fmt.partial_clone;
repo_fmt.partial_clone = NULL;
+
+ the_repository->ref_format = repo_fmt.ref_format;
}
}
/*
@@ -1645,6 +1670,7 @@ void check_repository_format(struct repository_format *fmt)
repo_set_hash_algo(the_repository, fmt->hash_algo);
the_repository->repository_format_partial_clone =
xstrdup_or_null(fmt->partial_clone);
+ the_repository->ref_format = fmt->ref_format;
clear_repository_format(&repo_fmt);
}
diff --git a/t/perf/p1401-ref-operations.sh b/t/perf/p1401-ref-operations.sh
new file mode 100755
index 0000000000..0b88a2f531
--- /dev/null
+++ b/t/perf/p1401-ref-operations.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+test_description="Tests performance of ref operations"
+
+. ./perf-lib.sh
+
+test_perf_large_repo
+
+test_perf 'git pack-refs (v1)' '
+ git commit --allow-empty -m "change one ref" &&
+ git pack-refs --all
+'
+
+test_perf 'git for-each-ref (v1)' '
+ git for-each-ref --format="%(refname)" >/dev/null
+'
+
+test_perf 'git for-each-ref prefix (v1)' '
+ git for-each-ref --format="%(refname)" refs/tags/ >/dev/null
+'
+
+test_expect_success 'configure packed-refs v2' '
+ git config core.repositoryFormatVersion 1 &&
+ git config --add extensions.refFormat files &&
+ git config --add extensions.refFormat packed &&
+ git config --add extensions.refFormat packed-v2 &&
+ git config refs.packedRefsVersion 2 &&
+ git commit --allow-empty -m "change one ref" &&
+ git pack-refs --all &&
+ test_copy_bytes 16 .git/packed-refs | xxd >actual &&
+ grep PREF actual
+'
+
+test_perf 'git pack-refs (v2)' '
+ git commit --allow-empty -m "change one ref" &&
+ git pack-refs --all
+'
+
+test_perf 'git pack-refs (v2;hashing)' '
+ git commit --allow-empty -m "change one ref" &&
+ git -c refs.hashPackedRefs=true pack-refs --all
+'
+
+test_perf 'git for-each-ref (v2)' '
+ git for-each-ref --format="%(refname)" >/dev/null
+'
+
+test_perf 'git for-each-ref prefix (v2)' '
+ git for-each-ref --format="%(refname)" refs/tags/ >/dev/null
+'
+
+test_done
diff --git a/t/t1409-avoid-packing-refs.sh b/t/t1409-avoid-packing-refs.sh
index f23c0152a8..74dedd57e9 100755
--- a/t/t1409-avoid-packing-refs.sh
+++ b/t/t1409-avoid-packing-refs.sh
@@ -9,13 +9,29 @@ TEST_PASSES_SANITIZE_LEAK=true
# shouldn't upset readers, and it should be omitted if the file is
# ever rewritten.
mark_packed_refs () {
- sed -e "s/^\(#.*\)/\1 t1409 /" .git/packed-refs >.git/packed-refs.new &&
- mv .git/packed-refs.new .git/packed-refs
+ if test "$GIT_TEST_PACKED_REFS_VERSION" = "2"
+ then
+ size=$(wc -c < .git/packed-refs) &&
+ pos=$(expr $size - 4) &&
+ printf "FAKE" | dd of=".git/packed-refs" bs=1 seek="$pos" conv=notrunc
+ else
+ sed -e "s/^\(#.*\)/\1 t1409 /" .git/packed-refs >.git/packed-refs.new &&
+ mv .git/packed-refs.new .git/packed-refs
+ fi
}
# Verify that the packed-refs file is still marked.
check_packed_refs_marked () {
- grep -q '^#.* t1409 ' .git/packed-refs
+ if test "$GIT_TEST_PACKED_REFS_VERSION" = "2"
+ then
+ size=$(wc -c < .git/packed-refs) &&
+ pos=$(expr $size - 4) &&
+ tail -c 4 .git/packed-refs >actual &&
+ printf "FAKE" >expect &&
+ test_cmp expect actual
+ else
+ grep -q '^#.* t1409 ' .git/packed-refs
+ fi
}
test_expect_success 'setup' '
diff --git a/t/t1600-index.sh b/t/t1600-index.sh
index 010989f90e..24ab90ca04 100755
--- a/t/t1600-index.sh
+++ b/t/t1600-index.sh
@@ -103,4 +103,12 @@ test_expect_success 'index version config precedence' '
test_index_version 0 true 2 2
'
+test_expect_success 'index.computeHash config option' '
+ (
+ rm -f .git/index &&
+ git -c index.computeHash=false add a &&
+ git fsck
+ )
+'
+
test_done
diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh
index 577f32dc71..76251dfe05 100755
--- a/t/t3210-pack-refs.sh
+++ b/t/t3210-pack-refs.sh
@@ -159,7 +159,7 @@ test_expect_success 'delete ref while another dangling packed ref' '
test_expect_success 'pack ref directly below refs/' '
git update-ref refs/top HEAD &&
git pack-refs --all --prune &&
- grep refs/top .git/packed-refs &&
+ git rev-parse refs/top &&
test_path_is_missing .git/refs/top
'
@@ -197,7 +197,7 @@ test_expect_success 'notice d/f conflict with existing ref' '
test_must_fail git branch foo/bar/baz/lots/of/extra/components
'
-test_expect_success 'reject packed-refs with unterminated line' '
+test_expect_success PACKED_REFS_V1 'reject packed-refs with unterminated line' '
cp .git/packed-refs .git/packed-refs.bak &&
test_when_finished "mv .git/packed-refs.bak .git/packed-refs" &&
printf "%s" "$HEAD refs/zzzzz" >>.git/packed-refs &&
@@ -206,7 +206,7 @@ test_expect_success 'reject packed-refs with unterminated line' '
test_cmp expected_err err
'
-test_expect_success 'reject packed-refs containing junk' '
+test_expect_success PACKED_REFS_V1 'reject packed-refs containing junk' '
cp .git/packed-refs .git/packed-refs.bak &&
test_when_finished "mv .git/packed-refs.bak .git/packed-refs" &&
printf "%s\n" "bogus content" >>.git/packed-refs &&
@@ -215,7 +215,7 @@ test_expect_success 'reject packed-refs containing junk' '
test_cmp expected_err err
'
-test_expect_success 'reject packed-refs with a short SHA-1' '
+test_expect_success PACKED_REFS_V1 'reject packed-refs with a short SHA-1' '
cp .git/packed-refs .git/packed-refs.bak &&
test_when_finished "mv .git/packed-refs.bak .git/packed-refs" &&
printf "%.7s %s\n" $HEAD refs/zzzzz >>.git/packed-refs &&
diff --git a/t/t3212-ref-formats.sh b/t/t3212-ref-formats.sh
new file mode 100755
index 0000000000..5583f16db4
--- /dev/null
+++ b/t/t3212-ref-formats.sh
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+test_description='test across ref formats'
+
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
+. ./test-lib.sh
+
+test_expect_success 'extensions.refFormat requires core.repositoryFormatVersion=1' '
+ test_when_finished rm -rf broken &&
+
+ # Force sha1 to ensure GIT_TEST_DEFAULT_HASH does
+ # not imply a value of core.repositoryFormatVersion.
+ git init --object-format=sha1 broken &&
+ git -C broken config extensions.refFormat files &&
+ test_must_fail git -C broken status 2>err &&
+ grep "repo version is 0, but v1-only extension found" err
+'
+
+test_expect_success 'invalid extensions.refFormat' '
+ test_when_finished rm -rf broken &&
+ git init broken &&
+ git -C broken config core.repositoryFormatVersion 1 &&
+ git -C broken config extensions.refFormat bogus &&
+ test_must_fail git -C broken status 2>err &&
+ grep "invalid value for '\''extensions.refFormat'\'': '\''bogus'\''" err
+'
+
+test_expect_success 'extensions.refFormat=packed only' '
+ git init only-packed &&
+ (
+ cd only-packed &&
+ git config core.repositoryFormatVersion 1 &&
+ git config extensions.refFormat packed &&
+ test_commit A &&
+ test_path_exists .git/packed-refs &&
+ test_path_is_missing .git/refs/tags/A
+ )
+'
+
+test_expect_success 'extensions.refFormat=files only' '
+ test_commit T &&
+ git pack-refs --all &&
+ git init only-loose &&
+ (
+ cd only-loose &&
+ git config core.repositoryFormatVersion 1 &&
+ git config extensions.refFormat files &&
+ test_commit A &&
+ test_commit B &&
+ test_must_fail git pack-refs 2>err &&
+ grep "refusing to create" err &&
+ test_path_is_missing .git/packed-refs &&
+
+ # Refuse to parse a packed-refs file.
+ cp ../.git/packed-refs .git/packed-refs &&
+ test_must_fail git rev-parse refs/tags/T
+ )
+'
+
+test_expect_success 'extensions.refFormat=files,packed-v2' '
+ test_commit Q &&
+ git pack-refs --all &&
+ git init no-packed-v1 &&
+ (
+ cd no-packed-v1 &&
+ git config core.repositoryFormatVersion 1 &&
+ git config extensions.refFormat files &&
+ git config --add extensions.refFormat packed-v2 &&
+ test_commit A &&
+ test_commit B &&
+
+ # Refuse to parse a v1 packed-refs file.
+ cp ../.git/packed-refs .git/packed-refs &&
+ test_must_fail git rev-parse refs/tags/Q &&
+ rm -f .git/packed-refs &&
+
+ git for-each-ref --format="%(refname) %(objectname)" >expect-all &&
+ git for-each-ref --format="%(refname) %(objectname)" \
+ refs/tags/* >expect-tags &&
+
+ # Create a v2 packed-refs file
+ git pack-refs --all &&
+ test_path_exists .git/packed-refs &&
+ for t in A B
+ do
+ test_path_is_missing .git/refs/tags/$t &&
+ git rev-parse refs/tags/$t || return 1
+ done &&
+
+ git for-each-ref --format="%(refname) %(objectname)" >actual-all &&
+ test_cmp expect-all actual-all &&
+ git for-each-ref --format="%(refname) %(objectname)" \
+ refs/tags/* >actual-tags &&
+ test_cmp expect-tags actual-tags
+ )
+'
+
+test_done
diff --git a/t/t5502-quickfetch.sh b/t/t5502-quickfetch.sh
index b160f8b7fb..0c4aadebae 100755
--- a/t/t5502-quickfetch.sh
+++ b/t/t5502-quickfetch.sh
@@ -122,7 +122,7 @@ test_expect_success 'quickfetch should not copy from alternate' '
'
-test_expect_success 'quickfetch should handle ~1000 refs (on Windows)' '
+test_expect_success PACKED_REFS_V1 'quickfetch should handle ~1000 refs (on Windows)' '
git gc &&
head=$(git rev-parse HEAD) &&
diff --git a/t/t5539-fetch-http-shallow.sh b/t/t5539-fetch-http-shallow.sh
index 3ea75d34ca..5e3b430436 100755
--- a/t/t5539-fetch-http-shallow.sh
+++ b/t/t5539-fetch-http-shallow.sh
@@ -5,6 +5,13 @@ test_description='fetch/clone from a shallow clone over http'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+# If GIT_TEST_PACKED_REFS_VERSION=2, then the packed-refs file will
+# be written in v2 format without extensions.refFormat=packed-v2. This
+# causes issues for the HTTP server which does not carry over the
+# environment variable to the server process.
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index fbad2d5ff5..495437dd3c 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -7,6 +7,13 @@ test_description='test smart pushing over http via http-backend'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+# If GIT_TEST_PACKED_REFS_VERSION=2, then the packed-refs file will
+# be written in v2 format without extensions.refFormat=packed-v2. This
+# causes issues for the HTTP server which does not carry over the
+# environment variable to the server process.
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
. ./test-lib.sh
ROOT_PATH="$PWD"
diff --git a/t/t5542-push-http-shallow.sh b/t/t5542-push-http-shallow.sh
index c2cc83182f..c47b18b9fa 100755
--- a/t/t5542-push-http-shallow.sh
+++ b/t/t5542-push-http-shallow.sh
@@ -5,6 +5,13 @@ test_description='push from/to a shallow clone over http'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+# If GIT_TEST_PACKED_REFS_VERSION=2, then the packed-refs file will
+# be written in v2 format without extensions.refFormat=packed-v2. This
+# causes issues for the HTTP server which does not carry over the
+# environment variable to the server process.
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index bc0719a4fc..faf6bd44d8 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -5,6 +5,13 @@ test_description="test smart fetching over http via http-backend ($HTTP_PROTO)"
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+# If GIT_TEST_PACKED_REFS_VERSION=2, then the packed-refs file will
+# be written in v2 format without extensions.refFormat=packed-v2. This
+# causes issues for the HTTP server which does not carry over the
+# environment variable to the server process.
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-httpd.sh
test "$HTTP_PROTO" = "HTTP/2" && enable_http2
diff --git a/t/t5558-clone-bundle-uri.sh b/t/t5558-clone-bundle-uri.sh
index 9155f31fa2..3e35322155 100755
--- a/t/t5558-clone-bundle-uri.sh
+++ b/t/t5558-clone-bundle-uri.sh
@@ -2,6 +2,13 @@
test_description='test fetching bundles with --bundle-uri'
+# If GIT_TEST_PACKED_REFS_VERSION=2, then the packed-refs file will
+# be written in v2 format without extensions.refFormat=packed-v2. This
+# causes issues for the HTTP server which does not carry over the
+# environment variable to the server process.
+GIT_TEST_PACKED_REFS_VERSION=0
+export GIT_TEST_PACKED_REFS_VERSION
+
. ./test-lib.sh
test_expect_success 'fail to clone from non-existent file' '
diff --git a/t/test-lib.sh b/t/test-lib.sh
index ce319c9963..090cfb9595 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -1975,3 +1975,7 @@ test_lazy_prereq FSMONITOR_DAEMON '
git version --build-options >output &&
grep "feature: fsmonitor--daemon" output
'
+
+test_lazy_prereq PACKED_REFS_V1 '
+ test "$GIT_TEST_PACKED_REFS_VERSION" -ne "2"
+' \ No newline at end of file