Every worktree can carry a deterministic merge flow in <git-dir>/pkgmgr/flow.yml (never committed, gone with the worktree): pull lists the refs merged into the branch, in order, before a gate, deploy or merge; push lists the branches the finished branch is merged into, in order, each then pushed by the operator.
- pkgmgr worktree flow [PATH] [--json] shows it; flow set (--pull REF.. | --no-pull) (--push BRANCH.. | --no-push) replaces both lists atomically (0600 temp file, os.replace, pkgmgr/ of mode 0700); flow clear removes it. JSON: worktree, git_dir, branch, flow_branch, pull, push, source (file, none, invalid), warnings.
- pkgmgr worktree up gains repeatable --pull/--push; without them pull is the base and push its local branch (origin/main gives main).
- pkgmgr branch tree carries "flow": {pull, push, source} or null on every item with a worktree, read from the linked worktree's metadata dir without running git inside the worktree; the Agent Orchestrator shows it.
The file and the arguments may come from an agent: every entry must equal its git check-ref-format --branch output (scan mode, cwd /) and must not start with -; the reader opens pkgmgr/ and flow.yml without following links, non-blocking, refuses anything but a regular file, caps at 64 KiB, uses yaml.safe_load, rejects unknown keys, wrong types and bool versions, and never raises into callers: a bad file reads as source invalid with empty lists and a warning.
Tests: 30 new (unit and integration on real repositories), unit suite 500 OK, integration 254 with the two known sandbox signing errors of test_publish_integration only.
🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 97 %
⏱️ Time: 93 %
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.0 KiB
Python
106 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from unittest.mock import patch
|
|
|
|
from pkgmgr.actions.branch import gc, tree
|
|
from pkgmgr.actions.worktree import flow
|
|
from tests.integration.test_branch_gc import GcFixture
|
|
|
|
|
|
class TestBranchTreeFlow(GcFixture, unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.shared = os.path.join(self.root, "shared-worktrees")
|
|
os.makedirs(self.shared)
|
|
self.metas = os.path.join(self.repo, ".git", "worktrees")
|
|
for patcher in (
|
|
patch.object(gc, "host_pid_namespace", return_value=True),
|
|
patch.object(gc, "list_compose_containers", return_value=[]),
|
|
patch.object(gc, "_process_cwds", return_value={}),
|
|
patch.object(tree, "secret_service_token", return_value=(None, "locked")),
|
|
):
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def _entry(self) -> dict:
|
|
out = io.StringIO()
|
|
with redirect_stdout(out):
|
|
code = tree.branch_tree(
|
|
[self.repo], fmt="json", output=None, worktrees_root=self.shared
|
|
)
|
|
self.assertEqual(code, 0)
|
|
return json.loads(out.getvalue())["repos"][0]
|
|
|
|
def _flows(self, entry: dict) -> dict[str, dict | None]:
|
|
return {
|
|
item["branch"]: item["flow"] for item in entry["items"] if item["branch"]
|
|
}
|
|
|
|
def _flow_warnings(self, entry: dict) -> list[str]:
|
|
return [w for w in entry["warnings"] if w.startswith("flow of ")]
|
|
|
|
def test_items_carry_their_flow_or_null(self) -> None:
|
|
flow.write_flow(
|
|
os.path.join(self.metas, "wt-merged"), "merged", ["origin/main"], ["prod"]
|
|
)
|
|
flow.write_flow(os.path.join(self.repo, ".git"), "main", [], ["origin/main"])
|
|
|
|
entry = self._entry()
|
|
flows = self._flows(entry)
|
|
self.assertEqual(
|
|
flows["merged"],
|
|
{"pull": ["origin/main"], "push": ["prod"], "source": "file"},
|
|
)
|
|
self.assertEqual(
|
|
flows["main"], {"pull": [], "push": ["origin/main"], "source": "file"}
|
|
)
|
|
self.assertIsNone(flows["dirty"])
|
|
self.assertIsNone(flows["wip"])
|
|
self.assertTrue(all("flow" in item for item in entry["items"]))
|
|
self.assertEqual(self._flow_warnings(entry), [])
|
|
|
|
def test_invalid_and_mismatched_flows_warn_without_failing(self) -> None:
|
|
folder = os.path.join(self.metas, "wt-dirty", flow.FLOW_DIR)
|
|
os.makedirs(folder)
|
|
with open(os.path.join(folder, flow.FLOW_FILE), "w", encoding="utf-8") as fh:
|
|
fh.write("version: 1\npull: [a..b]\npush: []\n")
|
|
flow.write_flow(os.path.join(self.metas, "wt-merged"), "gone", ["main"], [])
|
|
|
|
entry = self._entry()
|
|
flows = self._flows(entry)
|
|
self.assertEqual(flows["dirty"], {"pull": [], "push": [], "source": "invalid"})
|
|
self.assertEqual(flows["merged"]["source"], "file")
|
|
warnings = self._flow_warnings(entry)
|
|
self.assertEqual(len(warnings), 2)
|
|
self.assertTrue(
|
|
any(
|
|
w.startswith(f"flow of {self.dirty_wt}: pkgmgr/flow.yml")
|
|
for w in warnings
|
|
)
|
|
)
|
|
self.assertIn(
|
|
f"flow of {self.merged_wt}: flow was set on gone, the worktree is on merged",
|
|
warnings,
|
|
)
|
|
|
|
def test_a_forged_registration_reads_no_flow(self) -> None:
|
|
flow.write_flow(os.path.join(self.metas, "wt-merged"), "merged", ["main"], [])
|
|
with open(os.path.join(self.metas, "wt-locked", "gitdir"), "w") as fh:
|
|
fh.write(os.path.join(self.merged_wt, ".git") + "\n")
|
|
|
|
items = {item["branch"]: item for item in self._entry()["items"]}
|
|
|
|
for branch in ("merged", "locked"):
|
|
with self.subTest(branch=branch):
|
|
self.assertEqual(items[branch]["worktree"], self.merged_wt)
|
|
self.assertIsNone(items[branch]["flow"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|