Course outline
Full-Stack course / Tools and the web

Git and GitHub

Reading

The idea

Imagine a video game with a very good save system. You can save at any moment and give each save a name. You can keep several save slots side by side and try something risky in one without touching the others. And you can keep a backup copy in the cloud, so a dead laptop does not end your game.

Git is that save system for your work. GitHub is the cloud where the backup copies live, and where other people can look at your slots and say "yes, add this to the main save". Every professional developer uses both, every day.

Git is already installed from the last lesson, and the commands below work the same in your macOS or Linux terminal and in the Ubuntu window on Windows (WSL2). You will also need a free account at github.com. Create one before you start the hands-on steps.

Before your first save, tell Git who you are. Git stamps your name on every checkpoint, so it asks once:

git config --global user.name "Sam Rivera"
git config --global user.email "sam@example.com"
git config --global init.defaultBranch main
git config --global core.editor "code --wait"

git config --global stores a setting for your whole computer. Use your own name and the email address you used for GitHub. The third line names your main save slot main in every new project. The last line makes Git open VS Code when it needs you to write a longer message. The --wait flag tells VS Code to hold Git until you close that tab. Without it Git would use an old editor called vim, which is famously hard to leave.

init, add, commit and reading the log

In this save system, a repository (or repo) is a project folder that Git is watching. Create one with a tiny recipe file:

mkdir recipes
cd recipes
git init
Initialized empty Git repository in /home/sam/recipes/.git/

git init ("initialize") tells Git to start watching this folder. It creates a hidden folder called .git, which is where Git keeps all the saves. Never edit anything inside .git by hand.

Now open VS Code with code soup.txt and type these three lines, then save:

Tomato soup
- 4 tomatoes
- 1 teaspoon of salt

Ask Git what it sees with git status, which reports the state of your folder:

git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	soup.txt

nothing added to commit but untracked files present (use "git add" to track)

Read it piece by piece. On branch main is the save slot you are in. No commits yet means you have made no saves. Untracked files means Git sees soup.txt but is not recording it yet.

Saving takes two steps. First you choose what goes into the checkpoint with git add. The place those chosen changes wait is called the staging area. Then you make the checkpoint itself, called a commit, with git commit:

git add soup.txt
git commit -m "Add tomato soup recipe"
[main (root-commit) 3f2a1bc] Add tomato soup recipe
 1 file changed, 3 insertions(+)
 create mode 100644 soup.txt

The -m flag lets you write the checkpoint's label right there in quotes. 3f2a1bc is the checkpoint's ID, a short code Git makes up. root-commit just means this is the very first save.

Why two steps? Because you might have changed five files but want one checkpoint to hold only two of them. Staging lets you pick.

To see the history of checkpoints, read the log:

git log
commit 3f2a1bcd5e0b9a8c7d6e5f4a3b2c1d0e9f8a7b6c (HEAD -> main)
Author: Sam Rivera <sam@example.com>
Date:   Fri Oct 16 10:02:11 2026 -0400

    Add tomato soup recipe

You see the full ID, who saved it, when, and the label. HEAD is Git's "you are here" marker, and HEAD -> main says you are standing at the newest checkpoint in the main slot. For a shorter view, add --oneline:

git log --oneline
3f2a1bc (HEAD -> main) Add tomato soup recipe

Before saving, you can also ask what changed since the last checkpoint. After editing soup.txt, run git diff. It prints the lines removed (starting with -) and added (starting with +). Press q to leave the viewer it opens.

Branches and merging

A branch is a parallel save slot. You open a new slot, experiment in it, and main stays safe. If the experiment works, you bring it back. If not, you throw the slot away.

Make a new slot with git switch -c (switch moves you between slots, and -c means "create it first"):

git switch -c add-bread
Switched to a new branch 'add-bread'

Create bread.txt in VS Code with two lines (Bread and - 3 cups of flour), then save a checkpoint in this slot:

git add bread.txt
git commit -m "Add bread recipe"

Now go back to the main slot and look around:

git switch main
ls
soup.txt

bread.txt is gone. It is safe in the add-bread slot, and main does not know about it yet. To list your slots, run git branch. The one with a * is where you are standing.

To bring the work over, stand in the slot that should receive it and merge the other one in:

git merge add-bread
Updating 3f2a1bc..9d8e7f6
Fast-forward
 bread.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 bread.txt

