145

My organization is considering moving from SVN to Git. One argument against moving is as follows:

How do we do versioning?

We have an SDK distribution based on the NetBeans Platform. As the SVN revisions are simple numbers we can use them to extend the version numbers of our plugins and SDK builds. How do we handle this when we move to Git?

Possible solutions:

  • Using the build number from Hudson (Problem: you have to check Hudson to correlate that to an actual Git version)
  • Manually upping the version for nightly and stable (Problem: Learning curve, human error)

If someone else has encountered a similar problem and solved it, we'd love to hear how.

Tulains Córdova
  • 39,201
  • 12
  • 97
  • 154
Erlend
  • 1,553
  • 3
  • 10
  • 6
  • 4
    Could you get your hudson (not [jenkins](http://en.wikipedia.org/wiki/Jenkins_%28software%29)?) server to automatically add a `git` tag after each successful build? This would have the added advantage that it makes it really clear which `git` commits have build issues or test failures, since they would remain un-tagged. – Mark Booth Mar 29 '12 at 18:39
  • 2
    see http://stackoverflow.com/questions/677436/how-to-get-the-git-commit-count – CharlesB Mar 30 '12 at 21:00
  • As a side note, you can add the build count to the tag by [tracking the build times](http://stackoverflow.com/a/7768075/912144). – Shahbaz Jul 09 '14 at 09:07
  • Not sure if a viable solution, but how about exporting from git to a svn repo right before every build? Then just build from the svn repo - if centralized is what we want, just use that instead. – Jonny Dec 04 '15 at 20:54

6 Answers6

177

Use tags to mark commits with version numbers:

git tag -a v2.5 -m 'Version 2.5'

Push tags upstream—this is not done by default:

git push --tags

Then use the describe command:

git describe --tags --long

This gives you a string of the format:

v2.5-0-gdeadbee
^    ^ ^^
|    | ||
|    | |'-- SHA of HEAD (first seven chars)
|    | '-- "g" is for git
|    '---- number of commits since last tag
|
'--------- last tag
Jon Purdy
  • 20,437
  • 7
  • 63
  • 95
  • Agree - it should be easy to automate nightly tag numbering if you need that, and promotion to stable is manual anyway. – Useless Mar 28 '12 at 19:00
  • 25
    Small improvement: `git describe --long --tags --dirty --always`. 'Dirty' will tell you if there were local changes when the 'describe' was done (meaning it can't fully describe the state of the repo). 'Always' means you won't get an error when there are no tags. It will fallback to just a commit hash. So you can get `76001f2-dirty` as an example. Obviously, seeing 'dirty' means somebody messed up. – Mike Weller Mar 14 '13 at 13:00
  • 3
    How can this work when the tag is generated *last*. Normally you want builds going forward to have the *next* version of your product. But they will always be forced to use the *last* version in this case. Only the final, shipped build will have the proper number. – void.pointer Aug 20 '15 at 21:34
  • @void.pointer: Sure, this version number answers the question “which release was this commit based on?” not “which release will this commit be in?” However, you’re free to interpret tags differently. For example, if you tag `HEAD` as `v2.5`, you can just as well interpret that as the *start* of the 2.5 release cycle, then tag `v2.5-release` or whatever you like. – Jon Purdy Aug 20 '15 at 22:44
  • @JonPurdy Tags are not traditionally used to define a "starting" point, however. In any case, the start will always be the commit tagged as the last release. So I don't see a reason to tag a commit 2 times for this purpose. – void.pointer Aug 20 '15 at 23:23
  • 11
    Another small improvement. If you want to have other tags as well, but use a specifically patterned tag for revision generation, you may use the `--match` option like this: `git describe --long --tags --dirty --always --match 'v[0-9]\.[0-9]'` – Alexander Amelkin Jun 27 '16 at 15:13
  • @void.pointer, this highly depends on how you tag. I understand your point and am familiar with practices where integration and test team tags the source code before shipping it to a customer. In my opinion that doesn't hinder use of this approach. You may still use 'vM.m' styled tags to mark the beginning of development of a new version, and the I&T team can still use something like "Release M.m" tags to tag the release they approved for shipping to a customer. See my previous comment on matching specific tags. – Alexander Amelkin Jun 27 '16 at 15:17
  • This will not work if your build artifact, e.g., RPM package, requires the build number stamped inside. This is because you will need to tag first before you can build. Automated builds are always run on the latest commit (HEAD). You cannot possibly create a tag for every commit. – alvinabad Sep 05 '18 at 23:44
  • @alvinabad Two questions: 1) Why can't you create a tag for every commit? 2) Why is `git describe` insufficient for providing a build number for an untagged commit? – 8bittree May 14 '19 at 20:18
  • @8bittree, 1) you could create a tag for every commit. But why would you do that? A tag is supposed to provide meaningful identification of a commit in history. If you tag every commit then you'll have as many tags as commits. 2) The answer provided suggested to use git describe to retrieve the build number information once the tag has been created. You'll use this to retrive the previous build number to determine the next build number to use. But this won't work if you need to stamp the build# inside a package like RPM, because you'll need to create the tag first before you can run the build. – alvinabad May 19 '19 at 07:54
  • @alvinabad 1) A commit hash uniquely identifies a commit, but carries no information itself. A tag can provide information at a glance. (Is `cafebeef` newer than `12345678`? I'd have to go digging through git to find out. But I can be pretty sure `v2.3.4` is newer than `v2.3.3`, no need to dive into the repo.) 2) Why can you not stamp the `git describe` output as the build# inside the RPM? – 8bittree May 20 '19 at 15:45
  • Today the answer to this question should likely be to use gitversion, which follows the semantic versioning specification. https://gitversion.net/, https://semver.org/ – Queeg Jul 02 '23 at 21:04
56

This has come up on a few projects for me. The best solution I've had so far is to generate a version number like this:

x.y.<number of commits>.r<git-hash>

Typically, it's generated by our build system using a combination of some static file or tag to get the major revision numbers, git rev-list HEAD | wc -l (which was faster than using git log), and git rev-parse HEAD. The reasoning was follows:

  1. We needed the ability to have high-level versioning happen explicitly (i.e. x.y)
  2. When parallel development was happening, we needed to NEVER generate the same version number.
  3. We wanted to easily track down where a version came from.
  4. When parallel lines were merged, we wanted the new version to resolve higher than either of the branches.

Number 2 is invisible to most people, but is really important, and really difficult with distributed source control. SVN helps with this by giving you a single revision number. It turns out that a commit count is as close as you can get, while magically solving #4 as well. In the presence of branches, this is still not unique, in which case we add the hash, which neatly solves #3 as well.

Most of this was to accommodate deploying via Python's pip. This guaranteed that pip install would maybe be a bit odd during parallel development (i.e. packages from people on different branches would intermingle, but in a deterministic fashion), but that after merges, everything sorted out. Barring the presence of an exposed rebase or amend, this worked quite nicely for the above requirements.

In case you're wondering, we chose to put the r in front of the hash due to some weirdness with how Python packaging handles letters in version numbers (i.e. a-e are less than 0, which would make "1.3.10.a1234" < "1.3.10" < "1.3.10.1234").

Jayson
  • 708
  • 5
  • 6
  • 1
    btw, how did you deal with the chicken-egg problem of determining the git-hash before you check it in? Did you use some form of .gitignore or some other trick? – kfmfe04 Nov 16 '12 at 23:58
  • 5
    I didn't. I don't use the hash until package build time, which is long after check-in. Different languages have different ways to inject this. For Python, I use './setup.py egg_info -b ".${BUILD_VERSION}" sdist'. For C and C++, I define a macro at compile time with 'CFLAGS=-D "${BUILD_VERSION}"'. For Go, I define a symbol at link time with 'go install -ldflags appmodule.BuildVersion"-X .${BUILD_VERSION}"'. – Jayson Jun 12 '13 at 23:12
  • 3
    This should be the best answer. – alvinabad Sep 05 '18 at 23:46
  • very good answer – haelix Sep 18 '18 at 15:00
  • How do you handle branches being deleted. Doesn't the number go down then? – MaTePe Sep 18 '20 at 06:51
11

This might be a bit overkill, but I'll let you know how we do it.

We use a branching structure very similar to this.

Hudson builds off our "develop" branches and increments build numbers starting from 0. The build number is unique to each project and gets tagged in version control. The reason is so that you can tell exactly which develop branch build 42 came from, for example (each project can have several develop branches in parallel, because each project can have several teams working on different aspects of the project).

When we decide that a particular build is good enough to be released, the commit that triggered that build gets tagged with a release version number, which is decided by marketing. This means that the dev teams don't care about what the final version number is and marketing is free to shuffle around version numbers as it sees fit. The final version number and build number are both present in the released product.

Example: 2.1.0 build 1337

This means, for a specific product release, you can tell which was the last team to have worked on it and you can query git for all the commits leading up to release to diagnose a problem if you need to.

Carl
  • 822
  • 6
  • 11
10

Versions are identified hashing the SHA1 hashes of all the files in the stored directory tree at the time of checkin. This hash is stored alongside the hashes of the parent checkin(s) so that the full history can be read.

Take a look at the process of using 'git-describe' by way of GIT-VERSION-GEN and how you can add this via your build process when you tag your release.

Here is a nice blog that gives an example of how to get what you want:

http://cd34.com/blog/programming/using-git-to-generate-an-automatic-version-number/

0

Jon Purdy has the right idea. git flow makes the actual management of these branches easy, as well, and branch management is an argument for moving to git.

Let's start with a basic rundown of git, since you're coming from the svn-to-git perspective. Consider in git the following:

master--...............-.....-..............-
        \             /     /              /
         ---develop---------............../
                            \            /
                             --feature---

Above, you branch master to develop (denoted by the \), and branch develop to a feature branch. We merge those branches back up (denoted by /), with commits (-) along a branch. (If there's no commit but the merge is way to the right, there are . indicators to show that the next - is the next commit).

Easy enough. What if we have a hotfix in our main release?

master--...............-.....-................-...........-.........-
        \             /     /                / \         /|        /
         \           /     /                /   -hotfix-- V       /
          ---develop---------............../..............-...----
                             \            / \             V   /
                              --feature---   --feature2...----

Above, develop branched from master. The bug discovered in master was fixed by branching from master, fixing it, and merging back into master. We then merged master into develop, and then develop into feature2, which rolled the new code from hotfix into these branches.

When you merge feature2 back to develop, its history includes develop with the hotfix. Likewise, develop is merged into feature2 with the new code from master, so merging develop back to master will happen without a hitch, as it's based on that commit in master at that time—as if you had branched from master at that point.

So here's another way to do that.

master--..........-........-
        \        /\       /
         ---1.0--  --1.1-- 

Your 1.0 releases get tagged—1.0.1, 1.0.2, 1.0.3, and so forth.

Now here's a trick: you found a bug in 1.0 and it affects 1.1, 1.2, and 1.3. What do you do?

You branch off your latest or earliest maintained release and fix it. Then you merge your new hotfix branch into 1.3—and into 1.2, 1.1, and 1.0. Don't branch from each of the maintenance version branches; don't merge 1.0 into master or merge master back into 1.0. Take the one hotfix branch and merge it into all your version branches. If there are conflicts, it will tell you; review your code to ensure the changes are correct (git diff is your friend).

Now that specific change is applied everywhere. The lineage is branched, but it's okay. It's not haphazard. Tag the 1.3 head as 1.3.17, merge it into every feature-in-progress branched from 1.3, and move on.

The git flow extension helps manage these maintenance, feature, and hotfix branches for you. Once you get the workflow down, this is trivial and takes a huge amount of trouble out of source code management.

I've seen this done on programming teams, but I've not worked that deeply as a programmer myself, so I'm still getting my head around the day-to-day workflow myself.

-4

Pro Git in section 7.2 "Git Attributes" in "Keyword" Expansion part contains a nice example of using smudge&clean filters for generating RCS-style keywords. You can use the same technique for embedding some-version-string into code, formatted and calculated according to your rules. You still can use git describe as a staring point, but you have the possibility to transform to any more appropriate form and get from v2.5-14-feebdaed, for example, clean 2.5.14

CharlesB
  • 142
  • 10
Lazy Badger
  • 1,935
  • 12
  • 16
  • 9
    -1 for ruining a good answer with completely uncalled for ad hominem attacks. – Jörg W Mittag Mar 29 '12 at 00:52
  • 9
    Who's to say it was *git-boys* voting you down. It could easily be people who prefer a little [civility](http://programmers.stackexchange.com/faq#etiquette). – Mark Booth Mar 29 '12 at 18:19
  • FYI, I've just edited the answer. – Keith Thompson Jun 05 '12 at 02:38
  • `git describe` outputs the tag name unless `--long` is passed or there are commits since the last tag, so it's perfectly clean already. If you weren't changing the defaults, it would have given you exactly what you wanted. – strcat Jan 25 '15 at 04:36