Nikolaus
Nikolaus

Reputation: 57

How to list all tags from commits, which are not contained in any branch?

We've got a bunch of repositories, where lots of commits are not contained in any branch but are only kept alive because of a tag. I'd like to list all such tags. Haven't figured out, how to do that. Does anyone have an idea, how to achieve that?

Upvotes: 3

Views: 99

Answers (1)

torek
torek

Reputation: 488193

Tags aren't from commits, they just point to commits. But your question does have an answer—it's just phrased a bit oddly. The more accurate phrasing leads us to the answers:

How, for each tag, can I test whether the commit the tag identifies is contained within any branch?

Hence we would like to operate on each tag and perform some test. There are commands that enumerate each tag. For scripting purposes, git for-each-ref is the best1 tool. So we start with:

git for-each-ref refs/tags

which prints out all tags (to its standard output) along with extra information about the target of each tag:

$ git for-each-ref refs/tags
04c6e9e9ca34226db095bbaa1218030f99f0b7c6 commit refs/tags/a
d5aef6e4d58cfe1549adef5b436f3ace984e8c86 tag    refs/tags/b

for instance. The a tag goes directly to a commit, i.e., is a lightweight tag; the b tag goes to an annotated tag object.

This isn't a solution yet but it's getting us there. The next thing we'd like to do is find out whether the target of an annotated tag is a commit object, and if so, find the commit object's hash. It turns out that git for-each-ref itself can do this, using the --format directive %(*objecttype) and %(*objectname). Annoyingly, these %(*...) directives produce nothing when the tag is a lightweight tag, which requires a bit of gimmickry:

git for-each-ref \
    --format='%(refname) %(objecttype) %(objectname) %(*objecttype) %(*objectname)' \
    refs/tags

(I've broken this into multiple lines for posting purposes; in a script, we can just have a single long line without the backslash-newline sequences).

This produces as its output a series of lines of either three or five columns each. The first three columns are the reference name, the object type (which might be "tag"), the initial tag hash ID, and then if the last two columns are present, the target type and final target ID. We need to feed these to a shell script:

git for-each-ref \
    --format='%(refname) %(objecttype) %(objectname) %(*objecttype) %(*objectname)' \
    refs/tags |
    while read name dtype dobj itype iobj; do
        ...
    done

Now, inside the ... section, we implement our test: is the direct or indirect object a commit, and if so, is it reachable by any branch name?

The "is the object a commit" test is straightforward enough. First, though, let's use the indirect object and name if they exist, otherwise the direct object and name:

    if [ $dtype = tag ]; then
        otype=$itype obj=$iobj
    else
        otype=$dtype obj=$dobj
    fi

Now we'll skip non-commit objects:

    [ $otype == commit ] || continue

and last, we'll test whether the object's hash ID is reachable from some branch name:

    n=$(git for-each-ref refs/heads --contains $obj | wc -l)

This for-each-ref prints out each branch name (and other data as usual for for-each-ref) that reaches the given object. We don't care about the actual names, just whether there are any names, so let's count how many lines the inner for-each-ref prints. If it's zero, this tag is keeping this commit alive, so let's print the tag:

    if [ $n -eq 0 ]; then
        echo "tag $name keeps $obj alive"
    fi

When we run the whole thing, there's one minor flaw: this prints tag refs/tags/a for instance. We can fix that up by using %(refname:short) in the initial --format and we'll get tag a instead.

So, the final script reads:

git for-each-ref --format='%(refname:short) %(objecttype) %(objectname) %(*objecttype) %(*objectname)' refs/tags |
while read name dtype dobj itype iobj; do
    if [ $dtype = tag ]; then
        otype=$itype obj=$iobj
    else
        otype=$dtype obj=$dobj
    fi
    [ $otype == commit ] || continue
    n=$(git for-each-ref refs/heads --contains $obj | wc -l)
    if [ $n -eq 0 ]; then
        echo "tag $name keeps $obj alive"
    fi
done

(I've added this to GitHub here. The script could stand some improvement to make it take Git options and such, but for now I don't really care that much. It's also very slow, which could be improved by writing it as something other than a very simple shell script, but see previous remark.)


1Best is one of those difficult-to-measure things, but at least I find it to be the best one.

Upvotes: 5

Related Questions