Fast-forward means main had not changed while you were away, so Git simply slid the "you are here" marker forward to the newest checkpoint. Nothing needed combining. Once merged, tidy up the slot you no longer need:

git branch -d add-bread
Deleted branch add-bread (was 9d8e7f6).

The -d flag deletes a branch only if it has been merged, so you cannot lose work by accident.

Remotes, push and pull

A remote is a cloud backup copy of your repo. The standard name for your main remote is origin. Pushing uploads your new checkpoints to it. Pulling downloads checkpoints other people (or another computer of yours) added.

First, install GitHub's helper tool, gh, which can sign Git in to your account and create repos for you. On macOS run brew install gh. On Linux and Ubuntu in WSL2 run sudo apt install -y gh. Then sign in:

gh auth login

Choose GitHub.com, then HTTPS, say yes when it asks whether to authenticate Git with your GitHub credentials, and choose "Login with a web browser". It shows a one-time code to type into the page it opens.

Now create an empty repo on GitHub and connect your folder to it:

gh repo create recipes --private --source=. --remote=origin
git remote -v
origin	https://github.com/sam/recipes.git (fetch)
origin	https://github.com/sam/recipes.git (push)

--private means only you can see it. git remote -v lists your remotes: the name origin and the address it points to. Now upload your main slot:

git push -u origin main
To https://github.com/sam/recipes.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Read the middle line as "my main was sent to the remote's main". The -u flag links your local main to origin/main, so from now on a plain git push and git pull know where to go. Refresh your repo's page on github.com and your files are there.

To download new work from the cloud copy:

git pull
Already up to date.

If a friend (or you, editing in the GitHub page) added checkpoints, the output would instead list Fast-forward and the files that changed.

Pushing can be refused. If the remote has checkpoints you do not have, Git stops you:

 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/sam/recipes.git'

rejected means the push was turned away, and fetch first is the instruction: download the newer work first. Run git pull, then push again.

Pull requests and review

On a team, nobody drops work straight into main. Instead you push your own slot and open a pull request (often shortened to PR). A PR is a request that says "please merge my slot into the main one", and it gives teammates a place to review your work: read each changed line, comment on it, and approve it or ask for changes before it joins.

git switch -c add-salad

Create salad.txt with Green salad and - 1 head of lettuce, then:

git add salad.txt
git commit -m "Add green salad recipe"
git push -u origin add-salad
gh pr create --fill
Creating pull request for add-salad into main in sam/recipes

https://github.com/sam/recipes/pull/1

git push -u origin add-salad uploads the new slot. gh pr create --fill opens the PR and fills in its title and description from your commit message. Open the page it prints in your browser.

Your tour of a PR page:

  • The Files changed tab shows every line that differs. Hover a line and press the blue plus button to leave a comment on exactly that line.
  • A reviewer then chooses Comment, Approve or Request changes.
  • When everyone is happy, the Merge pull request button brings the slot into main.

GitHub does not let you approve your own PR, so when you work alone you leave a comment and merge it yourself. The habit is still worth building. After the merge, update your computer and tidy up:

git switch main
git pull
git branch -d add-salad

Resolving merge conflicts

Sometimes two slots change the very same spot, and Git cannot tell which version you want. This is a merge conflict. The game stops and asks you which version of that scene to keep. It is a normal event, not a disaster.

Here is one on purpose. Start a slot where the soup uses 2 teaspoons of salt. Edit the last line of soup.txt in VS Code to - 2 teaspoons of salt and save:

git switch -c more-salt
git commit -am "Use 2 teaspoons of salt"

The -a flag stages every file Git already tracks, so -am adds and commits in one go (it does not pick up brand-new files). Now go back to main and change the same line a different way, to - half a teaspoon of salt:

git switch main
git commit -am "Use half a teaspoon of salt"
git merge more-salt
Auto-merging soup.txt
CONFLICT (content): Merge conflict in soup.txt
Automatic merge failed; fix conflicts and then commit the result.

CONFLICT (content) names the problem, in soup.txt names the file, and the last line is your instruction. Open soup.txt and you will find Git marked the clash for you:

Tomato soup
- 4 tomatoes
<<<<<<< HEAD
- half a teaspoon of salt
=======
- 2 teaspoons of salt
>>>>>>> more-salt

These are the conflict markers. Between <<<<<<< HEAD and ======= is your current slot's version. Between ======= and >>>>>>> more-salt is the incoming one. Your job is to decide, edit the file so it holds exactly the final text, and delete all three marker lines:

