Monorepo for Tangled tangled.org
11

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