Skip to main content
Published / updated

Remote Repositories

Before you start

You need: local commits (Article 01), and a GitHub account.

Time: about 50 minutes, at the keyboard.

Learning objective

Connect a local repository to a remote, push and pull your work, and understand why fetch and pull are different commands.

Topics

  • What a remote is
  • Authentication
  • remote add, push, clone
  • Tracking branches
  • fetch vs pull
  • Merge and rebase on pull
  • Rejected pushes
  • Force pushing

What a remote is

A remote is a named URL pointing at another copy of the repository — usually on GitHub, GitLab, Azure DevOps or Bitbucket.

Every clone is a complete repository, with the full history, all branches and every commit. This is why Git is called distributed: you can commit, branch, merge, view history and revert with no network at all. The remote is a shared meeting point, not the authority your local copy depends on.

The practical consequence: network problems never block your work. Commit locally, push when the connection returns.

git remote -v
origin https://github.com/TeamSahasra/school-portal.git (fetch)
origin https://github.com/TeamSahasra/school-portal.git (push)

origin is a convention, not a keyword — the default name Git gives the remote you cloned from.

Authentication

HTTPS with a personal access token:

git remote add origin https://github.com/TeamSahasra/school-portal.git

GitHub stopped accepting account passwords for Git operations in 2021. Generate a personal access token (Settings → Developer settings → Personal access tokens) and use it as the password. Scope it to repo and give it an expiry.

git config --global credential.helper manager # Windows
git config --global credential.helper osxkeychain # macOS

Without a credential helper you are pasting the token on every push.

SSH with a key pair:

ssh-keygen -t ed25519 -C "ravi.kumar@nexcoding.in"
cat ~/.ssh/id_ed25519.pub # add this to GitHub → SSH keys
ssh -T git@github.com # verify
git remote add origin git@github.com:TeamSahasra/school-portal.git

Never share or commit the private key (id_ed25519, no .pub). Only the .pub file goes to GitHub.

SSH is worth the ten minutes of setup: no expiring tokens, no re-authentication.

Connecting and pushing

git remote add origin https://github.com/TeamSahasra/school-portal.git
git branch -M main
git push -u origin main

-M renames the current branch to main. -u sets upstream tracking, which is why later pushes can be a bare git push.

git remote -v
git remote rename origin upstream
git remote set-url origin git@github.com:TeamSahasra/school-portal.git
git remote remove origin

set-url is how you switch a repository from HTTPS to SSH without re-cloning.

Cloning

git clone https://github.com/TeamSahasra/school-portal.git
git clone <url> school-portal-fix # into a named folder
git clone -b develop <url> # a specific branch
git clone --depth 1 <url> # shallow, latest commit only

clone creates the folder, downloads the history, sets up origin and checks out the default branch. A shallow clone is faster for CI but cannot show history — do not use it for work you will commit.

Tracking branches

git branch -vv
* main a3f9c1d [origin/main] Add student search
develop 8c2e4f1 [origin/develop: ahead 2] Fix fee rounding
spike 1a9b3c5 Experiment with caching

spike has no upstream, so git push on it fails until you set one:

git push -u origin spike
git branch --set-upstream-to=origin/spike spike

origin/main is a local reference, not the remote. It records what main looked like on the remote the last time you contacted it. It goes stale, and git fetch is what updates it.

That distinction explains most confusion in this article.

fetch vs pull

git fetch origin # download; change nothing in your working directory
git pull origin main # fetch, then merge into the current branch

git pull is git fetch followed by git merge.

git fetch origin
git log HEAD..origin/main --oneline # what is coming
git diff HEAD origin/main # what will change
git merge origin/main # merge when ready

Fetch first when you have uncommitted work or a big change is expected. Pull can produce a conflict at a moment you did not choose. Fetch never touches your working directory, so you can look before deciding.

git fetch --all
git fetch --prune # delete local refs to branches deleted on the remote

--prune matters after a few months: without it, git branch -r lists dozens of origin/feature/... branches that were merged and deleted long ago.

Merge or rebase on pull

git pull # merge (default)
git pull --rebase # replay your commits on top
git pull --ff-only # fail rather than create a merge
merge:
A---B---C (origin/main)
\ \
D---M (main)

rebase:
A---B---C---D' (main)
MergeRebase
HistoryPreserves what happenedLinear
Merge commitsYesNo
Rewrites hashesNoYes
Safe on shared branchesYesNo

