git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v3 2/5] pack-objects: reset kept-pack cache for cruft walk

From
QGQin ShiCheng via GitGitGadget <gitgitgadget@gmail.com>
Date
Oct 8, 2026, 09:52 UTC
Message-ID
<58019e4983586888191ca16e042511d257f46d4b.1791453141.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2219.v3.git.1791453141.gitgitgadget@gmail.com>
From: Qin ShiCheng <qeesung@live.com>

When writing a cruft pack with an expiration, pack-objects first collects the recent objects and then walks from them to rescue whatever they reach, expired or not. A pack the caller did not list is marked kept while collecting, so that its objects are not copied into the cruft pack, and unmarked before the walk, so that the walk can go through it.

The walk does not see the unmarking. Whether an object sits in a kept pack is answered from a cache that is built on first use and only dropped when asked about a different kind of kept pack. Collecting builds it while the unlisted pack is still marked, the walk asks the same kind of question, and so the unlisted pack stays in it: the walk stops there, and whatever lies beyond it in an expired pack is left out of the cruft pack, to go when that pack is deleted.

This went unnoticed because of "--honor-pack-keep". repack passes it, and when there is a ".keep" file it makes the collecting side ask about on-disk and in-core kept packs together while the walk asks about in-core ones alone; the cache is rebuilt each time the question changes, and by accident the walk sees the current marks. Take the ".keep" file away and the objects are lost today. A later commit stops repack from passing "--honor-pack-keep" at all, so fix this first.

Drop the cache after re-marking. The loop over the object sources that does so lives in packfile.c, as repo_invalidate_kept_pack_caches(), next to has_object_kept_pack() which reads the cache. Like it, the loop assumes every source is a files backend; keeping that assumption in packfile.c rather than adding it to pack-objects means the two can move together once packfile management is pushed down into that backend.

The test builds an unreachable chain whose middle commit sits in a pack pack-objects is not told about and whose oldest objects have expired; without the fix the cruft pack holds only the recent tip.

