Git Common Fixes
Scenario-driven fixes for the Git situations that actually show up — undoing commits, recovering "lost" work, rewriting history safely, and untangling conflicts. Each section stands alone; jump to whichever one matches your mess.
1. reset --soft / --mixed / --hard
flowchart LR
subgraph soft["--soft"]
S1[HEAD moves] --> S2[Index unchanged]
S2 --> S3[Working tree unchanged]
end
subgraph mixed["--mixed (default)"]
M1[HEAD moves] --> M2[Index reset]
M2 --> M3[Working tree unchanged]
end
subgraph hard["--hard"]
H1[HEAD moves] --> H2[Index reset]
H2 --> H3[Working tree reset]
end
| Mode | HEAD | Index (staged) | Working tree |
|---|---|---|---|
--soft |
✅ moves | unchanged | unchanged |
--mixed |
✅ moves | ✅ reset | unchanged |
--hard |
✅ moves | ✅ reset | ✅ reset ⚠️ |
git reset --soft HEAD~1 # undo commit, keep staged
git reset --mixed HEAD~1 # undo commit + unstage, keep files
git reset --hard HEAD~1 # undo commit + discard all changes
You want to undo the last commit but keep its changes staged, ready to re-commit. Which reset mode do you use, and why not --hard?
--soft. It only moves HEAD, leaving the index and working tree
untouched, so everything from the undone commit stays staged.
--hard also resets the index and working tree, discarding the
actual file changes, not just the commit record.
2. Amend Last Commit
# Fix commit message only
git commit --amend -m "correct message"
# Add forgotten file
git add forgotten.go
git commit --amend --no-edit # keeps existing message
# Already pushed? Must force push
git push --force-with-lease
You already pushed a commit, then ran git commit --amend to fix its message. Why does a plain git push fail afterward?
git push --force-with-lease instead.
3. Recover Deleted Branch with Reflog
flowchart TD
A[Branch deleted] --> B[git reflog]
B --> C["Find commit SHA<br/>e.g. abc1234"]
C --> D[git checkout -b recovered abc1234]
D --> E[Branch restored]
git reflog # find the tip commit of deleted branch
git checkout -b recovered abc1234 # recreate branch at that SHA
# or
git branch recovered abc1234
Reflog expires after 90 days by default.
Step through the recovery:
git reflog. It lists everywhere
HEAD has recently pointed, including the tip of the branch you just
deleted.
abc1234.
git checkout -b recovered
abc1234 (or git branch recovered abc1234) points a
new ref at that SHA.
You deleted a branch by mistake five minutes ago. Is the work actually gone?
git reflog still has the tip commit's SHA, so
git checkout -b recovered <sha> gets it back, as long
as you're within the reflog's default 90-day expiry window.
4. Interactive Rebase
git rebase -i HEAD~4 # rewrite last 4 commits
In the editor:
pick a1b2c3 first commit
squash d4e5f6 fixup for first # merge into previous
reword 7g8h9i bad message # edit message
edit j0k1l2 needs splitting # pause to amend
drop m3n4o5 mistake # delete entirely
# During edit pause:
git add .
git commit --amend
git rebase --continue
# Abort at any time:
git rebase --abort
git rebase -i HEAD~4 opens an
editor listing the last 4 commits, oldest first, each prefixed
pick.
squash folds a commit into the one above it,
reword just edits the message, edit pauses to
let you amend the commit's contents, drop removes it
entirely.
reword stops briefly for a new message and
moves on by itself.
edit. Make your changes,
git add ., git commit --amend, then
git rebase --continue to resume.
git rebase --abort at any point
rewinds everything as if you'd never started.
What's the difference between reword and edit in an interactive rebase plan?
reword only pauses to edit that commit's message — fix
it and the rebase carries on by itself. edit pauses the whole
rebase at that commit so you can change its actual contents, and you have
to run git add ., git commit --amend, and
git rebase --continue yourself to move forward.
5. Cherry-pick
git cherry-pick abc1234 # apply single commit
git cherry-pick abc1234 def5678 # apply multiple commits
git cherry-pick abc1234..def5678 # apply a range (exclusive start)
git cherry-pick abc1234^..def5678 # apply a range (inclusive start)
# Conflict during cherry-pick:
git add .
git cherry-pick --continue
# or bail:
git cherry-pick --abort
abc1234 up through def5678.
abc1234 itself is not replayed.
^ backs up one commit before
abc1234, so the range now includes abc1234
itself through def5678.
Why might git cherry-pick abc1234..def5678 leave out a commit you expected to see applied?
abc1234 itself is not replayed, only commits after it up to
def5678. Use abc1234^..def5678 to include
abc1234 too.
6. git bisect
git bisect start
git bisect bad # current commit is broken
git bisect good v1.2.0 # this tag was working
# Git checks out midpoint — test it, then:
git bisect good # or: git bisect bad
# Repeat until bisect prints the first bad commit
git bisect reset # exit bisect mode
# Automate with a test script (exit 0 = good, non-zero = bad):
git bisect run ./test.sh
git bisect start begins the
session.
git bisect
bad.
git bisect good
v1.2.0 — any older commit or tag you know worked.
git bisect good or git bisect bad.
git bisect reset leaves bisect
mode and returns you to where you started.
What does git bisect run ./test.sh expect from your script's exit code to decide good vs bad?
0 means the commit is good. Any non-zero exit code
means it's bad — the same convention as any shell script's success
status.
7. Detached HEAD
Detached HEAD = HEAD points to a commit SHA, not a branch ref.
git checkout abc1234 # → detached HEAD
# Save work before checking out something else:
git checkout -b save-detached-work # create branch from current position
# or tag it:
git tag temp-save
If you already left without saving, find it in reflog:
git reflog | head -20 # look for "checkout: moving from"
git checkout -b recovered <sha>
You checked out a specific commit SHA, made a few commits, then checked out main without saving your work first. Is it lost?
git reflog still recorded them — look for a
"checkout: moving from" entry in git reflog | head -20, then
git checkout -b recovered <sha> to get it back.
8. Remove Committed Secrets
# Install git-filter-repo (preferred over filter-branch)
pip install git-filter-repo
# Remove a specific file from all history
git filter-repo --path secrets.env --invert-paths
# Replace a secret string everywhere in history
git filter-repo --replace-text <(echo "AKIAIOSFODNN7EXAMPLE==>REMOVED")
# After rewriting history, force push ALL branches
git push --force --all
git push --force --tags
Also: revoke the secret immediately. Assume it's compromised. Remove from all forks.
filter-branch.
--path ... --invert-paths, or scrub a
string wherever it appears with --replace-text.
git push --force
--all and git push --force --tags — every
commit after the rewrite has a new SHA, so every branch and tag needs
updating on the remote.
You've successfully scrubbed a secret from all of Git history with filter-repo and force-pushed everywhere. Is the incident over?
9. Merge Conflict Resolution
git merge feature-branch
# CONFLICT in src/handler.go
# Option A: use a mergetool
git mergetool # opens vimdiff / VS Code / etc.
# Option B: manual edit
# In the file, resolve between <<<<<<< HEAD and >>>>>>> feature-branch
git add src/handler.go
git merge --continue # or: git commit
# Abort entirely:
git merge --abort
# Useful during conflict resolution:
git diff # see all conflicts
git checkout --ours path # accept our version
git checkout --theirs path # accept their version
git mergetool opens a configured diff tool (vimdiff, VS
Code, etc.) that walks you through each conflict side by side. Once
you've resolved everything in the tool, git add the file
and git merge --continue.
<<<<<<< HEAD and
>>>>>>> feature-branch by hand, then
git add the file and git merge --continue (or
git commit).
What's the difference between git checkout --ours path and manually editing between the <<<<<<</>>>>>>> markers?
--ours/--theirs takes one whole side of the
conflict for that file, discarding the other side entirely. Manually
editing between the markers lets you keep pieces of both, resolving
line by line instead of picking a whole side.
10. Stash
git stash # stash tracked changes
git stash -u # include untracked files
git stash -m "wip: auth fix" # named stash
git stash list # show all stashes
git stash show -p stash@{1} # diff of a stash
git stash pop # apply latest + remove from list
git stash apply stash@{2} # apply specific, keep in list
git stash drop stash@{1} # delete specific stash
git stash clear # delete all stashes ⚠️
# Stash only staged changes:
git stash --staged
After git stash pop, is the stash still in your list to apply again on another branch?
pop applies and removes it in one step. To apply
the same stash to more than one branch, use git stash apply
instead, which keeps it in the list until you explicitly git stash
drop it.