cheat-sheets

Git Recipes

Task-oriented Git recipes: “I did X by mistake, how do I get back?” and “how do I find Y?”. Basics (init, add, commit, branches, remotes) live in Git, GitHub from the terminal in GitHub and the gh CLI, and error-message-led fixes in Git Troubleshooting. Commands here were checked against current Git (2.5x). Lines marked rewrites history change commit IDs, so only do them on commits you have not shared, or coordinate and use --force-with-lease.

Documentation: Git reference, Pro Git book, git-filter-repo

Undo and Fix Commits #

COMMANDDESCRIPTION
git reset --soft HEAD~1Undo the last commit, keep changes staged
git reset HEAD~1Undo the last commit, keep changes unstaged (--mixed, the default)
git reset --hard HEAD~1Undo the last commit and discard the changes
git reset --hard origin/<BRANCH>Throw away all local commits and changes, match the remote
git commit --amendEdit the message and include staged changes in the last commit
git commit --amend --no-editAdd staged changes to the last commit, keep the message
git commit --amend --reset-authorReset author to the current user.name / user.email
git restore <FILE>Discard unstaged changes in a file
git restore --staged <FILE>Unstage a file, keep the edits
git restore --source=<COMMIT> -- <FILE>Bring one file back from another commit
git restore -p <FILE>Discard changes hunk by hunk
git revert <COMMIT>New commit that undoes a commit (safe on shared branches)
git revert <OLD>..<NEW>Revert a range, one revert per commit (-n to combine into one)
git revert -m 1 <MERGE_COMMIT>Undo a merge, keeping parent 1 (the branch you merged into)

After pushing, prefer revert. If you amend or reset a pushed commit on your own branch:

bash
git push --force-with-lease

--force-with-lease refuses if someone else pushed in the meantime, unlike --force.

Reverting a merge: Git then thinks the branch’s changes are already merged, so merging that branch again brings nothing in. To re-merge later, first revert the revert (git revert <REVERT_COMMIT>), then merge.

Committed on the Wrong Branch #

Not pushed yet, work is on main but belongs on a new branch:

bash
git branch feature            # new branch at the current commit
git reset --hard origin/main  # put main back (drops the commits from main only)
git switch feature

Last commit belongs on an existing branch:

bash
git switch <TARGET>
git cherry-pick main          # copy the commit
git switch main
git reset --hard HEAD~1       # remove it from main

Uncommitted work on the wrong branch: git stash, git switch <RIGHT_BRANCH>, git stash pop. Git also carries uncommitted changes across git switch if they do not conflict.

Interactive Rebase, Squash, Fixup #

COMMANDDESCRIPTION
git rebase -i HEAD~4Edit the last four commits
git rebase -i --rootEdit from the very first commit
git rebase -i <BASE>Edit everything after <BASE> (for a branch: git rebase -i main)
git rebase --continue / --abort / --skipResolve and carry on, cancel, or drop the current commit
git commit --fixup <COMMIT>Create a fixup! commit aimed at an older commit
git commit --squash <COMMIT>Same, but lets you edit the message at squash time
git rebase -i --autosquash <BASE>Reorder and mark fixup! / squash! commits automatically
git config --global rebase.autoSquash trueMake --autosquash the default for interactive rebases
git config --global rebase.autoStash trueStash and restore dirty changes around a rebase
git rebase --onto <NEW_BASE> <OLD_BASE> <BRANCH>Move a branch to a new base (for example after its parent was squash-merged)
git rebase main --exec '<TEST_CMD>'Run a command after each commit during the rebase
git merge --squash <BRANCH>Stage the branch’s net change as one commit (you then git commit)

Commands in the todo list:

KEYWORDEFFECT
pickKeep the commit
rewordKeep, edit the message
editStop to amend the commit, then git rebase --continue
squashMeld into the previous commit and combine messages
fixupMeld into the previous commit and discard this message
dropDelete the commit
exec <CMD>Run a shell command at this point

Typical flow to tidy a branch before a PR:

bash
git commit --fixup <COMMIT_TO_FIX>
git rebase -i --autosquash origin/main
git push --force-with-lease

Reorder the lines to reorder the commits. To split a commit, mark it edit, then git reset HEAD~1, commit the pieces separately, and git rebase --continue.

