Remotes: push, pull & fetch

Git · Lesson 15 of 28 8 min read Updated

So far all of Priya's work lives on her laptop. A remote is another copy of the same repository, usually on GitHub, that she can send commits to and receive commits from. Remotes are how a team shares work, and how your project survives if your laptop is stolen on the train to Lucknow.

This lesson covers the commands for working with remotes: adding one, pushing, fetching, pulling, tracking branches, and what to do when Git rejects your push.

Adding a remote

When you clone, Git adds the remote origin for you. For a project you started with git init, you add it yourself after creating an empty repository on GitHub (the next lesson shows how). The name origin is only a convention for "the main remote":

Terminal
git remote add origin git@github.com:priya-sharma/sweet-shop.git
git remote -v
Output
origin	git@github.com:priya-sharma/sweet-shop.git (fetch)
origin	git@github.com:priya-sharma/sweet-shop.git (push)

Other remote commands: git remote rename origin github, git remote remove old-server, and git remote set-url origin <new-url> to switch between HTTPS and SSH addresses.

Pushing for the first time

git push uploads your commits to the remote. The first time, add -u (--set-upstream) so Git remembers that your local main belongs with origin's main:

Terminal
git push -u origin main
Output
Enumerating objects: 89, done.
Counting objects: 100% (89/89), done.
Delta compression using up to 8 threads
Compressing objects: 100% (79/79), done.
Writing objects: 100% (89/89), 8.83 KiB | 30.00 KiB/s, done.
Total 89 (delta 34), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:priya-sharma/sweet-shop.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Git packed 89 objects (all the commits, folders and file versions) and sent them. The last line is the important one: main now tracks origin/main. Its upstream is set, so from now on a plain git push or git pull on this branch knows where to go.

git branch -vv shows each branch's upstream in square brackets:

Terminal
git branch -vv
Output
* main c378c08 [origin/main] Add sweet name field

Remote-tracking branches

After a push or fetch, your repository contains remote-tracking branches such as origin/main. They are read-only bookmarks that record where the branches on the remote were the last time you talked to it. They do not update by themselves; only fetch, pull and push update them. git branch -a lists them next to your local branches.

Fetching: see what others did

Kabir cloned the repository, committed "Announce free delivery" and pushed. Priya runs git fetch, which downloads new commits and updates origin/main, but does not change her own main or her files:

Terminal
git fetch
git status
Output
From github.com:priya-sharma/sweet-shop
   c378c08..b8b1b21  main       -> origin/main
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

Fetching is always safe. Before bringing the changes in, Priya can review them. main..origin/main means "commits in origin/main that are not in main":

Terminal
git log --oneline main..origin/main
Output
b8b1b21 (origin/main, origin/HEAD) Announce free delivery

Pulling: fetch and integrate

git pull is git fetch followed by integrating origin/main into your main. Here her branch is simply behind, so it fast-forwards:

Terminal
git pull
Output
Updating c378c08..b8b1b21
Fast-forward
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

When your push is rejected

Now both of them work at once. Kabir pushes a team page; Priya, unaware, commits a link colour and tries to push:

Terminal
git push
Output
To github.com:priya-sharma/sweet-shop.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'github.com:priya-sharma/sweet-shop.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.

Git refuses because accepting her push would throw away Kabir's commit. This protects everyone. She must first combine his work with hers. But git pull now stops too, because the branches have diverged and Git wants her to choose how to combine them:

Output
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:
fatal: Need to specify how to reconcile divergent branches.

pull.rebase false creates a merge commit; pull.rebase true replays her local commit on top of Kabir's (see Rebasing); pull.ff only refuses unless a fast-forward is possible. Set one globally and the question never comes back. Priya rebases this time, then pushes:

Terminal
git pull --rebase
git push
Output
Successfully rebased and updated refs/heads/main.
To github.com:priya-sharma/sweet-shop.git
   64bc4ed..78e6efb  main -> main

Pushing and deleting other branches

Push a new branch the same way as main, with -u the first time. Delete a branch on the remote with --delete:

Terminal
git push -u origin reviews
git push origin --delete reviews
Output
To github.com:priya-sharma/sweet-shop.git
 * [new branch]      reviews -> reviews
branch 'reviews' set up to track 'origin/reviews'.
To github.com:priya-sharma/sweet-shop.git
 - [deleted]         reviews

Branches deleted on the remote by others stay in your list of origin/... branches until you prune them: git fetch --prune, or set fetch.prune to true once.

Force pushing safely

After you amend or rebase a branch you already pushed, its history no longer matches the remote's, and a normal push is rejected as "non-fast-forward". If the branch is yours alone, you may replace the remote copy. Always use --force-with-lease:

Terminal
git push --force-with-lease
Output
To github.com:priya-sharma/sweet-shop.git
 + c4f0c1a...3248043 search-box -> search-box (forced update)

--force-with-lease overwrites only if the remote branch is still where you last saw it. If someone pushed in the meantime, it refuses instead of silently deleting their work, which plain --force would do.

Common mistakes

  • Forgetting -u on the first push. Later, plain git push complains that the branch has no upstream. Run git push -u origin <branch> once.
  • Thinking git fetch changed your files. It only updates origin/... branches. Pull or merge to bring changes in.
  • Pulling with uncommitted changes. Commit or stash first so a merge problem does not mix with half-done work.
  • Fixing a rejected push with --force. That deletes your teammate's commits. Pull, then push.

Key points

  • A remote is another copy of the repository; origin is the usual name for the main one.
  • git push -u origin main publishes a branch and sets its upstream; afterwards plain git push works.
  • git fetch downloads and updates origin/... without touching your files; git pull fetches and integrates.
  • A rejected push means the remote has commits you lack: pull (merge or rebase), then push.
  • Use git push --force-with-lease only on your own branches, never on shared ones.

Exercise

Simulate a team without GitHub. Create a bare repository with git init --bare team.git, clone it twice into folders aarav and isha, and set a different user.name in each. Commit and push from aarav, then commit (without pulling) and push from isha. Read the rejection, fix it with a rebasing pull, push, and finally pull in aarav.

Show solution
Terminal
git init --bare team.git
git clone team.git aarav
git clone team.git isha
cd aarav
git config set user.name "Aarav Mehta"
git commit --allow-empty -m "Aarav: start project"
git push -u origin main
cd ../isha
git config set user.name "Isha Nair"
git commit --allow-empty -m "Isha: add notes"
git push -u origin main
git pull --rebase origin main
git push -u origin main
cd ../aarav
git pull

A bare repository has no working tree, just the history, which is exactly what a server like GitHub stores. Both clones warn "You appear to have cloned an empty repository", which is expected. --allow-empty makes a commit without changing any file, handy for tests like this. Isha's first push is rejected because Aarav pushed first; after git pull --rebase her commit sits on top of his, and her push succeeds.

Quick Quiz

1. What does -u do in git push -u origin main?

2. How is git fetch different from git pull?

3. Your push is rejected with "fetch first". What is the right fix?

4. Why is --force-with-lease safer than --force?

Learn Git with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call