forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
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.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 GetRepo(e Execer, did, name string) (*Repo, error) {
313 var repo Repo
314 var description, spindle sql.NullString
315
316 row := e.QueryRow(`
317 select did, name, knot, created, description, spindle, rkey
318 from repos
319 where did = ? and name = ?
320 `,
321 did,
322 name,
323 )
324
325 var createdAt string
326 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil {
327 return nil, err
328 }
329 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
330 repo.Created = createdAtTime
331
332 if description.Valid {
333 repo.Description = description.String
334 }
335
336 if spindle.Valid {
337 repo.Spindle = spindle.String
338 }
339
340 return &repo, nil
341}
342
343func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) {
344 var repo Repo
345 var nullableDescription sql.NullString
346
347 row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri)
348
349 var createdAt string
350 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil {
351 return nil, err
352 }
353 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
354 repo.Created = createdAtTime
355
356 if nullableDescription.Valid {
357 repo.Description = nullableDescription.String
358 } else {
359 repo.Description = ""
360 }
361
362 return &repo, nil
363}
364
365func AddRepo(e Execer, repo *Repo) error {
366 _, err := e.Exec(
367 `insert into repos
368 (did, name, knot, rkey, at_uri, description, source)
369 values (?, ?, ?, ?, ?, ?, ?)`,
370 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source,
371 )
372 return err
373}
374
375func RemoveRepo(e Execer, did, name string) error {
376 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
377 return err
378}
379
380func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
381 var nullableSource sql.NullString
382 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
383 if err != nil {
384 return "", err
385 }
386 return nullableSource.String, nil
387}
388
389func GetForksByDid(e Execer, did string) ([]Repo, error) {
390 var repos []Repo
391
392 rows, err := e.Query(
393 `select distinct r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source
394 from repos r
395 left join collaborators c on r.at_uri = c.repo_at
396 where (r.did = ? or c.subject_did = ?)
397 and r.source is not null
398 and r.source != ''
399 order by r.created desc`,
400 did, did,
401 )
402 if err != nil {
403 return nil, err
404 }
405 defer rows.Close()
406
407 for rows.Next() {
408 var repo Repo
409 var createdAt string
410 var nullableDescription sql.NullString
411 var nullableSource sql.NullString
412
413 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
414 if err != nil {
415 return nil, err
416 }
417
418 if nullableDescription.Valid {
419 repo.Description = nullableDescription.String
420 }
421
422 if nullableSource.Valid {
423 repo.Source = nullableSource.String
424 }
425
426 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
427 if err != nil {
428 repo.Created = time.Now()
429 } else {
430 repo.Created = createdAtTime
431 }
432
433 repos = append(repos, repo)
434 }
435
436 if err := rows.Err(); err != nil {
437 return nil, err
438 }
439
440 return repos, nil
441}
442
443func GetForkByDid(e Execer, did string, name string) (*Repo, error) {
444 var repo Repo
445 var createdAt string
446 var nullableDescription sql.NullString
447 var nullableSource sql.NullString
448
449 row := e.QueryRow(
450 `select did, name, knot, rkey, description, created, source
451 from repos
452 where did = ? and name = ? and source is not null and source != ''`,
453 did, name,
454 )
455
456 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
457 if err != nil {
458 return nil, err
459 }
460
461 if nullableDescription.Valid {
462 repo.Description = nullableDescription.String
463 }
464
465 if nullableSource.Valid {
466 repo.Source = nullableSource.String
467 }
468
469 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
470 if err != nil {
471 repo.Created = time.Now()
472 } else {
473 repo.Created = createdAtTime
474 }
475
476 return &repo, nil
477}
478
479func UpdateDescription(e Execer, repoAt, newDescription string) error {
480 _, err := e.Exec(
481 `update repos set description = ? where at_uri = ?`, newDescription, repoAt)
482 return err
483}
484
485func UpdateSpindle(e Execer, repoAt string, spindle *string) error {
486 _, err := e.Exec(
487 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt)
488 return err
489}
490
491type RepoStats struct {
492 Language string
493 StarCount int
494 IssueCount IssueCount
495 PullCount PullCount
496}