revision: use priority queue for non-limited streaming walks

The streaming (non-limited) walk in get_revision_1() inserts newly discovered parent commits into a date-sorted queue via commit_list_insert_by_date(), which scans the linked list to find the insertion point -- O(w) per insert, where w is the width of the active walk frontier. Replace this with an O(log w) priority queue. Add a commit_queue field to rev_info alongside the existing commits linked list. The two representations are mutually exclusive: setup and external callers that need list access use the linked list, then get_revision_1() lazily drains it into the priority queue on first call. Add a REV_WALK_NO_WALK enum value to distinguish the no_walk case (which still uses the commit list) from the streaming case. The conversion function rev_info_commit_list_to_queue() is public so callers that know they will iterate can convert early. Combined with the limit_list() priority queue change already in master, this eliminates all O(w) sorted linked-list insertion from the revision walk machinery. Signed-off-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristofer Karlsson committed May 27, 2026 at 15:50 UTC dd4bc01c0a8fc871a68a5027ed5ac953fa47fc6e
4 files changed +41 -41
commit.c
-13
@@ -729,19 +729,6 @@ void commit_list_free(struct commit_list *list)
729 pop_commit(&list);
730 }
731
732 -struct commit_list * commit_list_insert_by_date(struct commit *item, struct commit_list **list)
733 -{
734 - struct commit_list **pp = list;
735 - struct commit_list *p;
736 - while ((p = *pp) != NULL) {
737 - if (p->item->date < item->date) {
738 - break;
739 - }
740 - pp = &p->next;
741 - }
742 - return commit_list_insert(item, pp);
743 -}
744 -
732 static int commit_list_compare_by_date(const struct commit_list *a,
733 const struct commit_list *b)
734 {
commit.h
-2
@@ -191,8 +191,6 @@ int commit_list_contains(struct commit *item,
191 struct commit_list **commit_list_append(struct commit *commit,
192 struct commit_list **next);
193 unsigned commit_list_count(const struct commit_list *l);
194 -struct commit_list *commit_list_insert_by_date(struct commit *item,
195 - struct commit_list **list);
194 void commit_list_sort_by_date(struct commit_list **list);
195
196 /* Shallow copy of the input list */
revision.c
+30 -25
@@ -1116,7 +1116,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)
1116 }
1117
1118 static int process_parents(struct rev_info *revs, struct commit *commit,
1119 - struct commit_list **list, struct prio_queue *queue)
1119 + struct prio_queue *queue)
1120 {
1121 struct commit_list *parent = commit->parents;
1122 unsigned pass_flags;
@@ -1158,8 +1158,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1158 if (p->object.flags & SEEN)
1159 continue;
1160 p->object.flags |= (SEEN | NOT_USER_GIVEN);
1161 - if (list)
1162 - commit_list_insert_by_date(p, list);
1161 if (queue)
1162 prio_queue_put(queue, p);
1163 if (revs->exclude_first_parent_only)
@@ -1207,8 +1205,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
1205 p->object.flags |= pass_flags | CHILD_VISITED;
1206 if (!(p->object.flags & SEEN)) {
1207 p->object.flags |= (SEEN | NOT_USER_GIVEN);
1210 - if (list)
1211 - commit_list_insert_by_date(p, list);
1208 if (queue)
1209 prio_queue_put(queue, p);
1210 }
@@ -1470,7 +1466,7 @@ static int limit_list(struct rev_info *revs)
1466
1467 if (revs->max_age != -1 && (commit->date < revs->max_age))
1468 obj->flags |= UNINTERESTING;
1473 - if (process_parents(revs, commit, NULL, &queue) < 0) {
1469 + if (process_parents(revs, commit, &queue) < 0) {
1470 clear_prio_queue(&queue);
1471 return -1;
1472 }
@@ -3257,6 +3253,7 @@ static void free_void_commit_list(void *list)
3253 void release_revisions(struct rev_info *revs)
3254 {
3255 commit_list_free(revs->commits);
3256 + clear_prio_queue(&revs->commit_queue);
3257 commit_list_free(revs->ancestry_path_bottoms);
3258 release_display_notes(&revs->notes_opt);
3259 object_array_clear(&revs->pending);
@@ -3726,7 +3723,7 @@ static void explore_walk_step(struct rev_info *revs)
3723 if (revs->max_age != -1 && (c->date < revs->max_age))
3724 c->object.flags |= UNINTERESTING;
3725
3729 - if (process_parents(revs, c, NULL, NULL) < 0)
3726 + if (process_parents(revs, c, NULL) < 0)
3727 return;
3728
3729 if (c->object.flags & UNINTERESTING)
@@ -3902,7 +3899,7 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit)
3899 {
3900 struct commit_list *p;
3901 struct topo_walk_info *info = revs->topo_walk_info;
3905 - if (process_parents(revs, commit, NULL, NULL) < 0) {
3902 + if (process_parents(revs, commit, NULL) < 0) {
3903 if (!revs->ignore_missing_links)
3904 die("Failed to traverse parents of commit %s",
3905 oid_to_hex(&commit->object.oid));
@@ -3938,6 +3935,13 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit)
3935 }
3936 }
3937
3938 +void rev_info_commit_list_to_queue(struct rev_info *revs)
3939 +{
3940 + while (revs->commits)
3941 + prio_queue_put(&revs->commit_queue, pop_commit(&revs->commits));
3942 +}
3943 +
3944 +
3945 int prepare_revision_walk(struct rev_info *revs)
3946 {
3947 int i;
@@ -4006,7 +4010,7 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs,
4010 for (;;) {
4011 struct commit *p = *pp;
4012 if (!revs->limited)
4009 - if (process_parents(revs, p, NULL, queue) < 0)
4013 + if (process_parents(revs, p, queue) < 0)
4014 return rewrite_one_error;
4015 if (p->object.flags & UNINTERESTING)
4016 return rewrite_one_ok;
@@ -4020,27 +4024,18 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs,
4024 }
4025 }
4026
4023 -static void merge_queue_into_list(struct prio_queue *q, struct commit_list **list)
4027 +static void merge_queue_into_prio_queue(struct prio_queue *from,
4028 + struct prio_queue *to)
4029 {
4025 - while (q->nr) {
4026 - struct commit *item = prio_queue_peek(q);
4027 - struct commit_list *p = *list;
4028 -
4029 - if (p && p->item->date >= item->date)
4030 - list = &p->next;
4031 - else {
4032 - p = commit_list_insert(item, list);
4033 - list = &p->next; /* skip newly added item */
4034 - prio_queue_get(q); /* pop item */
4035 - }
4036 - }
4030 + while (from->nr)
4031 + prio_queue_put(to, prio_queue_get(from));
4032 }
4033
4034 static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
4035 {
4036 struct prio_queue queue = { compare_commits_by_commit_date };
4037 enum rewrite_result ret = rewrite_one_1(revs, pp, &queue);
4043 - merge_queue_into_list(&queue, &revs->commits);
4038 + merge_queue_into_prio_queue(&queue, &revs->commit_queue);
4039 clear_prio_queue(&queue);
4040 return ret;
4041 }
@@ -4331,6 +4326,7 @@ enum rev_walk_mode {
4326 REV_WALK_REFLOG,
4327 REV_WALK_TOPO,
4328 REV_WALK_LIMITED,
4329 + REV_WALK_NO_WALK,
4330 REV_WALK_STREAMING,
4331 };
4332
@@ -4342,6 +4338,8 @@ static enum rev_walk_mode get_walk_mode(struct rev_info *revs)
4338 return REV_WALK_TOPO;
4339 if (revs->limited)
4340 return REV_WALK_LIMITED;
4341 + if (revs->no_walk)
4342 + return REV_WALK_NO_WALK;
4343 return REV_WALK_STREAMING;
4344 }
4345
@@ -4349,6 +4347,9 @@ static struct commit *get_revision_1(struct rev_info *revs)
4347 {
4348 enum rev_walk_mode mode = get_walk_mode(revs);
4349
4350 + if (mode == REV_WALK_STREAMING && revs->commits)
4351 + rev_info_commit_list_to_queue(revs);
4352 +
4353 while (1) {
4354 struct commit *commit;
4355
@@ -4360,9 +4361,12 @@ static struct commit *get_revision_1(struct rev_info *revs)
4361 commit = next_topo_commit(revs);
4362 break;
4363 case REV_WALK_LIMITED:
4363 - case REV_WALK_STREAMING:
4364 + case REV_WALK_NO_WALK:
4365 commit = pop_commit(&revs->commits);
4366 break;
4367 + case REV_WALK_STREAMING:
4368 + commit = prio_queue_get(&revs->commit_queue);
4369 + break;
4370 }
4371
4372 if (!commit)
@@ -4390,12 +4394,13 @@ static struct commit *get_revision_1(struct rev_info *revs)
4394 break;
4395 case REV_WALK_STREAMING:
4396 if (process_parents(revs, commit,
4393 - &revs->commits, NULL) < 0) {
4397 + &revs->commit_queue) < 0) {
4398 if (!revs->ignore_missing_links)
4399 die("Failed to traverse parents of commit %s",
4400 oid_to_hex(&commit->object.oid));
4401 }
4402 break;
4403 + case REV_WALK_NO_WALK:
4404 case REV_WALK_LIMITED:
4405 break;
4406 }
revision.h
+11 -1
@@ -12,6 +12,7 @@
12 #include "decorate.h"
13 #include "ident.h"
14 #include "list-objects-filter-options.h"
15 +#include "prio-queue.h"
16 #include "strvec.h"
17
18 /**
@@ -122,8 +123,14 @@ struct oidset;
123 struct topo_walk_info;
124
125 struct rev_info {
125 - /* Starting list */
126 + /*
127 + * Work queue of commits, stored as either a linked list or a
128 + * priority queue, but never both at the same time.
129 + * rev_info_commit_list_to_queue() converts list to queue.
130 + */
131 struct commit_list *commits;
132 + struct prio_queue commit_queue;
133 +
134 struct object_array pending;
135 struct repository *repo;
136
@@ -400,6 +407,7 @@ struct rev_info {
407 * uninitialized.
408 */
409 #define REV_INFO_INIT { \
410 + .commit_queue = { .compare = compare_commits_by_commit_date }, \
411 .abbrev = DEFAULT_ABBREV, \
412 .simplify_history = 1, \
413 .pruning.flags.recursive = 1, \
@@ -478,6 +486,8 @@ void reset_revision_walk(void);
486 */
487 int prepare_revision_walk(struct rev_info *revs);
488
489 +/* Drain the commits linked list into the priority queue. */
490 +void rev_info_commit_list_to_queue(struct rev_info *revs);
491 /**
492 * Takes a pointer to a `rev_info` structure and iterates over it, returning a
493 * `struct commit *` each time you call it. The end of the revision list is