Post

Gitignore is not a backup — how I almost lost my content/

Administrator

I thought I was being tidy.

This site keeps Markdown under content/. At some point that folder became runtime data: edited in Studio, mounted into Docker, not something I wanted in every commit. So a feature branch did the “grown-up” thing:

  1. stop tracking content/
  2. add /content/ to .gitignore
  3. merge into main

I even have a small helper — call it merge-delete — that merges a branch and then deletes it. Convenient. Until the merge finished and my posts directory looked like this:

  • one leftover post
  • one homepage markdown file
  • everything else gone from disk

Luckily I still had content.tar.gz. That is not a strategy. That is luck.

What actually deleted the files

Branch deletion did not wipe the data. The merge did.

When Git still tracked files under content/, those paths lived in the commit tree. The “untrack content/” commit does not only update .gitignore. It removes those paths from the tree — the same kind of change as deleting the files in that commit.

Merge that into main, and Git updates your working tree to match the result. Tracked files that no longer exist in the merged tree are removed from disk. After that, /content/ is ignored, so a later checkout will not bring them back. From Git’s point of view the job is done. From yours, the blog body just vanished.

Ignored-and-never-tracked files usually survive checkouts. The dangerous sequence is:

tracked → deleted in a commit → ignored

That is what merge-delete applied in one step.

Prevention checklist

1. Back up before you untrack

Before the commit that removes content/ from the index, take a copy Git cannot touch:

bash
tar -czf ~/backups/content-$(date +%Y%m%d).tar.gz content/

Or rsync to another disk / object storage. Do this before git rm -r --cached content/ (or whatever untrack step you use), not after a hopeful merge.

2. Untrack without deleting the working copy — then verify

Typical safe-ish untrack:

bash
git rm -r --cached content/
# add /content/ to .gitignore
git commit -m "Untrack content/ (runtime data)"

--cached drops paths from the index but leaves files on disk in that working tree. The trap appears later, when another clone or another branch checkout materialises the “files deleted from the tree” side of the same commit. So still treat the backup as mandatory.

After untracking, confirm what Git thinks:

bash
git ls-files content | wc -l   # should be 0
ls content/posts | wc -l       # should still be your full set
git check-ignore -v content/posts

3. Keep runtime content outside the repo tree

If content/ is Docker runtime data, prefer a bind mount or named volume that is not the same folder Git checks out into.

Examples of the idea:

  • host path: ~/blog-data/content → container /app/content
  • named volume for content, repo checkout only for app code

Then a merge that rewrites the repo working tree cannot empty your only copy.

This project’s .gitignore already scopes that intent with a leading slash:

gitignore
/volumes/
/content/

so only the repo-root paths are ignored — not something like app/components/content/. The ignore rule is fine. The missing piece was a durable home for the data besides the Git working tree.

4. Make merge helpers refuse “delete tree + ignore” surprises

If you keep a merge-delete alias, add a guard before merge:

bash
# sketch — refuse if the branch removes a previously tracked content tree
if git diff --name-status main..."$branch" | grep -E '^D\tcontent/'; then
  echo "Branch deletes content/ paths. Backup first, or abort."
  exit 1
fi

Or at least print a loud warning and require a typed confirmation when the incoming diff deletes anything under content/ or other data roots.

Convenience scripts should be careful around data directories. Merge + delete is fine for feature code. It is hostile to “this folder used to be in Git and now it is not”.

5. Prefer volume restore over Git restore for day-to-day ops

Yes, you can often recover old tracked files from history:

bash
git checkout '<commit-before-untrack>' -- content/
# or
git archive '<commit-before-untrack>' content | tar -x

That only works for whatever was committed. Studio edits, drafts, and uploads that never hit Git are gone unless you backed them up. Once /content/ is ignored, Git is no longer your CMS backup.

Treat recovery as:

  1. restore content.tar.gz / volume snapshot
  2. only use git checkout / git archive as a secondary source for older published snapshots

6. Automate boring backups

Something small and boring beats another heroic evening:

  • nightly tar or rclone of the content mount
  • retain a few dated archives
  • optionally dump whatever else lives next to it (data/, media buckets, SQLite, etc.)

If content is sacred enough to publish, it is sacred enough for a cron job.

The rule I am sticking to

Ignoring a folder does not mean “Git will leave my only copy alone forever.”
It means “Git will stop protecting this folder.”

So for runtime Markdown and media:

  1. backup first
  2. untrack second
  3. store the live copy on a volume or path outside checkout drama
  4. keep merge helpers from silently applying mass deletes under data roots

content.tar.gz saved me once. The goal is never needing that sentence again.