commit-reach: introduce struct paint_state with per-side counters

Add a paint_state struct for use by paint_down_to_common() that wraps a prio_queue with per-side commit counters. Each non-stale queued commit occupies exactly one counter bucket based on its paint flags: PARENT1-only, PARENT2-only, or both sides (a pending merge-base candidate). The counters are maintained by paint_count_update() which adjusts the appropriate bucket by a signed delta. An exhaustive switch on the paint+stale bits documents all valid flag combinations in one place. Convert paint_down_to_common() to use paint_state. The loop now drains the queue via paint_queue_get() which returns NULL when all counters reach zero, replacing the old pointer-based termination (max_nonstale). This is equivalent behavior -- both conditions detect that no non-stale entries remain. paint_queue_get() uses a "pop first" form: it dequeues a commit, then checks the counters. This means the loop exits one iteration earlier than the old code in some topologies (the popped stale commit is never processed), so a few step counts drop by one. The existing nonstale_queue is left in place for ahead_behind(), though nonstale_queue_put_dedup() and nonstale_queue_get_dedup() became unused and are removed. Signed-off-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristofer Karlsson committed Jul 11, 2026 at 13:27 UTC 351dfe5aa75807a4a9ae7880fd39144a24bcb172
3 files changed +82 -36
Documentation/technical/paint-down-to-common.adoc
+3 -6
@@ -94,15 +94,12 @@ re-enqueued is bounded by the number of flag transitions.
94 Termination
95 -----------
96
97 -The walk uses a `nonstale_queue` wrapper around `prio_queue` that
98 -tracks `max_nonstale`: the lowest-priority non-stale commit enqueued
99 -so far. Once that commit is dequeued, every remaining entry is known
100 -to be STALE and the loop terminates. Specifically, the main loop
97 +The walk tracks the number of commits of each type in the queue
98 +(PARENT1-only, PARENT2-only, pending merge-base). The main loop
99 ends when one of the following conditions holds:
100
101 1. The queue is empty.
104 - 2. `max_nonstale` has been dequeued, meaning the queue only contains
105 - STALE entries.
102 + 2. The queue contains only stale entries.
103 3. Generation cutoff: the dequeued commit's generation is below
104 a caller-supplied `min_generation` threshold.
105 4. Single result: the caller only needs one merge base, one has
commit-reach.c
+76 -27
@@ -79,21 +79,73 @@ static void clear_nonstale_queue(struct nonstale_queue *queue)
79 queue->max_nonstale = NULL;
80 }
81
82 -static void nonstale_queue_put_dedup(struct nonstale_queue *queue,
83 - struct commit *c)
82 +/*
83 + * Priority queue with per-side commit counters for paint_down_to_common().
84 + * Each non-stale queued commit occupies exactly one bucket: PARENT1-only,
85 + * PARENT2-only, or both (a pending merge-base candidate).
86 + */
87 +struct paint_state {
88 + struct prio_queue queue;
89 + size_t parent1_count;
90 + size_t parent2_count;
91 + size_t mb_candidate_count;
92 + int gen_ordered;
93 +};
94 +
95 +static void paint_count_update(struct paint_state *state,
96 + unsigned flags, int delta)
97 {
85 - if (c->object.flags & ENQUEUED)
86 - return;
87 - c->object.flags |= ENQUEUED;
88 - nonstale_queue_put(queue, c);
98 + switch (flags & (PARENT1 | PARENT2 | STALE)) {
99 + case PARENT1:
100 + state->parent1_count += delta;
101 + break;
102 +
103 + case PARENT2:
104 + state->parent2_count += delta;
105 + break;
106 +
107 + case PARENT1 | PARENT2:
108 + state->mb_candidate_count += delta;
109 + break;
110 +
111 + case PARENT1 | PARENT2 | STALE:
112 + break;
113 +
114 + default:
115 + BUG("unexpected paint state");
116 + }
117 +}
118 +
119 +static void paint_queue_put(struct paint_state *state,
120 + struct commit *c, unsigned add_flags)
121 +{
122 + unsigned old_flags = c->object.flags;
123 + c->object.flags |= add_flags;
124 +
125 + if (old_flags & ENQUEUED) {
126 + paint_count_update(state, old_flags, -1);
127 + paint_count_update(state, c->object.flags, 1);
128 + } else {
129 + c->object.flags |= ENQUEUED;
130 + prio_queue_put(&state->queue, c);
131 + paint_count_update(state, c->object.flags, 1);
132 + }
133 }
134
91 -static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue)
135 +static struct commit *paint_queue_get(struct paint_state *state)
136 {
93 - struct commit *commit = nonstale_queue_get(queue);
137 + struct commit *commit = prio_queue_get(&state->queue);
138 +
139 + if (!commit)
140 + return NULL;
141 +
142 + commit->object.flags &= ~ENQUEUED;
143 +
144 + if (!state->parent1_count && !state->parent2_count &&
145 + !state->mb_candidate_count)
146 + return NULL;
147
95 - if (commit)
96 - commit->object.flags &= ~ENQUEUED;
148 + paint_count_update(state, commit->object.flags, -1);
149 return commit;
150 }
151
@@ -109,18 +161,19 @@ static int paint_down_to_common(struct repository *r,
161 enum merge_base_flags mb_flags,
162 struct commit_list **result)
163 {
112 - struct nonstale_queue queue = {
113 - { compare_commits_by_gen_then_commit_date }
164 + struct paint_state state = {
165 + .queue = { compare_commits_by_gen_then_commit_date },
166 + .gen_ordered = 1,
167 };
168 + struct commit *commit;
169 int i;
116 - int gen_ordered = 1;
170 int steps = 0;
171 timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
172 struct commit_list **tail = result;
173
174 if (!min_generation && !corrected_commit_dates_enabled(r)) {
122 - queue.pq.compare = compare_commits_by_commit_date;
123 - gen_ordered = 0;
175 + state.queue.compare = compare_commits_by_commit_date;
176 + state.gen_ordered = 0;
177 }
178
179 one->object.flags |= PARENT1;
@@ -128,15 +181,12 @@ static int paint_down_to_common(struct repository *r,
181 commit_list_append(one, result);
182 return 0;
183 }
131 - nonstale_queue_put_dedup(&queue, one);
184 + paint_queue_put(&state, one, 0);
185
133 - for (i = 0; i < n; i++) {
134 - twos[i]->object.flags |= PARENT2;
135 - nonstale_queue_put_dedup(&queue, twos[i]);
136 - }
186 + for (i = 0; i < n; i++)
187 + paint_queue_put(&state, twos[i], PARENT2);
188
138 - while (queue.max_nonstale) {
139 - struct commit *commit = nonstale_queue_get_dedup(&queue);
189 + while ((commit = paint_queue_get(&state))) {
190 struct commit_list *parents;
191 int flags;
192 timestamp_t generation = commit_graph_generation(commit);
@@ -162,7 +212,7 @@ static int paint_down_to_common(struct repository *r,
212 * descendant of this one.
213 */
214 if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
165 - gen_ordered &&
215 + state.gen_ordered &&
216 generation < GENERATION_NUMBER_INFINITY)
217 break;
218 }
@@ -176,7 +226,7 @@ static int paint_down_to_common(struct repository *r,
226 if ((p->object.flags & flags) == flags)
227 continue;
228 if (repo_parse_commit(r, p)) {
179 - clear_nonstale_queue(&queue);
229 + clear_prio_queue(&state.queue);
230 commit_list_free(*result);
231 *result = NULL;
232 /*
@@ -191,12 +241,11 @@ static int paint_down_to_common(struct repository *r,
241 return error(_("could not parse commit %s"),
242 oid_to_hex(&p->object.oid));
243 }
194 - p->object.flags |= flags;
195 - nonstale_queue_put_dedup(&queue, p);
244 + paint_queue_put(&state, p, flags);
245 }
246 }
247
199 - clear_nonstale_queue(&queue);
248 + clear_prio_queue(&state.queue);
249 trace2_data_intmax("paint_down_to_common", r,
250 "steps", steps);
251 commit_list_sort_by_date(result);
t/t6600-test-reach.sh
+3 -3
@@ -366,7 +366,7 @@ test_expect_success 'get_merge_bases_many:pending-stale' '
366 git rev-parse ps-B
367 } >expect &&
368 test_all_modes get_merge_bases_many &&
369 - test_paint_down_steps 6 6 6 6
369 + test_paint_down_steps 5 5 5 5
370 '
371
372 test_expect_success 'get_merge_bases_many:infinity-both-sides' '
@@ -381,7 +381,7 @@ test_expect_success 'get_merge_bases_many:infinity-both-sides' '
381 git rev-parse pi-B
382 } >expect &&
383 test_all_modes get_merge_bases_many &&
384 - test_paint_down_steps 5 5 5 5
384 + test_paint_down_steps 5 4 5 5
385 '
386
387 test_expect_success 'setup mixed finite/INFINITY topology' '
@@ -438,7 +438,7 @@ test_expect_success 'merge-base --all with clock skew and redundant ancestor (si
438 >input &&
439 git rev-parse se2-MB1 >expect &&
440 run_all_modes git merge-base --all se2-A se2-B &&
441 - test_paint_down_steps 8 7 8 8
441 + test_paint_down_steps 8 6 8 8
442 '
443
444 test_expect_success 'reduce_heads' '