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