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 }