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}