Three variations on the same mistake — work ends up associated with the wrong branch — and the
fix differs completely depending on whether the work is committed yet. This is the mental
model's snapshot/immutability point (git-mental-model.md)
made practical: uncommitted content isn't anchored to any branch at all, so it's a stash-and-move
problem; committed content is baked into an immutable commit object, so it's a copy-the-diff
problem (cherry-pick), not a move.
This page assumes you've read the "stash workflow" section of
git-three-trees-recovery-primer.md for what
a stash actually is (real commit objects, pop vs apply, selective stashing) — this page is
the scenario-driven companion, not a restatement.
| Situation | Is it committed? | Right tool |
|---|---|---|
| Mid-work, realize you're on the wrong branch | No | git stash → switch/create branch → pop/apply |
| Don't know which branch has a given file | N/A | git cat-file -e sweep, or git log --all --source |
| Already committed to the wrong branch | Yes | git cherry-pick (+ reset/rebase -i/revert to remove the original) |
Situation: you created feat/pub-app-sqlandspeccing for SQL/public-app work, but you've
actually been heads-down on letters. git status --short shows a realistic mix:
M src/main/java/com/tcvcog/tcvce/coordinators/LetterCoordinator.java
M docs/subsystems/letters+emailing/letters-index.md
?? docs/subsystems/letters+emailing/letters-index-pt4.md
?? docs/subsystems/letters+emailing/IV-E-nov-legacy-migration-spec.md
None of it is committed. Per the mental model, uncommitted content isn't attached to a branch —
only commits are — so this is purely a stash-and-relocate problem, not the cherry-pick problem in
§3 below.
git status --short
Column 1 = staged (index vs HEAD), column 2 = unstaged (working tree vs index) — see the
status --short decoding table in
git-three-trees-recovery-primer.md. If every
line reads M (leading space), nothing is staged — the easy case, covered below. If anything
shows in column 1 (M , A , MM), some hunks are already staged and you need the --index
flag when reapplying (explained inline below) or you'll lose that distinction.
Also check for ?? untracked files — plain git stash ignores them entirely; you need -u.
Confirm the current branch has no commits of its own yet:
git log --oneline master..feat/pub-app-sqlandspeccing # empty output = no divergence
If that's empty, git stash branch is the one-shot recipe:
# capture everything, including new untracked files, staged or not
git stash push -u -m "letters work stashed off pub-app-sqlandspeccing by mistake"
# create + checkout a new branch at the same commit, and reapply in one motion
git stash branch feat/letters-pt4-continued
git stash branch <name> creates the branch at the commit that was HEAD when the stash was
taken, then applies the stash to both the working tree and the index — so whatever staged/
unstaged split you had is restored automatically, no separate --index flag needed.
Gotcha: that new branch is rooted at "wherever HEAD was when you stashed," not necessarily at
master. If the divergence check above had shown commits, those commits belong to
feat/pub-app-sqlandspeccing specifically and would come along for the ride — not what you want.
In that case, pick the base explicitly instead:
git stash push -u -m "letters work"
git switch -c feat/letters-pt4-continued master # explicit, correct base
git stash pop --index # --index restores the staged/unstaged split
git stash push -u -m "letters work stashed off pub-app-sqlandspeccing by mistake"
git switch lsa7-d-letterscont
git stash pop --index
stash branch only ever creates a new branch, so it's not an option here — switch manually,
then reapply. Popping doesn't care what branch is currently checked out; it's just a patch
application against whatever's in the tree. If that target branch's copies of these files differ
enough from what they looked like at stash time, pop can conflict, with the same
conflict-marker behavior as any other patch application. If you're not confident the apply will
be clean, use git stash apply --index first (keeps the stash entry as a safety net) and only
git stash drop once you've confirmed the result builds.
A stash always captures the index and the working tree as two separate snapshots, so the
staged/unstaged split is preserved in the stash entry itself. But by default, git stash apply
/ pop collapse everything back into a single unstaged blob when reapplying. If you'd staged some
hunks before realizing you were on the wrong branch, always reapply with --index
(git stash pop --index / git stash apply --index) to reproduce that split. If everything was
unstaged to begin with, plain pop/apply reproduces that state exactly — there's no split to
lose, so --index is a harmless no-op in that case too.
Situation: codeconnect/database/patches/dbpatch_beta92.sql isn't in your working tree, and
git log -- <path> on the current branch shows nothing — but you know it exists on some branch
among the two dozen in this repo.
Two different questions need two different tools, and conflating them is the trap:
git for-each-ref --format='%(refname:short)' refs/heads/ refs/remotes/ | while read -r b; do
git cat-file -e "$b:codeconnect/database/patches/dbpatch_beta92.sql" 2>/dev/null && echo "$b"
done
git cat-file -e <ref>:<path> exits 0 if that path exists as a blob in that ref's tree, 1
otherwise — no checkout required, no output on failure. It's the existence-only analog of
git show <ref>:<path>, which would print the file's contents.
# every commit, on every ref, that touched this path — annotated with which ref found it
git log --all --oneline --source -- '**/dbpatch_beta92.sql'
--source prints the ref name through which log reached each commit — only meaningful when
walking multiple refs (--all, or an explicit branch list), and it shows one contributing ref
per commit, not necessarily every branch containing it. Pair it with --contains for the
complete list once you have a SHA:
git branch --all --contains <sha>
Common trap: --grep searches commit messages, not file paths or file contents —
git log --all --grep=dbpatch_beta92 finds nothing unless someone happened to type that filename
into a commit subject. The pathspec after -- is what matches paths. Searching file contents
across history (e.g. "which commit ever added this table name") is a third, different tool — the
pickaxe, git log -S<string> --all.
git show lsa7-ceartweaks:codeconnect/database/patches/dbpatch_beta92.sql \
> codeconnect/database/patches/dbpatch_beta92.sql
Contrast with §1: this is committed, so immutability applies. You can't move a commit — you copy
its diff onto a different parent (cherry-pick), then separately decide what happens to the
original.
git log --oneline -1 # confirm it's really the tip; note the SHA
git branch feat/correct-branch # new branch pointing at the SAME commit — includes it
git reset --hard HEAD~1 # rewind the wrong branch, dropping the commit
git switch feat/correct-branch # now has exactly that commit
Safe only because the commit is unpushed and genuinely the last one. reset --hard unconditionally
drops it from the wrong branch's history — recoverable briefly via git reflog, not a durable
safety net. See the revert-strategies table in
git-mental-model.md for how this compares to the other
"undo" options.
git log --oneline feat/wrong-branch # find the SHA of the misplaced commit
git switch feat/correct-branch
git cherry-pick <sha> # copies the same diff as a NEW commit here
Cherry-pick takes the diff between that commit and its parent and reapplies it as a brand-new
commit object — new SHA, new parent, same tree changes. Conflicts are handled exactly like a merge
conflict: edit, git add the resolved paths, git cherry-pick --continue.
Then deal with the original on the wrong branch:
git rebase -i <sha>~1, change pick to drop for that commit.git revert <sha> on the wrong branch instead: it adds an inverse commit rather than erasingstatus --short decoding, and the stash mechanics this page builds on.--merged, --contains, --grep) used above.