Git Handbook

Git is a distributed version control system created by Linus Torvalds in 2005. Every developer has a full copy of the repository history — no central server required for local operations. Git tracks snapshots, not diffs, and uses a content-addressable store (SHA-1 / SHA-256) to ensure integrity. It is the universal standard for source control, used from solo projects to multi-thousand-engineer monorepos.

This handbook is organised around what you want to do, not around commands. Each section covers a real-world use case with the commands you actually need.

Resources

Setup & Config

Identity

Git embeds your name and email in every commit. Set these before your first commit.

bash
# Set globally (stored in ~/.gitconfig)
git config --global user.name  "Ada Lovelace"
git config --global user.email "ada@example.com"

# Set per-repo (overrides global)
git config user.email "work@company.com"

# Show effective config
git config --list --show-origin

Useful Aliases

Aliases shorten repetitive commands. They live in ~/.gitconfig under [alias].

bash
# Compact log graph
git config --global alias.lg "log --oneline --graph --decorate --all"

# Shorter status
git config --global alias.st "status -sb"

# Undo last commit, keep changes staged
git config --global alias.undo "reset --soft HEAD~1"

# List all aliases
git config --global alias.aliases "config --get-regexp alias"

Editor & Diff Tool

bash
# Use VS Code as commit editor and diff tool
git config --global core.editor "code --wait"
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'

# Use VS Code as merge tool
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

Daily Work

What changed?

bash
# What is staged, modified, untracked?
git status
git status -sb          # compact output

# Diff working tree vs last commit
git diff                # unstaged changes
git diff --staged       # staged changes (will go into next commit)
git diff HEAD           # all changes (staged + unstaged) vs last commit

Stage & Unstage

The staging area (index) lets you compose commits precisely. You can stage individual hunks with -p to keep each commit focused.

bash
# Stage specific file / directory
git add src/app.js
git add src/

# Stage only parts of a file (interactive hunk picker)
git add -p src/app.js

# Stage everything (tracked + untracked)
git add .

# Unstage a file (keep changes in working tree)
git restore --staged src/app.js

# Discard changes in working tree (DESTRUCTIVE — changes are gone)
git restore src/app.js

Commit

bash
# Commit staged changes
git commit -m "feat: add login endpoint"

# Stage all tracked files and commit in one step
git commit -am "fix: correct off-by-one in pagination"

# Open editor for a multi-line message
git commit

# Commit with a body
git commit -m "feat: rate limiting" -m "Adds a token-bucket per IP.
Configurable via RATE_LIMIT_RPS env var.
Closes #42"

Amend Last Commit

Fix a mistake in the most recent commit — before it is pushed to a shared branch.

bash
# Fix the last commit message (not yet pushed)
git commit --amend -m "correct message"

# Add a forgotten file to the last commit
git add forgotten.js
git commit --amend --no-edit

# Change the author of the last commit
git commit --amend --author="Ada Lovelace <ada@example.com>" --no-edit

# ⚠️  Never amend commits that have already been pushed to a shared branch

Stash Work-in-Progress

Need to switch context urgently? Stash saves your dirty working tree so you can come back to it later.

bash
# Save WIP (tracked files only)
git stash
git stash push -m "half-done feature X"

# Include untracked files
git stash push -u -m "with new files"

# List stashes
git stash list

# Apply latest stash and keep it in the list
git stash apply

# Apply specific stash
git stash apply stash@{2}

# Apply and remove from list
git stash pop

# Drop a stash
git stash drop stash@{0}

# Show diff of a stash
git stash show -p stash@{0}

Branches

Create & Switch

Branches in Git are cheap — just a pointer to a commit. Create them freely.

bash
# Create and switch in one step
git switch -c feature/login

# Create from a specific point (commit / tag / branch)
git switch -c hotfix/typo origin/main

# Switch to existing branch
git switch main

# Legacy syntax (still works)
git checkout -b feature/login

Merge

bash
# Fast-forward merge (linear history, no merge commit)
git switch main
git merge feature/login

