This repository has no description
1package pulls
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "slices"
8 "time"
9
10 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
11
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/appview/db"
14 "tangled.org/core/appview/models"
15 "tangled.org/core/hostutil"
16 "tangled.org/core/types"
17)
18
19// spindle upgrades may change how a commit is resolved and hashed (even if very rare...)
20const (
21 workflowCacheSize = 4096
22 workflowCacheTTL = time.Hour
23)
24
25func (s *Pulls) describeWorkflowAt(r *http.Request, f *models.Repo, sha, sourceRepo string) (*tangled.CiDescribeWorkflowDefinition_Output, error) {
26 key := fmt.Sprintf("%s|%s|%s|%s", f.Spindle, f.RepoDid, sha, sourceRepo)
27 if cached, ok := s.workflowDescCache.Get(key); ok {
28 return cached, nil
29 }
30
31 spindleUrl, err := hostutil.EnsureHttpScheme(f.Spindle)
32 if err != nil {
33 return nil, err
34 }
35 client := &indigoxrpc.Client{Host: spindleUrl}
36 out, err := tangled.CiDescribeWorkflowDefinition(r.Context(), client, f.RepoDid, sha, sourceRepo)
37 if err != nil {
38 return nil, err
39 }
40
41 s.workflowDescCache.Add(key, out)
42 return out, nil
43}
44
45// returns the merge-base of the pull's source and target branches.
46//
47// older rounds resolve it live on the fork's knot, which tracks the
48// target branch as refs/hidden/<source>/<target>.
49func (s *Pulls) mergeBase(r *http.Request, pull *models.Pull) (string, error) {
50 if mb := pull.LatestSubmission().MergeBase; mb != "" {
51 return mb, nil
52 }
53
54 sourceBranch := pull.PullSource.Branch
55 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, pull.TargetBranch)
56
57 key := fmt.Sprintf("%s|%s|%s", pull.PullSource.RepoDid, sourceBranch, pull.TargetBranch)
58 if cached, ok := s.mergeBaseCache.Get(key); ok {
59 return cached, nil
60 }
61
62 forkRepo, err := db.GetRepoByDid(s.db, pull.PullSource.RepoDid.String())
63 if err != nil {
64 return "", fmt.Errorf("resolving fork repo: %w", err)
65 }
66 compareBytes, err := tangled.RepoCompare(r.Context(), s.knotClient(forkRepo.Knot), forkRepo.RepoIdentifier(), hiddenRef, sourceBranch)
67 if err != nil {
68 return "", fmt.Errorf("comparing %s...%s: %w", hiddenRef, sourceBranch, err)
69 }
70 var comparison types.RepoFormatPatchResponse
71 if err := json.Unmarshal(compareBytes, &comparison); err != nil {
72 return "", fmt.Errorf("decoding comparison: %w", err)
73 }
74 if comparison.MergeBase == "" {
75 return "", fmt.Errorf("no merge base between %s and %s", hiddenRef, sourceBranch)
76 }
77
78 s.mergeBaseCache.Add(key, comparison.MergeBase)
79 return comparison.MergeBase, nil
80}
81
82// compares the spindle's workflow-definition fingerprints at the
83// pull head and its merge-base with the target branch.
84func (s *Pulls) workflowChangeFromSpindle(r *http.Request, f *models.Repo, pull *models.Pull) (bool, []string, error) {
85 sourceRepo := pull.PullSource.RepoDid.String()
86
87 head, err := s.describeWorkflowAt(r, f, pull.LatestSha(), sourceRepo)
88 if err != nil {
89 return false, nil, err
90 }
91 // a definition not derived from the repo cannot change between commits
92 if !head.Derived {
93 return false, nil, nil
94 }
95
96 baseSha, err := s.mergeBase(r, pull)
97 if err != nil {
98 return false, nil, err
99 }
100 base, err := s.describeWorkflowAt(r, f, baseSha, sourceRepo)
101 if err != nil {
102 return false, nil, err
103 }
104 if !base.Derived {
105 return false, nil, nil
106 }
107 if head.Hash != nil && base.Hash != nil && *head.Hash == *base.Hash {
108 return false, nil, nil
109 }
110
111 names := slices.Concat(head.Workflows, base.Workflows)
112 slices.Sort(names)
113 return true, slices.Compact(names), nil
114}