// landedwork.go answers one question two surfaces were guessing at:
// has this branch's work already reached the target?
//
// Both the check-in page ("Propose <branch>") and the landings page's
// unproposed list asked four cheap questions — is this the tip, is it
// not the target, is the branch open, does no open landing name it —
// and none of them is the question. Landing a branch normally CLOSES
// it, so the open test covers the usual case; a branch landed and left
// open still got the offer, and accepting it opened a record that
// rendered "merges up-to-date" on arrival. An empty landing in the
// queue, and a wasted trip.
//
// The honest test is the merge probe the store already answers, which
// is why this file is small: it is a placement decision, not an
// algorithm.
package api
import (
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/zitcli"
)
// landedProbeCap bounds how many probes one unproposed listing will
// pay for.
//
// Each probe is a zit subprocess. The four cheap questions usually
// leave nothing — a forge with no stranded work probes nothing at all —
// but a store that has accumulated dozens of open leaves must not turn
// one page view into dozens of subprocesses. Past the cap the rows are
// reported UNPROBED, which is exactly the behaviour that existed before
// this file: the reader may be sent to a record that says up-to-date on
// arrival. Degrading to the old answer beats degrading to a slow page,
// and beats silently dropping rows nobody checked.
const landedProbeCap = 12
// alreadyLanded reports whether branchTip's work is already in
// targetTip, by asking the store.
//
// A probe that cannot run answers FALSE — not landed. The whole point
// of this check is to withhold an offer, and withholding one because a
// subprocess failed would hide real stranded work behind an
// infrastructure fault. The old behaviour is the safe default here.
//
// Keyed on the pair of tips, so the answer is memoised for as long as
// neither side moves — which is what makes it affordable on a page-view
// path at all.
func alreadyLanded(repo *Repo, branchTip, targetTip string) bool {
if repo == nil || repo.Zit == nil || branchTip == "" || targetTip == "" {
return false
}
state, _, err := repo.Zit.MergeProbe(branchTip, targetTip, branchTip+":"+targetTip)
if err != nil {
return false
}
return state == "up-to-date"
}
// dropLandedRows removes the rows whose work is already in the target,
// probing at most landedProbeCap of them.
//
// Ordered as the caller built it — newest branch activity first — so
// the cap spends its probes on the rows a reader is most likely to act
// on rather than on whatever the index happened to list last.
func dropLandedRows(repo *Repo, rows []unproposedRow, targetTip string) []unproposedRow {
if targetTip == "" {
return rows
}
out := make([]unproposedRow, 0, len(rows))
probes := 0
for _, row := range rows {
if probes < landedProbeCap {
probes++
if alreadyLanded(repo, row.Tip, targetTip) {
continue
}
}
out = append(out, row)
}
return out
}
// branchLandedIn is branch.merges' input: one branch, against the
// repository's default branch.
type branchLandedIn struct {
Repo string `json:"repo" validate:"required,max=64"`
Branch string `json:"branch" validate:"required,max=200"`
}
// branchLandedOut says where the branch stands relative to the target.
type branchLandedOut struct {
// State is the store's own word: "clean", "up-to-date",
// "unrelated", "conflict", or empty when no probe could run. Passed
// through rather than folded to a boolean, because a caller
// deciding whether to offer a control wants "conflict" and
// "up-to-date" to read differently even if both withhold it.
State string `json:"state"`
// Landed is the one question the offer turns on, stated plainly so
// a template does not have to know the vocabulary.
Landed bool `json:"landed"`
Target string `json:"target"`
Tip string `json:"tip"`
}
// tipOfBranch resolves a branch name to its tip through the index.
func tipOfBranch(ix *index.Index, name string) string {
if ix == nil || name == "" {
return ""
}
b, ok, err := ix.TipOf(name)
if err != nil || !ok {
return ""
}
return b.Tip
}
// leafKey is zitcli's key shape, named here so the callers read as one
// thought rather than reaching across packages mid-condition.
func leafKey(tip string) string { return zitcli.LeafKey(tip) }