Git LFS to lore, and good riddance

Git LFS keeps large binaries out of the pack format. It does not make Git understand binaries - it just relocates the problem to a side channel with its own failure modes. lore was built binary-first from the object model up. Below: the technical case for why that distinction actually matters, and the migration path once you're convinced.

Git's binary problem, and the band-aid over it

Git's object model assumes a text-shaped world: hash a blob, and when history moves, zlib-deflate a delta against the nearest prior blob. That's a fine trick for a 40-line diff in a header file. Point it at a 2 GB .uasset and the delta heuristic finds nothing to exploit, gives up, and stores the object whole. What did you expect a text-diff algorithm to find in common between two zstd-compressed mip chains? Your .git directory quietly becomes a second copy of your asset library, then a third, then a fourth. LFS doesn't fix this - it moves the whole-object tax out of the pack and onto a pointer file, so you get to keep paying it, just somewhere less visible.

Pain pointWhat's actually happening
Every edit re-stores the whole file A delta filter tuned for line diffs finds no structural similarity between two zstd-compressed texture mips. Flip one texel and you get a brand-new multi-gigabyte object, stored in full, next to the old one.
History only grows There's no garbage collector deciding old asset revisions are unreachable and safe to drop - they sit in the object store forever, whether any live branch references them or not.
Dedup is whole-blob, exact-match only LFS addresses content by the SHA-256 of the entire file. "Nearly identical" isn't "identical" to a hash function - move one texel and the whole object is a new, unrelated hash with zero storage reuse against the old one.
No partial or lazy fetch The smudge filter pulls an LFS object in full before checkout proceeds. There is no byte-range concept - reading one KB of a 10 GB file still downloads all 10 GB.
Pointer-stub roulette A pointer file is ~130 bytes of YAML-flavored text pretending to be your asset. Skip the smudge step - a fresh clone, a misconfigured CI runner - and you commit that pointer text as if it were the real texture. Nothing stops you.
A meter running you don't control Hosted LFS storage and bandwidth are billed as their own SKU, separate from the repository. A team can be nowhere near its Git quota and still get throttled just for checking out assets.
Locking is a courtesy, not a lock git lfs lock writes a row to a server-side table that git push never reads. Enforcement happens in Slack, when someone notices, after the fact.
History rewrite is a scorched-earth op git lfs migrate import rewrites every commit that ever touched a tracked path - new tree hashes, new commit SHAs, top to bottom. Every existing clone, fork, and open PR is now stale and has to be re-cut.

None of this is LFS being badly written. It's what you get when you staple a side-channel onto a version control system whose object model was never told binaries exist. Why would a hash-the-whole-blob dedup scheme, designed for occasionally-duplicated files, hold up against a repository that's mostly large files getting incrementally re-baked?

Where lore stops apologizing for binaries

  1. Binaries aren't a plugin. lore is binary-first by design - every file, text or binary, goes through the same content-addressed storage path. No pointer file, no smudge filter, no second tool to forget to run.
  2. Chunk boundaries are content-defined, not file-defined. lore runs a FastCDC rolling hash over the byte stream and drops a boundary wherever it matches a pattern - averaging 64 KiB chunks, with a 32 KiB floor and 256 KiB ceiling. Edit one region of a multi-gigabyte file and only the fragments that actually moved get re-hashed and re-stored; everything else in the file keeps its existing addresses.
  3. Dedup runs on fragments, addressed by BLAKE3 - not on a SHA-256 of the whole blob. Identical byte ranges dedupe across unrelated files, not just across byte-for-byte duplicate ones.
  4. Sparse is the default, not a flag you discover after getting burned. A clone materializes only what you ask for; the rest is fetched fragment-by-fragment, lazily, on demand. No experimental-mode warning label attached.
  5. One clone step, full stop. lore clone gets you real bytes directly - no pointer-then-smudge relay race, and nothing to misconfigure into shipping a stub to your teammates.
  6. Disk is a hard cap, not a running meter. Your workspace's quota is enforced by the filesystem itself - writes stop at the limit, deletes always work - with no separate bandwidth bill accruing in the background.

Credit where it's due, though: lore's own file locking is exactly as toothless as LFS's today - advisory, unenforced at push. Migrating won't fix that particular office-politics problem. It'll just change which tool's docs you're re-reading when it bites you.

How to actually migrate

The good news: if you don't need the crime scene - your Git history - preserved inside lore, migrating is almost insultingly simple. A repository is just whatever files exist right now; lore has no opinion about how they got that way. What lore does not have is a git-lfs migrate-shaped importer - carrying the full commit graph over means hand-rolling a converter that replays every historical tree as a lore revision, and nobody has shipped that for you. Really want to be the one who writes it? If not: archive the Git+LFS repo read-only and move on.

  1. Materialize the LFS content first. Skip this and you'll commit a folder of YAML pointer stubs and call it a migration:
    git lfs pull
  2. Create a workspace and repository on folkorama, then install the lore CLI and get a credential, same as in Getting started.
  3. Clone the empty repository using the URL from its dashboard page:
    lore clone lores://<your-subdomain>.…/<repo-id> migrated-repo
    cd migrated-repo
  4. Copy the working tree across, leaving Git's own bookkeeping behind:
    rsync -a --exclude='.git' --exclude='.gitattributes' --exclude='.gitignore' \
      /path/to/git-lfs-checkout/ ./

    A .gitignore maps roughly onto a .loreignore - the two aren't byte-compatible, so port the patterns by hand rather than copying the file.

  5. Stage, commit, push.
    lore stage .
    lore commit -m "Import from Git LFS"
    lore push
  6. Verify, then actually cut over. Run lore status, open a few of the large assets to confirm they're not pointer text, and point your team at the new clone URL. Keep the old Git+LFS remote around, read-only, for archaeology.

Want the history too? In principle, sure: walk the Git+LFS commit graph, materialize each historical tree, replay it as a stage + commit pair against the API. In principle you could also hand-assemble a compiler. Neither is a Tuesday-afternoon job - budget for it honestly, and don't let anyone promise your team a magic history-import button that doesn't exist.