feat(tia): support projects in git repository subdirectories (monorepos) - #1809
Open
einar-hansen wants to merge 1 commit into
Open
feat(tia): support projects in git repository subdirectories (monorepos)#1809einar-hansen wants to merge 1 commit into
einar-hansen wants to merge 1 commit into
Conversation
Tia previously refused to run when the project root was not the git repository root, telling monorepo users to "give the project its own git repository". The underlying problem was a path-space mismatch: git's plumbing (status --porcelain, diff --name-only, show SHA:path) speaks repository-root-relative paths, while the dependency graph, watch patterns and coverage edges all speak project-root-relative paths. ChangedFiles now detects the project's location inside the repository once (git rev-parse --show-prefix) and translates at every git boundary: - status/diff output is mapped to project-relative paths, and paths outside the project subtree are dropped — they are as invisible to the project's graph as files outside the repository are for a root-level project, and leaking them through would hit the unknown-file catch-all and force a full rerun on every sibling-project change; - the diff listing runs with -z (git C-quotes non-ASCII paths, which would silently fail the prefix match and drop a real change — a stale replay) and --no-renames (rename detection reports only the NEW path, so a project file renamed to a path outside the subtree would vanish from the listing; as delete+add both sides surface); - git show SHA:path (behavioural-change filtering, composer.lock drift summaries) resolves project-relative paths through the prefix. A new GitRepository helper locates the governing repository by walking ancestors, fixing three more root==project assumptions: Fingerprint's tracked-lockfile check, BaselineSync's GitHub-repo detection (baseline fetching silently never activated for subdirectory projects), and Storage's origin-based project key — which now also carries the subdirectory prefix, so `apps/api` and `services/api` in one repository never share a graph store. Root-level projects keep their exact existing behaviour: the prefix is empty and every translation is the identity. The TiaRequiresRepositoryRoot exception is removed along with the guard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What:
Description:
Love the Tia engine! Unfortunately I couldn't try it on one of my projects, since it's a monorepo — so I decided to use AI to build monorepo support instead. This PR was written by Claude Fable (Anthropic); I've reviewed it multiple times and it works on my machine. 🙂
The problem
Today, Tia refuses to start if your Laravel app lives in a subfolder of a bigger repo:
That's a dealbreaker for monorepos — one repo containing
apps/api,apps/web, shared tooling.The reason for the restriction turns out to be simple: git and Tia describe files from two different starting points. When Tia asks git "what changed?", git answers with paths measured from the repo root:
apps/api/app/Models/User.php. But Tia's dependency graph stores everything measured from the project root:app/Models/User.php. When the project is the repo root, those are the same string and everything works. In a subfolder, every single lookup misses — so the current code just blocks the whole scenario.The fix
Teach the git boundary to translate.
ChangedFilesasks git once where the project sits inside the repo (git rev-parse --show-prefix→apps/api/), and then:apps/api/app/Models/User.phpbecomesapp/Models/User.php, which is what the graph knows.composer.lockat an old commit becomesgit show <sha>:apps/api/composer.lock.apps/web, your api tests shouldn't rerun — sibling projects are as invisible to this project as files outside the repo are for a normal project today. Without this, every unknown path would hit Tia's "I don't know this file, rerun everything" safety net, and monorepo replays would never actually replay.Two git flags matter more than they look:
-z— without it, git escapes filenames containing non-ASCII characters ("apps/api/\303\246.php"instead ofapps/api/æ.php). An escaped path doesn't match the prefix, so the change would be silently dropped — meaning tests that should rerun would be replayed from cache instead. That's the one failure Tia can never afford.-zmakes git output raw bytes.--no-renames— when git detects a rename it only reports the new location. If a file moves out of the project folder, the new path is outside the prefix and gets ignored — and the old path is never mentioned, so the file just… vanishes from Tia's view, while its dependents keep replaying stale results. Disabling rename detection makes a rename look like "old file deleted, new file added", so both sides are visible. (Fun side effect: this also covers root-level projects where someone hasdiff.renames=truein their global git config, which quietly has the same problem today.)Beyond
ChangedFiles, three other places assumed "project root == repo root", all fixed via a small newGitRepositoryhelper that walks up parent directories to find the governing.git:BaselineSynclooked for.git/configinside the project folder to figure out which GitHub repo to fetch baselines from — in a subfolder there's no.gitthere, so baseline fetching silently never activated.Storagederives its cache key from the git origin URL. Two apps in the same repo share that URL, soapps/apiandservices/apiwould have collided on one graph store — the key now includes the subfolder path. For a project at the repo root the subfolder is empty, so existing root-level projects keep byte-identical keys (pinned by a test) and no caches are invalidated by upgrading.Fingerprint's check for "is this file gitignored?" needed to know it's inside a repo at all.Root-level projects are untouched by all of this. The prefix is an empty string for them, so every translation is a no-op, storage keys come out byte-for-byte identical (nobody's cache gets invalidated by upgrading), and a regression test pins that.
Verification
composer testpasses in full, including updated visual snapshots.apps/apiof a monorepo. Recording takes ~6 minutes; after that, a full-suite replay takes ~10.5 seconds, editing one test file reruns only that file, and commits that only touchapps/webor the repo root replay 100% with zero reruns.The real-world runs were done with the fix from #1806 applied — we hit #1785 independently while testing and landed on the same fix. Without it, suites whose teardown touches the Laravel container see replays alternate green/red, as described in that issue.
One known limitation: the baseline artifact name is fixed (
pest-tia-baseline), so two Tia-baselined projects in the same repository would collide on it. Happy to follow up on that if it's a setup you'd want supported.