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 "fmt" 5 "net/http" 6 "strconv" 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/xrpcclient" 13 "tangled.org/core/orm" 14 "tangled.org/core/patchutil" 15 "tangled.org/core/types" 16 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 19 "github.com/go-chi/chi/v5" 20) 21 22// htmx fragment 23func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 24 l := s.logger.With("handler", "PullActions") 25 26 switch r.Method { 27 case http.MethodGet: 28 user := s.oauth.GetMultiAccountUser(r) 29 if user != nil { 30 l = l.With("user", user.Did) 31 } 32 33 f, err := s.repoResolver.Resolve(r) 34 if err != nil { 35 l.Error("failed to get repo and knot", "err", err) 36 return 37 } 38 39 pull, ok := r.Context().Value("pull").(*models.Pull) 40 if !ok { 41 l.Error("failed to get pull") 42 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 43 return 44 } 45 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 46 47 // can be nil if this pull is not stacked 48 stack, _ := r.Context().Value("stack").(models.Stack) 49 50 roundNumberStr := chi.URLParam(r, "round") 51 roundNumber, err := strconv.Atoi(roundNumberStr) 52 if err != nil { 53 roundNumber = pull.LastRoundNumber() 54 } 55 if roundNumber >= len(pull.Submissions) { 56 http.Error(w, "bad round id", http.StatusBadRequest) 57 l.Error("failed to parse round id", "err", err, "round_number", roundNumber) 58 return 59 } 60 61 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 62 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 63 resubmitResult := pages.Unknown 64 if user.Did == pull.OwnerDid { 65 resubmitResult = s.resubmitCheck(r, f, pull, stack) 66 } 67 68 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 69 LoggedInUser: user, 70 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 71 Pull: pull, 72 RoundNumber: roundNumber, 73 MergeCheck: mergeCheckResponse, 74 ResubmitCheck: resubmitResult, 75 BranchDeleteStatus: branchDeleteStatus, 76 Stack: stack, 77 }) 78 return 79 } 80} 81 82func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff bool) { 83 l := s.logger.With("handler", "repoPullHelper", "interdiff", interdiff) 84 85 user := s.oauth.GetMultiAccountUser(r) 86 if user != nil { 87 l = l.With("user", user.Did) 88 } 89 90 f, err := s.repoResolver.Resolve(r) 91 if err != nil { 92 l.Error("failed to get repo and knot", "err", err) 93 return 94 } 95 96 pull, ok := r.Context().Value("pull").(*models.Pull) 97 if !ok { 98 l.Error("failed to get pull") 99 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 100 return 101 } 102 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 103 104 if user != nil { 105 repoDid := f.RepoDid 106 userDid := user.Did 107 pullId := pull.PullId 108 go func() { 109 if err := db.MarkNotificationsReadForPull(s.db, userDid, repoDid, pullId); err != nil { 110 l.Error("failed to mark pull notifications as read", "err", err) 111 } 112 }() 113 114 atUri := pull.AtUri().String() 115 go func() { 116 if err := db.UpsertRecentLink(s.db, userDid, models.RecentLinkTypePull, atUri); err != nil { 117 l.Error("failed to upsert recent link", "err", err) 118 } 119 }() 120 } 121 122 backlinks, err := db.GetBacklinks(s.db, pull.AtUri()) 123 if err != nil { 124 l.Error("failed to get pull backlinks", "err", err) 125 s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.") 126 return 127 } 128 129 roundId := chi.URLParam(r, "round") 130 roundIdInt := pull.LastRoundNumber() 131 if r, err := strconv.Atoi(roundId); err == nil { 132 roundIdInt = r 133 } 134 if roundIdInt >= len(pull.Submissions) { 135 http.Error(w, "bad round id", http.StatusBadRequest) 136 l.Error("failed to parse round id", "err", err, "round_number", roundIdInt) 137 return 138 } 139 140 var diffOpts types.DiffOpts 141 if d := r.URL.Query().Get("diff"); d == "split" { 142 diffOpts.Split = true 143 } 144 145 // can be nil if this pull is not stacked 146 stack, _ := r.Context().Value("stack").(models.Stack) 147 148 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 149 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 150 resubmitResult := pages.Unknown 151 if user != nil && user.Did == pull.OwnerDid { 152 resubmitResult = s.resubmitCheck(r, f, pull, stack) 153 } 154 155 m := make(map[string]models.Pipeline) 156 157 var shas []string 158 for _, s := range pull.Submissions { 159 shas = append(shas, s.SourceRev) 160 } 161 for _, p := range stack { 162 shas = append(shas, p.LatestSha()) 163 } 164 165 ps, err := db.GetPipelineStatuses( 166 s.db, 167 len(shas), 168 orm.FilterEq("p.repo_did", f.RepoDid), 169 orm.FilterIn("p.sha", shas), 170 ) 171 if err != nil { 172 l.Error("failed to fetch pipeline statuses", "err", err) 173 // non-fatal 174 } 175 176 for _, p := range ps { 177 m[p.Sha] = p 178 } 179 180 entities := []syntax.ATURI{pull.AtUri()} 181 for _, s := range pull.Submissions { 182 for _, c := range s.Comments { 183 entities = append(entities, c.FeedCommentAtUri()) 184 } 185 } 186 reactions, err := db.ListReactionDisplayDataMap(s.db, entities, 20) 187 if err != nil { 188 l.Error("failed to get pull reactions", "err", err) 189 } 190 191 var userReactions map[syntax.ATURI]map[models.ReactionKind]bool 192 if user != nil { 193 userReactions, err = db.ListReactionStatusMap(s.db, entities, syntax.DID(user.Did)) 194 if err != nil { 195 s.logger.Error("failed to get user reactions", "err", err) 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 vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) 216 vouchSkips := make(map[syntax.DID]bool) 217 if user != nil { 218 participants := pull.Participants() 219 vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), participants) 220 if err != nil { 221 l.Error("failed to fetch vouch relationships", "err", err) 222 } 223 ownerDid := syntax.DID(pull.OwnerDid) 224 skipped, err := db.IsVouchSkipped(s.db, user.Did, pull.OwnerDid) 225 if err != nil { 226 l.Error("failed to check vouch skip", "err", err) 227 } 228 vouchSkips[ownerDid] = skipped 229 } 230 231 patch := pull.Submissions[roundIdInt].CombinedPatch() 232 var diff types.DiffRenderer 233 diff = patchutil.AsNiceDiff(patch, pull.TargetBranch) 234 235 if interdiff { 236 currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 237 if err != nil { 238 l.Error("failed to interdiff; current patch malformed", "err", err, "round_number", roundIdInt) 239 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 240 return 241 } 242 243 previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 244 if err != nil { 245 l.Error("failed to interdiff; previous patch malformed", "err", err, "round_number", roundIdInt) 246 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 247 return 248 } 249 250 diff = patchutil.Interdiff(previousPatch, currentPatch) 251 } 252 253 err = s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 254 LoggedInUser: user, 255 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 256 Pull: pull, 257 Stack: stack, 258 Backlinks: backlinks, 259 BranchDeleteStatus: branchDeleteStatus, 260 MergeCheck: mergeCheckResponse, 261 ResubmitCheck: resubmitResult, 262 Pipelines: m, 263 Diff: diff, 264 DiffOpts: diffOpts, 265 ActiveRound: roundIdInt, 266 IsInterdiff: interdiff, 267 268 Reactions: reactions, 269 UserReacted: userReactions, 270 271 LabelDefs: defs, 272 VouchRelationships: vouchRelationships, 273 VouchSkips: vouchSkips, 274 }) 275 if err != nil { 276 l.Error("failed to render page", "err", err) 277 } 278} 279 280func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 281 l := s.logger.With("handler", "RepoSinglePull") 282 283 pull, ok := r.Context().Value("pull").(*models.Pull) 284 if !ok { 285 l.Error("failed to get pull") 286 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 287 return 288 } 289 290 http.Redirect(w, r, r.URL.String()+fmt.Sprintf("/round/%d", pull.LastRoundNumber()), http.StatusFound) 291} 292 293func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse { 294 if pull.State == models.PullMerged { 295 return types.MergeCheckResponse{} 296 } 297 298 xrpcc := s.knotClient(f.Knot) 299 300 // combine patches of substack 301 subStack := stack.Below(pull) 302 // collect the portion of the stack that is mergeable 303 mergeable := subStack.Mergeable() 304 // combine each patch 305 patch := mergeable.CombinedPatch() 306 307 resp, err := tangled.RepoMergeCheck( 308 r.Context(), 309 xrpcc, 310 &tangled.RepoMergeCheck_Input{ 311 Did: f.Did, 312 Name: f.Name, 313 Branch: pull.TargetBranch, 314 Patch: patch, 315 }, 316 ) 317 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 318 s.logger.Error("failed to check for mergeability", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "target_branch", pull.TargetBranch) 319 return types.MergeCheckResponse{ 320 Error: fmt.Sprintf("failed to check merge status: %s", xrpcerr.Error()), 321 } 322 } 323 324 return mergeCheckResponseFrom(resp) 325} 326 327func mergeCheckResponseFrom(resp *tangled.RepoMergeCheck_Output) types.MergeCheckResponse { 328 conflicts := make([]types.ConflictInfo, len(resp.Conflicts)) 329 for i, c := range resp.Conflicts { 330 conflicts[i] = types.ConflictInfo{Filename: c.Filename, Reason: c.Reason} 331 } 332 out := types.MergeCheckResponse{ 333 IsConflicted: resp.Is_conflicted, 334 Conflicts: conflicts, 335 } 336 if resp.Message != nil { 337 out.Message = *resp.Message 338 } 339 if resp.Error != nil { 340 out.Error = *resp.Error 341 } 342 return out 343} 344 345func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *models.BranchDeleteStatus { 346 if pull.State != models.PullMerged { 347 return nil 348 } 349 350 user := s.oauth.GetMultiAccountUser(r) 351 if user == nil { 352 return nil 353 } 354 355 var branch string 356 // check if the branch exists 357 // NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates 358 if pull.IsBranchBased() { 359 branch = pull.PullSource.Branch 360 } else if pull.IsForkBased() { 361 branch = pull.PullSource.Branch 362 repo = pull.PullSource.Repo 363 } else { 364 return nil 365 } 366 367 // deleted fork 368 if repo == nil { 369 return nil 370 } 371 372 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 373 if !s.acl.HasRepoPermission(r.Context(), repo, user.Did, "repo:push") { 374 return nil 375 } 376 377 xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 378 resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoDid) 379 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 380 s.logger.Error("failed to get branch", "xrpcerr", xrpcerr, "err", err) 381 return nil 382 } 383 384 return &models.BranchDeleteStatus{ 385 Repo: repo, 386 Branch: resp.Name, 387 } 388} 389 390func (s *Pulls) resubmitCheck(r *http.Request, repo *models.Repo, pull *models.Pull, stack models.Stack) pages.ResubmitResult { 391 if pull.State == models.PullMerged || pull.State == models.PullAbandoned || pull.PullSource == nil { 392 return pages.Unknown 393 } 394 395 var sourceRepoDid string 396 if pull.PullSource.RepoDid != nil { 397 sourceRepoDid = string(*pull.PullSource.RepoDid) 398 } else { 399 sourceRepoDid = repo.RepoDid 400 } 401 402 xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} 403 branchResp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepoDid) 404 if err != nil { 405 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 406 s.logger.Error("failed to call XRPC repo.branches", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "branch", pull.PullSource.Branch) 407 return pages.Unknown 408 } 409 s.logger.Error("failed to reach knotserver", "err", err, "pull_id", pull.PullId) 410 return pages.Unknown 411 } 412 413 targetBranch := branchResp 414 415 top := stack[0] 416 latestSourceRev := top.LatestSha() 417 418 if latestSourceRev != targetBranch.Hash { 419 return pages.ShouldResubmit 420 } 421 422 return pages.ShouldNotResubmit 423} 424 425func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 426 s.repoPullHelper(w, r, false) 427} 428 429func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 430 s.repoPullHelper(w, r, true) 431} 432 433func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 434 l := s.logger.With("handler", "RepoPullPatchRaw") 435 436 pull, ok := r.Context().Value("pull").(*models.Pull) 437 if !ok { 438 l.Error("failed to get pull") 439 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 440 return 441 } 442 l = l.With("pull_id", pull.PullId) 443 444 roundId := chi.URLParam(r, "round") 445 roundIdInt, err := strconv.Atoi(roundId) 446 if err != nil || roundIdInt >= len(pull.Submissions) { 447 http.Error(w, "bad round id", http.StatusBadRequest) 448 l.Error("failed to parse round id", "err", err, "round_id_str", roundId) 449 return 450 } 451 452 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 453 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 454}