Process all RX handoff commits in TX watcher

This commit is contained in:
LittleSam129
2026-09-22 11:46:53 +03:00
parent a516b99015
commit a9317989a2
6 changed files with 544 additions and 43 deletions

View File

@@ -35,6 +35,10 @@ class ResultBranchMissing(ResultChannelError):
"""The configured result branch does not exist on the remote."""
class ResultHistoryRewritten(ResultChannelError):
"""The result branch no longer descends from the processed commit."""
@dataclass(frozen=True)
class RxHandoffResult:
schema_version: int
@@ -382,6 +386,32 @@ class GitResultChannel:
def branch_exists(self) -> bool:
return self.remote_head() is not None
def _tracking_reference(self) -> str:
return f"refs/remotes/{self.remote}/{self.branch}"
def _optional_revision(self, revision: str) -> str | None:
result = self._run(["rev-parse", "--verify", revision], check=False)
if result.returncode != 0:
return None
value = result.stdout.strip()
if not _HEAD_RE.fullmatch(value):
raise ResultChannelError(f"invalid Git revision for {revision}")
return value
def _is_ancestor(self, ancestor: str, descendant: str) -> bool:
result = self._run(
["merge-base", "--is-ancestor", ancestor, descendant],
check=False,
)
if result.returncode == 0:
return True
if result.returncode == 1:
return False
raise ResultChannelError(
f"cannot compare result history {ancestor}..{descendant}: "
f"{result.stderr.strip()}"
)
def fetch(self) -> str:
initial_fetch = self._run(
["fetch", "--quiet", self.remote, self.branch],
@@ -399,8 +429,17 @@ class GitResultChannel:
)
tracking_reference = (
f"refs/heads/{self.branch}:"
f"refs/remotes/{self.remote}/{self.branch}"
f"{self._tracking_reference()}"
)
previous_tracking_head = self._optional_revision(self._tracking_reference())
if (
previous_tracking_head is not None
and not self._is_ancestor(previous_tracking_head, remote_head)
):
raise ResultHistoryRewritten(
"remote result branch is not a fast-forward of the local "
f"tracking branch: {previous_tracking_head} -> {remote_head}"
)
tracking_fetch = self._run(
["fetch", "--quiet", self.remote, tracking_reference],
check=False,
@@ -411,7 +450,7 @@ class GitResultChannel:
f"{tracking_fetch.stderr.strip()}"
)
tracking_head = self._run(
["rev-parse", f"refs/remotes/{self.remote}/{self.branch}"]
["rev-parse", self._tracking_reference()]
).stdout.strip()
if tracking_head != remote_head:
raise ResultChannelError("remote-tracking result branch is stale")
@@ -419,16 +458,100 @@ class GitResultChannel:
def read(self) -> tuple[RxHandoffResult, str, str]:
remote_head = self.fetch()
reference = f"refs/remotes/{self.remote}/{self.branch}"
json_result = self._run(["show", f"{reference}:{RESULT_JSON_PATH}"])
return self.read_commit(remote_head)
def read_commit(self, commit: str) -> tuple[RxHandoffResult, str, str]:
"""Read and validate the result stored by one exact commit."""
if not _HEAD_RE.fullmatch(commit):
raise ResultChannelError("result commit must be a full lowercase Git hash")
commit_check = self._run(
["cat-file", "-e", f"{commit}^{{commit}}"],
check=False,
)
if commit_check.returncode != 0:
raise ResultChannelError(f"result commit is unavailable: {commit}")
json_result = self._run(
["show", f"{commit}:{RESULT_JSON_PATH}"],
check=False,
)
if json_result.returncode != 0:
raise ResultChannelError(
f"result JSON is unavailable at commit {commit}: "
f"{json_result.stderr.strip()}"
)
raw = json.loads(json_result.stdout)
if not isinstance(raw, dict):
raise ResultChannelError("remote result JSON root is not an object")
raise ResultChannelError(
f"result JSON root is not an object at commit {commit}"
)
result = result_from_mapping(raw)
observed_head = self._run(["rev-parse", reference]).stdout.strip()
if observed_head != remote_head:
raise ResultChannelError("result commit differs from remote HEAD")
return result, result_digest(result), observed_head
return result, result_digest(result), commit
def commits_since(
self,
last_processed_commit: str | None,
) -> tuple[list[str], str]:
"""Return every new result-branch commit from oldest to newest."""
remote_head = self.fetch()
if last_processed_commit is not None:
if not _HEAD_RE.fullmatch(last_processed_commit):
raise ResultChannelError(
"last processed result commit must be a full lowercase Git hash"
)
if self._optional_revision(last_processed_commit) is None:
raise ResultHistoryRewritten(
"last processed result commit is unavailable locally"
)
if not self._is_ancestor(last_processed_commit, remote_head):
raise ResultHistoryRewritten(
"remote result history no longer descends from the last "
f"processed commit: {last_processed_commit} -> {remote_head}"
)
if last_processed_commit == remote_head:
return [], remote_head
revision_range = f"{last_processed_commit}..{remote_head}"
history = self._run(
["rev-list", "--reverse", "--topo-order", revision_range]
).stdout
commits = [line.strip() for line in history.splitlines() if line.strip()]
return commits, remote_head
history = self._run(
["rev-list", "--reverse", "--topo-order", remote_head]
).stdout
all_commits = [line.strip() for line in history.splitlines() if line.strip()]
first_result_index: int | None = None
for index, commit in enumerate(all_commits):
exists = self._run(
["cat-file", "-e", f"{commit}:{RESULT_JSON_PATH}"],
check=False,
)
if exists.returncode == 0:
first_result_index = index
break
if first_result_index is None:
return [], remote_head
return all_commits[first_result_index:], remote_head
def find_commit_by_digest(self, digest: str) -> str | None:
"""Find the newest valid result commit with a known canonical digest."""
if not _SHA256_RE.fullmatch(digest):
raise ResultChannelError("stored result digest is invalid")
remote_head = self.fetch()
history = self._run(["rev-list", remote_head]).stdout
for commit in (
line.strip() for line in history.splitlines() if line.strip()
):
try:
_result, observed_digest, _head = self.read_commit(commit)
except (json.JSONDecodeError, ResultChannelError, ValueError):
continue
if observed_digest == digest:
return commit
return None
def publish(self, result: RxHandoffResult) -> PublishOutcome:
validate_result(result)