Git Refresher
Version control — branching, merging, rebasing, and advanced workflows quick reference
Table of Contents
Core Concepts
Git is a distributed version control system. Every clone is a full repository with complete history. Understanding the four-zone model and the object database is the foundation for everything else.
The Four Zones
Changes move through four distinct areas before becoming part of history:
# Zone 1: Working Tree — files you see and edit on disk
# Zone 2: Staging Area (Index) — snapshot prepared for the next commit
# Zone 3: Local Repository — committed history stored in .git/objects/
# Zone 4: Remote Repository — shared copy on GitHub, GitLab, etc.
# Flow:
# Edit file → git add → git commit → git push
# WorkTree → Index → Local Repo → Remote
git add -p lets you stage individual hunks within a single file.
Git Object Types
Git stores everything as one of four immutable object types, identified by their SHA-1 (soon SHA-256) hash:
| Object | Contains | Example use |
|---|---|---|
| blob | Raw file contents | Every version of every file |
| tree | Directory listing (filenames + blob/tree hashes) | Snapshot of a directory at commit time |
| commit | Tree hash, parent hash(es), author, message | A point in history |
| tag | Object hash + tagger + message | Annotated tags (v1.0.0) |
# Inspect any object by its hash
git cat-file -t abc1234 # print type: blob, tree, commit, tag
git cat-file -p abc1234 # pretty-print contents
# A commit object looks like:
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent 8f14e45fceea167a5a36dedd4bea2543dcdf25a
# author Alex <[email protected]> 1708000000 -0500
# committer Alex <[email protected]> 1708000000 -0500
#
# feat: add login endpoint
SHA-1 Hashes and Refs
Every object has a unique 40-character hex SHA-1 hash. Git lets you abbreviate to the shortest unambiguous prefix (usually 7 chars). Refs are human-readable pointers to hashes:
# HEAD — pointer to current commit (usually via a branch ref)
cat .git/HEAD # → ref: refs/heads/main
cat .git/refs/heads/main # → abc1234... (commit hash)
# When HEAD points directly to a hash (not a branch), you're in detached HEAD state
git checkout abc1234 # → detached HEAD
# Relative refs
git log HEAD~1 # one commit before HEAD
git log HEAD~3 # three commits before HEAD
git log HEAD^2 # second parent of a merge commit
git log main@{3} # main as it was 3 moves ago (reflog)
The DAG Model
Git history is a Directed Acyclic Graph (DAG). Each commit points to its parent(s). Merge commits have two parents. Branches are just movable pointers to commits — lightweight and cheap to create.
# Visualize the DAG
git log --all --graph --oneline --decorate
# Example output:
# * f3a1b2c (HEAD -> main, origin/main) feat: ship dashboard
# * d29eb67 Merge branch 'feature/login'
# |\
# | * 8c4f1e0 feat: add JWT auth
# | * 2a3b9d1 feat: login form
# * | 5c6d7e8 fix: typo in README
# |/
# * 1a2b3c4 init: project scaffold
Setup & Configuration
Initializing a Repository
# Create a new repo in the current directory
git init
# Create a new repo in a named directory
git init my-project
# Clone an existing repo
git clone https://github.com/user/repo.git
git clone https://github.com/user/repo.git my-local-name # rename directory
git clone --depth=1 https://github.com/user/repo.git # shallow clone (no history)
git clone --branch develop https://github.com/user/repo.git # clone specific branch
git clone [email protected]:user/repo.git # SSH clone
Configuration Levels
Git configuration is layered. More specific levels override broader ones:
| Level | File | Scope | Flag |
|---|---|---|---|
| system | /etc/gitconfig | All users on machine | --system |
| global | ~/.gitconfig | Your user account | --global |
| local | .git/config | This repository only | --local |
| worktree | .git/config.worktree | This worktree only | --worktree |
# Identity — required before your first commit
git config --global user.name "Alex"
git config --global user.email "[email protected]"
# Editor used for commit messages, rebase, etc.
git config --global core.editor "vim"
git config --global core.editor "code --wait" # VS Code
# Line ending normalization
git config --global core.autocrlf input # macOS/Linux: normalize CRLF → LF on commit
git config --global core.autocrlf true # Windows: LF → CRLF on checkout
# Default branch name for new repos
git config --global init.defaultBranch main
# Pull behavior: rebase instead of merge
git config --global pull.rebase true
# Push only the current branch (safer default)
git config --global push.default current
# Colorize output
git config --global color.ui auto
# Better diff algorithm
git config --global diff.algorithm histogram
# Reuse recorded resolution (rerere) — remembers conflict resolutions
git config --global rerere.enabled true
# View all config and their source files
git config --list --show-origin
Useful Aliases
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --all --graph --oneline --decorate"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.unstage "restore --staged"
git config --global alias.oops "commit --amend --no-edit"
# Use as: git lg, git st, git oops
SSH Key Setup
# Generate an ED25519 key (preferred over RSA)
ssh-keygen -t ed25519 -C "[email protected]"
# Start ssh-agent and add key
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Copy public key to clipboard (macOS)
pbcopy < ~/.ssh/id_ed25519.pub
# Then paste into GitHub: Settings → SSH and GPG keys → New SSH key
# Test the connection
ssh -T [email protected]
# ~/.ssh/config — multiple keys for different hosts
# Host github.com
# HostName github.com
# User git
# IdentityFile ~/.ssh/id_ed25519_personal
#
# Host github-work
# HostName github.com
# User git
# IdentityFile ~/.ssh/id_ed25519_work
Credential Helpers
# macOS keychain (recommended on macOS)
git config --global credential.helper osxkeychain
# Store credentials in memory for 15 minutes
git config --global credential.helper "cache --timeout=900"
# GitHub CLI handles auth automatically
gh auth login
gh auth status
Basic Workflow
Staging Changes
# Stage a specific file
git add src/main.py
# Stage all changes in the current directory (and below)
git add .
git add -A # also stages deletions from tracked dirs above cwd
# Interactive patch mode — choose individual hunks to stage
git add -p # or --patch
# y = stage hunk, n = skip, s = split, e = edit, q = quit
# Stage part of a new file (untracked)
git add -N file.py # mark as "intent to add", then git add -p works on it
# Unstage
git restore --staged file.py # modern (Git 2.23+)
git reset HEAD file.py # legacy equivalent
Committing
# Commit with inline message
git commit -m "feat: add user authentication"
# Open editor for a longer message
git commit
# Stage all tracked files and commit in one step (skips staging for untracked)
git commit -a -m "fix: handle null pointer in user service"
# Amend the last commit (message or staged changes)
git commit --amend -m "feat: add user authentication with JWT"
git commit --amend --no-edit # keep existing message, add staged changes
git commit --amend rewrites history. If you've already pushed the commit to a shared branch, amending creates a diverged history that forces collaborators to reset or re-pull. See the Undoing Changes section for safer alternatives.
Conventional Commits Format
# Format: <type>[optional scope]: <description>
# Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build
git commit -m "feat(auth): add OAuth2 Google login"
git commit -m "fix(api): handle empty response from payment gateway"
git commit -m "docs: update README with Docker setup instructions"
git commit -m "refactor(db): extract query builder to separate module"
# Breaking change: add ! after type or BREAKING CHANGE in footer
git commit -m "feat!: remove deprecated v1 API endpoints"
Checking Status and Diff
# Compact status view
git status
git status -s # short format: M=modified, A=added, ?=untracked
# Diff working tree vs staging area (unstaged changes)
git diff
# Diff staging area vs last commit (staged changes, what will be committed)
git diff --staged # or --cached
# Diff between commits or branches
git diff main..feature/login
git diff HEAD~3 HEAD -- src/auth.py # specific file over last 3 commits
# Word-level diff (useful for prose)
git diff --word-diff
Viewing History
# Basic log
git log
# One line per commit
git log --oneline
# Graph view with all branches
git log --all --graph --oneline --decorate
# Limit by author
git log --author="Alex"
# Limit by date
git log --since="2 weeks ago"
git log --after="2026-01-01" --before="2026-02-01"
# Search commit messages
git log --grep="login"
# Show commits that changed a specific function
git log -L :authenticate:src/auth.py
# Show which commits touched a specific string (pickaxe)
git log -S "SECRET_KEY"
# Show stats (files changed, insertions, deletions)
git log --stat
git log --shortstat # one line of stats per commit
# Limit number of commits
git log -10 # last 10 commits
# Show a single commit in detail
git show abc1234
git show HEAD # latest commit
git show HEAD:src/main.py # file content at that commit
Branching
Branches in Git are extremely cheap — just a 41-byte file containing a SHA-1 hash. Creating a branch is an O(1) operation. Use them liberally.
Creating and Navigating Branches
# List local branches (* marks current)
git branch
# List all branches including remote-tracking
git branch -a
# List remote branches only
git branch -r
# Create a branch (does NOT switch to it)
git branch feature/user-profile
# Switch to an existing branch
git switch feature/user-profile # modern (Git 2.23+)
git checkout feature/user-profile # legacy
# Create and switch in one step
git switch -c feature/user-profile # modern
git checkout -b feature/user-profile # legacy
# Create branch from a specific commit or tag
git switch -c hotfix/payment v1.2.3
git checkout -b hotfix/payment abc1234
# Rename a branch
git branch -m old-name new-name
git branch -m new-name # rename current branch
# Delete a merged branch (safe)
git branch -d feature/user-profile
# Force delete an unmerged branch
git branch -D feature/abandoned
# Delete a remote branch
git push origin --delete feature/user-profile
Tracking Branches and Upstream
# Set upstream (tracking) when pushing for the first time
git push -u origin feature/user-profile
# -u = --set-upstream
# Set upstream without pushing
git branch --set-upstream-to=origin/main main
# Check tracking relationships
git branch -vv
# Example output:
# * feature/login abc1234 [origin/feature/login: ahead 2, behind 1] feat: add form
# main def5678 [origin/main] chore: bump version
# Push current branch to remote (once upstream is set)
git push
# Pull and update tracking branch
git fetch origin
git merge origin/main # or git rebase origin/main
Detached HEAD
# Enter detached HEAD by checking out a commit hash or tag
git checkout abc1234
# HEAD is now at abc1234
# You can make commits in detached HEAD, but they're not on any branch
# They will be garbage collected unless you create a branch from them
# Save your work: create a branch from current detached HEAD
git switch -c my-experimental-branch
# Or just go back to a branch to discard detached work
git switch main
Branch Naming Conventions
# Common prefixes (choose one style and be consistent)
feature/user-authentication # new functionality
fix/null-pointer-exception # bug fixes
hotfix/payment-gateway-crash # urgent production fixes
refactor/extract-auth-service # code improvements without new features
chore/update-dependencies # maintenance
docs/add-api-documentation # documentation
test/add-integration-tests # test-only changes
release/v2.1.0 # release preparation
# Avoid: spaces, uppercase, special chars (except - and /)
# Use hyphens, not underscores: feature/user-login not feature/user_login
# Include ticket numbers when applicable: feature/PROJ-123-user-login
Merging
Fast-Forward Merge
When the target branch has no new commits since the feature branch was created, Git simply moves the pointer forward — no merge commit is needed.
# Fast-forward merge (default when possible)
git switch main
git merge feature/user-profile
# Result: main pointer moves to feature/user-profile's tip
# Before:
# main → A → B
# \
# C → D (feature/user-profile)
#
# After fast-forward:
# main → A → B → C → D
# Prevent fast-forward: always create a merge commit
git merge --no-ff feature/user-profile
# Useful for keeping feature branch history visible in main's log
Three-Way Merge
When both branches have diverged, Git finds the common ancestor and performs a three-way merge, producing a new merge commit with two parents.
# Before:
# main → A → B → E
# \
# feature → A → B → C → D
#
# After merge:
# main → A → B → E → M (merge commit, parents: E and D)
# \ /
# feature C → D
git switch main
git merge feature/payment
# View merge commit details
git show HEAD
# commit M...
# Merge: E... D... <-- two parent refs
Resolving Merge Conflicts
# Start a merge that hits conflicts
git merge feature/login
# CONFLICT (content): Merge conflict in src/auth.py
# Automatic merge failed; fix conflicts and then commit the result.
# See which files have conflicts
git status
# both modified: src/auth.py
# Conflict markers inside the file:
# <<<<<<< HEAD
# def login(username, password):
# return db.authenticate(username, password)
# =======
# def login(email, password):
# return db.authenticate_by_email(email, password)
# >>>>>>> feature/login
# Resolve: edit the file to the correct final state, removing markers
# Then stage the resolved file:
git add src/auth.py
# Complete the merge
git commit # Git pre-fills a merge commit message
# Abort and go back to pre-merge state
git merge --abort
Merge Strategies
# Default strategy (ort in modern Git, recursive in older)
git merge feature/login
# Prefer our version for all conflicts (dangerous — silently discards their work)
git merge -X ours feature/login
# Prefer their version for all conflicts
git merge -X theirs feature/login
# Squash all feature commits into a single staged change (doesn't auto-commit)
git merge --squash feature/login
git commit -m "feat: add user login (squashed)"
# Note: creates no merge commit; feature branch is not "merged" in Git's eyes
git mergetool opens your configured tool (VS Code, IntelliJ, vimdiff) with a 3-pane view showing LOCAL (yours), REMOTE (theirs), and BASE (common ancestor). Set it up once:git config --global merge.tool vscodegit config --global mergetool.vscode.cmd 'code --wait $MERGED'
Rebasing
Rebase replays your commits on top of another base commit, producing a linear history. It's a history-rewriting operation — use with care on shared branches.
Basic Rebase
# Situation: main has moved forward while you worked on feature/login
# Before:
# main → A → B → C → D
# feature → A → B → X → Y
# Rebase feature onto the tip of main
git switch feature/login
git rebase main
# After rebase: X and Y are replayed as X' and Y' on top of D
# main → A → B → C → D
# feature → A → B → C → D → X' → Y'
# Now a fast-forward merge is possible
git switch main
git merge feature/login # fast-forward, linear history
Rebase vs Merge: When to Use Each
| Scenario | Prefer Rebase | Prefer Merge |
|---|---|---|
| Update feature branch from main | Yes — linear history | OK, creates merge commit noise |
| Integrating a feature into main | After local cleanup | When preserving branch topology matters |
| Public/shared branch | Never — rewrites history | Yes — safe |
| Cleaning up WIP commits before PR | Yes — interactive rebase | No |
| Hotfix on release branch | Merge into both branches | Yes |
main or any branch others have cloned, their history diverges from yours. Everyone who has pulled will get conflicts or need to hard reset. Only rebase commits that exist only in your local repo or your private fork branch.
Interactive Rebase
Interactive rebase (-i) lets you edit, reorder, squash, and drop commits before sharing them. It's your commit history editor.
# Interactively edit the last 4 commits
git rebase -i HEAD~4
# Opens editor with a list like:
# pick abc1234 feat: add login form
# pick def5678 wip: debugging
# pick ghi9012 fix typo
# pick jkl3456 feat: add JWT validation
# Commands available per commit:
# pick = keep as-is
# reword = keep commit, edit message
# edit = stop at this commit to amend it
# squash = meld into previous commit (combine messages)
# fixup = meld into previous commit, discard this message
# drop = delete this commit entirely
# exec = run a shell command
# Example: squash the WIP and typo fix into the login form commit
# pick abc1234 feat: add login form
# squash def5678 wip: debugging
# squash ghi9012 fix typo
# pick jkl3456 feat: add JWT validation
Autosquash Workflow
# Mark a commit as a fixup for a previous commit while working
git commit --fixup=abc1234 # creates commit "fixup! feat: add login form"
git commit --squash=abc1234 # creates commit "squash! feat: add login form"
# Then run rebase with --autosquash to automatically order and mark them
git rebase -i --autosquash HEAD~6
# Git automatically moves fixup!/squash! commits next to their targets
# and sets the action to fixup/squash
# Enable autosquash by default (so -i always uses it)
git config --global rebase.autoSquash true
Rebase Onto
# Move a branch onto a different base
# Situation: feature/search was branched from feature/login,
# but you want to move it to main instead
# Before:
# main → A → B
# feature/login → A → B → C → D
# feature/search → A → B → C → X → Y
git rebase --onto main feature/login feature/search
# After: X and Y are replayed on top of B (main's tip)
# main → A → B
# feature/login → A → B → C → D
# feature/search → A → B → X' → Y'
Handling Rebase Conflicts
# When a rebase hits a conflict, it pauses at that commit
# CONFLICT (content): Merge conflict in src/auth.py
# Fix the conflict in the file, then:
git add src/auth.py
git rebase --continue # proceed to the next commit
# Skip this commit entirely (rarely correct)
git rebase --skip
# Abort rebase, restore original state
git rebase --abort
Remote Operations
Managing Remotes
# List remotes
git remote -v
# Add a remote
git remote add origin [email protected]:alex/my-project.git
git remote add upstream [email protected]:original/repo.git # for forks
# Rename a remote
git remote rename origin old-origin
# Remove a remote
git remote remove upstream
# Show detailed remote info
git remote show origin
# Change remote URL (e.g., switch HTTP to SSH)
git remote set-url origin [email protected]:alex/my-project.git
Fetch, Pull, Push
# Fetch: download objects and update remote-tracking branches (no local changes)
git fetch origin
git fetch --all # fetch all remotes
git fetch --prune # remove remote-tracking branches deleted on remote
# Pull: fetch + merge (or rebase if pull.rebase=true)
git pull
git pull --rebase # fetch + rebase (preferred for linear history)
git pull origin main # pull specific remote/branch
# Push: upload local commits to remote
git push
git push origin feature/login # push specific branch
git push -u origin feature/login # push and set upstream tracking
git push --all # push all branches
git push --tags # push all tags
Force Push (with care)
# --force-with-lease: safe force push
# Fails if the remote has commits you don't have locally (someone else pushed)
git push --force-with-lease
# --force: unconditional overwrite (DANGEROUS on shared branches)
git push --force
# Only acceptable on your own private feature branch after interactive rebase
# Never force push to: main, develop, release/*, or any branch others use
--force overwrites the remote unconditionally and can silently destroy teammates' commits. --force-with-lease checks that the remote ref matches what you last fetched, preventing you from clobbering work you haven't seen.
Fork Workflow
# Typical open-source fork setup
git clone [email protected]:alex/project-fork.git # your fork
git remote add upstream [email protected]:original/project.git
# Sync fork with upstream
git fetch upstream
git switch main
git merge upstream/main # or: git rebase upstream/main
git push origin main # update your fork's main
# Create feature branch from synced main
git switch -c feature/my-contribution
# ... make changes, commit ...
git push -u origin feature/my-contribution
# Then open a Pull Request from your fork to the upstream repo
Tracking Branch Configuration
# See all tracking relationships
git branch -vv
# Set upstream for existing branch
git branch --set-upstream-to=origin/develop develop
# Unset upstream
git branch --unset-upstream
Stashing
Stash saves your uncommitted changes to a temporary storage area so you can switch contexts without committing work-in-progress.
Basic Stash Operations
# Stash all tracked modified/deleted files and staged changes
git stash
git stash push # explicit form (same)
# Stash with a descriptive message
git stash push -m "WIP: half-done login form refactor"
# Include untracked files (-u)
git stash push -u -m "includes new untracked config file"
# Include ignored files too (-a / --all)
git stash push -a
# List all stashes
git stash list
# stash@{0}: WIP on main: abc1234 feat: dashboard
# stash@{1}: On feature/login: WIP: half-done login form refactor
# Apply most recent stash (keeps stash in list)
git stash apply
# Apply and remove from stash list (pop)
git stash pop
# Apply a specific stash
git stash apply stash@{1}
# Show what a stash contains
git stash show stash@{0}
git stash show -p stash@{0} # full diff
# Delete a specific stash
git stash drop stash@{1}
# Delete all stashes (no undo!)
git stash clear
Partial Stash
# Interactively choose hunks to stash (like git add -p)
git stash push -p
# y = stash this hunk, n = keep in working tree
# Stash only specific files
git stash push -m "stash auth changes only" -- src/auth.py src/models/user.py
Creating a Branch from a Stash
# Create a branch from the stash's base commit, then apply the stash
# Useful when the stash conflicts with current working tree
git stash branch feature/saved-work stash@{0}
# Equivalent to:
# git checkout -b feature/saved-work <stash-base-commit>
# git stash pop
git stash clear destroys them without warning. For context switches that last more than an hour, commit as WIP (git commit -m "WIP: do not merge") on a temp branch instead. You can always interactive-rebase it later.
Undoing Changes
Git has multiple mechanisms for undoing work, each with different safety profiles. Understanding which to use is critical.
Working Tree and Staging Area
# Discard unstaged changes to a file (PERMANENT — no undo)
git restore src/auth.py # modern (Git 2.23+)
git checkout -- src/auth.py # legacy equivalent
# Discard ALL unstaged changes in working tree
git restore .
# Unstage a file (move from index back to working tree)
git restore --staged src/auth.py
git reset HEAD src/auth.py # legacy
# Unstage everything
git restore --staged .
git reset — Moving HEAD
Reset moves the branch pointer (and optionally modifies the index/working tree):
| Mode | HEAD | Index | Working Tree | Use case |
|---|---|---|---|---|
--soft | Moves | Unchanged | Unchanged | Undo commit, keep staged |
--mixed | Moves | Reset | Unchanged | Undo commit and unstage (default) |
--hard | Moves | Reset | Reset | Nuke everything to a commit |
# Undo last commit, keep changes staged (soft)
git reset --soft HEAD~1
# Undo last commit, unstage changes, keep in working tree (mixed — default)
git reset HEAD~1
git reset --mixed HEAD~1 # explicit
# Undo last 3 commits and discard all changes completely (hard — DESTRUCTIVE)
git reset --hard HEAD~3
# Reset to a specific commit
git reset --hard abc1234
# After hard reset, check reflog to recover if needed
git reflog
git undo. Always confirm you don't need those changes. On shared branches, prefer git revert instead, which creates a new commit that undoes changes without rewriting history.
git revert — Safe History Undo
# Create a new commit that reverses the changes of abc1234
git revert abc1234
# Revert without immediately committing (stage only)
git revert --no-commit abc1234
git revert -n abc1234
# Revert a range of commits (oldest first)
git revert HEAD~3..HEAD
# Revert a merge commit (must specify which parent is "mainline")
git revert -m 1 abc1234 # -m 1 = keep the first parent's side
ORIG_HEAD
# Git saves the previous HEAD in ORIG_HEAD before risky operations
# (merge, rebase, reset) so you can easily undo them
git merge feature/login
# Something is wrong...
git reset --hard ORIG_HEAD # undo the merge
git rebase main
# Something is wrong...
git reset --hard ORIG_HEAD # undo the rebase
Reflog — Your Safety Net
# The reflog records every movement of HEAD
git reflog
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/login
# def5678 HEAD@{1}: rebase (pick): feat: add JWT validation
# ghi9012 HEAD@{2}: checkout: moving from main to feature/login
# jkl3456 HEAD@{3}: commit: feat: add login form
# Recover a commit that was "lost" after a hard reset or branch deletion
git checkout HEAD@{3} # detached HEAD at the lost commit
git switch -c recovery/lost-work # save it to a branch
# Reflog entries expire after 90 days (configurable)
git config --global gc.reflogExpire 180.days
Advanced History
git bisect — Binary Search for Bugs
Bisect performs a binary search through commit history to find the exact commit that introduced a bug.
# Start bisect session
git bisect start
# Mark current commit as bad (has the bug)
git bisect bad
# Mark a known-good commit (before the bug existed)
git bisect good v1.2.0
# or: git bisect good abc1234
# Git checks out a commit halfway between good and bad
# Run your test, then tell Git the result:
git bisect good # this commit is fine
git bisect bad # this commit has the bug
# Git continues until it identifies the exact culprit
# First bad commit:
# abc1234 feat: refactor database connection pool
# End the session and return to original HEAD
git bisect reset
# Automate with a test script (exit 0 = good, exit 1 = bad)
git bisect run python tests/test_payment.py
git blame — Who Changed This Line?
# Show who last modified each line of a file
git blame src/auth.py
# Show only a range of lines
git blame -L 40,60 src/auth.py
# Ignore whitespace changes
git blame -w src/auth.py
# Show the original commit (follow through moves/renames)
git blame -C src/auth.py # detect moved lines within the file
git blame -CCC src/auth.py # detect from any commit
# Annotate in VS Code
# Right-click in the gutter → "Open File History" (with GitLens)
git cherry-pick — Selective Commit Copying
# Apply a specific commit onto current branch
git cherry-pick abc1234
# Cherry-pick a range of commits (exclusive of first, inclusive of last)
git cherry-pick abc1234..def5678
# Equivalent: abc1234 is excluded; use abc1234^ to include it:
git cherry-pick abc1234^..def5678
# Cherry-pick without auto-committing (stage changes only)
git cherry-pick --no-commit abc1234
git cherry-pick -n abc1234
# If there's a conflict:
# fix the file, then:
git add conflicted_file.py
git cherry-pick --continue
# Abort
git cherry-pick --abort
git shortlog — Contribution Summary
# Summarize commits by author
git shortlog -s -n # -s = count only, -n = sort by count
# 42 Alex
# 18 Alice
# 7 Bob
# Since a tag
git shortlog v1.0.0..HEAD -s -n
Advanced git log
# Show commits that touched a specific string (pickaxe search)
git log -S "database_connection_pool" # string added or removed
git log -G "password.*hash" # regex in diff content
# Track a line range through history
git log -L 40,60:src/auth.py
# Show merge commits only
git log --merges
# Exclude merge commits
git log --no-merges
# Commits in branch-a but not in branch-b
git log branch-b..branch-a
# Commits in either branch but not both (symmetric difference)
git log branch-a...branch-b --left-right
Worktrees
Git worktrees let you check out multiple branches simultaneously in separate directories, sharing the same .git object database. No need to clone the repo again.
Managing Worktrees
# Add a new worktree for an existing branch
git worktree add ../project-hotfix hotfix/payment-crash
# Add a worktree and create a new branch
git worktree add -b feature/new-dashboard ../project-dashboard main
# List all worktrees
git worktree list
# /Users/yi/project abc1234 [main]
# /Users/yi/project-hotfix def5678 [hotfix/payment-crash]
# /Users/yi/project-dashboard (detached HEAD)
# Remove a worktree (the directory must be deleted separately)
git worktree remove ../project-hotfix
# Remove a worktree and its directory
rm -rf ../project-hotfix
git worktree prune # clean up stale worktree records
# Lock a worktree to prevent accidental pruning
git worktree lock ../project-hotfix --reason "active hotfix"
git worktree unlock ../project-hotfix
Use Cases
# Use case 1: Review a PR while keeping your feature branch intact
git worktree add ../review-pr-123 origin/feature/pr-branch
cd ../review-pr-123
# Run tests, read code — main worktree is untouched
# Use case 2: Apply a hotfix without stashing your WIP
git worktree add ../hotfix-work hotfix/critical-bug
cd ../hotfix-work
# Fix the bug, commit, push, merge
cd ../my-project
# Resume feature work exactly where you left off
# Use case 3: Build documentation site for a different branch
git worktree add ../docs-build gh-pages
.git objects and refs. A commit in one worktree is immediately visible in all others. Each worktree has its own index and working tree. You cannot check out the same branch in two worktrees simultaneously.
Submodules & Subtrees
Git Submodules
Submodules embed one Git repository inside another. The outer repo stores a reference to a specific commit in the inner repo.
# Add a submodule
git submodule add https://github.com/org/shared-lib.git lib/shared
# This creates:
# .gitmodules file with the submodule config
# lib/shared/ directory (initially populated)
# A commit object recording the submodule pointer
# Clone a repo with submodules
git clone --recurse-submodules https://github.com/yi/project.git
# Or after cloning:
git submodule init
git submodule update
# Update submodules to their recorded commits after a pull
git submodule update --init --recursive
# Pull latest changes in each submodule
git submodule update --remote --merge
# Run a command in all submodules
git submodule foreach 'git pull origin main'
# Check submodule status
git submodule status
# Remove a submodule (no single command — multi-step process)
git submodule deinit lib/shared
git rm lib/shared
rm -rf .git/modules/lib/shared
The .gitmodules File
# .gitmodules example
[submodule "lib/shared"]
path = lib/shared
url = https://github.com/org/shared-lib.git
branch = main # optional: track a branch instead of pinned commit
Git Subtrees (Alternative to Submodules)
Subtrees merge another repo's history directly into yours. No special knowledge is required to clone — it looks like normal files.
# Add a subtree (merges remote repo into prefix/ subdirectory)
git subtree add --prefix=lib/shared \
https://github.com/org/shared-lib.git main --squash
# Pull updates from the subtree remote
git subtree pull --prefix=lib/shared \
https://github.com/org/shared-lib.git main --squash
# Push local changes to the subtree remote
git subtree push --prefix=lib/shared \
https://github.com/org/shared-lib.git main
| Feature | Submodules | Subtrees |
|---|---|---|
| Cloning complexity | Extra steps (--recurse-submodules) | Normal clone |
| History | Separate repo, pinned commit | Merged into main history |
| Contributing back | Work inside submodule, push there | git subtree push |
| Dependency updates | Explicit submodule update | git subtree pull |
| Best for | Large external dependencies, strict versioning | Internal shared code, simpler workflows |
Hooks
Git hooks are scripts in .git/hooks/ that Git executes automatically at key points in the workflow. They are not committed to the repo by default (but tools like Husky manage this).
Client-Side Hooks
| Hook | Triggered by | Non-zero exit |
|---|---|---|
pre-commit | git commit (before editor) | Aborts commit |
prepare-commit-msg | After default message created | Aborts commit |
commit-msg | After message entered | Aborts commit |
post-commit | After commit created | No effect |
pre-push | Before git push | Aborts push |
pre-rebase | Before git rebase | Aborts rebase |
post-checkout | After git checkout | No effect |
post-merge | After successful merge | No effect |
Hook Script Examples
# .git/hooks/pre-commit (must be executable: chmod +x)
#!/bin/bash
# Run linter before allowing commit
echo "Running lint..."
npm run lint
if [ $? -ne 0 ]; then
echo "Lint failed. Commit aborted."
exit 1
fi
# Run tests
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fi
exit 0
# .git/hooks/commit-msg — enforce conventional commit format
#!/bin/bash
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+\))?: .{1,100}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message does not follow Conventional Commits format"
echo "Expected: type(scope): description"
echo "Example: feat(auth): add OAuth2 login"
exit 1
fi
exit 0
# .git/hooks/pre-push — block force push to main
#!/bin/bash
while read local_ref local_sha remote_ref remote_sha; do
if [[ "$remote_ref" == "refs/heads/main" ]]; then
echo "ERROR: Direct push to main is not allowed."
echo "Please open a Pull Request."
exit 1
fi
done
exit 0
Husky + lint-staged (JavaScript Projects)
# Install
npm install --save-dev husky lint-staged
# Enable Husky
npx husky init
# .husky/pre-commit
npx lint-staged
# package.json
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.py": ["black", "ruff check --fix"]
}
}
Server-Side Hooks
# Server hooks live on the remote (e.g., GitLab, self-hosted Git)
# pre-receive: runs before any refs are updated — can reject a push entirely
# update: runs once per ref being updated
# post-receive: runs after all refs updated — good for CI notifications, deploys
# Example: post-receive that triggers a deploy
#!/bin/bash
while read oldrev newrev refname; do
if [[ "$refname" == "refs/heads/main" ]]; then
echo "Deploying to production..."
cd /var/www/app
git pull origin main
npm install --production
pm2 restart all
fi
done
Git Workflows
Git Flow
A structured branching model for projects with scheduled releases. Uses long-lived main and develop branches plus short-lived feature, release, and hotfix branches.
# Main branches (live forever)
# main — production-ready code, tagged with versions
# develop — integration branch, latest development
# Supporting branches (deleted after merge)
# feature/* — branched from develop, merged back to develop
# release/* — branched from develop, merged to main AND develop
# hotfix/* — branched from main, merged to main AND develop
# Feature workflow
git switch develop
git switch -c feature/user-notifications
# ... work ...
git switch develop
git merge --no-ff feature/user-notifications
git branch -d feature/user-notifications
# Release workflow
git switch develop
git switch -c release/1.3.0
# bump version, last minute fixes
git switch main
git merge --no-ff release/1.3.0
git tag -a v1.3.0 -m "Release 1.3.0"
git switch develop
git merge --no-ff release/1.3.0
git branch -d release/1.3.0
# Hotfix workflow
git switch main
git switch -c hotfix/critical-null-check
# fix the bug
git switch main
git merge --no-ff hotfix/critical-null-check
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git switch develop
git merge --no-ff hotfix/critical-null-check
git branch -d hotfix/critical-null-check
GitHub Flow
A simpler model for continuous delivery. Only main is protected; all work is done in short-lived feature branches that merge directly to main via Pull Requests.
# 1. Create a descriptive branch from main
git switch -c feature/add-oauth-google
# 2. Make commits early and often
git commit -m "feat: add Google OAuth callback endpoint"
# 3. Push and open a Pull Request
git push -u origin feature/add-oauth-google
gh pr create --title "Add Google OAuth login" --base main
# 4. Discuss, review, and iterate
# 5. Deploy from the branch to staging (optional)
# 6. Merge PR into main (squash or merge commit)
# 7. Deploy main to production immediately
# 8. Delete the branch
git branch -d feature/add-oauth-google
git push origin --delete feature/add-oauth-google
Trunk-Based Development
Developers commit directly to main (or very short-lived feature branches). Relies heavily on feature flags, CI, and high test coverage.
# Developers integrate to main multiple times per day
# Feature flags control what users see
# Option A: direct commits to main (small teams)
git switch main
git pull --rebase
# ... small atomic change ...
git commit -m "fix: handle empty cart edge case"
git push
# Option B: short-lived branches (max 1-2 days)
git switch -c fix/empty-cart # branch from main
git commit -m "fix: handle empty cart edge case"
git push -u origin fix/empty-cart
gh pr create # open PR, get quick review
# Merge same day
Workflow Comparison
| Aspect | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| Complexity | High | Low | Medium |
| Release cadence | Scheduled | Continuous | Continuous |
| Long-lived branches | main + develop | main only | main only |
| Good for | Mobile apps, libraries with versioned releases | Web SaaS, small teams | Google/Facebook scale, high CI maturity |
| Hotfix support | Explicit hotfix/ branches | Branch from main, merge quickly | Fix forward or feature flag off |
| Requires | Discipline, git-flow CLI | Good PR culture | Feature flags, strong CI |
Internals
The .git Directory
.git/
├── HEAD # current branch ref or detached commit hash
├── config # repo-local git config
├── description # used by GitWeb (ignore for regular repos)
├── COMMIT_EDITMSG # last commit message
├── MERGE_HEAD # set during a merge
├── ORIG_HEAD # previous HEAD before merge/rebase/reset
├── index # the staging area (binary file)
├── hooks/ # hook scripts
├── info/
│ └── exclude # like .gitignore but not committed
├── objects/ # all Git objects (blobs, trees, commits, tags)
│ ├── pack/ # packfiles for efficient storage
│ └── info/ # index of packs
├── refs/
│ ├── heads/ # local branch refs (one file per branch)
│ ├── remotes/ # remote-tracking refs
│ └── tags/ # tag refs
└── logs/
├── HEAD # reflog for HEAD
└── refs/heads/ # reflog per branch
Inspecting Objects
# Resolve a ref to its full hash
git rev-parse HEAD
git rev-parse main
git rev-parse HEAD~3
git rev-parse v1.0.0^{} # dereference tag to commit
# Inspect any object
git cat-file -t abc1234 # type
git cat-file -p abc1234 # contents
git cat-file -s abc1234 # size in bytes
# List all objects in a tree
git ls-tree HEAD
git ls-tree -r HEAD # recursive (all files)
git ls-tree HEAD src/ # specific directory
# Show the object the staging area points to
git ls-files --stage
Packfiles
# Git periodically packs loose objects into binary packfiles for efficiency
# A packfile uses delta compression: stores differences between similar objects
# Manually trigger garbage collection and packing
git gc
git gc --aggressive # slower, better compression
# Count loose objects and pack size
git count-objects -v
# Verify repository integrity
git fsck
git fsck --full # more thorough check
# Show what's in packfiles
git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -20
# Shows largest objects (useful for finding bloat)
Refspecs
# A refspec maps remote refs to local refs
# Format: [+]<src>:<dst>
# In .git/config after git remote add origin:
# [remote "origin"]
# url = [email protected]:alex/project.git
# fetch = +refs/heads/*:refs/remotes/origin/*
# # ^ ^
# # remote local remote-tracking ref
# # + means force update (fast-forward not required)
# Push local branch to a differently-named remote branch
git push origin feature/login:refs/heads/feature/login-v2
# Fetch a specific remote branch into a local branch
git fetch origin main:refs/remotes/origin/main
# Delete a remote branch using refspec (empty src)
git push origin :feature/old-branch
Common Pitfalls
Force Push Disasters
git push --force on a shared branch.Recovery:
# Teammates who pulled before the force push — get their ORIG_HEAD or reflog
git reflog # find the SHA before the force push
git push origin <lost-sha>:main # restore (if you have push access)
# The victim recovers their own commits:
git fetch origin
git rebase origin/main
Prevention: Configure branch protection rules on GitHub. Always use --force-with-lease. Never --force on shared branches.
Detached HEAD Panic
# You're in detached HEAD and made commits — don't panic
git log --oneline -5 # note the commit hashes
git switch -c rescue/my-work # create a branch right now
# Your commits are saved. Push when ready.
Large Files Committed by Mistake
# Large binary files bloat the repo permanently (they're in history)
# Prevention: always add build artifacts and binaries to .gitignore before first commit
# If you committed a 500MB file and haven't pushed yet:
git reset --soft HEAD~1 # undo commit, keep file staged
git restore --staged largefile.zip
echo "largefile.zip" >> .gitignore
git add .gitignore
git commit -m "chore: ignore large binary artifacts"
# If you've already pushed, use git-filter-repo (modern tool) to scrub history
# WARNING: this rewrites all history — coordinate with entire team
pip install git-filter-repo
git filter-repo --path largefile.zip --invert-paths
# For future large file needs, use Git LFS
git lfs install
git lfs track "*.zip" "*.tar.gz" "*.mp4"
git add .gitattributes
git commit -m "chore: track large files with LFS"
.gitignore Not Working on Already-Tracked Files
# Adding a path to .gitignore has no effect on files already tracked by Git
# You must untrack them first
# Stop tracking a specific file (remove from index, keep on disk)
git rm --cached config/local.py
echo "config/local.py" >> .gitignore
git add .gitignore config/local.py # the gitignore change
git commit -m "chore: stop tracking local config file"
# Stop tracking an entire directory
git rm -r --cached node_modules/
echo "node_modules/" >> .gitignore
git commit -m "chore: stop tracking node_modules"
Credential / Secret Leaks
- Revoke the secret immediately (AWS console, GitHub settings, etc.) — before anything else
- Remove it from history with git-filter-repo
- Force push the cleaned history
- All collaborators must re-clone (their local copies still have the secret)
pip install git-filter-repo
git filter-repo --replace-text <(echo "ACTUAL_SECRET_VALUE==>REDACTED")
git push --force-with-lease origin --all
Prevention: Use pre-commit hooks with gitleaks or GitHub secret scanning. Store secrets in environment variables or a secrets manager, never in code.
Losing Stashed Work
# git stash clear or git stash drop with wrong index destroys stashes
# Recovery is possible if git gc hasn't run yet (objects still exist)
# Find dangling stash commits in reflog
git fsck --unreachable | grep commit | awk '{print $3}' | xargs git log --oneline
# Or look at stash reflog specifically
git log --all --oneline --no-walk $(git stash list | awk '{print $1}' | tr -d :)
# Apply a recovered stash commit
git stash apply abc1234
Merge Conflict Avoidance Strategies
# 1. Keep branches short-lived (merge within 1-3 days)
# 2. Pull/rebase from main frequently
git pull --rebase origin main # do this daily on active branches
# 3. Communicate with teammates when touching shared files
# 4. Use rerere to remember resolutions
git config --global rerere.enabled true
# Git auto-applies remembered resolutions on future conflicts
# 5. Split large files into smaller modules to reduce conflict surface
# 6. Avoid auto-formatting entire files in feature commits (formatting PRs separately)
# Check for upcoming conflicts before merging
git merge --no-commit --no-ff feature/login
# Review conflicts, then abort
git merge --abort
Merge vs Rebase Confusion Reference
| Question | Answer |
|---|---|
| Keep feature branch history visible in main? | git merge --no-ff |
| Linear history in main, clean log? | Rebase feature onto main, then fast-forward merge |
| Update feature branch with main changes? | git rebase main (private) or git merge main (public) |
| Undo a pushed commit safely? | git revert (new commit, safe) |
| Undo last commit locally, keep changes? | git reset --soft HEAD~1 |
| Nuke everything back to a commit? | git reset --hard <sha> (destructive) |
| Which commit broke this? | git bisect |
| Who wrote this line? | git blame |
Quick Reference: Daily Commands
# Morning: sync your branch
git fetch --prune
git rebase origin/main
# Before each commit: review what you're about to commit
git diff --staged
git status -s
# Write a good commit
git add -p # stage hunks selectively
git commit -m "feat: concise description"
# Before pushing: squash WIP commits
git rebase -i origin/main # interactive rebase against upstream
# Push safely
git push --force-with-lease # if you rebased
git push # if linear push
# End of day: stash or WIP commit
git stash push -m "EOD: partial implementation of X"
# or
git commit -m "WIP: do not merge - end of day"