Fetch, Pull and Push
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
This page follows Git 2.55.x, GitHub CLI 2.101.x, IntelliJ IDEA 2026.2, current stable VS Code with the GitHub
Pull Requests and Issues extension, and github.com as of September 2026, with a default branch named main.
Concept
Branches introduced remotes and remote-tracking branches:
origin is the local repository’s name for a remote repository, and origin/main is a local, read-only bookmark
that records where origin’s `main branch was the last time the local repository talked to it. Nothing keeps
origin/main current automatically — it only moves when a command explicitly contacts the remote, and that
command is git fetch.
git fetch downloads the commits, trees, blobs and tag objects that exist on the remote but not yet locally, and
updates the remote-tracking branches to match. It never touches the working tree or any local branch: after a
fetch, main is exactly where it was, and the new commits sit only under origin/main until something merges or
rebases them in. git pull is that "something" — it is git fetch immediately followed by an integration step
(a merge by default) that updates the current local branch and the working tree.
git push moves the opposite direction: it uploads the objects that exist on the current local branch but not on
origin, and asks the remote to move its branch ref to match. The remote only accepts the move if it is a
fast-forward — the remote’s current tip is an ancestor of what is being pushed — which is what keeps one
person’s push from silently discarding another person’s work.
The practical rule of thumb: git fetch is safe to run at any time because it can only add information locally;
git pull changes the current branch and can therefore create a merge commit or a conflict, so it deserves the
same attention as any other operation that touches the working tree.
Command line
Fetching
git fetch origin # download new objects and refs, update origin/*
git fetch --all # fetch from every configured remote, not just origin
git fetch --prune # also drop local origin/<branch> bookmarks whose remote branch was deleted
--prune matters once branches start being deleted after merge (see
Branches): without it, origin/old-feature lingers forever as
a stale remote-tracking branch even though old-feature no longer exists on GitHub. Derived from
the git-fetch manual page.
After a plain git fetch, compare the local branch to its remote-tracking branch before deciding what to do next:
git fetch origin
git log main..origin/main --oneline # commits on origin/main not yet on main
git diff main...origin/main # changes on origin/main since it diverged from main
Pulling
git pull # fetch + merge origin/<upstream> into the current branch
git pull --rebase # fetch + rebase the current branch onto origin/<upstream> instead
By default git pull only fast-forwards, and refuses when the branches have diverged until pull.rebase,
pull.ff or a command-line flag says how to reconcile them. Two configuration options set that choice once
instead of passing --rebase or --no-rebase on every invocation:
git config --global pull.rebase true # always rebase on pull, never merge
git config --global pull.ff only # refuse to pull unless it is a fast-forward (no merge, no rebase)
pull.rebase accepts true (rebase), false (merge, the historical default) or merges (rebase but preserve
existing merge commits); pull.ff accepts only, true or false. When neither is set and the branches have
diverged, modern Git refuses to guess and prints a hint similar to:
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
Merging, Rebasing and Conflicts explains
what the merge and rebase choice actually does and how to resolve the conflicts either one can raise. Derived from
the git-pull manual page.
Pushing
git push # push the current branch to its configured upstream
git push -u origin feature/42 # push and set origin/feature/42 as the upstream, in one step
-u (--set-upstream) only needs to run once per branch: after that, a plain git push and git pull on
feature/42 already know which remote branch to talk to. Forgetting it on the first push of a new branch is the
single most common friction point — Git then rejects a bare git push with a message that names the exact
git push --set-upstream origin feature/42 command to run instead. Setting push.autoSetupRemote removes the
need for -u altogether:
git config --global push.autoSetupRemote true
With that set, the first git push of a brand-new local branch creates and tracks the matching remote branch
automatically. Derived from the git-push manual page.
Recovering from a rejected push
A push is rejected with ! [rejected] … (non-fast-forward) whenever `origin’s branch has commits the local
branch does not — typically because a teammate pushed first. The recovery is always the same shape: bring those
commits in, deal with any conflict, then push again.
git fetch origin
git pull --rebase origin feature/42 # or: git merge origin/feature/42
# resolve any conflicts, then:
git push origin feature/42
Rewriting commits that are already on origin (an interactive rebase, git commit --amend) also produces a
non-fast-forward push, but for a different reason: the local history no longer contains the remote’s tip as an
ancestor, by design. That situation needs a force push rather than a pull — pulling would merge the old and the
rewritten commits back together and defeat the rebase.
--force-with-lease versus --force
git push --force-with-lease origin feature/42 # abort if origin moved since the last fetch
git push --force origin feature/42 # overwrite origin unconditionally
Both replace origin’s branch tip with the local one regardless of fast-forward, which is exactly what a rebased
or amended branch needs. The difference is safety: `--force-with-lease first checks that origin/feature/42 is
still where the local repository last saw it, and refuses the push if someone else has added commits in the
meantime — their work is never silently discarded. --force performs no such check.
Never force-push a shared or protected branch (main, develop, a release branch, or any branch other people
are actively basing work on). A force push replaces history that others may have already built commits on top of,
and GitHub branch protection rules exist precisely to block it on the branches that matter most (see
Branches). Reserve force pushes for a personal feature branch
that only its own author has checked out, and prefer --force-with-lease over plain --force even there.
Deleting a remote branch
git push origin --delete feature/42
This removes feature/42 from origin without touching the local branch or any local remote-tracking branch
(run git fetch --prune afterwards to clear origin/feature/42 locally too). Derived from
the git-push manual page.
On GitHub.com
There is nothing to literally "push" or "pull" from a web browser — github.com only ever shows the state that a
git push from somewhere else already produced, or offers a button that runs the equivalent of a fetch-and-merge
on the server side. In practice:
-
On a forked repository, the file-list page shows a Sync fork dropdown whenever the fork has fallen behind its upstream. Opening it lists the commits available upstream; clicking Update branch merges them into the fork’s default branch without needing a local clone at all.
-
On an open pull request, an Update branch button appears in the same situation — the PR’s base branch has moved ahead of where the PR branch forked from. Clicking it merges the base branch into the PR branch, the same outcome a local
git pullof the base branch followed by a merge would produce. -
From the browser’s perspective, a push performed elsewhere shows up passively: a newly pushed branch appears in the branch dropdown and the Branches list, and pushing a branch that is not yet
mainfor the first time surfaces a Compare & pull request banner on the repository’s home page, offering to open a pull request for it (see Pull Requests).
A push to a branch, or to a pull request’s branch, is also what triggers most CI on GitHub: workflows defined with
on: push or on: pull_request run automatically once the commits land on the remote, as covered on
Docker CI/CD with GitHub Actions (building and publishing
container images) and CI/CD and Over-the-Air Updates
(running lint, tests and end-to-end checks, and building or shipping an over-the-air update). Derived from
Syncing
a fork.
In IntelliJ IDEA
-
Git > Fetch downloads new objects and updates every remote-tracking branch without touching any local branch or the working tree — the exact equivalent of
git fetch. -
Update Project (Ctrl+T on Windows/Linux, ⌘T on macOS) is IntelliJ’s
git pull. By default the Update Project dialog opens each time (controlled by Update under Show options before in Settings › Version Control › Confirmation) and offers Merge, Rebase, or Reset to Remote Branch — corresponding togit pull --no-rebase,git pull --rebaseand discarding local commits respectively; see Merging, Rebasing and Conflicts for what to pick. -
Push (Ctrl+Shift+K on Windows/Linux, ⌘⇧K on macOS) opens the Push dialog: it lists every commit about to be sent, per repository, and lets the target branch be changed before confirming.
-
When a force push is needed, IntelliJ’s Push dialog offers Force Push, which under the hood runs
git push --force-with-leaserather than a plain--force— the IDE’s own safety net against overwriting a teammate’s commits. Force push is unavailable on any branch listed as protected in Settings › Version Control › Git.
Derived from Sync with a remote Git repository and Commit and push changes.
In VS Code
-
Git: Fetch (Command Palette) downloads new commits and updates remote-tracking branches only; Git: Fetch (Prune) additionally removes local remote-tracking branches whose remote branch was deleted.
-
Git: Pull fetches and integrates the current branch’s upstream, choosing fast-forward, merge or rebase according to the same Git configuration described above (
pull.rebase,pull.ff). -
Git: Push uploads the current branch’s commits; if it has no upstream yet, VS Code offers Git: Publish Branch instead, which pushes the branch and sets its upstream in one step — the graphical equivalent of
git push -u. -
The status bar shows a sync icon with an incoming/outgoing commit count (⇅) next to the current branch name. Clicking it runs Sync Changes: a pull immediately followed by a push, in one action.
-
The
git.autofetchsetting (off by default in most editors) makes VS Code rungit fetchperiodically in the background, at the interval set bygit.autofetchPeriod(180 seconds by default), so the status bar’s counts stay current without a manual fetch.
Derived from Repositories and remotes.
Common mistakes / tips
-
Force-pushing a shared branch. Covered above, worth repeating:
--force(or--force-with-leaseused carelessly on a branch others are also committing to) can discard commits nobody meant to lose. Treat it as a single-author, feature-branch-only tool, and prefer--force-with-lease. -
Forgetting
-uon the first push of a new branch. The push itself succeeds, but a later baregit pushorgit pullon that branch fails with no upstream configured. Either pass-uexplicitly the first time, or setpush.autoSetupRemoteonce and never think about it again. -
Treating
git fetchas risky. It is not — it only downloads and updates remote-tracking branches. Running it beforegit pull(rather than trusting pull’s implicit fetch) makes it possible to inspect what is about to be merged withgit log main..origin/mainbefore actually integrating it. -
Confusing "pull rejected" with "push rejected". A pull that hits a conflict is a merge/rebase conflict to resolve locally; a push that is rejected as non-fast-forward means the remote has commits the local branch lacks — the fix is to fetch/pull those commits first, not to force-push past them.
-
Assuming GitHub.com has a literal fetch/pull button. It does not; Sync fork and Update branch are the closest equivalents, and only apply to forks and pull requests respectively.