From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=209.85.216.41 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793005625; b=5LNCb4JhQGfu/9nB9yP/kqFle3PJvm/Tfb/kz3zFGWOlhzr70cblrX54vFC2AsDlzt17 A99B/z+sIWOrk13KDSKoff3EODeN6QCTuZgivLv6xZVH2DcSpjBl6QIdKzuk54zpIh6Yo rzNsjHTYhU+AjnNMd0YvZ8h5u5iWUraNO7MuRcpqq06F82SkNMQ8lu16WldS63ZtA1gu6 4oYb5aOlo1hxhSMdtF9xuXkzgWzk+i8FSn72TEI0iRnUVc8RV1qq3qAEsYp4TPRGoxGbB YtsVVJ1amFjbOPBADpKsdkTDIa/+iSLJ5FHEl8l7VWF4w+ASVbEWP39kMPZXjLe7P9A== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793005625; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Gm-Message-State:X-Gm-Gg:X-Received:Received:Message-Id:In-Reply-To: References:From:Date:Subject:Fcc:Content-Type: Content-Transfer-Encoding:MIME-Version:To:Cc; bh=hAkuDZBZb8T1s8OWsEKQaaWyQNfZ0iEFPMUDMy+e4Wo=; b=3au2prbx+XuqH+0L3Iw8IfQAlZ7zK1RESdJGMhdEXQb00TdVPtO1RMGdoQrXFTglCtAU tsGw3nb5M7TBdVFPR4xZxe6svnNgg2TjACI96b+WPjMw2EsEVuNIB9eb8tWoDFQdipz2l ON2QXmWHhZExDupC0rs2mCIYtaS7PbA9nScTieBnVU+VP47ySWBM8tnasd+LBJy5MZmuQ V1yIdXpCOXzvgMkMHJnlVyV8O4ZNVs1Bpmkh3YN/oOXVmZKMHdTQ1zJ7OlZpCCrM2huiV Eknbfkx/Wbi1Bf/geUhgRDocPie8XwqTj+++TEqzKsViKSEMXGLEj4/dCzB4vr1oMFA== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=fPGd288X; arc=none smtp.remote-ip=209.85.216.41 Received: from mail-pj1-f41.google.com (mail-pj1-f41.google.com [209.85.216.41]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EDE73606A31A for ; Mon, 26 Oct 2026 09:07:03 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org EDE73606A31A Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=fPGd288X Received: by mail-pj1-f41.google.com with SMTP id 98e67ed59e1d1-3ace9e6e1a5so1985897a91.0 for ; Mon, 26 Oct 2026 02:07:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1793005623; x=1793610423; darn=vger.kernel.org; h=cc:to:mime-version:content-transfer-encoding:content-type:fcc :subject:date:from:references:in-reply-to:message-id:from:to:cc :subject:date:message-id:reply-to:content-type; bh=hAkuDZBZb8T1s8OWsEKQaaWyQNfZ0iEFPMUDMy+e4Wo=; b=fPGd288XwPqMLOoYjKmaFlcEf0srJYmCf7BvK9KabgufapySTEMbjKMUfXazlu0DSc qSQCvifRzoDgmyyPqvCph4DYunLPkw0QfAKASW4JRcZLFDzB++8x5eN8Md9bC7EC/JHV Du9PaMDf/dEAiu34UHUb80YI453I5h2N4dApUn2GwdIh+YJEvXUHPL8V3A+RIelNcg+5 Ahz4pnhrvrOArorc8ZPMS0NcEcF3t3ltE8zmJRP0mS2+5htxDJKLcDAiVgVuKFk8cb7K DjUc39aeQtRCLLKxemKRDUj9N1zJvTrZ27hqSe/CdJoWdxOqMIZxzhiKnY4XrdTufUc0 BE8A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1793005623; x=1793610423; h=cc:to:mime-version:content-transfer-encoding:content-type:fcc :subject:date:from:references:in-reply-to:message-id:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=hAkuDZBZb8T1s8OWsEKQaaWyQNfZ0iEFPMUDMy+e4Wo=; b=rN8y2MKLZIdPl0/zqI3dfh4sCVLzxSQKMPts4WLFlmtD04dPN7YX8MQTxmFfjNCYYO CNKCAbP2idZzALdS8svXEE5sY6n78zU1SywTDZE70E9d24UQJFBUyi/GefAg+5OIoOZ9 1nMPvxzhW24sRSAr/dS5pAGftHRfR+9kLbZT2LuD+FL5BjjKP3YRChn6IwsxLGJcz+KT 9TlRw5ljrS9Q2LT9bnKN0J3f8GtGz2QONnb1eP/TT+IB4AYBdvfThNlLW4ZBf4xhDz3V HzrNTKeR94gYr9B/2oaFuW3nZ+LH2b17wcFx4Gf13pHsOTe4zXddmjFiJ9MIh/dgC/f8 20Gw== X-Gm-Message-State: AFq9FYIqT4UOwst4uKubVA6CPs6CYhH/EZh4eO4sypJ17CqKIEbu/uiP 9r1hjEPLs5I29Py4YiEgCKqnFv3mbWLEz3HRx3+JFpCFgdJv1h2QaAFVGT5Ux539 X-Gm-Gg: AYBFou2ppXk/83fBo8tZwwrNLnzdr5+K/NJwbBLIuGPgbcrsQULSngN9f+sJl/jZoiP nQIKmPF8kDIKOCAV+qwvUdcrAD1fYvSJP5S3FTIIxDhByoItJpPCCKjF6mcZQiDq1Ko3L89PW1K nVNY+2JYu7j10AAZACQjd8rVGh69154b6tK5afe2g0sx2zEnkXSlPeQsFbH1COMjVadkqUz0Fb5 gFRSAWwM+uTRWMJ8wZ89lIr6w/3Ui8+i5FVstGJ55/ToX1kuWG4vHFo/MEyl90q4fBPyIh27ylJ eVktHk4GPFCC0gPGK5upUsxKZUWet/Cm2ALsclRkqHfJRs6k6eTX5/GDwEL9WPXRS7n3v9Ws54t pcosCjBjAuHOoaMRIqNh9t0iMF4cnppb56ra3Fj3HA3swJ/WqqmArDl/6BNVaiRMFfxv8GdLWUn J3jiKMOb42PfpYe7JnH55IzN3aSxaBAU/eJfAcwIHxv89asIv6If00bPdwihtlRHxfTxoxYHlAJ hvAP6HaFEeIdWHrgahwo6j5lyj0hLLakUMkZnVb2CbB35pnBHu657dVCyEeflpD01wxBPv8/3mf pQ== X-Received: by 2002:a17:90a:dfc6:b0:3a0:eec6:b065 with SMTP id 98e67ed59e1d1-3adeb550e07mr3601365a91.13.1793005622487; Mon, 26 Oct 2026 02:07:02 -0700 (PDT) Received: from [127.0.0.1] ([20.168.94.86]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3adebb0667asm7697028a91.14.2026.10.26.02.07.01 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 26 Oct 2026 02:07:01 -0700 (PDT) Message-Id: In-Reply-To: References: From: "qeesung via GitGitGadget" Date: Mon, 26 Oct 2026 09:06:57 +0000 Subject: [PATCH v4 0/3] repack: don't lose objects to a ".keep" that appears mid-run Fcc: Sent Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Mailing-List: git@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 To: git@vger.kernel.org Cc: Patrick Steinhardt , Taylor Blau , Junio C Hamano , Justin Tobler , qeesung "git repack -d" could lose objects when a ".keep" file appeared on a pack it had already decided to delete. repack now tells pack-objects which packs it may delete, and pack-objects ignores ".keep" files on those packs. We hit this in production (a ref pointing at a missing commit, on git 2.43), and it reproduces on master: * repack scans for ".keep" files and decides which packs to delete; pack-objects scans again when it opens the packs; * in between, a fetch or a push brings in a pack identical to one of those, and a ".keep" ends up next to it; * pack-objects then leaves that pack's objects out, or does not loosen or keep them for -A, -k and --cruft, and repack deletes the pack anyway. v3 passed pack-objects the kept packs repack saw at startup instead of --honor-pack-keep. That missed -A, -k and --cruft, which skip packs with a ".keep" file regardless of that option. v4 goes the other way: * 1/3 fixes a stale kept-pack cache in the cruft walk. It reproduces on master, but under --honor-pack-keep any ".keep" file happens to hide it, including the one the race leaves; 3/3 takes that one away, so this comes first. * 2/3 adds "pack-objects --ignore-pack-keep-from=", which treats the packs named in the file as having no ".keep". * 3/3 makes "repack -d" pass the packs it may delete that way. Each fix has a test that fails without it; the race is reproduced in t7700 and t7703. The full suite passes. Known limitations: * Unreachable objects of a deleted pack are still dropped by "-a -d" and by the expiration options, as before. A pack written again under the same name keeps its old mtime, so objects a push brings back that way can still expire. Freshening the pack on such a collision would help, but that is for a separate series. * The cruft walk now goes through the packs of an alternate, as it must (see 1/3). On a "clone --shared" fork of git.git this took the walk in "repack -d -l --cruft" from about 2ms to about 3.3s, which is what master already does when a ".keep" file is around. This is based on master, now that ps/odb-files-alternates is in. It merges cleanly with next and seen (with v3 reverted), and the repack tests pass there. jt/odb-keep-lockfile-fix, Justin's fix for the receive-pack side that v1 had a patch for, leaves the race as it is. Changes since v3: * Redesigned as above. v3's 1/5, 3/5 and 4/5 are dropped, since nothing passes --keep-pack any more; 2/3 and 3/3 are new. * 1/3 (v3's 2/5) also lets the walk go through --keep-pack packs while still leaving their objects out, and its message explains why the walk goes through alternates. There are tests for both. Qin ShiCheng (3): pack-objects: reset kept-pack cache for cruft walk pack-objects: add --ignore-pack-keep-from repack: tell pack-objects which packs it may delete Documentation/git-pack-objects.adoc | 12 ++++ builtin/pack-objects.c | 55 +++++++++++++++- builtin/repack.c | 23 +++++++ odb/source-packed.h | 3 +- packfile.c | 18 +++++- packfile.h | 7 +++ repack.c | 26 ++++++++ repack.h | 14 ++++- t/t5329-pack-objects-cruft.sh | 98 +++++++++++++++++++++++++++++ t/t7700-repack.sh | 86 +++++++++++++++++++++++++ t/t7703-repack-geometric.sh | 39 ++++++++++++ 11 files changed, 375 insertions(+), 6 deletions(-) base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926 Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2219%2Fqeesung%2Frepack-kept-packs-snapshot-v4 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2219/qeesung/repack-kept-packs-snapshot-v4 Pull-Request: https://github.com/gitgitgadget/git/pull/2219 Range-diff vs v3: 1: 8cf72312c5 < -: ---------- pack-objects: keep --keep-pack open when following 2: 58019e4983 ! 1: 8cce2b6eb5 pack-objects: reset kept-pack cache for cruft walk @@ Commit message 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. + out of the cruft pack, to go when that pack is deleted. Whether that + happens depends on whether the question changes along the way, which + "--honor-pack-keep" does whenever there is a ".keep" file around. - 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 unmarking. Only packfile.c is to touch it, so + add repo_invalidate_kept_pack_caches() there, which drops it for every + object directory. - 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. + With the cache gone, the walk goes through every pack but the listed + fresh ones, as was intended, including packs borrowed from an + alternate. It has to: an object there may refer to a local one, for + example when two repositories borrow from each other, or when "git + repack --filter-to" has moved some objects to an alternate and left + the ones they refer to behind. Without "--local", whatever the walk + finds there is copied into the cruft pack, as it already is for loose + objects of an alternate; with "--local", nothing is. Either way, the + walk may now go through a history far bigger than the repository's + own, as it already did whenever there was a ".keep" file around. - 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. + It also includes packs given with "--keep-pack", whose objects are to + be left out. Turn those into "open" kept packs, as + "--stdin-packs=follow" does for its "!" packs: the walk goes through + them, and their objects are still left out. This is how an unlisted + pack with a ".keep" file is treated under "--honor-pack-keep". + + With "--keep-pack", the walk and the check that leaves the objects of + such a pack out ask about different kinds of kept packs, so the cache + is rebuilt each time they take turns, as already happens under + "--honor-pack-keep" whenever there is a ".keep" file around. repack + never gets here with "--keep-pack": it lists the packs it keeps as + fresh ones instead. + + The tests walk from a recent commit to an expired one through a pack + pack-objects is not told about, through one given with "--keep-pack", + and through one of an alternate holding trees whose blobs "git repack + --filter-to" left behind, with and without "--local". Signed-off-by: Qin ShiCheng @@ builtin/pack-objects.c: static void enumerate_and_traverse_cruft_objects(struct /* * 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. ++ * A pack given with --keep-pack must not halt it either, but as an ++ * "open" kept pack its objects are still left out. The kept-pack ++ * cache was built while the packs were still marked, so drop it ++ * too. */ - repo_for_each_pack(the_repository, p) +- repo_for_each_pack(the_repository, p) ++ repo_for_each_pack(the_repository, p) { p->pack_keep_in_core = 0; ++ if (p->pack_keep_in_core_open) ++ ignore_packed_keep_in_core_open = 1; ++ } 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")); +@@ builtin/pack-objects.c: static void read_cruft_objects(void) + + if (item) { + item->util = p; ++ } else if (p->pack_keep_in_core) { ++ /* ++ * Only --keep-pack marks a pack as kept this early. ++ * Make it an "open" kept pack as well, so that its ++ * objects are still left out once it is unmarked ++ * before the traversal; until then this has no effect. ++ */ ++ p->pack_keep_in_core_open = 1; + } else { + /* + * This pack wasn't mentioned in either the "fresh" or ## odb/source-packed.h ## @@ odb/source-packed.h: struct odb_source_packed { @@ packfile.c: int packfile_fill_entry(struct packed_git *p, + +void repo_invalidate_kept_pack_caches(struct repository *r) +{ -+ struct odb_source *source; ++ struct odb_source_files *files = ++ odb_source_files_downcast(r->objects->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); -+ } ++ for (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) ++ invalidate_kept_pack_cache(dir->packed); +} + static void maybe_invalidate_kept_pack_cache(struct odb_source_packed *store, @@ t/t5329-pack-objects-cruft.sh: test_expect_success 'cruft trees rescue sub-trees ) ' -+test_expect_success 'cruft traversal rescues through a pack it was not told about' ' ++# Build the history "packed", "old", "mid", "new" in the current repository: ++# "packed" is in the pack named in "in", "old" has expired, "mid" is in the ++# pack whose hash is in $mid and "new" is recent. The objects of packed..new ++# end up in "all.objects". ++cruft_chain () { ++ test_commit packed && ++ git repack -Ad && ++ basename "$(ls $packdir/pack-*.pack)" >in && ++ ++ test_commit old && ++ test_commit mid && ++ test_commit new && ++ ++ git rev-list --objects --no-object-names packed..old >old && ++ while read object ++ do ++ test-tool chmtime -1000 \ ++ "$objdir/$(test_oid_to_path $object)" || return 1 ++ done mid.objects && ++ mid=$(git pack-objects $packdir/pack all.objects ++} ++ ++test_expect_success 'cruft traversal goes through a pack it was not told about' ' + git init repo && + test_when_finished "rm -fr repo" && + ( + cd repo && ++ cruft_chain && + -+ test_commit packed && -+ git repack -Ad && -+ keep="$(basename "$(ls $packdir/pack-*.pack)")" && -+ -+ test_commit old && -+ test_commit mid && -+ test_commit new && ++ # "old" can only be rescued from "new" by walking through ++ # the "mid" pack. ++ cruft=$(git pack-objects --cruft \ ++ --cruft-expiration=750.seconds.ago $packdir/pack actual.raw && ++ cut -d" " -f1 actual && ++ sort expect && ++ test_cmp expect actual ++ ) ++' + -+ # "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 /dev/null && -+ git prune-packed && ++test_expect_success 'cruft traversal goes through a --keep-pack pack' ' ++ git init repo && ++ test_when_finished "rm -fr repo" && ++ ( ++ cd repo && ++ cruft_chain && + -+ cruft="$(echo $keep | git pack-objects --cruft \ ++ # The same, but with "mid" in a pack given with --keep-pack: ++ # "old" is still rescued, and none of the objects of that ++ # pack are copied. ++ cruft=$(git pack-objects --cruft \ + --cruft-expiration=750.seconds.ago \ -+ $packdir/pack)" && ++ --keep-pack=pack-$mid.pack $packdir/pack actual.raw && -+ + cut -d" " -f1 actual && -+ git rev-list --objects --no-object-names packed..new >expect.raw && -+ sort expect && -+ ++ sort all.sorted && ++ sort mid.sorted && ++ comm -23 all.sorted mid.sorted >expect && + test_cmp expect actual + ) +' ++ ++for opt in "" --local ++do ++ test_expect_success "cruft traversal goes through an alternate${opt:+ ($opt)}" ' ++ test_when_finished "rm -fr repo alt.git" && ++ git init repo && ++ git init --bare alt.git && ++ echo "$(pwd)/alt.git/objects" >repo/.git/objects/info/alternates && ++ test_commit -C repo base && ++ test_commit -C repo old && ++ tree=$(git -C repo rev-parse old^{tree}) && ++ blob=$(git -C repo rev-parse old:old.t) && ++ ++ # Move everything but the blobs to the alternate, then let ++ # "old" expire: its tree is in the alternate, but its blob ++ # is in an old local pack. ++ git -C repo repack -a -d && ++ git -C repo repack -a -d --filter=object:type=blob \ ++ --filter-to="$(pwd)/alt.git/objects/pack/pack" && ++ git -C repo tag -d old && ++ git -C repo reset --hard base && ++ git -C repo reflog expire --all --expire=all && ++ test-tool chmtime -10000 repo/$packdir/pack-*.pack && ++ ++ # A recent unreachable commit in "repo" keeps the blob, but only ++ # if the traversal goes through the tree in the alternate, which ++ # it has to do even with --local. ++ outer=$(printf "040000 tree %s\tdir\n" $tree | git -C repo mktree) && ++ git -C repo commit-tree -m recent $outer >/dev/null && ++ git -C repo repack --cruft --cruft-expiration=1.hour.ago -d $opt && ++ git -C repo cat-file -e $blob ++ ' ++done + test_expect_success 'expired objects are pruned' ' git init repo && 3: ff2146d002 < -: ---------- pack-objects: sort --keep-pack list for lookup 4: 148175cfa0 ! 2: 91ffbdf4fa pack-objects: add --keep-pack-from-file @@ Metadata Author: Qin ShiCheng ## Commit message ## - pack-objects: add --keep-pack-from-file + pack-objects: add --ignore-pack-keep-from - "--keep-pack" names one pack per occurrence, and there is only so much - room on the command line: ARG_MAX is shared with the environment, and - on Windows the whole line is capped at 32,767 characters, which a few - hundred pack names fill. Past that the spawn fails before pack-objects - has started. fetch-pack grew "--stdin" in 078b895fef (fetch-pack: new - --stdin option to read refs from stdin, 2012-04-02) for the same - reason. + repack decides which packs to delete before it runs pack-objects, but + pack-objects looks for ".keep" files on its own when it opens the + packs. A ".keep" that appears in between, for example from a push or + fetch whose index-pack wrote a pack identical to an existing one, makes + pack-objects treat as kept a pack that repack is about to delete. Its + objects are then left out of the new pack, or not loosened, or not + kept as unreachable, and they are gone once repack deletes the pack. - stdin is taken here: every mode repack drives pack-objects in already - uses it, for the revision list under "-a", object names for the - promisor pack, and pack lists for "--stdin-packs" and "--cruft". So - read the names from a file instead, one per line, skipping empty - lines. They go into the same list as the "--keep-pack" names and are - treated exactly alike: matched against local packs, ignored when they - match nothing, and kept open under "--stdin-packs=follow". A relative - path is resolved against the directory the user ran from, as - "--refs-snapshot" of "git multi-pack-index write" is. + Not all of that goes through "--honor-pack-keep": loosening objects + for "--unpack-unreachable", and collecting them for + "--keep-unreachable" and "--cruft", skip packs with a ".keep" file + regardless of it. - The list now holds strings from two sources, so let it own its copies. + Let the caller name such packs. "--ignore-pack-keep-from=" reads + names of local packs, one per line, given and matched as for + "--keep-pack", and treats them as if they had no ".keep" file. All of + the above go by p->pack_keep, which is read from the disk once, when a + pack is first seen, so clearing it for the named packs before anything + looks at it covers every one of them. Other packs, including any that + show up later, keep whatever their ".keep" files say. - repack is about to use this to hand pack-objects its own snapshot of - the packs that have a ".keep" file. + The names come from a file because there can be more of them than fit + on a command line (on Windows, the whole command line is limited to + 32767 characters), and the standard input is already taken in every + mode repack uses. Signed-off-by: Qin ShiCheng @@ Documentation/git-pack-objects.adoc: SYNOPSIS [--no-reuse-delta] [--delta-base-offset] [--non-empty] [--local] [--incremental] [--window=] [--depth=] [--revs [--unpacked | --all]] [--keep-pack=] -+ [--keep-pack-from-file=] ++ [--ignore-pack-keep-from=] [--cruft] [--cruft-expiration=