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