Subject: Re: [BUG] `git describe` doesn't traverse the graph in topological order

From: 'Ben Boeckel' <[email-masked]>
Date: Sat, 28 Feb 2026 01:11:43 -0500

On Thu, Nov 20, 2025 at 03:05:25 -0500, Jeff King wrote:
> On Wed, Nov 19, 2025 at 09:48:52PM -0500, 'Ben Boeckel' wrote:
> 
> > So I finally found some time to go back to this. The actual fix is
> > actually rather easy (patch attached). However, as guessed at previously
> > in the thread, the performance is in the tank without an up-to-date
> > commit graph ("instant" with it versus "minutes" without). On the other
> > hand, it is *accurate*. It does fix one expect-fail test case already in
> > the test suite (also included in the patch).
> 
> Minutes? Yikes. Let's look...
> 
> > +/*
> > + * Topological comparison: always return parents before children.
> > + * This is reverse topological order: children before parents.
> > + */
> > +static int compare_commits_topo(const void *a_, const void *b_, void *_unused_ UNUSED)
> > +{
> > +	struct commit *a = (struct commit *)a_;
> > +	struct commit *b = (struct commit *)b_;
> > +	if (repo_is_descendant_of(the_repository, a, &(struct commit_list){ b, NULL }))
> > +		return -1; // a is descendant, so comes before b
> > +	if (repo_is_descendant_of(the_repository, b, &(struct commit_list){ a, NULL }))
> > +		return 1; // b is descendant, so comes before a
> > +	// fallback: order by hash for determinism
> > +	return oidcmp(&a->object.oid, &b->object.oid);
> > +}
> 
> Ah. So you are doing two full traversals for each comparison. That is
> going to be expensive. You would do much better to walk all of history
> one time, marking the generation number (distance to root) of each
> commit, and then comparing generations here (if A has a lower generation
> than B, then you know that B cannot be an ancestor of A). Or if we have
> commit graphs, just use the generation numbers they already contain. ;)

Ok, so it sounds like I should, in `describe_commit`:

- check if commit graphs are enabled (and verified?): if so, use their
  generation numbers
- if they're not enabled, perform a local walk to store a generation
  number (somewhere?) that is `max(cmit->parents[].generation) + 1`
  (however the `generation` is stored)

and then in the comparator, use this to exclude one of the comparisons
at least. However…

> We do all of this already for the "--topo-order" option of the revision
> traversal machinery. If we have commit graphs, it can output in
> topographical order in a streaming way (see init_topo_walk() in
> revision.c). If not, then we collect all of the commits up front and
> call sort_in_topological_order().

The key here seems to be:

	if (revs->topo_order && !generation_numbers_enabled(the_repository))
		revs->limited = 1;

which then goes down the `sort_in_topological_order` path.

> Sadly, git-describe does not seem to use the traversal machinery, so it
> is not as easy as just setting revs.topo_order. Either we have to adapt
> to using the regular traversal code, or those same concepts need to be
> applied to its custom traversal.

I suppose I can try to convert it over to a proper walk following
`MyFirstObjectWalk.adoc` if that is a more fruitful path than the above
ideas. As long as all children of a commit are walked before the commit
itself, it should slot into the existing bookkeeping fairly well.

Thanks,

--Ben