Pull request facts come from seat git's pull, ancestry from contains, the review time from the runner's review step, and get shows one pipeline row per module (no run where the runner's list answers none). Builds: design 47 G §654, A-75 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
208 lines
6.8 KiB
Go
208 lines
6.8 KiB
Go
package main
|
|
|
|
// A fix whose pull request the tracker keeps no record for (merged before the tracker heard the delivery, or linked after it
|
|
// moved) learns its merge here, so the view's `delivered` event can cover it like any recorded fix (issue 1950). The delivery
|
|
// is read from the seat `git`'s `pull`. One that neither can say is left for the next time and logged; a pull request closed without merging becomes
|
|
// the withdrawn state the tracker already knows.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// pullFacts reads the seat `git`'s pull request block (the verb `pull`: state open, merged or closed, the merge commit
|
|
// and when it merged, and the head commit).
|
|
type pullFacts struct {
|
|
State string `json:"state"`
|
|
Head string `json:"head"`
|
|
MergeCommit string `json:"merge-commit"`
|
|
MergedAt *time.Time `json:"merged-at"`
|
|
}
|
|
|
|
// fromPull completes a delivery with what the forge's pull request says: whether it is merged or closed, the commit its
|
|
// merge landed as and when.
|
|
func (d *Delivery) fromPull(p pullFacts, repository string) {
|
|
d.Pulled = true
|
|
d.Merged = p.State == "merged"
|
|
d.Closed = p.State == "closed"
|
|
if d.Repository == "" {
|
|
d.Repository = strings.ToLower(repository)
|
|
}
|
|
if d.Merged {
|
|
if d.MergedAs == "" {
|
|
d.MergedAs = p.MergeCommit
|
|
}
|
|
if d.MergedAt == nil {
|
|
d.MergedAt = nonZero(p.MergedAt)
|
|
}
|
|
}
|
|
}
|
|
|
|
// answerOf is a seat verb's answer body: the object itself, or the one an `answer` field wraps. A verb that failed
|
|
// answers `error` (and `error-kind`) alone, which a caller reads as a failure, never as none.
|
|
func answerOf(raw json.RawMessage, verb string) (map[string]json.RawMessage, error) {
|
|
var m map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &m) != nil {
|
|
return nil, fmt.Errorf("%s is not readable: %.200s", verb, raw)
|
|
}
|
|
if w, ok := m["answer"]; ok {
|
|
var inner map[string]json.RawMessage
|
|
if json.Unmarshal(w, &inner) == nil {
|
|
m = inner
|
|
}
|
|
}
|
|
if e, ok := m["error"]; ok {
|
|
var why string
|
|
_ = json.Unmarshal(e, &why)
|
|
return nil, fmt.Errorf("%s failed: %.200s", verb, why)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// parsePull reads the answer of the seat `git`'s `pull` into a delivery, and answers the pull request's head commit.
|
|
func parsePull(raw json.RawMessage, repository string) (Delivery, string, error) {
|
|
m, err := answerOf(raw, "git.pull")
|
|
if err != nil {
|
|
return Delivery{}, "", err
|
|
}
|
|
var p pullFacts
|
|
if json.Unmarshal(m["pull"], &p) != nil || p.State == "" {
|
|
return Delivery{}, "", errors.New("git.pull answered no pull request")
|
|
}
|
|
var d Delivery
|
|
d.fromPull(p, repository)
|
|
return d, p.Head, nil
|
|
}
|
|
|
|
// askPull asks the seat `git` for one pull request by its repository (owner/name) and number.
|
|
func askPull(repository string, number string) (Delivery, string, error) {
|
|
var n int
|
|
if _, err := fmt.Sscanf(number, "%d", &n); err != nil || n <= 0 {
|
|
return Delivery{}, "", fmt.Errorf("%q is not a pull request number", number)
|
|
}
|
|
raw, err := stdio.Ask("seat:git.pull", map[string]any{"repository": repository, "number": n})
|
|
if err != nil {
|
|
return Delivery{}, "", err
|
|
}
|
|
return parsePull(raw, repository)
|
|
}
|
|
|
|
// busPull asks the seat `git` for one pull request (`pull`): what the delivery of a fix is learned from, now that
|
|
// the delivery's own `checks` is retired.
|
|
func busPull(owner string) DeliveryState {
|
|
return func(repository, number string) (Delivery, error) {
|
|
d, _, err := askPull(owner+"/"+repository, number)
|
|
return d, err
|
|
}
|
|
}
|
|
|
|
// learnFix gives a pull request an issue names and keeps no Fix record for its record: from the delivery's own state when it has
|
|
// one, else from the pull request itself (the delivery's, then the forge's). It answers whether a record was kept. A pull request
|
|
// still open is not kept (its own events say it when they come); one neither can say anything of is left for next time.
|
|
func (t *Tracker) learnFix(n uint64, pr string) (bool, error) {
|
|
repo, number, _ := strings.Cut(pr, "#")
|
|
var d Delivery
|
|
var derr error
|
|
if t.Deliveries != nil {
|
|
d, derr = t.Deliveries(repo, number)
|
|
}
|
|
if derr == nil && d.State != "" {
|
|
since := d.Since
|
|
if since.IsZero() {
|
|
since = t.now()
|
|
}
|
|
ch, err := t.recordFix(pr, d.heard(since), n)
|
|
return len(ch) > 0, err
|
|
}
|
|
if derr != nil || !d.Pulled {
|
|
if t.Pull == nil {
|
|
return false, derr
|
|
}
|
|
f, ferr := t.Pull(repo, number)
|
|
if ferr != nil {
|
|
if derr != nil {
|
|
return false, derr
|
|
}
|
|
t.Logf("[mesh-issues] the pull request %s (issue %d) could not be read from the forge (asked again later): %v", pr, n, ferr)
|
|
return false, nil
|
|
}
|
|
d = f
|
|
}
|
|
var h heardFix
|
|
switch {
|
|
case d.Merged:
|
|
// Kept as of now, the moment it was learned: the fix's state did not change days ago for the tracker, and the stuck
|
|
// sweep measures from it. MergedAt stays the forge's own moment.
|
|
h = heardFix{State: "merged", Merged: true, At: t.now(), MergedAt: d.MergedAt, Repository: d.Repository, MergedAs: d.MergedAs}
|
|
case d.Closed:
|
|
h = heardFix{State: "closed", Why: "closed without merging", At: t.now()}
|
|
default:
|
|
return false, nil
|
|
}
|
|
ch, err := t.recordFix(pr, h, n)
|
|
return len(ch) > 0, err
|
|
}
|
|
|
|
// learnUnheard learns, for at most most pull requests, each fix of issue n with no record; it answers whether any was kept.
|
|
func (t *Tracker) learnUnheard(n uint64, is *Issue, most int) (int, bool, error) {
|
|
asked, kept := 0, false
|
|
for _, pr := range fixPullRequests(is) {
|
|
if fixOf(is, pr) != nil {
|
|
continue
|
|
}
|
|
if asked == most {
|
|
break
|
|
}
|
|
asked++
|
|
ok, err := t.learnFix(n, pr)
|
|
if err != nil {
|
|
return asked, kept, fmt.Errorf("the delivery of %s (issue %d) could not be asked: %v", pr, n, err)
|
|
}
|
|
kept = kept || ok
|
|
}
|
|
return asked, kept, nil
|
|
}
|
|
|
|
// LearnFixes is the tool that asks again for one issue: every pull request its fixed-by names with no record is asked of the
|
|
// delivery and the forge now. It answers what each is kept as.
|
|
func (t *Tracker) LearnFixes(n uint64) (map[string]any, error) {
|
|
is, _, err := read(t.Store, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, _, err := t.learnUnheard(n, is, MostSweepAsks*2); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := t.coverReached(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := t.afterFix(n); err != nil {
|
|
return nil, err
|
|
}
|
|
is, _, err = read(t.Store, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
for _, pr := range fixPullRequests(is) {
|
|
switch f := fixOf(is, pr); {
|
|
case f == nil:
|
|
out[pr] = "nothing heard: neither the delivery nor the forge said"
|
|
case withdrawn(f):
|
|
out[pr] = "withdrawn: closed without merging"
|
|
case contains(is.Delivered, pr):
|
|
out[pr] = "delivered"
|
|
case f.Merged:
|
|
out[pr] = "merged as " + shortCommit(f.MergedAs) + ", not delivered"
|
|
default:
|
|
out[pr] = f.State
|
|
}
|
|
}
|
|
return map[string]any{"number": n, "fixes": out, "delivery": DeliveryLine(is), "pipelines": t.pipelinesOf(is)}, nil
|
|
}
|