DEV Community

David Bernard
David Bernard

Posted on • Updated on

For version as git tag: use annotated tag, not 'v' prefix

Tagging commit with meaningful version number like major.minor.patch (see Semantic Versioning 2.0.0 | Semantic Versioning) is a common way to name a source tree.

Why using 'v' prefix is not useful ?

A common practice is to prefix tags for version with 'v' like v1.0.0, but I don't understand why?

It is not to easily identify tag that refer to version with the pattern v*. Because this pattern also grabs non version tag that started by the letter v. So need to used a more complex pattern like v*.*.* or to combine a tool that support regexp.

# accept v1.0.0 and 1.0.0
git tag -l '*.*.*'

# combine with grep to have a more selective pattern
git tag -l | grep -P '\d+\.\d+\.\d+'
Enter fullscreen mode Exit fullscreen mode

If you use tag only for version then "why using a prefix ?" and if not then version tag will be potentially mixed with tag starting with a letter (a...,b..., ..., v..., w...). Without the 'v' prefix, digit will be before or after in alphabetical order, for time based order no change.

An other issue comes if you use the tag to extract the current version of a source tree, you need to remove the prefix.

So without v prefix you can do something like (github-actions Filter pattern cheat sheet):

on:
  push:
    tags:
      #- 'v*.*.*'
      - '[0-9]+.[0-9]+.[0-9]+'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set env
        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
      - name: Test
        run: |
          echo $RELEASE_VERSION
          echo ${{ env.RELEASE_VERSION }}
Enter fullscreen mode Exit fullscreen mode

Why annotated tag ?

Annotated tag are like "heavyweight" tag, meaning that like a commit they also include a message and an author. IMHO, they also are a prefect way to mark the importance of version number and to isolate them from lightweight tag.

# create an annotated tag
git tag -a "0.1.0" -m ":bookmark: 0.1.0"
Enter fullscreen mode Exit fullscreen mode

When using annotated tag only for version, it also means that you can use git describe to know the "semi-semantic version" of the current working tree (without need to apply filter).

❯ git describe -h
usage: git describe [<options>] [<commit-ish>...]
   or: git describe [<options>] --dirty

    --contains            find the tag that comes after the commit
    --debug               debug search strategy on stderr
    --all                 use any ref
    --tags                use any tag, even unannotated
    --long                always use long format
    --first-parent        only follow first parent
    --abbrev[=<n>]        use <n> digits to display object names
    --exact-match         only output exact matches
    --candidates <n>      consider <n> most recent tags (default: 10)
    --match <pattern>     only consider tags matching <pattern>
    --exclude <pattern>   do not consider tags matching <pattern>
    --always              show abbreviated commit object as fallback
    --dirty[=<mark>]      append <mark> on dirty working tree (default: "-dirty")
    --broken[=<mark>]     append <mark> on broken working tree (default: "-broken")

Enter fullscreen mode Exit fullscreen mode

My favorite arguments are:

git describe --always --dirty
Enter fullscreen mode Exit fullscreen mode

because :

  • if on the exact commit with the annotated tag, returns only the tag value (eg 0.1.0)
  • if after an annotated tag, returns <last_annotated_tag>-<number_of_commit>-g<last_commit_short_hash> (eg 0.1.0-2-ge453fae)
  • if no annotated tag, then output the short commit-hash (so never empty output, always a (non-semantic) version)
  • if uncommitted change in the working tree then append -dirty (ignore untracked and ignored files)
❯ git init
❯ touch README.md
❯ git add README.md
❯ git commit -m "add README"
[development (root-commit) 8edcbdc] add README
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md
❯ git describe --always --dirty
8edcbdc

❯ git tag -a "0.1.0" -m ":bookmark: 0.1.0"
❯ git describe --always --dirty
0.1.0

❯ echo "toto" > README.md
❯ git describe --always --dirty
0.1.0-dirty

❯ git commit -a -m "modify README"
[development 6cfe247] modify README
 1 file changed, 1 insertion(+)
❯ git describe --always --dirty
0.1.0-1-g6cfe247

❯ echo "toto 33" > README.md
❯ git describe --always --dirty
0.1.0-1-g6cfe247-dirty
Enter fullscreen mode Exit fullscreen mode

So you have an unique version number for any state of your working tree, and more human friendly than only the commit hash.

If using a build tool that can compute version number (instead of declarative like in a toml or xml) you can use this command to always have the unique and current version. By example for Gradle's based build, you can define the version as :

# build.gradle

version = "git --no-pager describe --always --dirty".execute().text.trim()
Enter fullscreen mode Exit fullscreen mode

Tips

Create the first annotated tag as soon as possible, maybe after your initial commit you can tag the version 0.0.0 if you don't know when 0.1.0 will be created.

Annotated tags can be push with commits in a single command via

git push --follow-tags
Enter fullscreen mode Exit fullscreen mode

Last words

In fact their is no opposition, you can stop using v prefix without using annotated tag OR using the v prefix with annotated tag is possible, but the better combinason (IMHO) is to not use v prefix and annotated tag for version.

Top comments (0)