# Git Cheat Sheet

> **Tool:** Git
> **Category:** Version Control
> **Verified against:** git 2.43.0 (local), flags verified via `git <cmd> -h` and tested against a scratch repo
> (worktree, bisect, sparse-checkout, blame, rebase all exercised live); current-release context (2.55.0 as of
> mid-2026, Git 3.0 targeted for late 2026 with SHA-256/reftable/Rust) cross-checked via WebSearch, 2026-08-29
> **Official docs:** https://git-scm.com/docs

## What it is and where it fits 🎯

Git is the distributed version control system underneath essentially every modern software workflow — every
clone is a full copy of the repository's history, not a thin client talking to a central server, which is
what makes branching, rebasing, and offline work cheap and fast compared to older centralized systems (SVN,
CVS). This page is the daily-driver subset — branching, rebasing, stashing, filtering history, diffing, and
recovering from mistakes with the reflog — not the full manpage, just the commands that actually come up while
working.

> [!NOTE]
> **Git 3.0 is targeted for late 2026** (SHA-256 as the default object hash instead of SHA-1, a Rust
> requirement for the build, and the `reftable` ref storage format claiming dramatically faster fetch/push on
> repos with many refs). None of that changes the commands on this page — it's a forward-looking note, not
> something to plan around yet, since ecosystem-wide SHA-256 support (GitHub, GitLab, Bitbucket) is still the
> long pole.

## The three-tree model, in one diagram ⚙️

```mermaid
flowchart LR
    WD["Working Directory<br/>(files you edit)"] -->|"git add"| IDX["Staging Area / Index<br/>(what the next commit will include)"]
    IDX -->|"git commit"| REPO["Repository<br/>(committed history)"]
    REPO -->|"git checkout / restore"| WD

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef ok fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    class WD info
    class REPO ok
```

Almost every "undo" command on this page is really about moving content between these three trees —
`restore` moves working-directory content, `reset` moves the staging area (and optionally the working
directory too), and `revert`/a new commit is the only one that changes the repository's history by *adding*
rather than rewriting.

## Branching

Create, switch, rename, and clean up branches.

```bash
git branch                          # list local branches
git branch -a                       # list local + remote-tracking branches
git branch --show-current           # print the current branch name
git switch -c feature/new-thing     # create and switch to a new branch
git switch main                     # switch to an existing branch
git branch -m old-name new-name     # rename a branch
git branch -d feature/done          # delete a branch (only if merged)
git branch -D feature/abandoned     # force-delete a branch (even if not merged)
```

> [!TIP]
> **`git switch` is the modern, safer replacement for `git checkout <branch>`** — it only touches branches, so
> it can't accidentally discard file changes the way `checkout` can (which historically overloaded branch
> switching and file restoration into one command, a frequent source of "I didn't mean to lose that change").
> Prefer `switch` for branch operations and `restore` (below) for file operations — `checkout` still works but
> carries the old, more dangerous, overloaded behavior.

## Stashing changes

Shelve work-in-progress without committing it.

```bash
git stash                           # stash tracked changes
git stash push -u -m "wip: auth"    # stash including untracked files, with a message
git stash list                      # show all stashes
git stash show -p stash@{0}         # view a stash's diff
git stash pop                       # apply the most recent stash and drop it
git stash apply stash@{1}           # apply a specific stash without dropping it
git stash drop stash@{1}            # delete a specific stash
git stash branch new-branch stash@{0}  # create a branch from a stash (useful after a conflict)
```

`stash pop` fails and leaves the stash in place if applying it produces a conflict — resolve the conflict,
then `git stash drop` manually.

## Rebasing

Rewrite a branch's history onto a new base, or clean it up before merging.

```bash
git rebase main                     # replay current branch's commits onto main
git rebase -i HEAD~5                # interactively squash/reorder/reword the last 5 commits
git rebase --onto main old-base feature   # move a branch to a different base commit
git rebase --continue               # after resolving a conflict mid-rebase
git rebase --skip                   # skip the commit currently causing a conflict
git rebase --abort                  # bail out and restore the branch to its pre-rebase state
```

