Skip to content
quarry Artifact [25d7f89ca5]
Login

Artifact [25d7f89ca5]

Artifact 25d7f89ca55774d9a3f4ee77a45a646c32df8d18b1d4c3339f4c2b7bd3e9641e:


package api

import (
	"context"
	"fmt"
	"strings"

	"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 target is still checked at accept time, where it
// belongs; this is the narrower question of who may aim the record.
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)
			}
			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() error {
				_, werr := repo.Zit.LandingRetarget(l.ID, author(ctx), to)
				return werr
			}); err != nil {
				return landingRetargetOut{}, err
			}
			return landingRetargetOut{ID: l.ID, Target: to}, nil
		},
	})
}