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.

sequenceDiagram participant Local as Local repository participant Origin as origin Note over Local,Origin: git fetch Local->>Origin: request new objects and refs Origin-->>Local: objects + updated origin/main Note over Local: working tree and main untouched Note over Local,Origin: git pull (fetch + merge) Local->>Origin: request new objects and refs Origin-->>Local: objects + updated origin/main Local->>Local: merge (or rebase) origin/main into main Note over Local: working tree updated Note over Local,Origin: git push Local->>Origin: upload new objects, ask to move main alt fast-forward Origin-->>Local: accepted, main moved else non-fast-forward Origin-->>Local: rejected Local->>Local: pull, resolve, push again end

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:

  1. 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.

  2. 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 pull of the base branch followed by a merge would produce.

  3. 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 main for 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

  1. 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.

  2. 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 to git pull --no-rebase, git pull --rebase and discarding local commits respectively; see Merging, Rebasing and Conflicts for what to pick.

  3. 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.

  4. When a force push is needed, IntelliJ’s Push dialog offers Force Push, which under the hood runs git push --force-with-lease rather 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.

In VS Code

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. The git.autofetch setting (off by default in most editors) makes VS Code run git fetch periodically in the background, at the interval set by git.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-lease used 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 -u on the first push of a new branch. The push itself succeeds, but a later bare git push or git pull on that branch fails with no upstream configured. Either pass -u explicitly the first time, or set push.autoSetupRemote once and never think about it again.

  • Treating git fetch as risky. It is not — it only downloads and updates remote-tracking branches. Running it before git pull (rather than trusting pull’s implicit fetch) makes it possible to inspect what is about to be merged with git log main..origin/main before 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.