Git Push Failure: A Story of Incomplete History Rewrite
The Story
I made a large Git commit that accidentally included some big files, which caused my push to fail. To fix this, I used git filter-repo to remove the history of certain large sampling files. Just to be safe, I also deleted some unused local large sampling files, manually removed the cached large files that had been staged in the commit, and added the directories that might contain large files to .gitignore. After all that cleanup, I thought everything was ready, so I committed and pushed again.
After 15 hours, VS Code was still showing a running blue line segment as if it were still trying to push. At that point, the situation looked paradoxical. My local repository differed from the remote GitHub repository by only two commits. The first commit added a large amount of content (189 files changed and nearly 23,000 lines inserted), and this was the commit that originally caused trouble. The second commit removed much of that content, deleting over 11,000 lines and leaving the working tree clean. Running git status showed no pending changes, and inspecting the current tree confirmed that no large files remained.
Initially, it even appeared that no push was happening at all. Running git remote -v on a separate terminal window revealed that no remote repository was configured, meaning that the apparent “15-hour push” was just a misleading VS Code UI state. After re-adding the GitHub remote, a real push attempt finally began.
That’s when the real problem surfaced.
Although the current commit history looked clean, Git attempted to upload a massive 12.66-gigabyte pack and promptly failed. This revealed a crucial detail that had been easy to miss: although I had used git filter-repo, I had not removed all large files from the repository history. In particular, large MCMC output files living under demo_results/—some exceeding 12 GB individually—were still present as blobs in an earlier commit. They had been deleted later, but they remained reachable in the commit graph.
This explained everything. Git does not push diffs or final snapshots; it pushes all objects reachable from the commits being sent. A file that is added in one commit and deleted in the next still exists in history and must be transferred. Commands like git diff only show net effects and therefore completely hide this class of problem.
At this point, two valid solutions existed. One option was to re-run git filter-repo correctly, this time explicitly removing all large artifacts (especially the demo_results/ directories) from history. Had this been done, the push would have succeeded: rewriting history would have worked, provided the rewrite was complete.
Instead of immediately rewriting history again, I paused to verify what exactly was happening. We carefully inspected the repository to locate the largest blobs and confirmed their origin. I verified that the problematic commits had never been pushed to GitHub, meaning no collaborators or published history would be affected. With that assurance, we chose a safer and simpler alternative.
Rather than rewriting history again, I dropped the problematic commits entirely. The main branch was reset to the remote base, and only the intended source code, notebooks, and documentation were selectively restored from a backup branch. All generated artifacts including sampling outputs, cache files, and experimental results were explicitly excluded, and .gitignore was strengthened to prevent future accidents.
After rebuilding the commit cleanly and verifying that no large blobs remained in the commit range, the repository was pushed again. This time, the push completed almost instantly.
In the end, the failure wasn’t caused by Git behaving unexpectedly, nor by history rewriting being ineffective. The real issue was an incomplete history rewrite combined with Git’s object-based push semantics.
Technical Report: Diagnosing and Resolving a Git Push Failure Caused by Incomplete History Rewrite
1. Initial Cleanup Attempt (Incomplete)
After the first push failed due to large files, history rewrite was performed:
| |
Problem: This removed some large files but not all. Large sampling outputs in other directories remained in history.
2. Misleading Initial Diagnosis
After cleanup, surface-level checks suggested the repository was clean:
| |
However, these commands only inspect the current snapshot, not the full object graph.
3. Remote Configuration Issue
git filter-repo removes the remote configuration by design:
| |
The “15-hour push” in VS Code was actually a misleading UI state—no push was happening at all. After discovering this:
| |
4. The Real Problem Surfaces
After re-adding the remote and attempting to push:
| |
Output:
| |
Git attempted to upload 12.66 GB and failed. The confusing “Everything up-to-date” message appeared because the push failed mid-transfer.
Key insight: Git pushes all objects reachable from commits, not diffs. A file added in commit A and deleted in commit B still exists in history and must be transferred.
5. Diagnosing the Incomplete Rewrite
Inspection of large blobs in the commit range being pushed:
| |
Result (actual output):
| |
A single file exceeded 13 GB (13,230,021,344 bytes ≈ 12.3 GiB). Multiple other files ranged from hundreds of megabytes to several gigabytes. All were .npz (NumPy compressed array) files containing MCMC sampling outputs.
6. Root Cause
The filter-repo command only removed one specific directory. Large sampling output files in other directories were never filtered, remaining reachable in the commit graph.
Alternative solution (not taken): A second, complete history rewrite:
| |
7. Chosen Solution: Drop Problematic Commits Entirely
Since the problematic commits were never pushed to GitHub, a simpler and safer approach was taken:
| |
Note: The actual workflow involved iterative git checkout backup/with-bad-commits -- <path> commands to selectively restore files, followed by git rm --cached commands to exclude unwanted files like __pycache__/ directories, binary assets, and generated outputs.
8. Verification Before Push
Critical verification to ensure no large blobs remain:
| |
Result: Command returned empty output, confirming no blobs in the commit range (only modifications to existing files).
| |
Showed only source code, notebooks, documentation files, and updated .gitignore staged for commit.
| |
9. Successful Push
| |
Output:
| |
Push completed in seconds. Total transfer was only ~111 KB instead of 12.66 GB.
Key Lessons
Git pushes objects, not diffs. Files deleted in later commits still exist in history if reachable from any commit being pushed. The “Everything up-to-date” message can appear even when a push fails mid-transfer.
Surface checks are insufficient. Commands like
git status,git diff, and evengit log origin/main..maincan return empty results while large blobs remain in the object graph. Always verify with:1 2 3git rev-list --objects origin/main..main | \ git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \ awk '$1=="blob"{print $3 "\t" $4}' | sort -nr | head -n 30History rewriting must be complete. An incomplete
filter-repooperation leaves problematic blobs in history. Size-based filtering is often safer than path-based filtering:1git filter-repo --strip-blobs-bigger-than 100Mgit filter-reporemoves remote configuration. After runningfilter-repo, remotes must be re-added:1 2git remote add origin <url> git fetch originUnpublished commits can simply be dropped. When problematic commits haven’t been pushed, resetting to
origin/mainand selectively restoring files is safer and simpler than multiple history rewrites.Iterative restoration workflow is practical. Using multiple
git checkout <branch> -- <path>commands followed by selectivegit rm --cachedcommands provides fine-grained control over what gets committed.Verify before pushing. Always inspect reachable objects in the push range, not just the working tree. An empty result from the blob inspection command confirms safety.
Quick Reference Commands
| |