# Git and GitHub

From your first commit to pull requests, undoing mistakes, GitHub Actions and a site on GitHub Pages.

Multimedia & Creative Technology · 3.1 Extra Webdev · Self-study · version 0.1.0 · 2026-10-14

Tags: git, github, terminal, security

Slides: <https://slides.mctantwerp.be/en/extra-webdev/01-github>

<!-- slide 2 -->

## After this lesson you can

- Set up Git and GitHub on your laptop and put any project in a repository
- Commit, push and pull without losing work, and write commit messages that make sense later
- Work on a branch, open a pull request, review someone else's and solve a merge conflict
- Undo almost any mistake, and pick the right command for it
- Contribute to a repo that is not yours through a fork
- Publish a site with GitHub Pages and let GitHub Actions check every push

> **Explanation:** Each of these points has a chapter of its own. If you already use Git through a GUI, read chapter 3 anyway: knowing which command sits behind a button is what saves you when the button does something unexpected.

<!-- slide 3 -->

## Agenda

1. Why version control
2. Setting up
3. The basic loop
4. GitHub
5. Branches
6. Pull requests
7. Issues and forks
8. Undoing things
9. Rebase, tags and rules
10. Automation and security

> **Explanation:** Never used Git: start at chapter 1 and do not skip anything. Used Git through GitHub Desktop or VS Code: skim chapters 1 and 2, but read 3 carefully. Already comfortable with branches and pull requests: start at chapter 8, and check chapter 10 before your next project.

<!-- slide 4 -->

## 1. Why version control

> **Explanation:** Before the commands: what problem Git solves, and what is Git and what is GitHub. Most confusion in the first weeks comes from mixing up those two.

<!-- slide 5 -->

**Git remembers every version.**

portfolio_final.zip, portfolio_final2.zip, portfolio_final_REALLY.zip. Git replaces that folder of copies with one history: every version, who made it, when, and why.

> **Explanation:** Ctrl+Z dies with your editor, and a backup on OneDrive syncs the broken version just as happily as the working one. Version control keeps every saved state with a reason attached, so you can go back to yesterday at four o'clock and see what changed since. That works alone, and it is the only sane way to work with two people on the same files.

<!-- slide 6 -->

### Git is not GitHub

#### Git

