Files
mesh-delivery/modules/mesh-delivery-forge-bridge/internal/bridge/moved.go
T
jschoubben dce4d01a58 FB1, FB9, FB10: the forge bridge writes one message per move of every watched branch
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
2026-11-12 09:15:04 +01:00

96 lines
4.6 KiB
Go

package bridge
import (
"context"
"encoding/json"
"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"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// BranchMovedSubject is the seat git's branch-moved (design 47 G §11): a branch's new head, which a merge and an admitted
// push alike say, kept on the seat's stream SEAT_git_branch-moved and heard there through a durable consumer.
const BranchMovedSubject = git.SubjectBranchMoved
// Moved is a branch-moved as the bridge reads it: the repository's two halves and the branch. The head the event says is
// never taken: the commit is read from the forge's own record by the seat's verb branch (§11).
type Moved struct {
Owner, Repo, Branch string
}
// MovedOf reads a branch-moved's body.
func MovedOf(data []byte) (Moved, error) {
var e git.BranchMovedEvent
if err := json.Unmarshal(data, &e); err != nil {
return Moved{}, unreadable(BranchMovedSubject, "the body is not json: %v", err)
}
m := Moved{Branch: e.Branch}
m.Owner, m.Repo, _ = strings.Cut(e.Repository, "/")
if m.Owner == "" || m.Repo == "" || strings.ContainsAny(m.Owner+m.Repo, "./ *>") || m.Branch == "" {
return Moved{}, unreadable(BranchMovedSubject, "no repository <owner>/<name> or branch (%q, %q)", e.Repository, e.Branch)
}
return m, nil
}
// Repository is `<owner>/<name>`.
func (m Moved) Repository() string { return repository(m.Owner, m.Repo) }
// HeadKey is the key of a watched branch's last head written in the state heads: `branch.<owner>.<name>.<escaped
// branch>` (A-236), apart from a pull request's `<owner>.<name>.<number>`.
func (m Moved) HeadKey() string {
return "branch." + m.Owner + "." + m.Repo + "." + keys.EscapeName(m.Branch)
}
// Branch is a branch as the seat git's verb branch reads it: its head and whether it is protected.
type Branch struct {
Head string
Protected bool
}
// ReadBranch reads a branch's head and protection through the seat git's verb branch (§11, §17); an answer that says
// an error, or names no 40-hex head, is an error, never a branch with none.
func ReadBranch(ctx context.Context, call Caller, m Moved) (Branch, error) {
a, err := askGit[git.BranchAnswer](ctx, call, git.VerbBranch, git.BranchInput{Repository: m.Repository(), Branch: m.Branch})
if err != nil {
return Branch{}, err
}
if !sha40.MatchString(a.Head) {
return Branch{}, fmt.Errorf("the seat git's branch %s of %s names no 40-hex head (%q)", m.Branch, m.Repository(), a.Head)
}
return Branch{Head: a.Head, Protected: a.Protected}, nil
}
// MoveMessage is the one message of a move of a watched branch to head (design 47 G §509, §714, A-236): merged, with
// the pull request and the watched branch as base, when the seat's verb pull names head a pull request's merge; pushed,
// with the branch, head and before (the head last written for the branch, empty for none), when it names none. Both
// carry the take-in inputs at head. A pull the forge could not say is ErrRetry, never read as none.
func MoveMessage(ctx context.Context, call Caller, m Moved, head, before string, trees Trees, files Files, kept Kept) (Message, error) {
what := fmt.Sprintf("%s of %s at %s", m.Branch, m.Repository(), head)
raw, err := call(ctx, GitKey(git.VerbPull), git.PullInput{Repository: m.Repository(), MergeCommit: head})
if err != nil {
return Message{}, fmt.Errorf("%w: %s: the seat git did not answer pull: %v", ErrRetry, what, err)
}
var a git.PullAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return Message{}, fmt.Errorf("%w: %s: the seat git's answer of pull is not its contract's: %v", ErrRetry, what, err)
}
f := forgemsg.Forge{Repository: m.Repository()}
switch {
case a.Error != "" && a.ErrorKind == git.ErrorKindNotFound:
f.Kind, f.Branch, f.Head, f.Before = forgemsg.ForgePushed, m.Branch, head, before
case a.Error != "":
return Message{}, fmt.Errorf("%w: %s: the seat git refused pull: %s", ErrRetry, what, a.Error)
case a.Pull == nil || a.Pull.Number <= 0 || a.Pull.MergeCommit != head || a.Pull.State != git.PullStateMerged:
return Message{}, fmt.Errorf("%w: %s: the seat git's pull at that merge commit names no merged pull request merged there", ErrRetry, what)
default:
if !sha40.MatchString(a.Pull.Head) || a.Pull.Branch == "" {
return Message{}, unreadable(BranchMovedSubject, "%s: the pull request %s#%d names no branch or 40-hex head", what, m.Repository(), a.Pull.Number)
}
f.Kind, f.Branch, f.Number, f.Head, f.Merge, f.Base = forgemsg.ForgeMerged, a.Pull.Branch, int(a.Pull.Number), a.Pull.Head, head, m.Branch
}
return landed(what, f, head, trees, files, kept)
}