Have you ever been told to download something from GitHub and been greeted with a long list of folders and no clear DOWNLOAD button? This is a jarring yet completely normal experience — git and GitHub are targeted towards software developers. But fear not, you will become a competent user of both tools at the end of this tutorial.

Git vs GitHub: the key distinction

Git and GitHub are two separate things. Git is a program installed on your computer that tracks changes to files in a project folder. That folder, once tracked by git, is called a local repository (or local repo) — it lives entirely on your machine and works with no internet connection required. GitHub is a separate website that hosts git repositories in the cloud. That cloud-hosted copy is called the remote repository (or remote repo). Think of your local repo as your personal working copy, and the remote repo as the shared, backed-up version that your whole team can access.

You turn any ordinary folder into a local git repo by running git init inside it. Once your local repo is linked to a GitHub repo, git push uploads your committed changes from your machine up to GitHub, and git pull downloads the latest changes from GitHub and applies them to your local repo.

Staging and commit

git add -A saves all of the changes you've made in your local repo to a temporary location known as the staging area. It allows for more granular version control than git commit — it's useful for grouping related changes before locking them in with a commit (pun intended).

git commit logs all of the staged changes you've made in your local repo up to this point, permanently. This is especially useful for when something goes wrong: git revert <commit-hash> creates a new commit that undoes the changes from a specific earlier commit, without erasing history. You may think of git commit as creating a newer version of the project.

To keep things simple you can just run git add -A && git commit -m "<commit message>" to create a newer version of your project, or append && git push to also upload the latest version to GitHub.


The four zones your code travels through. Changes start in your Working Directory, get staged with git add, locked in with git commit, and uploaded to GitHub with git push. git pull brings changes back down.

Downloading: clone, fetch, and pull

You might have also heard of git fetch and git clone — together with git pull they all function as the DOWNLOAD button, but they have important differences:

  • git clone <url> downloads the entire project from GitHub to your current working directory. Use this command if you are downloading a project for the very first time.
  • git fetch downloads the latest version of a project from GitHub to your already-synced local repo, but leaves your current local version unchanged.
  • Once you're ready, you can update your local version to match via git merge.
  • git pull is just git fetch && git merge in a single step.

Three ways to download from GitHub. git clone is for first-time setup; git pull is the everyday shorthand; git fetch followed by git merge gives you the same result in two explicit steps — useful when you want to inspect changes before applying them.

What about curl?

curl is a command-line tool for transferring data from any URL — it speaks HTTP, HTTPS, FTP, and a dozen other protocols. Where git clone and git fetch understand repositories, commits, and history, curl simply fetches whatever the server returns at a given address and either prints it or saves it to a file. It has no concept of version control.

