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