Lesson 8 of 15

Remote Repositories

What a Remote Is

A remote is another copy of your repository that lives somewhere else — usually on GitHub, GitLab or Bitbucket. Your repository stores a short nickname for each remote along with its URL, so you can type origin instead of a full address every time.

The name origin is pure convention, not a keyword. It is simply the name git clone gives to the place it cloned from. You can rename it, and a project can have several remotes at once — a common open-source setup is origin for your own copy and upstream for the original project.

This is where Git's distributed design pays off. A remote is not a master copy your repository depends on; it is a peer that happens to be reachable by everyone, and your local repository holds the full history whether or not the remote is available. That is also why a remote makes such a good backup: if your laptop is stolen, cloning from GitHub gives you back everything you had pushed, commit for commit.

One detail that trips people up: adding a remote sends nothing. git remote add only records a name and a URL in your config. Nothing travels over the network until you push or fetch.

Example
# Connect an existing local repository to an empty GitHub repository
git remote add origin git@github.com:ananya/college-portfolio.git

# What remotes does this repository know about?
git remote -v
# origin  git@github.com:ananya/college-portfolio.git (fetch)
# origin  git@github.com:ananya/college-portfolio.git (push)

# Change the URL (for example, switching from HTTPS to SSH)
git remote set-url origin git@github.com:ananya/college-portfolio.git

# Rename or remove
git remote rename origin github
git remote remove upstream

# Full details, including which local branch tracks what
git remote show origin
Notes
  • If git remote -v prints nothing, the repository has no remote and git push will fail with No configured push destination. That is the fix for one of the most common first-week errors: you created the repository locally with git init and never connected it to anything.

origin/main Is a Local Cache, Not the Server

This is the idea that makes remotes click, and almost no tutorial states it plainly. Alongside your own branches, your repository keeps a set of remote-tracking branches with names like origin/main. It is tempting to read origin/main as "the main branch on GitHub, right now". It is not. It is your local record of where GitHub's main branch was the last time you talked to GitHub.

So when Git tells you "your branch is up to date with origin/main", it is not making a claim about the server. It is comparing two things on your own disk. If a teammate pushed ten minutes ago and you have not fetched since, Git will still say you are up to date, quite honestly and quite uselessly.

git fetch is the command that refreshes that cache. It contacts the remote, downloads any new commits, and updates origin/main — and it changes none of your own branches and none of your files. That makes fetch completely safe: there is no situation in which fetching can break your work or cause a conflict.

This gives you a genuinely useful habit. Fetch first, look at what arrived, then decide. git log main..origin/main shows the commits the server has that you do not; git log origin/main..main shows the commits you have that the server does not. Reading those two before merging is how you stop being surprised by what a git pull is about to do.

Example
# Refresh your knowledge of the remote. Changes nothing else.
git fetch

# What has arrived that I don't have yet?
git log main..origin/main --oneline

# What do I have that GitHub doesn't?
git log origin/main..main --oneline

# See the difference before accepting it
git diff main origin/main

# Where does everything sit?
git branch -vv
# * main          a81c9f2 [origin/main: ahead 2, behind 1] Fix header
#   feature/cart  4b8e0c3 [origin/feature/cart] Add quantity selector

# Then, and only then, integrate
git merge origin/main
Notes
  • "Ahead 2, behind 1" means you have two commits GitHub has not seen and GitHub has one you have not taken. That state is called diverged, and it is the normal situation on a team project — not an error. Merging (or pulling) reconciles it.

git pull Is Just fetch + merge

git pull is not a separate mechanism. It runs git fetch and then immediately merges the fetched commits into your current branch. Every surprising thing pull does is really the merge half doing exactly what merges do — which means a pull can produce a merge conflict, and it can produce a merge commit.

Knowing this changes how you debug. "Pull created a weird commit I didn't write" is a merge commit, and it appeared because your branch and the remote had both moved. "Pull gave me a conflict" is an ordinary merge conflict, resolved with the exact process from the previous lesson: fix the markers, git add, commit.

You can ask pull to rebase instead of merge with git pull --rebase. Your local commits are replayed on top of what you fetched, so no merge commit appears and history stays linear. This is safe and often preferable for commits you have not pushed yet — and it is subject to the same rule as any rebase, so do not use it to rewrite commits others already have.

Recent versions of Git will stop and ask you to choose when your branch and the remote have diverged and you have not said which behaviour you want. The message talks about reconciling divergent branches. It is not an error in your repository; Git simply wants the preference set once. Pick merge if you want the safe default, or rebase if your team prefers a linear history.

Example
# These two are the same thing
git pull
#   ==  git fetch  +  git merge origin/<current-branch>

# Rebase instead of merge
git pull --rebase