> [!WARNING]
> **Never rebase a branch other people have already pulled** — it rewrites commit hashes, so anyone with the
> old history will get diverged/duplicate commits on their next pull. Rebase local/unshared branches only.

## Viewing history

Filter and format commit history for what you're actually looking for.

```bash
git log --oneline --graph --decorate    # compact visual history
git log --author="jane"                 # commits by a specific author
git log --since="2 weeks ago" --until="yesterday"   # commits in a date range
git log --grep="fix"                    # commits whose message matches a pattern
git log -- path/to/file.py              # history of a single file
git log -p -2                           # full diff for the last 2 commits
git log --stat -1                       # files changed + line counts for the last commit
```

> [!TIP]
> **`git log -S"someFunction"` (the "pickaxe") finds commits that changed the *number of times* a string
> appears** — useful for finding when a specific line of code was introduced or removed, which plain
> `--grep` (message text only) can't do. `-G"regex"` is the related variant that matches any diff line
> containing a regex match, not just a change in occurrence count — reach for `-G` when you want every commit
> that touched a pattern, `-S` when you specifically want the commit that added or removed it.

## Diffing

```bash
git diff                            # unstaged changes vs the index
git diff --staged                   # staged changes vs HEAD
git diff main..feature              # diff between two branches
git diff HEAD~3 HEAD                # diff between two points in history
git diff --stat                     # summary (files + line counts) instead of full diff
git diff -- path/to/file.py         # diff limited to one file
git diff --word-diff                 # word-level diff instead of line-level — much more readable for prose/docs changes
```

## Cherry-picking

```bash
git cherry-pick <commit-sha>        # apply a single commit onto the current branch
git cherry-pick -n <commit-sha>     # apply the changes but don't auto-commit
git cherry-pick --continue          # after resolving a conflict mid-cherry-pick
git cherry-pick --abort             # bail out of an in-progress cherry-pick
git cherry-pick A..B                 # apply a range of commits (exclusive of A, inclusive of B)
```

## Undoing changes and recovering with the reflog 🔍

```bash
git restore --staged path/to/file.py    # unstage a file (keep the changes)
git restore path/to/file.py             # discard unstaged changes to a file
git reset --soft HEAD~1                 # undo the last commit, keep changes staged
git reset --mixed HEAD~1                # undo the last commit, keep changes unstaged (default mode)
git reset --hard HEAD~1                 # undo the last commit and discard the changes entirely
git revert <commit-sha>                 # create a new commit that undoes a prior commit (safe for shared history)
git reflog                              # show a log of everywhere HEAD has pointed, including "lost" commits
git reset --hard HEAD@{2}               # recover to a state from the reflog (e.g. before a bad reset/rebase)
```

> [!IMPORTANT]
> **`reset --hard` is destructive to your working tree, but it is not destructive to the *repository*** —
> every commit it seems to throw away still exists and is recoverable via `git reflog` until git's garbage
> collector eventually prunes unreferenced commits (default ~90 days for reflog entries). If you ever do a
> `reset --hard` or a rebase you regret, `git reflog` is the first thing to check, not a last resort.

> [!TIP]
> For a branch already pushed and pulled by others, use `git push --force-with-lease` instead of
> `git push --force` after a rebase — it aborts the push if the remote has commits you haven't seen yet,
> preventing you from silently clobbering someone else's work.

## Working in multiple branches at once with worktrees

```bash
git worktree add ../repo-hotfix -b hotfix/urgent   # new branch, checked out in a sibling directory
git worktree add ../repo-review existing-branch    # check out an existing branch into a new worktree
git worktree list                                  # show every worktree linked to this repo
git worktree remove ../repo-hotfix                 # remove a worktree (must be clean, or add -f)
git worktree prune                                 # clean up admin files for worktrees deleted by hand
```

