Skip to content

Commit 6722174

Browse files
qeesunggitster
authored andcommitted
repack: tell pack-objects which packs are kept
repack works out which packs are redundant by looking for ".keep" files when it starts, then passes "--honor-pack-keep" to the pack-objects it spawns, which looks for them all over again. Two scans of the same directory, seconds apart, with nothing holding them together. A ".keep" that turns up in between loses objects. The parent did not see it, so the pack is on its list to delete. The child does see it, so it leaves that pack's objects out of the replacement. The parent deletes the pack regardless: repack_remove_redundant_pack() passes force_delete, which skips the ".keep" check in unlink_pack_path(). The objects are gone and repack exits successfully. The gap is easy to land in. index-pack writes its ".keep" before it renames the packfile into place, so a "git fetch" or a push being migrated out of its quarantine will do it. Checking for the ".keep" once more right before deleting would not help: a push holds it for a fraction of a second, and it may well be gone again by the time pack-objects has finished. Hand pack-objects the kept packs we collected at startup and drop "--honor-pack-keep". Both processes then work from one snapshot, and a ".keep" appearing or disappearing while we run cannot make them disagree. An earlier commit made sure a pack kept this way is no more of a boundary to the traversal than a ".keep" file was. The list goes into a file next to the refs snapshot we already write for "git multi-pack-index write", and is passed with "--keep-pack-from-file" to every pack-objects we spawn when "--pack-kept-objects" is not in effect, which is when "--honor-pack-keep" used to be. The cruft pack-objects already has the kept packs on its stdin; the file is redundant there, but it sees the same list as everybody else. With nothing to keep, no file is written and nothing is passed, which is what "--honor-pack-keep" came down to when it found no ".keep". The names go one per line, so a name with a newline in it cannot be passed. "--stdin-packs" and "--cruft" have the same limit and die on a name they cannot find, but "--keep-pack" ignores such a name, and the two halves of a garbled one could go on to exclude some other pack; refuse it up front instead. The user's own "--keep-pack" arguments keep being forwarded, since they apply either way. write_filtered_pack() had a loop passing the kept packs too, but without the ".pack" suffix pack-objects compares against; it goes. Kept packs borrowed from an alternate object directory were covered by "--honor-pack-keep" and are not by the snapshot, which only ever held local packs; repack never deletes those, so their objects now get packed rather than skipped, which costs room but cannot lose anything. Signed-off-by: Qin ShiCheng <qeesung@live.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 6fbd8ae commit 6722174

6 files changed

Lines changed: 177 additions & 7 deletions

File tree

