forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

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>

boltless.me eab29510 d6f45319

verified
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
··· 100 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 101 } 102 103 labelDefs, err := db.GetLabelDefinitions( 104 rp.db, 105 db.FilterIn("at_uri", f.Repo.Labels), ··· 121 RepoInfo: f.RepoInfo(user), 122 Issue: issue, 123 CommentList: issue.CommentList(), 124 OrderedReactionKinds: models.OrderedReactionKinds, 125 Reactions: reactionMap, 126 UserReacted: userReactions, ··· 156 newIssue := issue 157 newIssue.Title = r.FormValue("title") 158 newIssue.Body = r.FormValue("body") 159 160 if err := rp.validator.ValidateIssue(newIssue); err != nil { 161 l.Error("validation error", "err", err) ··· 592 newComment := comment 593 newComment.Body = newBody 594 newComment.Edited = &now 595 record := newComment.AsRecord() 596 597 tx, err := rp.db.Begin()
··· 100 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 101 } 102 103 + backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) 104 + if err != nil { 105 + l.Error("failed to fetch backlinks", "err", err) 106 + rp.pages.Error503(w) 107 + return 108 + } 109 + 110 labelDefs, err := db.GetLabelDefinitions( 111 rp.db, 112 db.FilterIn("at_uri", f.Repo.Labels), ··· 128 RepoInfo: f.RepoInfo(user), 129 Issue: issue, 130 CommentList: issue.CommentList(), 131 + Backlinks: backlinks, 132 OrderedReactionKinds: models.OrderedReactionKinds, 133 Reactions: reactionMap, 134 UserReacted: userReactions, ··· 164 newIssue := issue 165 newIssue.Title = r.FormValue("title") 166 newIssue.Body = r.FormValue("body") 167 + newIssue.Mentions, newIssue.References = rp.refResolver.Resolve(r.Context(), newIssue.Body) 168 169 if err := rp.validator.ValidateIssue(newIssue); err != nil { 170 l.Error("validation error", "err", err) ··· 601 newComment := comment 602 newComment.Body = newBody 603 newComment.Edited = &now 604 + newComment.Mentions, newComment.References = rp.refResolver.Resolve(r.Context(), newBody) 605 + 606 record := newComment.AsRecord() 607 608 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
··· 975 Active string 976 Issue *models.Issue 977 CommentList []models.CommentListItem 978 LabelDefs map[string]*models.LabelDefinition 979 980 OrderedReactionKinds []models.ReactionKind ··· 1128 Pull *models.Pull 1129 Stack models.Stack 1130 AbandonedPulls []*models.Pull 1131 BranchDeleteStatus *models.BranchDeleteStatus 1132 MergeCheck types.MergeCheckResponse 1133 ResubmitCheck ResubmitResult
··· 975 Active string 976 Issue *models.Issue 977 CommentList []models.CommentListItem 978 + Backlinks []models.RichReferenceLink 979 LabelDefs map[string]*models.LabelDefinition 980 981 OrderedReactionKinds []models.ReactionKind ··· 1129 Pull *models.Pull 1130 Stack models.Stack 1131 AbandonedPulls []*models.Pull 1132 + Backlinks []models.RichReferenceLink 1133 BranchDeleteStatus *models.BranchDeleteStatus 1134 MergeCheck types.MergeCheckResponse 1135 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" ··· 154 return 155 } 156 157 // can be nil if this pull is not stacked 158 stack, _ := r.Context().Value("stack").(models.Stack) 159 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, ··· 1207 } 1208 } 1209 1210 rkey := tid.TID() 1211 initialSubmission := models.PullSubmission{ 1212 Patch: patch, ··· 1220 OwnerDid: user.Did, 1221 RepoAt: f.RepoAt(), 1222 Rkey: rkey, 1223 Submissions: []*models.PullSubmission{ 1224 &initialSubmission, 1225 }, ··· 1307 1308 // build a stack out of this patch 1309 stackId := uuid.New() 1310 - stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String()) 1311 if err != nil { 1312 log.Println("failed to create stack", err) 1313 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1933 targetBranch := pull.TargetBranch 1934 1935 origStack, _ := r.Context().Value("stack").(models.Stack) 1936 - newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1937 if err != nil { 1938 log.Println("failed to create resubmitted stack", err) 1939 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2377 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2378 } 2379 2380 - func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2381 formatPatches, err := patchutil.ExtractPatches(patch) 2382 if err != nil { 2383 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2402 body := fp.Body 2403 rkey := tid.TID() 2404 2405 initialSubmission := models.PullSubmission{ 2406 Patch: fp.Raw, 2407 SourceRev: fp.SHA, ··· 2414 OwnerDid: user.Did, 2415 RepoAt: f.RepoAt(), 2416 Rkey: rkey, 2417 Submissions: []*models.PullSubmission{ 2418 &initialSubmission, 2419 },
··· 1 package pulls 2 3 import ( 4 + "context" 5 "database/sql" 6 "encoding/json" 7 "errors" ··· 155 return 156 } 157 158 + backlinks, err := db.GetBacklinks(s.db, pull.AtUri()) 159 + if err != nil { 160 + log.Println("failed to get pull backlinks", err) 161 + s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.") 162 + return 163 + } 164 + 165 // can be nil if this pull is not stacked 166 stack, _ := r.Context().Value("stack").(models.Stack) 167 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, ··· 1216 } 1217 } 1218 1219 + mentions, references := s.refResolver.Resolve(r.Context(), body) 1220 + 1221 rkey := tid.TID() 1222 initialSubmission := models.PullSubmission{ 1223 Patch: patch, ··· 1231 OwnerDid: user.Did, 1232 RepoAt: f.RepoAt(), 1233 Rkey: rkey, 1234 + Mentions: mentions, 1235 + References: references, 1236 Submissions: []*models.PullSubmission{ 1237 &initialSubmission, 1238 }, ··· 1320 1321 // build a stack out of this patch 1322 stackId := uuid.New() 1323 + stack, err := s.newStack(r.Context(), f, user, targetBranch, patch, pullSource, stackId.String()) 1324 if err != nil { 1325 log.Println("failed to create stack", err) 1326 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1946 targetBranch := pull.TargetBranch 1947 1948 origStack, _ := r.Context().Value("stack").(models.Stack) 1949 + newStack, err := s.newStack(r.Context(), f, user, targetBranch, patch, pull.PullSource, stackId) 1950 if err != nil { 1951 log.Println("failed to create resubmitted stack", err) 1952 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2390 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2391 } 2392 2393 + func (s *Pulls) newStack(ctx context.Context, f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2394 formatPatches, err := patchutil.ExtractPatches(patch) 2395 if err != nil { 2396 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2415 body := fp.Body 2416 rkey := tid.TID() 2417 2418 + mentions, references := s.refResolver.Resolve(ctx, body) 2419 + 2420 initialSubmission := models.PullSubmission{ 2421 Patch: fp.Raw, 2422 SourceRev: fp.SHA, ··· 2429 OwnerDid: user.Did, 2430 RepoAt: f.RepoAt(), 2431 Rkey: rkey, 2432 + Mentions: mentions, 2433 + References: references, 2434 Submissions: []*models.PullSubmission{ 2435 &initialSubmission, 2436 },