Repository navigation
feat: PGX Git — one Git branch, one isolated Postgres - #45
Merged
Merged
Conversation
Add the `pgx git` command group (init, status, ls, sync, env, exec, reset --from-main, prune, doctor, uninstall, shell, install-shell, hook): - every Git branch, per worktree, gets a copy-on-write database cloned from the latest validated, immutable main snapshot; plain `git switch` creates or reuses it through post-checkout/post-merge/post-rewrite hooks that are inserted non-destructively into existing hooks - main snapshots: schema + migration-bookkeeping fingerprints, staging copy (clone when idle, pg_dump when busy), validation, atomic rename and ALLOW_CONNECTIONS false; failures keep the previous snapshot; interrupted syncs are rolled back; pending migrations are reported - existing branch databases are never replaced; stale branches are shown and reset only explicitly - honest environment handling: `pgx git exec`, `pgx git env --shell`, an opt-in prompt integration for bash/zsh/fish; fail-closed sentinel; credentials redacted in human and JSON output unless --reveal - agents: in a PGX Git repository each session and subagent branches from its current Git branch's database (session key gains a Git scope); busy sources are copied logically, never terminated; Claude Code sessions that switch branches are rerouted, and a command that switches branches and then uses the database is refused before it runs - fault injection for the failure tests only with --features failpoints Tests: tests/git.rs (16 black-box scenarios with real repositories, hooks, worktrees, agents, crash recovery), unit tests per module; docs docs/pgx/local-dev/git.md; Skill and plugin updated; CI runs the suite with and without failpoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… recovery) Branch-switch guard - redirections (`2>&1`, `&>f`, `>|f`) stay inside their command; output filters (`| tail`, `| grep`) count as inert - `git checkout <x>` is a switch only when Git says <x> is a ref (or with -b/-B/--orphan/--detach/-); file restores pass - shell keywords and launchers (`if … then`, `!`, `time`, `env`, `nohup`) and Git global options with values (`--git-dir`, `--work-tree`, …) are seen through - a hook-rewritten command is recognised only by an exact parse, for that session or subagent, and is checked by the command it runs - the same check runs inside `pgx agent exec` / `pgx git exec` for `<shell> -c` scripts, so non-Claude hosts get it too (exit 2) - agents sharing a worktree: when HEAD moves under an agent, its next command is stopped once with the reason before its database changes Hooks - only the repository's own hooks directory is written; a shared core.hooksPath or one inside the work tree (husky's `.husky/_`) is left alone with the manual line; uninstall never touches them Identity - repository and worktree ids are written to `.git/pgx-repo-id` and each worktree's `pgx-worktree-id`: mv and `git worktree move` keep every mapping, a copied repository gets its own id; prune matches worktrees by id Locking and snapshots - a separate sync lock; the state lock is never held across a slow copy (explicit sync takes the pg_dump copy with only the sync lock) - automatic syncs (hooks, before a clone, reset) never wait and never dump: they skip, keeping the baseline, when a snapshot is running, main is busy or missing, or migrations are pending - recovery without pid checks (sync-lock liveness / state-lock invariant) - a snapshot that cannot be made read-only is not published - `pgx git reset` records an intent and is undone after a failure or crash - the engine's recorded main branch wins over a checked-out .pgx.toml Tests: guard unit tests; integration tests for shared/husky hook dirs, moved/copied repositories and worktrees, non-blocking automatic sync, crash-safe reset, wrapper guard, and the lock failpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
One Git branch. One isolated Postgres database. Automatically.
pgx gitconnects the Git lifecycle to PGX databases. Plaingit switchgives every branch (per worktree) its own copy-on-write database, cloned from the latest validated, immutable snapshot of the main database. Main updates never replace an existing branch database, and every AI agent session on a branch gets a private copy of that branch's database.Draft for review: not to be merged or released yet.
Review response (second commit,
3986373732)Independent review of
6c2ccc4. Every item was addressed, each with a test where it can be tested.Fix before merge
2>&1,&>f,>|f) stay inside their command, and output filters (| tail -5,| grep) count as inert.git checkout <x>is a switch only when Git says<x>is a ref (or with-b/-B/--orphan/--detach/-), sogit checkout app/models/user.rb && bin/rails testpasses.if git switch x; then …; fi,time git switch x && …,! git switch …,env A=1 git switch …andgit --git-dir .git switch x && …are now refused.# pgx-routedcomment no longer skips anything.core.hooksPathoutside the repository (global, shared) or inside the work tree (husky v9's generated.husky/_, a committed.githooks/) is left alone, and the report gives the line to add by hand.pgx git uninstallnever touches those directories.Design risks (decided)
pgx git initwrites.git/pgx-repo-idand each worktree'spgx-worktree-id.mvof the repository andgit worktree movekeep every mapping (tested). A copied repository, whose id names a repository that still exists elsewhere, gets a new id and its own databases (tested). Prune matches worktrees by id, and never treats a listed but missing worktree as removed.pgx git syncof a busy main runs itspg_dumpcopy with only the sync lock held.git switch -cwhile an explicit sync was stuck mid-copy took 285 ms.pgx agent execandpgx git execfor<shell> -cscripts (exit 2, nothing runs). Codex therefore gets it, and Cursor does when it uses the wrappers, which the Skill and docs now require in PGX Git repositories.Smaller issues
ALLOW_CONNECTIONS falsefails is not published (newlockfailpoint test).pgx git resetrecords an intent: move the original aside, clone the fresh copy, swap. After a failure or a crash it is undone, never completed, so no data is lost and no_stg_database is left behind (tested with an injected failure and a SIGKILL mid-reset)..pgx.tomlfor hooks, reset and prune (tested).CI for
6c2ccc4was green on all platforms (checked through the API; the checks page shown during the review did not list the CI jobs).Developer experience
Commands:
pgx git init | status | ls | sync | env | exec | reset --from-main | prune | doctor | uninstall | shell | install-shell, plus the internalpgx git hook. All of them accept--json.Architecture
New module
crates/bin/pgx/src/cmd/git/:config[git],[git.cleanup],[git.agents],[git.migrations]in.pgx.toml. Same tiny TOML subset as[agent], every value validated. A.pgx.tomlwithout[git]still loads.reponamingpgx_<repo>_<branch>_<hash>:[a-z0-9_], ≤ 63 bytes, hash over repo id + worktree id + full branch + role; collisions fall back to a 12-hex namestate$PGX_HOME/git/<repo_id>/state.json(0600, atomic rename) under a kernelflock(no stale locks). Intents carry the owner pid for crash recovery.fingerprintcoresync_locked), branch resolution (resolve_head), recovery, main database creation/repair, agent bindingenv--shellexports, prompt integration for bash/zsh/fish, rc-file blockhooksguardcmdsKey behaviours:
pg_dump→pg_restorecopy when main is in use. Connections are never terminated, and a busy database is never physically copied. The copy's fingerprint must match main's before it is published. Any failure drops the staging copy and keeps the previous snapshot. Interrupted syncs (killed process, engine restart) are rolled back on the next command. A snapshot or main database dropped by hand is repaired: main is restored from the snapshot, never republished empty.pending. A configured migration command runs only onpgx git sync --migrate(argv, no shell). Unattended runs needauto_apply_mainplus a prior approval of that exact text.stale;pgx git reset --from-mainis explicit, asks first (or--force), and refuses main and protected branches..gitfile) andcore.hooksPathare handled, and one hook install covers every worktree. A detached HEAD gets a per-worktree database; during a rebase the branch keeps its own.sh -eis safe. Hooks committed to the repo, symlinked hooks, and hooks in other languages (Ruby, Python, …) are skipped, with the line to add by hand. So are hooks directories that are shared or inside the work tree (husky). The hook never blocks Git.pgx git exec -- <cmd>(execs the command: its exit codes and signals are kept),eval "$(pgx git env --shell zsh)", or the opt-inpgx git install-shellprompt hook, which restores the previous environment when the shell leaves the repo. Fail closed:execrefuses to run, and the shell gets an unreachable sentinelDATABASE_URL, never the inherited one..pgx.toml, hooks, logs or state files.pgx git envmasks the password in human and--jsonoutput unless--revealis given.AI agents
[git.agents] enabled(the default), an agent session's branch is cloned from its current Git branch's database. The session key gains a Git scope (SessionKey.scope,AgentMeta.git_scope/git_branch, both optional). Withscope = Noneeverything is byte-identical to before, so existing records and non-Git projects are unaffected.SessionEnd/pgx agent downends the session's branches for every Git branch it used.DATABASE_URLis fixed when it starts, sogit switch x && bin/rails db:migratewould migrate the old branch's database. Claude Code'sPreToolUsehook denies such commands before they run, for the parent session and subagents alike. This coversgit switch,git checkout <ref>,gh pr checkout, andcdinto another worktree, followed by more commands. The agent is told the two safe forms: switch in one call and work in the next, orgit switch x && pgx git exec -- <cmd>, where the wrapper resolves after the switch.pgx git exec/pgx git envinside an agent session return the session's own branch.[git],pgx agentbehaves exactly as before.pgx branchandpgx cloneare unchanged.Benchmarks
Real measurements: macOS arm64 (M1 Pro, APFS), engine 0.1.2 dist, release build. Printed by
tests/git.rs --nocaptureand the hand-run workflow above; numbers are not hardcoded.git switch -cincluding the hook (clone + mapping + output)git switchto an existing branch including the hookpgx git sync, idle main (clone + validate + freeze + publish)pgx git syncwhen main is unchangedpgx git init, new projectgit switch -cwhile an explicit sync is stuck mid-copypgx git exec -- psql …(resolve + exec)Tests
pgxbinary). New tests for config validation (hostile values), branch-name rules, naming bounds and collisions, state round-trip and lock, fingerprints, shell quoting, hook block inertness (includingsh -e), the branch-switch guard, and argument parsing.tests/git.rs: 21 black-box tests (20 in the release configuration, where the fault-injection test is replaced by one provingPGX_GIT_FAILPOINTis ignored) with real temporary Git repositories, real hooks fired by realgit switch, and a real runtime:.pgx.toml/template and repeated init; no repo; external import schema-only by default--migratecore.hooksPathexecexit codes and signals, JSON redaction and--revealDATABASE_URL, unsafe config.pgx.tomlpredates[git]git worktree movekeep databases, a copy gets its own; automatic sync skips a busy main and never blocks behind a running snapshot; reset is undone after a failure or SIGKILL; wrapper guard for every host; recorded main branch winsPGX_GIT_FAILPOINTskills/tests/pgx/run.shpasses;e2e.sh39/39 (npmskillsCLI part skipped offline);plugins/sync.sh --checkpasses.cargo fmt --checkandcargo clippy --all-targets -D warningsare clean with and without--features failpoints(local rustc 1.92; CI's 1.96 will confirm).--test gitwith--features failpointsand then in the release configuration.Docs
New
docs/pgx/local-dev/git.md(guide,.pgx.tomlreference, Rails, Prisma, Django, Node, worktree, Claude Code, Codex and Cursor examples, limits). README,cli.md,agents.md, the PGX Skill (SKILL.mdand the newreferences/git-integration.md),detect-project.sh(git.repository/git.pgx_git) and the plugin README are updated. The plugin copy of the Skill is re-synced.Remaining limitations
pg_dump/pg_restore. Without them PGX refuses and explains rather than terminating connections.pgx git exec -- …; the Skill and docs say so.git switch; restart them.pgx gitexists. A release bump is needed before users get the command (intentionally not part of this PR).🤖 Generated with Claude Code