Multi-milestone work and documents on the new loop (M2.4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m45s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m45s
- Every milestone branch is cut from the latest default branch: the next milestone builds on the merged previous one, and a redo after a closed PR drops the rejected work. Uncommitted work comes along. - While the item's PR is open, start_milestone is refused, with the PR's state read from Gitea at that moment, not the last watch round. - finish_milestone(final): earlier PRs say "Part of #N", the last "Closes #N"; unsaid, it's final, as before. - close_item with the item's PR still open waits for the merge (L2: a closed item leaves nothing open). - The driver's instructions cover ARCHITECTURE.md and build plans under docs/plans/ (D2); the harness never parses a plan. - Parking stashes uncommitted work and pushes it to <item>-parked, put back on resume, instead of committing it onto the item's branch. The walk found a half-done review fix parked onto a PR that had merged meanwhile, where it was lost. Architecture D4's parking line says so. Tests: unit tests for fresh milestone branches, the merge wait (including a merge not yet watched), redo after a closed PR, Part of/Closes, close waiting on an open PR, and the parked-fix regression; the walk's new milestones_start_fresh property (fork-from-current fails 9 seeds); an e2e two-milestone feature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
da32cac5cf
commit
cd9ab2559e
10 files changed
+539
-53
No files matched your search
@@ -91,38 +91,73 @@ class Milestones:
|
||||
# -- parking and resuming (L1, D4) -----------------------------------------
|
||||
|
||||
def park(self, *, keep: tuple[str, ...]) -> str | None:
|
||||
"""Before another item uses the workspace: commit whatever the item
|
||||
on it left, and push its branch, so nothing of a parked item lives
|
||||
only on the build VM. Only a harness branch of an open item (one
|
||||
starting with a ``keep`` prefix) is parked; returns its name."""
|
||||
"""Before another item uses the workspace: nothing of the item on it
|
||||
may live only on the build VM. Its uncommitted work is stashed and
|
||||
pushed to ``<item>-parked``, never committed onto its branch, which
|
||||
may be a PR's that has merged meanwhile; resuming puts it back
|
||||
uncommitted, for wherever the work goes next. Commits the harness
|
||||
made but couldn't push are pushed. Only an open item's harness branch
|
||||
(one starting with a ``keep`` prefix, ``<item>-``) is parked; returns
|
||||
its name."""
|
||||
git = self._git
|
||||
branch = git.current_branch()
|
||||
if not keep or not branch.startswith(keep):
|
||||
prefix = next((k for k in keep if branch.startswith(k)), None)
|
||||
if prefix is None:
|
||||
return None
|
||||
git.commit_all("WIP: parked while another work item runs")
|
||||
if git.remote_sha(self._remote, branch, credential=self._credential) != (
|
||||
git.head_sha()
|
||||
sha = git.stash_all(f"parked from {branch}")
|
||||
if sha is not None:
|
||||
git.push_commit(
|
||||
self._remote, sha, f"{prefix}parked", credential=self._credential
|
||||
)
|
||||
remote = git.remote_sha(self._remote, branch, credential=self._credential)
|
||||
if remote != git.head_sha() and (
|
||||
remote is not None or git.commits_since(f"origin/{self._base}") > 0
|
||||
):
|
||||
git.push(self._remote, branch, credential=self._credential)
|
||||
return branch
|
||||
|
||||
def resume(self, branch: str, *, keep: tuple[str, ...], mine: str = "") -> None:
|
||||
"""Back to a parked item: park whatever is on the workspace now, then
|
||||
check out the item's branch. The local branch wins when it exists (it
|
||||
can only be ahead: parking pushed it); otherwise it comes from origin.
|
||||
A workspace already on one of the item's own branches (``mine``, a
|
||||
prefix) is where it left off, and stays."""
|
||||
def resume(self, branch: str, *, keep: tuple[str, ...], mine: str) -> None:
|
||||
"""Back to a parked item: park whatever is on the workspace now,
|
||||
check out the item's branch, and put its parked work back. The local
|
||||
branch wins when it exists (it can only be ahead: parking pushed it);
|
||||
otherwise it comes from origin. A workspace already on one of the
|
||||
item's own branches (``mine``, its ``<item>-`` prefix) stays."""
|
||||
git = self._git
|
||||
current = git.current_branch()
|
||||
if current == branch or (mine and current.startswith(mine)):
|
||||
if current != branch and not current.startswith(mine):
|
||||
self.park(keep=keep)
|
||||
if branch in git.local_branches():
|
||||
git.checkout_existing_branch(branch)
|
||||
git.reset_to(branch, branch)
|
||||
else:
|
||||
git.fetch(credential=self._credential)
|
||||
pushed = git.remote_sha(
|
||||
self._remote, branch, credential=self._credential
|
||||
)
|
||||
# A branch never pushed held nothing beyond the default branch.
|
||||
start = f"origin/{branch}" if pushed else f"origin/{self._base}"
|
||||
git.reset_to(branch, start)
|
||||
# Even when it stayed: a crash may have come between parking and the
|
||||
# next item's start.
|
||||
self.unpark(mine)
|
||||
|
||||
def unpark(self, mine: str) -> None:
|
||||
parked = f"{mine}parked"
|
||||
git = self._git
|
||||
sha = git.remote_sha(self._remote, parked, credential=self._credential)
|
||||
if sha is None:
|
||||
return
|
||||
self.park(keep=keep)
|
||||
if branch in git.local_branches():
|
||||
git.checkout_existing_branch(branch)
|
||||
git.reset_to(branch, branch)
|
||||
else:
|
||||
if not git.has_commit(sha):
|
||||
git.fetch(credential=self._credential)
|
||||
git.reset_to(branch, f"origin/{branch}")
|
||||
git.apply_stash(sha)
|
||||
git.delete_remote_branch(self._remote, parked, credential=self._credential)
|
||||
|
||||
def drop_parked(self, mine: str) -> None:
|
||||
"""A closed item's parked work, if any, goes: closing set it aside."""
|
||||
parked = f"{mine}parked"
|
||||
git = self._git
|
||||
if git.remote_sha(self._remote, parked, credential=self._credential):
|
||||
git.delete_remote_branch(self._remote, parked, credential=self._credential)
|
||||
|
||||
def _ahead_of_origin(self) -> bool:
|
||||
current = self._git.current_branch()
|
||||
@@ -136,8 +171,12 @@ class Milestones:
|
||||
# -- milestones (D4) -------------------------------------------------------
|
||||
|
||||
def start(self, branch: str) -> str:
|
||||
"""Cut the milestone's branch where the item is; returns its start."""
|
||||
self._git.checkout_new_branch(branch)
|
||||
"""Cut the milestone's branch from the latest default branch (D4):
|
||||
a milestone after a merge builds on it, and one redone after a closed
|
||||
PR doesn't carry the rejected work. Uncommitted work comes along.
|
||||
Returns where it starts."""
|
||||
self._git.fetch(credential=self._credential)
|
||||
self._git.checkout_new_branch(branch, f"origin/{self._base}")
|
||||
return self._git.head_sha()
|
||||
|
||||
def publish(
|
||||
@@ -148,6 +187,7 @@ class Milestones:
|
||||
title: str,
|
||||
summary: str,
|
||||
issue: int,
|
||||
final: bool = True,
|
||||
) -> Published:
|
||||
git = self._git
|
||||
try:
|
||||
@@ -184,7 +224,7 @@ class Milestones:
|
||||
head=branch,
|
||||
base=self._base,
|
||||
title=title,
|
||||
body=_body(summary, issue, left_out),
|
||||
body=_body(summary, issue, left_out, final),
|
||||
)
|
||||
self._store.commit_intent(intent, cause=f"opened PR #{pr.number}")
|
||||
return Published("opened", f"opened PR #{pr.number}", pr, left_out)
|
||||
@@ -227,10 +267,13 @@ class Milestones:
|
||||
return Fixed("failed", f"pushing the fix failed: {exc}")
|
||||
|
||||
|
||||
def _body(summary: str, issue: int, left_out: tuple[str, ...]) -> str:
|
||||
def _body(
|
||||
summary: str, issue: int, left_out: tuple[str, ...], final: bool = True
|
||||
) -> str:
|
||||
parts = [summary]
|
||||
if issue:
|
||||
parts += ["", f"Closes #{issue}"]
|
||||
# D1: the last PR closes the issue on merge; earlier ones are part of it.
|
||||
parts += ["", f"Closes #{issue}" if final else f"Part of #{issue}"]
|
||||
if left_out:
|
||||
parts += ["", "## Left out", "", "Build artifacts, never part of the work:"]
|
||||
parts += [f"- `{path}`" for path in left_out]
|
||||
|
||||
Reference in new issue
Block a user