Merge conflicts mid-rebase: fix the files, git add <FILE>, git rebase --continue. To use “ours” and “theirs” correctly note that during a rebase they are swapped: --ours is the branch you are rebasing onto, --theirs is your commit being replayed.

Recover with the Reflog #

The reflog records where HEAD and branches pointed, locally, for about 90 days (30 for unreachable entries by default). Almost nothing is lost after a bad reset --hard, rebase or deleted branch if it was ever committed.

COMMANDDESCRIPTION
git reflogHistory of where HEAD has been
git reflog show <BRANCH>Reflog for one branch
git reset --hard HEAD@{1}Go back to where HEAD was one move ago
git reset --hard <BRANCH>@{2.hours.ago}Go back in time
git branch rescue <SHA>Make a branch from a lost commit, the safest way to recover it
git switch -c <BRANCH> <SHA>Same and switch
git fsck --lost-foundDangling commits and blobs not in any reflog
git fsck --no-reflogs --unreachable | grep commitFind a dropped stash or an orphaned commit

Recover a deleted branch: find its last commit in git reflog, then git branch <NAME> <SHA>. Uncommitted changes that were never staged or stashed cannot be recovered by Git.

Cherry-pick and Patches #

COMMANDDESCRIPTION
git cherry-pick <COMMIT>Apply one commit here
git cherry-pick <A> <B>Several commits
git cherry-pick <A>^..<B>Inclusive range A to B
git cherry-pick -x <COMMIT>Append “(cherry picked from commit …)” to the message
git cherry-pick -n <COMMIT>Apply to the working tree without committing
git cherry-pick -m 1 <MERGE>Pick a merge commit relative to parent 1
git cherry-pick --continue / --abortAfter conflicts / cancel
git format-patch -3Write the last three commits as .patch files
git am < 0001-fix.patchApply patches as commits
git apply --check <FILE.diff>Test whether a diff applies

Bisect #

Binary search for the commit that introduced a bug.

bash
git bisect start
git bisect bad                 # current commit is broken
git bisect good v1.4.0         # last known good tag or commit
# Git checks out the midpoint: test it, then answer
git bisect good                # or: git bisect bad
git bisect reset               # finish and return to where you started

Automate with a script (exit 0 = good, 1-127 except 125 = bad, 125 = cannot test, skip):

bash
git bisect start HEAD v1.4.0
git bisect run ./test.sh
# or inline
git bisect run sh -c 'make && ./check'
git bisect reset

git bisect log shows the steps, git bisect skip skips an untestable commit.

Stash Recipes #

COMMANDDESCRIPTION
git stash push -m "<MESSAGE>"Stash tracked changes with a name
git stash push -uInclude untracked files (-a for ignored files too)
git stash push -pChoose hunks interactively
git stash push -- <PATH>Stash only certain paths
git stash push --keep-indexStash but leave staged changes in place (test the staged state)
git stash listList stashes
git stash show -p stash@{1}Show a stash as a diff
git stash apply stash@{1}Apply and keep the stash
git stash popApply the latest and drop it
git stash drop stash@{1}Delete one
git stash clearDelete all (recoverable only with git fsck)
git stash branch <BRANCH> stash@{0}Create a branch from the stash’s base commit and apply it, good for stashes that now conflict
git checkout stash@{0} -- <FILE>Take one file from a stash
git stash push -m "wip" && git pull --rebase && git stash popUpdate a dirty tree

git stash pop leaves the stash in the list if applying it conflicts, so you do not lose it.

Worktrees #

Check out several branches at once in separate folders sharing one repository (no second clone, no stashing to switch).

COMMANDDESCRIPTION
git worktree add ../<DIR> <BRANCH>New folder with an existing branch
git worktree add -b <NEW_BRANCH> ../<DIR> origin/mainNew folder with a new branch off origin/main
git worktree add --detach ../<DIR> <TAG>Detached HEAD, e.g. to test a release
git worktree listList worktrees
git worktree remove ../<DIR>Delete a clean worktree (--force if dirty)
git worktree pruneForget worktrees whose folder was deleted by hand
git worktree lock ../<DIR>Protect one on removable storage

A branch can be checked out in only one worktree at a time. After a PR merges: git worktree remove ../<DIR> and git branch -d <BRANCH>.

Sparse and Shallow Clones #

