Raw
  1package replica
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"os"
  8	"path/filepath"
  9
 10	"github.com/go-git/go-git/v5"
 11	"github.com/go-git/go-git/v5/plumbing"
 12	"github.com/go-git/go-git/v5/plumbing/object"
 13	"github.com/go-git/go-git/v5/storage/filesystem"
 14)
 15
 16const scratchName = "userbase-scratch.git"
 17
 18var errNoScratchDir = errors.New("replica: no directory for scratch repositories")
 19
 20func scratch(dir string) (*git.Repository, func(), error) {
 21	if dir == "" {
 22		return nil, nil, errNoScratchDir
 23	}
 24	p := filepath.Join(dir, scratchName)
 25	if err := os.RemoveAll(p); err != nil {
 26		return nil, nil, err
 27	}
 28	r, err := git.PlainInit(p, true)
 29	if err != nil {
 30		_ = os.RemoveAll(p)
 31		return nil, nil, err
 32	}
 33	return r, func() { _ = os.RemoveAll(p) }, nil
 34}
 35
 36func checkpointMissing(repo *git.Repository, id plumbing.Hash) (bool, error) {
 37	missing := func(err error) (bool, error) {
 38		if errors.Is(err, plumbing.ErrObjectNotFound) {
 39			return true, nil
 40		}
 41		return false, err
 42	}
 43	c, err := object.GetCommit(repo.Storer, id)
 44	if err != nil {
 45		return missing(err)
 46	}
 47	tree, err := c.Tree()
 48	if err != nil {
 49		return missing(err)
 50	}
 51	for _, e := range tree.Entries {
 52		if _, err := repo.Storer.EncodedObject(plumbing.AnyObject, e.Hash); err != nil {
 53			return missing(err)
 54		}
 55	}
 56	return false, nil
 57}
 58
 59func ensureCheckpoint(ctx context.Context, repo *git.Repository, dir string, primary Remote, k Checkpoint) error {
 60	if !k.Set {
 61		return nil
 62	}
 63	id := plumbing.NewHash(k.Value)
 64	missing, err := checkpointMissing(repo, id)
 65	if err != nil || !missing {
 66		return err
 67	}
 68	return heal(ctx, repo, dir, primary, id)
 69}
 70
 71func heal(ctx context.Context, repo *git.Repository, dir string, primary Remote, k plumbing.Hash) error {
 72	s, done, err := scratch(dir)
 73	if err != nil {
 74		return err
 75	}
 76	defer done()
 77	head, err := FetchHistory(ctx, s, primary.URL, primary.Auth, Branch)
 78	if err != nil {
 79		return err
 80	}
 81	if !onBranch(s, k, head) {
 82		return refused(ErrCheckpointMissing, "checkpoint %s, remote %s", k, head)
 83	}
 84	needed, err := content(s, k)
 85	if err != nil {
 86		return err
 87	}
 88	ancestors, err := ancestry(s, k)
 89	if err != nil {
 90		return err
 91	}
 92	for _, id := range ancestors {
 93		if _, err := repo.Storer.EncodedObject(plumbing.CommitObject, id); err != nil {
 94			needed = append(needed, id)
 95		}
 96	}
 97	return install(repo, s, needed)
 98}
 99
100func onBranch(r *git.Repository, id, head plumbing.Hash) bool {
101	if head.IsZero() {
102		return false
103	}
104	c, err := object.GetCommit(r.Storer, id)
105	if err != nil {
106		return false
107	}
108	if id == head {
109		return true
110	}
111	hc, err := object.GetCommit(r.Storer, head)
112	if err != nil {
113		return false
114	}
115	ok, err := c.IsAncestor(hc)
116	return err == nil && ok
117}
118
119func content(r *git.Repository, tip plumbing.Hash) ([]plumbing.Hash, error) {
120	c, err := object.GetCommit(r.Storer, tip)
121	if err != nil {
122		return nil, err
123	}
124	tree, err := c.Tree()
125	if err != nil {
126		return nil, err
127	}
128	if len(tree.Entries) != 1 {
129		return nil, ErrNotExport
130	}
131	return []plumbing.Hash{tip, tree.Hash, tree.Entries[0].Hash}, nil
132}
133
134func ancestry(r *git.Repository, tip plumbing.Hash) ([]plumbing.Hash, error) {
135	c, err := object.GetCommit(r.Storer, tip)
136	if err != nil {
137		return nil, err
138	}
139	var ids []plumbing.Hash
140	seen := map[plumbing.Hash]bool{tip: true}
141	stack := append([]plumbing.Hash{}, c.ParentHashes...)
142	for len(stack) > 0 {
143		id := stack[len(stack)-1]
144		stack = stack[:len(stack)-1]
145		if seen[id] {
146			continue
147		}
148		seen[id] = true
149		p, err := object.GetCommit(r.Storer, id)
150		if err != nil {
151			return nil, fmt.Errorf("replica: commit %s below %s: %w", id, tip, err)
152		}
153		ids = append(ids, id)
154		stack = append(stack, p.ParentHashes...)
155	}
156	return ids, nil
157}
158
159func objectsDir(repo *git.Repository) string {
160	s, ok := repo.Storer.(*filesystem.Storage)
161	if !ok {
162		return ""
163	}
164	return filepath.Join(s.Filesystem().Root(), "objects")
165}
166
167func install(dst, src *git.Repository, ids []plumbing.Hash) error {
168	objects := objectsDir(dst)
169	dirs := map[string]bool{}
170	for _, id := range ids {
171		obj, err := src.Storer.EncodedObject(plumbing.AnyObject, id)
172		if err != nil {
173			return err
174		}
175		if _, err := dst.Storer.SetEncodedObject(obj); err != nil {
176			return err
177		}
178		if objects == "" {
179			continue
180		}
181		p := loosePath(objects, id)
182		if err := syncPath(p); err != nil {
183			return err
184		}
185		dirs[filepath.Dir(p)] = true
186	}
187	if objects == "" {
188		return nil
189	}
190	for d := range dirs {
191		if err := syncPath(d); err != nil {
192			return err
193		}
194	}
195	return syncPath(objects)
196}