GitGroundworkGit
7 / 18 written chapters

G7 · Part 2 · Working together

Remotes and syncing

A remote is a URL with a nickname, and origin/main is your cached memory of it. fetch, pull and push only move refs and objects between the two.

9 min read3 diagrams8 sections
08On this page
  1. Three places a branch lives
  2. What fetch actually does
  3. fetch vs pull
  4. Pushing
  5. --force, --force-with-lease and --force-if-includes
  6. Multiple remotes and the fork workflow
  7. Tags on remotes
  8. Remote housekeeping
Fresher Mid Senior

A remote is a nickname for another copy of the repository: origin by convention for the one you cloned from, upstream by convention for the original project when you have forked it. Git has no central server built in. Every clone is a full repository, and a remote is simply another repository you have agreed to exchange commits with.

Three places a branch lives

The word "main" can mean three different things, and most confusion about syncing comes from mixing them up.

git pull = git fetch, then merge or rebase ON THE SERVER REMOTE-TRACKING, IN YOUR CLONE LOCAL BRANCH main refs/heads/main origin/main refs/remotes/origin/main main refs/heads/main fetch merge / rebase git push
Only the middle box is updated by fetch, and you never commit to it. Your own branch changes only when you merge or rebase, and the server changes only when you push.

origin/main is not the remote's main branch. It is your local cached snapshot of where main was the last time you talked to the server. It only updates when you fetch, pull or push. This is why git status saying "Your branch is up to date with 'origin/main'" can be wrong: it compares against the cache without asking the server. To ask the server without downloading anything, use git ls-remote origin main.

What fetch actually does

A remote is a few lines in .git/config. The line that does the work is the refspec:

.git/config after a clone

[remote "origin"]
	url = git@github.com:acme/shop.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main

