Skip to content

Working with GitHub: clone, remotes, push, and fetch

The previous lesson separated local Git from GitHub. This lesson connects them. A local repository has its own commits and branch pointers; a hosted repository has its own. Git transfers data between them only when you ask it to.

GitHub repository ← push / fetch → local Git repository

We will use an ASP.NET Core project called order-api. The example URL is illustrative; use your own repository URL in real commands.

Use clone when the repository already exists online and you want a local working copy:

Terminal window
git clone https://github.com/acme-learning/order-api.git
cd order-api
git status
git remote -v

By default, this creates an order-api directory containing checked-out project files and a hidden .git directory. Git downloads the repository’s objects and history, configures a remote called origin, creates remote-tracking references for the advertised branches, and normally checks out the remote’s default branch as a local branch that tracks it.

GitHub: main ── C1 ── C2 ── C3
clone ↓
Your machine: main ── C1 ── C2 ── C3
origin/main points to the last fetched main

The diagram shows the initial state, not automatic synchronization. Each repository can advance independently afterward. A normal clone downloads history, but options such as --depth deliberately make a shallow clone. Clone also does not create a checked-out local branch for every remote branch.

Public repositories can normally be cloned by anyone. A private repository requires permission and authentication. Authentication for pushing is separate from the fact that your repository has a URL.

Choose the starting point that matches the work you already have. Do not create two unrelated initial histories and expect Git to combine them automatically.

Option A: an existing local project with commits

Section titled “Option A: an existing local project with commits”

Create a new empty repository on GitHub. For this route, do not ask GitHub to initialize it with a README, license, or .gitignore; those would create a separate initial commit. Copy its URL, then run inside the existing local repository:

Terminal window
git status
git log --oneline -3
git remote add origin https://github.com/acme-learning/order-api.git
git remote -v
git branch --show-current

If the intended branch should be named main, rename the current branch only if needed:

Terminal window
git branch -M main

Then publish it and set its upstream:

Terminal window
git push -u origin main

remote add registers a destination; it does not upload anything. -u sets origin/main as the local main branch’s upstream for later commands such as git push, git pull, and ahead/behind status. Rename only when main is really the desired branch name; -M can overwrite an existing local branch name, so inspect first.

If the directory has files but is not yet a Git repository, first initialize it, review what will be committed, and make an initial commit:

Terminal window
git init
git status
git add .
git diff --cached --stat
git commit -m "Initial project setup"

Before git add ., configure .gitignore and inspect for credentials, generated bin/ and obj/ folders, and local settings. This is especially important in .NET projects.

Create a repository on GitHub and clone it:

Terminal window
git clone https://github.com/acme-learning/order-api.git
cd order-api

If it contains an initial README or other commit, edit or add the project files, then commit and push:

Terminal window
git status
git add .
git diff --cached --stat
git commit -m "Initial ASP.NET Core project setup"
git push

This route already has origin from cloning. A normal clone of an existing default branch also configures upstream tracking. For a completely empty remote repository, check the newly created branch and its upstream; the first push may need git push -u origin main instead of bare git push.

Your starting point Recommended route
Local Git history already exists Create an empty GitHub repository, add origin, push with -u
No local history and starting online Create on GitHub, clone, add files, commit, push
Local files but no Git history Initialize and commit locally, then follow Option A; or clone first and copy the files into the clone

Do not run git init inside an already cloned repository; it already has .git.

A remote is a named entry in the local repository’s configuration containing fetch and push URLs plus related refspec settings. It is a connection name and destination, not the hosted repository itself.

Terminal window
git remote
git remote -v
git remote add origin https://github.com/acme-learning/order-api.git
git remote rename origin company
git remote remove company

The examples above demonstrate separate operations, not a sequence to run on your project. Removing a remote removes its local configuration and associated remote-tracking references; it does not delete the GitHub repository or your local commits.

origin is a convention, not a reserved keyword. git clone normally creates that name; git init does not. A repository can have several remotes and each may use HTTPS or SSH URLs.

Inspect before editing a real remote:

Terminal window
git remote -v
git remote get-url origin

An open-source contributor may fork acme-learning/order-api into their own account. After cloning their fork, they could use:

origin → https://github.com/your-name/order-api.git
upstream → https://github.com/acme-learning/order-api.git

Register the original project as another remote:

Terminal window
git remote add upstream https://github.com/acme-learning/order-api.git
git remote -v
git fetch upstream

In this convention, origin is your fork, so you usually push your feature branches there. upstream is the original project, so you fetch its new commits. These names are only local labels: another team could choose different names. Two remotes are useful for fork-based contributions, not required for every project.

