GitHub Basics and Remote Repositories
From the Make notes on docker an GitHub curriculum
TL;DR
You'll learn how your local Git repository connects to a remote one on GitHub, what origin actually means, and how fetch, pull, and push move commits between the two. You'll also understand forks, pull requests, and the branch-based collaboration workflow teams actually use. By the end you can clone, branch, commit, push, and open a pull request confidently.
1. The Mental Model
Git tracks history on your machine; GitHub just hosts a copy of that history somewhere else so others can reach it. A "remote" is nothing magical — it's a nickname pointing at a URL, like a bookmark for another repository. Pulling and pushing are just syncing two independent histories, not some special GitHub feature. GitHub is a shared mailbox for commits, not the place where version control actually happens.
2. The Core Material
2.1 Remotes: what origin really is

Photo by Geri Tech on Pexels
When you clone a repository, Git automatically creates a remote called origin pointing at the URL you cloned from. "Origin" is just a convention — you could name it upstream, bob, anything.
git clone https://github.com/yourname/myproject.git
cd myproject
git remote -v
# origin https://github.com/yourname/myproject.git (fetch)
# origin https://github.com/yourname/myproject.git (push)
You can add more remotes. This matters a lot when you fork someone else's repo: your fork is origin, their original repo becomes upstream.
git remote add upstream https://github.com/original-owner/myproject.git
git remote -v
Each remote also tracks branches via remote-tracking branches — read-only references like origin/main that represent what Git last saw on the server. They update only when you fetch or pull; they don't move on their own, so they can go stale.
2.2 Fetch, pull, and push — the three-way relationship

Photo by Elina Volkova on Pexels
There are three states of truth at any moment: your working directory, your local branch (main), and the remote-tracking branch (origin/main).
git fetchdownloads new commits from the remote and updatesorigin/main, but does not touch your localmainor your working files. It's the safe, non-destructive move.git pullisgit fetch+git merge origin/main(or rebase, if configured) in one step. It actually changes your local branch.git pushuploads your local commits to update the remote branch. Git refuses if the remote has commits you don't have locally yet — that's the classic "non-fast-forward" rejection.
git fetch origin # see what's new, don't merge yet
git log main..origin/main # inspect incoming commits first
git pull origin main # fetch + merge in one go
git push origin main # upload your commits
Why does push get rejected? Because Git enforces a fast-forward by default: your branch pointer can only move forward along a straight line of history. If someone else pushed in the meantime, your local history has diverged, and Git won't silently overwrite their commits. You must pull (merge or rebase) first, resolve any conflicts, then push again.
flowchart LR
WD["Working Directory"] -- "git add" --> SA["Staging Area"]
SA -- "git commit" --> LR["Local Repo (main)"]
LR -- "git push" --> RR["Remote Repo (origin/main on GitHub)"]
RR -- "git fetch" --> RT["origin/main (tracking ref)"]
RT -- "git merge" --> LR
LR -- "git pull = fetch+merge" --> LR
2.3 Forks and pull requests: the collaboration loop

