forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "log" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/bluekeyes/go-gitdiff/gitdiff" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "go.opentelemetry.io/otel/attribute" 15 "go.opentelemetry.io/otel/trace" 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/patchutil" 18 "tangled.sh/tangled.sh/core/types" 19) 20 21type PullState int 22 23const ( 24 PullClosed PullState = iota 25 PullOpen 26 PullMerged 27) 28 29func (p PullState) String() string { 30 switch p { 31 case PullOpen: 32 return "open" 33 case PullMerged: 34 return "merged" 35 case PullClosed: 36 return "closed" 37 default: 38 return "closed" 39 } 40} 41 42func (p PullState) IsOpen() bool { 43 return p == PullOpen 44} 45func (p PullState) IsMerged() bool { 46 return p == PullMerged 47} 48func (p PullState) IsClosed() bool { 49 return p == PullClosed 50} 51 52type Pull struct { 53 // ids 54 ID int 55 PullId int 56 57 // at ids 58 RepoAt syntax.ATURI 59 OwnerDid string 60 Rkey string 61 62 // content 63 Title string 64 Body string 65 TargetBranch string 66 State PullState 67 Submissions []*PullSubmission 68 69 // meta 70 Created time.Time 71 PullSource *PullSource 72 73 // optionally, populate this when querying for reverse mappings 74 Repo *Repo 75} 76 77type PullSource struct { 78 Branch string 79 RepoAt *syntax.ATURI 80 81 // optionally populate this for reverse mappings 82 Repo *Repo 83} 84 85type PullSubmission struct { 86 // ids 87 ID int 88 PullId int 89 90 // at ids 91 RepoAt syntax.ATURI 92 93 // content 94 RoundNumber int 95 Patch string 96 Comments []PullComment 97 SourceRev string // include the rev that was used to create this submission: only for branch PRs 98 99 // meta 100 Created time.Time 101} 102 103type PullComment struct { 104 // ids 105 ID int 106 PullId int 107 SubmissionId int 108 109 // at ids 110 RepoAt string 111 OwnerDid string 112 CommentAt string 113 114 // content 115 Body string 116 117 // meta 118 Created time.Time 119} 120 121func (p *Pull) LatestPatch() string { 122 latestSubmission := p.Submissions[p.LastRoundNumber()] 123 return latestSubmission.Patch 124} 125 126func (p *Pull) PullAt() syntax.ATURI { 127 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey)) 128} 129 130func (p *Pull) LastRoundNumber() int { 131 return len(p.Submissions) - 1 132} 133 134func (p *Pull) IsPatchBased() bool { 135 return p.PullSource == nil 136} 137 138func (p *Pull) IsBranchBased() bool { 139 if p.PullSource != nil { 140 if p.PullSource.RepoAt != nil { 141 return p.PullSource.RepoAt == &p.RepoAt 142 } else { 143 // no repo specified 144 return true 145 } 146 } 147 return false 148} 149 150func (p *Pull) IsForkBased() bool { 151 if p.PullSource != nil { 152 if p.PullSource.RepoAt != nil { 153 // make sure repos are different 154 return p.PullSource.RepoAt != &p.RepoAt 155 } 156 } 157 return false 158} 159 160func (s PullSubmission) AsDiff(targetBranch string) ([]*gitdiff.File, error) { 161 patch := s.Patch 162 163 // if format-patch; then extract each patch 164 var diffs []*gitdiff.File 165 if patchutil.IsFormatPatch(patch) { 166 patches, err := patchutil.ExtractPatches(patch) 167 if err != nil { 168 return nil, err 169 } 170 var ps [][]*gitdiff.File 171 for _, p := range patches { 172 ps = append(ps, p.Files) 173 } 174 175 diffs = patchutil.CombineDiff(ps...) 176 } else { 177 d, _, err := gitdiff.Parse(strings.NewReader(patch)) 178 if err != nil { 179 return nil, err 180 } 181 diffs = d 182 } 183 184 return diffs, nil 185} 186 187func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff { 188 diffs, err := s.AsDiff(targetBranch) 189 if err != nil { 190 log.Println(err) 191 } 192 193 nd := types.NiceDiff{} 194 nd.Commit.Parent = targetBranch 195 196 for _, d := range diffs { 197 ndiff := types.Diff{} 198 ndiff.Name.New = d.NewName 199 ndiff.Name.Old = d.OldName 200 ndiff.IsBinary = d.IsBinary 201 ndiff.IsNew = d.IsNew 202 ndiff.IsDelete = d.IsDelete 203 ndiff.IsCopy = d.IsCopy 204 ndiff.IsRename = d.IsRename 205 206 for _, tf := range d.TextFragments { 207 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 208 for _, l := range tf.Lines { 209 switch l.Op { 210 case gitdiff.OpAdd: 211 nd.Stat.Insertions += 1 212 case gitdiff.OpDelete: 213 nd.Stat.Deletions += 1 214 } 215 } 216 } 217 218 nd.Diff = append(nd.Diff, ndiff) 219 } 220 221 nd.Stat.FilesChanged = len(diffs) 222 223 return nd 224} 225 226func (s PullSubmission) IsFormatPatch() bool { 227 return patchutil.IsFormatPatch(s.Patch) 228} 229 230func (s PullSubmission) AsFormatPatch() []patchutil.FormatPatch { 231 patches, err := patchutil.ExtractPatches(s.Patch) 232 if err != nil { 233 log.Println("error extracting patches from submission:", err) 234 return []patchutil.FormatPatch{} 235 } 236 237 return patches 238} 239 240func NewPull(ctx context.Context, tx *sql.Tx, pull *Pull) error { 241 span := trace.SpanFromContext(ctx) 242 defer span.End() 243 244 span.SetAttributes( 245 attribute.String("repo.at", pull.RepoAt.String()), 246 attribute.String("owner.did", pull.OwnerDid), 247 attribute.String("title", pull.Title), 248 attribute.String("target_branch", pull.TargetBranch), 249 ) 250 span.AddEvent("creating new pull request") 251 252 defer tx.Rollback() 253 254 _, err := tx.Exec(` 255 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 256 values (?, 1) 257 `, pull.RepoAt) 258 if err != nil { 259 span.RecordError(err) 260 return err 261 } 262 263 var nextId int 264 err = tx.QueryRow(` 265 update repo_pull_seqs 266 set next_pull_id = next_pull_id + 1 267 where repo_at = ? 268 returning next_pull_id - 1 269 `, pull.RepoAt).Scan(&nextId) 270 if err != nil { 271 span.RecordError(err) 272 return err 273 } 274 275 pull.PullId = nextId 276 pull.State = PullOpen 277 278 span.SetAttributes(attribute.Int("pull.id", pull.PullId)) 279 span.AddEvent("assigned pull ID") 280 281 var sourceBranch, sourceRepoAt *string 282 if pull.PullSource != nil { 283 sourceBranch = &pull.PullSource.Branch 284 if pull.PullSource.RepoAt != nil { 285 x := pull.PullSource.RepoAt.String() 286 sourceRepoAt = &x 287 } 288 } 289 290 _, err = tx.Exec( 291 ` 292 insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at) 293 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 294 pull.RepoAt, 295 pull.OwnerDid, 296 pull.PullId, 297 pull.Title, 298 pull.TargetBranch, 299 pull.Body, 300 pull.Rkey, 301 pull.State, 302 sourceBranch, 303 sourceRepoAt, 304 ) 305 if err != nil { 306 span.RecordError(err) 307 return err 308 } 309 310 span.AddEvent("inserted pull record") 311 312 _, err = tx.Exec(` 313 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 314 values (?, ?, ?, ?, ?) 315 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 316 if err != nil { 317 span.RecordError(err) 318 return err 319 } 320 321 span.AddEvent("inserted initial pull submission") 322 323 if err := tx.Commit(); err != nil { 324 span.RecordError(err) 325 return err 326 } 327 328 span.AddEvent("transaction committed successfully") 329 return nil 330} 331 332func GetPullAt(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { 333 pull, err := GetPull(ctx, e, repoAt, pullId) 334 if err != nil { 335 return "", err 336 } 337 return pull.PullAt(), err 338} 339 340func NextPullId(e Execer, repoAt syntax.ATURI) (int, error) { 341 var pullId int 342 err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_at = ?`, repoAt).Scan(&pullId) 343 return pullId - 1, err 344} 345 346func GetPulls(ctx context.Context, e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) { 347 span := trace.SpanFromContext(ctx) 348 defer span.End() 349 350 span.SetAttributes( 351 attribute.String("repoAt", repoAt.String()), 352 attribute.String("state", state.String()), 353 ) 354 span.AddEvent("querying pulls") 355 356 pulls := make(map[int]*Pull) 357 358 rows, err := e.QueryContext(ctx, ` 359 select 360 owner_did, 361 pull_id, 362 created, 363 title, 364 state, 365 target_branch, 366 body, 367 rkey, 368 source_branch, 369 source_repo_at 370 from 371 pulls 372 where 373 repo_at = ? and state = ?`, repoAt, state) 374 if err != nil { 375 span.RecordError(err) 376 return nil, err 377 } 378 defer rows.Close() 379 380 for rows.Next() { 381 var pull Pull 382 var createdAt string 383 var sourceBranch, sourceRepoAt sql.NullString 384 err := rows.Scan( 385 &pull.OwnerDid, 386 &pull.PullId, 387 &createdAt, 388 &pull.Title, 389 &pull.State, 390 &pull.TargetBranch, 391 &pull.Body, 392 &pull.Rkey, 393 &sourceBranch, 394 &sourceRepoAt, 395 ) 396 if err != nil { 397 span.RecordError(err) 398 return nil, err 399 } 400 401 createdTime, err := time.Parse(time.RFC3339, createdAt) 402 if err != nil { 403 span.RecordError(err) 404 return nil, err 405 } 406 pull.Created = createdTime 407 408 if sourceBranch.Valid { 409 pull.PullSource = &PullSource{ 410 Branch: sourceBranch.String, 411 } 412 if sourceRepoAt.Valid { 413 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 414 if err != nil { 415 span.RecordError(err) 416 return nil, err 417 } 418 pull.PullSource.RepoAt = &sourceRepoAtParsed 419 } 420 } 421 422 pulls[pull.PullId] = &pull 423 } 424 425 span.AddEvent("querying pull submissions") 426 span.SetAttributes(attribute.Int("pull_count", len(pulls))) 427 428 // get latest round no. for each pull 429 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 430 submissionsQuery := fmt.Sprintf(` 431 select 432 id, pull_id, round_number 433 from 434 pull_submissions 435 where 436 repo_at = ? and pull_id in (%s) 437 `, inClause) 438 439 args := make([]any, len(pulls)+1) 440 args[0] = repoAt.String() 441 idx := 1 442 for _, p := range pulls { 443 args[idx] = p.PullId 444 idx += 1 445 } 446 submissionsRows, err := e.QueryContext(ctx, submissionsQuery, args...) 447 if err != nil { 448 span.RecordError(err) 449 return nil, err 450 } 451 defer submissionsRows.Close() 452 453 for submissionsRows.Next() { 454 var s PullSubmission 455 err := submissionsRows.Scan( 456 &s.ID, 457 &s.PullId, 458 &s.RoundNumber, 459 ) 460 if err != nil { 461 span.RecordError(err) 462 return nil, err 463 } 464 465 if p, ok := pulls[s.PullId]; ok { 466 p.Submissions = make([]*PullSubmission, s.RoundNumber+1) 467 p.Submissions[s.RoundNumber] = &s 468 } 469 } 470 if err := rows.Err(); err != nil { 471 span.RecordError(err) 472 return nil, err 473 } 474 475 span.AddEvent("querying pull comments") 476 477 // get comment count on latest submission on each pull 478 inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 479 commentsQuery := fmt.Sprintf(` 480 select 481 count(id), pull_id 482 from 483 pull_comments 484 where 485 submission_id in (%s) 486 group by 487 submission_id 488 `, inClause) 489 490 args = []any{} 491 for _, p := range pulls { 492 args = append(args, p.Submissions[p.LastRoundNumber()].ID) 493 } 494 commentsRows, err := e.QueryContext(ctx, commentsQuery, args...) 495 if err != nil { 496 span.RecordError(err) 497 return nil, err 498 } 499 defer commentsRows.Close() 500 501 for commentsRows.Next() { 502 var commentCount, pullId int 503 err := commentsRows.Scan( 504 &commentCount, 505 &pullId, 506 ) 507 if err != nil { 508 span.RecordError(err) 509 return nil, err 510 } 511 if p, ok := pulls[pullId]; ok { 512 p.Submissions[p.LastRoundNumber()].Comments = make([]PullComment, commentCount) 513 } 514 } 515 if err := rows.Err(); err != nil { 516 span.RecordError(err) 517 return nil, err 518 } 519 520 span.AddEvent("sorting pulls by date") 521 522 orderedByDate := []*Pull{} 523 for _, p := range pulls { 524 orderedByDate = append(orderedByDate, p) 525 } 526 sort.Slice(orderedByDate, func(i, j int) bool { 527 return orderedByDate[i].Created.After(orderedByDate[j].Created) 528 }) 529 530 span.SetAttributes(attribute.Int("result_count", len(orderedByDate))) 531 return orderedByDate, nil 532} 533 534func GetPull(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 535 span := trace.SpanFromContext(ctx) 536 defer span.End() 537 538 span.SetAttributes(attribute.String("repoAt", repoAt.String()), attribute.Int("pull.id", pullId)) 539 span.AddEvent("query pull metadata") 540 541 query := ` 542 select 543 owner_did, 544 pull_id, 545 created, 546 title, 547 state, 548 target_branch, 549 repo_at, 550 body, 551 rkey, 552 source_branch, 553 source_repo_at 554 from 555 pulls 556 where 557 repo_at = ? and pull_id = ? 558 ` 559 row := e.QueryRowContext(ctx, query, repoAt, pullId) 560 561 var pull Pull 562 var createdAt string 563 var sourceBranch, sourceRepoAt sql.NullString 564 err := row.Scan( 565 &pull.OwnerDid, 566 &pull.PullId, 567 &createdAt, 568 &pull.Title, 569 &pull.State, 570 &pull.TargetBranch, 571 &pull.RepoAt, 572 &pull.Body, 573 &pull.Rkey, 574 &sourceBranch, 575 &sourceRepoAt, 576 ) 577 if err != nil { 578 span.RecordError(err) 579 return nil, err 580 } 581 582 createdTime, err := time.Parse(time.RFC3339, createdAt) 583 if err != nil { 584 span.RecordError(err) 585 return nil, err 586 } 587 pull.Created = createdTime 588 589 if sourceBranch.Valid { 590 pull.PullSource = &PullSource{ 591 Branch: sourceBranch.String, 592 } 593 if sourceRepoAt.Valid { 594 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 595 if err != nil { 596 span.RecordError(err) 597 return nil, err 598 } 599 pull.PullSource.RepoAt = &sourceRepoAtParsed 600 } 601 } 602 603 span.AddEvent("query submissions") 604 submissionsQuery := ` 605 select 606 id, pull_id, repo_at, round_number, patch, created, source_rev 607 from 608 pull_submissions 609 where 610 repo_at = ? and pull_id = ? 611 ` 612 submissionsRows, err := e.QueryContext(ctx, submissionsQuery, repoAt, pullId) 613 if err != nil { 614 span.RecordError(err) 615 return nil, err 616 } 617 defer submissionsRows.Close() 618 619 submissionsMap := make(map[int]*PullSubmission) 620 621 for submissionsRows.Next() { 622 var submission PullSubmission 623 var submissionCreatedStr string 624 var submissionSourceRev sql.NullString 625 err := submissionsRows.Scan( 626 &submission.ID, 627 &submission.PullId, 628 &submission.RepoAt, 629 &submission.RoundNumber, 630 &submission.Patch, 631 &submissionCreatedStr, 632 &submissionSourceRev, 633 ) 634 if err != nil { 635 span.RecordError(err) 636 return nil, err 637 } 638 639 submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 640 if err != nil { 641 span.RecordError(err) 642 return nil, err 643 } 644 submission.Created = submissionCreatedTime 645 646 if submissionSourceRev.Valid { 647 submission.SourceRev = submissionSourceRev.String 648 } 649 650 submissionsMap[submission.ID] = &submission 651 } 652 if err = submissionsRows.Close(); err != nil { 653 span.RecordError(err) 654 return nil, err 655 } 656 if len(submissionsMap) == 0 { 657 return &pull, nil 658 } 659 660 var args []any 661 for k := range submissionsMap { 662 args = append(args, k) 663 } 664 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 665 666 span.AddEvent("query comments") 667 commentsQuery := fmt.Sprintf(` 668 select 669 id, 670 pull_id, 671 submission_id, 672 repo_at, 673 owner_did, 674 comment_at, 675 body, 676 created 677 from 678 pull_comments 679 where 680 submission_id IN (%s) 681 order by 682 created asc 683 `, inClause) 684 commentsRows, err := e.QueryContext(ctx, commentsQuery, args...) 685 if err != nil { 686 span.RecordError(err) 687 return nil, err 688 } 689 defer commentsRows.Close() 690 691 for commentsRows.Next() { 692 var comment PullComment 693 var commentCreatedStr string 694 err := commentsRows.Scan( 695 &comment.ID, 696 &comment.PullId, 697 &comment.SubmissionId, 698 &comment.RepoAt, 699 &comment.OwnerDid, 700 &comment.CommentAt, 701 &comment.Body, 702 &commentCreatedStr, 703 ) 704 if err != nil { 705 span.RecordError(err) 706 return nil, err 707 } 708 709 commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 710 if err != nil { 711 span.RecordError(err) 712 return nil, err 713 } 714 comment.Created = commentCreatedTime 715 716 if submission, ok := submissionsMap[comment.SubmissionId]; ok { 717 submission.Comments = append(submission.Comments, comment) 718 } 719 } 720 if err = commentsRows.Err(); err != nil { 721 span.RecordError(err) 722 return nil, err 723 } 724 725 if pull.PullSource != nil && pull.PullSource.RepoAt != nil { 726 span.AddEvent("query pull source repo") 727 pullSourceRepo, err := GetRepoByAtUri(ctx, e, pull.PullSource.RepoAt.String()) 728 if err != nil { 729 span.RecordError(err) 730 log.Printf("failed to get repo by at uri: %v", err) 731 } else { 732 pull.PullSource.Repo = pullSourceRepo 733 } 734 } 735 736 pull.Submissions = make([]*PullSubmission, len(submissionsMap)) 737 for _, submission := range submissionsMap { 738 pull.Submissions[submission.RoundNumber] = submission 739 } 740 741 return &pull, nil 742} 743 744// timeframe here is directly passed into the sql query filter, and any 745// timeframe in the past should be negative; e.g.: "-3 months" 746func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]Pull, error) { 747 var pulls []Pull 748 749 rows, err := e.Query(` 750 select 751 p.owner_did, 752 p.repo_at, 753 p.pull_id, 754 p.created, 755 p.title, 756 p.state, 757 r.did, 758 r.name, 759 r.knot, 760 r.rkey, 761 r.created 762 from 763 pulls p 764 join 765 repos r on p.repo_at = r.at_uri 766 where 767 p.owner_did = ? and p.created >= date ('now', ?) 768 order by 769 p.created desc`, did, timeframe) 770 if err != nil { 771 return nil, err 772 } 773 defer rows.Close() 774 775 for rows.Next() { 776 var pull Pull 777 var repo Repo 778 var pullCreatedAt, repoCreatedAt string 779 err := rows.Scan( 780 &pull.OwnerDid, 781 &pull.RepoAt, 782 &pull.PullId, 783 &pullCreatedAt, 784 &pull.Title, 785 &pull.State, 786 &repo.Did, 787 &repo.Name, 788 &repo.Knot, 789 &repo.Rkey, 790 &repoCreatedAt, 791 ) 792 if err != nil { 793 return nil, err 794 } 795 796 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 797 if err != nil { 798 return nil, err 799 } 800 pull.Created = pullCreatedTime 801 802 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 803 if err != nil { 804 return nil, err 805 } 806 repo.Created = repoCreatedTime 807 808 pull.Repo = &repo 809 810 pulls = append(pulls, pull) 811 } 812 813 if err := rows.Err(); err != nil { 814 return nil, err 815 } 816 817 return pulls, nil 818} 819 820func NewPullComment(ctx context.Context, e Execer, comment *PullComment) (int64, error) { 821 span := trace.SpanFromContext(ctx) 822 defer span.End() 823 824 span.SetAttributes( 825 attribute.String("repo.at", comment.RepoAt), 826 attribute.Int("pull.id", comment.PullId), 827 attribute.Int("submission.id", comment.SubmissionId), 828 attribute.String("owner.did", comment.OwnerDid), 829 ) 830 span.AddEvent("inserting new pull comment") 831 832 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 833 res, err := e.ExecContext( 834 ctx, 835 query, 836 comment.OwnerDid, 837 comment.RepoAt, 838 comment.SubmissionId, 839 comment.CommentAt, 840 comment.PullId, 841 comment.Body, 842 ) 843 if err != nil { 844 span.RecordError(err) 845 return 0, err 846 } 847 848 i, err := res.LastInsertId() 849 if err != nil { 850 span.RecordError(err) 851 return 0, err 852 } 853 854 span.SetAttributes(attribute.Int64("comment.id", i)) 855 span.AddEvent("pull comment created successfully") 856 return i, nil 857} 858 859func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error { 860 _, err := e.Exec(`update pulls set state = ? where repo_at = ? and pull_id = ?`, pullState, repoAt, pullId) 861 return err 862} 863 864func ClosePull(e Execer, repoAt syntax.ATURI, pullId int) error { 865 err := SetPullState(e, repoAt, pullId, PullClosed) 866 return err 867} 868 869func ReopenPull(e Execer, repoAt syntax.ATURI, pullId int) error { 870 err := SetPullState(e, repoAt, pullId, PullOpen) 871 return err 872} 873 874func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error { 875 err := SetPullState(e, repoAt, pullId, PullMerged) 876 return err 877} 878 879func ResubmitPull(e Execer, pull *Pull, newPatch, sourceRev string) error { 880 newRoundNumber := len(pull.Submissions) 881 _, err := e.Exec(` 882 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 883 values (?, ?, ?, ?, ?) 884 `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch, sourceRev) 885 886 return err 887} 888 889type PullCount struct { 890 Open int 891 Merged int 892 Closed int 893} 894 895func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) { 896 row := e.QueryRow(` 897 select 898 count(case when state = ? then 1 end) as open_count, 899 count(case when state = ? then 1 end) as merged_count, 900 count(case when state = ? then 1 end) as closed_count 901 from pulls 902 where repo_at = ?`, 903 PullOpen, 904 PullMerged, 905 PullClosed, 906 repoAt, 907 ) 908 909 var count PullCount 910 if err := row.Scan(&count.Open, &count.Merged, &count.Closed); err != nil { 911 return PullCount{0, 0, 0}, err 912 } 913 914 return count, nil 915}