Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are their names the same: a local-tracking branch, the corresponding remote-tracking branch, and the corresponding remote branch being tracked?

Tags:

git

  1. Are the names of a remote-tracking branch and the corresponding remote branch being tracked necessarily the same?

    If they can have different names, how does git fetch match the two branches then? (a typical refspec to git fetch is +refs/heads/*:refs/remotes/remote/*)

  2. If I am correct, given a remote-tracking branch, we can create a local-tracking branch that associates with it but has a different branch name. (by the -b option in git checkout)

    Further if the names of the remote-tracking branch and the corresponding remote branch being tracked are the same, how does git push match the local-tracking branch and the remote branch? (a typical refspec to git push is +refs/heads/*:refs/heads/*)

like image 852
Tim Avatar asked Dec 31 '15 21:12

Tim


1 Answers

Are the names of a remote-tracking branch and the corresponding remote branch being tracked necessarily the same?

No. However, making them not-match results in a lot of pain (and I haven't tested it on the push side).

If they can have different names, how does git fetch match the two branches then? (a typical refspec to git fetch is +refs/heads/*:refs/remotes/remote/*)

You may have multiple fetch = lines, so you could, for instance, do this:

[remote "strange"]
    fetch = +refs/heads/master:refs/remotes/strange/surprise
    fetch = +refs/heads/other:refs/remotes/strange/surprise2

Note, however, that you can no longer use refs/heads/* on the left of any additional fetch refspecs, since that will match master and other and (presumably) map them to names other than surprise and surprise2, and git fetch aborts with an error message in this case. This effectively forces you to list every refs/heads name that you wish to copy from the given remote (strange, in this case).

(As I said, I haven't tested this with push and I don't know if push obeys the same mapping rules as fetch. There were some recent changes, around git 2.5 or so, to handle "triangular" work-flows better, where you fetch from localmirror and push to centralserver for instance. One of those changes was to add the same kind of name-mapping for the push remote. Presumably, before this new code went in, doing this kind of pushing brought even more pain, possibly even without a triangular work-flow; and presumably now it works better....)

We might call this "silly renaming tricks", and my advice would be: don't use them. :-) I think they'll work correctly with most commands, and fail with others, though I can't point to any specific failure examples (just vague memories of how I used to do things).

If I am correct, given a remote-tracking branch, we can create a local-tracking branch that associates with it but has a different branch name. (by the -b option in git checkout)

Yes; and this works fine for all kinds of local work. Again, I'd avoid the phrase "local-tracking branch", and just say "local branch with an upstream" as this is the direction git documentation has moved since about 1.7 (see below).


Remember that "the upstream of local branch $branch" is produced by:

  • obtaining the remote name from git config --get branch.$branch.remote, and
  • mapping branch-name from git config --get branch.$branch.merge through the fetch = refspecs for that remote.

Thus, suppose we've created two local branches test1 and test2 and have the following:

$ git config --get branch.test1.remote
origin
$ git config --get branch.test1.merge
refs/heads/test
$ git config --get branch.test2.remote
origin
$ git config --get branch.test2.merge
refs/heads/test

Both test1 and test2 refer to refs/heads/test, which is the name of a branch on the other git repository that will be located via the name origin: this is why we need to run these through the fetch = map(s) for origin.

In the absence of silly renaming tricks, the "mapped through" part leaves the branch-name part (everything after refs/heads) unchanged, and just replaces the middle bits, so that refs/heads/test becomes refs/remotes/origin/test. That's really easy to make assumptions about. I believe some lazy script-writers (including myself in the past) may have used this bit of shell script code:

fullbranch=$(git rev-parse --symbolic-full-name $branch) || exit 1
remote=$(git config --get branch.$branch.remote)
rmtbranch=refs/remotes/$remote/$branch

which not only assumes the lack of silly renaming tricks, it even assumes that if we're on branch test1, the upstream must be origin/test1, not origin/test. Slightly less lazy script writers (including myself in the past) then had to fix their scripts, e.g.:

fullbranch=$(git rev-parse --symbolic-full-name $branch) || exit 1
remote=$(git config --get branch.$branch.remote)
theirname=$(git config --get branch.$branch.merge)
rmtbranch=refs/remotes/$remote/${theirname#refs/heads/}

which now assumes that refs/heads/test on the origin maps to refs/remotes/origin/test in the local repository.

Adding silly renaming tricks means that we can't find the actual upstream name easily at all, but various commands (e.g., git merge, git rebase) automatically find the correct upstream. To make it easier for scripting, git version 1.7.0 grew the @{upstream} notation: you can now simply write $branch@{upstream}. The git parser looks up the upstream for you, and the above (broken) script fragment can be rewritten as:

rmtbranch=$(git rev-parse --symbolic-full-name $branch@{upstream}) || exit 1

This is all fine and good for fetch, but what about push? Well, if you're pushing to the same remote that you're fetching from, you just do the same thing. But you can, for any number of reasons,1 split them up: fetch from repository F and push to repository P. In this case, we might need different mappings for F and P.

Git 2.5 introduced @{push}, as noted by VonC in some earlier SO thread I can't find offhand, and in this github blog posting. The new @{push} notation simply switches from using the fetch upstream to using the push upstream, i.e., using the P mapping instead of the F mapping.

There's still one good question, and it's your last one here:

Further if the names of the remote-tracking branch and the corresponding remote branch being tracked are the same, how does git push match the local-tracking branch and the remote branch? (a typical refspec to git push is +refs/heads/*:refs/heads/*)

My answer for git 2.5, with the new push stuff, is: I don't really know for sure, but your "typical refspec" is no longer the default (since git 2.0). When you run git push with no refspec arguments, git looks up your push.default setting (along with a bunch more optional settings but push.default is effectively2 required). It has five possible values, and only one of them—not the default—amounts to refs/heads/*:refs/heads/*.

One of those settings is upstream, and that setting runs the current branch's merge setting through the map functions (presumably, in git 2.5 and later, through the new separate push map if present, else through the fetch map).


1One reason is to use a pull-request repository, as in the linked github blog post. Another is to fetch from a local mirror (as at a large corporate setup with local mirrors for various branch offices), but push to a single central server (the corporate-designated "master" site that all the local mirrors actually mirror).

2If you fail to set push.default, git spits out a lot of annoying text. The best way to shut it up is to set push.default, so that means you're "required" (in some sense anyway) to set it. And, since git 2.0, the default setting, if you haven't set it, is simple, which forbids silly renaming tricks.

like image 155
torek Avatar answered Oct 21 '22 10:10

torek