feat(git): adopt the destination capture --- P1a fixed, lane unblocked

The async completions rendered through whichever frontend was ambient
when git exited. Run `M-x git.status` in frontend A, let B become
active while `git status` runs, and A's panel opened in B. The
generation and the root were already captured at the keypress; the
frontend was the one input still read late.

Captured at invocation in all four entrances --- `git.status()`,
`_on_refresh`, `_deliver_root`'s hand-off, and the `git.diff-file`
command --- and threaded on the request table exactly as this module
already threads the generation and root. Committed under the profile
each surface actually takes: `"panel"` for `*git-status*`, `"document"`
for `*git-diff*`.

`set_status` moved INSIDE the status commit. Rows and message are now
computed first and emitted together, because a failure message
announcing a panel that the commit then refuses is the same misrouting
in its most confusing form.

A refused commit DROPS the render, which is the answer the
`expect_buffer` rule already gives when the panel a refresh belongs to
was killed. `commit_to` refuses before the body runs, so there is no
partial render to undo.

THE FIRST VERSION OF `g6_25` WAS WORTHLESS AND PASSED ITS OWN MUTATION.
`panel_text` finds `*git-status*` by NAME, which is global --- it
answers "does this buffer exist", not "which frontend is showing it"
--- so a render into the competitor satisfied it. Rewritten against
`side_window_for` and each view's active window, it now fails both
bites: removing the status commit grows a `*git-status*` panel in the
competing frontend; removing the diff commit hands it the document
window.

The merge also surfaced a cross-lane break invisible until the suites
ran: #232 made `purpose` required on `pmacs.process.spawn`, and this
module's spawn is on this branch, so it was never among the 11 sites
#232 updated. Each spawn now carries its own purpose and NOT the label
--- all three are labelled `git`, and only the purpose separates
resolving a repository from reading its status from diffing one file.

Existing tests that drive `_deliver_status` / `_deliver_diff` through
the internal seam now supply a captured destination. That includes the
one place it is load-bearing rather than uniform: `g6_23` reuses its
"stale" request table at the CURRENT ticket as its positive control,
which is the only one of those three deliveries that reaches a render.

Gates: 12/12 green --- git_status_stage1, destination_capture, journey
and worker_identity acceptance, plus the full sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-10 22:17:37 +02:00
parent 4109ce6246
commit 4903c7cfb6
No known key found for this signature in database
3 changed files with 401 additions and 72 deletions

View File

