Remotes & Collaboration
Remotes & Collaboration
Every command you have run so far — commit, branch, merge, rebase — operates entirely on your local repository. The moment you need to share work with another engineer, CI/CD, or a deployment pipeline, you must understand remotes: what they are, how Git tracks them, and the exact mechanics of fetch, pull, and push. At companies like GitHub itself, Shopify, and Stripe, dozens of teams collaborate on millions of commits using exactly the primitives covered here.
What a Remote Actually Is
A remote is nothing more than a named URL stored in .git/config. Git ships with no magic — origin is just a convention, not a keyword. You can have zero remotes, one, or many. Each remote has a name (origin, upstream, backup) and a URL (HTTPS or SSH). After git clone, Git automatically creates one remote named origin pointing at the URL you cloned from.
Under the hood, .git/config stores this as simple INI sections. Remote URLs are just strings — Git resolves them at network time using the appropriate transport (SSH, HTTPS, or the git:// protocol, which is unauthenticated and almost never used in production).
Remote-Tracking Branches
After a fetch, Git stores what it learned about the remote in remote-tracking branches — read-only references under refs/remotes/origin/. These are your local cache of the remote's state at the last time you fetched. They are never directly checked out; they exist so you can compare, diff, and merge without hitting the network again.
origin/main reflects what main looked like on the remote the last time you ran git fetch. Your teammate's push five minutes ago is invisible to you until you fetch again. This is by design — Git is an offline-first system.
fetch vs pull: The Critical Distinction
git fetch downloads new objects and updates remote-tracking branches. It does nothing to your working tree or your local branches. git pull is shorthand for git fetch followed immediately by either git merge or git rebase (depending on configuration). At big-tech companies, many senior engineers prefer explicit fetch + inspect + integrate, rather than pull, because it gives them a chance to see what is coming before applying it.
git config --global fetch.prune true once and forget it. Every git fetch will then automatically clean up stale remote-tracking branches (branches deleted on the remote). Without this, git branch -r fills up with ghost refs over time, confusing newcomers and slowing tab-completion.
push: Sending Your Work Upstream
git push uploads local objects to the remote and asks it to advance a branch pointer. The remote only accepts if the push is a fast-forward on the remote — meaning your new commit's ancestor chain includes the current remote tip. If the remote has moved ahead since your last fetch, Git rejects the push with a non-fast-forward error, forcing you to integrate first.
git push --force on a shared branch. --force overwrites the remote unconditionally, destroying commits your teammates may have already pulled. Use --force-with-lease instead: it includes a check that your remote-tracking ref matches the actual remote tip. If someone else pushed since your last fetch, the push is rejected — preventing silent data loss. Many organizations configure their Git hosting to reject --force on protected branches entirely.
Tracking Branches in Depth
A tracking branch (or upstream branch) is a local branch configured to track a remote-tracking branch. This relationship tells Git two things: where to push by default, and how far ahead/behind you are. Git displays this information in git status and git branch -vv.
Fork and Upstream Workflows
Open-source projects and many enterprise setups use a fork workflow: you do not push directly to the canonical repository. Instead you fork it (creating your own copy on the hosting platform), clone your fork, and send pull/merge requests back to the canonical repository. This is the standard model on GitHub, GitLab, and Bitbucket for any project where you lack write access to the main repo.
The key is configuring two remotes in your local clone: origin pointing to your fork (where you push), and upstream pointing to the canonical repository (where you fetch updates).
Common Failure Modes
- Forgetting to sync before branching. If you branch off a stale
mainwithout fetching upstream first, your feature branch is built on old code. Always rungit fetch upstream && git merge upstream/mainbefore cutting a new branch. - Non-fast-forward rejections.
error: failed to push some refsmeans the remote has commits you do not have locally. The fix is always the same:git fetch origin, rebase or merge the upstream work, then push again. - Stale remote-tracking branches. After a teammate deletes a branch on the remote, your
git branch -rstill shows it until yougit fetch --prune. Enable auto-pruning globally:git config --global fetch.prune true. - Pushing to the wrong remote. In fork workflows, always verify with
git remote -vbefore a push. A push toupstreaminstead oforiginwould try to write to a repo you may not have write access to — or worse, one you do have access to but should not pollute directly. - Diverged fork main branch. If your fork's
mainand the upstreammainhave diverged (you accidentally committed directly to your fork's main), usegit reset --hard upstream/mainfollowed by a force push to your fork'smainto restore alignment. This is the one case where a force push is acceptable — you are resetting your own fork's main, not a shared branch.