Git
.mdVerified against git 2.43.0, flags verified via `git <cmd> -h` and tested against a scratch repo in /tmp (worktree, bisect, sparse-checkout, blame, rebase all exercised live; submodule syntax confirmed via `git submodule -h` — the sandboxed environment blocks the `file://` transport needed to fully exercise `submodule add`), 2026-08-21 · official docs
The daily-driver subset of Git — 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.
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)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. Prefer it for branch operations.
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 stateNever 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 commitgit 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.
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 fileCherry-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-pickUndoing 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)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.
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 handA 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 fromgit bisect does a binary search across the commit range, so 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.
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 submoduleA 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)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#
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 checkoutCone 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 commitHooks 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).