From 3c5783698854b56376b775af5053ae6fbe825cc0 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Mon, 8 Jun 2026 19:10:50 +0000 Subject: [PATCH 01/11] prio-queue: rename .nr to .nr_ and add accessor helpers Rename the .nr member to .nr_ so that callers outside prio-queue.c that directly reference .nr get a compilation error. This catches both existing misuse and future in-flight topics. Add prio_queue_size() for callers that need to know the element count and prio_queue_for_each() for callers that need to walk all elements. Convert all external .nr users: - Loop conditions: use prio_queue_size(), prio_queue_get(), or prio_queue_peek() as the loop condition - Array iterations: use prio_queue_for_each() Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- builtin/describe.c | 11 ++++++++--- builtin/last-modified.c | 7 +++---- builtin/show-branch.c | 13 ++++++------- commit-reach.c | 14 +++++++------- fetch-pack.c | 4 ++-- negotiator/default.c | 4 +++- negotiator/skipping.c | 12 +++++++----- object-name.c | 2 +- pack-bitmap-write.c | 10 +++++----- path-walk.c | 8 ++++---- prio-queue.c | 38 +++++++++++++++++++------------------- prio-queue.h | 12 +++++++++++- revision.c | 16 +++++++--------- walker.c | 4 ++-- 14 files changed, 85 insertions(+), 70 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index 1c47d7c0b7c38d..8e88bdeea6cf1c 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -278,7 +278,7 @@ static void lazy_queue_put(struct lazy_queue *queue, void *thing) static bool lazy_queue_empty(const struct lazy_queue *queue) { - return queue->queue.nr == (queue->get_pending ? 1 : 0); + return prio_queue_size(&queue->queue) == (queue->get_pending ? 1 : 0); } static void lazy_queue_clear(struct lazy_queue *queue) @@ -292,9 +292,14 @@ static unsigned long finish_depth_computation(struct lazy_queue *queue, { unsigned long seen_commits = 0; struct oidset unflagged = OIDSET_INIT; + struct commit *commit; + int skip = queue->get_pending ? 1 : 0; - for (size_t i = queue->get_pending ? 1 : 0; i < queue->queue.nr; i++) { - struct commit *commit = queue->queue.array[i].data; + prio_queue_for_each(&queue->queue, commit) { + if (skip) { + skip = 0; + continue; + } if (!(commit->object.flags & best->flag_within)) oidset_insert(&unflagged, &commit->object.oid); } diff --git a/builtin/last-modified.c b/builtin/last-modified.c index 8900ceece1cdf3..5478182f2e95c2 100644 --- a/builtin/last-modified.c +++ b/builtin/last-modified.c @@ -344,6 +344,7 @@ static void process_parent(struct last_modified *lm, static int last_modified_run(struct last_modified *lm) { int max_count, queue_popped = 0; + struct commit *c, *n; struct prio_queue queue = { compare_commits_by_gen_then_commit_date }; struct prio_queue not_queue = { compare_commits_by_gen_then_commit_date }; struct commit_list *list; @@ -389,10 +390,9 @@ static int last_modified_run(struct last_modified *lm) } } - while (queue.nr) { + while ((c = prio_queue_get(&queue))) { int parent_i; struct commit_list *p; - struct commit *c = prio_queue_get(&queue); struct bitmap *active_c = active_paths_for(lm, c); if ((0 <= max_count && max_count < ++queue_popped) || @@ -416,9 +416,8 @@ static int last_modified_run(struct last_modified *lm) */ repo_parse_commit(lm->rev.repo, c); - while (not_queue.nr) { + while ((n = prio_queue_get(¬_queue))) { struct commit_list *np; - struct commit *n = prio_queue_get(¬_queue); repo_parse_commit(lm->rev.repo, n); diff --git a/builtin/show-branch.c b/builtin/show-branch.c index f02831b08500c4..8846f2376fc2ff 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -62,11 +62,10 @@ static const char *get_color_reset_code(void) static struct commit *interesting(struct prio_queue *queue) { - for (size_t i = 0; i < queue->nr; i++) { - struct commit *commit = queue->array[i].data; - if (commit->object.flags & UNINTERESTING) - continue; - return commit; + struct commit *commit; + prio_queue_for_each(queue, commit) { + if (!(commit->object.flags & UNINTERESTING)) + return commit; } return NULL; } @@ -228,11 +227,11 @@ static void join_revs(struct prio_queue *queue, { int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); int all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + struct commit *commit; - while (queue->nr) { + while ((commit = prio_queue_peek(queue))) { struct commit_list *parents; int still_interesting = !!interesting(queue); - struct commit *commit = prio_queue_peek(queue); bool get_pending = true; int flags = commit->object.flags & all_mask; diff --git a/commit-reach.c b/commit-reach.c index 9b3ea46d6f2824..a849de653ef544 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -41,8 +41,8 @@ static int compare_commits_by_gen(const void *_a, const void *_b) static int queue_has_nonstale(struct prio_queue *queue) { - for (size_t i = 0; i < queue->nr; i++) { - struct commit *commit = queue->array[i].data; + struct commit *commit; + prio_queue_for_each(queue, commit) { if (!(commit->object.flags & STALE)) return 1; } @@ -1070,6 +1070,7 @@ void ahead_behind(struct repository *r, struct ahead_behind_count *counts, size_t counts_nr) { struct prio_queue queue = { .compare = compare_commits_by_gen_then_commit_date }; + void *entry; size_t width = DIV_ROUND_UP(commits_nr, BITS_IN_EWORD); if (!commits_nr || !counts_nr) @@ -1135,8 +1136,8 @@ void ahead_behind(struct repository *r, /* STALE is used here, PARENT2 is used by insert_no_dup(). */ repo_clear_commit_marks(r, PARENT2 | STALE); - for (size_t i = 0; i < queue.nr; i++) - free_bit_array(queue.array[i].data); + prio_queue_for_each(&queue, entry) + free_bit_array(entry); clear_bit_arrays(&bit_arrays); clear_prio_queue(&queue); } @@ -1269,7 +1270,7 @@ int get_branch_base_for_tip(struct repository *r, size_t bases_nr) { int best_index = -1; - struct commit *branch_point = NULL; + struct commit *c, *branch_point = NULL; struct prio_queue queue = { compare_commits_by_gen_then_commit_date }; int found_missing_gen = 0; @@ -1322,8 +1323,7 @@ int get_branch_base_for_tip(struct repository *r, prio_queue_put(&queue, c); } - while (queue.nr) { - struct commit *c = prio_queue_get(&queue); + while ((c = prio_queue_get(&queue))) { int best_for_c = get_best(c); int best_for_p, positive; struct commit *parent; diff --git a/fetch-pack.c b/fetch-pack.c index 120e01f3cf2674..29c41132ee0495 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -662,8 +662,8 @@ static int mark_complete_oid(const struct reference *ref, void *cb_data UNUSED) static void mark_recent_complete_commits(struct fetch_pack_args *args, timestamp_t cutoff) { - while (complete.nr) { - struct commit *item = prio_queue_peek(&complete); + struct commit *item; + while ((item = prio_queue_peek(&complete))) { if (item->date < cutoff) break; print_verbose(args, _("Marking %s as complete"), diff --git a/negotiator/default.c b/negotiator/default.c index 78d58d57cebbfb..19cdf3808cf266 100644 --- a/negotiator/default.c +++ b/negotiator/default.c @@ -113,10 +113,12 @@ static const struct object_id *get_rev(struct negotiation_state *ns) unsigned int mark; struct commit_list *parents; - if (ns->rev_list.nr == 0 || ns->non_common_revs == 0) + if (ns->non_common_revs == 0) return NULL; commit = prio_queue_get(&ns->rev_list); + if (!commit) + return NULL; repo_parse_commit(the_repository, commit); parents = commit->parents; diff --git a/negotiator/skipping.c b/negotiator/skipping.c index 68c9b3b997dc80..db90fa77b5325b 100644 --- a/negotiator/skipping.c +++ b/negotiator/skipping.c @@ -143,8 +143,7 @@ static int push_parent(struct data *data, struct entry *entry, /* * Find the existing entry and use it. */ - for (size_t i = 0; i < data->rev_list.nr; i++) { - parent_entry = data->rev_list.array[i].data; + prio_queue_for_each(&data->rev_list, parent_entry) { if (parent_entry->commit == to_push) goto parent_found; } @@ -181,10 +180,12 @@ static const struct object_id *get_rev(struct data *data) struct commit_list *p; int parent_pushed = 0; - if (data->rev_list.nr == 0 || data->non_common_revs == 0) + if (data->non_common_revs == 0) return NULL; entry = prio_queue_get(&data->rev_list); + if (!entry) + return NULL; commit = entry->commit; commit->object.flags |= POPPED; if (!(commit->object.flags & COMMON)) @@ -253,8 +254,9 @@ static void have_sent(struct fetch_negotiator *n, struct commit *c) static void release(struct fetch_negotiator *n) { struct data *data = n->data; - for (size_t i = 0; i < data->rev_list.nr; i++) - free(data->rev_list.array[i].data); + void *entry; + prio_queue_for_each(&data->rev_list, entry) + free(entry); clear_prio_queue(&data->rev_list); FREE_AND_NULL(data); } diff --git a/object-name.c b/object-name.c index 9ac86f19c77bbd..2fedfe1761016e 100644 --- a/object-name.c +++ b/object-name.c @@ -1208,7 +1208,7 @@ static int get_oid_oneline(struct repository *r, l->item->object.flags |= ONELINE_SEEN; prio_queue_put(©, l->item); } - while (copy.nr) { + while (prio_queue_size(©)) { const char *p, *buf; struct commit *commit; int matches; diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c index 1c8070f99c03ca..ed9714b135cf52 100644 --- a/pack-bitmap-write.c +++ b/pack-bitmap-write.c @@ -513,6 +513,8 @@ static int fill_bitmap_commit(struct bitmap_writer *writer, struct bitmap_index *old_bitmap, const uint32_t *mapping) { + struct commit *c; + struct tree *tree; int found; uint32_t pos; if (!ent->bitmap) @@ -520,9 +522,8 @@ static int fill_bitmap_commit(struct bitmap_writer *writer, prio_queue_put(queue, commit); - while (queue->nr) { + while ((c = prio_queue_get(queue))) { struct commit_list *p; - struct commit *c = prio_queue_get(queue); if (old_bitmap && mapping) { struct ewah_bitmap *old; @@ -574,9 +575,8 @@ static int fill_bitmap_commit(struct bitmap_writer *writer, } } - while (tree_queue->nr) { - if (fill_bitmap_tree(writer, ent->bitmap, - prio_queue_get(tree_queue)) < 0) + while ((tree = prio_queue_get(tree_queue))) { + if (fill_bitmap_tree(writer, ent->bitmap, tree) < 0) return -1; } return 0; diff --git a/path-walk.c b/path-walk.c index 94ff90bd1566b6..cf3b2d0765961b 100644 --- a/path-walk.c +++ b/path-walk.c @@ -699,6 +699,7 @@ int walk_objects_by_path(struct path_walk_info *info) int ret; size_t commits_nr = 0, paths_nr = 0; struct commit *c; + char *path; struct type_and_oid_list *root_tree_list; struct type_and_oid_list *commit_list; struct path_walk_context ctx = { @@ -808,8 +809,7 @@ int walk_objects_by_path(struct path_walk_info *info) free(commit_list); trace2_region_enter("path-walk", "path-walk", info->revs->repo); - while (!ret && ctx.path_stack.nr) { - char *path = prio_queue_get(&ctx.path_stack); + while (!ret && (path = prio_queue_get(&ctx.path_stack))) { paths_nr++; ret = walk_path(&ctx, path); @@ -821,12 +821,12 @@ int walk_objects_by_path(struct path_walk_info *info) if (!strmap_empty(&ctx.paths_to_lists)) { struct hashmap_iter iter; struct strmap_entry *entry; + char *path; strmap_for_each_entry(&ctx.paths_to_lists, &iter, entry) push_to_stack(&ctx, entry->key); - while (!ret && ctx.path_stack.nr) { - char *path = prio_queue_get(&ctx.path_stack); + while (!ret && (path = prio_queue_get(&ctx.path_stack))) { paths_nr++; ret = walk_path(&ctx, path); diff --git a/prio-queue.c b/prio-queue.c index 9748528ce6ecd6..ead4faf4bbda4a 100644 --- a/prio-queue.c +++ b/prio-queue.c @@ -22,16 +22,16 @@ void prio_queue_reverse(struct prio_queue *queue) if (queue->compare) BUG("prio_queue_reverse() on non-LIFO queue"); - if (!queue->nr) + if (!queue->nr_) return; - for (i = 0; i < (j = (queue->nr - 1) - i); i++) + for (i = 0; i < (j = (queue->nr_ - 1) - i); i++) swap(queue, i, j); } void clear_prio_queue(struct prio_queue *queue) { FREE_AND_NULL(queue->array); - queue->nr = 0; + queue->nr_ = 0; queue->alloc = 0; queue->insertion_ctr = 0; } @@ -41,15 +41,15 @@ void prio_queue_put(struct prio_queue *queue, void *thing) size_t ix, parent; /* Append at the end */ - ALLOC_GROW(queue->array, queue->nr + 1, queue->alloc); - queue->array[queue->nr].ctr = queue->insertion_ctr++; - queue->array[queue->nr].data = thing; - queue->nr++; + ALLOC_GROW(queue->array, queue->nr_ + 1, queue->alloc); + queue->array[queue->nr_].ctr = queue->insertion_ctr++; + queue->array[queue->nr_].data = thing; + queue->nr_++; if (!queue->compare) return; /* LIFO */ /* Bubble up the new one */ - for (ix = queue->nr - 1; ix; ix = parent) { + for (ix = queue->nr_ - 1; ix; ix = parent) { parent = (ix - 1) / 2; if (compare(queue, parent, ix) <= 0) break; @@ -63,9 +63,9 @@ static void sift_down_root(struct prio_queue *queue) size_t ix, child; /* Push down the one at the root */ - for (ix = 0; ix * 2 + 1 < queue->nr; ix = child) { + for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) { child = ix * 2 + 1; /* left */ - if (child + 1 < queue->nr && + if (child + 1 < queue->nr_ && compare(queue, child, child + 1) >= 0) child++; /* use right child */ @@ -80,36 +80,36 @@ void *prio_queue_get(struct prio_queue *queue) { void *result; - if (!queue->nr) + if (!queue->nr_) return NULL; if (!queue->compare) - return queue->array[--queue->nr].data; /* LIFO */ + return queue->array[--queue->nr_].data; /* LIFO */ result = queue->array[0].data; - if (!--queue->nr) + if (!--queue->nr_) return result; - queue->array[0] = queue->array[queue->nr]; + queue->array[0] = queue->array[queue->nr_]; sift_down_root(queue); return result; } void *prio_queue_peek(struct prio_queue *queue) { - if (!queue->nr) + if (!queue->nr_) return NULL; if (!queue->compare) - return queue->array[queue->nr - 1].data; + return queue->array[queue->nr_ - 1].data; return queue->array[0].data; } void prio_queue_replace(struct prio_queue *queue, void *thing) { - if (!queue->nr) { + if (!queue->nr_) { prio_queue_put(queue, thing); } else if (!queue->compare) { - queue->array[queue->nr - 1].ctr = queue->insertion_ctr++; - queue->array[queue->nr - 1].data = thing; + queue->array[queue->nr_ - 1].ctr = queue->insertion_ctr++; + queue->array[queue->nr_ - 1].data = thing; } else { queue->array[0].ctr = queue->insertion_ctr++; queue->array[0].data = thing; diff --git a/prio-queue.h b/prio-queue.h index da7fad2f1f408f..7f2aa986b1c7db 100644 --- a/prio-queue.h +++ b/prio-queue.h @@ -30,7 +30,7 @@ struct prio_queue { prio_queue_compare_fn compare; size_t insertion_ctr; void *cb_data; - size_t alloc, nr; + size_t alloc, nr_; struct prio_queue_entry *array; }; @@ -52,6 +52,16 @@ void *prio_queue_get(struct prio_queue *); */ void *prio_queue_peek(struct prio_queue *); +static inline size_t prio_queue_size(const struct prio_queue *queue) +{ + return queue->nr_; +} + +#define prio_queue_for_each(queue, it) \ + for (size_t pq_ix_ = 0; \ + pq_ix_ < (queue)->nr_ && ((it) = (queue)->array[pq_ix_].data, 1); \ + pq_ix_++) + /* * Replace the "thing" that compares the smallest with a new "thing", * like prio_queue_get()+prio_queue_put() would do, but in a more diff --git a/revision.c b/revision.c index 5693618be4ec81..34e2d146f4e09d 100644 --- a/revision.c +++ b/revision.c @@ -476,16 +476,15 @@ static struct commit *handle_commit(struct rev_info *revs, static int everybody_uninteresting(struct prio_queue *orig, struct commit **interesting_cache) { - size_t i; + struct commit *commit; if (*interesting_cache) { - struct commit *commit = *interesting_cache; + commit = *interesting_cache; if (!(commit->object.flags & UNINTERESTING)) return 0; } - for (i = 0; i < orig->nr; i++) { - struct commit *commit = orig->array[i].data; + prio_queue_for_each(orig, commit) { if (commit->object.flags & UNINTERESTING) continue; @@ -1446,7 +1445,7 @@ static int limit_list(struct rev_info *revs) struct commit_list *original_list = revs->commits; struct commit_list *newlist = NULL; struct commit_list **p = &newlist; - struct commit *interesting_cache = NULL; + struct commit *commit, *interesting_cache = NULL; struct prio_queue queue = { .compare = compare_commits_by_commit_date }; if (revs->ancestry_path_implicit_bottoms) { @@ -1461,8 +1460,7 @@ static int limit_list(struct rev_info *revs) prio_queue_put(&queue, commit); } - while (queue.nr) { - struct commit *commit = prio_queue_get(&queue); + while ((commit = prio_queue_get(&queue))) { struct object *obj = &commit->object; if (commit == interesting_cache) @@ -4028,8 +4026,8 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs, static void merge_queue_into_list(struct prio_queue *q, struct commit_list **list) { - while (q->nr) { - struct commit *item = prio_queue_peek(q); + struct commit *item; + while ((item = prio_queue_peek(q))) { struct commit_list *p = *list; if (p && p->item->date >= item->date) diff --git a/walker.c b/walker.c index e98eb6da53692e..e3de77f0925082 100644 --- a/walker.c +++ b/walker.c @@ -84,12 +84,12 @@ static struct prio_queue complete = { compare_commits_by_commit_date }; static int process_commit(struct walker *walker, struct commit *commit) { struct commit_list *parents; + struct commit *item; if (repo_parse_commit(the_repository, commit)) return -1; - while (complete.nr) { - struct commit *item = prio_queue_peek(&complete); + while ((item = prio_queue_peek(&complete))) { if (item->date < commit->date) break; pop_most_recent_commit(&complete, COMPLETE); From 9f75e7a150edfe931391047bf84c697b4b15c4c4 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Mon, 8 Jun 2026 19:10:51 +0000 Subject: [PATCH 02/11] prio-queue: fold lazy_queue into prio_queue for automatic get+put fusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defer the actual removal in prio_queue_get() until the next operation. If that next operation is a prio_queue_put(), the removal and insertion are fused into a single replace — writing the new element at the root and sifting it down — which avoids a full remove-rebalance-insert cycle. This matches the dominant usage pattern in git's commit traversal: get a commit, then put its parents. The first parent insertion after each get is now a replace operation automatically. This generalizes the lazy_queue pattern from builtin/describe.c (introduced in 08bb69d70f) into prio_queue itself. Three callers independently implemented the same get+put fusion: - builtin/describe.c had a full lazy_queue wrapper - commit.c:pop_most_recent_commit() used peek+replace - builtin/show-branch.c:join_revs() used peek+replace All three now collapse to plain _get() and _put(), with the data structure handling the fusion internally. This simplifies callers and means every prio_queue user gets the optimization for free without needing to implement it manually. Remove prio_queue_replace() since no external callers remain. Benchmarked on a 1.8M-commit monorepo (30 interleaved runs, paired t-test, Xeon @ 2.20GHz): Code paths that previously did eager get+put (new optimization): Command base patched change p merge-base --all A A~1000 3828ms 3725ms -2.69% 0.0001 rev-list --count A~1000..A 3055ms 2986ms -2.27% 0.0601 log --oneline A~1000..A 3408ms 3350ms -1.71% 0.0482 Code paths that already had manual get+put fusion (expect neutral — the optimization moves into prio_queue but the number of heap operations stays the same): Command base patched change p show-branch A A~1000 9156ms 9127ms -0.32% 0.3470 describe (4751 revs, 81K repo) 1983ms 1963ms -1.02% <0.001 No regressions in any scenario. Suggested-by: René Scharfe Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- builtin/describe.c | 75 +++++++----------------------- builtin/show-branch.c | 11 ++--- commit.c | 11 +---- prio-queue.c | 92 ++++++++++++++++++++----------------- prio-queue.h | 15 ++---- t/unit-tests/u-prio-queue.c | 6 +-- 6 files changed, 78 insertions(+), 132 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index 8e88bdeea6cf1c..64424543ef229c 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -251,61 +251,19 @@ static int compare_pt(const void *a_, const void *b_) return 0; } -struct lazy_queue { - struct prio_queue queue; - bool get_pending; -}; - -#define LAZY_QUEUE_INIT { { compare_commits_by_commit_date }, false } - -static void *lazy_queue_get(struct lazy_queue *queue) -{ - if (queue->get_pending) - prio_queue_get(&queue->queue); - else - queue->get_pending = true; - return prio_queue_peek(&queue->queue); -} - -static void lazy_queue_put(struct lazy_queue *queue, void *thing) -{ - if (queue->get_pending) - prio_queue_replace(&queue->queue, thing); - else - prio_queue_put(&queue->queue, thing); - queue->get_pending = false; -} - -static bool lazy_queue_empty(const struct lazy_queue *queue) -{ - return prio_queue_size(&queue->queue) == (queue->get_pending ? 1 : 0); -} - -static void lazy_queue_clear(struct lazy_queue *queue) -{ - clear_prio_queue(&queue->queue); - queue->get_pending = false; -} - -static unsigned long finish_depth_computation(struct lazy_queue *queue, +static unsigned long finish_depth_computation(struct prio_queue *queue, struct possible_tag *best) { unsigned long seen_commits = 0; struct oidset unflagged = OIDSET_INIT; - struct commit *commit; - int skip = queue->get_pending ? 1 : 0; + struct commit *c; - prio_queue_for_each(&queue->queue, commit) { - if (skip) { - skip = 0; - continue; - } - if (!(commit->object.flags & best->flag_within)) - oidset_insert(&unflagged, &commit->object.oid); + prio_queue_for_each(queue, c) { + if (!(c->object.flags & best->flag_within)) + oidset_insert(&unflagged, &c->object.oid); } - while (!lazy_queue_empty(queue)) { - struct commit *c = lazy_queue_get(queue); + while ((c = prio_queue_get(queue))) { struct commit_list *parents = c->parents; seen_commits++; if (c->object.flags & best->flag_within) { @@ -321,7 +279,7 @@ static unsigned long finish_depth_computation(struct lazy_queue *queue, repo_parse_commit(the_repository, p); seen = p->object.flags & SEEN; if (!seen) - lazy_queue_put(queue, p); + prio_queue_put(queue, p); flag_before = p->object.flags & best->flag_within; p->object.flags |= c->object.flags; flag_after = p->object.flags & best->flag_within; @@ -369,8 +327,8 @@ static void append_suffix(int depth, const struct object_id *oid, struct strbuf static void describe_commit(struct commit *cmit, struct strbuf *dst) { - struct commit *gave_up_on = NULL; - struct lazy_queue queue = LAZY_QUEUE_INIT; + struct commit *c, *gave_up_on = NULL; + struct prio_queue queue = { compare_commits_by_commit_date }; struct commit_name *n; struct possible_tag all_matches[MAX_TAGS]; unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; @@ -412,9 +370,8 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst) } cmit->object.flags = SEEN; - lazy_queue_put(&queue, cmit); - while (!lazy_queue_empty(&queue)) { - struct commit *c = lazy_queue_get(&queue); + prio_queue_put(&queue, cmit); + while ((c = prio_queue_get(&queue))) { struct commit_list *parents = c->parents; struct commit_name **slot; @@ -448,7 +405,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst) t->depth++; } /* Stop if last remaining path already covered by best candidate(s) */ - if (annotated_cnt && lazy_queue_empty(&queue)) { + if (annotated_cnt && !prio_queue_size(&queue)) { int best_depth = INT_MAX; unsigned best_within = 0; for (cur_match = 0; cur_match < match_cnt; cur_match++) { @@ -471,7 +428,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst) struct commit *p = parents->item; repo_parse_commit(the_repository, p); if (!(p->object.flags & SEEN)) - lazy_queue_put(&queue, p); + prio_queue_put(&queue, p); p->object.flags |= c->object.flags; parents = parents->next; @@ -486,7 +443,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst) strbuf_add_unique_abbrev(dst, cmit_oid, abbrev); if (suffix) strbuf_addstr(dst, suffix); - lazy_queue_clear(&queue); + clear_prio_queue(&queue); return; } if (unannotated_cnt) @@ -502,11 +459,11 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst) QSORT(all_matches, match_cnt, compare_pt); if (gave_up_on) { - lazy_queue_put(&queue, gave_up_on); + prio_queue_put(&queue, gave_up_on); seen_commits--; } seen_commits += finish_depth_computation(&queue, &all_matches[0]); - lazy_queue_clear(&queue); + clear_prio_queue(&queue); if (debug) { static int label_width = -1; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index 8846f2376fc2ff..2435e8aeda40ef 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -232,12 +232,13 @@ static void join_revs(struct prio_queue *queue, while ((commit = prio_queue_peek(queue))) { struct commit_list *parents; int still_interesting = !!interesting(queue); - bool get_pending = true; int flags = commit->object.flags & all_mask; if (!still_interesting && extra <= 0) break; + prio_queue_get(queue); + mark_seen(commit, seen_p); if ((flags & all_revs) == all_revs) flags |= UNINTERESTING; @@ -253,14 +254,8 @@ static void join_revs(struct prio_queue *queue, if (mark_seen(p, seen_p) && !still_interesting) extra--; p->object.flags |= flags; - if (get_pending) - prio_queue_replace(queue, p); - else - prio_queue_put(queue, p); - get_pending = false; + prio_queue_put(queue, p); } - if (get_pending) - prio_queue_get(queue); } /* diff --git a/commit.c b/commit.c index fd8723502ed332..976bfc46186714 100644 --- a/commit.c +++ b/commit.c @@ -795,24 +795,17 @@ void commit_list_sort_by_date(struct commit_list **list) struct commit *pop_most_recent_commit(struct prio_queue *queue, unsigned int mark) { - struct commit *ret = prio_queue_peek(queue); - int get_pending = 1; + struct commit *ret = prio_queue_get(queue); struct commit_list *parents = ret->parents; while (parents) { struct commit *commit = parents->item; if (!repo_parse_commit(the_repository, commit) && !(commit->object.flags & mark)) { commit->object.flags |= mark; - if (get_pending) - prio_queue_replace(queue, commit); - else - prio_queue_put(queue, commit); - get_pending = 0; + prio_queue_put(queue, commit); } parents = parents->next; } - if (get_pending) - prio_queue_get(queue); return ret; } diff --git a/prio-queue.c b/prio-queue.c index ead4faf4bbda4a..199775d5afd22a 100644 --- a/prio-queue.c +++ b/prio-queue.c @@ -34,12 +34,48 @@ void clear_prio_queue(struct prio_queue *queue) queue->nr_ = 0; queue->alloc = 0; queue->insertion_ctr = 0; + queue->get_pending = 0; +} + +static void sift_down_root(struct prio_queue *queue) +{ + size_t ix, child; + + /* Push down the one at the root */ + for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) { + child = ix * 2 + 1; /* left */ + if (child + 1 < queue->nr_ && + compare(queue, child, child + 1) >= 0) + child++; /* use right child */ + + if (compare(queue, ix, child) <= 0) + break; + + swap(queue, child, ix); + } +} + +static inline void flush_get(struct prio_queue *queue) +{ + if (!queue->get_pending) + return; + queue->get_pending = 0; + queue->array[0] = queue->array[--queue->nr_]; + sift_down_root(queue); } void prio_queue_put(struct prio_queue *queue, void *thing) { size_t ix, parent; + if (queue->get_pending) { + queue->get_pending = 0; + queue->array[0].ctr = queue->insertion_ctr++; + queue->array[0].data = thing; + sift_down_root(queue); + return; + } + /* Append at the end */ ALLOC_GROW(queue->array, queue->nr_ + 1, queue->alloc); queue->array[queue->nr_].ctr = queue->insertion_ctr++; @@ -58,61 +94,33 @@ void prio_queue_put(struct prio_queue *queue, void *thing) } } -static void sift_down_root(struct prio_queue *queue) -{ - size_t ix, child; - - /* Push down the one at the root */ - for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) { - child = ix * 2 + 1; /* left */ - if (child + 1 < queue->nr_ && - compare(queue, child, child + 1) >= 0) - child++; /* use right child */ - - if (compare(queue, ix, child) <= 0) - break; - - swap(queue, child, ix); - } -} - void *prio_queue_get(struct prio_queue *queue) { - void *result; - - if (!queue->nr_) + if (queue->nr_ <= queue->get_pending) { + queue->nr_ = 0; + queue->get_pending = 0; return NULL; + } if (!queue->compare) return queue->array[--queue->nr_].data; /* LIFO */ - result = queue->array[0].data; - if (!--queue->nr_) - return result; + flush_get(queue); - queue->array[0] = queue->array[queue->nr_]; - sift_down_root(queue); - return result; + queue->get_pending = 1; + return queue->array[0].data; } void *prio_queue_peek(struct prio_queue *queue) { - if (!queue->nr_) + if (queue->nr_ <= queue->get_pending) { + queue->nr_ = 0; + queue->get_pending = 0; return NULL; + } if (!queue->compare) return queue->array[queue->nr_ - 1].data; - return queue->array[0].data; -} -void prio_queue_replace(struct prio_queue *queue, void *thing) -{ - if (!queue->nr_) { - prio_queue_put(queue, thing); - } else if (!queue->compare) { - queue->array[queue->nr_ - 1].ctr = queue->insertion_ctr++; - queue->array[queue->nr_ - 1].data = thing; - } else { - queue->array[0].ctr = queue->insertion_ctr++; - queue->array[0].data = thing; - sift_down_root(queue); - } + flush_get(queue); + + return queue->array[0].data; } diff --git a/prio-queue.h b/prio-queue.h index 7f2aa986b1c7db..570b48e6485b19 100644 --- a/prio-queue.h +++ b/prio-queue.h @@ -30,8 +30,9 @@ struct prio_queue { prio_queue_compare_fn compare; size_t insertion_ctr; void *cb_data; - size_t alloc, nr_; + size_t alloc, nr_; /* use prio_queue_size() for logical count */ struct prio_queue_entry *array; + unsigned get_pending; }; /* @@ -54,22 +55,14 @@ void *prio_queue_peek(struct prio_queue *); static inline size_t prio_queue_size(const struct prio_queue *queue) { - return queue->nr_; + return queue->nr_ - queue->get_pending; } #define prio_queue_for_each(queue, it) \ - for (size_t pq_ix_ = 0; \ + for (size_t pq_ix_ = (queue)->get_pending; \ pq_ix_ < (queue)->nr_ && ((it) = (queue)->array[pq_ix_].data, 1); \ pq_ix_++) -/* - * Replace the "thing" that compares the smallest with a new "thing", - * like prio_queue_get()+prio_queue_put() would do, but in a more - * efficient way. Does the same as prio_queue_put() if the queue is - * empty. - */ -void prio_queue_replace(struct prio_queue *queue, void *thing); - void clear_prio_queue(struct prio_queue *); /* Reverse the LIFO elements */ diff --git a/t/unit-tests/u-prio-queue.c b/t/unit-tests/u-prio-queue.c index 63e58114ae7dd5..af3e0b85988832 100644 --- a/t/unit-tests/u-prio-queue.c +++ b/t/unit-tests/u-prio-queue.c @@ -53,13 +53,13 @@ static void test_prio_queue(int *input, size_t input_size, prio_queue_reverse(&pq); break; case REPLACE: - peek = prio_queue_peek(&pq); + get = prio_queue_get(&pq); cl_assert(i + 1 < input_size); cl_assert(input[i + 1] >= 0); cl_assert(j < result_size); - cl_assert_equal_i(result[j], show(peek)); + cl_assert_equal_i(result[j], show(get)); j++; - prio_queue_replace(&pq, &input[++i]); + prio_queue_put(&pq, &input[++i]); break; default: prio_queue_put(&pq, &input[i]); From 27bba4258b9e4bd7a0313e3bb4cce8aeba268712 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 22 Jun 2026 10:47:54 +0200 Subject: [PATCH 03/11] odb/source: generalize `reprepare()` callback The `reprepare()` callback function can be used to flush caches of a given object source and then prepare it anew. This is for example used when a concurrent process may have written new objects. Ultimately, this can be seen as doing two separate steps: 1. We drop any caches. 2. We prepare the source. We have one callsite in git-grep(1) though that really only want to do (2). This is done by reaching into the "files" backend directly and then calling `odb_source_packed_prepare()`, which of course may not work with alternate backends. We could in theory just call `reprepare()` here, and that would likely not have any significant downside. But this would certainly feel like a code smell. Instead, generalize the `reprepare()` callback to `prepare()` with a flag that optionally instructs the backend to also flush the caches, which allows us to drop the external `odb_source_packed_prepare()` declaration. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/grep.c | 9 +++------ midx.c | 2 +- odb.c | 2 +- odb.h | 8 ++++++++ odb/source-files.c | 9 +++++---- odb/source-inmemory.c | 5 +++-- odb/source-loose.c | 8 +++++--- odb/source-packed.c | 34 ++++++++++++++++------------------ odb/source-packed.h | 9 --------- odb/source.h | 16 +++++++++------- packfile.c | 2 +- 11 files changed, 52 insertions(+), 52 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 8080d1bf5ec2b4..7361bf071ed162 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -25,12 +25,11 @@ #include "setup.h" #include "submodule.h" #include "submodule-config.h" -#include "object-file.h" #include "object-name.h" #include "odb.h" +#include "odb/source.h" #include "oid-array.h" #include "oidset.h" -#include "packfile.h" #include "pager.h" #include "path.h" #include "promisor-remote.h" @@ -1361,10 +1360,8 @@ int cmd_grep(int argc, struct odb_source *source; odb_prepare_alternates(the_repository->objects); - for (source = the_repository->objects->sources; source; source = source->next) { - struct odb_source_files *files = odb_source_files_downcast(source); - odb_source_packed_prepare(files->packed); - } + for (source = the_repository->objects->sources; source; source = source->next) + odb_source_prepare(source, 0); } start_threads(&opt); diff --git a/midx.c b/midx.c index cc6b94f9dd4fba..76c3f92cc374b8 100644 --- a/midx.c +++ b/midx.c @@ -101,7 +101,7 @@ static int midx_read_object_offsets(const unsigned char *chunk_start, struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source) { - odb_source_packed_prepare(source); + odb_source_prepare(&source->base, 0); return source->midx; } diff --git a/odb.c b/odb.c index 965ef68e4eca22..7b45390e129680 100644 --- a/odb.c +++ b/odb.c @@ -1086,7 +1086,7 @@ void odb_reprepare(struct object_database *o) odb_prepare_alternates(o); for (source = o->sources; source; source = source->next) - odb_source_reprepare(source); + odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES); o->object_count_valid = 0; diff --git a/odb.h b/odb.h index 0030467a52a4e5..c14c9030e47648 100644 --- a/odb.h +++ b/odb.h @@ -124,6 +124,14 @@ void odb_free(struct object_database *o); */ void odb_close(struct object_database *o); +enum odb_prepare_flags { + /* + * Flush caches, reload alternates and then re-prepare each object + * source so that new objects may become accessible. + */ + ODB_PREPARE_FLUSH_CACHES = (1 << 0), +}; + /* * Clear caches, reload alternates and then reload object sources so that new * objects may become accessible. diff --git a/odb/source-files.c b/odb/source-files.c index 3bc6419dd7e2f9..ad9e0b52f9134c 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -41,11 +41,12 @@ static void odb_source_files_close(struct odb_source *source) odb_source_close(&files->packed->base); } -static void odb_source_files_reprepare(struct odb_source *source) +static void odb_source_files_prepare(struct odb_source *source, + enum odb_prepare_flags flags) { struct odb_source_files *files = odb_source_files_downcast(source); - odb_source_reprepare(&files->loose->base); - odb_source_reprepare(&files->packed->base); + odb_source_prepare(&files->loose->base, flags); + odb_source_prepare(&files->packed->base, flags); } static int odb_source_files_read_object_info(struct odb_source *source, @@ -273,7 +274,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, files->base.free = odb_source_files_free; files->base.close = odb_source_files_close; - files->base.reprepare = odb_source_files_reprepare; + files->base.prepare = odb_source_files_prepare; files->base.read_object_info = odb_source_files_read_object_info; files->base.read_object_stream = odb_source_files_read_object_stream; files->base.for_each_object = odb_source_files_for_each_object; diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e004566d768b01..cc5e9e62cbb5e7 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -325,7 +325,8 @@ static void odb_source_inmemory_close(struct odb_source *source UNUSED) { } -static void odb_source_inmemory_reprepare(struct odb_source *source UNUSED) +static void odb_source_inmemory_prepare(struct odb_source *source UNUSED, + enum odb_prepare_flags flags UNUSED) { } @@ -365,7 +366,7 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb) source->base.free = odb_source_inmemory_free; source->base.close = odb_source_inmemory_close; - source->base.reprepare = odb_source_inmemory_reprepare; + source->base.prepare = odb_source_inmemory_prepare; source->base.read_object_info = odb_source_inmemory_read_object_info; source->base.read_object_stream = odb_source_inmemory_read_object_stream; source->base.for_each_object = odb_source_inmemory_for_each_object; diff --git a/odb/source-loose.c b/odb/source-loose.c index 7d7ea2fb842537..af46316e35d60b 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -672,10 +672,12 @@ static void odb_source_loose_clear_cache(struct odb_source_loose *loose) sizeof(loose->subdir_seen)); } -static void odb_source_loose_reprepare(struct odb_source *source) +static void odb_source_loose_prepare(struct odb_source *source, + enum odb_prepare_flags flags) { struct odb_source_loose *loose = odb_source_loose_downcast(source); - odb_source_loose_clear_cache(loose); + if (flags & ODB_PREPARE_FLUSH_CACHES) + odb_source_loose_clear_cache(loose); } static void odb_source_loose_close(struct odb_source *source UNUSED) @@ -716,7 +718,7 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, loose->base.free = odb_source_loose_free; loose->base.close = odb_source_loose_close; - loose->base.reprepare = odb_source_loose_reprepare; + loose->base.prepare = odb_source_loose_prepare; loose->base.read_object_info = odb_source_loose_read_object_info; loose->base.read_object_stream = odb_source_loose_read_object_stream; loose->base.for_each_object = odb_source_loose_for_each_object; diff --git a/odb/source-packed.c b/odb/source-packed.c index 42c28fba0e34b2..fa5a072488d5c7 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -15,7 +15,7 @@ static int find_pack_entry(struct odb_source_packed *store, { struct packfile_list_entry *l; - odb_source_packed_prepare(store); + odb_source_prepare(&store->base, 0); if (store->midx && fill_midx_entry(store->midx, oid, e)) return 1; @@ -47,7 +47,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source, * been added since the last time we have prepared the packfile store. */ if (flags & OBJECT_INFO_SECOND_READ) - odb_source_reprepare(source); + odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES); if (!find_pack_entry(packed, oid, &e)) return 1; @@ -668,27 +668,25 @@ static int sort_pack(const struct packfile_list_entry *a, return -1; } -void odb_source_packed_prepare(struct odb_source_packed *source) +static void odb_source_packed_prepare(struct odb_source *source, + enum odb_prepare_flags flags) { - if (source->initialized) + struct odb_source_packed *packed = odb_source_packed_downcast(source); + + if (flags & ODB_PREPARE_FLUSH_CACHES) + packed->initialized = false; + if (packed->initialized) return; - prepare_multi_pack_index_one(source); - prepare_packed_git_one(source); + prepare_multi_pack_index_one(packed); + prepare_packed_git_one(packed); - sort_packs(&source->packs.head, sort_pack); - for (struct packfile_list_entry *e = source->packs.head; e; e = e->next) + sort_packs(&packed->packs.head, sort_pack); + for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) if (!e->next) - source->packs.tail = e; + packed->packs.tail = e; - source->initialized = true; -} - -static void odb_source_packed_reprepare(struct odb_source *source) -{ - struct odb_source_packed *packed = odb_source_packed_downcast(source); - packed->initialized = false; - odb_source_packed_prepare(packed); + packed->initialized = true; } static void odb_source_packed_reparent(const char *name UNUSED, @@ -744,7 +742,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, packed->base.free = odb_source_packed_free; packed->base.close = odb_source_packed_close; - packed->base.reprepare = odb_source_packed_reprepare; + packed->base.prepare = odb_source_packed_prepare; packed->base.read_object_info = odb_source_packed_read_object_info; packed->base.read_object_stream = odb_source_packed_read_object_stream; packed->base.for_each_object = odb_source_packed_for_each_object; diff --git a/odb/source-packed.h b/odb/source-packed.h index 88994098c18e7d..d5230ac68c1297 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -82,13 +82,4 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so return container_of(source, struct odb_source_packed, base); } -/* - * Prepare the source by loading packfiles and multi-pack indices for - * all alternates. This becomes a no-op if the source is already prepared. - * - * It shouldn't typically be necessary to call this function directly, as - * functions that access the source know to prepare it. - */ -void odb_source_packed_prepare(struct odb_source_packed *source); - #endif diff --git a/odb/source.h b/odb/source.h index b9a7642b2c3c02..bbf1da3819e0a5 100644 --- a/odb/source.h +++ b/odb/source.h @@ -83,11 +83,12 @@ struct odb_source { void (*close)(struct odb_source *source); /* - * This callback is expected to clear underlying caches of the object - * database source. The function is called when the repository has for - * example just been repacked so that new objects will become visible. + * This callback is expected to prepare the source so that it becomes + * ready for use. It optionally clears underlying caches of the object + * database source. */ - void (*reprepare)(struct odb_source *source); + void (*prepare)(struct odb_source *source, + enum odb_prepare_flags flags); /* * This callback is expected to read object information from the object @@ -308,13 +309,14 @@ static inline void odb_source_close(struct odb_source *source) } /* - * Reprepare the object database source and clear any caches. Depending on the + * Prepare the object database source and clear any caches. Depending on the * backend used this may have the effect that concurrently-written objects * become visible. */ -static inline void odb_source_reprepare(struct odb_source *source) +static inline void odb_source_prepare(struct odb_source *source, + enum odb_prepare_flags flags) { - source->reprepare(source); + source->prepare(source, flags); } /* diff --git a/packfile.c b/packfile.c index 59cee7925daf0b..d78fae981a8ad8 100644 --- a/packfile.c +++ b/packfile.c @@ -855,7 +855,7 @@ void for_each_file_in_pack_dir(const char *objdir, struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store) { - odb_source_packed_prepare(store); + odb_source_prepare(&store->base, 0); if (store->midx) { struct multi_pack_index *m = store->midx; From b7fe8f06728f4d39d9b84454588185b384695902 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 22 Jun 2026 10:47:55 +0200 Subject: [PATCH 04/11] odb: introduce `odb_prepare()` Introduce `odb_prepare()` as a simple wrapper to prepare alternates and then prepare each individual source. Adapt git-grep(1) to use it. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/grep.c | 9 ++------- odb.c | 18 ++++++++++++------ odb.h | 8 ++++++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/builtin/grep.c b/builtin/grep.c index 7361bf071ed162..a7252d56a11ee0 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1356,13 +1356,8 @@ int cmd_grep(int argc, if (recurse_submodules) repo_read_gitmodules(the_repository, 1); - if (startup_info->have_repository) { - struct odb_source *source; - - odb_prepare_alternates(the_repository->objects); - for (source = the_repository->objects->sources; source; source = source->next) - odb_source_prepare(source, 0); - } + if (startup_info->have_repository) + odb_prepare(the_repository->objects, 0); start_threads(&opt); } else { diff --git a/odb.c b/odb.c index 7b45390e129680..11414c49a83f5a 100644 --- a/odb.c +++ b/odb.c @@ -1070,7 +1070,7 @@ void odb_free(struct object_database *o) free(o); } -void odb_reprepare(struct object_database *o) +void odb_prepare(struct object_database *o, enum odb_prepare_flags flags) { struct odb_source *source; @@ -1082,13 +1082,19 @@ void odb_reprepare(struct object_database *o) * the linked list, so existing odbs will continue to exist for * the lifetime of the process. */ - o->loaded_alternates = 0; - odb_prepare_alternates(o); + if (flags & ODB_PREPARE_FLUSH_CACHES) { + o->loaded_alternates = 0; + o->object_count_valid = 0; + } + odb_prepare_alternates(o); for (source = o->sources; source; source = source->next) - odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES); - - o->object_count_valid = 0; + odb_source_prepare(source, flags); obj_read_unlock(); } + +void odb_reprepare(struct object_database *o) +{ + odb_prepare(o, ODB_PREPARE_FLUSH_CACHES); +} diff --git a/odb.h b/odb.h index c14c9030e47648..b1c0f3767b2e68 100644 --- a/odb.h +++ b/odb.h @@ -133,9 +133,13 @@ enum odb_prepare_flags { }; /* - * Clear caches, reload alternates and then reload object sources so that new - * objects may become accessible. + * Prepare the object database for use. Calling this function is generally not + * needed, but can be useful in case the caller wants to pre-open individual + * sources. */ +void odb_prepare(struct object_database *o, enum odb_prepare_flags flags); + +/* Equivalent to `odb_prepare(o, ODB_PREPARE_FLUSH_CACHES)`. */ void odb_reprepare(struct object_database *o); /* From 5dea8b690b50a4f4d11ddc8f2f6cd24a816102ad Mon Sep 17 00:00:00 2001 From: Antonio De Stefani Date: Wed, 24 Jun 2026 11:36:18 +0200 Subject: [PATCH 05/11] gpg-interface: fix strip_cr_before_lf to only remove CR before LF c4adea82c5 (Convert CR/LF to LF in tag signatures, 2008-07-11) introduced CR stripping for GPG output on Windows, but intentionally stripped all CR characters unconditionally to "keep the code simpler", even though only CRLF sequences (Windows line endings) needed to be normalized. Later 2f47eae2a1 (Split GPG interface into its own helper library, 2011-09-07) moved the code into gpg-interface.c, and 29b315778e (ssh signing: add ssh key format and signing code, 2021-09-10) extracted it into the remove_cr_after() helper when adding SSH signing support, while noticing that it unconditionally strips all CRs, leaving a NEEDSWORK comment. Fix the loop to skip CR only when immediately followed by LF, keeping lone trailing CR characters intact. Rename the function to strip_cr_before_lf to reflect its corrected behavior, and update both call sites and their comments accordingly. Signed-off-by: Antonio De Stefani Signed-off-by: Junio C Hamano --- gpg-interface.c | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/gpg-interface.c b/gpg-interface.c index dafd5371fa806d..95abf1ef4e1a0c 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -990,21 +990,18 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, return ret; } -/* - * Strip CR from the line endings, in case we are on Windows. - * NEEDSWORK: make it trim only CRs before LFs and rename - */ -static void remove_cr_after(struct strbuf *buffer, size_t offset) +/* Strip CR before LF from the line endings, in case we are on Windows. */ +static void strip_cr_before_lf(struct strbuf *buffer, size_t offset) { size_t i, j; for (i = j = offset; i < buffer->len; i++) { - if (buffer->buf[i] != '\r') { - if (i != j) - buffer->buf[j] = buffer->buf[i]; - j++; - } + if (buffer->buf[i] == '\r' && + i + 1 < buffer->len && buffer->buf[i + 1] == '\n') + continue; + buffer->buf[j++] = buffer->buf[i]; } + strbuf_setlen(buffer, j); } @@ -1049,8 +1046,8 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, } strbuf_release(&gpg_status); - /* Strip CR from the line endings, in case we are on Windows. */ - remove_cr_after(signature, bottom); + /* Strip CR before LF from the line endings, in case we are on Windows. */ + strip_cr_before_lf(signature, bottom); return 0; } @@ -1136,8 +1133,8 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature, ssh_signature_filename.buf); goto out; } - /* Strip CR from the line endings, in case we are on Windows. */ - remove_cr_after(signature, bottom); + /* Strip CR before LF from the line endings, in case we are on Windows. */ + strip_cr_before_lf(signature, bottom); out: if (key_file) From 2faf0f0256cbf4cef447202276ca57d36e1d9380 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 27 Jun 2026 18:02:24 +0000 Subject: [PATCH 06/11] branch: suggest / on upstream slip When setting the upstream of the current branch to the 'main' branch of the remote 'origin', i.e., $ git branch --set-upstream-to origin/main it is easy to mistakenly write $ git branch --set-upstream-to origin main That is parsed as a request to set the upstream of the local branch 'main' to 'origin'. When 'main' does not exist, the command dies with: fatal: branch 'main' does not exist pointing at a branch the user never meant to name. When 'main' does exist, it instead dies with: fatal: the requested upstream branch 'origin' does not exist leaving the user equally puzzled. When the operated-on branch is missing and '/' names a real remote-tracking ref, suggest the intended form: $ git branch --set-upstream-to=origin/main The suggestion is gated on '/' existing so it only appears when a slipped slash is the likely explanation. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/branch.c | 32 ++++++++++++++++++++++++++++++++ t/t3200-branch.sh | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/builtin/branch.c b/builtin/branch.c index 1572a4f9ef2ab6..dede60d27b69ea 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -706,6 +706,29 @@ static int edit_branch_description(const char *branch_name) return 0; } +static void die_if_upstream_looks_like_remote(const char *new_upstream, const char *branch_name) +{ + struct strbuf remote_ref = STRBUF_INIT; + int code; + + if (strchr(new_upstream, '/') || + !remote_is_configured(remote_get(new_upstream), 0)) + return; + + strbuf_addf(&remote_ref, "refs/remotes/%s/%s", new_upstream, branch_name); + if (!refs_ref_exists(get_main_ref_store(the_repository), remote_ref.buf)) { + strbuf_release(&remote_ref); + return; + } + + code = die_message(_("--set-upstream-to takes a single / argument")); + advise_if_enabled(ADVICE_SET_UPSTREAM_FAILURE, + _("Did you mean to use: git branch --set-upstream-to=%s/%s?"), + new_upstream, branch_name); + strbuf_release(&remote_ref); + exit(code); +} + int cmd_branch(int argc, const char **argv, const char *prefix, @@ -957,6 +980,15 @@ int cmd_branch(int argc, if (!refs_ref_exists(get_main_ref_store(the_repository), branch->refname)) { if (!argc || branch_checked_out(branch->refname)) die(_("no commit on branch '%s' yet"), branch->name); + /* + * Check the advice up front to avoid the ref + * lookups when the hint is off. The helper still + * calls advise_if_enabled() so the hint carries the + * standard "disable this message" instructions. + */ + if (argc == 1 && + advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) + die_if_upstream_looks_like_remote(new_upstream, argv[0]); die(_("branch '%s' does not exist"), branch->name); } diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index e7829c2c4bfdc3..e2682a83a0c8d1 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1022,6 +1022,44 @@ test_expect_success '--set-upstream-to fails on a missing dst branch' ' test_cmp expect err ' +test_expect_success '--set-upstream-to suggests / on slip' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + git update-ref refs/remotes/slip-remote/slip-feature HEAD && + test_must_fail git branch --set-upstream-to slip-remote slip-feature 2>err && + test_grep "takes a single / argument" err && + test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip-feature?" err && + test_must_fail git -c advice.setUpstreamFailure=false \ + branch --set-upstream-to slip-remote slip-feature 2>err && + test_grep ! "Did you mean" err +' + +test_expect_success '--set-upstream-to does not suggest when no matching remote ref' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + test_must_fail git branch --set-upstream-to slip-remote no-such-branch 2>err && + test_grep "branch ${SQ}no-such-branch${SQ} does not exist" err && + test_grep ! "Did you mean" err +' + +test_expect_success '--set-upstream-to to a local branch is not mistaken for a slip' ' + git branch slip-local-upstream && + git branch slip-local-target && + git branch --set-upstream-to=slip-local-upstream slip-local-target 2>err && + test_grep ! "Did you mean" err && + echo refs/heads/slip-local-upstream >expect && + git config branch.slip-local-target.merge >actual && + test_cmp expect actual +' + +test_expect_success '--set-upstream-to slip suggestion keeps a slashed branch name' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + git update-ref refs/remotes/slip-remote/slip/feature HEAD && + test_must_fail git branch --set-upstream-to slip-remote slip/feature 2>err && + test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip/feature?" err +' + test_expect_success '--set-upstream-to fails on a missing src branch' ' test_must_fail git branch --set-upstream-to does-not-exist main 2>err && test_grep "the requested upstream branch '"'"'does-not-exist'"'"' does not exist" err From a85a43c4802c124a504c42c63129a97d7f8346c1 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 27 Jun 2026 18:02:25 +0000 Subject: [PATCH 07/11] push: suggest for a slash slip When pushing the 'main' branch to the remote 'origin', i.e., $ git push origin main it is easy to mistakenly write $ git push origin/main That is parsed as the repository to push to, and since 'origin/main' is neither a configured remote nor a path it dies with: fatal: 'origin/main' does not appear to be a git repository Often 'origin/main' does not exist as a repository, so the command fails without doing any harm, but it gives no hint that a space was meant instead of a slash and can leave the user puzzled. When the argument is not an existing path or configured remote but its part before the first slash names one, suggest the intended ' ' form: $ git push origin main The suggestion is shown as advice so it can be silenced with advice.pushRepoLooksLikeRef. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/config/advice.adoc | 5 +++++ advice.c | 1 + advice.h | 1 + builtin/push.c | 37 +++++++++++++++++++++++++++++++- t/t5529-push-errors.sh | 31 ++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 257db58918179a..fa77a5110eb10d 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -90,6 +90,11 @@ all advice messages. Shown when linkgit:git-push[1] rejects a forced update of a branch when its remote-tracking ref has updates that we do not have locally. + pushRepoLooksLikeRef:: + Shown when the repository given to linkgit:git-push[1] is not + a configured remote but looks like a `/` ref, + suggesting that the remote and branch be given as separate + arguments. pushUnqualifiedRefname:: Shown when linkgit:git-push[1] gives up trying to guess based on the source and destination refs what diff --git a/advice.c b/advice.c index 0018501b7bc103..63bf8b0c5f0481 100644 --- a/advice.c +++ b/advice.c @@ -69,6 +69,7 @@ static struct { [ADVICE_PUSH_NON_FF_CURRENT] = { "pushNonFFCurrent" }, [ADVICE_PUSH_NON_FF_MATCHING] = { "pushNonFFMatching" }, [ADVICE_PUSH_REF_NEEDS_UPDATE] = { "pushRefNeedsUpdate" }, + [ADVICE_PUSH_REPO_LOOKS_LIKE_REF] = { "pushRepoLooksLikeRef" }, [ADVICE_PUSH_UNQUALIFIED_REF_NAME] = { "pushUnqualifiedRefName" }, [ADVICE_PUSH_UPDATE_REJECTED] = { "pushUpdateRejected" }, [ADVICE_PUSH_UPDATE_REJECTED_ALIAS] = { "pushNonFastForward" }, /* backwards compatibility */ diff --git a/advice.h b/advice.h index 8def28068861df..66f6cd6a772d8c 100644 --- a/advice.h +++ b/advice.h @@ -36,6 +36,7 @@ enum advice_type { ADVICE_PUSH_NON_FF_CURRENT, ADVICE_PUSH_NON_FF_MATCHING, ADVICE_PUSH_REF_NEEDS_UPDATE, + ADVICE_PUSH_REPO_LOOKS_LIKE_REF, ADVICE_PUSH_UNQUALIFIED_REF_NAME, ADVICE_PUSH_UPDATE_REJECTED, ADVICE_PUSH_UPDATE_REJECTED_ALIAS, diff --git a/builtin/push.c b/builtin/push.c index 6021b71d668455..1b2ad3b8df7c55 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -8,6 +8,7 @@ #include "advice.h" #include "branch.h" #include "config.h" +#include "dir.h" #include "environment.h" #include "gettext.h" #include "hex.h" @@ -662,6 +663,29 @@ static int push_multiple(struct string_list *list, return result; } +static void die_if_repo_looks_like_ref(const char *repo) +{ + const char *slash = strchr(repo, '/'); + struct strbuf name = STRBUF_INIT; + int code; + + if (!slash || !slash[1] || file_exists(repo)) + return; + + strbuf_add(&name, repo, slash - repo); + if (!remote_is_configured(remote_get(name.buf), 0)) { + strbuf_release(&name); + return; + } + + code = die_message(_("'%s' is not a valid push target"), repo); + advise_if_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF, + _("Did you mean to use: git push %s %s?"), + name.buf, slash + 1); + strbuf_release(&name); + exit(code); +} + int cmd_push(int argc, const char **argv, const char *prefix, @@ -744,6 +768,17 @@ int cmd_push(int argc, if (repo) { if (!add_remote_or_group(repo, &remote_group)) { + struct remote *r; + + /* + * Check the advice up front to avoid the remote + * lookup when the hint is off. The helper still + * calls advise_if_enabled() so the hint carries the + * standard "disable this message" instructions. + */ + if (advice_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF)) + die_if_repo_looks_like_ref(repo); + /* * Not a configured remote name or group name. * Try treating it as a direct URL or path, e.g. @@ -753,7 +788,7 @@ int cmd_push(int argc, * from the URL so the loop below can handle it * identically to a named remote. */ - struct remote *r = pushremote_get(repo); + r = pushremote_get(repo); if (!r) die(_("bad repository '%s'"), repo); string_list_append(&remote_group, r->name); diff --git a/t/t5529-push-errors.sh b/t/t5529-push-errors.sh index 80b06a0cd2886d..2294645902c90e 100755 --- a/t/t5529-push-errors.sh +++ b/t/t5529-push-errors.sh @@ -54,6 +54,37 @@ test_expect_success 'detect empty remote with targeted refspec' ' grep "fatal: bad repository ${SQ}${SQ}" stderr ' +test_expect_success 'suggest for a / slip' ' + test_must_fail git push origin/main 2>stderr && + test_grep "${SQ}origin/main${SQ} is not a valid push target" stderr && + test_grep "hint: Did you mean to use: git push origin main?" stderr && + test_must_fail git -c advice.pushRepoLooksLikeRef=false push origin/main 2>stderr && + test_grep ! "Did you mean" stderr +' + +test_expect_success 'suggest when the branch has slashes' ' + test_must_fail git push origin/feature/x 2>stderr && + test_grep "hint: Did you mean to use: git push origin feature/x?" stderr +' + +test_expect_success 'no suggestion when prefix is not a configured remote' ' + test_must_fail git push not-a-remote/main 2>stderr && + test_grep ! "Did you mean" stderr +' + +test_expect_success 'no suggestion for a trailing slash with no branch' ' + test_must_fail git push origin/ 2>stderr && + test_grep ! "Did you mean" stderr +' + +test_expect_success 'no suggestion when the argument is an existing path' ' + test_when_finished "rm -rf origin" && + git init --bare origin/main && + git push origin/main HEAD:refs/heads/pushed 2>stderr && + test_grep ! "Did you mean" stderr && + git -C origin/main rev-parse --verify refs/heads/pushed +' + test_expect_success 'detect ambiguous refs early' ' git branch foo && git tag foo && From c6fb3b9c3ec6ca16ba2fbed154b41b22e5a088f0 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 28 Jun 2026 05:03:14 -0400 Subject: [PATCH 08/11] reftable: fix unlikely leak on API error If the reftable writer sees a bogus block size, we return with REFTABLE_API_ERROR, leaking the reftable_writer struct we previously allocated. Originally this case was a BUG(), but it became a regular return in 445f9f4f35 (reftable: stop using `BUG()` in trivial cases, 2025-02-18). We could obviously fix it by calling "reftable_free(wp)". But we can observe that we never use the allocated "wp" until after we've validated the input options. So let's just bump the allocation down. That fixes the leak, and I think makes the flow of the function more logical (we validate our inputs before doing any work). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- reftable/writer.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reftable/writer.c b/reftable/writer.c index 0133b649759bcf..1bd4aa388beb16 100644 --- a/reftable/writer.c +++ b/reftable/writer.c @@ -152,16 +152,16 @@ int reftable_writer_new(struct reftable_writer **out, struct reftable_write_options opts = {0}; struct reftable_writer *wp; - wp = reftable_calloc(1, sizeof(*wp)); - if (!wp) - return REFTABLE_OUT_OF_MEMORY_ERROR; - if (_opts) opts = *_opts; options_set_defaults(&opts); if (opts.block_size >= (1 << 24)) return REFTABLE_API_ERROR; + wp = reftable_calloc(1, sizeof(*wp)); + if (!wp) + return REFTABLE_OUT_OF_MEMORY_ERROR; + reftable_buf_init(&wp->block_writer_data.last_key); reftable_buf_init(&wp->last_key); reftable_buf_init(&wp->scratch); From cb77d1f30882b2662dc097c802af95e721604ae0 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 30 Jun 2026 02:41:59 -0400 Subject: [PATCH 09/11] t: move LSan errors from stdout to stderr When we find LSan errors, we dump them via "say_color", which goes to stdout. This is mostly harmless, since stdout and stderr tend to go to the same place (either the user's terminal, or to the ".out" file with --verbose-log). But when running under a TAP harness like prove, they are split and stdout is interpreted as TAP output. Historically even this was fine, as the extra lines on stdout would be ignored. But since 389c83025d (t: let prove fail when parsing invalid TAP output, 2026-06-04) we instruct the TAP reader to complain, and a leaking test will result in complaints like this (this is a real leak which we have yet to fix): $ GIT_TEST_COMMIT_GRAPH=1 make SANITIZE=leak test [...] Test Summary Report ------------------- t4014-format-patch.sh (Wstat: 256 (exited 1) Tests: 226 Failed: 30) Failed tests: 197-226 Non-zero exit status: 1 Parse errors: Unknown TAP token: "" Unknown TAP token: "=================================================================" Unknown TAP token: "==git==3693658==ERROR: LeakSanitizer: detected memory leaks" Unknown TAP token: "" Unknown TAP token: "Direct leak of 200 byte(s) in 1 object(s) allocated from:" Displayed the first 5 of 1531 TAP syntax errors. Re-run prove with the -p option to see them all. You still see the failing tests, so it's mostly just an annoyance. We can fix it by redirecting to stderr (actually descriptor 4, which is our verbose-respecting variant). I confirmed manually that the output still appears with --verbose-log, and even with a single-test "-i --verbose-only=197" going to the terminal. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/test-lib.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/test-lib.sh b/t/test-lib.sh index 70fd3e9bafb800..63db941bb7bc2c 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1190,14 +1190,14 @@ check_test_results_san_file_ () { then return fi && - say_color error "$(cat "$TEST_RESULTS_SAN_FILE".*)" && + say_color >&4 error "$(cat "$TEST_RESULTS_SAN_FILE".*)" && if test "$test_failure" = 0 then - say "Our logs revealed a memory leak, exit non-zero!" && + say >&4 "Our logs revealed a memory leak, exit non-zero!" && invert_exit_code=t else - say "Our logs revealed a memory leak..." + say >&4 "Our logs revealed a memory leak..." fi } From 973a0373ffd1b3f1f149bfac88c0bef3e6a0afd0 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 30 Jun 2026 02:43:01 -0400 Subject: [PATCH 10/11] format-patch: fix leak of rev_info in prepare_bases() In prepare_bases() we do a custom revision walk, separate from the main format-patch walk. After we finish, we fail to call release_revisions(), possibly leaking its contents. We failed to notice it so far because the revision machinery doesn't always allocate. But at least one case can trigger the leak: if a commit graph is present, then the topo-walk allocates revs.topo_walk_info and some associated data structures. You can see it in the test suite by running: make SANITIZE=leak cd t GIT_TEST_COMMIT_GRAPH=1 ./t4014-format-patch.sh which yields many entries like: ==git==3687620==ERROR: LeakSanitizer: detected memory leaks Direct leak of 200 byte(s) in 1 object(s) allocated from: #0 0x7f4ccba185cb in malloc ../../../../src/libsanitizer/lsan/lsan_interceptors.cpp:74 #1 0x55cd452cdd0b in do_xmalloc wrapper.c:55 #2 0x55cd452cdd9d in xmalloc wrapper.c:76 #3 0x55cd45255473 in init_topo_walk revision.c:3845 #4 0x55cd45255bef in prepare_revision_walk revision.c:4017 #5 0x55cd44ffec40 in prepare_bases builtin/log.c:1872 #6 0x55cd450010ec in cmd_format_patch builtin/log.c:2439 The un-released rev_info has been there since the code was added in fa2ab86d18 (format-patch: add '--base' option to record base tree info, 2016-04-26), but back then we didn't even have a way to release rev_info resources! The actual leak probably started around f0d9cc4196 (revision.c: begin refactoring --topo-order logic, 2018-11-01), but it's hard to bisect because there were so many other unrelated leaks back then. So I'm not sure exactly when the leak started beyond "long ago", but it is easy-ish to find now (since we've plugged all those other leaks) and the solution is clear. I didn't add a new test since we can demonstrate it with the existing ones, but it does require tweaking a test variable. We might consider ways to get more automatic leak-checking coverage there, but I think it should be done outside of this fix. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- builtin/log.c | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/log.c b/builtin/log.c index 8c0939dd42ada2..baf98b75e11bbe 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -1884,6 +1884,7 @@ static void prepare_bases(struct base_tree_info *bases, bases->nr_patch_id++; } clear_commit_base(&commit_base); + release_revisions(&revs); } static void print_bases(struct base_tree_info *bases, FILE *file) From 55526a18268bbc1ddaf8a6b7850c33d984eac9e9 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 13 Jul 2026 08:27:08 -0700 Subject: [PATCH 11/11] The 2nd batch for Git 2.56 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index fda10a014f85ba..2d01f5cdc0ab74 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -27,6 +27,10 @@ UI, Workflows & Features absolute and relative paths for "gitdir" and "commondir", supported by a new path-formatting helper extracted from "git rev-parse". + * When 'git push origin/main' or 'git branch origin main' is run, the + command is now recognized as a potential typo, and advice has been + added to offer a typo fix. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -74,6 +78,16 @@ Performance, Internal Implementation, Development Support etc. a default limit of at most one reroll per day to give reviewers across different time zones enough time to participate. + * The lazy priority queue optimization pattern (deferring actual removal + in 'prio_queue_get()' to allow get+put fusion) has been folded + directly into 'prio_queue' itself, speeding up commit traversal + workflows and simplifying callers. + + * The 'reprepare()' callback for object database sources has been + generalized into a 'prepare()' callback with an optional flush cache + flag, and a new 'odb_prepare()' wrapper has been introduced to allow + pre-opening object database sources. + Fixes since v2.55 ----------------- @@ -111,3 +125,18 @@ Fixes since v2.55 test_path_is_missing instead of ! grep on a file that shouldn't exist in the conflicted state. (merge eaad121fef sg/t3420-do-not-grep-in-missing-file later to maint). + + * The GPG and SSH signature parsing code has been corrected to strip + carriage return characters only when they immediately precede line + feeds, instead of unconditionally stripping all carriage returns. + (merge 5dea8b690b ad/gpg-strip-cr-before-lf later to maint). + + * A memory leak in the 'reftable_writer_new()' initialization function + has been fixed by delaying the allocation of 'struct reftable_writer' + until after input options are validated. + (merge c6fb3b9c3e jk/reftable-leakfix later to maint). + + * A memory leak in the '--base' handling of 'git format-patch' has been + plugged, and the leak reporting of the test suite when running under a + TAP harness has been improved. + (merge 973a0373ff jk/format-patch-leakfix later to maint).