forked from tangled.org/core
Monorepo for Tangled

appview: instrument all the things

anirudh.fi df653304 5c8b5c1f

verified
+31 -6
appview/db/issues.go
··· 1 package db 2 3 import ( 4 "database/sql" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "tangled.sh/tangled.sh/core/appview/pagination" 9 ) 10 ··· 103 return ownerDid, err 104 } 105 106 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 107 var issues []Issue 108 openValue := 0 109 if isOpen { 110 openValue = 1 111 } 112 113 - rows, err := e.Query( 114 ` 115 with numbered_issue as ( 116 select ··· 139 body, 140 open, 141 comment_count 142 - from 143 numbered_issue 144 - where 145 row_num between ? and ?`, 146 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 147 if err != nil { 148 return nil, err 149 } 150 defer rows.Close() ··· 155 var metadata IssueMetadata 156 err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 157 if err != nil { 158 return nil, err 159 } 160 161 createdTime, err := time.Parse(time.RFC3339, createdAt) 162 if err != nil { 163 return nil, err 164 } 165 issue.Created = createdTime ··· 169 } 170 171 if err := rows.Err(); err != nil { 172 return nil, err 173 } 174 175 return issues, nil 176 } 177 ··· 256 return issues, nil 257 } 258 259 - func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 260 query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 261 row := e.QueryRow(query, repoAt, issueId) 262 ··· 276 return &issue, nil 277 } 278 279 - func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 280 query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 281 row := e.QueryRow(query, repoAt, issueId) 282
··· 1 package db 2 3 import ( 4 + "context" 5 "database/sql" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 + "go.opentelemetry.io/otel" 10 + "go.opentelemetry.io/otel/attribute" 11 "tangled.sh/tangled.sh/core/appview/pagination" 12 ) 13 ··· 106 return ownerDid, err 107 } 108 109 + func GetIssues(ctx context.Context, e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 110 + ctx, span := otel.Tracer("db").Start(ctx, "GetIssues") 111 + defer span.End() 112 + 113 + span.SetAttributes( 114 + attribute.String("repo_at", repoAt.String()), 115 + attribute.Bool("is_open", isOpen), 116 + attribute.Int("page.offset", page.Offset), 117 + attribute.Int("page.limit", page.Limit), 118 + ) 119 + 120 var issues []Issue 121 openValue := 0 122 if isOpen { 123 openValue = 1 124 } 125 126 + rows, err := e.QueryContext( 127 + ctx, 128 ` 129 with numbered_issue as ( 130 select ··· 153 body, 154 open, 155 comment_count 156 + from 157 numbered_issue 158 + where 159 row_num between ? and ?`, 160 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 161 if err != nil { 162 + span.RecordError(err) 163 return nil, err 164 } 165 defer rows.Close() ··· 170 var metadata IssueMetadata 171 err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 172 if err != nil { 173 + span.RecordError(err) 174 return nil, err 175 } 176 177 createdTime, err := time.Parse(time.RFC3339, createdAt) 178 if err != nil { 179 + span.RecordError(err) 180 return nil, err 181 } 182 issue.Created = createdTime ··· 186 } 187 188 if err := rows.Err(); err != nil { 189 + span.RecordError(err) 190 return nil, err 191 } 192 193 + span.SetAttributes(attribute.Int("issues.count", len(issues))) 194 return issues, nil 195 } 196 ··· 275 return issues, nil 276 } 277 278 + func GetIssue(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 279 + ctx, span := otel.Tracer("db").Start(ctx, "GetIssue") 280 + defer span.End() 281 + 282 query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 283 row := e.QueryRow(query, repoAt, issueId) 284 ··· 298 return &issue, nil 299 } 300 301 + func GetIssueWithComments(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 302 + ctx, span := otel.Tracer("db").Start(ctx, "GetIssueWithComments") 303 + defer span.End() 304 + 305 query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 306 row := e.QueryRow(query, repoAt, issueId) 307
+29 -3
appview/db/profile.go
··· 1 package db 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 type RepoEvent struct { ··· 83 84 const TimeframeMonths = 7 85 86 - func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { 87 timeline := ProfileTimeline{ 88 ByMonth: make([]ByMonth, TimeframeMonths), 89 } ··· 92 93 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 94 if err != nil { 95 return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 96 } 97 98 // group pulls by month 99 for _, pull := range pulls { 100 pullMonth := pull.Created.Month() ··· 112 113 issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 114 if err != nil { 115 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 116 } 117 118 for _, issue := range issues { 119 issueMonth := issue.Created.Month() 120 ··· 129 *items = append(*items, &issue) 130 } 131 132 - repos, err := GetAllReposByDid(e, forDid) 133 if err != nil { 134 return nil, fmt.Errorf("error getting all repos by did: %w", err) 135 } 136 137 for _, repo := range repos { 138 // TODO: get this in the original query; requires COALESCE because nullable 139 var sourceRepo *Repo 140 if repo.Source != "" { 141 - sourceRepo, err = GetRepoByAtUri(e, repo.Source) 142 if err != nil { 143 return nil, err 144 } 145 }
··· 1 package db 2 3 import ( 4 + "context" 5 "fmt" 6 "time" 7 + 8 + "go.opentelemetry.io/otel/attribute" 9 + "go.opentelemetry.io/otel/codes" 10 + "go.opentelemetry.io/otel/trace" 11 ) 12 13 type RepoEvent struct { ··· 88 89 const TimeframeMonths = 7 90 91 + func MakeProfileTimeline(ctx context.Context, e Execer, forDid string) (*ProfileTimeline, error) { 92 + span := trace.SpanFromContext(ctx) 93 + defer span.End() 94 + 95 + span.SetAttributes( 96 + attribute.String("forDid", forDid), 97 + ) 98 + 99 timeline := ProfileTimeline{ 100 ByMonth: make([]ByMonth, TimeframeMonths), 101 } ··· 104 105 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 106 if err != nil { 107 + span.RecordError(err) 108 + span.SetStatus(codes.Error, "error getting pulls by owner did") 109 return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 110 } 111 112 + span.SetAttributes(attribute.Int("pulls.count", len(pulls))) 113 + 114 // group pulls by month 115 for _, pull := range pulls { 116 pullMonth := pull.Created.Month() ··· 128 129 issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 130 if err != nil { 131 + span.RecordError(err) 132 + span.SetStatus(codes.Error, "error getting issues by owner did") 133 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 134 } 135 136 + span.SetAttributes(attribute.Int("issues.count", len(issues))) 137 + 138 for _, issue := range issues { 139 issueMonth := issue.Created.Month() 140 ··· 149 *items = append(*items, &issue) 150 } 151 152 + repos, err := GetAllReposByDid(ctx, e, forDid) 153 if err != nil { 154 + span.RecordError(err) 155 + span.SetStatus(codes.Error, "error getting all repos by did") 156 return nil, fmt.Errorf("error getting all repos by did: %w", err) 157 } 158 + 159 + span.SetAttributes(attribute.Int("repos.count", len(repos))) 160 161 for _, repo := range repos { 162 // TODO: get this in the original query; requires COALESCE because nullable 163 var sourceRepo *Repo 164 if repo.Source != "" { 165 + sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source) 166 if err != nil { 167 + span.RecordError(err) 168 + span.SetStatus(codes.Error, "error getting repo by at uri") 169 return nil, err 170 } 171 }
+111 -25
appview/db/pulls.go
··· 1 package db 2 3 import ( 4 "database/sql" 5 "fmt" 6 "log" ··· 10 11 "github.com/bluekeyes/go-gitdiff/gitdiff" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "tangled.sh/tangled.sh/core/api/tangled" 14 "tangled.sh/tangled.sh/core/patchutil" 15 "tangled.sh/tangled.sh/core/types" ··· 234 return patches 235 } 236 237 - func NewPull(tx *sql.Tx, pull *Pull) error { 238 defer tx.Rollback() 239 240 _, err := tx.Exec(` ··· 242 values (?, 1) 243 `, pull.RepoAt) 244 if err != nil { 245 return err 246 } 247 ··· 253 returning next_pull_id - 1 254 `, pull.RepoAt).Scan(&nextId) 255 if err != nil { 256 return err 257 } 258 259 pull.PullId = nextId 260 pull.State = PullOpen 261 262 var sourceBranch, sourceRepoAt *string 263 if pull.PullSource != nil { 264 sourceBranch = &pull.PullSource.Branch ··· 284 sourceRepoAt, 285 ) 286 if err != nil { 287 return err 288 } 289 290 _, err = tx.Exec(` 291 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 292 values (?, ?, ?, ?, ?) 293 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 294 if err != nil { 295 return err 296 } 297 298 if err := tx.Commit(); err != nil { 299 return err 300 } 301 302 return nil 303 } 304 305 - func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { 306 - pull, err := GetPull(e, repoAt, pullId) 307 if err != nil { 308 return "", err 309 } ··· 316 return pullId - 1, err 317 } 318 319 - func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) { 320 pulls := make(map[int]*Pull) 321 322 - rows, err := e.Query(` 323 select 324 owner_did, 325 pull_id, ··· 336 where 337 repo_at = ? and state = ?`, repoAt, state) 338 if err != nil { 339 return nil, err 340 } 341 defer rows.Close() ··· 357 &sourceRepoAt, 358 ) 359 if err != nil { 360 return nil, err 361 } 362 363 createdTime, err := time.Parse(time.RFC3339, createdAt) 364 if err != nil { 365 return nil, err 366 } 367 pull.Created = createdTime ··· 373 if sourceRepoAt.Valid { 374 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 375 if err != nil { 376 return nil, err 377 } 378 pull.PullSource.RepoAt = &sourceRepoAtParsed ··· 382 pulls[pull.PullId] = &pull 383 } 384 385 // get latest round no. for each pull 386 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 387 submissionsQuery := fmt.Sprintf(` ··· 400 args[idx] = p.PullId 401 idx += 1 402 } 403 - submissionsRows, err := e.Query(submissionsQuery, args...) 404 if err != nil { 405 return nil, err 406 } 407 defer submissionsRows.Close() ··· 414 &s.RoundNumber, 415 ) 416 if err != nil { 417 return nil, err 418 } 419 ··· 423 } 424 } 425 if err := rows.Err(); err != nil { 426 return nil, err 427 } 428 429 // get comment count on latest submission on each pull 430 inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") ··· 443 for _, p := range pulls { 444 args = append(args, p.Submissions[p.LastRoundNumber()].ID) 445 } 446 - commentsRows, err := e.Query(commentsQuery, args...) 447 if err != nil { 448 return nil, err 449 } 450 defer commentsRows.Close() ··· 456 &pullId, 457 ) 458 if err != nil { 459 return nil, err 460 } 461 if p, ok := pulls[pullId]; ok { ··· 463 } 464 } 465 if err := rows.Err(); err != nil { 466 return nil, err 467 } 468 469 orderedByDate := []*Pull{} 470 for _, p := range pulls { ··· 474 return orderedByDate[i].Created.After(orderedByDate[j].Created) 475 }) 476 477 return orderedByDate, nil 478 } 479 480 - func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 481 query := ` 482 select 483 owner_did, ··· 496 where 497 repo_at = ? and pull_id = ? 498 ` 499 - row := e.QueryRow(query, repoAt, pullId) 500 501 var pull Pull 502 var createdAt string ··· 515 &sourceRepoAt, 516 ) 517 if err != nil { 518 return nil, err 519 } 520 521 createdTime, err := time.Parse(time.RFC3339, createdAt) 522 if err != nil { 523 return nil, err 524 } 525 pull.Created = createdTime 526 527 - // populate source 528 if sourceBranch.Valid { 529 pull.PullSource = &PullSource{ 530 Branch: sourceBranch.String, ··· 532 if sourceRepoAt.Valid { 533 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 534 if err != nil { 535 return nil, err 536 } 537 pull.PullSource.RepoAt = &sourceRepoAtParsed 538 } 539 } 540 541 submissionsQuery := ` 542 select 543 id, pull_id, repo_at, round_number, patch, created, source_rev ··· 546 where 547 repo_at = ? and pull_id = ? 548 ` 549 - submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) 550 if err != nil { 551 return nil, err 552 } 553 defer submissionsRows.Close() ··· 568 &submissionSourceRev, 569 ) 570 if err != nil { 571 return nil, err 572 } 573 574 submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 575 if err != nil { 576 return nil, err 577 } 578 submission.Created = submissionCreatedTime ··· 584 submissionsMap[submission.ID] = &submission 585 } 586 if err = submissionsRows.Close(); err != nil { 587 return nil, err 588 } 589 if len(submissionsMap) == 0 { ··· 595 args = append(args, k) 596 } 597 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 598 commentsQuery := fmt.Sprintf(` 599 select 600 id, ··· 612 order by 613 created asc 614 `, inClause) 615 - commentsRows, err := e.Query(commentsQuery, args...) 616 if err != nil { 617 return nil, err 618 } 619 defer commentsRows.Close() ··· 632 &commentCreatedStr, 633 ) 634 if err != nil { 635 return nil, err 636 } 637 638 commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 639 if err != nil { 640 return nil, err 641 } 642 comment.Created = commentCreatedTime 643 644 - // Add the comment to its submission 645 if submission, ok := submissionsMap[comment.SubmissionId]; ok { 646 submission.Comments = append(submission.Comments, comment) 647 } 648 - 649 } 650 if err = commentsRows.Err(); err != nil { 651 return nil, err 652 } 653 654 - var pullSourceRepo *Repo 655 - if pull.PullSource != nil { 656 - if pull.PullSource.RepoAt != nil { 657 - pullSourceRepo, err = GetRepoByAtUri(e, pull.PullSource.RepoAt.String()) 658 - if err != nil { 659 - log.Printf("failed to get repo by at uri: %v", err) 660 - } else { 661 - pull.PullSource.Repo = pullSourceRepo 662 - } 663 } 664 } 665 ··· 747 return pulls, nil 748 } 749 750 - func NewPullComment(e Execer, comment *PullComment) (int64, error) { 751 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 752 - res, err := e.Exec( 753 query, 754 comment.OwnerDid, 755 comment.RepoAt, ··· 759 comment.Body, 760 ) 761 if err != nil { 762 return 0, err 763 } 764 765 i, err := res.LastInsertId() 766 if err != nil { 767 return 0, err 768 } 769 770 return i, nil 771 } 772
··· 1 package db 2 3 import ( 4 + "context" 5 "database/sql" 6 "fmt" 7 "log" ··· 11 12 "github.com/bluekeyes/go-gitdiff/gitdiff" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 + "go.opentelemetry.io/otel/attribute" 15 + "go.opentelemetry.io/otel/trace" 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/patchutil" 18 "tangled.sh/tangled.sh/core/types" ··· 237 return patches 238 } 239 240 + func NewPull(ctx context.Context, tx *sql.Tx, pull *Pull) error { 241 + span := trace.SpanFromContext(ctx) 242 + defer span.End() 243 + 244 + span.SetAttributes( 245 + attribute.String("repo.at", pull.RepoAt.String()), 246 + attribute.String("owner.did", pull.OwnerDid), 247 + attribute.String("title", pull.Title), 248 + attribute.String("target_branch", pull.TargetBranch), 249 + ) 250 + span.AddEvent("creating new pull request") 251 + 252 defer tx.Rollback() 253 254 _, err := tx.Exec(` ··· 256 values (?, 1) 257 `, pull.RepoAt) 258 if err != nil { 259 + span.RecordError(err) 260 return err 261 } 262 ··· 268 returning next_pull_id - 1 269 `, pull.RepoAt).Scan(&nextId) 270 if err != nil { 271 + span.RecordError(err) 272 return err 273 } 274 275 pull.PullId = nextId 276 pull.State = PullOpen 277 278 + span.SetAttributes(attribute.Int("pull.id", pull.PullId)) 279 + span.AddEvent("assigned pull ID") 280 + 281 var sourceBranch, sourceRepoAt *string 282 if pull.PullSource != nil { 283 sourceBranch = &pull.PullSource.Branch ··· 303 sourceRepoAt, 304 ) 305 if err != nil { 306 + span.RecordError(err) 307 return err 308 } 309 + 310 + span.AddEvent("inserted pull record") 311 312 _, err = tx.Exec(` 313 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 314 values (?, ?, ?, ?, ?) 315 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 316 if err != nil { 317 + span.RecordError(err) 318 return err 319 } 320 + 321 + span.AddEvent("inserted initial pull submission") 322 323 if err := tx.Commit(); err != nil { 324 + span.RecordError(err) 325 return err 326 } 327 328 + span.AddEvent("transaction committed successfully") 329 return nil 330 } 331 332 + func GetPullAt(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { 333 + pull, err := GetPull(ctx, e, repoAt, pullId) 334 if err != nil { 335 return "", err 336 } ··· 343 return pullId - 1, err 344 } 345 346 + func GetPulls(ctx context.Context, e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) { 347 + span := trace.SpanFromContext(ctx) 348 + defer span.End() 349 + 350 + span.SetAttributes( 351 + attribute.String("repoAt", repoAt.String()), 352 + attribute.String("state", state.String()), 353 + ) 354 + span.AddEvent("querying pulls") 355 + 356 pulls := make(map[int]*Pull) 357 358 + rows, err := e.QueryContext(ctx, ` 359 select 360 owner_did, 361 pull_id, ··· 372 where 373 repo_at = ? and state = ?`, repoAt, state) 374 if err != nil { 375 + span.RecordError(err) 376 return nil, err 377 } 378 defer rows.Close() ··· 394 &sourceRepoAt, 395 ) 396 if err != nil { 397 + span.RecordError(err) 398 return nil, err 399 } 400 401 createdTime, err := time.Parse(time.RFC3339, createdAt) 402 if err != nil { 403 + span.RecordError(err) 404 return nil, err 405 } 406 pull.Created = createdTime ··· 412 if sourceRepoAt.Valid { 413 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 414 if err != nil { 415 + span.RecordError(err) 416 return nil, err 417 } 418 pull.PullSource.RepoAt = &sourceRepoAtParsed ··· 422 pulls[pull.PullId] = &pull 423 } 424 425 + span.AddEvent("querying pull submissions") 426 + span.SetAttributes(attribute.Int("pull_count", len(pulls))) 427 + 428 // get latest round no. for each pull 429 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 430 submissionsQuery := fmt.Sprintf(` ··· 443 args[idx] = p.PullId 444 idx += 1 445 } 446 + submissionsRows, err := e.QueryContext(ctx, submissionsQuery, args...) 447 if err != nil { 448 + span.RecordError(err) 449 return nil, err 450 } 451 defer submissionsRows.Close() ··· 458 &s.RoundNumber, 459 ) 460 if err != nil { 461 + span.RecordError(err) 462 return nil, err 463 } 464 ··· 468 } 469 } 470 if err := rows.Err(); err != nil { 471 + span.RecordError(err) 472 return nil, err 473 } 474 + 475 + span.AddEvent("querying pull comments") 476 477 // get comment count on latest submission on each pull 478 inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") ··· 491 for _, p := range pulls { 492 args = append(args, p.Submissions[p.LastRoundNumber()].ID) 493 } 494 + commentsRows, err := e.QueryContext(ctx, commentsQuery, args...) 495 if err != nil { 496 + span.RecordError(err) 497 return nil, err 498 } 499 defer commentsRows.Close() ··· 505 &pullId, 506 ) 507 if err != nil { 508 + span.RecordError(err) 509 return nil, err 510 } 511 if p, ok := pulls[pullId]; ok { ··· 513 } 514 } 515 if err := rows.Err(); err != nil { 516 + span.RecordError(err) 517 return nil, err 518 } 519 + 520 + span.AddEvent("sorting pulls by date") 521 522 orderedByDate := []*Pull{} 523 for _, p := range pulls { ··· 527 return orderedByDate[i].Created.After(orderedByDate[j].Created) 528 }) 529 530 + span.SetAttributes(attribute.Int("result_count", len(orderedByDate))) 531 return orderedByDate, nil 532 } 533 534 + func GetPull(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 535 + span := trace.SpanFromContext(ctx) 536 + defer span.End() 537 + 538 + span.SetAttributes(attribute.String("repoAt", repoAt.String()), attribute.Int("pull.id", pullId)) 539 + span.AddEvent("query pull metadata") 540 + 541 query := ` 542 select 543 owner_did, ··· 556 where 557 repo_at = ? and pull_id = ? 558 ` 559 + row := e.QueryRowContext(ctx, query, repoAt, pullId) 560 561 var pull Pull 562 var createdAt string ··· 575 &sourceRepoAt, 576 ) 577 if err != nil { 578 + span.RecordError(err) 579 return nil, err 580 } 581 582 createdTime, err := time.Parse(time.RFC3339, createdAt) 583 if err != nil { 584 + span.RecordError(err) 585 return nil, err 586 } 587 pull.Created = createdTime 588 589 if sourceBranch.Valid { 590 pull.PullSource = &PullSource{ 591 Branch: sourceBranch.String, ··· 593 if sourceRepoAt.Valid { 594 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 595 if err != nil { 596 + span.RecordError(err) 597 return nil, err 598 } 599 pull.PullSource.RepoAt = &sourceRepoAtParsed 600 } 601 } 602 603 + span.AddEvent("query submissions") 604 submissionsQuery := ` 605 select 606 id, pull_id, repo_at, round_number, patch, created, source_rev ··· 609 where 610 repo_at = ? and pull_id = ? 611 ` 612 + submissionsRows, err := e.QueryContext(ctx, submissionsQuery, repoAt, pullId) 613 if err != nil { 614 + span.RecordError(err) 615 return nil, err 616 } 617 defer submissionsRows.Close() ··· 632 &submissionSourceRev, 633 ) 634 if err != nil { 635 + span.RecordError(err) 636 return nil, err 637 } 638 639 submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 640 if err != nil { 641 + span.RecordError(err) 642 return nil, err 643 } 644 submission.Created = submissionCreatedTime ··· 650 submissionsMap[submission.ID] = &submission 651 } 652 if err = submissionsRows.Close(); err != nil { 653 + span.RecordError(err) 654 return nil, err 655 } 656 if len(submissionsMap) == 0 { ··· 662 args = append(args, k) 663 } 664 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 665 + 666 + span.AddEvent("query comments") 667 commentsQuery := fmt.Sprintf(` 668 select 669 id, ··· 681 order by 682 created asc 683 `, inClause) 684 + commentsRows, err := e.QueryContext(ctx, commentsQuery, args...) 685 if err != nil { 686 + span.RecordError(err) 687 return nil, err 688 } 689 defer commentsRows.Close() ··· 702 &commentCreatedStr, 703 ) 704 if err != nil { 705 + span.RecordError(err) 706 return nil, err 707 } 708 709 commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 710 if err != nil { 711 + span.RecordError(err) 712 return nil, err 713 } 714 comment.Created = commentCreatedTime 715 716 if submission, ok := submissionsMap[comment.SubmissionId]; ok { 717 submission.Comments = append(submission.Comments, comment) 718 } 719 } 720 if err = commentsRows.Err(); err != nil { 721 + span.RecordError(err) 722 return nil, err 723 } 724 725 + if pull.PullSource != nil && pull.PullSource.RepoAt != nil { 726 + span.AddEvent("query pull source repo") 727 + pullSourceRepo, err := GetRepoByAtUri(ctx, e, pull.PullSource.RepoAt.String()) 728 + if err != nil { 729 + span.RecordError(err) 730 + log.Printf("failed to get repo by at uri: %v", err) 731 + } else { 732 + pull.PullSource.Repo = pullSourceRepo 733 } 734 } 735 ··· 817 return pulls, nil 818 } 819 820 + func NewPullComment(ctx context.Context, e Execer, comment *PullComment) (int64, error) { 821 + span := trace.SpanFromContext(ctx) 822 + defer span.End() 823 + 824 + span.SetAttributes( 825 + attribute.String("repo.at", comment.RepoAt), 826 + attribute.Int("pull.id", comment.PullId), 827 + attribute.Int("submission.id", comment.SubmissionId), 828 + attribute.String("owner.did", comment.OwnerDid), 829 + ) 830 + span.AddEvent("inserting new pull comment") 831 + 832 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 833 + res, err := e.ExecContext( 834 + ctx, 835 query, 836 comment.OwnerDid, 837 comment.RepoAt, ··· 841 comment.Body, 842 ) 843 if err != nil { 844 + span.RecordError(err) 845 return 0, err 846 } 847 848 i, err := res.LastInsertId() 849 if err != nil { 850 + span.RecordError(err) 851 return 0, err 852 } 853 854 + span.SetAttributes(attribute.Int64("comment.id", i)) 855 + span.AddEvent("pull comment created successfully") 856 return i, nil 857 } 858
+114 -12
appview/db/repos.go
··· 1 package db 2 3 import ( 4 "database/sql" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 ) 9 10 type Repo struct { ··· 23 Source string 24 } 25 26 - func GetAllRepos(e Execer, limit int) ([]Repo, error) { 27 var repos []Repo 28 29 rows, err := e.Query( ··· 35 limit, 36 ) 37 if err != nil { 38 return nil, err 39 } 40 defer rows.Close() ··· 45 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 46 ) 47 if err != nil { 48 return nil, err 49 } 50 repos = append(repos, repo) 51 } 52 53 if err := rows.Err(); err != nil { 54 return nil, err 55 } 56 57 return repos, nil 58 } 59 60 - func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 61 var repos []Repo 62 63 rows, err := e.Query( ··· 81 order by r.created desc`, 82 did) 83 if err != nil { 84 return nil, err 85 } 86 defer rows.Close() ··· 94 95 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource) 96 if err != nil { 97 return nil, err 98 } 99 ··· 118 } 119 120 if err := rows.Err(); err != nil { 121 return nil, err 122 } 123 124 return repos, nil 125 } 126 127 - func GetRepo(e Execer, did, name string) (*Repo, error) { 128 var repo Repo 129 var nullableDescription sql.NullString 130 ··· 132 133 var createdAt string 134 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 135 return nil, err 136 } 137 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 146 return &repo, nil 147 } 148 149 - func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 150 var repo Repo 151 var nullableDescription sql.NullString 152 ··· 154 155 var createdAt string 156 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 157 return nil, err 158 } 159 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 168 return &repo, nil 169 } 170 171 - func AddRepo(e Execer, repo *Repo) error { 172 _, err := e.Exec( 173 `insert into repos 174 (did, name, knot, rkey, at_uri, description, source) 175 values (?, ?, ?, ?, ?, ?, ?)`, 176 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 177 ) 178 return err 179 } 180 181 - func RemoveRepo(e Execer, did, name string) error { 182 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 183 return err 184 } 185 186 - func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) { 187 var nullableSource sql.NullString 188 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 189 if err != nil { 190 return "", err 191 } 192 return nullableSource.String, nil 193 } 194 195 - func GetForksByDid(e Execer, did string) ([]Repo, error) { 196 var repos []Repo 197 198 rows, err := e.Query( ··· 203 did, 204 ) 205 if err != nil { 206 return nil, err 207 } 208 defer rows.Close() ··· 215 216 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 217 if err != nil { 218 return nil, err 219 } 220 ··· 237 } 238 239 if err := rows.Err(); err != nil { 240 return nil, err 241 } 242 243 return repos, nil 244 } 245 246 - func GetForkByDid(e Execer, did string, name string) (*Repo, error) { 247 var repo Repo 248 var createdAt string 249 var nullableDescription sql.NullString ··· 258 259 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 260 if err != nil { 261 return nil, err 262 } 263 ··· 279 return &repo, nil 280 } 281 282 - func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { 283 _, err := e.Exec( 284 `insert into collaborators (did, repo) 285 values (?, (select id from repos where did = ? and name = ? and knot = ?));`, 286 collaborator, repoOwnerDid, repoName, repoKnot) 287 return err 288 } 289 290 - func UpdateDescription(e Execer, repoAt, newDescription string) error { 291 _, err := e.Exec( 292 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 293 return err 294 } 295 296 - func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { 297 var repos []Repo 298 299 rows, err := e.Query( ··· 310 group by 311 r.id;`, collaborator) 312 if err != nil { 313 return nil, err 314 } 315 defer rows.Close() ··· 322 323 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount) 324 if err != nil { 325 return nil, err 326 } 327 ··· 344 } 345 346 if err := rows.Err(); err != nil { 347 return nil, err 348 } 349 350 return repos, nil 351 } 352
··· 1 package db 2 3 import ( 4 + "context" 5 "database/sql" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 + "go.opentelemetry.io/otel" 10 + "go.opentelemetry.io/otel/attribute" 11 ) 12 13 type Repo struct { ··· 26 Source string 27 } 28 29 + func GetAllRepos(ctx context.Context, e Execer, limit int) ([]Repo, error) { 30 + ctx, span := otel.Tracer("db").Start(ctx, "GetAllRepos") 31 + defer span.End() 32 + span.SetAttributes(attribute.Int("limit", limit)) 33 + 34 var repos []Repo 35 36 rows, err := e.Query( ··· 42 limit, 43 ) 44 if err != nil { 45 + span.RecordError(err) 46 return nil, err 47 } 48 defer rows.Close() ··· 53 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 54 ) 55 if err != nil { 56 + span.RecordError(err) 57 return nil, err 58 } 59 repos = append(repos, repo) 60 } 61 62 if err := rows.Err(); err != nil { 63 + span.RecordError(err) 64 return nil, err 65 } 66 67 + span.SetAttributes(attribute.Int("repos.count", len(repos))) 68 return repos, nil 69 } 70 71 + func GetAllReposByDid(ctx context.Context, e Execer, did string) ([]Repo, error) { 72 + ctx, span := otel.Tracer("db").Start(ctx, "GetAllReposByDid") 73 + defer span.End() 74 + span.SetAttributes(attribute.String("did", did)) 75 + 76 var repos []Repo 77 78 rows, err := e.Query( ··· 96 order by r.created desc`, 97 did) 98 if err != nil { 99 + span.RecordError(err) 100 return nil, err 101 } 102 defer rows.Close() ··· 110 111 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource) 112 if err != nil { 113 + span.RecordError(err) 114 return nil, err 115 } 116 ··· 135 } 136 137 if err := rows.Err(); err != nil { 138 + span.RecordError(err) 139 return nil, err 140 } 141 142 + span.SetAttributes(attribute.Int("repos.count", len(repos))) 143 return repos, nil 144 } 145 146 + func GetRepo(ctx context.Context, e Execer, did, name string) (*Repo, error) { 147 + ctx, span := otel.Tracer("db").Start(ctx, "GetRepo") 148 + defer span.End() 149 + span.SetAttributes( 150 + attribute.String("did", did), 151 + attribute.String("name", name), 152 + ) 153 + 154 var repo Repo 155 var nullableDescription sql.NullString 156 ··· 158 159 var createdAt string 160 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 161 + span.RecordError(err) 162 return nil, err 163 } 164 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 173 return &repo, nil 174 } 175 176 + func GetRepoByAtUri(ctx context.Context, e Execer, atUri string) (*Repo, error) { 177 + ctx, span := otel.Tracer("db").Start(ctx, "GetRepoByAtUri") 178 + defer span.End() 179 + span.SetAttributes(attribute.String("atUri", atUri)) 180 + 181 var repo Repo 182 var nullableDescription sql.NullString 183 ··· 185 186 var createdAt string 187 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 188 + span.RecordError(err) 189 return nil, err 190 } 191 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 200 return &repo, nil 201 } 202 203 + func AddRepo(ctx context.Context, e Execer, repo *Repo) error { 204 + ctx, span := otel.Tracer("db").Start(ctx, "AddRepo") 205 + defer span.End() 206 + span.SetAttributes( 207 + attribute.String("did", repo.Did), 208 + attribute.String("name", repo.Name), 209 + ) 210 + 211 _, err := e.Exec( 212 `insert into repos 213 (did, name, knot, rkey, at_uri, description, source) 214 values (?, ?, ?, ?, ?, ?, ?)`, 215 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 216 ) 217 + if err != nil { 218 + span.RecordError(err) 219 + } 220 return err 221 } 222 223 + func RemoveRepo(ctx context.Context, e Execer, did, name string) error { 224 + ctx, span := otel.Tracer("db").Start(ctx, "RemoveRepo") 225 + defer span.End() 226 + span.SetAttributes( 227 + attribute.String("did", did), 228 + attribute.String("name", name), 229 + ) 230 + 231 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 232 + if err != nil { 233 + span.RecordError(err) 234 + } 235 return err 236 } 237 238 + func GetRepoSource(ctx context.Context, e Execer, repoAt syntax.ATURI) (string, error) { 239 + ctx, span := otel.Tracer("db").Start(ctx, "GetRepoSource") 240 + defer span.End() 241 + span.SetAttributes(attribute.String("repoAt", repoAt.String())) 242 + 243 var nullableSource sql.NullString 244 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource) 245 if err != nil { 246 + span.RecordError(err) 247 return "", err 248 } 249 return nullableSource.String, nil 250 } 251 252 + func GetForksByDid(ctx context.Context, e Execer, did string) ([]Repo, error) { 253 + ctx, span := otel.Tracer("db").Start(ctx, "GetForksByDid") 254 + defer span.End() 255 + span.SetAttributes(attribute.String("did", did)) 256 + 257 var repos []Repo 258 259 rows, err := e.Query( ··· 264 did, 265 ) 266 if err != nil { 267 + span.RecordError(err) 268 return nil, err 269 } 270 defer rows.Close() ··· 277 278 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 279 if err != nil { 280 + span.RecordError(err) 281 return nil, err 282 } 283 ··· 300 } 301 302 if err := rows.Err(); err != nil { 303 + span.RecordError(err) 304 return nil, err 305 } 306 307 + span.SetAttributes(attribute.Int("forks.count", len(repos))) 308 return repos, nil 309 } 310 311 + func GetForkByDid(ctx context.Context, e Execer, did string, name string) (*Repo, error) { 312 + ctx, span := otel.Tracer("db").Start(ctx, "GetForkByDid") 313 + defer span.End() 314 + span.SetAttributes( 315 + attribute.String("did", did), 316 + attribute.String("name", name), 317 + ) 318 + 319 var repo Repo 320 var createdAt string 321 var nullableDescription sql.NullString ··· 330 331 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 332 if err != nil { 333 + span.RecordError(err) 334 return nil, err 335 } 336 ··· 352 return &repo, nil 353 } 354 355 + func AddCollaborator(ctx context.Context, e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { 356 + ctx, span := otel.Tracer("db").Start(ctx, "AddCollaborator") 357 + defer span.End() 358 + span.SetAttributes( 359 + attribute.String("collaborator", collaborator), 360 + attribute.String("repoOwnerDid", repoOwnerDid), 361 + attribute.String("repoName", repoName), 362 + ) 363 + 364 _, err := e.Exec( 365 `insert into collaborators (did, repo) 366 values (?, (select id from repos where did = ? and name = ? and knot = ?));`, 367 collaborator, repoOwnerDid, repoName, repoKnot) 368 + if err != nil { 369 + span.RecordError(err) 370 + } 371 return err 372 } 373 374 + func UpdateDescription(ctx context.Context, e Execer, repoAt, newDescription string) error { 375 + ctx, span := otel.Tracer("db").Start(ctx, "UpdateDescription") 376 + defer span.End() 377 + span.SetAttributes( 378 + attribute.String("repoAt", repoAt), 379 + attribute.String("description", newDescription), 380 + ) 381 + 382 _, err := e.Exec( 383 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) 384 + if err != nil { 385 + span.RecordError(err) 386 + } 387 return err 388 } 389 390 + func CollaboratingIn(ctx context.Context, e Execer, collaborator string) ([]Repo, error) { 391 + ctx, span := otel.Tracer("db").Start(ctx, "CollaboratingIn") 392 + defer span.End() 393 + span.SetAttributes(attribute.String("collaborator", collaborator)) 394 + 395 var repos []Repo 396 397 rows, err := e.Query( ··· 408 group by 409 r.id;`, collaborator) 410 if err != nil { 411 + span.RecordError(err) 412 return nil, err 413 } 414 defer rows.Close() ··· 421 422 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount) 423 if err != nil { 424 + span.RecordError(err) 425 return nil, err 426 } 427 ··· 444 } 445 446 if err := rows.Err(); err != nil { 447 + span.RecordError(err) 448 return nil, err 449 } 450 451 + span.SetAttributes(attribute.Int("repos.count", len(repos))) 452 return repos, nil 453 } 454
+5 -4
appview/db/star.go
··· 1 package db 2 3 import ( 4 "log" 5 "time" 6 ··· 17 Repo *Repo 18 } 19 20 - func (star *Star) ResolveRepo(e Execer) error { 21 if star.Repo != nil { 22 return nil 23 } 24 25 - repo, err := GetRepoByAtUri(e, star.RepoAt.String()) 26 if err != nil { 27 return err 28 } ··· 40 // Get a star record 41 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 42 query := ` 43 - select starred_by_did, repo_at, created, rkey 44 from stars 45 where starred_by_did = ? and repo_at = ?` 46 row := e.QueryRow(query, starredByDid, repoAt) ··· 97 var stars []Star 98 99 rows, err := e.Query(` 100 - select 101 s.starred_by_did, 102 s.repo_at, 103 s.rkey,
··· 1 package db 2 3 import ( 4 + "context" 5 "log" 6 "time" 7 ··· 18 Repo *Repo 19 } 20 21 + func (star *Star) ResolveRepo(ctx context.Context, e Execer) error { 22 if star.Repo != nil { 23 return nil 24 } 25 26 + repo, err := GetRepoByAtUri(ctx, e, star.RepoAt.String()) 27 if err != nil { 28 return err 29 } ··· 41 // Get a star record 42 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 43 query := ` 44 + select starred_by_did, repo_at, created, rkey 45 from stars 46 where starred_by_did = ? and repo_at = ?` 47 row := e.QueryRow(query, starredByDid, repoAt) ··· 98 var stars []Star 99 100 rows, err := e.Query(` 101 + select 102 s.starred_by_did, 103 s.repo_at, 104 s.rkey,
+28 -3
appview/db/timeline.go
··· 1 package db 2 3 import ( 4 "sort" 5 "time" 6 ) 7 8 type TimelineEvent struct { ··· 18 19 // TODO: this gathers heterogenous events from different sources and aggregates 20 // them in code; if we did this entirely in sql, we could order and limit and paginate easily 21 - func MakeTimeline(e Execer) ([]TimelineEvent, error) { 22 var events []TimelineEvent 23 limit := 50 24 25 - repos, err := GetAllRepos(e, limit) 26 if err != nil { 27 return nil, err 28 } 29 30 follows, err := GetAllFollows(e, limit) 31 if err != nil { 32 return nil, err 33 } 34 35 stars, err := GetAllStars(e, limit) 36 if err != nil { 37 return nil, err 38 } 39 40 for _, repo := range repos { 41 var sourceRepo *Repo 42 if repo.Source != "" { 43 - sourceRepo, err = GetRepoByAtUri(e, repo.Source) 44 if err != nil { 45 return nil, err 46 } 47 } ··· 75 if len(events) > limit { 76 events = events[:limit] 77 } 78 79 return events, nil 80 }
··· 1 package db 2 3 import ( 4 + "context" 5 "sort" 6 "time" 7 + 8 + "go.opentelemetry.io/otel/attribute" 9 + "go.opentelemetry.io/otel/trace" 10 ) 11 12 type TimelineEvent struct { ··· 22 23 // TODO: this gathers heterogenous events from different sources and aggregates 24 // them in code; if we did this entirely in sql, we could order and limit and paginate easily 25 + func MakeTimeline(ctx context.Context, e Execer) ([]TimelineEvent, error) { 26 + span := trace.SpanFromContext(ctx) 27 + defer span.End() 28 + 29 var events []TimelineEvent 30 limit := 50 31 32 + span.SetAttributes(attribute.Int("timeline.limit", limit)) 33 + 34 + repos, err := GetAllRepos(ctx, e, limit) 35 if err != nil { 36 + span.RecordError(err) 37 + span.SetAttributes(attribute.String("error.from", "GetAllRepos")) 38 return nil, err 39 } 40 + span.SetAttributes(attribute.Int("timeline.repos.count", len(repos))) 41 42 follows, err := GetAllFollows(e, limit) 43 if err != nil { 44 + span.RecordError(err) 45 + span.SetAttributes(attribute.String("error.from", "GetAllFollows")) 46 return nil, err 47 } 48 + span.SetAttributes(attribute.Int("timeline.follows.count", len(follows))) 49 50 stars, err := GetAllStars(e, limit) 51 if err != nil { 52 + span.RecordError(err) 53 + span.SetAttributes(attribute.String("error.from", "GetAllStars")) 54 return nil, err 55 } 56 + span.SetAttributes(attribute.Int("timeline.stars.count", len(stars))) 57 58 for _, repo := range repos { 59 var sourceRepo *Repo 60 if repo.Source != "" { 61 + sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source) 62 if err != nil { 63 + span.RecordError(err) 64 + span.SetAttributes( 65 + attribute.String("error.from", "GetRepoByAtUri"), 66 + attribute.String("repo.source", repo.Source), 67 + ) 68 return nil, err 69 } 70 } ··· 98 if len(events) > limit { 99 events = events[:limit] 100 } 101 + 102 + span.SetAttributes(attribute.Int("timeline.events.total", len(events))) 103 104 return events, nil 105 }
+1 -1
appview/state/artifact.go
··· 118 119 s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 120 LoggedInUser: user, 121 - RepoInfo: f.RepoInfo(s, user), 122 Artifact: artifact, 123 }) 124 }
··· 118 119 s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 120 LoggedInUser: user, 121 + RepoInfo: f.RepoInfo(r.Context(), s, user), 122 Artifact: artifact, 123 }) 124 }
+31 -13
appview/state/middleware.go
··· 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/go-chi/chi/v5" 15 "tangled.sh/tangled.sh/core/appview/db" 16 "tangled.sh/tangled.sh/core/appview/middleware" 17 ) ··· 19 func knotRoleMiddleware(s *State, group string) middleware.Middleware { 20 return func(next http.Handler) http.Handler { 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 // requires auth also 23 - actor := s.auth.GetUser(r) 24 if actor == nil { 25 // we need a logged in user 26 log.Printf("not logged in, redirecting") ··· 41 return 42 } 43 44 - next.ServeHTTP(w, r) 45 }) 46 } 47 } ··· 53 func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware { 54 return func(next http.Handler) http.Handler { 55 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 // requires auth also 57 - actor := s.auth.GetUser(r) 58 if actor == nil { 59 // we need a logged in user 60 log.Printf("not logged in, redirecting") 61 http.Error(w, "Forbiden", http.StatusUnauthorized) 62 return 63 } 64 - f, err := s.fullyResolvedRepo(r) 65 if err != nil { 66 http.Error(w, "malformed url", http.StatusBadRequest) 67 return ··· 75 return 76 } 77 78 - next.ServeHTTP(w, r) 79 }) 80 } 81 } ··· 101 return 102 } 103 104 - id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle) 105 if err != nil { 106 // invalid did or handle 107 log.Println("failed to resolve did/handle:", err) ··· 109 return 110 } 111 112 - ctx := context.WithValue(req.Context(), "resolvedId", *id) 113 114 next.ServeHTTP(w, req.WithContext(ctx)) 115 }) ··· 119 func ResolveRepo(s *State) middleware.Middleware { 120 return func(next http.Handler) http.Handler { 121 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 122 repoName := chi.URLParam(req, "repo") 123 - id, ok := req.Context().Value("resolvedId").(identity.Identity) 124 if !ok { 125 log.Println("malformed middleware") 126 w.WriteHeader(http.StatusInternalServerError) 127 return 128 } 129 130 - repo, err := db.GetRepo(s.db, id.DID.String(), repoName) 131 if err != nil { 132 // invalid did or handle 133 log.Println("failed to resolve repo") ··· 135 return 136 } 137 138 - ctx := context.WithValue(req.Context(), "knot", repo.Knot) 139 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 140 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 141 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) ··· 148 func ResolvePull(s *State) middleware.Middleware { 149 return func(next http.Handler) http.Handler { 150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 - f, err := s.fullyResolvedRepo(r) 152 if err != nil { 153 log.Println("failed to fully resolve repo", err) 154 http.Error(w, "invalid repo url", http.StatusNotFound) ··· 163 return 164 } 165 166 - pr, err := db.GetPull(s.db, f.RepoAt, prIdInt) 167 if err != nil { 168 log.Println("failed to get pull and comments", err) 169 return 170 } 171 172 - ctx := context.WithValue(r.Context(), "pull", pr) 173 174 next.ServeHTTP(w, r.WithContext(ctx)) 175 })
··· 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/go-chi/chi/v5" 15 + "go.opentelemetry.io/otel/attribute" 16 "tangled.sh/tangled.sh/core/appview/db" 17 "tangled.sh/tangled.sh/core/appview/middleware" 18 ) ··· 20 func knotRoleMiddleware(s *State, group string) middleware.Middleware { 21 return func(next http.Handler) http.Handler { 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 + ctx, span := s.t.TraceStart(r.Context(), "knotRoleMiddleware") 24 + defer span.End() 25 + 26 // requires auth also 27 + actor := s.auth.GetUser(r.WithContext(ctx)) 28 if actor == nil { 29 // we need a logged in user 30 log.Printf("not logged in, redirecting") ··· 45 return 46 } 47 48 + next.ServeHTTP(w, r.WithContext(ctx)) 49 }) 50 } 51 } ··· 57 func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware { 58 return func(next http.Handler) http.Handler { 59 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 + ctx, span := s.t.TraceStart(r.Context(), "RepoPermissionMiddleware") 61 + defer span.End() 62 + 63 // requires auth also 64 + actor := s.auth.GetUser(r.WithContext(ctx)) 65 if actor == nil { 66 // we need a logged in user 67 log.Printf("not logged in, redirecting") 68 http.Error(w, "Forbiden", http.StatusUnauthorized) 69 return 70 } 71 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 72 if err != nil { 73 http.Error(w, "malformed url", http.StatusBadRequest) 74 return ··· 82 return 83 } 84 85 + next.ServeHTTP(w, r.WithContext(ctx)) 86 }) 87 } 88 } ··· 108 return 109 } 110 111 + ctx, span := s.t.TraceStart(req.Context(), "ResolveIdent") 112 + defer span.End() 113 + 114 + id, err := s.resolver.ResolveIdent(ctx, didOrHandle) 115 if err != nil { 116 // invalid did or handle 117 log.Println("failed to resolve did/handle:", err) ··· 119 return 120 } 121 122 + ctx = context.WithValue(ctx, "resolvedId", *id) 123 124 next.ServeHTTP(w, req.WithContext(ctx)) 125 }) ··· 129 func ResolveRepo(s *State) middleware.Middleware { 130 return func(next http.Handler) http.Handler { 131 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 132 + ctx, span := s.t.TraceStart(req.Context(), "ResolveRepo") 133 + defer span.End() 134 + 135 repoName := chi.URLParam(req, "repo") 136 + id, ok := ctx.Value("resolvedId").(identity.Identity) 137 if !ok { 138 log.Println("malformed middleware") 139 w.WriteHeader(http.StatusInternalServerError) 140 return 141 } 142 143 + repo, err := db.GetRepo(ctx, s.db, id.DID.String(), repoName) 144 if err != nil { 145 // invalid did or handle 146 log.Println("failed to resolve repo") ··· 148 return 149 } 150 151 + ctx = context.WithValue(ctx, "knot", repo.Knot) 152 ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 153 ctx = context.WithValue(ctx, "repoDescription", repo.Description) 154 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) ··· 161 func ResolvePull(s *State) middleware.Middleware { 162 return func(next http.Handler) http.Handler { 163 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 164 + ctx, span := s.t.TraceStart(r.Context(), "ResolvePull") 165 + defer span.End() 166 + 167 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 168 if err != nil { 169 log.Println("failed to fully resolve repo", err) 170 http.Error(w, "invalid repo url", http.StatusNotFound) ··· 179 return 180 } 181 182 + pr, err := db.GetPull(ctx, s.db, f.RepoAt, prIdInt) 183 if err != nil { 184 log.Println("failed to get pull and comments", err) 185 return 186 } 187 188 + span.SetAttributes(attribute.Int("pull.id", prIdInt)) 189 + 190 + ctx = context.WithValue(ctx, "pull", pr) 191 192 next.ServeHTTP(w, r.WithContext(ctx)) 193 })
+33 -5
appview/state/profile.go
··· 10 11 "github.com/bluesky-social/indigo/atproto/identity" 12 "github.com/go-chi/chi/v5" 13 "tangled.sh/tangled.sh/core/appview/db" 14 "tangled.sh/tangled.sh/core/appview/pages" 15 ) 16 17 func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 18 didOrHandle := chi.URLParam(r, "user") 19 if didOrHandle == "" { 20 http.Error(w, "Bad request", http.StatusBadRequest) 21 return 22 } 23 24 - ident, ok := r.Context().Value("resolvedId").(identity.Identity) 25 if !ok { 26 s.pages.Error404(w) 27 return 28 } 29 30 - repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 31 if err != nil { 32 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 33 } 34 35 - collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 36 if err != nil { 37 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 38 } 39 40 - timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 41 if err != nil { 42 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 43 } 44 45 var didsToResolve []string ··· 60 } 61 } 62 } 63 64 - resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 65 didHandleMap := make(map[string]string) 66 for _, identity := range resolvedIds { 67 if !identity.Handle.IsInvalidHandle() { ··· 70 didHandleMap[identity.DID.String()] = identity.DID.String() 71 } 72 } 73 74 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 75 if err != nil { 76 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 77 } 78 79 loggedInUser := s.auth.GetUser(r) 80 followStatus := db.IsNotFollowing 81 if loggedInUser != nil { 82 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 83 } 84 85 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 86 s.pages.ProfilePage(w, pages.ProfilePageParams{
··· 10 11 "github.com/bluesky-social/indigo/atproto/identity" 12 "github.com/go-chi/chi/v5" 13 + "go.opentelemetry.io/otel/attribute" 14 "tangled.sh/tangled.sh/core/appview/db" 15 "tangled.sh/tangled.sh/core/appview/pages" 16 ) 17 18 func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 19 + ctx, span := s.t.TraceStart(r.Context(), "ProfilePage") 20 + defer span.End() 21 + 22 didOrHandle := chi.URLParam(r, "user") 23 if didOrHandle == "" { 24 http.Error(w, "Bad request", http.StatusBadRequest) 25 return 26 } 27 28 + ident, ok := ctx.Value("resolvedId").(identity.Identity) 29 if !ok { 30 s.pages.Error404(w) 31 + span.RecordError(fmt.Errorf("failed to resolve identity")) 32 return 33 } 34 35 + span.SetAttributes( 36 + attribute.String("user.did", ident.DID.String()), 37 + attribute.String("user.handle", ident.Handle.String()), 38 + ) 39 + 40 + repos, err := db.GetAllReposByDid(ctx, s.db, ident.DID.String()) 41 if err != nil { 42 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 43 + span.RecordError(err) 44 + span.SetAttributes(attribute.String("error.repos", err.Error())) 45 } 46 + span.SetAttributes(attribute.Int("repos.count", len(repos))) 47 48 + collaboratingRepos, err := db.CollaboratingIn(ctx, s.db, ident.DID.String()) 49 if err != nil { 50 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 51 + span.RecordError(err) 52 + span.SetAttributes(attribute.String("error.collaborating_repos", err.Error())) 53 } 54 + span.SetAttributes(attribute.Int("collaborating_repos.count", len(collaboratingRepos))) 55 56 + timeline, err := db.MakeProfileTimeline(ctx, s.db, ident.DID.String()) 57 if err != nil { 58 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 59 + span.RecordError(err) 60 + span.SetAttributes(attribute.String("error.timeline", err.Error())) 61 } 62 63 var didsToResolve []string ··· 78 } 79 } 80 } 81 + span.SetAttributes(attribute.Int("dids_to_resolve.count", len(didsToResolve))) 82 83 + resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve) 84 didHandleMap := make(map[string]string) 85 for _, identity := range resolvedIds { 86 if !identity.Handle.IsInvalidHandle() { ··· 89 didHandleMap[identity.DID.String()] = identity.DID.String() 90 } 91 } 92 + span.SetAttributes(attribute.Int("resolved_ids.count", len(resolvedIds))) 93 94 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 95 if err != nil { 96 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 97 + span.RecordError(err) 98 + span.SetAttributes(attribute.String("error.follow_stats", err.Error())) 99 } 100 + span.SetAttributes( 101 + attribute.Int("followers.count", followers), 102 + attribute.Int("following.count", following), 103 + ) 104 105 loggedInUser := s.auth.GetUser(r) 106 followStatus := db.IsNotFollowing 107 if loggedInUser != nil { 108 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 109 + span.SetAttributes(attribute.String("logged_in_user.did", loggedInUser.Did)) 110 } 111 + span.SetAttributes(attribute.String("follow_status", string(db.FollowStatus(followStatus)))) 112 113 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 114 s.pages.ProfilePage(w, pages.ProfilePageParams{
+633 -125
appview/state/pull.go
··· 1 package state 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "errors" ··· 11 "strconv" 12 "time" 13 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview" 16 "tangled.sh/tangled.sh/core/appview/auth" 17 "tangled.sh/tangled.sh/core/appview/db" 18 "tangled.sh/tangled.sh/core/appview/pages" 19 "tangled.sh/tangled.sh/core/patchutil" 20 "tangled.sh/tangled.sh/core/types" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 27 28 // htmx fragment 29 func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 30 switch r.Method { 31 case http.MethodGet: 32 user := s.auth.GetUser(r) 33 - f, err := s.fullyResolvedRepo(r) 34 if err != nil { 35 log.Println("failed to get repo and knot", err) 36 return 37 } 38 39 - pull, ok := r.Context().Value("pull").(*db.Pull) 40 if !ok { 41 log.Println("failed to get pull") 42 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") ··· 54 return 55 } 56 57 - mergeCheckResponse := s.mergeCheck(f, pull) 58 resubmitResult := pages.Unknown 59 if user.Did == pull.OwnerDid { 60 - resubmitResult = s.resubmitCheck(f, pull) 61 } 62 63 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 64 LoggedInUser: user, 65 - RepoInfo: f.RepoInfo(s, user), 66 Pull: pull, 67 RoundNumber: roundNumber, 68 MergeCheck: mergeCheckResponse, 69 ResubmitCheck: resubmitResult, 70 }) 71 return 72 } 73 } 74 75 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 76 user := s.auth.GetUser(r) 77 - f, err := s.fullyResolvedRepo(r) 78 if err != nil { 79 log.Println("failed to get repo and knot", err) 80 return 81 } 82 83 - pull, ok := r.Context().Value("pull").(*db.Pull) 84 if !ok { 85 - log.Println("failed to get pull") 86 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 87 return 88 } 89 90 totalIdents := 1 91 for _, submission := range pull.Submissions { 92 totalIdents += len(submission.Comments) ··· 104 } 105 } 106 107 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 108 didHandleMap := make(map[string]string) 109 for _, identity := range resolvedIds { 110 if !identity.Handle.IsInvalidHandle() { ··· 113 didHandleMap[identity.DID.String()] = identity.DID.String() 114 } 115 } 116 117 - mergeCheckResponse := s.mergeCheck(f, pull) 118 resubmitResult := pages.Unknown 119 if user != nil && user.Did == pull.OwnerDid { 120 - resubmitResult = s.resubmitCheck(f, pull) 121 } 122 123 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 124 LoggedInUser: user, 125 - RepoInfo: f.RepoInfo(s, user), 126 DidHandleMap: didHandleMap, 127 Pull: pull, 128 MergeCheck: mergeCheckResponse, ··· 130 }) 131 } 132 133 - func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse { 134 if pull.State == db.PullMerged { 135 return types.MergeCheckResponse{} 136 } ··· 190 return mergeCheckResponse 191 } 192 193 - func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult { 194 if pull.State == db.PullMerged || pull.PullSource == nil { 195 return pages.Unknown 196 } 197 ··· 199 200 if pull.PullSource.RepoAt != nil { 201 // fork-based pulls 202 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 203 if err != nil { 204 log.Println("failed to get source repo", err) 205 return pages.Unknown 206 } 207 ··· 210 repoName = sourceRepo.Name 211 } else { 212 // pulls within the same repo 213 knot = f.Knot 214 ownerDid = f.OwnerDid() 215 repoName = f.RepoName 216 } 217 218 us, err := NewUnsignedClient(knot, s.config.Dev) 219 if err != nil { 220 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 221 return pages.Unknown 222 } 223 224 resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 225 if err != nil { 226 log.Println("failed to reach knotserver", err) 227 return pages.Unknown 228 } 229 230 body, err := io.ReadAll(resp.Body) 231 if err != nil { 232 log.Printf("error reading response body: %v", err) 233 return pages.Unknown 234 } 235 defer resp.Body.Close() ··· 237 var result types.RepoBranchResponse 238 if err := json.Unmarshal(body, &result); err != nil { 239 log.Println("failed to parse response:", err) 240 return pages.Unknown 241 } 242 243 latestSubmission := pull.Submissions[pull.LastRoundNumber()] 244 if latestSubmission.SourceRev != result.Branch.Hash { 245 fmt.Println(latestSubmission.SourceRev, result.Branch.Hash) 246 return pages.ShouldResubmit 247 } 248 249 return pages.ShouldNotResubmit 250 } 251 252 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 253 - user := s.auth.GetUser(r) 254 - f, err := s.fullyResolvedRepo(r) 255 if err != nil { 256 log.Println("failed to get repo and knot", err) 257 return 258 } 259 260 - pull, ok := r.Context().Value("pull").(*db.Pull) 261 if !ok { 262 - log.Println("failed to get pull") 263 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 264 return 265 } ··· 269 if err != nil || roundIdInt >= len(pull.Submissions) { 270 http.Error(w, "bad round id", http.StatusBadRequest) 271 log.Println("failed to parse round id", err) 272 return 273 } 274 275 identsToResolve := []string{pull.OwnerDid} 276 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 277 didHandleMap := make(map[string]string) 278 for _, identity := range resolvedIds { 279 if !identity.Handle.IsInvalidHandle() { ··· 282 didHandleMap[identity.DID.String()] = identity.DID.String() 283 } 284 } 285 286 diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch) 287 288 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 289 LoggedInUser: user, 290 DidHandleMap: didHandleMap, 291 - RepoInfo: f.RepoInfo(s, user), 292 Pull: pull, 293 Round: roundIdInt, 294 Submission: pull.Submissions[roundIdInt], 295 Diff: &diff, 296 }) 297 - 298 } 299 300 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 301 user := s.auth.GetUser(r) 302 303 - f, err := s.fullyResolvedRepo(r) 304 if err != nil { 305 log.Println("failed to get repo and knot", err) 306 return 307 } 308 309 - pull, ok := r.Context().Value("pull").(*db.Pull) 310 if !ok { 311 log.Println("failed to get pull") 312 s.pages.Notice(w, "pull-error", "Failed to get pull.") 313 return 314 } 315 316 roundId := chi.URLParam(r, "round") 317 roundIdInt, err := strconv.Atoi(roundId) 318 if err != nil || roundIdInt >= len(pull.Submissions) { 319 http.Error(w, "bad round id", http.StatusBadRequest) 320 log.Println("failed to parse round id", err) 321 return 322 } 323 324 if roundIdInt == 0 { 325 http.Error(w, "bad round id", http.StatusBadRequest) 326 log.Println("cannot interdiff initial submission") 327 return 328 } 329 330 identsToResolve := []string{pull.OwnerDid} 331 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 332 didHandleMap := make(map[string]string) 333 for _, identity := range resolvedIds { 334 if !identity.Handle.IsInvalidHandle() { ··· 337 didHandleMap[identity.DID.String()] = identity.DID.String() 338 } 339 } 340 341 currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch) 342 if err != nil { 343 log.Println("failed to interdiff; current patch malformed") 344 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 345 return 346 } 347 ··· 349 if err != nil { 350 log.Println("failed to interdiff; previous patch malformed") 351 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 352 return 353 } 354 355 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 356 357 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 358 - LoggedInUser: s.auth.GetUser(r), 359 - RepoInfo: f.RepoInfo(s, user), 360 Pull: pull, 361 Round: roundIdInt, 362 DidHandleMap: didHandleMap, 363 Interdiff: interdiff, 364 }) 365 return 366 } 367 368 func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 369 - pull, ok := r.Context().Value("pull").(*db.Pull) 370 if !ok { 371 log.Println("failed to get pull") 372 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 373 return 374 } 375 376 roundId := chi.URLParam(r, "round") 377 roundIdInt, err := strconv.Atoi(roundId) 378 if err != nil || roundIdInt >= len(pull.Submissions) { 379 http.Error(w, "bad round id", http.StatusBadRequest) 380 log.Println("failed to parse round id", err) 381 return 382 } 383 384 identsToResolve := []string{pull.OwnerDid} 385 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 386 didHandleMap := make(map[string]string) 387 for _, identity := range resolvedIds { 388 if !identity.Handle.IsInvalidHandle() { ··· 391 didHandleMap[identity.DID.String()] = identity.DID.String() 392 } 393 } 394 395 w.Header().Set("Content-Type", "text/plain") 396 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 397 } 398 399 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 400 user := s.auth.GetUser(r) 401 params := r.URL.Query() 402 403 state := db.PullOpen 404 switch params.Get("state") { 405 case "closed": ··· 407 case "merged": 408 state = db.PullMerged 409 } 410 411 - f, err := s.fullyResolvedRepo(r) 412 if err != nil { 413 log.Println("failed to get repo and knot", err) 414 return 415 } 416 417 - pulls, err := db.GetPulls(s.db, f.RepoAt, state) 418 if err != nil { 419 log.Println("failed to get pulls", err) 420 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 421 return 422 } 423 424 for _, p := range pulls { 425 var pullSourceRepo *db.Repo 426 if p.PullSource != nil { 427 if p.PullSource.RepoAt != nil { 428 - pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String()) 429 if err != nil { 430 log.Printf("failed to get repo by at uri: %v", err) 431 continue ··· 435 } 436 } 437 } 438 439 identsToResolve := make([]string, len(pulls)) 440 for i, pull := range pulls { 441 identsToResolve[i] = pull.OwnerDid 442 } 443 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 444 didHandleMap := make(map[string]string) 445 for _, identity := range resolvedIds { 446 if !identity.Handle.IsInvalidHandle() { ··· 449 didHandleMap[identity.DID.String()] = identity.DID.String() 450 } 451 } 452 453 s.pages.RepoPulls(w, pages.RepoPullsParams{ 454 - LoggedInUser: s.auth.GetUser(r), 455 - RepoInfo: f.RepoInfo(s, user), 456 Pulls: pulls, 457 DidHandleMap: didHandleMap, 458 FilteringBy: state, 459 }) 460 return 461 } 462 463 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 464 - user := s.auth.GetUser(r) 465 - f, err := s.fullyResolvedRepo(r) 466 if err != nil { 467 log.Println("failed to get repo and knot", err) 468 return 469 } 470 471 - pull, ok := r.Context().Value("pull").(*db.Pull) 472 if !ok { 473 log.Println("failed to get pull") 474 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 475 return 476 } 477 478 roundNumberStr := chi.URLParam(r, "round") 479 roundNumber, err := strconv.Atoi(roundNumberStr) 480 if err != nil || roundNumber >= len(pull.Submissions) { 481 http.Error(w, "bad round id", http.StatusBadRequest) 482 log.Println("failed to parse round id", err) 483 return 484 } 485 486 switch r.Method { 487 case http.MethodGet: 488 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 489 LoggedInUser: user, 490 - RepoInfo: f.RepoInfo(s, user), 491 Pull: pull, 492 RoundNumber: roundNumber, 493 }) 494 return 495 case http.MethodPost: 496 body := r.FormValue("body") 497 if body == "" { 498 s.pages.Notice(w, "pull", "Comment body is required") 499 return 500 } 501 502 // Start a transaction 503 - tx, err := s.db.BeginTx(r.Context(), nil) 504 if err != nil { 505 log.Println("failed to start transaction", err) 506 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 507 return 508 } 509 defer tx.Rollback() 510 511 createdAt := time.Now().Format(time.RFC3339) 512 ownerDid := user.Did 513 514 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 515 if err != nil { 516 log.Println("failed to get pull at", err) 517 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 518 return 519 } 520 521 atUri := f.RepoAt.String() 522 - client, _ := s.auth.AuthorizedClient(r) 523 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 524 Collection: tangled.RepoPullCommentNSID, 525 Repo: user.Did, 526 Rkey: appview.TID(), ··· 537 if err != nil { 538 log.Println("failed to create pull comment", err) 539 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 540 return 541 } 542 543 // Create the pull comment in the database with the commentAt field 544 - commentId, err := db.NewPullComment(tx, &db.PullComment{ 545 OwnerDid: user.Did, 546 RepoAt: f.RepoAt.String(), 547 PullId: pull.PullId, ··· 552 if err != nil { 553 log.Println("failed to create pull comment", err) 554 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 555 return 556 } 557 558 - // Commit the transaction 559 if err = tx.Commit(); err != nil { 560 log.Println("failed to commit transaction", err) 561 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 568 } 569 570 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 571 - user := s.auth.GetUser(r) 572 - f, err := s.fullyResolvedRepo(r) 573 if err != nil { 574 log.Println("failed to get repo and knot", err) 575 return 576 } 577 578 switch r.Method { 579 case http.MethodGet: 580 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 581 if err != nil { 582 log.Printf("failed to create unsigned client for %s", f.Knot) 583 s.pages.Error503(w) 584 return 585 } ··· 587 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 588 if err != nil { 589 log.Println("failed to reach knotserver", err) 590 return 591 } 592 593 body, err := io.ReadAll(resp.Body) 594 if err != nil { 595 log.Printf("Error reading response body: %v", err) 596 return 597 } 598 ··· 600 err = json.Unmarshal(body, &result) 601 if err != nil { 602 log.Println("failed to parse response:", err) 603 return 604 } 605 606 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 607 LoggedInUser: user, 608 - RepoInfo: f.RepoInfo(s, user), 609 Branches: result.Branches, 610 }) 611 case http.MethodPost: 612 title := r.FormValue("title") 613 body := r.FormValue("body") 614 targetBranch := r.FormValue("targetBranch") 615 fromFork := r.FormValue("fork") 616 sourceBranch := r.FormValue("sourceBranch") 617 patch := r.FormValue("patch") 618 619 if targetBranch == "" { 620 s.pages.Notice(w, "pull", "Target branch is required.") 621 return 622 } 623 624 // Determine PR type based on input parameters 625 - isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 626 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 627 isForkBased := fromFork != "" && sourceBranch != "" 628 isPatchBased := patch != "" && !isBranchBased && !isForkBased 629 630 if isPatchBased && !patchutil.IsFormatPatch(patch) { 631 if title == "" { 632 s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 633 return 634 } 635 } ··· 637 // Validate we have at least one valid PR creation method 638 if !isBranchBased && !isPatchBased && !isForkBased { 639 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 640 return 641 } 642 643 // Can't mix branch-based and patch-based approaches 644 if isBranchBased && patch != "" { 645 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 646 return 647 } 648 649 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 650 if err != nil { 651 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 652 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 653 return 654 } ··· 656 caps, err := us.Capabilities() 657 if err != nil { 658 log.Println("error fetching knot caps", f.Knot, err) 659 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 660 return 661 } 662 663 if !caps.PullRequests.FormatPatch { 664 s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 665 return 666 } 667 ··· 669 if isBranchBased { 670 if !caps.PullRequests.BranchSubmissions { 671 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 672 return 673 } 674 - s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch) 675 } else if isForkBased { 676 if !caps.PullRequests.ForkSubmissions { 677 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 678 return 679 } 680 - s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch) 681 } else if isPatchBased { 682 if !caps.PullRequests.PatchSubmissions { 683 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 684 return 685 } 686 - s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch) 687 } 688 return 689 } 690 } 691 692 func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 693 pullSource := &db.PullSource{ 694 Branch: sourceBranch, 695 } ··· 701 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 702 if err != nil { 703 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 704 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 705 return 706 } ··· 708 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 709 if err != nil { 710 log.Println("failed to compare", err) 711 s.pages.Notice(w, "pull", err.Error()) 712 return 713 } ··· 715 sourceRev := comparison.Rev2 716 patch := comparison.Patch 717 718 if !patchutil.IsPatchValid(patch) { 719 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 720 return 721 } 722 723 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 724 } 725 726 func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 727 if !patchutil.IsPatchValid(patch) { 728 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 729 return 730 } 731 732 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 733 } 734 735 func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 736 - fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 737 if errors.Is(err, sql.ErrNoRows) { 738 s.pages.Notice(w, "pull", "No such fork.") 739 return 740 } else if err != nil { 741 log.Println("failed to fetch fork:", err) 742 s.pages.Notice(w, "pull", "Failed to fetch fork.") 743 return 744 } ··· 746 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 747 if err != nil { 748 log.Println("failed to fetch registration key:", err) 749 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 750 return 751 } ··· 753 sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 754 if err != nil { 755 log.Println("failed to create signed client:", err) 756 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 757 return 758 } ··· 760 us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 761 if err != nil { 762 log.Println("failed to create unsigned client:", err) 763 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 764 return 765 } ··· 767 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 768 if err != nil { 769 log.Println("failed to create hidden ref:", err, resp.StatusCode) 770 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 771 return 772 } 773 774 switch resp.StatusCode { 775 case 404: 776 case 400: 777 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 778 return 779 } 780 781 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 782 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 783 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 784 // hiddenRef: hidden/feature-1/main (on repo-fork) ··· 787 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 788 if err != nil { 789 log.Println("failed to compare across branches", err) 790 s.pages.Notice(w, "pull", err.Error()) 791 return 792 } 793 794 sourceRev := comparison.Rev2 795 patch := comparison.Patch 796 797 if !patchutil.IsPatchValid(patch) { 798 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 799 return 800 } ··· 802 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 803 if err != nil { 804 log.Println("failed to parse fork AT URI", err) 805 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 806 return 807 } 808 809 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 810 Branch: sourceBranch, 811 RepoAt: &forkAtUri, 812 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) ··· 823 pullSource *db.PullSource, 824 recordPullSource *tangled.RepoPull_Source, 825 ) { 826 - tx, err := s.db.BeginTx(r.Context(), nil) 827 if err != nil { 828 log.Println("failed to start tx") 829 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 830 return 831 } ··· 836 if title == "" { 837 formatPatches, err := patchutil.ExtractPatches(patch) 838 if err != nil { 839 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 840 return 841 } 842 if len(formatPatches) == 0 { 843 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 844 return 845 } 846 847 title = formatPatches[0].Title 848 body = formatPatches[0].Body 849 } 850 851 rkey := appview.TID() ··· 853 Patch: patch, 854 SourceRev: sourceRev, 855 } 856 - err = db.NewPull(tx, &db.Pull{ 857 Title: title, 858 Body: body, 859 TargetBranch: targetBranch, ··· 867 }) 868 if err != nil { 869 log.Println("failed to create pull request", err) 870 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 871 return 872 } 873 - client, _ := s.auth.AuthorizedClient(r) 874 pullId, err := db.NextPullId(s.db, f.RepoAt) 875 if err != nil { 876 log.Println("failed to get pull id", err) 877 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 878 return 879 } 880 881 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 882 Collection: tangled.RepoPullNSID, 883 Repo: user.Did, 884 Rkey: rkey, ··· 896 897 if err != nil { 898 log.Println("failed to create pull request", err) 899 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 900 return 901 } ··· 904 } 905 906 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 907 - _, err := s.fullyResolvedRepo(r) 908 if err != nil { 909 log.Println("failed to get repo and knot", err) 910 return 911 } 912 913 patch := r.FormValue("patch") 914 if patch == "" { 915 s.pages.Notice(w, "patch-error", "Patch is required.") 916 return 917 } 918 919 - if patch == "" || !patchutil.IsPatchValid(patch) { 920 s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 921 return 922 } 923 924 - if patchutil.IsFormatPatch(patch) { 925 s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.") 926 } else { 927 s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") ··· 929 } 930 931 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 932 - user := s.auth.GetUser(r) 933 - f, err := s.fullyResolvedRepo(r) 934 if err != nil { 935 log.Println("failed to get repo and knot", err) 936 return 937 } 938 939 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 940 - RepoInfo: f.RepoInfo(s, user), 941 }) 942 } 943 944 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 945 - user := s.auth.GetUser(r) 946 - f, err := s.fullyResolvedRepo(r) 947 if err != nil { 948 log.Println("failed to get repo and knot", err) 949 return 950 } 951 952 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 953 if err != nil { 954 log.Printf("failed to create unsigned client for %s", f.Knot) 955 s.pages.Error503(w) 956 return 957 } ··· 959 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 960 if err != nil { 961 log.Println("failed to reach knotserver", err) 962 return 963 } 964 965 body, err := io.ReadAll(resp.Body) 966 if err != nil { 967 log.Printf("Error reading response body: %v", err) 968 return 969 } 970 971 var result types.RepoBranchesResponse 972 err = json.Unmarshal(body, &result) 973 if err != nil { 974 log.Println("failed to parse response:", err) 975 return 976 } 977 978 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 979 - RepoInfo: f.RepoInfo(s, user), 980 Branches: result.Branches, 981 }) 982 } 983 984 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 985 - user := s.auth.GetUser(r) 986 - f, err := s.fullyResolvedRepo(r) 987 if err != nil { 988 log.Println("failed to get repo and knot", err) 989 return 990 } 991 992 - forks, err := db.GetForksByDid(s.db, user.Did) 993 if err != nil { 994 log.Println("failed to get forks", err) 995 return 996 } 997 998 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 999 - RepoInfo: f.RepoInfo(s, user), 1000 Forks: forks, 1001 }) 1002 } 1003 1004 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1005 - user := s.auth.GetUser(r) 1006 1007 - f, err := s.fullyResolvedRepo(r) 1008 if err != nil { 1009 log.Println("failed to get repo and knot", err) 1010 return 1011 } 1012 1013 forkVal := r.URL.Query().Get("fork") 1014 1015 // fork repo 1016 - repo, err := db.GetRepo(s.db, user.Did, forkVal) 1017 if err != nil { 1018 log.Println("failed to get repo", user.Did, forkVal) 1019 return 1020 } 1021 1022 sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1023 if err != nil { 1024 log.Printf("failed to create unsigned client for %s", repo.Knot) 1025 s.pages.Error503(w) 1026 return 1027 } ··· 1029 sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1030 if err != nil { 1031 log.Println("failed to reach knotserver for source branches", err) 1032 return 1033 } 1034 1035 sourceBody, err := io.ReadAll(sourceResp.Body) 1036 if err != nil { 1037 log.Println("failed to read source response body", err) 1038 return 1039 } 1040 defer sourceResp.Body.Close() ··· 1043 err = json.Unmarshal(sourceBody, &sourceResult) 1044 if err != nil { 1045 log.Println("failed to parse source branches response:", err) 1046 return 1047 } 1048 1049 targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1050 if err != nil { 1051 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1052 s.pages.Error503(w) 1053 return 1054 } ··· 1056 targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1057 if err != nil { 1058 log.Println("failed to reach knotserver for target branches", err) 1059 return 1060 } 1061 1062 targetBody, err := io.ReadAll(targetResp.Body) 1063 if err != nil { 1064 log.Println("failed to read target response body", err) 1065 return 1066 } 1067 defer targetResp.Body.Close() ··· 1070 err = json.Unmarshal(targetBody, &targetResult) 1071 if err != nil { 1072 log.Println("failed to parse target branches response:", err) 1073 return 1074 } 1075 1076 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1077 - RepoInfo: f.RepoInfo(s, user), 1078 SourceBranches: sourceResult.Branches, 1079 TargetBranches: targetResult.Branches, 1080 }) 1081 } 1082 1083 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1084 - user := s.auth.GetUser(r) 1085 - f, err := s.fullyResolvedRepo(r) 1086 if err != nil { 1087 log.Println("failed to get repo and knot", err) 1088 return 1089 } 1090 1091 - pull, ok := r.Context().Value("pull").(*db.Pull) 1092 if !ok { 1093 log.Println("failed to get pull") 1094 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1095 return 1096 } 1097 1098 switch r.Method { 1099 case http.MethodGet: 1100 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1101 - RepoInfo: f.RepoInfo(s, user), 1102 Pull: pull, 1103 }) 1104 return 1105 case http.MethodPost: 1106 if pull.IsPatchBased() { 1107 - s.resubmitPatch(w, r) 1108 return 1109 } else if pull.IsBranchBased() { 1110 - s.resubmitBranch(w, r) 1111 return 1112 } else if pull.IsForkBased() { 1113 - s.resubmitFork(w, r) 1114 return 1115 } 1116 } 1117 } 1118 1119 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1120 - user := s.auth.GetUser(r) 1121 1122 - pull, ok := r.Context().Value("pull").(*db.Pull) 1123 if !ok { 1124 log.Println("failed to get pull") 1125 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1126 return 1127 } 1128 1129 - f, err := s.fullyResolvedRepo(r) 1130 if err != nil { 1131 log.Println("failed to get repo and knot", err) 1132 return 1133 } 1134 1135 if user.Did != pull.OwnerDid { 1136 log.Println("unauthorized user") 1137 w.WriteHeader(http.StatusUnauthorized) 1138 return 1139 } 1140 1141 patch := r.FormValue("patch") 1142 1143 if err = validateResubmittedPatch(pull, patch); err != nil { 1144 s.pages.Notice(w, "resubmit-error", err.Error()) 1145 return 1146 } 1147 1148 - tx, err := s.db.BeginTx(r.Context(), nil) 1149 if err != nil { 1150 log.Println("failed to start tx") 1151 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1152 return 1153 } ··· 1156 err = db.ResubmitPull(tx, pull, patch, "") 1157 if err != nil { 1158 log.Println("failed to resubmit pull request", err) 1159 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1160 return 1161 } 1162 - client, _ := s.auth.AuthorizedClient(r) 1163 1164 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1165 if err != nil { 1166 // failed to get record 1167 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1168 return 1169 } 1170 1171 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1172 Collection: tangled.RepoPullNSID, 1173 Repo: user.Did, 1174 Rkey: pull.Rkey, ··· 1185 }) 1186 if err != nil { 1187 log.Println("failed to update record", err) 1188 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1189 return 1190 } 1191 1192 if err = tx.Commit(); err != nil { 1193 log.Println("failed to commit transaction", err) 1194 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1195 return 1196 } ··· 1200 } 1201 1202 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1203 - user := s.auth.GetUser(r) 1204 1205 - pull, ok := r.Context().Value("pull").(*db.Pull) 1206 if !ok { 1207 log.Println("failed to get pull") 1208 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1209 return 1210 } 1211 1212 - f, err := s.fullyResolvedRepo(r) 1213 if err != nil { 1214 log.Println("failed to get repo and knot", err) 1215 return 1216 } 1217 1218 if user.Did != pull.OwnerDid { 1219 log.Println("unauthorized user") 1220 w.WriteHeader(http.StatusUnauthorized) 1221 return 1222 } 1223 1224 - if !f.RepoInfo(s, user).Roles.IsPushAllowed() { 1225 log.Println("unauthorized user") 1226 w.WriteHeader(http.StatusUnauthorized) 1227 return 1228 } ··· 1230 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1231 if err != nil { 1232 log.Printf("failed to create client for %s: %s", f.Knot, err) 1233 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1234 return 1235 } ··· 1237 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1238 if err != nil { 1239 log.Printf("compare request failed: %s", err) 1240 s.pages.Notice(w, "resubmit-error", err.Error()) 1241 return 1242 } 1243 1244 sourceRev := comparison.Rev2 1245 patch := comparison.Patch 1246 1247 if err = validateResubmittedPatch(pull, patch); err != nil { 1248 s.pages.Notice(w, "resubmit-error", err.Error()) 1249 return 1250 } 1251 1252 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1253 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1254 return 1255 } 1256 1257 - tx, err := s.db.BeginTx(r.Context(), nil) 1258 if err != nil { 1259 log.Println("failed to start tx") 1260 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1261 return 1262 } ··· 1265 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1266 if err != nil { 1267 log.Println("failed to create pull request", err) 1268 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1269 return 1270 } 1271 - client, _ := s.auth.AuthorizedClient(r) 1272 1273 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1274 if err != nil { 1275 // failed to get record 1276 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1277 return 1278 } ··· 1280 recordPullSource := &tangled.RepoPull_Source{ 1281 Branch: pull.PullSource.Branch, 1282 } 1283 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1284 Collection: tangled.RepoPullNSID, 1285 Repo: user.Did, 1286 Rkey: pull.Rkey, ··· 1298 }) 1299 if err != nil { 1300 log.Println("failed to update record", err) 1301 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1302 return 1303 } 1304 1305 if err = tx.Commit(); err != nil { 1306 log.Println("failed to commit transaction", err) 1307 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1308 return 1309 } ··· 1313 } 1314 1315 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1316 - user := s.auth.GetUser(r) 1317 1318 - pull, ok := r.Context().Value("pull").(*db.Pull) 1319 if !ok { 1320 log.Println("failed to get pull") 1321 - s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.") 1322 return 1323 } 1324 1325 - f, err := s.fullyResolvedRepo(r) 1326 if err != nil { 1327 log.Println("failed to get repo and knot", err) 1328 return 1329 } 1330 1331 if user.Did != pull.OwnerDid { 1332 log.Println("unauthorized user") 1333 w.WriteHeader(http.StatusUnauthorized) 1334 return 1335 } 1336 1337 - forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 1338 if err != nil { 1339 log.Println("failed to get source repo", err) 1340 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1341 return 1342 } 1343 1344 // extract patch by performing compare 1345 ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1346 if err != nil { 1347 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1348 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1349 return 1350 } ··· 1352 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1353 if err != nil { 1354 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1355 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1356 return 1357 } ··· 1360 signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1361 if err != nil { 1362 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1363 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1364 return 1365 } ··· 1367 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1368 if err != nil || resp.StatusCode != http.StatusNoContent { 1369 log.Printf("failed to update tracking branch: %s", err) 1370 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1371 return 1372 } 1373 1374 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1375 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1376 if err != nil { 1377 log.Printf("failed to compare branches: %s", err) 1378 s.pages.Notice(w, "resubmit-error", err.Error()) 1379 return 1380 } 1381 1382 sourceRev := comparison.Rev2 1383 patch := comparison.Patch 1384 1385 if err = validateResubmittedPatch(pull, patch); err != nil { 1386 s.pages.Notice(w, "resubmit-error", err.Error()) 1387 return 1388 } 1389 1390 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1391 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1392 return 1393 } 1394 1395 - tx, err := s.db.BeginTx(r.Context(), nil) 1396 if err != nil { 1397 log.Println("failed to start tx") 1398 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1399 return 1400 } ··· 1403 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1404 if err != nil { 1405 log.Println("failed to create pull request", err) 1406 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1407 return 1408 } 1409 - client, _ := s.auth.AuthorizedClient(r) 1410 1411 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1412 if err != nil { 1413 // failed to get record 1414 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1415 return 1416 } ··· 1420 Branch: pull.PullSource.Branch, 1421 Repo: &repoAt, 1422 } 1423 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1424 Collection: tangled.RepoPullNSID, 1425 Repo: user.Did, 1426 Rkey: pull.Rkey, ··· 1438 }) 1439 if err != nil { 1440 log.Println("failed to update record", err) 1441 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1442 return 1443 } 1444 1445 if err = tx.Commit(); err != nil { 1446 log.Println("failed to commit transaction", err) 1447 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1448 return 1449 } ··· 1470 } 1471 1472 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1473 - f, err := s.fullyResolvedRepo(r) 1474 if err != nil { 1475 log.Println("failed to resolve repo:", err) 1476 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1477 return 1478 } 1479 1480 - pull, ok := r.Context().Value("pull").(*db.Pull) 1481 if !ok { 1482 log.Println("failed to get pull") 1483 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1484 return 1485 } 1486 1487 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1488 if err != nil { 1489 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1490 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1491 return 1492 } 1493 1494 - ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid) 1495 if err != nil { 1496 log.Printf("resolving identity: %s", err) 1497 w.WriteHeader(http.StatusNotFound) 1498 return 1499 } ··· 1501 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1502 if err != nil { 1503 log.Printf("failed to get primary email: %s", err) 1504 } 1505 1506 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1507 if err != nil { 1508 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1509 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1510 return 1511 } ··· 1514 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1515 if err != nil { 1516 log.Printf("failed to merge pull request: %s", err) 1517 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1518 return 1519 } 1520 1521 if resp.StatusCode == http.StatusOK { 1522 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1523 if err != nil { 1524 log.Printf("failed to update pull request status in database: %s", err) 1525 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1526 return 1527 } 1528 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1529 } else { 1530 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1531 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1532 } 1533 } 1534 1535 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1536 - user := s.auth.GetUser(r) 1537 1538 - f, err := s.fullyResolvedRepo(r) 1539 if err != nil { 1540 log.Println("malformed middleware") 1541 return 1542 } 1543 1544 - pull, ok := r.Context().Value("pull").(*db.Pull) 1545 if !ok { 1546 log.Println("failed to get pull") 1547 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1548 return 1549 } 1550 1551 // auth filter: only owner or collaborators can close 1552 roles := RolesInRepo(s, user, f) 1553 isCollaborator := roles.IsCollaborator() 1554 isPullAuthor := user.Did == pull.OwnerDid 1555 isCloseAllowed := isCollaborator || isPullAuthor 1556 if !isCloseAllowed { 1557 log.Println("failed to close pull") 1558 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1559 return 1560 } 1561 1562 // Start a transaction 1563 - tx, err := s.db.BeginTx(r.Context(), nil) 1564 if err != nil { 1565 log.Println("failed to start transaction", err) 1566 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1567 return 1568 } ··· 1571 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 1572 if err != nil { 1573 log.Println("failed to close pull", err) 1574 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1575 return 1576 } ··· 1578 // Commit the transaction 1579 if err = tx.Commit(); err != nil { 1580 log.Println("failed to commit transaction", err) 1581 s.pages.Notice(w, "pull-close", "Failed to close pull.") 1582 return 1583 } ··· 1587 } 1588 1589 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1590 - user := s.auth.GetUser(r) 1591 1592 - f, err := s.fullyResolvedRepo(r) 1593 if err != nil { 1594 log.Println("failed to resolve repo", err) 1595 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1596 return 1597 } 1598 1599 - pull, ok := r.Context().Value("pull").(*db.Pull) 1600 if !ok { 1601 log.Println("failed to get pull") 1602 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1603 return 1604 } 1605 1606 - // auth filter: only owner or collaborators can close 1607 roles := RolesInRepo(s, user, f) 1608 isCollaborator := roles.IsCollaborator() 1609 isPullAuthor := user.Did == pull.OwnerDid 1610 - isCloseAllowed := isCollaborator || isPullAuthor 1611 - if !isCloseAllowed { 1612 - log.Println("failed to close pull") 1613 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1614 return 1615 } 1616 1617 // Start a transaction 1618 - tx, err := s.db.BeginTx(r.Context(), nil) 1619 if err != nil { 1620 log.Println("failed to start transaction", err) 1621 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1622 return 1623 } ··· 1626 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 1627 if err != nil { 1628 log.Println("failed to reopen pull", err) 1629 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1630 return 1631 } ··· 1633 // Commit the transaction 1634 if err = tx.Commit(); err != nil { 1635 log.Println("failed to commit transaction", err) 1636 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1637 return 1638 }
··· 1 package state 2 3 import ( 4 + "context" 5 "database/sql" 6 "encoding/json" 7 "errors" ··· 12 "strconv" 13 "time" 14 15 + "go.opentelemetry.io/otel/attribute" 16 "tangled.sh/tangled.sh/core/api/tangled" 17 "tangled.sh/tangled.sh/core/appview" 18 "tangled.sh/tangled.sh/core/appview/auth" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 "tangled.sh/tangled.sh/core/patchutil" 22 + "tangled.sh/tangled.sh/core/telemetry" 23 "tangled.sh/tangled.sh/core/types" 24 25 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 30 31 // htmx fragment 32 func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 33 + ctx, span := s.t.TraceStart(r.Context(), "PullActions") 34 + defer span.End() 35 + 36 switch r.Method { 37 case http.MethodGet: 38 user := s.auth.GetUser(r) 39 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 40 if err != nil { 41 log.Println("failed to get repo and knot", err) 42 return 43 } 44 45 + pull, ok := ctx.Value("pull").(*db.Pull) 46 if !ok { 47 log.Println("failed to get pull") 48 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") ··· 60 return 61 } 62 63 + _, mergeSpan := s.t.TraceStart(ctx, "mergeCheck") 64 + mergeCheckResponse := s.mergeCheck(ctx, f, pull) 65 + mergeSpan.End() 66 + 67 resubmitResult := pages.Unknown 68 if user.Did == pull.OwnerDid { 69 + _, resubmitSpan := s.t.TraceStart(ctx, "resubmitCheck") 70 + resubmitResult = s.resubmitCheck(ctx, f, pull) 71 + resubmitSpan.End() 72 } 73 74 + _, renderSpan := s.t.TraceStart(ctx, "renderPullActions") 75 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 76 LoggedInUser: user, 77 + RepoInfo: f.RepoInfo(ctx, s, user), 78 Pull: pull, 79 RoundNumber: roundNumber, 80 MergeCheck: mergeCheckResponse, 81 ResubmitCheck: resubmitResult, 82 }) 83 + renderSpan.End() 84 return 85 } 86 } 87 88 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 89 + ctx, span := s.t.TraceStart(r.Context(), "RepoSinglePull") 90 + defer span.End() 91 + 92 user := s.auth.GetUser(r) 93 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 94 if err != nil { 95 log.Println("failed to get repo and knot", err) 96 + span.RecordError(err) 97 return 98 } 99 100 + pull, ok := ctx.Value("pull").(*db.Pull) 101 if !ok { 102 + err := errors.New("failed to get pull from context") 103 + log.Println(err) 104 + span.RecordError(err) 105 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 106 return 107 } 108 109 + attrs := telemetry.MapAttrs[string](map[string]string{ 110 + "pull.id": fmt.Sprintf("%d", pull.PullId), 111 + "pull.owner": pull.OwnerDid, 112 + }) 113 + 114 + span.SetAttributes(attrs...) 115 + 116 totalIdents := 1 117 for _, submission := range pull.Submissions { 118 totalIdents += len(submission.Comments) ··· 130 } 131 } 132 133 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 134 didHandleMap := make(map[string]string) 135 for _, identity := range resolvedIds { 136 if !identity.Handle.IsInvalidHandle() { ··· 139 didHandleMap[identity.DID.String()] = identity.DID.String() 140 } 141 } 142 + span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds))) 143 144 + mergeCheckResponse := s.mergeCheck(ctx, f, pull) 145 + 146 resubmitResult := pages.Unknown 147 if user != nil && user.Did == pull.OwnerDid { 148 + resubmitResult = s.resubmitCheck(ctx, f, pull) 149 } 150 151 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 152 LoggedInUser: user, 153 + RepoInfo: f.RepoInfo(ctx, s, user), 154 DidHandleMap: didHandleMap, 155 Pull: pull, 156 MergeCheck: mergeCheckResponse, ··· 158 }) 159 } 160 161 + func (s *State) mergeCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse { 162 if pull.State == db.PullMerged { 163 return types.MergeCheckResponse{} 164 } ··· 218 return mergeCheckResponse 219 } 220 221 + func (s *State) resubmitCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult { 222 + ctx, span := s.t.TraceStart(ctx, "resubmitCheck") 223 + defer span.End() 224 + 225 + span.SetAttributes(attribute.Int("pull.id", pull.PullId)) 226 + 227 if pull.State == db.PullMerged || pull.PullSource == nil { 228 + span.SetAttributes(attribute.String("result", "Unknown")) 229 return pages.Unknown 230 } 231 ··· 233 234 if pull.PullSource.RepoAt != nil { 235 // fork-based pulls 236 + span.SetAttributes(attribute.Bool("isForkBased", true)) 237 + sourceRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String()) 238 if err != nil { 239 log.Println("failed to get source repo", err) 240 + span.RecordError(err) 241 + span.SetAttributes(attribute.String("error", "failed_to_get_source_repo")) 242 + span.SetAttributes(attribute.String("result", "Unknown")) 243 return pages.Unknown 244 } 245 ··· 248 repoName = sourceRepo.Name 249 } else { 250 // pulls within the same repo 251 + span.SetAttributes(attribute.Bool("isBranchBased", true)) 252 knot = f.Knot 253 ownerDid = f.OwnerDid() 254 repoName = f.RepoName 255 } 256 257 + span.SetAttributes( 258 + attribute.String("knot", knot), 259 + attribute.String("ownerDid", ownerDid), 260 + attribute.String("repoName", repoName), 261 + attribute.String("sourceBranch", pull.PullSource.Branch), 262 + ) 263 + 264 us, err := NewUnsignedClient(knot, s.config.Dev) 265 if err != nil { 266 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 267 + span.RecordError(err) 268 + span.SetAttributes(attribute.String("error", "failed_to_setup_client")) 269 + span.SetAttributes(attribute.String("result", "Unknown")) 270 return pages.Unknown 271 } 272 273 resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch) 274 if err != nil { 275 log.Println("failed to reach knotserver", err) 276 + span.RecordError(err) 277 + span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver")) 278 + span.SetAttributes(attribute.String("result", "Unknown")) 279 return pages.Unknown 280 } 281 282 body, err := io.ReadAll(resp.Body) 283 if err != nil { 284 log.Printf("error reading response body: %v", err) 285 + span.RecordError(err) 286 + span.SetAttributes(attribute.String("error", "failed_to_read_response")) 287 + span.SetAttributes(attribute.String("result", "Unknown")) 288 return pages.Unknown 289 } 290 defer resp.Body.Close() ··· 292 var result types.RepoBranchResponse 293 if err := json.Unmarshal(body, &result); err != nil { 294 log.Println("failed to parse response:", err) 295 + span.RecordError(err) 296 + span.SetAttributes(attribute.String("error", "failed_to_parse_response")) 297 + span.SetAttributes(attribute.String("result", "Unknown")) 298 return pages.Unknown 299 } 300 301 latestSubmission := pull.Submissions[pull.LastRoundNumber()] 302 + 303 + span.SetAttributes( 304 + attribute.String("latestSubmission.SourceRev", latestSubmission.SourceRev), 305 + attribute.String("branch.Hash", result.Branch.Hash), 306 + ) 307 + 308 if latestSubmission.SourceRev != result.Branch.Hash { 309 fmt.Println(latestSubmission.SourceRev, result.Branch.Hash) 310 + span.SetAttributes(attribute.String("result", "ShouldResubmit")) 311 return pages.ShouldResubmit 312 } 313 314 + span.SetAttributes(attribute.String("result", "ShouldNotResubmit")) 315 return pages.ShouldNotResubmit 316 } 317 318 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 319 + ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatch") 320 + defer span.End() 321 + 322 + user := s.auth.GetUser(r.WithContext(ctx)) 323 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 324 if err != nil { 325 log.Println("failed to get repo and knot", err) 326 + span.RecordError(err) 327 return 328 } 329 330 + pull, ok := ctx.Value("pull").(*db.Pull) 331 if !ok { 332 + err := errors.New("failed to get pull from context") 333 + log.Println(err) 334 + span.RecordError(err) 335 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 336 return 337 } ··· 341 if err != nil || roundIdInt >= len(pull.Submissions) { 342 http.Error(w, "bad round id", http.StatusBadRequest) 343 log.Println("failed to parse round id", err) 344 + span.RecordError(err) 345 + span.SetAttributes(attribute.String("error", "bad_round_id")) 346 return 347 } 348 349 + span.SetAttributes( 350 + attribute.Int("pull.id", pull.PullId), 351 + attribute.Int("round", roundIdInt), 352 + attribute.String("pull.owner", pull.OwnerDid), 353 + ) 354 + 355 identsToResolve := []string{pull.OwnerDid} 356 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 357 didHandleMap := make(map[string]string) 358 for _, identity := range resolvedIds { 359 if !identity.Handle.IsInvalidHandle() { ··· 362 didHandleMap[identity.DID.String()] = identity.DID.String() 363 } 364 } 365 + span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds))) 366 367 diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch) 368 369 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 370 LoggedInUser: user, 371 DidHandleMap: didHandleMap, 372 + RepoInfo: f.RepoInfo(ctx, s, user), 373 Pull: pull, 374 Round: roundIdInt, 375 Submission: pull.Submissions[roundIdInt], 376 Diff: &diff, 377 }) 378 } 379 380 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 381 + ctx, span := s.t.TraceStart(r.Context(), "RepoPullInterdiff") 382 + defer span.End() 383 + 384 user := s.auth.GetUser(r) 385 386 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 387 if err != nil { 388 log.Println("failed to get repo and knot", err) 389 return 390 } 391 392 + pull, ok := ctx.Value("pull").(*db.Pull) 393 if !ok { 394 log.Println("failed to get pull") 395 s.pages.Notice(w, "pull-error", "Failed to get pull.") 396 return 397 } 398 399 + _, roundSpan := s.t.TraceStart(ctx, "parseRound") 400 roundId := chi.URLParam(r, "round") 401 roundIdInt, err := strconv.Atoi(roundId) 402 if err != nil || roundIdInt >= len(pull.Submissions) { 403 http.Error(w, "bad round id", http.StatusBadRequest) 404 log.Println("failed to parse round id", err) 405 + roundSpan.End() 406 return 407 } 408 409 if roundIdInt == 0 { 410 http.Error(w, "bad round id", http.StatusBadRequest) 411 log.Println("cannot interdiff initial submission") 412 + roundSpan.End() 413 return 414 } 415 + roundSpan.End() 416 417 + _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 418 identsToResolve := []string{pull.OwnerDid} 419 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 420 didHandleMap := make(map[string]string) 421 for _, identity := range resolvedIds { 422 if !identity.Handle.IsInvalidHandle() { ··· 425 didHandleMap[identity.DID.String()] = identity.DID.String() 426 } 427 } 428 + identSpan.End() 429 430 + _, diffSpan := s.t.TraceStart(ctx, "calculateInterdiff") 431 currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch) 432 if err != nil { 433 log.Println("failed to interdiff; current patch malformed") 434 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 435 + diffSpan.End() 436 return 437 } 438 ··· 440 if err != nil { 441 log.Println("failed to interdiff; previous patch malformed") 442 s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 443 + diffSpan.End() 444 return 445 } 446 447 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 448 + diffSpan.End() 449 450 + _, renderSpan := s.t.TraceStart(ctx, "renderInterdiffPage") 451 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 452 + LoggedInUser: s.auth.GetUser(r.WithContext(ctx)), 453 + RepoInfo: f.RepoInfo(ctx, s, user), 454 Pull: pull, 455 Round: roundIdInt, 456 DidHandleMap: didHandleMap, 457 Interdiff: interdiff, 458 }) 459 + renderSpan.End() 460 return 461 } 462 463 func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { 464 + ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatchRaw") 465 + defer span.End() 466 + 467 + pull, ok := ctx.Value("pull").(*db.Pull) 468 if !ok { 469 log.Println("failed to get pull") 470 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 471 return 472 } 473 474 + _, roundSpan := s.t.TraceStart(ctx, "parseRound") 475 roundId := chi.URLParam(r, "round") 476 roundIdInt, err := strconv.Atoi(roundId) 477 if err != nil || roundIdInt >= len(pull.Submissions) { 478 http.Error(w, "bad round id", http.StatusBadRequest) 479 log.Println("failed to parse round id", err) 480 + roundSpan.End() 481 return 482 } 483 + roundSpan.End() 484 485 + _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 486 identsToResolve := []string{pull.OwnerDid} 487 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 488 didHandleMap := make(map[string]string) 489 for _, identity := range resolvedIds { 490 if !identity.Handle.IsInvalidHandle() { ··· 493 didHandleMap[identity.DID.String()] = identity.DID.String() 494 } 495 } 496 + identSpan.End() 497 498 + _, writeSpan := s.t.TraceStart(ctx, "writePatch") 499 w.Header().Set("Content-Type", "text/plain") 500 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 501 + writeSpan.End() 502 } 503 504 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 505 + ctx, span := s.t.TraceStart(r.Context(), "RepoPulls") 506 + defer span.End() 507 + 508 user := s.auth.GetUser(r) 509 params := r.URL.Query() 510 511 + _, stateSpan := s.t.TraceStart(ctx, "determinePullState") 512 state := db.PullOpen 513 switch params.Get("state") { 514 case "closed": ··· 516 case "merged": 517 state = db.PullMerged 518 } 519 + stateSpan.End() 520 521 + _, repoSpan := s.t.TraceStart(ctx, "resolveRepo") 522 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 523 if err != nil { 524 log.Println("failed to get repo and knot", err) 525 + repoSpan.End() 526 return 527 } 528 + repoSpan.End() 529 530 + _, pullsSpan := s.t.TraceStart(ctx, "getPulls") 531 + pulls, err := db.GetPulls(ctx, s.db, f.RepoAt, state) 532 if err != nil { 533 log.Println("failed to get pulls", err) 534 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 535 + pullsSpan.End() 536 return 537 } 538 + pullsSpan.End() 539 540 + _, sourceRepoSpan := s.t.TraceStart(ctx, "resolvePullSources") 541 for _, p := range pulls { 542 var pullSourceRepo *db.Repo 543 if p.PullSource != nil { 544 if p.PullSource.RepoAt != nil { 545 + pullSourceRepo, err = db.GetRepoByAtUri(ctx, s.db, p.PullSource.RepoAt.String()) 546 if err != nil { 547 log.Printf("failed to get repo by at uri: %v", err) 548 continue ··· 552 } 553 } 554 } 555 + sourceRepoSpan.End() 556 557 + _, identSpan := s.t.TraceStart(ctx, "resolveIdentities") 558 identsToResolve := make([]string, len(pulls)) 559 for i, pull := range pulls { 560 identsToResolve[i] = pull.OwnerDid 561 } 562 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 563 didHandleMap := make(map[string]string) 564 for _, identity := range resolvedIds { 565 if !identity.Handle.IsInvalidHandle() { ··· 568 didHandleMap[identity.DID.String()] = identity.DID.String() 569 } 570 } 571 + identSpan.End() 572 573 + _, renderSpan := s.t.TraceStart(ctx, "renderPullsPage") 574 s.pages.RepoPulls(w, pages.RepoPullsParams{ 575 + LoggedInUser: s.auth.GetUser(r.WithContext(ctx)), 576 + RepoInfo: f.RepoInfo(ctx, s, user), 577 Pulls: pulls, 578 DidHandleMap: didHandleMap, 579 FilteringBy: state, 580 }) 581 + renderSpan.End() 582 return 583 } 584 585 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 586 + ctx, span := s.t.TraceStart(r.Context(), "PullComment") 587 + defer span.End() 588 + 589 + user := s.auth.GetUser(r.WithContext(ctx)) 590 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 591 if err != nil { 592 log.Println("failed to get repo and knot", err) 593 return 594 } 595 596 + pull, ok := ctx.Value("pull").(*db.Pull) 597 if !ok { 598 log.Println("failed to get pull") 599 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 600 return 601 } 602 603 + _, roundSpan := s.t.TraceStart(ctx, "parseRoundNumber") 604 roundNumberStr := chi.URLParam(r, "round") 605 roundNumber, err := strconv.Atoi(roundNumberStr) 606 if err != nil || roundNumber >= len(pull.Submissions) { 607 http.Error(w, "bad round id", http.StatusBadRequest) 608 log.Println("failed to parse round id", err) 609 + roundSpan.End() 610 return 611 } 612 + roundSpan.End() 613 614 switch r.Method { 615 case http.MethodGet: 616 + _, renderSpan := s.t.TraceStart(ctx, "renderCommentFragment") 617 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 618 LoggedInUser: user, 619 + RepoInfo: f.RepoInfo(ctx, s, user), 620 Pull: pull, 621 RoundNumber: roundNumber, 622 }) 623 + renderSpan.End() 624 return 625 case http.MethodPost: 626 + postCtx, postSpan := s.t.TraceStart(ctx, "CreateComment") 627 + defer postSpan.End() 628 + 629 + _, validateSpan := s.t.TraceStart(postCtx, "validateComment") 630 body := r.FormValue("body") 631 if body == "" { 632 s.pages.Notice(w, "pull", "Comment body is required") 633 + validateSpan.End() 634 return 635 } 636 + validateSpan.End() 637 638 // Start a transaction 639 + _, txSpan := s.t.TraceStart(postCtx, "startTransaction") 640 + tx, err := s.db.BeginTx(postCtx, nil) 641 if err != nil { 642 log.Println("failed to start transaction", err) 643 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 644 + txSpan.End() 645 return 646 } 647 defer tx.Rollback() 648 + txSpan.End() 649 650 createdAt := time.Now().Format(time.RFC3339) 651 ownerDid := user.Did 652 653 + _, pullAtSpan := s.t.TraceStart(postCtx, "getPullAt") 654 + pullAt, err := db.GetPullAt(postCtx, s.db, f.RepoAt, pull.PullId) 655 if err != nil { 656 log.Println("failed to get pull at", err) 657 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 658 + pullAtSpan.End() 659 return 660 } 661 + pullAtSpan.End() 662 663 + _, atProtoSpan := s.t.TraceStart(postCtx, "createAtProtoRecord") 664 atUri := f.RepoAt.String() 665 + client, _ := s.auth.AuthorizedClient(r.WithContext(postCtx)) 666 + atResp, err := comatproto.RepoPutRecord(postCtx, client, &comatproto.RepoPutRecord_Input{ 667 Collection: tangled.RepoPullCommentNSID, 668 Repo: user.Did, 669 Rkey: appview.TID(), ··· 680 if err != nil { 681 log.Println("failed to create pull comment", err) 682 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 683 + atProtoSpan.End() 684 return 685 } 686 + atProtoSpan.End() 687 688 // Create the pull comment in the database with the commentAt field 689 + _, dbSpan := s.t.TraceStart(postCtx, "createDbComment") 690 + commentId, err := db.NewPullComment(postCtx, tx, &db.PullComment{ 691 OwnerDid: user.Did, 692 RepoAt: f.RepoAt.String(), 693 PullId: pull.PullId, ··· 698 if err != nil { 699 log.Println("failed to create pull comment", err) 700 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 701 + dbSpan.End() 702 return 703 } 704 + dbSpan.End() 705 706 if err = tx.Commit(); err != nil { 707 log.Println("failed to commit transaction", err) 708 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 715 } 716 717 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 718 + ctx, span := s.t.TraceStart(r.Context(), "NewPull") 719 + defer span.End() 720 + 721 + user := s.auth.GetUser(r.WithContext(ctx)) 722 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 723 if err != nil { 724 log.Println("failed to get repo and knot", err) 725 + span.RecordError(err) 726 return 727 } 728 729 switch r.Method { 730 case http.MethodGet: 731 + span.SetAttributes(attribute.String("method", "GET")) 732 + 733 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 734 if err != nil { 735 log.Printf("failed to create unsigned client for %s", f.Knot) 736 + span.RecordError(err) 737 s.pages.Error503(w) 738 return 739 } ··· 741 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 742 if err != nil { 743 log.Println("failed to reach knotserver", err) 744 + span.RecordError(err) 745 return 746 } 747 748 body, err := io.ReadAll(resp.Body) 749 if err != nil { 750 log.Printf("Error reading response body: %v", err) 751 + span.RecordError(err) 752 return 753 } 754 ··· 756 err = json.Unmarshal(body, &result) 757 if err != nil { 758 log.Println("failed to parse response:", err) 759 + span.RecordError(err) 760 return 761 } 762 763 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 764 LoggedInUser: user, 765 + RepoInfo: f.RepoInfo(ctx, s, user), 766 Branches: result.Branches, 767 }) 768 case http.MethodPost: 769 + span.SetAttributes(attribute.String("method", "POST")) 770 + 771 title := r.FormValue("title") 772 body := r.FormValue("body") 773 targetBranch := r.FormValue("targetBranch") 774 fromFork := r.FormValue("fork") 775 sourceBranch := r.FormValue("sourceBranch") 776 patch := r.FormValue("patch") 777 + 778 + span.SetAttributes( 779 + attribute.String("targetBranch", targetBranch), 780 + attribute.String("sourceBranch", sourceBranch), 781 + attribute.Bool("hasFork", fromFork != ""), 782 + attribute.Bool("hasPatch", patch != ""), 783 + ) 784 785 if targetBranch == "" { 786 s.pages.Notice(w, "pull", "Target branch is required.") 787 + span.SetAttributes(attribute.String("error", "missing_target_branch")) 788 return 789 } 790 791 // Determine PR type based on input parameters 792 + isPushAllowed := f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() 793 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 794 isForkBased := fromFork != "" && sourceBranch != "" 795 isPatchBased := patch != "" && !isBranchBased && !isForkBased 796 797 + span.SetAttributes( 798 + attribute.Bool("isPushAllowed", isPushAllowed), 799 + attribute.Bool("isBranchBased", isBranchBased), 800 + attribute.Bool("isForkBased", isForkBased), 801 + attribute.Bool("isPatchBased", isPatchBased), 802 + ) 803 + 804 if isPatchBased && !patchutil.IsFormatPatch(patch) { 805 if title == "" { 806 s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 807 + span.SetAttributes(attribute.String("error", "missing_title_for_git_diff")) 808 return 809 } 810 } ··· 812 // Validate we have at least one valid PR creation method 813 if !isBranchBased && !isPatchBased && !isForkBased { 814 s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 815 + span.SetAttributes(attribute.String("error", "no_valid_pr_method")) 816 return 817 } 818 819 // Can't mix branch-based and patch-based approaches 820 if isBranchBased && patch != "" { 821 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 822 + span.SetAttributes(attribute.String("error", "mixed_pr_methods")) 823 return 824 } 825 826 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 827 if err != nil { 828 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 829 + span.RecordError(err) 830 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 831 return 832 } ··· 834 caps, err := us.Capabilities() 835 if err != nil { 836 log.Println("error fetching knot caps", f.Knot, err) 837 + span.RecordError(err) 838 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") 839 return 840 } 841 842 + span.SetAttributes( 843 + attribute.Bool("caps.pullRequests.formatPatch", caps.PullRequests.FormatPatch), 844 + attribute.Bool("caps.pullRequests.branchSubmissions", caps.PullRequests.BranchSubmissions), 845 + attribute.Bool("caps.pullRequests.forkSubmissions", caps.PullRequests.ForkSubmissions), 846 + attribute.Bool("caps.pullRequests.patchSubmissions", caps.PullRequests.PatchSubmissions), 847 + ) 848 + 849 if !caps.PullRequests.FormatPatch { 850 s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.") 851 + span.SetAttributes(attribute.String("error", "formatpatch_not_supported")) 852 return 853 } 854 ··· 856 if isBranchBased { 857 if !caps.PullRequests.BranchSubmissions { 858 s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?") 859 + span.SetAttributes(attribute.String("error", "branch_submissions_not_supported")) 860 return 861 } 862 + s.handleBranchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, sourceBranch) 863 } else if isForkBased { 864 if !caps.PullRequests.ForkSubmissions { 865 s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?") 866 + span.SetAttributes(attribute.String("error", "fork_submissions_not_supported")) 867 return 868 } 869 + s.handleForkBasedPull(w, r.WithContext(ctx), f, user, fromFork, title, body, targetBranch, sourceBranch) 870 } else if isPatchBased { 871 if !caps.PullRequests.PatchSubmissions { 872 s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.") 873 + span.SetAttributes(attribute.String("error", "patch_submissions_not_supported")) 874 return 875 } 876 + s.handlePatchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch) 877 } 878 return 879 } 880 } 881 882 func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 883 + ctx, span := s.t.TraceStart(r.Context(), "handleBranchBasedPull") 884 + defer span.End() 885 + 886 + span.SetAttributes( 887 + attribute.String("targetBranch", targetBranch), 888 + attribute.String("sourceBranch", sourceBranch), 889 + ) 890 + 891 pullSource := &db.PullSource{ 892 Branch: sourceBranch, 893 } ··· 899 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 900 if err != nil { 901 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 902 + span.RecordError(err) 903 + span.SetAttributes(attribute.String("error", "client_creation_failed")) 904 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 905 return 906 } ··· 908 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 909 if err != nil { 910 log.Println("failed to compare", err) 911 + span.RecordError(err) 912 + span.SetAttributes(attribute.String("error", "comparison_failed")) 913 s.pages.Notice(w, "pull", err.Error()) 914 return 915 } ··· 917 sourceRev := comparison.Rev2 918 patch := comparison.Patch 919 920 + span.SetAttributes(attribute.String("sourceRev", sourceRev)) 921 + 922 if !patchutil.IsPatchValid(patch) { 923 + span.SetAttributes(attribute.String("error", "invalid_patch_format")) 924 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 925 return 926 } 927 928 + s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 929 } 930 931 func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 932 + ctx, span := s.t.TraceStart(r.Context(), "handlePatchBasedPull") 933 + defer span.End() 934 + 935 + span.SetAttributes(attribute.String("targetBranch", targetBranch)) 936 + 937 if !patchutil.IsPatchValid(patch) { 938 + span.SetAttributes(attribute.String("error", "invalid_patch_format")) 939 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 940 return 941 } 942 943 + s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, "", nil, nil) 944 } 945 946 func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 947 + ctx, span := s.t.TraceStart(r.Context(), "handleForkBasedPull") 948 + defer span.End() 949 + 950 + span.SetAttributes( 951 + attribute.String("forkRepo", forkRepo), 952 + attribute.String("targetBranch", targetBranch), 953 + attribute.String("sourceBranch", sourceBranch), 954 + ) 955 + 956 + fork, err := db.GetForkByDid(ctx, s.db, user.Did, forkRepo) 957 if errors.Is(err, sql.ErrNoRows) { 958 + span.SetAttributes(attribute.String("error", "fork_not_found")) 959 s.pages.Notice(w, "pull", "No such fork.") 960 return 961 } else if err != nil { 962 log.Println("failed to fetch fork:", err) 963 + span.RecordError(err) 964 + span.SetAttributes(attribute.String("error", "fork_fetch_failed")) 965 s.pages.Notice(w, "pull", "Failed to fetch fork.") 966 return 967 } ··· 969 secret, err := db.GetRegistrationKey(s.db, fork.Knot) 970 if err != nil { 971 log.Println("failed to fetch registration key:", err) 972 + span.RecordError(err) 973 + span.SetAttributes(attribute.String("error", "registration_key_fetch_failed")) 974 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 975 return 976 } ··· 978 sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 979 if err != nil { 980 log.Println("failed to create signed client:", err) 981 + span.RecordError(err) 982 + span.SetAttributes(attribute.String("error", "signed_client_creation_failed")) 983 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 984 return 985 } ··· 987 us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 988 if err != nil { 989 log.Println("failed to create unsigned client:", err) 990 + span.RecordError(err) 991 + span.SetAttributes(attribute.String("error", "unsigned_client_creation_failed")) 992 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 993 return 994 } ··· 996 resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch) 997 if err != nil { 998 log.Println("failed to create hidden ref:", err, resp.StatusCode) 999 + span.RecordError(err) 1000 + span.SetAttributes(attribute.String("error", "hidden_ref_creation_failed")) 1001 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1002 return 1003 } 1004 1005 switch resp.StatusCode { 1006 case 404: 1007 + span.SetAttributes(attribute.String("error", "not_found_status")) 1008 case 400: 1009 + span.SetAttributes(attribute.String("error", "bad_request_status")) 1010 s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 1011 return 1012 } 1013 1014 hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 1015 + span.SetAttributes(attribute.String("hiddenRef", hiddenRef)) 1016 + 1017 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 1018 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 1019 // hiddenRef: hidden/feature-1/main (on repo-fork) ··· 1022 comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 1023 if err != nil { 1024 log.Println("failed to compare across branches", err) 1025 + span.RecordError(err) 1026 + span.SetAttributes(attribute.String("error", "branch_comparison_failed")) 1027 s.pages.Notice(w, "pull", err.Error()) 1028 return 1029 } 1030 1031 sourceRev := comparison.Rev2 1032 patch := comparison.Patch 1033 + span.SetAttributes(attribute.String("sourceRev", sourceRev)) 1034 1035 if !patchutil.IsPatchValid(patch) { 1036 + span.SetAttributes(attribute.String("error", "invalid_patch_format")) 1037 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 1038 return 1039 } ··· 1041 forkAtUri, err := syntax.ParseATURI(fork.AtUri) 1042 if err != nil { 1043 log.Println("failed to parse fork AT URI", err) 1044 + span.RecordError(err) 1045 + span.SetAttributes(attribute.String("error", "fork_aturi_parse_failed")) 1046 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1047 return 1048 } 1049 1050 + s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 1051 Branch: sourceBranch, 1052 RepoAt: &forkAtUri, 1053 }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}) ··· 1064 pullSource *db.PullSource, 1065 recordPullSource *tangled.RepoPull_Source, 1066 ) { 1067 + ctx, span := s.t.TraceStart(r.Context(), "createPullRequest") 1068 + defer span.End() 1069 + 1070 + span.SetAttributes( 1071 + attribute.String("targetBranch", targetBranch), 1072 + attribute.String("sourceRev", sourceRev), 1073 + attribute.Bool("hasPullSource", pullSource != nil), 1074 + ) 1075 + 1076 + tx, err := s.db.BeginTx(ctx, nil) 1077 if err != nil { 1078 log.Println("failed to start tx") 1079 + span.RecordError(err) 1080 + span.SetAttributes(attribute.String("error", "transaction_start_failed")) 1081 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1082 return 1083 } ··· 1088 if title == "" { 1089 formatPatches, err := patchutil.ExtractPatches(patch) 1090 if err != nil { 1091 + span.RecordError(err) 1092 + span.SetAttributes(attribute.String("error", "extract_patches_failed")) 1093 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 1094 return 1095 } 1096 if len(formatPatches) == 0 { 1097 + span.SetAttributes(attribute.String("error", "no_patches_found")) 1098 s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.") 1099 return 1100 } 1101 1102 title = formatPatches[0].Title 1103 body = formatPatches[0].Body 1104 + span.SetAttributes( 1105 + attribute.Bool("title_extracted", true), 1106 + attribute.Bool("body_extracted", formatPatches[0].Body != ""), 1107 + ) 1108 } 1109 1110 rkey := appview.TID() ··· 1112 Patch: patch, 1113 SourceRev: sourceRev, 1114 } 1115 + err = db.NewPull(ctx, tx, &db.Pull{ 1116 Title: title, 1117 Body: body, 1118 TargetBranch: targetBranch, ··· 1126 }) 1127 if err != nil { 1128 log.Println("failed to create pull request", err) 1129 + span.RecordError(err) 1130 + span.SetAttributes(attribute.String("error", "db_create_pull_failed")) 1131 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1132 return 1133 } 1134 + 1135 + client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1136 pullId, err := db.NextPullId(s.db, f.RepoAt) 1137 if err != nil { 1138 log.Println("failed to get pull id", err) 1139 + span.RecordError(err) 1140 + span.SetAttributes(attribute.String("error", "get_pull_id_failed")) 1141 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1142 return 1143 } 1144 + span.SetAttributes(attribute.Int("pullId", pullId)) 1145 1146 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1147 Collection: tangled.RepoPullNSID, 1148 Repo: user.Did, 1149 Rkey: rkey, ··· 1161 1162 if err != nil { 1163 log.Println("failed to create pull request", err) 1164 + span.RecordError(err) 1165 + span.SetAttributes(attribute.String("error", "atproto_create_record_failed")) 1166 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1167 + return 1168 + } 1169 + 1170 + if err = tx.Commit(); err != nil { 1171 + log.Println("failed to commit transaction", err) 1172 + span.RecordError(err) 1173 + span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 1174 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1175 return 1176 } ··· 1179 } 1180 1181 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 1182 + ctx, span := s.t.TraceStart(r.Context(), "ValidatePatch") 1183 + defer span.End() 1184 + 1185 + _, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1186 if err != nil { 1187 log.Println("failed to get repo and knot", err) 1188 + span.RecordError(err) 1189 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1190 return 1191 } 1192 1193 patch := r.FormValue("patch") 1194 + span.SetAttributes(attribute.Bool("hasPatch", patch != "")) 1195 + 1196 if patch == "" { 1197 + span.SetAttributes(attribute.String("error", "empty_patch")) 1198 s.pages.Notice(w, "patch-error", "Patch is required.") 1199 return 1200 } 1201 1202 + if !patchutil.IsPatchValid(patch) { 1203 + span.SetAttributes(attribute.String("error", "invalid_patch_format")) 1204 s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.") 1205 return 1206 } 1207 1208 + isFormatPatch := patchutil.IsFormatPatch(patch) 1209 + span.SetAttributes(attribute.Bool("isFormatPatch", isFormatPatch)) 1210 + 1211 + if isFormatPatch { 1212 s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.") 1213 } else { 1214 s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.") ··· 1216 } 1217 1218 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1219 + ctx, span := s.t.TraceStart(r.Context(), "PatchUploadFragment") 1220 + defer span.End() 1221 + 1222 + user := s.auth.GetUser(r.WithContext(ctx)) 1223 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1224 if err != nil { 1225 log.Println("failed to get repo and knot", err) 1226 + span.RecordError(err) 1227 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1228 return 1229 } 1230 1231 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1232 + RepoInfo: f.RepoInfo(ctx, s, user), 1233 }) 1234 } 1235 1236 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1237 + ctx, span := s.t.TraceStart(r.Context(), "CompareBranchesFragment") 1238 + defer span.End() 1239 + 1240 + user := s.auth.GetUser(r.WithContext(ctx)) 1241 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1242 if err != nil { 1243 log.Println("failed to get repo and knot", err) 1244 + span.RecordError(err) 1245 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1246 return 1247 } 1248 1249 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 1250 if err != nil { 1251 log.Printf("failed to create unsigned client for %s", f.Knot) 1252 + span.RecordError(err) 1253 + span.SetAttributes(attribute.String("error", "client_creation_failed")) 1254 s.pages.Error503(w) 1255 return 1256 } ··· 1258 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 1259 if err != nil { 1260 log.Println("failed to reach knotserver", err) 1261 + span.RecordError(err) 1262 + span.SetAttributes(attribute.String("error", "knotserver_connection_failed")) 1263 return 1264 } 1265 1266 body, err := io.ReadAll(resp.Body) 1267 if err != nil { 1268 log.Printf("Error reading response body: %v", err) 1269 + span.RecordError(err) 1270 + span.SetAttributes(attribute.String("error", "response_read_failed")) 1271 return 1272 } 1273 + defer resp.Body.Close() 1274 1275 var result types.RepoBranchesResponse 1276 err = json.Unmarshal(body, &result) 1277 if err != nil { 1278 log.Println("failed to parse response:", err) 1279 + span.RecordError(err) 1280 + span.SetAttributes(attribute.String("error", "response_parse_failed")) 1281 return 1282 } 1283 + span.SetAttributes(attribute.Int("branches.count", len(result.Branches))) 1284 1285 s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{ 1286 + RepoInfo: f.RepoInfo(ctx, s, user), 1287 Branches: result.Branches, 1288 }) 1289 } 1290 1291 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1292 + ctx, span := s.t.TraceStart(r.Context(), "CompareForksFragment") 1293 + defer span.End() 1294 + 1295 + user := s.auth.GetUser(r.WithContext(ctx)) 1296 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1297 if err != nil { 1298 log.Println("failed to get repo and knot", err) 1299 + span.RecordError(err) 1300 return 1301 } 1302 1303 + forks, err := db.GetForksByDid(ctx, s.db, user.Did) 1304 if err != nil { 1305 log.Println("failed to get forks", err) 1306 + span.RecordError(err) 1307 return 1308 } 1309 1310 s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{ 1311 + RepoInfo: f.RepoInfo(ctx, s, user), 1312 Forks: forks, 1313 }) 1314 } 1315 1316 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1317 + ctx, span := s.t.TraceStart(r.Context(), "CompareForksBranchesFragment") 1318 + defer span.End() 1319 + 1320 + user := s.auth.GetUser(r.WithContext(ctx)) 1321 1322 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1323 if err != nil { 1324 log.Println("failed to get repo and knot", err) 1325 + span.RecordError(err) 1326 return 1327 } 1328 1329 forkVal := r.URL.Query().Get("fork") 1330 + span.SetAttributes(attribute.String("fork", forkVal)) 1331 1332 // fork repo 1333 + repo, err := db.GetRepo(ctx, s.db, user.Did, forkVal) 1334 if err != nil { 1335 log.Println("failed to get repo", user.Did, forkVal) 1336 + span.RecordError(err) 1337 return 1338 } 1339 1340 sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1341 if err != nil { 1342 log.Printf("failed to create unsigned client for %s", repo.Knot) 1343 + span.RecordError(err) 1344 s.pages.Error503(w) 1345 return 1346 } ··· 1348 sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1349 if err != nil { 1350 log.Println("failed to reach knotserver for source branches", err) 1351 + span.RecordError(err) 1352 return 1353 } 1354 1355 sourceBody, err := io.ReadAll(sourceResp.Body) 1356 if err != nil { 1357 log.Println("failed to read source response body", err) 1358 + span.RecordError(err) 1359 return 1360 } 1361 defer sourceResp.Body.Close() ··· 1364 err = json.Unmarshal(sourceBody, &sourceResult) 1365 if err != nil { 1366 log.Println("failed to parse source branches response:", err) 1367 + span.RecordError(err) 1368 return 1369 } 1370 1371 targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1372 if err != nil { 1373 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1374 + span.RecordError(err) 1375 s.pages.Error503(w) 1376 return 1377 } ··· 1379 targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1380 if err != nil { 1381 log.Println("failed to reach knotserver for target branches", err) 1382 + span.RecordError(err) 1383 return 1384 } 1385 1386 targetBody, err := io.ReadAll(targetResp.Body) 1387 if err != nil { 1388 log.Println("failed to read target response body", err) 1389 + span.RecordError(err) 1390 return 1391 } 1392 defer targetResp.Body.Close() ··· 1395 err = json.Unmarshal(targetBody, &targetResult) 1396 if err != nil { 1397 log.Println("failed to parse target branches response:", err) 1398 + span.RecordError(err) 1399 return 1400 } 1401 1402 s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{ 1403 + RepoInfo: f.RepoInfo(ctx, s, user), 1404 SourceBranches: sourceResult.Branches, 1405 TargetBranches: targetResult.Branches, 1406 }) 1407 } 1408 1409 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1410 + ctx, span := s.t.TraceStart(r.Context(), "ResubmitPull") 1411 + defer span.End() 1412 + 1413 + user := s.auth.GetUser(r.WithContext(ctx)) 1414 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1415 if err != nil { 1416 log.Println("failed to get repo and knot", err) 1417 + span.RecordError(err) 1418 return 1419 } 1420 1421 + pull, ok := ctx.Value("pull").(*db.Pull) 1422 if !ok { 1423 log.Println("failed to get pull") 1424 + span.RecordError(errors.New("failed to get pull from context")) 1425 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1426 return 1427 } 1428 1429 + span.SetAttributes( 1430 + attribute.Int("pull.id", pull.PullId), 1431 + attribute.String("pull.owner", pull.OwnerDid), 1432 + attribute.String("method", r.Method), 1433 + ) 1434 + 1435 switch r.Method { 1436 case http.MethodGet: 1437 s.pages.PullResubmitFragment(w, pages.PullResubmitParams{ 1438 + RepoInfo: f.RepoInfo(ctx, s, user), 1439 Pull: pull, 1440 }) 1441 return 1442 case http.MethodPost: 1443 if pull.IsPatchBased() { 1444 + span.SetAttributes(attribute.String("pull.type", "patch_based")) 1445 + s.resubmitPatch(w, r.WithContext(ctx)) 1446 return 1447 } else if pull.IsBranchBased() { 1448 + span.SetAttributes(attribute.String("pull.type", "branch_based")) 1449 + s.resubmitBranch(w, r.WithContext(ctx)) 1450 return 1451 } else if pull.IsForkBased() { 1452 + span.SetAttributes(attribute.String("pull.type", "fork_based")) 1453 + s.resubmitFork(w, r.WithContext(ctx)) 1454 return 1455 } 1456 + span.SetAttributes(attribute.String("pull.type", "unknown")) 1457 } 1458 } 1459 1460 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1461 + ctx, span := s.t.TraceStart(r.Context(), "resubmitPatch") 1462 + defer span.End() 1463 1464 + user := s.auth.GetUser(r.WithContext(ctx)) 1465 + 1466 + pull, ok := ctx.Value("pull").(*db.Pull) 1467 if !ok { 1468 log.Println("failed to get pull") 1469 + span.RecordError(errors.New("failed to get pull from context")) 1470 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1471 return 1472 } 1473 1474 + span.SetAttributes( 1475 + attribute.Int("pull.id", pull.PullId), 1476 + attribute.String("pull.owner", pull.OwnerDid), 1477 + ) 1478 + 1479 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1480 if err != nil { 1481 log.Println("failed to get repo and knot", err) 1482 + span.RecordError(err) 1483 return 1484 } 1485 1486 if user.Did != pull.OwnerDid { 1487 log.Println("unauthorized user") 1488 + span.SetAttributes(attribute.String("error", "unauthorized_user")) 1489 w.WriteHeader(http.StatusUnauthorized) 1490 return 1491 } 1492 1493 patch := r.FormValue("patch") 1494 + span.SetAttributes(attribute.Bool("has_patch", patch != "")) 1495 1496 if err = validateResubmittedPatch(pull, patch); err != nil { 1497 + span.SetAttributes(attribute.String("error", "invalid_patch")) 1498 s.pages.Notice(w, "resubmit-error", err.Error()) 1499 return 1500 } 1501 1502 + tx, err := s.db.BeginTx(ctx, nil) 1503 if err != nil { 1504 log.Println("failed to start tx") 1505 + span.RecordError(err) 1506 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1507 return 1508 } ··· 1511 err = db.ResubmitPull(tx, pull, patch, "") 1512 if err != nil { 1513 log.Println("failed to resubmit pull request", err) 1514 + span.RecordError(err) 1515 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1516 return 1517 } 1518 + client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1519 1520 + ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1521 if err != nil { 1522 // failed to get record 1523 + span.RecordError(err) 1524 + span.SetAttributes(attribute.String("error", "record_not_found")) 1525 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1526 return 1527 } 1528 1529 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1530 Collection: tangled.RepoPullNSID, 1531 Repo: user.Did, 1532 Rkey: pull.Rkey, ··· 1543 }) 1544 if err != nil { 1545 log.Println("failed to update record", err) 1546 + span.RecordError(err) 1547 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1548 return 1549 } 1550 1551 if err = tx.Commit(); err != nil { 1552 log.Println("failed to commit transaction", err) 1553 + span.RecordError(err) 1554 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1555 return 1556 } ··· 1560 } 1561 1562 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1563 + ctx, span := s.t.TraceStart(r.Context(), "resubmitBranch") 1564 + defer span.End() 1565 + 1566 + user := s.auth.GetUser(r.WithContext(ctx)) 1567 1568 + pull, ok := ctx.Value("pull").(*db.Pull) 1569 if !ok { 1570 log.Println("failed to get pull") 1571 + span.RecordError(errors.New("failed to get pull from context")) 1572 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1573 return 1574 } 1575 1576 + span.SetAttributes( 1577 + attribute.Int("pull.id", pull.PullId), 1578 + attribute.String("pull.owner", pull.OwnerDid), 1579 + attribute.String("pull.source_branch", pull.PullSource.Branch), 1580 + attribute.String("pull.target_branch", pull.TargetBranch), 1581 + ) 1582 + 1583 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1584 if err != nil { 1585 log.Println("failed to get repo and knot", err) 1586 + span.RecordError(err) 1587 return 1588 } 1589 1590 if user.Did != pull.OwnerDid { 1591 log.Println("unauthorized user") 1592 + span.SetAttributes(attribute.String("error", "unauthorized_user")) 1593 w.WriteHeader(http.StatusUnauthorized) 1594 return 1595 } 1596 1597 + if !f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() { 1598 log.Println("unauthorized user") 1599 + span.SetAttributes(attribute.String("error", "push_not_allowed")) 1600 w.WriteHeader(http.StatusUnauthorized) 1601 return 1602 } ··· 1604 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1605 if err != nil { 1606 log.Printf("failed to create client for %s: %s", f.Knot, err) 1607 + span.RecordError(err) 1608 + span.SetAttributes(attribute.String("error", "client_creation_failed")) 1609 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1610 return 1611 } ··· 1613 comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1614 if err != nil { 1615 log.Printf("compare request failed: %s", err) 1616 + span.RecordError(err) 1617 + span.SetAttributes(attribute.String("error", "compare_failed")) 1618 s.pages.Notice(w, "resubmit-error", err.Error()) 1619 return 1620 } 1621 1622 sourceRev := comparison.Rev2 1623 patch := comparison.Patch 1624 + span.SetAttributes(attribute.String("source_rev", sourceRev)) 1625 1626 if err = validateResubmittedPatch(pull, patch); err != nil { 1627 + span.SetAttributes(attribute.String("error", "invalid_patch")) 1628 s.pages.Notice(w, "resubmit-error", err.Error()) 1629 return 1630 } 1631 1632 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1633 + span.SetAttributes(attribute.String("error", "no_changes")) 1634 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1635 return 1636 } 1637 1638 + tx, err := s.db.BeginTx(ctx, nil) 1639 if err != nil { 1640 log.Println("failed to start tx") 1641 + span.RecordError(err) 1642 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1643 return 1644 } ··· 1647 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1648 if err != nil { 1649 log.Println("failed to create pull request", err) 1650 + span.RecordError(err) 1651 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1652 return 1653 } 1654 + client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1655 1656 + ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1657 if err != nil { 1658 // failed to get record 1659 + span.RecordError(err) 1660 + span.SetAttributes(attribute.String("error", "record_not_found")) 1661 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1662 return 1663 } ··· 1665 recordPullSource := &tangled.RepoPull_Source{ 1666 Branch: pull.PullSource.Branch, 1667 } 1668 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1669 Collection: tangled.RepoPullNSID, 1670 Repo: user.Did, 1671 Rkey: pull.Rkey, ··· 1683 }) 1684 if err != nil { 1685 log.Println("failed to update record", err) 1686 + span.RecordError(err) 1687 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1688 return 1689 } 1690 1691 if err = tx.Commit(); err != nil { 1692 log.Println("failed to commit transaction", err) 1693 + span.RecordError(err) 1694 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1695 return 1696 } ··· 1700 } 1701 1702 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1703 + ctx, span := s.t.TraceStart(r.Context(), "resubmitFork") 1704 + defer span.End() 1705 1706 + user := s.auth.GetUser(r.WithContext(ctx)) 1707 + 1708 + pull, ok := ctx.Value("pull").(*db.Pull) 1709 if !ok { 1710 log.Println("failed to get pull") 1711 + span.RecordError(errors.New("failed to get pull from context")) 1712 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1713 return 1714 } 1715 1716 + span.SetAttributes( 1717 + attribute.Int("pull.id", pull.PullId), 1718 + attribute.String("pull.owner", pull.OwnerDid), 1719 + attribute.String("pull.source_branch", pull.PullSource.Branch), 1720 + attribute.String("pull.target_branch", pull.TargetBranch), 1721 + ) 1722 + 1723 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1724 if err != nil { 1725 log.Println("failed to get repo and knot", err) 1726 + span.RecordError(err) 1727 return 1728 } 1729 1730 if user.Did != pull.OwnerDid { 1731 log.Println("unauthorized user") 1732 + span.SetAttributes(attribute.String("error", "unauthorized_user")) 1733 w.WriteHeader(http.StatusUnauthorized) 1734 return 1735 } 1736 1737 + forkRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String()) 1738 if err != nil { 1739 log.Println("failed to get source repo", err) 1740 + span.RecordError(err) 1741 + span.SetAttributes(attribute.String("error", "source_repo_not_found")) 1742 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1743 return 1744 } 1745 1746 + span.SetAttributes( 1747 + attribute.String("fork.knot", forkRepo.Knot), 1748 + attribute.String("fork.did", forkRepo.Did), 1749 + attribute.String("fork.name", forkRepo.Name), 1750 + ) 1751 + 1752 // extract patch by performing compare 1753 ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1754 if err != nil { 1755 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1756 + span.RecordError(err) 1757 + span.SetAttributes(attribute.String("error", "client_creation_failed")) 1758 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1759 return 1760 } ··· 1762 secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot) 1763 if err != nil { 1764 log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err) 1765 + span.RecordError(err) 1766 + span.SetAttributes(attribute.String("error", "reg_key_not_found")) 1767 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1768 return 1769 } ··· 1772 signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1773 if err != nil { 1774 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1775 + span.RecordError(err) 1776 + span.SetAttributes(attribute.String("error", "signed_client_creation_failed")) 1777 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1778 return 1779 } ··· 1781 resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch) 1782 if err != nil || resp.StatusCode != http.StatusNoContent { 1783 log.Printf("failed to update tracking branch: %s", err) 1784 + span.RecordError(err) 1785 + span.SetAttributes(attribute.String("error", "hidden_ref_update_failed")) 1786 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1787 return 1788 } 1789 1790 hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1791 + span.SetAttributes(attribute.String("hidden_ref", hiddenRef)) 1792 + 1793 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1794 if err != nil { 1795 log.Printf("failed to compare branches: %s", err) 1796 + span.RecordError(err) 1797 + span.SetAttributes(attribute.String("error", "compare_failed")) 1798 s.pages.Notice(w, "resubmit-error", err.Error()) 1799 return 1800 } 1801 1802 sourceRev := comparison.Rev2 1803 patch := comparison.Patch 1804 + span.SetAttributes(attribute.String("source_rev", sourceRev)) 1805 1806 if err = validateResubmittedPatch(pull, patch); err != nil { 1807 + span.SetAttributes(attribute.String("error", "invalid_patch")) 1808 s.pages.Notice(w, "resubmit-error", err.Error()) 1809 return 1810 } 1811 1812 if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 1813 + span.SetAttributes(attribute.String("error", "no_changes")) 1814 s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 1815 return 1816 } 1817 1818 + tx, err := s.db.BeginTx(ctx, nil) 1819 if err != nil { 1820 log.Println("failed to start tx") 1821 + span.RecordError(err) 1822 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1823 return 1824 } ··· 1827 err = db.ResubmitPull(tx, pull, patch, sourceRev) 1828 if err != nil { 1829 log.Println("failed to create pull request", err) 1830 + span.RecordError(err) 1831 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1832 return 1833 } 1834 + client, _ := s.auth.AuthorizedClient(r.WithContext(ctx)) 1835 1836 + ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1837 if err != nil { 1838 // failed to get record 1839 + span.RecordError(err) 1840 + span.SetAttributes(attribute.String("error", "record_not_found")) 1841 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1842 return 1843 } ··· 1847 Branch: pull.PullSource.Branch, 1848 Repo: &repoAt, 1849 } 1850 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1851 Collection: tangled.RepoPullNSID, 1852 Repo: user.Did, 1853 Rkey: pull.Rkey, ··· 1865 }) 1866 if err != nil { 1867 log.Println("failed to update record", err) 1868 + span.RecordError(err) 1869 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1870 return 1871 } 1872 1873 if err = tx.Commit(); err != nil { 1874 log.Println("failed to commit transaction", err) 1875 + span.RecordError(err) 1876 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 1877 return 1878 } ··· 1899 } 1900 1901 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1902 + ctx, span := s.t.TraceStart(r.Context(), "MergePull") 1903 + defer span.End() 1904 + 1905 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1906 if err != nil { 1907 log.Println("failed to resolve repo:", err) 1908 + span.RecordError(err) 1909 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 1910 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1911 return 1912 } 1913 1914 + pull, ok := ctx.Value("pull").(*db.Pull) 1915 if !ok { 1916 log.Println("failed to get pull") 1917 + span.SetAttributes(attribute.String("error", "pull_not_in_context")) 1918 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1919 return 1920 } 1921 1922 + span.SetAttributes( 1923 + attribute.Int("pull.id", pull.PullId), 1924 + attribute.String("pull.owner", pull.OwnerDid), 1925 + attribute.String("target_branch", pull.TargetBranch), 1926 + ) 1927 + 1928 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1929 if err != nil { 1930 log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1931 + span.RecordError(err) 1932 + span.SetAttributes(attribute.String("error", "reg_key_not_found")) 1933 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1934 return 1935 } 1936 1937 + ident, err := s.resolver.ResolveIdent(ctx, pull.OwnerDid) 1938 if err != nil { 1939 log.Printf("resolving identity: %s", err) 1940 + span.RecordError(err) 1941 + span.SetAttributes(attribute.String("error", "resolve_identity_failed")) 1942 w.WriteHeader(http.StatusNotFound) 1943 return 1944 } ··· 1946 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid) 1947 if err != nil { 1948 log.Printf("failed to get primary email: %s", err) 1949 + span.RecordError(err) 1950 + span.SetAttributes(attribute.String("error", "get_email_failed")) 1951 } 1952 1953 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1954 if err != nil { 1955 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1956 + span.RecordError(err) 1957 + span.SetAttributes(attribute.String("error", "client_creation_failed")) 1958 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1959 return 1960 } ··· 1963 resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1964 if err != nil { 1965 log.Printf("failed to merge pull request: %s", err) 1966 + span.RecordError(err) 1967 + span.SetAttributes(attribute.String("error", "merge_failed")) 1968 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1969 return 1970 } 1971 1972 + span.SetAttributes(attribute.Int("response.status", resp.StatusCode)) 1973 + 1974 if resp.StatusCode == http.StatusOK { 1975 err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1976 if err != nil { 1977 log.Printf("failed to update pull request status in database: %s", err) 1978 + span.RecordError(err) 1979 + span.SetAttributes(attribute.String("error", "db_update_failed")) 1980 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1981 return 1982 } 1983 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1984 } else { 1985 log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1986 + span.SetAttributes(attribute.String("error", "non_ok_response")) 1987 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1988 } 1989 } 1990 1991 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1992 + ctx, span := s.t.TraceStart(r.Context(), "ClosePull") 1993 + defer span.End() 1994 1995 + user := s.auth.GetUser(r.WithContext(ctx)) 1996 + 1997 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1998 if err != nil { 1999 log.Println("malformed middleware") 2000 + span.RecordError(err) 2001 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 2002 return 2003 } 2004 2005 + pull, ok := ctx.Value("pull").(*db.Pull) 2006 if !ok { 2007 log.Println("failed to get pull") 2008 + span.SetAttributes(attribute.String("error", "pull_not_in_context")) 2009 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2010 return 2011 } 2012 2013 + span.SetAttributes( 2014 + attribute.Int("pull.id", pull.PullId), 2015 + attribute.String("pull.owner", pull.OwnerDid), 2016 + attribute.String("user.did", user.Did), 2017 + ) 2018 + 2019 // auth filter: only owner or collaborators can close 2020 roles := RolesInRepo(s, user, f) 2021 isCollaborator := roles.IsCollaborator() 2022 isPullAuthor := user.Did == pull.OwnerDid 2023 isCloseAllowed := isCollaborator || isPullAuthor 2024 + 2025 + span.SetAttributes( 2026 + attribute.Bool("is_collaborator", isCollaborator), 2027 + attribute.Bool("is_pull_author", isPullAuthor), 2028 + attribute.Bool("is_close_allowed", isCloseAllowed), 2029 + ) 2030 + 2031 if !isCloseAllowed { 2032 log.Println("failed to close pull") 2033 + span.SetAttributes(attribute.String("error", "unauthorized")) 2034 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 2035 return 2036 } 2037 2038 // Start a transaction 2039 + tx, err := s.db.BeginTx(ctx, nil) 2040 if err != nil { 2041 log.Println("failed to start transaction", err) 2042 + span.RecordError(err) 2043 + span.SetAttributes(attribute.String("error", "transaction_start_failed")) 2044 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2045 return 2046 } ··· 2049 err = db.ClosePull(tx, f.RepoAt, pull.PullId) 2050 if err != nil { 2051 log.Println("failed to close pull", err) 2052 + span.RecordError(err) 2053 + span.SetAttributes(attribute.String("error", "db_close_failed")) 2054 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2055 return 2056 } ··· 2058 // Commit the transaction 2059 if err = tx.Commit(); err != nil { 2060 log.Println("failed to commit transaction", err) 2061 + span.RecordError(err) 2062 + span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 2063 s.pages.Notice(w, "pull-close", "Failed to close pull.") 2064 return 2065 } ··· 2069 } 2070 2071 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 2072 + ctx, span := s.t.TraceStart(r.Context(), "ReopenPull") 2073 + defer span.End() 2074 2075 + user := s.auth.GetUser(r.WithContext(ctx)) 2076 + 2077 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2078 if err != nil { 2079 log.Println("failed to resolve repo", err) 2080 + span.RecordError(err) 2081 + span.SetAttributes(attribute.String("error", "resolve_repo_failed")) 2082 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2083 return 2084 } 2085 2086 + pull, ok := ctx.Value("pull").(*db.Pull) 2087 if !ok { 2088 log.Println("failed to get pull") 2089 + span.SetAttributes(attribute.String("error", "pull_not_in_context")) 2090 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 2091 return 2092 } 2093 2094 + span.SetAttributes( 2095 + attribute.Int("pull.id", pull.PullId), 2096 + attribute.String("pull.owner", pull.OwnerDid), 2097 + attribute.String("user.did", user.Did), 2098 + ) 2099 + 2100 + // auth filter: only owner or collaborators can reopen 2101 roles := RolesInRepo(s, user, f) 2102 isCollaborator := roles.IsCollaborator() 2103 isPullAuthor := user.Did == pull.OwnerDid 2104 + isReopenAllowed := isCollaborator || isPullAuthor 2105 + 2106 + span.SetAttributes( 2107 + attribute.Bool("is_collaborator", isCollaborator), 2108 + attribute.Bool("is_pull_author", isPullAuthor), 2109 + attribute.Bool("is_reopen_allowed", isReopenAllowed), 2110 + ) 2111 + 2112 + if !isReopenAllowed { 2113 + log.Println("failed to reopen pull") 2114 + span.SetAttributes(attribute.String("error", "unauthorized")) 2115 + s.pages.Notice(w, "pull-close", "You are unauthorized to reopen this pull.") 2116 return 2117 } 2118 2119 // Start a transaction 2120 + tx, err := s.db.BeginTx(ctx, nil) 2121 if err != nil { 2122 log.Println("failed to start transaction", err) 2123 + span.RecordError(err) 2124 + span.SetAttributes(attribute.String("error", "transaction_start_failed")) 2125 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2126 return 2127 } ··· 2130 err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 2131 if err != nil { 2132 log.Println("failed to reopen pull", err) 2133 + span.RecordError(err) 2134 + span.SetAttributes(attribute.String("error", "db_reopen_failed")) 2135 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2136 return 2137 } ··· 2139 // Commit the transaction 2140 if err = tx.Commit(); err != nil { 2141 log.Println("failed to commit transaction", err) 2142 + span.RecordError(err) 2143 + span.SetAttributes(attribute.String("error", "transaction_commit_failed")) 2144 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 2145 return 2146 }
+699 -95
appview/state/repo.go
··· 16 "strings" 17 "time" 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview" 21 "tangled.sh/tangled.sh/core/appview/auth" ··· 38 ) 39 40 func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 41 ref := chi.URLParam(r, "ref") 42 - f, err := s.fullyResolvedRepo(r) 43 if err != nil { 44 log.Println("failed to fully resolve repo", err) 45 return 46 } 47 48 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 49 if err != nil { 50 log.Printf("failed to create unsigned client for %s", f.Knot) 51 s.pages.Error503(w) 52 return 53 } ··· 56 if err != nil { 57 s.pages.Error503(w) 58 log.Println("failed to reach knotserver", err) 59 return 60 } 61 defer resp.Body.Close() ··· 63 body, err := io.ReadAll(resp.Body) 64 if err != nil { 65 log.Printf("Error reading response body: %v", err) 66 return 67 } 68 ··· 70 err = json.Unmarshal(body, &result) 71 if err != nil { 72 log.Printf("Error unmarshalling response body: %v", err) 73 return 74 } 75 ··· 112 tagCount := len(result.Tags) 113 fileCount := len(result.Files) 114 115 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 116 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 117 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] ··· 122 user := s.auth.GetUser(r) 123 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 124 LoggedInUser: user, 125 - RepoInfo: f.RepoInfo(s, user), 126 TagMap: tagMap, 127 RepoIndexResponse: result, 128 CommitsTrunc: commitsTrunc, ··· 134 } 135 136 func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 137 - f, err := s.fullyResolvedRepo(r) 138 if err != nil { 139 log.Println("failed to fully resolve repo", err) 140 return 141 } 142 ··· 149 } 150 151 ref := chi.URLParam(r, "ref") 152 153 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 154 if err != nil { 155 log.Println("failed to create unsigned client", err) 156 return 157 } 158 159 resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 160 if err != nil { 161 log.Println("failed to reach knotserver", err) 162 return 163 } 164 165 body, err := io.ReadAll(resp.Body) 166 if err != nil { 167 log.Printf("error reading response body: %v", err) 168 return 169 } 170 ··· 172 err = json.Unmarshal(body, &repolog) 173 if err != nil { 174 log.Println("failed to parse json response", err) 175 return 176 } 177 178 result, err := us.Tags(f.OwnerDid(), f.RepoName) 179 if err != nil { 180 log.Println("failed to reach knotserver", err) 181 return 182 } 183 ··· 190 tagMap[hash] = append(tagMap[hash], tag.Name) 191 } 192 193 user := s.auth.GetUser(r) 194 s.pages.RepoLog(w, pages.RepoLogParams{ 195 LoggedInUser: user, 196 TagMap: tagMap, 197 - RepoInfo: f.RepoInfo(s, user), 198 RepoLogResponse: repolog, 199 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 200 }) ··· 202 } 203 204 func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 205 - f, err := s.fullyResolvedRepo(r) 206 if err != nil { 207 log.Println("failed to get repo and knot", err) 208 w.WriteHeader(http.StatusBadRequest) ··· 211 212 user := s.auth.GetUser(r) 213 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 214 - RepoInfo: f.RepoInfo(s, user), 215 }) 216 return 217 } 218 219 func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 220 - f, err := s.fullyResolvedRepo(r) 221 if err != nil { 222 log.Println("failed to get repo and knot", err) 223 w.WriteHeader(http.StatusBadRequest) 224 return 225 } ··· 228 rkey := repoAt.RecordKey().String() 229 if rkey == "" { 230 log.Println("invalid aturi for repo", err) 231 w.WriteHeader(http.StatusInternalServerError) 232 return 233 } 234 235 user := s.auth.GetUser(r) 236 237 switch r.Method { 238 case http.MethodGet: 239 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 240 - RepoInfo: f.RepoInfo(s, user), 241 }) 242 return 243 case http.MethodPut: 244 user := s.auth.GetUser(r) 245 newDescription := r.FormValue("description") 246 client, _ := s.auth.AuthorizedClient(r) 247 248 // optimistic update 249 - err = db.UpdateDescription(s.db, string(repoAt), newDescription) 250 if err != nil { 251 - log.Println("failed to perferom update-description query", err) 252 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 253 return 254 } ··· 256 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 257 // 258 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 259 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) 260 if err != nil { 261 // failed to get record 262 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 263 return 264 } 265 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 266 Collection: tangled.RepoNSID, 267 Repo: user.Did, 268 Rkey: rkey, ··· 279 }) 280 281 if err != nil { 282 - log.Println("failed to perferom update-description query", err) 283 // failed to get record 284 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 285 return 286 } 287 288 - newRepoInfo := f.RepoInfo(s, user) 289 newRepoInfo.Description = newDescription 290 291 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ ··· 296 } 297 298 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 299 - f, err := s.fullyResolvedRepo(r) 300 if err != nil { 301 log.Println("failed to fully resolve repo", err) 302 return 303 } 304 ref := chi.URLParam(r, "ref") ··· 307 protocol = "https" 308 } 309 310 if !plumbing.IsHash(ref) { 311 s.pages.Error404(w) 312 return 313 } 314 315 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 316 if err != nil { 317 log.Println("failed to reach knotserver", err) 318 return 319 } 320 321 body, err := io.ReadAll(resp.Body) 322 if err != nil { 323 log.Printf("Error reading response body: %v", err) 324 return 325 } 326 ··· 328 err = json.Unmarshal(body, &result) 329 if err != nil { 330 log.Println("failed to parse response:", err) 331 return 332 } 333 334 user := s.auth.GetUser(r) 335 s.pages.RepoCommit(w, pages.RepoCommitParams{ 336 LoggedInUser: user, 337 - RepoInfo: f.RepoInfo(s, user), 338 RepoCommitResponse: result, 339 EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 340 }) ··· 342 } 343 344 func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 345 - f, err := s.fullyResolvedRepo(r) 346 if err != nil { 347 log.Println("failed to fully resolve repo", err) 348 return 349 } 350 ··· 354 if !s.config.Dev { 355 protocol = "https" 356 } 357 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 358 if err != nil { 359 log.Println("failed to reach knotserver", err) 360 return 361 } 362 363 body, err := io.ReadAll(resp.Body) 364 if err != nil { 365 log.Printf("Error reading response body: %v", err) 366 return 367 } 368 ··· 370 err = json.Unmarshal(body, &result) 371 if err != nil { 372 log.Println("failed to parse response:", err) 373 return 374 } 375 376 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 377 // so we can safely redirect to the "parent" (which is the same file). 378 if len(result.Files) == 0 && result.Parent == treePath { 379 - http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound) 380 return 381 } 382 ··· 398 BreadCrumbs: breadcrumbs, 399 BaseTreeLink: baseTreeLink, 400 BaseBlobLink: baseBlobLink, 401 - RepoInfo: f.RepoInfo(s, user), 402 RepoTreeResponse: result, 403 }) 404 return 405 } 406 407 func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 408 - f, err := s.fullyResolvedRepo(r) 409 if err != nil { 410 log.Println("failed to get repo and knot", err) 411 return 412 } 413 414 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 415 if err != nil { 416 log.Println("failed to create unsigned client", err) 417 return 418 } 419 420 result, err := us.Tags(f.OwnerDid(), f.RepoName) 421 if err != nil { 422 log.Println("failed to reach knotserver", err) 423 return 424 } 425 426 artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt)) 427 if err != nil { 428 log.Println("failed grab artifacts", err) 429 return 430 } 431 432 // convert artifacts to map for easy UI building 433 artifactMap := make(map[plumbing.Hash][]db.Artifact) ··· 451 } 452 } 453 454 user := s.auth.GetUser(r) 455 s.pages.RepoTags(w, pages.RepoTagsParams{ 456 LoggedInUser: user, 457 - RepoInfo: f.RepoInfo(s, user), 458 RepoTagsResponse: *result, 459 ArtifactMap: artifactMap, 460 DanglingArtifacts: danglingArtifacts, ··· 463 } 464 465 func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 466 - f, err := s.fullyResolvedRepo(r) 467 if err != nil { 468 log.Println("failed to get repo and knot", err) 469 return 470 } 471 472 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 473 if err != nil { 474 log.Println("failed to create unsigned client", err) 475 return 476 } 477 478 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 479 if err != nil { 480 log.Println("failed to reach knotserver", err) 481 return 482 } 483 484 body, err := io.ReadAll(resp.Body) 485 if err != nil { 486 log.Printf("Error reading response body: %v", err) 487 return 488 } 489 ··· 491 err = json.Unmarshal(body, &result) 492 if err != nil { 493 log.Println("failed to parse response:", err) 494 return 495 } 496 497 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 498 if a.IsDefault { ··· 514 user := s.auth.GetUser(r) 515 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 516 LoggedInUser: user, 517 - RepoInfo: f.RepoInfo(s, user), 518 RepoBranchesResponse: result, 519 }) 520 return 521 } 522 523 func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 524 - f, err := s.fullyResolvedRepo(r) 525 if err != nil { 526 log.Println("failed to get repo and knot", err) 527 return 528 } 529 ··· 533 if !s.config.Dev { 534 protocol = "https" 535 } 536 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 537 if err != nil { 538 log.Println("failed to reach knotserver", err) 539 return 540 } 541 542 body, err := io.ReadAll(resp.Body) 543 if err != nil { 544 log.Printf("Error reading response body: %v", err) 545 return 546 } 547 ··· 549 err = json.Unmarshal(body, &result) 550 if err != nil { 551 log.Println("failed to parse response:", err) 552 return 553 } 554 ··· 568 showRendered = r.URL.Query().Get("code") != "true" 569 } 570 571 user := s.auth.GetUser(r) 572 s.pages.RepoBlob(w, pages.RepoBlobParams{ 573 LoggedInUser: user, 574 - RepoInfo: f.RepoInfo(s, user), 575 RepoBlobResponse: result, 576 BreadCrumbs: breadcrumbs, 577 ShowRendered: showRendered, ··· 581 } 582 583 func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 584 - f, err := s.fullyResolvedRepo(r) 585 if err != nil { 586 log.Println("failed to get repo and knot", err) 587 return 588 } 589 ··· 594 if !s.config.Dev { 595 protocol = "https" 596 } 597 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 598 if err != nil { 599 log.Println("failed to reach knotserver", err) 600 return 601 } 602 603 body, err := io.ReadAll(resp.Body) 604 if err != nil { 605 log.Printf("Error reading response body: %v", err) 606 return 607 } 608 ··· 610 err = json.Unmarshal(body, &result) 611 if err != nil { 612 log.Println("failed to parse response:", err) 613 return 614 } 615 616 if result.IsBinary { 617 w.Header().Set("Content-Type", "application/octet-stream") ··· 625 } 626 627 func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 628 - f, err := s.fullyResolvedRepo(r) 629 if err != nil { 630 log.Println("failed to get repo and knot", err) 631 return 632 } 633 634 collaborator := r.FormValue("collaborator") 635 if collaborator == "" { 636 http.Error(w, "malformed form", http.StatusBadRequest) 637 return 638 } 639 640 - collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator) 641 if err != nil { 642 w.Write([]byte("failed to resolve collaborator did to a handle")) 643 return 644 } 645 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 646 647 // TODO: create an atproto record for this 648 649 secret, err := db.GetRegistrationKey(s.db, f.Knot) 650 if err != nil { 651 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 652 return 653 } 654 655 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 656 if err != nil { 657 log.Println("failed to create client to ", f.Knot) 658 return 659 } 660 661 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 662 if err != nil { 663 log.Printf("failed to make request to %s: %s", f.Knot, err) 664 return 665 } 666 667 if ksResp.StatusCode != http.StatusNoContent { 668 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 669 return 670 } 671 672 - tx, err := s.db.BeginTx(r.Context(), nil) 673 if err != nil { 674 log.Println("failed to start tx") 675 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 676 return 677 } ··· 685 686 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 687 if err != nil { 688 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 689 return 690 } 691 692 - err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 693 if err != nil { 694 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 695 return 696 } ··· 698 err = tx.Commit() 699 if err != nil { 700 log.Println("failed to commit changes", err) 701 http.Error(w, err.Error(), http.StatusInternalServerError) 702 return 703 } ··· 705 err = s.enforcer.E.SavePolicy() 706 if err != nil { 707 log.Println("failed to update ACLs", err) 708 http.Error(w, err.Error(), http.StatusInternalServerError) 709 return 710 } 711 712 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 713 - 714 } 715 716 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 717 user := s.auth.GetUser(r) 718 719 - f, err := s.fullyResolvedRepo(r) 720 if err != nil { 721 log.Println("failed to get repo and knot", err) 722 return 723 } 724 725 // remove record from pds 726 xrpcClient, _ := s.auth.AuthorizedClient(r) 727 repoRkey := f.RepoAt.RecordKey().String() 728 - _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ 729 Collection: tangled.RepoNSID, 730 Repo: user.Did, 731 Rkey: repoRkey, 732 }) 733 if err != nil { 734 log.Printf("failed to delete record: %s", err) 735 s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 736 return 737 } 738 log.Println("removed repo record ", f.RepoAt.String()) 739 740 secret, err := db.GetRegistrationKey(s.db, f.Knot) 741 if err != nil { 742 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 743 return 744 } 745 746 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 747 if err != nil { 748 log.Println("failed to create client to ", f.Knot) 749 return 750 } 751 752 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 753 if err != nil { 754 log.Printf("failed to make request to %s: %s", f.Knot, err) 755 return 756 } 757 758 if ksResp.StatusCode != http.StatusNoContent { 759 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 760 } else { 761 log.Println("removed repo from knot ", f.Knot) 762 } 763 764 - tx, err := s.db.BeginTx(r.Context(), nil) 765 if err != nil { 766 log.Println("failed to start tx") 767 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 768 return 769 } ··· 772 err = s.enforcer.E.LoadPolicy() 773 if err != nil { 774 log.Println("failed to rollback policies") 775 } 776 }() 777 778 // remove collaborator RBAC 779 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 780 if err != nil { 781 s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 782 return 783 } 784 for _, c := range repoCollaborators { 785 did := c[0] 786 s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) ··· 790 // remove repo RBAC 791 err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 792 if err != nil { 793 s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 794 return 795 } 796 797 // remove repo from db 798 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 799 if err != nil { 800 s.pages.Notice(w, "settings-delete", "Failed to update appview") 801 return 802 } ··· 805 err = tx.Commit() 806 if err != nil { 807 log.Println("failed to commit changes", err) 808 http.Error(w, err.Error(), http.StatusInternalServerError) 809 return 810 } ··· 812 err = s.enforcer.E.SavePolicy() 813 if err != nil { 814 log.Println("failed to update ACLs", err) 815 http.Error(w, err.Error(), http.StatusInternalServerError) 816 return 817 } ··· 820 } 821 822 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 823 - f, err := s.fullyResolvedRepo(r) 824 if err != nil { 825 log.Println("failed to get repo and knot", err) 826 return 827 } 828 829 branch := r.FormValue("branch") 830 if branch == "" { 831 http.Error(w, "malformed form", http.StatusBadRequest) 832 return 833 } 834 835 secret, err := db.GetRegistrationKey(s.db, f.Knot) 836 if err != nil { 837 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 838 return 839 } 840 841 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 842 if err != nil { 843 log.Println("failed to create client to ", f.Knot) 844 return 845 } 846 847 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 848 if err != nil { 849 log.Printf("failed to make request to %s: %s", f.Knot, err) 850 return 851 } 852 853 if ksResp.StatusCode != http.StatusNoContent { 854 s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 855 return 856 } ··· 859 } 860 861 func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 862 - f, err := s.fullyResolvedRepo(r) 863 if err != nil { 864 log.Println("failed to get repo and knot", err) 865 return 866 } 867 868 switch r.Method { 869 case http.MethodGet: 870 // for now, this is just pubkeys 871 user := s.auth.GetUser(r) 872 - repoCollaborators, err := f.Collaborators(r.Context(), s) 873 if err != nil { 874 log.Println("failed to get collaborators", err) 875 } 876 877 isCollaboratorInviteAllowed := false 878 if user != nil { ··· 881 isCollaboratorInviteAllowed = true 882 } 883 } 884 885 var branchNames []string 886 var defaultBranch string 887 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 888 if err != nil { 889 log.Println("failed to create unsigned client", err) 890 } else { 891 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 892 if err != nil { 893 log.Println("failed to reach knotserver", err) 894 } else { 895 defer resp.Body.Close() 896 897 body, err := io.ReadAll(resp.Body) 898 if err != nil { 899 log.Printf("Error reading response body: %v", err) 900 } else { 901 var result types.RepoBranchesResponse 902 err = json.Unmarshal(body, &result) 903 if err != nil { 904 log.Println("failed to parse response:", err) 905 } else { 906 for _, branch := range result.Branches { 907 branchNames = append(branchNames, branch.Name) 908 } 909 } 910 } 911 } ··· 913 defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName) 914 if err != nil { 915 log.Println("failed to reach knotserver", err) 916 } else { 917 defaultBranch = defaultBranchResp.Branch 918 } 919 } 920 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 921 LoggedInUser: user, 922 - RepoInfo: f.RepoInfo(s, user), 923 Collaborators: repoCollaborators, 924 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 925 Branches: branchNames, ··· 1008 return collaborators, nil 1009 } 1010 1011 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { 1012 isStarred := false 1013 if u != nil { 1014 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 1015 } 1016 1017 starCount, err := db.GetStarCount(s.db, f.RepoAt) 1018 if err != nil { 1019 log.Println("failed to get star count for ", f.RepoAt) 1020 } 1021 issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 1022 if err != nil { 1023 log.Println("failed to get issue count for ", f.RepoAt) 1024 } 1025 pullCount, err := db.GetPullCount(s.db, f.RepoAt) 1026 if err != nil { 1027 log.Println("failed to get issue count for ", f.RepoAt) 1028 } 1029 - source, err := db.GetRepoSource(s.db, f.RepoAt) 1030 if errors.Is(err, sql.ErrNoRows) { 1031 source = "" 1032 } else if err != nil { 1033 log.Println("failed to get repo source for ", f.RepoAt, err) 1034 } 1035 1036 var sourceRepo *db.Repo 1037 if source != "" { 1038 - sourceRepo, err = db.GetRepoByAtUri(s.db, source) 1039 if err != nil { 1040 log.Println("failed to get repo by at uri", err) 1041 } 1042 } 1043 1044 var sourceHandle *identity.Identity 1045 if sourceRepo != nil { 1046 - sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did) 1047 if err != nil { 1048 log.Println("failed to resolve source repo", err) 1049 } 1050 } 1051 1052 knot := f.Knot 1053 var disableFork bool 1054 us, err := NewUnsignedClient(knot, s.config.Dev) 1055 if err != nil { 1056 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1057 } else { 1058 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 1059 if err != nil { 1060 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 1061 } else { 1062 defer resp.Body.Close() 1063 body, err := io.ReadAll(resp.Body) 1064 if err != nil { 1065 log.Printf("error reading branch response body: %v", err) 1066 } else { 1067 var branchesResp types.RepoBranchesResponse 1068 if err := json.Unmarshal(body, &branchesResp); err != nil { 1069 log.Printf("error parsing branch response: %v", err) 1070 } else { 1071 disableFork = false 1072 } ··· 1074 if len(branchesResp.Branches) == 0 { 1075 disableFork = true 1076 } 1077 } 1078 } 1079 } ··· 1105 } 1106 1107 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1108 user := s.auth.GetUser(r) 1109 - f, err := s.fullyResolvedRepo(r) 1110 if err != nil { 1111 log.Println("failed to get repo and knot", err) 1112 return 1113 } 1114 ··· 1117 if err != nil { 1118 http.Error(w, "bad issue id", http.StatusBadRequest) 1119 log.Println("failed to parse issue id", err) 1120 return 1121 } 1122 1123 - issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 1124 if err != nil { 1125 log.Println("failed to get issue and comments", err) 1126 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1127 return 1128 } 1129 1130 - issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 1131 if err != nil { 1132 log.Println("failed to resolve issue owner", err) 1133 } 1134 1135 identsToResolve := make([]string, len(comments)) 1136 for i, comment := range comments { 1137 identsToResolve[i] = comment.OwnerDid 1138 } 1139 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1140 didHandleMap := make(map[string]string) 1141 for _, identity := range resolvedIds { 1142 if !identity.Handle.IsInvalidHandle() { ··· 1148 1149 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1150 LoggedInUser: user, 1151 - RepoInfo: f.RepoInfo(s, user), 1152 Issue: *issue, 1153 Comments: comments, 1154 1155 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 1156 DidHandleMap: didHandleMap, 1157 }) 1158 - 1159 } 1160 1161 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1162 user := s.auth.GetUser(r) 1163 - f, err := s.fullyResolvedRepo(r) 1164 if err != nil { 1165 log.Println("failed to get repo and knot", err) 1166 return 1167 } 1168 ··· 1171 if err != nil { 1172 http.Error(w, "bad issue id", http.StatusBadRequest) 1173 log.Println("failed to parse issue id", err) 1174 return 1175 } 1176 1177 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1178 if err != nil { 1179 log.Println("failed to get issue", err) 1180 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1181 return 1182 } 1183 1184 - collaborators, err := f.Collaborators(r.Context(), s) 1185 if err != nil { 1186 log.Println("failed to fetch repo collaborators: %w", err) 1187 } 1188 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1189 return user.Did == collab.Did 1190 }) 1191 isIssueOwner := user.Did == issue.OwnerDid 1192 1193 // TODO: make this more granular 1194 if isIssueOwner || isCollaborator { 1195 - 1196 closed := tangled.RepoIssueStateClosed 1197 1198 client, _ := s.auth.AuthorizedClient(r) 1199 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1200 Collection: tangled.RepoIssueStateNSID, 1201 Repo: user.Did, 1202 Rkey: appview.TID(), ··· 1210 1211 if err != nil { 1212 log.Println("failed to update issue state", err) 1213 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1214 return 1215 } ··· 1217 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1218 if err != nil { 1219 log.Println("failed to close issue", err) 1220 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1221 return 1222 } ··· 1225 return 1226 } else { 1227 log.Println("user is not permitted to close issue") 1228 http.Error(w, "for biden", http.StatusUnauthorized) 1229 return 1230 } 1231 } 1232 1233 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1234 user := s.auth.GetUser(r) 1235 - f, err := s.fullyResolvedRepo(r) 1236 if err != nil { 1237 log.Println("failed to get repo and knot", err) 1238 return 1239 } 1240 ··· 1243 if err != nil { 1244 http.Error(w, "bad issue id", http.StatusBadRequest) 1245 log.Println("failed to parse issue id", err) 1246 return 1247 } 1248 1249 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1250 if err != nil { 1251 log.Println("failed to get issue", err) 1252 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1253 return 1254 } 1255 1256 - collaborators, err := f.Collaborators(r.Context(), s) 1257 if err != nil { 1258 log.Println("failed to fetch repo collaborators: %w", err) 1259 } 1260 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1261 return user.Did == collab.Did 1262 }) 1263 isIssueOwner := user.Did == issue.OwnerDid 1264 1265 if isCollaborator || isIssueOwner { 1266 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1267 if err != nil { 1268 log.Println("failed to reopen issue", err) 1269 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1270 return 1271 } ··· 1273 return 1274 } else { 1275 log.Println("user is not the owner of the repo") 1276 http.Error(w, "forbidden", http.StatusUnauthorized) 1277 return 1278 } 1279 } 1280 1281 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1282 user := s.auth.GetUser(r) 1283 - f, err := s.fullyResolvedRepo(r) 1284 if err != nil { 1285 log.Println("failed to get repo and knot", err) 1286 return 1287 } 1288 ··· 1291 if err != nil { 1292 http.Error(w, "bad issue id", http.StatusBadRequest) 1293 log.Println("failed to parse issue id", err) 1294 return 1295 } 1296 1297 switch r.Method { 1298 case http.MethodPost: 1299 body := r.FormValue("body") 1300 if body == "" { 1301 s.pages.Notice(w, "issue", "Body is required") 1302 return 1303 } ··· 1305 commentId := mathrand.IntN(1000000) 1306 rkey := appview.TID() 1307 1308 err := db.NewIssueComment(s.db, &db.Comment{ 1309 OwnerDid: user.Did, 1310 RepoAt: f.RepoAt, ··· 1315 }) 1316 if err != nil { 1317 log.Println("failed to create comment", err) 1318 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1319 return 1320 } ··· 1325 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1326 if err != nil { 1327 log.Println("failed to get issue at", err) 1328 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1329 return 1330 } 1331 1332 atUri := f.RepoAt.String() 1333 client, _ := s.auth.AuthorizedClient(r) 1334 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1335 Collection: tangled.RepoIssueCommentNSID, 1336 Repo: user.Did, 1337 Rkey: rkey, ··· 1348 }) 1349 if err != nil { 1350 log.Println("failed to create comment", err) 1351 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1352 return 1353 } ··· 1358 } 1359 1360 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1361 user := s.auth.GetUser(r) 1362 - f, err := s.fullyResolvedRepo(r) 1363 if err != nil { 1364 log.Println("failed to get repo and knot", err) 1365 return 1366 } 1367 ··· 1370 if err != nil { 1371 http.Error(w, "bad issue id", http.StatusBadRequest) 1372 log.Println("failed to parse issue id", err) 1373 return 1374 } 1375 ··· 1378 if err != nil { 1379 http.Error(w, "bad comment id", http.StatusBadRequest) 1380 log.Println("failed to parse issue id", err) 1381 return 1382 } 1383 1384 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1385 if err != nil { 1386 log.Println("failed to get issue", err) 1387 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1388 return 1389 } ··· 1391 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1392 if err != nil { 1393 http.Error(w, "bad comment id", http.StatusBadRequest) 1394 return 1395 } 1396 1397 - identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid) 1398 if err != nil { 1399 log.Println("failed to resolve did") 1400 return 1401 } 1402 ··· 1409 1410 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1411 LoggedInUser: user, 1412 - RepoInfo: f.RepoInfo(s, user), 1413 DidHandleMap: didHandleMap, 1414 Issue: issue, 1415 Comment: comment, ··· 1417 } 1418 1419 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1420 user := s.auth.GetUser(r) 1421 - f, err := s.fullyResolvedRepo(r) 1422 if err != nil { 1423 log.Println("failed to get repo and knot", err) 1424 return 1425 } 1426 ··· 1429 if err != nil { 1430 http.Error(w, "bad issue id", http.StatusBadRequest) 1431 log.Println("failed to parse issue id", err) 1432 return 1433 } 1434 ··· 1437 if err != nil { 1438 http.Error(w, "bad comment id", http.StatusBadRequest) 1439 log.Println("failed to parse issue id", err) 1440 return 1441 } 1442 1443 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1444 if err != nil { 1445 log.Println("failed to get issue", err) 1446 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1447 return 1448 } ··· 1450 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1451 if err != nil { 1452 http.Error(w, "bad comment id", http.StatusBadRequest) 1453 return 1454 } 1455 1456 if comment.OwnerDid != user.Did { 1457 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1458 return 1459 } 1460 ··· 1462 case http.MethodGet: 1463 s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1464 LoggedInUser: user, 1465 - RepoInfo: f.RepoInfo(s, user), 1466 Issue: issue, 1467 Comment: comment, 1468 }) ··· 1472 client, _ := s.auth.AuthorizedClient(r) 1473 rkey := comment.Rkey 1474 1475 // optimistic update 1476 edited := time.Now() 1477 err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1478 if err != nil { 1479 log.Println("failed to perferom update-description query", err) 1480 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1481 return 1482 } ··· 1484 // rkey is optional, it was introduced later 1485 if comment.Rkey != "" { 1486 // update the record on pds 1487 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1488 if err != nil { 1489 // failed to get record 1490 log.Println(err, rkey) 1491 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1492 return 1493 } ··· 1499 createdAt := record["createdAt"].(string) 1500 commentIdInt64 := int64(commentIdInt) 1501 1502 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1503 Collection: tangled.RepoIssueCommentNSID, 1504 Repo: user.Did, 1505 Rkey: rkey, ··· 1517 }) 1518 if err != nil { 1519 log.Println(err) 1520 } 1521 } 1522 ··· 1530 // return new comment body with htmx 1531 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1532 LoggedInUser: user, 1533 - RepoInfo: f.RepoInfo(s, user), 1534 DidHandleMap: didHandleMap, 1535 Issue: issue, 1536 Comment: comment, 1537 }) 1538 return 1539 - 1540 } 1541 - 1542 } 1543 1544 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1545 user := s.auth.GetUser(r) 1546 - f, err := s.fullyResolvedRepo(r) 1547 if err != nil { 1548 log.Println("failed to get repo and knot", err) 1549 return 1550 } 1551 ··· 1554 if err != nil { 1555 http.Error(w, "bad issue id", http.StatusBadRequest) 1556 log.Println("failed to parse issue id", err) 1557 return 1558 } 1559 1560 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1561 if err != nil { 1562 log.Println("failed to get issue", err) 1563 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1564 return 1565 } ··· 1569 if err != nil { 1570 http.Error(w, "bad comment id", http.StatusBadRequest) 1571 log.Println("failed to parse issue id", err) 1572 return 1573 } 1574 1575 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1576 if err != nil { 1577 http.Error(w, "bad comment id", http.StatusBadRequest) 1578 return 1579 } 1580 1581 if comment.OwnerDid != user.Did { 1582 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1583 return 1584 } 1585 1586 if comment.Deleted != nil { 1587 http.Error(w, "comment already deleted", http.StatusBadRequest) 1588 return 1589 } 1590 ··· 1593 err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1594 if err != nil { 1595 log.Println("failed to delete comment") 1596 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 1597 return 1598 } ··· 1600 // delete from pds 1601 if comment.Rkey != "" { 1602 client, _ := s.auth.AuthorizedClient(r) 1603 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 1604 Collection: tangled.GraphFollowNSID, 1605 Repo: user.Did, 1606 Rkey: comment.Rkey, 1607 }) 1608 if err != nil { 1609 log.Println(err) 1610 } 1611 } 1612 ··· 1620 // htmx fragment of comment after deletion 1621 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1622 LoggedInUser: user, 1623 - RepoInfo: f.RepoInfo(s, user), 1624 DidHandleMap: didHandleMap, 1625 Issue: issue, 1626 Comment: comment, ··· 1629 } 1630 1631 func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1632 params := r.URL.Query() 1633 state := params.Get("state") 1634 isOpen := true ··· 1641 isOpen = true 1642 } 1643 1644 page, ok := r.Context().Value("page").(pagination.Page) 1645 if !ok { 1646 log.Println("failed to get page") 1647 page = pagination.FirstPage() 1648 } 1649 1650 user := s.auth.GetUser(r) 1651 - f, err := s.fullyResolvedRepo(r) 1652 if err != nil { 1653 log.Println("failed to get repo and knot", err) 1654 return 1655 } 1656 1657 - issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1658 if err != nil { 1659 log.Println("failed to get issues", err) 1660 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1661 return 1662 } 1663 1664 identsToResolve := make([]string, len(issues)) 1665 for i, issue := range issues { 1666 identsToResolve[i] = issue.OwnerDid 1667 } 1668 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1669 didHandleMap := make(map[string]string) 1670 for _, identity := range resolvedIds { 1671 if !identity.Handle.IsInvalidHandle() { ··· 1677 1678 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1679 LoggedInUser: s.auth.GetUser(r), 1680 - RepoInfo: f.RepoInfo(s, user), 1681 Issues: issues, 1682 DidHandleMap: didHandleMap, 1683 FilteringByOpen: isOpen, ··· 1687 } 1688 1689 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1690 user := s.auth.GetUser(r) 1691 1692 - f, err := s.fullyResolvedRepo(r) 1693 if err != nil { 1694 log.Println("failed to get repo and knot", err) 1695 return 1696 } 1697 1698 switch r.Method { 1699 case http.MethodGet: 1700 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1701 LoggedInUser: user, 1702 - RepoInfo: f.RepoInfo(s, user), 1703 }) 1704 case http.MethodPost: 1705 title := r.FormValue("title") 1706 body := r.FormValue("body") 1707 1708 if title == "" || body == "" { 1709 s.pages.Notice(w, "issues", "Title and body are required") 1710 return 1711 } 1712 1713 - tx, err := s.db.BeginTx(r.Context(), nil) 1714 if err != nil { 1715 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1716 return 1717 } ··· 1724 }) 1725 if err != nil { 1726 log.Println("failed to create issue", err) 1727 s.pages.Notice(w, "issues", "Failed to create issue.") 1728 return 1729 } ··· 1731 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1732 if err != nil { 1733 log.Println("failed to get issue id", err) 1734 s.pages.Notice(w, "issues", "Failed to create issue.") 1735 return 1736 } 1737 1738 client, _ := s.auth.AuthorizedClient(r) 1739 atUri := f.RepoAt.String() 1740 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1741 Collection: tangled.RepoIssueNSID, 1742 Repo: user.Did, 1743 - Rkey: appview.TID(), 1744 Record: &lexutil.LexiconTypeDecoder{ 1745 Val: &tangled.RepoIssue{ 1746 Repo: atUri, ··· 1753 }) 1754 if err != nil { 1755 log.Println("failed to create issue", err) 1756 s.pages.Notice(w, "issues", "Failed to create issue.") 1757 return 1758 } 1759 1760 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1761 if err != nil { 1762 log.Println("failed to set issue at", err) 1763 s.pages.Notice(w, "issues", "Failed to create issue.") 1764 return 1765 } ··· 1770 } 1771 1772 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1773 user := s.auth.GetUser(r) 1774 - f, err := s.fullyResolvedRepo(r) 1775 if err != nil { 1776 log.Printf("failed to resolve source repo: %v", err) 1777 return 1778 } 1779 1780 switch r.Method { 1781 case http.MethodGet: 1782 user := s.auth.GetUser(r) 1783 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1784 if err != nil { 1785 s.pages.Notice(w, "repo", "Invalid user account.") 1786 return 1787 } 1788 1789 s.pages.ForkRepo(w, pages.ForkRepoParams{ 1790 LoggedInUser: user, 1791 Knots: knots, 1792 - RepoInfo: f.RepoInfo(s, user), 1793 }) 1794 1795 case http.MethodPost: 1796 - 1797 knot := r.FormValue("knot") 1798 if knot == "" { 1799 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 1800 return 1801 } 1802 1803 ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1804 if err != nil || !ok { 1805 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1806 return 1807 } 1808 1809 forkName := fmt.Sprintf("%s", f.RepoName) 1810 1811 // this check is *only* to see if the forked repo name already exists 1812 // in the user's account. 1813 - existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName) 1814 if err != nil { 1815 if errors.Is(err, sql.ErrNoRows) { 1816 // no existing repo with this name found, we can use the name as is 1817 } else { 1818 log.Println("error fetching existing repo from db", err) 1819 s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1820 return 1821 } 1822 } else if existingRepo != nil { 1823 // repo with this name already exists, append random string 1824 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1825 } 1826 secret, err := db.GetRegistrationKey(s.db, knot) 1827 if err != nil { 1828 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1829 return 1830 } 1831 1832 client, err := NewSignedClient(knot, secret, s.config.Dev) 1833 if err != nil { 1834 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1835 return 1836 } ··· 1844 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1845 sourceAt := f.RepoAt.String() 1846 1847 rkey := appview.TID() 1848 repo := &db.Repo{ 1849 Did: user.Did, ··· 1853 Source: sourceAt, 1854 } 1855 1856 - tx, err := s.db.BeginTx(r.Context(), nil) 1857 if err != nil { 1858 log.Println(err) 1859 s.pages.Notice(w, "repo", "Failed to save repository information.") 1860 return 1861 } ··· 1864 err = s.enforcer.E.LoadPolicy() 1865 if err != nil { 1866 log.Println("failed to rollback policies") 1867 } 1868 }() 1869 1870 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1871 if err != nil { 1872 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1873 return 1874 } 1875 1876 switch resp.StatusCode { 1877 case http.StatusConflict: 1878 s.pages.Notice(w, "repo", "A repository with that name already exists.") 1879 return 1880 case http.StatusInternalServerError: 1881 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1882 case http.StatusNoContent: 1883 // continue 1884 } ··· 1886 xrpcClient, _ := s.auth.AuthorizedClient(r) 1887 1888 createdAt := time.Now().Format(time.RFC3339) 1889 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 1890 Collection: tangled.RepoNSID, 1891 Repo: user.Did, 1892 Rkey: rkey, ··· 1901 }) 1902 if err != nil { 1903 log.Printf("failed to create record: %s", err) 1904 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 1905 return 1906 } 1907 log.Println("created repo record: ", atresp.Uri) 1908 1909 repo.AtUri = atresp.Uri 1910 - err = db.AddRepo(tx, repo) 1911 if err != nil { 1912 log.Println(err) 1913 s.pages.Notice(w, "repo", "Failed to save repository information.") 1914 return 1915 } ··· 1919 err = s.enforcer.AddRepo(user.Did, knot, p) 1920 if err != nil { 1921 log.Println(err) 1922 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1923 return 1924 } ··· 1926 err = tx.Commit() 1927 if err != nil { 1928 log.Println("failed to commit changes", err) 1929 http.Error(w, err.Error(), http.StatusInternalServerError) 1930 return 1931 } ··· 1933 err = s.enforcer.E.SavePolicy() 1934 if err != nil { 1935 log.Println("failed to update ACLs", err) 1936 http.Error(w, err.Error(), http.StatusInternalServerError) 1937 return 1938 }
··· 16 "strings" 17 "time" 18 19 + "go.opentelemetry.io/otel/attribute" 20 + "go.opentelemetry.io/otel/codes" 21 "tangled.sh/tangled.sh/core/api/tangled" 22 "tangled.sh/tangled.sh/core/appview" 23 "tangled.sh/tangled.sh/core/appview/auth" ··· 40 ) 41 42 func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 43 + ctx, span := s.t.TraceStart(r.Context(), "RepoIndex") 44 + defer span.End() 45 + 46 ref := chi.URLParam(r, "ref") 47 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 48 if err != nil { 49 log.Println("failed to fully resolve repo", err) 50 + span.RecordError(err) 51 + span.SetStatus(codes.Error, "failed to fully resolve repo") 52 return 53 } 54 55 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 56 if err != nil { 57 log.Printf("failed to create unsigned client for %s", f.Knot) 58 + span.RecordError(err) 59 + span.SetStatus(codes.Error, "failed to create unsigned client") 60 s.pages.Error503(w) 61 return 62 } ··· 65 if err != nil { 66 s.pages.Error503(w) 67 log.Println("failed to reach knotserver", err) 68 + span.RecordError(err) 69 + span.SetStatus(codes.Error, "failed to reach knotserver") 70 return 71 } 72 defer resp.Body.Close() ··· 74 body, err := io.ReadAll(resp.Body) 75 if err != nil { 76 log.Printf("Error reading response body: %v", err) 77 + span.RecordError(err) 78 + span.SetStatus(codes.Error, "error reading response body") 79 return 80 } 81 ··· 83 err = json.Unmarshal(body, &result) 84 if err != nil { 85 log.Printf("Error unmarshalling response body: %v", err) 86 + span.RecordError(err) 87 + span.SetStatus(codes.Error, "error unmarshalling response body") 88 return 89 } 90 ··· 127 tagCount := len(result.Tags) 128 fileCount := len(result.Files) 129 130 + span.SetAttributes( 131 + attribute.Int("commits.count", commitCount), 132 + attribute.Int("branches.count", branchCount), 133 + attribute.Int("tags.count", tagCount), 134 + attribute.Int("files.count", fileCount), 135 + ) 136 + 137 commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 138 commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 139 tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] ··· 144 user := s.auth.GetUser(r) 145 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 146 LoggedInUser: user, 147 + RepoInfo: f.RepoInfo(ctx, s, user), 148 TagMap: tagMap, 149 RepoIndexResponse: result, 150 CommitsTrunc: commitsTrunc, ··· 156 } 157 158 func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 159 + ctx, span := s.t.TraceStart(r.Context(), "RepoLog") 160 + defer span.End() 161 + 162 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 163 if err != nil { 164 log.Println("failed to fully resolve repo", err) 165 + span.RecordError(err) 166 + span.SetStatus(codes.Error, "failed to fully resolve repo") 167 return 168 } 169 ··· 176 } 177 178 ref := chi.URLParam(r, "ref") 179 + span.SetAttributes(attribute.Int("page", page), attribute.String("ref", ref)) 180 181 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 182 if err != nil { 183 log.Println("failed to create unsigned client", err) 184 + span.RecordError(err) 185 + span.SetStatus(codes.Error, "failed to create unsigned client") 186 return 187 } 188 189 resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 190 if err != nil { 191 log.Println("failed to reach knotserver", err) 192 + span.RecordError(err) 193 + span.SetStatus(codes.Error, "failed to reach knotserver") 194 return 195 } 196 197 body, err := io.ReadAll(resp.Body) 198 if err != nil { 199 log.Printf("error reading response body: %v", err) 200 + span.RecordError(err) 201 + span.SetStatus(codes.Error, "error reading response body") 202 return 203 } 204 ··· 206 err = json.Unmarshal(body, &repolog) 207 if err != nil { 208 log.Println("failed to parse json response", err) 209 + span.RecordError(err) 210 + span.SetStatus(codes.Error, "failed to parse json response") 211 return 212 } 213 + 214 + span.SetAttributes(attribute.Int("commits.count", len(repolog.Commits))) 215 216 result, err := us.Tags(f.OwnerDid(), f.RepoName) 217 if err != nil { 218 log.Println("failed to reach knotserver", err) 219 + span.RecordError(err) 220 + span.SetStatus(codes.Error, "failed to reach knotserver for tags") 221 return 222 } 223 ··· 230 tagMap[hash] = append(tagMap[hash], tag.Name) 231 } 232 233 + span.SetAttributes(attribute.Int("tags.count", len(result.Tags))) 234 + 235 user := s.auth.GetUser(r) 236 s.pages.RepoLog(w, pages.RepoLogParams{ 237 LoggedInUser: user, 238 TagMap: tagMap, 239 + RepoInfo: f.RepoInfo(ctx, s, user), 240 RepoLogResponse: repolog, 241 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 242 }) ··· 244 } 245 246 func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 247 + ctx, span := s.t.TraceStart(r.Context(), "RepoDescriptionEdit") 248 + defer span.End() 249 + 250 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 251 if err != nil { 252 log.Println("failed to get repo and knot", err) 253 w.WriteHeader(http.StatusBadRequest) ··· 256 257 user := s.auth.GetUser(r) 258 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 259 + RepoInfo: f.RepoInfo(ctx, s, user), 260 }) 261 return 262 } 263 264 func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 265 + ctx, span := s.t.TraceStart(r.Context(), "RepoDescription") 266 + defer span.End() 267 + 268 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 269 if err != nil { 270 log.Println("failed to get repo and knot", err) 271 + span.RecordError(err) 272 + span.SetStatus(codes.Error, "failed to resolve repo") 273 w.WriteHeader(http.StatusBadRequest) 274 return 275 } ··· 278 rkey := repoAt.RecordKey().String() 279 if rkey == "" { 280 log.Println("invalid aturi for repo", err) 281 + span.RecordError(err) 282 + span.SetStatus(codes.Error, "invalid aturi for repo") 283 w.WriteHeader(http.StatusInternalServerError) 284 return 285 } 286 287 user := s.auth.GetUser(r) 288 + span.SetAttributes(attribute.String("method", r.Method)) 289 290 switch r.Method { 291 case http.MethodGet: 292 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 293 + RepoInfo: f.RepoInfo(ctx, s, user), 294 }) 295 return 296 case http.MethodPut: 297 user := s.auth.GetUser(r) 298 newDescription := r.FormValue("description") 299 + span.SetAttributes(attribute.String("description", newDescription)) 300 client, _ := s.auth.AuthorizedClient(r) 301 302 // optimistic update 303 + err = db.UpdateDescription(ctx, s.db, string(repoAt), newDescription) 304 if err != nil { 305 + log.Println("failed to perform update-description query", err) 306 + span.RecordError(err) 307 + span.SetStatus(codes.Error, "failed to update description in database") 308 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 309 return 310 } ··· 312 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 313 // 314 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 315 + ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoNSID, user.Did, rkey) 316 if err != nil { 317 // failed to get record 318 + span.RecordError(err) 319 + span.SetStatus(codes.Error, "failed to get record from PDS") 320 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 321 return 322 } 323 + 324 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 325 Collection: tangled.RepoNSID, 326 Repo: user.Did, 327 Rkey: rkey, ··· 338 }) 339 340 if err != nil { 341 + log.Println("failed to perform update-description query", err) 342 + span.RecordError(err) 343 + span.SetStatus(codes.Error, "failed to put record to PDS") 344 // failed to get record 345 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 346 return 347 } 348 349 + newRepoInfo := f.RepoInfo(ctx, s, user) 350 newRepoInfo.Description = newDescription 351 352 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ ··· 357 } 358 359 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 360 + ctx, span := s.t.TraceStart(r.Context(), "RepoCommit") 361 + defer span.End() 362 + 363 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 364 if err != nil { 365 log.Println("failed to fully resolve repo", err) 366 + span.RecordError(err) 367 + span.SetStatus(codes.Error, "failed to fully resolve repo") 368 return 369 } 370 ref := chi.URLParam(r, "ref") ··· 373 protocol = "https" 374 } 375 376 + span.SetAttributes(attribute.String("ref", ref), attribute.String("protocol", protocol)) 377 + 378 if !plumbing.IsHash(ref) { 379 + span.SetAttributes(attribute.Bool("invalid_hash", true)) 380 s.pages.Error404(w) 381 return 382 } 383 384 + requestURL := fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref) 385 + span.SetAttributes(attribute.String("request_url", requestURL)) 386 + 387 + resp, err := http.Get(requestURL) 388 if err != nil { 389 log.Println("failed to reach knotserver", err) 390 + span.RecordError(err) 391 + span.SetStatus(codes.Error, "failed to reach knotserver") 392 return 393 } 394 395 body, err := io.ReadAll(resp.Body) 396 if err != nil { 397 log.Printf("Error reading response body: %v", err) 398 + span.RecordError(err) 399 + span.SetStatus(codes.Error, "error reading response body") 400 return 401 } 402 ··· 404 err = json.Unmarshal(body, &result) 405 if err != nil { 406 log.Println("failed to parse response:", err) 407 + span.RecordError(err) 408 + span.SetStatus(codes.Error, "failed to parse response") 409 return 410 } 411 412 user := s.auth.GetUser(r) 413 s.pages.RepoCommit(w, pages.RepoCommitParams{ 414 LoggedInUser: user, 415 + RepoInfo: f.RepoInfo(ctx, s, user), 416 RepoCommitResponse: result, 417 EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 418 }) ··· 420 } 421 422 func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 423 + ctx, span := s.t.TraceStart(r.Context(), "RepoTree") 424 + defer span.End() 425 + 426 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 427 if err != nil { 428 log.Println("failed to fully resolve repo", err) 429 + span.RecordError(err) 430 + span.SetStatus(codes.Error, "failed to fully resolve repo") 431 return 432 } 433 ··· 437 if !s.config.Dev { 438 protocol = "https" 439 } 440 + 441 + span.SetAttributes( 442 + attribute.String("ref", ref), 443 + attribute.String("tree_path", treePath), 444 + attribute.String("protocol", protocol), 445 + ) 446 + 447 + requestURL := fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath) 448 + span.SetAttributes(attribute.String("request_url", requestURL)) 449 + 450 + resp, err := http.Get(requestURL) 451 if err != nil { 452 log.Println("failed to reach knotserver", err) 453 + span.RecordError(err) 454 + span.SetStatus(codes.Error, "failed to reach knotserver") 455 return 456 } 457 458 body, err := io.ReadAll(resp.Body) 459 if err != nil { 460 log.Printf("Error reading response body: %v", err) 461 + span.RecordError(err) 462 + span.SetStatus(codes.Error, "error reading response body") 463 return 464 } 465 ··· 467 err = json.Unmarshal(body, &result) 468 if err != nil { 469 log.Println("failed to parse response:", err) 470 + span.RecordError(err) 471 + span.SetStatus(codes.Error, "failed to parse response") 472 return 473 } 474 475 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 476 // so we can safely redirect to the "parent" (which is the same file). 477 if len(result.Files) == 0 && result.Parent == treePath { 478 + redirectURL := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent) 479 + span.SetAttributes(attribute.String("redirect_url", redirectURL)) 480 + http.Redirect(w, r, redirectURL, http.StatusFound) 481 return 482 } 483 ··· 499 BreadCrumbs: breadcrumbs, 500 BaseTreeLink: baseTreeLink, 501 BaseBlobLink: baseBlobLink, 502 + RepoInfo: f.RepoInfo(ctx, s, user), 503 RepoTreeResponse: result, 504 }) 505 return 506 } 507 508 func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 509 + ctx, span := s.t.TraceStart(r.Context(), "RepoTags") 510 + defer span.End() 511 + 512 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 513 if err != nil { 514 log.Println("failed to get repo and knot", err) 515 + span.RecordError(err) 516 + span.SetStatus(codes.Error, "failed to get repo and knot") 517 return 518 } 519 520 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 521 if err != nil { 522 log.Println("failed to create unsigned client", err) 523 + span.RecordError(err) 524 + span.SetStatus(codes.Error, "failed to create unsigned client") 525 return 526 } 527 528 result, err := us.Tags(f.OwnerDid(), f.RepoName) 529 if err != nil { 530 log.Println("failed to reach knotserver", err) 531 + span.RecordError(err) 532 + span.SetStatus(codes.Error, "failed to reach knotserver") 533 return 534 } 535 536 + span.SetAttributes(attribute.Int("tags.count", len(result.Tags))) 537 + 538 artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt)) 539 if err != nil { 540 log.Println("failed grab artifacts", err) 541 + span.RecordError(err) 542 + span.SetStatus(codes.Error, "failed to grab artifacts") 543 return 544 } 545 + 546 + span.SetAttributes(attribute.Int("artifacts.count", len(artifacts))) 547 548 // convert artifacts to map for easy UI building 549 artifactMap := make(map[plumbing.Hash][]db.Artifact) ··· 567 } 568 } 569 570 + span.SetAttributes(attribute.Int("dangling_artifacts.count", len(danglingArtifacts))) 571 + 572 user := s.auth.GetUser(r) 573 s.pages.RepoTags(w, pages.RepoTagsParams{ 574 LoggedInUser: user, 575 + RepoInfo: f.RepoInfo(ctx, s, user), 576 RepoTagsResponse: *result, 577 ArtifactMap: artifactMap, 578 DanglingArtifacts: danglingArtifacts, ··· 581 } 582 583 func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 584 + ctx, span := s.t.TraceStart(r.Context(), "RepoBranches") 585 + defer span.End() 586 + 587 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 588 if err != nil { 589 log.Println("failed to get repo and knot", err) 590 + span.RecordError(err) 591 + span.SetStatus(codes.Error, "failed to get repo and knot") 592 return 593 } 594 595 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 596 if err != nil { 597 log.Println("failed to create unsigned client", err) 598 + span.RecordError(err) 599 + span.SetStatus(codes.Error, "failed to create unsigned client") 600 return 601 } 602 603 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 604 if err != nil { 605 log.Println("failed to reach knotserver", err) 606 + span.RecordError(err) 607 + span.SetStatus(codes.Error, "failed to reach knotserver") 608 return 609 } 610 611 body, err := io.ReadAll(resp.Body) 612 if err != nil { 613 log.Printf("Error reading response body: %v", err) 614 + span.RecordError(err) 615 + span.SetStatus(codes.Error, "error reading response body") 616 return 617 } 618 ··· 620 err = json.Unmarshal(body, &result) 621 if err != nil { 622 log.Println("failed to parse response:", err) 623 + span.RecordError(err) 624 + span.SetStatus(codes.Error, "failed to parse response") 625 return 626 } 627 + 628 + span.SetAttributes(attribute.Int("branches.count", len(result.Branches))) 629 630 slices.SortFunc(result.Branches, func(a, b types.Branch) int { 631 if a.IsDefault { ··· 647 user := s.auth.GetUser(r) 648 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 649 LoggedInUser: user, 650 + RepoInfo: f.RepoInfo(ctx, s, user), 651 RepoBranchesResponse: result, 652 }) 653 return 654 } 655 656 func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 657 + ctx, span := s.t.TraceStart(r.Context(), "RepoBlob") 658 + defer span.End() 659 + 660 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 661 if err != nil { 662 log.Println("failed to get repo and knot", err) 663 + span.RecordError(err) 664 + span.SetStatus(codes.Error, "failed to get repo and knot") 665 return 666 } 667 ··· 671 if !s.config.Dev { 672 protocol = "https" 673 } 674 + 675 + span.SetAttributes( 676 + attribute.String("ref", ref), 677 + attribute.String("file_path", filePath), 678 + attribute.String("protocol", protocol), 679 + ) 680 + 681 + requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 682 + span.SetAttributes(attribute.String("request_url", requestURL)) 683 + 684 + resp, err := http.Get(requestURL) 685 if err != nil { 686 log.Println("failed to reach knotserver", err) 687 + span.RecordError(err) 688 + span.SetStatus(codes.Error, "failed to reach knotserver") 689 return 690 } 691 692 body, err := io.ReadAll(resp.Body) 693 if err != nil { 694 log.Printf("Error reading response body: %v", err) 695 + span.RecordError(err) 696 + span.SetStatus(codes.Error, "error reading response body") 697 return 698 } 699 ··· 701 err = json.Unmarshal(body, &result) 702 if err != nil { 703 log.Println("failed to parse response:", err) 704 + span.RecordError(err) 705 + span.SetStatus(codes.Error, "failed to parse response") 706 return 707 } 708 ··· 722 showRendered = r.URL.Query().Get("code") != "true" 723 } 724 725 + span.SetAttributes( 726 + attribute.Bool("is_binary", result.IsBinary), 727 + attribute.Bool("show_rendered", showRendered), 728 + attribute.Bool("render_toggle", renderToggle), 729 + ) 730 + 731 user := s.auth.GetUser(r) 732 s.pages.RepoBlob(w, pages.RepoBlobParams{ 733 LoggedInUser: user, 734 + RepoInfo: f.RepoInfo(ctx, s, user), 735 RepoBlobResponse: result, 736 BreadCrumbs: breadcrumbs, 737 ShowRendered: showRendered, ··· 741 } 742 743 func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 744 + ctx, span := s.t.TraceStart(r.Context(), "RepoBlobRaw") 745 + defer span.End() 746 + 747 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 748 if err != nil { 749 log.Println("failed to get repo and knot", err) 750 + span.RecordError(err) 751 + span.SetStatus(codes.Error, "failed to get repo and knot") 752 return 753 } 754 ··· 759 if !s.config.Dev { 760 protocol = "https" 761 } 762 + 763 + span.SetAttributes( 764 + attribute.String("ref", ref), 765 + attribute.String("file_path", filePath), 766 + attribute.String("protocol", protocol), 767 + ) 768 + 769 + requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 770 + span.SetAttributes(attribute.String("request_url", requestURL)) 771 + 772 + resp, err := http.Get(requestURL) 773 if err != nil { 774 log.Println("failed to reach knotserver", err) 775 + span.RecordError(err) 776 + span.SetStatus(codes.Error, "failed to reach knotserver") 777 return 778 } 779 780 body, err := io.ReadAll(resp.Body) 781 if err != nil { 782 log.Printf("Error reading response body: %v", err) 783 + span.RecordError(err) 784 + span.SetStatus(codes.Error, "error reading response body") 785 return 786 } 787 ··· 789 err = json.Unmarshal(body, &result) 790 if err != nil { 791 log.Println("failed to parse response:", err) 792 + span.RecordError(err) 793 + span.SetStatus(codes.Error, "failed to parse response") 794 return 795 } 796 + 797 + span.SetAttributes(attribute.Bool("is_binary", result.IsBinary)) 798 799 if result.IsBinary { 800 w.Header().Set("Content-Type", "application/octet-stream") ··· 808 } 809 810 func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 811 + ctx, span := s.t.TraceStart(r.Context(), "AddCollaborator") 812 + defer span.End() 813 + 814 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 815 if err != nil { 816 log.Println("failed to get repo and knot", err) 817 + span.RecordError(err) 818 + span.SetStatus(codes.Error, "failed to get repo and knot") 819 return 820 } 821 822 collaborator := r.FormValue("collaborator") 823 if collaborator == "" { 824 + span.SetAttributes(attribute.String("error", "malformed_form")) 825 http.Error(w, "malformed form", http.StatusBadRequest) 826 return 827 } 828 829 + span.SetAttributes(attribute.String("collaborator", collaborator)) 830 + 831 + collaboratorIdent, err := s.resolver.ResolveIdent(ctx, collaborator) 832 if err != nil { 833 + span.RecordError(err) 834 + span.SetStatus(codes.Error, "failed to resolve collaborator") 835 w.Write([]byte("failed to resolve collaborator did to a handle")) 836 return 837 } 838 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 839 + span.SetAttributes( 840 + attribute.String("collaborator_did", collaboratorIdent.DID.String()), 841 + attribute.String("collaborator_handle", collaboratorIdent.Handle.String()), 842 + ) 843 844 // TODO: create an atproto record for this 845 846 secret, err := db.GetRegistrationKey(s.db, f.Knot) 847 if err != nil { 848 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 849 + span.RecordError(err) 850 + span.SetStatus(codes.Error, "no key found for domain") 851 return 852 } 853 854 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 855 if err != nil { 856 log.Println("failed to create client to ", f.Knot) 857 + span.RecordError(err) 858 + span.SetStatus(codes.Error, "failed to create signed client") 859 return 860 } 861 862 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 863 if err != nil { 864 log.Printf("failed to make request to %s: %s", f.Knot, err) 865 + span.RecordError(err) 866 + span.SetStatus(codes.Error, "failed to make request to knotserver") 867 return 868 } 869 870 if ksResp.StatusCode != http.StatusNoContent { 871 + span.SetAttributes(attribute.Int("status_code", ksResp.StatusCode)) 872 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 873 return 874 } 875 876 + tx, err := s.db.BeginTx(ctx, nil) 877 if err != nil { 878 log.Println("failed to start tx") 879 + span.RecordError(err) 880 + span.SetStatus(codes.Error, "failed to start transaction") 881 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 882 return 883 } ··· 891 892 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 893 if err != nil { 894 + span.RecordError(err) 895 + span.SetStatus(codes.Error, "failed to add collaborator to enforcer") 896 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 897 return 898 } 899 900 + err = db.AddCollaborator(ctx, s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 901 if err != nil { 902 + span.RecordError(err) 903 + span.SetStatus(codes.Error, "failed to add collaborator to database") 904 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 905 return 906 } ··· 908 err = tx.Commit() 909 if err != nil { 910 log.Println("failed to commit changes", err) 911 + span.RecordError(err) 912 + span.SetStatus(codes.Error, "failed to commit transaction") 913 http.Error(w, err.Error(), http.StatusInternalServerError) 914 return 915 } ··· 917 err = s.enforcer.E.SavePolicy() 918 if err != nil { 919 log.Println("failed to update ACLs", err) 920 + span.RecordError(err) 921 + span.SetStatus(codes.Error, "failed to save enforcer policy") 922 http.Error(w, err.Error(), http.StatusInternalServerError) 923 return 924 } 925 926 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 927 } 928 929 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 930 + ctx, span := s.t.TraceStart(r.Context(), "DeleteRepo") 931 + defer span.End() 932 + 933 user := s.auth.GetUser(r) 934 935 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 936 if err != nil { 937 log.Println("failed to get repo and knot", err) 938 + span.RecordError(err) 939 + span.SetStatus(codes.Error, "failed to get repo and knot") 940 return 941 } 942 943 + span.SetAttributes( 944 + attribute.String("repo_name", f.RepoName), 945 + attribute.String("knot", f.Knot), 946 + attribute.String("owner_did", f.OwnerDid()), 947 + ) 948 + 949 // remove record from pds 950 xrpcClient, _ := s.auth.AuthorizedClient(r) 951 repoRkey := f.RepoAt.RecordKey().String() 952 + _, err = comatproto.RepoDeleteRecord(ctx, xrpcClient, &comatproto.RepoDeleteRecord_Input{ 953 Collection: tangled.RepoNSID, 954 Repo: user.Did, 955 Rkey: repoRkey, 956 }) 957 if err != nil { 958 log.Printf("failed to delete record: %s", err) 959 + span.RecordError(err) 960 + span.SetStatus(codes.Error, "failed to delete record from PDS") 961 s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 962 return 963 } 964 log.Println("removed repo record ", f.RepoAt.String()) 965 + span.SetAttributes(attribute.String("repo_at", f.RepoAt.String())) 966 967 secret, err := db.GetRegistrationKey(s.db, f.Knot) 968 if err != nil { 969 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 970 + span.RecordError(err) 971 + span.SetStatus(codes.Error, "no key found for domain") 972 return 973 } 974 975 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 976 if err != nil { 977 log.Println("failed to create client to ", f.Knot) 978 + span.RecordError(err) 979 + span.SetStatus(codes.Error, "failed to create client") 980 return 981 } 982 983 ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 984 if err != nil { 985 log.Printf("failed to make request to %s: %s", f.Knot, err) 986 + span.RecordError(err) 987 + span.SetStatus(codes.Error, "failed to make request to knotserver") 988 return 989 } 990 991 + span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode)) 992 if ksResp.StatusCode != http.StatusNoContent { 993 log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 994 + span.SetAttributes(attribute.Bool("knot_remove_failed", true)) 995 } else { 996 log.Println("removed repo from knot ", f.Knot) 997 + span.SetAttributes(attribute.Bool("knot_remove_success", true)) 998 } 999 1000 + tx, err := s.db.BeginTx(ctx, nil) 1001 if err != nil { 1002 log.Println("failed to start tx") 1003 + span.RecordError(err) 1004 + span.SetStatus(codes.Error, "failed to start transaction") 1005 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 1006 return 1007 } ··· 1010 err = s.enforcer.E.LoadPolicy() 1011 if err != nil { 1012 log.Println("failed to rollback policies") 1013 + span.RecordError(err) 1014 } 1015 }() 1016 1017 // remove collaborator RBAC 1018 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 1019 if err != nil { 1020 + span.RecordError(err) 1021 + span.SetStatus(codes.Error, "failed to get collaborators") 1022 s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 1023 return 1024 } 1025 + span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators))) 1026 + 1027 for _, c := range repoCollaborators { 1028 did := c[0] 1029 s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) ··· 1033 // remove repo RBAC 1034 err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 1035 if err != nil { 1036 + span.RecordError(err) 1037 + span.SetStatus(codes.Error, "failed to remove repo RBAC") 1038 s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 1039 return 1040 } 1041 1042 // remove repo from db 1043 + err = db.RemoveRepo(ctx, tx, f.OwnerDid(), f.RepoName) 1044 if err != nil { 1045 + span.RecordError(err) 1046 + span.SetStatus(codes.Error, "failed to remove repo from db") 1047 s.pages.Notice(w, "settings-delete", "Failed to update appview") 1048 return 1049 } ··· 1052 err = tx.Commit() 1053 if err != nil { 1054 log.Println("failed to commit changes", err) 1055 + span.RecordError(err) 1056 + span.SetStatus(codes.Error, "failed to commit transaction") 1057 http.Error(w, err.Error(), http.StatusInternalServerError) 1058 return 1059 } ··· 1061 err = s.enforcer.E.SavePolicy() 1062 if err != nil { 1063 log.Println("failed to update ACLs", err) 1064 + span.RecordError(err) 1065 + span.SetStatus(codes.Error, "failed to save policy") 1066 http.Error(w, err.Error(), http.StatusInternalServerError) 1067 return 1068 } ··· 1071 } 1072 1073 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1074 + ctx, span := s.t.TraceStart(r.Context(), "SetDefaultBranch") 1075 + defer span.End() 1076 + 1077 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1078 if err != nil { 1079 log.Println("failed to get repo and knot", err) 1080 + span.RecordError(err) 1081 + span.SetStatus(codes.Error, "failed to get repo and knot") 1082 return 1083 } 1084 1085 branch := r.FormValue("branch") 1086 if branch == "" { 1087 + span.SetAttributes(attribute.Bool("malformed_form", true)) 1088 + span.SetStatus(codes.Error, "malformed form") 1089 http.Error(w, "malformed form", http.StatusBadRequest) 1090 return 1091 } 1092 1093 + span.SetAttributes( 1094 + attribute.String("branch", branch), 1095 + attribute.String("repo_name", f.RepoName), 1096 + attribute.String("knot", f.Knot), 1097 + attribute.String("owner_did", f.OwnerDid()), 1098 + ) 1099 + 1100 secret, err := db.GetRegistrationKey(s.db, f.Knot) 1101 if err != nil { 1102 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 1103 + span.RecordError(err) 1104 + span.SetStatus(codes.Error, "no key found for domain") 1105 return 1106 } 1107 1108 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1109 if err != nil { 1110 log.Println("failed to create client to ", f.Knot) 1111 + span.RecordError(err) 1112 + span.SetStatus(codes.Error, "failed to create client") 1113 return 1114 } 1115 1116 ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 1117 if err != nil { 1118 log.Printf("failed to make request to %s: %s", f.Knot, err) 1119 + span.RecordError(err) 1120 + span.SetStatus(codes.Error, "failed to make request to knotserver") 1121 return 1122 } 1123 1124 + span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode)) 1125 if ksResp.StatusCode != http.StatusNoContent { 1126 + span.SetStatus(codes.Error, "failed to set default branch") 1127 s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 1128 return 1129 } ··· 1132 } 1133 1134 func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 1135 + ctx, span := s.t.TraceStart(r.Context(), "RepoSettings") 1136 + defer span.End() 1137 + 1138 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1139 if err != nil { 1140 log.Println("failed to get repo and knot", err) 1141 + span.RecordError(err) 1142 + span.SetStatus(codes.Error, "failed to get repo and knot") 1143 return 1144 } 1145 1146 + span.SetAttributes( 1147 + attribute.String("repo_name", f.RepoName), 1148 + attribute.String("knot", f.Knot), 1149 + attribute.String("owner_did", f.OwnerDid()), 1150 + attribute.String("method", r.Method), 1151 + ) 1152 + 1153 switch r.Method { 1154 case http.MethodGet: 1155 // for now, this is just pubkeys 1156 user := s.auth.GetUser(r) 1157 + repoCollaborators, err := f.Collaborators(ctx, s) 1158 if err != nil { 1159 log.Println("failed to get collaborators", err) 1160 + span.RecordError(err) 1161 + span.SetAttributes(attribute.String("error", "failed_to_get_collaborators")) 1162 } 1163 + span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators))) 1164 1165 isCollaboratorInviteAllowed := false 1166 if user != nil { ··· 1169 isCollaboratorInviteAllowed = true 1170 } 1171 } 1172 + span.SetAttributes(attribute.Bool("invite_allowed", isCollaboratorInviteAllowed)) 1173 1174 var branchNames []string 1175 var defaultBranch string 1176 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 1177 if err != nil { 1178 log.Println("failed to create unsigned client", err) 1179 + span.RecordError(err) 1180 + span.SetAttributes(attribute.String("error", "failed_to_create_unsigned_client")) 1181 } else { 1182 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 1183 if err != nil { 1184 log.Println("failed to reach knotserver", err) 1185 + span.RecordError(err) 1186 + span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_branches")) 1187 } else { 1188 defer resp.Body.Close() 1189 1190 body, err := io.ReadAll(resp.Body) 1191 if err != nil { 1192 log.Printf("Error reading response body: %v", err) 1193 + span.RecordError(err) 1194 + span.SetAttributes(attribute.String("error", "failed_to_read_branches_response")) 1195 } else { 1196 var result types.RepoBranchesResponse 1197 err = json.Unmarshal(body, &result) 1198 if err != nil { 1199 log.Println("failed to parse response:", err) 1200 + span.RecordError(err) 1201 + span.SetAttributes(attribute.String("error", "failed_to_parse_branches_response")) 1202 } else { 1203 for _, branch := range result.Branches { 1204 branchNames = append(branchNames, branch.Name) 1205 } 1206 + span.SetAttributes(attribute.Int("branches.count", len(branchNames))) 1207 } 1208 } 1209 } ··· 1211 defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName) 1212 if err != nil { 1213 log.Println("failed to reach knotserver", err) 1214 + span.RecordError(err) 1215 + span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_default_branch")) 1216 } else { 1217 defaultBranch = defaultBranchResp.Branch 1218 + span.SetAttributes(attribute.String("default_branch", defaultBranch)) 1219 } 1220 } 1221 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 1222 LoggedInUser: user, 1223 + RepoInfo: f.RepoInfo(ctx, s, user), 1224 Collaborators: repoCollaborators, 1225 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1226 Branches: branchNames, ··· 1309 return collaborators, nil 1310 } 1311 1312 + func (f *FullyResolvedRepo) RepoInfo(ctx context.Context, s *State, u *auth.User) repoinfo.RepoInfo { 1313 + ctx, span := s.t.TraceStart(ctx, "RepoInfo") 1314 + defer span.End() 1315 + 1316 isStarred := false 1317 if u != nil { 1318 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 1319 + span.SetAttributes(attribute.Bool("is_starred", isStarred)) 1320 } 1321 1322 starCount, err := db.GetStarCount(s.db, f.RepoAt) 1323 if err != nil { 1324 log.Println("failed to get star count for ", f.RepoAt) 1325 + span.RecordError(err) 1326 } 1327 + 1328 issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 1329 if err != nil { 1330 log.Println("failed to get issue count for ", f.RepoAt) 1331 + span.RecordError(err) 1332 } 1333 + 1334 pullCount, err := db.GetPullCount(s.db, f.RepoAt) 1335 if err != nil { 1336 log.Println("failed to get issue count for ", f.RepoAt) 1337 + span.RecordError(err) 1338 } 1339 + 1340 + span.SetAttributes( 1341 + attribute.Int("stats.stars", starCount), 1342 + attribute.Int("stats.issues.open", issueCount.Open), 1343 + attribute.Int("stats.issues.closed", issueCount.Closed), 1344 + attribute.Int("stats.pulls.open", pullCount.Open), 1345 + attribute.Int("stats.pulls.closed", pullCount.Closed), 1346 + attribute.Int("stats.pulls.merged", pullCount.Merged), 1347 + ) 1348 + 1349 + source, err := db.GetRepoSource(ctx, s.db, f.RepoAt) 1350 if errors.Is(err, sql.ErrNoRows) { 1351 source = "" 1352 } else if err != nil { 1353 log.Println("failed to get repo source for ", f.RepoAt, err) 1354 + span.RecordError(err) 1355 } 1356 1357 var sourceRepo *db.Repo 1358 if source != "" { 1359 + span.SetAttributes(attribute.String("source", source)) 1360 + sourceRepo, err = db.GetRepoByAtUri(ctx, s.db, source) 1361 if err != nil { 1362 log.Println("failed to get repo by at uri", err) 1363 + span.RecordError(err) 1364 } 1365 } 1366 1367 var sourceHandle *identity.Identity 1368 if sourceRepo != nil { 1369 + sourceHandle, err = s.resolver.ResolveIdent(ctx, sourceRepo.Did) 1370 if err != nil { 1371 log.Println("failed to resolve source repo", err) 1372 + span.RecordError(err) 1373 + } else if sourceHandle != nil { 1374 + span.SetAttributes(attribute.String("source_handle", sourceHandle.Handle.String())) 1375 } 1376 } 1377 1378 knot := f.Knot 1379 + span.SetAttributes(attribute.String("knot", knot)) 1380 + 1381 var disableFork bool 1382 us, err := NewUnsignedClient(knot, s.config.Dev) 1383 if err != nil { 1384 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1385 + span.RecordError(err) 1386 } else { 1387 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 1388 if err != nil { 1389 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 1390 + span.RecordError(err) 1391 } else { 1392 defer resp.Body.Close() 1393 body, err := io.ReadAll(resp.Body) 1394 if err != nil { 1395 log.Printf("error reading branch response body: %v", err) 1396 + span.RecordError(err) 1397 } else { 1398 var branchesResp types.RepoBranchesResponse 1399 if err := json.Unmarshal(body, &branchesResp); err != nil { 1400 log.Printf("error parsing branch response: %v", err) 1401 + span.RecordError(err) 1402 } else { 1403 disableFork = false 1404 } ··· 1406 if len(branchesResp.Branches) == 0 { 1407 disableFork = true 1408 } 1409 + span.SetAttributes( 1410 + attribute.Int("branches.count", len(branchesResp.Branches)), 1411 + attribute.Bool("disable_fork", disableFork), 1412 + ) 1413 } 1414 } 1415 } ··· 1441 } 1442 1443 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1444 + ctx, span := s.t.TraceStart(r.Context(), "RepoSingleIssue") 1445 + defer span.End() 1446 + 1447 user := s.auth.GetUser(r) 1448 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1449 if err != nil { 1450 log.Println("failed to get repo and knot", err) 1451 + span.RecordError(err) 1452 + span.SetStatus(codes.Error, "failed to resolve repo") 1453 return 1454 } 1455 ··· 1458 if err != nil { 1459 http.Error(w, "bad issue id", http.StatusBadRequest) 1460 log.Println("failed to parse issue id", err) 1461 + span.RecordError(err) 1462 + span.SetStatus(codes.Error, "failed to parse issue id") 1463 return 1464 } 1465 1466 + span.SetAttributes(attribute.Int("issue_id", issueIdInt)) 1467 + 1468 + issue, comments, err := db.GetIssueWithComments(ctx, s.db, f.RepoAt, issueIdInt) 1469 if err != nil { 1470 log.Println("failed to get issue and comments", err) 1471 + span.RecordError(err) 1472 + span.SetStatus(codes.Error, "failed to get issue and comments") 1473 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1474 return 1475 } 1476 1477 + span.SetAttributes( 1478 + attribute.Int("comments.count", len(comments)), 1479 + attribute.String("issue.title", issue.Title), 1480 + attribute.String("issue.owner_did", issue.OwnerDid), 1481 + ) 1482 + 1483 + issueOwnerIdent, err := s.resolver.ResolveIdent(ctx, issue.OwnerDid) 1484 if err != nil { 1485 log.Println("failed to resolve issue owner", err) 1486 + span.RecordError(err) 1487 + span.SetStatus(codes.Error, "failed to resolve issue owner") 1488 } 1489 1490 identsToResolve := make([]string, len(comments)) 1491 for i, comment := range comments { 1492 identsToResolve[i] = comment.OwnerDid 1493 } 1494 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 1495 didHandleMap := make(map[string]string) 1496 for _, identity := range resolvedIds { 1497 if !identity.Handle.IsInvalidHandle() { ··· 1503 1504 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1505 LoggedInUser: user, 1506 + RepoInfo: f.RepoInfo(ctx, s, user), 1507 Issue: *issue, 1508 Comments: comments, 1509 1510 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 1511 DidHandleMap: didHandleMap, 1512 }) 1513 } 1514 1515 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1516 + ctx, span := s.t.TraceStart(r.Context(), "CloseIssue") 1517 + defer span.End() 1518 + 1519 user := s.auth.GetUser(r) 1520 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1521 if err != nil { 1522 log.Println("failed to get repo and knot", err) 1523 + span.RecordError(err) 1524 + span.SetStatus(codes.Error, "failed to resolve repo") 1525 return 1526 } 1527 ··· 1530 if err != nil { 1531 http.Error(w, "bad issue id", http.StatusBadRequest) 1532 log.Println("failed to parse issue id", err) 1533 + span.RecordError(err) 1534 + span.SetStatus(codes.Error, "failed to parse issue id") 1535 return 1536 } 1537 1538 + span.SetAttributes(attribute.Int("issue_id", issueIdInt)) 1539 + 1540 + issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt) 1541 if err != nil { 1542 log.Println("failed to get issue", err) 1543 + span.RecordError(err) 1544 + span.SetStatus(codes.Error, "failed to get issue") 1545 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1546 return 1547 } 1548 1549 + collaborators, err := f.Collaborators(ctx, s) 1550 if err != nil { 1551 log.Println("failed to fetch repo collaborators: %w", err) 1552 + span.RecordError(err) 1553 + span.SetStatus(codes.Error, "failed to fetch repo collaborators") 1554 } 1555 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1556 return user.Did == collab.Did 1557 }) 1558 isIssueOwner := user.Did == issue.OwnerDid 1559 1560 + span.SetAttributes( 1561 + attribute.Bool("is_collaborator", isCollaborator), 1562 + attribute.Bool("is_issue_owner", isIssueOwner), 1563 + ) 1564 + 1565 // TODO: make this more granular 1566 if isIssueOwner || isCollaborator { 1567 closed := tangled.RepoIssueStateClosed 1568 1569 client, _ := s.auth.AuthorizedClient(r) 1570 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1571 Collection: tangled.RepoIssueStateNSID, 1572 Repo: user.Did, 1573 Rkey: appview.TID(), ··· 1581 1582 if err != nil { 1583 log.Println("failed to update issue state", err) 1584 + span.RecordError(err) 1585 + span.SetStatus(codes.Error, "failed to update issue state in PDS") 1586 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1587 return 1588 } ··· 1590 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1591 if err != nil { 1592 log.Println("failed to close issue", err) 1593 + span.RecordError(err) 1594 + span.SetStatus(codes.Error, "failed to close issue in database") 1595 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1596 return 1597 } ··· 1600 return 1601 } else { 1602 log.Println("user is not permitted to close issue") 1603 + span.SetAttributes(attribute.Bool("permission_denied", true)) 1604 http.Error(w, "for biden", http.StatusUnauthorized) 1605 return 1606 } 1607 } 1608 1609 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1610 + ctx, span := s.t.TraceStart(r.Context(), "ReopenIssue") 1611 + defer span.End() 1612 + 1613 user := s.auth.GetUser(r) 1614 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1615 if err != nil { 1616 log.Println("failed to get repo and knot", err) 1617 + span.RecordError(err) 1618 + span.SetStatus(codes.Error, "failed to resolve repo") 1619 return 1620 } 1621 ··· 1624 if err != nil { 1625 http.Error(w, "bad issue id", http.StatusBadRequest) 1626 log.Println("failed to parse issue id", err) 1627 + span.RecordError(err) 1628 + span.SetStatus(codes.Error, "failed to parse issue id") 1629 return 1630 } 1631 1632 + span.SetAttributes(attribute.Int("issue_id", issueIdInt)) 1633 + 1634 + issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt) 1635 if err != nil { 1636 log.Println("failed to get issue", err) 1637 + span.RecordError(err) 1638 + span.SetStatus(codes.Error, "failed to get issue") 1639 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1640 return 1641 } 1642 1643 + collaborators, err := f.Collaborators(ctx, s) 1644 if err != nil { 1645 log.Println("failed to fetch repo collaborators: %w", err) 1646 + span.RecordError(err) 1647 + span.SetStatus(codes.Error, "failed to fetch repo collaborators") 1648 } 1649 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1650 return user.Did == collab.Did 1651 }) 1652 isIssueOwner := user.Did == issue.OwnerDid 1653 + 1654 + span.SetAttributes( 1655 + attribute.Bool("is_collaborator", isCollaborator), 1656 + attribute.Bool("is_issue_owner", isIssueOwner), 1657 + ) 1658 1659 if isCollaborator || isIssueOwner { 1660 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1661 if err != nil { 1662 log.Println("failed to reopen issue", err) 1663 + span.RecordError(err) 1664 + span.SetStatus(codes.Error, "failed to reopen issue") 1665 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1666 return 1667 } ··· 1669 return 1670 } else { 1671 log.Println("user is not the owner of the repo") 1672 + span.SetAttributes(attribute.Bool("permission_denied", true)) 1673 http.Error(w, "forbidden", http.StatusUnauthorized) 1674 return 1675 } 1676 } 1677 1678 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1679 + ctx, span := s.t.TraceStart(r.Context(), "NewIssueComment") 1680 + defer span.End() 1681 + 1682 user := s.auth.GetUser(r) 1683 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1684 if err != nil { 1685 log.Println("failed to get repo and knot", err) 1686 + span.RecordError(err) 1687 + span.SetStatus(codes.Error, "failed to resolve repo") 1688 return 1689 } 1690 ··· 1693 if err != nil { 1694 http.Error(w, "bad issue id", http.StatusBadRequest) 1695 log.Println("failed to parse issue id", err) 1696 + span.RecordError(err) 1697 + span.SetStatus(codes.Error, "failed to parse issue id") 1698 return 1699 } 1700 1701 + span.SetAttributes( 1702 + attribute.Int("issue_id", issueIdInt), 1703 + attribute.String("method", r.Method), 1704 + ) 1705 + 1706 switch r.Method { 1707 case http.MethodPost: 1708 body := r.FormValue("body") 1709 if body == "" { 1710 + span.SetAttributes(attribute.Bool("missing_body", true)) 1711 s.pages.Notice(w, "issue", "Body is required") 1712 return 1713 } ··· 1715 commentId := mathrand.IntN(1000000) 1716 rkey := appview.TID() 1717 1718 + span.SetAttributes( 1719 + attribute.Int("comment_id", commentId), 1720 + attribute.String("rkey", rkey), 1721 + ) 1722 + 1723 err := db.NewIssueComment(s.db, &db.Comment{ 1724 OwnerDid: user.Did, 1725 RepoAt: f.RepoAt, ··· 1730 }) 1731 if err != nil { 1732 log.Println("failed to create comment", err) 1733 + span.RecordError(err) 1734 + span.SetStatus(codes.Error, "failed to create comment in database") 1735 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1736 return 1737 } ··· 1742 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1743 if err != nil { 1744 log.Println("failed to get issue at", err) 1745 + span.RecordError(err) 1746 + span.SetStatus(codes.Error, "failed to get issue at") 1747 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1748 return 1749 } 1750 1751 + span.SetAttributes(attribute.String("issue_at", issueAt)) 1752 + 1753 atUri := f.RepoAt.String() 1754 client, _ := s.auth.AuthorizedClient(r) 1755 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1756 Collection: tangled.RepoIssueCommentNSID, 1757 Repo: user.Did, 1758 Rkey: rkey, ··· 1769 }) 1770 if err != nil { 1771 log.Println("failed to create comment", err) 1772 + span.RecordError(err) 1773 + span.SetStatus(codes.Error, "failed to create comment in PDS") 1774 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1775 return 1776 } ··· 1781 } 1782 1783 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1784 + ctx, span := s.t.TraceStart(r.Context(), "IssueComment") 1785 + defer span.End() 1786 + 1787 user := s.auth.GetUser(r) 1788 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1789 if err != nil { 1790 log.Println("failed to get repo and knot", err) 1791 + span.RecordError(err) 1792 + span.SetStatus(codes.Error, "failed to resolve repo") 1793 return 1794 } 1795 ··· 1798 if err != nil { 1799 http.Error(w, "bad issue id", http.StatusBadRequest) 1800 log.Println("failed to parse issue id", err) 1801 + span.RecordError(err) 1802 + span.SetStatus(codes.Error, "failed to parse issue id") 1803 return 1804 } 1805 ··· 1808 if err != nil { 1809 http.Error(w, "bad comment id", http.StatusBadRequest) 1810 log.Println("failed to parse issue id", err) 1811 + span.RecordError(err) 1812 + span.SetStatus(codes.Error, "failed to parse comment id") 1813 return 1814 } 1815 1816 + span.SetAttributes( 1817 + attribute.Int("issue_id", issueIdInt), 1818 + attribute.Int("comment_id", commentIdInt), 1819 + ) 1820 + 1821 + issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt) 1822 if err != nil { 1823 log.Println("failed to get issue", err) 1824 + span.RecordError(err) 1825 + span.SetStatus(codes.Error, "failed to get issue") 1826 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1827 return 1828 } ··· 1830 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1831 if err != nil { 1832 http.Error(w, "bad comment id", http.StatusBadRequest) 1833 + span.RecordError(err) 1834 + span.SetStatus(codes.Error, "failed to get comment") 1835 return 1836 } 1837 1838 + identity, err := s.resolver.ResolveIdent(ctx, comment.OwnerDid) 1839 if err != nil { 1840 log.Println("failed to resolve did") 1841 + span.RecordError(err) 1842 + span.SetStatus(codes.Error, "failed to resolve did") 1843 return 1844 } 1845 ··· 1852 1853 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1854 LoggedInUser: user, 1855 + RepoInfo: f.RepoInfo(ctx, s, user), 1856 DidHandleMap: didHandleMap, 1857 Issue: issue, 1858 Comment: comment, ··· 1860 } 1861 1862 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1863 + ctx, span := s.t.TraceStart(r.Context(), "EditIssueComment") 1864 + defer span.End() 1865 + 1866 user := s.auth.GetUser(r) 1867 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 1868 if err != nil { 1869 log.Println("failed to get repo and knot", err) 1870 + span.RecordError(err) 1871 + span.SetStatus(codes.Error, "failed to resolve repo") 1872 return 1873 } 1874 ··· 1877 if err != nil { 1878 http.Error(w, "bad issue id", http.StatusBadRequest) 1879 log.Println("failed to parse issue id", err) 1880 + span.RecordError(err) 1881 + span.SetStatus(codes.Error, "failed to parse issue id") 1882 return 1883 } 1884 ··· 1887 if err != nil { 1888 http.Error(w, "bad comment id", http.StatusBadRequest) 1889 log.Println("failed to parse issue id", err) 1890 + span.RecordError(err) 1891 + span.SetStatus(codes.Error, "failed to parse comment id") 1892 return 1893 } 1894 1895 + span.SetAttributes( 1896 + attribute.Int("issue_id", issueIdInt), 1897 + attribute.Int("comment_id", commentIdInt), 1898 + attribute.String("method", r.Method), 1899 + ) 1900 + 1901 + issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt) 1902 if err != nil { 1903 log.Println("failed to get issue", err) 1904 + span.RecordError(err) 1905 + span.SetStatus(codes.Error, "failed to get issue") 1906 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1907 return 1908 } ··· 1910 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1911 if err != nil { 1912 http.Error(w, "bad comment id", http.StatusBadRequest) 1913 + span.RecordError(err) 1914 + span.SetStatus(codes.Error, "failed to get comment") 1915 return 1916 } 1917 1918 if comment.OwnerDid != user.Did { 1919 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1920 + span.SetAttributes(attribute.Bool("permission_denied", true)) 1921 return 1922 } 1923 ··· 1925 case http.MethodGet: 1926 s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1927 LoggedInUser: user, 1928 + RepoInfo: f.RepoInfo(ctx, s, user), 1929 Issue: issue, 1930 Comment: comment, 1931 }) ··· 1935 client, _ := s.auth.AuthorizedClient(r) 1936 rkey := comment.Rkey 1937 1938 + span.SetAttributes( 1939 + attribute.String("new_body", newBody), 1940 + attribute.String("rkey", rkey), 1941 + ) 1942 + 1943 // optimistic update 1944 edited := time.Now() 1945 err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1946 if err != nil { 1947 log.Println("failed to perferom update-description query", err) 1948 + span.RecordError(err) 1949 + span.SetStatus(codes.Error, "failed to edit comment in database") 1950 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1951 return 1952 } ··· 1954 // rkey is optional, it was introduced later 1955 if comment.Rkey != "" { 1956 // update the record on pds 1957 + ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1958 if err != nil { 1959 // failed to get record 1960 log.Println(err, rkey) 1961 + span.RecordError(err) 1962 + span.SetStatus(codes.Error, "failed to get record from PDS") 1963 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1964 return 1965 } ··· 1971 createdAt := record["createdAt"].(string) 1972 commentIdInt64 := int64(commentIdInt) 1973 1974 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 1975 Collection: tangled.RepoIssueCommentNSID, 1976 Repo: user.Did, 1977 Rkey: rkey, ··· 1989 }) 1990 if err != nil { 1991 log.Println(err) 1992 + span.RecordError(err) 1993 + span.SetStatus(codes.Error, "failed to put record to PDS") 1994 } 1995 } 1996 ··· 2004 // return new comment body with htmx 2005 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 2006 LoggedInUser: user, 2007 + RepoInfo: f.RepoInfo(ctx, s, user), 2008 DidHandleMap: didHandleMap, 2009 Issue: issue, 2010 Comment: comment, 2011 }) 2012 return 2013 } 2014 } 2015 2016 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 2017 + ctx, span := s.t.TraceStart(r.Context(), "DeleteIssueComment") 2018 + defer span.End() 2019 + 2020 user := s.auth.GetUser(r) 2021 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2022 if err != nil { 2023 log.Println("failed to get repo and knot", err) 2024 + span.RecordError(err) 2025 + span.SetStatus(codes.Error, "failed to resolve repo") 2026 return 2027 } 2028 ··· 2031 if err != nil { 2032 http.Error(w, "bad issue id", http.StatusBadRequest) 2033 log.Println("failed to parse issue id", err) 2034 + span.RecordError(err) 2035 + span.SetStatus(codes.Error, "failed to parse issue id") 2036 return 2037 } 2038 2039 + issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt) 2040 if err != nil { 2041 log.Println("failed to get issue", err) 2042 + span.RecordError(err) 2043 + span.SetStatus(codes.Error, "failed to get issue") 2044 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 2045 return 2046 } ··· 2050 if err != nil { 2051 http.Error(w, "bad comment id", http.StatusBadRequest) 2052 log.Println("failed to parse issue id", err) 2053 + span.RecordError(err) 2054 + span.SetStatus(codes.Error, "failed to parse comment id") 2055 return 2056 } 2057 2058 + span.SetAttributes( 2059 + attribute.Int("issue_id", issueIdInt), 2060 + attribute.Int("comment_id", commentIdInt), 2061 + ) 2062 + 2063 comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 2064 if err != nil { 2065 http.Error(w, "bad comment id", http.StatusBadRequest) 2066 + span.RecordError(err) 2067 + span.SetStatus(codes.Error, "failed to get comment") 2068 return 2069 } 2070 2071 if comment.OwnerDid != user.Did { 2072 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 2073 + span.SetAttributes(attribute.Bool("permission_denied", true)) 2074 return 2075 } 2076 2077 if comment.Deleted != nil { 2078 http.Error(w, "comment already deleted", http.StatusBadRequest) 2079 + span.SetAttributes(attribute.Bool("already_deleted", true)) 2080 return 2081 } 2082 ··· 2085 err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 2086 if err != nil { 2087 log.Println("failed to delete comment") 2088 + span.RecordError(err) 2089 + span.SetStatus(codes.Error, "failed to delete comment in database") 2090 s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 2091 return 2092 } ··· 2094 // delete from pds 2095 if comment.Rkey != "" { 2096 client, _ := s.auth.AuthorizedClient(r) 2097 + _, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 2098 Collection: tangled.GraphFollowNSID, 2099 Repo: user.Did, 2100 Rkey: comment.Rkey, 2101 }) 2102 if err != nil { 2103 log.Println(err) 2104 + span.RecordError(err) 2105 + span.SetStatus(codes.Error, "failed to delete record from PDS") 2106 } 2107 } 2108 ··· 2116 // htmx fragment of comment after deletion 2117 s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 2118 LoggedInUser: user, 2119 + RepoInfo: f.RepoInfo(ctx, s, user), 2120 DidHandleMap: didHandleMap, 2121 Issue: issue, 2122 Comment: comment, ··· 2125 } 2126 2127 func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 2128 + ctx, span := s.t.TraceStart(r.Context(), "RepoIssues") 2129 + defer span.End() 2130 + 2131 params := r.URL.Query() 2132 state := params.Get("state") 2133 isOpen := true ··· 2140 isOpen = true 2141 } 2142 2143 + span.SetAttributes( 2144 + attribute.Bool("is_open", isOpen), 2145 + attribute.String("state_param", state), 2146 + ) 2147 + 2148 page, ok := r.Context().Value("page").(pagination.Page) 2149 if !ok { 2150 log.Println("failed to get page") 2151 + span.SetAttributes(attribute.Bool("page_not_found", true)) 2152 page = pagination.FirstPage() 2153 } 2154 2155 user := s.auth.GetUser(r) 2156 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2157 if err != nil { 2158 log.Println("failed to get repo and knot", err) 2159 + span.RecordError(err) 2160 + span.SetStatus(codes.Error, "failed to resolve repo") 2161 return 2162 } 2163 2164 + issues, err := db.GetIssues(ctx, s.db, f.RepoAt, isOpen, page) 2165 if err != nil { 2166 log.Println("failed to get issues", err) 2167 + span.RecordError(err) 2168 + span.SetStatus(codes.Error, "failed to get issues") 2169 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 2170 return 2171 } 2172 2173 + span.SetAttributes(attribute.Int("issues.count", len(issues))) 2174 + 2175 identsToResolve := make([]string, len(issues)) 2176 for i, issue := range issues { 2177 identsToResolve[i] = issue.OwnerDid 2178 } 2179 + resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve) 2180 didHandleMap := make(map[string]string) 2181 for _, identity := range resolvedIds { 2182 if !identity.Handle.IsInvalidHandle() { ··· 2188 2189 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 2190 LoggedInUser: s.auth.GetUser(r), 2191 + RepoInfo: f.RepoInfo(ctx, s, user), 2192 Issues: issues, 2193 DidHandleMap: didHandleMap, 2194 FilteringByOpen: isOpen, ··· 2198 } 2199 2200 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 2201 + ctx, span := s.t.TraceStart(r.Context(), "NewIssue") 2202 + defer span.End() 2203 + 2204 user := s.auth.GetUser(r) 2205 2206 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2207 if err != nil { 2208 log.Println("failed to get repo and knot", err) 2209 + span.RecordError(err) 2210 + span.SetStatus(codes.Error, "failed to resolve repo") 2211 return 2212 } 2213 + 2214 + span.SetAttributes(attribute.String("method", r.Method)) 2215 2216 switch r.Method { 2217 case http.MethodGet: 2218 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 2219 LoggedInUser: user, 2220 + RepoInfo: f.RepoInfo(ctx, s, user), 2221 }) 2222 case http.MethodPost: 2223 title := r.FormValue("title") 2224 body := r.FormValue("body") 2225 2226 + span.SetAttributes( 2227 + attribute.String("title", title), 2228 + attribute.String("body_length", fmt.Sprintf("%d", len(body))), 2229 + ) 2230 + 2231 if title == "" || body == "" { 2232 + span.SetAttributes(attribute.Bool("form_validation_failed", true)) 2233 s.pages.Notice(w, "issues", "Title and body are required") 2234 return 2235 } 2236 2237 + tx, err := s.db.BeginTx(ctx, nil) 2238 if err != nil { 2239 + span.RecordError(err) 2240 + span.SetStatus(codes.Error, "failed to begin transaction") 2241 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 2242 return 2243 } ··· 2250 }) 2251 if err != nil { 2252 log.Println("failed to create issue", err) 2253 + span.RecordError(err) 2254 + span.SetStatus(codes.Error, "failed to create issue in database") 2255 s.pages.Notice(w, "issues", "Failed to create issue.") 2256 return 2257 } ··· 2259 issueId, err := db.GetIssueId(s.db, f.RepoAt) 2260 if err != nil { 2261 log.Println("failed to get issue id", err) 2262 + span.RecordError(err) 2263 + span.SetStatus(codes.Error, "failed to get issue id") 2264 s.pages.Notice(w, "issues", "Failed to create issue.") 2265 return 2266 } 2267 2268 + span.SetAttributes(attribute.Int("issue_id", issueId)) 2269 + 2270 client, _ := s.auth.AuthorizedClient(r) 2271 atUri := f.RepoAt.String() 2272 + rkey := appview.TID() 2273 + span.SetAttributes(attribute.String("rkey", rkey)) 2274 + 2275 + resp, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 2276 Collection: tangled.RepoIssueNSID, 2277 Repo: user.Did, 2278 + Rkey: rkey, 2279 Record: &lexutil.LexiconTypeDecoder{ 2280 Val: &tangled.RepoIssue{ 2281 Repo: atUri, ··· 2288 }) 2289 if err != nil { 2290 log.Println("failed to create issue", err) 2291 + span.RecordError(err) 2292 + span.SetStatus(codes.Error, "failed to create issue in PDS") 2293 s.pages.Notice(w, "issues", "Failed to create issue.") 2294 return 2295 } 2296 2297 + span.SetAttributes(attribute.String("issue_uri", resp.Uri)) 2298 + 2299 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 2300 if err != nil { 2301 log.Println("failed to set issue at", err) 2302 + span.RecordError(err) 2303 + span.SetStatus(codes.Error, "failed to set issue URI in database") 2304 s.pages.Notice(w, "issues", "Failed to create issue.") 2305 return 2306 } ··· 2311 } 2312 2313 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 2314 + ctx, span := s.t.TraceStart(r.Context(), "ForkRepo") 2315 + defer span.End() 2316 + 2317 user := s.auth.GetUser(r) 2318 + f, err := s.fullyResolvedRepo(r.WithContext(ctx)) 2319 if err != nil { 2320 log.Printf("failed to resolve source repo: %v", err) 2321 + span.RecordError(err) 2322 + span.SetStatus(codes.Error, "failed to resolve source repo") 2323 return 2324 } 2325 2326 + span.SetAttributes( 2327 + attribute.String("method", r.Method), 2328 + attribute.String("repo_name", f.RepoName), 2329 + attribute.String("owner_did", f.OwnerDid()), 2330 + attribute.String("knot", f.Knot), 2331 + ) 2332 + 2333 switch r.Method { 2334 case http.MethodGet: 2335 user := s.auth.GetUser(r) 2336 knots, err := s.enforcer.GetDomainsForUser(user.Did) 2337 if err != nil { 2338 + span.RecordError(err) 2339 + span.SetStatus(codes.Error, "failed to get domains for user") 2340 s.pages.Notice(w, "repo", "Invalid user account.") 2341 return 2342 } 2343 2344 + span.SetAttributes(attribute.Int("knots.count", len(knots))) 2345 + 2346 s.pages.ForkRepo(w, pages.ForkRepoParams{ 2347 LoggedInUser: user, 2348 Knots: knots, 2349 + RepoInfo: f.RepoInfo(ctx, s, user), 2350 }) 2351 2352 case http.MethodPost: 2353 knot := r.FormValue("knot") 2354 if knot == "" { 2355 + span.SetAttributes(attribute.Bool("missing_knot", true)) 2356 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 2357 return 2358 } 2359 2360 + span.SetAttributes(attribute.String("target_knot", knot)) 2361 + 2362 ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 2363 if err != nil || !ok { 2364 + span.SetAttributes( 2365 + attribute.Bool("permission_denied", true), 2366 + attribute.Bool("enforce_error", err != nil), 2367 + ) 2368 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 2369 return 2370 } 2371 2372 forkName := fmt.Sprintf("%s", f.RepoName) 2373 + span.SetAttributes(attribute.String("fork_name", forkName)) 2374 2375 // this check is *only* to see if the forked repo name already exists 2376 // in the user's account. 2377 + existingRepo, err := db.GetRepo(ctx, s.db, user.Did, f.RepoName) 2378 if err != nil { 2379 if errors.Is(err, sql.ErrNoRows) { 2380 // no existing repo with this name found, we can use the name as is 2381 + span.SetAttributes(attribute.Bool("repo_name_available", true)) 2382 } else { 2383 log.Println("error fetching existing repo from db", err) 2384 + span.RecordError(err) 2385 + span.SetStatus(codes.Error, "failed to check for existing repo") 2386 s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 2387 return 2388 } 2389 } else if existingRepo != nil { 2390 // repo with this name already exists, append random string 2391 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 2392 + span.SetAttributes( 2393 + attribute.Bool("repo_name_conflict", true), 2394 + attribute.String("adjusted_fork_name", forkName), 2395 + ) 2396 } 2397 + 2398 secret, err := db.GetRegistrationKey(s.db, knot) 2399 if err != nil { 2400 + span.RecordError(err) 2401 + span.SetStatus(codes.Error, "failed to get registration key") 2402 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 2403 return 2404 } 2405 2406 client, err := NewSignedClient(knot, secret, s.config.Dev) 2407 if err != nil { 2408 + span.RecordError(err) 2409 + span.SetStatus(codes.Error, "failed to create signed client") 2410 s.pages.Notice(w, "repo", "Failed to reach knot server.") 2411 return 2412 } ··· 2420 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 2421 sourceAt := f.RepoAt.String() 2422 2423 + span.SetAttributes( 2424 + attribute.String("fork_source_url", forkSourceUrl), 2425 + attribute.String("source_at", sourceAt), 2426 + ) 2427 + 2428 rkey := appview.TID() 2429 repo := &db.Repo{ 2430 Did: user.Did, ··· 2434 Source: sourceAt, 2435 } 2436 2437 + span.SetAttributes(attribute.String("rkey", rkey)) 2438 + 2439 + tx, err := s.db.BeginTx(ctx, nil) 2440 if err != nil { 2441 log.Println(err) 2442 + span.RecordError(err) 2443 + span.SetStatus(codes.Error, "failed to begin transaction") 2444 s.pages.Notice(w, "repo", "Failed to save repository information.") 2445 return 2446 } ··· 2449 err = s.enforcer.E.LoadPolicy() 2450 if err != nil { 2451 log.Println("failed to rollback policies") 2452 + span.RecordError(err) 2453 } 2454 }() 2455 2456 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 2457 if err != nil { 2458 + span.RecordError(err) 2459 + span.SetStatus(codes.Error, "failed to fork repo on knot server") 2460 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 2461 return 2462 } 2463 + 2464 + span.SetAttributes(attribute.Int("fork_response_status", resp.StatusCode)) 2465 2466 switch resp.StatusCode { 2467 case http.StatusConflict: 2468 + span.SetAttributes(attribute.Bool("name_conflict", true)) 2469 s.pages.Notice(w, "repo", "A repository with that name already exists.") 2470 return 2471 case http.StatusInternalServerError: 2472 + span.SetAttributes(attribute.Bool("server_error", true)) 2473 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 2474 + return 2475 case http.StatusNoContent: 2476 // continue 2477 } ··· 2479 xrpcClient, _ := s.auth.AuthorizedClient(r) 2480 2481 createdAt := time.Now().Format(time.RFC3339) 2482 + atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{ 2483 Collection: tangled.RepoNSID, 2484 Repo: user.Did, 2485 Rkey: rkey, ··· 2494 }) 2495 if err != nil { 2496 log.Printf("failed to create record: %s", err) 2497 + span.RecordError(err) 2498 + span.SetStatus(codes.Error, "failed to create record in PDS") 2499 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 2500 return 2501 } 2502 log.Println("created repo record: ", atresp.Uri) 2503 + span.SetAttributes(attribute.String("repo_uri", atresp.Uri)) 2504 2505 repo.AtUri = atresp.Uri 2506 + err = db.AddRepo(ctx, tx, repo) 2507 if err != nil { 2508 log.Println(err) 2509 + span.RecordError(err) 2510 + span.SetStatus(codes.Error, "failed to add repo to database") 2511 s.pages.Notice(w, "repo", "Failed to save repository information.") 2512 return 2513 } ··· 2517 err = s.enforcer.AddRepo(user.Did, knot, p) 2518 if err != nil { 2519 log.Println(err) 2520 + span.RecordError(err) 2521 + span.SetStatus(codes.Error, "failed to set up repository permissions") 2522 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 2523 return 2524 } ··· 2526 err = tx.Commit() 2527 if err != nil { 2528 log.Println("failed to commit changes", err) 2529 + span.RecordError(err) 2530 + span.SetStatus(codes.Error, "failed to commit transaction") 2531 http.Error(w, err.Error(), http.StatusInternalServerError) 2532 return 2533 } ··· 2535 err = s.enforcer.E.SavePolicy() 2536 if err != nil { 2537 log.Println("failed to update ACLs", err) 2538 + span.RecordError(err) 2539 + span.SetStatus(codes.Error, "failed to save policy") 2540 http.Error(w, err.Error(), http.StatusInternalServerError) 2541 return 2542 }
+24 -6
appview/state/repo_util.go
··· 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/go-chi/chi/v5" 14 "github.com/go-git/go-git/v5/plumbing/object" 15 "tangled.sh/tangled.sh/core/appview/auth" 16 "tangled.sh/tangled.sh/core/appview/db" 17 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 18 ) 19 20 func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 21 repoName := chi.URLParam(r, "repo") 22 - knot, ok := r.Context().Value("knot").(string) 23 if !ok { 24 log.Println("malformed middleware") 25 return nil, fmt.Errorf("malformed middleware") 26 } 27 - id, ok := r.Context().Value("resolvedId").(identity.Identity) 28 if !ok { 29 log.Println("malformed middleware") 30 return nil, fmt.Errorf("malformed middleware") 31 } 32 33 - repoAt, ok := r.Context().Value("repoAt").(string) 34 if !ok { 35 log.Println("malformed middleware") 36 return nil, fmt.Errorf("malformed middleware") ··· 56 } 57 58 ref = defaultBranch.Branch 59 } 60 61 - // pass through values from the middleware 62 - description, ok := r.Context().Value("repoDescription").(string) 63 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 64 65 return &FullyResolvedRepo{ 66 Knot: knot,
··· 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/go-chi/chi/v5" 14 "github.com/go-git/go-git/v5/plumbing/object" 15 + "go.opentelemetry.io/otel/attribute" 16 "tangled.sh/tangled.sh/core/appview/auth" 17 "tangled.sh/tangled.sh/core/appview/db" 18 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 19 + "tangled.sh/tangled.sh/core/telemetry" 20 ) 21 22 func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 23 + ctx := r.Context() 24 + 25 + attrs := telemetry.MapAttrs(map[string]string{ 26 + "repo": chi.URLParam(r, "repo"), 27 + "ref": chi.URLParam(r, "ref"), 28 + }) 29 + 30 + ctx, span := s.t.TraceStart(ctx, "fullyResolvedRepo", attrs...) 31 + defer span.End() 32 + 33 repoName := chi.URLParam(r, "repo") 34 + knot, ok := ctx.Value("knot").(string) 35 if !ok { 36 log.Println("malformed middleware") 37 return nil, fmt.Errorf("malformed middleware") 38 } 39 + 40 + span.SetAttributes(attribute.String("knot", knot)) 41 + 42 + id, ok := ctx.Value("resolvedId").(identity.Identity) 43 if !ok { 44 log.Println("malformed middleware") 45 return nil, fmt.Errorf("malformed middleware") 46 } 47 48 + span.SetAttributes(attribute.String("did", id.DID.String())) 49 + 50 + repoAt, ok := ctx.Value("repoAt").(string) 51 if !ok { 52 log.Println("malformed middleware") 53 return nil, fmt.Errorf("malformed middleware") ··· 73 } 74 75 ref = defaultBranch.Branch 76 + 77 + span.SetAttributes(attribute.String("default_branch", ref)) 78 } 79 80 + description, ok := ctx.Value("repoDescription").(string) 81 + addedAt, ok := ctx.Value("repoAddedAt").(string) 82 83 return &FullyResolvedRepo{ 84 Knot: knot,
+48 -8
appview/state/state.go
··· 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 "tangled.sh/tangled.sh/core/api/tangled" 22 "tangled.sh/tangled.sh/core/appview" 23 "tangled.sh/tangled.sh/core/appview/auth" ··· 83 if err != nil { 84 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 85 } 86 - err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper)) 87 if err != nil { 88 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 89 } ··· 198 } 199 200 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 201 user := s.auth.GetUser(r) 202 203 - timeline, err := db.MakeTimeline(s.db) 204 if err != nil { 205 log.Println(err) 206 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 207 } 208 ··· 221 didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 222 } 223 } 224 225 - resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 226 didHandleMap := make(map[string]string) 227 for _, identity := range resolvedIds { 228 if !identity.Handle.IsInvalidHandle() { ··· 231 didHandleMap[identity.DID.String()] = identity.DID.String() 232 } 233 } 234 235 s.pages.Timeline(w, pages.TimelineParams{ 236 LoggedInUser: user, ··· 594 } 595 596 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 597 switch r.Method { 598 case http.MethodGet: 599 user := s.auth.GetUser(r) 600 knots, err := s.enforcer.GetDomainsForUser(user.Did) 601 if err != nil { 602 s.pages.Notice(w, "repo", "Invalid user account.") 603 return 604 } 605 606 s.pages.NewRepo(w, pages.NewRepoParams{ 607 LoggedInUser: user, ··· 610 611 case http.MethodPost: 612 user := s.auth.GetUser(r) 613 614 domain := r.FormValue("domain") 615 if domain == "" { 616 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 617 return 618 } 619 620 repoName := r.FormValue("name") 621 if repoName == "" { 622 s.pages.Notice(w, "repo", "Repository name cannot be empty.") 623 return 624 } 625 626 // Check for valid repository name (GitHub-like rules) 627 // No spaces, only alphanumeric characters, dashes, and underscores ··· 639 if defaultBranch == "" { 640 defaultBranch = "main" 641 } 642 643 description := r.FormValue("description") 644 645 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 646 if err != nil || !ok { 647 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 648 return 649 } 650 651 - existingRepo, err := db.GetRepo(s.db, user.Did, repoName) 652 if err == nil && existingRepo != nil { 653 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 654 return 655 } 656 657 secret, err := db.GetRegistrationKey(s.db, domain) 658 if err != nil { 659 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 660 return 661 } 662 663 client, err := NewSignedClient(domain, secret, s.config.Dev) 664 if err != nil { 665 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 666 return 667 } ··· 675 Description: description, 676 } 677 678 - xrpcClient, _ := s.auth.AuthorizedClient(r) 679 680 createdAt := time.Now().Format(time.RFC3339) 681 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 682 Collection: tangled.RepoNSID, 683 Repo: user.Did, 684 Rkey: rkey, ··· 691 }}, 692 }) 693 if err != nil { 694 log.Printf("failed to create record: %s", err) 695 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 696 return 697 } 698 log.Println("created repo record: ", atresp.Uri) 699 700 - tx, err := s.db.BeginTx(r.Context(), nil) 701 if err != nil { 702 log.Println(err) 703 s.pages.Notice(w, "repo", "Failed to save repository information.") 704 return ··· 713 714 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 715 if err != nil { 716 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 717 return 718 } 719 720 switch resp.StatusCode { 721 case http.StatusConflict: ··· 728 } 729 730 repo.AtUri = atresp.Uri 731 - err = db.AddRepo(tx, repo) 732 if err != nil { 733 log.Println(err) 734 s.pages.Notice(w, "repo", "Failed to save repository information.") 735 return ··· 739 p, _ := securejoin.SecureJoin(user.Did, repoName) 740 err = s.enforcer.AddRepo(user.Did, domain, p) 741 if err != nil { 742 log.Println(err) 743 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 744 return ··· 746 747 err = tx.Commit() 748 if err != nil { 749 log.Println("failed to commit changes", err) 750 http.Error(w, err.Error(), http.StatusInternalServerError) 751 return ··· 753 754 err = s.enforcer.E.SavePolicy() 755 if err != nil { 756 log.Println("failed to update ACLs", err) 757 http.Error(w, err.Error(), http.StatusInternalServerError) 758 return
··· 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 + "go.opentelemetry.io/otel/attribute" 22 "tangled.sh/tangled.sh/core/api/tangled" 23 "tangled.sh/tangled.sh/core/appview" 24 "tangled.sh/tangled.sh/core/appview/auth" ··· 84 if err != nil { 85 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 86 } 87 + err = jc.StartJetstream(ctx, appview.Ingest(wrapper)) 88 if err != nil { 89 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 90 } ··· 199 } 200 201 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 202 + ctx, span := s.t.TraceStart(r.Context(), "Timeline") 203 + defer span.End() 204 + 205 user := s.auth.GetUser(r) 206 + span.SetAttributes(attribute.String("user.did", user.Did)) 207 208 + timeline, err := db.MakeTimeline(ctx, s.db) 209 if err != nil { 210 log.Println(err) 211 + span.RecordError(err) 212 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 213 } 214 ··· 227 didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 228 } 229 } 230 + span.SetAttributes(attribute.Int("dids.to_resolve.count", len(didsToResolve))) 231 232 + resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve) 233 didHandleMap := make(map[string]string) 234 for _, identity := range resolvedIds { 235 if !identity.Handle.IsInvalidHandle() { ··· 238 didHandleMap[identity.DID.String()] = identity.DID.String() 239 } 240 } 241 + span.SetAttributes(attribute.Int("dids.resolved.count", len(resolvedIds))) 242 243 s.pages.Timeline(w, pages.TimelineParams{ 244 LoggedInUser: user, ··· 602 } 603 604 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 605 + ctx, span := s.t.TraceStart(r.Context(), "NewRepo") 606 + defer span.End() 607 + 608 switch r.Method { 609 case http.MethodGet: 610 user := s.auth.GetUser(r) 611 + span.SetAttributes(attribute.String("user.did", user.Did)) 612 + span.SetAttributes(attribute.String("request.method", "GET")) 613 + 614 knots, err := s.enforcer.GetDomainsForUser(user.Did) 615 if err != nil { 616 + span.RecordError(err) 617 s.pages.Notice(w, "repo", "Invalid user account.") 618 return 619 } 620 + span.SetAttributes(attribute.Int("knots.count", len(knots))) 621 622 s.pages.NewRepo(w, pages.NewRepoParams{ 623 LoggedInUser: user, ··· 626 627 case http.MethodPost: 628 user := s.auth.GetUser(r) 629 + span.SetAttributes(attribute.String("user.did", user.Did)) 630 + span.SetAttributes(attribute.String("request.method", "POST")) 631 632 domain := r.FormValue("domain") 633 if domain == "" { 634 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 635 return 636 } 637 + span.SetAttributes(attribute.String("domain", domain)) 638 639 repoName := r.FormValue("name") 640 if repoName == "" { 641 s.pages.Notice(w, "repo", "Repository name cannot be empty.") 642 return 643 } 644 + span.SetAttributes(attribute.String("repo.name", repoName)) 645 646 // Check for valid repository name (GitHub-like rules) 647 // No spaces, only alphanumeric characters, dashes, and underscores ··· 659 if defaultBranch == "" { 660 defaultBranch = "main" 661 } 662 + span.SetAttributes(attribute.String("repo.default_branch", defaultBranch)) 663 664 description := r.FormValue("description") 665 666 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 667 if err != nil || !ok { 668 + if err != nil { 669 + span.RecordError(err) 670 + } 671 + span.SetAttributes(attribute.Bool("permission.granted", false)) 672 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 673 return 674 } 675 + span.SetAttributes(attribute.Bool("permission.granted", true)) 676 677 + existingRepo, err := db.GetRepo(ctx, s.db, user.Did, repoName) 678 if err == nil && existingRepo != nil { 679 + span.SetAttributes(attribute.Bool("repo.exists", true)) 680 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 681 return 682 } 683 + span.SetAttributes(attribute.Bool("repo.exists", false)) 684 685 secret, err := db.GetRegistrationKey(s.db, domain) 686 if err != nil { 687 + span.RecordError(err) 688 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 689 return 690 } 691 692 client, err := NewSignedClient(domain, secret, s.config.Dev) 693 if err != nil { 694 + span.RecordError(err) 695 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 696 return 697 } ··· 705 Description: description, 706 } 707 708 + rWithCtx := r.WithContext(ctx) 709 + xrpcClient, _ := s.auth.AuthorizedClient(rWithCtx) 710 711 createdAt := time.Now().Format(time.RFC3339) 712 + atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{ 713 Collection: tangled.RepoNSID, 714 Repo: user.Did, 715 Rkey: rkey, ··· 722 }}, 723 }) 724 if err != nil { 725 + span.RecordError(err) 726 log.Printf("failed to create record: %s", err) 727 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 728 return 729 } 730 log.Println("created repo record: ", atresp.Uri) 731 + span.SetAttributes(attribute.String("repo.uri", atresp.Uri)) 732 733 + tx, err := s.db.BeginTx(ctx, nil) 734 if err != nil { 735 + span.RecordError(err) 736 log.Println(err) 737 s.pages.Notice(w, "repo", "Failed to save repository information.") 738 return ··· 747 748 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 749 if err != nil { 750 + span.RecordError(err) 751 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 752 return 753 } 754 + span.SetAttributes(attribute.Int("knot_response.status", resp.StatusCode)) 755 756 switch resp.StatusCode { 757 case http.StatusConflict: ··· 764 } 765 766 repo.AtUri = atresp.Uri 767 + err = db.AddRepo(ctx, tx, repo) 768 if err != nil { 769 + span.RecordError(err) 770 log.Println(err) 771 s.pages.Notice(w, "repo", "Failed to save repository information.") 772 return ··· 776 p, _ := securejoin.SecureJoin(user.Did, repoName) 777 err = s.enforcer.AddRepo(user.Did, domain, p) 778 if err != nil { 779 + span.RecordError(err) 780 log.Println(err) 781 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 782 return ··· 784 785 err = tx.Commit() 786 if err != nil { 787 + span.RecordError(err) 788 log.Println("failed to commit changes", err) 789 http.Error(w, err.Error(), http.StatusInternalServerError) 790 return ··· 792 793 err = s.enforcer.E.SavePolicy() 794 if err != nil { 795 + span.RecordError(err) 796 log.Println("failed to update ACLs", err) 797 http.Error(w, err.Error(), http.StatusInternalServerError) 798 return
+14 -4
telemetry/telemetry.go
··· 2 3 import ( 4 "context" 5 6 - "go.opentelemetry.io/otel" 7 otelmetric "go.opentelemetry.io/otel/metric" 8 "go.opentelemetry.io/otel/sdk/metric" 9 "go.opentelemetry.io/otel/sdk/resource" ··· 60 return t.tracer 61 } 62 63 - func (t *Telemetry) TraceStart(ctx context.Context, name string) (context.Context, oteltrace.Span) { 64 - tracer := otel.Tracer(t.serviceName) 65 - return tracer.Start(ctx, name) 66 }
··· 2 3 import ( 4 "context" 5 + "fmt" 6 7 + "go.opentelemetry.io/otel/attribute" 8 otelmetric "go.opentelemetry.io/otel/metric" 9 "go.opentelemetry.io/otel/sdk/metric" 10 "go.opentelemetry.io/otel/sdk/resource" ··· 61 return t.tracer 62 } 63 64 + func (t *Telemetry) TraceStart(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, oteltrace.Span) { 65 + ctx, span := t.tracer.Start(ctx, name) 66 + span.SetAttributes(attrs...) 67 + return ctx, span 68 + } 69 + 70 + func MapAttrs[T any](attrs map[string]T) []attribute.KeyValue { 71 + var result []attribute.KeyValue 72 + for k, v := range attrs { 73 + result = append(result, attribute.Key(k).String(fmt.Sprintf("%v", v))) 74 + } 75 + return result 76 }