Friday afternoon. You have been building a feature all week. Five days of careful work, dozens of commits on a branch you created Monday morning. The branch is not merged yet because you wanted to clean it up first, maybe squash a few commits, write better messages. You are almost done.
Then you type the wrong command. Maybe you meant to delete a different branch. Maybe you were cleaning up old branches and your fingers moved faster than your brain. Whatever happened, the branch is gone. A week of work, vanished.
Your stomach drops. You stare at the terminal. You try git branch and your branch is not in the list. You try git log and there is no trace of those commits. The panic is immediate and physical. Cold sweat. A frantic tab opens in your browser. You type "git recover deleted branch" and hold your breath.
And then you find something remarkable. A forum post, a Stack Overflow answer, a colleague leaning over your shoulder saying "try git reflog." You type it. And there they are. Every single commit you made this week, listed with timestamps, waiting to be rescued. The branch pointer is gone, but the commits themselves never left. They have been sitting in your repository the whole time, invisible but intact.
This is not a bug. This is not a lucky accident. This is Git working exactly as designed. Because Git, at its core, is pathologically afraid of losing your data. And that paranoia, built into every layer of its architecture, is one of the best things about it.
Remember from earlier in this series how Git stores everything as objects with fingerprint addresses? Blobs, trees, commits, each one named by a hash of its content. When you create a commit, Git does not modify the previous commit. It creates a new object. The old one stays exactly where it was. Nothing in Git's object store is ever overwritten. New objects are added. Old objects just sit there.
This means that when you "delete" a branch, you are not deleting commits. You are deleting a pointer. The branch was just a tiny file containing one commit hash. Remove the file, and the commits it pointed to become orphans, objects with no name pointing to them, but they are still physically present in the repository.
The problem is finding them. If you do not know the hash of the commit you want, and nobody memorizes forty character hex strings, how do you get back to it?
This is where the reflog comes in. Git keeps a secret diary. Every time your branch pointer moves, every time you commit, every time you reset, every time you check out a different branch, Git writes a line in the reflog. It records what happened, when it happened, and what the pointer used to be. Think of it as a flight recorder for your repository. The reflog does not care whether your actions were smart or catastrophic. It records everything.
When you run git reflog, you see a chronological list of everywhere your branch has been. Every commit you made, every reset you performed, every rebase you attempted. Each entry includes the commit hash, a timestamp, and a short description of what happened. The entry you need, the one pointing to the tip of your deleted branch, is right there in the list.
The rescue is almost anticlimactic. You find the hash in the reflog. You run git branch followed by the name you want and that hash. The branch is back. All your commits, your week of work, restored in seconds.
But here is the part that makes this a design decision and not just a convenient accident. The reflog has an expiration policy, and it is deliberately generous. Entries for commits that are still reachable, still connected to a living branch, survive for ninety days by default. Entries for orphaned commits, the ones you "deleted" or rewrote, survive for thirty days. You have a full month to realize you lost something and recover it.
These are not arbitrary numbers. The Git maintainers chose them to cover the realistic window of human error. Thirty days is long enough that you will almost certainly notice something is missing before the safety net disappears. Ninety days for reachable commits means your normal workflow history persists for an entire quarter.
And even after the reflog entries expire, the commit objects themselves are not deleted immediately. They become eligible for garbage collection, a periodic cleanup process that Git runs automatically. But git gc is conservative by default. It does not run aggressively. Unreferenced objects can linger for weeks or months beyond their reflog expiry, giving you yet another window of recovery.
One important detail. The reflog is local. It lives on your machine, in your copy of the repository. It records what you did, not what anyone else did. If a colleague deletes a branch on the remote, your reflog will not help you recover their unpushed commits. But for your own work, it is an extraordinary safety net.
So the reflog catches you when you lose track of commits. But what about when you need to undo something deliberately? You made a commit you should not have. You merged the wrong branch. You want to go back in time. Git gives you several ways to do this, and choosing the right one depends on what you are actually trying to accomplish.
Think of it as a spectrum, from gentle to drastic.
At the gentle end is git reset with the soft flag. This is the lightest touch. It moves your branch pointer back to an earlier commit, but it leaves all your files exactly as they are. Your changes are still there, still staged, ready to be committed again. You have not lost anything. You have just rewound the history while keeping the work. This is useful when you made a commit with the wrong message, or committed to the wrong branch, or want to combine several recent commits into one. Soft reset says "that commit never happened, but the work stays."
In the middle is git reset with no flag at all, which defaults to mixed mode. This moves the branch pointer back and unstages your changes, but leaves them in your working directory. The files are still modified, but they are no longer staged for commit. You would need to git add them again. Mixed reset says "that commit never happened, the staging is undone, but your files are untouched."
At the drastic end is git reset with the hard flag. This is the nuclear option. It moves the branch pointer back, unstages everything, and then overwrites your working directory to match the target commit. Your changes are gone from the files. If you had uncommitted work, it disappears. Hard reset says "pretend everything after this point never happened." This is the one command in Git that can actually destroy uncommitted work. Committed work, remember, is still recoverable through the reflog. But uncommitted changes that get wiped by a hard reset are genuinely gone.
Then there is git revert, which takes a completely different approach. Instead of rewinding history, it creates a new commit that does the opposite of an existing one. If a commit added three lines, the revert commit removes those three lines. If a commit deleted a function, the revert commit adds it back. The original mistake stays in the history. The correction is added on top. Nothing is rewritten. The full story is preserved, including the part where you made an error and then fixed it.
This distinction matters enormously in team settings. If you have already shared your commits with others, if you have pushed to a remote repository that your colleagues are working from, rewriting history with git reset creates chaos. Your teammates have commits that no longer exist in your version of the branch. Their next pull or fetch will be confused at best, catastrophic at worst. But git revert is safe to share. It is just a new commit. It flows through the normal collaboration machinery without breaking anything.
And then there is git cherry-pick, which is not exactly an undo but solves a related problem. Cherry-pick takes a single commit from anywhere in your repository and applies it to your current branch. It copies the changes, not the commit itself, creating a new commit with the same modifications but a different hash and a different position in history.
The classic use case is backporting. You fix a critical bug on your development branch. The fix needs to go into the release branch too, but you cannot merge the entire development branch because it contains unfinished features. Cherry-pick lets you extract just the fix and apply it surgically. The Python project uses an automated cherry-picker tool for exactly this purpose, backporting fixes across multiple supported Python versions.
The beauty of these tools is that they compose. Real recovery scenarios rarely involve a single command. They involve looking at the reflog to find where you were, then using reset or checkout to get back there, then maybe cherry-picking specific commits onto the right branch.
Consider the rebase gone wrong. You are cleaning up your branch history with an interactive rebase. You squash some commits, reorder others, drop one that seemed unnecessary. You finish the rebase and realize with horror that the commit you dropped contained a critical change you forgot about. In a centralized system like Subversion, that change is gone. In Git, you open the reflog. There is an entry for every step of the rebase, including the state of your branch before it started. You find the pre-rebase commit hash, create a temporary branch pointing to it, and cherry-pick the dropped commit onto your current branch. Crisis resolved in under a minute.
Or the accidental hard reset. You meant to reset to the previous commit. Instead, you reset to a commit from three weeks ago. Dozens of commits, apparently gone. But the reflog remembers where you were one second before the reset. The entry is right there, labeled with the hash of where your branch used to point. One command to move the pointer back. Everything restored.
Or the stash that saved a deadline. You are deep into debugging a production issue. Halfway through, a colleague tells you there is an urgent fix needed on a completely different branch. Your current work is scattered across a dozen files. None of it is ready to commit. In the old days, you might have copied files to your desktop, or made a throwaway commit with a message like "work in progress do not merge." Git stash is cleaner. One command, git stash, and all your uncommitted changes are saved to a special holding area. Your working directory snaps back to a clean state. You switch branches, make the urgent fix, switch back, run git stash pop, and your half-finished debugging work reappears exactly as you left it.
Under the hood, stash is surprisingly elegant. It creates actual commit objects, usually two or three of them. One commit captures the state of your index, what you had staged. Another captures your working directory changes. If you used the untracked flag, a third captures files Git was not tracking yet. These commits are stored in a stack referenced by a special reflog, so you can have multiple stashes and retrieve them in any order. It is the full power of Git's object model applied to temporary storage.
The stash is not glamorous. Nobody writes blog posts about how git stash changed their life. But it is one of those quiet tools that prevents small disasters dozens of times a year. Every time you need to context-switch without losing your place, stash is there, silently turning your half-finished work into proper Git objects that will survive anything short of deleting the repository itself.
Step back for a moment and think about what all of these features have in common. The reflog recording every pointer movement for ninety days. The garbage collector waiting patiently before cleaning up orphaned objects. Reset preserving commits even as it moves branch pointers. Revert adding new history instead of erasing old history. Stash encoding temporary work as real commit objects.
Git is paranoid about losing your data. And that paranoia is not an accident. It is a direct consequence of the design decisions Linus Torvalds made in those first two weeks.
Content-addressing means every object is immutable. Once a blob, tree, or commit is created, it never changes. A new version creates a new object. The old one stays. This is not a policy choice that could be changed by a configuration setting. It is baked into the fundamental architecture. Objects are named by the hash of their content. Changing the content would change the name, which means it would be a different object. Immutability is not a feature of Git. It is a mathematical consequence of how Git names things.
This is why "deleting" a commit does not delete it. You cannot modify an object in Git's store. You can only stop pointing to it. And as long as something still references it, the reflog, another branch, a tag, a stash, it is not going anywhere. Even when nothing references it, the object physically exists on disk until the garbage collector runs and decides it is safe to clean up.
Every single piece of data, when Git tracks your content, we compress it, we delta it against everything else, but we also do a hash of the content, and we actually check it when we use it.
That hash checking is Git verifying its own integrity constantly. If a file gets corrupted on disk, a bit flip from hardware failure, the hash will not match. Git will notice. It will tell you.
If you have disc corruption, if you have RAM corruption, if you have any kind of problems at all, Git will notice them. It is not a question of if. It is a guarantee.
In a centralized system, silent corruption could propagate to every developer before anyone noticed. In Git, corruption is detected at the point of use because the fingerprint system makes it impossible to hide.
This matters beyond individual recovery stories. It matters for the entire software industry. When you clone a repository, you are getting a cryptographically verified copy of the complete history. Every commit, every file version, every tree structure has a hash that chains back to the very first commit. Tampering with any point in that chain would change all the downstream hashes, making the modification detectable. This is not just version control. It is a verifiable audit trail.
The Git maintainers, Junio Hamano and the contributor community, have continued this philosophy for twenty years. When new features are added, data preservation is the default. When trade-offs arise between convenience and safety, safety wins. The reflog expiry defaults are generous because it is better to use slightly more disk space than to lose someone's work. The garbage collector is conservative because an overeager cleanup could destroy recoverable history.
I guarantee you, if you put your data in Git, you can trust the fact that five years later, after it is converted from your hard disc to DVD to whatever new technology and you copied it along, five years later you can verify the data you get back out is the exact same data you put in.
Torvalds said that in two thousand seven, and the promise has held. So the next time you accidentally delete a branch, or reset to the wrong commit, or realize three days later that you should not have rebased, take a breath. Git almost certainly still has what you need. The commits are still there, sitting quietly in the object store, referenced in the reflog, waiting for you to come looking. You have thirty days for orphaned objects, ninety for reachable ones, and often much longer in practice.
Git never truly forgets. And for the millions of developers who have experienced that cold sweat moment of thinking they lost their work, that is not just a technical feature. It is a promise.
git reflog. Two words that turn panic into an inconvenience. Somewhere in that list, timestamped and waiting, is the commit you thought you lost. The branch you deleted. The rebase you regret. Git recorded every move you made, and the reflog kept the receipts. Ninety days of them. The hardest part is not the recovery. It is believing, in that moment of cold sweat, that the work is still there. It almost always is.