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

Compare changes

Choose any two refs to compare.

+1263 -170
api/tangled/cbor_gen.go
···
return nil
-
func (t *GitRefUpdate_LangBreakdown) MarshalCBOR(w io.Writer) error {
+
func (t *GitRefUpdate_IndividualLanguageSize) MarshalCBOR(w io.Writer) error {
if t == nil {
_, err := w.Write(cbg.CborNull)
return err
cw := cbg.NewCborWriter(w)
-
fieldCount := 1
-
if t.Inputs == nil {
-
fieldCount--
+
if _, err := cw.Write([]byte{162}); err != nil {
+
return err
-
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
+
// t.Lang (string) (string)
+
if len("lang") > 1000000 {
+
return xerrors.Errorf("Value in field \"lang\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("lang"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("lang")); err != nil {
return err
-
// t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice)
-
if t.Inputs != nil {
+
if len(t.Lang) > 1000000 {
+
return xerrors.Errorf("Value in field t.Lang was too long")
+
}
-
if len("inputs") > 1000000 {
-
return xerrors.Errorf("Value in field \"inputs\" was too long")
-
}
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Lang))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Lang)); err != nil {
+
return err
+
}
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string("inputs")); err != nil {
-
return err
-
}
+
// t.Size (int64) (int64)
+
if len("size") > 1000000 {
+
return xerrors.Errorf("Value in field \"size\" was too long")
+
}
-
if len(t.Inputs) > 8192 {
-
return xerrors.Errorf("Slice value in field t.Inputs was too long")
-
}
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("size")); err != nil {
+
return err
+
}
-
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil {
+
if t.Size >= 0 {
+
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil {
return err
-
for _, v := range t.Inputs {
-
if err := v.MarshalCBOR(cw); err != nil {
-
return err
-
}
-
+
} else {
+
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Size-1)); err != nil {
+
return err
+
return nil
-
func (t *GitRefUpdate_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) {
-
*t = GitRefUpdate_LangBreakdown{}
+
func (t *GitRefUpdate_IndividualLanguageSize) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = GitRefUpdate_IndividualLanguageSize{}
cr := cbg.NewCborReader(r)
···
if extra > cbg.MaxLength {
-
return fmt.Errorf("GitRefUpdate_LangBreakdown: map struct too large (%d)", extra)
+
return fmt.Errorf("GitRefUpdate_IndividualLanguageSize: map struct too large (%d)", extra)
n := extra
-
nameBuf := make([]byte, 6)
+
nameBuf := make([]byte, 4)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
switch string(nameBuf[:nameLen]) {
-
// t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice)
-
case "inputs":
-
-
maj, extra, err = cr.ReadHeader()
-
if err != nil {
-
return err
-
}
-
-
if extra > 8192 {
-
return fmt.Errorf("t.Inputs: array too large (%d)", extra)
-
}
+
// t.Lang (string) (string)
+
case "lang":
-
if maj != cbg.MajArray {
-
return fmt.Errorf("expected cbor array")
-
}
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
-
if extra > 0 {
-
t.Inputs = make([]*GitRefUpdate_IndividualLanguageSize, extra)
+
t.Lang = string(sval)
-
-
for i := 0; i < int(extra); i++ {
-
{
-
var maj byte
-
var extra uint64
-
var err error
-
_ = maj
-
_ = extra
-
_ = err
-
-
{
-
-
b, err := cr.ReadByte()
-
if err != nil {
-
return err
-
}
-
if b != cbg.CborNull[0] {
-
if err := cr.UnreadByte(); err != nil {
-
return err
-
}
-
t.Inputs[i] = new(GitRefUpdate_IndividualLanguageSize)
-
if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil {
-
return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err)
-
}
-
}
-
+
// t.Size (int64) (int64)
+
case "size":
+
{
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
var extraI int64
+
switch maj {
+
case cbg.MajUnsignedInt:
+
extraI = int64(extra)
+
if extraI < 0 {
+
return fmt.Errorf("int64 positive overflow")
-
+
case cbg.MajNegativeInt:
+
extraI = int64(extra)
+
if extraI < 0 {
+
return fmt.Errorf("int64 negative overflow")
+
}
+
extraI = -1 - extraI
+
default:
+
return fmt.Errorf("wrong type for int64 field: %d", maj)
+
+
t.Size = int64(extraI)
default:
···
return nil
-
func (t *GitRefUpdate_IndividualLanguageSize) MarshalCBOR(w io.Writer) error {
+
func (t *GitRefUpdate_LangBreakdown) MarshalCBOR(w io.Writer) error {
if t == nil {
_, err := w.Write(cbg.CborNull)
return err
cw := cbg.NewCborWriter(w)
+
fieldCount := 1
-
if _, err := cw.Write([]byte{162}); err != nil {
-
return err
-
}
-
-
// t.Lang (string) (string)
-
if len("lang") > 1000000 {
-
return xerrors.Errorf("Value in field \"lang\" was too long")
+
if t.Inputs == nil {
+
fieldCount--
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("lang"))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string("lang")); err != nil {
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
-
if len(t.Lang) > 1000000 {
-
return xerrors.Errorf("Value in field t.Lang was too long")
-
}
+
// t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice)
+
if t.Inputs != nil {
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Lang))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string(t.Lang)); err != nil {
-
return err
-
}
+
if len("inputs") > 1000000 {
+
return xerrors.Errorf("Value in field \"inputs\" was too long")
+
}
-
// t.Size (int64) (int64)
-
if len("size") > 1000000 {
-
return xerrors.Errorf("Value in field \"size\" was too long")
-
}
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("inputs")); err != nil {
+
return err
+
}
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string("size")); err != nil {
-
return err
-
}
+
if len(t.Inputs) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Inputs was too long")
+
}
-
if t.Size >= 0 {
-
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil {
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil {
return err
-
} else {
-
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Size-1)); err != nil {
-
return err
+
for _, v := range t.Inputs {
+
if err := v.MarshalCBOR(cw); err != nil {
+
return err
+
}
+
-
return nil
-
func (t *GitRefUpdate_IndividualLanguageSize) UnmarshalCBOR(r io.Reader) (err error) {
-
*t = GitRefUpdate_IndividualLanguageSize{}
+
func (t *GitRefUpdate_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = GitRefUpdate_LangBreakdown{}
cr := cbg.NewCborReader(r)
···
if extra > cbg.MaxLength {
-
return fmt.Errorf("GitRefUpdate_IndividualLanguageSize: map struct too large (%d)", extra)
+
return fmt.Errorf("GitRefUpdate_LangBreakdown: map struct too large (%d)", extra)
n := extra
-
nameBuf := make([]byte, 4)
+
nameBuf := make([]byte, 6)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
switch string(nameBuf[:nameLen]) {
-
// t.Lang (string) (string)
-
case "lang":
+
// t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice)
+
case "inputs":
-
{
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
-
if err != nil {
-
return err
-
}
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
-
t.Lang = string(sval)
+
if extra > 8192 {
+
return fmt.Errorf("t.Inputs: array too large (%d)", extra)
-
// t.Size (int64) (int64)
-
case "size":
-
{
-
maj, extra, err := cr.ReadHeader()
-
if err != nil {
-
return err
-
}
-
var extraI int64
-
switch maj {
-
case cbg.MajUnsignedInt:
-
extraI = int64(extra)
-
if extraI < 0 {
-
return fmt.Errorf("int64 positive overflow")
-
}
-
case cbg.MajNegativeInt:
-
extraI = int64(extra)
-
if extraI < 0 {
-
return fmt.Errorf("int64 negative overflow")
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Inputs = make([]*GitRefUpdate_IndividualLanguageSize, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
t.Inputs[i] = new(GitRefUpdate_IndividualLanguageSize)
+
if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil {
+
return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err)
+
}
+
}
+
-
extraI = -1 - extraI
-
default:
-
return fmt.Errorf("wrong type for int64 field: %d", maj)
+
-
-
t.Size = int64(extraI)
default:
···
return nil
+
func (t *LabelDefinition) MarshalCBOR(w io.Writer) error {
+
if t == nil {
+
_, err := w.Write(cbg.CborNull)
+
return err
+
}
+
+
cw := cbg.NewCborWriter(w)
+
fieldCount := 7
+
+
if t.Color == nil {
+
fieldCount--
+
}
+
+
if t.Multiple == nil {
+
fieldCount--
+
}
+
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
+
return err
+
}
+
+
// t.Name (string) (string)
+
if len("name") > 1000000 {
+
return xerrors.Errorf("Value in field \"name\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("name")); err != nil {
+
return err
+
}
+
+
if len(t.Name) > 1000000 {
+
return xerrors.Errorf("Value in field t.Name was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Name)); err != nil {
+
return err
+
}
+
+
// t.LexiconTypeID (string) (string)
+
if len("$type") > 1000000 {
+
return xerrors.Errorf("Value in field \"$type\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("$type")); err != nil {
+
return err
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.label.definition"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("sh.tangled.label.definition")); err != nil {
+
return err
+
}
+
+
// t.Color (string) (string)
+
if t.Color != nil {
+
+
if len("color") > 1000000 {
+
return xerrors.Errorf("Value in field \"color\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("color"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("color")); err != nil {
+
return err
+
}
+
+
if t.Color == nil {
+
if _, err := cw.Write(cbg.CborNull); err != nil {
+
return err
+
}
+
} else {
+
if len(*t.Color) > 1000000 {
+
return xerrors.Errorf("Value in field t.Color was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Color))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(*t.Color)); err != nil {
+
return err
+
}
+
}
+
}
+
+
// t.Scope (string) (string)
+
if len("scope") > 1000000 {
+
return xerrors.Errorf("Value in field \"scope\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("scope"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("scope")); err != nil {
+
return err
+
}
+
+
if len(t.Scope) > 1000000 {
+
return xerrors.Errorf("Value in field t.Scope was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Scope))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Scope)); err != nil {
+
return err
+
}
+
+
// t.Multiple (bool) (bool)
+
if t.Multiple != nil {
+
+
if len("multiple") > 1000000 {
+
return xerrors.Errorf("Value in field \"multiple\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("multiple"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("multiple")); err != nil {
+
return err
+
}
+
+
if t.Multiple == nil {
+
if _, err := cw.Write(cbg.CborNull); err != nil {
+
return err
+
}
+
} else {
+
if err := cbg.WriteBool(w, *t.Multiple); err != nil {
+
return err
+
}
+
}
+
}
+
+
// t.CreatedAt (string) (string)
+
if len("createdAt") > 1000000 {
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
+
return err
+
}
+
+
if len(t.CreatedAt) > 1000000 {
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
+
return err
+
}
+
+
// t.ValueType (tangled.LabelDefinition_ValueType) (struct)
+
if len("valueType") > 1000000 {
+
return xerrors.Errorf("Value in field \"valueType\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("valueType"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("valueType")); err != nil {
+
return err
+
}
+
+
if err := t.ValueType.MarshalCBOR(cw); err != nil {
+
return err
+
}
+
return nil
+
}
+
+
func (t *LabelDefinition) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = LabelDefinition{}
+
+
cr := cbg.NewCborReader(r)
+
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
defer func() {
+
if err == io.EOF {
+
err = io.ErrUnexpectedEOF
+
}
+
}()
+
+
if maj != cbg.MajMap {
+
return fmt.Errorf("cbor input should be of type map")
+
}
+
+
if extra > cbg.MaxLength {
+
return fmt.Errorf("LabelDefinition: map struct too large (%d)", extra)
+
}
+
+
n := extra
+
+
nameBuf := make([]byte, 9)
+
for i := uint64(0); i < n; i++ {
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+
if err != nil {
+
return err
+
}
+
+
if !ok {
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
continue
+
}
+
+
switch string(nameBuf[:nameLen]) {
+
// t.Name (string) (string)
+
case "name":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Name = string(sval)
+
}
+
// t.LexiconTypeID (string) (string)
+
case "$type":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.LexiconTypeID = string(sval)
+
}
+
// t.Color (string) (string)
+
case "color":
+
+
{
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Color = (*string)(&sval)
+
}
+
}
+
// t.Scope (string) (string)
+
case "scope":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Scope = string(sval)
+
}
+
// t.Multiple (bool) (bool)
+
case "multiple":
+
+
{
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
if maj != cbg.MajOther {
+
return fmt.Errorf("booleans must be major type 7")
+
}
+
+
var val bool
+
switch extra {
+
case 20:
+
val = false
+
case 21:
+
val = true
+
default:
+
return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra)
+
}
+
t.Multiple = &val
+
}
+
}
+
// t.CreatedAt (string) (string)
+
case "createdAt":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.CreatedAt = string(sval)
+
}
+
// t.ValueType (tangled.LabelDefinition_ValueType) (struct)
+
case "valueType":
+
+
{
+
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
t.ValueType = new(LabelDefinition_ValueType)
+
if err := t.ValueType.UnmarshalCBOR(cr); err != nil {
+
return xerrors.Errorf("unmarshaling t.ValueType pointer: %w", err)
+
}
+
}
+
+
}
+
+
default:
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
}
+
}
+
+
return nil
+
}
+
func (t *LabelDefinition_ValueType) MarshalCBOR(w io.Writer) error {
+
if t == nil {
+
_, err := w.Write(cbg.CborNull)
+
return err
+
}
+
+
cw := cbg.NewCborWriter(w)
+
fieldCount := 4
+
+
if t.Enum == nil {
+
fieldCount--
+
}
+
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
+
return err
+
}
+
+
// t.Enum ([]string) (slice)
+
if t.Enum != nil {
+
+
if len("enum") > 1000000 {
+
return xerrors.Errorf("Value in field \"enum\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("enum"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("enum")); err != nil {
+
return err
+
}
+
+
if len(t.Enum) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Enum was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Enum))); err != nil {
+
return err
+
}
+
for _, v := range t.Enum {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
+
+
// t.Type (string) (string)
+
if len("type") > 1000000 {
+
return xerrors.Errorf("Value in field \"type\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("type"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("type")); err != nil {
+
return err
+
}
+
+
if len(t.Type) > 1000000 {
+
return xerrors.Errorf("Value in field t.Type was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Type))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Type)); err != nil {
+
return err
+
}
+
+
// t.LexiconTypeID (string) (string)
+
if len("$type") > 1000000 {
+
return xerrors.Errorf("Value in field \"$type\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("$type")); err != nil {
+
return err
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.label.definition"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("sh.tangled.label.definition")); err != nil {
+
return err
+
}
+
+
// t.Format (string) (string)
+
if len("format") > 1000000 {
+
return xerrors.Errorf("Value in field \"format\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("format"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("format")); err != nil {
+
return err
+
}
+
+
if len(t.Format) > 1000000 {
+
return xerrors.Errorf("Value in field t.Format was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Format))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Format)); err != nil {
+
return err
+
}
+
return nil
+
}
+
+
func (t *LabelDefinition_ValueType) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = LabelDefinition_ValueType{}
+
+
cr := cbg.NewCborReader(r)
+
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
defer func() {
+
if err == io.EOF {
+
err = io.ErrUnexpectedEOF
+
}
+
}()
+
+
if maj != cbg.MajMap {
+
return fmt.Errorf("cbor input should be of type map")
+
}
+
+
if extra > cbg.MaxLength {
+
return fmt.Errorf("LabelDefinition_ValueType: map struct too large (%d)", extra)
+
}
+
+
n := extra
+
+
nameBuf := make([]byte, 6)
+
for i := uint64(0); i < n; i++ {
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+
if err != nil {
+
return err
+
}
+
+
if !ok {
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
continue
+
}
+
+
switch string(nameBuf[:nameLen]) {
+
// t.Enum ([]string) (slice)
+
case "enum":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Enum: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Enum = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Enum[i] = string(sval)
+
}
+
+
}
+
}
+
// t.Type (string) (string)
+
case "type":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Type = string(sval)
+
}
+
// t.LexiconTypeID (string) (string)
+
case "$type":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.LexiconTypeID = string(sval)
+
}
+
// t.Format (string) (string)
+
case "format":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Format = string(sval)
+
}
+
+
default:
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
}
+
}
+
+
return nil
+
}
+
func (t *LabelOp) MarshalCBOR(w io.Writer) error {
+
if t == nil {
+
_, err := w.Write(cbg.CborNull)
+
return err
+
}
+
+
cw := cbg.NewCborWriter(w)
+
+
if _, err := cw.Write([]byte{165}); err != nil {
+
return err
+
}
+
+
// t.Add ([]*tangled.LabelOp_Operand) (slice)
+
if len("add") > 1000000 {
+
return xerrors.Errorf("Value in field \"add\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("add"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("add")); err != nil {
+
return err
+
}
+
+
if len(t.Add) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Add was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Add))); err != nil {
+
return err
+
}
+
for _, v := range t.Add {
+
if err := v.MarshalCBOR(cw); err != nil {
+
return err
+
}
+
+
}
+
+
// t.LexiconTypeID (string) (string)
+
if len("$type") > 1000000 {
+
return xerrors.Errorf("Value in field \"$type\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("$type")); err != nil {
+
return err
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.label.op"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("sh.tangled.label.op")); err != nil {
+
return err
+
}
+
+
// t.Delete ([]*tangled.LabelOp_Operand) (slice)
+
if len("delete") > 1000000 {
+
return xerrors.Errorf("Value in field \"delete\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("delete"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("delete")); err != nil {
+
return err
+
}
+
+
if len(t.Delete) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Delete was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Delete))); err != nil {
+
return err
+
}
+
for _, v := range t.Delete {
+
if err := v.MarshalCBOR(cw); err != nil {
+
return err
+
}
+
+
}
+
+
// t.Subject (string) (string)
+
if len("subject") > 1000000 {
+
return xerrors.Errorf("Value in field \"subject\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("subject")); err != nil {
+
return err
+
}
+
+
if len(t.Subject) > 1000000 {
+
return xerrors.Errorf("Value in field t.Subject was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Subject)); err != nil {
+
return err
+
}
+
+
// t.PerformedAt (string) (string)
+
if len("performedAt") > 1000000 {
+
return xerrors.Errorf("Value in field \"performedAt\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("performedAt"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("performedAt")); err != nil {
+
return err
+
}
+
+
if len(t.PerformedAt) > 1000000 {
+
return xerrors.Errorf("Value in field t.PerformedAt was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.PerformedAt))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.PerformedAt)); err != nil {
+
return err
+
}
+
return nil
+
}
+
+
func (t *LabelOp) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = LabelOp{}
+
+
cr := cbg.NewCborReader(r)
+
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
defer func() {
+
if err == io.EOF {
+
err = io.ErrUnexpectedEOF
+
}
+
}()
+
+
if maj != cbg.MajMap {
+
return fmt.Errorf("cbor input should be of type map")
+
}
+
+
if extra > cbg.MaxLength {
+
return fmt.Errorf("LabelOp: map struct too large (%d)", extra)
+
}
+
+
n := extra
+
+
nameBuf := make([]byte, 11)
+
for i := uint64(0); i < n; i++ {
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+
if err != nil {
+
return err
+
}
+
+
if !ok {
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
continue
+
}
+
+
switch string(nameBuf[:nameLen]) {
+
// t.Add ([]*tangled.LabelOp_Operand) (slice)
+
case "add":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Add: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Add = make([]*LabelOp_Operand, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
t.Add[i] = new(LabelOp_Operand)
+
if err := t.Add[i].UnmarshalCBOR(cr); err != nil {
+
return xerrors.Errorf("unmarshaling t.Add[i] pointer: %w", err)
+
}
+
}
+
+
}
+
+
}
+
}
+
// t.LexiconTypeID (string) (string)
+
case "$type":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.LexiconTypeID = string(sval)
+
}
+
// t.Delete ([]*tangled.LabelOp_Operand) (slice)
+
case "delete":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Delete: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Delete = make([]*LabelOp_Operand, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
t.Delete[i] = new(LabelOp_Operand)
+
if err := t.Delete[i].UnmarshalCBOR(cr); err != nil {
+
return xerrors.Errorf("unmarshaling t.Delete[i] pointer: %w", err)
+
}
+
}
+
+
}
+
+
}
+
}
+
// t.Subject (string) (string)
+
case "subject":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Subject = string(sval)
+
}
+
// t.PerformedAt (string) (string)
+
case "performedAt":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.PerformedAt = string(sval)
+
}
+
+
default:
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
}
+
}
+
+
return nil
+
}
+
func (t *LabelOp_Operand) MarshalCBOR(w io.Writer) error {
+
if t == nil {
+
_, err := w.Write(cbg.CborNull)
+
return err
+
}
+
+
cw := cbg.NewCborWriter(w)
+
+
if _, err := cw.Write([]byte{162}); err != nil {
+
return err
+
}
+
+
// t.Key (string) (string)
+
if len("key") > 1000000 {
+
return xerrors.Errorf("Value in field \"key\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("key")); err != nil {
+
return err
+
}
+
+
if len(t.Key) > 1000000 {
+
return xerrors.Errorf("Value in field t.Key was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Key)); err != nil {
+
return err
+
}
+
+
// t.Value (string) (string)
+
if len("value") > 1000000 {
+
return xerrors.Errorf("Value in field \"value\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("value"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("value")); err != nil {
+
return err
+
}
+
+
if len(t.Value) > 1000000 {
+
return xerrors.Errorf("Value in field t.Value was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Value))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Value)); err != nil {
+
return err
+
}
+
return nil
+
}
+
+
func (t *LabelOp_Operand) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = LabelOp_Operand{}
+
+
cr := cbg.NewCborReader(r)
+
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
defer func() {
+
if err == io.EOF {
+
err = io.ErrUnexpectedEOF
+
}
+
}()
+
+
if maj != cbg.MajMap {
+
return fmt.Errorf("cbor input should be of type map")
+
}
+
+
if extra > cbg.MaxLength {
+
return fmt.Errorf("LabelOp_Operand: map struct too large (%d)", extra)
+
}
+
+
n := extra
+
+
nameBuf := make([]byte, 5)
+
for i := uint64(0); i < n; i++ {
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+
if err != nil {
+
return err
+
}
+
+
if !ok {
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
continue
+
}
+
+
switch string(nameBuf[:nameLen]) {
+
// t.Key (string) (string)
+
case "key":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Key = string(sval)
+
}
+
// t.Value (string) (string)
+
case "value":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Value = string(sval)
+
}
+
+
default:
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
}
+
}
+
+
return nil
+
}
func (t *Pipeline) MarshalCBOR(w io.Writer) error {
if t == nil {
_, err := w.Write(cbg.CborNull)
···
fieldCount--
+
if t.Labels == nil {
+
fieldCount--
+
}
+
if t.Source == nil {
fieldCount--
···
return err
-
// t.Owner (string) (string)
-
if len("owner") > 1000000 {
-
return xerrors.Errorf("Value in field \"owner\" was too long")
-
}
+
// t.Labels ([]string) (slice)
+
if t.Labels != nil {
+
+
if len("labels") > 1000000 {
+
return xerrors.Errorf("Value in field \"labels\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("labels"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("labels")); err != nil {
+
return err
+
}
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string("owner")); err != nil {
-
return err
-
}
+
if len(t.Labels) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Labels was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Labels))); err != nil {
+
return err
+
}
+
for _, v := range t.Labels {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
-
if len(t.Owner) > 1000000 {
-
return xerrors.Errorf("Value in field t.Owner was too long")
-
}
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string(t.Owner)); err != nil {
-
return err
+
}
// t.Source (string) (string)
···
t.LexiconTypeID = string(sval)
-
// t.Owner (string) (string)
-
case "owner":
+
// t.Labels ([]string) (slice)
+
case "labels":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Labels: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Labels = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
-
{
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
-
if err != nil {
-
return err
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Labels[i] = string(sval)
+
}
+
-
-
t.Owner = string(sval)
// t.Source (string) (string)
case "source":
+43
api/tangled/labeldefinition.go
···
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+
package tangled
+
+
// schema: sh.tangled.label.definition
+
+
import (
+
"github.com/bluesky-social/indigo/lex/util"
+
)
+
+
const (
+
LabelDefinitionNSID = "sh.tangled.label.definition"
+
)
+
+
func init() {
+
util.RegisterType("sh.tangled.label.definition", &LabelDefinition{})
+
} //
+
// RECORDTYPE: LabelDefinition
+
type LabelDefinition struct {
+
LexiconTypeID string `json:"$type,const=sh.tangled.label.definition" cborgen:"$type,const=sh.tangled.label.definition"`
+
// color: The hex value for the background color for the label. Appviews may choose to respect this.
+
Color *string `json:"color,omitempty" cborgen:"color,omitempty"`
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+
// multiple: Whether this label can be repeated for a given entity, eg.: [reviewer:foo, reviewer:bar]
+
Multiple *bool `json:"multiple,omitempty" cborgen:"multiple,omitempty"`
+
// name: The display name of this label.
+
Name string `json:"name" cborgen:"name"`
+
// scope: The area of the repo this label may apply to, eg.: sh.tangled.repo.issue. Appviews may choose to respect this.
+
Scope string `json:"scope" cborgen:"scope"`
+
// valueType: The type definition of this label. Appviews may allow sorting for certain types.
+
ValueType *LabelDefinition_ValueType `json:"valueType" cborgen:"valueType"`
+
}
+
+
// RECORDTYPE: LabelDefinition_ValueType
+
type LabelDefinition_ValueType struct {
+
LexiconTypeID string `json:"$type,const=sh.tangled.label.definition" cborgen:"$type,const=sh.tangled.label.definition"`
+
// enum: Closed set of values that this label can take.
+
Enum []string `json:"enum,omitempty" cborgen:"enum,omitempty"`
+
// format: An optional constraint that can be applied on string concrete types.
+
Format string `json:"format" cborgen:"format"`
+
// type: The concrete type of this label's value.
+
Type string `json:"type" cborgen:"type"`
+
}
+34
api/tangled/labelop.go
···
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+
package tangled
+
+
// schema: sh.tangled.label.op
+
+
import (
+
"github.com/bluesky-social/indigo/lex/util"
+
)
+
+
const (
+
LabelOpNSID = "sh.tangled.label.op"
+
)
+
+
func init() {
+
util.RegisterType("sh.tangled.label.op", &LabelOp{})
+
} //
+
// RECORDTYPE: LabelOp
+
type LabelOp struct {
+
LexiconTypeID string `json:"$type,const=sh.tangled.label.op" cborgen:"$type,const=sh.tangled.label.op"`
+
Add []*LabelOp_Operand `json:"add" cborgen:"add"`
+
Delete []*LabelOp_Operand `json:"delete" cborgen:"delete"`
+
PerformedAt string `json:"performedAt" cborgen:"performedAt"`
+
// subject: The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op.
+
Subject string `json:"subject" cborgen:"subject"`
+
}
+
+
// LabelOp_Operand is a "operand" in the sh.tangled.label.op schema.
+
type LabelOp_Operand struct {
+
// key: ATURI to the label definition
+
Key string `json:"key" cborgen:"key"`
+
// value: Stringified value of the label. This is first unstringed by appviews and then interpreted as a concrete value.
+
Value string `json:"value" cborgen:"value"`
+
}
+3 -2
api/tangled/tangledrepo.go
···
Description *string `json:"description,omitempty" cborgen:"description,omitempty"`
// knot: knot where the repo was created
Knot string `json:"knot" cborgen:"knot"`
+
// labels: List of labels that this repo subscribes to
+
Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"`
// name: name of the repo
-
Name string `json:"name" cborgen:"name"`
-
Owner string `json:"owner" cborgen:"owner"`
+
Name string `json:"name" cborgen:"name"`
// source: source of the repo
Source *string `json:"source,omitempty" cborgen:"source,omitempty"`
// spindle: CI runner to send jobs to and receive results from
+65 -1
appview/db/db.go
···
primary key (did, rkey)
);
+
create table if not exists label_definitions (
+
-- identifiers
+
id integer primary key autoincrement,
+
did text not null,
+
rkey text not null,
+
at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.definition' || '/' || rkey) stored,
+
+
-- content
+
name text not null,
+
value_type text not null check (value_type in (
+
"null",
+
"boolean",
+
"integer",
+
"string"
+
)),
+
value_format text not null default "any",
+
value_enum text, -- comma separated list
+
scope text not null,
+
color text,
+
multiple integer not null default 0,
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
unique (did, rkey)
+
unique (at_uri)
+
);
+
+
-- ops are flattened, a record may contain several additions and deletions, but the table will include one row per add/del
+
create table if not exists label_ops (
+
-- identifiers
+
id integer primary key autoincrement,
+
did text not null,
+
rkey text not null,
+
at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.op' || '/' || rkey) stored,
+
+
-- content
+
subject text not null,
+
operation text not null check (operation in ("add", "del")),
+
operand_key text not null,
+
operand_value text not null,
+
-- we need two time values: performed is declared by the user, indexed is calculated by the av
+
performed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
indexed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
-- traditionally (did, rkey) pair should be unique, but not in this case
+
-- operand_key should reference a label definition
+
foreign key (operand_key) references label_definitions (at_uri) on delete cascade,
+
unique (did, rkey, subject, operand_key, operand_value)
+
);
+
+
create table if not exists repo_labels (
+
-- identifiers
+
id integer primary key autoincrement,
+
+
-- repo identifiers
+
repo_at text not null,
+
+
-- label to subscribe to
+
label_at text not null,
+
+
unique (repo_at, label_at),
+
foreign key (label_at) references label_definitions (at_uri)
+
);
+
create table if not exists migrations (
id integer primary key autoincrement,
name text unique
···
})
conn.ExecContext(ctx, "pragma foreign_keys = on;")
-
// run migrations
runMigration(conn, "add-spindle-to-repos", func(tx *sql.Tx) error {
tx.Exec(`
alter table repos add column spindle text;
+13 -1
appview/db/issues.go
···
// optionally, populate this when querying for reverse mappings
// like comment counts, parent repo etc.
Comments []IssueComment
+
Labels LabelState
Repo *Repo
}
···
// collect comments
issueAts := slices.Collect(maps.Keys(issueMap))
+
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
if err != nil {
return nil, fmt.Errorf("failed to query comments: %w", err)
}
-
for i := range comments {
issueAt := comments[i].IssueAt
if issue, ok := issueMap[issueAt]; ok {
issue.Comments = append(issue.Comments, comments[i])
+
}
+
}
+
+
// collect allLabels for each issue
+
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
+
if err != nil {
+
return nil, fmt.Errorf("failed to query labels: %w", err)
+
}
+
for issueAt, labels := range allLabels {
+
if issue, ok := issueMap[issueAt.String()]; ok {
+
issue.Labels = labels
}
}
+721
appview/db/label.go
···
+
package db
+
+
import (
+
"crypto/sha1"
+
"database/sql"
+
"encoding/hex"
+
"errors"
+
"fmt"
+
"log"
+
"maps"
+
"slices"
+
"strings"
+
"time"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.sh/tangled.sh/core/api/tangled"
+
)
+
+
type ConcreteType string
+
+
const (
+
ConcreteTypeNull ConcreteType = "null"
+
ConcreteTypeString ConcreteType = "string"
+
ConcreteTypeInt ConcreteType = "integer"
+
ConcreteTypeBool ConcreteType = "boolean"
+
)
+
+
type ValueTypeFormat string
+
+
const (
+
ValueTypeFormatAny ValueTypeFormat = "any"
+
ValueTypeFormatDid ValueTypeFormat = "did"
+
)
+
+
// ValueType represents an atproto lexicon type definition with constraints
+
type ValueType struct {
+
Type ConcreteType `json:"type"`
+
Format ValueTypeFormat `json:"format,omitempty"`
+
Enum []string `json:"enum,omitempty"`
+
}
+
+
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
+
return tangled.LabelDefinition_ValueType{
+
Type: string(vt.Type),
+
Format: string(vt.Format),
+
Enum: vt.Enum,
+
}
+
}
+
+
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
+
return ValueType{
+
Type: ConcreteType(record.Type),
+
Format: ValueTypeFormat(record.Format),
+
Enum: record.Enum,
+
}
+
}
+
+
func (vt ValueType) IsConcreteType() bool {
+
return vt.Type == ConcreteTypeNull ||
+
vt.Type == ConcreteTypeString ||
+
vt.Type == ConcreteTypeInt ||
+
vt.Type == ConcreteTypeBool
+
}
+
+
func (vt ValueType) IsNull() bool {
+
return vt.Type == ConcreteTypeNull
+
}
+
+
func (vt ValueType) IsString() bool {
+
return vt.Type == ConcreteTypeString
+
}
+
+
func (vt ValueType) IsInt() bool {
+
return vt.Type == ConcreteTypeInt
+
}
+
+
func (vt ValueType) IsBool() bool {
+
return vt.Type == ConcreteTypeBool
+
}
+
+
func (vt ValueType) IsEnumType() bool {
+
return len(vt.Enum) > 0
+
}
+
+
type LabelDefinition struct {
+
Id int64
+
Did string
+
Rkey string
+
+
Name string
+
ValueType ValueType
+
Scope syntax.NSID
+
Color *string
+
Multiple bool
+
Created time.Time
+
}
+
+
func (l *LabelDefinition) AtUri() syntax.ATURI {
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
+
}
+
+
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
+
vt := l.ValueType.AsRecord()
+
return tangled.LabelDefinition{
+
Name: l.Name,
+
Color: l.Color,
+
CreatedAt: l.Created.Format(time.RFC3339),
+
Multiple: &l.Multiple,
+
Scope: l.Scope.String(),
+
ValueType: &vt,
+
}
+
}
+
+
// random color for a given seed
+
func randomColor(seed string) string {
+
hash := sha1.Sum([]byte(seed))
+
hexStr := hex.EncodeToString(hash[:])
+
r := hexStr[0:2]
+
g := hexStr[2:4]
+
b := hexStr[4:6]
+
+
return fmt.Sprintf("#%s%s%s", r, g, b)
+
}
+
+
func (ld LabelDefinition) GetColor() string {
+
if ld.Color == nil {
+
seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
+
color := randomColor(seed)
+
return color
+
}
+
+
return *ld.Color
+
}
+
+
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) LabelDefinition {
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
+
if err != nil {
+
created = time.Now()
+
}
+
+
multiple := false
+
if record.Multiple != nil {
+
multiple = *record.Multiple
+
}
+
+
var vt ValueType
+
if record.ValueType != nil {
+
vt = ValueTypeFromRecord(*record.ValueType)
+
}
+
+
return LabelDefinition{
+
Did: did,
+
Rkey: rkey,
+
+
Name: record.Name,
+
ValueType: vt,
+
Scope: syntax.NSID(record.Scope),
+
Color: record.Color,
+
Multiple: multiple,
+
Created: created,
+
}
+
}
+
+
func DeleteLabelDefinition(e Execer, filters ...filter) error {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
+
_, err := e.Exec(query, args...)
+
return err
+
}
+
+
func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) {
+
result, err := e.Exec(
+
`insert into label_definitions (
+
did,
+
rkey,
+
name,
+
value_type,
+
value_format,
+
value_enum,
+
scope,
+
color,
+
multiple,
+
created
+
)
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+
on conflict(did, rkey) do update set
+
name = excluded.name,
+
scope = excluded.scope,
+
color = excluded.color,
+
multiple = excluded.multiple`,
+
l.Did,
+
l.Rkey,
+
l.Name,
+
l.ValueType.Type,
+
l.ValueType.Format,
+
strings.Join(l.ValueType.Enum, ","),
+
l.Scope.String(),
+
l.Color,
+
l.Multiple,
+
l.Created.Format(time.RFC3339),
+
time.Now().Format(time.RFC3339),
+
)
+
if err != nil {
+
return 0, err
+
}
+
+
id, err := result.LastInsertId()
+
if err != nil {
+
return 0, err
+
}
+
+
l.Id = id
+
+
return id, nil
+
}
+
+
func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) {
+
var labelDefinitions []LabelDefinition
+
var conditions []string
+
var args []any
+
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(
+
`
+
select
+
id,
+
did,
+
rkey,
+
name,
+
value_type,
+
value_format,
+
value_enum,
+
scope,
+
color,
+
multiple,
+
created
+
from label_definitions
+
%s
+
order by created
+
`,
+
whereClause,
+
)
+
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
for rows.Next() {
+
var labelDefinition LabelDefinition
+
var createdAt, enumVariants string
+
var color sql.Null[string]
+
var multiple int
+
+
if err := rows.Scan(
+
&labelDefinition.Id,
+
&labelDefinition.Did,
+
&labelDefinition.Rkey,
+
&labelDefinition.Name,
+
&labelDefinition.ValueType.Type,
+
&labelDefinition.ValueType.Format,
+
&enumVariants,
+
&labelDefinition.Scope,
+
&color,
+
&multiple,
+
&createdAt,
+
); err != nil {
+
return nil, err
+
}
+
+
labelDefinition.Created, err = time.Parse(time.RFC3339, createdAt)
+
if err != nil {
+
labelDefinition.Created = time.Now()
+
}
+
+
if color.Valid {
+
labelDefinition.Color = &color.V
+
}
+
+
if multiple != 0 {
+
labelDefinition.Multiple = true
+
}
+
+
if enumVariants != "" {
+
labelDefinition.ValueType.Enum = strings.Split(enumVariants, ",")
+
}
+
+
labelDefinitions = append(labelDefinitions, labelDefinition)
+
}
+
+
return labelDefinitions, nil
+
}
+
+
// helper to get exactly one label def
+
func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) {
+
labels, err := GetLabelDefinitions(e, filters...)
+
if err != nil {
+
return nil, err
+
}
+
+
if labels == nil {
+
return nil, sql.ErrNoRows
+
}
+
+
if len(labels) != 1 {
+
return nil, fmt.Errorf("too many rows returned")
+
}
+
+
return &labels[0], nil
+
}
+
+
type LabelOp struct {
+
Id int64
+
Did string
+
Rkey string
+
Subject syntax.ATURI
+
Operation LabelOperation
+
OperandKey string
+
OperandValue string
+
PerformedAt time.Time
+
IndexedAt time.Time
+
}
+
+
func (l LabelOp) SortAt() time.Time {
+
createdAt := l.PerformedAt
+
indexedAt := l.IndexedAt
+
+
// if we don't have an indexedat, fall back to now
+
if indexedAt.IsZero() {
+
indexedAt = time.Now()
+
}
+
+
// if createdat is invalid (before epoch), treat as null -> return zero time
+
if createdAt.Before(time.UnixMicro(0)) {
+
return time.Time{}
+
}
+
+
// if createdat is <= indexedat, use createdat
+
if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
+
return createdAt
+
}
+
+
// otherwise, createdat is in the future relative to indexedat -> use indexedat
+
return indexedAt
+
}
+
+
type LabelOperation string
+
+
const (
+
LabelOperationAdd LabelOperation = "add"
+
LabelOperationDel LabelOperation = "del"
+
)
+
+
// a record can create multiple label ops
+
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
+
performed, err := time.Parse(time.RFC3339, record.PerformedAt)
+
if err != nil {
+
performed = time.Now()
+
}
+
+
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
+
return LabelOp{
+
Did: did,
+
Rkey: rkey,
+
Subject: syntax.ATURI(record.Subject),
+
OperandKey: operand.Key,
+
OperandValue: operand.Value,
+
PerformedAt: performed,
+
}
+
}
+
+
var ops []LabelOp
+
for _, o := range record.Add {
+
if o != nil {
+
op := mkOp(o)
+
op.Operation = LabelOperationAdd
+
ops = append(ops, op)
+
}
+
}
+
for _, o := range record.Delete {
+
if o != nil {
+
op := mkOp(o)
+
op.Operation = LabelOperationDel
+
ops = append(ops, op)
+
}
+
}
+
+
return ops
+
}
+
+
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
+
if len(ops) == 0 {
+
return tangled.LabelOp{}
+
}
+
+
// use the first operation to establish common fields
+
first := ops[0]
+
record := tangled.LabelOp{
+
Subject: string(first.Subject),
+
PerformedAt: first.PerformedAt.Format(time.RFC3339),
+
}
+
+
var addOperands []*tangled.LabelOp_Operand
+
var deleteOperands []*tangled.LabelOp_Operand
+
+
for _, op := range ops {
+
operand := &tangled.LabelOp_Operand{
+
Key: op.OperandKey,
+
Value: op.OperandValue,
+
}
+
+
switch op.Operation {
+
case LabelOperationAdd:
+
addOperands = append(addOperands, operand)
+
case LabelOperationDel:
+
deleteOperands = append(deleteOperands, operand)
+
default:
+
return tangled.LabelOp{}
+
}
+
}
+
+
record.Add = addOperands
+
record.Delete = deleteOperands
+
+
return record
+
}
+
+
func AddLabelOp(e Execer, l *LabelOp) (int64, error) {
+
now := time.Now()
+
result, err := e.Exec(
+
`insert into label_ops (
+
did,
+
rkey,
+
subject,
+
operation,
+
operand_key,
+
operand_value,
+
performed,
+
indexed
+
)
+
values (?, ?, ?, ?, ?, ?, ?, ?)
+
on conflict(did, rkey, subject, operand_key, operand_value) do update set
+
operation = excluded.operation,
+
operand_value = excluded.operand_value,
+
performed = excluded.performed,
+
indexed = excluded.indexed`,
+
l.Did,
+
l.Rkey,
+
l.Subject.String(),
+
string(l.Operation),
+
l.OperandKey,
+
l.OperandValue,
+
l.PerformedAt.Format(time.RFC3339),
+
now.Format(time.RFC3339),
+
)
+
if err != nil {
+
return 0, err
+
}
+
+
id, err := result.LastInsertId()
+
if err != nil {
+
return 0, err
+
}
+
+
l.Id = id
+
l.IndexedAt = now
+
+
return id, nil
+
}
+
+
func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) {
+
var labelOps []LabelOp
+
var conditions []string
+
var args []any
+
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(
+
`
+
select
+
id,
+
did,
+
rkey,
+
subject,
+
operation,
+
operand_key,
+
operand_value,
+
performed,
+
indexed
+
from label_ops
+
%s
+
order by indexed
+
`,
+
whereClause,
+
)
+
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
for rows.Next() {
+
var labelOp LabelOp
+
var performedAt, indexedAt string
+
+
if err := rows.Scan(
+
&labelOp.Id,
+
&labelOp.Did,
+
&labelOp.Rkey,
+
&labelOp.Subject,
+
&labelOp.Operation,
+
&labelOp.OperandKey,
+
&labelOp.OperandValue,
+
&performedAt,
+
&indexedAt,
+
); err != nil {
+
return nil, err
+
}
+
+
labelOp.PerformedAt, err = time.Parse(time.RFC3339, performedAt)
+
if err != nil {
+
labelOp.PerformedAt = time.Now()
+
}
+
+
labelOp.IndexedAt, err = time.Parse(time.RFC3339, indexedAt)
+
if err != nil {
+
labelOp.IndexedAt = time.Now()
+
}
+
+
labelOps = append(labelOps, labelOp)
+
}
+
+
return labelOps, nil
+
}
+
+
// get labels for a given list of subject URIs
+
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) {
+
ops, err := GetLabelOps(e, filters...)
+
if err != nil {
+
return nil, err
+
}
+
+
// group ops by subject
+
opsBySubject := make(map[syntax.ATURI][]LabelOp)
+
for _, op := range ops {
+
subject := syntax.ATURI(op.Subject)
+
opsBySubject[subject] = append(opsBySubject[subject], op)
+
}
+
+
// get all unique labelats for creating the context
+
labelAtSet := make(map[string]bool)
+
for _, op := range ops {
+
labelAtSet[op.OperandKey] = true
+
}
+
labelAts := slices.Collect(maps.Keys(labelAtSet))
+
+
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
+
if err != nil {
+
return nil, err
+
}
+
+
// apply label ops for each subject and collect results
+
results := make(map[syntax.ATURI]LabelState)
+
for subject, subjectOps := range opsBySubject {
+
state := NewLabelState()
+
actx.ApplyLabelOps(state, subjectOps)
+
results[subject] = state
+
}
+
+
log.Println("results for get labels", "s", results)
+
+
return results, nil
+
}
+
+
type set = map[string]struct{}
+
+
type LabelState struct {
+
inner map[string]set
+
}
+
+
func NewLabelState() LabelState {
+
return LabelState{
+
inner: make(map[string]set),
+
}
+
}
+
+
func (s LabelState) Inner() map[string]set {
+
return s.inner
+
}
+
+
func (s LabelState) ContainsLabel(l string) bool {
+
if valset, exists := s.inner[l]; exists {
+
if valset != nil {
+
return true
+
}
+
}
+
+
return false
+
}
+
+
func (s *LabelState) GetValSet(l string) set {
+
return s.inner[l]
+
}
+
+
type LabelApplicationCtx struct {
+
defs map[string]*LabelDefinition // labelAt -> labelDef
+
}
+
+
var (
+
LabelNoOpError = errors.New("no-op")
+
)
+
+
func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) {
+
labels, err := GetLabelDefinitions(e, filters...)
+
if err != nil {
+
return nil, err
+
}
+
+
defs := make(map[string]*LabelDefinition)
+
for _, l := range labels {
+
defs[l.AtUri().String()] = &l
+
}
+
+
return &LabelApplicationCtx{defs}, nil
+
}
+
+
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
+
def := c.defs[op.OperandKey]
+
+
switch op.Operation {
+
case LabelOperationAdd:
+
// if valueset is empty, init it
+
if state.inner[op.OperandKey] == nil {
+
state.inner[op.OperandKey] = make(set)
+
}
+
+
// if valueset is populated & this val alr exists, this labelop is a noop
+
if valueSet, exists := state.inner[op.OperandKey]; exists {
+
if _, exists = valueSet[op.OperandValue]; exists {
+
return LabelNoOpError
+
}
+
}
+
+
if def.Multiple {
+
// append to set
+
state.inner[op.OperandKey][op.OperandValue] = struct{}{}
+
} else {
+
// reset to just this value
+
state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
+
}
+
+
case LabelOperationDel:
+
// if label DNE, then deletion is a no-op
+
if valueSet, exists := state.inner[op.OperandKey]; !exists {
+
return LabelNoOpError
+
} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
+
return LabelNoOpError
+
}
+
+
if def.Multiple {
+
// remove from set
+
delete(state.inner[op.OperandKey], op.OperandValue)
+
} else {
+
// reset the entire label
+
delete(state.inner, op.OperandKey)
+
}
+
+
// if the map becomes empty, then set it to nil, this is just the inverse of add
+
if len(state.inner[op.OperandKey]) == 0 {
+
state.inner[op.OperandKey] = nil
+
}
+
+
}
+
+
return nil
+
}
+
+
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
+
// sort label ops in sort order first
+
slices.SortFunc(ops, func(a, b LabelOp) int {
+
return a.SortAt().Compare(b.SortAt())
+
})
+
+
// apply ops in sequence
+
for _, o := range ops {
+
_ = c.ApplyLabelOp(state, o)
+
}
+
}
+
+
type Label struct {
+
def *LabelDefinition
+
val set
+
}
+138 -31
appview/db/repos.go
···
Created time.Time
Description string
Spindle string
+
Labels []string
// optionally, populate this when querying for reverse mappings
RepoStats *RepoStats
// optional
Source string
+
}
+
+
func (r *Repo) AsRecord() tangled.Repo {
+
var source, spindle, description *string
+
+
if r.Source != "" {
+
source = &r.Source
+
}
+
+
if r.Spindle != "" {
+
spindle = &r.Spindle
+
}
+
+
if r.Description != "" {
+
description = &r.Description
+
}
+
+
return tangled.Repo{
+
Knot: r.Knot,
+
Name: r.Name,
+
Description: description,
+
CreatedAt: r.Created.Format(time.RFC3339),
+
Source: source,
+
Spindle: spindle,
+
}
}
func (r Repo) RepoAt() syntax.ATURI {
···
i++
}
+
// Get labels for all repos
+
labelsQuery := fmt.Sprintf(
+
`select repo_at, label_at from repo_labels where repo_at in (%s)`,
+
inClause,
+
)
+
rows, err = e.Query(labelsQuery, args...)
+
if err != nil {
+
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
+
}
+
for rows.Next() {
+
var repoat, labelat string
+
if err := rows.Scan(&repoat, &labelat); err != nil {
+
log.Println("err", "err", err)
+
continue
+
}
+
if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
+
r.Labels = append(r.Labels, labelat)
+
}
+
}
+
if err = rows.Err(); err != nil {
+
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
+
}
+
languageQuery := fmt.Sprintf(
`
select
···
return repos, nil
}
+
// helper to get exactly one repo
+
func GetRepo(e Execer, filters ...filter) (*Repo, error) {
+
repos, err := GetRepos(e, 0, filters...)
+
if err != nil {
+
return nil, err
+
}
+
+
if repos == nil {
+
return nil, sql.ErrNoRows
+
}
+
+
if len(repos) != 1 {
+
return nil, fmt.Errorf("too many rows returned")
+
}
+
+
return &repos[0], nil
+
}
+
func CountRepos(e Execer, filters ...filter) (int64, error) {
var conditions []string
var args []any
···
return count, nil
}
-
func GetRepo(e Execer, did, name string) (*Repo, error) {
-
var repo Repo
-
var description, spindle sql.NullString
-
-
row := e.QueryRow(`
-
select did, name, knot, created, description, spindle, rkey
-
from repos
-
where did = ? and name = ?
-
`,
-
did,
-
name,
-
)
-
-
var createdAt string
-
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil {
-
return nil, err
-
}
-
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
-
repo.Created = createdAtTime
-
-
if description.Valid {
-
repo.Description = description.String
-
}
-
-
if spindle.Valid {
-
repo.Spindle = spindle.String
-
}
-
-
return &repo, nil
-
}
-
func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) {
var repo Repo
var nullableDescription sql.NullString
···
IssueCount IssueCount
PullCount PullCount
}
+
+
type RepoLabel struct {
+
Id int64
+
RepoAt syntax.ATURI
+
LabelAt syntax.ATURI
+
}
+
+
func SubscribeLabel(e Execer, rl *RepoLabel) error {
+
query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)`
+
+
_, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String())
+
return err
+
}
+
+
func UnsubscribeLabel(e Execer, filters ...filter) error {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(`delete from repo_labels %s`, whereClause)
+
_, err := e.Exec(query, args...)
+
return err
+
}
+
+
func GetRepoLabels(e Execer, filters ...filter) ([]RepoLabel, error) {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause)
+
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var labels []RepoLabel
+
for rows.Next() {
+
var label RepoLabel
+
+
err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt)
+
if err != nil {
+
return nil, err
+
}
+
+
labels = append(labels, label)
+
}
+
+
if err = rows.Err(); err != nil {
+
return nil, err
+
}
+
+
return labels, nil
+
}
+26
appview/issues/issues.go
···
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
}
+
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*db.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
···
OrderedReactionKinds: db.OrderedReactionKinds,
Reactions: reactionCountMap,
UserReacted: userReactions,
+
LabelDefs: defs,
})
}
···
return
}
+
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*db.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: rp.oauth.GetUser(r),
RepoInfo: f.RepoInfo(user),
Issues: issues,
+
LabelDefs: defs,
FilteringByOpen: isOpen,
Page: page,
})
+1 -1
appview/issues/router.go
···
r.With(middleware.Paginate).Get("/", i.RepoIssues)
r.Route("/{issue}", func(r chi.Router) {
-
r.Use(mw.ResolveIssue())
+
r.Use(mw.ResolveIssue)
r.Get("/", i.RepoSingleIssue)
// authenticated routes
+240
appview/labels/labels.go
···
+
package labels
+
+
import (
+
"context"
+
"database/sql"
+
"errors"
+
"fmt"
+
"log/slog"
+
"net/http"
+
"time"
+
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
lexutil "github.com/bluesky-social/indigo/lex/util"
+
"github.com/go-chi/chi/v5"
+
+
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview/config"
+
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/middleware"
+
"tangled.sh/tangled.sh/core/appview/oauth"
+
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/reporesolver"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
+
"tangled.sh/tangled.sh/core/eventconsumer"
+
"tangled.sh/tangled.sh/core/idresolver"
+
"tangled.sh/tangled.sh/core/log"
+
"tangled.sh/tangled.sh/core/rbac"
+
"tangled.sh/tangled.sh/core/tid"
+
)
+
+
type Labels struct {
+
repoResolver *reporesolver.RepoResolver
+
idResolver *idresolver.Resolver
+
oauth *oauth.OAuth
+
pages *pages.Pages
+
db *db.DB
+
logger *slog.Logger
+
}
+
+
func New(
+
oauth *oauth.OAuth,
+
repoResolver *reporesolver.RepoResolver,
+
pages *pages.Pages,
+
spindlestream *eventconsumer.Consumer,
+
idResolver *idresolver.Resolver,
+
db *db.DB,
+
config *config.Config,
+
enforcer *rbac.Enforcer,
+
) *Labels {
+
logger := log.New("labels")
+
+
return &Labels{
+
oauth: oauth,
+
repoResolver: repoResolver,
+
pages: pages,
+
idResolver: idResolver,
+
db: db,
+
logger: logger,
+
}
+
}
+
+
func (l *Labels) Router(mw *middleware.Middleware) http.Handler {
+
r := chi.NewRouter()
+
+
r.With(middleware.AuthMiddleware(l.oauth)).Put("/perform", l.PerformLabelOp)
+
+
return r
+
}
+
+
func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
+
user := l.oauth.GetUser(r)
+
+
if err := r.ParseForm(); err != nil {
+
l.logger.Error("failed to parse form data", "error", err)
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
+
return
+
}
+
+
did := user.Did
+
rkey := tid.TID()
+
performedAt := time.Now()
+
indexedAt := time.Now()
+
repoAt := r.Form.Get("repo")
+
subjectUri := r.Form.Get("subject")
+
keys := r.Form["operand-key"]
+
vals := r.Form["operand-val"]
+
+
var labelOps []db.LabelOp
+
for i := range len(keys) {
+
op := r.FormValue(fmt.Sprintf("op-%d", i))
+
if op == "" {
+
op = string(db.LabelOperationDel)
+
}
+
key := keys[i]
+
val := vals[i]
+
+
labelOps = append(labelOps, db.LabelOp{
+
Did: did,
+
Rkey: rkey,
+
Subject: syntax.ATURI(subjectUri),
+
Operation: db.LabelOperation(op),
+
OperandKey: key,
+
OperandValue: val,
+
PerformedAt: performedAt,
+
IndexedAt: indexedAt,
+
})
+
}
+
+
// TODO: validate the operations
+
+
// find all the labels that this repo subscribes to
+
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
+
if err != nil {
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
+
return
+
}
+
+
var labelAts []string
+
for _, rl := range repoLabels {
+
labelAts = append(labelAts, rl.LabelAt.String())
+
}
+
+
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
+
if err != nil {
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
+
return
+
}
+
+
// calculate the start state by applying already known labels
+
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
+
if err != nil {
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
+
return
+
}
+
+
labelState := db.NewLabelState()
+
actx.ApplyLabelOps(labelState, existingOps)
+
+
// next, apply all ops introduced in this request and filter out ones that are no-ops
+
validLabelOps := labelOps[:0]
+
for _, op := range labelOps {
+
if err = actx.ApplyLabelOp(labelState, op); err != db.LabelNoOpError {
+
validLabelOps = append(validLabelOps, op)
+
}
+
}
+
+
// nothing to do
+
if len(validLabelOps) == 0 {
+
l.pages.HxRefresh(w)
+
return
+
}
+
+
// create an atproto record of valid ops
+
record := db.LabelOpsAsRecord(validLabelOps)
+
+
client, err := l.oauth.AuthorizedClient(r)
+
if err != nil {
+
l.logger.Error("failed to create client", "error", err)
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
+
return
+
}
+
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.LabelOpNSID,
+
Repo: did,
+
Rkey: rkey,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &record,
+
},
+
})
+
if err != nil {
+
l.logger.Error("failed to write to PDS", "error", err)
+
http.Error(w, "failed to write to PDS", http.StatusInternalServerError)
+
return
+
}
+
atUri := resp.Uri
+
+
tx, err := l.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
l.logger.Error("failed to start tx", "error", err)
+
return
+
}
+
+
rollback := func() {
+
err1 := tx.Rollback()
+
err2 := rollbackRecord(context.Background(), atUri, client)
+
+
// ignore txn complete errors, this is okay
+
if errors.Is(err1, sql.ErrTxDone) {
+
err1 = nil
+
}
+
+
if errs := errors.Join(err1, err2); errs != nil {
+
return
+
}
+
}
+
defer rollback()
+
+
for _, o := range validLabelOps {
+
if _, err := db.AddLabelOp(l.db, &o); err != nil {
+
l.logger.Error("failed to add op", "err", err)
+
return
+
}
+
+
l.logger.Info("performed label op", "did", o.Did, "rkey", o.Rkey, "kind", o.Operation, "subjcet", o.Subject, "key", o.OperandKey)
+
}
+
+
err = tx.Commit()
+
if err != nil {
+
return
+
}
+
+
// clear aturi when everything is successful
+
atUri = ""
+
+
l.pages.HxRefresh(w)
+
}
+
+
// this is used to rollback changes made to the PDS
+
//
+
// it is a no-op if the provided ATURI is empty
+
func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error {
+
if aturi == "" {
+
return nil
+
}
+
+
parsed := syntax.ATURI(aturi)
+
+
collection := parsed.Collection().String()
+
repo := parsed.Authority().String()
+
rkey := parsed.RecordKey().String()
+
+
_, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{
+
Collection: collection,
+
Repo: repo,
+
Rkey: rkey,
+
})
+
return err
+
}
+38 -37
appview/middleware/middleware.go
···
return
}
-
repo, err := db.GetRepo(mw.db, id.DID.String(), repoName)
+
repo, err := db.GetRepo(
+
mw.db,
+
db.FilterEq("did", id.DID.String()),
+
db.FilterEq("name", repoName),
+
)
if err != nil {
-
// invalid did or handle
-
log.Println("failed to resolve repo")
+
log.Println("failed to resolve repo", "err", err)
mw.pages.ErrorKnot404(w)
return
}
···
}
// middleware that is tacked on top of /{user}/{repo}/issues/{issue}
-
func (mw Middleware) ResolveIssue() middlewareFunc {
-
return func(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
f, err := mw.repoResolver.Resolve(r)
-
if err != nil {
-
log.Println("failed to fully resolve repo", err)
-
mw.pages.ErrorKnot404(w)
-
return
-
}
+
func (mw Middleware) ResolveIssue(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
f, err := mw.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to fully resolve repo", err)
+
mw.pages.ErrorKnot404(w)
+
return
+
}
-
issueIdStr := chi.URLParam(r, "issue")
-
issueId, err := strconv.Atoi(issueIdStr)
-
if err != nil {
-
log.Println("failed to fully resolve issue ID", err)
-
mw.pages.ErrorKnot404(w)
-
return
-
}
+
issueIdStr := chi.URLParam(r, "issue")
+
issueId, err := strconv.Atoi(issueIdStr)
+
if err != nil {
+
log.Println("failed to fully resolve issue ID", err)
+
mw.pages.ErrorKnot404(w)
+
return
+
}
-
issues, err := db.GetIssues(
-
mw.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("issue_id", issueId),
-
)
-
if err != nil {
-
log.Println("failed to get issues", "err", err)
-
return
-
}
-
if len(issues) != 1 {
-
log.Println("got incorrect number of issues", "len(issuse)", len(issues))
-
return
-
}
-
issue := issues[0]
+
issues, err := db.GetIssues(
+
mw.db,
+
db.FilterEq("repo_at", f.RepoAt()),
+
db.FilterEq("issue_id", issueId),
+
)
+
if err != nil {
+
log.Println("failed to get issues", "err", err)
+
return
+
}
+
if len(issues) != 1 {
+
log.Println("got incorrect number of issues", "len(issuse)", len(issues))
+
return
+
}
+
issue := issues[0]
-
ctx := context.WithValue(r.Context(), "issue", &issue)
-
next.ServeHTTP(w, r.WithContext(ctx))
-
})
-
}
+
ctx := context.WithValue(r.Context(), "issue", &issue)
+
next.ServeHTTP(w, r.WithContext(ctx))
+
})
}
// this should serve the go-import meta tag even if the path is technically
+3
appview/pages/funcmap.go
···
"split": func(s string) []string {
return strings.Split(s, "\n")
},
+
"join": func(elems []string, sep string) string {
+
return strings.Join(elems, sep)
+
},
"contains": func(s string, target string) bool {
return strings.Contains(s, target)
},
+8 -6
appview/pages/pages.go
···
type RepoGeneralSettingsParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
+
Labels []db.LabelDefinition
Active string
Tabs []map[string]any
Tab string
···
RepoInfo repoinfo.RepoInfo
Active string
Issues []db.Issue
+
LabelDefs map[string]*db.LabelDefinition
Page pagination.Page
FilteringByOpen bool
}
···
}
type RepoSingleIssueParams struct {
-
LoggedInUser *oauth.User
-
RepoInfo repoinfo.RepoInfo
-
Active string
-
Issue *db.Issue
-
CommentList []db.CommentListItem
-
IssueOwnerHandle string
+
LoggedInUser *oauth.User
+
RepoInfo repoinfo.RepoInfo
+
Active string
+
Issue *db.Issue
+
CommentList []db.CommentListItem
+
LabelDefs map[string]*db.LabelDefinition
OrderedReactionKinds []db.ReactionKind
Reactions map[db.ReactionKind]int
+2 -2
appview/pages/repoinfo/repoinfo.go
···
}
func (r RepoInfo) OwnerWithoutAt() string {
-
if strings.HasPrefix(r.OwnerWithAt(), "@") {
-
return strings.TrimPrefix(r.OwnerWithAt(), "@")
+
if after, ok := strings.CutPrefix(r.OwnerWithAt(), "@"); ok {
+
return after
} else {
return userutil.FlattenDid(r.OwnerDid)
}
+8
appview/pages/templates/labels/fragments/label.html
···
+
{{ define "labels/fragments/label" }}
+
{{ $d := .def }}
+
{{ $v := .val }}
+
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 text-sm">
+
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
+
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ $v }}{{ end }}
+
</span>
+
{{ end }}
+6
appview/pages/templates/labels/fragments/labelDef.html
···
+
{{ define "labels/fragments/labelDef" }}
+
<span class="flex items-center gap-2 font-normal normal-case">
+
{{ template "repo/fragments/colorBall" (dict "color" .GetColor) }}
+
{{ .Name }}
+
</span>
+
{{ end }}
+119
appview/pages/templates/repo/fragments/addLabelModal.html
···
+
{{ define "repo/fragments/addLabelModal" }}
+
{{ $root := .root }}
+
{{ $subject := .subject }}
+
{{ $state := .state }}
+
{{ with $root }}
+
<form
+
hx-put="/{{ .RepoInfo.FullName }}/labels/perform"
+
hx-on::after-request="if(event.detail.successful) this.reset()"
+
hx-indicator="#spinner"
+
hx-swap="none"
+
class="flex flex-col gap-4"
+
>
+
<p class="text-gray-500 dark:text-gray-400">Add, remove or update labels.</p>
+
+
<input class="hidden" name="repo" value="{{ .RepoInfo.RepoAt.String }}">
+
<input class="hidden" name="subject" value="{{ $subject }}">
+
+
<div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
+
{{ $id := 0 }}
+
{{ range $k, $valset := $state.Inner }}
+
{{ $d := index $root.LabelDefs $k }}
+
{{ range $v, $s := $valset }}
+
<div class="grid grid-cols-2 cursor-pointer rounded">
+
<label class="w-full flex items-center gap-2">
+
<input type="checkbox" name="op-{{$id}}" value="add" checked>
+
{{ template "labels/fragments/labelDef" $d }}
+
</label>
+
{{ template "valueTypeInput" (dict "valueType" $d.ValueType "value" $v "key" $k) }}
+
<input type="hidden" name="operand-key" value="{{ $k }}">
+
{{ $id = add $id 1 }}
+
</div>
+
{{ end }}
+
{{ end }}
+
+
{{ range $k, $d := $root.LabelDefs }}
+
{{ if not ($state.ContainsLabel $k) }}
+
<div class="grid grid-cols-2 cursor-pointer rounded">
+
<label class="w-full flex items-center gap-2">
+
<input type="checkbox" name="op-{{$id}}" value="add">
+
{{ template "labels/fragments/labelDef" $d }}
+
</label>
+
{{ template "valueTypeInput" (dict "valueType" $d.ValueType "value" "" "key" $k) }}
+
<input type="hidden" name="operand-key" value="{{ $k }}">
+
{{ $id = add $id 1 }}
+
</div>
+
{{ end }}
+
{{ end }}
+
</div>
+
+
<div class="flex gap-2 pt-2">
+
<button
+
type="button"
+
popovertarget="add-label-modal"
+
popovertargetaction="hide"
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+
>
+
{{ i "x" "size-4" }} cancel
+
</button>
+
<button type="submit" class="btn w-1/2 flex items-center">
+
<span class="inline-flex gap-2 items-center">{{ i "check" "size-4" }} save</span>
+
<span id="spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</div>
+
<div id="add-label-error" class="text-red-500 dark:text-red-400"></div>
+
</form>
+
{{ end }}
+
{{ end }}
+
+
{{ define "valueTypeInput" }}
+
{{ $valueType := .valueType }}
+
{{ $value := .value }}
+
{{ $key := .key }}
+
+
{{ if $valueType.IsEnumType }}
+
{{ template "enumTypeInput" $ }}
+
{{ else if $valueType.IsBool }}
+
{{ template "boolTypeInput" $ }}
+
{{ else if $valueType.IsInt }}
+
{{ template "intTypeInput" $ }}
+
{{ else if $valueType.IsString }}
+
{{ template "stringTypeInput" $ }}
+
{{ else if $valueType.IsNull }}
+
{{ template "nullTypeInput" $ }}
+
{{ end }}
+
{{ end }}
+
+
{{ define "enumTypeInput" }}
+
{{ $valueType := .valueType }}
+
{{ $value := .value }}
+
<select name="operand-val" class="w-full p-1 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
+
{{ range $valueType.Enum }}
+
<option value="{{.}}" {{ if eq $value . }} selected {{ end }}>{{.}}</option>
+
{{ end }}
+
</select>
+
{{ end }}
+
+
{{ define "boolTypeInput" }}
+
{{ $value := .value }}
+
<select name="operand-val" class="w-full p-1 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
+
<option value="true" {{ if $value }} selected {{ end }}>true</option>
+
<option value="false" {{ if not $value }} selected {{ end }}>false</option>
+
</select>
+
{{ end }}
+
+
{{ define "intTypeInput" }}
+
{{ $value := .value }}
+
<input class="p-1 w-full" type="number" name="operand-val" value="{{$value}}" max="100">
+
{{ end }}
+
+
{{ define "stringTypeInput" }}
+
{{ $value := .value }}
+
<input class="p-1 w-full" type="text" name="operand-val" value="{{$value}}">
+
{{ end }}
+
+
{{ define "nullTypeInput" }}
+
<input class="p-1" type="hidden" name="operand-val" value="null">
+
{{ end }}
+6
appview/pages/templates/repo/fragments/colorBall.html
···
+
{{ define "repo/fragments/colorBall" }}
+
<div
+
class="size-2 rounded-full {{ .classes }}"
+
style="background: radial-gradient(circle at 35% 35%, color-mix(in srgb, {{ .color }} 70%, white), {{ .color }} 30%, color-mix(in srgb, {{ .color }} 85%, black));"
+
></div>
+
{{ end }}
-6
appview/pages/templates/repo/fragments/languageBall.html
···
-
{{ define "repo/fragments/languageBall" }}
-
<div
-
class="size-2 rounded-full"
-
style="background: radial-gradient(circle at 35% 35%, color-mix(in srgb, {{ langColor . }} 70%, white), {{ langColor . }} 30%, color-mix(in srgb, {{ langColor . }} 85%, black));"
-
></div>
-
{{ end }}
+1 -1
appview/pages/templates/repo/index.html
···
<div
class="flex flex-grow items-center gap-2 text-xs align-items-center justify-center"
>
-
{{ template "repo/fragments/languageBall" $value.Name }}
+
{{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }}
<div>{{ or $value.Name "Other" }}
<span class="text-gray-500 dark:text-gray-400">
{{ if lt $value.Percentage 0.05 }}
+28 -2
appview/pages/templates/repo/issues/issue.html
···
{{ if .Issue.Body }}
<article id="body" class="mt-4 prose dark:prose-invert">{{ .Issue.Body | markdown }}</article>
{{ end }}
-
{{ template "issueReactions" . }}
+
<div class="flex flex-wrap gap-2 items-stretch">
+
{{ template "issueReactions" . }}
+
{{ template "issueLabels" . }}
+
</div>
</section>
{{ end }}
···
{{ end }}
{{ define "issueReactions" }}
-
<div class="flex items-center gap-2 mt-2">
+
<div class="flex items-center gap-2">
{{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }}
{{ range $kind := .OrderedReactionKinds }}
{{
···
"ThreadAt" $.Issue.AtUri)
}}
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "issueLabels" }}
+
{{ range $k, $valset := $.Issue.Labels.Inner }}
+
{{ $d := index $.LabelDefs $k }}
+
{{ range $v, $s := $valset }}
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v) }}
+
{{ end }}
+
{{ end }}
+
+
<button
+
class="btn text-gray-500 dark:text-gray-400"
+
popovertarget="add-label-modal"
+
{{ if not (or .RepoInfo.Roles.IsOwner .RepoInfo.Roles.IsCollaborator) }}disabled{{ end }}
+
popovertargetaction="toggle">
+
{{ i "plus" "size-4" }}
+
</button>
+
<div
+
id="add-label-modal"
+
popover
+
class="bg-white w-full sm:w-96 dark:bg-gray-800 p-6 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
+
{{ template "repo/fragments/addLabelModal" (dict "root" $ "subject" $.Issue.AtUri.String "state" $.Issue.Labels) }}
</div>
{{ end }}
+13 -3
appview/pages/templates/repo/issues/issues.html
···
<span class="text-gray-500">#{{ .IssueId }}</span>
</a>
</div>
-
<p class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
+
<div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
{{ $icon := "ban" }}
{{ $state := "closed" }}
···
<span class="before:content-['ยท']">
{{ $s := "s" }}
{{ if eq (len .Comments) 1 }}
-
{{ $s = "" }}
+
{{ $s = "" }}
{{ end }}
<a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a>
</span>
-
</p>
+
+
{{ if .Labels.Inner }}
+
<span class="before:content-['ยท']"></span>
+
{{ range $k, $valset := .Labels.Inner }}
+
{{ $d := index $.LabelDefs $k }}
+
{{ range $v, $s := $valset }}
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v) }}
+
{{ end }}
+
{{ end }}
+
{{ end }}
+
</div>
</div>
{{ end }}
</div>
+104
appview/pages/templates/repo/settings/fragments/addLabelDefModal.html
···
+
{{ define "repo/settings/fragments/addLabelDefModal" }}
+
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/label"
+
hx-indicator="#spinner"
+
hx-swap="none"
+
class="flex flex-col gap-4"
+
>
+
<p class="text-gray-500 dark:text-gray-400">Labels can have a name and a value. Set the value type to "none" to create a simple label.</p>
+
+
<div class="w-full">
+
<label for="name">Name</label>
+
<input class="w-full" type="text" id="label-name" name="name" required placeholder="improvement"/>
+
</div>
+
+
<!-- Value Type -->
+
<div class="w-full">
+
<label for="valueType">Value Type</label>
+
<select id="value-type" name="valueType" class="w-full p-3 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
+
<option value="string" selected>String</option>
+
<option value="integer">Integer</option>
+
<option value="boolean">Boolean</option>
+
<option value="null">None</option>
+
</select>
+
<details id="constrain-values" class="group">
+
<summary class="list-none cursor-pointer flex items-center gap-2 py-2">
+
<span class="group-open:hidden inline text-gray-500 dark:text-gray-400">{{ i "square-plus" "w-4 h-4" }}</span>
+
<span class="hidden group-open:inline text-gray-500 dark:text-gray-400">{{ i "square-minus" "w-4 h-4" }}</span>
+
<span>Constrain values</span>
+
</summary>
+
<input type="text" id="enumValues" name="enumValues" placeholder="value1, value2, value3" class="w-full"/>
+
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Enter comma-separated list of permitted values.</p>
+
</details>
+
</div>
+
+
<!-- Scope -->
+
<div class="w-full">
+
<label for="scope">Scope</label>
+
<select id="scope" name="scope" class="w-full p-3 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
+
<option value="sh.tangled.repo.issue">Issues</option>
+
<option value="sh.tangled.repo.pull">Pull Requests</option>
+
</select>
+
</div>
+
+
<!-- Color -->
+
<div class="w-full">
+
<label for="color">Color</label>
+
<div class="grid grid-cols-4 grid-rows-2 place-items-center">
+
{{ $colors := list "#ef4444" "#3b82f6" "#10b981" "#f59e0b" "#8b5cf6" "#ec4899" "#06b6d4" "#64748b" }}
+
{{ range $i, $color := $colors }}
+
<label class="relative">
+
<input type="radio" name="color" value="{{ $color }}" class="sr-only peer" {{ if eq $i 0 }} checked {{ end }}>
+
{{ template "repo/fragments/colorBall" (dict "color" $color "classes" "size-4 peer-checked:size-8 transition-all") }}
+
</label>
+
{{ end }}
+
</div>
+
</div>
+
+
<!-- Multiple -->
+
<div class="w-full flex flex-wrap gap-2">
+
<input type="checkbox" id="multiple" name="multiple" value="true" />
+
<span>
+
Allow multiple values
+
</span>
+
</div>
+
+
<div class="flex gap-2 pt-2">
+
<button
+
type="button"
+
popovertarget="add-labeldef-modal"
+
popovertargetaction="hide"
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+
>
+
{{ i "x" "size-4" }} cancel
+
</button>
+
<button type="submit" class="btn w-1/2 flex items-center">
+
<span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span>
+
<span id="spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</div>
+
<div id="add-label-error" class="text-red-500 dark:text-red-400"></div>
+
</form>
+
+
<script>
+
document.getElementById('value-type').addEventListener('change', function() {
+
const constrainValues = document.getElementById('constrain-values');
+
const selectedValue = this.value;
+
+
if (selectedValue === 'string' || selectedValue === 'integer') {
+
constrainValues.classList.remove('hidden');
+
} else {
+
constrainValues.classList.add('hidden');
+
constrainValues.removeAttribute('open');
+
document.getElementById('enumValues').value = '';
+
}
+
});
+
+
function toggleDarkMode() {
+
document.documentElement.classList.toggle('dark');
+
}
+
</script>
+
{{ end }}
+
+28
appview/pages/templates/repo/settings/fragments/labelListing.html
···
+
{{ define "repo/settings/fragments/labelListing" }}
+
{{ $root := index . 0 }}
+
{{ $label := index . 1 }}
+
<div id="label-{{$label.Id}}" class="flex items-center justify-between p-2 pl-4">
+
<div class="flex flex-col gap-1 text-sm min-w-0 max-w-[80%]">
+
{{ template "labels/fragments/labelDef" $label }}
+
<div class="flex flex-wrap text items-center gap-1 text-gray-500 dark:text-gray-400">
+
{{ $label.ValueType.Type }}
+
{{ if $label.ValueType.IsEnumType }}
+
<span class="before:content-['ยท'] before:select-none"></span>
+
{{ join $label.ValueType.Enum ", " }}
+
{{ end }}
+
</div>
+
</div>
+
<button
+
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
+
title="Delete label"
+
hx-delete="/{{ $root.RepoInfo.FullName }}/settings/label"
+
hx-swap="none"
+
hx-vals='{"label-id": "{{ $label.Id }}"}'
+
hx-confirm="Are you sure you want to delete the label `{{ $label.Name }}`?"
+
>
+
{{ i "trash-2" "w-5 h-5" }}
+
<span class="hidden md:inline">delete</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
{{ end }}
+42
appview/pages/templates/repo/settings/general.html
···
</div>
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
{{ template "branchSettings" . }}
+
{{ template "labelSettings" . }}
{{ template "deleteRepo" . }}
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
</div>
···
</div>
{{ end }}
+
{{ define "labelSettings" }}
+
<div class="flex flex-col gap-2">
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase font-bold">Labels</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
Manage your issues and pulls by creating labels to categorize them.
+
Only repository owners may configure labels.
+
</p>
+
</div>
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
+
<button
+
class="btn flex items-center gap-2"
+
popovertarget="add-labeldef-modal"
+
{{ if not (or .RepoInfo.Roles.IsOwner .RepoInfo.Roles.IsCollaborator) }}disabled{{ end }}
+
popovertargetaction="toggle">
+
{{ i "plus" "size-4" }}
+
add label
+
</button>
+
<div
+
id="add-labeldef-modal"
+
popover
+
class="bg-white w-full sm:w-96 dark:bg-gray-800 p-6 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
+
{{ template "repo/settings/fragments/addLabelDefModal" . }}
+
</div>
+
</div>
+
</div>
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full">
+
{{ range .Labels }}
+
{{ template "repo/settings/fragments/labelListing" (list $ .) }}
+
{{ else }}
+
<div class="flex items-center justify-center p-2 text-gray-500">
+
no labels added yet
+
</div>
+
{{ end }}
+
</div>
+
<div id="label-operation" class="error"></div>
+
</div>
+
{{ end }}
+
{{ define "deleteRepo" }}
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
···
</div>
{{ end }}
{{ end }}
+
+1 -1
appview/pages/templates/repo/settings/pipelines.html
···
hx-swap="none"
class="flex flex-col gap-2"
>
-
<p class="uppercase p-0">ADD SECRET</p>
+
<p class="uppercase p-0 font-bold">ADD SECRET</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Secrets are available as environment variables in the workflow.</p>
<input
type="text"
+1 -1
appview/pages/templates/user/fragments/repoCard.html
···
<div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto">
{{ with .Language }}
<div class="flex gap-2 items-center text-sm">
-
{{ template "repo/fragments/languageBall" . }}
+
{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}
<span>{{ . }}</span>
</div>
{{ end }}
+1 -1
appview/pages/templates/user/overview.html
···
{{ with .Repo.RepoStats }}
{{ with .Language }}
<div class="flex gap-2 items-center text-xs font-mono text-gray-400 ">
-
{{ template "repo/fragments/languageBall" . }}
+
{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}
<span>{{ . }}</span>
</div>
{{end }}
+6 -2
appview/pulls/pulls.go
···
forkOwnerDid := repoString[0]
forkName := repoString[1]
// fork repo
-
repo, err := db.GetRepo(s.db, forkOwnerDid, forkName)
+
repo, err := db.GetRepo(
+
s.db,
+
db.FilterEq("did", forkOwnerDid),
+
db.FilterEq("name", forkName),
+
)
if err != nil {
-
log.Println("failed to get repo", user.Did, forkVal)
+
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
return
+302 -46
appview/repo/repo.go
···
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
"tangled.sh/tangled.sh/core/appview/reporesolver"
+
"tangled.sh/tangled.sh/core/appview/validator"
xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
···
notifier notify.Notifier
logger *slog.Logger
serviceAuth *serviceauth.ServiceAuth
+
validator *validator.Validator
}
func New(
···
notifier notify.Notifier,
enforcer *rbac.Enforcer,
logger *slog.Logger,
+
validator *validator.Validator,
) *Repo {
return &Repo{oauth: oauth,
repoResolver: repoResolver,
···
notifier: notifier,
enforcer: enforcer,
logger: logger,
+
validator: validator,
}
}
···
return
}
+
newRepo := f.Repo
+
newRepo.Description = newDescription
+
record := newRepo.AsRecord()
+
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
//
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
-
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
if err != nil {
// failed to get record
rp.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
···
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
-
Repo: user.Did,
-
Rkey: rkey,
+
Repo: newRepo.Did,
+
Rkey: newRepo.Rkey,
SwapRecord: ex.Cid,
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.Repo{
-
Knot: f.Knot,
-
Name: f.Name,
-
Owner: user.Did,
-
CreatedAt: f.Created.Format(time.RFC3339),
-
Description: &newDescription,
-
Spindle: &f.Spindle,
-
},
+
Val: &record,
},
})
···
return
}
-
repoAt := f.RepoAt()
-
rkey := repoAt.RecordKey().String()
-
if rkey == "" {
-
fail("Failed to resolve repo. Try again later", err)
-
return
-
}
-
newSpindle := r.FormValue("spindle")
removingSpindle := newSpindle == "[[none]]" // see pages/templates/repo/settings/pipelines.html for more info on why we use this value
client, err := rp.oauth.AuthorizedClient(r)
···
}
}
+
newRepo := f.Repo
+
newRepo.Spindle = newSpindle
+
record := newRepo.AsRecord()
+
spindlePtr := &newSpindle
if removingSpindle {
spindlePtr = nil
+
newRepo.Spindle = ""
}
// optimistic update
-
err = db.UpdateSpindle(rp.db, string(repoAt), spindlePtr)
+
err = db.UpdateSpindle(rp.db, newRepo.RepoAt().String(), spindlePtr)
if err != nil {
fail("Failed to update spindle. Try again later.", err)
return
}
-
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
if err != nil {
fail("Failed to update spindle, no record found on PDS.", err)
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
-
Repo: user.Did,
-
Rkey: rkey,
+
Repo: newRepo.Did,
+
Rkey: newRepo.Rkey,
SwapRecord: ex.Cid,
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.Repo{
-
Knot: f.Knot,
-
Name: f.Name,
-
Owner: user.Did,
-
CreatedAt: f.Created.Format(time.RFC3339),
-
Description: &f.Description,
-
Spindle: spindlePtr,
-
},
+
Val: &record,
},
})
···
rp.pages.HxRefresh(w)
}
+
func (rp *Repo) AddLabel(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "AddLabel")
+
l = l.With("did", user.Did)
+
l = l.With("handle", user.Handle)
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
errorId := "add-label-error"
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
rp.pages.Notice(w, errorId, msg)
+
}
+
+
// get form values for label definition
+
name := r.FormValue("name")
+
concreteType := r.FormValue("valueType")
+
enumValues := r.FormValue("enumValues")
+
scope := r.FormValue("scope")
+
color := r.FormValue("color")
+
multiple := r.FormValue("multiple") == "true"
+
+
var variants []string
+
for part := range strings.SplitSeq(enumValues, ",") {
+
if part = strings.TrimSpace(part); part != "" {
+
variants = append(variants, part)
+
}
+
}
+
+
valueType := db.ValueType{
+
Type: db.ConcreteType(concreteType),
+
Format: db.ValueTypeFormatAny,
+
Enum: variants,
+
}
+
+
label := db.LabelDefinition{
+
Did: user.Did,
+
Rkey: tid.TID(),
+
Name: name,
+
ValueType: valueType,
+
Scope: syntax.NSID(scope),
+
Color: &color,
+
Multiple: multiple,
+
Created: time.Now(),
+
}
+
if err := rp.validator.ValidateLabelDefinition(&label); err != nil {
+
fail(err.Error(), err)
+
return
+
}
+
+
// announce this relation into the firehose, store into owners' pds
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
fail(err.Error(), err)
+
return
+
}
+
+
// emit a labelRecord
+
labelRecord := label.AsRecord()
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.LabelDefinitionNSID,
+
Repo: label.Did,
+
Rkey: label.Rkey,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &labelRecord,
+
},
+
})
+
// invalid record
+
if err != nil {
+
fail("Failed to write record to PDS.", err)
+
return
+
}
+
+
aturi := resp.Uri
+
l = l.With("at-uri", aturi)
+
l.Info("wrote label record to PDS")
+
+
// update the repo to subscribe to this label
+
newRepo := f.Repo
+
newRepo.Labels = append(newRepo.Labels, aturi)
+
repoRecord := newRepo.AsRecord()
+
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
+
if err != nil {
+
fail("Failed to update labels, no record found on PDS.", err)
+
return
+
}
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: newRepo.Did,
+
Rkey: newRepo.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &repoRecord,
+
},
+
})
+
+
tx, err := rp.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
fail("Failed to add label.", err)
+
return
+
}
+
+
rollback := func() {
+
err1 := tx.Rollback()
+
err2 := rollbackRecord(context.Background(), aturi, client)
+
+
// ignore txn complete errors, this is okay
+
if errors.Is(err1, sql.ErrTxDone) {
+
err1 = nil
+
}
+
+
if errs := errors.Join(err1, err2); errs != nil {
+
l.Error("failed to rollback changes", "errs", errs)
+
return
+
}
+
}
+
defer rollback()
+
+
_, err = db.AddLabelDefinition(tx, &label)
+
if err != nil {
+
fail("Failed to add label.", err)
+
return
+
}
+
+
err = db.SubscribeLabel(tx, &db.RepoLabel{
+
RepoAt: f.RepoAt(),
+
LabelAt: label.AtUri(),
+
})
+
+
err = tx.Commit()
+
if err != nil {
+
fail("Failed to add label.", err)
+
return
+
}
+
+
// clear aturi when everything is successful
+
aturi = ""
+
+
rp.pages.HxRefresh(w)
+
}
+
+
func (rp *Repo) DeleteLabel(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "DeleteLabel")
+
l = l.With("did", user.Did)
+
l = l.With("handle", user.Handle)
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
errorId := "label-operation"
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
rp.pages.Notice(w, errorId, msg)
+
}
+
+
// get form values
+
labelId := r.FormValue("label-id")
+
+
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
+
if err != nil {
+
fail("Failed to find label definition.", err)
+
return
+
}
+
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
fail(err.Error(), err)
+
return
+
}
+
+
// delete label record from PDS
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.LabelDefinitionNSID,
+
Repo: label.Did,
+
Rkey: label.Rkey,
+
})
+
if err != nil {
+
fail("Failed to delete label record from PDS.", err)
+
return
+
}
+
+
// update repo record to remove the label reference
+
newRepo := f.Repo
+
var updated []string
+
removedAt := label.AtUri().String()
+
for _, l := range newRepo.Labels {
+
if l != removedAt {
+
updated = append(updated, l)
+
}
+
}
+
newRepo.Labels = updated
+
repoRecord := newRepo.AsRecord()
+
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
+
if err != nil {
+
fail("Failed to update labels, no record found on PDS.", err)
+
return
+
}
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: newRepo.Did,
+
Rkey: newRepo.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &repoRecord,
+
},
+
})
+
if err != nil {
+
fail("Failed to update repo record.", err)
+
return
+
}
+
+
// transaction for DB changes
+
tx, err := rp.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
fail("Failed to delete label.", err)
+
return
+
}
+
defer tx.Rollback()
+
+
err = db.UnsubscribeLabel(
+
tx,
+
db.FilterEq("repo_at", f.RepoAt()),
+
db.FilterEq("label_at", removedAt),
+
)
+
if err != nil {
+
fail("Failed to unsubscribe label.", err)
+
return
+
}
+
+
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
+
if err != nil {
+
fail("Failed to delete label definition.", err)
+
return
+
}
+
+
err = tx.Commit()
+
if err != nil {
+
fail("Failed to delete label.", err)
+
return
+
}
+
+
// everything succeeded
+
rp.pages.HxRefresh(w)
+
}
+
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
user := rp.oauth.GetUser(r)
l := rp.logger.With("handler", "AddCollaborator")
···
return
-
err = db.AddCollaborator(rp.db, db.Collaborator{
+
err = db.AddCollaborator(tx, db.Collaborator{
Did: syntax.DID(currentUser.Did),
Rkey: rkey,
SubjectDid: collaboratorIdent.DID,
···
return
+
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
rp.pages.Error503(w)
+
return
+
}
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Branches: result.Branches,
+
Labels: labels,
Tabs: settingsTabs,
Tab: "general",
})
···
forkName := f.Name
// this check is *only* to see if the forked repo name already exists
// in the user's account.
-
existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name)
+
existingRepo, err := db.GetRepo(
+
rp.db,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("name", f.Name),
+
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
} else {
-
log.Println("error fetching existing repo from db", err)
+
log.Println("error fetching existing repo from db", "err", err)
rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
···
// create an atproto record for this fork
rkey := tid.TID()
repo := &db.Repo{
-
Did: user.Did,
-
Name: forkName,
-
Knot: targetKnot,
-
Rkey: rkey,
-
Source: sourceAt,
+
Did: user.Did,
+
Name: forkName,
+
Knot: targetKnot,
+
Rkey: rkey,
+
Source: sourceAt,
+
Description: existingRepo.Description,
+
Created: time.Now(),
+
record := repo.AsRecord()
xrpcClient, err := rp.oauth.AuthorizedClient(r)
if err != nil {
···
return
-
createdAt := time.Now().Format(time.RFC3339)
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.Repo{
-
Knot: repo.Knot,
-
Name: repo.Name,
-
CreatedAt: createdAt,
-
Owner: user.Did,
-
Source: &sourceAt,
-
}},
+
Val: &record,
+
},
})
if err != nil {
l.Error("failed to write to PDS", "err", err)
+2
appview/repo/router.go
···
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
r.Get("/", rp.RepoSettings)
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/label", rp.AddLabel)
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Delete("/label", rp.DeleteLabel)
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
r.Put("/branches/default", rp.SetDefaultBranch)
+8 -4
appview/state/router.go
···
"github.com/gorilla/sessions"
"tangled.sh/tangled.sh/core/appview/issues"
"tangled.sh/tangled.sh/core/appview/knots"
+
"tangled.sh/tangled.sh/core/appview/labels"
"tangled.sh/tangled.sh/core/appview/middleware"
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
"tangled.sh/tangled.sh/core/appview/pipelines"
···
r.Mount("/issues", s.IssuesRouter(mw))
r.Mount("/pulls", s.PullsRouter(mw))
r.Mount("/pipelines", s.PipelinesRouter(mw))
+
r.Mount("/labels", s.LabelsRouter(mw))
// These routes get proxied to the knot
r.Get("/info/refs", s.InfoRefs)
···
Db: s.db,
OAuth: s.oauth,
Pages: s.pages,
-
Config: s.config,
-
Enforcer: s.enforcer,
IdResolver: s.idResolver,
-
Knotstream: s.knotstream,
Logger: logger,
}
···
func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
logger := log.New("repo")
-
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer, logger)
+
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer, logger, s.validator)
return repo.Router(mw)
}
func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler {
pipes := pipelines.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.enforcer)
return pipes.Router(mw)
+
}
+
+
func (s *State) LabelsRouter(mw *middleware.Middleware) http.Handler {
+
ls := labels.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.enforcer)
+
return ls.Router(mw)
}
func (s *State) SignupRouter() http.Handler {
+9 -8
appview/state/state.go
···
}
// Check for existing repos
-
existingRepo, err := db.GetRepo(s.db, user.Did, repoName)
+
existingRepo, err := db.GetRepo(
+
s.db,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("name", repoName),
+
)
if err == nil && existingRepo != nil {
l.Info("repo exists")
s.pages.Notice(w, "repo", fmt.Sprintf("You already have a repository by this name on %s", existingRepo.Knot))
···
Knot: domain,
Rkey: rkey,
Description: description,
+
Created: time.Now(),
}
+
record := repo.AsRecord()
xrpcClient, err := s.oauth.AuthorizedClient(r)
if err != nil {
···
return
}
-
createdAt := time.Now().Format(time.RFC3339)
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.Repo{
-
Knot: repo.Knot,
-
Name: repoName,
-
CreatedAt: createdAt,
-
Owner: user.Did,
-
}},
+
Val: &record,
+
},
})
if err != nil {
l.Info("PDS write failed", "err", err)
-6
appview/strings/strings.go
···
"time"
"tangled.sh/tangled.sh/core/api/tangled"
-
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/middleware"
"tangled.sh/tangled.sh/core/appview/notify"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
-
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
-
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/tid"
"github.com/bluesky-social/indigo/api/atproto"
···
Db *db.DB
OAuth *oauth.OAuth
Pages *pages.Pages
-
Config *config.Config
-
Enforcer *rbac.Enforcer
IdResolver *idresolver.Resolver
Logger *slog.Logger
-
Knotstream *eventconsumer.Consumer
Notifier notify.Notifier
}
+77
appview/validator/label.go
···
+
package validator
+
+
import (
+
"fmt"
+
"regexp"
+
"strings"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"golang.org/x/exp/slices"
+
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview/db"
+
)
+
+
var (
+
// Label name should be alphanumeric with hyphens/underscores, but not start/end with them
+
labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
+
// Color should be a valid hex color
+
colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
+
// You can only label issues and pulls presently
+
validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}
+
)
+
+
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
+
if label.Name == "" {
+
return fmt.Errorf("label name is empty")
+
}
+
if len(label.Name) > 40 {
+
return fmt.Errorf("label name too long (max 40 graphemes)")
+
}
+
if len(label.Name) < 1 {
+
return fmt.Errorf("label name too short (min 1 grapheme)")
+
}
+
if !labelNameRegex.MatchString(label.Name) {
+
return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)")
+
}
+
+
if !label.ValueType.IsConcreteType() {
+
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
+
}
+
+
if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
+
return fmt.Errorf("null type cannot be used in conjunction with enum type")
+
}
+
+
// validate scope (nsid format)
+
if label.Scope == "" {
+
return fmt.Errorf("scope is required")
+
}
+
if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {
+
return fmt.Errorf("failed to parse scope: %w", err)
+
}
+
if !slices.Contains(validScopes, label.Scope) {
+
return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)
+
}
+
+
// validate color if provided
+
if label.Color != nil {
+
color := strings.TrimSpace(*label.Color)
+
if color == "" {
+
// empty color is fine, set to nil
+
label.Color = nil
+
} else {
+
if !colorRegex.MatchString(color) {
+
return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)")
+
}
+
// expand 3-digit hex to 6-digit hex
+
if len(color) == 4 { // #ABC
+
color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3])
+
}
+
// convert to uppercase for consistency
+
color = strings.ToUpper(color)
+
label.Color = &color
+
}
+
}
+
+
return nil
+
}
+5 -1
cmd/gen.go
···
tangled.GitRefUpdate{},
tangled.GitRefUpdate_CommitCountBreakdown{},
tangled.GitRefUpdate_IndividualEmailCommitCount{},
-
tangled.GitRefUpdate_LangBreakdown{},
tangled.GitRefUpdate_IndividualLanguageSize{},
+
tangled.GitRefUpdate_LangBreakdown{},
tangled.GitRefUpdate_Meta{},
tangled.GraphFollow{},
tangled.Knot{},
tangled.KnotMember{},
+
tangled.LabelDefinition{},
+
tangled.LabelDefinition_ValueType{},
+
tangled.LabelOp{},
+
tangled.LabelOp_Operand{},
tangled.Pipeline{},
tangled.Pipeline_CloneOpts{},
tangled.Pipeline_ManualTriggerData{},
+2 -2
knotserver/ingester.go
···
return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname)
}
-
didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name)
+
didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
if err != nil {
return fmt.Errorf("failed to construct relative repo path: %w", err)
}
···
Kind: string(workflow.TriggerKindPullRequest),
PullRequest: &trigger,
Repo: &tangled.Pipeline_TriggerRepo{
-
Did: repo.Owner,
+
Did: ident.DID.String(),
Knot: repo.Knot,
Repo: repo.Name,
},
-36
knotserver/util.go
···
package knotserver
import (
-
"net/http"
-
"os"
-
"path/filepath"
-
"github.com/bluesky-social/indigo/atproto/syntax"
-
securejoin "github.com/cyphar/filepath-securejoin"
-
"github.com/go-chi/chi/v5"
)
-
-
func didPath(r *http.Request) string {
-
did := chi.URLParam(r, "did")
-
name := chi.URLParam(r, "name")
-
path, _ := securejoin.SecureJoin(did, name)
-
filepath.Clean(path)
-
return path
-
}
-
-
func getDescription(path string) (desc string) {
-
db, err := os.ReadFile(filepath.Join(path, "description"))
-
if err == nil {
-
desc = string(db)
-
} else {
-
desc = ""
-
}
-
return
-
}
-
func setContentDisposition(w http.ResponseWriter, name string) {
-
h := "inline; filename=\"" + name + "\""
-
w.Header().Add("Content-Disposition", h)
-
}
-
-
func setGZipMIME(w http.ResponseWriter) {
-
setMIME(w, "application/gzip")
-
}
-
-
func setMIME(w http.ResponseWriter, mime string) {
-
w.Header().Add("Content-Type", mime)
-
}
var TIDClock = syntax.NewTIDClock(0)
+90
lexicons/label/definition.json
···
+
{
+
"lexicon": 1,
+
"id": "sh.tangled.label.definition",
+
"needsCbor": true,
+
"needsType": true,
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "any",
+
"record": {
+
"type": "object",
+
"required": [
+
"name",
+
"valueType",
+
"scope",
+
"createdAt"
+
],
+
"properties": {
+
"name": {
+
"type": "string",
+
"description": "The display name of this label.",
+
"minGraphemes": 1,
+
"maxGraphemes": 40
+
},
+
"valueType": {
+
"type": "ref",
+
"ref": "#valueType",
+
"description": "The type definition of this label. Appviews may allow sorting for certain types."
+
},
+
"scope": {
+
"type": "string",
+
"format": "nsid",
+
"description": "The area of the repo this label may apply to, eg.: sh.tangled.repo.issue. Appviews may choose to respect this."
+
},
+
"color": {
+
"type": "string",
+
"description": "The hex value for the background color for the label. Appviews may choose to respect this."
+
},
+
"createdAt": {
+
"type": "string",
+
"format": "datetime"
+
},
+
"multiple": {
+
"type": "boolean",
+
"description": "Whether this label can be repeated for a given entity, eg.: [reviewer:foo, reviewer:bar]"
+
}
+
}
+
}
+
},
+
"valueType": {
+
"type": "record",
+
"key": "any",
+
"record": {
+
"type": "object",
+
"required": [
+
"type",
+
"format"
+
],
+
"properties": {
+
"type": {
+
"type": "string",
+
"enum": [
+
"null",
+
"boolean",
+
"integer",
+
"string"
+
],
+
"description": "The concrete type of this label's value."
+
},
+
"format": {
+
"type": "string",
+
"enum": [
+
"any",
+
"did",
+
"nsid"
+
],
+
"description": "An optional constraint that can be applied on string concrete types."
+
},
+
"enum": {
+
"type": "array",
+
"description": "Closed set of values that this label can take.",
+
"items": {
+
"type": "string"
+
}
+
}
+
}
+
}
+
}
+
}
+
}
+64
lexicons/label/op.json
···
+
{
+
"lexicon": 1,
+
"id": "sh.tangled.label.op",
+
"needsCbor": true,
+
"needsType": true,
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": [
+
"subject",
+
"add",
+
"delete",
+
"performedAt"
+
],
+
"properties": {
+
"subject": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op."
+
},
+
"performedAt": {
+
"type": "string",
+
"format": "datetime"
+
},
+
"add": {
+
"type": "array",
+
"items": {
+
"type": "ref",
+
"ref": "#operand"
+
}
+
},
+
"delete": {
+
"type": "array",
+
"items": {
+
"type": "ref",
+
"ref": "#operand"
+
}
+
}
+
}
+
}
+
},
+
"operand": {
+
"type": "object",
+
"required": [
+
"key",
+
"value"
+
],
+
"properties": {
+
"key": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "ATURI to the label definition"
+
},
+
"value": {
+
"type": "string",
+
"description": "Stringified value of the label. This is first unstringed by appviews and then interpreted as a concrete value."
+
}
+
}
+
}
+
}
+
}
+8 -5
lexicons/repo/repo.json
···
"required": [
"name",
"knot",
-
"owner",
"createdAt"
],
"properties": {
"name": {
"type": "string",
"description": "name of the repo"
-
},
-
"owner": {
-
"type": "string",
-
"format": "did"
},
"knot": {
"type": "string",
···
"type": "string",
"format": "uri",
"description": "source of the repo"
+
},
+
"labels": {
+
"type": "array",
+
"description": "List of labels that this repo subscribes to",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
}
},
"createdAt": {
"type": "string",
+6 -6
spindle/ingester.go
···
l := s.l.With("component", "ingester", "record", tangled.RepoNSID)
-
l.Info("ingesting repo record")
+
l.Info("ingesting repo record", "did", did)
switch e.Commit.Operation {
case models.CommitOperationCreate, models.CommitOperationUpdate:
···
// no spindle configured for this repo
if record.Spindle == nil {
-
l.Info("no spindle configured", "did", record.Owner, "name", record.Name)
+
l.Info("no spindle configured", "name", record.Name)
return nil
}
// this repo did not want this spindle
if *record.Spindle != domain {
-
l.Info("different spindle configured", "did", record.Owner, "name", record.Name, "spindle", *record.Spindle, "domain", domain)
+
l.Info("different spindle configured", "name", record.Name, "spindle", *record.Spindle, "domain", domain)
return nil
}
// add this repo to the watch list
-
if err := s.db.AddRepo(record.Knot, record.Owner, record.Name); err != nil {
+
if err := s.db.AddRepo(record.Knot, did, record.Name); err != nil {
l.Error("failed to add repo", "error", err)
return fmt.Errorf("failed to add repo: %w", err)
}
-
didSlashRepo, err := securejoin.SecureJoin(record.Owner, record.Name)
+
didSlashRepo, err := securejoin.SecureJoin(did, record.Name)
if err != nil {
return err
}
// add repo to rbac
-
if err := s.e.AddRepo(record.Owner, rbac.ThisServer, didSlashRepo); err != nil {
+
if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil {
l.Error("failed to add repo to enforcer", "error", err)
return fmt.Errorf("failed to add repo: %w", err)
}
+1 -1
spindle/xrpc/add_secret.go
···
}
repo := resp.Value.Val.(*tangled.Repo)
-
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
if err != nil {
fail(xrpcerr.GenericError(err))
return
+1 -1
spindle/xrpc/list_secrets.go
···
}
repo := resp.Value.Val.(*tangled.Repo)
-
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
if err != nil {
fail(xrpcerr.GenericError(err))
return
+1 -1
spindle/xrpc/remove_secret.go
···
}
repo := resp.Value.Val.(*tangled.Repo)
-
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
if err != nil {
fail(xrpcerr.GenericError(err))
return