When you rebase a branch, Git does not delete your original commits. It never did. It just stops pointing at them. The old commits sit in your repository, still reachable through a hidden log, until you explicitly tell Git to clean up. This is not a safety net bolted on later. It is a direct consequence of how Git stores everything.
A Library Where Nothing Is Ever Overwritten
Imagine a library that never throws away a book. Every time someone “revises” a manuscript, the library creates a brand new book with a new ISBN. The old book stays on the shelf. The library catalog is a set of index cards. Each card points to one ISBN. When you “move” a branch, you simply write a new card that points to the new book. The old card goes into an archive drawer. The old book remains untouched.
That is Git’s object store. Every commit, every file version, every directory snapshot is an immutable object stored by its content hash. Branches and tags are just tiny files that hold a hash. They are the catalog cards. The reflog is the archive drawer, recording every card swap. Rebase writes new objects and updates a card. It never touches the old objects. That is why you can always find your way back.
The Content-Addressed Object Database
Git’s foundation is a content-addressable store. The name of an object is the SHA-1 hash of its contents. If you change a single byte, you get a new hash and a new object. The original object stays exactly as it was. Git stores objects under .git/objects, using the first two hex digits as a directory and the remaining 38 as the filename. Git Internals
There are four object types. A blob holds raw file contents, no filename, no permissions. A tree represents a directory. It lists entries, each with a mode, a filename, and the hash of a blob or another tree. A commit points to a root tree, zero or more parent commits, and metadata like author, committer, and message. An annotated tag is an optional fourth type that points to another object and adds a tagger and message. Git Objects
These objects form a Merkle DAG. A commit’s hash depends on its tree, its parents, and its metadata. The tree’s hash depends on the hashes of its blobs and subtrees. Change any file, and you get a new blob, which forces a new tree, which forces a new commit. The old commit, tree, and blob remain. Nothing is mutated in place. Git Internals - Git Objects
Git can store objects as loose files (one per object) or in packfiles for efficiency. Packfiles combine many objects and may use delta compression internally. But the logical identity of each object never changes. When you later repack, Git never alters the content of existing objects. It simply reorganizes their storage. This means rebase can create hundreds of new objects, and the old ones remain intact, just waiting to be cleaned up. Git Packfiles
What a Commit Actually Looks Like
A commit is a small text record, compressed and hashed. You can see its raw content with git cat-file -p <hash>. It looks like this:
tree 8e1f0c...
parent f5c3ab...
author Alice Example <alice@example.com> 1710000000 +0000
committer Alice Example <alice@example.com> 1710000000 +0000
Refactor widget configuration handling
The tree line points to the root snapshot. The parent lines form the history chain. A merge commit has multiple parents. The first commit in a repository has none. The author and committer timestamps are part of the object. So is the message. Any change to any of these fields produces a new hash and a new object. git-commit-tree documentation
This immutability is the whole game. When you amend a commit, you do not edit the old one. You create a new commit with a different parent (the old commit’s parent) and a new tree. The branch pointer moves to the new commit. The old commit stays. The same thing happens during rebase. The original commits are never overwritten. They just lose their branch reference.
Refs: The Mutable Layer
Humans do not work with raw hashes. Git provides references, or refs, which are simple files that store a hash. A branch like main lives in .git/refs/heads/main. Its content is a single 40-character hash. When you commit, Git writes new objects and then overwrites that file with the new commit hash. Git References
HEAD is usually a symbolic reference. It points to a branch. In a detached HEAD state, it holds a raw hash. When you run git rebase, Git eventually updates the branch ref to point to the new tip commit. It also writes the previous hash to ORIG_HEAD as a quick undo marker. But the real safety net is the reflog.
The Reflog: Your Branch’s Movement Diary
Every time a ref moves, Git appends a line to a log file under .git/logs. There is a log for HEAD and one for each branch. A typical entry records the old hash, the new hash, your identity, a timestamp, and a description of the action. git-reflog documentation
Run git reflog to see your recent HEAD movements. You will see entries like:
c3d2e1f HEAD@{0}: rebase finished: returning to refs/heads/feature
a1b2c3d HEAD@{1}: rebase: checkout main
...
The reflog is not just a history lesson. Git uses it for reachability. An object is considered reachable if it can be found from any ref or any unexpired reflog entry. The garbage collector will not delete reachable objects. So as long as a reflog entry mentions an old commit hash, that commit is protected. Git Internals - Maintenance and Data Recovery
When you rebase, the branch ref jumps to the new tip. The reflog records the old tip. The old commits remain reachable through that reflog entry. They are not garbage collected until the entry expires and no other refs point to them. By default, reflog entries expire after 90 days for unreachable commits. That gives you months to recover.
What Rebase Does, Step by Step
Rebase does not move commits. It replays changes. Given a feature branch with commits A, B, C based on M, and a target branch that has advanced to N, rebase does this:
- Identify the commits unique to your branch:
A,B,C. - For each commit, compute the diff it introduced relative to its parent.
- Apply that diff on top of
Nto create a new tree. - Write a new commit object with the new tree and
Nas its parent. Call itA'. - Repeat for
B, applying its diff on top ofA', creatingB'. - Repeat for
C, creatingC'. - Update the branch ref to point to
C'.
The original A, B, C remain in the object database. They still point to M and to each other. Their hashes are unchanged. The reflog records the branch’s move from C to C'. git-rebase documentation
Interactive rebase works the same way. When you squash commits, Git combines their diffs and creates a single new commit. The squashed commits are left as orphaned objects. When you reorder commits, Git replays them in the new order. Every operation produces new objects. None destroy the old ones.
Why You Can Always Go Back
After a rebase, you can recover the old branch tip with a simple reflog lookup. Find the entry from before the rebase and reset your branch to that hash. For example:
git reflog feature
# find the hash before the rebase
git reset --hard <old-hash>
Your branch is now back where it was. The new commits you created during the rebase remain as well, but they become unreachable from any branch. They will eventually be cleaned up unless you create a new branch to keep them.
Even if you force-push the rebased branch to a remote, your local reflog still holds the old commits. The remote repository does not have your reflog, so those old commits are not protected there. But on your machine, they survive. If you need to recover after a force-push, you can fetch the remote, find the old commit in your local reflog, and push it back.
Quick Reference
| Property | Value |
|---|---|
| Object store location | .git/objects |
| Default reflog expiration (unreachable) | 90 days |
| Default reflog expiration (reachable) | never (until gc with --prune=) |
| Object types | blob, tree, commit, tag |
| Rebase operation | cherry-pick each commit onto new base |
| Recovery command | git reset --hard HEAD@{n} |
| Garbage collection command | git gc |
Frequently Asked Questions
Q: How long do reflogs keep old commits after a rebase?
By default, entries for commits that are unreachable from any branch expire after 90 days. Entries for commits that are still reachable from some branch never expire. You can adjust this with gc.reflogExpireUnreachable. git-config documentation
Q: What happens to squashed commits during an interactive rebase?
They become orphaned objects. They are not reachable from the new branch tip, but they remain in the object database and are protected by the reflog until expiration. You can find them with git reflog or git fsck --lost-found.
Q: Can I recover from a rebase I already force-pushed to a remote?
Yes, if you still have the old commits in your local reflog. Find the old tip hash with git reflog, create a new branch at that hash, and force-push it again. The remote will not have your reflog, so you must act before your local reflog entries expire.
Q: Does git gc remove rebased commits immediately?
No. git gc respects reflog expiration. It will not prune objects that are reachable from any ref or any unexpired reflog entry. You can force immediate pruning with git gc --prune=now, but you should only do that if you are certain you no longer need the old history.
Q: How does rebase differ from cherry-pick in terms of object creation?
Rebase is essentially a series of cherry-picks. Each cherry-pick creates a new commit with a new hash. The difference is that rebase automates the process for a range of commits and updates the branch ref at the end. Both operations leave the original commits untouched.
Keep Your History Safe by Knowing Where It Lives
Rebase is not a destructive operation. It is a pointer update with a paper trail. Once you understand that Git never overwrites objects, you can rewrite history without fear. You can experiment, squash, reorder, and still walk back any change. The reflog is your undo stack.
If you want this kind of breakdown every week — how real systems actually work under the hood — subscribe to Internals Decoded at internalsdecoded.com. We never publish surface-level explanations. We go straight to the source code and the data structures that make the tools you use every day.