forked from tangled.org/core
Monorepo for Tangled

appview: merge pulls and associated ui changes

anirudh.fi cd8dfb18 c2f38ac4

verified
Changed files
+461 -89
appview
db
pages
templates
layouts
repo
state
+7 -6
appview/db/pulls.go
··· 10 10 type PullState int 11 11 12 12 const ( 13 - PullOpen PullState = iota 13 + PullClosed PullState = iota 14 + PullOpen 14 15 PullMerged 15 - PullClosed 16 16 ) 17 17 18 18 func (p PullState) String() string { ··· 87 87 } 88 88 89 89 pull.PullId = nextId 90 + pull.State = PullOpen 90 91 91 92 _, err = tx.Exec(` 92 - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, patch, rkey) 93 - values (?, ?, ?, ?, ?, ?, ?, ?) 94 - `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Patch, pull.Rkey) 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) 95 96 if err != nil { 96 97 return err 97 98 } ··· 134 135 pull_at, 135 136 body, 136 137 patch, 137 - rkey 138 + rkey 138 139 from 139 140 pulls 140 141 where
+7 -2
appview/pages/pages.go
··· 271 271 func (r RepoInfo) TabMetadata() map[string]any { 272 272 meta := make(map[string]any) 273 273 274 - meta["issues"] = r.Stats.IssueCount.Open 275 - meta["pulls"] = r.Stats.PullCount.Open 274 + if r.Stats.PullCount.Open > 0 { 275 + meta["pulls"] = r.Stats.PullCount.Open 276 + } 277 + 278 + if r.Stats.IssueCount.Open > 0 { 279 + meta["issues"] = r.Stats.IssueCount.Open 280 + } 276 281 277 282 // more stuff? 278 283
+1 -1
appview/pages/templates/layouts/repobase.html
··· 38 38 > 39 39 {{ $key }} 40 40 {{ if not (isNil $meta) }} 41 - <span class="bg-gray-200 rounded py-1/2 px-1 text-sm font-mono">{{ $meta }}</span> 41 + <span class="bg-gray-200 rounded py-1/2 px-1 text-sm">{{ $meta }}</span> 42 42 {{ end }} 43 43 </div> 44 44 </a>
+4 -4
appview/pages/templates/repo/pulls/new.html
··· 8 8 > 9 9 <div class="flex flex-col gap-4"> 10 10 <div> 11 - <label for="title">title</label> 11 + <label for="title">write a title</label> 12 12 <input type="text" name="title" id="title" class="w-full" /> 13 13 14 - <label for="targetBranch">target branch</label> 14 + <label for="targetBranch">select a target branch</label> 15 15 <p class="text-gray-500"> 16 16 The branch you want to make your change against. 17 17 </p> 18 18 <select name="targetBranch" class="p-1 border border-gray-200 bg-white"> 19 - <option disabled selected>Select a branch</option> 19 + <option disabled selected>select a branch</option> 20 20 {{ range .Branches }} 21 21 <option 22 22 value="{{ .Reference.Name }}" ··· 27 27 </select> 28 28 </div> 29 29 <div> 30 - <label for="body">body</label> 30 + <label for="body">add a description</label> 31 31 <textarea 32 32 name="body" 33 33 id="body"
+176 -53
appview/pages/templates/repo/pulls/pull.html
··· 4 4 {{ end }} 5 5 6 6 {{ define "repoContent" }} 7 - 8 7 <header class="pb-4"> 9 8 <h1 class="text-2xl"> 10 9 {{ .Pull.Title }} ··· 17 16 18 17 {{ if .Pull.State.IsOpen }} 19 18 {{ $bgColor = "bg-green-600" }} 20 - {{ $icon = "circle-dot" }} 19 + {{ $icon = "git-pull-request" }} 21 20 {{ else if .Pull.State.IsMerged }} 22 21 {{ $bgColor = "bg-purple-600" }} 23 22 {{ $icon = "git-merge" }} ··· 91 90 type="button" 92 91 class="btn btn-sm" 93 92 onclick="togglePatchEdit(true)" 93 + {{ if or .Pull.State.IsMerged .Pull.State.IsClosed }} 94 + disabled title="Cannot edit closed or merged 95 + pull requests" 96 + {{ end }} 94 97 > 95 98 <i data-lucide="edit" class="w-4 h-4 mr-1"></i>Edit 96 99 </button> ··· 147 150 </script> 148 151 </details> 149 152 </div> 150 - 151 - {{ if .MergeCheck }} 152 - <div class="mt-4" id="merge-check"> 153 - <div 154 - class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }} 155 - bg-red-50 border-red-200 156 - {{ else }} 157 - bg-green-50 border-green-200 158 - {{ end }}" 159 - > 160 - <div 161 - class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }} 162 - text-red-500 163 - {{ else }} 164 - text-green-500 165 - {{ end }}" 166 - > 167 - {{ if .MergeCheck.IsConflicted }} 168 - <i data-lucide="alert-triangle" class="w-4 h-4"></i> 169 - <span class="font-medium" 170 - >merge conflicts detected</span 171 - > 172 - {{ else }} 173 - <i data-lucide="check-circle" class="w-4 h-4"></i> 174 - <span class="font-medium">ready to merge</span> 175 - {{ end }} 176 - </div> 177 - 178 - {{ if .MergeCheck.IsConflicted }} 179 - <div class="mt-2"> 180 - <ul class="text-sm space-y-1"> 181 - {{ range .MergeCheck.Conflicts }} 182 - <li class="flex items-center"> 183 - <i 184 - data-lucide="file-warning" 185 - class="w-3 h-3 mr-1.5 text-red-500" 186 - ></i> 187 - <span class="font-mono" 188 - >{{ slice .Filename 0 (sub (len .Filename) 2) }}</span 189 - > 190 - </li> 191 - {{ end }} 192 - </ul> 193 - </div> 194 - {{ end }} 195 - </div> 196 - </div> 197 - {{ end }} 198 153 {{ end }} 199 154 200 155 {{ define "repoAfter" }} ··· 239 194 </div> 240 195 {{ end }} 241 196 197 + {{ if .Pull.State.IsMerged }} 198 + <div 199 + id="merge-status-card" 200 + class="rounded relative bg-purple-50 border border-purple-200 p-4" 201 + > 202 + {{ if gt (len .Comments) 0 }} 203 + <div 204 + class="absolute left-8 -top-4 w-px h-4 bg-gray-300" 205 + ></div> 206 + {{ else }} 207 + <div 208 + class="absolute left-8 -top-8 w-px h-8 bg-gray-300" 209 + ></div> 210 + {{ end }} 211 + 212 + 213 + <div class="flex items-center gap-2 text-purple-500"> 214 + <i data-lucide="git-merge" class="w-4 h-4"></i> 215 + <span class="font-medium" 216 + >Pull request successfully merged</span 217 + > 218 + </div> 219 + 220 + <div class="mt-2 text-sm text-gray-700"> 221 + <p> 222 + This pull request has been merged into the base branch. 223 + </p> 224 + </div> 225 + </div> 226 + {{ else if .MergeCheck }} 227 + <div 228 + id="merge-status-card" 229 + class="rounded relative {{ if .MergeCheck.IsConflicted }} 230 + bg-red-50 border border-red-200 231 + {{ else }} 232 + bg-green-50 border border-green-200 233 + {{ end }} p-4" 234 + > 235 + {{ if gt (len .Comments) 0 }} 236 + <div 237 + class="absolute left-8 -top-4 w-px h-4 bg-gray-300" 238 + ></div> 239 + {{ else }} 240 + <div 241 + class="absolute left-8 -top-8 w-px h-8 bg-gray-300" 242 + ></div> 243 + {{ end }} 244 + 245 + 246 + <div 247 + class="flex items-center gap-2 {{ if .MergeCheck.IsConflicted }} 248 + text-red-500 249 + {{ else }} 250 + text-green-500 251 + {{ end }}" 252 + > 253 + {{ if .MergeCheck.IsConflicted }} 254 + <i data-lucide="alert-triangle" class="w-4 h-4"></i> 255 + <span class="font-medium" 256 + >merge conflicts detected</span 257 + > 258 + {{ else }} 259 + <i data-lucide="check-circle" class="w-4 h-4"></i> 260 + <span class="font-medium">ready to merge</span> 261 + {{ end }} 262 + </div> 263 + 264 + {{ if .MergeCheck.IsConflicted }} 265 + <div class="mt-2"> 266 + <ul class="text-sm space-y-1"> 267 + {{ range .MergeCheck.Conflicts }} 268 + <li class="flex items-center"> 269 + <i 270 + data-lucide="file-warning" 271 + class="w-3 h-3 mr-1.5 text-red-500" 272 + ></i> 273 + <span class="font-mono" 274 + >{{ slice .Filename 0 (sub (len .Filename) 2) }}</span 275 + > 276 + </li> 277 + {{ end }} 278 + </ul> 279 + </div> 280 + <div class="mt-3 text-sm text-gray-700"> 281 + <p> 282 + Please resolve these conflicts locally and update 283 + the patch to continue with the merge. 284 + </p> 285 + </div> 286 + {{ else }} 287 + <div class="mt-2 text-sm text-gray-700"> 288 + <p> 289 + No conflicts detected with the base branch. This 290 + pull request can be merged safely. 291 + </p> 292 + </div> 293 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }} 294 + <div class="mt-4 flex items-center gap-2"> 295 + <form 296 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 297 + hx-swap="none" 298 + > 299 + <input 300 + type="hidden" 301 + name="targetBranch" 302 + value="{{ .Pull.TargetBranch }}" 303 + /> 304 + <input 305 + type="hidden" 306 + name="patch" 307 + value="{{ .Pull.Patch }}" 308 + /> 309 + <button 310 + type="submit" 311 + class="btn flex items-center gap-2" 312 + {{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }} 313 + disabled 314 + {{ end }} 315 + > 316 + <i 317 + data-lucide="git-merge" 318 + class="w-4 h-4 text-purple-500" 319 + ></i> 320 + <span>merge</span> 321 + </button> 322 + </form> 323 + 324 + {{ if or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }} 325 + {{ $action := "close" }} 326 + {{ $icon := "circle-x" }} 327 + {{ $hoverColor := "red" }} 328 + {{ if .Pull.State.IsClosed }} 329 + {{ $action = "reopen" }} 330 + {{ $icon = "circle-dot" }} 331 + {{ $hoverColor = "green" }} 332 + {{ end }} 333 + <form 334 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}" 335 + hx-swap="none" 336 + > 337 + <button 338 + type="submit" 339 + class="btn flex items-center gap-2" 340 + > 341 + <i 342 + data-lucide="{{ $icon }}" 343 + class="w-4 h-4 text-{{ $hoverColor }}-400" 344 + ></i> 345 + <span>{{ $action }}</span> 346 + </button> 347 + </form> 348 + <div id="pull-merge-error" class="error"></div> 349 + <div 350 + id="pull-merge-success" 351 + class="success" 352 + ></div> 353 + {{ end }} 354 + </div> 355 + {{ end }} 356 + {{ end }} 357 + </div> 358 + {{ end }} 242 359 </section> 243 360 244 361 {{ if .LoggedInUser }} 245 362 <form 246 363 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment" 247 364 class="mt-8" 365 + hx-swap="none" 248 366 > 249 367 <textarea 250 368 name="body" ··· 256 374 </form> 257 375 {{ end }} 258 376 259 - {{ if eq .LoggedInUser.Did .Pull.OwnerDid }} 377 + {{ if and (or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) (not .MergeCheck) (not .Pull.State.IsMerged) }} 260 378 {{ $action := "close" }} 261 379 {{ $icon := "circle-x" }} 262 380 {{ $hoverColor := "red" }} ··· 268 386 <form 269 387 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}" 270 388 hx-swap="none" 271 - class="mt-8"> 272 - <button type="submit" class="btn hover:bg-{{ $hoverColor }}-300"> 389 + class="mt-8" 390 + > 391 + <button type="submit" class="btn text-sm flex items-center gap-2"> 273 392 <i 274 393 data-lucide="{{ $icon }}" 275 394 class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400" ··· 278 397 </button> 279 398 </form> 280 399 {{ end }} 400 + 401 + 402 + <div id="pull-close"></div> 403 + <div id="pull-reopen"></div> 281 404 {{ end }}
+259 -18
appview/state/repo.go
··· 523 523 } 524 524 } 525 525 526 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 527 - if err != nil { 528 - log.Printf("failed to get registration key for %s", f.Knot) 529 - s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 530 - return 531 - } 532 - 533 526 var mergeCheckResponse types.MergeCheckResponse 534 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 535 - if err == nil { 536 - resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch) 527 + 528 + // Only perform merge check if the pull request is not already merged 529 + if pr.State != db.PullMerged { 530 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 537 531 if err != nil { 538 - log.Println("failed to check for mergeability:", err) 539 - } else { 540 - respBody, err := io.ReadAll(resp.Body) 532 + log.Printf("failed to get registration key for %s", f.Knot) 533 + s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 534 + return 535 + } 536 + 537 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 538 + if err == nil { 539 + resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch) 541 540 if err != nil { 542 - log.Println("failed to read merge check response body") 541 + log.Println("failed to check for mergeability:", err) 543 542 } else { 544 - err = json.Unmarshal(respBody, &mergeCheckResponse) 543 + respBody, err := io.ReadAll(resp.Body) 545 544 if err != nil { 546 - log.Println("failed to unmarshal merge check response", err) 545 + log.Println("failed to read merge check response body") 546 + } else { 547 + err = json.Unmarshal(respBody, &mergeCheckResponse) 548 + if err != nil { 549 + log.Println("failed to unmarshal merge check response", err) 550 + } 547 551 } 548 552 } 553 + } else { 554 + log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 549 555 } 550 - } else { 551 - log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 552 556 } 553 557 554 558 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ ··· 1432 1436 DidHandleMap: didHandleMap, 1433 1437 FilteringBy: state, 1434 1438 }) 1439 + return 1440 + } 1441 + 1442 + func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1443 + user := s.auth.GetUser(r) 1444 + f, err := fullyResolvedRepo(r) 1445 + if err != nil { 1446 + log.Println("failed to resolve repo:", err) 1447 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1448 + return 1449 + } 1450 + 1451 + // Get the pull request ID from the request URL 1452 + pullId := chi.URLParam(r, "pull") 1453 + pullIdInt, err := strconv.Atoi(pullId) 1454 + if err != nil { 1455 + log.Println("failed to parse pull ID:", err) 1456 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1457 + return 1458 + } 1459 + 1460 + // Get the patch data from the request body 1461 + patch := r.FormValue("patch") 1462 + branch := r.FormValue("targetBranch") 1463 + 1464 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 1465 + if err != nil { 1466 + log.Printf("no registration key found for domain %s: %s\n", f.Knot, err) 1467 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1468 + return 1469 + } 1470 + 1471 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1472 + if err != nil { 1473 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1474 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1475 + return 1476 + } 1477 + 1478 + // Merge the pull request 1479 + resp, err := ksClient.Merge([]byte(patch), user.Did, f.RepoName, branch) 1480 + if err != nil { 1481 + log.Printf("failed to merge pull request: %s", err) 1482 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1483 + return 1484 + } 1485 + 1486 + if resp.StatusCode == http.StatusOK { 1487 + err := db.MergePull(s.db, f.RepoAt, pullIdInt) 1488 + if err != nil { 1489 + log.Printf("failed to update pull request status in database: %s", err) 1490 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1491 + return 1492 + } 1493 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pullIdInt)) 1494 + } else { 1495 + log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode) 1496 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1497 + } 1498 + } 1499 + 1500 + func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 1501 + user := s.auth.GetUser(r) 1502 + f, err := fullyResolvedRepo(r) 1503 + if err != nil { 1504 + log.Println("failed to get repo and knot", err) 1505 + return 1506 + } 1507 + 1508 + pullId := chi.URLParam(r, "pull") 1509 + pullIdInt, err := strconv.Atoi(pullId) 1510 + if err != nil { 1511 + http.Error(w, "bad pull id", http.StatusBadRequest) 1512 + log.Println("failed to parse pull id", err) 1513 + return 1514 + } 1515 + 1516 + switch r.Method { 1517 + case http.MethodPost: 1518 + body := r.FormValue("body") 1519 + if body == "" { 1520 + s.pages.Notice(w, "pull", "Comment body is required") 1521 + return 1522 + } 1523 + 1524 + // Start a transaction 1525 + tx, err := s.db.BeginTx(r.Context(), nil) 1526 + if err != nil { 1527 + log.Println("failed to start transaction", err) 1528 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1529 + return 1530 + } 1531 + defer tx.Rollback() // Will be ignored if we commit 1532 + 1533 + commentId := rand.IntN(1000000) 1534 + createdAt := time.Now().Format(time.RFC3339) 1535 + commentIdInt64 := int64(commentId) 1536 + ownerDid := user.Did 1537 + 1538 + pullAt, err := db.GetPullAt(s.db, f.RepoAt, pullIdInt) 1539 + if err != nil { 1540 + log.Println("failed to get pull at", err) 1541 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1542 + return 1543 + } 1544 + 1545 + atUri := f.RepoAt.String() 1546 + client, _ := s.auth.AuthorizedClient(r) 1547 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1548 + Collection: tangled.RepoPullCommentNSID, 1549 + Repo: user.Did, 1550 + Rkey: s.TID(), 1551 + Record: &lexutil.LexiconTypeDecoder{ 1552 + Val: &tangled.RepoPullComment{ 1553 + Repo: &atUri, 1554 + Pull: pullAt, 1555 + CommentId: &commentIdInt64, 1556 + Owner: &ownerDid, 1557 + Body: &body, 1558 + CreatedAt: &createdAt, 1559 + }, 1560 + }, 1561 + }) 1562 + if err != nil { 1563 + log.Println("failed to create pull comment", err) 1564 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1565 + return 1566 + } 1567 + 1568 + // Create the pull comment in the database with the commentAt field 1569 + err = db.NewPullComment(tx, &db.PullComment{ 1570 + OwnerDid: user.Did, 1571 + RepoAt: f.RepoAt.String(), 1572 + CommentId: commentId, 1573 + PullId: pullIdInt, 1574 + Body: body, 1575 + CommentAt: atResp.Uri, 1576 + }) 1577 + if err != nil { 1578 + log.Println("failed to create pull comment", err) 1579 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1580 + return 1581 + } 1582 + 1583 + // Commit the transaction 1584 + if err = tx.Commit(); err != nil { 1585 + log.Println("failed to commit transaction", err) 1586 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 1587 + return 1588 + } 1589 + 1590 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pullIdInt, commentId)) 1591 + return 1592 + } 1593 + } 1594 + 1595 + func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1596 + f, err := fullyResolvedRepo(r) 1597 + if err != nil { 1598 + log.Println("malformed middleware") 1599 + return 1600 + } 1601 + 1602 + pullId := chi.URLParam(r, "pull") 1603 + pullIdInt, err := strconv.Atoi(pullId) 1604 + if err != nil { 1605 + log.Println("malformed middleware") 1606 + return 1607 + } 1608 + 1609 + // Start a transaction 1610 + tx, err := s.db.BeginTx(r.Context(), nil) 1611 + if err != nil { 1612 + log.Println("failed to start transaction", err) 1613 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1614 + return 1615 + } 1616 + 1617 + // Close the pull in the database 1618 + err = db.ClosePull(tx, f.RepoAt, pullIdInt) 1619 + if err != nil { 1620 + log.Println("failed to close pull", err) 1621 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1622 + return 1623 + } 1624 + 1625 + // Commit the transaction 1626 + if err = tx.Commit(); err != nil { 1627 + log.Println("failed to commit transaction", err) 1628 + s.pages.Notice(w, "pull-close", "Failed to close pull.") 1629 + return 1630 + } 1631 + 1632 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt)) 1633 + return 1634 + } 1635 + 1636 + func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1637 + f, err := fullyResolvedRepo(r) 1638 + if err != nil { 1639 + log.Println("failed to resolve repo", err) 1640 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1641 + return 1642 + } 1643 + 1644 + // Start a transaction 1645 + tx, err := s.db.BeginTx(r.Context(), nil) 1646 + if err != nil { 1647 + log.Println("failed to start transaction", err) 1648 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1649 + return 1650 + } 1651 + 1652 + pullId := chi.URLParam(r, "pull") 1653 + pullIdInt, err := strconv.Atoi(pullId) 1654 + if err != nil { 1655 + log.Println("failed to parse pull id", err) 1656 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1657 + return 1658 + } 1659 + 1660 + // Reopen the pull in the database 1661 + err = db.ReopenPull(tx, f.RepoAt, pullIdInt) 1662 + if err != nil { 1663 + log.Println("failed to reopen pull", err) 1664 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1665 + return 1666 + } 1667 + 1668 + // Commit the transaction 1669 + if err = tx.Commit(); err != nil { 1670 + log.Println("failed to commit transaction", err) 1671 + s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.") 1672 + return 1673 + } 1674 + 1675 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt)) 1435 1676 return 1436 1677 } 1437 1678
+4 -4
appview/state/router.go
··· 65 65 r.Get("/new", s.NewPull) 66 66 r.Post("/new", s.NewPull) 67 67 r.Patch("/{pull}/patch", s.EditPatch) 68 - // r.Post("/{pull}/comment", s.PullComment) 69 - // r.Post("/{pull}/close", s.ClosePull) 70 - // r.Post("/{pull}/reopen", s.ReopenPull) 71 - // r.Post("/{pull}/merge", s.MergePull) 68 + r.Post("/{pull}/comment", s.PullComment) 69 + r.Post("/{pull}/close", s.ClosePull) 70 + r.Post("/{pull}/reopen", s.ReopenPull) 71 + r.Post("/{pull}/merge", s.MergePull) 72 72 }) 73 73 }) 74 74
+3 -1
input.css
··· 139 139 hover:before:shadow-[0_2px_2px_0_rgba(20,20,96,0.1),inset_0_-2px_0_0_#f5f5f5] 140 140 focus:outline-none focus-visible:before:outline 141 141 focus-visible:before:outline-4 focus-visible:before:outline-gray-500 142 - active:before:shadow-[inset_0_2px_2px_0_rgba(20,20,96,0.1)]; 142 + active:before:shadow-[inset_0_2px_2px_0_rgba(20,20,96,0.1)] 143 + disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:before:border-gray-200 144 + disabled:hover:before:bg-white disabled:hover:before:shadow-none; 143 145 } 144 146 } 145 147 @layer utilities {