Skip to content
andydataguy

I committed the screenshots. Git kept a copy in every checkout.

AI & TECHNICAL · Quick Read~21 min read · 4,904 words09 AUG 2026

Committing the media was right for four reasons and I checked all four. I never checked what it would cost, and git multiplies that cost by the number of working copies you keep open. Every tool has a second meter.

[ BUILD LOG / PROCESS-FLOW ]
#git #worktrees

I. The decision was reasonable, which is the only reason it's worth writing about

I committed the screenshots.

The pile held diagnostic captures from a debugging run, rendered figures out of a build pipeline, and a few PDFs. All of it went into the repository next to the code that produced it, and I had four reasons.

The first was versioning: a figure and the code that rendered it moved together, so any commit handed me the exact pair. That one isn't theoretical. When a chart looks wrong three weeks later, the question is always whether the chart is stale or the code changed, and a repo where both live in the same commit answers it in one command instead of an afternoon.

The second was portability: anybody who cloned the repo got everything, with no second download and no step where they ask me for the assets. Third, it kept one source of truth, which is a standing rule in how I work and a good one, because the alternative is a bucket that drifts out of sync with the code and nobody notices until a build ships the wrong picture. Fourth, there was no second system: no lifecycle policy to maintain, no separate credentials that expire on a schedule nobody tracks, and no class of bug where the reference survives and the file doesn't.

All four are still true today, and nothing I learned later made any of them false.

I didn't get sloppy, and I didn't skip a review. I ran the review I knew how to run, and every check came back clean, because every check I ran was a benefit check. A tool's benefit and its cost show up on different schedules and in different units, and the benefit is the one you can see on the day you adopt it.

A shelf runs left to right like a timeline. At the left end, a lit amber lantern casts a level band of light along its whole length. Stacks of grey crates rise along the shelf from nothing to a tall pile that climbs above the band of light.
The benefit of committing media was complete on the first day and never grew. The cost started at zero and only climbed, which kept it invisible until it was large.

The cost arrived months later, in bytes, on a disk that ran out.

It didn't arrive dramatically, and there was no moment where something exploded. A build failed on a step that had worked a hundred times, the error had nothing to do with storage on its face, and it took a while to work back from the failure to a drive with nothing left on it. By the time the cause was legible, the cost had been accruing for months in a place I had no reason to look, which is the defining property of this kind of cost rather than an incidental detail of my particular week.

Committed media is cheap in the moment and permanent afterward. Git's storage model makes it permanent in a way I didn't understand. And the way I work multiplies whatever's in a checkout by the number of checkouts I keep open, which on a busy week is dozens. Each of those three facts is boring alone. Together they took a few gigabytes of screenshots and turned them into most of a drive.

None of the three is a git defect. Git does exactly what it says. The gap was in me: I evaluated what the tool would give me and never once wrote down what it would charge me, because on the day I decided, the charge was zero.

That gap has a name and a long paper trail, and a lesson with real prior art behind it beats one I invented last month. The mechanism is what carries the lesson over to whatever tool you're adopting this week.

II. What git actually does with a binary

Git is cheap for text because of delta compression. Git stores one version of a file in full when it packs objects, then describes the other versions as differences against it. A thousand-line source file edited two hundred times doesn't cost you two hundred file-sized objects. It costs one, plus two hundred small descriptions of what moved.

That mechanism does close to nothing for a PNG.

A PNG is already compressed. So is a JPEG, an MP4, most PDFs, and every asset a render pipeline emits. Compression works by removing redundancy, so a compressed file has already had its redundancy removed, and what's left looks like noise to a differ. Change one pixel and re-encode, and the bytes on disk have almost nothing in common with the bytes from the previous version. Git can't describe the second one as a small edit of the first, because it isn't one. Each version lands as its own full-sized object. Twelve rounds of a figure is twelve figures on disk, not one figure and eleven notes.