The most common flags:

  • -O — save the file using its original remote filename (e.g. curl -O https://example.com/file.zip)
  • -o <filename> — save to a specific local filename instead
  • -L — follow redirects. Many download URLs redirect once or twice before reaching the actual file; without -L you get the redirect response, not the file. Almost always needed.
  • -s — silent: suppress the progress meter and error messages. Useful in scripts.
  • -v — verbose: print full request and response headers. Useful for debugging.
  • -X <METHOD> — set the HTTP method: -X POST, -X DELETE, etc. Defaults to GET.
  • -H "<Header: value>" — add a request header, e.g. -H "Authorization: Bearer <token>" for authenticated API calls.
  • -d "<data>" — send a request body (implies POST). Commonly used with -H "Content-Type: application/json".

A typical one-liner to download a single raw file from GitHub:

curl -L https://raw.githubusercontent.com/user/repo/main/script.sh -o script.sh

curl vs git clone / git fetch at a glance:

  • curl downloads the raw bytes at a URL — a file, an API response, an HTML page. It knows nothing about commits, branches, or history.
  • git clone / git fetch download a git repository — the full history, all branches, and all metadata. You can revert to any past commit, create branches, and push changes back.
  • Use curl when you want a single file or need to talk to an API. Use git clone when you want to work with the project itself — edit, commit, and push.

Starting a new project from scratch (2026 workflow)

If you're starting a fresh project rather than cloning an existing one, the typical setup looks like this:

  1. Create a new repository on github.com (click the "+" icon → "New repository"). Give it a name and leave it empty — no README, no .gitignore — so it doesn't conflict with your local files.
  2. In your local project folder, run git init to make it a git repository.
  3. Create a .gitignore file in the root of your project folder. This is a plain text file that tells git which files and folders to never track — things like node_modules/ (JavaScript dependencies), .env (secrets and API keys), or __pycache__/ (Python cache). Without it, you risk accidentally committing files that are too large, auto-generated, or sensitive. GitHub maintains ready-made templates for most languages at github.com/github/gitignore.
  4. Link your local repo to the GitHub repo:
    git remote add origin <url>
    where <url> is the HTTPS URL shown on your new GitHub repo page. This tells git where git push and git pull should point.
  5. Stage, commit, and push your initial code:
    git add -A && git commit -m "initial commit" && git push -u origin main
    The -u flag sets the upstream so future git push and git pull commands don't need the branch name.

Authentication (easiest method in 2026): use the GitHub CLI. Install it from cli.github.com, then run gh auth login and follow the browser prompt. This handles OAuth automatically — no personal access tokens to create or manage.

Optional — GitHub CLI for one-step setup: if you have the GitHub CLI installed, you can skip steps 1–3 entirely. From inside your local folder, run:
gh repo create <repo-name> --public --source=. --remote=origin --push
This creates the GitHub repo, links it as origin, and pushes your initial commit all in one command.

Branches and pull requests

git checkout -b <branch-name> creates a new and identical copy of your project in a separate branch. This allows you to keep working without touching the main branch. For a solo project this is optional, but for a team project it's important to keep track of who made what changes. (The modern equivalent is git switch -c <branch-name>.)

Once you've made commits on your branch and pushed it to GitHub, you open a pull request (PR) on GitHub. A PR is a formal proposal to merge your branch into main — GitHub displays exactly what changed, and your teammates can leave comments, request changes, or approve it. Once approved, someone clicks "Merge pull request" and your branch is merged. This is the standard team collaboration workflow on GitHub: each person works on their own branch, then proposes changes via a PR rather than pushing directly to main.


A typical branch workflow. Commits A, B, C land on main. A feature branch is created at C and receives commits D and E. A pull request on GitHub proposes merging the feature branch back into main, producing merge commit F.

Quick-reference: useful commands

  • git diff — lists all unstaged changes
  • git diff --staged — lists all staged but uncommitted changes
  • git restore --staged <file> — unstages a file without losing your changes; moves it back from the staging area to your working directory
  • git restore <file> — discards all unstaged changes to a file, reverting it to its last committed state. Warning: this cannot be undone.
  • git status — gives a summary of which files have been changed and their tracking status (unstaged/staged)
  • git log — lists commit history
  • git branch -a — displays all git branches; the currently active branch is marked with an asterisk (*)
  • git switch <branch-name> — switches to an existing branch
  • git switch -c <new-branch> — creates a new branch and switches to it
  • git switch --detach <commit-hash> — moves you to a specific past commit. This puts you in a "detached HEAD" state, where HEAD points directly to that commit instead of a branch. Warning: any new commits made in this state are not part of any branch and may be lost if you switch away without first running git switch -c <new-branch-name>.
  • git merge <branch-name> — integrates the changes from the specified branch into your current branch. It creates a merge commit that ties together the histories of both branches, preserving the full record of where each change came from.
  • git rebase <branch-name> — replays your current branch's commits on top of the specified branch, resulting in a cleaner, linear commit history. Unlike merge, it rewrites your commits rather than adding a merge commit. Because it rewrites history, avoid rebasing commits that have already been pushed and shared with others.

git merge vs git rebase on the same starting point (main has A–B–C, feature has D–E). Merge adds a merge commit F and preserves the full branching history. Rebase replays D and E on top of C, producing rewritten commits D′ and E′ with a clean linear history — but no record of the branch.

Your daily workflow

This is a lot of information, but as a solo developer your daily workflow can be very simple. As an example:

  1. Navigate to your project folder and pull the latest changes at the start of each session:
    cd ~/path/to/local-repo && git pull
  2. Work on your project. At each logical stopping point, log your progress:
    git add -A && git commit -m "<what you changed>"
    Repeat this throughout your session — small, frequent commits are better than one large one at the end.
  3. Upload your progress to GitHub when you're done for the day:
    git push

Merging multiple branches into main

A common pattern: you have a main branch and several child branches, each representing one unit of work — a new article, a bug fix, a feature. The safest approach is to merge them into main one at a time, pushing to the remote after each merge so the remote always reflects your latest stable state.


Four feature branches created from commit C on main. Each is merged back one at a time, producing a merge commit (M1–M4). Push to the remote after each merge.

The workflow, step by step:

  1. Make sure main is current before you start:
    git checkout main && git pull
  2. Merge the first branch and push:
    git merge feature/feature-1 && git push origin main
    If conflicts arise, resolve them first (see below), then push.
  3. Repeat for each remaining branch in order.
  4. Delete the merged branches when done:
    git branch -d feature/feature-1 feature/feature-2 ...

How does git merge work under the hood

When you run git merge B while on branch A, git first locates the merge base — the most recent commit the two branches share in their history. It then computes two diffs: what changed from the merge base to A's latest commit, and what changed from the merge base to B's latest commit. Finally, it combines both sets of changes into a new merge commit that records both A and B as its parents. Because three commits are involved — the merge base plus the two branch tips — this is called a three-way merge.


A three-way merge. Git finds commit X — the merge base — computes what changed on each branch since X, then combines both into merge commit M.

Fast-forward. If branch B is a direct ancestor of A — meaning A has no new commits since B was created — there is nothing to combine. Git simply moves A's branch pointer forward to B's tip without creating a merge commit. You will notice this in git log: no merge commit appears, just a straight line of history. Pass --no-ff to force a merge commit regardless, which keeps a visible record that a branch existed.

Resolving merge conflicts

A conflict happens when two branches have each modified the same lines in the same file. Git cannot decide which version to keep, so it halts the merge and marks the affected files for you to resolve manually. Running git status will list every file flagged both modified.

Open a conflicted file and you will find conflict markers inserted by git:

<<<<<<< HEAD
the version from your current branch (main)
=======
the version being merged in (feature/article-2)
>>>>>>> feature/article-2

Everything between <<<<<<< HEAD and ======= is what your current branch has. Everything between ======= and >>>>>>> is what the incoming branch has.

Your job: edit the file to its correct final state, then delete all three marker lines. Three strategies:

  • Keep one side — delete the other block and all three markers. Use this when one version is simply right and the other is outdated.
  • Keep both — delete the three marker lines but keep both blocks of content. Use this when adding entries to a shared list — both additions are valid and should coexist.
  • Rewrite — delete everything between the markers and write the correct content from scratch. Use this when neither version is quite right on its own.

Once every conflicted file is saved with no markers remaining, complete the merge:

git add <conflicted-file>    # mark as resolved
git merge --continue          # opens editor for the merge commit message
git push origin main

To abandon the merge entirely and return to the pre-merge state: git merge --abort.

The most common conflict in web projects is a shared data file — a central articles.json, a navigation list, a config — where multiple branches each appended a new entry. The resolution is always the same: keep all entries from both sides, remove the markers, save, and continue.