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.
08On this page
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.
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.
| Setting | What pull does when you are behind and ahead | Good for |
|---|---|---|
pull.rebase false | Creates a merge commit, "Merge branch 'main' of github.com:acme/shop" | Teams that never rewrite anything |
pull.rebase true | Replays your unpushed commits on top of the new upstream | Most people; keeps history linear |
pull.ff only | Refuses. You choose merge or rebase yourself | Long-lived branches like main, where surprise commits are unwelcome |
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.
| Flag | Checks before overwriting | What can go wrong |
|---|---|---|
--force | Nothing | A teammate's commits pushed since your last fetch are silently deleted from the branch |
--force-with-lease | The 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-includes | The lease, and that the remote tip appears in your local reflog, meaning you really integrated it | Very 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.
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.
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 pushsends no tags. Push one withgit push origin v2.2.0, or all of them withgit push origin --tags.git push --follow-tagsalso sends any annotated tags reachable from the commits being pushed. Setpush.followTags trueto make it the default. Lightweight tags are never sent this way.git fetchbrings tags that point into the history it downloaded. Tags on unrelated commits needgit fetch --tags.- Tags are not namespaced per remote. There is no
origin/v2.2.0, so a fetched tag lands directly inrefs/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 rungit fetch --prune --prune-tags. This is why published tags should never move.
Remote housekeeping
| Task | Command |
|---|---|
| See a remote's URLs, branches and tracking state | git remote show origin |
| List refs on the server without fetching | git ls-remote --heads origin |
| Remove stale remote-tracking branches | git remote prune origin |
Fix origin/HEAD after the default branch is renamed | git remote set-head origin --auto |
| Move a repository to a new host | git remote set-url origin git@gitlab.com:acme/shop.git |
| Fetch from every remote at once | git fetch --all --prune |
| Clone only the latest commit, for CI | git clone --depth 1 <url> |
| Clone full history but download file contents on demand | git clone --filter=blob:none <url> |
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.
Finished reading?
0 of 18 chapters read in this series