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