package configsrc import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/plumbing/transport" "tug/internal/platform/replica" ) type view struct { kFound bool hFound bool damage error } func inspect(st storer.EncodedObjectStorer, k, h plumbing.Hash) view { var v view for _, id := range []plumbing.Hash{k, h} { if id.IsZero() { continue } if _, err := readConfig(st, id); err != nil && !errors.Is(err, errNoConfig) { v.damage = err return v } } if h.IsZero() { return v } if v.kFound, v.damage = walk(st, h, k); v.damage != nil || k.IsZero() || v.kFound { return v } v.hFound, v.damage = walk(st, k, h) return v } func walk(st storer.EncodedObjectStorer, from, target plumbing.Hash) (bool, error) { c, err := object.GetCommit(st, from) if err != nil { return false, err } found := false err = object.NewCommitPreorderIter(c, nil, nil).ForEach(func(c *object.Commit) error { if c.Hash == target { found = true } return nil }) return found, err } func (s *Source) repair(ctx context.Context, url string, auth transport.AuthMethod, branch plumbing.ReferenceName, k plumbing.Hash) (plumbing.Hash, *refusal, error) { beforeRepair() scratch := s.path(scratchName) if err := os.RemoveAll(scratch); err != nil { return plumbing.ZeroHash, nil, err } defer os.RemoveAll(scratch) r, err := git.PlainInit(scratch, true) if err != nil { return plumbing.ZeroHash, nil, err } head, err := replica.Fetch(ctx, r, url, auth, branch) if err != nil { return plumbing.ZeroHash, &refusal{KindUnreachable, err}, nil } v := inspect(r.Storer, k, head) if !k.IsZero() && !v.kFound { cause := errMissing if v.damage != nil { cause = fmt.Errorf("%w: %w", errMissing, v.damage) } return head, &refusal{KindCheckpointMissing, cause}, nil } if v.damage != nil { return head, &refusal{KindUnreachable, v.damage}, nil } if !k.IsZero() { if err := writeCheckpointRef(scratch, k); err != nil { return plumbing.ZeroHash, nil, err } } if err := syncTree(scratch); err != nil { return plumbing.ZeroHash, nil, err } if err := s.swap(scratch); err != nil { return plumbing.ZeroHash, nil, err } return head, nil, nil } func (s *Source) swap(scratch string) error { repo, aside := s.path(repoName), s.path(asideName) if err := os.RemoveAll(aside); err != nil { return err } if err := os.Rename(repo, aside); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } if err := os.Rename(scratch, repo); err != nil { if rerr := os.Rename(aside, repo); rerr != nil && !errors.Is(rerr, fs.ErrNotExist) { return errors.Join(err, rerr) } return err } if err := syncPath(s.dir); err != nil { return err } return os.RemoveAll(aside) } func (s *Source) recover() error { if err := os.RemoveAll(s.path(scratchName)); err != nil { return err } aside := s.path(asideName) if _, err := os.Lstat(aside); errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return err } _, err := os.Lstat(s.path(repoName)) switch { case errors.Is(err, fs.ErrNotExist): if err := os.Rename(aside, s.path(repoName)); err != nil { return err } return syncPath(s.dir) case err != nil: return err } return os.RemoveAll(aside) } func writeCheckpointRef(repo string, h plumbing.Hash) error { dir := filepath.Join(repo, "refs", "tug") if err := os.MkdirAll(dir, 0o755); err != nil { return err } if err := replaceFile(filepath.Join(repo, "tug-checkpoint.tmp"), filepath.Join(dir, "checkpoint"), []byte(h.String()+"\n")); err != nil { return err } return syncPath(filepath.Dir(dir)) } func replaceFile(tmp, p string, data []byte) error { if err := os.Remove(tmp); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return err } _, werr := f.Write(data) if err := errors.Join(werr, f.Chmod(0o600), f.Close()); err != nil { _ = os.Remove(tmp) return err } if err := syncPath(tmp); err != nil { _ = os.Remove(tmp) return err } if err := os.Rename(tmp, p); err != nil { _ = os.Remove(tmp) return err } return syncPath(filepath.Dir(p)) } func syncTree(root string) error { return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || d.Type().IsRegular() { return syncPath(p) } return nil }) } func fsync(p string) error { f, err := os.Open(p) if err != nil { return err } defer f.Close() return f.Sync() }