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