forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/bluekeyes/go-gitdiff/gitdiff" 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "tangled.sh/tangled.sh/core/types" 13) 14 15type PullState int 16 17const ( 18 PullClosed PullState = iota 19 PullOpen 20 PullMerged 21) 22 23func (p PullState) String() string { 24 switch p { 25 case PullOpen: 26 return "open" 27 case PullMerged: 28 return "merged" 29 case PullClosed: 30 return "closed" 31 default: 32 return "closed" 33 } 34} 35 36func (p PullState) IsOpen() bool { 37 return p == PullOpen 38} 39func (p PullState) IsMerged() bool { 40 return p == PullMerged 41} 42func (p PullState) IsClosed() bool { 43 return p == PullClosed 44} 45 46type Pull struct { 47 // ids 48 ID int 49 PullId int 50 51 // at ids 52 RepoAt syntax.ATURI 53 OwnerDid string 54 Rkey string 55 PullAt syntax.ATURI 56 57 // content 58 Title string 59 Body string 60 TargetBranch string 61 State PullState 62 Submissions []*PullSubmission 63 64 // meta 65 Created time.Time 66} 67 68type PullSubmission struct { 69 // ids 70 ID int 71 PullId int 72 73 // at ids 74 RepoAt syntax.ATURI 75 76 // content 77 RoundNumber int 78 Patch string 79 Comments []PullComment 80 81 // meta 82 Created time.Time 83} 84 85type PullComment struct { 86 // ids 87 ID int 88 PullId int 89 SubmissionId int 90 91 // at ids 92 RepoAt string 93 OwnerDid string 94 CommentAt string 95 96 // content 97 Body string 98 99 // meta 100 Created time.Time 101} 102 103func (p *Pull) LatestPatch() string { 104 latestSubmission := p.Submissions[p.LastRoundNumber()] 105 return latestSubmission.Patch 106} 107 108func (p *Pull) LastRoundNumber() int { 109 return len(p.Submissions) - 1 110} 111 112func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff { 113 patch := s.Patch 114 115 diffs, _, err := gitdiff.Parse(strings.NewReader(patch)) 116 if err != nil { 117 log.Println(err) 118 } 119 120 nd := types.NiceDiff{} 121 nd.Commit.Parent = targetBranch 122 123 for _, d := range diffs { 124 ndiff := types.Diff{} 125 ndiff.Name.New = d.NewName 126 ndiff.Name.Old = d.OldName 127 ndiff.IsBinary = d.IsBinary 128 ndiff.IsNew = d.IsNew 129 ndiff.IsDelete = d.IsDelete 130 ndiff.IsCopy = d.IsCopy 131 ndiff.IsRename = d.IsRename 132 133 for _, tf := range d.TextFragments { 134 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 135 for _, l := range tf.Lines { 136 switch l.Op { 137 case gitdiff.OpAdd: 138 nd.Stat.Insertions += 1 139 case gitdiff.OpDelete: 140 nd.Stat.Deletions += 1 141 } 142 } 143 } 144 145 nd.Diff = append(nd.Diff, ndiff) 146 } 147 148 nd.Stat.FilesChanged = len(diffs) 149 150 return nd 151} 152 153func NewPull(tx *sql.Tx, pull *Pull) error { 154 defer tx.Rollback() 155 156 _, err := tx.Exec(` 157 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 158 values (?, 1) 159 `, pull.RepoAt) 160 if err != nil { 161 return err 162 } 163 164 var nextId int 165 err = tx.QueryRow(` 166 update repo_pull_seqs 167 set next_pull_id = next_pull_id + 1 168 where repo_at = ? 169 returning next_pull_id - 1 170 `, pull.RepoAt).Scan(&nextId) 171 if err != nil { 172 return err 173 } 174 175 pull.PullId = nextId 176 pull.State = PullOpen 177 178 _, err = tx.Exec(` 179 insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state) 180 values (?, ?, ?, ?, ?, ?, ?, ?) 181 `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State) 182 if err != nil { 183 return err 184 } 185 186 _, err = tx.Exec(` 187 insert into pull_submissions (pull_id, repo_at, round_number, patch) 188 values (?, ?, ?, ?) 189 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch) 190 if err != nil { 191 return err 192 } 193 194 if err := tx.Commit(); err != nil { 195 return err 196 } 197 198 return nil 199} 200 201func SetPullAt(e Execer, repoAt syntax.ATURI, pullId int, pullAt string) error { 202 _, err := e.Exec(`update pulls set pull_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId) 203 return err 204} 205 206func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (string, error) { 207 var pullAt string 208 err := e.QueryRow(`select pull_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt) 209 return pullAt, err 210} 211 212func NextPullId(e Execer, repoAt syntax.ATURI) (int, error) { 213 var pullId int 214 err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_at = ?`, repoAt).Scan(&pullId) 215 return pullId - 1, err 216} 217 218func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]Pull, error) { 219 var pulls []Pull 220 221 rows, err := e.Query(` 222 select 223 owner_did, 224 pull_id, 225 created, 226 title, 227 state, 228 target_branch, 229 pull_at, 230 body, 231 rkey 232 from 233 pulls 234 where 235 repo_at = ? and state = ? 236 order by 237 created desc`, repoAt, state) 238 if err != nil { 239 return nil, err 240 } 241 defer rows.Close() 242 243 for rows.Next() { 244 var pull Pull 245 var createdAt string 246 err := rows.Scan( 247 &pull.OwnerDid, 248 &pull.PullId, 249 &createdAt, 250 &pull.Title, 251 &pull.State, 252 &pull.TargetBranch, 253 &pull.PullAt, 254 &pull.Body, 255 &pull.Rkey, 256 ) 257 if err != nil { 258 return nil, err 259 } 260 261 createdTime, err := time.Parse(time.RFC3339, createdAt) 262 if err != nil { 263 return nil, err 264 } 265 pull.Created = createdTime 266 267 pulls = append(pulls, pull) 268 } 269 270 if err := rows.Err(); err != nil { 271 return nil, err 272 } 273 274 return pulls, nil 275} 276 277func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 278 query := ` 279 select 280 owner_did, 281 pull_id, 282 created, 283 title, 284 state, 285 target_branch, 286 pull_at, 287 repo_at, 288 body, 289 rkey 290 from 291 pulls 292 where 293 repo_at = ? and pull_id = ? 294 ` 295 row := e.QueryRow(query, repoAt, pullId) 296 297 var pull Pull 298 var createdAt string 299 err := row.Scan( 300 &pull.OwnerDid, 301 &pull.PullId, 302 &createdAt, 303 &pull.Title, 304 &pull.State, 305 &pull.TargetBranch, 306 &pull.PullAt, 307 &pull.RepoAt, 308 &pull.Body, 309 &pull.Rkey, 310 ) 311 if err != nil { 312 return nil, err 313 } 314 315 createdTime, err := time.Parse(time.RFC3339, createdAt) 316 if err != nil { 317 return nil, err 318 } 319 pull.Created = createdTime 320 321 submissionsQuery := ` 322 select 323 id, pull_id, repo_at, round_number, patch, created 324 from 325 pull_submissions 326 where 327 repo_at = ? and pull_id = ? 328 ` 329 submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) 330 if err != nil { 331 return nil, err 332 } 333 defer submissionsRows.Close() 334 335 submissionsMap := make(map[int]*PullSubmission) 336 337 for submissionsRows.Next() { 338 var submission PullSubmission 339 var submissionCreatedStr string 340 err := submissionsRows.Scan( 341 &submission.ID, 342 &submission.PullId, 343 &submission.RepoAt, 344 &submission.RoundNumber, 345 &submission.Patch, 346 &submissionCreatedStr, 347 ) 348 if err != nil { 349 return nil, err 350 } 351 352 submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 353 if err != nil { 354 return nil, err 355 } 356 submission.Created = submissionCreatedTime 357 358 submissionsMap[submission.ID] = &submission 359 } 360 if err = submissionsRows.Close(); err != nil { 361 return nil, err 362 } 363 if len(submissionsMap) == 0 { 364 return &pull, nil 365 } 366 367 var args []any 368 for k := range submissionsMap { 369 args = append(args, k) 370 } 371 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 372 commentsQuery := fmt.Sprintf(` 373 select 374 id, 375 pull_id, 376 submission_id, 377 repo_at, 378 owner_did, 379 comment_at, 380 body, 381 created 382 from 383 pull_comments 384 where 385 submission_id IN (%s) 386 order by 387 created asc 388 `, inClause) 389 commentsRows, err := e.Query(commentsQuery, args...) 390 if err != nil { 391 return nil, err 392 } 393 defer commentsRows.Close() 394 395 for commentsRows.Next() { 396 var comment PullComment 397 var commentCreatedStr string 398 err := commentsRows.Scan( 399 &comment.ID, 400 &comment.PullId, 401 &comment.SubmissionId, 402 &comment.RepoAt, 403 &comment.OwnerDid, 404 &comment.CommentAt, 405 &comment.Body, 406 &commentCreatedStr, 407 ) 408 if err != nil { 409 return nil, err 410 } 411 412 commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 413 if err != nil { 414 return nil, err 415 } 416 comment.Created = commentCreatedTime 417 418 // Add the comment to its submission 419 if submission, ok := submissionsMap[comment.SubmissionId]; ok { 420 submission.Comments = append(submission.Comments, comment) 421 } 422 423 } 424 if err = commentsRows.Err(); err != nil { 425 return nil, err 426 } 427 428 pull.Submissions = make([]*PullSubmission, len(submissionsMap)) 429 for _, submission := range submissionsMap { 430 pull.Submissions[submission.RoundNumber] = submission 431 } 432 433 return &pull, nil 434} 435 436func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { 437 var pulls []Pull 438 439 rows, err := e.Query(` 440 select 441 owner_did, 442 repo_at, 443 pull_id, 444 created, 445 title, 446 state 447 from 448 pulls 449 where 450 owner_did = ? 451 order by 452 created desc`, did) 453 if err != nil { 454 return nil, err 455 } 456 defer rows.Close() 457 458 for rows.Next() { 459 var pull Pull 460 var createdAt string 461 err := rows.Scan( 462 &pull.OwnerDid, 463 &pull.RepoAt, 464 &pull.PullId, 465 &createdAt, 466 &pull.Title, 467 &pull.State, 468 ) 469 if err != nil { 470 return nil, err 471 } 472 473 createdTime, err := time.Parse(time.RFC3339, createdAt) 474 if err != nil { 475 return nil, err 476 } 477 pull.Created = createdTime 478 479 pulls = append(pulls, pull) 480 } 481 482 if err := rows.Err(); err != nil { 483 return nil, err 484 } 485 486 return pulls, nil 487} 488 489func NewPullComment(e Execer, comment *PullComment) (int64, error) { 490 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 491 res, err := e.Exec( 492 query, 493 comment.OwnerDid, 494 comment.RepoAt, 495 comment.SubmissionId, 496 comment.CommentAt, 497 comment.PullId, 498 comment.Body, 499 ) 500 if err != nil { 501 return 0, err 502 } 503 504 i, err := res.LastInsertId() 505 if err != nil { 506 return 0, err 507 } 508 509 return i, nil 510} 511 512func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error { 513 _, err := e.Exec(`update pulls set state = ? where repo_at = ? and pull_id = ?`, pullState, repoAt, pullId) 514 return err 515} 516 517func ClosePull(e Execer, repoAt syntax.ATURI, pullId int) error { 518 err := SetPullState(e, repoAt, pullId, PullClosed) 519 return err 520} 521 522func ReopenPull(e Execer, repoAt syntax.ATURI, pullId int) error { 523 err := SetPullState(e, repoAt, pullId, PullOpen) 524 return err 525} 526 527func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error { 528 err := SetPullState(e, repoAt, pullId, PullMerged) 529 return err 530} 531 532func ResubmitPull(e Execer, pull *Pull, newPatch string) error { 533 newRoundNumber := len(pull.Submissions) 534 _, err := e.Exec(` 535 insert into pull_submissions (pull_id, repo_at, round_number, patch) 536 values (?, ?, ?, ?) 537 `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch) 538 539 return err 540} 541 542type PullCount struct { 543 Open int 544 Merged int 545 Closed int 546} 547 548func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) { 549 row := e.QueryRow(` 550 select 551 count(case when state = ? then 1 end) as open_count, 552 count(case when state = ? then 1 end) as merged_count, 553 count(case when state = ? then 1 end) as closed_count 554 from pulls 555 where repo_at = ?`, 556 PullOpen, 557 PullMerged, 558 PullClosed, 559 repoAt, 560 ) 561 562 var count PullCount 563 if err := row.Scan(&count.Open, &count.Merged, &count.Closed); err != nil { 564 return PullCount{0, 0, 0}, err 565 } 566 567 return count, nil 568}