This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / db / pulls.go
12 kB 531 lines
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "maps" 8 "slices" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.org/core/appview/models" 15 "tangled.org/core/appview/pagination" 16 "tangled.org/core/orm" 17) 18 19func PutPull(ctx context.Context, tx *sql.Tx, pull *models.Pull, references []syntax.ATURI) error { 20 // ensure sequence exists 21 _, err := tx.ExecContext(ctx, ` 22 insert or ignore into repo_pull_seqs (repo_did, next_pull_id) 23 values (?, 1) 24 `, pull.RepoDid) 25 if err != nil { 26 return err 27 } 28 29 var exists bool 30 if err := tx.QueryRowContext(ctx, 31 `select exists (select 1 from pulls where at_uri = ?)`, 32 pull.AtUri(), 33 ).Scan(&exists); err != nil { 34 return err 35 } 36 37 if !exists { 38 // assign new ID for a PR 39 if err := tx.QueryRowContext(ctx, 40 `update repo_pull_seqs 41 set next_pull_id = next_pull_id + 1 42 where repo_did = ? 43 returning next_pull_id - 1`, 44 pull.RepoDid, 45 ).Scan(&pull.PullId); err != nil { 46 return err 47 } 48 } 49 50 result, err := tx.ExecContext(ctx, 51 `insert into pulls ( 52 owner_did, 53 rkey, 54 cid, 55 repo_did, 56 pull_id, 57 title, 58 body, 59 target_branch, 60 source_repo_did, 61 source_branch, 62 created, 63 state 64 ) 65 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 66 on conflict(at_uri) do update set 67 cid = excluded.cid, 68 repo_did = excluded.repo_did, 69 title = excluded.title, 70 body = excluded.body, 71 target_branch = excluded.target_branch, 72 source_repo_did = excluded.source_repo_did, 73 source_branch = excluded.source_branch, 74 created = excluded.created, 75 state = excluded.state 76 where pulls.cid is not excluded.cid`, 77 pull.OwnerDid, 78 pull.Rkey, 79 pull.Cid, 80 pull.RepoDid, 81 pull.PullId, 82 pull.Title, 83 pull.Body, 84 pull.TargetBranch, 85 pull.SourceRepo, 86 pull.SourceBranch, 87 pull.Created.Format(time.RFC3339), 88 pull.State, 89 ) 90 if err != nil { 91 return fmt.Errorf("inserting pr: %w", err) 92 } 93 94 id, err := result.LastInsertId() 95 if err != nil { 96 return err 97 } 98 pull.ID = id 99 100 // delete all existing versions 101 if _, err := tx.ExecContext(ctx, 102 `delete from pull_versions where pull_at = ?`, 103 pull.AtUri(), 104 ); err != nil { 105 return fmt.Errorf("deleting old pr versions: %w", err) 106 } 107 108 // re-create all versions 109 if len(pull.Versions) > 0 { 110 pullAt := pull.AtUri() 111 var sb strings.Builder 112 sb.WriteString(`insert into pull_versions (pull_at, id, head, base, created) values `) 113 args := make([]any, 0, len(pull.Versions)*5) 114 for i, v := range pull.Versions { 115 if i > 0 { 116 sb.WriteString(", ") 117 } 118 sb.WriteString("(?, ?, ?, ?, ?)") 119 args = append(args, pullAt, v.ID, v.Head, v.Base, v.Created.Format(time.RFC3339)) 120 } 121 if _, err := tx.ExecContext(ctx, sb.String(), args...); err != nil { 122 return fmt.Errorf("inserting pr versions: %w", err) 123 } 124 } 125 126 // update references when comment is updated 127 if err := putReferences(tx, pull.AtUri(), references); err != nil { 128 return fmt.Errorf("put reference_links: %w", err) 129 } 130 131 return nil 132} 133 134func SubmitPullVersion(ctx context.Context, q Execer, pullAt syntax.ATURI, version models.PullVersion) error { 135 _, err := q.ExecContext(ctx, 136 `insert into pull_versions (pull_at, id, head, base, created) 137 values (?, ?, ?, ?, ?)`, 138 pullAt, 139 version.ID, 140 version.Head, 141 version.Base, 142 version.Created.Format(time.RFC3339), 143 ) 144 return err 145} 146 147func GetPull(ctx context.Context, q Execer, filters ...orm.Filter) (*models.Pull, error) { 148 pulls, err := GetPullsPaginated(ctx, q, pagination.Page{Limit: 1}, filters...) 149 if err != nil { 150 return nil, err 151 } 152 if len(pulls) == 0 { 153 return nil, sql.ErrNoRows 154 } 155 return pulls[0], nil 156} 157 158func GetPullsPaginated(ctx context.Context, q Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Pull, error) { 159 pulls := make(map[syntax.ATURI]*models.Pull) 160 161 var conditions []string 162 var args []any 163 for _, filter := range filters { 164 conditions = append(conditions, filter.Condition()) 165 args = append(args, filter.Arg()...) 166 } 167 168 whereClause := "" 169 if conditions != nil { 170 whereClause = " where " + strings.Join(conditions, " and ") 171 } 172 pageClause := "" 173 if page.Limit != 0 { 174 pageClause = fmt.Sprintf( 175 " limit %d offset %d ", 176 page.Limit, 177 page.Offset, 178 ) 179 } 180 181 query := fmt.Sprintf(` 182 select 183 id, 184 owner_did, 185 rkey, 186 cid, 187 repo_did, 188 pull_id, 189 title, 190 body, 191 target_branch, 192 source_repo_did, 193 source_branch, 194 created, 195 state 196 from 197 pulls 198 %s 199 order by 200 created desc 201 %s 202 `, whereClause, pageClause) 203 204 rows, err := q.QueryContext(ctx, query, args...) 205 if err != nil { 206 return nil, err 207 } 208 defer rows.Close() 209 210 for rows.Next() { 211 var pull models.Pull 212 var createdAt string 213 var sourceRepo, sourceBranch sql.NullString 214 err := rows.Scan( 215 &pull.ID, 216 &pull.OwnerDid, 217 &pull.Rkey, 218 &pull.Cid, 219 &pull.RepoDid, 220 &pull.PullId, 221 &pull.Title, 222 &pull.Body, 223 &pull.TargetBranch, 224 &sourceRepo, 225 &sourceBranch, 226 &createdAt, 227 &pull.State, 228 ) 229 if err != nil { 230 return nil, fmt.Errorf("scanning row: %w", err) 231 } 232 233 createdTime, err := time.Parse(time.RFC3339, createdAt) 234 if err != nil { 235 return nil, fmt.Errorf("parsing created: %w", err) 236 } 237 pull.Created = createdTime 238 239 if sourceRepo.Valid { 240 pull.SourceRepo = syntax.DID(sourceRepo.String) 241 } else { 242 // fallback to pull.target.repo 243 pull.SourceRepo = pull.RepoDid 244 } 245 246 if sourceBranch.Valid { 247 pull.SourceBranch = &sourceBranch.String 248 } 249 250 pulls[pull.AtUri()] = &pull 251 } 252 if err := rows.Err(); err != nil { 253 return nil, fmt.Errorf("scanning rows: %w", err) 254 } 255 256 pullAts := slices.Collect(maps.Keys(pulls)) 257 258 versionsMap, err := ListVersions(ctx, q, pullAts) 259 if err != nil { 260 return nil, fmt.Errorf("querying versions: %w", err) 261 } 262 263 for pullAt, p := range pulls { 264 if versions, ok := versionsMap[pullAt]; ok { 265 p.Versions = versions 266 } else { 267 return nil, fmt.Errorf("find 0 versions for PR %s", pullAt) 268 } 269 } 270 271 // collect reverse repos 272 { 273 repoDids := make([]string, 0, len(pulls)) 274 for _, issue := range pulls { 275 repoDids = append(repoDids, string(issue.RepoDid)) 276 } 277 278 repos, err := GetRepos(q, orm.FilterIn("repo_did", repoDids)) 279 if err != nil { 280 return nil, fmt.Errorf("failed to build repo mappings: %w", err) 281 } 282 repoMap := make(map[syntax.DID]*models.Repo) 283 for i := range repos { 284 repoMap[syntax.DID(repos[i].RepoDid)] = &repos[i] 285 } 286 287 for pullAt, p := range pulls { 288 if r, ok := repoMap[p.RepoDid]; ok { 289 p.Repo = r 290 } else { 291 delete(pulls, pullAt) 292 } 293 } 294 } 295 296 // collect allLabels for each PR 297 { 298 allLabels, err := GetLabels(q, orm.FilterIn("subject", pullAts)) 299 if err != nil { 300 return nil, fmt.Errorf("failed to query labels: %w", err) 301 } 302 for pullAt, labels := range allLabels { 303 if pull, ok := pulls[pullAt]; ok { 304 pull.Labels = labels 305 } 306 } 307 } 308 309 orderedById := []*models.Pull{} 310 for _, p := range pulls { 311 orderedById = append(orderedById, p) 312 } 313 sort.Slice(orderedById, func(i, j int) bool { 314 return orderedById[i].PullId > orderedById[j].PullId 315 }) 316 317 return orderedById, nil 318} 319 320// mapping from pull -> pull submissions 321func ListVersions(ctx context.Context, q Execer, pullAts []syntax.ATURI) (map[syntax.ATURI][]models.PullVersion, error) { 322 filter := orm.FilterIn("pull_at", pullAts) 323 324 query := fmt.Sprintf(` 325 select 326 pull_at, 327 id, 328 head, 329 base, 330 created 331 from pull_versions 332 where %s 333 order by id asc 334 `, filter.Condition()) 335 336 rows, err := q.QueryContext(ctx, query, filter.Arg()...) 337 if err != nil { 338 return nil, fmt.Errorf("failed to query: %w", err) 339 } 340 defer rows.Close() 341 342 versionsMap := make(map[syntax.ATURI][]models.PullVersion) 343 344 for rows.Next() { 345 var version models.PullVersion 346 var pullAt syntax.ATURI 347 var createdAt string 348 err := rows.Scan( 349 &pullAt, 350 &version.ID, 351 &version.Head, 352 &version.Base, 353 &createdAt, 354 ) 355 if err != nil { 356 return nil, fmt.Errorf("scanning row: %w", err) 357 } 358 359 createdTime, err := time.Parse(time.RFC3339, createdAt) 360 if err != nil { 361 return nil, fmt.Errorf("parsing created: %w", err) 362 } 363 version.Created = createdTime 364 365 versionsMap[pullAt] = append(versionsMap[pullAt], version) 366 } 367 if err := rows.Err(); err != nil { 368 return nil, fmt.Errorf("scanning rows: %w", err) 369 } 370 371 comments, err := GetComments(q, orm.FilterIn("subject_uri", pullAts)) 372 if err != nil { 373 return nil, fmt.Errorf("failed to get pull comments: %w", err) 374 } 375 for _, comment := range comments { 376 if comment.PullRoundIdx == nil { 377 continue 378 } 379 versionIdx := *comment.PullRoundIdx 380 if versions, ok := versionsMap[syntax.ATURI(comment.Subject.Uri)]; ok { 381 if versionIdx >= len(versions) { 382 continue 383 } 384 versions[versionIdx].Comments = append(versions[versionIdx].Comments, comment) 385 } 386 } 387 388 // TODO: reverse-map version.Comments 389 390 return versionsMap, nil 391} 392 393// timeframe here is directly passed into the sql query filter, and any 394// timeframe in the past should be negative; e.g.: "-3 months" 395func GetPullsByOwnerDid(e Execer, did syntax.DID, timeframe string) ([]models.Pull, error) { 396 var pulls []models.Pull 397 398 rows, err := e.Query(` 399 select 400 p.owner_did, 401 p.repo_did, 402 p.pull_id, 403 p.created, 404 p.title, 405 p.state, 406 r.did, 407 r.name, 408 r.knot, 409 r.rkey, 410 r.created 411 from 412 pulls p 413 join 414 repos r on p.repo_did = r.repo_did 415 where 416 p.owner_did = ? and p.created >= date ('now', ?) 417 order by 418 p.created desc`, did, timeframe) 419 if err != nil { 420 return nil, err 421 } 422 defer rows.Close() 423 424 for rows.Next() { 425 var pull models.Pull 426 var repo models.Repo 427 var pullCreatedAt, repoCreatedAt string 428 err := rows.Scan( 429 &pull.OwnerDid, 430 &pull.RepoDid, 431 &pull.PullId, 432 &pullCreatedAt, 433 &pull.Title, 434 &pull.State, 435 &repo.Did, 436 &repo.Name, 437 &repo.Knot, 438 &repo.Rkey, 439 &repoCreatedAt, 440 ) 441 if err != nil { 442 return nil, err 443 } 444 445 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 446 if err != nil { 447 return nil, err 448 } 449 pull.Created = pullCreatedTime 450 451 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 452 if err != nil { 453 return nil, err 454 } 455 repo.Created = repoCreatedTime 456 457 pull.Repo = &repo 458 459 pulls = append(pulls, pull) 460 } 461 462 if err := rows.Err(); err != nil { 463 return nil, err 464 } 465 466 return pulls, nil 467} 468 469// use with transaction 470func setPullsState(e Execer, pullState models.PullState, filters ...orm.Filter) error { 471 var conditions []string 472 var args []any 473 474 args = append(args, pullState) 475 for _, filter := range filters { 476 conditions = append(conditions, filter.Condition()) 477 args = append(args, filter.Arg()...) 478 } 479 args = append(args, models.PullAbandoned) // only update state of non-deleted pulls 480 args = append(args, models.PullMerged) // only update state of non-merged pulls 481 482 whereClause := "" 483 if conditions != nil { 484 whereClause = " where " + strings.Join(conditions, " and ") 485 } 486 487 query := fmt.Sprintf("update pulls set state = ? %s and state <> ? and state <> ?", whereClause) 488 489 _, err := e.Exec(query, args...) 490 return err 491} 492 493func ClosePulls(e Execer, filters ...orm.Filter) error { 494 return setPullsState(e, models.PullClosed, filters...) 495} 496 497func ReopenPulls(e Execer, filters ...orm.Filter) error { 498 return setPullsState(e, models.PullOpen, filters...) 499} 500 501func MergePulls(e Execer, filters ...orm.Filter) error { 502 return setPullsState(e, models.PullMerged, filters...) 503} 504 505func AbandonPulls(e Execer, filters ...orm.Filter) error { 506 return setPullsState(e, models.PullAbandoned, filters...) 507} 508 509func GetPullCount(e Execer, repoDid string) (models.PullCount, error) { 510 row := e.QueryRow(` 511 select 512 count(case when state = ? then 1 end) as open_count, 513 count(case when state = ? then 1 end) as merged_count, 514 count(case when state = ? then 1 end) as closed_count, 515 count(case when state = ? then 1 end) as deleted_count 516 from pulls 517 where repo_did = ?`, 518 models.PullOpen, 519 models.PullMerged, 520 models.PullClosed, 521 models.PullAbandoned, 522 repoDid, 523 ) 524 525 var count models.PullCount 526 if err := row.Scan(&count.Open, &count.Merged, &count.Closed, &count.Deleted); err != nil { 527 return models.PullCount{Open: 0, Merged: 0, Closed: 0, Deleted: 0}, err 528 } 529 530 return count, nil 531}