Are "git fetch --tags --force" and "git pull <branch>" conmutative operations?

yucer picture yucer · Jan 18, 2017 · Viewed 9.1k times · Source

Normally the git tags are a fixed reference to a commit. But sometimes they are used to mark some event (last-build, base-line, etc..) and they change frequently.

I have an script that refreshes those kind of "floating" tags from the reference repository.

git fetch --tags --force

and also make pull from one branch:

git pull origin <mybranch>

I know that many git users warn about using floating tags, but i am forced to deal with that. My question is:

If the branch is marked by one of those floating tags... does the execution order of the commands matter?

I am afraid that git pull doesn't refresh the tags when they exist locally, and that it might work with the reference of the all tags if it runs first.

git pull has a --force option, but the help section of the option --no-tags explain the default behavior as:

By default, tags that point at objects that are downloaded from the remote repository are fetched and stored locally.

Does it means that the objects should be downloaded first in order to be able to refresh the tags ? In that case git pull should go first.

Which is the correct order?

Answer

torek picture torek · Jan 18, 2017

This gets into one of the more obscure corners of Git, but in the end the answer is "it doesn't matter initially which order you use". However, I'd recommend avoiding git pull in general, and never using it in scripts anyway. Plus, it does matter, in a different way, precisely when you fetch, as we will see below. So I'd recommend running your own git fetch first, then simply not using git pull at all.

git fetch

A plain git fetch (without --tags) uses a weird hybrid tag update by default, although each remote can define a default tag option that overrides this default. The weird hybrid is what you quoted: tags that point at objects that are downloaded from the remote repository are fetched and stored locally. The underlying mechanism for this is a bit tricky and I'll leave that for later.

