Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/learn.go
T
jschoubben 116751130f
mesh/delivery merged: source.main moved to novox/mesh-catalog@9bd824791509
mesh/check being carried to 116751130f5c on 9bd824791509 | from c05aa5e6eafd
Cover a merged fix from the delivery view's reached head in the sweep, not only from its once-said event, so a fix named after the saying is delivered (issue 1950)
2026-10-27 21:17:53 +01:00

183 lines
5.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 asked first (its `checks` carries the forge's own pull request beside its delivery); the forge is asked when the delivery
// cannot say. 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 forge's pull request block, as gitea_get_pull_request and mesh-delivery's checks both carry it.
type pullFacts struct {
Merged bool `json:"merged"`
State string `json:"state"`
MergeCommitSHA string `json:"merge_commit_sha"`
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.Merged
d.Closed = p.State == "closed" && !p.Merged
if d.Repository == "" {
d.Repository = strings.ToLower(repository)
}
if p.Merged {
if d.MergedAs == "" {
d.MergedAs = p.MergeCommitSHA
}
if d.MergedAt == nil {
d.MergedAt = nonZero(p.MergedAt)
}
}
}
// busPull asks the forge for one pull request (gitea_get_pull_request).
func busPull(owner string) DeliveryState {
return func(repository, number string) (Delivery, 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("gitea.gitea_get_pull_request", map[string]any{"owner": owner, "repo": repository, "number": n})
if err != nil {
return Delivery{}, err
}
var out struct {
Pull *pullFacts `json:"pull"`
Answer struct {
Pull *pullFacts `json:"pull"`
} `json:"answer"`
}
if json.Unmarshal(raw, &out) != nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request is not readable")
}
p := out.Pull
if p == nil {
p = out.Answer.Pull
}
if p == nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request answered no pull request")
}
var d Delivery
d.fromPull(*p, owner+"/"+repository)
return d, nil
}
}
// 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)}, nil
}