Skip to content
C Codeloom
Git

Git Remotes and Pull Requests Explained

Understand Git remotes, the difference between origin and upstream, fetch vs pull, the forking workflow, and how to open a pull request that is easy to review.

·9 min read · By Yash Kesharwani
Intermediate 13 min read

What you'll learn

  • What a Git remote is and how to add and inspect them
  • The conventional names "origin" and "upstream"
  • The difference between git fetch and git pull
  • How git push -u sets up branch tracking
  • The forking workflow on GitHub from clone to merge
  • Code review etiquette that makes PRs easier to ship

Prerequisites

A Git repository on your laptop is useful by itself. A Git repository connected to a remote is how teams actually ship software. This post covers the bits of Git that suddenly involve other humans: remotes, fetching, pulling, pushing, forks, and pull requests.

What a remote actually is

A remote is just a named URL pointing at another copy of the repository — usually on a server like GitHub. You can have as many as you want. Each one is a label like origin or upstream that you use in commands instead of typing the URL every time.

List the remotes of a repository:

git remote -v
# origin  git@github.com:yash/codeloom.git (fetch)
# origin  git@github.com:yash/codeloom.git (push)

The -v (verbose) flag shows the URLs. Each remote has a separate fetch URL and push URL — they are almost always the same.

When you git clone <url>, Git automatically adds a remote called origin pointing at the URL you cloned from. That is the only reason origin is special — it is a default name, not a Git concept.

Adding and removing remotes

git remote add upstream git@github.com:original-author/codeloom.git
git remote -v
# origin    git@github.com:yash/codeloom.git (fetch)
# origin    git@github.com:yash/codeloom.git (push)
# upstream  git@github.com:original-author/codeloom.git (fetch)
# upstream  git@github.com:original-author/codeloom.git (push)

Rename or remove:

git remote rename origin github
git remote remove upstream

origin vs upstream — the convention

Two names show up everywhere:

  • origin — your own copy of the repository. The remote you push to.
  • upstream — the original project you forked from. The remote you pull updates from but generally don’t push to.

If you only ever work on your own repos, you only need origin. The moment you fork someone else’s project and want to keep up with their changes, you add upstream.

This is just convention. Git does not care what you call your remotes — but every tutorial and contributor guide uses these names, so use them.

fetch vs pull

This is the question every Git learner eventually has to answer.

git fetch — download without merging

git fetch origin

fetch downloads new commits, branches, and tags from the remote into your remote-tracking branches (origin/main, origin/feature-foo, etc). It does not touch your local branches. Your main is still where it was; origin/main may have moved.

This is safe — you can run git fetch any time without affecting your working tree.

To see what changed:

git log main..origin/main --oneline    # commits on origin/main not in main
git log origin/main..main --oneline    # commits on main not in origin/main

git pull — fetch and merge

git pull origin main

pull is fetch followed by an integration of the remote branch into your current local branch. By default the integration is a merge — if the histories diverged, you get a merge commit.

Many teams prefer rebasing on pull instead:

git pull --rebase origin main

This replays your local commits on top of origin/main, producing a linear history. Configure it as the default for a branch:

git config branch.main.rebase true

Or for every branch you ever create:

git config --global pull.rebase true

The mental model: fetch is safe, pull modifies your branch. When in doubt, fetch first, look at what changed, then decide whether to merge or rebase.

git push -u and tracking

The first time you push a new branch:

git switch -c feature-auth
git commit -am "Add login flow"
git push -u origin feature-auth

The -u (short for --set-upstream) does two things:

  1. Pushes the branch to origin.
  2. Records that this local branch tracks origin/feature-auth.

From now on, you can just type:

git push          # pushes to origin/feature-auth automatically
git pull          # pulls from origin/feature-auth automatically

Without -u, every push needs the explicit remote and branch name. Always pass -u on the first push of a branch.

To see what each local branch is tracking:

git branch -vv
# * feature-auth  c3d4e5f [origin/feature-auth] Add login flow
#   main          b2c3d4e [origin/main] Initial commit

The bracketed part is the tracked remote branch.

Try it yourself. In a repo with a remote, run git fetch origin and then git log --oneline --all --graph. Look for branches with origin/ prefixes — those are remote-tracking branches. Compare with git branch -vv to see which local branches track which.

The forking workflow

