Skip to content
quarry Artifact [96b61dfc63]
Login

Artifact [96b61dfc63]

Artifact 96b61dfc63cfd554fdfb4176b2a0d2e0e49b5687d81513d23884a672f9f1d599:


// 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) }