+188
-44
api/tangled/cbor_gen.go
+188
-44
api/tangled/cbor_gen.go
···
2076
2076
fieldCount--
2077
2077
}
2078
2078
2079
-
if t.SourceRepo == nil {
2079
+
if t.Source == nil {
2080
2080
fieldCount--
2081
2081
}
2082
2082
···
2203
2203
}
2204
2204
}
2205
2205
2206
-
// t.CreatedAt (string) (string)
2207
-
if t.CreatedAt != nil {
2206
+
// t.Source (tangled.RepoPull_Source) (struct)
2207
+
if t.Source != nil {
2208
2208
2209
-
if len("createdAt") > 1000000 {
2210
-
return xerrors.Errorf("Value in field \"createdAt\" was too long")
2209
+
if len("source") > 1000000 {
2210
+
return xerrors.Errorf("Value in field \"source\" was too long")
2211
2211
}
2212
2212
2213
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
2213
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil {
2214
2214
return err
2215
2215
}
2216
-
if _, err := cw.WriteString(string("createdAt")); err != nil {
2216
+
if _, err := cw.WriteString(string("source")); err != nil {
2217
2217
return err
2218
2218
}
2219
2219
2220
-
if t.CreatedAt == nil {
2221
-
if _, err := cw.Write(cbg.CborNull); err != nil {
2222
-
return err
2223
-
}
2224
-
} else {
2225
-
if len(*t.CreatedAt) > 1000000 {
2226
-
return xerrors.Errorf("Value in field t.CreatedAt was too long")
2227
-
}
2228
-
2229
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil {
2230
-
return err
2231
-
}
2232
-
if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil {
2233
-
return err
2234
-
}
2220
+
if err := t.Source.MarshalCBOR(cw); err != nil {
2221
+
return err
2235
2222
}
2236
2223
}
2237
2224
2238
-
// t.SourceRepo (string) (string)
2239
-
if t.SourceRepo != nil {
2225
+
// t.CreatedAt (string) (string)
2226
+
if t.CreatedAt != nil {
2240
2227
2241
-
if len("sourceRepo") > 1000000 {
2242
-
return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
2228
+
if len("createdAt") > 1000000 {
2229
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
2243
2230
}
2244
2231
2245
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
2232
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
2246
2233
return err
2247
2234
}
2248
-
if _, err := cw.WriteString(string("sourceRepo")); err != nil {
2235
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
2249
2236
return err
2250
2237
}
2251
2238
2252
-
if t.SourceRepo == nil {
2239
+
if t.CreatedAt == nil {
2253
2240
if _, err := cw.Write(cbg.CborNull); err != nil {
2254
2241
return err
2255
2242
}
2256
2243
} else {
2257
-
if len(*t.SourceRepo) > 1000000 {
2258
-
return xerrors.Errorf("Value in field t.SourceRepo was too long")
2244
+
if len(*t.CreatedAt) > 1000000 {
2245
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
2259
2246
}
2260
2247
2261
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
2248
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil {
2262
2249
return err
2263
2250
}
2264
-
if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
2251
+
if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil {
2265
2252
return err
2266
2253
}
2267
2254
}
···
2436
2423
2437
2424
t.PullId = int64(extraI)
2438
2425
}
2439
-
// t.CreatedAt (string) (string)
2440
-
case "createdAt":
2426
+
// t.Source (tangled.RepoPull_Source) (struct)
2427
+
case "source":
2441
2428
2442
2429
{
2430
+
2443
2431
b, err := cr.ReadByte()
2444
2432
if err != nil {
2445
2433
return err
···
2448
2436
if err := cr.UnreadByte(); err != nil {
2449
2437
return err
2450
2438
}
2451
-
2452
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
2453
-
if err != nil {
2454
-
return err
2439
+
t.Source = new(RepoPull_Source)
2440
+
if err := t.Source.UnmarshalCBOR(cr); err != nil {
2441
+
return xerrors.Errorf("unmarshaling t.Source pointer: %w", err)
2455
2442
}
2456
-
2457
-
t.CreatedAt = (*string)(&sval)
2458
2443
}
2444
+
2459
2445
}
2460
-
// t.SourceRepo (string) (string)
2461
-
case "sourceRepo":
2446
+
// t.CreatedAt (string) (string)
2447
+
case "createdAt":
2462
2448
2463
2449
{
2464
2450
b, err := cr.ReadByte()
···
2475
2461
return err
2476
2462
}
2477
2463
2478
-
t.SourceRepo = (*string)(&sval)
2464
+
t.CreatedAt = (*string)(&sval)
2479
2465
}
2480
2466
}
2481
2467
// t.TargetRepo (string) (string)
···
2499
2485
}
2500
2486
2501
2487
t.TargetBranch = string(sval)
2488
+
}
2489
+
2490
+
default:
2491
+
// Field doesn't exist on this type, so ignore it
2492
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
2493
+
return err
2494
+
}
2495
+
}
2496
+
}
2497
+
2498
+
return nil
2499
+
}
2500
+
func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error {
2501
+
if t == nil {
2502
+
_, err := w.Write(cbg.CborNull)
2503
+
return err
2504
+
}
2505
+
2506
+
cw := cbg.NewCborWriter(w)
2507
+
fieldCount := 2
2508
+
2509
+
if t.Repo == nil {
2510
+
fieldCount--
2511
+
}
2512
+
2513
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
2514
+
return err
2515
+
}
2516
+
2517
+
// t.Repo (string) (string)
2518
+
if t.Repo != nil {
2519
+
2520
+
if len("repo") > 1000000 {
2521
+
return xerrors.Errorf("Value in field \"repo\" was too long")
2522
+
}
2523
+
2524
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil {
2525
+
return err
2526
+
}
2527
+
if _, err := cw.WriteString(string("repo")); err != nil {
2528
+
return err
2529
+
}
2530
+
2531
+
if t.Repo == nil {
2532
+
if _, err := cw.Write(cbg.CborNull); err != nil {
2533
+
return err
2534
+
}
2535
+
} else {
2536
+
if len(*t.Repo) > 1000000 {
2537
+
return xerrors.Errorf("Value in field t.Repo was too long")
2538
+
}
2539
+
2540
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil {
2541
+
return err
2542
+
}
2543
+
if _, err := cw.WriteString(string(*t.Repo)); err != nil {
2544
+
return err
2545
+
}
2546
+
}
2547
+
}
2548
+
2549
+
// t.Branch (string) (string)
2550
+
if len("branch") > 1000000 {
2551
+
return xerrors.Errorf("Value in field \"branch\" was too long")
2552
+
}
2553
+
2554
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil {
2555
+
return err
2556
+
}
2557
+
if _, err := cw.WriteString(string("branch")); err != nil {
2558
+
return err
2559
+
}
2560
+
2561
+
if len(t.Branch) > 1000000 {
2562
+
return xerrors.Errorf("Value in field t.Branch was too long")
2563
+
}
2564
+
2565
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil {
2566
+
return err
2567
+
}
2568
+
if _, err := cw.WriteString(string(t.Branch)); err != nil {
2569
+
return err
2570
+
}
2571
+
return nil
2572
+
}
2573
+
2574
+
func (t *RepoPull_Source) UnmarshalCBOR(r io.Reader) (err error) {
2575
+
*t = RepoPull_Source{}
2576
+
2577
+
cr := cbg.NewCborReader(r)
2578
+
2579
+
maj, extra, err := cr.ReadHeader()
2580
+
if err != nil {
2581
+
return err
2582
+
}
2583
+
defer func() {
2584
+
if err == io.EOF {
2585
+
err = io.ErrUnexpectedEOF
2586
+
}
2587
+
}()
2588
+
2589
+
if maj != cbg.MajMap {
2590
+
return fmt.Errorf("cbor input should be of type map")
2591
+
}
2592
+
2593
+
if extra > cbg.MaxLength {
2594
+
return fmt.Errorf("RepoPull_Source: map struct too large (%d)", extra)
2595
+
}
2596
+
2597
+
n := extra
2598
+
2599
+
nameBuf := make([]byte, 6)
2600
+
for i := uint64(0); i < n; i++ {
2601
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
2602
+
if err != nil {
2603
+
return err
2604
+
}
2605
+
2606
+
if !ok {
2607
+
// Field doesn't exist on this type, so ignore it
2608
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
2609
+
return err
2610
+
}
2611
+
continue
2612
+
}
2613
+
2614
+
switch string(nameBuf[:nameLen]) {
2615
+
// t.Repo (string) (string)
2616
+
case "repo":
2617
+
2618
+
{
2619
+
b, err := cr.ReadByte()
2620
+
if err != nil {
2621
+
return err
2622
+
}
2623
+
if b != cbg.CborNull[0] {
2624
+
if err := cr.UnreadByte(); err != nil {
2625
+
return err
2626
+
}
2627
+
2628
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
2629
+
if err != nil {
2630
+
return err
2631
+
}
2632
+
2633
+
t.Repo = (*string)(&sval)
2634
+
}
2635
+
}
2636
+
// t.Branch (string) (string)
2637
+
case "branch":
2638
+
2639
+
{
2640
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
2641
+
if err != nil {
2642
+
return err
2643
+
}
2644
+
2645
+
t.Branch = string(sval)
2502
2646
}
2503
2647
2504
2648
default:
+15
-9
api/tangled/repopull.go
+15
-9
api/tangled/repopull.go
···
17
17
} //
18
18
// RECORDTYPE: RepoPull
19
19
type RepoPull struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
-
CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"`
23
-
Patch string `json:"patch" cborgen:"patch"`
24
-
PullId int64 `json:"pullId" cborgen:"pullId"`
25
-
SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
26
-
TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
27
-
TargetRepo string `json:"targetRepo" cborgen:"targetRepo"`
28
-
Title string `json:"title" cborgen:"title"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
+
CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"`
23
+
Patch string `json:"patch" cborgen:"patch"`
24
+
PullId int64 `json:"pullId" cborgen:"pullId"`
25
+
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
26
+
TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
27
+
TargetRepo string `json:"targetRepo" cborgen:"targetRepo"`
28
+
Title string `json:"title" cborgen:"title"`
29
+
}
30
+
31
+
// RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
32
+
type RepoPull_Source struct {
33
+
Branch string `json:"branch" cborgen:"branch"`
34
+
Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"`
29
35
}
+8
-1
appview/db/db.go
+8
-1
appview/db/db.go
···
257
257
})
258
258
259
259
runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
260
-
// add unconstrained column
261
260
_, err := tx.Exec(`
262
261
alter table comments add column deleted text; -- timestamp
263
262
alter table comments add column edited text; -- timestamp
263
+
`)
264
+
return err
265
+
})
266
+
267
+
runMigration(db, "add-source-info-to-pulls", func(tx *sql.Tx) error {
268
+
_, err := tx.Exec(`
269
+
alter table pulls add column source_branch text;
270
+
alter table pulls add column source_repo_at text;
264
271
`)
265
272
return err
266
273
})
+82
-7
appview/db/pulls.go
+82
-7
appview/db/pulls.go
···
62
62
Submissions []*PullSubmission
63
63
64
64
// meta
65
-
Created time.Time
65
+
Created time.Time
66
+
PullSource *PullSource
67
+
}
68
+
69
+
type PullSource struct {
70
+
Branch string
71
+
Repo *syntax.ATURI
66
72
}
67
73
68
74
type PullSubmission struct {
···
107
113
108
114
func (p *Pull) LastRoundNumber() int {
109
115
return len(p.Submissions) - 1
116
+
}
117
+
118
+
func (p *Pull) IsSameRepoBranch() bool {
119
+
if p.PullSource != nil {
120
+
if p.PullSource.Repo != nil {
121
+
return p.PullSource.Repo == &p.RepoAt
122
+
} else {
123
+
// no repo specified
124
+
return true
125
+
}
126
+
}
127
+
return false
110
128
}
111
129
112
130
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
···
175
193
pull.PullId = nextId
176
194
pull.State = PullOpen
177
195
178
-
_, err = tx.Exec(`
179
-
insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state)
180
-
values (?, ?, ?, ?, ?, ?, ?, ?)
181
-
`, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State)
196
+
var sourceBranch, sourceRepoAt *string
197
+
if pull.PullSource != nil {
198
+
sourceBranch = &pull.PullSource.Branch
199
+
if pull.PullSource.Repo != nil {
200
+
x := pull.PullSource.Repo.String()
201
+
sourceRepoAt = &x
202
+
}
203
+
}
204
+
205
+
_, err = tx.Exec(
206
+
`
207
+
insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at)
208
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
209
+
pull.RepoAt,
210
+
pull.OwnerDid,
211
+
pull.PullId,
212
+
pull.Title,
213
+
pull.TargetBranch,
214
+
pull.Body,
215
+
pull.Rkey,
216
+
pull.State,
217
+
sourceBranch,
218
+
sourceRepoAt,
219
+
)
182
220
if err != nil {
183
221
return err
184
222
}
···
228
266
target_branch,
229
267
pull_at,
230
268
body,
231
-
rkey
269
+
rkey,
270
+
source_branch,
271
+
source_repo_at
232
272
from
233
273
pulls
234
274
where
···
243
283
for rows.Next() {
244
284
var pull Pull
245
285
var createdAt string
286
+
var sourceBranch, sourceRepoAt sql.NullString
246
287
err := rows.Scan(
247
288
&pull.OwnerDid,
248
289
&pull.PullId,
···
253
294
&pull.PullAt,
254
295
&pull.Body,
255
296
&pull.Rkey,
297
+
&sourceBranch,
298
+
&sourceRepoAt,
256
299
)
257
300
if err != nil {
258
301
return nil, err
···
264
307
}
265
308
pull.Created = createdTime
266
309
310
+
if sourceBranch.Valid {
311
+
pull.PullSource = &PullSource{
312
+
Branch: sourceBranch.String,
313
+
}
314
+
if sourceRepoAt.Valid {
315
+
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
316
+
if err != nil {
317
+
return nil, err
318
+
}
319
+
pull.PullSource.Repo = &sourceRepoAtParsed
320
+
}
321
+
}
322
+
267
323
pulls = append(pulls, pull)
268
324
}
269
325
···
286
342
pull_at,
287
343
repo_at,
288
344
body,
289
-
rkey
345
+
rkey,
346
+
source_branch,
347
+
source_repo_at
290
348
from
291
349
pulls
292
350
where
···
296
354
297
355
var pull Pull
298
356
var createdAt string
357
+
var sourceBranch, sourceRepoAt sql.NullString
299
358
err := row.Scan(
300
359
&pull.OwnerDid,
301
360
&pull.PullId,
···
307
366
&pull.RepoAt,
308
367
&pull.Body,
309
368
&pull.Rkey,
369
+
&sourceBranch,
370
+
&sourceRepoAt,
310
371
)
311
372
if err != nil {
312
373
return nil, err
···
317
378
return nil, err
318
379
}
319
380
pull.Created = createdTime
381
+
382
+
// populate source
383
+
if sourceBranch.Valid {
384
+
pull.PullSource = &PullSource{
385
+
Branch: sourceBranch.String,
386
+
}
387
+
if sourceRepoAt.Valid {
388
+
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
389
+
if err != nil {
390
+
return nil, err
391
+
}
392
+
pull.PullSource.Repo = &sourceRepoAtParsed
393
+
}
394
+
}
320
395
321
396
submissionsQuery := `
322
397
select
+2
-3
appview/pages/pages.go
+2
-3
appview/pages/pages.go
···
591
591
RepoInfo RepoInfo
592
592
Active string
593
593
DidHandleMap map[string]string
594
-
595
-
Pull db.Pull
596
-
MergeCheck types.MergeCheckResponse
594
+
Pull *db.Pull
595
+
MergeCheck types.MergeCheckResponse
597
596
}
598
597
599
598
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+15
-8
appview/pages/templates/fragments/pullActions.html
+15
-8
appview/pages/templates/fragments/pullActions.html
···
9
9
{{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }}
10
10
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
11
11
{{ $isLastRound := eq $roundNumber $lastIdx }}
12
+
{{ $isSameRepoBranch := .Pull.IsSameRepoBranch }}
12
13
<div class="relative w-fit">
13
14
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
14
15
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2">
···
36
37
{{ end }}
37
38
38
39
{{ if and $isPullAuthor $isOpen $isLastRound }}
39
-
<button
40
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
41
-
hx-target="#actions-{{$roundNumber}}"
42
-
hx-swap="outerHtml"
43
-
class="btn p-2 flex items-center gap-2">
44
-
{{ i "rotate-ccw" "w-4 h-4" }}
45
-
<span>resubmit</span>
46
-
</button>
40
+
<button id="resubmitBtn"
41
+
{{ if $isSameRepoBranch }}
42
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
43
+
{{ else }}
44
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
45
+
hx-target="#actions-{{$roundNumber}}"
46
+
hx-swap="outerHtml"
47
+
{{ end }}
48
+
49
+
hx-disabled-elt="#resubmitBtn"
50
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
51
+
{{ i "rotate-ccw" "w-4 h-4" }}
52
+
<span>resubmit</span>
53
+
</button>
47
54
{{ end }}
48
55
49
56
{{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }}
+73
-39
appview/pages/templates/repo/pulls/new.html
+73
-39
appview/pages/templates/repo/pulls/new.html
···
8
8
<ul class="list-decimal pl-10 space-y-2 text-gray-700 dark:text-gray-300">
9
9
<li class="leading-relaxed">Clone this repository.</li>
10
10
<li class="leading-relaxed">Make your changes in your local repository.</li>
11
-
<li class="leading-relaxed">Grab the diff using <code class="bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono text-sm">git diff</code>.</li>
11
+
<li class="leading-relaxed">Grab the diff using <code>git diff</code>.</li>
12
12
<li class="leading-relaxed">Paste the diff output in the form below.</li>
13
13
</ul>
14
14
</p>
···
19
19
hx-swap="none"
20
20
>
21
21
<div class="flex flex-col gap-4">
22
-
<div>
23
-
<label for="title" class="dark:text-white">write a title</label>
24
-
<input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" />
22
+
<div>
23
+
<label for="title" class="dark:text-white">write a title</label>
24
+
<input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" />
25
+
</div>
25
26
26
-
<label for="targetBranch" class="dark:text-white">select a target branch</label>
27
-
<p class="text-gray-500 dark:text-gray-400">
28
-
The branch you want to make your change against.
29
-
</p>
30
-
<select
31
-
name="targetBranch"
32
-
class="p-1 mb-2 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
33
-
>
34
-
<option disabled selected>select a branch</option>
35
-
{{ range .Branches }}
36
-
<option value="{{ .Reference.Name }}" class="py-1">
37
-
{{ .Reference.Name }}
38
-
</option>
39
-
{{ end }}
40
-
</select>
41
-
<label for="body" class="dark:text-white">add a description</label>
42
-
<textarea
43
-
name="body"
44
-
id="body"
45
-
rows="6"
46
-
class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600"
47
-
placeholder="Describe your change. Markdown is supported."
27
+
<div>
28
+
<label for="body" class="dark:text-white">add a description</label>
29
+
<textarea
30
+
name="body"
31
+
id="body"
32
+
rows="6"
33
+
class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600"
34
+
placeholder="Describe your change. Markdown is supported."
48
35
></textarea>
36
+
</div>
49
37
50
-
<div class="mt-4">
51
-
<label for="patch" class="dark:text-white">paste your patch here</label>
52
-
<textarea
53
-
name="patch"
54
-
id="patch"
55
-
rows="10"
56
-
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
57
-
placeholder="Paste your git diff output here."
58
-
></textarea>
59
-
</div>
60
-
</div>
61
-
<div>
62
-
<button type="submit" class="btn dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-white">create</button>
38
+
<div>
39
+
<label for="targetBranch" class="dark:text-white">configure branches</label>
40
+
<div class="flex flex-wrap gap-2 items-center">
41
+
<select
42
+
required
43
+
name="targetBranch"
44
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
45
+
>
46
+
<option disabled selected>target branch</option>
47
+
{{ range .Branches }}
48
+
<option value="{{ .Reference.Name }}" class="py-1">
49
+
{{ .Reference.Name }}
50
+
</option>
51
+
{{ end }}
52
+
</select>
53
+
54
+
{{ if .RepoInfo.Roles.IsPushAllowed }}
55
+
{{ i "move-left" "w-5 h-5" }}
56
+
<select
57
+
name="sourceBranch"
58
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
59
+
>
60
+
<option disabled selected>source branch</option>
61
+
{{ range .Branches }}
62
+
<option value="{{ .Reference.Name }}" class="py-1">
63
+
{{ .Reference.Name }}
64
+
</option>
65
+
{{ end }}
66
+
</select>
67
+
{{ end }}
68
+
63
69
</div>
70
+
</div>
71
+
72
+
<div class="mt-4">
73
+
{{ $label := "paste your patch here" }}
74
+
{{ $rows := 10 }}
75
+
{{ if .RepoInfo.Roles.IsPushAllowed }}
76
+
{{ $label = "or paste your patch here" }}
77
+
{{ $rows = 4 }}
78
+
{{ end }}
79
+
80
+
<label for="patch" class="dark:text-white">{{ $label }}</label>
81
+
<textarea
82
+
name="patch"
83
+
id="patch"
84
+
rows="{{$rows}}"
85
+
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
86
+
placeholder="Paste your git diff output here."
87
+
></textarea>
88
+
</div>
89
+
90
+
<div class="flex justify-end items-center gap-2">
91
+
<button type="submit" class="btn">create</button>
92
+
</div>
93
+
64
94
</div>
65
95
<div id="pull" class="error dark:text-red-300"></div>
66
96
</form>
67
97
{{ end }}
98
+
99
+
{{ define "repoAfter" }}
100
+
<div id="patch-preview" class="error dark:text-red-300"></div>
101
+
{{ end }}
+9
-1
appview/pages/templates/repo/pulls/pull.html
+9
-1
appview/pages/templates/repo/pulls/pull.html
···
39
39
<span class="select-none before:content-['\00B7']"></span>
40
40
<time>{{ .Pull.Created | timeFmt }}</time>
41
41
<span class="select-none before:content-['\00B7']"></span>
42
-
<span>targeting branch
42
+
<span>
43
+
targeting
43
44
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
44
45
{{ .Pull.TargetBranch }}
45
46
</span>
46
47
</span>
48
+
{{ if .Pull.IsSameRepoBranch }}
49
+
<span>from
50
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
51
+
{{ .Pull.PullSource.Branch }}
52
+
</span>
53
+
</span>
54
+
{{ end }}
47
55
</span>
48
56
</div>
49
57
+8
-1
appview/pages/templates/repo/pulls/pulls.html
+8
-1
appview/pages/templates/repo/pulls/pulls.html
···
73
73
</span>
74
74
75
75
<span class="before:content-['·']">
76
-
targeting branch
76
+
targeting
77
77
<span class="text-xs rounded bg-gray-100 dark:bg-gray-600 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
78
78
{{ .TargetBranch }}
79
79
</span>
80
80
</span>
81
+
{{ if .IsSameRepoBranch }}
82
+
<span>from
83
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
84
+
{{ .PullSource.Branch }}
85
+
</span>
86
+
</span>
87
+
{{ end }}
81
88
</p>
82
89
</div>
83
90
{{ end }}
+103
-3
appview/state/pull.go
+103
-3
appview/state/pull.go
···
110
110
LoggedInUser: user,
111
111
RepoInfo: f.RepoInfo(s, user),
112
112
DidHandleMap: didHandleMap,
113
-
Pull: *pull,
113
+
Pull: pull,
114
114
MergeCheck: mergeCheckResponse,
115
115
})
116
116
}
···
450
450
Branches: result.Branches,
451
451
})
452
452
case http.MethodPost:
453
+
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
453
454
title := r.FormValue("title")
454
455
body := r.FormValue("body")
455
456
targetBranch := r.FormValue("targetBranch")
457
+
sourceBranch := r.FormValue("sourceBranch")
456
458
patch := r.FormValue("patch")
457
459
458
-
if title == "" || body == "" || patch == "" || targetBranch == "" {
459
-
s.pages.Notice(w, "pull", "Title, body and patch diff are required.")
460
+
if patch == "" {
461
+
if isPushAllowed && sourceBranch == "" {
462
+
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
463
+
return
464
+
}
465
+
s.pages.Notice(w, "pull", "Patch is empty.")
460
466
return
461
467
}
462
468
469
+
if patch != "" && sourceBranch != "" {
470
+
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
471
+
return
472
+
}
473
+
474
+
if title == "" || body == "" || targetBranch == "" {
475
+
s.pages.Notice(w, "pull", "Title, body and target branch are required.")
476
+
return
477
+
}
478
+
479
+
// TODO: check if knot has this capability
480
+
var pullSource *db.PullSource
481
+
if sourceBranch != "" && isPushAllowed {
482
+
pullSource = &db.PullSource{
483
+
Branch: sourceBranch,
484
+
}
485
+
// generate a patch using /compare
486
+
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
487
+
if err != nil {
488
+
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
489
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
490
+
return
491
+
}
492
+
493
+
log.Println(targetBranch, sourceBranch)
494
+
495
+
resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
496
+
switch resp.StatusCode {
497
+
case 404:
498
+
case 400:
499
+
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
500
+
}
501
+
502
+
respBody, err := io.ReadAll(resp.Body)
503
+
if err != nil {
504
+
log.Println("failed to compare across branches")
505
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
506
+
}
507
+
defer resp.Body.Close()
508
+
509
+
var diffTreeResponse types.RepoDiffTreeResponse
510
+
err = json.Unmarshal(respBody, &diffTreeResponse)
511
+
if err != nil {
512
+
log.Println("failed to unmarshal diff tree response", err)
513
+
log.Println(string(respBody))
514
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
515
+
}
516
+
517
+
patch = diffTreeResponse.DiffTree.Patch
518
+
}
519
+
520
+
log.Println(patch)
521
+
463
522
// Validate patch format
464
523
if !isPatchValid(patch) {
465
524
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
···
488
547
Submissions: []*db.PullSubmission{
489
548
&initialSubmission,
490
549
},
550
+
PullSource: pullSource,
491
551
})
492
552
if err != nil {
493
553
log.Println("failed to create pull request", err)
···
553
613
return
554
614
case http.MethodPost:
555
615
patch := r.FormValue("patch")
616
+
617
+
// this pull is a branch based pull
618
+
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
619
+
if pull.IsSameRepoBranch() && isPushAllowed {
620
+
sourceBranch := pull.PullSource.Branch
621
+
targetBranch := pull.TargetBranch
622
+
// extract patch by performing compare
623
+
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
624
+
if err != nil {
625
+
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
626
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
627
+
return
628
+
}
629
+
630
+
log.Println(targetBranch, sourceBranch)
631
+
632
+
resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
633
+
switch resp.StatusCode {
634
+
case 404:
635
+
case 400:
636
+
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
637
+
}
638
+
639
+
respBody, err := io.ReadAll(resp.Body)
640
+
if err != nil {
641
+
log.Println("failed to compare across branches")
642
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
643
+
}
644
+
defer resp.Body.Close()
645
+
646
+
var diffTreeResponse types.RepoDiffTreeResponse
647
+
err = json.Unmarshal(respBody, &diffTreeResponse)
648
+
if err != nil {
649
+
log.Println("failed to unmarshal diff tree response", err)
650
+
log.Println(string(respBody))
651
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
652
+
}
653
+
654
+
patch = diffTreeResponse.DiffTree.Patch
655
+
}
556
656
557
657
if patch == "" {
558
658
s.pages.Notice(w, "resubmit-error", "Patch is empty.")
+15
appview/state/signer.go
+15
appview/state/signer.go
···
315
315
316
316
return us.client.Do(req)
317
317
}
318
+
319
+
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*http.Response, error) {
320
+
const (
321
+
Method = "GET"
322
+
)
323
+
324
+
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, rev1, rev2)
325
+
326
+
req, err := us.newRequest(Method, endpoint, nil)
327
+
if err != nil {
328
+
return nil, err
329
+
}
330
+
331
+
return us.client.Do(req)
332
+
}
+1
cmd/gen.go
+1
cmd/gen.go
+71
-10
knotserver/git/diff.go
+71
-10
knotserver/git/diff.go
···
6
6
"strings"
7
7
8
8
"github.com/bluekeyes/go-gitdiff/gitdiff"
9
+
"github.com/go-git/go-git/v5/plumbing"
9
10
"github.com/go-git/go-git/v5/plumbing/object"
10
11
"tangled.sh/tangled.sh/core/types"
11
12
)
···
46
47
}
47
48
48
49
nd := types.NiceDiff{}
49
-
nd.Commit.This = c.Hash.String()
50
-
51
-
if parent.Hash.IsZero() {
52
-
nd.Commit.Parent = ""
53
-
} else {
54
-
nd.Commit.Parent = parent.Hash.String()
55
-
}
56
-
nd.Commit.Author = c.Author
57
-
nd.Commit.Message = c.Message
58
-
59
50
for _, d := range diffs {
60
51
ndiff := types.Diff{}
61
52
ndiff.Name.New = d.NewName
···
82
73
}
83
74
84
75
nd.Stat.FilesChanged = len(diffs)
76
+
nd.Commit.This = c.Hash.String()
77
+
78
+
if parent.Hash.IsZero() {
79
+
nd.Commit.Parent = ""
80
+
} else {
81
+
nd.Commit.Parent = parent.Hash.String()
82
+
}
83
+
nd.Commit.Author = c.Author
84
+
nd.Commit.Message = c.Message
85
85
86
86
return &nd, nil
87
87
}
88
+
89
+
func (g *GitRepo) DiffTree(rev1, rev2 string) (*types.DiffTree, error) {
90
+
commit1, err := g.resolveRevision(rev1)
91
+
if err != nil {
92
+
return nil, fmt.Errorf("Invalid revision: %s", rev1)
93
+
}
94
+
95
+
commit2, err := g.resolveRevision(rev2)
96
+
if err != nil {
97
+
return nil, fmt.Errorf("Invalid revision: %s", rev2)
98
+
}
99
+
100
+
log.Println(commit1, commit2)
101
+
102
+
tree1, err := commit1.Tree()
103
+
if err != nil {
104
+
return nil, err
105
+
}
106
+
107
+
tree2, err := commit2.Tree()
108
+
if err != nil {
109
+
return nil, err
110
+
}
111
+
112
+
diff, err := object.DiffTree(tree1, tree2)
113
+
if err != nil {
114
+
return nil, err
115
+
}
116
+
117
+
patch, err := diff.Patch()
118
+
if err != nil {
119
+
return nil, err
120
+
}
121
+
122
+
diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
123
+
if err != nil {
124
+
return nil, err
125
+
}
126
+
127
+
return &types.DiffTree{
128
+
Rev1: commit1.Hash.String(),
129
+
Rev2: commit2.Hash.String(),
130
+
Patch: patch.String(),
131
+
Diff: diffs,
132
+
}, nil
133
+
}
134
+
135
+
func (g *GitRepo) resolveRevision(revStr string) (*object.Commit, error) {
136
+
rev, err := g.r.ResolveRevision(plumbing.Revision(revStr))
137
+
if err != nil {
138
+
return nil, fmt.Errorf("resolving revision %s: %w", revStr, err)
139
+
}
140
+
141
+
commit, err := g.r.CommitObject(*rev)
142
+
if err != nil {
143
+
144
+
return nil, fmt.Errorf("getting commit for %s: %w", revStr, err)
145
+
}
146
+
147
+
return commit, nil
148
+
}
+10
knotserver/git/git.go
+10
knotserver/git/git.go
···
131
131
return &g, nil
132
132
}
133
133
134
+
func PlainOpen(path string) (*GitRepo, error) {
135
+
var err error
136
+
g := GitRepo{path: path}
137
+
g.r, err = git.PlainOpen(path)
138
+
if err != nil {
139
+
return nil, fmt.Errorf("opening %s: %w", path, err)
140
+
}
141
+
return &g, nil
142
+
}
143
+
134
144
func (g *GitRepo) Commits() ([]*object.Commit, error) {
135
145
ci, err := g.r.Log(&git.LogOptions{From: g.h})
136
146
if err != nil {
+1
knotserver/handler.go
+1
knotserver/handler.go
+30
-1
knotserver/routes.go
+30
-1
knotserver/routes.go
···
36
36
37
37
capabilities := map[string]any{
38
38
"pull_requests": map[string]any{
39
-
"patch_submissions": true,
39
+
"patch_submissions": true,
40
+
"branch_submissions": true,
41
+
"fork_submissions": false,
40
42
},
41
43
}
42
44
···
681
683
}
682
684
writeError(w, err.Error(), http.StatusInternalServerError)
683
685
h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
686
+
}
687
+
688
+
func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
689
+
rev1 := chi.URLParam(r, "rev1")
690
+
rev1, _ = url.PathUnescape(rev1)
691
+
692
+
rev2 := chi.URLParam(r, "rev2")
693
+
rev2, _ = url.PathUnescape(rev2)
694
+
695
+
l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
696
+
697
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
698
+
gr, err := git.PlainOpen(path)
699
+
if err != nil {
700
+
notFound(w)
701
+
return
702
+
}
703
+
704
+
difftree, err := gr.DiffTree(rev1, rev2)
705
+
if err != nil {
706
+
l.Error("error comparing revisions", "msg", err.Error())
707
+
writeError(w, "error comparing revisions", http.StatusBadRequest)
708
+
return
709
+
}
710
+
711
+
writeJSON(w, types.RepoDiffTreeResponse{difftree})
712
+
return
684
713
}
685
714
686
715
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
+24
-5
lexicons/pulls/pull.json
+24
-5
lexicons/pulls/pull.json
···
9
9
"key": "tid",
10
10
"record": {
11
11
"type": "object",
12
-
"required": ["targetRepo", "targetBranch", "pullId", "title", "patch"],
12
+
"required": [
13
+
"targetRepo",
14
+
"targetBranch",
15
+
"pullId",
16
+
"title",
17
+
"patch"
18
+
],
13
19
"properties": {
14
20
"targetRepo": {
15
21
"type": "string",
···
18
24
"targetBranch": {
19
25
"type": "string"
20
26
},
21
-
"sourceRepo": {
22
-
"type": "string",
23
-
"format": "at-uri"
24
-
},
25
27
"pullId": {
26
28
"type": "integer"
27
29
},
···
37
39
},
38
40
"patch": {
39
41
"type": "string"
42
+
},
43
+
"source": {
44
+
"type": "ref",
45
+
"ref": "#source"
40
46
}
47
+
}
48
+
}
49
+
},
50
+
"source": {
51
+
"type": "object",
52
+
"required": ["branch"],
53
+
"properties": {
54
+
"branch": {
55
+
"type": "string"
56
+
},
57
+
"repo": {
58
+
"type": "string",
59
+
"format": "at-uri"
41
60
}
42
61
}
43
62
}
+7
types/diff.go
+7
types/diff.go
+4
types/repo.go
+4
types/repo.go