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