Tomato soup
- 4 tomatoes
- 1 teaspoon of salt

Then finish the merge. Staging the file tells Git "this one is resolved":

git add soup.txt
git commit -m "Merge more-salt: settle on 1 teaspoon"

If you get stuck halfway, git merge --abort puts everything back as it was before the merge started. VS Code also shows buttons above each conflict ("Accept Current Change", "Accept Incoming Change", "Accept Both Changes") that edit the markers for you.

Undoing mistakes: restore, revert and reset

Every game lets you load an old save. Git gives you three ways, from gentle to drastic.

restore reloads the last checkpoint for one file and throws away the edits you have not saved. Say you broke soup.txt while experimenting:

git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   soup.txt
git restore soup.txt

The file is back as of the last checkpoint. This cannot be undone, because those edits were never saved. To only take a file out of staging (without touching your edits), use git restore --staged soup.txt.

revert is for a checkpoint you already made. It does not erase it. It writes a new checkpoint that cancels the old one, so the history stays honest. This is the safe choice for anything you have already pushed:

git revert HEAD --no-edit
[main 5c4d3e2] Revert "Use half a teaspoon of salt"
 1 file changed, 1 insertion(+), 1 deletion(-)

HEAD means "the newest checkpoint", and --no-edit accepts Git's suggested label instead of opening the editor.

reset moves the "you are here" marker backward, so later checkpoints vanish as if they never happened. ~1 means "one step back":

git reset --soft HEAD~1

--soft removes the last checkpoint but leaves its changes staged and ready to save again, which is perfect for "I committed too early". Adding --hard instead removes the checkpoint and also throws away the changes, with no way back. Use --hard only on work that exists nowhere else, and never on checkpoints you have already pushed, because it rewrites history that your teammates may have downloaded.

gitignore and keeping secrets out of the repo

Some files should never go in the save system: scratch files, junk your computer creates, and above all files that hold passwords or access keys. A file called .gitignore is a list of names Git promises to never record.

Create a file called private-notes.txt with a made-up line like bank password: hunter2, and run git status. Git will offer it as a new file to save. Now create .gitignore containing these lines:

private-notes.txt
*.log
.DS_Store

The * is a wildcard meaning "anything", so *.log ignores every file ending in .log. .DS_Store is a junk file macOS scatters through folders. Check that Git obeys:

git status --short
git check-ignore -v private-notes.txt
?? .gitignore
.gitignore:1:private-notes.txt	private-notes.txt

git status --short squeezes the report into one line per file; ?? means untracked. private-notes.txt is no longer listed, and check-ignore -v shows which line of which file is hiding it. Save the .gitignore itself in a checkpoint so everyone who copies the repo gets the same rules.

An ignore rule only protects files Git has not recorded yet. If you already committed a secret, ignoring it later is too late. To stop tracking it while keeping your copy:

git rm --cached private-notes.txt
rm 'private-notes.txt'

The old checkpoints still contain the secret, and git log -p (the -p flag shows what each checkpoint changed) can display it. If the repo was ever pushed, assume the secret has been seen: go to wherever it came from and change the password or replace the key. Deleting the file does not make a leaked secret safe again.

Good commit messages and a README

A commit message is the label on a checkpoint, written for the person who has to find it later, and that person is usually you in six months. Compare these two histories:

a91c4de stuff
7e2b8f0 fix
c3d5a19 asdf
a91c4de Add salad dressing step to the green salad
7e2b8f0 Fix salt amount in tomato soup
c3d5a19 Add .gitignore for logs and notes

The second one reads like a table of contents. The habits that get you there:

  • Write a short first line (about 50 characters) in the imperative, like giving an order: "Add", "Fix", "Remove".
  • Make each checkpoint one logical change, so it has one clear label and is easy to revert.
  • When the reason matters, leave a blank line and explain the why in a few sentences. Run git commit with no -m and VS Code opens so you can write the body:
Fix salt amount in tomato soup

Two teaspoons made the soup too salty in testing, so go back to one.

A README is the front page of your repo. It is a file named README.md that GitHub displays automatically under your files. The .md means Markdown: plain text with light symbols, where # makes a heading, ** around words makes them bold and backticks make code. A first README answers three questions: what is this, how do I use it, and what state is it in.

# Recipes

A small collection of family recipes in plain text files.

**How to use:** open any `.txt` file in this folder and follow the steps.

