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