Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 028 / 100

Remote repositories: a Git & GitHub guide for developers

Push, pull and fetch explained.

Expona Admin7 min read

Remote repositories: a Git & GitHub guide for developers

Most of us work with remote repositories every single day, yet many developers never fully grasp what's happening behind the scenes when they push or pull. Understanding the mechanics of fetch, pull, and push—and how tracking branches fit into the picture—is the difference between confident deployment workflows and mysterious merge conflicts at 2 AM. In this guide, we'll break down remote repositories through the lens of practical production scenarios you'll actually encounter.

Why remote repositories matter

A remote repository is a version of your project stored on a server (GitHub, GitLab, Bitbucket, or your own infrastructure). It serves as the single source of truth for your team. Your local repository is a complete copy of the project history, but your teammates' changes live remotely. To synchronize, you need operations that safely move commits between your machine and the server. This is where fetch, pull, and push come in.

The key insight: these three operations are distinct. Fetch downloads. Pull downloads and merges. Push uploads. Conflating them causes real problems in team environments.

Fetch — Download remote changes without touching your branch

Fetch is the safest of the three operations. It downloads commits from the remote repository and updates your remote-tracking branches, but it does not modify your working directory or current branch. This makes it ideal for checking what your teammates have done without risking your local work.

Consider this scenario: you're on a feature branch called feature/checkout-redesign. Your teammate pushed updates to the main branch an hour ago, and you want to see what changed before deciding whether to rebase. You'd run:

bash
git fetch origin

This downloads all new commits from the remote and updates your local representation of origin/main, origin/develop, and any other branches. Your current branch remains exactly where it was. You can now inspect what's on origin/main safely:

bash
git log origin/main --oneline -5

Or compare it to your feature branch:

bash
git diff feature/checkout-redesign origin/main

Fetch is especially useful in CI/CD pipelines and before rebase operations. Many teams run git fetch as part of their pre-deployment checks to ensure they're aware of all remote changes.

You can also fetch from specific remotes or branches:

bash
git fetch origin main

This fetches only the main branch from the origin remote, which is faster if you're on a large monorepo and only care about one branch's changes.

Pull — Fetch plus merge or rebase

Pull is a convenience command that combines fetch and then either a merge or rebase. It downloads remote changes and immediately integrates them into your current branch. This is where things get tricky, because the integration step can create merge commits or rewrite history depending on your configuration.

The default behavior is merge. If you run git pull on a branch that has diverged from its remote tracking branch, Git will create a merge commit. Here's a real example from an e-commerce team:

You've been working on feature/payment-retry locally. Meanwhile, a teammate merged their changes to main and you want the latest. You'd normally run:

bash
git pull origin main

Git will fetch the remote main and then merge it into your current branch. If there are no conflicts, you'll get a new merge commit. If there are conflicts, Git pauses and asks you to resolve them.

Many production teams prefer rebase over merge for pull operations, because it keeps history linear and avoids the "merge commit clutter." To use rebase instead:

bash
git pull --rebase origin main

Or globally configure your repository:

bash
git config pull.rebase true

With rebase enabled, git pull will replay your local commits on top of the remote branch, resulting in a cleaner history with fewer merge commits. This is the standard in teams we've worked with across ride-hailing and SaaS platforms.

There's also git pull --rebase=interactive if you want to edit or squash commits during the pull, though that's usually reserved for detailed history cleanup.

The critical distinction: fetch is read-only; pull modifies your history. Always fetch first if you're uncertain.

Push — Upload your commits

Push sends your local commits to the remote repository. This is how your team accesses your work. Push is where many conflicts surface, because Git won't let you push if your local branch is behind the remote.

Simple case: you've made three commits on feature/auth-flow and want to share them:

bash
git push origin feature/auth-flow

Git uploads your commits to the remote and updates origin/feature/auth-flow. Your teammates can now see your work.

Common issue: the remote has commits you don't have locally. For example, a teammate pushed while you were offline. Git rejects the push:

bash
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.

You need to fetch and merge (or rebase) those remote commits first:

bash
git fetch origin
git rebase origin/feature/auth-flow
# or: git merge origin/feature/auth-flow
git push origin feature/auth-flow

Push is idempotent if no one else has committed. Push again and nothing happens (Git sees your commits are already there). Push is destructive if you force it: git push --force rewrites remote history and can erase teammates' work. Force push is sometimes necessary in production (e.g., squashing commits before merge), but it requires coordination and strong team practices. Most platforms now support "force-with-lease" as a safer alternative:

bash
git push --force-with-lease origin feature/auth-flow

This only succeeds if no one else has pushed since your last fetch, preventing accidental history obliteration.

Tracking branches — git push -u origin feature sets upstream once

Tracking branches link your local branch to a remote branch. Once linked, you can push and pull without specifying the remote and branch name each time. This is set up with the -u flag (short for --set-upstream).

First push of a feature branch:

bash
git push -u origin feature/recommendation-engine

The -u flag tells Git: "Push my commits and remember that feature/recommendation-engine tracks origin/feature/recommendation-engine." After this, you can simply run:

bash
git push
git pull

Git knows where to send your commits and fetch from. This saves dozens of keystrokes and mental effort over a long development session.

You can view your tracking setup:

bash
git branch -vv

Output might look like:

bash
  develop           abc1234 [origin/develop] Latest deployment
* feature/checkout  def5678 [origin/feature/checkout] Refactor payment UI

The text in brackets shows which remote branch is tracked. If a branch has no upstream, it shows nothing.

To change the tracked branch later (rare but sometimes necessary in reorg scenarios):

bash
git branch -u origin/main

This sets the current branch to track origin/main.

Teams benefit from consistent tracking because it reduces the surface for mistakes. When everyone pushes with -u on their first push, pull requests and code reviews become predictable. The remote branch exists exactly when you need it.

Common mistakes

Mistake 1: Pulling without understanding merge vs. rebase. Many developers pull and end up with "accidental" merge commits that clutter history. Set git config pull.rebase true once, and you'll get cleaner history by default. Communicate this preference with your team so everyone's pull behavior is aligned.

Mistake 2: Pushing before pulling. A developer commits locally, tries to push, gets rejected because the remote has new commits, and panics. Always pull (or fetch and rebase) before your first push of the day. Make it a habit.

Mistake 3: Using git push --force carelessly. Force push can destroy teammates' commits if you're not careful. Use --force-with-lease instead, or discuss with your team before force pushing anything on shared branches like main.

Mistake 4: Forgetting to use -u on first push. You push a new branch without -u, then next time you try to pull, Git doesn't know which remote branch to pull from and asks for clarification. Using -u the first time prevents this entirely.

Mistake 5: Fetching and then immediately merging everything. Fetch is meant to be safe and informational. Don't automatically merge everything you fetch. Check what changed, test locally if needed, then decide on merge or rebase.

Key takeaways

  • Fetch downloads remote commits and updates remote-tracking branches without touching your working directory. Use it to inspect changes safely.
  • Pull is fetch plus merge (or rebase if configured). It integrates remote changes into your current branch. Set pull.rebase true to avoid merge commits by default.
  • Push uploads your local commits to the remote. It fails if your local branch is behind; always pull or rebase first.
  • Tracking branches link local and remote branches. Use git push -u on first push to set up tracking automatically, then push and pull work without specifying the branch.
  • Understand the difference between these three operations and your workflow becomes more deliberate and less error-prone. Fetch to inspect. Pull to integrate. Push to share. Set upstream to reduce friction.
  • Configure your team's preferences for merge vs. rebase pull behavior early. Consistency prevents surprises and makes history readable.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles