Skip to content
quarry Artifact [d39fe3d6b8]
Login

Artifact [d39fe3d6b8]

Artifact d39fe3d6b8e626eb6485cef5efe36080a12c607f6263a7495c597452cfd60728:


package api

import (
	"context"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"

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

// RefusalMergeConflict is a pair archive's answer when the pair does not
// merge: there is no tree to test, and the conflicting paths are the
// verdict (RFC 0009 §premerge).
const RefusalMergeConflict = "merge_conflict"

// parsePair reads "pair:<branchTip>:<targetTip>". Both tips must be full
// check-in hashes: a pair names two fixed trees, and a name that could
// move would make the cached archive a lie.
func parsePair(v string) (branchTip, targetTip string, ok bool) {
	rest, ok := strings.CutPrefix(v, "pair:")
	if !ok {
		return "", "", false
	}
	branchTip, targetTip, ok = strings.Cut(rest, ":")
	if !ok || !fullHash(branchTip) || !fullHash(targetTip) {
		return "", "", false
	}
	return branchTip, targetTip, true
}

// pairArchive is archive.get for a premerge pair: the tree accepting
// branchTip onto targetTip would write, built from this forge's own
// store and never written to it. The runner's archive checkout fetches
// it exactly as it fetches a check-in, so a pair run needs no zit and no
// clone on the worker. A pair never changes, so its archive is cached
// like a check-in's.
func (d Deps) pairArchive(ctx context.Context, repo *Repo, in archiveIn, format string) (archiveFile, error) {
	branchTip, targetTip, ok := parsePair(in.Version)
	if !ok {
		return archiveFile{}, fmt.Errorf("a pair is pair:<branch tip>:<target tip>, both full check-in hashes: %w", ops.ErrInvalid)
	}
	if format != "tar" {
		return archiveFile{}, fmt.Errorf("a pair's tree is served as tar only: %w", ops.ErrInvalid)
	}
	name := in.Repo + "-pair-" + branchTip[:12] + "-" + targetTip[:12]
	lane, laneName := d.pairLaneFor(ctx, repo, branchTip, targetTip)
	build := func(into string) func(dst string) error {
		return func(dst string) error {
			return lane.DoNamed(ctx, "archive", in.Repo, func() error {
				return d.Bulk.DoNamed(ctx, "archive", in.Repo, func() error {
					start := time.Now()
					if err := repo.mergeArchiveTo(ctx, branchTip, targetTip, name, dst, maxArchiveBytes); err != nil {
						return err
					}
					logBuild(in.Repo, in.Version, format, laneName, into, dst, start)
					return nil
				})
			})
		}
	}
	produce := func() (string, bool, error) {
		var path string
		var err error
		temp := d.Archives == nil
		if temp {
			path, err = tempArchive(build("temp"))
		} else {
			path, err = d.Archives.Get(archiveKey(in.Repo, d.ZitVersion, in.Version, format), build("disk"))
		}
		var mc *zitcli.MergeConflict
		switch {
		case errors.As(err, &mc):
			return "", false, &ops.Refusal{Code: RefusalMergeConflict, Message: mc.Error(),
				Fields: map[string]string{"state": mc.State, "paths": strings.Join(mc.Paths, "\n")}}
		case errors.Is(err, work.ErrBusy):
			return "", false, fmt.Errorf("archive queue is full, retry shortly (%w): %w", work.ErrBusy, ops.ErrUnavailable)
		case errors.Is(err, zitcli.ErrNoVersion):
			return "", false, fmt.Errorf("no such check-in in pair %s: %w", in.Version, ops.ErrNotFound)
		case err != nil:
			log.Printf("archive %s@%s: %v", in.Repo, in.Version, err)
			return "", false, fmt.Errorf("%w: %v", ops.ErrUnavailable, err)
		}
		return path, temp, nil
	}
	return archiveFile{name: name + ".tar.gz", ctype: "application/gzip", version: in.Version, format: format,
		requested: in.Version, build: produce}, nil
}