# Always create a merge commit (record that branches diverged)
git merge --no-ff feature/login

# Abort a conflicting merge
git merge --abort

# After resolving conflicts manually:
git add resolved-file.js
git merge --continue

# Merge only specific file from another branch
git checkout feature/ui -- src/styles/theme.css

Rebase

Rebase moves your commits on top of another branch, creating a linear history. Use it on private branches; avoid it on shared ones.

bash
# Replay feature branch on top of main
git switch feature/login
git rebase main

# Abort if conflicts are too messy
git rebase --abort

# After resolving each conflict:
git add resolved-file.js
git rebase --continue

# Rebase onto a specific commit
git rebase --onto main feature/old feature/new

# ⚠️  Never rebase commits shared with others (force-push required after)

Delete & Rename

bash
# Delete a fully-merged branch
git branch -d feature/login

# Force delete (even if not merged)
git branch -D spike/experiment

# Delete remote tracking branch
git push origin --delete feature/login
git remote prune origin    # remove stale remote-tracking refs

# Rename current branch
git branch -m new-name

# Rename any branch
git branch -m old-name new-name

Track Remote

bash
# See tracking relationships
git branch -vv

# Set upstream for current branch
git branch --set-upstream-to=origin/main

# Push and set upstream at the same time
git push -u origin feature/login

# Fetch all remotes and prune deleted ones
git fetch --all --prune

Remote & Sync

Manage Remotes

bash
# List remotes
git remote -v

# Add a remote
git remote add origin https://github.com/user/repo.git
git remote add upstream https://github.com/original/repo.git

# Change remote URL (e.g. switch to SSH)
git remote set-url origin git@github.com:user/repo.git

# Rename / remove
git remote rename origin old-origin
git remote remove old-origin

Fetch & Pull

fetch downloads without touching your working tree. pull fetches then merges.

bash
# Download new commits, don't merge
git fetch origin
git fetch --all --prune

# Pull = fetch + merge (default)
git pull

# Pull and rebase instead of merge (cleaner history)
git pull --rebase

# Pull a specific branch into current
git pull origin main

# Update a local branch without switching to it
git fetch origin main:main

Push

bash
# Push current branch to its upstream
git push

# Push and set upstream in one go
git push -u origin feature/login

# Push a tag
git push origin v1.2.0
git push --tags        # push all tags

# Force push (after rebase / amend — ⚠️  not on shared branches)
git push --force-with-lease   # safe: fails if someone else pushed
git push --force              # ⚠️  overwrites without checking

# Delete a remote branch
git push origin --delete feature/old

Inspect History

Browse Log

bash
# All commits, one line each
git log --oneline

# Graph of all branches
git log --oneline --graph --decorate --all

# Commits by author
git log --author="Ada"

# Commits in date range
git log --since="2024-01-01" --until="2024-06-01"

# Commits that touched a file
git log --follow -- src/auth.js

# Last 5 commits
git log -5

# Show stat (files changed)
git log --stat

# Full diff for each commit
git log -p

Diff Anything

bash
# Working tree vs staging area
git diff

# Staged vs last commit
git diff --staged

# Compare two branches
git diff main..feature/login
git diff main...feature/login   # changes since they diverged

# Compare two commits
git diff abc1234 def5678

# Only file names that changed
git diff --name-only main..feature

# Diff a specific file between branches
git diff main -- src/auth.js

# Word-level diff (good for prose)
git diff --word-diff

Who Changed What?

bash
# Who last touched each line?
git blame src/auth.js

# Show only lines 10-30
git blame -L 10,30 src/auth.js

# Ignore whitespace changes
git blame -w src/auth.js

# Show the commit that deleted a line (requires -S and pickaxe)
git log -S "deleted_function_name" --source --all

The pickaxe (-S / -G) finds commits that added, removed, or changed a string — essential for tracking when a bug was introduced or a function was renamed.

bash
# Find all commits whose message matches
git log --grep="fix: login"

