Git and GitHub the Safe Way, Ch. 8: When It Goes Wrong
Part of Git and GitHub the Safe Way, a short book on git, GitHub, and the habits that keep a repository from hurting you.
Promise two was that no work is ever lost. This chapter is how git keeps it. There is an undo for every stage a change can be in, and the trick is matching the rung to the mistake. Climb no higher than you need: each rung up rewrites a little more, and the top rungs rewrite history that other people may already have.
The example is a journal with one line per day, in a repository that has a remote, because the stage of the mistake decides the fix.
Rung 1: a bad edit, not yet staged
You overwrote a file, or your editor did. git status shows M, and the last commit still has the good version:
git restore journal.txt
cat journal.txt
Monday: plan the week
Tuesday: write chapter 8
Wednesday: review
restore throws away uncommitted changes to that file. There is no undo for it, so look at git diff first if you are not sure. It only touches the file you name; git restore . does every file, which you want far less often than it seems.
Rung 2: staged by accident
git add . picked up a file that should not be there:
git add .
git status --short
A .env
M journal.txt
The file is on the list for the next commit but not in history yet. Take it off the list; the file itself stays where it is:
git restore --staged .env
git status --short
M journal.txt
?? .env
Now add it to .gitignore so it cannot happen again, as chapter 4 said. This rung is the cheapest place to catch a secret, and git diff --staged before every commit is how you catch it.
Rung 3: the last commit is wrong, and nobody has it yet
A typo in the message, a file you forgot, a change that belongs in the previous commit. As long as the commit has not been pushed, replace it:
git commit --amend -m "Add Thursday, and ignore .env"
git log --oneline
0cd8087 Add Thursday, and ignore .env
12b02a6 Add Wednesday
3463ad7 Add Tuesday
883ed6f Start the journal
To add a forgotten file, git add it and git commit --amend --no-edit. The commit’s fingerprint changes, because it is a new commit; the old one is gone from the branch. That is exactly why the rule is unpushed only. Amend a commit that is already on GitHub and your branch now disagrees with everyone’s copy of it.
Rung 4: the commit is wrong, and it has been pushed
Do not rewrite. Add a commit that undoes it:
git revert --no-edit HEAD
git log --oneline
3f63283 Revert "Add Friday"
12746dd Add Friday
0cd8087 Add Thursday, and ignore .env
12b02a6 Add Wednesday
3463ad7 Add Tuesday
883ed6f Start the journal
The mistake stays in history; the correction sits on top of it. Everyone who pulls gets both, nothing they have becomes invalid, and a year from now the log still explains what happened. git revert <id> works on any commit, not only the last one. This is the only rung you should ever use on main once a pull request has landed.
Rung 5: reset, and the seatbelt
git reset --hard moves the branch to an earlier commit and makes the folder match. It is the right tool for “I have been experimenting on my own branch and want to go back to how it was an hour ago,” and the wrong tool for anything that has been pushed.
git reset --hard HEAD~1
git log --oneline
3f63283 Revert "Add Friday"
12746dd Add Friday
...
The experiment commit is gone from the log. Not from git. Every move of a branch is recorded in the reflog, which is git’s diary of where you have been:
git reflog -5
3f63283 HEAD@{0}: reset: moving to HEAD~1
c625c96 HEAD@{1}: commit: Try an experiment
3f63283 HEAD@{2}: revert: Revert "Add Friday"
12746dd HEAD@{3}: commit: Add Friday
0cd8087 HEAD@{4}: commit (amend): Add Thursday, and ignore .env
HEAD@{1} is where you were one move ago. Go back:
git reset --hard HEAD@{1}
git log --oneline
c625c96 Try an experiment
3f63283 Revert "Add Friday"
...
The reflog is the reason “I lost my work” is almost never true in git. Commits are kept for weeks after nothing points at them. If you committed it, you can get it back; the reflog tells you the id.
Rung 6: a deleted branch
git branch -D deleted a branch with unmerged work on it. Same seatbelt:
git branch -D spike
git reflog -4
Deleted branch spike (was a61bfbf).
3f63283 HEAD@{0}: checkout: moving from spike to main
a61bfbf HEAD@{1}: commit: Spike work
3f63283 HEAD@{2}: checkout: moving from main to spike
3f63283 HEAD@{3}: reset: moving to HEAD~1
The branch’s last commit is right there. Give it a name again:
git branch spike HEAD@{1}
git log --oneline spike
a61bfbf Spike work
3f63283 Revert "Add Friday"
...
The one thing the reflog cannot bring back is work you never committed and then restored or deleted. Which is the quiet argument running through this whole book for committing small and often: a commit is free, and it is the only thing git promises to keep.
The three force-push rules
Rungs 3 and 5 rewrite history. If that history was already pushed, a normal git push is refused, and the internet will tell you to add --force. Three rules:
- Never to
main. Chapter 6 makes GitHub refuse it anyway. Rewriting a shared branch invalidates every copy of it and every pull request against it. - Only to your own branch, the one you alone are working on, typically to tidy commits before a pull request is reviewed.
- Always
--force-with-lease, never--force. The lease form refuses if someone else pushed to the branch since you last fetched, which is exactly the case where force would destroy their work. Plain--forcedoes not check.
git push --force-with-lease origin my-branch
If even that feels like too much, it is. A revert never needs force, and it is always available.
Conclusion
Match the rung to the mistake: restore for the working tree, restore --staged for the list, --amend for an unpushed last commit, revert for anything pushed, reset --hard for your own branch with the reflog as the seatbelt, git branch name HEAD@{n} for a deleted branch. Force pushes: never main, only yours, always with a lease. Next: the chapter that makes most of this unnecessary, by reading pull requests properly before they land.