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(*) 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}