Install the tools
Get every tool this deck uses. macOS uses Homebrew; Windows uses winget (built-in).
macOS — Homebrew
Homebrew (brew.sh) is the de-facto package manager for macOS. Install it first, then use it for everything else.
$ brew install gitAll-in-one install
$ # 1. Install Homebrew (one time)$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"$$ # 2. Install everything this deck uses$ brew install git$ brew install --cask visual-studio-code$$ # 3. Verify$ git --version
Tool by tool
Homebrew
requiredThe package manager for macOS — install it first, then brew install everything else.
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew --versionGit
requiredThe version control system itself — everything in this deck runs on it.
$ brew install git
git --versionVS Code
optionalRecommended editor — git config --global core.editor "code --wait" wires it into Git.
$ brew install --cask visual-studio-code
code --versionZsh
optionalA friendly shell — the default on macOS. On Windows, use it inside WSL for the best experience.
$ brew install zsh
zsh --versionVerify your install
Run this — if you see version numbers, you're ready to start the deck.
$ git --version && code --version
What is Git?
Git is a distributed version control system (DVCS) created by Linus Torvalds in 2005. It tracks changes to any set of files, lets many people work in parallel, and keeps a complete history on every clone.
Unlike older centralized systems, every developer has the full repository locally — so most operations are instant and work offline.
| Property | Meaning |
|---|---|
| Distributed | Every clone is a full backup — no single point of failure. |
| Snapshots | Each commit stores the full tree, not just a diff. |
| Branch-first | Branches are cheap pointers — create freely. |
The three areas of Git
Git moves your changes through three areas: the working directory, the staging area, and the repository. Understanding this flow is the key to everything else.
Working Directory
files you edit
git addStaging Area
next commit candidate
git commitRepository (.git)
committed history
pushRemotefetch| Area | What it is | Commands |
|---|---|---|
| Working Directory | The actual files on disk you see and edit. | git status · git diff · git restore <file> |
| Staging Area | A snapshot-in-waiting — what the next commit will capture. | git add <file> · git restore --staged <file> |
| Repository (.git) | The immutable history of commits, stored inside .git/. | git commit -m · git log · git show <hash> |
First-time setup
Do this once per machine — your identity follows every commit.
$ git config --global user.name "Your Name"
Set your name (stamped on every commit)
$ git config --global user.email "you@example.com"
Set your email
$ git config --global init.defaultBranch main
Default branch name for new repos
$ git config --global pull.rebase false
Pull = merge (not rebase) by default
$ git config --global core.editor "code --wait" # or vim/nano
Set the editor Git opens for messages— VS Code waits for you to close the tab before continuing.
$ git config --list
user.name=Your Name user.email=you@example.com init.defaultbranch=main pull.rebase=false core.editor=code --wait
Verify your config
Two ways to start a project — pick based on whether the repo already exists.
| Start a new project (local) | Join an existing project (remote) |
|---|---|
| Creates a fresh .git/ directory in an empty (or existing) folder. Use when starting from scratch — greenfield repos, prototypes, new services. | Downloads the entire history into a new folder. Use for existing repos — every commit, branch, and tag arrives ready to work on. |
| $ git init my-app && cd my-app | $ git clone https://github.com/torvalds/linux.git |
Your practice repo
Every command from here on runs against one real, shared sample repo — not a hypothetical.
One script builds a small sample app locally — a tiny web server with a login feature — with a fixed author and fixed commit dates. That means the hashes and outputs printed in this course are the exact same ones you'll see on your own machine. Each lesson starts with its own checkpoint: running it rebuilds the repo at that lesson's starting state, so you can jump around or recover from any mistake by re-running it.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section status-diff$ cd ~/git-course-lab/app
Build the first practice checkpoint
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --list
See all available checkpoints
Windows: use Git Bash
It's safe to re-run
Know where you are
status and diff are your dashboard — run them constantly.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section status-diff$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
$ git status
On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: app.js 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: README.md Untracked files: (use "git add <file>..." to include in what will be committed) notes.txt
What changed, what's staged
$ git status -s
M README.md M app.js ?? notes.txt
Short form
$ git diff
diff --git a/README.md b/README.md
index 9c23dd4..8c04ae9 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,7 @@ A tiny sample app used to practice Git commands.
## Run
node app.js
+
+## Status
+
+Work in progress.Unstaged changes
$ git diff --staged
diff --git a/app.js b/app.js
index d0abecd..c3d9383 100644
--- a/app.js
+++ b/app.js
@@ -4,7 +4,7 @@ function serverPort() {
function startServer() {
const port = serverPort();
- console.log(`listening on port ${port}`);
+ console.log(`server ready on port ${port}`);
}
startServer();Staged but not committed
$ git diff HEAD
diff --git a/README.md b/README.md
index 9c23dd4..8c04ae9 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,7 @@ A tiny sample app used to practice Git commands.
## Run
node app.js
+
+## Status
+
+Work in progress.
diff --git a/app.js b/app.js
index d0abecd..c3d9383 100644
--- a/app.js
+++ b/app.js
@@ -4,7 +4,7 @@ function serverPort() {
function startServer() {
const port = serverPort();
- console.log(`listening on port ${port}`);
+ console.log(`server ready on port ${port}`);
}
startServer();Everything since last commit
$ git diff main..feature/login
diff --git a/app.js b/app.js
index d0abecd..5bfdc8f 100644
--- a/app.js
+++ b/app.js
@@ -1,5 +1,5 @@
function serverPort() {
- return 3000;
+ return 8080;
}
function startServer() {
diff --git a/login.js b/login.js
new file mode 100644
index 0000000..664b184
--- /dev/null
+++ b/login.js
@@ -0,0 +1,8 @@
+function login(username, password) {
+ if (!username || !password) {
+ return false;
+ }
+ return username.length > 0 && password.length >= 8;
+}
+
+module.exports = { login };
diff --git a/login.test.js b/login.test.js
new file mode 100644
index 0000000..975e36b
--- /dev/null
+++ b/login.test.js
@@ -0,0 +1,13 @@
+const { login } = require("./login");
+
+test("rejects empty credentials", () => {
+ expect(login("", "")).toBe(false);
+});
+
+test("rejects short passwords", () => {
+ expect(login("jane", "short")).toBe(false);
+});
+
+test("accepts valid credentials", () => {
+ expect(login("jane", "longenough")).toBe(true);
+});Compare two branches
What's HEAD?
Legend — git status -s
??Untracked — new, not yet added MModified, not stagedM Modified and stagedA Added / newly stagedD DeletedPro tip
Stage: build the next commit
Move changes from working dir into the staging area. Review the staged diff before sealing it into history.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section stage$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
Why staging exists
$ git add README.md
Stage one file
$ git add .
Stage everything in cwd
$ git add -p
Stage by hunk (interactive)
$ git add -u
Stage modifications + deletions only
$ git restore --staged README.md
Unstage (keep changes)
$ git rm --cached debug.log
rm 'debug.log'
Untrack, keep on disk
Mistake to avoid
Before git add . becomes routine, add a .gitignore file at the repo root — a list of glob patterns Git should never track. Build artifacts, dependencies, and secrets stay out of every commit and off git status.
A minimal .gitignore
It only ignores UNtracked files
Commit: snapshot the staging area
A commit is an immutable checkpoint — write messages your future self can read.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section commit$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
$ git commit -m "docs: add usage section"
[main 43a400f] docs: add usage section 1 file changed, 4 insertions(+)
One-shot message (commits the staged README.md edit)
$ git commit
Opens $EDITOR
$ git commit -am "chore: tweak server ready message"
[main 04f92bb] chore: tweak server ready message 1 file changed, 1 insertion(+), 1 deletion(-)
Add tracked + commit (the still-unstaged app.js edit)
$ git commit --amend --no-edit
[main 4726b09] chore: tweak server ready message Date: Wed Jul 22 11:57:00 2026 -0300 1 file changed, 2 insertions(+), 1 deletion(-)
Fix last commit's content
$ git commit --amend -m "chore: tweak server ready message and add TODO"
[main 779428e] chore: tweak server ready message and add TODO Date: Wed Jul 22 11:57:00 2026 -0300 1 file changed, 2 insertions(+), 1 deletion(-)
Redo last commit message
Your hashes will differ here — and that's fine
Each commit is one immutable snapshot in .git/objects, identified by a 40-char SHA-1 hash of tree + parent + author + message. Tamper-evident by design.
| Rule | Why |
|---|---|
| Imperative mood — “Add login form” not “Added login form” | Matches the tone of Git's own messages (“Merge branch…”). |
| Subject ≤ 50 chars — body wraps at 72 | Fits terminals and git log --oneline cleanly. |
| Subject + blank line + body — explain WHY, not WHAT | The diff already shows what changed. |
Example
Prefix cheat-sheet: feat: fix: docs: refactor: test: chore: perf:
Golden rule
Read history: log, show, reflog
Three lenses on the past — each answers a different question.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section history$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
$ git log --oneline --graph --all --decorate
* 282f51a (HEAD -> main, tag: v1.0) Merge branch 'feature/login' |\ | * 3249f18 (feature/login) test: cover login validation | * 413c1c6 feat: validate login, dev port 8080 | * 1c5ac16 feat: add login form * | c2f1e3b fix: pin server port to 3000 |/ * 2970256 docs: add project README * 414267e chore: scaffold app
Compact visual tree
$ git log -p app.js
commit c2f1e3b9510fd76a1e193c9ce3d4f9788660cc98
Author: Jane Doe <jane@example.com>
Date: Fri Jul 17 09:12:00 2026 -0300
fix: pin server port to 3000
diff --git a/app.js b/app.js
index f7d00fe..d0abecd 100644
--- a/app.js
+++ b/app.js
@@ -1,5 +1,5 @@
function serverPort() {
- return 5000;
+ return 3000;
}
function startServer() {
commit 414267ed7f02a067b43fc1376a8f220d2e4fe5d2
Author: Jane Doe <jane@example.com>
Date: Wed Jul 15 09:00:00 2026 -0300
chore: scaffold app
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..f7d00fe
--- /dev/null
+++ b/app.js
@@ -0,0 +1,10 @@
+function serverPort() {
+ return 5000;
+}
+
+function startServer() {
+ const port = serverPort();
+ console.log(`listening on port ${port}`);
+}
+
+startServer();Patches touching a file— shows every commit that touched app.js, most recent first
$ git log --author="Jane" --since=2026-07-16
commit 282f51a33445856a0899deec5300e444a6e46035
Merge: c2f1e3b 3249f18
Author: Jane Doe <jane@example.com>
Date: Mon Jul 20 14:05:00 2026 -0300
Merge branch 'feature/login'
# Conflicts:
# app.js
commit c2f1e3b9510fd76a1e193c9ce3d4f9788660cc98
Author: Jane Doe <jane@example.com>
Date: Fri Jul 17 09:12:00 2026 -0300
fix: pin server port to 3000
commit 3249f18f37ec59db232be20e4628e73ba39d3021
Author: Jane Doe <jane@example.com>
Date: Thu Jul 16 18:05:00 2026 -0300
test: cover login validationFilter by author & date— an absolute date keeps this example stable — "2 weeks ago" would drift
$ git show 3249f18
commit 3249f18f37ec59db232be20e4628e73ba39d3021
Author: Jane Doe <jane@example.com>
Date: Thu Jul 16 18:05:00 2026 -0300
test: cover login validation
diff --git a/login.test.js b/login.test.js
new file mode 100644
index 0000000..975e36b
--- /dev/null
+++ b/login.test.js
@@ -0,0 +1,13 @@
+const { login } = require("./login");
+
+test("rejects empty credentials", () => {
+ expect(login("", "")).toBe(false);
+});
+
+test("rejects short passwords", () => {
+ expect(login("jane", "short")).toBe(false);
+});
+
+test("accepts valid credentials", () => {
+ expect(login("jane", "longenough")).toBe(true);
+});Inspect one commit
$ git show HEAD~3
commit 414267ed7f02a067b43fc1376a8f220d2e4fe5d2
Author: Jane Doe <jane@example.com>
Date: Wed Jul 15 09:00:00 2026 -0300
chore: scaffold app
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..f7d00fe
--- /dev/null
+++ b/app.js
@@ -0,0 +1,10 @@
+function serverPort() {
+ return 5000;
+}
+
+function startServer() {
+ const port = serverPort();
+ console.log(`listening on port ${port}`);
+}
+
+startServer();Three commits back
$ git reflog
282f51a HEAD@{0}: commit (merge): Merge branch 'feature/login'
c2f1e3b HEAD@{1}: commit: fix: pin server port to 3000
2970256 HEAD@{2}: checkout: moving from feature/login to main
3249f18 HEAD@{3}: commit: test: cover login validation
413c1c6 HEAD@{4}: commit: feat: validate login, dev port 8080
1c5ac16 HEAD@{5}: commit: feat: add login form
2970256 HEAD@{6}: checkout: moving from main to feature/login
2970256 HEAD@{7}: commit: docs: add project README
414267e HEAD@{8}: commit (initial): chore: scaffold appLocal HEAD/branch moves
$ git reflog --date=iso
282f51a HEAD@{2026-07-20 14:05:00 -0300}: commit (merge): Merge branch 'feature/login'
c2f1e3b HEAD@{2026-07-17 09:12:00 -0300}: commit: fix: pin server port to 3000
2970256 HEAD@{2026-07-16 18:10:00 -0300}: checkout: moving from feature/login to main
3249f18 HEAD@{2026-07-16 18:05:00 -0300}: commit: test: cover login validation
413c1c6 HEAD@{2026-07-16 10:15:00 -0300}: commit: feat: validate login, dev port 8080
1c5ac16 HEAD@{2026-07-15 11:40:00 -0300}: commit: feat: add login form
2970256 HEAD@{2026-07-15 11:30:00 -0300}: checkout: moving from main to feature/login
2970256 HEAD@{2026-07-15 09:30:00 -0300}: commit: docs: add project README
414267e HEAD@{2026-07-15 09:00:00 -0300}: commit (initial): chore: scaffold appReflog with timestamps
The daily flow: branch → commit → merge
Exactly what git log --oneline --graph --all draws — commits flow left→right, branches fork and merge back.
| Command | Answers |
|---|---|
| git log | Committed history — what everyone sees after a push. |
| git show <hash> | One specific commit: its message, its diff, its author. |
| git reflog | Your 90-day local safety net — every HEAD move, even resets. Use it to recover “lost” commits. |
Branch & switch
A branch is just a movable pointer. Switching updates your working directory.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section branch-switch$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
A branch is a pointer to a commit. HEAD points to the branch you're on.
$ git branch
feature/login hotfix/auth * main
List branches
$ git branch docs/faq
Create (stay on current)
$ git switch feature/login
Switched to branch 'feature/login'
Move to it
$ git switch -c feature/onboarding
Switched to a new branch 'feature/onboarding'
Create + switch (shortcut)
$ git switch -
Switched to branch 'feature/login'
Back to previous branch
$ git branch -d feature/login
Deleted branch feature/login (was 3249f18).
Delete (safe — feature/login is already merged)
$ git branch -d hotfix/auth
error: the branch 'hotfix/auth' is not fully merged hint: If you are sure you want to delete it, run 'git branch -D hotfix/auth' hint: Disable this message with "git config set advice.forceDeleteBranch false"
Delete an unmerged branch — Git refuses
$ git branch -D hotfix/auth
Deleted branch hotfix/auth (was c390c26).
Force delete
$ git branch -m docs/faq docs/help
Rename
Mental model
Git 2.23+
Merge: combine branches
Two strategies: fast-forward (no new commit) or 3-way merge (creates a merge commit).
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section merge$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
$ git merge docs/quickstart
Updating c2f1e3b..3e65e33 Fast-forward README.md | 5 +++++ 1 file changed, 5 insertions(+)
Fast-forward if possible
$ git merge --no-ff docs/changelog
Merge made by the 'ort' strategy. README.md | 4 ++++ 1 file changed, 4 insertions(+)
Force a merge commit (on another small branch)
$ git merge feature/login
Auto-merging app.js CONFLICT (content): Merge conflict in app.js Automatic merge failed; fix conflicts and then commit the result.
A real conflict: both sides touched app.js
$ git merge --abort
Bail out of a conflict
Fast-forward merge
When main hasn't moved, Git just slides the branch pointer forward. No new commit. Linear history.
3-way merge
When both branches moved, Git creates a merge commit (M) with two parents. Preserves divergence history.
| Step | Action |
|---|---|
| 1 | Edit the file — keep what you want, remove markers |
| 2 | git add <resolved-file> — mark resolved |
| 3 | git commit — finalize the merge |
| — | Escape hatch: git merge --abort to give up and restore the pre-merge state |
function serverPort() {<<<<<<< HEADreturn 3000;=======return 8080;>>>>>>> feature/login}
A conflict as Git leaves it in the file: HEAD is your side, the lines below ======= are theirs. Keep the right code, delete all three markers, then stage and commit.
Stash: park uncommitted work
Save changes for later without committing — a stack of temporary shelves.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section stash$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
Stash is a stack
push → adds to toppop → apply + remove topapply → apply, keepYour practice repo already has one stash waiting on feature/login ("wip: login form") — main starts dirty too (a modified app.js and an untracked notes.txt), so there's real WIP to park.
$ git stash
Saved working directory and index state WIP on main: 282f51a Merge branch 'feature/login'
Stash tracked changes
$ git stash -u
Saved working directory and index state WIP on main: 282f51a Merge branch 'feature/login'
Include untracked files
$ git stash push -m "wip: pagination todo"
Saved working directory and index state On main: wip: pagination todo
Named stash
$ git stash list
stash@{0}: On main: wip: pagination todo
stash@{1}: WIP on main: 282f51a Merge branch 'feature/login'
stash@{2}: WIP on main: 282f51a Merge branch 'feature/login'
stash@{3}: On feature/login: wip: login formSee all stashes
$ git stash pop
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: app.js
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (88761ecbe10e7996aaf7bb3c2235a805a1d13963)Apply + drop top
$ git stash apply stash@{0}
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: app.js Untracked files: (use "git add <file>..." to include in what will be committed) notes.txt scratch.txt no changes added to commit (use "git add" and/or "git commit -a")
Apply, keep stash
$ git stash drop stash@{0}
Dropped stash@{0} (df7b7376e388e5d1348255e7512e17b7df11c068)Discard one
$ git stash clear
Nuke all (careful!)
When to stash
Mental model
Rebase: replay commits on a new base
Rewrites your branch's history as if it started from the latest main.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section rebase$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
This checkpoint's feature/profile branch forked off main before the port fix that later landed on main — so it needs a rebase to catch up. HEAD starts on feature/profile.
$ git log --oneline --graph --all --decorate
* c2f1e3b (main) fix: pin server port to 3000 | * 7d77d96 (HEAD -> feature/profile) fix: trim profile name | * afabffd feat: accept name in profile | * 0ecba70 feat: add profile stub |/ * 2970256 docs: add project README * 414267e chore: scaffold app
feature/profile forked before main's latest commit
$ git rebase main
Successfully rebased and updated refs/heads/feature/profile.
Replay feature onto main tip
$ git log --oneline --graph --all --decorate
* 83bcea6 (HEAD -> feature/profile) fix: trim profile name * 1790ff9 feat: accept name in profile * 7d3d019 feat: add profile stub * c2f1e3b (main) fix: pin server port to 3000 * 2970256 docs: add project README * 414267e chore: scaffold app
feature/profile now sits directly on main — linear history
$ git rebase --continue
After fixing conflicts
$ git rebase --skip
Drop the conflicting commit
$ git rebase --abort
Bail out, restore original
$ git rebase -i HEAD~3
Interactive: squash / reword / reorder / drop
Notice the new hashes
| Aspect | Merge | Rebase |
|---|---|---|
| History | preserves branching | linear |
| New commits | merge commit only | rewrites feature commits |
| Conflict resolution | once, at merge | per commit, during replay |
| When to use | integrating shared work | tidying your local branch |
| Action | Effect |
|---|---|
| pick | keep as-is |
| reword | edit message |
| edit | pause to amend |
| squash | merge into previous |
| fixup | squash, drop message |
| drop | remove commit |
Golden rule
Reset & revert: undo safely
reset moves the branch pointer (local). revert adds an undoing commit (shared).
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section reset-revert$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
| Mode | HEAD | Staging | Working dir |
|---|---|---|---|
| --soft | moves | kept | kept |
| --mixed | moves | reset | kept |
| --hard | moves | reset | reset ⚠ |
| Mode | HEAD | Staging | Working dir | Use |
|---|---|---|---|---|
| --soft | → target | kept | kept | redo commit message |
| --mixed (default) | → target | reset | kept | unstage everything |
| --hard | → target | reset | reset (DESTRUCTIVE) | throw away all uncommitted work |
This checkpoint's log ends with a real mistake: chore: commit debug log by mistake accidentally re-added debug.log after it was already untracked back in the stage lesson.
$ git log --oneline -4
c0c554b chore: commit debug log by mistake 855cefa feat: add logout link 282f51a Merge branch 'feature/login' c2f1e3b fix: pin server port to 3000
Four most recent commits
$ git reset --soft HEAD~1
Undo last commit, keep everything staged
$ git reset HEAD~1
Undo the last commit and unstage its changes, keeping edits on disk
$ git reset --hard HEAD~1
Throw away all uncommitted work — irreversible without reflog
$ git revert --no-edit c0c554b
[main 74d9695] Revert "chore: commit debug log by mistake" 1 file changed, 1 deletion(-) delete mode 100644 debug.log
Undo via new commit (shared-safe)— your revert commit's hash will differ — it's stamped with your real time
$ git restore app.js
Discard unstaged edits to a file (DESTRUCTIVE)
$ git restore --staged README.md
Unstage a file, keep the edits
git revert needs a clean index
git restore <file> is destructive
| git reset <hash> | git revert <hash> | |
|---|---|---|
| Effect | Moves branch pointer backward | Adds a new commit that undoes the target |
| History | Rewrites history | Preserves history |
| Safe on | Local branches only | Shared branches |
Worktrees: multiple checkouts, one repo
Work on two branches at once — without cloning twice or stashing.
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section worktrees$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
$ git worktree add ../app-hotfix hotfix/auth
Preparing worktree (checking out 'hotfix/auth') HEAD is now at c390c26 feat: add logout stub
Create a sibling folder on branch hotfix/auth
$ git worktree list
~/git-course-lab/app 282f51a [main] ~/git-course-lab/app-hotfix c390c26 [hotfix/auth]
List all worktrees— real output shows the full path on disk, not ~ — shortened here for readability
$ git worktree remove ../app-hotfix
Remove a worktree
$ git worktree prune
Clean stale entries
Two folders, one .git object store
~/proj
→ main
shares .git/~/proj-hotfix
→ hotfix/auth
Why worktrees?
Gotcha
Remote: fetch, pull, push
Talk to other repositories — most often the team's shared origin.
Local
your .git
fetchpushpull = fetch + integrate
Remote
origin / upstream
Started with git init? Your repo has no remote yet. Create an empty repo on GitHub, then wire it up and publish — this three-line sequence is the canonical first push.
$ git remote add origin <url>
Name the remote 'origin' and point it at the URL
$ git branch -M main
Make sure the branch is called main
$ git push -u origin main
Push and set main to track origin/main
Where does 'origin' come from?
$ curl -fsSL {{ORIGIN}}/setup.sh | bash -s -- --section remote$ cd ~/git-course-lab/app
Start (or reset) this section's practice repo— made a mess? re-run this any time
This checkpoint's origin is a bare repo right next to yours (../origin.git) — it stands in for a real host so fetch/pull/push all run for real, offline. A teammate already pushed one commit you haven't fetched yet.
$ git remote -v
origin ../origin.git (fetch) origin ../origin.git (push)
List remotes
$ git remote add upstream <url>
Add a second remote
$ git fetch origin
From ../origin 282f51a..2c54a7b main -> origin/main
Download, don't merge
$ git fetch --all --prune
All remotes, drop deleted
$ git status
On branch main Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch) nothing to commit, working tree clean
status now knows you're behind — fetch never merges
$ git pull
Updating 282f51a..2c54a7b Fast-forward README.md | 4 ++++ 1 file changed, 4 insertions(+)
fetch + merge (current)
$ git pull --rebase
fetch + rebase (linear)
$ git push origin main
Upload
$ git push -u origin feature/copyright
To ../origin.git * [new branch] feature/copyright -> feature/copyright branch 'feature/copyright' set up to track 'origin/feature/copyright'.
Set upstream + push a local-only branch
$ git push --force-with-lease
Safe force-push
| Command | Direction | What it does |
|---|---|---|
| git fetch | remote → local | Downloads remote refs and objects. NEVER modifies your work — safe to run any time. |
| git pull | remote → local (integrate) | fetch + integrate. Merges by default; --rebase rebases instead. Always fetch first if unsure. |
| git push | local → remote | Upload local commits to remote. -u wires up tracking on first push. |
Force-push tip
Habits & cheatsheet
Internalize these and the rest is muscle memory.
Five rules → fewer merge headaches.
| # | Habit |
|---|---|
| 1 | Commit small, commit often — one logical change per commit. |
| 2 | Pull before you push — avoid the surprise-merge on push. |
| 3 | Rebase your own branches — keep linear history before sharing. |
| 4 | Never rebase shared branches — rewrite only what's local. |
| 5 | Write messages for next year's you — imperative, ≤50 chars, body explains WHY. |
On a team, those habits run inside one repeating loop: branch → push → open a pull request → get reviewed → merge.
| Step | What you do |
|---|---|
| 1 | Create a feature branch — git switch -c feature/x |
| 2 | Push it and set tracking — git push -u origin feature/x |
| 3 | Open a pull request (PR) on the host — GitHub, GitLab, etc. |
| 4 | Address review feedback — push follow-up commits to the same branch |
| 5 | Merge the PR — then delete the branch |
Tag your releases
Top 15 commands — cheatsheet
Internalize these and the rest is muscle memory.
git status -sshort state overview
git add -pstage by hunk
git commit -m "…" snapshot staged changes
git log --oneline --graph --allvisual history
git switch -c <branch>create + move
git merge <branch>integrate branches
git stashpark uncommitted work
git rebase -i HEAD~3tidy last 3 commits
git reset --soft HEAD~1undo last commit, keep changes
git revert <hash>undo via new commit (shared-safe)
git reflogrecover "lost" commits
git worktree addparallel checkouts
git fetch --prunesync remote refs
git pull --rebaselinear integration
git push -u origin <branch>first push w/ tracking