push sends commits and updates a selected branch on a remote, subject to access permissions and server rules:

Terminal window
git push origin main
git push origin feature/login

In these simple forms, the named local branch is pushed to a branch of the same name on origin. You can name a branch even when it is not the one currently checked out:

Terminal window
git push origin feature/login

This does not push uncommitted working-tree edits or every local branch. It also does not guarantee the server accepts the push: permissions, protected-branch rules, hooks, or newer remote commits can reject it.

For a newly created feature branch, set its upstream on the first push:

Terminal window
git switch feature/login
git push -u origin feature/login

Afterward, git push normally uses that upstream and your configured push behavior. Check before publishing:

Terminal window
git status
git log --oneline --decorate -5
git branch -vv

git status helps identify staged, unstaged, and ahead/behind state when tracking is configured. It does not by itself list every commit that will be pushed, and its remote information can be stale until you fetch.

These names can point to different commits:

Name Where it lives What it represents
main Your local repository Your writable local branch
origin/main Your local repository Local remote-tracking reference: your last known position of main on origin
main on GitHub Hosted repository The branch actually stored on the server

origin/main is not a live connection to GitHub. If a teammate pushes, your local reference remains at its old commit until a fetch or pull updates it. You do not normally commit directly on origin/main; commit on a local branch.

Local main: A──B
Local origin/main: A──B
GitHub main: A──B──C ← teammate pushed C
After git fetch origin:
Local main: A──B
Local origin/main: A──B──C
GitHub main: A──B──C

The local main and your working files have not changed yet. The remote-tracking reference is a local bookmark, not a separate checked-out working copy.

Fetch contacts a remote, downloads available objects, and updates matching remote-tracking references according to the repository’s fetch configuration:

Terminal window
git fetch origin
git status
git log --oneline main..origin/main

The last command lists commits reachable from origin/main but not from local main. It is useful before deciding how to integrate.

To request a particular branch:

Terminal window
git fetch origin main

With a normal clone’s fetch refspec, this updates origin/main as well as FETCH_HEAD. Custom configurations and command options can change which remote-tracking references update. git fetch origin does not necessarily fetch every possible server-side ref, and git fetch origin main does not mean “merge main into my current branch.”

Fetch does not merge, rebase, or update the checked-out branch’s working files. It does update your local Git metadata and can download new history; it is not a command that leaves the entire local repository byte-for-byte untouched.

fetch → learn about remote history
integrate → deliberately bring that history into the current branch

For example, after reviewing the new commits, you could fast-forward local main when possible:

Terminal window
git switch main
git merge --ff-only origin/main
dotnet test

git pull combines fetching with a configured integration step. Its exact merge/rebase behavior depends on configuration, so it is not a synonym for git fetch.

Suppose a teammate pushed a fix to the hosted main while you worked on a local payment branch:

Terminal window
git status
git fetch origin
git log --oneline main..origin/main

Inspect the new work. When your working tree is ready, update local main:

Terminal window
git switch main
git merge --ff-only origin/main
dotnet test

Continue your feature on its own branch. After committing and testing it, publish only that branch:

Terminal window
git switch feature/payment
git status
dotnet test
git push -u origin feature/payment

The push can still fail if the server’s branch moved or permissions deny it. Do not force-push merely to make an error disappear; inspect the rejection first.

  • Treating origin as GitHub itself rather than a local remote name.
  • Assuming a clone creates local branches for every remote branch.
  • Assuming git commit or git remote add uploads work.
  • Running git init inside an existing clone.
  • Creating separate initial commits locally and online, then expecting a simple first push.
  • Assuming git push origin main includes every local branch or uncommitted edit.
  • Treating origin/main as a live view of GitHub rather than a cached local ref.
  • Assuming fetch merges or changes working files.
  • Assuming a bare git push works before an upstream is configured.
  • Force-pushing after a non-fast-forward rejection without understanding what happened.
  • Clone when the hosted repository already exists; it creates a local repository and usually an origin remote.
  • To publish existing local history, connect it to a new empty hosted repository and push with upstream tracking.
  • A remote is a named local connection; one repository can have several.
  • Push sends selected committed history; fetch receives history and updates remote-tracking references without integrating it into your local branch.
  • Distinguish local main, local origin/main, and the actual hosted main.
  1. What does git clone create locally, and does it create a local branch for every remote branch?
  2. Why should the GitHub repository be empty when you publish an existing local history through Option A?
  3. Does git remote add origin <url> upload any commits?
  4. What does -u add to the first push?
  5. Which of main, origin/main, and GitHub’s main can be stale after a teammate pushes?
  6. What changes after git fetch origin and what stays untouched in your working tree?