Raw
  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}