153 lines
5.8 KiB
Go
153 lines
5.8 KiB
Go
package bridge
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Call is a call of a forge tool: the tool by its manifest key and its arguments.
|
|
type Call struct {
|
|
Tool string
|
|
Args map[string]any
|
|
}
|
|
|
|
// Act is one thing the bridge does on the forge for a key of the gate: the act's name (an owed key's act, `status=<context>`,
|
|
// `view`, `note`, `comment.<seq>`, or `merge`), the pull request and head it is on (Number 0 for a commit no pull
|
|
// request names) and the forge call that performs it.
|
|
type Act struct {
|
|
Name string
|
|
Repository string
|
|
Number int
|
|
Head string
|
|
Call Call
|
|
}
|
|
|
|
// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: delivery.ts
|
|
// commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view
|
|
// (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo,
|
|
// number, body), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads).
|
|
const (
|
|
commitStatusTool = "gitea.gitea_commit_status"
|
|
deliveryViewTool = "gitea.gitea_delivery_view"
|
|
noteAppendTool = "gitea.gitea_note_append"
|
|
addCommentTool = "gitea.gitea_add_comment"
|
|
mergePullTool = "gitea.gitea_merge_pull_request"
|
|
)
|
|
|
|
// MergeAct is the name of the act of merging a pull request.
|
|
const MergeAct = "merge"
|
|
|
|
func ownerRepo(repository string) (owner, repo string) {
|
|
owner, repo, _ = strings.Cut(repository, "/")
|
|
return owner, repo
|
|
}
|
|
|
|
// OwedAct turns an owed key and its value (mesh-sdk go/delivery OwedKey, EncodeOwed) into the forge call that performs
|
|
// it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment
|
|
// on no pull request are errors, each saying why.
|
|
func OwedAct(key string, value []byte) (Act, error) {
|
|
repository, number, head, name, err := delivery.ParseOwedKey(key)
|
|
if err != nil {
|
|
return Act{}, err
|
|
}
|
|
o, err := delivery.DecodeOwed(value)
|
|
if err != nil {
|
|
return Act{}, fmt.Errorf("the owed act %s: %w", key, err)
|
|
}
|
|
kind, context, err := delivery.ParseOwedAct(name)
|
|
if err != nil {
|
|
return Act{}, err
|
|
}
|
|
if o.Act != kind {
|
|
return Act{}, fmt.Errorf("the owed act %s says %q, not the %q its key names", key, o.Act, kind)
|
|
}
|
|
owner, repo := ownerRepo(repository)
|
|
a := Act{Name: name, Repository: repository, Number: number, Head: head}
|
|
needsPull := func() error {
|
|
if number <= 0 {
|
|
return fmt.Errorf("the owed %s %s is on a commit no pull request names, and it needs the pull request", kind, key)
|
|
}
|
|
return nil
|
|
}
|
|
switch kind {
|
|
case delivery.OwedStatus:
|
|
if o.Context != context {
|
|
return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context)
|
|
}
|
|
a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context,
|
|
"state": o.State, "description": o.Text, "target_url": o.Target}}
|
|
case delivery.OwedView:
|
|
if err := needsPull(); err != nil {
|
|
return Act{}, err
|
|
}
|
|
a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}}
|
|
case delivery.OwedNote:
|
|
a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}}
|
|
case delivery.OwedComment:
|
|
if err := needsPull(); err != nil {
|
|
return Act{}, err
|
|
}
|
|
a.Call = Call{Tool: addCommentTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}}
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// MergeActs turns the value of a merges key (mesh-sdk go/delivery EncodeMerge) into the merges to perform: one for each
|
|
// pull request in the state merging, in the order the value names them, at the head it expects and with the approved
|
|
// heads of every member of the group, `<owner>/<repo>@<full sha>` sorted (the merge tool's approved_heads). A pull
|
|
// request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool
|
|
// needs them for a group.
|
|
func MergeActs(value []byte) ([]Act, error) {
|
|
m, err := delivery.DecodeMerge(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var handed []delivery.MergePull
|
|
for _, p := range m.Pulls {
|
|
if p.State == delivery.MergeMerging {
|
|
handed = append(handed, p)
|
|
}
|
|
}
|
|
if len(handed) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(m.Approved) == 0 {
|
|
return nil, fmt.Errorf("the merge of the group %s names no approved heads, so the forge's merge tool has nothing to merge at", m.Group)
|
|
}
|
|
approved := make([]string, 0, len(m.Approved))
|
|
for key, sha := range m.Approved {
|
|
repository, _, ok := strings.Cut(key, "@")
|
|
if !ok {
|
|
return nil, fmt.Errorf("the approved head %q of the group %s is not <repository>@<number>", key, m.Group)
|
|
}
|
|
approved = append(approved, repository+"@"+sha)
|
|
}
|
|
slices.Sort(approved)
|
|
var acts []Act
|
|
for _, p := range handed {
|
|
owner, repo := ownerRepo(p.Repository)
|
|
acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: p.Number, Head: p.Head.Head,
|
|
Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": p.Number, "head": p.Head.Head,
|
|
"approved_heads": approved}}})
|
|
}
|
|
return acts, nil
|
|
}
|
|
|
|
// RefusedMessage is the refused message the bridge writes on FORGE when the forge refuses an act: the act, the pull
|
|
// request and head it was on, and the forge's own words.
|
|
func RefusedMessage(a Act, words string) (Message, error) {
|
|
event, err := delivery.ForgeEvent(a.Repository, delivery.ForgeRefused)
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeRefused, Repository: a.Repository, Number: a.Number,
|
|
Head: a.Head, Act: a.Name, Words: words})
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
return Message{Event: event, Data: body}, nil
|
|
}
|