Skip to content
Fix Code Error

Why do git fetch origin and git fetch : behave differently?

June 28, 2021 by Code Error
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 on origin. 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 our refs/remotes/origin/main, i.e., our remote-tracking name for their main. If they have a branch named develop, our Git will create or update a remote-tracking name refs/remotes/origin/develop. If they have a branch named feature/tall, our Git will create or update our refs/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

Related Articles

  • Why call git branch --unset-upstream to fixup?
  • Checkout another branch when there are uncommitted changes…
  • Why do I have to "git push --set-upstream origin "?
  • Git merge with force overwrite
  • Git workflow and rebase vs merge questions
  • Ubuntu apt-get unable to fetch packages
  • Difference between git checkout --track origin/branch and…
  • What exactly does the "u" do? "git push -u origin master" vs…
  • How do I add files and folders into GitHub repos?
  • fatal: does not appear to be a git repository

Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.

Post navigation

Previous Post:

how to while loop fullscreen mode

Next Post:

c++ read file in binary mode into object failed but is ok in stdin and file read in text

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • Get code errors & solutions at akashmittal.com
© 2022 Fix Code Error