<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Memory on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/memory/</link><description>Recent content in Memory on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Tue, 06 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/memory/index.xml" rel="self" type="application/rss+xml"/><item><title>Four lines cost a whole website</title><link>https://phpboyscout.uk/four-lines-cost-a-whole-website/</link><pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/four-lines-cost-a-whole-website/</guid><description>&lt;img src="https://phpboyscout.uk/four-lines-cost-a-whole-website/cover-four-lines-cost-a-whole-website_hu_649788fcac807b9a.jpg" alt="Featured image of post Four lines cost a whole website" /&gt;&lt;p&gt;Each time a project in the estate cuts a release, &lt;a class="link" href="https://colophon.phpboyscout.uk/" target="_blank" rel="noopener"
 &gt;colophon&lt;/a&gt; appends four lines to a YAML file on this blog, and the changelog page picks them up. Four lines. A date, a name, a tag and a URL.&lt;/p&gt;
&lt;p&gt;To write them, the release job was cloning the entire website into memory.&lt;/p&gt;
&lt;h2 id="what-it-was-doing"&gt;What it was doing
&lt;/h2&gt;&lt;p&gt;The adapter clones the blog, reads the file, adds the release to it, commits and pushes. In &lt;a class="link" href="https://repo.go.phpboyscout.uk/" target="_blank" rel="noopener"
 &gt;&lt;code&gt;go/repo&lt;/code&gt;&lt;/a&gt; terms the clone is already three options deep, because none of this wants the full history:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenInMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSingleBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithNoTags&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithShallowClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Shallow, one branch, no tags, and still the entire working tree of a website with a decade of posts in it, materialised in memory so that four lines can go into one file under &lt;code&gt;data/releases/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Git has sparse checkout for exactly this, and go-git supports it. &lt;code&gt;go/repo&lt;/code&gt; did not: its option list stopped at shallow, single-branch, no-tags and submodules, and the natural reading of that list is that sparse isn&amp;rsquo;t available.&lt;/p&gt;
&lt;h2 id="the-recipe-in-the-ticket"&gt;The recipe in the ticket
&lt;/h2&gt;&lt;p&gt;So the session building the adapter did the right thing and raised a ticket on &lt;code&gt;go/repo&lt;/code&gt; asking for it to be a first-class option. It also did a helpful thing: it had found a way to get sparse working through the abstraction, and it wrote that into the ticket as what it was doing meanwhile.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenInMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSingleBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithNoTags&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithShallowClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CloneOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NoCheckout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// reaching through&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...then, by hand, the bit that makes it sparse:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResetSparsely&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResetOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MixedReset&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data/releases&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That reach-through was allowed and the docs taught it: &lt;code&gt;go/repo&lt;/code&gt;&amp;rsquo;s option type was a bare callback over go-git&amp;rsquo;s own struct, so writing one by hand was how you reached any field the module had no option for. (That snippet compiles against &lt;code&gt;go/repo&lt;/code&gt; v0.4.0 and against nothing later.)&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a plausible recipe, and the shape you&amp;rsquo;ll find on issue threads. It&amp;rsquo;s also wrong in a way that doesn&amp;rsquo;t show, because &lt;code&gt;ResetSparsely&lt;/code&gt; under &lt;code&gt;MixedReset&lt;/code&gt; rewrites the index and &lt;strong&gt;writes no files at all&lt;/strong&gt;. The worktree after that second step is empty.&lt;/p&gt;
&lt;p&gt;The adapter then does this with it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// not there: os.IsNotExist, so nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// nil in, so a document of one entry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Filesystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CommitOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each of those lines is correct. It&amp;rsquo;s a read-modify-write over a structured document, which is what you want for YAML, because you can&amp;rsquo;t append to YAML by bolting bytes onto the end. &lt;code&gt;read&lt;/code&gt; returning nil for a missing file is deliberate and right: a project cutting its first release has no file yet, and that&amp;rsquo;s an ordinary answer, not an error.&lt;/p&gt;
&lt;p&gt;Put an empty worktree underneath it and those two correct behaviours combine into a commit that replaces the file with a single entry. Nothing fails. Nothing warns. The release goes green.&lt;/p&gt;
&lt;h2 id="the-spike-that-killed-it"&gt;The spike that killed it
&lt;/h2&gt;&lt;p&gt;The version of this story where it ate live data is a better story, and it isn&amp;rsquo;t the one I&amp;rsquo;ve got.&lt;/p&gt;
&lt;p&gt;The session that picked up the ticket didn&amp;rsquo;t implement the recipe that came with it. It built a fixture first, four files across three directories, cloned it into memory, ran each of the three possible second steps, then did the append and the commit and looked at what came out.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;second step&lt;/th&gt;
 &lt;th&gt;worktree after&lt;/th&gt;
 &lt;th&gt;the file in the commit&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ResetSparsely&lt;/code&gt;, &lt;code&gt;MixedReset&lt;/code&gt; (the recipe in the ticket)&lt;/td&gt;
 &lt;td&gt;empty&lt;/td&gt;
 &lt;td&gt;replaced by the one new entry&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ResetSparsely&lt;/code&gt;, &lt;code&gt;HardReset&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the one directory&lt;/td&gt;
 &lt;td&gt;original plus the new entry&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Checkout&lt;/code&gt; with &lt;code&gt;SparseCheckoutDirectories&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the one directory&lt;/td&gt;
 &lt;td&gt;original plus the new entry&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three rows, and the recipe in the ticket is the one that eats your file. The finding went back across to the session that raised it three minutes after the ticket was filed, before the adapter merged: the recipe leaves the worktree empty, the append truncates, go and look at the spec and the adapter before this goes anywhere.&lt;/p&gt;
