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 337func CountRepos(e Execer, filters ...filter) (int64, error) { 338 var conditions []string 339 var args []any 340 for _, filter := range filters { 341 conditions = append(conditions, filter.Condition()) 342 args = append(args, filter.Arg()...) 343 } 344 345 whereClause := "" 346 if conditions != nil { 347 whereClause = " where " + strings.Join(conditions, " and ") 348 } 349 350 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause) 351 var count int64 352 err := e.QueryRow(repoQuery, args...).Scan(&count) 353 354 if !errors.Is(err, sql.ErrNoRows) && err != nil { 355 return 0, err 356 } 357 358 return count, nil 359} 360 361func GetRepo(e Execer, did, name string) (*Repo, error) { 362 var repo Repo 363 var description, spindle sql.NullString 364 365 row := e.QueryRow(` 366 select did, name, knot, created, description, spindle, rkey 367 from repos 368 where did = ? and name = ? 369 `, 370 did, 371 name, 372 ) 373 374 var createdAt string 375 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil { 376 return nil, err 377 } 378 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 379 repo.Created = createdAtTime 380 381 if description.Valid { 382 repo.Description = description.String 383 } 384 385 if spindle.Valid { 386 repo.Spindle = spindle.String 387 } 388 389 return &repo, nil 390} 391 392func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 393 var repo Repo 394 var nullableDescription sql.NullString 395 396 row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) 397 398 var createdAt string 399 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { 400 return nil, err 401 } 402 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 403 repo.Created = createdAtTime 404 405 if nullableDescription.Valid { 406 repo.Description = nullableDescription.String 407 } else { 408 repo.Description = "" 409 } 410 411 return &repo, nil 412} 413 414func AddRepo(e Execer, repo *Repo) error { 415 _, err := e.Exec( 416 `insert into repos 417 (did, name, knot, rkey, at_uri, description, source) 418 values (?, ?, ?, ?, ?, ?, ?)`, 419 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, 420 ) 421 return err 422} 423 424func RemoveRepo(e Execer, did, name string) error { 425 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 426 return err 427} 428 429func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) { 430 var nullableSource sql.NullString 431 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 432 if err != nil { 433 return "", err 434 } 435 return nullableSource.String, nil 436} 437 438func GetForksByDid(e Execer, did string) ([]Repo, error) { 439 var repos []Repo 440 441 rows, err := e.Query( 442 `select distinct r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source 443 from repos r 444 left join collaborators c on r.at_uri = c.repo_at 445 where (r.did = ? or c.subject_did = ?) 446 and r.source is not null 447 and r.source != '' 448 order by r.created desc`, 449 did, did, 450 ) 451 if err != nil { 452 return nil, err 453 } 454 defer rows.Close() 455 456 for rows.Next() { 457 var repo Repo 458 var createdAt string 459 var nullableDescription sql.NullString 460 var nullableSource sql.NullString 461 462 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 463 if err != nil { 464 return nil, err 465 } 466 467 if nullableDescription.Valid { 468 repo.Description = nullableDescription.String 469 } 470 471 if nullableSource.Valid { 472 repo.Source = nullableSource.String 473 } 474 475 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 476 if err != nil { 477 repo.Created = time.Now() 478 } else { 479 repo.Created = createdAtTime 480 } 481 482 repos = append(repos, repo) 483 } 484 485 if err := rows.Err(); err != nil { 486 return nil, err 487 } 488 489 return repos, nil 490} 491 492func GetForkByDid(e Execer, did string, name string) (*Repo, error) { 493 var repo Repo 494 var createdAt string 495 var nullableDescription sql.NullString 496 var nullableSource sql.NullString 497 498 row := e.QueryRow( 499 `select did, name, knot, rkey, description, created, source 500 from repos 501 where did = ? and name = ? and source is not null and source != ''`, 502 did, name, 503 ) 504 505 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 506 if err != nil { 507 return nil, err 508 } 509 510 if nullableDescription.Valid { 511 repo.Description = nullableDescription.String 512 } 513 514 if nullableSource.Valid { 515 repo.Source = nullableSource.String 516 } 517 518 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 519 if err != nil { 520 repo.Created = time.Now() 521 } else { 522 repo.Created = createdAtTime 523 } 524 525 return &repo, nil 526} 527 528func UpdateDescription(e Execer, repoAt, newDescription string) error { 529 _, err := e.Exec( 530 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 531 return err 532} 533 534func UpdateSpindle(e Execer, repoAt string, spindle *string) error { 535 _, err := e.Exec( 536 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt) 537 return err 538} 539 540type RepoStats struct { 541 Language string 542 StarCount int 543 IssueCount IssueCount 544 PullCount PullCount 545} 546 547type RepoLabel struct { 548 Id int64 549 RepoAt syntax.ATURI 550 LabelAt syntax.ATURI 551} 552 553func SubscribeLabel(e Execer, rl *RepoLabel) error { 554 query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)` 555 556 _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String()) 557 return err 558} 559 560func UnsubscribeLabel(e Execer, filters ...filter) error { 561 var conditions []string 562 var args []any 563 for _, filter := range filters { 564 conditions = append(conditions, filter.Condition()) 565 args = append(args, filter.Arg()...) 566 } 567 568 whereClause := "" 569 if conditions != nil { 570 whereClause = " where " + strings.Join(conditions, " and ") 571 } 572 573 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause) 574 _, err := e.Exec(query, args...) 575 return err 576} 577 578func GetRepoLabels(e Execer, filters ...filter) ([]RepoLabel, error) { 579 var conditions []string 580 var args []any 581 for _, filter := range filters { 582 conditions = append(conditions, filter.Condition()) 583 args = append(args, filter.Arg()...) 584 } 585 586 whereClause := "" 587 if conditions != nil { 588 whereClause = " where " + strings.Join(conditions, " and ") 589 } 590 591 query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause) 592 593 rows, err := e.Query(query, args...) 594 if err != nil { 595 return nil, err 596 } 597 defer rows.Close() 598 599 var labels []RepoLabel 600 for rows.Next() { 601 var label RepoLabel 602 603 err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt) 604 if err != nil { 605 return nil, err 606 } 607 608 labels = append(labels, label) 609 } 610 611 if err = rows.Err(); err != nil { 612 return nil, err 613 } 614 615 return labels, nil 616}