The advanced Git Cheat Sheet
TLDR; Get The Advanced Git Cheat Sheet as a and by email. Use it as a quick reference for stash, rebase, cherry-pick, tags, bisect, reflog, worktrees, submodules and safe history recovery, then follow the detailed examples below.
Download The Advanced Git Cheat Sheet

Request the complete printable PDF and full-resolution PNG. We will send both files directly to your inbox.
What Should You Know Before Starting?
The Advanced Git Cheat Sheet continues the Ultimate Git Cheat Sheet. Read that guide first if you need an introduction to repositories, commits, branches, merges or remotes. Every example below uses the same the-ultimate-git-cheat-sheet project on GitHub.
Clone the project once before following the examples. Each section creates its own tutorial branch, files and commits before it demonstrates the advanced command, so you do not need to invent any missing state.
$ git clone https://github.com/aichbauer/the-ultimate-git-cheat-sheet.git
$ cd the-ultimate-git-cheat-sheet
# short version: git status -s
$ git status --short
## => output is empty because the working tree is clean
The output and short commit hashes shown below are illustrative. Your hashes, author details and dates will differ.
The examples inspect changes before committing them. Use git diff for unstaged changes in tracked files. After git add, use git diff --staged to inspect the version stored in the staging area, including the complete content of a newly added file.
What Is Git Stash?
The stash temporarily stores uncommitted changes and returns your working directory to a clean state. It is useful when you must switch branches before unfinished work is ready for a meaningful commit.
A stash is not a replacement for commits. Stashes are local, easy to forget and not shared when you push.
How Do You Save And Inspect A Stash?
# create an isolated branch and a tracked change to stash
# short version: git switch -c tutorial/stash main
$ git switch --create tutorial/stash main
$ printf '\nDocument remote-tracking branches here.\n' >> branches.md
# inspect the changed path and its unstaged patch before storing it
$ git status --short
## => output
### M branches.md
$ git diff -- branches.md
# store the tracked change with a useful description
# short version: git stash push -m "Draft remote notes"
$ git stash push --message "Draft remote notes"
## => output
### Saved working directory and index state On tutorial/stash: Draft remote notes
$ git status --short
## => output is empty because the working tree is clean
# create another tracked change and a new, untracked file
$ printf '\nSee tags.md for release tags.\n' >> README.md
$ printf '# Release Tags\n' > tags.md
# status includes both paths, while diff shows the tracked change
$ git status --short
## => output
### M README.md
### ?? tags.md
$ git diff -- README.md
# include the untracked file in this second stash
# short version: git stash push -u -m "Draft tags page"
$ git stash push --include-untracked --message "Draft tags page"
## => output
### Saved working directory and index state On tutorial/stash: Draft tags page
$ git status --short
## => output is empty because the working tree is clean
# list saved stashes
$ git stash list
## => output
### stash@{0}: On tutorial/stash: Draft tags page
### stash@{1}: On tutorial/stash: Draft remote notes
# inspect tracked and untracked changes in the newest stash
# short version: git stash show -u -p stash@{0}
$ git stash show --include-untracked --patch stash@{0}
## => output
### diff --git a/README.md b/README.md
### index 1f2e3d4..5a6b7c8 100644
### --- a/README.md
### +++ b/README.md
### @@ -3,3 +3,5 @@
### A practical reference for everyday Git commands.
###
### See commands.md for useful Git commands.
### +
### +See tags.md for release tags.
### diff --git a/tags.md b/tags.md
### new file mode 100644
### index 0000000..e01bcc9
### --- /dev/null
### +++ b/tags.md
### @@ -0,0 +1 @@
### +# Release Tags
git status --short reports tracked and untracked paths. An ordinary git diff shows unstaged changes to tracked files, but it does not show the content of a new untracked file such as tags.md. The --include-untracked option makes the stash inspection include that file.
How Do You Restore Or Delete A Stash?
# apply a stash but keep it in the list
$ git stash apply stash@{1}
## => output
### On branch tutorial/stash
### Changes not staged for commit:
### modified: branches.md
$ git status --short
## => output
### M branches.md
# apply the newest stash and remove it after success
$ git stash pop
## => output
### On branch tutorial/stash
### Changes not staged for commit:
### modified: README.md
### modified: branches.md
###
### Untracked files:
### tags.md
###
### Dropped refs/stash@{0} (f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d)
$ git status --short
## => output
### M README.md
### M branches.md
### ?? tags.md
$ git diff -- README.md branches.md
# delete one stash without applying it
$ git stash drop stash@{0}
## => output
### Dropped stash@{0} (1a2b3c4d5e6f7081928374655647382910abcdef)
# return this tutorial branch to a clean state
$ git restore README.md branches.md
$ rm tags.md
$ git status --short
## => output is empty because the working tree is clean
$ git switch main
# short version: git branch -D tutorial/stash
$ git branch --delete --force tutorial/stash
Applying a stash can create conflicts. Resolve them like merge conflicts, then stage the corrected files.
What Does Undoing Changes In Git Mean?
Undoing a change can mean restoring a file, removing it from the staging area, reversing a commit or moving a local branch to an earlier commit. The correct command depends on where the change currently lives.
How Do You Undo Changes In Git?
Start with git status --short. Its two-character code shows where each change currently lives: the first column represents the staging area and the second represents the working directory. For example, M README.md is modified but not staged, M README.md is staged and ?? notes.txt is an untracked file.
Git usually colors an unstaged M in the second column red and a staged M in the first column green. Therefore, git add README.md moves the M from the red, second-column position ( M README.md) to the green, first-column position (M README.md). git restore --staged README.md moves it back from the staged green position to the unstaged red position. Terminal themes and Git color settings can change the actual colors, so use the column position as the reliable indicator.
Choose the undo command that matches that state:
- Use
git restore <file>to discard unstaged changes in a tracked file. This permanently replaces the working copy with Git's saved version. - Use
git restore --staged <file>to remove a change from the staging area without discarding the file's contents. - Use
git commit --amendto update your latest local commit before other people depend on it. Amending replaces that commit with a new commit and hash. - Use
git revert <commit>to undo a committed change while preserving the existing history. Revert records the undo operation as another commit. - An untracked file is not yet stored by Git, so
git restorecannot recover it. Inspect it carefully before removing it withgit cleanor another file-removal command.
How Do You Restore A File Or Unstage It?
git restore works differently depending on whether you include --staged. Without that option, it discards unstaged changes in a tracked file. With --staged, it only removes changes from the staging area and keeps the edited contents in your working directory.
# create a branch and an unstaged change first
$ git switch --create tutorial/restore main
$ printf '\nTemporary introduction.\n' >> README.md
$ git status --short
## => output
### M README.md
$ git diff -- README.md
# now discard the unstaged change in that tracked file
# WARNING: the discarded file changes cannot normally be recovered by Git
$ git restore README.md
$ git status --short
## => output is empty because the working tree is clean
# create and stage another change first
$ printf '\nAdd a quick-start section.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
# now remove it from the staging area but keep it in the working tree
# short version: git restore -S README.md
$ git restore --staged README.md
$ git status --short
## => output
### M README.md
$ git diff -- README.md
# discard the remaining working-directory change before the next example
$ git restore README.md
$ git status --short
## => output is empty because the working tree is clean
The first git restore README.md discards the temporary introduction. The later git restore --staged README.md does not discard the quick-start text: it moves the M from the first column to the second column. In a normally colored terminal, the staged green M becomes an unstaged red M. The edit remains in the working directory until the final git restore README.md discards it.
How Do You Amend The Latest Commit?
Amend replaces the latest commit with a corrected version. Use it only for your own local commit before other people depend on that commit and its hash.
# create an isolated branch and commit a change
$ git switch --create tutorial/amend main
$ printf '\nAdd a quick-start section.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
# short version: git commit -m "Add quick-start note"
$ git commit --message "Add quick-start note"
$ printf '\nKeep the quick start concise.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
# update the latest commit and reuse its existing message
$ git commit --amend --no-edit
## => output
### [tutorial/amend c3d4e5f] Add quick-start note
### 1 file changed, 4 insertions(+)
The amended commit keeps the message Add quick-start note, but it now includes both the quick-start section and its correction. Here, --no-edit means "do not open the commit-message editor". It does not mean "make no changes": Git still adds the staged correction and replaces the original commit with a new commit and hash.
How Do You Revert A Commit?
Revert keeps an existing commit in history and creates a new commit that reverses its changes. This example first commits a new file and then reverts that commit.
# create an isolated branch for the revert example
$ git switch --create tutorial/revert main
# create and commit a new file
$ printf '# Restore Examples\n' > restore-examples.md
$ git add restore-examples.md
$ git status --short
## => output
### A restore-examples.md
$ git diff --staged -- restore-examples.md
$ git commit --message "Add restore examples"
## => output
### [tutorial/revert d4e5f6a] Add restore examples
### 1 file changed, 1 insertion(+)
### create mode 100644 restore-examples.md
The latest commit is now Add restore examples. Git calls the currently checked-out commit HEAD, so HEAD refers to that commit at this moment. The next command reverses the changes introduced by HEAD without deleting it from the history.
# reverse the latest commit and accept Git's generated message
$ git revert --no-edit HEAD
## => output
### [tutorial/revert e6f7a8b] Revert "Add restore examples"
### 1 file changed, 1 deletion(-)
### delete mode 100644 restore-examples.md
# show that the original and reversing commits both remain in history
$ git log --oneline --decorate --max-count=2
## => output
### e6f7a8b (HEAD -> tutorial/revert) Revert "Add restore examples"
### d4e5f6a Add restore examples
# confirm that no uncommitted changes remain
$ git status --short
## => output is empty because the working tree is clean
The first commit adds restore-examples.md. The second commit deletes that file, which reverses the first commit's effect. After git revert finishes, HEAD points to the new Revert "Add restore examples" commit, while the original Add restore examples commit remains directly below it in the log. In this command, --no-edit skips the message editor and accepts Git's generated Revert "Add restore examples" message. The revert still creates a new commit.
git revert is normally the safest choice for shared history because it reverses changes without removing existing commits.
How Do You Reset Local Commits?
Reset moves a branch to another commit. The mode decides what happens to the staging area and working directory.
# start from main and create a commit that changes a tracked file
$ git switch --create tutorial/reset main
$ printf '\nA reset example.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Add reset example"
# move HEAD back one commit and keep that change staged
$ git reset --soft HEAD~1
$ git status --short
## => output
### M README.md
# recreate the commit before trying the next reset mode
$ git commit --message "Add reset example"
# move HEAD back one commit and keep the change unstaged
$ git reset HEAD~1
$ git status --short
## => output
### M README.md
$ git diff -- README.md
# recreate the commit once more before demonstrating --hard
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Add reset example"
# DANGER: move the branch and discard the committed tracked change
$ git reset --hard HEAD~1
## => output
### HEAD is now at 0f687a1 Merge branch retention guidance
$ git status --short
## => output is empty because the working tree is clean
Do not use reset to rewrite commits that colleagues already use.
How Do You Safely Remove Untracked Files?
git clean removes files that are not tracked by Git. Always run it with --dry-run first so that you can review exactly what the real command would delete.
# create a disposable untracked file first
$ git switch --create tutorial/clean main
$ printf 'temporary notes\n' > notes.txt
$ git status --short
## => output
### ?? notes.txt
# preview untracked files without deleting anything
# short version: git clean -n
$ git clean --dry-run
## => output
### Would remove notes.txt
# only after checking the preview, delete the file
# short version: git clean -f
$ git clean --force
## => output
### Removing notes.txt
$ git status --short
## => output is empty because the working tree is clean
The --dry-run option previews the deletion, while --force authorizes it. Their short forms are -n and -f. An untracked file has never been committed, so Git may have no copy from which to restore it. If the preview contains anything you want to keep, move it elsewhere, add and commit it or stash it with git stash --include-untracked before cleaning.
How Do You Safely Push Rewritten History?
Rebase, commit amendment and reset can change commit hashes. If you intentionally rewrite your own unpublished branch after pushing it, a normal push is rejected because the histories no longer match.
Disclaimer: Never force-push rewritten history to a shared or protected branch without coordinating with everyone affected. Even --force-with-lease changes remote history and can disrupt work based on the commits you replace.
The push examples require your own writable fork. Replace <your-fork-url> with its HTTPS or SSH clone URL; do not use the original repository URL unless you maintain that repository.
# create a branch and publish it to a fork you can write to
$ git switch --create tutorial/force-push main
$ printf '\nForce-with-lease example.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Add force-with-lease example"
$ git remote add fork <your-fork-url>
# short version: git push -u fork tutorial/force-push
$ git push --set-upstream fork tutorial/force-push
# rewrite the branch locally
$ git commit --amend --message "Document force-with-lease"
# update your view of the fork before replacing its branch
$ git fetch fork
# replace the remote branch only if it still matches your known version
$ git push --force-with-lease fork tutorial/force-push
Prefer --force-with-lease over --force. A force push replaces the remote branch even when someone else has added commits. The lease adds a safety check: the push succeeds only when the remote branch is still at the commit you expect. If a colleague pushed work that you have not incorporated, Git rejects your push instead of overwriting it. Fetch and review the new remote commits before deciding how to proceed.
What Is A Rebase In Git?
A rebase takes commits from one line of history and replays their changes on a new base. The resulting commits have new parent relationships and new hashes.
Merging joins histories and may add a merge commit. Rebasing creates a linear history as if the work had started from the newer base. Both can be correct. Merge is usually safer for shared history; rebase is useful for cleaning your own local feature branch before sharing it.
Imagine that main and a feature branch have both moved forward from commit B:
main: A---B---C
\
feature: D---E
Commits D and E belong to the feature branch, while C is the newest commit on main.
What Does A Merge Do?
A merge preserves both lines of history and joins them. When Git creates a merge commit, the result looks like this:
main: A---B---C-------M
\ /
feature: D---E----/
The merge commit M has both C and E as parents. The original commits D and E keep their hashes because Git has not rewritten them. The graph also preserves the point at which the branches diverged.
What Does A Rebase Do?
A rebase takes the changes introduced by D and E and replays them after C:
Before rebase:
main: A---B---C
\
feature: D---E
After rebase:
main: A---B---C---D'---E'
D' contains the changes from D, and E' contains the changes from E. However, they are new commits with different parents and hashes. The resulting history is linear and looks as if the feature work had started after C.
Disclaimer: As a general rule, never rewrite commits that have already been pushed to a remote branch. Other people may have based their work on those commits, and rewriting them can cause conflicts, duplicate changes or lost work. Only rewrite remote history when your team has explicitly coordinated the change and everyone affected understands how to recover.
For example, a colleague may still have the original commits while the remote branch contains the rebased replacements:
colleague: A---B---D---E
remote: A---B---C---D'---E'
Although D and D' may introduce the same change, Git sees them as different commits. Combining these histories can cause duplicate changes and confusing conflicts. As a rule of thumb, rebase your own unpublished feature branch when you want a clean history, but prefer merging once other people depend on its commits.
How Do You Rebase And Clean Up Git History?
Rebase a private feature branch onto its new base, resolve any conflicts and optionally use interactive rebase to revise recent local commits.
How Do You Rebase A Feature Branch?
# create two feature commits from main
$ git switch --create tutorial/rebase main
$ printf '\nPrefer git switch for branch changes.\n' >> commands.md
$ git add commands.md
$ git status --short
## => output
### M commands.md
$ git diff --staged -- commands.md
$ git commit --message "Add switch tip"
$ printf '\nDelete merged branches when they are no longer needed.\n' >> branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
$ git commit --message "Add branch cleanup tip"
# create a separate base branch from main and move it forward
$ git switch --create tutorial/rebase-base main
$ printf '\nAdvanced examples continue in the companion guide.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Link advanced examples"
# inspect both lines of history before rebasing
$ git switch tutorial/rebase
$ git log --oneline --graph --decorate --all --max-count=4
## => output
### * b7c8d9e (tutorial/rebase-base) Link advanced examples
### | * d4e5f6a (HEAD -> tutorial/rebase) Add branch cleanup tip
### | * a1b2c3d Add switch tip
### |/
### * 0f687a1 (origin/main, origin/HEAD, main) Merge branch retention guidance
# replay the feature commits on the newer local base
$ git rebase tutorial/rebase-base
## => output
### Successfully rebased and updated refs/heads/tutorial/rebase.
# inspect the rewritten branch history
$ git log --oneline --graph --decorate --all --max-count=4
## => output
### * f6a7b8c (HEAD -> tutorial/rebase) Add branch cleanup tip
### * c3d4e5f Add switch tip
### * b7c8d9e (tutorial/rebase-base) Link advanced examples
### * 0f687a1 (origin/main, origin/HEAD, main) Merge branch retention guidance
Before the rebase, the | characters show that tutorial/rebase and tutorial/rebase-base have diverged from main. After the rebase, the graph is a single line: Git recreated the two feature commits after tutorial/rebase-base, so those commits have new hashes.
How Do You Resolve Rebase Conflicts?
If a conflict appears, edit the file, stage it and continue. You do not normally create the conflict-resolution commit yourself during a rebase.
# create two branches that edit the same line differently
$ git switch --create tutorial/rebase-conflict main
$ printf '# Branching Guide\n\nKeep branches focused and short-lived.\n' > branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
$ git commit --message "Recommend short-lived branches"
$ git switch --create tutorial/rebase-conflict-base main
$ printf '# Branching Guide\n\nDelete merged branches after review.\n' > branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
$ git commit --message "Clarify branch cleanup"
# start the rebase to produce a real content conflict
$ git switch tutorial/rebase-conflict
$ git rebase tutorial/rebase-conflict-base
## => output
### CONFLICT (content): Merge conflict in branches.md
### error: could not apply 1a2b3c4... Recommend short-lived branches
# resolve the conflict, stage the corrected file and continue
$ printf '# Branching Guide\n\nKeep branches focused, short-lived and delete them after review.\n' > branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
# continue the rebase; Git may open an editor for the commit message
$ git rebase --continue
## => output
### Successfully rebased and updated refs/heads/tutorial/rebase-conflict.
git rebase --continue may open your configured text editor so you can confirm or change the rebased commit's message. If you want to keep the existing message, leave the text unchanged and save and close the editor. Git continues the rebase only after the editor closes successfully.
- In Vim, press
iif you want to edit the message. When finished, pressEsc, type:wqand pressEnter. If you do not want to edit the message, skipiand useEsc,:wq, thenEnter. - In Nano, edit the message directly, press
Ctrl+Oto write it, pressEnterto confirm the filename and pressCtrl+Xto exit. - In VS Code, edit the message, save with
Cmd+Son macOS orCtrl+Son Windows and Linux, then close the commit-message tab or window.
At the conflict shown above, git rebase --skip would omit the stopped commit instead. git rebase --abort would cancel the whole rebase and restore tutorial/rebase-conflict to its original state. Choose one of --continue, --skip or --abort; they are alternative endings to the same stopped rebase.
How Do You Edit Commits With Interactive Rebase?
Interactive rebase lets you edit recent local history.
# create three local commits to edit
$ git switch --create tutorial/interactive-rebase main
$ printf '# Stash Notes\n' > stash-notes.md
$ git add stash-notes.md
$ git status --short
## => output
### A stash-notes.md
$ git diff --staged -- stash-notes.md
$ git commit --message "Add stash notes"
$ printf '\nUse descriptive stash messages.\n' >> stash-notes.md
$ git status --short
## => output
### M stash-notes.md
$ git diff -- stash-notes.md
# short version: git commit -am "Add stash message tip"
$ git commit --all --message "Add stash message tip"
$ printf '\nInspect a stash before applying it.\n' >> stash-notes.md
$ git status --short
## => output
### M stash-notes.md
$ git diff -- stash-notes.md
$ git commit --all --message "Add stash inspection tip"
# now edit those three unpublished commits
# short version: git rebase -i HEAD~3
$ git rebase --interactive HEAD~3
# in the editor:
# pick = keep the commit
# reword = change its message
# squash = combine it with the previous commit
# drop = remove it
# after saving and completing the rebase, inspect the rewritten history
$ git log --oneline --graph --decorate --max-count=4
Because rebase changes commit hashes, review the graph and run tests afterwards.
What Is Cherry-Pick In Git?
Cherry-pick applies the change introduced by one existing commit and creates a new commit on your current branch. It is useful when a small fix must be copied without merging the complete source branch.
It also duplicates the change in history. Do not cherry-pick a long sequence of commits when a merge or rebase would express the relationship better.
How Do You Cherry-Pick A Commit?
<cherry-pick-hash> is a placeholder, not a literal commit hash. Run the shown git log command, copy the hash from the first column of its output and replace <cherry-pick-hash> with that value.
# create a source branch and the fix that will be copied
$ git switch --create tutorial/branch-tip main
$ printf '\nUse git branch --merged before deleting branches.\n' >> branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
$ git commit --message "Add branch cleanup tip"
# show the newest commit on the source branch and copy its hash
$ git log --max-count=1 --oneline --decorate tutorial/branch-tip
## => output
### 7a8b9c0 (HEAD -> tutorial/branch-tip) Add branch cleanup tip
# create the target branch from main
$ git switch --create tutorial/cherry-pick main
## => output
### Switched to a new branch 'tutorial/cherry-pick'
$ printf '\nCherry-pick selected fixes into this branch.\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Prepare cherry-pick target"
# replace the placeholder with the hash copied from git log
$ git cherry-pick <cherry-pick-hash>
## => output
### [tutorial/cherry-pick 0d1e2f3] Add branch cleanup tip
### 1 file changed, 2 insertions(+), 1 deletion(-)
What Are Git Tags?
A branch points to the latest commit in an ongoing line of work and moves forward as new commits are added. A tag is a permanent label for one specific commit, often used for releases such as v1.0.0.
A lightweight tag is only a name. An annotated tag stores a message, tagger and date, and can be signed. Annotated tags are the better default for releases.
How Do You Create And Share Git Tags?
# create a release branch and a release commit first
$ git switch --create tutorial/release main
$ printf '\nRelease: 1.0.0\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Prepare version 1.0.0"
# create an annotated release tag at HEAD
# short version: git tag -a v1.0.0 -m "Release version 1.0.0"
$ git tag --annotate v1.0.0 --message "Release version 1.0.0"
# list and inspect tags
# short version: git tag -l
$ git tag --list
## => output
### v1.0.0
$ git show v1.0.0
## => output
### tag v1.0.0
### Tagger: Ada Lovelace <ada@example.com>
### Date: Thu Aug 6 09:00:00 2026 +0200
###
### Release version 1.0.0
###
### commit b7c8d9e (HEAD -> tutorial/release, tag: v1.0.0)
### Author: Ada Lovelace <ada@example.com>
### Date: Wed Aug 5 11:00:00 2026 +0200
###
### Prepare version 1.0.0
# add your writable fork and push one tag; a normal push does not push every local tag
$ git remote add release-fork <your-fork-url>
$ git push release-fork v1.0.0
# delete a local tag
# short version: git tag -d v1.0.0
$ git tag --delete v1.0.0
## => output
### Deleted tag 'v1.0.0' (was 4f5a6b7)
# delete the remote tag
# short version: git push -d release-fork v1.0.0
$ git push release-fork --delete v1.0.0
Avoid moving a published release tag. Create a new version when the released content changes.
What Is Git Bisect?
git bisect uses binary search to find the commit that introduced a bug. You identify one bad commit and one older good commit. Git checks out a commit halfway between them; after every good or bad answer, it discards half of the remaining candidates.
How Do You Find A Bug With Git Bisect?
You can test each selected commit yourself or let Git run a test command automatically until it identifies the first bad commit.
How Do You Test Commits Manually?
This walkthrough creates the lightweight tag bisect-good at the known-good commit. The tag gives that endpoint a memorable name, so you do not need to copy its hash into each later command. It is only a convenience: git bisect good accepts a revision, so you can use the full commit hash or a unique abbreviated hash instead, for example git bisect good a1b2c3d. An existing release tag also works if you know that release did not contain the bug.
# create a history with a known-good commit and a later regression
$ git switch --create tutorial/bisect main
$ printf 'safe\n' > mode.txt
$ git add mode.txt
$ git status --short
## => output
### A mode.txt
$ git diff --staged -- mode.txt
$ git commit --message "Add safe mode"
$ git tag bisect-good
$ printf 'safe\ninclude examples\n' > mode.txt
$ git status --short
## => output
### M mode.txt
$ git diff -- mode.txt
$ git commit --all --message "Add mode examples"
$ printf 'broken\ninclude examples\n' > mode.txt
$ git status --short
## => output
### M mode.txt
$ git diff -- mode.txt
$ git commit --all --message "Introduce mode regression"
$ printf '\nbisect these changes\n' >> README.md
$ git add README.md
$ git status --short
## => output
### M README.md
$ git diff --staged -- README.md
$ git commit --message "Document mode behavior"
# start the search and mark the current commit as broken
$ git bisect start
## => output
### status: waiting for both good and bad commits
$ git bisect bad
## => output
### status: waiting for good commit(s), bad commit known
# mark the known working tag
$ git bisect good bisect-good
## => output
### Bisecting: 0 revisions left to test after this (roughly 1 step)
### [3c4d5e6] Introduce mode regression
# inspect the checked-out version, then answer good or bad
$ cat mode.txt
## => output
### broken
### include examples
$ git bisect bad
## => output
### Bisecting: 0 revisions left to test after this (roughly 0 steps)
### [5d6e7f8] Add mode examples
# this earlier commit still has safe mode, so mark it good
$ cat mode.txt
## => output
### safe
### include examples
$ git bisect good
## => output
### 3c4d5e6 is the first bad commit
### commit 3c4d5e6
### Author: Ada Lovelace <ada@example.com>
### Date: Fri Aug 7 14:00:00 2026 +0200
###
### Introduce mode regression
# after Git identifies the first bad commit, leave bisect mode
$ git bisect reset
## => output
### Previous HEAD position was 5d6e7f8 Add mode examples
### Switched to branch 'tutorial/bisect'
How Do You Automate Bisect With A Test?
The test that exposes a regression often does not exist in the older commits. In that case, write a small reproduction script and keep it unchanged throughout the search. The script does not need to be tracked by Git: it can be an untracked file in the repository or live outside it, such as /tmp/bisect-regression.sh. Keeping it outside the repository avoids conflicts if a historical commit contains a file at the same path.
Run the script against both endpoints before starting. It must exit with 0 for the known-good commit and a value between 1 and 127, except 125, for the known-bad commit. Exit code 125 tells Git that a commit cannot be tested and should be skipped.
The printf command below writes a two-line script. The %s\n format prints each following string on its own line, while > creates or replaces /tmp/bisect-regression.sh with that output. The first line, #!/bin/sh, tells the operating system to run the file with the system shell. The second line quietly searches mode.txt for a line whose entire contents are broken: -q suppresses normal output, while ^ and $ anchor the match to the beginning and end of the line. The leading ! reverses the result, so finding broken produces exit status 1 for a bad commit, while not finding it produces 0 for a good commit. This example can make that assumption because mode.txt exists at every commit between the chosen endpoints.
# create a fixed test that succeeds for good commits and fails for bad ones
$ printf '%s\n' '#!/bin/sh' '! grep -q "^broken$" mode.txt' > /tmp/bisect-regression.sh
$ chmod +x /tmp/bisect-regression.sh
# verify that it succeeds at the good endpoint
# short version: git switch -d bisect-good
$ git switch --detach bisect-good
$ /tmp/bisect-regression.sh
## => output
### no output; exit status 0 means good
# verify that it fails at the bad endpoint
$ git switch tutorial/bisect
$ /tmp/bisect-regression.sh
## => output
### no output; exit status 1 means bad
$ git bisect start HEAD bisect-good
## => output
### Bisecting: 0 revisions left to test after this (roughly 1 step)
### [3c4d5e6] Introduce mode regression
$ git bisect run /tmp/bisect-regression.sh
## => output
### running '/tmp/bisect-regression.sh'
### running '/tmp/bisect-regression.sh'
### 3c4d5e6 is the first bad commit
### bisect found first bad commit
$ git bisect reset
## => output
### Previous HEAD position was 5d6e7f8 Add mode examples
### Switched to branch 'tutorial/bisect'
# remove the temporary test script after the search
$ rm /tmp/bisect-regression.sh
A clean working tree is strongly recommended because Git must check out many commits; tracked changes can block those checkouts, while untracked build artifacts can affect the result.
What Is Git Reflog?
The reflog records where local references such as HEAD have pointed. It can help after an accidental reset, rebase or deleted branch, even when the commit no longer appears in git log.
How Do You Recover Lost Work With Git Reflog?
# create a commit that will deliberately become unreachable from its branch
$ git switch --create tutorial/reflog main
$ printf '# Recovery Notes\n' > recovery-notes.md
$ git add recovery-notes.md
$ git status --short
## => output
### A recovery-notes.md
$ git diff --staged -- recovery-notes.md
$ git commit --message "Add recovery notes"
## => output
### [tutorial/reflog e6f7a8b] Add recovery notes
$ lost_commit=$(git rev-parse HEAD)
# remove that commit from the branch before looking for it in the reflog
$ git reset --hard HEAD~1
# inspect recent movements of HEAD
$ git reflog
## => output
### 0f687a1 (HEAD -> tutorial/reflog, main) HEAD@{0}: reset: moving to HEAD~1
### e6f7a8b HEAD@{1}: commit: Add recovery notes
### 0f687a1 HEAD@{2}: checkout: moving from tutorial/bisect to tutorial/reflog
# inspect a candidate before restoring it
$ git show "$lost_commit"
## => output
### commit e6f7a8b
### Author: Ada Lovelace <ada@example.com>
### Date: Sat Aug 8 09:30:00 2026 +0200
###
### Add recovery notes
# create a recovery branch without changing current work
$ git branch recovery/lost-work "$lost_commit"
Reflogs are local and expire. They are a safety net, not a backup strategy.
What Is Git Worktree?
A worktree gives one repository multiple working directories. You can keep your feature open in one directory while checking an urgent fix in another, without repeatedly stashing and switching.
How Do You Work On Multiple Branches With Git Worktree?
# keep the primary working directory on main
$ git switch main
# create a new branch based on main
$ git branch hotfix/branch-wording main
# check out the new branch in another working directory
$ git worktree add ../the-ultimate-git-cheat-sheet-hotfix hotfix/branch-wording
## => output
### Preparing worktree (checking out 'hotfix/branch-wording')
### HEAD is now at 0f687a1 Merge branch retention guidance
# list every worktree
$ git worktree list
## => output
### /Users/ada/the-ultimate-git-cheat-sheet 0f687a1 [main]
### /Users/ada/the-ultimate-git-cheat-sheet-hotfix 0f687a1 [hotfix/branch-wording]
# work in the second directory
$ cd ../the-ultimate-git-cheat-sheet-hotfix
$ printf '\nKeep branch names short and descriptive.\n' >> branches.md
$ git add branches.md
$ git status --short
## => output
### M branches.md
$ git diff --staged -- branches.md
$ git commit --message "Clarify branch naming"
# after committing the change, return and remove the extra working directory
$ cd ../the-ultimate-git-cheat-sheet
$ git worktree remove ../the-ultimate-git-cheat-sheet-hotfix
The same branch cannot normally be checked out in two worktrees at once. Remove or move valuable files before removing a worktree with uncommitted changes.
What Are Git Submodules?
A submodule lets one repository record a specific commit from another repository. The parent stores a reference, not the complete child history in its own history.
Submodules can be useful for an independently versioned dependency, but they add workflow complexity. A submodule directory can look present while its content is not initialized, and updating the child repository does not automatically update the reference in the parent.
How Do You Work With Git Submodules?
Add or update a submodule when the parent repository should reference another repository, and follow the complete cleanup process when removing one.
How Do You Add And Update A Submodule?
# create an isolated branch for the submodule example
$ git switch --create tutorial/submodule main
# add the Ultimate Git Cheat Sheet itself as a nested reference for this demonstration
$ git submodule add https://github.com/aichbauer/the-ultimate-git-cheat-sheet.git vendor/git-reference
$ git status --short
## => output
### A .gitmodules
### A vendor/git-reference
$ git diff --staged -- .gitmodules vendor/git-reference
$ git commit --message "Add Git reference submodule"
## => output
### [tutorial/submodule 4a5b6c7] Add Git reference submodule
### 2 files changed, 4 insertions(+)
### create mode 100644 .gitmodules
### create mode 160000 vendor/git-reference
# clone a project and initialize all submodules in one step
$ cd ..
# short version: git clone --recurse-submodules -b tutorial/submodule the-ultimate-git-cheat-sheet ultimate-git-with-submodule
$ git clone --recurse-submodules --branch tutorial/submodule the-ultimate-git-cheat-sheet ultimate-git-with-submodule
# make a normal clone as well, then initialize its submodule separately
$ git clone --branch tutorial/submodule the-ultimate-git-cheat-sheet ultimate-git-normal-clone
$ cd ultimate-git-normal-clone
$ git submodule update --init --recursive
# fetch and check out the configured remote branch for each submodule
$ git submodule update --remote --recursive
How Do You Remove A Submodule?
To remove a submodule, deinitialize it first, then remove its tracked path and commit the change.
# short version: git submodule deinit -f vendor/git-reference
$ git submodule deinit --force vendor/git-reference
## => output
### Cleared directory 'vendor/git-reference'
### Submodule 'vendor/git-reference' (https://github.com/aichbauer/the-ultimate-git-cheat-sheet.git) unregistered for path 'vendor/git-reference'
# short version: git rm -f vendor/git-reference
$ git rm --force vendor/git-reference
## => output
### rm 'vendor/git-reference'
$ git status --short
## => output
### M .gitmodules
### D vendor/git-reference
$ git diff --staged -- .gitmodules vendor/git-reference
$ git commit --message "Remove Git reference submodule"
## => output
### [tutorial/submodule 8d9e0f1] Remove Git reference submodule
### 2 files changed, 4 deletions(-)
### delete mode 160000 vendor/git-reference
What Did You Learn?
You can now temporarily store unfinished changes, reshape your own unpublished history, copy individual commits, mark releases and find the commit that introduced a bug. You also know how reflog, worktrees and submodules solve less common Git problems.
Use these tools deliberately. Inspect the repository state before rewriting history, avoid rebasing commits other people already use, and create a recovery branch before a destructive operation.
Return to the Ultimate Git Cheat Sheet for the everyday Git workflow and command reference.
Join Our CommunityYou liked this article? Share it with your colleagues and friends.
Sign up for our newsletter!
Do not miss out on our latest tips, guides, and updates – sign up for our newsletter now! We promise to only send you the most relevant and useful information.
By clicking subscribe, you agree to the privacy policy. You can unsubscribe at any time by clicking the link in the footer of our emails.
