builtin/gc: move geometric repacking into `odb_optimize()`

We have two major object database optimization strategies: - The legacy strategy used by git-gc(1), which absorbs loose objects into packfiles, and eventually merges all packfiles once we have too many of them. - The more recent "geometric" strategy used by git-maintenance(1), which merges packfiles using a geometric sequence. These two strategies are still using completely separate code paths. In a subsequent commit we'll want to make both strategies pluggable though. Prepare for this change by merging the "geometric" strategy into `odb_optimize()`. This also allows us to reuse some of the logic we have in that function. Note that this change requires us to adapt tests because we're now using "-q" instead of "--quiet". Naturally though, these invocations are of course equivalent to one another. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jul 13, 2026 at 07:52 UTC 7d663dd83ba2ae459178c98f96c0761d044698b4
2 files changed +96 -93
builtin/gc.c
+87 -84
@@ -593,6 +593,11 @@ static int keep_one_pack(struct string_list_item *item, void *data)
593 return 0;
594 }
595
596 +enum odb_optimize_strategy {
597 + ODB_OPTIMIZE_INCREMENTAL,
598 + ODB_OPTIMIZE_GEOMETRIC,
599 +};
600 +
601 enum odb_optimize_flags {
602 /* Enable verbose logging and progress reporting. */
603 ODB_OPTIMIZE_VERBOSE = (1 << 0),
@@ -605,6 +610,7 @@ enum odb_optimize_flags {
610 };
611
612 struct odb_optimize_options {
613 + enum odb_optimize_strategy strategy;
614 enum odb_optimize_flags flags;
615 const char *prune_expire;
616 const char *expire_to;
@@ -858,49 +864,87 @@ static int odb_optimize(struct object_database *odb,
864 *
865 * - Otherwise we perform an incremental repack.
866 */
861 - if (!(opts->flags & ODB_OPTIMIZE_AUTO)) {
862 - struct string_list keep_pack = STRING_LIST_INIT_NODUP;
863 -
864 - if (opts->keep_largest_pack != -1) {
865 - if (opts->keep_largest_pack)
866 - find_base_packs(&keep_pack, 0);
867 - } else if (big_pack_threshold) {
868 - find_base_packs(&keep_pack, big_pack_threshold);
869 - }
870 -
871 - add_repack_all_option(opts, &keep_pack, &repack_cmd.args);
872 - string_list_clear(&keep_pack, 0);
873 - } else {
874 - if (too_many_packs(gc_auto_pack_limit)) {
867 + switch (opts->strategy) {
868 + case ODB_OPTIMIZE_INCREMENTAL:
869 + if (!(opts->flags & ODB_OPTIMIZE_AUTO)) {
870 struct string_list keep_pack = STRING_LIST_INIT_NODUP;
871
877 - if (big_pack_threshold) {
878 - find_base_packs(&keep_pack, big_pack_threshold);
879 - if (keep_pack.nr >= gc_auto_pack_limit) {
880 - string_list_clear(&keep_pack, 0);
872 + if (opts->keep_largest_pack != -1) {
873 + if (opts->keep_largest_pack)
874 find_base_packs(&keep_pack, 0);
882 - }
883 - } else {
884 - struct packed_git *p = find_base_packs(&keep_pack, 0);
885 - uint64_t mem_have, mem_want;
886 -
887 - mem_have = total_ram();
888 - mem_want = estimate_repack_memory(p);
889 -
890 - /*
891 - * Only allow 1/2 of memory for pack-objects, leave
892 - * the rest for the OS and other processes in the
893 - * system.
894 - */
895 - if (!mem_have || mem_want < mem_have / 2)
896 - string_list_clear(&keep_pack, 0);
875 + } else if (big_pack_threshold) {
876 + find_base_packs(&keep_pack, big_pack_threshold);
877 }
878
879 add_repack_all_option(opts, &keep_pack, &repack_cmd.args);
880 string_list_clear(&keep_pack, 0);
881 } else {
902 - add_repack_incremental_option(&repack_cmd.args);
882 + if (too_many_packs(gc_auto_pack_limit)) {
883 + struct string_list keep_pack = STRING_LIST_INIT_NODUP;
884 +
885 + if (big_pack_threshold) {
886 + find_base_packs(&keep_pack, big_pack_threshold);
887 + if (keep_pack.nr >= gc_auto_pack_limit) {
888 + string_list_clear(&keep_pack, 0);
889 + find_base_packs(&keep_pack, 0);
890 + }
891 + } else {
892 + struct packed_git *p = find_base_packs(&keep_pack, 0);
893 + uint64_t mem_have, mem_want;
894 +
895 + mem_have = total_ram();
896 + mem_want = estimate_repack_memory(p);
897 +
898 + /*
899 + * Only allow 1/2 of memory for pack-objects, leave
900 + * the rest for the OS and other processes in the
901 + * system.
902 + */
903 + if (!mem_have || mem_want < mem_have / 2)
904 + string_list_clear(&keep_pack, 0);
905 + }
906 +
907 + add_repack_all_option(opts, &keep_pack, &repack_cmd.args);
908 + string_list_clear(&keep_pack, 0);
909 + } else {
910 + add_repack_incremental_option(&repack_cmd.args);
911 + }
912 }
913 +
914 + break;
915 + case ODB_OPTIMIZE_GEOMETRIC: {
916 + struct pack_geometry geometry = {
917 + .split_factor = 2,
918 + };
919 + struct pack_objects_args po_args = {
920 + .local = 1,
921 + };
922 + struct existing_packs existing_packs = EXISTING_PACKS_INIT;
923 + struct string_list kept_packs = STRING_LIST_INIT_DUP;
924 +
925 + repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor",
926 + &geometry.split_factor);
927 +
928 + existing_packs.repo = the_repository;
929 + existing_packs_collect(&existing_packs, &kept_packs);
930 + pack_geometry_init(&geometry, &existing_packs, &po_args);
931 + pack_geometry_split(&geometry);
932 +
933 + if (geometry.split < geometry.pack_nr) {
934 + strvec_pushf(&repack_cmd.args, "--geometric=%d",
935 + geometry.split_factor);
936 + } else {
937 + add_repack_all_option(opts, NULL, &repack_cmd.args);
938 + }
939 + if (the_repository->settings.core_multi_pack_index)
940 + strvec_push(&repack_cmd.args, "--write-midx");
941 +
942 + existing_packs_release(&existing_packs);
943 + pack_geometry_release(&geometry);
944 + break;
945 + }
946 + default:
947 + die("unknown maintenance strategy '%d'", opts->strategy);
948 }
949
950 if (run_command(&repack_cmd)) {
@@ -908,7 +952,8 @@ static int odb_optimize(struct object_database *odb,
952 goto out;
953 }
954
911 - if (opts->prune_expire) {
955 + /* Geometric repacking uses cruft packs, so we don't have to prune separately. */
956 + if (opts->strategy != ODB_OPTIMIZE_GEOMETRIC && opts->prune_expire) {
957 struct child_process prune_cmd = CHILD_PROCESS_INIT;
958
959 strvec_pushl(&prune_cmd.args, "prune", "--expire", NULL);
@@ -943,6 +988,7 @@ static int maintenance_task_odb(struct maintenance_run_opts *opts,
988 int aggressive)
989 {
990 struct odb_optimize_options odb_opts = {
991 + .strategy = ODB_OPTIMIZE_INCREMENTAL,
992 .keep_largest_pack = keep_largest_pack,
993 OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, aggressive),
994 };
@@ -1624,58 +1670,15 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts
1670 static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,
1671 struct gc_config *cfg)
1672 {
1627 - struct pack_geometry geometry = {
1628 - .split_factor = 2,
1629 - };
1630 - struct pack_objects_args po_args = {
1631 - .local = 1,
1673 + struct odb_optimize_options odb_opts = {
1674 + .strategy = ODB_OPTIMIZE_GEOMETRIC,
1675 + OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0),
1676 };
1633 - struct existing_packs existing_packs = EXISTING_PACKS_INIT;
1634 - struct string_list kept_packs = STRING_LIST_INIT_DUP;
1635 - struct child_process child = CHILD_PROCESS_INIT;
1636 - int ret;
1637 -
1638 - repo_config_get_int(the_repository, "maintenance.geometric-repack.splitFactor",
1639 - &geometry.split_factor);
1640 -
1641 - existing_packs.repo = the_repository;
1642 - existing_packs_collect(&existing_packs, &kept_packs);
1643 - pack_geometry_init(&geometry, &existing_packs, &po_args);
1644 - pack_geometry_split(&geometry);
1645 -
1646 - child.git_cmd = 1;
1647 - child.odb_to_close = the_repository->objects;
1648 -
1649 - strvec_pushl(&child.args, "repack", "-d", "-l", NULL);
1650 - if (geometry.split < geometry.pack_nr) {
1651 - strvec_pushf(&child.args, "--geometric=%d",
1652 - geometry.split_factor);
1653 - } else {
1654 - struct odb_optimize_options odb_opts = {
1655 - OPTIMIZE_FIELDS_FROM_GC_CONFIG(cfg, 0),
1656 - };
1677
1658 - if (!opts->quiet)
1659 - odb_opts.flags |= ODB_OPTIMIZE_VERBOSE;
1660 -
1661 - add_repack_all_option(&odb_opts, NULL, &child.args);
1662 - }
1663 - if (opts->quiet)
1664 - strvec_push(&child.args, "--quiet");
1665 - if (the_repository->settings.core_multi_pack_index)
1666 - strvec_push(&child.args, "--write-midx");
1667 -
1668 - if (run_command(&child)) {
1669 - ret = error(_("failed to perform geometric repack"));
1670 - goto out;
1671 - }
1672 -
1673 - ret = 0;
1678 + if (!opts->quiet)
1679 + odb_opts.flags |= ODB_OPTIMIZE_VERBOSE;
1680
1675 -out:
1676 - existing_packs_release(&existing_packs);
1677 - pack_geometry_release(&geometry);
1678 - return ret;
1681 + return odb_optimize(the_repository->objects, &odb_opts);
1682 }
1683
1684 static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)
t/t7900-maintenance.sh
+9 -9
@@ -574,8 +574,8 @@ run_and_verify_geometric_pack () {
574 rm -f "trace2.txt" &&
575 GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
576 git maintenance run --task=geometric-repack 2>/dev/null &&
577 - test_subcommand git repack -d -l --geometric=2 \
578 - --quiet --write-midx <trace2.txt &&
577 + test_subcommand git repack -d -l -q --geometric=2 \
578 + --write-midx <trace2.txt &&
579
580 # Verify that the number of packfiles matches our expectation.
581 ls -l .git/objects/pack/*.pack >packfiles &&
@@ -606,8 +606,8 @@ test_expect_success 'geometric repacking task' '
606 # The initial repack causes an all-into-one repack.
607 GIT_TRACE2_EVENT="$(pwd)/initial-repack.txt" \
608 git maintenance run --task=geometric-repack 2>/dev/null &&
609 - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
610 - --quiet --write-midx <initial-repack.txt &&
609 + test_subcommand git repack -d -l -q --cruft --cruft-expiration=2.weeks.ago \
610 + --write-midx <initial-repack.txt &&
611
612 # Repacking should now cause a no-op geometric repack because
613 # no packfiles need to be combined.
@@ -627,8 +627,8 @@ test_expect_success 'geometric repacking task' '
627 # an all-into-one-repack.
628 GIT_TRACE2_EVENT="$(pwd)/all-into-one-repack.txt" \
629 git maintenance run --task=geometric-repack 2>/dev/null &&
630 - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
631 - --quiet --write-midx <all-into-one-repack.txt &&
630 + test_subcommand git repack -d -l -q --cruft --cruft-expiration=2.weeks.ago \
631 + --write-midx <all-into-one-repack.txt &&
632
633 # The geometric repack soaks up unreachable objects.
634 echo blob-1 | git hash-object -w --stdin -t blob &&
@@ -662,8 +662,8 @@ test_expect_success 'geometric repacking task' '
662 run_and_verify_geometric_pack 3 &&
663 GIT_TRACE2_EVENT="$(pwd)/cruft-repack.txt" \
664 git maintenance run --task=geometric-repack 2>/dev/null &&
665 - test_subcommand git repack -d -l --cruft --cruft-expiration=2.weeks.ago \
666 - --quiet --write-midx <cruft-repack.txt &&
665 + test_subcommand git repack -d -l -q --cruft --cruft-expiration=2.weeks.ago \
666 + --write-midx <cruft-repack.txt &&
667 ls .git/objects/pack/*.pack >packs &&
668 test_line_count = 2 packs &&
669 ls .git/objects/pack/*.mtimes >cruft &&
@@ -754,7 +754,7 @@ test_expect_success 'geometric repacking honors configured split factor' '
754
755 test_geometric_repack_needed false splitFactor=2 &&
756 test_geometric_repack_needed true splitFactor=3 &&
757 - test_subcommand git repack -d -l --geometric=3 --quiet --write-midx <trace2.txt
757 + test_subcommand git repack -d -l -q --geometric=3 --write-midx <trace2.txt
758 )
759 '
760