3

If I run

git fetch --force origin "refs/tags/release-2017-12-22T15_28_47-05_00"

it outputs

From github.com:myname/myrepo
 * tag               release-2017-12-22T15_28_47-05_00 -> FETCH_HEAD

but then I don't see the branch if I do git tag -l and if I try to check it out with

git checkout -q "release-2017-12-22T15_28_47-05_00"

then I get an error about it not being found:

error: pathspec 'release-2017-12-22T15_28_47-05_00' did not match any file(s) known to git.

It does work if I instead execute

git fetch --all

which outputs

From github.com:myname/myrepo
 * [new tag]         release-2017-12-22T15_28_47-05_00 -> release-2017-12-22T15_28_47-05_00

and makes the tag available. Unfortunately, I'm encountering this error in CircleCI scripts that I don't have any control over so I can't just use this second method. They're running

git fetch --force origin "refs/tags/${CIRCLE_TAG}"
git reset --hard "$CIRCLE_SHA1"
git checkout -q "$CIRCLE_TAG"

which seems like it would work, but it runs into the pathspec error. Does anybody have any ideas about why this isn't working?

Ivanna
  • 1,197
  • 1
  • 12
  • 22

2 Answers2

3

There is what I consider to be a bug in Git tag fetching, and you may have tickled that a bit at some point. See Why is git fetch not fetching any tags? for details. However, the git fetch syntax you are using actually explicitly inhibits fetching tags by default.

The bottom line, though, is that this CircleCI script is buggy. It may be interacting with an additional Git bug, and Mark Adelsberger's suggestion of setting the tag option to --tags may help as long as you haven't run into the Git bug, but the CircleCI script is still wrong.

Analysis

Let's take this apart here:

git fetch --force origin "refs/tags/release-2017-12-22T15_28_47-05_00"

The --force here is doing you no good whatsoever. We'll see why in a moment.

The remaining two arguments, origin, and refs/tags/..., are the repository and refspec arguments, respectively.

The repository name origin provides the URL, so that your Git knows to use ssh to call up github.com:myname/myrepo (the user@host:path/to/repo syntax is a special Git-only spelling for the equivalent, but more-standard, ssh://user@host/path/to/repo URL). This repository name origin would also provide a default set of refspecs, if you gave none on the command line; but you are giving some on the command line, so the default refspecs are less important.

The last argument—your refspec—is where things go wrong. A refspec in general consists of two parts separated by a colon, which Git refers to as src and dst. You can prefix the pair with a plus sign + to set a force flag on that one specific refspec, or use --force to set the force flag on all refspecs. (You can list more than one refspec on the command line—every argument after the repository is a refspec, so you could run git fetch origin srcref1:dstref1 srcref2:dstref2, for instance.)

You did not use a colon : in your refspec (nor a leading + but you did use --force). The meaning here is different for git fetch and for git push—I mention this only because both commands take refspecs, but they do different things with colon-free refspecs. For git fetch, if the :dst part of the refspec is missing, that tells Git to throw away the name after fetching the appropriate underlying Git objects.

(When the name being discarded like this is a branch name that appears in the default refspecs provided by the specified repository argument, Git doesn't throw it away after all, which is why the default refspecs are still somewhat relevant—but this isn't a branch name, it's a tag name.)

Every hash that git fetch fetches, git fetch writes to the old Git-1.5-and-earlier compatibility file, .git/FETCH_HEAD, that programs like git pull still use as well. So even though git fetch is throwing the name away, it saves the hash ID (and some auxiliary data as well) in FETCH_HEAD. This is why you see, as a result, the line:

 * tag               release-2017-12-22T15_28_47-05_00 -> FETCH_HEAD

This line is git fetch's way of telling you: I found a tag. I copied the object to which the tag points. Then, as you instructed, I threw away the tag name, and just wrote the hash ID to the file FETCH_HEAD. So we're all good, right?

If you didn't want git fetch to throw the name away, you should have supplied a dst part in your refspec:

git fetch origin refs/tags/release-2017-12-22T15_28_47-05_00:refs/tags/release-2017-12-22T15_28_47-05_00

for instance. (For tag names, it's normal to use the exact same name on both sides of the colon.) This tells Git that, having fetched a tag with the name release-2017-12-22T15_28_47-05_00 from the remote repository, it should write a tag named release-2017-12-22T15_28_47-05_00 into the local repository, pointing to same object (same Git hash ID).

This is where the force flag comes into effect. If that tag already exists on the local system, --force tells Git to overwrite it, rather than producing an error. If the tag doesn't exist, --force has no effect (and of course if the tag already exists with the correct value, re-writing it with the same value has no effect as well). So --force is only useful if you supply some destination reference—a :dst part—in your command-line refspecs.

(If you were fetching branch names, Git would apply the normal branch name update rules, which allow the write as long as the operation is a "fast-forward", but not if it's not. Here --force still means "always allow the write", but a branch update is allowed even without --force as long as it's a fast-forward. A tag update is not allowed without --force, except for a bug in Git versions 1.8.1 and earlier, which apply the branch rules by mistake.)

The fix is clear enough: the script should have the git fetch line changed to read:

git fetch origin "+refs/tags/${CIRCLE_TAG}:refs/tags/${CIRCLE_TAG}"

so that Git is forced to create or update the tag name in the local repository. (Note, I used the shorter/simpler +-means-force option here, which is not required, it's just the style I like.) Or, alternatively, the script could use the git fetch that writes no local name, as it does now, then fish the correct hash ID out of the FETCH_HEAD file, a la git pull. But that's a bigger change to the script, and means that there is no permanent name for the target commit, which probably has additional drawbacks.

You could give all this analysis to the CircleCI folks, who might argue that the Git bug itself should be fixed too (which it probably should), but given that there are buggy Gits all over the world, and that the meaning of a refspec without a local name is pretty well defined, it would be simpler and more reliable to change the script to repeat the tag on both sides of the refspec.

torek
  • 448,244
  • 59
  • 642
  • 775
0

One possible issue would be if you're fetching a tag that points to a commit not already in your local history. In that case, the commit will end up not being reachable form any local branch, and I don't think fetch will copy the tag by default in that case.

If that's the situation, then you could likely get it to work by passing the --tags option to fetch; but since you don't control the script, you might have to change your repo's configuration instead

git config remote.origin.tagOpt --tags

This will have the side-effect that other tags will be fetched as well, though.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52