- A program on your laptop, free and open source
- Does all the real work: commits, history, branches, merges
- Works completely offline
- Docs and the free book: [git-scm.com/book](https://git-scm.com/book/en/v2)

#### GitHub

- A website that hosts Git repositories, owned by Microsoft
- Adds what a team needs on top: pull requests, reviews, issues, Actions, Pages
- Without Git on your laptop, you can do very little with it
- Docs: [docs.github.com](https://docs.github.com)

> **Explanation:** Git was written by Linus Torvalds in 2005 for the Linux kernel. GitHub came in 2008 and was bought by Microsoft in 2018. GitLab, Bitbucket and Codeberg do the same job as GitHub; your Git knowledge carries over one to one. Whatever host you pick, the commands in this deck stay the same.

<!-- slide 7 -->

### Four words

- **Repository**: A project under version control: your files plus their complete history. On your laptop, that history lives in a hidden folder `.git`.
- **Commit**: One saved snapshot of your project, with a message, an author and a date. The history is a chain of commits.
- **Remote**: A copy of the repo somewhere else, usually on GitHub. Its default name is `origin`. Your laptop and the remote each have the full history.
- **Branch**: A separate line of commits, so you can work on something without touching the version that works. The main line is called `main`.

> **Explanation:** You will meet these four words in every error message, tutorial and job interview. A commit is identified by a hash, a long hexadecimal code like 4b7d9e2f... Git usually shows the first seven characters, and those are enough to point at a commit in any command.

<!-- slide 8 -->

## 2. Setting up

> **Explanation:** Fifteen minutes, once per laptop. Do it properly now: a wrong email address means your commits are not linked to your GitHub profile, and that is annoying to fix afterwards.

<!-- slide 9 -->

### Installing Git

- **macOS**: `xcode-select --install` in Terminal, or `brew install git` if you use Homebrew.
- **Windows**: The installer from [git-scm.com/downloads](https://git-scm.com/downloads). It brings Git Bash, a terminal that understands the same commands as on a Mac. Use that one.

```console
$ git --version
git version 2.56.0
```

A version number means Git is installed. Any version from the last few years works for this deck.

> **Explanation:** On a Mac, xcode-select installs Apple's command line tools, Git included. That version lags behind a little, which is fine. With Homebrew you get the newest one. On Windows, accept the defaults of the installer, but pick VS Code or nano as the editor when it asks, instead of Vim. On Linux, use your package manager: sudo apt install git on Ubuntu.

<!-- slide 10 -->

### Introduce yourself to Git

```console
$ git config --global user.name "Your Name"
$ git config --global user.email "you@example.com"
$ git config --global init.defaultBranch main
$ git config --global pull.rebase false
$ git config --global core.editor nano
```

1. Every commit carries your name. Use your real name, not your gamertag.
2. The same address as on your GitHub account, or GitHub cannot link your commits to your profile.
3. New repos start on a branch called main, like on GitHub.
4. A pull may merge. Without it, recent Git stops with "divergent branches".
5. When Git needs a message from you, it opens nano instead of Vim.

> **Explanation:** Want to keep your real email address private? On GitHub, under Settings, Emails, tick Keep my email addresses private. GitHub then shows a noreply address (a number plus your username at users.noreply.github.com). Use that one in user.email. The --global flag writes to a file in your home folder, ~/.gitconfig, so it counts for every repo on this laptop.

<!-- slide 11 -->

### Logging in with gh

```console
$ gh auth login
? Where do you use GitHub? GitHub.com
? What is your preferred protocol for Git operations? HTTPS
? Authenticate Git with your GitHub credentials? Yes
? How would you like to authenticate GitHub CLI? Login with a web browser

! First copy your one-time code: 4F2A-9C1B
Press Enter to open https://github.com/login/device in your browser...
✓ Authentication complete.
✓ Logged in as samserrien
```

The easiest way to let your laptop talk to GitHub. You log in once in the browser, gh stores the token in your system keychain.

Install it from [cli.github.com](https://cli.github.com). You will use it again for pull requests in chapter 6.

> **Explanation:** gh is the GitHub CLI, a separate program next to Git: brew install gh on a Mac, winget install GitHub.cli on Windows, or the installer from cli.github.com. When you pick HTTPS, gh also offers to log Git in with the same account, so git push stops asking for a password. Your GitHub password itself never works for git push, it has not for years.

<!-- slide 12 -->

### Or an SSH key

```console
$ ssh-keygen -t ed25519 -C "you@example.com"
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9k2... you@example.com
$ ssh -T git@github.com
Hi samserrien! You've successfully authenticated, but GitHub does not provide shell access.
```

1. Makes a key pair in ~/.ssh. Enter accepts the default location.
2. The public key, ending in .pub. Paste it on GitHub under Settings, SSH and GPG keys, New SSH key.
3. Tests the connection. Your own username means it works.

With SSH you clone with the url that starts with `git@github.com:`. The full guide: [Connecting to GitHub with SSH](https://docs.github.com/en/authentication/connecting-to-github-with-ssh).

> **Explanation:** SSH is the alternative to HTTPS: a key pair instead of a token. The private key (the file without .pub) never leaves your laptop, do not mail it or put it in a repo. A passphrase protects the key if your laptop gets stolen; on a Mac, ssh-add --apple-use-keychain remembers it for you. If gh auth login worked, you do not need this slide yet, but you will need SSH keys for servers later.

<!-- slide 13 -->

## 3. The basic loop

> **Explanation:** This is the loop you will repeat hundreds of times a week: change, add, commit. Everything in this chapter happens on your laptop only. GitHub only enters the picture in chapter 4.

<!-- slide 14 -->

### Where a change lives

- **01 Working tree**: Your files as they are on disk. What you change here is in no commit yet.
- **02 Staging area**: What goes into the next commit. You put a change here with `git add`.
- **03 Local repo**: The history on your laptop. `git commit` turns the staging area into a commit.
- **04 Remote**: The copy on GitHub. Your commits only get there with `git push`.

Every command in this chapter moves a change one box to the right, or tells you which box it is in. Read more in [Pro Git, Recording changes](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository).

> **Explanation:** The staging area confuses everyone at first: why not commit everything right away? Because you often change two unrelated things, and two small commits are easier to read and undo than one big one. git add lets you pick. A new file that Git has never seen is untracked: Git sees it, but leaves it out of every commit until you add it once.

<!-- slide 15 -->

### Your first commit

```console
$ git init
Initialized empty Git repository in /Users/you/portfolio/.git/
$ git status
On branch main
No commits yet
Untracked files:
       index.html
       style.css
$ git add index.html style.css
$ git commit -m "Add homepage"
[main (root-commit) 8f3a2c1] Add homepage
 2 files changed, 64 insertions(+)
```

1. Turns the folder into a repository. Once per project.
2. Git sees two files it does not track yet. Red means: not staged.
3. Stages both. git add . stages everything in the folder, so read the status first.
4. One commit with both files, and the message after -m.

> **Explanation:** git init only creates the hidden .git folder, nothing else changes. Run it in the root of your project, never in your home folder: then Git would track every file you own. If git status suddenly lists your Documents and Downloads, that is what happened; deleting the .git folder in your home folder undoes it.

<!-- slide 16 -->

### Reading git status

```console
$ git status
On branch main
Changes to be committed:
 (use "git restore --staged <file>..." to unstage)
       modified:   index.html

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:   style.css
```

Green is staged and goes into the next commit. Red is changed but not staged, and stays out. The lines in grey are hints: Git tells you how to move a file from one group to the other.

> **Explanation:** Make git status a reflex: before every add, before every commit, and whenever you are confused. Git almost always prints the command you need next between brackets. Ahead of origin by 1 commit means you committed but did not push yet; that matters once GitHub is involved.

<!-- slide 17 -->

### git diff

```console
$ git diff
diff --git a/style.css b/style.css
@@ -1,4 +1,4 @@
 body {
-  font-family: Arial;
+  font-family: system-ui;
   margin: 0;
 }
```

Shows what changed line by line: red is removed, green is added.

`git diff` shows what is not staged yet. `git diff --staged` shows what is staged and goes into the next commit.

> **Explanation:** By default git diff opens in a pager when the output is long: scroll with the arrows or space, and quit with q. git diff --staged is the one to run right before a commit, it is exactly what goes in. In VS Code or GitHub Desktop, clicking a changed file shows the same diff with colours.

<!-- slide 18 -->

### git log

```console
$ git log --oneline
a1c3f80 [green](HEAD -> main) [dim]Add contact form
9e0b3a7 [dim]Fix footer link
8f3a2c1 [dim]Add homepage
$ git config --global alias.lg "log --oneline --graph --all"
```

One line per commit, newest first: the short hash and the message. `HEAD` is where you are now. The second command makes `git lg` a shortcut for the long version with the branches drawn as lines.

> **Explanation:** The hash in front of every line is how you point at a commit in other commands, like git show 9e0b3a7 or git revert 9e0b3a7. The alias saves you typing: after the config line, git lg works in every repo. --graph draws branches as lines, which you will need in chapter 5.

<!-- slide 19 -->

### .gitignore

*.gitignore*

```bash
# Comes back with npm install
node_modules/

# Build output
dist/

# Secrets
.env

# OS junk
.DS_Store
```

One pattern per line, in the root of your repo. Git acts as if these files do not exist.

The .gitignore file itself you do commit, so everyone on the project ignores the same things.

Templates per stack: [github.com/github/gitignore](https://github.com/github/gitignore).

> **Explanation:** node_modules can be hundreds of megabytes and comes back with npm install, so it never goes in a repo. dist is your build output, which a server or a workflow can rebuild. .DS_Store is junk macOS drops in every folder. For a new project, GitHub suggests a template when you create the repo, and github.com/github/gitignore has one for every stack.

<!-- slide 20 -->

### Ignored too late

```console
$ echo ".env" >> .gitignore
$ git rm --cached .env
rm '.env'
$ git commit -m "Stop tracking .env"
```

A file Git already tracks, it keeps tracking, .gitignore or not. `git rm --cached` removes it from the repo but leaves it on your disk. Older commits still contain it.

> **Explanation:** This happens to everyone once: you add .env to .gitignore after it was already committed, and Git keeps tracking it. The rule is that .gitignore only affects untracked files. If the file contained a real key and you already pushed it, removing it is not enough: chapter 10 explains what to do then.

<!-- slide 21 -->

### Commit messages

#### Not like this

- "update"
- "wip"
- "fix"
- "asdfgh"
- One commit with three days of work in it

#### Like this

- "Fix typo on the about page"
- "Add contact link to the navigation"
- "Move footer styles to footer.css"
- Small commits that each do one thing
- One language per project, usually English

> **Explanation:** Start with a verb in the imperative, as if you finish the sentence 'If applied, this commit will...': Add, Fix, Remove, Move. Keep the first line under about 50 characters, GitHub cuts off longer ones. Need more? Leave out -m, Git opens your editor, and everything after an empty line becomes the description. Many teams use Conventional Commits (feat:, fix:, docs:); when a project does, follow it.

<!-- slide 22 -->

### You run git add style.css, then change style.css again, then git commit. What is in the commit?

- A. Both changes
- B. Only the change from before git add
- C. Only the last change
- D. Nothing, Git asks you to add again

**Answer:** B. Only the change from before git add

git add stores a copy of the file as it was at that moment. The later change is still in your working tree, and git status shows style.css twice: once green, once red. Add it again to include it.

> **Explanation:** This is the staging area doing exactly what it promises. It also means you can stage half of your work on purpose, and that git add right before git commit is the safest habit.

<!-- slide 23 -->

## 4. GitHub

> **Explanation:** Now your history leaves your laptop. A remote is your backup, the place your team syncs with, and the start of everything GitHub adds: pull requests, issues, Actions and Pages.

<!-- slide 24 -->

### A repository on GitHub

- **[01 New repository](https://github.com/new)**: The plus at the top right of GitHub, or github.com/new.
- **02 Name and visibility**: Lowercase with hyphens, like `my-portfolio`. Public or private.
- **03 Start empty or not**: Tick Add README to clone it right away. Leave everything off to push a project you already have.

> [!TIP]
> With gh it is one command from inside your project folder: `gh repo create my-portfolio --public --source=. --push`. That creates the repo, adds it as origin and pushes.

> **Explanation:** Public is fine for portfolio work and school projects, and recruiters can see it. Private for anything with client code or personal data. The README, .gitignore and license checkboxes create a first commit on GitHub. That is fine when you clone afterwards, but it is in the way when you want to push a project that already exists on your laptop.

<!-- slide 25 -->

### Pushing a project you already have

```console
$ git remote add origin https://github.com/samserrien/my-portfolio.git
$ git remote -v
origin  https://github.com/samserrien/my-portfolio.git (fetch)
origin  https://github.com/samserrien/my-portfolio.git (push)
$ git push -u origin main
To https://github.com/samserrien/my-portfolio.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
```

1. Tells your repo where origin is. The url is on the page of the new, empty repo.
2. Checks which remotes this repo knows.
3. The first push. Refresh GitHub and your files are there.

> **Explanation:** -u (or --set-upstream) links your local main to origin/main. You type it once per branch; after that git push and git pull know where to go. If git push answers 'rejected' on the very first push, the repo on GitHub was not empty: you ticked README. Then git pull first, or start over with an empty repo.

<!-- slide 26 -->

### Cloning

```console
$ git clone https://github.com/samserrien/my-portfolio.git
Cloning into 'my-portfolio'...
Receiving objects: 100% (12/12), done.
$ cd my-portfolio
$ git log --oneline
a1c3f80 (HEAD -> main, origin/main) Add contact form
9e0b3a7 Fix footer link
8f3a2c1 Add homepage
```

1. Copies the repo into a new folder with the name of the repo.
2. Git commands only work inside a repo, so step into it.
3. The full history came along, and origin/main shows where GitHub is.

> **Explanation:** The url is under the green Code button on GitHub, with a tab for HTTPS and one for SSH. Pick the one that matches how you logged in. A clone gets the whole history, not just the latest files, and origin is set up for you. With gh it is gh repo clone samserrien/my-portfolio.

<!-- slide 27 -->

### Push is not sync

#### What many people think

- Push makes GitHub equal to my folder
- What I delete disappears from GitHub too
- I pushed last, so my version wins

#### What Git does

- Push adds your new commits after the ones that are already there
- Origin only moves forward, it never goes back
- Origin has commits you do not have? Then Git refuses. Pull first.

> **Explanation:** This misunderstanding is behind almost every rejected push. Origin only moves forward: a push adds commits, it never removes them. Deleting a file locally and pushing does not erase it from the history on GitHub, it adds a commit in which the file is gone.

<!-- slide 28 -->

### Pull before you push

```console
$ git push
! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/samserrien/my-portfolio.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. [...] use 'git pull' before pushing again.
$ git pull
From https://github.com/samserrien/my-portfolio
   a1c3f80..c6c26b4  main       -> origin/main
Merge made by the 'ort' strategy.
$ git push
   c6c26b4..91d4e7b  main -> main
```

Someone pushed while you were working. Git does not overwrite their work, it asks you to bring it in first.

> **Explanation:** git pull is two commands in one: git fetch downloads the new commits of origin, git merge adds them to your branch. When you and your teammate changed different files or different lines, Git merges by itself. Same lines: that is a merge conflict, chapter 5. When Git opens an editor for a merge message, the default text is fine: save and close (in nano, Ctrl+X, then Y).

<!-- slide 29 -->

### A README

*README.md*

```md
# My portfolio

A static site with my school projects.
Live at https://samserrien.github.io/my-portfolio

## Run it locally

npm install
npm run dev
```

The README in the root of a repo is what GitHub shows under the list of files. Three things belong in it: what this is, how to run it, where it is live.

**[Markdown](https://slides.mctantwerp.be/en/general/01-markdown)**: Headings, lists, links and code blocks, the syntax of every README.

> **Explanation:** GitHub renders README.md as the front page of your repo, so it is the first thing a recruiter or a teammate reads. Put in it what the project is, how to run it locally and where it is live. Markdown is the same syntax as in Discord or Obsidian; the deck Markdown in General explains all of it.

<!-- slide 30 -->

### Around a repo on GitHub

| Tab | What you find there |
| --- | --- |
| Code | The files, the README, the branch switcher and the green Code button with the clone url |
| Issues | Bugs, ideas and tasks, each with a number. Chapter 7. |
| Pull requests | Branches that want to be merged, with their review. Chapter 6. |
| Actions | Workflows that run on every push, and their logs. Chapter 10. |
| Settings | Collaborators, Pages, rulesets, secrets. Only for admins. |

Every commit has its own page: click the number of commits above the list of files, then a message.

> **Explanation:** Press the full stop key on any repo page and GitHub opens the repo in github.dev, VS Code in your browser. Handy for a quick fix on a laptop without your setup. Settings is only visible to people with admin rights on the repo; everything about Pages, rulesets and secrets lives there.

<!-- slide 31 -->

### GitHub Desktop

- A ticked file is git add
- Summary and Commit to main is git commit
- Push origin is git push, Fetch origin is git fetch
- Download: [github.com/apps/desktop](https://github.com/apps/desktop)

> **Explanation:** Every GUI is a set of buttons on top of the commands in this deck, so pick whatever is fastest for you; most developers mix the editor for committing with the terminal for when something goes wrong. GitHub Desktop is free, for macOS and Windows, and handy for projects that are not code: design files, video, documentation. It has its own Git inside, but you still want Git in your terminal. Its menu Branch holds New branch, Merge into current branch and Create pull request.

<!-- slide 32 -->

### VS Code

- **Source Control panel**: The branch icon in the sidebar, or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>G</kbd>. It is git status, always open: M is modified, U untracked.
- **Stage and commit**: The + next to a file stages it, the message box and Commit make the commit. Select lines in a diff to stage only those.
- **Sync Changes**: After a commit the button becomes Sync: a pull, then a push.
- **Conflicts**: Accept Current, Accept Incoming and Accept Both above every conflict, and a merge editor for big ones.

Docs: [Source Control in VS Code](https://code.visualstudio.com/docs/sourcecontrol/overview).

> **Explanation:** Sync Changes is a pull followed by a push, so it stops at a conflict like a pull in the terminal would. The Source Control Graph at the bottom of the panel draws your branches like git log --graph. Set git config --global core.editor 'code --wait' to have merge and commit messages open in a VS Code tab.

<!-- slide 33 -->

### You changed index.html, ran git add and git commit. Is the change on GitHub now?

- A. Yes, a commit saves online
- B. No, you still have to push
- C. Only if you are on main
- D. Yes, if gh auth login worked

**Answer:** B. No, you still have to push

A commit only exists in your local repo. git push sends it to origin. Logging in only decides whether that push is allowed, it does not push anything by itself.

> **Explanation:** Committing is cheap and local, so commit often. Push at least at the end of every work session: a commit that only lives on your laptop dies with your laptop.

<!-- slide 34 -->

### Task: Your first repo on GitHub

With a project you already have, or a fresh folder with an index.html. Command line, not a GUI, this time.

1. Install Git, set your name, email and defaultBranch, and log in with gh auth login
2. Turn the project into a repo with a .gitignore and a README that says what it is
3. Make at least three commits with messages that start with a verb
4. Create an empty repo on GitHub, add it as origin and push
5. Done when your three commits show up under Commits on GitHub, with your avatar next to them

> **Explanation:** Stuck on the first push? Read the error line by line: rejected usually means the repo on GitHub was not empty, and Authentication failed means gh auth login or your SSH key is not set up. The slides Logging in with gh and Pushing a project you already have show the commands.

<!-- slide 35 -->

## 5. Branches

> **Explanation:** Branches are what makes Git usable in a team. Even alone they are worth it: an experiment on a branch that fails, you throw away without a trace on main.

<!-- slide 36 -->

**A branch is a separate line of commits.**

You keep working without touching main, and bring it all in at once when it works. Until then, main stays the version you can show.

> **Explanation:** It feels like a copy of your project, because your files change when you switch. Technically Git copies nothing: a branch is a tiny file with the hash of one commit in it, that moves along when you commit. That is why creating a branch takes no time, even in a huge repo.

<!-- slide 37 -->

### Creating and switching

```console
$ git switch -c feature/nav
Switched to a new branch 'feature/nav'
$ git branch
* feature/nav
  main
$ git commit -am "Add nav bar"
[feature/nav 4b7d9e2] Add nav bar
$ git switch main
Switched to branch 'main'
$ git push -u origin feature/nav
```

1. -c creates the branch and switches to it. It starts from where you are.
2. Your local branches. The star marks the one you are on.
3. The commit lands on feature/nav. -a stages every tracked file that changed.
4. Back on main, the nav bar is gone from your folder. It is safe in its commit.
5. Puts the branch on GitHub too. The -u only the first time.

> **Explanation:** Name branches after what they do: feature/contact-form, fix/footer-link. Lowercase, hyphens, no spaces. You will also see git checkout -b in older tutorials; it does the same as git switch -c. git switch exists since Git 2.23 (2019) because checkout did too many unrelated things.

<!-- slide 38 -->

### What a switch does to your folder

- **01 HEAD moves**: HEAD points at the branch you are on. New commits go there and nowhere else.
- **02 Files change**: Git rewrites your folder to the last commit of that branch. Files appear and disappear, and that is normal.
- **03 Uncommitted work travels**: Changes you did not commit stay in your folder and come along to the other branch.

> **Explanation:** Switching with uncommitted changes is allowed as long as they do not clash with the other branch; they simply come along. That surprises people: you think you work on main and the half-done nav bar is still there. Commit or stash before you switch and you never have that confusion. git stash is in chapter 8.

<!-- slide 39 -->

### Merging

```console
$ git switch main
$ git pull
Already up to date.
$ git merge feature/nav
Updating 9e0b3a7..4b7d9e2
Fast-forward
 nav.html | 24 ++++++++++++++++++++++++
$ git push
$ git branch -d feature/nav
Deleted branch feature/nav (was 4b7d9e2).
```

1. Go to the branch that should receive the work.
2. Get the newest main first.
3. Brings the commits of feature/nav into main.
4. Now the merge is on GitHub too.
5. The branch did its job and can go.

> **Explanation:** Always pull main before you merge into it, or you merge into an outdated main and get a rejected push. git branch -d refuses to delete a branch that is not merged yet; that is a safety net, -D overrides it. git push origin --delete removes the branch on GitHub. Fast-forward in the output means main had not moved since you branched off, so Git just slid main forward without an extra commit. Had main moved on, Git would tie both lines together in a merge commit with two parents, and that is also where conflicts come from.

<!-- slide 40 -->

### When Git cannot choose

- **Git merges by itself**: Different files, or different lines in the same file. You do not even notice.
- **A merge conflict**: The same lines changed on both sides, or one side edited a file the other deleted. Git stops and asks you.

```console
$ git merge feature/hero
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
```

> **Explanation:** The most common misunderstanding: working in the same file always gives a conflict. It does not. Git merges line by line, so two people editing different parts of style.css merge without any trouble. A conflict is also never a sign that something broke; Git just refuses to guess which version is right.

<!-- slide 41 -->

### A conflict in your file

*index.html*

```html
<<<<<<< HEAD
<h1>Welcome to my portfolio</h1>
=======
<h1>Hi, I make websites</h1>
>>>>>>> feature/hero
```

1. From here: your version, the branch you are on.
2. The divider between the two sides.
3. Until here: the version that comes in, and where it comes from.

The markers are plain text in your file. Edit it until it is the version you want, and delete all three marker lines.

> **Explanation:** Current change and incoming change are the names VS Code uses for the two sides. Sometimes the right answer is one of the two versions, often it is a mix of both written by hand. Search the whole project for <<<<<<< before you continue: a forgotten marker breaks your HTML or your JavaScript.

<!-- slide 42 -->

### Solving a conflict

```console
$ git status
You have unmerged paths.
Unmerged paths:
       both modified:   index.html
$ code index.html
$ git add index.html
$ git commit
[main 3f9a2d1] Merge branch 'feature/hero'
```

1. Lists every file with a conflict.
2. Fix it in your editor and remove the markers.
3. Marks the conflict as solved.
4. Finishes the merge.

> [!TIP]
> Lost? `git merge --abort` puts everything back as it was before the merge. Nothing of yours is lost.

> **Explanation:** git add on a conflicted file means: I chose, this is the right content. The commit then finishes the merge with a message Git already wrote for you. In VS Code you get buttons above every conflict (Accept Current, Accept Incoming, Accept Both) and a merge editor for bigger ones. GitHub Desktop shows the conflicted files and opens them in your editor.

<!-- slide 43 -->

### You and a teammate both changed the same line of style.css. What happens when you merge?

- A. The last push wins
- B. Git stops, puts both versions in the file and waits for you
- C. Git keeps both lines, one under the other
- D. Your local changes are lost

**Answer:** B. Git stops, puts both versions in the file and waits for you

Git never guesses on the same lines. Both versions end up between conflict markers, nothing is lost, and the merge waits until you edit the file, git add it and commit. Keeping both lines is something you can choose yourself, but Git never does it for you.

> **Explanation:** Fewer conflicts: pull before you start, keep branches short-lived, and split work per file or component. A shared formatter like Prettier also helps, otherwise one person's editor reformats every file the other touches.

<!-- slide 44 -->

## 6. Pull requests

> **Explanation:** In a team you rarely merge into main yourself. You ask for it, someone else reads your code, and checks run automatically. That is a pull request, and it is how almost every company and open source project works.

<!-- slide 45 -->

### Why a pull request

- Someone else reads your code before it is on main, and catches what you no longer see
- GitHub Actions runs your build and tests on the branch, before anything is merged
- The discussion about a choice stays next to the code, not in a chat you never find back
- The owner of a project keeps control: anyone may propose, they decide

> **Explanation:** On GitLab the same thing is called a merge request. Also useful when you work alone: a pull request per feature gives you a page with the diff, the checks and your own notes, and a clean history of what went in when.

<!-- slide 46 -->

### The life of a pull request

`feature/contact` / `main`: add form, style it, PR #12, fix review, merged

You push your branch and open a pull request to main. The review asks for a change, you push a fix to the same branch, and the pull request follows. After the approval it is merged and main has your work.

> **Explanation:** The pull request compares two branches, not two commits. That is why a new push to your branch shows up in the open pull request by itself: you never open a second one to fix review comments. When the PR is merged, the branch is done; delete it, on GitHub there is a button right after the merge.

<!-- slide 47 -->

### Opening one

1. **Push your branch**: git push -u origin feature/contact. GitHub shows a yellow bar with Compare & pull request.
2. **Base and compare**: Base is where it goes (main), compare is your branch. Check that the diff only holds your work.
3. **Title and description**: What changed and why. Fixes #8 closes issue 8 when it is merged.
4. **Reviewers**: Pick someone in the sidebar. They get a notification.

The docs: [Creating a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).

> **Explanation:** A good description says what changed, why, and how to test it. Screenshots for anything visual. Draft pull request (the arrow next to the green button) means: not ready for review, but I want the checks to run or a colleague to have a look. GitHub Desktop has Branch, Create pull request, which opens the same page in your browser.

<!-- slide 48 -->

### A review

**#12 Add contact form** (`feature/contact` → `main`)

*contact.html*

```diff
  <form action="/send" method="post">
+   <input type="email" name="email">
+   <textarea name="message"></textarea>
+   <button>Send</button>
  </form>
```

> **Alex Janssens** (9): This field has no label, so a screen reader says nothing useful. Can you add one, and mark it as required?

> **Explanation:** This pull request is a working mock: type a reply and send it, and the push button adds a commit to the conversation, the way a real one would. A reviewer finishes with one of three choices: Comment (just remarks), Approve (good to merge) or Request changes (not before this is fixed). A resolved thread you mark with Resolve conversation, so the next reader sees what is still open.

<!-- slide 49 -->

### Giving a review

#### Not like this

- "This is wrong."
- Three screens about indentation a formatter would fix
- Approve without reading, because it has to go fast

#### Like this

- "If email is empty, this crashes. Can you catch that?"
- A question instead of a verdict when you are not sure
- A suggested change for small fixes, the author commits it with one click
- Say what is good, too

> **Explanation:** The suggestion button is the icon with a plus and a minus in the toolbar of a review comment. It inserts a suggestion block with the current line, which you then edit. The author sees a Commit suggestion button: one click and it is a commit on their branch. Ideal for typos and one-line fixes.

<!-- slide 50 -->

### Three ways to merge

- **Create a merge commit**: Every commit of the branch stays, plus a merge commit on top. The full story stays visible.
- **Squash and merge**: All commits become one commit on main, with the title of the pull request. Clean, but the steps in between are gone.
- **Rebase and merge**: Your commits are replayed one by one on top of main, without a merge commit. A straight line.

The three options sit behind the arrow next to the green merge button. More in [About merge methods](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github).

> **Explanation:** My preference is a merge commit: later you still see exactly which pull request brought which commits. Squash is popular in teams where branches are full of 'wip' commits, because main only gets one clean commit per pull request. Which ones are allowed, the owner sets in Settings, General, Pull Requests.

<!-- slide 51 -->

### A pull request with a conflict

```console
$ git switch feature/contact
$ git fetch origin
$ git merge origin/main
CONFLICT (content): Merge conflict in index.html
$ git add index.html && git commit
$ git push
```

1. Go to your own branch, not to main.
2. Download what is new on GitHub, without merging yet.
3. Bring the newest main into your branch. The conflict shows up here.
4. Solve it like any conflict.
5. The pull request turns green by itself.

> **Explanation:** GitHub can solve small conflicts in the browser (Resolve conflicts on the pull request page), but only simple ones. Doing it locally means you can run the site before you push. The Update branch button on the pull request does the same merge of main into your branch, when there is no conflict.

<!-- slide 52 -->

### Pull requests with gh

```console
$ gh pr create --fill
https://github.com/samserrien/my-portfolio/pull/12
$ gh pr list
#12  Add contact form  feature/contact
$ gh pr checkout 12
$ gh pr merge 12 --merge --delete-branch
```

1. Opens a pull request for the branch you are on, title and body from your commits.
2. The open pull requests of this repo.
3. Gets the branch of a pull request locally, to run it before you review.
4. Merges with a merge commit and cleans up the branch.

> **Explanation:** gh pr checkout is the one reviewers love: it fetches the branch of a pull request, also from a fork, so you can run the code before you approve it. gh pr create without flags asks the questions one by one. gh browse opens the current repo on github.com. Everything is in the manual at cli.github.com/manual.

<!-- slide 53 -->

## 7. Issues and forks

> **Explanation:** GitHub is also where a project is planned: what is broken, what is next, who does what. Then the other half of collaborating: contributing to a repo that is not yours.

<!-- slide 54 -->

### Issues

- **An issue**: A bug, an idea or a task, with a number like #8. A title, a description in Markdown, and a thread for the discussion.
- **Labels**: bug, enhancement, documentation, good first issue. Filter on them to see what matters now.
- **Assignees**: Who works on it. One person per issue keeps it clear.
- **Milestones**: A group of issues with a due date, like v1.0 or Sprint 3. GitHub shows how much of it is done.

> **Explanation:** An issue is not only for bugs. Ideas, questions and to-dos for yourself work just as well. Labels are free text with a colour; GitHub creates a set like bug, enhancement and good first issue in every new repo. In open source, good first issue is how maintainers say: a newcomer can pick this up.

<!-- slide 55 -->

### Closing an issue with a pull request

*Pull request description*

```md
Adds a label to every field of the
contact form, and marks email as required.

Fixes #8
```

When this pull request is merged into main, issue #8 closes by itself, with a link to the code that fixed it.

The keywords: close, fix and resolve, in any form (closes, fixed, resolves). See [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue).

> **Explanation:** Writing #8 anywhere (a commit, a comment) creates a link, but only the keywords close the issue. They only work when the pull request targets the default branch, usually main. A pull request to develop with Fixes #8 links nothing and closes nothing.

<!-- slide 56 -->

### GitHub Projects

- **Board**: Columns like Todo, In progress and Done, with issues and pull requests as cards.
- **Table**: The same items as a spreadsheet, with fields of your own: priority, estimate, sprint.
- **Roadmap**: The items on a timeline, using a date field or the milestones.

A view of your issues, not a copy: close an issue and its card follows. [About Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects).

> **Explanation:** A project belongs to your account or an organisation, not to one repo, so one board can hold issues from the front end and the back end repo together. Built-in workflows move an item to Done when its issue is closed or its pull request merged. For a group project this replaces a separate Trello board, and the cards are the actual issues.

<!-- slide 57 -->

### Collaborator or fork

#### Collaborator

- The owner invites you, and you can push to the repo itself
- You work on branches and open pull requests inside the same repo
- For your team and your group projects

#### Fork

- A copy of the repo under your own account, which you can change freely
- You push to your fork and open a pull request to the original
- For open source, a class repo, any repo you cannot push to

> **Explanation:** Rule of thumb: your own team, branches in one repo. Someone else's project, a fork. Inviting a collaborator happens under Settings, Collaborators; they get an email and must accept before they can push. A fork remembers where it came from, so GitHub offers to open a pull request to the original.

<!-- slide 58 -->

### Contributing to someone else's repo

1. **Fork**: The Fork button at the top of the repo. You get a copy under your account.
2. **Clone your fork**: Your copy, not the original. Make a branch for your change.
3. **Push to your fork**: git push -u origin fix/typo-readme. Origin is your fork.
4. **Pull request**: GitHub offers Contribute, Open pull request, from your fork to the original.

> [!TIP]
> `gh repo fork owner/repo --clone` forks and clones in one go, and sets up the original as `upstream`.

> **Explanation:** The original repo is called the upstream. Read CONTRIBUTING.md before you start: many projects want an issue first, a certain commit style, or tests. A pull request that ignores those rules usually gets closed without discussion. Small first contributions (a typo in the docs, a broken link) are a good way in.

<!-- slide 59 -->

### Keeping a fork in sync

```console
$ git remote add upstream https://github.com/mctantwerp/class-repo.git
$ git fetch upstream
$ git switch main
$ git merge upstream/main
$ git push
```

1. Once: a second remote that points at the original.
2. Downloads what is new in the original.
3. Sync your main, not a feature branch.
4. Brings the original into your main.
5. And into your fork on GitHub.

> **Explanation:** Your fork does not update itself. While you work, the original moves on, and your pull request conflicts with it. Syncing before you start a new branch prevents most of that. On GitHub, the Sync fork button above the list of files does the same for the fork on GitHub; afterwards you still git pull locally.

<!-- slide 60 -->

### Task: A pull request with a review

Per two, each in your own repo from the first assignment. Branches and the push on the command line, the pull request on github.com.

1. Invite each other as collaborator under Settings, Collaborators
2. Open an issue in your own repo, then a branch and a pull request that fixes it with Fixes #n
3. Review each other's pull request: at least two comments, one of them a suggested change
4. Push a fix for the review on the same branch, approve, merge and delete the branch
5. Done when the issue closed by itself and main has a merge commit of the pull request

> **Explanation:** No classmate at hand? Open the pull request in your own repo and review it yourself the next day, or fork a repo of a classmate and send them a small fix. For the review, use the suggestion button once. Stuck on the merge: the slides Opening one and A pull request with a conflict.

<!-- slide 61 -->

## 8. Undoing things

> **Explanation:** The good news first: almost everything in Git can be undone, as long as you committed it. Git keeps every commit for weeks, even ones no branch points at anymore. Changes you never committed, Git has never seen: delete them and they are gone. That is the real argument for committing small and often.

<!-- slide 62 -->

### Throwing away a change

```console
$ git restore --staged index.html
$ git restore index.html
```

1. Takes the file out of the staging area. Your change stays in the file.
2. Throws away your change, back to the last commit. No undo.

Older tutorials use `git checkout -- index.html` and `git reset HEAD index.html` for the same. [git restore](https://git-scm.com/docs/git-restore) is the clearer name.

> **Explanation:** git restore on a file with uncommitted changes is one of the few commands without a way back: those changes were never in a commit. Read the file name twice. In VS Code this is the curved arrow next to a file (Discard Changes), in GitHub Desktop a right-click, Discard changes.

<!-- slide 63 -->

### Fixing the last commit

```console
$ git commit --amend -m "Add contact form with labels"
$ git add forgotten.css
$ git commit --amend --no-edit
```

1. A typo in your last message? This replaces it.
2. Forgot a file? Stage it first.
3. It joins the last commit, with the same message.

> [!WARNING]
> Only for a commit you have not pushed yet. Amend rewrites history.

> **Explanation:** --amend replaces the last commit with a new one, with a new hash. That is fine as long as nobody has it yet. Amend a commit you already pushed and the next push is rejected, because your history no longer matches origin. Then leave it, and make a new commit instead.

<!-- slide 64 -->

### git stash

```console
$ git stash -u
Saved working directory and index state WIP on feature/nav: 4b7d9e2
$ git switch main
$ git switch feature/nav
$ git stash pop
       modified:   nav.html
Dropped refs/stash@{0}
```

1. Parks your changes, new files included. Your folder is clean.
2. Do the urgent thing on another branch.
3. Come back to where you were.
4. Your changes are back, and the stash is removed.

> **Explanation:** The typical moment: you are halfway into something and an urgent fix comes in on another branch. git stash list shows what is parked; stashes are easy to forget, so do not use them as long-term storage. -u also takes untracked files along, which a plain git stash leaves behind.

<!-- slide 65 -->

### git revert

```console
$ git log --oneline
c6c26b4 (HEAD -> main, origin/main) Add newsletter popup
9e0b3a7 Fix footer link
$ git revert --no-edit c6c26b4
[main 7c1d2e9] Revert "Add newsletter popup"
 1 file changed, 31 deletions(-)
$ git push
```

A new commit that does the opposite of an old one. The old commit stays in the history, so this is the safe undo on a branch others already pulled. Docs: [git revert](https://git-scm.com/docs/git-revert).

> **Explanation:** Revert is the undo for anything that is already pushed, because it only adds to the history. Everyone who pulled keeps a history that matches. Reverting a merge commit needs -m 1 to say which side to keep; look that up in the docs when you need it.

<!-- slide 66 -->

### git reset

| Command | Commits after it | Your changes |
| --- | --- | --- |
| `git reset --soft HEAD~1` | Off the branch | Staged, ready to commit again |
| `git reset HEAD~1` | Off the branch | In your files, not staged (--mixed, the default) |
| `git reset --hard HEAD~1` | Off the branch | **Gone, also everything you did not commit** |

> [!WARNING]
> **Only for commits nobody else has**
>
> --hard is the one command in this deck that destroys work you never committed. Run `git status` first.

> **Explanation:** HEAD~1 means one commit before where you are, HEAD~3 three commits back. Reset moves your branch back; the commits after it are not deleted right away, just no longer on the branch. That is why the reflog on the next slide can bring them back. On a pushed branch, reset plus a force push breaks the repo of everyone who pulled: use revert there.

<!-- slide 67 -->

### git reflog

```console
$ git reset --hard HEAD~2
HEAD is now at 9e0b3a7 Fix footer link
$ git reflog
9e0b3a7 HEAD@{0}: reset: moving to HEAD~2
a1c3f80 HEAD@{1}: commit: Add contact form
c6c26b4 HEAD@{2}: commit: Add newsletter popup
$ git reset --hard a1c3f80
HEAD is now at a1c3f80 Add contact form
```

A diary of every place HEAD has been. Find the hash from before the mistake and reset back to it. Docs: [git reflog](https://git-scm.com/docs/git-reflog).

> **Explanation:** The reflog is local and personal: it is not pushed, and a fresh clone has an empty one. Entries stay for about 90 days by default. Before you delete a repo and clone it again in a panic, look here; nine times out of ten your work is still there.

<!-- slide 68 -->

### Drill: which undo

**1. You just committed, and the message has a typo. You have not pushed.**

- **git commit --amend** ✓
- git revert HEAD
- git reset --hard HEAD~1

Amend replaces the last commit, message included. Nobody has it yet, so rewriting is fine. Revert would add a second commit, reset --hard would throw your work away.

**2. A commit from yesterday broke the site. It is pushed and your team pulled it.**

- git reset --hard and force push
- **git revert of that commit, then push** ✓
- Delete the repo and clone again

Revert adds a commit that undoes it, so everyone's history keeps matching. A reset with a force push rewrites a history your teammates already have, and breaks their next pull.

**3. You staged .env by accident with git add . and did not commit yet.**

- **git restore --staged .env** ✓
- git restore .env
- git rm .env

restore --staged takes it out of the staging area and leaves the file alone. Plain restore does nothing useful for a new file, and git rm would delete it from your disk. Then add .env to .gitignore.

**4. Halfway through a feature, you have to fix a bug on main right now.**

- **git stash, switch, fix, switch back, git stash pop** ✓
- git reset --hard and start the feature again
- Commit everything to main as "wip"

Stash parks the half-done work without a commit. A wip commit on main puts broken code on the branch that has to work. A commit on your feature branch would also be fine.

**5. You ran git reset --hard HEAD~3 and want those three commits back.**

- They are gone for good
- **git reflog, then reset to the hash from before** ✓
- git revert HEAD~3

The commits still exist, no branch points at them anymore. The reflog shows the hash where HEAD was before the reset, and git reset --hard to that hash puts the branch back.

> **Explanation:** The question behind every case is the same: is it committed, and is it pushed? Not committed: restore or stash. Committed but not pushed: amend or reset. Pushed: revert. Read the explanation after every answer, also when you got it right.

<!-- slide 69 -->

## 9. Rebase, tags and rules

> **Explanation:** The semi-advanced part. Rebase keeps your history readable, tags mark versions, and rules on main make sure nobody, you included, skips the pull request.

<!-- slide 70 -->

### Rebase or merge

#### What rebase does

- `git rebase main` on your branch replays your commits on top of the newest main
- A straight line instead of a merge commit, the history reads like a story
- Every replayed commit gets a new hash, so it counts as a new commit
- Conflicts are solved per commit, so you can get several in a row

#### The golden rule

- Never rebase commits others already pulled
- They have the old hashes, you push new ones, and their Git sees the same work twice
- Your own branch that nobody else uses: rebase freely
- Already pushed your own branch? Then `git push --force-with-lease`, never a plain `--force`

> **Explanation:** git pull --rebase does a fetch plus a rebase instead of a merge, for people who dislike the merge commits a pull creates. If a rebase gets messy, git rebase --abort brings you back to before it started. --force-with-lease refuses when someone else pushed to the branch in the meantime, so it is the safe way to force push your own branch after a rebase.

<!-- slide 71 -->

### Interactive rebase

```console
$ git rebase -i HEAD~3
```

*git-rebase-todo*

```bash
pick 4b7d9e2 Add nav bar
fixup 6d1e0a4 typo
squash a1c3f80 Style nav bar
```

1. `pick` keeps the commit as it is.
2. `fixup` melts it into the one above and drops its message.
3. `squash` does the same, but lets you combine the messages.

Three commits become one clean commit. All commands are listed in [git rebase, interactive mode](https://git-scm.com/docs/git-rebase#_interactive_mode).

> **Explanation:** The list opens in your editor, oldest commit on top. Change the word in front of a line, save and close. reword lets you change a message, drop removes a commit, and you can reorder lines to reorder commits. The usual reason: cleaning up wip and typo commits before you open a pull request. Same rule as on the previous slide, only for commits nobody else has.

<!-- slide 72 -->

### Tags and releases

```console
$ git tag -a v1.0.0 -m "First public version"
$ git push origin v1.0.0
 * [new tag]         v1.0.0 -> v1.0.0
$ gh release create v1.0.0 --generate-notes
```

1. A fixed name on the commit you are on. Unlike a branch, it never moves.
2. Tags only go to GitHub when you push them.
3. A release on GitHub, with notes from the merged pull requests.

Versions follow [semver](https://semver.org): MAJOR.MINOR.PATCH. A bug fix goes from 1.0.0 to 1.0.1, a new feature to 1.1.0, and something that breaks existing use to 2.0.0.

> **Explanation:** -a makes an annotated tag, with a message, an author and a date; use that for releases. Tags are not pushed with a normal git push, you push them by name or with --tags. On GitHub, Releases on the right of the repo page turns a tag into a page with notes and downloads; --generate-notes writes them from the merged pull requests.

<!-- slide 73 -->

### Protecting main

Settings, Rules, Rulesets, New branch ruleset. Target the default branch, set Enforcement status to Active, and tick the rules you want. See [About rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets).

- **Require a pull request before merging**: Nobody pushes to main directly. Set the number of required approvals here too.
- **Require status checks to pass**: The merge button waits until your workflow is green.
- **Block force pushes**: Nobody can rewrite the history of main.
- **Restrict deletions**: Nobody can delete main by accident.

> **Explanation:** Rulesets are the newer form of branch protection rules; both still exist, and rulesets can stack and be switched on and off without deleting them. They are free for public repos; a private repo needs GitHub Pro (free for students through the GitHub Student Developer Pack) or a team plan. Leave Bypass list empty if you want the rules to count for yourself too.

<!-- slide 74 -->

### You rebased a branch that your teammate also works on, and pushed with --force. What happens?

- A. Nothing, rebase only changes your local copy
- B. Their next pull runs into duplicate commits and conflicts with their own history
- C. GitHub merges both versions automatically
- D. Their commits move to a new branch

**Answer:** B. Their next pull runs into duplicate commits and conflicts with their own history

After the rebase the commits have new hashes. Your teammate still has the old ones, so Git sees two versions of the same work. Their pull gives conflicts or duplicates, and a careless force push from them can erase your work. Rebase only what nobody else has.

> **Explanation:** When you want a straight history on a shared branch, use git pull --rebase for your own new commits only, or let the pull request do a squash or rebase merge. Both only rewrite commits that were never shared.

<!-- slide 75 -->

## 10. Automation and security

> **Explanation:** Last GitHub chapter: let GitHub do the boring checks for you on every push, publish a site for free, and keep your keys out of public repos.

<!-- slide 76 -->

### GitHub Actions

- **01 A trigger**: Something happens: a push, a pull request, a tag, a schedule, a button.
- **02 A runner**: GitHub starts a clean virtual machine, usually Ubuntu.
- **03 Steps**: Your commands run on it: install, lint, build, test, deploy. Green or red.

A workflow is a YAML file in `.github/workflows/`. Results are under the Actions tab, and next to every commit and pull request as a check. Start with the [quickstart](https://docs.github.com/en/actions/get-started/quickstart).

> **Explanation:** Actions is free for public repos. Private repos get a monthly amount of free minutes, which is plenty for a school project. A runner is a fresh virtual machine per job: nothing from a previous run is on it, which is why every workflow starts with a checkout of your code. The Marketplace on github.com lists thousands of ready-made actions.

<!-- slide 77 -->

### A first workflow

*.github/workflows/ci.yml*

```yaml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run build
```

1. A name, and when it runs: every push and every pull request.
2. One job on a fresh Ubuntu machine.
3. Gets your code on that machine.
4. Installs Node 24 and caches npm packages between runs.
5. Installs and builds. A failing step makes the check red.

> **Explanation:** npm ci installs exactly what is in package-lock.json, and fails when the lock file is out of date, which is what you want on a server. Add a line run: npm run lint if your project has a linter. The version after @ pins the major version of an action: v7 for checkout and setup-node on 2026-10-14, check their READMEs for the newest one. YAML cares about indentation, use spaces, never tabs.

<!-- slide 78 -->

### Secrets in Actions

A workflow that deploys or calls an API needs a key, and that key never goes in the YAML. Store it under Settings, Secrets and variables, Actions, New repository secret.

```yaml
- run: npm run deploy
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}
```

The workflow reads it as an environment variable. Nobody can read the value back on GitHub, not even you. [Using secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).

> **Explanation:** Secrets are not passed to workflows triggered from a fork, so a stranger cannot open a pull request that prints your keys. GitHub masks a secret in the logs, but that is no reason to echo it. For a deploy key or an API token, make one just for the workflow, with as few rights as possible, so you can revoke it without breaking anything else.

<!-- slide 79 -->

### GitHub Pages

- **Deploy from a branch**: Plain HTML and CSS. Settings, Pages, Build and deployment, Source: Deploy from a branch. Pick main and the root or /docs folder, then Save.
- **GitHub Actions**: A project with a build step, like Vite. Source: GitHub Actions, and a workflow builds the site and deploys the output folder.

A minute later the site is live at `username.github.io/repo`, and every push to main updates it. See [Configuring a publishing source](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site) and [Vite on GitHub Pages](https://vite.dev/guide/static-deploy).

**[Deploy your site](https://slides.mctantwerp.be/en/extra-webdev/04-deploy)**: Other hosts, preview deploys and your own domain.

> **Explanation:** Pages hosts static files only: HTML, CSS, JavaScript, images. No PHP, no database, no Laravel. Free for public repos; private repos need GitHub Pro. A Vite project needs base: '/my-portfolio/' in vite.config.js, because a project site lives in a subfolder; without it every asset gives a 404. The Vite docs have a ready-made workflow.

<!-- slide 80 -->

### Dependabot

Reads your package-lock.json and warns you when a package you use has a known vulnerability.

Switch it on under Settings, Advanced Security, Dependabot alerts. Turn on security updates next to it, and Dependabot opens the pull request with the fix.

*.github/dependabot.yml*

```yaml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
```

Docs: [Configuring Dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/configuring-dependabot-alerts).

> **Explanation:** An alert is not an emergency in itself: read the severity and whether the vulnerable code is something your site actually uses. Security updates make the fix as a pull request for you; your workflow then checks whether the update breaks the build before you merge. Version updates in dependabot.yml do the same for every update, not just the security ones.

<!-- slide 81 -->

**A pushed secret is a public secret.**

Deleting the file in a new commit does not help: it is still in the history, on GitHub and in every clone. An API key, a password or a .env that was ever pushed is leaked.

> **Explanation:** Bots scan every public push on GitHub for keys, within minutes. A leaked cloud key is a classic way to wake up to a bill for someone else's crypto mining. Push protection, on by default for your account on public repos, blocks most known key formats before they leave your laptop. Do not click the bypass unless you are sure it is a test value.

<!-- slide 82 -->

### When a secret leaks

1. **Revoke the key**: Right away, at the service that issued it, and make a new one. This is the step that actually stops the damage.
2. **Keep the new one out**: Put it in .env, put .env in .gitignore, and commit a .env.example with empty values.
3. **Clean the history if needed**: git filter-repo --sensitive-data-removal --invert-paths --path .env, then a force push. Read the guide first.

The guide: [Removing sensitive data](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository).

**[Accounts and secrets](https://slides.mctantwerp.be/en/general/07-accounts-and-secrets)**: .env files, password managers and two factor.

> **Explanation:** Rewriting history is the last step, not the first: it is a lot of work, it changes every hash after the commit, and everyone has to clone again. GitHub's own guide uses git filter-repo, a separate tool you install with brew or pip. If rotating the key solves the risk, that is often all you need to do.

<!-- slide 83 -->

### Task: Ship it with Pages and Actions

Your portfolio repo from the first assignment, or any static site or Vite project of your own.

1. Publish the site with GitHub Pages and put the live url in your README
2. Add a workflow that installs and builds on every push and pull request
3. Add a ruleset on main: pull request required, status checks must pass, no force pushes
4. Change something through a branch and a pull request, and merge it once the check is green
5. Done when the change is live, and a direct push to main is rejected

> **Explanation:** The ruleset needs a public repo, or GitHub Pro for a private one. If the workflow stays red, open the failed run under Actions and read the log of the red step: usually it is a missing script in package.json or a lock file that is not committed. The slides A first workflow, GitHub Pages and Protecting main have everything you need.

## Glossary

- **Repository**: A project under version control: your files plus the complete history of every change. Usually shortened to repo. (slide 7)
- **Commit**: A saved snapshot of what you staged, with a message, an author and a date. The history of a repo is a chain of commits. (slide 7)
- **Remote**: A copy of the repo somewhere else, usually on GitHub. The default name for it is origin. Push sends commits there, pull brings them back. (slide 7)
- **Staging area**: What goes into the next commit. git add puts a change there, git commit turns everything in it into one commit. (slide 14)
- **.gitignore**: A file in your repo with one pattern per line for what Git should leave alone, like node_modules/ and .env. It only works for files Git does not track yet. (slide 19)
- **git push**: Sends your local commits to the remote and adds them after what is already there. When the remote has commits you do not have, Git refuses and you pull first. (slide 27)
- **git pull**: Fetches the commits of the remote that you do not have yet and merges them into your branch, in one command. (slide 28)
- **Branch**: A separate line of commits next to main. Technically a name that points at one commit and moves along with every new commit you make on it. (slide 36)
- **Merge conflict**: Both sides changed the same lines in a different way. Git puts both versions in the file between conflict markers and waits until you choose. (slide 41)
- **Pull request**: A request on GitHub to merge one branch into another. Someone reviews the code first, checks run on it, and the discussion stays next to the code. (slide 45)
- **Fork**: A copy of someone else's repo under your own account. You push to your fork and open a pull request to the original, the upstream. (slide 57)
- **git stash**: Parks your uncommitted changes so your folder is clean, for example to switch branches. git stash pop brings them back. (slide 64)
- **git revert**: Makes a new commit that does the opposite of an old one. The old commit stays in the history, so it is safe on a branch that is already pushed. (slide 65)
- **git reset**: Moves your branch back to an earlier commit. --soft keeps the changes staged, --mixed keeps them in your folder, --hard throws them away. Only for commits nobody else has. (slide 66)
- **git reflog**: The diary of where HEAD has been, also after a reset. The place to find the hash of a commit you thought you lost. (slide 67)
- **Rebase**: Replays the commits of your branch on top of another branch. The history becomes a straight line, but every replayed commit gets a new hash, so never rebase what others already pulled. (slide 70)
- **Tag**: A fixed name on one commit, like v1.0.0. Unlike a branch it never moves. On GitHub a release is built on a tag. (slide 72)
- **Ruleset**: Rules for branches in the settings of a repo, like: nobody pushes to main directly, every change needs a pull request with an approval and green checks. (slide 73)
- **Workflow**: A YAML file in .github/workflows that tells GitHub Actions what to run and when, for example a build on every push and pull request. (slide 77)
- **GitHub Pages**: Free static hosting by GitHub, straight from a branch or from a workflow. A project site ends up at username.github.io/repo. (slide 79)

## Links and sources

### In the deck

- [Pro Git](https://git-scm.com/book/en/v2) (slide 6): The free book, chapters 1 to 3 cover most of this deck
- [docs.github.com](https://docs.github.com) (slide 6)
- [Git downloads](https://git-scm.com/downloads) (slide 9)
- [cli.github.com](https://cli.github.com) (slide 11)
- [Connecting to GitHub with SSH](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) (slide 12)
- [Pro Git, Recording changes](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) (slide 14)
- [A collection of .gitignore templates](https://github.com/github/gitignore) (slide 19)
- [New repository](https://github.com/new) (slide 24)
- [GitHub Desktop](https://github.com/apps/desktop) (slide 31)
- [Source Control in VS Code](https://code.visualstudio.com/docs/sourcecontrol/overview) (slide 32)
- [Creating a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) (slide 47)
- [About merge methods on GitHub](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github) (slide 50)
- [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue) (slide 55): The keywords, only on the default branch
- [About Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) (slide 56)
- [git restore](https://git-scm.com/docs/git-restore) (slide 62)
- [git revert](https://git-scm.com/docs/git-revert) (slide 65)
- [git reflog](https://git-scm.com/docs/git-reflog) (slide 67)
- [git rebase, interactive mode](https://git-scm.com/docs/git-rebase#_interactive_mode) (slide 71)
- [Semantic Versioning](https://semver.org) (slide 72)
- [About rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets) (slide 73)
- [GitHub Actions quickstart](https://docs.github.com/en/actions/get-started/quickstart) (slide 76)
- [Using secrets in GitHub Actions](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets) (slide 78): Settings, Secrets and variables, Actions. Not passed to workflows from forks
- [Configuring a publishing source for your GitHub Pages site](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site) (slide 79)
- [Deploying a static site: GitHub Pages](https://vite.dev/guide/static-deploy) (slide 79): The base option and a workflow for a Vite build
- [Configuring Dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/configuring-dependabot-alerts) (slide 80): Settings, Advanced Security
- [Removing sensitive data from a repository](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository) (slide 82): Rotate first, then git filter-repo and GitHub Support

### Sources

- [git config](https://git-scm.com/docs/git-config): init.defaultBranch, pull.rebase, core.editor
- [git switch](https://git-scm.com/docs/git-switch)
- [git reset](https://git-scm.com/docs/git-reset): --soft, --mixed and --hard
- [git stash](https://git-scm.com/docs/git-stash)
- [git rebase](https://git-scm.com/docs/git-rebase): Interactive mode and the todo list
- [Ignoring files](https://docs.github.com/en/get-started/git-basics/ignoring-files)
- [gh auth login](https://cli.github.com/manual/gh_auth_login): Browser flow, protocol choice, SSH key upload
- [GitHub CLI manual](https://cli.github.com/manual/): gh repo create, gh pr create, gh pr checkout, gh release create
- [Generating a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
- [About pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)
- [Incorporating feedback in your pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/incorporating-feedback-in-your-pull-request): Suggested changes and Commit suggestion
- [Syncing a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork): Sync fork, Update branch, gh repo sync and upstream
- [Creating rulesets for a repository](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository): Free for public repositories, private ones need Pro or Team
- [Available rules for rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets)
- [actions/checkout](https://github.com/actions/checkout): v7 on 2026-10-14
- [actions/setup-node](https://github.com/actions/setup-node): v7 on 2026-10-14
- [About push protection](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection)
