Skip to content

Instantly share code, notes, and snippets.

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

Updates remote references along with their history.

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

$ git push origin ba:bb

pushes the local branch ba to the branch bb at origin. If the remote branch doesn't exist it gets created.

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

If remote.origin.push is +refs/heads/*:refs/heads/*, then

$ git push origin ba

is equivalent to git push origin +ba:refs/heads/ba and (force-)pushes the local branch ba to the branch ba at origin.

If remote.origin.push is refs/heads/ba:refs/heads/bb, then the command is equivalent to git push origin ba:refs/heads/bb and pushes the local branch ba to the branch bb at origin.

If remote.<name>.push is not set and push.default is upstream, then it (git push <repository> <refspec> without :<dst>) pushes the local branch to its upstream branch. If push.default is not upstream, to its matching (the same name) branch.

: is a refspec that tells git to update matching branches. I.e. for each local branch l, if there's a remote branch r that has the same name, then push l to r:

$ git push origin :

If <refspec> is not passed, then remote.<name>.push is used instead (git push origin is equivalent to git push origin <remote.origin.push>).

If remote.origin.push is ba, then

$ git push origin

is equivalent to git push origin ba.

If <refspec> is not passed and remote.<name>.push is not set, then see push.default.

The relevant docs:

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[1] for the meaning of push.default).

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

If git push [<repository>] without any <refspec> argument is set to update some ref at the destination with <src> with remote.<repository>.push configuration variable, :<dst> part can be omitted—​such a push will update a ref that <src> normally updates without any <refspec> on the command line. Otherwise, missing :<dst> means to update the same ref as the <src>.

The special refspec : (or +: to allow non-fast-forward updates) directs Git to push "matching" branches: for every branch that exists on the local side, the remote side is updated if a branch of the same name already exists on the remote side.

https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrefspecgt82308203

git push origin

Without additional configuration, pushes the current branch to the configured upstream (branch.<name>.merge configuration variable) if it has the same name as the current branch, and errors out without pushing otherwise.

The default behavior of this command when no <refspec> is given can be configured by setting the push option of the remote, or the push.default configuration variable.

For example, to default to pushing only the current branch to origin use git config remote.origin.push HEAD. Any valid <refspec> (like the ones in the examples below) can be configured as the default for git push origin.

git push origin :

Push "matching" branches to origin. See <refspec> in the OPTIONS section above for a description of "matching" branches.

git push origin master

Find a ref that matches master in the source repository (most likely, it would find refs/heads/master), and update the same ref (e.g. refs/heads/master) in origin repository with it. If master did not exist remotely, it would be created.

git push origin HEAD

A handy way to push the current branch to the same name on the remote.

git push mothership master:satellite/master dev:satellite/dev

Use the source ref that matches master (e.g. refs/heads/master) to update the ref that matches satellite/master (most probably refs/remotes/satellite/master) in the mothership repository; do the same for dev and satellite/dev.

See the section describing <refspec>... above for a discussion of the matching semantics.

This is to emulate git fetch run on the mothership using git push that is run in the opposite direction in order to integrate the work done on satellite, and is often necessary when you can only make connection in one way (i.e. satellite can ssh into mothership but mothership cannot initiate connection to satellite because the latter is behind a firewall or does not run sshd).

After running this git push on the satellite machine, you would ssh into the mothership and run git merge there to complete the emulation of git pull that were run on mothership to pull changes made on satellite.

git push origin HEAD:master

Push the current branch to the remote ref matching master in the origin repository. This form is convenient to push the current branch without thinking about its local name.

git push origin master:refs/heads/experimental

Create the branch experimental in the origin repository by copying the current master branch. This form is only needed to create a new branch or tag in the remote repository when the local name and the remote name are different; otherwise, the ref name on its own will work.

git push origin +dev:master

Update the origin repository’s master branch with the dev branch, allowing non-fast-forward updates. This can leave unreferenced commits dangling in the origin repository. Consider the following situation, where a fast-forward is not possible:

o---o---o---A---B  origin/master
     \
      X---Y---Z  dev

The above command would change the origin repository to

      A---B  (unnamed branch)
     /
o---o---o---X---Y---Z  master

Commits A and B would no longer belong to a branch with a symbolic name, and so would be unreachable. As such, these commits would be removed by a git gc command on the origin repository.

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

remote.<name>.push

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

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

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 "pushes to the specified branch" {
    start_cloned_repo
    git commit --allow-empty -m B

    git push origin ba:ba

    assert_equal_sha origin/ba ba
}

@test "the remote branch gets created when not exists" {
    start_cloned_repo
    git commit --allow-empty -m B

    git push origin ba:bb

    assert_branch refs/remotes/origin/bb
}

@test "remote.<name>.push is used as a refmap when no :<dst>" {
    start_cloned_repo
    git config remote.origin.push refs/heads/ba:bb
    git commit --allow-empty -m B

    git push origin ba

    assert_equal_sha origin/bb ba
}

@test "allows non-fast-forwards when there's a + in remote.<name>.push" {
    start_cloned_repo
    git config remote.origin.push +refs/heads/ba:ba
    git commit --allow-empty --amend -m A2

    git push origin ba

    assert_equal_sha origin/ba ba
}

@test "but <branch> without refs/heads doesn't match the branch" {
    start_cloned_repo
    git config remote.origin.push ba:bb
    git commit --allow-empty -m B

    git push origin ba

    refute_branch refs/remotes/origin/bb
}

@test "pushes to the upstream branch when remote.<name>.push is not set and push.default = upstream" {
    start_cloned_repo
    git config push.default upstream
    git push origin ba:bb; git branch -u origin/bb
    git commit --allow-empty -m B

    git push origin ba

    assert_equal_sha origin/bb ba
}

@test "pushes to the matching branch when remote.<name>.push is not set and push.default != upstream" {
    start_cloned_repo
    git push origin ba:bb; git branch -u origin/bb
    git commit --allow-empty -m B

    git push origin ba

    assert_equal_sha origin/ba ba
}

@test "the remote branch gets created when not exists (no :<dst>)" {
    start_cloned_repo
    git checkout -b bb
    git commit --allow-empty -m B

    git push origin bb

    assert_branch refs/remotes/origin/bb
}

@test ": pushes when the matching branch exists" {
    start_cloned_repo
    git commit --allow-empty -m B

    git push origin :

    assert_equal_sha origin/ba ba
}

@test ": doesn't push when no matching branch" {
    start_cloned_repo
    git checkout -b bb
    git commit --allow-empty -m B

    git push origin :

    refute_branch refs/remotes/origin/bb
}

@test ": pushes other branches" {
    start_cloned_repo
    git commit --allow-empty -m B
    git checkout -b bb

    git push origin :

    assert_equal_sha origin/ba ba
}

@test "remote.<name>.push is used when no <refspec>" {
    start_cloned_repo
    git config remote.origin.push ba:bb
    git commit --allow-empty -m B

    git push origin

    assert_equal_sha origin/bb ba
}

@test "push.default doesn't affect it when remote.<name>.push is set" {
    start_cloned_repo
    git config push.default upstream
    git config remote.origin.push ba
    git push origin ba:bb; git branch -u origin/bb
    git commit --allow-empty -m B

    git push origin

    assert_equal_sha origin/ba ba
}

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() {
    (mkrepo)
    cd "$BATS_TEST_TMPDIR"
    git clone --bare a a.git
    git clone a.git b
    cd b
    git config user.email [email protected]
    git config user.name "Your Name"
}

mkrepo() {
    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)
}

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