@ -231,9 +231,17 @@ local pump = {}
-- it terminates. A spawn failure calls `on_done` too, with -- it terminates. A spawn failure calls `on_done` too, with
-- `spawn_error` set --- §1.2's silence asymmetry: the failure must be -- `spawn_error` set --- §1.2's silence asymmetry: the failure must be
-- surfaced with guidance, never swallowed. -- surfaced with guidance, never swallowed.
local function run_git(label, root, rest, on_done) local function run_git(label, purpose, root, rest, on_done)
local spec = { local spec = {
label = label, label = label,
-- Required since worker identity Stage 1 (#232), and deliberately
-- NOT the label. `label` identifies the process --- "git status" ---
-- while this says what the run is FOR, which is what someone reading
-- `*workers*` or the statusline activity indicator needs: three of
-- this module's spawns are all "git", and only the purpose tells
-- them apart. Each call site writes its own; copying the label
-- across is the failure that ruling was made against.
purpose = purpose,
command = pmacs.git._program, command = pmacs.git._program,
args = pmacs.git.argv(root, rest), args = pmacs.git.argv(root, rest),
stdin = "null", stdin = "null",
@ -241,6 +249,7 @@ local function run_git(label, root, rest, on_done)
if root then spec.cwd = root end if root then spec.cwd = root end
pmacs.git._last_spawn = { pmacs.git._last_spawn = {
command = spec.command, args = spec.args, cwd = spec.cwd, label = spec.label, command = spec.command, args = spec.args, cwd = spec.cwd, label = spec.label,
purpose = spec.purpose,
} }
local log = pmacs.git._spawn_log local log = pmacs.git._spawn_log
log[#log + 1] = spec.args log[#log + 1] = spec.args
@ -670,6 +679,44 @@ local function open_status_panel(rows)
state.buffer = pmacs.window.buffer() state.buffer = pmacs.window.buffer()
end end
-- ---------------------------------------------------------------------
-- The destination boundary (Q#JR14, Q#DC-1)
-- ---------------------------------------------------------------------
--
-- Every continuation in this module renders a tick or more after the
-- keypress that asked for it, and until #231 there was nothing it could
-- render *to* except ambient state: whichever frontend happened to be
-- active when git exited. Run `M-x git.status` in frontend A, let B
-- become active while `git status` runs, and A's panel opened in B.
--
-- So the destination is CAPTURED AT INVOCATION and threaded through,
-- exactly as this module already threads the generation, the root and
-- the row --- and for the same reason. `state.root` has a comment
-- explaining why reading module-level state per step is wrong; the
-- ambient frontend is that same argument one layer down, and it was the
-- one thing still being read late.
--- Run `body` against the destination captured at invocation.
---
--- Returns false when the commit is REFUSED --- the captured window or
--- buffer is gone, or the frontend can no longer satisfy `profile`.
--- A refusal drops the render, which is the same answer the
--- `expect_buffer` rule already gives when the panel a refresh belongs
--- to has been killed: a result whose destination no longer exists is
--- not a result to force somewhere else. `commit_to` refuses BEFORE the
--- body runs, so a refusal is mutation-free and there is no partial
--- render to undo.
---
--- Deliberately not `pcall`-wrapped. A refusal returns `(false, reason)`
--- and is handled here; anything that RAISES is a defect in this module
--- (a fabricated destination, an unknown profile) and must not be
--- swallowed into a silent no-op.
local function commit_ui(dest, profile, body)
local ok, reason = pmacs.window.commit_to(dest, body, profile)
if ok == false then return false, reason end
return true
end
--- The status-refresh ticket currently in force. --- The status-refresh ticket currently in force.
--- ---
--- Exposed alongside `_deliver_status` below, and for the same reason: --- Exposed alongside `_deliver_status` below, and for the same reason:
@ -704,7 +751,12 @@ function pmacs.git._deliver_status(request, res)
if not (live and valid) then return end if not (live and valid) then return end
end end
local rows -- Rows and the status line are COMPUTED here and EMITTED inside the
-- commit below. Splitting them is the point: `set_status` is a UI
-- mutation, and a failure message announcing a panel that the commit
-- then refuses to open is the misrouting this boundary exists to
-- prevent, in its most confusing form.
local rows, message
if res.ok and res.code == 0 then if res.ok and res.code == 0 then
local parsed = pmacs.git.parse_status(res.stdout) local parsed = pmacs.git.parse_status(res.stdout)
state.branch = parsed.branch state.branch = parsed.branch
@ -714,41 +766,50 @@ function pmacs.git._deliver_status(request, res)
local reason = failure_reason(res) local reason = failure_reason(res)
state.branch = state.branch or {} state.branch = state.branch or {}
rows = failure_rows(reason) rows = failure_rows(reason)
pmacs.editor.set_status("git status: " .. reason) message = "git status: " .. reason
end end
open_status_panel(rows) -- The PANEL profile: `*git-status*` is a bottom-panel surface
-- `listview.open` resets collapse and does NOT preserve selection --- -- (`listview.open` defaults `display` to `"panel"`), so the
-- only `listview.refresh` does, and that is the synchronous path this -- stale-intent checks that guard replacing a document window do not
-- model cannot use. So re-seating is owned here. -- apply --- but only while the placement really is a panel, which is
-- -- what the profile's own refusal keeps true for the extent of this
-- And `open`'s own `seat_cursor(p, 1)` cannot be relied on either: it -- body (Q#DC-2).
-- walks DOWN from wherever the cursor is, on the premise that a fresh commit_ui(request.dest, "panel", function()
-- `switch_active_buffer` zeroed it. Re-opening a panel that is if message then pmacs.editor.set_status(message) end
-- already displayed does not zero anything, so that walk would land open_status_panel(rows)
-- one row below the previous cursor instead of on row 1. The handler -- `listview.open` resets collapse and does NOT preserve selection ---
-- therefore seats unconditionally, from line 0. -- only `listview.refresh` does, and that is the synchronous path this
local target = 1 -- model cannot use. So re-seating is owned here.
if request.selected_path then --
for i, row in ipairs(state.rows) do -- And `open`'s own `seat_cursor(p, 1)` cannot be relied on either: it
if row.path == request.selected_path then -- walks DOWN from wherever the cursor is, on the premise that a fresh
target = i -- `switch_active_buffer` zeroed it. Re-opening a panel that is
break -- already displayed does not zero anything, so that walk would land
-- one row below the previous cursor instead of on row 1. The handler
-- therefore seats unconditionally, from line 0.
local target = 1
if request.selected_path then
for i, row in ipairs(state.rows) do
if row.path == request.selected_path then
target = i
break
end
end end
end end
end -- If the captured path is gone --- the commonest case, since a file
-- If the captured path is gone --- the commonest case, since a file -- that stopped being modified drops out of status --- `target` stays
-- that stopped being modified drops out of status --- `target` stays -- 1 and nothing is said about it. That is the correct answer, not a
-- 1 and nothing is said about it. That is the correct answer, not a -- failure.
-- failure. pmacs.editor.clear_selection()
pmacs.editor.clear_selection() pmacs.editor.set_view_top(0)
pmacs.editor.set_view_top(0) pmacs.editor.move_to_line(0)
pmacs.editor.move_to_line(0) -- Walked with `move_down` rather than a single `move_to_line(target)`
-- Walked with `move_down` rather than a single `move_to_line(target)` -- because motion is what drags the viewport along; a bare cursor set
-- because motion is what drags the viewport along; a bare cursor set -- would leave a long status list scrolled to the top with the cursor
-- would leave a long status list scrolled to the top with the cursor -- off screen. This is `listview.refresh`'s own idiom.
-- off screen. This is `listview.refresh`'s own idiom. for _ = 1, target do pmacs.editor.move_down() end
for _ = 1, target do pmacs.editor.move_down() end end)
end end
-- The path of the row under the cursor right now, or nil. -- The path of the row under the cursor right now, or nil.
@ -767,13 +828,19 @@ end
--- generation and replace the newer request. Reserving at the command --- generation and replace the newer request. Reserving at the command
--- and carrying it through is what makes the ordering the user's, not --- and carrying it through is what makes the ordering the user's, not
--- the filesystem's. --- the filesystem's.
local function start_status(root, expect_buffer, want_selection, generation) local function start_status(root, expect_buffer, want_selection, generation, dest)
local request = { local request = {
generation = generation, generation = generation,
expect_buffer = expect_buffer, expect_buffer = expect_buffer,
selected_path = want_selection and selected_path() or nil, selected_path = want_selection and selected_path() or nil,
-- A parameter for the same reason `generation` is: it belongs to
-- the invocation, and this function can run from a root-resolution
-- callback that is already a tick removed from it.
dest = dest,
} }
run_git("git status", root, run_git("git status",
"reading the working tree status of " .. root .. " for the *git-status* panel",
root,
{ "status", "--porcelain=v2", "--branch", "-z" }, { "status", "--porcelain=v2", "--branch", "-z" },
function(res) pmacs.git._deliver_status(request, res) end) function(res) pmacs.git._deliver_status(request, res) end)
end end
@ -793,8 +860,12 @@ function pmacs.git._on_refresh()
end end
-- Reserved HERE, at the keypress, for the same reason `git.status` -- Reserved HERE, at the keypress, for the same reason `git.status`
-- reserves at the command: `g` needs no root lookup, so this is -- reserves at the command: `g` needs no root lookup, so this is
-- already the moment of invocation. -- already the moment of invocation. The destination is captured on
start_status(state.root, state.buffer, true, status_requests.reserve()) -- the same line of reasoning --- `g` is pressed IN the panel, so the
-- frontend showing it is the one this refresh belongs to, and that is
-- true now and possibly not true when git exits.
start_status(state.root, state.buffer, true, status_requests.reserve(),
pmacs.window.capture_destination())
return listview_rows("(refreshing...)") return listview_rows("(refreshing...)")
end end
@ -865,8 +936,12 @@ function pmacs.git._deliver_root(request, res)
-- A fresh open carries no buffer expectation, so a panel the user -- A fresh open carries no buffer expectation, so a panel the user
-- killed earlier does not make this run drop its own first result. -- killed earlier does not make this run drop its own first result.
state.buffer = nil state.buffer = nil
-- The generation reserved at the command, NOT a fresh one. -- The generation reserved at the command, NOT a fresh one --- and the
start_status(root, nil, false, request.generation) -- destination captured there, for the same reason. This callback runs
-- after `git rev-parse` has exited, so capturing here would read the
-- ambient frontend a round trip late and reintroduce exactly the
-- misrouting the capture exists to close.
start_status(root, nil, false, request.generation, request.dest)
end end
--- Open (or re-open) `*git-status*` for the repository containing the --- Open (or re-open) `*git-status*` for the repository containing the
@ -885,10 +960,19 @@ function pmacs.git.status()
-- invocation that starts no work must not invalidate one that is -- invocation that starts no work must not invalidate one that is
-- already in flight, and an invocation that does start work must own -- already in flight, and an invocation that does start work must own
-- the newest generation from that moment on. -- the newest generation from that moment on.
local request = { generation = status_requests.reserve(), dir = dir } -- Captured alongside the generation, at the same moment and for the
-- same reason: this is the last instant at which "the frontend the
-- user asked from" is knowable without guessing.
local request = {
generation = status_requests.reserve(),
dir = dir,
dest = pmacs.window.capture_destination(),
}
-- The root rule (Q#G-2): ask git, and let a non-zero exit BE the -- The root rule (Q#G-2): ask git, and let a non-zero exit BE the
-- "not a repository" answer. `-C <dir>` with no root of our own. -- "not a repository" answer. `-C <dir>` with no root of our own.
run_git("git rev-parse", nil, { "-C", dir, "rev-parse", "--show-toplevel" }, run_git("git rev-parse",
"resolving which Git repository contains " .. dir,
nil, { "-C", dir, "rev-parse", "--show-toplevel" },
function(res) pmacs.git._deliver_root(request, res) end) function(res) pmacs.git._deliver_root(request, res) end)
end end
@ -1038,11 +1122,21 @@ local function advance_diff(request)
if body:gsub("%s", "") == "" then if body:gsub("%s", "") == "" then
body = "(no differences)" body = "(no differences)"
end end
show_diff_buffer(string.format("git diff --- %s\n%s", -- The DOCUMENT profile, and the contrast with the status channel is
pmacs.git.display_path(request.row.path), request.plan.header), body) -- the whole of Q#DC-2: `*git-diff*` REPLACES a document window, so
-- every stale-intent check applies. If the window the `d` was
-- pressed from now holds a different buffer, this diff is answering
-- a question about a view the user has already left, and the commit
-- is refused rather than allowed to overwrite it.
commit_ui(request.dest, "document", function()
show_diff_buffer(string.format("git diff --- %s\n%s",
pmacs.git.display_path(request.row.path), request.plan.header), body)
end)
return return
end end
run_git("git diff", request.root, step.args, run_git("git diff",
"diffing " .. pmacs.git.display_path(request.row.path) .. " against HEAD for *git-diff*",
request.root, step.args,
function(res) pmacs.git._deliver_diff(request, step, res) end) function(res) pmacs.git._deliver_diff(request, step, res) end)
end end
@ -1067,9 +1161,15 @@ function pmacs.git._deliver_diff(request, step, res)
if not diff_requests.is_current(request.generation) then return end if not diff_requests.is_current(request.generation) then return end
if not diff_step_ok(step, res) then if not diff_step_ok(step, res) then
local reason = failure_reason(res) local reason = failure_reason(res)
show_diff_buffer(string.format("git diff --- %s", -- The failure render is a render: same destination, same profile,
pmacs.git.display_path(request.row.path)), reason) -- same refusal. Announcing a diff failure into whatever frontend
pmacs.editor.set_status("git diff: " .. reason) -- happens to be active would be the identical misrouting as
-- announcing a success there.
commit_ui(request.dest, "document", function()
show_diff_buffer(string.format("git diff --- %s",
pmacs.git.display_path(request.row.path)), reason)
pmacs.editor.set_status("git diff: " .. reason)
end)
return return
end end
local text = utf8_clean(res.stdout) local text = utf8_clean(res.stdout)
@ -1085,10 +1185,10 @@ end
-- Run `plan`'s steps in order under an ALREADY-RESERVED diff ticket, -- Run `plan`'s steps in order under an ALREADY-RESERVED diff ticket,
-- then render. `generation` is a parameter for the same reason -- then render. `generation` is a parameter for the same reason
-- `start_status`'s is: it belongs to the keypress, not to this call. -- `start_status`'s is: it belongs to the keypress, not to this call.
local function run_diff_plan(row, plan, root, generation) local function run_diff_plan(row, plan, root, generation, dest)
advance_diff { advance_diff {
generation = generation, row = row, plan = plan, root = root, generation = generation, row = row, plan = plan, root = root,
pieces = {}, index = 0, pieces = {}, index = 0, dest = dest,
} }
end end
@ -1122,10 +1222,13 @@ pmacs.command.define {
-- Everything the plan runs on is captured HERE, at the keypress, and -- Everything the plan runs on is captured HERE, at the keypress, and
-- threaded through every step: the ticket, reserved AFTER the early -- threaded through every step: the ticket, reserved AFTER the early
-- returns above so a `d` that starts no work cannot supersede one -- returns above so a `d` that starts no work cannot supersede one
-- that is already in flight; the root; and the unborn flag. All three -- that is already in flight; the root; the unborn flag; and the
-- describe the repository the user is looking at right now, and all -- DESTINATION. The first three describe the repository the user is
-- three are replaced wholesale by a `git.status` against another one. -- looking at right now and are replaced wholesale by a `git.status`
-- against another one; the fourth describes the window they are
-- looking at it IN, which nothing in this module replaces and
-- nothing outside it announces.
run_diff_plan(row, diff_plan(row, (state.branch or {}).unborn == true), run_diff_plan(row, diff_plan(row, (state.branch or {}).unborn == true),
state.root, diff_requests.reserve()) state.root, diff_requests.reserve(), pmacs.window.capture_destination())
end, end,
} }

View File

@ -269,7 +269,20 @@ census.
**PR #227** — https://github.com/levineuwirth/pmacs/pull/227. Opened **PR #227** — https://github.com/levineuwirth/pmacs/pull/227. Opened
2026-08-09 at `4002734`, after the framing was approved at revision 5 2026-08-09 at `4002734`, after the framing was approved at revision 5
and the full gate suite went green. **Now at `4b82d1e`, MERGE-BLOCKED.** and the full gate suite went green. **UNBLOCKED 2026-08-10**: `main` was
merged in (72 commits) and the destination capture #231 provides is
adopted below. Re-gated 12/12 green on the merged tree.
**One cross-lane break the merge surfaced**, recorded because it was
invisible until the suites ran: #232 made `purpose` **required** on
`pmacs.process.spawn`, and this module's spawn lives on this branch, so
it was never among the 11 call sites #232 updated — every git test
failed at once. Each of the three spawns now carries its own purpose,
and deliberately **not** the label, which is the copy #232's ruling was
made against: all three are labelled `git`, and only the purpose
distinguishes "resolving which repository contains `<dir>`" from
"reading the working tree status of `<root>`" from "diffing `<path>`
against HEAD".
**Review round 1 found three blockers. Two are fixed; the third is why **Review round 1 found three blockers. Two are fixed; the third is why
this lane is blocked.** this lane is blocked.**
@ -299,17 +312,32 @@ this lane is blocked.**
`display_sequence` escapes only through `describe.key` and `display_sequence` escapes only through `describe.key` and
`keymap.list`, both of which require the sequence to be bound `keymap.list`, both of which require the sequence to be bound
already. Verified; no binding was added for this. already. Verified; no binding was added for this.
- **P1a NOT fixed, and deliberately — it blocks this PR.** The async - **P1a FIXED — the block is lifted.** The async completions displayed
completions display UI without capturing the initiating frontend UI without capturing the initiating frontend, so a result surfaced in
(`builtin/runtime/git.lua:684` and `:965` at `723afa7`), so a result whichever frontend was active when git exited. `commit_to` was the
surfaces in whichever frontend is active when git exits. `commit_to` right mechanism and was **not Lua-reachable** outside a directory
is the right mechanism and is **not Lua-reachable** outside a open, so the capture half shipped as **#231** (`0e4c58d`) and this
directory open, so the fix lives in the **`destination-capture`** lane adopts it now that `main` carries it:
lane — approved and in implementation. This lane adopts it after that
lands. Verified untouched: `show_diff_buffer`'s body and - **Captured at invocation** in all four entrances — `git.status()`,
`open_status_panel`'s `listview.open` are byte-identical to `4002734`, `_on_refresh` (the `g` keypress), `_deliver_root`'s hand-off, and
and no commit on this branch adds a `commit_to` call or a frontend the `git.diff-file` command — and threaded on the request table
capture anywhere. exactly as the generation and root already were. The ambient
frontend was the one input still being read late.
- **Committed under the profile the surface actually takes**:
`"panel"` for `*git-status*`, `"document"` for `*git-diff*` (Q#DC-2).
- **`set_status` moved INSIDE the status commit.** A failure message
announcing a panel that the commit then refuses is the same
misrouting in its most confusing form, so rows and message are now
computed first and emitted together.
- **Witnessed by `g6_25`**, and the first version of that test was
**worthless**: `panel_text` finds `*git-status*` by NAME, which is
global, so a render into the wrong frontend satisfied it and the
test passed its own mutation. Rewritten per frontend
(`side_window_for`, and each view's active window) it fails both
bites — removing the status commit grows a `*git-status*` panel in
the competitor, removing the diff commit hands it the document
window.
**The second citation written here in round 1 was wrong**`:854` **The second citation written here in round 1 was wrong**`:854`
pointed at `local unstaged = …` inside `diff_plan`, not at a display pointed at `local unstaged = …` inside `diff_plan`, not at a display

View File

@ -688,7 +688,8 @@ fn g6_2b_a_non_utf8_row_refuses_both_gestures_with_a_message() {
&s, &s,
&format!( &format!(
"pmacs.git._deliver_status(\n\ "pmacs.git._deliver_status(\n\
{{ generation = pmacs.git._generation() }},\n\ {{ generation = pmacs.git._generation(),\n\
dest = pmacs.window.capture_destination() }},\n\
{{ ok = true, code = 0, stdout = {payload}, stderr = '' }})" {{ ok = true, code = 0, stdout = {payload}, stderr = '' }})"
), ),
); );
@ -1034,7 +1035,8 @@ fn g6_4b_a_copy_says_copied_and_a_rename_says_renamed() {
&s, &s,
&format!( &format!(
"pmacs.git._deliver_status(\n\ "pmacs.git._deliver_status(\n\
{{ generation = pmacs.git._generation() }},\n\ {{ generation = pmacs.git._generation(),\n\
dest = pmacs.window.capture_destination() }},\n\
{{ ok = true, code = 0, stdout = {}, stderr = '' }})", {{ ok = true, code = 0, stdout = {}, stderr = '' }})",
z_payload(&refs) z_payload(&refs)
), ),
@ -1323,7 +1325,8 @@ fn g6_22_a_two_step_plan_keeps_the_root_it_started_with() {
&s, &s,
&format!( &format!(
"pmacs.git._deliver_root(\n\ "pmacs.git._deliver_root(\n\
{{ generation = pmacs.git._generation(), dir = {b:?} }},\n\ {{ generation = pmacs.git._generation(), dir = {b:?},\n\
dest = pmacs.window.capture_destination() }},\n\
{{ ok = true, code = 0, stdout = {b:?}, stderr = '' }})" {{ ok = true, code = 0, stdout = {b:?}, stderr = '' }})"
), ),
); );
@ -1722,7 +1725,8 @@ fn g6_17_a_stale_completion_discards_its_rows() {
&format!( &format!(
"local current = pmacs.git._generation()\n\ "local current = pmacs.git._generation()\n\
pmacs.git._deliver_status(\n\ pmacs.git._deliver_status(\n\
{{ generation = current - 1 }},\n\ {{ generation = current - 1,\n\
dest = pmacs.window.capture_destination() }},\n\
{{ ok = true, code = 0, stdout = {payload}, stderr = '' }})" {{ ok = true, code = 0, stdout = {payload}, stderr = '' }})"
), ),
); );
@ -1743,7 +1747,8 @@ fn g6_17_a_stale_completion_discards_its_rows() {
&s, &s,
&format!( &format!(
"pmacs.git._deliver_status(\n\ "pmacs.git._deliver_status(\n\
{{ generation = pmacs.git._generation() }},\n\ {{ generation = pmacs.git._generation(),\n\
dest = pmacs.window.capture_destination() }},\n\
{{ ok = true, code = 0, stdout = {payload}, stderr = '' }})" {{ ok = true, code = 0, stdout = {payload}, stderr = '' }})"
), ),
); );
@ -1958,7 +1963,8 @@ fn g6_23_a_superseded_diff_does_not_replace_the_newer_one() {
"pmacs.git._deliver_diff(\n\ "pmacs.git._deliver_diff(\n\
{{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\ {{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\
plan = {{ header = 'against HEAD', steps = {{}} }},\n\ plan = {{ header = 'against HEAD', steps = {{}} }},\n\
root = '/', pieces = {{}}, index = 0 }},\n\ root = '/', pieces = {{}}, index = 0,\n\
dest = pmacs.window.capture_destination() }},\n\
{{}},\n\ {{}},\n\
{{ ok = true, code = 0, stdout = 'STALE-DIFF-SENTINEL\\n', stderr = '' }})" {{ ok = true, code = 0, stdout = 'STALE-DIFF-SENTINEL\\n', stderr = '' }})"
); );
@ -1978,7 +1984,8 @@ fn g6_23_a_superseded_diff_does_not_replace_the_newer_one() {
"pmacs.git._deliver_diff(\n\ "pmacs.git._deliver_diff(\n\
{{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\ {{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\
plan = {{ header = 'against HEAD', steps = {{}} }},\n\ plan = {{ header = 'against HEAD', steps = {{}} }},\n\
root = '/', pieces = {{}}, index = 0 }},\n\ root = '/', pieces = {{}}, index = 0,\n\
dest = pmacs.window.capture_destination() }},\n\
{{}},\n\ {{}},\n\
{{ ok = true, code = 128, stdout = '',\n\ {{ ok = true, code = 128, stdout = '',\n\
stderr = 'fatal: STALE-FAILURE' }})" stderr = 'fatal: STALE-FAILURE' }})"
@ -2477,6 +2484,197 @@ fn g6_config_git_enabled_is_registry_defined_and_honoured() {
assert!(panel_text(&s).is_empty(), "and no panel opened"); assert!(panel_text(&s).is_empty(), "and no panel opened");
} }
// ---------------------------------------------------------------------------
// Q#G-9 — the continuation lands where it was ASKED FROM
// ---------------------------------------------------------------------------
/// The frontend that competes for ambient authority while git runs.
///
/// Same shape and same id as `destination_capture_acceptance`'s, so the
/// two suites describe one mechanism rather than two conventions.
const COMPETITOR: FrontendId = FrontendId(7);
/// The buffer name in `fid`'s own side (panel) slot, or `None` when that
/// frontend has no panel.
///
/// **Per-frontend on purpose.** `panel_text` finds `*git-status*` by
/// NAME, which is global — it answers "does this buffer exist and what
/// is in it", not "which frontend is showing it". A name lookup is
/// therefore satisfied by a render into the wrong frontend, and an
/// earlier draft of `g6_25` passed its own mutation for exactly that
/// reason.
fn panel_buffer_name(s: &EditorState, fid: FrontendId) -> Option<String> {
let core = s.core.borrow();
let side = core.side_window_for(fid)?;
let buffer_id = core.windows.get(&side)?.buffer_id;
let reg = core.registry.borrow();
Some(reg.get(buffer_id).ok()?.name().to_string())
}
/// The buffer name in `fid`'s active window.
fn active_name_in(s: &EditorState, fid: FrontendId) -> Option<String> {
let core = s.core.borrow();
let win = core.views.get(&fid)?.active;
let buffer_id = core.windows.get(&win)?.buffer_id;
let reg = core.registry.borrow();
Some(reg.get(buffer_id).ok()?.name().to_string())
}
/// Register a second frontend with its own single-window layout.
fn attach_frontend(s: &EditorState) -> pmacs::window::WindowId {
use pmacs::window::{FrontendView, Layout, Window, WindowId};
let win = WindowId::next();
let mut core = s.core.borrow_mut();
let buffer_id = core.active_buffer_id();
let text_view = {
let reg = core.registry.borrow();
pmacs::text_view::TextView::new(reg.get(buffer_id).expect("buffer"))
};
core.windows
.insert(win, Window::new(win, buffer_id, text_view));
core.register_frontend_view(
COMPETITOR,
FrontendView {
layout: Layout::single(win),
active: win,
fold_projection: true,
panel_capable: true,
frame_geometry: None,
panel_hidden: false,
},
);
win
}
/// **The defect this lane's review found, pinned on both channels.**
///
/// `git status` and `git diff` settle a tick or more after the keypress.
/// Before the destination capture, both rendered through whichever
/// frontend was ambient *at completion* — so running `M-x git.status` in
/// frontend A and letting B become active while git ran opened A's panel
/// in B. The generation and root were already captured at invocation;
/// the frontend was the one thing still read late.
///
/// Driven through `_deliver_status` / `_deliver_diff` — the seams the
/// concurrency tests already use — because the switch has to happen
/// *while the work is in flight*, and no arrangement of real subprocess
/// timing can produce that moment on demand.
///
/// **Falsified by dropping either `commit_ui` call**: the render then
/// follows the ambient frontend and the competitor's window changes.
/// Asserting only that the capturing frontend got the buffer would pass
/// on a commit that wrote to *both*, so the competitor is asserted
/// unchanged as well.
#[test]
fn g6_25_a_completion_lands_in_the_frontend_that_asked() {
let (_td, root) = tempdir();
init_repo(&root);
mixed_repo(&root);
let mut s = editor();
open_panel(&mut s, &root, "staged.txt");
let other_win = attach_frontend(&s);
let other_before = {
let core = s.core.borrow();
core.windows.get(&other_win).map(|w| w.buffer_id)
};
// Captured while LOCAL is the acting frontend — the invocation.
exec(&s, "saved_dest = pmacs.window.capture_destination()");
// The competitor takes ambient authority while git is "running".
s.core.borrow_mut().active_frontend = COMPETITOR;
let row = format!("1 .M N... 100644 100644 100644 {H} {H} SENTINEL.txt");
let payload = z_payload(&["# branch.oid deadbeef", "# branch.head main", &row]);
exec(
&s,
&format!(
"pmacs.git._deliver_status(\n\
{{ generation = pmacs.git._generation(), dest = saved_dest }},\n\
{{ ok = true, code = 0, stdout = {payload}, stderr = '' }})"
),
);
// Asserted PER FRONTEND. `panel_text` alone would pass on a render
// into the competitor, because it finds the buffer by name.
assert_eq!(
panel_buffer_name(&s, FrontendId::LOCAL).as_deref(),
Some("*git-status*"),
"the panel must be in the capturing frontend's own side slot"
);
assert_eq!(
panel_buffer_name(&s, COMPETITOR),
None,
"the competing frontend must not have grown a panel"
);
assert_eq!(
{
let core = s.core.borrow();
core.windows.get(&other_win).map(|w| w.buffer_id)
},
other_before,
"…and its document window is untouched too"
);
s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
assert!(
panel_text(&s).contains("SENTINEL.txt"),
"the rows must have rendered:\n{}",
panel_text(&s)
);
// The diff channel, same shape, DOCUMENT profile. An empty remaining
// plan makes `advance_diff` render on this very delivery.
let root_str = root.display().to_string();
exec(&s, "saved_dest = pmacs.window.capture_destination()");
s.core.borrow_mut().active_frontend = COMPETITOR;
let other_before_diff = {
let core = s.core.borrow();
core.windows.get(&other_win).map(|w| w.buffer_id)
};
exec(
&s,
&format!(
"pmacs.git._deliver_diff(\n\
{{ generation = pmacs.git._diff_generation(),\n\
row = {{ path = 'SENTINEL.txt' }},\n\
plan = {{ steps = {{}}, header = 'hdr' }},\n\
pieces = {{}}, index = 0, root = {root_str:?},\n\
dest = saved_dest }},\n\
{{ }},\n\
{{ ok = true, code = 0, stdout = 'diff body', stderr = '' }})"
),
);
// The diff takes a DOCUMENT window, so the discriminating question
// is which frontend's active window now holds `*git-diff*`.
assert_ne!(
active_name_in(&s, COMPETITOR).as_deref(),
Some("*git-diff*"),
"the diff must not have taken the competing frontend's window"
);
assert_eq!(
{
let core = s.core.borrow();
core.windows.get(&other_win).map(|w| w.buffer_id)
},
other_before_diff,
"…and that window's buffer is unchanged"
);
assert_eq!(
active_name_in(&s, FrontendId::LOCAL).as_deref(),
Some("*git-diff*"),
"the diff must land in the capturing frontend's own window"
);
s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
assert!(
diff_text(&s).contains("diff body"),
"…carrying the body it was given:\n{}",
diff_text(&s)
);
}
// Isolated bootstrap storage roots: an integration test is compiled // Isolated bootstrap storage roots: an integration test is compiled
// without `cfg(test)`, so a raw `EditorState::new()` would read the // without `cfg(test)`, so a raw `EditorState::new()` would read the
// developer's real `init.lua` and write into their real data root. // developer's real `init.lua` and write into their real data root.