**Status:** soup, bread and salad are done. Desserts are coming.

Save it, then:

git add README.md
git commit -m "Add README"
git push

Refresh the repo page on github.com and your README is the first thing visitors see.

Common mistakes

  • Forgetting git add before git commit. Git answers nothing added to commit but untracked files present. Stage the file, then commit.
  • Committing everything blindly with git add .. The . means "every change here", and that includes a file with a password. Run git status first and add files by name.
  • Working directly on main. It is easy to merge a branch later and very annoying to untangle a mess in main. Make a branch for each piece of work.
  • Panicking at conflict markers. The <<<<<<< lines are just text Git inserted. Pick the right content, delete all three marker lines, then git add and git commit.
  • Using git reset --hard on anything precious. There is no undo. Prefer restore for uncommitted edits and revert for shared checkpoints.
  • Thinking a deleted secret is gone. It stays in the history. Change the password or key at its source.
  • Pushing before pulling. If Git says rejected ... (fetch first), run git pull and push again.
  • Vague messages like "fix" or "update". Future you will not remember which fix.
  • Keeping repos in Windows-side folders when using WSL2. Keep them in your Ubuntu home folder (~) so Git stays fast.

Try it

  1. Make your first checkpoint. Run the four git config --global lines from the start of the lesson with your own name and email. Then run mkdir trip-notes, cd trip-notes and git init. Create packing.txt in VS Code containing Packing list, - passport and - charger on three lines. Run git status and confirm it lists packing.txt as untracked. Then run git add packing.txt, git commit -m "Add packing list" and git log --oneline. You should see one line with your message.
  2. Branch and merge. Run git switch -c add-snacks, add a line - snacks to packing.txt, and run git commit -am "Add snacks to packing list". Run git switch main and open packing.txt: the snacks line should be missing. Run git merge add-snacks, see Fast-forward, then run git branch -d add-snacks and git log --oneline. You should now see two checkpoints.
  3. Connect a remote. Install gh (brew install gh or sudo apt install -y gh) and run gh auth login. Then run gh repo create trip-notes --private --source=. --remote=origin, followed by git push -u origin main. Open your new repo on github.com and check that packing.txt is there. On that page, click the pencil icon to edit packing.txt, add a line - sunglasses and commit it on the page. Back in your terminal run git pull and confirm the sunglasses line appears in your local file.
  4. Open a pull request. Run git switch -c add-maps, add - maps to packing.txt, then run git commit -am "Add maps to packing list", git push -u origin add-maps and gh pr create --fill. Open the PR page it prints, click the Files changed tab, hover the new line and leave a comment on it, then click Merge pull request. Back in the terminal run git switch main, git pull and git branch -d add-maps. Confirm - maps is now in your local file.
  5. Cause and resolve a conflict. Run git switch -c usb-c, change the - charger line to - USB-C charger, and run git commit -am "Name the charger type". Run git switch main, change the same line to - charger and cable, and run git commit -am "Pack a cable too". Run git merge usb-c and confirm Git says CONFLICT. Open the file, keep - USB-C charger and cable, delete the three marker lines, then run git add packing.txt and git commit -m "Merge usb-c: keep both details". Run git log --oneline and see the merge.
  6. Try all three undo tools. Add a line - oops to packing.txt, run git status to see it modified, then run git restore packing.txt and check the line is gone. Add - bad idea, run git commit -am "Add bad idea", then run git revert HEAD --no-edit and check the log shows a Revert checkpoint. Finally add - hat, run git commit -am "Add hat", then git reset --soft HEAD~1 and git status: the hat change should be staged but the checkpoint gone. Run git restore --staged packing.txt and git restore packing.txt to clean up.
  7. Ignore a file. Create private-notes.txt containing bank password: hunter2 and run git status: it appears as untracked. Create .gitignore containing private-notes.txt and *.log on separate lines, run git status --short, and confirm private-notes.txt is no longer listed. Run git check-ignore -v private-notes.txt to see which rule hides it. Commit the ignore file with git add .gitignore and git commit -m "Ignore private notes and logs".
  8. Write a good message and a README. Create README.md in VS Code with a title line starting with # Trip notes, one sentence about what the repo is, and a line starting with **Status:**. Run git add README.md, then run git commit with no -m. VS Code opens: write a first line like Add README, a blank line, and one sentence explaining why, then save and close that tab. Run git log to see your full message, then git push, and refresh your repo page to see the README displayed under your files.