‎builtin/repack.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ int cmd_repack(int argc,
167167
struct oidset drop_oids = OIDSET_INIT;
168168
struct pack_geometry geometry = { 0 };
169169
struct tempfile *refs_snapshot = NULL;
170+
struct tempfile *kept_packs_snapshot = NULL;
170171
int i, ret;
171172
int show_progress;
172173

@@ -456,6 +457,19 @@ int cmd_repack(int argc,
456457

457458
existing.repo = repo;
458459
existing_packs_collect(&existing, &keep_pack_list);
460+
if (existing.kept_packs.nr) {
461+
struct strbuf path = STRBUF_INIT;
462+
463+
strbuf_addf(&path, "%s/%s_XXXXXX",
464+
repo_get_object_directory(repo), "kept-packs");
465+
466+
kept_packs_snapshot = xmks_tempfile(path.buf);
467+
existing_packs_snapshot_kept(&existing, kept_packs_snapshot);
468+
po_args.kept_packs_snapshot =
469+
get_tempfile_path(kept_packs_snapshot);
470+
471+
strbuf_release(&path);
472+
}
459473

460474
if (geometry.split_factor) {
461475
if (pack_everything)
@@ -644,6 +658,7 @@ int cmd_repack(int argc,
644658
cruft_po_args.quiet = po_args.quiet;
645659
cruft_po_args.delta_base_offset = po_args.delta_base_offset;
646660
cruft_po_args.pack_kept_objects = 0;
661+
cruft_po_args.kept_packs_snapshot = po_args.kept_packs_snapshot;
647662

648663
ret = write_cruft_pack(&opts, cruft_expiration,
649664
combine_cruft_below_size, &names,

‎repack-filtered.c‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ int write_filtered_pack(const struct write_pack_opts *opts,
2525

2626
strvec_push(&cmd.args, "--stdin-packs");
2727

28-
for_each_string_list_item(item, &existing->kept_packs)
29-
strvec_pushf(&cmd.args, "--keep-pack=%s", item->string);
30-
3128
cmd.in = -1;
3229

3330
ret = start_command(&cmd);

‎repack.c‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ void prepare_pack_objects(struct child_process *cmd,
3838
strvec_push(&cmd->args, "--quiet");
3939
if (args->delta_base_offset)
4040
strvec_push(&cmd->args, "--delta-base-offset");
41-
if (!args->pack_kept_objects)
42-
strvec_push(&cmd->args, "--honor-pack-keep");
41+
if (!args->pack_kept_objects && args->kept_packs_snapshot)
42+
strvec_pushf(&cmd->args, "--keep-pack-from-file=%s",
43+
args->kept_packs_snapshot);
4344
strvec_push(&cmd->args, out);
4445
cmd->git_cmd = 1;
4546
cmd->out = -1;
@@ -167,6 +168,35 @@ void existing_packs_collect(struct existing_packs *existing,
167168
strbuf_release(&buf);
168169
}
169170

171+
void existing_packs_snapshot_kept(const struct existing_packs *existing,
172+
struct tempfile *f)
173+
{
174+
struct string_list_item *item;
175+
FILE *out = fdopen_tempfile(f, "w");
176+
177+
if (!out)
178+
die(_("could not open tempfile %s for writing"),
179+
get_tempfile_path(f));
180+
181+
for_each_string_list_item(item, &existing->kept_packs) {
182+
/*
183+
* A newline would split the name in two, and pack-objects
184+
* quietly keeps whichever packs the halves happen to name.
185+
*/
186+
if (strchr(item->string, '\n'))
187+
die(_("cannot keep pack '%s': its name contains a newline"),
188+
item->string);
189+
fprintf(out, "%s.pack\n", item->string);
190+
}
191+
192+
if (close_tempfile_gently(f)) {
193+
int save_errno = errno;
194+
delete_tempfile(&f);
195+
errno = save_errno;
196+
die_errno(_("could not close kept packs snapshot tempfile"));
197+
}
198+
}
199+
170200
int existing_packs_has_non_kept(const struct existing_packs *existing)
171201
{
172202
return existing->non_kept_packs.nr || existing->cruft_packs.nr;

‎repack.h‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ struct pack_objects_args {
1919
int path_walk;
2020
int delta_base_offset;
2121
int pack_kept_objects;
22+
/*
23+
* File naming the packs to leave alone, one "<name>.pack" per line;
24+
* NULL when there are none. pack-objects reads it rather than
25+
* looking for ".keep" files itself, so that a ".keep" created or
26+
* removed while we run cannot make the two of us disagree over
27+
* which packs are being repacked.
28+
*/
29+
const char *kept_packs_snapshot;
2230
struct list_objects_filter_options filter_options;
2331
};
2432

@@ -28,6 +36,7 @@ struct pack_objects_args {
2836
}
2937

3038
struct child_process;
39+
struct tempfile;
3140

3241
void prepare_pack_objects(struct child_process *cmd,
3342
const struct pack_objects_args *args,
@@ -79,6 +88,12 @@ struct existing_packs {
7988
*/
8089
void existing_packs_collect(struct existing_packs *existing,
8190
const struct string_list *extra_keep);
91+
/*
92+
* Writes the names of the kept packs, one "<name>.pack" per line, into
93+
* the given tempfile, for pack-objects to read with --keep-pack-from-file.
94+
*/
95+
void existing_packs_snapshot_kept(const struct existing_packs *existing,
96+
struct tempfile *f);
8297
int existing_packs_has_non_kept(const struct existing_packs *existing);
8398
int existing_pack_is_marked_for_deletion(struct string_list_item *item);
8499
void existing_packs_retain_cruft(struct existing_packs *existing,
@@ -138,8 +153,6 @@ void pack_geometry_remove_redundant(struct pack_geometry *geometry,
138153
bool wrote_incremental_midx);
139154
void pack_geometry_release(struct pack_geometry *geometry);
140155

141-
struct tempfile;
142-
143156
enum repack_write_midx_mode {
144157
REPACK_WRITE_MIDX_NONE,
145158
REPACK_WRITE_MIDX_DEFAULT,

‎t/t7700-repack.sh‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,49 @@ test_expect_success 'repack --keep-pack' '
254254
)
255255
'
256256

257+
test_expect_success 'repack --keep-pack with --pack-kept-objects' '
258+
test_create_repo keep-pack-kept-objects &&
259+
(
260+
cd keep-pack-kept-objects &&
261+
git config pack.window 0 &&
262+
git config maintenance.auto false &&
263+
P1=$(commit_and_pack 1) &&
264+
P2=$(commit_and_pack 2) &&
265+
266+
# "--pack-kept-objects" is about packs that have a ".keep"
267+
# file. A pack named with "--keep-pack" stays out of the
268+
# result regardless, objects included.
269+
git repack -a -d --pack-kept-objects --keep-pack $P1 &&
270+
ls .git/objects/pack/*.pack >counts &&
271+
test_line_count = 2 counts &&
272+
test-tool find-pack -c 1 HEAD~1 &&
273+
test-tool find-pack -c 1 HEAD~1: &&
274+
git fsck
275+
)
276+
'
277+
278+
test_expect_success FUNNYNAMES 'a kept pack whose name has a newline is refused' '
279+
test_create_repo keep-pack-newline &&
280+
(
281+
cd keep-pack-newline &&
282+
git config maintenance.auto false &&
283+
test_commit base &&
284+
git repack -ad &&
285+
286+
# The names pack-objects is told to keep go one per line, so
287+
# this one would come out as two, and the first of them is
288+
# the name of the pack holding everything else.
289+
victim="$(basename "$(ls .git/objects/pack/pack-*.pack)")" &&
290+
name="$(printf "%s\nother" "$victim")" &&
291+
P=$(git rev-parse HEAD | git pack-objects ".git/objects/pack/$name") &&
292+
>".git/objects/pack/$name-$P.keep" &&
293+
294+
test_must_fail git repack -ad 2>err &&
295+
test_grep "contains a newline" err &&
296+
git fsck
297+
)
298+
'
299+
257300
test_expect_success 'repacking fails when missing .pack actually means missing objects' '
258301
test_create_repo idx-without-pack &&
259302
(

‎t/t7703-repack-geometric.sh‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,4 +541,76 @@ test_expect_success 'geometric repack works with promisor packs' '
541541
)
542542
'
543543

544+
test_expect_success 'a ".keep" that shows up mid-repack does not lose objects' '
545+
test_when_finished "rm -fr race" &&
546+
git init race &&
547+
(
548+
cd race &&
549+
550+
test_commit kept &&
551+
test_commit pack &&
552+
553+
KEPT=$(git pack-objects --revs $packdir/pack <<-EOF
554+
refs/tags/kept
555+
EOF
556+
) &&
557+
git pack-objects --revs $packdir/pack <<-EOF &&
558+
refs/tags/pack
559+
^refs/tags/kept
560+
EOF
561+
git prune-packed &&
562+
563+
# Neither pack is twice the size of the other, so both are
564+
# redundant and get deleted. Have a ".keep" appear on one of
565+
# them as pack-objects starts, after the repack has decided
566+
# to delete it: pack-objects used to notice the ".keep" and
567+
# leave those objects out of the replacement pack.
568+
mkdir shim &&
569+
write_script shim/git <<-EOF &&
570+
test "\$1" = "pack-objects" && >"$(pwd)/$packdir/pack-$KEPT.keep"
571+
GIT_EXEC_PATH="$GIT_EXEC_PATH" exec "$GIT_EXEC_PATH/git" "\$@"
572+
EOF
573+
574+
git --exec-path="$(pwd)/shim" repack --geometric 2 -d &&
575+
576+
git fsck
577+
)
578+
'
579+
580+
test_expect_success 'a kept pack does not stop the traversal from rescuing objects' '
581+
test_when_finished "rm -fr kept-open" &&
582+
git init kept-open &&
583+
(
584+
cd kept-open &&
585+
git config repack.midxMustContainCruft false &&
586+
587+
test_commit a &&
588+
test_commit b &&
589+
b=$(git rev-parse b) &&
590+
git repack -ad &&
591+
592+
# Make "b" unreachable and sweep it, together with its tree
593+
# and blob, into a cruft pack.
594+
git tag -d b &&
595+
git reset --hard a &&
596+
git reflog expire --all --expire=all &&
597+
git repack -ad --cruft &&
598+
599+
# Bring the commit back on its own, in a pack marked as kept.
600+
# Its tree and blob are still only in the cruft pack.
601+
kept=$(echo $b | git pack-objects $packdir/pack) &&
602+
>$packdir/pack-$kept.keep &&
603+
604+
# Build on top of it, so that the repack has to look through
605+
# the kept pack to find out what the new commit depends on.
606+
git update-ref refs/heads/master \
607+
$(git commit-tree a^{tree} -p $b -m c) &&
608+
609+
git repack --geometric 2 -d --write-midx --write-bitmap-index &&
610+
test_path_is_file $packdir/multi-pack-index &&
611+
ls $packdir/multi-pack-index-*.bitmap >bitmaps &&
612+
test_line_count = 1 bitmaps
613+
)
614+
'
615+
544616
test_done

0 commit comments

Comments
 (0)