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