# Find commits that added/removed a string (pickaxe)
git log -S "password_hash" --source --all

# Find commits that changed a regex pattern
git log -G "function login"

# Search file contents across history
git grep "TODO" HEAD
git grep "TODO" $(git rev-list --all)  # all commits (slow)

Find a Regression (bisect)

Binary-search the commit history to find exactly which commit introduced a bug. Git checks out the midpoint; you test and mark good/bad; repeat until Git pinpoints the culprit.

bash
# Start bisect session
git bisect start

# Mark current commit as bad (has the bug)
git bisect bad

# Mark a known-good commit
git bisect good v1.0.0

# Git checks out a midpoint — test it, then mark:
git bisect good    # or: git bisect bad

# Repeat until Git identifies the first bad commit.

# Automate with a test script (exit 0 = good, exit 1 = bad)
git bisect run npm test

# End the session
git bisect reset

Undo & Fix

Discard Local Changes

bash
# Discard all unstaged changes (DESTRUCTIVE)
git restore .

# Discard changes in one file
git restore src/app.js

# Remove untracked files (dry run first!)
git clean -n          # show what would be deleted
git clean -f          # delete untracked files
git clean -fd         # delete untracked files + dirs
git clean -fdx        # also delete ignored files (⚠️  node_modules etc.)

Reset to a State

reset moves the HEAD pointer (and optionally the index and working tree) to any commit. --soft is safe; --hard is destructive.

bash
# Move HEAD back N commits, keep changes staged
git reset --soft HEAD~1
git reset --soft HEAD~3

# Move HEAD back, unstage changes (default)
git reset HEAD~1
git reset --mixed HEAD~1

# Move HEAD back and DISCARD all changes (DESTRUCTIVE)
git reset --hard HEAD~1
git reset --hard origin/main   # match remote exactly

# Unstage a specific file (without moving HEAD)
git reset HEAD -- src/app.js

Revert a Commit

revert creates a new commit that undoes the effect of an old one. Unlike reset, it is safe to use on shared/public branches.

bash
# Create a new commit that undoes a previous one (safe for shared branches)
git revert abc1234

# Revert without opening the editor
git revert abc1234 --no-edit

# Revert a merge commit (specify which parent to keep)
git revert -m 1 abc1234

# Revert a range of commits (oldest first)
git revert oldest^..newest

Recover Anything (reflog)

The reflog records every position HEAD has been in, including after hard resets, rebases, and deleted branches. It is your last resort for recovering "lost" work.

bash
# See every HEAD position (including resets, rebases)
git reflog

# Recover a branch you accidentally deleted
git checkout -b recovered-branch HEAD@{3}

# Undo a hard reset
git reset --hard HEAD@{1}

# Reflog for a specific branch
git reflog show feature/login

# Reflog entries expire after 90 days by default

Rewrite History

These tools rewrite commits and change SHAs. Only use them on commits not yet pushed to a shared branch, or when you have team agreement and plan a coordinated force-push.

Interactive Rebase

The most powerful tool for cleaning up a branch before merging — reorder, rename, squash, or drop commits.

bash
# Rewrite the last 4 commits
git rebase -i HEAD~4

# Rewrite since diverging from main
git rebase -i main

# Actions in the editor:
#   pick   — keep as-is
#   reword — keep but edit message
#   edit   — pause to amend the commit
#   squash — merge into previous commit, combine messages
#   fixup  — merge into previous commit, discard message
#   drop   — delete the commit entirely
#   exec   — run a shell command after the commit

Fixup & Squash

bash
# Create a fixup commit targeting an earlier commit by message
git commit --fixup abc1234

# Then auto-squash during rebase
git rebase -i --autosquash HEAD~5

# Squash all commits on feature branch into one
git switch main
git merge --squash feature/login
git commit -m "feat: login feature"  # single tidy commit

Cherry-pick

Apply a commit from any branch onto the current one — useful for backporting fixes.

bash
# Apply a single commit from another branch
git cherry-pick abc1234

# Apply a range of commits
git cherry-pick abc1234^..def5678

