From Version Control with Git by Loeliger 2ed,
What if you don’t specify a refspec at all on a git push command? How does Git know what to do or where to send data?
First, without an explicit remote given to the command, Git assumes you want to use origin.
Without a refspec, git push will send your commits to the remote for all branches that are common between your repository and the upstream repository. Any local branch that is not already present in the upstream repository will not be sent upstream; branches must already exist and match names.
Are "all branches that are common between your repository and the upstream repository" the same as all the remote-tracking branches in the local repository and the corresponding tracked branches in the remote repository?
The quote is for git push
.
Does it (especially the part for the case without a refspec) apply to git fetch
and to git pull
?
A refspec maps a branch in the local repository to a branch in a remote repository. This makes it possible to manage remote branches using local Git commands and to configure some advanced git push and git fetch behavior.
Some common reasons include: - You are not logged in to your account: see File > Options. - You may need to log out and log back in to refresh your token. - You do not have permission to access this repository. - The repository is archived on GitHub.
Fetch changes Fetched changes are stored as a remote branch, which gives you a chance to review them before you merge them with your files. Since fetch does not affect your local development environment, this is a safe way to get an update of all changes to a remote repository.
(I'm afraid this answer is once again rather long, because the questions make assumptions that are—at least today—incorrect. Jump to the boldface section for the actual answers, but everything that comes before that also matters.)
The book text you quote is outdated and (now) just plain wrong (this is a hazard with evolving software, the books get outdated):
First, without an explicit remote given to the command, Git assumes you want to use origin.
I have not, in a relatively quick scan through git's own git history, found precisely when the behavior changed, but the documentation changed in git 1.8.2.1, and now says:
When the command line does not specify where to push with the
<repository>
argument,branch.*.remote
configuration for the current branch is consulted to determine where to push. If the configuration is missing, it defaults to origin.
(This behavior matches git fetch
as well—unsurprisingly, since they use the same bit of source code to get that result.) This fails to mention the optional branch.$branch.pushremote
setting, which overrides branch.$branch.remote
. In all cases here $branch
represents your current branch, as shown by the output of git symbolic-ref --short HEAD
. (If you're in "detached HEAD" mode, so that git symbolic-ref
fails, git push
normally gives you a "fatal: You are not currently on a branch" error, I think since version 2.0 when the defaults changed.)
This is still not the whole story; the git-config documentation adds yet one more quirk:
remote.pushDefault
The remote to push to by default. Overridesbranch.<name>.remote
for all branches, and is overridden bybranch.<name>.pushRemote
for specific branches.
(Fortunately, as of today, I think that covers everything. That is, git starts by looking for a pushremote
setting for the current branch. If there is no such setting, it next looks for remote.pushdefault
. If that is also not set, it falls back on the remote
setting for the current branch. The behavior in older versions of git may vary. Note also that configuration items are case-insensitive: pushremote
and pushRemote
are the same configuration name [this makes me wonder if the branch name parts are case-insensitive as well; I'll have to test that out sometime].)
The next part about refspecs is even more wrong, due to git having acquired several changes over time, some of which are not documented well even in git's built-in documentation. Your quote reads:
Without a refspec, git push will send your commits to the remote for all branches that are common between your repository and the upstream repository.
The new documentation says:
When the command line does not specify what to push with
<refspec>...
arguments or--all
,--mirror
,--tags
options, the command finds the default<refspec>
by consultingremote.*.push
configuration, and if it is not found, honorspush.default
configuration to decide what to push (See git-config for the meaning ofpush.default
).
The push.default
setting was added around version 1.8 of git, but its default value changed in git 2.0. If you have not configured a push.default
you get this warning:
warning: push.default is unset; ...
ever since the configuration item became available. Consulting the git-config documentation, you will now find this:
push.default
Defines the actiongit push
should take if no refspec is explicitly given. Different values are well-suited for specific workflows ...
There are five options, which I will simply list here (see the linked documentation for details): nothing
, current
, simple
, upstream
, and matching
. The book text you quote describes matching
, which was the default behavior prior to git version 2.0. The new default is simple
.
git push
, if you configure "matching" behaviorAre "all branches that are common between your repository and the upstream repository" the same as all the remote-tracking branches in the local repository and the corresponding tracked branches in the remote repository?
No.
I recommend that you try running git ls-remote origin
(or replace origin
with the name of any other remote):
$ git ls-remote origin
28274d02c489f4c7e68153056e9061a46f62d7a0 HEAD
1ff88560c8d22bcdb528a6629239d638f927cb96 refs/heads/maint
28274d02c489f4c7e68153056e9061a46f62d7a0 refs/heads/master
0ac5344e619fec2068de9ab2afdb99d1af8854be refs/heads/next
5467631257c047b16d95929559dd1887d27b0ddc refs/heads/pu
68a0f56b615b61afdbd86be01a3ca63dca70edc0 refs/heads/todo
d5aef6e4d58cfe1549adef5b436f3ace984e8c86 refs/tags/gitgui-0.10.0
3d654be48f65545c4d3e35f5d3bbed5489820930 refs/tags/gitgui-0.10.0^{}
[snip -- lots more omitted]
Then, try git for-each-ref refs/heads
:
$ git for-each-ref refs/heads
You'll see similar output, but with different SHA-1s, and the word commit
sandwiched in the middle.
The main point here is that each "head" listed in refs/heads
—in both outputs, from ls-remote
and from for-each-ref
—represent the (local) branches that git push
considers when doing "matching". The heads that match are added to the push list, giving the set of refspecs (in internal form but easily expressed as name:name
or, if you used the --force
flag, +name:name
).
This happens even if there is no upstream configured for those local branches. Thus, if they (the remote) have a refs/heads/bob
that's meant for Bob Roberts to use, and you have a refs/heads/bob
that you're using for information about your plumbing bobs or fishing bobbers or whatever, git will attempt to match those up even though your bob
is not meant to go upstream.
push
vs fetch
The quote is for
git push
. Does it (especially the part for the case without a refspec) apply togit fetch
and togit pull
?
No.
The behavior of git fetch
is easier to explain, because it doesn't do all the weird stuff on you that git pull
does. The git pull
command is supposed to be convenient, but git pull
is trying to mash a fundamentally repository-wide "fetch" operation together with a fundamentally branch-constrained "merge" operation. These two do not play well together.1 (At best, I think git pull
could at some point be modified to do a simpler, repository-wide "fetch all, then check out and merge, pairwise, some branch(es) based on tracking configs and arguments" sequence. I think this would make it behave the way most people seem to expect it to. It would also be mostly backwards-incompatible, and behave the way it does now only for the simplest—but default—case: fetch all, then merge current branch only. But that's all speculation anyway.)
If you omit all refspecs, git fetch
reads your remote.$remote.fetch
configuration lines. (Note that this "omit all refspecs" clause means you must also avoid the old "Named file in $GIT_DIR/branches
" method, for instance.)
The default configuration line (singular) for a remote named origin
usually looks like this (view your local repository configuration file to see it):
[remote "origin"]
+refs/heads/*:refs/remotes/origin/*
This is the actual mechanism by which "remote-tracking branches" come into existence.
The fetch
process starts off with that same git ls-remote origin
step, which gets a list of every reference—branch, tag, or what-have-you—from the remote. The fetch
code goes on to match these up with each of the fetch =
lines under [remote "origin"]
. Asterisks on the left side of a refspec have the obvious meaning; asterisks on the right side are replaced with whatever the left-side asterisk matched. (In current versions of git the *
must match a "whole part", loosely defined as "stuff between slashes", but in the upcoming git this constraint will be relaxed—though I'm not sure to what useful end.)
You may have as many fetch =
lines as you like. Each original reference is run through each line to get a new (replacement) reference: if the left side matches, the right side provides the replacement. (If the right side is missing or empty, the behavior gets a bit weird for historical reasons, partly to keep git pull
working, partly because the desired behavior changed around git 1.8 or so.)
The results of all these replacements form the renamed references, and determine which references are copied at all; but there must be at most one result for each original reference: you can't map "their" refs/heads/foo to "your" refs/remotes/origin/bar and your refs/remotes/origin/zoink, for instance.
To make a repository act as a mirror, you can use +refs/*:refs/*
to bring over all references with no renaming (but this will clobber your local branches on each fetch
so this mostly makes sense only with --bare
).
("Pruning"—removing "dead" references based on what the other side sent during the startup negotiations for a fetch or push—is done separately and only if you set the --prune
flag.)
1It didn't start out this way. In fact, the original pull
script predated remotes and remote-tracking branches entirely. Commit 7ef76925d9c19ef74874e1735e2436e56d0c4897
split pull
into pull
and fetch
and then the two evolved in different directions over time; once "remotes" and "remote-tracking branches" came into existence, they'd evolved to the point that trying to mix them was, I think, just a bad idea, and now it's even worse. :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With