A worktree lets you have two branches checked out simultaneously from the same repository — e.g. keep `main`
building in one directory while you work on a feature in another — without the `stash`/`switch`/`stash pop`
dance. All worktrees share the same `.git` object store, so commits, branches, and tags are visible across all
of them immediately.

## Finding the commit that introduced a bug with bisect

```bash
git bisect start                    # begin a bisect session
git bisect bad HEAD                 # mark the current commit as broken
git bisect good v1.4.0              # mark a known-good commit/tag
# git checks out a commit halfway between good and bad — test it, then:
git bisect good                     # this commit is fine, keep searching later commits
git bisect bad                      # this commit is broken, keep searching earlier commits
git bisect reset                    # done — return to the branch/commit you started from
```

> [!TIP]
> **`git bisect` does a binary search across the commit range** — a range of ~1000 commits takes about 10
> steps, not 1000. For a bug with an automatable repro (a failing test, a script that exits non-zero), skip
> the manual good/bad loop entirely with `git bisect run ./test-script.sh` — it drives the whole search for
> you and stops on the first bad commit, unattended.

## Working with submodules

```bash
git submodule add https://github.com/org/lib.git vendor/lib   # add a submodule at a path
git submodule status                                            # show each submodule's checked-out commit
git clone --recurse-submodules <repo-url>                       # clone a repo and its submodules together
git submodule update --init --recursive                         # populate submodules after a plain clone
git submodule update --remote vendor/lib                        # pull the submodule's latest tracked-branch commit
git submodule foreach 'git status'                               # run a command inside every submodule
```

A submodule pins the parent repo to one exact commit of the child repo, not a branch — `git submodule update`
alone checks that commit back out even if the child repo has moved on. `--remote` is what actually advances
the pin to the latest upstream commit; you still need to `git add` and commit the resulting pointer change in
the parent repo afterward.

## Interactive rebase in depth

```bash
git rebase -i HEAD~5                # open the last 5 commits in an editor as a todo list
git rebase -i --autosquash HEAD~5   # auto-reorder fixup!/squash! commits next to their targets
git commit --fixup <commit-sha>     # create a fixup commit, paired with --autosquash above
git rebase --exec "make test" -i HEAD~5   # run a command after each commit as it's replayed
GIT_SEQUENCE_EDITOR=true git rebase -i HEAD~5   # non-interactively accept the default todo (scripting)
```

The interactive todo list supports more than `pick`/`squash`/`reword`/`drop`: `edit` pauses on that commit so
you can amend it or split it into several with `git reset HEAD^` + re-committing, and `exec` runs an arbitrary
shell command between commits (useful for making sure every intermediate commit still builds).
`--autosquash` combined with `git commit --fixup` is the standard workflow for "amend an earlier commit"
without hand-editing the todo list — create the fixup commit, then let autosquash reorder and squash it into
place.

## Blaming a file to find who/when changed a line

```bash
git blame path/to/file.py                    # annotate every line with its introducing commit
git blame -L 40,60 path/to/file.py            # limit to a line range
git blame -L :funcName path/to/file.py        # limit to a specific function's lines
git blame -w path/to/file.py                  # ignore whitespace-only changes when attributing lines
git blame --ignore-rev <commit-sha> path/to/file.py   # skip a noisy commit (e.g. a mass reformat)
```

> [!TIP]
> A large reformat or auto-fix commit ruins `blame`'s usefulness for every line it touches. Fix this
> permanently by adding the reformat commit's SHA to a `.git-blame-ignore-revs` file and configuring
> `git config blame.ignoreRevsFile .git-blame-ignore-revs` — `blame` then skips straight past it to the real
> authorial commit, and GitHub/GitLab respect the same file in their web blame views.

## Checking out only part of a large repo with sparse-checkout

```bash
git clone --filter=blob:none --sparse <repo-url>   # clone without downloading file contents yet
cd <repo> && git sparse-checkout init --cone         # enable cone mode (fast, directory-based)
git sparse-checkout set services/api services/web    # only these directories are checked out to disk
git sparse-checkout add services/shared              # add another directory to the working set
git sparse-checkout disable                          # go back to a full checkout
```

