included packs (those not beginning with `^`), excluding any
objects listed in the excluded packs (beginning with `^`).
+
-When `mode` is "follow", objects from packs not listed on stdin receive
-special treatment. Objects within unlisted packs will be included if
-those objects are (1) reachable from the included packs, and (2) not
-found in any excluded packs. This mode is useful, for example, to
-resurrect once-unreachable objects found in cruft packs to generate
-packs which are closed under reachability up to the boundary set by the
-excluded packs.
+When `mode` is "follow" packs may additionally be prefixed with `!`,
+indicating that they are excluded but not necessarily closed under
+reachability. In addition to objects in included packs, the resulting
+pack may include additional objects based on the following:
++
+--
+* If any packs are marked with `!`, then objects reachable from such
+ packs or included ones via objects outside of excluded-closed packs
+ will be included. In this case, all `^` packs are treated as closed
+ under reachability.
+* Otherwise (if there are no `!` packs), objects within unlisted packs
+ will be included if those objects are (1) reachable from the
+ included packs, and (2) not found in any excluded packs.
+--
++
+This mode is useful, for example, to resurrect once-unreachable
+objects found in cruft packs to generate packs which are closed under
+reachability up to the boundary set by the excluded packs.
+
Incompatible with `--revs`, or options that imply `--revs` (such as
`--all`), with the exception of `--unpacked`, which is compatible.
static int incremental;
static int ignore_packed_keep_on_disk;
static int ignore_packed_keep_in_core;
+static int ignore_packed_keep_in_core_open;
static int ignore_packed_keep_in_core_has_cruft;
static int allow_ofs_delta;
static struct pack_idx_option pack_idx_opts;
/*
* Then handle .keep first, as we have a fast(er) path there.
*/
- if (ignore_packed_keep_on_disk || ignore_packed_keep_in_core) {
+ if (ignore_packed_keep_on_disk || ignore_packed_keep_in_core ||
+ ignore_packed_keep_in_core_open) {
/*
* Set the flags for the kept-pack cache to be the ones we want
* to ignore.
flags |= KEPT_PACK_ON_DISK;
if (ignore_packed_keep_in_core)
flags |= KEPT_PACK_IN_CORE;
+ if (ignore_packed_keep_in_core_open)
+ flags |= KEPT_PACK_IN_CORE_OPEN;
/*
* If the object is in a pack that we want to ignore, *and* we
return 0;
if (ignore_packed_keep_in_core && p->pack_keep_in_core)
return 0;
+ if (ignore_packed_keep_in_core_open && p->pack_keep_in_core_open)
+ return 0;
if (has_object_kept_pack(p->repo, oid, flags))
return 0;
} else {
void *_data)
{
off_t ofs;
+ struct object_info oi = OBJECT_INFO_INIT;
enum object_type type = OBJ_NONE;
display_progress(progress_state, ++nr_seen);
if (have_duplicate_entry(oid, 0))
return 0;
- ofs = nth_packed_object_offset(p, pos);
- if (!want_object_in_pack(oid, 0, &p, &ofs))
- return 0;
+ stdin_packs_found_nr++;
- if (p) {
- struct object_info oi = OBJECT_INFO_INIT;
-
- oi.typep = &type;
- if (packed_object_info(p, ofs, &oi) < 0) {
- die(_("could not get type of object %s in pack %s"),
- oid_to_hex(oid), p->pack_name);
- } else if (type == OBJ_COMMIT) {
- struct rev_info *revs = _data;
- /*
- * commits in included packs are used as starting points for the
- * subsequent revision walk
- */
- add_pending_oid(revs, NULL, oid, 0);
- }
+ ofs = nth_packed_object_offset(p, pos);
- stdin_packs_found_nr++;
+ oi.typep = &type;
+ if (packed_object_info(p, ofs, &oi) < 0) {
+ die(_("could not get type of object %s in pack %s"),
+ oid_to_hex(oid), p->pack_name);
+ } else if (type == OBJ_COMMIT) {
+ struct rev_info *revs = _data;
+ /*
+ * commits in included packs are used as starting points
+ * for the subsequent revision walk
+ *
+ * Note that we do want to walk through commits that are
+ * present in excluded-open ('!') packs to pick up any
+ * objects reachable from them not present in the
+ * excluded-closed ('^') packs.
+ *
+ * However, we'll only add those objects to the packing
+ * list after checking `want_object_in_pack()` below.
+ */
+ add_pending_oid(revs, NULL, oid, 0);
}
+ if (!want_object_in_pack(oid, 0, &p, &ofs))
+ return 0;
+
create_object_entry(oid, type, 0, 0, 0, p, ofs);
return 0;
* - STDIN_PACK_EXCLUDE_CLOSED: objects in any packs with this flag
* bit set should be excluded from the output pack.
*
- * Objects in packs whose 'kind' bits include STDIN_PACK_INCLUDE are
- * used as traversal tips when invoked with --stdin-packs=follow.
+ * - STDIN_PACK_EXCLUDE_OPEN: objects in any packs with this flag
+ * bit set should be excluded from the output pack, but are not
+ * guaranteed to be closed under reachability.
+ *
+ * Objects in packs whose 'kind' bits include STDIN_PACK_INCLUDE or
+ * STDIN_PACK_EXCLUDE_OPEN are used as traversal tips when invoked
+ * with --stdin-packs=follow.
*/
enum stdin_pack_info_kind {
STDIN_PACK_INCLUDE = (1<<0),
STDIN_PACK_EXCLUDE_CLOSED = (1<<1),
+ STDIN_PACK_EXCLUDE_OPEN = (1<<2),
};
struct stdin_pack_info {
return 0;
}
+static int stdin_packs_include_check_obj(struct object *obj, void *data UNUSED)
+{
+ return !has_object_kept_pack(to_pack.repo, &obj->oid,
+ KEPT_PACK_IN_CORE);
+}
+
+static int stdin_packs_include_check(struct commit *commit, void *data)
+{
+ return stdin_packs_include_check_obj((struct object *)commit, data);
+}
+
static void stdin_packs_add_pack_entries(struct strmap *packs,
struct rev_info *revs)
{
for_each_string_list_item(item, &keys) {
struct stdin_pack_info *info = item->util;
- if (info->kind & STDIN_PACK_INCLUDE)
+ if (info->kind & STDIN_PACK_EXCLUDE_OPEN) {
+ /*
+ * When open-excluded packs ("!") are present, stop
+ * the parent walk at closed-excluded ("^") packs.
+ * Objects behind a "^" boundary are guaranteed to
+ * have closure and should not be rescued.
+ */
+ revs->include_check = stdin_packs_include_check;
+ revs->include_check_obj = stdin_packs_include_check_obj;
+ }
+
+ if ((info->kind & STDIN_PACK_INCLUDE) ||
+ (info->kind & STDIN_PACK_EXCLUDE_OPEN))
for_each_object_in_pack(info->p,
add_object_entry_from_pack,
revs,
string_list_clear(&keys, 0);
}
-static void stdin_packs_read_input(struct rev_info *revs)
+static void stdin_packs_read_input(struct rev_info *revs,
+ enum stdin_packs_mode mode)
{
struct strbuf buf = STRBUF_INIT;
struct strmap packs = STRMAP_INIT;
continue;
else if (*key == '^')
kind = STDIN_PACK_EXCLUDE_CLOSED;
+ else if (*key == '!' && mode == STDIN_PACKS_MODE_FOLLOW)
+ kind = STDIN_PACK_EXCLUDE_OPEN;
if (kind != STDIN_PACK_INCLUDE)
key++;
p->pack_keep_in_core = 1;
}
+ if (info->kind & STDIN_PACK_EXCLUDE_OPEN) {
+ /*
+ * Marking excluded open packs as kept in-core
+ * (open) for the same reason as we marked
+ * exclude closed packs as kept in-core.
+ *
+ * Use a separate flag here to ensure we don't
+ * halt our traversal at these packs, since they
+ * are not guaranteed to have closure.
+ *
+ */
+ p->pack_keep_in_core_open = 1;
+ }
+
info->p = p;
}
/* avoids adding objects in excluded packs */
ignore_packed_keep_in_core = 1;
- stdin_packs_read_input(&revs);
+ if (mode == STDIN_PACKS_MODE_FOLLOW) {
+ /*
+ * In '--stdin-packs=follow' mode, additionally ignore
+ * objects in excluded-open packs to prevent them from
+ * appearing in the resulting pack.
+ */
+ ignore_packed_keep_in_core_open = 1;
+ }
+ stdin_packs_read_input(&revs, mode);
if (rev_list_unpacked)
add_unreachable_loose_objects(&revs);
stdin_packs__follow_with_only HEAD HEAD^{tree}
'
+test_expect_success '--stdin-packs=follow with open-excluded packs' '
+ test_when_finished "rm -fr repo" &&
+
+ git init repo &&
+ (
+ cd repo &&
+ git config set maintenance.auto false &&
+
+ git branch -M main &&
+
+ # Create the following commit structure:
+ #
+ # A <-- B <-- D (main)
+ # ^
+ # \
+ # C (other)
+ test_commit A &&
+ test_commit B &&
+ git checkout -B other &&
+ test_commit C &&
+ git checkout main &&
+ test_commit D &&
+
+ A="$(echo A | git pack-objects --revs $packdir/pack)" &&
+ B="$(echo A..B | git pack-objects --revs $packdir/pack)" &&
+ C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&
+ D="$(echo B..D | git pack-objects --revs $packdir/pack)" &&
+
+ C_ONLY="$(git rev-parse other | git pack-objects $packdir/pack)" &&
+
+ git prune-packed &&
+
+ # Create a pack using --stdin-packs=follow where:
+ #
+ # - pack D is included,
+ # - pack C_ONLY is excluded, but open,
+ # - pack B is excluded, but closed, and
+ # - packs A and C are unknown
+ #
+ # The resulting pack should therefore contain:
+ #
+ # - objects from the included pack D,
+ # - A.t (rescued via D^{tree}), and
+ # - C^{tree} and C.t (rescued via pack C_ONLY)
+ #
+ # , but should omit:
+ #
+ # - C (excluded via C_ONLY),
+ # - objects from pack B (trivially excluded-closed)
+ # - A and A^{tree} (ancestors of B)
+ P=$(git pack-objects --stdin-packs=follow $packdir/pack <<-EOF
+ pack-$D.pack
+ !pack-$C_ONLY.pack
+ ^pack-$B.pack
+ EOF
+ ) &&
+
+ {
+ objects_in_packs $D &&
+ git rev-parse A:A.t "C^{tree}" C:C.t
+ } >expect.raw &&
+ sort expect.raw >expect &&
+
+ objects_in_packs $P >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '--stdin-packs with !-delimited pack without follow' '
+ test_when_finished "rm -fr repo" &&
+
+ git init repo &&
+ (
+ test_commit A &&
+ test_commit B &&
+ test_commit C &&
+
+ A="$(echo A | git pack-objects --revs $packdir/pack)" &&
+ B="$(echo A..B | git pack-objects --revs $packdir/pack)" &&
+ C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&
+
+ cat >in <<-EOF &&
+ !pack-$A.pack
+ pack-$B.pack
+ pack-$C.pack
+ EOF
+
+ # Without --stdin-packs=follow, we treat the first
+ # line of input as a literal packfile name, and thus
+ # expect pack-objects to complain of a missing pack
+ test_must_fail git pack-objects --stdin-packs --stdout \
+ >/dev/null <in 2>err &&
+ test_grep "could not find pack .!pack-$A.pack." err &&
+
+ # With --stdin-packs=follow, we treat the second line
+ # of input as indicating pack-$A.pack is an excluded
+ # open pack, and thus expect pack-objects to succeed
+ P=$(git pack-objects --stdin-packs=follow $packdir/pack <in) &&
+
+ objects_in_packs $B $C >expect &&
+ objects_in_packs $P >actual &&
+ test_cmp expect actual
+ )
+'
+
test_done