Repository navigation
✨ Stage, switch and commit in REPL entries - #927
Conversation
A REPL entry now performs `<Git.Add>`, `<Git.Switch>` and `<Git.Commit>` in the checkout it is working in — the ambient one it was started in, or a managed Repository or Worktree it selected — and retains what each produced. Leave the entry at a question, change the branch, the index and HEAD from a shell, reopen the printed location, and the question still shows the object id the commit bound: no `git` runs at all, not even a `write-tree` or a `git var`, and the external work stands untouched. Answering performs the work after the question for the first time. A new entry stages and commits what the checkout holds now. The performers are the ordinary ones an `xmd run` uses, so the authored semantics are unchanged: exactly the pathspecs a document wrote, the ordinary branch rules, and the current index under the invoking person's own author and committer. What is added is memory, with the honesty that costs. A saved commit id is history rather than a prediction and nothing re-resolves it; an operation that physically finished and was never acknowledged runs again and may instead refuse `empty-index`; a native Add that refused an ignored path keeps what it already staged and claims no rollback. There is no transaction, no commit adoption and no exactly-once local effect. A new provider-neutral codec owns the closed version-1 `repl_repository_git` record: the authenticated Repository identity, the selection's checkout and common directory, the working directory as received, and the operation's own arguments — with a commit's message described by a digest and a byte length rather than copied a second time. It parses absolute host checkout paths and reuses the shared state parser and result types without widening the Workspace-relative Workflow parsers. A new Deno module owns the three recorded handlers, private routing over the entry's own registrations, the two holds in slot-then-common order, one lazy commit identity per entry, refusal classification that keeps the route that reported it, and the native-completion-versus-acknowledgement boundary.
…ging it (#181, 3/6) Two reproduced defects let a REPL entry append local Git history the same build cannot read. `<Git.Switch>` captured the received branch into its request and then read the invocation again when it called the performer. The invocation is the caller's own object and the operation suspends several times in between — hashing the request, routing, taking the turn and taking two holds — so ordinary middleware that transforms and delegates could leave the record naming `ready` while the checkout moved to `changed`, with that mismatched result already acknowledged. The branch is now read once, before the first suspension, and that one value is what both the request and the performer use. The local codec refused an empty supplied base. `<Git.Switch base="">` is the absence of a base to the component, so no document reaches it, but the public Api can be called with one and the ordinary performer ignores a base for a branch that already exists — so the switch succeeded, was acknowledged, and then could be read back by nothing, which also refused the whole history at admission. The codec now carries a Switch base and requestedBase as null or any string, the empty one included, keeping the exact bytes and the exact request/result equality. The branch stays a non-empty name, a non-string base is still refused, the commit a created branch started at is still an object id, and the shared Workflow parsers are untouched. Both were acknowledged before anything checked them, so the live callback now reads its own payload back — against the request and this entry's registered placements — before the coordinator publishes it. An unreadable or mismatched result takes the existing fatal path with nothing appended, by the same route the replay side already used. Regressions: a deterministic changing-branch request through ordinary Git middleware, a direct empty base on an existing branch replaying from a real unfinished prefix, and portable codec coverage for the empty base with wrong-type and missing-member controls. The E5 common-directory row no longer writes a file between the question and its first new Git call, so what acquires the hold there is Git itself.
3341c66 to
c8b5664
Compare
| expect(yield* exists(join(working, "after.md"))).toBe(false); | ||
| const recorded = yield* gitSaid(working, ["rev-parse", "HEAD"], home); | ||
| expect(recorded).toMatch(/^[0-9a-f]{40}$/); | ||
| // The commit holds exactly what the staging child staged. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // The commit holds exactly what the staging child staged. |
| const theirIndex = yield* gitSaid(working, ["write-tree"], home); | ||
| expect(theirIndex).not.toBe(yield* gitSaid(working, ["rev-parse", "HEAD^{tree}"], home)); | ||
|
|
||
| // Forgotten here, so what the log holds afterwards is what *reopening* ran. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Forgotten here, so what the log holds afterwards is what *reopening* ran. |
| yield* reach(second, "[answer]"); | ||
| yield* second.type("\r"); | ||
| // The saved object id, which describes the earlier commit rather than | ||
| // today's HEAD. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // today's HEAD. |
| const registration = target.registration; | ||
| const slot = registration.slot; | ||
| // The root this placement recorded, not the one configured now: a restored | ||
| // placement's slot belongs to the root it was selected under. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // placement's slot belongs to the root it was selected under. |
| // read from the directory this entry started in. A host that cannot say is | ||
| // remembered as not saying for the rest of the entry, and only `<Git.Commit>` | ||
| // refuses — so the read happens when a commit needs it, not before, and a | ||
| // completed Commit's replay never reaches it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // completed Commit's replay never reaches it. |
|
|
||
| // The implementation slot, where the ordinary provider installs its own, so | ||
| // middleware a document or a host puts above this composes exactly as it does | ||
| // in an ordinary run. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in an ordinary run. |
| // Staging moves the index and nothing else, so the branch, the commit and the | ||
| // HEAD tree are where the command found them. The index tree may differ or | ||
| // not: staging what is already staged changes nothing, and a result saying so | ||
| // is describing what happened. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is describing what happened. |
| return undefined; | ||
| } | ||
| // Switching to the branch a checkout is already on moves nothing: no reset, no | ||
| // reference change, and nothing staged or unstaged. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reference change, and nothing staged or unstaged. |
| // checkout was; its tree is the tree the index described; and the checkout | ||
| // ends on that commit with HEAD and index both equal to it. Committing an | ||
| // index that already matched HEAD is the condition this operation refuses | ||
| // rather than one it records. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rather than one it records. |
|
Rebased onto The earlier exact-SHA PASS was anchored to
Three sentences the combination untrued were corrected: the architecture file-work row no longer claims "no local Git operation and nothing external", the REPL registration's doc no longer says "nothing else" (the issue capability is selected separately, and that route was audited as untouched), and §2.3 of the workflow spec no longer lists Re-ran the whole frozen battery on the rebased tree — F1 10/124, F2 32/139, F3 39/168, F4 28/153, F5 5 steps → rebuild → 5 steps, F6 Node 51/51 and Bun 51/51 — plus the three preserved review probes, 2 suites / 3 steps. Nothing failed at any point. Staying a draft pending a fresh verdict on this head. |
PR #927: ✨ Stage, switch and commit in REPL entries19 files, +4550 / -92 Scope🔴 PR has 4642 lines changed. Split into focused PRs. 🟡 4642 lines changed. PRs under 400 receive more thorough review. 🟡 Changes span 8 directories. Structural✅ No structural bloat detected. Slop
Static AnalysisOxlint: 4 diagnostics across 3 files (3 rules) no-shadow (2): packages/git/src/deno/retained-repository/provider.ts CorrectnessNo extraneous code patterns detected. |
…181, 3/6) Three CI failures, two causes, neither in the delivered behavior. `<Git.Commit>` borrows the invoking person's Git identity, and the public journey starts the REPL in an isolated `HOME` of its own. On this Mac `git var` auto- detects one from the operating-system user, so the row passed locally; a runner whose hostname has no domain can auto-detect nothing, `git var` fails, and the component reported its ordinary missing-identity refusal. The product was right and the fixture was wrong: that home now carries a `user.name` and `user.email` the way a person's would, and the commit is held to them — which also makes the row prove the real reader end to end rather than whatever the host happened to guess. It fixed both `test-deno (7/15)`, where the source journey runs, and `smoke`, where the compiled one does. `lint` failed on two uses of "claims" in `architecture.md`, which `scripts/lint-architecture.js` forbids in prose. Both now say what they mean.
| const origin = through === "ambient" ? "" : yield* seedOrigin(home); | ||
| // The ambient journey is typed from inside the checkout; the managed one | ||
| // from a directory inside no checkout, so what it works in is the placement | ||
| // it selected. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it selected. |
| `${JOURNEY_IDENTITY.name} <${JOURNEY_IDENTITY.email}>`, | ||
| ); | ||
| // And the document's own Git really ran, which is what makes the empty log | ||
| // below say something. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // below say something. |
| const theirIndex = yield* gitSaid(working, ["write-tree"], home); | ||
| expect(theirIndex).not.toBe(yield* gitSaid(working, ["rev-parse", "HEAD^{tree}"], home)); | ||
|
|
||
| // Forgotten here, so what the log holds afterwards is what *reopening* ran. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Forgotten here, so what the log holds afterwards is what *reopening* ran. |
| yield* showing(second, "waiting for an answer", ACT_MS); | ||
|
|
||
| // No completed operation ran again, and no `git` ran at all — not a | ||
| // `write-tree`, not a `git var`, not a `rev-parse`. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // `write-tree`, not a `git var`, not a `rev-parse`. |
| yield* third.type("\r"); | ||
| const today = yield* moved(working, home, theirs); | ||
| yield* showing(third, `again is ${today}`, ACT_MS); | ||
| // Today's staged work, not the commit the earlier entry retained. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Today's staged work, not the commit the earlier entry retained. |
| const value = yield* recorded(entry, COMMIT, request, function* (checkout) { | ||
| // Read here, because this is the operation that needs it: an entry that | ||
| // commits nothing asks the host nothing about who it is, and a completed | ||
| // Commit never reaches this callback at all. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Commit never reaches this callback at all. |
| // payload, so a failed protocol envelope under that type is not a record | ||
| // this host ever wrote — and what it says about work already done is | ||
| // unknown. The three older types keep their existing policy, where a | ||
| // non-ok envelope is the engine's own failure rather than theirs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // non-ok envelope is the engine's own failure rather than theirs. |
| // Staging moves the index and nothing else, so the branch, the commit and the | ||
| // HEAD tree are where the command found them. The index tree may differ or | ||
| // not: staging what is already staged changes nothing, and a result saying so | ||
| // is describing what happened. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is describing what happened. |
| return undefined; | ||
| } | ||
| // Switching to the branch a checkout is already on moves nothing: no reset, no | ||
| // reference change, and nothing staged or unstaged. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reference change, and nothing staged or unstaged. |
| // checkout was; its tree is the tree the index described; and the checkout | ||
| // ends on that commit with HEAD and index both equal to it. Committing an | ||
| // index that already matched HEAD is the condition this operation refuses | ||
| // rather than one it records. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rather than one it records. |
Why
REPL entries already retain file operations and managed Repository/Worktree selections across leaving and returning. They still refuse
<Git.Add>,<Git.Switch>and<Git.Commit>, preventing an entry from staging and committing its work before pausing for a decision.This adds the three local Git operations to Deno source and compiled REPL entries. It is delivery 3 of #181, following #902 and #925.
What changes
Before: staging, switching and committing require an ordinary run or a workflow.
After: an entry performs them in its ambient checkout or a managed Repository/Worktree and retains each result. For example, inside a checkout with an initial commit:
Leave at the question, change the file, index and HEAD from a shell, then reopen the printed location. The question still shows the saved commit ID. No completed Git operation runs again—not even
write-treeorgit var—and the external changes survive. Answering runs the new suffix; a new entry uses today's checkout.Live operations reuse ordinary semantics: Add stages explicit pathspecs, Switch follows the existing branch rules, and Commit records the current index under the invoking user's author and committer identity. The saved commit ID describes the earlier result and makes no assertion about current HEAD.
How it works
Each handler snapshots its received request and matches the owning coroutine's journal position before resolving paths or acquiring resources. Live work authenticates the selection through the entry's private registry, takes the managed-slot hold before the shared Git-directory mutation hold, and reuses the existing performer. Commit identity is acquired lazily once per entry.
The local codec retains fixed operation results and closed refusal classifications. The live payload is parsed and checked against its request and registered placement before publication. Ordinary Git middleware keeps its existing answer, transform, refuse and delegate behavior.
Review guide
Start with “Staging, switching and committing in an entry” in
specs/repl-spec.md, then review:packages/git/src/deno/retained-repository/git.ts: request snapshots, replay bypass, ownership and validation before publication.packages/git/src/retained-repository/git-records.tsandadmission.ts: request/result matching and refusal parsing.packages/git/src/deno/retained-repository/provider.ts: private checkout registration and lazy identity.What must stay true
How to verify it
Focused evidence on
3341c669:The public journey logs every Git invocation through a forwarding shim, so replay cannot silently perform observational commands. A commit interrupted after native completion but before acknowledgement is retried live and reports ordinary
empty-index, proving that no historical commit is adopted or predicted.Scope
Includes local Add/Switch/Commit retention, runtime integration, record admission, regression coverage and matching architecture/specification updates.
Preserves ordinary-run behavior, Workflow transactions, component forms, the shared authored/generated Files route and existing external-operation protocols. Generated Git remains excluded. Node and Bun retain ordinary File/Dir behavior and refuse these Git operations before native work. Push and pull-request/evidence delivery remain separate; Issues is an independent track.
New abstractions
ReplGitEntrysupplies the handlers with their entry-owned session, identity acquisition, holds, serialization and private registrations.The local Git codec handles ordinary host checkout paths while preserving the existing Workspace-relative Workflow parsers.
ReplGitCheckoutpairs ordinary checkout routing data with the placement needed to reacquire ownership.Each new abstraction has concrete consumers or a clear ownership purpose.
No speculative functionality is included.
Risks and limitations
Native work and journal acknowledgement are separate. An interrupted or failed append can leave Git changes without a saved result; continuation then runs the operation live. No rollback or exactly-once guarantee is added. Native Add can partially stage matching paths before refusing an ignored path, and its retained refusal makes no rollback claim.
The checkout holds coordinate participating XMD entries/processes; external editors and Git commands can still change the checkout. No dependency, lockfile, release, build, runtime-runner or CI-job changes are included.
Scope confirmation