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