Skip to content
quarry Artifact [fc45db8d2b]
Login

Artifact [fc45db8d2b]

Artifact fc45db8d2b908844a10e247402fe7b9bc1f68362f6644d9fca3ddd5b6ecc3f72:


package api

import (
	"context"
	"fmt"
	"slices"
	"strings"

	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/lease"
	"craftdesign.group/quarry/internal/ops"
)

type landingRetargetIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
	ID   string `json:"id" validate:"required,max=64"`
	To   string `json:"to" validate:"required,max=120"`
}

type landingRetargetOut struct {
	ID     string `json:"id"`
	Target string `json:"target"`
}

// registerLandingRetarget points an open record at a different target.
//
// It exists because a stack can strand itself. Landings on a stack are
// proposed onto the rung below, and when that rung lands on its own the
// target's work is already in the destination — so zit refuses the
// merge and says to retarget. zit has the verb; quarry had no way to
// reach it, which made a stranded landing unrecoverable from anything
// but a shell. Somebody holding a phone could read the instruction and
// not act on it.
//
// It takes accept's letter rather than the forum one. Retargeting does
// not write a merge, but it decides WHERE the next merge is written,
// and a record that can be aimed by anyone who may post in its thread
// is a way to point somebody else's work at a branch they never chose.
// Protection on the NEW target is still checked at accept time, where
// it belongs; who may aim the record is the author or whoever could
// accept it where it stands.
func registerLandingRetarget(r *ops.Registry, d Deps) error {
	return ops.Register(r, ops.Op[landingRetargetIn, landingRetargetOut]{
		Name:  "landing.retarget",
		Caps:  "i",
		Doc:   "Point an open record at a different target branch, or stack it on another open landing by naming that landing as !N: it then lands after that one, whether or not its branch was built on it. Unstacking is a retarget to the branch the strata lands on. The record's author may move it unless rungs opened by others stand on it and would move with it; whoever owns the branch its strata lands on may move it, both where it stands and where it would go. Refused when the move would make a loop, or when the two branches share no history.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/r/{repo}/landing/{id}/retarget",
		Run: func(ctx context.Context, in landingRetargetIn) (landingRetargetOut, error) {
			if err := d.refuseUnlessWritable(in.Repo); err != nil {
				return landingRetargetOut{}, err
			}
			repo, err := d.repoFor(ctx, in.Repo)
			if err != nil {
				return landingRetargetOut{}, err
			}
			// !12 or 12 is the landing's number, as landing.get reads it.
			in.ID = d.landingRef(in.Repo, repo, in.ID)
			l, ok, err := repo.Index.LandingByPrefix(in.ID)
			if err != nil {
				return landingRetargetOut{}, err
			}
			if !ok {
				return landingRetargetOut{}, fmt.Errorf("no landing %q: %w", in.ID, ops.ErrNotFound)
			}
			if l.State != "open" {
				return landingRetargetOut{}, fmt.Errorf(
					"the record is %s; a settled record does not move: %w", l.State, ops.ErrInvalid)
			}
			to := strings.TrimSpace(in.To)
			if to == "" {
				return landingRetargetOut{}, fmt.Errorf(
					"name the branch to land on: %w", ops.ErrInvalid)
			}
			if to, err = d.stackTarget(in.Repo, repo, to); err != nil {
				return landingRetargetOut{}, err
			}
			all, err := repo.Index.Landings("", 0)
			if err != nil {
				return landingRetargetOut{}, err
			}
			// WHO may move it: its author, or an owner of the branch its
			// strata lands on — where it stands now and where the move
			// would put it, so nobody can carry somebody else's work
			// out of a destination they do not own or into one. A
			// stacked rung's own target is a feature branch that nobody
			// protects; asking it would let any developer reorder a
			// strata. dmestas ruled it so (ae900759, then 2026-10-25 for
			// strata): the people who own the destination decide what
			// lands there and in what order, and the author may always
			// arrange their own proposal.
			//
			// An anonymous caller is nobody rather than everybody: an
			// empty author matches an empty record author by string
			// alone, which is two absences and not one person.
			//
			// A rung whose base was accepted and not yet retargeted lands
			// where that base did (landedBaseTarget), not on the base's
			// branch, which nobody protects.
			now, _, _ := index.LandsOn(all, l.Target)
			if landed := landedBaseTarget(repo, l); landed != "" {
				now = landed
			}
			if now == "" {
				now = l.Target
			}
			after, through, ok := index.LandsOn(all, to)
			who := author(ctx)
			owns := func(branch string) bool { return d.Protect == nil || d.Protect.MayJoin(in.Repo, who, branch) }
			// Moving a landing moves every rung stacked on it. The author
			// may arrange their own proposal, but carrying somebody else's
			// rungs with it is the destination owner's act.
			mine := who == l.Author && !carriesOthers(all, l, who)
			if who == "" || (!mine && !(owns(now) && (!ok || owns(after)))) {
				if who != "" && who == l.Author {
					return landingRetargetOut{}, fmt.Errorf(
						"%w: rungs opened by others stand on %.12s and would move with it; that is for whoever owns %s, where they land",
						ops.ErrForbidden, l.ID, now)
				}
				return landingRetargetOut{}, fmt.Errorf(
					"%w: %.12s was opened by %s; moving somebody else's record is for whoever owns %s, where it lands",
					ops.ErrForbidden, l.ID, landingAuthorName(l.Author), now)
			}
			if to == l.Branch {
				return landingRetargetOut{}, fmt.Errorf(
					"a branch cannot land on itself: %w", ops.ErrInvalid)
			}
			if to == l.Target {
				// Already where it was asked to go. The end state the
				// caller wanted, so this succeeds and writes nothing.
				return landingRetargetOut{ID: l.ID, Target: l.Target}, nil
			}
			if err := stackHazard(ctx, repo, l.Branch, to, through, ok); err != nil {
				return landingRetargetOut{}, err
			}
			if err := repo.write(func(tok lease.Token) error {
				_, werr := repo.Zit.LandingRetarget(tok, l.ID, author(ctx), to)
				return werr
			}); err != nil {
				return landingRetargetOut{}, err
			}
			return landingRetargetOut{ID: l.ID, Target: to}, nil
		},
	})
}

