Aller au contenu

Pushing to a remote

Pulling brings the remote's commits into the local repository. Pushing does the opposite: it sends local commits to the remote, so that the other copies can get them.

flowchart LR
    local["Local repository<br/>main"]
    remote[("Remote: origin<br/>main")]
    remote -- "git pull origin main" --> local
    local -- "git push origin main" --> remote

git push

git push <remote> <local-branch>

This sends the commits of <local-branch> to the branch of the same name on <remote>. Only commits are pushed: save your changes locally with git commit first. Uncommitted edits and staged files stay on your machine.

On the laptop, Sam adds a network printer and commits. git status shows that the local branch is now ahead of the remote:

$ git commit -am "Add network printer"
[main 1b9488f] Add network printer
 1 file changed, 1 insertion(+)
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

Push the local main to origin:

$ git push origin main
To /srv/git/homelab.git
   f9731d8..1b9488f  main -> main
Part Meaning
To /srv/git/homelab.git The remote that received the commits
f9731d8..1b9488f The remote's main moved from f9731d8 to 1b9488f
main -> main Local main was pushed to the remote's main

Over a network, the output also includes progress lines (Enumerating objects, Counting objects, Writing objects...) while Git transfers the data.

This is the commit the desktop received with git fetch on the previous page.

The push/pull workflow

When several copies of a repository are in use, each one pulls the others' work before adding its own:

sequenceDiagram
    participant D as Desktop
    participant O as origin (server)
    participant L as Laptop
    L->>L: commit "Add network printer"
    L->>O: git push origin main
    D->>O: git pull origin main
    O-->>D: "Add network printer"
    D->>D: commit "Add security camera"
    D->>O: git push origin main
    L->>O: git pull origin main
    O-->>L: "Add security camera"

The rule: pull before you push. The remote only accepts a push that builds on top of what it already has.

When the push is rejected

Sam notes the NAS backup job from the laptop and pushes it. Then, on the desktop, which has not pulled that commit, Sam adds a security camera and tries to push:

$ git commit -am "Add security camera"
[main 0a350f3] Add security camera
 1 file changed, 1 insertion(+)
$ git push origin main
To /srv/git/homelab.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '/srv/git/homelab.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The remote's main has a commit (Note NAS backup job) that the desktop does not have. Accepting the push would replace it and lose it, so the remote refuses. The two histories have diverged:

%%{init: {"gitGraph": {"showBranches": true, "rotateCommitLabel": false, "mainBranchName": "origin"}, "themeVariables": {"git0": "#43a047", "git1": "#1e88e5", "git2": "#fb8c00", "gitBranchLabel0": "#ffffff", "gitBranchLabel1": "#ffffff", "gitBranchLabel2": "#ffffff", "gitInv0": "#ffffff", "commitLabelFontSize": "13px"}}}%%
gitGraph TB:
    commit id: "5477908 Mention VLANs in the diagram to-do"
    branch desktop
    commit id: "0a350f3 Add security camera"
    checkout origin
    commit id: "9f7d9bf Note NAS backup job"
    checkout desktop
    merge origin id: "f46f496 Merge (after git pull)"

Pull first

The fix is to integrate the remote's work, then push again. Since both sides have new commits, the merge part of git pull is a three-way merge. Recent versions of Git do not guess how to integrate divergent histories, and ask you to choose:

$ git pull origin main
From /srv/git/homelab
 * branch            main       -> FETCH_HEAD
   5477908..9f7d9bf  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
Choice Option Result
Merge --no-rebase, or git config pull.rebase false Creates a merge commit joining both histories (what these notes use)
Rebase --rebase, or git config pull.rebase true Replays your local commits on top of the remote's, for a linear history
Fast-forward only --ff-only, or git config pull.ff only Only accepts pulls with no local commits; fails otherwise

To merge, set the preference once with git config --global pull.rebase false, or pass --no-rebase for a single pull:

$ git pull --no-rebase origin main
From /srv/git/homelab
 * branch            main       -> FETCH_HEAD
Merge made by the 'ort' strategy.
 services.md | 1 +
 1 file changed, 1 insertion(+)

A merge commit was created, with the default message Merge branch 'main' of /srv/git/homelab. As with git merge, Git opens the text editor to confirm that message. If the same lines had been changed on both sides, the pull would stop with a conflict, resolved exactly like on the merge conflicts page.

Now the desktop's main contains the remote's commits, and the push is accepted:

