Skip to content

Remotes: fetch, pull, and push

Mid / Senior

What a remote and a remote-tracking branch really are, what fetch downloads, why pull is fetch + merge (or rebase), and how push and force-with-lease update someone else's refs.

Created 15 October 2026

6 concepts · detail hidden

  1. 01Glossary — terms used in this page
    • Remote — a named URL of another copy of the repository (origin, upstream), stored in .git/config.
    • Remote-tracking branch — a local, read-only ref like origin/main that records where a remote’s branch was at your last fetch.
    • Upstream (tracking) branch — the remote branch a local branch is configured to sync with; used by bare git pull / git push and for ahead/behind counts.
    • Refspec — a mapping rule src:dst telling fetch/push which refs to transfer and where to store them.
    • Fast-forward — updating a branch by simply moving its pointer forward along existing history, because the new commit already contains the old one as an ancestor.
    • Merge base — the best common ancestor of two commits.
    • FETCH_HEAD — a temporary ref file recording what the last fetch downloaded.
    #c-glossary-terms-used-in-this-page
  2. 02Remotes and remote-tracking branches

    A remote is just a nickname for a URL plus a fetch rule:

    $ git remote -v
    origin  git@github.com:acme/app.git (fetch)
    origin  git@github.com:acme/app.git (push)
    
    $ git config --get-regexp '^remote\.origin'
    remote.origin.url git@github.com:acme/app.git
    remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
    

    That fetch refspec reads: take every branch under refs/heads/ on the remote and store it locally under refs/remotes/origin/. The leading + allows non-fast-forward updates (if someone force-pushed, your origin/x follows).

    So there are three different things named “main”:

    Ref Lives Updated by
    main on the server the remote repo others’ pushes, your push
    origin/main (refs/remotes/origin/main) your repo only git fetch (and pull/push, which update it as a side effect)
    main (refs/heads/main) your repo your commits, merges, resets

    origin/main is a cached snapshot, not a live connection. It can be hours out of date. Git never talks to the network except during clone, fetch, pull, push, ls-remote (and a few related commands).

    Upstream tracking

    git switch -c feature/login
    git push -u origin feature/login      # -u sets the upstream
    git status
    # On branch feature/login
    # Your branch is ahead of 'origin/feature/login' by 2 commits.
    

    -u writes into .git/config:

    [branch "feature/login"]
        remote = origin
        merge = refs/heads/feature/login
    

    “Ahead by 2” is computed locally by comparing feature/login and origin/feature/login — after a fetch the count may change. git rev-list --left-right --count main...origin/main gives the raw ahead/behind numbers.

    #c-remotes-and-remote-tracking-branches
  3. 03What `git fetch` actually does

    fetch is the safe, read-only-for-your-work network operation. It never touches your working tree, index, or local branches.

    Step by step

    1. Connect to the remote and get the list of its refs and their hashes (git ls-remote origin shows the same).
    2. Negotiate: your client tells the server which commits it already has (“haves”); the server works out which objects you are missing.
    3. The server sends a packfile with exactly those objects (commits, trees, blobs).
    4. Your Git stores the objects and updates remote-tracking refs according to the refspec (origin/main moves).
    5. Writes .git/FETCH_HEAD and reflog entries for the remote-tracking refs.
    Before fetch                         After fetch
    local:   A---B---C   main            A---B---C   main
                     ^ origin/main                \
                                                   D---E   origin/main
    server:  A---B---D---E  main
    

    Your main did not move. Now you can inspect before integrating:

    git fetch origin
    git log --oneline main..origin/main     # what they have that I don't
    git diff main...origin/main             # their changes since our merge base
    

    Useful flags

    • git fetch --all — every remote.
    • git fetch --prune (or fetch.prune=true in config) — delete origin/x refs whose branch was deleted on the server. Without it, stale remote-tracking branches accumulate.
    • git fetch --tags — also fetch all tags (by default only tags pointing into fetched history).
    • git fetch origin main:main — fetch and fast-forward local main without checking it out (refused if not a fast-forward or if main is checked out).
    #c-what-git-fetch-actually-does
  4. 04What `git pull` actually does — fetch + integrate
    git pull  ==  git fetch  +  git merge <upstream>      (default)
    git pull --rebase  ==  git fetch  +  git rebase <upstream>
    

    The integration step depends on how the histories relate:

    Case 1 — you have no local commits: fast-forward

    A---B           main
         \
          C---D     origin/main
    
    => main simply moves to D. No new commit.
    

    Case 2 — both sides have new commits: diverged

          X---Y     main (yours)
         /
    A---B
         \
          C---D     origin/main
    
    • merge (git pull --no-rebase): creates a merge commit M with parents Y and D. History shows a “Merge branch ‘main’ of github.com:…” bubble — the infamous noise in shared branches.
    • rebase (git pull --rebase): replays X and Y on top of D as new commits X' Y'. Linear history, no merge commit; your local commits get new hashes (fine — they were never pushed).
    • fast-forward only (git pull --ff-only): refuses and makes you decide.
    merge:   A---B---X---Y---M         rebase:   A---B---C---D---X'---Y'
                  \         /
                   C-------D
    

    Configure it once

    Since Git 2.27, pulling diverged branches without a configured strategy prints a warning (newer versions refuse) asking you to choose:

    git config --global pull.rebase true      # rebase local commits onto upstream
    # or
    git config --global pull.ff only          # only ever fast-forward; integrate deliberately
    git config --global rebase.autoStash true # stash/unstash dirty changes around pull --rebase
    

    A common team convention: pull.rebase true for keeping your own unpushed work linear, and real merges only via pull requests.

    Pitfalls

    • pull on a dirty working tree can fail or produce conflicts mixed with your uncommitted edits — commit or stash first (or enable autoStash).
    • pull with merge conflicts leaves you mid-merge (or mid-rebase): resolve, git add, then git merge --continue / git rebase --continue — or abort with --abort.
    • Many experienced engineers prefer fetch then explicit merge/rebase, so they see what is coming in before integrating.
    #c-what-git-pull-actually-does-fetch-integrate
  5. 05What `git push` does, and why it gets rejected

    push is the mirror of fetch: send the objects the server lacks, then ask the server to move its ref.

    git push origin feature/login
    # equivalent refspec: refs/heads/feature/login:refs/heads/feature/login
    

    Steps

    1. Ask the server for its refs.
    2. Compute objects the server is missing; send them as a pack.
    3. Request: “update refs/heads/feature/login from <old hash> to <new hash>”.
    4. The server accepts only if the update is a fast-forward (old hash is an ancestor of new) — unless forced — and only if server-side hooks / branch protection allow it.
    5. On success, your local origin/feature/login is updated too.

    The rejection

     ! [rejected]        main -> main (fetch first)
    error: failed to push some refs to 'github.com:acme/app.git'
    hint: Updates were rejected because the remote contains work that you do not have locally.
    

    Someone pushed in between. Pushing would discard their commits, so the server refuses. Fix: git pull --rebase (or fetch + merge), then push again.

    Force pushing — and force-with-lease

    After rebasing or amending an already-pushed branch, your new history is no longer a descendant of the remote one, so you must force:

    git push --force                 # dangerous: overwrite whatever is there
    git push --force-with-lease      # safe(r): overwrite only if the remote is still
                                     # where my origin/<branch> says it is
    

    --force-with-lease sends the expected old value. If a colleague pushed after your last fetch, the remote no longer matches, and the push is refused instead of silently deleting their commits.

    Caveat: a plain git fetch (e.g. by your IDE in the background) updates origin/<branch> and silently “renews the lease”. Stricter: --force-with-lease=<branch>:<expected-hash> or --force-if-includes (Git 2.30+), which also checks that the remote tip was integrated into your local branch.

    Rule: never force-push shared branches (main, release/*); protect them on the server. Force-pushing your own feature branch after a rebase is normal.

    Other push forms

    git push origin --delete feature/old     # delete a remote branch
    git push origin v1.2.0                   # push one tag
    git push --follow-tags                   # push commits + annotated tags reachable from them
    git push origin HEAD:refs/for/main       # push to an arbitrary ref (Gerrit-style review)
    
    #c-what-git-push-does-and-why-it-gets-rejected
  6. 06Clone, forks, and the upstream remote

    git clone <url> is roughly:

    git init app && cd app
    git remote add origin <url>
    git fetch origin
    git switch main            # creates local main tracking origin/main
    

    Only the default branch gets a local branch; others exist as origin/* and become local the first time you git switch <name> (Git auto-creates a tracking branch when exactly one remote has that name).

    Fork workflow

    upstream (acme/app)  ──fetch──▶  your clone  ──push──▶  origin (you/app fork)
                                                             └── open PR into upstream
    
    git remote add upstream git@github.com:acme/app.git
    git fetch upstream
    git rebase upstream/main          # keep your feature current
    git push --force-with-lease origin feature/x
    

    Partial and shallow clones (big repos, CI)

    • git clone --depth 1 — shallow: only the latest commit’s history. Fast for CI, but log, blame, and merge-base computation are limited; deepen with git fetch --unshallow.
    • git clone --filter=blob:none — partial (blobless): full commit/tree history, file contents downloaded lazily on checkout. Usually a better default for big repos than shallow.
    • git sparse-checkout set src/service-a — only materialise part of the tree in a monorepo.
    #c-clone-forks-and-the-upstream-remote