The bridge watches the registrations' branches by origin (a first view of registrations, then registration-changed), before the first registration the gate's source's (source-moved); no branch is named in its code, Trunk goes. Every move is heard on the seat git's branch-moved through the bridge's durable on SEAT_git_branch-moved, its head read by the seat's verb branch, never the event's: merged with its pull request and `base` (the watched branch) when the seat's pull names the head a pull request's merge, else `pushed` with branch, head and `before`, both with the take-in inputs at the head. The last head written per watched branch is kept in the state heads under branch.<owner>.<name>.<escaped branch>, so a move heard twice writes once, and each watched branch's head is read at the start and when a branch is registered. A move of an unprotected branch writes nothing. pull.merged into a watched branch writes nothing (its message is the move's); the forge-made merge's trunk filter goes. Builds: design 47 G §4, §7, §11, §20, §41, §507, §509, §532, §714, A-227, A-236 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
112 lines
4.9 KiB
Go
112 lines
4.9 KiB
Go
package bridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
git "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/git"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
|
)
|
|
|
|
// Caller calls a seat's verb by its manifest key (`seat:<seat>.<verb>`) with a JSON body and returns its answer: the
|
|
// bridge's process calls through the bus as its own principal, its tools bundle through the runtime (stdio.Ask).
|
|
type Caller func(ctx context.Context, key string, body any) (json.RawMessage, error)
|
|
|
|
// GitKey is the manifest key of a verb of the seat `git` (novox/hq design 47 *The seat `git`*): the bridge reaches the
|
|
// forge through the seat alone and names no module that holds it.
|
|
func GitKey(verb string) string { return "seat:" + git.Seat + "." + verb }
|
|
|
|
// askGit calls one verb of the seat `git` and decodes its answer into A. An answer the contract cannot type, or one
|
|
// that says an error, is the call's error: a failed read is never read as none (design 47 *A failed read answers an
|
|
// error*).
|
|
func askGit[A any](ctx context.Context, call Caller, verb string, in any) (A, error) {
|
|
var a A
|
|
raw, err := call(ctx, GitKey(verb), in)
|
|
if err != nil {
|
|
return a, fmt.Errorf("the seat git did not answer %s: %w", verb, err)
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return a, fmt.Errorf("the seat git's answer of %s is not its contract's: %w", verb, err)
|
|
}
|
|
var said struct {
|
|
Error string `json:"error"`
|
|
}
|
|
_ = json.Unmarshal(raw, &said)
|
|
if said.Error != "" {
|
|
return a, fmt.Errorf("the seat git refused %s: %s", verb, said.Error)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// repository is `<owner>/<name>`, as the seat names a repository.
|
|
func repository(owner, repo string) string { return owner + "/" + repo }
|
|
|
|
// TreesVia asks the seat `git` for a commit's trees (verb trees).
|
|
func TreesVia(ctx context.Context, call Caller) Trees {
|
|
return func(owner, repo, sha string) (string, map[string]string, error) {
|
|
a, err := askGit[git.TreesAnswer](ctx, call, git.VerbTrees, git.TreesInput{Repository: repository(owner, repo), Commit: sha})
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if a.Root == "" {
|
|
return "", nil, fmt.Errorf("the seat git's trees of %s at %s name no root tree", repository(owner, repo), sha)
|
|
}
|
|
return a.Root, a.Trees, nil
|
|
}
|
|
}
|
|
|
|
// FilesVia asks the seat `git` for one file at a ref (verb file). A file the seat says is absent is ErrNotFound; an
|
|
// answer that says neither absent nor content is an error, never an empty file.
|
|
func FilesVia(ctx context.Context, call Caller) Files {
|
|
return func(owner, repo, path, ref string) ([]byte, error) {
|
|
raw, err := call(ctx, GitKey(git.VerbFile), git.FileInput{Repository: repository(owner, repo), Path: path, Ref: ref})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the seat git did not answer file: %w", err)
|
|
}
|
|
var a struct {
|
|
git.FileAnswer
|
|
Content *string `json:"content"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return nil, fmt.Errorf("the seat git's answer of file is not its contract's: %w", err)
|
|
}
|
|
switch {
|
|
case a.Error != "":
|
|
return nil, fmt.Errorf("the seat git refused file %s at %s: %s", path, ref, a.Error)
|
|
case a.Absent:
|
|
return nil, fmt.Errorf("%w: %s at %s in %s", ErrNotFound, path, ref, repository(owner, repo))
|
|
case a.Content == nil:
|
|
return nil, fmt.Errorf("the seat git's file %s at %s says neither its content nor that it is absent", path, ref)
|
|
}
|
|
return []byte(*a.Content), nil
|
|
}
|
|
}
|
|
|
|
// errNoAnswer is a read whose answer carries nothing the contract says it must.
|
|
var errNoAnswer = errors.New("the seat git's answer carries nothing")
|
|
|
|
// MergedMessage is the merged message of a merge into the branch base, asked again: built as the bridge builds the one
|
|
// it writes on FORGE for the branch's move (MoveMessage), from the pull request's owner, repository, number, branch, head,
|
|
// merge commit and base, which the seat git's verb pull answers. It reads the forge for every module directory (no kept
|
|
// inputs).
|
|
func MergedMessage(owner, repo string, number int, branch, head, merge, base string, trees Trees, files Files) (Message, error) {
|
|
if number <= 0 || branch == "" || base == "" || !sha40.MatchString(head) {
|
|
return Message{}, unreadable("bridge_merged_message", "%s#%d names no branch, base or 40-hex head", repository(owner, repo), number)
|
|
}
|
|
f := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository(owner, repo), Branch: branch, Number: number, Head: head,
|
|
Merge: merge, Base: base}
|
|
return landed(repository(owner, repo)+"#"+fmt.Sprint(number), f, merge, trees, files, nil)
|
|
}
|
|
|
|
// splitRepository is a `<owner>/<name>` cut in two, refusing anything else.
|
|
func splitRepository(repository string) (string, string, error) {
|
|
owner, name, ok := strings.Cut(strings.TrimSpace(repository), "/")
|
|
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
|
return "", "", fmt.Errorf("repository %q is not <owner>/<name>", repository)
|
|
}
|
|
return owner, name, nil
|
|
}
|