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.

18 min 8 sections

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:

bash
git remote -v
text
origin	[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:

bash
git clone [email protected]:ada/notes-app.git
cd notes-app
text
Cloning 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:

bash
git branch -a
text
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main

Only 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:

bash
git fetch
text
From github.com:ada/notes-app
   1ab03ad..bc8e289  main       -> origin/main

Crucially, 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:

bash
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't

Suppose you committed locally while a teammate pushed. After fetching, the graph shows the two lines of work:

text
* 5892ad7 (HEAD -> main) Alice's change
| * bc8e289 (origin/main) Bob's change
|/  
* 1ab03ad Initial commit

git status summarises the same situation by comparing your branch with its upstream:

text
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:

text
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:

SettingOn diverged historyResulting history
pull.rebase falseCreates a merge commitAccurate but noisy: "Merge branch 'main' of github.com:..." commits
pull.rebase trueReplays your local commits on top of the fetched onesLinear, as if you had started after their work
pull.ff onlyRefuses; you decide manuallyNever 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:

bash
git config --global pull.rebase true
git pull
git log --oneline --graph
text
* 55d0eba (HEAD -> main) Alice's change
* bc8e289 (origin/main) Bob's change
* 1ab03ad Initial commit

Notice 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:

bash
git switch -c feature/login
# ...commit some work...
git push -u origin feature/login
text
To 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:

text
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/login

Setting 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:

bash
git branch -vv
text
* feature/login 7e80f6c [origin/feature/login] Add login form
  main          5892ad7 [origin/main: ahead 1, behind 1] Alice's change

Rejected 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:

text
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.

DiagramTwo people, one remote
Your repoorigin (GitHub)Teammate's repo1git clone2All commits; origin/main = 1ab03ad3git push (Bob's change)4main now at bc8e2895git commit (Alice's change)6git push7! [rejected] main -> main (fetch first)8git fetch9bc8e289; origin/main moves10git rebase origin/main11git push12main now at 55d0eba (fast-forward)

Tidying up remote branches

After a feature branch is merged, delete it on the remote and prune stale bookmarks:

bash
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:

bash
git remote add origin [email protected]:ada/notes-app.git
git push -u origin main

GitHub needs to know it is really you. There are two transports, shown by the URL format:

URLTransportAuthentication
[email protected]:ada/notes-app.gitSSHAn SSH key pair; public key uploaded to your GitHub account
https://github.com/ada/notes-app.gitHTTPSA 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:

bash
ssh-keygen -t ed25519 -C "[email protected]"
ssh -T [email protected]
text
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:

bash
gh auth login

Change 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:

  1. Create a bare repository with git init --bare server.git to act as GitHub.
  2. Create a repository alice, commit a README.md, add ../server.git as origin and push main with an upstream.
  3. Clone the server into bob. In bob, commit a change and push.
  4. Back in alice, commit a different file without pulling first, then try to push. Read the error, inspect with git fetch and git status, integrate with a rebasing pull, and push successfully.
Show solution
bash
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 commit

The 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; origin is just the conventional name.
  • origin/main is a cached bookmark of the remote branch, updated only by fetching.
  • git fetch is always safe: it never changes your branches or working tree.
  • git pull is fetch plus integrate; choose merge, rebase or fast-forward-only with pull.rebase / pull.ff.
  • Push with -u the first time to set an upstream; a rejected push means "pull first", never "force".
  • GitHub authenticates over SSH keys or HTTPS tokens; gh auth login sets up HTTPS for you.

Check yourself

Quiz

0/4 correct · 4 left

  1. 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. 2.Which statement about git fetch is true?

  3. 3.Your push to main is rejected with "(fetch first)". What should you do?

  4. 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.