Raw
  1package replica
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"time"
  8
  9	"github.com/go-git/go-git/v5"
 10	"github.com/go-git/go-git/v5/plumbing"
 11	"github.com/go-git/go-git/v5/plumbing/object"
 12	"github.com/go-git/go-git/v5/plumbing/storer"
 13	"golang.org/x/crypto/ssh"
 14)
 15
 16type RestoreInput struct {
 17	Repo   *git.Repository
 18	Dir    string
 19	Remote Remote
 20	Commit plumbing.Hash
 21	Keys   []ssh.PublicKey
 22	DryRun bool
 23	Import func(ctx context.Context, blob []byte, restored, checkpoint plumbing.Hash) error
 24}
 25
 26type RestoreResult struct {
 27	Head, Commit plumbing.Hash
 28	Blob         []byte
 29	HeadBlob     []byte
 30	When         time.Time
 31}
 32
 33var (
 34	ErrNothingToRestore = errors.New("replica: the remote branch is empty")
 35	ErrNotOnBranch      = errors.New("replica: the chosen commit is not on the remote branch")
 36	ErrChainUnverified  = errors.New("replica: a commit between the root and the remote head is not signed by an allowed key")
 37	ErrMergeCommit      = errors.New("replica: the history holds a merge commit")
 38	ErrNoAllowedKeys    = errors.New("replica: no allowed signing key")
 39	errNoImport         = errors.New("replica: a restore that is not a dry run needs an import")
 40)
 41
 42func Restore(ctx context.Context, in RestoreInput) (RestoreResult, error) {
 43	if len(in.Keys) == 0 {
 44		return RestoreResult{}, ErrNoAllowedKeys
 45	}
 46	if !in.DryRun && (in.Import == nil || in.Repo == nil) {
 47		return RestoreResult{}, errNoImport
 48	}
 49	s, done, err := scratch(in.Dir)
 50	if err != nil {
 51		return RestoreResult{}, err
 52	}
 53	defer done()
 54	head, err := FetchHistory(ctx, s, in.Remote.URL, in.Remote.Auth, Branch)
 55	if err != nil {
 56		return RestoreResult{}, err
 57	}
 58	if head.IsZero() {
 59		return RestoreResult{}, ErrNothingToRestore
 60	}
 61	chosen := in.Commit
 62	if chosen.IsZero() {
 63		chosen = head
 64	}
 65	if !onBranch(s, chosen, head) {
 66		return RestoreResult{}, fmt.Errorf("%w: %s", ErrNotOnBranch, chosen)
 67	}
 68	if err := verifyChain(s.Storer, head, in.Keys); err != nil {
 69		return RestoreResult{}, err
 70	}
 71	c, err := object.GetCommit(s.Storer, chosen)
 72	if err != nil {
 73		return RestoreResult{}, err
 74	}
 75	blob, err := ReadFile(s.Storer, chosen)
 76	if err != nil {
 77		return RestoreResult{}, err
 78	}
 79	res := RestoreResult{Head: head, Commit: chosen, Blob: blob, HeadBlob: blob, When: c.Committer.When.UTC()}
 80	if chosen != head {
 81		if res.HeadBlob, err = ReadFile(s.Storer, head); err != nil {
 82			return RestoreResult{}, err
 83		}
 84	}
 85	if in.DryRun {
 86		return res, nil
 87	}
 88	set, err := content(s, head)
 89	if err != nil {
 90		return RestoreResult{}, err
 91	}
 92	ancestors, err := ancestry(s, head)
 93	if err != nil {
 94		return RestoreResult{}, err
 95	}
 96	if err := install(in.Repo, s, append(set, ancestors...)); err != nil {
 97		return RestoreResult{}, err
 98	}
 99	if err := in.Import(ctx, blob, chosen, head); err != nil {
100		return RestoreResult{}, err
101	}
102	if err := in.Repo.Storer.SetReference(plumbing.NewHashReference(Branch, head)); err != nil {
103		return RestoreResult{}, err
104	}
105	if err := in.Repo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, Branch)); err != nil {
106		return RestoreResult{}, err
107	}
108	return res, nil
109}
110
111func verifyChain(objects storer.EncodedObjectStorer, id plumbing.Hash, keys []ssh.PublicKey) error {
112	for {
113		c, err := verifyAny(objects, id, keys)
114		if err != nil {
115			return err
116		}
117		switch len(c.ParentHashes) {
118		case 0:
119			return nil
120		case 1:
121			id = c.ParentHashes[0]
122		default:
123			return fmt.Errorf("%w: %s", ErrMergeCommit, id)
124		}
125	}
126}
127
128func verifyAny(objects storer.EncodedObjectStorer, id plumbing.Hash, keys []ssh.PublicKey) (*object.Commit, error) {
129	var first error
130	for _, k := range keys {
131		c, err := VerifyCommit(objects, id, k)
132		if err == nil {
133			return c, nil
134		}
135		if first == nil {
136			first = err
137		}
138	}
139	return nil, fmt.Errorf("%w: commit %s: %w", ErrChainUnverified, id, signatureKind(first))
140}