GIT4: the seat git's branch answers a branch's head and whether a rule refuses a direct push and admits only merges

Builds: design 47 G §5, §111, §310 branch

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:32:33 +01:00
parent b471b5cf9e
commit 0bf636826a
3 changed files with 21 additions and 3 deletions
+2 -1
View File
@@ -170,6 +170,7 @@
"scope": "mesh",
"serves": [
"pull",
"branch",
"open-pulls",
"tags",
"trees",
@@ -251,7 +252,7 @@
"type": "string",
"trusted": true,
"runtime": true,
"default": "{\"publish\":[\"mesh-build-go\",\"mesh-build-node\"],\"publish-part\":[\"mesh-build-go\",\"mesh-build-node\"],\"remove\":[\"mesh-delivery-builder\"],\"versions\":[\"*\"],\"pull\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-gate\"],\"open-pulls\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"tags\":[\"mesh-delivery-forge-bridge\"],\"trees\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"file\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"contains\":[\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"statuses\":[\"mesh-delivery-gate\"],\"status\":[\"mesh-delivery-forge-bridge\"],\"comment\":[\"mesh-delivery-forge-bridge\"],\"view\":[\"mesh-delivery-forge-bridge\"],\"note\":[\"mesh-delivery-forge-bridge\"],\"merge-pull\":[\"mesh-delivery-forge-bridge\"]}",
"default": "{\"publish\":[\"mesh-build-go\",\"mesh-build-node\"],\"publish-part\":[\"mesh-build-go\",\"mesh-build-node\"],\"remove\":[\"mesh-delivery-builder\"],\"versions\":[\"*\"],\"pull\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-gate\"],\"branch\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\"],\"open-pulls\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"tags\":[\"mesh-delivery-forge-bridge\"],\"trees\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"file\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"contains\":[\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"statuses\":[\"mesh-delivery-gate\"],\"status\":[\"mesh-delivery-forge-bridge\"],\"comment\":[\"mesh-delivery-forge-bridge\"],\"view\":[\"mesh-delivery-forge-bridge\"],\"note\":[\"mesh-delivery-forge-bridge\"],\"merge-pull\":[\"mesh-delivery-forge-bridge\"]}",
"description": "per verb of the seats the module holds, the callers that may call it (JSON object of verb to callers; * every proved caller, operator the operator's session); each holder reads the lines of its own verbs. The package seats' lines are design 51's (empty is that list for them); the seat git's are design 47's: its reads the parts that declare them in invokes, its acts (status, comment, view, note, merge-pull) the forge bridge alone, and a verb of git with no line is called by no one"
}
},
+15 -2
View File
@@ -20,7 +20,7 @@ import { callerOf, refusal, parseVerbCallers, type VerbCallers } from "@novox/me
import { config } from "@novox/mesh-sdk/runtime";
import {
seat as GIT, verbs as GIT_VERBS,
type ContainsAnswer, type CommentAnswer, type ErrorKind, type FileAnswer, type MergePullAnswer, type Merging, type NoteAnswer,
type BranchAnswer, type ContainsAnswer, type CommentAnswer, type ErrorKind, type FileAnswer, type MergePullAnswer, type Merging, type NoteAnswer,
type OpenPull, type OpenPullsAnswer, type Pull, type PullAnswer, type Status, type StatusAnswer, type StatusesAnswer,
type TagsAnswer, type TreesAnswer, type ViewAnswer,
} from "@novox/mesh-delivery-contracts-git";
@@ -170,6 +170,14 @@ export function seatTools(gitea: GiteaClient, deps: SeatDeps): ToolDefinition[]
const pull = pullOf(full, p);
return pull.state === "open" ? { pull, merging: await mergingOf(gitea, owner, repo, p) } : { pull };
}),
verb(GIT_VERBS.branch, "The seat git's branch: a branch's head, and whether a protection rule refuses a direct push and admits only merges.",
async (args): Promise<BranchAnswer> => {
const { owner, repo, full } = repositoryOf(args.repository);
const branch = String(args.branch ?? "");
// The head first: a branch the forge does not hold is its 404, the kind not-found, never an unprotected branch.
const head = await gitea.branchHead(owner, repo, branch);
return { repository: full, branch, head, protected: pushRefused(await gitea.appliedProtection(owner, repo, branch)) };
}),
verb(GIT_VERBS.openPulls, "The seat git's open-pulls: every open pull request of every repository, with the statuses its base requires and those its head has.",
async (): Promise<OpenPullsAnswer> => {
const pulls: OpenPull[] = (await gitea.openPulls()).map((o) => ({
@@ -273,6 +281,11 @@ export function seatTools(gitea: GiteaClient, deps: SeatDeps): ToolDefinition[]
];
}
/** Whether a protection rule refuses a direct push and admits only merges: the rule exists and takes no push. */
export function pushRefused(rule: { enable_push?: boolean } | null): boolean {
return !!rule && rule.enable_push === false;
}
function holds(comments: GiteaComment[], id: string): boolean {
return comments.some((c) => String(c.body ?? "").includes(actMarker(id)));
}
@@ -295,7 +308,7 @@ export async function mergePull(gitea: GiteaClient, args: Record<string, unknown
return no(`its head is ${String(pull.head_sha).slice(0, 12)}, not the ${head.slice(0, 12)} the caller names`);
}
const rule = await gitea.appliedProtection(owner, repo, String(pull.base ?? ""));
if (!rule || rule.enable_push !== false) {
if (!pushRefused(rule)) {
return no(`its base ${pull.base} is guarded by no protection rule that refuses a push`);
}
try {
+4
View File
@@ -562,6 +562,10 @@ test("every read and act of the seat git answers its contract, and an error is a
["pull", "by merge commit", { repository: "novox/app", "merge-commit": OTHER }, {}, (a) => [a.pull.state, a.pull["merge-commit"], a.merging], ["merged", OTHER, undefined]],
["pull", "unknown", { repository: "novox/app", number: 404 }, {}, (a) => [a.pull, a["error-kind"]], [undefined, "not-found"]],
["pull", "forge down", { repository: "novox/app", number: 9 }, { getPullRequest: down }, (a) => [a.pull, a["error-kind"]], [undefined, "failed"]],
["branch", "a branch a rule guards", { repository: "novox/app", branch: "main" }, { branchHead: async () => SHA, appliedProtection: async () => ({ rule_name: "main", enable_push: false }) }, (a) => [a.head, a.protected], [SHA, true]],
["branch", "a branch no rule guards", { repository: "novox/app", branch: "scratch" }, { branchHead: async () => SHA, appliedProtection: async () => null }, (a) => [a.head, a.protected], [SHA, false]],
["branch", "a rule that takes a push", { repository: "novox/app", branch: "main" }, { branchHead: async () => SHA, appliedProtection: async () => ({ rule_name: "main", enable_push: true }) }, (a) => [a.protected], [false]],
["branch", "no such branch", { repository: "novox/app", branch: "nope" }, { branchHead: gone }, (a) => [a.head, a["error-kind"]], [undefined, "not-found"]],
["open-pulls", "one", {}, {}, (a) => [a.pulls.length, a.pulls[0].required], [1, ["mesh/check"]]],
["open-pulls", "forge down", {}, { openPulls: down }, (a) => [a.pulls, a["error-kind"]], [undefined, "failed"]],
["tags", "a page", { repository: "novox/app", prefix: "go/" }, {}, (a) => [a.tags, a.page, a.bound, a.more], [["go/v0.1.1", "go/v0.1.0"], 1, 50, undefined]],