<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: The Machine Underneath</title>
    <description>The latest articles on DEV Community by The Machine Underneath (@machineunderneath).</description>
    <link>https://dev.to/machineunderneath</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119852%2F1a72bf4d-d933-4eba-957b-e2c4a9a2c391.png</url>
      <title>DEV Community: The Machine Underneath</title>
      <link>https://dev.to/machineunderneath</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/machineunderneath"/>
    <language>en</language>
    <item>
      <title>Git never deletes anything: what a commit actually stores</title>
      <dc:creator>The Machine Underneath</dc:creator>
      <pubDate>Thu, 10 Sep 2026 20:44:08 +0000</pubDate>
      <link>https://dev.to/machineunderneath/git-never-deletes-anything-what-a-commit-actually-stores-3nil</link>
      <guid>https://dev.to/machineunderneath/git-never-deletes-anything-what-a-commit-actually-stores-3nil</guid>
      <description>&lt;p&gt;You commit a file with an API key in it. You notice within a minute, remove the file, commit the removal, and push. The file is gone from your working tree — and the key is still readable with two commands. &lt;code&gt;git gc&lt;/code&gt; does not remove it either.&lt;/p&gt;

&lt;p&gt;That is not a bug, and not a mistake on your part. It follows from what a commit records: not your change, but a complete snapshot, in which every object is named after its own contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;D &lt;span class="nt"&gt;--name-only&lt;/span&gt;   &lt;span class="c"&gt;# every file ever deleted&lt;/span&gt;
git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;commit&amp;gt;:&amp;lt;path&amp;gt;             &lt;span class="c"&gt;# read any of them back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git gc&lt;/code&gt; does not help here, and that is consistent rather than surprising: the commit that introduced the file is still reachable, so the blob it points to is still reachable, and &lt;code&gt;gc&lt;/code&gt; only removes what nothing points to.&lt;/p&gt;

&lt;h2&gt;
  
  
  A commit is a snapshot, not a diff
&lt;/h2&gt;

&lt;p&gt;Three files, one commit. Change one byte in one of them, commit again. Count the objects: &lt;strong&gt;eight&lt;/strong&gt; — two commits, two trees, and four blobs. Not "one diff".&lt;/p&gt;

&lt;p&gt;The tree of the second commit lists a new hash for the file you touched and &lt;strong&gt;the same hashes as before&lt;/strong&gt; for the two you did not. That is what "complete snapshot with the unchanged parts reused" means, and it is why &lt;code&gt;git show&lt;/code&gt; has to compute the diff at display time: the repository never stored one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hundred identical files are one object
&lt;/h2&gt;

&lt;p&gt;Write the same line into a hundred files and commit. Distinct blob objects: &lt;strong&gt;one&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Deduplication is not an optimisation Git performs. The object's name is a hash of its content, so identical content cannot be two objects — there is nowhere for the second one to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the repository would be enormous
&lt;/h2&gt;

&lt;p&gt;This is the objection I expected to be the hard one, and it has a measured answer.&lt;/p&gt;

&lt;p&gt;Fifty one commits on a two-hundred-thousand-line file, each commit appending a line:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;loose objects, before packing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22032 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after &lt;code&gt;git gc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;847.96 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ratio&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26x smaller&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;how the 51 versions are stored&lt;/td&gt;
&lt;td&gt;2 in full, 49 as deltas of about 94 bytes each&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing was thrown away and not one object was rewritten. Packing keeps a base and a chain of deltas — which is the whole point: &lt;strong&gt;"not deleted" and "not stored twice" are different statements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running the same script on a different machine (git 2.43.0) gave 24276 KiB loose, 468 KiB packed, one object in full and fifty as deltas. The ratio depends on your git version and on how the file changes; the shape does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I had wrong before measuring
&lt;/h2&gt;

&lt;p&gt;Both are the kind of claim that gets repeated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The reflog window is thirty days, not ninety&lt;/strong&gt;, for anything your branches can no longer reach — which is always the case after a &lt;code&gt;reset&lt;/code&gt;, an &lt;code&gt;amend&lt;/code&gt; or a &lt;code&gt;rebase&lt;/code&gt;. Ninety days applies to entries still reachable from a branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fresh clone does contain the deleted object.&lt;/strong&gt; It simply does not check the file out. "Not in the working tree" is not "not in the repository".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mechanism, animated
&lt;/h2&gt;

