]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'tb/pseudo-merge-reachability-bitmap'
authorJunio C Hamano <gitster@pobox.com>
Mon, 24 Jun 2024 23:39:13 +0000 (16:39 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 24 Jun 2024 23:39:13 +0000 (16:39 -0700)
The pseudo-merge reachability bitmap to help more efficient storage
of the reachability bitmap in a repository with too many refs has
been added.

* tb/pseudo-merge-reachability-bitmap: (26 commits)
  pack-bitmap.c: ensure pseudo-merge offset reads are bounded
  Documentation/technical/bitmap-format.txt: add missing position table
  t/perf: implement performance tests for pseudo-merge bitmaps
  pseudo-merge: implement support for finding existing merges
  ewah: `bitmap_equals_ewah()`
  pack-bitmap: extra trace2 information
  pack-bitmap.c: use pseudo-merges during traversal
  t/test-lib-functions.sh: support `--notick` in `test_commit_bulk()`
  pack-bitmap: implement test helpers for pseudo-merge
  ewah: implement `ewah_bitmap_popcount()`
  pseudo-merge: implement support for reading pseudo-merge commits
  pack-bitmap.c: read pseudo-merge extension
  pseudo-merge: scaffolding for reads
  pack-bitmap: extract `read_bitmap()` function
  pack-bitmap-write.c: write pseudo-merge table
  pseudo-merge: implement support for selecting pseudo-merge commits
  config: introduce `git_config_double()`
  pack-bitmap: make `bitmap_writer_push_bitmapped_commit()` public
  pack-bitmap: implement `bitmap_writer_has_bitmapped_object_id()`
  pack-bitmap-write: support storing pseudo-merge commits
  ...

1  2 
Documentation/Makefile
Documentation/config.txt
Makefile
builtin/pack-objects.c
config.c
config.h
midx-write.c
object.h
pack-bitmap.c
pseudo-merge.c

Simple merge
Simple merge
diff --cc Makefile
Simple merge
Simple merge
diff --cc config.c
Simple merge
diff --cc config.h
Simple merge
diff --cc midx-write.c
Simple merge
diff --cc object.h
Simple merge
diff --cc pack-bitmap.c
Simple merge
diff --cc pseudo-merge.c
index 0000000000000000000000000000000000000000,a117520996c3771ec579502e6413e8f595d2df9b..e3e0393f110720568c64bb500a84956a99a0c19e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,756 +1,757 @@@
 -      if (!peel_iterated_oid(oid, &peeled))
+ #include "git-compat-util.h"
+ #include "pseudo-merge.h"
+ #include "date.h"
+ #include "oid-array.h"
+ #include "strbuf.h"
+ #include "config.h"
+ #include "string-list.h"
+ #include "refs.h"
+ #include "pack-bitmap.h"
+ #include "commit.h"
+ #include "alloc.h"
+ #include "progress.h"
+ #include "hex.h"
+ #define DEFAULT_PSEUDO_MERGE_DECAY 1.0
+ #define DEFAULT_PSEUDO_MERGE_MAX_MERGES 64
+ #define DEFAULT_PSEUDO_MERGE_SAMPLE_RATE 1
+ #define DEFAULT_PSEUDO_MERGE_THRESHOLD approxidate("1.week.ago")
+ #define DEFAULT_PSEUDO_MERGE_STABLE_THRESHOLD approxidate("1.month.ago")
+ #define DEFAULT_PSEUDO_MERGE_STABLE_SIZE 512
+ static double gitexp(double base, int exp)
+ {
+       double result = 1;
+       while (1) {
+               if (exp % 2)
+                       result *= base;
+               exp >>= 1;
+               if (!exp)
+                       break;
+               base *= base;
+       }
+       return result;
+ }
+ static uint32_t pseudo_merge_group_size(const struct pseudo_merge_group *group,
+                                       const struct pseudo_merge_matches *matches,
+                                       uint32_t i)
+ {
+       double C = 0.0f;
+       uint32_t n;
+       /*
+        * The size of pseudo-merge groups decays according to a power series,
+        * which looks like:
+        *
+        *   f(n) = C * n^-k
+        *
+        * , where 'n' is the n-th pseudo-merge group, 'f(n)' is its size, 'k'
+        * is the decay rate, and 'C' is a scaling value.
+        *
+        * The value of C depends on the number of groups, decay rate, and total
+        * number of commits. It is computed such that if there are M and N
+        * total groups and commits, respectively, that:
+        *
+        *   N = f(0) + f(1) + ... f(M-1)
+        *
+        * Rearranging to isolate C, we get:
+        *
+        *   N = \sum_{n=1}^M C / n^k
+        *
+        *   N / C = \sum_{n=1}^M n^-k
+        *
+        *   C = N / \sum_{n=1}^M n^-k
+        *
+        * For example, if we have a decay rate of 'k' being equal to 1.5, 'N'
+        * total commits equal to 10,000, and 'M' being equal to 6 groups, then
+        * the (rounded) group sizes are:
+        *
+        *   { 5469, 1934, 1053, 684, 489, 372 }
+        *
+        * increasing the number of total groups, say to 10, scales the group
+        * sizes appropriately:
+        *
+        *   { 5012, 1772, 964, 626, 448, 341, 271, 221, 186, 158 }
+        */
+       for (n = 0; n < group->max_merges; n++)
+               C += 1.0 / gitexp(n + 1, group->decay);
+       C = matches->unstable_nr / C;
+       return (uint32_t)((C / gitexp(i + 1, group->decay)) + 0.5);
+ }
+ static void pseudo_merge_group_init(struct pseudo_merge_group *group)
+ {
+       memset(group, 0, sizeof(struct pseudo_merge_group));
+       strmap_init_with_options(&group->matches, NULL, 0);
+       group->decay = DEFAULT_PSEUDO_MERGE_DECAY;
+       group->max_merges = DEFAULT_PSEUDO_MERGE_MAX_MERGES;
+       group->sample_rate = DEFAULT_PSEUDO_MERGE_SAMPLE_RATE;
+       group->threshold = DEFAULT_PSEUDO_MERGE_THRESHOLD;
+       group->stable_threshold = DEFAULT_PSEUDO_MERGE_STABLE_THRESHOLD;
+       group->stable_size = DEFAULT_PSEUDO_MERGE_STABLE_SIZE;
+ }
+ static int pseudo_merge_config(const char *var, const char *value,
+                              const struct config_context *ctx,
+                              void *cb_data)
+ {
+       struct string_list *list = cb_data;
+       struct string_list_item *item;
+       struct pseudo_merge_group *group;
+       struct strbuf buf = STRBUF_INIT;
+       const char *sub, *key;
+       size_t sub_len;
+       int ret = 0;
+       if (parse_config_key(var, "bitmappseudomerge", &sub, &sub_len, &key))
+               goto done;
+       if (!sub_len)
+               goto done;
+       strbuf_add(&buf, sub, sub_len);
+       item = string_list_lookup(list, buf.buf);
+       if (!item) {
+               item = string_list_insert(list, buf.buf);
+               item->util = xmalloc(sizeof(struct pseudo_merge_group));
+               pseudo_merge_group_init(item->util);
+       }
+       group = item->util;
+       if (!strcmp(key, "pattern")) {
+               struct strbuf re = STRBUF_INIT;
+               free(group->pattern);
+               if (*value != '^')
+                       strbuf_addch(&re, '^');
+               strbuf_addstr(&re, value);
+               group->pattern = xcalloc(1, sizeof(regex_t));
+               if (regcomp(group->pattern, re.buf, REG_EXTENDED))
+                       die(_("failed to load pseudo-merge regex for %s: '%s'"),
+                           sub, re.buf);
+               strbuf_release(&re);
+       } else if (!strcmp(key, "decay")) {
+               group->decay = git_config_double(var, value, ctx->kvi);
+               if (group->decay < 0) {
+                       warning(_("%s must be non-negative, using default"), var);
+                       group->decay = DEFAULT_PSEUDO_MERGE_DECAY;
+               }
+       } else if (!strcmp(key, "samplerate")) {
+               group->sample_rate = git_config_double(var, value, ctx->kvi);
+               if (!(0 <= group->sample_rate && group->sample_rate <= 1)) {
+                       warning(_("%s must be between 0 and 1, using default"), var);
+                       group->sample_rate = DEFAULT_PSEUDO_MERGE_SAMPLE_RATE;
+               }
+       } else if (!strcmp(key, "threshold")) {
+               if (git_config_expiry_date(&group->threshold, var, value)) {
+                       ret = -1;
+                       goto done;
+               }
+       } else if (!strcmp(key, "maxmerges")) {
+               group->max_merges = git_config_int(var, value, ctx->kvi);
+               if (group->max_merges < 0) {
+                       warning(_("%s must be non-negative, using default"), var);
+                       group->max_merges = DEFAULT_PSEUDO_MERGE_MAX_MERGES;
+               }
+       } else if (!strcmp(key, "stablethreshold")) {
+               if (git_config_expiry_date(&group->stable_threshold, var, value)) {
+                       ret = -1;
+                       goto done;
+               }
+       } else if (!strcmp(key, "stablesize")) {
+               group->stable_size = git_config_int(var, value, ctx->kvi);
+               if (group->stable_size <= 0) {
+                       warning(_("%s must be positive, using default"), var);
+                       group->stable_size = DEFAULT_PSEUDO_MERGE_STABLE_SIZE;
+               }
+       }
+ done:
+       strbuf_release(&buf);
+       return ret;
+ }
+ void load_pseudo_merges_from_config(struct string_list *list)
+ {
+       struct string_list_item *item;
+       git_config(pseudo_merge_config, list);
+       for_each_string_list_item(item, list) {
+               struct pseudo_merge_group *group = item->util;
+               if (!group->pattern)
+                       die(_("pseudo-merge group '%s' missing required pattern"),
+                           item->string);
+               if (group->threshold < group->stable_threshold)
+                       die(_("pseudo-merge group '%s' has unstable threshold "
+                             "before stable one"), item->string);
+       }
+ }
+ static int find_pseudo_merge_group_for_ref(const char *refname,
+                                          const struct object_id *oid,
+                                          int flags UNUSED,
+                                          void *_data)
+ {
+       struct bitmap_writer *writer = _data;
+       struct object_id peeled;
+       struct commit *c;
+       uint32_t i;
+       int has_bitmap;
 -      for_each_ref(find_pseudo_merge_group_for_ref, writer);
++      if (!peel_iterated_oid(the_repository, oid, &peeled))
+               oid = &peeled;
+       c = lookup_commit(the_repository, oid);
+       if (!c)
+               return 0;
+       has_bitmap = bitmap_writer_has_bitmapped_object_id(writer, oid);
+       for (i = 0; i < writer->pseudo_merge_groups.nr; i++) {
+               struct pseudo_merge_group *group;
+               struct pseudo_merge_matches *matches;
+               struct strbuf group_name = STRBUF_INIT;
+               regmatch_t captures[16];
+               size_t j;
+               group = writer->pseudo_merge_groups.items[i].util;
+               if (regexec(group->pattern, refname, ARRAY_SIZE(captures),
+                           captures, 0))
+                       continue;
+               if (captures[ARRAY_SIZE(captures) - 1].rm_so != -1)
+                       warning(_("pseudo-merge regex from config has too many capture "
+                                 "groups (max=%"PRIuMAX")"),
+                               (uintmax_t)ARRAY_SIZE(captures) - 2);
+               for (j = !!group->pattern->re_nsub; j < ARRAY_SIZE(captures); j++) {
+                       regmatch_t *match = &captures[j];
+                       if (match->rm_so == -1)
+                               continue;
+                       if (group_name.len)
+                               strbuf_addch(&group_name, '-');
+                       strbuf_add(&group_name, refname + match->rm_so,
+                                  match->rm_eo - match->rm_so);
+               }
+               matches = strmap_get(&group->matches, group_name.buf);
+               if (!matches) {
+                       matches = xcalloc(1, sizeof(*matches));
+                       strmap_put(&group->matches, strbuf_detach(&group_name, NULL),
+                                  matches);
+               }
+               if (c->date <= group->stable_threshold) {
+                       ALLOC_GROW(matches->stable, matches->stable_nr + 1,
+                                  matches->stable_alloc);
+                       matches->stable[matches->stable_nr++] = c;
+               } else if (c->date <= group->threshold && !has_bitmap) {
+                       ALLOC_GROW(matches->unstable, matches->unstable_nr + 1,
+                                  matches->unstable_alloc);
+                       matches->unstable[matches->unstable_nr++] = c;
+               }
+               strbuf_release(&group_name);
+       }
+       return 0;
+ }
+ static struct commit *push_pseudo_merge(struct pseudo_merge_group *group)
+ {
+       struct commit *merge;
+       ALLOC_GROW(group->merges, group->merges_nr + 1, group->merges_alloc);
+       merge = alloc_commit_node(the_repository);
+       merge->object.parsed = 1;
+       merge->object.flags |= BITMAP_PSEUDO_MERGE;
+       group->merges[group->merges_nr++] = merge;
+       return merge;
+ }
+ static struct pseudo_merge_commit_idx *pseudo_merge_idx(kh_oid_map_t *pseudo_merge_commits,
+                                                       const struct object_id *oid)
+ {
+       struct pseudo_merge_commit_idx *pmc;
+       int hash_ret;
+       khiter_t hash_pos = kh_put_oid_map(pseudo_merge_commits, *oid,
+                                          &hash_ret);
+       if (hash_ret) {
+               CALLOC_ARRAY(pmc, 1);
+               kh_value(pseudo_merge_commits, hash_pos) = pmc;
+       } else {
+               pmc = kh_value(pseudo_merge_commits, hash_pos);
+       }
+       return pmc;
+ }
+ #define MIN_PSEUDO_MERGE_SIZE 8
+ static void select_pseudo_merges_1(struct bitmap_writer *writer,
+                                  struct pseudo_merge_group *group,
+                                  struct pseudo_merge_matches *matches)
+ {
+       uint32_t i, j;
+       uint32_t stable_merges_nr;
+       if (!matches->stable_nr && !matches->unstable_nr)
+               return; /* all tips in this group already have bitmaps */
+       stable_merges_nr = matches->stable_nr / group->stable_size;
+       if (matches->stable_nr % group->stable_size)
+               stable_merges_nr++;
+       /* make stable_merges_nr pseudo merges for stable commits */
+       for (i = 0, j = 0; i < stable_merges_nr; i++) {
+               struct commit *merge;
+               struct commit_list **p;
+               merge = push_pseudo_merge(group);
+               p = &merge->parents;
+               /*
+                * For each pseudo-merge created above, add parents to the
+                * allocated commit node from the stable set of commits
+                * (un-bitmapped, newer than the stable threshold).
+                */
+               do {
+                       struct commit *c;
+                       struct pseudo_merge_commit_idx *pmc;
+                       if (j >= matches->stable_nr)
+                               break;
+                       c = matches->stable[j++];
+                       /*
+                        * Here and below, make sure that we keep our mapping of
+                        * commits -> pseudo-merge(s) which include the key'd
+                        * commit up-to-date.
+                        */
+                       pmc = pseudo_merge_idx(writer->pseudo_merge_commits,
+                                              &c->object.oid);
+                       ALLOC_GROW(pmc->pseudo_merge, pmc->nr + 1, pmc->alloc);
+                       pmc->pseudo_merge[pmc->nr++] = writer->pseudo_merges_nr;
+                       p = commit_list_append(c, p);
+               } while (j % group->stable_size);
+               bitmap_writer_push_commit(writer, merge, 1);
+               writer->pseudo_merges_nr++;
+       }
+       /* make up to group->max_merges pseudo merges for unstable commits */
+       for (i = 0, j = 0; i < group->max_merges; i++) {
+               struct commit *merge;
+               struct commit_list **p;
+               uint32_t size, end;
+               merge = push_pseudo_merge(group);
+               p = &merge->parents;
+               size = pseudo_merge_group_size(group, matches, i);
+               end = size < MIN_PSEUDO_MERGE_SIZE ? matches->unstable_nr : j + size;
+               /*
+                * For each pseudo-merge commit created above, add parents to
+                * the allocated commit node from the unstable set of commits
+                * (newer than the stable threshold).
+                *
+                * Account for the sample rate, since not every candidate from
+                * the set of stable commits will be included as a pseudo-merge
+                * parent.
+                */
+               for (; j < end && j < matches->unstable_nr; j++) {
+                       struct commit *c = matches->unstable[j];
+                       struct pseudo_merge_commit_idx *pmc;
+                       if (j % (uint32_t)(1.0 / group->sample_rate))
+                               continue;
+                       pmc = pseudo_merge_idx(writer->pseudo_merge_commits,
+                                              &c->object.oid);
+                       ALLOC_GROW(pmc->pseudo_merge, pmc->nr + 1, pmc->alloc);
+                       pmc->pseudo_merge[pmc->nr++] = writer->pseudo_merges_nr;
+                       p = commit_list_append(c, p);
+               }
+               bitmap_writer_push_commit(writer, merge, 1);
+               writer->pseudo_merges_nr++;
+               if (end >= matches->unstable_nr)
+                       break;
+       }
+ }
+ static int commit_date_cmp(const void *va, const void *vb)
+ {
+       timestamp_t a = (*(const struct commit **)va)->date;
+       timestamp_t b = (*(const struct commit **)vb)->date;
+       if (a < b)
+               return -1;
+       else if (a > b)
+               return 1;
+       return 0;
+ }
+ static void sort_pseudo_merge_matches(struct pseudo_merge_matches *matches)
+ {
+       QSORT(matches->stable, matches->stable_nr, commit_date_cmp);
+       QSORT(matches->unstable, matches->unstable_nr, commit_date_cmp);
+ }
+ void select_pseudo_merges(struct bitmap_writer *writer,
+                         struct commit **commits, size_t commits_nr)
+ {
+       struct progress *progress = NULL;
+       uint32_t i;
+       if (!writer->pseudo_merge_groups.nr)
+               return;
+       if (writer->show_progress)
+               progress = start_progress("Selecting pseudo-merge commits",
+                                         writer->pseudo_merge_groups.nr);
++      refs_for_each_ref(get_main_ref_store(the_repository),
++                        find_pseudo_merge_group_for_ref, writer);
+       for (i = 0; i < writer->pseudo_merge_groups.nr; i++) {
+               struct pseudo_merge_group *group;
+               struct hashmap_iter iter;
+               struct strmap_entry *e;
+               group = writer->pseudo_merge_groups.items[i].util;
+               strmap_for_each_entry(&group->matches, &iter, e) {
+                       struct pseudo_merge_matches *matches = e->value;
+                       sort_pseudo_merge_matches(matches);
+                       select_pseudo_merges_1(writer, group, matches);
+               }
+               display_progress(progress, i + 1);
+       }
+       stop_progress(&progress);
+ }
+ void free_pseudo_merge_map(struct pseudo_merge_map *pm)
+ {
+       uint32_t i;
+       for (i = 0; i < pm->nr; i++) {
+               ewah_pool_free(pm->v[i].commits);
+               ewah_pool_free(pm->v[i].bitmap);
+       }
+       free(pm->v);
+ }
+ struct pseudo_merge_commit_ext {
+       uint32_t nr;
+       const unsigned char *ptr;
+ };
+ static int pseudo_merge_ext_at(const struct pseudo_merge_map *pm,
+                              struct pseudo_merge_commit_ext *ext, size_t at)
+ {
+       if (at >= pm->map_size)
+               return error(_("extended pseudo-merge read out-of-bounds "
+                              "(%"PRIuMAX" >= %"PRIuMAX")"),
+                            (uintmax_t)at, (uintmax_t)pm->map_size);
+       if (at + 4 >= pm->map_size)
+               return error(_("extended pseudo-merge entry is too short "
+                              "(%"PRIuMAX" >= %"PRIuMAX")"),
+                            (uintmax_t)(at + 4), (uintmax_t)pm->map_size);
+       ext->nr = get_be32(pm->map + at);
+       ext->ptr = pm->map + at + sizeof(uint32_t);
+       return 0;
+ }
+ struct ewah_bitmap *pseudo_merge_bitmap(const struct pseudo_merge_map *pm,
+                                       struct pseudo_merge *merge)
+ {
+       if (!merge->loaded_commits)
+               BUG("cannot use unloaded pseudo-merge bitmap");
+       if (!merge->loaded_bitmap) {
+               size_t at = merge->bitmap_at;
+               merge->bitmap = read_bitmap(pm->map, pm->map_size, &at);
+               merge->loaded_bitmap = 1;
+       }
+       return merge->bitmap;
+ }
+ struct pseudo_merge *use_pseudo_merge(const struct pseudo_merge_map *pm,
+                                     struct pseudo_merge *merge)
+ {
+       if (!merge->loaded_commits) {
+               size_t pos = merge->at;
+               merge->commits = read_bitmap(pm->map, pm->map_size, &pos);
+               merge->bitmap_at = pos;
+               merge->loaded_commits = 1;
+       }
+       return merge;
+ }
+ static struct pseudo_merge *pseudo_merge_at(const struct pseudo_merge_map *pm,
+                                           struct object_id *oid,
+                                           size_t want)
+ {
+       size_t lo = 0;
+       size_t hi = pm->nr;
+       while (lo < hi) {
+               size_t mi = lo + (hi - lo) / 2;
+               size_t got = pm->v[mi].at;
+               if (got == want)
+                       return use_pseudo_merge(pm, &pm->v[mi]);
+               else if (got < want)
+                       hi = mi;
+               else
+                       lo = mi + 1;
+       }
+       warning(_("could not find pseudo-merge for commit %s at offset %"PRIuMAX),
+               oid_to_hex(oid), (uintmax_t)want);
+       return NULL;
+ }
+ struct pseudo_merge_commit {
+       uint32_t commit_pos;
+       uint64_t pseudo_merge_ofs;
+ };
+ #define PSEUDO_MERGE_COMMIT_RAWSZ (sizeof(uint32_t)+sizeof(uint64_t))
+ static void read_pseudo_merge_commit_at(struct pseudo_merge_commit *merge,
+                                       const unsigned char *at)
+ {
+       merge->commit_pos = get_be32(at);
+       merge->pseudo_merge_ofs = get_be64(at + sizeof(uint32_t));
+ }
+ static int nth_pseudo_merge_ext(const struct pseudo_merge_map *pm,
+                               struct pseudo_merge_commit_ext *ext,
+                               struct pseudo_merge_commit *merge,
+                               uint32_t n)
+ {
+       size_t ofs;
+       if (n >= ext->nr)
+               return error(_("extended pseudo-merge lookup out-of-bounds "
+                              "(%"PRIu32" >= %"PRIu32")"), n, ext->nr);
+       ofs = get_be64(ext->ptr + st_mult(n, sizeof(uint64_t)));
+       if (ofs >= pm->map_size)
+               return error(_("out-of-bounds read: (%"PRIuMAX" >= %"PRIuMAX")"),
+                            (uintmax_t)ofs, (uintmax_t)pm->map_size);
+       read_pseudo_merge_commit_at(merge, pm->map + ofs);
+       return 0;
+ }
+ static unsigned apply_pseudo_merge(const struct pseudo_merge_map *pm,
+                                  struct pseudo_merge *merge,
+                                  struct bitmap *result,
+                                  struct bitmap *roots)
+ {
+       if (merge->satisfied)
+               return 0;
+       if (!ewah_bitmap_is_subset(merge->commits, roots ? roots : result))
+               return 0;
+       bitmap_or_ewah(result, pseudo_merge_bitmap(pm, merge));
+       if (roots)
+               bitmap_or_ewah(roots, pseudo_merge_bitmap(pm, merge));
+       merge->satisfied = 1;
+       return 1;
+ }
+ static int pseudo_merge_commit_cmp(const void *va, const void *vb)
+ {
+       struct pseudo_merge_commit merge;
+       uint32_t key = *(uint32_t*)va;
+       read_pseudo_merge_commit_at(&merge, vb);
+       if (key < merge.commit_pos)
+               return -1;
+       if (key > merge.commit_pos)
+               return 1;
+       return 0;
+ }
+ static struct pseudo_merge_commit *find_pseudo_merge(const struct pseudo_merge_map *pm,
+                                                    uint32_t pos)
+ {
+       if (!pm->commits_nr)
+               return NULL;
+       return bsearch(&pos, pm->commits, pm->commits_nr,
+                      PSEUDO_MERGE_COMMIT_RAWSZ, pseudo_merge_commit_cmp);
+ }
+ int apply_pseudo_merges_for_commit(const struct pseudo_merge_map *pm,
+                                  struct bitmap *result,
+                                  struct commit *commit, uint32_t commit_pos)
+ {
+       struct pseudo_merge *merge;
+       struct pseudo_merge_commit *merge_commit;
+       int ret = 0;
+       merge_commit = find_pseudo_merge(pm, commit_pos);
+       if (!merge_commit)
+               return 0;
+       if (merge_commit->pseudo_merge_ofs & ((uint64_t)1<<63)) {
+               struct pseudo_merge_commit_ext ext = { 0 };
+               off_t ofs = merge_commit->pseudo_merge_ofs & ~((uint64_t)1<<63);
+               uint32_t i;
+               if (pseudo_merge_ext_at(pm, &ext, ofs) < -1) {
+                       warning(_("could not read extended pseudo-merge table "
+                                 "for commit %s"),
+                               oid_to_hex(&commit->object.oid));
+                       return ret;
+               }
+               for (i = 0; i < ext.nr; i++) {
+                       if (nth_pseudo_merge_ext(pm, &ext, merge_commit, i) < 0)
+                               return ret;
+                       merge = pseudo_merge_at(pm, &commit->object.oid,
+                                               merge_commit->pseudo_merge_ofs);
+                       if (!merge)
+                               return ret;
+                       if (apply_pseudo_merge(pm, merge, result, NULL))
+                               ret++;
+               }
+       } else {
+               merge = pseudo_merge_at(pm, &commit->object.oid,
+                                       merge_commit->pseudo_merge_ofs);
+               if (!merge)
+                       return ret;
+               if (apply_pseudo_merge(pm, merge, result, NULL))
+                       ret++;
+       }
+       if (ret)
+               cascade_pseudo_merges(pm, result, NULL);
+       return ret;
+ }
+ int cascade_pseudo_merges(const struct pseudo_merge_map *pm,
+                         struct bitmap *result,
+                         struct bitmap *roots)
+ {
+       unsigned any_satisfied;
+       int ret = 0;
+       do {
+               struct pseudo_merge *merge;
+               uint32_t i;
+               any_satisfied = 0;
+               for (i = 0; i < pm->nr; i++) {
+                       merge = use_pseudo_merge(pm, &pm->v[i]);
+                       if (apply_pseudo_merge(pm, merge, result, roots)) {
+                               any_satisfied |= 1;
+                               ret++;
+                       }
+               }
+       } while (any_satisfied);
+       return ret;
+ }
+ struct pseudo_merge *pseudo_merge_for_parents(const struct pseudo_merge_map *pm,
+                                             struct bitmap *parents)
+ {
+       struct pseudo_merge *match = NULL;
+       size_t i;
+       if (!pm->nr)
+               return NULL;
+       /*
+        * NOTE: this loop is quadratic in the worst-case (where no
+        * matching pseudo-merge bitmaps are found), but in practice
+        * this is OK for a few reasons:
+        *
+        *   - Rejecting pseudo-merge bitmaps that do not match the
+        *     given commit is done quickly (i.e. `bitmap_equals_ewah()`
+        *     returns early when we know the two bitmaps aren't equal.
+        *
+        *   - Already matched pseudo-merge bitmaps (which we track with
+        *     the `->satisfied` bit here) are skipped as potential
+        *     candidates.
+        *
+        *   - The number of pseudo-merges should be small (in the
+        *     hundreds for most repositories).
+        *
+        * If in the future this semi-quadratic behavior does become a
+        * problem, another approach would be to keep track of which
+        * pseudo-merges are still "viable" after enumerating the
+        * pseudo-merge commit's parents:
+        *
+        *   - A pseudo-merge bitmap becomes non-viable when the bit(s)
+        *     corresponding to one or more parent(s) of the given
+        *     commit are not set in a candidate pseudo-merge's commits
+        *     bitmap.
+        *
+        *   - After processing all bits, enumerate the remaining set of
+        *     viable pseudo-merge bitmaps, and check that their
+        *     popcount() matches the number of parents in the given
+        *     commit.
+        */
+       for (i = 0; i < pm->nr; i++) {
+               struct pseudo_merge *candidate = use_pseudo_merge(pm, &pm->v[i]);
+               if (!candidate || candidate->satisfied)
+                       continue;
+               if (!bitmap_equals_ewah(parents, candidate->commits))
+                       continue;
+               match = candidate;
+               match->satisfied = 1;
+               break;
+       }
+       return match;
+ }