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