Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ summary: |
The landing file is written before the rebase and committed after it. It
should name the rebased work commit, which the lane knows once
`rebase_in_worktree` returns, or be written after the rebase.

Fixed at 8b9d3374. The runner writes the landing file at the attempt,
before any rebase, and the rebase replays it unchanged. After a rebase
onto a moved base, the lane now points each replayed landing file at its
work commit's new sha, the parent of the commit that added the file, and
amends the rebased tip; a document branch rebased onto `main` is
restated the same way.
54 changes: 54 additions & 0 deletions src/torve/application/lane.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from __future__ import annotations

import re
import subprocess
from collections.abc import Callable
from dataclasses import dataclass, field
Expand Down Expand Up @@ -750,6 +751,57 @@ def _keep_ref(root: Path, ref: str, sha: str) -> None:
# ....................... #


def _restate_landings(workdir: Path, onto: str) -> None:
"""Point every landing file a rebase replayed at its work commit's new sha.

A landing names the work commit before it (S-0058/D-12, S-0059/D-9), and a
rebase gives that commit a new sha while replaying the file unchanged, so
the record would name a commit no branch holds. A landing commit always
follows its work commit, so the work commit's new sha is the parent of the
commit that added the file; the correction amends the rebased tip."""

from torve.domain.spec import LANDING_FILE

def git(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", "-C", str(workdir), *args], capture_output=True, text=True, check=False
)

changed: list[str] = []

for name in git("diff", "--name-only", "--diff-filter=A", f"{onto}..HEAD").stdout.split():
if not LANDING_FILE.match(name.rsplit("/", 1)[-1]):
continue

path = workdir / name
text = path.read_text(encoding="utf-8")
named = re.search(r"^commit: '?([0-9a-f]{7,40})'?$", text, re.MULTILINE)

if (
named is None
or git("merge-base", "--is-ancestor", named.group(1), "HEAD").returncode == 0
):
continue

added = git(
"log", "--format=%H", "--diff-filter=A", f"{onto}..HEAD", "--", name
).stdout.split()

if not added:
continue

work = git("rev-parse", f"{added[0]}^").stdout.strip()
path.write_text(text[: named.start(1)] + work + text[named.end(1) :], encoding="utf-8")
changed.append(name)

if changed:
git("add", "--", *changed)
git("commit", "--amend", "--no-edit", "--no-verify")


# ....................... #


def _completes(root: Path, task_id: str, document: str) -> bool:
"""Whether this landing leaves the document complete (S-0093/D-1)."""

