Collaborating with Remotes

Remotes, fetch vs pull, pushing, the pull request workflow, and .gitignore.

Remotes

A remote is just a named reference to another copy of the repository, usually hosted somewhere like GitHub, GitLab, or a private Git server. Cloning a repository automatically sets up a remote named origin pointing at wherever you cloned from:

Bash
$ git clone https://github.com/example/app.git
$ cd app
$ git remote -v
origin  https://github.com/example/app.git (fetch)
origin  https://github.com/example/app.git (push)

You can add additional remotes (common when working with a fork):

Bash
$ git remote add upstream https://github.com/original-owner/app.git
$ git remote -v
origin    https://github.com/example/app.git (fetch)
origin    https://github.com/example/app.git (push)
upstream  https://github.com/original-owner/app.git (fetch)
upstream  https://github.com/original-owner/app.git (push)

fetch vs. pull

Both commands retrieve commits from a remote, but they differ in whether they also update your current branch:

  • git fetch downloads new commits and branches from the remote into your local repository's remote-tracking branches (e.g., origin/main) — but it does not touch your current working branch at all. It's purely informational until you decide what to do with what was fetched.
  • git pull is effectively git fetch immediately followed by git merge origin/main (or git rebase, if configured) into your current branch — it updates your actual branch right away.
Bash
$ git fetch origin
$ git log main..origin/main --oneline    # see what's new on the remote, before merging anything
d4e5f6a Add password reset flow

$ git merge origin/main                  # now actually bring it in
Bash
$ git pull origin main                   # fetch + merge, in one step

git fetch first is a safer habit when you want to review incoming changes before they touch your branch; git pull is faster for the common case of "just get me up to date."

Pushing

Bash
$ git push origin main

git push uploads your local commits to the remote branch. The first time you push a new local branch, you typically need to set its upstream tracking reference:

Bash
$ git push -u origin feature/user-login

The -u (--set-upstream) flag links your local feature/user-login branch to origin/feature/user-login, so future plain git push/git pull on that branch know where to go without specifying the remote and branch again.

Git refuses a push if the remote has commits you don't have locally yet (someone else pushed first) — you need to git pull (or fetch + merge/rebase) to reconcile before pushing again. This protection is exactly what prevents one person's push from silently discarding another's work.

The pull request workflow

Most teams don't push directly to main. Instead, they follow a pull request (PR) — sometimes called a "merge request" — workflow:

  1. Create a feature branch off main: git switch -c feature/user-login.
  2. Commit your work, then push the branch to the remote: git push -u origin feature/user-login.
  3. Open a pull request on GitHub/GitLab, proposing to merge feature/user-login into main.
  4. Teammates review the diff, leave comments, and request changes if needed — you push additional commits to the same branch to address feedback, and the PR updates automatically.
  5. Once approved (and, typically, once automated CI checks pass), the PR is merged into main — often as a merge commit, a squash merge (all commits combined into one), or a rebase, depending on the team's convention.
  6. The feature branch is deleted, both locally and on the remote, since its work is now part of main.

This workflow exists to put a review step between "code someone wrote" and "code that's part of the shared, deployed history" — the branch and remote mechanics you've learned are the plumbing that makes that review gate possible.

.gitignore

Not everything in a project directory belongs in version control — build artifacts, dependency directories, local environment files with secrets, and IDE-specific files should never be committed. A .gitignore file tells Git which paths to leave untracked entirely:

Gitignore
# Dependencies
/vendor/
/node_modules/

# Environment and secrets
.env
.env.local

# Build output
/dist/
/build/

# Logs
*.log

# OS/editor cruft
.DS_Store
.idea/
.vscode/

Each line is a pattern; * is a wildcard, and a trailing / matches directories specifically. .gitignore only prevents untracked files from being picked up by git add . or shown in git status — if a file is already tracked, adding it to .gitignore later doesn't automatically remove it from the repository:

Bash
$ git rm --cached .env    # stop tracking a file, but keep it on disk locally
$ git commit -m "Stop tracking .env"

Common mistakes

  • Committing a .env file or other secrets before adding it to .gitignore — once it's in history, adding it to .gitignore afterward does not remove it from past commits; that requires rewriting history (e.g., git filter-repo) and rotating any leaked credentials.
  • Confusing fetch with pull and being surprised that git fetch alone didn't update the working branch — it's intentionally read-only until you merge or rebase.
  • Pushing directly to main on a team repository instead of going through a pull request, skipping code review entirely.

Interview questions

Q: What's the difference between git fetch and git pull? git fetch downloads new commits and branches from the remote into local remote-tracking references (like origin/main) without touching your current branch — it's purely informational. git pull does a fetch and then immediately merges (or rebases) the fetched changes into your current branch, actually updating your working branch's history.

Q: Why does Git reject a push sometimes, and how do you fix it? Git rejects a push when the remote branch has commits that your local branch doesn't have — usually because someone else pushed in the meantime. You resolve it by pulling (fetching and merging or rebasing) those new commits into your local branch first, then pushing again; this prevents one push from silently overwriting another person's work.

Q: What is a .gitignore file for, and what does adding a file to it not do? It tells Git which untracked files or directories to ignore — never stage or commit them automatically, and hide them from git status. It has no effect on files that are already tracked: if a file was committed before being added to .gitignore, it stays tracked and continues to show up in diffs until explicitly untracked with git rm --cached.