Two shelves. On the upper one, a single thick book is followed by a row of thin paper slips that take little space. On the lower one, a row of heavy grey crates, each full size, runs off the edge and bows the shelf.
For text, git keeps one full copy and describes each later version as a small difference. A compressed image has no difference left to describe, so twelve rounds of one chart are twelve full files.

The second half is the part that cost me: history is permanent.

Delete the file. Commit the deletion. Push it. You've freed exactly nothing. The blob still sits in every commit that ever carried it, because that's what a version control system is for, and every one of those commits still has to be reconstructible. The only way to get those bytes back is to rewrite history, which means every collaborator re-clones and every existing reference to a rewritten commit breaks.

A long train of coupled wagons, each carrying the same heavy amber crate, stretches back off the left edge. Only the last wagon is empty, and a worker stands beside it holding a broom.
Deleting a file and committing the deletion empties the newest wagon. Every earlier commit still carries the bytes, and the only way to unload them is to rewrite history for everyone.

So the cost profile is unusual. It's monotonic. It never goes down on its own. Ordinary maintenance doesn't touch it. And it accrues from the first commit while staying invisible until it's large, because nothing in the tool says anything at the moment you add the file. git add poster.png produces the same silence as git add handler.py.

I'll correct one thing before I overstate it, since overstating a mechanism is how you end up with a confident wrong model of a tool. Git does still compress packed objects, and a binary isn't literally stored as raw bytes every time. What fails is the delta step, the one that makes text nearly free. In practice, repeated versions of media accumulate at close to full size, and no amount of git gc recovers what deltas would have saved on text.

The platforms know this, which is the best available evidence that it's a class of problem rather than my personal error. GitHub warns on any file over 50 MiB, blocks anything over 100 MiB outright, recommends repositories stay under 1 GB, strongly recommends under 5 GB, guides 10 GB for the on-disk .git directory, and enforces a hard 2 GB limit on a single push. GitLab.com defaults to a 10 GB repository size including LFS objects and a 5 GiB push limit, configurable per project, group, or instance, and rejects the push when you exceed it.

LimitGitHubGitLab.com (default)
Single file50 MiB warning, 100 MiB hard blockGoverned by the repository limit
RepositoryUnder 1 GB recommended, under 5 GB strongly recommended10 GB including LFS objects
On-disk .git10 GB guidanceConfigurable per project, group, or instance
Single push2 GB5 GiB
Published limits, both platforms, as documented

Nobody writes limits like these for a hypothetical. They exist because enough people committed enough binaries to make them necessary.

I was nowhere near a single-file limit. Not one asset came close to 50 MiB. Every individual decision I made sat comfortably inside every published guideline, which is how the total got where it got: the limits are stated per file and per repository, and what cost me was neither.

Small amber parcels slide one at a time through a roomy metal sizing hoop. Beyond it, the parcels have piled into a heap that fills the right side of the room from floor to ceiling.
No asset came near the single-file warning, and the repository stayed inside every published guideline. The limits measure files and repositories, while the disk filled with a total neither one counts.

III. Worktrees, and the multiplier I hadn't priced at all

A git worktree lets one repository have several working directories checked out at once, each on its own branch. The working copies all share one object store. That's the whole feature, and it's good.

I use them constantly. Every parallel stream of work gets its own worktree, because a worktree is the cleanest scoping primitive I've found: the files under that path are exactly that stream's working set, so two streams editing the same file stop being a merge problem and start being two directories. When several streams run at once, that isolation is what keeps the arrangement tractable. I'd recommend the practice to anybody. I still do.

Now hold that next to a repository carrying a few gigabytes of committed media.

The mechanism deserves precision, because I had it wrong in my head for a while, and I had it wrong in the direction that made me look worse rather than better. A worktree doesn't copy history. The object store, meaning every commit and every blob, is shared once across all the worktrees attached to a repository. What each worktree gets is its own checked-out copy of the tracked files plus a small amount of metadata pointing back at that shared store.

