Monorepo for Tangled tangled.org
at sl/pdsmigration 912 lines 21 kB view raw
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.RepoAt == nil && new.RepoAt == nil { 34 return true 35 } 36 if existing.RepoAt == nil || new.RepoAt == nil { 37 return false 38 } 39 return *existing.RepoAt == *new.RepoAt 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_at, next_pull_id) 64 values (?, 1) 65 `, pull.RepoAt) 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.RepoAt == pull.RepoAt && 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_at, next_pull_id) 123 values (?, 1) 124 `, pull.RepoAt) 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_at = ? 134 returning next_pull_id - 1 135 `, pull.RepoAt).Scan(&nextId) 136 if err != nil { 137 return err 138 } 139 140 pull.PullId = nextId 141 pull.State = models.PullOpen 142 143 var sourceBranch, sourceRepoAt *string 144 if pull.PullSource != nil { 145 sourceBranch = &pull.PullSource.Branch 146 if pull.PullSource.RepoAt != nil { 147 x := pull.PullSource.RepoAt.String() 148 sourceRepoAt = &x 149 } 150 } 151 152 result, err := tx.Exec( 153 ` 154 insert into pulls ( 155 repo_at, 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_at 166 ) 167 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 168 pull.RepoAt, 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 sourceRepoAt, 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, sourceRepoAt *string 228 if pull.PullSource != nil { 229 sourceBranch = &pull.PullSource.Branch 230 if pull.PullSource.RepoAt != nil { 231 x := pull.PullSource.RepoAt.String() 232 sourceRepoAt = &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_at = ? 244 where owner_did = ? and rkey = ? 245 `, pull.Title, pull.Body, pull.TargetBranch, pull.DependentOn, sourceBranch, sourceRepoAt, 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, repoAt syntax.ATURI) (int, error) { 287 var pullId int 288 err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_at = ?`, repoAt).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_at, 320 pull_id, 321 created, 322 title, 323 state, 324 target_branch, 325 body, 326 rkey, 327 source_branch, 328 source_repo_at, 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, sourceRepoAt, dependentOn sql.NullString 348 err := rows.Scan( 349 &pull.ID, 350 &pull.OwnerDid, 351 &pull.RepoAt, 352 &pull.PullId, 353 &createdAt, 354 &pull.Title, 355 &pull.State, 356 &pull.TargetBranch, 357 &pull.Body, 358 &pull.Rkey, 359 &sourceBranch, 360 &sourceRepoAt, 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 sourceRepoAt.Valid { 378 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 379 if err != nil { 380 return nil, err 381 } 382 pull.PullSource.RepoAt = &sourceRepoAtParsed 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 421 var repoAts []syntax.ATURI 422 for _, p := range pulls { 423 repoAts = append(repoAts, p.RepoAt) 424 if p.PullSource != nil && p.PullSource.RepoAt != nil { 425 repoAts = append(repoAts, *p.PullSource.RepoAt) 426 } 427 } 428 429 repos, err := GetRepos(e, orm.FilterIn("at_uri", repoAts)) 430 if err != nil && !errors.Is(err, sql.ErrNoRows) { 431 return nil, fmt.Errorf("failed to get source repos: %w", err) 432 } 433 434 repoMap := make(map[syntax.ATURI]*models.Repo) 435 for _, r := range repos { 436 repoMap[r.RepoAt()] = &r 437 } 438 439 for _, p := range pulls { 440 if repo, ok := repoMap[p.RepoAt]; ok { 441 p.Repo = repo 442 } 443 444 if p.PullSource != nil && p.PullSource.RepoAt != nil { 445 if sourceRepo, ok := repoMap[*p.PullSource.RepoAt]; ok { 446 p.PullSource.Repo = sourceRepo 447 } 448 } 449 } 450 451 allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts)) 452 if err != nil { 453 return nil, fmt.Errorf("failed to query reference_links: %w", err) 454 } 455 for pullAt, references := range allReferences { 456 if pull, ok := pulls[pullAt]; ok { 457 pull.References = references 458 } 459 } 460 461 orderedByPullId := []*models.Pull{} 462 for _, p := range pulls { 463 orderedByPullId = append(orderedByPullId, p) 464 } 465 sort.Slice(orderedByPullId, func(i, j int) bool { 466 return orderedByPullId[i].PullId > orderedByPullId[j].PullId 467 }) 468 469 return orderedByPullId, nil 470} 471 472func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 473 return GetPullsPaginated(e, pagination.Page{}, filters...) 474} 475 476func GetPull(e Execer, filters ...orm.Filter) (*models.Pull, error) { 477 pulls, err := GetPullsPaginated(e, pagination.Page{Limit: 1}, filters...) 478 if err != nil { 479 return nil, err 480 } 481 if len(pulls) == 0 { 482 return nil, sql.ErrNoRows 483 } 484 485 return pulls[0], nil 486} 487 488// mapping from pull -> pull submissions 489func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 490 var conditions []string 491 var args []any 492 for _, filter := range filters { 493 conditions = append(conditions, filter.Condition()) 494 args = append(args, filter.Arg()...) 495 } 496 497 whereClause := "" 498 if conditions != nil { 499 whereClause = " where " + strings.Join(conditions, " and ") 500 } 501 502 query := fmt.Sprintf(` 503 select 504 id, 505 pull_at, 506 round_number, 507 patch, 508 combined, 509 created, 510 source_rev, 511 patch_blob_ref, 512 patch_blob_mime, 513 patch_blob_size 514 from 515 pull_submissions 516 %s 517 order by 518 round_number asc 519 `, whereClause) 520 521 rows, err := e.Query(query, args...) 522 if err != nil { 523 return nil, err 524 } 525 defer rows.Close() 526 527 pullMap := make(map[syntax.ATURI][]*models.PullSubmission) 528 529 for rows.Next() { 530 var submission models.PullSubmission 531 var submissionCreatedStr string 532 var submissionSourceRev, submissionCombined sql.Null[string] 533 var patchBlobRef, patchBlobMime sql.Null[string] 534 var patchBlobSize sql.Null[int64] 535 err := rows.Scan( 536 &submission.ID, 537 &submission.PullAt, 538 &submission.RoundNumber, 539 &submission.Patch, 540 &submissionCombined, 541 &submissionCreatedStr, 542 &submissionSourceRev, 543 &patchBlobRef, 544 &patchBlobMime, 545 &patchBlobSize, 546 ) 547 if err != nil { 548 return nil, err 549 } 550 551 if t, err := time.Parse(time.RFC3339, submissionCreatedStr); err == nil { 552 submission.Created = t 553 } 554 555 if submissionSourceRev.Valid { 556 submission.SourceRev = submissionSourceRev.V 557 } 558 559 if submissionCombined.Valid { 560 submission.Combined = submissionCombined.V 561 } 562 563 if patchBlobRef.Valid { 564 submission.Blob.Ref = lexutil.LexLink(cid.MustParse(patchBlobRef.V)) 565 } 566 567 if patchBlobMime.Valid { 568 submission.Blob.MimeType = patchBlobMime.V 569 } 570 571 if patchBlobSize.Valid { 572 submission.Blob.Size = patchBlobSize.V 573 } 574 575 pullMap[submission.PullAt] = append(pullMap[submission.PullAt], &submission) 576 } 577 578 if err := rows.Err(); err != nil { 579 return nil, err 580 } 581 582 // Get comments for all submissions using GetComments 583 pullAts := slices.Collect(maps.Keys(pullMap)) 584 comments, err := GetComments(e, orm.FilterIn("subject_uri", pullAts)) 585 if err != nil { 586 return nil, fmt.Errorf("failed to get pull comments: %w", err) 587 } 588 for _, comment := range comments { 589 if comment.PullRoundIdx != nil { 590 roundIdx := *comment.PullRoundIdx 591 if submissions, ok := pullMap[syntax.ATURI(comment.Subject.Uri)]; ok { 592 if roundIdx < len(submissions) { 593 submission := submissions[roundIdx] 594 submission.Comments = append(submission.Comments, comment) 595 } 596 } 597 } 598 } 599 600 // sort each one by round number 601 for _, s := range pullMap { 602 slices.SortFunc(s, func(a, b *models.PullSubmission) int { 603 return cmp.Compare(a.RoundNumber, b.RoundNumber) 604 }) 605 } 606 607 return pullMap, nil 608} 609 610// timeframe here is directly passed into the sql query filter, and any 611// timeframe in the past should be negative; e.g.: "-3 months" 612func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) { 613 var pulls []models.Pull 614 615 rows, err := e.Query(` 616 select 617 p.owner_did, 618 p.repo_at, 619 p.pull_id, 620 p.created, 621 p.title, 622 p.state, 623 r.did, 624 r.name, 625 r.knot, 626 r.rkey, 627 r.created 628 from 629 pulls p 630 join 631 repos r on p.repo_at = r.at_uri 632 where 633 p.owner_did = ? and p.created >= date ('now', ?) 634 order by 635 p.created desc`, did, timeframe) 636 if err != nil { 637 return nil, err 638 } 639 defer rows.Close() 640 641 for rows.Next() { 642 var pull models.Pull 643 var repo models.Repo 644 var pullCreatedAt, repoCreatedAt string 645 err := rows.Scan( 646 &pull.OwnerDid, 647 &pull.RepoAt, 648 &pull.PullId, 649 &pullCreatedAt, 650 &pull.Title, 651 &pull.State, 652 &repo.Did, 653 &repo.Name, 654 &repo.Knot, 655 &repo.Rkey, 656 &repoCreatedAt, 657 ) 658 if err != nil { 659 return nil, err 660 } 661 662 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 663 if err != nil { 664 return nil, err 665 } 666 pull.Created = pullCreatedTime 667 668 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 669 if err != nil { 670 return nil, err 671 } 672 repo.Created = repoCreatedTime 673 674 pull.Repo = &repo 675 676 pulls = append(pulls, pull) 677 } 678 679 if err := rows.Err(); err != nil { 680 return nil, err 681 } 682 683 return pulls, nil 684} 685 686// use with transaction 687func SetPullsState(e Execer, pullState models.PullState, filters ...orm.Filter) error { 688 var conditions []string 689 var args []any 690 691 args = append(args, pullState) 692 for _, filter := range filters { 693 conditions = append(conditions, filter.Condition()) 694 args = append(args, filter.Arg()...) 695 } 696 args = append(args, models.PullAbandoned) // only update state of non-deleted pulls 697 args = append(args, models.PullMerged) // only update state of non-merged pulls 698 699 whereClause := "" 700 if conditions != nil { 701 whereClause = " where " + strings.Join(conditions, " and ") 702 } 703 704 query := fmt.Sprintf("update pulls set state = ? %s and state <> ? and state <> ?", whereClause) 705 706 _, err := e.Exec(query, args...) 707 return err 708} 709 710func ClosePulls(e Execer, filters ...orm.Filter) error { 711 return SetPullsState(e, models.PullClosed, filters...) 712} 713 714func ReopenPulls(e Execer, filters ...orm.Filter) error { 715 return SetPullsState(e, models.PullOpen, filters...) 716} 717 718func MergePulls(e Execer, filters ...orm.Filter) error { 719 return SetPullsState(e, models.PullMerged, filters...) 720} 721 722func AbandonPulls(e Execer, filters ...orm.Filter) error { 723 return SetPullsState(e, models.PullAbandoned, filters...) 724} 725 726func ResubmitPull( 727 e Execer, 728 pullAt syntax.ATURI, 729 newRoundNumber int, 730 newPatch string, 731 combinedPatch string, 732 newSourceRev string, 733 blob *lexutil.LexBlob, 734) error { 735 _, err := e.Exec(` 736 insert into pull_submissions ( 737 pull_at, 738 round_number, 739 patch, 740 combined, 741 source_rev, 742 patch_blob_ref, 743 patch_blob_mime, 744 patch_blob_size 745 ) 746 values (?, ?, ?, ?, ?, ?, ?, ?) 747 `, pullAt, newRoundNumber, newPatch, combinedPatch, newSourceRev, blob.Ref.String(), blob.MimeType, blob.Size) 748 749 return err 750} 751 752func SetDependentOn(e Execer, dependentOn syntax.ATURI, filters ...orm.Filter) error { 753 var conditions []string 754 var args []any 755 756 args = append(args, dependentOn) 757 758 for _, filter := range filters { 759 conditions = append(conditions, filter.Condition()) 760 args = append(args, filter.Arg()...) 761 } 762 763 whereClause := "" 764 if conditions != nil { 765 whereClause = " where " + strings.Join(conditions, " and ") 766 } 767 768 query := fmt.Sprintf("update pulls set dependent_on = ? %s", whereClause) 769 _, err := e.Exec(query, args...) 770 771 return err 772} 773 774func GetPullCount(e Execer, repoAt syntax.ATURI) (models.PullCount, error) { 775 row := e.QueryRow(` 776 select 777 count(case when state = ? then 1 end) as open_count, 778 count(case when state = ? then 1 end) as merged_count, 779 count(case when state = ? then 1 end) as closed_count, 780 count(case when state = ? then 1 end) as deleted_count 781 from pulls 782 where repo_at = ?`, 783 models.PullOpen, 784 models.PullMerged, 785 models.PullClosed, 786 models.PullAbandoned, 787 repoAt, 788 ) 789 790 var count models.PullCount 791 if err := row.Scan(&count.Open, &count.Merged, &count.Closed, &count.Deleted); err != nil { 792 return models.PullCount{Open: 0, Merged: 0, Closed: 0, Deleted: 0}, err 793 } 794 795 return count, nil 796} 797 798// change-id dependent_on 799// 800// 4 w ,-------- at_uri(z) (TOP) 801// 3 z <----',------- at_uri(y) 802// 2 y <-----',------ at_uri(x) 803// 1 x <------' nil (BOT) 804// 805// `w` has no dependents, so it is the top of the stack 806// 807// this unfortunately does a db query for *each* pull of the stack, 808// ideally this would be a recursive query, but in the interest of implementation simplicity, 809// we took the less performant route 810// 811// TODO: make this less bad 812func GetStack(e Execer, atUri syntax.ATURI) (models.Stack, error) { 813 // first get the pull for the given at-uri 814 pull, err := GetPull(e, orm.FilterEq("at_uri", atUri)) 815 if err != nil { 816 return nil, err 817 } 818 819 // Collect all pulls in the stack by traversing up and down 820 allPulls := []*models.Pull{pull} 821 visited := sets.New[syntax.ATURI]() 822 823 // Traverse up to find all dependents 824 current := pull 825 for { 826 dependent, err := GetPull(e, 827 orm.FilterEq("dependent_on", current.AtUri()), 828 orm.FilterNotEq("state", models.PullAbandoned), 829 ) 830 if err != nil || dependent == nil { 831 break 832 } 833 if visited.Contains(dependent.AtUri()) { 834 return allPulls, fmt.Errorf("circular dependency detected in stack") 835 } 836 allPulls = append(allPulls, dependent) 837 visited.Insert(dependent.AtUri()) 838 current = dependent 839 } 840 841 // Traverse down to find all dependencies 842 current = pull 843 for current.DependentOn != nil { 844 dependency, err := GetPull( 845 e, 846 orm.FilterEq("at_uri", current.DependentOn), 847 orm.FilterNotEq("state", models.PullAbandoned), 848 ) 849 850 if err != nil { 851 return allPulls, fmt.Errorf("failed to find parent pull request, stack is malformed, missing PR: %s", current.DependentOn) 852 } 853 if visited.Contains(dependency.AtUri()) { 854 return allPulls, fmt.Errorf("circular dependency detected in stack") 855 } 856 allPulls = append(allPulls, dependency) 857 visited.Insert(dependency.AtUri()) 858 current = dependency 859 } 860 861 // sort the list: find the top and build ordered list 862 atUriMap := make(map[syntax.ATURI]*models.Pull, len(allPulls)) 863 dependentMap := make(map[syntax.ATURI]*models.Pull, len(allPulls)) 864 865 for _, p := range allPulls { 866 atUriMap[p.AtUri()] = p 867 if p.DependentOn != nil { 868 dependentMap[*p.DependentOn] = p 869 } 870 } 871 872 // the top of the stack is the pull that no other pull depends on 873 var topPull *models.Pull 874 for _, maybeTop := range allPulls { 875 if _, ok := dependentMap[maybeTop.AtUri()]; !ok { 876 topPull = maybeTop 877 break 878 } 879 } 880 881 pulls := []*models.Pull{} 882 for { 883 pulls = append(pulls, topPull) 884 if topPull.DependentOn != nil { 885 if next, ok := atUriMap[*topPull.DependentOn]; ok { 886 topPull = next 887 } else { 888 return pulls, fmt.Errorf("failed to find parent pull request, stack is malformed") 889 } 890 } else { 891 break 892 } 893 } 894 895 return pulls, nil 896} 897 898func GetAbandonedPulls(e Execer, atUri syntax.ATURI) ([]*models.Pull, error) { 899 stack, err := GetStack(e, atUri) 900 if err != nil { 901 return nil, err 902 } 903 904 var abandoned []*models.Pull 905 for _, p := range stack { 906 if p.State == models.PullAbandoned { 907 abandoned = append(abandoned, p) 908 } 909 } 910 911 return abandoned, nil 912}