forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

Compare changes

Choose any two refs to compare.

Changed files
+3847 -1138
api
appview
crypto
jetstream
knotserver
lexicons
nix
orm
patchutil
sets
spindle
engines
nixery
types
+649 -8
api/tangled/cbor_gen.go
···
cw := cbg.NewCborWriter(w)
-
fieldCount := 5
+
fieldCount := 7
if t.Body == nil {
+
fieldCount--
+
}
+
+
if t.Mentions == nil {
+
fieldCount--
+
}
+
+
if t.References == nil {
fieldCount--
···
return err
+
// t.Mentions ([]string) (slice)
+
if t.Mentions != nil {
+
+
if len("mentions") > 1000000 {
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("mentions")); err != nil {
+
return err
+
}
+
+
if len(t.Mentions) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
+
return err
+
}
+
for _, v := range t.Mentions {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
+
// t.CreatedAt (string) (string)
if len("createdAt") > 1000000 {
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
return err
+
+
// t.References ([]string) (slice)
+
if t.References != nil {
+
+
if len("references") > 1000000 {
+
return xerrors.Errorf("Value in field \"references\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("references")); err != nil {
+
return err
+
}
+
+
if len(t.References) > 8192 {
+
return xerrors.Errorf("Slice value in field t.References was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
+
return err
+
}
+
for _, v := range t.References {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
return nil
···
n := extra
-
nameBuf := make([]byte, 9)
+
nameBuf := make([]byte, 10)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
t.Title = string(sval)
+
}
+
// t.Mentions ([]string) (slice)
+
case "mentions":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Mentions = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Mentions[i] = string(sval)
+
}
+
+
}
// t.CreatedAt (string) (string)
case "createdAt":
···
t.CreatedAt = string(sval)
+
// t.References ([]string) (slice)
+
case "references":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.References: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.References = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.References[i] = string(sval)
+
}
+
+
}
+
}
default:
// Field doesn't exist on this type, so ignore it
···
cw := cbg.NewCborWriter(w)
-
fieldCount := 5
+
fieldCount := 7
+
+
if t.Mentions == nil {
+
fieldCount--
+
}
+
+
if t.References == nil {
+
fieldCount--
+
}
if t.ReplyTo == nil {
fieldCount--
···
+
// t.Mentions ([]string) (slice)
+
if t.Mentions != nil {
+
+
if len("mentions") > 1000000 {
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("mentions")); err != nil {
+
return err
+
}
+
+
if len(t.Mentions) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
+
return err
+
}
+
for _, v := range t.Mentions {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
+
// t.CreatedAt (string) (string)
if len("createdAt") > 1000000 {
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
return err
+
+
// t.References ([]string) (slice)
+
if t.References != nil {
+
+
if len("references") > 1000000 {
+
return xerrors.Errorf("Value in field \"references\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("references")); err != nil {
+
return err
+
}
+
+
if len(t.References) > 8192 {
+
return xerrors.Errorf("Slice value in field t.References was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
+
return err
+
}
+
for _, v := range t.References {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
return nil
···
n := extra
-
nameBuf := make([]byte, 9)
+
nameBuf := make([]byte, 10)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
t.ReplyTo = (*string)(&sval)
+
}
+
}
+
// t.Mentions ([]string) (slice)
+
case "mentions":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Mentions = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Mentions[i] = string(sval)
+
}
+
// t.CreatedAt (string) (string)
···
t.CreatedAt = string(sval)
+
}
+
// t.References ([]string) (slice)
+
case "references":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.References: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.References = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.References[i] = string(sval)
+
}
+
+
}
default:
···
cw := cbg.NewCborWriter(w)
-
fieldCount := 7
+
fieldCount := 9
if t.Body == nil {
+
fieldCount--
+
}
+
+
if t.Mentions == nil {
+
fieldCount--
+
}
+
+
if t.References == nil {
fieldCount--
···
return err
+
// t.Mentions ([]string) (slice)
+
if t.Mentions != nil {
+
+
if len("mentions") > 1000000 {
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("mentions")); err != nil {
+
return err
+
}
+
+
if len(t.Mentions) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
+
return err
+
}
+
for _, v := range t.Mentions {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
+
// t.CreatedAt (string) (string)
if len("createdAt") > 1000000 {
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
return err
+
+
// t.References ([]string) (slice)
+
if t.References != nil {
+
+
if len("references") > 1000000 {
+
return xerrors.Errorf("Value in field \"references\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("references")); err != nil {
+
return err
+
}
+
+
if len(t.References) > 8192 {
+
return xerrors.Errorf("Slice value in field t.References was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
+
return err
+
}
+
for _, v := range t.References {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
return nil
···
n := extra
-
nameBuf := make([]byte, 9)
+
nameBuf := make([]byte, 10)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
+
}
+
// t.Mentions ([]string) (slice)
+
case "mentions":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Mentions = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Mentions[i] = string(sval)
+
}
+
+
}
// t.CreatedAt (string) (string)
case "createdAt":
···
t.CreatedAt = string(sval)
+
// t.References ([]string) (slice)
+
case "references":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.References: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.References = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.References[i] = string(sval)
+
}
+
+
}
+
}
default:
// Field doesn't exist on this type, so ignore it
···
cw := cbg.NewCborWriter(w)
+
fieldCount := 6
-
if _, err := cw.Write([]byte{164}); err != nil {
+
if t.Mentions == nil {
+
fieldCount--
+
}
+
+
if t.References == nil {
+
fieldCount--
+
}
+
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
···
return err
+
// t.Mentions ([]string) (slice)
+
if t.Mentions != nil {
+
+
if len("mentions") > 1000000 {
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("mentions")); err != nil {
+
return err
+
}
+
+
if len(t.Mentions) > 8192 {
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
+
return err
+
}
+
for _, v := range t.Mentions {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
+
}
+
// t.CreatedAt (string) (string)
if len("createdAt") > 1000000 {
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
return err
+
}
+
+
// t.References ([]string) (slice)
+
if t.References != nil {
+
+
if len("references") > 1000000 {
+
return xerrors.Errorf("Value in field \"references\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("references")); err != nil {
+
return err
+
}
+
+
if len(t.References) > 8192 {
+
return xerrors.Errorf("Slice value in field t.References was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
+
return err
+
}
+
for _, v := range t.References {
+
if len(v) > 1000000 {
+
return xerrors.Errorf("Value in field v was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(v)); err != nil {
+
return err
+
}
+
+
}
return nil
···
n := extra
-
nameBuf := make([]byte, 9)
+
nameBuf := make([]byte, 10)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
···
t.LexiconTypeID = string(sval)
+
// t.Mentions ([]string) (slice)
+
case "mentions":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.Mentions = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Mentions[i] = string(sval)
+
}
+
+
}
+
}
// t.CreatedAt (string) (string)
case "createdAt":
···
t.CreatedAt = string(sval)
+
}
+
// t.References ([]string) (slice)
+
case "references":
+
+
maj, extra, err = cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
+
if extra > 8192 {
+
return fmt.Errorf("t.References: array too large (%d)", extra)
+
}
+
+
if maj != cbg.MajArray {
+
return fmt.Errorf("expected cbor array")
+
}
+
+
if extra > 0 {
+
t.References = make([]string, extra)
+
}
+
+
for i := 0; i < int(extra); i++ {
+
{
+
var maj byte
+
var extra uint64
+
var err error
+
_ = maj
+
_ = extra
+
_ = err
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.References[i] = string(sval)
+
}
+
+
}
default:
+7 -5
api/tangled/issuecomment.go
···
} //
// RECORDTYPE: RepoIssueComment
type RepoIssueComment struct {
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
-
Body string `json:"body" cborgen:"body"`
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
-
Issue string `json:"issue" cborgen:"issue"`
-
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
+
Body string `json:"body" cborgen:"body"`
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+
Issue string `json:"issue" cborgen:"issue"`
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
+
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
}
+6 -4
api/tangled/pullcomment.go
···
} //
// RECORDTYPE: RepoPullComment
type RepoPullComment struct {
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
-
Body string `json:"body" cborgen:"body"`
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
-
Pull string `json:"pull" cborgen:"pull"`
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
+
Body string `json:"body" cborgen:"body"`
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
+
Pull string `json:"pull" cborgen:"pull"`
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
}
+7 -5
api/tangled/repoissue.go
···
} //
// RECORDTYPE: RepoIssue
type RepoIssue struct {
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
-
Repo string `json:"repo" cborgen:"repo"`
-
Title string `json:"title" cborgen:"title"`
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
+
Repo string `json:"repo" cborgen:"repo"`
+
Title string `json:"title" cborgen:"title"`
}
+2
api/tangled/repopull.go
···
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
Patch string `json:"patch" cborgen:"patch"`
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
Target *RepoPull_Target `json:"target" cborgen:"target"`
Title string `json:"title" cborgen:"title"`
+6 -45
appview/commitverify/verify.go
···
import (
"log"
-
"github.com/go-git/go-git/v5/plumbing/object"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/crypto"
···
return ""
}
-
func GetVerifiedObjectCommits(e db.Execer, emailToDid map[string]string, commits []*object.Commit) (VerifiedCommits, error) {
-
ndCommits := []types.NiceDiff{}
-
for _, commit := range commits {
-
ndCommits = append(ndCommits, ObjectCommitToNiceDiff(commit))
-
}
-
return GetVerifiedCommits(e, emailToDid, ndCommits)
-
}
-
-
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.NiceDiff) (VerifiedCommits, error) {
+
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) {
vcs := VerifiedCommits{}
didPubkeyCache := make(map[string][]models.PublicKey)
for _, commit := range ndCommits {
-
c := commit.Commit
-
-
committerEmail := c.Committer.Email
+
committerEmail := commit.Committer.Email
if did, exists := emailToDid[committerEmail]; exists {
// check if we've already fetched public keys for this did
pubKeys, ok := didPubkeyCache[did]
···
}
// try to verify with any associated pubkeys
+
payload := commit.Payload()
+
signature := commit.PGPSignature
for _, pk := range pubKeys {
-
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
+
if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok {
fp, err := crypto.SSHFingerprint(pk.Key)
if err != nil {
log.Println("error computing ssh fingerprint:", err)
}
-
vc := verifiedCommit{fingerprint: fp, hash: c.This}
+
vc := verifiedCommit{fingerprint: fp, hash: commit.This}
vcs[vc] = struct{}{}
break
}
···
return vcs, nil
}
-
-
// ObjectCommitToNiceDiff is a compatibility function to convert a
-
// commit object into a NiceDiff structure.
-
func ObjectCommitToNiceDiff(c *object.Commit) types.NiceDiff {
-
var niceDiff types.NiceDiff
-
-
// set commit information
-
niceDiff.Commit.Message = c.Message
-
niceDiff.Commit.Author = c.Author
-
niceDiff.Commit.This = c.Hash.String()
-
niceDiff.Commit.Committer = c.Committer
-
niceDiff.Commit.Tree = c.TreeHash.String()
-
niceDiff.Commit.PGPSignature = c.PGPSignature
-
-
changeId, ok := c.ExtraHeaders["change-id"]
-
if ok {
-
niceDiff.Commit.ChangedId = string(changeId)
-
}
-
-
// set parent hash if available
-
if len(c.ParentHashes) > 0 {
-
niceDiff.Commit.Parent = c.ParentHashes[0].String()
-
}
-
-
// XXX: Stats and Diff fields are typically populated
-
// after fetching the actual diff information, which isn't
-
// directly available in the commit object itself.
-
-
return niceDiff
-
}
+3 -2
appview/db/artifact.go
···
"github.com/go-git/go-git/v5/plumbing"
"github.com/ipfs/go-cid"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func AddArtifact(e Execer, artifact models.Artifact) error {
···
return err
}
-
func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) {
+
func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) {
var artifacts []models.Artifact
var conditions []string
···
return artifacts, nil
}
-
func DeleteArtifact(e Execer, filters ...filter) error {
+
func DeleteArtifact(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+4 -3
appview/db/collaborators.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func AddCollaborator(e Execer, c models.Collaborator) error {
···
return err
}
-
func DeleteCollaborator(e Execer, filters ...filter) error {
+
func DeleteCollaborator(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return nil, nil
}
-
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
+
return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
}
-
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
+
func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) {
var collaborators []models.Collaborator
var conditions []string
var args []any
+33 -137
appview/db/db.go
···
import (
"context"
"database/sql"
-
"fmt"
"log/slog"
-
"reflect"
"strings"
_ "github.com/mattn/go-sqlite3"
"tangled.org/core/log"
+
"tangled.org/core/orm"
)
type DB struct {
···
email_notifications integer not null default 0
);
+
create table if not exists reference_links (
+
id integer primary key autoincrement,
+
from_at text not null,
+
to_at text not null,
+
unique (from_at, to_at)
+
);
+
create table if not exists migrations (
id integer primary key autoincrement,
name text unique
···
-- indexes for better performance
create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
+
create index if not exists idx_references_from_at on reference_links(from_at);
+
create index if not exists idx_references_to_at on reference_links(to_at);
`)
if err != nil {
return nil, err
}
// run migrations
-
runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
tx.Exec(`
alter table repos add column description text check (length(description) <= 200);
`)
return nil
})
-
runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
// add unconstrained column
_, err := tx.Exec(`
alter table public_keys
···
return nil
})
-
runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table comments drop column comment_at;
alter table comments add column rkey text;
···
return err
})
-
runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table comments add column deleted text; -- timestamp
alter table comments add column edited text; -- timestamp
···
return err
})
-
runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table pulls add column source_branch text;
alter table pulls add column source_repo_at text;
···
return err
})
-
runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table repos add column source text;
`)
···
//
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
conn.ExecContext(ctx, "pragma foreign_keys = off;")
-
runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table pulls_new (
-- identifiers
···
})
conn.ExecContext(ctx, "pragma foreign_keys = on;")
-
runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
tx.Exec(`
alter table repos add column spindle text;
`)
···
// drop all knot secrets, add unique constraint to knots
//
// knots will henceforth use service auth for signed requests
-
runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table registrations_new (
id integer primary key autoincrement,
···
})
// recreate and add rkey + created columns with default constraint
-
runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
// create new table
// - repo_at instead of repo integer
// - rkey field
···
return err
})
-
runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table issues add column rkey text not null default '';
···
})
// repurpose the read-only column to "needs-upgrade"
-
runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table registrations rename column read_only to needs_upgrade;
`)
···
})
// require all knots to upgrade after the release of total xrpc
-
runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
_, err := tx.Exec(`
update registrations set needs_upgrade = 1;
`)
···
})
// require all knots to upgrade after the release of total xrpc
-
runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table spindles add column needs_upgrade integer not null default 0;
`)
···
//
// disable foreign-keys for the next migration
conn.ExecContext(ctx, "pragma foreign_keys = off;")
-
runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists issues_new (
-- identifiers
···
// - new columns
// * column "reply_to" which can be any other comment
// * column "at-uri" which is a generated column
-
runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists issue_comments (
-- identifiers
···
//
// disable foreign-keys for the next migration
conn.ExecContext(ctx, "pragma foreign_keys = off;")
-
runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists pulls_new (
-- identifiers
···
//
// disable foreign-keys for the next migration
conn.ExecContext(ctx, "pragma foreign_keys = off;")
-
runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists pull_submissions_new (
-- identifiers
···
// knots may report the combined patch for a comparison, we can store that on the appview side
// (but not on the pds record), because calculating the combined patch requires a git index
-
runMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table pull_submissions add column combined text;
`)
return err
})
-
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table profile add column pronouns text;
`)
return err
})
-
runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table repos add column website text;
alter table repos add column topics text;
···
return err
})
-
runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table notification_preferences add column user_mentioned integer not null default 1;
`)
···
})
// remove the foreign key constraints from stars.
-
runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
+
orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table stars_new (
id integer primary key autoincrement,
···
}, nil
-
type migrationFn = func(*sql.Tx) error
-
-
func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
-
logger = logger.With("migration", name)
-
-
tx, err := c.BeginTx(context.Background(), nil)
-
if err != nil {
-
return err
-
}
-
defer tx.Rollback()
-
-
var exists bool
-
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
-
if err != nil {
-
return err
-
}
-
-
if !exists {
-
// run migration
-
err = migrationFn(tx)
-
if err != nil {
-
logger.Error("failed to run migration", "err", err)
-
return err
-
}
-
-
// mark migration as complete
-
_, err = tx.Exec("insert into migrations (name) values (?)", name)
-
if err != nil {
-
logger.Error("failed to mark migration as complete", "err", err)
-
return err
-
}
-
-
// commit the transaction
-
if err := tx.Commit(); err != nil {
-
return err
-
}
-
-
logger.Info("migration applied successfully")
-
} else {
-
logger.Warn("skipped migration, already applied")
-
}
-
-
return nil
-
}
-
func (d *DB) Close() error {
return d.DB.Close()
-
-
type filter struct {
-
key string
-
arg any
-
cmp string
-
}
-
-
func newFilter(key, cmp string, arg any) filter {
-
return filter{
-
key: key,
-
arg: arg,
-
cmp: cmp,
-
}
-
}
-
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
-
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
-
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
-
func FilterContains(key string, arg any) filter {
-
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
-
}
-
-
func (f filter) Condition() string {
-
rv := reflect.ValueOf(f.arg)
-
kind := rv.Kind()
-
-
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
-
if rv.Len() == 0 {
-
// always false
-
return "1 = 0"
-
}
-
-
placeholders := make([]string, rv.Len())
-
for i := range placeholders {
-
placeholders[i] = "?"
-
}
-
-
return fmt.Sprintf("%s %s (%s)", f.key, f.cmp, strings.Join(placeholders, ", "))
-
}
-
-
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
-
}
-
-
func (f filter) Arg() []any {
-
rv := reflect.ValueOf(f.arg)
-
kind := rv.Kind()
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
-
if rv.Len() == 0 {
-
return nil
-
}
-
-
out := make([]any, rv.Len())
-
for i := range rv.Len() {
-
out[i] = rv.Index(i).Interface()
-
}
-
return out
-
}
-
-
return []any{f.arg}
-
}
+4 -3
appview/db/follow.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func AddFollow(e Execer, follow *models.Follow) error {
···
return result, nil
}
-
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
+
func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
var follows []models.Follow
var conditions []string
···
}
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
-
return GetFollows(e, 0, FilterEq("subject_did", did))
+
return GetFollows(e, 0, orm.FilterEq("subject_did", did))
}
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
-
return GetFollows(e, 0, FilterEq("user_did", did))
+
return GetFollows(e, 0, orm.FilterEq("user_did", did))
}
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+92 -36
appview/db/issues.go
···
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.org/core/api/tangled"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
+
"tangled.org/core/orm"
)
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
issues, err := GetIssues(
tx,
-
FilterEq("did", issue.Did),
-
FilterEq("rkey", issue.Rkey),
+
orm.FilterEq("did", issue.Did),
+
orm.FilterEq("rkey", issue.Rkey),
)
switch {
case err != nil:
···
returning rowid, issue_id
`, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body)
-
return row.Scan(&issue.Id, &issue.IssueId)
+
err = row.Scan(&issue.Id, &issue.IssueId)
+
if err != nil {
+
return fmt.Errorf("scan row: %w", err)
+
}
+
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
+
return fmt.Errorf("put reference_links: %w", err)
+
}
+
return nil
}
func updateIssue(tx *sql.Tx, issue *models.Issue) error {
···
set title = ?, body = ?, edited = ?
where did = ? and rkey = ?
`, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey)
-
return err
+
if err != nil {
+
return err
+
}
+
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
+
return fmt.Errorf("put reference_links: %w", err)
+
}
+
return nil
}
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
issueMap := make(map[string]*models.Issue) // at-uri -> issue
var conditions []string
···
whereClause = " where " + strings.Join(conditions, " and ")
}
-
pLower := FilterGte("row_num", page.Offset+1)
-
pUpper := FilterLte("row_num", page.Offset+page.Limit)
+
pLower := orm.FilterGte("row_num", page.Offset+1)
+
pUpper := orm.FilterLte("row_num", page.Offset+page.Limit)
pageClause := ""
if page.Limit > 0 {
···
repoAts = append(repoAts, string(issue.RepoAt))
}
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
if err != nil {
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
}
···
// collect comments
issueAts := slices.Collect(maps.Keys(issueMap))
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
if err != nil {
return nil, fmt.Errorf("failed to query comments: %w", err)
}
···
}
// collect allLabels for each issue
-
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts))
if err != nil {
return nil, fmt.Errorf("failed to query labels: %w", err)
}
for issueAt, labels := range allLabels {
if issue, ok := issueMap[issueAt.String()]; ok {
issue.Labels = labels
+
}
+
}
+
+
// collect references for each issue
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts))
+
if err != nil {
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
+
}
+
for issueAt, references := range allReferencs {
+
if issue, ok := issueMap[issueAt.String()]; ok {
+
issue.References = references
}
}
···
issues, err := GetIssuesPaginated(
e,
pagination.Page{},
-
FilterEq("repo_at", repoAt),
-
FilterEq("issue_id", issueId),
+
orm.FilterEq("repo_at", repoAt),
+
orm.FilterEq("issue_id", issueId),
)
if err != nil {
return nil, err
···
return &issues[0], nil
}
-
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
+
func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) {
return GetIssuesPaginated(e, pagination.Page{}, filters...)
}
···
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
var ids []int64
-
var filters []filter
+
var filters []orm.Filter
openValue := 0
if opts.IsOpen {
openValue = 1
}
-
filters = append(filters, FilterEq("open", openValue))
+
filters = append(filters, orm.FilterEq("open", openValue))
if opts.RepoAt != "" {
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
}
var conditions []string
···
return ids, nil
}
-
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
-
result, err := e.Exec(
+
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
+
result, err := tx.Exec(
`insert into issue_comments (
did,
rkey,
···
return 0, err
}
+
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
+
return 0, fmt.Errorf("put reference_links: %w", err)
+
}
+
return id, nil
}
-
func DeleteIssueComments(e Execer, filters ...filter) error {
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
-
var comments []models.IssueComment
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
+
commentMap := make(map[string]*models.IssueComment)
var conditions []string
var args []any
···
comment.ReplyTo = &replyTo.V
}
-
comments = append(comments, comment)
+
atUri := comment.AtUri().String()
+
commentMap[atUri] = &comment
}
if err = rows.Err(); err != nil {
return nil, err
}
+
// collect references for each comments
+
commentAts := slices.Collect(maps.Keys(commentMap))
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
+
if err != nil {
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
+
}
+
for commentAt, references := range allReferencs {
+
if comment, ok := commentMap[commentAt.String()]; ok {
+
comment.References = references
+
}
+
}
+
+
var comments []models.IssueComment
+
for _, c := range commentMap {
+
comments = append(comments, *c)
+
}
+
+
sort.Slice(comments, func(i, j int) bool {
+
return comments[i].Created.After(comments[j].Created)
+
})
+
return comments, nil
}
-
func DeleteIssues(e Execer, filters ...filter) error {
-
var conditions []string
-
var args []any
-
for _, filter := range filters {
-
conditions = append(conditions, filter.Condition())
-
args = append(args, filter.Arg()...)
+
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+
_, err := tx.Exec(
+
`delete from issues
+
where did = ? and rkey = ?`,
+
did,
+
rkey,
+
)
+
if err != nil {
+
return fmt.Errorf("delete issue: %w", err)
}
-
whereClause := ""
-
if conditions != nil {
-
whereClause = " where " + strings.Join(conditions, " and ")
+
uri := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey))
+
err = deleteReferences(tx, uri)
+
if err != nil {
+
return fmt.Errorf("delete reference_links: %w", err)
}
-
query := fmt.Sprintf(`delete from issues %s`, whereClause)
-
_, err := e.Exec(query, args...)
-
return err
+
return nil
}
-
func CloseIssues(e Execer, filters ...filter) error {
+
func CloseIssues(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func ReopenIssues(e Execer, filters ...filter) error {
+
func ReopenIssues(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+8 -7
appview/db/label.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
// no updating type for now
···
return id, nil
}
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
+
func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
+
func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) {
var labelDefinitions []models.LabelDefinition
var conditions []string
var args []any
···
}
// helper to get exactly one label def
-
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
+
func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) {
labels, err := GetLabelDefinitions(e, filters...)
if err != nil {
return nil, err
···
return id, nil
}
-
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
+
func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) {
var labelOps []models.LabelOp
var conditions []string
var args []any
···
}
// get labels for a given list of subject URIs
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
+
func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) {
ops, err := GetLabelOps(e, filters...)
if err != nil {
return nil, err
···
}
labelAts := slices.Collect(maps.Keys(labelAtSet))
-
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
+
actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts))
if err != nil {
return nil, err
}
···
return results, nil
}
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
+
func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) {
labels, err := GetLabelDefinitions(e, filters...)
if err != nil {
return nil, err
+5 -4
appview/db/language.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
-
func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) {
+
func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return nil
}
-
func DeleteRepoLanguages(e Execer, filters ...filter) error {
+
func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
err := DeleteRepoLanguages(
tx,
-
FilterEq("repo_at", repoAt),
-
FilterEq("ref", ref),
+
orm.FilterEq("repo_at", repoAt),
+
orm.FilterEq("ref", ref),
)
if err != nil {
return fmt.Errorf("failed to delete existing languages: %w", err)
+14 -13
appview/db/notifications.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
+
"tangled.org/core/orm"
)
func CreateNotification(e Execer, notification *models.Notification) error {
···
}
// GetNotificationsPaginated retrieves notifications with filters and pagination
-
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) {
var conditions []string
var args []any
···
}
// GetNotificationsWithEntities retrieves notifications with their related entities
-
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) {
var conditions []string
var args []any
···
}
// GetNotifications retrieves notifications with filters
-
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
+
func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) {
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
}
-
func CountNotifications(e Execer, filters ...filter) (int64, error) {
+
func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
}
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
-
idFilter := FilterEq("id", notificationID)
-
recipientFilter := FilterEq("recipient_did", userDID)
+
idFilter := orm.FilterEq("id", notificationID)
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
query := fmt.Sprintf(`
UPDATE notifications
···
}
func MarkAllNotificationsRead(e Execer, userDID string) error {
-
recipientFilter := FilterEq("recipient_did", userDID)
-
readFilter := FilterEq("read", 0)
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
+
readFilter := orm.FilterEq("read", 0)
query := fmt.Sprintf(`
UPDATE notifications
···
}
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
-
idFilter := FilterEq("id", notificationID)
-
recipientFilter := FilterEq("recipient_did", userDID)
+
idFilter := orm.FilterEq("id", notificationID)
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
query := fmt.Sprintf(`
DELETE FROM notifications
···
}
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
-
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
+
prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid))
if err != nil {
return nil, err
}
···
return p, nil
}
-
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
+
func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) {
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
var conditions []string
···
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
cutoff := time.Now().Add(-olderThan)
-
createdFilter := FilterLte("created", cutoff)
+
createdFilter := orm.FilterLte("created", cutoff)
query := fmt.Sprintf(`
DELETE FROM notifications
+6 -5
appview/db/pipeline.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
-
func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
+
func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
var pipelines []models.Pipeline
var conditions []string
···
// this is a mega query, but the most useful one:
// get N pipelines, for each one get the latest status of its N workflows
-
func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
+
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
var conditions []string
var args []any
for _, filter := range filters {
-
filter.key = "p." + filter.key // the table is aliased in the query to `p`
+
filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
conditions = append(conditions, filter.Condition())
args = append(args, filter.Arg()...)
}
···
conditions = nil
args = nil
for _, p := range pipelines {
-
knotFilter := FilterEq("pipeline_knot", p.Knot)
-
rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
+
knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
+
rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
args = append(args, p.Knot)
args = append(args, p.Rkey)
+6 -5
appview/db/profile.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
const TimeframeMonths = 7
···
issues, err := GetIssues(
e,
-
FilterEq("did", forDid),
-
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
+
orm.FilterEq("did", forDid),
+
orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
)
if err != nil {
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
*items = append(*items, &issue)
}
-
repos, err := GetRepos(e, 0, FilterEq("did", forDid))
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
if err != nil {
return nil, fmt.Errorf("error getting all repos by did: %w", err)
}
···
return tx.Commit()
}
-
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
+
func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
}
// ensure all pinned repos are either own repos or collaborating repos
-
repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
if err != nil {
log.Printf("getting repos for %s: %s", profile.Did, err)
}
+69 -24
appview/db/pulls.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
insert into pull_submissions (pull_at, round_number, patch, combined, source_rev)
values (?, ?, ?, ?, ?)
`, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev)
-
return err
+
if err != nil {
+
return err
+
}
+
+
if err := putReferences(tx, pull.AtUri(), pull.References); err != nil {
+
return fmt.Errorf("put reference_links: %w", err)
+
}
+
+
return nil
}
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
···
return pullId - 1, err
}
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
+
func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
pulls := make(map[syntax.ATURI]*models.Pull)
var conditions []string
···
for _, p := range pulls {
pullAts = append(pullAts, p.AtUri())
}
-
submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
+
submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
if err != nil {
return nil, fmt.Errorf("failed to get submissions: %w", err)
}
···
}
// collect allLabels for each issue
-
allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
if err != nil {
return nil, fmt.Errorf("failed to query labels: %w", err)
}
···
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
}
}
-
sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
+
sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("failed to get source repos: %w", err)
}
···
}
}
+
allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
+
if err != nil {
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
+
}
+
for pullAt, references := range allReferences {
+
if pull, ok := pulls[pullAt]; ok {
+
pull.References = references
+
}
+
}
+
orderedByPullId := []*models.Pull{}
for _, p := range pulls {
orderedByPullId = append(orderedByPullId, p)
···
return orderedByPullId, nil
}
-
func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
+
func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
return GetPullsWithLimit(e, 0, filters...)
}
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
var ids []int64
-
var filters []filter
-
filters = append(filters, FilterEq("state", opts.State))
+
var filters []orm.Filter
+
filters = append(filters, orm.FilterEq("state", opts.State))
if opts.RepoAt != "" {
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
}
var conditions []string
···
}
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
-
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
+
pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
if err != nil {
return nil, err
}
···
}
// mapping from pull -> pull submissions
-
func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
+
func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
// Get comments for all submissions using GetPullComments
submissionIds := slices.Collect(maps.Keys(submissionMap))
-
comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
if err != nil {
-
return nil, err
+
return nil, fmt.Errorf("failed to get pull comments: %w", err)
}
for _, comment := range comments {
if submission, ok := submissionMap[comment.SubmissionId]; ok {
···
return m, nil
}
-
func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
}
defer rows.Close()
-
var comments []models.PullComment
+
commentMap := make(map[string]*models.PullComment)
for rows.Next() {
var comment models.PullComment
var createdAt string
···
comment.Created = t
}
-
comments = append(comments, comment)
+
atUri := comment.AtUri().String()
+
commentMap[atUri] = &comment
}
if err := rows.Err(); err != nil {
return nil, err
}
+
// collect references for each comments
+
commentAts := slices.Collect(maps.Keys(commentMap))
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
+
if err != nil {
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
+
}
+
for commentAt, references := range allReferencs {
+
if comment, ok := commentMap[commentAt.String()]; ok {
+
comment.References = references
+
}
+
}
+
+
var comments []models.PullComment
+
for _, c := range commentMap {
+
comments = append(comments, *c)
+
}
+
+
sort.Slice(comments, func(i, j int) bool {
+
return comments[i].Created.Before(comments[j].Created)
+
})
+
return comments, nil
}
···
return pulls, nil
}
-
func NewPullComment(e Execer, comment *models.PullComment) (int64, error) {
+
func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) {
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
-
res, err := e.Exec(
+
res, err := tx.Exec(
query,
comment.OwnerDid,
comment.RepoAt,
···
i, err := res.LastInsertId()
if err != nil {
return 0, err
+
}
+
+
if err := putReferences(tx, comment.AtUri(), comment.References); err != nil {
+
return 0, fmt.Errorf("put reference_links: %w", err)
}
return i, nil
···
return err
}
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
var conditions []string
var args []any
···
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
// otherwise submissions are immutable
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
var conditions []string
var args []any
···
func GetStack(e Execer, stackId string) (models.Stack, error) {
unorderedPulls, err := GetPulls(
e,
-
FilterEq("stack_id", stackId),
-
FilterNotEq("state", models.PullDeleted),
+
orm.FilterEq("stack_id", stackId),
+
orm.FilterNotEq("state", models.PullDeleted),
)
if err != nil {
return nil, err
···
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
pulls, err := GetPulls(
e,
-
FilterEq("stack_id", stackId),
-
FilterEq("state", models.PullDeleted),
+
orm.FilterEq("stack_id", stackId),
+
orm.FilterEq("state", models.PullDeleted),
)
if err != nil {
return nil, err
+2 -1
appview/db/punchcard.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
// this adds to the existing count
···
return err
}
-
func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) {
+
func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) {
punchcard := &models.Punchcard{}
now := time.Now()
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+463
appview/db/reference.go
···
+
package db
+
+
import (
+
"database/sql"
+
"fmt"
+
"strings"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
+
)
+
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
+
// It will ignore missing refLinks.
+
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
+
var (
+
issueRefs []models.ReferenceLink
+
pullRefs []models.ReferenceLink
+
)
+
for _, ref := range refLinks {
+
switch ref.Kind {
+
case models.RefKindIssue:
+
issueRefs = append(issueRefs, ref)
+
case models.RefKindPull:
+
pullRefs = append(pullRefs, ref)
+
}
+
}
+
issueUris, err := findIssueReferences(e, issueRefs)
+
if err != nil {
+
return nil, fmt.Errorf("find issue references: %w", err)
+
}
+
pullUris, err := findPullReferences(e, pullRefs)
+
if err != nil {
+
return nil, fmt.Errorf("find pull references: %w", err)
+
}
+
+
return append(issueUris, pullUris...), nil
+
}
+
+
func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
+
if len(refLinks) == 0 {
+
return nil, nil
+
}
+
vals := make([]string, len(refLinks))
+
args := make([]any, 0, len(refLinks)*4)
+
for i, ref := range refLinks {
+
vals[i] = "(?, ?, ?, ?)"
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
+
}
+
query := fmt.Sprintf(
+
`with input(owner_did, name, issue_id, comment_id) as (
+
values %s
+
)
+
select
+
i.did, i.rkey,
+
c.did, c.rkey
+
from input inp
+
join repos r
+
on r.did = inp.owner_did
+
and r.name = inp.name
+
join issues i
+
on i.repo_at = r.at_uri
+
and i.issue_id = inp.issue_id
+
left join issue_comments c
+
on inp.comment_id is not null
+
and c.issue_at = i.at_uri
+
and c.id = inp.comment_id
+
`,
+
strings.Join(vals, ","),
+
)
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var uris []syntax.ATURI
+
+
for rows.Next() {
+
// Scan rows
+
var issueOwner, issueRkey string
+
var commentOwner, commentRkey sql.NullString
+
var uri syntax.ATURI
+
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
+
return nil, err
+
}
+
if commentOwner.Valid && commentRkey.Valid {
+
uri = syntax.ATURI(fmt.Sprintf(
+
"at://%s/%s/%s",
+
commentOwner.String,
+
tangled.RepoIssueCommentNSID,
+
commentRkey.String,
+
))
+
} else {
+
uri = syntax.ATURI(fmt.Sprintf(
+
"at://%s/%s/%s",
+
issueOwner,
+
tangled.RepoIssueNSID,
+
issueRkey,
+
))
+
}
+
uris = append(uris, uri)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
+
return uris, nil
+
}
+
+
func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
+
if len(refLinks) == 0 {
+
return nil, nil
+
}
+
vals := make([]string, len(refLinks))
+
args := make([]any, 0, len(refLinks)*4)
+
for i, ref := range refLinks {
+
vals[i] = "(?, ?, ?, ?)"
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
+
}
+
query := fmt.Sprintf(
+
`with input(owner_did, name, pull_id, comment_id) as (
+
values %s
+
)
+
select
+
p.owner_did, p.rkey,
+
c.comment_at
+
from input inp
+
join repos r
+
on r.did = inp.owner_did
+
and r.name = inp.name
+
join pulls p
+
on p.repo_at = r.at_uri
+
and p.pull_id = inp.pull_id
+
left join pull_comments c
+
on inp.comment_id is not null
+
and c.repo_at = r.at_uri and c.pull_id = p.pull_id
+
and c.id = inp.comment_id
+
`,
+
strings.Join(vals, ","),
+
)
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var uris []syntax.ATURI
+
+
for rows.Next() {
+
// Scan rows
+
var pullOwner, pullRkey string
+
var commentUri sql.NullString
+
var uri syntax.ATURI
+
if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil {
+
return nil, err
+
}
+
if commentUri.Valid {
+
// no-op
+
uri = syntax.ATURI(commentUri.String)
+
} else {
+
uri = syntax.ATURI(fmt.Sprintf(
+
"at://%s/%s/%s",
+
pullOwner,
+
tangled.RepoPullNSID,
+
pullRkey,
+
))
+
}
+
uris = append(uris, uri)
+
}
+
return uris, nil
+
}
+
+
func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error {
+
err := deleteReferences(tx, fromAt)
+
if err != nil {
+
return fmt.Errorf("delete old reference_links: %w", err)
+
}
+
if len(references) == 0 {
+
return nil
+
}
+
+
values := make([]string, 0, len(references))
+
args := make([]any, 0, len(references)*2)
+
for _, ref := range references {
+
values = append(values, "(?, ?)")
+
args = append(args, fromAt, ref)
+
}
+
_, err = tx.Exec(
+
fmt.Sprintf(
+
`insert into reference_links (from_at, to_at)
+
values %s`,
+
strings.Join(values, ","),
+
),
+
args...,
+
)
+
if err != nil {
+
return fmt.Errorf("insert new reference_links: %w", err)
+
}
+
return nil
+
}
+
+
func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error {
+
_, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt)
+
return err
+
}
+
+
func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
+
var (
+
conditions []string
+
args []any
+
)
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
rows, err := e.Query(
+
fmt.Sprintf(
+
`select from_at, to_at from reference_links %s`,
+
whereClause,
+
),
+
args...,
+
)
+
if err != nil {
+
return nil, fmt.Errorf("query reference_links: %w", err)
+
}
+
defer rows.Close()
+
+
result := make(map[syntax.ATURI][]syntax.ATURI)
+
+
for rows.Next() {
+
var from, to syntax.ATURI
+
if err := rows.Scan(&from, &to); err != nil {
+
return nil, fmt.Errorf("scan row: %w", err)
+
}
+
+
result[from] = append(result[from], to)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
+
return result, nil
+
}
+
+
func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) {
+
rows, err := e.Query(
+
`select from_at from reference_links
+
where to_at = ?`,
+
target,
+
)
+
if err != nil {
+
return nil, fmt.Errorf("query backlinks: %w", err)
+
}
+
defer rows.Close()
+
+
var (
+
backlinks []models.RichReferenceLink
+
backlinksMap = make(map[string][]syntax.ATURI)
+
)
+
for rows.Next() {
+
var from syntax.ATURI
+
if err := rows.Scan(&from); err != nil {
+
return nil, fmt.Errorf("scan row: %w", err)
+
}
+
nsid := from.Collection().String()
+
backlinksMap[nsid] = append(backlinksMap[nsid], from)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
+
var ls []models.RichReferenceLink
+
ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID])
+
if err != nil {
+
return nil, fmt.Errorf("get issue backlinks: %w", err)
+
}
+
backlinks = append(backlinks, ls...)
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
+
if err != nil {
+
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
+
}
+
backlinks = append(backlinks, ls...)
+
ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID])
+
if err != nil {
+
return nil, fmt.Errorf("get pull backlinks: %w", err)
+
}
+
backlinks = append(backlinks, ls...)
+
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID])
+
if err != nil {
+
return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
+
}
+
backlinks = append(backlinks, ls...)
+
+
return backlinks, nil
+
}
+
+
func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
+
if len(aturis) == 0 {
+
return nil, nil
+
}
+
vals := make([]string, len(aturis))
+
args := make([]any, 0, len(aturis)*2)
+
for i, aturi := range aturis {
+
vals[i] = "(?, ?)"
+
did := aturi.Authority().String()
+
rkey := aturi.RecordKey().String()
+
args = append(args, did, rkey)
+
}
+
rows, err := e.Query(
+
fmt.Sprintf(
+
`select r.did, r.name, i.issue_id, i.title, i.open
+
from issues i
+
join repos r
+
on r.at_uri = i.repo_at
+
where (i.did, i.rkey) in (%s)`,
+
strings.Join(vals, ","),
+
),
+
args...,
+
)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
var refLinks []models.RichReferenceLink
+
for rows.Next() {
+
var l models.RichReferenceLink
+
l.Kind = models.RefKindIssue
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
+
return nil, err
+
}
+
refLinks = append(refLinks, l)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
return refLinks, nil
+
}
+
+
func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
+
if len(aturis) == 0 {
+
return nil, nil
+
}
+
filter := orm.FilterIn("c.at_uri", aturis)
+
rows, err := e.Query(
+
fmt.Sprintf(
+
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
+
from issue_comments c
+
join issues i
+
on i.at_uri = c.issue_at
+
join repos r
+
on r.at_uri = i.repo_at
+
where %s`,
+
filter.Condition(),
+
),
+
filter.Arg()...,
+
)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
var refLinks []models.RichReferenceLink
+
for rows.Next() {
+
var l models.RichReferenceLink
+
l.Kind = models.RefKindIssue
+
l.CommentId = new(int)
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
+
return nil, err
+
}
+
refLinks = append(refLinks, l)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
return refLinks, nil
+
}
+
+
func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
+
if len(aturis) == 0 {
+
return nil, nil
+
}
+
vals := make([]string, len(aturis))
+
args := make([]any, 0, len(aturis)*2)
+
for i, aturi := range aturis {
+
vals[i] = "(?, ?)"
+
did := aturi.Authority().String()
+
rkey := aturi.RecordKey().String()
+
args = append(args, did, rkey)
+
}
+
rows, err := e.Query(
+
fmt.Sprintf(
+
`select r.did, r.name, p.pull_id, p.title, p.state
+
from pulls p
+
join repos r
+
on r.at_uri = p.repo_at
+
where (p.owner_did, p.rkey) in (%s)`,
+
strings.Join(vals, ","),
+
),
+
args...,
+
)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
var refLinks []models.RichReferenceLink
+
for rows.Next() {
+
var l models.RichReferenceLink
+
l.Kind = models.RefKindPull
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
+
return nil, err
+
}
+
refLinks = append(refLinks, l)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
return refLinks, nil
+
}
+
+
func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
+
if len(aturis) == 0 {
+
return nil, nil
+
}
+
filter := orm.FilterIn("c.comment_at", aturis)
+
rows, err := e.Query(
+
fmt.Sprintf(
+
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
+
from repos r
+
join pulls p
+
on r.at_uri = p.repo_at
+
join pull_comments c
+
on r.at_uri = c.repo_at and p.pull_id = c.pull_id
+
where %s`,
+
filter.Condition(),
+
),
+
filter.Arg()...,
+
)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
var refLinks []models.RichReferenceLink
+
for rows.Next() {
+
var l models.RichReferenceLink
+
l.Kind = models.RefKindPull
+
l.CommentId = new(int)
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
+
return nil, err
+
}
+
refLinks = append(refLinks, l)
+
}
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("iterate rows: %w", err)
+
}
+
return refLinks, nil
+
}
+4 -3
appview/db/registration.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
-
func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) {
+
func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) {
var registrations []models.Registration
var conditions []string
···
return registrations, nil
}
-
func MarkRegistered(e Execer, filters ...filter) error {
+
func MarkRegistered(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func DeleteKnot(e Execer, filters ...filter) error {
+
func DeleteKnot(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+6 -5
appview/db/repos.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
-
func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
+
func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) {
repoMap := make(map[syntax.ATURI]*models.Repo)
var conditions []string
···
}
// helper to get exactly one repo
-
func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
+
func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
repos, err := GetRepos(e, 0, filters...)
if err != nil {
return nil, err
···
return &repos[0], nil
}
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
+
func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func UnsubscribeLabel(e Execer, filters ...filter) error {
+
func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
+
func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
var conditions []string
var args []any
for _, filter := range filters {
+6 -5
appview/db/spindle.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
-
func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) {
+
func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) {
var spindles []models.Spindle
var conditions []string
···
return err
}
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
+
func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return res.RowsAffected()
}
-
func DeleteSpindle(e Execer, filters ...filter) error {
+
func DeleteSpindle(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
+
func RemoveSpindleMember(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
···
return err
}
-
func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) {
+
func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) {
var members []models.SpindleMember
var conditions []string
+5 -4
appview/db/star.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func AddStar(e Execer, star *models.Star) error {
···
// GetRepoStars return a list of stars each holding target repository.
// If there isn't known repo with starred at-uri, those stars will be ignored.
-
func GetRepoStars(e Execer, limit int, filters ...filter) ([]models.RepoStar, error) {
+
func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return nil, nil
}
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
if err != nil {
return nil, err
}
···
return repoStars, nil
}
-
func CountStars(e Execer, filters ...filter) (int64, error) {
+
func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
}
// get full repo data
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
if err != nil {
return nil, err
}
+4 -3
appview/db/strings.go
···
"time"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func AddString(e Execer, s models.String) error {
···
return err
}
-
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
+
func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) {
var all []models.String
var conditions []string
···
return all, nil
}
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
+
func CountStrings(e Execer, filters ...orm.Filter) (int64, error) {
var conditions []string
var args []any
for _, filter := range filters {
···
return count, nil
}
-
func DeleteString(e Execer, filters ...filter) error {
+
func DeleteString(e Execer, filters ...orm.Filter) error {
var conditions []string
var args []any
for _, filter := range filters {
+9 -8
appview/db/timeline.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
// TODO: this gathers heterogenous events from different sources and aggregates
···
}
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
-
filters := make([]filter, 0)
+
filters := make([]orm.Filter, 0)
if userIsFollowing != nil {
-
filters = append(filters, FilterIn("did", userIsFollowing))
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
}
repos, err := GetRepos(e, limit, filters...)
···
var origRepos []models.Repo
if args != nil {
-
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
+
origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args))
}
if err != nil {
return nil, err
···
}
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
-
filters := make([]filter, 0)
+
filters := make([]orm.Filter, 0)
if userIsFollowing != nil {
-
filters = append(filters, FilterIn("did", userIsFollowing))
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
}
stars, err := GetRepoStars(e, limit, filters...)
···
}
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
-
filters := make([]filter, 0)
+
filters := make([]orm.Filter, 0)
if userIsFollowing != nil {
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
+
filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
}
follows, err := GetFollows(e, limit, filters...)
···
return nil, nil
}
-
profiles, err := GetProfiles(e, FilterIn("did", subjects))
+
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
if err != nil {
return nil, err
}
+47 -29
appview/ingester.go
···
"tangled.org/core/appview/serververify"
"tangled.org/core/appview/validator"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
)
···
err = db.AddArtifact(i.Db, artifact)
case jmodels.CommitOperationDelete:
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
}
if err != nil {
···
err = db.UpsertProfile(tx, &profile)
case jmodels.CommitOperationDelete:
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
}
if err != nil {
···
// get record from db first
members, err := db.GetSpindleMembers(
ddb,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", did),
+
orm.FilterEq("rkey", rkey),
)
if err != nil || len(members) != 1 {
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
// remove record by rkey && update enforcer
if err = db.RemoveSpindleMember(
tx,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", did),
+
orm.FilterEq("rkey", rkey),
); err != nil {
return fmt.Errorf("failed to remove from db: %w", err)
}
···
// get record from db first
spindles, err := db.GetSpindles(
ddb,
-
db.FilterEq("owner", did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", did),
+
orm.FilterEq("instance", instance),
)
if err != nil || len(spindles) != 1 {
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
// remove spindle members first
err = db.RemoveSpindleMember(
tx,
-
db.FilterEq("owner", did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", did),
+
orm.FilterEq("instance", instance),
)
if err != nil {
return err
···
err = db.DeleteSpindle(
tx,
-
db.FilterEq("owner", did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", did),
+
orm.FilterEq("instance", instance),
)
if err != nil {
return err
···
case jmodels.CommitOperationDelete:
if err := db.DeleteString(
ddb,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", did),
+
orm.FilterEq("rkey", rkey),
); err != nil {
l.Error("failed to delete", "err", err)
return fmt.Errorf("failed to delete string record: %w", err)
···
// get record from db first
registrations, err := db.GetRegistrations(
ddb,
-
db.FilterEq("domain", domain),
-
db.FilterEq("did", did),
+
orm.FilterEq("domain", domain),
+
orm.FilterEq("did", did),
)
if err != nil {
return fmt.Errorf("failed to get registration: %w", err)
···
err = db.DeleteKnot(
tx,
-
db.FilterEq("did", did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
return err
···
return nil
case jmodels.CommitOperationDelete:
+
tx, err := ddb.BeginTx(ctx, nil)
+
if err != nil {
+
l.Error("failed to begin transaction", "err", err)
+
return err
+
}
+
defer tx.Rollback()
+
if err := db.DeleteIssues(
-
ddb,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
tx,
+
did,
+
rkey,
); err != nil {
l.Error("failed to delete", "err", err)
return fmt.Errorf("failed to delete issue record: %w", err)
+
}
+
if err := tx.Commit(); err != nil {
+
l.Error("failed to commit txn", "err", err)
+
return err
}
return nil
···
return fmt.Errorf("failed to validate comment: %w", err)
}
-
_, err = db.AddIssueComment(ddb, *comment)
+
tx, err := ddb.Begin()
+
if err != nil {
+
return fmt.Errorf("failed to start transaction: %w", err)
+
}
+
defer tx.Rollback()
+
+
_, err = db.AddIssueComment(tx, *comment)
if err != nil {
return fmt.Errorf("failed to create issue comment: %w", err)
}
-
return nil
+
return tx.Commit()
case jmodels.CommitOperationDelete:
if err := db.DeleteIssueComments(
ddb,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", did),
+
orm.FilterEq("rkey", rkey),
); err != nil {
return fmt.Errorf("failed to delete issue comment record: %w", err)
}
···
case jmodels.CommitOperationDelete:
if err := db.DeleteLabelDefinition(
ddb,
-
db.FilterEq("did", did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", did),
+
orm.FilterEq("rkey", rkey),
); err != nil {
return fmt.Errorf("failed to delete labeldef record: %w", err)
}
···
var repo *models.Repo
switch collection {
case tangled.RepoIssueNSID:
-
i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject))
+
i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject))
if err != nil || len(i) != 1 {
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
}
···
return fmt.Errorf("unsupport label subject: %s", collection)
-
actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels))
+
actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels))
if err != nil {
return fmt.Errorf("failed to build label application ctx: %w", err)
+116 -78
appview/issues/issues.go
···
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
issues_indexer "tangled.org/core/appview/indexer/issues"
+
"tangled.org/core/appview/mentions"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
-
"tangled.org/core/appview/pages/markup"
"tangled.org/core/appview/pages/repoinfo"
"tangled.org/core/appview/pagination"
"tangled.org/core/appview/reporesolver"
"tangled.org/core/appview/validator"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
)
type Issues struct {
-
oauth *oauth.OAuth
-
repoResolver *reporesolver.RepoResolver
-
enforcer *rbac.Enforcer
-
pages *pages.Pages
-
idResolver *idresolver.Resolver
-
db *db.DB
-
config *config.Config
-
notifier notify.Notifier
-
logger *slog.Logger
-
validator *validator.Validator
-
indexer *issues_indexer.Indexer
+
oauth *oauth.OAuth
+
repoResolver *reporesolver.RepoResolver
+
enforcer *rbac.Enforcer
+
pages *pages.Pages
+
idResolver *idresolver.Resolver
+
mentionsResolver *mentions.Resolver
+
db *db.DB
+
config *config.Config
+
notifier notify.Notifier
+
logger *slog.Logger
+
validator *validator.Validator
+
indexer *issues_indexer.Indexer
}
func New(
···
enforcer *rbac.Enforcer,
pages *pages.Pages,
idResolver *idresolver.Resolver,
+
mentionsResolver *mentions.Resolver,
db *db.DB,
config *config.Config,
notifier notify.Notifier,
···
logger *slog.Logger,
) *Issues {
return &Issues{
-
oauth: oauth,
-
repoResolver: repoResolver,
-
enforcer: enforcer,
-
pages: pages,
-
idResolver: idResolver,
-
db: db,
-
config: config,
-
notifier: notifier,
-
logger: logger,
-
validator: validator,
-
indexer: indexer,
+
oauth: oauth,
+
repoResolver: repoResolver,
+
enforcer: enforcer,
+
pages: pages,
+
idResolver: idResolver,
+
mentionsResolver: mentionsResolver,
+
db: db,
+
config: config,
+
notifier: notifier,
+
logger: logger,
+
validator: validator,
+
indexer: indexer,
}
}
···
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
}
+
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
+
if err != nil {
+
l.Error("failed to fetch backlinks", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
labelDefs, err := db.GetLabelDefinitions(
rp.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", tangled.RepoIssueNSID),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
)
if err != nil {
l.Error("failed to fetch labels", "err", err)
···
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
Issue: issue,
CommentList: issue.CommentList(),
+
Backlinks: backlinks,
OrderedReactionKinds: models.OrderedReactionKinds,
Reactions: reactionMap,
UserReacted: userReactions,
···
newIssue := issue
newIssue.Title = r.FormValue("title")
newIssue.Body = r.FormValue("body")
+
newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body)
if err := rp.validator.ValidateIssue(newIssue); err != nil {
l.Error("validation error", "err", err)
···
}
l = l.With("did", issue.Did, "rkey", issue.Rkey)
+
tx, err := rp.db.Begin()
+
if err != nil {
+
l.Error("failed to start transaction", "err", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
+
return
+
}
+
defer tx.Rollback()
+
// delete from PDS
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
···
}
// delete from db
-
if err := db.DeleteIssues(rp.db, db.FilterEq("id", issue.Id)); err != nil {
+
if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil {
l.Error("failed to delete issue", "err", err)
rp.pages.Notice(w, noticeId, "Failed to delete issue.")
return
}
+
tx.Commit()
rp.notifier.DeleteIssue(r.Context(), issue)
// return to all issues page
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
rp.pages.HxRedirect(w, "/"+ownerSlashRepo+"/issues")
}
···
if isIssueOwner || isRepoOwner || isCollaborator {
err = db.CloseIssues(
rp.db,
-
db.FilterEq("id", issue.Id),
+
orm.FilterEq("id", issue.Id),
)
if err != nil {
l.Error("failed to close issue", "err", err)
···
// notify about the issue closure
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
return
} else {
···
if isCollaborator || isRepoOwner || isIssueOwner {
err := db.ReopenIssues(
rp.db,
-
db.FilterEq("id", issue.Id),
+
orm.FilterEq("id", issue.Id),
)
if err != nil {
l.Error("failed to reopen issue", "err", err)
···
// notify about the issue reopen
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
return
} else {
···
replyTo = &replyToUri
}
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
+
comment := models.IssueComment{
-
Did: user.Did,
-
Rkey: tid.TID(),
-
IssueAt: issue.AtUri().String(),
-
ReplyTo: replyTo,
-
Body: body,
-
Created: time.Now(),
+
Did: user.Did,
+
Rkey: tid.TID(),
+
IssueAt: issue.AtUri().String(),
+
ReplyTo: replyTo,
+
Body: body,
+
Created: time.Now(),
+
Mentions: mentions,
+
References: references,
}
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
l.Error("failed to validate comment", "err", err)
···
}
}()
-
commentId, err := db.AddIssueComment(rp.db, comment)
+
tx, err := rp.db.Begin()
+
if err != nil {
+
l.Error("failed to start transaction", "err", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
+
return
+
}
+
defer tx.Rollback()
+
+
commentId, err := db.AddIssueComment(tx, comment)
if err != nil {
l.Error("failed to create comment", "err", err)
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
+
err = tx.Commit()
+
if err != nil {
+
l.Error("failed to commit transaction", "err", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
+
return
+
}
// reset atUri to make rollback a no-op
atUri = ""
···
// notify about the new comment
comment.Id = commentId
-
rawMentions := markup.FindUserMentions(comment.Body)
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
-
var mentions []syntax.DID
-
for _, ident := range idents {
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
-
mentions = append(mentions, ident.DID)
-
}
-
}
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
}
···
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetIssueComments(
rp.db,
-
db.FilterEq("id", commentId),
+
orm.FilterEq("id", commentId),
)
if err != nil {
l.Error("failed to fetch comment", "id", commentId)
···
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetIssueComments(
rp.db,
-
db.FilterEq("id", commentId),
+
orm.FilterEq("id", commentId),
)
if err != nil {
l.Error("failed to fetch comment", "id", commentId)
···
newComment := comment
newComment.Body = newBody
newComment.Edited = &now
+
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
+
record := newComment.AsRecord()
-
_, err = db.AddIssueComment(rp.db, newComment)
+
tx, err := rp.db.Begin()
+
if err != nil {
+
l.Error("failed to start transaction", "err", err)
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
+
return
+
}
+
defer tx.Rollback()
+
+
_, err = db.AddIssueComment(tx, newComment)
if err != nil {
l.Error("failed to perferom update-description query", "err", err)
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
+
tx.Commit()
// rkey is optional, it was introduced later
if newComment.Rkey != "" {
···
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetIssueComments(
rp.db,
-
db.FilterEq("id", commentId),
+
orm.FilterEq("id", commentId),
)
if err != nil {
l.Error("failed to fetch comment", "id", commentId)
···
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetIssueComments(
rp.db,
-
db.FilterEq("id", commentId),
+
orm.FilterEq("id", commentId),
)
if err != nil {
l.Error("failed to fetch comment", "id", commentId)
···
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetIssueComments(
rp.db,
-
db.FilterEq("id", commentId),
+
orm.FilterEq("id", commentId),
)
if err != nil {
l.Error("failed to fetch comment", "id", commentId)
···
// optimistic deletion
deleted := time.Now()
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
if err != nil {
l.Error("failed to delete comment", "err", err)
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
issues, err = db.GetIssues(
rp.db,
-
db.FilterIn("id", res.Hits),
+
orm.FilterIn("id", res.Hits),
)
if err != nil {
l.Error("failed to get issues", "err", err)
···
issues, err = db.GetIssuesPaginated(
rp.db,
page,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("open", openInt),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("open", openInt),
)
if err != nil {
l.Error("failed to get issues", "err", err)
···
labelDefs, err := db.GetLabelDefinitions(
rp.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", tangled.RepoIssueNSID),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
)
if err != nil {
l.Error("failed to fetch labels", "err", err)
···
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
})
case http.MethodPost:
+
body := r.FormValue("body")
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
+
issue := &models.Issue{
-
RepoAt: f.RepoAt(),
-
Rkey: tid.TID(),
-
Title: r.FormValue("title"),
-
Body: r.FormValue("body"),
-
Open: true,
-
Did: user.Did,
-
Created: time.Now(),
-
Repo: &f.Repo,
+
RepoAt: f.RepoAt(),
+
Rkey: tid.TID(),
+
Title: r.FormValue("title"),
+
Body: body,
+
Open: true,
+
Did: user.Did,
+
Created: time.Now(),
+
Mentions: mentions,
+
References: references,
+
Repo: f,
}
if err := rp.validator.ValidateIssue(issue); err != nil {
···
// everything is successful, do not rollback the atproto record
atUri = ""
-
rawMentions := markup.FindUserMentions(issue.Body)
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
-
var mentions []syntax.DID
-
for _, ident := range idents {
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
-
mentions = append(mentions, ident.DID)
-
}
-
}
rp.notifier.NewIssue(r.Context(), issue, mentions)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
return
}
+3 -3
appview/issues/opengraph.go
···
// Get owner handle for avatar
var ownerHandle string
-
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Repo.Did)
+
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Did)
if err != nil {
-
ownerHandle = f.Repo.Did
+
ownerHandle = f.Did
} else {
ownerHandle = "@" + owner.Handle.String()
}
-
card, err := rp.drawIssueSummaryCard(issue, &f.Repo, commentCount, ownerHandle)
+
card, err := rp.drawIssueSummaryCard(issue, f, commentCount, ownerHandle)
if err != nil {
log.Println("failed to draw issue summary card", err)
http.Error(w, "failed to draw issue summary card", http.StatusInternalServerError)
+19 -18
appview/knots/knots.go
···
"tangled.org/core/appview/xrpcclient"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
···
user := k.OAuth.GetUser(r)
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
+
orm.FilterEq("did", user.Did),
)
if err != nil {
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
l.Error("failed to get registrations", "err", err)
···
repos, err := db.GetRepos(
k.Db,
0,
-
db.FilterEq("knot", domain),
+
orm.FilterEq("knot", domain),
)
if err != nil {
l.Error("failed to get knot repos", "err", err)
···
// get record from db first
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
l.Error("failed to get registration", "err", err)
···
err = db.DeleteKnot(
tx,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
l.Error("failed to delete registration", "err", err)
···
// get record from db first
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
l.Error("failed to get registration", "err", err)
···
// Get updated registration to show
registrations, err = db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
)
if err != nil {
l.Error("failed to get registration", "err", err)
···
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
-
db.FilterIsNot("registered", "null"),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
+
orm.FilterIsNot("registered", "null"),
)
if err != nil {
l.Error("failed to get registration", "err", err)
···
registrations, err := db.GetRegistrations(
k.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("domain", domain),
-
db.FilterIsNot("registered", "null"),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("domain", domain),
+
orm.FilterIsNot("registered", "null"),
)
if err != nil {
l.Error("failed to get registration", "err", err)
+5 -4
appview/labels/labels.go
···
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/validator"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
···
repoAt := r.Form.Get("repo")
subjectUri := r.Form.Get("subject")
-
repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt))
+
repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
if err != nil {
fail("Failed to get repository.", err)
return
}
// find all the labels that this repo subscribes to
-
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
+
repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
if err != nil {
fail("Failed to get labels for this repository.", err)
return
···
labelAts = append(labelAts, rl.LabelAt.String())
}
-
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
+
actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
if err != nil {
fail("Invalid form data.", err)
return
}
// calculate the start state by applying already known labels
-
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
+
existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
if err != nil {
fail("Invalid form data.", err)
return
+67
appview/mentions/resolver.go
···
+
package mentions
+
+
import (
+
"context"
+
"log/slog"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.org/core/appview/config"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/pages/markup"
+
"tangled.org/core/idresolver"
+
)
+
+
type Resolver struct {
+
config *config.Config
+
idResolver *idresolver.Resolver
+
execer db.Execer
+
logger *slog.Logger
+
}
+
+
func New(
+
config *config.Config,
+
idResolver *idresolver.Resolver,
+
execer db.Execer,
+
logger *slog.Logger,
+
) *Resolver {
+
return &Resolver{
+
config,
+
idResolver,
+
execer,
+
logger,
+
}
+
}
+
+
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
+
l := r.logger.With("method", "Resolve")
+
+
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
+
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
+
+
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
+
var mentions []syntax.DID
+
for _, ident := range idents {
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
+
mentions = append(mentions, ident.DID)
+
}
+
}
+
l.Debug("found mentions", "mentions", mentions)
+
+
var resolvedRefs []models.ReferenceLink
+
for _, rawRef := range rawRefs {
+
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
+
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
+
continue
+
}
+
rawRef.Handle = string(ident.DID)
+
resolvedRefs = append(resolvedRefs, rawRef)
+
}
+
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
+
if err != nil {
+
l.Error("failed running query", "err", err)
+
}
+
l.Debug("found references", "refs", aturiRefs)
+
+
return mentions, aturiRefs
+
}
+4 -3
appview/middleware/middleware.go
···
"tangled.org/core/appview/pagination"
"tangled.org/core/appview/reporesolver"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
)
···
repo, err := db.GetRepo(
mw.db,
-
db.FilterEq("did", id.DID.String()),
-
db.FilterEq("name", repoName),
+
orm.FilterEq("did", id.DID.String()),
+
orm.FilterEq("name", repoName),
)
if err != nil {
log.Println("failed to resolve repo", "err", err)
···
return
}
-
fullName := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
fullName := reporesolver.GetBaseRepoPath(r, f)
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
if r.URL.Query().Get("go-get") == "1" {
+70 -34
appview/models/issue.go
···
)
type Issue struct {
-
Id int64
-
Did string
-
Rkey string
-
RepoAt syntax.ATURI
-
IssueId int
-
Created time.Time
-
Edited *time.Time
-
Deleted *time.Time
-
Title string
-
Body string
-
Open bool
+
Id int64
+
Did string
+
Rkey string
+
RepoAt syntax.ATURI
+
IssueId int
+
Created time.Time
+
Edited *time.Time
+
Deleted *time.Time
+
Title string
+
Body string
+
Open bool
+
Mentions []syntax.DID
+
References []syntax.ATURI
// optionally, populate this when querying for reverse mappings
// like comment counts, parent repo etc.
···
}
func (i *Issue) AsRecord() tangled.RepoIssue {
+
mentions := make([]string, len(i.Mentions))
+
for i, did := range i.Mentions {
+
mentions[i] = string(did)
+
}
+
references := make([]string, len(i.References))
+
for i, uri := range i.References {
+
references[i] = string(uri)
+
}
return tangled.RepoIssue{
-
Repo: i.RepoAt.String(),
-
Title: i.Title,
-
Body: &i.Body,
-
CreatedAt: i.Created.Format(time.RFC3339),
+
Repo: i.RepoAt.String(),
+
Title: i.Title,
+
Body: &i.Body,
+
Mentions: mentions,
+
References: references,
+
CreatedAt: i.Created.Format(time.RFC3339),
}
}
···
}
type IssueComment struct {
-
Id int64
-
Did string
-
Rkey string
-
IssueAt string
-
ReplyTo *string
-
Body string
-
Created time.Time
-
Edited *time.Time
-
Deleted *time.Time
+
Id int64
+
Did string
+
Rkey string
+
IssueAt string
+
ReplyTo *string
+
Body string
+
Created time.Time
+
Edited *time.Time
+
Deleted *time.Time
+
Mentions []syntax.DID
+
References []syntax.ATURI
}
func (i *IssueComment) AtUri() syntax.ATURI {
···
}
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
+
mentions := make([]string, len(i.Mentions))
+
for i, did := range i.Mentions {
+
mentions[i] = string(did)
+
}
+
references := make([]string, len(i.References))
+
for i, uri := range i.References {
+
references[i] = string(uri)
+
}
return tangled.RepoIssueComment{
-
Body: i.Body,
-
Issue: i.IssueAt,
-
CreatedAt: i.Created.Format(time.RFC3339),
-
ReplyTo: i.ReplyTo,
+
Body: i.Body,
+
Issue: i.IssueAt,
+
CreatedAt: i.Created.Format(time.RFC3339),
+
ReplyTo: i.ReplyTo,
+
Mentions: mentions,
+
References: references,
}
}
···
return nil, err
}
+
i := record
+
mentions := make([]syntax.DID, len(record.Mentions))
+
for i, did := range record.Mentions {
+
mentions[i] = syntax.DID(did)
+
}
+
references := make([]syntax.ATURI, len(record.References))
+
for i, uri := range i.References {
+
references[i] = syntax.ATURI(uri)
+
}
+
comment := IssueComment{
-
Did: ownerDid,
-
Rkey: rkey,
-
Body: record.Body,
-
IssueAt: record.Issue,
-
ReplyTo: record.ReplyTo,
-
Created: created,
+
Did: ownerDid,
+
Rkey: rkey,
+
Body: record.Body,
+
IssueAt: record.Issue,
+
ReplyTo: record.ReplyTo,
+
Created: created,
+
Mentions: mentions,
+
References: references,
}
return &comment, nil
+3 -1
appview/models/profile.go
···
}
type ByMonth struct {
+
Commits int
RepoEvents []RepoEvent
IssueEvents IssueEvents
PullEvents PullEvents
···
func (b ByMonth) IsEmpty() bool {
return len(b.RepoEvents) == 0 &&
len(b.IssueEvents.Items) == 0 &&
-
len(b.PullEvents.Items) == 0
+
len(b.PullEvents.Items) == 0 &&
+
b.Commits == 0
}
type IssueEvents struct {
+41 -3
appview/models/pull.go
···
TargetBranch string
State PullState
Submissions []*PullSubmission
+
Mentions []syntax.DID
+
References []syntax.ATURI
// stacking
StackId string // nullable string
···
source.Repo = &s
}
}
+
mentions := make([]string, len(p.Mentions))
+
for i, did := range p.Mentions {
+
mentions[i] = string(did)
+
}
+
references := make([]string, len(p.References))
+
for i, uri := range p.References {
+
references[i] = string(uri)
+
}
record := tangled.RepoPull{
-
Title: p.Title,
-
Body: &p.Body,
-
CreatedAt: p.Created.Format(time.RFC3339),
+
Title: p.Title,
+
Body: &p.Body,
+
Mentions: mentions,
+
References: references,
+
CreatedAt: p.Created.Format(time.RFC3339),
Target: &tangled.RepoPull_Target{
Repo: p.RepoAt.String(),
Branch: p.TargetBranch,
···
// content
Body string
+
+
// meta
+
Mentions []syntax.DID
+
References []syntax.ATURI
// meta
Created time.Time
}
+
+
func (p *PullComment) AtUri() syntax.ATURI {
+
return syntax.ATURI(p.CommentAt)
+
}
+
+
// func (p *PullComment) AsRecord() tangled.RepoPullComment {
+
// mentions := make([]string, len(p.Mentions))
+
// for i, did := range p.Mentions {
+
// mentions[i] = string(did)
+
// }
+
// references := make([]string, len(p.References))
+
// for i, uri := range p.References {
+
// references[i] = string(uri)
+
// }
+
// return tangled.RepoPullComment{
+
// Pull: p.PullAt,
+
// Body: p.Body,
+
// Mentions: mentions,
+
// References: references,
+
// CreatedAt: p.Created.Format(time.RFC3339),
+
// }
+
// }
func (p *Pull) LastRoundNumber() int {
return len(p.Submissions) - 1
+49
appview/models/reference.go
···
+
package models
+
+
import "fmt"
+
+
type RefKind int
+
+
const (
+
RefKindIssue RefKind = iota
+
RefKindPull
+
)
+
+
func (k RefKind) String() string {
+
if k == RefKindIssue {
+
return "issues"
+
} else {
+
return "pulls"
+
}
+
}
+
+
// /@alice.com/cool-proj/issues/123
+
// /@alice.com/cool-proj/issues/123#comment-321
+
type ReferenceLink struct {
+
Handle string
+
Repo string
+
Kind RefKind
+
SubjectId int
+
CommentId *int
+
}
+
+
func (l ReferenceLink) String() string {
+
comment := ""
+
if l.CommentId != nil {
+
comment = fmt.Sprintf("#comment-%d", *l.CommentId)
+
}
+
return fmt.Sprintf("/%s/%s/%s/%d%s",
+
l.Handle,
+
l.Repo,
+
l.Kind.String(),
+
l.SubjectId,
+
comment,
+
)
+
}
+
+
type RichReferenceLink struct {
+
ReferenceLink
+
Title string
+
// reusing PullState for both issue & PR
+
State PullState
+
}
+5 -4
appview/notifications/notifications.go
···
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pagination"
+
"tangled.org/core/orm"
)
type Notifications struct {
···
total, err := db.CountNotifications(
n.db,
-
db.FilterEq("recipient_did", user.Did),
+
orm.FilterEq("recipient_did", user.Did),
)
if err != nil {
l.Error("failed to get total notifications", "err", err)
···
notifications, err := db.GetNotificationsWithEntities(
n.db,
page,
-
db.FilterEq("recipient_did", user.Did),
+
orm.FilterEq("recipient_did", user.Did),
)
if err != nil {
l.Error("failed to get notifications", "err", err)
···
count, err := db.CountNotifications(
n.db,
-
db.FilterEq("recipient_did", user.Did),
-
db.FilterEq("read", 0),
+
orm.FilterEq("recipient_did", user.Did),
+
orm.FilterEq("read", 0),
)
if err != nil {
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77 -66
appview/notify/db/db.go
···
import (
"context"
"log"
-
"maps"
"slices"
"github.com/bluesky-social/indigo/atproto/syntax"
···
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
+
"tangled.org/core/sets"
)
const (
-
maxMentions = 5
+
maxMentions = 8
)
type databaseNotifier struct {
···
return
}
var err error
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt)))
if err != nil {
log.Printf("NewStar: failed to get repos: %v", err)
return
}
actorDid := syntax.DID(star.Did)
-
recipients := []syntax.DID{syntax.DID(repo.Did)}
+
recipients := sets.Singleton(syntax.DID(repo.Did))
eventType := models.NotificationTypeRepoStarred
entityType := "repo"
entityId := star.RepoAt.String()
···
}
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
-
-
// build the recipients list
-
// - owner of the repo
-
// - collaborators in the repo
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build the recipients list
+
// - owner of the repo
+
// - collaborators in the repo
+
// - remove users already mentioned
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
+
}
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(issue.Did)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
}
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
if err != nil {
log.Printf("NewIssueComment: failed to get issues: %v", err)
return
···
}
issue := issues[0]
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
+
// built the recipients list:
+
// - the owner of the repo
+
// - | if the comment is a reply -> everybody on that thread
+
// | if the comment is a top level -> just the issue owner
+
// - remove mentioned users from the recipients list
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
if comment.IsReply() {
// if this comment is a reply, then notify everybody in that thread
parentAtUri := *comment.ReplyTo
-
allThreads := issue.CommentList()
// find the parent thread, and add all DIDs from here to the recipient list
-
for _, t := range allThreads {
+
for _, t := range issue.CommentList() {
if t.Self.AtUri().String() == parentAtUri {
-
recipients = append(recipients, t.Participants()...)
+
for _, p := range t.Participants() {
+
recipients.Insert(p)
+
}
}
}
} else {
// not a reply, notify just the issue author
-
recipients = append(recipients, syntax.DID(issue.Did))
+
recipients.Insert(syntax.DID(issue.Did))
+
}
+
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(comment.Did)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
actorDid := syntax.DID(follow.UserDid)
-
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
+
recipients := sets.Singleton(syntax.DID(follow.SubjectDid))
eventType := models.NotificationTypeFollowed
entityType := "follow"
entityId := follow.UserDid
···
}
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPull: failed to get repos: %v", err)
return
}
-
-
// build the recipients list
-
// - owner of the repo
-
// - collaborators in the repo
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build the recipients list
+
// - owner of the repo
+
// - collaborators in the repo
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
actorDid := syntax.DID(pull.OwnerDid)
···
return
}
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
if err != nil {
log.Printf("NewPullComment: failed to get repos: %v", err)
return
···
// build up the recipients list:
// - repo owner
// - all pull participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
+
// - remove those already mentioned
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, p := range pull.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
+
}
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(comment.OwnerDid)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
}
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
-
// build up the recipients list:
-
// - repo owner
-
// - repo collaborators
-
// - all issue participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build up the recipients list:
+
// - repo owner
+
// - repo collaborators
+
// - all issue participants
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
for _, p := range issue.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
}
entityType := "pull"
···
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
// Get repo details
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPullState: failed to get repos: %v", err)
return
}
-
// build up the recipients list:
-
// - repo owner
-
// - all pull participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build up the recipients list:
+
// - repo owner
+
// - all pull participants
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
for _, p := range pull.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
}
entityType := "pull"
···
func (n *databaseNotifier) notifyEvent(
actorDid syntax.DID,
-
recipients []syntax.DID,
+
recipients sets.Set[syntax.DID],
eventType models.NotificationType,
entityType string,
entityId string,
···
issueId *int64,
pullId *int64,
) {
-
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
-
recipients = recipients[:maxMentions]
+
// if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody
+
if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions {
+
return
}
-
recipientSet := make(map[syntax.DID]struct{})
-
for _, did := range recipients {
-
// everybody except actor themselves
-
if did != actorDid {
-
recipientSet[did] = struct{}{}
-
}
-
}
+
+
recipients.Remove(actorDid)
prefMap, err := db.GetNotificationPreferences(
n.db,
-
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
+
orm.FilterIn("user_did", slices.Collect(recipients.All())),
)
if err != nil {
// failed to get prefs for users
···
defer tx.Rollback()
// filter based on preferences
-
for recipientDid := range recipientSet {
+
for recipientDid := range recipients.All() {
prefs, ok := prefMap[recipientDid]
if !ok {
prefs = models.DefaultNotificationPreferences(recipientDid)
+3 -2
appview/oauth/handler.go
···
"tangled.org/core/api/tangled"
"tangled.org/core/appview/db"
"tangled.org/core/consts"
+
"tangled.org/core/orm"
"tangled.org/core/tid"
)
···
// and create an sh.tangled.spindle.member record with that
spindleMembers, err := db.GetSpindleMembers(
o.Db,
-
db.FilterEq("instance", "spindle.tangled.sh"),
-
db.FilterEq("subject", did),
+
orm.FilterEq("instance", "spindle.tangled.sh"),
+
orm.FilterEq("subject", did),
)
if err != nil {
l.Error("failed to get spindle members", "err", err)
+1 -1
appview/pages/funcmap.go
···
}
return pairs, nil
},
-
"append": func(s []string, values ...string) []string {
+
"append": func(s []any, values ...any) []any {
s = append(s, values...)
return s
},
-26
appview/pages/markup/markdown.go
···
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
-
treeblood "github.com/wyatt915/goldmark-treeblood"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/ast"
···
extension.NewFootnote(
extension.WithFootnoteIDPrefix([]byte("footnote")),
),
-
treeblood.MathML(),
callout.CalloutExtention,
textension.AtExt,
),
···
}
return path.Join(rctx.CurrentDir, dst)
-
}
-
-
// FindUserMentions returns Set of user handles from given markup soruce.
-
// It doesn't guarntee unique DIDs
-
func FindUserMentions(source string) []string {
-
var (
-
mentions []string
-
mentionsSet = make(map[string]struct{})
-
md = NewMarkdown()
-
sourceBytes = []byte(source)
-
root = md.Parser().Parse(text.NewReader(sourceBytes))
-
)
-
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
-
if entering && n.Kind() == textension.KindAt {
-
handle := n.(*textension.AtNode).Handle
-
mentionsSet[handle] = struct{}{}
-
return ast.WalkSkipChildren, nil
-
}
-
return ast.WalkContinue, nil
-
})
-
for handle := range mentionsSet {
-
mentions = append(mentions, handle)
-
}
-
return mentions
}
func isAbsoluteUrl(link string) bool {
+124
appview/pages/markup/reference_link.go
···
+
package markup
+
+
import (
+
"maps"
+
"net/url"
+
"path"
+
"slices"
+
"strconv"
+
"strings"
+
+
"github.com/yuin/goldmark/ast"
+
"github.com/yuin/goldmark/text"
+
"tangled.org/core/appview/models"
+
textension "tangled.org/core/appview/pages/markup/extension"
+
)
+
+
// FindReferences collects all links referencing tangled-related objects
+
// like issues, PRs, comments or even @-mentions
+
// This funciton doesn't actually check for the existence of records in the DB
+
// or the PDS; it merely returns a list of what are presumed to be references.
+
func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) {
+
var (
+
refLinkSet = make(map[models.ReferenceLink]struct{})
+
mentionsSet = make(map[string]struct{})
+
md = NewMarkdown()
+
sourceBytes = []byte(source)
+
root = md.Parser().Parse(text.NewReader(sourceBytes))
+
)
+
// trim url scheme. the SSL shouldn't matter
+
baseUrl = strings.TrimPrefix(baseUrl, "https://")
+
baseUrl = strings.TrimPrefix(baseUrl, "http://")
+
+
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+
if !entering {
+
return ast.WalkContinue, nil
+
}
+
switch n.Kind() {
+
case textension.KindAt:
+
handle := n.(*textension.AtNode).Handle
+
mentionsSet[handle] = struct{}{}
+
return ast.WalkSkipChildren, nil
+
case ast.KindLink:
+
dest := string(n.(*ast.Link).Destination)
+
ref := parseTangledLink(baseUrl, dest)
+
if ref != nil {
+
refLinkSet[*ref] = struct{}{}
+
}
+
return ast.WalkSkipChildren, nil
+
case ast.KindAutoLink:
+
an := n.(*ast.AutoLink)
+
if an.AutoLinkType == ast.AutoLinkURL {
+
dest := string(an.URL(sourceBytes))
+
ref := parseTangledLink(baseUrl, dest)
+
if ref != nil {
+
refLinkSet[*ref] = struct{}{}
+
}
+
}
+
return ast.WalkSkipChildren, nil
+
}
+
return ast.WalkContinue, nil
+
})
+
mentions := slices.Collect(maps.Keys(mentionsSet))
+
references := slices.Collect(maps.Keys(refLinkSet))
+
return mentions, references
+
}
+
+
func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
+
u, err := url.Parse(urlStr)
+
if err != nil {
+
return nil
+
}
+
+
if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
+
return nil
+
}
+
+
p := path.Clean(u.Path)
+
parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
+
if len(parts) < 4 {
+
// need at least: handle / repo / kind / id
+
return nil
+
}
+
+
var (
+
handle = parts[0]
+
repo = parts[1]
+
kindSeg = parts[2]
+
subjectSeg = parts[3]
+
)
+
+
handle = strings.TrimPrefix(handle, "@")
+
+
var kind models.RefKind
+
switch kindSeg {
+
case "issues":
+
kind = models.RefKindIssue
+
case "pulls":
+
kind = models.RefKindPull
+
default:
+
return nil
+
}
+
+
subjectId, err := strconv.Atoi(subjectSeg)
+
if err != nil {
+
return nil
+
}
+
var commentId *int
+
if u.Fragment != "" {
+
if strings.HasPrefix(u.Fragment, "comment-") {
+
commentIdStr := u.Fragment[len("comment-"):]
+
if id, err := strconv.Atoi(commentIdStr); err == nil {
+
commentId = &id
+
}
+
}
+
}
+
+
return &models.ReferenceLink{
+
Handle: handle,
+
Repo: repo,
+
Kind: kind,
+
SubjectId: subjectId,
+
CommentId: commentId,
+
}
+
}
+3 -2
appview/pages/pages.go
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/go-git/go-git/v5/plumbing"
-
"github.com/go-git/go-git/v5/plumbing/object"
)
//go:embed templates/* static legal
···
RepoInfo repoinfo.RepoInfo
Active string
TagMap map[string][]string
-
CommitsTrunc []*object.Commit
+
CommitsTrunc []types.Commit
TagsTrunc []*types.TagReference
BranchesTrunc []types.Branch
// ForkInfo *types.ForkInfo
···
Active string
Issue *models.Issue
CommentList []models.CommentListItem
+
Backlinks []models.RichReferenceLink
LabelDefs map[string]*models.LabelDefinition
OrderedReactionKinds []models.ReactionKind
···
Pull *models.Pull
Stack models.Stack
AbandonedPulls []*models.Pull
+
Backlinks []models.RichReferenceLink
BranchDeleteStatus *models.BranchDeleteStatus
MergeCheck types.MergeCheckResponse
ResubmitCheck ResubmitResult
+22
appview/pages/templates/fragments/tinyAvatarList.html
···
+
{{ define "fragments/tinyAvatarList" }}
+
{{ $all := .all }}
+
{{ $classes := .classes }}
+
{{ $ps := take $all 5 }}
+
<div class="inline-flex items-center -space-x-3">
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
+
{{ range $i, $p := $ps }}
+
<img
+
src="{{ tinyAvatar . }}"
+
alt=""
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
+
/>
+
{{ end }}
+
+
{{ if gt (len $all) 5 }}
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
+
+{{ sub (len $all) 5 }}
+
</span>
+
{{ end }}
+
</div>
+
{{ end }}
+
+34 -9
appview/pages/templates/repo/commit.html
···
</div>
<div class="flex flex-wrap items-center space-x-2">
-
<p class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-300">
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
-
{{ if $did }}
-
{{ template "user/fragments/picHandleLink" $did }}
-
{{ else }}
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $commit.Author.Name }}</a>
-
{{ end }}
+
<p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
+
{{ template "attribution" . }}
<span class="px-1 select-none before:content-['\00B7']"></span>
-
{{ template "repo/fragments/time" $commit.Author.When }}
+
{{ template "repo/fragments/time" $commit.Committer.When }}
<span class="px-1 select-none before:content-['\00B7']"></span>
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
</section>
{{end}}
+
+
{{ define "attribution" }}
+
{{ $commit := .Diff.Commit }}
+
{{ $showCommitter := true }}
+
{{ if eq $commit.Author.Email $commit.Committer.Email }}
+
{{ $showCommitter = false }}
+
{{ end }}
+
+
{{ if $showCommitter }}
+
authored by {{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid) }}
+
{{ range $commit.CoAuthors }}
+
{{ template "attributedUser" (list .Email .Name $.EmailToDid) }}
+
{{ end }}
+
and committed by {{ template "attributedUser" (list $commit.Committer.Email $commit.Committer.Name $.EmailToDid) }}
+
{{ else }}
+
{{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid )}}
+
{{ end }}
+
{{ end }}
+
+
{{ define "attributedUser" }}
+
{{ $email := index . 0 }}
+
{{ $name := index . 1 }}
+
{{ $map := index . 2 }}
+
{{ $did := index $map $email }}
+
+
{{ if $did }}
+
{{ template "user/fragments/picHandleLink" $did }}
+
{{ else }}
+
<a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a>
+
{{ end }}
+
{{ end }}
{{ define "topbarLayout" }}
<header class="col-span-full" style="z-index: 20;">
+49
appview/pages/templates/repo/fragments/backlinks.html
···
+
{{ define "repo/fragments/backlinks" }}
+
{{ if .Backlinks }}
+
<div id="at-uri-panel" class="px-2 md:px-0">
+
<div>
+
<span class="text-sm py-1 font-bold text-gray-500 dark:text-gray-400">Referenced by</span>
+
</div>
+
<ul>
+
{{ range .Backlinks }}
+
<li>
+
{{ $repoOwner := resolve .Handle }}
+
{{ $repoName := .Repo }}
+
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
+
<div class="flex flex-col">
+
<div class="flex gap-2 items-center">
+
{{ if .State.IsClosed }}
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ i "ban" "w-4 h-4" }}
+
</span>
+
{{ else if eq .Kind.String "issues" }}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "circle-dot" "w-4 h-4" }}
+
</span>
+
{{ else if .State.IsOpen }}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "git-pull-request" "w-4 h-4" }}
+
</span>
+
{{ else if .State.IsMerged }}
+
<span class="text-purple-600 dark:text-purple-500">
+
{{ i "git-merge" "w-4 h-4" }}
+
</span>
+
{{ else }}
+
<span class="text-gray-600 dark:text-gray-300">
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
+
</span>
+
{{ end }}
+
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
+
</div>
+
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
+
<div>
+
<span>on <a href="/{{ $repoUrl }}">{{ $repoUrl }}</a></span>
+
</div>
+
{{ end }}
+
</div>
+
</li>
+
{{ end }}
+
</ul>
+
</div>
+
{{ end }}
+
{{ end }}
+15 -1
appview/pages/templates/repo/fragments/editLabelPanel.html
···
{{ $fieldName := $def.AtUri }}
{{ $valueType := $def.ValueType }}
{{ $value := .value }}
+
{{ if $valueType.IsDidFormat }}
{{ $value = trimPrefix (resolve .value) "@" }}
+
<actor-typeahead>
+
<input
+
autocapitalize="none"
+
autocorrect="off"
+
autocomplete="off"
+
placeholder="user.tngl.sh"
+
value="{{$value}}"
+
name="{{$fieldName}}"
+
type="text"
+
class="p-1 w-full text-sm"
+
/>
+
</actor-typeahead>
+
{{ else }}
+
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
{{ end }}
-
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
{{ end }}
{{ define "nullTypeInput" }}
+1 -16
appview/pages/templates/repo/fragments/participants.html
···
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
</div>
-
<div class="flex items-center -space-x-3 mt-2">
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
-
{{ range $i, $p := $ps }}
-
<img
-
src="{{ tinyAvatar . }}"
-
alt=""
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
-
/>
-
{{ end }}
-
-
{{ if gt (len $all) 5 }}
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
-
+{{ sub (len $all) 5 }}
-
</span>
-
{{ end }}
-
</div>
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }}
</div>
{{ end }}
+31 -9
appview/pages/templates/repo/index.html
···
{{ end }}
<div class="flex items-center justify-between pb-5">
{{ block "branchSelector" . }}{{ end }}
-
<div class="flex md:hidden items-center gap-2">
+
<div class="flex md:hidden items-center gap-3">
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
</a>
···
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap">
{{ range $value := .Languages }}
<div
-
class="flex flex-grow items-center gap-2 text-xs align-items-center justify-center"
+
class="flex items-center gap-2 text-xs align-items-center justify-center"
>
{{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }}
<div>{{ or $value.Name "Other" }}
···
{{ define "branchSelector" }}
<div class="flex gap-2 items-center justify-between w-full">
-
<div class="flex gap-2 items-center">
+
<div class="flex gap-2 items-stretch">
<select
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
<span
class="mx-1 before:content-['·'] before:select-none"
></span>
-
<span>
-
{{ $did := index $.EmailToDid .Author.Email }}
-
<a href="{{ if $did }}/{{ resolve $did }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
-
>{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ .Author.Name }}{{ end }}</a>
-
</span>
+
{{ template "attribution" (list . $.EmailToDid) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
{{ template "repo/fragments/time" .Committer.When }}
···
{{ end }}
</div>
</div>
+
{{ end }}
+
+
{{ define "attribution" }}
+
{{ $commit := index . 0 }}
+
{{ $map := index . 1 }}
+
<span class="flex items-center">
+
{{ $author := index $map $commit.Author.Email }}
+
{{ $coauthors := $commit.CoAuthors }}
+
{{ $all := list }}
+
+
{{ if $author }}
+
{{ $all = append $all $author }}
+
{{ end }}
+
{{ range $coauthors }}
+
{{ $co := index $map .Email }}
+
{{ if $co }}
+
{{ $all = append $all $co }}
+
{{ end }}
+
{{ end }}
+
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
+
class="no-underline hover:underline">
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
+
</a>
+
</span>
{{ end }}
{{ define "branchList" }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
{{ end }}
{{ define "timestamp" }}
-
<a href="#{{ .Comment.Id }}"
+
<a href="#comment-{{ .Comment.Id }}"
class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline"
-
id="{{ .Comment.Id }}">
+
id="comment-{{ .Comment.Id }}">
{{ if .Comment.Deleted }}
{{ template "repo/fragments/shortTimeAgo" .Comment.Deleted }}
{{ else if .Comment.Edited }}
+3
appview/pages/templates/repo/issues/issue.html
···
"Subject" $.Issue.AtUri
"State" $.Issue.Labels) }}
{{ template "repo/fragments/participants" $.Issue.Participants }}
+
{{ template "repo/fragments/backlinks"
+
(dict "RepoInfo" $.RepoInfo
+
"Backlinks" $.Backlinks) }}
{{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }}
</div>
</div>
+40 -23
appview/pages/templates/repo/log.html
···
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
{{ $grid := "grid grid-cols-14 gap-4" }}
<div class="{{ $grid }}">
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2">Author</div>
+
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-1"></div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
</div>
{{ range $index, $commit := .Commits }}
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
<div class="{{ $grid }} py-3">
-
<div class="align-top truncate col-span-2">
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
{{ if $did }}
-
{{ template "user/fragments/picHandleLink" $did }}
-
{{ else }}
-
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
-
{{ end }}
+
<div class="align-top col-span-3">
+
{{ template "attribution" (list $commit $.EmailToDid) }}
</div>
<div class="align-top font-mono flex items-start col-span-3">
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
<div class="align-top col-span-6">
<div>
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
+
{{ if gt (len $messageParts) 1 }}
<button class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button>
{{ end }}
···
</span>
{{ end }}
{{ end }}
+
+
<!-- ci status -->
+
<span class="text-xs">
+
{{ $pipeline := index $.Pipelines .Hash.String }}
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
+
{{ end }}
+
</span>
</div>
{{ if gt (len $messageParts) 1 }}
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
{{ end }}
-
</div>
-
<div class="align-top col-span-1">
-
<!-- ci status -->
-
{{ $pipeline := index $.Pipelines .Hash.String }}
-
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
-
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
-
{{ end }}
</div>
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">{{ template "repo/fragments/shortTimeAgo" $commit.Committer.When }}</div>
</div>
···
</a>
</span>
<span class="mx-2 before:content-['·'] before:select-none"></span>
-
<span>
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
<a href="{{ if $did }}/{{ $did }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
-
{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ $commit.Author.Name }}{{ end }}
-
</a>
-
</span>
+
{{ template "attribution" (list $commit $.EmailToDid) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
···
</div>
</section>
+
{{ end }}
+
+
{{ define "attribution" }}
+
{{ $commit := index . 0 }}
+
{{ $map := index . 1 }}
+
<span class="flex items-center gap-1">
+
{{ $author := index $map $commit.Author.Email }}
+
{{ $coauthors := $commit.CoAuthors }}
+
{{ $all := list }}
+
+
{{ if $author }}
+
{{ $all = append $all $author }}
+
{{ end }}
+
{{ range $coauthors }}
+
{{ $co := index $map .Email }}
+
{{ if $co }}
+
{{ $all = append $all $co }}
+
{{ end }}
+
{{ end }}
+
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
+
class="no-underline hover:underline">
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
+
</a>
+
</span>
{{ end }}
{{ define "repoAfter" }}
+3
appview/pages/templates/repo/pulls/pull.html
···
"Subject" $.Pull.AtUri
"State" $.Pull.Labels) }}
{{ template "repo/fragments/participants" $.Pull.Participants }}
+
{{ template "repo/fragments/backlinks"
+
(dict "RepoInfo" $.RepoInfo
+
"Backlinks" $.Backlinks) }}
{{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }}
</div>
</div>
+3 -1
appview/pages/templates/user/followers.html
···
"FollowersCount" .FollowersCount
"FollowingCount" .FollowingCount) }}
{{ else }}
-
<p class="px-6 dark:text-white">This user does not have any followers yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span>This user does not have any followers yet.</span>
+
</div>
{{ end }}
</div>
{{ end }}
+3 -1
appview/pages/templates/user/following.html
···
"FollowersCount" .FollowersCount
"FollowingCount" .FollowingCount) }}
{{ else }}
-
<p class="px-6 dark:text-white">This user does not follow anyone yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span>This user does not follow anyone yet.</span>
+
</div>
{{ end }}
</div>
{{ end }}
+20 -2
appview/pages/templates/user/overview.html
···
<p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p>
<div class="flex flex-col gap-4 relative">
{{ if .ProfileTimeline.IsEmpty }}
-
<p class="dark:text-white">This user does not have any activity yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span class="flex items-center gap-2">
+
This user does not have any activity yet.
+
</span>
+
</div>
{{ end }}
{{ with .ProfileTimeline }}
···
</p>
<div class="flex flex-col gap-1">
+
{{ block "commits" .Commits }} {{ end }}
{{ block "repoEvents" .RepoEvents }} {{ end }}
{{ block "issueEvents" .IssueEvents }} {{ end }}
{{ block "pullEvents" .PullEvents }} {{ end }}
···
{{ end }}
{{ end }}
</div>
+
{{ end }}
+
+
{{ define "commits" }}
+
{{ if . }}
+
<div class="flex flex-wrap items-center gap-1">
+
{{ i "git-commit-horizontal" "size-5" }}
+
created {{ . }} commits
+
</div>
+
{{ end }}
{{ end }}
{{ define "repoEvents" }}
···
{{ template "user/fragments/repoCard" (list $ . false) }}
</div>
{{ else }}
-
<p class="dark:text-white">This user does not have any pinned repos.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span class="flex items-center gap-2">
+
This user does not have any pinned repos.
+
</span>
+
</div>
{{ end }}
</div>
</div>
+3 -1
appview/pages/templates/user/repos.html
···
{{ template "user/fragments/repoCard" (list $ . false) }}
</div>
{{ else }}
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span>This user does not have any repos yet.</span>
+
</div>
{{ end }}
</div>
{{ end }}
+9 -6
appview/pages/templates/user/signup.html
···
page to complete your registration.
</span>
<div class="w-full mt-4 text-center">
-
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
</div>
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
<span>join now</span>
</button>
+
<p class="text-sm text-gray-500">
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
+
</p>
+
+
<p id="signup-msg" class="error w-full"></p>
+
<p class="text-sm text-gray-500 pt-4">
+
By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>.
+
</p>
</form>
-
<p class="text-sm text-gray-500">
-
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
-
</p>
-
-
<p id="signup-msg" class="error w-full"></p>
</main>
</body>
</html>
+3 -1
appview/pages/templates/user/starred.html
···
{{ template "user/fragments/repoCard" (list $ . true) }}
</div>
{{ else }}
-
<p class="px-6 dark:text-white">This user does not have any starred repos yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span>This user does not have any starred repos yet.</span>
+
</div>
{{ end }}
</div>
{{ end }}
+3 -1
appview/pages/templates/user/strings.html
···
{{ template "singleString" (list $ .) }}
</div>
{{ else }}
-
<p class="px-6 dark:text-white">This user does not have any strings yet.</p>
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
+
<span>This user does not have any strings yet.</span>
+
</div>
{{ end }}
</div>
{{ end }}
+12 -11
appview/pipelines/pipelines.go
···
"tangled.org/core/appview/reporesolver"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
spindlemodel "tangled.org/core/spindle/models"
···
ps, err := db.GetPipelineStatuses(
p.db,
30,
-
db.FilterEq("repo_owner", f.Did),
-
db.FilterEq("repo_name", f.Name),
-
db.FilterEq("knot", f.Knot),
+
orm.FilterEq("repo_owner", f.Did),
+
orm.FilterEq("repo_name", f.Name),
+
orm.FilterEq("knot", f.Knot),
)
if err != nil {
l.Error("failed to query db", "err", err)
···
ps, err := db.GetPipelineStatuses(
p.db,
1,
-
db.FilterEq("repo_owner", f.Did),
-
db.FilterEq("repo_name", f.Name),
-
db.FilterEq("knot", f.Knot),
-
db.FilterEq("id", pipelineId),
+
orm.FilterEq("repo_owner", f.Did),
+
orm.FilterEq("repo_name", f.Name),
+
orm.FilterEq("knot", f.Knot),
+
orm.FilterEq("id", pipelineId),
)
if err != nil {
l.Error("failed to query db", "err", err)
···
ps, err := db.GetPipelineStatuses(
p.db,
1,
-
db.FilterEq("repo_owner", f.Did),
-
db.FilterEq("repo_name", f.Name),
-
db.FilterEq("knot", f.Knot),
-
db.FilterEq("id", pipelineId),
+
orm.FilterEq("repo_owner", f.Did),
+
orm.FilterEq("repo_name", f.Name),
+
orm.FilterEq("knot", f.Knot),
+
orm.FilterEq("id", pipelineId),
)
if err != nil || len(ps) != 1 {
l.Error("pipeline query failed", "err", err, "count", len(ps))
+3 -2
appview/pulls/opengraph.go
···
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/ogcard"
+
"tangled.org/core/orm"
"tangled.org/core/patchutil"
"tangled.org/core/types"
)
···
}
// Get comment count from database
-
comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID))
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
if err != nil {
log.Printf("failed to get pull comments: %v", err)
}
···
filesChanged = niceDiff.Stat.FilesChanged
}
-
card, err := s.drawPullSummaryCard(pull, &f.Repo, commentCount, diffStats, filesChanged)
+
card, err := s.drawPullSummaryCard(pull, f, commentCount, diffStats, filesChanged)
if err != nil {
log.Println("failed to draw pull summary card", err)
http.Error(w, "failed to draw pull summary card", http.StatusInternalServerError)
+85 -69
appview/pulls/pulls.go
···
package pulls
import (
+
"context"
"database/sql"
"encoding/json"
"errors"
···
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
pulls_indexer "tangled.org/core/appview/indexer/pulls"
+
"tangled.org/core/appview/mentions"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/appview/oauth"
···
"tangled.org/core/appview/validator"
"tangled.org/core/appview/xrpcclient"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/patchutil"
"tangled.org/core/rbac"
"tangled.org/core/tid"
···
)
type Pulls struct {
-
oauth *oauth.OAuth
-
repoResolver *reporesolver.RepoResolver
-
pages *pages.Pages
-
idResolver *idresolver.Resolver
-
db *db.DB
-
config *config.Config
-
notifier notify.Notifier
-
enforcer *rbac.Enforcer
-
logger *slog.Logger
-
validator *validator.Validator
-
indexer *pulls_indexer.Indexer
+
oauth *oauth.OAuth
+
repoResolver *reporesolver.RepoResolver
+
pages *pages.Pages
+
idResolver *idresolver.Resolver
+
mentionsResolver *mentions.Resolver
+
db *db.DB
+
config *config.Config
+
notifier notify.Notifier
+
enforcer *rbac.Enforcer
+
logger *slog.Logger
+
validator *validator.Validator
+
indexer *pulls_indexer.Indexer
}
func New(
···
repoResolver *reporesolver.RepoResolver,
pages *pages.Pages,
resolver *idresolver.Resolver,
+
mentionsResolver *mentions.Resolver,
db *db.DB,
config *config.Config,
notifier notify.Notifier,
···
logger *slog.Logger,
) *Pulls {
return &Pulls{
-
oauth: oauth,
-
repoResolver: repoResolver,
-
pages: pages,
-
idResolver: resolver,
-
db: db,
-
config: config,
-
notifier: notifier,
-
enforcer: enforcer,
-
logger: logger,
-
validator: validator,
-
indexer: indexer,
+
oauth: oauth,
+
repoResolver: repoResolver,
+
pages: pages,
+
idResolver: resolver,
+
mentionsResolver: mentionsResolver,
+
db: db,
+
config: config,
+
notifier: notifier,
+
enforcer: enforcer,
+
logger: logger,
+
validator: validator,
+
indexer: indexer,
}
}
···
return
}
-
mergeCheckResponse := s.mergeCheck(r, &f.Repo, pull, stack)
-
branchDeleteStatus := s.branchDeleteStatus(r, &f.Repo, pull)
+
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
+
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
resubmitResult := pages.Unknown
if user.Did == pull.OwnerDid {
-
resubmitResult = s.resubmitCheck(r, &f.Repo, pull, stack)
+
resubmitResult = s.resubmitCheck(r, f, pull, stack)
}
s.pages.PullActionsFragment(w, pages.PullActionsParams{
···
return
}
+
backlinks, err := db.GetBacklinks(s.db, pull.AtUri())
+
if err != nil {
+
log.Println("failed to get pull backlinks", err)
+
s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.")
+
return
+
}
+
// can be nil if this pull is not stacked
stack, _ := r.Context().Value("stack").(models.Stack)
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull)
-
mergeCheckResponse := s.mergeCheck(r, &f.Repo, pull, stack)
-
branchDeleteStatus := s.branchDeleteStatus(r, &f.Repo, pull)
+
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
+
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
resubmitResult := pages.Unknown
if user != nil && user.Did == pull.OwnerDid {
-
resubmitResult = s.resubmitCheck(r, &f.Repo, pull, stack)
+
resubmitResult = s.resubmitCheck(r, f, pull, stack)
}
m := make(map[string]models.Pipeline)
···
ps, err := db.GetPipelineStatuses(
s.db,
len(shas),
-
db.FilterEq("repo_owner", f.Did),
-
db.FilterEq("repo_name", f.Name),
-
db.FilterEq("knot", f.Knot),
-
db.FilterIn("sha", shas),
+
orm.FilterEq("repo_owner", f.Did),
+
orm.FilterEq("repo_name", f.Name),
+
orm.FilterEq("knot", f.Knot),
+
orm.FilterIn("sha", shas),
)
if err != nil {
log.Printf("failed to fetch pipeline statuses: %s", err)
···
labelDefs, err := db.GetLabelDefinitions(
s.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", tangled.RepoPullNSID),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", tangled.RepoPullNSID),
)
if err != nil {
log.Println("failed to fetch labels", err)
···
Pull: pull,
Stack: stack,
AbandonedPulls: abandonedPulls,
+
Backlinks: backlinks,
BranchDeleteStatus: branchDeleteStatus,
MergeCheck: mergeCheckResponse,
ResubmitCheck: resubmitResult,
···
pulls, err := db.GetPulls(
s.db,
-
db.FilterIn("id", ids),
+
orm.FilterIn("id", ids),
)
if err != nil {
log.Println("failed to get pulls", err)
···
ps, err := db.GetPipelineStatuses(
s.db,
len(shas),
-
db.FilterEq("repo_owner", f.Did),
-
db.FilterEq("repo_name", f.Name),
-
db.FilterEq("knot", f.Knot),
-
db.FilterIn("sha", shas),
+
orm.FilterEq("repo_owner", f.Did),
+
orm.FilterEq("repo_name", f.Name),
+
orm.FilterEq("knot", f.Knot),
+
orm.FilterIn("sha", shas),
)
if err != nil {
log.Printf("failed to fetch pipeline statuses: %s", err)
···
labelDefs, err := db.GetLabelDefinitions(
s.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", tangled.RepoPullNSID),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", tangled.RepoPullNSID),
)
if err != nil {
log.Println("failed to fetch labels", err)
···
}
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
-
l := s.logger.With("handler", "PullComment")
user := s.oauth.GetUser(r)
f, err := s.repoResolver.Resolve(r)
if err != nil {
···
return
}
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
+
// Start a transaction
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
···
Body: body,
CommentAt: atResp.Uri,
SubmissionId: pull.Submissions[roundNumber].ID,
+
Mentions: mentions,
+
References: references,
}
// Create the pull comment in the database with the commentAt field
···
return
}
-
rawMentions := markup.FindUserMentions(comment.Body)
-
idents := s.idResolver.ResolveIdents(r.Context(), rawMentions)
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
-
var mentions []syntax.DID
-
for _, ident := range idents {
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
-
mentions = append(mentions, ident.DID)
-
}
-
}
s.notifier.NewPullComment(r.Context(), comment, mentions)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId))
return
}
···
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
return
}
-
s.handleBranchBasedPull(w, r, &f.Repo, user, title, body, targetBranch, sourceBranch, isStacked)
+
s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch, isStacked)
} else if isForkBased {
if !caps.PullRequests.ForkSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
return
}
-
s.handleForkBasedPull(w, r, &f.Repo, user, fromFork, title, body, targetBranch, sourceBranch, isStacked)
+
s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch, isStacked)
} else if isPatchBased {
if !caps.PullRequests.PatchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
return
}
-
s.handlePatchBasedPull(w, r, &f.Repo, user, title, body, targetBranch, patch, isStacked)
+
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch, isStacked)
}
return
}
···
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
+
rkey := tid.TID()
initialSubmission := models.PullSubmission{
Patch: patch,
···
OwnerDid: user.Did,
RepoAt: repo.RepoAt(),
Rkey: rkey,
+
Mentions: mentions,
+
References: references,
Submissions: []*models.PullSubmission{
&initialSubmission,
},
···
// build a stack out of this patch
stackId := uuid.New()
-
stack, err := newStack(repo, user, targetBranch, patch, pullSource, stackId.String())
+
stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String())
if err != nil {
log.Println("failed to create stack", err)
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
···
// fork repo
repo, err := db.GetRepo(
s.db,
-
db.FilterEq("did", forkOwnerDid),
-
db.FilterEq("name", forkName),
+
orm.FilterEq("did", forkOwnerDid),
+
orm.FilterEq("name", forkName),
if err != nil {
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
patch := r.FormValue("patch")
-
s.resubmitPullHelper(w, r, &f.Repo, user, pull, patch, "", "")
+
s.resubmitPullHelper(w, r, f, user, pull, patch, "", "")
func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
···
patch := comparison.FormatPatchRaw
combined := comparison.CombinedPatchRaw
-
s.resubmitPullHelper(w, r, &f.Repo, user, pull, patch, combined, sourceRev)
+
s.resubmitPullHelper(w, r, f, user, pull, patch, combined, sourceRev)
func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
···
patch := comparison.FormatPatchRaw
combined := comparison.CombinedPatchRaw
-
s.resubmitPullHelper(w, r, &f.Repo, user, pull, patch, combined, sourceRev)
+
s.resubmitPullHelper(w, r, f, user, pull, patch, combined, sourceRev)
func (s *Pulls) resubmitPullHelper(
···
targetBranch := pull.TargetBranch
origStack, _ := r.Context().Value("stack").(models.Stack)
-
newStack, err := newStack(repo, user, targetBranch, patch, pull.PullSource, stackId)
+
newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId)
if err != nil {
log.Println("failed to create resubmitted stack", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
tx,
p.ParentChangeId,
// these should be enough filters to be unique per-stack
-
db.FilterEq("repo_at", p.RepoAt.String()),
-
db.FilterEq("owner_did", p.OwnerDid),
-
db.FilterEq("change_id", p.ChangeId),
+
orm.FilterEq("repo_at", p.RepoAt.String()),
+
orm.FilterEq("owner_did", p.OwnerDid),
+
orm.FilterEq("change_id", p.ChangeId),
if err != nil {
···
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
···
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
···
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
-
func newStack(repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
+
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
formatPatches, err := patchutil.ExtractPatches(patch)
if err != nil {
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
body := fp.Body
rkey := tid.TID()
+
mentions, references := s.mentionsResolver.Resolve(ctx, body)
+
initialSubmission := models.PullSubmission{
Patch: fp.Raw,
SourceRev: fp.SHA,
···
OwnerDid: user.Did,
RepoAt: repo.RepoAt(),
Rkey: rkey,
+
Mentions: mentions,
+
References: references,
Submissions: []*models.PullSubmission{
&initialSubmission,
},
+12 -11
appview/repo/artifact.go
···
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/orm"
"tangled.org/core/tid"
"tangled.org/core/types"
···
return
}
-
tag, err := rp.resolveTag(r.Context(), &f.Repo, tagParam)
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
tagParam := chi.URLParam(r, "tag")
filename := chi.URLParam(r, "file")
-
tag, err := rp.resolveTag(r.Context(), &f.Repo, tagParam)
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
artifacts, err := db.GetArtifact(
rp.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("tag", tag.Tag.Hash[:]),
-
db.FilterEq("name", filename),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("tag", tag.Tag.Hash[:]),
+
orm.FilterEq("name", filename),
)
if err != nil {
log.Println("failed to get artifacts", err)
···
artifacts, err := db.GetArtifact(
rp.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("tag", tag[:]),
-
db.FilterEq("name", filename),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("tag", tag[:]),
+
orm.FilterEq("name", filename),
)
if err != nil {
log.Println("failed to get artifacts", err)
···
defer tx.Rollback()
err = db.DeleteArtifact(tx,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("tag", artifact.Tag[:]),
-
db.FilterEq("name", filename),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("tag", artifact.Tag[:]),
+
orm.FilterEq("name", filename),
)
if err != nil {
log.Println("failed to remove artifact record from db", err)
+4 -4
appview/repo/blob.go
···
xrpcc := &indigoxrpc.Client{
Host: host,
}
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Repo.Name)
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
···
return
}
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
// Use XRPC response directly instead of converting to internal types
var breadcrumbs [][]string
···
}
// Create the blob view
-
blobView := NewBlobView(resp, rp.config, &f.Repo, ref, filePath, r.URL.Query())
+
blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query())
user := rp.oauth.GetUser(r)
···
if !rp.config.Core.Dev {
scheme = "https"
}
-
repo := fmt.Sprintf("%s/%s", f.Did, f.Repo.Name)
+
repo := f.DidSlashRepo()
baseURL := &url.URL{
Scheme: scheme,
Host: f.Knot,
+4 -3
appview/repo/feed.go
···
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
+
"tangled.org/core/orm"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
···
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
const feedLimitPerType = 100
-
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", repo.RepoAt()))
+
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt()))
if err != nil {
return nil, err
}
···
issues, err := db.GetIssuesPaginated(
rp.db,
pagination.Page{Limit: feedLimitPerType},
-
db.FilterEq("repo_at", repo.RepoAt()),
+
orm.FilterEq("repo_at", repo.RepoAt()),
)
if err != nil {
return nil, err
···
}
ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Name
-
feed, err := rp.getRepoFeed(r.Context(), &f.Repo, ownerSlashRepo)
+
feed, err := rp.getRepoFeed(r.Context(), f, ownerSlashRepo)
if err != nil {
log.Println("failed to get repo feed:", err)
rp.pages.Error500(w)
+7 -6
appview/repo/index.go
···
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/orm"
"tangled.org/core/types"
"github.com/go-chi/chi/v5"
···
user := rp.oauth.GetUser(r)
// Build index response from multiple XRPC calls
-
result, err := rp.buildIndexResponse(r.Context(), xrpcc, &f.Repo, ref)
+
result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
if errors.Is(xrpcerr, xrpcclient.ErrXrpcUnsupported) {
l.Error("failed to call XRPC repo.index", "err", err)
···
l.Error("failed to get email to did map", "err", err)
}
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc)
if err != nil {
l.Error("failed to GetVerifiedObjectCommits", "err", err)
}
// TODO: a bit dirty
-
languageInfo, err := rp.getLanguageInfo(r.Context(), l, &f.Repo, xrpcc, result.Ref, ref == "")
+
languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, xrpcc, result.Ref, ref == "")
if err != nil {
l.Warn("failed to compute language percentages", "err", err)
// non-fatal
···
for _, c := range commitsTrunc {
shas = append(shas, c.Hash.String())
}
-
pipelines, err := getPipelineStatuses(rp.db, &f.Repo, shas)
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
if err != nil {
l.Error("failed to fetch pipeline statuses", "err", err)
// non-fatal
···
// first attempt to fetch from db
langs, err := db.GetRepoLanguages(
rp.db,
-
db.FilterEq("repo_at", repo.RepoAt()),
-
db.FilterEq("ref", currentRef),
+
orm.FilterEq("repo_at", repo.RepoAt()),
+
orm.FilterEq("ref", currentRef),
)
if err != nil || langs == nil {
+4 -4
appview/repo/log.go
···
l.Error("failed to fetch email to did mapping", "err", err)
}
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits)
if err != nil {
l.Error("failed to GetVerifiedObjectCommits", "err", err)
}
···
for _, c := range xrpcResp.Commits {
shas = append(shas, c.Hash.String())
}
-
pipelines, err := getPipelineStatuses(rp.db, &f.Repo, shas)
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
if err != nil {
l.Error("failed to getPipelineStatuses", "err", err)
// non-fatal
···
l.Error("failed to get email to did mapping", "err", err)
}
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit})
if err != nil {
l.Error("failed to GetVerifiedCommits", "err", err)
}
user := rp.oauth.GetUser(r)
-
pipelines, err := getPipelineStatuses(rp.db, &f.Repo, []string{result.Diff.Commit.This})
+
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
if err != nil {
l.Error("failed to getPipelineStatuses", "err", err)
// non-fatal
+4 -3
appview/repo/opengraph.go
···
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/ogcard"
+
"tangled.org/core/orm"
"tangled.org/core/types"
)
···
var languageStats []types.RepoLanguageDetails
langs, err := db.GetRepoLanguages(
rp.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("is_default_ref", 1),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("is_default_ref", 1),
)
if err != nil {
log.Printf("failed to get language stats from db: %v", err)
···
})
}
-
card, err := rp.drawRepoSummaryCard(&f.Repo, languageStats)
+
card, err := rp.drawRepoSummaryCard(f, languageStats)
if err != nil {
log.Println("failed to draw repo summary card", err)
http.Error(w, "failed to draw repo summary card", http.StatusInternalServerError)
+26 -25
appview/repo/repo.go
···
xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
"tangled.org/core/xrpc/serviceauth"
···
}
}
-
newRepo := f.Repo
+
newRepo := *f
newRepo.Spindle = newSpindle
record := newRepo.AsRecord()
···
l.Info("wrote label record to PDS")
// update the repo to subscribe to this label
-
newRepo := f.Repo
+
newRepo := *f
newRepo.Labels = append(newRepo.Labels, aturi)
repoRecord := newRepo.AsRecord()
···
// get form values
labelId := r.FormValue("label-id")
-
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
+
label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId))
if err != nil {
fail("Failed to find label definition.", err)
return
···
}
// update repo record to remove the label reference
-
newRepo := f.Repo
+
newRepo := *f
var updated []string
removedAt := label.AtUri().String()
for _, l := range newRepo.Labels {
···
err = db.UnsubscribeLabel(
tx,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("label_at", removedAt),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterEq("label_at", removedAt),
)
if err != nil {
fail("Failed to unsubscribe label.", err)
return
}
-
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
+
err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id))
if err != nil {
fail("Failed to delete label definition.", err)
return
···
}
labelAts := r.Form["label"]
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
if err != nil {
fail("Failed to subscribe to label.", err)
return
}
-
newRepo := f.Repo
+
newRepo := *f
newRepo.Labels = append(newRepo.Labels, labelAts...)
// dedup
···
return
}
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
if err != nil {
fail("Failed to update labels, no record found on PDS.", err)
return
···
}
labelAts := r.Form["label"]
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
if err != nil {
fail("Failed to unsubscribe to label.", err)
return
}
// update repo record to remove the label reference
-
newRepo := f.Repo
+
newRepo := *f
var updated []string
for _, l := range newRepo.Labels {
if !slices.Contains(labelAts, l) {
···
return
}
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
if err != nil {
fail("Failed to update labels, no record found on PDS.", err)
return
···
err = db.UnsubscribeLabel(
rp.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterIn("label_at", labelAts),
+
orm.FilterEq("repo_at", f.RepoAt()),
+
orm.FilterIn("label_at", labelAts),
)
if err != nil {
fail("Failed to unsubscribe label.", err)
···
labelDefs, err := db.GetLabelDefinitions(
rp.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", subject.Collection().String()),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", subject.Collection().String()),
)
if err != nil {
l.Error("failed to fetch label defs", "err", err)
···
defs[l.AtUri().String()] = &l
}
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
if err != nil {
l.Error("failed to build label state", "err", err)
return
···
labelDefs, err := db.GetLabelDefinitions(
rp.db,
-
db.FilterIn("at_uri", f.Repo.Labels),
-
db.FilterContains("scope", subject.Collection().String()),
+
orm.FilterIn("at_uri", f.Labels),
+
orm.FilterContains("scope", subject.Collection().String()),
)
if err != nil {
l.Error("failed to fetch labels", "err", err)
···
defs[l.AtUri().String()] = &l
}
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
if err != nil {
l.Error("failed to build label state", "err", err)
return
···
// in the user's account.
existingRepo, err := db.GetRepo(
rp.db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("name", forkName),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("name", forkName),
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
···
uri = "http"
-
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Repo.Name)
+
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name)
l = l.With("cloneUrl", forkSourceUrl)
sourceAt := f.RepoAt().String()
···
Knot: targetKnot,
Rkey: rkey,
Source: sourceAt,
-
Description: f.Repo.Description,
+
Description: f.Description,
Created: time.Now(),
Labels: rp.config.Label.DefaultLabelDefs,
+16 -17
appview/repo/repo_util.go
···
package repo
import (
+
"maps"
"slices"
"sort"
"strings"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
"tangled.org/core/types"
-
-
"github.com/go-git/go-git/v5/plumbing/object"
)
func sortFiles(files []types.NiceTree) {
···
})
}
-
func uniqueEmails(commits []*object.Commit) []string {
+
func uniqueEmails(commits []types.Commit) []string {
emails := make(map[string]struct{})
for _, commit := range commits {
-
if commit.Author.Email != "" {
-
emails[commit.Author.Email] = struct{}{}
-
}
-
if commit.Committer.Email != "" {
-
emails[commit.Committer.Email] = struct{}{}
+
emails[commit.Author.Email] = struct{}{}
+
emails[commit.Committer.Email] = struct{}{}
+
for _, c := range commit.CoAuthors() {
+
emails[c.Email] = struct{}{}
}
}
-
var uniqueEmails []string
-
for email := range emails {
-
uniqueEmails = append(uniqueEmails, email)
-
}
-
return uniqueEmails
+
+
// delete empty emails if any, from the set
+
delete(emails, "")
+
+
return slices.Collect(maps.Keys(emails))
}
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
ps, err := db.GetPipelineStatuses(
d,
len(shas),
-
db.FilterEq("repo_owner", repo.Did),
-
db.FilterEq("repo_name", repo.Name),
-
db.FilterEq("knot", repo.Knot),
-
db.FilterIn("sha", shas),
+
orm.FilterEq("repo_owner", repo.Did),
+
orm.FilterEq("repo_name", repo.Name),
+
orm.FilterEq("knot", repo.Knot),
+
orm.FilterIn("sha", shas),
)
if err != nil {
return nil, err
+6 -5
appview/repo/settings.go
···
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/orm"
"tangled.org/core/types"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
return
}
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
if err != nil {
l.Error("failed to fetch labels", "err", err)
rp.pages.Error503(w)
return
}
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels))
if err != nil {
l.Error("failed to fetch labels", "err", err)
rp.pages.Error503(w)
···
labels = labels[:n]
subscribedLabels := make(map[string]struct{})
-
for _, l := range f.Repo.Labels {
+
for _, l := range f.Labels {
subscribedLabels[l] = struct{}{}
}
···
collaborators = append(collaborators, c)
}
return collaborators, nil
-
}(&f.Repo)
+
}(f)
if err != nil {
l.Error("failed to get collaborators", "err", err)
}
···
}
l.Debug("got", "topicsStr", topicStr, "topics", topics)
-
newRepo := f.Repo
+
newRepo := *f
newRepo.Description = description
newRepo.Website = website
newRepo.Topics = topics
+2 -1
appview/repo/tags.go
···
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/orm"
"tangled.org/core/types"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
···
rp.pages.Error503(w)
return
}
-
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt()))
+
artifacts, err := db.GetArtifact(rp.db, orm.FilterEq("repo_at", f.RepoAt()))
if err != nil {
l.Error("failed grab artifacts", "err", err)
return
+1 -1
appview/repo/tree.go
···
result.ReadmeFileName = xrpcResp.Readme.Filename
result.Readme = xrpcResp.Readme.Contents
}
-
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, &f.Repo)
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
if len(result.Files) == 0 && result.Parent == treePath {
+3 -9
appview/reporesolver/resolver.go
···
"tangled.org/core/rbac"
)
-
type ResolvedRepo struct {
-
models.Repo
-
}
-
type RepoResolver struct {
config *config.Config
enforcer *rbac.Enforcer
···
}
// TODO: move this out of `RepoResolver` struct
-
func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
+
func (rr *RepoResolver) Resolve(r *http.Request) (*models.Repo, error) {
repo, ok := r.Context().Value("repo").(*models.Repo)
if !ok {
log.Println("malformed middleware: `repo` not exist in context")
return nil, fmt.Errorf("malformed middleware")
}
-
return &ResolvedRepo{
-
Repo: *repo,
-
}, nil
+
return repo, nil
}
// 1. [x] replace `RepoInfo` to `reporesolver.GetRepoInfo(r *http.Request, repo, user)`
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
-
// 3. [ ] remove `ResolvedRepo`
+
// 3. [x] remove `ResolvedRepo`
// 4. [ ] replace reporesolver to reposervice
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo {
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
+5 -4
appview/serververify/verify.go
···
"tangled.org/core/api/tangled"
"tangled.org/core/appview/db"
"tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
)
···
// mark this spindle as verified in the db
rowId, err := db.VerifySpindle(
tx,
-
db.FilterEq("owner", owner),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", owner),
+
orm.FilterEq("instance", instance),
)
if err != nil {
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
// mark as registered
err = db.MarkRegistered(
tx,
-
db.FilterEq("did", owner),
-
db.FilterEq("domain", domain),
+
orm.FilterEq("did", owner),
+
orm.FilterEq("domain", domain),
)
if err != nil {
return fmt.Errorf("failed to register domain: %w", err)
+25 -24
appview/spindles/spindles.go
···
"tangled.org/core/appview/serververify"
"tangled.org/core/appview/xrpcclient"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
···
user := s.OAuth.GetUser(r)
all, err := db.GetSpindles(
s.Db,
-
db.FilterEq("owner", user.Did),
+
orm.FilterEq("owner", user.Did),
)
if err != nil {
s.Logger.Error("failed to fetch spindles", "err", err)
···
spindles, err := db.GetSpindles(
s.Db,
-
db.FilterEq("instance", instance),
-
db.FilterEq("owner", user.Did),
-
db.FilterIsNot("verified", "null"),
+
orm.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterIsNot("verified", "null"),
)
if err != nil || len(spindles) != 1 {
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
repos, err := db.GetRepos(
s.Db,
0,
-
db.FilterEq("spindle", instance),
+
orm.FilterEq("spindle", instance),
)
if err != nil {
l.Error("failed to get spindle repos", "err", err)
···
spindles, err := db.GetSpindles(
s.Db,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil || len(spindles) != 1 {
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
// remove spindle members first
err = db.RemoveSpindleMember(
tx,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil {
l.Error("failed to remove spindle members", "err", err)
···
err = db.DeleteSpindle(
tx,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil {
l.Error("failed to delete spindle", "err", err)
···
spindles, err := db.GetSpindles(
s.Db,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil || len(spindles) != 1 {
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
verifiedSpindle, err := db.GetSpindles(
s.Db,
-
db.FilterEq("id", rowId),
+
orm.FilterEq("id", rowId),
)
if err != nil || len(verifiedSpindle) != 1 {
l.Error("failed get new spindle", "err", err)
···
spindles, err := db.GetSpindles(
s.Db,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil || len(spindles) != 1 {
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
spindles, err := db.GetSpindles(
s.Db,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("instance", instance),
)
if err != nil || len(spindles) != 1 {
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
// get the record from the DB first:
members, err := db.GetSpindleMembers(
s.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("instance", instance),
-
db.FilterEq("subject", memberId.DID),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("instance", instance),
+
orm.FilterEq("subject", memberId.DID),
)
if err != nil || len(members) != 1 {
l.Error("failed to get member", "err", err)
···
// remove from db
if err = db.RemoveSpindleMember(
tx,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("instance", instance),
-
db.FilterEq("subject", memberId.DID),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("instance", instance),
+
orm.FilterEq("subject", memberId.DID),
); err != nil {
l.Error("failed to remove spindle member", "err", err)
fail()
+6 -5
appview/state/gfi.go
···
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pagination"
"tangled.org/core/consts"
+
"tangled.org/core/orm"
)
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
-
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
if err != nil {
log.Println("failed to get gfi label def", err)
s.pages.Error500(w)
return
}
-
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
+
repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
if err != nil {
log.Println("failed to get repo labels", err)
s.pages.Error503(w)
···
pagination.Page{
Limit: 500,
},
-
db.FilterIn("repo_at", repoUris),
-
db.FilterEq("open", 1),
+
orm.FilterIn("repo_at", repoUris),
+
orm.FilterEq("open", 1),
)
if err != nil {
log.Println("failed to get issues", err)
···
}
if len(uriList) > 0 {
-
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
if err != nil {
log.Println("failed to fetch labels", err)
}
+6 -5
appview/state/knotstream.go
···
ec "tangled.org/core/eventconsumer"
"tangled.org/core/eventconsumer/cursor"
"tangled.org/core/log"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/workflow"
···
knots, err := db.GetRegistrations(
d,
-
db.FilterIsNot("registered", "null"),
+
orm.FilterIsNot("registered", "null"),
)
if err != nil {
return nil, err
···
repos, err := db.GetRepos(
d,
0,
-
db.FilterEq("did", record.RepoDid),
-
db.FilterEq("name", record.RepoName),
+
orm.FilterEq("did", record.RepoDid),
+
orm.FilterEq("name", record.RepoName),
)
if err != nil {
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
repos, err := db.GetRepos(
d,
0,
-
db.FilterEq("did", record.TriggerMetadata.Repo.Did),
-
db.FilterEq("name", record.TriggerMetadata.Repo.Repo),
+
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
+
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
)
if err != nil {
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+22 -12
appview/state/profile.go
···
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
+
"tangled.org/core/orm"
)
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
return nil, fmt.Errorf("failed to get profile: %w", err)
}
-
repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did))
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
if err != nil {
return nil, fmt.Errorf("failed to get repo count: %w", err)
}
-
stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did))
+
stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did))
if err != nil {
return nil, fmt.Errorf("failed to get string count: %w", err)
}
-
starredCount, err := db.CountStars(s.db, db.FilterEq("did", did))
+
starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did))
if err != nil {
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
}
···
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
punchcard, err := db.MakePunchcard(
s.db,
-
db.FilterEq("did", did),
-
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
-
db.FilterLte("date", now.Format(time.DateOnly)),
+
orm.FilterEq("did", did),
+
orm.FilterGte("date", startOfYear.Format(time.DateOnly)),
+
orm.FilterLte("date", now.Format(time.DateOnly)),
)
if err != nil {
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
repos, err := db.GetRepos(
s.db,
0,
-
db.FilterEq("did", profile.UserDid),
+
orm.FilterEq("did", profile.UserDid),
)
if err != nil {
l.Error("failed to fetch repos", "err", err)
···
l.Error("failed to create timeline", "err", err)
}
+
// populate commit counts in the timeline, using the punchcard
+
currentMonth := time.Now().Month()
+
for _, p := range profile.Punchcard.Punches {
+
idx := currentMonth - p.Date.Month()
+
if int(idx) < len(timeline.ByMonth) {
+
timeline.ByMonth[idx].Commits += p.Count
+
}
+
}
+
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
LoggedInUser: s.oauth.GetUser(r),
Card: profile,
···
repos, err := db.GetRepos(
s.db,
0,
-
db.FilterEq("did", profile.UserDid),
+
orm.FilterEq("did", profile.UserDid),
)
if err != nil {
l.Error("failed to get repos", "err", err)
···
}
l = l.With("profileDid", profile.UserDid)
-
stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid))
+
stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid))
if err != nil {
l.Error("failed to get stars", "err", err)
s.pages.Error500(w)
···
}
l = l.With("profileDid", profile.UserDid)
-
strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid))
+
strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid))
if err != nil {
l.Error("failed to get strings", "err", err)
s.pages.Error500(w)
···
followDids = append(followDids, extractDid(follow))
}
-
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
+
profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids))
if err != nil {
l.Error("failed to get profiles", "followDids", followDids, "err", err)
return &params, err
···
log.Printf("getting profile data for %s: %s", user.Did, err)
}
-
repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did))
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
if err != nil {
log.Printf("getting repos for %s: %s", user.Did, err)
}
+2
appview/state/router.go
···
s.enforcer,
s.pages,
s.idResolver,
+
s.mentionsResolver,
s.db,
s.config,
s.notifier,
···
s.repoResolver,
s.pages,
s.idResolver,
+
s.mentionsResolver,
s.db,
s.config,
s.notifier,
+2 -1
appview/state/spindlestream.go
···
ec "tangled.org/core/eventconsumer"
"tangled.org/core/eventconsumer/cursor"
"tangled.org/core/log"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
spindle "tangled.org/core/spindle/models"
)
···
spindles, err := db.GetSpindles(
d,
-
db.FilterIsNot("verified", "null"),
+
orm.FilterIsNot("verified", "null"),
)
if err != nil {
return nil, err
+29 -23
appview/state/state.go
···
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
"tangled.org/core/appview/indexer"
+
"tangled.org/core/appview/mentions"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
dbnotify "tangled.org/core/appview/notify/db"
···
"tangled.org/core/jetstream"
"tangled.org/core/log"
tlog "tangled.org/core/log"
+
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
···
)
type State struct {
-
db *db.DB
-
notifier notify.Notifier
-
indexer *indexer.Indexer
-
oauth *oauth.OAuth
-
enforcer *rbac.Enforcer
-
pages *pages.Pages
-
idResolver *idresolver.Resolver
-
posthog posthog.Client
-
jc *jetstream.JetstreamClient
-
config *config.Config
-
repoResolver *reporesolver.RepoResolver
-
knotstream *eventconsumer.Consumer
-
spindlestream *eventconsumer.Consumer
-
logger *slog.Logger
-
validator *validator.Validator
+
db *db.DB
+
notifier notify.Notifier
+
indexer *indexer.Indexer
+
oauth *oauth.OAuth
+
enforcer *rbac.Enforcer
+
pages *pages.Pages
+
idResolver *idresolver.Resolver
+
mentionsResolver *mentions.Resolver
+
posthog posthog.Client
+
jc *jetstream.JetstreamClient
+
config *config.Config
+
repoResolver *reporesolver.RepoResolver
+
knotstream *eventconsumer.Consumer
+
spindlestream *eventconsumer.Consumer
+
logger *slog.Logger
+
validator *validator.Validator
}
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
repoResolver := reporesolver.New(config, enforcer, d)
+
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
+
wrapper := db.DbWrapper{Execer: d}
jc, err := jetstream.NewJetstreamClient(
config.Jetstream.Endpoint,
···
enforcer,
pages,
res,
+
mentionsResolver,
posthog,
jc,
config,
···
return
}
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
if err != nil {
// non-fatal
}
···
regs, err := db.GetRegistrations(
s.db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("needs_upgrade", 1),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("needs_upgrade", 1),
)
if err != nil {
l.Error("non-fatal: failed to get registrations", "err", err)
···
spindles, err := db.GetSpindles(
s.db,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("needs_upgrade", 1),
+
orm.FilterEq("owner", user.Did),
+
orm.FilterEq("needs_upgrade", 1),
)
if err != nil {
l.Error("non-fatal: failed to get spindles", "err", err)
···
// Check for existing repos
existingRepo, err := db.GetRepo(
s.db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("name", repoName),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("name", repoName),
)
if err == nil && existingRepo != nil {
l.Info("repo exists")
···
}
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
-
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
+
defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults))
if err != nil {
return err
}
+7 -6
appview/strings/strings.go
···
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pages/markup"
"tangled.org/core/idresolver"
+
"tangled.org/core/orm"
"tangled.org/core/tid"
"github.com/bluesky-social/indigo/api/atproto"
···
strings, err := db.GetStrings(
s.Db,
0,
-
db.FilterEq("did", id.DID),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", id.DID),
+
orm.FilterEq("rkey", rkey),
)
if err != nil {
l.Error("failed to fetch string", "err", err)
···
all, err := db.GetStrings(
s.Db,
0,
-
db.FilterEq("did", id.DID),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", id.DID),
+
orm.FilterEq("rkey", rkey),
)
if err != nil {
l.Error("failed to fetch string", "err", err)
···
if err := db.DeleteString(
s.Db,
-
db.FilterEq("did", user.Did),
-
db.FilterEq("rkey", rkey),
+
orm.FilterEq("did", user.Did),
+
orm.FilterEq("rkey", rkey),
); err != nil {
fail("Failed to delete string.", err)
return
+2 -1
appview/validator/issue.go
···
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
+
"tangled.org/core/orm"
)
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
// if comments have parents, only ingest ones that are 1 level deep
if comment.ReplyTo != nil {
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
if err != nil {
return fmt.Errorf("failed to fetch parent comment: %w", err)
}
+1 -34
crypto/verify.go
···
"crypto/sha256"
"encoding/base64"
"fmt"
-
"strings"
"github.com/hiddeco/sshsig"
"golang.org/x/crypto/ssh"
-
"tangled.org/core/types"
)
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
// to sha-512 for all key types anyway.
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
-
return err, err == nil
-
}
-
// VerifyCommitSignature reconstructs the payload used to sign a commit. This is
-
// essentially the git cat-file output but without the gpgsig header.
-
//
-
// Caveats: signature verification will fail on commits with more than one parent,
-
// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field
-
// and we are unable to reconstruct the payload correctly.
-
//
-
// Ideally this should directly operate on an *object.Commit.
-
func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) {
-
signature := commit.Commit.PGPSignature
-
-
author := bytes.NewBuffer([]byte{})
-
committer := bytes.NewBuffer([]byte{})
-
commit.Commit.Author.Encode(author)
-
commit.Commit.Committer.Encode(committer)
-
-
payload := strings.Builder{}
-
-
fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree)
-
if commit.Commit.Parent != "" {
-
fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent)
-
}
-
fmt.Fprintf(&payload, "author %s\n", author.String())
-
fmt.Fprintf(&payload, "committer %s\n", committer.String())
-
if commit.Commit.ChangedId != "" {
-
fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId)
-
}
-
fmt.Fprintf(&payload, "\n%s", commit.Commit.Message)
-
-
return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String()))
+
return err, err == nil
}
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+3 -3
flake.lock
···
},
"nixpkgs": {
"locked": {
-
"lastModified": 1751984180,
-
"narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=",
+
"lastModified": 1765186076,
+
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0",
+
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
-2
flake.nix
···
}).buildGoApplication;
modules = ./nix/gomod2nix.toml;
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
-
inherit (pkgs) gcc;
inherit sqlite-lib-src;
};
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
nativeBuildInputs = [
pkgs.go
pkgs.air
-
pkgs.tilt
pkgs.gopls
pkgs.httpie
pkgs.litecli
+1 -3
go.mod
···
module tangled.org/core
-
go 1.24.4
+
go 1.25.0
require (
github.com/Blank-Xu/sql-adapter v1.1.1
···
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v3 v3.3.3
github.com/whyrusleeping/cbor-gen v0.3.1
-
github.com/wyatt915/goldmark-treeblood v0.0.1
github.com/yuin/goldmark v1.7.13
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
-
github.com/wyatt915/treeblood v0.1.16 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-4
go.sum
···
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
-
github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs=
-
github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208=
-
github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y=
-
github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+15 -4
jetstream/jetstream.go
···
// existing instances of the closure when j.WantedDids is mutated
return func(ctx context.Context, evt *models.Event) error {
+
j.mu.RLock()
// empty filter => all dids allowed
-
if len(j.wantedDids) == 0 {
-
return processFunc(ctx, evt)
+
matches := len(j.wantedDids) == 0
+
if !matches {
+
if _, ok := j.wantedDids[evt.Did]; ok {
+
matches = true
+
}
}
+
j.mu.RUnlock()
-
if _, ok := j.wantedDids[evt.Did]; ok {
+
if matches {
return processFunc(ctx, evt)
} else {
return nil
···
go func() {
if j.waitForDid {
-
for len(j.wantedDids) == 0 {
+
for {
+
j.mu.RLock()
+
hasDid := len(j.wantedDids) != 0
+
j.mu.RUnlock()
+
if hasDid {
+
break
+
}
time.Sleep(time.Second)
}
}
+81
knotserver/db/db.go
···
+
package db
+
+
import (
+
"context"
+
"database/sql"
+
"log/slog"
+
"strings"
+
+
_ "github.com/mattn/go-sqlite3"
+
"tangled.org/core/log"
+
)
+
+
type DB struct {
+
db *sql.DB
+
logger *slog.Logger
+
}
+
+
func Setup(ctx context.Context, dbPath string) (*DB, error) {
+
// https://github.com/mattn/go-sqlite3#connection-string
+
opts := []string{
+
"_foreign_keys=1",
+
"_journal_mode=WAL",
+
"_synchronous=NORMAL",
+
"_auto_vacuum=incremental",
+
}
+
+
logger := log.FromContext(ctx)
+
logger = log.SubLogger(logger, "db")
+
+
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
+
if err != nil {
+
return nil, err
+
}
+
+
conn, err := db.Conn(ctx)
+
if err != nil {
+
return nil, err
+
}
+
defer conn.Close()
+
+
_, err = conn.ExecContext(ctx, `
+
create table if not exists known_dids (
+
did text primary key
+
);
+
+
create table if not exists public_keys (
+
id integer primary key autoincrement,
+
did text not null,
+
key text not null,
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
unique(did, key),
+
foreign key (did) references known_dids(did) on delete cascade
+
);
+
+
create table if not exists _jetstream (
+
id integer primary key autoincrement,
+
last_time_us integer not null
+
);
+
+
create table if not exists events (
+
rkey text not null,
+
nsid text not null,
+
event text not null, -- json
+
created integer not null default (strftime('%s', 'now')),
+
primary key (rkey, nsid)
+
);
+
+
create table if not exists migrations (
+
id integer primary key autoincrement,
+
name text unique
+
);
+
`)
+
if err != nil {
+
return nil, err
+
}
+
+
return &DB{
+
db: db,
+
logger: logger,
+
}, nil
+
}
-64
knotserver/db/init.go
···
-
package db
-
-
import (
-
"database/sql"
-
"strings"
-
-
_ "github.com/mattn/go-sqlite3"
-
)
-
-
type DB struct {
-
db *sql.DB
-
}
-
-
func Setup(dbPath string) (*DB, error) {
-
// https://github.com/mattn/go-sqlite3#connection-string
-
opts := []string{
-
"_foreign_keys=1",
-
"_journal_mode=WAL",
-
"_synchronous=NORMAL",
-
"_auto_vacuum=incremental",
-
}
-
-
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
-
if err != nil {
-
return nil, err
-
}
-
-
// NOTE: If any other migration is added here, you MUST
-
// copy the pattern in appview: use a single sql.Conn
-
// for every migration.
-
-
_, err = db.Exec(`
-
create table if not exists known_dids (
-
did text primary key
-
);
-
-
create table if not exists public_keys (
-
id integer primary key autoincrement,
-
did text not null,
-
key text not null,
-
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
-
unique(did, key),
-
foreign key (did) references known_dids(did) on delete cascade
-
);
-
-
create table if not exists _jetstream (
-
id integer primary key autoincrement,
-
last_time_us integer not null
-
);
-
-
create table if not exists events (
-
rkey text not null,
-
nsid text not null,
-
event text not null, -- json
-
created integer not null default (strftime('%s', 'now')),
-
primary key (rkey, nsid)
-
);
-
`)
-
if err != nil {
-
return nil, err
-
}
-
-
return &DB{db: db}, nil
-
}
+1 -17
knotserver/git/diff.go
···
nd.Diff = append(nd.Diff, ndiff)
}
-
nd.Stat.FilesChanged = len(diffs)
-
nd.Commit.This = c.Hash.String()
-
nd.Commit.PGPSignature = c.PGPSignature
-
nd.Commit.Committer = c.Committer
-
nd.Commit.Tree = c.TreeHash.String()
-
-
if parent.Hash.IsZero() {
-
nd.Commit.Parent = ""
-
} else {
-
nd.Commit.Parent = parent.Hash.String()
-
}
-
nd.Commit.Author = c.Author
-
nd.Commit.Message = c.Message
-
-
if v, ok := c.ExtraHeaders["change-id"]; ok {
-
nd.Commit.ChangedId = string(v)
-
}
+
nd.Commit.FromGoGitCommit(c)
return &nd, nil
}
+38 -2
knotserver/git/fork.go
···
import (
"errors"
"fmt"
+
"log/slog"
+
"net/url"
"os/exec"
+
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
+
knotconfig "tangled.org/core/knotserver/config"
)
-
func Fork(repoPath, source string) error {
-
cloneCmd := exec.Command("git", "clone", "--bare", source, repoPath)
+
func Fork(repoPath, source string, cfg *knotconfig.Config) error {
+
u, err := url.Parse(source)
+
if err != nil {
+
return fmt.Errorf("failed to parse source URL: %w", err)
+
}
+
+
if o := optimizeClone(u, cfg); o != nil {
+
u = o
+
}
+
+
cloneCmd := exec.Command("git", "clone", "--bare", u.String(), repoPath)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to bare clone repository: %w", err)
}
···
}
return nil
+
}
+
+
func optimizeClone(u *url.URL, cfg *knotconfig.Config) *url.URL {
+
// only optimize if it's the same host
+
if u.Host != cfg.Server.Hostname {
+
return nil
+
}
+
+
local := filepath.Join(cfg.Repo.ScanPath, u.Path)
+
+
// sanity check: is there a git repo there?
+
if _, err := PlainOpen(local); err != nil {
+
return nil
+
}
+
+
// create optimized file:// URL
+
optimized := &url.URL{
+
Scheme: "file",
+
Path: local,
+
}
+
+
slog.Debug("performing local clone", "url", optimized.String())
+
return optimized
}
func (g *GitRepo) Sync() error {
+1 -1
knotserver/server.go
···
logger.Info("running in dev mode, signature verification is disabled")
}
-
db, err := db.Setup(c.Server.DBPath)
+
db, err := db.Setup(ctx, c.Server.DBPath)
if err != nil {
return fmt.Errorf("failed to load db: %w", err)
}
+1 -1
knotserver/xrpc/create_repo.go
···
repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
if data.Source != nil && *data.Source != "" {
-
err = git.Fork(repoPath, *data.Source)
+
err = git.Fork(repoPath, *data.Source, h.Config)
if err != nil {
l.Error("forking repo", "error", err.Error())
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+6 -1
knotserver/xrpc/repo_log.go
···
return
}
+
tcommits := make([]types.Commit, len(commits))
+
for i, c := range commits {
+
tcommits[i].FromGoGitCommit(c)
+
}
+
// Create response using existing types.RepoLogResponse
response := types.RepoLogResponse{
-
Commits: commits,
+
Commits: tcommits,
Ref: ref,
Page: (offset / limit) + 1,
PerPage: limit,
+14
lexicons/issue/comment.json
···
"replyTo": {
"type": "string",
"format": "at-uri"
+
},
+
"mentions": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "did"
+
}
+
},
+
"references": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
}
}
}
}
+14
lexicons/issue/issue.json
···
"createdAt": {
"type": "string",
"format": "datetime"
+
},
+
"mentions": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "did"
+
}
+
},
+
"references": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
}
}
}
}
+14
lexicons/pulls/comment.json
···
"createdAt": {
"type": "string",
"format": "datetime"
+
},
+
"mentions": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "did"
+
}
+
},
+
"references": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
}
}
}
}
+14
lexicons/pulls/pull.json
···
"createdAt": {
"type": "string",
"format": "datetime"
+
},
+
"mentions": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "did"
+
}
+
},
+
"references": {
+
"type": "array",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
}
}
}
}
-30
nix/gomod2nix.toml
···
[mod."github.com/davecgh/go-spew"]
version = "v1.1.2-0.20180830191138-d8f796af33cc"
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
-
version = "v4.4.0"
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
[mod."github.com/dgraph-io/ristretto"]
version = "v0.2.0"
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
[mod."github.com/klauspost/cpuid/v2"]
version = "v2.3.0"
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
-
[mod."github.com/lestrrat-go/blackmagic"]
-
version = "v1.0.4"
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
-
[mod."github.com/lestrrat-go/httpcc"]
-
version = "v1.0.1"
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
-
[mod."github.com/lestrrat-go/httprc"]
-
version = "v1.0.6"
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
-
[mod."github.com/lestrrat-go/iter"]
-
version = "v1.0.2"
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
-
[mod."github.com/lestrrat-go/jwx/v2"]
-
version = "v2.1.6"
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
-
[mod."github.com/lestrrat-go/option"]
-
version = "v1.0.1"
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
[mod."github.com/lucasb-eyer/go-colorful"]
version = "v1.2.0"
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
[mod."github.com/ryanuber/go-glob"]
version = "v1.0.0"
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
-
[mod."github.com/segmentio/asm"]
-
version = "v1.2.0"
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
[mod."github.com/sergi/go-diff"]
version = "v1.1.0"
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
[mod."github.com/whyrusleeping/cbor-gen"]
version = "v0.3.1"
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
-
[mod."github.com/wyatt915/goldmark-treeblood"]
-
version = "v0.0.1"
-
hash = "sha256-hAVFaktO02MiiqZFffr8ZlvFEfwxw4Y84OZ2t7e5G7g="
-
[mod."github.com/wyatt915/treeblood"]
-
version = "v0.1.16"
-
hash = "sha256-T68sa+iVx0qY7dDjXEAJvRWQEGXYIpUsf9tcWwO1tIw="
[mod."github.com/xo/terminfo"]
version = "v0.0.0-20220910002029-abceb7e1c41e"
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
+2
nix/modules/knot.nix
···
Match User ${cfg.gitUser}
AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper
AuthorizedKeysCommandUser nobody
+
ChallengeResponseAuthentication no
+
PasswordAuthentication no
'';
};
+7 -5
nix/pkgs/sqlite-lib.nix
···
{
-
gcc,
stdenv,
sqlite-lib-src,
}:
stdenv.mkDerivation {
name = "sqlite-lib";
src = sqlite-lib-src;
-
nativeBuildInputs = [gcc];
+
buildPhase = ''
-
gcc -c sqlite3.c
-
ar rcs libsqlite3.a sqlite3.o
-
ranlib libsqlite3.a
+
$CC -c sqlite3.c
+
$AR rcs libsqlite3.a sqlite3.o
+
$RANLIB libsqlite3.a
+
'';
+
+
installPhase = ''
mkdir -p $out/include $out/lib
cp *.h $out/include
cp libsqlite3.a $out/lib
+122
orm/orm.go
···
+
package orm
+
+
import (
+
"context"
+
"database/sql"
+
"fmt"
+
"log/slog"
+
"reflect"
+
"strings"
+
)
+
+
type migrationFn = func(*sql.Tx) error
+
+
func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
+
logger = logger.With("migration", name)
+
+
tx, err := c.BeginTx(context.Background(), nil)
+
if err != nil {
+
return err
+
}
+
defer tx.Rollback()
+
+
var exists bool
+
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
+
if err != nil {
+
return err
+
}
+
+
if !exists {
+
// run migration
+
err = migrationFn(tx)
+
if err != nil {
+
logger.Error("failed to run migration", "err", err)
+
return err
+
}
+
+
// mark migration as complete
+
_, err = tx.Exec("insert into migrations (name) values (?)", name)
+
if err != nil {
+
logger.Error("failed to mark migration as complete", "err", err)
+
return err
+
}
+
+
// commit the transaction
+
if err := tx.Commit(); err != nil {
+
return err
+
}
+
+
logger.Info("migration applied successfully")
+
} else {
+
logger.Warn("skipped migration, already applied")
+
}
+
+
return nil
+
}
+
+
type Filter struct {
+
Key string
+
arg any
+
Cmp string
+
}
+
+
func newFilter(key, cmp string, arg any) Filter {
+
return Filter{
+
Key: key,
+
arg: arg,
+
Cmp: cmp,
+
}
+
}
+
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
+
func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
+
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
+
func FilterContains(key string, arg any) Filter {
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
+
}
+
+
func (f Filter) Condition() string {
+
rv := reflect.ValueOf(f.arg)
+
kind := rv.Kind()
+
+
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
+
if rv.Len() == 0 {
+
// always false
+
return "1 = 0"
+
}
+
+
placeholders := make([]string, rv.Len())
+
for i := range placeholders {
+
placeholders[i] = "?"
+
}
+
+
return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
+
}
+
+
return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
+
}
+
+
func (f Filter) Arg() []any {
+
rv := reflect.ValueOf(f.arg)
+
kind := rv.Kind()
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
+
if rv.Len() == 0 {
+
return nil
+
}
+
+
out := make([]any, rv.Len())
+
for i := range rv.Len() {
+
out[i] = rv.Index(i).Interface()
+
}
+
return out
+
}
+
+
return []any{f.arg}
+
}
-1
patchutil/patchutil.go
···
}
nd := types.NiceDiff{}
-
nd.Commit.Parent = targetBranch
for _, d := range diffs {
ndiff := types.Diff{}
+31
sets/gen.go
···
+
package sets
+
+
import (
+
"math/rand"
+
"reflect"
+
"testing/quick"
+
)
+
+
func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value {
+
s := New[T]()
+
+
var zero T
+
itemType := reflect.TypeOf(zero)
+
+
for {
+
if s.Len() >= size {
+
break
+
}
+
+
item, ok := quick.Value(itemType, rand)
+
if !ok {
+
continue
+
}
+
+
if val, ok := item.Interface().(T); ok {
+
s.Insert(val)
+
}
+
}
+
+
return reflect.ValueOf(s)
+
}
+35
sets/readme.txt
···
+
sets
+
----
+
set datastructure for go with generics and iterators. the
+
api is supposed to mimic rust's std::collections::HashSet api.
+
+
s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4}))
+
s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6}))
+
+
union := sets.Collect(s1.Union(s2))
+
intersect := sets.Collect(s1.Intersection(s2))
+
diff := sets.Collect(s1.Difference(s2))
+
symdiff := sets.Collect(s1.SymmetricDifference(s2))
+
+
s1.Len() // 4
+
s1.Contains(1) // true
+
s1.IsEmpty() // false
+
s1.IsSubset(s2) // true
+
s1.IsSuperset(s2) // false
+
s1.IsDisjoint(s2) // false
+
+
if exists := s1.Insert(1); exists {
+
// already existed in set
+
}
+
+
if existed := s1.Remove(1); existed {
+
// existed in set, now removed
+
}
+
+
+
testing
+
-------
+
includes property-based tests using the wonderful
+
testing/quick module!
+
+
go test -v
+174
sets/set.go
···
+
package sets
+
+
import (
+
"iter"
+
"maps"
+
)
+
+
type Set[T comparable] struct {
+
data map[T]struct{}
+
}
+
+
func New[T comparable]() Set[T] {
+
return Set[T]{
+
data: make(map[T]struct{}),
+
}
+
}
+
+
func (s *Set[T]) Insert(item T) bool {
+
_, exists := s.data[item]
+
s.data[item] = struct{}{}
+
return !exists
+
}
+
+
func Singleton[T comparable](item T) Set[T] {
+
n := New[T]()
+
_ = n.Insert(item)
+
return n
+
}
+
+
func (s *Set[T]) Remove(item T) bool {
+
_, exists := s.data[item]
+
if exists {
+
delete(s.data, item)
+
}
+
return exists
+
}
+
+
func (s Set[T]) Contains(item T) bool {
+
_, exists := s.data[item]
+
return exists
+
}
+
+
func (s Set[T]) Len() int {
+
return len(s.data)
+
}
+
+
func (s Set[T]) IsEmpty() bool {
+
return len(s.data) == 0
+
}
+
+
func (s *Set[T]) Clear() {
+
s.data = make(map[T]struct{})
+
}
+
+
func (s Set[T]) All() iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Clone() Set[T] {
+
return Set[T]{
+
data: maps.Clone(s.data),
+
}
+
}
+
+
func (s Set[T]) Union(other Set[T]) iter.Seq[T] {
+
if s.Len() >= other.Len() {
+
return chain(s.All(), other.Difference(s))
+
} else {
+
return chain(other.All(), s.Difference(other))
+
}
+
}
+
+
func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for _, seq := range seqs {
+
for item := range seq {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Difference(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
for item := range other.data {
+
if !s.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) IsSubset(other Set[T]) bool {
+
for item := range s.data {
+
if !other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func (s Set[T]) IsSuperset(other Set[T]) bool {
+
return other.IsSubset(s)
+
}
+
+
func (s Set[T]) IsDisjoint(other Set[T]) bool {
+
for item := range s.data {
+
if other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func (s Set[T]) Equal(other Set[T]) bool {
+
if s.Len() != other.Len() {
+
return false
+
}
+
for item := range s.data {
+
if !other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func Collect[T comparable](seq iter.Seq[T]) Set[T] {
+
result := New[T]()
+
for item := range seq {
+
result.Insert(item)
+
}
+
return result
+
}
+411
sets/set_test.go
···
+
package sets
+
+
import (
+
"slices"
+
"testing"
+
"testing/quick"
+
)
+
+
func TestNew(t *testing.T) {
+
s := New[int]()
+
if s.Len() != 0 {
+
t.Errorf("New set should be empty, got length %d", s.Len())
+
}
+
if !s.IsEmpty() {
+
t.Error("New set should be empty")
+
}
+
}
+
+
func TestFromSlice(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3, 2, 1}))
+
if s.Len() != 3 {
+
t.Errorf("Expected length 3, got %d", s.Len())
+
}
+
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
+
t.Error("Set should contain all unique elements from slice")
+
}
+
}
+
+
func TestInsert(t *testing.T) {
+
s := New[string]()
+
+
if !s.Insert("hello") {
+
t.Error("First insert should return true")
+
}
+
if s.Insert("hello") {
+
t.Error("Duplicate insert should return false")
+
}
+
if s.Len() != 1 {
+
t.Errorf("Expected length 1, got %d", s.Len())
+
}
+
}
+
+
func TestRemove(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s.Remove(2) {
+
t.Error("Remove existing element should return true")
+
}
+
if s.Remove(2) {
+
t.Error("Remove non-existing element should return false")
+
}
+
if s.Contains(2) {
+
t.Error("Element should be removed")
+
}
+
if s.Len() != 2 {
+
t.Errorf("Expected length 2, got %d", s.Len())
+
}
+
}
+
+
func TestContains(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s.Contains(1) {
+
t.Error("Should contain 1")
+
}
+
if s.Contains(4) {
+
t.Error("Should not contain 4")
+
}
+
}
+
+
func TestClear(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
s.Clear()
+
+
if !s.IsEmpty() {
+
t.Error("Set should be empty after clear")
+
}
+
if s.Len() != 0 {
+
t.Errorf("Expected length 0, got %d", s.Len())
+
}
+
}
+
+
func TestIterator(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
var items []int
+
+
for item := range s.All() {
+
items = append(items, item)
+
}
+
+
slices.Sort(items)
+
expected := []int{1, 2, 3}
+
if !slices.Equal(items, expected) {
+
t.Errorf("Expected %v, got %v", expected, items)
+
}
+
}
+
+
func TestClone(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := s1.Clone()
+
+
if !s1.Equal(s2) {
+
t.Error("Cloned set should be equal to original")
+
}
+
+
s2.Insert(4)
+
if s1.Contains(4) {
+
t.Error("Modifying clone should not affect original")
+
}
+
}
+
+
func TestUnion(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{2, 3}))
+
+
result := Collect(s1.Union(s2))
+
expected := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestIntersection(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{2, 3}))
+
result := Collect(s1.Intersection(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestDifference(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{1}))
+
result := Collect(s1.Difference(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestSymmetricDifference(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{1, 4}))
+
result := Collect(s1.SymmetricDifference(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestSymmetricDifferenceCommutativeProperty(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
result1 := Collect(s1.SymmetricDifference(s2))
+
result2 := Collect(s2.SymmetricDifference(s1))
+
+
if !result1.Equal(result2) {
+
t.Errorf("Expected %v, got %v", result1, result2)
+
}
+
}
+
+
func TestIsSubset(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s1.IsSubset(s2) {
+
t.Error("s1 should be subset of s2")
+
}
+
if s2.IsSubset(s1) {
+
t.Error("s2 should not be subset of s1")
+
}
+
}
+
+
func TestIsSuperset(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{1, 2}))
+
+
if !s1.IsSuperset(s2) {
+
t.Error("s1 should be superset of s2")
+
}
+
if s2.IsSuperset(s1) {
+
t.Error("s2 should not be superset of s1")
+
}
+
}
+
+
func TestIsDisjoint(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{3, 4}))
+
s3 := Collect(slices.Values([]int{2, 3}))
+
+
if !s1.IsDisjoint(s2) {
+
t.Error("s1 and s2 should be disjoint")
+
}
+
if s1.IsDisjoint(s3) {
+
t.Error("s1 and s3 should not be disjoint")
+
}
+
}
+
+
func TestEqual(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{3, 2, 1}))
+
s3 := Collect(slices.Values([]int{1, 2}))
+
+
if !s1.Equal(s2) {
+
t.Error("s1 and s2 should be equal")
+
}
+
if s1.Equal(s3) {
+
t.Error("s1 and s3 should not be equal")
+
}
+
}
+
+
func TestCollect(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{2, 3}))
+
+
unionSet := Collect(s1.Union(s2))
+
if unionSet.Len() != 3 {
+
t.Errorf("Expected union set length 3, got %d", unionSet.Len())
+
}
+
if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) {
+
t.Error("Union set should contain 1, 2, and 3")
+
}
+
+
diffSet := Collect(s1.Difference(s2))
+
if diffSet.Len() != 1 {
+
t.Errorf("Expected difference set length 1, got %d", diffSet.Len())
+
}
+
if !diffSet.Contains(1) {
+
t.Error("Difference set should contain 1")
+
}
+
}
+
+
func TestPropertySingleonLen(t *testing.T) {
+
f := func(item int) bool {
+
single := Singleton(item)
+
return single.Len() == 1
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyInsertIdempotent(t *testing.T) {
+
f := func(s Set[int], item int) bool {
+
clone := s.Clone()
+
+
clone.Insert(item)
+
firstLen := clone.Len()
+
+
clone.Insert(item)
+
secondLen := clone.Len()
+
+
return firstLen == secondLen
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyUnionCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
union1 := Collect(s1.Union(s2))
+
union2 := Collect(s2.Union(s1))
+
return union1.Equal(union2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIntersectionCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
inter1 := Collect(s1.Intersection(s2))
+
inter2 := Collect(s2.Intersection(s1))
+
return inter1.Equal(inter2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyCloneEquals(t *testing.T) {
+
f := func(s Set[int]) bool {
+
clone := s.Clone()
+
return s.Equal(clone)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIntersectionIsSubset(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
inter := Collect(s1.Intersection(s2))
+
return inter.IsSubset(s1) && inter.IsSubset(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyUnionIsSuperset(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
union := Collect(s1.Union(s2))
+
return union.IsSuperset(s1) && union.IsSuperset(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyDifferenceDisjoint(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
diff := Collect(s1.Difference(s2))
+
return diff.IsDisjoint(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertySymmetricDifferenceCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
symDiff1 := Collect(s1.SymmetricDifference(s2))
+
symDiff2 := Collect(s2.SymmetricDifference(s1))
+
return symDiff1.Equal(symDiff2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyRemoveWorks(t *testing.T) {
+
f := func(s Set[int], item int) bool {
+
clone := s.Clone()
+
clone.Insert(item)
+
clone.Remove(item)
+
return !clone.Contains(item)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyClearEmpty(t *testing.T) {
+
f := func(s Set[int]) bool {
+
s.Clear()
+
return s.IsEmpty() && s.Len() == 0
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIsSubsetReflexive(t *testing.T) {
+
f := func(s Set[int]) bool {
+
return s.IsSubset(s)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyDeMorganUnion(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool {
+
// create a universe that contains both sets
+
u := universe.Clone()
+
for item := range s1.All() {
+
u.Insert(item)
+
}
+
for item := range s2.All() {
+
u.Insert(item)
+
}
+
+
// (A u B)' = A' n B'
+
union := Collect(s1.Union(s2))
+
complementUnion := Collect(u.Difference(union))
+
+
complementS1 := Collect(u.Difference(s1))
+
complementS2 := Collect(u.Difference(s2))
+
intersectionComplements := Collect(complementS1.Intersection(complementS2))
+
+
return complementUnion.Equal(intersectionComplements)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+5 -3
spindle/engines/nixery/engine.go
···
workflowEnvs.AddEnv(s.Key, s.Value)
}
-
step := w.Steps[idx].(Step)
+
step := w.Steps[idx]
select {
case <-ctx.Done():
···
}
envs := append(EnvVars(nil), workflowEnvs...)
-
for k, v := range step.environment {
-
envs.AddEnv(k, v)
+
if nixStep, ok := step.(Step); ok {
+
for k, v := range nixStep.environment {
+
envs.AddEnv(k, v)
+
}
}
envs.AddEnv("HOME", homeDir)
+199
types/commit.go
···
+
package types
+
+
import (
+
"bytes"
+
"encoding/json"
+
"fmt"
+
"maps"
+
"regexp"
+
"strings"
+
+
"github.com/go-git/go-git/v5/plumbing"
+
"github.com/go-git/go-git/v5/plumbing/object"
+
)
+
+
type Commit struct {
+
// hash of the commit object.
+
Hash plumbing.Hash `json:"hash,omitempty"`
+
+
// author is the original author of the commit.
+
Author object.Signature `json:"author"`
+
+
// committer is the one performing the commit, might be different from author.
+
Committer object.Signature `json:"committer"`
+
+
// message is the commit message, contains arbitrary text.
+
Message string `json:"message"`
+
+
// treehash is the hash of the root tree of the commit.
+
Tree string `json:"tree"`
+
+
// parents are the hashes of the parent commits of the commit.
+
ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`
+
+
// pgpsignature is the pgp signature of the commit.
+
PGPSignature string `json:"pgp_signature,omitempty"`
+
+
// mergetag is the embedded tag object when a merge commit is created by
+
// merging a signed tag.
+
MergeTag string `json:"merge_tag,omitempty"`
+
+
// changeid is a unique identifier for the change (e.g., gerrit change-id).
+
ChangeId string `json:"change_id,omitempty"`
+
+
// extraheaders contains additional headers not captured by other fields.
+
ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`
+
+
// deprecated: kept for backwards compatibility with old json format.
+
This string `json:"this,omitempty"`
+
+
// deprecated: kept for backwards compatibility with old json format.
+
Parent string `json:"parent,omitempty"`
+
}
+
+
// types.Commit is an unify two commit structs:
+
// - git.object.Commit from
+
// - types.NiceDiff.commit
+
//
+
// to do this in backwards compatible fashion, we define the base struct
+
// to use the same fields as NiceDiff.Commit, and then we also unmarshal
+
// the struct fields from go-git structs, this custom unmarshal makes sense
+
// of both representations and unifies them to have maximal data in either
+
// form.
+
func (c *Commit) UnmarshalJSON(data []byte) error {
+
type Alias Commit
+
+
aux := &struct {
+
*object.Commit
+
*Alias
+
}{
+
Alias: (*Alias)(c),
+
}
+
+
if err := json.Unmarshal(data, aux); err != nil {
+
return err
+
}
+
+
c.FromGoGitCommit(aux.Commit)
+
+
return nil
+
}
+
+
// fill in as much of Commit as possible from the given go-git commit
+
func (c *Commit) FromGoGitCommit(gc *object.Commit) {
+
if gc == nil {
+
return
+
}
+
+
if c.Hash.IsZero() {
+
c.Hash = gc.Hash
+
}
+
if c.This == "" {
+
c.This = gc.Hash.String()
+
}
+
if isEmptySignature(c.Author) {
+
c.Author = gc.Author
+
}
+
if isEmptySignature(c.Committer) {
+
c.Committer = gc.Committer
+
}
+
if c.Message == "" {
+
c.Message = gc.Message
+
}
+
if c.Tree == "" {
+
c.Tree = gc.TreeHash.String()
+
}
+
if c.PGPSignature == "" {
+
c.PGPSignature = gc.PGPSignature
+
}
+
if c.MergeTag == "" {
+
c.MergeTag = gc.MergeTag
+
}
+
+
if len(c.ParentHashes) == 0 {
+
c.ParentHashes = gc.ParentHashes
+
}
+
if c.Parent == "" && len(gc.ParentHashes) > 0 {
+
c.Parent = gc.ParentHashes[0].String()
+
}
+
+
if len(c.ExtraHeaders) == 0 {
+
c.ExtraHeaders = make(map[string][]byte)
+
maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
+
}
+
+
if c.ChangeId == "" {
+
if v, ok := gc.ExtraHeaders["change-id"]; ok {
+
c.ChangeId = string(v)
+
}
+
}
+
}
+
+
func isEmptySignature(s object.Signature) bool {
+
return s.Email == "" && s.Name == "" && s.When.IsZero()
+
}
+
+
// produce a verifiable payload from this commit's metadata
+
func (c *Commit) Payload() string {
+
author := bytes.NewBuffer([]byte{})
+
c.Author.Encode(author)
+
+
committer := bytes.NewBuffer([]byte{})
+
c.Committer.Encode(committer)
+
+
payload := strings.Builder{}
+
+
fmt.Fprintf(&payload, "tree %s\n", c.Tree)
+
+
if len(c.ParentHashes) > 0 {
+
for _, p := range c.ParentHashes {
+
fmt.Fprintf(&payload, "parent %s\n", p.String())
+
}
+
} else {
+
// present for backwards compatibility
+
fmt.Fprintf(&payload, "parent %s\n", c.Parent)
+
}
+
+
fmt.Fprintf(&payload, "author %s\n", author.String())
+
fmt.Fprintf(&payload, "committer %s\n", committer.String())
+
+
if c.ChangeId != "" {
+
fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
+
} else if v, ok := c.ExtraHeaders["change-id"]; ok {
+
fmt.Fprintf(&payload, "change-id %s\n", string(v))
+
}
+
+
fmt.Fprintf(&payload, "\n%s", c.Message)
+
+
return payload.String()
+
}
+
+
var (
+
coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
+
)
+
+
func (commit Commit) CoAuthors() []object.Signature {
+
var coAuthors []object.Signature
+
seen := make(map[string]bool)
+
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
+
+
for _, match := range matches {
+
if len(match) >= 3 {
+
name := strings.TrimSpace(match[1])
+
email := strings.TrimSpace(match[2])
+
+
if seen[email] {
+
continue
+
}
+
seen[email] = true
+
+
coAuthors = append(coAuthors, object.Signature{
+
Name: name,
+
Email: email,
+
When: commit.Committer.When,
+
})
+
}
+
}
+
+
return coAuthors
+
}
+2 -12
types/diff.go
···
import (
"github.com/bluekeyes/go-gitdiff/gitdiff"
-
"github.com/go-git/go-git/v5/plumbing/object"
)
type DiffOpts struct {
···
// A nicer git diff representation.
type NiceDiff struct {
-
Commit struct {
-
Message string `json:"message"`
-
Author object.Signature `json:"author"`
-
This string `json:"this"`
-
Parent string `json:"parent"`
-
PGPSignature string `json:"pgp_signature"`
-
Committer object.Signature `json:"committer"`
-
Tree string `json:"tree"`
-
ChangedId string `json:"change_id"`
-
} `json:"commit"`
-
Stat struct {
+
Commit Commit `json:"commit"`
+
Stat struct {
FilesChanged int `json:"files_changed"`
Insertions int `json:"insertions"`
Deletions int `json:"deletions"`
+17 -17
types/repo.go
···
)
type RepoIndexResponse struct {
-
IsEmpty bool `json:"is_empty"`
-
Ref string `json:"ref,omitempty"`
-
Readme string `json:"readme,omitempty"`
-
ReadmeFileName string `json:"readme_file_name,omitempty"`
-
Commits []*object.Commit `json:"commits,omitempty"`
-
Description string `json:"description,omitempty"`
-
Files []NiceTree `json:"files,omitempty"`
-
Branches []Branch `json:"branches,omitempty"`
-
Tags []*TagReference `json:"tags,omitempty"`
-
TotalCommits int `json:"total_commits,omitempty"`
+
IsEmpty bool `json:"is_empty"`
+
Ref string `json:"ref,omitempty"`
+
Readme string `json:"readme,omitempty"`
+
ReadmeFileName string `json:"readme_file_name,omitempty"`
+
Commits []Commit `json:"commits,omitempty"`
+
Description string `json:"description,omitempty"`
+
Files []NiceTree `json:"files,omitempty"`
+
Branches []Branch `json:"branches,omitempty"`
+
Tags []*TagReference `json:"tags,omitempty"`
+
TotalCommits int `json:"total_commits,omitempty"`
}
type RepoLogResponse struct {
-
Commits []*object.Commit `json:"commits,omitempty"`
-
Ref string `json:"ref,omitempty"`
-
Description string `json:"description,omitempty"`
-
Log bool `json:"log,omitempty"`
-
Total int `json:"total,omitempty"`
-
Page int `json:"page,omitempty"`
-
PerPage int `json:"per_page,omitempty"`
+
Commits []Commit `json:"commits,omitempty"`
+
Ref string `json:"ref,omitempty"`
+
Description string `json:"description,omitempty"`
+
Log bool `json:"log,omitempty"`
+
Total int `json:"total,omitempty"`
+
Page int `json:"page,omitempty"`
+
PerPage int `json:"per_page,omitempty"`
}
type RepoCommitResponse struct {