-1
appview/config.go
-1
appview/config.go
···
17
CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"`
18
AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"`
19
AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"`
20
-
EnableTelemetry bool `env:"TANGLED_TELEMETRY_ENABLED, default=false"`
21
}
22
23
func LoadConfig(ctx context.Context) (*Config, error) {
+6
-31
appview/db/issues.go
+6
-31
appview/db/issues.go
···
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
···
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
+3
-29
appview/db/profile.go
+3
-29
appview/db/profile.go
···
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
}
···
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
}
+25
-111
appview/db/pulls.go
+25
-111
appview/db/pulls.go
···
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
···
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
+12
-114
appview/db/repos.go
+12
-114
appview/db/repos.go
···
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
···
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
+4
-5
appview/db/star.go
+4
-5
appview/db/star.go
···
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,
···
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,
+3
-28
appview/db/timeline.go
+3
-28
appview/db/timeline.go
···
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
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
-1
appview/state/artifact.go
+1
-1
appview/state/artifact.go
+13
-31
appview/state/middleware.go
+13
-31
appview/state/middleware.go
···
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
})
···
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
})
+5
-33
appview/state/profile.go
+5
-33
appview/state/profile.go
···
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{
···
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{
+125
-633
appview/state/pull.go
+125
-633
appview/state/pull.go
···
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
}
···
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
}
+95
-699
appview/state/repo.go
+95
-699
appview/state/repo.go
···
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
}
···
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
}
+6
-24
appview/state/repo_util.go
+6
-24
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
-
"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,
···
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,
+1
-6
appview/state/router.go
+1
-6
appview/state/router.go
···
13
func (s *State) Router() http.Handler {
14
router := chi.NewRouter()
15
16
-
if s.t != nil {
17
-
// top-level telemetry middleware
18
-
// router.Use(s.t.RequestDuration())
19
-
// router.Use(s.t.RequestInFlight())
20
-
}
21
-
22
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
23
pat := chi.URLParam(r, "*")
24
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
···
54
55
func (s *State) UserRouter() http.Handler {
56
r := chi.NewRouter()
57
// strip @ from user
58
r.Use(StripLeadingAt)
59
···
13
func (s *State) Router() http.Handler {
14
router := chi.NewRouter()
15
16
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
17
pat := chi.URLParam(r, "*")
18
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
···
48
49
func (s *State) UserRouter() http.Handler {
50
r := chi.NewRouter()
51
+
52
// strip @ from user
53
r.Use(StripLeadingAt)
54
+9
-69
appview/state/state.go
+9
-69
appview/state/state.go
···
9
"log"
10
"log/slog"
11
"net/http"
12
-
"runtime/debug"
13
"strings"
14
"time"
15
···
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"
···
26
"tangled.sh/tangled.sh/core/appview/pages"
27
"tangled.sh/tangled.sh/core/jetstream"
28
"tangled.sh/tangled.sh/core/rbac"
29
-
"tangled.sh/tangled.sh/core/telemetry"
30
)
31
32
type State struct {
···
37
pages *pages.Pages
38
resolver *appview.Resolver
39
jc *jetstream.JetstreamClient
40
-
t *telemetry.Telemetry
41
config *appview.Config
42
}
43
44
-
func Make(ctx context.Context, config *appview.Config) (*State, error) {
45
d, err := db.Make(config.DbPath)
46
if err != nil {
47
return nil, err
···
63
64
resolver := appview.NewResolver()
65
66
-
bi, ok := debug.ReadBuildInfo()
67
-
var version string
68
-
if ok {
69
-
version = bi.Main.Version
70
-
} else {
71
-
version = "v0.0.0-unknown"
72
-
}
73
-
74
wrapper := db.DbWrapper{d}
75
jc, err := jetstream.NewJetstreamClient(
76
config.JetstreamEndpoint,
···
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
}
91
92
-
var tele *telemetry.Telemetry
93
-
if config.EnableTelemetry {
94
-
tele, err = telemetry.NewTelemetry(ctx, "appview", version, config.Dev)
95
-
if err != nil {
96
-
return nil, fmt.Errorf("failed to setup telemetry: %w", err)
97
-
}
98
-
}
99
-
100
state := &State{
101
d,
102
auth,
···
105
pgs,
106
resolver,
107
jc,
108
-
tele,
109
config,
110
}
111
···
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,
···
634
}
635
636
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
637
-
ctx, span := s.t.TraceStart(r.Context(), "NewRepo")
638
-
defer span.End()
639
-
640
switch r.Method {
641
case http.MethodGet:
642
user := s.auth.GetUser(r)
643
-
span.SetAttributes(attribute.String("user.did", user.Did))
644
-
span.SetAttributes(attribute.String("request.method", "GET"))
645
-
646
knots, err := s.enforcer.GetDomainsForUser(user.Did)
647
if err != nil {
648
-
span.RecordError(err)
649
s.pages.Notice(w, "repo", "Invalid user account.")
650
return
651
}
652
-
span.SetAttributes(attribute.Int("knots.count", len(knots)))
653
654
s.pages.NewRepo(w, pages.NewRepoParams{
655
LoggedInUser: user,
···
658
659
case http.MethodPost:
660
user := s.auth.GetUser(r)
661
-
span.SetAttributes(attribute.String("user.did", user.Did))
662
-
span.SetAttributes(attribute.String("request.method", "POST"))
663
664
domain := r.FormValue("domain")
665
if domain == "" {
666
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
667
return
668
}
669
-
span.SetAttributes(attribute.String("domain", domain))
670
671
repoName := r.FormValue("name")
672
if repoName == "" {
673
s.pages.Notice(w, "repo", "Repository name cannot be empty.")
674
return
675
}
676
-
span.SetAttributes(attribute.String("repo.name", repoName))
677
678
if err := validateRepoName(repoName); err != nil {
679
s.pages.Notice(w, "repo", err.Error())
···
684
if defaultBranch == "" {
685
defaultBranch = "main"
686
}
687
-
span.SetAttributes(attribute.String("repo.default_branch", defaultBranch))
688
689
description := r.FormValue("description")
690
691
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
692
if err != nil || !ok {
693
-
if err != nil {
694
-
span.RecordError(err)
695
-
}
696
-
span.SetAttributes(attribute.Bool("permission.granted", false))
697
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
698
return
699
}
700
-
span.SetAttributes(attribute.Bool("permission.granted", true))
701
702
-
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, repoName)
703
if err == nil && existingRepo != nil {
704
-
span.SetAttributes(attribute.Bool("repo.exists", true))
705
s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
706
return
707
}
708
-
span.SetAttributes(attribute.Bool("repo.exists", false))
709
710
secret, err := db.GetRegistrationKey(s.db, domain)
711
if err != nil {
712
-
span.RecordError(err)
713
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
714
return
715
}
716
717
client, err := NewSignedClient(domain, secret, s.config.Dev)
718
if err != nil {
719
-
span.RecordError(err)
720
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
721
return
722
}
···
730
Description: description,
731
}
732
733
-
rWithCtx := r.WithContext(ctx)
734
-
xrpcClient, _ := s.auth.AuthorizedClient(rWithCtx)
735
736
createdAt := time.Now().Format(time.RFC3339)
737
-
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
738
Collection: tangled.RepoNSID,
739
Repo: user.Did,
740
Rkey: rkey,
···
747
}},
748
})
749
if err != nil {
750
-
span.RecordError(err)
751
log.Printf("failed to create record: %s", err)
752
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
753
return
754
}
755
log.Println("created repo record: ", atresp.Uri)
756
-
span.SetAttributes(attribute.String("repo.uri", atresp.Uri))
757
758
-
tx, err := s.db.BeginTx(ctx, nil)
759
if err != nil {
760
-
span.RecordError(err)
761
log.Println(err)
762
s.pages.Notice(w, "repo", "Failed to save repository information.")
763
return
···
772
773
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
774
if err != nil {
775
-
span.RecordError(err)
776
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
777
return
778
}
779
-
span.SetAttributes(attribute.Int("knot_response.status", resp.StatusCode))
780
781
switch resp.StatusCode {
782
case http.StatusConflict:
···
789
}
790
791
repo.AtUri = atresp.Uri
792
-
err = db.AddRepo(ctx, tx, repo)
793
if err != nil {
794
-
span.RecordError(err)
795
log.Println(err)
796
s.pages.Notice(w, "repo", "Failed to save repository information.")
797
return
···
801
p, _ := securejoin.SecureJoin(user.Did, repoName)
802
err = s.enforcer.AddRepo(user.Did, domain, p)
803
if err != nil {
804
-
span.RecordError(err)
805
log.Println(err)
806
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
807
return
···
809
810
err = tx.Commit()
811
if err != nil {
812
-
span.RecordError(err)
813
log.Println("failed to commit changes", err)
814
http.Error(w, err.Error(), http.StatusInternalServerError)
815
return
···
817
818
err = s.enforcer.E.SavePolicy()
819
if err != nil {
820
-
span.RecordError(err)
821
log.Println("failed to update ACLs", err)
822
http.Error(w, err.Error(), http.StatusInternalServerError)
823
return
···
9
"log"
10
"log/slog"
11
"net/http"
12
"strings"
13
"time"
14
···
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
securejoin "github.com/cyphar/filepath-securejoin"
19
"github.com/go-chi/chi/v5"
20
"tangled.sh/tangled.sh/core/api/tangled"
21
"tangled.sh/tangled.sh/core/appview"
22
"tangled.sh/tangled.sh/core/appview/auth"
···
24
"tangled.sh/tangled.sh/core/appview/pages"
25
"tangled.sh/tangled.sh/core/jetstream"
26
"tangled.sh/tangled.sh/core/rbac"
27
)
28
29
type State struct {
···
34
pages *pages.Pages
35
resolver *appview.Resolver
36
jc *jetstream.JetstreamClient
37
config *appview.Config
38
}
39
40
+
func Make(config *appview.Config) (*State, error) {
41
d, err := db.Make(config.DbPath)
42
if err != nil {
43
return nil, err
···
59
60
resolver := appview.NewResolver()
61
62
wrapper := db.DbWrapper{d}
63
jc, err := jetstream.NewJetstreamClient(
64
config.JetstreamEndpoint,
···
72
if err != nil {
73
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
74
}
75
+
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
76
if err != nil {
77
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
78
}
79
80
state := &State{
81
d,
82
auth,
···
85
pgs,
86
resolver,
87
jc,
88
config,
89
}
90
···
178
}
179
180
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
181
user := s.auth.GetUser(r)
182
183
+
timeline, err := db.MakeTimeline(s.db)
184
if err != nil {
185
log.Println(err)
186
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
187
}
188
···
201
didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did)
202
}
203
}
204
205
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
206
didHandleMap := make(map[string]string)
207
for _, identity := range resolvedIds {
208
if !identity.Handle.IsInvalidHandle() {
···
211
didHandleMap[identity.DID.String()] = identity.DID.String()
212
}
213
}
214
215
s.pages.Timeline(w, pages.TimelineParams{
216
LoggedInUser: user,
···
606
}
607
608
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
609
switch r.Method {
610
case http.MethodGet:
611
user := s.auth.GetUser(r)
612
knots, err := s.enforcer.GetDomainsForUser(user.Did)
613
if err != nil {
614
s.pages.Notice(w, "repo", "Invalid user account.")
615
return
616
}
617
618
s.pages.NewRepo(w, pages.NewRepoParams{
619
LoggedInUser: user,
···
622
623
case http.MethodPost:
624
user := s.auth.GetUser(r)
625
626
domain := r.FormValue("domain")
627
if domain == "" {
628
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
629
return
630
}
631
632
repoName := r.FormValue("name")
633
if repoName == "" {
634
s.pages.Notice(w, "repo", "Repository name cannot be empty.")
635
return
636
}
637
638
if err := validateRepoName(repoName); err != nil {
639
s.pages.Notice(w, "repo", err.Error())
···
644
if defaultBranch == "" {
645
defaultBranch = "main"
646
}
647
648
description := r.FormValue("description")
649
650
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
651
if err != nil || !ok {
652
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
653
return
654
}
655
656
+
existingRepo, err := db.GetRepo(s.db, user.Did, repoName)
657
if err == nil && existingRepo != nil {
658
s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
659
return
660
}
661
662
secret, err := db.GetRegistrationKey(s.db, domain)
663
if err != nil {
664
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
665
return
666
}
667
668
client, err := NewSignedClient(domain, secret, s.config.Dev)
669
if err != nil {
670
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
671
return
672
}
···
680
Description: description,
681
}
682
683
+
xrpcClient, _ := s.auth.AuthorizedClient(r)
684
685
createdAt := time.Now().Format(time.RFC3339)
686
+
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
687
Collection: tangled.RepoNSID,
688
Repo: user.Did,
689
Rkey: rkey,
···
696
}},
697
})
698
if err != nil {
699
log.Printf("failed to create record: %s", err)
700
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
701
return
702
}
703
log.Println("created repo record: ", atresp.Uri)
704
705
+
tx, err := s.db.BeginTx(r.Context(), nil)
706
if err != nil {
707
log.Println(err)
708
s.pages.Notice(w, "repo", "Failed to save repository information.")
709
return
···
718
719
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
720
if err != nil {
721
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
722
return
723
}
724
725
switch resp.StatusCode {
726
case http.StatusConflict:
···
733
}
734
735
repo.AtUri = atresp.Uri
736
+
err = db.AddRepo(tx, repo)
737
if err != nil {
738
log.Println(err)
739
s.pages.Notice(w, "repo", "Failed to save repository information.")
740
return
···
744
p, _ := securejoin.SecureJoin(user.Did, repoName)
745
err = s.enforcer.AddRepo(user.Did, domain, p)
746
if err != nil {
747
log.Println(err)
748
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
749
return
···
751
752
err = tx.Commit()
753
if err != nil {
754
log.Println("failed to commit changes", err)
755
http.Error(w, err.Error(), http.StatusInternalServerError)
756
return
···
758
759
err = s.enforcer.E.SavePolicy()
760
if err != nil {
761
log.Println("failed to update ACLs", err)
762
http.Error(w, err.Error(), http.StatusInternalServerError)
763
return
+2
-4
cmd/appview/main.go
+2
-4
cmd/appview/main.go
···
14
func main() {
15
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
16
17
-
ctx := context.Background()
18
-
19
-
c, err := appview.LoadConfig(ctx)
20
if err != nil {
21
log.Println("failed to load config", "error", err)
22
return
23
}
24
25
-
state, err := state.Make(ctx, c)
26
27
if err != nil {
28
log.Fatal(err)
···
14
func main() {
15
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
16
17
+
c, err := appview.LoadConfig(context.Background())
18
if err != nil {
19
log.Println("failed to load config", "error", err)
20
return
21
}
22
23
+
state, err := state.Make(c)
24
25
if err != nil {
26
log.Fatal(err)
+6
-21
go.mod
+6
-21
go.mod
···
26
github.com/sethvargo/go-envconfig v1.1.0
27
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
28
github.com/yuin/goldmark v1.4.13
29
-
go.opentelemetry.io/otel v1.35.0
30
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
31
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
32
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0
33
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0
34
-
go.opentelemetry.io/otel/metric v1.35.0
35
-
go.opentelemetry.io/otel/sdk v1.35.0
36
-
go.opentelemetry.io/otel/sdk/metric v1.35.0
37
-
go.opentelemetry.io/otel/trace v1.35.0
38
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
39
)
40
···
48
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
49
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
50
github.com/casbin/govaluate v1.3.0 // indirect
51
-
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
52
github.com/cespare/xxhash/v2 v2.3.0 // indirect
53
github.com/cloudflare/circl v1.6.0 // indirect
54
github.com/davecgh/go-spew v1.1.1 // indirect
···
57
github.com/felixge/httpsnoop v1.0.4 // indirect
58
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
59
github.com/go-git/go-billy/v5 v5.6.2 // indirect
60
-
github.com/go-logr/logr v1.4.2 // indirect
61
github.com/go-logr/stdr v1.2.2 // indirect
62
github.com/goccy/go-json v0.10.2 // indirect
63
github.com/gogo/protobuf v1.3.2 // indirect
64
github.com/gorilla/css v1.0.1 // indirect
65
github.com/gorilla/securecookie v1.1.2 // indirect
66
github.com/gorilla/websocket v1.5.1 // indirect
67
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
68
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
69
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
70
github.com/hashicorp/golang-lru v1.0.2 // indirect
···
110
github.com/xanzy/ssh-agent v0.3.3 // indirect
111
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
112
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
113
-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
114
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
115
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
116
-
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
117
go.uber.org/atomic v1.11.0 // indirect
118
go.uber.org/multierr v1.11.0 // indirect
119
go.uber.org/zap v1.26.0 // indirect
120
golang.org/x/crypto v0.37.0 // indirect
121
golang.org/x/net v0.39.0 // indirect
122
-
golang.org/x/sys v0.33.0 // indirect
123
-
golang.org/x/text v0.24.0 // indirect
124
golang.org/x/time v0.5.0 // indirect
125
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
126
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
127
-
google.golang.org/grpc v1.71.0 // indirect
128
-
google.golang.org/protobuf v1.36.5 // indirect
129
gopkg.in/warnings.v0 v0.1.2 // indirect
130
gopkg.in/yaml.v3 v3.0.1 // indirect
131
lukechampine.com/blake3 v1.2.1 // indirect
···
26
github.com/sethvargo/go-envconfig v1.1.0
27
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
28
github.com/yuin/goldmark v1.4.13
29
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
30
)
31
···
39
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
40
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
41
github.com/casbin/govaluate v1.3.0 // indirect
42
github.com/cespare/xxhash/v2 v2.3.0 // indirect
43
github.com/cloudflare/circl v1.6.0 // indirect
44
github.com/davecgh/go-spew v1.1.1 // indirect
···
47
github.com/felixge/httpsnoop v1.0.4 // indirect
48
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
49
github.com/go-git/go-billy/v5 v5.6.2 // indirect
50
+
github.com/go-logr/logr v1.4.1 // indirect
51
github.com/go-logr/stdr v1.2.2 // indirect
52
github.com/goccy/go-json v0.10.2 // indirect
53
github.com/gogo/protobuf v1.3.2 // indirect
54
github.com/gorilla/css v1.0.1 // indirect
55
github.com/gorilla/securecookie v1.1.2 // indirect
56
github.com/gorilla/websocket v1.5.1 // indirect
57
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
58
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
59
github.com/hashicorp/golang-lru v1.0.2 // indirect
···
99
github.com/xanzy/ssh-agent v0.3.3 // indirect
100
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
101
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
102
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
103
+
go.opentelemetry.io/otel v1.21.0 // indirect
104
+
go.opentelemetry.io/otel/metric v1.21.0 // indirect
105
+
go.opentelemetry.io/otel/trace v1.21.0 // indirect
106
go.uber.org/atomic v1.11.0 // indirect
107
go.uber.org/multierr v1.11.0 // indirect
108
go.uber.org/zap v1.26.0 // indirect
109
golang.org/x/crypto v0.37.0 // indirect
110
golang.org/x/net v0.39.0 // indirect
111
+
golang.org/x/sys v0.32.0 // indirect
112
golang.org/x/time v0.5.0 // indirect
113
+
google.golang.org/protobuf v1.34.2 // indirect
114
gopkg.in/warnings.v0 v0.1.2 // indirect
115
gopkg.in/yaml.v3 v3.0.1 // indirect
116
lukechampine.com/blake3 v1.2.1 // indirect
+18
-48
go.sum
+18
-48
go.sum
···
42
github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
43
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
44
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
45
-
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
46
-
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
47
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
48
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
49
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
···
84
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
85
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
86
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
87
-
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
88
-
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
89
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
90
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
91
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
95
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
96
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
97
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
98
-
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
99
-
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
100
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
101
-
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
102
-
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
103
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
104
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
105
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
···
115
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
116
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
117
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
118
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
119
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
120
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
121
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
122
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
233
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
234
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
235
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
236
-
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
237
-
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
238
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
239
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
240
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
···
274
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
275
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
276
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
277
-
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
278
-
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
279
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
280
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
281
-
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
282
-
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
283
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
284
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
285
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
286
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
287
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
288
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
289
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
290
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
291
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
292
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
293
-
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
294
-
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
295
-
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
296
-
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
297
-
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
298
-
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
299
-
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
300
-
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
301
-
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
302
-
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
303
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
304
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
305
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
306
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
307
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
308
-
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
309
-
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
310
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
311
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
312
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
381
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
382
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
383
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
384
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
385
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
386
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
387
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
388
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
···
419
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
420
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
421
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
422
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
423
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
424
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
425
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
426
-
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
427
-
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
428
-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
429
-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
430
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
431
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
432
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
···
42
github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
43
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
44
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
45
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
46
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
47
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
···
82
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
83
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
84
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
85
+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
86
+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
87
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
88
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
89
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
93
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
94
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
95
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
96
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
97
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
98
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
99
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
100
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
101
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
···
111
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
112
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
113
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
114
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
115
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
116
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
227
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
228
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
229
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
230
+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
231
+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
232
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
233
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
234
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
···
268
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
269
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
270
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
271
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
272
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
273
+
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
274
+
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
275
+
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
276
+
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
277
+
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
278
+
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
279
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
280
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
281
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
282
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
283
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
284
+
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
285
+
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
286
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
287
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
288
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
357
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
358
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
359
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
360
+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
361
+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
362
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
363
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
364
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
···
395
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
396
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
397
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
398
+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
399
+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
400
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
401
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
402
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-88
telemetry/middleware.go
-88
telemetry/middleware.go
···
1
-
package telemetry
2
-
3
-
import (
4
-
"fmt"
5
-
"net/http"
6
-
"time"
7
-
8
-
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
9
-
otelmetric "go.opentelemetry.io/otel/metric"
10
-
"go.opentelemetry.io/otel/semconv/v1.13.0/httpconv"
11
-
)
12
-
13
-
func (t *Telemetry) RequestDuration() func(next http.Handler) http.Handler {
14
-
const (
15
-
metricNameRequestDurationMs = "request_duration_millis"
16
-
metricUnitRequestDurationMs = "ms"
17
-
metricDescRequestDurationMs = "Measures the latency of HTTP requests processed by the server, in milliseconds."
18
-
)
19
-
histogram, err := t.meter.Int64Histogram(
20
-
metricNameRequestDurationMs,
21
-
otelmetric.WithDescription(metricDescRequestDurationMs),
22
-
otelmetric.WithUnit(metricUnitRequestDurationMs),
23
-
)
24
-
if err != nil {
25
-
panic(fmt.Sprintf("unable to create %s histogram: %v", metricNameRequestDurationMs, err))
26
-
}
27
-
28
-
return func(next http.Handler) http.Handler {
29
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30
-
// capture the start time of the request
31
-
startTime := time.Now()
32
-
33
-
// execute next http handler
34
-
next.ServeHTTP(w, r)
35
-
36
-
// record the request duration
37
-
duration := time.Since(startTime)
38
-
histogram.Record(
39
-
r.Context(),
40
-
int64(duration.Milliseconds()),
41
-
otelmetric.WithAttributes(
42
-
httpconv.ServerRequest(t.serviceName, r)...,
43
-
),
44
-
)
45
-
})
46
-
}
47
-
}
48
-
49
-
func (t *Telemetry) RequestInFlight() func(next http.Handler) http.Handler {
50
-
const (
51
-
metricNameRequestInFlight = "request_in_flight"
52
-
metricDescRequestInFlight = "Measures the number of concurrent HTTP requests being processed by the server."
53
-
metricUnitRequestInFlight = "1"
54
-
)
55
-
56
-
// counter to capture requests in flight
57
-
counter, err := t.meter.Int64UpDownCounter(
58
-
metricNameRequestInFlight,
59
-
otelmetric.WithDescription(metricDescRequestInFlight),
60
-
otelmetric.WithUnit(metricUnitRequestInFlight),
61
-
)
62
-
if err != nil {
63
-
panic(fmt.Sprintf("unable to create %s counter: %v", metricNameRequestInFlight, err))
64
-
}
65
-
66
-
return func(next http.Handler) http.Handler {
67
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68
-
attrs := otelmetric.WithAttributes(httpconv.ServerRequest(t.serviceName, r)...)
69
-
70
-
// increase the number of requests in flight
71
-
counter.Add(r.Context(), 1, attrs)
72
-
73
-
// execute next http handler
74
-
next.ServeHTTP(w, r)
75
-
76
-
// decrease the number of requests in flight
77
-
counter.Add(r.Context(), -1, attrs)
78
-
})
79
-
}
80
-
}
81
-
82
-
func (t *Telemetry) WithRouteTag() func(next http.Handler) http.Handler {
83
-
return func(next http.Handler) http.Handler {
84
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85
-
otelhttp.WithRouteTag(r.URL.Path, next)
86
-
})
87
-
}
88
-
}
···
-65
telemetry/provider.go
-65
telemetry/provider.go
···
1
-
package telemetry
2
-
3
-
import (
4
-
"context"
5
-
"fmt"
6
-
"time"
7
-
8
-
"go.opentelemetry.io/otel"
9
-
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
10
-
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
11
-
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
12
-
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
13
-
"go.opentelemetry.io/otel/sdk/metric"
14
-
"go.opentelemetry.io/otel/sdk/resource"
15
-
"go.opentelemetry.io/otel/sdk/trace"
16
-
)
17
-
18
-
func NewTracerProvider(ctx context.Context, res *resource.Resource, isDev bool) (*trace.TracerProvider, error) {
19
-
var exporter trace.SpanExporter
20
-
var err error
21
-
22
-
if isDev {
23
-
exporter, err = stdouttrace.New()
24
-
if err != nil {
25
-
return nil, fmt.Errorf("failed to create stdout trace exporter: %w", err)
26
-
}
27
-
} else {
28
-
exporter, err = otlptracegrpc.New(ctx)
29
-
if err != nil {
30
-
return nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err)
31
-
}
32
-
}
33
-
34
-
tp := trace.NewTracerProvider(
35
-
trace.WithBatcher(exporter, trace.WithBatchTimeout(1*time.Second)),
36
-
trace.WithResource(res),
37
-
)
38
-
otel.SetTracerProvider(tp)
39
-
40
-
return tp, nil
41
-
}
42
-
43
-
func NewMeterProvider(ctx context.Context, res *resource.Resource, isDev bool) (*metric.MeterProvider, error) {
44
-
var exporter metric.Exporter
45
-
var err error
46
-
47
-
if isDev {
48
-
exporter, err = stdoutmetric.New()
49
-
if err != nil {
50
-
return nil, fmt.Errorf("failed to create stdout metric exporter: %w", err)
51
-
}
52
-
} else {
53
-
exporter, err = otlpmetricgrpc.New(ctx)
54
-
if err != nil {
55
-
return nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err)
56
-
}
57
-
}
58
-
59
-
mp := metric.NewMeterProvider(
60
-
metric.WithReader(metric.NewPeriodicReader(exporter, metric.WithInterval(10*time.Second))),
61
-
metric.WithResource(res),
62
-
)
63
-
otel.SetMeterProvider(mp)
64
-
return mp, nil
65
-
}
···
-76
telemetry/telemetry.go
-76
telemetry/telemetry.go
···
1
-
package telemetry
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"
11
-
"go.opentelemetry.io/otel/sdk/trace"
12
-
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
13
-
oteltrace "go.opentelemetry.io/otel/trace"
14
-
)
15
-
16
-
type Telemetry struct {
17
-
tp *trace.TracerProvider
18
-
mp *metric.MeterProvider
19
-
20
-
meter otelmetric.Meter
21
-
tracer oteltrace.Tracer
22
-
23
-
serviceName string
24
-
serviceVersion string
25
-
}
26
-
27
-
func NewTelemetry(ctx context.Context, serviceName, serviceVersion string, isDev bool) (*Telemetry, error) {
28
-
res := resource.NewWithAttributes(
29
-
semconv.SchemaURL,
30
-
semconv.ServiceName(serviceName),
31
-
semconv.ServiceVersion(serviceVersion),
32
-
)
33
-
34
-
tp, err := NewTracerProvider(ctx, res, isDev)
35
-
if err != nil {
36
-
return nil, err
37
-
}
38
-
39
-
// mp, err := NewMeterProvider(ctx, res, isDev)
40
-
// if err != nil {
41
-
// return nil, err
42
-
// }
43
-
44
-
return &Telemetry{
45
-
tp: tp,
46
-
//mp: mp,
47
-
48
-
//meter: mp.Meter(serviceName),
49
-
tracer: tp.Tracer(serviceVersion),
50
-
51
-
serviceName: serviceName,
52
-
serviceVersion: serviceVersion,
53
-
}, nil
54
-
}
55
-
56
-
func (t *Telemetry) Meter() otelmetric.Meter {
57
-
return t.meter
58
-
}
59
-
60
-
func (t *Telemetry) Tracer() oteltrace.Tracer {
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
-
}
···