One large glowing vault sits at the bottom. Three pipes rise from it to three separate rooms above, and each room holds its own identical stack of framed pictures.
Worktrees share one store of commits, so history is kept once. Each worktree checks out its own copy of every tracked file, so the pictures are kept once per worktree.

So the accurate sentence is one object store and N working directories, not N repositories.

That correction makes the accounting less bad than my first read of it, and it fixes nothing, because checked-out files are exactly where the media lives. Every tracked PNG on that branch exists as a real file on disk in every worktree that has it checked out.

Then it's arithmetic, and the arithmetic is boring, which is the problem. A few gigabytes of committed media, times dozens of open checkouts, is most of a drive. It's no spike or incident, and nothing about it looks like a failure while it's happening: it's a number climbing at a rate nobody set an alarm on, until a build failed for a reason that had nothing to do with the build.

Two things about that number make it hard to see coming. It's superlinear in a way that doesn't feel superlinear: adding one worktree costs whatever the working tree currently weighs, so the marginal cost of a worktree grows every time somebody commits another asset, and the marginal cost of an asset grows every time somebody opens another worktree. Both operations feel free at the moment you perform them, because each one is individually small. And neither shows up anywhere you'd routinely look. git count-objects -vH reports the object store, which is shared and therefore fine. The repository on the hosting side looks fine, because it is fine. The number that matters is the sum of directories on a local disk, and no git command reports it, because from git's point of view it isn't one thing.

Committing the media was defensible, and I'd defend it again on a repository of pure text. Running a worktree per stream of work is a practice I hold to, and it's the right one. Both halves passed review.

Neither decision was wrong. The cost lived in the interaction between them, and I never reviewed the interaction, because I'd reviewed each half on its own and each half came back clean.

Reviews are organized around decisions, and costs don't respect that boundary. A per-decision review catches a bad decision and structurally can't catch two good decisions whose product is expensive. Multiply anything by anything and you get a third number that neither factor's review ever looked at.

Two small tidy plots, each fenced and lit by a lantern at its gate. Behind and between them spreads a much larger dark field of heaped grey stones, with no fence and no light.
Committing media passed review, and so did running a worktree per stream of work. Their product, gigabytes times dozens of checkouts, sat outside both reviews, because no review covers how two settled decisions combine.

It gets worse when the two decisions are separated in time, which they usually are. The media went in during one stretch of work. The worktree practice arrived later, for unrelated reasons, and was evaluated on its own merits against the problem it solved. Nobody re-opens a settled decision because a new and unrelated one just landed, and there's no review that fires on "these two existing things now compose." Both decisions were correct when made and remained correct afterward. The product of them was never anybody's agenda item.

In general, whenever you adopt a tool that multiplies something, go find out what it multiplies. Worktrees multiply the working tree. Containers multiply the image. Parallel CI multiplies the cache. Replicas multiply the storage. Every one of those is a fine thing to adopt, and every one of them turns some other quantity you already have into a bigger quantity, and the multiplier is invisible on the day you adopt it because the thing it multiplies is small that day.

IV. The checkout that made the point

I wrote this note in a fresh worktree.

Creating it took minutes, most of that spent writing twelve thousand eight hundred files to disk, because that's what checking out a branch carrying committed media does. I sat and watched the counter for a while.

V. Every fix moves the cost, and one of them barely moves it at all

There's a standard answer to all of this, and the standard answer is Git LFS. Large files get replaced in the repository by small pointer files, the real bytes live on a separate server, and history stops carrying them. It works. It's the right call for plenty of teams.

It also has a meter, and the meter has two dials.

GitHub bills LFS on storage, charged hourly, and on bandwidth, charged per gibibyte downloaded. Free and Pro accounts include 10 GiB of each per month; Team and Enterprise Cloud include 250 GiB of each. Past that it's $0.07 per GiB-month for storage and $0.0875 per GiB for bandwidth. If the spending budget sits at $0, LFS usage gets blocked for the rest of the month once the included quota is gone, which converts a money problem into an availability problem at the worst moment.

