Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

begin work on round-based review

needs frontend bits

Akshay 54d19f66 c2e645e0

+1000 -904
+51 -11
appview/db/db.go
··· 104 104 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade 105 105 ); 106 106 create table if not exists pulls ( 107 + -- identifiers 107 108 id integer primary key autoincrement, 108 - owner_did text not null, 109 - repo_at text not null, 110 109 pull_id integer not null, 110 + 111 + -- at identifiers 112 + repo_at text not null, 113 + owner_did text not null, 114 + rkey text not null, 115 + pull_at text, 116 + 117 + -- content 111 118 title text not null, 112 119 body text not null, 113 - patch text, 114 - pull_at text, 115 - rkey text not null, 116 120 target_branch text not null, 117 121 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed 122 + 123 + -- meta 118 124 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 125 + 126 + -- constraints 119 127 unique(repo_at, pull_id), 120 128 foreign key (repo_at) references repos(at_uri) on delete cascade 121 129 ); 122 - create table if not exists pull_comments ( 130 + 131 + -- every pull must have atleast 1 submission: the initial submission 132 + create table if not exists pull_submissions ( 133 + -- identifiers 123 134 id integer primary key autoincrement, 124 - owner_did text not null, 125 135 pull_id integer not null, 136 + 137 + -- at identifiers 126 138 repo_at text not null, 127 - comment_id integer not null, 128 - comment_at text not null, 129 - body text not null, 139 + 140 + -- content, these are immutable, and require a resubmission to update 141 + round_number integer not null default 0, 142 + patch text, 143 + 144 + -- meta 130 145 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 131 - unique(pull_id, comment_id), 146 + 147 + -- constraints 148 + unique(repo_at, pull_id, round_number), 132 149 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade 133 150 ); 151 + 152 + create table if not exists pull_comments ( 153 + -- identifiers 154 + id integer primary key autoincrement, 155 + pull_id integer not null, 156 + submission_id integer not null, 157 + 158 + -- at identifiers 159 + repo_at text not null, 160 + owner_did text not null, 161 + comment_at text not null, 162 + 163 + -- content 164 + body text not null, 165 + 166 + -- meta 167 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 168 + 169 + -- constraints 170 + foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade, 171 + foreign key (submission_id) references pull_submissions(id) on delete cascade 172 + ); 173 + 134 174 create table if not exists _jetstream ( 135 175 id integer primary key autoincrement, 136 176 last_time_us integer not null
+235 -88
appview/db/pulls.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "fmt" 6 + "strings" 5 7 "time" 6 8 7 9 "github.com/bluesky-social/indigo/atproto/syntax" ··· 41 39 } 42 40 43 41 type Pull struct { 44 - ID int 45 - OwnerDid string 46 - RepoAt syntax.ATURI 47 - PullAt syntax.ATURI 48 - TargetBranch string 49 - Patch string 50 - PullId int 42 + // ids 43 + ID int 44 + PullId int 45 + 46 + // at ids 47 + RepoAt syntax.ATURI 48 + OwnerDid string 49 + Rkey string 50 + PullAt syntax.ATURI 51 + 52 + // content 51 53 Title string 52 54 Body string 55 + TargetBranch string 53 56 State PullState 54 - Created time.Time 55 - Rkey string 57 + Submissions []*PullSubmission 58 + 59 + // meta 60 + Created time.Time 61 + } 62 + 63 + type PullSubmission struct { 64 + // ids 65 + ID int 66 + PullId int 67 + 68 + // at ids 69 + RepoAt syntax.ATURI 70 + 71 + // content 72 + RoundNumber int 73 + Patch string 74 + Comments []PullComment 75 + 76 + // meta 77 + Created time.Time 56 78 } 57 79 58 80 type PullComment struct { 59 - ID int 60 - OwnerDid string 61 - PullId int 81 + // ids 82 + ID int 83 + PullId int 84 + SubmissionId int 85 + 86 + // at ids 62 87 RepoAt string 63 - CommentId int 88 + OwnerDid string 64 89 CommentAt string 65 - Body string 66 - Created time.Time 90 + 91 + // content 92 + Body string 93 + 94 + // meta 95 + Created time.Time 96 + } 97 + 98 + func (p *Pull) LatestPatch() string { 99 + latestSubmission := p.Submissions[len(p.Submissions)-1] 100 + return latestSubmission.Patch 67 101 } 68 102 69 103 func NewPull(tx *sql.Tx, pull *Pull) error { ··· 128 90 pull.State = PullOpen 129 91 130 92 _, err = tx.Exec(` 131 - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, patch, rkey, state) 132 - values (?, ?, ?, ?, ?, ?, ?, ?, ?) 133 - `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Patch, pull.Rkey, pull.State) 93 + insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state) 94 + values (?, ?, ?, ?, ?, ?, ?, ?) 95 + `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State) 96 + if err != nil { 97 + return err 98 + } 99 + 100 + _, err = tx.Exec(` 101 + insert into pull_submissions (pull_id, repo_at, round_number, patch) 102 + values (?, ?, ?, ?) 103 + `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch) 134 104 if err != nil { 135 105 return err 136 106 } ··· 180 134 target_branch, 181 135 pull_at, 182 136 body, 183 - patch, 184 137 rkey 185 138 from 186 139 pulls ··· 195 150 for rows.Next() { 196 151 var pull Pull 197 152 var createdAt string 198 - err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 153 + err := rows.Scan( 154 + &pull.OwnerDid, 155 + &pull.PullId, 156 + &createdAt, 157 + &pull.Title, 158 + &pull.State, 159 + &pull.TargetBranch, 160 + &pull.PullAt, 161 + &pull.Body, 162 + &pull.Rkey, 163 + ) 199 164 if err != nil { 200 165 return nil, err 201 166 } ··· 227 172 } 228 173 229 174 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 230 - query := `select owner_did, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` 175 + query := ` 176 + select 177 + owner_did, 178 + pull_id, 179 + created, 180 + title, 181 + state, 182 + target_branch, 183 + pull_at, 184 + repo_at, 185 + body, 186 + rkey 187 + from 188 + pulls 189 + where 190 + repo_at = ? and pull_id = ? 191 + ` 231 192 row := e.QueryRow(query, repoAt, pullId) 232 193 233 194 var pull Pull 234 195 var createdAt string 235 - err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 196 + err := row.Scan( 197 + &pull.OwnerDid, 198 + &pull.PullId, 199 + &createdAt, 200 + &pull.Title, 201 + &pull.State, 202 + &pull.TargetBranch, 203 + &pull.PullAt, 204 + &pull.RepoAt, 205 + &pull.Body, 206 + &pull.Rkey, 207 + ) 236 208 if err != nil { 237 209 return nil, err 238 210 } ··· 269 187 return nil, err 270 188 } 271 189 pull.Created = createdTime 190 + 191 + submissionsQuery := ` 192 + select 193 + id, pull_id, repo_at, round_number, patch, created 194 + from 195 + pull_submissions 196 + where 197 + repo_at = ? and pull_id = ? 198 + ` 199 + submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) 200 + if err != nil { 201 + return nil, err 202 + } 203 + defer submissionsRows.Close() 204 + 205 + submissionsMap := make(map[int]*PullSubmission) 206 + 207 + for submissionsRows.Next() { 208 + var submission PullSubmission 209 + var submissionCreatedStr string 210 + err := submissionsRows.Scan( 211 + &submission.ID, 212 + &submission.PullId, 213 + &submission.RepoAt, 214 + &submission.RoundNumber, 215 + &submission.Patch, 216 + &submissionCreatedStr, 217 + ) 218 + if err != nil { 219 + return nil, err 220 + } 221 + 222 + submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 223 + if err != nil { 224 + return nil, err 225 + } 226 + submission.Created = submissionCreatedTime 227 + 228 + submissionsMap[submission.ID] = &submission 229 + } 230 + if err = submissionsRows.Close(); err != nil { 231 + return nil, err 232 + } 233 + if len(submissionsMap) == 0 { 234 + return &pull, nil 235 + } 236 + 237 + var args []any 238 + for k := range submissionsMap { 239 + args = append(args, k) 240 + } 241 + inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 242 + commentsQuery := fmt.Sprintf(` 243 + select 244 + id, 245 + pull_id, 246 + submission_id, 247 + repo_at, 248 + owner_did, 249 + comment_at, 250 + body, 251 + created 252 + from 253 + pull_comments 254 + where 255 + submission_id IN (%s) 256 + order by 257 + created asc 258 + `, inClause) 259 + commentsRows, err := e.Query(commentsQuery, args...) 260 + if err != nil { 261 + return nil, err 262 + } 263 + defer commentsRows.Close() 264 + 265 + for commentsRows.Next() { 266 + var comment PullComment 267 + var commentCreatedStr string 268 + err := commentsRows.Scan( 269 + &comment.ID, 270 + &comment.PullId, 271 + &comment.SubmissionId, 272 + &comment.RepoAt, 273 + &comment.OwnerDid, 274 + &comment.CommentAt, 275 + &comment.Body, 276 + &commentCreatedStr, 277 + ) 278 + if err != nil { 279 + return nil, err 280 + } 281 + 282 + commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 283 + if err != nil { 284 + return nil, err 285 + } 286 + comment.Created = commentCreatedTime 287 + 288 + // Add the comment to its submission 289 + if submission, ok := submissionsMap[comment.SubmissionId]; ok { 290 + submission.Comments = append(submission.Comments, comment) 291 + } 292 + 293 + } 294 + if err = commentsRows.Err(); err != nil { 295 + return nil, err 296 + } 297 + 298 + pull.Submissions = make([]*PullSubmission, len(submissionsMap)) 299 + for _, submission := range submissionsMap { 300 + pull.Submissions[submission.RoundNumber] = submission 301 + } 272 302 273 303 return &pull, nil 274 304 } 275 305 276 - func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, []PullComment, error) { 277 - query := `select owner_did, pull_id, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` 278 - row := e.QueryRow(query, repoAt, pullId) 279 - 280 - var pull Pull 281 - var createdAt string 282 - err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 283 - if err != nil { 284 - return nil, nil, err 285 - } 286 - 287 - createdTime, err := time.Parse(time.RFC3339, createdAt) 288 - if err != nil { 289 - return nil, nil, err 290 - } 291 - pull.Created = createdTime 292 - 293 - comments, err := GetPullComments(e, repoAt, pullId) 294 - if err != nil { 295 - return nil, nil, err 296 - } 297 - 298 - return &pull, comments, nil 299 - } 300 - 301 - func NewPullComment(e Execer, comment *PullComment) error { 302 - query := `insert into pull_comments (owner_did, repo_at, comment_at, pull_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` 303 - _, err := e.Exec( 306 + func NewPullComment(e Execer, comment *PullComment) (int64, error) { 307 + query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 308 + res, err := e.Exec( 304 309 query, 305 310 comment.OwnerDid, 306 311 comment.RepoAt, 312 + comment.SubmissionId, 307 313 comment.CommentAt, 308 314 comment.PullId, 309 - comment.CommentId, 310 315 comment.Body, 311 316 ) 312 - return err 313 - } 314 - 315 - func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComment, error) { 316 - var comments []PullComment 317 - 318 - 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) 319 - if err == sql.ErrNoRows { 320 - return []PullComment{}, nil 321 - } 322 317 if err != nil { 323 - return nil, err 324 - } 325 - defer rows.Close() 326 - 327 - for rows.Next() { 328 - var comment PullComment 329 - var createdAt string 330 - err := rows.Scan(&comment.OwnerDid, &comment.PullId, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt) 331 - if err != nil { 332 - return nil, err 333 - } 334 - 335 - createdAtTime, err := time.Parse(time.RFC3339, createdAt) 336 - if err != nil { 337 - return nil, err 338 - } 339 - comment.Created = createdAtTime 340 - 341 - comments = append(comments, comment) 318 + return 0, err 342 319 } 343 320 344 - if err := rows.Err(); err != nil { 345 - return nil, err 321 + i, err := res.LastInsertId() 322 + if err != nil { 323 + return 0, err 346 324 } 347 325 348 - return comments, nil 326 + return i, nil 349 327 } 350 328 351 329 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error { ··· 425 283 426 284 func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error { 427 285 err := SetPullState(e, repoAt, pullId, PullMerged) 286 + return err 287 + } 288 + 289 + func ResubmitPull(e Execer, pull *Pull, newPatch string) error { 290 + newRoundNumber := len(pull.Submissions) 291 + _, err := e.Exec(` 292 + insert into pull_submissions (pull_id, repo_at, round_number, patch) 293 + values (?, ?, ?, ?) 294 + `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch) 295 + 428 296 return err 429 297 } 430 298 ··· 464 312 } 465 313 466 314 return count, nil 467 - } 468 - 469 - func EditPatch(e Execer, repoAt syntax.ATURI, pullId int, patch string) error { 470 - _, err := e.Exec(`update pulls set patch = ? where repo_at = ? and pull_id = ?`, patch, repoAt, pullId) 471 - return err 472 315 }
+4 -1
appview/pages/funcmap.go
··· 58 58 }, 59 59 "timeFmt": humanize.Time, 60 60 "byteFmt": humanize.Bytes, 61 - "length": func(slice interface{}) int { 61 + "length": func(slice any) int { 62 62 v := reflect.ValueOf(slice) 63 63 if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { 64 64 return v.Len() ··· 109 109 "isNil": func(t any) bool { 110 110 // returns false for other "zero" values 111 111 return t == nil 112 + }, 113 + "list": func(args ...any) []any { 114 + return args 112 115 }, 113 116 } 114 117 }
+7 -8
appview/pages/pages.go
··· 543 543 } 544 544 545 545 type RepoSinglePullParams struct { 546 - LoggedInUser *auth.User 547 - RepoInfo RepoInfo 548 - DidHandleMap map[string]string 549 - Pull db.Pull 550 - PullOwnerHandle string 551 - Comments []db.PullComment 552 - Active string 553 - MergeCheck types.MergeCheckResponse 546 + LoggedInUser *auth.User 547 + RepoInfo RepoInfo 548 + Active string 549 + DidHandleMap map[string]string 550 + 551 + Pull db.Pull 552 + MergeCheck types.MergeCheckResponse 554 553 } 555 554 556 555 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+92 -165
appview/pages/templates/repo/pulls/pull.html
··· 1 1 {{ define "title" }} 2 - {{ .Pull.Title }} &middot; pull #{{ .Pull.PullId }} &middot; 3 - {{ .RepoInfo.FullName }} 2 + {{ .Pull.Title }} &middot; pull #{{ .Pull.PullId }} &middot; {{ .RepoInfo.FullName }} 4 3 {{ end }} 5 4 6 5 {{ define "repoContent" }} ··· 20 21 {{ $bgColor = "bg-purple-600" }} 21 22 {{ $icon = "git-merge" }} 22 23 {{ end }} 23 - 24 24 25 25 <section> 26 26 <div class="flex items-center gap-2"> ··· 53 55 {{ end }} 54 56 </section> 55 57 56 - <div class="flex flex-col justify-end mt-4"> 57 - <details> 58 - <summary 59 - class="list-none cursor-pointer sticky top-0 bg-white rounded-sm px-3 py-2 border border-gray-200 flex items-center text-gray-700 hover:bg-gray-50 transition-colors mt-auto" 60 - > 61 - <i data-lucide="code" class="w-4 h-4 mr-2"></i> 62 - <span>patch</span> 63 - </summary> 64 - <div class="relative"> 65 - <pre 66 - id="patch-preview" 67 - class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm" 68 - > 69 - {{- .Pull.Patch -}} 70 - </pre 71 - > 72 - <form 73 - id="patch-form" 74 - hx-patch="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/patch" 75 - hx-swap="none" 76 - > 77 - <textarea 78 - id="patch" 79 - name="patch" 80 - class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden" 81 - >{{- .Pull.Patch -}}</textarea> 82 - 83 - <div class="flex gap-2 justify-end mt-2"> 84 - <button 85 - id="edit-patch-btn" 86 - type="button" 87 - class="btn btn-sm" 88 - onclick="togglePatchEdit(true)" 89 - {{ if or .Pull.State.IsMerged .Pull.State.IsClosed }} 90 - disabled title="Cannot edit closed or merged 91 - pull requests" 92 - {{ end }} 93 - > 94 - <i data-lucide="edit" class="w-4 h-4 mr-1"></i>Edit 95 - </button> 96 - <button 97 - id="save-patch-btn" 98 - type="submit" 99 - class="btn btn-sm bg-green-500 hidden" 100 - > 101 - <i data-lucide="save" class="w-4 h-4 mr-1"></i>Save 102 - </button> 103 - <button 104 - id="cancel-patch-btn" 105 - type="button" 106 - class="btn btn-sm bg-gray-300 hidden" 107 - onclick="togglePatchEdit(false)" 108 - > 109 - Cancel 110 - </button> 111 - </div> 112 - </form> 113 - 114 - <div id="pull-error" class="error"></div> 115 - <div id="pull-success" class="success"></div> 116 - </div> 117 - <script> 118 - function togglePatchEdit(editMode) { 119 - const preview = document.getElementById("patch-preview"); 120 - const editor = document.getElementById("patch"); 121 - const editBtn = document.getElementById("edit-patch-btn"); 122 - const saveBtn = document.getElementById("save-patch-btn"); 123 - const cancelBtn = 124 - document.getElementById("cancel-patch-btn"); 125 - 126 - if (editMode) { 127 - preview.classList.add("hidden"); 128 - editor.classList.remove("hidden"); 129 - editBtn.classList.add("hidden"); 130 - saveBtn.classList.remove("hidden"); 131 - cancelBtn.classList.remove("hidden"); 132 - } else { 133 - preview.classList.remove("hidden"); 134 - editor.classList.add("hidden"); 135 - editBtn.classList.remove("hidden"); 136 - saveBtn.classList.add("hidden"); 137 - cancelBtn.classList.add("hidden"); 138 - } 139 - } 140 - 141 - document 142 - .getElementById("save-patch-btn") 143 - .addEventListener("click", function () { 144 - togglePatchEdit(false); 145 - }); 146 - </script> 147 - </details> 148 - </div> 149 58 {{ end }} 150 59 151 60 {{ define "repoAfter" }} 61 + <section id="submissions"> 62 + {{ block "submissions" . }} {{ end }} 63 + </section> 64 + 152 65 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 153 66 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 154 67 155 - <section id="comments" class="mt-8 space-y-4 relative"> 156 - {{ block "comments" . }} {{ end }} 68 + {{ if $isPullAuthor }} 69 + <section id="update-card" class="mt-8 space-y-4 relative"> 70 + {{ block "resubmitCard" . }} {{ end }} 71 + </section> 72 + {{ end }} 157 73 74 + <section id="merge-card" class="mt-8 space-y-4 relative"> 158 75 {{ if .Pull.State.IsMerged }} 159 76 {{ block "alreadyMergedCard" . }} {{ end }} 160 77 {{ else if .MergeCheck }} ··· 80 167 {{ end }} 81 168 {{ end }} 82 169 </section> 83 - 84 - {{ block "newComment" . }} {{ end }} 85 170 86 171 {{ if and (or $isPullAuthor $isPushAllowed) (not .Pull.State.IsMerged) }} 87 172 {{ $action := "close" }} ··· 103 192 <div id="pull-reopen"></div> 104 193 {{ end }} 105 194 106 - {{ define "comments" }} 107 - {{ range $index, $comment := .Comments }} 108 - <div 109 - id="comment-{{ .CommentId }}" 110 - class="rounded bg-white p-4 relative drop-shadow-sm" 111 - > 112 - {{ if eq $index 0 }} 113 - <div 114 - class="absolute left-8 -top-8 w-px h-8 bg-gray-300" 115 - ></div> 116 - {{ else }} 117 - <div 118 - class="absolute left-8 -top-4 w-px h-4 bg-gray-300" 119 - ></div> 120 - {{ end }} 121 - <div class="flex items-center gap-2 mb-2 text-gray-400"> 122 - {{ $owner := index $.DidHandleMap .OwnerDid }} 123 - <span class="text-sm"> 124 - <a 125 - href="/{{ $owner }}" 126 - class="no-underline hover:underline" 127 - >{{ $owner }}</a 128 - > 129 - </span> 130 - <span 131 - class="px-1 select-none before:content-['\00B7']" 132 - ></span> 133 - <a 134 - href="#{{ .CommentId }}" 135 - class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline" 136 - id="{{ .CommentId }}" 137 - > 138 - {{ .Created | timeFmt }} 139 - </a> 195 + {{ define "submissions" }} 196 + {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 197 + {{ range $idx, $item := .Pull.Submissions }} 198 + {{ with $item }} 199 + <details {{ if eq $idx $lastIdx }}open{{ end }}> 200 + <summary>round #{{ .RoundNumber }}, {{ .Created | timeFmt }}, received {{ len .Comments }} comments</summary> 201 + <div> 202 + <h2>patch submitted by {{index $.DidHandleMap $.Pull.OwnerDid}}</h2> 203 + <pre><code>{{- .Patch -}}</code></pre> 204 + 205 + {{ range .Comments }} 206 + <div id="comment-{{.ID}}"> 207 + {{ index $.DidHandleMap .OwnerDid }} commented {{ .Created | timeFmt }}: {{ .Body }} 140 208 </div> 141 - <div class="prose"> 142 - {{ .Body | markdown }} 143 - </div> 209 + {{ end }} 210 + {{ block "newComment" (list $ .ID) }} {{ end }} 144 211 </div> 212 + </details> 213 + {{ end }} 145 214 {{ end }} 146 215 {{ end }} 147 216 148 217 {{ define "newComment" }} 149 - {{ if .LoggedInUser }} 150 - <form 151 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment" 152 - class="mt-8" 153 - hx-swap="none"> 154 - <textarea 155 - name="body" 156 - class="w-full p-2 rounded border border-gray-200" 157 - placeholder="Add to the discussion..." 158 - ></textarea> 159 - <button type="submit" class="btn mt-2">comment</button> 160 - <div id="pull-comment"></div> 161 - </form> 162 - {{ else }} 163 - <div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8"> 164 - <a href="/login" class="underline">login</a> to join the discussion 165 - </div> 218 + {{ $rootObj := index . 0 }} 219 + {{ $submissionId := index . 1 }} 220 + 221 + {{ with $rootObj }} 222 + {{ if .LoggedInUser }} 223 + <form 224 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment" 225 + class="mt-8" 226 + hx-swap="none"> 227 + <input type="hidden" name="submissionId" value="{{ $submissionId }}"> 228 + <textarea 229 + name="body" 230 + class="w-full p-2 rounded border border-gray-200" 231 + placeholder="Add to the discussion..." 232 + ></textarea> 233 + <button type="submit" class="btn mt-2">comment</button> 234 + <div id="pull-comment"></div> 235 + </form> 236 + {{ else }} 237 + <div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8"> 238 + <a href="/login" class="underline">login</a> to join the discussion 239 + </div> 240 + {{ end }} 166 241 {{ end }} 167 242 {{ end }} 168 243 ··· 184 287 <div 185 288 id="merge-status-card" 186 289 class="rounded relative border bg-red-50 border-red-200 p-4"> 187 - {{ if gt (len .Comments) 0 }} 188 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div> 189 - {{ else }} 190 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div> 191 - {{ end }} 192 290 193 291 <div class="flex items-center gap-2 text-red-500"> 194 292 <i data-lucide="alert-triangle" class="w-4 h-4"></i> ··· 220 328 <div 221 329 id="merge-status-card" 222 330 class="rounded relative border bg-green-50 border-green-200 p-4"> 223 - {{ if gt (len .Comments) 0 }} 224 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div> 225 - {{ else }} 226 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div> 227 - {{ end }} 228 331 229 332 <div class="flex items-center gap-2 text-green-500"> 230 333 <i data-lucide="check-circle" class="w-4 h-4"></i> ··· 240 353 {{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }} 241 354 disabled 242 355 {{ end }}> 243 - <i data-lucide="git-merge" class="w-4 h-4 text-purple-500"></i> 356 + <i data-lucide="git-merge" class="w-4 h-4"></i> 244 357 <span>merge</span> 245 358 </button> 246 359 {{ end }} 247 360 248 361 <div id="pull-merge-error" class="error"></div> 249 362 <div id="pull-merge-success" class="success"></div> 363 + </div> 364 + </div> 365 + {{ end }} 366 + 367 + {{ define "resubmitCard" }} 368 + <div 369 + id="resubmit-pull-card" 370 + class="rounded relative border bg-amber-50 border-amber-200 p-4"> 371 + 372 + <div class="flex items-center gap-2 text-amber-500"> 373 + <i data-lucide="edit" class="w-4 h-4"></i> 374 + <span class="font-medium">Resubmit your patch</span> 375 + </div> 376 + 377 + <div class="mt-2 text-sm text-gray-700"> 378 + You can update this patch to address reviews if any. 379 + This begins a new round of reviews, 380 + you can still view your previous submissions and reviews. 381 + </div> 382 + 383 + <div class="mt-4 flex items-center gap-2"> 384 + <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full"> 385 + <textarea 386 + name="patch" 387 + class="w-full p-2 rounded border border-gray-200" 388 + placeholder="Enter new patch" 389 + ></textarea> 390 + <button 391 + type="submit" 392 + class="btn flex items-center gap-2" 393 + {{ if or .Pull.State.IsClosed }} 394 + disabled 395 + {{ end }}> 396 + <i data-lucide="refresh-ccw" class="w-4 h-4"></i> 397 + <span>resubmit</span> 398 + </button> 399 + </form> 400 + 401 + <div id="resubmit-error" class="error"></div> 402 + <div id="resubmit-success" class="success"></div> 250 403 </div> 251 404 </div> 252 405 {{ end }}
+1 -2
appview/state/middleware.go
··· 240 240 return 241 241 } 242 242 243 - pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt) 243 + pr, err := db.GetPull(s.db, f.RepoAt, prIdInt) 244 244 if err != nil { 245 245 log.Println("failed to get pull and comments", err) 246 246 return 247 247 } 248 248 249 249 ctx := context.WithValue(r.Context(), "pull", pr) 250 - ctx = context.WithValue(ctx, "pull_comments", comments) 251 250 252 251 next.ServeHTTP(w, r.WithContext(ctx)) 253 252 })
+609
appview/state/pull.go
··· 1 + package state 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "log" 8 + "net/http" 9 + "strconv" 10 + "time" 11 + 12 + "github.com/sotangled/tangled/api/tangled" 13 + "github.com/sotangled/tangled/appview/db" 14 + "github.com/sotangled/tangled/appview/pages" 15 + "github.com/sotangled/tangled/types" 16 + 17 + comatproto "github.com/bluesky-social/indigo/api/atproto" 18 + lexutil "github.com/bluesky-social/indigo/lex/util" 19 + ) 20 + 21 + func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 22 + user := s.auth.GetUser(r) 23 + f, err := fullyResolvedRepo(r) 24 + if err != nil { 25 + log.Println("failed to get repo and knot", err) 26 + return 27 + } 28 + 29 + pull, ok := r.Context().Value("pull").(*db.Pull) 30 + if !ok { 31 + log.Println("failed to get pull") 32 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 33 + return 34 + } 35 + 36 + totalIdents := 1 37 + for _, submission := range pull.Submissions { 38 + totalIdents += len(submission.Comments) 39 + } 40 + 41 + identsToResolve := make([]string, totalIdents) 42 + 43 + // populate idents 44 + identsToResolve[0] = pull.OwnerDid 45 + idx := 1 46 + for _, submission := range pull.Submissions { 47 + for _, comment := range submission.Comments { 48 + identsToResolve[idx] = comment.OwnerDid 49 + idx += 1 50 + } 51 + } 52 + 53 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 54 + didHandleMap := make(map[string]string) 55 + for _, identity := range resolvedIds { 56 + if !identity.Handle.IsInvalidHandle() { 57 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 58 + } else { 59 + didHandleMap[identity.DID.String()] = identity.DID.String() 60 + } 61 + } 62 + 63 + var mergeCheckResponse types.MergeCheckResponse 64 + 65 + // Only perform merge check if the pull request is not already merged 66 + if pull.State != db.PullMerged { 67 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 68 + if err != nil { 69 + log.Printf("failed to get registration key for %s", f.Knot) 70 + s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 71 + return 72 + } 73 + 74 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 75 + if err == nil { 76 + resp, err := ksClient.MergeCheck([]byte(pull.LatestPatch()), pull.OwnerDid, f.RepoName, pull.TargetBranch) 77 + if err != nil { 78 + log.Println("failed to check for mergeability:", err) 79 + } else { 80 + respBody, err := io.ReadAll(resp.Body) 81 + if err != nil { 82 + log.Println("failed to read merge check response body") 83 + } else { 84 + err = json.Unmarshal(respBody, &mergeCheckResponse) 85 + if err != nil { 86 + log.Println("failed to unmarshal merge check response", err) 87 + } 88 + } 89 + } 90 + } else { 91 + log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 92 + } 93 + } 94 + 95 + s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 96 + LoggedInUser: user, 97 + RepoInfo: f.RepoInfo(s, user), 98 + DidHandleMap: didHandleMap, 99 + Pull: *pull, 100 + MergeCheck: mergeCheckResponse, 101 + }) 102 + } 103 + 104 + func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 105 + user := s.auth.GetUser(r) 106 + params := r.URL.Query() 107 + 108 + state := db.PullOpen 109 + switch params.Get("state") { 110 + case "closed": 111 + state = db.PullClosed 112 + case "merged": 113 + state = db.PullMerged 114 + } 115 + 116 + f, err := fullyResolvedRepo(r) 117 + if err != nil { 118 + log.Println("failed to get repo and knot", err) 119 + return 120 + } 121 + 122 + pulls, err := db.GetPulls(s.db, f.RepoAt, state) 123 + if err != nil { 124 + log.Println("failed to get pulls", err) 125 + s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 126 + return 127 + } 128 + 129 + identsToResolve := make([]string, len(pulls)) 130 + for i, pull := range pulls { 131 + identsToResolve[i] = pull.OwnerDid 132 + } 133 + resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 134 + didHandleMap := make(map[string]string) 135 + for _, identity := range resolvedIds { 136 + if !identity.Handle.IsInvalidHandle() { 137 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 138 + } else { 139 + didHandleMap[identity.DID.String()] = identity.DID.String() 140 + } 141 + } 142 + 143 + s.pages.RepoPulls(w, pages.RepoPullsParams{ 144 + LoggedInUser: s.auth.GetUser(r), 145 + RepoInfo: f.RepoInfo(s, user), 146 + Pulls: pulls, 147 + DidHandleMap: didHandleMap, 148 + FilteringBy: state, 149 + }) 150 + return 151 + } 152 + 153 + func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 154 + user := s.auth.GetUser(r) 155 + f, err := fullyResolvedRepo(r) 156 + if err != nil { 157 + log.Println("failed to get repo and knot", err) 158 + return 159 + } 160 + 161 + pull, ok := r.Context().Value("pull").(*db.Pull) 162 + if !ok { 163 + log.Println("failed to get pull") 164 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 165 + return 166 + } 167 + 168 + switch r.Method { 169 + case http.MethodPost: 170 + body := r.FormValue("body") 171 + if body == "" { 172 + s.pages.Notice(w, "pull", "Comment body is required") 173 + return 174 + } 175 + 176 + submissionIdstr := r.FormValue("submissionId") 177 + submissionId, err := strconv.Atoi(submissionIdstr) 178 + if err != nil { 179 + s.pages.Notice(w, "pull", "Invalid comment submission.") 180 + return 181 + } 182 + 183 + // Start a transaction 184 + tx, err := s.db.BeginTx(r.Context(), nil) 185 + if err != nil { 186 + log.Println("failed to start transaction", err) 187 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 188 + return 189 + } 190 + defer tx.Rollback() 191 + 192 + createdAt := time.Now().Format(time.RFC3339) 193 + ownerDid := user.Did 194 + 195 + pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 196 + if err != nil { 197 + log.Println("failed to get pull at", err) 198 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 199 + return 200 + } 201 + 202 + atUri := f.RepoAt.String() 203 + client, _ := s.auth.AuthorizedClient(r) 204 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 205 + Collection: tangled.RepoPullCommentNSID, 206 + Repo: user.Did, 207 + Rkey: s.TID(), 208 + Record: &lexutil.LexiconTypeDecoder{ 209 + Val: &tangled.RepoPullComment{ 210 + Repo: &atUri, 211 + Pull: pullAt, 212 + Owner: &ownerDid, 213 + Body: &body, 214 + CreatedAt: &createdAt, 215 + }, 216 + }, 217 + }) 218 + if err != nil { 219 + log.Println("failed to create pull comment", err) 220 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 221 + return 222 + } 223 + 224 + // Create the pull comment in the database with the commentAt field 225 + commentId, err := db.NewPullComment(tx, &db.PullComment{ 226 + OwnerDid: user.Did, 227 + RepoAt: f.RepoAt.String(), 228 + PullId: pull.PullId, 229 + Body: body, 230 + CommentAt: atResp.Uri, 231 + SubmissionId: submissionId, 232 + }) 233 + if err != nil { 234 + log.Println("failed to create pull comment", err) 235 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 236 + return 237 + } 238 + 239 + // Commit the transaction 240 + if err = tx.Commit(); err != nil { 241 + log.Println("failed to commit transaction", err) 242 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 243 + return 244 + } 245 + 246 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 247 + return 248 + } 249 + } 250 + 251 + func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 252 + user := s.auth.GetUser(r) 253 + f, err := fullyResolvedRepo(r) 254 + if err != nil { 255 + log.Println("failed to get repo and knot", err) 256 + return 257 + } 258 + 259 + switch r.Method { 260 + case http.MethodGet: 261 + us, err := NewUnsignedClient(f.Knot, s.config.Dev) 262 + if err != nil { 263 + log.Printf("failed to create unsigned client for %s", f.Knot) 264 + s.pages.Error503(w) 265 + return 266 + } 267 + 268 + resp, err := us.Branches(f.OwnerDid(), f.RepoName) 269 + if err != nil { 270 + log.Println("failed to reach knotserver", err) 271 + return 272 + } 273 + 274 + body, err := io.ReadAll(resp.Body) 275 + if err != nil { 276 + log.Printf("Error reading response body: %v", err) 277 + return 278 + } 279 + 280 + var result types.RepoBranchesResponse 281 + err = json.Unmarshal(body, &result) 282 + if err != nil { 283 + log.Println("failed to parse response:", err) 284 + return 285 + } 286 + 287 + s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 288 + LoggedInUser: user, 289 + RepoInfo: f.RepoInfo(s, user), 290 + Branches: result.Branches, 291 + }) 292 + case http.MethodPost: 293 + title := r.FormValue("title") 294 + body := r.FormValue("body") 295 + targetBranch := r.FormValue("targetBranch") 296 + patch := r.FormValue("patch") 297 + 298 + if title == "" || body == "" || patch == "" || targetBranch == "" { 299 + s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 300 + return 301 + } 302 + 303 + tx, err := s.db.BeginTx(r.Context(), nil) 304 + if err != nil { 305 + log.Println("failed to start tx") 306 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 307 + return 308 + } 309 + defer tx.Rollback() 310 + 311 + rkey := s.TID() 312 + initialSubmission := db.PullSubmission{ 313 + Patch: patch, 314 + } 315 + err = db.NewPull(tx, &db.Pull{ 316 + Title: title, 317 + Body: body, 318 + TargetBranch: targetBranch, 319 + OwnerDid: user.Did, 320 + RepoAt: f.RepoAt, 321 + Rkey: rkey, 322 + Submissions: []*db.PullSubmission{ 323 + &initialSubmission, 324 + }, 325 + }) 326 + if err != nil { 327 + log.Println("failed to create pull request", err) 328 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 329 + return 330 + } 331 + client, _ := s.auth.AuthorizedClient(r) 332 + pullId, err := db.NextPullId(s.db, f.RepoAt) 333 + if err != nil { 334 + log.Println("failed to get pull id", err) 335 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 336 + return 337 + } 338 + 339 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 340 + Collection: tangled.RepoPullNSID, 341 + Repo: user.Did, 342 + Rkey: rkey, 343 + Record: &lexutil.LexiconTypeDecoder{ 344 + Val: &tangled.RepoPull{ 345 + Title: title, 346 + PullId: int64(pullId), 347 + TargetRepo: string(f.RepoAt), 348 + TargetBranch: targetBranch, 349 + Patch: patch, 350 + }, 351 + }, 352 + }) 353 + 354 + err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 355 + if err != nil { 356 + log.Println("failed to get pull id", err) 357 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 358 + return 359 + } 360 + 361 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 362 + return 363 + } 364 + } 365 + 366 + func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 367 + user := s.auth.GetUser(r) 368 + f, err := fullyResolvedRepo(r) 369 + if err != nil { 370 + log.Println("failed to get repo and knot", err) 371 + return 372 + } 373 + 374 + pull, ok := r.Context().Value("pull").(*db.Pull) 375 + if !ok { 376 + log.Println("failed to get pull") 377 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 378 + return 379 + } 380 + 381 + switch r.Method { 382 + case http.MethodPost: 383 + patch := r.FormValue("patch") 384 + 385 + if patch == "" { 386 + s.pages.Notice(w, "resubmit-error", "Patch is empty.") 387 + return 388 + } 389 + 390 + if patch == pull.LatestPatch() { 391 + s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.") 392 + return 393 + } 394 + 395 + tx, err := s.db.BeginTx(r.Context(), nil) 396 + if err != nil { 397 + log.Println("failed to start tx") 398 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 399 + return 400 + } 401 + defer tx.Rollback() 402 + 403 + err = db.ResubmitPull(tx, pull, patch) 404 + if err != nil { 405 + log.Println("failed to create pull request", err) 406 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 407 + return 408 + } 409 + client, _ := s.auth.AuthorizedClient(r) 410 + 411 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 412 + if err != nil { 413 + // failed to get record 414 + s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 415 + return 416 + } 417 + 418 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 419 + Collection: tangled.RepoPullNSID, 420 + Repo: user.Did, 421 + Rkey: pull.Rkey, 422 + SwapRecord: ex.Cid, 423 + Record: &lexutil.LexiconTypeDecoder{ 424 + Val: &tangled.RepoPull{ 425 + Title: pull.Title, 426 + PullId: int64(pull.PullId), 427 + TargetRepo: string(f.RepoAt), 428 + TargetBranch: pull.TargetBranch, 429 + Patch: patch, // new patch 430 + }, 431 + }, 432 + }) 433 + if err != nil { 434 + log.Println("failed to update record", err) 435 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 436 + return 437 + } 438 + 439 + if err = tx.Commit(); err != nil { 440 + log.Println("failed to commit transaction", err) 441 + s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.") 442 + return 443 + } 444 + 445 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 446 + return 447 + } 448 + } 449 + 450 + func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 451 + user := s.auth.GetUser(r) 452 + f, err := fullyResolvedRepo(r) 453 + if err != nil { 454 + log.Println("failed to resolve repo:", err) 455 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 456 + return 457 + } 458 + 459 + pull, ok := r.Context().Value("pull").(*db.Pull) 460 + if !ok { 461 + log.Println("failed to get pull") 462 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 463 + return 464 + } 465 + 466 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 467 + if err != nil { 468 + log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 469 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 470 + return 471 + } 472 + 473 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 474 + if err != nil { 475 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 476 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 477 + return 478 + } 479 + 480 + // Merge the pull request 481 + resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch) 482 + if err != nil { 483 + log.Printf("failed to merge pull request: %s", err) 484 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 485 + return 486 + } 487 + 488 + if resp.StatusCode == http.StatusOK { 489 + err := db.MergePull(s.db, f.RepoAt, pull.PullId) 490 + if err != nil { 491 + log.Printf("failed to update pull request status in database: %s", err) 492 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 493 + return 494 + } 495 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 496 + } else { 497 + log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 498 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 499 + } 500 + } 501 + 502 + func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 503 + user := s.auth.GetUser(r) 504 + 505 + f, err := fullyResolvedRepo(r) 506 + if err != nil { 507 + log.Println("malformed middleware") 508 + return 509 + } 510 + 511 + pull, ok := r.Context().Value("pull").(*db.Pull) 512 + if !ok { 513 + log.Println("failed to get pull") 514 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 515 + return 516 + } 517 + 518 + // auth filter: only owner or collaborators can close 519 + roles := RolesInRepo(s, user, f) 520 + isCollaborator := roles.IsCollaborator() 521 + isPullAuthor := user.Did == pull.OwnerDid 522 + isCloseAllowed := isCollaborator || isPullAuthor 523 + if !isCloseAllowed { 524 + log.Println("failed to close pull") 525 + s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 526 + return 527 + } 528 + 529 + // Start a transaction 530 + tx, err := s.db.BeginTx(r.Context(), nil) 531 + if err != nil { 532 + log.Println("failed to start transaction", err) 533 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 534 + return 535 + } 536 + 537 + // Close the pull in the database 538 + err = db.ClosePull(tx, f.RepoAt, pull.PullId) 539 + if err != nil { 540 + log.Println("failed to close pull", err) 541 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 542 + return 543 + } 544 + 545 + // Commit the transaction 546 + if err = tx.Commit(); err != nil { 547 + log.Println("failed to commit transaction", err) 548 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 549 + return 550 + } 551 + 552 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 553 + return 554 + } 555 + 556 + func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 557 + user := s.auth.GetUser(r) 558 + 559 + f, err := fullyResolvedRepo(r) 560 + if err != nil { 561 + log.Println("failed to resolve repo", err) 562 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 563 + return 564 + } 565 + 566 + pull, ok := r.Context().Value("pull").(*db.Pull) 567 + if !ok { 568 + log.Println("failed to get pull") 569 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 570 + return 571 + } 572 + 573 + // auth filter: only owner or collaborators can close 574 + roles := RolesInRepo(s, user, f) 575 + isCollaborator := roles.IsCollaborator() 576 + isPullAuthor := user.Did == pull.OwnerDid 577 + isCloseAllowed := isCollaborator || isPullAuthor 578 + if !isCloseAllowed { 579 + log.Println("failed to close pull") 580 + s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 581 + return 582 + } 583 + 584 + // Start a transaction 585 + tx, err := s.db.BeginTx(r.Context(), nil) 586 + if err != nil { 587 + log.Println("failed to start transaction", err) 588 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 589 + return 590 + } 591 + 592 + // Reopen the pull in the database 593 + err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 594 + if err != nil { 595 + log.Println("failed to reopen pull", err) 596 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 597 + return 598 + } 599 + 600 + // Commit the transaction 601 + if err = tx.Commit(); err != nil { 602 + log.Println("failed to commit transaction", err) 603 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 604 + return 605 + } 606 + 607 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 608 + return 609 + }
-628
appview/state/repo.go
··· 230 230 } 231 231 } 232 232 233 - func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) { 234 - user := s.auth.GetUser(r) 235 - 236 - patch := r.FormValue("patch") 237 - if patch == "" { 238 - s.pages.Notice(w, "pull-error", "Patch is required.") 239 - return 240 - } 241 - 242 - pull, ok := r.Context().Value("pull").(*db.Pull) 243 - if !ok { 244 - log.Println("failed to get pull") 245 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 246 - return 247 - } 248 - 249 - if pull.OwnerDid != user.Did { 250 - log.Println("failed to edit pull information") 251 - s.pages.Notice(w, "pull-error", "Unauthorized") 252 - return 253 - } 254 - 255 - f, err := fullyResolvedRepo(r) 256 - if err != nil { 257 - log.Println("failed to get repo and knot", err) 258 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 259 - return 260 - } 261 - 262 - // Start a transaction for database operations 263 - tx, err := s.db.BeginTx(r.Context(), nil) 264 - if err != nil { 265 - log.Println("failed to start transaction", err) 266 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 267 - return 268 - } 269 - 270 - // Set up deferred rollback that will be overridden by commit if successful 271 - defer tx.Rollback() 272 - 273 - // Update patch in the database within transaction 274 - err = db.EditPatch(tx, f.RepoAt, pull.PullId, patch) 275 - if err != nil { 276 - log.Println("failed to update patch", err) 277 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 278 - return 279 - } 280 - 281 - // Update the atproto record 282 - client, _ := s.auth.AuthorizedClient(r) 283 - pullAt := pull.PullAt 284 - 285 - // Get the existing record first 286 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pullAt.RecordKey().String()) 287 - if err != nil { 288 - log.Println("failed to get existing pull record", err) 289 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 290 - return 291 - } 292 - 293 - // Update the record 294 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 295 - Collection: tangled.RepoPullNSID, 296 - Repo: user.Did, 297 - Rkey: pullAt.RecordKey().String(), 298 - SwapRecord: ex.Cid, 299 - Record: &lexutil.LexiconTypeDecoder{ 300 - Val: &tangled.RepoPull{ 301 - Title: pull.Title, 302 - PullId: int64(pull.PullId), 303 - TargetRepo: string(f.RepoAt), 304 - TargetBranch: pull.TargetBranch, 305 - Patch: patch, 306 - }, 307 - }, 308 - }) 309 - 310 - if err != nil { 311 - log.Println("failed to update pull record in atproto", err) 312 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 313 - return 314 - } 315 - 316 - // Commit the transaction now that both operations have succeeded 317 - err = tx.Commit() 318 - if err != nil { 319 - log.Println("failed to commit transaction", err) 320 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 321 - return 322 - } 323 - 324 - targetBranch := pull.TargetBranch 325 - 326 - // Perform merge check 327 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 328 - if err != nil { 329 - log.Printf("no key found for domain %s: %s\n", f.Knot, err) 330 - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 331 - return 332 - } 333 - 334 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 335 - if err != nil { 336 - log.Printf("failed to create signed client for %s", f.Knot) 337 - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 338 - return 339 - } 340 - 341 - resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch) 342 - if err != nil { 343 - log.Println("failed to check mergeability", err) 344 - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 345 - return 346 - } 347 - 348 - respBody, err := io.ReadAll(resp.Body) 349 - if err != nil { 350 - log.Println("failed to read knotserver response body") 351 - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 352 - return 353 - } 354 - 355 - var mergeCheckResponse types.MergeCheckResponse 356 - err = json.Unmarshal(respBody, &mergeCheckResponse) 357 - if err != nil { 358 - log.Println("failed to unmarshal merge check response", err) 359 - s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 360 - return 361 - } 362 - 363 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 364 - return 365 - } 366 - 367 - func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 368 - user := s.auth.GetUser(r) 369 - f, err := fullyResolvedRepo(r) 370 - if err != nil { 371 - log.Println("failed to get repo and knot", err) 372 - return 373 - } 374 - 375 - switch r.Method { 376 - case http.MethodGet: 377 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 378 - if err != nil { 379 - log.Printf("failed to create unsigned client for %s", f.Knot) 380 - s.pages.Error503(w) 381 - return 382 - } 383 - 384 - resp, err := us.Branches(f.OwnerDid(), f.RepoName) 385 - if err != nil { 386 - log.Println("failed to reach knotserver", err) 387 - return 388 - } 389 - 390 - body, err := io.ReadAll(resp.Body) 391 - if err != nil { 392 - log.Printf("Error reading response body: %v", err) 393 - return 394 - } 395 - 396 - var result types.RepoBranchesResponse 397 - err = json.Unmarshal(body, &result) 398 - if err != nil { 399 - log.Println("failed to parse response:", err) 400 - return 401 - } 402 - 403 - s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 404 - LoggedInUser: user, 405 - RepoInfo: f.RepoInfo(s, user), 406 - Branches: result.Branches, 407 - }) 408 - case http.MethodPost: 409 - title := r.FormValue("title") 410 - body := r.FormValue("body") 411 - targetBranch := r.FormValue("targetBranch") 412 - patch := r.FormValue("patch") 413 - 414 - if title == "" || body == "" || patch == "" || targetBranch == "" { 415 - s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 416 - return 417 - } 418 - 419 - tx, err := s.db.BeginTx(r.Context(), nil) 420 - if err != nil { 421 - log.Println("failed to start tx") 422 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 423 - return 424 - } 425 - 426 - defer func() { 427 - tx.Rollback() 428 - err = s.enforcer.E.LoadPolicy() 429 - if err != nil { 430 - log.Println("failed to rollback policies") 431 - } 432 - }() 433 - 434 - err = db.NewPull(tx, &db.Pull{ 435 - Title: title, 436 - Body: body, 437 - TargetBranch: targetBranch, 438 - Patch: patch, 439 - OwnerDid: user.Did, 440 - RepoAt: f.RepoAt, 441 - }) 442 - if err != nil { 443 - log.Println("failed to create pull request", err) 444 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 445 - return 446 - } 447 - client, _ := s.auth.AuthorizedClient(r) 448 - pullId, err := db.NextPullId(s.db, f.RepoAt) 449 - if err != nil { 450 - log.Println("failed to get pull id", err) 451 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 452 - return 453 - } 454 - 455 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 456 - Collection: tangled.RepoPullNSID, 457 - Repo: user.Did, 458 - Rkey: s.TID(), 459 - Record: &lexutil.LexiconTypeDecoder{ 460 - Val: &tangled.RepoPull{ 461 - Title: title, 462 - PullId: int64(pullId), 463 - TargetRepo: string(f.RepoAt), 464 - TargetBranch: targetBranch, 465 - Patch: patch, 466 - }, 467 - }, 468 - }) 469 - 470 - err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 471 - if err != nil { 472 - log.Println("failed to get pull id", err) 473 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 474 - return 475 - } 476 - 477 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 478 - return 479 - } 480 - } 481 - 482 - func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 483 - user := s.auth.GetUser(r) 484 - f, err := fullyResolvedRepo(r) 485 - if err != nil { 486 - log.Println("failed to get repo and knot", err) 487 - return 488 - } 489 - 490 - pull, ok1 := r.Context().Value("pull").(*db.Pull) 491 - comments, ok2 := r.Context().Value("pull_comments").([]db.PullComment) 492 - if !ok1 || !ok2 { 493 - log.Println("failed to get pull") 494 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 495 - return 496 - } 497 - 498 - identsToResolve := make([]string, len(comments)) 499 - for i, comment := range comments { 500 - identsToResolve[i] = comment.OwnerDid 501 - } 502 - identsToResolve = append(identsToResolve, pull.OwnerDid) 503 - 504 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 505 - didHandleMap := make(map[string]string) 506 - for _, identity := range resolvedIds { 507 - if !identity.Handle.IsInvalidHandle() { 508 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 509 - } else { 510 - didHandleMap[identity.DID.String()] = identity.DID.String() 511 - } 512 - } 513 - 514 - var mergeCheckResponse types.MergeCheckResponse 515 - 516 - // Only perform merge check if the pull request is not already merged 517 - if pull.State != db.PullMerged { 518 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 519 - if err != nil { 520 - log.Printf("failed to get registration key for %s", f.Knot) 521 - s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 522 - return 523 - } 524 - 525 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 526 - if err == nil { 527 - resp, err := ksClient.MergeCheck([]byte(pull.Patch), pull.OwnerDid, f.RepoName, pull.TargetBranch) 528 - if err != nil { 529 - log.Println("failed to check for mergeability:", err) 530 - } else { 531 - respBody, err := io.ReadAll(resp.Body) 532 - if err != nil { 533 - log.Println("failed to read merge check response body") 534 - } else { 535 - err = json.Unmarshal(respBody, &mergeCheckResponse) 536 - if err != nil { 537 - log.Println("failed to unmarshal merge check response", err) 538 - } 539 - } 540 - } 541 - } else { 542 - log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 543 - } 544 - } 545 - 546 - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 547 - LoggedInUser: user, 548 - RepoInfo: f.RepoInfo(s, user), 549 - Pull: *pull, 550 - Comments: comments, 551 - DidHandleMap: didHandleMap, 552 - MergeCheck: mergeCheckResponse, 553 - }) 554 - } 555 - 556 233 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 557 234 f, err := fullyResolvedRepo(r) 558 235 if err != nil { ··· 1068 1391 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1069 1392 return 1070 1393 } 1071 - } 1072 - 1073 - func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1074 - user := s.auth.GetUser(r) 1075 - params := r.URL.Query() 1076 - 1077 - state := db.PullOpen 1078 - switch params.Get("state") { 1079 - case "closed": 1080 - state = db.PullClosed 1081 - case "merged": 1082 - state = db.PullMerged 1083 - } 1084 - 1085 - f, err := fullyResolvedRepo(r) 1086 - if err != nil { 1087 - log.Println("failed to get repo and knot", err) 1088 - return 1089 - } 1090 - 1091 - pulls, err := db.GetPulls(s.db, f.RepoAt, state) 1092 - if err != nil { 1093 - log.Println("failed to get pulls", err) 1094 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1095 - return 1096 - } 1097 - 1098 - identsToResolve := make([]string, len(pulls)) 1099 - for i, pull := range pulls { 1100 - identsToResolve[i] = pull.OwnerDid 1101 - } 1102 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1103 - didHandleMap := make(map[string]string) 1104 - for _, identity := range resolvedIds { 1105 - if !identity.Handle.IsInvalidHandle() { 1106 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1107 - } else { 1108 - didHandleMap[identity.DID.String()] = identity.DID.String() 1109 - } 1110 - } 1111 - 1112 - s.pages.RepoPulls(w, pages.RepoPullsParams{ 1113 - LoggedInUser: s.auth.GetUser(r), 1114 - RepoInfo: f.RepoInfo(s, user), 1115 - Pulls: pulls, 1116 - DidHandleMap: didHandleMap, 1117 - FilteringBy: state, 1118 - }) 1119 - return 1120 - } 1121 - 1122 - func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1123 - user := s.auth.GetUser(r) 1124 - f, err := fullyResolvedRepo(r) 1125 - if err != nil { 1126 - log.Println("failed to resolve repo:", err) 1127 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1128 - return 1129 - } 1130 - 1131 - pull, ok := r.Context().Value("pull").(*db.Pull) 1132 - if !ok { 1133 - log.Println("failed to get pull") 1134 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1135 - return 1136 - } 1137 - 1138 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 1139 - if err != nil { 1140 - log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1141 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1142 - return 1143 - } 1144 - 1145 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1146 - if err != nil { 1147 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1148 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1149 - return 1150 - } 1151 - 1152 - // Merge the pull request 1153 - resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch) 1154 - if err != nil { 1155 - log.Printf("failed to merge pull request: %s", err) 1156 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1157 - return 1158 - } 1159 - 1160 - if resp.StatusCode == http.StatusOK { 1161 - err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1162 - if err != nil { 1163 - log.Printf("failed to update pull request status in database: %s", err) 1164 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1165 - return 1166 - } 1167 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1168 - } else { 1169 - log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1170 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1171 - } 1172 - } 1173 - 1174 - func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 1175 - user := s.auth.GetUser(r) 1176 - f, err := fullyResolvedRepo(r) 1177 - if err != nil { 1178 - log.Println("failed to get repo and knot", err) 1179 - return 1180 - } 1181 - 1182 - pullId := chi.URLParam(r, "pull") 1183 - pullIdInt, err := strconv.Atoi(pullId) 1184 - if err != nil { 1185 - http.Error(w, "bad pull id", http.StatusBadRequest) 1186 - log.Println("failed to parse pull id", err) 1187 - return 1188 - } 1189 - 1190 - switch r.Method { 1191 - case http.MethodPost: 1192 - body := r.FormValue("body") 1193 - if body == "" { 1194 - s.pages.Notice(w, "pull", "Comment body is required") 1195 - return 1196 - } 1197 - 1198 - // Start a transaction 1199 - tx, err := s.db.BeginTx(r.Context(), nil) 1200 - if err != nil { 1201 - log.Println("failed to start transaction", err) 1202 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1203 - return 1204 - } 1205 - defer tx.Rollback() // Will be ignored if we commit 1206 - 1207 - commentId := rand.IntN(1000000) 1208 - createdAt := time.Now().Format(time.RFC3339) 1209 - commentIdInt64 := int64(commentId) 1210 - ownerDid := user.Did 1211 - 1212 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pullIdInt) 1213 - if err != nil { 1214 - log.Println("failed to get pull at", err) 1215 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1216 - return 1217 - } 1218 - 1219 - atUri := f.RepoAt.String() 1220 - client, _ := s.auth.AuthorizedClient(r) 1221 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1222 - Collection: tangled.RepoPullCommentNSID, 1223 - Repo: user.Did, 1224 - Rkey: s.TID(), 1225 - Record: &lexutil.LexiconTypeDecoder{ 1226 - Val: &tangled.RepoPullComment{ 1227 - Repo: &atUri, 1228 - Pull: pullAt, 1229 - CommentId: &commentIdInt64, 1230 - Owner: &ownerDid, 1231 - Body: &body, 1232 - CreatedAt: &createdAt, 1233 - }, 1234 - }, 1235 - }) 1236 - if err != nil { 1237 - log.Println("failed to create pull comment", err) 1238 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1239 - return 1240 - } 1241 - 1242 - // Create the pull comment in the database with the commentAt field 1243 - err = db.NewPullComment(tx, &db.PullComment{ 1244 - OwnerDid: user.Did, 1245 - RepoAt: f.RepoAt.String(), 1246 - CommentId: commentId, 1247 - PullId: pullIdInt, 1248 - Body: body, 1249 - CommentAt: atResp.Uri, 1250 - }) 1251 - if err != nil { 1252 - log.Println("failed to create pull comment", err) 1253 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1254 - return 1255 - } 1256 - 1257 - // Commit the transaction 1258 - if err = tx.Commit(); err != nil { 1259 - log.Println("failed to commit transaction", err) 1260 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1261 - return 1262 - } 1263 - 1264 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pullIdInt, commentId)) 1265 - return 1266 - } 1267 - } 1268 - 1269 - func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1270 - user := s.auth.GetUser(r) 1271 - 1272 - f, err := fullyResolvedRepo(r) 1273 - if err != nil { 1274 - log.Println("malformed middleware") 1275 - return 1276 - } 1277 - 1278 - pull, ok := r.Context().Value("pull").(*db.Pull) 1279 - if !ok { 1280 - log.Println("failed to get pull") 1281 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1282 - return 1283 - } 1284 - 1285 - // auth filter: only owner or collaborators can close 1286 - roles := RolesInRepo(s, user, f) 1287 - isCollaborator := roles.IsCollaborator() 1288 - isPullAuthor := user.Did == pull.OwnerDid 1289 - isCloseAllowed := isCollaborator || isPullAuthor 1290 - if !isCloseAllowed { 1291 - log.Println("failed to close pull") 1292 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1293 - return 1294 - } 1295 - 1296 - // Start a transaction 1297 - tx, err := s.db.BeginTx(r.Context(), nil) 1298 - if err != nil { 1299 - log.Println("failed to start transaction", err) 1300 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1301 - return 1302 - } 1303 - 1304 - // Close the pull in the database 1305 - err = db.ClosePull(tx, f.RepoAt, pull.PullId) 1306 - if err != nil { 1307 - log.Println("failed to close pull", err) 1308 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1309 - return 1310 - } 1311 - 1312 - // Commit the transaction 1313 - if err = tx.Commit(); err != nil { 1314 - log.Println("failed to commit transaction", err) 1315 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1316 - return 1317 - } 1318 - 1319 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1320 - return 1321 - } 1322 - 1323 - func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1324 - user := s.auth.GetUser(r) 1325 - 1326 - f, err := fullyResolvedRepo(r) 1327 - if err != nil { 1328 - log.Println("failed to resolve repo", err) 1329 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1330 - return 1331 - } 1332 - 1333 - pull, ok := r.Context().Value("pull").(*db.Pull) 1334 - if !ok { 1335 - log.Println("failed to get pull") 1336 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1337 - return 1338 - } 1339 - 1340 - // auth filter: only owner or collaborators can close 1341 - roles := RolesInRepo(s, user, f) 1342 - isCollaborator := roles.IsCollaborator() 1343 - isPullAuthor := user.Did == pull.OwnerDid 1344 - isCloseAllowed := isCollaborator || isPullAuthor 1345 - if !isCloseAllowed { 1346 - log.Println("failed to close pull") 1347 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1348 - return 1349 - } 1350 - 1351 - // Start a transaction 1352 - tx, err := s.db.BeginTx(r.Context(), nil) 1353 - if err != nil { 1354 - log.Println("failed to start transaction", err) 1355 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1356 - return 1357 - } 1358 - 1359 - // Reopen the pull in the database 1360 - err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 1361 - if err != nil { 1362 - log.Println("failed to reopen pull", err) 1363 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1364 - return 1365 - } 1366 - 1367 - // Commit the transaction 1368 - if err = tx.Commit(); err != nil { 1369 - log.Println("failed to commit transaction", err) 1370 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1371 - return 1372 - } 1373 - 1374 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1375 - return 1376 1394 } 1377 1395 1378 1396 func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
+1 -1
appview/state/router.go
··· 70 70 // authorized requests below this point 71 71 r.Group(func(r chi.Router) { 72 72 r.Use(AuthMiddleware(s)) 73 - r.Patch("/patch", s.EditPatch) 73 + r.Post("/resubmit", s.ResubmitPull) 74 74 r.Post("/comment", s.PullComment) 75 75 r.Post("/close", s.ClosePull) 76 76 r.Post("/reopen", s.ReopenPull)