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}