I am trying to find the nearest tag after a commit in a particular branch.
I am aware that git describe will walk back and find the nearest ancestor with a tag but how do I move in the opposite direction.
For example:
Given commit 2 I would like to return the future tag. Where git describe would return the past tag.
How about git describe --contains <commit 2> | cut -d'~' -f1
? As that would refer back to <commit 2>
from the tag by the number of commits, only take the part before ~
as the tag name (given that your tag names do not contain ~
).
Commits' arrows go in the other direction. Commit 1 does not point to commit 2; instead, commit 2 points to commit 1. This matters, because...
In your graph you have drawn just four commits, with neither branches nor merges. That is, you have drawn the following:
A <- B <- C <- D <-- branch-tip
(I have switched from numbers to letters, since there are 26 letters and only ten one-digit numbers, not that I'm using up all ten one-digit numbers anyway....) If you are at commit B
, it is very easy to go back to commit A
: that's the direction the arrows point. It's hard—in fact, impossible without assistance—to go the other way.
The key bit of assistance we need is an identity or locator for a later commit. For instance, using the name branch-tip
, we can locate commit D
. From D
, we can go back to C
, then back to B
, and now we have traced a usable path.
But this graph is too simple. Let's draw one with a branch in it. Since we know Git's arrows are all backwards, let's not bother drawing the internal arrows, and just use connecting lines, knowing that when we draw horizontally like this, we can only move leftward. And, let's put in your tags, too, plus a few more:
tag:past
|
| tag:future
| | tag:distantfuture
v v v
A--B--C---D <-- branch1
\
E--F <-- branch2
^ ^
| |
| tag:future3
|
tag:future2
Now, which future tag would you like to find for commit B
, future
or future2
? We know you want neither future3
nor distantfuture
, but future2
and future
are both just one step away from B
.
The key is that they're one step in some direction: towards the tip of branch1
or towards the tip of branch2
. You may wish to pick a direction, and you do that the same way that you help Git find commit D
or F
in the first place, by picking an identifier that locates a commit that eventually comes back to B
.
If you do want to control the direction, you will want to use git rev-list --ancestry-path
to find commits that are descendants of B
but ancestors of the given branch-tip (excluding B
itself and including the branch tip).
Then, combine this list of commits with something like Vampire's answer, so that you look only at tags pointing to commits on this ancestry path. That is, throw out of the list of "tags that contain commit B" any tag pointing to a commit that is not in the git rev-list
output.
Note that this won't solve a problem that occurs in the case of a graph with merges as well as branches:
C--D
/ \
A--B G--H <-- branch-tip
\ /
E--F
Here, if there are tags pointing to commits D
and F
, both are equidistant from B
, yet both are also on the ancestry path leading from B
to H
.
Note that Vampire's answer simply uses all tags as the way to locate later commits that could lead back to B
, using --contains
to restrict the list to tags that do, eventually, lead back to B
. The rev-list --count
method then finds their "distance". But if there are merges in the graph, this count may be somewhat flawed. For instance, in this last graph, with the ring of commits, the "distance" from H
to B
is really four; but there are six commits in the path from H
back to B
. It's just that two of them—C-D
and E-F
—can be traversed in parallel.
(Note: if you are using --ancestry-path
to limit the testing to tags that point to commits within some particular graph path, the slight flaw in the git rev-list --count
metric won't matter, as the same counting-flaw will apply to all ambiguities. It's only a problem if you are not using --ancestry-path
: in this case, a tag containing a merge commit may produce a much higher count than a tag not containing a merge commit, yet actually be "closer" to commit B
.)
git tag --contains <commit 2> | xargs -I {} sh -c 'echo "$(git rev-list --count <commit 2>..{}) {}"' | sort -n | head -n 1 | cut -d ' ' -f 2-
Nearest here is determined by amount of commits. If there are multiple ones that have the same distance, it is not guaranteed which one you will get. If you want all with the same smallest distance, remove the head
call from the pipeline.
Note: This might not work optimally in case of merges, if you have branched and merged those branches after the commit in question and have a tag after that merge, commits of both branches would contribute to the amount of commits and thus the distance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With