COMMANDDESCRIPTION
git clone --depth 1 <URL>Latest commit only, much smaller (--branch <TAG> for a release)
git clone --depth 1 --single-branch -b <BRANCH> <URL>One branch, shallow
git fetch --deepen 50Fetch 50 more commits of history
git fetch --unshallowConvert to a full clone
git clone --filter=blob:none <URL>Partial clone: full history, file contents fetched on demand
git clone --filter=blob:none --sparse <URL>Partial and only the top-level files checked out
git sparse-checkout set <DIR1> <DIR2>Check out only these folders (cone mode)
git sparse-checkout add <DIR>Add another folder
git sparse-checkout listShow the selection
git sparse-checkout disableCheck out everything again

Shallow clones limit git log, blame, bisect and some CI versioning (such as git describe). In GitHub Actions use fetch-depth: 0 for full history. A blob-less partial clone usually works better than --depth when you need history.

Rewrite Author or Email #

COMMANDDESCRIPTION
git commit --amend --reset-author --no-editFix the last commit
git rebase -r --root --exec 'git commit --amend --no-edit --reset-author'Reset author on every commit (set user.name / user.email first). Rewrites history
git rebase -r <BASE> --exec '...'Same, only commits after <BASE>
git filter-repo --mailmap <FILE>Bulk rewrite across the whole repo from a mailmap file
.mailmap fileFix names in git log and shortlog output without rewriting anything

.mailmap and filter-repo --mailmap use this format:

text
Correct Name <correct@example.com> <old@example.com>

git filter-branch still exists but its own docs recommend git filter-repo instead. For a rebase-based rewrite of only your own commits on a branch, force-push afterwards with --force-with-lease.

Remove a Committed Secret #

Step 0: rotate the secret. Treat anything that was ever pushed to a remote as compromised, because clones, forks, CI logs and caches may already hold it. Rewriting history is clean-up, not a substitute for revoking the credential.

Not pushed yet: git commit --amend or interactive rebase (drop or edit the commit), then check with git log -p -S'<SECRET>'.

Already pushed (needs git-filter-repo, apt install git-filter-repo or pip install git-filter-repo), run on a fresh clone (filter-repo refuses non-fresh clones unless you pass --force):

bash
git clone <URL> repo-clean && cd repo-clean

# remove a whole file from all history
git filter-repo --invert-paths --path <PATH/TO/SECRET_FILE>

# or replace a string everywhere (one rule per line in the file)
printf '%s\n' '<SECRET>==>***REMOVED***' > replacements.txt
git filter-repo --replace-text replacements.txt

# filter-repo drops the remote as a safety measure, so re-add it
git remote add origin <URL>
git push --force --all
git push --force --tags

