views:

1185

answers:

1

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.

+3  A: 

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.

Charles Bailey
You're right on the money with this - tag objects behave as I'd expect, and refuse to move. I think they may be suitable for atomic resource allocation. Thank you very much for answering.
meowsqueak
One last comment - although annotated tags cannot be moved forward, it's still possible to reapply an existing annotated tag if it's on the same commit. So there's still a small window where both processes choose the same tag on the same commit and both are able to apply the tag successfully. I will have to add extra logic to catch when this happens and abort one of the processes.
meowsqueak
If I've understood correctly then this sounds like to me like a bug. I think that git *should* refuse a tag update to a new tag object even if the tag object references the same commit.
Charles Bailey
I did some more digging - it will refuse the second process's tag update if the second process thinks it is creating a new tag (i.e. hasn't run git-fetch --tags before pushing the tag). However if it knows the tag exists on the remote (push following a fetch), then it seems to allow the update.In my system, I allow for this by ensuring that I create these unique tags "blind" - i.e. create and push them without checking if they exist in-between. In practical terms this means avoiding a git-fetch --tags in the middle of a 'git tag ...' and 'git push origin tag ...' sequence.
meowsqueak