---
title: "Remote Collaboration"
description: "See exactly what fetch, pull, and push change across local and remote repositories."
source: https://vizlearn.app/cs/version-control/remote-collaboration
---

# Remote Collaboration

Remote work in Git gets easier when you stop treating the remote as one thing. Your clone has its own object database, checked-out branch, and cached view of the server. Fetch, pull, and push cross different boundaries, so they can leave those pieces at different commits.

`origin/main` is a receipt from the last network conversation, not a live window onto the server. Keep that in mind and the three commands become easier to separate:

- `git fetch origin` writes local objects and `origin/main`. It does not move local `main` or the checkout.
- `git pull --ff-only` fetches, then may move local `main` and the checkout if the history can fast-forward.
- `git push origin main` may write remote objects and remote `main`, but only after the server accepts the ancestry check.

## The local clone and origin keep separate state

This lesson keeps two repositories on screen. The left panel is your local clone. It contains the local object database, `refs/heads/main`, the remote-tracking ref `refs/remotes/origin/main`, `HEAD`, and the checked-out `app.py`. The right panel is the repository called `origin`, with its own object database and authoritative `refs/heads/main`.

Those similarly named refs are separate stored values:

- `main` is your local branch. A local commit or a successful integration can move it.
- `origin/main` is a local remote-tracking ref. It records the remote tip your clone most recently learned.
- `origin:main` is the lesson's compact label for `refs/heads/main` inside the remote repository.

Immediately after the modeled clone, all three refs point to one base commit. Both object databases contain that commit, and the checkout contains `app.py` with `v1`. Agreement is only a starting state. A teammate can move the remote without touching either ref in your clone.

The commit boxes use deterministic Git-format object IDs. Each commit names a tree and parent; the tree names the blob for `app.py`. The animation groups a commit, tree, and blob as a commit bundle in transit, then reports the exact number of Git objects copied.

## Step by Step through one collaboration cycle

The default run starts when a teammate's accepted push creates `server-fix` with `app.py` set to `v2`. First the remote stores the new blob, tree, and commit. Then `origin:main` advances from the clone base to that commit. Your local `main`, local `origin/main`, and checkout are untouched. At this point the server is ahead, but your clone has not learned that fact.

Next, `git fetch origin` contacts the server, which advertises its current `main` tip. Your clone compares that tip's reachable history with its local object database and copies the missing commit bundle. Object arrival alone does not publish a local name, so refs stay fixed during transit. Once the objects are durable locally, fetch advances only `origin/main` to `server-fix`.

Now the local object database contains the teammate's commit, but local `main` and the checkout still point to `v1`. That intermediate state is the main reason a remote-tracking ref exists: you can inspect what the server reported before integrating it into your current branch.

The next command is `git pull --ff-only`. Pull is two operations joined together. It fetches first, then attempts integration. Because the preceding explicit fetch already updated `origin/main`, the fetch half is up to date: zero objects arrive and no ref moves. Git then checks whether local `main` is an ancestor of `origin/main`. It is, so `main` can move forward along the existing parent chain. The branch advances to `server-fix`, and the checkout becomes `app.py` `v2`. No merge commit is created.

You then make a local commit named `client-ui` with `app.py` set to `v3`. Git stores a new local blob, tree, and commit whose parent is `server-fix`. A separate ref write advances local `main`, and the checkout now shows `v3`. Neither `origin/main` nor `origin:main` moves. Your branch is one commit ahead of the last known remote state.

Finally, `git push origin main` proposes the local tip as the new remote `main`. The server checks the ancestry in the opposite direction from pull: its current tip must be an ancestor of the proposed local tip. That condition holds, so the missing `client-ui` bundle crosses to the remote object database. Only after the objects arrive does the server advance `origin:main`. The successful update report then refreshes local `origin/main`. The run ends with all three refs on `client-ui` and the checkout on `v3`.

## Why the three refs matter

The three-ref ledger answers a different question in each row:

- What commit is my checked-out branch publishing locally? Read `main`.
- What remote tip has this clone observed? Read `origin/main`.
- What does the server currently publish? Read `origin:main`.

Here is the trap: `origin/main` sounds like an address on the remote, but it lives in your clone. A teammate can push while this local receipt stays stale. Fetch refreshes it without moving your branch.

This separation also explains why commit IDs do not change during a normal fetch, fast-forward pull, or push. The same content-addressed objects are copied between databases. Refs are then updated to name those existing objects. The network does not translate a local commit into a different remote commit.

### Read the three refs in a real repository

This model is useful when you need to check whether your view of the remote is fresh or diagnose a non-fast-forward rejection:

- `git status` describes the checkout and current branch. Its ahead/behind result uses the locally stored upstream ref, so run `git fetch` first when freshness matters.
- `git branch -vv` shows each local branch and its configured upstream. It is a quick way to confirm that `main` tracks `origin/main`.
- `git log --oneline main..origin/main` shows commits known only through the tracking ref. Reverse the range—`origin/main..main`—to see local-only commits.

After a rejected push or `--ff-only` pull, inspect both ranges before choosing a merge or rebase strategy. Recovery is outside this lesson, but the three-ref model tells you what diverged and which information may still be stale.

## Edge Cases: when fast-forward rules stop

If you fetch while everything is synchronized, the server advertises a tip your clone already has and `origin/main` already names. The result is zero object transfers and zero ref writes.

If local `main` and `origin/main` have different children of a shared base, `git pull --ff-only` cannot integrate them with one forward ref move. Its fetch half still has lasting effects: new remote objects remain in the local database, and `origin/main` remains updated. The integration half then stops with local `main` and the checkout unchanged.

A push performs its safety check against the remote's actual current tip, not merely your cached `origin/main`. If another teammate advanced the server after your last fetch, your local tracking ref may be stale. When that unseen remote tip is not an ancestor of local `main`, the push is rejected as non-fast-forward. In this model, the rejected operation makes zero durable remote object writes and zero remote ref writes.

An up-to-date push has no missing object payload and does not move the remote branch. If its successful response carries newer authoritative information than the local tracking ref, the trace may still refresh `origin/main`.

## Common mistakes

`git fetch` does not update your checked-out file. It downloads reachable objects and updates remote-tracking refs. Use a merge, rebase, or pull when you intend to integrate that history.

`git pull` is not a special third synchronization primitive. It begins with fetch, then runs an integration strategy. This lesson chooses `--ff-only` so every accepted integration is visible as a single branch move, and every divergence stops without a merge commit.

`git push` does not upload the working tree or index. Only objects reachable from the proposed ref can be sent. An uncommitted edit is not reachable from `main`, so it cannot appear in the push payload.

Finally, rejection does not mean your local commit disappeared. The local object and branch remain intact. The server refuses to replace its tip with a history that omits the current remote tip.

## Where the model stops

Real Git negotiates packs, may delta-compress objects, follows configured refspecs, supports many branches and remotes, and can integrate pull with merge or rebase. This lesson fixes one branch, one file, one remote, and `pull --ff-only`. It also caps the visible trace at eight unique commits. Those limits keep the ref and ancestry rules readable; they are not repository limits in Git.

For the command contracts behind the model, see the official documentation for [`git fetch`](https://git-scm.com/docs/git-fetch), [`git pull`](https://git-scm.com/docs/git-pull), and [`git push`](https://git-scm.com/docs/git-push).
