Verified16 commandsAI-assisted

Git

.md

Verified against git 2.43.0 (local), flags verified via `git <cmd> -h` and tested against a scratch repo · official 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 ⚙️#

Diagram

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.

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.

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.

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.

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#

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#

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 🔍#

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#

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#

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#

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#

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#

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-revsblame 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#

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#

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:

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:

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.