···3844384438453845 return nil
38463846}
38473847+func (t *PijulRefUpdate) MarshalCBOR(w io.Writer) error {
38483848+ if t == nil {
38493849+ _, err := w.Write(cbg.CborNull)
38503850+ return err
38513851+ }
38523852+38533853+ cw := cbg.NewCborWriter(w)
38543854+ fieldCount := 8
38553855+38563856+ if t.Languages == nil {
38573857+ fieldCount--
38583858+ }
38593859+38603860+ if t.OldState == nil {
38613861+ fieldCount--
38623862+ }
38633863+38643864+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
38653865+ return err
38663866+ }
38673867+38683868+ // t.Repo (string) (string)
38693869+ if len("repo") > 1000000 {
38703870+ return xerrors.Errorf("Value in field \"repo\" was too long")
38713871+ }
38723872+38733873+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil {
38743874+ return err
38753875+ }
38763876+ if _, err := cw.WriteString(string("repo")); err != nil {
38773877+ return err
38783878+ }
38793879+38803880+ if len(t.Repo) > 1000000 {
38813881+ return xerrors.Errorf("Value in field t.Repo was too long")
38823882+ }
38833883+38843884+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil {
38853885+ return err
38863886+ }
38873887+ if _, err := cw.WriteString(string(t.Repo)); err != nil {
38883888+ return err
38893889+ }
38903890+38913891+ // t.CommitterDid (string) (string)
38923892+ if len("committerDid") > 1000000 {
38933893+ return xerrors.Errorf("Value in field \"committerDid\" was too long")
38943894+ }
38953895+38963896+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("committerDid"))); err != nil {
38973897+ return err
38983898+ }
38993899+ if _, err := cw.WriteString(string("committerDid")); err != nil {
39003900+ return err
39013901+ }
39023902+39033903+ if len(t.CommitterDid) > 1000000 {
39043904+ return xerrors.Errorf("Value in field t.CommitterDid was too long")
39053905+ }
39063906+39073907+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CommitterDid))); err != nil {
39083908+ return err
39093909+ }
39103910+ if _, err := cw.WriteString(string(t.CommitterDid)); err != nil {
39113911+ return err
39123912+ }
39133913+39143914+ // t.LexiconTypeID (string) (string)
39153915+ if len("$type") > 1000000 {
39163916+ return xerrors.Errorf("Value in field \"$type\" was too long")
39173917+ }
39183918+39193919+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
39203920+ return err
39213921+ }
39223922+ if _, err := cw.WriteString(string("$type")); err != nil {
39233923+ return err
39243924+ }
39253925+39263926+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.pijul.refUpdate"))); err != nil {
39273927+ return err
39283928+ }
39293929+ if _, err := cw.WriteString(string("sh.tangled.pijul.refUpdate")); err != nil {
39303930+ return err
39313931+ }
39323932+39333933+ // t.Changes ([]string) (slice)
39343934+ if len("changes") > 1000000 {
39353935+ return xerrors.Errorf("Value in field \"changes\" was too long")
39363936+ }
39373937+39383938+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("changes"))); err != nil {
39393939+ return err
39403940+ }
39413941+ if _, err := cw.WriteString(string("changes")); err != nil {
39423942+ return err
39433943+ }
39443944+39453945+ if len(t.Changes) > 8192 {
39463946+ return xerrors.Errorf("Slice value in field t.Changes was too long")
39473947+ }
39483948+39493949+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Changes))); err != nil {
39503950+ return err
39513951+ }
39523952+ for _, v := range t.Changes {
39533953+ if len(v) > 1000000 {
39543954+ return xerrors.Errorf("Value in field v was too long")
39553955+ }
39563956+39573957+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
39583958+ return err
39593959+ }
39603960+ if _, err := cw.WriteString(string(v)); err != nil {
39613961+ return err
39623962+ }
39633963+39643964+ }
39653965+39663966+ // t.Channel (string) (string)
39673967+ if len("channel") > 1000000 {
39683968+ return xerrors.Errorf("Value in field \"channel\" was too long")
39693969+ }
39703970+39713971+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("channel"))); err != nil {
39723972+ return err
39733973+ }
39743974+ if _, err := cw.WriteString(string("channel")); err != nil {
39753975+ return err
39763976+ }
39773977+39783978+ if len(t.Channel) > 1000000 {
39793979+ return xerrors.Errorf("Value in field t.Channel was too long")
39803980+ }
39813981+39823982+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Channel))); err != nil {
39833983+ return err
39843984+ }
39853985+ if _, err := cw.WriteString(string(t.Channel)); err != nil {
39863986+ return err
39873987+ }
39883988+39893989+ // t.NewState (string) (string)
39903990+ if len("newState") > 1000000 {
39913991+ return xerrors.Errorf("Value in field \"newState\" was too long")
39923992+ }
39933993+39943994+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("newState"))); err != nil {
39953995+ return err
39963996+ }
39973997+ if _, err := cw.WriteString(string("newState")); err != nil {
39983998+ return err
39993999+ }
40004000+40014001+ if len(t.NewState) > 1000000 {
40024002+ return xerrors.Errorf("Value in field t.NewState was too long")
40034003+ }
40044004+40054005+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.NewState))); err != nil {
40064006+ return err
40074007+ }
40084008+ if _, err := cw.WriteString(string(t.NewState)); err != nil {
40094009+ return err
40104010+ }
40114011+40124012+ // t.OldState (string) (string)
40134013+ if t.OldState != nil {
40144014+40154015+ if len("oldState") > 1000000 {
40164016+ return xerrors.Errorf("Value in field \"oldState\" was too long")
40174017+ }
40184018+40194019+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("oldState"))); err != nil {
40204020+ return err
40214021+ }
40224022+ if _, err := cw.WriteString(string("oldState")); err != nil {
40234023+ return err
40244024+ }
40254025+40264026+ if t.OldState == nil {
40274027+ if _, err := cw.Write(cbg.CborNull); err != nil {
40284028+ return err
40294029+ }
40304030+ } else {
40314031+ if len(*t.OldState) > 1000000 {
40324032+ return xerrors.Errorf("Value in field t.OldState was too long")
40334033+ }
40344034+40354035+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.OldState))); err != nil {
40364036+ return err
40374037+ }
40384038+ if _, err := cw.WriteString(string(*t.OldState)); err != nil {
40394039+ return err
40404040+ }
40414041+ }
40424042+ }
40434043+40444044+ // t.Languages (tangled.PijulRefUpdate_Languages) (struct)
40454045+ if t.Languages != nil {
40464046+40474047+ if len("languages") > 1000000 {
40484048+ return xerrors.Errorf("Value in field \"languages\" was too long")
40494049+ }
40504050+40514051+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("languages"))); err != nil {
40524052+ return err
40534053+ }
40544054+ if _, err := cw.WriteString(string("languages")); err != nil {
40554055+ return err
40564056+ }
40574057+40584058+ if err := t.Languages.MarshalCBOR(cw); err != nil {
40594059+ return err
40604060+ }
40614061+ }
40624062+ return nil
40634063+}
40644064+40654065+func (t *PijulRefUpdate) UnmarshalCBOR(r io.Reader) (err error) {
40664066+ *t = PijulRefUpdate{}
40674067+40684068+ cr := cbg.NewCborReader(r)
40694069+40704070+ maj, extra, err := cr.ReadHeader()
40714071+ if err != nil {
40724072+ return err
40734073+ }
40744074+ defer func() {
40754075+ if err == io.EOF {
40764076+ err = io.ErrUnexpectedEOF
40774077+ }
40784078+ }()
40794079+40804080+ if maj != cbg.MajMap {
40814081+ return fmt.Errorf("cbor input should be of type map")
40824082+ }
40834083+40844084+ if extra > cbg.MaxLength {
40854085+ return fmt.Errorf("PijulRefUpdate: map struct too large (%d)", extra)
40864086+ }
40874087+40884088+ n := extra
40894089+40904090+ nameBuf := make([]byte, 9)
40914091+ for i := uint64(0); i < n; i++ {
40924092+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
40934093+ if err != nil {
40944094+ return err
40954095+ }
40964096+40974097+ if !ok {
40984098+ // Field doesn't exist on this type, so ignore it
40994099+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
41004100+ return err
41014101+ }
41024102+ continue
41034103+ }
41044104+41054105+ switch string(nameBuf[:nameLen]) {
41064106+ // t.Repo (string) (string)
41074107+ case "repo":
41084108+41094109+ {
41104110+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41114111+ if err != nil {
41124112+ return err
41134113+ }
41144114+41154115+ t.Repo = string(sval)
41164116+ }
41174117+ // t.LexiconTypeID (string) (string)
41184118+ case "$type":
41194119+41204120+ {
41214121+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41224122+ if err != nil {
41234123+ return err
41244124+ }
41254125+41264126+ t.LexiconTypeID = string(sval)
41274127+ }
41284128+ // t.Changes ([]string) (slice)
41294129+ case "changes":
41304130+41314131+ maj, extra, err = cr.ReadHeader()
41324132+ if err != nil {
41334133+ return err
41344134+ }
41354135+41364136+ if extra > 8192 {
41374137+ return fmt.Errorf("t.Changes: array too large (%d)", extra)
41384138+ }
41394139+41404140+ if maj != cbg.MajArray {
41414141+ return fmt.Errorf("expected cbor array")
41424142+ }
41434143+41444144+ if extra > 0 {
41454145+ t.Changes = make([]string, extra)
41464146+ }
41474147+41484148+ for i := 0; i < int(extra); i++ {
41494149+ {
41504150+ var maj byte
41514151+ var extra uint64
41524152+ var err error
41534153+ _ = maj
41544154+ _ = extra
41554155+ _ = err
41564156+41574157+ {
41584158+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41594159+ if err != nil {
41604160+ return err
41614161+ }
41624162+41634163+ t.Changes[i] = string(sval)
41644164+ }
41654165+41664166+ }
41674167+ }
41684168+ // t.Channel (string) (string)
41694169+ case "channel":
41704170+41714171+ {
41724172+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41734173+ if err != nil {
41744174+ return err
41754175+ }
41764176+41774177+ t.Channel = string(sval)
41784178+ }
41794179+ // t.CommitterDid (string) (string)
41804180+ case "committerDid":
41814181+41824182+ {
41834183+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41844184+ if err != nil {
41854185+ return err
41864186+ }
41874187+41884188+ t.CommitterDid = string(sval)
41894189+ }
41904190+ // t.NewState (string) (string)
41914191+ case "newState":
41924192+41934193+ {
41944194+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
41954195+ if err != nil {
41964196+ return err
41974197+ }
41984198+41994199+ t.NewState = string(sval)
42004200+ }
42014201+ // t.OldState (string) (string)
42024202+ case "oldState":
42034203+42044204+ {
42054205+ b, err := cr.ReadByte()
42064206+ if err != nil {
42074207+ return err
42084208+ }
42094209+ if b != cbg.CborNull[0] {
42104210+ if err := cr.UnreadByte(); err != nil {
42114211+ return err
42124212+ }
42134213+42144214+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
42154215+ if err != nil {
42164216+ return err
42174217+ }
42184218+42194219+ t.OldState = (*string)(&sval)
42204220+ }
42214221+ }
42224222+ // t.Languages (tangled.PijulRefUpdate_Languages) (struct)
42234223+ case "languages":
42244224+42254225+ {
42264226+42274227+ b, err := cr.ReadByte()
42284228+ if err != nil {
42294229+ return err
42304230+ }
42314231+ if b != cbg.CborNull[0] {
42324232+ if err := cr.UnreadByte(); err != nil {
42334233+ return err
42344234+ }
42354235+ t.Languages = new(PijulRefUpdate_Languages)
42364236+ if err := t.Languages.UnmarshalCBOR(cr); err != nil {
42374237+ return xerrors.Errorf("unmarshaling t.Languages pointer: %w", err)
42384238+ }
42394239+ }
42404240+42414241+ }
42424242+42434243+ default:
42444244+ // Field doesn't exist on this type, so ignore it
42454245+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
42464246+ return err
42474247+ }
42484248+ }
42494249+ }
42504250+42514251+ return nil
42524252+}
38474253func (t *Pipeline) MarshalCBOR(w io.Writer) error {
38484254 if t == nil {
38494255 _, err := w.Write(cbg.CborNull)
···74807886 }
7481788774827888 t.CreatedAt = string(sval)
78897889+ }
78907890+78917891+ default:
78927892+ // Field doesn't exist on this type, so ignore it
78937893+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
78947894+ return err
78957895+ }
78967896+ }
78977897+ }
78987898+78997899+ return nil
79007900+}
79017901+func (t *RepoDiscussion) MarshalCBOR(w io.Writer) error {
79027902+ if t == nil {
79037903+ _, err := w.Write(cbg.CborNull)
79047904+ return err
79057905+ }
79067906+79077907+ cw := cbg.NewCborWriter(w)
79087908+ fieldCount := 8
79097909+79107910+ if t.Body == nil {
79117911+ fieldCount--
79127912+ }
79137913+79147914+ if t.Mentions == nil {
79157915+ fieldCount--
79167916+ }
79177917+79187918+ if t.References == nil {
79197919+ fieldCount--
79207920+ }
79217921+79227922+ if t.TargetChannel == nil {
79237923+ fieldCount--
79247924+ }
79257925+79267926+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
79277927+ return err
79287928+ }
79297929+79307930+ // t.Body (string) (string)
79317931+ if t.Body != nil {
79327932+79337933+ if len("body") > 1000000 {
79347934+ return xerrors.Errorf("Value in field \"body\" was too long")
79357935+ }
79367936+79377937+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil {
79387938+ return err
79397939+ }
79407940+ if _, err := cw.WriteString(string("body")); err != nil {
79417941+ return err
79427942+ }
79437943+79447944+ if t.Body == nil {
79457945+ if _, err := cw.Write(cbg.CborNull); err != nil {
79467946+ return err
79477947+ }
79487948+ } else {
79497949+ if len(*t.Body) > 1000000 {
79507950+ return xerrors.Errorf("Value in field t.Body was too long")
79517951+ }
79527952+79537953+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil {
79547954+ return err
79557955+ }
79567956+ if _, err := cw.WriteString(string(*t.Body)); err != nil {
79577957+ return err
79587958+ }
79597959+ }
79607960+ }
79617961+79627962+ // t.Repo (string) (string)
79637963+ if len("repo") > 1000000 {
79647964+ return xerrors.Errorf("Value in field \"repo\" was too long")
79657965+ }
79667966+79677967+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil {
79687968+ return err
79697969+ }
79707970+ if _, err := cw.WriteString(string("repo")); err != nil {
79717971+ return err
79727972+ }
79737973+79747974+ if len(t.Repo) > 1000000 {
79757975+ return xerrors.Errorf("Value in field t.Repo was too long")
79767976+ }
79777977+79787978+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil {
79797979+ return err
79807980+ }
79817981+ if _, err := cw.WriteString(string(t.Repo)); err != nil {
79827982+ return err
79837983+ }
79847984+79857985+ // t.LexiconTypeID (string) (string)
79867986+ if len("$type") > 1000000 {
79877987+ return xerrors.Errorf("Value in field \"$type\" was too long")
79887988+ }
79897989+79907990+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
79917991+ return err
79927992+ }
79937993+ if _, err := cw.WriteString(string("$type")); err != nil {
79947994+ return err
79957995+ }
79967996+79977997+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.discussion"))); err != nil {
79987998+ return err
79997999+ }
80008000+ if _, err := cw.WriteString(string("sh.tangled.repo.discussion")); err != nil {
80018001+ return err
80028002+ }
80038003+80048004+ // t.Title (string) (string)
80058005+ if len("title") > 1000000 {
80068006+ return xerrors.Errorf("Value in field \"title\" was too long")
80078007+ }
80088008+80098009+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil {
80108010+ return err
80118011+ }
80128012+ if _, err := cw.WriteString(string("title")); err != nil {
80138013+ return err
80148014+ }
80158015+80168016+ if len(t.Title) > 1000000 {
80178017+ return xerrors.Errorf("Value in field t.Title was too long")
80188018+ }
80198019+80208020+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil {
80218021+ return err
80228022+ }
80238023+ if _, err := cw.WriteString(string(t.Title)); err != nil {
80248024+ return err
80258025+ }
80268026+80278027+ // t.Mentions ([]string) (slice)
80288028+ if t.Mentions != nil {
80298029+80308030+ if len("mentions") > 1000000 {
80318031+ return xerrors.Errorf("Value in field \"mentions\" was too long")
80328032+ }
80338033+80348034+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
80358035+ return err
80368036+ }
80378037+ if _, err := cw.WriteString(string("mentions")); err != nil {
80388038+ return err
80398039+ }
80408040+80418041+ if len(t.Mentions) > 8192 {
80428042+ return xerrors.Errorf("Slice value in field t.Mentions was too long")
80438043+ }
80448044+80458045+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
80468046+ return err
80478047+ }
80488048+ for _, v := range t.Mentions {
80498049+ if len(v) > 1000000 {
80508050+ return xerrors.Errorf("Value in field v was too long")
80518051+ }
80528052+80538053+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
80548054+ return err
80558055+ }
80568056+ if _, err := cw.WriteString(string(v)); err != nil {
80578057+ return err
80588058+ }
80598059+80608060+ }
80618061+ }
80628062+80638063+ // t.CreatedAt (string) (string)
80648064+ if len("createdAt") > 1000000 {
80658065+ return xerrors.Errorf("Value in field \"createdAt\" was too long")
80668066+ }
80678067+80688068+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
80698069+ return err
80708070+ }
80718071+ if _, err := cw.WriteString(string("createdAt")); err != nil {
80728072+ return err
80738073+ }
80748074+80758075+ if len(t.CreatedAt) > 1000000 {
80768076+ return xerrors.Errorf("Value in field t.CreatedAt was too long")
80778077+ }
80788078+80798079+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
80808080+ return err
80818081+ }
80828082+ if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
80838083+ return err
80848084+ }
80858085+80868086+ // t.References ([]string) (slice)
80878087+ if t.References != nil {
80888088+80898089+ if len("references") > 1000000 {
80908090+ return xerrors.Errorf("Value in field \"references\" was too long")
80918091+ }
80928092+80938093+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
80948094+ return err
80958095+ }
80968096+ if _, err := cw.WriteString(string("references")); err != nil {
80978097+ return err
80988098+ }
80998099+81008100+ if len(t.References) > 8192 {
81018101+ return xerrors.Errorf("Slice value in field t.References was too long")
81028102+ }
81038103+81048104+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
81058105+ return err
81068106+ }
81078107+ for _, v := range t.References {
81088108+ if len(v) > 1000000 {
81098109+ return xerrors.Errorf("Value in field v was too long")
81108110+ }
81118111+81128112+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
81138113+ return err
81148114+ }
81158115+ if _, err := cw.WriteString(string(v)); err != nil {
81168116+ return err
81178117+ }
81188118+81198119+ }
81208120+ }
81218121+81228122+ // t.TargetChannel (string) (string)
81238123+ if t.TargetChannel != nil {
81248124+81258125+ if len("targetChannel") > 1000000 {
81268126+ return xerrors.Errorf("Value in field \"targetChannel\" was too long")
81278127+ }
81288128+81298129+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetChannel"))); err != nil {
81308130+ return err
81318131+ }
81328132+ if _, err := cw.WriteString(string("targetChannel")); err != nil {
81338133+ return err
81348134+ }
81358135+81368136+ if t.TargetChannel == nil {
81378137+ if _, err := cw.Write(cbg.CborNull); err != nil {
81388138+ return err
81398139+ }
81408140+ } else {
81418141+ if len(*t.TargetChannel) > 1000000 {
81428142+ return xerrors.Errorf("Value in field t.TargetChannel was too long")
81438143+ }
81448144+81458145+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.TargetChannel))); err != nil {
81468146+ return err
81478147+ }
81488148+ if _, err := cw.WriteString(string(*t.TargetChannel)); err != nil {
81498149+ return err
81508150+ }
81518151+ }
81528152+ }
81538153+ return nil
81548154+}
81558155+81568156+func (t *RepoDiscussion) UnmarshalCBOR(r io.Reader) (err error) {
81578157+ *t = RepoDiscussion{}
81588158+81598159+ cr := cbg.NewCborReader(r)
81608160+81618161+ maj, extra, err := cr.ReadHeader()
81628162+ if err != nil {
81638163+ return err
81648164+ }
81658165+ defer func() {
81668166+ if err == io.EOF {
81678167+ err = io.ErrUnexpectedEOF
81688168+ }
81698169+ }()
81708170+81718171+ if maj != cbg.MajMap {
81728172+ return fmt.Errorf("cbor input should be of type map")
81738173+ }
81748174+81758175+ if extra > cbg.MaxLength {
81768176+ return fmt.Errorf("RepoDiscussion: map struct too large (%d)", extra)
81778177+ }
81788178+81798179+ n := extra
81808180+81818181+ nameBuf := make([]byte, 13)
81828182+ for i := uint64(0); i < n; i++ {
81838183+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
81848184+ if err != nil {
81858185+ return err
81868186+ }
81878187+81888188+ if !ok {
81898189+ // Field doesn't exist on this type, so ignore it
81908190+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
81918191+ return err
81928192+ }
81938193+ continue
81948194+ }
81958195+81968196+ switch string(nameBuf[:nameLen]) {
81978197+ // t.Body (string) (string)
81988198+ case "body":
81998199+82008200+ {
82018201+ b, err := cr.ReadByte()
82028202+ if err != nil {
82038203+ return err
82048204+ }
82058205+ if b != cbg.CborNull[0] {
82068206+ if err := cr.UnreadByte(); err != nil {
82078207+ return err
82088208+ }
82098209+82108210+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82118211+ if err != nil {
82128212+ return err
82138213+ }
82148214+82158215+ t.Body = (*string)(&sval)
82168216+ }
82178217+ }
82188218+ // t.Repo (string) (string)
82198219+ case "repo":
82208220+82218221+ {
82228222+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82238223+ if err != nil {
82248224+ return err
82258225+ }
82268226+82278227+ t.Repo = string(sval)
82288228+ }
82298229+ // t.LexiconTypeID (string) (string)
82308230+ case "$type":
82318231+82328232+ {
82338233+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82348234+ if err != nil {
82358235+ return err
82368236+ }
82378237+82388238+ t.LexiconTypeID = string(sval)
82398239+ }
82408240+ // t.Title (string) (string)
82418241+ case "title":
82428242+82438243+ {
82448244+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82458245+ if err != nil {
82468246+ return err
82478247+ }
82488248+82498249+ t.Title = string(sval)
82508250+ }
82518251+ // t.Mentions ([]string) (slice)
82528252+ case "mentions":
82538253+82548254+ maj, extra, err = cr.ReadHeader()
82558255+ if err != nil {
82568256+ return err
82578257+ }
82588258+82598259+ if extra > 8192 {
82608260+ return fmt.Errorf("t.Mentions: array too large (%d)", extra)
82618261+ }
82628262+82638263+ if maj != cbg.MajArray {
82648264+ return fmt.Errorf("expected cbor array")
82658265+ }
82668266+82678267+ if extra > 0 {
82688268+ t.Mentions = make([]string, extra)
82698269+ }
82708270+82718271+ for i := 0; i < int(extra); i++ {
82728272+ {
82738273+ var maj byte
82748274+ var extra uint64
82758275+ var err error
82768276+ _ = maj
82778277+ _ = extra
82788278+ _ = err
82798279+82808280+ {
82818281+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82828282+ if err != nil {
82838283+ return err
82848284+ }
82858285+82868286+ t.Mentions[i] = string(sval)
82878287+ }
82888288+82898289+ }
82908290+ }
82918291+ // t.CreatedAt (string) (string)
82928292+ case "createdAt":
82938293+82948294+ {
82958295+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
82968296+ if err != nil {
82978297+ return err
82988298+ }
82998299+83008300+ t.CreatedAt = string(sval)
83018301+ }
83028302+ // t.References ([]string) (slice)
83038303+ case "references":
83048304+83058305+ maj, extra, err = cr.ReadHeader()
83068306+ if err != nil {
83078307+ return err
83088308+ }
83098309+83108310+ if extra > 8192 {
83118311+ return fmt.Errorf("t.References: array too large (%d)", extra)
83128312+ }
83138313+83148314+ if maj != cbg.MajArray {
83158315+ return fmt.Errorf("expected cbor array")
83168316+ }
83178317+83188318+ if extra > 0 {
83198319+ t.References = make([]string, extra)
83208320+ }
83218321+83228322+ for i := 0; i < int(extra); i++ {
83238323+ {
83248324+ var maj byte
83258325+ var extra uint64
83268326+ var err error
83278327+ _ = maj
83288328+ _ = extra
83298329+ _ = err
83308330+83318331+ {
83328332+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
83338333+ if err != nil {
83348334+ return err
83358335+ }
83368336+83378337+ t.References[i] = string(sval)
83388338+ }
83398339+83408340+ }
83418341+ }
83428342+ // t.TargetChannel (string) (string)
83438343+ case "targetChannel":
83448344+83458345+ {
83468346+ b, err := cr.ReadByte()
83478347+ if err != nil {
83488348+ return err
83498349+ }
83508350+ if b != cbg.CborNull[0] {
83518351+ if err := cr.UnreadByte(); err != nil {
83528352+ return err
83538353+ }
83548354+83558355+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
83568356+ if err != nil {
83578357+ return err
83588358+ }
83598359+83608360+ t.TargetChannel = (*string)(&sval)
83618361+ }
83628362+ }
83638363+83648364+ default:
83658365+ // Field doesn't exist on this type, so ignore it
83668366+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
83678367+ return err
83688368+ }
83698369+ }
83708370+ }
83718371+83728372+ return nil
83738373+}
83748374+func (t *RepoDiscussionComment) MarshalCBOR(w io.Writer) error {
83758375+ if t == nil {
83768376+ _, err := w.Write(cbg.CborNull)
83778377+ return err
83788378+ }
83798379+83808380+ cw := cbg.NewCborWriter(w)
83818381+ fieldCount := 7
83828382+83838383+ if t.Mentions == nil {
83848384+ fieldCount--
83858385+ }
83868386+83878387+ if t.References == nil {
83888388+ fieldCount--
83898389+ }
83908390+83918391+ if t.ReplyTo == nil {
83928392+ fieldCount--
83938393+ }
83948394+83958395+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
83968396+ return err
83978397+ }
83988398+83998399+ // t.Body (string) (string)
84008400+ if len("body") > 1000000 {
84018401+ return xerrors.Errorf("Value in field \"body\" was too long")
84028402+ }
84038403+84048404+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil {
84058405+ return err
84068406+ }
84078407+ if _, err := cw.WriteString(string("body")); err != nil {
84088408+ return err
84098409+ }
84108410+84118411+ if len(t.Body) > 1000000 {
84128412+ return xerrors.Errorf("Value in field t.Body was too long")
84138413+ }
84148414+84158415+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil {
84168416+ return err
84178417+ }
84188418+ if _, err := cw.WriteString(string(t.Body)); err != nil {
84198419+ return err
84208420+ }
84218421+84228422+ // t.LexiconTypeID (string) (string)
84238423+ if len("$type") > 1000000 {
84248424+ return xerrors.Errorf("Value in field \"$type\" was too long")
84258425+ }
84268426+84278427+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
84288428+ return err
84298429+ }
84308430+ if _, err := cw.WriteString(string("$type")); err != nil {
84318431+ return err
84328432+ }
84338433+84348434+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.discussion.comment"))); err != nil {
84358435+ return err
84368436+ }
84378437+ if _, err := cw.WriteString(string("sh.tangled.repo.discussion.comment")); err != nil {
84388438+ return err
84398439+ }
84408440+84418441+ // t.ReplyTo (string) (string)
84428442+ if t.ReplyTo != nil {
84438443+84448444+ if len("replyTo") > 1000000 {
84458445+ return xerrors.Errorf("Value in field \"replyTo\" was too long")
84468446+ }
84478447+84488448+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("replyTo"))); err != nil {
84498449+ return err
84508450+ }
84518451+ if _, err := cw.WriteString(string("replyTo")); err != nil {
84528452+ return err
84538453+ }
84548454+84558455+ if t.ReplyTo == nil {
84568456+ if _, err := cw.Write(cbg.CborNull); err != nil {
84578457+ return err
84588458+ }
84598459+ } else {
84608460+ if len(*t.ReplyTo) > 1000000 {
84618461+ return xerrors.Errorf("Value in field t.ReplyTo was too long")
84628462+ }
84638463+84648464+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ReplyTo))); err != nil {
84658465+ return err
84668466+ }
84678467+ if _, err := cw.WriteString(string(*t.ReplyTo)); err != nil {
84688468+ return err
84698469+ }
84708470+ }
84718471+ }
84728472+84738473+ // t.Mentions ([]string) (slice)
84748474+ if t.Mentions != nil {
84758475+84768476+ if len("mentions") > 1000000 {
84778477+ return xerrors.Errorf("Value in field \"mentions\" was too long")
84788478+ }
84798479+84808480+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
84818481+ return err
84828482+ }
84838483+ if _, err := cw.WriteString(string("mentions")); err != nil {
84848484+ return err
84858485+ }
84868486+84878487+ if len(t.Mentions) > 8192 {
84888488+ return xerrors.Errorf("Slice value in field t.Mentions was too long")
84898489+ }
84908490+84918491+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
84928492+ return err
84938493+ }
84948494+ for _, v := range t.Mentions {
84958495+ if len(v) > 1000000 {
84968496+ return xerrors.Errorf("Value in field v was too long")
84978497+ }
84988498+84998499+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
85008500+ return err
85018501+ }
85028502+ if _, err := cw.WriteString(string(v)); err != nil {
85038503+ return err
85048504+ }
85058505+85068506+ }
85078507+ }
85088508+85098509+ // t.CreatedAt (string) (string)
85108510+ if len("createdAt") > 1000000 {
85118511+ return xerrors.Errorf("Value in field \"createdAt\" was too long")
85128512+ }
85138513+85148514+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
85158515+ return err
85168516+ }
85178517+ if _, err := cw.WriteString(string("createdAt")); err != nil {
85188518+ return err
85198519+ }
85208520+85218521+ if len(t.CreatedAt) > 1000000 {
85228522+ return xerrors.Errorf("Value in field t.CreatedAt was too long")
85238523+ }
85248524+85258525+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
85268526+ return err
85278527+ }
85288528+ if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
85298529+ return err
85308530+ }
85318531+85328532+ // t.Discussion (string) (string)
85338533+ if len("discussion") > 1000000 {
85348534+ return xerrors.Errorf("Value in field \"discussion\" was too long")
85358535+ }
85368536+85378537+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("discussion"))); err != nil {
85388538+ return err
85398539+ }
85408540+ if _, err := cw.WriteString(string("discussion")); err != nil {
85418541+ return err
85428542+ }
85438543+85448544+ if len(t.Discussion) > 1000000 {
85458545+ return xerrors.Errorf("Value in field t.Discussion was too long")
85468546+ }
85478547+85488548+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Discussion))); err != nil {
85498549+ return err
85508550+ }
85518551+ if _, err := cw.WriteString(string(t.Discussion)); err != nil {
85528552+ return err
85538553+ }
85548554+85558555+ // t.References ([]string) (slice)
85568556+ if t.References != nil {
85578557+85588558+ if len("references") > 1000000 {
85598559+ return xerrors.Errorf("Value in field \"references\" was too long")
85608560+ }
85618561+85628562+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
85638563+ return err
85648564+ }
85658565+ if _, err := cw.WriteString(string("references")); err != nil {
85668566+ return err
85678567+ }
85688568+85698569+ if len(t.References) > 8192 {
85708570+ return xerrors.Errorf("Slice value in field t.References was too long")
85718571+ }
85728572+85738573+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
85748574+ return err
85758575+ }
85768576+ for _, v := range t.References {
85778577+ if len(v) > 1000000 {
85788578+ return xerrors.Errorf("Value in field v was too long")
85798579+ }
85808580+85818581+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
85828582+ return err
85838583+ }
85848584+ if _, err := cw.WriteString(string(v)); err != nil {
85858585+ return err
85868586+ }
85878587+85888588+ }
85898589+ }
85908590+ return nil
85918591+}
85928592+85938593+func (t *RepoDiscussionComment) UnmarshalCBOR(r io.Reader) (err error) {
85948594+ *t = RepoDiscussionComment{}
85958595+85968596+ cr := cbg.NewCborReader(r)
85978597+85988598+ maj, extra, err := cr.ReadHeader()
85998599+ if err != nil {
86008600+ return err
86018601+ }
86028602+ defer func() {
86038603+ if err == io.EOF {
86048604+ err = io.ErrUnexpectedEOF
86058605+ }
86068606+ }()
86078607+86088608+ if maj != cbg.MajMap {
86098609+ return fmt.Errorf("cbor input should be of type map")
86108610+ }
86118611+86128612+ if extra > cbg.MaxLength {
86138613+ return fmt.Errorf("RepoDiscussionComment: map struct too large (%d)", extra)
86148614+ }
86158615+86168616+ n := extra
86178617+86188618+ nameBuf := make([]byte, 10)
86198619+ for i := uint64(0); i < n; i++ {
86208620+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
86218621+ if err != nil {
86228622+ return err
86238623+ }
86248624+86258625+ if !ok {
86268626+ // Field doesn't exist on this type, so ignore it
86278627+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
86288628+ return err
86298629+ }
86308630+ continue
86318631+ }
86328632+86338633+ switch string(nameBuf[:nameLen]) {
86348634+ // t.Body (string) (string)
86358635+ case "body":
86368636+86378637+ {
86388638+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
86398639+ if err != nil {
86408640+ return err
86418641+ }
86428642+86438643+ t.Body = string(sval)
86448644+ }
86458645+ // t.LexiconTypeID (string) (string)
86468646+ case "$type":
86478647+86488648+ {
86498649+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
86508650+ if err != nil {
86518651+ return err
86528652+ }
86538653+86548654+ t.LexiconTypeID = string(sval)
86558655+ }
86568656+ // t.ReplyTo (string) (string)
86578657+ case "replyTo":
86588658+86598659+ {
86608660+ b, err := cr.ReadByte()
86618661+ if err != nil {
86628662+ return err
86638663+ }
86648664+ if b != cbg.CborNull[0] {
86658665+ if err := cr.UnreadByte(); err != nil {
86668666+ return err
86678667+ }
86688668+86698669+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
86708670+ if err != nil {
86718671+ return err
86728672+ }
86738673+86748674+ t.ReplyTo = (*string)(&sval)
86758675+ }
86768676+ }
86778677+ // t.Mentions ([]string) (slice)
86788678+ case "mentions":
86798679+86808680+ maj, extra, err = cr.ReadHeader()
86818681+ if err != nil {
86828682+ return err
86838683+ }
86848684+86858685+ if extra > 8192 {
86868686+ return fmt.Errorf("t.Mentions: array too large (%d)", extra)
86878687+ }
86888688+86898689+ if maj != cbg.MajArray {
86908690+ return fmt.Errorf("expected cbor array")
86918691+ }
86928692+86938693+ if extra > 0 {
86948694+ t.Mentions = make([]string, extra)
86958695+ }
86968696+86978697+ for i := 0; i < int(extra); i++ {
86988698+ {
86998699+ var maj byte
87008700+ var extra uint64
87018701+ var err error
87028702+ _ = maj
87038703+ _ = extra
87048704+ _ = err
87058705+87068706+ {
87078707+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
87088708+ if err != nil {
87098709+ return err
87108710+ }
87118711+87128712+ t.Mentions[i] = string(sval)
87138713+ }
87148714+87158715+ }
87168716+ }
87178717+ // t.CreatedAt (string) (string)
87188718+ case "createdAt":
87198719+87208720+ {
87218721+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
87228722+ if err != nil {
87238723+ return err
87248724+ }
87258725+87268726+ t.CreatedAt = string(sval)
87278727+ }
87288728+ // t.Discussion (string) (string)
87298729+ case "discussion":
87308730+87318731+ {
87328732+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
87338733+ if err != nil {
87348734+ return err
87358735+ }
87368736+87378737+ t.Discussion = string(sval)
87388738+ }
87398739+ // t.References ([]string) (slice)
87408740+ case "references":
87418741+87428742+ maj, extra, err = cr.ReadHeader()
87438743+ if err != nil {
87448744+ return err
87458745+ }
87468746+87478747+ if extra > 8192 {
87488748+ return fmt.Errorf("t.References: array too large (%d)", extra)
87498749+ }
87508750+87518751+ if maj != cbg.MajArray {
87528752+ return fmt.Errorf("expected cbor array")
87538753+ }
87548754+87558755+ if extra > 0 {
87568756+ t.References = make([]string, extra)
87578757+ }
87588758+87598759+ for i := 0; i < int(extra); i++ {
87608760+ {
87618761+ var maj byte
87628762+ var extra uint64
87638763+ var err error
87648764+ _ = maj
87658765+ _ = extra
87668766+ _ = err
87678767+87688768+ {
87698769+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
87708770+ if err != nil {
87718771+ return err
87728772+ }
87738773+87748774+ t.References[i] = string(sval)
87758775+ }
87768776+87778777+ }
87788778+ }
87798779+87808780+ default:
87818781+ // Field doesn't exist on this type, so ignore it
87828782+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
87838783+ return err
87848784+ }
87858785+ }
87868786+ }
87878787+87888788+ return nil
87898789+}
87908790+func (t *RepoDiscussionState) MarshalCBOR(w io.Writer) error {
87918791+ if t == nil {
87928792+ _, err := w.Write(cbg.CborNull)
87938793+ return err
87948794+ }
87958795+87968796+ cw := cbg.NewCborWriter(w)
87978797+87988798+ if _, err := cw.Write([]byte{164}); err != nil {
87998799+ return err
88008800+ }
88018801+88028802+ // t.LexiconTypeID (string) (string)
88038803+ if len("$type") > 1000000 {
88048804+ return xerrors.Errorf("Value in field \"$type\" was too long")
88058805+ }
88068806+88078807+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
88088808+ return err
88098809+ }
88108810+ if _, err := cw.WriteString(string("$type")); err != nil {
88118811+ return err
88128812+ }
88138813+88148814+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.discussion.state"))); err != nil {
88158815+ return err
88168816+ }
88178817+ if _, err := cw.WriteString(string("sh.tangled.repo.discussion.state")); err != nil {
88188818+ return err
88198819+ }
88208820+88218821+ // t.State (string) (string)
88228822+ if len("state") > 1000000 {
88238823+ return xerrors.Errorf("Value in field \"state\" was too long")
88248824+ }
88258825+88268826+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("state"))); err != nil {
88278827+ return err
88288828+ }
88298829+ if _, err := cw.WriteString(string("state")); err != nil {
88308830+ return err
88318831+ }
88328832+88338833+ if len(t.State) > 1000000 {
88348834+ return xerrors.Errorf("Value in field t.State was too long")
88358835+ }
88368836+88378837+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.State))); err != nil {
88388838+ return err
88398839+ }
88408840+ if _, err := cw.WriteString(string(t.State)); err != nil {
88418841+ return err
88428842+ }
88438843+88448844+ // t.CreatedAt (string) (string)
88458845+ if len("createdAt") > 1000000 {
88468846+ return xerrors.Errorf("Value in field \"createdAt\" was too long")
88478847+ }
88488848+88498849+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
88508850+ return err
88518851+ }
88528852+ if _, err := cw.WriteString(string("createdAt")); err != nil {
88538853+ return err
88548854+ }
88558855+88568856+ if len(t.CreatedAt) > 1000000 {
88578857+ return xerrors.Errorf("Value in field t.CreatedAt was too long")
88588858+ }
88598859+88608860+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
88618861+ return err
88628862+ }
88638863+ if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
88648864+ return err
88658865+ }
88668866+88678867+ // t.Discussion (string) (string)
88688868+ if len("discussion") > 1000000 {
88698869+ return xerrors.Errorf("Value in field \"discussion\" was too long")
88708870+ }
88718871+88728872+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("discussion"))); err != nil {
88738873+ return err
88748874+ }
88758875+ if _, err := cw.WriteString(string("discussion")); err != nil {
88768876+ return err
88778877+ }
88788878+88798879+ if len(t.Discussion) > 1000000 {
88808880+ return xerrors.Errorf("Value in field t.Discussion was too long")
88818881+ }
88828882+88838883+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Discussion))); err != nil {
88848884+ return err
88858885+ }
88868886+ if _, err := cw.WriteString(string(t.Discussion)); err != nil {
88878887+ return err
88888888+ }
88898889+ return nil
88908890+}
88918891+88928892+func (t *RepoDiscussionState) UnmarshalCBOR(r io.Reader) (err error) {
88938893+ *t = RepoDiscussionState{}
88948894+88958895+ cr := cbg.NewCborReader(r)
88968896+88978897+ maj, extra, err := cr.ReadHeader()
88988898+ if err != nil {
88998899+ return err
89008900+ }
89018901+ defer func() {
89028902+ if err == io.EOF {
89038903+ err = io.ErrUnexpectedEOF
89048904+ }
89058905+ }()
89068906+89078907+ if maj != cbg.MajMap {
89088908+ return fmt.Errorf("cbor input should be of type map")
89098909+ }
89108910+89118911+ if extra > cbg.MaxLength {
89128912+ return fmt.Errorf("RepoDiscussionState: map struct too large (%d)", extra)
89138913+ }
89148914+89158915+ n := extra
89168916+89178917+ nameBuf := make([]byte, 10)
89188918+ for i := uint64(0); i < n; i++ {
89198919+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
89208920+ if err != nil {
89218921+ return err
89228922+ }
89238923+89248924+ if !ok {
89258925+ // Field doesn't exist on this type, so ignore it
89268926+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
89278927+ return err
89288928+ }
89298929+ continue
89308930+ }
89318931+89328932+ switch string(nameBuf[:nameLen]) {
89338933+ // t.LexiconTypeID (string) (string)
89348934+ case "$type":
89358935+89368936+ {
89378937+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
89388938+ if err != nil {
89398939+ return err
89408940+ }
89418941+89428942+ t.LexiconTypeID = string(sval)
89438943+ }
89448944+ // t.State (string) (string)
89458945+ case "state":
89468946+89478947+ {
89488948+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
89498949+ if err != nil {
89508950+ return err
89518951+ }
89528952+89538953+ t.State = string(sval)
89548954+ }
89558955+ // t.CreatedAt (string) (string)
89568956+ case "createdAt":
89578957+89588958+ {
89598959+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
89608960+ if err != nil {
89618961+ return err
89628962+ }
89638963+89648964+ t.CreatedAt = string(sval)
89658965+ }
89668966+ // t.Discussion (string) (string)
89678967+ case "discussion":
89688968+89698969+ {
89708970+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
89718971+ if err != nil {
89728972+ return err
89738973+ }
89748974+89758975+ t.Discussion = string(sval)
74838976 }
7484897774858978 default:
+30
api/tangled/discussioncomment.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.discussion.comment
66+77+import (
88+ "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+const (
1212+ RepoDiscussionCommentNSID = "sh.tangled.repo.discussion.comment"
1313+)
1414+1515+func init() {
1616+ util.RegisterType("sh.tangled.repo.discussion.comment", &RepoDiscussionComment{})
1717+} //
1818+// RECORDTYPE: RepoDiscussionComment
1919+type RepoDiscussionComment struct {
2020+ LexiconTypeID string `json:"$type,const=sh.tangled.repo.discussion.comment" cborgen:"$type,const=sh.tangled.repo.discussion.comment"`
2121+ // body: Comment text
2222+ Body string `json:"body" cborgen:"body"`
2323+ CreatedAt string `json:"createdAt" cborgen:"createdAt"`
2424+ // discussion: The discussion this comment belongs to
2525+ Discussion string `json:"discussion" cborgen:"discussion"`
2626+ Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
2727+ References []string `json:"references,omitempty" cborgen:"references,omitempty"`
2828+ // replyTo: If this is a reply, the parent comment's at-uri
2929+ ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
3030+}
+26
api/tangled/discussionstate.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.discussion.state
66+77+import (
88+ "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+const (
1212+ RepoDiscussionStateNSID = "sh.tangled.repo.discussion.state"
1313+)
1414+1515+func init() {
1616+ util.RegisterType("sh.tangled.repo.discussion.state", &RepoDiscussionState{})
1717+} //
1818+// RECORDTYPE: RepoDiscussionState
1919+type RepoDiscussionState struct {
2020+ LexiconTypeID string `json:"$type,const=sh.tangled.repo.discussion.state" cborgen:"$type,const=sh.tangled.repo.discussion.state"`
2121+ CreatedAt string `json:"createdAt" cborgen:"createdAt"`
2222+ // discussion: The discussion this state change applies to
2323+ Discussion string `json:"discussion" cborgen:"discussion"`
2424+ // state: The new state of the discussion
2525+ State string `json:"state" cborgen:"state"`
2626+}
+81
api/tangled/pijulrefUpdate.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.pijul.refUpdate
66+77+import (
88+ "fmt"
99+ "io"
1010+1111+ cid "github.com/ipfs/go-cid"
1212+ cbg "github.com/whyrusleeping/cbor-gen"
1313+1414+ "github.com/bluesky-social/indigo/lex/util"
1515+)
1616+1717+const (
1818+ PijulRefUpdateNSID = "sh.tangled.pijul.refUpdate"
1919+)
2020+2121+func init() {
2222+ util.RegisterType("sh.tangled.pijul.refUpdate", &PijulRefUpdate{})
2323+} //
2424+// RECORDTYPE: PijulRefUpdate
2525+type PijulRefUpdate struct {
2626+ LexiconTypeID string `json:"$type,const=sh.tangled.pijul.refUpdate" cborgen:"$type,const=sh.tangled.pijul.refUpdate"`
2727+ // changes: Base32 change hashes pushed in this operation, in application order
2828+ Changes []string `json:"changes" cborgen:"changes"`
2929+ // channel: Channel that was updated
3030+ Channel string `json:"channel" cborgen:"channel"`
3131+ // committerDid: DID of the pusher
3232+ CommitterDid string `json:"committerDid" cborgen:"committerDid"`
3333+ // languages: Optional map of language name to line count
3434+ Languages *PijulRefUpdate_Languages `json:"languages,omitempty" cborgen:"languages,omitempty"`
3535+ // newState: Pijul Merkle state hash after push
3636+ NewState string `json:"newState" cborgen:"newState"`
3737+ // oldState: Pijul Merkle state hash before push (empty for new channels)
3838+ OldState *string `json:"oldState,omitempty" cborgen:"oldState,omitempty"`
3939+ // repo: AT URI of the repository
4040+ Repo string `json:"repo" cborgen:"repo"`
4141+}
4242+4343+// Map of language name to lines of code
4444+type PijulRefUpdate_Languages struct {
4545+}
4646+4747+func (t *PijulRefUpdate_Languages) MarshalCBOR(w io.Writer) error {
4848+ if t == nil {
4949+ _, err := w.Write(cbg.CborNull)
5050+ return err
5151+ }
5252+ cw := cbg.NewCborWriter(w)
5353+ if err := cw.WriteMajorTypeHeader(cbg.MajMap, 0); err != nil {
5454+ return err
5555+ }
5656+ return nil
5757+}
5858+5959+func (t *PijulRefUpdate_Languages) UnmarshalCBOR(r io.Reader) error {
6060+ *t = PijulRefUpdate_Languages{}
6161+ cr := cbg.NewCborReader(r)
6262+ maj, extra, err := cr.ReadHeader()
6363+ if err != nil {
6464+ return err
6565+ }
6666+ if maj != cbg.MajMap {
6767+ return fmt.Errorf("expected cbor map for PijulRefUpdate_Languages")
6868+ }
6969+ // skip all entries
7070+ for i := uint64(0); i < extra; i++ {
7171+ // skip key
7272+ if err := cbg.ScanForLinks(cr, func(_ cid.Cid) {}); err != nil {
7373+ return err
7474+ }
7575+ // skip value
7676+ if err := cbg.ScanForLinks(cr, func(_ cid.Cid) {}); err != nil {
7777+ return err
7878+ }
7979+ }
8080+ return nil
8181+}
+50
api/tangled/repoapplyChanges.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.applyChanges
66+77+import (
88+ "context"
99+1010+ "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+const (
1414+ RepoApplyChangesNSID = "sh.tangled.repo.applyChanges"
1515+)
1616+1717+// RepoApplyChanges_Input is the input argument to a sh.tangled.repo.applyChanges call.
1818+type RepoApplyChanges_Input struct {
1919+ // changes: List of change hashes to apply (in order)
2020+ Changes []string `json:"changes" cborgen:"changes"`
2121+ // channel: Target channel to apply changes to
2222+ Channel string `json:"channel" cborgen:"channel"`
2323+ // repo: Repository identifier in format 'did:plc:.../repoName'
2424+ Repo string `json:"repo" cborgen:"repo"`
2525+}
2626+2727+// RepoApplyChanges_Output is the output of a sh.tangled.repo.applyChanges call.
2828+type RepoApplyChanges_Output struct {
2929+ // applied: List of successfully applied change hashes
3030+ Applied []string `json:"applied" cborgen:"applied"`
3131+ // failed: List of changes that failed to apply
3232+ Failed []*RepoApplyChanges_Output_Failed_Elem `json:"failed,omitempty" cborgen:"failed,omitempty"`
3333+ // newState: Pijul channel Merkle state hash after applying changes. Empty string if unavailable.
3434+ NewState string `json:"newState,omitempty" cborgen:"newState,omitempty"`
3535+}
3636+3737+type RepoApplyChanges_Output_Failed_Elem struct {
3838+ Error string `json:"error" cborgen:"error"`
3939+ Hash string `json:"hash" cborgen:"hash"`
4040+}
4141+4242+// RepoApplyChanges calls the XRPC method "sh.tangled.repo.applyChanges".
4343+func RepoApplyChanges(ctx context.Context, c util.LexClient, input *RepoApplyChanges_Input) (*RepoApplyChanges_Output, error) {
4444+ var out RepoApplyChanges_Output
4545+ if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.applyChanges", nil, input, &out); err != nil {
4646+ return nil, err
4747+ }
4848+4949+ return &out, nil
5050+}
+54
api/tangled/repochangeGet.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.changeGet
66+77+import (
88+ "context"
99+1010+ "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+const (
1414+ RepoChangeGetNSID = "sh.tangled.repo.changeGet"
1515+)
1616+1717+// RepoChangeGet_Author is a "author" in the sh.tangled.repo.changeGet schema.
1818+type RepoChangeGet_Author struct {
1919+ Did *string `json:"did,omitempty" cborgen:"did,omitempty"`
2020+ Email *string `json:"email,omitempty" cborgen:"email,omitempty"`
2121+ Name string `json:"name" cborgen:"name"`
2222+}
2323+2424+// RepoChangeGet_Output is the output of a sh.tangled.repo.changeGet call.
2525+type RepoChangeGet_Output struct {
2626+ Authors []*RepoChangeGet_Author `json:"authors" cborgen:"authors"`
2727+ // dependencies: Hashes of changes this change depends on
2828+ Dependencies []string `json:"dependencies,omitempty" cborgen:"dependencies,omitempty"`
2929+ // diff: Raw diff content of the change
3030+ Diff *string `json:"diff,omitempty" cborgen:"diff,omitempty"`
3131+ // hash: Change hash (base32 encoded)
3232+ Hash string `json:"hash" cborgen:"hash"`
3333+ // message: Change description
3434+ Message string `json:"message" cborgen:"message"`
3535+ // timestamp: When the change was recorded
3636+ Timestamp *string `json:"timestamp,omitempty" cborgen:"timestamp,omitempty"`
3737+}
3838+3939+// RepoChangeGet calls the XRPC method "sh.tangled.repo.changeGet".
4040+//
4141+// hash: Change hash to retrieve
4242+// repo: Repository identifier in format 'did:plc:.../repoName'
4343+func RepoChangeGet(ctx context.Context, c util.LexClient, hash string, repo string) (*RepoChangeGet_Output, error) {
4444+ var out RepoChangeGet_Output
4545+4646+ params := map[string]interface{}{}
4747+ params["hash"] = hash
4848+ params["repo"] = repo
4949+ if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.changeGet", params, nil, &out); err != nil {
5050+ return nil, err
5151+ }
5252+5353+ return &out, nil
5454+}
+71
api/tangled/repochangeList.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.changeList
66+77+import (
88+ "context"
99+1010+ "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+const (
1414+ RepoChangeListNSID = "sh.tangled.repo.changeList"
1515+)
1616+1717+// RepoChangeList_Author is a "author" in the sh.tangled.repo.changeList schema.
1818+type RepoChangeList_Author struct {
1919+ Did *string `json:"did,omitempty" cborgen:"did,omitempty"`
2020+ Email *string `json:"email,omitempty" cborgen:"email,omitempty"`
2121+ Name string `json:"name" cborgen:"name"`
2222+}
2323+2424+// RepoChangeList_ChangeEntry is a "changeEntry" in the sh.tangled.repo.changeList schema.
2525+type RepoChangeList_ChangeEntry struct {
2626+ Authors []*RepoChangeList_Author `json:"authors" cborgen:"authors"`
2727+ // dependencies: Hashes of changes this change depends on
2828+ Dependencies []string `json:"dependencies,omitempty" cborgen:"dependencies,omitempty"`
2929+ // hash: Change hash (base32 encoded)
3030+ Hash string `json:"hash" cborgen:"hash"`
3131+ // message: Change description
3232+ Message string `json:"message" cborgen:"message"`
3333+ // timestamp: When the change was recorded
3434+ Timestamp *string `json:"timestamp,omitempty" cborgen:"timestamp,omitempty"`
3535+}
3636+3737+// RepoChangeList_Output is the output of a sh.tangled.repo.changeList call.
3838+type RepoChangeList_Output struct {
3939+ Changes []*RepoChangeList_ChangeEntry `json:"changes" cborgen:"changes"`
4040+ Channel *string `json:"channel,omitempty" cborgen:"channel,omitempty"`
4141+ Page int64 `json:"page" cborgen:"page"`
4242+ Per_page int64 `json:"per_page" cborgen:"per_page"`
4343+ Total int64 `json:"total" cborgen:"total"`
4444+}
4545+4646+// RepoChangeList calls the XRPC method "sh.tangled.repo.changeList".
4747+//
4848+// channel: Pijul channel name (defaults to main channel)
4949+// cursor: Pagination cursor (offset)
5050+// limit: Maximum number of changes to return
5151+// repo: Repository identifier in format 'did:plc:.../repoName'
5252+func RepoChangeList(ctx context.Context, c util.LexClient, channel string, cursor string, limit int64, repo string) (*RepoChangeList_Output, error) {
5353+ var out RepoChangeList_Output
5454+5555+ params := map[string]interface{}{}
5656+ if channel != "" {
5757+ params["channel"] = channel
5858+ }
5959+ if cursor != "" {
6060+ params["cursor"] = cursor
6161+ }
6262+ if limit != 0 {
6363+ params["limit"] = limit
6464+ }
6565+ params["repo"] = repo
6666+ if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.changeList", params, nil, &out); err != nil {
6767+ return nil, err
6868+ }
6969+7070+ return &out, nil
7171+}
+51
api/tangled/repochannelList.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package tangled
44+55+// schema: sh.tangled.repo.channelList
66+77+import (
88+ "context"
99+1010+ "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+const (
1414+ RepoChannelListNSID = "sh.tangled.repo.channelList"
1515+)
1616+1717+// RepoChannelList_Channel is a "channel" in the sh.tangled.repo.channelList schema.
1818+type RepoChannelList_Channel struct {
1919+ // is_current: Whether this is the currently active channel
2020+ Is_current *bool `json:"is_current,omitempty" cborgen:"is_current,omitempty"`
2121+ // name: Channel name
2222+ Name string `json:"name" cborgen:"name"`
2323+}
2424+2525+// RepoChannelList_Output is the output of a sh.tangled.repo.channelList call.
2626+type RepoChannelList_Output struct {
2727+ Channels []*RepoChannelList_Channel `json:"channels" cborgen:"channels"`
2828+}
2929+3030+// RepoChannelList calls the XRPC method "sh.tangled.repo.channelList".
3131+//
3232+// cursor: Pagination cursor (offset)
3333+// limit: Maximum number of channels to return
3434+// repo: Repository identifier in format 'did:plc:.../repoName'
3535+func RepoChannelList(ctx context.Context, c util.LexClient, cursor string, limit int64, repo string) (*RepoChannelList_Output, error) {
3636+ var out RepoChannelList_Output
3737+3838+ params := map[string]interface{}{}
3939+ if cursor != "" {
4040+ params["cursor"] = cursor
4141+ }
4242+ if limit != 0 {
4343+ params["limit"] = limit
4444+ }
4545+ params["repo"] = repo
4646+ if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.channelList", params, nil, &out); err != nil {
4747+ return nil, err
4848+ }
4949+5050+ return &out, nil
5151+}
+2
api/tangled/repocreate.go
···2626 Rkey string `json:"rkey" cborgen:"rkey"`
2727 // source: A source URL to clone from, populate this when forking or importing a repository.
2828 Source *string `json:"source,omitempty" cborgen:"source,omitempty"`
2929+ // vcs: Version control system to use for the repository (git or pijul).
3030+ Vcs *string `json:"vcs,omitempty" cborgen:"vcs,omitempty"`
2931}
30323133// RepoCreate_Output is the output of a sh.tangled.repo.create call.
···1010 {{ if gt (len .BranchesTrunc) 0 }}
1111 <div class="flex flex-col items-center">
1212 <p class="text-center pt-5 text-gray-400 dark:text-gray-500">
1313- This branch is empty. Other branches in this repository are populated:
1313+ This {{ if .RepoInfo.IsPijul }}channel{{ else }}branch{{ end }} is empty. Other {{ if .RepoInfo.IsPijul }}channels{{ else }}branches{{ end }} in this repository are populated:
1414 </p>
1515 <div class="mt-4 grid grid-cols-1 divide-y divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 w-full md:w-1/2">
1616 {{ range $br := .BranchesTrunc }}
···9797 <div class="col-span-1 md:col-span-2">
9898 <h2 class="text-sm pb-2 uppercase font-bold">Default Labels</h2>
9999 <p class="text-gray-500 dark:text-gray-400">
100100- Manage your issues and pulls by creating labels to categorize them. Only
100100+ Manage your {{ if .RepoInfo.IsPijul }}discussions{{ else }}issues and pulls{{ end }} by creating labels to categorize them. Only
101101 repository owners may configure labels. You may choose to subscribe to
102102 default labels, or create entirely custom labels.
103103 <p>
···240240 </div>
241241 {{ end }}
242242{{ end }}
243243-
···8080 id integer primary key autoincrement,
8181 name text unique
8282 );
8383+8484+ create table if not exists pijul_signing_keys (
8585+ public_key text primary key,
8686+ did text not null,
8787+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
8888+ foreign key (did) references known_dids(did) on delete cascade
8989+ );
8390 `)
8491 if err != nil {
8592 return nil, err
+54
knotserver/db/pijulkeys.go
···11+package db
22+33+import "strings"
44+55+// StorePijulSigningKey stores a verified pijul public key → DID mapping.
66+func (d *DB) StorePijulSigningKey(publicKey, did string) error {
77+ _, err := d.db.Exec(`
88+ insert or replace into pijul_signing_keys (public_key, did)
99+ values (?, ?)
1010+ `, publicKey, did)
1111+ return err
1212+}
1313+1414+// GetDidForPijulKey returns the DID associated with a pijul public key.
1515+func (d *DB) GetDidForPijulKey(publicKey string) (string, error) {
1616+ var did string
1717+ err := d.db.QueryRow(`
1818+ select did from pijul_signing_keys where public_key = ?
1919+ `, publicKey).Scan(&did)
2020+ return did, err
2121+}
2222+2323+// GetPijulKeyToDid returns a map of pijul public key → DID for the given keys.
2424+func (d *DB) GetPijulKeyToDid(keys []string) (map[string]string, error) {
2525+ if len(keys) == 0 {
2626+ return make(map[string]string), nil
2727+ }
2828+2929+ placeholders := make([]string, len(keys))
3030+ args := make([]any, len(keys))
3131+ for i, k := range keys {
3232+ placeholders[i] = "?"
3333+ args[i] = k
3434+ }
3535+3636+ rows, err := d.db.Query(`
3737+ select public_key, did from pijul_signing_keys
3838+ where public_key in (`+strings.Join(placeholders, ",")+`)
3939+ `, args...)
4040+ if err != nil {
4141+ return nil, err
4242+ }
4343+ defer rows.Close()
4444+4545+ result := make(map[string]string)
4646+ for rows.Next() {
4747+ var key, did string
4848+ if err := rows.Scan(&key, &did); err != nil {
4949+ return nil, err
5050+ }
5151+ result[key] = did
5252+ }
5353+ return result, rows.Err()
5454+}
+5
knotserver/git/git.go
···8080 return g.h
8181}
82828383+// Path returns the on-disk path of the repository.
8484+func (g *GitRepo) Path() string {
8585+ return g.path
8686+}
8787+8388// re-open a repository and update references
8489func (g *GitRepo) Refresh() error {
8590 refreshed, err := PlainOpen(g.path)
···11+package pijul
22+33+import (
44+ "fmt"
55+)
66+77+// Diff represents the difference between two states
88+type Diff struct {
99+ Raw string `json:"raw"`
1010+ Files []FileDiff `json:"files,omitempty"`
1111+ Stats *DiffStats `json:"stats,omitempty"`
1212+}
1313+1414+// FileDiff represents changes to a single file
1515+type FileDiff struct {
1616+ Path string `json:"path"`
1717+ OldPath string `json:"old_path,omitempty"` // for renames
1818+ Status string `json:"status"` // added, modified, deleted, renamed
1919+ Additions int `json:"additions"`
2020+ Deletions int `json:"deletions"`
2121+ Patch string `json:"patch,omitempty"`
2222+}
2323+2424+// DiffStats contains summary statistics for a diff
2525+type DiffStats struct {
2626+ FilesChanged int `json:"files_changed"`
2727+ Additions int `json:"additions"`
2828+ Deletions int `json:"deletions"`
2929+}
3030+3131+// Diff returns the diff of uncommitted changes
3232+func (p *PijulRepo) Diff() (*Diff, error) {
3333+ output, err := p.diff()
3434+ if err != nil {
3535+ return nil, fmt.Errorf("pijul diff: %w", err)
3636+ }
3737+3838+ return &Diff{
3939+ Raw: string(output),
4040+ }, nil
4141+}
4242+4343+// DiffChange returns the diff for a specific change
4444+func (p *PijulRepo) DiffChange(hash string) (*Diff, error) {
4545+ output, err := p.change(hash)
4646+ if err != nil {
4747+ return nil, fmt.Errorf("pijul change %s: %w", hash, err)
4848+ }
4949+5050+ return &Diff{
5151+ Raw: string(output),
5252+ }, nil
5353+}
5454+5555+// DiffBetween returns the diff between two channels or states
5656+func (p *PijulRepo) DiffBetween(from, to string) (*Diff, error) {
5757+ args := []string{}
5858+ if from != "" {
5959+ args = append(args, "--channel", from)
6060+ }
6161+ if to != "" {
6262+ args = append(args, "--channel", to)
6363+ }
6464+6565+ output, err := p.diff(args...)
6666+ if err != nil {
6767+ return nil, fmt.Errorf("pijul diff: %w", err)
6868+ }
6969+7070+ return &Diff{
7171+ Raw: string(output),
7272+ }, nil
7373+}
+306
knotserver/pijul/pijul.go
···11+package pijul
22+33+import (
44+ "archive/tar"
55+ "bytes"
66+ "errors"
77+ "fmt"
88+ "io"
99+ "io/fs"
1010+ "os"
1111+ "os/exec"
1212+ "path/filepath"
1313+1414+ securejoin "github.com/cyphar/filepath-securejoin"
1515+)
1616+1717+var (
1818+ ErrBinaryFile = errors.New("binary file")
1919+ ErrNotBinaryFile = errors.New("not binary file")
2020+ ErrNoPijulRepo = errors.New("not a pijul repository")
2121+ ErrChannelNotFound = errors.New("channel not found")
2222+ ErrChangeNotFound = errors.New("change not found")
2323+ ErrPathNotFound = errors.New("path not found")
2424+)
2525+2626+// PijulRepo represents a Pijul repository
2727+type PijulRepo struct {
2828+ path string
2929+ channelName string // current channel (empty means default)
3030+}
3131+3232+// Open opens a Pijul repository at the given path with optional channel
3333+func Open(path string, channel string) (*PijulRepo, error) {
3434+ // Verify it's a pijul repository
3535+ pijulDir := filepath.Join(path, ".pijul")
3636+ if _, err := os.Stat(pijulDir); os.IsNotExist(err) {
3737+ return nil, fmt.Errorf("%w: %s", ErrNoPijulRepo, path)
3838+ }
3939+4040+ p := &PijulRepo{
4141+ path: path,
4242+ channelName: channel,
4343+ }
4444+4545+ // Verify channel exists if specified
4646+ if channel != "" {
4747+ channels, err := p.Channels()
4848+ if err != nil {
4949+ return nil, fmt.Errorf("listing channels: %w", err)
5050+ }
5151+ found := false
5252+ for _, ch := range channels {
5353+ if ch.Name == channel {
5454+ found = true
5555+ break
5656+ }
5757+ }
5858+ if !found {
5959+ return nil, fmt.Errorf("%w: %s", ErrChannelNotFound, channel)
6060+ }
6161+ }
6262+6363+ return p, nil
6464+}
6565+6666+// PlainOpen opens a Pijul repository without setting a specific channel
6767+func PlainOpen(path string) (*PijulRepo, error) {
6868+ // Verify it's a pijul repository
6969+ pijulDir := filepath.Join(path, ".pijul")
7070+ if _, err := os.Stat(pijulDir); os.IsNotExist(err) {
7171+ return nil, fmt.Errorf("%w: %s", ErrNoPijulRepo, path)
7272+ }
7373+7474+ return &PijulRepo{path: path}, nil
7575+}
7676+7777+// Path returns the repository path
7878+func (p *PijulRepo) Path() string {
7979+ return p.path
8080+}
8181+8282+// CurrentChannel returns the current channel (or empty for default)
8383+func (p *PijulRepo) CurrentChannel() string {
8484+ return p.channelName
8585+}
8686+8787+// FindDefaultChannel returns the default channel name
8888+func (p *PijulRepo) FindDefaultChannel() (string, error) {
8989+ channels, err := p.Channels()
9090+ if err != nil {
9191+ return "", err
9292+ }
9393+9494+ // Look for 'main' first, then fall back to first channel
9595+ for _, ch := range channels {
9696+ if ch.Name == "main" {
9797+ return "main", nil
9898+ }
9999+ }
100100+101101+ if len(channels) > 0 {
102102+ return channels[0].Name, nil
103103+ }
104104+105105+ return "main", nil // default
106106+}
107107+108108+// SetDefaultChannel changes which channel is considered default
109109+// In Pijul, this would typically be done by renaming channels
110110+func (p *PijulRepo) SetDefaultChannel(channel string) error {
111111+ // Pijul doesn't have a built-in default branch concept like git HEAD
112112+ // This is typically managed at application level
113113+ // For now, just verify the channel exists
114114+ channels, err := p.Channels()
115115+ if err != nil {
116116+ return err
117117+ }
118118+119119+ for _, ch := range channels {
120120+ if ch.Name == channel {
121121+ return nil
122122+ }
123123+ }
124124+125125+ return fmt.Errorf("%w: %s", ErrChannelNotFound, channel)
126126+}
127127+128128+// FileContent reads a file from the working copy at a specific path
129129+// Note: Pijul doesn't have the concept of reading files at a specific revision
130130+// like git. We read from the working directory or need to use pijul credit.
131131+func (p *PijulRepo) FileContent(filePath string) ([]byte, error) {
132132+ fullPath, err := securejoin.SecureJoin(p.path, filePath)
133133+ if err != nil {
134134+ return nil, fmt.Errorf("invalid path: %w", err)
135135+ }
136136+137137+ content, err := os.ReadFile(fullPath)
138138+ if err != nil {
139139+ if os.IsNotExist(err) {
140140+ return nil, fmt.Errorf("%w: %s", ErrPathNotFound, filePath)
141141+ }
142142+ return nil, err
143143+ }
144144+145145+ return content, nil
146146+}
147147+148148+// FileContentN reads up to cap bytes of a file
149149+func (p *PijulRepo) FileContentN(filePath string, cap int64) ([]byte, error) {
150150+ fullPath, err := securejoin.SecureJoin(p.path, filePath)
151151+ if err != nil {
152152+ return nil, fmt.Errorf("invalid path: %w", err)
153153+ }
154154+155155+ f, err := os.Open(fullPath)
156156+ if err != nil {
157157+ if os.IsNotExist(err) {
158158+ return nil, fmt.Errorf("%w: %s", ErrPathNotFound, filePath)
159159+ }
160160+ return nil, err
161161+ }
162162+ defer f.Close()
163163+164164+ // Check if binary
165165+ buf := make([]byte, 512)
166166+ n, err := f.Read(buf)
167167+ if err != nil && err != io.EOF {
168168+ return nil, err
169169+ }
170170+ if isBinary(buf[:n]) {
171171+ return nil, ErrBinaryFile
172172+ }
173173+174174+ // Reset and read up to cap
175175+ if _, err := f.Seek(0, 0); err != nil {
176176+ return nil, err
177177+ }
178178+179179+ content := make([]byte, cap)
180180+ n, err = f.Read(content)
181181+ if err != nil && err != io.EOF {
182182+ return nil, err
183183+ }
184184+185185+ return content[:n], nil
186186+}
187187+188188+// RawContent reads raw file content without binary check
189189+func (p *PijulRepo) RawContent(filePath string) ([]byte, error) {
190190+ fullPath, err := securejoin.SecureJoin(p.path, filePath)
191191+ if err != nil {
192192+ return nil, fmt.Errorf("invalid path: %w", err)
193193+ }
194194+ return os.ReadFile(fullPath)
195195+}
196196+197197+// isBinary checks if data appears to be binary
198198+func isBinary(data []byte) bool {
199199+ for _, b := range data {
200200+ if b == 0 {
201201+ return true
202202+ }
203203+ }
204204+ return false
205205+}
206206+207207+// WriteTar writes the repository contents to a tar archive
208208+func (p *PijulRepo) WriteTar(w io.Writer, prefix string) error {
209209+ tw := tar.NewWriter(w)
210210+ defer tw.Close()
211211+212212+ return filepath.Walk(p.path, func(path string, info fs.FileInfo, err error) error {
213213+ if err != nil {
214214+ return err
215215+ }
216216+217217+ // Skip .pijul directory
218218+ if info.IsDir() && filepath.Base(path) == ".pijul" {
219219+ return filepath.SkipDir
220220+ }
221221+222222+ relPath, err := filepath.Rel(p.path, path)
223223+ if err != nil {
224224+ return err
225225+ }
226226+227227+ if relPath == "." {
228228+ return nil
229229+ }
230230+231231+ header, err := tar.FileInfoHeader(info, "")
232232+ if err != nil {
233233+ return err
234234+ }
235235+236236+ header.Name = filepath.Join(prefix, relPath)
237237+238238+ if err := tw.WriteHeader(header); err != nil {
239239+ return err
240240+ }
241241+242242+ if !info.IsDir() {
243243+ if err := copyFileToTar(tw, path); err != nil {
244244+ return err
245245+ }
246246+ }
247247+248248+ return nil
249249+ })
250250+}
251251+252252+// copyFileToTar copies a single file into a tar writer, closing the file before returning.
253253+func copyFileToTar(tw *tar.Writer, path string) error {
254254+ f, err := os.Open(path)
255255+ if err != nil {
256256+ return err
257257+ }
258258+ defer f.Close()
259259+ _, err = io.Copy(tw, f)
260260+ return err
261261+}
262262+263263+// InitRepo initializes a new Pijul repository
264264+func InitRepo(path string, bare bool) error {
265265+ if err := os.MkdirAll(path, 0755); err != nil {
266266+ return fmt.Errorf("creating directory: %w", err)
267267+ }
268268+269269+ args := []string{"init"}
270270+ if bare {
271271+ // Pijul doesn't have explicit bare repos like git
272272+ // A "bare" repo is typically just a repo without a working directory
273273+ args = append(args, "--kind=bare")
274274+ }
275275+276276+ cmd := exec.Command("pijul", args...)
277277+ cmd.Dir = path
278278+279279+ var stderr bytes.Buffer
280280+ cmd.Stderr = &stderr
281281+282282+ if err := cmd.Run(); err != nil {
283283+ return fmt.Errorf("pijul init: %w, stderr: %s", err, stderr.String())
284284+ }
285285+286286+ return nil
287287+}
288288+289289+// Clone clones a Pijul repository
290290+func Clone(url, destPath string, channel string) error {
291291+ args := []string{"clone", url, destPath}
292292+ if channel != "" {
293293+ args = append(args, "--channel", channel)
294294+ }
295295+296296+ cmd := exec.Command("pijul", args...)
297297+298298+ var stderr bytes.Buffer
299299+ cmd.Stderr = &stderr
300300+301301+ if err := cmd.Run(); err != nil {
302302+ return fmt.Errorf("pijul clone: %w, stderr: %s", err, stderr.String())
303303+ }
304304+305305+ return nil
306306+}
+131
knotserver/pijul/repo.go
···11+package pijul
22+33+import (
44+ "bytes"
55+ "fmt"
66+ "os"
77+ "os/exec"
88+)
99+1010+// InitBare initializes a bare Pijul repository
1111+func InitBare(repoPath string) error {
1212+ if err := os.MkdirAll(repoPath, 0755); err != nil {
1313+ return fmt.Errorf("creating directory: %w", err)
1414+ }
1515+1616+ cmd := exec.Command("pijul", "init")
1717+ cmd.Dir = repoPath
1818+1919+ var stderr bytes.Buffer
2020+ cmd.Stderr = &stderr
2121+2222+ if err := cmd.Run(); err != nil {
2323+ return fmt.Errorf("pijul init: %w, stderr: %s", err, stderr.String())
2424+ }
2525+2626+ // remove .ignore auto-generated by pijul init
2727+ os.Remove(fmt.Sprintf("%s/.ignore", repoPath))
2828+2929+ return nil
3030+}
3131+3232+// Fork clones a repository to a new location
3333+func Fork(srcPath, destPath string) error {
3434+ // For local fork, we can use pijul clone
3535+ cmd := exec.Command("pijul", "clone", srcPath, destPath)
3636+3737+ var stderr bytes.Buffer
3838+ cmd.Stderr = &stderr
3939+4040+ if err := cmd.Run(); err != nil {
4141+ return fmt.Errorf("pijul clone: %w, stderr: %s", err, stderr.String())
4242+ }
4343+4444+ return nil
4545+}
4646+4747+// Push pushes changes to a remote
4848+func (p *PijulRepo) Push(remote string, channel string) error {
4949+ args := []string{remote}
5050+ if channel != "" {
5151+ args = append(args, "--channel", channel)
5252+ }
5353+5454+ _, err := p.runPijulCmd("push", args...)
5555+ return err
5656+}
5757+5858+// Pull pulls changes from a remote
5959+func (p *PijulRepo) Pull(remote string, channel string) error {
6060+ args := []string{remote}
6161+ if channel != "" {
6262+ args = append(args, "--channel", channel)
6363+ }
6464+6565+ _, err := p.runPijulCmd("pull", args...)
6666+ return err
6767+}
6868+6969+// Apply applies a change to the repository
7070+func (p *PijulRepo) Apply(changeHash string) error {
7171+ args := []string{changeHash}
7272+ if p.channelName != "" {
7373+ args = append(args, "--channel", p.channelName)
7474+ }
7575+ _, err := p.runPijulCmd("apply", args...)
7676+ return err
7777+}
7878+7979+// Unrecord removes a change from the channel and resets the working copy
8080+// to match (like git reset --hard).
8181+func (p *PijulRepo) Unrecord(changeHash string) error {
8282+ args := []string{changeHash}
8383+ if p.channelName != "" {
8484+ args = append(args, "--channel", p.channelName)
8585+ }
8686+ if _, err := p.runPijulCmd("unrecord", args...); err != nil {
8787+ return err
8888+ }
8989+9090+ // Reset working copy to match the channel state
9191+ resetArgs := []string{}
9292+ if p.channelName != "" {
9393+ resetArgs = append(resetArgs, "--channel", p.channelName)
9494+ }
9595+ _, err := p.runPijulCmd("reset", resetArgs...)
9696+ return err
9797+}
9898+9999+// Record creates a new change (like git commit)
100100+func (p *PijulRepo) Record(message string, authors []Author) error {
101101+ args := []string{"-m", message}
102102+103103+ for _, author := range authors {
104104+ authorStr := author.Name
105105+ if author.Email != "" {
106106+ authorStr = fmt.Sprintf("%s <%s>", author.Name, author.Email)
107107+ }
108108+ args = append(args, "--author", authorStr)
109109+ }
110110+111111+ if p.channelName != "" {
112112+ args = append(args, "--channel", p.channelName)
113113+ }
114114+115115+ _, err := p.runPijulCmd("record", args...)
116116+ return err
117117+}
118118+119119+// Add adds files to be tracked
120120+func (p *PijulRepo) Add(paths ...string) error {
121121+ args := append([]string{}, paths...)
122122+ _, err := p.runPijulCmd("add", args...)
123123+ return err
124124+}
125125+126126+// Remove removes files from tracking
127127+func (p *PijulRepo) Remove(paths ...string) error {
128128+ args := append([]string{}, paths...)
129129+ _, err := p.runPijulCmd("remove", args...)
130130+ return err
131131+}
+197
knotserver/pijul/tree.go
···11+package pijul
22+33+import (
44+ "context"
55+ "fmt"
66+ "io/fs"
77+ "os"
88+ "path"
99+ "path/filepath"
1010+ "strings"
1111+1212+ securejoin "github.com/cyphar/filepath-securejoin"
1313+ "tangled.org/core/types"
1414+)
1515+1616+// TreeEntry represents a file or directory in the repository tree
1717+type TreeEntry struct {
1818+ Name string `json:"name"`
1919+ Mode fs.FileMode `json:"mode"`
2020+ Size int64 `json:"size"`
2121+ IsDir bool `json:"is_dir"`
2222+}
2323+2424+// FileTree returns the file tree at the given path
2525+// For Pijul, we read directly from the working directory
2626+func (p *PijulRepo) FileTree(ctx context.Context, treePath string) ([]types.NiceTree, error) {
2727+ fullPath, err := securejoin.SecureJoin(p.path, treePath)
2828+ if err != nil {
2929+ return nil, fmt.Errorf("invalid path: %w", err)
3030+ }
3131+3232+ info, err := os.Stat(fullPath)
3333+ if err != nil {
3434+ if os.IsNotExist(err) {
3535+ return nil, ErrPathNotFound
3636+ }
3737+ return nil, err
3838+ }
3939+4040+ // If it's a file, return empty (no tree for files)
4141+ if !info.IsDir() {
4242+ return []types.NiceTree{}, nil
4343+ }
4444+4545+ entries, err := os.ReadDir(fullPath)
4646+ if err != nil {
4747+ return nil, err
4848+ }
4949+5050+ trees := make([]types.NiceTree, 0, len(entries))
5151+5252+ for _, entry := range entries {
5353+ // Skip .pijul directory
5454+ if entry.Name() == ".pijul" {
5555+ continue
5656+ }
5757+5858+ info, err := entry.Info()
5959+ if err != nil {
6060+ continue
6161+ }
6262+6363+ trees = append(trees, types.NiceTree{
6464+ Name: entry.Name(),
6565+ Mode: fileModeToString(info.Mode()),
6666+ Size: info.Size(),
6767+ // LastCommit would require additional work to implement
6868+ // For now, we leave it nil
6969+ })
7070+ }
7171+7272+ return trees, nil
7373+}
7474+7575+// fileModeToString converts fs.FileMode to octal string representation
7676+func fileModeToString(mode fs.FileMode) string {
7777+ // Convert to git-style mode representation
7878+ if mode.IsDir() {
7979+ return "040000"
8080+ }
8181+ if mode&fs.ModeSymlink != 0 {
8282+ return "120000"
8383+ }
8484+ if mode&0111 != 0 {
8585+ return "100755"
8686+ }
8787+ return "100644"
8888+}
8989+9090+// Walk callback type
9191+type WalkCallback func(path string, info fs.FileInfo, isDir bool) error
9292+9393+// Walk traverses the file tree
9494+func (p *PijulRepo) Walk(ctx context.Context, root string, cb WalkCallback) error {
9595+ startPath, err := securejoin.SecureJoin(p.path, root)
9696+ if err != nil {
9797+ return fmt.Errorf("invalid path: %w", err)
9898+ }
9999+100100+ return filepath.WalkDir(startPath, func(walkPath string, d fs.DirEntry, err error) error {
101101+ if err != nil {
102102+ return err
103103+ }
104104+105105+ // Check context
106106+ select {
107107+ case <-ctx.Done():
108108+ return ctx.Err()
109109+ default:
110110+ }
111111+112112+ // Skip .pijul directory
113113+ if d.IsDir() && filepath.Base(walkPath) == ".pijul" {
114114+ return filepath.SkipDir
115115+ }
116116+117117+ // Get relative path
118118+ relPath, err := filepath.Rel(p.path, walkPath)
119119+ if err != nil {
120120+ return err
121121+ }
122122+123123+ if relPath == "." {
124124+ return nil
125125+ }
126126+127127+ info, err := d.Info()
128128+ if err != nil {
129129+ return err
130130+ }
131131+132132+ return cb(relPath, info, d.IsDir())
133133+ })
134134+}
135135+136136+// ListFiles returns all tracked files in the repository
137137+func (p *PijulRepo) ListFiles() ([]string, error) {
138138+ output, err := p.runPijulCmd("ls")
139139+ if err != nil {
140140+ return nil, err
141141+ }
142142+143143+ lines := strings.Split(strings.TrimSpace(string(output)), "\n")
144144+ if len(lines) == 1 && lines[0] == "" {
145145+ return []string{}, nil
146146+ }
147147+148148+ return lines, nil
149149+}
150150+151151+// IsTracked checks if a file is tracked by Pijul
152152+func (p *PijulRepo) IsTracked(filePath string) (bool, error) {
153153+ files, err := p.ListFiles()
154154+ if err != nil {
155155+ return false, err
156156+ }
157157+158158+ for _, f := range files {
159159+ if f == filePath {
160160+ return true, nil
161161+ }
162162+ }
163163+164164+ return false, nil
165165+}
166166+167167+// FileExists checks if a file exists in the working directory
168168+func (p *PijulRepo) FileExists(filePath string) bool {
169169+ fullPath, err := securejoin.SecureJoin(p.path, filePath)
170170+ if err != nil {
171171+ return false
172172+ }
173173+ _, err = os.Stat(fullPath)
174174+ return err == nil
175175+}
176176+177177+// IsDir checks if a path is a directory
178178+func (p *PijulRepo) IsDir(treePath string) (bool, error) {
179179+ fullPath, err := securejoin.SecureJoin(p.path, treePath)
180180+ if err != nil {
181181+ return false, fmt.Errorf("invalid path: %w", err)
182182+ }
183183+ info, err := os.Stat(fullPath)
184184+ if err != nil {
185185+ return false, err
186186+ }
187187+ return info.IsDir(), nil
188188+}
189189+190190+// MakeNiceTree creates a NiceTree from file info
191191+func MakeNiceTree(name string, info fs.FileInfo) types.NiceTree {
192192+ return types.NiceTree{
193193+ Name: path.Base(name),
194194+ Mode: fileModeToString(info.Mode()),
195195+ Size: info.Size(),
196196+ }
197197+}
+349
knotserver/pijul_http.go
···11+package knotserver
22+33+import (
44+ "crypto/sha256"
55+ "encoding/base64"
66+ "encoding/json"
77+ "fmt"
88+ "io"
99+ "net/http"
1010+ "os"
1111+ "path/filepath"
1212+ "strconv"
1313+ "strings"
1414+1515+ "github.com/go-chi/chi/v5"
1616+ "tangled.org/core/api/tangled"
1717+ "tangled.org/core/knotserver/db"
1818+ "tangled.org/core/knotserver/pijul"
1919+ "tangled.org/core/rbac"
2020+)
2121+2222+// PijulHttpGet handles GET /{did}/{name}/.pijul — the pijul HTTP remote read protocol.
2323+// Dispatches on query string: ?id, ?state, ?changelist, ?change, ?tag.
2424+func (h *Knot) PijulHttpGet(w http.ResponseWriter, r *http.Request) {
2525+ repoPath, ok := repoPathFromcontext(r.Context())
2626+ if !ok {
2727+ http.Error(w, "repository not found", http.StatusNotFound)
2828+ return
2929+ }
3030+3131+ q := r.URL.Query()
3232+ switch {
3333+ case q.Has("id"):
3434+ h.pijulGetId(w, repoPath, q.Get("channel"))
3535+ case q.Has("state"):
3636+ h.pijulGetState(w, r, repoPath, q.Get("channel"), q.Get("state"))
3737+ case q.Has("changelist"):
3838+ h.pijulGetChangelist(w, r, repoPath, q.Get("channel"), q.Get("changelist"))
3939+ case q.Has("change"):
4040+ h.pijulGetChange(w, r, repoPath, q.Get("change"))
4141+ case q.Has("tag"):
4242+ h.pijulGetTag(w, r, repoPath, q.Get("tag"))
4343+ default:
4444+ http.NotFound(w, r)
4545+ }
4646+}
4747+4848+// PijulHttpPost handles POST /{did}/{name}/.pijul — the pijul HTTP remote write protocol.
4949+// Dispatches on query string: ?apply (upload + apply a change), ?tagup (upload a tag).
5050+func (h *Knot) PijulHttpPost(w http.ResponseWriter, r *http.Request) {
5151+ repoPath, ok := repoPathFromcontext(r.Context())
5252+ if !ok {
5353+ http.Error(w, "repository not found", http.StatusNotFound)
5454+ return
5555+ }
5656+5757+ q := r.URL.Query()
5858+ if hash := q.Get("apply"); hash != "" {
5959+ h.pijulApplyChange(w, r, repoPath, hash, q.Get("to_channel"))
6060+ } else if hash := q.Get("tagup"); hash != "" {
6161+ h.pijulTagUp(w, r, repoPath, hash, q.Get("to_channel"))
6262+ } else {
6363+ http.Error(w, "unknown pijul operation", http.StatusBadRequest)
6464+ }
6565+}
6666+6767+// pijulGetId returns a stable 16-byte remote identifier for the given repo+channel.
6868+// The pijul client uses this to detect remote identity changes and invalidate its cache.
6969+func (h *Knot) pijulGetId(w http.ResponseWriter, repoPath, channel string) {
7070+ sum := sha256.Sum256([]byte(repoPath + ":" + channel))
7171+ w.Header().Set("Content-Type", "application/octet-stream")
7272+ w.Write(sum[:16])
7373+}
7474+7575+// pijulGetState returns the Merkle state at position posStr.
7676+// Response format: "{pos} {merkle1} {merkle2}\n" (space-separated).
7777+// When posStr is empty, returns the latest (newest) state.
7878+func (h *Knot) pijulGetState(w http.ResponseWriter, r *http.Request, repoPath, channel, posStr string) {
7979+ pr, err := pijul.Open(repoPath, channel)
8080+ if err != nil {
8181+ http.Error(w, "failed to open repository", http.StatusInternalServerError)
8282+ return
8383+ }
8484+8585+ entries, err := pr.LogWithState(channel)
8686+ if err != nil {
8787+ h.l.Error("pijul log --state failed", "err", err)
8888+ http.Error(w, "failed to read change log", http.StatusInternalServerError)
8989+ return
9090+ }
9191+9292+ if len(entries) == 0 {
9393+ // Empty channel: no state yet.
9494+ http.Error(w, "no changes in channel", http.StatusNotFound)
9595+ return
9696+ }
9797+9898+ var pos uint64
9999+ if posStr == "" {
100100+ pos = entries[len(entries)-1].Pos // latest
101101+ } else {
102102+ pos, err = strconv.ParseUint(posStr, 10, 64)
103103+ if err != nil {
104104+ http.Error(w, "invalid position", http.StatusBadRequest)
105105+ return
106106+ }
107107+ if pos >= uint64(len(entries)) {
108108+ pos = uint64(len(entries) - 1)
109109+ }
110110+ }
111111+112112+ entry := entries[pos]
113113+ // Return "{pos} {merkle} {merkle}\n"
114114+ // We use the same Merkle for both channel state and tag state (no tag support yet).
115115+ fmt.Fprintf(w, "%d %s %s\n", entry.Pos, entry.State, entry.State)
116116+}
117117+118118+// pijulGetChangelist returns all changes from position fromStr onwards (oldest-first).
119119+// Response format: one line per change: "{pos}.{hash}.{merkle}\n", terminated by an empty line.
120120+func (h *Knot) pijulGetChangelist(w http.ResponseWriter, r *http.Request, repoPath, channel, fromStr string) {
121121+ pr, err := pijul.Open(repoPath, channel)
122122+ if err != nil {
123123+ http.Error(w, "failed to open repository", http.StatusInternalServerError)
124124+ return
125125+ }
126126+127127+ entries, err := pr.LogWithState(channel)
128128+ if err != nil {
129129+ h.l.Error("pijul log --state failed", "err", err)
130130+ http.Error(w, "failed to read change log", http.StatusInternalServerError)
131131+ return
132132+ }
133133+134134+ var from uint64
135135+ if fromStr != "" {
136136+ from, err = strconv.ParseUint(fromStr, 10, 64)
137137+ if err != nil {
138138+ http.Error(w, "invalid from position", http.StatusBadRequest)
139139+ return
140140+ }
141141+ }
142142+143143+ w.Header().Set("Content-Type", "text/plain")
144144+ for _, e := range entries {
145145+ if e.Pos < from {
146146+ continue
147147+ }
148148+ fmt.Fprintf(w, "%d.%s.%s\n", e.Pos, e.Hash, e.State)
149149+ }
150150+ // Empty line signals end of changelist.
151151+ fmt.Fprintln(w)
152152+}
153153+154154+// pijulGetChange serves a single change file as raw bytes.
155155+// The change is read from .pijul/changes/{hash[:2]}/{hash[2:]}.change
156156+func (h *Knot) pijulGetChange(w http.ResponseWriter, r *http.Request, repoPath, hash string) {
157157+ if len(hash) < 3 {
158158+ http.Error(w, "invalid change hash", http.StatusBadRequest)
159159+ return
160160+ }
161161+ changePath := filepath.Join(repoPath, ".pijul", "changes", hash[:2], hash[2:]+".change")
162162+ data, err := os.ReadFile(changePath)
163163+ if err != nil {
164164+ if os.IsNotExist(err) {
165165+ http.Error(w, "change not found", http.StatusNotFound)
166166+ } else {
167167+ http.Error(w, "failed to read change", http.StatusInternalServerError)
168168+ }
169169+ return
170170+ }
171171+ w.Header().Set("Content-Type", "application/octet-stream")
172172+ w.Header().Set("Content-Length", strconv.Itoa(len(data)))
173173+ w.Write(data)
174174+}
175175+176176+// pijulGetTag serves a tag file (for tagged states). We don't support tags yet;
177177+// return 404 so the client falls back gracefully.
178178+func (h *Knot) pijulGetTag(w http.ResponseWriter, r *http.Request, repoPath, hash string) {
179179+ http.Error(w, "tags not supported", http.StatusNotFound)
180180+}
181181+182182+// pijulApplyChange handles POST ?.apply=HASH&to_channel=CHANNEL.
183183+// It saves the uploaded change file, applies it, and emits an SSE event.
184184+func (h *Knot) pijulApplyChange(w http.ResponseWriter, r *http.Request, repoPath, hash, channel string) {
185185+ // 1. Authenticate the pusher.
186186+ pusherDid, err := parseBearerDID(r)
187187+ if err != nil {
188188+ http.Error(w, "unauthorized: "+err.Error(), http.StatusForbidden)
189189+ return
190190+ }
191191+192192+ // 2. RBAC: verify push permission.
193193+ ownerDid := chi.URLParam(r, "did")
194194+ repoName := chi.URLParam(r, "name")
195195+ repoDid, dbErr := h.db.GetRepoDid(ownerDid, repoName)
196196+ if dbErr != nil {
197197+ repoDid = ownerDid + "/" + repoName
198198+ }
199199+ ok, rbacErr := h.e.IsPushAllowed(pusherDid, rbac.ThisServer, repoDid)
200200+ if rbacErr != nil || !ok {
201201+ http.Error(w, "forbidden", http.StatusForbidden)
202202+ return
203203+ }
204204+205205+ // 3. Save the change file to disk.
206206+ if len(hash) < 3 {
207207+ http.Error(w, "invalid change hash", http.StatusBadRequest)
208208+ return
209209+ }
210210+ changeDir := filepath.Join(repoPath, ".pijul", "changes", hash[:2])
211211+ if err := os.MkdirAll(changeDir, 0755); err != nil {
212212+ http.Error(w, "failed to create change directory", http.StatusInternalServerError)
213213+ return
214214+ }
215215+ changePath := filepath.Join(changeDir, hash[2:]+".change")
216216+217217+ body, err := io.ReadAll(r.Body)
218218+ if err != nil {
219219+ http.Error(w, "failed to read request body", http.StatusBadRequest)
220220+ return
221221+ }
222222+ if err := os.WriteFile(changePath, body, 0644); err != nil {
223223+ http.Error(w, "failed to write change file", http.StatusInternalServerError)
224224+ return
225225+ }
226226+227227+ // 4. Apply the change to the channel.
228228+ if channel == "" {
229229+ channel = "main"
230230+ }
231231+ pr, err := pijul.Open(repoPath, channel)
232232+ if err != nil {
233233+ os.Remove(changePath)
234234+ http.Error(w, "failed to open repository", http.StatusInternalServerError)
235235+ return
236236+ }
237237+ if err := pr.Apply(hash); err != nil {
238238+ os.Remove(changePath)
239239+ h.l.Error("pijul apply failed", "hash", hash, "channel", channel, "err", err)
240240+ http.Error(w, "failed to apply change: "+err.Error(), http.StatusInternalServerError)
241241+ return
242242+ }
243243+244244+ // 5. Get channel state and emit SSE event (best-effort).
245245+ newState, _ := pr.ChannelState(channel)
246246+ go h.emitPijulRefUpdate(pusherDid, ownerDid, repoName, repoDid, channel, hash, newState)
247247+248248+ w.WriteHeader(http.StatusOK)
249249+}
250250+251251+// pijulTagUp handles POST ?tagup=HASH&to_channel=CHANNEL.
252252+// Tags (state snapshots) are saved but not broadcast — they're used for pinning.
253253+func (h *Knot) pijulTagUp(w http.ResponseWriter, r *http.Request, repoPath, hash, channel string) {
254254+ if _, err := parseBearerDID(r); err != nil {
255255+ http.Error(w, "unauthorized", http.StatusForbidden)
256256+ return
257257+ }
258258+259259+ if len(hash) < 3 {
260260+ http.Error(w, "invalid tag hash", http.StatusBadRequest)
261261+ return
262262+ }
263263+ tagDir := filepath.Join(repoPath, ".pijul", "tags", hash[:2])
264264+ if err := os.MkdirAll(tagDir, 0755); err != nil {
265265+ http.Error(w, "failed to create tag directory", http.StatusInternalServerError)
266266+ return
267267+ }
268268+ tagPath := filepath.Join(tagDir, hash[2:]+".tag")
269269+ body, err := io.ReadAll(r.Body)
270270+ if err != nil {
271271+ http.Error(w, "failed to read request body", http.StatusBadRequest)
272272+ return
273273+ }
274274+ if err := os.WriteFile(tagPath, body, 0644); err != nil {
275275+ http.Error(w, "failed to write tag file", http.StatusInternalServerError)
276276+ return
277277+ }
278278+ w.WriteHeader(http.StatusOK)
279279+}
280280+281281+// emitPijulRefUpdate inserts a PijulRefUpdate event into the knotserver event log,
282282+// which is streamed to the appview via the /events WebSocket.
283283+func (h *Knot) emitPijulRefUpdate(committerDid, ownerDid, repoName, repoDid, channel, changeHash, newState string) {
284284+ repoAt := fmt.Sprintf("at://%s/%s/%s", repoDid, tangled.RepoNSID, repoName)
285285+286286+ record := tangled.PijulRefUpdate{
287287+ Repo: repoAt,
288288+ Channel: channel,
289289+ Changes: []string{changeHash},
290290+ CommitterDid: committerDid,
291291+ NewState: newState,
292292+ }
293293+294294+ eventJson, err := json.Marshal(record)
295295+ if err != nil {
296296+ h.l.Error("failed to marshal pijulRefUpdate event", "err", err)
297297+ return
298298+ }
299299+300300+ event := db.Event{
301301+ Rkey: TID(),
302302+ Nsid: tangled.PijulRefUpdateNSID,
303303+ EventJson: string(eventJson),
304304+ }
305305+306306+ if err := h.db.InsertEvent(event, h.n); err != nil {
307307+ h.l.Error("failed to insert pijulRefUpdate event", "err", err)
308308+ }
309309+}
310310+311311+// parseBearerDID extracts the DID from an AT Protocol Bearer JWT in the
312312+// Authorization header. It decodes the JWT payload without verifying the
313313+// signature — full verification is a future TODO.
314314+func parseBearerDID(r *http.Request) (string, error) {
315315+ auth := strings.TrimSpace(r.Header.Get("Authorization"))
316316+ token := strings.TrimPrefix(auth, "Bearer ")
317317+ if token == "" || token == auth {
318318+ return "", fmt.Errorf("missing Bearer token")
319319+ }
320320+321321+ parts := strings.Split(token, ".")
322322+ if len(parts) != 3 {
323323+ return "", fmt.Errorf("malformed JWT")
324324+ }
325325+326326+ payload, err := base64.RawURLEncoding.DecodeString(parts[1])
327327+ if err != nil {
328328+ return "", fmt.Errorf("invalid JWT payload encoding")
329329+ }
330330+331331+ var claims struct {
332332+ Sub string `json:"sub"`
333333+ Iss string `json:"iss"`
334334+ }
335335+ if err := json.Unmarshal(payload, &claims); err != nil {
336336+ return "", fmt.Errorf("invalid JWT payload JSON")
337337+ }
338338+339339+ // AT Protocol user JWTs use "sub" for the DID.
340340+ did := claims.Sub
341341+ if did == "" {
342342+ did = claims.Iss
343343+ }
344344+ if did == "" || !strings.HasPrefix(did, "did:") {
345345+ return "", fmt.Errorf("JWT does not contain a valid DID")
346346+ }
347347+348348+ return did, nil
349349+}
+59-6
knotserver/router.go
···55 "fmt"
66 "log/slog"
77 "net/http"
88+ "path/filepath"
89 "strings"
9101111+ securejoin "github.com/cyphar/filepath-securejoin"
1012 "github.com/go-chi/chi/v5"
1113 "tangled.org/core/idresolver"
1214 "tangled.org/core/jetstream"
···81838284 r.Route("/{did}", func(r chi.Router) {
8385 r.Use(h.resolveDidRedirect)
8484-8585- r.Get("/info/refs", h.InfoRefs)
8686- r.Post("/git-upload-archive", h.UploadArchive)
8787- r.Post("/git-upload-pack", h.UploadPack)
8888- r.Post("/git-receive-pack", h.ReceivePack)
8989-9086 r.Route("/{name}", func(r chi.Router) {
8787+ r.Use(h.resolveRepo)
8888+ // routes for git operations
9189 r.Get("/info/refs", h.InfoRefs)
9290 r.Post("/git-upload-archive", h.UploadArchive)
9391 r.Post("/git-upload-pack", h.UploadPack)
9492 r.Post("/git-receive-pack", h.ReceivePack)
9393+ // pijul HTTP remote protocol
9494+ r.Get("/.pijul", h.PijulHttpGet)
9595+ r.Post("/.pijul", h.PijulHttpPost)
9596 })
9697 })
9798···148149 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect)
149150 })
150151}
152152+153153+type ctxRepoPathKey struct{}
154154+155155+func repoPathFromcontext(ctx context.Context) (string, bool) {
156156+ v, ok := ctx.Value(ctxRepoPathKey{}).(string)
157157+ return v, ok
158158+}
159159+160160+// resolveRepo is a http middleware that constructs repo path from given did & name pair.
161161+// It first tries to resolve via the database (repo DID lookup), then falls back to
162162+// the legacy path layout (scanPath/ownerDid/name).
163163+func (h *Knot) resolveRepo(next http.Handler) http.Handler {
164164+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165165+ did := chi.URLParam(r, "did")
166166+ name := chi.URLParam(r, "name")
167167+168168+ // Try database resolution first (repo DID based path)
169169+ repoDid, err := h.db.GetRepoDid(did, name)
170170+ if err == nil {
171171+ repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid)
172172+ if resolveErr == nil {
173173+ ctx := context.WithValue(r.Context(), ctxRepoPathKey{}, repoPath)
174174+ next.ServeHTTP(w, r.WithContext(ctx))
175175+ return
176176+ }
177177+ }
178178+179179+ // Fall back to legacy path layout: scanPath/ownerDid/name
180180+ repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name))
181181+ if err != nil {
182182+ w.WriteHeader(http.StatusNotFound)
183183+ w.Write([]byte("Repository not found"))
184184+ return
185185+ }
186186+187187+ exist, err := isDir(repoPath)
188188+ if err != nil {
189189+ w.WriteHeader(http.StatusInternalServerError)
190190+ w.Write([]byte("Failed to check repository path"))
191191+ return
192192+ }
193193+ if !exist {
194194+ w.WriteHeader(http.StatusNotFound)
195195+ w.Write([]byte("Repository not found"))
196196+ return
197197+ }
198198+199199+ ctx := context.WithValue(r.Context(), ctxRepoPathKey{}, repoPath)
200200+ next.ServeHTTP(w, r.WithContext(ctx))
201201+ })
202202+}
203203+151204152205func (h *Knot) configureOwner() error {
153206 cfgOwner := h.c.Server.Owner
+75
knotserver/vcs/backend.go
···11+package vcs
22+33+import (
44+ "context"
55+ "errors"
66+ "io"
77+88+ "tangled.org/core/knotserver/git"
99+ "tangled.org/core/knotserver/pijul"
1010+)
1111+1212+// ErrBinaryFile is returned by FileContentN when the file appears to be binary.
1313+// It matches both git.ErrBinaryFile and pijul.ErrBinaryFile.
1414+var ErrBinaryFile = errors.New("binary file")
1515+1616+// IsBinaryFileError returns true if the error indicates a binary file,
1717+// matching against both the vcs, git, and pijul error types.
1818+func IsBinaryFileError(err error) bool {
1919+ return errors.Is(err, ErrBinaryFile) ||
2020+ errors.Is(err, git.ErrBinaryFile) ||
2121+ errors.Is(err, pijul.ErrBinaryFile)
2222+}
2323+2424+// ReadRepo provides read-only access to a VCS repository, abstracting
2525+// over git and pijul.
2626+type ReadRepo interface {
2727+ // VCSType returns "git" or "pijul".
2828+ VCSType() string
2929+3030+ // Path returns the on-disk path of the repository.
3131+ Path() string
3232+3333+ // History returns history entries (commits/changes) with pagination.
3434+ History(offset, limit int) ([]HistoryEntry, error)
3535+3636+ // TotalHistoryEntries returns the total count of commits/changes.
3737+ TotalHistoryEntries() (int, error)
3838+3939+ // HistoryEntry returns a single commit/change by hash.
4040+ HistoryEntry(hash string) (*HistoryEntry, error)
4141+4242+ // Branches returns branches/channels with optional pagination.
4343+ Branches(opts *PaginationOpts) ([]BranchInfo, error)
4444+4545+ // DefaultBranch returns the name of the default branch/channel.
4646+ DefaultBranch() (string, error)
4747+4848+ // FileTree returns the directory listing at the given path.
4949+ FileTree(ctx context.Context, path string) ([]TreeEntry, error)
5050+5151+ // FileContentN reads up to cap bytes of a file, returning ErrBinaryFile
5252+ // if the file appears to be binary.
5353+ FileContentN(path string, cap int64) ([]byte, error)
5454+5555+ // RawContent reads the full raw content of a file.
5656+ RawContent(path string) ([]byte, error)
5757+5858+ // WriteTar writes a tar archive of the repository to w, prefixing
5959+ // all paths with prefix.
6060+ WriteTar(w io.Writer, prefix string) error
6161+6262+ // Tags returns tags with optional pagination. Pijul repos return nil.
6363+ Tags(opts *PaginationOpts) ([]TagInfo, error)
6464+}
6565+6666+// MutableRepo extends ReadRepo with write operations.
6767+type MutableRepo interface {
6868+ ReadRepo
6969+7070+ // SetDefaultBranch sets the default branch/channel.
7171+ SetDefaultBranch(name string) error
7272+7373+ // DeleteBranch deletes a branch/channel.
7474+ DeleteBranch(name string) error
7575+}
+40
knotserver/vcs/detect.go
···11+package vcs
22+33+import (
44+ "fmt"
55+ "os"
66+ "path/filepath"
77+)
88+99+// IsPijulRepo checks if the given path contains a Pijul repository.
1010+func IsPijulRepo(path string) bool {
1111+ info, err := os.Stat(filepath.Join(path, ".pijul"))
1212+ if err != nil {
1313+ return false
1414+ }
1515+ return info.IsDir()
1616+}
1717+1818+// IsGitRepo checks if the given path contains a Git repository.
1919+func IsGitRepo(path string) bool {
2020+ info, err := os.Stat(filepath.Join(path, ".git"))
2121+ if err != nil {
2222+ // Also check for bare git repos (HEAD file at top level).
2323+ if _, err := os.Stat(filepath.Join(path, "HEAD")); err == nil {
2424+ return true
2525+ }
2626+ return false
2727+ }
2828+ return info.IsDir()
2929+}
3030+3131+// DetectVCS detects whether a path contains a Git or Pijul repository.
3232+func DetectVCS(path string) (string, error) {
3333+ if IsPijulRepo(path) {
3434+ return "pijul", nil
3535+ }
3636+ if IsGitRepo(path) {
3737+ return "git", nil
3838+ }
3939+ return "", fmt.Errorf("no VCS repository found at %s", path)
4040+}
···3434 "type": "string",
3535 "format": "did",
3636 "description": "Optional user-provided did:web to use as the repo identity instead of minting a did:plc."
3737+ },
3838+ "vcs": {
3939+ "type": "string",
4040+ "description": "Version control system to use for the repository (git or pijul)."
3741 }
3842 }
3943 }