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

Compare changes

Choose any two refs to compare.

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