package api
import (
"context"
"sort"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// queuedLanding is one open landing the caller could accept, with the
// repository it is in: the queue spans every repository, so a row that
// did not say where it was would not be a link anybody could follow.
type queuedLanding struct {
Repo string `json:"repo"`
index.LandingSummary
}
type landingQueueIn struct{}
type landingQueueOut struct {
Landings []queuedLanding `json:"landings"`
Count int `json:"count"`
}
// registerLandingQueue wires `landing.queue`, the reviewer's side of
// landings. Who opened a landing is the least interesting fact about it
// to the person who has to decide it; this lists the decisions.
func registerLandingQueue(r *ops.Registry, d Deps) error {
return ops.Register(r, ops.Op[landingQueueIn, landingQueueOut]{
Name: "landing.queue",
Doc: "Every open landing, across all repositories, that the signed-in account may accept: " +
"it holds the check-in capability on the repository, and the target is not a protected " +
"branch it does not own. Mirrors that refuse local writes are left out, as are landings " +
"superseded by a newer one on the same branch. Newest activity first.",
Kind: ops.Query,
Auth: ops.Token,
Route: "GET /api/v1/me/landings",
Run: func(ctx context.Context, _ landingQueueIn) (landingQueueOut, error) {
who := ops.User(ctx)
if who == "" {
return landingQueueOut{Landings: []queuedLanding{}}, nil
}
ls := d.landingQueue(ctx, who)
return landingQueueOut{Landings: ls, Count: len(ls)}, nil
},
})
}
// landingQueue reads the open landings the account may accept. Each
// repository is asked the same two questions the accept itself asks
// (landing.accept: the "i" letter, then RFC 0007's MayJoin), so the
// queue never lists a landing whose button would refuse.
//
// A repository that cannot be read is skipped rather than failing the
// list: one broken index must not hide every other decision.
func (d Deps) landingQueue(ctx context.Context, who string) []queuedLanding {
out := []queuedLanding{}
for _, name := range d.Repos.Names() {
if d.companionParent(name) != "" {
continue
}
if ok, _ := d.Writable(name); !ok {
continue
}
if d.CanRead != nil && !d.CanRead(ctx, name) {
continue
}
if !d.mayAcceptIn(ctx, who, name) {
continue
}
repo, ok := d.Repos.Get(name)
if !ok || repo.Index == nil {
continue
}
open, err := repo.Index.Landings("open", 0)
if err != nil || len(open) == 0 {
continue
}
branches := make([]string, 0, len(open))
for _, l := range open {
branches = append(branches, l.Branch)
}
verdicts, _ := repo.Index.VerdictsForLandings(branches)
nums := d.landingNumbers(name, repo, nil)
for _, l := range open {
if l.SupersededBy != "" {
continue
}
if d.Protect != nil && !d.Protect.MayJoin(name, who, l.Target) {
continue
}
l.CI = verdicts[l.Branch]
l.Number = nums[l.ID]
out = append(out, queuedLanding{Repo: name, LandingSummary: l})
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].Updated > out[j].Updated })
return out
}
// mayAcceptIn is the accept's capability check for a repository other
// than the one the request named. With a capability plane it asks the
// plane; without one it reads the request's own letters, and a forge
// with neither gates nothing, as every other op on it does.
func (d Deps) mayAcceptIn(ctx context.Context, who, repo string) bool {
if d.CapsFor != nil {
return d.CapsFor(who, repo).HasAny("i")
}
if ops.HasCaps(ctx) {
return ops.CapsOf(ctx).HasAny("i")
}
return true
}