1package replica
2
3import (
4 "errors"
5 "fmt"
6 "io"
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/filemode"
12 "github.com/go-git/go-git/v5/plumbing/object"
13 "github.com/go-git/go-git/v5/plumbing/storer"
14)
15
16const (
17 FileName = "userbase.age"
18 Message = "userbase update\n"
19 AuthorName = "tug"
20 AuthorEmail = "tug@userbase.invalid"
21)
22
23var (
24 ErrParentMissing = errors.New("replica: parent commit is not in the local repository")
25 ErrNotExport = errors.New("replica: commit does not hold exactly one userbase.age file")
26)
27
28func Author(when time.Time) object.Signature {
29 return object.Signature{Name: AuthorName, Email: AuthorEmail, When: when.UTC().Truncate(time.Second)}
30}
31
32func storeObject(s storer.EncodedObjectStorer, encode func(plumbing.EncodedObject) error) (plumbing.Hash, error) {
33 obj := s.NewEncodedObject()
34 if err := encode(obj); err != nil {
35 return plumbing.ZeroHash, err
36 }
37 return s.SetEncodedObject(obj)
38}
39
40func CommitFile(repo *git.Repository, branch plumbing.ReferenceName, parent plumbing.Hash, content []byte, when time.Time, signer git.Signer) (plumbing.Hash, error) {
41 s := repo.Storer
42 commit := &object.Commit{
43 Author: Author(when),
44 Committer: Author(when),
45 Message: Message,
46 }
47 if !parent.IsZero() {
48 if _, err := object.GetCommit(s, parent); err != nil {
49 return plumbing.ZeroHash, fmt.Errorf("%w: %s: %v", ErrParentMissing, parent, err)
50 }
51 commit.ParentHashes = []plumbing.Hash{parent}
52 }
53 blob, err := storeObject(s, func(o plumbing.EncodedObject) error {
54 o.SetType(plumbing.BlobObject)
55 w, err := o.Writer()
56 if err != nil {
57 return err
58 }
59 if _, err := w.Write(content); err != nil {
60 return err
61 }
62 return w.Close()
63 })
64 if err != nil {
65 return plumbing.ZeroHash, err
66 }
67 tree := &object.Tree{Entries: []object.TreeEntry{{Name: FileName, Mode: filemode.Regular, Hash: blob}}}
68 if commit.TreeHash, err = storeObject(s, tree.Encode); err != nil {
69 return plumbing.ZeroHash, err
70 }
71 payload := &plumbing.MemoryObject{}
72 if err := commit.EncodeWithoutSignature(payload); err != nil {
73 return plumbing.ZeroHash, err
74 }
75 r, err := payload.Reader()
76 if err != nil {
77 return plumbing.ZeroHash, err
78 }
79 sig, err := signer.Sign(r)
80 if err != nil {
81 return plumbing.ZeroHash, fmt.Errorf("replica: sign commit: %w", err)
82 }
83 commit.PGPSignature = string(sig)
84 id, err := storeObject(s, commit.Encode)
85 if err != nil {
86 return plumbing.ZeroHash, err
87 }
88 if err := s.SetReference(plumbing.NewHashReference(branch, id)); err != nil {
89 return plumbing.ZeroHash, err
90 }
91 return id, nil
92}
93
94func ReadFile(objects storer.EncodedObjectStorer, id plumbing.Hash) ([]byte, error) {
95 c, err := object.GetCommit(objects, id)
96 if err != nil {
97 return nil, err
98 }
99 tree, err := c.Tree()
100 if err != nil {
101 return nil, err
102 }
103 if len(tree.Entries) != 1 || tree.Entries[0].Name != FileName || tree.Entries[0].Mode != filemode.Regular {
104 return nil, ErrNotExport
105 }
106 blob, err := object.GetBlob(objects, tree.Entries[0].Hash)
107 if err != nil {
108 return nil, err
109 }
110 rd, err := blob.Reader()
111 if err != nil {
112 return nil, err
113 }
114 defer rd.Close()
115 return io.ReadAll(rd)
116}