Read the refspec as source:destination. The left side names refs on the server, refs/heads/*, which means every branch. The right side names where to store them locally, under refs/remotes/origin/. The leading + means "update the destination even if it is not a fast-forward", which is why a teammate's force push to their branch shows up as a (forced update) instead of an error. So a fetch is: ask the server which refs it has, download every object you are missing, then rewrite your refs/remotes/origin/* to match.

git fetch, line by line

$ git fetch --prune origin
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 9 (delta 4), reused 7 (delta 3), pack-reused 0
Unpacking objects: 100% (9/9), 1.21 KiB | 206.00 KiB/s, done.
From github.com:acme/shop
 - [deleted]         (none)        -> origin/fix/null-cart
   c04b8a9..e71d3f2  main          -> origin/main
 + 5a1c2e0...9d8e7f6 feat/search   -> origin/feat/search  (forced update)
 * [new branch]      fix/tax-round -> origin/fix/tax-round
 * [new tag]         v2.2.0        -> v2.2.0

Two dots mean a fast-forward. Three dots and a + mean the branch was rewritten and the old tip is no longer in its history. [deleted] comes from --prune: without it, a branch deleted on the server lives on as a stale origin/... ref in your clone forever. Make pruning the default with git config --global fetch.prune true.

You can fetch refs the default refspec ignores. GitHub, for example, exposes every pull request as refs/pull/<n>/head, so git fetch origin pull/412/head:pr-412 gives you a local branch of someone's pull request without adding their fork as a remote. After any fetch, FETCH_HEAD names what was just fetched.

fetch vs pull

fetch downloads new commits and updates your remote-tracking branches. It does not touch your local branches or your working files, so it is always safe to run. pull is fetch followed by an integration step into the current branch. That step changes your branch, so it can produce conflicts.

these are equivalent

$ git pull
$ git fetch origin && git merge origin/main

$ git pull --rebase
$ git fetch origin && git rebase origin/main

$ git pull --ff-only
$ git fetch origin && git merge --ff-only origin/main

Modern Git will not guess when your branch and its upstream have both moved. Unless you have told it how to reconcile, it stops:

pull on a diverged branch with no config

$ git pull
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.
SettingWhat pull does when you are behind and aheadGood for
pull.rebase falseCreates a merge commit, "Merge branch 'main' of github.com:acme/shop"Teams that never rewrite anything
pull.rebase trueReplays your unpushed commits on top of the new upstreamMost people; keeps history linear
pull.ff onlyRefuses. You choose merge or rebase yourselfLong-lived branches like main, where surprise commits are unwelcome
AFTER git fetch: ahead 2, behind 3 R1 R2 R3 origin/main merge base L1 L2 main ← HEAD AFTER git pull --rebase origin/main L1′ L2′ main ← HEAD
Rebase-on-pull turns "ahead 2, behind 3" into "ahead 2, behind 0". L1′ and L2′ are new commits with new IDs, which is fine because nobody else had L1 and L2 yet. A merge-on-pull would instead add a merge commit joining L2 and R3.

Pushing

git push is the reverse of fetch: send the objects the server is missing, then ask it to move a ref. By default the server only accepts the move if it is a fast-forward, meaning the ref's current commit is an ancestor of the new one, so nothing already there is lost.

a rejected push

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

(fetch first) means the server has commits you do not even have. (non-fast-forward) means you have them but your branch does not include them, typically after you rebased. The fix for the first is to pull. The fix for the second depends on whether you meant to rewrite the branch.

useful push settings and forms

$ git config --global push.autoSetupRemote true
$ git push
branch 'feat/coupons' set up to track 'origin/feat/coupons'.

$ git push origin HEAD
$ git push origin feat/coupons:review/coupons
$ git push origin --delete feat/coupons
$ git push --dry-run

With push.autoSetupRemote (Git 2.37 and later) the first plain git push of a new branch creates it on the server and sets the upstream, so you never type -u origin my-branch again. The default push.default=simple pushes only the current branch, and only to a branch of the same name. The src:dst form pushes to a different name, and --delete deletes the remote branch.

--force, --force-with-lease and --force-if-includes

After a rebase or an amend, your branch is no longer a fast-forward of the server's copy, so you must force. The three flags differ in what they check first.

FlagChecks before overwritingWhat can go wrong
--forceNothingA teammate's commits pushed since your last fetch are silently deleted from the branch
--force-with-leaseThe server's ref still equals your origin/<branch>If something fetched in the background (an IDE, a prompt tool), your origin/ ref is already up to date, so the check passes even though you never looked at the new commits
--force-with-lease --force-if-includesThe lease, and that the remote tip appears in your local reflog, meaning you really integrated itVery little. Git 2.30 and later

a lease doing its job

$ git push --force-with-lease
To github.com:acme/shop.git
 ! [rejected]        feat/search -> feat/search (stale info)
error: failed to push some refs to 'github.com:acme/shop.git'

$ git push --force-with-lease --force-if-includes
To github.com:acme/shop.git
 ! [rejected]        feat/search -> feat/search (remote ref updated since checkout)
error: failed to push some refs to 'github.com:acme/shop.git'

$ git config --global alias.pushf 'push --force-with-lease --force-if-includes'

(stale info) means the branch moved on the server since your last fetch. Fetch, look at what arrived with git log feat/search..origin/feat/search, fold it in, then push again. (remote ref updated since checkout) is --force-if-includes catching the background-fetch case: your origin/feat/search was updated, but the new tip never became part of your branch. Protected branches such as main should reject force pushes on the server side anyway; these flags protect everything else.

For seniors: what actually crosses the wire

Current Git clients speak wire protocol v2 by default. A fetch starts with ls-refs, in which the client asks only for the refs its refspec names, so fetching one branch from a server with a million refs no longer downloads a million ref names. Then comes negotiation: the client sends want lines for the tips it needs and have lines for commits it already has, walking back through its own history, until the server has found a common base. The server builds one pack of the missing objects, reusing deltas it already has on disk. That is why a fetch after months away can spend visible time in "Counting objects", why a shallow clone makes later fetches harder for the server, and why GIT_TRACE_PACKET=1 git fetch is the tool when a fetch is inexplicably slow. Push runs the same exchange in the other direction, and the server applies all ref updates in one go only if you ask for git push --atomic.

Multiple remotes and the fork workflow

On open source projects, and in companies that use forks, you cannot push to the main repository. You push to your own copy, and open a pull request from it. That gives you two remotes.

upstream github.com/acme/shop origin github.com/you/shop your clone ~/code/shop pull request git fetch upstream git push origin
Commits flow in a loop: in from upstream, out to your fork, and back to upstream only through a reviewed pull request.

setting up and syncing a fork

$ git clone git@github.com:you/shop.git && cd shop
$ git remote add upstream git@github.com:acme/shop.git
$ git remote -v
origin    git@github.com:you/shop.git (fetch)
origin    git@github.com:you/shop.git (push)
upstream  git@github.com:acme/shop.git (fetch)
upstream  git@github.com:acme/shop.git (push)

$ git remote set-url --push upstream no_push
$ git fetch upstream
$ git switch -c fix/tax-round upstream/main
$ git push -u origin fix/tax-round

$ git fetch upstream
$ git rebase upstream/main
$ git push --force-with-lease

Branch from upstream/main, not from your fork's main, which is usually months behind. The set-url --push ... no_push line gives the upstream remote a push URL that cannot work, so an accidental git push upstream fails instead of landing on the real project if you happen to have write access. Your fork's own main does not need to be kept in sync at all.

Tags on remotes

Tags do not travel the way branches do, which surprises people at release time.

  • git push sends no tags. Push one with git push origin v2.2.0, or all of them with git push origin --tags.
  • git push --follow-tags also sends any annotated tags reachable from the commits being pushed. Set push.followTags true to make it the default. Lightweight tags are never sent this way.
  • git fetch brings tags that point into the history it downloaded. Tags on unrelated commits need git fetch --tags.
  • Tags are not namespaced per remote. There is no origin/v2.2.0, so a fetched tag lands directly in refs/tags/, and Git will not overwrite a local tag of the same name that points elsewhere: ! [rejected] v2.2.0 -> v2.2.0 (would clobber existing tag).
  • Deleting a tag on the server is git push origin --delete v2.2.0. Others still have it until they run git fetch --prune --prune-tags. This is why published tags should never move.

Remote housekeeping

TaskCommand
See a remote's URLs, branches and tracking stategit remote show origin
List refs on the server without fetchinggit ls-remote --heads origin
Remove stale remote-tracking branchesgit remote prune origin
Fix origin/HEAD after the default branch is renamedgit remote set-head origin --auto
Move a repository to a new hostgit remote set-url origin git@gitlab.com:acme/shop.git
Fetch from every remote at oncegit fetch --all --prune
Clone only the latest commit, for CIgit clone --depth 1 <url>
Clone full history but download file contents on demandgit clone --filter=blob:none <url>
Shallow or partial?

A shallow clone (--depth 1) cuts history off, so git log, git blame and git merge-base stop at the cut and can give wrong answers. It is right for a build that only needs the files. A partial clone (--filter=blob:none) keeps every commit and tree and fetches old file contents only when a command needs them. It is usually the better choice for developers working in a very large repository.