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