&lt;p&gt;So the file is fine. Every release this blog has ever listed is still in it. What nearly happened is the gap between a few minutes of measuring and a bug that reports success for as long as you leave it be.&lt;/p&gt;
&lt;p&gt;The recipe being wrong doesn&amp;rsquo;t bother me much. That trap is sat on public issue threads for anyone to copy, it needs the right reset mode and the wrong one fails silently, and I&amp;rsquo;d have copied it myself&amp;hellip; it looks like the answer. What gets me is that it went into a ticket as a fact before anyone had run it, and a throwaway fixture found it out before anybody built on it. If the decision turns on behaviour, go and watch the behaviour.&lt;/p&gt;
&lt;h2 id="one-option-both-steps"&gt;One option, both steps
&lt;/h2&gt;&lt;p&gt;The fix doesn&amp;rsquo;t defend against that recipe. It removes the ability to write it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;worktree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenInMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSingleBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithNoTags&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithShallowClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSparseCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data/releases/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That last option sets &lt;code&gt;NoCheckout&lt;/code&gt; on the clone &lt;em&gt;and&lt;/em&gt; does the sparse checkout afterwards, and the step it runs is &lt;code&gt;Checkout&lt;/code&gt; with the sparse directories rather than &lt;code&gt;ResetSparsely&lt;/code&gt;. go-git&amp;rsquo;s &lt;code&gt;Checkout&lt;/code&gt; composes the reference to move to with the sparse list and picks the reset mode itself, so the module never chooses &lt;code&gt;MixedReset&lt;/code&gt; and neither does anybody calling it. One name, no ordering, no mode, no go-git import reaching the consumer.&lt;/p&gt;
&lt;p&gt;The half-measure was tempting and I&amp;rsquo;m glad it didn&amp;rsquo;t happen: an option called &lt;code&gt;WithNoCheckout()&lt;/code&gt; that sets the flag and leaves the second step to the caller keeps the whole trap and gives it a friendlier name.&lt;/p&gt;
&lt;p&gt;Owning that second step cost a type change, because &lt;code&gt;CloneOption&lt;/code&gt; had been &lt;code&gt;func(*git.CloneOptions)&lt;/code&gt; and go-git&amp;rsquo;s struct has nowhere to carry a post-clone instruction, nor any way to reach the handle that would run one. &lt;code&gt;go/repo&lt;/code&gt; owns a &lt;code&gt;CloneOptions&lt;/code&gt; struct now, embedding go-git&amp;rsquo;s, so a hand-written option still reaches every field it ever did. Below 1.0, so it went out with a &lt;code&gt;Release-Note:&lt;/code&gt; trailer instead of a breaking-change footer, and the migration cost to the one real consumer came out at nothing, measured, not assumed: colophon had no hand-written clone options in committed code at all.&lt;/p&gt;
&lt;p&gt;The fixture was still up, so I had it measure the saving I&amp;rsquo;d been assuming was large. Three hundred incompressible 20 KiB files: 12.6 MiB of live heap after a full in-memory open, 5.4 MiB after a sparse one. Real, and less than half what I&amp;rsquo;d have guessed, because the packfile gets fetched and held either way. Sparse saves the working-tree copy, not the objects. Keeping the objects small is the job of the shallow, single-branch, tagless options, and they&amp;rsquo;re separate options for a reason.&lt;/p&gt;
&lt;h2 id="two-ways-to-get-it-wrong"&gt;Two ways to get it wrong
&lt;/h2&gt;&lt;p&gt;The option refuses an empty directory list, and it refuses a list that matches nothing, and I&amp;rsquo;d argue for those being two different errors rather than one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ErrNoSparseDirectories&lt;/code&gt; is &lt;code&gt;WithSparseCheckout()&lt;/code&gt; with nothing in it, which compiles, and is a plausible bug the day the directory comes from a config value that was never set. Applying that as a full checkout silently spends the memory the caller asked to save; applying it as an empty sparse set hands back an empty worktree and says nothing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ErrNoSparseMatch&lt;/code&gt; is the typo, a directory the tree doesn&amp;rsquo;t have, and it&amp;rsquo;s in there because the colophon session went and measured what happens without it. The checkout succeeds, the worktree is empty, and the first sign of anything wrong is &lt;code&gt;cannot create empty commit: clean working tree&lt;/code&gt; from a commit three calls later. An idempotent writer can&amp;rsquo;t tell that from &amp;ldquo;there was nothing to do this time&amp;rdquo;, so a mistyped path reports success indefinitely.&lt;/p&gt;
&lt;p&gt;An empty list is a value never set. A list matching nothing is a value set wrong. Different mistakes, different fixes, and both land where the mistake was made instead of three frames downstream in a disguise.&lt;/p&gt;
&lt;p&gt;The ticket was right about the need, right about the cost, and right that the module should own this. It was wrong about one reset mode, in a way that comes out green.&lt;/p&gt;</description></item></channel></rss>