Skip to content

Subtree Split or Rsync Snapshot: Picking the Publish Mechanism

Adityo Guni Waluyo

One folder or five scattered paths? The shape of your include set decides when subtree split is safe and when an rsync snapshot wins.

My split-publish script ran smoothly for months while the public part was one folder. Then the public set changed: a custom theme folder, two plugin folders, mu-plugins, and composer manifests. Five scattered paths that all had to be exposed at once. My first instinct was to bend git subtree split into covering multiple prefixes. Wrong. The command git subtree split -P <prefix> -b <branch> builds a synthetic history containing only the commits that touched that one prefix, moves its contents to the repo root, and prints a single commit id you can push as a standalone repo [1]. It takes exactly one prefix. I was solving the wrong question. The real question was never which tool is strongest, it was what shape my include set has.

The include set shape decides the mechanism

If the public part is one folder, subtree split wins. The folder's full history survives intact, and the official manpage documents a property most people miss: repeated splits of the same history with the same settings produce identical commit ids [1]. The price: the published branch is rewritten history, every republish is a force-push, and the public branch must be treated as read-only. GitHub's own guide, now built on git filter-repo --subdirectory-filter, reminds you that a split repo carries none of the original branches and tags [2]. Healthy pattern, as long as you know you are publishing a reconstruction, not the same repo.

Once the public set is five paths in three different corners, I stop forcing a single prefix and switch to an rsync allowlist plus a snapshot commit. Each publish is an ordinary commit on top of the previous snapshot, carrying the full-repo commit hash it was built from, pushed normally. Never forced. The public branch stays stable and auditable: every public commit traces back to a source commit. There is a third door for the heaviest rewriting jobs, git filter-repo, now recommended by the git project over git filter-branch, with the heaviest requirements too: git 2.36.0+ and python3 3.6+ [4]. GitHub's split tutorial targets git 2.22+ [2]. Two different numbers for two different contexts, not a contradiction.

The third door and the first-match trap

The part nobody expects bites you on the rsync side. Include/exclude rules are first-match-wins: the first rule that matches takes effect, so an include that should override an exclude must be listed before it [3]. An excluded directory is not scanned at all by the sender, which doubles as a traversal saving on big trees [3].

MechanismInclude set shapeHistoryPush type
git subtree splitOne folderSynthetic, preservedForce-push
rsync + snapshot commitMany scattered pathsSnapshot chainPlain push
git filter-repoComplex rewritingRewrittenForce-push

My own allowlist template shows the concrete pattern: parents before children, then a double + line pulling in everything under an allowed folder. The export script carries two safety nets. The rsync run ends with --exclude '*' so anything outside the allowlist falls to a default deny, and a pre-commit check aborts the push if wp-config*.php, .env, or .pem files made it into the view folder. Then there is --delete, which removes files in the destination that are no longer in the source list [3]. This is why first-match-wins matters twice: it decides what gets copied AND what may be deleted. On the receiving side, an exclude can protect a file from deletion while an include puts it at risk. One misplaced line can leak internal files into the public view or silently shield stale files from cleanup forever.

The interesting part is not the number of options but the order of the decision. The include set shape is a fact you can check five seconds before writing the script; tool preference is just taste, and taste drifts. One folder: subtree split, full history, accept the force-push. Many scattered paths: rsync allowlist with a snapshot chain, a stable public branch that never demands force. Heaviest rewriting: git filter-repo, strongest door, highest requirements. Three tools, one question up front: what does your include set look like?

Sources

Related articles