Skip to content
quarry Artifact [abeadd3a40]
Login

Artifact [abeadd3a40]

Artifact abeadd3a40eae97a1ca4b1405701bac0fb8d91b504a1fddd89427b097e5b4d33:


package api

import (
	"context"
	"fmt"
	"net/url"
	pathpkg "path"
	"strings"

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

// FossilMirrorStore keeps the setting of a repository's mirror to a
// Fossil server (RFC 0022, Fossil remote). The store owns every rule
// that decides whether a mirror may exist (the address, the login,
// private, takedown), so the ops here only map its refusals. The
// password goes in and never comes back out.
type FossilMirrorStore interface {
	FossilMirrorOf(repo string) (oltp.FossilMirror, bool, error)
	SetFossilMirror(repo, remote, user, password, by, via string) (oltp.FossilMirror, error)
	DisableFossilMirror(repo, by, via string) (bool, error)
}

type fossilMirrorIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
	// Remote is the https sync address of a Fossil server's copy of
	// the repository, such as https://fossil.example.org/repo.
	Remote string `json:"remote" validate:"max=300"`
	// User is the login on that server the push authenticates as; it
	// needs the capability to push.
	User string `json:"user" validate:"max=64"`
	// Password is that login's password. Required when the mirror is
	// first set and when the remote or the user changes; empty keeps
	// the stored one.
	Password string `json:"password" validate:"max=200"`
}

type fossilMirrorRepoIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
}

type fossilMirrorOut struct {
	Repo string `json:"repo"`
	On   bool   `json:"on"`
	// Remote and User are where and as whom the repository is pushed.
	Remote string `json:"remote,omitempty"`
	User   string `json:"user,omitempty"`
	// Pushed is trunk's tip at the last push the server accepted, and
	// when.
	Pushed   string `json:"pushed,omitempty"`
	PushedAt string `json:"pushed_at,omitempty"`
	// Error is the last failure in words, and when.
	Error   string `json:"error,omitempty"`
	ErrorAt string `json:"error_at,omitempty"`
	// Paused says why the mirror is on but not pushing: the repository
	// is private or taken down. Nothing already pushed is withdrawn.
	Paused string `json:"paused,omitempty"`
}

func fossilMirrorView(m oltp.FossilMirror, on bool) fossilMirrorOut {
	if !on {
		return fossilMirrorOut{Repo: m.Repo}
	}
	return fossilMirrorOut{Repo: m.Repo, On: true, Remote: m.Remote, User: m.User,
		Pushed: m.Pushed, PushedAt: m.PushedAt, Error: m.Err, ErrorAt: m.ErrAt}
}