Look at which dial is the dangerous one. Storage tracks how much you keep, which is roughly the number you were already thinking about. Bandwidth tracks how much gets downloaded, and most of that downloading is CI, pulling the same assets on every fresh checkout, every run, forever. A repository that's cheap to keep can be expensive to move, and the move happens on a schedule set by your merge rate rather than by anything you decided.

Two valve wheels on a pipe wall. A hand has turned the left wheel once and let go. The right wheel keeps turning as a mechanical arm, driven by a passing line of small carts, gives it another turn with every cart.
Storage grows when a person adds a file. Bandwidth grows every time automation checks the repository out, so its pace is set by how often the team merges.

So LFS converts a disk-space cost into a metered-bandwidth cost, and metered costs scale with automation while disk costs scale with content. For a team merging all day, that trade can go either way.

The same thing is true of every other option, which is the part I'd want somebody to tell me before I picked one.

Shallow clone (--depth=N) truncates history, so the clone is small. It also limits which commands work at all, produces behaviour people find surprising, and pushes stress onto later fetches. GitHub's own guidance discourages it for day-to-day developer use and points at CI as the place it belongs.

Partial clone (--filter=blob:none) keeps every commit and tree and defers file contents until something asks for them. Nothing is lost, and nothing is free either: checkout, blame, or reading an old file triggers a fetch for the missing objects. The bill is deferred to first access rather than removed, and a blobless clone used broadly enough drifts back toward the size of a full one. It also needs the server to support serving those objects on demand, so some of the work moves to the host.

Sparse-checkout reduces what gets written into the working tree without reducing what gets transferred. That makes it the one that matches my problem, since my problem was N copies of the working tree, not transfer.

An external store, whether that's object storage, a package registry, or something purpose-built like DVC or git-annex, takes the bytes out of git entirely. The price is atomicity: one commit no longer describes the full state of code plus assets. You now maintain a second lifecycle policy, a second set of credentials, and a class of failure where the pointer outlives the thing it points at.

OptionWhat it actually fixesWhere the cost lands instead
Git LFSHistory stops carrying large blobsMetered storage plus metered download bandwidth, scaling with CI rather than with content
Shallow cloneInitial transfer sizeHistory-dependent commands break; later fetches carry the stress
Partial cloneInitial transfer size, with history intactDeferred fetches on first access; server serves objects on demand
Sparse-checkoutWorking-tree size on diskTransfer is unchanged; you have to know which paths you need
External storeGit stays small and fastCode and assets stop being atomic; second auth, second lifecycle, reference rot
Five options, and the dimension each one moves the cost into

My thesis doesn't fully hold here, and I'd rather write that down than route around it.

I went into this expecting every mitigation to be a trap, and that expectation was wrong. Partial clone combined with sparse-checkout is close to genuinely low-friction, provided the workflow is narrow and stays narrow. If a workflow only ever needs a slice of the tree and a shallow slice of history, both mechanisms do what they say with very little coming back at you later. The deferred-fetch cost only materializes when somebody wanders outside the slice, and a disciplined CI job never does.

So "every benefit has a paired cost curve" is too neat as a slogan, and I'm not going to pretend otherwise to keep a tidy argument. The defensible version is narrower and more useful: these mitigations are cost transpositions rather than escapes, and the ambush comes from the new cost being invisible at the moment you adopt. Sometimes the new position is much better than the old one. That's a good trade, and it's still a trade, and knowing which dimension you just moved the cost into is the difference between choosing a trade and discovering one.

Four different containers stand in a row: a wooden crate, a pipe mouth, an hourglass-shaped bowl and a basket of knotted rope. The same heavy amber stone sits in each one, the same size every time.
LFS moves the cost to bandwidth, partial clone to first access, an external store to a second system to maintain. Each fix moves the stone to a different container; some carry it far more easily, and none makes it disappear.

VI. The pattern already has a name, and it isn't mine

I went looking for prior art expecting to find a gap, and ran into a well-worn idea with a century of paperwork behind it. The useful move is to stand on the citation instead of coining something.

