[git] What is the Git equivalent for revision number?

We use SVN at work, but for my personal projects I decided to use Git. So I installed Git yesterday, and I wonder what is the revision number equivalent in Git.

Let's say we work on version 3.0.8 and every bug fix has its own revision number we can use when we talk about this bug fix. So if I tag the code in Git to 3.0.8 what then I can use as a revision number or some other more detailed kind of identification? I find the hash not so user friendly for humans.

The answer is


We're using this command to get version and revision from git:

git describe --always --tags --dirty

It returns

  • commit hash as revision when no tagging is used (e.g. gcc7b71f)
  • tag name as version when on a tag (e.g. v2.1.0, used for releases)
  • tag name, revision number since last tag and commit hash when after a tag (e.g. v5.3.0-88-gcc7b71f)
  • same as above plus a "dirty" tag if the working tree has local modifications (e.g. v5.3.0-88-gcc7b71f-dirty)

See also: https://www.git-scm.com/docs/git-describe#Documentation/git-describe.txt


If you're interested, I managed version numbers automatically from git infos here under the format

<major>.<minor>.<patch>-b<build>

where build is the total number of commits. You'll see the interesting code in the Makefile. Here is the relevant part to access the different part of the version number:

LAST_TAG_COMMIT = $(shell git rev-list --tags --max-count=1)
LAST_TAG = $(shell git describe --tags $(LAST_TAG_COMMIT) )
TAG_PREFIX = "latex-tutorial-v"

VERSION  = $(shell head VERSION)
# OR try to guess directly from the last git tag
#VERSION    = $(shell  git describe --tags $(LAST_TAG_COMMIT) | sed "s/^$(TAG_PREFIX)//")
MAJOR      = $(shell echo $(VERSION) | sed "s/^\([0-9]*\).*/\1/")
MINOR      = $(shell echo $(VERSION) | sed "s/[0-9]*\.\([0-9]*\).*/\1/")
PATCH      = $(shell echo $(VERSION) | sed "s/[0-9]*\.[0-9]*\.\([0-9]*\).*/\1/")
# total number of commits       
BUILD      = $(shell git log --oneline | wc -l | sed -e "s/[ \t]*//g")

#REVISION   = $(shell git rev-list $(LAST_TAG).. --count)
#ROOTDIR    = $(shell git rev-parse --show-toplevel)
NEXT_MAJOR_VERSION = $(shell expr $(MAJOR) + 1).0.0-b$(BUILD)
NEXT_MINOR_VERSION = $(MAJOR).$(shell expr $(MINOR) + 1).0-b$(BUILD)
NEXT_PATCH_VERSION = $(MAJOR).$(MINOR).$(shell expr $(PATCH) + 1)-b$(BUILD)

Consider to use

git-rev-label

Gives information about Git repository revision in format like master-c73-gabc6bec. Can fill template string or file with environment variables and information from Git. Useful to provide information about version of the program: branch, tag, commit hash, commits count, dirty status, date and time. One of the most useful things is count of commits, not taking into account merged branches - only first parent.


The problem with using the git hash as the build number is that it's not monotonically increasing. OSGi suggests using a time-stamp for the build number. It looks like the number of commits to the branch could be used in place of the subversion or perforce change number.


A Bash function:

git_rev ()
{
    d=`date +%Y%m%d`
    c=`git rev-list --full-history --all --abbrev-commit | wc -l | sed -e 's/^ *//'`
    h=`git rev-list --full-history --all --abbrev-commit | head -1`
    echo ${c}:${h}:${d}
}

outputs something like

$ git_rev
2:0f8e14e:20130220

That is

commit_count:last_abbrev_commit:date_YYmmdd

Each commit has a unique hash. Other than that there are no revision numbers in git. You'll have to tag commits yourself if you want more user-friendliness.


Along with the SHA-1 id of the commit, date and time of the server time would have helped?

Something like this:

