Aller au contenu

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

  1. On the repository page, click Fork (top right). The number next to it counts the existing forks.
  2. 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 main branch only Ticked Only the default branch is copied. Untick it to copy every branch
  3. 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 to origin (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:

  1. Open github.com/octocat/Spoon-Knife, note its number of branches, and fork it with the default options.
  2. On your fork, note the "forked from" line, the number of branches, and the comparison line above the files.
  3. Clone your fork into ~/git-practice/fork (public: no credentials needed).
  4. Add the original repository as the remote upstream, and fetch it.
  5. 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 the main branch only), says forked from octocat/Spoon-Knife, and This branch is up to date with octocat/Spoon-Knife:main.
  • git remote -v lists origin (your fork) and upstream (octocat/Spoon-Knife), each for fetch and push.
  • git branch -a shows origin/main, and three upstream/ 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.
  • origin is 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:

  1. Play Alex: clone the fork into alex, configure a name and email, and add Sam's repository (../sam-homelab.git) as upstream.
  2. Play Sam: in sam, add nas,192.168.1.20,storage, commit with the message Add NAS, and push.
  3. Play Alex: check git status (is the fork up to date?), then bring Sam's commit into the clone and into the fork.
  4. Still as Alex: create a branch add-printer, add printer01,192.168.1.30,printer, commit with the message Add network printer, and push the branch to the fork with an upstream.
  5. Try to push add-printer to upstream. (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 status says up to date with 'origin/main' even though Sam has a new commit: the clone only knows the fork. After git fetch upstream and git merge upstream/main (a fast-forward), git push origin main updates the fork.
  • Task 4 prints * [new branch] add-printer -> add-printer for the fork.
  • git -C ../alex-homelab.git log --oneline --all shows Add network printer and Add NAS; git -C ../sam-homelab.git branch shows only main.
  • 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 status compares 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 pull in 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:

  1. On GitHub, in your fork, edit README.md (any small change) and commit to main.
  2. Read the comparison line of your fork. Click Contribute to see what it offers, but do not open a pull request: octocat/Spoon-Knife receives thousands of practice pull requests, and yours is not needed.
  3. In the clone, pull your change from origin, then fetch upstream.
  4. With Git, list the commits your main has that upstream/main does not have, and the other way around.
  5. 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..main shows your commit; git log --oneline main..upstream/main shows 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..B lists the commits reachable from B but not from A: 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.