why does git allow remote tags to move, or why you can't use git-tag for atomic test-and-set

I have a problem where two similar processes are running in parallel within separate clones of the same repository (typically on different computers). Each time a process runs, it fetches the latest tags from the remote and then deduces a unique number based on the tags it sees.

E.g. if these tags exist on the remote: 1.0 1.1 1.2 1.3
then a process will choose 1.4 as the next number.

Before the process starts, it creates a new tag and pushes this back to the remote:

$ git tag 1.4 HEAD
$ git push origin tag 1.4

The idea was that this is a way to atomically select numbers. The other process, if it’s looking at the same time, might also decide to use 1.4, but when it comes to push it’s tag, it should discover that 1.4 already exists, and choose 1.5 instead (and try again).

My hope was that I could treat git tag pushes as atomic.

Unfortunately, for some weird reason, git allows remote tags to move in certain circumstances!

For example, let’s say tag 1.4 has been put on origin/master and pushed. The other process wants to put tag 1.4 on, say, origin/master^, which would involve moving the tag backwards. Git will reject this with a ‘non-fast-forward’ error:

Process A:

$ git tag 1.4 origin/master
$ git push origin tag 1.4
Total 0 (delta 0), reused 0 (delta 0)
To /repo1
 * [new tag]         1.4 -> 1.4

Process B:

$ git tag 1.4 origin/master^
$ git push origin tag 1.4
To /repo1
 ! [rejected]        1.4 -> 1.4 (non-fast forward)
error: failed to push some refs to '/repo1'

Ok, that’s fine, Process B can use this to try 1.5 instead.

But consider this situation:

Process A:

$ git tag 1.4 origin/master
$ git push origin tag 1.4
Total 0 (delta 0), reused 0 (delta 0)
To /repo1
 * [new tag]         1.4 -> 1.4

Process B:

$ git tag 1.4 origin/master
$ git push origin tag 1.4
Everything up-to-date

Oh. That’s a shame – git didn’t indicate that this tag already exists on the remote. Actually, it does, with -v:

$ git push origin tag 1.4 -v
Pushing to /repo1
To /repo1
 = [up to date]      1.4 -> 1.4
Everything up-to-date

Ok, so I can do some sort of stderr redirect, search for ” = “, and that will allow Process B to determine that 1.4 is already in use.

But that’s a bit silly. And it gets worse:

Process A:

$ git push origin tag 1.4
Total 0 (delta 0), reused 0 (delta 0)
To /repo1
 * [new tag]         1.4 -> 1.4

Process B:

$ git push origin tag 1.4
Total 0 (delta 0), reused 0 (delta 0)
To /repo1
   fd0e09e..c6cdac9  1.4 -> 1.4

Argg! What? Git has just moved the remote tag without warning!

So it seems to me that remote tags in git are fundamentally broken – they shouldn’t just “move” without an explicit request. More to the point, they should refuse to move by default.

Also, the git-tag command should provide a way to atomically test-and-set a tag.

But clearly it doesn’t. Running git fetch first isn’t going to help because there’s still a window of conflict and even if there is a conflict, in one of the three scenarios the tag simply moves!

What is going on here?

Is there another way to test-and-set a tag?

If not, how do people allocate and reserve build numbers in an automated build environment? How do you reliably detect when two processes have inadvertently picked up the same build number?

Using git 1.6.1.2.

  • Version numbering with SmartGit
  • Maven release plugin - tagbase configuration - tags folder included in project
  • git label groups or stackoverflow type tagging
  • .git/tags file is HUGE
  • Is it possible with Git to retrieve a list of tags that exist only in a certain branch?
  • Building Grails application with bamboo using Git tag
  • “git svn fetch” fails, ls-tree dying because of missing tree object
  • Getting OpenCV2.4.2 with GIT for Windows
  • One Solution collect form web for “why does git allow remote tags to move, or why you can't use git-tag for atomic test-and-set”

    I think that you’re tagging strategy would be best served if you used real tag objects instead of lightweight tags which are more designed as local labels.

    You can create a tag object by specifying one of the -a (or -m/-F), -s or -u options (git help tag).

    Try your example but adding -m "1.4 tag" to every invocation of git tag. Tag objects can’t direct descendants of other tag objects so every push case that you want to fail above should fail.

    Git Baby is a git and github fan, let's start git clone.