commit happened at 11:30:25 on 19 aug 2013 would show as 6886bbb7be18e63fc4be68ba41917b48f02e09d7_19aug2013_113025


I wrote some PowerShell utilities for retrieving version information from Git and simplifying tagging

functions: Get-LastVersion, Get-Revision, Get-NextMajorVersion, Get-NextMinorVersion, TagNextMajorVersion, TagNextMinorVersion:

# Returns the last version by analysing existing tags,
# assumes an initial tag is present, and
# assumes tags are named v{major}.{minor}.[{revision}]
#
function Get-LastVersion(){
  $lastTagCommit = git rev-list --tags --max-count=1
  $lastTag = git describe --tags $lastTagCommit
  $tagPrefix = "v"
  $versionString = $lastTag -replace "$tagPrefix", ""
  Write-Host -NoNewline "last tagged commit "
  Write-Host -NoNewline -ForegroundColor "yellow" $lastTag
  Write-Host -NoNewline " revision "
  Write-Host -ForegroundColor "yellow" "$lastTagCommit"
  [reflection.assembly]::LoadWithPartialName("System.Version")

  $version = New-Object System.Version($versionString)
  return $version;
}

# Returns current revision by counting the number of commits to HEAD
function Get-Revision(){
   $lastTagCommit = git rev-list HEAD
   $revs  = git rev-list $lastTagCommit |  Measure-Object -Line
   return $revs.Lines
}

# Returns the next major version {major}.{minor}.{revision}
function Get-NextMajorVersion(){
    $version = Get-LastVersion;
    [reflection.assembly]::LoadWithPartialName("System.Version")
    [int] $major = $version.Major+1;
    $rev = Get-Revision
    $nextMajor = New-Object System.Version($major, 0, $rev);
    return $nextMajor;
}

# Returns the next minor version {major}.{minor}.{revision}
function Get-NextMinorVersion(){
    $version = Get-LastVersion;
    [reflection.assembly]::LoadWithPartialName("System.Version")
    [int] $minor = $version.Minor+1;
    $rev = Get-Revision
    $next = New-Object System.Version($version.Major, $minor, $rev);
    return $next;
}

# Creates a tag with the next minor version
function TagNextMinorVersion($tagMessage){
    $version = Get-NextMinorVersion;
    $tagName = "v{0}" -f "$version".Trim();
    Write-Host -NoNewline "Tagging next minor version to ";
    Write-Host -ForegroundColor DarkYellow "$tagName";
    git tag -a $tagName -m $tagMessage
}

# Creates a tag with the next major version (minor version starts again at 0)
function TagNextMajorVersion($tagMessage){
    $version = Get-NextMajorVersion;
    $tagName = "v{0}" -f "$version".Trim();
    Write-Host -NoNewline "Tagging next majo version to ";
    Write-Host -ForegroundColor DarkYellow "$tagName";
    git tag -a $tagName -m $tagMessage
}

This is what I did in my makefile based on others solutions. Note not only does this give your code a revision number, it also appends the hash which allows you to recreate the release.

# Set the source control revision similar to subversion to use in 'c'
# files as a define.
# You must build in the master branch otherwise the build branch will
# be prepended to the revision and/or "dirty" appended. This is to
# clearly ID developer builds.
REPO_REVISION_:=$(shell git rev-list HEAD --count)
BUILD_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
BUILD_REV_ID:=$(shell git rev-parse HEAD)
BUILD_REV_ID_SHORT:=$(shell git describe --long --tags --dirty --always)
ifeq ($(BUILD_BRANCH), master)
REPO_REVISION:=$(REPO_REVISION_)_g$(BUILD_REV_ID_SHORT)
else
REPO_REVISION:=$(BUILD_BRANCH)_$(REPO_REVISION_)_r$(BUILD_REV_ID_SHORT)
endif
export REPO_REVISION
export BUILD_BRANCH
export BUILD_REV_ID