Then:

  • Everyone with a clone must re-clone (or hard-reset and prune). Old clones would otherwise push the secret back.
  • Forks and open pull requests keep the old commits. On GitHub, pull request refs (refs/pull/*) and cached commit URLs can stay reachable after a force-push, so for sensitive data contact GitHub Support to remove cached views and run garbage collection. See GitHub’s “Removing sensitive data from a repository” guide.
  • Remove it from build caches, CI logs, release artifacts and container images too.
  • Add the file to .gitignore, and add a pre-commit scanner such as gitleaks or trufflehog, and enable GitHub secret scanning and push protection.
  • To keep a secret in the repo safely, encrypt it (git-crypt in Git, SOPS, Bitwarden Cheat-Sheet).

Clean Untracked Files #

COMMANDDESCRIPTION
git clean -n -dDry run: list untracked files and folders that would go
git clean -f -dDelete untracked files and folders
git clean -f -d -xAlso delete ignored files (build output, .env)
git clean -f -d -XDelete only ignored files
git clean -iInteractive
git clean -f -d -e '<PATTERN>'Keep files matching a pattern
git restore . && git clean -fdReset the working tree to HEAD (tracked and untracked), irreversible

Always run the dry run (-n) first. git clean cannot be undone.

Find Who or What Changed a Line #

COMMANDDESCRIPTION
git blame <FILE>Last commit per line
git blame -L 40,60 <FILE>Only lines 40 to 60
git blame -w -C -C -C <FILE>Ignore whitespace and detect lines moved or copied between files
git blame --ignore-rev <COMMIT> <FILE>Skip a formatting-only commit
git config blame.ignoreRevsFile .git-blame-ignore-revsSkip every commit listed in that file by default (GitHub honours this file name too)
git log -p --follow -- <FILE>History of a file including renames
git log -L 40,60:<FILE>History of a line range
git log -L :<FUNCTION>:<FILE>History of one function
git log -S'<STRING>' --onelineCommits where the number of occurrences of a string changed (pickaxe)
git log -G'<REGEX>' --onelineCommits whose diff has a line matching a regex
git log -S'<STRING>' -p --allSame, across all branches, with patches
git log --diff-filter=D --summary -- <PATH>Which commit deleted a file
git log --all -- <PATH>History of a path even if deleted
git log --author='<NAME>' --since='2 weeks ago'Filter by person and date
git log --grep='<TEXT>' -iSearch commit messages
git show <COMMIT>:<PATH>A file as of an old commit
git describe --tags <COMMIT>Nearest tag
git branch --contains <COMMIT> / git tag --contains <COMMIT>Which branches or tags include it

-S finds when a string was added or removed, -G finds changes to lines that match a pattern (useful when the count is unchanged). See Regex for the pattern syntax.

Compare Branches #

COMMANDDESCRIPTION
git log main..feature --onelineCommits on feature not on main
git log --left-right --oneline main...featureCommits unique to each side (< main, > feature)
git diff main...featureChanges on feature since it branched (merge base), which is what a PR shows
git diff main..featureDirect difference between the two tips
git diff --stat main...featureSummary of files changed
git diff --name-only main...featureChanged file names only
git range-diff main old-feature new-featureCompare two versions of a rebased series
git cherry -v main featureWhich commits are already upstream by patch (+ not yet, - yes)
git branch --merged main / --no-merged mainBranches fully merged into main or not
git merge-base main featureCommon ancestor
git diff <A> <B> -- <PATH>One file between two commits or branches

Delete merged local branches:

bash
git branch --merged main | grep -vE '^\*|main|master' | xargs -r git branch -d
git fetch --prune

Squash-merged branches do not count as merged to Git, so git branch -d refuses; check with gh pr list --head <BRANCH> --state all and then use -D.

Tags and Releases #

COMMANDDESCRIPTION
git tag -a v1.2.0 -m "Release 1.2.0"Annotated tag (use these for releases)
git tag -s v1.2.0 -m "Release 1.2.0"Signed tag
git tag v1.2.0 <COMMIT>Tag an older commit
git push origin v1.2.0Publish one tag
git push --follow-tagsPush commits plus the annotated tags that point at them
git push origin --delete v1.2.0Delete a remote tag
git tag -d v1.2.0Delete a local tag
git tag -l 'v1.*' --sort=-v:refnameList by version, newest first
git describe --tags --abbrev=0Latest tag reachable from here
git fetch --tags --forceUpdate tags that moved on the remote
git log v1.1.0..v1.2.0 --onelineChangelog between two tags
gh release create v1.2.0 --generate-notesGitHub release with auto notes (see GitHub and the gh CLI)
gh release create v1.2.0 --notes-file CHANGELOG.md ./dist/*Release with notes and assets
gh release create v1.2.0 --target <BRANCH>Create the tag from a branch if it does not exist yet

Avoid moving published tags. If you must: git tag -f v1.2.0 <COMMIT> then git push --force origin v1.2.0.

Submodules #

COMMANDDESCRIPTION
git submodule add <URL> <PATH>Add a submodule (commit .gitmodules and the new path)
git clone --recurse-submodules <URL>Clone with submodules
git submodule update --init --recursiveFetch submodules in an existing clone
git submodule update --remote --merge <PATH>Move a submodule to the latest of its tracked branch
git submodule statusShow checked-out commits (+ out of date, - not initialised)
git submodule foreach 'git pull origin main'Run a command in each
git config --global submodule.recurse trueMake pull, checkout and similar recurse into submodules
git diff --submoduleShow submodule changes as commit lists

Remove one:

bash
git submodule deinit -f <PATH>
git rm -f <PATH>
rm -rf .git/modules/<PATH>
git commit -m "Remove submodule <PATH>"

The parent repo records a commit SHA for each submodule, so after git pull run git submodule update --init --recursive (or set submodule.recurse).

Quality-of-life Config #

Set once with git config --global <KEY> <VALUE>; they live in ~/.gitconfig.

SETTINGEFFECT
push.autoSetupRemote truegit push on a new branch sets upstream automatically (Git 2.37+)
pull.ff onlygit pull refuses to create merge commits (or use pull.rebase true)
fetch.prune trueDrop deleted remote branches on every fetch
rebase.autoSquash trueHonour fixup! commits in rebase -i
rebase.autoStash trueStash dirty changes around a rebase
rerere.enabled trueRemember conflict resolutions and replay them
merge.conflictStyle zdiff3Show the base version in conflict markers (Git 2.35+)
diff.algorithm histogramBetter diffs for moved code
branch.sort -committerdateMost recent branches first in git branch
tag.sort -version:refnameTags in version order
column.ui autoColumns in git branch and git tag output
help.autocorrect promptSuggest and ask when you mistype a command (Git 2.46+)
init.defaultBranch mainName for new repos
core.editor "code --wait"Editor for commit messages and rebases

Aliases:

ini
[alias]
    st = status -sb
    co = switch
    br = branch -vv
    lg = log --oneline --graph --decorate --all
    last = log -1 --stat
    unstage = restore --staged
    amend = commit --amend --no-edit
    undo = reset --soft HEAD~1
    wip = !git add -A && git commit -m 'wip'
    fp = push --force-with-lease
    gone = !git branch -vv | awk '/: gone]/{print $1}'

Alias with ! runs in the shell. Create one on the command line: git config --global alias.lg "log --oneline --graph --decorate --all".

Sign Commits with an SSH Key #

Needs Git 2.34 or newer. Reuse your existing SSH key (see OpenSSH Cheat-Sheet).

bash
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true

# local verification: list who is allowed to sign
echo "$(git config user.email) $(cut -d' ' -f1,2 ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
COMMANDDESCRIPTION
git commit -S -m "msg"Sign one commit explicitly
git log --show-signature -1Show signature status
git log --format='%h %G? %an'G good, B bad, N unsigned, E cannot check
git verify-commit <COMMIT>Verify one commit
git config user.signingkey "key::ssh-ed25519 AAAA..."Inline key, or use ssh-agent with a key file path
git rebase --exec 'git commit --amend --no-edit -n -S' -i <BASE>Sign existing commits (rewrites history)

For the “Verified” badge on GitHub, add the same public key under Settings > SSH and GPG keys with the key type Signing Key (authentication and signing are separate entries even for one key). Same on GitLab and Gitea (GitLab and the glab CLI, Gitea (and Forgejo)). The allowed_signers file is only for local verification.

Forks and Upstream #

bash
gh repo fork <OWNER>/<REPO> --clone          # fork and clone, sets origin (fork) and upstream
cd <REPO>
git remote -v                                # check; add by hand if missing:
git remote add upstream https://github.com/<OWNER>/<REPO>.git

Keep a fork up to date:

bash
git fetch upstream
git switch main
git merge --ff-only upstream/main            # or: git rebase upstream/main
git push origin main

Or in one go on GitHub: gh repo sync <YOUR_USER>/<REPO> (branch of the fork; --force to overwrite a diverged branch).

Contribute:

bash
git fetch upstream
git switch -c fix-typo upstream/main         # branch off the latest upstream
# ...commit...
git push -u origin fix-typo
gh pr create --repo <OWNER>/<REPO> --fill    # PR from your fork to the upstream repo
COMMANDDESCRIPTION
gh pr checkout <NUMBER>Check out a PR (including from a fork) locally
gh pr checkout <NUMBER> -b <LOCAL_BRANCH>Under a name of your choice
git fetch origin pull/<NUMBER>/head:pr-<NUMBER>Same without gh
gh pr create --repo <OWNER>/<REPO> --head <USER>:<BRANCH>PR from an explicit fork branch
git branch -u upstream/main mainTrack upstream with local main
git remote rename origin forkRename a remote
gh repo set-default <OWNER>/<REPO>Pick which repo gh commands target
gh pr merge <NUMBER> --squash --delete-branchMerge from the CLI (see GitHub and the gh CLI)

After a squash merge, update your branch base with git fetch upstream && git rebase --onto upstream/main <OLD_BASE> <BRANCH> or just delete the branch and start fresh.

Search all sheets

Tip: / or Ctrl/Cmd+K to open, Esc to close.