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.

0/0 checks

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?


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?


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:

1. Branch deleted. The ref pointing at your work is gone, but the commit it pointed to is still findable — it's not erased just because nothing points at it anymore.
2. Run git reflog. It lists everywhere HEAD has recently pointed, including the tip of the branch you just deleted.
3. Find the commit SHA. Look for the entry from just before the deletion, e.g. abc1234.
4. Recreate the branch. git checkout -b recovered abc1234 (or git branch recovered abc1234) points a new ref at that SHA.
5. Branch restored. Everything is back — but only if you do this before the reflog entry expires (90 days by default).

You deleted a branch by mistake five minutes ago. Is the work actually gone?


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
1. Start. git rebase -i HEAD~4 opens an editor listing the last 4 commits, oldest first, each prefixed pick.
2. Edit the plan. Change the verbs on any line: 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.
3. Git replays top to bottom. Each line's action runs in order — reword stops briefly for a new message and moves on by itself.
4. Pause on edit. Make your changes, git add ., git commit --amend, then git rebase --continue to resume.
5. Done, or bail out. The rebase finishes once the last line is replayed. 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?


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
Exclusive start — applies every commit after abc1234 up through def5678. abc1234 itself is not replayed.
Inclusive start — the ^ 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?


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
1. Start. git bisect start begins the session.
2. Mark the current commit bad. git bisect bad.
3. Mark a known-good point. git bisect good v1.2.0 — any older commit or tag you know worked.
4. Git checks out the midpoint. Test it, then report back with git bisect good or git bisect bad.
5. Repeat. Each answer halves the remaining range, until bisect prints the first bad commit.
6. Exit. 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?


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?


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.

1. Install git-filter-repo. Preferred over the older, slower filter-branch.
2. Rewrite history. Remove the offending file entirely with --path ... --invert-paths, or scrub a string wherever it appears with --replace-text.
3. Force push everything. 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.
4. Revoke the secret. Do this immediately, regardless of how clean the rewrite was. Assume it's compromised, and make sure it's gone from any forks too.

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.
Open the file directly and resolve everything between <<<<<<< 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?


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
Applies the latest stash to your working tree and removes it from the stash list.
Applies a stash to your working tree but keeps it in the list — useful for applying the same stash to more than one branch.

After git stash pop, is the stash still in your list to apply again on another branch?