forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "slices" 8 "strings" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.sh/tangled.sh/core/api/tangled" 14) 15 16type Repo struct { 17 Did string 18 Name string 19 Knot string 20 Rkey string 21 Created time.Time 22 AtUri string 23 Description string 24 Spindle string 25 26 // optionally, populate this when querying for reverse mappings 27 RepoStats *RepoStats 28 29 // optional 30 Source string 31} 32 33func (r Repo) RepoAt() syntax.ATURI { 34 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 35} 36 37func (r Repo) DidSlashRepo() string { 38 p, _ := securejoin.SecureJoin(r.Did, r.Name) 39 return p 40} 41 42func GetAllRepos(e Execer, limit int) ([]Repo, error) { 43 var repos []Repo 44 45 rows, err := e.Query( 46 `select did, name, knot, rkey, description, created, source 47 from repos 48 order by created desc 49 limit ? 50 `, 51 limit, 52 ) 53 if err != nil { 54 return nil, err 55 } 56 defer rows.Close() 57 58 for rows.Next() { 59 var repo Repo 60 err := scanRepo( 61 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 62 ) 63 if err != nil { 64 return nil, err 65 } 66 repos = append(repos, repo) 67 } 68 69 if err := rows.Err(); err != nil { 70 return nil, err 71 } 72 73 return repos, nil 74} 75 76func GetRepos(e Execer, filters ...filter) ([]Repo, error) { 77 repoMap := make(map[syntax.ATURI]*Repo) 78 79 var conditions []string 80 var args []any 81 for _, filter := range filters { 82 conditions = append(conditions, filter.Condition()) 83 args = append(args, filter.Arg()...) 84 } 85 86 whereClause := "" 87 if conditions != nil { 88 whereClause = " where " + strings.Join(conditions, " and ") 89 } 90 91 repoQuery := fmt.Sprintf( 92 `select 93 did, 94 name, 95 knot, 96 rkey, 97 created, 98 description, 99 source, 100 spindle 101 from 102 repos r 103 %s`, 104 whereClause, 105 ) 106 rows, err := e.Query(repoQuery, args...) 107 108 if err != nil { 109 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 110 } 111 112 for rows.Next() { 113 var repo Repo 114 var createdAt string 115 var description, source, spindle sql.NullString 116 117 err := rows.Scan( 118 &repo.Did, 119 &repo.Name, 120 &repo.Knot, 121 &repo.Rkey, 122 &createdAt, 123 &description, 124 &source, 125 &spindle, 126 ) 127 if err != nil { 128 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 129 } 130 131 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 132 repo.Created = t 133 } 134 if description.Valid { 135 repo.Description = description.String 136 } 137 if source.Valid { 138 repo.Source = source.String 139 } 140 if spindle.Valid { 141 repo.Spindle = spindle.String 142 } 143 144 repo.RepoStats = &RepoStats{} 145 repoMap[repo.RepoAt()] = &repo 146 } 147 148 if err = rows.Err(); err != nil { 149 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 150 } 151 152 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ") 153 args = make([]any, len(repoMap)) 154 155 i := 0 156 for _, r := range repoMap { 157 args[i] = r.RepoAt() 158 i++ 159 } 160 161 languageQuery := fmt.Sprintf( 162 ` 163 select 164 repo_at, language 165 from 166 repo_languages r1 167 where 168 repo_at IN (%s) 169 and is_default_ref = 1 170 and id = ( 171 select id 172 from repo_languages r2 173 where r2.repo_at = r1.repo_at 174 and r2.is_default_ref = 1 175 order by bytes desc 176 limit 1 177 ); 178 `, 179 inClause, 180 ) 181 rows, err = e.Query(languageQuery, args...) 182 if err != nil { 183 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 184 } 185 for rows.Next() { 186 var repoat, lang string 187 if err := rows.Scan(&repoat, &lang); err != nil { 188 log.Println("err", "err", err) 189 continue 190 } 191 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 192 r.RepoStats.Language = lang 193 } 194 } 195 if err = rows.Err(); err != nil { 196 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 197 } 198 199 starCountQuery := fmt.Sprintf( 200 `select 201 repo_at, count(1) 202 from stars 203 where repo_at in (%s) 204 group by repo_at`, 205 inClause, 206 ) 207 rows, err = e.Query(starCountQuery, args...) 208 if err != nil { 209 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 210 } 211 for rows.Next() { 212 var repoat string 213 var count int 214 if err := rows.Scan(&repoat, &count); err != nil { 215 log.Println("err", "err", err) 216 continue 217 } 218 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 219 r.RepoStats.StarCount = count 220 } 221 } 222 if err = rows.Err(); err != nil { 223 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 224 } 225 226 issueCountQuery := fmt.Sprintf( 227 `select 228 repo_at, 229 count(case when open = 1 then 1 end) as open_count, 230 count(case when open = 0 then 1 end) as closed_count 231 from issues 232 where repo_at in (%s) 233 group by repo_at`, 234 inClause, 235 ) 236 rows, err = e.Query(issueCountQuery, args...) 237 if err != nil { 238 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 239 } 240 for rows.Next() { 241 var repoat string 242 var open, closed int 243 if err := rows.Scan(&repoat, &open, &closed); err != nil { 244 log.Println("err", "err", err) 245 continue 246 } 247 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 248 r.RepoStats.IssueCount.Open = open 249 r.RepoStats.IssueCount.Closed = closed 250 } 251 } 252 if err = rows.Err(); err != nil { 253 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 254 } 255 256 pullCountQuery := fmt.Sprintf( 257 `select 258 repo_at, 259 count(case when state = ? then 1 end) as open_count, 260 count(case when state = ? then 1 end) as merged_count, 261 count(case when state = ? then 1 end) as closed_count, 262 count(case when state = ? then 1 end) as deleted_count 263 from pulls 264 where repo_at in (%s) 265 group by repo_at`, 266 inClause, 267 ) 268 args = append([]any{ 269 PullOpen, 270 PullMerged, 271 PullClosed, 272 PullDeleted, 273 }, args...) 274 rows, err = e.Query( 275 pullCountQuery, 276 args..., 277 ) 278 if err != nil { 279 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 280 } 281 for rows.Next() { 282 var repoat string 283 var open, merged, closed, deleted int 284 if err := rows.Scan(&repoat, &open, &merged, &closed, &deleted); err != nil { 285 log.Println("err", "err", err) 286 continue 287 } 288 if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 289 r.RepoStats.PullCount.Open = open 290 r.RepoStats.PullCount.Merged = merged 291 r.RepoStats.PullCount.Closed = closed 292 r.RepoStats.PullCount.Deleted = deleted 293 } 294 } 295 if err = rows.Err(); err != nil { 296 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 297 } 298 299 var repos []Repo 300 for _, r := range repoMap { 301 repos = append(repos, *r) 302 } 303 304 slices.SortFunc(repos, func(a, b Repo) int { 305 if a.Created.After(b.Created) { 306 return 1 307 } 308 return -1 309 }) 310 311 return repos, nil 312} 313 314func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 315 var repos []Repo 316 317 rows, err := e.Query( 318 `select 319 r.did, 320 r.name, 321 r.knot, 322 r.rkey, 323 r.description, 324 r.created, 325 count(s.id) as star_count, 326 r.source 327 from 328 repos r 329 left join 330 stars s on r.at_uri = s.repo_at 331 where 332 r.did = ? 333 group by 334 r.at_uri 335 order by r.created desc`, 336 did) 337 if err != nil { 338 return nil, err 339 } 340 defer rows.Close() 341 342 for rows.Next() { 343 var repo Repo 344 var repoStats RepoStats 345 var createdAt string 346 var nullableDescription sql.NullString 347 var nullableSource sql.NullString 348 349 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource) 350 if err != nil { 351 return nil, err 352 } 353 354 if nullableDescription.Valid { 355 repo.Description = nullableDescription.String 356 } 357 358 if nullableSource.Valid { 359 repo.Source = nullableSource.String 360 } 361 362 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 363 if err != nil { 364 repo.Created = time.Now() 365 } else { 366 repo.Created = createdAtTime 367 } 368 369 repo.RepoStats = &repoStats 370 371 repos = append(repos, repo) 372 } 373 374 if err := rows.Err(); err != nil { 375 return nil, err 376 } 377 378 return repos, nil 379} 380 381func GetRepo(e Execer, did, name string) (*Repo, error) { 382 var repo Repo 383 var description, spindle sql.NullString 384 385 row := e.QueryRow(` 386 select did, name, knot, created, at_uri, description, spindle 387 from repos 388 where did = ? and name = ? 389 `, 390 did, 391 name, 392 ) 393 394 var createdAt string 395 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 396 return nil, err 397 } 398 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 399 repo.Created = createdAtTime 400 401 if description.Valid { 402 repo.Description = description.String 403 } 404 405 if spindle.Valid { 406 repo.Spindle = spindle.String 407 } 408 409 return &repo, nil 410} 411 412func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 413 var repo Repo 414 var nullableDescription sql.NullString 415 416 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 417 418 var createdAt string 419 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 420 return nil, err 421 } 422 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 423 repo.Created = createdAtTime 424 425 if nullableDescription.Valid { 426 repo.Description = nullableDescription.String 427 } else { 428 repo.Description = "" 429 } 430 431 return &repo, nil 432} 433 434func AddRepo(e Execer, repo *Repo) error { 435 _, err := e.Exec( 436 `insert into repos 437 (did, name, knot, rkey, at_uri, description, source) 438 values (?, ?, ?, ?, ?, ?, ?)`, 439 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 440 ) 441 return err 442} 443 444func RemoveRepo(e Execer, did, name string) error { 445 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 446 return err 447} 448 449func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) { 450 var nullableSource sql.NullString 451 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 452 if err != nil { 453 return "", err 454 } 455 return nullableSource.String, nil 456} 457 458func GetForksByDid(e Execer, did string) ([]Repo, error) { 459 var repos []Repo 460 461 rows, err := e.Query( 462 `select did, name, knot, rkey, description, created, at_uri, source 463 from repos 464 where did = ? and source is not null and source != '' 465 order by created desc`, 466 did, 467 ) 468 if err != nil { 469 return nil, err 470 } 471 defer rows.Close() 472 473 for rows.Next() { 474 var repo Repo 475 var createdAt string 476 var nullableDescription sql.NullString 477 var nullableSource sql.NullString 478 479 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 480 if err != nil { 481 return nil, err 482 } 483 484 if nullableDescription.Valid { 485 repo.Description = nullableDescription.String 486 } 487 488 if nullableSource.Valid { 489 repo.Source = nullableSource.String 490 } 491 492 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 493 if err != nil { 494 repo.Created = time.Now() 495 } else { 496 repo.Created = createdAtTime 497 } 498 499 repos = append(repos, repo) 500 } 501 502 if err := rows.Err(); err != nil { 503 return nil, err 504 } 505 506 return repos, nil 507} 508 509func GetForkByDid(e Execer, did string, name string) (*Repo, error) { 510 var repo Repo 511 var createdAt string 512 var nullableDescription sql.NullString 513 var nullableSource sql.NullString 514 515 row := e.QueryRow( 516 `select did, name, knot, rkey, description, created, at_uri, source 517 from repos 518 where did = ? and name = ? and source is not null and source != ''`, 519 did, name, 520 ) 521 522 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 523 if err != nil { 524 return nil, err 525 } 526 527 if nullableDescription.Valid { 528 repo.Description = nullableDescription.String 529 } 530 531 if nullableSource.Valid { 532 repo.Source = nullableSource.String 533 } 534 535 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 536 if err != nil { 537 repo.Created = time.Now() 538 } else { 539 repo.Created = createdAtTime 540 } 541 542 return &repo, nil 543} 544 545func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { 546 _, err := e.Exec( 547 `insert into collaborators (did, repo) 548 values (?, (select id from repos where did = ? and name = ? and knot = ?));`, 549 collaborator, repoOwnerDid, repoName, repoKnot) 550 return err 551} 552 553func UpdateDescription(e Execer, repoAt, newDescription string) error { 554 _, err := e.Exec( 555 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 556 return err 557} 558 559func UpdateSpindle(e Execer, repoAt, spindle string) error { 560 _, err := e.Exec( 561 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt) 562 return err 563} 564 565func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 566 rows, err := e.Query(`select repo from collaborators where did = ?`, collaborator) 567 if err != nil { 568 return nil, err 569 } 570 defer rows.Close() 571 572 var repoIds []int 573 for rows.Next() { 574 var id int 575 err := rows.Scan(&id) 576 if err != nil { 577 return nil, err 578 } 579 repoIds = append(repoIds, id) 580 } 581 if err := rows.Err(); err != nil { 582 return nil, err 583 } 584 if repoIds == nil { 585 return nil, nil 586 } 587 588 return GetRepos(e, FilterIn("id", repoIds)) 589} 590 591type RepoStats struct { 592 Language string 593 StarCount int 594 IssueCount IssueCount 595 PullCount PullCount 596} 597 598func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error { 599 var createdAt string 600 var nullableDescription sql.NullString 601 var nullableSource sql.NullString 602 if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil { 603 return err 604 } 605 606 if nullableDescription.Valid { 607 *description = nullableDescription.String 608 } else { 609 *description = "" 610 } 611 612 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 613 if err != nil { 614 *created = time.Now() 615 } else { 616 *created = createdAtTime 617 } 618 619 if nullableSource.Valid { 620 *source = nullableSource.String 621 } else { 622 *source = "" 623 } 624 625 return nil 626}