"An idiot admires complexity, a genius admires simplicity" – Terry Davis
A hands-on workshop covering the fundamentals of git and GitHub. This guide walks through everything from initializing your first repository to collaborating with a team using pull requests. It is designed as a companion to the lecture; read through it, run the commands, and build your understanding by doing.
Pre-requisites: A GitHub account, a terminal, and a willingness to break things.
This guide assumes comfortable knowledge of basic shell commands. You should also have added your SSH key to GitHub before starting.
Check out the html deployed website: README | Playground
- What is git?
- Installing git
- Repositories
- Staging, Committing, and Logging
- Light Plumbing — How git Stores Data
- Config
- SSH Key Setup with GitHub
- Branches
- Merge
- Rebase
- Reset
- Remotes
- GitHub
- Pull Requests
- Gitignore
- Part 2
- Interactive Session
Git is a Version Control System (VCS) with roughly 95% adoption among developers. It lets you keep a history of code changes by author, revert mistakes, collaborate with others, and maintain backups of your work. Every saved snapshot in git is called a commit,a set of changes tied to an author and a date.
Git was created in 2005 by Linus Torvalds (creator of the Linux kernel) after license disputes with BitKeeper. He built the first version in just 5 days. It is currently maintained by Junio Hamano.
Important: GitHub ≠ git. Git is the tool. GitHub is a commercial hosting service that uses git under the hood. We interact with git through the command line (CLI).
Verify your installation:
git versionsudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:git-core/ppa
sudo apt update
sudo apt install gitbrew update
brew install gitUse recent versions of git. Older versions may lack features covered in this workshop.
Git has excellent built-in documentation. You can access it any time with man git. Some useful navigation shortcuts for the pager:
| Key | Action |
|---|---|
q |
Quit |
j / k |
Move one line down / up |
d / u |
Half page down / up |
/<term> |
Search for "term" |
n / N |
Next / previous search result |
g / G |
Top / bottom of document |
A repository (repo) is a directory that contains your project files plus a special .git folder where git stores all of its internal tracking and versioning data.
mkdir research
cd research
git initThat's it. You now have a git repository. The .git directory inside is where all the magic lives — commits, branches, tags, and every object git needs.
Every file in your working directory is in one of three states:
- Untracked — git doesn't know about it yet
- Staged — marked for inclusion in the next commit
- Committed — safely stored in the repo history
Check where things stand at any time:
git statusBefore a change can be committed, it must be staged (added to the index):
git add <filename> # stage a specific file
git restore --staged <filename> # unstage a file
git rm --cached <filename> # remove from staging and stop trackingA commit is a snapshot of the repo at a point in time. Each commit records who made it, when, and what changed.
git commit -m "your message here" # commit with an inline message
git commit --amend # modify the most recent commitWrite clear, descriptive commit messages. A common convention is to prefix with a category, e.g. "A: add contents.md" or "fix: resolve null pointer in parser".
View the commit history:
git log # full log with pager (arrow keys to scroll, q to exit)
git --no-pager log -n 1 # show only the latest commit, no pagerEach entry in the log shows a commit hash — a unique SHA-1 identifier. You can refer to any commit by its first 7 characters.
echo "# Research Project" > contents.md
git status # untracked
git add contents.md # staged
git status # ready to commit
git commit -m "A: add contents.md" # committed
git log # inspect your historyThis staging → committing → logging cycle is roughly 50% of your daily git workflow. The other 40% is collaboration with remotes, and the remaining 10% is fixing things and advanced usage.
Git uses a collection of data structures internally (DAGs, Merkle trees, hash maps, and more), but you don't need to understand all of them. Here's the simplified version.
Every commit gets a unique hash derived from its content, message, author info, timestamp, and parent commit hash. These are SHA-1 hashes and are effectively unique (though collisions are theoretically possible).
git log # find a commit hash
git --no-pager log -n 1 # just the latestAll data in your project is stored inside .git/objects. There are three key object types:
- commit — a snapshot pointing to a tree, with metadata (author, message, parent)
- tree — represents a directory, listing blobs and other trees
- blob — represents a file's contents
You can inspect any object with git's plumbing command:
git cat-file -p <hash> # pretty-print the contents of an objectGit stores entire snapshots per commit, not just diffs. But it's smart about it — it uses compression and file packing, and if a file hasn't changed between commits, it stores only one copy. You can verify this by inspecting blob hashes across commits; unchanged files will have the same hash.
| Path | Purpose |
|---|---|
HEAD |
Pointer to the current branch/commit |
index |
Snapshot of the staging area |
objects/ |
All versioned data (commits, trees, blobs) |
refs/heads/ |
Branch tip references |
config |
Local repository settings |
hooks/ |
Scripts that run on events (e.g. pre-commit) |
logs/ |
History of ref updates |
COMMIT_EDITMSG |
Last commit message |
description |
Repo description (used by some tools) |
Git stores author information with every commit, so it needs to know who you are.
git config set --global user.name "Your Name"
git config set --global user.email "your.email@example.com"git config list --local # list local config
git config list --global # list global config
cat ~/.gitconfig # view the global config file directly
git config get <section>.<key> # get a single value
git config unset <section>.<key> # remove a value
git config remove-section <section> # remove an entire sectionGit config has four levels, each overriding the one above:
- system (
/etc/gitconfig) — all users on the machine - global (
~/.gitconfig) — all repos for the current user - local (
.git/config) — the specific repo - worktree (
.git/config.worktree) — a specific worktree
The lowest (most specific) level wins when there's a conflict.
SSH keys let you authenticate with GitHub securely without entering credentials every time.
ssh-keygen -t ed25519 -C "your.email@example.com"Press Enter for the default location. Set a passphrase (recommended).
cat ~/.ssh/id_ed25519.pub # copy this outputGo to GitHub → Settings → SSH and GPG Keys and paste it as both an Authentication and Signing key.
git config set --global user.signingkey ~/.ssh/id_ed25519.pub
git config set --global gpg.format ssh
git config set --global commit.gpgsign true
git config set --global init.defaultbranch main
git config set --global core.editor <your-editor> # e.g. vim, nano, code
git config set --global pull.rebase trueIf you're on WSL/Linux and get repeated passphrase prompts, you may need to configure ssh-agent. See GitHub's SSH agent guide for details.
A branch is just a named pointer to a specific commit. Branches are cheap — creating one doesn't copy any files.
git branch # local branches
git branch -r # remote branches
git branch -a # all branchesgit branch <name> # create a new branch
git switch <name> # switch to an existing branch
git switch -c <name> # create AND switch in one step
git switch -c <name> <hash> # branch from a specific commitPrefer git switch over git checkout for branch operations. switch is safer and more explicit about intent.
Git defaults to master, GitHub defaults to main. Rename if needed:
git branch -m <oldname> <newname>When you commit on a branch, only that branch moves forward. Other branches are unaffected.
git switch -c add_classics
echo "title,director,year" > classics.csv
git add classics.csv
git commit -m "D: added classics.csv"
git log
```**[Playground](playground.html)**
### How branches are stored
Branch tips are stored as simple text files in `.git/refs/heads/`, each containing a single commit hash. A "ref" is a pointer to a commit — all branches are refs, but not all refs are branches (tags are refs too).
```bash
git --no-pager log --decorate=full --oneline # see full ref names
find .git/refs/heads -type f -exec cat {} \; # peek at what's storedWhen you're done working on a branch, you typically merge it back into main.
When two branches have diverged (both have commits the other doesn't), git finds a common ancestor (the merge base), replays changes from both branches, and records the result in a new merge commit with two parents.
git switch main # switch to the branch you're merging INTO
git merge <feature-branch> # merge the feature branch in
git log --oneline --decorate --graph --parents # visualize the resultIf the base branch has not diverged (no new commits since the feature branched off), git simply moves the pointer forward. No merge commit is created.
# 1. Update main with a new commit
git switch main
# (edit contents.md)
git add contents.md
git commit -m "E: update contents"
# 2. Merge your feature branch
git merge add_classics -m "F: merge add_classics"
git log --oneline --graph --all
# 3. Clean up
git branch -d add_classics
# 4. Try a fast-forward merge
git switch -c update_titles
# (edit titles.md)
git add titles.md
git commit -m "G: update titles"
git switch main
git merge update_titles # fast-forward, no merge commit
git branch -d update_titlesRebase is the most debated topic in git. Many developers misuse or avoid it because they don't understand it. Used correctly, it keeps your history clean and linear.
Instead of creating a merge commit, rebase replays your branch's commits on top of another branch:
- Identifies the latest commit on the target branch (e.g.
main) - Replays each of your branch's commits onto that new base
- Updates your branch pointer to the end of the replayed commits
git switch <your-branch>
git rebase mainThe result: a linear history that looks as if you branched off the latest main, even if main had new commits since you branched.
| Merge | Rebase | |
|---|---|---|
| History | Preserves true branching history | Creates linear history |
| Merge commits | Creates them | Avoids them |
| Readability | Cluttered on busy projects | Clean, easy to follow |
| Safety | Safe on any branch | Never rebase public/shared branches |
Never rebase a branch that other people have checked out. If someone else is working on the same branch, rewriting its history will cause headaches. Only rebase your own local branches.
# 1. Create a branch from an older commit
git switch -c update_dune <commit-hash> # branch off an earlier point
# 2. Add some commits
# (edit quotes/dune.md)
git add quotes/dune.md
git commit -m "H: update dune quotes"
# (edit again)
git add quotes/dune.md
git commit -m "I: more dune quotes"
# 3. Rebase onto main
git rebase main
git log --oneline --graph --all # observe the clean linear historyReset is how you undo things in git. It moves the branch pointer (and HEAD) backwards.
git reset --soft <commit-hash>Your files are untouched. The changes from the undone commits remain staged, ready to be re-committed differently.
git reset <commit-hash>Your files are untouched, but changes are unstaged. You'll need to git add again before committing.
git reset --hard <commit-hash>Dangerous. This discards all changes — committed, staged, and in your working directory. If you delete a file and then reset --hard past the commit that added it, that file is gone.
| Mode | Moves HEAD | Unstages | Removes file changes |
|---|---|---|---|
--soft |
✓ | ✗ | ✗ |
--mixed |
✓ | ✓ | ✗ |
--hard |
✓ | ✓ | ✓ |
A remote is a copy of the repository hosted elsewhere (typically on GitHub). The convention is to call the primary remote origin.
git remote add origin <url>
git remote get-url origin # verifygit fetch origin # download objects and refs, doesn't change your filesAfter fetching, remote branches are available as origin/<branch> but aren't merged into your local branches yet.
git merge origin/main # merge fetched data into your current branchgit push origin main # push main to remote
git push origin <local>:<remote> # push to a differently named remote branch
git push origin :<remote-branch> # delete a remote branchgit pull origin main # equivalent to fetch + merge
git pull origin main --rebase # fetch + rebase instead of merge
git pull origin main --no-rebase # explicitly merge (if your default is rebase)git clone git@github.com:<username>/<repo>.gitGitHub is the most popular git hosting service (alternatives include GitLab, Codeberg, Gitea, and self-hosting).
It provides cloud backup, collaboration tools, and a public portfolio for your work. GitHub also has a CLI (gh) which is quite useful.
- Create a new repository on GitHub (leave it blank)
- In your local repo, add it as a remote:
git remote add origin git@github.com:<username>/research.git
git ls-remote # verify the connection- Push your work:
git push origin maingit ls-remote # list remote refs
git ls-files # list files in the index
git ls-tree <tree-hash> # list contents of a tree object
git remote show origin # detailed remote infoIf everyone pushes directly to main, you'll end up with a broken codebase. Pull requests (PRs) are GitHub's mechanism for proposing, reviewing, and merging changes.
- Create a feature branch and make your changes:
git switch -c add_classics
# (make changes)
git add .
git commit -m "L: add classic films"- Push the branch to GitHub:
git push origin add_classics- On GitHub: Pull Requests → New Pull Request
- Set base branch to
main, compare branch toadd_classics - Create the pull request — describe your changes
- A teammate reviews and approves
- Merge the PR on GitHub
- Clean up:
# Delete the remote branch on GitHub (or use the GitHub UI)
git switch main
git pull origin main # get the merged changes
git branch -d add_classics # delete local branchA .gitignore file tells git which files and directories to ignore. You should always commit your .gitignore.
- Generated files — compiled code, build output,
.htmlfrom pandoc - Dependencies —
node_modules/,venv/, packages - Environment-specific files — editor configs, OS files
- Sensitive data — passwords, API keys,
.envfiles
# Ignore a directory anywhere in the tree
node_modules
# Ignore by extension
*.txt
# Rooted pattern — only matches at .gitignore level
/main.c
# Negation — keep a specific file that would otherwise be ignored
!keep.txt
# Ignore directory contents except one file
temp/*
!temp/instructions.mdYou can place .gitignore files in subdirectories. Patterns in deeper .gitignore files only apply to that subtree.
# Create some files we don't want tracked
mkdir secure
echo "root: 12345" > secure/passwords.txt
echo "# Lab Group" > labgroup.md
# Create .gitignore
touch .gitignore
# (add patterns: secure/, *.html, etc.)
git add .gitignore labgroup.md
git commit -m "M: add gitignore and lab group"If you have Markdown files in your project, you can use Pandoc to convert them to HTML (or many other formats). This is a nice way to preview docs:
# Install
sudo apt install pandoc # Linux/WSL
brew install pandoc # macOS
# Convert
pandoc labgroup.md -o labgroup.html
# Open
xdg-open labgroup.html # Linux
open labgroup.html # macOS
explorer.exe labgroup.html # WSLSince the generated .html is a build artifact, add *.html to your .gitignore.
- Fork → Forking a repo and contributing
- Reflog → Reference Log for lost commits
- Merge Conflicts → Why they arise and how to resolve them
- Rebase Conflicts → Why they arise and keep history clean
- Squash → How to squash commits, usually asked
- Stash → Work on multiple things without branching
- Revert → Scalpel git reset, safely undo changes
- Cherry Pick → Selectively move changes from branch to another
- Bisect → Find bugs faster instead of through git history
- Worktrees → Parallel development instead of old branches
- Tags → Tags to version and release code
When both branches modified the same part of the same file, git can't auto-resolve. You'll see conflict markers in the file:
<<<<<<< HEAD
your changes
=======
their changes
>>>>>>> feature-branch
Edit the file to resolve the conflict, remove the markers:
git diff # useful command for merge conflicts to see what is conflictingthen:
git add <resolved-file>
git merge --continueIf you just want to accept all changes from one side:
git checkout --ours <file> # keep the current branch's version
git checkout --theirs <file> # keep the incoming branch's versionHead over to the Playground to start practicing. There are challenges, merge conflicts, and a sandbox to break things in.
| Command | What it does |
|---|---|
git init |
Initialize a new repo |
git status |
Show working tree status |
git add <file> |
Stage changes |
git commit -m "msg" |
Commit staged changes |
git log --oneline --graph --all |
Visualize commit history |
git branch <name> |
Create a branch |
git switch <name> |
Switch branches |
git switch -c <name> |
Create and switch |
git merge <branch> |
Merge a branch into current |
git rebase <branch> |
Rebase current onto branch |
git reset --soft HEAD~ |
Undo last commit, keep staged |
git reset --hard HEAD~ |
Undo last commit, discard all |
git remote add origin <url> |
Add a remote |
git fetch origin |
Download remote data |
git pull origin main |
Fetch and merge |
git push origin main |
Push to remote |
git clone <url> |
Clone a remote repo |
git cat-file -p <hash> |
Inspect an object |
Workshop by Hans-Kristian Erik Östlund — March 2026