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