With modern Git (1.8.3.4 in my case) and not using branches you can do:

$ git rev-list --count HEAD
68

Git does not have the same concept of revision numbers as subversion. Instead each given snapshot made with a commit is tagged by a SHA1 checksum. Why? There are several problems with a running revno in a distributed version control system:

First, since development is not linear at all, the attachment of a number is rather hard as a problem to solve in a way which will satisfy your need as a programmer. Trying to fix this by adding a number might quickly become problematic when the number does not behave as you expect.

Second, revision numbers may be generated on different machines. This makes synchronization of numbers much harder - especially since connectivity is one-way; you may not even have access to all machines that has the repository.

Third, in git, somewhat pioneered by the now defunct OpenCM system, the identity of a commit (what the commit is) is equivalent to its name (the SHA id). This naming = identity concept is very strong. When you sit with a commit name in hand it also identifies the commit in an unforgeable way. This in turn lets you check all of your commits back to the first initial one for corruption with the git fsck command.

Now, since we have a DAG (Directed Acyclic Graph) of revisions and these constitute the current tree, we need some tools to solve your problem: How do we discriminate different versions. First, you can omit part of the hash if a given prefix, 1516bd say, uniquely identifies your commit. But this is also rather contrived. Instead, the trick is to use tags and or branches. A tag or branch is akin to a "yellow stick it note" you attach to a given commit SHA1-id. Tags are, in essence, meant to be non-moving whereas a branch will move when new commits are made to its HEAD. There are ways to refer to a commit around a tag or branch, see the man page of git-rev-parse.

Usually, if you need to work on a specific piece of code, that piece is undergoing changes and should as such be a branch with a saying topic name. Creating lots of branches (20-30 per programmer is not unheard of, with some 4-5 published for others to work on) is the trick for effective git. Every piece of work should start as its own branch and then be merged in when it is tested. Unpublished branches can be rewritten entirely and this part of destroying history is a force of git.

When the change is accepted into master it somewhat freezes and becomes archeology. At that point, you can tag it, but more often a reference to the particular commit is made in a bug tracker or issue tracker via the sha1 sum. Tags tend to be reserved for version bumps and branch points for maintenance branches (for old versions).


Post build event for Visual Studio

echo  >RevisionNumber.cs static class Git { public static int RevisionNumber =
git  >>RevisionNumber.cs rev-list --count HEAD
echo >>RevisionNumber.cs ; }

The git describe command creates a slightly more human readable name that refers to a specific commit. For example, from the documentation:

With something like git.git current tree, I get:

[torvalds@g5 git]$ git describe parent
v1.0.4-14-g2414721

i.e. the current head of my "parent" branch is based on v1.0.4, but since it has a few commits on top of that, describe has added the number of additional commits ("14") and an abbreviated object name for the commit itself ("2414721") at the end.

As long as you use sensibly named tags to tag particular releases, this can be considered to be roughly equivalent to a SVN "revision number".


I'd just like to note another possible approach - and that is by using git git-notes(1), in existence since v 1.6.6 (Note to Self - Git) (I'm using git version 1.7.9.5).

Basically, I used git svn to clone an SVN repository with linear history (no standard layout, no branches, no tags), and I wanted to compare revision numbers in the cloned git repository. This git clone doesn't have tags by default, so I cannot use git describe. The strategy here likely would work only for linear history - not sure how it would turn out with merges etc.; but here is the basic strategy:

  • Ask git rev-list for list of all commit history
    • Since rev-list is by default in "reverse chronological order", we'd use its --reverse switch to get list of commits sorted by oldest first
  • Use bash shell to
    • increase a counter variable on each commit as a revision counter,
    • generate and add a "temporary" git note for each commit
  • Then, browse the log by using git log with --notes, which will also dump a commit's note, which in this case would be the "revision number"
  • When done, erase the temporary notes (NB: I'm not sure if these notes are committed or not; they don't really show in git status)

