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