From 52010c9733158be8fcb1acb2d43960cdc8d31b2c Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 4 Jul 2026 14:57:13 +0200 Subject: [PATCH 1/2] chore: register user/ as a git submodule Converts the orphaned gitlink (160000 with no .gitmodules) into a proper submodule pointing at the content remote, and absorbs user/.git into .git/modules/user so linked worktrees can populate user/. Bumps the pin from 75da83d to the current pushed origin/main (924cfc1) as a clean baseline. Enables per-worktree user/ checkouts for parallel dual-repo work. Co-Authored-By: Claude Opus 4.8 --- .gitmodules | 4 ++++ user | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0420f21 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "user"] + path = user + url = ssh://git@m038-nas.tail63ee39.ts.net:222/m038/intotheeast-com-content.git + branch = main diff --git a/user b/user index 75da83d..924cfc1 160000 --- a/user +++ b/user @@ -1 +1 @@ -Subproject commit 75da83d6e1d856176efdfe68eea0c292f602ba8c +Subproject commit 924cfc18e21d050151e211a1dff877921150d1df From 1710ad8612dee42493535528e7e0680cc89451b3 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 4 Jul 2026 15:13:50 +0200 Subject: [PATCH 2/2] docs: document dual-repo submodule workflow and pointer-bump convention Add docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md covering the outer-repo + user/ submodule structure, when to bump the pin (cross-repo feature boundaries, not routine content), the reachable/push-first rules, and the worktree + per-worktree dev-server flow. Update CLAUDE.md's folder explanation and add a "Dual-repo submodule structure" section. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 11 +- .../dual-repo-submodule-workflow.md | 104 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md diff --git a/CLAUDE.md b/CLAUDE.md index 7824842..17a3322 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ - **./**: Grav CMS dev environment for intotheeast travel blog - **scripts/**: Server install and maintenance scripts -- **user/**: Site content, config, pages, and theme (standalone git repo — do not modify from here) +- **user/**: Site content, config, pages, and theme — its own git repo (`intotheeast-com-content.git`), tracked by the outer repo as a **git submodule** (pinned commit). See "Dual-repo submodule structure" below and `docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md` - **docs/**: All plans, specs, and project documentation (moved here from `user/docs/` on 2026-06-19) - **docs/solutions/**: documented solutions to past problems (bugs, patterns, workflow gotchas), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in a documented area - **CONCEPTS.md** (repo root): shared domain vocabulary (Trip, Entry, Story, Active Trip). Relevant when orienting to the codebase or discussing domain concepts @@ -142,6 +142,15 @@ Always use `make` commands for anything on the production server (`make remote-i Only these folders are tracked in the `user/` Git repo: `pages/`, `config/`, `accounts/`, `themes/`. The `plugins/` and `data/` folders are excluded. +### Dual-repo submodule structure + +`user/` is a **git submodule** of the outer repo (`.gitmodules` at the root; git dir absorbed into `.git/modules/user`). Full workflow: `docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md`. The essentials: + +- **Two repos, two cadences.** Outer repo = dev environment (tests/docs/scripts/Docker). `user/` = content + theme, with its own remote and `make content-push` cadence. The outer repo pins an exact `user/` commit via the `user` gitlink. +- **Pointer-bump convention.** Routine content changes → **do not** bump the pin (leave it stale; harmless). At the **end of a cross-repo feature** → bump the pin once to the finished `user/` commit. Pin a commit reachable from `user/`'s published `main` (prefer the merge-to-main commit, not a squash-away branch tip), and **push `user/` before the outer repo** (superproject references a child SHA that must already exist upstream). The pin is dev-side coordination only — production pulls `user/` via the content webhook independently. +- **`M user` / `m user` is normal.** `M` = pin differs from `user/` HEAD (bump pending/intentional). `m` = submodule working tree dirty (e.g. local-testing `config/site.yaml`). Neither is an error — do not "fix" them by committing the gitlink or the `site.yaml`. +- **Worktrees for parallel work.** A worktree off `main` gets its own `user/` (`git submodule update --init user`) and can run its own dev server (`docker compose -p itte- up` — the `./user` mount is relative, so each worktree serves its own content). Tooling worktrees live under `.worktrees/` (excluded via `.git/info/exclude`). To add a commit to `main` while the main checkout is on another branch, use a throwaway `main` worktree rather than `git checkout main`. + ## 1. Environment modes ### Rule: do not switch modes during development diff --git a/docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md b/docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md new file mode 100644 index 0000000..61fe2f4 --- /dev/null +++ b/docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md @@ -0,0 +1,104 @@ +--- +title: Dual-repo submodule workflow — outer dev-env repo + user/ content submodule +date: 2026-07-04 +category: architecture-patterns +module: Repo structure — outer repo + user/ content repo +problem_type: architecture_pattern +component: git +severity: medium +applies_when: + - Starting a feature that touches both the outer repo and user/ (theme, plugins, pages) + - Setting up a git worktree for long-running work while doing other work in parallel + - Deciding when to bump the user/ submodule pointer in the outer repo + - A git worktree of the outer repo shows an empty or broken user/ directory + - Seeing a persistent "M user" / "m user" dirty state in the outer repo +tags: [git, submodule, worktree, dual-repo, user-repo, docker, content-sync, pointer-bump] +--- + +# Dual-repo submodule workflow + +## Context + +This project is **two independent git repositories** that happen to be nested: + +- **Outer repo** (`intotheeast-com.git`) — the Grav dev environment: `tests/`, `scripts/`, `docs/`, `docker-compose.yml`, `Dockerfile`, `Makefile`, `CLAUDE.md`. +- **`user/` repo** (`intotheeast-com-content.git`) — all site content and the theme: `pages/`, `config/`, `accounts/`, `themes/`. It has its own remote (the Gitea content mirror) and its own release cadence (`make content-push` → webhook → production pull). + +As of 2026-07-04, the outer repo tracks `user/` as a **proper git submodule** (`.gitmodules` at the outer root, git dir absorbed into `.git/modules/user`). Before that it was an *orphaned gitlink* — a `160000` tree entry with no `.gitmodules`, so git had no URL to populate or update it. That broke worktrees (a fresh outer worktree got an empty `user/`) and offered no supported sync path. + +## Why a submodule (and not untracking) + +Two options were weighed: make it a real submodule, or stop tracking `user/` in the outer repo entirely (gitignore it, symlink the real checkout in). + +The submodule was chosen deliberately, for one reason that outweighs its ceremony: + +- **Routine content churn is benign** — day-to-day entries/stories change `user/` constantly and never break the dev environment. Those changes do **not** need to be reflected in the outer repo. +- **Cross-repo *features* must be tracked together.** A feature like the journal post-form touches both repos (a plugin + theme JS/CSS in `user/`, and tests/docs in the outer repo). The outer repo pinning an exact `user/` commit records *"this dev-env state expects this content/theme state"* — so checking out the outer feature also gets the matching `user/` code. That coupling is real and worth having. +- It enables **per-worktree `user/` checkouts**, which is what makes true parallel work across both repos possible (see below). This was a hard requirement. + +The cost accepted: a persistent `M user` dirty signal (intrinsic to submodules under active development) and the possibility of gitlink merge conflicts between outer branches. Neither is removed by the submodule; they are the price of version pinning. + +## The pointer-bump convention + +The outer repo's `user` gitlink stores an exact `user/` commit SHA. **When to bump it:** + +- **Routine content changes → do not bump.** Push content with `make content-push` and leave the outer pin where it is. A stale pin during normal content work is expected and harmless. +- **At the end of a cross-repo feature → bump once.** When the feature's `user/` work is finalized, update the outer pin to the finished `user/` commit, as the final step of the feature (its own `chore: bump user pointer to ` commit, or folded into the final integration commit). + +Two rules keep the pin from dangling for other machines/clones: + +1. **Pin a commit reachable from `user/`'s published `main`.** Prefer the **merge-to-main commit**. Pinning a feature-branch tip is safe *only* if that exact commit survives onto `main` (fast-forward / no-squash merge); a squashed-away tip becomes an orphaned SHA and `git submodule update` fails elsewhere. +2. **Push `user/` before the outer repo.** The submodule golden rule: the superproject references a child SHA, so the child must already be pushed. `make content-push` handles the `user/` push — just do it before pushing the outer branch. + +Production is unaffected either way: prod pulls `user/` directly via the content-remote webhook, independent of the outer repo's pin. The pin is **dev-side coordination only**. + +## Parallel work: worktree + its own dev server + +The payoff. Because `docker-compose.yml` mounts `./user` **relative to the compose file**, and a worktree is a full copy of the outer tree (compose file included), each worktree serves *its own* `user/`. Two worktrees = two independent sites, no gitlink collisions. + +Set up a feature worktree off `main`: + +```bash +# outer worktree on a new feature branch +git worktree add .worktrees/ -b feat/ main +cd .worktrees/ + +# populate user/ at the pinned SHA, then branch it for the cross-repo work +git submodule update --init user +git -C user checkout -b feat/ + +# its own dev server — separate project name + port from the main checkout's :8081 +docker compose -p itte- up -d +``` + +`.worktrees/` is kept out of git via `.git/info/exclude` (local, shared across worktrees — no committed `.gitignore` change needed). + +### Teardown + +A submodule inside a linked worktree stores its git dir under `.git/modules/user/worktrees/`, so removing the outer worktree needs a second cleanup step: + +```bash +docker compose -p itte- down +cd "$(git rev-parse --show-toplevel)" # back to the main checkout +git -C .worktrees/ submodule deinit user # detach the submodule worktree +git worktree remove .worktrees/ # remove the outer worktree +git branch -d feat/ # if merged +``` + +### Landing a commit on main without disturbing the main checkout + +When the main checkout is mid-work on another branch, add a commit to `main` through a throwaway worktree instead of `git checkout main` (which would yank branches out from under an open IDE): + +```bash +git worktree add .worktrees/main-tmp main +git -C .worktrees/main-tmp cherry-pick # or edit + commit +git worktree remove .worktrees/main-tmp +``` + +## Gotchas + +- **`M user` / `m user` is normal.** Uppercase `M` = the pin differs from `user/` HEAD (bump pending or intentional). Lowercase `m` = the submodule working tree is dirty (e.g. an uncommitted `config/site.yaml` used for local testing). Neither is an error. +- **Gitlink merge conflicts still happen.** If two outer branches pin different `user/` SHAs, merging them conflicts on the `user` entry. Resolve by choosing the correct (usually newer, merged) SHA, then `git add user`. +- **Worktrees need `submodule update --init`.** A fresh outer worktree has an empty `user/` until you run it — it is not automatic. +- **The submodule git dir was absorbed** (`git submodule absorbgitdirs user`) so all worktrees share `.git/modules/user`. `user/.git` is now a gitfile (`gitdir: ../.git/modules/user`), not a directory. `make content-push`/`content-pull` still operate on `user/` normally. +- **Access requires the content remote** (SSH over Tailscale). A machine that cannot reach it cannot `submodule update` — but it could never clone `user/` anyway, so this is not a regression.