Monorepo for Tangled
tangled.org
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}