Git – Checkout a remote tag when two remotes have the same tag name

Executive summary: what you want to achieve is possible, but first you must invent remote tags.

You do this with a series of refspecs, one for each remote. The rest of this is about what these are, how they work, and so on.


Your question asks about checking out a “remote tag”, but Git does not have remote tags, and this:

but I’m doing something weird where I have a lot of remotes, and I’m worried about what happens if two remotes have the same tag. Is there a way to specify the remote when checking out the tag?

reveals (I think) the source of your confusion.

Let’s back up for a moment and just talk about what Git has in a general sense, which are “references”. To help cement the idea, specific forms of references include your local branch names (master, devel, feature, and so on), “remote branch names” like origin/master and stuff_from_bobs_computer/master, and tag names. Things like Git’s “stash” also use references, and even HEAD is a reference, though it’s a very special one, and usually a “symbolic” reference. The point here is that Git has lots of forms of references, and they all really work the same way in the end: a reference name resolves, in the end, to one of those big SHA-1 values, 676699a0e0cdfd97521f3524c763222f1c30a094 or whatever.

Most references—the exceptions are things like HEAD, ORIG_HEAD, MERGE_HEAD, and a few others along those lines—are actually spelled with names that start with refs/. These are kept in a sort of directory- or folder-like structure,1 with sub-directories: refs/tags/ contains your tags,2 refs/heads/ contains all your branches, and refs/remotes/ contains all your remote branches.

The remote branches are further subdivided by the name of the remote: refs/remotes/origin/ contains all the origin remote-branches, while refs/remotes/stuff_from_bobs_computer/ contains all the stuff_from_bobs_computer remote-branches. If you have a lot of remotes, you have a lot of sub-directories inside refs/remotes/.

I just mentioned that your tags are all in refs/tags/. What about the remotes’ tags, all the tags on all the various remotes? Well, again, git doesn’t have “remote tags”. Git does have “remote branches”, but those are in fact all local. They are stored in your repository, under the refs/remotes/ heading.

When your Git contacts a “remote”—usually through git fetch remote, but also for push (and the initial clone step, for that matter), your Git asks the remote Git3 the question: “What local branches do you have? What are their SHA-1 values?” This is, in fact, how fetch works: as a simplified description, the process of fetching consists of asking the remote Git “hey, whaddaya got?” and it gives you a set of names and SHA-1s. Your Git then checks to see if it has the same SHA-1s. If so, the conversation is done; if not, your Git then says “ok, I need whatever’s in the commit(s) for these SHA-1s”, which actually turns out to be another bunch of SHA-1s, and your Git and theirs talk it over to figure out which files and such you need as well, all identified by SHA-1s. Your Git brings over those objects, and stuffs the new SHA-1s into your refs/remotes/, under the name of the remote and then under their local branch names.

If you ask for tags with your fetch, your Git does a bit more.4 Instead of just asking their Git about their branches, your Git also asks theirs about their tags as well. Again, their Git just gives you a list of names and SHA-1s. Your Git then brings over any underlying objects needed, and then—here’s the key to the whole problem—it writes their tag names into your refs/tags/.

So, what happens when you go over to remote origin and ask it for tags, and it says “I have refs/tags/pinky and refs/tags/brain“, is that this creates, for you, local tags pinky and brain, also named refs/tags/pinky and refs/tags/brain in your reference name-space.

Now you go over to Bob’s computer (the remote named stuff_from_bobs_computer above) and ask it for tags. He’s into neurology, rather than Warner Brothers and Sister, and his tags are refs/tags/spinal_cord and refs/tags/brain, and the second one is probably not related to the one on origin. Uh oh!

Exactly what happens here gets a little bit complicated,5 but in short, this is just a bad situation and you should probably avoid it if possible. There are two easy (well…) ways to avoid it. One, with obvious drawback, is: just don’t get their tags. Then you won’t have any tag conflicts. The other is: keep all their tags separated from each other (and maybe from yours as well). It turns out that the second one is not really that difficult. You just have to “invent” remote tags.

Let’s take a quick side look at how Git actually implements “remote branches”, and how fetch --tags works. They both use the same basic mechanism, what git calls “refspecs”.

In its simplest form a refspec just looks like two ref names with a colon between them: refs/heads/master:refs/heads/master, for instance. In fact, you can even leave out the refs/heads/ and Git will put it in for you,6 and sometimes you can leave out the colon and the repeated name as well. This is the kind of thing you use with git push: git push origin branch means to push to origin, using your refs/heads/branch, and call it refs/heads/branch when it arrives on “their” Git as well.

For fetch, though, doing remote branches, you get a refspec that looks like this:

