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 ofU
is said to trackU
.
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:
- figure out which branch you're "on" right now (as in
git status
sayson branch main
or whatever); - use the upstream setting of this branch to determine a remote (e.g.,
origin
) and a branch name on that remote; and - in effect, run
git push remote branch
, assuming apush.default
setting ofsimple
orcurrent
.
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.