package replica import ( "context" "errors" "fmt" "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/storage/filesystem" ) const scratchName = "userbase-scratch.git" var errNoScratchDir = errors.New("replica: no directory for scratch repositories") func scratch(dir string) (*git.Repository, func(), error) { if dir == "" { return nil, nil, errNoScratchDir } p := filepath.Join(dir, scratchName) if err := os.RemoveAll(p); err != nil { return nil, nil, err } r, err := git.PlainInit(p, true) if err != nil { _ = os.RemoveAll(p) return nil, nil, err } return r, func() { _ = os.RemoveAll(p) }, nil } func checkpointMissing(repo *git.Repository, id plumbing.Hash) (bool, error) { missing := func(err error) (bool, error) { if errors.Is(err, plumbing.ErrObjectNotFound) { return true, nil } return false, err } c, err := object.GetCommit(repo.Storer, id) if err != nil { return missing(err) } tree, err := c.Tree() if err != nil { return missing(err) } for _, e := range tree.Entries { if _, err := repo.Storer.EncodedObject(plumbing.AnyObject, e.Hash); err != nil { return missing(err) } } return false, nil } func ensureCheckpoint(ctx context.Context, repo *git.Repository, dir string, primary Remote, k Checkpoint) error { if !k.Set { return nil } id := plumbing.NewHash(k.Value) missing, err := checkpointMissing(repo, id) if err != nil || !missing { return err } return heal(ctx, repo, dir, primary, id) } func heal(ctx context.Context, repo *git.Repository, dir string, primary Remote, k plumbing.Hash) error { s, done, err := scratch(dir) if err != nil { return err } defer done() head, err := FetchHistory(ctx, s, primary.URL, primary.Auth, Branch) if err != nil { return err } if !onBranch(s, k, head) { return refused(ErrCheckpointMissing, "checkpoint %s, remote %s", k, head) } needed, err := content(s, k) if err != nil { return err } ancestors, err := ancestry(s, k) if err != nil { return err } for _, id := range ancestors { if _, err := repo.Storer.EncodedObject(plumbing.CommitObject, id); err != nil { needed = append(needed, id) } } return install(repo, s, needed) } func onBranch(r *git.Repository, id, head plumbing.Hash) bool { if head.IsZero() { return false } c, err := object.GetCommit(r.Storer, id) if err != nil { return false } if id == head { return true } hc, err := object.GetCommit(r.Storer, head) if err != nil { return false } ok, err := c.IsAncestor(hc) return err == nil && ok } func content(r *git.Repository, tip plumbing.Hash) ([]plumbing.Hash, error) { c, err := object.GetCommit(r.Storer, tip) if err != nil { return nil, err } tree, err := c.Tree() if err != nil { return nil, err } if len(tree.Entries) != 1 { return nil, ErrNotExport } return []plumbing.Hash{tip, tree.Hash, tree.Entries[0].Hash}, nil } func ancestry(r *git.Repository, tip plumbing.Hash) ([]plumbing.Hash, error) { c, err := object.GetCommit(r.Storer, tip) if err != nil { return nil, err } var ids []plumbing.Hash seen := map[plumbing.Hash]bool{tip: true} stack := append([]plumbing.Hash{}, c.ParentHashes...) for len(stack) > 0 { id := stack[len(stack)-1] stack = stack[:len(stack)-1] if seen[id] { continue } seen[id] = true p, err := object.GetCommit(r.Storer, id) if err != nil { return nil, fmt.Errorf("replica: commit %s below %s: %w", id, tip, err) } ids = append(ids, id) stack = append(stack, p.ParentHashes...) } return ids, nil } func objectsDir(repo *git.Repository) string { s, ok := repo.Storer.(*filesystem.Storage) if !ok { return "" } return filepath.Join(s.Filesystem().Root(), "objects") } func install(dst, src *git.Repository, ids []plumbing.Hash) error { objects := objectsDir(dst) dirs := map[string]bool{} for _, id := range ids { obj, err := src.Storer.EncodedObject(plumbing.AnyObject, id) if err != nil { return err } if _, err := dst.Storer.SetEncodedObject(obj); err != nil { return err } if objects == "" { continue } p := loosePath(objects, id) if err := syncPath(p); err != nil { return err } dirs[filepath.Dir(p)] = true } if objects == "" { return nil } for d := range dirs { if err := syncPath(d); err != nil { return err } } return syncPath(objects) }