+49
-9
appview/db/db.go
+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
+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
+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
+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
+92
-165
appview/pages/templates/repo/pulls/pull.html
···
1
1
{{ define "title" }}
2
-
{{ .Pull.Title }} · pull #{{ .Pull.PullId }} ·
3
-
{{ .RepoInfo.FullName }}
2
+
{{ .Pull.Title }} · pull #{{ .Pull.PullId }} · {{ .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
+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
+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
-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
+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)