&lt;p&gt;Fourteen minutes, no face, no music, every number measured:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4KC_1APTn4o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The script behind every number above
&lt;/h2&gt;

&lt;p&gt;Run it and post what you get. If a number comes out different, include your git version — I would rather fix the article than defend it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# What a commit actually stores — every number below comes out of git, not out of memory.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#   bash git-objects.sh&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Three questions, in order:&lt;/span&gt;
&lt;span class="c"&gt;#   1. is a commit a diff or a snapshot?&lt;/span&gt;
&lt;span class="c"&gt;#   2. what happens to a hundred identical files?&lt;/span&gt;
&lt;span class="c"&gt;#   3. if nothing is ever deleted, why is the repository not enormous?&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git init &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git config user.email a@b&lt;span class="p"&gt;;&lt;/span&gt; git config user.name t
git config commit.gpgsign &lt;span class="nb"&gt;false

echo&lt;/span&gt; &lt;span class="s2"&gt;"== 1. A commit points at a tree, not at a diff =========================="&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'alpha\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; a.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'beta\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; b.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'gamma\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; c.txt
git add &lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; one
git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; HEAD | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"-- the tree is a list of names and content hashes"&lt;/span&gt;
git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'HEAD^{tree}'&lt;/span&gt;

&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"-- change one byte in a.txt and commit again"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'alphaX\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; a.txt&lt;span class="p"&gt;;&lt;/span&gt; git add &lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; two
git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'HEAD^{tree}'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   a.txt has a new hash; b.txt and c.txt keep theirs, so the second commit"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   is a complete snapshot in which the unchanged parts are reused"&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"-- object count: 2 commits + 2 trees + 4 blobs = 8"&lt;/span&gt;
git count-objects &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt;

&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== 2. A hundred identical files ========================================="&lt;/span&gt;
&lt;span class="nv"&gt;D2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git init &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git config user.email a@b&lt;span class="p"&gt;;&lt;/span&gt; git config user.name t
git config commit.gpgsign &lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 100&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'same content\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"f&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; many
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"-- distinct blob objects for 100 identical files:"&lt;/span&gt;
git cat-file &lt;span class="nt"&gt;--batch-all-objects&lt;/span&gt; &lt;span class="nt"&gt;--batch-check&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$2=="blob"'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   one, not a hundred: the object name is a function of the content,"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   so identical content is one object by construction"&lt;/span&gt;

&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"== 3. Fifty one commits on a large file, loose vs packed ================"&lt;/span&gt;
&lt;span class="nv"&gt;D3&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git init &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git config user.email a@b&lt;span class="p"&gt;;&lt;/span&gt; git config user.name t
git config commit.gpgsign &lt;span class="nb"&gt;false
seq &lt;/span&gt;1 200000 | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/$/ line/'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; big.txt
git add &lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; base
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 50&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s changed\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; big.txt
  git add &lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; git commit &lt;span class="nt"&gt;-qm&lt;/span&gt; &lt;span class="s2"&gt;"edit &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"-- loose objects, before packing:"&lt;/span&gt;
git count-objects &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(count|size):'&lt;/span&gt;
git gc &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--aggressive&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; git gc &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"-- after packing:"&lt;/span&gt;
git count-objects &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(in-pack|size-pack):'&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"-- how the pack stores those 51 versions of big.txt:"&lt;/span&gt;
&lt;span class="c"&gt;# verify-pack columns: sha1 type size size-in-pack offset [depth base-sha1]&lt;/span&gt;
&lt;span class="c"&gt;# a delta row therefore has 7 fields, a fully stored object has 5&lt;/span&gt;
git verify-pack &lt;span class="nt"&gt;-v&lt;/span&gt; .git/objects/pack/&lt;span class="k"&gt;*&lt;/span&gt;.idx 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$2=="blob"{print (NF&amp;gt;=7 ? "delta" : "stored in full")}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"   nothing was thrown away and no object was rewritten — packing stores"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   one base and a chain of deltas, which is why 'not deleted' and"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   'not stored twice' are different statements"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line for your own repository, if you read no further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;D &lt;span class="nt"&gt;--name-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every file that ever left your project is on that list, and every one of them is still readable.&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>programming</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
