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
96 lines
4.6 KiB
Go
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)
|
|
}
|