Skip to content
quarry Artifact [9fdd820eb1]
Login

Artifact [9fdd820eb1]

Artifact 9fdd820eb12b601e6b9abe01da07fdaf1a6f0721dd21f5f02aadcc514d9b8f33:


package api

import (
	"context"
	"fmt"
	"strings"

	"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. The recovery for a stack whose base landed on its own.",
		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
			}
			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)
			}
			// WHO may move it: its author, or anyone who could accept
			// it — the same `i` this op already takes, admitted to the
			// record's target by the branch's protection, exactly as
			// accept checks it. dmestas ruled it so (ae900759): the
			// people who own the destination decide where things land,
			// and a rung stuck on a rejected base should be movable by
			// the maintainer who would land it, not only by an author
			// who may be gone. Retargeting your own record is part of
			// proposing; moving somebody else's is the acceptor's act.
			// It used to take the moderation letters instead, which
			// made a forum moderator able to aim a merge and a branch
			// owner unable to.
			//
			// 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.
			who := author(ctx)
			mayAccept := d.Protect == nil || d.Protect.MayJoin(in.Repo, who, l.Target)
			if who == "" || (who != l.Author && !mayAccept) {
				return landingRetargetOut{}, fmt.Errorf(
					"%w: %.12s was opened by %s; moving somebody else's record is for whoever may accept it onto %s",
					ops.ErrForbidden, l.ID, landingAuthorName(l.Author), l.Target)
			}
			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 == 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 := 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
}