This repository has no description
0

Configure Feed

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

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