First, let's note that git has a default location of notes - but you can also specify a ref(erence) for notes - which would store them in a different directory under .git; for instance, while in a git repo folder, you can call git notes get-ref to see what directory that will be:

$ git notes get-ref
refs/notes/commits
$ git notes --ref=whatever get-ref
refs/notes/whatever

The thing to be noted is that if you notes add with a --ref, you must also afterwards use that reference again - otherwise you may get errors like "No note found for object XXX...".

For this example, I have chosen to call the ref of the notes "linrev" (for linear revision) - this also means it is not likely the procedure will interfere with already existing notes. I am also using the --git-dir switch, since being a git newbie, I had some problems understanding it - so I'd like to "remember for later" :); and I also use --no-pager to suppress spawning of less when using git log.

So, assuming you're in a directory, with a subfolder myrepo_git which is a git repository; one could do:

### check for already existing notes:

$ git --git-dir=./myrepo_git/.git notes show
# error: No note found for object 04051f98ece25cff67e62d13c548dacbee6c1e33.
$ git --git-dir=./myrepo_git/.git notes --ref=linrev show
# error: No note found for object 04051f98ece25cff67e62d13c548dacbee6c1e33.

### iterate through rev-list three, oldest first,
### create a cmdline adding a revision count as note to each revision

$ ix=0; for ih in $(git --git-dir=./myrepo_git/.git rev-list --reverse HEAD); do \
  TCMD="git --git-dir=./myrepo_git/.git notes --ref linrev"; \
  TCMD="$TCMD add $ih -m \"(r$((++ix)))\""; \
  echo "$TCMD"; \
  eval "$TCMD"; \
done

# git --git-dir=./myrepo_git/.git notes --ref linrev add 6886bbb7be18e63fc4be68ba41917b48f02e09d7 -m "(r1)"
# git --git-dir=./myrepo_git/.git notes --ref linrev add f34910dbeeee33a40806d29dd956062d6ab3ad97 -m "(r2)"
# ...
# git --git-dir=./myrepo_git/.git notes --ref linrev add 04051f98ece25cff67e62d13c548dacbee6c1e33 -m "(r15)"

### check status - adding notes seem to not affect it:

$ cd myrepo_git/
$ git status
# # On branch master
# nothing to commit (working directory clean)
$ cd ../

### check notes again:

$ git --git-dir=./myrepo_git/.git notes show
# error: No note found for object 04051f98ece25cff67e62d13c548dacbee6c1e33.
$ git --git-dir=./myrepo_git/.git notes --ref=linrev show
# (r15)

### note is saved - now let's issue a `git log` command, using a format string and notes:

$ git --git-dir=./myrepo_git/.git --no-pager log --notes=linrev --format=format:"%h: %an: %ad:  >>%s<< %N" HEAD
# 04051f9: _user_: Sun Apr 21 18:29:02 2013 +0000:  >>test message 15 << (r15)
# 77f3902: _user_: Sun Apr 21 18:29:00 2013 +0000:  >>test message 14<< (r14)
# ...
# 6886bbb: _user_: Sun Apr 21 17:11:52 2013 +0000:  >>initial test message 1<< (r1)

### test git log with range:

$ git --git-dir=./myrepo_git/.git --no-pager log --notes=linrev --format=format:"%h: %an: %ad:  >>%s<< %N" HEAD^..HEAD
# 04051f9: _user_: Sun Apr 21 18:29:02 2013 +0000:  >>test message 15 << (r15)

### erase notes - again must iterate through rev-list

$ ix=0; for ih in $(git --git-dir=./myrepo_git/.git rev-list --reverse HEAD); do \
  TCMD="git --git-dir=./myrepo_git/.git notes --ref linrev"; \
  TCMD="$TCMD remove $ih"; \
  echo "$TCMD"; \
  eval "$TCMD"; \
