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