Step 04 of 10
Remotes & GitHub
Clone, fetch, pull and push with confidence, read ahead/behind status, and connect a local repository to GitHub over SSH or HTTPS.
So far every commit has lived on your machine. Collaboration in Git works by exchanging commits between copies of a repository, and a remote is simply a named address of another copy. This lesson explains what origin/main really is, why git fetch is always safe, what git pull does behind the scenes, and how to push your work to GitHub without fighting authentication or rejected pushes.
Remotes are just other repositories
There is nothing special about the copy on GitHub. It is an ordinary Git repository (without a working tree) that happens to live on a server everyone can reach. Your local repository stores a short name for it, by convention origin:
git remote -vorigin [email protected]:ada/notes-app.git (fetch)
origin [email protected]:ada/notes-app.git (push)Git is distributed: your local repository contains the complete history, not a partial checkout. You can commit, branch, view logs and diff with no network at all. You only talk to a remote when you deliberately exchange commits, using four commands: clone, fetch, pull and push.
Cloning a repository
git clone downloads an entire repository and sets everything up:
git clone [email protected]:ada/notes-app.git
cd notes-appCloning into 'notes-app'...
remote: Enumerating objects: 42, done.
remote: Counting objects: 100% (42/42), done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 42 (delta 12), reused 38 (delta 9), pack-reused 0
Receiving objects: 100% (42/42), 8.21 KiB | 8.21 MiB/s, done.
Resolving deltas: 100% (12/12), done.Clone did four things: created the directory, copied every object, added a remote named origin pointing at the URL, and created a local main branch that tracks origin/main. List every branch, including remote ones:
git branch -a* main
remotes/origin/HEAD -> origin/main
remotes/origin/mainOnly main is a local branch. The others are remote-tracking branches, explained next. If the remote has a feature/login branch, you do not need to create it by hand: git switch feature/login notices origin/feature/login and creates a local branch that tracks it.
Remote-tracking branches and fetch
origin/main is a remote-tracking branch: a read-only bookmark recording where main was on origin the last time you talked to it. It is a cached snapshot, not a live view. If a teammate pushes, your origin/main does not change until you ask.
git fetch asks the remote for any new commits and updates the remote-tracking branches:
git fetchFrom github.com:ada/notes-app
1ab03ad..bc8e289 main -> origin/mainCrucially, fetch never touches your local branches or your working tree. It only downloads and moves the origin/* bookmarks. That makes it safe to run at any time, and it lets you look before you integrate:
git log --oneline main..origin/main # what they have that I don't
git log --oneline origin/main..main # what I have that they don'tSuppose you committed locally while a teammate pushed. After fetching, the graph shows the two lines of work:
* 5892ad7 (HEAD -> main) Alice's change
| * bc8e289 (origin/main) Bob's change
|/
* 1ab03ad Initial commitgit status summarises the same situation by comparing your branch with its upstream:
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)Pull: fetch, then integrate
git pull is two commands in one: git fetch, followed by integrating the upstream branch into your current branch. When your branch has no local commits, integration is a fast-forward and nothing interesting happens. When histories have diverged, Git needs to know how to combine them, and modern Git refuses to guess:
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
...
fatal: Need to specify how to reconcile divergent branches.The three choices:
| Setting | On diverged history | Resulting history |
|---|---|---|
pull.rebase false | Creates a merge commit | Accurate but noisy: "Merge branch 'main' of github.com:..." commits |
pull.rebase true | Replays your local commits on top of the fetched ones | Linear, as if you had started after their work |
pull.ff only | Refuses; you decide manually | Never surprising |
Many teams use pull.rebase true for keeping a local branch up to date, because your unpublished local commits are simply moved after the shared ones:
git config --global pull.rebase true
git pull
git log --oneline --graph* 55d0eba (HEAD -> main) Alice's change
* bc8e289 (origin/main) Bob's change
* 1ab03ad Initial commitNotice Alice's commit has a new ID: it was recreated on top of Bob's. Rebasing is covered properly in its own lesson; for now, just know that rebasing commits you have not yet pushed is safe. You can also override the setting per call with git pull --rebase, git pull --no-rebase or git pull --ff-only.
Pushing and upstream branches
git push sends your commits to the remote and moves the remote's branch to match. The first time you push a new branch, use -u (--set-upstream) so Git remembers the connection:
git switch -c feature/login
# ...commit some work...
git push -u origin feature/loginTo github.com:ada/notes-app.git
* [new branch] feature/login -> feature/login
branch 'feature/login' set up to track 'origin/feature/login'.The upstream (or tracking branch) is what plain git push, git pull and the ahead/behind counts in git status refer to. Forget -u and Git tells you:
fatal: The current branch feature/login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/loginSetting git config --global push.autoSetupRemote true makes plain git push do this automatically for new branches. To see every branch's upstream and ahead/behind counts at once:
git branch -vv* feature/login 7e80f6c [origin/feature/login] Add login form
main 5892ad7 [origin/main: ahead 1, behind 1] Alice's changeRejected pushes
A push only succeeds if it is a fast-forward for the remote branch, meaning your commits build on everything already there. If a teammate pushed first, you get:
To github.com:ada/notes-app.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:ada/notes-app.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally.This is Git protecting your teammate's commits. The fix is always the same: git pull to integrate their work, re-run your tests, then git push again. Forcing the push would delete their commits from the remote; when forcing is legitimately needed, after rewriting your own branch, you will use the safer --force-with-lease, covered in the rebasing lesson.
Here is the whole loop across three copies of the same repository. Step through it and notice which copy changes at each step: fetch only ever updates your bookmark, and the second push succeeds because it is now a fast-forward for origin.
Tidying up remote branches
After a feature branch is merged, delete it on the remote and prune stale bookmarks:
git push origin --delete feature/login
git fetch --prune--prune removes remote-tracking branches whose remote branch no longer exists. Setting git config --global fetch.prune true makes this the default.
Connecting to GitHub
To publish a project that already exists locally, create an empty repository on GitHub (no README or licence, so histories do not conflict), then add it as a remote and push:
git remote add origin [email protected]:ada/notes-app.git
git push -u origin mainGitHub needs to know it is really you. There are two transports, shown by the URL format:
| URL | Transport | Authentication |
|---|---|---|
[email protected]:ada/notes-app.git | SSH | An SSH key pair; public key uploaded to your GitHub account |
https://github.com/ada/notes-app.git | HTTPS | A credential helper storing a token (passwords are not accepted) |
For SSH, generate a key once, add the public half (~/.ssh/id_ed25519.pub) under your account's SSH keys settings, and test it:
ssh-keygen -t ed25519 -C "[email protected]"
ssh -T [email protected]Hi ada! You've successfully authenticated, but GitHub does not provide shell access.For HTTPS, the simplest route is GitHub's CLI, which signs you in through the browser and configures Git's credential helper for you:
gh auth loginChange an existing remote's URL with git remote set-url origin <new-url>, for example when switching from HTTPS to SSH.
Practice
You can practise collaboration without any account by using a local "server" repository:
- Create a bare repository with
git init --bare server.gitto act as GitHub. - Create a repository
alice, commit aREADME.md, add../server.gitasoriginand pushmainwith an upstream. - Clone the server into
bob. Inbob, commit a change and push. - Back in
alice, commit a different file without pulling first, then try to push. Read the error, inspect withgit fetchandgit status, integrate with a rebasing pull, and push successfully.
Show solution
git init --bare server.git
git init alice && cd alice
echo "# Shared notes" > README.md
git add README.md && git commit -m "Initial commit"
git remote add origin ../server.git
git push -u origin main
cd ..
git clone server.git bob && cd bob
echo "Bob was here" >> README.md
git commit -am "Bob's change"
git push
cd ../alice
echo "todo" > todo.txt
git add todo.txt && git commit -m "Alice's change"
git push # rejected (fetch first)
git fetch
git status # "have diverged ... 1 and 1 different commits"
git log --oneline --graph --all
git pull --rebase # replays Alice's commit on top of Bob's
git push # now a fast-forward for the server
git log --oneline # Alice's change, Bob's change, Initial commitThe push was rejected because server.git had Bob's commit, which Alice's main did not contain. After git pull --rebase, Alice's branch contains Bob's commit plus her own on top, so the push is a fast-forward.
Key takeaways
- A remote is a named URL of another repository;
originis just the conventional name. origin/mainis a cached bookmark of the remote branch, updated only by fetching.git fetchis always safe: it never changes your branches or working tree.git pullis fetch plus integrate; choose merge, rebase or fast-forward-only withpull.rebase/pull.ff.- Push with
-uthe first time to set an upstream; a rejected push means "pull first", never "force". - GitHub authenticates over SSH keys or HTTPS tokens;
gh auth loginsets up HTTPS for you.
Check yourself
Quiz
0/4 correct · 4 left
1.A teammate pushed to main five minutes ago. You have not run any network command since. What does git status on your main report?
2.Which statement about git fetch is true?
3.Your push to main is rejected with "(fetch first)". What should you do?
4.What does the -u in git push -u origin feature/login do?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.