Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run git push, pull and fetch without refspec argument

Tags:

git

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?

like image 834
Tim Avatar asked Dec 30 '15 05:12

Tim


People also ask

What is Refspec in git?

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.

Can git fetch but not push?

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.

Does git fetch affect remote?

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.


1 Answers

(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. Overrides branch.<name>.remote for all branches, and is overridden by branch.<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 consulting remote.*.push configuration, and if it is not found, honors push.default configuration to decide what to push (See git-config for the meaning of push.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 action git 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.

On git push, if you configure "matching" behavior

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?

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.

On push vs fetch

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?

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. :-)

like image 127
torek Avatar answered Sep 22 '22 01:09

torek