Photo by Ann Hannes on Pexels
A fork is a full copy of someone else's repository under your own GitHub account. You can't push directly to a repo you don't own, so forking gives you a writable copy. A pull request (PR) is a request asking the original repo's maintainers to pull your changes from your fork (or branch) into theirs — it's a diff plus a conversation thread, not a Git command.
Typical open-source contribution flow:
# 1. Fork on GitHub's website first, then clone your fork
git clone https://github.com/yourname/their-project.git
cd their-project
# 2. Add the original repo as upstream, to stay in sync
git remote add upstream https://github.com/original-owner/their-project.git
# 3. Create a feature branch — never work directly on main
git checkout -b fix-typo-in-readme
# 4. Make changes, commit
git add README.md
git commit -m "Fix typo in installation section"
# 5. Push the branch to YOUR fork (origin), not upstream
git push origin fix-typo-in-readme
Then on GitHub's web UI, you click "Compare & pull request" — this opens a PR from yourname:fix-typo-in-readme into original-owner:main. Maintainers review, comment, request changes, and eventually merge. Before opening the PR (or while it's under review), keep your branch current with upstream:
git fetch upstream
git rebase upstream/main # or: git merge upstream/main
git push origin fix-typo-in-readme --force-with-lease # only after rebase
Use --force-with-lease instead of plain --force — it refuses to overwrite the remote branch if someone else pushed to it since your last fetch, protecting you from clobbering collaborators' work.
3. Worked Example
Let's trace a complete small contribution end to end, with exact commands and expected output.
Setup — you want to fix a bug in a team's shared repo acme/widgets, where you already have push access (no fork needed, just branches):
git clone https://github.com/acme/widgets.git
cd widgets
git checkout -b bugfix-null-pointer
Make the change:
# widgets/calculator.py
def divide(a, b):
if b == 0:
raise ValueError("division by zero") # was missing this check
return a / b
Commit and push the branch:
git add widgets/calculator.py
git commit -m "Guard divide() against division by zero"
git push -u origin bugfix-null-pointer
The -u (--set-upstream) flag links your local bugfix-null-pointer branch to origin/bugfix-null-pointer, so future git push/git pull on this branch need no arguments.
Expected terminal output after push:
Enumerating objects: 7, done.
...
remote: Create a pull request for 'bugfix-null-pointer' on GitHub by visiting:
remote: https://github.com/acme/widgets/pull/new/bugfix-null-pointer
To https://github.com/acme/widgets.git
* [new branch] bugfix-null-pointer -> bugfix-null-pointer
Click that link (or go to GitHub manually), set the base branch to main, write a PR description explaining why (not just what), and submit. A teammate reviews, maybe leaves a comment: "add a test." You respond locally:
# add the test
git add tests/test_calculator.py
git commit -m "Add test for divide-by-zero guard"
git push # branch already tracks origin/bugfix-null-pointer, so no args needed
GitHub automatically appends the new commit to the same open PR — no new PR needed. Once approved, the maintainer clicks "Merge pull request" on GitHub (squash, merge commit, or rebase — team's choice). Afterward, clean up locally:
git checkout main
git pull origin main
git branch -d bugfix-null-pointer # delete merged local branch
git push origin --delete bugfix-null-pointer # delete remote branch
This whole cycle — branch, commit, push, PR, review, merge, cleanup — is the daily rhythm of real collaborative development.
4. Production Pitfalls & Best Practices
4.1 Real-World Best Practices

Photo by Yoga Vidya Mandiram on Pexels
- Never commit directly to
mainon shared repos. Always branch, even for tiny fixes — it makes review and rollback trivial. - Pull before you push, especially on shared branches, to minimize merge conflicts and avoid rejected pushes.
- Write descriptive branch names like
fix/login-timeoutorfeature/csv-export, notpatch-1— it helps reviewers and future-you. - Keep PRs small and focused. A 40-line PR gets reviewed in minutes; a 2000-line PR gets rubber-stamped or ignored.
- Use
.gitignorebefore your first commit, not after — once a file likenode_modules/or.envis tracked, removing it from history is painful. - Rebase your feature branch on the latest
mainbefore opening a PR, so the diff reviewers see is clean and conflict-free. - Tag releases (
git tag v1.2.0 && git push origin v1.2.0) instead of relying on commit messages to mark shipped versions.
4.2 Common Bugs & Anti-Patterns
Mistake 1: Force-pushing a shared branch and erasing teammates' commits.
# BAD — overwrites origin/main unconditionally
git push --force origin main
# FIXED — fails safely if someone else pushed since you last fetched
git fetch origin
git push --force-with-lease origin main
Mistake 2: Committing secrets then trying to "delete" them.
# BAD — the secret is still in history, just not in HEAD
git rm .env
git commit -m "remove secret"
git push
# FIXED — rewrite history to actually purge it, then rotate the credential
git filter-repo --path .env --invert-paths
git push --force-with-lease origin main
# ALSO: revoke/rotate the leaked key immediately — assume it's compromised
Mistake 3: Cloning with HTTPS and getting stuck on password prompts.
# BAD — HTTPS asks for a password GitHub no longer accepts (removed 2021)
git clone https://github.com/you/repo.git
Password for 'https://you@github.com':
# FIXED — use a Personal Access Token as the password, or switch to SSH
git remote set-url origin git@github.com:you/repo.git
git push # uses your SSH key instead
Mistake 4: Forgetting which remote-tracking branch is stale.
# BAD — assumes origin/main is current without checking
git merge origin/main
# FIXED — fetch first so the tracking ref is actually up to date
git fetch origin
git merge origin/main
4.3 When to Use What (Decision Matrix
| Situation | Right approach |
|---|---|
| You have push access to the repo | Create a branch directly, skip forking |
| You don't own the repo (open source) | Fork it, clone your fork, add upstream remote |
| You just want to see new commits without merging | git fetch, then inspect with git log main..origin/main |
| You want fetch + merge in one step | git pull |
Your branch is behind main before opening a PR |
git fetch upstream && git rebase upstream/main |
| You accidentally pushed a secret | Rotate the credential immediately, then rewrite history with git filter-repo |
5. Now Try It
Create a new public repository on GitHub called git-practice. Clone it locally, create a branch add-notes, add a file notes.md with one sentence about what a remote-tracking branch is, commit it, and push the branch with git push -u origin add-notes. Then open a pull request on GitHub from add-notes into main, write a one-line description, and merge it yourself. Finally, run git fetch origin and git pull origin main locally to sync.
Success looks like: your local main branch contains the notes.md commit, the PR shows as merged on GitHub, and git remote -v plus git branch -a correctly show your origin remote and both local/remote branches.
Frequently asked about GitHub Basics and Remote Repositories
More from Make notes on docker an GitHub
Get the full Make notes on docker an GitHub curriculum
Clone the complete plan to your dashboard for unlimited AI-generated notes, practice quizzes, and a personalised revision schedule.
Save this course free