Rebase rewrites commits. D becomes D' — a different hash. Harmless for commits only you have; destructive for commits others have pulled.

Rule: rebase your own unpushed commits, merge everything else.

git config --global pull.rebase false # merge
git config --global pull.ff only # refuse implicit merges

pull.ff only is a good default for beginners: it fails instead of creating a surprise merge, forcing you to decide.

Rejected pushes

! [rejected] main -> main (fetch first)
error: failed to push some refs
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.

Someone pushed since you last pulled.

git pull # or --rebase
# resolve any conflict
git push

This is Git protecting you. Accepting the push would discard their commits. The rejection is correct behaviour, not an error to work around.

The wrong fix is git push --force, which does exactly the discarding Git just prevented.

Force pushing

git push --force # dangerous
git push --force-with-lease # safer

--force overwrites the remote branch with yours, deleting any commit you do not have. If a colleague pushed an hour ago and you force push, their work is gone from the branch.

--force-with-lease refuses if the remote moved since your last fetch. It force pushes only when you are overwriting exactly what you thought you were. If you must force, use this.

Legitimate uses are narrow:

  • Your own feature branch, after an intentional rebase
  • Removing a secret from a branch nobody else uses
  • A branch you own, after --amend

Never force push main, develop, or any shared branch. Most teams protect them at the server so this cannot happen.

Diagnosing

SymptomCause
Authentication failedPassword used instead of a token, or an expired token
Permission denied (publickey)SSH key not added to GitHub, or agent not running
failed to push (fetch first)Remote has commits you lack — pull
no upstream branchBranch never pushed — git push -u origin <branch>
origin/main looks oldNot fetched — origin/main is a cached reference
Remote branches that no longer existNever pruned — git fetch --prune
A colleague's commits disappearedSomeone force pushed
git remote -v
git branch -vv
git fetch --prune
ssh -T git@github.com

Errors you will hit

MessageCauseFix
Authentication failedUsed a password instead of a tokenPersonal access token, or SSH
Permission denied (publickey)SSH key not added to GitHubAdd the .pub key
failed to push some refs (fetch first)The remote has commits you lackgit pull, then push
no upstream branchBranch never pushedgit push -u origin <branch>
origin/main looks out of dateIt is a cached referencegit fetch
A colleague's commits disappearedSomeone force pushedRecover from reflog; talk to the team

A rejected push is Git protecting you. Accepting it would discard someone else's work — --force is not the fix.

Common mistakes

  • Using an account password instead of a token
  • Committing a private SSH key
  • git pull with uncommitted work, then a conflict at a bad moment
  • Assuming origin/main is live
  • --force to escape a rejected push
  • Force pushing a shared branch
  • Rebasing commits others have pulled
  • Never pruning
  • Shallow-cloning a repository you will commit to
  • Pushing without -u and repeating the full command forever

Practice

The course exercise is push and pull between local and remote.

  1. Create a GitHub repository, connect a local one, push main.
  2. Set up SSH keys and switch the remote with set-url. Verify with ssh -T.
  3. Run git branch -vv and identify which branches track a remote.
  4. Create a branch and push without -u. Read the error, then fix it.
  5. Change a file on GitHub's web editor. Run git fetch, then git log HEAD..origin/main and git diff HEAD origin/main before merging.
  6. Prove origin/main is stale: change the file on GitHub, run git log origin/main, then fetch and run it again.
  7. Create a rejected push deliberately — commit on GitHub and locally, then push. Fix it by pulling.
  8. Do the same again and fix it with git pull --rebase. Compare the graphs with git log --oneline --graph --all.
  9. Clone the same repository into a second folder and simulate two developers pushing and pulling.
  10. In the second clone, commit and push. In the first, commit a different change and force push. Confirm the second clone's commit is gone.
  11. Repeat with --force-with-lease and confirm it refuses.
  12. Delete a branch on GitHub, then run git branch -r before and after git fetch --prune.
  13. Set pull.ff only and see what happens when histories diverge.

Exercises 10 and 11 are the pair that make force pushing feel real. Do them on a throwaway repository.

You can now

  • Connect a local repository to a remote
  • Authenticate with a token or SSH
  • Read a rejected push and respond correctly
  • Say what origin/main is and when it updates
  • Use --force-with-lease rather than --force

Review questions

  1. What does git fetch do that git pull does not?
  2. What is origin/main, and when does it change?
  3. Why does Git reject a push, and what is the correct response?
  4. When is a force push acceptable, and which flag should you use?

Next: Branching