This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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