func (d Deps) registerFossilMirror(r *ops.Registry) error {
	store := func(ctx context.Context, repo string) error {
		if _, err := d.repoFor(ctx, repo); err != nil {
			return err
		}
		if d.FossilMirrors == nil {
			return fmt.Errorf("this forge keeps no mirror settings: %w", ops.ErrUnavailable)
		}
		return nil
	}
	if err := ops.Register(r, ops.Op[fossilMirrorRepoIn, fossilMirrorOut]{
		Name: "repo.fossilmirror.get",
		Caps: "a",
		Doc: "A repository's mirror to a Fossil server (RFC 0022, Fossil remote): the remote, the login, " +
			"trunk's tip at the last push and the last error. The password is never returned.",
		Kind:  ops.Query,
		Auth:  ops.Token,
		Route: "GET /api/v1/r/{repo}/fossil-mirror",
		Run: func(ctx context.Context, in fossilMirrorRepoIn) (fossilMirrorOut, error) {
			if err := store(ctx, in.Repo); err != nil {
				return fossilMirrorOut{}, err
			}
			m, on, err := d.FossilMirrors.FossilMirrorOf(in.Repo)
			if err != nil {
				return fossilMirrorOut{}, err
			}
			m.Repo = in.Repo
			out := fossilMirrorView(m, on)
			if on {
				out.Paused = d.gitMirrorPaused(in.Repo)
			}
			return out, nil
		},
	}); err != nil {
		return err
	}
	if err := ops.Register(r, ops.Op[fossilMirrorIn, fossilMirrorOut]{
		Name: "repo.fossilmirror.set",
		Caps: "a",
		Doc: "Mirror a public repository to a Fossil server, or change where it is pushed. `remote` is the " +
			"https sync address of a copy of this repository on that server (it must share this repository's " +
			"project code, as a clone does), `user` a login there that may push, `password` its password: " +
			"required when the mirror is first set or its remote or user changes, otherwise empty keeps the " +
			"stored one. Everything Fossil sync carries is pushed (check-ins, wiki, tickets, forum, every " +
			"branch), and nothing on the server is ever discarded. Refused for a private repository, while a " +
			"takedown is in force, for a private or loopback host unless the forge permits them, and for the " +
			"Fossil this repository is itself mirrored from.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/r/{repo}/fossil-mirror",
		Run: func(ctx context.Context, in fossilMirrorIn) (fossilMirrorOut, error) {
			if err := store(ctx, in.Repo); err != nil {
				return fossilMirrorOut{}, err
			}
			in.Remote = strings.TrimSpace(in.Remote)
			// Syntax first, so the host below is one the store would
			// keep, and so a refusal names the address, not the lookup.
			if err := oltp.CheckFossilRemote(in.Remote); err != nil {
				return fossilMirrorOut{}, gitMirrorErr(err)
			}
			if d.FossilMirrorHost != nil {
				if u, err := url.Parse(in.Remote); err == nil {
					if err := d.FossilMirrorHost(ctx, u.Hostname()); err != nil {
						return fossilMirrorOut{}, fmt.Errorf("%v: %w", err, ops.ErrInvalid)
					}
				}
			}
			// One direction per repository: pushing it back to the
			// Fossil it follows would make two sources of record.
			if d.Mirrors != nil {
				if up, ok, err := d.Mirrors.MirrorFor(in.Repo); err == nil && ok && up.Enabled && !up.Git &&
					sameFossilRemote(in.Remote, up.URL) {
					return fossilMirrorOut{}, fmt.Errorf("%s follows %s; mirror to a different Fossil: %w",
						in.Repo, up.URL, ops.ErrInvalid)
				}
			}
			m, err := d.FossilMirrors.SetFossilMirror(in.Repo, in.Remote, in.User, in.Password, ops.User(ctx), ops.Via(ctx))
			if err != nil {
				return fossilMirrorOut{}, gitMirrorErr(err)
			}
			if d.FossilMirrorChanged != nil {
				d.FossilMirrorChanged(in.Repo)
			}
			return fossilMirrorView(m, true), nil
		},
	}); err != nil {
		return err
	}
	return ops.Register(r, ops.Op[fossilMirrorRepoIn, fossilMirrorOut]{
		Name: "repo.fossilmirror.off",
		Caps: "a",
		Doc: "Stop mirroring a repository to its Fossil server and discard the stored password. Nothing " +
			"already pushed is withdrawn.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/r/{repo}/fossil-mirror/off",
		Run: func(ctx context.Context, in fossilMirrorRepoIn) (fossilMirrorOut, error) {
			if err := store(ctx, in.Repo); err != nil {
				return fossilMirrorOut{}, err
			}
			if _, err := d.FossilMirrors.DisableFossilMirror(in.Repo, ops.User(ctx), ops.Via(ctx)); err != nil {
				return fossilMirrorOut{}, err
			}
			return fossilMirrorOut{Repo: in.Repo}, nil
		},
	})
}

// fossilMirrorAbout is the About line's host path and web address for a
// Fossil mirror that is on and current; empty otherwise. One whose last
// push failed, or that is paused, is not claimed.
func (d Deps) fossilMirrorAbout(repo string) (label, link string) {
	if d.FossilMirrors == nil {
		return "", ""
	}
	m, on, err := d.FossilMirrors.FossilMirrorOf(repo)
	if err != nil || !on || m.PushedAt == "" || m.Err != "" || d.gitMirrorPaused(repo) != "" {
		return "", ""
	}
	host, path := fossilPlace(m.Remote)
	if host == "" {
		return "", ""
	}
	label = host
	if path != "" {
		label += "/" + path
	}
	return label, "https://" + label
}

// fossilPlace is a remote's host and path in the one spelling that
// compares: the host lower-cased and without the port its scheme
// implies, the path percent-decoded, cleaned, without slashes at either
// end and without a trailing /xfer (the sync endpoint of the same
// repository).
func fossilPlace(remote string) (host, path string) {
	u, err := url.Parse(strings.TrimSpace(remote))
	if err != nil || u.Hostname() == "" {
		return "", ""
	}
	host = strings.ToLower(u.Hostname())
	if p := u.Port(); p != "" && !(p == "443" && u.Scheme == "https") && !(p == "80" && u.Scheme == "http") {
		host += ":" + p
	}
	path = strings.Trim(pathpkg.Clean("/"+u.Path), "/")
	if path == "xfer" {
		path = ""
	}
	path = strings.TrimSuffix(path, "/xfer")
	return host, path
}

// sameFossilRemote reports whether two spellings name one Fossil
// repository: the scheme, a default port, case in the host and slashes
// at either end of the path do not make a different place.
func sameFossilRemote(a, b string) bool {
	ha, pa := fossilPlace(a)
	hb, pb := fossilPlace(b)
	return ha != "" && ha == hb && pa == pb
}