forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+5 -1
.tangled/workflows/build.yml
··· 1 1 when: 2 - - event: ["push"] 2 + - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 5 dependencies: ··· 22 22 - name: build knot 23 23 command: | 24 24 go build -o knot.out ./cmd/knot 25 + 26 + - name: build spindle 27 + command: | 28 + go build -o spindle.out ./cmd/spindle
+1 -1
.tangled/workflows/fmt.yml
··· 1 1 when: 2 - - event: ["push"] 2 + - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 5 dependencies:
+2 -2
.tangled/workflows/test.yml
··· 1 1 when: 2 - - event: ["push"] 3 - branch: ["master", "test-ci"] 2 + - event: ["push", "pull_request"] 3 + branch: ["master"] 4 4 5 5 dependencies: 6 6 nixpkgs:
+269 -297
api/tangled/cbor_gen.go
··· 504 504 505 505 return nil 506 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 + } 507 705 func (t *FeedStar) MarshalCBOR(w io.Writer) error { 508 706 if t == nil { 509 707 _, err := w.Write(cbg.CborNull) ··· 2188 2386 2189 2387 return nil 2190 2388 } 2191 - func (t *Pipeline_Dependencies_Elem) MarshalCBOR(w io.Writer) error { 2389 + func (t *Pipeline_Dependency) MarshalCBOR(w io.Writer) error { 2192 2390 if t == nil { 2193 2391 _, err := w.Write(cbg.CborNull) 2194 2392 return err ··· 2258 2456 return nil 2259 2457 } 2260 2458 2261 - func (t *Pipeline_Dependencies_Elem) UnmarshalCBOR(r io.Reader) (err error) { 2262 - *t = Pipeline_Dependencies_Elem{} 2459 + func (t *Pipeline_Dependency) UnmarshalCBOR(r io.Reader) (err error) { 2460 + *t = Pipeline_Dependency{} 2263 2461 2264 2462 cr := cbg.NewCborReader(r) 2265 2463 ··· 2278 2476 } 2279 2477 2280 2478 if extra > cbg.MaxLength { 2281 - return fmt.Errorf("Pipeline_Dependencies_Elem: map struct too large (%d)", extra) 2479 + return fmt.Errorf("Pipeline_Dependency: map struct too large (%d)", extra) 2282 2480 } 2283 2481 2284 2482 n := extra ··· 2378 2576 return err 2379 2577 } 2380 2578 2381 - // t.Inputs ([]*tangled.Pipeline_ManualTriggerData_Inputs_Elem) (slice) 2579 + // t.Inputs ([]*tangled.Pipeline_Pair) (slice) 2382 2580 if t.Inputs != nil { 2383 2581 2384 2582 if len("inputs") > 1000000 { ··· 2450 2648 } 2451 2649 2452 2650 switch string(nameBuf[:nameLen]) { 2453 - // t.Inputs ([]*tangled.Pipeline_ManualTriggerData_Inputs_Elem) (slice) 2651 + // t.Inputs ([]*tangled.Pipeline_Pair) (slice) 2454 2652 case "inputs": 2455 2653 2456 2654 maj, extra, err = cr.ReadHeader() ··· 2467 2665 } 2468 2666 2469 2667 if extra > 0 { 2470 - t.Inputs = make([]*Pipeline_ManualTriggerData_Inputs_Elem, extra) 2668 + t.Inputs = make([]*Pipeline_Pair, extra) 2471 2669 } 2472 2670 2473 2671 for i := 0; i < int(extra); i++ { ··· 2489 2687 if err := cr.UnreadByte(); err != nil { 2490 2688 return err 2491 2689 } 2492 - t.Inputs[i] = new(Pipeline_ManualTriggerData_Inputs_Elem) 2690 + t.Inputs[i] = new(Pipeline_Pair) 2493 2691 if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil { 2494 2692 return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err) 2495 2693 } ··· 2510 2708 2511 2709 return nil 2512 2710 } 2513 - func (t *Pipeline_ManualTriggerData_Inputs_Elem) MarshalCBOR(w io.Writer) error { 2711 + func (t *Pipeline_Pair) MarshalCBOR(w io.Writer) error { 2514 2712 if t == nil { 2515 2713 _, err := w.Write(cbg.CborNull) 2516 2714 return err ··· 2570 2768 return nil 2571 2769 } 2572 2770 2573 - func (t *Pipeline_ManualTriggerData_Inputs_Elem) UnmarshalCBOR(r io.Reader) (err error) { 2574 - *t = Pipeline_ManualTriggerData_Inputs_Elem{} 2771 + func (t *Pipeline_Pair) UnmarshalCBOR(r io.Reader) (err error) { 2772 + *t = Pipeline_Pair{} 2575 2773 2576 2774 cr := cbg.NewCborReader(r) 2577 2775 ··· 2590 2788 } 2591 2789 2592 2790 if extra > cbg.MaxLength { 2593 - return fmt.Errorf("Pipeline_ManualTriggerData_Inputs_Elem: map struct too large (%d)", extra) 2791 + return fmt.Errorf("Pipeline_Pair: map struct too large (%d)", extra) 2594 2792 } 2595 2793 2596 2794 n := extra ··· 3014 3212 3015 3213 return nil 3016 3214 } 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 3215 func (t *PipelineStatus) MarshalCBOR(w io.Writer) error { 3153 3216 if t == nil { 3154 3217 _, err := w.Write(cbg.CborNull) ··· 3511 3574 3512 3575 return nil 3513 3576 } 3514 - 3515 3577 func (t *Pipeline_Step) MarshalCBOR(w io.Writer) error { 3516 3578 if t == nil { 3517 3579 _, err := w.Write(cbg.CborNull) ··· 3575 3637 return err 3576 3638 } 3577 3639 3578 - // t.Environment ([]*tangled.Pipeline_Step_Environment_Elem) (slice) 3640 + // t.Environment ([]*tangled.Pipeline_Pair) (slice) 3579 3641 if t.Environment != nil { 3580 3642 3581 3643 if len("environment") > 1000000 { ··· 3669 3731 3670 3732 t.Command = string(sval) 3671 3733 } 3672 - // t.Environment ([]*tangled.Pipeline_Step_Environment_Elem) (slice) 3734 + // t.Environment ([]*tangled.Pipeline_Pair) (slice) 3673 3735 case "environment": 3674 3736 3675 3737 maj, extra, err = cr.ReadHeader() ··· 3686 3748 } 3687 3749 3688 3750 if extra > 0 { 3689 - t.Environment = make([]*Pipeline_Step_Environment_Elem, extra) 3751 + t.Environment = make([]*Pipeline_Pair, extra) 3690 3752 } 3691 3753 3692 3754 for i := 0; i < int(extra); i++ { ··· 3708 3770 if err := cr.UnreadByte(); err != nil { 3709 3771 return err 3710 3772 } 3711 - t.Environment[i] = new(Pipeline_Step_Environment_Elem) 3773 + t.Environment[i] = new(Pipeline_Pair) 3712 3774 if err := t.Environment[i].UnmarshalCBOR(cr); err != nil { 3713 3775 return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err) 3714 3776 } ··· 4274 4336 4275 4337 } 4276 4338 4277 - // t.Environment ([]*tangled.Pipeline_Workflow_Environment_Elem) (slice) 4339 + // t.Environment ([]*tangled.Pipeline_Pair) (slice) 4278 4340 if len("environment") > 1000000 { 4279 4341 return xerrors.Errorf("Value in field \"environment\" was too long") 4280 4342 } ··· 4300 4362 4301 4363 } 4302 4364 4303 - // t.Dependencies ([]tangled.Pipeline_Dependencies_Elem) (slice) 4365 + // t.Dependencies ([]*tangled.Pipeline_Dependency) (slice) 4304 4366 if len("dependencies") > 1000000 { 4305 4367 return xerrors.Errorf("Value in field \"dependencies\" was too long") 4306 4368 } ··· 4449 4511 4450 4512 } 4451 4513 } 4452 - // t.Environment ([]*tangled.Pipeline_Workflow_Environment_Elem) (slice) 4514 + // t.Environment ([]*tangled.Pipeline_Pair) (slice) 4453 4515 case "environment": 4454 4516 4455 4517 maj, extra, err = cr.ReadHeader() ··· 4466 4528 } 4467 4529 4468 4530 if extra > 0 { 4469 - t.Environment = make([]*Pipeline_Workflow_Environment_Elem, extra) 4531 + t.Environment = make([]*Pipeline_Pair, extra) 4470 4532 } 4471 4533 4472 4534 for i := 0; i < int(extra); i++ { ··· 4488 4550 if err := cr.UnreadByte(); err != nil { 4489 4551 return err 4490 4552 } 4491 - t.Environment[i] = new(Pipeline_Workflow_Environment_Elem) 4553 + t.Environment[i] = new(Pipeline_Pair) 4492 4554 if err := t.Environment[i].UnmarshalCBOR(cr); err != nil { 4493 4555 return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err) 4494 4556 } ··· 4498 4560 4499 4561 } 4500 4562 } 4501 - // t.Dependencies ([]tangled.Pipeline_Dependencies_Elem) (slice) 4563 + // t.Dependencies ([]*tangled.Pipeline_Dependency) (slice) 4502 4564 case "dependencies": 4503 4565 4504 4566 maj, extra, err = cr.ReadHeader() ··· 4515 4577 } 4516 4578 4517 4579 if extra > 0 { 4518 - t.Dependencies = make([]Pipeline_Dependencies_Elem, extra) 4580 + t.Dependencies = make([]*Pipeline_Dependency, extra) 4519 4581 } 4520 4582 4521 4583 for i := 0; i < int(extra); i++ { ··· 4529 4591 4530 4592 { 4531 4593 4532 - if err := t.Dependencies[i].UnmarshalCBOR(cr); err != nil { 4533 - return xerrors.Errorf("unmarshaling t.Dependencies[i]: %w", err) 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 + } 4534 4606 } 4535 4607 4536 4608 } 4537 4609 4538 4610 } 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 4611 } 4674 4612 4675 4613 default: ··· 7268 7206 } 7269 7207 7270 7208 cw := cbg.NewCborWriter(w) 7271 - fieldCount := 2 7209 + fieldCount := 3 7272 7210 7273 7211 if t.Repo == nil { 7274 7212 fieldCount-- 7275 7213 } 7276 7214 7277 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 { 7278 7239 return err 7279 7240 } 7280 7241 ··· 7376 7337 } 7377 7338 7378 7339 switch string(nameBuf[:nameLen]) { 7379 - // t.Repo (string) (string) 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) 7380 7352 case "repo": 7381 7353 7382 7354 {
+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
··· 32 32 type RepoPull_Source struct { 33 33 Branch string `json:"branch" cborgen:"branch"` 34 34 Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 35 + Sha string `json:"sha" cborgen:"sha"` 35 36 }
+13 -21
api/tangled/tangledpipeline.go
··· 29 29 Submodules bool `json:"submodules" cborgen:"submodules"` 30 30 } 31 31 32 - type Pipeline_Dependencies_Elem struct { 32 + // Pipeline_Dependency is a "dependency" in the sh.tangled.pipeline schema. 33 + type Pipeline_Dependency struct { 33 34 Packages []string `json:"packages" cborgen:"packages"` 34 35 Registry string `json:"registry" cborgen:"registry"` 35 36 } 36 37 37 38 // Pipeline_ManualTriggerData is a "manualTriggerData" in the sh.tangled.pipeline schema. 38 39 type Pipeline_ManualTriggerData struct { 39 - Inputs []*Pipeline_ManualTriggerData_Inputs_Elem `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 40 + Inputs []*Pipeline_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 40 41 } 41 42 42 - type Pipeline_ManualTriggerData_Inputs_Elem struct { 43 + // Pipeline_Pair is a "pair" in the sh.tangled.pipeline schema. 44 + type Pipeline_Pair struct { 43 45 Key string `json:"key" cborgen:"key"` 44 46 Value string `json:"value" cborgen:"value"` 45 47 } ··· 61 63 62 64 // Pipeline_Step is a "step" in the sh.tangled.pipeline schema. 63 65 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"` 66 + Command string `json:"command" cborgen:"command"` 67 + Environment []*Pipeline_Pair `json:"environment,omitempty" cborgen:"environment,omitempty"` 68 + Name string `json:"name" cborgen:"name"` 72 69 } 73 70 74 71 // Pipeline_TriggerMetadata is a "triggerMetadata" in the sh.tangled.pipeline schema. ··· 90 87 91 88 // Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema. 92 89 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"` 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"` 103 95 }
+1 -1
appview/db/artifact.go
··· 27 27 } 28 28 29 29 func (a *Artifact) ArtifactAt() syntax.ATURI { 30 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", a.Did, tangled.RepoPullNSID, a.Rkey)) 30 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", a.Did, tangled.RepoArtifactNSID, a.Rkey)) 31 31 } 32 32 33 33 func AddArtifact(e Execer, artifact Artifact) error {
+10
appview/db/db.go
··· 199 199 unique(starred_by_did, repo_at) 200 200 ); 201 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 + 202 212 create table if not exists emails ( 203 213 id integer primary key autoincrement, 204 214 did text not null,
+2 -2
appview/db/issues.go
··· 277 277 } 278 278 279 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 = ?` 280 + query := `select owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 281 281 row := e.QueryRow(query, repoAt, issueId) 282 282 283 283 var issue Issue 284 284 var createdAt string 285 - err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) 285 + err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 286 286 if err != nil { 287 287 return nil, nil, err 288 288 }
+4 -3
appview/db/pipeline.go
··· 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/go-git/go-git/v5/plumbing" 11 11 spindle "tangled.sh/tangled.sh/core/spindle/models" 12 + "tangled.sh/tangled.sh/core/workflow" 12 13 ) 13 14 14 15 type Pipeline struct { ··· 85 86 86 87 type Trigger struct { 87 88 Id int 88 - Kind string 89 + Kind workflow.TriggerKind 89 90 90 91 // push trigger fields 91 92 PushRef *string ··· 100 101 } 101 102 102 103 func (t *Trigger) IsPush() bool { 103 - return t != nil && t.Kind == "push" 104 + return t != nil && t.Kind == workflow.TriggerKindPush 104 105 } 105 106 106 107 func (t *Trigger) IsPullRequest() bool { 107 - return t != nil && t.Kind == "pull_request" 108 + return t != nil && t.Kind == workflow.TriggerKindPullRequest 108 109 } 109 110 110 111 func (t *Trigger) TargetRef() string {
+6
appview/db/pulls.go
··· 87 87 if p.PullSource != nil { 88 88 s := p.PullSource.AsRecord() 89 89 source = &s 90 + source.Sha = p.LatestSha() 90 91 } 91 92 92 93 record := tangled.RepoPull{ ··· 162 163 func (p *Pull) LatestPatch() string { 163 164 latestSubmission := p.Submissions[p.LastRoundNumber()] 164 165 return latestSubmission.Patch 166 + } 167 + 168 + func (p *Pull) LatestSha() string { 169 + latestSubmission := p.Submissions[p.LastRoundNumber()] 170 + return latestSubmission.SourceRev 165 171 } 166 172 167 173 func (p *Pull) PullAt() syntax.ATURI {
+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
··· 102 102 wg.Wait() 103 103 return results 104 104 } 105 + 106 + func (r *Resolver) InvalidateIdent(ctx context.Context, arg string) error { 107 + id, err := syntax.ParseAtIdentifier(arg) 108 + if err != nil { 109 + return err 110 + } 111 + 112 + return r.directory.Purge(ctx, *id) 113 + }
+26 -20
appview/ingester.go
··· 40 40 } 41 41 }() 42 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) 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) 62 69 } 63 70 64 71 if err != nil { 65 - l := i.Logger.With("nsid", e.Commit.Collection) 66 72 l.Error("error ingesting record", "err", err) 67 73 } 68 74
+16
appview/issues/issues.go
··· 11 11 12 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 13 "github.com/bluesky-social/indigo/atproto/data" 14 + "github.com/bluesky-social/indigo/atproto/syntax" 14 15 lexutil "github.com/bluesky-social/indigo/lex/util" 15 16 "github.com/go-chi/chi/v5" 16 17 "github.com/posthog/posthog-go" ··· 79 80 return 80 81 } 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 + 82 94 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) 83 95 if err != nil { 84 96 log.Println("failed to resolve issue owner", err) ··· 106 118 107 119 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 108 120 DidHandleMap: didHandleMap, 121 + 122 + OrderedReactionKinds: db.OrderedReactionKinds, 123 + Reactions: reactionCountMap, 124 + UserReacted: userReactions, 109 125 }) 110 126 111 127 }
+36 -14
appview/pages/pages.go
··· 690 690 IssueOwnerHandle string 691 691 DidHandleMap map[string]string 692 692 693 + OrderedReactionKinds []db.ReactionKind 694 + Reactions map[db.ReactionKind]int 695 + UserReacted map[db.ReactionKind]bool 696 + 693 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) 694 709 } 695 710 696 711 func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { ··· 797 812 AbandonedPulls []*db.Pull 798 813 MergeCheck types.MergeCheckResponse 799 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 800 820 } 801 821 802 822 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { ··· 805 825 } 806 826 807 827 type RepoPullPatchParams struct { 808 - LoggedInUser *oauth.User 809 - DidHandleMap map[string]string 810 - RepoInfo repoinfo.RepoInfo 811 - Pull *db.Pull 812 - Stack db.Stack 813 - Diff *types.NiceDiff 814 - Round int 815 - Submission *db.PullSubmission 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 816 837 } 817 838 818 839 // this name is a mouthful ··· 821 842 } 822 843 823 844 type RepoPullInterdiffParams struct { 824 - LoggedInUser *oauth.User 825 - DidHandleMap map[string]string 826 - RepoInfo repoinfo.RepoInfo 827 - Pull *db.Pull 828 - Round int 829 - Interdiff *patchutil.InterdiffResult 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 830 852 } 831 853 832 854 // this name is a mouthful
+1 -1
appview/pages/templates/layouts/repobase.html
··· 64 64 </div> 65 65 </nav> 66 66 <section 67 - class="bg-white dark:bg-gray-800 p-6 rounded relative w-full drop-shadow-sm dark:text-white" 67 + class="bg-white dark:bg-gray-800 p-6 rounded relative w-full dark:text-white" 68 68 > 69 69 {{ block "repoContent" . }}{{ end }} 70 70 </section>
+11 -10
appview/pages/templates/layouts/topbar.html
··· 1 1 {{ define "layouts/topbar" }} 2 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"> 3 + <div class="container flex justify-between p-0 items-center"> 4 4 <div id="left-items"> 5 5 <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic"> 6 6 tangled<sub>alpha</sub> ··· 19 19 {{ i "code" "size-4" }} source 20 20 </a> 21 21 </div> 22 - <div id="right-items" class="flex gap-2"> 22 + <div id="right-items" class="flex items-center gap-4"> 23 23 {{ with .LoggedInUser }} 24 - <a href="/repo/new" hx-boost="true"> 25 - {{ i "plus" "w-6 h-6" }} 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 26 </a> 27 27 {{ block "dropDown" . }} {{ end }} 28 28 {{ else }} ··· 36 36 {{ define "dropDown" }} 37 37 <details class="relative inline-block text-left"> 38 38 <summary 39 - class="cursor-pointer list-none" 39 + class="cursor-pointer list-none flex items-center" 40 40 > 41 - {{ didOrHandle .Did .Handle }} 41 + {{ $user := didOrHandle .Did .Handle }} 42 + {{ template "user/fragments/picHandleLink" $user }} 42 43 </summary> 43 44 <div 44 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" 45 46 > 46 - <a href="/{{ didOrHandle .Did .Handle }}">profile</a> 47 + <a href="/{{ $user }}">profile</a> 47 48 <a href="/knots">knots</a> 48 49 <a href="/spindles">spindles</a> 49 50 <a href="/settings">settings</a> 50 - <a href="#" 51 - hx-post="/logout" 52 - hx-swap="none" 51 + <a href="#" 52 + hx-post="/logout" 53 + hx-swap="none" 53 54 class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 54 55 logout 55 56 </a>
+1 -1
appview/pages/templates/repo/commit.html
··· 59 59 <div class="flex items-center gap-2 my-2"> 60 60 {{ i "user" "w-4 h-4" }} 61 61 {{ $committerDidOrHandle := index $.EmailToDidOrHandle $commit.Committer.Email }} 62 - <a href="/{{ $committerDidOrHandle }}">{{ $committerDidOrHandle }}</a> 62 + <a href="/{{ $committerDidOrHandle }}">{{ template "user/fragments/picHandleLink" $committerDidOrHandle }}</a> 63 63 </div> 64 64 <div class="my-1 pt-2 text-xs border-t"> 65 65 <div class="text-gray-600 dark:text-gray-300">SSH Key Fingerprint:</div>
+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
··· 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 }}
+1 -1
appview/pages/templates/repo/index.html
··· 266 266 {{ end }}" 267 267 class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 268 268 >{{ if $didOrHandle }} 269 - {{ $didOrHandle }} 269 + {{ template "user/fragments/picHandleLink" $didOrHandle }} 270 270 {{ else }} 271 271 {{ .Author.Name }} 272 272 {{ end }}</a
+6 -6
appview/pages/templates/repo/issues/fragments/issueComment.html
··· 1 1 {{ define "repo/issues/fragments/issueComment" }} 2 2 {{ with .Comment }} 3 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"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm flex-wrap"> 5 5 {{ $owner := index $.DidHandleMap .OwnerDid }} 6 - <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 6 + {{ template "user/fragments/picHandleLink" $owner }} 7 7 8 8 <span class="before:content-['ยท']"></span> 9 9 <a ··· 18 18 {{ .Created | timeFmt }} 19 19 {{ end }} 20 20 </a> 21 - 21 + 22 22 <!-- show user "hats" --> 23 23 {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 24 24 {{ if $isIssueAuthor }} ··· 29 29 30 30 {{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }} 31 31 {{ if and $isCommentOwner (not .Deleted) }} 32 - <button 33 - class="btn px-2 py-1 text-sm" 32 + <button 33 + class="btn px-2 py-1 text-sm" 34 34 hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 35 35 hx-swap="outerHTML" 36 36 hx-target="#comment-container-{{.CommentId}}" 37 37 > 38 38 {{ i "pencil" "w-4 h-4" }} 39 39 </button> 40 - <button 40 + <button 41 41 class="btn px-2 py-1 text-sm text-red-500 flex gap-2 items-center group" 42 42 hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 43 43 hx-confirm="Are you sure you want to delete your comment?"
+16 -2
appview/pages/templates/repo/issues/issue.html
··· 33 33 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 34 34 opened by 35 35 {{ $owner := didOrHandle .Issue.OwnerDid .IssueOwnerHandle }} 36 - {{ template "user/fragments/picHandle" $owner }} 36 + {{ template "user/fragments/picHandleLink" $owner }} 37 37 <span class="select-none before:content-['\00B7']"></span> 38 38 <time title="{{ .Issue.Created | longTimeFmt }}"> 39 39 {{ .Issue.Created | timeFmt }} ··· 46 46 {{ .Issue.Body | markdown }} 47 47 </article> 48 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> 49 63 </section> 50 64 {{ end }} 51 65 ··· 76 90 > 77 91 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5"> 78 92 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 79 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 93 + {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} 80 94 </div> 81 95 <textarea 82 96 id="comment-textarea"
+2 -2
appview/pages/templates/repo/issues/issues.html
··· 27 27 </div> 28 28 <a 29 29 href="/{{ .RepoInfo.FullName }}/issues/new" 30 - class="btn text-sm flex items-center justify-center gap-2 no-underline hover:no-underline" 30 + class="btn-create text-sm flex items-center justify-center gap-2 no-underline hover:no-underline hover:text-white" 31 31 > 32 32 {{ i "circle-plus" "w-4 h-4" }} 33 33 <span>new</span> ··· 66 66 67 67 <span class="ml-1"> 68 68 {{ $owner := index $.DidHandleMap .OwnerDid }} 69 - {{ template "user/fragments/picHandle" $owner }} 69 + {{ template "user/fragments/picHandleLink" $owner }} 70 70 </span> 71 71 72 72 <span class="before:content-['ยท']">
+5 -4
appview/pages/templates/repo/issues/new.html
··· 23 23 ></textarea> 24 24 </div> 25 25 <div> 26 - <button type="submit" class="btn flex items-center gap-2"> 27 - create 28 - <span id="spinner" class="group"> 29 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 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" }} 30 31 </span> 31 32 </button> 32 33 </div>
+2 -2
appview/pages/templates/repo/log.html
··· 31 31 <td class=" py-3 align-top"> 32 32 {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 33 33 {{ if $didOrHandle }} 34 - <a href="/{{ $didOrHandle }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $didOrHandle }}</a> 34 + {{ template "user/fragments/picHandleLink" $didOrHandle }} 35 35 {{ else }} 36 36 <a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a> 37 37 {{ end }} ··· 159 159 {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 160 160 <a href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}" 161 161 class="text-gray-500 dark:text-gray-400 no-underline hover:underline"> 162 - {{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }} 162 + {{ if $didOrHandle }}{{ template "user/fragments/picHandleLink" $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }} 163 163 </a> 164 164 </span> 165 165 <div class="inline-block px-1 select-none after:content-['ยท']"></div>
+8 -7
appview/pages/templates/repo/new.html
··· 60 60 </fieldset> 61 61 62 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> 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> 70 71 </div> 71 72 </form> 72 73 </div>
+3 -4
appview/pages/templates/repo/pipelines/fragments/logBlock.html
··· 1 1 {{ define "repo/pipelines/fragments/logBlock" }} 2 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"> 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 5 <div class="group-open:hidden flex items-center gap-1"> 6 6 {{ i "chevron-right" "w-4 h-4" }} {{ .Name }} 7 7 </div> ··· 9 9 {{ i "chevron-down" "w-4 h-4" }} {{ .Name }} 10 10 </div> 11 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> 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> 14 13 </details> 15 14 </div> 16 15 {{ end }}
+1 -3
appview/pages/templates/repo/pipelines/fragments/logLine.html
··· 1 1 {{ define "repo/pipelines/fragments/logLine" }} 2 - <div id="step-body-{{ .Id }}" hx-swap-oob="beforeend"> 3 - <p>{{ .Content }}</p> 4 - </div> 2 + <div id="step-body-{{ .Id }}" hx-swap-oob="beforeend" class="whitespace-pre"><p>{{ .Content }}</p></div> 5 3 {{ end }} 6 4
+1 -1
appview/pages/templates/repo/pipelines/fragments/tooltip.html
··· 10 10 {{ $lastStatus := $all.Latest }} 11 11 {{ $kind := $lastStatus.Status.String }} 12 12 13 - {{ $t := $pipeline.TimeTaken }} 13 + {{ $t := .TimeTaken }} 14 14 {{ $time := "" }} 15 15 {{ if $t }} 16 16 {{ $time = durationFmt $t }}
+13 -8
appview/pages/templates/repo/pipelines/pipelines.html
··· 34 34 {{ $p := index . 1 }} 35 35 {{ with $p }} 36 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"> 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"> 38 42 {{ $target := .Trigger.TargetRef }} 39 43 {{ $workflows := .Workflows }} 40 44 {{ $link := "" }} ··· 43 47 {{ end }} 44 48 {{ if .Trigger.IsPush }} 45 49 <span class="font-bold">{{ $target }}</span> 46 - <span>push</span> 47 50 <span class="hidden md:inline-flex gap-2 items-center font-mono text-sm"> 48 51 {{ $old := deref .Trigger.PushOldSha }} 49 52 {{ $new := deref .Trigger.PushNewSha }} ··· 53 56 <a href="/{{ $root.RepoInfo.FullName }}/commit/{{ $old }}">{{ slice $old 0 8 }}</a> 54 57 </span> 55 58 {{ 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 }} 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> 62 67 </span> 63 68 </span> 64 69 {{ end }}
+17 -1
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 29 29 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 30 30 opened by 31 31 {{ $owner := index $.DidHandleMap .Pull.OwnerDid }} 32 - {{ template "user/fragments/picHandle" $owner }} 32 + {{ template "user/fragments/picHandleLink" $owner }} 33 33 <span class="select-none before:content-['\00B7']"></span> 34 34 <time>{{ .Pull.Created | timeFmt }}</time> 35 35 ··· 60 60 <article id="body" class="mt-8 prose dark:prose-invert"> 61 61 {{ .Pull.Body | markdown }} 62 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> 63 79 {{ end }} 64 80 </section> 65 81
+3 -4
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 1 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 - <div 3 - id="pull-comment-card-{{ .RoundNumber }}" 2 + <div 3 + id="pull-comment-card-{{ .RoundNumber }}" 4 4 class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 5 <div class="text-sm text-gray-500 dark:text-gray-400"> 6 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 6 + {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} 7 7 </div> 8 8 <form 9 9 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" ··· 38 38 </form> 39 39 </div> 40 40 {{ end }} 41 -
+3 -2
appview/pages/templates/repo/pulls/fragments/pullStack.html
··· 10 10 {{ i "chevrons-down-up" "w-4 h-4" }} 11 11 </span> 12 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> 13 + <span class="bg-gray-200 dark:bg-gray-700 font-normal rounded py-1/2 px-1 text-sm">{{ len .Stack }}</span> 14 14 </span> 15 15 </summary> 16 16 {{ block "pullList" (list .Stack $) }} {{ end }} ··· 41 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 42 {{ range $pull := $list }} 43 43 {{ $isCurrent := false }} 44 + {{ $pipeline := index $root.Pipelines $pull.LatestSha }} 44 45 {{ with $root.Pull }} 45 46 {{ $isCurrent = eq $pull.PullId $root.Pull.PullId }} 46 47 {{ end }} ··· 52 53 </div> 53 54 {{ end }} 54 55 <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 + {{ template "repo/pulls/fragments/summarizedHeader" (list $pull $pipeline) }} 56 57 </div> 57 58 </div> 58 59 </a>
+36 -26
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 1 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 }} 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> 6 14 </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 15 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> 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> 29 39 </div> 30 - </div> 40 + {{ end }} 31 41 {{ end }} 32 42
+1 -1
appview/pages/templates/repo/pulls/new.html
··· 141 141 </div> 142 142 143 143 <div class="flex justify-start items-center gap-2 mt-4"> 144 - <button type="submit" class="btn flex items-center gap-2"> 144 + <button type="submit" class="btn-create flex items-center gap-2"> 145 145 {{ i "git-pull-request-create" "w-4 h-4" }} 146 146 create pull 147 147 <span id="create-pull-spinner" class="group">
+47 -8
appview/pages/templates/repo/pulls/pull.html
··· 5 5 {{ define "extrameta" }} 6 6 {{ $title := printf "%s &middot; pull #%d &middot; %s" .Pull.Title .Pull.PullId .RepoInfo.FullName }} 7 7 {{ $url := printf "https://tangled.sh/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }} 8 - 8 + 9 9 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 10 10 {{ end }} 11 11 ··· 46 46 </div> 47 47 <!-- round summary --> 48 48 <div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400"> 49 - <span> 49 + <span class="gap-1 flex items-center"> 50 50 {{ $owner := index $.DidHandleMap $.Pull.OwnerDid }} 51 51 {{ $re := "re" }} 52 52 {{ if eq .RoundNumber 0 }} 53 53 {{ $re = "" }} 54 54 {{ end }} 55 55 <span class="hidden md:inline">{{$re}}submitted</span> 56 - by <a href="/{{ $owner }}">{{ $owner }}</a> 56 + by {{ template "user/fragments/picHandleLink" $owner }} 57 57 <span class="select-none before:content-['\00B7']"></span> 58 58 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}"><time>{{ .Created | shortTimeFmt }}</time></a> 59 59 <span class="select-none before:content-['ยท']"></span> ··· 68 68 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 69 69 hx-boost="true" 70 70 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 71 - {{ i "file-diff" "w-4 h-4" }} 71 + {{ i "file-diff" "w-4 h-4" }} 72 72 <span class="hidden md:inline">diff</span> 73 73 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 74 74 </a> ··· 150 150 {{ if gt $cidx 0 }} 151 151 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 152 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> 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 156 <span class="before:content-['ยท']"></span> 157 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 158 </div> ··· 162 162 </div> 163 163 {{ end }} 164 164 165 + {{ block "pipelineStatus" (list $ .) }} {{ end }} 166 + 165 167 {{ if eq $lastIdx .RoundNumber }} 166 168 {{ block "mergeStatus" $ }} {{ end }} 167 169 {{ block "resubmitStatus" $ }} {{ end }} ··· 260 262 {{ end }} 261 263 {{ end }} 262 264 263 - {{ define "commits" }} 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 }} 264 303 {{ end }}
+3 -3
appview/pages/templates/repo/pulls/pulls.html
··· 34 34 </div> 35 35 <a 36 36 href="/{{ .RepoInfo.FullName }}/pulls/new" 37 - class="btn text-sm flex items-center gap-2 no-underline hover:no-underline" 37 + class="btn-create text-sm flex items-center gap-2 no-underline hover:no-underline hover:text-white" 38 38 > 39 39 {{ i "git-pull-request-create" "w-4 h-4" }} 40 40 <span>new</span> ··· 76 76 </span> 77 77 78 78 <span class="ml-1"> 79 - {{ template "user/fragments/picHandle" $owner }} 79 + {{ template "user/fragments/picHandleLink" $owner }} 80 80 </span> 81 81 82 82 <span> ··· 156 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 157 <div class="flex gap-2 items-center px-6"> 158 158 <div class="flex-grow min-w-0 w-full py-2"> 159 - {{ template "repo/pulls/fragments/summarizedHeader" $pull }} 159 + {{ template "repo/pulls/fragments/summarizedHeader" (list $pull 0) }} 160 160 </div> 161 161 </div> 162 162 </a>
+7 -1
appview/pages/templates/repo/tree.html
··· 11 11 {{ template "repo/fragments/meta" . }} 12 12 {{ $title := printf "%s at %s &middot; %s" $path .Ref .RepoInfo.FullName }} 13 13 {{ $url := printf "https://tangled.sh/%s/tree/%s%s" .RepoInfo.FullName .Ref $path }} 14 - 14 + 15 15 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 16 16 {{ end }} 17 17 ··· 63 63 </div> 64 64 </a> 65 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> 66 68 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> 69 + </div> 67 70 {{ end }} 68 71 </div> 69 72 </div> ··· 80 83 </div> 81 84 </a> 82 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> 83 88 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> 89 + </div> 84 90 {{ end }} 85 91 </div> 86 92 </div>
+5 -5
appview/pages/templates/timeline.html
··· 61 61 {{ $userHandle := index $.DidHandleMap .Repo.Did }} 62 62 <div class="flex items-center"> 63 63 <p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2"> 64 - {{ template "user/fragments/picHandle" $userHandle }} 64 + {{ template "user/fragments/picHandleLink" $userHandle }} 65 65 {{ if .Source }} 66 66 forked 67 67 <a ··· 95 95 {{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }} 96 96 <div class="flex items-center"> 97 97 <p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2"> 98 - {{ template "user/fragments/picHandle" $userHandle }} 98 + {{ template "user/fragments/picHandleLink" $userHandle }} 99 99 followed 100 - {{ template "user/fragments/picHandle" $subjectHandle }} 100 + {{ template "user/fragments/picHandleLink" $subjectHandle }} 101 101 <time 102 102 class="text-gray-700 dark:text-gray-400 text-xs" 103 103 >{{ .Follow.FollowedAt | timeFmt }}</time ··· 108 108 {{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }} 109 109 {{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }} 110 110 <div class="flex items-center"> 111 - {{ template "user/fragments/picHandle" $starrerHandle }} 112 - <p class="text-gray-600 dark:text-gray-300 flex items-center gap-2"> 111 + <p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2"> 112 + {{ template "user/fragments/picHandleLink" $starrerHandle }} 113 113 starred 114 114 <a 115 115 href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}"
+6 -8
appview/pages/templates/user/fragments/picHandle.html
··· 1 1 {{ define "user/fragments/picHandle" }} 2 - <a href="/{{ . }}" class="flex items-center"> 3 - <img 4 - src="{{ tinyAvatar . }}" 5 - alt="{{ . }}" 6 - class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700" 7 - /> 8 - {{ . | truncateAt30 }} 9 - </a> 2 + <img 3 + src="{{ tinyAvatar . }}" 4 + alt="{{ . }}" 5 + class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700" 6 + /> 7 + {{ . | truncateAt30 }} 10 8 {{ end }}
+5
appview/pages/templates/user/fragments/picHandleLink.html
··· 1 + {{ define "user/fragments/picHandleLink" }} 2 + <a href="/{{ . }}" class="flex items-center"> 3 + {{ template "user/fragments/picHandle" . }} 4 + </a> 5 + {{ end }}
+69 -12
appview/pulls/pulls.go
··· 167 167 resubmitResult = s.resubmitCheck(f, pull, stack) 168 168 } 169 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 + 170 212 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 171 213 LoggedInUser: user, 172 - RepoInfo: f.RepoInfo(user), 214 + RepoInfo: repoInfo, 173 215 DidHandleMap: didHandleMap, 174 216 Pull: pull, 175 217 Stack: stack, 176 218 AbandonedPulls: abandonedPulls, 177 219 MergeCheck: mergeCheckResponse, 178 220 ResubmitCheck: resubmitResult, 221 + Pipelines: m, 222 + 223 + OrderedReactionKinds: db.OrderedReactionKinds, 224 + Reactions: reactionCountMap, 225 + UserReacted: userReactions, 179 226 }) 180 227 } 181 228 ··· 798 845 sourceBranch string, 799 846 isStacked bool, 800 847 ) { 801 - pullSource := &db.PullSource{ 802 - Branch: sourceBranch, 803 - } 804 - recordPullSource := &tangled.RepoPull_Source{ 805 - Branch: sourceBranch, 806 - } 807 - 808 848 // Generate a patch using /compare 809 849 ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 810 850 if err != nil { ··· 828 868 return 829 869 } 830 870 871 + pullSource := &db.PullSource{ 872 + Branch: sourceBranch, 873 + } 874 + recordPullSource := &tangled.RepoPull_Source{ 875 + Branch: sourceBranch, 876 + Sha: comparison.Rev2, 877 + } 878 + 831 879 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource, isStacked) 832 880 } 833 881 ··· 914 962 return 915 963 } 916 964 917 - s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{ 965 + pullSource := &db.PullSource{ 918 966 Branch: sourceBranch, 919 967 RepoAt: &forkAtUri, 920 - }, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri}, isStacked) 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) 921 976 } 922 977 923 978 func (s *Pulls) createPullRequest( ··· 934 989 ) { 935 990 if isStacked { 936 991 // creates a series of PRs, each linking to the previous, identified by jj's change-id 937 - s.createStackedPulLRequest( 992 + s.createStackedPullRequest( 938 993 w, 939 994 r, 940 995 f, ··· 1049 1104 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 1050 1105 } 1051 1106 1052 - func (s *Pulls) createStackedPulLRequest( 1107 + func (s *Pulls) createStackedPullRequest( 1053 1108 w http.ResponseWriter, 1054 1109 r *http.Request, 1055 1110 f *reporesolver.ResolvedRepo, ··· 1566 1621 if pull.IsBranchBased() { 1567 1622 recordPullSource = &tangled.RepoPull_Source{ 1568 1623 Branch: pull.PullSource.Branch, 1624 + Sha: sourceRev, 1569 1625 } 1570 1626 } 1571 1627 if pull.IsForkBased() { ··· 1573 1629 recordPullSource = &tangled.RepoPull_Source{ 1574 1630 Branch: pull.PullSource.Branch, 1575 1631 Repo: &repoAt, 1632 + Sha: sourceRev, 1576 1633 } 1577 1634 } 1578 1635
+1 -1
appview/repo/index.go
··· 133 133 for _, c := range commitsTrunc { 134 134 shas = append(shas, c.Hash.String()) 135 135 } 136 - pipelines, err := rp.getPipelineStatuses(repoInfo, shas) 136 + pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 137 137 if err != nil { 138 138 log.Printf("failed to fetch pipeline statuses: %s", err) 139 139 // non-fatal
+2 -2
appview/repo/repo.go
··· 139 139 for _, c := range repolog.Commits { 140 140 shas = append(shas, c.Hash.String()) 141 141 } 142 - pipelines, err := rp.getPipelineStatuses(repoInfo, shas) 142 + pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 143 143 if err != nil { 144 144 log.Println(err) 145 145 // non-fatal ··· 304 304 305 305 user := rp.oauth.GetUser(r) 306 306 repoInfo := f.RepoInfo(user) 307 - pipelines, err := rp.getPipelineStatuses(repoInfo, []string{result.Diff.Commit.This}) 307 + pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This}) 308 308 if err != nil { 309 309 log.Println(err) 310 310 // non-fatal
+3 -2
appview/repo/repo_util.go
··· 105 105 // grab pipelines from DB and munge that into a hashmap with commit sha as key 106 106 // 107 107 // golang is so blessed that it requires 35 lines of imperative code for this 108 - func (rp *Repo) getPipelineStatuses( 108 + func getPipelineStatuses( 109 + d *db.DB, 109 110 repoInfo repoinfo.RepoInfo, 110 111 shas []string, 111 112 ) (map[string]db.Pipeline, error) { ··· 116 117 } 117 118 118 119 ps, err := db.GetPipelineStatuses( 119 - rp.db, 120 + d, 120 121 db.FilterEq("repo_owner", repoInfo.OwnerDid), 121 122 db.FilterEq("repo_name", repoInfo.Name), 122 123 db.FilterEq("knot", repoInfo.Knot),
+7 -8
appview/state/knotstream.go
··· 143 143 // trigger info 144 144 var trigger db.Trigger 145 145 var sha string 146 - switch record.TriggerMetadata.Kind { 146 + trigger.Kind = workflow.TriggerKind(record.TriggerMetadata.Kind) 147 + switch trigger.Kind { 147 148 case workflow.TriggerKindPush: 148 - trigger.Kind = workflow.TriggerKindPush 149 149 trigger.PushRef = &record.TriggerMetadata.Push.Ref 150 150 trigger.PushNewSha = &record.TriggerMetadata.Push.NewSha 151 151 trigger.PushOldSha = &record.TriggerMetadata.Push.OldSha 152 152 sha = *trigger.PushNewSha 153 153 case workflow.TriggerKindPullRequest: 154 - trigger.Kind = workflow.TriggerKindPush 155 154 trigger.PRSourceBranch = &record.TriggerMetadata.PullRequest.SourceBranch 156 155 trigger.PRTargetBranch = &record.TriggerMetadata.PullRequest.TargetBranch 157 156 trigger.PRSourceSha = &record.TriggerMetadata.PullRequest.SourceSha ··· 161 160 162 161 tx, err := d.Begin() 163 162 if err != nil { 164 - return err 163 + return fmt.Errorf("failed to start txn: %w", err) 165 164 } 166 165 167 166 triggerId, err := db.AddTrigger(tx, trigger) 168 167 if err != nil { 169 - return err 168 + return fmt.Errorf("failed to add trigger entry: %w", err) 170 169 } 171 170 172 171 pipeline := db.Pipeline{ ··· 180 179 181 180 err = db.AddPipeline(tx, pipeline) 182 181 if err != nil { 183 - return err 182 + return fmt.Errorf("failed to add pipeline: %w", err) 184 183 } 185 184 186 185 err = tx.Commit() 187 186 if err != nil { 188 - return err 187 + return fmt.Errorf("failed to commit txn: %w", err) 189 188 } 190 189 191 - return err 190 + return nil 192 191 }
+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
··· 137 137 r.Delete("/", s.Star) 138 138 }) 139 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 + 140 145 r.Route("/profile", func(r chi.Router) { 141 146 r.Use(middleware.AuthMiddleware(s.oauth)) 142 147 r.Get("/edit-bio", s.EditBioFragment)
+7 -1
appview/state/spindlestream.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "fmt" 6 7 "log/slog" 7 8 "strings" 8 9 "time" ··· 100 101 ExitCode: exitCode, 101 102 } 102 103 103 - return db.AddPipelineStatus(d, status) 104 + err = db.AddPipelineStatus(d, status) 105 + if err != nil { 106 + return fmt.Errorf("failed to add pipeline status: %w", err) 107 + } 108 + 109 + return nil 104 110 }
+3 -4
cmd/gen.go
··· 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 17 tangled.ActorProfile{}, 18 + tangled.FeedReaction{}, 18 19 tangled.FeedStar{}, 19 20 tangled.GitRefUpdate{}, 20 21 tangled.GitRefUpdate_Meta{}, ··· 24 25 tangled.KnotMember{}, 25 26 tangled.Pipeline{}, 26 27 tangled.Pipeline_CloneOpts{}, 27 - tangled.Pipeline_Dependencies_Elem{}, 28 + tangled.Pipeline_Dependency{}, 28 29 tangled.Pipeline_ManualTriggerData{}, 29 - tangled.Pipeline_ManualTriggerData_Inputs_Elem{}, 30 + tangled.Pipeline_Pair{}, 30 31 tangled.Pipeline_PullRequestTriggerData{}, 31 32 tangled.Pipeline_PushTriggerData{}, 32 - tangled.Pipeline_Step_Environment_Elem{}, 33 33 tangled.PipelineStatus{}, 34 34 tangled.Pipeline_Step{}, 35 35 tangled.Pipeline_TriggerMetadata{}, 36 36 tangled.Pipeline_TriggerRepo{}, 37 37 tangled.Pipeline_Workflow{}, 38 - tangled.Pipeline_Workflow_Environment_Elem{}, 39 38 tangled.PublicKey{}, 40 39 tangled.Repo{}, 41 40 tangled.RepoArtifact{},
+5 -4
docs/hacking.md
··· 47 47 `nixos-shell` like so: 48 48 49 49 ```bash 50 - QEMU_NET_OPTS="hostfwd=tcp::6000-:6000,hostfwd=tcp::2222-:22" nixos-shell --flake .#knotVM 50 + nix run .#vm 51 + # or nixos-shell --flake .#vm 51 52 52 53 # hit Ctrl-a + c + q to exit the VM 53 54 ``` 54 55 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: 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: 58 59 59 60 ```bash 60 61 Host nixos-shell
+33
docs/knot-hosting.md
··· 89 89 systemctl start knotserver 90 90 ``` 91 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 + 92 125 You should now have a running knot server! You can finalize your registration by hitting the 93 126 `initialize` button on the [/knots](/knots) page. 94 127
+12 -3
docs/spindle/hosting.md
··· 31 31 2. **Build the Spindle binary.** 32 32 33 33 ```shell 34 - go build -o spindle core/spindle/server.go 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 35 44 ``` 36 45 37 - 3. **Run the Spindle binary.** 46 + 4. **Run the Spindle binary.** 38 47 39 48 ```shell 40 - ./spindle 49 + ./cmd/spindle/spindle 41 50 ``` 42 51 43 52 Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
+12 -1
eventconsumer/consumer.go
··· 172 172 func (c *Consumer) startConnectionLoop(ctx context.Context, source Source) { 173 173 defer c.wg.Done() 174 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 175 185 for { 176 186 select { 177 187 case <-ctx.Done(): 178 188 return 179 - default: 189 + case <-timer.C: 180 190 err := c.runConnection(ctx, source) 181 191 if err != nil { 182 192 c.logger.Error("failed to run connection", "err", err) 183 193 } 194 + timer.Reset(1 * time.Minute) 184 195 } 185 196 } 186 197 }
+47 -50
flake.nix
··· 53 53 }: let 54 54 supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; 55 55 forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 56 - nixpkgsFor = forAllSystems (system: 57 - import nixpkgs { 58 - inherit system; 59 - overlays = [self.overlays.default]; 60 - }); 56 + nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); 61 57 inherit (gitignore.lib) gitignoreSource; 62 - in { 63 - overlays.default = final: prev: let 58 + mkPackageSet = pkgs: let 64 59 goModHash = "sha256-SLi+nALwCd/Lzn3aljwPqCo2UaM9hl/4OAjcHQLt2Bk="; 65 - appviewDeps = { 66 - inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src goModHash gitignoreSource; 60 + sqlite-lib = pkgs.callPackage ./nix/pkgs/sqlite-lib.nix { 61 + inherit (pkgs) gcc; 62 + inherit sqlite-lib-src; 67 63 }; 68 - knotDeps = { 69 - inherit goModHash gitignoreSource; 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; 70 68 }; 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; 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; 88 77 89 78 packages = forAllSystems (system: let 90 79 pkgs = nixpkgsFor.${system}; 91 - staticPkgs = pkgs.pkgsStatic; 92 - crossPkgs = pkgs.pkgsCross.gnu64.pkgsStatic; 80 + packages = mkPackageSet pkgs; 81 + staticPackages = mkPackageSet pkgs.pkgsStatic; 82 + crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 93 83 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; 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; 101 91 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; 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; 107 97 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; 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; 112 102 }); 113 - defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); 114 - formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 103 + defaultPackage = forAllSystems (system: self.packages.${system}.appview); 104 + formatter = forAllSystems (system: nixpkgsFor.${system}.alejandra); 115 105 devShells = forAllSystems (system: let 116 106 pkgs = nixpkgsFor.${system}; 107 + packages' = self.packages.${system}; 117 108 staticShell = pkgs.mkShell.override { 118 109 stdenv = pkgs.pkgsStatic.stdenv; 119 110 }; ··· 124 115 pkgs.air 125 116 pkgs.gopls 126 117 pkgs.httpie 127 - pkgs.lexgen 128 118 pkgs.litecli 129 119 pkgs.websocat 130 120 pkgs.tailwindcss 131 121 pkgs.nixos-shell 132 122 pkgs.redis 123 + packages'.lexgen 133 124 ]; 134 125 shellHook = '' 135 126 mkdir -p appview/pages/static/{fonts,icons} ··· 139 130 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 140 131 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 141 132 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 142 - export TANGLED_OAUTH_JWKS="$(${pkgs.genjwks}/bin/genjwks)" 133 + export TANGLED_OAUTH_JWKS="$(${packages'.genjwks}/bin/genjwks)" 143 134 ''; 144 135 env.CGO_ENABLED = 1; 145 136 }; ··· 172 163 watch-tailwind = { 173 164 type = "app"; 174 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 + ''); 175 172 }; 176 173 }); 177 174
+22 -19
input.css
··· 74 74 75 75 @layer components { 76 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; 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; 96 99 } 97 100 98 101 .prose img {
+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
··· 147 147 } 148 148 149 149 func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error { 150 - const ( 151 - WorkflowDir = ".tangled/workflows" 152 - ) 153 - 154 150 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 155 151 if err != nil { 156 152 return err ··· 166 162 return err 167 163 } 168 164 169 - workflowDir, err := gr.FileTree(context.Background(), WorkflowDir) 165 + workflowDir, err := gr.FileTree(context.Background(), workflow.WorkflowDir) 170 166 if err != nil { 171 167 return err 172 168 } ··· 177 173 continue 178 174 } 179 175 180 - fpath := filepath.Join(WorkflowDir, e.Name) 176 + fpath := filepath.Join(workflow.WorkflowDir, e.Name) 181 177 contents, err := gr.RawContent(fpath) 182 178 if err != nil { 183 179 continue
-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 - }
+1
knotserver/server.go
··· 75 75 jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{ 76 76 tangled.PublicKeyNSID, 77 77 tangled.KnotMemberNSID, 78 + tangled.RepoPullNSID, 78 79 }, nil, logger, db, true, c.Server.LogDids) 79 80 if err != nil { 80 81 logger.Error("failed to setup jetstream", "error", err)
+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
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["triggerMetadata", "workflows"], 12 + "required": [ 13 + "triggerMetadata", 14 + "workflows" 15 + ], 13 16 "properties": { 14 17 "triggerMetadata": { 15 18 "type": "ref", ··· 27 30 }, 28 31 "triggerMetadata": { 29 32 "type": "object", 30 - "required": ["kind", "repo"], 33 + "required": [ 34 + "kind", 35 + "repo" 36 + ], 31 37 "properties": { 32 38 "kind": { 33 39 "type": "string", 34 - "enum": ["push", "pull_request", "manual"] 40 + "enum": [ 41 + "push", 42 + "pull_request", 43 + "manual" 44 + ] 35 45 }, 36 46 "repo": { 37 47 "type": "ref", ··· 53 63 }, 54 64 "triggerRepo": { 55 65 "type": "object", 56 - "required": ["knot", "did", "repo", "defaultBranch"], 66 + "required": [ 67 + "knot", 68 + "did", 69 + "repo", 70 + "defaultBranch" 71 + ], 57 72 "properties": { 58 73 "knot": { 59 74 "type": "string" ··· 72 87 }, 73 88 "pushTriggerData": { 74 89 "type": "object", 75 - "required": ["ref", "newSha", "oldSha"], 90 + "required": [ 91 + "ref", 92 + "newSha", 93 + "oldSha" 94 + ], 76 95 "properties": { 77 96 "ref": { 78 97 "type": "string" ··· 91 110 }, 92 111 "pullRequestTriggerData": { 93 112 "type": "object", 94 - "required": ["sourceBranch", "targetBranch", "sourceSha", "action"], 113 + "required": [ 114 + "sourceBranch", 115 + "targetBranch", 116 + "sourceSha", 117 + "action" 118 + ], 95 119 "properties": { 96 120 "sourceBranch": { 97 121 "type": "string" ··· 115 139 "inputs": { 116 140 "type": "array", 117 141 "items": { 118 - "type": "object", 119 - "required": ["key", "value"], 120 - "properties": { 121 - "key": { 122 - "type": "string" 123 - }, 124 - "value": { 125 - "type": "string" 126 - } 127 - } 142 + "type": "ref", 143 + "ref": "#pair" 128 144 } 129 145 } 130 146 } 131 147 }, 132 148 "workflow": { 133 149 "type": "object", 134 - "required": ["name", "dependencies", "steps", "environment", "clone"], 150 + "required": [ 151 + "name", 152 + "dependencies", 153 + "steps", 154 + "environment", 155 + "clone" 156 + ], 135 157 "properties": { 136 158 "name": { 137 159 "type": "string" 138 160 }, 139 161 "dependencies": { 140 - "type": "ref", 141 - "ref": "#dependencies" 162 + "type": "array", 163 + "items": { 164 + "type": "ref", 165 + "ref": "#dependency" 166 + } 142 167 }, 143 168 "steps": { 144 169 "type": "array", ··· 150 175 "environment": { 151 176 "type": "array", 152 177 "items": { 153 - "type": "object", 154 - "required": ["key", "value"], 155 - "properties": { 156 - "key": { 157 - "type": "string" 158 - }, 159 - "value": { 160 - "type": "string" 161 - } 162 - } 178 + "type": "ref", 179 + "ref": "#pair" 163 180 } 164 181 }, 165 182 "clone": { ··· 168 185 } 169 186 } 170 187 }, 171 - "dependencies": { 172 - "type": "array", 173 - "items": { 174 - "type": "object", 175 - "required": ["registry", "packages"], 176 - "properties": { 177 - "registry": { 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": { 178 201 "type": "string" 179 - }, 180 - "packages": { 181 - "type": "array", 182 - "items": { 183 - "type": "string" 184 - } 185 202 } 186 203 } 187 204 } 188 205 }, 189 206 "cloneOpts": { 190 207 "type": "object", 191 - "required": ["skip", "depth", "submodules"], 208 + "required": [ 209 + "skip", 210 + "depth", 211 + "submodules" 212 + ], 192 213 "properties": { 193 214 "skip": { 194 215 "type": "boolean" ··· 203 224 }, 204 225 "step": { 205 226 "type": "object", 206 - "required": ["name", "command"], 227 + "required": [ 228 + "name", 229 + "command" 230 + ], 207 231 "properties": { 208 232 "name": { 209 233 "type": "string" ··· 214 238 "environment": { 215 239 "type": "array", 216 240 "items": { 217 - "type": "object", 218 - "required": ["key", "value"], 219 - "properties": { 220 - "key": { 221 - "type": "string" 222 - }, 223 - "value": { 224 - "type": "string" 225 - } 226 - } 241 + "type": "ref", 242 + "ref": "#pair" 227 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" 228 259 } 229 260 } 230 261 }
+7 -1
lexicons/pulls/pull.json
··· 51 51 "source": { 52 52 "type": "object", 53 53 "required": [ 54 - "branch" 54 + "branch", 55 + "sha" 55 56 ], 56 57 "properties": { 57 58 "branch": { 58 59 "type": "string" 60 + }, 61 + "sha": { 62 + "type": "string", 63 + "minLength": 40, 64 + "maxLength": 40 59 65 }, 60 66 "repo": { 61 67 "type": "string",
+1 -1
spindle/engine/engine.go
··· 90 90 91 91 reader, err := e.docker.ImagePull(ctx, w.Image, image.PullOptions{}) 92 92 if err != nil { 93 - e.l.Error("pipeline failed!", "workflowId", wid, "error", err.Error()) 93 + e.l.Error("pipeline image pull failed!", "image", w.Image, "workflowId", wid, "error", err.Error()) 94 94 95 95 err := e.db.StatusFailed(wid, err.Error(), -1, e.n) 96 96 if err != nil {
+2 -2
spindle/engine/envs_test.go
··· 34 34 if got == nil { 35 35 got = EnvVars{} 36 36 } 37 - assert.Equal(t, tt.want, got) 37 + assert.ElementsMatch(t, tt.want, got) 38 38 }) 39 39 } 40 40 } ··· 44 44 ev.AddEnv("FOO", "bar") 45 45 ev.AddEnv("BAZ", "qux") 46 46 want := EnvVars{"FOO=bar", "BAZ=qux"} 47 - assert.Equal(t, want, ev) 47 + assert.ElementsMatch(t, want, ev) 48 48 }
+9 -6
spindle/models/pipeline.go
··· 69 69 70 70 setup.addStep(nixConfStep()) 71 71 setup.addStep(cloneStep(*twf, *pl.TriggerMetadata, cfg.Server.Dev)) 72 - setup.addStep(checkoutStep(*twf, *pl.TriggerMetadata)) 73 72 // this step could be empty 74 73 if s := dependencyStep(*twf); s != nil { 75 74 setup.addStep(*s) ··· 83 82 return &Pipeline{Workflows: workflows} 84 83 } 85 84 86 - func workflowEnvToMap(envs []*tangled.Pipeline_Workflow_Environment_Elem) map[string]string { 85 + func workflowEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string { 87 86 envMap := map[string]string{} 88 87 for _, env := range envs { 89 - envMap[env.Key] = env.Value 88 + if env != nil { 89 + envMap[env.Key] = env.Value 90 + } 90 91 } 91 92 return envMap 92 93 } 93 94 94 - func stepEnvToMap(envs []*tangled.Pipeline_Step_Environment_Elem) map[string]string { 95 + func stepEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string { 95 96 envMap := map[string]string{} 96 97 for _, env := range envs { 97 - envMap[env.Key] = env.Value 98 + if env != nil { 99 + envMap[env.Key] = env.Value 100 + } 98 101 } 99 102 return envMap 100 103 } 101 104 102 - func workflowImage(deps []tangled.Pipeline_Dependencies_Elem, nixery string) string { 105 + func workflowImage(deps []*tangled.Pipeline_Dependency, nixery string) string { 103 106 var dependencies string 104 107 for _, d := range deps { 105 108 if d.Registry == "nixpkgs" {
+45 -56
spindle/models/setup_steps.go
··· 5 5 "path" 6 6 "strings" 7 7 8 - "github.com/go-git/go-git/v5/plumbing" 9 8 "tangled.sh/tangled.sh/core/api/tangled" 10 9 "tangled.sh/tangled.sh/core/workflow" 11 10 ) ··· 19 18 } 20 19 } 21 20 22 - // checkoutStep checks out the specified ref in the cloned repository. 23 - func checkoutStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata) Step { 24 - if twf.Clone.Skip { 25 - return Step{} 26 - } 27 - 28 - var ref string 29 - switch tr.Kind { 30 - case "push": 31 - ref = tr.Push.NewSha 32 - case "pull_request": 33 - ref = tr.PullRequest.TargetBranch 34 - 35 - // TODO: this needs to be specified in lexicon 36 - case "manual": 37 - ref = tr.Repo.DefaultBranch 38 - } 39 - 40 - checkoutCmd := fmt.Sprintf("git config advice.detachedHead false; git checkout --progress --force %s", ref) 41 - 42 - return Step{ 43 - Command: checkoutCmd, 44 - Name: "Checkout ref " + ref, 45 - } 46 - } 47 - 48 21 // cloneOptsAsSteps processes clone options and adds corresponding steps 49 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 50 29 func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step { 51 30 if twf.Clone.Skip { 52 31 return Step{} 53 32 } 54 33 55 - uri := "https://" 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://" 56 41 if dev { 57 - uri = "http://" 42 + scheme = "http://" 58 43 tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal") 59 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)) 60 47 61 - cloneUrl := uri + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo) 62 - cloneCmd := []string{"git", "clone", cloneUrl, "."} 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)) 63 58 64 - // default clone depth is 1 65 - cloneDepth := 1 66 - if twf.Clone.Depth > 1 { 67 - cloneDepth = int(twf.Clone.Depth) 68 - } 69 - cloneCmd = append(cloneCmd, fmt.Sprintf("--depth=%d", cloneDepth)) 59 + // optionally recurse submodules 60 + if twf.Clone.Submodules { 61 + fetchArgs = append(fetchArgs, "--recurse-submodules=yes") 62 + } 70 63 71 - // select the clone branch 72 - cloneBranch := "" 73 - switch tr.Kind { 74 - case workflow.TriggerKindManual: 75 - // TODO: unimplemented 76 - case workflow.TriggerKindPush: 77 - ref := tr.Push.Ref 78 - refName := plumbing.ReferenceName(ref) 79 - cloneBranch = refName.Short() 80 - case workflow.TriggerKindPullRequest: 81 - cloneBranch = tr.PullRequest.SourceBranch 82 - } 64 + // set remote to fetch from 65 + fetchArgs = append(fetchArgs, "origin") 83 66 84 - if cloneBranch != "" { 85 - cloneCmd = append(cloneCmd, fmt.Sprintf("--branch=%s", cloneBranch)) 86 - } 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 + } 87 76 88 - if twf.Clone.Submodules { 89 - cloneCmd = append(cloneCmd, "--recursive") 77 + commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " "))) 90 78 } 91 79 92 - fmt.Println(strings.Join(cloneCmd, " ")) 80 + // run git checkout 81 + commands = append(commands, "git checkout FETCH_HEAD") 93 82 94 83 cloneStep := Step{ 95 - Command: strings.Join(cloneCmd, " "), 84 + Command: strings.Join(commands, "\n"), 96 85 Name: "Clone repository into workspace", 97 86 } 98 87 return cloneStep
+2 -2
workflow/compile.go
··· 98 98 Name: s.Name, 99 99 } 100 100 for k, v := range s.Environment { 101 - e := &tangled.Pipeline_Step_Environment_Elem{ 101 + e := &tangled.Pipeline_Pair{ 102 102 Key: k, 103 103 Value: v, 104 104 } ··· 107 107 cw.Steps = append(cw.Steps, &step) 108 108 } 109 109 for k, v := range w.Environment { 110 - e := &tangled.Pipeline_Workflow_Environment_Elem{ 110 + e := &tangled.Pipeline_Pair{ 111 111 Key: k, 112 112 Value: v, 113 113 }
+1 -1
workflow/compile_test.go
··· 9 9 ) 10 10 11 11 var trigger = tangled.Pipeline_TriggerMetadata{ 12 - Kind: TriggerKindPush, 12 + Kind: string(TriggerKindPush), 13 13 Push: &tangled.Pipeline_PushTriggerData{ 14 14 Ref: "refs/heads/main", 15 15 OldSha: strings.Repeat("0", 40),
+15 -6
workflow/def.go
··· 4 4 "errors" 5 5 "fmt" 6 6 "slices" 7 + "strings" 7 8 8 9 "tangled.sh/tangled.sh/core/api/tangled" 9 10 ··· 51 52 } 52 53 53 54 StringList []string 55 + 56 + TriggerKind string 54 57 ) 55 58 56 59 const ( 57 - TriggerKindPush string = "push" 58 - TriggerKindPullRequest string = "pull_request" 59 - TriggerKindManual string = "manual" 60 + WorkflowDir = ".tangled/workflows" 61 + 62 + TriggerKindPush TriggerKind = "push" 63 + TriggerKindPullRequest TriggerKind = "pull_request" 64 + TriggerKindManual TriggerKind = "manual" 60 65 ) 66 + 67 + func (t TriggerKind) String() string { 68 + return strings.ReplaceAll(string(t), "_", " ") 69 + } 61 70 62 71 func FromFile(name string, contents []byte) (Workflow, error) { 63 72 var wf Workflow ··· 167 176 } 168 177 169 178 // conversion utilities to atproto records 170 - func (d Dependencies) AsRecord() []tangled.Pipeline_Dependencies_Elem { 171 - var deps []tangled.Pipeline_Dependencies_Elem 179 + func (d Dependencies) AsRecord() []*tangled.Pipeline_Dependency { 180 + var deps []*tangled.Pipeline_Dependency 172 181 for registry, packages := range d { 173 - deps = append(deps, tangled.Pipeline_Dependencies_Elem{ 182 + deps = append(deps, &tangled.Pipeline_Dependency{ 174 183 Registry: registry, 175 184 Packages: packages, 176 185 })