From: Taylor Blau <hidden> Date: 2021-11-29 22:25:55
This series implements "cruft packs", a pack which stores accumulated
unreachable objects, along with a new ".mtimes" file which tracks each
object's last known modification time.
This idea was discussed recently-ish in [1], but the most thorough
discussion I could find is in [2]. The approach settled on in this
series is laid out in detail by the first patch.
For the uninitiated, cruft packs enable repositories to safely run
`git repack -Ad` by storing unreachable objects which have not yet
"aged out" in a separate pack. This prevents repositories from storing
a potentially large number of these such objects as loose.
This series is structured as follows:
- The first patch describes the technical details of cruft packs.
- The next five patches implement reading and writing the new
`.mtimes` format.
- The next six patches implement `git pack-objects --cruft`. The
first five implement this mode when no grace period is specified,
and the six patch adds support for the grace period.
- The next five patches integrate cruft packs with `git repack`,
including the new-ish `--geometric` mode.
- The final patch handles object freshening for objects stored in a
cruft pack.
Thanks in advance for your review.
[1]: https://lore.kernel.org/git/20170610080626.sjujpmgkli4muh7h@sigill.intra.peff.net/
[2]: https://lore.kernel.org/git/E1SdhJ9-0006B1-6p@tytso-glaptop.cam.corp.google.com/
Taylor Blau (17):
Documentation/technical: add cruft-packs.txt
pack-mtimes: support reading .mtimes files
pack-write: pass 'struct packing_data' to 'stage_tmp_packfiles'
chunk-format.h: extract oid_version()
pack-mtimes: support writing pack .mtimes files
t/helper: add 'pack-mtimes' test-tool
builtin/pack-objects.c: return from create_object_entry()
builtin/pack-objects.c: --cruft without expiration
reachable: add options to add_unseen_recent_objects_to_traversal
reachable: report precise timestamps from objects in cruft packs
builtin/pack-objects.c: --cruft with expiration
builtin/repack.c: support generating a cruft pack
builtin/repack.c: allow configuring cruft pack generation
builtin/repack.c: use named flags for existing_packs
builtin/repack.c: add cruft packs to MIDX during geometric repack
builtin/gc.c: conditionally avoid pruning objects via loose
sha1-file.c: don't freshen cruft packs
Documentation/Makefile | 1 +
Documentation/config/gc.txt | 21 +-
Documentation/config/repack.txt | 9 +
Documentation/git-gc.txt | 5 +
Documentation/git-pack-objects.txt | 23 +
Documentation/git-repack.txt | 11 +
Documentation/technical/cruft-packs.txt | 95 ++++
Documentation/technical/pack-format.txt | 22 +
Makefile | 2 +
builtin/gc.c | 10 +-
builtin/pack-objects.c | 306 ++++++++++-
builtin/repack.c | 189 ++++++-
bulk-checkin.c | 2 +-
chunk-format.c | 12 +
chunk-format.h | 3 +
commit-graph.c | 18 +-
midx.c | 18 +-
object-file.c | 4 +-
object-store.h | 7 +-
pack-mtimes.c | 139 +++++
pack-mtimes.h | 16 +
pack-objects.c | 6 +
pack-objects.h | 20 +
pack-write.c | 90 +++-
pack.h | 4 +
packfile.c | 18 +-
packfile.h | 1 +
reachable.c | 58 +-
reachable.h | 9 +-
t/helper/test-pack-mtimes.c | 53 ++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t5327-pack-objects-cruft.sh | 685 ++++++++++++++++++++++++
33 files changed, 1757 insertions(+), 102 deletions(-)
create mode 100644 Documentation/technical/cruft-packs.txt
create mode 100644 pack-mtimes.c
create mode 100644 pack-mtimes.h
create mode 100644 t/helper/test-pack-mtimes.c
create mode 100755 t/t5327-pack-objects-cruft.sh
--
2.34.1.25.gb3157a20e6
From: Taylor Blau <hidden> Date: 2021-11-29 22:25:58
Create a technical document to explain cruft packs. It contains a brief
overview of the problem, some background, details on the implementation,
and a couple of alternative approaches not considered here.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/Makefile | 1 +
Documentation/technical/cruft-packs.txt | 95 +++++++++++++++++++++++++
2 files changed, 96 insertions(+)
create mode 100644 Documentation/technical/cruft-packs.txt
@@ -0,0 +1,95 @@+= Cruft packs++Cruft packs offer an alternative to Git's traditional mechanism of removing+unreachable objects. This document provides an overview of Git's pruning+mechanism, and how cruft packs can be used instead to accomplish the same.++== Background++To remove unreachable objects from your repository, Git offers `git repack -Ad`+(see linkgit:git-repack[1]). Quoting from the documentation:++[quote]+[...] unreachable objects in a previous pack become loose, unpacked objects,+instead of being left in the old pack. [...] loose unreachable objects will be+pruned according to normal expiry rules with the next 'git gc' invocation.++Unreachable objects aren't removed immediately, since doing so could race with+an incoming push which may reference an object which is about to be deleted.+Instead, those unreachable objects are stored as loose object and stay that way+until they are older than the expiration window, at which point they are removed+by linkgit:git-prune[1].++Git must store these unreachable objects loose in order to keep track of their+per-object mtimes. If these unreachable objects were written into one big pack,+then either freshening that pack (because an object contained within it was+re-written) or creating a new pack of unreachable objects would cause the pack's+mtime to get updated, and the objects within it would never leave the expiration+window. Instead, objects are stored loose in order to keep track of the+individual object mtimes and avoid a situation where all cruft objects are+freshened at once.++This can lead to undesirable situations when a repository contains many+unreachable objects which have not yet left the grace period. Having large+directories in the shards of `.git/objects` can lead to decreased performance in+the repository. But given enough unreachable objects, this can lead to inode+starvation and degrade the performance of the whole system. Since we+can never pack those objects, these repositories often take up a large amount of+disk space, since we can only zlib compress them, but not store them in delta+chains.++== Cruft packs++Cruft packs are designed to eliminate the need for storing unreachable objects+in a loose state by including the per-object mtimes in a separate file alongside+a single pack containing all loose objects.++A cruft pack is written by `git repack --cruft` when generating a new pack.+linkgit:git-pack-objects[1]'s `--cruft` option. Note that `git repack --cruft`+is a classic all-into-one repack, meaning that everything in the resulting pack is+reachable, and everything else is unreachable. Once written, the `--cruft`+option instructs `git repack` to generate another pack containing only objects+not packed in the previous step (which equates to packing all unreachable+objects together). This progresses as follows:++ 1. Enumerate every object, marking any object which is (a) not contained in a+ kept-pack, and (b) whose mtime is within the grace period as a traversal+ tip.++ 2. Perform a reachability traversal based on the tips gathered in the previous+ step, adding every object along the way to the pack.++ 3. Write the pack out, along with a `.mtimes` file that records the per-object+ timestamps.++This mode is invoked internally by linkgit:git-repack[1] when instructed to+write a cruft pack. Crucially, the set of in-core kept packs is exactly the set+of packs which will not be deleted by the repack; in other words, they contain+all of the repository's reachable objects.++When a repository already has a cruft pack, `git repack --cruft` typically only+adds objects to it. An exception to this is when `git repack` is given the+`--cruft-expiration` option, which allows the generated cruft pack to omit+expired objects instead of waiting for linkgit:git-gc[1] to expire those objects+later on.++It is linkgit:git-gc[1] that is typically responsible for removing expired+unreachable objects.++== Alternatives++Notable alternatives to this design include:++ - The location of the per-object mtime data, and+ - Whether cruft packs should be incremental or not.++On the location of mtime data, a new auxiliary file tied to the pack was chosen+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain+support for optional chunks of data, it may make sense to consolidate the+`.mtimes` format into the `.idx` itself.++Incremental cruft packs (i.e., where each time a repository is repacked a new+cruft pack is generated containing only the unreachable objects introduced since+the last time a cruft pack was written) are significantly more complicated to+construct, and so aren't pursued here. The obvious drawback to the current+implementation is that the entire cruft pack must be re-written from scratch.
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:00
To store the individual mtimes of objects in a cruft pack, introduce a
new `.mtimes` format that can optionally accompany a single pack in the
repository.
The format is defined in Documentation/technical/pack-format.txt, and
stores a 4-byte network order timestamp for each object in name (index)
order.
This patch prepares for cruft packs by defining the `.mtimes` format,
and introducing a basic API that callers can use to read out individual
mtimes.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/technical/pack-format.txt | 22 ++++
Makefile | 1 +
builtin/repack.c | 1 +
object-store.h | 5 +-
pack-mtimes.c | 139 ++++++++++++++++++++++++
pack-mtimes.h | 16 +++
packfile.c | 18 ++-
packfile.h | 1 +
8 files changed, 200 insertions(+), 3 deletions(-)
create mode 100644 pack-mtimes.c
create mode 100644 pack-mtimes.h
@@ -294,6 +294,28 @@ Pack file entry: <+ All 4-byte numbers are in network order.+== pack-*.mtimes files have the format:++ - A 4-byte magic number '0x4d544d45' ('MTME').++ - A 4-byte version identifier (= 1).++ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).++ - A table of mtimes (one per packed object, num_objects in total, each+ a 4-byte unsigned integer in network order), in the same order as+ objects appear in the index file (e.g., the first entry in the mtime+ table corresponds to the object with the lowest lexically-sorted+ oid). The mtimes count standard epoch seconds.++ - A trailer, containing a:++ checksum of the corresponding packfile, and++ a checksum of all of the above.++All 4-byte numbers are in network order.+ == multi-pack-index (MIDX) files have the following format: The multi-pack-index files refer to multiple pack-files and loose objects.
@@ -0,0 +1,139 @@+#include"pack-mtimes.h"+#include"object-store.h"+#include"packfile.h"++staticchar*pack_mtimes_filename(structpacked_git*p)+{+size_tlen;+if(!strip_suffix(p->pack_name,".pack",&len))+BUG("pack_name does not end in .pack");+/* NEEDSWORK: this could reuse code from pack-revindex.c. */+returnxstrfmt("%.*s.mtimes",(int)len,p->pack_name);+}++intpack_has_mtimes(structpacked_git*p)+{+structstatst;+char*fname=pack_mtimes_filename(p);++if(stat(fname,&st)<0){+if(errno==ENOENT)+return0;+die_errno(_("could not stat %s"),fname);+}++free(fname);+return1;+}++#define MTIMES_HEADER_SIZE (12)+#define MTIMES_MIN_SIZE (MTIMES_HEADER_SIZE + (2 * the_hash_algo->rawsz))++structmtimes_header{+uint32_tsignature;+uint32_tversion;+uint32_thash_id;+};++staticintload_pack_mtimes_file(char*mtimes_file,+uint32_tnum_objects,+constuint32_t**data_p,size_t*len_p)+{+intfd,ret=0;+structstatst;+void*data=NULL;+size_tmtimes_size;+uint32_t*hdr;++fd=git_open(mtimes_file);++if(fd<0){+ret=-1;+gotocleanup;+}+if(fstat(fd,&st)){+ret=error_errno(_("failed to read %s"),mtimes_file);+gotocleanup;+}++mtimes_size=xsize_t(st.st_size);++if(mtimes_size<MTIMES_MIN_SIZE){+ret=error(_("mtimes file %s is too small"),mtimes_file);+gotocleanup;+}++if(mtimes_size-MTIMES_MIN_SIZE!=st_mult(sizeof(uint32_t),num_objects)){+ret=error(_("mtimes file %s is corrupt"),mtimes_file);+gotocleanup;+}++data=hdr=xmmap(NULL,mtimes_size,PROT_READ,MAP_PRIVATE,fd,0);++if(ntohl(*hdr)!=MTIMES_SIGNATURE){+ret=error(_("mtimes file %s has unknown signature"),mtimes_file);+gotocleanup;+}++if(ntohl(*++hdr)!=1){+ret=error(_("mtimes file %s has unsupported version %"PRIu32),+mtimes_file,ntohl(*hdr));+gotocleanup;+}+hdr++;+if(!(ntohl(*hdr)==1||ntohl(*hdr)==2)){+ret=error(_("mtimes file %s has unsupported hash id %"PRIu32),+mtimes_file,ntohl(*hdr));+gotocleanup;+}++cleanup:+if(ret){+if(data)+munmap(data,mtimes_size);+}else{+*len_p=mtimes_size;+*data_p=(constuint32_t*)data;+}++close(fd);+returnret;+}++intload_pack_mtimes(structpacked_git*p)+{+char*mtimes_name=NULL;+intret=0;++if(!p->is_cruft)+returnret;/* not a cruft pack */+if(p->mtimes_map)+returnret;/* already loaded */++ret=open_pack_index(p);+if(ret<0)+gotocleanup;++mtimes_name=pack_mtimes_filename(p);+ret=load_pack_mtimes_file(mtimes_name,+p->num_objects,+&p->mtimes_map,+&p->mtimes_size);+if(ret)+gotocleanup;++cleanup:+free(mtimes_name);+returnret;+}++uint32_tnth_packed_mtime(structpacked_git*p,uint32_tpos)+{+if(!p->mtimes_map)+BUG("pack .mtimes file not loaded for %s",p->pack_name);+if(p->num_objects<=pos)+BUG("pack .mtimes out-of-bounds (%"PRIu32" vs %"PRIu32")",+pos,p->num_objects);++returnget_be32(p->mtimes_map+pos+3);+}
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:01
There are three definitions of an identical function which converts
`the_hash_algo` into either 1 (for SHA-1) or 2 (for SHA-256). There is a
copy of this function for writing both the commit-graph and
multi-pack-index file, and another inline definition used to write the
.rev header.
Consolidate these into a single definition in chunk-format.h. It's not
clear that this is the best header to define this function in, but it
should do for now.
(Worth noting, the .rev caller expects a 4-byte unsigned, but the other
two callers work with a single unsigned byte. The consolidated version
uses the latter type, and lets the compiler widen it when required).
Another caller will be added in a subsequent patch.
Signed-off-by: Taylor Blau <redacted>
---
chunk-format.c | 12 ++++++++++++
chunk-format.h | 3 +++
commit-graph.c | 18 +++---------------
midx.c | 18 +++---------------
pack-write.c | 15 ++-------------
5 files changed, 23 insertions(+), 43 deletions(-)
@@ -365,9 +353,9 @@ struct commit_graph *parse_commit_graph(struct repository *r,}hash_version=*(unsignedchar*)(data+5);-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("commit-graph hash version %X does not match version %X"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));returnNULL;}
@@ -1908,7 +1896,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)hashwrite_be32(f,GRAPH_SIGNATURE);hashwrite_u8(f,GRAPH_VERSION);-hashwrite_u8(f,oid_version());+hashwrite_u8(f,oid_version(the_hash_algo));hashwrite_u8(f,get_num_chunks(cf));hashwrite_u8(f,ctx->num_commit_graphs_after-1);
@@ -131,9 +119,9 @@ struct multi_pack_index *load_multi_pack_index(const char *object_dir, int localm->version);hash_version=m->data[MIDX_BYTE_HASH_VERSION];-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("multi-pack-index hash version %u does not match version %u"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));gotocleanup_fail;}m->hash_len=the_hash_algo->rawsz;
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:03
This structure will be used to communicate the per-object mtimes when
writing a cruft pack. Here, we need the full packing_data structure
because the mtime information is stored in an array there, not on the
individual object_entry's themselves (to avoid paying the overhead in
structure width for operations which do not generate a cruft pack).
We haven't passed this information down before because one of the two
callers (in bulk-checkin.c) does not have a packing_data structure at
all. In that case (where no cruft pack will be generated), NULL is
passed instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 3 ++-
bulk-checkin.c | 2 +-
pack-write.c | 1 +
pack.h | 3 +++
4 files changed, 7 insertions(+), 2 deletions(-)
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:04
Now that the `.mtimes` format is defined, supplement the pack-write API
to be able to conditionally write an `.mtimes` file along with a pack by
setting an additional flag and passing an oidmap that contains the
timestamps corresponding to each object in the pack.
Signed-off-by: Taylor Blau <redacted>
---
pack-objects.c | 6 ++++
pack-objects.h | 20 ++++++++++++++
pack-write.c | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++
pack.h | 1 +
4 files changed, 101 insertions(+)
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:11
This function behaves very similarly to what we will need in
pack-objects in order to implement cruft packs with expiration. But it
is lacking a couple of things. Namely, it needs:
- a mechanism to communicate the timestamps of individual recent
objects to some external caller
- and, in the case of packed objects, our future caller will also want
to know the originating pack, as well as the offset within that pack
at which the object can be found
- finally, it needs a way to skip over packs which are marked as kept
in-core.
To address the first two, add a callback interface in this patch which
reports the time of each recent object, as well as a (packed_git,
off_t) pair for packed objects.
Likewise, add a new option to the packed object iterators to skip over
packs which are marked as kept in core. This option will become
implicitly tested in a future patch.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 2 +-
reachable.c | 51 +++++++++++++++++++++++++++++++++++-------
reachable.h | 9 +++++++-
3 files changed, 52 insertions(+), 10 deletions(-)
@@ -126,7 +146,7 @@ static int add_recent_loose(const struct object_id *oid,returnerror_errno("unable to stat %s",oid_to_hex(oid));}-add_recent_object(oid,st.st_mtime,data);+add_recent_object(oid,NULL,0,st.st_mtime,data);return0;}
@@ -134,29 +154,43 @@ static int add_recent_packed(const struct object_id *oid,structpacked_git*p,uint32_tpos,void*data){-structobject*obj=lookup_object(the_repository,oid);+structobject*obj;++if(!want_recent_object(data,oid))+return0;++obj=lookup_object(the_repository,oid);if(obj&&obj->flags&SEEN)return0;-add_recent_object(oid,p->mtime,data);+add_recent_object(oid,p,nth_packed_object_offset(p,pos),p->mtime,data);return0;}intadd_unseen_recent_objects_to_traversal(structrev_info*revs,-timestamp_ttimestamp)+timestamp_ttimestamp,+report_recent_object_fn*cb,+intignore_in_core_kept_packs){structrecent_datadata;+enumfor_each_object_flagsflags;intr;data.revs=revs;data.timestamp=timestamp;+data.cb=cb;+data.ignore_in_core_kept_packs=ignore_in_core_kept_packs;r=for_each_loose_object(add_recent_loose,&data,FOR_EACH_OBJECT_LOCAL_ONLY);if(r)returnr;-returnfor_each_packed_object(add_recent_packed,&data,-FOR_EACH_OBJECT_LOCAL_ONLY);++flags=FOR_EACH_OBJECT_LOCAL_ONLY|FOR_EACH_OBJECT_PACK_ORDER;+if(ignore_in_core_kept_packs)+flags|=FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS;++returnfor_each_packed_object(add_recent_packed,&data,flags);}staticintmark_object_seen(conststructobject_id*oid,
@@ -217,7 +251,8 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,if(mark_recent){revs->ignore_missing_links=1;-if(add_unseen_recent_objects_to_traversal(revs,mark_recent))+if(add_unseen_recent_objects_to_traversal(revs,mark_recent,+NULL,0))die("unable to mark recent objects");if(prepare_revision_walk(revs))die("revision walk setup failed");
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:15
In servers which set the pack.window configuration to a large value, we
can wind up spending quite a lot of time finding new bases when breaking
delta chains between reachable and unreachable objects while generating
a cruft pack.
Introduce a handful of `repack.cruft*` configuration variables to
control the parameters used by pack-objects when generating a cruft
pack.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/config/repack.txt | 9 ++++
builtin/repack.c | 50 ++++++++++++++------
t/t5327-pack-objects-cruft.sh | 83 +++++++++++++++++++++++++++++++++
3 files changed, 128 insertions(+), 14 deletions(-)
@@ -25,3 +25,12 @@ repack.writeBitmaps:: space and extra time spent on the initial repack. This has no effect if multiple packfiles are created. Defaults to true on bare repos, false otherwise.++repack.cruftWindow::+repack.cruftWindowMemory::+repack.cruftDepth::+repack.cruftThreads::+ Parameters used by linkgit:git-pack-objects[1] when generating+ a cruft pack and the respective parameters are not given over+ the command line. See similarly named `pack.*` configuration+ variables for defaults and meaning.
@@ -511,4 +511,87 @@ test_expect_success 'cruft repack ignores pack.packSizeLimit' ')'+test_expect_success'cruft repack respects repack.cruftWindow''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&++GIT_TRACE2_EVENT=$(pwd)/event.trace\+git-cpack.window=1-crepack.cruftWindow=2repack\+--cruft--window=3&&++grep"pack-objects.*--window=2.*--cruft"event.trace+)+'++test_expect_success'cruft repack respects --window by default''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&++GIT_TRACE2_EVENT=$(pwd)/event.trace\+git-cpack.window=2repack--cruft--window=3&&++grep"pack-objects.*--window=3.*--cruft"event.trace+)+'++test_expect_success'cruft repack respects --quiet''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+GIT_PROGRESS_DELAY=0gitrepack--cruft--quiet2>err&&+test_must_be_emptyerr+)+'++test_expect_success'cruft --local drops unreachable objects''+gitinitalternate&&+gitinitrepo&&+test_when_finished"rm -fr alternate repo"&&++test_commit-Calternatebase&&+# Pack all objects in alterate so that the cruft repack in "repo" sees+# the object it dropped due to `--local` as packed. Otherwise this+# object would not appear packed anywhere (since it is not packed in+# alternate and likewise not part of the cruft pack in the other repo+# because of `--local`).+git-Calternaterepack-ad&&++(+cdrepo&&++object="$(git-C../alternaterev-parseHEAD:base.t)"&&+git-C../alternatecat-file-p$object>contents&&++# Write some reachable objects and two unreachable ones: one+# that the alternate has and another that is unique.+test_commitother&&+githash-object-w-tblobcontents&&+cruft="$(echocruft|githash-object-w-tblob--stdin)"&&++(cd../alternate/.git/objects&&pwd)\+>.git/objects/info/alternates&&++test_path_is_file$objdir/$(test_oid_to_path$cruft)&&+test_path_is_file$objdir/$(test_oid_to_path$object)&&++gitrepack-d--cruft--local&&++test-toolpack-mtimes"$(basename$(ls$packdir/pack-*.mtimes))"\+>objects&&+!grep$objectobjects&&+grep$cruftobjects+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:16
In the next patch, we will implement and test support for writing a
cruft pack via a special mode of `git pack-objects`. To make sure that
objects are written with the correct timestamps, and a new test-tool
that can dump the object names and corresponding timestamps from a given
`.mtimes` file.
Signed-off-by: Taylor Blau <redacted>
---
Makefile | 1 +
t/helper/test-pack-mtimes.c | 53 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
4 files changed, 56 insertions(+)
create mode 100644 t/helper/test-pack-mtimes.c
@@ -0,0 +1,53 @@+#include"git-compat-util.h"+#include"test-tool.h"+#include"strbuf.h"+#include"object-store.h"+#include"packfile.h"+#include"pack-mtimes.h"++staticintdump_mtimes(structpacked_git*p)+{+uint32_ti;+if(load_pack_mtimes(p)<0)+die("could not load pack .mtimes");++for(i=0;i<p->num_objects;i++){+structobject_idoid;+if(nth_packed_object_id(&oid,p,i)<0)+die("could not load object id at position %"PRIu32,i);++printf("%s %"PRIu32"\n",+oid_to_hex(&oid),nth_packed_mtime(p,i));+}++return0;+}++staticconstchar*pack_mtimes_usage="\n"+" test-tool pack-mtimes <pack-name.mtimes>";++intcmd__pack_mtimes(intargc,constchar**argv)+{+structstrbufbuf=STRBUF_INIT;+structpacked_git*p;++setup_git_directory();++if(argc!=2)+usage(pack_mtimes_usage);++for(p=get_all_packs(the_repository);p;p=p->next){+strbuf_addstr(&buf,basename(p->pack_name));+strbuf_strip_suffix(&buf,".pack");+strbuf_addstr(&buf,".mtimes");++if(!strcmp(buf.buf,argv[1]))+break;++strbuf_reset(&buf);+}++strbuf_release(&buf);++returnp?dump_mtimes(p):1;+}
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:17
In a previous patch, pack-objects learned how to generate a cruft pack
so long as no objects are dropped.
This patch teaches pack-objects to handle the case where a non-never
`--cruft-expiration` value is passed. This case is slightly more
complicated than before, because we want pack-objects to save
unreachable objects which would have been pruned when there is another
recent (i.e., non-prunable) unreachable object which reaches the other.
We'll call these objects "unreachable but reachable-from-recent".
Here is how pack-objects handles `--cruft-expiration`:
- Instead of adding all objects outside of the kept pack(s) into the
packing list, only handle the ones whose mtime is within the grace
period.
- Construct a reachability traversal whose tips are the
unreachable-but-recent objects.
- Then, walk along that traversal, stopping if we reach an object in
the kept pack. At each step along the traversal, we add the object
we are visiting to the packing list.
In the majority of these cases, any object we visit in this traversal
will already be in our packing list. But we will sometimes encounter
reachable-from-recent cruft objects, which we want to retain even if
they aged out of the grace period.
The most subtle point of this process is that we actually don't need to
bother to update the rescued object's mtime. Even though we will write
an .mtimes file with a value that is older than the expiration window,
it will continue to survive cruft repacks so long as any objects which
reach it haven't aged out.
That is, a future repack will also exclude that object from the initial
packing list, only to discover it later on when doing the reachability
traversal.
Finally, stopping early once an object is found in a kept pack is safe
to do because the kept packs ordinarily represent which packs will
survive after repacking. Assuming that it _isn't_ safe to halt a
traversal early would mean that there is some ancestor object which is
missing, which implies repository corruption (i.e., the complete set of
reachable objects isn't present).
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 84 +++++++++++++++++++-
t/t5327-pack-objects-cruft.sh | 143 ++++++++++++++++++++++++++++++++++
2 files changed, 226 insertions(+), 1 deletion(-)
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:19
When generating a cruft pack, the caller within pack-objects will want
to know the precise timestamps of cruft objects (i.e., their
corresponding values in the .mtimes table) rather than the mtime of the
cruft pack itself.
Teach add_recent_packed() to lookup each object's precise mtime from the
.mtimes file if one exists (indicated by the is_cruft bit on the
packed_git structure).
A couple of small things worth noting here:
- load_pack_mtimes() needs to be called before asking for
nth_packed_mtime(), and that call is done lazily here. That function
exits early if the .mtimes file has already been opened and parsed,
so only the first call is slow.
- Checking the is_cruft bit can be done without any extra work on the
caller's behalf, since it is set up for us automatically as a
side-effect of calling add_packed_git() (just like the 'pack_keep'
and 'pack_promisor' bits).
Signed-off-by: Taylor Blau <redacted>
---
reachable.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:22
A new caller in the next commit will want to immediately modify the
object_entry structure created by create_object_entry(). Instead of
forcing that caller to wastefully look-up the entry we just created,
return it from create_object_entry() instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:23
Teach `pack-objects` how to generate a cruft pack when no objects are
dropped (i.e., `--cruft-expiration=never`). Later patches will teach
`pack-objects` how to generate a cruft pack that prunes objects.
When generating a cruft pack which does not prune objects, we want to
collect all unreachable objects into a single pack (noting and updating
their mtimes as we accumulate them). Ordinary use will pass the result
of a `git repack -A` as a kept pack, so when this patch says "kept
pack", readers should think "reachable objects".
Generating a non-expiring cruft packs works as follows:
- Callers provide a list of every pack they know about, and indicate
which packs are about to be removed.
- All packs which are going to be removed (we'll call these the
redundant ones) are marked as kept in-core, as well as any packs
that `pack-objects` found but the caller did not specify.
These packs are presumed to have entered the repository between
the caller collecting packs and invoking `pack-objects`. Since we
do not want to include objects in these packs (because we don't know
which of their objects are or aren't reachable), these are also
marked as kept in-core.
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
This results in a new cruft pack which contains all known objects that
aren't included in the kept packs. When the kept pack is the result of
`git repack -A`, the resulting pack contains all unreachable objects.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 23 +++
builtin/pack-objects.c | 203 ++++++++++++++++++++++++++-
object-file.c | 2 +-
object-store.h | 2 +
t/t5327-pack-objects-cruft.sh | 218 +++++++++++++++++++++++++++++
5 files changed, 442 insertions(+), 6 deletions(-)
create mode 100755 t/t5327-pack-objects-cruft.sh
@@ -95,6 +96,28 @@ base-name:: Incompatible with `--revs`, or options that imply `--revs` (such as `--all`), with the exception of `--unpacked`, which is compatible.+--cruft::+ Packs unreachable objects into a separate "cruft" pack, denoted+ by the existence of a `.mtimes` file. Pack names provided over+ stdin indicate which packs will remain after a `git repack`.+ Pack names prefixed with a `-` indicate those which will be+ removed. The contents of the cruft pack are all objects not+ contained in the surviving packs specified by `--keep-pack`)+ which have not exceeded the grace period (see+ `--cruft-expiration` below), or which have exceeded the grace+ period, but are reachable from an other object which hasn't.+++Incompatible with `--unpack-unreachable`, `--keep-unreachable`,+`--pack-loose-unreachable`, `--stdin-packs`, as well as any other+options which imply `--revs`. Also incompatible with `--max-pack-size`;+when this option is set, the maximum pack size is not inferred from+`pack.packSizeLimit`.++--cruft-expiration=<approxidate>::+ If specified, objects are eliminated from the cruft pack if they+ have an mtime older than `<approxidate>`. If unspecified (and+ given `--cruft`), then no objects are eliminated.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -3389,6 +3395,135 @@ static void read_packs_list_from_stdin(void)string_list_clear(&exclude_packs,0);}+staticintadd_cruft_object_entry(conststructobject_id*oid,enumobject_typetype,+structpacked_git*pack,off_toffset,+constchar*name,uint32_tmtime)+{+structobject_entry*entry;++display_progress(progress_state,++nr_seen);++entry=packlist_find(&to_pack,oid);+if(entry){+if(name){+entry->hash=pack_name_hash(name);+entry->no_try_delta=name&&no_try_delta(name);+}+}else{+if(!want_object_in_pack(oid,0,&pack,&offset))+return0;+if(!pack&&type==OBJ_BLOB&&!has_loose_object(oid)){+/*+*Ifatraversedtreehasamissingblobthenwewant+*toavoidaddingthatmissingobjecttoourpack.+*+*Thisonlyappliestomissingblobs,nottrees,+*becausethetraversalneedstoparsesub-treesbut+*notblobs.+*+*Noteweonlyperformthischeckwhenwecouldn't+*alreadyfindtheobjectinapack,sowe'rereally+*limitedto"ensure non-tip blobs which don't exist in+*packsdoexistvialooseobjects". Confused?+*/+return0;+}++entry=create_object_entry(oid,type,pack_name_hash(name),+0,name&&no_try_delta(name),+pack,offset);+}++if(mtime>oe_cruft_mtime(&to_pack,entry))+oe_set_cruft_mtime(&to_pack,entry,mtime);+return1;+}++staticvoidmark_pack_kept_in_core(structstring_list*packs,unsignedkeep)+{+structstring_list_item*item=NULL;+for_each_string_list_item(item,packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=keep;+}+}++staticvoidadd_unreachable_loose_objects(void);+staticvoidadd_objects_in_unpacked_packs(void);++staticvoidenumerate_cruft_objects(void)+{+if(progress)+progress_state=start_progress(_("Enumerating cruft objects"),0);++add_objects_in_unpacked_packs();+add_unreachable_loose_objects();++stop_progress(&progress_state);+}++staticvoidread_cruft_objects(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listdiscard_packs=STRING_LIST_INIT_DUP;+structstring_listfresh_packs=STRING_LIST_INIT_DUP;+structpacked_git*p;++ignore_packed_keep_in_core=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='-')+string_list_append(&discard_packs,buf.buf+1);+else+string_list_append(&fresh_packs,buf.buf);+strbuf_reset(&buf);+}++string_list_sort(&discard_packs);+string_list_sort(&fresh_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);+structstring_list_item*item;++item=string_list_lookup(&fresh_packs,pack_name);+if(!item)+item=string_list_lookup(&discard_packs,pack_name);++if(item){+item->util=p;+}else{+/*+*Thispackwasn'tmentionedineitherthe"fresh"or+*"discard"list,sothecallerdidn'tknowaboutit.+*+*Markitaskeptsothatitsobjectsareignoredby+*add_unseen_recent_objects_to_traversal().We'll+*unmarkitbeforestartingthetraversalsoitdoesn't+*haltthetraversalearly.+*/+p->pack_keep_in_core=1;+}+}++mark_pack_kept_in_core(&fresh_packs,1);+mark_pack_kept_in_core(&discard_packs,0);++if(cruft_expiration)+die("--cruft-expiration not yet implemented");+else+enumerate_cruft_objects();++strbuf_release(&buf);+string_list_clear(&discard_packs,0);+string_list_clear(&fresh_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3521,7 +3656,24 @@ static int add_object_in_unpacked_pack(const struct object_id *oid,uint32_tpos,void*_data){-add_object_entry(oid,OBJ_NONE,"",0);+if(cruft){+off_toffset;+time_tmtime;++if(pack->is_cruft){+if(load_pack_mtimes(pack)<0)+die(_("could not load cruft pack .mtimes"));+mtime=nth_packed_mtime(pack,pos);+}else{+mtime=pack->mtime;+}+offset=nth_packed_object_offset(pack,pos);++add_cruft_object_entry(oid,OBJ_NONE,pack,offset,+NULL,mtime);+}else{+add_object_entry(oid,OBJ_NONE,"",0);+}return0;}
@@ -3545,7 +3697,19 @@ static int add_loose_object(const struct object_id *oid, const char *path,return0;}-add_object_entry(oid,type,"",0);+if(cruft){+structstatst;+if(stat(path,&st)<0){+if(errno==ENOENT)+return0;+returnerror_errno("unable to stat %s",oid_to_hex(oid));+}++add_cruft_object_entry(oid,type,NULL,0,NULL,+st.st_mtime);+}else{+add_object_entry(oid,type,"",0);+}return0;}
@@ -3864,6 +4028,20 @@ static int option_parse_unpack_unreachable(const struct option *opt,return0;}+staticintoption_parse_cruft_expiration(conststructoption*opt,+constchar*arg,intunset)+{+if(unset){+cruft=0;+cruft_expiration=0;+}else{+cruft=1;+if(arg)+cruft_expiration=approxidate(arg);+}+return0;+}+intcmd_pack_objects(intargc,constchar**argv,constchar*prefix){intuse_internal_rev_list=0;
@@ -3936,6 +4114,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_CALLBACK_F(0,"unpack-unreachable",NULL,N_("time"),N_("unpack unreachable objects newer than <time>"),PARSE_OPT_OPTARG,option_parse_unpack_unreachable),+OPT_BOOL(0,"cruft",&cruft,N_("create a cruft pack")),+OPT_CALLBACK_F(0,"cruft-expiration",NULL,N_("time"),+N_("expire cruft objects older than <time>"),+PARSE_OPT_OPTARG,option_parse_cruft_expiration),OPT_BOOL(0,"sparse",&sparse,N_("use the sparse reachability algorithm")),OPT_BOOL(0,"thin",&thin,
@@ -4060,7 +4242,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(!HAVE_THREADS&&delta_search_threads!=1)warning(_("no threads support, ignoring --threads"));-if(!pack_to_stdout&&!pack_size_limit)+if(!pack_to_stdout&&!pack_size_limit&&!cruft)pack_size_limit=pack_size_limit_cfg;if(pack_to_stdout&&pack_size_limit)die(_("--max-pack-size cannot be used to build a pack for transfer"));
@@ -4087,6 +4269,15 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(stdin_packs&&use_internal_rev_list)die(_("cannot use internal rev list with --stdin-packs"));+if(cruft){+if(use_internal_rev_list)+die(_("cannot use internal rev list with --cruft"));+if(stdin_packs)+die(_("cannot use --stdin-packs with --cruft"));+if(pack_size_limit)+die(_("cannot use --max-pack-size with --cruft"));+}+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -0,0 +1,218 @@+#!/bin/sh++test_description='cruft pack related pack-objects tests'+../test-lib.sh++objdir=.git/objects+packdir=$objdir/pack++basic_cruft_pack_tests(){+expire="$1"++test_expect_success"unreachable loose objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+gitrepack-Ad&&+test_commitloose&&++test-toolchmtime+2000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose:loose.t))" &&+test-toolchmtime+1000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose^{tree}))" &&++(+gitrev-list--objects--no-object-namesbase..loose|+whilereadoid+do+path="$objdir/$(test_oid_to_path"$oid")"&&+printf"%s %d\n""$oid""$(test-toolchmtime--get"$path")"+done|+sort-k1+)>expect&&++keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+cruft="$(echo$keep|gitpack-objects--cruft\+--cruft-expiration="$expire"$packdir/pack)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable packed objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+other="$(gitpack-objects--delta-base-offset\+$packdir/pack<objects)" &&+gitprune-packed&&++test-toolchmtime--get-100"$packdir/pack-$other.pack">expect&&++cruft="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$other.pack+EOF+)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++cut-d" "-f2<actual.raw|sort-u>actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable cruft objects are repacked (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++cruft_a="$(echo$keep|gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack)"&&+gitprune-packed&&+cruft_b="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$cruft_a.pack+EOF+)" &&++test-toolpack-mtimes"pack-$cruft_a.mtimes">expect.raw&&+test-toolpack-mtimes"pack-$cruft_b.mtimes">actual.raw&&++sort<expect.raw>expect&&+sort<actual.raw>actual&&++test_cmpexpectactual+)+'++test_expect_success"multiple cruft packs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++test_commitcruft&&+loose="$objdir/$(test_oid_to_path$(gitrev-parsecruft))"&&++# generate three copies of the cruft object in different+# cruft packs, each with a unique mtime:+# - one expired (1000 seconds ago)+# - two non-expired (one 1000 seconds in the future,+# one 1500 seconds in the future)+test-toolchmtime=-1000"$loose"&&+gitpack-objects--cruft$packdir/pack-A<<-EOF&&+$keep+EOF+test-toolchmtime=+1000"$loose"&&+gitpack-objects--cruft$packdir/pack-B<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+EOF+test-toolchmtime=+1500"$loose"&&+gitpack-objects--cruft$packdir/pack-C<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+EOF++# ensure the resulting cruft pack takes the most recent+# mtime among all copies+cruft="$(gitpack-objects--cruft\+--cruft-expiration="$expire"\+$packdir/pack<<-EOF+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+-$(basename$(ls$packdir/pack-C-*.pack))+EOF+)" &&++test-toolpack-mtimes"$(basename$(ls$packdir/pack-C-*.mtimes))">expect.raw&&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++sortexpect.raw>expect&&+sortactual.raw>actual&&+test_cmpexpectactual+)+'++test_expect_success"cruft packs tolerate missing trees (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++tree="$(gitrev-parsecruft^{tree})"&&++gitreset--hardreachable&&+gittag-dcruft&&+rm-fr.git/logs&&++# remove the unreachable tree, but leave the commit+# which has it as its root tree in-tact+rm-fr"$objdir/$(test_oid_to_path"$tree")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'++test_expect_success"cruft packs tolerate missing blobs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++blob="$(gitrev-parsecruft:cruft.t)"&&++gitreset--hardreachable&&+gittag-dcruft&&+rm-fr.git/logs&&++# remove the unreachable blob, but leave the commit (and+# the root tree of that commit) in-tact+rm-fr"$objdir/$(test_oid_to_path"$blob")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'+}++basic_cruft_pack_testsnever++test_done
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:27
Expose a way to split the contents of a repository into a main and cruft
pack when doing an all-into-one repack with `git repack --cruft -d`, and
a complementary configuration variable.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 11 ++
Documentation/technical/cruft-packs.txt | 2 +-
builtin/repack.c | 112 ++++++++++++++++-
t/t5327-pack-objects-cruft.sh | 153 ++++++++++++++++++++++++
4 files changed, 272 insertions(+), 6 deletions(-)
@@ -63,6 +63,17 @@ to the new separate pack will be written. Also run 'git prune-packed' to remove redundant loose object files.+--cruft::+ Same as `-a`, unless `-d` is used. Then any unreachable objects+ are packed into a separate cruft pack. Unreachable objects can+ be pruned using the normal expiry rules with the next `git gc`+ invocation (see linkgit:git-gc[1]). Incompatible with `-k`.++--cruft-expiration=<approxidate>::+ Expire unreachable objects older than `<approxidate>`+ immediately instead of waiting for the next `git gc` invocation.+ Only useful with `--cruft -d`.+ -l:: Pass the `--local` option to 'git pack-objects'. See linkgit:git-pack-objects[1].
@@ -16,7 +16,7 @@ pruned according to normal expiry rules with the next 'git gc' invocation. Unreachable objects aren't removed immediately, since doing so could race with an incoming push which may reference an object which is about to be deleted.-Instead, those unreachable objects are stored as loose object and stay that way+Instead, those unreachable objects are stored as loose objects and stay that way until they are older than the expiration window, at which point they are removed by linkgit:git-prune[1].
@@ -598,6 +604,67 @@ static int write_midx_included_packs(struct string_list *include,returnfinish_command(&cmd);}+staticintwrite_cruft_pack(conststructpack_objects_args*args,+constchar*pack_prefix,+structstring_list*names,+structstring_list*existing_packs,+structstring_list*existing_kept_packs)+{+structchild_processcmd=CHILD_PROCESS_INIT;+structstrbufline=STRBUF_INIT;+structstring_list_item*item;+FILE*in,*out;+intret;++prepare_pack_objects(&cmd,args);++strvec_push(&cmd.args,"--cruft");+if(cruft_expiration)+strvec_pushf(&cmd.args,"--cruft-expiration=%s",+cruft_expiration);++strvec_push(&cmd.args,"--honor-pack-keep");+strvec_push(&cmd.args,"--non-empty");+strvec_push(&cmd.args,"--max-pack-size=0");++cmd.in=-1;++ret=start_command(&cmd);+if(ret)+returnret;++/*+*nameshasaconfusingdoubleuse:itbothprovidesthelist+*ofjust-writtennewpacks,andacceptsthenameofthecruft+*packwearewriting.+*+*Bythetimeitisreadhere,itcontainsonlythepack(s)+*thatwerejustwritten,whichisexactlythesetofpackswe+*wanttoconsiderkept.+*/+in=xfdopen(cmd.in,"w");+for_each_string_list_item(item,names)+fprintf(in,"%s-%s.pack\n",pack_prefix,item->string);+for_each_string_list_item(item,existing_packs)+fprintf(in,"-%s.pack\n",item->string);+for_each_string_list_item(item,existing_kept_packs)+fprintf(in,"%s.pack\n",item->string);+fclose(in);++out=xfdopen(cmd.out,"r");+while(strbuf_getline_lf(&line,out)!=EOF){+if(line.len!=the_hash_algo->hexsz)+die(_("repack: Expecting full hex object ID lines only "+"from pack-objects."));+string_list_append(names,line.buf);+}+fclose(out);++strbuf_release(&line);++returnfinish_command(&cmd);+}+intcmd_repack(intargc,constchar**argv,constchar*prefix){structchild_processcmd=CHILD_PROCESS_INIT;
@@ -614,7 +681,6 @@ int cmd_repack(int argc, const char **argv, const char *prefix)intshow_progress=isatty(2);/* variables to be filled by option parsing */-intpack_everything=0;intdelete_redundant=0;constchar*unpack_unreachable=NULL;intkeep_unreachable=0;
@@ -630,6 +696,11 @@ int cmd_repack(int argc, const char **argv, const char *prefix)OPT_BIT('A',NULL,&pack_everything,N_("same as -a, and turn unreachable objects loose"),LOOSEN_UNREACHABLE|ALL_INTO_ONE),+OPT_BIT(0,"cruft",&pack_everything,+N_("same as -a, pack unreachable cruft objects separately"),+PACK_CRUFT|ALL_INTO_ONE),+OPT_STRING(0,"cruft-expiration",&cruft_expiration,N_("approxidate"),+N_("with -C, expire objects older than this")),OPT_BOOL('d',NULL,&delete_redundant,N_("remove redundant packs, and run git-prune-packed")),OPT_BOOL('f',NULL,&po_args.no_reuse_delta,
@@ -681,6 +752,14 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(keep_unreachable&&(unpack_unreachable||(pack_everything&LOOSEN_UNREACHABLE)))die(_("--keep-unreachable and -A are incompatible"));+if(pack_everything&PACK_CRUFT&&delete_redundant){+if(unpack_unreachable||(pack_everything&LOOSEN_UNREACHABLE))+die(_("--cruft and -A are incompatible"));+if(keep_unreachable)+die(_("--cruft and -k are incompatible"));+if(!(pack_everything&ALL_INTO_ONE))+die(_("--cruft must be combined with all-into-one"));+}if(write_bitmaps<0){if(!write_midx&&
@@ -825,6 +912,21 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(!names.nr&&!po_args.quiet)printf_ln(_("Nothing new to pack."));+if(pack_everything&PACK_CRUFT){+constchar*pack_prefix;+if(!skip_prefix(packtmp,packdir,&pack_prefix))+die(_("pack prefix %s does not begin with objdir %s"),+packtmp,packdir);+if(*pack_prefix=='/')+pack_prefix++;++ret=write_cruft_pack(&po_args,pack_prefix,&names,+&existing_nonkept_packs,+&existing_kept_packs);+if(ret)+returnret;+}+for_each_string_list_item(item,&names){item->util=(void*)(uintptr_t)populate_pack_exts(item->string);}
@@ -358,4 +358,157 @@ test_expect_success 'expired objects are pruned' ')'+test_expect_success'repack --cruft generates a cruft pack''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitbranch-Mmain&&+gitcheckout--orphanother&&+test_commitunreachable&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dunreachable&&+# objects are not cruft if they are contained in the reflogs+rm-fr.git/logs&&++gitrev-list--objects--all--no-object-names>reachable.raw&&+gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&+sort<reachable.raw>reachable&&+comm-13reachableobjects>unreachable&&++gitrepack--cruft-d&&++cruft=$(basename$(ls$packdir/pack-*.mtimes).mtimes)&&+pack=$(basename$(ls$packdir/pack-*.pack|grep-v$cruft).pack)&&++gitshow-index<$packdir/$pack.idx>actual.raw&&+cut-f2-d" "actual.raw|sort>actual&&+test_cmpreachableactual&&++gitshow-index<$packdir/$cruft.idx>actual.raw&&+cut-f2-d" "actual.raw|sort>actual&&+test_cmpunreachableactual+)+'++test_expect_success'loose objects mtimes upsert others''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+gitbranch-Mmain&&++gitcheckout--orphanother&&+test_commitcruft&&+# incremental repack, leaving existing objects loose (so+# they can be "freshened")+gitrepack&&++tip="$(gitrev-parsecruft)"&&+path="$objdir/$(test_oid_to_path"$(gitrev-parsecruft)")"&&+test-toolchmtime--get+1000"$path">expect&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dcruft&&+rm-fr.git/logs&&++gitrepack--cruft-d&&++mtimes="$(basename$(ls$packdir/pack-*.mtimes))"&&+test-toolpack-mtimes"$mtimes">actual.raw&&+grep"$tip"actual.raw|cut-d" "-f2>actual&&+test_cmpexpectactual+)+'++test_expect_success'cruft packs are not included in geometric repack''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+gitbranch-Mmain&&++gitcheckout--orphanother&&+test_commitcruft&&+gitrepack-d&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dcruft&&+rm-fr.git/logs&&++gitrepack--cruft&&++find$packdir-typef|sort>before&&+gitrepack--geometric=2-d&&+find$packdir-typef|sort>after&&++test_cmpbeforeafter+)+'+test_expect_success'cruft repack with no reachable objects''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+gitrepack-ad&&++base="$(gitrev-parsebase)"&&++gitfor-each-ref--format="delete %(refname)">in&&+gitupdate-ref--stdin<in&&+rm-fr.git/logs&&+rm-fr.git/index&&++gitrepack--cruft-d&&++gitcat-file-t$base+)+'++test_expect_success'cruft repack ignores --max-pack-size''+gitinitmax-pack-size&&+(+cdmax-pack-size&&+test_commitbase&&+# two cruft objects which exceed the maximum pack size+test-toolgenrandomfoo1048576|githash-object--stdin-w&&+test-toolgenrandombar1048576|githash-object--stdin-w&&+gitrepack--cruft--max-pack-size=1M&&+find$packdir-name"*.mtimes">cruft&&+test_line_count=1cruft&&+test-toolpack-mtimes"$(basename"$(catcruft)")">objects&&+test_line_count=2objects+)+'++test_expect_success'cruft repack ignores pack.packSizeLimit''+(+cdmax-pack-size&&+# repack everything back together to remove the existing cruft+# pack (but to keep its objects)+gitrepack-adk&&+git-cpack.packSizeLimit=1Mrepack--cruft&&+# ensure the same post condition is met when --max-pack-size+# would otherwise be inferred from the configuration+find$packdir-name"*.mtimes">cruft&&+test_line_count=1cruft&&+test-toolpack-mtimes"$(basename"$(catcruft)")">objects&&+test_line_count=2objects+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:29
When using cruft packs, the following race can occur when a geometric
repack that writes a MIDX bitmap takes place afterwords:
- First, create an unreachable object and do an all-into-one cruft
repack which stores that object in the repository's cruft pack.
- Then make that object reachable.
- Finally, do a geometric repack and write a MIDX bitmap.
Assuming that we are sufficiently unlucky as to select a commit from the
MIDX which reaches that object for bitmapping, then the `git
multi-pack-index` process will complain that that object is missing.
The reason is because we don't include cruft packs in the MIDX when
doing a geometric repack. Since the "make that object reachable" doesn't
necessarily mean that we'll create a new copy of that object in one of
the packs that will get rolled up as part of a geometric repack, it's
possible that the MIDX won't see any copies of that now-reachable
object.
Of course, it's desirable to avoid including cruft packs in the MIDX
because it causes the MIDX to store a bunch of objects which are likely
to get thrown away. But excluding that pack does open us up to the above
race.
This patch demonstrates the bug, and resolves it by including cruft
packs in the MIDX even when doing a geometric repack.
Signed-off-by: Taylor Blau <redacted>
---
builtin/repack.c | 19 +++++++++++++++++--
t/t5327-pack-objects-cruft.sh | 26 ++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 2 deletions(-)
@@ -594,4 +594,30 @@ test_expect_success 'cruft --local drops unreachable objects' ')'+test_expect_success'MIDX bitmaps tolerate reachable cruft objects''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&+unreachable="$(gitrev-parsecruft)"&&++gitreset--hard$unreachable^&&+gittag-dcruft&&+rm-fr.git/logs&&++gitrepack--cruft-d&&++# resurrect the unreachable object via a new commit. the+# new commit will get selected for a bitmap, but be+# missing one of its parents from the selected packs.+gitreset--hard$unreachable&&+test_commitresurrect&&++gitrepack--write-midx--write-bitmap-index--geometric=2-d+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:31
Expose the new `git repack --cruft` mode from `git gc` via a new opt-in
flag. When invoked like `git gc --cruft`, `git gc` will avoid exploding
unreachable objects as loose ones, and instead create a cruft pack and
`.mtimes` file.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/config/gc.txt | 21 +++++++++++++-------
Documentation/git-gc.txt | 5 +++++
builtin/gc.c | 10 +++++++++-
t/t5327-pack-objects-cruft.sh | 37 +++++++++++++++++++++++++++++++++++
4 files changed, 65 insertions(+), 8 deletions(-)
@@ -81,14 +81,21 @@ gc.packRefs:: to enable it within all non-bare repos or it can be set to a boolean value. The default is `true`.+gc.cruftPacks::+ Store unreachable objects in a cruft pack (see+ linkgit:git-repack[1]) instead of as loose objects. The default+ is `false`.+ gc.pruneExpire::- When 'git gc' is run, it will call 'prune --expire 2.weeks.ago'.- Override the grace period with this config variable. The value- "now" may be used to disable this grace period and always prune- unreachable objects immediately, or "never" may be used to- suppress pruning. This feature helps prevent corruption when- 'git gc' runs concurrently with another process writing to the- repository; see the "NOTES" section of linkgit:git-gc[1].+ When 'git gc' is run, it will call 'prune --expire 2.weeks.ago'+ (and 'repack --cruft --cruft-expiration 2.weeks.ago' if using+ cruft packs via `gc.cruftPacks` or `--cruft`). Override the+ grace period with this config variable. The value "now" may be+ used to disable this grace period and always prune unreachable+ objects immediately, or "never" may be used to suppress pruning.+ This feature helps prevent corruption when 'git gc' runs+ concurrently with another process writing to the repository; see+ the "NOTES" section of linkgit:git-gc[1]. gc.worktreePruneExpire:: When 'git gc' is run, it calls
@@ -54,6 +54,11 @@ other housekeeping tasks (e.g. rerere, working trees, reflog...) will be performed as well.+--cruft::+ When expiring unreachable objects, pack them separately into a+ cruft pack instead of storing the loose objects as loose+ objects.+ --prune=<date>:: Prune loose objects older than date (default is 2 weeks ago, overridable by the config variable `gc.pruneExpire`).
@@ -668,6 +675,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)die(FAILED_RUN,repack.v[0]);if(prune_expire){+/* run `git prune` even if using cruft packs */strvec_push(&prune,prune_expire);if(quiet)strvec_push(&prune,"--no-progress");
@@ -429,6 +429,43 @@ test_expect_success 'loose objects mtimes upsert others' ')'+test_expect_success'expiring cruft objects with git gc''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitbranch-Mmain&&+gitcheckout--orphanother&&+test_commitunreachable&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dunreachable&&+# objects are not cruft if they are contained in the reflogs+rm-fr.git/logs&&++gitrev-list--objects--all--no-object-names>reachable.raw&&+gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&+sort<reachable.raw>reachable&&+comm-13reachableobjects>unreachable&&++gitrepack--cruft-d&&++mtimes=$(ls.git/objects/pack/pack-*.mtimes)&&+test_path_is_file$mtimes&&++gitgc--cruft--prune=now&&++gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&++comm-23unreachableobjects>removed&&+test_cmpunreachableremoved&&+test_path_is_missing$mtimes+)+'+ test_expect_success'cruft packs are not included in geometric repack''gitinitrepo&&test_when_finished"rm -fr repo"&&
From: Taylor Blau <hidden> Date: 2021-11-29 22:26:35
We don't bother to freshen objects stored in a cruft pack individually
by updating the `.mtimes` file. This is because we can't portably `mmap`
and write into the middle of a file (i.e., to update the mtime of just
one object). Instead, we would have to rewrite the entire `.mtimes` file
which may incur some wasted effort especially if there a lot of cruft
objects and they are freshened infrequently.
Instead, force the freshening code to avoid an optimizing write by
writing out the object loose and letting it pick up a current mtime.
This works because we prefer the mtime of the loose copy of an object
when both a loose and packed one exist (whether or not the packed copy
comes from a cruft pack or not).
This could certainly do with a test and/or be included earlier in this
series/PR, but I want to wait until after I have a chance to clean up
the overly-repetitive nature of the cruft pack tests in general.
Signed-off-by: Taylor Blau <redacted>
---
object-file.c | 2 ++
t/t5327-pack-objects-cruft.sh | 25 +++++++++++++++++++++++++
2 files changed, 27 insertions(+)
From: Taylor Blau <hidden> Date: 2021-11-29 22:27:04
We use the `util` pointer for items in the `existing_packs` string list
to indicate which packs are going to be deleted. Since that has so far
been the only use of that `util` pointer, we just set it to 0 or 1.
But we're going to add an additional state to this field in the next
patch, so prepare for that by adding a #define for the first bit so we
can more expressively inspect the flags state.
Signed-off-by: Taylor Blau <redacted>
---
builtin/repack.c | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
+Notable alternatives to this design include:
+
+ - The location of the per-object mtime data, and
+ - Whether cruft packs should be incremental or not.
It was not obvious from this sentence that "incremental" meant that
we could store a number of cruft packs and use the mtime of each pack
as the time for all contained objects.
+On the location of mtime data, a new auxiliary file tied to the pack was chosen
+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain
+support for optional chunks of data, it may make sense to consolidate the
+`.mtimes` format into the `.idx` itself.
+
+Incremental cruft packs (i.e., where each time a repository is repacked a new
+cruft pack is generated containing only the unreachable objects introduced since
+the last time a cruft pack was written) are significantly more complicated to
+construct, and so aren't pursued here. The obvious drawback to the current
+implementation is that the entire cruft pack must be re-written from scratch.
But you seem to be pointing that direction here. The difference being
that you don't discuss how a list of cruft packs could avoid the .mtimes
file.
I think what is hidden underneath "significantly more complicated to
construct" are situations such as "this object was in an old cruft
pack, but then became reachable, but now is unreachable again". I'll
try to remember to come back to this after seeing the situations you
cover in your tests.
Thanks,
-Stolee
+== pack-*.mtimes files have the format:
+
+ - A 4-byte magic number '0x4d544d45' ('MTME').
+
+ - A 4-byte version identifier (= 1).
+
+ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).
I vaguely remember complaints about using a 1-byte identifier in
the commit-graph and multi-pack-index formats because the "standard"
way to refer to these hash functions was a magic number that had a
meaning in ASCII that helped human readers a bit. I cannot find an
example of such 4-byte identifiers, but perhaps brian (CC'd) could
remind us.
You are using a 4-byte identifier, but using the same values as
those 1-byte identifiers.
+ - A table of mtimes (one per packed object, num_objects in total, each
+ a 4-byte unsigned integer in network order), in the same order as
+ objects appear in the index file (e.g., the first entry in the mtime
+ table corresponds to the object with the lowest lexically-sorted
+ oid). The mtimes count standard epoch seconds.
This paragraph seemed awkward. Here is a rephrasing that might be
less awkward:
- A table of 4-byte unsigned integers in network order. The ith value
is the modified time (mtime) of the ith object of the corresponding
pack in lexicographic order. The mtime represents standard epoch
seconds.
Storing these mtimes in 32-bits means we will hit the 2038 problem.
The commit-graph stores commit times with an extra two bits to extend
the lifetime by another hundred years or so.
Could we extend the lifetime of cruft packs by decreasing the granularity
here? Should 'mtime' store a number of _minutes_ instead of seconds? That
should be enough granularity for these purposes.
+ - A trailer, containing a:
+
+ checksum of the corresponding packfile, and
+
+ a checksum of all of the above.
Could you specify the checksum as having length according to the
specified hash function?
+All 4-byte numbers are in network order.
+
Maybe this could be at the start of the format, since the file
version and hash function are both 4-byte numbers here and we
could remove the mention of network order from the mtime values.
+static char *pack_mtimes_filename(struct packed_git *p)
+{
+ size_t len;
+ if (!strip_suffix(p->pack_name, ".pack", &len))
+ BUG("pack_name does not end in .pack");
+ /* NEEDSWORK: this could reuse code from pack-revindex.c. */
+ return xstrfmt("%.*s.mtimes", (int)len, p->pack_name);
+}
I see your NEEDSWORK here and you are probably referring to this:
static char *pack_revindex_filename(struct packed_git *p)
{
size_t len;
if (!strip_suffix(p->pack_name, ".pack", &len))
BUG("pack_name does not end in .pack");
return xstrfmt("%.*s.rev", (int)len, p->pack_name);
}
and the implementation is identical except for the new trailer
(which exist in the exts[] array in builtin/repack.c, but could
also be pulled out into a header somewhere.
I'm happy to delay any cleanup of these code clones until later,
if at all, because doing it right might mean moving more code
than we like. Such refactorings aren't worth it most of the time.
+ if (mtimes_size - MTIMES_MIN_SIZE != st_mult(sizeof(uint32_t), num_objects)) {
+ ret = error(_("mtimes file %s is corrupt"), mtimes_file);
This message could be more informative: "mtimes file %s has the wrong size"?
+ data = hdr = xmmap(NULL, mtimes_size, PROT_READ, MAP_PRIVATE, fd, 0);
+
+ if (ntohl(*hdr) != MTIMES_SIGNATURE) {
+ ret = error(_("mtimes file %s has unknown signature"), mtimes_file);
+ goto cleanup;
+ }
Interesting that you defined 'struct mtimes_header' before this
method, but don't use it here (in favor of moving a uint32_t
pointer). Perhaps you are avoiding pointing the struct at the
memory map, but you could also do this:
struct mtimes_header header;
header.signature = ntohl(hdr[0]);
header.version = ntohl(hdr[1]);
header.hash_id = ntohl(hdr[2]);
And then operate on the struct for your validation.
At the very least, 'struct mtimes_header' is defined but not
used in this patch. If you decide to not use it this way, then
maybe delay its definition.
+
+ if (ntohl(*++hdr) != 1) {
+ ret = error(_("mtimes file %s has unsupported version %"PRIu32),
+ mtimes_file, ntohl(*hdr));
Unlike the commit-graph, if we don't understand the version we
cannot simply ignore the data. error() is appropriate here.
+int load_pack_mtimes(struct packed_git *p)
+{
+ char *mtimes_name = NULL;
+ int ret = 0;
+
+ if (!p->is_cruft)
+ return ret; /* not a cruft pack */
Interesting that this indicator is essentially "we have an mtimes
file for this pack", but it makes sense to include that check next
to the .keep and .promisor checks.
+uint32_t nth_packed_mtime(struct packed_git *p, uint32_t pos)
+{
+ if (!p->mtimes_map)
+ BUG("pack .mtimes file not loaded for %s", p->pack_name);
+ if (p->num_objects <= pos)
+ BUG("pack .mtimes out-of-bounds (%"PRIu32" vs %"PRIu32")",
+ pos, p->num_objects);
+
+ return get_be32(p->mtimes_map + pos + 3);
+}
(Speaking of that refactoring earlier, here is a second definition of
exts[] that would be valuable to unify.)
The hunks I did not comment on look good. Nice standard file format
stuff.
Thanks,
-Stolee
There are three definitions of an identical function which converts
`the_hash_algo` into either 1 (for SHA-1) or 2 (for SHA-256). There is a
copy of this function for writing both the commit-graph and
multi-pack-index file, and another inline definition used to write the
.rev header.
Consolidate these into a single definition in chunk-format.h. It's not
clear that this is the best header to define this function in, but it
should do for now.
Thanks for consolidating these!
(Worth noting, the .rev caller expects a 4-byte unsigned, but the other
two callers work with a single unsigned byte. The consolidated version
uses the latter type, and lets the compiler widen it when required).
Another caller will be added in a subsequent patch.
I notice that you don't use this in load_pack_mtimes_file(),
in pack-mtimes.c but you could at this point.
The code you do touch looks good.
Thanks,
-Stolee
When writing a pack, it appears that the cruft_mtime array
maps to objects in pack-order, not idx-order, correct? That
might be worth mentioning in the struct definition because
it differs from the .mtimes file.
The name "objects" here confused me at first, thinking it
corresponded to the objects member of 'struct packing_data', but
that is being handled by the fact that 'objects' is actually a
lex-sorted list of pack_idx_entry pointers (and they happen to
also point to 'struct object_entry' values because the 'struct
pack_idx_entry' is the first member.
So this is (very densely) handling the translation from pack-order
to lex-order through the double pointer 'objects'. I'm not sure if
there is a way to make it more clear or if every reader will need
to do the same mental gymnastics I had to do.
From: brian m. carlson <hidden> Date: 2021-12-02 22:32:37
On 2021-12-02 at 15:06:07, Derrick Stolee wrote:
On 11/29/2021 5:25 PM, Taylor Blau wrote:
quoted
+== pack-*.mtimes files have the format:
+
+ - A 4-byte magic number '0x4d544d45' ('MTME').
+
+ - A 4-byte version identifier (= 1).
+
+ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).
I vaguely remember complaints about using a 1-byte identifier in
the commit-graph and multi-pack-index formats because the "standard"
way to refer to these hash functions was a magic number that had a
meaning in ASCII that helped human readers a bit. I cannot find an
example of such 4-byte identifiers, but perhaps brian (CC'd) could
remind us.
You are using a 4-byte identifier, but using the same values as
those 1-byte identifiers.
The preferred value is the_hash_algo->format_id. For SHA-1, that's
"sha1", big-endian (0x73686131) and for SHA-256 it's "s256", big-endian
(0x73323536).
There's also hash_algo_by_id to turn the format ID into an index into
the hash_algos array, but you need to check for GIT_HASH_UNKNOWN (0)
first.
These will be used in index v3, which I haven't sent out patches for
yet.
--
brian m. carlson (he/him or they/them)
Toronto, Ontario, CA
From: Taylor Blau <hidden> Date: 2021-12-03 21:53:51
On Thu, Dec 02, 2021 at 09:33:51AM -0500, Derrick Stolee wrote:
On 11/29/2021 5:25 PM, Taylor Blau wrote:
quoted
+Notable alternatives to this design include:
+
+ - The location of the per-object mtime data, and
+ - Whether cruft packs should be incremental or not.
It was not obvious from this sentence that "incremental" meant that
we could store a number of cruft packs and use the mtime of each pack
as the time for all contained objects.
Yes, I think I meant "incremental" in the sense of "incremental commit-
graphs". But it's clearer to say "storing unreachable objects in
multiple cruft packs" (and then giving an example later on). Thanks!
I think what is hidden underneath "significantly more complicated to
construct" are situations such as "this object was in an old cruft
pack, but then became reachable, but now is unreachable again". I'll
try to remember to come back to this after seeing the situations you
cover in your tests.
Yeah, I'm being deliberately vague here, since the aim of this paragraph
is to illustrate "this is much more complicated than what we implement
here, and the trade-offs are..."
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2021-12-03 22:24:07
On Thu, Dec 02, 2021 at 10:06:07AM -0500, Derrick Stolee wrote:
On 11/29/2021 5:25 PM, Taylor Blau wrote:
quoted
+== pack-*.mtimes files have the format:
+
+ - A 4-byte magic number '0x4d544d45' ('MTME').
+
+ - A 4-byte version identifier (= 1).
+
+ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).
I vaguely remember complaints about using a 1-byte identifier in
the commit-graph and multi-pack-index formats because the "standard"
way to refer to these hash functions was a magic number that had a
meaning in ASCII that helped human readers a bit. I cannot find an
example of such 4-byte identifiers, but perhaps brian (CC'd) could
remind us.
You are using a 4-byte identifier, but using the same values as
those 1-byte identifiers.
Yeah, I'm definitely borrowing from the commit-graph and multi-pack
index formats here. Though I believe we did the same thing for .rev
files, too (and checking with Documentation/technical/pack-format.txt
confirms as much).
I don't have a strong feeling about using the 4-byte identifier or not.
But making this field four bytes wide is very much intentional, since it
makes sure that all of our reads are aligned, which should yield much
better cache performance (assuming the page size is also a multiple of
four).
I don't, but if others feel strongly we could write the magic
identifiers brian points out downthread here instead. (It would be
mildly inconvenient for GitHub, which has many hundreds of thousands of
these files laying around everywhere with '1' as the identifier. But
since the magic identifiers don't collide with the values proposed here,
GitHub's fork could easily be taught to accept both on the reading side,
but only write out the special identifier).
quoted
+ - A table of mtimes (one per packed object, num_objects in total, each
+ a 4-byte unsigned integer in network order), in the same order as
+ objects appear in the index file (e.g., the first entry in the mtime
+ table corresponds to the object with the lowest lexically-sorted
+ oid). The mtimes count standard epoch seconds.
This paragraph seemed awkward. Here is a rephrasing that might be
less awkward:
- A table of 4-byte unsigned integers in network order. The ith value
is the modified time (mtime) of the ith object of the corresponding
pack in lexicographic order. The mtime represents standard epoch
seconds.
Thanks, this is clearer. I went with a blend of the two:
- A table of 4-byte unsigned integers in network order. The ith
value is the modification time (mtime) of the ith object in the
corresponding pack by lexicographic (index) order. The mtimes
count standard epoch seconds.
Storing these mtimes in 32-bits means we will hit the 2038 problem.
The commit-graph stores commit times with an extra two bits to extend
the lifetime by another hundred years or so.
Could we extend the lifetime of cruft packs by decreasing the granularity
here? Should 'mtime' store a number of _minutes_ instead of seconds? That
should be enough granularity for these purposes.
Perhaps, though it does add some complexity to the code that deals with
this format at the expense of some future-proofing. I'm open to it,
though.
quoted
+ - A trailer, containing a:
+
+ checksum of the corresponding packfile, and
+
+ a checksum of all of the above.
Could you specify the checksum as having length according to the
specified hash function?
Great suggestion, thanks.
quoted
+All 4-byte numbers are in network order.
+
Maybe this could be at the start of the format, since the file
version and hash function are both 4-byte numbers here and we
could remove the mention of network order from the mtime values.
This is copy-and-pasted from the .rev section above, where I think I
added the "All 4-byte numbers are in network order" bit at the end in
response to a suggestion opposite yours ;).
Here I would probably rather stay consistent with the surrounding
sections.
quoted
+static char *pack_mtimes_filename(struct packed_git *p)
+{
+ size_t len;
+ if (!strip_suffix(p->pack_name, ".pack", &len))
+ BUG("pack_name does not end in .pack");
+ /* NEEDSWORK: this could reuse code from pack-revindex.c. */
+ return xstrfmt("%.*s.mtimes", (int)len, p->pack_name);
+}
I see your NEEDSWORK here and you are probably referring to this:
static char *pack_revindex_filename(struct packed_git *p)
{
size_t len;
if (!strip_suffix(p->pack_name, ".pack", &len))
BUG("pack_name does not end in .pack");
return xstrfmt("%.*s.rev", (int)len, p->pack_name);
}
and the implementation is identical except for the new trailer
(which exist in the exts[] array in builtin/repack.c, but could
also be pulled out into a header somewhere.
I'm happy to delay any cleanup of these code clones until later,
if at all, because doing it right might mean moving more code
than we like. Such refactorings aren't worth it most of the time.
Yeah, I think your thoughts matched my own when writing this. Which is
to say, I felt it prudent to call out that there is an opportunity to
DRY these two up, but I'm not convinced that such a clean up would be
worthwhile.
+ if (mtimes_size - MTIMES_MIN_SIZE != st_mult(sizeof(uint32_t), num_objects)) {
+ ret = error(_("mtimes file %s is corrupt"), mtimes_file);
This message could be more informative: "mtimes file %s has the wrong size"?
Copy-and-pasting here again from the corresponding code for the .rev
file, which is why I didn't opt to change the message here. Probably
many of these checks could be extracted out and shared between the two
paths, but I don't think we should attempt it here.
quoted
+ data = hdr = xmmap(NULL, mtimes_size, PROT_READ, MAP_PRIVATE, fd, 0);
+
+ if (ntohl(*hdr) != MTIMES_SIGNATURE) {
+ ret = error(_("mtimes file %s has unknown signature"), mtimes_file);
+ goto cleanup;
+ }
Interesting that you defined 'struct mtimes_header' before this
method, but don't use it here (in favor of moving a uint32_t
pointer). Perhaps you are avoiding pointing the struct at the
memory map, but you could also do this:
struct mtimes_header header;
header.signature = ntohl(hdr[0]);
header.version = ntohl(hdr[1]);
header.hash_id = ntohl(hdr[2]);
And then operate on the struct for your validation.
At the very least, 'struct mtimes_header' is defined but not
used in this patch. If you decide to not use it this way, then
maybe delay its definition.
Yeah, not reading directly out of the struct is intentional, since the
compiler is free to insert padding between these members, which would
break any subsequent reads out of the struct.
But I like your idea to assign the fields manually, thanks!
quoted
+int load_pack_mtimes(struct packed_git *p)
+{
+ char *mtimes_name = NULL;
+ int ret = 0;
+
+ if (!p->is_cruft)
+ return ret; /* not a cruft pack */
Interesting that this indicator is essentially "we have an mtimes
file for this pack", but it makes sense to include that check next
to the .keep and .promisor checks.
I think I had originally called it "mtimes" but changed it to "cruft",
since it makes sense as a prefix similar to the others (that is, "keep
pack", "promisor pack", and "cruft pack", not "mtimes pack").
The hunks I did not comment on look good. Nice standard file format
stuff.
From: Taylor Blau <hidden> Date: 2021-12-03 22:40:43
On Thu, Dec 02, 2021 at 10:22:05AM -0500, Derrick Stolee wrote:
I notice that you don't use this in load_pack_mtimes_file(),
in pack-mtimes.c but you could at this point.
Hmm, I'm confused. Te extracted function converts a pointer to a struct
git_hash_algo into a uint32, but here we just care about reading the
four byte value we wrote.
Thanks,
Taylor
When writing a pack, it appears that the cruft_mtime array
maps to objects in pack-order, not idx-order, correct? That
might be worth mentioning in the struct definition because
it differs from the .mtimes file.
Great observation and suggestion, thank you! The comment that I
ultimately settled on is:
/*
* Used when writing cruft packs.
*
* Object mtimes are stored in pack order when writing, but
* written out in lexicographic (index) order.
*/
uint32_t *cruft_mtime;
The name "objects" here confused me at first, thinking it
corresponded to the objects member of 'struct packing_data', but
that is being handled by the fact that 'objects' is actually a
lex-sorted list of pack_idx_entry pointers (and they happen to
also point to 'struct object_entry' values because the 'struct
pack_idx_entry' is the first member.
So this is (very densely) handling the translation from pack-order
to lex-order through the double pointer 'objects'. I'm not sure if
there is a way to make it more clear or if every reader will need
to do the same mental gymnastics I had to do.
Exactly, and sorry that I didn't point this out more clearly. It's been
long enough since I wrote this code that I can sympathize with the
mental gymnastics required ;).
On Mon, Nov 29, 2021 at 7:29 PM Taylor Blau [off-list ref] wrote:
quoted hunk
Create a technical document to explain cruft packs. It contains a brief
overview of the problem, some background, details on the implementation,
and a couple of alternative approaches not considered here.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/Makefile | 1 +
Documentation/technical/cruft-packs.txt | 95 +++++++++++++++++++++++++
2 files changed, 96 insertions(+)
create mode 100644 Documentation/technical/cruft-packs.txt
@@ -0,0 +1,95 @@+= Cruft packs++Cruft packs offer an alternative to Git's traditional mechanism of removing+unreachable objects. This document provides an overview of Git's pruning+mechanism, and how cruft packs can be used instead to accomplish the same.++== Background++To remove unreachable objects from your repository, Git offers `git repack -Ad`+(see linkgit:git-repack[1]). Quoting from the documentation:++[quote]+[...] unreachable objects in a previous pack become loose, unpacked objects,+instead of being left in the old pack. [...] loose unreachable objects will be+pruned according to normal expiry rules with the next 'git gc' invocation.++Unreachable objects aren't removed immediately, since doing so could race with+an incoming push which may reference an object which is about to be deleted.+Instead, those unreachable objects are stored as loose object and stay that way+until they are older than the expiration window, at which point they are removed+by linkgit:git-prune[1].++Git must store these unreachable objects loose in order to keep track of their+per-object mtimes. If these unreachable objects were written into one big pack,+then either freshening that pack (because an object contained within it was+re-written) or creating a new pack of unreachable objects would cause the pack's+mtime to get updated, and the objects within it would never leave the expiration+window. Instead, objects are stored loose in order to keep track of the+individual object mtimes and avoid a situation where all cruft objects are+freshened at once.++This can lead to undesirable situations when a repository contains many+unreachable objects which have not yet left the grace period. Having large+directories in the shards of `.git/objects` can lead to decreased performance in+the repository. But given enough unreachable objects, this can lead to inode+starvation and degrade the performance of the whole system. Since we+can never pack those objects, these repositories often take up a large amount of+disk space, since we can only zlib compress them, but not store them in delta+chains.++== Cruft packs++Cruft packs are designed to eliminate the need for storing unreachable objects+in a loose state by including the per-object mtimes in a separate file alongside+a single pack containing all loose objects.
I had the same question as Stolee here: why not use the cruft-pack's
mtime for all the objects in it? Much later below, you make it clear
that a repository will generally only have one cruft pack which kind
of answers the question, but the repeated mention of "cruft packs"
throughout the document subtly made me make the opposite assumption.
It might be nice to address the almost-always-only-one-cruft-pack
earlier on, which may also help answer the question about why you need
to store individual mtimes in an additional file.
+A cruft pack is written by `git repack --cruft` when generating a new pack.
+linkgit:git-pack-objects[1]'s `--cruft` option. Note that `git repack --cruft`
+is a classic all-into-one repack, meaning that everything in the resulting pack is
+reachable, and everything else is unreachable. Once written, the `--cruft`
+option instructs `git repack` to generate another pack containing only objects
+not packed in the previous step (which equates to packing all unreachable
+objects together). This progresses as follows:
+
+ 1. Enumerate every object, marking any object which is (a) not contained in a
+ kept-pack, and (b) whose mtime is within the grace period as a traversal
+ tip.
+
+ 2. Perform a reachability traversal based on the tips gathered in the previous
+ step, adding every object along the way to the pack.
+
+ 3. Write the pack out, along with a `.mtimes` file that records the per-object
+ timestamps.
+
+This mode is invoked internally by linkgit:git-repack[1] when instructed to
+write a cruft pack. Crucially, the set of in-core kept packs is exactly the set
+of packs which will not be deleted by the repack; in other words, they contain
+all of the repository's reachable objects.
+
+When a repository already has a cruft pack, `git repack --cruft` typically only
+adds objects to it. An exception to this is when `git repack` is given the
+`--cruft-expiration` option, which allows the generated cruft pack to omit
+expired objects instead of waiting for linkgit:git-gc[1] to expire those objects
+later on.
+
+It is linkgit:git-gc[1] that is typically responsible for removing expired
+unreachable objects.
+
+== Alternatives
+
+Notable alternatives to this design include:
+
+ - The location of the per-object mtime data, and
+ - Whether cruft packs should be incremental or not.
+
+On the location of mtime data, a new auxiliary file tied to the pack was chosen
+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain
+support for optional chunks of data, it may make sense to consolidate the
+`.mtimes` format into the `.idx` itself.
+
+Incremental cruft packs (i.e., where each time a repository is repacked a new
+cruft pack is generated containing only the unreachable objects introduced since
+the last time a cruft pack was written) are significantly more complicated to
+construct, and so aren't pursued here. The obvious drawback to the current
+implementation is that the entire cruft pack must be re-written from scratch.
--
2.34.1.25.gb3157a20e6
From: Taylor Blau <hidden> Date: 2021-12-04 23:32:56
On Sat, Dec 04, 2021 at 02:20:23PM -0800, Elijah Newren wrote:
quoted
+== Cruft packs
+
+Cruft packs are designed to eliminate the need for storing unreachable objects
+in a loose state by including the per-object mtimes in a separate file alongside
+a single pack containing all loose objects.
I had the same question as Stolee here: why not use the cruft-pack's
mtime for all the objects in it? Much later below, you make it clear
that a repository will generally only have one cruft pack which kind
of answers the question, but the repeated mention of "cruft packs"
throughout the document subtly made me make the opposite assumption.
It might be nice to address the almost-always-only-one-cruft-pack
earlier on, which may also help answer the question about why you need
to store individual mtimes in an additional file.
Responding to your suggestions out of order ;-). Throughout the
document, I wrote "cruft packs" in the sense of "the feature this series
implements", not "multiple cruft packs".
But my wording is unintentionally vague, especially because this
document does talk about why this series stores unreachable objects in a
single cruft pack. I updated my copy to make clear the difference
between the two, which should hopefully avoid any confusion here in the
future.
As far as why not use the cruft pack's timestamp as the mtime for all of
the unreachable objects contained within it, there are a few reasons:
It makes freshening objects more complicated. Not because we couldn't
freshen individual objects (we would likely do so in the same way this
series does, by rewriting it loose and using the loose copy's mtime
instead), but because it makes it complicated to repack a repository
with many cruft packs. If I have a handful of cruft packs, and freshen a
handful of objects within them, I now need to update many cruft packs,
or pay the price of storing their objects twice (if I instead don't
rewrite them and keep the loose copies around).
It also makes it impossible to share deltas between cruft objects that
don't have the same timestamp, unless the cruft packs are stored thin
(in which case it becomes much more complicated to figure out which
cruft packs can be safely pruned without storing information about which
other packs a thin pack has deltas against).
I'm sure there were others, but these are the ones that I could recall
off the top of my head. This all felt like a little too much detail for
the "alternative designs" section, but if you think some or all of this
would be interesting to memorialize not just on the mailing list, let me
know.
Thanks,
Taylor
On Thu, Dec 02, 2021 at 10:22:05AM -0500, Derrick Stolee wrote:
quoted
I notice that you don't use this in load_pack_mtimes_file(),
in pack-mtimes.c but you could at this point.
Hmm, I'm confused. Te extracted function converts a pointer to a struct
git_hash_algo into a uint32, but here we just care about reading the
four byte value we wrote.
nit: you return an int here so you can use it as an error code...
+{
+ uint32_t i;
+ if (load_pack_mtimes(p) < 0)
+ die("could not load pack .mtimes");
+
+ for (i = 0; i < p->num_objects; i++) {
+ struct object_id oid;
+ if (nth_packed_object_id(&oid, p, i) < 0)
+ die("could not load object id at position %"PRIu32, i);
+
+ printf("%s %"PRIu32"\n",
+ oid_to_hex(&oid), nth_packed_mtime(p, i));
+ }
+
+ return 0;
But always return 0 unless you die().
+ return p ? dump_mtimes(p) : 1;
It makes this line concise, I suppose.
Perhaps just use "return dump_mtimes(p)" and have dump_mtimes()
return 1 if the given pack is NULL?
Thanks,
-Stolee
Generating a non-expiring cruft packs works as follows:
I had trouble parsing the documentation changes below, so I came back
to this commit message to see if that helps.
- Callers provide a list of every pack they know about, and indicate
which packs are about to be removed.
This corresponds to the list over stdin.
- All packs which are going to be removed (we'll call these the
redundant ones) are marked as kept in-core, as well as any packs
that `pack-objects` found but the caller did not specify.
Ok, so as an implementation detail we mark these as keep packs.
These packs are presumed to have entered the repository between
the caller collecting packs and invoking `pack-objects`. Since we
do not want to include objects in these packs (because we don't know
which of their objects are or aren't reachable), these are also
marked as kept in-core.
Here, "are presumed" is doing a lot of work. Theoretically, there could
be three categories:
1. This pack was just repacked and will be removed because all of its
objects were placed into new objects.
2. Either this pack was repacked and contains important reachable objects
OR we did a repack of reachable objects and this pack contained some
extra, unreachable objects.
3. This pack was added to the repository while creating those repacked
packs from category 2, so we don't know if things are reachable or
not.
So, the packs that we discover on-disk but are not specified over stdin
are in this third category, but these are grouped with category 1 as we
will treat them the same.
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
Here, we are looking at all of the objects in category 2 as well as
loose objects.
This results in a new cruft pack which contains all known objects that
aren't included in the kept packs. When the kept pack is the result of
`git repack -A`, the resulting pack contains all unreachable objects.
This now describes how 'git repack' will interface with this new change
to pack-objects. I'll keep an eye out for that.
+--cruft::
Now getting to this description.
+ Packs unreachable objects into a separate "cruft" pack, denoted
+ by the existence of a `.mtimes` file. Pack names provided over
+ stdin indicate which packs will remain after a `git repack`.
+ Pack names prefixed with a `-` indicate those which will be
+ removed. (...)
This description is too tied to 'git repack'. Can we describe the
input using terms independent of the 'git repack' operation? I need
to keep reading.
(...) The contents of the cruft pack are all objects not
+ contained in the surviving packs specified by `--keep-pack`)
Now you use --keep-pack, which is a way of specifying a pack as
"in-core keep" which was not in your commit message. Here, we also
don't link the packs over stdin to the concept of keep packs.
+ which have not exceeded the grace period (see
+ `--cruft-expiration` below), or which have exceeded the grace
+ period, but are reachable from an other object which hasn't.
And now we think about the grace period! There is so much going on
that I need to break it down to understand.
An object is _excluded_ from the new cruft pack if
1. It is reachable from at least one reference.
2. It is in a pack from stdin prefixed with "-"
3. It is in a pack specified by `--keep-pack`
4. It is in an existing cruft pack and the .mtimes file states
that its mtime is at least as recent as the time specified by
the --cruft-expiration option.
Breaking it down into a list like this helps me, at least. I'm not
sure what the best way would look like.
(Needing to pause here and look at the implementation later.)
Thanks,
-Stolee
I don't love the global nr_seen here, but it is pervasive through the
file. OK.
+ entry = packlist_find(&to_pack, oid);
+ if (entry) {
+ if (name) {
+ entry->hash = pack_name_hash(name);
+ entry->no_try_delta = name && no_try_delta(name);
This is already in an "if (name)" block, so "name &&" isn't needed.
+ }
+ } else {
+ if (!want_object_in_pack(oid, 0, &pack, &offset))
+ return 0;
+ if (!pack && type == OBJ_BLOB && !has_loose_object(oid)) {
+ /*
+ * If a traversed tree has a missing blob then we want
+ * to avoid adding that missing object to our pack.
+ *
+ * This only applies to missing blobs, not trees,
+ * because the traversal needs to parse sub-trees but
+ * not blobs.
+ *
+ * Note we only perform this check when we couldn't
+ * already find the object in a pack, so we're really
+ * limited to "ensure non-tip blobs which don't exist in
+ * packs do exist via loose objects". Confused?
+ */
+ return 0;
+ }
+
+ entry = create_object_entry(oid, type, pack_name_hash(name),
+ 0, name && no_try_delta(name),
+ pack, offset);
+ }
+
+ if (mtime > oe_cruft_mtime(&to_pack, entry))
+ oe_set_cruft_mtime(&to_pack, entry, mtime);
+ return 1;
I was confused at this "return 1" here, while other cases return 0.
It turns out that there are multiple methods in this file that have
different semantics: add_loose_object() and add_object_entry_from_pack()
are both called from iterators where "return 1" means "stop iterating"
so they return 0 always. add_object_entry_from_bitmap() is used to
iterate over a bitmap and "return 1" means "include this object".
However, the return code for add_cruft_object_entry() is never used,
so it should probably return void or swap the meanings to have nonzero
mean an error occurred.
Interesting that this is a potential issue. We are expecting the pack
to be loaded before we get here. Is this more because some packs might
not actually load, but it's fine as long as we don't mark them as kept?
Here is a global that we are suddenly changing. Should we not be
returning it to its initial state when this method is complete?
+static int option_parse_cruft_expiration(const struct option *opt,
+ const char *arg, int unset)
+{
+ if (unset) {
+ cruft = 0;
This unassignment of 'cruft' when cruft-expiration is unset with
--no-cruft-expiration seems odd. I would expect
git pack-objects --cruft --no-cruft-expiration
to still make a cruft pack, but not expire anything. It seems that
your code here makes --no-cruft-expiration disable the --cruft option.
+ /*
+ * Re-mark only the fresh packs as kept so that objects in
+ * unknown packs do not halt the reachability traversal early.
+ */
+ for (p = get_all_packs(the_repository); p; p = p->next)
+ p->pack_keep_in_core = 0;
+ mark_pack_kept_in_core(fresh_packs, 1);
Are we ever going to recover this pack_keep_in_core state? Should we
be saving it somewhere so we can return without mutating this state
permanently?
@@ -3515,7 +3597,7 @@ static void read_cruft_objects(void) mark_pack_kept_in_core(&discard_packs, 0); if (cruft_expiration)- die("--cruft-expiration not yet implemented");+ enumerate_and_traverse_cruft_objects(&fresh_packs); else enumerate_cruft_objects();
basic_cruft_pack_tests never
+basic_cruft_pack_tests 2.weeks.ago
I'm surprised these tests didn't require any changes to adapt to the
new expiration date. But I suppose none of the mtimes were older than
two weeks ago?
I continue to miss something in these tests, because I don't see how
things are becoming unreachable.
Thanks,
-Stolee
I can understand the use of OPT_BIT here. Keep in mind that --no-cruft would
remove the '-a' option, if it already existed. Perhaps we should just use
OPT_BOOL and update to add the ALL_INTO_ONE if PACK_CRUFT exists?
Here, --no-cruft-expiration will set cruft_expiration to NULL and not overwrite
the --cruft option, as expected. Just pointing out that this is different than
the option in 'git pack-objects'.
From: Taylor Blau <hidden> Date: 2022-01-07 19:41:49
On Fri, Dec 03, 2021 at 05:24:03PM -0500, Taylor Blau wrote:
On Thu, Dec 02, 2021 at 10:06:07AM -0500, Derrick Stolee wrote:
- A table of 4-byte unsigned integers in network order. The ith
value is the modification time (mtime) of the ith object in the
corresponding pack by lexicographic (index) order. The mtimes
count standard epoch seconds.
quoted
Storing these mtimes in 32-bits means we will hit the 2038 problem.
The commit-graph stores commit times with an extra two bits to extend
the lifetime by another hundred years or so.
Could we extend the lifetime of cruft packs by decreasing the granularity
here? Should 'mtime' store a number of _minutes_ instead of seconds? That
should be enough granularity for these purposes.
Perhaps, though it does add some complexity to the code that deals with
this format at the expense of some future-proofing. I'm open to it,
though.
I still have quite a bit of review from this topic sitting in my inbox.
But this had been lingering on my mind, and I realized I said something
incorrect. 32-bit mtimes won't cause us to run into the "2038" problem,
since these aren't signed values. So storing epoch seconds in a uint32_t
should get us into the year 2106.
If anybody is still using cruft packs by then, I'll call this project a
wild success ;-). So in the meantime, I don't think it makes sense to
reduce the granularity and/or use extra bits to store the timestamps.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-02-23 22:24:56
On Mon, Dec 06, 2021 at 04:16:04PM -0500, Derrick Stolee wrote:
On 11/29/2021 5:25 PM, Taylor Blau wrote:
quoted
+static int dump_mtimes(struct packed_git *p)
nit: you return an int here so you can use it as an error code...
quoted
+{
+ uint32_t i;
+ if (load_pack_mtimes(p) < 0)
+ die("could not load pack .mtimes");
+
+ for (i = 0; i < p->num_objects; i++) {
+ struct object_id oid;
+ if (nth_packed_object_id(&oid, p, i) < 0)
+ die("could not load object id at position %"PRIu32, i);
+
+ printf("%s %"PRIu32"\n",
+ oid_to_hex(&oid), nth_packed_mtime(p, i));
+ }
+
+ return 0;
But always return 0 unless you die().
quoted
+ return p ? dump_mtimes(p) : 1;
It makes this line concise, I suppose.
Perhaps just use "return dump_mtimes(p)" and have dump_mtimes()
return 1 if the given pack is NULL?
I think just dying in the case we have a NULL pack is fine, and it
should be OK to lump it in the same case as "could not load pack .mtimes".
But we may want to catch the case a little earlier while we still have
the pack name handy. Perhaps something like this on top:
I don't love the global nr_seen here, but it is pervasive through the
file. OK.
Yeah; this is how all of the existing progress code works in
pack-objects.
quoted
+ entry = packlist_find(&to_pack, oid);
+ if (entry) {
+ if (name) {
+ entry->hash = pack_name_hash(name);
+ entry->no_try_delta = name && no_try_delta(name);
This is already in an "if (name)" block, so "name &&" isn't needed.
Thanks; this is a copy-and-paste from add_object_entry(), where we
aren't in a conditional on "name". We could also fold the conditional on
whether or not name is NULL into no_try_delta itself, since all existing
calls look like "name && no_try_delta(name)".
So adding something like:
if (!name)
return 0;
to the beginning of no_try_delta()'s implementation would allow us to
get rid of the handful of "name &&"s. But I'm trying to avoid touching
other parts of pack-objects as much as I can, so I'll hold off for now.
quoted
+ }
+ } else {
+ if (!want_object_in_pack(oid, 0, &pack, &offset))
+ return 0;
+ if (!pack && type == OBJ_BLOB && !has_loose_object(oid)) {
+ /*
+ * If a traversed tree has a missing blob then we want
+ * to avoid adding that missing object to our pack.
+ *
+ * This only applies to missing blobs, not trees,
+ * because the traversal needs to parse sub-trees but
+ * not blobs.
+ *
+ * Note we only perform this check when we couldn't
+ * already find the object in a pack, so we're really
+ * limited to "ensure non-tip blobs which don't exist in
+ * packs do exist via loose objects". Confused?
+ */
+ return 0;
+ }
+
+ entry = create_object_entry(oid, type, pack_name_hash(name),
+ 0, name && no_try_delta(name),
+ pack, offset);
+ }
+
+ if (mtime > oe_cruft_mtime(&to_pack, entry))
+ oe_set_cruft_mtime(&to_pack, entry, mtime);
+ return 1;
I was confused at this "return 1" here, while other cases return 0.
It turns out that there are multiple methods in this file that have
different semantics: add_loose_object() and add_object_entry_from_pack()
are both called from iterators where "return 1" means "stop iterating"
so they return 0 always. add_object_entry_from_bitmap() is used to
iterate over a bitmap and "return 1" means "include this object".
However, the return code for add_cruft_object_entry() is never used,
so it should probably return void or swap the meanings to have nonzero
mean an error occurred.
Yes, exactly. And thanks for tracing out both of the different
meanings/interpretations of these add_xyz_entry() functions. As you can
imagine, this implementation is copy-and-pasted from add_object_entry(),
which was specialized for this use here. At the time, I gave some effort
towards trying to share more code with add_object_entry() for this
special case, but it ended up being pretty awkward, hence the separate
implementation.
Ironically, add_object_entry()'s return code is also unused, so we could
probably clean that up, too. But like the above, I'll avoid it for now
in an effort to touch as little of pack-objects in this patch as I can.
Interesting that this is a potential issue. We are expecting the pack
to be loaded before we get here. Is this more because some packs might
not actually load, but it's fine as long as we don't mark them as kept?
Not quite "loaded" (though any pack structures that we look at by this
point will be fully "loaded"). Instead, we're making sure that all of
the packs names we read from stdin could be matched to packs that we
found in the repository (i.e., that we produce an appropriate error
message if we found "pack-does-not-exist.pack" on stdin).
This is all because we process input from stdin in two phases:
- First, read all of the input into two string_lists, one for the
packs we're about to discard (anything that start with '-'), and
another for all of the "fresh" packs (i.e., anything that we're not
going to discard).
- Then, loop through all of the packed_git structs we have, querying
both of the aforementioned string lists for input that matches each
pack's `pack_name` field, and setting the `->util` pointer of the
matching string_list_entry appropriately.
Following those two steps, any list entries that have a NULL util
pointer correspond with bogus input, so we want to call die() there.
Here is a global that we are suddenly changing. Should we not be
returning it to its initial state when this method is complete?
We could, although it won't matter in practice, because we'll want to
keep that setting around for our traversal, after which point
pack-objects will exit.
quoted
+static int option_parse_cruft_expiration(const struct option *opt,
+ const char *arg, int unset)
+{
+ if (unset) {
+ cruft = 0;
This unassignment of 'cruft' when cruft-expiration is unset with
--no-cruft-expiration seems odd. I would expect
git pack-objects --cruft --no-cruft-expiration
to still make a cruft pack, but not expire anything. It seems that
your code here makes --no-cruft-expiration disable the --cruft option.
Hmm. I could see compelling reasoning that goes both ways. On the one
hand, `--no-cruft-expiration` (to me, at least) seems to imply "set
`--cruft-expiration` to "never"). On the other hand, it also matches our
convention of `--no`-prefixed options to unset some value. This
implementation takes the latter approach, though we could easily change
it to set the cruft expiration to "never".
I don't have a strong opinion about which is better, so I'm happy to do
either if you have a better sense about which has more expected
behavior.
I am missing how this test creates _unreachable_ objects. I would expect removal of
some refs or a 'git reset --hard' somewhere. What am I missing?
For this and the other tests the so-called "unreachable" objects are
technically reachable, but we can treat them as unreachable by putting
them in the "discard" packs list (or by not mentioning them at all to
`git pack-objects --cruft`).
quoted
+ # remove the unreachable tree, but leave the commit
+ # which has it as its root tree in-tact
nit: "intact" is one word.
Thanks; fixed here and in the other test which was added by this commit.
Thanks,
Taylor
+ /*
+ * Re-mark only the fresh packs as kept so that objects in
+ * unknown packs do not halt the reachability traversal early.
+ */
+ for (p = get_all_packs(the_repository); p; p = p->next)
+ p->pack_keep_in_core = 0;
+ mark_pack_kept_in_core(fresh_packs, 1);
Are we ever going to recover this pack_keep_in_core state? Should we
be saving it somewhere so we can return without mutating this state
permanently?
In the same sense that we are free to modify the global
ignore_packed_keep_in_core variable (because we only stop caring about
the modified state right before the program is about to exist) we can
freely mutate these variables, too.
@@ -3515,7 +3597,7 @@ static void read_cruft_objects(void) mark_pack_kept_in_core(&discard_packs, 0); if (cruft_expiration)- die("--cruft-expiration not yet implemented");+ enumerate_and_traverse_cruft_objects(&fresh_packs); else enumerate_cruft_objects();
quoted
basic_cruft_pack_tests never
+basic_cruft_pack_tests 2.weeks.ago
I'm surprised these tests didn't require any changes to adapt to the
new expiration date. But I suppose none of the mtimes were older than
two weeks ago?
From: Taylor Blau <hidden> Date: 2022-02-23 23:37:54
(Jumping forward a little bit while responding to your review to finish
my train of though before I log off for today...)
On Tue, Dec 07, 2021 at 10:38:05AM -0500, Derrick Stolee wrote:
@@ -358,4 +358,157 @@ test_expect_success 'expired objects are pruned' ')'+test_expect_success'repack --cruft generates a cruft pack''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitbranch-Mmain&&+gitcheckout--orphanother&&
Here is a way to make objects unreachable!
Yes, indeed. And this is the first spot where we *need* to care about
object reachability, because the set of packs that `git repack` passes
over stdin to `git pack-objects --cruft` depends on which objects are
and aren't reachable.
In the tests that exercise `pack-objects --cruft` directly, we can
pretend that certain packs contain only unreachable objects by marking
them as "discarded".
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-03-01 02:48:37
On Mon, Dec 06, 2021 at 04:44:31PM -0500, Derrick Stolee wrote:
On 11/29/2021 5:25 PM, Taylor Blau wrote:
quoted
Generating a non-expiring cruft packs works as follows:
I had trouble parsing the documentation changes below, so I came back
to this commit message to see if that helps.
quoted
- Callers provide a list of every pack they know about, and indicate
which packs are about to be removed.
This corresponds to the list over stdin.
quoted
- All packs which are going to be removed (we'll call these the
redundant ones) are marked as kept in-core, as well as any packs
that `pack-objects` found but the caller did not specify.
Ok, so as an implementation detail we mark these as keep packs.
quoted
These packs are presumed to have entered the repository between
the caller collecting packs and invoking `pack-objects`. Since we
do not want to include objects in these packs (because we don't know
which of their objects are or aren't reachable), these are also
marked as kept in-core.
Here, "are presumed" is doing a lot of work. Theoretically, there could
be three categories:
1. This pack was just repacked and will be removed because all of its
objects were placed into new objects.
2. Either this pack was repacked and contains important reachable objects
OR we did a repack of reachable objects and this pack contained some
extra, unreachable objects.
3. This pack was added to the repository while creating those repacked
packs from category 2, so we don't know if things are reachable or
not.
So, the packs that we discover on-disk but are not specified over stdin
are in this third category, but these are grouped with category 1 as we
will treat them the same.
Ah, I think I caused some unintentional confusion by attaching "are
presumed" to "these packs", when it wasn't clear that "these packs"
meant "ones that aren't listed over stdin".
Since the caller is supposed to provide a complete picture of the
repository as they see it, any packs known to the pack-objects process
that aren't mentioned over stdin are assumed to have entered the
repository after the caller was spun up.
I'll clarify this section of the commit message, since I agree it is
unnecessarily confusing.
quoted
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
Here, we are looking at all of the objects in category 2 as well as
loose objects.
We're enumerating any objects that aren't in packs which are marked as
kept in-core (along with loose objects which don't appear in packs that
are marked as kept in-core).
The in-core kept packs are ones that the caller (and I find it's helpful
to read "the caller" as "git repack") has marked as "will delete". So
the non in-core pack(s) that we're looking at here contain all reachable
objects (e.g., like you would get with `git repack -A`).
quoted
+ Packs unreachable objects into a separate "cruft" pack, denoted
+ by the existence of a `.mtimes` file. Pack names provided over
+ stdin indicate which packs will remain after a `git repack`.
+ Pack names prefixed with a `-` indicate those which will be
+ removed. (...)
This description is too tied to 'git repack'. Can we describe the
input using terms independent of the 'git repack' operation? I need
to keep reading.
quoted
(...) The contents of the cruft pack are all objects not
+ contained in the surviving packs specified by `--keep-pack`)
Now you use --keep-pack, which is a way of specifying a pack as
"in-core keep" which was not in your commit message. Here, we also
don't link the packs over stdin to the concept of keep packs.
The mention of `--keep-pack` is a mistake left over from a previous
version; thanks for spotting. Here's a version of the first paragraph
from this piece of documentation which is less tied to `git repack` and
hopefully a little clearer:
--cruft::
Packs unreachable objects into a separate "cruft" pack, denoted
by the existence of a `.mtimes` file. Typically used by `git
repack --cruft`. Callers provide a list of pack names and
indicate which packs will remain in the repository, along with
which packs will be deleted (indicated by the `-` prefix). The
contents of the cruft pack are all objects not contained in the
surviving packs which have not exceeded the grace period (see
`--cruft-expiration` below), or which have exceeded the grace
period, but are reachable from an other object which hasn't.
quoted
+ which have not exceeded the grace period (see
+ `--cruft-expiration` below), or which have exceeded the grace
+ period, but are reachable from an other object which hasn't.
And now we think about the grace period! There is so much going on
that I need to break it down to understand.
An object is _excluded_ from the new cruft pack if
1. It is reachable from at least one reference.
2. It is in a pack from stdin prefixed with "-"
3. It is in a pack specified by `--keep-pack`
4. It is in an existing cruft pack and the .mtimes file states
that its mtime is at least as recent as the time specified by
the --cruft-expiration option.
Breaking it down into a list like this helps me, at least. I'm not
sure what the best way would look like.
Given some expiration T, cruft packs contain all unreachable objects
which are newer than T, along with any cruft objects (i.e., those not
directly reachable from any ref) which are older than T, but reachable
from another cruft object newer than T.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:04
Here is a reroll of my series to implement "cruft packs", a pack which
stores accumulated unreachable objects, along with a new ".mtimes" file
which tracks each object's last known modification time.
This was on the list towards the end of 2021[1], and I have been
accumulating small changes to it locally for a couple of months now.
Major changes since last time include:
- Clearer documentation and commit message(s) to better illustrate how
the feature works and is supposed to be used.
- Some minor documentation updates to pack-format.txt, which make some
ambiguous details more explicit.
- Minor code movement / tweaks to make things easier to read, ensure
that functions aren't introduced in patches before they are used /
etc.
- Moved the new test script to t5328 (instead of t5327, which happens
to be taken up by a new MIDX bitmap-related test), and purged it of
all "rm -fr .git/logs" (replacing them with "git reflog --expire
--all --expire=all" instead).
- A new test which fixes a bug where loose objects which have copies
that appear in a cruft pack would not get accumulated when doing a
`--geometric` repack.
For convenience, a range-diff is below. Thanks in advance for taking
another look!
[1]: https://lore.kernel.org/git/cover.1638224692.git.me@ttaylorr.com/
Taylor Blau (17):
Documentation/technical: add cruft-packs.txt
pack-mtimes: support reading .mtimes files
pack-write: pass 'struct packing_data' to 'stage_tmp_packfiles'
chunk-format.h: extract oid_version()
pack-mtimes: support writing pack .mtimes files
t/helper: add 'pack-mtimes' test-tool
builtin/pack-objects.c: return from create_object_entry()
builtin/pack-objects.c: --cruft without expiration
reachable: add options to add_unseen_recent_objects_to_traversal
reachable: report precise timestamps from objects in cruft packs
builtin/pack-objects.c: --cruft with expiration
builtin/repack.c: support generating a cruft pack
builtin/repack.c: allow configuring cruft pack generation
builtin/repack.c: use named flags for existing_packs
builtin/repack.c: add cruft packs to MIDX during geometric repack
builtin/gc.c: conditionally avoid pruning objects via loose
sha1-file.c: don't freshen cruft packs
Documentation/Makefile | 1 +
Documentation/config/gc.txt | 21 +-
Documentation/config/repack.txt | 9 +
Documentation/git-gc.txt | 5 +
Documentation/git-pack-objects.txt | 30 +
Documentation/git-repack.txt | 11 +
Documentation/technical/cruft-packs.txt | 97 ++++
Documentation/technical/pack-format.txt | 19 +
Makefile | 2 +
builtin/gc.c | 10 +-
builtin/pack-objects.c | 304 +++++++++-
builtin/repack.c | 183 +++++-
bulk-checkin.c | 2 +-
chunk-format.c | 12 +
chunk-format.h | 3 +
commit-graph.c | 18 +-
midx.c | 18 +-
object-file.c | 4 +-
object-store.h | 7 +-
pack-mtimes.c | 129 +++++
pack-mtimes.h | 15 +
pack-objects.c | 6 +
pack-objects.h | 25 +
pack-write.c | 93 ++-
pack.h | 4 +
packfile.c | 19 +-
reachable.c | 58 +-
reachable.h | 9 +-
t/helper/test-pack-mtimes.c | 56 ++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t5328-pack-objects-cruft.sh | 739 ++++++++++++++++++++++++
32 files changed, 1810 insertions(+), 101 deletions(-)
create mode 100644 Documentation/technical/cruft-packs.txt
create mode 100644 pack-mtimes.c
create mode 100644 pack-mtimes.h
create mode 100644 t/helper/test-pack-mtimes.c
create mode 100755 t/t5328-pack-objects-cruft.sh
Range-diff against v1:
1: a9f7c738e0 ! 1: 784ee7e0ee Documentation/technical: add cruft-packs.txt
@@ Documentation/technical/cruft-packs.txt (new)
@@
+= Cruft packs
+
-+Cruft packs offer an alternative to Git's traditional mechanism of removing
-+unreachable objects. This document provides an overview of Git's pruning
-+mechanism, and how cruft packs can be used instead to accomplish the same.
++The cruft packs feature offer an alternative to Git's traditional mechanism of
++removing unreachable objects. This document provides an overview of Git's
++pruning mechanism, and how a cruft pack can be used instead to accomplish the
++same.
+
+== Background
+
@@ Documentation/technical/cruft-packs.txt (new)
+
+== Cruft packs
+
-+Cruft packs are designed to eliminate the need for storing unreachable objects
-+in a loose state by including the per-object mtimes in a separate file alongside
-+a single pack containing all loose objects.
++A cruft pack eliminates the need for storing unreachable objects in a loose
++state by including the per-object mtimes in a separate file alongside a single
++pack containing all loose objects.
+
+A cruft pack is written by `git repack --cruft` when generating a new pack.
+linkgit:git-pack-objects[1]'s `--cruft` option. Note that `git repack --cruft`
@@ Documentation/technical/cruft-packs.txt (new)
+Notable alternatives to this design include:
+
+ - The location of the per-object mtime data, and
-+ - Whether cruft packs should be incremental or not.
++ - Storing unreachable objects in multiple cruft packs.
+
+On the location of mtime data, a new auxiliary file tied to the pack was chosen
+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain
+support for optional chunks of data, it may make sense to consolidate the
+`.mtimes` format into the `.idx` itself.
+
-+Incremental cruft packs (i.e., where each time a repository is repacked a new
-+cruft pack is generated containing only the unreachable objects introduced since
-+the last time a cruft pack was written) are significantly more complicated to
-+construct, and so aren't pursued here. The obvious drawback to the current
-+implementation is that the entire cruft pack must be re-written from scratch.
++Storing unreachable objects among multiple cruft packs (e.g., creating a new
++cruft pack during each repacking operation including only unreachable objects
++which aren't already stored in an earlier cruft pack) is significantly more
++complicated to construct, and so aren't pursued here. The obvious drawback to
++the current implementation is that the entire cruft pack must be re-written from
++scratch.
2: 7d4ae7bd3e ! 2: 101b34660c pack-mtimes: support reading .mtimes files
@@ Documentation/technical/pack-format.txt: Pack file entry: <+
+
+ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).
+
-+ - A table of mtimes (one per packed object, num_objects in total, each
-+ a 4-byte unsigned integer in network order), in the same order as
-+ objects appear in the index file (e.g., the first entry in the mtime
-+ table corresponds to the object with the lowest lexically-sorted
-+ oid). The mtimes count standard epoch seconds.
++ - A table of 4-byte unsigned integers in network order. The ith
++ value is the modification time (mtime) of the ith object in the
++ corresponding pack by lexicographic (index) order. The mtimes
++ count standard epoch seconds.
+
-+ - A trailer, containing a:
-+
-+ checksum of the corresponding packfile, and
-+
-+ a checksum of all of the above.
++ - A trailer, containing a checksum of the corresponding packfile,
++ and a checksum of all of the above (each having length according
++ to the specified hash function).
+
+All 4-byte numbers are in network order.
+
@@ pack-mtimes.c (new)
+ return xstrfmt("%.*s.mtimes", (int)len, p->pack_name);
+}
+
-+int pack_has_mtimes(struct packed_git *p)
-+{
-+ struct stat st;
-+ char *fname = pack_mtimes_filename(p);
-+
-+ if (stat(fname, &st) < 0) {
-+ if (errno == ENOENT)
-+ return 0;
-+ die_errno(_("could not stat %s"), fname);
-+ }
-+
-+ free(fname);
-+ return 1;
-+}
-+
+#define MTIMES_HEADER_SIZE (12)
+#define MTIMES_MIN_SIZE (MTIMES_HEADER_SIZE + (2 * the_hash_algo->rawsz))
+
@@ pack-mtimes.c (new)
+ struct stat st;
+ void *data = NULL;
+ size_t mtimes_size;
++ struct mtimes_header header;
+ uint32_t *hdr;
+
+ fd = git_open(mtimes_file);
@@ pack-mtimes.c (new)
+
+ data = hdr = xmmap(NULL, mtimes_size, PROT_READ, MAP_PRIVATE, fd, 0);
+
-+ if (ntohl(*hdr) != MTIMES_SIGNATURE) {
++ header.signature = ntohl(hdr[0]);
++ header.version = ntohl(hdr[1]);
++ header.hash_id = ntohl(hdr[2]);
++
++ if (header.signature != MTIMES_SIGNATURE) {
+ ret = error(_("mtimes file %s has unknown signature"), mtimes_file);
+ goto cleanup;
+ }
+
-+ if (ntohl(*++hdr) != 1) {
++ if (header.version != 1) {
+ ret = error(_("mtimes file %s has unsupported version %"PRIu32),
-+ mtimes_file, ntohl(*hdr));
++ mtimes_file, header.version);
+ goto cleanup;
+ }
-+ hdr++;
-+ if (!(ntohl(*hdr) == 1 || ntohl(*hdr) == 2)) {
++
++ if (!(header.hash_id == 1 || header.hash_id == 2)) {
+ ret = error(_("mtimes file %s has unsupported hash id %"PRIu32),
-+ mtimes_file, ntohl(*hdr));
++ mtimes_file, header.hash_id);
+ goto cleanup;
+ }
+
@@ pack-mtimes.h (new)
+
+struct packed_git;
+
-+int pack_has_mtimes(struct packed_git *p);
+int load_pack_mtimes(struct packed_git *p);
+
+uint32_t nth_packed_mtime(struct packed_git *p, uint32_t pos);
@@ pack-mtimes.h (new)
+#endif
## packfile.c ##
-@@ packfile.c: void close_pack_revindex(struct packed_git *p) {
+@@ packfile.c: static void close_pack_revindex(struct packed_git *p)
p->revindex_data = NULL;
}
-+void close_pack_mtimes(struct packed_git *p) {
++static void close_pack_mtimes(struct packed_git *p)
++{
+ if (!p->mtimes_map)
+ return;
+
@@ packfile.c: static void prepare_pack(const char *full_name, size_t full_name_len
string_list_append(data->garbage, full_name);
else
report_garbage(PACKDIR_FILE_GARBAGE, full_name);
-
- ## packfile.h ##
-@@ packfile.h: uint32_t get_pack_fanout(struct packed_git *p, uint32_t value);
- unsigned char *use_pack(struct packed_git *, struct pack_window **, off_t, unsigned long *);
- void close_pack_windows(struct packed_git *);
- void close_pack_revindex(struct packed_git *);
-+void close_pack_mtimes(struct packed_git *p);
- void close_pack(struct packed_git *);
- void close_object_store(struct raw_object_store *o);
- void unuse_pack(struct pack_window **);
3: 7f4612e859 = 3: a94d7dfeb3 pack-write: pass 'struct packing_data' to 'stage_tmp_packfiles'
4: ea245b7216 = 4: 1e0ed363ae chunk-format.h: extract oid_version()
5: deece9eb70 ! 5: 5236490688 pack-mtimes: support writing pack .mtimes files
@@ pack-objects.h: struct packing_data {
unsigned int *tree_depth;
unsigned char *layer;
+
-+ /* cruft packs */
++ /*
++ * Used when writing cruft packs.
++ *
++ * Object mtimes are stored in pack order when writing, but
++ * written out in lexicographic (index) order.
++ */
+ uint32_t *cruft_mtime;
};
@@ pack-write.c: const char *write_rev_file_order(const char *rev_name,
+ hashwrite_be32(f, oid_version(the_hash_algo));
+}
+
++/*
++ * Writes the object mtimes of "objects" for use in a .mtimes file.
++ * Note that objects must be in lexicographic (index) order, which is
++ * the expected ordering of these values in the .mtimes file.
++ */
+static void write_mtimes_objects(struct hashfile *f,
+ struct packing_data *to_pack,
+ struct pack_idx_entry **objects,
@@ pack-write.c: const char *write_rev_file_order(const char *rev_name,
+ write_mtimes_objects(f, to_pack, objects, nr_objects);
+ write_mtimes_trailer(f, hash);
+
-+ if (mtimes_name && adjust_shared_perm(mtimes_name) < 0)
++ if (adjust_shared_perm(mtimes_name) < 0)
+ die(_("failed to make %s readable"), mtimes_name);
+
+ finalize_hashfile(f, NULL,
@@ pack-write.c: void stage_tmp_packfiles(struct strbuf *name_buffer,
+ mtimes_tmp_name = write_mtimes_file(NULL, to_pack, written_list,
+ nr_written,
+ hash);
-+ if (adjust_shared_perm(mtimes_tmp_name))
-+ die_errno("unable to make temporary mtimes file readable");
+ }
+
rename_tmp_packfile(name_buffer, pack_tmp_name, "pack");
6: e0a7b3b310 ! 6: 78313bc441 t/helper: add 'pack-mtimes' test-tool
@@ t/helper/test-pack-mtimes.c (new)
+#include "packfile.h"
+#include "pack-mtimes.h"
+
-+static int dump_mtimes(struct packed_git *p)
++static void dump_mtimes(struct packed_git *p)
+{
+ uint32_t i;
+ if (load_pack_mtimes(p) < 0)
@@ t/helper/test-pack-mtimes.c (new)
+ printf("%s %"PRIu32"\n",
+ oid_to_hex(&oid), nth_packed_mtime(p, i));
+ }
-+
-+ return 0;
+}
+
+static const char *pack_mtimes_usage = "\n"
@@ t/helper/test-pack-mtimes.c (new)
+
+ strbuf_release(&buf);
+
-+ return p ? dump_mtimes(p) : 1;
++ if (!p)
++ die("could not find pack '%s'", argv[1]);
++
++ dump_mtimes(p);
++
++ return 0;
+}
## t/helper/test-tool.c ##
7: 5710933127 = 7: 142098668d builtin/pack-objects.c: return from create_object_entry()
8: 66165917a4 ! 8: 2517a6be3d builtin/pack-objects.c: --cruft without expiration
@@ Commit message
which packs are about to be removed.
- All packs which are going to be removed (we'll call these the
- redundant ones) are marked as kept in-core, as well as any packs
- that `pack-objects` found but the caller did not specify.
+ redundant ones) are marked as kept in-core.
- These packs are presumed to have entered the repository between
- the caller collecting packs and invoking `pack-objects`. Since we
- do not want to include objects in these packs (because we don't know
- which of their objects are or aren't reachable), these are also
- marked as kept in-core.
+ Any packs the caller did not mention (but are known to the
+ `pack-objects` process) are also marked as kept in-core. Packs not
+ mentioned by the caller are assumed to be unknown to them, i.e.,
+ they entered the repository after the caller decided which packs
+ should be kept and which should be discarded.
+
+ Since we do not want to include objects in these "unknown" packs
+ (because we don't know which of their objects are or aren't
+ reachable), these are also marked as kept in-core.
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
@@ Documentation/git-pack-objects.txt: SYNOPSIS
[--local] [--incremental] [--window=<n>] [--depth=<n>]
[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]
+ [--cruft] [--cruft-expiration=<time>]
- [--stdout [--filter=<filter-spec>] | base-name]
- [--shallow] [--keep-true-parents] [--[no-]sparse] < object-list
+ [--stdout [--filter=<filter-spec>] | <base-name>]
+ [--shallow] [--keep-true-parents] [--[no-]sparse] < <object-list>
@@ Documentation/git-pack-objects.txt: base-name::
Incompatible with `--revs`, or options that imply `--revs` (such as
@@ Documentation/git-pack-objects.txt: base-name::
+--cruft::
+ Packs unreachable objects into a separate "cruft" pack, denoted
-+ by the existence of a `.mtimes` file. Pack names provided over
-+ stdin indicate which packs will remain after a `git repack`.
-+ Pack names prefixed with a `-` indicate those which will be
-+ removed. The contents of the cruft pack are all objects not
-+ contained in the surviving packs specified by `--keep-pack`)
-+ which have not exceeded the grace period (see
++ by the existence of a `.mtimes` file. Typically used by `git
++ repack --cruft`. Callers provide a list of pack names and
++ indicate which packs will remain in the repository, along with
++ which packs will be deleted (indicated by the `-` prefix). The
++ contents of the cruft pack are all objects not contained in the
++ surviving packs which have not exceeded the grace period (see
+ `--cruft-expiration` below), or which have exceeded the grace
+ period, but are reachable from an other object which hasn't.
++
++When the input lists a pack containing all reachable objects (and lists
++all other packs as pending deletion), the corresponding cruft pack will
++contain all unreachable objects (with mtime newer than the
++`--cruft-expiration`) along with any unreachable objects whose mtime is
++older than the `--cruft-expiration`, but are reachable from an
++unreachable object whose mtime is newer than the `--cruft-expiration`).
+++
+Incompatible with `--unpack-unreachable`, `--keep-unreachable`,
+`--pack-loose-unreachable`, `--stdin-packs`, as well as any other
+options which imply `--revs`. Also incompatible with `--max-pack-size`;
@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
string_list_clear(&exclude_packs, 0);
}
-+static int add_cruft_object_entry(const struct object_id *oid, enum object_type type,
-+ struct packed_git *pack, off_t offset,
-+ const char *name, uint32_t mtime)
++static void add_cruft_object_entry(const struct object_id *oid, enum object_type type,
++ struct packed_git *pack, off_t offset,
++ const char *name, uint32_t mtime)
+{
+ struct object_entry *entry;
+
@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
+ if (entry) {
+ if (name) {
+ entry->hash = pack_name_hash(name);
-+ entry->no_try_delta = name && no_try_delta(name);
++ entry->no_try_delta = no_try_delta(name);
+ }
+ } else {
+ if (!want_object_in_pack(oid, 0, &pack, &offset))
-+ return 0;
++ return;
+ if (!pack && type == OBJ_BLOB && !has_loose_object(oid)) {
+ /*
+ * If a traversed tree has a missing blob then we want
@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
+ * limited to "ensure non-tip blobs which don't exist in
+ * packs do exist via loose objects". Confused?
+ */
-+ return 0;
++ return;
+ }
+
+ entry = create_object_entry(oid, type, pack_name_hash(name),
@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
+
+ if (mtime > oe_cruft_mtime(&to_pack, entry))
+ oe_set_cruft_mtime(&to_pack, entry, mtime);
-+ return 1;
++ return;
+}
+
+static void mark_pack_kept_in_core(struct string_list *packs, unsigned keep)
@@ builtin/pack-objects.c: int cmd_pack_objects(int argc, const char **argv, const
read_packs_list_from_stdin();
if (rev_list_unpacked)
add_unreachable_loose_objects();
-- } else if (!use_internal_rev_list)
-+ } else if (cruft)
++ } else if (cruft) {
+ read_cruft_objects();
-+ else if (!use_internal_rev_list)
+ } else if (!use_internal_rev_list) {
read_object_list_from_stdin();
- else {
- get_object_list(rp.nr, rp.v);
+ } else {
## object-file.c ##
@@ object-file.c: int has_loose_object_nonlocal(const struct object_id *oid)
@@ object-store.h: int repo_has_object_file_with_flags(struct repository *r,
/*
- ## t/t5327-pack-objects-cruft.sh (new) ##
+ ## t/t5328-pack-objects-cruft.sh (new) ##
@@
+#!/bin/sh
+
@@ t/t5327-pack-objects-cruft.sh (new)
+
+ git reset --hard reachable &&
+ git tag -d cruft &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ # remove the unreachable tree, but leave the commit
-+ # which has it as its root tree in-tact
++ # which has it as its root tree intact
+ rm -fr "$objdir/$(test_oid_to_path "$tree")" &&
+
+ git repack -Ad &&
@@ t/t5327-pack-objects-cruft.sh (new)
+
+ git reset --hard reachable &&
+ git tag -d cruft &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ # remove the unreachable blob, but leave the commit (and
-+ # the root tree of that commit) in-tact
++ # the root tree of that commit) intact
+ rm -fr "$objdir/$(test_oid_to_path "$blob")" &&
+
+ git repack -Ad &&
9: 02f7fce788 = 9: 6f0e84273f reachable: add options to add_unseen_recent_objects_to_traversal
10: 52e9ac5710 = 10: a8bde361f9 reachable: report precise timestamps from objects in cruft packs
11: 37fda94785 ! 11: d68ce28132 builtin/pack-objects.c: --cruft with expiration
@@ Commit message
Signed-off-by: Taylor Blau [off-list ref]
## builtin/pack-objects.c ##
-@@ builtin/pack-objects.c: static int add_cruft_object_entry(const struct object_id *oid, enum object_type
- return 1;
+@@ builtin/pack-objects.c: static void add_cruft_object_entry(const struct object_id *oid, enum object_type
+ return;
}
+static void show_cruft_object(struct object *obj, const char *name, void *data)
@@ builtin/pack-objects.c: static void read_cruft_objects(void)
enumerate_cruft_objects();
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: basic_cruft_pack_tests () {
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: basic_cruft_pack_tests () {
}
basic_cruft_pack_tests never
12: a05675ab83 ! 12: e5317cd472 builtin/repack.c: support generating a cruft pack
@@ builtin/repack.c: static int write_midx_included_packs(struct string_list *inclu
{
struct child_process cmd = CHILD_PROCESS_INIT;
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
- int show_progress = isatty(2);
+ int show_progress;
/* variables to be filled by option parsing */
- int pack_everything = 0;
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
LOOSEN_UNREACHABLE | ALL_INTO_ONE),
+ OPT_BIT(0, "cruft", &pack_everything,
+ N_("same as -a, pack unreachable cruft objects separately"),
-+ PACK_CRUFT | ALL_INTO_ONE),
++ PACK_CRUFT),
+ OPT_STRING(0, "cruft-expiration", &cruft_expiration, N_("approxidate"),
+ N_("with -C, expire objects older than this")),
OPT_BOOL('d', NULL, &delete_redundant,
N_("remove redundant packs, and run git-prune-packed")),
OPT_BOOL('f', NULL, &po_args.no_reuse_delta,
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
- if (keep_unreachable &&
(unpack_unreachable || (pack_everything & LOOSEN_UNREACHABLE)))
- die(_("--keep-unreachable and -A are incompatible"));
-+ if (pack_everything & PACK_CRUFT && delete_redundant) {
+ die(_("options '%s' and '%s' cannot be used together"), "--keep-unreachable", "-A");
+
++ if (pack_everything & PACK_CRUFT) {
++ pack_everything |= ALL_INTO_ONE;
++
+ if (unpack_unreachable || (pack_everything & LOOSEN_UNREACHABLE))
-+ die(_("--cruft and -A are incompatible"));
++ die(_("options '%s' and '%s' cannot be used together"), "--cruft", "-A");
+ if (keep_unreachable)
-+ die(_("--cruft and -k are incompatible"));
-+ if (!(pack_everything & ALL_INTO_ONE))
-+ die(_("--cruft must be combined with all-into-one"));
++ die(_("options '%s' and '%s' cannot be used together"), "--cruft", "-k");
+ }
-
++
if (write_bitmaps < 0) {
if (!write_midx &&
+ (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
if (pack_everything & ALL_INTO_ONE) {
repack_promisor_objects(&po_args, &names);
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
for_each_string_list_item(item, &names) {
strvec_pushf(&cmd.args, "--keep-pack=%s-%s.pack",
packtmp_name, item->string);
-@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
- return ret;
-
- if (geometry) {
-+ struct packed_git *p;
- FILE *in = xfdopen(cmd.in, "w");
- /*
- * The resulting pack should contain all objects in packs that
-@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
- fprintf(in, "%s\n", pack_basename(geometry->pack[i]));
- for (i = geometry->split; i < geometry->pack_nr; i++)
- fprintf(in, "^%s\n", pack_basename(geometry->pack[i]));
-+
-+ for (p = get_all_packs(the_repository); p; p = p->next) {
-+ if (!p->is_cruft)
-+ continue;
-+ fprintf(in, "^%s\n", pack_basename(p));
-+ }
- fclose(in);
- }
-
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
if (!names.nr && !po_args.quiet)
printf_ln(_("Nothing new to pack."));
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
item->util = (void *)(uintptr_t)populate_pack_exts(item->string);
}
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned' '
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned' '
)
'
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned'
+ git branch -D other &&
+ git tag -d unreachable &&
+ # objects are not cruft if they are contained in the reflogs
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ git rev-list --objects --all --no-object-names >reachable.raw &&
+ git cat-file --batch-all-objects --batch-check="%(objectname)" >objects &&
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned'
+ git checkout main &&
+ git branch -D other &&
+ git tag -d cruft &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ git repack --cruft -d &&
+
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned'
+ git checkout main &&
+ git branch -D other &&
+ git tag -d cruft &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ git repack --cruft &&
+
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned'
+ test_cmp before after
+ )
+'
++
++test_expect_success 'repack --geometric collects once-cruft objects' '
++ git init repo &&
++ test_when_finished "rm -fr repo" &&
++ (
++ cd repo &&
++
++ test_commit reachable &&
++ git repack -Ad &&
++ git branch -M main &&
++
++ git checkout --orphan other &&
++ git rm -rf . &&
++ test_commit --no-tag cruft &&
++ cruft="$(git rev-parse HEAD)" &&
++
++ git checkout main &&
++ git branch -D other &&
++ git reflog expire --all --expire=all &&
++
++ # Pack the objects created in the previous step into a cruft
++ # pack. Intentionally leave loose copies of those objects
++ # around so we can pick them up in a subsequent --geometric
++ # reapack.
++ git repack --cruft &&
++
++ # Now make those objects reachable, and ensure that they are
++ # packed into the new pack created via a --geometric repack.
++ git update-ref refs/heads/other $cruft &&
++
++ # Without this object, the set of unpacked objects is exactly
++ # the set of objects already in the cruft pack. Tweak that set
++ # to ensure we do not overwrite the cruft pack entirely.
++ test_commit reachable2 &&
++
++ find $packdir -name "pack-*.idx" | sort >before &&
++ git repack --geometric=2 -d &&
++ find $packdir -name "pack-*.idx" | sort >after &&
++
++ {
++ git rev-list --objects --no-object-names $cruft &&
++ git rev-list --objects --no-object-names reachable..reachable2
++ } >want.raw &&
++ sort want.raw >want &&
++
++ pack=$(comm -13 before after) &&
++ git show-index <$pack >objects.raw &&
++
++ cut -d" " -f2 objects.raw | sort >got &&
++
++ test_cmp want got
++ )
++'
++
+test_expect_success 'cruft repack with no reachable objects' '
+ git init repo &&
+ test_when_finished "rm -fr repo" &&
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'expired objects are pruned'
+
+ git for-each-ref --format="delete %(refname)" >in &&
+ git update-ref --stdin <in &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+ rm -fr .git/index &&
+
+ git repack --cruft -d &&
13: 0d2dfaa062 ! 13: b548dbbf80 builtin/repack.c: allow configuring cruft pack generation
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
&existing_kept_packs);
if (ret)
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'cruft repack ignores pack.packSizeLimit' '
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: test_expect_success 'cruft repack ignores pack.packSizeLimit' '
)
'
14: fd50c39657 = 14: e6eee7f15c builtin/repack.c: use named flags for existing_packs
15: b2937ceda7 ! 15: b09dbc9fe5 builtin/repack.c: add cruft packs to MIDX during geometric repack
@@ builtin/repack.c: static void midx_included_packs(struct string_list *include,
for_each_string_list_item(item, existing_nonkept_packs) {
if ((uintptr_t)item->util & DELETE_PACK)
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'cruft --local drops unreachable objects' '
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: test_expect_success 'cruft --local drops unreachable objects' '
)
'
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'cruft --local drops unreacha
+
+ git reset --hard $unreachable^ &&
+ git tag -d cruft &&
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ git repack --cruft -d &&
+
16: 394de0199f ! 16: 7a21ae1494 builtin/gc.c: conditionally avoid pruning objects via loose
@@ builtin/gc.c: int cmd_gc(int argc, const char **argv, const char *prefix)
if (quiet)
strvec_push(&prune, "--no-progress");
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'loose objects mtimes upsert others' '
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: test_expect_success 'loose objects mtimes upsert others' '
)
'
@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'loose objects mtimes upsert
+ git branch -D other &&
+ git tag -d unreachable &&
+ # objects are not cruft if they are contained in the reflogs
-+ rm -fr .git/logs &&
++ git reflog expire --all --expire=all &&
+
+ git rev-list --objects --all --no-object-names >reachable.raw &&
+ git cat-file --batch-all-objects --batch-check="%(objectname)" >objects &&
17: 99aace8e16 ! 17: b729b80963 sha1-file.c: don't freshen cruft packs
@@ object-file.c: static int freshen_packed_object(const struct object_id *oid)
return 1;
if (!freshen_file(e.p->pack_name))
- ## t/t5327-pack-objects-cruft.sh ##
-@@ t/t5327-pack-objects-cruft.sh: test_expect_success 'MIDX bitmaps tolerate reachable cruft objects' '
+ ## t/t5328-pack-objects-cruft.sh ##
+@@ t/t5328-pack-objects-cruft.sh: test_expect_success 'MIDX bitmaps tolerate reachable cruft objects' '
)
'
--
2.35.1.73.gccc5557600
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:11
Create a technical document to explain cruft packs. It contains a brief
overview of the problem, some background, details on the implementation,
and a couple of alternative approaches not considered here.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/Makefile | 1 +
Documentation/technical/cruft-packs.txt | 97 +++++++++++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 Documentation/technical/cruft-packs.txt
@@ -0,0 +1,97 @@+= Cruft packs++The cruft packs feature offer an alternative to Git's traditional mechanism of+removing unreachable objects. This document provides an overview of Git's+pruning mechanism, and how a cruft pack can be used instead to accomplish the+same.++== Background++To remove unreachable objects from your repository, Git offers `git repack -Ad`+(see linkgit:git-repack[1]). Quoting from the documentation:++[quote]+[...] unreachable objects in a previous pack become loose, unpacked objects,+instead of being left in the old pack. [...] loose unreachable objects will be+pruned according to normal expiry rules with the next 'git gc' invocation.++Unreachable objects aren't removed immediately, since doing so could race with+an incoming push which may reference an object which is about to be deleted.+Instead, those unreachable objects are stored as loose object and stay that way+until they are older than the expiration window, at which point they are removed+by linkgit:git-prune[1].++Git must store these unreachable objects loose in order to keep track of their+per-object mtimes. If these unreachable objects were written into one big pack,+then either freshening that pack (because an object contained within it was+re-written) or creating a new pack of unreachable objects would cause the pack's+mtime to get updated, and the objects within it would never leave the expiration+window. Instead, objects are stored loose in order to keep track of the+individual object mtimes and avoid a situation where all cruft objects are+freshened at once.++This can lead to undesirable situations when a repository contains many+unreachable objects which have not yet left the grace period. Having large+directories in the shards of `.git/objects` can lead to decreased performance in+the repository. But given enough unreachable objects, this can lead to inode+starvation and degrade the performance of the whole system. Since we+can never pack those objects, these repositories often take up a large amount of+disk space, since we can only zlib compress them, but not store them in delta+chains.++== Cruft packs++A cruft pack eliminates the need for storing unreachable objects in a loose+state by including the per-object mtimes in a separate file alongside a single+pack containing all loose objects.++A cruft pack is written by `git repack --cruft` when generating a new pack.+linkgit:git-pack-objects[1]'s `--cruft` option. Note that `git repack --cruft`+is a classic all-into-one repack, meaning that everything in the resulting pack is+reachable, and everything else is unreachable. Once written, the `--cruft`+option instructs `git repack` to generate another pack containing only objects+not packed in the previous step (which equates to packing all unreachable+objects together). This progresses as follows:++ 1. Enumerate every object, marking any object which is (a) not contained in a+ kept-pack, and (b) whose mtime is within the grace period as a traversal+ tip.++ 2. Perform a reachability traversal based on the tips gathered in the previous+ step, adding every object along the way to the pack.++ 3. Write the pack out, along with a `.mtimes` file that records the per-object+ timestamps.++This mode is invoked internally by linkgit:git-repack[1] when instructed to+write a cruft pack. Crucially, the set of in-core kept packs is exactly the set+of packs which will not be deleted by the repack; in other words, they contain+all of the repository's reachable objects.++When a repository already has a cruft pack, `git repack --cruft` typically only+adds objects to it. An exception to this is when `git repack` is given the+`--cruft-expiration` option, which allows the generated cruft pack to omit+expired objects instead of waiting for linkgit:git-gc[1] to expire those objects+later on.++It is linkgit:git-gc[1] that is typically responsible for removing expired+unreachable objects.++== Alternatives++Notable alternatives to this design include:++ - The location of the per-object mtime data, and+ - Storing unreachable objects in multiple cruft packs.++On the location of mtime data, a new auxiliary file tied to the pack was chosen+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain+support for optional chunks of data, it may make sense to consolidate the+`.mtimes` format into the `.idx` itself.++Storing unreachable objects among multiple cruft packs (e.g., creating a new+cruft pack during each repacking operation including only unreachable objects+which aren't already stored in an earlier cruft pack) is significantly more+complicated to construct, and so aren't pursued here. The obvious drawback to+the current implementation is that the entire cruft pack must be re-written from+scratch.
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:13
This structure will be used to communicate the per-object mtimes when
writing a cruft pack. Here, we need the full packing_data structure
because the mtime information is stored in an array there, not on the
individual object_entry's themselves (to avoid paying the overhead in
structure width for operations which do not generate a cruft pack).
We haven't passed this information down before because one of the two
callers (in bulk-checkin.c) does not have a packing_data structure at
all. In that case (where no cruft pack will be generated), NULL is
passed instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 3 ++-
bulk-checkin.c | 2 +-
pack-write.c | 1 +
pack.h | 3 +++
4 files changed, 7 insertions(+), 2 deletions(-)
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:14
To store the individual mtimes of objects in a cruft pack, introduce a
new `.mtimes` format that can optionally accompany a single pack in the
repository.
The format is defined in Documentation/technical/pack-format.txt, and
stores a 4-byte network order timestamp for each object in name (index)
order.
This patch prepares for cruft packs by defining the `.mtimes` format,
and introducing a basic API that callers can use to read out individual
mtimes.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/technical/pack-format.txt | 19 ++++
Makefile | 1 +
builtin/repack.c | 1 +
object-store.h | 5 +-
pack-mtimes.c | 129 ++++++++++++++++++++++++
pack-mtimes.h | 15 +++
packfile.c | 19 +++-
7 files changed, 186 insertions(+), 3 deletions(-)
create mode 100644 pack-mtimes.c
create mode 100644 pack-mtimes.h
@@ -294,6 +294,25 @@ Pack file entry: <+ All 4-byte numbers are in network order.+== pack-*.mtimes files have the format:++ - A 4-byte magic number '0x4d544d45' ('MTME').++ - A 4-byte version identifier (= 1).++ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).++ - A table of 4-byte unsigned integers in network order. The ith+ value is the modification time (mtime) of the ith object in the+ corresponding pack by lexicographic (index) order. The mtimes+ count standard epoch seconds.++ - A trailer, containing a checksum of the corresponding packfile,+ and a checksum of all of the above (each having length according+ to the specified hash function).++All 4-byte numbers are in network order.+ == multi-pack-index (MIDX) files have the following format: The multi-pack-index files refer to multiple pack-files and loose objects.
@@ -0,0 +1,129 @@+#include"pack-mtimes.h"+#include"object-store.h"+#include"packfile.h"++staticchar*pack_mtimes_filename(structpacked_git*p)+{+size_tlen;+if(!strip_suffix(p->pack_name,".pack",&len))+BUG("pack_name does not end in .pack");+/* NEEDSWORK: this could reuse code from pack-revindex.c. */+returnxstrfmt("%.*s.mtimes",(int)len,p->pack_name);+}++#define MTIMES_HEADER_SIZE (12)+#define MTIMES_MIN_SIZE (MTIMES_HEADER_SIZE + (2 * the_hash_algo->rawsz))++structmtimes_header{+uint32_tsignature;+uint32_tversion;+uint32_thash_id;+};++staticintload_pack_mtimes_file(char*mtimes_file,+uint32_tnum_objects,+constuint32_t**data_p,size_t*len_p)+{+intfd,ret=0;+structstatst;+void*data=NULL;+size_tmtimes_size;+structmtimes_headerheader;+uint32_t*hdr;++fd=git_open(mtimes_file);++if(fd<0){+ret=-1;+gotocleanup;+}+if(fstat(fd,&st)){+ret=error_errno(_("failed to read %s"),mtimes_file);+gotocleanup;+}++mtimes_size=xsize_t(st.st_size);++if(mtimes_size<MTIMES_MIN_SIZE){+ret=error(_("mtimes file %s is too small"),mtimes_file);+gotocleanup;+}++if(mtimes_size-MTIMES_MIN_SIZE!=st_mult(sizeof(uint32_t),num_objects)){+ret=error(_("mtimes file %s is corrupt"),mtimes_file);+gotocleanup;+}++data=hdr=xmmap(NULL,mtimes_size,PROT_READ,MAP_PRIVATE,fd,0);++header.signature=ntohl(hdr[0]);+header.version=ntohl(hdr[1]);+header.hash_id=ntohl(hdr[2]);++if(header.signature!=MTIMES_SIGNATURE){+ret=error(_("mtimes file %s has unknown signature"),mtimes_file);+gotocleanup;+}++if(header.version!=1){+ret=error(_("mtimes file %s has unsupported version %"PRIu32),+mtimes_file,header.version);+gotocleanup;+}++if(!(header.hash_id==1||header.hash_id==2)){+ret=error(_("mtimes file %s has unsupported hash id %"PRIu32),+mtimes_file,header.hash_id);+gotocleanup;+}++cleanup:+if(ret){+if(data)+munmap(data,mtimes_size);+}else{+*len_p=mtimes_size;+*data_p=(constuint32_t*)data;+}++close(fd);+returnret;+}++intload_pack_mtimes(structpacked_git*p)+{+char*mtimes_name=NULL;+intret=0;++if(!p->is_cruft)+returnret;/* not a cruft pack */+if(p->mtimes_map)+returnret;/* already loaded */++ret=open_pack_index(p);+if(ret<0)+gotocleanup;++mtimes_name=pack_mtimes_filename(p);+ret=load_pack_mtimes_file(mtimes_name,+p->num_objects,+&p->mtimes_map,+&p->mtimes_size);+if(ret)+gotocleanup;++cleanup:+free(mtimes_name);+returnret;+}++uint32_tnth_packed_mtime(structpacked_git*p,uint32_tpos)+{+if(!p->mtimes_map)+BUG("pack .mtimes file not loaded for %s",p->pack_name);+if(p->num_objects<=pos)+BUG("pack .mtimes out-of-bounds (%"PRIu32" vs %"PRIu32")",+pos,p->num_objects);++returnget_be32(p->mtimes_map+pos+3);+}
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:16
There are three definitions of an identical function which converts
`the_hash_algo` into either 1 (for SHA-1) or 2 (for SHA-256). There is a
copy of this function for writing both the commit-graph and
multi-pack-index file, and another inline definition used to write the
.rev header.
Consolidate these into a single definition in chunk-format.h. It's not
clear that this is the best header to define this function in, but it
should do for now.
(Worth noting, the .rev caller expects a 4-byte unsigned, but the other
two callers work with a single unsigned byte. The consolidated version
uses the latter type, and lets the compiler widen it when required).
Another caller will be added in a subsequent patch.
Signed-off-by: Taylor Blau <redacted>
---
chunk-format.c | 12 ++++++++++++
chunk-format.h | 3 +++
commit-graph.c | 18 +++---------------
midx.c | 18 +++---------------
pack-write.c | 15 ++-------------
5 files changed, 23 insertions(+), 43 deletions(-)
@@ -365,9 +353,9 @@ struct commit_graph *parse_commit_graph(struct repository *r,}hash_version=*(unsignedchar*)(data+5);-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("commit-graph hash version %X does not match version %X"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));returnNULL;}
@@ -1911,7 +1899,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)hashwrite_be32(f,GRAPH_SIGNATURE);hashwrite_u8(f,GRAPH_VERSION);-hashwrite_u8(f,oid_version());+hashwrite_u8(f,oid_version(the_hash_algo));hashwrite_u8(f,get_num_chunks(cf));hashwrite_u8(f,ctx->num_commit_graphs_after-1);
@@ -134,9 +122,9 @@ struct multi_pack_index *load_multi_pack_index(const char *object_dir, int localm->version);hash_version=m->data[MIDX_BYTE_HASH_VERSION];-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("multi-pack-index hash version %u does not match version %u"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));gotocleanup_fail;}m->hash_len=the_hash_algo->rawsz;
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:18
In the next patch, we will implement and test support for writing a
cruft pack via a special mode of `git pack-objects`. To make sure that
objects are written with the correct timestamps, and a new test-tool
that can dump the object names and corresponding timestamps from a given
`.mtimes` file.
Signed-off-by: Taylor Blau <redacted>
---
Makefile | 1 +
t/helper/test-pack-mtimes.c | 56 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
4 files changed, 59 insertions(+)
create mode 100644 t/helper/test-pack-mtimes.c
@@ -0,0 +1,56 @@+#include"git-compat-util.h"+#include"test-tool.h"+#include"strbuf.h"+#include"object-store.h"+#include"packfile.h"+#include"pack-mtimes.h"++staticvoiddump_mtimes(structpacked_git*p)+{+uint32_ti;+if(load_pack_mtimes(p)<0)+die("could not load pack .mtimes");++for(i=0;i<p->num_objects;i++){+structobject_idoid;+if(nth_packed_object_id(&oid,p,i)<0)+die("could not load object id at position %"PRIu32,i);++printf("%s %"PRIu32"\n",+oid_to_hex(&oid),nth_packed_mtime(p,i));+}+}++staticconstchar*pack_mtimes_usage="\n"+" test-tool pack-mtimes <pack-name.mtimes>";++intcmd__pack_mtimes(intargc,constchar**argv)+{+structstrbufbuf=STRBUF_INIT;+structpacked_git*p;++setup_git_directory();++if(argc!=2)+usage(pack_mtimes_usage);++for(p=get_all_packs(the_repository);p;p=p->next){+strbuf_addstr(&buf,basename(p->pack_name));+strbuf_strip_suffix(&buf,".pack");+strbuf_addstr(&buf,".mtimes");++if(!strcmp(buf.buf,argv[1]))+break;++strbuf_reset(&buf);+}++strbuf_release(&buf);++if(!p)+die("could not find pack '%s'",argv[1]);++dump_mtimes(p);++return0;+}
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:19
Now that the `.mtimes` format is defined, supplement the pack-write API
to be able to conditionally write an `.mtimes` file along with a pack by
setting an additional flag and passing an oidmap that contains the
timestamps corresponding to each object in the pack.
Signed-off-by: Taylor Blau <redacted>
---
pack-objects.c | 6 ++++
pack-objects.h | 25 ++++++++++++++++
pack-write.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++
pack.h | 1 +
4 files changed, 109 insertions(+)
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:28
A new caller in the next commit will want to immediately modify the
object_entry structure created by create_object_entry(). Instead of
forcing that caller to wastefully look-up the entry we just created,
return it from create_object_entry() instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:36
Teach `pack-objects` how to generate a cruft pack when no objects are
dropped (i.e., `--cruft-expiration=never`). Later patches will teach
`pack-objects` how to generate a cruft pack that prunes objects.
When generating a cruft pack which does not prune objects, we want to
collect all unreachable objects into a single pack (noting and updating
their mtimes as we accumulate them). Ordinary use will pass the result
of a `git repack -A` as a kept pack, so when this patch says "kept
pack", readers should think "reachable objects".
Generating a non-expiring cruft packs works as follows:
- Callers provide a list of every pack they know about, and indicate
which packs are about to be removed.
- All packs which are going to be removed (we'll call these the
redundant ones) are marked as kept in-core.
Any packs the caller did not mention (but are known to the
`pack-objects` process) are also marked as kept in-core. Packs not
mentioned by the caller are assumed to be unknown to them, i.e.,
they entered the repository after the caller decided which packs
should be kept and which should be discarded.
Since we do not want to include objects in these "unknown" packs
(because we don't know which of their objects are or aren't
reachable), these are also marked as kept in-core.
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
This results in a new cruft pack which contains all known objects that
aren't included in the kept packs. When the kept pack is the result of
`git repack -A`, the resulting pack contains all unreachable objects.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 30 ++++
builtin/pack-objects.c | 201 +++++++++++++++++++++++++-
object-file.c | 2 +-
object-store.h | 2 +
t/t5328-pack-objects-cruft.sh | 218 +++++++++++++++++++++++++++++
5 files changed, 448 insertions(+), 5 deletions(-)
create mode 100755 t/t5328-pack-objects-cruft.sh
@@ -95,6 +96,35 @@ base-name:: Incompatible with `--revs`, or options that imply `--revs` (such as `--all`), with the exception of `--unpacked`, which is compatible.+--cruft::+ Packs unreachable objects into a separate "cruft" pack, denoted+ by the existence of a `.mtimes` file. Typically used by `git+ repack --cruft`. Callers provide a list of pack names and+ indicate which packs will remain in the repository, along with+ which packs will be deleted (indicated by the `-` prefix). The+ contents of the cruft pack are all objects not contained in the+ surviving packs which have not exceeded the grace period (see+ `--cruft-expiration` below), or which have exceeded the grace+ period, but are reachable from an other object which hasn't.+++When the input lists a pack containing all reachable objects (and lists+all other packs as pending deletion), the corresponding cruft pack will+contain all unreachable objects (with mtime newer than the+`--cruft-expiration`) along with any unreachable objects whose mtime is+older than the `--cruft-expiration`, but are reachable from an+unreachable object whose mtime is newer than the `--cruft-expiration`).+++Incompatible with `--unpack-unreachable`, `--keep-unreachable`,+`--pack-loose-unreachable`, `--stdin-packs`, as well as any other+options which imply `--revs`. Also incompatible with `--max-pack-size`;+when this option is set, the maximum pack size is not inferred from+`pack.packSizeLimit`.++--cruft-expiration=<approxidate>::+ If specified, objects are eliminated from the cruft pack if they+ have an mtime older than `<approxidate>`. If unspecified (and+ given `--cruft`), then no objects are eliminated.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -3389,6 +3395,135 @@ static void read_packs_list_from_stdin(void)string_list_clear(&exclude_packs,0);}+staticvoidadd_cruft_object_entry(conststructobject_id*oid,enumobject_typetype,+structpacked_git*pack,off_toffset,+constchar*name,uint32_tmtime)+{+structobject_entry*entry;++display_progress(progress_state,++nr_seen);++entry=packlist_find(&to_pack,oid);+if(entry){+if(name){+entry->hash=pack_name_hash(name);+entry->no_try_delta=no_try_delta(name);+}+}else{+if(!want_object_in_pack(oid,0,&pack,&offset))+return;+if(!pack&&type==OBJ_BLOB&&!has_loose_object(oid)){+/*+*Ifatraversedtreehasamissingblobthenwewant+*toavoidaddingthatmissingobjecttoourpack.+*+*Thisonlyappliestomissingblobs,nottrees,+*becausethetraversalneedstoparsesub-treesbut+*notblobs.+*+*Noteweonlyperformthischeckwhenwecouldn't+*alreadyfindtheobjectinapack,sowe'rereally+*limitedto"ensure non-tip blobs which don't exist in+*packsdoexistvialooseobjects". Confused?+*/+return;+}++entry=create_object_entry(oid,type,pack_name_hash(name),+0,name&&no_try_delta(name),+pack,offset);+}++if(mtime>oe_cruft_mtime(&to_pack,entry))+oe_set_cruft_mtime(&to_pack,entry,mtime);+return;+}++staticvoidmark_pack_kept_in_core(structstring_list*packs,unsignedkeep)+{+structstring_list_item*item=NULL;+for_each_string_list_item(item,packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=keep;+}+}++staticvoidadd_unreachable_loose_objects(void);+staticvoidadd_objects_in_unpacked_packs(void);++staticvoidenumerate_cruft_objects(void)+{+if(progress)+progress_state=start_progress(_("Enumerating cruft objects"),0);++add_objects_in_unpacked_packs();+add_unreachable_loose_objects();++stop_progress(&progress_state);+}++staticvoidread_cruft_objects(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listdiscard_packs=STRING_LIST_INIT_DUP;+structstring_listfresh_packs=STRING_LIST_INIT_DUP;+structpacked_git*p;++ignore_packed_keep_in_core=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='-')+string_list_append(&discard_packs,buf.buf+1);+else+string_list_append(&fresh_packs,buf.buf);+strbuf_reset(&buf);+}++string_list_sort(&discard_packs);+string_list_sort(&fresh_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);+structstring_list_item*item;++item=string_list_lookup(&fresh_packs,pack_name);+if(!item)+item=string_list_lookup(&discard_packs,pack_name);++if(item){+item->util=p;+}else{+/*+*Thispackwasn'tmentionedineitherthe"fresh"or+*"discard"list,sothecallerdidn'tknowaboutit.+*+*Markitaskeptsothatitsobjectsareignoredby+*add_unseen_recent_objects_to_traversal().We'll+*unmarkitbeforestartingthetraversalsoitdoesn't+*haltthetraversalearly.+*/+p->pack_keep_in_core=1;+}+}++mark_pack_kept_in_core(&fresh_packs,1);+mark_pack_kept_in_core(&discard_packs,0);++if(cruft_expiration)+die("--cruft-expiration not yet implemented");+else+enumerate_cruft_objects();++strbuf_release(&buf);+string_list_clear(&discard_packs,0);+string_list_clear(&fresh_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3521,7 +3656,24 @@ static int add_object_in_unpacked_pack(const struct object_id *oid,uint32_tpos,void*_data){-add_object_entry(oid,OBJ_NONE,"",0);+if(cruft){+off_toffset;+time_tmtime;++if(pack->is_cruft){+if(load_pack_mtimes(pack)<0)+die(_("could not load cruft pack .mtimes"));+mtime=nth_packed_mtime(pack,pos);+}else{+mtime=pack->mtime;+}+offset=nth_packed_object_offset(pack,pos);++add_cruft_object_entry(oid,OBJ_NONE,pack,offset,+NULL,mtime);+}else{+add_object_entry(oid,OBJ_NONE,"",0);+}return0;}
@@ -3545,7 +3697,19 @@ static int add_loose_object(const struct object_id *oid, const char *path,return0;}-add_object_entry(oid,type,"",0);+if(cruft){+structstatst;+if(stat(path,&st)<0){+if(errno==ENOENT)+return0;+returnerror_errno("unable to stat %s",oid_to_hex(oid));+}++add_cruft_object_entry(oid,type,NULL,0,NULL,+st.st_mtime);+}else{+add_object_entry(oid,type,"",0);+}return0;}
@@ -3864,6 +4028,20 @@ static int option_parse_unpack_unreachable(const struct option *opt,return0;}+staticintoption_parse_cruft_expiration(conststructoption*opt,+constchar*arg,intunset)+{+if(unset){+cruft=0;+cruft_expiration=0;+}else{+cruft=1;+if(arg)+cruft_expiration=approxidate(arg);+}+return0;+}+intcmd_pack_objects(intargc,constchar**argv,constchar*prefix){intuse_internal_rev_list=0;
@@ -3936,6 +4114,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_CALLBACK_F(0,"unpack-unreachable",NULL,N_("time"),N_("unpack unreachable objects newer than <time>"),PARSE_OPT_OPTARG,option_parse_unpack_unreachable),+OPT_BOOL(0,"cruft",&cruft,N_("create a cruft pack")),+OPT_CALLBACK_F(0,"cruft-expiration",NULL,N_("time"),+N_("expire cruft objects older than <time>"),+PARSE_OPT_OPTARG,option_parse_cruft_expiration),OPT_BOOL(0,"sparse",&sparse,N_("use the sparse reachability algorithm")),OPT_BOOL(0,"thin",&thin,
@@ -4062,7 +4244,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(!HAVE_THREADS&&delta_search_threads!=1)warning(_("no threads support, ignoring --threads"));-if(!pack_to_stdout&&!pack_size_limit)+if(!pack_to_stdout&&!pack_size_limit&&!cruft)pack_size_limit=pack_size_limit_cfg;if(pack_to_stdout&&pack_size_limit)die(_("--max-pack-size cannot be used to build a pack for transfer"));
@@ -4089,6 +4271,15 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(stdin_packs&&use_internal_rev_list)die(_("cannot use internal rev list with --stdin-packs"));+if(cruft){+if(use_internal_rev_list)+die(_("cannot use internal rev list with --cruft"));+if(stdin_packs)+die(_("cannot use --stdin-packs with --cruft"));+if(pack_size_limit)+die(_("cannot use --max-pack-size with --cruft"));+}+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -0,0 +1,218 @@+#!/bin/sh++test_description='cruft pack related pack-objects tests'+../test-lib.sh++objdir=.git/objects+packdir=$objdir/pack++basic_cruft_pack_tests(){+expire="$1"++test_expect_success"unreachable loose objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+gitrepack-Ad&&+test_commitloose&&++test-toolchmtime+2000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose:loose.t))" &&+test-toolchmtime+1000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose^{tree}))" &&++(+gitrev-list--objects--no-object-namesbase..loose|+whilereadoid+do+path="$objdir/$(test_oid_to_path"$oid")"&&+printf"%s %d\n""$oid""$(test-toolchmtime--get"$path")"+done|+sort-k1+)>expect&&++keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+cruft="$(echo$keep|gitpack-objects--cruft\+--cruft-expiration="$expire"$packdir/pack)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable packed objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+other="$(gitpack-objects--delta-base-offset\+$packdir/pack<objects)" &&+gitprune-packed&&++test-toolchmtime--get-100"$packdir/pack-$other.pack">expect&&++cruft="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$other.pack+EOF+)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++cut-d" "-f2<actual.raw|sort-u>actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable cruft objects are repacked (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++cruft_a="$(echo$keep|gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack)"&&+gitprune-packed&&+cruft_b="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$cruft_a.pack+EOF+)" &&++test-toolpack-mtimes"pack-$cruft_a.mtimes">expect.raw&&+test-toolpack-mtimes"pack-$cruft_b.mtimes">actual.raw&&++sort<expect.raw>expect&&+sort<actual.raw>actual&&++test_cmpexpectactual+)+'++test_expect_success"multiple cruft packs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++test_commitcruft&&+loose="$objdir/$(test_oid_to_path$(gitrev-parsecruft))"&&++# generate three copies of the cruft object in different+# cruft packs, each with a unique mtime:+# - one expired (1000 seconds ago)+# - two non-expired (one 1000 seconds in the future,+# one 1500 seconds in the future)+test-toolchmtime=-1000"$loose"&&+gitpack-objects--cruft$packdir/pack-A<<-EOF&&+$keep+EOF+test-toolchmtime=+1000"$loose"&&+gitpack-objects--cruft$packdir/pack-B<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+EOF+test-toolchmtime=+1500"$loose"&&+gitpack-objects--cruft$packdir/pack-C<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+EOF++# ensure the resulting cruft pack takes the most recent+# mtime among all copies+cruft="$(gitpack-objects--cruft\+--cruft-expiration="$expire"\+$packdir/pack<<-EOF+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+-$(basename$(ls$packdir/pack-C-*.pack))+EOF+)" &&++test-toolpack-mtimes"$(basename$(ls$packdir/pack-C-*.mtimes))">expect.raw&&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++sortexpect.raw>expect&&+sortactual.raw>actual&&+test_cmpexpectactual+)+'++test_expect_success"cruft packs tolerate missing trees (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++tree="$(gitrev-parsecruft^{tree})"&&++gitreset--hardreachable&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++# remove the unreachable tree, but leave the commit+# which has it as its root tree intact+rm-fr"$objdir/$(test_oid_to_path"$tree")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'++test_expect_success"cruft packs tolerate missing blobs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++blob="$(gitrev-parsecruft:cruft.t)"&&++gitreset--hardreachable&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++# remove the unreachable blob, but leave the commit (and+# the root tree of that commit) intact+rm-fr"$objdir/$(test_oid_to_path"$blob")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'+}++basic_cruft_pack_testsnever++test_done
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:37
This function behaves very similarly to what we will need in
pack-objects in order to implement cruft packs with expiration. But it
is lacking a couple of things. Namely, it needs:
- a mechanism to communicate the timestamps of individual recent
objects to some external caller
- and, in the case of packed objects, our future caller will also want
to know the originating pack, as well as the offset within that pack
at which the object can be found
- finally, it needs a way to skip over packs which are marked as kept
in-core.
To address the first two, add a callback interface in this patch which
reports the time of each recent object, as well as a (packed_git,
off_t) pair for packed objects.
Likewise, add a new option to the packed object iterators to skip over
packs which are marked as kept in core. This option will become
implicitly tested in a future patch.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 2 +-
reachable.c | 51 +++++++++++++++++++++++++++++++++++-------
reachable.h | 9 +++++++-
3 files changed, 52 insertions(+), 10 deletions(-)
@@ -126,7 +146,7 @@ static int add_recent_loose(const struct object_id *oid,returnerror_errno("unable to stat %s",oid_to_hex(oid));}-add_recent_object(oid,st.st_mtime,data);+add_recent_object(oid,NULL,0,st.st_mtime,data);return0;}
@@ -134,29 +154,43 @@ static int add_recent_packed(const struct object_id *oid,structpacked_git*p,uint32_tpos,void*data){-structobject*obj=lookup_object(the_repository,oid);+structobject*obj;++if(!want_recent_object(data,oid))+return0;++obj=lookup_object(the_repository,oid);if(obj&&obj->flags&SEEN)return0;-add_recent_object(oid,p->mtime,data);+add_recent_object(oid,p,nth_packed_object_offset(p,pos),p->mtime,data);return0;}intadd_unseen_recent_objects_to_traversal(structrev_info*revs,-timestamp_ttimestamp)+timestamp_ttimestamp,+report_recent_object_fn*cb,+intignore_in_core_kept_packs){structrecent_datadata;+enumfor_each_object_flagsflags;intr;data.revs=revs;data.timestamp=timestamp;+data.cb=cb;+data.ignore_in_core_kept_packs=ignore_in_core_kept_packs;r=for_each_loose_object(add_recent_loose,&data,FOR_EACH_OBJECT_LOCAL_ONLY);if(r)returnr;-returnfor_each_packed_object(add_recent_packed,&data,-FOR_EACH_OBJECT_LOCAL_ONLY);++flags=FOR_EACH_OBJECT_LOCAL_ONLY|FOR_EACH_OBJECT_PACK_ORDER;+if(ignore_in_core_kept_packs)+flags|=FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS;++returnfor_each_packed_object(add_recent_packed,&data,flags);}staticintmark_object_seen(conststructobject_id*oid,
@@ -217,7 +251,8 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,if(mark_recent){revs->ignore_missing_links=1;-if(add_unseen_recent_objects_to_traversal(revs,mark_recent))+if(add_unseen_recent_objects_to_traversal(revs,mark_recent,+NULL,0))die("unable to mark recent objects");if(prepare_revision_walk(revs))die("revision walk setup failed");
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:39
In a previous patch, pack-objects learned how to generate a cruft pack
so long as no objects are dropped.
This patch teaches pack-objects to handle the case where a non-never
`--cruft-expiration` value is passed. This case is slightly more
complicated than before, because we want pack-objects to save
unreachable objects which would have been pruned when there is another
recent (i.e., non-prunable) unreachable object which reaches the other.
We'll call these objects "unreachable but reachable-from-recent".
Here is how pack-objects handles `--cruft-expiration`:
- Instead of adding all objects outside of the kept pack(s) into the
packing list, only handle the ones whose mtime is within the grace
period.
- Construct a reachability traversal whose tips are the
unreachable-but-recent objects.
- Then, walk along that traversal, stopping if we reach an object in
the kept pack. At each step along the traversal, we add the object
we are visiting to the packing list.
In the majority of these cases, any object we visit in this traversal
will already be in our packing list. But we will sometimes encounter
reachable-from-recent cruft objects, which we want to retain even if
they aged out of the grace period.
The most subtle point of this process is that we actually don't need to
bother to update the rescued object's mtime. Even though we will write
an .mtimes file with a value that is older than the expiration window,
it will continue to survive cruft repacks so long as any objects which
reach it haven't aged out.
That is, a future repack will also exclude that object from the initial
packing list, only to discover it later on when doing the reachability
traversal.
Finally, stopping early once an object is found in a kept pack is safe
to do because the kept packs ordinarily represent which packs will
survive after repacking. Assuming that it _isn't_ safe to halt a
traversal early would mean that there is some ancestor object which is
missing, which implies repository corruption (i.e., the complete set of
reachable objects isn't present).
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 84 +++++++++++++++++++-
t/t5328-pack-objects-cruft.sh | 143 ++++++++++++++++++++++++++++++++++
2 files changed, 226 insertions(+), 1 deletion(-)
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:42
When generating a cruft pack, the caller within pack-objects will want
to know the precise timestamps of cruft objects (i.e., their
corresponding values in the .mtimes table) rather than the mtime of the
cruft pack itself.
Teach add_recent_packed() to lookup each object's precise mtime from the
.mtimes file if one exists (indicated by the is_cruft bit on the
packed_git structure).
A couple of small things worth noting here:
- load_pack_mtimes() needs to be called before asking for
nth_packed_mtime(), and that call is done lazily here. That function
exits early if the .mtimes file has already been opened and parsed,
so only the first call is slow.
- Checking the is_cruft bit can be done without any extra work on the
caller's behalf, since it is set up for us automatically as a
side-effect of calling add_packed_git() (just like the 'pack_keep'
and 'pack_promisor' bits).
Signed-off-by: Taylor Blau <redacted>
---
reachable.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:43
In servers which set the pack.window configuration to a large value, we
can wind up spending quite a lot of time finding new bases when breaking
delta chains between reachable and unreachable objects while generating
a cruft pack.
Introduce a handful of `repack.cruft*` configuration variables to
control the parameters used by pack-objects when generating a cruft
pack.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/config/repack.txt | 9 ++++
builtin/repack.c | 50 ++++++++++++++------
t/t5328-pack-objects-cruft.sh | 83 +++++++++++++++++++++++++++++++++
3 files changed, 128 insertions(+), 14 deletions(-)
@@ -25,3 +25,12 @@ repack.writeBitmaps:: space and extra time spent on the initial repack. This has no effect if multiple packfiles are created. Defaults to true on bare repos, false otherwise.++repack.cruftWindow::+repack.cruftWindowMemory::+repack.cruftDepth::+repack.cruftThreads::+ Parameters used by linkgit:git-pack-objects[1] when generating+ a cruft pack and the respective parameters are not given over+ the command line. See similarly named `pack.*` configuration+ variables for defaults and meaning.
@@ -565,4 +565,87 @@ test_expect_success 'cruft repack ignores pack.packSizeLimit' ')'+test_expect_success'cruft repack respects repack.cruftWindow''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&++GIT_TRACE2_EVENT=$(pwd)/event.trace\+git-cpack.window=1-crepack.cruftWindow=2repack\+--cruft--window=3&&++grep"pack-objects.*--window=2.*--cruft"event.trace+)+'++test_expect_success'cruft repack respects --window by default''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&++GIT_TRACE2_EVENT=$(pwd)/event.trace\+git-cpack.window=2repack--cruft--window=3&&++grep"pack-objects.*--window=3.*--cruft"event.trace+)+'++test_expect_success'cruft repack respects --quiet''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+GIT_PROGRESS_DELAY=0gitrepack--cruft--quiet2>err&&+test_must_be_emptyerr+)+'++test_expect_success'cruft --local drops unreachable objects''+gitinitalternate&&+gitinitrepo&&+test_when_finished"rm -fr alternate repo"&&++test_commit-Calternatebase&&+# Pack all objects in alterate so that the cruft repack in "repo" sees+# the object it dropped due to `--local` as packed. Otherwise this+# object would not appear packed anywhere (since it is not packed in+# alternate and likewise not part of the cruft pack in the other repo+# because of `--local`).+git-Calternaterepack-ad&&++(+cdrepo&&++object="$(git-C../alternaterev-parseHEAD:base.t)"&&+git-C../alternatecat-file-p$object>contents&&++# Write some reachable objects and two unreachable ones: one+# that the alternate has and another that is unique.+test_commitother&&+githash-object-w-tblobcontents&&+cruft="$(echocruft|githash-object-w-tblob--stdin)"&&++(cd../alternate/.git/objects&&pwd)\+>.git/objects/info/alternates&&++test_path_is_file$objdir/$(test_oid_to_path$cruft)&&+test_path_is_file$objdir/$(test_oid_to_path$object)&&++gitrepack-d--cruft--local&&++test-toolpack-mtimes"$(basename$(ls$packdir/pack-*.mtimes))"\+>objects&&+!grep$objectobjects&&+grep$cruftobjects+)+'+ test_done
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:44
Expose a way to split the contents of a repository into a main and cruft
pack when doing an all-into-one repack with `git repack --cruft -d`, and
a complementary configuration variable.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 11 ++
Documentation/technical/cruft-packs.txt | 2 +-
builtin/repack.c | 106 +++++++++++-
t/t5328-pack-objects-cruft.sh | 207 ++++++++++++++++++++++++
4 files changed, 320 insertions(+), 6 deletions(-)
@@ -63,6 +63,17 @@ to the new separate pack will be written. Also run 'git prune-packed' to remove redundant loose object files.+--cruft::+ Same as `-a`, unless `-d` is used. Then any unreachable objects+ are packed into a separate cruft pack. Unreachable objects can+ be pruned using the normal expiry rules with the next `git gc`+ invocation (see linkgit:git-gc[1]). Incompatible with `-k`.++--cruft-expiration=<approxidate>::+ Expire unreachable objects older than `<approxidate>`+ immediately instead of waiting for the next `git gc` invocation.+ Only useful with `--cruft -d`.+ -l:: Pass the `--local` option to 'git pack-objects'. See linkgit:git-pack-objects[1].
@@ -17,7 +17,7 @@ pruned according to normal expiry rules with the next 'git gc' invocation. Unreachable objects aren't removed immediately, since doing so could race with an incoming push which may reference an object which is about to be deleted.-Instead, those unreachable objects are stored as loose object and stay that way+Instead, those unreachable objects are stored as loose objects and stay that way until they are older than the expiration window, at which point they are removed by linkgit:git-prune[1].
@@ -600,6 +606,67 @@ static int write_midx_included_packs(struct string_list *include,returnfinish_command(&cmd);}+staticintwrite_cruft_pack(conststructpack_objects_args*args,+constchar*pack_prefix,+structstring_list*names,+structstring_list*existing_packs,+structstring_list*existing_kept_packs)+{+structchild_processcmd=CHILD_PROCESS_INIT;+structstrbufline=STRBUF_INIT;+structstring_list_item*item;+FILE*in,*out;+intret;++prepare_pack_objects(&cmd,args);++strvec_push(&cmd.args,"--cruft");+if(cruft_expiration)+strvec_pushf(&cmd.args,"--cruft-expiration=%s",+cruft_expiration);++strvec_push(&cmd.args,"--honor-pack-keep");+strvec_push(&cmd.args,"--non-empty");+strvec_push(&cmd.args,"--max-pack-size=0");++cmd.in=-1;++ret=start_command(&cmd);+if(ret)+returnret;++/*+*nameshasaconfusingdoubleuse:itbothprovidesthelist+*ofjust-writtennewpacks,andacceptsthenameofthecruft+*packwearewriting.+*+*Bythetimeitisreadhere,itcontainsonlythepack(s)+*thatwerejustwritten,whichisexactlythesetofpackswe+*wanttoconsiderkept.+*/+in=xfdopen(cmd.in,"w");+for_each_string_list_item(item,names)+fprintf(in,"%s-%s.pack\n",pack_prefix,item->string);+for_each_string_list_item(item,existing_packs)+fprintf(in,"-%s.pack\n",item->string);+for_each_string_list_item(item,existing_kept_packs)+fprintf(in,"%s.pack\n",item->string);+fclose(in);++out=xfdopen(cmd.out,"r");+while(strbuf_getline_lf(&line,out)!=EOF){+if(line.len!=the_hash_algo->hexsz)+die(_("repack: Expecting full hex object ID lines only "+"from pack-objects."));+string_list_append(names,line.buf);+}+fclose(out);++strbuf_release(&line);++returnfinish_command(&cmd);+}+intcmd_repack(intargc,constchar**argv,constchar*prefix){structchild_processcmd=CHILD_PROCESS_INIT;
@@ -616,7 +683,6 @@ int cmd_repack(int argc, const char **argv, const char *prefix)intshow_progress;/* variables to be filled by option parsing */-intpack_everything=0;intdelete_redundant=0;constchar*unpack_unreachable=NULL;intkeep_unreachable=0;
@@ -632,6 +698,11 @@ int cmd_repack(int argc, const char **argv, const char *prefix)OPT_BIT('A',NULL,&pack_everything,N_("same as -a, and turn unreachable objects loose"),LOOSEN_UNREACHABLE|ALL_INTO_ONE),+OPT_BIT(0,"cruft",&pack_everything,+N_("same as -a, pack unreachable cruft objects separately"),+PACK_CRUFT),+OPT_STRING(0,"cruft-expiration",&cruft_expiration,N_("approxidate"),+N_("with -C, expire objects older than this")),OPT_BOOL('d',NULL,&delete_redundant,N_("remove redundant packs, and run git-prune-packed")),OPT_BOOL('f',NULL,&po_args.no_reuse_delta,
@@ -684,6 +755,15 @@ int cmd_repack(int argc, const char **argv, const char *prefix)(unpack_unreachable||(pack_everything&LOOSEN_UNREACHABLE)))die(_("options '%s' and '%s' cannot be used together"),"--keep-unreachable","-A");+if(pack_everything&PACK_CRUFT){+pack_everything|=ALL_INTO_ONE;++if(unpack_unreachable||(pack_everything&LOOSEN_UNREACHABLE))+die(_("options '%s' and '%s' cannot be used together"),"--cruft","-A");+if(keep_unreachable)+die(_("options '%s' and '%s' cannot be used together"),"--cruft","-k");+}+if(write_bitmaps<0){if(!write_midx&&(!(pack_everything&ALL_INTO_ONE)||!is_bare_repository()))
@@ -829,6 +910,21 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(!names.nr&&!po_args.quiet)printf_ln(_("Nothing new to pack."));+if(pack_everything&PACK_CRUFT){+constchar*pack_prefix;+if(!skip_prefix(packtmp,packdir,&pack_prefix))+die(_("pack prefix %s does not begin with objdir %s"),+packtmp,packdir);+if(*pack_prefix=='/')+pack_prefix++;++ret=write_cruft_pack(&po_args,pack_prefix,&names,+&existing_nonkept_packs,+&existing_kept_packs);+if(ret)+returnret;+}+for_each_string_list_item(item,&names){item->util=(void*)(uintptr_t)populate_pack_exts(item->string);}
@@ -358,4 +358,211 @@ test_expect_success 'expired objects are pruned' ')'+test_expect_success'repack --cruft generates a cruft pack''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitbranch-Mmain&&+gitcheckout--orphanother&&+test_commitunreachable&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dunreachable&&+# objects are not cruft if they are contained in the reflogs+gitreflogexpire--all--expire=all&&++gitrev-list--objects--all--no-object-names>reachable.raw&&+gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&+sort<reachable.raw>reachable&&+comm-13reachableobjects>unreachable&&++gitrepack--cruft-d&&++cruft=$(basename$(ls$packdir/pack-*.mtimes).mtimes)&&+pack=$(basename$(ls$packdir/pack-*.pack|grep-v$cruft).pack)&&++gitshow-index<$packdir/$pack.idx>actual.raw&&+cut-f2-d" "actual.raw|sort>actual&&+test_cmpreachableactual&&++gitshow-index<$packdir/$cruft.idx>actual.raw&&+cut-f2-d" "actual.raw|sort>actual&&+test_cmpunreachableactual+)+'++test_expect_success'loose objects mtimes upsert others''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+gitbranch-Mmain&&++gitcheckout--orphanother&&+test_commitcruft&&+# incremental repack, leaving existing objects loose (so+# they can be "freshened")+gitrepack&&++tip="$(gitrev-parsecruft)"&&+path="$objdir/$(test_oid_to_path"$(gitrev-parsecruft)")"&&+test-toolchmtime--get+1000"$path">expect&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++gitrepack--cruft-d&&++mtimes="$(basename$(ls$packdir/pack-*.mtimes))"&&+test-toolpack-mtimes"$mtimes">actual.raw&&+grep"$tip"actual.raw|cut-d" "-f2>actual&&+test_cmpexpectactual+)+'++test_expect_success'cruft packs are not included in geometric repack''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+gitbranch-Mmain&&++gitcheckout--orphanother&&+test_commitcruft&&+gitrepack-d&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++gitrepack--cruft&&++find$packdir-typef|sort>before&&+gitrepack--geometric=2-d&&+find$packdir-typef|sort>after&&++test_cmpbeforeafter+)+'++test_expect_success'repack --geometric collects once-cruft objects''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+gitbranch-Mmain&&++gitcheckout--orphanother&&+gitrm-rf.&&+test_commit--no-tagcruft&&+cruft="$(gitrev-parseHEAD)"&&++gitcheckoutmain&&+gitbranch-Dother&&+gitreflogexpire--all--expire=all&&++# Pack the objects created in the previous step into a cruft+# pack. Intentionally leave loose copies of those objects+# around so we can pick them up in a subsequent --geometric+# reapack.+gitrepack--cruft&&++# Now make those objects reachable, and ensure that they are+# packed into the new pack created via a --geometric repack.+gitupdate-refrefs/heads/other$cruft&&++# Without this object, the set of unpacked objects is exactly+# the set of objects already in the cruft pack. Tweak that set+# to ensure we do not overwrite the cruft pack entirely.+test_commitreachable2&&++find$packdir-name"pack-*.idx"|sort>before&&+gitrepack--geometric=2-d&&+find$packdir-name"pack-*.idx"|sort>after&&++{+gitrev-list--objects--no-object-names$cruft&&+gitrev-list--objects--no-object-namesreachable..reachable2+}>want.raw&&+sortwant.raw>want&&++pack=$(comm-13beforeafter)&&+gitshow-index<$pack>objects.raw&&++cut-d" "-f2objects.raw|sort>got&&++test_cmpwantgot+)+'++test_expect_success'cruft repack with no reachable objects''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+gitrepack-ad&&++base="$(gitrev-parsebase)"&&++gitfor-each-ref--format="delete %(refname)">in&&+gitupdate-ref--stdin<in&&+gitreflogexpire--all--expire=all&&+rm-fr.git/index&&++gitrepack--cruft-d&&++gitcat-file-t$base+)+'++test_expect_success'cruft repack ignores --max-pack-size''+gitinitmax-pack-size&&+(+cdmax-pack-size&&+test_commitbase&&+# two cruft objects which exceed the maximum pack size+test-toolgenrandomfoo1048576|githash-object--stdin-w&&+test-toolgenrandombar1048576|githash-object--stdin-w&&+gitrepack--cruft--max-pack-size=1M&&+find$packdir-name"*.mtimes">cruft&&+test_line_count=1cruft&&+test-toolpack-mtimes"$(basename"$(catcruft)")">objects&&+test_line_count=2objects+)+'++test_expect_success'cruft repack ignores pack.packSizeLimit''+(+cdmax-pack-size&&+# repack everything back together to remove the existing cruft+# pack (but to keep its objects)+gitrepack-adk&&+git-cpack.packSizeLimit=1Mrepack--cruft&&+# ensure the same post condition is met when --max-pack-size+# would otherwise be inferred from the configuration+find$packdir-name"*.mtimes">cruft&&+test_line_count=1cruft&&+test-toolpack-mtimes"$(basename"$(catcruft)")">objects&&+test_line_count=2objects+)+'+ test_done
From: Taylor Blau <hidden> Date: 2022-03-02 00:58:45
We use the `util` pointer for items in the `existing_packs` string list
to indicate which packs are going to be deleted. Since that has so far
been the only use of that `util` pointer, we just set it to 0 or 1.
But we're going to add an additional state to this field in the next
patch, so prepare for that by adding a #define for the first bit so we
can more expressively inspect the flags state.
Signed-off-by: Taylor Blau <redacted>
---
builtin/repack.c | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
From: Taylor Blau <hidden> Date: 2022-03-02 00:59:04
When using cruft packs, the following race can occur when a geometric
repack that writes a MIDX bitmap takes place afterwords:
- First, create an unreachable object and do an all-into-one cruft
repack which stores that object in the repository's cruft pack.
- Then make that object reachable.
- Finally, do a geometric repack and write a MIDX bitmap.
Assuming that we are sufficiently unlucky as to select a commit from the
MIDX which reaches that object for bitmapping, then the `git
multi-pack-index` process will complain that that object is missing.
The reason is because we don't include cruft packs in the MIDX when
doing a geometric repack. Since the "make that object reachable" doesn't
necessarily mean that we'll create a new copy of that object in one of
the packs that will get rolled up as part of a geometric repack, it's
possible that the MIDX won't see any copies of that now-reachable
object.
Of course, it's desirable to avoid including cruft packs in the MIDX
because it causes the MIDX to store a bunch of objects which are likely
to get thrown away. But excluding that pack does open us up to the above
race.
This patch demonstrates the bug, and resolves it by including cruft
packs in the MIDX even when doing a geometric repack.
Signed-off-by: Taylor Blau <redacted>
---
builtin/repack.c | 19 +++++++++++++++++--
t/t5328-pack-objects-cruft.sh | 26 ++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 2 deletions(-)
@@ -648,4 +648,30 @@ test_expect_success 'cruft --local drops unreachable objects' ')'+test_expect_success'MIDX bitmaps tolerate reachable cruft objects''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&+unreachable="$(gitrev-parsecruft)"&&++gitreset--hard$unreachable^&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++gitrepack--cruft-d&&++# resurrect the unreachable object via a new commit. the+# new commit will get selected for a bitmap, but be+# missing one of its parents from the selected packs.+gitreset--hard$unreachable&&+test_commitresurrect&&++gitrepack--write-midx--write-bitmap-index--geometric=2-d+)+'+ test_done
From: Taylor Blau <hidden> Date: 2022-03-02 00:59:05
Expose the new `git repack --cruft` mode from `git gc` via a new opt-in
flag. When invoked like `git gc --cruft`, `git gc` will avoid exploding
unreachable objects as loose ones, and instead create a cruft pack and
`.mtimes` file.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/config/gc.txt | 21 +++++++++++++-------
Documentation/git-gc.txt | 5 +++++
builtin/gc.c | 10 +++++++++-
t/t5328-pack-objects-cruft.sh | 37 +++++++++++++++++++++++++++++++++++
4 files changed, 65 insertions(+), 8 deletions(-)
@@ -81,14 +81,21 @@ gc.packRefs:: to enable it within all non-bare repos or it can be set to a boolean value. The default is `true`.+gc.cruftPacks::+ Store unreachable objects in a cruft pack (see+ linkgit:git-repack[1]) instead of as loose objects. The default+ is `false`.+ gc.pruneExpire::- When 'git gc' is run, it will call 'prune --expire 2.weeks.ago'.- Override the grace period with this config variable. The value- "now" may be used to disable this grace period and always prune- unreachable objects immediately, or "never" may be used to- suppress pruning. This feature helps prevent corruption when- 'git gc' runs concurrently with another process writing to the- repository; see the "NOTES" section of linkgit:git-gc[1].+ When 'git gc' is run, it will call 'prune --expire 2.weeks.ago'+ (and 'repack --cruft --cruft-expiration 2.weeks.ago' if using+ cruft packs via `gc.cruftPacks` or `--cruft`). Override the+ grace period with this config variable. The value "now" may be+ used to disable this grace period and always prune unreachable+ objects immediately, or "never" may be used to suppress pruning.+ This feature helps prevent corruption when 'git gc' runs+ concurrently with another process writing to the repository; see+ the "NOTES" section of linkgit:git-gc[1]. gc.worktreePruneExpire:: When 'git gc' is run, it calls
@@ -54,6 +54,11 @@ other housekeeping tasks (e.g. rerere, working trees, reflog...) will be performed as well.+--cruft::+ When expiring unreachable objects, pack them separately into a+ cruft pack instead of storing the loose objects as loose+ objects.+ --prune=<date>:: Prune loose objects older than date (default is 2 weeks ago, overridable by the config variable `gc.pruneExpire`).
@@ -671,6 +678,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)die(FAILED_RUN,repack.v[0]);if(prune_expire){+/* run `git prune` even if using cruft packs */strvec_push(&prune,prune_expire);if(quiet)strvec_push(&prune,"--no-progress");
@@ -429,6 +429,43 @@ test_expect_success 'loose objects mtimes upsert others' ')'+test_expect_success'expiring cruft objects with git gc''+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitbranch-Mmain&&+gitcheckout--orphanother&&+test_commitunreachable&&++gitcheckoutmain&&+gitbranch-Dother&&+gittag-dunreachable&&+# objects are not cruft if they are contained in the reflogs+gitreflogexpire--all--expire=all&&++gitrev-list--objects--all--no-object-names>reachable.raw&&+gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&+sort<reachable.raw>reachable&&+comm-13reachableobjects>unreachable&&++gitrepack--cruft-d&&++mtimes=$(ls.git/objects/pack/pack-*.mtimes)&&+test_path_is_file$mtimes&&++gitgc--cruft--prune=now&&++gitcat-file--batch-all-objects--batch-check="%(objectname)">objects&&++comm-23unreachableobjects>removed&&+test_cmpunreachableremoved&&+test_path_is_missing$mtimes+)+'+ test_expect_success'cruft packs are not included in geometric repack''gitinitrepo&&test_when_finished"rm -fr repo"&&
From: Taylor Blau <hidden> Date: 2022-03-02 00:59:06
We don't bother to freshen objects stored in a cruft pack individually
by updating the `.mtimes` file. This is because we can't portably `mmap`
and write into the middle of a file (i.e., to update the mtime of just
one object). Instead, we would have to rewrite the entire `.mtimes` file
which may incur some wasted effort especially if there a lot of cruft
objects and they are freshened infrequently.
Instead, force the freshening code to avoid an optimizing write by
writing out the object loose and letting it pick up a current mtime.
This works because we prefer the mtime of the loose copy of an object
when both a loose and packed one exist (whether or not the packed copy
comes from a cruft pack or not).
This could certainly do with a test and/or be included earlier in this
series/PR, but I want to wait until after I have a chance to clean up
the overly-repetitive nature of the cruft pack tests in general.
Signed-off-by: Taylor Blau <redacted>
---
object-file.c | 2 ++
t/t5328-pack-objects-cruft.sh | 25 +++++++++++++++++++++++++
2 files changed, 27 insertions(+)
To store the individual mtimes of objects in a cruft pack, introduce a
new `.mtimes` format that can optionally accompany a single pack in the
repository.
The format is defined in Documentation/technical/pack-format.txt, and
stores a 4-byte network order timestamp for each object in name (index)
order.
This patch prepares for cruft packs by defining the `.mtimes` format,
and introducing a basic API that callers can use to read out individual
mtimes.
...
+int load_pack_mtimes(struct packed_git *p)
+{
+ char *mtimes_name = NULL;
+ int ret = 0;
+
+ if (!p->is_cruft)
+ return ret; /* not a cruft pack */
+ if (p->mtimes_map)
+ return ret; /* already loaded */
+
+ ret = open_pack_index(p);
+ if (ret < 0)
+ goto cleanup;
+
+ mtimes_name = pack_mtimes_filename(p);
+ ret = load_pack_mtimes_file(mtimes_name,
+ p->num_objects,
+ &p->mtimes_map,
+ &p->mtimes_size);
+ if (ret)
+ goto cleanup;
This looked odd to me, so I supposed that you had some code
that would be inserted between this 'goto cleanup' and the
'cleanup:' label, but I did not find such an insertion in
the remaining patchs. This 'if' can be deleted.
Here is a reroll of my series to implement "cruft packs", a pack which
stores accumulated unreachable objects, along with a new ".mtimes" file
which tracks each object's last known modification time.
This was on the list towards the end of 2021[1], and I have been
accumulating small changes to it locally for a couple of months now.
Major changes since last time include:
- Clearer documentation and commit message(s) to better illustrate how
the feature works and is supposed to be used.
- Some minor documentation updates to pack-format.txt, which make some
ambiguous details more explicit.
- Minor code movement / tweaks to make things easier to read, ensure
that functions aren't introduced in patches before they are used /
etc.
- Moved the new test script to t5328 (instead of t5327, which happens
to be taken up by a new MIDX bitmap-related test), and purged it of
all "rm -fr .git/logs" (replacing them with "git reflog --expire
--all --expire=all" instead).
- A new test which fixes a bug where loose objects which have copies
that appear in a cruft pack would not get accumulated when doing a
`--geometric` repack.
For convenience, a range-diff is below. Thanks in advance for taking
another look!
It had been a while since my last read, so I read the patches
in full one more time. I found a couple nitpicks, but otherwise
everything is looking good.
Thanks,
-Stolee
From: Taylor Blau <hidden> Date: 2022-03-02 21:28:23
On Wed, Mar 02, 2022 at 03:19:57PM -0500, Derrick Stolee wrote:
Nit: just realized this include could be replaced by a struct
declaration:
quoted
struct progress;
struct rev_info;
Like these. 'struct object;' should be enough for the typedef.
Good catch. We would need one for the packed_git struct, too. I don't
have a strong opinion about including object.h or not, though needing
two stubs pushes me slightly in the direction of leaving the include
alone.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-03-02 21:33:42
On Wed, Mar 02, 2022 at 03:22:18PM -0500, Derrick Stolee wrote:
quoted
+ ret = load_pack_mtimes_file(mtimes_name,
+ p->num_objects,
+ &p->mtimes_map,
+ &p->mtimes_size);
+ if (ret)
+ goto cleanup;
This looked odd to me, so I supposed that you had some code
that would be inserted between this 'goto cleanup' and the
'cleanup:' label, but I did not find such an insertion in
the remaining patchs. This 'if' can be deleted.
Thanks for spotting. My gut was that there must be something in the
range-diff between this and the previous round, but there isn't. So this
code has always been there.
It likely comes from load_pack_revindex_from_disk(), which assigns the
`revindex_data` member of `struct packed_git` after calling
load_revindex_from_disk(), but only if it returned zero.
We don't have to assign mtimes_data here (since it doesn't exist, and)
because all of our reads into mtimes_map are offset by 3 to adjust for
the width of the header.
Anyway, we don't need this if statement here, so I'll drop it.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-03-02 21:36:39
On Wed, Mar 02, 2022 at 03:23:05PM -0500, Derrick Stolee wrote:
quoted
For convenience, a range-diff is below. Thanks in advance for taking
another look!
It had been a while since my last read, so I read the patches
in full one more time. I found a couple nitpicks, but otherwise
everything is looking good.
Thanks for reading! I took both of your suggestions (along with Junio's
to rename the test script to t5329 to avoid a clash with your series)
and will re-submit a tiny reroll shortly.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2022-03-03 00:20:53
Create a technical document to explain cruft packs. It contains a brief
overview of the problem, some background, details on the implementation,
and a couple of alternative approaches not considered here.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/Makefile | 1 +
Documentation/technical/cruft-packs.txt | 97 +++++++++++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 Documentation/technical/cruft-packs.txt
@@ -0,0 +1,97 @@+= Cruft packs++The cruft packs feature offer an alternative to Git's traditional mechanism of+removing unreachable objects. This document provides an overview of Git's+pruning mechanism, and how a cruft pack can be used instead to accomplish the+same.++== Background++To remove unreachable objects from your repository, Git offers `git repack -Ad`+(see linkgit:git-repack[1]). Quoting from the documentation:++[quote]+[...] unreachable objects in a previous pack become loose, unpacked objects,+instead of being left in the old pack. [...] loose unreachable objects will be+pruned according to normal expiry rules with the next 'git gc' invocation.++Unreachable objects aren't removed immediately, since doing so could race with+an incoming push which may reference an object which is about to be deleted.+Instead, those unreachable objects are stored as loose object and stay that way+until they are older than the expiration window, at which point they are removed+by linkgit:git-prune[1].++Git must store these unreachable objects loose in order to keep track of their+per-object mtimes. If these unreachable objects were written into one big pack,+then either freshening that pack (because an object contained within it was+re-written) or creating a new pack of unreachable objects would cause the pack's+mtime to get updated, and the objects within it would never leave the expiration+window. Instead, objects are stored loose in order to keep track of the+individual object mtimes and avoid a situation where all cruft objects are+freshened at once.++This can lead to undesirable situations when a repository contains many+unreachable objects which have not yet left the grace period. Having large+directories in the shards of `.git/objects` can lead to decreased performance in+the repository. But given enough unreachable objects, this can lead to inode+starvation and degrade the performance of the whole system. Since we+can never pack those objects, these repositories often take up a large amount of+disk space, since we can only zlib compress them, but not store them in delta+chains.++== Cruft packs++A cruft pack eliminates the need for storing unreachable objects in a loose+state by including the per-object mtimes in a separate file alongside a single+pack containing all loose objects.++A cruft pack is written by `git repack --cruft` when generating a new pack.+linkgit:git-pack-objects[1]'s `--cruft` option. Note that `git repack --cruft`+is a classic all-into-one repack, meaning that everything in the resulting pack is+reachable, and everything else is unreachable. Once written, the `--cruft`+option instructs `git repack` to generate another pack containing only objects+not packed in the previous step (which equates to packing all unreachable+objects together). This progresses as follows:++ 1. Enumerate every object, marking any object which is (a) not contained in a+ kept-pack, and (b) whose mtime is within the grace period as a traversal+ tip.++ 2. Perform a reachability traversal based on the tips gathered in the previous+ step, adding every object along the way to the pack.++ 3. Write the pack out, along with a `.mtimes` file that records the per-object+ timestamps.++This mode is invoked internally by linkgit:git-repack[1] when instructed to+write a cruft pack. Crucially, the set of in-core kept packs is exactly the set+of packs which will not be deleted by the repack; in other words, they contain+all of the repository's reachable objects.++When a repository already has a cruft pack, `git repack --cruft` typically only+adds objects to it. An exception to this is when `git repack` is given the+`--cruft-expiration` option, which allows the generated cruft pack to omit+expired objects instead of waiting for linkgit:git-gc[1] to expire those objects+later on.++It is linkgit:git-gc[1] that is typically responsible for removing expired+unreachable objects.++== Alternatives++Notable alternatives to this design include:++ - The location of the per-object mtime data, and+ - Storing unreachable objects in multiple cruft packs.++On the location of mtime data, a new auxiliary file tied to the pack was chosen+to avoid complicating the `.idx` format. If the `.idx` format were ever to gain+support for optional chunks of data, it may make sense to consolidate the+`.mtimes` format into the `.idx` itself.++Storing unreachable objects among multiple cruft packs (e.g., creating a new+cruft pack during each repacking operation including only unreachable objects+which aren't already stored in an earlier cruft pack) is significantly more+complicated to construct, and so aren't pursued here. The obvious drawback to+the current implementation is that the entire cruft pack must be re-written from+scratch.
From: Taylor Blau <hidden> Date: 2022-03-03 00:20:55
This structure will be used to communicate the per-object mtimes when
writing a cruft pack. Here, we need the full packing_data structure
because the mtime information is stored in an array there, not on the
individual object_entry's themselves (to avoid paying the overhead in
structure width for operations which do not generate a cruft pack).
We haven't passed this information down before because one of the two
callers (in bulk-checkin.c) does not have a packing_data structure at
all. In that case (where no cruft pack will be generated), NULL is
passed instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 3 ++-
bulk-checkin.c | 2 +-
pack-write.c | 1 +
pack.h | 3 +++
4 files changed, 7 insertions(+), 2 deletions(-)
From: Taylor Blau <hidden> Date: 2022-03-03 00:20:55
To store the individual mtimes of objects in a cruft pack, introduce a
new `.mtimes` format that can optionally accompany a single pack in the
repository.
The format is defined in Documentation/technical/pack-format.txt, and
stores a 4-byte network order timestamp for each object in name (index)
order.
This patch prepares for cruft packs by defining the `.mtimes` format,
and introducing a basic API that callers can use to read out individual
mtimes.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/technical/pack-format.txt | 19 ++++
Makefile | 1 +
builtin/repack.c | 1 +
object-store.h | 5 +-
pack-mtimes.c | 126 ++++++++++++++++++++++++
pack-mtimes.h | 15 +++
packfile.c | 19 +++-
7 files changed, 183 insertions(+), 3 deletions(-)
create mode 100644 pack-mtimes.c
create mode 100644 pack-mtimes.h
@@ -294,6 +294,25 @@ Pack file entry: <+ All 4-byte numbers are in network order.+== pack-*.mtimes files have the format:++ - A 4-byte magic number '0x4d544d45' ('MTME').++ - A 4-byte version identifier (= 1).++ - A 4-byte hash function identifier (= 1 for SHA-1, 2 for SHA-256).++ - A table of 4-byte unsigned integers in network order. The ith+ value is the modification time (mtime) of the ith object in the+ corresponding pack by lexicographic (index) order. The mtimes+ count standard epoch seconds.++ - A trailer, containing a checksum of the corresponding packfile,+ and a checksum of all of the above (each having length according+ to the specified hash function).++All 4-byte numbers are in network order.+ == multi-pack-index (MIDX) files have the following format: The multi-pack-index files refer to multiple pack-files and loose objects.
@@ -0,0 +1,126 @@+#include"pack-mtimes.h"+#include"object-store.h"+#include"packfile.h"++staticchar*pack_mtimes_filename(structpacked_git*p)+{+size_tlen;+if(!strip_suffix(p->pack_name,".pack",&len))+BUG("pack_name does not end in .pack");+/* NEEDSWORK: this could reuse code from pack-revindex.c. */+returnxstrfmt("%.*s.mtimes",(int)len,p->pack_name);+}++#define MTIMES_HEADER_SIZE (12)+#define MTIMES_MIN_SIZE (MTIMES_HEADER_SIZE + (2 * the_hash_algo->rawsz))++structmtimes_header{+uint32_tsignature;+uint32_tversion;+uint32_thash_id;+};++staticintload_pack_mtimes_file(char*mtimes_file,+uint32_tnum_objects,+constuint32_t**data_p,size_t*len_p)+{+intfd,ret=0;+structstatst;+void*data=NULL;+size_tmtimes_size;+structmtimes_headerheader;+uint32_t*hdr;++fd=git_open(mtimes_file);++if(fd<0){+ret=-1;+gotocleanup;+}+if(fstat(fd,&st)){+ret=error_errno(_("failed to read %s"),mtimes_file);+gotocleanup;+}++mtimes_size=xsize_t(st.st_size);++if(mtimes_size<MTIMES_MIN_SIZE){+ret=error(_("mtimes file %s is too small"),mtimes_file);+gotocleanup;+}++if(mtimes_size-MTIMES_MIN_SIZE!=st_mult(sizeof(uint32_t),num_objects)){+ret=error(_("mtimes file %s is corrupt"),mtimes_file);+gotocleanup;+}++data=hdr=xmmap(NULL,mtimes_size,PROT_READ,MAP_PRIVATE,fd,0);++header.signature=ntohl(hdr[0]);+header.version=ntohl(hdr[1]);+header.hash_id=ntohl(hdr[2]);++if(header.signature!=MTIMES_SIGNATURE){+ret=error(_("mtimes file %s has unknown signature"),mtimes_file);+gotocleanup;+}++if(header.version!=1){+ret=error(_("mtimes file %s has unsupported version %"PRIu32),+mtimes_file,header.version);+gotocleanup;+}++if(!(header.hash_id==1||header.hash_id==2)){+ret=error(_("mtimes file %s has unsupported hash id %"PRIu32),+mtimes_file,header.hash_id);+gotocleanup;+}++cleanup:+if(ret){+if(data)+munmap(data,mtimes_size);+}else{+*len_p=mtimes_size;+*data_p=(constuint32_t*)data;+}++close(fd);+returnret;+}++intload_pack_mtimes(structpacked_git*p)+{+char*mtimes_name=NULL;+intret=0;++if(!p->is_cruft)+returnret;/* not a cruft pack */+if(p->mtimes_map)+returnret;/* already loaded */++ret=open_pack_index(p);+if(ret<0)+gotocleanup;++mtimes_name=pack_mtimes_filename(p);+ret=load_pack_mtimes_file(mtimes_name,+p->num_objects,+&p->mtimes_map,+&p->mtimes_size);+cleanup:+free(mtimes_name);+returnret;+}++uint32_tnth_packed_mtime(structpacked_git*p,uint32_tpos)+{+if(!p->mtimes_map)+BUG("pack .mtimes file not loaded for %s",p->pack_name);+if(p->num_objects<=pos)+BUG("pack .mtimes out-of-bounds (%"PRIu32" vs %"PRIu32")",+pos,p->num_objects);++returnget_be32(p->mtimes_map+pos+3);+}
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:02
There are three definitions of an identical function which converts
`the_hash_algo` into either 1 (for SHA-1) or 2 (for SHA-256). There is a
copy of this function for writing both the commit-graph and
multi-pack-index file, and another inline definition used to write the
.rev header.
Consolidate these into a single definition in chunk-format.h. It's not
clear that this is the best header to define this function in, but it
should do for now.
(Worth noting, the .rev caller expects a 4-byte unsigned, but the other
two callers work with a single unsigned byte. The consolidated version
uses the latter type, and lets the compiler widen it when required).
Another caller will be added in a subsequent patch.
Signed-off-by: Taylor Blau <redacted>
---
chunk-format.c | 12 ++++++++++++
chunk-format.h | 3 +++
commit-graph.c | 18 +++---------------
midx.c | 18 +++---------------
pack-write.c | 15 ++-------------
5 files changed, 23 insertions(+), 43 deletions(-)
@@ -365,9 +353,9 @@ struct commit_graph *parse_commit_graph(struct repository *r,}hash_version=*(unsignedchar*)(data+5);-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("commit-graph hash version %X does not match version %X"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));returnNULL;}
@@ -1911,7 +1899,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)hashwrite_be32(f,GRAPH_SIGNATURE);hashwrite_u8(f,GRAPH_VERSION);-hashwrite_u8(f,oid_version());+hashwrite_u8(f,oid_version(the_hash_algo));hashwrite_u8(f,get_num_chunks(cf));hashwrite_u8(f,ctx->num_commit_graphs_after-1);
@@ -134,9 +122,9 @@ struct multi_pack_index *load_multi_pack_index(const char *object_dir, int localm->version);hash_version=m->data[MIDX_BYTE_HASH_VERSION];-if(hash_version!=oid_version()){+if(hash_version!=oid_version(the_hash_algo)){error(_("multi-pack-index hash version %u does not match version %u"),-hash_version,oid_version());+hash_version,oid_version(the_hash_algo));gotocleanup_fail;}m->hash_len=the_hash_algo->rawsz;
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:05
Now that the `.mtimes` format is defined, supplement the pack-write API
to be able to conditionally write an `.mtimes` file along with a pack by
setting an additional flag and passing an oidmap that contains the
timestamps corresponding to each object in the pack.
Signed-off-by: Taylor Blau <redacted>
---
pack-objects.c | 6 ++++
pack-objects.h | 25 ++++++++++++++++
pack-write.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++
pack.h | 1 +
4 files changed, 109 insertions(+)
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:05
In the next patch, we will implement and test support for writing a
cruft pack via a special mode of `git pack-objects`. To make sure that
objects are written with the correct timestamps, and a new test-tool
that can dump the object names and corresponding timestamps from a given
`.mtimes` file.
Signed-off-by: Taylor Blau <redacted>
---
Makefile | 1 +
t/helper/test-pack-mtimes.c | 56 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
4 files changed, 59 insertions(+)
create mode 100644 t/helper/test-pack-mtimes.c
@@ -0,0 +1,56 @@+#include"git-compat-util.h"+#include"test-tool.h"+#include"strbuf.h"+#include"object-store.h"+#include"packfile.h"+#include"pack-mtimes.h"++staticvoiddump_mtimes(structpacked_git*p)+{+uint32_ti;+if(load_pack_mtimes(p)<0)+die("could not load pack .mtimes");++for(i=0;i<p->num_objects;i++){+structobject_idoid;+if(nth_packed_object_id(&oid,p,i)<0)+die("could not load object id at position %"PRIu32,i);++printf("%s %"PRIu32"\n",+oid_to_hex(&oid),nth_packed_mtime(p,i));+}+}++staticconstchar*pack_mtimes_usage="\n"+" test-tool pack-mtimes <pack-name.mtimes>";++intcmd__pack_mtimes(intargc,constchar**argv)+{+structstrbufbuf=STRBUF_INIT;+structpacked_git*p;++setup_git_directory();++if(argc!=2)+usage(pack_mtimes_usage);++for(p=get_all_packs(the_repository);p;p=p->next){+strbuf_addstr(&buf,basename(p->pack_name));+strbuf_strip_suffix(&buf,".pack");+strbuf_addstr(&buf,".mtimes");++if(!strcmp(buf.buf,argv[1]))+break;++strbuf_reset(&buf);+}++strbuf_release(&buf);++if(!p)+die("could not find pack '%s'",argv[1]);++dump_mtimes(p);++return0;+}
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:16
A new caller in the next commit will want to immediately modify the
object_entry structure created by create_object_entry(). Instead of
forcing that caller to wastefully look-up the entry we just created,
return it from create_object_entry() instead.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:17
Teach `pack-objects` how to generate a cruft pack when no objects are
dropped (i.e., `--cruft-expiration=never`). Later patches will teach
`pack-objects` how to generate a cruft pack that prunes objects.
When generating a cruft pack which does not prune objects, we want to
collect all unreachable objects into a single pack (noting and updating
their mtimes as we accumulate them). Ordinary use will pass the result
of a `git repack -A` as a kept pack, so when this patch says "kept
pack", readers should think "reachable objects".
Generating a non-expiring cruft packs works as follows:
- Callers provide a list of every pack they know about, and indicate
which packs are about to be removed.
- All packs which are going to be removed (we'll call these the
redundant ones) are marked as kept in-core.
Any packs the caller did not mention (but are known to the
`pack-objects` process) are also marked as kept in-core. Packs not
mentioned by the caller are assumed to be unknown to them, i.e.,
they entered the repository after the caller decided which packs
should be kept and which should be discarded.
Since we do not want to include objects in these "unknown" packs
(because we don't know which of their objects are or aren't
reachable), these are also marked as kept in-core.
- Then, we enumerate all objects in the repository, and add them to
our packing list if they do not appear in an in-core kept pack.
This results in a new cruft pack which contains all known objects that
aren't included in the kept packs. When the kept pack is the result of
`git repack -A`, the resulting pack contains all unreachable objects.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 30 ++++
builtin/pack-objects.c | 201 +++++++++++++++++++++++++-
object-file.c | 2 +-
object-store.h | 2 +
t/t5329-pack-objects-cruft.sh | 218 +++++++++++++++++++++++++++++
5 files changed, 448 insertions(+), 5 deletions(-)
create mode 100755 t/t5329-pack-objects-cruft.sh
@@ -95,6 +96,35 @@ base-name:: Incompatible with `--revs`, or options that imply `--revs` (such as `--all`), with the exception of `--unpacked`, which is compatible.+--cruft::+ Packs unreachable objects into a separate "cruft" pack, denoted+ by the existence of a `.mtimes` file. Typically used by `git+ repack --cruft`. Callers provide a list of pack names and+ indicate which packs will remain in the repository, along with+ which packs will be deleted (indicated by the `-` prefix). The+ contents of the cruft pack are all objects not contained in the+ surviving packs which have not exceeded the grace period (see+ `--cruft-expiration` below), or which have exceeded the grace+ period, but are reachable from an other object which hasn't.+++When the input lists a pack containing all reachable objects (and lists+all other packs as pending deletion), the corresponding cruft pack will+contain all unreachable objects (with mtime newer than the+`--cruft-expiration`) along with any unreachable objects whose mtime is+older than the `--cruft-expiration`, but are reachable from an+unreachable object whose mtime is newer than the `--cruft-expiration`).+++Incompatible with `--unpack-unreachable`, `--keep-unreachable`,+`--pack-loose-unreachable`, `--stdin-packs`, as well as any other+options which imply `--revs`. Also incompatible with `--max-pack-size`;+when this option is set, the maximum pack size is not inferred from+`pack.packSizeLimit`.++--cruft-expiration=<approxidate>::+ If specified, objects are eliminated from the cruft pack if they+ have an mtime older than `<approxidate>`. If unspecified (and+ given `--cruft`), then no objects are eliminated.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -3389,6 +3395,135 @@ static void read_packs_list_from_stdin(void)string_list_clear(&exclude_packs,0);}+staticvoidadd_cruft_object_entry(conststructobject_id*oid,enumobject_typetype,+structpacked_git*pack,off_toffset,+constchar*name,uint32_tmtime)+{+structobject_entry*entry;++display_progress(progress_state,++nr_seen);++entry=packlist_find(&to_pack,oid);+if(entry){+if(name){+entry->hash=pack_name_hash(name);+entry->no_try_delta=no_try_delta(name);+}+}else{+if(!want_object_in_pack(oid,0,&pack,&offset))+return;+if(!pack&&type==OBJ_BLOB&&!has_loose_object(oid)){+/*+*Ifatraversedtreehasamissingblobthenwewant+*toavoidaddingthatmissingobjecttoourpack.+*+*Thisonlyappliestomissingblobs,nottrees,+*becausethetraversalneedstoparsesub-treesbut+*notblobs.+*+*Noteweonlyperformthischeckwhenwecouldn't+*alreadyfindtheobjectinapack,sowe'rereally+*limitedto"ensure non-tip blobs which don't exist in+*packsdoexistvialooseobjects". Confused?+*/+return;+}++entry=create_object_entry(oid,type,pack_name_hash(name),+0,name&&no_try_delta(name),+pack,offset);+}++if(mtime>oe_cruft_mtime(&to_pack,entry))+oe_set_cruft_mtime(&to_pack,entry,mtime);+return;+}++staticvoidmark_pack_kept_in_core(structstring_list*packs,unsignedkeep)+{+structstring_list_item*item=NULL;+for_each_string_list_item(item,packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=keep;+}+}++staticvoidadd_unreachable_loose_objects(void);+staticvoidadd_objects_in_unpacked_packs(void);++staticvoidenumerate_cruft_objects(void)+{+if(progress)+progress_state=start_progress(_("Enumerating cruft objects"),0);++add_objects_in_unpacked_packs();+add_unreachable_loose_objects();++stop_progress(&progress_state);+}++staticvoidread_cruft_objects(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listdiscard_packs=STRING_LIST_INIT_DUP;+structstring_listfresh_packs=STRING_LIST_INIT_DUP;+structpacked_git*p;++ignore_packed_keep_in_core=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='-')+string_list_append(&discard_packs,buf.buf+1);+else+string_list_append(&fresh_packs,buf.buf);+strbuf_reset(&buf);+}++string_list_sort(&discard_packs);+string_list_sort(&fresh_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);+structstring_list_item*item;++item=string_list_lookup(&fresh_packs,pack_name);+if(!item)+item=string_list_lookup(&discard_packs,pack_name);++if(item){+item->util=p;+}else{+/*+*Thispackwasn'tmentionedineitherthe"fresh"or+*"discard"list,sothecallerdidn'tknowaboutit.+*+*Markitaskeptsothatitsobjectsareignoredby+*add_unseen_recent_objects_to_traversal().We'll+*unmarkitbeforestartingthetraversalsoitdoesn't+*haltthetraversalearly.+*/+p->pack_keep_in_core=1;+}+}++mark_pack_kept_in_core(&fresh_packs,1);+mark_pack_kept_in_core(&discard_packs,0);++if(cruft_expiration)+die("--cruft-expiration not yet implemented");+else+enumerate_cruft_objects();++strbuf_release(&buf);+string_list_clear(&discard_packs,0);+string_list_clear(&fresh_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3521,7 +3656,24 @@ static int add_object_in_unpacked_pack(const struct object_id *oid,uint32_tpos,void*_data){-add_object_entry(oid,OBJ_NONE,"",0);+if(cruft){+off_toffset;+time_tmtime;++if(pack->is_cruft){+if(load_pack_mtimes(pack)<0)+die(_("could not load cruft pack .mtimes"));+mtime=nth_packed_mtime(pack,pos);+}else{+mtime=pack->mtime;+}+offset=nth_packed_object_offset(pack,pos);++add_cruft_object_entry(oid,OBJ_NONE,pack,offset,+NULL,mtime);+}else{+add_object_entry(oid,OBJ_NONE,"",0);+}return0;}
@@ -3545,7 +3697,19 @@ static int add_loose_object(const struct object_id *oid, const char *path,return0;}-add_object_entry(oid,type,"",0);+if(cruft){+structstatst;+if(stat(path,&st)<0){+if(errno==ENOENT)+return0;+returnerror_errno("unable to stat %s",oid_to_hex(oid));+}++add_cruft_object_entry(oid,type,NULL,0,NULL,+st.st_mtime);+}else{+add_object_entry(oid,type,"",0);+}return0;}
@@ -3864,6 +4028,20 @@ static int option_parse_unpack_unreachable(const struct option *opt,return0;}+staticintoption_parse_cruft_expiration(conststructoption*opt,+constchar*arg,intunset)+{+if(unset){+cruft=0;+cruft_expiration=0;+}else{+cruft=1;+if(arg)+cruft_expiration=approxidate(arg);+}+return0;+}+intcmd_pack_objects(intargc,constchar**argv,constchar*prefix){intuse_internal_rev_list=0;
@@ -3936,6 +4114,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_CALLBACK_F(0,"unpack-unreachable",NULL,N_("time"),N_("unpack unreachable objects newer than <time>"),PARSE_OPT_OPTARG,option_parse_unpack_unreachable),+OPT_BOOL(0,"cruft",&cruft,N_("create a cruft pack")),+OPT_CALLBACK_F(0,"cruft-expiration",NULL,N_("time"),+N_("expire cruft objects older than <time>"),+PARSE_OPT_OPTARG,option_parse_cruft_expiration),OPT_BOOL(0,"sparse",&sparse,N_("use the sparse reachability algorithm")),OPT_BOOL(0,"thin",&thin,
@@ -4062,7 +4244,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(!HAVE_THREADS&&delta_search_threads!=1)warning(_("no threads support, ignoring --threads"));-if(!pack_to_stdout&&!pack_size_limit)+if(!pack_to_stdout&&!pack_size_limit&&!cruft)pack_size_limit=pack_size_limit_cfg;if(pack_to_stdout&&pack_size_limit)die(_("--max-pack-size cannot be used to build a pack for transfer"));
@@ -4089,6 +4271,15 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(stdin_packs&&use_internal_rev_list)die(_("cannot use internal rev list with --stdin-packs"));+if(cruft){+if(use_internal_rev_list)+die(_("cannot use internal rev list with --cruft"));+if(stdin_packs)+die(_("cannot use --stdin-packs with --cruft"));+if(pack_size_limit)+die(_("cannot use --max-pack-size with --cruft"));+}+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -0,0 +1,218 @@+#!/bin/sh++test_description='cruft pack related pack-objects tests'+../test-lib.sh++objdir=.git/objects+packdir=$objdir/pack++basic_cruft_pack_tests(){+expire="$1"++test_expect_success"unreachable loose objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitbase&&+gitrepack-Ad&&+test_commitloose&&++test-toolchmtime+2000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose:loose.t))" &&+test-toolchmtime+1000"$objdir/$(test_oid_to_path\+$(gitrev-parseloose^{tree}))" &&++(+gitrev-list--objects--no-object-namesbase..loose|+whilereadoid+do+path="$objdir/$(test_oid_to_path"$oid")"&&+printf"%s %d\n""$oid""$(test-toolchmtime--get"$path")"+done|+sort-k1+)>expect&&++keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+cruft="$(echo$keep|gitpack-objects--cruft\+--cruft-expiration="$expire"$packdir/pack)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable packed objects are packed (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&+other="$(gitpack-objects--delta-base-offset\+$packdir/pack<objects)" &&+gitprune-packed&&++test-toolchmtime--get-100"$packdir/pack-$other.pack">expect&&++cruft="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$other.pack+EOF+)" &&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++cut-d" "-f2<actual.raw|sort-u>actual&&++test_cmpexpectactual+)+'++test_expect_success"unreachable cruft objects are repacked (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitpacked&&+gitrepack-Ad&&+test_commitother&&++gitrev-list--objects--no-object-namespacked..>objects&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++cruft_a="$(echo$keep|gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack)"&&+gitprune-packed&&+cruft_b="$(gitpack-objects--cruft--cruft-expiration="$expire"$packdir/pack<<-EOF+$keep+-pack-$cruft_a.pack+EOF+)" &&++test-toolpack-mtimes"pack-$cruft_a.mtimes">expect.raw&&+test-toolpack-mtimes"pack-$cruft_b.mtimes">actual.raw&&++sort<expect.raw>expect&&+sort<actual.raw>actual&&++test_cmpexpectactual+)+'++test_expect_success"multiple cruft packs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+gitrepack-Ad&&+keep="$(basename"$(ls$packdir/pack-*.pack)")"&&++test_commitcruft&&+loose="$objdir/$(test_oid_to_path$(gitrev-parsecruft))"&&++# generate three copies of the cruft object in different+# cruft packs, each with a unique mtime:+# - one expired (1000 seconds ago)+# - two non-expired (one 1000 seconds in the future,+# one 1500 seconds in the future)+test-toolchmtime=-1000"$loose"&&+gitpack-objects--cruft$packdir/pack-A<<-EOF&&+$keep+EOF+test-toolchmtime=+1000"$loose"&&+gitpack-objects--cruft$packdir/pack-B<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+EOF+test-toolchmtime=+1500"$loose"&&+gitpack-objects--cruft$packdir/pack-C<<-EOF&&+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+EOF++# ensure the resulting cruft pack takes the most recent+# mtime among all copies+cruft="$(gitpack-objects--cruft\+--cruft-expiration="$expire"\+$packdir/pack<<-EOF+$keep+-$(basename$(ls$packdir/pack-A-*.pack))+-$(basename$(ls$packdir/pack-B-*.pack))+-$(basename$(ls$packdir/pack-C-*.pack))+EOF+)" &&++test-toolpack-mtimes"$(basename$(ls$packdir/pack-C-*.mtimes))">expect.raw&&+test-toolpack-mtimes"pack-$cruft.mtimes">actual.raw&&++sortexpect.raw>expect&&+sortactual.raw>actual&&+test_cmpexpectactual+)+'++test_expect_success"cruft packs tolerate missing trees (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++tree="$(gitrev-parsecruft^{tree})"&&++gitreset--hardreachable&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++# remove the unreachable tree, but leave the commit+# which has it as its root tree intact+rm-fr"$objdir/$(test_oid_to_path"$tree")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'++test_expect_success"cruft packs tolerate missing blobs (expire $expire)"'+gitinitrepo&&+test_when_finished"rm -fr repo"&&+(+cdrepo&&++test_commitreachable&&+test_commitcruft&&++blob="$(gitrev-parsecruft:cruft.t)"&&++gitreset--hardreachable&&+gittag-dcruft&&+gitreflogexpire--all--expire=all&&++# remove the unreachable blob, but leave the commit (and+# the root tree of that commit) intact+rm-fr"$objdir/$(test_oid_to_path"$blob")"&&++gitrepack-Ad&&+basename$(ls$packdir/pack-*.pack)>in&&+gitpack-objects--cruft--cruft-expiration="$expire"\+$packdir/pack<in+)+'+}++basic_cruft_pack_testsnever++test_done
From: Taylor Blau <hidden> Date: 2022-03-03 00:21:19
This function behaves very similarly to what we will need in
pack-objects in order to implement cruft packs with expiration. But it
is lacking a couple of things. Namely, it needs:
- a mechanism to communicate the timestamps of individual recent
objects to some external caller
- and, in the case of packed objects, our future caller will also want
to know the originating pack, as well as the offset within that pack
at which the object can be found
- finally, it needs a way to skip over packs which are marked as kept
in-core.
To address the first two, add a callback interface in this patch which
reports the time of each recent object, as well as a (packed_git,
off_t) pair for packed objects.
Likewise, add a new option to the packed object iterators to skip over
packs which are marked as kept in core. This option will become
implicitly tested in a future patch.
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 2 +-
reachable.c | 51 +++++++++++++++++++++++++++++++++++-------
reachable.h | 9 +++++++-
3 files changed, 52 insertions(+), 10 deletions(-)
@@ -126,7 +146,7 @@ static int add_recent_loose(const struct object_id *oid,returnerror_errno("unable to stat %s",oid_to_hex(oid));}-add_recent_object(oid,st.st_mtime,data);+add_recent_object(oid,NULL,0,st.st_mtime,data);return0;}
@@ -134,29 +154,43 @@ static int add_recent_packed(const struct object_id *oid,structpacked_git*p,uint32_tpos,void*data){-structobject*obj=lookup_object(the_repository,oid);+structobject*obj;++if(!want_recent_object(data,oid))+return0;++obj=lookup_object(the_repository,oid);if(obj&&obj->flags&SEEN)return0;-add_recent_object(oid,p->mtime,data);+add_recent_object(oid,p,nth_packed_object_offset(p,pos),p->mtime,data);return0;}intadd_unseen_recent_objects_to_traversal(structrev_info*revs,-timestamp_ttimestamp)+timestamp_ttimestamp,+report_recent_object_fn*cb,+intignore_in_core_kept_packs){structrecent_datadata;+enumfor_each_object_flagsflags;intr;data.revs=revs;data.timestamp=timestamp;+data.cb=cb;+data.ignore_in_core_kept_packs=ignore_in_core_kept_packs;r=for_each_loose_object(add_recent_loose,&data,FOR_EACH_OBJECT_LOCAL_ONLY);if(r)returnr;-returnfor_each_packed_object(add_recent_packed,&data,-FOR_EACH_OBJECT_LOCAL_ONLY);++flags=FOR_EACH_OBJECT_LOCAL_ONLY|FOR_EACH_OBJECT_PACK_ORDER;+if(ignore_in_core_kept_packs)+flags|=FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS;++returnfor_each_packed_object(add_recent_packed,&data,flags);}staticintmark_object_seen(conststructobject_id*oid,
@@ -217,7 +251,8 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,if(mark_recent){revs->ignore_missing_links=1;-if(add_unseen_recent_objects_to_traversal(revs,mark_recent))+if(add_unseen_recent_objects_to_traversal(revs,mark_recent,+NULL,0))die("unable to mark recent objects");if(prepare_revision_walk(revs))die("revision walk setup failed");