Signed-off-by: Qin ShiCheng <qeesung@live.com>
---
 builtin/pack-objects.c        |  3 +++
 odb/source-packed.h           |  3 ++-
 packfile.c                    | 19 +++++++++++++++--
 packfile.h                    |  7 ++++++
 t/t5329-pack-objects-cruft.sh | 40 +++++++++++++++++++++++++++++++++++
 5 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 6f579173b0..f86b3661c5 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -4301,10 +4301,13 @@ static void enumerate_and_traverse_cruft_objects(struct string_list *fresh_packs
 	/*
 	 * Re-mark only the fresh packs as kept so that objects in
 	 * unknown packs do not halt the reachability traversal early.
+	 * The kept-pack cache was built while those packs were still
+	 * marked, so drop it too.
 	 */
 	repo_for_each_pack(the_repository, p)
 		p->pack_keep_in_core = 0;
 	mark_pack_kept_in_core(fresh_packs, 1);
+	repo_invalidate_kept_pack_caches(the_repository);
 
 	if (prepare_revision_walk(&revs))
 		die(_("revision walk setup failed"));
diff --git a/odb/source-packed.h b/odb/source-packed.h
index a0f6b5096d..c8c6088a69 100644
--- a/odb/source-packed.h
+++ b/odb/source-packed.h
@@ -25,7 +25,8 @@ struct odb_source_packed {
 	 * Should not be accessed directly, but via
 	 * `packfile_store_get_kept_pack_cache()`. The list of packs gets
 	 * invalidated when the stored flags and the flags passed to
-	 * `packfile_store_get_kept_pack_cache()` mismatch.
+	 * `packfile_store_get_kept_pack_cache()` mismatch, or explicitly via
+	 * `repo_invalidate_kept_pack_caches()`.
 	 */
 	struct {
 		struct packed_git **packs;
diff --git a/packfile.c b/packfile.c
index 4fa5fd67c8..399c0622cd 100644
--- a/packfile.c
+++ b/packfile.c
@@ -1870,6 +1870,22 @@ int packfile_fill_entry(struct packed_git *p,
 	return 1;
 }
 
+static void invalidate_kept_pack_cache(struct odb_source_packed *store)
+{
+	FREE_AND_NULL(store->kept_cache.packs);
+	store->kept_cache.flags = 0;
+}
+
+void repo_invalidate_kept_pack_caches(struct repository *r)
+{
+	struct odb_source *source;
+
+	for (source = r->objects->sources; source; source = source->next) {
+		struct odb_source_files *files = odb_source_files_downcast(source);
+		invalidate_kept_pack_cache(files->packed);
+	}
+}
+
 static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store,
 					     unsigned flags)
 {
@@ -1877,8 +1893,7 @@ static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store,
 		return;
 	if (store->kept_cache.flags == flags)
 		return;
-	FREE_AND_NULL(store->kept_cache.packs);
-	store->kept_cache.flags = 0;
+	invalidate_kept_pack_cache(store);
 }
 
 struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed *store,
diff --git a/packfile.h b/packfile.h
index 6d30d15a00..e2db6bfff7 100644
--- a/packfile.h
+++ b/packfile.h
@@ -144,6 +144,13 @@ enum kept_pack_type {
 struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed *store,
 						       unsigned flags);
 
+/*
+ * Drop every packfile store's cache of kept packs, so that the next call
+ * to `packfile_store_get_kept_pack_cache()` rebuilds it, e.g. after
+ * changing which packs are kept in core.
+ */
+void repo_invalidate_kept_pack_caches(struct repository *r);
+
 struct pack_window {
 	struct pack_window *next;
 	unsigned char *base;
diff --git a/t/t5329-pack-objects-cruft.sh b/t/t5329-pack-objects-cruft.sh
index 12cda06373..6302f60b75 100755
--- a/t/t5329-pack-objects-cruft.sh
+++ b/t/t5329-pack-objects-cruft.sh
@@ -332,6 +332,46 @@ test_expect_success 'cruft trees rescue sub-trees, blobs' '
 	)
 '
 
+test_expect_success 'cruft traversal rescues through a pack it was not told about' '
+	git init repo &&
+	test_when_finished "rm -fr repo" &&
+	(
+		cd repo &&
+
+		test_commit packed &&
+		git repack -Ad &&
+		keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
+
+		test_commit old &&
+		test_commit mid &&
+		test_commit new &&
+
+		# "old" has expired, "new" is recent, and "mid" sits in a
+		# pack that pack-objects is not told about. Rescuing "old"
+		# from "new" means walking through that pack.
+		git rev-list --objects --no-object-names packed..old >old &&
+		while read object
+		do
+			test-tool chmtime -1000 \
+				"$objdir/$(test_oid_to_path $object)" || exit 1
+		done <old &&
+		git rev-list --objects --no-object-names old..mid |
+		git pack-objects $packdir/pack >/dev/null &&
+		git prune-packed &&
+
+		cruft="$(echo $keep | git pack-objects --cruft \
+			--cruft-expiration=750.seconds.ago \
+			$packdir/pack)" &&
+		test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
+
+		cut -d" " -f1 <actual.raw | sort >actual &&
+		git rev-list --objects --no-object-names packed..new >expect.raw &&
+		sort <expect.raw >expect &&
+
+		test_cmp expect actual
+	)
+'
+
 test_expect_success 'expired objects are pruned' '
 	git init repo &&
 	test_when_finished "rm -fr repo" &&
-- 
gitgitgadget
Previous: Qin ShiCheng via GitGitGadgetNext: Qin ShiCheng via GitGitGadget
Message 21 of 27 in “repack: don't lose objects to a ".keep" that appears mid-run”
  1. 0/6 repack: don't lose objects to a ".keep" that appears mid-runqeesung via GitGitGadget, Sep 14, 2026
  2. 1/6 odb: don't remove a ".keep" we never installedQin ShiCheng via GitGitGadget, Sep 14, 2026
  3. Justin ToblerSep 15, 2026
  4. Qin ShiChengSep 16, 2026
  5. 2/6 pack-objects: keep --keep-pack open when followingQin ShiCheng via GitGitGadget, Sep 14, 2026
  6. 3/6 pack-objects: reset kept-pack cache for cruft walkQin ShiCheng via GitGitGadget, Sep 14, 2026
  7. 4/6 pack-objects: sort --keep-pack list for lookupQin ShiCheng via GitGitGadget, Sep 14, 2026
  8. 5/6 pack-objects: add --keep-pack-from-fileQin ShiCheng via GitGitGadget, Sep 14, 2026
  9. 6/6 repack: tell pack-objects which packs are keptQin ShiCheng via GitGitGadget, Sep 14, 2026
  10. 0/5 repack: don't lose objects to a ".keep" that appears mid-runqeesung via GitGitGadget, Sep 18, 2026
  11. 1/5 pack-objects: keep --keep-pack open when followingQin ShiCheng via GitGitGadget, Sep 18, 2026
  12. 2/5 pack-objects: reset kept-pack cache for cruft walkQin ShiCheng via GitGitGadget, Sep 18, 2026
  13. Junio C HamanoSep 22, 2026
  14. Qin ShiChengSep 23, 2026
  15. Junio C HamanoSep 23, 2026
  16. 4/5 pack-objects: add --keep-pack-from-fileQin ShiCheng via GitGitGadget, Sep 18, 2026
  17. 3/5 pack-objects: sort --keep-pack list for lookupQin ShiCheng via GitGitGadget, Sep 18, 2026
  18. 5/5 repack: tell pack-objects which packs are keptQin ShiCheng via GitGitGadget, Sep 18, 2026
  19. 0/5 repack: don't lose objects to a ".keep" that appears mid-runqeesung via GitGitGadget, Oct 8, 2026
  20. 1/5 pack-objects: keep --keep-pack open when followingQin ShiCheng via GitGitGadget, Oct 8, 2026
  21. 2/5 pack-objects: reset kept-pack cache for cruft walkQin ShiCheng via GitGitGadget, Oct 8, 2026
  22. 3/5 pack-objects: sort --keep-pack list for lookupQin ShiCheng via GitGitGadget, Oct 8, 2026
  23. 4/5 pack-objects: add --keep-pack-from-fileQin ShiCheng via GitGitGadget, Oct 8, 2026
  24. 5/5 repack: tell pack-objects which packs are keptQin ShiCheng via GitGitGadget, Oct 8, 2026
  25. Junio C HamanoOct 8, 2026
  26. Qin ShiChengOct 9, 2026
  27. Junio C HamanoOct 9, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.