Git & Collaboration Workflows

Remotes & Collaboration

18 min Lesson 4 of 28

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.

# Inspect all configured remotes and their URLs git remote -v # origin git@github.com:acme/platform.git (fetch) # origin git@github.com:acme/platform.git (push) # Add a second remote (e.g., a backup mirror or the canonical upstream) git remote add upstream git@github.com:original-org/platform.git # Change a remote URL (e.g., switching from HTTPS to SSH after setting up keys) git remote set-url origin git@github.com:acme/platform.git # Remove a remote you no longer need git remote remove backup # Full view of what Git knows about a remote: fetch/push URLs, # tracking branches, and HEAD git remote show origin

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.

# List all remote-tracking branches git branch -r # origin/HEAD -> origin/main # origin/main # origin/feature/payments # upstream/main # List both local and remote-tracking branches git branch -a # See exactly where remote-tracking branches point git log --oneline origin/main -5 # Compare your local main with the remote-tracking copy git diff main origin/main
Remote-tracking branches are snapshots, not live views. 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.

# Fetch ALL remotes (updates all remote-tracking branches, no local changes) git fetch --all # Fetch a specific remote git fetch origin # Fetch and delete local remote-tracking refs that no longer exist on the remote git fetch --prune origin # The safe review pattern used by experienced engineers: git fetch origin git log --oneline --graph origin/main ^main # What is incoming? git diff main origin/main # What changed? git merge origin/main # Integrate manually # Equivalent of the above in one shot (but no chance to inspect first): git pull origin main # Pull with rebase instead of merge (keeps a linear local history) git pull --rebase origin main # Set rebase as the permanent default for all pulls: git config --global pull.rebase true
Production default: always fetch --prune. Set 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.

# Push the current branch and set its upstream tracking (first push) git push -u origin feature/payments # -u sets the tracking relationship so future pushes/pulls need no arguments # Subsequent pushes on the same branch (upstream already set) git push # Push all local branches to origin (rarely what you want — be explicit) git push --all origin # Push a specific local branch to a differently-named remote branch git push origin local-branch:remote-branch # Delete a remote branch (two equivalent forms) git push origin --delete feature/old-feature git push origin :feature/old-feature # Dry run: see what would be pushed without actually pushing git push --dry-run origin main # Force push with lease (SAFER than --force; fails if someone else pushed first) git push --force-with-lease origin feature/payments
Never use 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.

# See tracking relationships for all local branches git branch -vv # * feature/payments a3f21bc [origin/feature/payments: ahead 2, behind 1] Add card tokenization # main 9e1c4da [origin/main] Merge PR #412 # Manually set upstream for an existing branch git branch --set-upstream-to=origin/main main # Unset the upstream (detach from remote tracking) git branch --unset-upstream feature/old # Check your position relative to the upstream without fetching # (uses the cached remote-tracking ref) git status # On branch feature/payments # Your branch is ahead of 'origin/feature/payments' by 2 commits.
fetch, pull, and push data flows between local and remote Remote (origin) main feature/payments Remote-Tracking Refs (refs/remotes/origin/*) origin/main origin/feature/payments Local Repo main feature/payments Working Tree fetch merge / rebase pull = fetch + merge/rebase push
Data flow between the remote, remote-tracking refs, and your local branches. fetch only updates the middle layer; merge/rebase integrates into local branches; pull does both; push sends local commits back to the remote.

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

# 1. Fork on GitHub (web UI), then clone YOUR fork git clone git@github.com:yourname/kubernetes.git cd kubernetes # 2. Add the canonical repository as "upstream" git remote add upstream git@github.com:kubernetes/kubernetes.git # Verify: you now have two remotes git remote -v # origin git@github.com:yourname/kubernetes.git (fetch) # origin git@github.com:yourname/kubernetes.git (push) # upstream git@github.com:kubernetes/kubernetes.git (fetch) # upstream git@github.com:kubernetes/kubernetes.git (push) # 3. Keep your fork in sync with upstream (do this before every new branch) git fetch upstream git switch main git merge upstream/main # or: git rebase upstream/main git push origin main # keep your fork's main current too # 4. Create a feature branch from an up-to-date base git switch -c feature/add-hpa-scaling # 5. Push to YOUR fork and open a Pull Request to upstream git push -u origin feature/add-hpa-scaling
Fork and upstream collaboration workflow Canonical Repo kubernetes/kubernetes (upstream) Your Fork yourname/kubernetes (origin) Local Clone ~/kubernetes remotes: origin + upstream fetch upstream push origin Pull Request
Fork workflow: you fetch from upstream (canonical), push to origin (your fork), and open a Pull Request back to the canonical repository.

Common Failure Modes

  • Forgetting to sync before branching. If you branch off a stale main without fetching upstream first, your feature branch is built on old code. Always run git fetch upstream && git merge upstream/main before cutting a new branch.
  • Non-fast-forward rejections. error: failed to push some refs means 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 -r still shows it until you git 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 -v before a push. A push to upstream instead of origin would 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 main and the upstream main have diverged (you accidentally committed directly to your fork's main), use git reset --hard upstream/main followed by a force push to your fork's main to 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.
SSH keys, not passwords. Always configure SSH key authentication for your Git remotes in production CI/CD pipelines and developer workstations. HTTPS with personal access tokens (PATs) is acceptable but requires token rotation. SSH deploy keys scoped to a single repository are the cleanest approach for CI runners. Store private keys in a secrets manager (Vault, AWS Secrets Manager, GitHub Actions Secrets) — never hardcode them.