Expand Down Expand Up @@ -1427,6 +1479,7 @@ def _rebase_document(
return

try:
_restate_landings(workdir, base)
exit_code, summary = _regate(workdir, base, None)

finally:
Expand Down Expand Up @@ -1721,6 +1774,7 @@ def _land_rebased(
return

try:
_restate_landings(workdir, base)
exit_code, summary = _regate(workdir, base, task_id)

finally:
Expand Down
32 changes: 26 additions & 6 deletions src/torve/application/projections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1456,14 +1456,34 @@ def status_report(root: Path, *, board: Board | None = None) -> dict[str, Any]:
carriers was ever written (S-0044/A-5).
"""

if board is not None:
runs = runs_from_board(board)
runs = runs_from_board(board) if board is not None else []

if runs:
return {"schema_version": 1, "runs": runs}
if not runs:
runs = [s.to_record() for s in RunState.load_all(root.resolve() / naming.WORKTREE_DIR)]

states = RunState.load_all(root.resolve() / naming.WORKTREE_DIR)
return {"schema_version": 1, "runs": [s.to_record() for s in states]}
landed = landed_ids(root, board)

for run in runs:
if run.get("state") == TaskState.READY.value and run.get("task_id") in landed:
run["state"] = LANDED

return {"schema_version": 1, "runs": runs}


# What a reader is shown for a candidate whose landing is recorded. The state
# machine ends a candidate at `ready` and records the landing beside it, so a
# task that landed weeks ago read as waiting to land; the display says what the
# record already knows, and the state machine is unchanged.
LANDED = "landed"


def landed_ids(root: Path, board: Board | None = None) -> set[str]:
"""Every task whose landing is recorded: on the board, by the lane, or by
the tree's own trailers and landing files."""

landed = {view.task_id for view in board.tasks.values() if view.landed_sha} if board else set()

return landed | set(lane_landings(root)) | shipped_ids(root)


# ....................... #
Expand Down
23 changes: 16 additions & 7 deletions src/torve/cli/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
load_config,
runtime_for,
)
from torve.domain.states import EXIT_CONFIG, EXIT_OK
from torve.domain.states import EXIT_CONFIG, EXIT_OK, TaskState

if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Callable
Expand Down Expand Up @@ -529,11 +529,9 @@ def _document_waits(root: Path, board: Board) -> dict[str, dict[str, list[str]]]
so the reader knows which pull request to look at. A landing the board,
the lane or the base already holds is no wait."""

from torve.application.projections import cross_document_waits, lane_landings, shipped_ids
from torve.application.projections import cross_document_waits, landed_ids

landed = {view.task_id for view in board.tasks.values() if view.landed_sha}
landed |= set(lane_landings(root)) | shipped_ids(root)
waits = cross_document_waits(root, landed)
waits = cross_document_waits(root, landed_ids(root, board))

return {
task_id: by_document for task_id, by_document in waits.items() if task_id in board.tasks
Expand Down Expand Up @@ -563,8 +561,19 @@ def board_cmd(
call, which is the same thing a manager does when it restarts.
"""

from torve.application.projections import LANDED, landed_ids

result = asyncio.run(_board(dsn_for(root, dsn) or None, partition))
waits = _document_waits(root, result)
landed = landed_ids(root, result)

def shown(view: TaskView) -> str:
"""A candidate whose landing is recorded reads `landed`, not `ready`."""

if view.state is TaskState.READY and view.task_id in landed:
return LANDED

return str(view.state)

if fmt is Format.JSON:
emit_json(
Expand All @@ -579,7 +588,7 @@ def board_cmd(
# nobody will ever claim it. The role is what tells
# the two kinds of queued apart.
"role": view.contract.role if view.contract else None,
"state": str(view.state),
"state": shown(view),
"attempts": view.attempts,
"claimed_by": view.claimed_by,
"landed_sha": view.landed_sha,
Expand All @@ -603,7 +612,7 @@ def board_cmd(
(
view.task_id,
view.contract.role if view.contract else "—",
view.escalation or str(view.state),
view.escalation or shown(view),
str(view.attempts),
view.claimed_by or "—",
_burn(view),
Expand Down
22 changes: 22 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,28 @@ def test_status_json_carries_persisted_records(tmp_path):
assert json.loads(result.stdout) == {"schema_version": 1, "runs": []}


def test_status_shows_a_landed_candidate_as_landed(tmp_path):
from torve.application.runstate import RunState
from torve.application.telemetry import engine_event
from torve.base import naming
from torve.domain.states import TaskState

for task_id in ("T-7501", "T-7502"):
state = RunState(task_id=task_id, path=naming.state_file(tmp_path, task_id))
state.state = TaskState.READY
state.save()

# T-7501's landing is recorded; T-7502 is a candidate still waiting to land.
engine_event(
tmp_path, "lane_landed", {"task": "T-7501", "mode": "fast-forward", "sha": "a" * 40}
)

result = CliRunner().invoke(app, ["status", "--root", str(tmp_path), "--format", "json"])
assert result.exit_code == 0, result.output
states = {run["task_id"]: run["state"] for run in json.loads(result.stdout)["runs"]}
assert states == {"T-7501": "landed", "T-7502": "ready"}


def test_doctor_json_and_exit():
result = CliRunner().invoke(app, ["doctor", "--format", "json"])
document = json.loads(result.stdout)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_lane.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,50 @@ def test_two_candidates_land_serially_first_ff_then_rebased(lane_repo):
assert all(r["approver"] == "Lane Operator" for r in landed)


def _landing_commit(root: Path, task_id: str) -> str:
"""The runner's second commit on a candidate: a landing file naming the
work commit before it (S-0059/D-9). Returns the landing file's path."""
branch = naming.branch(task_id)
work = git(root, "rev-parse", branch)
git(root, "checkout", "-q", branch)
path = f".torve/execution/{task_id}-1-20261006T000000Z.yaml"
(root / path).parent.mkdir(parents=True, exist_ok=True)
(root / path).write_text(f"task: {task_id}\ncommit: {work}\n", encoding="utf-8")
git(root, "add", "-A")
git(root, "commit", "-q", "--no-gpg-sign", "-m", f"torve({task_id}): landing of attempt 1")
git(root, "checkout", "-q", "main")
return path


def test_a_rebased_landing_names_the_commit_that_landed(lane_repo):
candidate(lane_repo, "T-7011", "one.py", "one = 1\n")
candidate(lane_repo, "T-7012", "two.py", "two = 2\n")
path = _landing_commit(lane_repo, "T-7012")
before = git(lane_repo, "rev-parse", f"{naming.branch('T-7012')}~1")

result = invoke_merge(lane_repo)
assert result.exit_code == 0, result.output

# T-7012 was rebased onto T-7011's landing, so its work commit has a new
# sha. The landing file on main must name that one, not the pre-rebase
# commit no branch holds any more (S-0058/D-12).
named = git(lane_repo, "show", f"main:{path}").split("commit: ", 1)[1].strip()
added = git(lane_repo, "log", "--format=%H", "--diff-filter=A", "main", "--", path)
assert named != before
assert named == git(lane_repo, "rev-parse", f"{added}^")
git(lane_repo, "merge-base", "--is-ancestor", named, "main")


def test_a_fast_forward_landing_keeps_the_commit_it_named(lane_repo):
candidate(lane_repo, "T-7013", "one.py", "one = 1\n")
path = _landing_commit(lane_repo, "T-7013")
work = git(lane_repo, "rev-parse", f"{naming.branch('T-7013')}~1")

assert invoke_merge(lane_repo).exit_code == 0

assert git(lane_repo, "show", f"main:{path}").split("commit: ", 1)[1].strip() == work


def test_a_red_rebase_puts_the_branch_back_so_the_next_pass_regates(lane_repo):
"""T-0391 landed on a battery that had gone red, in two passes.

Expand Down
4 changes: 4 additions & 0 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,10 @@ async def fake_board(dsn, partition):

assert shown.exit_code == 0, shown.output
assert json.loads(shown.output)["waiting_on_documents"] == {"T-0003": {"S-0090": ["T-0002"]}}
# A candidate whose landing is recorded reads landed, not ready.
states = {row["task"]: row["state"] for row in json.loads(shown.output)["tasks"]}
assert states["T-0001"] == "landed"
assert states["T-0002"] != "landed"

text = runner.invoke(app, ["manager", "board", "repo", "--root", str(tmp_path)])

Expand Down
Loading