done
# git --git-dir=./myrepo_git/.git notes --ref linrev remove 6886bbb7be18e63fc4be68ba41917b48f02e09d7
# Removing note for object 6886bbb7be18e63fc4be68ba41917b48f02e09d7
# git --git-dir=./myrepo_git/.git notes --ref linrev remove f34910dbeeee33a40806d29dd956062d6ab3ad97
# Removing note for object f34910dbeeee33a40806d29dd956062d6ab3ad97
# ...
# git --git-dir=./myrepo_git/.git notes --ref linrev remove 04051f98ece25cff67e62d13c548dacbee6c1e33
# Removing note for object 04051f98ece25cff67e62d13c548dacbee6c1e33

### check notes again:

$ git --git-dir=./myrepo_git/.git notes show
# error: No note found for object 04051f98ece25cff67e62d13c548dacbee6c1e33.
$ git --git-dir=./myrepo_git/.git notes --ref=linrev show
# error: No note found for object 04051f98ece25cff67e62d13c548dacbee6c1e33.

So, at least in my specific case of fully linear history with no branches, the revision numbers seem to match with this approach - and additionally, it seems that this approach will allow using git log with revision ranges, while still getting the right revision numbers - YMMV with a different context, though...

Hope this helps someone,
Cheers!


EDIT: Ok, here it is a bit easier, with git aliases for the above loops, called setlinrev and unsetlinrev; when in your git repository folder, do (Note the nasty bash escaping, see also #16136745 - Add a Git alias containing a semicolon):

cat >> .git/config <<"EOF"
[alias]
  setlinrev = "!bash -c 'ix=0; for ih in $(git rev-list --reverse HEAD); do \n\
      TCMD=\"git notes --ref linrev\"; \n\
      TCMD=\"$TCMD add $ih -m \\\"(r\\$((++ix)))\\\"\"; \n\
      #echo \"$TCMD\"; \n\
      eval \"$TCMD\"; \n\
    done; \n\
    echo \"Linear revision notes are set.\" '"

  unsetlinrev = "!bash -c 'ix=0; for ih in $(git rev-list --reverse HEAD); do \n\
      TCMD=\"git notes --ref linrev\"; \n\
      TCMD=\"$TCMD remove $ih\"; \n\
      #echo \"$TCMD\"; \n\
      eval \"$TCMD 2>/dev/null\"; \n\
    done; \n\
    echo \"Linear revision notes are unset.\" '"
EOF

... so you can simply invoke git setlinrev before trying to do log involving linear revision notes; and git unsetlinrev to delete those notes when you're done; an example from inside the git repo directory:

$ git log --notes=linrev --format=format:"%h: %an: %ad:  >>%s<< %N" HEAD^..HEAD
04051f9: _user_: Sun Apr 21 18:29:02 2013 +0000:  >>test message 15 <<

$ git setlinrev
Linear revision notes are set.
$ git log --notes=linrev --format=format:"%h: %an: %ad:  >>%s<< %N" HEAD^..HEAD
04051f9: _user_: Sun Apr 21 18:29:02 2013 +0000:  >>test message 15 << (r15)
$ git unsetlinrev
Linear revision notes are unset.

$ git log --notes=linrev --format=format:"%h: %an: %ad:  >>%s<< %N" HEAD^..HEAD
04051f9: _user_: Sun Apr 21 18:29:02 2013 +0000:  >>test message 15 <<

The time it would take the shell to complete these aliases, would depend on the size of the repository history.


For people who have an Ant build process, you can generate a version number for a project on git with this target:

<target name="generate-version">

    <exec executable="git" outputproperty="version.revisions">
        <arg value="log"/>
        <arg value="--oneline"/>
    </exec>

    <resourcecount property="version.revision" count="0" when="eq">
        <tokens>
            <concat>
                <filterchain>
                    <tokenfilter>
                        <stringtokenizer delims="\r" />
                    </tokenfilter>
                </filterchain>
            <propertyresource name="version.revisions" />
            </concat>
        </tokens>
    </resourcecount>
    <echo>Revision : ${version.revision}</echo>

    <exec executable="git" outputproperty="version.hash">
        <arg value="rev-parse"/>
        <arg value="--short"/>
        <arg value="HEAD"/>
    </exec>
    <echo>Hash : ${version.hash}</echo>


    <exec executable="git" outputproperty="version.branch">
        <arg value="rev-parse"/>
        <arg value="--abbrev-ref"/>
        <arg value="HEAD"/>
    </exec>
    <echo>Branch : ${version.branch}</echo>

    <exec executable="git" outputproperty="version.diff">
        <arg value="diff"/>
    </exec>

    <condition property="version.dirty" value="" else="-dirty">
        <equals arg1="${version.diff}" arg2=""/>
    </condition>

    <tstamp>
        <format property="version.date" pattern="yyyy-mm-dd.HH:mm:ss" locale="en,US"/>
    </tstamp>
    <echo>Date : ${version.date}</echo>

    <property name="version" value="${version.revision}.${version.hash}.${version.branch}${version.dirty}.${version.date}" />

    <echo>Version : ${version}</echo>

    <echo file="version.properties" append="false">version = ${version}</echo>

</target>

The result looks like this:

generate-version:
    [echo] Generate version
    [echo] Revision : 47
    [echo] Hash : 2af0b99
    [echo] Branch : master
    [echo] Date : 2015-04-20.15:04:03
    [echo] Version : 47.2af0b99.master-dirty.2015-04-20.15:04:03

The dirty flag is here when you have file(s) not committed when you generate the version number. Because usually, when you build/package your application, every code modification has to be in the repository.


The other posters are right, there is no "revision-number".

I think the best way is to use Tags for "releases"!

But I made use of the following to fake revision numbers (just for clients to see revisions and the progress, as they wanted to have the same increasing revisions from git as they where use to by subversion).

Show the "current revision" of "HEAD" is simulated by using this:

git rev-list HEAD | wc -l

But what if the client tells me that there is a bug in "revision" 1302 ?

For this I added the following to the [alias] section of my ~/.gitconfig:

show-rev-number = !sh -c 'git rev-list --reverse HEAD | nl | awk \"{ if(\\$1 == "$0") { print \\$2 }}\"'

using git show-rev-number 1302 will then print the hash for the "revision" :)

