Repository navigation
feat(multi-repo): projects that own several repositories - #7699
Draft
harshithmullapudi wants to merge 14 commits into
Draft
harshithmullapudi wants to merge 14 commits into
harshithmullapudi wants to merge 14 commits into
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:13 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:14 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:14 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:14 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:14 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 21, 2026 02:14 — with
GitHub Actions
Inactive
|
No installed development build can load this pull request as an update: its native fingerprint is |
Contributor
🚀 Preview Deployment🔗 Preview Links
Preview updates automatically with new commits |
3 tasks done
…pace A project keeps an ordered folder list (`project_folders`), and creating a workspace checks every folder out into one container, one worktree per folder, recorded in `workspace_repos`. Position 0 stays mirrored onto `projects.repo_path` and `workspaces.worktree_path`, so every single-repo surface keeps reading exactly the row it always did and a project with one repository behaves identically either way. `git.*` procedures take an optional `repo` naming a folder; omitting it means the primary. A backfill materializes the position-0 rows that every existing project and workspace implicitly had. Creation is all-or-nothing: the first failed folder rolls back the worktrees already added, deletes the branches the run minted, and removes the container, so a partial workspace can never reach the database. Deletion walks every checkout and only removes the container once all of them are gone. Behind the `multi-repo-projects` flag, which gates adding a second repository, not what the host can store.
They are published as Superset pages and linked from the PR. Keeping ~2,900 lines of HTML in the diff buries the code, and the implementation page's claim that file read/write is unaffected was disproved by the workspace-root fix in this branch. Claude-Session: https://claude.ai/code/session_011GHUNf4G6WuLNxx4ivYAon
…ary checkout A multi-repo workspace opens at a container whose secondary checkouts are siblings of the primary, so anything scoped to `worktreePath` rejected them. Saving any file outside the primary failed with "Path is outside workspace root". Split the two meanings that were sharing one accessor: `resolveWorkspaceRoot` returns the container for the filesystem sandbox, `resolveWorkspacePrimaryWorktree` returns the primary for anything reaching for git. Pointing the single accessor at the container fixed saving but broke git watching, because `watchPath` accepts only the exact service root. Also: attribute agent usage by container so work in a secondary is not misattributed, give `listGitIgnoredDirs` a ceiling so it cannot walk out of a container into an enclosing repo, and refuse `project.remove` while another project's live workspace still checks that repo out. Claude-Session: https://claude.ai/code/session_011GHUNf4G6WuLNxx4ivYAon
`projects.repo_path` is NOT NULL, so a project is required to be a repository. A multi-repo project was therefore its primary repo renamed, and one repo could anchor exactly one project. Codex models a project as its own identity with a name and an ordered folder list, and lets the same folder back any number of projects. Add that container rather than changing what a project is: `project_groups` holds the name, `project_group_members` holds the ordered membership, and `projects` is untouched — all 173 readers of `repo_path` keep their invariant. `ON DELETE restrict` on the member's project reference is the load-bearing part: the database refuses to delete a repository a group still references, so the cascade that could strand a live workspace's checkout is unreachable rather than guarded. Ships dark. Nothing reads these tables yet. Claude-Session: https://claude.ai/code/session_011GHUNf4G6WuLNxx4ivYAon
Read projects from `projectGroups` when the flag is on: create a Project and add every source folder identically, edit its folders in settings, and show a repo-count badge in the sidebar. Flag off reads the repository list exactly as before — the sidebar overlay is simply not applied, and the equivalence for a backfilled single-member group is asserted in a test rather than assumed. Follows Codex's vocabulary: the container is a "Project", its members are "Source folders", and a repository stops being user-facing. Claude-Session: https://claude.ai/code/session_011GHUNf4G6WuLNxx4ivYAon
The project container was only applied in the sidebar, so every other project chooser still listed repositories: a Project over two folders appeared as its primary repo's name, and its other folders appeared as projects of their own. The overlay moves out of the sidebar hook into `useGroupedProjects`, and the new-workspace composer, the recent-project list behind the tasks and automations pickers, and the sidebar all read through it. A repository adopted as another Project's first source folder is the primary of that Project *and* of its own backfilled one — `project.create` reuses the row for an already-imported path — so the group that owns several folders now wins the tie. Picking the older backfilled group rendered the wrong name and, host-side, checked out one folder instead of all of them. The picker's items key on project id so same-named projects stay individually selectable, with a filter that scores only the name: ids in the searchable value made every hex-ish query subsequence-match a UUID.
Settings listed repositories, so a Project appeared under its primary repo's name and its other folders sat beside it as peers — the page you landed on mixed the container's settings (name, source folders) with one repository's (location, sparse checkout, lifecycle scripts). The list now shows one row per Project with its folders indented beneath, and the two kinds of settings split: `/settings/projects/group/$groupId` owns the name, icon, source folders, and the settings that describe the whole checkout — branch prefix, naming instructions, the container's base directory — while a folder row opens the repository page it always was, headed by a link back to the Project it belongs to. A Project over one folder still renders as the single row it did before, and its page keeps everything. Deleting a Project removes the container only; `ON DELETE restrict` on the membership rows means its repositories could not have followed it anyway. Adding a second folder from a single-folder page lands on the Project page that now owns those settings, rather than watching them unmount in place.
…rap each `workspaces.create` resolved folders from `project_folders`, which for a project assembled out of several repositories holds only its own — so creating a workspace from it produced a single-repo checkout and none of the folders the settings page listed. It now appends the rest of the Project's membership, additively: a backfilled group can hold fewer members than the folder list it came from, so a member whose repository a folder row already covers is skipped rather than replacing it, and the primary checkout takes the directory name its membership row advertises. Lifecycle scripts follow the same shape. Setup and teardown resolve per checkout and chain in folder order in one shell, so each repository bootstraps by its own conventions and a failure still short-circuits the rest. A workspace whose only script is the primary's resolves to exactly the command it did before. The agent is sent back to the primary checkout after a chained setup, since the chain ends wherever the last folder ran.
Dogfooding the container needs the flag on, and the local dev account is not in the PostHog release condition — the same reason the plugins flag falls back to the dev build a few lines above it.
Three surfaces still described the primary checkout only. The Run button ran the project's `run` script from the workspace's primary worktree. It now resolves one per checkout and starts them together — `cd <checkout> && …` per folder, backgrounded and waited on, because a run script is usually a server and chaining would leave all but the first unstarted. One folder resolves to the command a single-repo workspace has always run. `GitWatcher` attached one watcher per workspace, off `workspaces.worktree_path`, so editing a file in any other checkout emitted no `git:changed` at all — per-folder status went stale until a window focus or the 30s refetch. Watch state is now keyed per checkout (`.git/` watcher, worktree subscription and gitignored-dir set each), while batching, debouncing and the emitted event stay per workspace: consumers still see one coalesced signal. A secondary checkout contributes a broad refresh rather than paths, which are read as primary-relative downstream. The top-bar Changes pill describes the selected folder, so a dirty sibling was invisible. It now carries a dot when another folder has changes, and renders even when the selected folder is clean.
…pace is stored A workspace over several repositories is one `workspaces` row — `worktree_path` the primary checkout, `root_path` the container — plus a `workspace_repos` row per checkout. That is what the repo picker, per-folder git status, the `repo` argument on `git.*`, the Run button and the watcher all read. The sandbox seed wrote a different shape: a project AND a workspace row per repository, the secondaries under ids derived from the workspace's, and no `workspace_repos` rows at all — so every one of those surfaces degraded to the primary checkout on a cloud box, and per-checkout watching attached once. The box now writes the local shape from the same environment it already reads: one workspace row with `root_path` set when there is more than one repository, one project per repository (a box has no base repo, so the checkout is still the repo), and one repo row per checkout in the environment's order, carrying `baseBranch` where the control plane sent one. Nothing in the sandbox contract moves; `SUPERSET_SANDBOX_REPOSITORIES` already carried everything needed. Boxes seeded the old way are in the wild, so the seed converges them on the next boot rather than leaving two shapes alive: it materializes the repo rows against the project rows that already exist (matched by checkout path, so the ids agent and terminal state reference survive) and deletes the derived workspace rows. Deleting rather than keeping them, because the derived ids were never addressable — the desktop filters sandbox-host rows out of its list, and `terminal_sessions.origin_workspace_id` is ON DELETE SET NULL, so an orphan degrades to a session with no origin instead of a dangling row. A single-repository box is left exactly as it was: one workspace, null `root_path`, no repo rows — `listWorkspaceRepos` synthesizes its primary, and a test asserts the rows byte for byte. The repo rows are rewritten whole rather than merged when the environment's list stops matching (its hooks repository decides which checkout is primary, and an environment can be edited), because moving a repository between positions row by row trips the (workspace, position) unique index. Only when it actually differs, so a restart is still a no-op. Out of scope, and noted in docs/cloud-sandbox-mismatches.md: mapping a local Project's folder list onto a cloud workspace (cloud repositories must be connected GitHub repos in one installation, so a local-only source folder has no cloud counterpart — a product decision, not a mapping), and per-folder lifecycle scripts on the box, which still run from the single hooks checkout.
…wn id Keying every checkout's watch state by `workspaceId + worktreePath` moved the primary's entry out from under the id that `watched`, `ignoredDirs` and their readers have always addressed it by, so the lazy-registration coverage for #6729 stopped finding it — six tests, timing out rather than failing an assertion, which is what hid it behind the debounce windows. Only extra checkouts need a composite key. The primary keeps the workspace's id, so the map reads exactly as it did before a workspace could hold more than one checkout. The one genuine seam change stays: the ignore-set swap is per checkout (`refreshCheckoutWatcherIgnores`), since the native watcher it re-derives is attached at the checkout, not at the container that `resolveWorkspaceRoot` returns for a multi-repo workspace.
The folder switcher in the workspace sidebar was a folder glyph and a line of text, which reads as a path control rather than as the repository switcher it is. Each row and the trigger now carry the checkout's GitHub avatar — the same thumbnail the sidebar and the project pickers use — falling back to the letter tile for a checkout with no remote. A project assembled out of local folders has no remote of its own, so its avatar now falls through to its source folders before giving up on the letter tile: the sidebar, the settings list and the Project page all resolve it the same way.
…main The rebase conflicted on every catalog — main and this branch each added messages — and each replayed commit took main's side, which dropped the translations this branch had written. Regenerated from source and refilled from the pre-rebase tip, so both sides' messages are present and translated.
harshithmullapudi
force-pushed
the
multi-repo-projects-v0
branch
from
September 22, 2026 12:38
b8dd975 to
f22770d
Compare
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:38 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:39 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:39 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:39 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:39 — with
GitHub Actions
Inactive
harshithmullapudi
temporarily deployed
to
preview
September 22, 2026 12:39 — with
GitHub Actions
Inactive
3 tasks done
This branch was previously deployed
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.
Lets a project own several repositories, so one workspace checks them all out on one branch and one agent sees all of them.
Ships behind
multi-repo-projects(PostHog). Flag off is byte-identical to today — asserted in a test, not assumed.What you get
Add source folders to a project, and every workspace started in it gets one container with a checkout per folder on one branch name. A folder picker in the right sidebar re-roots Files and Changes. The CLI takes
--projectrepeatedly and prints areposarray.The model change, and why it's here
projects.repo_pathisNOT NULL, so a project is required to be a repository. A multi-repo project was therefore its primary repo renamed — which means one repo could anchor exactly one project, ever, and deleting a folder-project could strand a live workspace.Rather than change what a project is, this adds the container above it:
project_groupsproject_group_membersprojectsAll 173 readers of
repo_pathkeep their invariant.ON DELETE restricton the member's project reference is load-bearing: the database refuses to delete a repository a group still references, so the cascade that could strand a checkout is unreachable rather than guarded. Verified against a real SQLite db, not just the tests.Vocabulary follows Codex, whose model this matches (
{ id, name, rootPaths[] }, same folder in any number of projects — read out of its own state file): the container is a Project, members are Source folders, a repository stops being user-facing.Review order
c5e52d678— the single-root fixes. Smallest, and explains the container/primary split everything else depends on.c22dacbdc— schema + router. Ships dark; nothing reads it.3829f7757— the UI. The only commit that can change existing behaviour.Known gaps, deliberately not fixed here
pullRequests.createForWorkspaceand the PR-metadata git procedures never got therepoargument.workspace_pull_requestsis already many-to-many, so this is additive later.projectGroupshas no member-rename procedure, so the menu item is hidden. The old path has it.project_group_memberscarries norepo_url, so portability into a cloud sandbox depends on joining to the member's project row. Worth adding before cloud depends on it.project.createismachineOnly.create-worktrees.test.ts:190is flaky — failed 1 of 3 runs.Verification
typecheck42/42. Desktop 4022 pass / 36 fail — 2 knownterminal-ws-transport, 34 git/shell integration tests that pass in isolation and fail only under full-suite parallel load (spot-checked). host-service failures match the branch baseline, which swings ~32–44 run to run in the GitWatcher/timing family.check:i18nclean, all 16 locales translated. Zero comments added across the diff.Context
https://claude.ai/code/session_011GHUNf4G6WuLNxx4ivYAon
Summary by cubic
Lets a project own several repositories, so one workspace checks them all out into one container on one branch, and one agent sees all of them. Ships behind the
multi-repo-projectsflag (defaulted on in dev builds); with the flag off, behavior is byte-identical to today, asserted in a test.Host changes
project_groupsandproject_group_membersabove the unchangedprojectstable (plusproject_folders,workspace_repos, and a backfill that materializes position-0 rows);ON DELETE restrictmakes the cascade that could strand a live checkout unreachable.workspaces.createchecks every folder of a Project out together and walks them all on delete; creation is all-or-nothing — the first failed folder rolls back the worktrees, branches, and container, so a partial workspace never reaches the database.git.*procedures take an optionalrepothat defaults to the primary.resolveWorkspaceRootvs primary worktree, per-checkout watch keying), so saving works in any folder and per-folder status stays live. Agent usage attributes to the container, andproject.removeis refused while another project's live workspace still checks that repo out.root_pathset, oneworkspace_reposrow per checkout — and boxes seeded the old way are converged on boot.Desktop and CLI
useGroupedProjectsoverlay, so the sidebar, new-workspace composer, and recent-project lists all show the container's name and a repo-count badge. A repository adopted as another Project's first source folder resolves to the group that owns several folders. The CLI takes--projectrepeatedly and prints areposarray.Known gaps: PR creation still addresses the primary repository only, and folder selection lives in renderer memory, so it does not survive a restart. Cloud repositories must be connected GitHub repos in one installation, so a local-only source folder has no cloud counterpart.
Written for commit f22770d. Summary will update on new commits.