Team training · Block 2 of 6

Undo and recovery

Destroying your work four ways, and getting it back

2.1

The safety net

Git is almost append-only

Commands that look destructive usually just move a pointer.

The commit object stays on disk. What changes is whether anything still points at it.

A commit nothing points to is unreferenced — invisible to git log, but very much still there.

The label moves — the reflog remembers

91ac5d3 e91e334 c584509 f6c6987 main ← HEAD main ← HEAD unreferenced — still on disk
$ git reflog
e91e334 HEAD@{0}: reset: moving to HEAD~2
f6c6987 HEAD@{1}: commit: Add end-to-end invoice_total   <- the tip that just fell off
c584509 HEAD@{2}: commit: Add tax calculation
e91e334 HEAD@{3}: commit: Add README
91ac5d3 HEAD@{4}: commit: Add discount helper

The label moved and two commits fell off the walk — but git reflog still lists every one by the same id. Matching colours tie each node to its journal line: f6c6987 is orphaned in the graph, yet sitting right there at HEAD@{1}.

2.2

reset

One command, three depths

Working dir Staging Repository your edits what's queued branch pointer --soft --mixed --hard · reaches all the way into your working directory

All three move the branch pointer. How far left they reach is the only difference.

--soft — uncommit, keep everything staged

$ git reset --soft HEAD~1

$ git status
Changes to be committed:
  modified:   invoice.py

The commit is gone from the branch. Its contents are sitting in staging, ready to recommit.

Use it for: "I committed too early" or "these two commits should be one".

--mixed — uncommit and unstage

$ git reset HEAD~1        # --mixed is the default

$ git status
Changes not staged for commit:
  modified:   invoice.py

Commit undone, staging cleared. Your edits are still in the working directory, untouched.

Use it for: "undo that commit, let me restage things differently".

--hard — the one that bites

$ git reset --hard HEAD~2
HEAD is now at e91e334 Add README

$ git status
nothing to commit, working tree clean

Two commits off the branch and your working directory overwritten to match.

The commits are recoverable. Uncommitted edits it overwrote are not.

Recovering the commits

$ git reflog
e91e334 HEAD@{0}: reset: moving to HEAD~2
f6c6987 HEAD@{1}: commit: Add end-to-end invoice_total   <- there it is

$ git branch rescue HEAD@{1}      # non-destructive: just a new label
$ git log --oneline --all --graph
$ git reset --hard rescue          # once you've verified

Create a label first, verify, then move. Don't fix a panic with another --hard.

When it looks broken

Commandment 09

When it looks broken, stop. Check git reflog, then ask. Improvising turns a recoverable mess into a lost afternoon.

Nearly every unrecoverable situation started as a recoverable one that someone kept typing at.

2.3

Detached HEAD

It's a state, not an error

$ git switch --detach c584509
Note: switching to 'c584509'.

You are in 'detached HEAD' state. You can look around, make
experimental changes and commit them...
91ac5d3 c584509 f6c6987 main ← HEAD HEAD → main → commit HEAD HEAD → commit — no branch in between

Normally HEAD → branch → commit. Detached means HEAD → commit directly, with no branch in between.

Commits you make here belong to no branch. Switch away and they become unreferenced.

Getting out

$ git switch -c experiment     # keep the work: give it a branch

$ git switch main              # discard it: just leave

If you made commits and left without branching, git reflog still has them.

2.4

revert versus reset

Two ways to undo

reset — move the label back $ git reset --hard HEAD~1 91ac5d3 f6c6987 c584509 main ← HEAD main ← HEAD orphaned revert — add an undoing commit $ git revert 91ac5d3 91ac5d3 f6c6987 c584509 main ← HEAD 8b3f1a9 reverted — kept main ← HEAD

Two mechanisms, not two spellings. reset moves the label back and c584509 falls off the branch — history rewritten. revert 91ac5d3 rewrites nothing: the commit stays put and a new 8b3f1a9 is stacked on top to cancel it — and revert can target any commit, not just the tip.

revert in practice

$ git revert c584509
[main 8b3f1a9] Revert "Add tax calculation"

$ git log --oneline
8b3f1a9 Revert "Add tax calculation"
f6c6987 Add end-to-end invoice_total
c584509 Add tax calculation

The original commit is still there. Nobody's history is invalidated, so nobody needs to do anything.

kata · basic-revert

The rule

Commandment 06

Revert in public, reset in private.

Undo shared mistakes by adding a commit. Undo local ones by moving the pointer.

"Public" means anyone else could have pulled it. On our setup: has it been pushed?

Where we got to

SituationReach for
Committed too earlyreset --soft
Want to restage differentlyreset (mixed)
Throw away local commits entirelyreset --hard
Undo something already pushedrevert
Lost a commit somehowreflog, then branch
Not sure what happenedStop. Ask.

Recoverable: commits, branches, rebases, bad merges. Not recoverable: edits you never committed.