git/list[1] front-page[2] threads[3] people[4] search[5] about
 

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

From
'B'Ben Boeckel' <ben.boeckel@kitware.com>
Date
Nov 20, 2025, 02:48 UTC
Message-ID
<aR6BlHflRVLN8_XO@rotor>
In-Reply-To
<033c01d9ed8a$c6916f30$53b44d90$@nexbridge.com>
On Fri, Sep 22, 2023 at 15:27:01 -0400, rsbecker@nexbridge.com wrote:
Show 6 quoted lines
> I actually thought it worked that way. This may end up in a bigger
> change than fixing the issue because --first-parent does not appear to
> be sufficient to resolve the correct tag from your graph. My thought
> on using multiple commitish values to do that may help, but
> implementing that could lead to an O(n*m) scan (n=max commit tree
> width, m=depth to tag), plus a commitish hash lookup.

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).

We could go one of two (or more! feel free to offer alternatives) ways:
- swallow the pill and accept the performance for accurate results
  (e.g., warn if there is not a recent `commit-graph`)
- add an `--accurate` flag to optionally use it with the caveat that
  reported descriptions may *change* under the flag (e.g., with the
  reproducer script, a "working" description is `tag-release-7-g<hash>`,
  but with the graph, it is the correct `tag-release-5-g<hash>`.

Also note the the reproducer provided was "fixed" in 7379046221 (describe: stop digging for max_candidates+1, 2024-11-06) because it stopped searching because there were no more tags in the history. Tagging the root commit preserves the reproducer state. I've attached an updated reproducer script as well.

Thoughts on a plan forward?
--Ben
From ca4df5b9c9542315f77c166d47d5c63a2ebdafd1 Mon Sep 17 00:00:00 2001
From: Ben Boeckel <mathstuf@gmail.com>
Date: Wed, 12 Nov 2025 23:53:20 -0500
Subject: [PATCH 1/1] describe: traverse commits by ancestry instead of commit
 date

An ancestor commit should never be traversed before its descendents. This could happen if a series of commits are made in rapid succession and they all share a commit date (to the 1-second resolution supported in the metadata).

This was discovered in VTK's history where a `git describe` would return the previous release's tag name rather than the one just made. The problematic topology looks like:

    H ---- M1 -- M2 -- M3 -- M4 - ROOT
    |       \     \     \    \    /|
    |        \     \     |    \  / |
    |         \    R2 ---|---- R1 |
    |          \   /     |    /  /
     \          \ /       \  /  /
      P1 - P2 - P3 ------- P4 --

Where P1 and P3 are tagged commits. If all commits share a commit date, `git describe` traverses in the following order:

  - H
  - M1
  - P1 (tagged)
  - M2
  - P3 (tagged)
  - P2
  - M3
  - R2
  - P4
  - M4
  - ROOT
  - R1

Although P1 is traversed before P3, P1's depth is incremented due to the `flag_within` check despite the ancestry actually being the other way around. When all is said and done, the description is reported as `P3-7-g<hash>` despite the P1 tagged commit having it as an ancestor. If P1 is restricted using `describe --match`, it is reported as `P1-10-g<hash>` due to the traversal order issue.

Using topology sorting on the commit queue, the description is accurately reported as `P1-5-g<hash>` instead and the traversal order is:

Instead of commit date heuristics, use ancestry as the sort constraint. This also fixes one expect-failure test case as well. However, the performance depends on having a `git commit-graph` available.

Reported-in: <ZNffWAgldUZdpQcr@farprobe>
---
 builtin/describe.c  | 20 +++++++++++++++++++-
 t/t6120-describe.sh |  2 +-
 2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/builtin/describe.c b/builtin/describe.c
index ffaf8d9f0a..789586e5a5 100644
--- a/builtin/describe.c
+++ b/builtin/describe.c
@@ -3,6 +3,7 @@
 
 #include "builtin.h"
 #include "config.h"
+#include "commit-reach.h"
 #include "environment.h"
 #include "gettext.h"
 #include "hex.h"
@@ -256,7 +257,23 @@ struct lazy_queue {
 	bool get_pending;
 };
 
-#define LAZY_QUEUE_INIT { { compare_commits_by_commit_date }, false }
+/*
+ * 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);
+}
+
+#define LAZY_QUEUE_INIT { { compare_commits_topo }, false }
 
 static void *lazy_queue_get(struct lazy_queue *queue)
 {
@@ -413,6 +430,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 		struct commit_list *parents = c->parents;
 		struct commit_name **slot;
 
+		fprintf(stderr, "\n\nlooking at commit %s\n", oid_to_hex(&c->object.oid));
 		seen_commits++;
 
 		if (match_cnt == max_candidates ||
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561a..36e1b9d848 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -711,7 +711,7 @@ test_expect_success 'setup: describe commits with disjoint bases 2' '
 '
 
 check_describe -C disjoint2 "B-3-gHASH" HEAD
-check_describe -C disjoint2 --expect-failure "B-3-gHASH" --candidates=2 HEAD
+check_describe -C disjoint2 "B-3-gHASH" --candidates=2 HEAD
 
 test_expect_success 'setup misleading taggerdates' '
 	GIT_COMMITTER_DATE="2006-12-12 12:31" git tag -a -m "another tag" newer-tag-older-commit unique-file~1
-- 
2.51.1
Previous: rsbecker@nexbridge.comNext: Jeff King
Message 13 of 20 in “[BUG] `git describe` doesn't traverse the graph in topological order”
  1. Ben BoeckelAug 12, 2023
  2. Ben BoeckelSep 22, 2023
  3. rsbecker@nexbridge.comSep 22, 2023
  4. 'Ben Boeckel'Sep 22, 2023
  5. rsbecker@nexbridge.comSep 22, 2023
  6. 'Ben Boeckel'Sep 22, 2023
  7. Junio C HamanoSep 22, 2023
  8. rsbecker@nexbridge.comSep 22, 2023
  9. 'Ben Boeckel'Sep 22, 2023
  10. rsbecker@nexbridge.comSep 22, 2023
  11. 'Ben Boeckel'Sep 22, 2023
  12. rsbecker@nexbridge.comSep 22, 2023
  13. 'Ben Boeckel'Nov 20, 2025
  14. Jeff KingNov 20, 2025
  15. 'Ben Boeckel'Feb 28, 2026
  16. 'Ben Boeckel'Sep 22, 2023
  17. 'Ben Boeckel'Sep 23, 2023
  18. Kristoffer HaugsbakkSep 22, 2023
  19. Kristoffer HaugsbakkSep 22, 2023
  20. 'Ben Boeckel'Sep 22, 2023

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.