forked from tangled.org/core
this repo has no description

Revert "telemetry: init telemetry package"

This reverts commit 44f2b1f562faf1f36be90385a699a60991db6b86.

-1
appview/config.go
···
CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"`
AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"`
AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"`
-
EnableTelemetry bool `env:"TANGLED_TELEMETRY_ENABLED, default=false"`
}
func LoadConfig(ctx context.Context) (*Config, error) {
···
CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"`
AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"`
AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"`
}
func LoadConfig(ctx context.Context) (*Config, error) {
+6 -31
appview/db/issues.go
···
package db
import (
-
"context"
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
-
"go.opentelemetry.io/otel"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/pagination"
)
···
return ownerDid, err
}
-
func GetIssues(ctx context.Context, e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetIssues")
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("repo_at", repoAt.String()),
-
attribute.Bool("is_open", isOpen),
-
attribute.Int("page.offset", page.Offset),
-
attribute.Int("page.limit", page.Limit),
-
)
-
var issues []Issue
openValue := 0
if isOpen {
openValue = 1
}
-
rows, err := e.QueryContext(
-
ctx,
`
with numbered_issue as (
select
···
body,
open,
comment_count
-
from
numbered_issue
-
where
row_num between ? and ?`,
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
var metadata IssueMetadata
err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
if err != nil {
-
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
-
span.RecordError(err)
return nil, err
}
issue.Created = createdTime
···
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
span.SetAttributes(attribute.Int("issues.count", len(issues)))
return issues, nil
}
···
return issues, nil
}
-
func GetIssue(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetIssue")
-
defer span.End()
-
query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
···
return &issue, nil
}
-
func GetIssueWithComments(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetIssueWithComments")
-
defer span.End()
-
query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
···
package db
import (
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.sh/tangled.sh/core/appview/pagination"
)
···
return ownerDid, err
}
+
func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
var issues []Issue
openValue := 0
if isOpen {
openValue = 1
}
+
rows, err := e.Query(
`
with numbered_issue as (
select
···
body,
open,
comment_count
+
from
numbered_issue
+
where
row_num between ? and ?`,
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
···
var metadata IssueMetadata
err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
if err != nil {
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
return nil, err
}
issue.Created = createdTime
···
}
if err := rows.Err(); err != nil {
return nil, err
}
return issues, nil
}
···
return issues, nil
}
+
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
···
return &issue, nil
}
+
func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
+3 -29
appview/db/profile.go
···
package db
import (
-
"context"
"fmt"
"time"
-
-
"go.opentelemetry.io/otel/attribute"
-
"go.opentelemetry.io/otel/codes"
-
"go.opentelemetry.io/otel/trace"
)
type RepoEvent struct {
···
const TimeframeMonths = 7
-
func MakeProfileTimeline(ctx context.Context, e Execer, forDid string) (*ProfileTimeline, error) {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("forDid", forDid),
-
)
-
timeline := ProfileTimeline{
ByMonth: make([]ByMonth, TimeframeMonths),
}
···
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error getting pulls by owner did")
return nil, fmt.Errorf("error getting pulls by owner did: %w", err)
}
-
span.SetAttributes(attribute.Int("pulls.count", len(pulls)))
-
// group pulls by month
for _, pull := range pulls {
pullMonth := pull.Created.Month()
···
issues, err := GetIssuesByOwnerDid(e, forDid, timeframe)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error getting issues by owner did")
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
}
-
span.SetAttributes(attribute.Int("issues.count", len(issues)))
-
for _, issue := range issues {
issueMonth := issue.Created.Month()
···
*items = append(*items, &issue)
}
-
repos, err := GetAllReposByDid(ctx, e, forDid)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error getting all repos by did")
return nil, fmt.Errorf("error getting all repos by did: %w", err)
}
-
-
span.SetAttributes(attribute.Int("repos.count", len(repos)))
for _, repo := range repos {
// TODO: get this in the original query; requires COALESCE because nullable
var sourceRepo *Repo
if repo.Source != "" {
-
sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error getting repo by at uri")
return nil, err
}
}
···
package db
import (
"fmt"
"time"
)
type RepoEvent struct {
···
const TimeframeMonths = 7
+
func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) {
timeline := ProfileTimeline{
ByMonth: make([]ByMonth, TimeframeMonths),
}
···
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
if err != nil {
return nil, fmt.Errorf("error getting pulls by owner did: %w", err)
}
// group pulls by month
for _, pull := range pulls {
pullMonth := pull.Created.Month()
···
issues, err := GetIssuesByOwnerDid(e, forDid, timeframe)
if err != nil {
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
}
for _, issue := range issues {
issueMonth := issue.Created.Month()
···
*items = append(*items, &issue)
}
+
repos, err := GetAllReposByDid(e, forDid)
if err != nil {
return nil, fmt.Errorf("error getting all repos by did: %w", err)
}
for _, repo := range repos {
// TODO: get this in the original query; requires COALESCE because nullable
var sourceRepo *Repo
if repo.Source != "" {
+
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
if err != nil {
return nil, err
}
}
+25 -111
appview/db/pulls.go
···
package db
import (
-
"context"
"database/sql"
"fmt"
"log"
···
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/bluesky-social/indigo/atproto/syntax"
-
"go.opentelemetry.io/otel/attribute"
-
"go.opentelemetry.io/otel/trace"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
···
return patches
}
-
func NewPull(ctx context.Context, tx *sql.Tx, pull *Pull) error {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("repo.at", pull.RepoAt.String()),
-
attribute.String("owner.did", pull.OwnerDid),
-
attribute.String("title", pull.Title),
-
attribute.String("target_branch", pull.TargetBranch),
-
)
-
span.AddEvent("creating new pull request")
-
defer tx.Rollback()
_, err := tx.Exec(`
···
values (?, 1)
`, pull.RepoAt)
if err != nil {
-
span.RecordError(err)
return err
}
···
returning next_pull_id - 1
`, pull.RepoAt).Scan(&nextId)
if err != nil {
-
span.RecordError(err)
return err
}
pull.PullId = nextId
pull.State = PullOpen
-
span.SetAttributes(attribute.Int("pull.id", pull.PullId))
-
span.AddEvent("assigned pull ID")
-
var sourceBranch, sourceRepoAt *string
if pull.PullSource != nil {
sourceBranch = &pull.PullSource.Branch
···
sourceRepoAt,
)
if err != nil {
-
span.RecordError(err)
return err
}
-
-
span.AddEvent("inserted pull record")
_, err = tx.Exec(`
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
values (?, ?, ?, ?, ?)
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
if err != nil {
-
span.RecordError(err)
return err
}
-
-
span.AddEvent("inserted initial pull submission")
if err := tx.Commit(); err != nil {
-
span.RecordError(err)
return err
}
-
span.AddEvent("transaction committed successfully")
return nil
}
-
func GetPullAt(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
-
pull, err := GetPull(ctx, e, repoAt, pullId)
if err != nil {
return "", err
}
···
return pullId - 1, err
}
-
func GetPulls(ctx context.Context, e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("repoAt", repoAt.String()),
-
attribute.String("state", state.String()),
-
)
-
span.AddEvent("querying pulls")
-
pulls := make(map[int]*Pull)
-
rows, err := e.QueryContext(ctx, `
select
owner_did,
pull_id,
···
where
repo_at = ? and state = ?`, repoAt, state)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
&sourceRepoAt,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
-
span.RecordError(err)
return nil, err
}
pull.Created = createdTime
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
-
span.RecordError(err)
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
···
pulls[pull.PullId] = &pull
}
-
span.AddEvent("querying pull submissions")
-
span.SetAttributes(attribute.Int("pull_count", len(pulls)))
-
// get latest round no. for each pull
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
submissionsQuery := fmt.Sprintf(`
···
args[idx] = p.PullId
idx += 1
}
-
submissionsRows, err := e.QueryContext(ctx, submissionsQuery, args...)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer submissionsRows.Close()
···
&s.RoundNumber,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
···
}
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
-
span.AddEvent("querying pull comments")
// get comment count on latest submission on each pull
inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
···
for _, p := range pulls {
args = append(args, p.Submissions[p.LastRoundNumber()].ID)
}
-
commentsRows, err := e.QueryContext(ctx, commentsQuery, args...)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer commentsRows.Close()
···
&pullId,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
if p, ok := pulls[pullId]; ok {
···
}
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
-
span.AddEvent("sorting pulls by date")
orderedByDate := []*Pull{}
for _, p := range pulls {
···
return orderedByDate[i].Created.After(orderedByDate[j].Created)
})
-
span.SetAttributes(attribute.Int("result_count", len(orderedByDate)))
return orderedByDate, nil
}
-
func GetPull(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
-
span.SetAttributes(attribute.String("repoAt", repoAt.String()), attribute.Int("pull.id", pullId))
-
span.AddEvent("query pull metadata")
-
query := `
select
owner_did,
···
where
repo_at = ? and pull_id = ?
`
-
row := e.QueryRowContext(ctx, query, repoAt, pullId)
var pull Pull
var createdAt string
···
&sourceRepoAt,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
-
span.RecordError(err)
return nil, err
}
pull.Created = createdTime
if sourceBranch.Valid {
pull.PullSource = &PullSource{
Branch: sourceBranch.String,
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
-
span.RecordError(err)
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
}
}
-
span.AddEvent("query submissions")
submissionsQuery := `
select
id, pull_id, repo_at, round_number, patch, created, source_rev
···
where
repo_at = ? and pull_id = ?
`
-
submissionsRows, err := e.QueryContext(ctx, submissionsQuery, repoAt, pullId)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer submissionsRows.Close()
···
&submissionSourceRev,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr)
if err != nil {
-
span.RecordError(err)
return nil, err
}
submission.Created = submissionCreatedTime
···
submissionsMap[submission.ID] = &submission
}
if err = submissionsRows.Close(); err != nil {
-
span.RecordError(err)
return nil, err
}
if len(submissionsMap) == 0 {
···
args = append(args, k)
}
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ")
-
-
span.AddEvent("query comments")
commentsQuery := fmt.Sprintf(`
select
id,
···
order by
created asc
`, inClause)
-
commentsRows, err := e.QueryContext(ctx, commentsQuery, args...)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer commentsRows.Close()
···
&commentCreatedStr,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr)
if err != nil {
-
span.RecordError(err)
return nil, err
}
comment.Created = commentCreatedTime
if submission, ok := submissionsMap[comment.SubmissionId]; ok {
submission.Comments = append(submission.Comments, comment)
}
}
if err = commentsRows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
if pull.PullSource != nil && pull.PullSource.RepoAt != nil {
-
span.AddEvent("query pull source repo")
-
pullSourceRepo, err := GetRepoByAtUri(ctx, e, pull.PullSource.RepoAt.String())
-
if err != nil {
-
span.RecordError(err)
-
log.Printf("failed to get repo by at uri: %v", err)
-
} else {
-
pull.PullSource.Repo = pullSourceRepo
}
}
···
return pulls, nil
}
-
func NewPullComment(ctx context.Context, e Execer, comment *PullComment) (int64, error) {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("repo.at", comment.RepoAt),
-
attribute.Int("pull.id", comment.PullId),
-
attribute.Int("submission.id", comment.SubmissionId),
-
attribute.String("owner.did", comment.OwnerDid),
-
)
-
span.AddEvent("inserting new pull comment")
-
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
-
res, err := e.ExecContext(
-
ctx,
query,
comment.OwnerDid,
comment.RepoAt,
···
comment.Body,
)
if err != nil {
-
span.RecordError(err)
return 0, err
}
i, err := res.LastInsertId()
if err != nil {
-
span.RecordError(err)
return 0, err
}
-
span.SetAttributes(attribute.Int64("comment.id", i))
-
span.AddEvent("pull comment created successfully")
return i, nil
}
···
package db
import (
"database/sql"
"fmt"
"log"
···
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
···
return patches
}
+
func NewPull(tx *sql.Tx, pull *Pull) error {
defer tx.Rollback()
_, err := tx.Exec(`
···
values (?, 1)
`, pull.RepoAt)
if err != nil {
return err
}
···
returning next_pull_id - 1
`, pull.RepoAt).Scan(&nextId)
if err != nil {
return err
}
pull.PullId = nextId
pull.State = PullOpen
var sourceBranch, sourceRepoAt *string
if pull.PullSource != nil {
sourceBranch = &pull.PullSource.Branch
···
sourceRepoAt,
)
if err != nil {
return err
}
_, err = tx.Exec(`
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
values (?, ?, ?, ?, ?)
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
+
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+
pull, err := GetPull(e, repoAt, pullId)
if err != nil {
return "", err
}
···
return pullId - 1, err
}
+
func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) {
pulls := make(map[int]*Pull)
+
rows, err := e.Query(`
select
owner_did,
pull_id,
···
where
repo_at = ? and state = ?`, repoAt, state)
if err != nil {
return nil, err
}
defer rows.Close()
···
&sourceRepoAt,
)
if err != nil {
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
return nil, err
}
pull.Created = createdTime
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
···
pulls[pull.PullId] = &pull
}
// get latest round no. for each pull
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
submissionsQuery := fmt.Sprintf(`
···
args[idx] = p.PullId
idx += 1
}
+
submissionsRows, err := e.Query(submissionsQuery, args...)
if err != nil {
return nil, err
}
defer submissionsRows.Close()
···
&s.RoundNumber,
)
if err != nil {
return nil, err
}
···
}
}
if err := rows.Err(); err != nil {
return nil, err
}
// get comment count on latest submission on each pull
inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
···
for _, p := range pulls {
args = append(args, p.Submissions[p.LastRoundNumber()].ID)
}
+
commentsRows, err := e.Query(commentsQuery, args...)
if err != nil {
return nil, err
}
defer commentsRows.Close()
···
&pullId,
)
if err != nil {
return nil, err
}
if p, ok := pulls[pullId]; ok {
···
}
}
if err := rows.Err(); err != nil {
return nil, err
}
orderedByDate := []*Pull{}
for _, p := range pulls {
···
return orderedByDate[i].Created.After(orderedByDate[j].Created)
})
return orderedByDate, nil
}
+
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
query := `
select
owner_did,
···
where
repo_at = ? and pull_id = ?
`
+
row := e.QueryRow(query, repoAt, pullId)
var pull Pull
var createdAt string
···
&sourceRepoAt,
)
if err != nil {
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
return nil, err
}
pull.Created = createdTime
+
// populate source
if sourceBranch.Valid {
pull.PullSource = &PullSource{
Branch: sourceBranch.String,
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
}
}
submissionsQuery := `
select
id, pull_id, repo_at, round_number, patch, created, source_rev
···
where
repo_at = ? and pull_id = ?
`
+
submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId)
if err != nil {
return nil, err
}
defer submissionsRows.Close()
···
&submissionSourceRev,
)
if err != nil {
return nil, err
}
submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr)
if err != nil {
return nil, err
}
submission.Created = submissionCreatedTime
···
submissionsMap[submission.ID] = &submission
}
if err = submissionsRows.Close(); err != nil {
return nil, err
}
if len(submissionsMap) == 0 {
···
args = append(args, k)
}
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ")
commentsQuery := fmt.Sprintf(`
select
id,
···
order by
created asc
`, inClause)
+
commentsRows, err := e.Query(commentsQuery, args...)
if err != nil {
return nil, err
}
defer commentsRows.Close()
···
&commentCreatedStr,
)
if err != nil {
return nil, err
}
commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr)
if err != nil {
return nil, err
}
comment.Created = commentCreatedTime
+
// Add the comment to its submission
if submission, ok := submissionsMap[comment.SubmissionId]; ok {
submission.Comments = append(submission.Comments, comment)
}
+
}
if err = commentsRows.Err(); err != nil {
return nil, err
}
+
var pullSourceRepo *Repo
+
if pull.PullSource != nil {
+
if pull.PullSource.RepoAt != nil {
+
pullSourceRepo, err = GetRepoByAtUri(e, pull.PullSource.RepoAt.String())
+
if err != nil {
+
log.Printf("failed to get repo by at uri: %v", err)
+
} else {
+
pull.PullSource.Repo = pullSourceRepo
+
}
}
}
···
return pulls, nil
}
+
func NewPullComment(e Execer, comment *PullComment) (int64, error) {
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
+
res, err := e.Exec(
query,
comment.OwnerDid,
comment.RepoAt,
···
comment.Body,
)
if err != nil {
return 0, err
}
i, err := res.LastInsertId()
if err != nil {
return 0, err
}
return i, nil
}
+12 -114
appview/db/repos.go
···
package db
import (
-
"context"
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
-
"go.opentelemetry.io/otel"
-
"go.opentelemetry.io/otel/attribute"
)
type Repo struct {
···
Source string
}
-
func GetAllRepos(ctx context.Context, e Execer, limit int) ([]Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetAllRepos")
-
defer span.End()
-
span.SetAttributes(attribute.Int("limit", limit))
-
var repos []Repo
rows, err := e.Query(
···
limit,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
-
func GetAllReposByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetAllReposByDid")
-
defer span.End()
-
span.SetAttributes(attribute.String("did", did))
-
var repos []Repo
rows, err := e.Query(
···
order by r.created desc`,
did)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource)
if err != nil {
-
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
-
func GetRepo(ctx context.Context, e Execer, did, name string) (*Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetRepo")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("did", did),
-
attribute.String("name", name),
-
)
-
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
-
span.RecordError(err)
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
-
func GetRepoByAtUri(ctx context.Context, e Execer, atUri string) (*Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetRepoByAtUri")
-
defer span.End()
-
span.SetAttributes(attribute.String("atUri", atUri))
-
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
-
span.RecordError(err)
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
-
func AddRepo(ctx context.Context, e Execer, repo *Repo) error {
-
ctx, span := otel.Tracer("db").Start(ctx, "AddRepo")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("did", repo.Did),
-
attribute.String("name", repo.Name),
-
)
-
_, err := e.Exec(
`insert into repos
(did, name, knot, rkey, at_uri, description, source)
values (?, ?, ?, ?, ?, ?, ?)`,
repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
)
-
if err != nil {
-
span.RecordError(err)
-
}
return err
}
-
func RemoveRepo(ctx context.Context, e Execer, did, name string) error {
-
ctx, span := otel.Tracer("db").Start(ctx, "RemoveRepo")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("did", did),
-
attribute.String("name", name),
-
)
-
_, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
-
if err != nil {
-
span.RecordError(err)
-
}
return err
}
-
func GetRepoSource(ctx context.Context, e Execer, repoAt syntax.ATURI) (string, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetRepoSource")
-
defer span.End()
-
span.SetAttributes(attribute.String("repoAt", repoAt.String()))
-
var nullableSource sql.NullString
err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
if err != nil {
-
span.RecordError(err)
return "", err
}
return nullableSource.String, nil
}
-
func GetForksByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetForksByDid")
-
defer span.End()
-
span.SetAttributes(attribute.String("did", did))
-
var repos []Repo
rows, err := e.Query(
···
did,
)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
-
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
span.SetAttributes(attribute.Int("forks.count", len(repos)))
return repos, nil
}
-
func GetForkByDid(ctx context.Context, e Execer, did string, name string) (*Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "GetForkByDid")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("did", did),
-
attribute.String("name", name),
-
)
-
var repo Repo
var createdAt string
var nullableDescription sql.NullString
···
err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
-
span.RecordError(err)
return nil, err
}
···
return &repo, nil
}
-
func AddCollaborator(ctx context.Context, e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
-
ctx, span := otel.Tracer("db").Start(ctx, "AddCollaborator")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("collaborator", collaborator),
-
attribute.String("repoOwnerDid", repoOwnerDid),
-
attribute.String("repoName", repoName),
-
)
-
_, err := e.Exec(
`insert into collaborators (did, repo)
values (?, (select id from repos where did = ? and name = ? and knot = ?));`,
collaborator, repoOwnerDid, repoName, repoKnot)
-
if err != nil {
-
span.RecordError(err)
-
}
return err
}
-
func UpdateDescription(ctx context.Context, e Execer, repoAt, newDescription string) error {
-
ctx, span := otel.Tracer("db").Start(ctx, "UpdateDescription")
-
defer span.End()
-
span.SetAttributes(
-
attribute.String("repoAt", repoAt),
-
attribute.String("description", newDescription),
-
)
-
_, err := e.Exec(
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
-
if err != nil {
-
span.RecordError(err)
-
}
return err
}
-
func CollaboratingIn(ctx context.Context, e Execer, collaborator string) ([]Repo, error) {
-
ctx, span := otel.Tracer("db").Start(ctx, "CollaboratingIn")
-
defer span.End()
-
span.SetAttributes(attribute.String("collaborator", collaborator))
-
var repos []Repo
rows, err := e.Query(
···
group by
r.id;`, collaborator)
if err != nil {
-
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount)
if err != nil {
-
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
-
span.RecordError(err)
return nil, err
}
-
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
···
package db
import (
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
)
type Repo struct {
···
Source string
}
+
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
var repos []Repo
rows, err := e.Query(
···
limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
···
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
)
if err != nil {
return nil, err
}
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
return nil, err
}
return repos, nil
}
+
func GetAllReposByDid(e Execer, did string) ([]Repo, error) {
var repos []Repo
rows, err := e.Query(
···
order by r.created desc`,
did)
if err != nil {
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource)
if err != nil {
return nil, err
}
···
}
if err := rows.Err(); err != nil {
return nil, err
}
return repos, nil
}
+
func GetRepo(e Execer, did, name string) (*Repo, error) {
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
+
func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) {
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
+
func AddRepo(e Execer, repo *Repo) error {
_, err := e.Exec(
`insert into repos
(did, name, knot, rkey, at_uri, description, source)
values (?, ?, ?, ?, ?, ?, ?)`,
repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
)
return err
}
+
func RemoveRepo(e Execer, did, name string) error {
_, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
return err
}
+
func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
var nullableSource sql.NullString
err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
if err != nil {
return "", err
}
return nullableSource.String, nil
}
+
func GetForksByDid(e Execer, did string) ([]Repo, error) {
var repos []Repo
rows, err := e.Query(
···
did,
)
if err != nil {
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
return nil, err
}
···
}
if err := rows.Err(); err != nil {
return nil, err
}
return repos, nil
}
+
func GetForkByDid(e Execer, did string, name string) (*Repo, error) {
var repo Repo
var createdAt string
var nullableDescription sql.NullString
···
err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
return nil, err
}
···
return &repo, nil
}
+
func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
_, err := e.Exec(
`insert into collaborators (did, repo)
values (?, (select id from repos where did = ? and name = ? and knot = ?));`,
collaborator, repoOwnerDid, repoName, repoKnot)
return err
}
+
func UpdateDescription(e Execer, repoAt, newDescription string) error {
_, err := e.Exec(
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
return err
}
+
func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) {
var repos []Repo
rows, err := e.Query(
···
group by
r.id;`, collaborator)
if err != nil {
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount)
if err != nil {
return nil, err
}
···
}
if err := rows.Err(); err != nil {
return nil, err
}
return repos, nil
}
+4 -5
appview/db/star.go
···
package db
import (
-
"context"
"log"
"time"
···
Repo *Repo
}
-
func (star *Star) ResolveRepo(ctx context.Context, e Execer) error {
if star.Repo != nil {
return nil
}
-
repo, err := GetRepoByAtUri(ctx, e, star.RepoAt.String())
if err != nil {
return err
}
···
// Get a star record
func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
query := `
-
select starred_by_did, repo_at, created, rkey
from stars
where starred_by_did = ? and repo_at = ?`
row := e.QueryRow(query, starredByDid, repoAt)
···
var stars []Star
rows, err := e.Query(`
-
select
s.starred_by_did,
s.repo_at,
s.rkey,
···
package db
import (
"log"
"time"
···
Repo *Repo
}
+
func (star *Star) ResolveRepo(e Execer) error {
if star.Repo != nil {
return nil
}
+
repo, err := GetRepoByAtUri(e, star.RepoAt.String())
if err != nil {
return err
}
···
// Get a star record
func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
query := `
+
select starred_by_did, repo_at, created, rkey
from stars
where starred_by_did = ? and repo_at = ?`
row := e.QueryRow(query, starredByDid, repoAt)
···
var stars []Star
rows, err := e.Query(`
+
select
s.starred_by_did,
s.repo_at,
s.rkey,
+3 -28
appview/db/timeline.go
···
package db
import (
-
"context"
"sort"
"time"
-
-
"go.opentelemetry.io/otel/attribute"
-
"go.opentelemetry.io/otel/trace"
)
type TimelineEvent struct {
···
// TODO: this gathers heterogenous events from different sources and aggregates
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
-
func MakeTimeline(ctx context.Context, e Execer) ([]TimelineEvent, error) {
-
span := trace.SpanFromContext(ctx)
-
defer span.End()
-
var events []TimelineEvent
limit := 50
-
span.SetAttributes(attribute.Int("timeline.limit", limit))
-
-
repos, err := GetAllRepos(ctx, e, limit)
if err != nil {
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.from", "GetAllRepos"))
return nil, err
}
-
span.SetAttributes(attribute.Int("timeline.repos.count", len(repos)))
follows, err := GetAllFollows(e, limit)
if err != nil {
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.from", "GetAllFollows"))
return nil, err
}
-
span.SetAttributes(attribute.Int("timeline.follows.count", len(follows)))
stars, err := GetAllStars(e, limit)
if err != nil {
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.from", "GetAllStars"))
return nil, err
}
-
span.SetAttributes(attribute.Int("timeline.stars.count", len(stars)))
for _, repo := range repos {
var sourceRepo *Repo
if repo.Source != "" {
-
sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source)
if err != nil {
-
span.RecordError(err)
-
span.SetAttributes(
-
attribute.String("error.from", "GetRepoByAtUri"),
-
attribute.String("repo.source", repo.Source),
-
)
return nil, err
}
}
···
if len(events) > limit {
events = events[:limit]
}
-
-
span.SetAttributes(attribute.Int("timeline.events.total", len(events)))
return events, nil
}
···
package db
import (
"sort"
"time"
)
type TimelineEvent struct {
···
// TODO: this gathers heterogenous events from different sources and aggregates
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
+
func MakeTimeline(e Execer) ([]TimelineEvent, error) {
var events []TimelineEvent
limit := 50
+
repos, err := GetAllRepos(e, limit)
if err != nil {
return nil, err
}
follows, err := GetAllFollows(e, limit)
if err != nil {
return nil, err
}
stars, err := GetAllStars(e, limit)
if err != nil {
return nil, err
}
for _, repo := range repos {
var sourceRepo *Repo
if repo.Source != "" {
+
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
if err != nil {
return nil, err
}
}
···
if len(events) > limit {
events = events[:limit]
}
return events, nil
}
+1 -1
appview/state/artifact.go
···
s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(r.Context(), s, user),
Artifact: artifact,
})
}
···
s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Artifact: artifact,
})
}
+13 -31
appview/state/middleware.go
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/middleware"
)
···
func knotRoleMiddleware(s *State, group string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "knotRoleMiddleware")
-
defer span.End()
-
// requires auth also
-
actor := s.auth.GetUser(r.WithContext(ctx))
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
···
return
}
-
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
···
func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoPermissionMiddleware")
-
defer span.End()
-
// requires auth also
-
actor := s.auth.GetUser(r.WithContext(ctx))
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
http.Error(w, "Forbiden", http.StatusUnauthorized)
return
}
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
http.Error(w, "malformed url", http.StatusBadRequest)
return
···
return
}
-
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
···
return
}
-
ctx, span := s.t.TraceStart(req.Context(), "ResolveIdent")
-
defer span.End()
-
-
id, err := s.resolver.ResolveIdent(ctx, didOrHandle)
if err != nil {
// invalid did or handle
log.Println("failed to resolve did/handle:", err)
···
return
}
-
ctx = context.WithValue(ctx, "resolvedId", *id)
next.ServeHTTP(w, req.WithContext(ctx))
})
···
func ResolveRepo(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-
ctx, span := s.t.TraceStart(req.Context(), "ResolveRepo")
-
defer span.End()
-
repoName := chi.URLParam(req, "repo")
-
id, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
w.WriteHeader(http.StatusInternalServerError)
return
}
-
repo, err := db.GetRepo(ctx, s.db, id.DID.String(), repoName)
if err != nil {
// invalid did or handle
log.Println("failed to resolve repo")
···
return
}
-
ctx = context.WithValue(ctx, "knot", repo.Knot)
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
···
func ResolvePull(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ResolvePull")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
http.Error(w, "invalid repo url", http.StatusNotFound)
···
return
}
-
pr, err := db.GetPull(ctx, s.db, f.RepoAt, prIdInt)
if err != nil {
log.Println("failed to get pull and comments", err)
return
}
-
span.SetAttributes(attribute.Int("pull.id", prIdInt))
-
-
ctx = context.WithValue(ctx, "pull", pr)
next.ServeHTTP(w, r.WithContext(ctx))
})
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/middleware"
)
···
func knotRoleMiddleware(s *State, group string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// requires auth also
+
actor := s.auth.GetUser(r)
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
···
return
}
+
next.ServeHTTP(w, r)
})
}
}
···
func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// requires auth also
+
actor := s.auth.GetUser(r)
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
http.Error(w, "Forbiden", http.StatusUnauthorized)
return
}
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
http.Error(w, "malformed url", http.StatusBadRequest)
return
···
return
}
+
next.ServeHTTP(w, r)
})
}
}
···
return
}
+
id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle)
if err != nil {
// invalid did or handle
log.Println("failed to resolve did/handle:", err)
···
return
}
+
ctx := context.WithValue(req.Context(), "resolvedId", *id)
next.ServeHTTP(w, req.WithContext(ctx))
})
···
func ResolveRepo(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
repoName := chi.URLParam(req, "repo")
+
id, ok := req.Context().Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
w.WriteHeader(http.StatusInternalServerError)
return
}
+
repo, err := db.GetRepo(s.db, id.DID.String(), repoName)
if err != nil {
// invalid did or handle
log.Println("failed to resolve repo")
···
return
}
+
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
···
func ResolvePull(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
http.Error(w, "invalid repo url", http.StatusNotFound)
···
return
}
+
pr, err := db.GetPull(s.db, f.RepoAt, prIdInt)
if err != nil {
log.Println("failed to get pull and comments", err)
return
}
+
ctx := context.WithValue(r.Context(), "pull", pr)
next.ServeHTTP(w, r.WithContext(ctx))
})
+5 -33
appview/state/profile.go
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
)
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ProfilePage")
-
defer span.End()
-
didOrHandle := chi.URLParam(r, "user")
if didOrHandle == "" {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
-
ident, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
s.pages.Error404(w)
-
span.RecordError(fmt.Errorf("failed to resolve identity"))
return
}
-
span.SetAttributes(
-
attribute.String("user.did", ident.DID.String()),
-
attribute.String("user.handle", ident.Handle.String()),
-
)
-
-
repos, err := db.GetAllReposByDid(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.repos", err.Error()))
}
-
span.SetAttributes(attribute.Int("repos.count", len(repos)))
-
collaboratingRepos, err := db.CollaboratingIn(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.collaborating_repos", err.Error()))
}
-
span.SetAttributes(attribute.Int("collaborating_repos.count", len(collaboratingRepos)))
-
timeline, err := db.MakeProfileTimeline(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.timeline", err.Error()))
}
var didsToResolve []string
···
}
}
}
-
span.SetAttributes(attribute.Int("dids_to_resolve.count", len(didsToResolve)))
-
resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
span.SetAttributes(attribute.Int("resolved_ids.count", len(resolvedIds)))
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
if err != nil {
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error.follow_stats", err.Error()))
}
-
span.SetAttributes(
-
attribute.Int("followers.count", followers),
-
attribute.Int("following.count", following),
-
)
loggedInUser := s.auth.GetUser(r)
followStatus := db.IsNotFollowing
if loggedInUser != nil {
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
-
span.SetAttributes(attribute.String("logged_in_user.did", loggedInUser.Did))
}
-
span.SetAttributes(attribute.String("follow_status", string(db.FollowStatus(followStatus))))
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
)
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
didOrHandle := chi.URLParam(r, "user")
if didOrHandle == "" {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
if !ok {
s.pages.Error404(w)
return
}
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
if err != nil {
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
}
+
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
if err != nil {
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
}
+
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
if err != nil {
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
}
var didsToResolve []string
···
}
}
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
if err != nil {
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
}
loggedInUser := s.auth.GetUser(r)
followStatus := db.IsNotFollowing
if loggedInUser != nil {
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
}
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
+125 -633
appview/state/pull.go
···
package state
import (
-
"context"
"database/sql"
"encoding/json"
"errors"
···
"strconv"
"time"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/patchutil"
-
"tangled.sh/tangled.sh/core/telemetry"
"tangled.sh/tangled.sh/core/types"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
// htmx fragment
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "PullActions")
-
defer span.End()
-
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
return
}
-
_, mergeSpan := s.t.TraceStart(ctx, "mergeCheck")
-
mergeCheckResponse := s.mergeCheck(ctx, f, pull)
-
mergeSpan.End()
-
resubmitResult := pages.Unknown
if user.Did == pull.OwnerDid {
-
_, resubmitSpan := s.t.TraceStart(ctx, "resubmitCheck")
-
resubmitResult = s.resubmitCheck(ctx, f, pull)
-
resubmitSpan.End()
}
-
_, renderSpan := s.t.TraceStart(ctx, "renderPullActions")
s.pages.PullActionsFragment(w, pages.PullActionsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
RoundNumber: roundNumber,
MergeCheck: mergeCheckResponse,
ResubmitCheck: resubmitResult,
})
-
renderSpan.End()
return
}
}
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoSinglePull")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
-
err := errors.New("failed to get pull from context")
-
log.Println(err)
-
span.RecordError(err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
attrs := telemetry.MapAttrs[string](map[string]string{
-
"pull.id": fmt.Sprintf("%d", pull.PullId),
-
"pull.owner": pull.OwnerDid,
-
})
-
-
span.SetAttributes(attrs...)
-
totalIdents := 1
for _, submission := range pull.Submissions {
totalIdents += len(submission.Comments)
···
}
}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds)))
-
mergeCheckResponse := s.mergeCheck(ctx, f, pull)
-
resubmitResult := pages.Unknown
if user != nil && user.Did == pull.OwnerDid {
-
resubmitResult = s.resubmitCheck(ctx, f, pull)
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Pull: pull,
MergeCheck: mergeCheckResponse,
···
})
}
-
func (s *State) mergeCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse {
if pull.State == db.PullMerged {
return types.MergeCheckResponse{}
}
···
return mergeCheckResponse
}
-
func (s *State) resubmitCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult {
-
ctx, span := s.t.TraceStart(ctx, "resubmitCheck")
-
defer span.End()
-
-
span.SetAttributes(attribute.Int("pull.id", pull.PullId))
-
if pull.State == db.PullMerged || pull.PullSource == nil {
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
···
if pull.PullSource.RepoAt != nil {
// fork-based pulls
-
span.SetAttributes(attribute.Bool("isForkBased", true))
-
sourceRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_get_source_repo"))
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
···
repoName = sourceRepo.Name
} else {
// pulls within the same repo
-
span.SetAttributes(attribute.Bool("isBranchBased", true))
knot = f.Knot
ownerDid = f.OwnerDid()
repoName = f.RepoName
}
-
span.SetAttributes(
-
attribute.String("knot", knot),
-
attribute.String("ownerDid", ownerDid),
-
attribute.String("repoName", repoName),
-
attribute.String("sourceBranch", pull.PullSource.Branch),
-
)
-
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_setup_client"))
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver"))
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_read_response"))
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
defer resp.Body.Close()
···
var result types.RepoBranchResponse
if err := json.Unmarshal(body, &result); err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_parse_response"))
-
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
latestSubmission := pull.Submissions[pull.LastRoundNumber()]
-
-
span.SetAttributes(
-
attribute.String("latestSubmission.SourceRev", latestSubmission.SourceRev),
-
attribute.String("branch.Hash", result.Branch.Hash),
-
)
-
if latestSubmission.SourceRev != result.Branch.Hash {
fmt.Println(latestSubmission.SourceRev, result.Branch.Hash)
-
span.SetAttributes(attribute.String("result", "ShouldResubmit"))
return pages.ShouldResubmit
}
-
span.SetAttributes(attribute.String("result", "ShouldNotResubmit"))
return pages.ShouldNotResubmit
}
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatch")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
-
err := errors.New("failed to get pull from context")
-
log.Println(err)
-
span.RecordError(err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
···
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "bad_round_id"))
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.Int("round", roundIdInt),
-
attribute.String("pull.owner", pull.OwnerDid),
-
)
-
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds)))
diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch)
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
LoggedInUser: user,
DidHandleMap: didHandleMap,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
Round: roundIdInt,
Submission: pull.Submissions[roundIdInt],
Diff: &diff,
})
}
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoPullInterdiff")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to get pull.")
return
}
-
_, roundSpan := s.t.TraceStart(ctx, "parseRound")
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
-
roundSpan.End()
return
}
if roundIdInt == 0 {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("cannot interdiff initial submission")
-
roundSpan.End()
return
}
-
roundSpan.End()
-
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
identSpan.End()
-
_, diffSpan := s.t.TraceStart(ctx, "calculateInterdiff")
currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch)
if err != nil {
log.Println("failed to interdiff; current patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
-
diffSpan.End()
return
}
···
if err != nil {
log.Println("failed to interdiff; previous patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
-
diffSpan.End()
return
}
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
-
diffSpan.End()
-
_, renderSpan := s.t.TraceStart(ctx, "renderInterdiffPage")
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
-
LoggedInUser: s.auth.GetUser(r.WithContext(ctx)),
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
Round: roundIdInt,
DidHandleMap: didHandleMap,
Interdiff: interdiff,
})
-
renderSpan.End()
return
}
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatchRaw")
-
defer span.End()
-
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
_, roundSpan := s.t.TraceStart(ctx, "parseRound")
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
-
roundSpan.End()
return
}
-
roundSpan.End()
-
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
identSpan.End()
-
_, writeSpan := s.t.TraceStart(ctx, "writePatch")
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
-
writeSpan.End()
}
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoPulls")
-
defer span.End()
-
user := s.auth.GetUser(r)
params := r.URL.Query()
-
_, stateSpan := s.t.TraceStart(ctx, "determinePullState")
state := db.PullOpen
switch params.Get("state") {
case "closed":
···
case "merged":
state = db.PullMerged
}
-
stateSpan.End()
-
_, repoSpan := s.t.TraceStart(ctx, "resolveRepo")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
repoSpan.End()
return
}
-
repoSpan.End()
-
_, pullsSpan := s.t.TraceStart(ctx, "getPulls")
-
pulls, err := db.GetPulls(ctx, s.db, f.RepoAt, state)
if err != nil {
log.Println("failed to get pulls", err)
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
-
pullsSpan.End()
return
}
-
pullsSpan.End()
-
_, sourceRepoSpan := s.t.TraceStart(ctx, "resolvePullSources")
for _, p := range pulls {
var pullSourceRepo *db.Repo
if p.PullSource != nil {
if p.PullSource.RepoAt != nil {
-
pullSourceRepo, err = db.GetRepoByAtUri(ctx, s.db, p.PullSource.RepoAt.String())
if err != nil {
log.Printf("failed to get repo by at uri: %v", err)
continue
···
}
}
}
-
sourceRepoSpan.End()
-
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := make([]string, len(pulls))
for i, pull := range pulls {
identsToResolve[i] = pull.OwnerDid
}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
identSpan.End()
-
_, renderSpan := s.t.TraceStart(ctx, "renderPullsPage")
s.pages.RepoPulls(w, pages.RepoPullsParams{
-
LoggedInUser: s.auth.GetUser(r.WithContext(ctx)),
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pulls: pulls,
DidHandleMap: didHandleMap,
FilteringBy: state,
})
-
renderSpan.End()
return
}
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "PullComment")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
_, roundSpan := s.t.TraceStart(ctx, "parseRoundNumber")
roundNumberStr := chi.URLParam(r, "round")
roundNumber, err := strconv.Atoi(roundNumberStr)
if err != nil || roundNumber >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
-
roundSpan.End()
return
}
-
roundSpan.End()
switch r.Method {
case http.MethodGet:
-
_, renderSpan := s.t.TraceStart(ctx, "renderCommentFragment")
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
RoundNumber: roundNumber,
})
-
renderSpan.End()
return
case http.MethodPost:
-
postCtx, postSpan := s.t.TraceStart(ctx, "CreateComment")
-
defer postSpan.End()
-
-
_, validateSpan := s.t.TraceStart(postCtx, "validateComment")
body := r.FormValue("body")
if body == "" {
s.pages.Notice(w, "pull", "Comment body is required")
-
validateSpan.End()
return
}
-
validateSpan.End()
// Start a transaction
-
_, txSpan := s.t.TraceStart(postCtx, "startTransaction")
-
tx, err := s.db.BeginTx(postCtx, nil)
if err != nil {
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
-
txSpan.End()
return
}
defer tx.Rollback()
-
txSpan.End()
createdAt := time.Now().Format(time.RFC3339)
ownerDid := user.Did
-
_, pullAtSpan := s.t.TraceStart(postCtx, "getPullAt")
-
pullAt, err := db.GetPullAt(postCtx, s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to get pull at", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
-
pullAtSpan.End()
return
}
-
pullAtSpan.End()
-
_, atProtoSpan := s.t.TraceStart(postCtx, "createAtProtoRecord")
atUri := f.RepoAt.String()
-
client, _ := s.auth.AuthorizedClient(r.WithContext(postCtx))
-
atResp, err := comatproto.RepoPutRecord(postCtx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullCommentNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
-
atProtoSpan.End()
return
}
-
atProtoSpan.End()
// Create the pull comment in the database with the commentAt field
-
_, dbSpan := s.t.TraceStart(postCtx, "createDbComment")
-
commentId, err := db.NewPullComment(postCtx, tx, &db.PullComment{
OwnerDid: user.Did,
RepoAt: f.RepoAt.String(),
PullId: pull.PullId,
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
-
dbSpan.End()
return
}
-
dbSpan.End()
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
···
}
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewPull")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
switch r.Method {
case http.MethodGet:
-
span.SetAttributes(attribute.String("method", "GET"))
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
span.RecordError(err)
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
return
}
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Branches: result.Branches,
})
case http.MethodPost:
-
span.SetAttributes(attribute.String("method", "POST"))
-
title := r.FormValue("title")
body := r.FormValue("body")
targetBranch := r.FormValue("targetBranch")
fromFork := r.FormValue("fork")
sourceBranch := r.FormValue("sourceBranch")
patch := r.FormValue("patch")
-
-
span.SetAttributes(
-
attribute.String("targetBranch", targetBranch),
-
attribute.String("sourceBranch", sourceBranch),
-
attribute.Bool("hasFork", fromFork != ""),
-
attribute.Bool("hasPatch", patch != ""),
-
)
if targetBranch == "" {
s.pages.Notice(w, "pull", "Target branch is required.")
-
span.SetAttributes(attribute.String("error", "missing_target_branch"))
return
}
// Determine PR type based on input parameters
-
isPushAllowed := f.RepoInfo(ctx, s, user).Roles.IsPushAllowed()
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
isForkBased := fromFork != "" && sourceBranch != ""
isPatchBased := patch != "" && !isBranchBased && !isForkBased
-
span.SetAttributes(
-
attribute.Bool("isPushAllowed", isPushAllowed),
-
attribute.Bool("isBranchBased", isBranchBased),
-
attribute.Bool("isForkBased", isForkBased),
-
attribute.Bool("isPatchBased", isPatchBased),
-
)
-
if isPatchBased && !patchutil.IsFormatPatch(patch) {
if title == "" {
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
-
span.SetAttributes(attribute.String("error", "missing_title_for_git_diff"))
return
}
}
···
// Validate we have at least one valid PR creation method
if !isBranchBased && !isPatchBased && !isForkBased {
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
-
span.SetAttributes(attribute.String("error", "no_valid_pr_method"))
return
}
// Can't mix branch-based and patch-based approaches
if isBranchBased && patch != "" {
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
-
span.SetAttributes(attribute.String("error", "mixed_pr_methods"))
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
-
span.RecordError(err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
···
caps, err := us.Capabilities()
if err != nil {
log.Println("error fetching knot caps", f.Knot, err)
-
span.RecordError(err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Bool("caps.pullRequests.formatPatch", caps.PullRequests.FormatPatch),
-
attribute.Bool("caps.pullRequests.branchSubmissions", caps.PullRequests.BranchSubmissions),
-
attribute.Bool("caps.pullRequests.forkSubmissions", caps.PullRequests.ForkSubmissions),
-
attribute.Bool("caps.pullRequests.patchSubmissions", caps.PullRequests.PatchSubmissions),
-
)
-
if !caps.PullRequests.FormatPatch {
s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.")
-
span.SetAttributes(attribute.String("error", "formatpatch_not_supported"))
return
}
···
if isBranchBased {
if !caps.PullRequests.BranchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
-
span.SetAttributes(attribute.String("error", "branch_submissions_not_supported"))
return
}
-
s.handleBranchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, sourceBranch)
} else if isForkBased {
if !caps.PullRequests.ForkSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
-
span.SetAttributes(attribute.String("error", "fork_submissions_not_supported"))
return
}
-
s.handleForkBasedPull(w, r.WithContext(ctx), f, user, fromFork, title, body, targetBranch, sourceBranch)
} else if isPatchBased {
if !caps.PullRequests.PatchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
-
span.SetAttributes(attribute.String("error", "patch_submissions_not_supported"))
return
}
-
s.handlePatchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch)
}
return
}
}
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) {
-
ctx, span := s.t.TraceStart(r.Context(), "handleBranchBasedPull")
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("targetBranch", targetBranch),
-
attribute.String("sourceBranch", sourceBranch),
-
)
-
pullSource := &db.PullSource{
Branch: sourceBranch,
}
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
if err != nil {
log.Println("failed to compare", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "comparison_failed"))
s.pages.Notice(w, "pull", err.Error())
return
}
···
sourceRev := comparison.Rev2
patch := comparison.Patch
-
span.SetAttributes(attribute.String("sourceRev", sourceRev))
-
if !patchutil.IsPatchValid(patch) {
-
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
-
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
}
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) {
-
ctx, span := s.t.TraceStart(r.Context(), "handlePatchBasedPull")
-
defer span.End()
-
-
span.SetAttributes(attribute.String("targetBranch", targetBranch))
-
if !patchutil.IsPatchValid(patch) {
-
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
-
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, "", nil, nil)
}
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
-
ctx, span := s.t.TraceStart(r.Context(), "handleForkBasedPull")
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("forkRepo", forkRepo),
-
attribute.String("targetBranch", targetBranch),
-
attribute.String("sourceBranch", sourceBranch),
-
)
-
-
fork, err := db.GetForkByDid(ctx, s.db, user.Did, forkRepo)
if errors.Is(err, sql.ErrNoRows) {
-
span.SetAttributes(attribute.String("error", "fork_not_found"))
s.pages.Notice(w, "pull", "No such fork.")
return
} else if err != nil {
log.Println("failed to fetch fork:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "fork_fetch_failed"))
s.pages.Notice(w, "pull", "Failed to fetch fork.")
return
}
···
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
if err != nil {
log.Println("failed to fetch registration key:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "registration_key_fetch_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create signed client:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "signed_client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
us, err := NewUnsignedClient(fork.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "unsigned_client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
if err != nil {
log.Println("failed to create hidden ref:", err, resp.StatusCode)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "hidden_ref_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
switch resp.StatusCode {
case 404:
-
span.SetAttributes(attribute.String("error", "not_found_status"))
case 400:
-
span.SetAttributes(attribute.String("error", "bad_request_status"))
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
return
}
hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)
-
span.SetAttributes(attribute.String("hiddenRef", hiddenRef))
-
// We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking
// the targetBranch on the target repository. This code is a bit confusing, but here's an example:
// hiddenRef: hidden/feature-1/main (on repo-fork)
···
comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
if err != nil {
log.Println("failed to compare across branches", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "branch_comparison_failed"))
s.pages.Notice(w, "pull", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
-
span.SetAttributes(attribute.String("sourceRev", sourceRev))
if !patchutil.IsPatchValid(patch) {
-
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
···
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
if err != nil {
log.Println("failed to parse fork AT URI", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "fork_aturi_parse_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
Branch: sourceBranch,
RepoAt: &forkAtUri,
}, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri})
···
pullSource *db.PullSource,
recordPullSource *tangled.RepoPull_Source,
) {
-
ctx, span := s.t.TraceStart(r.Context(), "createPullRequest")
-
defer span.End()
-
-
span.SetAttributes(
-
attribute.String("targetBranch", targetBranch),
-
attribute.String("sourceRev", sourceRev),
-
attribute.Bool("hasPullSource", pullSource != nil),
-
)
-
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
if title == "" {
formatPatches, err := patchutil.ExtractPatches(patch)
if err != nil {
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "extract_patches_failed"))
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
return
}
if len(formatPatches) == 0 {
-
span.SetAttributes(attribute.String("error", "no_patches_found"))
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
return
}
title = formatPatches[0].Title
body = formatPatches[0].Body
-
span.SetAttributes(
-
attribute.Bool("title_extracted", true),
-
attribute.Bool("body_extracted", formatPatches[0].Body != ""),
-
)
}
rkey := appview.TID()
···
Patch: patch,
SourceRev: sourceRev,
}
-
err = db.NewPull(ctx, tx, &db.Pull{
Title: title,
Body: body,
TargetBranch: targetBranch,
···
})
if err != nil {
log.Println("failed to create pull request", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "db_create_pull_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
-
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
pullId, err := db.NextPullId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get pull id", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "get_pull_id_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
span.SetAttributes(attribute.Int("pullId", pullId))
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: rkey,
···
if err != nil {
log.Println("failed to create pull request", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "atproto_create_record_failed"))
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
-
if err = tx.Commit(); err != nil {
-
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
}
func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ValidatePatch")
-
defer span.End()
-
-
_, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
patch := r.FormValue("patch")
-
span.SetAttributes(attribute.Bool("hasPatch", patch != ""))
-
if patch == "" {
-
span.SetAttributes(attribute.String("error", "empty_patch"))
s.pages.Notice(w, "patch-error", "Patch is required.")
return
}
-
if !patchutil.IsPatchValid(patch) {
-
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")
return
}
-
isFormatPatch := patchutil.IsFormatPatch(patch)
-
span.SetAttributes(attribute.Bool("isFormatPatch", isFormatPatch))
-
-
if isFormatPatch {
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.")
} else {
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
···
}
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "PatchUploadFragment")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
})
}
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "CompareBranchesFragment")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "knotserver_connection_failed"))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "response_read_failed"))
return
}
-
defer resp.Body.Close()
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "response_parse_failed"))
return
}
-
span.SetAttributes(attribute.Int("branches.count", len(result.Branches)))
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
Branches: result.Branches,
})
}
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "CompareForksFragment")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
-
forks, err := db.GetForksByDid(ctx, s.db, user.Did)
if err != nil {
log.Println("failed to get forks", err)
-
span.RecordError(err)
return
}
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
Forks: forks,
})
}
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "CompareForksBranchesFragment")
-
defer span.End()
-
user := s.auth.GetUser(r.WithContext(ctx))
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
forkVal := r.URL.Query().Get("fork")
-
span.SetAttributes(attribute.String("fork", forkVal))
// fork repo
-
repo, err := db.GetRepo(ctx, s.db, user.Did, forkVal)
if err != nil {
log.Println("failed to get repo", user.Did, forkVal)
-
span.RecordError(err)
return
}
sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", repo.Knot)
-
span.RecordError(err)
s.pages.Error503(w)
return
}
···
sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name)
if err != nil {
log.Println("failed to reach knotserver for source branches", err)
-
span.RecordError(err)
return
}
sourceBody, err := io.ReadAll(sourceResp.Body)
if err != nil {
log.Println("failed to read source response body", err)
-
span.RecordError(err)
return
}
defer sourceResp.Body.Close()
···
err = json.Unmarshal(sourceBody, &sourceResult)
if err != nil {
log.Println("failed to parse source branches response:", err)
-
span.RecordError(err)
return
}
targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
-
span.RecordError(err)
s.pages.Error503(w)
return
}
···
targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver for target branches", err)
-
span.RecordError(err)
return
}
targetBody, err := io.ReadAll(targetResp.Body)
if err != nil {
log.Println("failed to read target response body", err)
-
span.RecordError(err)
return
}
defer targetResp.Body.Close()
···
err = json.Unmarshal(targetBody, &targetResult)
if err != nil {
log.Println("failed to parse target branches response:", err)
-
span.RecordError(err)
return
}
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
SourceBranches: sourceResult.Branches,
TargetBranches: targetResult.Branches,
})
}
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ResubmitPull")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.RecordError(errors.New("failed to get pull from context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("method", r.Method),
-
)
-
switch r.Method {
case http.MethodGet:
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
})
return
case http.MethodPost:
if pull.IsPatchBased() {
-
span.SetAttributes(attribute.String("pull.type", "patch_based"))
-
s.resubmitPatch(w, r.WithContext(ctx))
return
} else if pull.IsBranchBased() {
-
span.SetAttributes(attribute.String("pull.type", "branch_based"))
-
s.resubmitBranch(w, r.WithContext(ctx))
return
} else if pull.IsForkBased() {
-
span.SetAttributes(attribute.String("pull.type", "fork_based"))
-
s.resubmitFork(w, r.WithContext(ctx))
return
}
-
span.SetAttributes(attribute.String("pull.type", "unknown"))
}
}
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "resubmitPatch")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.RecordError(errors.New("failed to get pull from context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
)
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
-
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
}
patch := r.FormValue("patch")
-
span.SetAttributes(attribute.Bool("has_patch", patch != ""))
if err = validateResubmittedPatch(pull, patch); err != nil {
-
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, "")
if err != nil {
log.Println("failed to resubmit pull request", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
return
}
-
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "resubmitBranch")
-
defer span.End()
-
user := s.auth.GetUser(r.WithContext(ctx))
-
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.RecordError(errors.New("failed to get pull from context"))
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("pull.source_branch", pull.PullSource.Branch),
-
attribute.String("pull.target_branch", pull.TargetBranch),
-
)
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
-
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
}
-
if !f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() {
log.Println("unauthorized user")
-
span.SetAttributes(attribute.String("error", "push_not_allowed"))
w.WriteHeader(http.StatusUnauthorized)
return
}
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
if err != nil {
log.Printf("compare request failed: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "compare_failed"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
-
span.SetAttributes(attribute.String("source_rev", sourceRev))
if err = validateResubmittedPatch(pull, patch); err != nil {
-
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
-
span.SetAttributes(attribute.String("error", "no_changes"))
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
-
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
···
recordPullSource := &tangled.RepoPull_Source{
Branch: pull.PullSource.Branch,
}
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "resubmitFork")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.RecordError(errors.New("failed to get pull from context"))
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("pull.source_branch", pull.PullSource.Branch),
-
attribute.String("pull.target_branch", pull.TargetBranch),
-
)
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
-
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
}
-
forkRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "source_repo_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.String("fork.knot", forkRepo.Knot),
-
attribute.String("fork.did", forkRepo.Did),
-
attribute.String("fork.name", forkRepo.Name),
-
)
-
// extract patch by performing compare
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
if err != nil {
log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "reg_key_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "signed_client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
if err != nil || resp.StatusCode != http.StatusNoContent {
log.Printf("failed to update tracking branch: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "hidden_ref_update_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)
-
span.SetAttributes(attribute.String("hidden_ref", hiddenRef))
-
comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
if err != nil {
log.Printf("failed to compare branches: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "compare_failed"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
-
span.SetAttributes(attribute.String("source_rev", sourceRev))
if err = validateResubmittedPatch(pull, patch); err != nil {
-
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
-
span.SetAttributes(attribute.String("error", "no_changes"))
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
-
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
···
Branch: pull.PullSource.Branch,
Repo: &repoAt,
}
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "MergePull")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to resolve repo:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("target_branch", pull.TargetBranch),
-
)
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "reg_key_not_found"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
-
ident, err := s.resolver.ResolveIdent(ctx, pull.OwnerDid)
if err != nil {
log.Printf("resolving identity: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_identity_failed"))
w.WriteHeader(http.StatusNotFound)
return
}
···
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
if err != nil {
log.Printf("failed to get primary email: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "get_email_failed"))
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
···
resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address)
if err != nil {
log.Printf("failed to merge pull request: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "merge_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
-
span.SetAttributes(attribute.Int("response.status", resp.StatusCode))
-
if resp.StatusCode == http.StatusOK {
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Printf("failed to update pull request status in database: %s", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "db_update_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
} else {
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
-
span.SetAttributes(attribute.String("error", "non_ok_response"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
}
}
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ClosePull")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("malformed middleware")
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("user.did", user.Did),
-
)
-
// auth filter: only owner or collaborators can close
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
isCloseAllowed := isCollaborator || isPullAuthor
-
-
span.SetAttributes(
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_pull_author", isPullAuthor),
-
attribute.Bool("is_close_allowed", isCloseAllowed),
-
)
-
if !isCloseAllowed {
log.Println("failed to close pull")
-
span.SetAttributes(attribute.String("error", "unauthorized"))
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
return
}
// Start a transaction
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start transaction", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to close pull", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "db_close_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
}
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ReopenPull")
-
defer span.End()
-
-
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to resolve repo", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
-
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("pull.id", pull.PullId),
-
attribute.String("pull.owner", pull.OwnerDid),
-
attribute.String("user.did", user.Did),
-
)
-
-
// auth filter: only owner or collaborators can reopen
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
-
isReopenAllowed := isCollaborator || isPullAuthor
-
-
span.SetAttributes(
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_pull_author", isPullAuthor),
-
attribute.Bool("is_reopen_allowed", isReopenAllowed),
-
)
-
-
if !isReopenAllowed {
-
log.Println("failed to reopen pull")
-
span.SetAttributes(attribute.String("error", "unauthorized"))
-
s.pages.Notice(w, "pull-close", "You are unauthorized to reopen this pull.")
return
}
// Start a transaction
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start transaction", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
···
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to reopen pull", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "db_reopen_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
···
package state
import (
"database/sql"
"encoding/json"
"errors"
···
"strconv"
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
// htmx fragment
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
return
}
+
mergeCheckResponse := s.mergeCheck(f, pull)
resubmitResult := pages.Unknown
if user.Did == pull.OwnerDid {
+
resubmitResult = s.resubmitCheck(f, pull)
}
s.pages.PullActionsFragment(w, pages.PullActionsParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Pull: pull,
RoundNumber: roundNumber,
MergeCheck: mergeCheckResponse,
ResubmitCheck: resubmitResult,
})
return
}
}
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
+
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
totalIdents := 1
for _, submission := range pull.Submissions {
totalIdents += len(submission.Comments)
···
}
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
mergeCheckResponse := s.mergeCheck(f, pull)
resubmitResult := pages.Unknown
if user != nil && user.Did == pull.OwnerDid {
+
resubmitResult = s.resubmitCheck(f, pull)
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
Pull: pull,
MergeCheck: mergeCheckResponse,
···
})
}
+
func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse {
if pull.State == db.PullMerged {
return types.MergeCheckResponse{}
}
···
return mergeCheckResponse
}
+
func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult {
if pull.State == db.PullMerged || pull.PullSource == nil {
return pages.Unknown
}
···
if pull.PullSource.RepoAt != nil {
// fork-based pulls
+
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
return pages.Unknown
}
···
repoName = sourceRepo.Name
} else {
// pulls within the same repo
knot = f.Knot
ownerDid = f.OwnerDid()
repoName = f.RepoName
}
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
return pages.Unknown
}
resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
if err != nil {
log.Println("failed to reach knotserver", err)
return pages.Unknown
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
return pages.Unknown
}
defer resp.Body.Close()
···
var result types.RepoBranchResponse
if err := json.Unmarshal(body, &result); err != nil {
log.Println("failed to parse response:", err)
return pages.Unknown
}
latestSubmission := pull.Submissions[pull.LastRoundNumber()]
if latestSubmission.SourceRev != result.Branch.Hash {
fmt.Println(latestSubmission.SourceRev, result.Branch.Hash)
return pages.ShouldResubmit
}
return pages.ShouldNotResubmit
}
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
+
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
···
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
return
}
identsToResolve := []string{pull.OwnerDid}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch)
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
LoggedInUser: user,
DidHandleMap: didHandleMap,
+
RepoInfo: f.RepoInfo(s, user),
Pull: pull,
Round: roundIdInt,
Submission: pull.Submissions[roundIdInt],
Diff: &diff,
})
+
}
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to get pull.")
return
}
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
return
}
if roundIdInt == 0 {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("cannot interdiff initial submission")
return
}
identsToResolve := []string{pull.OwnerDid}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch)
if err != nil {
log.Println("failed to interdiff; current patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
return
}
···
if err != nil {
log.Println("failed to interdiff; previous patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
return
}
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
+
LoggedInUser: s.auth.GetUser(r),
+
RepoInfo: f.RepoInfo(s, user),
Pull: pull,
Round: roundIdInt,
DidHandleMap: didHandleMap,
Interdiff: interdiff,
})
return
}
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
return
}
identsToResolve := []string{pull.OwnerDid}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
}
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
params := r.URL.Query()
state := db.PullOpen
switch params.Get("state") {
case "closed":
···
case "merged":
state = db.PullMerged
}
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pulls, err := db.GetPulls(s.db, f.RepoAt, state)
if err != nil {
log.Println("failed to get pulls", err)
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
return
}
for _, p := range pulls {
var pullSourceRepo *db.Repo
if p.PullSource != nil {
if p.PullSource.RepoAt != nil {
+
pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String())
if err != nil {
log.Printf("failed to get repo by at uri: %v", err)
continue
···
}
}
}
identsToResolve := make([]string, len(pulls))
for i, pull := range pulls {
identsToResolve[i] = pull.OwnerDid
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
s.pages.RepoPulls(w, pages.RepoPullsParams{
+
LoggedInUser: s.auth.GetUser(r),
+
RepoInfo: f.RepoInfo(s, user),
Pulls: pulls,
DidHandleMap: didHandleMap,
FilteringBy: state,
})
return
}
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
roundNumberStr := chi.URLParam(r, "round")
roundNumber, err := strconv.Atoi(roundNumberStr)
if err != nil || roundNumber >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
return
}
switch r.Method {
case http.MethodGet:
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Pull: pull,
RoundNumber: roundNumber,
})
return
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
s.pages.Notice(w, "pull", "Comment body is required")
return
}
// Start a transaction
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
return
}
defer tx.Rollback()
createdAt := time.Now().Format(time.RFC3339)
ownerDid := user.Did
+
pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to get pull at", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
return
}
atUri := f.RepoAt.String()
+
client, _ := s.auth.AuthorizedClient(r)
+
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullCommentNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
return
}
// Create the pull comment in the database with the commentAt field
+
commentId, err := db.NewPullComment(tx, &db.PullComment{
OwnerDid: user.Did,
RepoAt: f.RepoAt.String(),
PullId: pull.PullId,
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
return
}
+
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
···
}
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
switch r.Method {
case http.MethodGet:
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Branches: result.Branches,
})
case http.MethodPost:
title := r.FormValue("title")
body := r.FormValue("body")
targetBranch := r.FormValue("targetBranch")
fromFork := r.FormValue("fork")
sourceBranch := r.FormValue("sourceBranch")
patch := r.FormValue("patch")
if targetBranch == "" {
s.pages.Notice(w, "pull", "Target branch is required.")
return
}
// Determine PR type based on input parameters
+
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
isForkBased := fromFork != "" && sourceBranch != ""
isPatchBased := patch != "" && !isBranchBased && !isForkBased
if isPatchBased && !patchutil.IsFormatPatch(patch) {
if title == "" {
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
return
}
}
···
// Validate we have at least one valid PR creation method
if !isBranchBased && !isPatchBased && !isForkBased {
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
return
}
// Can't mix branch-based and patch-based approaches
if isBranchBased && patch != "" {
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
···
caps, err := us.Capabilities()
if err != nil {
log.Println("error fetching knot caps", f.Knot, err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
if !caps.PullRequests.FormatPatch {
s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.")
return
}
···
if isBranchBased {
if !caps.PullRequests.BranchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
return
}
+
s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch)
} else if isForkBased {
if !caps.PullRequests.ForkSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
return
}
+
s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch)
} else if isPatchBased {
if !caps.PullRequests.PatchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
return
}
+
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch)
}
return
}
}
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) {
pullSource := &db.PullSource{
Branch: sourceBranch,
}
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
if err != nil {
log.Println("failed to compare", err)
s.pages.Notice(w, "pull", err.Error())
return
}
···
sourceRev := comparison.Rev2
patch := comparison.Patch
if !patchutil.IsPatchValid(patch) {
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
}
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) {
if !patchutil.IsPatchValid(patch) {
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil)
}
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
+
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
if errors.Is(err, sql.ErrNoRows) {
s.pages.Notice(w, "pull", "No such fork.")
return
} else if err != nil {
log.Println("failed to fetch fork:", err)
s.pages.Notice(w, "pull", "Failed to fetch fork.")
return
}
···
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
if err != nil {
log.Println("failed to fetch registration key:", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create signed client:", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
us, err := NewUnsignedClient(fork.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client:", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
if err != nil {
log.Println("failed to create hidden ref:", err, resp.StatusCode)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
switch resp.StatusCode {
case 404:
case 400:
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
return
}
hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)
// We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking
// the targetBranch on the target repository. This code is a bit confusing, but here's an example:
// hiddenRef: hidden/feature-1/main (on repo-fork)
···
comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
if err != nil {
log.Println("failed to compare across branches", err)
s.pages.Notice(w, "pull", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
if !patchutil.IsPatchValid(patch) {
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
···
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
if err != nil {
log.Println("failed to parse fork AT URI", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
Branch: sourceBranch,
RepoAt: &forkAtUri,
}, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri})
···
pullSource *db.PullSource,
recordPullSource *tangled.RepoPull_Source,
) {
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
if title == "" {
formatPatches, err := patchutil.ExtractPatches(patch)
if err != nil {
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
return
}
if len(formatPatches) == 0 {
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
return
}
title = formatPatches[0].Title
body = formatPatches[0].Body
}
rkey := appview.TID()
···
Patch: patch,
SourceRev: sourceRev,
}
+
err = db.NewPull(tx, &db.Pull{
Title: title,
Body: body,
TargetBranch: targetBranch,
···
})
if err != nil {
log.Println("failed to create pull request", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
+
client, _ := s.auth.AuthorizedClient(r)
pullId, err := db.NextPullId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get pull id", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: rkey,
···
if err != nil {
log.Println("failed to create pull request", err)
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
}
func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
+
_, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
patch := r.FormValue("patch")
if patch == "" {
s.pages.Notice(w, "patch-error", "Patch is required.")
return
}
+
if patch == "" || !patchutil.IsPatchValid(patch) {
s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")
return
}
+
if patchutil.IsFormatPatch(patch) {
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.")
} else {
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
···
}
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
+
RepoInfo: f.RepoInfo(s, user),
})
}
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
+
RepoInfo: f.RepoInfo(s, user),
Branches: result.Branches,
})
}
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
forks, err := db.GetForksByDid(s.db, user.Did)
if err != nil {
log.Println("failed to get forks", err)
return
}
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
+
RepoInfo: f.RepoInfo(s, user),
Forks: forks,
})
}
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
forkVal := r.URL.Query().Get("fork")
// fork repo
+
repo, err := db.GetRepo(s.db, user.Did, forkVal)
if err != nil {
log.Println("failed to get repo", user.Did, forkVal)
return
}
sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", repo.Knot)
s.pages.Error503(w)
return
}
···
sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name)
if err != nil {
log.Println("failed to reach knotserver for source branches", err)
return
}
sourceBody, err := io.ReadAll(sourceResp.Body)
if err != nil {
log.Println("failed to read source response body", err)
return
}
defer sourceResp.Body.Close()
···
err = json.Unmarshal(sourceBody, &sourceResult)
if err != nil {
log.Println("failed to parse source branches response:", err)
return
}
targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
s.pages.Error503(w)
return
}
···
targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver for target branches", err)
return
}
targetBody, err := io.ReadAll(targetResp.Body)
if err != nil {
log.Println("failed to read target response body", err)
return
}
defer targetResp.Body.Close()
···
err = json.Unmarshal(targetBody, &targetResult)
if err != nil {
log.Println("failed to parse target branches response:", err)
return
}
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
+
RepoInfo: f.RepoInfo(s, user),
SourceBranches: sourceResult.Branches,
TargetBranches: targetResult.Branches,
})
}
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
switch r.Method {
case http.MethodGet:
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
+
RepoInfo: f.RepoInfo(s, user),
Pull: pull,
})
return
case http.MethodPost:
if pull.IsPatchBased() {
+
s.resubmitPatch(w, r)
return
} else if pull.IsBranchBased() {
+
s.resubmitBranch(w, r)
return
} else if pull.IsForkBased() {
+
s.resubmitFork(w, r)
return
}
}
}
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
w.WriteHeader(http.StatusUnauthorized)
return
}
patch := r.FormValue("patch")
if err = validateResubmittedPatch(pull, patch); err != nil {
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, "")
if err != nil {
log.Println("failed to resubmit pull request", err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
return
}
+
client, _ := s.auth.AuthorizedClient(r)
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
return
}
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
w.WriteHeader(http.StatusUnauthorized)
return
}
+
if !f.RepoInfo(s, user).Roles.IsPushAllowed() {
log.Println("unauthorized user")
w.WriteHeader(http.StatusUnauthorized)
return
}
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", f.Knot, err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
if err != nil {
log.Printf("compare request failed: %s", err)
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
if err = validateResubmittedPatch(pull, patch); err != nil {
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
+
client, _ := s.auth.AuthorizedClient(r)
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
···
recordPullSource := &tangled.RepoPull_Source{
Branch: pull.PullSource.Branch,
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
return
}
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
w.WriteHeader(http.StatusUnauthorized)
return
}
+
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
// extract patch by performing compare
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
if err != nil {
log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
if err != nil || resp.StatusCode != http.StatusNoContent {
log.Printf("failed to update tracking branch: %s", err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)
comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
if err != nil {
log.Printf("failed to compare branches: %s", err)
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
if err = validateResubmittedPatch(pull, patch); err != nil {
s.pages.Notice(w, "resubmit-error", err.Error())
return
}
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
}
+
client, _ := s.auth.AuthorizedClient(r)
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
}
···
Branch: pull.PullSource.Branch,
Repo: &repoAt,
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
}
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
}
···
}
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to resolve repo:", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
+
ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid)
if err != nil {
log.Printf("resolving identity: %s", err)
w.WriteHeader(http.StatusNotFound)
return
}
···
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
if err != nil {
log.Printf("failed to get primary email: %s", err)
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
···
resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address)
if err != nil {
log.Printf("failed to merge pull request: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
if resp.StatusCode == http.StatusOK {
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Printf("failed to update pull request status in database: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
} else {
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
}
}
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("malformed middleware")
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
// auth filter: only owner or collaborators can close
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
isCloseAllowed := isCollaborator || isPullAuthor
if !isCloseAllowed {
log.Println("failed to close pull")
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
return
}
// Start a transaction
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to close pull", err)
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
}
···
}
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to resolve repo", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
// auth filter: only owner or collaborators can close
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
+
isCloseAllowed := isCollaborator || isPullAuthor
+
if !isCloseAllowed {
+
log.Println("failed to close pull")
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
return
}
// Start a transaction
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
···
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to reopen pull", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
+95 -699
appview/state/repo.go
···
"strings"
"time"
-
"go.opentelemetry.io/otel/attribute"
-
"go.opentelemetry.io/otel/codes"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
)
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoIndex")
-
defer span.End()
-
ref := chi.URLParam(r, "ref")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
s.pages.Error503(w)
return
}
···
if err != nil {
s.pages.Error503(w)
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
defer resp.Body.Close()
···
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Printf("Error unmarshalling response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error unmarshalling response body")
return
}
···
tagCount := len(result.Tags)
fileCount := len(result.Files)
-
span.SetAttributes(
-
attribute.Int("commits.count", commitCount),
-
attribute.Int("branches.count", branchCount),
-
attribute.Int("tags.count", tagCount),
-
attribute.Int("files.count", fileCount),
-
)
-
commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
···
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
TagMap: tagMap,
RepoIndexResponse: result,
CommitsTrunc: commitsTrunc,
···
}
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoLog")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
···
}
ref := chi.URLParam(r, "ref")
-
span.SetAttributes(attribute.Int("page", page), attribute.String("ref", ref))
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &repolog)
if err != nil {
log.Println("failed to parse json response", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse json response")
return
}
-
-
span.SetAttributes(attribute.Int("commits.count", len(repolog.Commits)))
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver for tags")
return
}
···
tagMap[hash] = append(tagMap[hash], tag.Name)
}
-
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
-
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
LoggedInUser: user,
TagMap: tagMap,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoLogResponse: repolog,
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
})
···
}
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoDescriptionEdit")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
user := s.auth.GetUser(r)
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
})
return
}
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoDescription")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
w.WriteHeader(http.StatusBadRequest)
return
}
···
rkey := repoAt.RecordKey().String()
if rkey == "" {
log.Println("invalid aturi for repo", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "invalid aturi for repo")
w.WriteHeader(http.StatusInternalServerError)
return
}
user := s.auth.GetUser(r)
-
span.SetAttributes(attribute.String("method", r.Method))
switch r.Method {
case http.MethodGet:
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
})
return
case http.MethodPut:
user := s.auth.GetUser(r)
newDescription := r.FormValue("description")
-
span.SetAttributes(attribute.String("description", newDescription))
client, _ := s.auth.AuthorizedClient(r)
// optimistic update
-
err = db.UpdateDescription(ctx, s.db, string(repoAt), newDescription)
if err != nil {
-
log.Println("failed to perform update-description query", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to update description in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
//
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
// failed to get record
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
return
}
-
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
-
log.Println("failed to perform update-description query", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to put record to PDS")
// failed to get record
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
return
}
-
newRepoInfo := f.RepoInfo(ctx, s, user)
newRepoInfo.Description = newDescription
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
···
}
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoCommit")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
ref := chi.URLParam(r, "ref")
···
protocol = "https"
}
-
span.SetAttributes(attribute.String("ref", ref), attribute.String("protocol", protocol))
-
if !plumbing.IsHash(ref) {
-
span.SetAttributes(attribute.Bool("invalid_hash", true))
s.pages.Error404(w)
return
}
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
-
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse response")
return
}
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoCommitResponse: result,
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
})
···
}
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoTree")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
-
span.SetAttributes(
-
attribute.String("ref", ref),
-
attribute.String("tree_path", treePath),
-
attribute.String("protocol", protocol),
-
)
-
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
-
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse response")
return
}
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
if len(result.Files) == 0 && result.Parent == treePath {
-
redirectURL := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent)
-
span.SetAttributes(attribute.String("redirect_url", redirectURL))
-
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
···
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
BaseBlobLink: baseBlobLink,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTreeResponse: result,
})
return
}
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoTags")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
-
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
-
artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt))
if err != nil {
log.Println("failed grab artifacts", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to grab artifacts")
return
}
-
-
span.SetAttributes(attribute.Int("artifacts.count", len(artifacts)))
// convert artifacts to map for easy UI building
artifactMap := make(map[plumbing.Hash][]db.Artifact)
···
}
}
-
span.SetAttributes(attribute.Int("dangling_artifacts.count", len(danglingArtifacts)))
-
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTagsResponse: *result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
···
}
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBranches")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse response")
return
}
-
-
span.SetAttributes(attribute.Int("branches.count", len(result.Branches)))
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
if a.IsDefault {
···
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBranchesResponse: result,
})
return
}
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBlob")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
-
span.SetAttributes(
-
attribute.String("ref", ref),
-
attribute.String("file_path", filePath),
-
attribute.String("protocol", protocol),
-
)
-
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
-
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse response")
return
}
···
showRendered = r.URL.Query().Get("code") != "true"
}
-
span.SetAttributes(
-
attribute.Bool("is_binary", result.IsBinary),
-
attribute.Bool("show_rendered", showRendered),
-
attribute.Bool("render_toggle", renderToggle),
-
)
-
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBlobResponse: result,
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
···
}
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBlobRaw")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
-
span.SetAttributes(
-
attribute.String("ref", ref),
-
attribute.String("file_path", filePath),
-
attribute.String("protocol", protocol),
-
)
-
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
-
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse response")
return
}
-
-
span.SetAttributes(attribute.Bool("is_binary", result.IsBinary))
if result.IsBinary {
w.Header().Set("Content-Type", "application/octet-stream")
···
}
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "AddCollaborator")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
collaborator := r.FormValue("collaborator")
if collaborator == "" {
-
span.SetAttributes(attribute.String("error", "malformed_form"))
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
-
span.SetAttributes(attribute.String("collaborator", collaborator))
-
-
collaboratorIdent, err := s.resolver.ResolveIdent(ctx, collaborator)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve collaborator")
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
}
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
-
span.SetAttributes(
-
attribute.String("collaborator_did", collaboratorIdent.DID.String()),
-
attribute.String("collaborator_handle", collaboratorIdent.Handle.String()),
-
)
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create signed client")
return
}
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
if ksResp.StatusCode != http.StatusNoContent {
-
span.SetAttributes(attribute.Int("status_code", ksResp.StatusCode))
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
return
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to add collaborator to enforcer")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
-
err = db.AddCollaborator(ctx, s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to add collaborator to database")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to save enforcer policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
}
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "DeleteRepo")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
-
span.SetAttributes(
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
-
)
-
// remove record from pds
xrpcClient, _ := s.auth.AuthorizedClient(r)
repoRkey := f.RepoAt.RecordKey().String()
-
_, err = comatproto.RepoDeleteRecord(ctx, xrpcClient, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: repoRkey,
})
if err != nil {
log.Printf("failed to delete record: %s", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to delete record from PDS")
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
return
}
log.Println("removed repo record ", f.RepoAt.String())
-
span.SetAttributes(attribute.String("repo_at", f.RepoAt.String()))
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create client")
return
}
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
-
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
-
span.SetAttributes(attribute.Bool("knot_remove_failed", true))
} else {
log.Println("removed repo from knot ", f.Knot)
-
span.SetAttributes(attribute.Bool("knot_remove_success", true))
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
-
span.RecordError(err)
}
}()
// remove collaborator RBAC
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get collaborators")
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
return
}
-
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
-
for _, c := range repoCollaborators {
did := c[0]
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
···
// remove repo RBAC
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to remove repo RBAC")
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
return
}
// remove repo from db
-
err = db.RemoveRepo(ctx, tx, f.OwnerDid(), f.RepoName)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to remove repo from db")
s.pages.Notice(w, "settings-delete", "Failed to update appview")
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
}
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "SetDefaultBranch")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
branch := r.FormValue("branch")
if branch == "" {
-
span.SetAttributes(attribute.Bool("malformed_form", true))
-
span.SetStatus(codes.Error, "malformed form")
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
-
span.SetAttributes(
-
attribute.String("branch", branch),
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
-
)
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create client")
return
}
ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
-
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
-
span.SetStatus(codes.Error, "failed to set default branch")
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
return
}
···
}
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoSettings")
-
defer span.End()
-
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
-
span.SetAttributes(
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
-
attribute.String("method", r.Method),
-
)
-
switch r.Method {
case http.MethodGet:
// for now, this is just pubkeys
user := s.auth.GetUser(r)
-
repoCollaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to get collaborators", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_get_collaborators"))
}
-
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
isCollaboratorInviteAllowed := false
if user != nil {
···
isCollaboratorInviteAllowed = true
}
}
-
span.SetAttributes(attribute.Bool("invite_allowed", isCollaboratorInviteAllowed))
var branchNames []string
var defaultBranch string
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_create_unsigned_client"))
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_branches"))
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_read_branches_response"))
} else {
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_parse_branches_response"))
} else {
for _, branch := range result.Branches {
branchNames = append(branchNames, branch.Name)
}
-
span.SetAttributes(attribute.Int("branches.count", len(branchNames)))
}
}
}
···
defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
-
span.RecordError(err)
-
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_default_branch"))
} else {
defaultBranch = defaultBranchResp.Branch
-
span.SetAttributes(attribute.String("default_branch", defaultBranch))
}
}
s.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
Branches: branchNames,
···
return collaborators, nil
}
-
func (f *FullyResolvedRepo) RepoInfo(ctx context.Context, s *State, u *auth.User) repoinfo.RepoInfo {
-
ctx, span := s.t.TraceStart(ctx, "RepoInfo")
-
defer span.End()
-
isStarred := false
if u != nil {
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
-
span.SetAttributes(attribute.Bool("is_starred", isStarred))
}
starCount, err := db.GetStarCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get star count for ", f.RepoAt)
-
span.RecordError(err)
}
-
issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
-
span.RecordError(err)
}
-
pullCount, err := db.GetPullCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
-
span.RecordError(err)
}
-
-
span.SetAttributes(
-
attribute.Int("stats.stars", starCount),
-
attribute.Int("stats.issues.open", issueCount.Open),
-
attribute.Int("stats.issues.closed", issueCount.Closed),
-
attribute.Int("stats.pulls.open", pullCount.Open),
-
attribute.Int("stats.pulls.closed", pullCount.Closed),
-
attribute.Int("stats.pulls.merged", pullCount.Merged),
-
)
-
-
source, err := db.GetRepoSource(ctx, s.db, f.RepoAt)
if errors.Is(err, sql.ErrNoRows) {
source = ""
} else if err != nil {
log.Println("failed to get repo source for ", f.RepoAt, err)
-
span.RecordError(err)
}
var sourceRepo *db.Repo
if source != "" {
-
span.SetAttributes(attribute.String("source", source))
-
sourceRepo, err = db.GetRepoByAtUri(ctx, s.db, source)
if err != nil {
log.Println("failed to get repo by at uri", err)
-
span.RecordError(err)
}
}
var sourceHandle *identity.Identity
if sourceRepo != nil {
-
sourceHandle, err = s.resolver.ResolveIdent(ctx, sourceRepo.Did)
if err != nil {
log.Println("failed to resolve source repo", err)
-
span.RecordError(err)
-
} else if sourceHandle != nil {
-
span.SetAttributes(attribute.String("source_handle", sourceHandle.Handle.String()))
}
}
knot := f.Knot
-
span.SetAttributes(attribute.String("knot", knot))
-
var disableFork bool
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s: %v", knot, err)
-
span.RecordError(err)
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
-
span.RecordError(err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading branch response body: %v", err)
-
span.RecordError(err)
} else {
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(body, &branchesResp); err != nil {
log.Printf("error parsing branch response: %v", err)
-
span.RecordError(err)
} else {
disableFork = false
}
···
if len(branchesResp.Branches) == 0 {
disableFork = true
}
-
span.SetAttributes(
-
attribute.Int("branches.count", len(branchesResp.Branches)),
-
attribute.Bool("disable_fork", disableFork),
-
)
}
}
}
···
}
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoSingleIssue")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
-
issue, comments, err := db.GetIssueWithComments(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue and comments", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue and comments")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
-
span.SetAttributes(
-
attribute.Int("comments.count", len(comments)),
-
attribute.String("issue.title", issue.Title),
-
attribute.String("issue.owner_did", issue.OwnerDid),
-
)
-
-
issueOwnerIdent, err := s.resolver.ResolveIdent(ctx, issue.OwnerDid)
if err != nil {
log.Println("failed to resolve issue owner", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve issue owner")
}
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Issue: *issue,
Comments: comments,
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
DidHandleMap: didHandleMap,
})
}
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "CloseIssue")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
-
collaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
}
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
-
span.SetAttributes(
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_issue_owner", isIssueOwner),
-
)
-
// TODO: make this more granular
if isIssueOwner || isCollaborator {
closed := tangled.RepoIssueStateClosed
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueStateNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to update issue state", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to update issue state in PDS")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to close issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to close issue in database")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
return
} else {
log.Println("user is not permitted to close issue")
-
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "for biden", http.StatusUnauthorized)
return
}
}
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ReopenIssue")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
-
collaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
}
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
-
-
span.SetAttributes(
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_issue_owner", isIssueOwner),
-
)
if isCollaborator || isIssueOwner {
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to reopen issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to reopen issue")
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
return
}
···
return
} else {
log.Println("user is not the owner of the repo")
-
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "forbidden", http.StatusUnauthorized)
return
}
}
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewIssueComment")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
-
span.SetAttributes(
-
attribute.Int("issue_id", issueIdInt),
-
attribute.String("method", r.Method),
-
)
-
switch r.Method {
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
-
span.SetAttributes(attribute.Bool("missing_body", true))
s.pages.Notice(w, "issue", "Body is required")
return
}
···
commentId := mathrand.IntN(1000000)
rkey := appview.TID()
-
span.SetAttributes(
-
attribute.Int("comment_id", commentId),
-
attribute.String("rkey", rkey),
-
)
-
err := db.NewIssueComment(s.db, &db.Comment{
OwnerDid: user.Did,
RepoAt: f.RepoAt,
···
})
if err != nil {
log.Println("failed to create comment", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create comment in database")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
···
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue at", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue at")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
-
span.SetAttributes(attribute.String("issue_at", issueAt))
-
atUri := f.RepoAt.String()
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println("failed to create comment", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create comment in PDS")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
···
}
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "IssueComment")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
return
}
-
span.SetAttributes(
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
-
)
-
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get comment")
return
}
-
identity, err := s.resolver.ResolveIdent(ctx, comment.OwnerDid)
if err != nil {
log.Println("failed to resolve did")
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve did")
return
}
···
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
}
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "EditIssueComment")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
return
}
-
span.SetAttributes(
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
-
attribute.String("method", r.Method),
-
)
-
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get comment")
return
}
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
span.SetAttributes(attribute.Bool("permission_denied", true))
return
}
···
case http.MethodGet:
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
Issue: issue,
Comment: comment,
})
···
client, _ := s.auth.AuthorizedClient(r)
rkey := comment.Rkey
-
span.SetAttributes(
-
attribute.String("new_body", newBody),
-
attribute.String("rkey", rkey),
-
)
-
// optimistic update
edited := time.Now()
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
if err != nil {
log.Println("failed to perferom update-description query", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to edit comment in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
// rkey is optional, it was introduced later
if comment.Rkey != "" {
// update the record on pds
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
if err != nil {
// failed to get record
log.Println(err, rkey)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
return
}
···
createdAt := record["createdAt"].(string)
commentIdInt64 := int64(commentIdInt)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println(err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to put record to PDS")
}
}
···
// return new comment body with htmx
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
})
return
}
}
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "DeleteIssueComment")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
return
}
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
return
}
-
span.SetAttributes(
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
-
)
-
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get comment")
return
}
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
span.SetAttributes(attribute.Bool("permission_denied", true))
return
}
if comment.Deleted != nil {
http.Error(w, "comment already deleted", http.StatusBadRequest)
-
span.SetAttributes(attribute.Bool("already_deleted", true))
return
}
···
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
log.Println("failed to delete comment")
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to delete comment in database")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
return
}
···
// delete from pds
if comment.Rkey != "" {
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
Repo: user.Did,
Rkey: comment.Rkey,
})
if err != nil {
log.Println(err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to delete record from PDS")
}
}
···
// htmx fragment of comment after deletion
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
}
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoIssues")
-
defer span.End()
-
params := r.URL.Query()
state := params.Get("state")
isOpen := true
···
isOpen = true
}
-
span.SetAttributes(
-
attribute.Bool("is_open", isOpen),
-
attribute.String("state_param", state),
-
)
-
page, ok := r.Context().Value("page").(pagination.Page)
if !ok {
log.Println("failed to get page")
-
span.SetAttributes(attribute.Bool("page_not_found", true))
page = pagination.FirstPage()
}
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
-
issues, err := db.GetIssues(ctx, s.db, f.RepoAt, isOpen, page)
if err != nil {
log.Println("failed to get issues", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issues")
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
return
}
-
span.SetAttributes(attribute.Int("issues.count", len(issues)))
-
identsToResolve := make([]string, len(issues))
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
}
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: s.auth.GetUser(r),
-
RepoInfo: f.RepoInfo(ctx, s, user),
Issues: issues,
DidHandleMap: didHandleMap,
FilteringByOpen: isOpen,
···
}
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewIssue")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
return
}
-
-
span.SetAttributes(attribute.String("method", r.Method))
switch r.Method {
case http.MethodGet:
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(ctx, s, user),
})
case http.MethodPost:
title := r.FormValue("title")
body := r.FormValue("body")
-
span.SetAttributes(
-
attribute.String("title", title),
-
attribute.String("body_length", fmt.Sprintf("%d", len(body))),
-
)
-
if title == "" || body == "" {
-
span.SetAttributes(attribute.Bool("form_validation_failed", true))
s.pages.Notice(w, "issues", "Title and body are required")
return
}
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
return
}
···
})
if err != nil {
log.Println("failed to create issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create issue in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
···
issueId, err := db.GetIssueId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue id", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get issue id")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
span.SetAttributes(attribute.Int("issue_id", issueId))
-
client, _ := s.auth.AuthorizedClient(r)
atUri := f.RepoAt.String()
-
rkey := appview.TID()
-
span.SetAttributes(attribute.String("rkey", rkey))
-
-
resp, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueNSID,
Repo: user.Did,
-
Rkey: rkey,
Record: &lexutil.LexiconTypeDecoder{
Val: &tangled.RepoIssue{
Repo: atUri,
···
})
if err != nil {
log.Println("failed to create issue", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create issue in PDS")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
span.SetAttributes(attribute.String("issue_uri", resp.Uri))
-
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
if err != nil {
log.Println("failed to set issue at", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to set issue URI in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
···
}
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ForkRepo")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to resolve source repo")
return
}
-
span.SetAttributes(
-
attribute.String("method", r.Method),
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("owner_did", f.OwnerDid()),
-
attribute.String("knot", f.Knot),
-
)
-
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get domains for user")
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
-
span.SetAttributes(attribute.Int("knots.count", len(knots)))
-
s.pages.ForkRepo(w, pages.ForkRepoParams{
LoggedInUser: user,
Knots: knots,
-
RepoInfo: f.RepoInfo(ctx, s, user),
})
case http.MethodPost:
knot := r.FormValue("knot")
if knot == "" {
-
span.SetAttributes(attribute.Bool("missing_knot", true))
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
}
-
span.SetAttributes(attribute.String("target_knot", knot))
-
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
if err != nil || !ok {
-
span.SetAttributes(
-
attribute.Bool("permission_denied", true),
-
attribute.Bool("enforce_error", err != nil),
-
)
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
forkName := fmt.Sprintf("%s", f.RepoName)
-
span.SetAttributes(attribute.String("fork_name", forkName))
// this check is *only* to see if the forked repo name already exists
// in the user's account.
-
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, f.RepoName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
-
span.SetAttributes(attribute.Bool("repo_name_available", true))
} else {
log.Println("error fetching existing repo from db", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to check for existing repo")
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
}
} else if existingRepo != nil {
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
-
span.SetAttributes(
-
attribute.Bool("repo_name_conflict", true),
-
attribute.String("adjusted_fork_name", forkName),
-
)
}
-
secret, err := db.GetRegistrationKey(s.db, knot)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to get registration key")
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
return
}
client, err := NewSignedClient(knot, secret, s.config.Dev)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create signed client")
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
···
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
-
span.SetAttributes(
-
attribute.String("fork_source_url", forkSourceUrl),
-
attribute.String("source_at", sourceAt),
-
)
-
rkey := appview.TID()
repo := &db.Repo{
Did: user.Did,
···
Source: sourceAt,
}
-
span.SetAttributes(attribute.String("rkey", rkey))
-
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println(err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
-
span.RecordError(err)
}
}()
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
if err != nil {
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to fork repo on knot server")
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
-
-
span.SetAttributes(attribute.Int("fork_response_status", resp.StatusCode))
switch resp.StatusCode {
case http.StatusConflict:
-
span.SetAttributes(attribute.Bool("name_conflict", true))
s.pages.Notice(w, "repo", "A repository with that name already exists.")
return
case http.StatusInternalServerError:
-
span.SetAttributes(attribute.Bool("server_error", true))
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
-
return
case http.StatusNoContent:
// continue
}
···
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
-
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Printf("failed to create record: %s", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to create record in PDS")
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
-
span.SetAttributes(attribute.String("repo_uri", atresp.Uri))
repo.AtUri = atresp.Uri
-
err = db.AddRepo(ctx, tx, repo)
if err != nil {
log.Println(err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to add repo to database")
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
···
err = s.enforcer.AddRepo(user.Did, knot, p)
if err != nil {
log.Println(err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to set up repository permissions")
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
-
span.RecordError(err)
-
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
"strings"
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
)
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
s.pages.Error503(w)
return
}
···
if err != nil {
s.pages.Error503(w)
log.Println("failed to reach knotserver", err)
return
}
defer resp.Body.Close()
···
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Printf("Error unmarshalling response body: %v", err)
return
}
···
tagCount := len(result.Tags)
fileCount := len(result.Files)
commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
···
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
TagMap: tagMap,
RepoIndexResponse: result,
CommitsTrunc: commitsTrunc,
···
}
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
···
}
ref := chi.URLParam(r, "ref")
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
}
resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &repolog)
if err != nil {
log.Println("failed to parse json response", err)
return
}
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
···
tagMap[hash] = append(tagMap[hash], tag.Name)
}
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
LoggedInUser: user,
TagMap: tagMap,
+
RepoInfo: f.RepoInfo(s, user),
RepoLogResponse: repolog,
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
})
···
}
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
user := s.auth.GetUser(r)
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
+
RepoInfo: f.RepoInfo(s, user),
})
return
}
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
return
}
···
rkey := repoAt.RecordKey().String()
if rkey == "" {
log.Println("invalid aturi for repo", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
user := s.auth.GetUser(r)
switch r.Method {
case http.MethodGet:
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
+
RepoInfo: f.RepoInfo(s, user),
})
return
case http.MethodPut:
user := s.auth.GetUser(r)
newDescription := r.FormValue("description")
client, _ := s.auth.AuthorizedClient(r)
// optimistic update
+
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
if err != nil {
+
log.Println("failed to perferom update-description query", err)
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
//
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
// failed to get record
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
return
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
+
log.Println("failed to perferom update-description query", err)
// failed to get record
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
return
}
+
newRepoInfo := f.RepoInfo(s, user)
newRepoInfo.Description = newDescription
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
···
}
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
···
protocol = "https"
}
if !plumbing.IsHash(ref) {
s.pages.Error404(w)
return
}
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
RepoCommitResponse: result,
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
})
···
}
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
···
if !s.config.Dev {
protocol = "https"
}
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
if len(result.Files) == 0 && result.Parent == treePath {
+
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
return
}
···
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
BaseBlobLink: baseBlobLink,
+
RepoInfo: f.RepoInfo(s, user),
RepoTreeResponse: result,
})
return
}
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
}
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt))
if err != nil {
log.Println("failed grab artifacts", err)
return
}
// convert artifacts to map for easy UI building
artifactMap := make(map[plumbing.Hash][]db.Artifact)
···
}
}
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
RepoTagsResponse: *result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
···
}
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
}
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
if a.IsDefault {
···
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
RepoBranchesResponse: result,
})
return
}
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if !s.config.Dev {
protocol = "https"
}
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
···
showRendered = r.URL.Query().Get("code") != "true"
}
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
RepoBlobResponse: result,
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
···
}
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if !s.config.Dev {
protocol = "https"
}
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
if err != nil {
log.Println("failed to reach knotserver", err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
return
}
if result.IsBinary {
w.Header().Set("Content-Type", "application/octet-stream")
···
}
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
collaborator := r.FormValue("collaborator")
if collaborator == "" {
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
+
collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
if err != nil {
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
}
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
}
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
return
}
if ksResp.StatusCode != http.StatusNoContent {
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
return
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
+
err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
+
}
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
// remove record from pds
xrpcClient, _ := s.auth.AuthorizedClient(r)
repoRkey := f.RepoAt.RecordKey().String()
+
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: repoRkey,
})
if err != nil {
log.Printf("failed to delete record: %s", err)
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
return
}
log.Println("removed repo record ", f.RepoAt.String())
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
}
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
return
}
if ksResp.StatusCode != http.StatusNoContent {
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
} else {
log.Println("removed repo from knot ", f.Knot)
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
// remove collaborator RBAC
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
if err != nil {
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
return
}
for _, c := range repoCollaborators {
did := c[0]
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
···
// remove repo RBAC
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
return
}
// remove repo from db
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
if err != nil {
s.pages.Notice(w, "settings-delete", "Failed to update appview")
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
}
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
branch := r.FormValue("branch")
if branch == "" {
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
}
ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
return
}
if ksResp.StatusCode != http.StatusNoContent {
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
return
}
···
}
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
switch r.Method {
case http.MethodGet:
// for now, this is just pubkeys
user := s.auth.GetUser(r)
+
repoCollaborators, err := f.Collaborators(r.Context(), s)
if err != nil {
log.Println("failed to get collaborators", err)
}
isCollaboratorInviteAllowed := false
if user != nil {
···
isCollaboratorInviteAllowed = true
}
}
var branchNames []string
var defaultBranch string
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
} else {
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
} else {
for _, branch := range result.Branches {
branchNames = append(branchNames, branch.Name)
}
}
}
}
···
defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
} else {
defaultBranch = defaultBranchResp.Branch
}
}
s.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
Branches: branchNames,
···
return collaborators, nil
}
+
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
isStarred := false
if u != nil {
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
}
starCount, err := db.GetStarCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get star count for ", f.RepoAt)
}
issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
}
pullCount, err := db.GetPullCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
}
+
source, err := db.GetRepoSource(s.db, f.RepoAt)
if errors.Is(err, sql.ErrNoRows) {
source = ""
} else if err != nil {
log.Println("failed to get repo source for ", f.RepoAt, err)
}
var sourceRepo *db.Repo
if source != "" {
+
sourceRepo, err = db.GetRepoByAtUri(s.db, source)
if err != nil {
log.Println("failed to get repo by at uri", err)
}
}
var sourceHandle *identity.Identity
if sourceRepo != nil {
+
sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did)
if err != nil {
log.Println("failed to resolve source repo", err)
}
}
knot := f.Knot
var disableFork bool
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s: %v", knot, err)
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading branch response body: %v", err)
} else {
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(body, &branchesResp); err != nil {
log.Printf("error parsing branch response: %v", err)
} else {
disableFork = false
}
···
if len(branchesResp.Branches) == 0 {
disableFork = true
}
}
}
}
···
}
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue and comments", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
+
issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
if err != nil {
log.Println("failed to resolve issue owner", err)
}
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Issue: *issue,
Comments: comments,
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
DidHandleMap: didHandleMap,
})
+
}
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
+
collaborators, err := f.Collaborators(r.Context(), s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
}
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
// TODO: make this more granular
if isIssueOwner || isCollaborator {
+
closed := tangled.RepoIssueStateClosed
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueStateNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to update issue state", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to close issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
return
} else {
log.Println("user is not permitted to close issue")
http.Error(w, "for biden", http.StatusUnauthorized)
return
}
}
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
+
collaborators, err := f.Collaborators(r.Context(), s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
}
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
if isCollaborator || isIssueOwner {
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to reopen issue", err)
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
return
}
···
return
} else {
log.Println("user is not the owner of the repo")
http.Error(w, "forbidden", http.StatusUnauthorized)
return
}
}
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
switch r.Method {
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
s.pages.Notice(w, "issue", "Body is required")
return
}
···
commentId := mathrand.IntN(1000000)
rkey := appview.TID()
err := db.NewIssueComment(s.db, &db.Comment{
OwnerDid: user.Did,
RepoAt: f.RepoAt,
···
})
if err != nil {
log.Println("failed to create comment", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
···
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue at", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
atUri := f.RepoAt.String()
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println("failed to create comment", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
···
}
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
}
+
identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid)
if err != nil {
log.Println("failed to resolve did")
return
}
···
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
}
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
}
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
return
}
···
case http.MethodGet:
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
Issue: issue,
Comment: comment,
})
···
client, _ := s.auth.AuthorizedClient(r)
rkey := comment.Rkey
// optimistic update
edited := time.Now()
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
if err != nil {
log.Println("failed to perferom update-description query", err)
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
// rkey is optional, it was introduced later
if comment.Rkey != "" {
// update the record on pds
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
if err != nil {
// failed to get record
log.Println(err, rkey)
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
return
}
···
createdAt := record["createdAt"].(string)
commentIdInt64 := int64(commentIdInt)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println(err)
}
}
···
// return new comment body with htmx
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
})
return
+
}
+
}
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
return
}
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
}
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
return
}
if comment.Deleted != nil {
http.Error(w, "comment already deleted", http.StatusBadRequest)
return
}
···
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
log.Println("failed to delete comment")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
return
}
···
// delete from pds
if comment.Rkey != "" {
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
Repo: user.Did,
Rkey: comment.Rkey,
})
if err != nil {
log.Println(err)
}
}
···
// htmx fragment of comment after deletion
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
}
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
state := params.Get("state")
isOpen := true
···
isOpen = true
}
page, ok := r.Context().Value("page").(pagination.Page)
if !ok {
log.Println("failed to get page")
page = pagination.FirstPage()
}
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page)
if err != nil {
log.Println("failed to get issues", err)
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
return
}
identsToResolve := make([]string, len(issues))
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: s.auth.GetUser(r),
+
RepoInfo: f.RepoInfo(s, user),
Issues: issues,
DidHandleMap: didHandleMap,
FilteringByOpen: isOpen,
···
}
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
switch r.Method {
case http.MethodGet:
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
})
case http.MethodPost:
title := r.FormValue("title")
body := r.FormValue("body")
if title == "" || body == "" {
s.pages.Notice(w, "issues", "Title and body are required")
return
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
return
}
···
})
if err != nil {
log.Println("failed to create issue", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
···
issueId, err := db.GetIssueId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue id", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
client, _ := s.auth.AuthorizedClient(r)
atUri := f.RepoAt.String()
+
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueNSID,
Repo: user.Did,
+
Rkey: appview.TID(),
Record: &lexutil.LexiconTypeDecoder{
Val: &tangled.RepoIssue{
Repo: atUri,
···
})
if err != nil {
log.Println("failed to create issue", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
if err != nil {
log.Println("failed to set issue at", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
···
}
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
return
}
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
s.pages.ForkRepo(w, pages.ForkRepoParams{
LoggedInUser: user,
Knots: knots,
+
RepoInfo: f.RepoInfo(s, user),
})
case http.MethodPost:
+
knot := r.FormValue("knot")
if knot == "" {
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
}
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
if err != nil || !ok {
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
forkName := fmt.Sprintf("%s", f.RepoName)
// this check is *only* to see if the forked repo name already exists
// in the user's account.
+
existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
} else {
log.Println("error fetching existing repo from db", err)
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
}
} else if existingRepo != nil {
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
}
secret, err := db.GetRegistrationKey(s.db, knot)
if err != nil {
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
return
}
client, err := NewSignedClient(knot, secret, s.config.Dev)
if err != nil {
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
···
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
rkey := appview.TID()
repo := &db.Repo{
Did: user.Did,
···
Source: sourceAt,
}
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
if err != nil {
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
switch resp.StatusCode {
case http.StatusConflict:
s.pages.Notice(w, "repo", "A repository with that name already exists.")
return
case http.StatusInternalServerError:
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
case http.StatusNoContent:
// continue
}
···
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
+
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Printf("failed to create record: %s", err)
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
repo.AtUri = atresp.Uri
+
err = db.AddRepo(tx, repo)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
···
err = s.enforcer.AddRepo(user.Did, knot, p)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+6 -24
appview/state/repo_util.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing/object"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
-
"tangled.sh/tangled.sh/core/telemetry"
)
func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
-
ctx := r.Context()
-
-
attrs := telemetry.MapAttrs(map[string]string{
-
"repo": chi.URLParam(r, "repo"),
-
"ref": chi.URLParam(r, "ref"),
-
})
-
-
ctx, span := s.t.TraceStart(ctx, "fullyResolvedRepo", attrs...)
-
defer span.End()
-
repoName := chi.URLParam(r, "repo")
-
knot, ok := ctx.Value("knot").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
-
-
span.SetAttributes(attribute.String("knot", knot))
-
-
id, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
-
span.SetAttributes(attribute.String("did", id.DID.String()))
-
-
repoAt, ok := ctx.Value("repoAt").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
···
}
ref = defaultBranch.Branch
-
-
span.SetAttributes(attribute.String("default_branch", ref))
}
-
description, ok := ctx.Value("repoDescription").(string)
-
addedAt, ok := ctx.Value("repoAddedAt").(string)
return &FullyResolvedRepo{
Knot: knot,
···
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
)
func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
repoName := chi.URLParam(r, "repo")
+
knot, ok := r.Context().Value("knot").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
+
repoAt, ok := r.Context().Value("repoAt").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
···
}
ref = defaultBranch.Branch
}
+
// pass through values from the middleware
+
description, ok := r.Context().Value("repoDescription").(string)
+
addedAt, ok := r.Context().Value("repoAddedAt").(string)
return &FullyResolvedRepo{
Knot: knot,
+1 -6
appview/state/router.go
···
func (s *State) Router() http.Handler {
router := chi.NewRouter()
-
if s.t != nil {
-
// top-level telemetry middleware
-
// router.Use(s.t.RequestDuration())
-
// router.Use(s.t.RequestInFlight())
-
}
-
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
pat := chi.URLParam(r, "*")
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
···
func (s *State) UserRouter() http.Handler {
r := chi.NewRouter()
// strip @ from user
r.Use(StripLeadingAt)
···
func (s *State) Router() http.Handler {
router := chi.NewRouter()
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
pat := chi.URLParam(r, "*")
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
···
func (s *State) UserRouter() http.Handler {
r := chi.NewRouter()
+
// strip @ from user
r.Use(StripLeadingAt)
+9 -69
appview/state/state.go
···
"log"
"log/slog"
"net/http"
-
"runtime/debug"
"strings"
"time"
···
lexutil "github.com/bluesky-social/indigo/lex/util"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
-
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/jetstream"
"tangled.sh/tangled.sh/core/rbac"
-
"tangled.sh/tangled.sh/core/telemetry"
)
type State struct {
···
pages *pages.Pages
resolver *appview.Resolver
jc *jetstream.JetstreamClient
-
t *telemetry.Telemetry
config *appview.Config
}
-
func Make(ctx context.Context, config *appview.Config) (*State, error) {
d, err := db.Make(config.DbPath)
if err != nil {
return nil, err
···
resolver := appview.NewResolver()
-
bi, ok := debug.ReadBuildInfo()
-
var version string
-
if ok {
-
version = bi.Main.Version
-
} else {
-
version = "v0.0.0-unknown"
-
}
-
wrapper := db.DbWrapper{d}
jc, err := jetstream.NewJetstreamClient(
config.JetstreamEndpoint,
···
if err != nil {
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
}
-
err = jc.StartJetstream(ctx, appview.Ingest(wrapper))
if err != nil {
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
}
-
var tele *telemetry.Telemetry
-
if config.EnableTelemetry {
-
tele, err = telemetry.NewTelemetry(ctx, "appview", version, config.Dev)
-
if err != nil {
-
return nil, fmt.Errorf("failed to setup telemetry: %w", err)
-
}
-
}
-
state := &State{
d,
auth,
···
pgs,
resolver,
jc,
-
tele,
config,
}
···
}
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "Timeline")
-
defer span.End()
-
user := s.auth.GetUser(r)
-
span.SetAttributes(attribute.String("user.did", user.Did))
-
timeline, err := db.MakeTimeline(ctx, s.db)
if err != nil {
log.Println(err)
-
span.RecordError(err)
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
}
···
didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did)
}
}
-
span.SetAttributes(attribute.Int("dids.to_resolve.count", len(didsToResolve)))
-
resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
-
span.SetAttributes(attribute.Int("dids.resolved.count", len(resolvedIds)))
s.pages.Timeline(w, pages.TimelineParams{
LoggedInUser: user,
···
}
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewRepo")
-
defer span.End()
-
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
-
span.SetAttributes(attribute.String("user.did", user.Did))
-
span.SetAttributes(attribute.String("request.method", "GET"))
-
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
-
span.RecordError(err)
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
-
span.SetAttributes(attribute.Int("knots.count", len(knots)))
s.pages.NewRepo(w, pages.NewRepoParams{
LoggedInUser: user,
···
case http.MethodPost:
user := s.auth.GetUser(r)
-
span.SetAttributes(attribute.String("user.did", user.Did))
-
span.SetAttributes(attribute.String("request.method", "POST"))
domain := r.FormValue("domain")
if domain == "" {
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
}
-
span.SetAttributes(attribute.String("domain", domain))
repoName := r.FormValue("name")
if repoName == "" {
s.pages.Notice(w, "repo", "Repository name cannot be empty.")
return
}
-
span.SetAttributes(attribute.String("repo.name", repoName))
if err := validateRepoName(repoName); err != nil {
s.pages.Notice(w, "repo", err.Error())
···
if defaultBranch == "" {
defaultBranch = "main"
}
-
span.SetAttributes(attribute.String("repo.default_branch", defaultBranch))
description := r.FormValue("description")
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
if err != nil || !ok {
-
if err != nil {
-
span.RecordError(err)
-
}
-
span.SetAttributes(attribute.Bool("permission.granted", false))
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
-
span.SetAttributes(attribute.Bool("permission.granted", true))
-
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, repoName)
if err == nil && existingRepo != nil {
-
span.SetAttributes(attribute.Bool("repo.exists", true))
s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
return
}
-
span.SetAttributes(attribute.Bool("repo.exists", false))
secret, err := db.GetRegistrationKey(s.db, domain)
if err != nil {
-
span.RecordError(err)
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
return
}
client, err := NewSignedClient(domain, secret, s.config.Dev)
if err != nil {
-
span.RecordError(err)
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
return
}
···
Description: description,
}
-
rWithCtx := r.WithContext(ctx)
-
xrpcClient, _ := s.auth.AuthorizedClient(rWithCtx)
createdAt := time.Now().Format(time.RFC3339)
-
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
}},
})
if err != nil {
-
span.RecordError(err)
log.Printf("failed to create record: %s", err)
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
-
span.SetAttributes(attribute.String("repo.uri", atresp.Uri))
-
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
-
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
if err != nil {
-
span.RecordError(err)
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
-
span.SetAttributes(attribute.Int("knot_response.status", resp.StatusCode))
switch resp.StatusCode {
case http.StatusConflict:
···
}
repo.AtUri = atresp.Uri
-
err = db.AddRepo(ctx, tx, repo)
if err != nil {
-
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
p, _ := securejoin.SecureJoin(user.Did, repoName)
err = s.enforcer.AddRepo(user.Did, domain, p)
if err != nil {
-
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
···
err = tx.Commit()
if err != nil {
-
span.RecordError(err)
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
···
err = s.enforcer.E.SavePolicy()
if err != nil {
-
span.RecordError(err)
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
···
"log"
"log/slog"
"net/http"
"strings"
"time"
···
lexutil "github.com/bluesky-social/indigo/lex/util"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/jetstream"
"tangled.sh/tangled.sh/core/rbac"
)
type State struct {
···
pages *pages.Pages
resolver *appview.Resolver
jc *jetstream.JetstreamClient
config *appview.Config
}
+
func Make(config *appview.Config) (*State, error) {
d, err := db.Make(config.DbPath)
if err != nil {
return nil, err
···
resolver := appview.NewResolver()
wrapper := db.DbWrapper{d}
jc, err := jetstream.NewJetstreamClient(
config.JetstreamEndpoint,
···
if err != nil {
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
}
+
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
if err != nil {
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
}
state := &State{
d,
auth,
···
pgs,
resolver,
jc,
config,
}
···
}
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
timeline, err := db.MakeTimeline(s.db)
if err != nil {
log.Println(err)
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
}
···
didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did)
}
}
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
s.pages.Timeline(w, pages.TimelineParams{
LoggedInUser: user,
···
}
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
s.pages.NewRepo(w, pages.NewRepoParams{
LoggedInUser: user,
···
case http.MethodPost:
user := s.auth.GetUser(r)
domain := r.FormValue("domain")
if domain == "" {
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
}
repoName := r.FormValue("name")
if repoName == "" {
s.pages.Notice(w, "repo", "Repository name cannot be empty.")
return
}
if err := validateRepoName(repoName); err != nil {
s.pages.Notice(w, "repo", err.Error())
···
if defaultBranch == "" {
defaultBranch = "main"
}
description := r.FormValue("description")
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
if err != nil || !ok {
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
+
existingRepo, err := db.GetRepo(s.db, user.Did, repoName)
if err == nil && existingRepo != nil {
s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
return
}
secret, err := db.GetRegistrationKey(s.db, domain)
if err != nil {
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
return
}
client, err := NewSignedClient(domain, secret, s.config.Dev)
if err != nil {
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
return
}
···
Description: description,
}
+
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
+
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
}},
})
if err != nil {
log.Printf("failed to create record: %s", err)
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
if err != nil {
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
switch resp.StatusCode {
case http.StatusConflict:
···
}
repo.AtUri = atresp.Uri
+
err = db.AddRepo(tx, repo)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
p, _ := securejoin.SecureJoin(user.Did, repoName)
err = s.enforcer.AddRepo(user.Did, domain, p)
if err != nil {
log.Println(err)
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+2 -4
cmd/appview/main.go
···
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
-
ctx := context.Background()
-
-
c, err := appview.LoadConfig(ctx)
if err != nil {
log.Println("failed to load config", "error", err)
return
}
-
state, err := state.Make(ctx, c)
if err != nil {
log.Fatal(err)
···
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
+
c, err := appview.LoadConfig(context.Background())
if err != nil {
log.Println("failed to load config", "error", err)
return
}
+
state, err := state.Make(c)
if err != nil {
log.Fatal(err)
+6 -21
go.mod
···
github.com/sethvargo/go-envconfig v1.1.0
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
github.com/yuin/goldmark v1.4.13
-
go.opentelemetry.io/otel v1.35.0
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0
-
go.opentelemetry.io/otel/metric v1.35.0
-
go.opentelemetry.io/otel/sdk v1.35.0
-
go.opentelemetry.io/otel/sdk/metric v1.35.0
-
go.opentelemetry.io/otel/trace v1.35.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
)
···
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
-
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
···
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
-
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
···
github.com/xanzy/ssh-agent v0.3.3 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
-
golang.org/x/sys v0.33.0 // indirect
-
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
-
google.golang.org/grpc v1.71.0 // indirect
-
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
···
github.com/sethvargo/go-envconfig v1.1.0
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
github.com/yuin/goldmark v1.4.13
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
)
···
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
···
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
+
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
···
github.com/xanzy/ssh-agent v0.3.3 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
+
go.opentelemetry.io/otel v1.21.0 // indirect
+
go.opentelemetry.io/otel/metric v1.21.0 // indirect
+
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
+
golang.org/x/sys v0.32.0 // indirect
golang.org/x/time v0.5.0 // indirect
+
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
+18 -48
go.sum
···
github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
-
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
···
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
···
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
···
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
-
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
-
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
-
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
-
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
···
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
-
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
-
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
-
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
···
github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
···
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
···
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
···
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
···
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-88
telemetry/middleware.go
···
-
package telemetry
-
-
import (
-
"fmt"
-
"net/http"
-
"time"
-
-
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
-
otelmetric "go.opentelemetry.io/otel/metric"
-
"go.opentelemetry.io/otel/semconv/v1.13.0/httpconv"
-
)
-
-
func (t *Telemetry) RequestDuration() func(next http.Handler) http.Handler {
-
const (
-
metricNameRequestDurationMs = "request_duration_millis"
-
metricUnitRequestDurationMs = "ms"
-
metricDescRequestDurationMs = "Measures the latency of HTTP requests processed by the server, in milliseconds."
-
)
-
histogram, err := t.meter.Int64Histogram(
-
metricNameRequestDurationMs,
-
otelmetric.WithDescription(metricDescRequestDurationMs),
-
otelmetric.WithUnit(metricUnitRequestDurationMs),
-
)
-
if err != nil {
-
panic(fmt.Sprintf("unable to create %s histogram: %v", metricNameRequestDurationMs, err))
-
}
-
-
return func(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
// capture the start time of the request
-
startTime := time.Now()
-
-
// execute next http handler
-
next.ServeHTTP(w, r)
-
-
// record the request duration
-
duration := time.Since(startTime)
-
histogram.Record(
-
r.Context(),
-
int64(duration.Milliseconds()),
-
otelmetric.WithAttributes(
-
httpconv.ServerRequest(t.serviceName, r)...,
-
),
-
)
-
})
-
}
-
}
-
-
func (t *Telemetry) RequestInFlight() func(next http.Handler) http.Handler {
-
const (
-
metricNameRequestInFlight = "request_in_flight"
-
metricDescRequestInFlight = "Measures the number of concurrent HTTP requests being processed by the server."
-
metricUnitRequestInFlight = "1"
-
)
-
-
// counter to capture requests in flight
-
counter, err := t.meter.Int64UpDownCounter(
-
metricNameRequestInFlight,
-
otelmetric.WithDescription(metricDescRequestInFlight),
-
otelmetric.WithUnit(metricUnitRequestInFlight),
-
)
-
if err != nil {
-
panic(fmt.Sprintf("unable to create %s counter: %v", metricNameRequestInFlight, err))
-
}
-
-
return func(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
attrs := otelmetric.WithAttributes(httpconv.ServerRequest(t.serviceName, r)...)
-
-
// increase the number of requests in flight
-
counter.Add(r.Context(), 1, attrs)
-
-
// execute next http handler
-
next.ServeHTTP(w, r)
-
-
// decrease the number of requests in flight
-
counter.Add(r.Context(), -1, attrs)
-
})
-
}
-
}
-
-
func (t *Telemetry) WithRouteTag() func(next http.Handler) http.Handler {
-
return func(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
otelhttp.WithRouteTag(r.URL.Path, next)
-
})
-
}
-
}
···
-65
telemetry/provider.go
···
-
package telemetry
-
-
import (
-
"context"
-
"fmt"
-
"time"
-
-
"go.opentelemetry.io/otel"
-
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
-
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
-
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
-
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
-
"go.opentelemetry.io/otel/sdk/metric"
-
"go.opentelemetry.io/otel/sdk/resource"
-
"go.opentelemetry.io/otel/sdk/trace"
-
)
-
-
func NewTracerProvider(ctx context.Context, res *resource.Resource, isDev bool) (*trace.TracerProvider, error) {
-
var exporter trace.SpanExporter
-
var err error
-
-
if isDev {
-
exporter, err = stdouttrace.New()
-
if err != nil {
-
return nil, fmt.Errorf("failed to create stdout trace exporter: %w", err)
-
}
-
} else {
-
exporter, err = otlptracegrpc.New(ctx)
-
if err != nil {
-
return nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err)
-
}
-
}
-
-
tp := trace.NewTracerProvider(
-
trace.WithBatcher(exporter, trace.WithBatchTimeout(1*time.Second)),
-
trace.WithResource(res),
-
)
-
otel.SetTracerProvider(tp)
-
-
return tp, nil
-
}
-
-
func NewMeterProvider(ctx context.Context, res *resource.Resource, isDev bool) (*metric.MeterProvider, error) {
-
var exporter metric.Exporter
-
var err error
-
-
if isDev {
-
exporter, err = stdoutmetric.New()
-
if err != nil {
-
return nil, fmt.Errorf("failed to create stdout metric exporter: %w", err)
-
}
-
} else {
-
exporter, err = otlpmetricgrpc.New(ctx)
-
if err != nil {
-
return nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err)
-
}
-
}
-
-
mp := metric.NewMeterProvider(
-
metric.WithReader(metric.NewPeriodicReader(exporter, metric.WithInterval(10*time.Second))),
-
metric.WithResource(res),
-
)
-
otel.SetMeterProvider(mp)
-
return mp, nil
-
}
···
-76
telemetry/telemetry.go
···
-
package telemetry
-
-
import (
-
"context"
-
"fmt"
-
-
"go.opentelemetry.io/otel/attribute"
-
otelmetric "go.opentelemetry.io/otel/metric"
-
"go.opentelemetry.io/otel/sdk/metric"
-
"go.opentelemetry.io/otel/sdk/resource"
-
"go.opentelemetry.io/otel/sdk/trace"
-
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
-
oteltrace "go.opentelemetry.io/otel/trace"
-
)
-
-
type Telemetry struct {
-
tp *trace.TracerProvider
-
mp *metric.MeterProvider
-
-
meter otelmetric.Meter
-
tracer oteltrace.Tracer
-
-
serviceName string
-
serviceVersion string
-
}
-
-
func NewTelemetry(ctx context.Context, serviceName, serviceVersion string, isDev bool) (*Telemetry, error) {
-
res := resource.NewWithAttributes(
-
semconv.SchemaURL,
-
semconv.ServiceName(serviceName),
-
semconv.ServiceVersion(serviceVersion),
-
)
-
-
tp, err := NewTracerProvider(ctx, res, isDev)
-
if err != nil {
-
return nil, err
-
}
-
-
// mp, err := NewMeterProvider(ctx, res, isDev)
-
// if err != nil {
-
// return nil, err
-
// }
-
-
return &Telemetry{
-
tp: tp,
-
//mp: mp,
-
-
//meter: mp.Meter(serviceName),
-
tracer: tp.Tracer(serviceVersion),
-
-
serviceName: serviceName,
-
serviceVersion: serviceVersion,
-
}, nil
-
}
-
-
func (t *Telemetry) Meter() otelmetric.Meter {
-
return t.meter
-
}
-
-
func (t *Telemetry) Tracer() oteltrace.Tracer {
-
return t.tracer
-
}
-
-
func (t *Telemetry) TraceStart(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, oteltrace.Span) {
-
ctx, span := t.tracer.Start(ctx, name)
-
span.SetAttributes(attrs...)
-
return ctx, span
-
}
-
-
func MapAttrs[T any](attrs map[string]T) []attribute.KeyValue {
-
var result []attribute.KeyValue
-
for k, v := range attrs {
-
result = append(result, attribute.Key(k).String(fmt.Sprintf("%v", v)))
-
}
-
return result
-
}
···