Total cost of ownership is the umbrella and it fits almost exactly. Its claim is that purchase price is one component of a lifecycle cost that also includes support, maintenance, training, integration, migration, downtime, and ongoing operations, and that those components frequently dominate. Swap "purchase price" for "the benefit I evaluated at adoption" and that's this entire note in one sentence. TCO is broad and a little bloodless as a phrase, which is probably why it kept not occurring to me while I was busy being surprised.

Externalities sharpens it for the case where the bill doesn't land on whoever adopted the tool. The classical formulation, associated with Pigou, is that an actor's decision can impose costs on parties the price never accounted for. That's the exact structure of a developer committing assets and an infrastructure budget absorbing it, or an engineering team turning on verbose logging and finance meeting the result a quarter later. Private incentive underprices system cost. Nobody has to behave badly for it to happen.

Unit economics, and FinOps as its operational discipline, is the practice half. It measures cost per unit of output continuously and attributes it to whoever generates it, rather than reconstructing it from an invoice after the fact. It's the one with an actual method attached.

Two more get cited around this idea, and neither is the right label.

Jevons paradox, from William Stanley Jevons in The Coal Question (1865), says efficiency gains can raise total consumption because the lower cost per use expands the number of uses. It's a real neighbour of this problem and it describes something specific: a rebound effect driven by efficiency. It isn't a general theory of unmetered adoption cost. Worktrees made checkouts cheap enough that I kept far more of them open than I otherwise would have, so there's a genuine Jevons component in my story. It's a component, not the frame.

Technical debt, Ward Cunningham's coinage, is about a deliberate shortcut creating a future obligation that accrues interest. It rhymes, and it's a partial fit at best, because my problem wasn't a shortcut. I did the thorough version of the wrong analysis. Nothing was deferred; a whole dimension was never opened.

So the defensible sentence, which I'd rather write than a slogan with my name on it, is this: this is a total-cost-of-ownership problem with an externality attached, where the first-order benefit is immediate and visible and the operating cost curve arrives later, in a different meter, on a different budget.

In a small lamplit room, a person drops a pebble into a long pipe. The pipe runs through a wall into a second room, where a large stone falls out onto a scale and tips it hard as another person looks up from a desk of closed account books.
Developer tooling has no purchase order. One person types a command that costs nothing that day, and the bill arrives months later in another meter, on a budget that belongs to someone else.

What I'd add to the prior art, and it's small, is where it bites in engineering tooling specifically. TCO literature grew up around procurement, where somebody signs something and a finance function is watching. Developer tooling has no signing moment. You adopt a tool by typing a command, and there's no purchase order, no vendor review, no line item, and no meeting where anybody asks what this costs at scale. The decision that starts the meter looks exactly like a decision that starts nothing.

VII. The same shape, in tools that aren't git

Once the shape is visible it turns up everywhere, including in four tools I use and like.

Docker layer caching gets adopted for build speed, and it delivers. The cost curve is retained layers, registry storage that grows in a place nobody checks, and pull times that creep. Some providers meter it directly: CircleCI charges 200 credits per job for Docker Layer Caching, which puts the number on the invoice as its own line item. The same cost exists everywhere else and is just harder to find.

`node_modules` copied into an image gets adopted for reproducibility and install speed, and it's paid for in layer size, registry storage, and CI bandwidth on every pull, which is why so much Docker advice is really advice about .dockerignore and multi-stage builds.

CI caches get adopted to avoid recomputing what hasn't changed. The cost is storage plus a pruning policy that has no owner, because cache governance is nobody's favorite work and a stale cache does its damage quietly, sometimes as a wrong build rather than a big bill.

Observability retention gets adopted for debugging power, which is worth paying for and worth designing deliberately. The structure is what makes it dangerous: ingest, indexing, custom metrics, and retention are separate meters. Datadog, as one documented example, bills log ingest and log indexing as different lines, so usage can climb on a dial nobody is watching while the dial everybody is watching looks fine. High-cardinality metrics are the classic escalation, since cardinality grows from a code change rather than from a decision anyone recognized as a cost decision. A developer adds a tag carrying a user ID or a URL, which is an obviously good idea for debugging, and the number of distinct time series goes from hundreds to however many users you have. No line in that diff says "cost."

