This repository has no description
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}