Skip to content
quarry Artifact [3966e49480]
Login

Artifact [3966e49480]

Artifact 3966e49480905fd452101290e1f917e8b352d205a9e9cb19940b67bf341095b4:


package api

import (
	"context"
	"fmt"
	"log"

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

// The merge view belongs wherever two branches meet (#351), not only on
// a landing: the merge page shows any branch onto any target, and the
// branches list says how each open branch stands against trunk. These
// are the pair-keyed ops; landing.conflicts and landing.resolve are the
// same calls with the pair read off the landing.

type mergePairIn struct {
	Repo   string `json:"repo" validate:"required,max=64"`
	Branch string `json:"branch" validate:"required,max=200"`
	// Onto is the target; empty is the repository's default branch.
	Onto string `json:"onto" validate:"max=200"`
}

type mergeResolveIn struct {
	Repo      string        `json:"repo" validate:"required,max=64"`
	Branch    string        `json:"branch" validate:"required,max=200"`
	Onto      string        `json:"onto" validate:"max=200"`
	BranchTip string        `json:"branchTip" validate:"required,max=64"`
	TargetTip string        `json:"targetTip" validate:"required,max=64"`
	Message   string        `json:"message" validate:"max=4096"`
	Files     []resolveFile `json:"files" validate:"max=50"`
}

// pairOf resolves the pair a merge op names: the repository, the
// branch, and the target, defaulting to the repository's default
// branch. A branch the index has never seen is not found, so a typo
// does not read as a clean merge.
func (d Deps) pairOf(ctx context.Context, repoName, branch, onto string) (*Repo, string, error) {
	repo, err := d.repoFor(ctx, repoName)
	if err != nil {
		return nil, "", err
	}
	if onto == "" {
		onto, _ = defaultBranchOf(repo)
	}
	for _, b := range []string{branch, onto} {
		if branchTipOf(repo, b) == "" {
			return nil, "", fmt.Errorf("no branch %q here: %w", b, ops.ErrNotFound)
		}
	}
	if branch == onto {
		return nil, "", fmt.Errorf("a branch does not merge onto itself: %w", ops.ErrInvalid)
	}
	return repo, onto, nil
}

func (d Deps) registerMergePair(r *ops.Registry) error {
	if err := ops.Register(r, ops.Op[mergePairIn, landingConflictsOut]{
		Name: "merge.conflicts",
		Caps: "o",
		Doc: "A branch's merge onto a target, file by file and region by region, as landing.conflicts gives it for a landing. " +
			"`onto` defaults to the repository's default branch. The verdict is clean, up-to-date, unrelated or conflict; only a conflict carries files.",
		Kind:  ops.Query,
		Auth:  ops.Anonymous,
		Route: "GET /api/v1/r/{repo}/merge",
		Run: func(ctx context.Context, in mergePairIn) (landingConflictsOut, error) {
			repo, onto, err := d.pairOf(ctx, in.Repo, in.Branch, in.Onto)
			if err != nil {
				return landingConflictsOut{}, err
			}
			return d.pairConflicts(ctx, in.Repo, repo, in.Branch, onto)
		},
	}); err != nil {
		return err
	}
	return ops.Register(r, ops.Op[mergeResolveIn, landingResolveOut]{
		Name: "merge.resolve",
		Caps: "i",
		Doc: "Commit a resolution of a branch onto a target, as landing.resolve does for a landing: the merge of the target into the branch, on the branch, as the caller. " +
			"Answers and refusals are landing.resolve's.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/r/{repo}/merge/resolve",
		Run: func(ctx context.Context, in mergeResolveIn) (landingResolveOut, error) {
			if err := d.refuseUnlessWritable(in.Repo); err != nil {
				return landingResolveOut{}, err
			}
			repo, onto, err := d.pairOf(ctx, in.Repo, in.Branch, in.Onto)
			if err != nil {
				return landingResolveOut{}, err
			}
			return d.pairResolve(ctx, in.Repo, repo, in.Branch, onto, resolveAnswers{BranchTip: in.BranchTip,
				TargetTip: in.TargetTip, Message: in.Message, Files: in.Files}, "the merge page")
		},
	})
}

// branchMerge is how a branch stands against the default branch, from
// the probe cache: clean, conflict (with how many files), up-to-date,
// unrelated, or empty when nothing has been asked yet. Behind is how
// many of the target's check-ins the branch has not seen.
type branchMerge struct {
	Onto   string `json:"onto"`
	State  string `json:"state,omitempty"`
	Files  int    `json:"files,omitempty"`
	Behind int    `json:"behind,omitempty"`
}

// withMergeStanding fills each open branch's standing against the
// default branch. It never runs a merge: a list of branches would
// otherwise cost a merge each. WarmBranchProbes fills the cache.
func (d Deps) withMergeStanding(repo *Repo, rows []branchRow) {
	if repo == nil || repo.Zit == nil || repo.Index == nil {
		return
	}
	onto, ontoTip := defaultBranchOf(repo)
	if onto == "" || ontoTip == "" {
		return
	}
	for i := range rows {
		b := &rows[i]
		if b.Closed || b.Name == onto || b.Tip == "" {
			continue
		}
		m := &branchMerge{Onto: onto}
		if state, files, ok := repo.Zit.MergeProbeCached(b.Tip + ":" + ontoTip); ok {
			m.State, m.Files = state, len(files)
		}
		if n, err := repo.Index.Behind(b.Name, onto); err == nil {
			m.Behind = n
		}
		b.Merge = m
	}
}

// warmBranchLimit bounds the branches probed per repository per round:
// the newest open ones, which are the ones a reader looks at.
const warmBranchLimit = 30

// WarmBranchProbes asks zit how each recently active open branch merges
// onto its repository's default branch, for any pair of tips not yet
// asked about, so the branches list can show it without waiting. Run
// from the same ticker as WarmMergeProbes. Returns how many were asked.
func (d Deps) WarmBranchProbes() int {
	if d.Repos == nil {
		return 0
	}
	n := 0
	for _, name := range d.Repos.Names() {
		repo, ok := d.Repos.Get(name)
		if !ok || repo.Index == nil || repo.Zit == nil {
			continue
		}
		onto, ontoTip := defaultBranchOf(repo)
		if onto == "" || ontoTip == "" {
			continue
		}
		bs, err := repo.Index.BranchesLimited(warmBranchLimit, true)
		if err != nil {
			continue
		}
		for _, b := range bs {
			if b.Closed || b.Name == onto || b.Tip == "" {
				continue
			}
			key := b.Tip + ":" + ontoTip
			if _, _, ok := repo.Zit.MergeProbeCached(key); ok {
				continue
			}
			if _, _, err := repo.Zit.MergeProbe(b.Tip, ontoTip, key); err != nil {
				log.Printf("merge: probe %s/%s onto %s: %v", name, b.Name, onto, err)
				continue
			}
			n++
		}
	}
	return n
}