// landingAuthorName names a record's opener for a refusal without
// turning an anonymous proposal into a person called "".
func landingAuthorName(user string) string {
	if user == "" {
		return "an anonymous visitor"
	}
	return user
}

// strataVerdict is the verdict of a whole strata: green only when every
// rung is. A rung's checks test it merged onto the rung below, so the
// top's verdict covers the strata only when each rung was built on the
// one under it; a rung stacked on a sibling it never contained leaves
// the rungs beneath that one untested by the top's run. The worst rung
// answers for the stack, a failure before anything else.
func strataVerdict(segs []index.StackSegment) string {
	worst := "ci-pass"
	for _, sg := range segs {
		switch {
		case sg.CI == "ci-fail":
			return "ci-fail"
		case sg.CI != "ci-pass" && worst == "ci-pass":
			worst = sg.CI
		}
	}
	if len(segs) == 0 {
		return ""
	}
	return worst
}

// strataRelations says, for each rung above the bottom, whether its
// branch was built on the rung below it or only stacked there. Both
// land the same way, but a reader who sees "on #12" assumes the first;
// the page says "after" when it is the second.
func strataRelations(ctx context.Context, repo *Repo, st *index.Strata) {
	if repo == nil || repo.Index == nil || repo.Zit == nil {
		return
	}
	for i := range st.Segments {
		sg := &st.Segments[i]
		if i == 0 || sg.State != "open" {
			continue
		}
		tip, _ := repo.Index.BranchTip(sg.Branch)
		under, _ := repo.Index.BranchTip(sg.Target)
		if tip == "" || under == "" {
			continue
		}
		base, err := repo.Zit.MergeBase(ctx, tip, under)
		if err != nil || base == "" {
			continue
		}
		// Built on the rung below when the two last met on that rung's
		// branch — at its tip, or at an earlier check-in of it if it has
		// moved on since. Meeting anywhere else (the strata's target,
		// typically) means this branch never held any of that rung.
		on, err := repo.Index.BranchOf([]string{base})
		if err != nil {
			continue
		}
		if base == under || on[base] == sg.Target {
			sg.Relation = "built-on"
		} else {
			sg.Relation = "after"
		}
	}
}

// stackTarget reads a target that names another landing — "!12", or "!"
// and a hash prefix — as that landing's branch, so a landing can be
// stacked on another by naming it. Anything else is a branch name and
// comes back as it is. A settled landing has no strata to join.
func (d Deps) stackTarget(name string, repo *Repo, to string) (string, error) {
	if !strings.HasPrefix(to, "!") {
		return to, nil
	}
	ref := d.landingRef(name, repo, to)
	if ref == to {
		ref = to[1:]
	}
	if ref == "" {
		return "", fmt.Errorf("name the landing to stack on after the !: %w", ops.ErrInvalid)
	}
	under, found, err := repo.Index.LandingByPrefix(ref)
	if err != nil {
		return "", err
	}
	if !found {
		return "", fmt.Errorf("no landing %s: %w", to, ops.ErrNotFound)
	}
	if under.State != "open" {
		return "", fmt.Errorf("landing %s is %s; stack on an open landing: %w", to, under.State, ops.ErrInvalid)
	}
	return under.Branch, nil
}

// stackHazard refuses aiming branch at to when no walk could land it:
// a loop, where to's strata already stands on branch (through and ok
// are index.LandsOn's reading of to), or two branches that share no
// history, between which no merge exists. Being unrelated by ANCESTRY
// is fine — a branch may land after another it was never built on, and
// its checks test it merged onto that one — so only history is asked.
func stackHazard(ctx context.Context, repo *Repo, branch, to string, through []string, ok bool) error {
	if !ok {
		return fmt.Errorf("%s's strata is a loop already; nothing on it can land: %w", to, ops.ErrInvalid)
	}
	if slices.Contains(through, branch) {
		return fmt.Errorf("%s already lands on %s; aiming %s at it would make a loop: %w", to, branch, branch, ops.ErrInvalid)
	}
	if repo.Zit == nil || repo.Index == nil {
		return nil
	}
	tip, _ := repo.Index.BranchTip(branch)
	onto, _ := repo.Index.BranchTip(to)
	if tip == "" || onto == "" {
		return nil
	}
	if base, err := repo.Zit.MergeBase(ctx, tip, onto); err == nil && base == "" {
		return fmt.Errorf("%s and %s share no history, so %s cannot land on it: %w", branch, to, branch, ops.ErrInvalid)
	}
	return nil
}

// carriesOthers reports whether some open landing by somebody other
// than who stands on l, directly or further up its strata, and so would
// move with it.
func carriesOthers(all []index.LandingSummary, l index.LandingSummary, who string) bool {
	for _, o := range all {
		if o.ID == l.ID || o.State != "open" || o.SupersededBy != "" || o.Author == who {
			continue
		}
		if _, through, _ := index.LandsOn(all, o.Target); slices.Contains(through, l.Branch) {
			return true
		}
	}
	return false
}