Skip to content

Instantly share code, notes, and snippets.

@x-yuri
Last active December 22, 2024 07:12
Show Gist options
  • Save x-yuri/82d4d970a8eb89d04f04d56aa923d399 to your computer and use it in GitHub Desktop.
Save x-yuri/82d4d970a8eb89d04f04d56aa923d399 to your computer and use it in GitHub Desktop.
git fetch
$ git fetch [<repository> [<refspec>...]]

Fetches references along with their history into the local repository.

<refspec> tells git which local reference to update with which remote one.

$ git fetch origin ba:refs/remotes/origin/ba

fetches the branch ba from origin to a remote-tracking branch origin/ba.

If :<dst> is not specified, then remote.<name>.fetch is used as a refmap. I.e. remote.<name>.fetch determines where to fetch a given reference.

If remote.origin.fetch is +refs/heads/*:refs/remotes/origin/* (which is normally the case when the repository was cloned or the remote was added with git remote add origin <URL>), then

$ git fetch origin ba

is equivalent to git fetch origin +ba:refs/remotes/origin/ba and (force-)fetches the branch ba from origin to a remote-tracking branch origin/ba.

If remote.origin.fetch is refs/heads/ba:refs/heads/bb, then the command is equivalent to git fetch origin ba:refs/heads/bb and fetches the branch ba from origin to the local branch bb.

If remote.<name>.fetch is not set, no references are created/updated locally. FETCH_HEAD is set to the tip of the fetched reference, and that's that.

If <refspec> is not passed (e.g. git fetch origin), it acts according to remote.<name>.fetch.

Normally (+refs/heads/*:refs/remotes/origin/*) it fetches remote branches to the corresponding remote-tracking branches.

If remote.origin.fetch is not set, then it fetches:

  • the upstream branch if set
  • the matching (the same name) branch if exists
  • the remote HEAD

to FETCH_HEAD.

If <repository> is not passed (git fetch), it fetches from:

If the remote was not chosen, it fetches nothing.

The relevant docs:

When no remote is specified, by default the origin remote will be used, unless there’s an upstream branch configured for the current branch.

https://git-scm.com/docs/git-fetch#_description

When no <refspec>s appear on the command line, the refs to fetch are read from remote.<repository>.fetch variables instead (see CONFIGURED REMOTE-TRACKING BRANCHES below).

https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-ltrefspecgt

When git fetch is run with explicit branches and/or tags to fetch on the command line, e.g. git fetch origin master, the <refspec>s given on the command line determine what are to be fetched (e.g. master in the example, which is a short-hand for master:, which in turn means "fetch the master branch but I do not explicitly say what remote-tracking branch to update with it from the command line"), and the example command will fetch only the master branch. The remote.<repository>.fetch values determine which remote-tracking branch, if any, is updated. When used in this way, the remote.<repository>.fetch values do not have any effect in deciding what gets fetched (i.e. the values are not used as refspecs when the command-line lists refspecs); they are only used to decide where the refs that are fetched are stored by acting as a mapping.

https://git-scm.com/docs/git-fetch#_configured_remote_tracking_branches

  • Update the remote-tracking branches:

    $ git fetch origin
    

    The above command copies all branches from the remote refs/heads/ namespace and stores them to the local refs/remotes/origin/ namespace, unless the remote.<repository>.fetch option is used to specify a non-default refspec.

  • Using refspecs explicitly:

    $ git fetch origin +seen:seen maint:tmp
    

    This updates (or creates, as necessary) branches seen and tmp in the local repository by fetching from the branches (respectively) seen and maint from the remote repository.

    The seen branch will be updated even if it does not fast-forward, because it is prefixed with a plus sign; tmp will not be.

  • Peek at a remote’s branch, without configuring the remote in your local repository:

    $ git fetch git://git.kernel.org/pub/scm/git/git.git maint
    $ git log FETCH_HEAD
    

    The first command fetches the maint branch from the repository at git://git.kernel.org/pub/scm/git/git.git and the second command uses FETCH_HEAD to examine the branch with git-log[1]. The fetched objects will eventually be removed by git’s built-in housekeeping (see git-gc[1]).

https://git-scm.com/docs/git-fetch#_examples

remote.<name>.fetch

The default set of "refspec" for git-fetch[1]. See git-fetch[1].

https://git-scm.com/docs/git-config#Documentation/git-config.txt-remoteltnamegtfetch

a.bats:

strict() { set -euo pipefail; shopt -s inherit_errexit; "$@"; }

setup() {
    [ "$BATS_LIB_PATH" = /usr/lib/bats ] && BATS_LIB_PATH=$HOME/.bats/lib:$BATS_LIB_PATH
    bats_load_library bats-support
    bats_load_library bats-assert
    strict
}

# the state of the cloned repository (1 commit, 1 local branch ba)
# A (HEAD -> ba, origin/ba)

@test "fetches to the specified reference" {
    start_cloned_repo
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin ba:bb

    assert_equal_sha bb `cd ../a && git rev-parse ba`
}

@test "fetches to the reference in remote.<name>.fetch when no :<dst>" {
    start_cloned_repo
    git config remote.origin.fetch 'refs/heads/ba:refs/heads/bb'
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin ba

    assert_equal_sha bb `cd ../a && git rev-parse ba`
}

@test "allows non-fast-forwards when there's a + in remote.<name>.fetch" {
    start_cloned_repo
    git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
    (cd ../a && git commit --allow-empty --amend -m A2)

    git fetch origin ba

    assert_equal_sha origin/ba `cd ../a && git rev-parse ba`
}

@test "fetches to FETCH_HEAD when no remote.<name>.fetch" {
    start_cloned_repo
    git config unset remote.origin.fetch
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin ba

    assert_equal_sha FETCH_HEAD `cd ../a && git rev-parse ba`
}

@test ": fetches HEAD to FETCH_HEAD" {
    start_cloned_repo
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin :

    assert_equal_sha FETCH_HEAD `cd ../a && git rev-parse HEAD`
}

@test "fetches according to remote.<name>.fetch when no <refspec>" {
    start_cloned_repo
    git config remote.origin.fetch 'refs/heads/ba:refs/heads/bb'
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin

    assert_equal_sha bb `cd ../a && git rev-parse ba`
}

@test "fetches the upstream branch to FETCH_HEAD when no remote.<name>.fetch" {
    start_cloned_repo
    git push origin ba:bb; git branch -u origin/bb
    git config unset remote.origin.fetch
    (cd ../a && git checkout bb && git commit --allow-empty -m B)

    git fetch origin

    assert_equal_sha FETCH_HEAD `cd ../a && git rev-parse bb`
}

@test "fetches the matching branch to FETCH_HEAD when no upstream branch" {
    start_cloned_repo
    git config unset remote.origin.fetch
    git branch --unset-upstream
    (cd ../a && git commit --allow-empty -m B)

    git fetch origin

    assert_equal_sha FETCH_HEAD `cd ../a && git rev-parse ba`
}

@test "fetches the remote HEAD to FETCH_HEAD when no matching branch" {
    start_cloned_repo
    git config unset remote.origin.fetch
    (cd ../a && git commit --allow-empty -m B)
    git checkout -b bb

    git fetch origin

    assert_equal_sha FETCH_HEAD `cd ../a && git rev-parse HEAD`
}

@test "fetches from the specified repository" {
    start_cloned_repo
    git remote add origin2 ../a2
    (cd ../a2 && git commit --allow-empty -m B)

    git fetch origin2

    assert_equal_sha origin2/ba `cd ../a2 && git rev-parse ba`
}

@test "uses the upstream branch to choose a remote" {
    start_cloned_repo
    git remote add origin2 ../a2
    git fetch origin2; git branch -u origin2/ba; git branch -dr origin2/ba
    (cd ../a2 && git commit --allow-empty -m B)

    git fetch

    assert_equal_sha origin2/ba `cd ../a2 && git rev-parse ba`
}

@test "chooses a non-origin remote when no upstream branch and no other remotes" {
    start_cloned_repo
    git remote rm origin
    git remote add origin2 ../a2
    (cd ../a2 && git commit --allow-empty -m B)

    git fetch

    assert_equal_sha origin2/ba `cd ../a2 && git rev-parse ba`
}

@test "chooses the origin remote when no upstream branch and more than 1 remote" {
    start_cloned_repo
    git branch --unset-upstream
    (cd ../a && git commit --allow-empty -m B)
    git remote add origin2 ../a2

    git fetch

    assert_equal_sha origin/ba `cd ../a && git rev-parse ba`
}

@test "fetches nothing when more than 1 remote and the origin remote doesn't exist" {
    start_cloned_repo
    git remote rm origin
    git remote add origin2 ../a2
    git remote add origin3 ../a3

    git fetch

    refute_branch refs/remotes/origin2/ba
    refute_branch refs/remotes/origin3/ba
}

@test "doesn't fetch into checked out branches" {
    start_cloned_repo

    run git fetch origin ba:ba

    assert_equal "$status" 128
    assert_output -p "fatal: refusing to fetch into branch 'refs/heads/ba' checked out at"
}

assert_equal_sha() {
    assert_equal "`git rev-parse "$1"`" "`git rev-parse "$2"`"
}

assert_not_equal_sha() {
    ! assert_equal_sha "$1" "$2"
}

assert_branch() {
    git show-ref --verify --quiet "$1"
}

refute_branch() {
    run git show-ref --verify --quiet "$1"
    assert_equal "$status" 1
}

start_cloned_repo() {
    (mkrepos)
    cd "$BATS_TEST_TMPDIR"
    git clone a b
    cd b
}

mkrepos() {
    cd "$BATS_TEST_TMPDIR"
    mkdir a
    (cd a
    git init
    git branch -m ba
    git config user.email [email protected]
    git config user.name "Your Name"
    git commit --allow-empty -m A)

    cp -r a a2
    cp -r a a3
}

ml() { for l; do printf '%s\n' "$l"; done; }
$ docker run --rm -itv "$PWD":/app -w /app alpine:3.21
/ # apk add git bash ncurses
/ # git clone https://github.com/bats-core/bats-core ~/.bats
/ # git clone https://github.com/bats-core/bats-support ~/.bats/lib/bats-support
/ # git clone https://github.com/bats-core/bats-assert ~/.bats/lib/bats-assert
/ # ~/.bats/bin/bats a.bats
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment