A vibe coded tangled fork which supports pijul.
at 5bf28708dcf8972c724fb0c33fcab1281cbc3f27 630 lines 15 kB view raw
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}