This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / db / repos.go
22 kB 859 lines
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "log" 9 "slices" 10 "strings" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/appview/pagination" 17 "tangled.org/core/orm" 18) 19 20func RenameRepo(tx *sql.Tx, did, oldRkey, newRkey, newName string) error { 21 newAtURI := fmt.Sprintf("at://%s/sh.tangled.repo/%s", did, newRkey) 22 23 res, err := tx.Exec( 24 `update repos set rkey = ?, name = ?, at_uri = ? where did = ? and rkey = ?`, 25 newRkey, newName, newAtURI, did, oldRkey, 26 ) 27 if err != nil { 28 return fmt.Errorf("update repos row: %w", err) 29 } 30 if n, _ := res.RowsAffected(); n == 0 { 31 return fmt.Errorf("no repo row found for did=%s rkey=%s", did, oldRkey) 32 } 33 34 if _, err := tx.Exec( 35 `update pipelines set repo_name = ? where repo_owner = ? and repo_name = ?`, 36 newRkey, did, oldRkey, 37 ); err != nil { 38 return fmt.Errorf("rename pipelines.repo_name: %w", err) 39 } 40 41 return nil 42} 43 44func UpdateRepoDisplayName(e Execer, did, rkey, newName string) error { 45 _, err := e.Exec( 46 `update repos set name = ? where did = ? and rkey = ?`, 47 newName, did, rkey, 48 ) 49 return err 50} 51 52func RecordRepoRename(e Execer, ownerDid, oldRkey, repoDid string) error { 53 _, err := e.Exec( 54 `insert into repo_renames (owner_did, old_rkey, repo_did) 55 values (?, ?, ?) 56 on conflict(owner_did, old_rkey) do update set 57 repo_did = excluded.repo_did, 58 renamed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`, 59 ownerDid, oldRkey, repoDid, 60 ) 61 return err 62} 63 64func DeleteRepoRename(e Execer, ownerDid, oldRkey string) error { 65 _, err := e.Exec( 66 `delete from repo_renames where owner_did = ? and old_rkey = ?`, 67 ownerDid, oldRkey, 68 ) 69 return err 70} 71 72func LookupRepoRename(e Execer, ownerDid, oldRkey string) (*models.Repo, error) { 73 var repoDid string 74 err := e.QueryRow( 75 `select repo_did from repo_renames where owner_did = ? and old_rkey = ?`, 76 ownerDid, oldRkey, 77 ).Scan(&repoDid) 78 if err != nil { 79 return nil, err 80 } 81 82 repo, err := GetRepoByDid(e, repoDid) 83 if err != nil { 84 return nil, err 85 } 86 return repo, nil 87} 88 89func GetRepos(e Execer, filters ...orm.Filter) ([]models.Repo, error) { 90 return GetReposPaginated(e, pagination.Page{}, filters...) 91} 92 93func GetReposPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Repo, error) { 94 var conditions []string 95 var args []any 96 for _, filter := range filters { 97 conditions = append(conditions, filter.Condition()) 98 args = append(args, filter.Arg()...) 99 } 100 101 whereClause := "" 102 if conditions != nil { 103 whereClause = " where " + strings.Join(conditions, " and ") 104 } 105 106 pageClause := "" 107 if page.Limit != 0 { 108 pageClause = fmt.Sprintf(" limit %d offset %d", page.Limit, page.Offset) 109 } 110 111 // main query to get repos with pagination 112 query := fmt.Sprintf(` 113 select 114 id, 115 did, 116 name, 117 knot, 118 rkey, 119 created, 120 description, 121 website, 122 topics, 123 source, 124 spindle, 125 repo_did 126 from repos 127 %s 128 order by created desc 129 %s 130 `, whereClause, pageClause) 131 132 rows, err := e.Query(query, args...) 133 if err != nil { 134 return nil, err 135 } 136 defer rows.Close() 137 138 repoMap := make(map[string]*models.Repo) 139 for rows.Next() { 140 var repo models.Repo 141 var createdAt string 142 var description, website, topicStr, source, spindle, repoDid sql.NullString 143 144 err := rows.Scan( 145 &repo.Id, 146 &repo.Did, 147 &repo.Name, 148 &repo.Knot, 149 &repo.Rkey, 150 &createdAt, 151 &description, 152 &website, 153 &topicStr, 154 &source, 155 &spindle, 156 &repoDid, 157 ) 158 if err != nil { 159 return nil, err 160 } 161 162 // parse created timestamp 163 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 164 repo.Created = t 165 } 166 167 // handle nullable fields 168 if description.Valid { 169 repo.Description = description.String 170 } 171 if website.Valid { 172 repo.Website = website.String 173 } 174 if topicStr.Valid { 175 repo.Topics = strings.Fields(topicStr.String) 176 } 177 if source.Valid { 178 repo.Source = source.String 179 } 180 if spindle.Valid { 181 repo.Spindle = spindle.String 182 } 183 if repoDid.Valid { 184 repo.RepoDid = repoDid.String 185 } 186 187 repo.RepoStats = &models.RepoStats{} 188 repoMap[repo.RepoDid] = &repo 189 } 190 191 if err = rows.Err(); err != nil { 192 return nil, err 193 } 194 195 // if no repos, return early 196 if len(repoMap) == 0 { 197 return nil, nil 198 } 199 200 // build IN clause for related queries 201 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ") 202 args = make([]any, len(repoMap)) 203 i := 0 204 for _, r := range repoMap { 205 args[i] = r.RepoDid 206 i++ 207 } 208 209 // get labels for all repos 210 { 211 labelsQuery := fmt.Sprintf( 212 `select repo_did, label_at from repo_labels where repo_did in (%s)`, 213 inClause, 214 ) 215 216 rows, err = e.Query(labelsQuery, args...) 217 if err != nil { 218 return nil, fmt.Errorf("failed to execute repo_labels query: %w", err) 219 } 220 defer rows.Close() 221 222 for rows.Next() { 223 var repoDid, labelat string 224 if err := rows.Scan(&repoDid, &labelat); err != nil { 225 log.Println("err", err) 226 continue 227 } 228 if r, ok := repoMap[repoDid]; ok { 229 r.Labels = append(r.Labels, labelat) 230 } 231 } 232 if err = rows.Err(); err != nil { 233 return nil, fmt.Errorf("failed to execute repo_labels query: %w", err) 234 } 235 } 236 237 // get primary language for all repos 238 { 239 languageQuery := fmt.Sprintf(` 240 select repo_did, language 241 from ( 242 select 243 repo_did, language, 244 row_number() over ( 245 partition by repo_did 246 order by bytes desc 247 ) as rn 248 from repo_languages 249 where repo_did in (%s) 250 and is_default_ref = 1 251 and language <> '' 252 ) 253 where rn = 1 254 `, inClause) 255 256 rows, err = e.Query(languageQuery, args...) 257 if err != nil { 258 return nil, fmt.Errorf("failed to execute lang query: %w", err) 259 } 260 defer rows.Close() 261 262 for rows.Next() { 263 var repoDid, lang string 264 if err := rows.Scan(&repoDid, &lang); err != nil { 265 log.Println("err", err) 266 continue 267 } 268 if r, ok := repoMap[repoDid]; ok { 269 r.RepoStats.Language = lang 270 } 271 } 272 if err = rows.Err(); err != nil { 273 return nil, fmt.Errorf("failed to execute lang query: %w", err) 274 } 275 } 276 277 // get star counts 278 { 279 starCountQuery := fmt.Sprintf( 280 `select subject, count(*) from deduped_stars where subject_type = 'repo' and subject in (%s) group by subject`, 281 inClause, 282 ) 283 284 rows, err = e.Query(starCountQuery, args...) 285 if err != nil { 286 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 287 } 288 defer rows.Close() 289 290 for rows.Next() { 291 var repoDid string 292 var count int 293 if err := rows.Scan(&repoDid, &count); err != nil { 294 log.Println("err", "err", err) 295 continue 296 } 297 if r, ok := repoMap[repoDid]; ok { 298 r.RepoStats.StarCount = count 299 } 300 } 301 if err = rows.Err(); err != nil { 302 return nil, fmt.Errorf("failed to execute star-count query: %w", err) 303 } 304 } 305 306 // get issue counts 307 { 308 issueCountQuery := fmt.Sprintf(` 309 select 310 repo_did, 311 count(case when open = 1 then 1 end) as open_count, 312 count(case when open = 0 then 1 end) as closed_count 313 from issues 314 where repo_did in (%s) 315 group by repo_did 316 `, inClause) 317 318 rows, err = e.Query(issueCountQuery, args...) 319 if err != nil { 320 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 321 } 322 defer rows.Close() 323 324 for rows.Next() { 325 var repoDid string 326 var open, closed int 327 if err := rows.Scan(&repoDid, &open, &closed); err != nil { 328 log.Println("err", err) 329 continue 330 } 331 if r, ok := repoMap[repoDid]; ok { 332 r.RepoStats.IssueCount.Open = open 333 r.RepoStats.IssueCount.Closed = closed 334 } 335 } 336 if err = rows.Err(); err != nil { 337 return nil, fmt.Errorf("failed to execute issue-count query: %w", err) 338 } 339 } 340 341 // get pull counts 342 { 343 pullCountQuery := fmt.Sprintf(` 344 select 345 repo_did, 346 count(case when state = ? then 1 end) as open_count, 347 count(case when state = ? then 1 end) as merged_count, 348 count(case when state = ? then 1 end) as closed_count, 349 count(case when state = ? then 1 end) as deleted_count 350 from pulls 351 where repo_did in (%s) 352 group by repo_did 353 `, inClause) 354 355 pullArgs := append([]any{ 356 models.PullOpen, 357 models.PullMerged, 358 models.PullClosed, 359 models.PullAbandoned, 360 }, args...) 361 362 rows, err = e.Query(pullCountQuery, pullArgs...) 363 if err != nil { 364 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 365 } 366 defer rows.Close() 367 368 for rows.Next() { 369 var repoDid string 370 var open, merged, closed, deleted int 371 if err := rows.Scan(&repoDid, &open, &merged, &closed, &deleted); err != nil { 372 log.Println("err", "err", err) 373 continue 374 } 375 if r, ok := repoMap[repoDid]; ok { 376 r.RepoStats.PullCount.Open = open 377 r.RepoStats.PullCount.Merged = merged 378 r.RepoStats.PullCount.Closed = closed 379 r.RepoStats.PullCount.Deleted = deleted 380 } 381 } 382 if err = rows.Err(); err != nil { 383 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err) 384 } 385 } 386 387 // get forks — only query repos with a non-empty repo_did, since source 388 // stores the upstream's repo_did and an empty string would match all 389 var forksArgs []any 390 for _, r := range repoMap { 391 if r.RepoDid != "" { 392 forksArgs = append(forksArgs, r.RepoDid) 393 } 394 } 395 396 if len(forksArgs) > 0 { 397 forksInClause := strings.TrimSuffix(strings.Repeat("?, ", len(forksArgs)), ", ") 398 399 forksCountQuery := fmt.Sprintf( 400 `select source, count(1) from repos where source in (%s) group by source`, 401 forksInClause, 402 ) 403 404 rows, err = e.Query(forksCountQuery, forksArgs...) 405 if err != nil { 406 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 407 } 408 defer rows.Close() 409 410 for rows.Next() { 411 var repodid string 412 var count int 413 if err := rows.Scan(&repodid, &count); err != nil { 414 log.Println("failed to scan fork count", "err", err) 415 continue 416 } 417 418 if r, ok := repoMap[repodid]; ok { 419 r.RepoStats.ForkCount = count 420 } 421 } 422 if err = rows.Err(); err != nil { 423 return nil, fmt.Errorf("failed to execute fork-count query: %w", err) 424 } 425 } 426 427 var repos []models.Repo 428 for _, r := range repoMap { 429 repos = append(repos, *r) 430 } 431 432 // sort by created timestamp (desc) 433 slices.SortFunc(repos, func(a, b models.Repo) int { 434 if a.Created.After(b.Created) { 435 return -1 436 } 437 return 1 438 }) 439 440 return repos, nil 441} 442 443// helper to get exactly one repo 444func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) { 445 repos, err := GetReposPaginated(e, pagination.Page{Limit: 1}, filters...) 446 if err != nil { 447 return nil, err 448 } 449 450 if repos == nil { 451 return nil, sql.ErrNoRows 452 } 453 454 if len(repos) != 1 { 455 return nil, fmt.Errorf("too few rows returned") 456 } 457 458 return &repos[0], nil 459} 460 461func CountRepos(e Execer, filters ...orm.Filter) (int64, error) { 462 var conditions []string 463 var args []any 464 for _, filter := range filters { 465 conditions = append(conditions, filter.Condition()) 466 args = append(args, filter.Arg()...) 467 } 468 469 whereClause := "" 470 if conditions != nil { 471 whereClause = " where " + strings.Join(conditions, " and ") 472 } 473 474 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause) 475 var count int64 476 err := e.QueryRow(repoQuery, args...).Scan(&count) 477 478 if !errors.Is(err, sql.ErrNoRows) && err != nil { 479 return 0, err 480 } 481 482 return count, nil 483} 484 485func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { 486 var repo models.Repo 487 var nullableDescription sql.NullString 488 var nullableWebsite sql.NullString 489 var nullableTopicStr sql.NullString 490 var nullableRepoDid sql.NullString 491 var nullableSource sql.NullString 492 var nullableSpindle sql.NullString 493 494 row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 495 496 var createdAt string 497 if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 498 return nil, err 499 } 500 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 501 repo.Created = createdAtTime 502 503 if nullableDescription.Valid { 504 repo.Description = nullableDescription.String 505 } 506 if nullableWebsite.Valid { 507 repo.Website = nullableWebsite.String 508 } 509 if nullableTopicStr.Valid { 510 repo.Topics = strings.Fields(nullableTopicStr.String) 511 } 512 if nullableSource.Valid { 513 repo.Source = nullableSource.String 514 } 515 if nullableSpindle.Valid { 516 repo.Spindle = nullableSpindle.String 517 } 518 if nullableRepoDid.Valid { 519 repo.RepoDid = nullableRepoDid.String 520 } 521 522 return &repo, nil 523} 524 525func PutRepo(tx *sql.Tx, repo models.Repo) error { 526 var repoDid *string 527 if repo.RepoDid != "" { 528 repoDid = &repo.RepoDid 529 } 530 _, err := tx.Exec( 531 `update repos 532 set name = ?, knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did) 533 where did = ? and rkey = ? 534 `, 535 repo.Name, repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey, 536 ) 537 return err 538} 539 540func AddRepo(tx *sql.Tx, repo *models.Repo) error { 541 var repoDid *string 542 if repo.RepoDid != "" { 543 repoDid = &repo.RepoDid 544 } 545 result, err := tx.Exec( 546 `insert into repos 547 (did, name, knot, rkey, at_uri, description, website, topics, source, repo_did) 548 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 549 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, repoDid, 550 ) 551 if err != nil { 552 return fmt.Errorf("failed to insert repo: %w", err) 553 } 554 555 id, err := result.LastInsertId() 556 if err != nil { 557 return fmt.Errorf("failed to get last insert id: %w", err) 558 } 559 repo.Id = id 560 561 for _, dl := range repo.Labels { 562 if err := SubscribeLabel(tx, &models.RepoLabel{ 563 RepoDid: syntax.DID(repo.RepoDid), 564 LabelAt: syntax.ATURI(dl), 565 }); err != nil { 566 return fmt.Errorf("failed to subscribe to label: %w", err) 567 } 568 } 569 570 return nil 571} 572 573func RemoveRepo(e Execer, did, rkey string) error { 574 _, err := e.Exec(`delete from repos where did = ? and rkey = ?`, did, rkey) 575 return err 576} 577 578func GetRepoSource(e Execer, repoDid string) (string, error) { 579 var nullableSource sql.NullString 580 err := e.QueryRow(`select source from repos where repo_did = ?`, repoDid).Scan(&nullableSource) 581 if err != nil { 582 return "", err 583 } 584 return nullableSource.String, nil 585} 586 587func GetRepoSourceRepo(e Execer, repoDid string) (*models.Repo, error) { 588 source, err := GetRepoSource(e, repoDid) 589 if source == "" || errors.Is(err, sql.ErrNoRows) { 590 return nil, nil 591 } 592 if err != nil { 593 return nil, err 594 } 595 if strings.HasPrefix(source, "did:") { 596 return GetRepoByDid(e, source) 597 } 598 return GetRepoByAtUri(e, source) 599} 600 601func GetForksByDid(e Execer, did string) ([]models.Repo, error) { 602 var repos []models.Repo 603 604 rows, err := e.Query( 605 `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did 606 from repos r 607 left join collaborators c on r.repo_did = c.repo_did 608 where (r.did = ? or c.subject_did = ?) 609 and r.source is not null 610 and r.source != '' 611 order by r.created desc`, 612 did, did, 613 ) 614 if err != nil { 615 return nil, err 616 } 617 defer rows.Close() 618 619 for rows.Next() { 620 var repo models.Repo 621 var createdAt string 622 var nullableDescription sql.NullString 623 var nullableWebsite sql.NullString 624 var nullableSource sql.NullString 625 var nullableRepoDid sql.NullString 626 627 err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid) 628 if err != nil { 629 return nil, err 630 } 631 632 if nullableDescription.Valid { 633 repo.Description = nullableDescription.String 634 } 635 if nullableWebsite.Valid { 636 repo.Website = nullableWebsite.String 637 } 638 639 if nullableSource.Valid { 640 repo.Source = nullableSource.String 641 } 642 if nullableRepoDid.Valid { 643 repo.RepoDid = nullableRepoDid.String 644 } 645 646 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 647 if err != nil { 648 repo.Created = time.Now() 649 } else { 650 repo.Created = createdAtTime 651 } 652 653 repos = append(repos, repo) 654 } 655 656 if err := rows.Err(); err != nil { 657 return nil, err 658 } 659 660 return repos, nil 661} 662 663func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) { 664 return GetRepo(e, orm.FilterEq("repo_did", repoDid)) 665} 666 667func GetForkByRepoDid(e Execer, repoDid string) (*models.Repo, error) { 668 return GetRepo(e, orm.FilterEq("repo_did", repoDid), orm.FilterNotEq("source", "")) 669} 670 671// TODO: just queue every legacy records regardless of target repo has a DID or not. 672// doable after we have `repo_did` column in db for each tables. 673func EnqueuePdsRewritesForRepo(tx *sql.Tx, repoDid, repoAtUri string) error { 674 type record struct { 675 userDidCol string 676 table string 677 nsid syntax.NSID 678 fkCol string 679 fkVal string 680 } 681 sources := []record{ 682 {"did", "repos", tangled.RepoNSID, "at_uri", repoAtUri}, 683 {"did", "issues", tangled.RepoIssueNSID, "repo_did", repoDid}, 684 {"owner_did", "pulls", tangled.RepoPullNSID, "repo_did", repoDid}, 685 {"did", "collaborators", tangled.RepoCollaboratorNSID, "repo_did", repoDid}, 686 {"did", "artifacts", tangled.RepoArchiveNSID, "repo_did", repoDid}, 687 {"did", "stars", tangled.FeedStarNSID, "subject", repoDid}, 688 } 689 690 for _, src := range sources { 691 rows, err := tx.Query( 692 fmt.Sprintf(`SELECT %s, rkey FROM %s WHERE %s = ?`, src.userDidCol, src.table, src.fkCol), 693 src.fkVal, 694 ) 695 if err != nil { 696 return fmt.Errorf("query %s for pds rewrites: %w", src.table, err) 697 } 698 699 var pairs []struct{ did, rkey string } 700 for rows.Next() { 701 var d string 702 var r sql.NullString 703 if scanErr := rows.Scan(&d, &r); scanErr != nil { 704 rows.Close() 705 return fmt.Errorf("scan %s for pds rewrites: %w", src.table, scanErr) 706 } 707 if !r.Valid { 708 continue 709 } 710 pairs = append(pairs, struct{ did, rkey string }{d, r.String}) 711 } 712 rows.Close() 713 if rowsErr := rows.Err(); rowsErr != nil { 714 return fmt.Errorf("iterate %s for pds rewrites: %w", src.table, rowsErr) 715 } 716 717 for _, p := range pairs { 718 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(p.did), src.nsid, syntax.RecordKey(p.rkey)); err != nil { 719 return fmt.Errorf("enqueue pds rewrite for %s/%s: %w", src.table, p.rkey, err) 720 } 721 } 722 } 723 724 profileRows, err := tx.Query( 725 `SELECT DISTINCT did FROM profile_pinned_repositories WHERE pin = ?`, 726 repoAtUri, 727 ) 728 if err != nil { 729 return fmt.Errorf("query profile_pinned_repositories for pds rewrites: %w", err) 730 } 731 var profileDids []string 732 for profileRows.Next() { 733 var d string 734 if scanErr := profileRows.Scan(&d); scanErr != nil { 735 profileRows.Close() 736 return fmt.Errorf("scan profile_pinned_repositories for pds rewrites: %w", scanErr) 737 } 738 profileDids = append(profileDids, d) 739 } 740 profileRows.Close() 741 if profileRowsErr := profileRows.Err(); profileRowsErr != nil { 742 return fmt.Errorf("iterate profile_pinned_repositories for pds rewrites: %w", profileRowsErr) 743 } 744 745 for _, d := range profileDids { 746 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(d), tangled.ActorProfileNSID, "self"); err != nil { 747 return fmt.Errorf("enqueue pds rewrite for profile/%s: %w", d, err) 748 } 749 } 750 751 return nil 752} 753 754func CascadeRepoDid(tx *sql.Tx, repoAtUri, repoDid string) error { 755 _, err := tx.Exec( 756 `UPDATE repos SET repo_did = ? WHERE at_uri = ?`, 757 repoDid, repoAtUri, 758 ) 759 if err != nil { 760 return fmt.Errorf("cascade repo_did to repos: %w", err) 761 } 762 763 _, err = tx.Exec( 764 `UPDATE repos SET source = ? WHERE source = ?`, 765 repoDid, repoAtUri, 766 ) 767 if err != nil { 768 return fmt.Errorf("cascade repo_did to repos.source: %w", err) 769 } 770 771 return nil 772} 773 774func UpdateDescription(e Execer, repoDid, newDescription string) error { 775 _, err := e.Exec( 776 `update repos set description = ? where repo_did = ?`, newDescription, repoDid) 777 return err 778} 779 780func UpdateSpindle(e Execer, repoDid string, spindle *string) error { 781 _, err := e.Exec( 782 `update repos set spindle = ? where repo_did = ?`, spindle, repoDid) 783 return err 784} 785 786func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 787 query := `insert or ignore into repo_labels (repo_did, label_at) values (?, ?)` 788 789 _, err := e.Exec(query, string(rl.RepoDid), rl.LabelAt.String()) 790 return err 791} 792 793func UnsubscribeLabel(e Execer, filters ...orm.Filter) error { 794 var conditions []string 795 var args []any 796 for _, filter := range filters { 797 conditions = append(conditions, filter.Condition()) 798 args = append(args, filter.Arg()...) 799 } 800 801 whereClause := "" 802 if conditions != nil { 803 whereClause = " where " + strings.Join(conditions, " and ") 804 } 805 806 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause) 807 _, err := e.Exec(query, args...) 808 return err 809} 810 811func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) { 812 var conditions []string 813 var args []any 814 for _, filter := range filters { 815 conditions = append(conditions, filter.Condition()) 816 args = append(args, filter.Arg()...) 817 } 818 819 whereClause := "" 820 if conditions != nil { 821 whereClause = " where " + strings.Join(conditions, " and ") 822 } 823 824 query := fmt.Sprintf(`select id, repo_did, label_at from repo_labels %s`, whereClause) 825 826 rows, err := e.Query(query, args...) 827 if err != nil { 828 return nil, err 829 } 830 defer rows.Close() 831 832 var labels []models.RepoLabel 833 for rows.Next() { 834 var label models.RepoLabel 835 836 err := rows.Scan(&label.Id, &label.RepoDid, &label.LabelAt) 837 if err != nil { 838 return nil, err 839 } 840 841 labels = append(labels, label) 842 } 843 844 if err = rows.Err(); err != nil { 845 return nil, err 846 } 847 848 return labels, nil 849} 850 851func GetForkCount(e Execer, sourceDID string) (int, error) { 852 forks := 0 853 err := e.QueryRow( 854 `select count(source) from repos where source = ?`, sourceDID).Scan(&forks) 855 if err != nil { 856 return 0, err 857 } 858 return forks, nil 859}