Why do git fetch origin and git fetch : behave differently?
Posted By: Anonymous
While I am working on child-branch
on my working tree, I am informed that the remote main
(master) branch was updated. Then, I want to merge those changes into my child-branch
, without the need to checkout and pull main
. If I use git fetch --all
, I don’t get the result I expect:
user MINGW64 /d/DATA/Sources/Git/GitTest (child-branch)
$ git fetch --all
Fetching origin
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 652 bytes | 54.00 KiB/s, done.
From https://github.com/myrepo/GitTest
b84530e..4d6f085 main -> origin/main
user MINGW64 /d/DATA/Sources/Git/GitTest (child-branch)
$ git merge main
Already up to date.
But, if I go with git fetch origin main:main
, it does do what I expect:
user MINGW64 /d/DATA/Sources/Git/GitTest (child-branch)
$ git fetch origin main:main
From https://github.com/myrepo/GitTest
b84530e..4d6f085 main -> main
user MINGW64 /d/DATA/Sources/Git/GitTest (child-branch)
$ git merge main
Updating b84530e..4d6f085
Fast-forward
File_02 | 1 +
1 file changed, 1 insertion(+)
create mode 100644 File_02
I want to understand why fetch behaves different with these different parameters because I am working on a project with many repositories, and normally I work on a child-branch of the master branch in each repository.
I want to write a command that updates the master branches of each repository contained in my project folder automatically, so that I don’t need to checkout and pull each master branch of each repository, and later I can just merge those changes into my child branches. I tried this:
find . -name .git -type d | xargs -n1 -I% sh -c 'echo %; git --git-dir=% --work-tree=%/.. fetch --all --recurse-submodules;'
But it doesn’t work because fetch --all
does not do what I expect, and I cannot use fetch origin main:main
because each repository may have different names for the master branches (the name of the master branch in the remote can be master, integration or main)
Is it possible to use some git command similar to fetch --all
that allows me to have the changes ready to merge into my local child-branches?
Solution
TL;DR
The key to understanding this aspect of Git is that your branches are completely independent of any other Git’s branches. You may or may not choose to use similar names, e.g., if you have a branch named feature/tall
, you might want some other Git also to have a branch named feature/tall
. But when you do this, these are still completely different branches. You do not share branches with some other Git repository. You only share commits with that Git repository. They have their own branch names.
Long
The command:
git fetch --all
means: run git fetch
against all remotes. So the question here is how many remotes you have listed: what does git remote
print? If it prints one remote name, specifically origin
, then git fetch --all
means the same thing as git fetch origin
.
What that means … well, your job at this point is to read the git fetch
documentation. It is long and quite complicated and if you need help interpreting it, that would be unsurprising. So let’s press on:
But, if I go with
git fetch origin main:main
, it does do what I expect …
Let’s take a look at the SYNOPSIS section of the documentation:
SYNOPSIS
git fetch [<options>] [<repository> [<refspec>…​]]
git fetch [<options>] <group>
git fetch --multiple [<options>] [(<repository> | <group>)…​]
git fetch --all [<options>]
All the various options start with one or two dashes, so origin
is not an option. You are not using --all
this time, so the final line is not relevant, and you are not using --multiple
, so the third line is not relevant. Hence origin
can only either be a <group>
—in which case no additional parameters are allowed; main:main
would be an error here—or a <repository>
.
This means the first line is the one that applies: we’re doing git fetch <repository> <refspec>
. So origin
here is the repository and main:main
is the refspec.
The next sections of the documentation are quite long and confusing: git fetch
has a very long history behind it, dating back to before the invention of the so-called remote. We get to skip over most of this because origin
is a remote
, so we now can skip directly down to this section:
REMOTES
The name of one of the following can be used instead of a URL as<repository>
argument:
- a remote in the Git configuration file:
$GIT_DIR/config
,- [two more options snipped here].
All of these also allow you to omit the refspec from the command line because they each contain a refspec which git will use by default.
Hence origin
supplies both a URL—so that you don’t have to put in some long https://
or ssh://
URL here—and a default refspec. That default refspec is the one that git fetch --all
uses; you’d get the same effect with git fetch origin
, which omits the separate <refspec>
argument.
Last, then, we need to look at what a <refspec>
is. This requires going back up from the REMOTES section, and it gets rather confusing and detailed.
Rather than quoting the documentation at this point, I will summarize this, leaving out the newfangled negative refspecs. In its second-simplest form, a refspec is just a pair of words, separated by a colon, such as main:main
. The name on the left is the source and the name on the right is the destination.
The default refspec for most remotes, however, is not this second-simplest form. In fact, the setting you are likely to find, if you run:
git config --get-all remote.origin.fetch
is:
+refs/heads/*:refs/remotes/origin/*
This uses fully qualified references, i.e., names that begin with refs/
. A fully qualified reference is unambiguous.
Given a reference like zorg
, is this a branch name, or a tag name, or what? It’s not immediately clear to anyone or anything, including Git itself. Branch names, in Git, internally begin with refs/heads/
. Tag names internally begin with refs/tags/
. Another form of name, the remote-tracking name, begins with refs/remotes/
and goes on to name the specific remote. Using this fully-spelled-out form tells Git that refs/heads/zorg
is definitely a branch name, or refs/remotes/origin/zorg
is definitely a remote-tracking name, while refs/tags/v1.2
is definitely a tag name. When writing any form of script, it’s wise to use the fully-spelled-out names, so that there is no ambiguity.
When you use an ambiguous name, such as main
, Git will try to guess whether this is a branch name, or a tag name, or whatever. If there is in fact a branch named main
, and no tag named main
, Git will guess that you really meant refs/heads/main
. So:
git fetch origin main:main
winds up being short for:
git fetch origin refs/heads/main:refs/heads/main
because there is a branch named main
(and no tag named main
to confuse things).
In the end, this explains why your git fetch origin main:main
did what you wanted it to do: Git qualified main
to refs/heads/main
(in both cases) and wound up updating your local refs/heads/main
. But note that this is very different from the default refspec of:
+refs/heads/*:refs/remotes/origin/*
This default refspec makes use of two special features:
-
It begins with a plus sign
+
. This makes it a forced refspec. That is, the:destination
part of the update will happen regardless of whether the update is a fast-forward operation. For more about that, search StackOverflow. -
It uses an asterisk
*
in both the source and destination parts. The source part,refs/heads/*
, matches every branch in the source repository. That is, we match all branch names over onorigin
. The destination part,refs/remotes/origin/*
, has its*
expanded to put in whatever name was matched on the left side. So their—origin
‘s—main
branch becomes ourrefs/remotes/origin/main
, i.e., our remote-tracking name for theirmain
. If they have a branch nameddevelop
, our Git will create or update a remote-tracking namerefs/remotes/origin/develop
. If they have a branch namedfeature/tall
, our Git will create or update ourrefs/remotes/origin/feature/tall
.
In this way, running git fetch origin
will have our Git get, from their Git repository over at origin
, all new commits they have on all branches, bring them over, put them in our Git repository here, and create-or-update all our remote-tracking names to match their branch names. That way, after git fetch origin
, we have an origin/*
for every branch they have over at origin
.
Is it possible to use some git command similar to
fetch --all
that allows me to have the changes ready to merge into my local child-branches?
That’s just git fetch
. It will update all the origin/*
names. These are the names you should use to refer to commits you got from the other Git.
Your branches are not child branches. Branches have no parent/child relationships. Humans being human, we like to use same or similar names to keep track of similar commits, so after we make a branch zorg
in our Git we often like to have some other Git call its branch zorg
, or maybe jean-baptiste/zorg
or something. That lets us keep them together in our heads. But that’s just for us. All Git cares about are the commit hash IDs. Git has names—in our repository, for us to use, and in each other Git repository, for each other Git to use—to help find the hash IDs of commits. But it’s only the commits themselves that matter. We can change our names any time we like.
Answered By: Anonymous
Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.