$ git log --oneline --graph -5
*   f46f496 Merge branch 'main' of /srv/git/homelab
|\
| * 9f7d9bf Note NAS backup job
* | 0a350f3 Add security camera
|/
* 5477908 Mention VLANs in the diagram to-do
* 7eb0ef7 Document print server
$ git push origin main
To /srv/git/homelab.git
   9f7d9bf..f46f496  main -> main

Pulling without the editor: --no-edit

git pull --no-edit origin main accepts the default merge commit message without opening the editor. It is convenient in scripts, but the editor is a last chance to review what is being merged: skip it only when you are confident about what the remote contains.

Do not force a rejected push

git push --force makes the remote accept the push anyway, by replacing its history with yours: the commits you had not pulled are lost for everyone. Never use it on a shared branch to get past a rejection. Pull, then push. If you really need to overwrite a branch that only you use, prefer --force-with-lease, which refuses if the remote contains commits you have not seen.

Pushing a new branch

Sam fixes a wrong IP address on a local branch, hotfix, which does not exist on the remote. Pushing a branch the remote does not have creates it there:

$ git switch -c hotfix
Switched to a new branch 'hotfix'
$ git commit -qam "Fix web01 IP address"
$ git push origin hotfix
To /srv/git/homelab.git
 * [new branch]      hotfix -> hotfix

* [new branch] confirms that hotfix now exists on origin. Other copies receive it at their next fetch, as origin/hotfix.

On GitHub, the remote also prints a link to open a pull request for the new branch:

remote:
remote: Create a pull request for 'hotfix' on GitHub by visiting:
remote:      https://github.com/sam-rivera/homelab/pull/new/hotfix
remote:

Upstream branches: git push -u

The push above created the remote branch, but the local hotfix is not linked to it: git status cannot say whether you are ahead or behind, and a plain git push or git pull does not know where to go. Add -u (--set-upstream) to record origin/hotfix as the upstream of the local branch:

$ git push -u origin hotfix
Everything up-to-date
branch 'hotfix' set up to track 'origin/hotfix'.

Usually -u is used on the very first push, which creates the branch and sets the upstream at once. From then on, the short forms work on that branch:

Command Equivalent
git push git push origin hotfix
git pull git pull origin hotfix

git branch -vv shows the upstream of each local branch in brackets:

$ git branch -vv
* hotfix 3a8f393 [origin/hotfix] Fix web01 IP address
  main   f46f496 [origin/main] Merge branch 'main' of /srv/git/homelab

A branch created by git clone (such as main) or by git switch <name> from a remote branch gets its upstream automatically.

Summary

Command Result
git push origin main Send the commits of local main to main on origin
git pull origin main then git push origin main Integrate the remote's work first when the push is rejected
git pull --no-rebase origin main Pull divergent histories by creating a merge commit
git config --global pull.rebase false Make merging the default way to pull divergent histories
git pull --no-edit origin main Pull and accept the default merge message, without the editor
git push origin hotfix Create hotfix on origin
git push -u origin hotfix Same, and set origin/hotfix as the upstream of the local branch
git branch -vv Show the upstream of each local branch

Common mistakes

  • Pushing before committing. Only commits are sent; uncommitted work stays local.
  • Pushing without pulling first. The push is rejected as soon as the remote has commits you do not have.
  • Forcing a rejected push. --force erases other people's commits on the remote.
  • Forgetting -u on the first push of a branch. Plain git push and git pull then fail on that branch until an upstream is set.

Hands-on labs

Three labs, from guided to more autonomous. Each one has its own setup, so you can do them in any order and repeat them as often as you like. Type the commands rather than pasting them, and read every output before moving on: the goal is to build reflexes, not to reach the end.

Lab 1: collaborate through a remote

Objective: push from two clones of the same repository, handle a rejected push, and publish a new branch.

Prerequisites and initial state: Git installed. The setup creates a "server" and two up-to-date clones, desktop and laptop, in ~/git-practice/push.

Setup:

mkdir -p ~/git-practice/push && cd ~/git-practice/push
git init -q --bare -b main server.git
git clone -q server.git desktop 2>/dev/null
cd desktop
git config user.name "Desktop User"
git config user.email "[email protected]"
git config pull.rebase false
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
printf '# Services\n- pi-dns: Pi-hole\n' > services.md
git add . && git commit -q -m "Create inventory" && git push -q origin main
cd .. && git clone -q server.git laptop
cd laptop
git config user.name "Laptop User"
git config user.email "[email protected]"
git config pull.rebase false
cd ..

