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