简体   繁体   中英

Is there a difference between “remotes/origin/dev” and “origin/dev” in your local repo?

Is there a difference between "remotes/origin/dev" and "origin/dev" in your local repo?

I seem to have witnessed some discrepancy, but haven't yet proven it to myself.

Typically, remotes/origin/dev and origin/dev are two ways to express the same thing. But this is not necessarily the case! The problem is similar to what happens if you create both a branch named X and a tag named X . In this case, simply writing X is ambiguous, and in different situations, you may have to write something different.

The whole story is rather long, but necessary to understand what is or can be going on.

Git references have full names

All of these names—branch names like master , tag names like v1.2 , and remote-tracking names like origin/master —live in what is termed, in Computer Science / Informatics, a separate name space or namespace . If there's ever a problem of ambiguity, what we do is the same thing we do at a party where all the men are named Bruce and all the women are named Sheila: we use a different and/or longer name. In programming languages we tend to call these qualified names and they look like std::map . In Git, we just call them reference names and they start with refs/ and go on to identify which name space we mean.

Git's existing, standardized reference name-spaces are, in alphabetical order and at the time I write this: 1

  • refs/heads/ : branch names
  • refs/namespaces/ : a reserved space for recursion (special hacks for git upload-pack and git receive-pack , really—these are not meant for ordinary use)
  • refs/notes/ : used by git notes
  • refs/remotes/ : remote-tracking names
  • refs/replace/ : used by git replace
  • refs/stash (no trailing slash, there can be no names within this): used by git stash
  • refs/tags/ : tag names

What this means is that if you have created both refs/heads/X , which is a branch named X , and refs/tags/X , which is a tag named X , you can explicitly spell out refs/heads/X to mean branch X and refs/tags/X to mean tag X . There are annoying exceptions to the standard rules, but first, let's look at those rules.


1 The name spaces have grown over time.


The usual rules

In general, when Git is going to show you a reference, it tends to shorten it according to some simple rules. And, if you use an unqualified reference—by which I mean a name not starting with refs/ —Git has a six-step process to figure out what you meant. This six-step process is described in the gitrevisions documentation :

When ambiguous, a < refname > is disambiguated by taking the first match in the following rules:

  1. If $GIT_DIR/<refname> exists, that is what you mean (this is usually useful only for HEAD , FETCH_HEAD , ORIG_HEAD , MERGE_HEAD and CHERRY_PICK_HEAD );

  2. otherwise, refs/<refname> if it exists;

  3. otherwise, refs/tags/<refname> if it exists;

  4. otherwise, refs/heads/<refname> if it exists;

  5. otherwise, refs/remotes/<refname> if it exists;

  6. otherwise, refs/remotes/<refname>/HEAD if it exists.

So if you write master , and refs/heads/master exists, you usually get the branch named master (from step 4). Therefore, if Git is going to print refs/heads/master , it can probably just print master . Similarly, if you write origin/dev , and refs/remotes/origin/dev exists, you usually get that (from step 5)—so if Git is going to print refs/remotes/origin/dev , it can probably just print origin/dev .

There are lots of exceptions

If you run git branch -r , Git strips off refs/remotes/ :

$ git branch -r
  origin/HEAD -> origin/master
  origin/maint
  origin/master
  origin/next
  origin/pu
  origin/todo

This tells us that refs/remotes/origin/HEAD exists in refs/remotes/ , and so on. These will match in step 5 above.

But if you run git branch -a , Git strips off only refs/ from the remote-tracking names:

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/maint
  remotes/origin/master
  remotes/origin/next
  remotes/origin/pu
  remotes/origin/todo

The current branch master , which is really refs/heads/master , has had two components stripped away: refs/ and heads/ . But the remote-tracking names, which previously had two components stripped away, now have only one stripped: remotes/origin/master for instance. These will still work, and in fact, they will match earlier, in step 2. But why are they inconsistent? The only answer seems to be: it's traditional.

Now suppose you accidentally create a tag named master , ie, the fully qualified name refs/tags/master . According to the list of six steps, if you write the name master , Git should find the tag first, because that's step 3. Let's find out if it does. First, let's see what hash ID master names, then pick a different (earlier commit) hash ID:

$ git rev-parse master
b7bd9486b055c3f967a870311e704e3bb0654e4f
$ git rev-parse master~3
18f2717578853edfdaed5fb7361b5f992a68a79e

