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:
parent
4109ce6246
commit
4903c7cfb6
|
|
@ -231,9 +231,17 @@ local pump = {}
|
|||
-- it terminates. A spawn failure calls `on_done` too, with
|
||||
-- `spawn_error` set --- §1.2's silence asymmetry: the failure must be
|
||||
-- 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 = {
|
||||
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,
|
||||
args = pmacs.git.argv(root, rest),
|
||||
stdin = "null",
|
||||
|
|
@ -241,6 +249,7 @@ local function run_git(label, root, rest, on_done)
|
|||
if root then spec.cwd = root end
|
||||
pmacs.git._last_spawn = {
|
||||
command = spec.command, args = spec.args, cwd = spec.cwd, label = spec.label,
|
||||
purpose = spec.purpose,
|
||||
}
|
||||
local log = pmacs.git._spawn_log
|
||||
log[#log + 1] = spec.args
|
||||
|
|
@ -670,6 +679,44 @@ local function open_status_panel(rows)
|
|||
state.buffer = pmacs.window.buffer()
|
||||
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.
|
||||
---
|
||||
--- 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
|
||||
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
|
||||
local parsed = pmacs.git.parse_status(res.stdout)
|
||||
state.branch = parsed.branch
|
||||
|
|
@ -714,41 +766,50 @@ function pmacs.git._deliver_status(request, res)
|
|||
local reason = failure_reason(res)
|
||||
state.branch = state.branch or {}
|
||||
rows = failure_rows(reason)
|
||||
pmacs.editor.set_status("git status: " .. reason)
|
||||
message = "git status: " .. reason
|
||||
end
|
||||
|
||||
open_status_panel(rows)
|
||||
-- `listview.open` resets collapse and does NOT preserve selection ---
|
||||
-- only `listview.refresh` does, and that is the synchronous path this
|
||||
-- model cannot use. So re-seating is owned here.
|
||||
--
|
||||
-- And `open`'s own `seat_cursor(p, 1)` cannot be relied on either: it
|
||||
-- walks DOWN from wherever the cursor is, on the premise that a fresh
|
||||
-- `switch_active_buffer` zeroed it. Re-opening a panel that is
|
||||
-- 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
|
||||
-- The PANEL profile: `*git-status*` is a bottom-panel surface
|
||||
-- (`listview.open` defaults `display` to `"panel"`), so the
|
||||
-- stale-intent checks that guard replacing a document window do not
|
||||
-- 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
|
||||
-- body (Q#DC-2).
|
||||
commit_ui(request.dest, "panel", function()
|
||||
if message then pmacs.editor.set_status(message) end
|
||||
open_status_panel(rows)
|
||||
-- `listview.open` resets collapse and does NOT preserve selection ---
|
||||
-- only `listview.refresh` does, and that is the synchronous path this
|
||||
-- model cannot use. So re-seating is owned here.
|
||||
--
|
||||
-- And `open`'s own `seat_cursor(p, 1)` cannot be relied on either: it
|
||||
-- walks DOWN from wherever the cursor is, on the premise that a fresh
|
||||
-- `switch_active_buffer` zeroed it. Re-opening a panel that is
|
||||
-- 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
|
||||
-- If the captured path is gone --- the commonest case, since a file
|
||||
-- that stopped being modified drops out of status --- `target` stays
|
||||
-- 1 and nothing is said about it. That is the correct answer, not a
|
||||
-- failure.
|
||||
pmacs.editor.clear_selection()
|
||||
pmacs.editor.set_view_top(0)
|
||||
pmacs.editor.move_to_line(0)
|
||||
-- Walked with `move_down` rather than a single `move_to_line(target)`
|
||||
-- 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
|
||||
-- off screen. This is `listview.refresh`'s own idiom.
|
||||
for _ = 1, target do pmacs.editor.move_down() end
|
||||
-- If the captured path is gone --- the commonest case, since a file
|
||||
-- that stopped being modified drops out of status --- `target` stays
|
||||
-- 1 and nothing is said about it. That is the correct answer, not a
|
||||
-- failure.
|
||||
pmacs.editor.clear_selection()
|
||||
pmacs.editor.set_view_top(0)
|
||||
pmacs.editor.move_to_line(0)
|
||||
-- Walked with `move_down` rather than a single `move_to_line(target)`
|
||||
-- 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
|
||||
-- off screen. This is `listview.refresh`'s own idiom.
|
||||
for _ = 1, target do pmacs.editor.move_down() end
|
||||
end)
|
||||
end
|
||||
|
||||
-- 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
|
||||
--- and carrying it through is what makes the ordering the user's, not
|
||||
--- 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 = {
|
||||
generation = generation,
|
||||
expect_buffer = expect_buffer,
|
||||
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" },
|
||||
function(res) pmacs.git._deliver_status(request, res) end)
|
||||
end
|
||||
|
|
@ -793,8 +860,12 @@ function pmacs.git._on_refresh()
|
|||
end
|
||||
-- Reserved HERE, at the keypress, for the same reason `git.status`
|
||||
-- reserves at the command: `g` needs no root lookup, so this is
|
||||
-- already the moment of invocation.
|
||||
start_status(state.root, state.buffer, true, status_requests.reserve())
|
||||
-- already the moment of invocation. The destination is captured on
|
||||
-- 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...)")
|
||||
end
|
||||
|
||||
|
|
@ -865,8 +936,12 @@ function pmacs.git._deliver_root(request, res)
|
|||
-- A fresh open carries no buffer expectation, so a panel the user
|
||||
-- killed earlier does not make this run drop its own first result.
|
||||
state.buffer = nil
|
||||
-- The generation reserved at the command, NOT a fresh one.
|
||||
start_status(root, nil, false, request.generation)
|
||||
-- The generation reserved at the command, NOT a fresh one --- and the
|
||||
-- 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
|
||||
|
||||
--- 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
|
||||
-- already in flight, and an invocation that does start work must own
|
||||
-- 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
|
||||
-- "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)
|
||||
end
|
||||
|
||||
|
|
@ -1038,11 +1122,21 @@ local function advance_diff(request)
|
|||
if body:gsub("%s", "") == "" then
|
||||
body = "(no differences)"
|
||||
end
|
||||
show_diff_buffer(string.format("git diff --- %s\n%s",
|
||||
pmacs.git.display_path(request.row.path), request.plan.header), body)
|
||||
-- The DOCUMENT profile, and the contrast with the status channel is
|
||||
-- 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
|
||||
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)
|
||||
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_step_ok(step, res) then
|
||||
local reason = failure_reason(res)
|
||||
show_diff_buffer(string.format("git diff --- %s",
|
||||
pmacs.git.display_path(request.row.path)), reason)
|
||||
pmacs.editor.set_status("git diff: " .. reason)
|
||||
-- The failure render is a render: same destination, same profile,
|
||||
-- same refusal. Announcing a diff failure into whatever frontend
|
||||
-- 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
|
||||
end
|
||||
local text = utf8_clean(res.stdout)
|
||||
|
|
@ -1085,10 +1185,10 @@ end
|
|||
-- Run `plan`'s steps in order under an ALREADY-RESERVED diff ticket,
|
||||
-- then render. `generation` is a parameter for the same reason
|
||||
-- `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 {
|
||||
generation = generation, row = row, plan = plan, root = root,
|
||||
pieces = {}, index = 0,
|
||||
pieces = {}, index = 0, dest = dest,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -1122,10 +1222,13 @@ pmacs.command.define {
|
|||
-- Everything the plan runs on is captured HERE, at the keypress, and
|
||||
-- threaded through every step: the ticket, reserved AFTER the early
|
||||
-- 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
|
||||
-- describe the repository the user is looking at right now, and all
|
||||
-- three are replaced wholesale by a `git.status` against another one.
|
||||
-- that is already in flight; the root; the unborn flag; and the
|
||||
-- DESTINATION. The first three describe the repository the user is
|
||||
-- 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),
|
||||
state.root, diff_requests.reserve())
|
||||
state.root, diff_requests.reserve(), pmacs.window.capture_destination())
|
||||
end,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,7 +269,20 @@ census.
|
|||
|
||||
**PR #227** — https://github.com/levineuwirth/pmacs/pull/227. Opened
|
||||
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
|
||||
this lane is blocked.**
|
||||
|
|
@ -299,17 +312,32 @@ this lane is blocked.**
|
|||
`display_sequence` escapes only through `describe.key` and
|
||||
`keymap.list`, both of which require the sequence to be bound
|
||||
already. Verified; no binding was added for this.
|
||||
- **P1a NOT fixed, and deliberately — it blocks this PR.** The async
|
||||
completions display UI without capturing the initiating frontend
|
||||
(`builtin/runtime/git.lua:684` and `:965` at `723afa7`), so a result
|
||||
surfaces in whichever frontend is active when git exits. `commit_to`
|
||||
is the right mechanism and is **not Lua-reachable** outside a
|
||||
directory open, so the fix lives in the **`destination-capture`**
|
||||
lane — approved and in implementation. This lane adopts it after that
|
||||
lands. Verified untouched: `show_diff_buffer`'s body and
|
||||
`open_status_panel`'s `listview.open` are byte-identical to `4002734`,
|
||||
and no commit on this branch adds a `commit_to` call or a frontend
|
||||
capture anywhere.
|
||||
- **P1a FIXED — the block is lifted.** The async completions displayed
|
||||
UI without capturing the initiating frontend, so a result surfaced in
|
||||
whichever frontend was active when git exited. `commit_to` was the
|
||||
right mechanism and was **not Lua-reachable** outside a directory
|
||||
open, so the capture half shipped as **#231** (`0e4c58d`) and this
|
||||
lane adopts it now that `main` carries it:
|
||||
|
||||
- **Captured at invocation** in all four entrances — `git.status()`,
|
||||
`_on_refresh` (the `g` keypress), `_deliver_root`'s hand-off, and
|
||||
the `git.diff-file` command — and threaded on the request table
|
||||
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`
|
||||
pointed at `local unstaged = …` inside `diff_plan`, not at a display
|
||||
|
|
|
|||
|
|
@ -688,7 +688,8 @@ fn g6_2b_a_non_utf8_row_refuses_both_gestures_with_a_message() {
|
|||
&s,
|
||||
&format!(
|
||||
"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 = '' }})"
|
||||
),
|
||||
);
|
||||
|
|
@ -1034,7 +1035,8 @@ fn g6_4b_a_copy_says_copied_and_a_rename_says_renamed() {
|
|||
&s,
|
||||
&format!(
|
||||
"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 = '' }})",
|
||||
z_payload(&refs)
|
||||
),
|
||||
|
|
@ -1323,7 +1325,8 @@ fn g6_22_a_two_step_plan_keeps_the_root_it_started_with() {
|
|||
&s,
|
||||
&format!(
|
||||
"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 = '' }})"
|
||||
),
|
||||
);
|
||||
|
|
@ -1722,7 +1725,8 @@ fn g6_17_a_stale_completion_discards_its_rows() {
|
|||
&format!(
|
||||
"local current = pmacs.git._generation()\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 = '' }})"
|
||||
),
|
||||
);
|
||||
|
|
@ -1743,7 +1747,8 @@ fn g6_17_a_stale_completion_discards_its_rows() {
|
|||
&s,
|
||||
&format!(
|
||||
"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 = '' }})"
|
||||
),
|
||||
);
|
||||
|
|
@ -1958,7 +1963,8 @@ fn g6_23_a_superseded_diff_does_not_replace_the_newer_one() {
|
|||
"pmacs.git._deliver_diff(\n\
|
||||
{{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\
|
||||
plan = {{ header = 'against HEAD', steps = {{}} }},\n\
|
||||
root = '/', pieces = {{}}, index = 0 }},\n\
|
||||
root = '/', pieces = {{}}, index = 0,\n\
|
||||
dest = pmacs.window.capture_destination() }},\n\
|
||||
{{}},\n\
|
||||
{{ 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\
|
||||
{{ generation = {gen_a}, row = {{ path = 'staged.txt' }},\n\
|
||||
plan = {{ header = 'against HEAD', steps = {{}} }},\n\
|
||||
root = '/', pieces = {{}}, index = 0 }},\n\
|
||||
root = '/', pieces = {{}}, index = 0,\n\
|
||||
dest = pmacs.window.capture_destination() }},\n\
|
||||
{{}},\n\
|
||||
{{ ok = true, code = 128, stdout = '',\n\
|
||||
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");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
// without `cfg(test)`, so a raw `EditorState::new()` would read the
|
||||
// developer's real `init.lua` and write into their real data root.
|
||||
|
|
|
|||
Loading…
Reference in New Issue