Table of Contents
1. What Is Git? 2. Install & Setup 3. How Git Works Under the Hood 4. The Three Areas 5. Basic Workflow 6. Branches & Pointers 7. Merging 8. Reading & Navigating History 9. Undoing Things (Reset, Revert, Restore) 10. Rebase 11. Cherry-Pick & Squash 12. Stashing 13. Remotes & Collaboration 14. GitHub CLI (gh) & GitLab CLI (glab) 15. Advanced Git 16. Practice Quiz1. What Is Git?
Git is a distributed version control system. It tracks every change you make to your code, lets you go back to any point in time, and lets multiple people work on the same project without destroying each other's work.
Created by Linus Torvalds in 2005 (yes, the same person who made Linux) because the existing tools were too slow for the Linux kernel's massive codebase.
Why Git Matters
- Time travel -- go back to any previous version of your code
- Parallel work -- branches let you work on features without breaking main
- Collaboration -- multiple developers can work on the same repo
- Safety net -- almost nothing in Git is truly lost (reflog saves you)
- Industry standard -- every company, every open-source project uses Git
Git is the version control tool that runs on your machine. GitHub (and GitLab, Bitbucket) are hosting platforms that store Git repositories online. You can use Git without GitHub. GitHub just makes collaboration easier.
The Key Insight: Snapshots, Not Diffs
Most people think Git stores "changes" (diffs). It doesn't. Git stores snapshots of your entire project at each commit. Every commit is a complete picture of every file at that moment.
Other VCS (SVN, etc.): Store CHANGES between versions v1 → [+3 lines] → v2 → [-1 line, +5 lines] → v3 Git: Stores SNAPSHOTS of the entire project Commit A = snapshot of all files at time A Commit B = snapshot of all files at time B Commit C = snapshot of all files at time C (If a file didn't change, Git just stores a pointer to the previous version -- so it's still space-efficient)
This is why Git is so fast. To show you "version 3" of your project, it doesn't have to replay changes 1→2→3. It just loads snapshot 3 directly.
2. Install & Setup
Installing Git
Linux (Debian/Ubuntu/Pop!_OS)
# Git comes pre-installed on most distros, but just in case:
sudo apt update
sudo apt install git
# Verify installation
git --version
# git version 2.43.0
macOS
# Git comes with Xcode Command Line Tools:
xcode-select --install
# Or install via Homebrew:
brew install git
# Verify
git --version
First-Time Configuration
Git needs to know who you are. This info gets attached to every commit you make.
Bash
# Set your identity (REQUIRED -- do this first)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Set default branch name to "main" (instead of "master")
git config --global init.defaultBranch main
# Set your default editor (for commit messages, rebase, etc.)
git config --global core.editor "vim" # or nano, or code --wait
# Enable colored output
git config --global color.ui auto
# View all your settings
git config --list
--global applies to all repos for your user (~/.gitconfig). --local (default) applies only to the current repo (.git/config). --system applies to every user on the machine (/etc/gitconfig). Local overrides global, global overrides system.
SSH Key Setup (Stop Typing Your Password)
SSH keys let you push/pull without entering your password every time. You create a key pair: a private key (stays on your machine, NEVER share) and a public key (give to GitHub/GitLab).
Bash
# 1. Generate an SSH key pair
ssh-keygen -t ed25519 -C "your.email@example.com"
# Press Enter for default location (~/.ssh/id_ed25519)
# Enter a passphrase (recommended) or leave empty
# 2. Start the SSH agent
eval "$(ssh-agent -s)"
# 3. Add your private key to the agent
ssh-add ~/.ssh/id_ed25519
# 4. Copy your PUBLIC key
cat ~/.ssh/id_ed25519.pub
# Copy the output
# 5. Go to GitHub → Settings → SSH Keys → New SSH Key
# Paste the public key. Done.
# 6. Test it
ssh -T git@github.com
# "Hi username! You've successfully authenticated..."
Installing GitHub CLI (gh)
Bash
# Linux (Debian/Ubuntu/Pop!_OS)
sudo apt install gh
# Or via Homebrew (macOS/Linux)
brew install gh
# Authenticate
gh auth login
# Follow the prompts -- choose SSH, browser auth
# Verify
gh auth status
Installing GitLab CLI (glab)
Bash
# Via Homebrew (macOS/Linux)
brew install glab
# Or download from releases
# https://gitlab.com/gitlab-org/cli/-/releases
# Authenticate
glab auth login
# Choose your GitLab instance, create a personal access token
# Verify
glab auth status
Useful Aliases
Bash
# Create short aliases for common commands
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.lg "log --oneline --graph --all --decorate"
# Now you can use:
git st # instead of git status
git lg # beautiful commit graph
3. How Git Works Under the Hood
Understanding Git's internals turns you from someone who memorizes commands into someone who knows what's happening. Everything lives in the .git directory.
The .git Directory
Bash
my-project/
├── .git/ # ALL of Git lives here
│ ├── objects/ # All commits, trees, blobs (the actual data)
│ ├── refs/ # Branch and tag pointers
│ │ ├── heads/ # Local branches (main, feature-x, etc.)
│ │ ├── remotes/ # Remote-tracking branches
│ │ └── tags/ # Tag references
│ ├── HEAD # Points to current branch (or commit)
│ ├── config # Repo-specific configuration
│ ├── index # The staging area (binary file)
│ └── logs/ # Reflog -- history of HEAD movements
├── src/
├── README.md
└── ...
If you delete the .git folder, you lose ALL history. Your files stay, but every commit, branch, and remote is gone. Treat .git as sacred.
Git's Four Object Types
Everything Git stores is one of four types of objects, identified by a SHA-1 hash (40-character hex string):
| Object | What It Stores | Analogy |
|---|---|---|
| Blob | File contents (just the raw data, not the filename) | A single file |
| Tree | A directory listing -- maps filenames to blobs and other trees | A folder |
| Commit | A snapshot -- points to a tree + metadata (author, message, parent commit) | A save point |
| Tag | A named pointer to a commit (annotated tags have extra metadata) | A bookmark |
How a commit is structured: commit abc123... ├── tree def456... (root directory snapshot) │ ├── blob 111aaa... → README.md │ ├── blob 222bbb... → index.html │ └── tree 333ccc... → src/ │ ├── blob 444ddd... → app.js │ └── blob 555eee... → style.css ├── parent 789xyz... (previous commit) ├── author Sean <sean@...> └── message "Add login page"
SHA-1 Hashes: Git's Content Addressing
Every object in Git is identified by a SHA-1 hash of its contents. This means:
- If two files have identical content, they share the same blob (deduplication)
- If a commit's hash is
abc123, you can always refer to it by that hash - You only need the first 7-8 characters (Git auto-resolves:
abc123f) - It's impossible to change history without changing hashes -- Git is tamper-proof
Bash
# See the raw content of any Git object
git cat-file -p HEAD # Show current commit's data
git cat-file -p HEAD:README.md # Show a file at HEAD
git cat-file -t abc123f # Show type: commit, tree, blob, tag
4. The Three Areas
This is the single most important concept in Git. Every file lives in one of three areas:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ WORKING │ │ STAGING AREA │ │ REPOSITORY │ │ DIRECTORY │ │ (Index) │ │ (.git) │ │ │ │ │ │ │ │ Your actual │ │ Files ready │ │ Permanent │ │ files on disk │ ───► │ to be committed │ ───► │ snapshots │ │ │ │ │ │ (commits) │ │ │ git │ │ git │ │ │ │ add │ │commit│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘
Working Directory
Your actual files on disk. When you edit a file in your editor, you're changing the working directory. Git sees it as "modified" but won't track the change until you stage it.
Staging Area (Index)
A holding area for changes you want in your next commit. git add moves changes here. Think of it as "I want to include this change in my next commit". You can stage some files and leave others unstaged -- this gives you precise control over what goes into each commit.
Repository (.git)
The permanent history. When you git commit, everything in the staging area becomes a new snapshot in the repository. Commits are (almost) permanent.
You changed 3 files: auth.js, style.css, debug-notes.txt You only want to commit the auth and CSS changes: git add auth.js style.css ← stage only these two git commit -m "Add login styling" debug-notes.txt stays as an unstaged change in your working directory. It won't be in the commit.
File States
| State | Meaning | Where |
|---|---|---|
| Untracked | New file Git has never seen | Working directory |
| Modified | Tracked file that has changed | Working directory |
| Staged | Change marked for next commit | Staging area |
| Committed | Safely stored in repository | .git |
5. Basic Workflow
Creating a Repository
Bash
# Option 1: Start a new project
mkdir my-project
cd my-project
git init
# Creates .git directory. You now have a repo with no commits.
# Option 2: Clone an existing repo
git clone git@github.com:username/repo.git
# Downloads entire repo + history. Sets up remote "origin" automatically.
# Clone into a specific folder name
git clone git@github.com:username/repo.git my-folder
The Core Loop
Bash
# 1. Check what's changed
git status
# 2. See the actual changes (diff)
git diff # Unstaged changes
git diff --staged # Staged changes (what will be committed)
# 3. Stage changes
git add filename.js # Stage a specific file
git add src/ # Stage an entire directory
git add -p # Stage interactively (pick specific hunks)
# 4. Commit
git commit -m "Add user authentication"
# 5. Push to remote
git push
git add . stages EVERYTHING. This can accidentally include .env files, node_modules, debug logs, or other junk. Prefer staging specific files by name, or use git add -p to review each change before staging.
Writing Good Commit Messages
Bash
# Short message for small changes:
git commit -m "Fix null pointer in user login"
# Multi-line message (opens your editor):
git commit
# In the editor:
# Line 1: Short summary (50 chars max, imperative mood)
# Line 2: blank
# Line 3+: Detailed explanation of WHY (not what)
Use imperative mood: "Add feature" not "Added feature". Think of it as completing the sentence: "If applied, this commit will add feature." Keep the first line under 50 characters. Explain why in the body, not what (the diff shows what).
.gitignore
Tell Git which files to ignore. Create a .gitignore file in your repo root:
.gitignore
# Dependencies
node_modules/
venv/
__pycache__/
# Environment variables (NEVER commit these)
.env
.env.local
# Build output
dist/
build/
# IDE files
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
6. Branches & Pointers
This is where Git's elegance shows. Once you understand this model, everything else makes sense.
What Is a Branch?
A branch is just a pointer (a 40-byte text file) that points to a commit. That's it. Creating a branch is instant because Git just creates a tiny file containing a commit hash.
A branch is just a pointer to a commit: .git/refs/heads/main → contains: a1b2c3d .git/refs/heads/feature-x → contains: e4f5g6h That's literally all a branch is. A text file with a hash.
HEAD: Where You Are Now
HEAD is a special pointer that tells Git "which branch (or commit) you're currently on." It usually points to a branch name, not directly to a commit.
Normal state (HEAD points to a branch): HEAD → main → commit C .git/HEAD contains: "ref: refs/heads/main" Detached HEAD (HEAD points directly to a commit): HEAD → commit B (not attached to any branch) .git/HEAD contains: "a1b2c3d4e5f6..."
Visual: How Branches Work
Let's trace through creating a branch and making commits:
Step 1: You have 3 commits on main
═══════════════════════════════════
A ← B ← C
↑
main
↑
HEAD
(Each commit points BACK to its parent.
"main" points to the latest commit C.
HEAD points to main.)
Step 2: Create a new branch "feature"
══════════════════════════════════════
$ git branch feature
A ← B ← C
↑
main
feature
↑
HEAD (still on main)
(Both branches point to the same commit.
HEAD is still on main.)
Step 3: Switch to the feature branch
═════════════════════════════════════
$ git checkout feature
(or: git switch feature)
A ← B ← C
↑
main
feature
↑
HEAD (now on feature)
Step 4: Make a commit on feature
════════════════════════════════
A ← B ← C ← D
↑ ↑
main feature
↑
HEAD
(feature moved forward. main stayed put.)
Step 5: Switch back to main and commit
═══════════════════════════════════════
$ git checkout main
(edit files, commit)
← D
/ ↑
A ← B ← C feature
\
← E
↑
main
↑
HEAD
(History has DIVERGED. This is normal.
main and feature have different commits.)
Git's history is a Directed Acyclic Graph (DAG). Each commit points back to its parent(s). Branches are just movable pointers into this graph. When you merge, a commit gets two parents. The graph never has cycles -- you can't be your own ancestor.
Branch Commands
Bash
# List branches
git branch # Local branches
git branch -a # All branches (local + remote)
git branch -v # Show last commit on each branch
# Create a branch
git branch feature-x # Create but don't switch
git checkout -b feature-x # Create AND switch (classic)
git switch -c feature-x # Create AND switch (modern)
# Switch branches
git checkout main # Classic
git switch main # Modern (preferred)
# Rename a branch
git branch -m old-name new-name # Rename any branch
git branch -m new-name # Rename current branch
# Delete a branch
git branch -d feature-x # Safe delete (only if merged)
git branch -D feature-x # Force delete (even if not merged)
If you git checkout abc123 (a commit hash instead of a branch name), you enter "detached HEAD" state. Any commits you make here will be orphaned when you switch away -- they're not on any branch. If you want to keep them, create a branch first: git checkout -b my-branch.
7. Merging
Merging combines the work from two branches. Git is smart about this -- it can usually figure out how to combine changes automatically.
Fast-Forward Merge
When the target branch hasn't diverged (no new commits since the branch was created), Git just moves the pointer forward. No new commit needed.
Before (main has no new commits since feature branched off):
A ← B ← C ← D ← E
↑ ↑
main feature
$ git checkout main
$ git merge feature
After (fast-forward -- main just moved forward):
A ← B ← C ← D ← E
↑
main
feature
No merge commit created. History is linear.
Three-Way Merge
When both branches have new commits (history has diverged), Git creates a merge commit with two parents.
Before (both branches have new commits):
← D ← E
/ ↑
A ← B ← C feature
\
← F ← G
↑
main
$ git checkout main
$ git merge feature
After (merge commit M has two parents):
← D ← E ──┐
/ ↑ \
A ← B ← C feature \
\ ↓
← F ← G ← ← ← M
↑
main
M is the merge commit. It has TWO parents: G and E.
Merge Conflicts
When both branches changed the same lines in the same file, Git can't auto-merge. You get a conflict:
Bash
$ git merge feature
# CONFLICT (content): Merge conflict in src/auth.js
# Automatic merge failed; fix conflicts and then commit.
Git marks the conflicting sections in the file:
src/auth.js (with conflict markers)
function login(user) {
<<<<<<< HEAD
return authenticate(user.email, user.password);
=======
return verifyCredentials(user.username, user.token);
>>>>>>> feature
}
Reading conflict markers:
<<<<<<< HEAD
(your current branch's version)
=======
(the incoming branch's version)
>>>>>>> feature
To resolve:
1. Edit the file -- keep what you want, remove the markers
2. git add src/auth.js (mark as resolved)
3. git commit (finish the merge)
Bash
# During a conflict, useful commands:
git status # Shows which files have conflicts
git diff # Shows the conflict details
git merge --abort # Abort the merge entirely, go back to before
# After manually fixing conflicts:
git add src/auth.js # Mark as resolved
git commit # Complete the merge
8. Reading & Navigating History
git log -- Your Time Machine
Bash
# Basic log
git log
# One-line compact view
git log --oneline
# Beautiful graph of ALL branches
git log --oneline --graph --all --decorate
# Show the last 5 commits
git log -5
# Log for a specific file
git log --oneline -- src/auth.js
# Search commits by message
git log --grep="fix login"
# Search commits by code change (pickaxe)
git log -S "functionName" # Find when a string was added/removed
# Show who changed each line (blame)
git blame src/auth.js
# Show what a specific commit changed
git show abc123f
* e4f5g6h (HEAD -> main) Merge feature into main |\ | * c3d4e5f (feature) Add user dashboard | * a1b2c3d Add auth middleware |/ * 9f8e7d6 Initial setup * 1a2b3c4 First commit
Referring to Commits
Git gives you many ways to refer to commits without typing full hashes:
| Syntax | Meaning | Example |
|---|---|---|
HEAD | Current commit | git show HEAD |
HEAD~1 | One commit before HEAD | git show HEAD~1 |
HEAD~3 | Three commits before HEAD | git diff HEAD~3 |
HEAD^ | First parent of HEAD | git show HEAD^ |
HEAD^2 | Second parent (merge commits only) | git show HEAD^2 |
main | Tip of main branch | git diff main |
abc123f | Specific commit by hash | git show abc123f |
v1.0 | Tagged commit | git show v1.0 |
~ vs ^ (they're different!):
~ means "go back N commits along the first parent"
^ means "choose which parent" (for merge commits)
HEAD~3 = great-grandparent (3 commits back)
HEAD^2 = second parent of HEAD (the merged branch)
Visual:
← D ← E (feature)
/ \
A ← B ← C M (merge commit, HEAD)
\ /
← F ← G
HEAD = M
HEAD^ = G (first parent -- the branch you were ON)
HEAD^2 = E (second parent -- the branch you MERGED)
HEAD~1 = G (same as HEAD^ for first parent)
HEAD~2 = F
HEAD~3 = C
git reflog -- Your Safety Net
The reflog records every single time HEAD moves. Even if you "lose" commits by resetting or deleting branches, the reflog remembers them for 90 days.
Bash
# Show the reflog (every HEAD movement)
git reflog
# Output looks like:
# e4f5g6h HEAD@{0}: commit: Add dashboard
# a1b2c3d HEAD@{1}: checkout: moving from main to feature
# 9f8e7d6 HEAD@{2}: commit: Initial setup
# You can use reflog references to recover anything:
git checkout HEAD@{3} # Go back to where HEAD was 3 moves ago
git branch recovery HEAD@{5} # Create branch at an old position
Did a bad reset? Deleted a branch? Force-pushed and lost commits? git reflog shows you the hash of every state HEAD has been in. Find the hash of the state you want, then git checkout -b rescue <hash> to recover.
9. Undoing Things (Reset, Revert, Restore)
There are many ways to undo in Git. The right one depends on what you want to undo and whether the change has been pushed.
git restore -- Undo Working Directory Changes
Bash
# Discard changes in a file (go back to last committed version)
git restore src/auth.js
# Unstage a file (move from staging back to working directory)
git restore --staged src/auth.js
# Restore a file to a specific commit's version
git restore --source=HEAD~3 src/auth.js
git reset -- Move the Branch Pointer Back
Reset moves the current branch pointer to a different commit. It has three modes:
The three reset modes:
$ git reset --soft HEAD~1
─────────────────────────
Moves branch pointer back, but:
✓ Working directory: UNCHANGED
✓ Staging area: KEEPS the changes (ready to re-commit)
Use case: "I want to redo my last commit message or
combine it with more changes"
$ git reset --mixed HEAD~1 (this is the DEFAULT)
─────────────────────────────
Moves branch pointer back, and:
✓ Working directory: UNCHANGED
✗ Staging area: CLEARED (changes become unstaged)
Use case: "I want to unstage everything and re-add
selectively"
$ git reset --hard HEAD~1
─────────────────────────
Moves branch pointer back, and:
✗ Working directory: REVERTED (changes DELETED)
✗ Staging area: CLEARED
Use case: "Nuke everything, go back to this commit"
⚠️ DANGEROUS -- you lose uncommitted work
Visual: git reset --hard HEAD~2
Before:
A ← B ← C ← D ← E
↑
main (HEAD)
After:
A ← B ← C D E (D and E are now orphaned)
↑
main (HEAD)
D and E still exist in the reflog for ~90 days.
But your working directory now matches commit C.
git revert -- Safely Undo a Published Commit
Revert creates a NEW commit that undoes the changes of an old commit. Unlike reset, it doesn't rewrite history -- safe for shared branches.
Bash
# Revert the most recent commit
git revert HEAD
# Revert a specific commit
git revert abc123f
# Revert without auto-committing (stage the revert, then commit yourself)
git revert --no-commit abc123f
Visual: git revert HEAD
Before:
A ← B ← C ← D
↑
main
After (D' undoes D's changes):
A ← B ← C ← D ← D'
↑
main
D still exists in history. D' is a new commit
that applies the inverse of D's changes.
Reset: Use on LOCAL branches that haven't been pushed. It rewrites history.
Revert: Use on SHARED branches (main, develop). It adds a new commit, so other people's history isn't broken.
Rule: Never rewrite history that others have pulled.
Amending the Last Commit
Bash
# Forgot to stage a file? Typo in the message?
git add forgotten-file.js
git commit --amend -m "Updated commit message"
# Or just change the message (no file changes)
git commit --amend -m "Better message"
# ⚠️ Only do this BEFORE pushing. Amend rewrites the commit hash.
10. Rebase
Rebase is the most powerful (and most misunderstood) tool in Git. It replays your commits on top of a different base commit, creating a linear history.
What Rebase Does
Before rebase (feature branched from C, main moved to E):
← D ← E
/ ↑
A ← B ← C main
\
← F ← G
↑
feature (HEAD)
$ git checkout feature
$ git rebase main
After rebase (F and G replayed on top of E):
A ← B ← C ← D ← E ← F' ← G'
↑ ↑
main feature (HEAD)
F' and G' are NEW commits (new hashes) with the same
changes as F and G, but they now have E as their ancestor
instead of C.
History is now LINEAR. No merge commit needed.
Rebase takes your branch's commits, temporarily sets them aside, moves your branch to the new base, then replays each commit one by one on top.
Rebase vs Merge
| Aspect | Merge | Rebase |
|---|---|---|
| History | Preserves true history (diverge + merge) | Creates linear history (cleaner) |
| Commits | Creates a merge commit | Rewrites commit hashes |
| Safety | Safe for shared branches | Only for local/unpushed branches |
| Conflicts | Resolve once | May need to resolve per-commit |
| Best for | Integrating shared work | Cleaning up local feature branches |
Never rebase commits that have been pushed to a shared branch. Rebase rewrites commit hashes. If someone else has pulled your old commits, rebasing creates a nightmare of duplicate commits. Only rebase your local, unpushed work.
Interactive Rebase -- Rewrite History Like a Pro
Interactive rebase (-i) lets you edit, reorder, squash, drop, or reword your last N commits before pushing. This is how you clean up messy local history into beautiful commits.
Bash
# Interactively rebase the last 4 commits
git rebase -i HEAD~4
# This opens your editor with something like:
pick a1b2c3d Add user model
pick e4f5g6h WIP: trying auth
pick 9f8e7d6 Fix typo
pick 1a2b3c4 Actually fix auth
# Change the commands to reshape history:
pick a1b2c3d Add user model
squash e4f5g6h WIP: trying auth # combine with previous
drop 9f8e7d6 Fix typo # delete this commit entirely
reword 1a2b3c4 Actually fix auth # edit the message
Interactive Rebase Commands
| Command | Short | What It Does |
|---|---|---|
pick | p | Keep the commit as-is |
reword | r | Keep the commit but edit the message |
edit | e | Pause at this commit so you can amend it |
squash | s | Combine with previous commit (edit combined message) |
fixup | f | Combine with previous commit (discard this message) |
drop | d | Delete this commit entirely |
You made 5 messy commits while working: abc1234 Add login form def5678 fix button ghi9012 wip jkl3456 actually works now mno7890 cleanup $ git rebase -i HEAD~5 In the editor, change to: pick abc1234 Add login form fixup def5678 fix button fixup ghi9012 wip fixup jkl3456 actually works now fixup mno7890 cleanup Result: ONE clean commit "Add login form" with all changes combined.
Handling Rebase Conflicts
Bash
# If a conflict occurs during rebase:
# 1. Fix the conflict in the file
# 2. Stage the fix
git add src/auth.js
# 3. Continue the rebase
git rebase --continue
# Or abort the entire rebase (go back to before)
git rebase --abort
# Or skip this particular commit
git rebase --skip
11. Cherry-Pick & Squash
Cherry-Pick: Grab a Single Commit
Cherry-pick copies a specific commit from any branch and applies it to your current branch. It creates a new commit with the same changes but a different hash.
Before cherry-pick:
A ← B ← C ← D
↑ ↑
main feature
You want commit D on main, but NOT commit C.
$ git checkout main
$ git cherry-pick D
After:
A ← B ← D' (D' has same changes as D, new hash)
↑
main
feature is unchanged:
A ← B ← C ← D
↑
feature
Bash
# Cherry-pick a single commit
git cherry-pick abc123f
# Cherry-pick multiple commits
git cherry-pick abc123f def456g
# Cherry-pick a range of commits
git cherry-pick abc123f..def456g
# Cherry-pick without committing (just stage the changes)
git cherry-pick --no-commit abc123f
# If there's a conflict:
git cherry-pick --abort # Cancel
git cherry-pick --continue # After resolving conflicts
Cherry-pick is great for: hotfixes (grab a fix from develop into main), backporting (apply a fix to an older release branch), or when you accidentally committed to the wrong branch. Don't use it as a substitute for regular merging -- it duplicates commits.
Squashing Commits
Squashing combines multiple commits into one. There are two ways to do it:
Bash
# Method 1: Interactive rebase (covered in section 10)
git rebase -i HEAD~4
# Change "pick" to "squash" or "fixup" for commits to combine
# Method 2: Squash merge (merge a branch as a single commit)
git checkout main
git merge --squash feature
git commit -m "Add complete login feature"
# All of feature's commits become ONE commit on main
# Method 3: Soft reset + re-commit
git reset --soft HEAD~4 # Undo last 4 commits, keep changes staged
git commit -m "Single clean commit"
Squash merge visual:
Before:
← D ← E ← F
/ ↑
A ← B ← C feature
↑
main
$ git checkout main
$ git merge --squash feature
$ git commit -m "Add login feature"
After:
A ← B ← C ← G (G contains all changes from D+E+F)
↑
main
Note: feature branch is NOT deleted. G has only ONE parent (C).
This is NOT a merge commit -- it's a regular commit with
squashed changes.
12. Stashing
Stash temporarily saves your uncommitted changes so you can switch branches with a clean working directory. Think of it as a clipboard for your changes.
Bash
# Save all uncommitted changes (staged + unstaged)
git stash
# Save with a descriptive message
git stash push -m "WIP: login form validation"
# Stash only specific files
git stash push -m "partial work" src/auth.js src/login.js
# Stash including untracked files
git stash -u
# List all stashes
git stash list
# stash@{0}: On feature: WIP: login form validation
# stash@{1}: On main: quick fix attempt
# Apply the most recent stash (keep it in stash list)
git stash apply
# Apply and REMOVE from stash list
git stash pop
# Apply a specific stash
git stash apply stash@{2}
# See what's in a stash
git stash show stash@{0} # Summary
git stash show -p stash@{0} # Full diff
# Delete a specific stash
git stash drop stash@{1}
# Delete ALL stashes
git stash clear
You're working on a feature, but need to fix a bug on main: 1. git stash push -m "WIP: feature work" ← save your changes 2. git checkout main ← switch to main 3. (fix the bug, commit it) 4. git checkout feature ← switch back 5. git stash pop ← restore your changes
13. Remotes & Collaboration
Remotes are copies of your repository hosted elsewhere (GitHub, GitLab, etc.). origin is the default name for the remote you cloned from.
Remote Basics
Bash
# See your remotes
git remote -v
# origin git@github.com:user/repo.git (fetch)
# origin git@github.com:user/repo.git (push)
# Add a remote
git remote add origin git@github.com:user/repo.git
# Add a second remote (e.g., upstream for forks)
git remote add upstream git@github.com:original/repo.git
# Rename a remote
git remote rename origin github
# Remove a remote
git remote remove upstream
Push, Pull, Fetch
Three operations for syncing with remotes: ┌────────────┐ ┌────────────┐ │ LOCAL REPO │ ──── git push ───► │ REMOTE │ │ │ │ (GitHub) │ │ │ ◄─── git fetch ──── │ │ │ │ │ │ │ │ ◄── git pull ────── │ │ │ │ (fetch + merge) │ │ └────────────┘ └────────────┘
Bash
# Push your branch to remote
git push origin main
# Push and set upstream tracking (first push of a new branch)
git push -u origin feature-x
# After -u, you can just use "git push" without specifying remote/branch
# Fetch: download remote changes WITHOUT merging
git fetch origin
# Updates origin/main etc. but doesn't touch your local branches
# Pull: fetch + merge in one step
git pull origin main
# Same as: git fetch origin && git merge origin/main
# Pull with rebase (cleaner history)
git pull --rebase origin main
# Same as: git fetch origin && git rebase origin/main
git fetch is always safe -- it downloads but doesn't touch your work. git pull can cause merge conflicts because it also merges. When in doubt, fetch first, then review what changed before merging or rebasing.
Remote-Tracking Branches
When you fetch, Git stores remote branches as origin/main, origin/feature, etc. These are read-only snapshots of where the remote's branches were last time you fetched.
Bash
# See all remote-tracking branches
git branch -r
# See how far ahead/behind you are
git status
# "Your branch is ahead of 'origin/main' by 2 commits"
# See the difference between local and remote
git log origin/main..main # Commits you have that remote doesn't
git log main..origin/main # Commits remote has that you don't
The Fork Workflow (Open Source)
Bash
# 1. Fork the repo on GitHub (click the Fork button)
# 2. Clone YOUR fork
git clone git@github.com:YOUR-USER/repo.git
# 3. Add the original repo as "upstream"
git remote add upstream git@github.com:ORIGINAL/repo.git
# 4. Create a feature branch
git checkout -b my-feature
# 5. Make changes, commit, push to YOUR fork
git push -u origin my-feature
# 6. Open a Pull Request on GitHub (from your fork to upstream)
# 7. Keep your fork up to date
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
14. GitHub CLI (gh) & GitLab CLI (glab)
CLI tools let you manage issues, PRs, CI/CD, and more without leaving the terminal.
GitHub CLI (gh) -- Essential Commands
Bash
# ─── Repos ───
gh repo create my-project --public # Create a new repo
gh repo clone user/repo # Clone a repo
gh repo fork user/repo # Fork a repo
gh repo view # View current repo info
# ─── Pull Requests ───
gh pr create --title "Add auth" --body "Description here"
gh pr list # List open PRs
gh pr view 42 # View PR #42
gh pr checkout 42 # Check out PR #42 locally
gh pr merge 42 # Merge PR #42
gh pr diff 42 # View PR diff
gh pr review 42 --approve # Approve a PR
# ─── Issues ───
gh issue create --title "Bug: login fails"
gh issue list # List open issues
gh issue view 10 # View issue #10
gh issue close 10 # Close issue #10
# ─── CI/CD (Actions) ───
gh run list # List recent workflow runs
gh run view 12345 # View a specific run
gh run watch 12345 # Watch a run in real-time
gh run rerun 12345 # Re-run a failed run
# ─── Releases ───
gh release create v1.0.0 --title "v1.0.0" --notes "First release"
gh release list
# ─── Gists ───
gh gist create file.py --public
gh gist list
GitLab CLI (glab) -- Essential Commands
Bash
# ─── Merge Requests (GitLab's version of PRs) ───
glab mr create --title "Add auth" --description "Details..."
glab mr list
glab mr view 42
glab mr checkout 42
glab mr merge 42
glab mr approve 42
# ─── Issues ───
glab issue create --title "Bug report"
glab issue list
glab issue view 10
glab issue close 10
# ─── CI/CD Pipelines ───
glab ci list # List pipelines
glab ci view # View current pipeline
glab ci trace # Stream job logs
glab ci retry # Retry failed pipeline
# ─── Repos ───
glab repo clone group/project
glab repo view
Set up shell aliases for your most common workflows:
alias prc='gh pr create --fill' -- create PR with auto-filled title/body
alias prco='gh pr checkout' -- quickly check out a PR
alias prl='gh pr list' -- list open PRs
15. Advanced Git
git bisect -- Binary Search for Bugs
Bisect uses binary search to find which commit introduced a bug. It's incredibly efficient -- for 1000 commits, it finds the bad one in ~10 steps.
Bash
# Start bisecting
git bisect start
# Tell Git the current commit is bad
git bisect bad
# Tell Git a known good commit
git bisect good abc123f
# Git checks out a middle commit. Test it, then tell Git:
git bisect good # This commit is fine
git bisect bad # This commit has the bug
# Keep going until Git finds the exact commit
# "abc123f is the first bad commit"
# Done -- go back to normal
git bisect reset
# AUTOMATED bisect (if you have a test script):
git bisect start HEAD abc123f
git bisect run npm test
# Git automatically tests each commit and finds the bad one!
git worktree -- Multiple Working Directories
Worktrees let you check out multiple branches simultaneously in separate directories. No more stashing to switch branches.
Bash
# Create a worktree for a different branch
git worktree add ../hotfix-branch hotfix
# Now you have TWO directories:
# /my-project/ → main branch
# /hotfix-branch/ → hotfix branch
# List worktrees
git worktree list
# Remove a worktree when done
git worktree remove ../hotfix-branch
Tags -- Marking Releases
Bash
# Lightweight tag (just a name pointing to a commit)
git tag v1.0.0
# Annotated tag (has message, author, date -- preferred for releases)
git tag -a v1.0.0 -m "First stable release"
# Tag a specific commit
git tag -a v0.9.0 abc123f -m "Beta release"
# List tags
git tag -l
# Push tags to remote
git push origin v1.0.0 # Push a specific tag
git push origin --tags # Push all tags
# Delete a tag
git tag -d v1.0.0 # Delete locally
git push origin --delete v1.0.0 # Delete from remote
Submodules -- Repos Inside Repos
Bash
# Add a submodule
git submodule add git@github.com:user/lib.git libs/lib
# Clone a repo that has submodules
git clone --recurse-submodules git@github.com:user/repo.git
# Or if you already cloned:
git submodule update --init --recursive
# Update all submodules to latest
git submodule update --remote
Git Hooks -- Automate on Events
Hooks are scripts that run automatically at certain Git events. They live in .git/hooks/.
| Hook | When It Runs | Common Use |
|---|---|---|
pre-commit | Before a commit is created | Run linter, formatter, tests |
commit-msg | After writing commit message | Enforce commit message format |
pre-push | Before pushing | Run full test suite |
post-merge | After a merge | Install dependencies |
Bash
# Example pre-commit hook (.git/hooks/pre-commit)
#!/bin/bash
echo "Running linter..."
npm run lint
if [ $? -ne 0 ]; then
echo "Lint failed. Commit aborted."
exit 1
fi
# Make it executable
chmod +x .git/hooks/pre-commit
Useful Advanced Commands
Bash
# Clean untracked files
git clean -n # Dry run -- show what would be deleted
git clean -fd # Actually delete untracked files and directories
# Show a file at a specific commit
git show HEAD~3:src/app.js
# Compare two branches
git diff main..feature
# See which branches contain a commit
git branch --contains abc123f
# Create a patch (email a diff)
git format-patch -1 HEAD # Last commit as a .patch file
git am 0001-fix.patch # Apply a patch
# Compact your repo (garbage collection)
git gc
# Count lines changed by each author
git shortlog -sn
Git Cheat Sheet
| Task | Command |
|---|---|
| Initialize repo | git init |
| Clone repo | git clone <url> |
| Check status | git status |
| Stage files | git add <file> |
| Commit | git commit -m "msg" |
| Push | git push |
| Pull | git pull |
| Create branch | git switch -c name |
| Switch branch | git switch name |
| Merge branch | git merge name |
| View log | git log --oneline --graph --all |
| View reflog | git reflog |
| Stash changes | git stash push -m "msg" |
| Restore stash | git stash pop |
| Undo unstaged changes | git restore <file> |
| Unstage | git restore --staged <file> |
| Revert commit | git revert <hash> |
| Reset (soft) | git reset --soft HEAD~1 |
| Cherry-pick | git cherry-pick <hash> |
| Interactive rebase | git rebase -i HEAD~N |
| Find bug commit | git bisect start |
16. Practice Quiz
Q1: What does Git store at each commit?
Q2: What is a Git branch?
Q3: What does HEAD point to?
Q4: What is the difference between git fetch and git pull?
git fetch updates your remote-tracking branches (origin/main) but never touches your local branches or working directory. git pull is shorthand for fetch followed by merge (or rebase with --rebase).Q5: You committed to main by mistake. The commit hasn't been pushed. What's the safest way to undo it?
Q6: What does interactive rebase (git rebase -i) let you do?
Q7: When should you NEVER use git rebase?
Q8: What does git cherry-pick do?
Q9: You ran git reset --hard HEAD~3 and lost 3 commits. How can you recover them?
git reflog records every HEAD movement, including resets. Find the hash of the commit before the reset (it'll be in the reflog), then git checkout -b recovery <hash> to create a branch at that point. The reflog keeps entries for ~90 days. Almost nothing is truly lost in Git.Q10: What is the staging area (index) for?
git add moves changes into the staging area. git commit takes everything in the staging area and creates a permanent snapshot. This lets you commit selectively -- stage only the changes you want in each commit.