This repository has no description
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(1) from 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 RemoveReposByKnot(e Execer, knot string) error {
579 _, err := e.Exec(`delete from repos where knot = ?`, knot)
580 return err
581}
582
583func GetRepoSource(e Execer, repoDid string) (string, error) {
584 var nullableSource sql.NullString
585 err := e.QueryRow(`select source from repos where repo_did = ?`, repoDid).Scan(&nullableSource)
586 if err != nil {
587 return "", err
588 }
589 return nullableSource.String, nil
590}
591
592func GetRepoSourceRepo(e Execer, repoDid string) (*models.Repo, error) {
593 source, err := GetRepoSource(e, repoDid)
594 if source == "" || errors.Is(err, sql.ErrNoRows) {
595 return nil, nil
596 }
597 if err != nil {
598 return nil, err
599 }
600 if strings.HasPrefix(source, "did:") {
601 return GetRepoByDid(e, source)
602 }
603 return GetRepoByAtUri(e, source)
604}
605
606func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
607 var repos []models.Repo
608
609 rows, err := e.Query(
610 `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did
611 from repos r
612 left join collaborators c on r.repo_did = c.repo_did
613 where (r.did = ? or c.subject_did = ?)
614 and r.source is not null
615 and r.source != ''
616 order by r.created desc`,
617 did, did,
618 )
619 if err != nil {
620 return nil, err
621 }
622 defer rows.Close()
623
624 for rows.Next() {
625 var repo models.Repo
626 var createdAt string
627 var nullableDescription sql.NullString
628 var nullableWebsite sql.NullString
629 var nullableSource sql.NullString
630 var nullableRepoDid sql.NullString
631
632 err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid)
633 if err != nil {
634 return nil, err
635 }
636
637 if nullableDescription.Valid {
638 repo.Description = nullableDescription.String
639 }
640 if nullableWebsite.Valid {
641 repo.Website = nullableWebsite.String
642 }
643
644 if nullableSource.Valid {
645 repo.Source = nullableSource.String
646 }
647 if nullableRepoDid.Valid {
648 repo.RepoDid = nullableRepoDid.String
649 }
650
651 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
652 if err != nil {
653 repo.Created = time.Now()
654 } else {
655 repo.Created = createdAtTime
656 }
657
658 repos = append(repos, repo)
659 }
660
661 if err := rows.Err(); err != nil {
662 return nil, err
663 }
664
665 return repos, nil
666}
667
668func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) {
669 return GetRepo(e, orm.FilterEq("repo_did", repoDid))
670}
671
672func GetForkByRepoDid(e Execer, repoDid string) (*models.Repo, error) {
673 return GetRepo(e, orm.FilterEq("repo_did", repoDid), orm.FilterNotEq("source", ""))
674}
675
676// TODO: just queue every legacy records regardless of target repo has a DID or not.
677// doable after we have `repo_did` column in db for each tables.
678func EnqueuePdsRewritesForRepo(tx *sql.Tx, repoDid, repoAtUri string) error {
679 type record struct {
680 userDidCol string
681 table string
682 nsid syntax.NSID
683 fkCol string
684 fkVal string
685 }
686 sources := []record{
687 {"did", "repos", tangled.RepoNSID, "at_uri", repoAtUri},
688 {"did", "issues", tangled.RepoIssueNSID, "repo_did", repoDid},
689 {"owner_did", "pulls", tangled.RepoPullNSID, "repo_did", repoDid},
690 {"did", "collaborators", tangled.RepoCollaboratorNSID, "repo_did", repoDid},
691 {"did", "artifacts", tangled.RepoArchiveNSID, "repo_did", repoDid},
692 {"did", "stars", tangled.FeedStarNSID, "subject", repoDid},
693 }
694
695 for _, src := range sources {
696 rows, err := tx.Query(
697 fmt.Sprintf(`SELECT %s, rkey FROM %s WHERE %s = ?`, src.userDidCol, src.table, src.fkCol),
698 src.fkVal,
699 )
700 if err != nil {
701 return fmt.Errorf("query %s for pds rewrites: %w", src.table, err)
702 }
703
704 var pairs []struct{ did, rkey string }
705 for rows.Next() {
706 var d string
707 var r sql.NullString
708 if scanErr := rows.Scan(&d, &r); scanErr != nil {
709 rows.Close()
710 return fmt.Errorf("scan %s for pds rewrites: %w", src.table, scanErr)
711 }
712 if !r.Valid {
713 continue
714 }
715 pairs = append(pairs, struct{ did, rkey string }{d, r.String})
716 }
717 rows.Close()
718 if rowsErr := rows.Err(); rowsErr != nil {
719 return fmt.Errorf("iterate %s for pds rewrites: %w", src.table, rowsErr)
720 }
721
722 for _, p := range pairs {
723 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(p.did), src.nsid, syntax.RecordKey(p.rkey)); err != nil {
724 return fmt.Errorf("enqueue pds rewrite for %s/%s: %w", src.table, p.rkey, err)
725 }
726 }
727 }
728
729 profileRows, err := tx.Query(
730 `SELECT DISTINCT did FROM profile_pinned_repositories WHERE pin = ?`,
731 repoAtUri,
732 )
733 if err != nil {
734 return fmt.Errorf("query profile_pinned_repositories for pds rewrites: %w", err)
735 }
736 var profileDids []string
737 for profileRows.Next() {
738 var d string
739 if scanErr := profileRows.Scan(&d); scanErr != nil {
740 profileRows.Close()
741 return fmt.Errorf("scan profile_pinned_repositories for pds rewrites: %w", scanErr)
742 }
743 profileDids = append(profileDids, d)
744 }
745 profileRows.Close()
746 if profileRowsErr := profileRows.Err(); profileRowsErr != nil {
747 return fmt.Errorf("iterate profile_pinned_repositories for pds rewrites: %w", profileRowsErr)
748 }
749
750 for _, d := range profileDids {
751 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(d), tangled.ActorProfileNSID, "self"); err != nil {
752 return fmt.Errorf("enqueue pds rewrite for profile/%s: %w", d, err)
753 }
754 }
755
756 return nil
757}
758
759func CascadeRepoDid(tx *sql.Tx, repoAtUri, repoDid string) error {
760 _, err := tx.Exec(
761 `UPDATE repos SET repo_did = ? WHERE at_uri = ?`,
762 repoDid, repoAtUri,
763 )
764 if err != nil {
765 return fmt.Errorf("cascade repo_did to repos: %w", err)
766 }
767
768 _, err = tx.Exec(
769 `UPDATE repos SET source = ? WHERE source = ?`,
770 repoDid, repoAtUri,
771 )
772 if err != nil {
773 return fmt.Errorf("cascade repo_did to repos.source: %w", err)
774 }
775
776 return nil
777}
778
779func UpdateDescription(e Execer, repoDid, newDescription string) error {
780 _, err := e.Exec(
781 `update repos set description = ? where repo_did = ?`, newDescription, repoDid)
782 return err
783}
784
785func UpdateSpindle(e Execer, repoDid string, spindle *string) error {
786 _, err := e.Exec(
787 `update repos set spindle = ? where repo_did = ?`, spindle, repoDid)
788 return err
789}
790
791func SubscribeLabel(e Execer, rl *models.RepoLabel) error {
792 query := `insert or ignore into repo_labels (repo_did, label_at) values (?, ?)`
793
794 _, err := e.Exec(query, string(rl.RepoDid), rl.LabelAt.String())
795 return err
796}
797
798func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
799 var conditions []string
800 var args []any
801 for _, filter := range filters {
802 conditions = append(conditions, filter.Condition())
803 args = append(args, filter.Arg()...)
804 }
805
806 whereClause := ""
807 if conditions != nil {
808 whereClause = " where " + strings.Join(conditions, " and ")
809 }
810
811 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause)
812 _, err := e.Exec(query, args...)
813 return err
814}
815
816func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
817 var conditions []string
818 var args []any
819 for _, filter := range filters {
820 conditions = append(conditions, filter.Condition())
821 args = append(args, filter.Arg()...)
822 }
823
824 whereClause := ""
825 if conditions != nil {
826 whereClause = " where " + strings.Join(conditions, " and ")
827 }
828
829 query := fmt.Sprintf(`select id, repo_did, label_at from repo_labels %s`, whereClause)
830
831 rows, err := e.Query(query, args...)
832 if err != nil {
833 return nil, err
834 }
835 defer rows.Close()
836
837 var labels []models.RepoLabel
838 for rows.Next() {
839 var label models.RepoLabel
840
841 err := rows.Scan(&label.Id, &label.RepoDid, &label.LabelAt)
842 if err != nil {
843 return nil, err
844 }
845
846 labels = append(labels, label)
847 }
848
849 if err = rows.Err(); err != nil {
850 return nil, err
851 }
852
853 return labels, nil
854}
855
856func GetForkCount(e Execer, sourceDID string) (int, error) {
857 forks := 0
858 err := e.QueryRow(
859 `select count(source) from repos where source = ?`, sourceDID).Scan(&forks)
860 if err != nil {
861 return 0, err
862 }
863 return forks, nil
864}