Now let's create the tag master with hash ID 18f2717578853edfdaed5fb7361b5f992a68a79e , so that step 3 will find this 18f27... thing instead of step 4 finding the b7bd9... thing:

$ git tag master 18f2717578853edfdaed5fb7361b5f992a68a79e
$ git rev-parse master
warning: refname 'master' is ambiguous.
18f2717578853edfdaed5fb7361b5f992a68a79e

Aha: we get a warning , and Git does in fact find the tag instead of the branch. So if we run git checkout master , we'll check out the tag , right? Wrong!

$ git checkout master
warning: refname 'master' is ambiguous.
Already on 'master'
Your branch is up-to-date with 'origin/master'.
$ git rev-parse HEAD
b7bd9486b055c3f967a870311e704e3bb0654e4f

The git checkout command tried the name as a branch name first, and found commit b7bd9486b055c3f967a870311e704e3bb0654e4f ! It still gave us the warning, but it used the branch name. If we want the tag name we must spell it out completely, or use tags/master to get it through step 2. I prefer the full spelling myself:

$ git checkout refs/tags/master
Note: checking out 'refs/tags/master'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 18f2717578... Merge branch 'ms/core-icase-doc'

It's a good idea to delete the extra master and get back to sanity:

$ git tag -d master
Deleted tag 'master' (was 18f2717578)
$ git checkout master
Previous HEAD position was 18f2717578... Merge branch 'ms/core-icase-doc'
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

Git is full of weird corner cases like this and the only way to find out how each command really behaves is to experiment (or avoid ambiguous situations).

What this means for the original question

It's possible to create a branch named origin/dev —whose full name is therefore refs/heads/origin/dev —or a remote-tracking name whose full name is refs/remotes/remotes/origin/dev , for instance. If we strip two name components from this last name, we see remotes/origin/dev , which looks like the kind of name we get from git branch -a when it strips just one component. If you're using the various coloring options, the remote-tracking name will be in red by default, with branch names in green or black by default, so some of these will stand out. But it's definitely possible to get yourself some bad situations.

To view all the references with their full names, use git for-each-ref . Note that in a repository with many tags or branches, this can produce a lot of output, so I have stripped down the output from the Git repository for Git:

b7bd9486b055c3f967a870311e704e3bb0654e4f commit refs/heads/master
b7bd9486b055c3f967a870311e704e3bb0654e4f commit refs/remotes/origin/HEAD
53f9a3e157dbbc901a02ac2c73346d375e24978c commit refs/remotes/origin/maint
b7bd9486b055c3f967a870311e704e3bb0654e4f commit refs/remotes/origin/master
5c9ce644c390ec4ef3ba4adc94e7f4af17ade36b commit refs/remotes/origin/next
1aaaa8cf15ba4eb62d485c5c8b64d6a75b9e7c3f commit refs/remotes/origin/pu
f59de5ad04b18866024fb298ddb276cb51d91673 commit refs/remotes/origin/todo
d5aef6e4d58cfe1549adef5b436f3ace984e8c86 tag    refs/tags/gitgui-0.10.0
33682a5e98adfd8ba4ce0e21363c443bd273eb77 tag    refs/tags/gitgui-0.10.1
ca9b793bda20c7d011c96895e9407fac2df9648b tag    refs/tags/gitgui-0.10.2
[mass snippage]
f883596e997fe5bcbc5e89bee01b869721326109 tag    refs/tags/v2.9.3
8d091e9ed473c372a5b89d1258d1c3ad01daa04c tag    refs/tags/v2.9.4
dcba104ffdcf2f27bc5058d8321e7a6c2fe8f27e tag    refs/tags/v2.9.5

The name here (in the third column) is fully qualified, so you can see if there is anything odd going on. You can also inspect only particular portions of the name-space, and use --format directives, to limit output:

$ git for-each-ref --format='%(refname)' refs/remotes/origin
refs/remotes/origin/HEAD
refs/remotes/origin/maint
refs/remotes/origin/master
refs/remotes/origin/next
refs/remotes/origin/pu
refs/remotes/origin/todo

If you think that you are in a bad situation—especially if Git is warning you about ambiguous names—you can use git for-each-ref to analyze your actual situation, and use that to plan your recovery.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM