+31
-6
appview/db/issues.go
+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
+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
+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
+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
+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
+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
+1
-1
appview/state/artifact.go
+31
-13
appview/state/middleware.go
+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
+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
+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
+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
+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
+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
+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
}