I made a Blog Post (in german) about that "technique" some time ago.


The SHA1 hash of the commit is the equivalent to a Subversion revision number.


From the Git manual, tags are a brilliant answer to this issue:

Creating an annotated tag in Git is simple. The easiest way is to specify -a when you run the tag command:

$ git tag -a v1.4 -m 'my version 1.4'

$ git tag
v0.1
v1.3
v1.4

Check out 2.6 Git Basics - Tagging


Examples related to git

Does the target directory for a git clone have to match the repo name? Git fatal: protocol 'https' is not supported Git is not working after macOS Update (xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools) git clone: Authentication failed for <URL> destination path already exists and is not an empty directory SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443 GitLab remote: HTTP Basic: Access denied and fatal Authentication How can I switch to another branch in git? VS 2017 Git Local Commit DB.lock error on every commit How to remove an unpushed outgoing commit in Visual Studio?

Examples related to version-control

How can I switch to another branch in git? Do I commit the package-lock.json file created by npm 5? Project vs Repository in GitHub Remove a modified file from pull request Git push: "fatal 'origin' does not appear to be a git repository - fatal Could not read from remote repository." Git: How to squash all commits on branch git: updates were rejected because the remote contains work that you do not have locally Sourcetree - undo unpushed commits Cannot checkout, file is unmerged Git diff between current branch and master but not including unmerged master commits

Examples related to revision-history

Go to particular revision What is the Git equivalent for revision number? How to view file history in Git? Unable to show a Git tree in terminal

Examples related to version-numbering

What is the Git equivalent for revision number?