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