# Set the preference once, globally
git config --global pull.rebase false   # always merge (the classic default)
git config --global pull.rebase true    # always rebase
git config --global pull.ff only        # refuse to pull unless fast-forward

# A pull that conflicts is just a merge that conflicted
git pull
# CONFLICT (content): Merge conflict in src/cart.js
#   ... fix the file ...
git add src/cart.js
git commit

# Safer habit on a team project: look before you leap
git fetch
git log main..origin/main --oneline
git merge origin/main
Notes
  • pull.ff only is worth considering while you are learning. It makes git pull refuse to do anything except a clean fast-forward, so you can never be surprised by an automatic merge. When it refuses, you fetch and merge deliberately and can see exactly what is happening.

Pushing, and the Rejection Everyone Meets

git push sends your commits to the remote and moves the remote's branch label forward. The first time you push a new branch, Git does not know where it should go, so you pass -u origin branch-name. The -u sets an upstream — a permanent link between your local branch and the remote one — and after that a bare git push and git pull both know what you mean.

Sooner or later a push will be rejected with a message about the update being non-fast-forward and a hint to integrate remote changes first. Read it calmly: it means the remote branch has commits you do not have. Accepting your push would move the branch label somewhere that abandons them, so Git refuses. This is a safety feature and it has saved more student projects than any other message in Git.

The correct response is to bring those commits in and then push: git pull (or fetch and merge), resolve anything that conflicts, then git push. This works, it is safe, and it is what you should do every single time you see this message.

The incorrect response, which the internet will offer you within about thirty seconds of searching, is git push --force. That tells the remote to discard whatever it has and accept your version instead. On a shared branch this deletes your teammates' commits from the server — commits that may exist nowhere else if they had not pulled them elsewhere. Never force-push a shared branch, and never force-push main.

Example
# First push of a new branch: set the upstream link
git push -u origin feature/cart

# After that, this is enough
git push
git pull

# The rejection
git push
# ! [rejected]        main -> main (fetch first)
# error: failed to push some refs to 'github.com:ananya/repo.git'
# hint: Updates were rejected because the remote contains work that you do
# hint: not have locally. ... integrate the remote changes before pushing again.

# The correct fix
git pull            # brings their commits in, may need conflict resolution
git push            # now accepted

# Pushing tags — they are NOT sent by a normal push
git push origin v1.0.0
git push --tags

# Delete a remote branch
git push origin --delete feature/cart
Notes
  • If a push hangs or fails with a permission error, the cause is almost always authentication rather than Git itself. Test SSH with ssh -T git@github.com. Over HTTPS, remember that GitHub wants a personal access token, not your account password.

Force-Push, and the Safer Version

There is one situation where force-pushing is legitimate: you rewrote your own commits on your own branch — amended a message, squashed some commits, or rebased onto main — and the remote copy still has the old versions. Since the histories no longer match, an ordinary push is rejected, and the rewrite has to be published somehow.

Even then, prefer git push --force-with-lease over plain --force. Plain --force means "overwrite whatever is there, I do not care what it is". --force-with-lease means "overwrite it only if the remote is still exactly where I last saw it". If a teammate pushed to your branch while you were rebasing, plain force silently destroys their commit; --force-with-lease refuses and tells you.

One subtlety quietly defeats that protection: the check compares against your remote-tracking branch, so running git fetch immediately before a --force-with-lease updates that reference and makes the lease agree with whatever is now on the server. If you are about to force-push, do not fetch first "just to be safe" — that is precisely what removes the safety.

The practical rule is short. Force-pushing a personal feature branch nobody else is working on: acceptable, use --force-with-lease, and tell your team. Force-pushing main or a branch someone else has pulled: do not, even once. Recovering from that means chasing down every teammate's laptop hoping one of them still has the missing commits.

Example
# You rebased your own unshared branch and now need to publish it
git push --force-with-lease

# Plain force — overwrites unconditionally, no checks. Avoid.
# git push --force

# What --force-with-lease protects you from
#   You:      rebase feature/cart, then force-push
#   Teammate: pushed a commit to feature/cart ten minutes ago
#   --force              -> their commit is gone from the server
#   --force-with-lease   -> ! [rejected] (stale info) : nothing is destroyed

# If a force-push has already damaged a branch, the commits may still
# exist in someone's local reflog:
git reflog
git switch -c recovery <hash-from-reflog>
  • Rejected push on a shared branch → git pull, resolve, push again. Never force.
  • Rejected push after rewriting your own private branch → --force-with-lease, and mention it to the team
  • Do not force-push main, develop, or any branch with an open pull request others are reviewing
  • GitHub branch protection rules can block force-pushes to main outright — turn that on for any project with more than one contributor
  • Push at the end of every working session. A commit that exists only on your laptop is not backed up.
Ask AI