Monorepo for Tangled tangled.org
3

Configure Feed

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

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 var shas []string 212 213 pullMap := make(map[string]*models.Pull) 214 for _, p := range pulls { 215 shas = append(shas, p.LatestSha()) 216 pullMap[p.AtUri().String()] = p 217 } 218 219 // track which PRs have been added to stacks 220 visited := make(map[string]bool) 221 222 // group stacked PRs together using dependent_on relationships 223 for _, p := range pulls { 224 if visited[p.AtUri().String()] { 225 continue 226 } 227 228 root := p 229 for root.DependentOn != nil { 230 if parent, ok := pullMap[root.DependentOn.String()]; ok { 231 root = parent 232 } else { 233 break // parent not in current page 234 } 235 } 236 237 var stack models.Stack 238 current := root 239 for { 240 if visited[current.AtUri().String()] { 241 break 242 } 243 stack = append(stack, current) 244 visited[current.AtUri().String()] = true 245 246 found := false 247 for _, candidate := range pulls { 248 if candidate.DependentOn != nil && 249 candidate.DependentOn.String() == current.AtUri().String() { 250 current = candidate 251 found = true 252 break 253 } 254 } 255 if !found { 256 break 257 } 258 } 259 260 slices.Reverse(stack) 261 stacks = append(stacks, stack) 262 } 263 264 ps, err := db.GetPipelineStatuses( 265 s.db, 266 len(shas), 267 orm.FilterEq("p.repo_did", f.RepoDid), 268 orm.FilterIn("p.sha", shas), 269 ) 270 if err != nil { 271 l.Warn("failed to fetch pipeline statuses", "err", err) 272 // non-fatal 273 } 274 m := make(map[string]models.Pipeline) 275 for _, p := range ps { 276 m[p.Sha] = p 277 } 278 279 labelDefs, err := db.GetLabelDefinitions( 280 s.db, 281 orm.FilterIn("at_uri", f.Labels), 282 orm.FilterContains("scope", tangled.RepoPullNSID), 283 ) 284 if err != nil { 285 l.Error("failed to fetch labels", "err", err) 286 s.pages.Error503(w) 287 return 288 } 289 290 defs := make(map[string]*models.LabelDefinition) 291 for _, l := range labelDefs { 292 defs[l.AtUri().String()] = &l 293 } 294 295 filterState := "" 296 if state != nil { 297 filterState = state.String() 298 } 299 300 vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) 301 if user != nil { 302 dids := make([]syntax.DID, len(pulls)) 303 for i, p := range pulls { 304 dids[i] = syntax.DID(p.OwnerDid) 305 } 306 vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), dids) 307 if err != nil { 308 l.Error("failed to fetch vouch relationships", "err", err) 309 } 310 } 311 312 err = s.pages.RepoPulls(w, pages.RepoPullsParams{ 313 BaseParams: pages.BaseParamsFromContext(r.Context()), 314 RepoInfo: repoInfo, 315 Pulls: pulls, 316 LabelDefs: defs, 317 FilterState: filterState, 318 FilterQuery: query.String(), 319 Stacks: stacks, 320 Pipelines: m, 321 Page: page, 322 PullCount: totalPulls, 323 VouchRelationships: vouchRelationships, 324 }) 325 if err != nil { 326 l.Error("failed to render page", "err", err) 327 } 328}