Notice what those four have in common, because it's the same property that got me. In every case the multiplier is a thing the team deliberately adopted and would adopt again: caching, reproducibility, parallelism, visibility. None of them is waste. Each one takes an existing quantity and repeats it, and repetition is exactly the behavior that was wanted. The cost is the feature working as designed, not a side effect of it working badly.

Four different objects on a plinth: a stack of layered plates, a bundle of nested boxes, a chest of small drawers and a spool of tangled thread. Lit from the front, each casts the same rising curved shadow on the wall behind.
Layer caching, bundled dependencies, CI caches and high-cardinality metrics each bill in a different unit. Each one repeats something on purpose, so every shadow climbs the same way.

One piece of the research changed what's in this section.

I went looking for named incidents with dollar figures. The famous bill-shock stories are all over the internet and almost none of them trace to a primary source. The pattern is a number in a vendor-comparison blog post, cited by another blog post, cited by a third, with no original invoice, ticket, talk, or postmortem anywhere underneath. I found a widely repeated five-figure overage attributed to unique-URL metrics and couldn't get it back to anybody who was actually there.

So there are no incident numbers in this note. The incidents almost certainly happened. I just couldn't verify the ones I found, and a number I can't trace is worth less to you than no number at all.

That's the transferable part. Vendor-documented pricing mechanics are citable, checkable, and dated. Anonymous horror-story totals are content marketing, and they're specifically the content marketing of companies selling the alternative. When somebody quotes you a number for what a tool cost a team, ask where the number came from. The mechanics are the real evidence. The totals are usually somebody's funnel.

Two chains hang side by side. The left one descends link by link into a large block of glowing bedrock. The grey one on the right curls around and links back into itself, with an empty dashed outline where an anchor should be.
Vendor pricing pages can be traced, checked and dated. Most famous bill-shock totals lead to a blog that cites another blog, and the chain closes without ever reaching an invoice.

VIII. What to do at adoption, while the cost is genuinely zero

The whole difficulty is that adoption day is the worst possible day to think about cost, and it's also the only day the thinking is cheap. The tool is exciting, it just solved something, the meter reads zero, and every number you'd want to reason about is a number you'd have to make up. Later, when the numbers are real, the tool is load-bearing and the analysis has turned into a migration.

There are five moves, in order. None of them take long, and the first one does most of the work.

  1. Name the meter, then name the second one. Write down the unit this tool bills in: bytes stored, bytes moved, requests, seats, runs, retention days, concurrent instances. Then keep going, because almost every tool that surprised anybody had more than one meter, and the ambush is never the meter on the pricing page's headline. LFS quotes storage and gets you on bandwidth. Observability quotes hosts and gets you on cardinality. Git doesn't quote anything at all, which is its own answer to the question.
  2. Model the curve, not the value. The value today is zero and tells you nothing. What you want is the shape: what does this cost at ten times my current usage, and what drives the ten. If the driver is something you control, fine. If the driver is automation, merge rate, or team size, the curve is going to move without anybody deciding to move it. Write the driver down next to the meter, because the driver is what you'll actually watch, and it's usually a number you already track for other reasons.
  3. Set the alarm now. A budget alert, a disk-usage check, a repo-size check in CI. At adoption it's a two-minute task nobody argues about. After the first surprise it's a retro, a remediation, and a policy. It's the same alarm at a wildly different price. An alarm is also the only kind of owner that survives, which is the argument that every artifact class needs a mechanical owner rather than somebody's standing discipline.
  4. Price the exit. What does leaving cost, in migration work and in whatever gets rewritten? Tools accumulate stickiness quietly, and history rewriting is the git-flavored version of an exit cost that starts near zero and grows monotonically. Notice which exit costs grow with usage and which stay flat: a flat exit cost means you can defer the decision cheaply, and a growing one means deferring is itself a decision.
  5. Decide how much of the above this deserves. Bezos's framing is the useful filter: a two-way door gets a quick look and a reversible trial, and a one-way door gets the full pass. Committing binaries is a one-way door wearing a two-way door's clothes, because git add is trivially reversible and the history it creates is not.
