`git describe` doesn't traverse the graph in topological order
Hi,
I found an issue where git describe doesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.
Repository: https://gitlab.kitware.com/vtk/vtk.gitmaster as of: dedf87b3a1b7e5be5d8cdb46b37ad3030590b8ac
$ git rev-parse HEAD
dedf87b3a1b7e5be5d8cdb46b37ad3030590b8ac
$ git rev-parse HEAD~2^2
da2482f716310fc59ac4be42ce977f6badc6af95
$ git rev-prase v9.3.0.rc1
f150d52568f4e00aa9c8b1568a521a08ded8d4cb
$ git rev-prase v9.3.0.rc1^{commit}
da2482f716310fc59ac4be42ce977f6badc6af95
$ git rev-parse HEAD~2^2~2
0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5
$ git rev-prase v9.3.0.rc0
e5e13b14629d445bf65d5f8a181920ed9b97d54c
$ git rev-prase v9.3.0.rc0^{commit}
0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5
$ git describe HEAD
v9.3.0.rc0-56-gdedf87b3a1
$ git describe --matches v9.3.0.rc1 HEAD
v9.3.0.rc1-86876-gdedf87b3a1As you can see:
- v9.3.0.rc1 is "closer" to HEAD than v9.3.0.rc0 (created as a workaround for this bug; v9.2.6 is otherwise reported)
- v9.3.0.rc0 is an ancestor of v9.3.0.rc1
- Both v9.3.0.rc0 and v9.3.0.rc1 are ancestors of HEAD
- git describe reports that HEAD is "closest" to v9.3.0.rc0
- Forcing the issue and asking for v9.3.0.rc1 shows that it thinks there are almost 87000 commits somehow not on that commit.
I have a reproducer script attached. It reproduces back to 2.9.0 and probably before. 2.8.0 didn't support the structure hiding that newer OpenSSL 1.1 has done and given that it's at least that old, I don't think it matters too much for backporting or anything like that.
I instrumented git describe with some printf debugging (diff attached) and found out that the commit traversal is not happening in topological order. I suspect that this is the root cause of the issue:
looking at commit 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5
depth of 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5: 16
find order of 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5: 2
setting flag 4 for commit 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5
flag for 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5: 5
pushing depth of da2482f716310fc59ac4be42ce977f6badc6af95 because of 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5 (flag_within): 16
setting flag 5 for commit d478d2e22e81ee2602035fe1d731b402d9b4eda7 due to ancestry
looking at commit 1c3d839dac92761ae0866e23d89bdc8ee690de08
depth of 1c3d839dac92761ae0866e23d89bdc8ee690de08: 17
find order of 1c3d839dac92761ae0866e23d89bdc8ee690de08: 3
setting flag 8 for commit 1c3d839dac92761ae0866e23d89bdc8ee690de08
flag for 1c3d839dac92761ae0866e23d89bdc8ee690de08: b
pushing depth of 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5 because of 1c3d839dac92761ae0866e23d89bdc8ee690de08 (flag_within): 17
setting flag f for commit 0a77d7cf4fdbf489ee5d38c6fec6517574cdaeb5 due to ancestryIt looks at 0a77d7cf4 before it looks at 1c3d839da (which is v9.3.0.rc1~), but 1c3d839da~ *is* 0a77d7cf4. Because 0a77d7cf4 has already passed on its presence flags to its parent(s), the update performed when processing 1c3d839da has no effect. Therefore the "entire" history is not seen as being reachable from da2482f71 and it ends up not being the best match.
I will note that the authorship date of 1c3d839da is before that of either 0a77d7cf4 or da2482f71 (due to a rebase that reordered the commits to keep 1c3d839da on the release-only part of the branch), but the reproducer script doesn't seem to care that much.
I suspect that building of the commit_list is the problem, probably by using commit_list_insert_by_date instead of by topological sorting. The reproducer script doesn't do anything (AFAICT) sneaky with dates (e.g., rebasing and such) though, so I'm nowhere near 100% confident about that.
Perhaps commits should be re-scheduled if their flags get updated based on a newly discovered ancestor while traversing? I suspect that depth tracking becomes more complicated in that case though because the second pass on 0a77d7cf4 needs to subtract a depth from the relevant tags with the new flag value. But it'd find the right tag at least…
Thanks,
--Ben
builtin/describe.c +14 -2
@@ -264,8 +264,10 @@ static unsigned long finish_depth_computation( }if (!a)break ; - }else + }else { best->depth++; +fprintf (stderr,"pushing depth of %s (finish_depth_computation): %d\n" ,oid_to_hex (&c->object.oid), best->depth); + }while (parents) {struct commit *p = parents->item;repo_parse_commit (the_repository, p);
@@ -363,19 +365,24 @@ static void describe_commit(struct object_id *oid, 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++; slot =commit_names_peek (&commit_names, c); n = slot ? *slot :NULL ;if (n) {if (!tags && !all && n->prio <2 ) { +fprintf (stderr,"skipping unannotated tag %s\n" ,oid_to_hex (&c->object.oid)); unannotated_cnt++; }else if (match_cnt < max_candidates) {struct possible_tag *t = &all_matches[match_cnt++]; t->name = n; t->depth = seen_commits -1 ; +fprintf (stderr,"depth of %s: %d\n" ,oid_to_hex (&c->object.oid), t->depth); t->flag_within =1u << match_cnt; t->found_order = match_cnt; +fprintf (stderr,"find order of %s: %d\n" ,oid_to_hex (&c->object.oid), t->found_order); c->object.flags |= t->flag_within; +fprintf (stderr,"setting flag %x for commit %s\n" , t->flag_within,oid_to_hex (&c->object.oid));if (n->prio ==2 ) annotated_cnt++; }
@@ -386,11 +393,15 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) }for (cur_match =0 ; cur_match < match_cnt; cur_match++) {struct possible_tag *t = &all_matches[cur_match]; -if (!(c->object.flags & t->flag_within)) +if (!(c->object.flags & t->flag_within)) { t->depth++; +fprintf (stderr,"flag for %s: %x\n" ,oid_to_hex (&c->object.oid), c->object.flags); +fprintf (stderr,"pushing depth of %s because of %s (flag_within): %d\n" ,oid_to_hex (&t->name->peeled),oid_to_hex (&c->object.oid), t->depth); + } }/* Stop if last remaining path already covered by best candidate(s) */ if (annotated_cnt && !list) { +fprintf (stderr,"checking for best candidate\n" );int best_depth =INT_MAX ;unsigned best_within =0 ;for (cur_match =0 ; cur_match < match_cnt; cur_match++) {
@@ -415,6 +426,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst)if (!(p->object.flags &SEEN ))commit_list_insert_by_date (p, &list); p->object.flags |= c->object.flags; +fprintf (stderr,"setting flag %x for commit %s due to ancestry\n" , p->object.flags,oid_to_hex (&p->object.oid)); parents = parents->next;if (first_parent)
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.
Bump. Can anyone provide guidance as to what the best solution to this might be?
Thanks,
--Ben
Quoted 6 lines · Ben Boeckel
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.Bump. Can anyone provide guidance as to what the best solution to this might be?
Can you provide details? git describe is sensitive to --first-parent and whether the tag has annotations. --Randall
Quoted 10 lines · On Fri, Sep 22, 2023 at 12:13:00 -0400, [email protected] wrote:
Quoted 6 lines · Ben Boeckel
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.Bump. Can anyone provide guidance as to what the best solution to this might be?
Can you provide details?
git describeis sensitive to --first-parent and whether the tag has annotations.
I provided more details and a reproducer in the original email:
https://lore.kernel.org/git/ZNffWAgldUZdpQcr@farprobe/T/#u
Thanks,
--Ben
Quoted 7 lines · Ben Boeckel
Quoted 6 lines · On Fri, Sep 22, 2023 at 12:13:00 -0400, [email protected] wrote:
Quoted 5 lines · Ben Boeckel
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.
Quoted 8 lines
Quoted 4 lines
Quoted 1 line
Bump. Can anyone provide guidance as to what the best solution to this might be?
Can you provide details?
git describeis sensitive to --first-parent and whether the tag has annotations.I provided more details and a reproducer in the original email:
As I indicated, the command is sensitive to --first-parent. For example:
$ git describe v9.3.0.rc0-520-g1339e86833 $ git describe --first-parent v9.0.0.rc1-5143-g1339e86833
You have multiple parents in your tree of HEAD. This is probably confusing the interpretation. The most closely connected tag to HEAD is v9.3.0.rc0, from what I can read from your tree. Dates and times of the commit do not participate in this determination, to my knowledge. You can force selection of a subset of tags by specifying the --match=pattern argument.
There appears to be a merge at 446120fd88 which brings v9.3.0.rc0 closer to HEAD than v9.3.0.rc1.
Quoted 15 lines · On Fri, Sep 22, 2023 at 13:14:30 -0400, [email protected] wrote:
Quoted 14 lines · Ben Boeckel
Quoted 10 lines · On Fri, Sep 22, 2023 at 12:13:00 -0400, [email protected] wrote:
Quoted 6 lines · Ben Boeckel
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.Bump. Can anyone provide guidance as to what the best solution to this might be?
Can you provide details?
git describeis sensitive to --first-parent and whether the tag has annotations.I provided more details and a reproducer in the original email:
Quoted 8 lines
As I indicated, the command is sensitive to --first-parent. For example:
$ git describe v9.3.0.rc0-520-g1339e86833 $ git describe --first-parent v9.0.0.rc1-5143-g1339e86833
Sorry, but this is just even more confusing to me as neither tag is on the first-parent history of HEAD.
Quoted 6 lines
You have multiple parents in your tree of HEAD. This is probably confusing the interpretation. The most closely connected tag to HEAD is v9.3.0.rc0, from what I can read from your tree. Dates and times of the commit do not participate in this determination, to my knowledge. You can force selection of a subset of tags by specifying the --match=pattern argument.
I don't see how that is possible since v9.3.0.rc0 is v9.3.0.rc1~2. Note the "not on the tag" commit count for the descriptions being wildly different.
Quoted 2 lines
There appears to be a merge at
446120fd88which brings v9.3.0.rc0 closer to HEAD than v9.3.0.rc1.
That is still giving an incorrect description as there are *fewer* commits not on rc1 than rc0 relative to HEAD (as rc0 is an ancestor of rc1).
Thanks,
--Ben
Quoted 1 line · <[email protected]> writes:
There appears to be a merge at
446120fd88which brings v9.3.0.rc0 closer to HEAD than v9.3.0.rc1.
I didn't look at the actual graph but let me say I trust you ;-)
I wonder if there should be an obvious "explain why you gave this name" mode added to the command, though. The command should be able to say "The closest path from HEAD to any tag is via this, that, and that commit, which is N hops to tag T0", and from there, the user should be able to say "Oh, I thought T1 was closer, let me try again to describe HEAD, limiting the candidate only to T1" and run the command in that mode, which should be able to say "The closest path from HEAD to any tag that is allowed as a candidate is via these commits, which is M hops to tag T1". And if M is smaller than N, then that may deserve to trigger a bug report (but as you said, there are rules like preferring annotated over unannotated tags involved, so it may not as straight-forward as comparing the two integer hop counts).
Thanks for digging.
Quoted 3 lines · Junio C Hamano
Quoted 1 line · <[email protected]> writes:
There appears to be a merge at
446120fd88which brings v9.3.0.rc0 closer
to HEAD
Quoted 5 lines
than v9.3.0.rc1.
I didn't look at the actual graph but let me say I trust you ;-)
I wonder if there should be an obvious "explain why you gave this name"
mode added
Quoted 1 line
to the command, though. The command should be able to say "The closest
path from
Quoted 1 line
HEAD to any tag is via this, that, and that commit, which is N hops to tag
T0", and
Quoted 1 line
from there, the user should be able to say "Oh, I thought T1 was closer,
let me try
Quoted 1 line
again to describe HEAD, limiting the candidate only to T1" and run the
command in
Quoted 1 line
that mode, which should be able to say "The closest path from HEAD to any
tag that
Quoted 1 line
is allowed as a candidate is via these commits, which is M hops to tag T1".
And if M
Quoted 1 line
is smaller than N, then that may deserve to trigger a bug report (but as
you said,
Quoted 1 line
there are rules like preferring annotated over unannotated tags involved,
so it may
Quoted 3 lines
not as straight-forward as comparing the two integer hop counts).
Thanks for digging.
I'm wondering whether we need something more general that --first-parent. Perhaps something like
git describe commitish [ commitish ... ]
Where the traversal must cross the set of specified commitish points in history in order to find the expected tag. In Ben's case, I do not think that would help much, given the complexity of his history. Perhaps a --verbose argument might display the analysis path done by git describe as above. Sadly, I am not familiar with this code area.
What confuses me is how, in the other subthread, that adding sleep 1 to the construction of history should make any difference. My understanding is that the path to the tag is invariant of the commit-date.
Quoted 3 lines · On Fri, Sep 22, 2023 at 14:12:31 -0400, [email protected] wrote:
What confuses me is how, in the other subthread, that adding sleep 1 to the construction of history should make any difference. My understanding is that the path to the tag is invariant of the commit-date.
Yes. It is explained that the commit date stored is only to 1 second granularity. Since the commits are stored in commit-date, an equal commit date ends up "twisting" the history and traversing some ancestors of commits before the commits themsevles. This loses the "seen" bit tracking that is done and ends up labeling way more commits as "not part of" ancestors. By sleeping for a second, the commit dates can be totally ordered reliably.
And this tracks with my and the other thread's result that the traversal is not paying attention to the topological history properly.
--Ben
Quoted 11 lines · Ben Boeckel
Quoted 3 lines · On Fri, Sep 22, 2023 at 14:12:31 -0400, [email protected] wrote:
What confuses me is how, in the other subthread, that adding sleep 1 to the construction of history should make any difference. My understanding is that the path to the tag is invariant of the commit-date.
Yes. It is explained that the commit date stored is only to 1 second granularity. Since the commits are stored in commit-date, an equal commit date ends up "twisting" the history and traversing some ancestors of commits before the commits themsevles. This loses the "seen" bit tracking that is done and ends up labeling way more commits as "not part of" ancestors. By sleeping for a second, the commit dates can be totally ordered reliably.
This is going to be awkward to resolve as time_t only resolves (portably) to 1 second intervals. I still would prefer the resolution to be path-based rather than time-based.
Quoted 11 lines · On Fri, Sep 22, 2023 at 14:49:58 -0400, [email protected] wrote:
Quoted 6 lines · Ben Boeckel
Yes. It is explained that the commit date stored is only to 1 second granularity. Since the commits are stored in commit-date, an equal commit date ends up "twisting" the history and traversing some ancestors of commits before the commits themsevles. This loses the "seen" bit tracking that is done and ends up labeling way more commits as "not part of" ancestors. By sleeping for a second, the commit dates can be totally ordered reliably.
This is going to be awkward to resolve as
time_tonly resolves (portably) to 1 second intervals. I still would prefer the resolution to be path-based rather than time-based.
I certainly agree, but I'm not sure of the best way of doing that. Do we create/load a commit graph and use that for resolving insertion order into the commit heap?
--Ben
Quoted 10 lines · Ben Boeckel
Quoted 4 lines · On Fri, Sep 22, 2023 at 14:49:58 -0400, [email protected] wrote:
Quoted 3 lines · Ben Boeckel
Yes. It is explained that the commit date stored is only to 1 second granularity. Since the commits are stored in commit-date, an equal commit date ends up "twisting" the history and traversing some ancestors of
commits before the commits themsevles.
Quoted 4 lines
Quoted 3 lines
This loses the "seen" bit tracking that is done and ends up labeling way more commits as "not part of" ancestors. By sleeping for a second, the commit dates can be totally ordered reliably.
Quoted 6 lines
Quoted 3 lines
This is going to be awkward to resolve as
time_tonly resolves (portably) to 1 second intervals. I still would prefer the resolution to be path-based rather than time-based.I certainly agree, but I'm not sure of the best way of doing that. Do we create/load a commit graph and use that for resolving insertion order into the commit heap?
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.
Quoted 6 lines · On Fri, Sep 22, 2023 at 15:27:01 -0400, [email protected] wrote:
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 <[email protected]>
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(-)
builtin/describe.c +19 -1
@@ -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 ||
t/t6120-describe.sh +1 -1
@@ -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
Signature · 1 line
2.51.1
Quoted 6 lines · Ben Boeckel
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...
Quoted 5 lines
builtin/describe.c +5
-0+/* +* 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)
builtin/describe.c struct lazy_queue { · line 274
Quoted 10 lines
builtin/describe.c +10
-0+{ +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. ;)
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().
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.
-Peff
Quoted 25 lines · Jeff King
Quoted 6 lines · Ben Boeckel
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...
Quoted 14 lines
builtin/describe.c +14
-0+/* +* 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);
Quoted 8 lines
Quoted 1 line
+}
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…
Quoted 5 lines
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()inrevision.c). If not, then we collect all of the commits up front and callsort_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.
Quoted 4 lines
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
Quoted 6 lines · Junio C Hamano
Quoted 1 line · <[email protected]> writes:
There appears to be a merge at
446120fd88which brings v9.3.0.rc0 closer to HEAD than v9.3.0.rc1.I didn't look at the actual graph but let me say I trust you ;-)
Quoted 13 lines
I wonder if there should be an obvious "explain why you gave this name" mode added to the command, though. The command should be able to say "The closest path from HEAD to any tag is via this, that, and that commit, which is N hops to tag T0", and from there, the user should be able to say "Oh, I thought T1 was closer, let me try again to describe HEAD, limiting the candidate only to T1" and run the command in that mode, which should be able to say "The closest path from HEAD to any tag that is allowed as a candidate is via these commits, which is M hops to tag T1". And if M is smaller than N, then that may deserve to trigger a bug report (but as you said, there are rules like preferring annotated over unannotated tags involved, so it may not as straight-forward as comparing the two integer hop counts).
The thing is that the count is what is wrong here, so the determination of what is "closer" is wrong. Any explanation would say things like "commit X~10 is not part of X".
--Ben
Quoted 2 lines · On Fri, Sep 22, 2023 at 13:14:30 -0400, [email protected] wrote:
There appears to be a merge at
446120fd88which brings v9.3.0.rc0 closer to HEAD than v9.3.0.rc1.
I'll also note that .rc0 was added as a fix for the situation of .rc1 not being found properly. Without that, it finds v9.2.6 as the "closest" tag.
--Ben
Quoted 11 lines · On Fri, Sep 22, 2023, at 18:13, [email protected] wrote:
Quoted 6 lines · Ben Boeckel
Quoted 3 lines · Ben Boeckel
I found an issue where
git describedoesn't find a "closer" tag than another tag as the correct one to base the description off of. I have a reproducer, but I'll first give details of the real world issue.Bump. Can anyone provide guidance as to what the best solution to this might be?
Can you provide details?
git describeis sensitive to --first-parent and whether the tag has annotations.
--Randall
Both of the tags (v9.3.0.rc0 and v9.3.0.rc1) are annotated ones.
Signature · 1 line
Kristoffer Haugsbakk
Looks related:
Message-ID: CABPp-BH2zuYe87xhjdp5v7M7i+EfEgLHAZgwfzJUAxGk1CFgfA@mail.gmail.com
Via: https://stackoverflow.com/questions/72886894/git-describe-is-not-returning-the-expected-tag
Signature · 1 line
Kristoffer Haugsbakk
Quoted 5 lines · Kristoffer Haugsbakk
Looks related:
Message-ID: CABPp-BH2zuYe87xhjdp5v7M7i+EfEgLHAZgwfzJUAxGk1CFgfA@mail.gmail.com
Via: https://stackoverflow.com/questions/72886894/git-describe-is-not-returning-the-expected-tag
Thanks. It seems that these discussions previously determined the same (painful) pill:
SZEDER Gábor at https://lore.kernel.org/git/[email protected]/:
I think the proper way to fix this issue would be to make 'git describe' traverse the history in topographical order. Alas, I'm afraid this would result in a noticable performance penalty on big histories without a commit graph.
The sleep 1 is probably the remedy we'll use if time to actually fix this doesn't come up (or the "proper" fix is deemed as "too expensive").
Thanks for the links,
--Ben