Adding --tags to the git fetch arguments has almost the same effect as specifying, on the command line, refs/tags/*:refs/tags/*. (We'll see the difference in a moment.) Note that this does not have the force flag set in the refspec, yet testing shows that the fetched tags are force-updated anyway.

Adding --force has the same effect as setting the force flag in each explicit refspec. In other words, git fetch --tags --force is roughly equivalent to running git fetch '+refs/tags/*:refs/tags/*': if the remote has tag refs/tags/foo pointing to commit 1234567..., your Git will replace any existing refs/tags/foo so that you now have your own refs/tags/foo also pointing to commit 1234567.... (But as observed in practice, it does that even with just --tags.)

Note that in all cases, git fetch writes information about what it fetched to the file FETCH_HEAD. For instance:

$ cat .git/FETCH_HEAD
e05806da9ec4aff8adfed142ab2a2b3b02e33c8c        branch 'master' of git://git.kernel.org/pub/scm/git/git
a274e0a036ea886a31f8b216564ab1b4a3142f6c    not-for-merge   branch 'maint' of git://git.kernel.org/pub/scm/git/git
c69c2f50cfc0dcd4bcd014c7fd56e344a7c5522f    not-for-merge   branch 'next' of git://git.kernel.org/pub/scm/git/git
4e24a51e4d5c19f3fb16d09634811f5c26922c01    not-for-merge   branch 'pu' of git://git.kernel.org/pub/scm/git/git
2135c1c06eeb728901f96ac403a8af10e6145065    not-for-merge   branch 'todo' of git://git.kernel.org/pub/scm/git/git

(from an earlier fetch run without --tags, and then):

$ git fetch --tags
[fetch messages]
$ cat .git/FETCH_HEAD
cat .git/FETCH_HEAD 
d7dffce1cebde29a0c4b309a79e4345450bf352a        branch 'master' of git://git.kernel.org/pub/scm/git/git
a274e0a036ea886a31f8b216564ab1b4a3142f6c    not-for-merge   branch 'maint' of git://git.kernel.org/pub/scm/git/git
8553c6e5137d7fde1cda49817bcc035d3ce35aeb    not-for-merge   branch 'next' of git://git.kernel.org/pub/scm/git/git
31148811db6039be66eb3d6cbd84af067e0f0e13    not-for-merge   branch 'pu' of git://git.kernel.org/pub/scm/git/git
aa3afa0b4ab4f07e6b36f0712fd58229735afddc    not-for-merge   branch 'todo' of git://git.kernel.org/pub/scm/git/git
d5aef6e4d58cfe1549adef5b436f3ace984e8c86    not-for-merge   tag 'gitgui-0.10.0' of git://git.kernel.org/pub/scm/git/git
[much more, snipped]

We will come back to this in just a moment.

The fetch may, depending on whatever additional refspecs it finds—this is usually controlled by the remote.origin.fetch configuration entries—update some set of remote-tracking branches, and create or update some of your tags. If you are configured as a fetch mirror, with your update refspec being +refs/*:refs/*, you get literally everything. Note that this refspec has the force flag set, and brings over all branches, all tags, all remote-tracking branches, and all notes. There are more obscure details about what refspecs get used when, but using --tags, with or without --force, does not override the configuration entries (whereas writing an explicit set of refspecs does, so this is one way—maybe the only way—--tags differs from writing out refs/tags/*:refs/tags/*).

Updates in your own reference space—your own remote-tracking branches and tags, usually—do matter, but ... not for pull, as we'll see in the next section.

git pull

I like to say that git pull just runs git fetch followed by a second Git command, where the second command defaults to git merge unless you instruct it to use git rebase. This is true and correct, but there is an obscure detail in the way. This was easier to say before git fetch was rewritten as C code: back when it was a script you could follow the script's git fetch and git merge commands and see what the actual arguments were.

When git pull runs either git merge or git rebase, it does not use your remote-tracking branches and tags. Instead, it uses the records left behind in FETCH_HEAD.

If you examine the examples above, you will see that they tell us that initially, refs/heads/master in the repository on git.kernel.org pointed to commit e05806d.... After I ran git fetch --tags, the new FETCH_HEAD file tells us that refs/heads/master in the repository on git.kernel.org pointed (at the time I ran fetch, it may have changed by now) to commit d7dffce....

When git pull runs git merge or git rebase, it passes these raw SHA-1 numbers through. So it does not matter what your reference names resolve to. The git fetch I ran did in fact update origin/master:

$ git rev-parse origin/master
d7dffce1cebde29a0c4b309a79e4345450bf352a

but even if it had not, git pull would pass d7dffce1cebde29a0c4b309a79e4345450bf352a to the second command.

So, suppose you were fetching tags without --force and got object 1234567.... Suppose further that, had you been fetching tags with force, this would be the result of git rev-parse refs/tags/last-build, but because you did not use --force, your own repository left last-build pointing to 8888888... (a very lucky commit in China :-) ). If you, personally, say "tell me about last-build" you will get revision 8888888.... But git pull knows that it got 1234567... and no matter what else happens, it will just pass the number 1234567... to its second command, if something calls for that.

Again, it gets that number out of FETCH_HEAD. So what matter here are the (full) contents of FETCH_HEAD, which are determined by whether you fetch with -a / --append, or not. You only need/want --append in special cases that won't apply here (when you are fetching from multiple separate repositories, or fetching in separate steps for debugging purposes, or some such).

Of course, it does matter later

If you want / need your last-build tag to get updated, you will have to run git fetch --tags --force at some point—and now we get into atomicity issues.

Suppose that you have run git fetch, with or without --tags and with or without --force, perhaps by running git pull which runs git fetch without --tags. You now have commit 1234567... locally, and the name last-build points to either 8888888... (not updated) or 1234567... (updated). Now you run git fetch --tags --force to update everything. It's possible that now, the remote has moved last-build yet again. If so, you'll get the new value, and update your local tag.

It's possible, with this sequence, that you never saw 8888888.... You might have a branch that incorporates that commit, but not know that commit by that tag—and now that you are updating your tags, you won't know 8888888... by that tag now, either. Is that good, bad, or indifferent? That's up to you.

Avoiding git pull

Since git pull merely runs git fetch followed by a second command, you can just run git fetch yourself, followed by the second command. This gives you full control over the fetch step, and lets you avoid a redundant fetch.

Since you do control the fetch step, you can specify precisely, using refspecs, just what you want updated. Now it's time to visit the weird hybrid tag update mechanism as well.

Take any repository you have handy and run git ls-remote. This will show you what it is that git fetch sees when it connects:

$ git ls-remote | head
From git://git.kernel.org/pub/scm/git/git.git
3313b78c145ba9212272b5318c111cde12bfef4a    HEAD
ad36dc8b4b165bf9eb3576b42a241164e312d48c    refs/heads/maint
3313b78c145ba9212272b5318c111cde12bfef4a    refs/heads/master
af746e49c281f2a2946222252a1effea7c9bcf8b    refs/heads/next
6391604f1412fd6fe047444931335bf92c168008    refs/heads/pu
aa3afa0b4ab4f07e6b36f0712fd58229735afddc    refs/heads/todo
d5aef6e4d58cfe1549adef5b436f3ace984e8c86    refs/tags/gitgui-0.10.0
3d654be48f65545c4d3e35f5d3bbed5489820930    refs/tags/gitgui-0.10.0^{}
33682a5e98adfd8ba4ce0e21363c443bd273eb77    refs/tags/gitgui-0.10.1
729ffa50f75a025935623bfc58d0932c65f7de2f    refs/tags/gitgui-0.10.1^{}

Your Git gets, from the remote Git, a list of all references and their targets. For references that are (annotated) tags, this includes the tag object's final target as well: that's the gitgui-0.10.0^{} here. This syntax represents a peeled tag (see gitrevisions, though it does not use the word "peeled" here).

Your Git then, by default, brings over every branch—everything named refs/heads/*—by asking for the commits to which they point, and any additional commits and other objects needed to complete those commits. (You do not have to download objects you already have, only those you lack-but-need.) Your Git can then look through all the peeled tags to see if any of the tags points to one of those commits. If so, your Git takes—with or without --force mode, depending on your fetch—the given tag. If that tag points to a tag object, rather than directly to a commit, your Git adds that tag object to the collection as well.

In Git versions before 1.8.2, Git mistakenly applies the branch rules to pushed tag updates: they are allowed without --force as long as the result is a fast-forward. That is, the previous tag target would merely need to be an ancestor of the new tag target. This only affects lightweight tags, obviously, and in any case Git versions 1.8.2 and higher have "never replace a tag without --force" behavior on push. Yet the observed behavior for Git 2.10.x and 2.11.x is that tags get replaced on fetch, when using --tags.

But no matter what, if your goal is to forcibly update all tags and all remote-tracking branches in the usual way, git fetch --tags --force --prune will do it; or you can git fetch --prune '+refs/tags/*:refs/tags/*' '+refs/heads/*:refs/remotes/origin/*', which uses the + syntax to force both tag and remote-tracking branch updates. (The --prune is optional as usual.) The force flag may be unnecessary, but is at least harmless here, and might do something useful in some Git versions. And now that your tags and remote-tracking branches are updated, you can use git merge or git rebase with no arguments at all, to merge or rebase using the current branch's configured upstream. You can repeat this for as many branches as you like, never needing to run git pull (with its redundant fetch) at all.