forked from tangled.org/core
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 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}