Home > OS >  a git branch fetching changes from an untracked branch
a git branch fetching changes from an untracked branch

Time:11-09

for my repo git branch -vv returns the follwing

  dev  5eed80e [origin/dev: behind 1] Create NeweestFileBaby
  dev2 9a50723 commiting first1 change
* main 8898177 [origin/main] Create NewDev

So I am checked out at my local main branch

Then I make a change in my remote dev branch in github, then for an experiment i do

git fetch

at this point I am not expecting to fetch changes from remote dev since my local main branch is set to track changes from remote main but for some reaseon it does fetch the changes from remote dev, again I m checked out at local main.

could you please tell me why does this happen?

this is interesting because with git push the logic kind of works corretcly because I am able to push cahnges only to my respective tracking branches from respective local branches that are set upstream to respective remote branches.

CodePudding user response:

First a side note: "Untracked branch" is the wrong phrase here.

Git's terminology is not good, because:

  • the verb track applies to files: some file is either tracked or untracked, based on whether that file appears in Git's index at this very moment; but
  • the verb track applies to branch names, in that a branch B that has an upstream setting of U is said to track U.

These two notions are completely different. The first one means that the tracked file will be in the next commit (if it's still tracked at that time), while the second one means that git status will report on different commits reachable from B and U and that git merge or git rebase with no additional arguments will use U if B is the current branch.

We're familiar with this problem in other cases: for instance, the word set, even when treated as just a noun or just a verb ("set a place at the table"), has many meanings; it has another set when treated as an adjective. But at least with Git there was, once, some hope that we might not have to deal with this kind of craziness. If the upstream of a branch were only ever referred to as "upstream", we wouldn't need an extra meaning for track. But almost 20 years down the line, we still have both terms. (And, of course, the word upstream is a bit problematic as well, but let's not go there now.)

But there are two big asymmetries between git fetch and git push here:

This is interesting because with git push the logic kind of works correctly because I am able to push changes only to my respective tracking branches from respective local branches that are set upstream to respective remote branches.

Technically, you push commits (not changes as commits are snapshots plus metadata), but yes: git push, with no extra arguments, tells Git to:

  1. figure out which branch you're "on" right now (as in git status says on branch main or whatever);
  2. use the upstream setting of this branch to determine a remote (e.g., origin) and a branch name on that remote; and
  3. in effect, run git push remote branch, assuming a push.default setting of simple or current.

Note that with push.default set to upstream, the effect here is git push remote branch:reverse-map-of-upstream instead, i.e., the branch can have a different name on the remote, but with simple and current, the push goes to the same name on the remote. With push.default set to matching, the behavior here is that from Git version 1: the equivalent of git push remote :, which has not been found to be very useful but is preserved for backwards compatibility.

With git fetch, however, the default still uses step 1 and half of step 2: Git will figure out the correct remote from the current branch (or use a hardcoded fallback, origin, if this fails). But after that point, Git uses the remote.remote.fetch setting(s) (this is a multivalued configuration setting, so there can be more than one entry listed). These provide the refspec argument(s), which above—for git push—were a single branch name or a colon-separated pair of branch names for the two recommended values (matching being the non-recommended one, and nothing causing the git push to fail).

The default here for origin, if you have not set it, is:

 refs/heads/*:refs/remotes/origin/*

Note the two "glob-like" asterisk * characters here. This refspec means match all branch names on the remote, and copy each one to a remote-tracking name locally. So we git fetch all the new commits from origin, bringing all of them into our local repository, and then we update all our remote-tracking refs/remotes/origin/whatever names.

The effect is that git fetch means fetch all branches from origin, which is just what you saw. The updates of the remote-tracking names are implied by the refspec refs/heads/*:refs/remotes/origin/* (with the leading setting the "force" flag, so that each name is updated even if the operation is not a fast-forward one).

Note that running:

git fetch origin main

will limit the git fetch operation so that, in spite of however many branch names the Git software at the origin URL lists, we bring over only those new commits needed to update our origin/main remote-tracking name. Our Git software adds those new-to-us commits to our object database. Curiously, despite the lack of a colon and refs/remotes/origin/main—we didn't run git fetch origin main:refs/remotes/origin/main—our Git now goes ahead and updates our origin/main. But why?

The answer lies in that same backwards compatibility issue that plagues git push so that matching means git push with a colon refspec. Without going into all the boring history here, let's just note that git fetch now (since Git 1.8.2) "opportunistically updates" remote-tracking names as configured in the remote.remote.fetch line(s), whether or not you give a specific refspec.

The bottom line here is that:

git fetch origin

fetches all configured fetch branches from origin, and "all configured fetch branches" defaults to "all branches". There is a --all flag for git fetch, but it means all remotes: it has no effect on which branches are to be fetched. Then, too, there's the terminology issue: we don't actually fetch branches. We fetch commits. We just find the commits to fetch using names, which may be branch names. These are branch names as seen on the remote. The commits come into our own local Git-objects-database, and then Git updates—or doesn't update—remote-tracking names as specified via the refspec arguments.

Unnumbered footnotes

A refspec is, in effect, a pair of names separated by a colon :, and optionally prefixed with a plus sign . The names can be branch or tag names (abbreviated) or fully spelled out, e.g., refs/heads/main or refs/tags/v1.2. Doing the full spelling-out is wise in any automated code since Git's attempt to match a partial name to a full name may yield surprises. The name on the left side of the colon is the source, which for git push is local and for git fetch is remote. The name on the right side of the colon is the destination, which for git push is remote and for git fetch is local. The plus sign, as noted above, sets the --force flag for that particular ref. Refspecs may use glob * in a limited fashion; the * must appear on both sides. The limits on * matching were improved at some point to allow, e.g., refs/heads/pr-*:refs/remotes/pr-* if you like; before that * had to match "whole component" parts (i.e., just before or after a slash).

What I call remote-tracking names (origin/main or, fully spelled out, refs/remotes/origin/main), modern Git documentation calls remote-tracking branch names. These are not branch names because branch names start with refs/heads/.

For fetching from multiple remotes, I recommend using git remote update rather than git fetch. This is partly because ancient Git required it, but I think overall that code has been better-maintained.

  • Related