This repository has no description
1package pulls
2
3import (
4 "context"
5 "net/http"
6
7 "tangled.org/core/api/tangled"
8 "tangled.org/core/appview/db"
9 "tangled.org/core/appview/models"
10 "tangled.org/core/appview/pages"
11 "tangled.org/core/appview/pagination"
12 "tangled.org/core/appview/searchquery"
13 "tangled.org/core/orm"
14
15 "github.com/bluesky-social/indigo/atproto/syntax"
16)
17
18func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
19 l := s.logger.With("handler", "RepoPulls")
20
21 user := s.oauth.GetMultiAccountUser(r)
22 if user != nil {
23 l = l.With("user", user.Did)
24 }
25
26 params := r.URL.Query()
27 page := pagination.FromContext(r.Context())
28
29 f, err := s.repoResolver.Resolve(r)
30 if err != nil {
31 l.Error("failed to get repo and knot", "err", err)
32 return
33 }
34 l = l.With("repo_at", f.RepoAt().String())
35
36 query := searchquery.Parse(params.Get("q"))
37
38 var state *models.PullState
39 if urlState := params.Get("state"); urlState != "" {
40 switch urlState {
41 case "open":
42 state = ptrPullState(models.PullOpen)
43 case "closed":
44 state = ptrPullState(models.PullClosed)
45 case "merged":
46 state = ptrPullState(models.PullMerged)
47 }
48 query.Set("state", urlState)
49 } else if queryState := query.Get("state"); queryState != nil {
50 switch *queryState {
51 case "open":
52 state = ptrPullState(models.PullOpen)
53 case "closed":
54 state = ptrPullState(models.PullClosed)
55 case "merged":
56 state = ptrPullState(models.PullMerged)
57 }
58 } else if _, hasQ := params["q"]; !hasQ {
59 state = ptrPullState(models.PullOpen)
60 query.Set("state", "open")
61 }
62
63 resolve := func(ctx context.Context, ident string) (string, error) {
64 id, err := s.idResolver.ResolveIdent(ctx, ident)
65 if err != nil {
66 return "", err
67 }
68 return id.DID.String(), nil
69 }
70
71 authorDid, negatedAuthorDids := searchquery.ResolveAuthor(r.Context(), query, resolve, l)
72
73 labels := query.GetAll("label")
74 negatedLabels := query.GetAllNegated("label")
75 labelValues := query.GetDynamicTags()
76 negatedLabelValues := query.GetNegatedDynamicTags()
77
78 // resolve DID-format label values: if a dynamic tag's label
79 // definition has format "did", resolve the handle to a DID
80 if len(labelValues) > 0 || len(negatedLabelValues) > 0 {
81 labelDefs, err := db.GetLabelDefinitions(
82 s.db,
83 orm.FilterIn("at_uri", f.Labels),
84 orm.FilterContains("scope", tangled.RepoPullNSID),
85 )
86 if err == nil {
87 didLabels := make(map[string]bool)
88 for _, def := range labelDefs {
89 if def.ValueType.Format == models.ValueTypeFormatDid {
90 didLabels[def.Name] = true
91 }
92 }
93 labelValues = searchquery.ResolveDIDLabelValues(r.Context(), labelValues, didLabels, resolve, l)
94 negatedLabelValues = searchquery.ResolveDIDLabelValues(r.Context(), negatedLabelValues, didLabels, resolve, l)
95 } else {
96 l.Debug("failed to fetch label definitions for DID resolution", "err", err)
97 }
98 }
99
100 tf := searchquery.ExtractTextFilters(query)
101
102 searchOpts := models.PullSearchOptions{
103 Keywords: tf.Keywords,
104 Phrases: tf.Phrases,
105 RepoDid: f.RepoDid,
106 State: state,
107 AuthorDid: authorDid,
108 Labels: labels,
109 LabelValues: labelValues,
110 NegatedKeywords: tf.NegatedKeywords,
111 NegatedPhrases: tf.NegatedPhrases,
112 NegatedLabels: negatedLabels,
113 NegatedLabelValues: negatedLabelValues,
114 NegatedAuthorDids: negatedAuthorDids,
115 Page: page,
116 }
117
118 var totalPulls int
119 if state == nil {
120 totalPulls = f.RepoStats.PullCount.Open + f.RepoStats.PullCount.Merged + f.RepoStats.PullCount.Closed
121 } else {
122 switch *state {
123 case models.PullOpen:
124 totalPulls = f.RepoStats.PullCount.Open
125 case models.PullMerged:
126 totalPulls = f.RepoStats.PullCount.Merged
127 case models.PullClosed:
128 totalPulls = f.RepoStats.PullCount.Closed
129 }
130 }
131
132 repoInfo := s.repoResolver.GetRepoInfo(r, user)
133
134 ctx := r.Context()
135
136 var pulls []*models.Pull
137
138 if searchOpts.HasSearchFilters() {
139 res, err := s.indexer.Search(ctx, searchOpts)
140 if err != nil {
141 l.Error("failed to search for pulls", "err", err)
142 return
143 }
144 totalPulls = int(res.Total)
145 l.Debug("searched pulls with indexer", "count", len(res.Hits))
146
147 // update tab counts to reflect filtered results
148 countOpts := searchOpts
149 countOpts.Page = pagination.Page{Limit: 1}
150 for _, ps := range []models.PullState{models.PullOpen, models.PullMerged, models.PullClosed} {
151 countOpts.State = &ps
152 countRes, err := s.indexer.Search(ctx, countOpts)
153 if err != nil {
154 continue
155 }
156 switch ps {
157 case models.PullOpen:
158 repoInfo.Stats.PullCount.Open = int(countRes.Total)
159 case models.PullMerged:
160 repoInfo.Stats.PullCount.Merged = int(countRes.Total)
161 case models.PullClosed:
162 repoInfo.Stats.PullCount.Closed = int(countRes.Total)
163 }
164 }
165
166 if len(res.Hits) > 0 {
167 pulls, err = db.GetPullsPaginated(
168 ctx,
169 s.db,
170 pagination.Page{Limit: searchOpts.Page.Limit},
171 orm.FilterIn("id", res.Hits),
172 )
173 if err != nil {
174 l.Error("failed to get pulls", "err", err)
175 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
176 return
177 }
178 }
179 } else {
180 filters := []orm.Filter{
181 orm.FilterEq("repo_did", f.RepoDid),
182 }
183 if state != nil {
184 filters = append(filters, orm.FilterEq("state", *state))
185 }
186 pulls, err = db.GetPullsPaginated(
187 ctx,
188 s.db,
189 page,
190 filters...,
191 )
192 if err != nil {
193 l.Error("failed to get pulls", "err", err)
194 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
195 return
196 }
197 }
198
199 labelDefs, err := db.GetLabelDefinitions(
200 s.db,
201 orm.FilterIn("at_uri", f.Labels),
202 orm.FilterContains("scope", tangled.RepoPullNSID),
203 )
204 if err != nil {
205 l.Error("failed to fetch labels", "err", err)
206 s.pages.Error503(w)
207 return
208 }
209
210 defs := make(map[string]*models.LabelDefinition)
211 for _, l := range labelDefs {
212 defs[l.AtUri().String()] = &l
213 }
214
215 filterState := ""
216 if state != nil {
217 filterState = state.String()
218 }
219
220 vouchRelationships := make(map[syntax.DID]*models.VouchRelationship)
221 if user != nil {
222 dids := make([]syntax.DID, len(pulls))
223 for i, p := range pulls {
224 dids[i] = syntax.DID(p.OwnerDid)
225 }
226 vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), dids)
227 if err != nil {
228 l.Error("failed to fetch vouch relationships", "err", err)
229 }
230 }
231
232 err = s.pages.RepoPulls(w, pages.RepoPullsParams{
233 BaseParams: pages.BaseParamsFromContext(r.Context()),
234 RepoInfo: repoInfo,
235 Pulls: pulls,
236 LabelDefs: defs,
237 FilterState: filterState,
238 FilterQuery: query.String(),
239 Page: page,
240 PullCount: totalPulls,
241 VouchRelationships: vouchRelationships,
242 })
243 if err != nil {
244 l.Error("failed to render page", "err", err)
245 }
246}