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
|
-- 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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue