package spindle
import (
"context"
"errors"
"fmt"
pathpkg "path"
"strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/gitclient"
"tangled.org/core/gitutil"
"tangled.org/core/spindle/db"
"tangled.org/core/spindle/knotclient"
"tangled.org/core/workflow"
)
// no fetch here, it uses a clone that already has the commit or asks the knot
func (s *Spindle) readWorkflowDir(ctx context.Context, repoDid syntax.DID, ref string) (workflow.RawPipeline, string, error) {
repo, err := s.db.GetRepoByDid(repoDid)
if err != nil {
return nil, "", fmt.Errorf("unknown repoDid %s: %w", repoDid, err)
}
// one client per read, so the knot is asked for its caps once
git := s.knots.Git(knotclient.Knot{Host: repo.Knot}, repoDid)
admitted := false
admit := func() error {
if admitted {
return nil
}
admitted = true
return s.workflowDefs.admit(repoDid)
}
commit := ref
if !gitutil.Rev(ref).IsObjectID() {
if err := admit(); err != nil {
return nil, "", err
}
if commit, err = s.resolveWorkflowRef(ctx, git, *repo, ref); err != nil {
return nil, "", err
}
}
raw, ok, err := s.readLocalWorkflows(ctx, s.newRepoPath(repoDid), commit)
if ok || err != nil {
return raw, commit, err
}
if err := admit(); err != nil {
return nil, "", err
}
raw, err = s.readKnotWorkflows(ctx, git, commit)
return raw, commit, err
}
func (s *Spindle) resolveWorkflowRef(ctx context.Context, git *gitclient.Client, repo db.Repo, ref string) (string, error) {
if ref == "HEAD" {
_, hash, err := s.getRepoDefaultBranch(ctx, git, repo)
return hash, err
}
hash, err := git.Resolve(ctx, ref)
if err != nil {
return "", fmt.Errorf("resolving %s of %s: %w", ref, repo.RepoDid, err)
}
return string(hash), nil
}
// no lock, a fetch replacing the clone only makes git fail, which falls back to the knot
func (s *Spindle) readLocalWorkflows(ctx context.Context, repoPath, commit string) (raw workflow.RawPipeline, ok bool, err error) {
if _, err := gitutil.OutputOffline(ctx, "-C", repoPath, "cat-file", "-e", commit+"^{commit}"); err != nil {
return nil, false, nil
}
raw, err = readWorkflowTree(ctx, gitutil.OutputOffline, repoPath, commit)
if errors.Is(err, errTooManyWorkflowFiles) {
return nil, true, err
}
return raw, err == nil, nil
}
func (s *Spindle) readKnotWorkflows(ctx context.Context, git *gitclient.Client, commit string) (workflow.RawPipeline, error) {
files, err := git.ReadDir(ctx, gitclient.Oid(commit), workflow.WorkflowDir)
if errors.Is(err, gitclient.ErrPathNotFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("listing %s: %w", workflow.WorkflowDir, err)
}
var raw workflow.RawPipeline
for _, file := range files {
if !isRegularFileMode(file.Mode) {
continue
}
if len(raw) >= maxWorkflowFiles {
return nil, fmt.Errorf("%w: more than %d files under %s", errTooManyWorkflowFiles, maxWorkflowFiles, workflow.WorkflowDir)
}
raw = append(raw, workflow.RawWorkflow{Name: file.Name, Contents: file.Data})
}
return raw, nil
}
// the knot pads modes to seven digits, git prints six
func isRegularFileMode(mode string) bool {
mode = strings.TrimLeft(mode, "0")
return mode == "100644" || mode == "100755"
}
type gitOutput func(ctx context.Context, args ...string) ([]byte, error)
// a symlink in .tangled could point reads at files on this host
func readWorkflowTree(ctx context.Context, git gitOutput, repoPath, commit string) (workflow.RawPipeline, error) {
listing, err := git(ctx, "-C", repoPath, "ls-tree", "-z", commit, workflow.WorkflowDir+"/")
if err != nil {
return nil, fmt.Errorf("listing %s: %w", workflow.WorkflowDir, err)
}
var raw workflow.RawPipeline
for _, entry := range strings.Split(string(listing), "\x00") {
meta, path, ok := strings.Cut(entry, "\t")
fields := strings.Fields(meta)
if !ok || len(fields) != 3 || !isRegularFileMode(fields[0]) {
continue
}
if len(raw) >= maxWorkflowFiles {
return nil, fmt.Errorf("%w: more than %d files under %s", errTooManyWorkflowFiles, maxWorkflowFiles, workflow.WorkflowDir)
}
contents, err := git(ctx, "-C", repoPath, "cat-file", "blob", fields[2])
if err != nil {
return nil, fmt.Errorf("reading raw content of '%s': %w", path, err)
}
raw = append(raw, workflow.RawWorkflow{Name: pathpkg.Base(path), Contents: contents})
}
return raw, nil
}