1. 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 NOT GitHub

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
Config Levels

--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
└── ...
Never Manually Edit .git

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):

ObjectWhat It StoresAnalogy
BlobFile contents (just the raw data, not the filename)A single file
TreeA directory listing -- maps filenames to blobs and other treesA folder
CommitA snapshot -- points to a tree + metadata (author, message, parent commit)A save point
TagA 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.

Example: Selective Staging
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

StateMeaningWhere
UntrackedNew file Git has never seenWorking directory
ModifiedTracked file that has changedWorking directory
StagedChange marked for next commitStaging area
CommittedSafely 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
Avoid git add .

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)
Commit Message Convention

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.)
The DAG

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)
Detached HEAD State

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
Example: git log --oneline --graph --all
* 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:

SyntaxMeaningExample
HEADCurrent commitgit show HEAD
HEAD~1One commit before HEADgit show HEAD~1
HEAD~3Three commits before HEADgit diff HEAD~3
HEAD^First parent of HEADgit show HEAD^
HEAD^2Second parent (merge commits only)git show HEAD^2
mainTip of main branchgit diff main
abc123fSpecific commit by hashgit show abc123f
v1.0Tagged commitgit 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
The Reflog Saves Lives

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 vs Revert: When to Use Which

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

AspectMergeRebase
HistoryPreserves true history (diverge + merge)Creates linear history (cleaner)
CommitsCreates a merge commitRewrites commit hashes
SafetySafe for shared branchesOnly for local/unpushed branches
ConflictsResolve onceMay need to resolve per-commit
Best forIntegrating shared workCleaning up local feature branches
The Golden Rule of Rebase

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

CommandShortWhat It Does
pickpKeep the commit as-is
rewordrKeep the commit but edit the message
editePause at this commit so you can amend it
squashsCombine with previous commit (edit combined message)
fixupfCombine with previous commit (discard this message)
dropdDelete this commit entirely
Example: Squashing WIP Commits
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
When to Cherry-Pick

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
Common Stash Workflow
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
Fetch vs Pull

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
Pro Tip: gh + aliases

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/.

HookWhen It RunsCommon Use
pre-commitBefore a commit is createdRun linter, formatter, tests
commit-msgAfter writing commit messageEnforce commit message format
pre-pushBefore pushingRun full test suite
post-mergeAfter a mergeInstall 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

TaskCommand
Initialize repogit init
Clone repogit clone <url>
Check statusgit status
Stage filesgit add <file>
Commitgit commit -m "msg"
Pushgit push
Pullgit pull
Create branchgit switch -c name
Switch branchgit switch name
Merge branchgit merge name
View loggit log --oneline --graph --all
View refloggit reflog
Stash changesgit stash push -m "msg"
Restore stashgit stash pop
Undo unstaged changesgit restore <file>
Unstagegit restore --staged <file>
Revert commitgit revert <hash>
Reset (soft)git reset --soft HEAD~1
Cherry-pickgit cherry-pick <hash>
Interactive rebasegit rebase -i HEAD~N
Find bug commitgit bisect start

16. Practice Quiz

Q1: What does Git store at each commit?

C) A snapshot of the entire project. Git stores a complete snapshot at each commit. Files that haven't changed are stored as pointers to the previous identical blob, so it's space-efficient. But conceptually, every commit is a full picture of your project.

Q2: What is a Git branch?

B) A lightweight movable pointer to a commit. A branch is literally a 40-byte file containing a commit hash. Creating a branch is instant. When you make a new commit, the branch pointer automatically moves forward to the new commit.

Q3: What does HEAD point to?

A) The current branch. HEAD is a symbolic reference that usually points to a branch name (e.g., "ref: refs/heads/main"). That branch then points to a commit. In "detached HEAD" state, HEAD points directly to a commit hash instead of a branch.

Q4: What is the difference between git fetch and git pull?

C) fetch downloads without merging, pull does fetch + merge. 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?

B) git reset --soft HEAD~1. Since the commit hasn't been pushed, it's safe to rewrite history. --soft moves the branch pointer back one commit but keeps all your changes staged. You can then switch to the correct branch and commit there. --hard would delete your changes, and revert is overkill for unpushed commits.

Q6: What does interactive rebase (git rebase -i) let you do?

D) All of the above. Interactive rebase gives you full control: pick (keep), reword (edit message), edit (pause to amend), squash (combine with previous), fixup (combine and discard message), and drop (delete). It's the most powerful history-editing tool in Git.

Q7: When should you NEVER use git rebase?

A) On commits that have been pushed to a shared branch. The golden rule: never rebase published commits. Rebase rewrites commit hashes, so if someone else has pulled the old commits, their history diverges from yours and creates chaos. Only rebase local, unpushed work.

Q8: What does git cherry-pick do?

B) Copies a specific commit to the current branch. Cherry-pick takes the changes introduced by a specific commit and creates a new commit with those same changes on your current branch. The new commit gets a different hash. It's useful for hotfixes and backporting.

Q9: You ran git reset --hard HEAD~3 and lost 3 commits. How can you recover them?

C) Use the reflog. 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?

D) A holding area for preparing commits. The staging area (index) sits between your working directory and the repository. 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.