Tasks:

  1. In laptop, add nas,192.168.1.20,storage to inventory.csv, commit with the message Add NAS, check git status, and push.
  2. In desktop, without pulling, add - nas: Samba shares to services.md, commit with the message Document NAS service, and push. Read the error.
  3. Still in desktop, pull main from origin without opening an editor, then push again.
  4. In desktop, create a branch hotfix, change the IP of pi-dns to 192.168.1.11, commit with the message Fix DNS IP, and push it to origin with an upstream.
  5. In laptop, fetch from origin, and pull main.

Expected result and verification:

  • At task 1, git status says Your branch is ahead of 'origin/main' by 1 commit before the push.
  • Task 2 fails with ! [rejected] main -> main (fetch first).
  • After task 3, git log --oneline --graph in desktop shows Merge branch 'main' of ... joining Add NAS and Document NAS service.
  • Task 4 prints * [new branch] hotfix -> hotfix and branch 'hotfix' set up to track 'origin/hotfix'.; git branch -vv shows [origin/hotfix].
  • At task 5, the fetch lists * [new branch] hotfix -> origin/hotfix, and after the pull, laptop's services.md contains - nas: Samba shares.
Solution
# 1. Laptop: commit and push
cd ~/git-practice/push/laptop
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -am "Add NAS"
git status                         # ahead of 'origin/main' by 1 commit
git push origin main

# 2. Desktop: commit and push without pulling
cd ../desktop
echo "- nas: Samba shares" >> services.md
git commit -am "Document NAS service"
git push origin main               # ! [rejected] main -> main (fetch first)

# 3. Pull (three-way merge), then push
git pull --no-edit origin main     # Merge made by the 'ort' strategy.
git log --oneline --graph
git push origin main

# 4. New branch with an upstream
git switch -c hotfix
sed -i 's/192.168.1.10/192.168.1.11/' inventory.csv
git commit -am "Fix DNS IP"
git push -u origin hotfix          # * [new branch] ... set up to track 'origin/hotfix'
git branch -vv

# 5. Laptop: receive everything
cd ../laptop
git fetch origin                   # * [new branch] hotfix -> origin/hotfix
git pull origin main               # Fast-forward
cat services.md                    # - nas: Samba shares
  • The setup sets pull.rebase false in each clone, so the pull in task 3 merges instead of asking how to reconcile.
  • The two commits of tasks 1 and 2 touched different files, so the merge needed no conflict resolution.
  • The laptop could start working on the fix with git switch hotfix, which creates a local hotfix tracking origin/hotfix.

Clean up when you are done: rm -rf ~/git-practice/push.

Lab 2: two admins, one line

Objective: play two people who change the same line at the same time, and go through the full cycle: rejected push, pull with a conflict, resolution, push.

Prerequisites and initial state: Git installed. The setup creates a "server" and two up-to-date clones in ~/git-practice/push-conflict: sam (Sam's desktop) and alex (Alex's laptop).

Setup:

mkdir -p ~/git-practice/push-conflict && cd ~/git-practice/push-conflict
git init -q --bare -b main server.git
git clone -q server.git sam 2>/dev/null
cd sam
git config user.name "Sam Rivera"
git config user.email "[email protected]"
git config pull.rebase false
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\nnas,192.168.1.20,storage\n' > inventory.csv
git add . && git commit -q -m "Create inventory" && git push -q origin main
cd .. && git clone -q server.git alex
cd alex
git config user.name "Alex Martin"
git config user.email "[email protected]"
git config pull.rebase false
cd ..

Tasks:

  1. Play Alex: in alex, change the role of the NAS from storage to nas, commit with the message Rename NAS role, and push.
  2. Play Sam, who has not pulled: in sam, change the IP of the NAS to 192.168.1.21, commit with the message Move NAS to .21, and push. Read the error.
  3. Still as Sam, pull main from origin. Read the result.
  4. Resolve the conflict by keeping both changes, conclude the merge without opening an editor, and push.
  5. Play Alex again: pull, and check that Alex's inventory now matches Sam's.

Expected result and verification:

  • Task 2 fails with ! [rejected] main -> main (fetch first).
  • Task 3 prints CONFLICT (content): Merge conflict in inventory.csv and Automatic merge failed.
  • Task 4: the NAS line reads nas,192.168.1.21,nas, and the push succeeds.
  • Task 5 is a fast-forward. git log --oneline --graph is identical in both clones, with the merge commit on top, and git status says up to date with 'origin/main' in both.
Solution
# 1. Alex pushes first
cd ~/git-practice/push-conflict/alex
sed -i 's/nas,192.168.1.20,storage/nas,192.168.1.20,nas/' inventory.csv
git commit -am "Rename NAS role"
git push origin main

