+7
-6
appview/db/pulls.go
+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
+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
+1
-1
appview/pages/templates/layouts/repobase.html
+4
-4
appview/pages/templates/repo/pulls/new.html
+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
+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
+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
+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
+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 {