2
  1. If I am correct, a remote-tracking branch can be created when cloning the remote repository. Are there other cases when a remote-tracking branch is created?

  2. If I am right, a remote-tracking branch is updated when fetching/pulling from the remote repository. Are there other cases when a remote-tracking branch is updated?

  3. As a special case of 2, when git push a local branch to the remote repository, if the local branch has an associated remote-tracking branch in the local repository (i.e. if the local branch is a local-tracking branch, defined in Version Control with Git by Loeliger 2ed), does git push update the remote-tracking branch , or the remote-tracking branch can be updated only indirectly from the remote repository by running git fetch or git pull after git push?

  4. As a special case of 1, if git push pushes local non-tracking branches to remote branches (i.e. if there is no corresponding remote branches to the local branches to be pushed), will git push create remote-tracking branches associated with the local non-tracking branches and turn them into local tracking ones?

Tim
  • 1
  • 141
  • 372
  • 590

2 Answers2

7

Let's take these three four :-) questions in order, more or less:

  1. ... a remote-tracking branch can be created when cloning the remote repository. Are there other cases when a remote-tracking branch is created?

They are; and at least potentially yes, but let's get to that later.

Specifically, when git clone is making an initial clone of another repository, it adds a fetch = configuration line for the remote. The line added depends on these two options:

  • --origin name: alters the name of the remote. Normally the name is just origin and the fetch configuration is fetch = +refs/heads/*:refs/remotes/origin/*, but if you change the name of the remote to, say, zerblatt, you get +refs/heads/*:refs/remotes/zerblatt/*.

  • --mirror: alters the fetch configuration to read fetch = +refs/*:refs/*. Note that in this case there are probably no remote-tracking branches after all, since "remote-tracking branches" is just a weakly abstracted way of saying "references in the refs/remotes/ name-space" and now we don't have any renaming going on. (If the repository being cloned presents its own remote-tracking branches—references in refs/remotes/—they will get copied and we will get remote-tracking branches. We might run git ls-remote to find out what references they have before we start cloning, in order to find out, although it's a little tricky since if we haven't started cloning, we don't have a repository in which to configure a remote so as to use git ls-remote. There is a method though!)

Let's go on to:

  1. If I am right, a remote-tracking branch is updated when fetching/pulling from the remote repository. Are there other cases when a remote-tracking branch is updated?

Yes: in particular a remote-tracking branch (which, again, is just a reference in the refs/remotes/ name-space) is updated automatically on at least some successful git push operations. Exactly which ones depends on your version of git, since push didn't always update them: the documentation noting this update first appeared in git 1.8.4 although the updates probably went in around git 1.7; and git 1.9 and later also update them in so-called "triangular workflows" (fetch from one remote, push to a different remote).


Let me take a little break here and make a few more notes about remote-tracking branches.

As we already noted, a remote-tracking branch is simply a reference whose full name begins with refs/remotes/. Git has a "plumbing" command, git update-ref, that you (or anyone or any script) can use to update any reference. For instance, suppose you've fetched from origin fairly recently, then added one commit to your own master (that's set with origin/master as its upstream) so that git status says you're "ahead 1". If you were to run:

git update-ref refs/remotes/origin/master master

and then run git status, your git would claim that you are now up-to-date. What happened is that you've told your git that "their" master (origin/master) points to the same commit as your own, even though you have not pushed your own commit yet. (If you do run this, you can simply run git fetch origin to fix refs/remotes/origin/master, or of course use git update-ref to fix it.)

This exposes the underlying mechanism here: git simply writes, to refs/remotes/origin/master, the actual SHA-1 ID of the commit object your git saw when it talked to their (the remote's) git. There is a strong constraint on this: git can't put that SHA-1 ID in unless that SHA-1 ID corresponds to an actual object already stored in your own repository. In practice, git "feels" (and is) safe in writing that ID there after a successful fetch or push, since after a successful fetch, you must have the object, and to complete a push, you must have the object, and in both cases git has just seen that ID corresponding to some name on the given remote.

This also shows how git status can say "ahead 1" in the first place: it simply counts the commits reachable from your master that are not reachable from your master's upstream. That is:

ahead=$(git rev-list --count master@{u}..master)
behind=$(git rev-list --count master..master@{u})
echo "branch master is ahead $ahead and behind $behind"

This information is as up-to-date (or out-of-date) as the last time the remote-tracking branch was properly updated.

Let's also note, now, that git clone can be split up into several separate git commands. Let's assume you're cloning with neither --origin nor --mirror and that the url is simply $url (and that none of these steps fail):

mkdir myclone && cd myclone && git init
git remote add origin $url
git fetch origin
git checkout ...

(exactly what to git checkout is a bit of a mystery; and the git fetch command can be skipped if we add -f to the git remote add line, but I intend to do something in between here for illustration purposes). What does each command do?

  • The mkdir + cd + git-init sequence creates a new empty, suitable-for-clone repository.
  • The git remote add line configures the remote origin to fetch from $url and adds a fetch = +refs/heads/*:refs/remotes/origin/* line.
  • The git fetch origin command then mostly-completes the cloning process (the missing bit is the final git checkout).

Now, suppose that before we run git fetch origin, we run other git commands, such as git config --edit and mess with the fetch = line. We can set things up so that we don't get remote-tracking branches. We can create commits of our own, unrelated to what is on the actual remote, and use git update-ref to assign them to remote-tracking branches. We can run git ls-remote to find out what branches exist on the remote.

None of these are particularly useful but they are all possible. (And if anyone has any good reason to do tricky branch-name-mapping things by creating a lot of fetch = lines, perhaps they are useful after all.)

(What should we git checkout, on that last line? The answer depends on several things, only some of which we have direct control over. If you ran git clone with -b branch, that's the one we can handle the most easily: we should git checkout branch. If there's a refs/remotes/origin/branch we'll get a local branch branch that has its upstream set to origin/branch. If you did not specify a -b option, though, then what to check out, to emulate your git's git clone, depends on both your version of git, and the remote's version, as well as what we'd see from git ls-remote. Newer gits ask for and receive the branch name. Older gits take the [internal equivalent of the] ls-remote output and compare the SHA-1 the remote git shows for HEAD to the SHA-1 the remote git shows for each branch: if there's exactly one match, that's the branch; if there are multiple matches, pick one arbitrarily; if there are no matches at all, use master. If a newer git is talking to an older git that does not support the new "tell me the branch by name" option, the newer git falls back to the older method.)


Back to your questions:

  1. As a special case of 2, when git push a local branch to the remote repository, if the local branch has an associated remote-tracking branch in the local repository (i.e. if the local branch is a local-tracking branch, defined in Version Control with Git by Loeliger 2ed), does git push update the remote-tracking branch, or the remote-tracking branch can be updated only indirectly from the remote repository by running git fetch or git pull after git push?

I find this question confusing. There's no special casing involved here. At some point, we know that your git push has decided to send remote R a request that, in effect, says: "please set your refs/heads/foo to SHA-1 1234567890123456789012345678901234567890" (substitute in the correct refs/heads/ name and SHA-1 ID as needed). (When using --force-with-lease the request has more information in it, and in any case the request also carries the "force" flag. It's up to the remote to decide whether to obey the "force" flag. However, it's important to note here that the request delivers a raw SHA-1, and not the name of your branch in your local git repository. The remote git gets just his reference-name, and the SHA-1. What this means in practice is that the remote's pre- and post-receive and update hooks cannot see your branch names. [They don't get to see the force flag either, which I consider a minor bug, but that's another issue entirely.])

Their git replies to this request with either a "yes, done" or "no: error: <details>" answer.

Your git then has the option of treating the "yes, done" answer as sufficient to update your remote-tracking branch for remote R. (Of course a "no" reply means there is nothing to update.) It doesn't matter what local branch, if any, you're on, nor what local branches you have, nor whether any of them have upstreams set. This is in part because this same code allows you to do:

git push origin 1234567890123456789012345678901234567890:refs/heads/foo

to set their refs/heads/foo to that commit (assuming the commit ID is valid; your git will check your repository first, and deliver the commit to their git if necessary, as usual).

The tricky bit for your git, in terms of doing a remote-tracking branch update, is figuring out what name your git should replace refs/heads/foo with. This is where the linear vs triangular work-flow stuff comes in, and where we must check which version of git you have. If you are using a triangular work-flow and your git is older than 1.9, your git doesn't know what to update, and updates nothing. If your git is older than about 1.7 or so, it never tries to figure out what to update, and updates nothing. Otherwise it uses the appropriate refspec mapping to translate refs/heads/foo to see what to update.

Finally:

  1. As a special case of 1, if git push pushes local non-tracking branches to remote branches (i.e. if there is no corresponding remote branches to the local branches to be pushed), will git push create remote-tracking branches associated with the local non-tracking branches and turn them into local tracking ones?

Pieces of this question still don't make sense to me, but pieces do make sense. Let's consider a concrete example, and ignore both triangular work-flows and weird name translations due to complicated multiple fetch = lines, so that we're dealing with simple git push origin myname:theirname commands. Let's further assume that the git version is reasonably up-to-date.

Again, your git, given git push origin myname:theirname, starts by translating myname to a raw SHA-1 ID. If you git push origin myname your git also consults your push.default to fill in the theirname part from the myname part, but let's assume you've given an explicit name, refs/heads/foo for instance. (This also lets you push by raw SHA-1 ID. In other words, it takes most of the complications away, and leaves us just with the git-to-git "push" session to worry about, for now.)

Your git now phones up their git using the URL for the remote. (If the URL refers to another repository on your own computer, your git plays both "your git" and "their git" roles, as it were, and uses a bunch of shortcuts as well, but let's just consider the over-the-Internet-phone case here.)

After some basic protocol handshaking, your git sends over any objects needed, then sends over all your update proposals, all at once (from each refspec you gave to your git push):

please set refs/heads/theirname to 123456...
please set refs/heads/anothername to 987654...

and so on.

Their git runs these requests through its checking-rules (both the built-in fast-forward checks, and any receive-side hooks: pre-receive and update) to see whether to allow them. Then it either writes the new SHA-1s into its references and says "yes, done" or rejects the update and says "no".

Your git takes all these replies and decides whether to update or create a refs/remotes/origin/theirname and/or refs/remotes/origin/anothername. (Remember, we're assuming the remote is named origin, and that your git is recent, etc.) For any "yes" reply, your git does update-or-create that name; for any "no", your git does not.

The update-or-create is done as if your git fetch had run git update-ref (though it just invokes the actual update directly, rather than using fork/exec or spawn, of course).

Now, once all of this is done, there's one more thing your git can do, and it depends on whether you supplied the -u (aka --set-upstream) flag to git push (which of course depends on whether your git push is new enough to have the -u flag; I forget when it appeared). It also requires that the left hand side of your push refspec(s) originally resolved to branch names, rather than raw SHA-1s. In this case your git will still have all the names.

If you specify -u, then—as long as the push succeeds, your git effectively runs git branch --set-upstream-to to set or change the upstream for that branch. (Again, it just does this internally, of course.)

Let's put all these together into a fairly complex example. Suppose you have your own local branch foo and a remote named origin, and you do:

$ git fetch origin
[output snipped]
$ git for-each-ref refs/remotes/origin  # let's see their branches
biguglysha1 commit  refs/remotes/origin/HEAD
biguglysha1 commit  refs/remotes/origin/master
# this confirms that they don't have a "refs/heads/theirname"
$ git push -u origin foo:refs/heads/theirname
[output snipped, but assume it says "all done"]

The full spelling, refs/heads/theirname, is required to cause the branch to get created successfully here (if the branch already exists you can use the short name, but then we have a boring case instead of an interesting one).

Because their git created the branch according to the name you supplied, and your git is new enough and you haven't set up weird name maps, you now have a refs/remotes/origin/theirname. Because you specified -u, your local branch foo now has its upstream set to origin/theirname as well. A git branch -vv will now show your foo as "tracking" origin/theirname and up-to-date.

This happened in two parts: when their git accepted the request to set refs/heads/theirname, your git created refs/remotes/origin/theirname, and figuring out refs/remotes/origin/theirname required going through the fetch = map; and then when your git applied the -u option, your git set your branch.foo.remote to origin and your branch.foo.merge to refs/heads/theirname. Interestingly, the second part—applying the -u flag—does not need the map at all, since branch.foo.merge is set to refs/heads/theirname. But for git branch -vv to show origin/theirname, it has to go through the map.

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

Answers:

  1. After initially cloning a Git repository, whenever somebody pushes up a new branch, a remote-tracking branch will be created for this new branch after a doing routine fetch (or pull).

  2. Not that I'm aware of. Fetching or pulling should be the only two operations that update a remote-tracking branch.

  3. Not always. Attempting to push a local branch with a corresponding remote-tracking branch that cannot be fast-forwarded (i.e. the remote-tracking branch contains commits not currently present in a local branch) will result in failure.

miqh
  • 3,624
  • 2
  • 28
  • 38
  • Thanks. About 3, the intention of my question is, when push is successful, does `git push` update the remote-tracking branch , or the remote-tracking branch can be updated only indirectly from the remote repository by running `git fetch` or `git pull` after `git push`? – Tim Dec 31 '15 at 00:05
  • Ah, right. There shouldn't be a need to explicitly `fetch` (or `pull`) after a `push` in order to have your remote-tracking branch updated - this is why it's called a remote-_**tracking**_ branch. It's history state resembles whatever is on the remote. The remote-tracking branch is effectively written whenever you do a successful `push`. – miqh Dec 31 '15 at 00:29
  • 1
    One note here: `git push` does indeed update your own repo's idea of "their" branch, by updating the remote-tracking branch, when their git replies to your git saying that the push succeeded. However, this push-command opportunistic update wasn't always in git. I can't remember how far in the past it changed to always happen. But fetch was different! When fetching with an explicit refspec, git used to avoid updating remote-tracking branches in some cases; *that* particular behavior changed in git 1.8.2, and it now *always* does the opportunistic update (mapping via the `fetch =` config). – torek Dec 31 '15 at 02:53
  • @torek Isn't it rather 1.8.4? http://stackoverflow.com/a/20967347/6309. And only git 1.9 introduced a symmetric behavior for git push: https://github.com/git/git/commit/ca02465b41311fe7634acb9bb5b5c61975ef5f38 – VonC Dec 31 '15 at 07:14
  • @VonC: yes, faulty memory on my part, it was 1.8.4. The push change in 1.9 is specific to triangular workflows though: the 1.8.4 release notes remark that git was already doing push-triggered opportunistic remote-tracking branch updates for typical centralized workflows. – torek Dec 31 '15 at 07:22
  • miqid: In "Attempting to push a local branch with a corresponding remote-tracking branch that cannot be fast-forwarded (i.e. the remote-tracking branch contains commits not currently present in a local branch) will result in failure.", I suspect you mean the corresponding branch in the remote repository by "a corresponding remote-tracking branch", because "remote-tracking branch" exists in the local repository not in the remote repository, and the fast forward merge happens in the remote repository not in the local repository. @torek: do you agree with me? or am I wrong? Thanks. – Tim Jan 01 '16 at 00:19
  • @Tim: agree, and this is why I say fast-forward is more a property of a label move than of an actual merge: push is allowed (no forcing necessary) when the *push recipient* is able to do a fast forward. The receive side code is usually working with a `--bare` repo which (having no work directory) cannot run a real merge. "Fast forward-able" is defined as "old target of reference is reachable from new target of reference". – torek Jan 01 '16 at 00:41
  • @torek: Thanks. In the local repository, is the remote-tracking branch updated by merging the local-tracking branch into it, or updated by implicitly fetching from the already updated branch in the remote repo? ("implicitly" in the sense that the fetching is done inside `git push` not explicitly running `git fetch/pull` after `git push`.) If former, is the merging necessarily fast-forward? – Tim Jan 01 '16 at 01:13
  • The remote-tracking branch is *not* updated by merge. The intent is that the remote-tracking name (`refs/remotes/origin/foo`) should remember "where `foo` was on the remote when we last synchronized". Successful push = synch = update; successful fetch updates per `fetch=` refspec (usually with leading + hence forced update even if not fast-forward, but if you leave out the + sign, fast-forward limiting rules apply even for fetch). – torek Jan 01 '16 at 03:03
  • @torek: (1) Do you mean the remote-tracking branch is updated by implicitly fetching from the remote branch just updated? So is the remote-tracking branch not updated within the local repository, but from the remote repo to the local repo? (2) in the remote repository, is the remote branch updated by merging? But is there other remote branch merged into the remote branch? We can't say the local-tracking branch is merged into the remote branch, because they are not in the same repository, correct? – Tim Jan 01 '16 at 03:28
  • (3) if `git push` pushes local non-tracking branches to remote branches, i.e. there is no corresponding remote branches, will `git push` create remote-tracking branches associated with the local non-tracking branches and turn them into local tracking ones? (Note that I added this to my post) – Tim Jan 01 '16 at 03:46
  • @Tim: this comment section discussion has gotten way out of hand... I'll move most of this to an answer instead (and then maybe delete some of the comments? it's hard to summarize them well though, I think). But that will have to be later... – torek Jan 01 '16 at 04:38