# 2. Sam is behind without knowing it
cd ../sam
sed -i 's/nas,192.168.1.20,storage/nas,192.168.1.21,storage/' inventory.csv
git commit -am "Move NAS to .21"
git push origin main               # ! [rejected] main -> main (fetch first)

# 3. Pull: the merge part stops on the conflict
git pull origin main               # CONFLICT (content): Merge conflict in inventory.csv

# 4. Keep both changes on the same line
cat inventory.csv                  # <<<<<<< HEAD / nas,...21,storage / ======= / nas,...20,nas / >>>>>>> <hash>
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\nnas,192.168.1.21,nas\n' > inventory.csv
git add inventory.csv
git commit --no-edit               # Merge branch 'main' of ...
git push origin main

# 5. Alex catches up
cd ../alex
git pull origin main               # Fast-forward
cat inventory.csv                  # nas,192.168.1.21,nas
git log --oneline --graph
  • The rejection protects Alex's commit: Git never lets a push silently replace commits it does not have.
  • A conflict during git pull is resolved exactly like one during git merge: the pull is a fetch followed by a merge. The only visible difference: the last marker shows the hash of the fetched commit instead of a branch name.
  • Combining both edits in one line is often the right answer: each person changed a different column.

Clean up when you are done: rm -rf ~/git-practice/push-conflict.

Lab 3: final lab, a branch from start to finish

Objective: combine everything from this part: start from an empty server, work on a branch from two computers, merge it, and clean up everywhere.

Prerequisites and initial state: Git installed. No setup: you start from nothing in ~/git-practice/final-remote.

Tasks:

  1. Create an empty bare repository server.git whose default branch is main.
  2. Clone it into desktop. Configure a name and email in the clone, create inventory.csv (header and pi-dns line), commit with the message Create inventory, and push main with an upstream.
  3. Clone the server into laptop and configure it the same way. Create a branch monitoring, add mon01,192.168.1.50,monitoring, commit with the message Add monitoring server, and push the branch with an upstream.
  4. On desktop, fetch, switch to monitoring to review it, then merge it into main and push main.
  5. On laptop, switch to main, pull, and delete the local monitoring branch with the safe option.
  6. Still on laptop, delete the branch on the server: git push origin --delete monitoring.
  7. On desktop, delete the local monitoring, then fetch with --prune. List all branches in both clones.

Expected result and verification:

  • Task 2: the clone warns You appear to have cloned an empty repository., and the push prints branch 'main' set up to track 'origin/main'.
  • Task 4: the merge is a fast-forward.
  • Task 6 prints - [deleted] monitoring.
  • Task 7: the fetch prints - [deleted] (none) -> origin/monitoring. Finally, git branch -a shows only * main, remotes/origin/HEAD -> origin/main, and remotes/origin/main in both clones, and git log --oneline shows Add monitoring server and Create inventory in both clones.
Solution
# 1. Empty server
mkdir -p ~/git-practice/final-remote && cd ~/git-practice/final-remote
git init --bare -b main server.git

# 2. First commit, first push
git clone server.git desktop       # warning: You appear to have cloned an empty repository.
cd desktop
git config user.name "Desktop User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add inventory.csv
git commit -m "Create inventory"
git push -u origin main            # branch 'main' set up to track 'origin/main'.

# 3. Work on a branch from the laptop
cd ..
git clone server.git laptop
cd laptop
git config user.name "Laptop User"
git config user.email "[email protected]"
git switch -c monitoring
echo "mon01,192.168.1.50,monitoring" >> inventory.csv
git commit -am "Add monitoring server"
git push -u origin monitoring

# 4. Review and merge from the desktop
cd ../desktop
git fetch origin
git switch monitoring              # set up to track 'origin/monitoring'
cat inventory.csv
git switch main
git merge monitoring               # Fast-forward
git push origin main

# 5. Laptop catches up and cleans up
cd ../laptop
git switch main
git pull origin main               # Fast-forward
git branch -d monitoring

# 6. Delete the branch on the server
git push origin --delete monitoring   # - [deleted] monitoring

# 7. Desktop cleans up too
cd ../desktop
git branch -d monitoring
git fetch --prune origin           # - [deleted] (none) -> origin/monitoring
git branch -a
cd ../laptop && git branch -a
  • Cloning an empty repository works: the first git push -u origin main creates main on the server.
  • Deleting a branch happens in three places: the local branch (git branch -d), the branch on the server (git push origin --delete), and the remote-tracking branch in other clones (git fetch --prune).

Clean up when you are done: rm -rf ~/git-practice/final-remote.