+28
.tangled/workflows/build.yml
+28
.tangled/workflows/build.yml
···
···
1
+
when:
2
+
- event: ["push", "pull_request"]
3
+
branch: ["master"]
4
+
5
+
dependencies:
6
+
nixpkgs:
7
+
- go
8
+
- gcc
9
+
10
+
environment:
11
+
CGO_ENABLED: 1
12
+
13
+
steps:
14
+
- name: patch static dir
15
+
command: |
16
+
mkdir -p appview/pages/static; touch appview/pages/static/x
17
+
18
+
- name: build appview
19
+
command: |
20
+
go build -o appview.out ./cmd/appview
21
+
22
+
- name: build knot
23
+
command: |
24
+
go build -o knot.out ./cmd/knot
25
+
26
+
- name: build spindle
27
+
command: |
28
+
go build -o spindle.out ./cmd/spindle
+18
.tangled/workflows/fmt.yml
+18
.tangled/workflows/fmt.yml
+19
.tangled/workflows/test.yml
+19
.tangled/workflows/test.yml
···
···
1
+
when:
2
+
- event: ["push", "pull_request"]
3
+
branch: ["master"]
4
+
5
+
dependencies:
6
+
nixpkgs:
7
+
- go
8
+
- gcc
9
+
10
+
steps:
11
+
- name: patch static dir
12
+
command: |
13
+
mkdir -p appview/pages/static; touch appview/pages/static/x
14
+
15
+
- name: run all tests
16
+
environment:
17
+
CGO_ENABLED: 1
18
+
command: |
19
+
go test -v ./...
+269
-297
api/tangled/cbor_gen.go
+269
-297
api/tangled/cbor_gen.go
···
504
505
return nil
506
}
507
func (t *FeedStar) MarshalCBOR(w io.Writer) error {
508
if t == nil {
509
_, err := w.Write(cbg.CborNull)
···
2188
2189
return nil
2190
}
2191
-
func (t *Pipeline_Dependencies_Elem) MarshalCBOR(w io.Writer) error {
2192
if t == nil {
2193
_, err := w.Write(cbg.CborNull)
2194
return err
···
2258
return nil
2259
}
2260
2261
-
func (t *Pipeline_Dependencies_Elem) UnmarshalCBOR(r io.Reader) (err error) {
2262
-
*t = Pipeline_Dependencies_Elem{}
2263
2264
cr := cbg.NewCborReader(r)
2265
···
2278
}
2279
2280
if extra > cbg.MaxLength {
2281
-
return fmt.Errorf("Pipeline_Dependencies_Elem: map struct too large (%d)", extra)
2282
}
2283
2284
n := extra
···
2378
return err
2379
}
2380
2381
-
// t.Inputs ([]*tangled.Pipeline_ManualTriggerData_Inputs_Elem) (slice)
2382
if t.Inputs != nil {
2383
2384
if len("inputs") > 1000000 {
···
2450
}
2451
2452
switch string(nameBuf[:nameLen]) {
2453
-
// t.Inputs ([]*tangled.Pipeline_ManualTriggerData_Inputs_Elem) (slice)
2454
case "inputs":
2455
2456
maj, extra, err = cr.ReadHeader()
···
2467
}
2468
2469
if extra > 0 {
2470
-
t.Inputs = make([]*Pipeline_ManualTriggerData_Inputs_Elem, extra)
2471
}
2472
2473
for i := 0; i < int(extra); i++ {
···
2489
if err := cr.UnreadByte(); err != nil {
2490
return err
2491
}
2492
-
t.Inputs[i] = new(Pipeline_ManualTriggerData_Inputs_Elem)
2493
if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil {
2494
return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err)
2495
}
···
2510
2511
return nil
2512
}
2513
-
func (t *Pipeline_ManualTriggerData_Inputs_Elem) MarshalCBOR(w io.Writer) error {
2514
if t == nil {
2515
_, err := w.Write(cbg.CborNull)
2516
return err
···
2570
return nil
2571
}
2572
2573
-
func (t *Pipeline_ManualTriggerData_Inputs_Elem) UnmarshalCBOR(r io.Reader) (err error) {
2574
-
*t = Pipeline_ManualTriggerData_Inputs_Elem{}
2575
2576
cr := cbg.NewCborReader(r)
2577
···
2590
}
2591
2592
if extra > cbg.MaxLength {
2593
-
return fmt.Errorf("Pipeline_ManualTriggerData_Inputs_Elem: map struct too large (%d)", extra)
2594
}
2595
2596
n := extra
···
3014
3015
return nil
3016
}
3017
-
3018
-
func (t *Pipeline_Step_Environment_Elem) MarshalCBOR(w io.Writer) error {
3019
-
if t == nil {
3020
-
_, err := w.Write(cbg.CborNull)
3021
-
return err
3022
-
}
3023
-
3024
-
cw := cbg.NewCborWriter(w)
3025
-
3026
-
if _, err := cw.Write([]byte{162}); err != nil {
3027
-
return err
3028
-
}
3029
-
3030
-
// t.Key (string) (string)
3031
-
if len("key") > 1000000 {
3032
-
return xerrors.Errorf("Value in field \"key\" was too long")
3033
-
}
3034
-
3035
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil {
3036
-
return err
3037
-
}
3038
-
if _, err := cw.WriteString(string("key")); err != nil {
3039
-
return err
3040
-
}
3041
-
3042
-
if len(t.Key) > 1000000 {
3043
-
return xerrors.Errorf("Value in field t.Key was too long")
3044
-
}
3045
-
3046
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil {
3047
-
return err
3048
-
}
3049
-
if _, err := cw.WriteString(string(t.Key)); err != nil {
3050
-
return err
3051
-
}
3052
-
3053
-
// t.Value (string) (string)
3054
-
if len("value") > 1000000 {
3055
-
return xerrors.Errorf("Value in field \"value\" was too long")
3056
-
}
3057
-
3058
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("value"))); err != nil {
3059
-
return err
3060
-
}
3061
-
if _, err := cw.WriteString(string("value")); err != nil {
3062
-
return err
3063
-
}
3064
-
3065
-
if len(t.Value) > 1000000 {
3066
-
return xerrors.Errorf("Value in field t.Value was too long")
3067
-
}
3068
-
3069
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Value))); err != nil {
3070
-
return err
3071
-
}
3072
-
if _, err := cw.WriteString(string(t.Value)); err != nil {
3073
-
return err
3074
-
}
3075
-
return nil
3076
-
}
3077
-
3078
-
func (t *Pipeline_Step_Environment_Elem) UnmarshalCBOR(r io.Reader) (err error) {
3079
-
*t = Pipeline_Step_Environment_Elem{}
3080
-
3081
-
cr := cbg.NewCborReader(r)
3082
-
3083
-
maj, extra, err := cr.ReadHeader()
3084
-
if err != nil {
3085
-
return err
3086
-
}
3087
-
defer func() {
3088
-
if err == io.EOF {
3089
-
err = io.ErrUnexpectedEOF
3090
-
}
3091
-
}()
3092
-
3093
-
if maj != cbg.MajMap {
3094
-
return fmt.Errorf("cbor input should be of type map")
3095
-
}
3096
-
3097
-
if extra > cbg.MaxLength {
3098
-
return fmt.Errorf("Pipeline_Step_Environment_Elem: map struct too large (%d)", extra)
3099
-
}
3100
-
3101
-
n := extra
3102
-
3103
-
nameBuf := make([]byte, 5)
3104
-
for i := uint64(0); i < n; i++ {
3105
-
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
3106
-
if err != nil {
3107
-
return err
3108
-
}
3109
-
3110
-
if !ok {
3111
-
// Field doesn't exist on this type, so ignore it
3112
-
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
3113
-
return err
3114
-
}
3115
-
continue
3116
-
}
3117
-
3118
-
switch string(nameBuf[:nameLen]) {
3119
-
// t.Key (string) (string)
3120
-
case "key":
3121
-
3122
-
{
3123
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3124
-
if err != nil {
3125
-
return err
3126
-
}
3127
-
3128
-
t.Key = string(sval)
3129
-
}
3130
-
// t.Value (string) (string)
3131
-
case "value":
3132
-
3133
-
{
3134
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3135
-
if err != nil {
3136
-
return err
3137
-
}
3138
-
3139
-
t.Value = string(sval)
3140
-
}
3141
-
3142
-
default:
3143
-
// Field doesn't exist on this type, so ignore it
3144
-
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
3145
-
return err
3146
-
}
3147
-
}
3148
-
}
3149
-
3150
-
return nil
3151
-
}
3152
func (t *PipelineStatus) MarshalCBOR(w io.Writer) error {
3153
if t == nil {
3154
_, err := w.Write(cbg.CborNull)
···
3511
3512
return nil
3513
}
3514
-
3515
func (t *Pipeline_Step) MarshalCBOR(w io.Writer) error {
3516
if t == nil {
3517
_, err := w.Write(cbg.CborNull)
···
3575
return err
3576
}
3577
3578
-
// t.Environment ([]*tangled.Pipeline_Step_Environment_Elem) (slice)
3579
if t.Environment != nil {
3580
3581
if len("environment") > 1000000 {
···
3669
3670
t.Command = string(sval)
3671
}
3672
-
// t.Environment ([]*tangled.Pipeline_Step_Environment_Elem) (slice)
3673
case "environment":
3674
3675
maj, extra, err = cr.ReadHeader()
···
3686
}
3687
3688
if extra > 0 {
3689
-
t.Environment = make([]*Pipeline_Step_Environment_Elem, extra)
3690
}
3691
3692
for i := 0; i < int(extra); i++ {
···
3708
if err := cr.UnreadByte(); err != nil {
3709
return err
3710
}
3711
-
t.Environment[i] = new(Pipeline_Step_Environment_Elem)
3712
if err := t.Environment[i].UnmarshalCBOR(cr); err != nil {
3713
return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err)
3714
}
···
4274
4275
}
4276
4277
-
// t.Environment ([]*tangled.Pipeline_Workflow_Environment_Elem) (slice)
4278
if len("environment") > 1000000 {
4279
return xerrors.Errorf("Value in field \"environment\" was too long")
4280
}
···
4300
4301
}
4302
4303
-
// t.Dependencies ([]tangled.Pipeline_Dependencies_Elem) (slice)
4304
if len("dependencies") > 1000000 {
4305
return xerrors.Errorf("Value in field \"dependencies\" was too long")
4306
}
···
4449
4450
}
4451
}
4452
-
// t.Environment ([]*tangled.Pipeline_Workflow_Environment_Elem) (slice)
4453
case "environment":
4454
4455
maj, extra, err = cr.ReadHeader()
···
4466
}
4467
4468
if extra > 0 {
4469
-
t.Environment = make([]*Pipeline_Workflow_Environment_Elem, extra)
4470
}
4471
4472
for i := 0; i < int(extra); i++ {
···
4488
if err := cr.UnreadByte(); err != nil {
4489
return err
4490
}
4491
-
t.Environment[i] = new(Pipeline_Workflow_Environment_Elem)
4492
if err := t.Environment[i].UnmarshalCBOR(cr); err != nil {
4493
return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err)
4494
}
···
4498
4499
}
4500
}
4501
-
// t.Dependencies ([]tangled.Pipeline_Dependencies_Elem) (slice)
4502
case "dependencies":
4503
4504
maj, extra, err = cr.ReadHeader()
···
4515
}
4516
4517
if extra > 0 {
4518
-
t.Dependencies = make([]Pipeline_Dependencies_Elem, extra)
4519
}
4520
4521
for i := 0; i < int(extra); i++ {
···
4529
4530
{
4531
4532
-
if err := t.Dependencies[i].UnmarshalCBOR(cr); err != nil {
4533
-
return xerrors.Errorf("unmarshaling t.Dependencies[i]: %w", err)
4534
}
4535
4536
}
4537
4538
}
4539
-
}
4540
-
4541
-
default:
4542
-
// Field doesn't exist on this type, so ignore it
4543
-
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
4544
-
return err
4545
-
}
4546
-
}
4547
-
}
4548
-
4549
-
return nil
4550
-
}
4551
-
func (t *Pipeline_Workflow_Environment_Elem) MarshalCBOR(w io.Writer) error {
4552
-
if t == nil {
4553
-
_, err := w.Write(cbg.CborNull)
4554
-
return err
4555
-
}
4556
-
4557
-
cw := cbg.NewCborWriter(w)
4558
-
4559
-
if _, err := cw.Write([]byte{162}); err != nil {
4560
-
return err
4561
-
}
4562
-
4563
-
// t.Key (string) (string)
4564
-
if len("key") > 1000000 {
4565
-
return xerrors.Errorf("Value in field \"key\" was too long")
4566
-
}
4567
-
4568
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil {
4569
-
return err
4570
-
}
4571
-
if _, err := cw.WriteString(string("key")); err != nil {
4572
-
return err
4573
-
}
4574
-
4575
-
if len(t.Key) > 1000000 {
4576
-
return xerrors.Errorf("Value in field t.Key was too long")
4577
-
}
4578
-
4579
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil {
4580
-
return err
4581
-
}
4582
-
if _, err := cw.WriteString(string(t.Key)); err != nil {
4583
-
return err
4584
-
}
4585
-
4586
-
// t.Value (string) (string)
4587
-
if len("value") > 1000000 {
4588
-
return xerrors.Errorf("Value in field \"value\" was too long")
4589
-
}
4590
-
4591
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("value"))); err != nil {
4592
-
return err
4593
-
}
4594
-
if _, err := cw.WriteString(string("value")); err != nil {
4595
-
return err
4596
-
}
4597
-
4598
-
if len(t.Value) > 1000000 {
4599
-
return xerrors.Errorf("Value in field t.Value was too long")
4600
-
}
4601
-
4602
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Value))); err != nil {
4603
-
return err
4604
-
}
4605
-
if _, err := cw.WriteString(string(t.Value)); err != nil {
4606
-
return err
4607
-
}
4608
-
return nil
4609
-
}
4610
-
4611
-
func (t *Pipeline_Workflow_Environment_Elem) UnmarshalCBOR(r io.Reader) (err error) {
4612
-
*t = Pipeline_Workflow_Environment_Elem{}
4613
-
4614
-
cr := cbg.NewCborReader(r)
4615
-
4616
-
maj, extra, err := cr.ReadHeader()
4617
-
if err != nil {
4618
-
return err
4619
-
}
4620
-
defer func() {
4621
-
if err == io.EOF {
4622
-
err = io.ErrUnexpectedEOF
4623
-
}
4624
-
}()
4625
-
4626
-
if maj != cbg.MajMap {
4627
-
return fmt.Errorf("cbor input should be of type map")
4628
-
}
4629
-
4630
-
if extra > cbg.MaxLength {
4631
-
return fmt.Errorf("Pipeline_Workflow_Environment_Elem: map struct too large (%d)", extra)
4632
-
}
4633
-
4634
-
n := extra
4635
-
4636
-
nameBuf := make([]byte, 5)
4637
-
for i := uint64(0); i < n; i++ {
4638
-
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
4639
-
if err != nil {
4640
-
return err
4641
-
}
4642
-
4643
-
if !ok {
4644
-
// Field doesn't exist on this type, so ignore it
4645
-
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
4646
-
return err
4647
-
}
4648
-
continue
4649
-
}
4650
-
4651
-
switch string(nameBuf[:nameLen]) {
4652
-
// t.Key (string) (string)
4653
-
case "key":
4654
-
4655
-
{
4656
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
4657
-
if err != nil {
4658
-
return err
4659
-
}
4660
-
4661
-
t.Key = string(sval)
4662
-
}
4663
-
// t.Value (string) (string)
4664
-
case "value":
4665
-
4666
-
{
4667
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
4668
-
if err != nil {
4669
-
return err
4670
-
}
4671
-
4672
-
t.Value = string(sval)
4673
}
4674
4675
default:
···
7268
}
7269
7270
cw := cbg.NewCborWriter(w)
7271
-
fieldCount := 2
7272
7273
if t.Repo == nil {
7274
fieldCount--
7275
}
7276
7277
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
7278
return err
7279
}
7280
···
7376
}
7377
7378
switch string(nameBuf[:nameLen]) {
7379
-
// t.Repo (string) (string)
7380
case "repo":
7381
7382
{
···
504
505
return nil
506
}
507
+
func (t *FeedReaction) MarshalCBOR(w io.Writer) error {
508
+
if t == nil {
509
+
_, err := w.Write(cbg.CborNull)
510
+
return err
511
+
}
512
+
513
+
cw := cbg.NewCborWriter(w)
514
+
515
+
if _, err := cw.Write([]byte{164}); err != nil {
516
+
return err
517
+
}
518
+
519
+
// t.LexiconTypeID (string) (string)
520
+
if len("$type") > 1000000 {
521
+
return xerrors.Errorf("Value in field \"$type\" was too long")
522
+
}
523
+
524
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
525
+
return err
526
+
}
527
+
if _, err := cw.WriteString(string("$type")); err != nil {
528
+
return err
529
+
}
530
+
531
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.feed.reaction"))); err != nil {
532
+
return err
533
+
}
534
+
if _, err := cw.WriteString(string("sh.tangled.feed.reaction")); err != nil {
535
+
return err
536
+
}
537
+
538
+
// t.Subject (string) (string)
539
+
if len("subject") > 1000000 {
540
+
return xerrors.Errorf("Value in field \"subject\" was too long")
541
+
}
542
+
543
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil {
544
+
return err
545
+
}
546
+
if _, err := cw.WriteString(string("subject")); err != nil {
547
+
return err
548
+
}
549
+
550
+
if len(t.Subject) > 1000000 {
551
+
return xerrors.Errorf("Value in field t.Subject was too long")
552
+
}
553
+
554
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil {
555
+
return err
556
+
}
557
+
if _, err := cw.WriteString(string(t.Subject)); err != nil {
558
+
return err
559
+
}
560
+
561
+
// t.Reaction (string) (string)
562
+
if len("reaction") > 1000000 {
563
+
return xerrors.Errorf("Value in field \"reaction\" was too long")
564
+
}
565
+
566
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reaction"))); err != nil {
567
+
return err
568
+
}
569
+
if _, err := cw.WriteString(string("reaction")); err != nil {
570
+
return err
571
+
}
572
+
573
+
if len(t.Reaction) > 1000000 {
574
+
return xerrors.Errorf("Value in field t.Reaction was too long")
575
+
}
576
+
577
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Reaction))); err != nil {
578
+
return err
579
+
}
580
+
if _, err := cw.WriteString(string(t.Reaction)); err != nil {
581
+
return err
582
+
}
583
+
584
+
// t.CreatedAt (string) (string)
585
+
if len("createdAt") > 1000000 {
586
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
587
+
}
588
+
589
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
590
+
return err
591
+
}
592
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
593
+
return err
594
+
}
595
+
596
+
if len(t.CreatedAt) > 1000000 {
597
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
598
+
}
599
+
600
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
601
+
return err
602
+
}
603
+
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
604
+
return err
605
+
}
606
+
return nil
607
+
}
608
+
609
+
func (t *FeedReaction) UnmarshalCBOR(r io.Reader) (err error) {
610
+
*t = FeedReaction{}
611
+
612
+
cr := cbg.NewCborReader(r)
613
+
614
+
maj, extra, err := cr.ReadHeader()
615
+
if err != nil {
616
+
return err
617
+
}
618
+
defer func() {
619
+
if err == io.EOF {
620
+
err = io.ErrUnexpectedEOF
621
+
}
622
+
}()
623
+
624
+
if maj != cbg.MajMap {
625
+
return fmt.Errorf("cbor input should be of type map")
626
+
}
627
+
628
+
if extra > cbg.MaxLength {
629
+
return fmt.Errorf("FeedReaction: map struct too large (%d)", extra)
630
+
}
631
+
632
+
n := extra
633
+
634
+
nameBuf := make([]byte, 9)
635
+
for i := uint64(0); i < n; i++ {
636
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
637
+
if err != nil {
638
+
return err
639
+
}
640
+
641
+
if !ok {
642
+
// Field doesn't exist on this type, so ignore it
643
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
644
+
return err
645
+
}
646
+
continue
647
+
}
648
+
649
+
switch string(nameBuf[:nameLen]) {
650
+
// t.LexiconTypeID (string) (string)
651
+
case "$type":
652
+
653
+
{
654
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
655
+
if err != nil {
656
+
return err
657
+
}
658
+
659
+
t.LexiconTypeID = string(sval)
660
+
}
661
+
// t.Subject (string) (string)
662
+
case "subject":
663
+
664
+
{
665
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
666
+
if err != nil {
667
+
return err
668
+
}
669
+
670
+
t.Subject = string(sval)
671
+
}
672
+
// t.Reaction (string) (string)
673
+
case "reaction":
674
+
675
+
{
676
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
677
+
if err != nil {
678
+
return err
679
+
}
680
+
681
+
t.Reaction = string(sval)
682
+
}
683
+
// t.CreatedAt (string) (string)
684
+
case "createdAt":
685
+
686
+
{
687
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
688
+
if err != nil {
689
+
return err
690
+
}
691
+
692
+
t.CreatedAt = string(sval)
693
+
}
694
+
695
+
default:
696
+
// Field doesn't exist on this type, so ignore it
697
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
698
+
return err
699
+
}
700
+
}
701
+
}
702
+
703
+
return nil
704
+
}
705
func (t *FeedStar) MarshalCBOR(w io.Writer) error {
706
if t == nil {
707
_, err := w.Write(cbg.CborNull)
···
2386
2387
return nil
2388
}
2389
+
func (t *Pipeline_Dependency) MarshalCBOR(w io.Writer) error {
2390
if t == nil {
2391
_, err := w.Write(cbg.CborNull)
2392
return err
···
2456
return nil
2457
}
2458
2459
+
func (t *Pipeline_Dependency) UnmarshalCBOR(r io.Reader) (err error) {
2460
+
*t = Pipeline_Dependency{}
2461
2462
cr := cbg.NewCborReader(r)
2463
···
2476
}
2477
2478
if extra > cbg.MaxLength {
2479
+
return fmt.Errorf("Pipeline_Dependency: map struct too large (%d)", extra)
2480
}
2481
2482
n := extra
···
2576
return err
2577
}
2578
2579
+
// t.Inputs ([]*tangled.Pipeline_Pair) (slice)
2580
if t.Inputs != nil {
2581
2582
if len("inputs") > 1000000 {
···
2648
}
2649
2650
switch string(nameBuf[:nameLen]) {
2651
+
// t.Inputs ([]*tangled.Pipeline_Pair) (slice)
2652
case "inputs":
2653
2654
maj, extra, err = cr.ReadHeader()
···
2665
}
2666
2667
if extra > 0 {
2668
+
t.Inputs = make([]*Pipeline_Pair, extra)
2669
}
2670
2671
for i := 0; i < int(extra); i++ {
···
2687
if err := cr.UnreadByte(); err != nil {
2688
return err
2689
}
2690
+
t.Inputs[i] = new(Pipeline_Pair)
2691
if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil {
2692
return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err)
2693
}
···
2708
2709
return nil
2710
}
2711
+
func (t *Pipeline_Pair) MarshalCBOR(w io.Writer) error {
2712
if t == nil {
2713
_, err := w.Write(cbg.CborNull)
2714
return err
···
2768
return nil
2769
}
2770
2771
+
func (t *Pipeline_Pair) UnmarshalCBOR(r io.Reader) (err error) {
2772
+
*t = Pipeline_Pair{}
2773
2774
cr := cbg.NewCborReader(r)
2775
···
2788
}
2789
2790
if extra > cbg.MaxLength {
2791
+
return fmt.Errorf("Pipeline_Pair: map struct too large (%d)", extra)
2792
}
2793
2794
n := extra
···
3212
3213
return nil
3214
}
3215
func (t *PipelineStatus) MarshalCBOR(w io.Writer) error {
3216
if t == nil {
3217
_, err := w.Write(cbg.CborNull)
···
3574
3575
return nil
3576
}
3577
func (t *Pipeline_Step) MarshalCBOR(w io.Writer) error {
3578
if t == nil {
3579
_, err := w.Write(cbg.CborNull)
···
3637
return err
3638
}
3639
3640
+
// t.Environment ([]*tangled.Pipeline_Pair) (slice)
3641
if t.Environment != nil {
3642
3643
if len("environment") > 1000000 {
···
3731
3732
t.Command = string(sval)
3733
}
3734
+
// t.Environment ([]*tangled.Pipeline_Pair) (slice)
3735
case "environment":
3736
3737
maj, extra, err = cr.ReadHeader()
···
3748
}
3749
3750
if extra > 0 {
3751
+
t.Environment = make([]*Pipeline_Pair, extra)
3752
}
3753
3754
for i := 0; i < int(extra); i++ {
···
3770
if err := cr.UnreadByte(); err != nil {
3771
return err
3772
}
3773
+
t.Environment[i] = new(Pipeline_Pair)
3774
if err := t.Environment[i].UnmarshalCBOR(cr); err != nil {
3775
return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err)
3776
}
···
4336
4337
}
4338
4339
+
// t.Environment ([]*tangled.Pipeline_Pair) (slice)
4340
if len("environment") > 1000000 {
4341
return xerrors.Errorf("Value in field \"environment\" was too long")
4342
}
···
4362
4363
}
4364
4365
+
// t.Dependencies ([]*tangled.Pipeline_Dependency) (slice)
4366
if len("dependencies") > 1000000 {
4367
return xerrors.Errorf("Value in field \"dependencies\" was too long")
4368
}
···
4511
4512
}
4513
}
4514
+
// t.Environment ([]*tangled.Pipeline_Pair) (slice)
4515
case "environment":
4516
4517
maj, extra, err = cr.ReadHeader()
···
4528
}
4529
4530
if extra > 0 {
4531
+
t.Environment = make([]*Pipeline_Pair, extra)
4532
}
4533
4534
for i := 0; i < int(extra); i++ {
···
4550
if err := cr.UnreadByte(); err != nil {
4551
return err
4552
}
4553
+
t.Environment[i] = new(Pipeline_Pair)
4554
if err := t.Environment[i].UnmarshalCBOR(cr); err != nil {
4555
return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err)
4556
}
···
4560
4561
}
4562
}
4563
+
// t.Dependencies ([]*tangled.Pipeline_Dependency) (slice)
4564
case "dependencies":
4565
4566
maj, extra, err = cr.ReadHeader()
···
4577
}
4578
4579
if extra > 0 {
4580
+
t.Dependencies = make([]*Pipeline_Dependency, extra)
4581
}
4582
4583
for i := 0; i < int(extra); i++ {
···
4591
4592
{
4593
4594
+
b, err := cr.ReadByte()
4595
+
if err != nil {
4596
+
return err
4597
+
}
4598
+
if b != cbg.CborNull[0] {
4599
+
if err := cr.UnreadByte(); err != nil {
4600
+
return err
4601
+
}
4602
+
t.Dependencies[i] = new(Pipeline_Dependency)
4603
+
if err := t.Dependencies[i].UnmarshalCBOR(cr); err != nil {
4604
+
return xerrors.Errorf("unmarshaling t.Dependencies[i] pointer: %w", err)
4605
+
}
4606
}
4607
4608
}
4609
4610
}
4611
}
4612
4613
default:
···
7206
}
7207
7208
cw := cbg.NewCborWriter(w)
7209
+
fieldCount := 3
7210
7211
if t.Repo == nil {
7212
fieldCount--
7213
}
7214
7215
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
7216
+
return err
7217
+
}
7218
+
7219
+
// t.Sha (string) (string)
7220
+
if len("sha") > 1000000 {
7221
+
return xerrors.Errorf("Value in field \"sha\" was too long")
7222
+
}
7223
+
7224
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sha"))); err != nil {
7225
+
return err
7226
+
}
7227
+
if _, err := cw.WriteString(string("sha")); err != nil {
7228
+
return err
7229
+
}
7230
+
7231
+
if len(t.Sha) > 1000000 {
7232
+
return xerrors.Errorf("Value in field t.Sha was too long")
7233
+
}
7234
+
7235
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Sha))); err != nil {
7236
+
return err
7237
+
}
7238
+
if _, err := cw.WriteString(string(t.Sha)); err != nil {
7239
return err
7240
}
7241
···
7337
}
7338
7339
switch string(nameBuf[:nameLen]) {
7340
+
// t.Sha (string) (string)
7341
+
case "sha":
7342
+
7343
+
{
7344
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7345
+
if err != nil {
7346
+
return err
7347
+
}
7348
+
7349
+
t.Sha = string(sval)
7350
+
}
7351
+
// t.Repo (string) (string)
7352
case "repo":
7353
7354
{
+24
api/tangled/feedreaction.go
+24
api/tangled/feedreaction.go
···
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.feed.reaction
6
+
7
+
import (
8
+
"github.com/bluesky-social/indigo/lex/util"
9
+
)
10
+
11
+
const (
12
+
FeedReactionNSID = "sh.tangled.feed.reaction"
13
+
)
14
+
15
+
func init() {
16
+
util.RegisterType("sh.tangled.feed.reaction", &FeedReaction{})
17
+
} //
18
+
// RECORDTYPE: FeedReaction
19
+
type FeedReaction struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.feed.reaction" cborgen:"$type,const=sh.tangled.feed.reaction"`
21
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
22
+
Reaction string `json:"reaction" cborgen:"reaction"`
23
+
Subject string `json:"subject" cborgen:"subject"`
24
+
}
+1
api/tangled/repopull.go
+1
api/tangled/repopull.go
+13
-21
api/tangled/tangledpipeline.go
+13
-21
api/tangled/tangledpipeline.go
···
29
Submodules bool `json:"submodules" cborgen:"submodules"`
30
}
31
32
-
type Pipeline_Dependencies_Elem struct {
33
Packages []string `json:"packages" cborgen:"packages"`
34
Registry string `json:"registry" cborgen:"registry"`
35
}
36
37
// Pipeline_ManualTriggerData is a "manualTriggerData" in the sh.tangled.pipeline schema.
38
type Pipeline_ManualTriggerData struct {
39
-
Inputs []*Pipeline_ManualTriggerData_Inputs_Elem `json:"inputs,omitempty" cborgen:"inputs,omitempty"`
40
}
41
42
-
type Pipeline_ManualTriggerData_Inputs_Elem struct {
43
Key string `json:"key" cborgen:"key"`
44
Value string `json:"value" cborgen:"value"`
45
}
···
61
62
// Pipeline_Step is a "step" in the sh.tangled.pipeline schema.
63
type Pipeline_Step struct {
64
-
Command string `json:"command" cborgen:"command"`
65
-
Environment []*Pipeline_Step_Environment_Elem `json:"environment,omitempty" cborgen:"environment,omitempty"`
66
-
Name string `json:"name" cborgen:"name"`
67
-
}
68
-
69
-
type Pipeline_Step_Environment_Elem struct {
70
-
Key string `json:"key" cborgen:"key"`
71
-
Value string `json:"value" cborgen:"value"`
72
}
73
74
// Pipeline_TriggerMetadata is a "triggerMetadata" in the sh.tangled.pipeline schema.
···
90
91
// Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema.
92
type Pipeline_Workflow struct {
93
-
Clone *Pipeline_CloneOpts `json:"clone" cborgen:"clone"`
94
-
Dependencies []Pipeline_Dependencies_Elem `json:"dependencies" cborgen:"dependencies"`
95
-
Environment []*Pipeline_Workflow_Environment_Elem `json:"environment" cborgen:"environment"`
96
-
Name string `json:"name" cborgen:"name"`
97
-
Steps []*Pipeline_Step `json:"steps" cborgen:"steps"`
98
-
}
99
-
100
-
type Pipeline_Workflow_Environment_Elem struct {
101
-
Key string `json:"key" cborgen:"key"`
102
-
Value string `json:"value" cborgen:"value"`
103
}
···
29
Submodules bool `json:"submodules" cborgen:"submodules"`
30
}
31
32
+
// Pipeline_Dependency is a "dependency" in the sh.tangled.pipeline schema.
33
+
type Pipeline_Dependency struct {
34
Packages []string `json:"packages" cborgen:"packages"`
35
Registry string `json:"registry" cborgen:"registry"`
36
}
37
38
// Pipeline_ManualTriggerData is a "manualTriggerData" in the sh.tangled.pipeline schema.
39
type Pipeline_ManualTriggerData struct {
40
+
Inputs []*Pipeline_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"`
41
}
42
43
+
// Pipeline_Pair is a "pair" in the sh.tangled.pipeline schema.
44
+
type Pipeline_Pair struct {
45
Key string `json:"key" cborgen:"key"`
46
Value string `json:"value" cborgen:"value"`
47
}
···
63
64
// Pipeline_Step is a "step" in the sh.tangled.pipeline schema.
65
type Pipeline_Step struct {
66
+
Command string `json:"command" cborgen:"command"`
67
+
Environment []*Pipeline_Pair `json:"environment,omitempty" cborgen:"environment,omitempty"`
68
+
Name string `json:"name" cborgen:"name"`
69
}
70
71
// Pipeline_TriggerMetadata is a "triggerMetadata" in the sh.tangled.pipeline schema.
···
87
88
// Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema.
89
type Pipeline_Workflow struct {
90
+
Clone *Pipeline_CloneOpts `json:"clone" cborgen:"clone"`
91
+
Dependencies []*Pipeline_Dependency `json:"dependencies" cborgen:"dependencies"`
92
+
Environment []*Pipeline_Pair `json:"environment" cborgen:"environment"`
93
+
Name string `json:"name" cborgen:"name"`
94
+
Steps []*Pipeline_Step `json:"steps" cborgen:"steps"`
95
}
+1
-1
appview/db/artifact.go
+1
-1
appview/db/artifact.go
+10
appview/db/db.go
+10
appview/db/db.go
···
199
unique(starred_by_did, repo_at)
200
);
201
202
+
create table if not exists reactions (
203
+
id integer primary key autoincrement,
204
+
reacted_by_did text not null,
205
+
thread_at text not null,
206
+
kind text not null,
207
+
rkey text not null,
208
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
209
+
unique(reacted_by_did, thread_at, kind)
210
+
);
211
+
212
create table if not exists emails (
213
id integer primary key autoincrement,
214
did text not null,
+2
-2
appview/db/issues.go
+2
-2
appview/db/issues.go
···
277
}
278
279
func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
280
-
query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
281
row := e.QueryRow(query, repoAt, issueId)
282
283
var issue Issue
284
var createdAt string
285
-
err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open)
286
if err != nil {
287
return nil, nil, err
288
}
···
277
}
278
279
func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
280
+
query := `select owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?`
281
row := e.QueryRow(query, repoAt, issueId)
282
283
var issue Issue
284
var createdAt string
285
+
err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt)
286
if err != nil {
287
return nil, nil, err
288
}
+4
-3
appview/db/pipeline.go
+4
-3
appview/db/pipeline.go
···
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
"github.com/go-git/go-git/v5/plumbing"
11
spindle "tangled.sh/tangled.sh/core/spindle/models"
12
)
13
14
type Pipeline struct {
···
85
86
type Trigger struct {
87
Id int
88
-
Kind string
89
90
// push trigger fields
91
PushRef *string
···
100
}
101
102
func (t *Trigger) IsPush() bool {
103
-
return t != nil && t.Kind == "push"
104
}
105
106
func (t *Trigger) IsPullRequest() bool {
107
-
return t != nil && t.Kind == "pull_request"
108
}
109
110
func (t *Trigger) TargetRef() string {
···
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
"github.com/go-git/go-git/v5/plumbing"
11
spindle "tangled.sh/tangled.sh/core/spindle/models"
12
+
"tangled.sh/tangled.sh/core/workflow"
13
)
14
15
type Pipeline struct {
···
86
87
type Trigger struct {
88
Id int
89
+
Kind workflow.TriggerKind
90
91
// push trigger fields
92
PushRef *string
···
101
}
102
103
func (t *Trigger) IsPush() bool {
104
+
return t != nil && t.Kind == workflow.TriggerKindPush
105
}
106
107
func (t *Trigger) IsPullRequest() bool {
108
+
return t != nil && t.Kind == workflow.TriggerKindPullRequest
109
}
110
111
func (t *Trigger) TargetRef() string {
+6
appview/db/pulls.go
+6
appview/db/pulls.go
···
87
if p.PullSource != nil {
88
s := p.PullSource.AsRecord()
89
source = &s
90
}
91
92
record := tangled.RepoPull{
···
162
func (p *Pull) LatestPatch() string {
163
latestSubmission := p.Submissions[p.LastRoundNumber()]
164
return latestSubmission.Patch
165
}
166
167
func (p *Pull) PullAt() syntax.ATURI {
···
87
if p.PullSource != nil {
88
s := p.PullSource.AsRecord()
89
source = &s
90
+
source.Sha = p.LatestSha()
91
}
92
93
record := tangled.RepoPull{
···
163
func (p *Pull) LatestPatch() string {
164
latestSubmission := p.Submissions[p.LastRoundNumber()]
165
return latestSubmission.Patch
166
+
}
167
+
168
+
func (p *Pull) LatestSha() string {
169
+
latestSubmission := p.Submissions[p.LastRoundNumber()]
170
+
return latestSubmission.SourceRev
171
}
172
173
func (p *Pull) PullAt() syntax.ATURI {
+141
appview/db/reaction.go
+141
appview/db/reaction.go
···
···
1
+
package db
2
+
3
+
import (
4
+
"log"
5
+
"time"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
)
9
+
10
+
type ReactionKind string
11
+
12
+
const (
13
+
Like ReactionKind = "๐"
14
+
Unlike = "๐"
15
+
Laugh = "๐"
16
+
Celebration = "๐"
17
+
Confused = "๐ซค"
18
+
Heart = "โค๏ธ"
19
+
Rocket = "๐"
20
+
Eyes = "๐"
21
+
)
22
+
23
+
func (rk ReactionKind) String() string {
24
+
return string(rk)
25
+
}
26
+
27
+
var OrderedReactionKinds = []ReactionKind{
28
+
Like,
29
+
Unlike,
30
+
Laugh,
31
+
Celebration,
32
+
Confused,
33
+
Heart,
34
+
Rocket,
35
+
Eyes,
36
+
}
37
+
38
+
func ParseReactionKind(raw string) (ReactionKind, bool) {
39
+
k, ok := (map[string]ReactionKind{
40
+
"๐": Like,
41
+
"๐": Unlike,
42
+
"๐": Laugh,
43
+
"๐": Celebration,
44
+
"๐ซค": Confused,
45
+
"โค๏ธ": Heart,
46
+
"๐": Rocket,
47
+
"๐": Eyes,
48
+
})[raw]
49
+
return k, ok
50
+
}
51
+
52
+
type Reaction struct {
53
+
ReactedByDid string
54
+
ThreadAt syntax.ATURI
55
+
Created time.Time
56
+
Rkey string
57
+
Kind ReactionKind
58
+
}
59
+
60
+
func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind, rkey string) error {
61
+
query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey) values (?, ?, ?, ?)`
62
+
_, err := e.Exec(query, reactedByDid, threadAt, kind, rkey)
63
+
return err
64
+
}
65
+
66
+
// Get a reaction record
67
+
func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) (*Reaction, error) {
68
+
query := `
69
+
select reacted_by_did, thread_at, created, rkey
70
+
from reactions
71
+
where reacted_by_did = ? and thread_at = ? and kind = ?`
72
+
row := e.QueryRow(query, reactedByDid, threadAt, kind)
73
+
74
+
var reaction Reaction
75
+
var created string
76
+
err := row.Scan(&reaction.ReactedByDid, &reaction.ThreadAt, &created, &reaction.Rkey)
77
+
if err != nil {
78
+
return nil, err
79
+
}
80
+
81
+
createdAtTime, err := time.Parse(time.RFC3339, created)
82
+
if err != nil {
83
+
log.Println("unable to determine followed at time")
84
+
reaction.Created = time.Now()
85
+
} else {
86
+
reaction.Created = createdAtTime
87
+
}
88
+
89
+
return &reaction, nil
90
+
}
91
+
92
+
// Remove a reaction
93
+
func DeleteReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) error {
94
+
_, err := e.Exec(`delete from reactions where reacted_by_did = ? and thread_at = ? and kind = ?`, reactedByDid, threadAt, kind)
95
+
return err
96
+
}
97
+
98
+
// Remove a reaction
99
+
func DeleteReactionByRkey(e Execer, reactedByDid string, rkey string) error {
100
+
_, err := e.Exec(`delete from reactions where reacted_by_did = ? and rkey = ?`, reactedByDid, rkey)
101
+
return err
102
+
}
103
+
104
+
func GetReactionCount(e Execer, threadAt syntax.ATURI, kind ReactionKind) (int, error) {
105
+
count := 0
106
+
err := e.QueryRow(
107
+
`select count(reacted_by_did) from reactions where thread_at = ? and kind = ?`, threadAt, kind).Scan(&count)
108
+
if err != nil {
109
+
return 0, err
110
+
}
111
+
return count, nil
112
+
}
113
+
114
+
func GetReactionCountMap(e Execer, threadAt syntax.ATURI) (map[ReactionKind]int, error) {
115
+
countMap := map[ReactionKind]int{}
116
+
for _, kind := range OrderedReactionKinds {
117
+
count, err := GetReactionCount(e, threadAt, kind)
118
+
if err != nil {
119
+
return map[ReactionKind]int{}, nil
120
+
}
121
+
countMap[kind] = count
122
+
}
123
+
return countMap, nil
124
+
}
125
+
126
+
func GetReactionStatus(e Execer, userDid string, threadAt syntax.ATURI, kind ReactionKind) bool {
127
+
if _, err := GetReaction(e, userDid, threadAt, kind); err != nil {
128
+
return false
129
+
} else {
130
+
return true
131
+
}
132
+
}
133
+
134
+
func GetReactionStatusMap(e Execer, userDid string, threadAt syntax.ATURI) map[ReactionKind]bool {
135
+
statusMap := map[ReactionKind]bool{}
136
+
for _, kind := range OrderedReactionKinds {
137
+
count := GetReactionStatus(e, userDid, threadAt, kind)
138
+
statusMap[kind] = count
139
+
}
140
+
return statusMap
141
+
}
+9
appview/idresolver/resolver.go
+9
appview/idresolver/resolver.go
+36
-20
appview/ingester.go
+36
-20
appview/ingester.go
···
40
}
41
}()
42
43
-
if e.Kind != models.EventKindCommit {
44
-
return nil
45
-
}
46
-
47
-
switch e.Commit.Collection {
48
-
case tangled.GraphFollowNSID:
49
-
err = i.ingestFollow(e)
50
-
case tangled.FeedStarNSID:
51
-
err = i.ingestStar(e)
52
-
case tangled.PublicKeyNSID:
53
-
err = i.ingestPublicKey(e)
54
-
case tangled.RepoArtifactNSID:
55
-
err = i.ingestArtifact(e)
56
-
case tangled.ActorProfileNSID:
57
-
err = i.ingestProfile(e)
58
-
case tangled.SpindleMemberNSID:
59
-
err = i.ingestSpindleMember(e)
60
-
case tangled.SpindleNSID:
61
-
err = i.ingestSpindle(e)
62
}
63
64
if err != nil {
65
-
l := i.Logger.With("nsid", e.Commit.Collection)
66
l.Error("error ingesting record", "err", err)
67
}
68
···
475
ddb, ok := i.Db.Execer.(*db.DB)
476
if !ok {
477
return fmt.Errorf("failed to index profile record, invalid db cast")
478
}
479
480
tx, err := ddb.Begin()
···
40
}
41
}()
42
43
+
l := i.Logger.With("kind", e.Kind)
44
+
switch e.Kind {
45
+
case models.EventKindAccount:
46
+
if !e.Account.Active && *e.Account.Status == "deactivated" {
47
+
err = i.IdResolver.InvalidateIdent(ctx, e.Account.Did)
48
+
}
49
+
case models.EventKindIdentity:
50
+
err = i.IdResolver.InvalidateIdent(ctx, e.Identity.Did)
51
+
case models.EventKindCommit:
52
+
switch e.Commit.Collection {
53
+
case tangled.GraphFollowNSID:
54
+
err = i.ingestFollow(e)
55
+
case tangled.FeedStarNSID:
56
+
err = i.ingestStar(e)
57
+
case tangled.PublicKeyNSID:
58
+
err = i.ingestPublicKey(e)
59
+
case tangled.RepoArtifactNSID:
60
+
err = i.ingestArtifact(e)
61
+
case tangled.ActorProfileNSID:
62
+
err = i.ingestProfile(e)
63
+
case tangled.SpindleMemberNSID:
64
+
err = i.ingestSpindleMember(e)
65
+
case tangled.SpindleNSID:
66
+
err = i.ingestSpindle(e)
67
+
}
68
+
l = i.Logger.With("nsid", e.Commit.Collection)
69
}
70
71
if err != nil {
72
l.Error("error ingesting record", "err", err)
73
}
74
···
481
ddb, ok := i.Db.Execer.(*db.DB)
482
if !ok {
483
return fmt.Errorf("failed to index profile record, invalid db cast")
484
+
}
485
+
486
+
// get record from db first
487
+
spindles, err := db.GetSpindles(
488
+
ddb,
489
+
db.FilterEq("owner", did),
490
+
db.FilterEq("instance", instance),
491
+
)
492
+
if err != nil || len(spindles) != 1 {
493
+
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
494
}
495
496
tx, err := ddb.Begin()
+16
appview/issues/issues.go
+16
appview/issues/issues.go
···
11
12
comatproto "github.com/bluesky-social/indigo/api/atproto"
13
"github.com/bluesky-social/indigo/atproto/data"
14
lexutil "github.com/bluesky-social/indigo/lex/util"
15
"github.com/go-chi/chi/v5"
16
"github.com/posthog/posthog-go"
···
79
return
80
}
81
82
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
83
if err != nil {
84
log.Println("failed to resolve issue owner", err)
···
106
107
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
108
DidHandleMap: didHandleMap,
109
})
110
111
}
···
11
12
comatproto "github.com/bluesky-social/indigo/api/atproto"
13
"github.com/bluesky-social/indigo/atproto/data"
14
+
"github.com/bluesky-social/indigo/atproto/syntax"
15
lexutil "github.com/bluesky-social/indigo/lex/util"
16
"github.com/go-chi/chi/v5"
17
"github.com/posthog/posthog-go"
···
80
return
81
}
82
83
+
reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt))
84
+
if err != nil {
85
+
log.Println("failed to get issue reactions")
86
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
87
+
}
88
+
89
+
userReactions := map[db.ReactionKind]bool{}
90
+
if user != nil {
91
+
userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt))
92
+
}
93
+
94
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
95
if err != nil {
96
log.Println("failed to resolve issue owner", err)
···
118
119
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
120
DidHandleMap: didHandleMap,
121
+
122
+
OrderedReactionKinds: db.OrderedReactionKinds,
123
+
Reactions: reactionCountMap,
124
+
UserReacted: userReactions,
125
})
126
127
}
+1
-1
appview/middleware/middleware.go
+1
-1
appview/middleware/middleware.go
+15
-1
appview/pages/funcmap.go
+15
-1
appview/pages/funcmap.go
···
1
package pages
2
3
import (
4
"errors"
5
"fmt"
6
"html"
···
19
"tangled.sh/tangled.sh/core/appview/pages/markup"
20
)
21
22
-
func funcMap() template.FuncMap {
23
return template.FuncMap{
24
"split": func(s string) []string {
25
return strings.Split(s, "\n")
···
246
u, _ := url.PathUnescape(s)
247
return u
248
},
249
}
250
}
251
252
func icon(name string, classes []string) (template.HTML, error) {
···
1
package pages
2
3
import (
4
+
"crypto/hmac"
5
+
"crypto/sha256"
6
+
"encoding/hex"
7
"errors"
8
"fmt"
9
"html"
···
22
"tangled.sh/tangled.sh/core/appview/pages/markup"
23
)
24
25
+
func (p *Pages) funcMap() template.FuncMap {
26
return template.FuncMap{
27
"split": func(s string) []string {
28
return strings.Split(s, "\n")
···
249
u, _ := url.PathUnescape(s)
250
return u
251
},
252
+
253
+
"tinyAvatar": p.tinyAvatar,
254
}
255
+
}
256
+
257
+
func (p *Pages) tinyAvatar(handle string) string {
258
+
handle = strings.TrimPrefix(handle, "@")
259
+
secret := p.avatar.SharedSecret
260
+
h := hmac.New(sha256.New, []byte(secret))
261
+
h.Write([]byte(handle))
262
+
signature := hex.EncodeToString(h.Sum(nil))
263
+
return fmt.Sprintf("%s/%s/%s?size=tiny", p.avatar.Host, signature, handle)
264
}
265
266
func icon(name string, classes []string) (template.HTML, error) {
+42
-18
appview/pages/pages.go
+42
-18
appview/pages/pages.go
···
40
41
type Pages struct {
42
t map[string]*template.Template
43
dev bool
44
embedFS embed.FS
45
templateDir string // Path to templates on disk for dev mode
···
57
p := &Pages{
58
t: make(map[string]*template.Template),
59
dev: config.Core.Dev,
60
embedFS: Files,
61
rctx: rctx,
62
templateDir: "appview/pages",
···
90
name := strings.TrimPrefix(path, "templates/")
91
name = strings.TrimSuffix(name, ".html")
92
tmpl, err := template.New(name).
93
-
Funcs(funcMap()).
94
ParseFS(p.embedFS, path)
95
if err != nil {
96
log.Fatalf("setting up fragment: %v", err)
···
131
allPaths = append(allPaths, fragmentPaths...)
132
allPaths = append(allPaths, path)
133
tmpl, err := template.New(name).
134
-
Funcs(funcMap()).
135
ParseFS(p.embedFS, allPaths...)
136
if err != nil {
137
return fmt.Errorf("setting up template: %w", err)
···
185
}
186
187
// Create a new template
188
-
tmpl := template.New(name).Funcs(funcMap())
189
190
// Parse layouts
191
layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html")
···
446
Raw bool
447
EmailToDidOrHandle map[string]string
448
VerifiedCommits commitverify.VerifiedCommits
449
-
Languages *types.RepoLanguageResponse
450
Pipelines map[string]db.Pipeline
451
types.RepoIndexResponse
452
}
···
688
IssueOwnerHandle string
689
DidHandleMap map[string]string
690
691
State string
692
}
693
694
func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
695
params.Active = "issues"
696
if params.Issue.Open {
···
795
AbandonedPulls []*db.Pull
796
MergeCheck types.MergeCheckResponse
797
ResubmitCheck ResubmitResult
798
}
799
800
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
···
803
}
804
805
type RepoPullPatchParams struct {
806
-
LoggedInUser *oauth.User
807
-
DidHandleMap map[string]string
808
-
RepoInfo repoinfo.RepoInfo
809
-
Pull *db.Pull
810
-
Stack db.Stack
811
-
Diff *types.NiceDiff
812
-
Round int
813
-
Submission *db.PullSubmission
814
}
815
816
// this name is a mouthful
···
819
}
820
821
type RepoPullInterdiffParams struct {
822
-
LoggedInUser *oauth.User
823
-
DidHandleMap map[string]string
824
-
RepoInfo repoinfo.RepoInfo
825
-
Pull *db.Pull
826
-
Round int
827
-
Interdiff *patchutil.InterdiffResult
828
}
829
830
// this name is a mouthful
···
40
41
type Pages struct {
42
t map[string]*template.Template
43
+
avatar config.AvatarConfig
44
dev bool
45
embedFS embed.FS
46
templateDir string // Path to templates on disk for dev mode
···
58
p := &Pages{
59
t: make(map[string]*template.Template),
60
dev: config.Core.Dev,
61
+
avatar: config.Avatar,
62
embedFS: Files,
63
rctx: rctx,
64
templateDir: "appview/pages",
···
92
name := strings.TrimPrefix(path, "templates/")
93
name = strings.TrimSuffix(name, ".html")
94
tmpl, err := template.New(name).
95
+
Funcs(p.funcMap()).
96
ParseFS(p.embedFS, path)
97
if err != nil {
98
log.Fatalf("setting up fragment: %v", err)
···
133
allPaths = append(allPaths, fragmentPaths...)
134
allPaths = append(allPaths, path)
135
tmpl, err := template.New(name).
136
+
Funcs(p.funcMap()).
137
ParseFS(p.embedFS, allPaths...)
138
if err != nil {
139
return fmt.Errorf("setting up template: %w", err)
···
187
}
188
189
// Create a new template
190
+
tmpl := template.New(name).Funcs(p.funcMap())
191
192
// Parse layouts
193
layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html")
···
448
Raw bool
449
EmailToDidOrHandle map[string]string
450
VerifiedCommits commitverify.VerifiedCommits
451
+
Languages []types.RepoLanguageDetails
452
Pipelines map[string]db.Pipeline
453
types.RepoIndexResponse
454
}
···
690
IssueOwnerHandle string
691
DidHandleMap map[string]string
692
693
+
OrderedReactionKinds []db.ReactionKind
694
+
Reactions map[db.ReactionKind]int
695
+
UserReacted map[db.ReactionKind]bool
696
+
697
State string
698
}
699
700
+
type ThreadReactionFragmentParams struct {
701
+
ThreadAt syntax.ATURI
702
+
Kind db.ReactionKind
703
+
Count int
704
+
IsReacted bool
705
+
}
706
+
707
+
func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error {
708
+
return p.executePlain("repo/fragments/reaction", w, params)
709
+
}
710
+
711
func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
712
params.Active = "issues"
713
if params.Issue.Open {
···
812
AbandonedPulls []*db.Pull
813
MergeCheck types.MergeCheckResponse
814
ResubmitCheck ResubmitResult
815
+
Pipelines map[string]db.Pipeline
816
+
817
+
OrderedReactionKinds []db.ReactionKind
818
+
Reactions map[db.ReactionKind]int
819
+
UserReacted map[db.ReactionKind]bool
820
}
821
822
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
···
825
}
826
827
type RepoPullPatchParams struct {
828
+
LoggedInUser *oauth.User
829
+
DidHandleMap map[string]string
830
+
RepoInfo repoinfo.RepoInfo
831
+
Pull *db.Pull
832
+
Stack db.Stack
833
+
Diff *types.NiceDiff
834
+
Round int
835
+
Submission *db.PullSubmission
836
+
OrderedReactionKinds []db.ReactionKind
837
}
838
839
// this name is a mouthful
···
842
}
843
844
type RepoPullInterdiffParams struct {
845
+
LoggedInUser *oauth.User
846
+
DidHandleMap map[string]string
847
+
RepoInfo repoinfo.RepoInfo
848
+
Pull *db.Pull
849
+
Round int
850
+
Interdiff *patchutil.InterdiffResult
851
+
OrderedReactionKinds []db.ReactionKind
852
}
853
854
// this name is a mouthful
+1
-1
appview/pages/templates/layouts/base.html
+1
-1
appview/pages/templates/layouts/base.html
···
9
/>
10
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
11
<script src="/static/htmx.min.js"></script>
12
-
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.2"></script>
13
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
14
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
15
{{ block "extrameta" . }}{{ end }}
···
9
/>
10
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
11
<script src="/static/htmx.min.js"></script>
12
+
<script src="/static/htmx-ext-ws.min.js"></script>
13
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
14
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
15
{{ block "extrameta" . }}{{ end }}
+26
-23
appview/pages/templates/layouts/repobase.html
+26
-23
appview/pages/templates/layouts/repobase.html
···
1
{{ define "title" }}{{ .RepoInfo.FullName }}{{ end }}
2
3
{{ define "content" }}
4
-
<section id="repo-header" class="mb-4 py-2 px-6 dark:text-white">
5
-
{{ if .RepoInfo.Source }}
6
-
<p class="text-sm">
7
-
<div class="flex items-center">
8
-
{{ i "git-fork" "w-3 h-3 mr-1"}}
9
-
forked from
10
-
{{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}
11
-
<a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a>
12
-
</div>
13
-
</p>
14
-
{{ end }}
15
-
<div class="text-lg flex items-center justify-between">
16
-
<div>
17
-
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
18
-
<span class="select-none">/</span>
19
-
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
20
-
</div>
21
22
-
{{ template "repo/fragments/repoActions" .RepoInfo }}
23
-
</div>
24
-
{{ template "repo/fragments/repoDescription" . }}
25
-
</section>
26
-
<section class="min-h-screen flex flex-col drop-shadow-sm">
27
<nav class="w-full pl-4 overflow-auto">
28
<div class="flex z-60">
29
{{ $activeTabStyles := "-mb-px bg-white dark:bg-gray-800" }}
···
61
</div>
62
</nav>
63
<section
64
-
class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"
65
>
66
{{ block "repoContent" . }}{{ end }}
67
</section>
···
1
{{ define "title" }}{{ .RepoInfo.FullName }}{{ end }}
2
3
{{ define "content" }}
4
+
<section id="repo-header" class="mb-4 py-2 px-6 dark:text-white">
5
+
{{ if .RepoInfo.Source }}
6
+
<p class="text-sm">
7
+
<div class="flex items-center">
8
+
{{ i "git-fork" "w-3 h-3 mr-1"}}
9
+
forked from
10
+
{{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}
11
+
<a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a>
12
+
</div>
13
+
</p>
14
+
{{ end }}
15
+
<div class="text-lg flex items-center justify-between">
16
+
<div>
17
+
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
18
+
<span class="select-none">/</span>
19
+
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
20
+
</div>
21
+
22
+
{{ template "repo/fragments/repoActions" .RepoInfo }}
23
+
</div>
24
+
{{ template "repo/fragments/repoDescription" . }}
25
+
</section>
26
27
+
<section
28
+
class="min-h-screen w-full flex flex-col drop-shadow-sm"
29
+
>
30
<nav class="w-full pl-4 overflow-auto">
31
<div class="flex z-60">
32
{{ $activeTabStyles := "-mb-px bg-white dark:bg-gray-800" }}
···
64
</div>
65
</nav>
66
<section
67
+
class="bg-white dark:bg-gray-800 p-6 rounded relative w-full dark:text-white"
68
>
69
{{ block "repoContent" . }}{{ end }}
70
</section>
+11
-10
appview/pages/templates/layouts/topbar.html
+11
-10
appview/pages/templates/layouts/topbar.html
···
1
{{ define "layouts/topbar" }}
2
<nav class="space-x-4 mb-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm">
3
-
<div class="container flex justify-between p-0">
4
<div id="left-items">
5
<a href="/" hx-boost="true" class="flex gap-2 font-semibold italic">
6
tangled<sub>alpha</sub>
···
19
{{ i "code" "size-4" }} source
20
</a>
21
</div>
22
-
<div id="right-items" class="flex gap-2">
23
{{ with .LoggedInUser }}
24
-
<a href="/repo/new" hx-boost="true">
25
-
{{ i "plus" "w-6 h-6" }}
26
</a>
27
{{ block "dropDown" . }} {{ end }}
28
{{ else }}
···
36
{{ define "dropDown" }}
37
<details class="relative inline-block text-left">
38
<summary
39
-
class="cursor-pointer list-none"
40
>
41
-
{{ didOrHandle .Did .Handle }}
42
</summary>
43
<div
44
class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"
45
>
46
-
<a href="/{{ didOrHandle .Did .Handle }}">profile</a>
47
<a href="/knots">knots</a>
48
<a href="/spindles">spindles</a>
49
<a href="/settings">settings</a>
50
-
<a href="#"
51
-
hx-post="/logout"
52
-
hx-swap="none"
53
class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
54
logout
55
</a>
···
1
{{ define "layouts/topbar" }}
2
<nav class="space-x-4 mb-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm">
3
+
<div class="container flex justify-between p-0 items-center">
4
<div id="left-items">
5
<a href="/" hx-boost="true" class="flex gap-2 font-semibold italic">
6
tangled<sub>alpha</sub>
···
19
{{ i "code" "size-4" }} source
20
</a>
21
</div>
22
+
<div id="right-items" class="flex items-center gap-4">
23
{{ with .LoggedInUser }}
24
+
<a href="/repo/new" hx-boost="true" class="btn-create hover:no-underline hover:text-white">
25
+
{{ i "plus" "w-4 h-4" }}
26
</a>
27
{{ block "dropDown" . }} {{ end }}
28
{{ else }}
···
36
{{ define "dropDown" }}
37
<details class="relative inline-block text-left">
38
<summary
39
+
class="cursor-pointer list-none flex items-center"
40
>
41
+
{{ $user := didOrHandle .Did .Handle }}
42
+
{{ template "user/fragments/picHandleLink" $user }}
43
</summary>
44
<div
45
class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"
46
>
47
+
<a href="/{{ $user }}">profile</a>
48
<a href="/knots">knots</a>
49
<a href="/spindles">spindles</a>
50
<a href="/settings">settings</a>
51
+
<a href="#"
52
+
hx-post="/logout"
53
+
hx-swap="none"
54
class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
55
logout
56
</a>
+1
-1
appview/pages/templates/repo/commit.html
+1
-1
appview/pages/templates/repo/commit.html
···
59
<div class="flex items-center gap-2 my-2">
60
{{ i "user" "w-4 h-4" }}
61
{{ $committerDidOrHandle := index $.EmailToDidOrHandle $commit.Committer.Email }}
62
-
<a href="/{{ $committerDidOrHandle }}">{{ $committerDidOrHandle }}</a>
63
</div>
64
<div class="my-1 pt-2 text-xs border-t">
65
<div class="text-gray-600 dark:text-gray-300">SSH Key Fingerprint:</div>
···
59
<div class="flex items-center gap-2 my-2">
60
{{ i "user" "w-4 h-4" }}
61
{{ $committerDidOrHandle := index $.EmailToDidOrHandle $commit.Committer.Email }}
62
+
<a href="/{{ $committerDidOrHandle }}">{{ template "user/fragments/picHandleLink" $committerDidOrHandle }}</a>
63
</div>
64
<div class="my-1 pt-2 text-xs border-t">
65
<div class="text-gray-600 dark:text-gray-300">SSH Key Fingerprint:</div>
+34
appview/pages/templates/repo/fragments/reaction.html
+34
appview/pages/templates/repo/fragments/reaction.html
···
···
1
+
{{ define "repo/fragments/reaction" }}
2
+
<button
3
+
id="reactIndi-{{ .Kind }}"
4
+
class="flex justify-center items-center min-w-8 min-h-8 rounded border
5
+
leading-4 px-3 gap-1
6
+
{{ if eq .Count 0 }}
7
+
hidden
8
+
{{ end }}
9
+
{{ if .IsReacted }}
10
+
bg-sky-100
11
+
border-sky-400
12
+
dark:bg-sky-900
13
+
dark:border-sky-500
14
+
{{ else }}
15
+
border-gray-200
16
+
hover:bg-gray-50
17
+
hover:border-gray-300
18
+
dark:border-gray-700
19
+
dark:hover:bg-gray-700
20
+
dark:hover:border-gray-600
21
+
{{ end }}
22
+
"
23
+
{{ if .IsReacted }}
24
+
hx-delete="/react?subject={{ .ThreadAt }}&kind={{ .Kind }}"
25
+
{{ else }}
26
+
hx-post="/react?subject={{ .ThreadAt }}&kind={{ .Kind }}"
27
+
{{ end }}
28
+
hx-swap="outerHTML"
29
+
hx-trigger="click from:(#reactBtn-{{ .Kind }}, #reactIndi-{{ .Kind }})"
30
+
hx-disabled-elt="this"
31
+
>
32
+
<span>{{ .Kind }}</span> <span>{{ .Count }}</span>
33
+
</button>
34
+
{{ end }}
+30
appview/pages/templates/repo/fragments/reactionsPopUp.html
+30
appview/pages/templates/repo/fragments/reactionsPopUp.html
···
···
1
+
{{ define "repo/fragments/reactionsPopUp" }}
2
+
<details
3
+
id="reactionsPopUp"
4
+
class="relative inline-block"
5
+
>
6
+
<summary
7
+
class="flex justify-center items-center min-w-8 min-h-8 rounded border border-gray-200 dark:border-gray-700
8
+
hover:bg-gray-50
9
+
hover:border-gray-300
10
+
dark:hover:bg-gray-700
11
+
dark:hover:border-gray-600
12
+
cursor-pointer list-none"
13
+
>
14
+
{{ i "smile" "size-4" }}
15
+
</summary>
16
+
<div
17
+
class="absolute flex left-0 z-10 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg"
18
+
>
19
+
{{ range $kind := . }}
20
+
<button
21
+
id="reactBtn-{{ $kind }}"
22
+
class="size-12 hover:bg-gray-100 dark:hover:bg-gray-700"
23
+
hx-on:click="this.parentElement.parentElement.removeAttribute('open')"
24
+
>
25
+
{{ $kind }}
26
+
</button>
27
+
{{ end }}
28
+
</div>
29
+
</details>
30
+
{{ end }}
+17
-2
appview/pages/templates/repo/index.html
+17
-2
appview/pages/templates/repo/index.html
···
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }}
8
{{ end }}
9
10
-
11
{{ define "repoContent" }}
12
<main>
13
<div class="flex items-center justify-between pb-5">
14
{{ block "branchSelector" . }}{{ end }}
15
<div class="flex md:hidden items-center gap-4">
···
30
</div>
31
</main>
32
{{ end }}
33
34
{{ define "branchSelector" }}
35
<div class="flex gap-2 items-center items-stretch justify-center">
···
251
{{ end }}"
252
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
253
>{{ if $didOrHandle }}
254
-
{{ $didOrHandle }}
255
{{ else }}
256
{{ .Author.Name }}
257
{{ end }}</a
···
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }}
8
{{ end }}
9
10
{{ define "repoContent" }}
11
<main>
12
+
{{ if .Languages }}
13
+
{{ block "repoLanguages" . }}{{ end }}
14
+
{{ end }}
15
<div class="flex items-center justify-between pb-5">
16
{{ block "branchSelector" . }}{{ end }}
17
<div class="flex md:hidden items-center gap-4">
···
32
</div>
33
</main>
34
{{ end }}
35
+
36
+
{{ define "repoLanguages" }}
37
+
<div class="flex gap-[1px] -m-6 mb-6 overflow-hidden rounded-t">
38
+
{{ range $value := .Languages }}
39
+
<div
40
+
title='{{ or $value.Name "Other" }} {{ printf "%.1f" $value.Percentage }}%'
41
+
class="h-[4px] rounded-full"
42
+
style="background-color: {{ $value.Color }}; width: {{ $value.Percentage }}%"
43
+
></div>
44
+
{{ end }}
45
+
</div>
46
+
{{ end }}
47
+
48
49
{{ define "branchSelector" }}
50
<div class="flex gap-2 items-center items-stretch justify-center">
···
266
{{ end }}"
267
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
268
>{{ if $didOrHandle }}
269
+
{{ template "user/fragments/picHandleLink" $didOrHandle }}
270
{{ else }}
271
{{ .Author.Name }}
272
{{ end }}</a
+6
-6
appview/pages/templates/repo/issues/fragments/issueComment.html
+6
-6
appview/pages/templates/repo/issues/fragments/issueComment.html
···
1
{{ define "repo/issues/fragments/issueComment" }}
2
{{ with .Comment }}
3
<div id="comment-container-{{.CommentId}}">
4
-
<div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm">
5
{{ $owner := index $.DidHandleMap .OwnerDid }}
6
-
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
7
8
<span class="before:content-['ยท']"></span>
9
<a
···
18
{{ .Created | timeFmt }}
19
{{ end }}
20
</a>
21
-
22
<!-- show user "hats" -->
23
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
24
{{ if $isIssueAuthor }}
···
29
30
{{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }}
31
{{ if and $isCommentOwner (not .Deleted) }}
32
-
<button
33
-
class="btn px-2 py-1 text-sm"
34
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
35
hx-swap="outerHTML"
36
hx-target="#comment-container-{{.CommentId}}"
37
>
38
{{ i "pencil" "w-4 h-4" }}
39
</button>
40
-
<button
41
class="btn px-2 py-1 text-sm text-red-500 flex gap-2 items-center group"
42
hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
43
hx-confirm="Are you sure you want to delete your comment?"
···
1
{{ define "repo/issues/fragments/issueComment" }}
2
{{ with .Comment }}
3
<div id="comment-container-{{.CommentId}}">
4
+
<div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm flex-wrap">
5
{{ $owner := index $.DidHandleMap .OwnerDid }}
6
+
{{ template "user/fragments/picHandleLink" $owner }}
7
8
<span class="before:content-['ยท']"></span>
9
<a
···
18
{{ .Created | timeFmt }}
19
{{ end }}
20
</a>
21
+
22
<!-- show user "hats" -->
23
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
24
{{ if $isIssueAuthor }}
···
29
30
{{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }}
31
{{ if and $isCommentOwner (not .Deleted) }}
32
+
<button
33
+
class="btn px-2 py-1 text-sm"
34
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
35
hx-swap="outerHTML"
36
hx-target="#comment-container-{{.CommentId}}"
37
>
38
{{ i "pencil" "w-4 h-4" }}
39
</button>
40
+
<button
41
class="btn px-2 py-1 text-sm text-red-500 flex gap-2 items-center group"
42
hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
43
hx-confirm="Are you sure you want to delete your comment?"
+36
-24
appview/pages/templates/repo/issues/issue.html
+36
-24
appview/pages/templates/repo/issues/issue.html
···
4
{{ define "extrameta" }}
5
{{ $title := printf "%s · issue #%d · %s" .Issue.Title .Issue.IssueId .RepoInfo.FullName }}
6
{{ $url := printf "https://tangled.sh/%s/issues/%d" .RepoInfo.FullName .Issue.IssueId }}
7
-
8
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
9
{{ end }}
10
···
30
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
31
<span class="text-white">{{ .State }}</span>
32
</div>
33
-
<span class="text-gray-500 dark:text-gray-400 text-sm">
34
opened by
35
{{ $owner := didOrHandle .Issue.OwnerDid .IssueOwnerHandle }}
36
-
<a href="/{{ $owner }}" class="no-underline hover:underline"
37
-
>{{ $owner }}</a
38
-
>
39
-
<span class="px-1 select-none before:content-['\00B7']"></span>
40
<time title="{{ .Issue.Created | longTimeFmt }}">
41
{{ .Issue.Created | timeFmt }}
42
</time>
···
48
{{ .Issue.Body | markdown }}
49
</article>
50
{{ end }}
51
</section>
52
{{ end }}
53
···
71
72
{{ define "newComment" }}
73
{{ if .LoggedInUser }}
74
-
<form
75
-
id="comment-form"
76
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
77
hx-on::after-request="if(event.detail.successful) this.reset()"
78
>
79
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5">
80
<div class="text-sm pb-2 text-gray-500 dark:text-gray-400">
81
-
{{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }}
82
</div>
83
<textarea
84
id="comment-textarea"
···
90
<div id="issue-comment"></div>
91
<div id="issue-action" class="error"></div>
92
</div>
93
-
94
<div class="flex gap-2 mt-2">
95
-
<button
96
id="comment-button"
97
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
98
type="submit"
···
109
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
110
{{ $isRepoOwner := .RepoInfo.Roles.IsOwner }}
111
{{ if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "open") }}
112
-
<button
113
id="close-button"
114
-
type="button"
115
class="btn flex items-center gap-2"
116
hx-indicator="#close-spinner"
117
hx-trigger="click"
···
122
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
123
</span>
124
</button>
125
-
<div
126
-
id="close-with-comment"
127
-
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
128
-
hx-trigger="click from:#close-button"
129
hx-disabled-elt="#close-with-comment"
130
hx-target="#issue-comment"
131
hx-indicator="#close-spinner"
···
133
hx-swap="none"
134
>
135
</div>
136
-
<div
137
-
id="close-issue"
138
hx-disabled-elt="#close-issue"
139
-
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/close"
140
-
hx-trigger="click from:#close-button"
141
hx-target="#issue-action"
142
hx-indicator="#close-spinner"
143
hx-swap="none"
···
155
});
156
</script>
157
{{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "closed") }}
158
-
<button
159
-
type="button"
160
class="btn flex items-center gap-2"
161
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/reopen"
162
hx-indicator="#reopen-spinner"
···
206
});
207
</script>
208
</div>
209
-
</form>
210
{{ else }}
211
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-fit">
212
<a href="/login" class="underline">login</a> to join the discussion
···
4
{{ define "extrameta" }}
5
{{ $title := printf "%s · issue #%d · %s" .Issue.Title .Issue.IssueId .RepoInfo.FullName }}
6
{{ $url := printf "https://tangled.sh/%s/issues/%d" .RepoInfo.FullName .Issue.IssueId }}
7
+
8
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
9
{{ end }}
10
···
30
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
31
<span class="text-white">{{ .State }}</span>
32
</div>
33
+
<span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1">
34
opened by
35
{{ $owner := didOrHandle .Issue.OwnerDid .IssueOwnerHandle }}
36
+
{{ template "user/fragments/picHandleLink" $owner }}
37
+
<span class="select-none before:content-['\00B7']"></span>
38
<time title="{{ .Issue.Created | longTimeFmt }}">
39
{{ .Issue.Created | timeFmt }}
40
</time>
···
46
{{ .Issue.Body | markdown }}
47
</article>
48
{{ end }}
49
+
50
+
<div class="flex items-center gap-2 mt-2">
51
+
{{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }}
52
+
{{ range $kind := .OrderedReactionKinds }}
53
+
{{
54
+
template "repo/fragments/reaction"
55
+
(dict
56
+
"Kind" $kind
57
+
"Count" (index $.Reactions $kind)
58
+
"IsReacted" (index $.UserReacted $kind)
59
+
"ThreadAt" $.Issue.IssueAt)
60
+
}}
61
+
{{ end }}
62
+
</div>
63
</section>
64
{{ end }}
65
···
83
84
{{ define "newComment" }}
85
{{ if .LoggedInUser }}
86
+
<form
87
+
id="comment-form"
88
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
89
hx-on::after-request="if(event.detail.successful) this.reset()"
90
>
91
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5">
92
<div class="text-sm pb-2 text-gray-500 dark:text-gray-400">
93
+
{{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }}
94
</div>
95
<textarea
96
id="comment-textarea"
···
102
<div id="issue-comment"></div>
103
<div id="issue-action" class="error"></div>
104
</div>
105
+
106
<div class="flex gap-2 mt-2">
107
+
<button
108
id="comment-button"
109
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
110
type="submit"
···
121
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
122
{{ $isRepoOwner := .RepoInfo.Roles.IsOwner }}
123
{{ if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "open") }}
124
+
<button
125
id="close-button"
126
+
type="button"
127
class="btn flex items-center gap-2"
128
hx-indicator="#close-spinner"
129
hx-trigger="click"
···
134
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
135
</span>
136
</button>
137
+
<div
138
+
id="close-with-comment"
139
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
140
+
hx-trigger="click from:#close-button"
141
hx-disabled-elt="#close-with-comment"
142
hx-target="#issue-comment"
143
hx-indicator="#close-spinner"
···
145
hx-swap="none"
146
>
147
</div>
148
+
<div
149
+
id="close-issue"
150
hx-disabled-elt="#close-issue"
151
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/close"
152
+
hx-trigger="click from:#close-button"
153
hx-target="#issue-action"
154
hx-indicator="#close-spinner"
155
hx-swap="none"
···
167
});
168
</script>
169
{{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "closed") }}
170
+
<button
171
+
type="button"
172
class="btn flex items-center gap-2"
173
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/reopen"
174
hx-indicator="#reopen-spinner"
···
218
});
219
</script>
220
</div>
221
+
</form>
222
{{ else }}
223
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-fit">
224
<a href="/login" class="underline">login</a> to join the discussion
+6
-6
appview/pages/templates/repo/issues/issues.html
+6
-6
appview/pages/templates/repo/issues/issues.html
···
3
{{ define "extrameta" }}
4
{{ $title := "issues"}}
5
{{ $url := printf "https://tangled.sh/%s/issues" .RepoInfo.FullName }}
6
-
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
8
{{ end }}
9
···
27
</div>
28
<a
29
href="/{{ .RepoInfo.FullName }}/issues/new"
30
-
class="btn text-sm flex items-center justify-center gap-2 no-underline hover:no-underline"
31
>
32
{{ i "circle-plus" "w-4 h-4" }}
33
<span>new</span>
···
49
<span class="text-gray-500">#{{ .IssueId }}</span>
50
</a>
51
</div>
52
-
<p class="text-sm text-gray-500 dark:text-gray-400">
53
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
54
{{ $icon := "ban" }}
55
{{ $state := "closed" }}
···
64
<span class="text-white dark:text-white">{{ $state }}</span>
65
</span>
66
67
-
<span>
68
-
{{ $owner := index $.DidHandleMap .OwnerDid }}
69
-
<a href="/{{ $owner }}">{{ $owner }}</a>
70
</span>
71
72
<span class="before:content-['ยท']">
···
3
{{ define "extrameta" }}
4
{{ $title := "issues"}}
5
{{ $url := printf "https://tangled.sh/%s/issues" .RepoInfo.FullName }}
6
+
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
8
{{ end }}
9
···
27
</div>
28
<a
29
href="/{{ .RepoInfo.FullName }}/issues/new"
30
+
class="btn-create text-sm flex items-center justify-center gap-2 no-underline hover:no-underline hover:text-white"
31
>
32
{{ i "circle-plus" "w-4 h-4" }}
33
<span>new</span>
···
49
<span class="text-gray-500">#{{ .IssueId }}</span>
50
</a>
51
</div>
52
+
<p class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
53
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
54
{{ $icon := "ban" }}
55
{{ $state := "closed" }}
···
64
<span class="text-white dark:text-white">{{ $state }}</span>
65
</span>
66
67
+
<span class="ml-1">
68
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
69
+
{{ template "user/fragments/picHandleLink" $owner }}
70
</span>
71
72
<span class="before:content-['ยท']">
+5
-4
appview/pages/templates/repo/issues/new.html
+5
-4
appview/pages/templates/repo/issues/new.html
···
23
></textarea>
24
</div>
25
<div>
26
+
<button type="submit" class="btn-create flex items-center gap-2">
27
+
{{ i "circle-plus" "w-4 h-4" }}
28
+
create issue
29
+
<span id="create-pull-spinner" class="group">
30
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
31
</span>
32
</button>
33
</div>
+2
-2
appview/pages/templates/repo/log.html
+2
-2
appview/pages/templates/repo/log.html
···
31
<td class=" py-3 align-top">
32
{{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }}
33
{{ if $didOrHandle }}
34
-
<a href="/{{ $didOrHandle }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $didOrHandle }}</a>
35
{{ else }}
36
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
37
{{ end }}
···
159
{{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }}
160
<a href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
161
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
162
-
{{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }}
163
</a>
164
</span>
165
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
···
31
<td class=" py-3 align-top">
32
{{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }}
33
{{ if $didOrHandle }}
34
+
{{ template "user/fragments/picHandleLink" $didOrHandle }}
35
{{ else }}
36
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
37
{{ end }}
···
159
{{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }}
160
<a href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
161
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
162
+
{{ if $didOrHandle }}{{ template "user/fragments/picHandleLink" $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }}
163
</a>
164
</span>
165
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
+8
-7
appview/pages/templates/repo/new.html
+8
-7
appview/pages/templates/repo/new.html
···
60
</fieldset>
61
62
<div class="space-y-2">
63
-
<button type="submit" class="btn flex items-center">
64
-
create repo
65
-
<span id="spinner" class="group">
66
-
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
67
-
</span>
68
-
</button>
69
-
<div id="repo" class="error"></div>
70
</div>
71
</form>
72
</div>
···
60
</fieldset>
61
62
<div class="space-y-2">
63
+
<button type="submit" class="btn-create flex items-center gap-2">
64
+
{{ i "book-plus" "w-4 h-4" }}
65
+
create repo
66
+
<span id="create-pull-spinner" class="group">
67
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
68
+
</span>
69
+
</button>
70
+
<div id="repo" class="error"></div>
71
</div>
72
</form>
73
</div>
+3
-4
appview/pages/templates/repo/pipelines/fragments/logBlock.html
+3
-4
appview/pages/templates/repo/pipelines/fragments/logBlock.html
···
1
{{ define "repo/pipelines/fragments/logBlock" }}
2
<div id="lines" hx-swap-oob="beforeend">
3
-
<details id="step-{{ .Id }}" {{if not .Collapsed}}open{{end}} class="group bg-gray-100 px-2 dark:bg-gray-900">
4
-
<summary class="sticky top-0 py-1 list-none cursor-pointer py-2 bg-gray-100 dark:bg-gray-900 hover:text-gray-500 hover:dark:text-gray-400">
5
<div class="group-open:hidden flex items-center gap-1">
6
{{ i "chevron-right" "w-4 h-4" }} {{ .Name }}
7
</div>
···
9
{{ i "chevron-down" "w-4 h-4" }} {{ .Name }}
10
</div>
11
</summary>
12
-
<div class="text-blue-600 dark:text-blue-300 font-mono">{{ .Command }}</div>
13
-
<div id="step-body-{{ .Id }}" class="font-mono"></div>
14
</details>
15
</div>
16
{{ end }}
···
1
{{ define "repo/pipelines/fragments/logBlock" }}
2
<div id="lines" hx-swap-oob="beforeend">
3
+
<details id="step-{{ .Id }}" {{if not .Collapsed}}open{{end}} class="group bg-gray-100 pb-2 px-2 dark:bg-gray-900">
4
+
<summary class="sticky top-0 pt-2 group-open:pb-2 list-none cursor-pointer bg-gray-100 dark:bg-gray-900 hover:text-gray-500 hover:dark:text-gray-400">
5
<div class="group-open:hidden flex items-center gap-1">
6
{{ i "chevron-right" "w-4 h-4" }} {{ .Name }}
7
</div>
···
9
{{ i "chevron-down" "w-4 h-4" }} {{ .Name }}
10
</div>
11
</summary>
12
+
<div class="font-mono whitespace-pre overflow-x-auto"><div class="text-blue-600 dark:text-blue-300">{{ .Command }}</div><div id="step-body-{{ .Id }}"></div></div>
13
</details>
14
</div>
15
{{ end }}
+1
-3
appview/pages/templates/repo/pipelines/fragments/logLine.html
+1
-3
appview/pages/templates/repo/pipelines/fragments/logLine.html
+1
-1
appview/pages/templates/repo/pipelines/fragments/tooltip.html
+1
-1
appview/pages/templates/repo/pipelines/fragments/tooltip.html
+13
-8
appview/pages/templates/repo/pipelines/pipelines.html
+13
-8
appview/pages/templates/repo/pipelines/pipelines.html
···
34
{{ $p := index . 1 }}
35
{{ with $p }}
36
<div class="grid grid-cols-6 md:grid-cols-12 gap-2 items-center w-full">
37
-
<div class="col-span-2 md:col-span-8 flex items-center gap-4">
38
{{ $target := .Trigger.TargetRef }}
39
{{ $workflows := .Workflows }}
40
{{ $link := "" }}
···
43
{{ end }}
44
{{ if .Trigger.IsPush }}
45
<span class="font-bold">{{ $target }}</span>
46
-
<span>push</span>
47
<span class="hidden md:inline-flex gap-2 items-center font-mono text-sm">
48
{{ $old := deref .Trigger.PushOldSha }}
49
{{ $new := deref .Trigger.PushNewSha }}
···
53
<a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $old }}">{{ slice $old 0 8 }}</a>
54
</span>
55
{{ else if .Trigger.IsPullRequest }}
56
-
<span>
57
-
pull request
58
-
<span class="inline-flex gap-2 items-center">
59
-
{{ $target }}
60
-
{{ i "arrow-left" "size-4" }}
61
-
{{ .Trigger.PRSourceBranch }}
62
</span>
63
</span>
64
{{ end }}
···
34
{{ $p := index . 1 }}
35
{{ with $p }}
36
<div class="grid grid-cols-6 md:grid-cols-12 gap-2 items-center w-full">
37
+
<div class="text-sm md:text-base col-span-1">
38
+
{{ .Trigger.Kind.String }}
39
+
</div>
40
+
41
+
<div class="col-span-2 md:col-span-7 flex items-center gap-4">
42
{{ $target := .Trigger.TargetRef }}
43
{{ $workflows := .Workflows }}
44
{{ $link := "" }}
···
47
{{ end }}
48
{{ if .Trigger.IsPush }}
49
<span class="font-bold">{{ $target }}</span>
50
<span class="hidden md:inline-flex gap-2 items-center font-mono text-sm">
51
{{ $old := deref .Trigger.PushOldSha }}
52
{{ $new := deref .Trigger.PushNewSha }}
···
56
<a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $old }}">{{ slice $old 0 8 }}</a>
57
</span>
58
{{ else if .Trigger.IsPullRequest }}
59
+
{{ $sha := deref .Trigger.PRSourceSha }}
60
+
<span class="inline-flex gap-2 items-center">
61
+
<span class="font-bold">{{ $target }}</span>
62
+
{{ i "arrow-left" "size-4" }}
63
+
{{ .Trigger.PRSourceBranch }}
64
+
<span class="text-sm font-mono">
65
+
@
66
+
<a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $sha }}">{{ slice $sha 0 8 }}</a>
67
</span>
68
</span>
69
{{ end }}
+18
-4
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+18
-4
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
26
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
27
<span class="text-white">{{ .Pull.State.String }}</span>
28
</div>
29
-
<span class="text-gray-500 dark:text-gray-400 text-sm">
30
opened by
31
{{ $owner := index $.DidHandleMap .Pull.OwnerDid }}
32
-
<a href="/{{ $owner }}" class="no-underline hover:underline"
33
-
>{{ $owner }}</a
34
-
>
35
<span class="select-none before:content-['\00B7']"></span>
36
<time>{{ .Pull.Created | timeFmt }}</time>
37
···
62
<article id="body" class="mt-8 prose dark:prose-invert">
63
{{ .Pull.Body | markdown }}
64
</article>
65
{{ end }}
66
</section>
67
···
26
{{ i $icon "w-4 h-4 mr-1.5 text-white" }}
27
<span class="text-white">{{ .Pull.State.String }}</span>
28
</div>
29
+
<span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1">
30
opened by
31
{{ $owner := index $.DidHandleMap .Pull.OwnerDid }}
32
+
{{ template "user/fragments/picHandleLink" $owner }}
33
<span class="select-none before:content-['\00B7']"></span>
34
<time>{{ .Pull.Created | timeFmt }}</time>
35
···
60
<article id="body" class="mt-8 prose dark:prose-invert">
61
{{ .Pull.Body | markdown }}
62
</article>
63
+
{{ end }}
64
+
65
+
{{ with .OrderedReactionKinds }}
66
+
<div class="flex items-center gap-2 mt-2">
67
+
{{ template "repo/fragments/reactionsPopUp" . }}
68
+
{{ range $kind := . }}
69
+
{{
70
+
template "repo/fragments/reaction"
71
+
(dict
72
+
"Kind" $kind
73
+
"Count" (index $.Reactions $kind)
74
+
"IsReacted" (index $.UserReacted $kind)
75
+
"ThreadAt" $.Pull.PullAt)
76
+
}}
77
+
{{ end }}
78
+
</div>
79
{{ end }}
80
</section>
81
+3
-4
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
+3
-4
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
···
1
{{ define "repo/pulls/fragments/pullNewComment" }}
2
-
<div
3
-
id="pull-comment-card-{{ .RoundNumber }}"
4
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2">
5
<div class="text-sm text-gray-500 dark:text-gray-400">
6
-
{{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }}
7
</div>
8
<form
9
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
···
38
</form>
39
</div>
40
{{ end }}
41
-
···
1
{{ define "repo/pulls/fragments/pullNewComment" }}
2
+
<div
3
+
id="pull-comment-card-{{ .RoundNumber }}"
4
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2">
5
<div class="text-sm text-gray-500 dark:text-gray-400">
6
+
{{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }}
7
</div>
8
<form
9
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
···
38
</form>
39
</div>
40
{{ end }}
+3
-2
appview/pages/templates/repo/pulls/fragments/pullStack.html
+3
-2
appview/pages/templates/repo/pulls/fragments/pullStack.html
···
10
{{ i "chevrons-down-up" "w-4 h-4" }}
11
</span>
12
STACK
13
-
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-sm ml-1">{{ len .Stack }}</span>
14
</span>
15
</summary>
16
{{ block "pullList" (list .Stack $) }} {{ end }}
···
41
<div class="grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
42
{{ range $pull := $list }}
43
{{ $isCurrent := false }}
44
{{ with $root.Pull }}
45
{{ $isCurrent = eq $pull.PullId $root.Pull.PullId }}
46
{{ end }}
···
52
</div>
53
{{ end }}
54
<div class="{{ if not $isCurrent }} pl-6 {{ end }} flex-grow min-w-0 w-full py-2">
55
-
{{ template "repo/pulls/fragments/summarizedHeader" $pull }}
56
</div>
57
</div>
58
</a>
···
10
{{ i "chevrons-down-up" "w-4 h-4" }}
11
</span>
12
STACK
13
+
<span class="bg-gray-200 dark:bg-gray-700 font-normal rounded py-1/2 px-1 text-sm">{{ len .Stack }}</span>
14
</span>
15
</summary>
16
{{ block "pullList" (list .Stack $) }} {{ end }}
···
41
<div class="grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
42
{{ range $pull := $list }}
43
{{ $isCurrent := false }}
44
+
{{ $pipeline := index $root.Pipelines $pull.LatestSha }}
45
{{ with $root.Pull }}
46
{{ $isCurrent = eq $pull.PullId $root.Pull.PullId }}
47
{{ end }}
···
53
</div>
54
{{ end }}
55
<div class="{{ if not $isCurrent }} pl-6 {{ end }} flex-grow min-w-0 w-full py-2">
56
+
{{ template "repo/pulls/fragments/summarizedHeader" (list $pull $pipeline) }}
57
</div>
58
</div>
59
</a>
+36
-26
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
+36
-26
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
···
1
{{ define "repo/pulls/fragments/summarizedHeader" }}
2
-
<div class="flex text-sm items-center justify-between w-full">
3
-
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
4
-
<div class="flex-shrink-0">
5
-
{{ template "repo/pulls/fragments/summarizedPullState" .State }}
6
</div>
7
-
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
8
-
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
9
-
{{ .Title }}
10
-
</span>
11
-
</div>
12
13
-
<div class="flex-shrink-0">
14
-
{{ $latestRound := .LastRoundNumber }}
15
-
{{ $lastSubmission := index .Submissions $latestRound }}
16
-
{{ $commentCount := len $lastSubmission.Comments }}
17
-
<span>
18
-
<div class="inline-flex items-center gap-2">
19
-
{{ i "message-square" "w-3 h-3 md:hidden" }}
20
-
{{ $commentCount }}
21
-
<span class="hidden md:inline">comment{{if ne $commentCount 1}}s{{end}}</span>
22
-
</div>
23
-
</span>
24
-
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
25
-
<span>
26
-
<span class="hidden md:inline">round</span>
27
-
<span class="font-mono">#{{ $latestRound }}</span>
28
-
</span>
29
</div>
30
-
</div>
31
{{ end }}
32
···
1
{{ define "repo/pulls/fragments/summarizedHeader" }}
2
+
{{ $pull := index . 0 }}
3
+
{{ $pipeline := index . 1 }}
4
+
{{ with $pull }}
5
+
<div class="flex text-sm items-center justify-between w-full">
6
+
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
7
+
<div class="flex-shrink-0">
8
+
{{ template "repo/pulls/fragments/summarizedPullState" .State }}
9
+
</div>
10
+
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
11
+
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
12
+
{{ .Title }}
13
+
</span>
14
</div>
15
16
+
<div class="flex-shrink-0 flex items-center">
17
+
{{ $latestRound := .LastRoundNumber }}
18
+
{{ $lastSubmission := index .Submissions $latestRound }}
19
+
{{ $commentCount := len $lastSubmission.Comments }}
20
+
{{ if $pipeline }}
21
+
<div class="inline-flex items-center gap-2">
22
+
{{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }}
23
+
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
24
+
</div>
25
+
{{ end }}
26
+
<span>
27
+
<div class="inline-flex items-center gap-2">
28
+
{{ i "message-square" "w-3 h-3 md:hidden" }}
29
+
{{ $commentCount }}
30
+
<span class="hidden md:inline">comment{{if ne $commentCount 1}}s{{end}}</span>
31
+
</div>
32
+
</span>
33
+
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
34
+
<span>
35
+
<span class="hidden md:inline">round</span>
36
+
<span class="font-mono">#{{ $latestRound }}</span>
37
+
</span>
38
+
</div>
39
</div>
40
+
{{ end }}
41
{{ end }}
42
+1
-1
appview/pages/templates/repo/pulls/new.html
+1
-1
appview/pages/templates/repo/pulls/new.html
+47
-8
appview/pages/templates/repo/pulls/pull.html
+47
-8
appview/pages/templates/repo/pulls/pull.html
···
5
{{ define "extrameta" }}
6
{{ $title := printf "%s · pull #%d · %s" .Pull.Title .Pull.PullId .RepoInfo.FullName }}
7
{{ $url := printf "https://tangled.sh/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }}
8
-
9
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
10
{{ end }}
11
···
46
</div>
47
<!-- round summary -->
48
<div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
49
-
<span>
50
{{ $owner := index $.DidHandleMap $.Pull.OwnerDid }}
51
{{ $re := "re" }}
52
{{ if eq .RoundNumber 0 }}
53
{{ $re = "" }}
54
{{ end }}
55
<span class="hidden md:inline">{{$re}}submitted</span>
56
-
by <a href="/{{ $owner }}">{{ $owner }}</a>
57
<span class="select-none before:content-['\00B7']"></span>
58
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}"><time>{{ .Created | shortTimeFmt }}</time></a>
59
<span class="select-none before:content-['ยท']"></span>
···
68
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
69
hx-boost="true"
70
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
71
-
{{ i "file-diff" "w-4 h-4" }}
72
<span class="hidden md:inline">diff</span>
73
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
74
</a>
···
150
{{ if gt $cidx 0 }}
151
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
152
{{ end }}
153
-
<div class="text-sm text-gray-500 dark:text-gray-400">
154
-
{{ $owner := index $.DidHandleMap $c.OwnerDid }}
155
-
<a href="/{{$owner}}">{{$owner}}</a>
156
<span class="before:content-['ยท']"></span>
157
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"><time>{{ $c.Created | shortTimeFmt }}</time></a>
158
</div>
···
162
</div>
163
{{ end }}
164
165
{{ if eq $lastIdx .RoundNumber }}
166
{{ block "mergeStatus" $ }} {{ end }}
167
{{ block "resubmitStatus" $ }} {{ end }}
···
260
{{ end }}
261
{{ end }}
262
263
-
{{ define "commits" }}
264
{{ end }}
···
5
{{ define "extrameta" }}
6
{{ $title := printf "%s · pull #%d · %s" .Pull.Title .Pull.PullId .RepoInfo.FullName }}
7
{{ $url := printf "https://tangled.sh/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }}
8
+
9
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
10
{{ end }}
11
···
46
</div>
47
<!-- round summary -->
48
<div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
49
+
<span class="gap-1 flex items-center">
50
{{ $owner := index $.DidHandleMap $.Pull.OwnerDid }}
51
{{ $re := "re" }}
52
{{ if eq .RoundNumber 0 }}
53
{{ $re = "" }}
54
{{ end }}
55
<span class="hidden md:inline">{{$re}}submitted</span>
56
+
by {{ template "user/fragments/picHandleLink" $owner }}
57
<span class="select-none before:content-['\00B7']"></span>
58
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}"><time>{{ .Created | shortTimeFmt }}</time></a>
59
<span class="select-none before:content-['ยท']"></span>
···
68
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
69
hx-boost="true"
70
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
71
+
{{ i "file-diff" "w-4 h-4" }}
72
<span class="hidden md:inline">diff</span>
73
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
74
</a>
···
150
{{ if gt $cidx 0 }}
151
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
152
{{ end }}
153
+
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1">
154
+
{{ $owner := index $.DidHandleMap $c.OwnerDid }}
155
+
{{ template "user/fragments/picHandleLink" $owner }}
156
<span class="before:content-['ยท']"></span>
157
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"><time>{{ $c.Created | shortTimeFmt }}</time></a>
158
</div>
···
162
</div>
163
{{ end }}
164
165
+
{{ block "pipelineStatus" (list $ .) }} {{ end }}
166
+
167
{{ if eq $lastIdx .RoundNumber }}
168
{{ block "mergeStatus" $ }} {{ end }}
169
{{ block "resubmitStatus" $ }} {{ end }}
···
262
{{ end }}
263
{{ end }}
264
265
+
{{ define "pipelineStatus" }}
266
+
{{ $root := index . 0 }}
267
+
{{ $submission := index . 1 }}
268
+
{{ $pipeline := index $root.Pipelines $submission.SourceRev }}
269
+
{{ with $pipeline }}
270
+
{{ $id := .Id }}
271
+
{{ if .Statuses }}
272
+
<div class="max-w-80 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
273
+
{{ range $name, $all := .Statuses }}
274
+
<a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
275
+
<div
276
+
class="flex gap-2 items-center justify-between p-2">
277
+
{{ $lastStatus := $all.Latest }}
278
+
{{ $kind := $lastStatus.Status.String }}
279
+
280
+
{{ $t := .TimeTaken }}
281
+
{{ $time := "" }}
282
+
283
+
{{ if $t }}
284
+
{{ $time = durationFmt $t }}
285
+
{{ else }}
286
+
{{ $time = printf "%s ago" (shortTimeFmt $lastStatus.Created) }}
287
+
{{ end }}
288
+
289
+
<div id="left" class="flex items-center gap-2 flex-shrink-0">
290
+
{{ template "repo/pipelines/fragments/workflowSymbol" $all }}
291
+
{{ $name }}
292
+
</div>
293
+
<div id="right" class="flex items-center gap-2 flex-shrink-0">
294
+
<span class="font-bold">{{ $kind }}</span>
295
+
<time>{{ $time }}</time>
296
+
</div>
297
+
</div>
298
+
</a>
299
+
{{ end }}
300
+
</div>
301
+
{{ end }}
302
+
{{ end }}
303
{{ end }}
+7
-7
appview/pages/templates/repo/pulls/pulls.html
+7
-7
appview/pages/templates/repo/pulls/pulls.html
···
3
{{ define "extrameta" }}
4
{{ $title := "pulls"}}
5
{{ $url := printf "https://tangled.sh/%s/pulls" .RepoInfo.FullName }}
6
-
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
8
{{ end }}
9
···
34
</div>
35
<a
36
href="/{{ .RepoInfo.FullName }}/pulls/new"
37
-
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"
38
>
39
{{ i "git-pull-request-create" "w-4 h-4" }}
40
<span>new</span>
···
54
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
55
</a>
56
</div>
57
-
<p class="text-sm text-gray-500 dark:text-gray-400">
58
{{ $owner := index $.DidHandleMap .OwnerDid }}
59
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
60
{{ $icon := "ban" }}
···
75
<span class="text-white">{{ .State.String }}</span>
76
</span>
77
78
-
<span>
79
-
<a href="/{{ $owner }}" class="dark:text-gray-300">{{ $owner }}</a>
80
</span>
81
82
-
<span class="before:content-['ยท']">
83
<time>
84
{{ .Created | timeFmt }}
85
</time>
···
156
<a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
157
<div class="flex gap-2 items-center px-6">
158
<div class="flex-grow min-w-0 w-full py-2">
159
-
{{ template "repo/pulls/fragments/summarizedHeader" $pull }}
160
</div>
161
</div>
162
</a>
···
3
{{ define "extrameta" }}
4
{{ $title := "pulls"}}
5
{{ $url := printf "https://tangled.sh/%s/pulls" .RepoInfo.FullName }}
6
+
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
8
{{ end }}
9
···
34
</div>
35
<a
36
href="/{{ .RepoInfo.FullName }}/pulls/new"
37
+
class="btn-create text-sm flex items-center gap-2 no-underline hover:no-underline hover:text-white"
38
>
39
{{ i "git-pull-request-create" "w-4 h-4" }}
40
<span>new</span>
···
54
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
55
</a>
56
</div>
57
+
<p class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
58
{{ $owner := index $.DidHandleMap .OwnerDid }}
59
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
60
{{ $icon := "ban" }}
···
75
<span class="text-white">{{ .State.String }}</span>
76
</span>
77
78
+
<span class="ml-1">
79
+
{{ template "user/fragments/picHandleLink" $owner }}
80
</span>
81
82
+
<span>
83
<time>
84
{{ .Created | timeFmt }}
85
</time>
···
156
<a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
157
<div class="flex gap-2 items-center px-6">
158
<div class="flex-grow min-w-0 w-full py-2">
159
+
{{ template "repo/pulls/fragments/summarizedHeader" (list $pull 0) }}
160
</div>
161
</div>
162
</a>
+7
-1
appview/pages/templates/repo/tree.html
+7
-1
appview/pages/templates/repo/tree.html
···
11
{{ template "repo/fragments/meta" . }}
12
{{ $title := printf "%s at %s · %s" $path .Ref .RepoInfo.FullName }}
13
{{ $url := printf "https://tangled.sh/%s/tree/%s%s" .RepoInfo.FullName .Ref $path }}
14
-
15
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
16
{{ end }}
17
···
63
</div>
64
</a>
65
{{ if .LastCommit}}
66
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
67
{{ end }}
68
</div>
69
</div>
···
80
</div>
81
</a>
82
{{ if .LastCommit}}
83
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
84
{{ end }}
85
</div>
86
</div>
···
11
{{ template "repo/fragments/meta" . }}
12
{{ $title := printf "%s at %s · %s" $path .Ref .RepoInfo.FullName }}
13
{{ $url := printf "https://tangled.sh/%s/tree/%s%s" .RepoInfo.FullName .Ref $path }}
14
+
15
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
16
{{ end }}
17
···
63
</div>
64
</a>
65
{{ if .LastCommit}}
66
+
<div class="flex items-end gap-2">
67
+
<span class="text text-gray-500 dark:text-gray-400 mr-6">{{ .LastCommit.Message }}</span>
68
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
69
+
</div>
70
{{ end }}
71
</div>
72
</div>
···
83
</div>
84
</a>
85
{{ if .LastCommit}}
86
+
<div class="flex items-end gap-2">
87
+
<span class="text text-gray-500 dark:text-gray-400 mr-6">{{ .LastCommit.Message }}</span>
88
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
89
+
</div>
90
{{ end }}
91
</div>
92
</div>
+9
-25
appview/pages/templates/timeline.html
+9
-25
appview/pages/templates/timeline.html
···
60
{{ if .Repo }}
61
{{ $userHandle := index $.DidHandleMap .Repo.Did }}
62
<div class="flex items-center">
63
-
<p class="text-gray-600 dark:text-gray-300">
64
-
<a
65
-
href="/{{ $userHandle }}"
66
-
class="no-underline hover:underline"
67
-
>{{ $userHandle | truncateAt30 }}</a
68
-
>
69
{{ if .Source }}
70
forked
71
<a
72
href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}"
73
class="no-underline hover:underline"
74
>
75
-
{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}
76
-
</a>
77
to
78
<a
79
href="/{{ $userHandle }}/{{ .Repo.Name }}"
···
98
{{ $userHandle := index $.DidHandleMap .Follow.UserDid }}
99
{{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }}
100
<div class="flex items-center">
101
-
<p class="text-gray-600 dark:text-gray-300">
102
-
<a
103
-
href="/{{ $userHandle }}"
104
-
class="no-underline hover:underline"
105
-
>{{ $userHandle | truncateAt30 }}</a
106
-
>
107
followed
108
-
<a
109
-
href="/{{ $subjectHandle }}"
110
-
class="no-underline hover:underline"
111
-
>{{ $subjectHandle | truncateAt30 }}</a
112
-
>
113
<time
114
class="text-gray-700 dark:text-gray-400 text-xs"
115
>{{ .Follow.FollowedAt | timeFmt }}</time
···
120
{{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }}
121
{{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }}
122
<div class="flex items-center">
123
-
<p class="text-gray-600 dark:text-gray-300">
124
-
<a
125
-
href="/{{ $starrerHandle }}"
126
-
class="no-underline hover:underline"
127
-
>{{ $starrerHandle | truncateAt30 }}</a
128
-
>
129
starred
130
<a
131
href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}"
···
60
{{ if .Repo }}
61
{{ $userHandle := index $.DidHandleMap .Repo.Did }}
62
<div class="flex items-center">
63
+
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
64
+
{{ template "user/fragments/picHandleLink" $userHandle }}
65
{{ if .Source }}
66
forked
67
<a
68
href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}"
69
class="no-underline hover:underline"
70
>
71
+
{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}</a
72
+
>
73
to
74
<a
75
href="/{{ $userHandle }}/{{ .Repo.Name }}"
···
94
{{ $userHandle := index $.DidHandleMap .Follow.UserDid }}
95
{{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }}
96
<div class="flex items-center">
97
+
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
98
+
{{ template "user/fragments/picHandleLink" $userHandle }}
99
followed
100
+
{{ template "user/fragments/picHandleLink" $subjectHandle }}
101
<time
102
class="text-gray-700 dark:text-gray-400 text-xs"
103
>{{ .Follow.FollowedAt | timeFmt }}</time
···
108
{{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }}
109
{{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }}
110
<div class="flex items-center">
111
+
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
112
+
{{ template "user/fragments/picHandleLink" $starrerHandle }}
113
starred
114
<a
115
href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}"
+8
appview/pages/templates/user/fragments/picHandle.html
+8
appview/pages/templates/user/fragments/picHandle.html
+5
appview/pages/templates/user/fragments/picHandleLink.html
+5
appview/pages/templates/user/fragments/picHandleLink.html
+70
-13
appview/pulls/pulls.go
+70
-13
appview/pulls/pulls.go
···
167
resubmitResult = s.resubmitCheck(f, pull, stack)
168
}
169
170
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
171
LoggedInUser: user,
172
-
RepoInfo: f.RepoInfo(user),
173
DidHandleMap: didHandleMap,
174
Pull: pull,
175
Stack: stack,
176
AbandonedPulls: abandonedPulls,
177
MergeCheck: mergeCheckResponse,
178
ResubmitCheck: resubmitResult,
179
})
180
}
181
···
447
}
448
}
449
450
-
w.Header().Set("Content-Type", "text/plain")
451
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
452
}
453
···
798
sourceBranch string,
799
isStacked bool,
800
) {
801
-
pullSource := &db.PullSource{
802
-
Branch: sourceBranch,
803
-
}
804
-
recordPullSource := &tangled.RepoPull_Source{
805
-
Branch: sourceBranch,
806
-
}
807
-
808
// Generate a patch using /compare
809
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
810
if err != nil {
···
828
return
829
}
830
831
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
832
}
833
···
914
return
915
}
916
917
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
918
Branch: sourceBranch,
919
RepoAt: &forkAtUri,
920
-
}, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked)
921
}
922
923
func (s *Pulls) createPullRequest(
···
934
) {
935
if isStacked {
936
// creates a series of PRs, each linking to the previous, identified by jj's change-id
937
-
s.createStackedPulLRequest(
938
w,
939
r,
940
f,
···
1049
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1050
}
1051
1052
-
func (s *Pulls) createStackedPulLRequest(
1053
w http.ResponseWriter,
1054
r *http.Request,
1055
f *reporesolver.ResolvedRepo,
···
1566
if pull.IsBranchBased() {
1567
recordPullSource = &tangled.RepoPull_Source{
1568
Branch: pull.PullSource.Branch,
1569
}
1570
}
1571
if pull.IsForkBased() {
···
1573
recordPullSource = &tangled.RepoPull_Source{
1574
Branch: pull.PullSource.Branch,
1575
Repo: &repoAt,
1576
}
1577
}
1578
···
167
resubmitResult = s.resubmitCheck(f, pull, stack)
168
}
169
170
+
repoInfo := f.RepoInfo(user)
171
+
172
+
m := make(map[string]db.Pipeline)
173
+
174
+
var shas []string
175
+
for _, s := range pull.Submissions {
176
+
shas = append(shas, s.SourceRev)
177
+
}
178
+
for _, p := range stack {
179
+
shas = append(shas, p.LatestSha())
180
+
}
181
+
for _, p := range abandonedPulls {
182
+
shas = append(shas, p.LatestSha())
183
+
}
184
+
185
+
ps, err := db.GetPipelineStatuses(
186
+
s.db,
187
+
db.FilterEq("repo_owner", repoInfo.OwnerDid),
188
+
db.FilterEq("repo_name", repoInfo.Name),
189
+
db.FilterEq("knot", repoInfo.Knot),
190
+
db.FilterIn("sha", shas),
191
+
)
192
+
if err != nil {
193
+
log.Printf("failed to fetch pipeline statuses: %s", err)
194
+
// non-fatal
195
+
}
196
+
197
+
for _, p := range ps {
198
+
m[p.Sha] = p
199
+
}
200
+
201
+
reactionCountMap, err := db.GetReactionCountMap(s.db, pull.PullAt())
202
+
if err != nil {
203
+
log.Println("failed to get pull reactions")
204
+
s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.")
205
+
}
206
+
207
+
userReactions := map[db.ReactionKind]bool{}
208
+
if user != nil {
209
+
userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt())
210
+
}
211
+
212
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
213
LoggedInUser: user,
214
+
RepoInfo: repoInfo,
215
DidHandleMap: didHandleMap,
216
Pull: pull,
217
Stack: stack,
218
AbandonedPulls: abandonedPulls,
219
MergeCheck: mergeCheckResponse,
220
ResubmitCheck: resubmitResult,
221
+
Pipelines: m,
222
+
223
+
OrderedReactionKinds: db.OrderedReactionKinds,
224
+
Reactions: reactionCountMap,
225
+
UserReacted: userReactions,
226
})
227
}
228
···
494
}
495
}
496
497
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
498
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
499
}
500
···
845
sourceBranch string,
846
isStacked bool,
847
) {
848
// Generate a patch using /compare
849
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
850
if err != nil {
···
868
return
869
}
870
871
+
pullSource := &db.PullSource{
872
+
Branch: sourceBranch,
873
+
}
874
+
recordPullSource := &tangled.RepoPull_Source{
875
+
Branch: sourceBranch,
876
+
Sha: comparison.Rev2,
877
+
}
878
+
879
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
880
}
881
···
962
return
963
}
964
965
+
pullSource := &db.PullSource{
966
Branch: sourceBranch,
967
RepoAt: &forkAtUri,
968
+
}
969
+
recordPullSource := &tangled.RepoPull_Source{
970
+
Branch: sourceBranch,
971
+
Repo: &fork.AtUri,
972
+
Sha: sourceRev,
973
+
}
974
+
975
+
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked)
976
}
977
978
func (s *Pulls) createPullRequest(
···
989
) {
990
if isStacked {
991
// creates a series of PRs, each linking to the previous, identified by jj's change-id
992
+
s.createStackedPullRequest(
993
w,
994
r,
995
f,
···
1104
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1105
}
1106
1107
+
func (s *Pulls) createStackedPullRequest(
1108
w http.ResponseWriter,
1109
r *http.Request,
1110
f *reporesolver.ResolvedRepo,
···
1621
if pull.IsBranchBased() {
1622
recordPullSource = &tangled.RepoPull_Source{
1623
Branch: pull.PullSource.Branch,
1624
+
Sha: sourceRev,
1625
}
1626
}
1627
if pull.IsForkBased() {
···
1629
recordPullSource = &tangled.RepoPull_Source{
1630
Branch: pull.PullSource.Branch,
1631
Repo: &repoAt,
1632
+
Sha: sourceRev,
1633
}
1634
}
1635
+55
-3
appview/repo/index.go
+55
-3
appview/repo/index.go
···
6
"log"
7
"net/http"
8
"slices"
9
"strings"
10
11
"tangled.sh/tangled.sh/core/appview/commitverify"
···
18
"tangled.sh/tangled.sh/core/types"
19
20
"github.com/go-chi/chi/v5"
21
)
22
23
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
···
121
}
122
}
123
124
-
repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref)
125
if err != nil {
126
log.Printf("failed to compute language percentages: %s", err)
127
// non-fatal
···
131
for _, c := range commitsTrunc {
132
shas = append(shas, c.Hash.String())
133
}
134
-
pipelines, err := rp.getPipelineStatuses(repoInfo, shas)
135
if err != nil {
136
log.Printf("failed to fetch pipeline statuses: %s", err)
137
// non-fatal
···
148
BranchesTrunc: branchesTrunc,
149
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
150
VerifiedCommits: vc,
151
-
Languages: repoLanguages,
152
Pipelines: pipelines,
153
})
154
return
155
}
156
157
func getForkInfo(
···
6
"log"
7
"net/http"
8
"slices"
9
+
"sort"
10
"strings"
11
12
"tangled.sh/tangled.sh/core/appview/commitverify"
···
19
"tangled.sh/tangled.sh/core/types"
20
21
"github.com/go-chi/chi/v5"
22
+
"github.com/go-enry/go-enry/v2"
23
)
24
25
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
···
123
}
124
}
125
126
+
languageInfo, err := getLanguageInfo(f, signedClient, ref)
127
if err != nil {
128
log.Printf("failed to compute language percentages: %s", err)
129
// non-fatal
···
133
for _, c := range commitsTrunc {
134
shas = append(shas, c.Hash.String())
135
}
136
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
137
if err != nil {
138
log.Printf("failed to fetch pipeline statuses: %s", err)
139
// non-fatal
···
150
BranchesTrunc: branchesTrunc,
151
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
152
VerifiedCommits: vc,
153
+
Languages: languageInfo,
154
Pipelines: pipelines,
155
})
156
return
157
+
}
158
+
159
+
func getLanguageInfo(
160
+
f *reporesolver.ResolvedRepo,
161
+
signedClient *knotclient.SignedClient,
162
+
ref string,
163
+
) ([]types.RepoLanguageDetails, error) {
164
+
repoLanguages, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, ref)
165
+
if err != nil {
166
+
return []types.RepoLanguageDetails{}, err
167
+
}
168
+
if repoLanguages == nil {
169
+
repoLanguages = &types.RepoLanguageResponse{Languages: make(map[string]int64)}
170
+
}
171
+
172
+
var totalSize int64
173
+
for _, fileSize := range repoLanguages.Languages {
174
+
totalSize += fileSize
175
+
}
176
+
177
+
var languageStats []types.RepoLanguageDetails
178
+
var otherPercentage float32 = 0
179
+
180
+
for lang, size := range repoLanguages.Languages {
181
+
percentage := (float32(size) / float32(totalSize)) * 100
182
+
183
+
if percentage <= 0.5 {
184
+
otherPercentage += percentage
185
+
continue
186
+
}
187
+
188
+
color := enry.GetColor(lang)
189
+
190
+
languageStats = append(languageStats, types.RepoLanguageDetails{Name: lang, Percentage: percentage, Color: color})
191
+
}
192
+
193
+
sort.Slice(languageStats, func(i, j int) bool {
194
+
if languageStats[i].Name == enry.OtherLanguage {
195
+
return false
196
+
}
197
+
if languageStats[j].Name == enry.OtherLanguage {
198
+
return true
199
+
}
200
+
if languageStats[i].Percentage != languageStats[j].Percentage {
201
+
return languageStats[i].Percentage > languageStats[j].Percentage
202
+
}
203
+
return languageStats[i].Name < languageStats[j].Name
204
+
})
205
+
206
+
return languageStats, nil
207
}
208
209
func getForkInfo(
+3
-3
appview/repo/repo.go
+3
-3
appview/repo/repo.go
···
139
for _, c := range repolog.Commits {
140
shas = append(shas, c.Hash.String())
141
}
142
-
pipelines, err := rp.getPipelineStatuses(repoInfo, shas)
143
if err != nil {
144
log.Println(err)
145
// non-fatal
···
304
305
user := rp.oauth.GetUser(r)
306
repoInfo := f.RepoInfo(user)
307
-
pipelines, err := rp.getPipelineStatuses(repoInfo, []string{result.Diff.Commit.This})
308
if err != nil {
309
log.Println(err)
310
// non-fatal
···
590
return
591
}
592
593
-
w.Header().Set("Content-Type", "text/plain")
594
w.Write([]byte(result.Contents))
595
return
596
}
···
139
for _, c := range repolog.Commits {
140
shas = append(shas, c.Hash.String())
141
}
142
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
143
if err != nil {
144
log.Println(err)
145
// non-fatal
···
304
305
user := rp.oauth.GetUser(r)
306
repoInfo := f.RepoInfo(user)
307
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
308
if err != nil {
309
log.Println(err)
310
// non-fatal
···
590
return
591
}
592
593
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
594
w.Write([]byte(result.Contents))
595
return
596
}
+3
-2
appview/repo/repo_util.go
+3
-2
appview/repo/repo_util.go
···
105
// grab pipelines from DB and munge that into a hashmap with commit sha as key
106
//
107
// golang is so blessed that it requires 35 lines of imperative code for this
108
-
func (rp *Repo) getPipelineStatuses(
109
repoInfo repoinfo.RepoInfo,
110
shas []string,
111
) (map[string]db.Pipeline, error) {
···
116
}
117
118
ps, err := db.GetPipelineStatuses(
119
-
rp.db,
120
db.FilterEq("repo_owner", repoInfo.OwnerDid),
121
db.FilterEq("repo_name", repoInfo.Name),
122
db.FilterEq("knot", repoInfo.Knot),
···
105
// grab pipelines from DB and munge that into a hashmap with commit sha as key
106
//
107
// golang is so blessed that it requires 35 lines of imperative code for this
108
+
func getPipelineStatuses(
109
+
d *db.DB,
110
repoInfo repoinfo.RepoInfo,
111
shas []string,
112
) (map[string]db.Pipeline, error) {
···
117
}
118
119
ps, err := db.GetPipelineStatuses(
120
+
d,
121
db.FilterEq("repo_owner", repoInfo.OwnerDid),
122
db.FilterEq("repo_name", repoInfo.Name),
123
db.FilterEq("knot", repoInfo.Knot),
+7
-8
appview/state/knotstream.go
+7
-8
appview/state/knotstream.go
···
143
// trigger info
144
var trigger db.Trigger
145
var sha string
146
-
switch record.TriggerMetadata.Kind {
147
case workflow.TriggerKindPush:
148
-
trigger.Kind = workflow.TriggerKindPush
149
trigger.PushRef = &record.TriggerMetadata.Push.Ref
150
trigger.PushNewSha = &record.TriggerMetadata.Push.NewSha
151
trigger.PushOldSha = &record.TriggerMetadata.Push.OldSha
152
sha = *trigger.PushNewSha
153
case workflow.TriggerKindPullRequest:
154
-
trigger.Kind = workflow.TriggerKindPush
155
trigger.PRSourceBranch = &record.TriggerMetadata.PullRequest.SourceBranch
156
trigger.PRTargetBranch = &record.TriggerMetadata.PullRequest.TargetBranch
157
trigger.PRSourceSha = &record.TriggerMetadata.PullRequest.SourceSha
···
161
162
tx, err := d.Begin()
163
if err != nil {
164
-
return err
165
}
166
167
triggerId, err := db.AddTrigger(tx, trigger)
168
if err != nil {
169
-
return err
170
}
171
172
pipeline := db.Pipeline{
···
180
181
err = db.AddPipeline(tx, pipeline)
182
if err != nil {
183
-
return err
184
}
185
186
err = tx.Commit()
187
if err != nil {
188
-
return err
189
}
190
191
-
return err
192
}
···
143
// trigger info
144
var trigger db.Trigger
145
var sha string
146
+
trigger.Kind = workflow.TriggerKind(record.TriggerMetadata.Kind)
147
+
switch trigger.Kind {
148
case workflow.TriggerKindPush:
149
trigger.PushRef = &record.TriggerMetadata.Push.Ref
150
trigger.PushNewSha = &record.TriggerMetadata.Push.NewSha
151
trigger.PushOldSha = &record.TriggerMetadata.Push.OldSha
152
sha = *trigger.PushNewSha
153
case workflow.TriggerKindPullRequest:
154
trigger.PRSourceBranch = &record.TriggerMetadata.PullRequest.SourceBranch
155
trigger.PRTargetBranch = &record.TriggerMetadata.PullRequest.TargetBranch
156
trigger.PRSourceSha = &record.TriggerMetadata.PullRequest.SourceSha
···
160
161
tx, err := d.Begin()
162
if err != nil {
163
+
return fmt.Errorf("failed to start txn: %w", err)
164
}
165
166
triggerId, err := db.AddTrigger(tx, trigger)
167
if err != nil {
168
+
return fmt.Errorf("failed to add trigger entry: %w", err)
169
}
170
171
pipeline := db.Pipeline{
···
179
180
err = db.AddPipeline(tx, pipeline)
181
if err != nil {
182
+
return fmt.Errorf("failed to add pipeline: %w", err)
183
}
184
185
err = tx.Commit()
186
if err != nil {
187
+
return fmt.Errorf("failed to commit txn: %w", err)
188
}
189
190
+
return nil
191
}
+126
appview/state/reaction.go
+126
appview/state/reaction.go
···
···
1
+
package state
2
+
3
+
import (
4
+
"log"
5
+
"net/http"
6
+
"time"
7
+
8
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
11
+
lexutil "github.com/bluesky-social/indigo/lex/util"
12
+
"tangled.sh/tangled.sh/core/api/tangled"
13
+
"tangled.sh/tangled.sh/core/appview"
14
+
"tangled.sh/tangled.sh/core/appview/db"
15
+
"tangled.sh/tangled.sh/core/appview/pages"
16
+
)
17
+
18
+
func (s *State) React(w http.ResponseWriter, r *http.Request) {
19
+
currentUser := s.oauth.GetUser(r)
20
+
21
+
subject := r.URL.Query().Get("subject")
22
+
if subject == "" {
23
+
log.Println("invalid form")
24
+
return
25
+
}
26
+
27
+
subjectUri, err := syntax.ParseATURI(subject)
28
+
if err != nil {
29
+
log.Println("invalid form")
30
+
return
31
+
}
32
+
33
+
reactionKind, ok := db.ParseReactionKind(r.URL.Query().Get("kind"))
34
+
if !ok {
35
+
log.Println("invalid reaction kind")
36
+
return
37
+
}
38
+
39
+
client, err := s.oauth.AuthorizedClient(r)
40
+
if err != nil {
41
+
log.Println("failed to authorize client", err)
42
+
return
43
+
}
44
+
45
+
switch r.Method {
46
+
case http.MethodPost:
47
+
createdAt := time.Now().Format(time.RFC3339)
48
+
rkey := appview.TID()
49
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
50
+
Collection: tangled.FeedReactionNSID,
51
+
Repo: currentUser.Did,
52
+
Rkey: rkey,
53
+
Record: &lexutil.LexiconTypeDecoder{
54
+
Val: &tangled.FeedReaction{
55
+
Subject: subjectUri.String(),
56
+
Reaction: reactionKind.String(),
57
+
CreatedAt: createdAt,
58
+
},
59
+
},
60
+
})
61
+
if err != nil {
62
+
log.Println("failed to create atproto record", err)
63
+
return
64
+
}
65
+
66
+
err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey)
67
+
if err != nil {
68
+
log.Println("failed to react", err)
69
+
return
70
+
}
71
+
72
+
count, err := db.GetReactionCount(s.db, subjectUri, reactionKind)
73
+
if err != nil {
74
+
log.Println("failed to get reaction count for ", subjectUri)
75
+
}
76
+
77
+
log.Println("created atproto record: ", resp.Uri)
78
+
79
+
s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{
80
+
ThreadAt: subjectUri,
81
+
Kind: reactionKind,
82
+
Count: count,
83
+
IsReacted: true,
84
+
})
85
+
86
+
return
87
+
case http.MethodDelete:
88
+
reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind)
89
+
if err != nil {
90
+
log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri)
91
+
return
92
+
}
93
+
94
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
95
+
Collection: tangled.FeedReactionNSID,
96
+
Repo: currentUser.Did,
97
+
Rkey: reaction.Rkey,
98
+
})
99
+
100
+
if err != nil {
101
+
log.Println("failed to remove reaction")
102
+
return
103
+
}
104
+
105
+
err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey)
106
+
if err != nil {
107
+
log.Println("failed to delete reaction from DB")
108
+
// this is not an issue, the firehose event might have already done this
109
+
}
110
+
111
+
count, err := db.GetReactionCount(s.db, subjectUri, reactionKind)
112
+
if err != nil {
113
+
log.Println("failed to get reaction count for ", subjectUri)
114
+
return
115
+
}
116
+
117
+
s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{
118
+
ThreadAt: subjectUri,
119
+
Kind: reactionKind,
120
+
Count: count,
121
+
IsReacted: false,
122
+
})
123
+
124
+
return
125
+
}
126
+
}
+5
appview/state/router.go
+5
appview/state/router.go
···
137
r.Delete("/", s.Star)
138
})
139
140
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) {
141
+
r.Post("/", s.React)
142
+
r.Delete("/", s.React)
143
+
})
144
+
145
r.Route("/profile", func(r chi.Router) {
146
r.Use(middleware.AuthMiddleware(s.oauth))
147
r.Get("/edit-bio", s.EditBioFragment)
+7
-1
appview/state/spindlestream.go
+7
-1
appview/state/spindlestream.go
+90
-69
avatar/src/index.js
+90
-69
avatar/src/index.js
···
1
export default {
2
-
async fetch(request, env) {
3
-
const url = new URL(request.url);
4
-
const { pathname } = url;
5
6
-
if (!pathname || pathname === '/') {
7
-
return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
8
-
You can't use this directly unforunately since all requests are signed and may only originate from the appview.`);
9
-
}
10
11
-
const cache = caches.default;
12
13
-
let cacheKey = request.url;
14
-
let response = await cache.match(cacheKey);
15
-
if (response) {
16
-
return response;
17
-
}
18
19
-
const pathParts = pathname.slice(1).split('/');
20
-
if (pathParts.length < 2) {
21
-
return new Response('Bad URL', { status: 400 });
22
-
}
23
24
-
const [signatureHex, actor] = pathParts;
25
26
-
const actorBytes = new TextEncoder().encode(actor);
27
28
-
const key = await crypto.subtle.importKey(
29
-
'raw',
30
-
new TextEncoder().encode(env.AVATAR_SHARED_SECRET),
31
-
{ name: 'HMAC', hash: 'SHA-256' },
32
-
false,
33
-
['sign', 'verify'],
34
-
);
35
36
-
const computedSigBuffer = await crypto.subtle.sign('HMAC', key, actorBytes);
37
-
const computedSig = Array.from(new Uint8Array(computedSigBuffer))
38
-
.map((b) => b.toString(16).padStart(2, '0'))
39
-
.join('');
40
41
-
console.log({
42
-
level: 'debug',
43
-
message: 'avatar request for: ' + actor,
44
-
computedSignature: computedSig,
45
-
providedSignature: signatureHex,
46
-
});
47
48
-
const sigBytes = Uint8Array.from(signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)));
49
-
const valid = await crypto.subtle.verify('HMAC', key, sigBytes, actorBytes);
50
51
-
if (!valid) {
52
-
return new Response('Invalid signature', { status: 403 });
53
-
}
54
55
-
try {
56
-
const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, { method: 'GET' });
57
-
const profile = await profileResponse.json();
58
-
const avatar = profile.avatar;
59
60
-
if (!avatar) {
61
-
return new Response(`avatar not found for ${actor}.`, { status: 404 });
62
-
}
63
64
-
// fetch the actual avatar image
65
-
const avatarResponse = await fetch(avatar);
66
-
if (!avatarResponse.ok) {
67
-
return new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse.status });
68
-
}
69
70
-
const avatarData = await avatarResponse.arrayBuffer();
71
-
const contentType = avatarResponse.headers.get('content-type') || 'image/jpeg';
72
73
-
response = new Response(avatarData, {
74
-
headers: {
75
-
'Content-Type': contentType,
76
-
'Cache-Control': 'public, max-age=43200', // 12 h
77
-
},
78
-
});
79
80
-
// cache it in cf using request.url as the key
81
-
await cache.put(cacheKey, response.clone());
82
-
83
-
return response;
84
-
} catch (error) {
85
-
return new Response(`error fetching avatar: ${error.message}`, { status: 500 });
86
-
}
87
-
},
88
};
···
1
export default {
2
+
async fetch(request, env) {
3
+
const url = new URL(request.url);
4
+
const { pathname, searchParams } = url;
5
6
+
if (!pathname || pathname === "/") {
7
+
return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
8
+
You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`);
9
+
}
10
11
+
const size = searchParams.get("size");
12
+
const resizeToTiny = size === "tiny";
13
14
+
const cache = caches.default;
15
+
let cacheKey = request.url;
16
+
let response = await cache.match(cacheKey);
17
+
if (response) return response;
18
19
+
const pathParts = pathname.slice(1).split("/");
20
+
if (pathParts.length < 2) {
21
+
return new Response("Bad URL", { status: 400 });
22
+
}
23
24
+
const [signatureHex, actor] = pathParts;
25
+
const actorBytes = new TextEncoder().encode(actor);
26
27
+
const key = await crypto.subtle.importKey(
28
+
"raw",
29
+
new TextEncoder().encode(env.AVATAR_SHARED_SECRET),
30
+
{ name: "HMAC", hash: "SHA-256" },
31
+
false,
32
+
["sign", "verify"],
33
+
);
34
35
+
const computedSigBuffer = await crypto.subtle.sign("HMAC", key, actorBytes);
36
+
const computedSig = Array.from(new Uint8Array(computedSigBuffer))
37
+
.map((b) => b.toString(16).padStart(2, "0"))
38
+
.join("");
39
40
+
console.log({
41
+
level: "debug",
42
+
message: "avatar request for: " + actor,
43
+
computedSignature: computedSig,
44
+
providedSignature: signatureHex,
45
+
});
46
47
+
const sigBytes = Uint8Array.from(
48
+
signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)),
49
+
);
50
+
const valid = await crypto.subtle.verify("HMAC", key, sigBytes, actorBytes);
51
52
+
if (!valid) {
53
+
return new Response("Invalid signature", { status: 403 });
54
+
}
55
56
+
try {
57
+
const profileResponse = await fetch(
58
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
59
+
);
60
+
const profile = await profileResponse.json();
61
+
const avatar = profile.avatar;
62
63
+
if (!avatar) {
64
+
return new Response(`avatar not found for ${actor}.`, { status: 404 });
65
+
}
66
67
+
// Resize if requested
68
+
let avatarResponse;
69
+
if (resizeToTiny) {
70
+
avatarResponse = await fetch(avatar, {
71
+
cf: {
72
+
image: {
73
+
width: 32,
74
+
height: 32,
75
+
fit: "cover",
76
+
format: "webp",
77
+
},
78
+
},
79
+
});
80
+
} else {
81
+
avatarResponse = await fetch(avatar);
82
+
}
83
84
+
if (!avatarResponse.ok) {
85
+
return new Response(`failed to fetch avatar for ${actor}.`, {
86
+
status: avatarResponse.status,
87
+
});
88
+
}
89
90
+
const avatarData = await avatarResponse.arrayBuffer();
91
+
const contentType =
92
+
avatarResponse.headers.get("content-type") || "image/jpeg";
93
94
+
response = new Response(avatarData, {
95
+
headers: {
96
+
"Content-Type": contentType,
97
+
"Cache-Control": "public, max-age=43200",
98
+
},
99
+
});
100
101
+
await cache.put(cacheKey, response.clone());
102
+
return response;
103
+
} catch (error) {
104
+
return new Response(`error fetching avatar: ${error.message}`, {
105
+
status: 500,
106
+
});
107
+
}
108
+
},
109
};
-50
cmd/eventconsumer/main.go
-50
cmd/eventconsumer/main.go
···
1
-
package main
2
-
3
-
import (
4
-
"context"
5
-
"flag"
6
-
"fmt"
7
-
"strings"
8
-
"time"
9
-
10
-
"tangled.sh/tangled.sh/core/knotclient"
11
-
)
12
-
13
-
func main() {
14
-
knots := flag.String("knots", "", "list of knots to connect to")
15
-
retryFlag := flag.Duration("retry", 1*time.Minute, "retry interval")
16
-
maxRetryFlag := flag.Duration("max-retry", 30*time.Minute, "max retry interval")
17
-
workerCount := flag.Int("workers", 10, "goroutine pool size")
18
-
19
-
flag.Parse()
20
-
21
-
if *knots == "" {
22
-
fmt.Println("error: -knots is required")
23
-
flag.Usage()
24
-
return
25
-
}
26
-
27
-
ccfg := knotclient.ConsumerConfig{
28
-
ProcessFunc: processEvent,
29
-
RetryInterval: *retryFlag,
30
-
MaxRetryInterval: *maxRetryFlag,
31
-
WorkerCount: *workerCount,
32
-
Dev: true,
33
-
}
34
-
for k := range strings.SplitSeq(*knots, ",") {
35
-
ccfg.AddEventSource(knotclient.NewEventSource(k))
36
-
}
37
-
38
-
consumer := knotclient.NewEventConsumer(ccfg)
39
-
40
-
ctx, cancel := context.WithCancel(context.Background())
41
-
consumer.Start(ctx)
42
-
time.Sleep(1 * time.Hour)
43
-
cancel()
44
-
consumer.Stop()
45
-
}
46
-
47
-
func processEvent(_ context.Context, source knotclient.EventSource, msg knotclient.Message) error {
48
-
fmt.Printf("From %s (%s, %s): %s\n", source.Knot, msg.Rkey, msg.Nsid, string(msg.EventJson))
49
-
return nil
50
-
}
···
+3
-4
cmd/gen.go
+3
-4
cmd/gen.go
···
15
"api/tangled/cbor_gen.go",
16
"tangled",
17
tangled.ActorProfile{},
18
tangled.FeedStar{},
19
tangled.GitRefUpdate{},
20
tangled.GitRefUpdate_Meta{},
···
24
tangled.KnotMember{},
25
tangled.Pipeline{},
26
tangled.Pipeline_CloneOpts{},
27
-
tangled.Pipeline_Dependencies_Elem{},
28
tangled.Pipeline_ManualTriggerData{},
29
-
tangled.Pipeline_ManualTriggerData_Inputs_Elem{},
30
tangled.Pipeline_PullRequestTriggerData{},
31
tangled.Pipeline_PushTriggerData{},
32
-
tangled.Pipeline_Step_Environment_Elem{},
33
tangled.PipelineStatus{},
34
tangled.Pipeline_Step{},
35
tangled.Pipeline_TriggerMetadata{},
36
tangled.Pipeline_TriggerRepo{},
37
tangled.Pipeline_Workflow{},
38
-
tangled.Pipeline_Workflow_Environment_Elem{},
39
tangled.PublicKey{},
40
tangled.Repo{},
41
tangled.RepoArtifact{},
···
15
"api/tangled/cbor_gen.go",
16
"tangled",
17
tangled.ActorProfile{},
18
+
tangled.FeedReaction{},
19
tangled.FeedStar{},
20
tangled.GitRefUpdate{},
21
tangled.GitRefUpdate_Meta{},
···
25
tangled.KnotMember{},
26
tangled.Pipeline{},
27
tangled.Pipeline_CloneOpts{},
28
+
tangled.Pipeline_Dependency{},
29
tangled.Pipeline_ManualTriggerData{},
30
+
tangled.Pipeline_Pair{},
31
tangled.Pipeline_PullRequestTriggerData{},
32
tangled.Pipeline_PushTriggerData{},
33
tangled.PipelineStatus{},
34
tangled.Pipeline_Step{},
35
tangled.Pipeline_TriggerMetadata{},
36
tangled.Pipeline_TriggerRepo{},
37
tangled.Pipeline_Workflow{},
38
tangled.PublicKey{},
39
tangled.Repo{},
40
tangled.RepoArtifact{},
+1
-1
cmd/punchcardPopulate/main.go
+1
-1
cmd/punchcardPopulate/main.go
+5
-4
docs/hacking.md
+5
-4
docs/hacking.md
···
47
`nixos-shell` like so:
48
49
```bash
50
-
QEMU_NET_OPTS="hostfwd=tcp::6000-:6000,hostfwd=tcp::2222-:22" nixos-shell --flake .#knotVM
51
52
# hit Ctrl-a + c + q to exit the VM
53
```
54
55
-
This starts a knot on port 6000 with `ssh` exposed on port
56
-
2222. You can push repositories to this VM with this ssh
57
-
config block on your main machine:
58
59
```bash
60
Host nixos-shell
···
47
`nixos-shell` like so:
48
49
```bash
50
+
nix run .#vm
51
+
# or nixos-shell --flake .#vm
52
53
# hit Ctrl-a + c + q to exit the VM
54
```
55
56
+
This starts a knot on port 6000, a spindle on port 6555
57
+
with `ssh` exposed on port 2222. You can push repositories
58
+
to this VM with this ssh config block on your main machine:
59
60
```bash
61
Host nixos-shell
+33
docs/knot-hosting.md
+33
docs/knot-hosting.md
···
89
systemctl start knotserver
90
```
91
92
+
The last step is to configure a reverse proxy like Nginx or Caddy to front yourself
93
+
knot. Here's an example configuration for Nginx:
94
+
95
+
```
96
+
server {
97
+
listen 80;
98
+
listen [::]:80;
99
+
server_name knot.example.com;
100
+
101
+
location / {
102
+
proxy_pass http://localhost:5555;
103
+
proxy_set_header Host $host;
104
+
proxy_set_header X-Real-IP $remote_addr;
105
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
106
+
proxy_set_header X-Forwarded-Proto $scheme;
107
+
}
108
+
109
+
# wss endpoint for git events
110
+
location /events {
111
+
proxy_set_header X-Forwarded-For $remote_addr;
112
+
proxy_set_header Host $http_host;
113
+
proxy_set_header Upgrade websocket;
114
+
proxy_set_header Connection Upgrade;
115
+
proxy_pass http://localhost:5555;
116
+
}
117
+
# additional config for SSL/TLS go here.
118
+
}
119
+
120
+
```
121
+
122
+
Remember to use Let's Encrypt or similar to procure a certificate for your
123
+
knot domain.
124
+
125
You should now have a running knot server! You can finalize your registration by hitting the
126
`initialize` button on the [/knots](/knots) page.
127
+24
docs/spindle/architecture.md
+24
docs/spindle/architecture.md
···
···
1
+
# spindle architecture
2
+
3
+
Spindle is a small CI runner service. Here's a high level overview of how it operates:
4
+
5
+
* listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
6
+
[`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream.
7
+
* when a new repo record comes through (typically when you add a spindle to a
8
+
repo from the settings), spindle then resolves the underlying knot and
9
+
subscribes to repo events (see:
10
+
[`sh.tangled.pipeline`](/lexicons/pipeline.json)).
11
+
* the spindle engine then handles execution of the pipeline, with results and
12
+
logs beamed on the spindle event stream over wss
13
+
14
+
### the engine
15
+
16
+
At present, the only supported backend is Docker. Spindle executes each step in
17
+
the pipeline in a fresh container, with state persisted across steps within the
18
+
`/tangled/workspace` directory.
19
+
20
+
The base image for the container is constructed on the fly using
21
+
[Nixery](https://nixery.dev), which is handy for caching layers for frequently
22
+
used packages.
23
+
24
+
The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
+52
docs/spindle/hosting.md
+52
docs/spindle/hosting.md
···
···
1
+
# spindle self-hosting guide
2
+
3
+
## prerequisites
4
+
5
+
* Go
6
+
* Docker (the only supported backend currently)
7
+
8
+
## configuration
9
+
10
+
Spindle is configured using environment variables. The following environment variables are available:
11
+
12
+
* `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`).
13
+
* `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`).
14
+
* `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required).
15
+
* `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`).
16
+
* `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`).
17
+
* `SPINDLE_SERVER_OWNER`: The DID of the owner (required).
18
+
* `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`).
19
+
* `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`).
20
+
* `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`).
21
+
22
+
## running spindle
23
+
24
+
1. **Set the environment variables.** For example:
25
+
26
+
```shell
27
+
export SPINDLE_SERVER_HOSTNAME="your-hostname"
28
+
export SPINDLE_SERVER_OWNER="your-did"
29
+
```
30
+
31
+
2. **Build the Spindle binary.**
32
+
33
+
```shell
34
+
cd core
35
+
go mod download
36
+
go build -o cmd/spindle/spindle cmd/spindle/main.go
37
+
```
38
+
39
+
3. **Create the log directory.**
40
+
41
+
```shell
42
+
sudo mkdir -p /var/log/spindle
43
+
sudo chown $USER:$USER -R /var/log/spindle
44
+
```
45
+
46
+
4. **Run the Spindle binary.**
47
+
48
+
```shell
49
+
./cmd/spindle/spindle
50
+
```
51
+
52
+
Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
+59
docs/spindle/pipeline.md
+59
docs/spindle/pipeline.md
···
···
1
+
# spindle pipeline manifest
2
+
3
+
Spindle pipelines are defined under the `.tangled/workflows` directory in a
4
+
repo. Generally:
5
+
6
+
* Pipelines are defined in YAML.
7
+
* Dependencies can be specified from
8
+
[Nixpkgs](https://search.nixos.org) or custom registries.
9
+
* Environment variables can be set globally or per-step.
10
+
11
+
Here's an example that uses all fields:
12
+
13
+
```yaml
14
+
# build_and_test.yaml
15
+
when:
16
+
- event: ["push", "pull_request"]
17
+
branch: ["main", "develop"]
18
+
- event: ["manual"]
19
+
20
+
dependencies:
21
+
## from nixpkgs
22
+
nixpkgs:
23
+
- nodejs
24
+
## custom registry
25
+
git+https://tangled.sh/@oppi.li/statix:
26
+
- statix
27
+
28
+
steps:
29
+
- name: "Install dependencies"
30
+
command: "npm install"
31
+
environment:
32
+
NODE_ENV: "development"
33
+
CI: "true"
34
+
35
+
- name: "Run linter"
36
+
command: "npm run lint"
37
+
38
+
- name: "Run tests"
39
+
command: "npm test"
40
+
environment:
41
+
NODE_ENV: "test"
42
+
JEST_WORKERS: "2"
43
+
44
+
- name: "Build application"
45
+
command: "npm run build"
46
+
environment:
47
+
NODE_ENV: "production"
48
+
49
+
environment:
50
+
BUILD_NUMBER: "123"
51
+
GIT_BRANCH: "main"
52
+
53
+
## current repository is cloned and checked out at the target ref
54
+
## by default.
55
+
clone:
56
+
skip: false
57
+
depth: 50
58
+
submodules: true
59
+
```
+43
-25
eventconsumer/consumer.go
+43
-25
eventconsumer/consumer.go
···
12
"tangled.sh/tangled.sh/core/eventconsumer/cursor"
13
"tangled.sh/tangled.sh/core/log"
14
15
"github.com/gorilla/websocket"
16
)
17
···
170
171
func (c *Consumer) startConnectionLoop(ctx context.Context, source Source) {
172
defer c.wg.Done()
173
-
retryInterval := c.cfg.RetryInterval
174
for {
175
select {
176
case <-ctx.Done():
177
return
178
-
default:
179
err := c.runConnection(ctx, source)
180
if err != nil {
181
-
c.logger.Error("connection failed", "source", source, "err", err)
182
-
}
183
-
184
-
// apply jitter
185
-
jitter := time.Duration(c.randSource.Int63n(int64(retryInterval) / 5))
186
-
delay := retryInterval + jitter
187
-
188
-
if retryInterval < c.cfg.MaxRetryInterval {
189
-
retryInterval *= 2
190
-
if retryInterval > c.cfg.MaxRetryInterval {
191
-
retryInterval = c.cfg.MaxRetryInterval
192
-
}
193
}
194
-
c.logger.Info("retrying connection", "source", source, "delay", delay)
195
-
select {
196
-
case <-time.After(delay):
197
-
case <-ctx.Done():
198
-
return
199
-
}
200
}
201
}
202
}
203
204
func (c *Consumer) runConnection(ctx context.Context, source Source) error {
205
-
connCtx, cancel := context.WithTimeout(ctx, c.cfg.ConnectionTimeout)
206
-
defer cancel()
207
-
208
cursor := c.cfg.CursorStore.Get(source.Key())
209
210
u, err := source.Url(cursor, c.cfg.Dev)
···
213
}
214
215
c.logger.Info("connecting", "url", u.String())
216
-
conn, _, err := c.dialer.DialContext(connCtx, u.String(), nil)
217
if err != nil {
218
return err
219
}
220
-
defer conn.Close()
221
c.connMap.Store(source, conn)
222
defer c.connMap.Delete(source)
223
224
c.logger.Info("connected", "source", source)
···
12
"tangled.sh/tangled.sh/core/eventconsumer/cursor"
13
"tangled.sh/tangled.sh/core/log"
14
15
+
"github.com/avast/retry-go/v4"
16
"github.com/gorilla/websocket"
17
)
18
···
171
172
func (c *Consumer) startConnectionLoop(ctx context.Context, source Source) {
173
defer c.wg.Done()
174
+
175
+
// attempt connection initially
176
+
err := c.runConnection(ctx, source)
177
+
if err != nil {
178
+
c.logger.Error("failed to run connection", "err", err)
179
+
}
180
+
181
+
timer := time.NewTimer(1 * time.Minute)
182
+
defer timer.Stop()
183
+
184
+
// every subsequent attempt is delayed by 1 minute
185
for {
186
select {
187
case <-ctx.Done():
188
return
189
+
case <-timer.C:
190
err := c.runConnection(ctx, source)
191
if err != nil {
192
+
c.logger.Error("failed to run connection", "err", err)
193
}
194
+
timer.Reset(1 * time.Minute)
195
}
196
}
197
}
198
199
func (c *Consumer) runConnection(ctx context.Context, source Source) error {
200
cursor := c.cfg.CursorStore.Get(source.Key())
201
202
u, err := source.Url(cursor, c.cfg.Dev)
···
205
}
206
207
c.logger.Info("connecting", "url", u.String())
208
+
209
+
retryOpts := []retry.Option{
210
+
retry.Attempts(0), // infinite attempts
211
+
retry.DelayType(retry.BackOffDelay),
212
+
retry.Delay(c.cfg.RetryInterval),
213
+
retry.MaxDelay(c.cfg.MaxRetryInterval),
214
+
retry.MaxJitter(c.cfg.RetryInterval / 5),
215
+
retry.OnRetry(func(n uint, err error) {
216
+
c.logger.Info("retrying connection",
217
+
"source", source,
218
+
"url", u.String(),
219
+
"attempt", n+1,
220
+
"err", err,
221
+
)
222
+
}),
223
+
retry.Context(ctx),
224
+
}
225
+
226
+
var conn *websocket.Conn
227
+
228
+
err = retry.Do(func() error {
229
+
connCtx, cancel := context.WithTimeout(ctx, c.cfg.ConnectionTimeout)
230
+
defer cancel()
231
+
conn, _, err = c.dialer.DialContext(connCtx, u.String(), nil)
232
+
return err
233
+
}, retryOpts...)
234
if err != nil {
235
return err
236
}
237
+
238
c.connMap.Store(source, conn)
239
+
defer conn.Close()
240
defer c.connMap.Delete(source)
241
242
c.logger.Info("connected", "source", source)
+50
-52
flake.nix
+50
-52
flake.nix
···
53
}: let
54
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
55
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
56
-
nixpkgsFor = forAllSystems (system:
57
-
import nixpkgs {
58
-
inherit system;
59
-
overlays = [self.overlays.default];
60
-
});
61
inherit (gitignore.lib) gitignoreSource;
62
-
in {
63
-
overlays.default = final: prev: let
64
-
goModHash = "sha256-2RUwj16RNaZ/gCOcd7b3LRCHiROCRj9HuzbBdLdgWGo=";
65
-
appviewDeps = {
66
-
inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src goModHash gitignoreSource;
67
};
68
-
knotDeps = {
69
-
inherit goModHash gitignoreSource;
70
};
71
-
spindleDeps = {
72
-
inherit goModHash gitignoreSource;
73
-
};
74
-
mkPackageSet = pkgs: {
75
-
lexgen = pkgs.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
76
-
appview = pkgs.callPackage ./nix/pkgs/appview.nix appviewDeps;
77
-
knot = pkgs.callPackage ./nix/pkgs/knot.nix {};
78
-
spindle = pkgs.callPackage ./nix/pkgs/spindle.nix spindleDeps;
79
-
knot-unwrapped = pkgs.callPackage ./nix/pkgs/knot-unwrapped.nix knotDeps;
80
-
sqlite-lib = pkgs.callPackage ./nix/pkgs/sqlite-lib.nix {
81
-
inherit (pkgs) gcc;
82
-
inherit sqlite-lib-src;
83
-
};
84
-
genjwks = pkgs.callPackage ./nix/pkgs/genjwks.nix {inherit goModHash gitignoreSource;};
85
-
};
86
-
in
87
-
mkPackageSet final;
88
89
packages = forAllSystems (system: let
90
pkgs = nixpkgsFor.${system};
91
-
staticPkgs = pkgs.pkgsStatic;
92
-
crossPkgs = pkgs.pkgsCross.gnu64.pkgsStatic;
93
in {
94
-
appview = pkgs.appview;
95
-
lexgen = pkgs.lexgen;
96
-
knot = pkgs.knot;
97
-
knot-unwrapped = pkgs.knot-unwrapped;
98
-
spindle = pkgs.spindle;
99
-
genjwks = pkgs.genjwks;
100
-
sqlite-lib = pkgs.sqlite-lib;
101
102
-
pkgsStatic-appview = staticPkgs.appview;
103
-
pkgsStatic-knot = staticPkgs.knot;
104
-
pkgsStatic-knot-unwrapped = staticPkgs.knot-unwrapped;
105
-
pkgsStatic-spindle = staticPkgs.spindle;
106
-
pkgsStatic-sqlite-lib = staticPkgs.sqlite-lib;
107
108
-
pkgsCross-gnu64-pkgsStatic-appview = crossPkgs.appview;
109
-
pkgsCross-gnu64-pkgsStatic-knot = crossPkgs.knot;
110
-
pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPkgs.knot-unwrapped;
111
-
pkgsCross-gnu64-pkgsStatic-spindle = crossPkgs.spindle;
112
});
113
-
defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview);
114
-
formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra);
115
devShells = forAllSystems (system: let
116
pkgs = nixpkgsFor.${system};
117
staticShell = pkgs.mkShell.override {
118
stdenv = pkgs.pkgsStatic.stdenv;
119
};
···
124
pkgs.air
125
pkgs.gopls
126
pkgs.httpie
127
-
pkgs.lexgen
128
pkgs.litecli
129
pkgs.websocat
130
pkgs.tailwindcss
131
pkgs.nixos-shell
132
pkgs.redis
133
];
134
shellHook = ''
135
mkdir -p appview/pages/static/{fonts,icons}
136
-
${pkgs.uglify-js}/bin/uglifyjs ${htmx-src} ${htmx-ws-src} -c -m > appview/pages/static/htmx.min.js
137
cp -rf ${lucide-src}/*.svg appview/pages/static/icons/
138
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
139
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
140
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/
141
-
export TANGLED_OAUTH_JWKS="$(${pkgs.genjwks}/bin/genjwks)"
142
'';
143
env.CGO_ENABLED = 1;
144
};
···
171
watch-tailwind = {
172
type = "app";
173
program = ''${tailwind-watcher}/bin/run'';
174
};
175
});
176
···
53
}: let
54
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
55
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
56
+
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
57
inherit (gitignore.lib) gitignoreSource;
58
+
mkPackageSet = pkgs: let
59
+
goModHash = "sha256-SLi+nALwCd/Lzn3aljwPqCo2UaM9hl/4OAjcHQLt2Bk=";
60
+
sqlite-lib = pkgs.callPackage ./nix/pkgs/sqlite-lib.nix {
61
+
inherit (pkgs) gcc;
62
+
inherit sqlite-lib-src;
63
};
64
+
genjwks = pkgs.callPackage ./nix/pkgs/genjwks.nix {inherit goModHash gitignoreSource;};
65
+
lexgen = pkgs.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
66
+
appview = pkgs.callPackage ./nix/pkgs/appview.nix {
67
+
inherit sqlite-lib htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src goModHash gitignoreSource;
68
};
69
+
spindle = pkgs.callPackage ./nix/pkgs/spindle.nix {inherit sqlite-lib goModHash gitignoreSource;};
70
+
knot-unwrapped = pkgs.callPackage ./nix/pkgs/knot-unwrapped.nix {inherit sqlite-lib goModHash gitignoreSource;};
71
+
knot = pkgs.callPackage ./nix/pkgs/knot.nix {inherit knot-unwrapped;};
72
+
in {
73
+
inherit lexgen appview spindle knot-unwrapped knot sqlite-lib genjwks;
74
+
};
75
+
in {
76
+
overlays.default = final: prev: mkPackageSet final;
77
78
packages = forAllSystems (system: let
79
pkgs = nixpkgsFor.${system};
80
+
packages = mkPackageSet pkgs;
81
+
staticPackages = mkPackageSet pkgs.pkgsStatic;
82
+
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
83
in {
84
+
appview = packages.appview;
85
+
lexgen = packages.lexgen;
86
+
knot = packages.knot;
87
+
knot-unwrapped = packages.knot-unwrapped;
88
+
spindle = packages.spindle;
89
+
genjwks = packages.genjwks;
90
+
sqlite-lib = packages.sqlite-lib;
91
92
+
pkgsStatic-appview = staticPackages.appview;
93
+
pkgsStatic-knot = staticPackages.knot;
94
+
pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped;
95
+
pkgsStatic-spindle = staticPackages.spindle;
96
+
pkgsStatic-sqlite-lib = staticPackages.sqlite-lib;
97
98
+
pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview;
99
+
pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot;
100
+
pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped;
101
+
pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle;
102
});
103
+
defaultPackage = forAllSystems (system: self.packages.${system}.appview);
104
+
formatter = forAllSystems (system: nixpkgsFor.${system}.alejandra);
105
devShells = forAllSystems (system: let
106
pkgs = nixpkgsFor.${system};
107
+
packages' = self.packages.${system};
108
staticShell = pkgs.mkShell.override {
109
stdenv = pkgs.pkgsStatic.stdenv;
110
};
···
115
pkgs.air
116
pkgs.gopls
117
pkgs.httpie
118
pkgs.litecli
119
pkgs.websocat
120
pkgs.tailwindcss
121
pkgs.nixos-shell
122
pkgs.redis
123
+
packages'.lexgen
124
];
125
shellHook = ''
126
mkdir -p appview/pages/static/{fonts,icons}
127
+
cp -f ${htmx-src} appview/pages/static/htmx.min.js
128
+
cp -f ${htmx-ws-src} appview/pages/static/htmx-ext-ws.min.js
129
cp -rf ${lucide-src}/*.svg appview/pages/static/icons/
130
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
131
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
132
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/
133
+
export TANGLED_OAUTH_JWKS="$(${packages'.genjwks}/bin/genjwks)"
134
'';
135
env.CGO_ENABLED = 1;
136
};
···
163
watch-tailwind = {
164
type = "app";
165
program = ''${tailwind-watcher}/bin/run'';
166
+
};
167
+
vm = {
168
+
type = "app";
169
+
program = toString (pkgs.writeShellScript "vm" ''
170
+
${pkgs.nixos-shell}/bin/nixos-shell --flake .#vm
171
+
'');
172
};
173
});
174
+1
go.mod
+1
go.mod
···
49
github.com/Microsoft/go-winio v0.6.2 // indirect
50
github.com/ProtonMail/go-crypto v1.2.0 // indirect
51
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
52
github.com/aymerick/douceur v0.2.0 // indirect
53
github.com/beorn7/perks v1.0.1 // indirect
54
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
···
49
github.com/Microsoft/go-winio v0.6.2 // indirect
50
github.com/ProtonMail/go-crypto v1.2.0 // indirect
51
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
52
+
github.com/avast/retry-go/v4 v4.6.1 // indirect
53
github.com/aymerick/douceur v0.2.0 // indirect
54
github.com/beorn7/perks v1.0.1 // indirect
55
github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
+2
go.sum
+2
go.sum
···
17
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
18
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
19
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
20
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
21
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
22
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
···
17
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
18
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
19
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
20
+
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
21
+
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
22
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
23
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
24
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+1
-2
guard/guard.go
+1
-2
guard/guard.go
···
160
gitCmd.Stderr = os.Stderr
161
gitCmd.Stdin = os.Stdin
162
gitCmd.Env = append(os.Environ(),
163
-
fmt.Sprintf("GIT_USER_DID=%s", identity.DID.String()),
164
-
fmt.Sprintf("GIT_USER_HANDLE=%s", identity.Handle.String()),
165
fmt.Sprintf("GIT_USER_PDS_ENDPOINT=%s", identity.PDSEndpoint()),
166
)
167
+1
-1
hook/hook.go
+1
-1
hook/hook.go
+22
-19
input.css
+22
-19
input.css
···
74
75
@layer components {
76
.btn {
77
-
@apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center
78
-
justify-center bg-transparent px-2 pb-[0.2rem] text-base
79
-
text-gray-900 before:absolute before:inset-0 before:-z-10
80
-
before:block before:rounded before:border before:border-gray-200
81
-
before:bg-white before:drop-shadow-sm
82
-
before:content-[''] hover:before:border-gray-300
83
-
hover:before:bg-gray-50
84
-
hover:before:shadow-[0_2px_2px_0_rgba(20,20,96,0.1),inset_0_-2px_0_0_#f5f5f5]
85
-
focus:outline-none focus-visible:before:outline
86
-
focus-visible:before:outline-4 focus-visible:before:outline-gray-500
87
-
active:before:shadow-[inset_0_2px_2px_0_rgba(20,20,96,0.1)]
88
-
disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:before:border-gray-200
89
-
disabled:hover:before:bg-white disabled:hover:before:shadow-none
90
-
dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700
91
-
dark:hover:before:border-gray-600 dark:hover:before:bg-gray-700
92
-
dark:hover:before:shadow-[0_2px_2px_0_rgba(0,0,0,0.2),inset_0_-2px_0_0_#2d3748]
93
-
dark:focus-visible:before:outline-gray-400
94
-
dark:active:before:shadow-[inset_0_2px_2px_0_rgba(0,0,0,0.3)]
95
-
dark:disabled:hover:before:bg-gray-800 dark:disabled:hover:before:border-gray-700;
96
}
97
98
.prose img {
···
74
75
@layer components {
76
.btn {
77
+
@apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center
78
+
bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900
79
+
before:absolute before:inset-0 before:-z-10 before:block before:rounded
80
+
before:border before:border-gray-200 before:bg-white
81
+
before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.1),0_1px_0_0_rgba(0,0,0,0.04)]
82
+
before:content-[''] before:transition-all before:duration-150 before:ease-in-out
83
+
hover:before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.15),0_2px_1px_0_rgba(0,0,0,0.06)]
84
+
hover:before:bg-gray-50
85
+
dark:hover:before:bg-gray-700
86
+
active:before:shadow-[inset_0_2px_2px_0_rgba(0,0,0,0.1)]
87
+
focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400
88
+
disabled:cursor-not-allowed disabled:opacity-50
89
+
dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700;
90
+
}
91
+
92
+
.btn-create {
93
+
@apply btn text-white
94
+
before:bg-green-600 hover:before:bg-green-700
95
+
dark:before:bg-green-700 dark:hover:before:bg-green-800
96
+
before:border before:border-green-700 hover:before:border-green-800
97
+
focus-visible:before:outline-green-500
98
+
disabled:before:bg-green-400 dark:disabled:before:bg-green-600;
99
}
100
101
.prose img {
+15
-16
knotserver/git/git.go
+15
-16
knotserver/git/git.go
···
2
3
import (
4
"archive/tar"
5
"fmt"
6
"io"
7
"io/fs"
···
158
fmt.Sprintf("--count"),
159
)
160
if err != nil {
161
-
return 0, fmt.Errorf("failed to run rev-list", err)
162
}
163
164
count, err := strconv.Atoi(strings.TrimSpace(string(output)))
···
201
}
202
203
func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
204
-
buf := []byte{}
205
-
206
c, err := g.r.CommitObject(g.h)
207
if err != nil {
208
return nil, fmt.Errorf("commit object: %w", err)
···
219
}
220
221
isbin, _ := file.IsBinary()
222
-
223
-
if !isbin {
224
-
reader, err := file.Reader()
225
-
if err != nil {
226
-
return nil, err
227
-
}
228
-
bufReader := io.LimitReader(reader, cap)
229
-
_, err = bufReader.Read(buf)
230
-
if err != nil {
231
-
return nil, err
232
-
}
233
-
return buf, nil
234
-
} else {
235
return nil, ErrBinaryFile
236
}
237
}
238
239
func (g *GitRepo) FileContent(path string) (string, error) {
···
2
3
import (
4
"archive/tar"
5
+
"bytes"
6
"fmt"
7
"io"
8
"io/fs"
···
159
fmt.Sprintf("--count"),
160
)
161
if err != nil {
162
+
return 0, fmt.Errorf("failed to run rev-list: %w", err)
163
}
164
165
count, err := strconv.Atoi(strings.TrimSpace(string(output)))
···
202
}
203
204
func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
205
c, err := g.r.CommitObject(g.h)
206
if err != nil {
207
return nil, fmt.Errorf("commit object: %w", err)
···
218
}
219
220
isbin, _ := file.IsBinary()
221
+
if isbin {
222
return nil, ErrBinaryFile
223
}
224
+
225
+
reader, err := file.Reader()
226
+
if err != nil {
227
+
return nil, err
228
+
}
229
+
230
+
buf := new(bytes.Buffer)
231
+
if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil {
232
+
return nil, err
233
+
}
234
+
235
+
return buf.Bytes(), nil
236
}
237
238
func (g *GitRepo) FileContent(path string) (string, error) {
+79
knotserver/git/tree.go
+79
knotserver/git/tree.go
···
2
3
import (
4
"context"
5
+
"errors"
6
"fmt"
7
"path"
8
"time"
···
79
80
return nts
81
}
82
+
83
+
var (
84
+
TerminateWalk error = errors.New("terminate walk")
85
+
)
86
+
87
+
type callback = func(node object.TreeEntry, parent *object.Tree, fullPath string) error
88
+
89
+
func (g *GitRepo) Walk(
90
+
ctx context.Context,
91
+
root string,
92
+
cb callback,
93
+
) error {
94
+
c, err := g.r.CommitObject(g.h)
95
+
if err != nil {
96
+
return fmt.Errorf("commit object: %w", err)
97
+
}
98
+
99
+
tree, err := c.Tree()
100
+
if err != nil {
101
+
return fmt.Errorf("file tree: %w", err)
102
+
}
103
+
104
+
subtree := tree
105
+
if root != "" {
106
+
subtree, err = tree.Tree(root)
107
+
if err != nil {
108
+
return fmt.Errorf("sub tree: %w", err)
109
+
}
110
+
}
111
+
112
+
return g.walkHelper(ctx, root, subtree, cb)
113
+
}
114
+
115
+
func (g *GitRepo) walkHelper(
116
+
ctx context.Context,
117
+
root string,
118
+
currentTree *object.Tree,
119
+
cb callback,
120
+
) error {
121
+
for _, e := range currentTree.Entries {
122
+
// check if context hits deadline before processing
123
+
select {
124
+
case <-ctx.Done():
125
+
return ctx.Err()
126
+
default:
127
+
}
128
+
129
+
mode, err := e.Mode.ToOSFileMode()
130
+
if err != nil {
131
+
// TODO: log this
132
+
continue
133
+
}
134
+
135
+
if e.Mode.IsFile() {
136
+
err = cb(e, currentTree, root)
137
+
if errors.Is(err, TerminateWalk) {
138
+
return err
139
+
}
140
+
}
141
+
142
+
// e is a directory
143
+
if mode.IsDir() {
144
+
subtree, err := currentTree.Tree(e.Name)
145
+
if err != nil {
146
+
return fmt.Errorf("sub tree %s: %w", e.Name, err)
147
+
}
148
+
149
+
fullPath := path.Join(root, e.Name)
150
+
151
+
err = g.walkHelper(ctx, fullPath, subtree, cb)
152
+
if err != nil {
153
+
return err
154
+
}
155
+
}
156
+
}
157
+
158
+
return nil
159
+
}
+1
-1
knotserver/handler.go
+1
-1
knotserver/handler.go
+305
knotserver/ingester.go
+305
knotserver/ingester.go
···
···
1
+
package knotserver
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"io"
8
+
"net/http"
9
+
"net/url"
10
+
"path/filepath"
11
+
"slices"
12
+
"strings"
13
+
14
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
15
+
"github.com/bluesky-social/indigo/atproto/syntax"
16
+
"github.com/bluesky-social/indigo/xrpc"
17
+
"github.com/bluesky-social/jetstream/pkg/models"
18
+
securejoin "github.com/cyphar/filepath-securejoin"
19
+
"tangled.sh/tangled.sh/core/api/tangled"
20
+
"tangled.sh/tangled.sh/core/appview/idresolver"
21
+
"tangled.sh/tangled.sh/core/knotserver/db"
22
+
"tangled.sh/tangled.sh/core/knotserver/git"
23
+
"tangled.sh/tangled.sh/core/log"
24
+
"tangled.sh/tangled.sh/core/workflow"
25
+
)
26
+
27
+
func (h *Handle) processPublicKey(ctx context.Context, did string, record tangled.PublicKey) error {
28
+
l := log.FromContext(ctx)
29
+
pk := db.PublicKey{
30
+
Did: did,
31
+
PublicKey: record,
32
+
}
33
+
if err := h.db.AddPublicKey(pk); err != nil {
34
+
l.Error("failed to add public key", "error", err)
35
+
return fmt.Errorf("failed to add public key: %w", err)
36
+
}
37
+
l.Info("added public key from firehose", "did", did)
38
+
return nil
39
+
}
40
+
41
+
func (h *Handle) processKnotMember(ctx context.Context, did string, record tangled.KnotMember) error {
42
+
l := log.FromContext(ctx)
43
+
44
+
if record.Domain != h.c.Server.Hostname {
45
+
l.Error("domain mismatch", "domain", record.Domain, "expected", h.c.Server.Hostname)
46
+
return fmt.Errorf("domain mismatch: %s != %s", record.Domain, h.c.Server.Hostname)
47
+
}
48
+
49
+
ok, err := h.e.E.Enforce(did, ThisServer, ThisServer, "server:invite")
50
+
if err != nil || !ok {
51
+
l.Error("failed to add member", "did", did)
52
+
return fmt.Errorf("failed to enforce permissions: %w", err)
53
+
}
54
+
55
+
if err := h.e.AddKnotMember(ThisServer, record.Subject); err != nil {
56
+
l.Error("failed to add member", "error", err)
57
+
return fmt.Errorf("failed to add member: %w", err)
58
+
}
59
+
l.Info("added member from firehose", "member", record.Subject)
60
+
61
+
if err := h.db.AddDid(did); err != nil {
62
+
l.Error("failed to add did", "error", err)
63
+
return fmt.Errorf("failed to add did: %w", err)
64
+
}
65
+
h.jc.AddDid(did)
66
+
67
+
if err := h.fetchAndAddKeys(ctx, did); err != nil {
68
+
return fmt.Errorf("failed to fetch and add keys: %w", err)
69
+
}
70
+
71
+
return nil
72
+
}
73
+
74
+
func (h *Handle) processPull(ctx context.Context, did string, record tangled.RepoPull) error {
75
+
l := log.FromContext(ctx)
76
+
l = l.With("handler", "processPull")
77
+
l = l.With("did", did)
78
+
l = l.With("target_repo", record.TargetRepo)
79
+
l = l.With("target_branch", record.TargetBranch)
80
+
81
+
if record.Source == nil {
82
+
reason := "not a branch-based pull request"
83
+
l.Info("ignoring pull record", "reason", reason)
84
+
return fmt.Errorf("ignoring pull record: %s", reason)
85
+
}
86
+
87
+
if record.Source.Repo != nil {
88
+
reason := "fork based pull"
89
+
l.Info("ignoring pull record", "reason", reason)
90
+
return fmt.Errorf("ignoring pull record: %s", reason)
91
+
}
92
+
93
+
allDids, err := h.db.GetAllDids()
94
+
if err != nil {
95
+
return err
96
+
}
97
+
98
+
// presently: we only process PRs from collaborators for pipelines
99
+
if !slices.Contains(allDids, did) {
100
+
reason := "not a known did"
101
+
l.Info("rejecting pull record", "reason", reason)
102
+
return fmt.Errorf("rejected pull record: %s, %s", reason, did)
103
+
}
104
+
105
+
repoAt, err := syntax.ParseATURI(record.TargetRepo)
106
+
if err != nil {
107
+
return err
108
+
}
109
+
110
+
// resolve this aturi to extract the repo record
111
+
resolver := idresolver.DefaultResolver()
112
+
ident, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
113
+
if err != nil || ident.Handle.IsInvalidHandle() {
114
+
return fmt.Errorf("failed to resolve handle: %w", err)
115
+
}
116
+
117
+
xrpcc := xrpc.Client{
118
+
Host: ident.PDSEndpoint(),
119
+
}
120
+
121
+
resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
122
+
if err != nil {
123
+
return err
124
+
}
125
+
126
+
repo := resp.Value.Val.(*tangled.Repo)
127
+
128
+
if repo.Knot != h.c.Server.Hostname {
129
+
reason := "not this knot"
130
+
l.Info("rejecting pull record", "reason", reason)
131
+
return fmt.Errorf("rejected pull record: %s", reason)
132
+
}
133
+
134
+
didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name)
135
+
if err != nil {
136
+
return err
137
+
}
138
+
139
+
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo)
140
+
if err != nil {
141
+
return err
142
+
}
143
+
144
+
gr, err := git.Open(repoPath, record.Source.Branch)
145
+
if err != nil {
146
+
return err
147
+
}
148
+
149
+
workflowDir, err := gr.FileTree(ctx, workflow.WorkflowDir)
150
+
if err != nil {
151
+
return err
152
+
}
153
+
154
+
var pipeline workflow.Pipeline
155
+
for _, e := range workflowDir {
156
+
if !e.IsFile {
157
+
continue
158
+
}
159
+
160
+
fpath := filepath.Join(workflow.WorkflowDir, e.Name)
161
+
contents, err := gr.RawContent(fpath)
162
+
if err != nil {
163
+
continue
164
+
}
165
+
166
+
wf, err := workflow.FromFile(e.Name, contents)
167
+
if err != nil {
168
+
// TODO: log here, respond to client that is pushing
169
+
h.l.Error("failed to parse workflow", "err", err, "path", fpath)
170
+
continue
171
+
}
172
+
173
+
pipeline = append(pipeline, wf)
174
+
}
175
+
176
+
trigger := tangled.Pipeline_PullRequestTriggerData{
177
+
Action: "create",
178
+
SourceBranch: record.Source.Branch,
179
+
SourceSha: record.Source.Sha,
180
+
TargetBranch: record.TargetBranch,
181
+
}
182
+
183
+
compiler := workflow.Compiler{
184
+
Trigger: tangled.Pipeline_TriggerMetadata{
185
+
Kind: string(workflow.TriggerKindPullRequest),
186
+
PullRequest: &trigger,
187
+
Repo: &tangled.Pipeline_TriggerRepo{
188
+
Did: repo.Owner,
189
+
Knot: repo.Knot,
190
+
Repo: repo.Name,
191
+
},
192
+
},
193
+
}
194
+
195
+
cp := compiler.Compile(pipeline)
196
+
eventJson, err := json.Marshal(cp)
197
+
if err != nil {
198
+
return err
199
+
}
200
+
201
+
// do not run empty pipelines
202
+
if cp.Workflows == nil {
203
+
return nil
204
+
}
205
+
206
+
event := db.Event{
207
+
Rkey: TID(),
208
+
Nsid: tangled.PipelineNSID,
209
+
EventJson: string(eventJson),
210
+
}
211
+
212
+
return h.db.InsertEvent(event, h.n)
213
+
}
214
+
215
+
func (h *Handle) fetchAndAddKeys(ctx context.Context, did string) error {
216
+
l := log.FromContext(ctx)
217
+
218
+
keysEndpoint, err := url.JoinPath(h.c.AppViewEndpoint, "keys", did)
219
+
if err != nil {
220
+
l.Error("error building endpoint url", "did", did, "error", err.Error())
221
+
return fmt.Errorf("error building endpoint url: %w", err)
222
+
}
223
+
224
+
resp, err := http.Get(keysEndpoint)
225
+
if err != nil {
226
+
l.Error("error getting keys", "did", did, "error", err)
227
+
return fmt.Errorf("error getting keys: %w", err)
228
+
}
229
+
defer resp.Body.Close()
230
+
231
+
if resp.StatusCode == http.StatusNotFound {
232
+
l.Info("no keys found for did", "did", did)
233
+
return nil
234
+
}
235
+
236
+
plaintext, err := io.ReadAll(resp.Body)
237
+
if err != nil {
238
+
l.Error("error reading response body", "error", err)
239
+
return fmt.Errorf("error reading response body: %w", err)
240
+
}
241
+
242
+
for _, key := range strings.Split(string(plaintext), "\n") {
243
+
if key == "" {
244
+
continue
245
+
}
246
+
pk := db.PublicKey{
247
+
Did: did,
248
+
}
249
+
pk.Key = key
250
+
if err := h.db.AddPublicKey(pk); err != nil {
251
+
l.Error("failed to add public key", "error", err)
252
+
return fmt.Errorf("failed to add public key: %w", err)
253
+
}
254
+
}
255
+
return nil
256
+
}
257
+
258
+
func (h *Handle) processMessages(ctx context.Context, event *models.Event) error {
259
+
did := event.Did
260
+
if event.Kind != models.EventKindCommit {
261
+
return nil
262
+
}
263
+
264
+
var err error
265
+
defer func() {
266
+
eventTime := event.TimeUS
267
+
lastTimeUs := eventTime + 1
268
+
fmt.Println("lastTimeUs", lastTimeUs)
269
+
if err := h.db.SaveLastTimeUs(lastTimeUs); err != nil {
270
+
err = fmt.Errorf("(deferred) failed to save last time us: %w", err)
271
+
}
272
+
}()
273
+
274
+
raw := json.RawMessage(event.Commit.Record)
275
+
276
+
switch event.Commit.Collection {
277
+
case tangled.PublicKeyNSID:
278
+
var record tangled.PublicKey
279
+
if err := json.Unmarshal(raw, &record); err != nil {
280
+
return fmt.Errorf("failed to unmarshal record: %w", err)
281
+
}
282
+
if err := h.processPublicKey(ctx, did, record); err != nil {
283
+
return fmt.Errorf("failed to process public key: %w", err)
284
+
}
285
+
286
+
case tangled.KnotMemberNSID:
287
+
var record tangled.KnotMember
288
+
if err := json.Unmarshal(raw, &record); err != nil {
289
+
return fmt.Errorf("failed to unmarshal record: %w", err)
290
+
}
291
+
if err := h.processKnotMember(ctx, did, record); err != nil {
292
+
return fmt.Errorf("failed to process knot member: %w", err)
293
+
}
294
+
case tangled.RepoPullNSID:
295
+
var record tangled.RepoPull
296
+
if err := json.Unmarshal(raw, &record); err != nil {
297
+
return fmt.Errorf("failed to unmarshal record: %w", err)
298
+
}
299
+
if err := h.processPull(ctx, did, record); err != nil {
300
+
return fmt.Errorf("failed to process knot member: %w", err)
301
+
}
302
+
}
303
+
304
+
return err
305
+
}
+2
-6
knotserver/internal.go
+2
-6
knotserver/internal.go
···
147
}
148
149
func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
150
-
const (
151
-
WorkflowDir = ".tangled/workflows"
152
-
)
153
-
154
didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName)
155
if err != nil {
156
return err
···
166
return err
167
}
168
169
-
workflowDir, err := gr.FileTree(context.Background(), WorkflowDir)
170
if err != nil {
171
return err
172
}
···
177
continue
178
}
179
180
-
fpath := filepath.Join(WorkflowDir, e.Name)
181
contents, err := gr.RawContent(fpath)
182
if err != nil {
183
continue
···
147
}
148
149
func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
150
didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName)
151
if err != nil {
152
return err
···
162
return err
163
}
164
165
+
workflowDir, err := gr.FileTree(context.Background(), workflow.WorkflowDir)
166
if err != nil {
167
return err
168
}
···
173
continue
174
}
175
176
+
fpath := filepath.Join(workflow.WorkflowDir, e.Name)
177
contents, err := gr.RawContent(fpath)
178
if err != nil {
179
continue
-147
knotserver/jetstream.go
-147
knotserver/jetstream.go
···
1
-
package knotserver
2
-
3
-
import (
4
-
"context"
5
-
"encoding/json"
6
-
"fmt"
7
-
"io"
8
-
"net/http"
9
-
"net/url"
10
-
"strings"
11
-
12
-
"github.com/bluesky-social/jetstream/pkg/models"
13
-
"tangled.sh/tangled.sh/core/api/tangled"
14
-
"tangled.sh/tangled.sh/core/knotserver/db"
15
-
"tangled.sh/tangled.sh/core/log"
16
-
)
17
-
18
-
func (h *Handle) processPublicKey(ctx context.Context, did string, record tangled.PublicKey) error {
19
-
l := log.FromContext(ctx)
20
-
pk := db.PublicKey{
21
-
Did: did,
22
-
PublicKey: record,
23
-
}
24
-
if err := h.db.AddPublicKey(pk); err != nil {
25
-
l.Error("failed to add public key", "error", err)
26
-
return fmt.Errorf("failed to add public key: %w", err)
27
-
}
28
-
l.Info("added public key from firehose", "did", did)
29
-
return nil
30
-
}
31
-
32
-
func (h *Handle) processKnotMember(ctx context.Context, did string, record tangled.KnotMember) error {
33
-
l := log.FromContext(ctx)
34
-
35
-
if record.Domain != h.c.Server.Hostname {
36
-
l.Error("domain mismatch", "domain", record.Domain, "expected", h.c.Server.Hostname)
37
-
return fmt.Errorf("domain mismatch: %s != %s", record.Domain, h.c.Server.Hostname)
38
-
}
39
-
40
-
ok, err := h.e.E.Enforce(did, ThisServer, ThisServer, "server:invite")
41
-
if err != nil || !ok {
42
-
l.Error("failed to add member", "did", did)
43
-
return fmt.Errorf("failed to enforce permissions: %w", err)
44
-
}
45
-
46
-
if err := h.e.AddKnotMember(ThisServer, record.Subject); err != nil {
47
-
l.Error("failed to add member", "error", err)
48
-
return fmt.Errorf("failed to add member: %w", err)
49
-
}
50
-
l.Info("added member from firehose", "member", record.Subject)
51
-
52
-
if err := h.db.AddDid(did); err != nil {
53
-
l.Error("failed to add did", "error", err)
54
-
return fmt.Errorf("failed to add did: %w", err)
55
-
}
56
-
h.jc.AddDid(did)
57
-
58
-
if err := h.fetchAndAddKeys(ctx, did); err != nil {
59
-
return fmt.Errorf("failed to fetch and add keys: %w", err)
60
-
}
61
-
62
-
return nil
63
-
}
64
-
65
-
func (h *Handle) fetchAndAddKeys(ctx context.Context, did string) error {
66
-
l := log.FromContext(ctx)
67
-
68
-
keysEndpoint, err := url.JoinPath(h.c.AppViewEndpoint, "keys", did)
69
-
if err != nil {
70
-
l.Error("error building endpoint url", "did", did, "error", err.Error())
71
-
return fmt.Errorf("error building endpoint url: %w", err)
72
-
}
73
-
74
-
resp, err := http.Get(keysEndpoint)
75
-
if err != nil {
76
-
l.Error("error getting keys", "did", did, "error", err)
77
-
return fmt.Errorf("error getting keys: %w", err)
78
-
}
79
-
defer resp.Body.Close()
80
-
81
-
if resp.StatusCode == http.StatusNotFound {
82
-
l.Info("no keys found for did", "did", did)
83
-
return nil
84
-
}
85
-
86
-
plaintext, err := io.ReadAll(resp.Body)
87
-
if err != nil {
88
-
l.Error("error reading response body", "error", err)
89
-
return fmt.Errorf("error reading response body: %w", err)
90
-
}
91
-
92
-
for _, key := range strings.Split(string(plaintext), "\n") {
93
-
if key == "" {
94
-
continue
95
-
}
96
-
pk := db.PublicKey{
97
-
Did: did,
98
-
}
99
-
pk.Key = key
100
-
if err := h.db.AddPublicKey(pk); err != nil {
101
-
l.Error("failed to add public key", "error", err)
102
-
return fmt.Errorf("failed to add public key: %w", err)
103
-
}
104
-
}
105
-
return nil
106
-
}
107
-
108
-
func (h *Handle) processMessages(ctx context.Context, event *models.Event) error {
109
-
did := event.Did
110
-
if event.Kind != models.EventKindCommit {
111
-
return nil
112
-
}
113
-
114
-
var err error
115
-
defer func() {
116
-
eventTime := event.TimeUS
117
-
lastTimeUs := eventTime + 1
118
-
fmt.Println("lastTimeUs", lastTimeUs)
119
-
if err := h.db.SaveLastTimeUs(lastTimeUs); err != nil {
120
-
err = fmt.Errorf("(deferred) failed to save last time us: %w", err)
121
-
}
122
-
}()
123
-
124
-
raw := json.RawMessage(event.Commit.Record)
125
-
126
-
switch event.Commit.Collection {
127
-
case tangled.PublicKeyNSID:
128
-
var record tangled.PublicKey
129
-
if err := json.Unmarshal(raw, &record); err != nil {
130
-
return fmt.Errorf("failed to unmarshal record: %w", err)
131
-
}
132
-
if err := h.processPublicKey(ctx, did, record); err != nil {
133
-
return fmt.Errorf("failed to process public key: %w", err)
134
-
}
135
-
136
-
case tangled.KnotMemberNSID:
137
-
var record tangled.KnotMember
138
-
if err := json.Unmarshal(raw, &record); err != nil {
139
-
return fmt.Errorf("failed to unmarshal record: %w", err)
140
-
}
141
-
if err := h.processKnotMember(ctx, did, record); err != nil {
142
-
return fmt.Errorf("failed to process knot member: %w", err)
143
-
}
144
-
}
145
-
146
-
return err
147
-
}
···
+44
-34
knotserver/routes.go
+44
-34
knotserver/routes.go
···
18
"strconv"
19
"strings"
20
"sync"
21
22
securejoin "github.com/cyphar/filepath-securejoin"
23
"github.com/gliderlabs/ssh"
···
763
}
764
765
func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
766
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
767
ref := chi.URLParam(r, "ref")
768
ref, _ = url.PathUnescape(ref)
769
770
l := h.l.With("handler", "RepoLanguages")
771
772
-
gr, err := git.Open(path, ref)
773
if err != nil {
774
l.Error("opening repo", "error", err.Error())
775
notFound(w)
776
return
777
}
778
779
-
languageFileCount := make(map[string]int)
780
781
-
err = recurseEntireTree(r.Context(), gr, func(absPath string) {
782
-
lang, safe := enry.GetLanguageByExtension(absPath)
783
-
if len(lang) == 0 || !safe {
784
-
content, _ := gr.FileContentN(absPath, 1024)
785
-
if !safe {
786
-
lang = enry.GetLanguage(absPath, content)
787
-
} else {
788
-
lang, _ = enry.GetLanguageByContent(absPath, content)
789
-
if len(lang) == 0 {
790
-
return
791
-
}
792
-
}
793
}
794
795
-
v, ok := languageFileCount[lang]
796
-
if ok {
797
-
languageFileCount[lang] = v + 1
798
-
} else {
799
-
languageFileCount[lang] = 1
800
}
801
-
}, "")
802
if err != nil {
803
l.Error("failed to recurse file tree", "error", err.Error())
804
writeError(w, err.Error(), http.StatusNoContent)
805
return
806
}
807
808
-
resp := types.RepoLanguageResponse{Languages: languageFileCount}
809
810
writeJSON(w, resp)
811
return
812
}
813
814
-
func recurseEntireTree(ctx context.Context, git *git.GitRepo, callback func(absPath string), filePath string) error {
815
-
files, err := git.FileTree(ctx, filePath)
816
-
if err != nil {
817
-
log.Println(err)
818
-
return err
819
}
820
821
-
for _, file := range files {
822
-
absPath := path.Join(filePath, file.Name)
823
-
if !file.IsFile {
824
-
return recurseEntireTree(ctx, git, callback, absPath)
825
-
}
826
-
callback(absPath)
827
}
828
829
-
return nil
830
}
831
832
func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
···
18
"strconv"
19
"strings"
20
"sync"
21
+
"time"
22
23
securejoin "github.com/cyphar/filepath-securejoin"
24
"github.com/gliderlabs/ssh"
···
764
}
765
766
func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
767
+
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
768
ref := chi.URLParam(r, "ref")
769
ref, _ = url.PathUnescape(ref)
770
771
l := h.l.With("handler", "RepoLanguages")
772
773
+
gr, err := git.Open(repoPath, ref)
774
if err != nil {
775
l.Error("opening repo", "error", err.Error())
776
notFound(w)
777
return
778
}
779
780
+
sizes := make(map[string]int64)
781
+
782
+
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
783
+
defer cancel()
784
+
785
+
err = gr.Walk(ctx, "", func(node object.TreeEntry, parent *object.Tree, root string) error {
786
+
filepath := path.Join(root, node.Name)
787
+
788
+
content, err := gr.FileContentN(filepath, 16*1024) // 16KB
789
+
if err != nil {
790
+
return nil
791
+
}
792
+
793
+
if enry.IsGenerated(filepath, content) {
794
+
return nil
795
+
}
796
797
+
language := analyzeLanguage(node, content)
798
+
if group := enry.GetLanguageGroup(language); group != "" {
799
+
language = group
800
}
801
802
+
langType := enry.GetLanguageType(language)
803
+
if langType != enry.Programming && langType != enry.Markup && langType != enry.Unknown {
804
+
return nil
805
}
806
+
807
+
sz, _ := parent.Size(node.Name)
808
+
sizes[language] += sz
809
+
810
+
return nil
811
+
})
812
if err != nil {
813
l.Error("failed to recurse file tree", "error", err.Error())
814
writeError(w, err.Error(), http.StatusNoContent)
815
return
816
}
817
818
+
resp := types.RepoLanguageResponse{Languages: sizes}
819
820
writeJSON(w, resp)
821
return
822
}
823
824
+
func analyzeLanguage(node object.TreeEntry, content []byte) string {
825
+
language, ok := enry.GetLanguageByExtension(node.Name)
826
+
if ok {
827
+
return language
828
}
829
830
+
language, ok = enry.GetLanguageByFilename(node.Name)
831
+
if ok {
832
+
return language
833
+
}
834
+
835
+
if len(content) == 0 {
836
+
return enry.OtherLanguage
837
}
838
839
+
return enry.GetLanguage(node.Name, content)
840
}
841
842
func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
+1
knotserver/server.go
+1
knotserver/server.go
···
75
jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{
76
tangled.PublicKeyNSID,
77
tangled.KnotMemberNSID,
78
+
tangled.RepoPullNSID,
79
}, nil, logger, db, true, c.Server.LogDids)
80
if err != nil {
81
logger.Error("failed to setup jetstream", "error", err)
+34
lexicons/feed/reaction.json
+34
lexicons/feed/reaction.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.feed.reaction",
4
+
"needsCbor": true,
5
+
"needsType": true,
6
+
"defs": {
7
+
"main": {
8
+
"type": "record",
9
+
"key": "tid",
10
+
"record": {
11
+
"type": "object",
12
+
"required": [
13
+
"subject",
14
+
"reaction",
15
+
"createdAt"
16
+
],
17
+
"properties": {
18
+
"subject": {
19
+
"type": "string",
20
+
"format": "at-uri"
21
+
},
22
+
"reaction": {
23
+
"type": "string",
24
+
"enum": [ "๐", "๐", "๐", "๐", "๐ซค", "โค๏ธ", "๐", "๐" ]
25
+
},
26
+
"createdAt": {
27
+
"type": "string",
28
+
"format": "datetime"
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
+85
-54
lexicons/pipeline.json
+85
-54
lexicons/pipeline.json
···
9
"key": "tid",
10
"record": {
11
"type": "object",
12
-
"required": ["triggerMetadata", "workflows"],
13
"properties": {
14
"triggerMetadata": {
15
"type": "ref",
···
27
},
28
"triggerMetadata": {
29
"type": "object",
30
-
"required": ["kind", "repo"],
31
"properties": {
32
"kind": {
33
"type": "string",
34
-
"enum": ["push", "pull_request", "manual"]
35
},
36
"repo": {
37
"type": "ref",
···
53
},
54
"triggerRepo": {
55
"type": "object",
56
-
"required": ["knot", "did", "repo", "defaultBranch"],
57
"properties": {
58
"knot": {
59
"type": "string"
···
72
},
73
"pushTriggerData": {
74
"type": "object",
75
-
"required": ["ref", "newSha", "oldSha"],
76
"properties": {
77
"ref": {
78
"type": "string"
···
91
},
92
"pullRequestTriggerData": {
93
"type": "object",
94
-
"required": ["sourceBranch", "targetBranch", "sourceSha", "action"],
95
"properties": {
96
"sourceBranch": {
97
"type": "string"
···
115
"inputs": {
116
"type": "array",
117
"items": {
118
-
"type": "object",
119
-
"required": ["key", "value"],
120
-
"properties": {
121
-
"key": {
122
-
"type": "string"
123
-
},
124
-
"value": {
125
-
"type": "string"
126
-
}
127
-
}
128
}
129
}
130
}
131
},
132
"workflow": {
133
"type": "object",
134
-
"required": ["name", "dependencies", "steps", "environment", "clone"],
135
"properties": {
136
"name": {
137
"type": "string"
138
},
139
"dependencies": {
140
-
"type": "ref",
141
-
"ref": "#dependencies"
142
},
143
"steps": {
144
"type": "array",
···
150
"environment": {
151
"type": "array",
152
"items": {
153
-
"type": "object",
154
-
"required": ["key", "value"],
155
-
"properties": {
156
-
"key": {
157
-
"type": "string"
158
-
},
159
-
"value": {
160
-
"type": "string"
161
-
}
162
-
}
163
}
164
},
165
"clone": {
···
168
}
169
}
170
},
171
-
"dependencies": {
172
-
"type": "array",
173
-
"items": {
174
-
"type": "object",
175
-
"required": ["registry", "packages"],
176
-
"properties": {
177
-
"registry": {
178
"type": "string"
179
-
},
180
-
"packages": {
181
-
"type": "array",
182
-
"items": {
183
-
"type": "string"
184
-
}
185
}
186
}
187
}
188
},
189
"cloneOpts": {
190
"type": "object",
191
-
"required": ["skip", "depth", "submodules"],
192
"properties": {
193
"skip": {
194
"type": "boolean"
···
203
},
204
"step": {
205
"type": "object",
206
-
"required": ["name", "command"],
207
"properties": {
208
"name": {
209
"type": "string"
···
214
"environment": {
215
"type": "array",
216
"items": {
217
-
"type": "object",
218
-
"required": ["key", "value"],
219
-
"properties": {
220
-
"key": {
221
-
"type": "string"
222
-
},
223
-
"value": {
224
-
"type": "string"
225
-
}
226
-
}
227
}
228
}
229
}
230
}
···
9
"key": "tid",
10
"record": {
11
"type": "object",
12
+
"required": [
13
+
"triggerMetadata",
14
+
"workflows"
15
+
],
16
"properties": {
17
"triggerMetadata": {
18
"type": "ref",
···
30
},
31
"triggerMetadata": {
32
"type": "object",
33
+
"required": [
34
+
"kind",
35
+
"repo"
36
+
],
37
"properties": {
38
"kind": {
39
"type": "string",
40
+
"enum": [
41
+
"push",
42
+
"pull_request",
43
+
"manual"
44
+
]
45
},
46
"repo": {
47
"type": "ref",
···
63
},
64
"triggerRepo": {
65
"type": "object",
66
+
"required": [
67
+
"knot",
68
+
"did",
69
+
"repo",
70
+
"defaultBranch"
71
+
],
72
"properties": {
73
"knot": {
74
"type": "string"
···
87
},
88
"pushTriggerData": {
89
"type": "object",
90
+
"required": [
91
+
"ref",
92
+
"newSha",
93
+
"oldSha"
94
+
],
95
"properties": {
96
"ref": {
97
"type": "string"
···
110
},
111
"pullRequestTriggerData": {
112
"type": "object",
113
+
"required": [
114
+
"sourceBranch",
115
+
"targetBranch",
116
+
"sourceSha",
117
+
"action"
118
+
],
119
"properties": {
120
"sourceBranch": {
121
"type": "string"
···
139
"inputs": {
140
"type": "array",
141
"items": {
142
+
"type": "ref",
143
+
"ref": "#pair"
144
}
145
}
146
}
147
},
148
"workflow": {
149
"type": "object",
150
+
"required": [
151
+
"name",
152
+
"dependencies",
153
+
"steps",
154
+
"environment",
155
+
"clone"
156
+
],
157
"properties": {
158
"name": {
159
"type": "string"
160
},
161
"dependencies": {
162
+
"type": "array",
163
+
"items": {
164
+
"type": "ref",
165
+
"ref": "#dependency"
166
+
}
167
},
168
"steps": {
169
"type": "array",
···
175
"environment": {
176
"type": "array",
177
"items": {
178
+
"type": "ref",
179
+
"ref": "#pair"
180
}
181
},
182
"clone": {
···
185
}
186
}
187
},
188
+
"dependency": {
189
+
"type": "object",
190
+
"required": [
191
+
"registry",
192
+
"packages"
193
+
],
194
+
"properties": {
195
+
"registry": {
196
+
"type": "string"
197
+
},
198
+
"packages": {
199
+
"type": "array",
200
+
"items": {
201
"type": "string"
202
}
203
}
204
}
205
},
206
"cloneOpts": {
207
"type": "object",
208
+
"required": [
209
+
"skip",
210
+
"depth",
211
+
"submodules"
212
+
],
213
"properties": {
214
"skip": {
215
"type": "boolean"
···
224
},
225
"step": {
226
"type": "object",
227
+
"required": [
228
+
"name",
229
+
"command"
230
+
],
231
"properties": {
232
"name": {
233
"type": "string"
···
238
"environment": {
239
"type": "array",
240
"items": {
241
+
"type": "ref",
242
+
"ref": "#pair"
243
}
244
+
}
245
+
}
246
+
},
247
+
"pair": {
248
+
"type": "object",
249
+
"required": [
250
+
"key",
251
+
"value"
252
+
],
253
+
"properties": {
254
+
"key": {
255
+
"type": "string"
256
+
},
257
+
"value": {
258
+
"type": "string"
259
}
260
}
261
}
+7
-1
lexicons/pulls/pull.json
+7
-1
lexicons/pulls/pull.json
+2
-2
nix/pkgs/appview.nix
+2
-2
nix/pkgs/appview.nix
···
10
sqlite-lib,
11
goModHash,
12
gitignoreSource,
13
-
uglify-js,
14
}:
15
buildGoModule {
16
inherit stdenv;
···
22
postUnpack = ''
23
pushd source
24
mkdir -p appview/pages/static/{fonts,icons}
25
-
${uglify-js}/bin/uglifyjs ${htmx-src} ${htmx-ws-src} -c -m > appview/pages/static/htmx.min.js
26
cp -rf ${lucide-src}/*.svg appview/pages/static/icons/
27
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
28
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
···
10
sqlite-lib,
11
goModHash,
12
gitignoreSource,
13
}:
14
buildGoModule {
15
inherit stdenv;
···
21
postUnpack = ''
22
pushd source
23
mkdir -p appview/pages/static/{fonts,icons}
24
+
cp -f ${htmx-src} appview/pages/static/htmx.min.js
25
+
cp -f ${htmx-ws-src} appview/pages/static/htmx-ext-ws.min.js
26
cp -rf ${lucide-src}/*.svg appview/pages/static/icons/
27
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
28
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
+4
-1
spindle/engine/engine.go
+4
-1
spindle/engine/engine.go
···
90
91
reader, err := e.docker.ImagePull(ctx, w.Image, image.PullOptions{})
92
if err != nil {
93
-
e.l.Error("pipeline failed!", "workflowId", wid, "error", err.Error())
94
95
err := e.db.StatusFailed(wid, err.Error(), -1, e.n)
96
if err != nil {
···
413
ReadOnly: false,
414
TmpfsOptions: &mount.TmpfsOptions{
415
Mode: 0o1777, // world-writeable sticky bit
416
},
417
},
418
{
···
90
91
reader, err := e.docker.ImagePull(ctx, w.Image, image.PullOptions{})
92
if err != nil {
93
+
e.l.Error("pipeline image pull failed!", "image", w.Image, "workflowId", wid, "error", err.Error())
94
95
err := e.db.StatusFailed(wid, err.Error(), -1, e.n)
96
if err != nil {
···
413
ReadOnly: false,
414
TmpfsOptions: &mount.TmpfsOptions{
415
Mode: 0o1777, // world-writeable sticky bit
416
+
Options: [][]string{
417
+
{"exec"},
418
+
},
419
},
420
},
421
{
+4
-11
spindle/engine/envs_test.go
+4
-11
spindle/engine/envs_test.go
···
1
package engine
2
3
import (
4
-
"reflect"
5
"testing"
6
)
7
8
func TestConstructEnvs(t *testing.T) {
···
27
want: EnvVars{"FOO=bar", "BAZ=qux"},
28
},
29
}
30
-
31
for _, tt := range tests {
32
t.Run(tt.name, func(t *testing.T) {
33
got := ConstructEnvs(tt.in)
34
-
35
if got == nil {
36
got = EnvVars{}
37
}
38
-
39
-
if !reflect.DeepEqual(got, tt.want) {
40
-
t.Errorf("ConstructEnvs() = %v, want %v", got, tt.want)
41
-
}
42
})
43
}
44
}
···
47
ev := EnvVars{}
48
ev.AddEnv("FOO", "bar")
49
ev.AddEnv("BAZ", "qux")
50
-
51
want := EnvVars{"FOO=bar", "BAZ=qux"}
52
-
if !reflect.DeepEqual(ev, want) {
53
-
t.Errorf("AddEnv result = %v, want %v", ev, want)
54
-
}
55
}
···
1
package engine
2
3
import (
4
"testing"
5
+
6
+
"github.com/stretchr/testify/assert"
7
)
8
9
func TestConstructEnvs(t *testing.T) {
···
28
want: EnvVars{"FOO=bar", "BAZ=qux"},
29
},
30
}
31
for _, tt := range tests {
32
t.Run(tt.name, func(t *testing.T) {
33
got := ConstructEnvs(tt.in)
34
if got == nil {
35
got = EnvVars{}
36
}
37
+
assert.ElementsMatch(t, tt.want, got)
38
})
39
}
40
}
···
43
ev := EnvVars{}
44
ev.AddEnv("FOO", "bar")
45
ev.AddEnv("BAZ", "qux")
46
want := EnvVars{"FOO=bar", "BAZ=qux"}
47
+
assert.ElementsMatch(t, want, ev)
48
}
+4
-4
spindle/ingester.go
+4
-4
spindle/ingester.go
···
66
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
67
}
68
69
-
ok, err := s.e.E.Enforce(did, rbacDomain, rbacDomain, "server:invite")
70
if err != nil || !ok {
71
-
l.Error("failed to add member", "did", did)
72
return fmt.Errorf("failed to enforce permissions: %w", err)
73
}
74
···
78
}
79
l.Info("added member from firehose", "member", record.Subject)
80
81
-
if err := s.db.AddDid(did); err != nil {
82
l.Error("failed to add did", "error", err)
83
return fmt.Errorf("failed to add did: %w", err)
84
}
85
-
s.jc.AddDid(did)
86
87
return nil
88
···
66
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
67
}
68
69
+
ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain)
70
if err != nil || !ok {
71
+
l.Error("failed to add member", "did", did, "error", err)
72
return fmt.Errorf("failed to enforce permissions: %w", err)
73
}
74
···
78
}
79
l.Info("added member from firehose", "member", record.Subject)
80
81
+
if err := s.db.AddDid(record.Subject); err != nil {
82
l.Error("failed to add did", "error", err)
83
return fmt.Errorf("failed to add did: %w", err)
84
}
85
+
s.jc.AddDid(record.Subject)
86
87
return nil
88
+10
-7
spindle/models/pipeline.go
+10
-7
spindle/models/pipeline.go
···
68
setup := &setupSteps{}
69
70
setup.addStep(nixConfStep())
71
-
setup.addStep(cloneStep(*twf, *pl.TriggerMetadata.Repo, cfg.Server.Dev))
72
-
setup.addStep(checkoutStep(*twf, *pl.TriggerMetadata))
73
// this step could be empty
74
if s := dependencyStep(*twf); s != nil {
75
setup.addStep(*s)
···
83
return &Pipeline{Workflows: workflows}
84
}
85
86
-
func workflowEnvToMap(envs []*tangled.Pipeline_Workflow_Environment_Elem) map[string]string {
87
envMap := map[string]string{}
88
for _, env := range envs {
89
-
envMap[env.Key] = env.Value
90
}
91
return envMap
92
}
93
94
-
func stepEnvToMap(envs []*tangled.Pipeline_Step_Environment_Elem) map[string]string {
95
envMap := map[string]string{}
96
for _, env := range envs {
97
-
envMap[env.Key] = env.Value
98
}
99
return envMap
100
}
101
102
-
func workflowImage(deps []tangled.Pipeline_Dependencies_Elem, nixery string) string {
103
var dependencies string
104
for _, d := range deps {
105
if d.Registry == "nixpkgs" {
···
68
setup := &setupSteps{}
69
70
setup.addStep(nixConfStep())
71
+
setup.addStep(cloneStep(*twf, *pl.TriggerMetadata, cfg.Server.Dev))
72
// this step could be empty
73
if s := dependencyStep(*twf); s != nil {
74
setup.addStep(*s)
···
82
return &Pipeline{Workflows: workflows}
83
}
84
85
+
func workflowEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string {
86
envMap := map[string]string{}
87
for _, env := range envs {
88
+
if env != nil {
89
+
envMap[env.Key] = env.Value
90
+
}
91
}
92
return envMap
93
}
94
95
+
func stepEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string {
96
envMap := map[string]string{}
97
for _, env := range envs {
98
+
if env != nil {
99
+
envMap[env.Key] = env.Value
100
+
}
101
}
102
return envMap
103
}
104
105
+
func workflowImage(deps []*tangled.Pipeline_Dependency, nixery string) string {
106
var dependencies string
107
for _, d := range deps {
108
if d.Registry == "nixpkgs" {
+49
-41
spindle/models/setup_steps.go
+49
-41
spindle/models/setup_steps.go
···
6
"strings"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
)
10
11
func nixConfStep() Step {
···
17
}
18
}
19
20
-
// checkoutStep checks out the specified ref in the cloned repository.
21
-
func checkoutStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata) Step {
22
if twf.Clone.Skip {
23
return Step{}
24
}
25
26
-
var ref string
27
-
switch tr.Kind {
28
-
case "push":
29
-
ref = tr.Push.Ref
30
-
case "pull_request":
31
-
ref = tr.PullRequest.TargetBranch
32
33
-
// TODO: this needs to be specified in lexicon
34
-
case "manual":
35
-
ref = tr.Repo.DefaultBranch
36
-
}
37
38
-
checkoutCmd := fmt.Sprintf("git config advice.detachedHead false; git checkout --progress --force %s", ref)
39
-
40
-
return Step{
41
-
Command: checkoutCmd,
42
-
Name: "Checkout ref " + ref,
43
}
44
-
}
45
46
-
// cloneOptsAsSteps processes clone options and adds corresponding steps
47
-
// to the beginning of the workflow's step list if cloning is not skipped.
48
-
func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerRepo, dev bool) Step {
49
-
if twf.Clone.Skip {
50
-
return Step{}
51
-
}
52
53
-
uri := "https://"
54
-
if dev {
55
-
uri = "http://"
56
-
tr.Knot = strings.ReplaceAll(tr.Knot, "localhost", "host.docker.internal")
57
-
}
58
59
-
cloneUrl := uri + path.Join(tr.Knot, tr.Did, tr.Repo)
60
-
cloneCmd := []string{"git", "clone", cloneUrl, "."}
61
62
-
// default clone depth is 1
63
-
cloneDepth := 1
64
-
if twf.Clone.Depth > 1 {
65
-
cloneDepth = int(twf.Clone.Depth)
66
-
}
67
-
cloneCmd = append(cloneCmd, []string{"--depth", fmt.Sprintf("%d", cloneDepth)}...)
68
69
-
if twf.Clone.Submodules {
70
-
cloneCmd = append(cloneCmd, "--recursive")
71
}
72
73
-
fmt.Println(strings.Join(cloneCmd, " "))
74
75
cloneStep := Step{
76
-
Command: strings.Join(cloneCmd, " "),
77
Name: "Clone repository into workspace",
78
}
79
return cloneStep
···
6
"strings"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
+
"tangled.sh/tangled.sh/core/workflow"
10
)
11
12
func nixConfStep() Step {
···
18
}
19
}
20
21
+
// cloneOptsAsSteps processes clone options and adds corresponding steps
22
+
// to the beginning of the workflow's step list if cloning is not skipped.
23
+
//
24
+
// the steps to do here are:
25
+
// - git init
26
+
// - git remote add origin <url>
27
+
// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
28
+
// - git checkout FETCH_HEAD
29
+
func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step {
30
if twf.Clone.Skip {
31
return Step{}
32
}
33
34
+
var commands []string
35
36
+
// initialize git repo in workspace
37
+
commands = append(commands, "git init")
38
39
+
// add repo as git remote
40
+
scheme := "https://"
41
+
if dev {
42
+
scheme = "http://"
43
+
tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal")
44
}
45
+
url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo)
46
+
commands = append(commands, fmt.Sprintf("git remote add origin %s", url))
47
48
+
// run git fetch
49
+
{
50
+
var fetchArgs []string
51
52
+
// default clone depth is 1
53
+
depth := 1
54
+
if twf.Clone.Depth > 1 {
55
+
depth = int(twf.Clone.Depth)
56
+
}
57
+
fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth))
58
+
59
+
// optionally recurse submodules
60
+
if twf.Clone.Submodules {
61
+
fetchArgs = append(fetchArgs, "--recurse-submodules=yes")
62
+
}
63
64
+
// set remote to fetch from
65
+
fetchArgs = append(fetchArgs, "origin")
66
67
+
// set revision to checkout
68
+
switch workflow.TriggerKind(tr.Kind) {
69
+
case workflow.TriggerKindManual:
70
+
// TODO: unimplemented
71
+
case workflow.TriggerKindPush:
72
+
fetchArgs = append(fetchArgs, tr.Push.NewSha)
73
+
case workflow.TriggerKindPullRequest:
74
+
fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha)
75
+
}
76
77
+
commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")))
78
}
79
80
+
// run git checkout
81
+
commands = append(commands, "git checkout FETCH_HEAD")
82
83
cloneStep := Step{
84
+
Command: strings.Join(commands, "\n"),
85
Name: "Clone repository into workspace",
86
}
87
return cloneStep
+1
-1
spindle/server.go
+1
-1
spindle/server.go
+8
-2
types/repo.go
+8
-2
types/repo.go
+2
-2
workflow/compile.go
+2
-2
workflow/compile.go
···
98
Name: s.Name,
99
}
100
for k, v := range s.Environment {
101
-
e := &tangled.Pipeline_Step_Environment_Elem{
102
Key: k,
103
Value: v,
104
}
···
107
cw.Steps = append(cw.Steps, &step)
108
}
109
for k, v := range w.Environment {
110
-
e := &tangled.Pipeline_Workflow_Environment_Elem{
111
Key: k,
112
Value: v,
113
}
+1
-1
workflow/compile_test.go
+1
-1
workflow/compile_test.go
+15
-8
workflow/def.go
+15
-8
workflow/def.go
···
4
"errors"
5
"fmt"
6
"slices"
7
8
"tangled.sh/tangled.sh/core/api/tangled"
9
···
51
}
52
53
StringList []string
54
)
55
56
const (
57
-
TriggerKindPush string = "push"
58
-
TriggerKindPullRequest string = "pull_request"
59
-
TriggerKindManual string = "manual"
60
)
61
62
func FromFile(name string, contents []byte) (Workflow, error) {
63
var wf Workflow
64
···
127
if refName.IsBranch() {
128
return slices.Contains(c.Branch, refName.Short())
129
}
130
-
fmt.Println("no", c.Branch, refName.Short())
131
-
132
return false
133
}
134
···
169
}
170
171
// conversion utilities to atproto records
172
-
func (d Dependencies) AsRecord() []tangled.Pipeline_Dependencies_Elem {
173
-
var deps []tangled.Pipeline_Dependencies_Elem
174
for registry, packages := range d {
175
-
deps = append(deps, tangled.Pipeline_Dependencies_Elem{
176
Registry: registry,
177
Packages: packages,
178
})
···
4
"errors"
5
"fmt"
6
"slices"
7
+
"strings"
8
9
"tangled.sh/tangled.sh/core/api/tangled"
10
···
52
}
53
54
StringList []string
55
+
56
+
TriggerKind string
57
)
58
59
const (
60
+
WorkflowDir = ".tangled/workflows"
61
+
62
+
TriggerKindPush TriggerKind = "push"
63
+
TriggerKindPullRequest TriggerKind = "pull_request"
64
+
TriggerKindManual TriggerKind = "manual"
65
)
66
67
+
func (t TriggerKind) String() string {
68
+
return strings.ReplaceAll(string(t), "_", " ")
69
+
}
70
+
71
func FromFile(name string, contents []byte) (Workflow, error) {
72
var wf Workflow
73
···
136
if refName.IsBranch() {
137
return slices.Contains(c.Branch, refName.Short())
138
}
139
return false
140
}
141
···
176
}
177
178
// conversion utilities to atproto records
179
+
func (d Dependencies) AsRecord() []*tangled.Pipeline_Dependency {
180
+
var deps []*tangled.Pipeline_Dependency
181
for registry, packages := range d {
182
+
deps = append(deps, &tangled.Pipeline_Dependency{
183
Registry: registry,
184
Packages: packages,
185
})