1package configsrc
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "os"
9 "path/filepath"
10
11 "github.com/go-git/go-git/v5"
12 "github.com/go-git/go-git/v5/plumbing"
13 "github.com/go-git/go-git/v5/plumbing/object"
14 "github.com/go-git/go-git/v5/plumbing/storer"
15 "github.com/go-git/go-git/v5/plumbing/transport"
16
17 "tug/internal/platform/replica"
18)
19
20type view struct {
21 kFound bool
22 hFound bool
23 damage error
24}
25
26func inspect(st storer.EncodedObjectStorer, k, h plumbing.Hash) view {
27 var v view
28 for _, id := range []plumbing.Hash{k, h} {
29 if id.IsZero() {
30 continue
31 }
32 if _, err := readConfig(st, id); err != nil && !errors.Is(err, errNoConfig) {
33 v.damage = err
34 return v
35 }
36 }
37 if h.IsZero() {
38 return v
39 }
40 if v.kFound, v.damage = walk(st, h, k); v.damage != nil || k.IsZero() || v.kFound {
41 return v
42 }
43 v.hFound, v.damage = walk(st, k, h)
44 return v
45}
46
47func walk(st storer.EncodedObjectStorer, from, target plumbing.Hash) (bool, error) {
48 c, err := object.GetCommit(st, from)
49 if err != nil {
50 return false, err
51 }
52 found := false
53 err = object.NewCommitPreorderIter(c, nil, nil).ForEach(func(c *object.Commit) error {
54 if c.Hash == target {
55 found = true
56 }
57 return nil
58 })
59 return found, err
60}
61
62func (s *Source) repair(ctx context.Context, url string, auth transport.AuthMethod, branch plumbing.ReferenceName, k plumbing.Hash) (plumbing.Hash, *refusal, error) {
63 beforeRepair()
64 scratch := s.path(scratchName)
65 if err := os.RemoveAll(scratch); err != nil {
66 return plumbing.ZeroHash, nil, err
67 }
68 defer os.RemoveAll(scratch)
69 r, err := git.PlainInit(scratch, true)
70 if err != nil {
71 return plumbing.ZeroHash, nil, err
72 }
73 head, err := replica.Fetch(ctx, r, url, auth, branch)
74 if err != nil {
75 return plumbing.ZeroHash, &refusal{KindUnreachable, err}, nil
76 }
77 v := inspect(r.Storer, k, head)
78 if !k.IsZero() && !v.kFound {
79 cause := errMissing
80 if v.damage != nil {
81 cause = fmt.Errorf("%w: %w", errMissing, v.damage)
82 }
83 return head, &refusal{KindCheckpointMissing, cause}, nil
84 }
85 if v.damage != nil {
86 return head, &refusal{KindUnreachable, v.damage}, nil
87 }
88 if !k.IsZero() {
89 if err := writeCheckpointRef(scratch, k); err != nil {
90 return plumbing.ZeroHash, nil, err
91 }
92 }
93 if err := syncTree(scratch); err != nil {
94 return plumbing.ZeroHash, nil, err
95 }
96 if err := s.swap(scratch); err != nil {
97 return plumbing.ZeroHash, nil, err
98 }
99 return head, nil, nil
100}
101
102func (s *Source) swap(scratch string) error {
103 repo, aside := s.path(repoName), s.path(asideName)
104 if err := os.RemoveAll(aside); err != nil {
105 return err
106 }
107 if err := os.Rename(repo, aside); err != nil && !errors.Is(err, fs.ErrNotExist) {
108 return err
109 }
110 if err := os.Rename(scratch, repo); err != nil {
111 if rerr := os.Rename(aside, repo); rerr != nil && !errors.Is(rerr, fs.ErrNotExist) {
112 return errors.Join(err, rerr)
113 }
114 return err
115 }
116 if err := syncPath(s.dir); err != nil {
117 return err
118 }
119 return os.RemoveAll(aside)
120}
121
122func (s *Source) recover() error {
123 if err := os.RemoveAll(s.path(scratchName)); err != nil {
124 return err
125 }
126 aside := s.path(asideName)
127 if _, err := os.Lstat(aside); errors.Is(err, fs.ErrNotExist) {
128 return nil
129 } else if err != nil {
130 return err
131 }
132 _, err := os.Lstat(s.path(repoName))
133 switch {
134 case errors.Is(err, fs.ErrNotExist):
135 if err := os.Rename(aside, s.path(repoName)); err != nil {
136 return err
137 }
138 return syncPath(s.dir)
139 case err != nil:
140 return err
141 }
142 return os.RemoveAll(aside)
143}
144
145func writeCheckpointRef(repo string, h plumbing.Hash) error {
146 dir := filepath.Join(repo, "refs", "tug")
147 if err := os.MkdirAll(dir, 0o755); err != nil {
148 return err
149 }
150 if err := replaceFile(filepath.Join(repo, "tug-checkpoint.tmp"), filepath.Join(dir, "checkpoint"), []byte(h.String()+"\n")); err != nil {
151 return err
152 }
153 return syncPath(filepath.Dir(dir))
154}
155
156func replaceFile(tmp, p string, data []byte) error {
157 if err := os.Remove(tmp); err != nil && !errors.Is(err, fs.ErrNotExist) {
158 return err
159 }
160 f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
161 if err != nil {
162 return err
163 }
164 _, werr := f.Write(data)
165 if err := errors.Join(werr, f.Chmod(0o600), f.Close()); err != nil {
166 _ = os.Remove(tmp)
167 return err
168 }
169 if err := syncPath(tmp); err != nil {
170 _ = os.Remove(tmp)
171 return err
172 }
173 if err := os.Rename(tmp, p); err != nil {
174 _ = os.Remove(tmp)
175 return err
176 }
177 return syncPath(filepath.Dir(p))
178}
179
180func syncTree(root string) error {
181 return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
182 if err != nil {
183 return err
184 }
185 if d.IsDir() || d.Type().IsRegular() {
186 return syncPath(p)
187 }
188 return nil
189 })
190}
191
192func fsync(p string) error {
193 f, err := os.Open(p)
194 if err != nil {
195 return err
196 }
197 defer f.Close()
198 return f.Sync()
199}