From f51436e43a4eb0c5d5c457e73aa291a6ebe227b1 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Mon, 7 Sep 2026 14:36:21 +0200 Subject: [PATCH 1/2] Clone a new site with the bundled Git, partial rather than shallow Flow 1 of phase 3 of #364 (#385): the first write the binary makes, and the one whose blast radius is a repository that did not exist before. src/git-clone.cjs runs `git clone --filter=blob:none --single-branch --branch trunk --progress`, writes the repository config the app relies on at clone time (core.autocrlf=false, core.symlinks=false, and core.longpaths=true on Windows) and turns Git's progress lines into `{ phase, loaded, total }` events, the same ` /` line the terminal panel already shows. Partial, not shallow: the whole history comes down without the blobs, which is a merge base for every pull request (#351) at about 57 MB, and nothing the app does afterwards needs a blob that is not there. wordpress:setup hands the child to the quit sweep for the length of the clone and removes the directory it created if the clone fails, so a half-written tree is never adopted as a site. isomorphic-git leaves main.js; its remaining callers are the write flows behind the facades. The wiring tests stub cloneSite where they stubbed isomorphic-git's clone; a new integration test clones from a local repository over file:// (a transport the old engine never had) and checks the config, the promisor remote, the missing older blob and the progress shape. The user guide stops saying Git is isomorphic-git. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01J4uRM5JosAcMbKXowaXyVu --- .../instructions/code-review.instructions.md | 2 +- AGENTS.md | 2 +- docs/guide/creating-a-site.md | 4 +- docs/guide/getting-started.md | 2 +- src/git-clone.cjs | 183 ++++++++++++++++++ src/main.js | 45 +++-- src/remove-tree.js | 9 +- src/setup-tracker.js | 2 +- tests/unit/git-clone.integration.test.cjs | 79 ++++++++ tests/unit/git-clone.test.cjs | 61 ++++++ tests/unit/ipc-wiring.test.cjs | 10 +- 11 files changed, 366 insertions(+), 33 deletions(-) create mode 100644 src/git-clone.cjs create mode 100644 tests/unit/git-clone.integration.test.cjs create mode 100644 tests/unit/git-clone.test.cjs diff --git a/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md index 19859547..db5199f5 100644 --- a/.github/instructions/code-review.instructions.md +++ b/.github/instructions/code-review.instructions.md @@ -61,7 +61,7 @@ Invariants. Breaking one is a `[fix here]` finding even when the code works on t **Child processes run on Electron's bundled Node, never the host's.** Spawns go through `process.execPath` with `ELECTRON_RUN_AS_NODE=1` in the environment (see `runNpmWithEngineRetry` and the `playground:start` handler in `src/main.js`, and `buildChildEnv`). A bare `spawn('node')` or `spawn('npm')` assumes a host toolchain that is not there. On Windows child `npm` processes find a `node` at all only because of the `PATH` shim built by `ensureNodeShimDir` — new spawns must inherit that environment rather than build their own. The one exception is the bundled Git, which is not a Node process and gets its own environment from `src/git-binary.cjs` (next invariant). -**Git is the binary the app ships, never the host's.** Since #364 the app bundles Git through `dugite`, unpacked from `app.asar`. `require('dugite')` appears in exactly one file, `src/git-binary.cjs`; every Git spawn resolves the binary with its `resolveGitBinary`, takes its environment from `buildGitEnv`, its options from `SPAWN_OPTIONS` (which already sets `detached` the way section 4 asks), and starts its arguments with `BASE_ARGS`. That env drops every `GIT_*` variable the host had (dugite would otherwise honour `LOCAL_GIT_DIRECTORY` and `GIT_EXEC_PATH` and run a different Git), turns the host's system and global config off, and turns prompting off, so the host's shell or `~/.gitconfig` cannot change what the app does. A `spawn('git')` that relies on `PATH`, a hand-joined path into the dugite tree, a `GitProcess.exec` outside that file, a Git call given `buildChildEnv`'s environment, or one spawned without an explicit `cwd` is a regression. Parse only porcelain-stable output, with the flag that pins it (`--porcelain=v2`, `-z`, an explicit `--format`); parsing human-facing output is a finding however convenient. `src/git-run.cjs` is the only module that spawns the binary and `src/git-read.cjs` the only one that parses its output; a new read belongs there, with a parser test on fixture bytes, not inline at a call site. Since #384 every read outside the write flows runs on the bundled Git and returns the same shapes the `isomorphic-git` calls returned (status rows included), so a facade signature that changes with the engine is a finding; the writes and the clone, with the reads inside them, still run on `isomorphic-git` until #385, and patch and diff generation stays hand-rolled in `src/main.js` until the phase that moves it. +**Git is the binary the app ships, never the host's.** Since #364 the app bundles Git through `dugite`, unpacked from `app.asar`. `require('dugite')` appears in exactly one file, `src/git-binary.cjs`; every Git spawn resolves the binary with its `resolveGitBinary`, takes its environment from `buildGitEnv`, its options from `SPAWN_OPTIONS` (which already sets `detached` the way section 4 asks), and starts its arguments with `BASE_ARGS`. That env drops every `GIT_*` variable the host had (dugite would otherwise honour `LOCAL_GIT_DIRECTORY` and `GIT_EXEC_PATH` and run a different Git), turns the host's system and global config off, and turns prompting off, so the host's shell or `~/.gitconfig` cannot change what the app does. A `spawn('git')` that relies on `PATH`, a hand-joined path into the dugite tree, a `GitProcess.exec` outside that file, a Git call given `buildChildEnv`'s environment, or one spawned without an explicit `cwd` is a regression. Parse only porcelain-stable output, with the flag that pins it (`--porcelain=v2`, `-z`, an explicit `--format`); parsing human-facing output is a finding however convenient. `src/git-run.cjs` is the only module that spawns the binary and `src/git-read.cjs` the only one that parses its output; a new read belongs there, with a parser test on fixture bytes, not inline at a call site. Since #384 every read outside the write flows runs on the bundled Git and returns the same shapes the `isomorphic-git` calls returned (status rows included), so a facade signature that changes with the engine is a finding; the new-site clone runs on it too (`src/git-clone.cjs`, the one place that reads Git's human-facing progress lines, because there is no porcelain for progress); the remaining writes, with the reads inside them, still run on `isomorphic-git` until #385, and patch and diff generation stays hand-rolled in `src/main.js` until the phase that moves it. **`electron-store` is the only persistence layer.** No database, no sidecar JSON. It holds the site registry and per-site metadata and is the single source of truth for "known sites". A second store, a cache file, or state parked in a module-level variable that outlives a handler is architectural drift — flag it. diff --git a/AGENTS.md b/AGENTS.md index 405f4d4f..35d7fdd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,7 +87,7 @@ When asked to add an existing pull request to an existing stack, preserve its co ## Architecture notes (non-obvious) - **Child processes run on Electron's own Node, not the system Node.** `npm install`, `npm run