On this page
Your pull request just merged. You’re already on the next branch, and
now your local main is stale: the merge landed on the remote, not in
your clone. Sooner or later you’ll cut a branch from main, rebase onto
it, or just read it, and you’d rather all of that see today’s history
instead of last week’s.
The move everyone knows is to go visit: git switch main, git pull,
git switch -. It works. It also checks out main for the privilege of
running one fast-forward, and a checkout stopped being free
years ago: dev servers restart, watchers
refire, generated files churn, and if .gitignore changed in the meantime
you come back to a wall of untracked files. Two context switches
to move one pointer.
Git can move a branch you’re not standing on. It has been able to all along.
The colon in the refspec
git fetch takes refspecs: <source>:<destination>. The source names a
ref on the remote; the destination names the ref in your repository that
the fetched history lands in. The everyday git fetch origin gets its
refspec from your config: +refs/heads/*:refs/remotes/origin/*, remote
branches in, tracking refs out. Write a refspec by hand and the
destination can be a local branch:
$ git branch --show-current
feature
$ git log --oneline -1 main
e7d9e76 init
$ git fetch origin main:main
From …/origin
e7d9e76..114c7a5 main -> main
e7d9e76..114c7a5 main -> origin/main
Two updates from one fetch: main:main moved your local branch, and
because its source is covered by the configured refspec, Git updated the
matching tracking ref origin/main too. And nothing else moved at all:
$ git log --oneline -1 main
114c7a5 Add lexer (#13)
$ git branch --show-current
feature
$ git status --porcelain
$
Fetch never touches your index or your working tree. No checkout, nothing
restarted, and main is current: git switch -c next main now starts
from today’s history, and git rebase main picks up the fresh base.
The destination doesn’t have to exist yet, and it doesn’t have to match the source’s name:
$ git fetch origin main:hotfix-base
From …/origin
* [new branch] main -> hotfix-base
A single fetch also takes as many refspecs as you care to give it, so several branches can come current in one trip.
The remote is optional
If a plain git fetch origin has already run (your hand, your editor, a
background sync), the new commits are sitting in your repository with
only the tracking ref pointing at them, and your local branch is the one
thing still stale. No second network trip needed: a repository
can fetch from itself.
$ git fetch origin
From …/origin
114c7a5..c21574c main -> origin/main
$ git fetch . origin/main:main
From .
114c7a5..c21574c origin/main -> main
$ git log --oneline -1 main
c21574c Add tests (#14)
. is the repository you’re in, treated as its own remote. The first
command moved the tracking ref over the network; the second copied it
onto the branch without any. The ref update is the same either way, so
everything that follows (the refusals, the fast-forward rule) applies to
a fetch from . exactly as it does to one from origin.
The branch you’re standing on is off limits
The exception is the branch that’s checked out. Switch to main and
try to update it the same way:
$ git fetch origin main:main
fatal: refusing to fetch into branch 'refs/heads/main' checked out at '…/repo'
Note the fatal: this aborts the whole fetch before anything transfers,
tracking ref included.
The refusal protects an agreement. A checkout is three things in
sync: the branch ref, the index, and the working tree. Fetch moves refs
and reconciles nothing, so aiming it at the checked-out branch would
move one of the three and leave the other two describing a commit that
is no longer HEAD. There’s a flag that overrides the refusal, and it
makes a tidy demonstration of why you shouldn’t:
$ git fetch --update-head-ok origin main:main # demo only — clean tree required
From …/origin
c21574c..0538f6b main -> main
c21574c..0538f6b main -> origin/main
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: docs.md
That is a staged deletion you never made. The fetched commit added
docs.md; your index still describes the tree from before it;
git status reports the difference as your doing. Commit from here
and you revert the change you just pulled. Provided the working tree
is clean, git reset --hard puts the three back in agreement by
rebuilding the index and working tree to match the moved branch; with
uncommitted work present it would destroy that work too. Then
git switch - back to feature.
The flag exists for git pull, which passes it on every fetch it runs.
A stock pull doesn’t need it: its fetch half only writes origin/*, and
the merge or rebase half is what moves your branch. But a pull carrying
a refspec (git pull origin main:main, for instance) does fetch
straight into the checked-out branch. What makes that safe is that pull
reconciles the index and working tree immediately afterward (its output
even says “fast-forwarding your working tree”); that reconciliation is
the step --update-head-ok alone skips.
The same refusal covers a branch checked out anywhere. If main lives
in a linked worktree, fetching into it fails from the primary checkout
too, and the message names the path holding it:
$ git worktree add ../wt-main main
Preparing worktree (checking out 'main')
HEAD is now at 0538f6b Add docs (#15)
$ git fetch origin main:main
fatal: refusing to fetch into branch 'refs/heads/main' checked out at '…/wt-main'
$ git worktree remove ../wt-main
Nor is the refusal a quirk of fetch: git branch -f declines to move a
checked-out branch with the same reasoning. For an idle branch, though,
the two are not interchangeable. branch -f repoints the ref wherever
you say, no questions asked; the refspec holds the fast-forward line
you’re about to meet. And for the branch you’re on, the updater is
the one you already know, git pull, for exactly the
reconciliation reason above.
When the branch has commits of its own
A fast-forward is the only move a plain refspec will make. If main has
a commit the remote doesn’t, because you committed to it by accident or
you’re carrying a local patch, the update is rejected:
$ git log --oneline -1 main
c540dc1 wip: note to self
$ git rev-parse --short origin/main
0538f6b
$ git fetch origin main:main
From …/origin
! [rejected] main -> main (non-fast-forward)
0538f6b..62fb195 main -> origin/main
$ git rev-parse --short origin/main
0538f6b
A fast-forward discards nothing; anything else could. It’s the same conservatism as the checkout refusal, applied to history instead of the working tree.
Look at the two rev-parse calls, though. The ! [rejected] line is
truthful; the origin/main line under it never took effect — the ref
reads 0538f6b before and after, whatever the output says about
62fb195. On the Git these transcripts ran on (2.51), a rejected
refspec takes the opportunistic tracking update down with it. The
manual doesn’t specify which way this goes, so another version may keep
the tracking update; the habit that holds everywhere is to let
rev-parse tell you what moved after any rejection, and to run a plain
git fetch origin if you want the tracking ref current.
You can insist. A leading + on the refspec (or --force)
permits the non-fast-forward:
$ git fetch origin +main:main
From …/origin
+ c540dc1...62fb195 main -> main (forced update)
0538f6b..62fb195 main -> origin/main
$ git log --oneline -1 'main@{1}'
c540dc1 wip: note to self
Notice the tracking line finally sticking: it’s the same
0538f6b..62fb195 the rejection printed and canceled. And notice what
the force is: a reset wearing fetch’s clothing. main now matches the
remote, and your local commit is off the branch — not destroyed,
main@{1} still names it, but off the branch, silently. If the stray
commit should keep a real name rather than a reflog entry, park it
before the force: git branch rescue main, then the forced fetch, and
afterward rescue points at your commit while main matches the remote.
If instead you meant to keep both sides on main, you want a merge or
a rebase, and those need a working tree. Any working tree satisfies
them, including one that exists for thirty seconds. Say the local
commit is deliberate this time, and the remote has moved again:
$ git fetch origin
From …/origin
62fb195..5a013dc main -> origin/main
$ git log --oneline -1 main
687bfb7 fix: local-only patch
$ git worktree add ../tmp main
Preparing worktree (checking out 'main')
HEAD is now at 687bfb7 fix: local-only patch
$ git -C ../tmp merge --no-edit origin/main
Merge made by the 'ort' strategy.
release.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 release.md
$ git worktree remove ../tmp
$ git log --oneline -2 main
a0ffdc9 Merge remote-tracking branch 'origin/main'
687bfb7 fix: local-only patch
Your own checkout never blinked. For the rebase preference,
git -C ../tmp rebase origin/main would have served the same way. Two
things about where this leaves you: main now carries a commit the
remote lacks, so the next plain main:main fetch is a rejection until
you push; and whether a merge like this one will conflict is
answerable before you run it,
from the same seat.
Or don’t do any of this
The feature this post has been circling ships in GitDesktop,
the Git client I work on. After a fetch, every branch row shows how far
it sits ahead or behind its upstream, and a branch with commits to pull
offers Update from origin/… in its context menu: no switching, your
checkout stays put. Under that item, in order: an ancestry check to
classify the case, then git fetch . origin/main:main when it’s a
fast-forward, and for a diverged branch the throwaway-worktree merge
from above — created, merged, removed, a conflicted merge aborted so
the branch is left exactly as it was. Aimed at the branch you’re
standing on, it does the one reasonable thing left and merges in place,
conflicts and all, the way a plain pull would.
A branch is a name for a commit, and a name can move without you standing on it. You check out a branch to work on it; updating it never required your presence.
Written while building GitDesktop.
A desktop Git client for GitHub, GitLab and Bitbucket — free and open source, with AI you control or hide entirely.