Monorepo for Tangled tangled.org

begin work on round-based review

needs frontend bits

Changed files
+988 -892
appview
+49 -9
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, 109 + pull_id integer not null, 110 + 111 + -- at identifiers 112 + repo_at text not null, 108 113 owner_did text not null, 109 - repo_at text not null, 110 - pull_id integer 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 ); 130 + 131 + -- every pull must have atleast 1 submission: the initial submission 132 + create table if not exists pull_submissions ( 133 + -- identifiers 134 + id integer primary key autoincrement, 135 + pull_id integer not null, 136 + 137 + -- at identifiers 138 + repo_at 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 145 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 146 + 147 + -- constraints 148 + unique(repo_at, pull_id, round_number), 149 + foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade 150 + ); 151 + 122 152 create table if not exists pull_comments ( 153 + -- identifiers 123 154 id integer primary key autoincrement, 124 - owner_did text not null, 125 155 pull_id integer not null, 156 + submission_id integer not null, 157 + 158 + -- at identifiers 126 159 repo_at text not null, 127 - comment_id integer not null, 160 + owner_did text not null, 128 161 comment_at text not null, 162 + 163 + -- content 129 164 body text not null, 165 + 166 + -- meta 130 167 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 131 - unique(pull_id, comment_id), 132 - foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade 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 133 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
+225 -78
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" ··· 39 41 } 40 42 41 43 type Pull struct { 42 - ID int 43 - OwnerDid string 44 - RepoAt syntax.ATURI 45 - PullAt syntax.ATURI 46 - TargetBranch string 47 - Patch string 48 - PullId int 44 + // ids 45 + ID int 46 + PullId int 47 + 48 + // at ids 49 + RepoAt syntax.ATURI 50 + OwnerDid string 51 + Rkey string 52 + PullAt syntax.ATURI 53 + 54 + // content 49 55 Title string 50 56 Body string 57 + TargetBranch string 51 58 State PullState 52 - Created time.Time 53 - Rkey string 59 + Submissions []*PullSubmission 60 + 61 + // meta 62 + Created time.Time 63 + } 64 + 65 + type PullSubmission struct { 66 + // ids 67 + ID int 68 + PullId int 69 + 70 + // at ids 71 + RepoAt syntax.ATURI 72 + 73 + // content 74 + RoundNumber int 75 + Patch string 76 + Comments []PullComment 77 + 78 + // meta 79 + Created time.Time 54 80 } 55 81 56 82 type PullComment struct { 57 - ID int 83 + // ids 84 + ID int 85 + PullId int 86 + SubmissionId int 87 + 88 + // at ids 89 + RepoAt string 58 90 OwnerDid string 59 - PullId int 60 - RepoAt string 61 - CommentId int 62 91 CommentAt string 63 - Body string 64 - Created time.Time 92 + 93 + // content 94 + Body string 95 + 96 + // meta 97 + Created time.Time 98 + } 99 + 100 + func (p *Pull) LatestPatch() string { 101 + latestSubmission := p.Submissions[len(p.Submissions)-1] 102 + return latestSubmission.Patch 65 103 } 66 104 67 105 func NewPull(tx *sql.Tx, pull *Pull) error { ··· 90 128 pull.State = PullOpen 91 129 92 130 _, err = tx.Exec(` 93 - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, patch, rkey, state) 94 - values (?, ?, ?, ?, ?, ?, ?, ?, ?) 95 - `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Patch, pull.Rkey, pull.State) 131 + insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state) 132 + values (?, ?, ?, ?, ?, ?, ?, ?) 133 + `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State) 134 + if err != nil { 135 + return err 136 + } 137 + 138 + _, err = tx.Exec(` 139 + insert into pull_submissions (pull_id, repo_at, round_number, patch) 140 + values (?, ?, ?, ?) 141 + `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch) 96 142 if err != nil { 97 143 return err 98 144 } ··· 134 180 target_branch, 135 181 pull_at, 136 182 body, 137 - patch, 138 183 rkey 139 184 from 140 185 pulls ··· 150 195 for rows.Next() { 151 196 var pull Pull 152 197 var createdAt string 153 - err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 198 + err := rows.Scan( 199 + &pull.OwnerDid, 200 + &pull.PullId, 201 + &createdAt, 202 + &pull.Title, 203 + &pull.State, 204 + &pull.TargetBranch, 205 + &pull.PullAt, 206 + &pull.Body, 207 + &pull.Rkey, 208 + ) 154 209 if err != nil { 155 210 return nil, err 156 211 } ··· 172 227 } 173 228 174 229 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { 175 - query := `select owner_did, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` 230 + query := ` 231 + select 232 + owner_did, 233 + pull_id, 234 + created, 235 + title, 236 + state, 237 + target_branch, 238 + pull_at, 239 + repo_at, 240 + body, 241 + rkey 242 + from 243 + pulls 244 + where 245 + repo_at = ? and pull_id = ? 246 + ` 176 247 row := e.QueryRow(query, repoAt, pullId) 177 248 178 249 var pull Pull 179 250 var createdAt string 180 - err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 251 + err := row.Scan( 252 + &pull.OwnerDid, 253 + &pull.PullId, 254 + &createdAt, 255 + &pull.Title, 256 + &pull.State, 257 + &pull.TargetBranch, 258 + &pull.PullAt, 259 + &pull.RepoAt, 260 + &pull.Body, 261 + &pull.Rkey, 262 + ) 181 263 if err != nil { 182 264 return nil, err 183 265 } ··· 188 270 } 189 271 pull.Created = createdTime 190 272 191 - return &pull, nil 192 - } 193 - 194 - func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, []PullComment, error) { 195 - query := `select owner_did, pull_id, created, title, state, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?` 196 - row := e.QueryRow(query, repoAt, pullId) 197 - 198 - var pull Pull 199 - var createdAt string 200 - err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.State, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey) 273 + submissionsQuery := ` 274 + select 275 + id, pull_id, repo_at, round_number, patch, created 276 + from 277 + pull_submissions 278 + where 279 + repo_at = ? and pull_id = ? 280 + ` 281 + submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) 201 282 if err != nil { 202 - return nil, nil, err 283 + return nil, err 203 284 } 285 + defer submissionsRows.Close() 204 286 205 - createdTime, err := time.Parse(time.RFC3339, createdAt) 206 - if err != nil { 207 - return nil, nil, err 208 - } 209 - pull.Created = createdTime 287 + submissionsMap := make(map[int]*PullSubmission) 210 288 211 - comments, err := GetPullComments(e, repoAt, pullId) 212 - if err != nil { 213 - return nil, nil, err 214 - } 215 - 216 - return &pull, comments, nil 217 - } 289 + for submissionsRows.Next() { 290 + var submission PullSubmission 291 + var submissionCreatedStr string 292 + err := submissionsRows.Scan( 293 + &submission.ID, 294 + &submission.PullId, 295 + &submission.RepoAt, 296 + &submission.RoundNumber, 297 + &submission.Patch, 298 + &submissionCreatedStr, 299 + ) 300 + if err != nil { 301 + return nil, err 302 + } 218 303 219 - func NewPullComment(e Execer, comment *PullComment) error { 220 - query := `insert into pull_comments (owner_did, repo_at, comment_at, pull_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` 221 - _, err := e.Exec( 222 - query, 223 - comment.OwnerDid, 224 - comment.RepoAt, 225 - comment.CommentAt, 226 - comment.PullId, 227 - comment.CommentId, 228 - comment.Body, 229 - ) 230 - return err 231 - } 304 + submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 305 + if err != nil { 306 + return nil, err 307 + } 308 + submission.Created = submissionCreatedTime 232 309 233 - func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComment, error) { 234 - var comments []PullComment 310 + submissionsMap[submission.ID] = &submission 311 + } 312 + if err = submissionsRows.Close(); err != nil { 313 + return nil, err 314 + } 315 + if len(submissionsMap) == 0 { 316 + return &pull, nil 317 + } 235 318 236 - 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) 237 - if err == sql.ErrNoRows { 238 - return []PullComment{}, nil 319 + var args []any 320 + for k := range submissionsMap { 321 + args = append(args, k) 239 322 } 323 + inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 324 + commentsQuery := fmt.Sprintf(` 325 + select 326 + id, 327 + pull_id, 328 + submission_id, 329 + repo_at, 330 + owner_did, 331 + comment_at, 332 + body, 333 + created 334 + from 335 + pull_comments 336 + where 337 + submission_id IN (%s) 338 + order by 339 + created asc 340 + `, inClause) 341 + commentsRows, err := e.Query(commentsQuery, args...) 240 342 if err != nil { 241 343 return nil, err 242 344 } 243 - defer rows.Close() 345 + defer commentsRows.Close() 244 346 245 - for rows.Next() { 347 + for commentsRows.Next() { 246 348 var comment PullComment 247 - var createdAt string 248 - err := rows.Scan(&comment.OwnerDid, &comment.PullId, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt) 349 + var commentCreatedStr string 350 + err := commentsRows.Scan( 351 + &comment.ID, 352 + &comment.PullId, 353 + &comment.SubmissionId, 354 + &comment.RepoAt, 355 + &comment.OwnerDid, 356 + &comment.CommentAt, 357 + &comment.Body, 358 + &commentCreatedStr, 359 + ) 249 360 if err != nil { 250 361 return nil, err 251 362 } 252 363 253 - createdAtTime, err := time.Parse(time.RFC3339, createdAt) 364 + commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 254 365 if err != nil { 255 366 return nil, err 256 367 } 257 - comment.Created = createdAtTime 368 + comment.Created = commentCreatedTime 369 + 370 + // Add the comment to its submission 371 + if submission, ok := submissionsMap[comment.SubmissionId]; ok { 372 + submission.Comments = append(submission.Comments, comment) 373 + } 374 + 375 + } 376 + if err = commentsRows.Err(); err != nil { 377 + return nil, err 378 + } 379 + 380 + pull.Submissions = make([]*PullSubmission, len(submissionsMap)) 381 + for _, submission := range submissionsMap { 382 + pull.Submissions[submission.RoundNumber] = submission 383 + } 384 + 385 + return &pull, nil 386 + } 258 387 259 - comments = append(comments, comment) 388 + func NewPullComment(e Execer, comment *PullComment) (int64, error) { 389 + query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 390 + res, err := e.Exec( 391 + query, 392 + comment.OwnerDid, 393 + comment.RepoAt, 394 + comment.SubmissionId, 395 + comment.CommentAt, 396 + comment.PullId, 397 + comment.Body, 398 + ) 399 + if err != nil { 400 + return 0, err 260 401 } 261 402 262 - if err := rows.Err(); err != nil { 263 - return nil, err 403 + i, err := res.LastInsertId() 404 + if err != nil { 405 + return 0, err 264 406 } 265 407 266 - return comments, nil 408 + return i, nil 267 409 } 268 410 269 411 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error { ··· 286 428 return err 287 429 } 288 430 431 + func ResubmitPull(e Execer, pull *Pull, newPatch string) error { 432 + newRoundNumber := len(pull.Submissions) 433 + _, err := e.Exec(` 434 + insert into pull_submissions (pull_id, repo_at, round_number, patch) 435 + values (?, ?, ?, ?) 436 + `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch) 437 + 438 + return err 439 + } 440 + 289 441 type PullCount struct { 290 442 Open int 291 443 Merged int ··· 313 465 314 466 return count, nil 315 467 } 316 - 317 - func EditPatch(e Execer, repoAt syntax.ATURI, pullId int, patch string) error { 318 - _, err := e.Exec(`update pulls set patch = ? where repo_at = ? and pull_id = ?`, patch, repoAt, pullId) 319 - return err 320 - }
+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" }} ··· 21 20 {{ $bgColor = "bg-purple-600" }} 22 21 {{ $icon = "git-merge" }} 23 22 {{ end }} 24 - 25 23 26 24 <section> 27 25 <div class="flex items-center gap-2"> ··· 55 53 {{ end }} 56 54 </section> 57 55 58 - <div class="flex flex-col justify-end mt-4"> 59 - <details> 60 - <summary 61 - 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" 62 - > 63 - <i data-lucide="code" class="w-4 h-4 mr-2"></i> 64 - <span>patch</span> 65 - </summary> 66 - <div class="relative"> 67 - <pre 68 - id="patch-preview" 69 - class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm" 70 - > 71 - {{- .Pull.Patch -}} 72 - </pre 73 - > 74 - <form 75 - id="patch-form" 76 - hx-patch="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/patch" 77 - hx-swap="none" 78 - > 79 - <textarea 80 - id="patch" 81 - name="patch" 82 - class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden" 83 - >{{- .Pull.Patch -}}</textarea> 84 - 85 - <div class="flex gap-2 justify-end mt-2"> 86 - <button 87 - id="edit-patch-btn" 88 - type="button" 89 - class="btn btn-sm" 90 - onclick="togglePatchEdit(true)" 91 - {{ if or .Pull.State.IsMerged .Pull.State.IsClosed }} 92 - disabled title="Cannot edit closed or merged 93 - pull requests" 94 - {{ end }} 95 - > 96 - <i data-lucide="edit" class="w-4 h-4 mr-1"></i>Edit 97 - </button> 98 - <button 99 - id="save-patch-btn" 100 - type="submit" 101 - class="btn btn-sm bg-green-500 hidden" 102 - > 103 - <i data-lucide="save" class="w-4 h-4 mr-1"></i>Save 104 - </button> 105 - <button 106 - id="cancel-patch-btn" 107 - type="button" 108 - class="btn btn-sm bg-gray-300 hidden" 109 - onclick="togglePatchEdit(false)" 110 - > 111 - Cancel 112 - </button> 113 - </div> 114 - </form> 115 - 116 - <div id="pull-error" class="error"></div> 117 - <div id="pull-success" class="success"></div> 118 - </div> 119 - <script> 120 - function togglePatchEdit(editMode) { 121 - const preview = document.getElementById("patch-preview"); 122 - const editor = document.getElementById("patch"); 123 - const editBtn = document.getElementById("edit-patch-btn"); 124 - const saveBtn = document.getElementById("save-patch-btn"); 125 - const cancelBtn = 126 - document.getElementById("cancel-patch-btn"); 127 - 128 - if (editMode) { 129 - preview.classList.add("hidden"); 130 - editor.classList.remove("hidden"); 131 - editBtn.classList.add("hidden"); 132 - saveBtn.classList.remove("hidden"); 133 - cancelBtn.classList.remove("hidden"); 134 - } else { 135 - preview.classList.remove("hidden"); 136 - editor.classList.add("hidden"); 137 - editBtn.classList.remove("hidden"); 138 - saveBtn.classList.add("hidden"); 139 - cancelBtn.classList.add("hidden"); 140 - } 141 - } 142 - 143 - document 144 - .getElementById("save-patch-btn") 145 - .addEventListener("click", function () { 146 - togglePatchEdit(false); 147 - }); 148 - </script> 149 - </details> 150 - </div> 151 56 {{ end }} 152 57 153 58 {{ define "repoAfter" }} 59 + <section id="submissions"> 60 + {{ block "submissions" . }} {{ end }} 61 + </section> 62 + 154 63 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 155 64 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 156 65 157 - <section id="comments" class="mt-8 space-y-4 relative"> 158 - {{ block "comments" . }} {{ end }} 66 + {{ if $isPullAuthor }} 67 + <section id="update-card" class="mt-8 space-y-4 relative"> 68 + {{ block "resubmitCard" . }} {{ end }} 69 + </section> 70 + {{ end }} 159 71 72 + <section id="merge-card" class="mt-8 space-y-4 relative"> 160 73 {{ if .Pull.State.IsMerged }} 161 74 {{ block "alreadyMergedCard" . }} {{ end }} 162 75 {{ else if .MergeCheck }} ··· 167 80 {{ end }} 168 81 {{ end }} 169 82 </section> 170 - 171 - {{ block "newComment" . }} {{ end }} 172 83 173 84 {{ if and (or $isPullAuthor $isPushAllowed) (not .Pull.State.IsMerged) }} 174 85 {{ $action := "close" }} ··· 192 103 <div id="pull-reopen"></div> 193 104 {{ end }} 194 105 195 - {{ define "comments" }} 196 - {{ range $index, $comment := .Comments }} 197 - <div 198 - id="comment-{{ .CommentId }}" 199 - class="rounded bg-white p-4 relative drop-shadow-sm" 200 - > 201 - {{ if eq $index 0 }} 202 - <div 203 - class="absolute left-8 -top-8 w-px h-8 bg-gray-300" 204 - ></div> 205 - {{ else }} 206 - <div 207 - class="absolute left-8 -top-4 w-px h-4 bg-gray-300" 208 - ></div> 209 - {{ end }} 210 - <div class="flex items-center gap-2 mb-2 text-gray-400"> 211 - {{ $owner := index $.DidHandleMap .OwnerDid }} 212 - <span class="text-sm"> 213 - <a 214 - href="/{{ $owner }}" 215 - class="no-underline hover:underline" 216 - >{{ $owner }}</a 217 - > 218 - </span> 219 - <span 220 - class="px-1 select-none before:content-['\00B7']" 221 - ></span> 222 - <a 223 - href="#{{ .CommentId }}" 224 - class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline" 225 - id="{{ .CommentId }}" 226 - > 227 - {{ .Created | timeFmt }} 228 - </a> 229 - </div> 230 - <div class="prose"> 231 - {{ .Body | markdown }} 106 + {{ define "submissions" }} 107 + {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 108 + {{ range $idx, $item := .Pull.Submissions }} 109 + {{ with $item }} 110 + <details {{ if eq $idx $lastIdx }}open{{ end }}> 111 + <summary>round #{{ .RoundNumber }}, {{ .Created | timeFmt }}, received {{ len .Comments }} comments</summary> 112 + <div> 113 + <h2>patch submitted by {{index $.DidHandleMap $.Pull.OwnerDid}}</h2> 114 + <pre><code>{{- .Patch -}}</code></pre> 115 + 116 + {{ range .Comments }} 117 + <div id="comment-{{.ID}}"> 118 + {{ index $.DidHandleMap .OwnerDid }} commented {{ .Created | timeFmt }}: {{ .Body }} 232 119 </div> 120 + {{ end }} 121 + {{ block "newComment" (list $ .ID) }} {{ end }} 233 122 </div> 123 + </details> 124 + {{ end }} 234 125 {{ end }} 235 126 {{ end }} 236 127 237 128 {{ define "newComment" }} 238 - {{ if .LoggedInUser }} 239 - <form 240 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment" 241 - class="mt-8" 242 - hx-swap="none"> 243 - <textarea 244 - name="body" 245 - class="w-full p-2 rounded border border-gray-200" 246 - placeholder="Add to the discussion..." 247 - ></textarea> 248 - <button type="submit" class="btn mt-2">comment</button> 249 - <div id="pull-comment"></div> 250 - </form> 251 - {{ else }} 252 - <div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8"> 253 - <a href="/login" class="underline">login</a> to join the discussion 254 - </div> 129 + {{ $rootObj := index . 0 }} 130 + {{ $submissionId := index . 1 }} 131 + 132 + {{ with $rootObj }} 133 + {{ if .LoggedInUser }} 134 + <form 135 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment" 136 + class="mt-8" 137 + hx-swap="none"> 138 + <input type="hidden" name="submissionId" value="{{ $submissionId }}"> 139 + <textarea 140 + name="body" 141 + class="w-full p-2 rounded border border-gray-200" 142 + placeholder="Add to the discussion..." 143 + ></textarea> 144 + <button type="submit" class="btn mt-2">comment</button> 145 + <div id="pull-comment"></div> 146 + </form> 147 + {{ else }} 148 + <div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8"> 149 + <a href="/login" class="underline">login</a> to join the discussion 150 + </div> 151 + {{ end }} 255 152 {{ end }} 256 153 {{ end }} 257 154 ··· 287 184 <div 288 185 id="merge-status-card" 289 186 class="rounded relative border bg-red-50 border-red-200 p-4"> 290 - {{ if gt (len .Comments) 0 }} 291 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div> 292 - {{ else }} 293 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div> 294 - {{ end }} 295 187 296 188 <div class="flex items-center gap-2 text-red-500"> 297 189 <i data-lucide="alert-triangle" class="w-4 h-4"></i> ··· 328 220 <div 329 221 id="merge-status-card" 330 222 class="rounded relative border bg-green-50 border-green-200 p-4"> 331 - {{ if gt (len .Comments) 0 }} 332 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div> 333 - {{ else }} 334 - <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div> 335 - {{ end }} 336 223 337 224 <div class="flex items-center gap-2 text-green-500"> 338 225 <i data-lucide="check-circle" class="w-4 h-4"></i> ··· 353 240 {{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }} 354 241 disabled 355 242 {{ end }}> 356 - <i data-lucide="git-merge" class="w-4 h-4 text-purple-500"></i> 243 + <i data-lucide="git-merge" class="w-4 h-4"></i> 357 244 <span>merge</span> 358 245 </button> 359 246 {{ end }} ··· 363 250 </div> 364 251 </div> 365 252 {{ end }} 253 + 254 + {{ define "resubmitCard" }} 255 + <div 256 + id="resubmit-pull-card" 257 + class="rounded relative border bg-amber-50 border-amber-200 p-4"> 258 + 259 + <div class="flex items-center gap-2 text-amber-500"> 260 + <i data-lucide="edit" class="w-4 h-4"></i> 261 + <span class="font-medium">Resubmit your patch</span> 262 + </div> 263 + 264 + <div class="mt-2 text-sm text-gray-700"> 265 + You can update this patch to address reviews if any. 266 + This begins a new round of reviews, 267 + you can still view your previous submissions and reviews. 268 + </div> 269 + 270 + <div class="mt-4 flex items-center gap-2"> 271 + <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full"> 272 + <textarea 273 + name="patch" 274 + class="w-full p-2 rounded border border-gray-200" 275 + placeholder="Enter new patch" 276 + ></textarea> 277 + <button 278 + type="submit" 279 + class="btn flex items-center gap-2" 280 + {{ if or .Pull.State.IsClosed }} 281 + disabled 282 + {{ end }}> 283 + <i data-lucide="refresh-ccw" class="w-4 h-4"></i> 284 + <span>resubmit</span> 285 + </button> 286 + </form> 287 + 288 + <div id="resubmit-error" class="error"></div> 289 + <div id="resubmit-success" class="success"></div> 290 + </div> 291 + </div> 292 + {{ 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 { ··· 1391 1068 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1392 1069 return 1393 1070 } 1394 - } 1395 - 1396 - func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1397 - user := s.auth.GetUser(r) 1398 - params := r.URL.Query() 1399 - 1400 - state := db.PullOpen 1401 - switch params.Get("state") { 1402 - case "closed": 1403 - state = db.PullClosed 1404 - case "merged": 1405 - state = db.PullMerged 1406 - } 1407 - 1408 - f, err := fullyResolvedRepo(r) 1409 - if err != nil { 1410 - log.Println("failed to get repo and knot", err) 1411 - return 1412 - } 1413 - 1414 - pulls, err := db.GetPulls(s.db, f.RepoAt, state) 1415 - if err != nil { 1416 - log.Println("failed to get pulls", err) 1417 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1418 - return 1419 - } 1420 - 1421 - identsToResolve := make([]string, len(pulls)) 1422 - for i, pull := range pulls { 1423 - identsToResolve[i] = pull.OwnerDid 1424 - } 1425 - resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1426 - didHandleMap := make(map[string]string) 1427 - for _, identity := range resolvedIds { 1428 - if !identity.Handle.IsInvalidHandle() { 1429 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1430 - } else { 1431 - didHandleMap[identity.DID.String()] = identity.DID.String() 1432 - } 1433 - } 1434 - 1435 - s.pages.RepoPulls(w, pages.RepoPullsParams{ 1436 - LoggedInUser: s.auth.GetUser(r), 1437 - RepoInfo: f.RepoInfo(s, user), 1438 - Pulls: pulls, 1439 - DidHandleMap: didHandleMap, 1440 - FilteringBy: state, 1441 - }) 1442 - return 1443 - } 1444 - 1445 - func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1446 - user := s.auth.GetUser(r) 1447 - f, err := fullyResolvedRepo(r) 1448 - if err != nil { 1449 - log.Println("failed to resolve repo:", err) 1450 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1451 - return 1452 - } 1453 - 1454 - pull, ok := r.Context().Value("pull").(*db.Pull) 1455 - if !ok { 1456 - log.Println("failed to get pull") 1457 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1458 - return 1459 - } 1460 - 1461 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 1462 - if err != nil { 1463 - log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1464 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1465 - return 1466 - } 1467 - 1468 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1469 - if err != nil { 1470 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1471 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1472 - return 1473 - } 1474 - 1475 - // Merge the pull request 1476 - resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch) 1477 - if err != nil { 1478 - log.Printf("failed to merge pull request: %s", err) 1479 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1480 - return 1481 - } 1482 - 1483 - if resp.StatusCode == http.StatusOK { 1484 - err := db.MergePull(s.db, f.RepoAt, pull.PullId) 1485 - if err != nil { 1486 - log.Printf("failed to update pull request status in database: %s", err) 1487 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1488 - return 1489 - } 1490 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1491 - } else { 1492 - log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1493 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1494 - } 1495 - } 1496 - 1497 - func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 1498 - user := s.auth.GetUser(r) 1499 - f, err := fullyResolvedRepo(r) 1500 - if err != nil { 1501 - log.Println("failed to get repo and knot", err) 1502 - return 1503 - } 1504 - 1505 - pullId := chi.URLParam(r, "pull") 1506 - pullIdInt, err := strconv.Atoi(pullId) 1507 - if err != nil { 1508 - http.Error(w, "bad pull id", http.StatusBadRequest) 1509 - log.Println("failed to parse pull id", err) 1510 - return 1511 - } 1512 - 1513 - switch r.Method { 1514 - case http.MethodPost: 1515 - body := r.FormValue("body") 1516 - if body == "" { 1517 - s.pages.Notice(w, "pull", "Comment body is required") 1518 - return 1519 - } 1520 - 1521 - // Start a transaction 1522 - tx, err := s.db.BeginTx(r.Context(), nil) 1523 - if err != nil { 1524 - log.Println("failed to start transaction", err) 1525 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1526 - return 1527 - } 1528 - defer tx.Rollback() // Will be ignored if we commit 1529 - 1530 - commentId := rand.IntN(1000000) 1531 - createdAt := time.Now().Format(time.RFC3339) 1532 - commentIdInt64 := int64(commentId) 1533 - ownerDid := user.Did 1534 - 1535 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pullIdInt) 1536 - if err != nil { 1537 - log.Println("failed to get pull at", err) 1538 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1539 - return 1540 - } 1541 - 1542 - atUri := f.RepoAt.String() 1543 - client, _ := s.auth.AuthorizedClient(r) 1544 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1545 - Collection: tangled.RepoPullCommentNSID, 1546 - Repo: user.Did, 1547 - Rkey: s.TID(), 1548 - Record: &lexutil.LexiconTypeDecoder{ 1549 - Val: &tangled.RepoPullComment{ 1550 - Repo: &atUri, 1551 - Pull: pullAt, 1552 - CommentId: &commentIdInt64, 1553 - Owner: &ownerDid, 1554 - Body: &body, 1555 - CreatedAt: &createdAt, 1556 - }, 1557 - }, 1558 - }) 1559 - if err != nil { 1560 - log.Println("failed to create pull comment", err) 1561 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1562 - return 1563 - } 1564 - 1565 - // Create the pull comment in the database with the commentAt field 1566 - err = db.NewPullComment(tx, &db.PullComment{ 1567 - OwnerDid: user.Did, 1568 - RepoAt: f.RepoAt.String(), 1569 - CommentId: commentId, 1570 - PullId: pullIdInt, 1571 - Body: body, 1572 - CommentAt: atResp.Uri, 1573 - }) 1574 - if err != nil { 1575 - log.Println("failed to create pull comment", err) 1576 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1577 - return 1578 - } 1579 - 1580 - // Commit the transaction 1581 - if err = tx.Commit(); err != nil { 1582 - log.Println("failed to commit transaction", err) 1583 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1584 - return 1585 - } 1586 - 1587 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pullIdInt, commentId)) 1588 - return 1589 - } 1590 - } 1591 - 1592 - func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1593 - user := s.auth.GetUser(r) 1594 - 1595 - f, err := fullyResolvedRepo(r) 1596 - if err != nil { 1597 - log.Println("malformed middleware") 1598 - return 1599 - } 1600 - 1601 - pull, ok := r.Context().Value("pull").(*db.Pull) 1602 - if !ok { 1603 - log.Println("failed to get pull") 1604 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1605 - return 1606 - } 1607 - 1608 - // auth filter: only owner or collaborators can close 1609 - roles := RolesInRepo(s, user, f) 1610 - isCollaborator := roles.IsCollaborator() 1611 - isPullAuthor := user.Did == pull.OwnerDid 1612 - isCloseAllowed := isCollaborator || isPullAuthor 1613 - if !isCloseAllowed { 1614 - log.Println("failed to close pull") 1615 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1616 - return 1617 - } 1618 - 1619 - // Start a transaction 1620 - tx, err := s.db.BeginTx(r.Context(), nil) 1621 - if err != nil { 1622 - log.Println("failed to start transaction", err) 1623 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1624 - return 1625 - } 1626 - 1627 - // Close the pull in the database 1628 - err = db.ClosePull(tx, f.RepoAt, pull.PullId) 1629 - if err != nil { 1630 - log.Println("failed to close pull", err) 1631 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1632 - return 1633 - } 1634 - 1635 - // Commit the transaction 1636 - if err = tx.Commit(); err != nil { 1637 - log.Println("failed to commit transaction", err) 1638 - s.pages.Notice(w, "pull-close", "Failed to close pull.") 1639 - return 1640 - } 1641 - 1642 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1643 - return 1644 - } 1645 - 1646 - func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1647 - user := s.auth.GetUser(r) 1648 - 1649 - f, err := fullyResolvedRepo(r) 1650 - if err != nil { 1651 - log.Println("failed to resolve repo", err) 1652 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1653 - return 1654 - } 1655 - 1656 - pull, ok := r.Context().Value("pull").(*db.Pull) 1657 - if !ok { 1658 - log.Println("failed to get pull") 1659 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 1660 - return 1661 - } 1662 - 1663 - // auth filter: only owner or collaborators can close 1664 - roles := RolesInRepo(s, user, f) 1665 - isCollaborator := roles.IsCollaborator() 1666 - isPullAuthor := user.Did == pull.OwnerDid 1667 - isCloseAllowed := isCollaborator || isPullAuthor 1668 - if !isCloseAllowed { 1669 - log.Println("failed to close pull") 1670 - s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.") 1671 - return 1672 - } 1673 - 1674 - // Start a transaction 1675 - tx, err := s.db.BeginTx(r.Context(), nil) 1676 - if err != nil { 1677 - log.Println("failed to start transaction", err) 1678 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1679 - return 1680 - } 1681 - 1682 - // Reopen the pull in the database 1683 - err = db.ReopenPull(tx, f.RepoAt, pull.PullId) 1684 - if err != nil { 1685 - log.Println("failed to reopen pull", err) 1686 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1687 - return 1688 - } 1689 - 1690 - // Commit the transaction 1691 - if err = tx.Commit(); err != nil { 1692 - log.Println("failed to commit transaction", err) 1693 - s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1694 - return 1695 - } 1696 - 1697 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 1698 - return 1699 1071 } 1700 1072 1701 1073 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)