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