Cone mode (the default since Git 2.25+) restricts sparse-checkout to whole directories, which is both faster
and far less error-prone than the old pattern-based mode — use it unless you have a specific need for
file-level glob patterns. Pairing `--filter=blob:none` on the clone with sparse-checkout is what actually
saves bandwidth and disk: the filter skips downloading file contents outside your sparse set, not just
skipping them from the working tree.

## Hooks basics

```bash
ls .git/hooks/                       # every hook Git supports ships here as a *.sample file
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
git config core.hooksPath .githooks  # point Git at a repo-tracked hooks directory instead
git commit --no-verify                # skip commit-time hooks (pre-commit, commit-msg) for one commit
```

Hooks in `.git/hooks/` are local-only and never cloned with the repo — every teammate has to install them by
hand, which is why real projects instead commit a hooks directory (e.g. `.githooks/`) and point Git at it with
`core.hooksPath`, or use a wrapper tool like `pre-commit` or `husky` that manages installation. The most
commonly used hooks are `pre-commit` (runs before a commit is created — linting, formatting checks),
`commit-msg` (validates the message itself — e.g. enforcing Conventional Commits), and `pre-push` (runs before
`git push` — a last gate, like running the test suite).

## Real-world scenario: recovering from a bad interactive rebase

An interactive rebase that goes wrong (a botched conflict resolution, an accidentally dropped commit) is one
of the most anxiety-inducing git moments — and one of the most recoverable, precisely because of the reflog:

```bash
git reflog                           # find the entry from right before the rebase started, e.g. "HEAD@{8}: rebase (start)"
git reset --hard HEAD@{8}             # restore the branch to exactly that state
```

> [!CAUTION]
> This works because `rebase` never actually deletes commits — it creates new ones and moves the branch
> pointer, leaving the old commits dangling but still present in the object database until garbage collection.
> The reflog is what remembers where the branch pointer was at every step, which is why `git reflog` should be
> the very first move after any rebase/reset that went wrong, before trying to manually reconstruct anything.

## Real-world scenario: bisecting a regression with an automated repro

A performance regression was introduced sometime in the last 200 commits, and there's a benchmark script that
exits non-zero when the regression is present:

```bash
git bisect start
git bisect bad HEAD
git bisect good v2.1.0
git bisect run ./scripts/benchmark-check.sh
# git bisect run drives the whole search, checking out and testing each candidate commit automatically
git bisect reset
```

At ~200 commits, a manual bisect would take roughly 8 rounds of "checkout, test by hand, report good/bad" —
`bisect run` does the same 8 rounds unattended in the time it takes the script to execute each time, which is
the difference between a five-minute investigation and a half-day one on a slow-to-build project.

## Common pitfalls

- **Rebasing a shared branch** — see the WARNING above; this is the single most damaging git mistake possible
  short of an actual history-destroying `push --force`.
- **Panicking after a bad `reset --hard`/rebase instead of checking `git reflog` first** — see the CAUTION
  above; almost everything is recoverable for ~90 days.
- **Using `git checkout <branch>` out of habit** instead of `git switch` — not wrong, but carries the risk of
  the old overloaded file-restoration behavior; `switch`/`restore` are the more precise modern tools.
- **`git submodule update` without `--remote`** and expecting the submodule to have advanced — it only
  checks out the pinned commit, never moves the pin forward on its own.

## When to reach for something else

Git itself has no opinion on branching strategy, PR workflow, or commit-message convention — those are policy
layered on top (see this site's CI/CD & Delivery content for GitOps and branching-strategy coverage). For very
large monorepos where even sparse-checkout isn't enough, some organizations reach for a purpose-built
monorepo tool (Bazel's remote caching, or a virtual filesystem like Microsoft's VFS for Git) rather than
stretching plain Git further — a genuinely different scale of problem than what this page covers.
