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