# Apply but don't commit yet (stage the changes)
git cherry-pick --no-commit abc1234

# If conflicts:
git cherry-pick --continue
git cherry-pick --abort

Tags & Releases

Create & Push Tags

Use annotated tags for releases — they store a message, tagger, and date.

bash
# Lightweight tag (just a pointer)
git tag v1.0.0

# Annotated tag (recommended for releases — stores author + message)
git tag -a v1.0.0 -m "First stable release"

# Tag a specific commit
git tag -a v1.0.0 abc1234 -m "Release"

# Push a single tag
git push origin v1.0.0

# Push all tags
git push --tags

# List tags matching a pattern
git tag -l "v1.*"

Delete Tags

bash
# Delete local tag
git tag -d v1.0.0-beta

# Delete remote tag
git push origin --delete v1.0.0-beta

# Move a tag to current HEAD (re-tag)
git tag -f v1.0.0          # move local
git push --force origin v1.0.0  # ⚠️  update remote

Submodules

Add & Init

bash
# Add a submodule
git submodule add https://github.com/org/lib.git libs/lib

# Clone a repo that has submodules
git clone --recurse-submodules https://github.com/org/project.git

# If you already cloned without --recurse-submodules
git submodule init
git submodule update

Update

bash
# Pull latest for all submodules
git submodule update --remote --merge

# Run a command in every submodule
git submodule foreach 'git checkout main && git pull'

# Remove a submodule completely
git submodule deinit libs/lib
git rm libs/lib
rm -rf .git/modules/libs/lib

Advanced

Worktrees

Check out multiple branches simultaneously in separate directories — no stashing required. Ideal when you need to test a hotfix while keeping your feature branch intact.

bash
# Check out a second branch in a separate directory (no stash needed)
git worktree add ../project-hotfix hotfix/critical

# List all worktrees
git worktree list

# Remove a worktree (after you're done)
git worktree remove ../project-hotfix

# Useful pattern: keep main checked out while working on a feature
git worktree add ../project-main main

Sparse Checkout

Download only part of a large monorepo, keeping disk usage and clone time low.

bash
# Only download / check out a subdirectory of a large monorepo
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/ui packages/api

# Add more paths later
git sparse-checkout add packages/shared

Hooks

Git hooks are shell scripts that run automatically at key points in the workflow.

bash
# Hooks live in .git/hooks/ (not committed) or in a shared hooks dir.
# To share hooks, configure:
git config core.hooksPath .githooks

# Common hook files (make them executable: chmod +x):
#   pre-commit       — run lints / tests before commit
#   commit-msg       — validate commit message format
#   pre-push         — run tests before push
#   post-merge       — e.g. run npm install after pull

# Example pre-commit hook (.githooks/pre-commit):
# #!/bin/sh
# npm run lint || exit 1

Patches by Email

bash
# Create a patch file for the last 3 commits
git format-patch HEAD~3

# Create a single patch from a diff
git diff > my-changes.patch

# Apply a patch (tries to apply cleanly)
git apply my-changes.patch

# Apply a format-patch (preserves commit metadata)
git am 0001-feat-login.patch

# Send patches by email (git send-email)
git send-email --to=maintainer@project.org HEAD~3

Useful Config Flags

Performance and workflow improvements worth enabling globally.

bash
# Speed up large repos
git config core.fsmonitor true       # OS file-system watcher
git config core.untrackedCache true  # cache untracked file lookup

# Always rebase on pull (cleaner history)
git config --global pull.rebase true

# Prune stale remote refs on every fetch
git config --global fetch.prune true

# Auto-setup tracking when pushing a new branch
git config --global push.autoSetupRemote true

# Use patience diff algorithm (better for large refactors)
git config --global diff.algorithm patience

# Show moved lines differently in diffs
git config --global diff.colorMoved zebra

# Store credentials (macOS keychain / Windows Credential Manager)
git config --global credential.helper osxkeychain    # macOS
git config --global credential.helper manager        # Windows