+refs/heads/*:refs/remotes/origin/*

The + at the front means “force”, and the *s do the obvious thing. Your Git talks to theirs and gets a list of refs. Those that match refs/heads/*, yours brings over (along with their repository objects as needed)—but then it sticks them in your repo under names staring with refs/remotes/origin/, and now you have all the “remote branches” from origin.7

When you run git fetch --tags, your git adds +refs/tags/*:refs/tags/* to the refspecs it uses.8 That brings their tags over and puts them in your local tags. So all you have to do is give fetch a refspec that looks like:

+refs/tags/*:refs/rtags/origin/*

and suddenly you’ll have a whole new name-space of “remote tags” under refs/rtags/ (for origin only, in this case). It’s safe to use the + force-flag here since you are just updating your copy of their tags: if they’ve force-moved (or deleted and re-created) a tag, you force-move your copy. You may also want or even need --no-tags behavior, which you can get by specifying --no-tags on the command line, or, well, see the next paragraph.

The only remaining handy item to know is that git fetch gets its default refspecs, for any given remote, from the Git config file.9 If you examine your Git config file, you’ll see a fetch = line under each remote, using the +refs/heads/*:refs/remotes/remote-name/* string. You can have as many fetch = lines as you like per remote, so you can add one to bring over their tags, but put them in your newly-(re)invented “remote tags” name-space. You may also want to make --no-tags the default for this remote by setting tagOpt = --no-tags in this same section. See this comment by user200783 for details.

As with all Git commands that resolve a name to a raw SHA-1, you can then git checkout by full ref-name to get into “detached HEAD” mode on the corresponding SHA-1:

git checkout refs/rtag/stuff_from_bobs_computer/spinal_cord

Because Git does not have the idea of “remote tags” built in, you have to spell out the long form (see gitrevisions for details).


1In fact, it’s a real directory, in .git/refs. However, there’s also a “packed” form for refs, that wind up in .git/packed-refs. The packed form is meant to save time and effort with refs that don’t change often (or at all, as is common with tags). There is also an ongoing effort to rewrite the “back end” storage system for references, so at some point a lot of this may change. This change is needed for Windows and Mac systems. Git believes that branch and tag names are case-sensitive: that you can have branch polish for your shoeshine material, and Polish for your sausages. The packed versions are case-sensitive, so this works; but the stored-in-files versions sometimes aren’t, so it doesn’t!

2I’m glossing over the difference between lightweight and annotated tags here. Annotated tags are actual objects in the repository, while lightweight tags are labels in the refs/tags/ space. However, in general, each annotated tag has one corresponding lightweight tag, so for this particular usage, they work out the same.

3It’s almost always another Git repo, although there are now adapters for Git to Mercurial, svn, and so on. They have their own tricks for pretending to be Git repos. Also, this description is not meant to be definitive: the actual sequence of operations is coded for transfer efficiency, rather than for making-sense-to-humans.

4I’ve glossed over a bit of special weirdness about plain fetch and clone here, i.e., the versions without --tags. The versions with --tags are easy to explain: they bring over all tags using the refspecs I’ve described here—and, at least in Git 2.10 and 2.11, --tags also does forced-updates, as if the + force flag were set. But unless you explicitly call for --no-tags, a plain fetch (and clone) brings over some tags. The sneaky thing it does is to look for tags that correspond to objects that are coming in due to the fetch, and it adds those (without forcing updates) to your (global) tags name-space. Without --tags your Git won’t overwrite your own existing tags; with --tags, your Git will overwrite your own existing tags, at least in Git 2.10, per actual experiments performed in early 2017.

5Older versions of Git applied “branch” rules to tags during push (but not necessarily fetch), allowing a tag update if it was a fast-forward, and otherwise requiring the force flag. Newer version of git push just require the force-tag. The fetch refspec from --tags does not have the force flag set, yet acts as though it does. I have not experimented with push with --tags. There’s one more special git fetch weirdness about --tags vs --no-tags vs explicit refspecs, having to do with how --prune works. The documentation says that --prune applies to any explicit command-line refs/tags/ refspecs, but not to the implicit --tags refspec. I have not experimented to verify this, either.

6For your Git to fill in refs/heads/ or refs/tags/ for you, your Git has to be able to figure out which one you meant. There are some cases where it does, and some where it doesn’t. If your Git fails to figure it out, you’ll get an error message, and can try again with it filled in—but in scripts you should always fill it in explicitly, to get more-predictable behavior. If you are just running git push to push an existing branch, you can almost always let your Git figure it out.

7Leaving out the colon and the second name does not work so well for git fetch: it tells your Git not to update your own references at all! This seems senseless, but actually can be useful, because git fetch always writes the special file FETCH_HEAD. You can fish the Git object IDs (SHA-1s) out of the special file and see what got fetched. This is mostly a holdover from very early versions of Git, before remote-tracking branches were invented.

8The refspec that git fetch --tags and git push --tags uses is pre-compiled internally, in Git version 2.10, and handled by some special case code. The pre-compiled form does not have the + flag set; yet experimentation shows that fetched tags are force-updated in Git 2.10/2.11. I recall experimenting years ago with Git 1.x, and finding that these --tags-fetched tags were not force-updated, so I think this has changed, but that may be just faulty memory. In any case, if you are (re)inventing remote tags, you most likely do not want to use an explicit --tags.

9In fact, this is how mirrors work. For instance, with fetch = +*:* you get a pure fetch mirror. The fetch process can see all refs. You can see them yourself with git ls-remote. It’s also how --single-branch works: if you use --single-branch during cloning, your Git config file will list only the one single branch in the fetch line. To convert from single-branch to all-branch, simply edit the line to contain the usual glob-pattern entry.

Leave a Comment