Cloning and forking¶
There are two ways to make a copy of a GitHub repository. Cloning copies it to your computer, to work on it with Git. Forking copies it to your GitHub account, to change it without needing access to the original. Both are often combined: you fork a repository, then clone your fork.
Cloning a GitHub repository¶
Cloning was covered in Remote repositories: git clone copies the whole repository, history included, and remembers the original as the remote origin. Updates then go back and forth: git pull brings in the remote's new commits, git push sends yours (if you have write access).
On GitHub, the green Code button gives everything needed to clone a repository:
| Option | Content |
|---|---|
| Local > HTTPS | https://github.com/sam-rivera/homelab.git. Works everywhere; needs a token for private repositories and for pushing |
| Local > SSH | [email protected]:sam-rivera/homelab.git. Needs an SSH key registered on your account |
| Local > GitHub CLI | gh repo clone sam-rivera/homelab. Uses the GitHub CLI's authentication |
| Open with GitHub Desktop | Clones with the GitHub Desktop application |
| Download ZIP | The files of the selected branch only: no history, no .git, no link to the repository. Not a clone |
| Codespaces tab | Opens the repository in a development environment in the cloud |
Click the copy icon next to the URL, then in the terminal, from the directory where the clone should be created:
$ pwd
/home/sam/projects
$ git clone https://github.com/sam-rivera/homelab.git
Cloning into 'homelab'...
Username for 'https://github.com': sam-rivera
Password for 'https://[email protected]':
Progress lines follow (remote: Enumerating objects..., Receiving objects...) while Git downloads the history.
The username and token are only asked for private repositories (or not at all, if a credential helper remembers them). A second argument chooses the directory name: git clone https://github.com/sam-rivera/homelab.git homelab-laptop.
Cloning an empty repository¶
An empty GitHub repository can be cloned too. Git warns about it, and the clone is ready for a first commit and git push -u origin main:
$ git clone https://github.com/sam-rivera/empty-project.git
Cloning into 'empty-project'...
warning: You appear to have cloned an empty repository.
Forking a repository¶
Alex wants to contribute to an open source project that documents homelab setups, but has no write access to it: cloning it and pushing would be refused. The solution is to fork it.
A fork is a new repository, on GitHub, under your account, that starts as a copy of another repository (called the upstream repository):
- it is yours: you have full control over it, and can push, create branches, and change its settings;
- it is independent: your changes do not affect the original, so it is a safe place to experiment;
- it stays related: GitHub remembers where it comes from ("forked from ..."), which allows you to sync it with the original, and to propose your changes to the original through a pull request.
flowchart LR
up[("Upstream on GitHub<br/>sam-rivera/homelab")]
fork[("Fork on GitHub<br/>alex-martin/homelab")]
local["Alex's local clone"]
up -- "1. Fork (on GitHub)" --> fork
fork -- "2. git clone" --> local
local -- "3. git push origin" --> fork
fork -- "4. Pull request" --> up
up -- "git fetch upstream" --> local
Who can fork what¶
- Public repositories: anyone with a GitHub account can fork them.
- Private repositories: only people who have access to the repository, and only if forking is allowed (organizations can disable it in their settings). A fork of a private repository stays private, and is deleted if its owner loses access to the original.
Creating a fork¶
- On the repository page, click Fork (top right). The number next to it counts the existing forks.
-
In the Create a new fork form:
Field Default Meaning Owner Your account Where the fork is created: your account, or an organization Repository name Same as the original Can be changed to tell the fork apart Description The original's Optional Copy the mainbranch onlyTicked Only the default branch is copied. Untick it to copy every branch -
Click Create fork.
A few seconds later, the fork opens: alex-martin/homelab, with forked from sam-rivera/homelab under its name. A line above the files compares its branch with the original:
| Message | Meaning |
|---|---|
This branch is up to date with sam-rivera/homelab:main |
Same commits as the original |
This branch is 2 commits behind sam-rivera/homelab:main |
The original has moved on |
This branch is 1 commit ahead of sam-rivera/homelab:main |
Your fork has commits the original does not have |
Keeping a fork up to date¶
The fork does not follow the original automatically. When it is behind, the Sync fork button, next to that line, offers Update branch: GitHub merges the new commits of the original into your branch. If your branch has conflicting changes, GitHub asks you to resolve them locally, or offers to discard your commits.
Locally, the usual way is to add the original repository as a second remote, named upstream by convention:
$ git clone https://github.com/alex-martin/homelab.git
Cloning into 'homelab'...
$ cd homelab
$ git remote add upstream https://github.com/sam-rivera/homelab.git
$ git remote -v
origin https://github.com/alex-martin/homelab.git (fetch)
origin https://github.com/alex-martin/homelab.git (push)
upstream https://github.com/sam-rivera/homelab.git (fetch)
upstream https://github.com/sam-rivera/homelab.git (push)
| Remote | Points to | Used for |
|---|---|---|
origin |
Your fork | Pushing your branches |
upstream |
The original repository | Getting the new commits of the project |
When Sam adds commits to the original, Alex brings them into the clone, then into the fork:
$ git fetch upstream
From https://github.com/sam-rivera/homelab
* [new branch] main -> upstream/main
$ git merge upstream/main
Updating 3c1035d..0ed8fc3
Fast-forward
inventory.csv | 1 +
1 file changed, 1 insertion(+)
$ git push origin main
To https://github.com/alex-martin/homelab.git
3c1035d..0ed8fc3 main -> main
Then Alex works on a branch, pushes it to the fork, and opens a pull request from there:
$ git switch -c add-printer
Switched to a new branch 'add-printer'
$ git commit -am "Add network printer"
$ git push -u origin add-printer
To https://github.com/alex-martin/homelab.git
* [new branch] add-printer -> add-printer
branch 'add-printer' set up to track 'origin/add-printer'.
Clone, fork, or branch?¶
| Clone | Fork | Branch | |
|---|---|---|---|
| What it creates | A copy on your computer | A copy on GitHub, under your account | A new line of development inside the repository |
| Needs | Git; read access | A GitHub account; read access | Write access to the repository (collaborator) |
| Link to the original | The remote origin |
"forked from", Sync fork, pull requests across forks | Same repository |
| How changes go back | git push (if you have write access) |
A pull request from the fork | A pull request, or a merge |
| Typical use | Working locally on any repository | Contributing to projects you cannot write to; experimenting | Day-to-day work in a team |
In short: collaborators branch, outsiders fork. And whatever you choose, you will probably clone to work locally.
Summary¶
| Action | How |
|---|---|
| Get the clone URL | Code > Local > HTTPS or SSH |
| Clone | git clone <url> [directory] |
| Fork | Fork > Create a new fork > Create fork |
| Track the original from a clone of a fork | git remote add upstream <url-of-original> |
| Update a fork from the browser | Sync fork > Update branch |
| Update a fork locally | git fetch upstream, git merge upstream/main, git push origin main |
Common mistakes¶
- Downloading the ZIP instead of cloning. There is no history, and no way to pull or push.
- Expecting a fork to follow the original. Sync it with Sync fork or
git fetch upstream. - Pushing to
upstream. Without write access, it is refused: push toorigin(your fork) and open a pull request. - Forking a repository you can write to. A branch is simpler, and keeps everything in one place.
Hands-on labs¶
Three labs, from guided to more autonomous. Labs 1 and 3 need your GitHub account; Lab 2 runs entirely in the terminal. Replace <username> with your GitHub username. The labs use octocat/Spoon-Knife, a public repository GitHub provides to practice forking.
Lab 1: fork and clone¶
Objective: fork a public repository, clone the fork, and connect the clone to the original.
Prerequisites and initial state: a GitHub account, Git installed. No existing fork of octocat/Spoon-Knife in your account (if you have one, delete it first, or reuse it and skip task 1).
Setup:
mkdir -p ~/git-practice/fork && cd ~/git-practice/fork
Tasks:
- Open github.com/octocat/Spoon-Knife, note its number of branches, and fork it with the default options.
- On your fork, note the "forked from" line, the number of branches, and the comparison line above the files.
- Clone your fork into
~/git-practice/fork(public: no credentials needed). - Add the original repository as the remote
upstream, and fetch it. - List the remotes, and all the branches the clone knows about.
Expected result and verification:
- The original has 3 branches; the fork has only
main(because of Copy themainbranch only), says forked from octocat/Spoon-Knife, and This branch is up to date withoctocat/Spoon-Knife:main. git remote -vlistsorigin(your fork) andupstream(octocat/Spoon-Knife), each for fetch and push.git branch -ashowsorigin/main, and threeupstream/branches:change-the-title,main,test-branch.
Solution
cd ~/git-practice/fork
git clone https://github.com/<username>/Spoon-Knife.git
cd Spoon-Knife
git remote add upstream https://github.com/octocat/Spoon-Knife.git
git fetch upstream
git remote -v
git branch -a
- The branches not copied into the fork are still reachable from the clone, through
upstream. originis always the repository you cloned: here, your fork.
Keep the fork and the clone for Lab 3.
Lab 2: the fork workflow, rehearsal¶
Objective: go through the complete fork workflow, including a sync, with local bare repositories playing GitHub: Sam's original, and Alex's fork.
Prerequisites and initial state: Git installed. The setup creates, in ~/git-practice/fork-flow, Sam's repository on "GitHub" (sam-homelab.git) and Sam's clone (sam), then Alex's fork (alex-homelab.git), made the way GitHub does it: a server-side copy.
Setup:
mkdir -p ~/git-practice/fork-flow && cd ~/git-practice/fork-flow
git init -q --bare -b main sam-homelab.git
git clone -q sam-homelab.git sam 2>/dev/null
cd sam
git config user.name "Sam Rivera"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory" && git push -q origin main
cd ..
git clone -q --bare sam-homelab.git alex-homelab.git # the "Fork" button
Tasks:
- Play Alex: clone the fork into
alex, configure a name and email, and add Sam's repository (../sam-homelab.git) asupstream. - Play Sam: in
sam, addnas,192.168.1.20,storage, commit with the messageAdd NAS, and push. - Play Alex: check
git status(is the fork up to date?), then bring Sam's commit into the clone and into the fork. - Still as Alex: create a branch
add-printer, addprinter01,192.168.1.30,printer, commit with the messageAdd network printer, and push the branch to the fork with an upstream. - Try to push
add-printertoupstream. (On GitHub, without write access, this would be refused; here, nothing stops you: do not do it, just answer where the branch must go, and why.)
Expected result and verification:
- Task 3:
git statussaysup to date with 'origin/main'even though Sam has a new commit: the clone only knows the fork. Aftergit fetch upstreamandgit merge upstream/main(a fast-forward),git push origin mainupdates the fork. - Task 4 prints
* [new branch] add-printer -> add-printerfor the fork. git -C ../alex-homelab.git log --oneline --allshowsAdd network printerandAdd NAS;git -C ../sam-homelab.git branchshows onlymain.- Task 5: the branch goes to
origin(the fork), and reaches Sam's repository only through a pull request, which Sam decides to merge or not.
Solution
# 1. Alex clones the fork and adds upstream
cd ~/git-practice/fork-flow
git clone alex-homelab.git alex
cd alex
git config user.name "Alex Martin"
git config user.email "[email protected]"
git remote add upstream ../sam-homelab.git
git remote -v
# 2. Sam moves on
cd ../sam
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -am "Add NAS"
git push origin main
# 3. Alex syncs: upstream -> clone -> fork
cd ../alex
git status # up to date with 'origin/main' (the fork is behind too)
git fetch upstream # * [new branch] main -> upstream/main
git merge upstream/main # Fast-forward
git push origin main
# 4. A branch on the fork
git switch -c add-printer
echo "printer01,192.168.1.30,printer" >> inventory.csv
git commit -am "Add network printer"
git push -u origin add-printer
# Verification
git -C ../alex-homelab.git log --oneline --all
git -C ../sam-homelab.git branch
git statuscompares with what the clone knows. Neither the clone nor the fork follows the original by themselves.- On GitHub, steps 3 could also be done with Sync fork > Update branch, followed by
git pullin the clone.
Clean up when you are done: rm -rf ~/git-practice/fork-flow.
Lab 3: ahead and behind¶
Objective: make your fork diverge from the original, read GitHub's comparison, and see the same divergence from Git.
Prerequisites and initial state: the fork and clone from Lab 1.
Tasks:
- On GitHub, in your fork, edit
README.md(any small change) and commit tomain. - Read the comparison line of your fork. Click Contribute to see what it offers, but do not open a pull request:
octocat/Spoon-Knifereceives thousands of practice pull requests, and yours is not needed. - In the clone, pull your change from
origin, then fetchupstream. - With Git, list the commits your
mainhas thatupstream/maindoes not have, and the other way around. - Delete the fork.
Expected result and verification:
- Task 2: This branch is 1 commit ahead of
octocat/Spoon-Knife:main; Contribute offers Open pull request. - Task 4:
git log --oneline upstream/main..mainshows your commit;git log --oneline main..upstream/mainshows nothing. - Task 5: the fork disappears from your repositories; the original is not affected.
Solution
cd ~/git-practice/fork/Spoon-Knife
git pull origin main
git fetch upstream
git log --oneline upstream/main..main # your commit: what you would propose
git log --oneline main..upstream/main # nothing: you are not behind
A..Blists the commits reachable fromBbut not fromA: exactly GitHub's "ahead" and "behind" counts.- Deleting a fork never affects the original repository, nor other people's forks.
Clean up when you are done: delete the fork on GitHub (Settings > General > Danger Zone > Delete this repository, in your fork), and rm -rf ~/git-practice/fork.