Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1package db 2 3import ( 4 "cmp" 5 "database/sql" 6 "errors" 7 "fmt" 8 "maps" 9 "slices" 10 "sort" 11 "strings" 12 "time" 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 "github.com/ipfs/go-cid" 17 "tangled.org/core/appview/models" 18 "tangled.org/core/appview/pagination" 19 "tangled.org/core/orm" 20 "tangled.org/core/sets" 21) 22 23func comparePullSource(existing, new *models.PullSource) bool { 24 if existing == nil && new == nil { 25 return true 26 } 27 if existing == nil || new == nil { 28 return false 29 } 30 if existing.Branch != new.Branch { 31 return false 32 } 33 if existing.RepoDid == nil && new.RepoDid == nil { 34 return true 35 } 36 if existing.RepoDid == nil || new.RepoDid == nil { 37 return false 38 } 39 return *existing.RepoDid == *new.RepoDid 40} 41 42func compareSubmissions(existing, new []*models.PullSubmission) bool { 43 if len(existing) != len(new) { 44 return false 45 } 46 for i := range existing { 47 if existing[i].Blob.Ref.String() != new[i].Blob.Ref.String() { 48 return false 49 } 50 if existing[i].Blob.MimeType != new[i].Blob.MimeType { 51 return false 52 } 53 if existing[i].Blob.Size != new[i].Blob.Size { 54 return false 55 } 56 } 57 return true 58} 59 60func PutPull(tx *sql.Tx, pull *models.Pull) error { 61 // ensure sequence exists 62 _, err := tx.Exec(` 63 insert or ignore into repo_pull_seqs (repo_did, next_pull_id) 64 values (?, 1) 65 `, pull.RepoDid) 66 if err != nil { 67 return err 68 } 69 70 pulls, err := GetPulls( 71 tx, 72 orm.FilterEq("owner_did", pull.OwnerDid), 73 orm.FilterEq("rkey", pull.Rkey), 74 ) 75 switch { 76 case err != nil: 77 return err 78 case len(pulls) == 0: 79 return createNewPull(tx, pull) 80 case len(pulls) != 1: // should be unreachable 81 return fmt.Errorf("invalid number of pulls returned: %d", len(pulls)) 82 default: 83 existingPull := pulls[0] 84 if existingPull.State == models.PullMerged { 85 return nil 86 } 87 88 dependentOnEqual := (existingPull.DependentOn == nil && pull.DependentOn == nil) || 89 (existingPull.DependentOn != nil && pull.DependentOn != nil && *existingPull.DependentOn == *pull.DependentOn) 90 91 pullSourceEqual := comparePullSource(existingPull.PullSource, pull.PullSource) 92 submissionsEqual := compareSubmissions(existingPull.Submissions, pull.Submissions) 93 94 if existingPull.Title == pull.Title && 95 existingPull.Body == pull.Body && 96 existingPull.TargetBranch == pull.TargetBranch && 97 existingPull.RepoDid == pull.RepoDid && 98 dependentOnEqual && 99 pullSourceEqual && 100 submissionsEqual { 101 return nil 102 } 103 104 isLonger := len(existingPull.Submissions) < len(pull.Submissions) 105 if isLonger { 106 isAppendOnly := compareSubmissions(existingPull.Submissions, pull.Submissions[:len(existingPull.Submissions)]) 107 if !isAppendOnly { 108 return fmt.Errorf("the new pull does not treat submissions as append-only") 109 } 110 } else if !submissionsEqual { 111 return fmt.Errorf("the new pull does not treat submissions as append-only") 112 } 113 114 pull.ID = existingPull.ID 115 pull.PullId = existingPull.PullId 116 return updatePull(tx, pull, existingPull) 117 } 118} 119 120func createNewPull(tx *sql.Tx, pull *models.Pull) error { 121 _, err := tx.Exec(` 122 insert or ignore into repo_pull_seqs (repo_did, next_pull_id) 123 values (?, 1) 124 `, pull.RepoDid) 125 if err != nil { 126 return err 127 } 128 129 var nextId int 130 err = tx.QueryRow(` 131 update repo_pull_seqs 132 set next_pull_id = next_pull_id + 1 133 where repo_did = ? 134 returning next_pull_id - 1 135 `, pull.RepoDid).Scan(&nextId) 136 if err != nil { 137 return err 138 } 139 140 pull.PullId = nextId 141 pull.State = models.PullOpen 142 143 var sourceBranch, sourceRepoDid *string 144 if pull.PullSource != nil { 145 sourceBranch = &pull.PullSource.Branch 146 if pull.PullSource.RepoDid != nil { 147 x := string(*pull.PullSource.RepoDid) 148 sourceRepoDid = &x 149 } 150 } 151 152 result, err := tx.Exec( 153 ` 154 insert into pulls ( 155 repo_did, 156 owner_did, 157 pull_id, 158 title, 159 target_branch, 160 body, 161 rkey, 162 state, 163 dependent_on, 164 source_branch, 165 source_repo_did 166 ) 167 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 168 pull.RepoDid, 169 pull.OwnerDid, 170 pull.PullId, 171 pull.Title, 172 pull.TargetBranch, 173 pull.Body, 174 pull.Rkey, 175 pull.State, 176 pull.DependentOn, 177 sourceBranch, 178 sourceRepoDid, 179 ) 180 if err != nil { 181 return err 182 } 183 184 // Set the database primary key ID 185 id, err := result.LastInsertId() 186 if err != nil { 187 return err 188 } 189 pull.ID = int(id) 190 191 for i, s := range pull.Submissions { 192 _, err = tx.Exec(` 193 insert into pull_submissions ( 194 pull_at, 195 round_number, 196 patch, 197 combined, 198 source_rev, 199 patch_blob_ref, 200 patch_blob_mime, 201 patch_blob_size 202 ) 203 values (?, ?, ?, ?, ?, ?, ?, ?) 204 `, 205 pull.AtUri(), 206 i, 207 s.Patch, 208 s.Combined, 209 s.SourceRev, 210 s.Blob.Ref.String(), 211 s.Blob.MimeType, 212 s.Blob.Size, 213 ) 214 if err != nil { 215 return err 216 } 217 } 218 219 if err := putReferences(tx, pull.AtUri(), pull.References); err != nil { 220 return fmt.Errorf("put reference_links: %w", err) 221 } 222 223 return nil 224} 225 226func updatePull(tx *sql.Tx, pull *models.Pull, existingPull *models.Pull) error { 227 var sourceBranch, sourceRepoDid *string 228 if pull.PullSource != nil { 229 sourceBranch = &pull.PullSource.Branch 230 if pull.PullSource.RepoDid != nil { 231 x := string(*pull.PullSource.RepoDid) 232 sourceRepoDid = &x 233 } 234 } 235 236 _, err := tx.Exec(` 237 update pulls set 238 title = ?, 239 body = ?, 240 target_branch = ?, 241 dependent_on = ?, 242 source_branch = ?, 243 source_repo_did = ? 244 where owner_did = ? and rkey = ? 245 `, pull.Title, pull.Body, pull.TargetBranch, pull.DependentOn, sourceBranch, sourceRepoDid, pull.OwnerDid, pull.Rkey) 246 if err != nil { 247 return err 248 } 249 250 // insert new submissions (append-only) 251 for i := len(existingPull.Submissions); i < len(pull.Submissions); i++ { 252 s := pull.Submissions[i] 253 _, err = tx.Exec(` 254 insert into pull_submissions ( 255 pull_at, 256 round_number, 257 patch, 258 combined, 259 source_rev, 260 patch_blob_ref, 261 patch_blob_mime, 262 patch_blob_size 263 ) 264 values (?, ?, ?, ?, ?, ?, ?, ?) 265 `, 266 pull.AtUri(), 267 i, 268 s.Patch, 269 s.Combined, 270 s.SourceRev, 271 s.Blob.Ref.String(), 272 s.Blob.MimeType, 273 s.Blob.Size, 274 ) 275 if err != nil { 276 return err 277 } 278 } 279 280 if err := putReferences(tx, pull.AtUri(), pull.References); err != nil { 281 return fmt.Errorf("put reference_links: %w", err) 282 } 283 return nil 284} 285 286func NextPullId(e Execer, repoDid string) (int, error) { 287 var pullId int 288 err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_did = ?`, repoDid).Scan(&pullId) 289 return pullId - 1, err 290} 291 292func GetPullsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Pull, error) { 293 pulls := make(map[syntax.ATURI]*models.Pull) 294 295 var conditions []string 296 var args []any 297 for _, filter := range filters { 298 conditions = append(conditions, filter.Condition()) 299 args = append(args, filter.Arg()...) 300 } 301 302 whereClause := "" 303 if conditions != nil { 304 whereClause = " where " + strings.Join(conditions, " and ") 305 } 306 pageClause := "" 307 if page.Limit != 0 { 308 pageClause = fmt.Sprintf( 309 " limit %d offset %d ", 310 page.Limit, 311 page.Offset, 312 ) 313 } 314 315 query := fmt.Sprintf(` 316 select 317 id, 318 owner_did, 319 repo_did, 320 pull_id, 321 created, 322 title, 323 state, 324 target_branch, 325 body, 326 rkey, 327 source_branch, 328 source_repo_did, 329 dependent_on 330 from 331 pulls 332 %s 333 order by 334 created desc 335 %s 336 `, whereClause, pageClause) 337 338 rows, err := e.Query(query, args...) 339 if err != nil { 340 return nil, err 341 } 342 defer rows.Close() 343 344 for rows.Next() { 345 var pull models.Pull 346 var createdAt string 347 var sourceBranch, sourceRepoDid, dependentOn sql.NullString 348 err := rows.Scan( 349 &pull.ID, 350 &pull.OwnerDid, 351 &pull.RepoDid, 352 &pull.PullId, 353 &createdAt, 354 &pull.Title, 355 &pull.State, 356 &pull.TargetBranch, 357 &pull.Body, 358 &pull.Rkey, 359 &sourceBranch, 360 &sourceRepoDid, 361 &dependentOn, 362 ) 363 if err != nil { 364 return nil, err 365 } 366 367 createdTime, err := time.Parse(time.RFC3339, createdAt) 368 if err != nil { 369 return nil, err 370 } 371 pull.Created = createdTime 372 373 if sourceBranch.Valid { 374 pull.PullSource = &models.PullSource{ 375 Branch: sourceBranch.String, 376 } 377 if sourceRepoDid.Valid { 378 sourceRepoDidParsed, err := syntax.ParseDID(sourceRepoDid.String) 379 if err != nil { 380 return nil, err 381 } 382 pull.PullSource.RepoDid = &sourceRepoDidParsed 383 } 384 } 385 386 if dependentOn.Valid { 387 x := syntax.ATURI(dependentOn.String) 388 pull.DependentOn = &x 389 } 390 391 pulls[pull.AtUri()] = &pull 392 } 393 394 var pullAts []syntax.ATURI 395 for _, p := range pulls { 396 pullAts = append(pullAts, p.AtUri()) 397 } 398 submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts)) 399 if err != nil { 400 return nil, fmt.Errorf("failed to get submissions: %w", err) 401 } 402 403 for pullAt, submissions := range submissionsMap { 404 if p, ok := pulls[pullAt]; ok { 405 p.Submissions = submissions 406 } 407 } 408 409 // collect allLabels for each issue 410 allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts)) 411 if err != nil { 412 return nil, fmt.Errorf("failed to query labels: %w", err) 413 } 414 for pullAt, labels := range allLabels { 415 if p, ok := pulls[pullAt]; ok { 416 p.Labels = labels 417 } 418 } 419 420 // build up reverse mappings: p.Repo and p.PullSource.Repo 421 var repoDids []syntax.DID 422 for _, p := range pulls { 423 repoDids = append(repoDids, p.RepoDid) 424 if p.PullSource != nil && p.PullSource.RepoDid != nil { 425 repoDids = append(repoDids, *p.PullSource.RepoDid) 426 } 427 } 428 429 repos, err := GetRepos(e, orm.FilterIn("repo_did", repoDids)) 430 if err != nil && !errors.Is(err, sql.ErrNoRows) { 431 return nil, fmt.Errorf("failed to get repos: %w", err) 432 } 433 434 repoMap := make(map[syntax.DID]*models.Repo) 435 for _, r := range repos { 436 repoMap[syntax.DID(r.RepoDid)] = &r 437 } 438 439 for _, p := range pulls { 440 if repo, ok := repoMap[p.RepoDid]; ok { 441 p.Repo = repo 442 } 443 if p.PullSource != nil && p.PullSource.RepoDid != nil { 444 if sourceRepo, ok := repoMap[*p.PullSource.RepoDid]; ok { 445 p.PullSource.Repo = sourceRepo 446 } 447 } 448 } 449 450 allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts)) 451 if err != nil { 452 return nil, fmt.Errorf("failed to query reference_links: %w", err) 453 } 454 for pullAt, references := range allReferences { 455 if pull, ok := pulls[pullAt]; ok { 456 pull.References = references 457 } 458 } 459 460 orderedByPullId := []*models.Pull{} 461 for _, p := range pulls { 462 orderedByPullId = append(orderedByPullId, p) 463 } 464 sort.Slice(orderedByPullId, func(i, j int) bool { 465 return orderedByPullId[i].PullId > orderedByPullId[j].PullId 466 }) 467 468 return orderedByPullId, nil 469} 470 471func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 472 return GetPullsPaginated(e, pagination.Page{}, filters...) 473} 474 475func GetPull(e Execer, filters ...orm.Filter) (*models.Pull, error) { 476 pulls, err := GetPullsPaginated(e, pagination.Page{Limit: 1}, filters...) 477 if err != nil { 478 return nil, err 479 } 480 if len(pulls) == 0 { 481 return nil, sql.ErrNoRows 482 } 483 484 return pulls[0], nil 485} 486 487// mapping from pull -> pull submissions 488func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 489 var conditions []string 490 var args []any 491 for _, filter := range filters { 492 conditions = append(conditions, filter.Condition()) 493 args = append(args, filter.Arg()...) 494 } 495 496 whereClause := "" 497 if conditions != nil { 498 whereClause = " where " + strings.Join(conditions, " and ") 499 } 500 501 query := fmt.Sprintf(` 502 select 503 id, 504 pull_at, 505 round_number, 506 patch, 507 combined, 508 created, 509 source_rev, 510 patch_blob_ref, 511 patch_blob_mime, 512 patch_blob_size 513 from 514 pull_submissions 515 %s 516 order by 517 round_number asc 518 `, whereClause) 519 520 rows, err := e.Query(query, args...) 521 if err != nil { 522 return nil, err 523 } 524 defer rows.Close() 525 526 pullMap := make(map[syntax.ATURI][]*models.PullSubmission) 527 528 for rows.Next() { 529 var submission models.PullSubmission 530 var submissionCreatedStr string 531 var submissionSourceRev, submissionCombined sql.Null[string] 532 var patchBlobRef, patchBlobMime sql.Null[string] 533 var patchBlobSize sql.Null[int64] 534 err := rows.Scan( 535 &submission.ID, 536 &submission.PullAt, 537 &submission.RoundNumber, 538 &submission.Patch, 539 &submissionCombined, 540 &submissionCreatedStr, 541 &submissionSourceRev, 542 &patchBlobRef, 543 &patchBlobMime, 544 &patchBlobSize, 545 ) 546 if err != nil { 547 return nil, err 548 } 549 550 if t, err := time.Parse(time.RFC3339, submissionCreatedStr); err == nil { 551 submission.Created = t 552 } 553 554 if submissionSourceRev.Valid { 555 submission.SourceRev = submissionSourceRev.V 556 } 557 558 if submissionCombined.Valid { 559 submission.Combined = submissionCombined.V 560 } 561 562 if patchBlobRef.Valid { 563 submission.Blob.Ref = lexutil.LexLink(cid.MustParse(patchBlobRef.V)) 564 } 565 566 if patchBlobMime.Valid { 567 submission.Blob.MimeType = patchBlobMime.V 568 } 569 570 if patchBlobSize.Valid { 571 submission.Blob.Size = patchBlobSize.V 572 } 573 574 pullMap[submission.PullAt] = append(pullMap[submission.PullAt], &submission) 575 } 576 577 if err := rows.Err(); err != nil { 578 return nil, err 579 } 580 581 // Get comments for all submissions using GetComments 582 pullAts := slices.Collect(maps.Keys(pullMap)) 583 comments, err := GetComments(e, orm.FilterIn("subject_uri", pullAts)) 584 if err != nil { 585 return nil, fmt.Errorf("failed to get pull comments: %w", err) 586 } 587 for _, comment := range comments { 588 if comment.PullRoundIdx != nil { 589 roundIdx := *comment.PullRoundIdx 590 if submissions, ok := pullMap[syntax.ATURI(comment.Subject.Uri)]; ok { 591 if roundIdx < len(submissions) { 592 submission := submissions[roundIdx] 593 submission.Comments = append(submission.Comments, comment) 594 } 595 } 596 } 597 } 598 599 // sort each one by round number 600 for _, s := range pullMap { 601 slices.SortFunc(s, func(a, b *models.PullSubmission) int { 602 return cmp.Compare(a.RoundNumber, b.RoundNumber) 603 }) 604 } 605 606 return pullMap, nil 607} 608 609// timeframe here is directly passed into the sql query filter, and any 610// timeframe in the past should be negative; e.g.: "-3 months" 611func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) { 612 var pulls []models.Pull 613 614 rows, err := e.Query(` 615 select 616 p.owner_did, 617 p.repo_did, 618 p.pull_id, 619 p.created, 620 p.title, 621 p.state, 622 r.did, 623 r.name, 624 r.knot, 625 r.rkey, 626 r.created 627 from 628 pulls p 629 join 630 repos r on p.repo_did = r.repo_did 631 where 632 p.owner_did = ? and p.created >= date ('now', ?) 633 order by 634 p.created desc`, did, timeframe) 635 if err != nil { 636 return nil, err 637 } 638 defer rows.Close() 639 640 for rows.Next() { 641 var pull models.Pull 642 var repo models.Repo 643 var pullCreatedAt, repoCreatedAt string 644 err := rows.Scan( 645 &pull.OwnerDid, 646 &pull.RepoDid, 647 &pull.PullId, 648 &pullCreatedAt, 649 &pull.Title, 650 &pull.State, 651 &repo.Did, 652 &repo.Name, 653 &repo.Knot, 654 &repo.Rkey, 655 &repoCreatedAt, 656 ) 657 if err != nil { 658 return nil, err 659 } 660 661 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 662 if err != nil { 663 return nil, err 664 } 665 pull.Created = pullCreatedTime 666 667 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 668 if err != nil { 669 return nil, err 670 } 671 repo.Created = repoCreatedTime 672 673 pull.Repo = &repo 674 675 pulls = append(pulls, pull) 676 } 677 678 if err := rows.Err(); err != nil { 679 return nil, err 680 } 681 682 return pulls, nil 683} 684 685// use with transaction 686func SetPullsState(e Execer, pullState models.PullState, filters ...orm.Filter) error { 687 var conditions []string 688 var args []any 689 690 args = append(args, pullState) 691 for _, filter := range filters { 692 conditions = append(conditions, filter.Condition()) 693 args = append(args, filter.Arg()...) 694 } 695 args = append(args, models.PullAbandoned) // only update state of non-deleted pulls 696 args = append(args, models.PullMerged) // only update state of non-merged pulls 697 698 whereClause := "" 699 if conditions != nil { 700 whereClause = " where " + strings.Join(conditions, " and ") 701 } 702 703 query := fmt.Sprintf("update pulls set state = ? %s and state <> ? and state <> ?", whereClause) 704 705 _, err := e.Exec(query, args...) 706 return err 707} 708 709func ClosePulls(e Execer, filters ...orm.Filter) error { 710 return SetPullsState(e, models.PullClosed, filters...) 711} 712 713func ReopenPulls(e Execer, filters ...orm.Filter) error { 714 return SetPullsState(e, models.PullOpen, filters...) 715} 716 717func MergePulls(e Execer, filters ...orm.Filter) error { 718 return SetPullsState(e, models.PullMerged, filters...) 719} 720 721func AbandonPulls(e Execer, filters ...orm.Filter) error { 722 return SetPullsState(e, models.PullAbandoned, filters...) 723} 724 725func ResubmitPull( 726 e Execer, 727 pullAt syntax.ATURI, 728 newRoundNumber int, 729 newPatch string, 730 combinedPatch string, 731 newSourceRev string, 732 blob *lexutil.LexBlob, 733) error { 734 _, err := e.Exec(` 735 insert into pull_submissions ( 736 pull_at, 737 round_number, 738 patch, 739 combined, 740 source_rev, 741 patch_blob_ref, 742 patch_blob_mime, 743 patch_blob_size 744 ) 745 values (?, ?, ?, ?, ?, ?, ?, ?) 746 `, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Ref.String(), blob.MimeType, blob.Size) 747 748 return err 749} 750 751func SetDependentOn(e Execer, dependentOn syntax.ATURI, filters ...orm.Filter) error { 752 var conditions []string 753 var args []any 754 755 args = append(args, dependentOn) 756 757 for _, filter := range filters { 758 conditions = append(conditions, filter.Condition()) 759 args = append(args, filter.Arg()...) 760 } 761 762 whereClause := "" 763 if conditions != nil { 764 whereClause = " where " + strings.Join(conditions, " and ") 765 } 766 767 query := fmt.Sprintf("update pulls set dependent_on = ? %s", whereClause) 768 _, err := e.Exec(query, args...) 769 770 return err 771} 772 773func GetPullCount(e Execer, repoDid string) (models.PullCount, error) { 774 row := e.QueryRow(` 775 select 776 count(case when state = ? then 1 end) as open_count, 777 count(case when state = ? then 1 end) as merged_count, 778 count(case when state = ? then 1 end) as closed_count, 779 count(case when state = ? then 1 end) as deleted_count 780 from pulls 781 where repo_did = ?`, 782 models.PullOpen, 783 models.PullMerged, 784 models.PullClosed, 785 models.PullAbandoned, 786 repoDid, 787 ) 788 789 var count models.PullCount 790 if err := row.Scan(&count.Open, &count.Merged, &count.Closed, &count.Deleted); err != nil { 791 return models.PullCount{Open: 0, Merged: 0, Closed: 0, Deleted: 0}, err 792 } 793 794 return count, nil 795} 796 797// change-id dependent_on 798// 799// 4 w ,-------- at_uri(z) (TOP) 800// 3 z <----',------- at_uri(y) 801// 2 y <-----',------ at_uri(x) 802// 1 x <------' nil (BOT) 803// 804// `w` has no dependents, so it is the top of the stack 805// 806// this unfortunately does a db query for *each* pull of the stack, 807// ideally this would be a recursive query, but in the interest of implementation simplicity, 808// we took the less performant route 809// 810// TODO: make this less bad 811func GetStack(e Execer, atUri syntax.ATURI) (models.Stack, error) { 812 // first get the pull for the given at-uri 813 pull, err := GetPull(e, orm.FilterEq("at_uri", atUri)) 814 if err != nil { 815 return nil, err 816 } 817 818 // Collect all pulls in the stack by traversing up and down 819 allPulls := []*models.Pull{pull} 820 visited := sets.New[syntax.ATURI]() 821 822 // Traverse up to find all dependents 823 current := pull 824 for { 825 dependent, err := GetPull(e, 826 orm.FilterEq("dependent_on", current.AtUri()), 827 orm.FilterNotEq("state", models.PullAbandoned), 828 ) 829 if err != nil || dependent == nil { 830 break 831 } 832 if visited.Contains(dependent.AtUri()) { 833 return allPulls, fmt.Errorf("circular dependency detected in stack") 834 } 835 allPulls = append(allPulls, dependent) 836 visited.Insert(dependent.AtUri()) 837 current = dependent 838 } 839 840 // Traverse down to find all dependencies 841 current = pull 842 for current.DependentOn != nil { 843 dependency, err := GetPull( 844 e, 845 orm.FilterEq("at_uri", current.DependentOn), 846 orm.FilterNotEq("state", models.PullAbandoned), 847 ) 848 849 if err != nil { 850 return allPulls, fmt.Errorf("failed to find parent pull request, stack is malformed, missing PR: %s", current.DependentOn) 851 } 852 if visited.Contains(dependency.AtUri()) { 853 return allPulls, fmt.Errorf("circular dependency detected in stack") 854 } 855 allPulls = append(allPulls, dependency) 856 visited.Insert(dependency.AtUri()) 857 current = dependency 858 } 859 860 // sort the list: find the top and build ordered list 861 atUriMap := make(map[syntax.ATURI]*models.Pull, len(allPulls)) 862 dependentMap := make(map[syntax.ATURI]*models.Pull, len(allPulls)) 863 864 for _, p := range allPulls { 865 atUriMap[p.AtUri()] = p 866 if p.DependentOn != nil { 867 dependentMap[*p.DependentOn] = p 868 } 869 } 870 871 // the top of the stack is the pull that no other pull depends on 872 var topPull *models.Pull 873 for _, maybeTop := range allPulls { 874 if _, ok := dependentMap[maybeTop.AtUri()]; !ok { 875 topPull = maybeTop 876 break 877 } 878 } 879 880 pulls := []*models.Pull{} 881 for { 882 pulls = append(pulls, topPull) 883 if topPull.DependentOn != nil { 884 if next, ok := atUriMap[*topPull.DependentOn]; ok { 885 topPull = next 886 } else { 887 return pulls, fmt.Errorf("failed to find parent pull request, stack is malformed") 888 } 889 } else { 890 break 891 } 892 } 893 894 return pulls, nil 895} 896 897func GetAbandonedPulls(e Execer, atUri syntax.ATURI) ([]*models.Pull, error) { 898 stack, err := GetStack(e, atUri) 899 if err != nil { 900 return nil, err 901 } 902 903 var abandoned []*models.Pull 904 for _, p := range stack { 905 if p.State == models.PullAbandoned { 906 abandoned = append(abandoned, p) 907 } 908 } 909 910 return abandoned, nil 911}