Monorepo for Tangled tangled.org

draft: render backlinks #765

merged opened by boltless.me targeting master from feat/mentions
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3m4ya772l5c22
+361 -6
Diff #7
+19 -1
appview/db/pulls.go
··· 93 93 insert into pull_submissions (pull_at, round_number, patch, combined, source_rev) 94 94 values (?, ?, ?, ?, ?) 95 95 `, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev) 96 - return err 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 97 105 } 98 106 99 107 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { ··· 266 274 } 267 275 } 268 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 + 269 287 orderedByPullId := []*models.Pull{} 270 288 for _, p := range pulls { 271 289 orderedByPullId = append(orderedByPullId, p)
+212
appview/db/reference.go
··· 249 249 250 250 return result, nil 251 251 } 252 + 253 + func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 254 + rows, err := e.Query( 255 + `select from_at from reference_links 256 + where to_at = ?`, 257 + target, 258 + ) 259 + if err != nil { 260 + return nil, fmt.Errorf("query backlinks: %w", err) 261 + } 262 + defer rows.Close() 263 + 264 + var ( 265 + backlinks []models.RichReferenceLink 266 + backlinksMap = make(map[string][]syntax.ATURI) 267 + ) 268 + for rows.Next() { 269 + var from syntax.ATURI 270 + if err := rows.Scan(&from); err != nil { 271 + return nil, fmt.Errorf("scan row: %w", err) 272 + } 273 + nsid := from.Collection().String() 274 + backlinksMap[nsid] = append(backlinksMap[nsid], from) 275 + } 276 + if err := rows.Err(); err != nil { 277 + return nil, fmt.Errorf("iterate rows: %w", err) 278 + } 279 + 280 + var ls []models.RichReferenceLink 281 + ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 282 + if err != nil { 283 + return nil, fmt.Errorf("get issue backlinks: %w", err) 284 + } 285 + backlinks = append(backlinks, ls...) 286 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 287 + if err != nil { 288 + return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 289 + } 290 + backlinks = append(backlinks, ls...) 291 + ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 292 + if err != nil { 293 + return nil, fmt.Errorf("get pull backlinks: %w", err) 294 + } 295 + backlinks = append(backlinks, ls...) 296 + ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 297 + if err != nil { 298 + return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 299 + } 300 + backlinks = append(backlinks, ls...) 301 + 302 + return backlinks, nil 303 + } 304 + 305 + func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 306 + if len(aturis) == 0 { 307 + return nil, nil 308 + } 309 + vals := make([]string, len(aturis)) 310 + args := make([]any, 0, len(aturis)*2) 311 + for i, aturi := range aturis { 312 + vals[i] = "(?, ?)" 313 + did := aturi.Authority().String() 314 + rkey := aturi.RecordKey().String() 315 + args = append(args, did, rkey) 316 + } 317 + rows, err := e.Query( 318 + fmt.Sprintf( 319 + `select r.did, r.name, i.issue_id, i.title, i.open 320 + from issues i 321 + join repos r 322 + on r.at_uri = i.repo_at 323 + where (i.did, i.rkey) in (%s)`, 324 + strings.Join(vals, ","), 325 + ), 326 + args..., 327 + ) 328 + if err != nil { 329 + return nil, err 330 + } 331 + defer rows.Close() 332 + var refLinks []models.RichReferenceLink 333 + for rows.Next() { 334 + var l models.RichReferenceLink 335 + l.Kind = models.RefKindIssue 336 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 337 + return nil, err 338 + } 339 + refLinks = append(refLinks, l) 340 + } 341 + if err := rows.Err(); err != nil { 342 + return nil, fmt.Errorf("iterate rows: %w", err) 343 + } 344 + return refLinks, nil 345 + } 346 + 347 + func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 348 + if len(aturis) == 0 { 349 + return nil, nil 350 + } 351 + filter := FilterIn("c.at_uri", aturis) 352 + rows, err := e.Query( 353 + fmt.Sprintf( 354 + `select r.did, r.name, i.issue_id, c.id, i.title, i.open 355 + from issue_comments c 356 + join issues i 357 + on i.at_uri = c.issue_at 358 + join repos r 359 + on r.at_uri = i.repo_at 360 + where %s`, 361 + filter.Condition(), 362 + ), 363 + filter.Arg()..., 364 + ) 365 + if err != nil { 366 + return nil, err 367 + } 368 + defer rows.Close() 369 + var refLinks []models.RichReferenceLink 370 + for rows.Next() { 371 + var l models.RichReferenceLink 372 + l.Kind = models.RefKindIssue 373 + l.CommentId = new(int) 374 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 375 + return nil, err 376 + } 377 + refLinks = append(refLinks, l) 378 + } 379 + if err := rows.Err(); err != nil { 380 + return nil, fmt.Errorf("iterate rows: %w", err) 381 + } 382 + return refLinks, nil 383 + } 384 + 385 + func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 386 + if len(aturis) == 0 { 387 + return nil, nil 388 + } 389 + vals := make([]string, len(aturis)) 390 + args := make([]any, 0, len(aturis)*2) 391 + for i, aturi := range aturis { 392 + vals[i] = "(?, ?)" 393 + did := aturi.Authority().String() 394 + rkey := aturi.RecordKey().String() 395 + args = append(args, did, rkey) 396 + } 397 + rows, err := e.Query( 398 + fmt.Sprintf( 399 + `select r.did, r.name, p.pull_id, p.title, p.state 400 + from pulls p 401 + join repos r 402 + on r.at_uri = p.repo_at 403 + where (p.owner_did, p.rkey) in (%s)`, 404 + strings.Join(vals, ","), 405 + ), 406 + args..., 407 + ) 408 + if err != nil { 409 + return nil, err 410 + } 411 + defer rows.Close() 412 + var refLinks []models.RichReferenceLink 413 + for rows.Next() { 414 + var l models.RichReferenceLink 415 + l.Kind = models.RefKindPull 416 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 417 + return nil, err 418 + } 419 + refLinks = append(refLinks, l) 420 + } 421 + if err := rows.Err(); err != nil { 422 + return nil, fmt.Errorf("iterate rows: %w", err) 423 + } 424 + return refLinks, nil 425 + } 426 + 427 + func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 428 + if len(aturis) == 0 { 429 + return nil, nil 430 + } 431 + filter := FilterIn("c.comment_at", aturis) 432 + rows, err := e.Query( 433 + fmt.Sprintf( 434 + `select r.did, r.name, p.pull_id, c.id, p.title, p.state 435 + from repos r 436 + join pulls p 437 + on r.at_uri = p.repo_at 438 + join pull_comments c 439 + on r.at_uri = c.repo_at and p.pull_id = c.pull_id 440 + where %s`, 441 + filter.Condition(), 442 + ), 443 + filter.Arg()..., 444 + ) 445 + if err != nil { 446 + return nil, err 447 + } 448 + defer rows.Close() 449 + var refLinks []models.RichReferenceLink 450 + for rows.Next() { 451 + var l models.RichReferenceLink 452 + l.Kind = models.RefKindPull 453 + l.CommentId = new(int) 454 + if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 455 + return nil, err 456 + } 457 + refLinks = append(refLinks, l) 458 + } 459 + if err := rows.Err(); err != nil { 460 + return nil, fmt.Errorf("iterate rows: %w", err) 461 + } 462 + return refLinks, nil 463 + }
+8
appview/issues/issues.go
··· 100 100 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 101 101 } 102 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 + 103 110 labelDefs, err := db.GetLabelDefinitions( 104 111 rp.db, 105 112 db.FilterIn("at_uri", f.Repo.Labels), ··· 121 128 RepoInfo: f.RepoInfo(user), 122 129 Issue: issue, 123 130 CommentList: issue.CommentList(), 131 + Backlinks: backlinks, 124 132 OrderedReactionKinds: models.OrderedReactionKinds, 125 133 Reactions: reactionMap, 126 134 UserReacted: userReactions,
+14 -2
appview/models/pull.go
··· 66 66 TargetBranch string 67 67 State PullState 68 68 Submissions []*PullSubmission 69 + Mentions []syntax.DID 70 + References []syntax.ATURI 69 71 70 72 // stacking 71 73 StackId string // nullable string ··· 92 94 source.Repo = &s 93 95 } 94 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 + } 95 105 96 106 record := tangled.RepoPull{ 97 - Title: p.Title, 98 - Body: &p.Body, 107 + Title: p.Title, 108 + Body: &p.Body, 109 + Mentions: mentions, 110 + References: references, 99 111 CreatedAt: p.Created.Format(time.RFC3339), 100 112 Target: &tangled.RepoPull_Target{ 101 113 Repo: p.RepoAt.String(),
+31
appview/models/reference.go
··· 1 1 package models 2 2 3 + import "fmt" 4 + 3 5 type RefKind int 4 6 5 7 const ( ··· 7 9 RefKindPull 8 10 ) 9 11 12 + func (k RefKind) String() string { 13 + if k == RefKindIssue { 14 + return "issues" 15 + } else { 16 + return "pulls" 17 + } 18 + } 19 + 10 20 // /@alice.com/cool-proj/issues/123 11 21 // /@alice.com/cool-proj/issues/123#comment-321 12 22 type ReferenceLink struct { ··· 16 26 SubjectId int 17 27 CommentId *int 18 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 975 Active string 976 976 Issue *models.Issue 977 977 CommentList []models.CommentListItem 978 + Backlinks []models.RichReferenceLink 978 979 LabelDefs map[string]*models.LabelDefinition 979 980 980 981 OrderedReactionKinds []models.ReactionKind ··· 1128 1129 Pull *models.Pull 1129 1130 Stack models.Stack 1130 1131 AbandonedPulls []*models.Pull 1132 + Backlinks []models.RichReferenceLink 1131 1133 BranchDeleteStatus *models.BranchDeleteStatus 1132 1134 MergeCheck types.MergeCheckResponse 1133 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 20 "Subject" $.Issue.AtUri 21 21 "State" $.Issue.Labels) }} 22 22 {{ template "repo/fragments/participants" $.Issue.Participants }} 23 + {{ template "repo/fragments/backlinks" 24 + (dict "RepoInfo" $.RepoInfo 25 + "Backlinks" $.Backlinks) }} 23 26 {{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }} 24 27 </div> 25 28 </div>
+3
appview/pages/templates/repo/pulls/pull.html
··· 21 21 "Subject" $.Pull.AtUri 22 22 "State" $.Pull.Labels) }} 23 23 {{ template "repo/fragments/participants" $.Pull.Participants }} 24 + {{ template "repo/fragments/backlinks" 25 + (dict "RepoInfo" $.RepoInfo 26 + "Backlinks" $.Backlinks) }} 24 27 {{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }} 25 28 </div> 26 29 </div>
+20 -3
appview/pulls/pulls.go
··· 1 1 package pulls 2 2 3 3 import ( 4 + "context" 4 5 "database/sql" 5 6 "encoding/json" 6 7 "errors" ··· 154 155 return 155 156 } 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 + 157 165 // can be nil if this pull is not stacked 158 166 stack, _ := r.Context().Value("stack").(models.Stack) 159 167 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 229 237 Pull: pull, 230 238 Stack: stack, 231 239 AbandonedPulls: abandonedPulls, 240 + Backlinks: backlinks, 232 241 BranchDeleteStatus: branchDeleteStatus, 233 242 MergeCheck: mergeCheckResponse, 234 243 ResubmitCheck: resubmitResult, ··· 1207 1216 } 1208 1217 } 1209 1218 1219 + mentions, references := s.refResolver.Resolve(r.Context(), body) 1220 + 1210 1221 rkey := tid.TID() 1211 1222 initialSubmission := models.PullSubmission{ 1212 1223 Patch: patch, ··· 1220 1231 OwnerDid: user.Did, 1221 1232 RepoAt: f.RepoAt(), 1222 1233 Rkey: rkey, 1234 + Mentions: mentions, 1235 + References: references, 1223 1236 Submissions: []*models.PullSubmission{ 1224 1237 &initialSubmission, 1225 1238 }, ··· 1307 1320 1308 1321 // build a stack out of this patch 1309 1322 stackId := uuid.New() 1310 - stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String()) 1323 + stack, err := s.newStack(r.Context(), f, user, targetBranch, patch, pullSource, stackId.String()) 1311 1324 if err != nil { 1312 1325 log.Println("failed to create stack", err) 1313 1326 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1933 1946 targetBranch := pull.TargetBranch 1934 1947 1935 1948 origStack, _ := r.Context().Value("stack").(models.Stack) 1936 - newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId) 1949 + newStack, err := s.newStack(r.Context(), f, user, targetBranch, patch, pull.PullSource, stackId) 1937 1950 if err != nil { 1938 1951 log.Println("failed to create resubmitted stack", err) 1939 1952 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2377 2390 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 2378 2391 } 2379 2392 2380 - func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 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) { 2381 2394 formatPatches, err := patchutil.ExtractPatches(patch) 2382 2395 if err != nil { 2383 2396 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2402 2415 body := fp.Body 2403 2416 rkey := tid.TID() 2404 2417 2418 + mentions, references := s.refResolver.Resolve(ctx, body) 2419 + 2405 2420 initialSubmission := models.PullSubmission{ 2406 2421 Patch: fp.Raw, 2407 2422 SourceRev: fp.SHA, ··· 2414 2429 OwnerDid: user.Did, 2415 2430 RepoAt: f.RepoAt(), 2416 2431 Rkey: rkey, 2432 + Mentions: mentions, 2433 + References: references, 2417 2434 Submissions: []*models.PullSubmission{ 2418 2435 &initialSubmission, 2419 2436 },

History

12 rounds 1 comment
sign up or login to add to the discussion
1 commit
expand
appview: backlinks
3/3 success
expand
expand 1 comment

tested this locally: and all edge cases i can think of are handled pretty well! the rendering of backlinks also looks good. thanks for the brilliant work on this!

pull request successfully merged
1 commit
expand
appview: backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
appview: backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
appview: backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
appview: backlinks
1/3 failed, 2/3 timeout
expand
expand 0 comments
1 commit
expand
draft: render backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
draft: render backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
draft: render backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
draft: render backlinks
3/3 success
expand
expand 0 comments
1 commit
expand
draft: render backlinks
2/3 failed, 1/3 running
expand
expand 0 comments
1 commit
expand
draft: render backlinks
1/3 failed, 2/3 timeout
expand
expand 0 comments
1 commit
expand
draft: render backlinks
1/3 failed, 2/3 timeout
expand
expand 0 comments