From 54d19f66fe99873a2c85805bc430c0585a856739 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 16 Mar 2025 00:40:58 +0000 Subject: [PATCH] begin work on round-based review needs frontend bits --- appview/db/db.go | 58 +- appview/db/pulls.go | 305 ++++++--- appview/pages/funcmap.go | 5 +- appview/pages/pages.go | 15 +- appview/pages/templates/repo/pulls/pull.html | 257 +++----- appview/state/middleware.go | 3 +- appview/state/pull.go | 609 ++++++++++++++++++ appview/state/repo.go | 628 ------------------- appview/state/router.go | 2 +- 9 files changed, 989 insertions(+), 893 deletions(-) create mode 100644 appview/state/pull.go diff --git a/appview/db/db.go b/appview/db/db.go index 8c78a560..01809ee1 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -104,33 +104,73 @@ func Make(dbPath string) (*DB, error) { foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade ); create table if not exists pulls ( + -- identifiers id integer primary key autoincrement, - owner_did text not null, - repo_at text not null, pull_id integer not null, + + -- at identifiers + repo_at text not null, + owner_did text not null, + rkey text not null, + pull_at text, + + -- content title text not null, body text not null, - patch text, - pull_at text, - rkey text not null, target_branch text not null, state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed + + -- meta created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + -- constraints unique(repo_at, pull_id), foreign key (repo_at) references repos(at_uri) on delete cascade ); + + -- every pull must have atleast 1 submission: the initial submission + create table if not exists pull_submissions ( + -- identifiers + id integer primary key autoincrement, + pull_id integer not null, + + -- at identifiers + repo_at text not null, + + -- content, these are immutable, and require a resubmission to update + round_number integer not null default 0, + patch text, + + -- meta + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + -- constraints + unique(repo_at, pull_id, round_number), + foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade + ); + create table if not exists pull_comments ( + -- identifiers id integer primary key autoincrement, - owner_did text not null, pull_id integer not null, + submission_id integer not null, + + -- at identifiers repo_at text not null, - comment_id integer not null, + owner_did text not null, comment_at text not null, + + -- content body text not null, + + -- meta created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - unique(pull_id, comment_id), - foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade + + -- constraints + foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade, + foreign key (submission_id) references pull_submissions(id) on delete cascade ); + create table if not exists _jetstream ( id integer primary key autoincrement, last_time_us integer not null diff --git a/appview/db/pulls.go b/appview/db/pulls.go index d426a7fd..37e98ad5 100644 --- a/appview/db/pulls.go +++ b/appview/db/pulls.go @@ -2,6 +2,8 @@ package db import ( "database/sql" + "fmt" + "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" @@ -39,29 +41,65 @@ func (p PullState) IsClosed() bool { } type Pull struct { - ID int - OwnerDid string - RepoAt syntax.ATURI - PullAt syntax.ATURI - TargetBranch string - Patch string - PullId int + // ids + ID int + PullId int + + // at ids + RepoAt syntax.ATURI + OwnerDid string + Rkey string + PullAt syntax.ATURI + + // content Title string Body string + TargetBranch string State PullState - Created time.Time - Rkey string + Submissions []*PullSubmission + + // meta + Created time.Time +} + +type PullSubmission struct { + // ids + ID int + PullId int + + // at ids + RepoAt syntax.ATURI + + // content + RoundNumber int + Patch string + Comments []PullComment + + // meta + Created time.Time } type PullComment struct { - ID int - OwnerDid string - PullId int + // ids + ID int + PullId int + SubmissionId int + + // at ids RepoAt string - CommentId int + OwnerDid string CommentAt string - Body string - Created time.Time + + // content + Body string + + // meta + Created time.Time +} + +func (p *Pull) LatestPatch() string { + latestSubmission := p.Submissions[len(p.Submissions)-1] + return latestSubmission.Patch } func NewPull(tx *sql.Tx, pull *Pull) error { @@ -90,9 +128,17 @@ func NewPull(tx *sql.Tx, pull *Pull) error { pull.State = PullOpen _, err = tx.Exec(` - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, patch, rkey, state) - values (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Patch, pull.Rkey, pull.State) + insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state) + values (?, ?, ?, ?, ?, ?, ?, ?) + `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State) + if err != nil { + return err + } + + _, err = tx.Exec(` + insert into pull_submissions (pull_id, repo_at, round_number, patch) + values (?, ?, ?, ?) + `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch) if err != nil { return err } @@ -134,7 +180,6 @@ func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]Pull, error) { target_branch, pull_at, body, - patch, rkey from pulls @@ -150,7 +195,17 @@ func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]Pull, error) { for rows.Next() { var pull Pull var createdAt string - err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) + err := rows.Scan( + &pull.OwnerDid, + &pull.PullId, + &createdAt, + &pull.Title, + &pull.State, + &pull.TargetBranch, + &pull.PullAt, + &pull.Body, + &pull.Rkey, + ) if err != nil { return nil, err } @@ -172,12 +227,39 @@ func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]Pull, error) { } func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { - query := `select owner_did, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` + query := ` + select + owner_did, + pull_id, + created, + title, + state, + target_branch, + pull_at, + repo_at, + body, + rkey + from + pulls + where + repo_at = ? and pull_id = ? + ` row := e.QueryRow(query, repoAt, pullId) var pull Pull var createdAt string - err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) + err := row.Scan( + &pull.OwnerDid, + &pull.PullId, + &createdAt, + &pull.Title, + &pull.State, + &pull.TargetBranch, + &pull.PullAt, + &pull.RepoAt, + &pull.Body, + &pull.Rkey, + ) if err != nil { return nil, err } @@ -188,82 +270,142 @@ func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { } pull.Created = createdTime - return &pull, nil -} - -func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, []PullComment, error) { - query := `select owner_did, pull_id, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` - row := e.QueryRow(query, repoAt, pullId) - - var pull Pull - var createdAt string - err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) + submissionsQuery := ` + select + id, pull_id, repo_at, round_number, patch, created + from + pull_submissions + where + repo_at = ? and pull_id = ? + ` + submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) if err != nil { - return nil, nil, err + return nil, err } + defer submissionsRows.Close() + + submissionsMap := make(map[int]*PullSubmission) + + for submissionsRows.Next() { + var submission PullSubmission + var submissionCreatedStr string + err := submissionsRows.Scan( + &submission.ID, + &submission.PullId, + &submission.RepoAt, + &submission.RoundNumber, + &submission.Patch, + &submissionCreatedStr, + ) + if err != nil { + return nil, err + } - createdTime, err := time.Parse(time.RFC3339, createdAt) - if err != nil { - return nil, nil, err - } - pull.Created = createdTime + submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) + if err != nil { + return nil, err + } + submission.Created = submissionCreatedTime - comments, err := GetPullComments(e, repoAt, pullId) - if err != nil { - return nil, nil, err + submissionsMap[submission.ID] = &submission + } + if err = submissionsRows.Close(); err != nil { + return nil, err + } + if len(submissionsMap) == 0 { + return &pull, nil } - return &pull, comments, nil -} - -func NewPullComment(e Execer, comment *PullComment) error { - query := `insert into pull_comments (owner_did, repo_at, comment_at, pull_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` - _, err := e.Exec( - query, - comment.OwnerDid, - comment.RepoAt, - comment.CommentAt, - comment.PullId, - comment.CommentId, - comment.Body, - ) - return err -} - -func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComment, error) { - var comments []PullComment - - rows, err := e.Query(`select owner_did, pull_id, comment_id, comment_at, body, created from pull_comments where repo_at = ? and pull_id = ? order by created asc`, repoAt, pullId) - if err == sql.ErrNoRows { - return []PullComment{}, nil + var args []any + for k := range submissionsMap { + args = append(args, k) } + inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") + commentsQuery := fmt.Sprintf(` + select + id, + pull_id, + submission_id, + repo_at, + owner_did, + comment_at, + body, + created + from + pull_comments + where + submission_id IN (%s) + order by + created asc + `, inClause) + commentsRows, err := e.Query(commentsQuery, args...) if err != nil { return nil, err } - defer rows.Close() + defer commentsRows.Close() - for rows.Next() { + for commentsRows.Next() { var comment PullComment - var createdAt string - err := rows.Scan(&comment.OwnerDid, &comment.PullId, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt) + var commentCreatedStr string + err := commentsRows.Scan( + &comment.ID, + &comment.PullId, + &comment.SubmissionId, + &comment.RepoAt, + &comment.OwnerDid, + &comment.CommentAt, + &comment.Body, + &commentCreatedStr, + ) if err != nil { return nil, err } - createdAtTime, err := time.Parse(time.RFC3339, createdAt) + commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) if err != nil { return nil, err } - comment.Created = createdAtTime + comment.Created = commentCreatedTime - comments = append(comments, comment) - } + // Add the comment to its submission + if submission, ok := submissionsMap[comment.SubmissionId]; ok { + submission.Comments = append(submission.Comments, comment) + } - if err := rows.Err(); err != nil { + } + if err = commentsRows.Err(); err != nil { return nil, err } - return comments, nil + pull.Submissions = make([]*PullSubmission, len(submissionsMap)) + for _, submission := range submissionsMap { + pull.Submissions[submission.RoundNumber] = submission + } + + return &pull, 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.SubmissionId, + comment.CommentAt, + comment.PullId, + comment.Body, + ) + if err != nil { + return 0, err + } + + i, err := res.LastInsertId() + if err != nil { + return 0, err + } + + return i, nil } func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error { @@ -286,6 +428,16 @@ func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error { return err } +func ResubmitPull(e Execer, pull *Pull, newPatch string) error { + newRoundNumber := len(pull.Submissions) + _, err := e.Exec(` + insert into pull_submissions (pull_id, repo_at, round_number, patch) + values (?, ?, ?, ?) + `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch) + + return err +} + type PullCount struct { Open int Merged int @@ -313,8 +465,3 @@ func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) { return count, nil } - -func EditPatch(e Execer, repoAt syntax.ATURI, pullId int, patch string) error { - _, err := e.Exec(`update pulls set patch = ? where repo_at = ? and pull_id = ?`, patch, repoAt, pullId) - return err -} diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index 330be147..10b59251 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -58,7 +58,7 @@ func funcMap() template.FuncMap { }, "timeFmt": humanize.Time, "byteFmt": humanize.Bytes, - "length": func(slice interface{}) int { + "length": func(slice any) int { v := reflect.ValueOf(slice) if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { return v.Len() @@ -110,5 +110,8 @@ func funcMap() template.FuncMap { // returns false for other "zero" values return t == nil }, + "list": func(args ...any) []any { + return args + }, } } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index eb2c7986..57c14795 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -543,14 +543,13 @@ func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { } type RepoSinglePullParams struct { - LoggedInUser *auth.User - RepoInfo RepoInfo - DidHandleMap map[string]string - Pull db.Pull - PullOwnerHandle string - Comments []db.PullComment - Active string - MergeCheck types.MergeCheckResponse + LoggedInUser *auth.User + RepoInfo RepoInfo + Active string + DidHandleMap map[string]string + + Pull db.Pull + MergeCheck types.MergeCheckResponse } func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html index c4910854..87b4f0f1 100644 --- a/appview/pages/templates/repo/pulls/pull.html +++ b/appview/pages/templates/repo/pulls/pull.html @@ -1,6 +1,5 @@ {{ define "title" }} - {{ .Pull.Title }} · pull #{{ .Pull.PullId }} · - {{ .RepoInfo.FullName }} + {{ .Pull.Title }} · pull #{{ .Pull.PullId }} · {{ .RepoInfo.FullName }} {{ end }} {{ define "repoContent" }} @@ -22,7 +21,6 @@ {{ $icon = "git-merge" }} {{ end }} -
-
-
- - - patch - -
-
-       {{- .Pull.Patch -}}
-                
-
- - -
- - - -
-
- -
-
-
- -
-
{{ end }} {{ define "repoAfter" }} +
+ {{ block "submissions" . }} {{ end }} +
+ {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} -
- {{ block "comments" . }} {{ end }} + {{ if $isPullAuthor }} +
+ {{ block "resubmitCard" . }} {{ end }} +
+ {{ end }} +
{{ if .Pull.State.IsMerged }} {{ block "alreadyMergedCard" . }} {{ end }} {{ else if .MergeCheck }} @@ -168,8 +81,6 @@ {{ end }}
- {{ block "newComment" . }} {{ end }} - {{ if and (or $isPullAuthor $isPushAllowed) (not .Pull.State.IsMerged) }} {{ $action := "close" }} {{ $icon := "circle-x" }} @@ -192,66 +103,52 @@
{{ end }} -{{ define "comments" }} - {{ range $index, $comment := .Comments }} -
- {{ if eq $index 0 }} -
- {{ else }} -
- {{ end }} -
- {{ $owner := index $.DidHandleMap .OwnerDid }} - - {{ $owner }} - - - - {{ .Created | timeFmt }} - -
-
- {{ .Body | markdown }} +{{ define "submissions" }} + {{ $lastIdx := sub (len .Pull.Submissions) 1 }} + {{ range $idx, $item := .Pull.Submissions }} + {{ with $item }} +
+ round #{{ .RoundNumber }}, {{ .Created | timeFmt }}, received {{ len .Comments }} comments +
+

patch submitted by {{index $.DidHandleMap $.Pull.OwnerDid}}

+
{{- .Patch -}}
+ + {{ range .Comments }} +
+ {{ index $.DidHandleMap .OwnerDid }} commented {{ .Created | timeFmt }}: {{ .Body }}
+ {{ end }} + {{ block "newComment" (list $ .ID) }} {{ end }}
+
+ {{ end }} {{ end }} {{ end }} {{ define "newComment" }} - {{ if .LoggedInUser }} -
- - -
-
- {{ else }} -
- login to join the discussion -
+ {{ $rootObj := index . 0 }} + {{ $submissionId := index . 1 }} + + {{ with $rootObj }} + {{ if .LoggedInUser }} +
+ + + +
+
+ {{ else }} +
+ login to join the discussion +
+ {{ end }} {{ end }} {{ end }} @@ -287,11 +184,6 @@
- {{ if gt (len .Comments) 0 }} -
- {{ else }} -
- {{ end }}
@@ -328,11 +220,6 @@
- {{ if gt (len .Comments) 0 }} -
- {{ else }} -
- {{ end }}
@@ -353,7 +240,7 @@ {{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }} disabled {{ end }}> - + merge {{ end }} @@ -363,3 +250,43 @@
{{ end }} + +{{ define "resubmitCard" }} +
+ +
+ + Resubmit your patch +
+ +
+ You can update this patch to address reviews if any. + This begins a new round of reviews, + you can still view your previous submissions and reviews. +
+ +
+
+ + +
+ +
+
+
+
+{{ end }} diff --git a/appview/state/middleware.go b/appview/state/middleware.go index bce4a919..e1c35f2b 100644 --- a/appview/state/middleware.go +++ b/appview/state/middleware.go @@ -240,14 +240,13 @@ func ResolvePull(s *State) Middleware { return } - pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt) + 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) - ctx = context.WithValue(ctx, "pull_comments", comments) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/appview/state/pull.go b/appview/state/pull.go new file mode 100644 index 00000000..c91ad1a8 --- /dev/null +++ b/appview/state/pull.go @@ -0,0 +1,609 @@ +package state + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" + "time" + + "github.com/sotangled/tangled/api/tangled" + "github.com/sotangled/tangled/appview/db" + "github.com/sotangled/tangled/appview/pages" + "github.com/sotangled/tangled/types" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + f, err := 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) + } + + identsToResolve := make([]string, totalIdents) + + // populate idents + identsToResolve[0] = pull.OwnerDid + idx := 1 + for _, submission := range pull.Submissions { + for _, comment := range submission.Comments { + identsToResolve[idx] = comment.OwnerDid + idx += 1 + } + } + + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) + didHandleMap := make(map[string]string) + for _, identity := range resolvedIds { + if !identity.Handle.IsInvalidHandle() { + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) + } else { + didHandleMap[identity.DID.String()] = identity.DID.String() + } + } + + var mergeCheckResponse types.MergeCheckResponse + + // Only perform merge check if the pull request is not already merged + if pull.State != db.PullMerged { + secret, err := db.GetRegistrationKey(s.db, f.Knot) + if err != nil { + log.Printf("failed to get registration key for %s", f.Knot) + s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") + return + } + + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + if err == nil { + resp, err := ksClient.MergeCheck([]byte(pull.LatestPatch()), pull.OwnerDid, f.RepoName, pull.TargetBranch) + if err != nil { + log.Println("failed to check for mergeability:", err) + } else { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Println("failed to read merge check response body") + } else { + err = json.Unmarshal(respBody, &mergeCheckResponse) + if err != nil { + log.Println("failed to unmarshal merge check response", err) + } + } + } + } else { + log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) + } + } + + s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ + LoggedInUser: user, + RepoInfo: f.RepoInfo(s, user), + DidHandleMap: didHandleMap, + Pull: *pull, + MergeCheck: mergeCheckResponse, + }) +} + +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": + state = db.PullClosed + case "merged": + state = db.PullMerged + } + + f, err := 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 + } + + 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()] = fmt.Sprintf("@%s", identity.Handle.String()) + } else { + 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 := 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.MethodPost: + body := r.FormValue("body") + if body == "" { + s.pages.Notice(w, "pull", "Comment body is required") + return + } + + submissionIdstr := r.FormValue("submissionId") + submissionId, err := strconv.Atoi(submissionIdstr) + if err != nil { + s.pages.Notice(w, "pull", "Invalid comment submission.") + 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: s.TID(), + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.RepoPullComment{ + Repo: &atUri, + Pull: pullAt, + Owner: &ownerDid, + Body: &body, + CreatedAt: &createdAt, + }, + }, + }) + 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, + Body: body, + CommentAt: atResp.Uri, + SubmissionId: submissionId, + }) + 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.") + return + } + + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) + return + } +} + +func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + f, err := 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 + } + + var result types.RepoBranchesResponse + 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") + patch := r.FormValue("patch") + + if title == "" || body == "" || patch == "" || targetBranch == "" { + s.pages.Notice(w, "pull", "Title, body and patch diff are required.") + return + } + + 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 + } + defer tx.Rollback() + + rkey := s.TID() + initialSubmission := db.PullSubmission{ + Patch: patch, + } + err = db.NewPull(tx, &db.Pull{ + Title: title, + Body: body, + TargetBranch: targetBranch, + OwnerDid: user.Did, + RepoAt: f.RepoAt, + Rkey: rkey, + Submissions: []*db.PullSubmission{ + &initialSubmission, + }, + }) + 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 + } + + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.RepoPullNSID, + Repo: user.Did, + Rkey: rkey, + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.RepoPull{ + Title: title, + PullId: int64(pullId), + TargetRepo: string(f.RepoAt), + TargetBranch: targetBranch, + Patch: patch, + }, + }, + }) + + err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) + 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 + } + + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) + return + } +} + +func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + f, err := 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.MethodPost: + patch := r.FormValue("patch") + + if patch == "" { + s.pages.Notice(w, "resubmit-error", "Patch is empty.") + return + } + + if patch == pull.LatestPatch() { + s.pages.Notice(w, "resubmit-error", "Patch is identical to previous 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 + } + defer tx.Rollback() + + err = db.ResubmitPull(tx, pull, patch) + 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 + } + + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.RepoPullNSID, + Repo: user.Did, + Rkey: pull.Rkey, + SwapRecord: ex.Cid, + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.RepoPull{ + Title: pull.Title, + PullId: int64(pull.PullId), + TargetRepo: string(f.RepoAt), + TargetBranch: pull.TargetBranch, + Patch: patch, // new patch + }, + }, + }) + 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 + } + + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) + return + } +} + +func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + f, err := 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 + } + + 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 + } + + // Merge the pull request + resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch) + 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 := 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 + } + + // Close the pull in the database + 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 + } + + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) + return +} + +func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + + f, err := 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 + } + + // Reopen the pull in the database + 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 + } + + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) + return +} diff --git a/appview/state/repo.go b/appview/state/repo.go index fd4b64f8..1a6e3bf8 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -230,329 +230,6 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { } } -func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - - patch := r.FormValue("patch") - if patch == "" { - s.pages.Notice(w, "pull-error", "Patch is required.") - 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 pull.OwnerDid != user.Did { - log.Println("failed to edit pull information") - s.pages.Notice(w, "pull-error", "Unauthorized") - return - } - - f, err := fullyResolvedRepo(r) - if err != nil { - log.Println("failed to get repo and knot", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - // Start a transaction for database operations - tx, err := s.db.BeginTx(r.Context(), nil) - if err != nil { - log.Println("failed to start transaction", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - // Set up deferred rollback that will be overridden by commit if successful - defer tx.Rollback() - - // Update patch in the database within transaction - err = db.EditPatch(tx, f.RepoAt, pull.PullId, patch) - if err != nil { - log.Println("failed to update patch", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - // Update the atproto record - client, _ := s.auth.AuthorizedClient(r) - pullAt := pull.PullAt - - // Get the existing record first - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pullAt.RecordKey().String()) - if err != nil { - log.Println("failed to get existing pull record", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - // Update the record - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.RepoPullNSID, - Repo: user.Did, - Rkey: pullAt.RecordKey().String(), - SwapRecord: ex.Cid, - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.RepoPull{ - Title: pull.Title, - PullId: int64(pull.PullId), - TargetRepo: string(f.RepoAt), - TargetBranch: pull.TargetBranch, - Patch: patch, - }, - }, - }) - - if err != nil { - log.Println("failed to update pull record in atproto", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - // Commit the transaction now that both operations have succeeded - err = tx.Commit() - if err != nil { - log.Println("failed to commit transaction", err) - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - targetBranch := pull.TargetBranch - - // Perform merge check - secret, err := db.GetRegistrationKey(s.db, f.Knot) - if err != nil { - log.Printf("no key found for domain %s: %s\n", f.Knot, err) - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") - return - } - - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) - if err != nil { - log.Printf("failed to create signed client for %s", f.Knot) - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") - return - } - - resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch) - if err != nil { - log.Println("failed to check mergeability", err) - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") - return - } - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - log.Println("failed to read knotserver response body") - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") - return - } - - var mergeCheckResponse types.MergeCheckResponse - err = json.Unmarshal(respBody, &mergeCheckResponse) - if err != nil { - log.Println("failed to unmarshal merge check response", err) - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") - return - } - - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) - return -} - -func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - f, err := 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 - } - - var result types.RepoBranchesResponse - 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") - patch := r.FormValue("patch") - - if title == "" || body == "" || patch == "" || targetBranch == "" { - s.pages.Notice(w, "pull", "Title, body and patch diff are required.") - return - } - - 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 - } - - defer func() { - tx.Rollback() - err = s.enforcer.E.LoadPolicy() - if err != nil { - log.Println("failed to rollback policies") - } - }() - - err = db.NewPull(tx, &db.Pull{ - Title: title, - Body: body, - TargetBranch: targetBranch, - Patch: patch, - OwnerDid: user.Did, - RepoAt: f.RepoAt, - }) - 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 - } - - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.RepoPullNSID, - Repo: user.Did, - Rkey: s.TID(), - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.RepoPull{ - Title: title, - PullId: int64(pullId), - TargetRepo: string(f.RepoAt), - TargetBranch: targetBranch, - Patch: patch, - }, - }, - }) - - err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) - 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 - } - - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) - return - } -} - -func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - f, err := fullyResolvedRepo(r) - if err != nil { - log.Println("failed to get repo and knot", err) - return - } - - pull, ok1 := r.Context().Value("pull").(*db.Pull) - comments, ok2 := r.Context().Value("pull_comments").([]db.PullComment) - if !ok1 || !ok2 { - log.Println("failed to get pull") - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") - return - } - - identsToResolve := make([]string, len(comments)) - for i, comment := range comments { - identsToResolve[i] = comment.OwnerDid - } - identsToResolve = append(identsToResolve, 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()] = fmt.Sprintf("@%s", identity.Handle.String()) - } else { - didHandleMap[identity.DID.String()] = identity.DID.String() - } - } - - var mergeCheckResponse types.MergeCheckResponse - - // Only perform merge check if the pull request is not already merged - if pull.State != db.PullMerged { - secret, err := db.GetRegistrationKey(s.db, f.Knot) - if err != nil { - log.Printf("failed to get registration key for %s", f.Knot) - s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") - return - } - - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) - if err == nil { - resp, err := ksClient.MergeCheck([]byte(pull.Patch), pull.OwnerDid, f.RepoName, pull.TargetBranch) - if err != nil { - log.Println("failed to check for mergeability:", err) - } else { - respBody, err := io.ReadAll(resp.Body) - if err != nil { - log.Println("failed to read merge check response body") - } else { - err = json.Unmarshal(respBody, &mergeCheckResponse) - if err != nil { - log.Println("failed to unmarshal merge check response", err) - } - } - } - } else { - log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) - } - } - - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ - LoggedInUser: user, - RepoInfo: f.RepoInfo(s, user), - Pull: *pull, - Comments: comments, - DidHandleMap: didHandleMap, - MergeCheck: mergeCheckResponse, - }) -} - func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { f, err := fullyResolvedRepo(r) if err != nil { @@ -1393,311 +1070,6 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { } } -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": - state = db.PullClosed - case "merged": - state = db.PullMerged - } - - f, err := 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 - } - - 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()] = fmt.Sprintf("@%s", identity.Handle.String()) - } else { - 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) MergePull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - f, err := 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 - } - - 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 - } - - // Merge the pull request - resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch) - 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) PullComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - f, err := fullyResolvedRepo(r) - if err != nil { - log.Println("failed to get repo and knot", err) - return - } - - pullId := chi.URLParam(r, "pull") - pullIdInt, err := strconv.Atoi(pullId) - if err != nil { - http.Error(w, "bad pull id", http.StatusBadRequest) - log.Println("failed to parse pull id", err) - return - } - - switch r.Method { - 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() // Will be ignored if we commit - - commentId := rand.IntN(1000000) - createdAt := time.Now().Format(time.RFC3339) - commentIdInt64 := int64(commentId) - ownerDid := user.Did - - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pullIdInt) - 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: s.TID(), - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.RepoPullComment{ - Repo: &atUri, - Pull: pullAt, - CommentId: &commentIdInt64, - Owner: &ownerDid, - Body: &body, - CreatedAt: &createdAt, - }, - }, - }) - 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 - err = db.NewPullComment(tx, &db.PullComment{ - OwnerDid: user.Did, - RepoAt: f.RepoAt.String(), - CommentId: commentId, - PullId: pullIdInt, - Body: body, - CommentAt: atResp.Uri, - }) - 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.") - return - } - - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pullIdInt, commentId)) - return - } -} - -func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - - f, err := 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 - } - - // Close the pull in the database - 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 - } - - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) - return -} - -func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - - f, err := 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 - } - - // Reopen the pull in the database - 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 - } - - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) - return -} - func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { repoName := chi.URLParam(r, "repo") knot, ok := r.Context().Value("knot").(string) diff --git a/appview/state/router.go b/appview/state/router.go index 3fc19fc4..149ae1f5 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -70,7 +70,7 @@ func (s *State) UserRouter() http.Handler { // authorized requests below this point r.Group(func(r chi.Router) { r.Use(AuthMiddleware(s)) - r.Patch("/patch", s.EditPatch) + r.Post("/resubmit", s.ResubmitPull) r.Post("/comment", s.PullComment) r.Post("/close", s.ClosePull) r.Post("/reopen", s.ReopenPull) -- 2.51.2