Journal

Two branches, 1,714 files apart: audit before you merge

In February 2026, Jewely's code existed in two versions. Both started from the same commit, dated 29 August 2025. One carried Crown-DP. The other ran Louis Julian in production. To...

Article4 tagged threads

In February 2026, Jewely's code existed in two versions. Both started from the same commit, dated 29 August 2025. One carried Crown-DP. The other ran Louis Julian in production.

To build a common CMS, I had to decide which of the two became the base. This note covers how I made that decision, what I ruled out, and what it cost.

The gap, measured

Measure Value
Files that differ between the two branches 1,714
Lines added and removed +143,590 / −99,248
Commits on the Crown-DP side since the split 643
Commits on the Louis Julian side since the split 1,435

The two branches had not done the same work. On the Crown-DP side, the architecture had changed: support for several shops, interchangeable payment methods, a block-based page editor, 198 tests, a Docker Swarm deployment. On the Louis Julian side, the branch had followed the life of a site in production: the Rolex pages, the ERP, maintenance.

A read-only audit, before any merge

I ran no merge. I first compared the two branches file by file, layer by layer, and gave each file a verdict.

Verdict What it means Files
Take The Crown-DP version replaces the other about 1,500
Keep both Each house's theme about 180
Merge by hand The file changed on both sides about 30
Keep the old one The file exists only on the Louis Julian side about 5
Ignore Temporary or abandoned files about 5

The audit changed the size of the problem. There were not 1,714 files to merge. There were about thirty to read line by line: the product model, collections, back-office roles, the Rolex services, the import commands.

The decision: one trunk, and the other house ported onto it

I had three options.

  1. Merge Crown-DP into the historical branch. Ruled out. The conflicts would have been massive, and the historical branch had no tests to say whether the merge had broken anything.
  2. Keep two codebases. Ruled out. Every feature would have been built twice, and the gap would have kept growing.
  3. Make Crown-DP the trunk, and port Louis Julian onto it. Chosen. Louis Julian becomes a theme and a configuration on top of the common code.

The criterion was not the age of the branch, nor its number of commits. It was the tests: the branch that had them could tell me whether the next steps were going well.

One database per house

The second decision was about data. The configuration had planned a shop_id column on every table, to keep all the houses in a single database. I did not implement it. Each house has its own server, its own database and its own Docker image.

               common code (Laravel, Vue back office)
                            │
            ┌───────────────┴───────────────┐
  APP_SLUG_NAME=crown-dp        APP_SLUG_NAME=louis-julian
            │                               │
  configuration + templates       configuration + templates
  Docker image                    Docker image
  database                        database

Two reasons. A query that forgets a filter cannot show one house's orders to another. A failed migration or a server that goes down affects one house only.

The cost is known: there are as many deployments as there are houses. "Built once" holds for the code, not for the release. The shop_id column remains plan B, written down as such in the roadmap, for the day the number of houses makes that cost too high.

What stops the two houses from drifting apart again

No condition on a house's name in the common code. What sets a house apart is an option, declared in its configuration:

function feature_enabled(string $feature, bool $default = false): bool
{
    return (bool) shop_config("features.{$feature}", $default);
}

An option declared for one house must exist for the other, even as false. The list of options becomes a contract between the houses and the common code.

Hardcoded names, searched for before the port. The audit found some: Crown-DP's identifier in a template helper, and a sender email address in three files. They were fixed before porting the second house, not after.

Tests, once per house.

strategy:
    fail-fast: false
    matrix:
        shop: [crown-dp, louis-julian]

fail-fast: false is deliberate. If one house breaks, I also want the other one's result, to know whether the defect is in the common code or in a theme.

What it cost

  • Cleaning up first. The repository had 109 branches. I kept 8 before starting, so that nobody would branch off a dead one.
  • Complete themes. A house can no longer borrow another house's template, so each house must have all of its own. The incident that forced this rule is told in One CMS for several houses, without them looking alike.
  • One more Git step. Each pre-production branch must contain only its own house's files. I chose per-directory .gitignore files rather than a sparse checkout, because there is nothing to configure on each machine. The price: after every merge from the main branch, the other houses' files have to be removed from the index. The decision note says so, and says when to reopen the question: at the fourth house.
  • Two steps, not one. Louis Julian's code was ported onto the common trunk on 9 and 10 March 2026, and the site ran on it in pre-production. Its production then moved to a variant of the CMS: an instance of its own, with its database, its server and its own updates.

What I take from it

  • Measure before choosing. The frightening number was 1,714. The useful number was 30.
  • Choose the trunk on what protects the next steps. Here, the tests.
  • Write down plan B and what triggers it. shop_id for a single database, the sparse checkout at the fourth house. The next person knows what was ruled out and when to come back to it.

The product side of the result is in the case study Crown-DP and the Jewely CMS.