# Remotes & GitHub

> Clone, fetch, pull and push with confidence, read ahead/behind status, and connect a local repository to GitHub over SSH or HTTPS.

Git & Collaboration roadmap · Core concepts · 18 min read · https://levelcode.org/roadmaps/git/remotes

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	git@github.com:ada/notes-app.git (fetch)
origin	git@github.com: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 git@github.com: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)
```

> [!IMPORTANT]
> `git status` compares against your **local copy** of `origin/main`. "Your branch is up to date with 'origin/main'" only means "up to date with what I fetched last time". Run `git fetch` first when the answer matters.

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

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

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

```sequence
title: Two people, one remote
participants: Your repo, origin (GitHub), Teammate's repo
Your repo -> origin (GitHub): git clone
> Clone copies every commit and records where the remote's main is as origin/main.
origin (GitHub) --> Your repo: All commits; origin/main = 1ab03ad
Teammate's repo -> origin (GitHub): git push (Bob's change)
> The teammate's push is a fast-forward, so origin accepts it. Your origin/main still says 1ab03ad: nothing has told your repository.
origin (GitHub) --> Teammate's repo: main now at bc8e289
Your repo -> Your repo: git commit (Alice's change)
> Committing is local. Your main and origin's main have now diverged.
Your repo -> origin (GitHub): git push
origin (GitHub) --> Your repo: ! [rejected] main -> main (fetch first)
> origin has bc8e289, which your main does not contain, so accepting your push would discard it.
Your repo -> origin (GitHub): git fetch
origin (GitHub) --> Your repo: bc8e289; origin/main moves
> Fetch downloads Bob's commit and moves origin/main. Your main and your files are untouched.
Your repo -> Your repo: git rebase origin/main
> This is the integrate half of git pull --rebase: your commit is replayed on top of Bob's and gets a new ID, 55d0eba.
Your repo -> origin (GitHub): git push
origin (GitHub) --> Your repo: main now at 55d0eba (fast-forward)
```

> [!WARNING]
> Never respond to a rejected push on a shared branch like `main` with `git push --force`. It overwrites the remote branch with yours and silently discards whatever your teammates pushed.

### 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 git@github.com: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:

| URL | Transport | Authentication |
| --- | --- | --- |
| `git@github.com: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:

```bash
ssh-keygen -t ed25519 -C "ada@example.com"
ssh -T git@github.com
```

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

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

## Quiz

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?

   A. That you are behind origin/main
   B. Whatever it last knew: it compares with your local origin/main, which has not been updated
   C. An error until you fetch
   D. That your branch has diverged

   **Answer: B.** git status never contacts the remote. It compares your branch with the remote-tracking branch origin/main, which only changes when you fetch (or pull).

2. Which statement about git fetch is true?

   A. It merges the remote branch into your current branch
   B. It discards local commits that are not on the remote
   C. It downloads new commits and updates remote-tracking branches, leaving your local branches and files alone
   D. It pushes your commits and then pulls theirs

   **Answer: C.** Fetch only downloads objects and moves refs like origin/main. Integrating them into your branch is a separate step (merge or rebase), which git pull combines with fetch.

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

   A. Run git push --force
   B. Delete your local main and clone again
   C. Rename your branch and push it instead
   D. Pull to integrate the remote commits, check everything still works, then push again

   **Answer: D.** The remote has commits you do not. Integrating them makes your push a fast-forward. Forcing would overwrite and discard your teammates' commits.

4. What does the -u in git push -u origin feature/login do?

   A. Sets origin/feature/login as the branch's upstream, so later push, pull and status use it
   B. Pushes only unmerged commits
   C. Updates all branches at once
   D. Uploads untracked files too

   **Answer: A.** -u (--set-upstream) records the tracking relationship. After that, plain git push and git pull know where to go, and git status can report ahead/behind counts.
