forked from tangled.org/core
Monorepo for Tangled

appview: backlinks

currently all backlinks are parsed with markdown parser.
So only explict urls like below are supported:

- <https://tangled.org/owner.com/repo-name/issues/123>
- [full url](https://tangled.org/owner.com/repo-name/issues/123)
- [absolute path](/owner.com/repo-name/issues/123)

Also `#comment-123` fragment is supported too.

All references in issue/pull/comment records are stored in at-uri
format. we do have a `mentions` field, but it isn't used yet. mentions
are parsed on rendering and aren't stored anywhere for now.

Signed-off-by: Seongmin Lee <git@boltless.me>

authored by boltless.me and committed by Tangled 442e2ab1 290b2ac5

Changed files
+365 -7
appview
db
issues
models
pages
templates
repo
fragments
issues
pulls
pulls
+19 -1
appview/db/pulls.go
··· 93 insert into pull_submissions (pull_at, round_number, patch, combined, source_rev) 94 values (?, ?, ?, ?, ?) 95 `, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev) 96 - return err 97 } 98 99 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { ··· 263 if sourceRepo, ok := sourceRepoMap[*p.PullSource.RepoAt]; ok { 264 p.PullSource.Repo = sourceRepo 265 } 266 } 267 } 268
··· 93 insert into pull_submissions (pull_at, round_number, patch, combined, source_rev) 94 values (?, ?, ?, ?, ?) 95 `, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev) 96 + if err != nil { 97 + return err 98 + } 99 + 100 + if err := putReferences(tx, pull.AtUri(), pull.References); err != nil { 101 + return fmt.Errorf("put reference_links: %w", err) 102 + } 103 + 104 + return nil 105 } 106 107 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { ··· 271 if sourceRepo, ok := sourceRepoMap[*p.PullSource.RepoAt]; ok { 272 p.PullSource.Repo = sourceRepo 273 } 274 + } 275 + } 276 + 277 + allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts)) 278 + if err != nil { 279 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 280 + } 281 + for pullAt, references := range allReferences { 282 + if pull, ok := pulls[pullAt]; ok { 283 + pull.References = references 284 } 285 } 286
+212
appview/db/reference.go
··· 248 249 return result, nil 250 }
··· 248 249 return result, nil 250 } 251 + 252 + func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 253 + rows, err := e.Query( 254 + `select from_at from reference_links 255 + where to_at = ?`, 256 + target, 257 + ) 258 + if err != nil { 259 + return nil, fmt.Errorf("query backlinks: %w", err) 260 + } 261 + defer rows.Close() 262 + 263 + var ( 264 + backlinks []models.RichReferenceLink 265 + backlinksMap = make(map[string][]syntax.ATURI) 266 + ) 267 + for rows.Next() { 268 + var from syntax.ATURI 269 + if err := rows.Scan(&from); err != nil { 270 + return nil, fmt.Errorf("scan row: %w", err) 271 + } 272 + nsid := from.Collection().String() 273 + backlinksMap[nsid] = append(backlinksMap[nsid], from) 274 + } 275 + if err := rows.Err(); err != nil { 276 + return nil, fmt.Errorf("iterate rows: %w", err) 277 + } 278 + 279 + var ls []models.RichReferenceLink 280 + ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 281 + if err != nil { 282 + return nil, fmt.Errorf("get issue backlinks: %w", err) 283 + } 284 + backlinks = append(backlinks, ls...) 285 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 286 + if err != nil { 287 + return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 288 + } 289 + backlinks = append(backlinks, ls...) 290 + ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 291 + if err != nil { 292 + return nil, fmt.Errorf("get pull backlinks: %w", err) 293 + } 294 + backlinks = append(backlinks, ls...) 295 + ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 296 + if err != nil { 297 + return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 298 + } 299 + backlinks = append(backlinks, ls...) 300 + 301 + return backlinks, nil 302 + } 303 + 304 + func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 305 + if len(aturis) == 0 { 306 + return nil, nil 307 + } 308 + vals := make([]string, len(aturis)) 309 + args := make([]any, 0, len(aturis)*2) 310 + for i, aturi := range aturis { 311 + vals[i] = "(?, ?)" 312 + did := aturi.Authority().String() 313 + rkey := aturi.RecordKey().String() 314 + args = append(args, did, rkey) 315 + } 316 + rows, err := e.Query( 317 + fmt.Sprintf( 318 + `select r.did, r.name, i.issue_id, i.title, i.open 319 + from issues i 320 + join repos r 321 + on r.at_uri = i.repo_at 322 + where (i.did, i.rkey) in (%s)`, 323 + strings.Join(vals, ","), 324 + ), 325 + args..., 326 + ) 327 + if err != nil { 328 + return nil, err 329 + } 330 + defer rows.Close() 331 + var refLinks []models.RichReferenceLink 332 + for rows.Next() { 333 + var l models.RichReferenceLink 334 + l.Kind = models.RefKindIssue 335 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 336 + return nil, err 337 + } 338 + refLinks = append(refLinks, l) 339 + } 340 + if err := rows.Err(); err != nil { 341 + return nil, fmt.Errorf("iterate rows: %w", err) 342 + } 343 + return refLinks, nil 344 + } 345 + 346 + func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 347 + if len(aturis) == 0 { 348 + return nil, nil 349 + } 350 + filter := FilterIn("c.at_uri", aturis) 351 + rows, err := e.Query( 352 + fmt.Sprintf( 353 + `select r.did, r.name, i.issue_id, c.id, i.title, i.open 354 + from issue_comments c 355 + join issues i 356 + on i.at_uri = c.issue_at 357 + join repos r 358 + on r.at_uri = i.repo_at 359 + where %s`, 360 + filter.Condition(), 361 + ), 362 + filter.Arg()..., 363 + ) 364 + if err != nil { 365 + return nil, err 366 + } 367 + defer rows.Close() 368 + var refLinks []models.RichReferenceLink 369 + for rows.Next() { 370 + var l models.RichReferenceLink 371 + l.Kind = models.RefKindIssue 372 + l.CommentId = new(int) 373 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 374 + return nil, err 375 + } 376 + refLinks = append(refLinks, l) 377 + } 378 + if err := rows.Err(); err != nil { 379 + return nil, fmt.Errorf("iterate rows: %w", err) 380 + } 381 + return refLinks, nil 382 + } 383 + 384 + func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 385 + if len(aturis) == 0 { 386 + return nil, nil 387 + } 388 + vals := make([]string, len(aturis)) 389 + args := make([]any, 0, len(aturis)*2) 390 + for i, aturi := range aturis { 391 + vals[i] = "(?, ?)" 392 + did := aturi.Authority().String() 393 + rkey := aturi.RecordKey().String() 394 + args = append(args, did, rkey) 395 + } 396 + rows, err := e.Query( 397 + fmt.Sprintf( 398 + `select r.did, r.name, p.pull_id, p.title, p.state 399 + from pulls p 400 + join repos r 401 + on r.at_uri = p.repo_at 402 + where (p.owner_did, p.rkey) in (%s)`, 403 + strings.Join(vals, ","), 404 + ), 405 + args..., 406 + ) 407 + if err != nil { 408 + return nil, err 409 + } 410 + defer rows.Close() 411 + var refLinks []models.RichReferenceLink 412 + for rows.Next() { 413 + var l models.RichReferenceLink 414 + l.Kind = models.RefKindPull 415 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 416 + return nil, err 417 + } 418 + refLinks = append(refLinks, l) 419 + } 420 + if err := rows.Err(); err != nil { 421 + return nil, fmt.Errorf("iterate rows: %w", err) 422 + } 423 + return refLinks, nil 424 + } 425 + 426 + func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 427 + if len(aturis) == 0 { 428 + return nil, nil 429 + } 430 + filter := FilterIn("c.comment_at", aturis) 431 + rows, err := e.Query( 432 + fmt.Sprintf( 433 + `select r.did, r.name, p.pull_id, c.id, p.title, p.state 434 + from repos r 435 + join pulls p 436 + on r.at_uri = p.repo_at 437 + join pull_comments c 438 + on r.at_uri = c.repo_at and p.pull_id = c.pull_id 439 + where %s`, 440 + filter.Condition(), 441 + ), 442 + filter.Arg()..., 443 + ) 444 + if err != nil { 445 + return nil, err 446 + } 447 + defer rows.Close() 448 + var refLinks []models.RichReferenceLink 449 + for rows.Next() { 450 + var l models.RichReferenceLink 451 + l.Kind = models.RefKindPull 452 + l.CommentId = new(int) 453 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 454 + return nil, err 455 + } 456 + refLinks = append(refLinks, l) 457 + } 458 + if err := rows.Err(); err != nil { 459 + return nil, fmt.Errorf("iterate rows: %w", err) 460 + } 461 + return refLinks, nil 462 + }
+11
appview/issues/issues.go
··· 104 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 105 } 106 107 labelDefs, err := db.GetLabelDefinitions( 108 rp.db, 109 db.FilterIn("at_uri", f.Labels), ··· 125 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 126 Issue: issue, 127 CommentList: issue.CommentList(), 128 OrderedReactionKinds: models.OrderedReactionKinds, 129 Reactions: reactionMap, 130 UserReacted: userReactions, ··· 155 newIssue := issue 156 newIssue.Title = r.FormValue("title") 157 newIssue.Body = r.FormValue("body") 158 159 if err := rp.validator.ValidateIssue(newIssue); err != nil { 160 l.Error("validation error", "err", err) ··· 575 newComment := comment 576 newComment.Body = newBody 577 newComment.Edited = &now 578 record := newComment.AsRecord() 579 580 tx, err := rp.db.Begin()
··· 104 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 105 } 106 107 + backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) 108 + if err != nil { 109 + l.Error("failed to fetch backlinks", "err", err) 110 + rp.pages.Error503(w) 111 + return 112 + } 113 + 114 labelDefs, err := db.GetLabelDefinitions( 115 rp.db, 116 db.FilterIn("at_uri", f.Labels), ··· 132 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 133 Issue: issue, 134 CommentList: issue.CommentList(), 135 + Backlinks: backlinks, 136 OrderedReactionKinds: models.OrderedReactionKinds, 137 Reactions: reactionMap, 138 UserReacted: userReactions, ··· 163 newIssue := issue 164 newIssue.Title = r.FormValue("title") 165 newIssue.Body = r.FormValue("body") 166 + newIssue.Mentions, newIssue.References = rp.refResolver.Resolve(r.Context(), newIssue.Body) 167 168 if err := rp.validator.ValidateIssue(newIssue); err != nil { 169 l.Error("validation error", "err", err) ··· 584 newComment := comment 585 newComment.Body = newBody 586 newComment.Edited = &now 587 + newComment.Mentions, newComment.References = rp.refResolver.Resolve(r.Context(), newBody) 588 + 589 record := newComment.AsRecord() 590 591 tx, err := rp.db.Begin()
+15 -3
appview/models/pull.go
··· 66 TargetBranch string 67 State PullState 68 Submissions []*PullSubmission 69 70 // stacking 71 StackId string // nullable string ··· 92 source.Repo = &s 93 } 94 } 95 96 record := tangled.RepoPull{ 97 - Title: p.Title, 98 - Body: &p.Body, 99 - CreatedAt: p.Created.Format(time.RFC3339), 100 Target: &tangled.RepoPull_Target{ 101 Repo: p.RepoAt.String(), 102 Branch: p.TargetBranch,
··· 66 TargetBranch string 67 State PullState 68 Submissions []*PullSubmission 69 + Mentions []syntax.DID 70 + References []syntax.ATURI 71 72 // stacking 73 StackId string // nullable string ··· 94 source.Repo = &s 95 } 96 } 97 + mentions := make([]string, len(p.Mentions)) 98 + for i, did := range p.Mentions { 99 + mentions[i] = string(did) 100 + } 101 + references := make([]string, len(p.References)) 102 + for i, uri := range p.References { 103 + references[i] = string(uri) 104 + } 105 106 record := tangled.RepoPull{ 107 + Title: p.Title, 108 + Body: &p.Body, 109 + Mentions: mentions, 110 + References: references, 111 + CreatedAt: p.Created.Format(time.RFC3339), 112 Target: &tangled.RepoPull_Target{ 113 Repo: p.RepoAt.String(), 114 Branch: p.TargetBranch,
+31
appview/models/reference.go
··· 1 package models 2 3 type RefKind int 4 5 const ( ··· 7 RefKindPull 8 ) 9 10 // /@alice.com/cool-proj/issues/123 11 // /@alice.com/cool-proj/issues/123#comment-321 12 type ReferenceLink struct { ··· 16 SubjectId int 17 CommentId *int 18 }
··· 1 package models 2 3 + import "fmt" 4 + 5 type RefKind int 6 7 const ( ··· 9 RefKindPull 10 ) 11 12 + func (k RefKind) String() string { 13 + if k == RefKindIssue { 14 + return "issues" 15 + } else { 16 + return "pulls" 17 + } 18 + } 19 + 20 // /@alice.com/cool-proj/issues/123 21 // /@alice.com/cool-proj/issues/123#comment-321 22 type ReferenceLink struct { ··· 26 SubjectId int 27 CommentId *int 28 } 29 + 30 + func (l ReferenceLink) String() string { 31 + comment := "" 32 + if l.CommentId != nil { 33 + comment = fmt.Sprintf("#comment-%d", *l.CommentId) 34 + } 35 + return fmt.Sprintf("/%s/%s/%s/%d%s", 36 + l.Handle, 37 + l.Repo, 38 + l.Kind.String(), 39 + l.SubjectId, 40 + comment, 41 + ) 42 + } 43 + 44 + type RichReferenceLink struct { 45 + ReferenceLink 46 + Title string 47 + // reusing PullState for both issue & PR 48 + State PullState 49 + }
+2
appview/pages/pages.go
··· 934 Active string 935 Issue *models.Issue 936 CommentList []models.CommentListItem 937 LabelDefs map[string]*models.LabelDefinition 938 939 OrderedReactionKinds []models.ReactionKind ··· 1087 Pull *models.Pull 1088 Stack models.Stack 1089 AbandonedPulls []*models.Pull 1090 BranchDeleteStatus *models.BranchDeleteStatus 1091 MergeCheck types.MergeCheckResponse 1092 ResubmitCheck ResubmitResult
··· 934 Active string 935 Issue *models.Issue 936 CommentList []models.CommentListItem 937 + Backlinks []models.RichReferenceLink 938 LabelDefs map[string]*models.LabelDefinition 939 940 OrderedReactionKinds []models.ReactionKind ··· 1088 Pull *models.Pull 1089 Stack models.Stack 1090 AbandonedPulls []*models.Pull 1091 + Backlinks []models.RichReferenceLink 1092 BranchDeleteStatus *models.BranchDeleteStatus 1093 MergeCheck types.MergeCheckResponse 1094 ResubmitCheck ResubmitResult
+49
appview/pages/templates/repo/fragments/backlinks.html
···
··· 1 + {{ define "repo/fragments/backlinks" }} 2 + {{ if .Backlinks }} 3 + <div id="at-uri-panel" class="px-2 md:px-0"> 4 + <div> 5 + <span class="text-sm py-1 font-bold text-gray-500 dark:text-gray-400">Referenced by</span> 6 + </div> 7 + <ul> 8 + {{ range .Backlinks }} 9 + <li> 10 + {{ $repoOwner := resolve .Handle }} 11 + {{ $repoName := .Repo }} 12 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 13 + <div class="flex flex-col"> 14 + <div class="flex gap-2 items-center"> 15 + {{ if .State.IsClosed }} 16 + <span class="text-gray-500 dark:text-gray-400"> 17 + {{ i "ban" "w-4 h-4" }} 18 + </span> 19 + {{ else if eq .Kind.String "issues" }} 20 + <span class="text-green-600 dark:text-green-500"> 21 + {{ i "circle-dot" "w-4 h-4" }} 22 + </span> 23 + {{ else if .State.IsOpen }} 24 + <span class="text-green-600 dark:text-green-500"> 25 + {{ i "git-pull-request" "w-4 h-4" }} 26 + </span> 27 + {{ else if .State.IsMerged }} 28 + <span class="text-purple-600 dark:text-purple-500"> 29 + {{ i "git-merge" "w-4 h-4" }} 30 + </span> 31 + {{ else }} 32 + <span class="text-gray-600 dark:text-gray-300"> 33 + {{ i "git-pull-request-closed" "w-4 h-4" }} 34 + </span> 35 + {{ end }} 36 + <a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 37 + </div> 38 + {{ if not (eq $.RepoInfo.FullName $repoUrl) }} 39 + <div> 40 + <span>on <a href="/{{ $repoUrl }}">{{ $repoUrl }}</a></span> 41 + </div> 42 + {{ end }} 43 + </div> 44 + </li> 45 + {{ end }} 46 + </ul> 47 + </div> 48 + {{ end }} 49 + {{ end }}
+3
appview/pages/templates/repo/issues/issue.html
··· 20 "Subject" $.Issue.AtUri 21 "State" $.Issue.Labels) }} 22 {{ template "repo/fragments/participants" $.Issue.Participants }} 23 {{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }} 24 </div> 25 </div>
··· 20 "Subject" $.Issue.AtUri 21 "State" $.Issue.Labels) }} 22 {{ template "repo/fragments/participants" $.Issue.Participants }} 23 + {{ template "repo/fragments/backlinks" 24 + (dict "RepoInfo" $.RepoInfo 25 + "Backlinks" $.Backlinks) }} 26 {{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }} 27 </div> 28 </div>
+3
appview/pages/templates/repo/pulls/pull.html
··· 21 "Subject" $.Pull.AtUri 22 "State" $.Pull.Labels) }} 23 {{ template "repo/fragments/participants" $.Pull.Participants }} 24 {{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }} 25 </div> 26 </div>
··· 21 "Subject" $.Pull.AtUri 22 "State" $.Pull.Labels) }} 23 {{ template "repo/fragments/participants" $.Pull.Participants }} 24 + {{ template "repo/fragments/backlinks" 25 + (dict "RepoInfo" $.RepoInfo 26 + "Backlinks" $.Backlinks) }} 27 {{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }} 28 </div> 29 </div>
+20 -3
appview/pulls/pulls.go
··· 1 package pulls 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "errors" ··· 155 return 156 } 157 158 // can be nil if this pull is not stacked 159 stack, _ := r.Context().Value("stack").(models.Stack) 160 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 229 Pull: pull, 230 Stack: stack, 231 AbandonedPulls: abandonedPulls, 232 BranchDeleteStatus: branchDeleteStatus, 233 MergeCheck: mergeCheckResponse, 234 ResubmitCheck: resubmitResult, ··· 1196 } 1197 } 1198 1199 rkey := tid.TID() 1200 initialSubmission := models.PullSubmission{ 1201 Patch: patch, ··· 1209 OwnerDid: user.Did, 1210 RepoAt: repo.RepoAt(), 1211 Rkey: rkey, 1212 Submissions: []*models.PullSubmission{ 1213 &initialSubmission, 1214 }, ··· 1297 1298 // build a stack out of this patch 1299 stackId := uuid.New() 1300 - stack, err := newStack(repo, user, targetBranch, patch, pullSource, stackId.String()) 1301 if err != nil { 1302 log.Println("failed to create stack", err) 1303 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1911 targetBranch := pull.TargetBranch 1912 1913 origStack, _ := r.Context().Value("stack").(models.Stack) 1914 - newStack, err := newStack(repo, user, targetBranch, patch, pull.PullSource, stackId) 1915 if err != nil { 1916 log.Println("failed to create resubmitted stack", err) 1917 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2359 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2360 } 2361 2362 - func newStack(repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2363 formatPatches, err := patchutil.ExtractPatches(patch) 2364 if err != nil { 2365 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2384 body := fp.Body 2385 rkey := tid.TID() 2386 2387 initialSubmission := models.PullSubmission{ 2388 Patch: fp.Raw, 2389 SourceRev: fp.SHA, ··· 2396 OwnerDid: user.Did, 2397 RepoAt: repo.RepoAt(), 2398 Rkey: rkey, 2399 Submissions: []*models.PullSubmission{ 2400 &initialSubmission, 2401 },
··· 1 package pulls 2 3 import ( 4 + "context" 5 "database/sql" 6 "encoding/json" 7 "errors" ··· 156 return 157 } 158 159 + backlinks, err := db.GetBacklinks(s.db, pull.AtUri()) 160 + if err != nil { 161 + log.Println("failed to get pull backlinks", err) 162 + s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.") 163 + return 164 + } 165 + 166 // can be nil if this pull is not stacked 167 stack, _ := r.Context().Value("stack").(models.Stack) 168 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 237 Pull: pull, 238 Stack: stack, 239 AbandonedPulls: abandonedPulls, 240 + Backlinks: backlinks, 241 BranchDeleteStatus: branchDeleteStatus, 242 MergeCheck: mergeCheckResponse, 243 ResubmitCheck: resubmitResult, ··· 1205 } 1206 } 1207 1208 + mentions, references := s.refResolver.Resolve(r.Context(), body) 1209 + 1210 rkey := tid.TID() 1211 initialSubmission := models.PullSubmission{ 1212 Patch: patch, ··· 1220 OwnerDid: user.Did, 1221 RepoAt: repo.RepoAt(), 1222 Rkey: rkey, 1223 + Mentions: mentions, 1224 + References: references, 1225 Submissions: []*models.PullSubmission{ 1226 &initialSubmission, 1227 }, ··· 1310 1311 // build a stack out of this patch 1312 stackId := uuid.New() 1313 + stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String()) 1314 if err != nil { 1315 log.Println("failed to create stack", err) 1316 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1924 targetBranch := pull.TargetBranch 1925 1926 origStack, _ := r.Context().Value("stack").(models.Stack) 1927 + newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId) 1928 if err != nil { 1929 log.Println("failed to create resubmitted stack", err) 1930 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2372 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2373 } 2374 2375 + func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2376 formatPatches, err := patchutil.ExtractPatches(patch) 2377 if err != nil { 2378 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2397 body := fp.Body 2398 rkey := tid.TID() 2399 2400 + mentions, references := s.refResolver.Resolve(ctx, body) 2401 + 2402 initialSubmission := models.PullSubmission{ 2403 Patch: fp.Raw, 2404 SourceRev: fp.SHA, ··· 2411 OwnerDid: user.Did, 2412 RepoAt: repo.RepoAt(), 2413 Rkey: rkey, 2414 + Mentions: mentions, 2415 + References: references, 2416 Submissions: []*models.PullSubmission{ 2417 &initialSubmission, 2418 },