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