The pattern used by most open-source projects, and many companies:

  1. Fork the project’s repository on GitHub. You now have your own copy at github.com/you/project.
  2. Clone your fork locally:
    git clone git@github.com:you/project.git
    cd project
    Git automatically sets origin to your fork.
  3. Add the upstream remote so you can pull updates from the real project:
    git remote add upstream git@github.com:original/project.git
  4. Create a feature branch:
    git switch -c add-cool-feature
  5. Commit your changes, then push to your fork:
    git push -u origin add-cool-feature
  6. Open a pull request from you/project:add-cool-feature to original/project:main.

To stay up to date with the original project:

git fetch upstream
git switch main
git merge upstream/main         # or: git rebase upstream/main
git push origin main

Many teams use a single shared repo instead of forks. In that case there is no upstream — just origin, and you push feature branches there directly. The PR is then origin:feature-x -> origin:main. Everything else is the same.

Opening a pull request

A pull request (or “merge request” on GitLab) is a request to merge one branch into another, with a discussion attached. On GitHub:

  1. Push your branch (git push -u origin feature-x).
  2. Visit the repo on GitHub. There is usually a “Compare & pull request” banner — click it.
  3. Pick the base branch (usually main) and your feature branch.
  4. Write a description.
  5. Click Create pull request.

A good PR description has four sections:

## What
One sentence on what changed.

## Why
The reason this change exists — link to the issue or ticket.

## How
The interesting implementation choices a reviewer should know.

## Test plan
The steps you took to verify it works.

You will not regret writing this every time. Reviewers will thank you.

Keeping the PR up to date

If main moves while your PR is open and you need to integrate the changes:

git fetch origin
git switch feature-x
git rebase origin/main         # or: git merge origin/main
git push --force-with-lease    # because rebase rewrote history

Warning. Plain git push --force can overwrite a teammate’s work pushed to the same branch. Always prefer git push --force-with-lease — it refuses to overwrite changes you have not seen yet.

Responding to review

A few things that make code review go well — for both sides:

  • Address comments rather than dismissing them. “Done in commit abc123” closes the loop better than just resolving.
  • Don’t squash mid-review. Reviewers re-reading the diff after a force-push lose their place. Squash at the end, or let the merge do it.
  • Treat nits seriously but proportionally. Don’t block on style; do fix the broken thing.
  • Be specific in comments. “This will break when X is empty” is more actionable than “Are you sure about this?”

A complete worked example

You want to contribute a small typo fix to an open-source project.

# 1. Fork on GitHub, then clone your fork
git clone git@github.com:yash/cool-project.git
cd cool-project

# 2. Add upstream
git remote add upstream git@github.com:original-author/cool-project.git
git remote -v

# 3. Make sure your main is fresh
git fetch upstream
git switch main
git merge upstream/main
git push origin main

# 4. Branch and fix
git switch -c fix-readme-typo
sed -i '' 's/teh/the/' README.md   # macOS sed
git add README.md
git commit -m "Fix typo in README"

# 5. Push and open a PR
git push -u origin fix-readme-typo
# Open the GitHub URL the push command prints

That is the entire flow. Every contribution to every open-source project on GitHub uses some variant of these steps.

Try it yourself. Fork any small repository you find on GitHub (your own old project works). Clone it, add an upstream remote, create a branch, change a file, push, and open a PR from the GitHub UI. You don’t need to merge it — close the PR when done. The point is the muscle memory.

Recap

You now know:

  • A remote is a named URL pointing at another copy of the repo. git remote -v lists them.
  • origin is your copy; upstream is the original (convention only).
  • git fetch is safe — it downloads but does not modify local branches.
  • git pull = fetch + merge (or rebase, if configured).
  • git push -u origin <branch> pushes and sets up tracking on the first push.
  • The forking workflow: fork on GitHub, clone, add upstream, branch, commit, push, open PR.
  • Use --force-with-lease instead of --force when rewriting pushed history.
  • A clear PR description (what, why, how, test plan) gets reviews faster.

Next steps

The next post returns to Docker and tackles one of the most impactful image-size optimizations: multi-stage builds. You will see how to ship a 20MB image from a Dockerfile that needed 800MB to build.

→ Next: Docker Multi-Stage Builds for Smaller Images

Questions or feedback? Email codeloomdevv@gmail.com.