Two identical glass tanks filled by identical taps. The left one has a small float alarm mounted low, glowing where its bell rang as the water first rose past it. The grey tank on the right has no alarm, and its water has reached the brim.
The same rising cost fills both tanks. A budget alert or a repo-size check set at adoption rings while the level is still low; without one, the first signal is a failed build.

Klein's pre-mortem is worth running on anything in the one-way column: assume it's a year later and this went badly, and say why. The answer is almost always a meter nobody named in step one, which is what makes the ritual worth the fifteen minutes.

For git specifically, here's the version of this I'd hand to somebody starting a repo today.

Decide at the first binary, not the hundredth. The first one is a decision and the hundredth is a habit, and nothing between them will feel like a moment worth stopping for. If the assets are derived, meaning some renderer or build step produces them from something else in the repo, then the renderer is the source of truth and the output doesn't belong in history at all. Ignore it, commit the thing that generates it, and accept that a fresh clone has to build before it can look. If the assets are source, meaning nothing in the repo can regenerate them, then they're real inputs and they need a real home, which is either LFS or an external store, chosen by whether you'd rather pay in bandwidth or in atomicity.

And if you're running worktrees over a repo that already carries media, sparse-checkout is the lever that matches the problem, because it's the only one of the five that reduces the working tree rather than the transfer.

IX. Where this leaves me

Here's the mechanism in one paragraph, for handing to somebody else. Git can't delta-compress already-compressed files, so every version of a binary lands at close to full size. History is permanent, so deleting the file frees nothing without a rewrite. Nothing warns you at commit time, so the cost accrues from the first commit and stays invisible until it's large. And any practice that multiplies working copies, worktrees being the one that got me, multiplies whatever is in them. All four properties are documented, none of them is a defect, and I'd checked exactly zero of them before deciding.

An amber river runs through four stone gateways in a row: a plain arch, a portcullis, a cloth-wrapped gate and a gate that splits the flow into many channels. The river widens after each one and fills a large basin at the end to the brim.
Compressed files get no deltas, history keeps every version, nothing warns at commit time, and every worktree checks out another copy. Four documented behaviors, each doing what it says, add up to a full drive.

I still commit media in some repos. The four benefits I opened with are real, and I'd take them again on a small repo with one checkout and no CI pulling assets on every run. What changed is that I write down what it costs before I do it, and I know which lever to reach for when the answer comes back wrong.

If you've never had a tool arrive with a bill you didn't see coming, this note has nothing for you and I'd rather you spend the time elsewhere. And if you came here for a list of tools to avoid, I don't have one and wouldn't trust it if I did. That list changes every year. Name the meter on the day you adopt, while it reads zero and looking costs you nothing.

Sources worth your time: the `git worktree` documentation at git-scm.com, for the shared-object-store-many-working-trees model stated by the people who built it. GitHub's "About large files on GitHub" and "Repository limits" pages for the file, repository, and push limits, and GitLab's account and limit settings documentation for its defaults. GitHub's "About billing for Git Large File Storage" for the two-dial LFS meter and the current per-GiB rates. The GitHub blog's "Get up to speed with partial clone and shallow clone" for what each mechanism does and what it costs you afterward, and the partial-clone documentation at git-scm.com for the promisor-remote side of it. The openFrameworks wiki page on moving binaries out of the repo, and inovex's writeup on migrating binary bloat, for two independent accounts of the same slow failure. Jevons, The Coal Question (1865), for the rebound effect, which is a neighbour of this problem and not its name.

#git#worktrees#tooling#cost

Keep reading