Managing Tags Scripts

Listing, sorting and deleting tags, and finding the nearest one to HEAD.

A repository accumulates tags, and eventually you need to answer questions about them: what is the latest, what came before it, and which ones are safe to delete.

git tag on its own lists names in lexicographic order, which is not the order they were created in. That matters as soon as versions reach two digits, because v1.10.0 sorts before v1.9.0.

Listing and sorting

#!/usr/bin/env bash
# list-tags.sh
#
# List tags newest-first, with the date each was created.

set -euo pipefail

for tag in $(git for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags); do
  # an annotated tag carries a tagger date; a lightweight one falls back to the commit
  when=$(git for-each-ref --format='%(creatordate:short)' "refs/tags/$tag")
  printf '%-16s %s\n' "$tag" "$when"
done

git tag -l is the quick version and is fine interactively. The script above is for a release checklist or a changelog, where the ordering has to be right.

The nearest tag

git describe --tags --abbrev=0            # the most recent tag reachable from HEAD
git describe --tags --long                # same, plus the commit count
git describe --tags v1.4.0..HEAD          # how far HEAD is past a specific tag

git describe is what build scripts use to derive a version number, and it is why annotated tags matter — plain git describe skips lightweight ones, so --tags is needed if the repository has any.

Finding what is unreleased

git log --oneline "$(git describe --tags --abbrev=0)"..HEAD

Everything since the last tag, which is the list of commits a release would contain. Combined with --oneline it is the fastest way to write release notes.

Deleting

git tag -d v1.4.0                  # local only
git push origin --delete v1.4.0    # and remote

Deleting a tag that was already released is worth avoiding: anything pinning to it — a dependency, a Docker image, a paper citing a version — loses a reference that pointed somewhere real. The tags that are safe to remove are the ones you created by mistake, and the script that avoids those is the one in Tagging Scripts, which refuses to overwrite an existing tag in the first place.

Pruning by pattern

git tag -l 'v1.*' --sort=-v:refname | tail -n +11 | xargs -r git tag -d

Keeps the ten most recent v1.* tags and deletes the rest. Dry-run it by dropping the -d before you run it for real.