+649
-8
api/tangled/cbor_gen.go
+649
-8
api/tangled/cbor_gen.go
···
6938
}
6939
6940
cw := cbg.NewCborWriter(w)
6941
-
fieldCount := 5
6942
6943
if t.Body == nil {
6944
fieldCount--
6945
}
6946
···
7045
return err
7046
}
7047
7048
// t.CreatedAt (string) (string)
7049
if len("createdAt") > 1000000 {
7050
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7067
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7068
return err
7069
}
7070
return nil
7071
}
7072
···
7095
7096
n := extra
7097
7098
-
nameBuf := make([]byte, 9)
7099
for i := uint64(0); i < n; i++ {
7100
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7101
if err != nil {
···
7164
}
7165
7166
t.Title = string(sval)
7167
}
7168
// t.CreatedAt (string) (string)
7169
case "createdAt":
···
7176
7177
t.CreatedAt = string(sval)
7178
}
7179
7180
default:
7181
// Field doesn't exist on this type, so ignore it
···
7194
}
7195
7196
cw := cbg.NewCborWriter(w)
7197
-
fieldCount := 5
7198
7199
if t.ReplyTo == nil {
7200
fieldCount--
···
7301
}
7302
}
7303
7304
// t.CreatedAt (string) (string)
7305
if len("createdAt") > 1000000 {
7306
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7323
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7324
return err
7325
}
7326
return nil
7327
}
7328
···
7351
7352
n := extra
7353
7354
-
nameBuf := make([]byte, 9)
7355
for i := uint64(0); i < n; i++ {
7356
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7357
if err != nil {
···
7419
}
7420
7421
t.ReplyTo = (*string)(&sval)
7422
}
7423
}
7424
// t.CreatedAt (string) (string)
···
7431
}
7432
7433
t.CreatedAt = string(sval)
7434
}
7435
7436
default:
···
7614
}
7615
7616
cw := cbg.NewCborWriter(w)
7617
-
fieldCount := 7
7618
7619
if t.Body == nil {
7620
fieldCount--
7621
}
7622
···
7760
return err
7761
}
7762
7763
// t.CreatedAt (string) (string)
7764
if len("createdAt") > 1000000 {
7765
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7782
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7783
return err
7784
}
7785
return nil
7786
}
7787
···
7810
7811
n := extra
7812
7813
-
nameBuf := make([]byte, 9)
7814
for i := uint64(0); i < n; i++ {
7815
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7816
if err != nil {
···
7919
}
7920
}
7921
7922
}
7923
// t.CreatedAt (string) (string)
7924
case "createdAt":
···
7931
7932
t.CreatedAt = string(sval)
7933
}
7934
7935
default:
7936
// Field doesn't exist on this type, so ignore it
···
7949
}
7950
7951
cw := cbg.NewCborWriter(w)
7952
7953
-
if _, err := cw.Write([]byte{164}); err != nil {
7954
return err
7955
}
7956
···
8019
return err
8020
}
8021
8022
// t.CreatedAt (string) (string)
8023
if len("createdAt") > 1000000 {
8024
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
8040
}
8041
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
8042
return err
8043
}
8044
return nil
8045
}
···
8069
8070
n := extra
8071
8072
-
nameBuf := make([]byte, 9)
8073
for i := uint64(0); i < n; i++ {
8074
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
8075
if err != nil {
···
8118
8119
t.LexiconTypeID = string(sval)
8120
}
8121
// t.CreatedAt (string) (string)
8122
case "createdAt":
8123
···
8128
}
8129
8130
t.CreatedAt = string(sval)
8131
}
8132
8133
default:
···
6938
}
6939
6940
cw := cbg.NewCborWriter(w)
6941
+
fieldCount := 7
6942
6943
if t.Body == nil {
6944
+
fieldCount--
6945
+
}
6946
+
6947
+
if t.Mentions == nil {
6948
+
fieldCount--
6949
+
}
6950
+
6951
+
if t.References == nil {
6952
fieldCount--
6953
}
6954
···
7053
return err
7054
}
7055
7056
+
// t.Mentions ([]string) (slice)
7057
+
if t.Mentions != nil {
7058
+
7059
+
if len("mentions") > 1000000 {
7060
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
7061
+
}
7062
+
7063
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
7064
+
return err
7065
+
}
7066
+
if _, err := cw.WriteString(string("mentions")); err != nil {
7067
+
return err
7068
+
}
7069
+
7070
+
if len(t.Mentions) > 8192 {
7071
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
7072
+
}
7073
+
7074
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
7075
+
return err
7076
+
}
7077
+
for _, v := range t.Mentions {
7078
+
if len(v) > 1000000 {
7079
+
return xerrors.Errorf("Value in field v was too long")
7080
+
}
7081
+
7082
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7083
+
return err
7084
+
}
7085
+
if _, err := cw.WriteString(string(v)); err != nil {
7086
+
return err
7087
+
}
7088
+
7089
+
}
7090
+
}
7091
+
7092
// t.CreatedAt (string) (string)
7093
if len("createdAt") > 1000000 {
7094
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7111
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7112
return err
7113
}
7114
+
7115
+
// t.References ([]string) (slice)
7116
+
if t.References != nil {
7117
+
7118
+
if len("references") > 1000000 {
7119
+
return xerrors.Errorf("Value in field \"references\" was too long")
7120
+
}
7121
+
7122
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
7123
+
return err
7124
+
}
7125
+
if _, err := cw.WriteString(string("references")); err != nil {
7126
+
return err
7127
+
}
7128
+
7129
+
if len(t.References) > 8192 {
7130
+
return xerrors.Errorf("Slice value in field t.References was too long")
7131
+
}
7132
+
7133
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
7134
+
return err
7135
+
}
7136
+
for _, v := range t.References {
7137
+
if len(v) > 1000000 {
7138
+
return xerrors.Errorf("Value in field v was too long")
7139
+
}
7140
+
7141
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7142
+
return err
7143
+
}
7144
+
if _, err := cw.WriteString(string(v)); err != nil {
7145
+
return err
7146
+
}
7147
+
7148
+
}
7149
+
}
7150
return nil
7151
}
7152
···
7175
7176
n := extra
7177
7178
+
nameBuf := make([]byte, 10)
7179
for i := uint64(0); i < n; i++ {
7180
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7181
if err != nil {
···
7244
}
7245
7246
t.Title = string(sval)
7247
+
}
7248
+
// t.Mentions ([]string) (slice)
7249
+
case "mentions":
7250
+
7251
+
maj, extra, err = cr.ReadHeader()
7252
+
if err != nil {
7253
+
return err
7254
+
}
7255
+
7256
+
if extra > 8192 {
7257
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
7258
+
}
7259
+
7260
+
if maj != cbg.MajArray {
7261
+
return fmt.Errorf("expected cbor array")
7262
+
}
7263
+
7264
+
if extra > 0 {
7265
+
t.Mentions = make([]string, extra)
7266
+
}
7267
+
7268
+
for i := 0; i < int(extra); i++ {
7269
+
{
7270
+
var maj byte
7271
+
var extra uint64
7272
+
var err error
7273
+
_ = maj
7274
+
_ = extra
7275
+
_ = err
7276
+
7277
+
{
7278
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7279
+
if err != nil {
7280
+
return err
7281
+
}
7282
+
7283
+
t.Mentions[i] = string(sval)
7284
+
}
7285
+
7286
+
}
7287
}
7288
// t.CreatedAt (string) (string)
7289
case "createdAt":
···
7296
7297
t.CreatedAt = string(sval)
7298
}
7299
+
// t.References ([]string) (slice)
7300
+
case "references":
7301
+
7302
+
maj, extra, err = cr.ReadHeader()
7303
+
if err != nil {
7304
+
return err
7305
+
}
7306
+
7307
+
if extra > 8192 {
7308
+
return fmt.Errorf("t.References: array too large (%d)", extra)
7309
+
}
7310
+
7311
+
if maj != cbg.MajArray {
7312
+
return fmt.Errorf("expected cbor array")
7313
+
}
7314
+
7315
+
if extra > 0 {
7316
+
t.References = make([]string, extra)
7317
+
}
7318
+
7319
+
for i := 0; i < int(extra); i++ {
7320
+
{
7321
+
var maj byte
7322
+
var extra uint64
7323
+
var err error
7324
+
_ = maj
7325
+
_ = extra
7326
+
_ = err
7327
+
7328
+
{
7329
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7330
+
if err != nil {
7331
+
return err
7332
+
}
7333
+
7334
+
t.References[i] = string(sval)
7335
+
}
7336
+
7337
+
}
7338
+
}
7339
7340
default:
7341
// Field doesn't exist on this type, so ignore it
···
7354
}
7355
7356
cw := cbg.NewCborWriter(w)
7357
+
fieldCount := 7
7358
+
7359
+
if t.Mentions == nil {
7360
+
fieldCount--
7361
+
}
7362
+
7363
+
if t.References == nil {
7364
+
fieldCount--
7365
+
}
7366
7367
if t.ReplyTo == nil {
7368
fieldCount--
···
7469
}
7470
}
7471
7472
+
// t.Mentions ([]string) (slice)
7473
+
if t.Mentions != nil {
7474
+
7475
+
if len("mentions") > 1000000 {
7476
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
7477
+
}
7478
+
7479
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
7480
+
return err
7481
+
}
7482
+
if _, err := cw.WriteString(string("mentions")); err != nil {
7483
+
return err
7484
+
}
7485
+
7486
+
if len(t.Mentions) > 8192 {
7487
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
7488
+
}
7489
+
7490
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
7491
+
return err
7492
+
}
7493
+
for _, v := range t.Mentions {
7494
+
if len(v) > 1000000 {
7495
+
return xerrors.Errorf("Value in field v was too long")
7496
+
}
7497
+
7498
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7499
+
return err
7500
+
}
7501
+
if _, err := cw.WriteString(string(v)); err != nil {
7502
+
return err
7503
+
}
7504
+
7505
+
}
7506
+
}
7507
+
7508
// t.CreatedAt (string) (string)
7509
if len("createdAt") > 1000000 {
7510
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7527
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7528
return err
7529
}
7530
+
7531
+
// t.References ([]string) (slice)
7532
+
if t.References != nil {
7533
+
7534
+
if len("references") > 1000000 {
7535
+
return xerrors.Errorf("Value in field \"references\" was too long")
7536
+
}
7537
+
7538
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
7539
+
return err
7540
+
}
7541
+
if _, err := cw.WriteString(string("references")); err != nil {
7542
+
return err
7543
+
}
7544
+
7545
+
if len(t.References) > 8192 {
7546
+
return xerrors.Errorf("Slice value in field t.References was too long")
7547
+
}
7548
+
7549
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
7550
+
return err
7551
+
}
7552
+
for _, v := range t.References {
7553
+
if len(v) > 1000000 {
7554
+
return xerrors.Errorf("Value in field v was too long")
7555
+
}
7556
+
7557
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7558
+
return err
7559
+
}
7560
+
if _, err := cw.WriteString(string(v)); err != nil {
7561
+
return err
7562
+
}
7563
+
7564
+
}
7565
+
}
7566
return nil
7567
}
7568
···
7591
7592
n := extra
7593
7594
+
nameBuf := make([]byte, 10)
7595
for i := uint64(0); i < n; i++ {
7596
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7597
if err != nil {
···
7659
}
7660
7661
t.ReplyTo = (*string)(&sval)
7662
+
}
7663
+
}
7664
+
// t.Mentions ([]string) (slice)
7665
+
case "mentions":
7666
+
7667
+
maj, extra, err = cr.ReadHeader()
7668
+
if err != nil {
7669
+
return err
7670
+
}
7671
+
7672
+
if extra > 8192 {
7673
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
7674
+
}
7675
+
7676
+
if maj != cbg.MajArray {
7677
+
return fmt.Errorf("expected cbor array")
7678
+
}
7679
+
7680
+
if extra > 0 {
7681
+
t.Mentions = make([]string, extra)
7682
+
}
7683
+
7684
+
for i := 0; i < int(extra); i++ {
7685
+
{
7686
+
var maj byte
7687
+
var extra uint64
7688
+
var err error
7689
+
_ = maj
7690
+
_ = extra
7691
+
_ = err
7692
+
7693
+
{
7694
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7695
+
if err != nil {
7696
+
return err
7697
+
}
7698
+
7699
+
t.Mentions[i] = string(sval)
7700
+
}
7701
+
7702
}
7703
}
7704
// t.CreatedAt (string) (string)
···
7711
}
7712
7713
t.CreatedAt = string(sval)
7714
+
}
7715
+
// t.References ([]string) (slice)
7716
+
case "references":
7717
+
7718
+
maj, extra, err = cr.ReadHeader()
7719
+
if err != nil {
7720
+
return err
7721
+
}
7722
+
7723
+
if extra > 8192 {
7724
+
return fmt.Errorf("t.References: array too large (%d)", extra)
7725
+
}
7726
+
7727
+
if maj != cbg.MajArray {
7728
+
return fmt.Errorf("expected cbor array")
7729
+
}
7730
+
7731
+
if extra > 0 {
7732
+
t.References = make([]string, extra)
7733
+
}
7734
+
7735
+
for i := 0; i < int(extra); i++ {
7736
+
{
7737
+
var maj byte
7738
+
var extra uint64
7739
+
var err error
7740
+
_ = maj
7741
+
_ = extra
7742
+
_ = err
7743
+
7744
+
{
7745
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7746
+
if err != nil {
7747
+
return err
7748
+
}
7749
+
7750
+
t.References[i] = string(sval)
7751
+
}
7752
+
7753
+
}
7754
}
7755
7756
default:
···
7934
}
7935
7936
cw := cbg.NewCborWriter(w)
7937
+
fieldCount := 9
7938
7939
if t.Body == nil {
7940
+
fieldCount--
7941
+
}
7942
+
7943
+
if t.Mentions == nil {
7944
+
fieldCount--
7945
+
}
7946
+
7947
+
if t.References == nil {
7948
fieldCount--
7949
}
7950
···
8088
return err
8089
}
8090
8091
+
// t.Mentions ([]string) (slice)
8092
+
if t.Mentions != nil {
8093
+
8094
+
if len("mentions") > 1000000 {
8095
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
8096
+
}
8097
+
8098
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
8099
+
return err
8100
+
}
8101
+
if _, err := cw.WriteString(string("mentions")); err != nil {
8102
+
return err
8103
+
}
8104
+
8105
+
if len(t.Mentions) > 8192 {
8106
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
8107
+
}
8108
+
8109
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
8110
+
return err
8111
+
}
8112
+
for _, v := range t.Mentions {
8113
+
if len(v) > 1000000 {
8114
+
return xerrors.Errorf("Value in field v was too long")
8115
+
}
8116
+
8117
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8118
+
return err
8119
+
}
8120
+
if _, err := cw.WriteString(string(v)); err != nil {
8121
+
return err
8122
+
}
8123
+
8124
+
}
8125
+
}
8126
+
8127
// t.CreatedAt (string) (string)
8128
if len("createdAt") > 1000000 {
8129
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
8146
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
8147
return err
8148
}
8149
+
8150
+
// t.References ([]string) (slice)
8151
+
if t.References != nil {
8152
+
8153
+
if len("references") > 1000000 {
8154
+
return xerrors.Errorf("Value in field \"references\" was too long")
8155
+
}
8156
+
8157
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
8158
+
return err
8159
+
}
8160
+
if _, err := cw.WriteString(string("references")); err != nil {
8161
+
return err
8162
+
}
8163
+
8164
+
if len(t.References) > 8192 {
8165
+
return xerrors.Errorf("Slice value in field t.References was too long")
8166
+
}
8167
+
8168
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
8169
+
return err
8170
+
}
8171
+
for _, v := range t.References {
8172
+
if len(v) > 1000000 {
8173
+
return xerrors.Errorf("Value in field v was too long")
8174
+
}
8175
+
8176
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8177
+
return err
8178
+
}
8179
+
if _, err := cw.WriteString(string(v)); err != nil {
8180
+
return err
8181
+
}
8182
+
8183
+
}
8184
+
}
8185
return nil
8186
}
8187
···
8210
8211
n := extra
8212
8213
+
nameBuf := make([]byte, 10)
8214
for i := uint64(0); i < n; i++ {
8215
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
8216
if err != nil {
···
8319
}
8320
}
8321
8322
+
}
8323
+
// t.Mentions ([]string) (slice)
8324
+
case "mentions":
8325
+
8326
+
maj, extra, err = cr.ReadHeader()
8327
+
if err != nil {
8328
+
return err
8329
+
}
8330
+
8331
+
if extra > 8192 {
8332
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
8333
+
}
8334
+
8335
+
if maj != cbg.MajArray {
8336
+
return fmt.Errorf("expected cbor array")
8337
+
}
8338
+
8339
+
if extra > 0 {
8340
+
t.Mentions = make([]string, extra)
8341
+
}
8342
+
8343
+
for i := 0; i < int(extra); i++ {
8344
+
{
8345
+
var maj byte
8346
+
var extra uint64
8347
+
var err error
8348
+
_ = maj
8349
+
_ = extra
8350
+
_ = err
8351
+
8352
+
{
8353
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8354
+
if err != nil {
8355
+
return err
8356
+
}
8357
+
8358
+
t.Mentions[i] = string(sval)
8359
+
}
8360
+
8361
+
}
8362
}
8363
// t.CreatedAt (string) (string)
8364
case "createdAt":
···
8371
8372
t.CreatedAt = string(sval)
8373
}
8374
+
// t.References ([]string) (slice)
8375
+
case "references":
8376
+
8377
+
maj, extra, err = cr.ReadHeader()
8378
+
if err != nil {
8379
+
return err
8380
+
}
8381
+
8382
+
if extra > 8192 {
8383
+
return fmt.Errorf("t.References: array too large (%d)", extra)
8384
+
}
8385
+
8386
+
if maj != cbg.MajArray {
8387
+
return fmt.Errorf("expected cbor array")
8388
+
}
8389
+
8390
+
if extra > 0 {
8391
+
t.References = make([]string, extra)
8392
+
}
8393
+
8394
+
for i := 0; i < int(extra); i++ {
8395
+
{
8396
+
var maj byte
8397
+
var extra uint64
8398
+
var err error
8399
+
_ = maj
8400
+
_ = extra
8401
+
_ = err
8402
+
8403
+
{
8404
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8405
+
if err != nil {
8406
+
return err
8407
+
}
8408
+
8409
+
t.References[i] = string(sval)
8410
+
}
8411
+
8412
+
}
8413
+
}
8414
8415
default:
8416
// Field doesn't exist on this type, so ignore it
···
8429
}
8430
8431
cw := cbg.NewCborWriter(w)
8432
+
fieldCount := 6
8433
8434
+
if t.Mentions == nil {
8435
+
fieldCount--
8436
+
}
8437
+
8438
+
if t.References == nil {
8439
+
fieldCount--
8440
+
}
8441
+
8442
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
8443
return err
8444
}
8445
···
8508
return err
8509
}
8510
8511
+
// t.Mentions ([]string) (slice)
8512
+
if t.Mentions != nil {
8513
+
8514
+
if len("mentions") > 1000000 {
8515
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
8516
+
}
8517
+
8518
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
8519
+
return err
8520
+
}
8521
+
if _, err := cw.WriteString(string("mentions")); err != nil {
8522
+
return err
8523
+
}
8524
+
8525
+
if len(t.Mentions) > 8192 {
8526
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
8527
+
}
8528
+
8529
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
8530
+
return err
8531
+
}
8532
+
for _, v := range t.Mentions {
8533
+
if len(v) > 1000000 {
8534
+
return xerrors.Errorf("Value in field v was too long")
8535
+
}
8536
+
8537
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8538
+
return err
8539
+
}
8540
+
if _, err := cw.WriteString(string(v)); err != nil {
8541
+
return err
8542
+
}
8543
+
8544
+
}
8545
+
}
8546
+
8547
// t.CreatedAt (string) (string)
8548
if len("createdAt") > 1000000 {
8549
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
8565
}
8566
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
8567
return err
8568
+
}
8569
+
8570
+
// t.References ([]string) (slice)
8571
+
if t.References != nil {
8572
+
8573
+
if len("references") > 1000000 {
8574
+
return xerrors.Errorf("Value in field \"references\" was too long")
8575
+
}
8576
+
8577
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
8578
+
return err
8579
+
}
8580
+
if _, err := cw.WriteString(string("references")); err != nil {
8581
+
return err
8582
+
}
8583
+
8584
+
if len(t.References) > 8192 {
8585
+
return xerrors.Errorf("Slice value in field t.References was too long")
8586
+
}
8587
+
8588
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
8589
+
return err
8590
+
}
8591
+
for _, v := range t.References {
8592
+
if len(v) > 1000000 {
8593
+
return xerrors.Errorf("Value in field v was too long")
8594
+
}
8595
+
8596
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8597
+
return err
8598
+
}
8599
+
if _, err := cw.WriteString(string(v)); err != nil {
8600
+
return err
8601
+
}
8602
+
8603
+
}
8604
}
8605
return nil
8606
}
···
8630
8631
n := extra
8632
8633
+
nameBuf := make([]byte, 10)
8634
for i := uint64(0); i < n; i++ {
8635
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
8636
if err != nil {
···
8679
8680
t.LexiconTypeID = string(sval)
8681
}
8682
+
// t.Mentions ([]string) (slice)
8683
+
case "mentions":
8684
+
8685
+
maj, extra, err = cr.ReadHeader()
8686
+
if err != nil {
8687
+
return err
8688
+
}
8689
+
8690
+
if extra > 8192 {
8691
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
8692
+
}
8693
+
8694
+
if maj != cbg.MajArray {
8695
+
return fmt.Errorf("expected cbor array")
8696
+
}
8697
+
8698
+
if extra > 0 {
8699
+
t.Mentions = make([]string, extra)
8700
+
}
8701
+
8702
+
for i := 0; i < int(extra); i++ {
8703
+
{
8704
+
var maj byte
8705
+
var extra uint64
8706
+
var err error
8707
+
_ = maj
8708
+
_ = extra
8709
+
_ = err
8710
+
8711
+
{
8712
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8713
+
if err != nil {
8714
+
return err
8715
+
}
8716
+
8717
+
t.Mentions[i] = string(sval)
8718
+
}
8719
+
8720
+
}
8721
+
}
8722
// t.CreatedAt (string) (string)
8723
case "createdAt":
8724
···
8729
}
8730
8731
t.CreatedAt = string(sval)
8732
+
}
8733
+
// t.References ([]string) (slice)
8734
+
case "references":
8735
+
8736
+
maj, extra, err = cr.ReadHeader()
8737
+
if err != nil {
8738
+
return err
8739
+
}
8740
+
8741
+
if extra > 8192 {
8742
+
return fmt.Errorf("t.References: array too large (%d)", extra)
8743
+
}
8744
+
8745
+
if maj != cbg.MajArray {
8746
+
return fmt.Errorf("expected cbor array")
8747
+
}
8748
+
8749
+
if extra > 0 {
8750
+
t.References = make([]string, extra)
8751
+
}
8752
+
8753
+
for i := 0; i < int(extra); i++ {
8754
+
{
8755
+
var maj byte
8756
+
var extra uint64
8757
+
var err error
8758
+
_ = maj
8759
+
_ = extra
8760
+
_ = err
8761
+
8762
+
{
8763
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8764
+
if err != nil {
8765
+
return err
8766
+
}
8767
+
8768
+
t.References[i] = string(sval)
8769
+
}
8770
+
8771
+
}
8772
}
8773
8774
default:
+7
-5
api/tangled/issuecomment.go
+7
-5
api/tangled/issuecomment.go
···
17
} //
18
// RECORDTYPE: RepoIssueComment
19
type RepoIssueComment struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
21
-
Body string `json:"body" cborgen:"body"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Issue string `json:"issue" cborgen:"issue"`
24
-
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
25
}
···
17
} //
18
// RECORDTYPE: RepoIssueComment
19
type RepoIssueComment struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
21
+
Body string `json:"body" cborgen:"body"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Issue string `json:"issue" cborgen:"issue"`
24
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
+
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
27
}
+6
-4
api/tangled/pullcomment.go
+6
-4
api/tangled/pullcomment.go
···
17
} //
18
// RECORDTYPE: RepoPullComment
19
type RepoPullComment struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
21
-
Body string `json:"body" cborgen:"body"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Pull string `json:"pull" cborgen:"pull"`
24
}
···
17
} //
18
// RECORDTYPE: RepoPullComment
19
type RepoPullComment struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
21
+
Body string `json:"body" cborgen:"body"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
Pull string `json:"pull" cborgen:"pull"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
}
+7
-5
api/tangled/repoissue.go
+7
-5
api/tangled/repoissue.go
···
17
} //
18
// RECORDTYPE: RepoIssue
19
type RepoIssue struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
21
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Repo string `json:"repo" cborgen:"repo"`
24
-
Title string `json:"title" cborgen:"title"`
25
}
···
17
} //
18
// RECORDTYPE: RepoIssue
19
type RepoIssue struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
21
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
25
+
Repo string `json:"repo" cborgen:"repo"`
26
+
Title string `json:"title" cborgen:"title"`
27
}
+2
api/tangled/repopull.go
+2
api/tangled/repopull.go
···
20
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
Patch string `json:"patch" cborgen:"patch"`
24
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
25
Target *RepoPull_Target `json:"target" cborgen:"target"`
26
Title string `json:"title" cborgen:"title"`
···
20
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
Patch string `json:"patch" cborgen:"patch"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
27
Target *RepoPull_Target `json:"target" cborgen:"target"`
28
Title string `json:"title" cborgen:"title"`
+6
-45
appview/commitverify/verify.go
+6
-45
appview/commitverify/verify.go
···
3
import (
4
"log"
5
6
-
"github.com/go-git/go-git/v5/plumbing/object"
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
"tangled.org/core/crypto"
···
35
return ""
36
}
37
38
-
func GetVerifiedObjectCommits(e db.Execer, emailToDid map[string]string, commits []*object.Commit) (VerifiedCommits, error) {
39
-
ndCommits := []types.NiceDiff{}
40
-
for _, commit := range commits {
41
-
ndCommits = append(ndCommits, ObjectCommitToNiceDiff(commit))
42
-
}
43
-
return GetVerifiedCommits(e, emailToDid, ndCommits)
44
-
}
45
-
46
-
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.NiceDiff) (VerifiedCommits, error) {
47
vcs := VerifiedCommits{}
48
49
didPubkeyCache := make(map[string][]models.PublicKey)
50
51
for _, commit := range ndCommits {
52
-
c := commit.Commit
53
-
54
-
committerEmail := c.Committer.Email
55
if did, exists := emailToDid[committerEmail]; exists {
56
// check if we've already fetched public keys for this did
57
pubKeys, ok := didPubkeyCache[did]
···
67
}
68
69
// try to verify with any associated pubkeys
70
for _, pk := range pubKeys {
71
-
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
72
73
fp, err := crypto.SSHFingerprint(pk.Key)
74
if err != nil {
75
log.Println("error computing ssh fingerprint:", err)
76
}
77
78
-
vc := verifiedCommit{fingerprint: fp, hash: c.This}
79
vcs[vc] = struct{}{}
80
break
81
}
···
86
87
return vcs, nil
88
}
89
-
90
-
// ObjectCommitToNiceDiff is a compatibility function to convert a
91
-
// commit object into a NiceDiff structure.
92
-
func ObjectCommitToNiceDiff(c *object.Commit) types.NiceDiff {
93
-
var niceDiff types.NiceDiff
94
-
95
-
// set commit information
96
-
niceDiff.Commit.Message = c.Message
97
-
niceDiff.Commit.Author = c.Author
98
-
niceDiff.Commit.This = c.Hash.String()
99
-
niceDiff.Commit.Committer = c.Committer
100
-
niceDiff.Commit.Tree = c.TreeHash.String()
101
-
niceDiff.Commit.PGPSignature = c.PGPSignature
102
-
103
-
changeId, ok := c.ExtraHeaders["change-id"]
104
-
if ok {
105
-
niceDiff.Commit.ChangedId = string(changeId)
106
-
}
107
-
108
-
// set parent hash if available
109
-
if len(c.ParentHashes) > 0 {
110
-
niceDiff.Commit.Parent = c.ParentHashes[0].String()
111
-
}
112
-
113
-
// XXX: Stats and Diff fields are typically populated
114
-
// after fetching the actual diff information, which isn't
115
-
// directly available in the commit object itself.
116
-
117
-
return niceDiff
118
-
}
···
3
import (
4
"log"
5
6
"tangled.org/core/appview/db"
7
"tangled.org/core/appview/models"
8
"tangled.org/core/crypto"
···
34
return ""
35
}
36
37
+
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) {
38
vcs := VerifiedCommits{}
39
40
didPubkeyCache := make(map[string][]models.PublicKey)
41
42
for _, commit := range ndCommits {
43
+
committerEmail := commit.Committer.Email
44
if did, exists := emailToDid[committerEmail]; exists {
45
// check if we've already fetched public keys for this did
46
pubKeys, ok := didPubkeyCache[did]
···
56
}
57
58
// try to verify with any associated pubkeys
59
+
payload := commit.Payload()
60
+
signature := commit.PGPSignature
61
for _, pk := range pubKeys {
62
+
if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok {
63
64
fp, err := crypto.SSHFingerprint(pk.Key)
65
if err != nil {
66
log.Println("error computing ssh fingerprint:", err)
67
}
68
69
+
vc := verifiedCommit{fingerprint: fp, hash: commit.This}
70
vcs[vc] = struct{}{}
71
break
72
}
···
77
78
return vcs, nil
79
}
+3
-2
appview/db/artifact.go
+3
-2
appview/db/artifact.go
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
)
12
13
func AddArtifact(e Execer, artifact models.Artifact) error {
···
37
return err
38
}
39
40
-
func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) {
41
var artifacts []models.Artifact
42
43
var conditions []string
···
109
return artifacts, nil
110
}
111
112
-
func DeleteArtifact(e Execer, filters ...filter) error {
113
var conditions []string
114
var args []any
115
for _, filter := range filters {
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddArtifact(e Execer, artifact models.Artifact) error {
···
38
return err
39
}
40
41
+
func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) {
42
var artifacts []models.Artifact
43
44
var conditions []string
···
110
return artifacts, nil
111
}
112
113
+
func DeleteArtifact(e Execer, filters ...orm.Filter) error {
114
var conditions []string
115
var args []any
116
for _, filter := range filters {
+4
-3
appview/db/collaborators.go
+4
-3
appview/db/collaborators.go
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
)
10
11
func AddCollaborator(e Execer, c models.Collaborator) error {
···
16
return err
17
}
18
19
-
func DeleteCollaborator(e Execer, filters ...filter) error {
20
var conditions []string
21
var args []any
22
for _, filter := range filters {
···
58
return nil, nil
59
}
60
61
-
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
62
}
63
64
-
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
65
var collaborators []models.Collaborator
66
var conditions []string
67
var args []any
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func AddCollaborator(e Execer, c models.Collaborator) error {
···
17
return err
18
}
19
20
+
func DeleteCollaborator(e Execer, filters ...orm.Filter) error {
21
var conditions []string
22
var args []any
23
for _, filter := range filters {
···
59
return nil, nil
60
}
61
62
+
return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
63
}
64
65
+
func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) {
66
var collaborators []models.Collaborator
67
var conditions []string
68
var args []any
+33
-137
appview/db/db.go
+33
-137
appview/db/db.go
···
3
import (
4
"context"
5
"database/sql"
6
-
"fmt"
7
"log/slog"
8
-
"reflect"
9
"strings"
10
11
_ "github.com/mattn/go-sqlite3"
12
"tangled.org/core/log"
13
)
14
15
type DB struct {
···
561
email_notifications integer not null default 0
562
);
563
564
create table if not exists migrations (
565
id integer primary key autoincrement,
566
name text unique
···
569
-- indexes for better performance
570
create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
571
create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
572
`)
573
if err != nil {
574
return nil, err
575
}
576
577
// run migrations
578
-
runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
579
tx.Exec(`
580
alter table repos add column description text check (length(description) <= 200);
581
`)
582
return nil
583
})
584
585
-
runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
586
// add unconstrained column
587
_, err := tx.Exec(`
588
alter table public_keys
···
605
return nil
606
})
607
608
-
runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
609
_, err := tx.Exec(`
610
alter table comments drop column comment_at;
611
alter table comments add column rkey text;
···
613
return err
614
})
615
616
-
runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
617
_, err := tx.Exec(`
618
alter table comments add column deleted text; -- timestamp
619
alter table comments add column edited text; -- timestamp
···
621
return err
622
})
623
624
-
runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
625
_, err := tx.Exec(`
626
alter table pulls add column source_branch text;
627
alter table pulls add column source_repo_at text;
···
630
return err
631
})
632
633
-
runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
634
_, err := tx.Exec(`
635
alter table repos add column source text;
636
`)
···
642
//
643
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
644
conn.ExecContext(ctx, "pragma foreign_keys = off;")
645
-
runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
646
_, err := tx.Exec(`
647
create table pulls_new (
648
-- identifiers
···
699
})
700
conn.ExecContext(ctx, "pragma foreign_keys = on;")
701
702
-
runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
703
tx.Exec(`
704
alter table repos add column spindle text;
705
`)
···
709
// drop all knot secrets, add unique constraint to knots
710
//
711
// knots will henceforth use service auth for signed requests
712
-
runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
713
_, err := tx.Exec(`
714
create table registrations_new (
715
id integer primary key autoincrement,
···
732
})
733
734
// recreate and add rkey + created columns with default constraint
735
-
runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
736
// create new table
737
// - repo_at instead of repo integer
738
// - rkey field
···
786
return err
787
})
788
789
-
runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
790
_, err := tx.Exec(`
791
alter table issues add column rkey text not null default '';
792
···
798
})
799
800
// repurpose the read-only column to "needs-upgrade"
801
-
runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
802
_, err := tx.Exec(`
803
alter table registrations rename column read_only to needs_upgrade;
804
`)
···
806
})
807
808
// require all knots to upgrade after the release of total xrpc
809
-
runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
810
_, err := tx.Exec(`
811
update registrations set needs_upgrade = 1;
812
`)
···
814
})
815
816
// require all knots to upgrade after the release of total xrpc
817
-
runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
818
_, err := tx.Exec(`
819
alter table spindles add column needs_upgrade integer not null default 0;
820
`)
···
832
//
833
// disable foreign-keys for the next migration
834
conn.ExecContext(ctx, "pragma foreign_keys = off;")
835
-
runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
836
_, err := tx.Exec(`
837
create table if not exists issues_new (
838
-- identifiers
···
902
// - new columns
903
// * column "reply_to" which can be any other comment
904
// * column "at-uri" which is a generated column
905
-
runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
906
_, err := tx.Exec(`
907
create table if not exists issue_comments (
908
-- identifiers
···
962
//
963
// disable foreign-keys for the next migration
964
conn.ExecContext(ctx, "pragma foreign_keys = off;")
965
-
runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
966
_, err := tx.Exec(`
967
create table if not exists pulls_new (
968
-- identifiers
···
1043
//
1044
// disable foreign-keys for the next migration
1045
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1046
-
runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1047
_, err := tx.Exec(`
1048
create table if not exists pull_submissions_new (
1049
-- identifiers
···
1097
1098
// knots may report the combined patch for a comparison, we can store that on the appview side
1099
// (but not on the pds record), because calculating the combined patch requires a git index
1100
-
runMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1101
_, err := tx.Exec(`
1102
alter table pull_submissions add column combined text;
1103
`)
1104
return err
1105
})
1106
1107
-
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1108
_, err := tx.Exec(`
1109
alter table profile add column pronouns text;
1110
`)
1111
return err
1112
})
1113
1114
-
runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1115
_, err := tx.Exec(`
1116
alter table repos add column website text;
1117
alter table repos add column topics text;
···
1119
return err
1120
})
1121
1122
-
runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1123
_, err := tx.Exec(`
1124
alter table notification_preferences add column user_mentioned integer not null default 1;
1125
`)
···
1127
})
1128
1129
// remove the foreign key constraints from stars.
1130
-
runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1131
_, err := tx.Exec(`
1132
create table stars_new (
1133
id integer primary key autoincrement,
···
1171
}, nil
1172
}
1173
1174
-
type migrationFn = func(*sql.Tx) error
1175
-
1176
-
func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
1177
-
logger = logger.With("migration", name)
1178
-
1179
-
tx, err := c.BeginTx(context.Background(), nil)
1180
-
if err != nil {
1181
-
return err
1182
-
}
1183
-
defer tx.Rollback()
1184
-
1185
-
var exists bool
1186
-
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
1187
-
if err != nil {
1188
-
return err
1189
-
}
1190
-
1191
-
if !exists {
1192
-
// run migration
1193
-
err = migrationFn(tx)
1194
-
if err != nil {
1195
-
logger.Error("failed to run migration", "err", err)
1196
-
return err
1197
-
}
1198
-
1199
-
// mark migration as complete
1200
-
_, err = tx.Exec("insert into migrations (name) values (?)", name)
1201
-
if err != nil {
1202
-
logger.Error("failed to mark migration as complete", "err", err)
1203
-
return err
1204
-
}
1205
-
1206
-
// commit the transaction
1207
-
if err := tx.Commit(); err != nil {
1208
-
return err
1209
-
}
1210
-
1211
-
logger.Info("migration applied successfully")
1212
-
} else {
1213
-
logger.Warn("skipped migration, already applied")
1214
-
}
1215
-
1216
-
return nil
1217
-
}
1218
-
1219
func (d *DB) Close() error {
1220
return d.DB.Close()
1221
}
1222
-
1223
-
type filter struct {
1224
-
key string
1225
-
arg any
1226
-
cmp string
1227
-
}
1228
-
1229
-
func newFilter(key, cmp string, arg any) filter {
1230
-
return filter{
1231
-
key: key,
1232
-
arg: arg,
1233
-
cmp: cmp,
1234
-
}
1235
-
}
1236
-
1237
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1238
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1239
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1240
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1241
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1242
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1243
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1244
-
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
1245
-
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
1246
-
func FilterContains(key string, arg any) filter {
1247
-
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
1248
-
}
1249
-
1250
-
func (f filter) Condition() string {
1251
-
rv := reflect.ValueOf(f.arg)
1252
-
kind := rv.Kind()
1253
-
1254
-
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
1255
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1256
-
if rv.Len() == 0 {
1257
-
// always false
1258
-
return "1 = 0"
1259
-
}
1260
-
1261
-
placeholders := make([]string, rv.Len())
1262
-
for i := range placeholders {
1263
-
placeholders[i] = "?"
1264
-
}
1265
-
1266
-
return fmt.Sprintf("%s %s (%s)", f.key, f.cmp, strings.Join(placeholders, ", "))
1267
-
}
1268
-
1269
-
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
1270
-
}
1271
-
1272
-
func (f filter) Arg() []any {
1273
-
rv := reflect.ValueOf(f.arg)
1274
-
kind := rv.Kind()
1275
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1276
-
if rv.Len() == 0 {
1277
-
return nil
1278
-
}
1279
-
1280
-
out := make([]any, rv.Len())
1281
-
for i := range rv.Len() {
1282
-
out[i] = rv.Index(i).Interface()
1283
-
}
1284
-
return out
1285
-
}
1286
-
1287
-
return []any{f.arg}
1288
-
}
···
3
import (
4
"context"
5
"database/sql"
6
"log/slog"
7
"strings"
8
9
_ "github.com/mattn/go-sqlite3"
10
"tangled.org/core/log"
11
+
"tangled.org/core/orm"
12
)
13
14
type DB struct {
···
560
email_notifications integer not null default 0
561
);
562
563
+
create table if not exists reference_links (
564
+
id integer primary key autoincrement,
565
+
from_at text not null,
566
+
to_at text not null,
567
+
unique (from_at, to_at)
568
+
);
569
+
570
create table if not exists migrations (
571
id integer primary key autoincrement,
572
name text unique
···
575
-- indexes for better performance
576
create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
577
create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
578
+
create index if not exists idx_references_from_at on reference_links(from_at);
579
+
create index if not exists idx_references_to_at on reference_links(to_at);
580
`)
581
if err != nil {
582
return nil, err
583
}
584
585
// run migrations
586
+
orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
587
tx.Exec(`
588
alter table repos add column description text check (length(description) <= 200);
589
`)
590
return nil
591
})
592
593
+
orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
594
// add unconstrained column
595
_, err := tx.Exec(`
596
alter table public_keys
···
613
return nil
614
})
615
616
+
orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
617
_, err := tx.Exec(`
618
alter table comments drop column comment_at;
619
alter table comments add column rkey text;
···
621
return err
622
})
623
624
+
orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
625
_, err := tx.Exec(`
626
alter table comments add column deleted text; -- timestamp
627
alter table comments add column edited text; -- timestamp
···
629
return err
630
})
631
632
+
orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
633
_, err := tx.Exec(`
634
alter table pulls add column source_branch text;
635
alter table pulls add column source_repo_at text;
···
638
return err
639
})
640
641
+
orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
642
_, err := tx.Exec(`
643
alter table repos add column source text;
644
`)
···
650
//
651
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
652
conn.ExecContext(ctx, "pragma foreign_keys = off;")
653
+
orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
654
_, err := tx.Exec(`
655
create table pulls_new (
656
-- identifiers
···
707
})
708
conn.ExecContext(ctx, "pragma foreign_keys = on;")
709
710
+
orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
711
tx.Exec(`
712
alter table repos add column spindle text;
713
`)
···
717
// drop all knot secrets, add unique constraint to knots
718
//
719
// knots will henceforth use service auth for signed requests
720
+
orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
721
_, err := tx.Exec(`
722
create table registrations_new (
723
id integer primary key autoincrement,
···
740
})
741
742
// recreate and add rkey + created columns with default constraint
743
+
orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
744
// create new table
745
// - repo_at instead of repo integer
746
// - rkey field
···
794
return err
795
})
796
797
+
orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
798
_, err := tx.Exec(`
799
alter table issues add column rkey text not null default '';
800
···
806
})
807
808
// repurpose the read-only column to "needs-upgrade"
809
+
orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
810
_, err := tx.Exec(`
811
alter table registrations rename column read_only to needs_upgrade;
812
`)
···
814
})
815
816
// require all knots to upgrade after the release of total xrpc
817
+
orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
818
_, err := tx.Exec(`
819
update registrations set needs_upgrade = 1;
820
`)
···
822
})
823
824
// require all knots to upgrade after the release of total xrpc
825
+
orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
826
_, err := tx.Exec(`
827
alter table spindles add column needs_upgrade integer not null default 0;
828
`)
···
840
//
841
// disable foreign-keys for the next migration
842
conn.ExecContext(ctx, "pragma foreign_keys = off;")
843
+
orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
844
_, err := tx.Exec(`
845
create table if not exists issues_new (
846
-- identifiers
···
910
// - new columns
911
// * column "reply_to" which can be any other comment
912
// * column "at-uri" which is a generated column
913
+
orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
914
_, err := tx.Exec(`
915
create table if not exists issue_comments (
916
-- identifiers
···
970
//
971
// disable foreign-keys for the next migration
972
conn.ExecContext(ctx, "pragma foreign_keys = off;")
973
+
orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
974
_, err := tx.Exec(`
975
create table if not exists pulls_new (
976
-- identifiers
···
1051
//
1052
// disable foreign-keys for the next migration
1053
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1054
+
orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1055
_, err := tx.Exec(`
1056
create table if not exists pull_submissions_new (
1057
-- identifiers
···
1105
1106
// knots may report the combined patch for a comparison, we can store that on the appview side
1107
// (but not on the pds record), because calculating the combined patch requires a git index
1108
+
orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1109
_, err := tx.Exec(`
1110
alter table pull_submissions add column combined text;
1111
`)
1112
return err
1113
})
1114
1115
+
orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1116
_, err := tx.Exec(`
1117
alter table profile add column pronouns text;
1118
`)
1119
return err
1120
})
1121
1122
+
orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1123
_, err := tx.Exec(`
1124
alter table repos add column website text;
1125
alter table repos add column topics text;
···
1127
return err
1128
})
1129
1130
+
orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1131
_, err := tx.Exec(`
1132
alter table notification_preferences add column user_mentioned integer not null default 1;
1133
`)
···
1135
})
1136
1137
// remove the foreign key constraints from stars.
1138
+
orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1139
_, err := tx.Exec(`
1140
create table stars_new (
1141
id integer primary key autoincrement,
···
1179
}, nil
1180
}
1181
1182
func (d *DB) Close() error {
1183
return d.DB.Close()
1184
}
+4
-3
appview/db/follow.go
+4
-3
appview/db/follow.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
func AddFollow(e Execer, follow *models.Follow) error {
···
134
return result, nil
135
}
136
137
-
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138
var follows []models.Follow
139
140
var conditions []string
···
191
}
192
193
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194
-
return GetFollows(e, 0, FilterEq("subject_did", did))
195
}
196
197
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198
-
return GetFollows(e, 0, FilterEq("user_did", did))
199
}
200
201
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
func AddFollow(e Execer, follow *models.Follow) error {
···
135
return result, nil
136
}
137
138
+
func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
139
var follows []models.Follow
140
141
var conditions []string
···
192
}
193
194
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
195
+
return GetFollows(e, 0, orm.FilterEq("subject_did", did))
196
}
197
198
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
199
+
return GetFollows(e, 0, orm.FilterEq("user_did", did))
200
}
201
202
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+92
-36
appview/db/issues.go
+92
-36
appview/db/issues.go
···
10
"time"
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
"tangled.org/core/appview/pagination"
15
)
16
17
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
26
27
issues, err := GetIssues(
28
tx,
29
-
FilterEq("did", issue.Did),
30
-
FilterEq("rkey", issue.Rkey),
31
)
32
switch {
33
case err != nil:
···
69
returning rowid, issue_id
70
`, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body)
71
72
-
return row.Scan(&issue.Id, &issue.IssueId)
73
}
74
75
func updateIssue(tx *sql.Tx, issue *models.Issue) error {
···
79
set title = ?, body = ?, edited = ?
80
where did = ? and rkey = ?
81
`, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey)
82
-
return err
83
}
84
85
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
86
issueMap := make(map[string]*models.Issue) // at-uri -> issue
87
88
var conditions []string
···
98
whereClause = " where " + strings.Join(conditions, " and ")
99
}
100
101
-
pLower := FilterGte("row_num", page.Offset+1)
102
-
pUpper := FilterLte("row_num", page.Offset+page.Limit)
103
104
pageClause := ""
105
if page.Limit > 0 {
···
189
repoAts = append(repoAts, string(issue.RepoAt))
190
}
191
192
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
193
if err != nil {
194
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
195
}
···
212
// collect comments
213
issueAts := slices.Collect(maps.Keys(issueMap))
214
215
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
216
if err != nil {
217
return nil, fmt.Errorf("failed to query comments: %w", err)
218
}
···
224
}
225
226
// collect allLabels for each issue
227
-
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
228
if err != nil {
229
return nil, fmt.Errorf("failed to query labels: %w", err)
230
}
231
for issueAt, labels := range allLabels {
232
if issue, ok := issueMap[issueAt.String()]; ok {
233
issue.Labels = labels
234
}
235
}
236
···
250
issues, err := GetIssuesPaginated(
251
e,
252
pagination.Page{},
253
-
FilterEq("repo_at", repoAt),
254
-
FilterEq("issue_id", issueId),
255
)
256
if err != nil {
257
return nil, err
···
263
return &issues[0], nil
264
}
265
266
-
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
267
return GetIssuesPaginated(e, pagination.Page{}, filters...)
268
}
269
···
271
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
272
var ids []int64
273
274
-
var filters []filter
275
openValue := 0
276
if opts.IsOpen {
277
openValue = 1
278
}
279
-
filters = append(filters, FilterEq("open", openValue))
280
if opts.RepoAt != "" {
281
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
282
}
283
284
var conditions []string
···
323
return ids, nil
324
}
325
326
-
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
327
-
result, err := e.Exec(
328
`insert into issue_comments (
329
did,
330
rkey,
···
363
return 0, err
364
}
365
366
return id, nil
367
}
368
369
-
func DeleteIssueComments(e Execer, filters ...filter) error {
370
var conditions []string
371
var args []any
372
for _, filter := range filters {
···
385
return err
386
}
387
388
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
389
-
var comments []models.IssueComment
390
391
var conditions []string
392
var args []any
···
465
comment.ReplyTo = &replyTo.V
466
}
467
468
-
comments = append(comments, comment)
469
}
470
471
if err = rows.Err(); err != nil {
472
return nil, err
473
}
474
475
return comments, nil
476
}
477
478
-
func DeleteIssues(e Execer, filters ...filter) error {
479
-
var conditions []string
480
-
var args []any
481
-
for _, filter := range filters {
482
-
conditions = append(conditions, filter.Condition())
483
-
args = append(args, filter.Arg()...)
484
}
485
486
-
whereClause := ""
487
-
if conditions != nil {
488
-
whereClause = " where " + strings.Join(conditions, " and ")
489
}
490
491
-
query := fmt.Sprintf(`delete from issues %s`, whereClause)
492
-
_, err := e.Exec(query, args...)
493
-
return err
494
}
495
496
-
func CloseIssues(e Execer, filters ...filter) error {
497
var conditions []string
498
var args []any
499
for _, filter := range filters {
···
511
return err
512
}
513
514
-
func ReopenIssues(e Execer, filters ...filter) error {
515
var conditions []string
516
var args []any
517
for _, filter := range filters {
···
10
"time"
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
+
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/pagination"
16
+
"tangled.org/core/orm"
17
)
18
19
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
28
29
issues, err := GetIssues(
30
tx,
31
+
orm.FilterEq("did", issue.Did),
32
+
orm.FilterEq("rkey", issue.Rkey),
33
)
34
switch {
35
case err != nil:
···
71
returning rowid, issue_id
72
`, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body)
73
74
+
err = row.Scan(&issue.Id, &issue.IssueId)
75
+
if err != nil {
76
+
return fmt.Errorf("scan row: %w", err)
77
+
}
78
+
79
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
80
+
return fmt.Errorf("put reference_links: %w", err)
81
+
}
82
+
return nil
83
}
84
85
func updateIssue(tx *sql.Tx, issue *models.Issue) error {
···
89
set title = ?, body = ?, edited = ?
90
where did = ? and rkey = ?
91
`, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey)
92
+
if err != nil {
93
+
return err
94
+
}
95
+
96
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
97
+
return fmt.Errorf("put reference_links: %w", err)
98
+
}
99
+
return nil
100
}
101
102
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
issueMap := make(map[string]*models.Issue) // at-uri -> issue
104
105
var conditions []string
···
115
whereClause = " where " + strings.Join(conditions, " and ")
116
}
117
118
+
pLower := orm.FilterGte("row_num", page.Offset+1)
119
+
pUpper := orm.FilterLte("row_num", page.Offset+page.Limit)
120
121
pageClause := ""
122
if page.Limit > 0 {
···
206
repoAts = append(repoAts, string(issue.RepoAt))
207
}
208
209
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
210
if err != nil {
211
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
212
}
···
229
// collect comments
230
issueAts := slices.Collect(maps.Keys(issueMap))
231
232
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to query comments: %w", err)
235
}
···
241
}
242
243
// collect allLabels for each issue
244
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts))
245
if err != nil {
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
248
for issueAt, labels := range allLabels {
249
if issue, ok := issueMap[issueAt.String()]; ok {
250
issue.Labels = labels
251
+
}
252
+
}
253
+
254
+
// collect references for each issue
255
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts))
256
+
if err != nil {
257
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
+
}
259
+
for issueAt, references := range allReferencs {
260
+
if issue, ok := issueMap[issueAt.String()]; ok {
261
+
issue.References = references
262
}
263
}
264
···
278
issues, err := GetIssuesPaginated(
279
e,
280
pagination.Page{},
281
+
orm.FilterEq("repo_at", repoAt),
282
+
orm.FilterEq("issue_id", issueId),
283
)
284
if err != nil {
285
return nil, err
···
291
return &issues[0], nil
292
}
293
294
+
func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) {
295
return GetIssuesPaginated(e, pagination.Page{}, filters...)
296
}
297
···
299
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
300
var ids []int64
301
302
+
var filters []orm.Filter
303
openValue := 0
304
if opts.IsOpen {
305
openValue = 1
306
}
307
+
filters = append(filters, orm.FilterEq("open", openValue))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
351
return ids, nil
352
}
353
354
+
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
355
+
result, err := tx.Exec(
356
`insert into issue_comments (
357
did,
358
rkey,
···
391
return 0, err
392
}
393
394
+
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
395
+
return 0, fmt.Errorf("put reference_links: %w", err)
396
+
}
397
+
398
return id, nil
399
}
400
401
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
402
var conditions []string
403
var args []any
404
for _, filter := range filters {
···
417
return err
418
}
419
420
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
+
commentMap := make(map[string]*models.IssueComment)
422
423
var conditions []string
424
var args []any
···
497
comment.ReplyTo = &replyTo.V
498
}
499
500
+
atUri := comment.AtUri().String()
501
+
commentMap[atUri] = &comment
502
}
503
504
if err = rows.Err(); err != nil {
505
return nil, err
506
}
507
508
+
// collect references for each comments
509
+
commentAts := slices.Collect(maps.Keys(commentMap))
510
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
511
+
if err != nil {
512
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
513
+
}
514
+
for commentAt, references := range allReferencs {
515
+
if comment, ok := commentMap[commentAt.String()]; ok {
516
+
comment.References = references
517
+
}
518
+
}
519
+
520
+
var comments []models.IssueComment
521
+
for _, c := range commentMap {
522
+
comments = append(comments, *c)
523
+
}
524
+
525
+
sort.Slice(comments, func(i, j int) bool {
526
+
return comments[i].Created.After(comments[j].Created)
527
+
})
528
+
529
return comments, nil
530
}
531
532
+
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
533
+
_, err := tx.Exec(
534
+
`delete from issues
535
+
where did = ? and rkey = ?`,
536
+
did,
537
+
rkey,
538
+
)
539
+
if err != nil {
540
+
return fmt.Errorf("delete issue: %w", err)
541
}
542
543
+
uri := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey))
544
+
err = deleteReferences(tx, uri)
545
+
if err != nil {
546
+
return fmt.Errorf("delete reference_links: %w", err)
547
}
548
549
+
return nil
550
}
551
552
+
func CloseIssues(e Execer, filters ...orm.Filter) error {
553
var conditions []string
554
var args []any
555
for _, filter := range filters {
···
567
return err
568
}
569
570
+
func ReopenIssues(e Execer, filters ...orm.Filter) error {
571
var conditions []string
572
var args []any
573
for _, filter := range filters {
+8
-7
appview/db/label.go
+8
-7
appview/db/label.go
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
)
14
15
// no updating type for now
···
59
return id, nil
60
}
61
62
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
63
var conditions []string
64
var args []any
65
for _, filter := range filters {
···
75
return err
76
}
77
78
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79
var labelDefinitions []models.LabelDefinition
80
var conditions []string
81
var args []any
···
167
}
168
169
// helper to get exactly one label def
170
-
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
171
labels, err := GetLabelDefinitions(e, filters...)
172
if err != nil {
173
return nil, err
···
227
return id, nil
228
}
229
230
-
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231
var labelOps []models.LabelOp
232
var conditions []string
233
var args []any
···
302
}
303
304
// get labels for a given list of subject URIs
305
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
306
ops, err := GetLabelOps(e, filters...)
307
if err != nil {
308
return nil, err
···
322
}
323
labelAts := slices.Collect(maps.Keys(labelAtSet))
324
325
-
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
326
if err != nil {
327
return nil, err
328
}
···
338
return results, nil
339
}
340
341
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
342
labels, err := GetLabelDefinitions(e, filters...)
343
if err != nil {
344
return nil, err
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
+
"tangled.org/core/orm"
14
)
15
16
// no updating type for now
···
60
return id, nil
61
}
62
63
+
func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error {
64
var conditions []string
65
var args []any
66
for _, filter := range filters {
···
76
return err
77
}
78
79
+
func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) {
80
var labelDefinitions []models.LabelDefinition
81
var conditions []string
82
var args []any
···
168
}
169
170
// helper to get exactly one label def
171
+
func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) {
172
labels, err := GetLabelDefinitions(e, filters...)
173
if err != nil {
174
return nil, err
···
228
return id, nil
229
}
230
231
+
func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) {
232
var labelOps []models.LabelOp
233
var conditions []string
234
var args []any
···
303
}
304
305
// get labels for a given list of subject URIs
306
+
func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) {
307
ops, err := GetLabelOps(e, filters...)
308
if err != nil {
309
return nil, err
···
323
}
324
labelAts := slices.Collect(maps.Keys(labelAtSet))
325
326
+
actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts))
327
if err != nil {
328
return nil, err
329
}
···
339
return results, nil
340
}
341
342
+
func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) {
343
labels, err := GetLabelDefinitions(e, filters...)
344
if err != nil {
345
return nil, err
+5
-4
appview/db/language.go
+5
-4
appview/db/language.go
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) {
13
var conditions []string
14
var args []any
15
for _, filter := range filters {
···
85
return nil
86
}
87
88
-
func DeleteRepoLanguages(e Execer, filters ...filter) error {
89
var conditions []string
90
var args []any
91
for _, filter := range filters {
···
107
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
108
err := DeleteRepoLanguages(
109
tx,
110
-
FilterEq("repo_at", repoAt),
111
-
FilterEq("ref", ref),
112
)
113
if err != nil {
114
return fmt.Errorf("failed to delete existing languages: %w", err)
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) {
14
var conditions []string
15
var args []any
16
for _, filter := range filters {
···
86
return nil
87
}
88
89
+
func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error {
90
var conditions []string
91
var args []any
92
for _, filter := range filters {
···
108
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
109
err := DeleteRepoLanguages(
110
tx,
111
+
orm.FilterEq("repo_at", repoAt),
112
+
orm.FilterEq("ref", ref),
113
)
114
if err != nil {
115
return fmt.Errorf("failed to delete existing languages: %w", err)
+14
-13
appview/db/notifications.go
+14
-13
appview/db/notifications.go
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
)
15
16
func CreateNotification(e Execer, notification *models.Notification) error {
···
44
}
45
46
// GetNotificationsPaginated retrieves notifications with filters and pagination
47
-
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
48
var conditions []string
49
var args []any
50
···
113
}
114
115
// GetNotificationsWithEntities retrieves notifications with their related entities
116
-
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
117
var conditions []string
118
var args []any
119
···
256
}
257
258
// GetNotifications retrieves notifications with filters
259
-
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
260
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
261
}
262
263
-
func CountNotifications(e Execer, filters ...filter) (int64, error) {
264
var conditions []string
265
var args []any
266
for _, filter := range filters {
···
285
}
286
287
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
288
-
idFilter := FilterEq("id", notificationID)
289
-
recipientFilter := FilterEq("recipient_did", userDID)
290
291
query := fmt.Sprintf(`
292
UPDATE notifications
···
314
}
315
316
func MarkAllNotificationsRead(e Execer, userDID string) error {
317
-
recipientFilter := FilterEq("recipient_did", userDID)
318
-
readFilter := FilterEq("read", 0)
319
320
query := fmt.Sprintf(`
321
UPDATE notifications
···
334
}
335
336
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
337
-
idFilter := FilterEq("id", notificationID)
338
-
recipientFilter := FilterEq("recipient_did", userDID)
339
340
query := fmt.Sprintf(`
341
DELETE FROM notifications
···
362
}
363
364
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
365
-
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
366
if err != nil {
367
return nil, err
368
}
···
375
return p, nil
376
}
377
378
-
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
379
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
380
381
var conditions []string
···
483
484
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
485
cutoff := time.Now().Add(-olderThan)
486
-
createdFilter := FilterLte("created", cutoff)
487
488
query := fmt.Sprintf(`
489
DELETE FROM notifications
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
func CreateNotification(e Execer, notification *models.Notification) error {
···
45
}
46
47
// GetNotificationsPaginated retrieves notifications with filters and pagination
48
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) {
49
var conditions []string
50
var args []any
51
···
114
}
115
116
// GetNotificationsWithEntities retrieves notifications with their related entities
117
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) {
118
var conditions []string
119
var args []any
120
···
257
}
258
259
// GetNotifications retrieves notifications with filters
260
+
func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) {
261
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
262
}
263
264
+
func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) {
265
var conditions []string
266
var args []any
267
for _, filter := range filters {
···
286
}
287
288
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
289
+
idFilter := orm.FilterEq("id", notificationID)
290
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
291
292
query := fmt.Sprintf(`
293
UPDATE notifications
···
315
}
316
317
func MarkAllNotificationsRead(e Execer, userDID string) error {
318
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
319
+
readFilter := orm.FilterEq("read", 0)
320
321
query := fmt.Sprintf(`
322
UPDATE notifications
···
335
}
336
337
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
338
+
idFilter := orm.FilterEq("id", notificationID)
339
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
340
341
query := fmt.Sprintf(`
342
DELETE FROM notifications
···
363
}
364
365
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
366
+
prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid))
367
if err != nil {
368
return nil, err
369
}
···
376
return p, nil
377
}
378
379
+
func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) {
380
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
381
382
var conditions []string
···
484
485
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
486
cutoff := time.Now().Add(-olderThan)
487
+
createdFilter := orm.FilterLte("created", cutoff)
488
489
query := fmt.Sprintf(`
490
DELETE FROM notifications
+6
-5
appview/db/pipeline.go
+6
-5
appview/db/pipeline.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13
var pipelines []models.Pipeline
14
15
var conditions []string
···
168
169
// this is a mega query, but the most useful one:
170
// get N pipelines, for each one get the latest status of its N workflows
171
-
func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172
var conditions []string
173
var args []any
174
for _, filter := range filters {
175
-
filter.key = "p." + filter.key // the table is aliased in the query to `p`
176
conditions = append(conditions, filter.Condition())
177
args = append(args, filter.Arg()...)
178
}
···
264
conditions = nil
265
args = nil
266
for _, p := range pipelines {
267
-
knotFilter := FilterEq("pipeline_knot", p.Knot)
268
-
rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
269
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270
args = append(args, p.Knot)
271
args = append(args, p.Rkey)
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
14
var pipelines []models.Pipeline
15
16
var conditions []string
···
169
170
// this is a mega query, but the most useful one:
171
// get N pipelines, for each one get the latest status of its N workflows
172
+
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
173
var conditions []string
174
var args []any
175
for _, filter := range filters {
176
+
filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
177
conditions = append(conditions, filter.Condition())
178
args = append(args, filter.Arg()...)
179
}
···
265
conditions = nil
266
args = nil
267
for _, p := range pipelines {
268
+
knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269
+
rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
270
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
271
args = append(args, p.Knot)
272
args = append(args, p.Rkey)
+6
-5
appview/db/profile.go
+6
-5
appview/db/profile.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
const TimeframeMonths = 7
···
44
45
issues, err := GetIssues(
46
e,
47
-
FilterEq("did", forDid),
48
-
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49
)
50
if err != nil {
51
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
65
*items = append(*items, &issue)
66
}
67
68
-
repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69
if err != nil {
70
return nil, fmt.Errorf("error getting all repos by did: %w", err)
71
}
···
199
return tx.Commit()
200
}
201
202
-
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
203
var conditions []string
204
var args []any
205
for _, filter := range filters {
···
441
}
442
443
// ensure all pinned repos are either own repos or collaborating repos
444
-
repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
445
if err != nil {
446
log.Printf("getting repos for %s: %s", profile.Did, err)
447
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
const TimeframeMonths = 7
···
45
46
issues, err := GetIssues(
47
e,
48
+
orm.FilterEq("did", forDid),
49
+
orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
50
)
51
if err != nil {
52
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
66
*items = append(*items, &issue)
67
}
68
69
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
70
if err != nil {
71
return nil, fmt.Errorf("error getting all repos by did: %w", err)
72
}
···
200
return tx.Commit()
201
}
202
203
+
func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
204
var conditions []string
205
var args []any
206
for _, filter := range filters {
···
442
}
443
444
// ensure all pinned repos are either own repos or collaborating repos
445
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
446
if err != nil {
447
log.Printf("getting repos for %s: %s", profile.Did, err)
448
}
+69
-24
appview/db/pulls.go
+69
-24
appview/db/pulls.go
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
)
17
18
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
93
insert into pull_submissions (pull_at, round_number, patch, combined, source_rev)
94
values (?, ?, ?, ?, ?)
95
`, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev)
96
-
return err
97
}
98
99
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
···
110
return pullId - 1, err
111
}
112
113
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
114
pulls := make(map[syntax.ATURI]*models.Pull)
115
116
var conditions []string
···
221
for _, p := range pulls {
222
pullAts = append(pullAts, p.AtUri())
223
}
224
-
submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
225
if err != nil {
226
return nil, fmt.Errorf("failed to get submissions: %w", err)
227
}
···
233
}
234
235
// collect allLabels for each issue
236
-
allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
237
if err != nil {
238
return nil, fmt.Errorf("failed to query labels: %w", err)
239
}
···
250
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
251
}
252
}
253
-
sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
254
if err != nil && !errors.Is(err, sql.ErrNoRows) {
255
return nil, fmt.Errorf("failed to get source repos: %w", err)
256
}
···
266
}
267
}
268
269
orderedByPullId := []*models.Pull{}
270
for _, p := range pulls {
271
orderedByPullId = append(orderedByPullId, p)
···
277
return orderedByPullId, nil
278
}
279
280
-
func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
281
return GetPullsWithLimit(e, 0, filters...)
282
}
283
284
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
285
var ids []int64
286
287
-
var filters []filter
288
-
filters = append(filters, FilterEq("state", opts.State))
289
if opts.RepoAt != "" {
290
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
291
}
292
293
var conditions []string
···
343
}
344
345
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
346
-
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
347
if err != nil {
348
return nil, err
349
}
···
355
}
356
357
// mapping from pull -> pull submissions
358
-
func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
359
var conditions []string
360
var args []any
361
for _, filter := range filters {
···
430
431
// Get comments for all submissions using GetPullComments
432
submissionIds := slices.Collect(maps.Keys(submissionMap))
433
-
comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
434
if err != nil {
435
-
return nil, err
436
}
437
for _, comment := range comments {
438
if submission, ok := submissionMap[comment.SubmissionId]; ok {
···
456
return m, nil
457
}
458
459
-
func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
460
var conditions []string
461
var args []any
462
for _, filter := range filters {
···
492
}
493
defer rows.Close()
494
495
-
var comments []models.PullComment
496
for rows.Next() {
497
var comment models.PullComment
498
var createdAt string
···
514
comment.Created = t
515
}
516
517
-
comments = append(comments, comment)
518
}
519
520
if err := rows.Err(); err != nil {
521
return nil, err
522
}
523
524
return comments, nil
525
}
526
···
600
return pulls, nil
601
}
602
603
-
func NewPullComment(e Execer, comment *models.PullComment) (int64, error) {
604
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
605
-
res, err := e.Exec(
606
query,
607
comment.OwnerDid,
608
comment.RepoAt,
···
618
i, err := res.LastInsertId()
619
if err != nil {
620
return 0, err
621
}
622
623
return i, nil
···
664
return err
665
}
666
667
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
668
var conditions []string
669
var args []any
670
···
688
689
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
690
// otherwise submissions are immutable
691
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
692
var conditions []string
693
var args []any
694
···
746
func GetStack(e Execer, stackId string) (models.Stack, error) {
747
unorderedPulls, err := GetPulls(
748
e,
749
-
FilterEq("stack_id", stackId),
750
-
FilterNotEq("state", models.PullDeleted),
751
)
752
if err != nil {
753
return nil, err
···
791
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
792
pulls, err := GetPulls(
793
e,
794
-
FilterEq("stack_id", stackId),
795
-
FilterEq("state", models.PullDeleted),
796
)
797
if err != nil {
798
return nil, err
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
+
"tangled.org/core/orm"
17
)
18
19
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
94
insert into pull_submissions (pull_at, round_number, patch, combined, source_rev)
95
values (?, ?, ?, ?, ?)
96
`, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev)
97
+
if err != nil {
98
+
return err
99
+
}
100
+
101
+
if err := putReferences(tx, pull.AtUri(), pull.References); err != nil {
102
+
return fmt.Errorf("put reference_links: %w", err)
103
+
}
104
+
105
+
return nil
106
}
107
108
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
···
119
return pullId - 1, err
120
}
121
122
+
func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
123
pulls := make(map[syntax.ATURI]*models.Pull)
124
125
var conditions []string
···
230
for _, p := range pulls {
231
pullAts = append(pullAts, p.AtUri())
232
}
233
+
submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
234
if err != nil {
235
return nil, fmt.Errorf("failed to get submissions: %w", err)
236
}
···
242
}
243
244
// collect allLabels for each issue
245
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
246
if err != nil {
247
return nil, fmt.Errorf("failed to query labels: %w", err)
248
}
···
259
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
260
}
261
}
262
+
sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
263
if err != nil && !errors.Is(err, sql.ErrNoRows) {
264
return nil, fmt.Errorf("failed to get source repos: %w", err)
265
}
···
275
}
276
}
277
278
+
allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279
+
if err != nil {
280
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
281
+
}
282
+
for pullAt, references := range allReferences {
283
+
if pull, ok := pulls[pullAt]; ok {
284
+
pull.References = references
285
+
}
286
+
}
287
+
288
orderedByPullId := []*models.Pull{}
289
for _, p := range pulls {
290
orderedByPullId = append(orderedByPullId, p)
···
296
return orderedByPullId, nil
297
}
298
299
+
func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
300
return GetPullsWithLimit(e, 0, filters...)
301
}
302
303
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
304
var ids []int64
305
306
+
var filters []orm.Filter
307
+
filters = append(filters, orm.FilterEq("state", opts.State))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
362
}
363
364
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
365
+
pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
366
if err != nil {
367
return nil, err
368
}
···
374
}
375
376
// mapping from pull -> pull submissions
377
+
func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
378
var conditions []string
379
var args []any
380
for _, filter := range filters {
···
449
450
// Get comments for all submissions using GetPullComments
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
452
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453
if err != nil {
454
+
return nil, fmt.Errorf("failed to get pull comments: %w", err)
455
}
456
for _, comment := range comments {
457
if submission, ok := submissionMap[comment.SubmissionId]; ok {
···
475
return m, nil
476
}
477
478
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479
var conditions []string
480
var args []any
481
for _, filter := range filters {
···
511
}
512
defer rows.Close()
513
514
+
commentMap := make(map[string]*models.PullComment)
515
for rows.Next() {
516
var comment models.PullComment
517
var createdAt string
···
533
comment.Created = t
534
}
535
536
+
atUri := comment.AtUri().String()
537
+
commentMap[atUri] = &comment
538
}
539
540
if err := rows.Err(); err != nil {
541
return nil, err
542
}
543
544
+
// collect references for each comments
545
+
commentAts := slices.Collect(maps.Keys(commentMap))
546
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547
+
if err != nil {
548
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
549
+
}
550
+
for commentAt, references := range allReferencs {
551
+
if comment, ok := commentMap[commentAt.String()]; ok {
552
+
comment.References = references
553
+
}
554
+
}
555
+
556
+
var comments []models.PullComment
557
+
for _, c := range commentMap {
558
+
comments = append(comments, *c)
559
+
}
560
+
561
+
sort.Slice(comments, func(i, j int) bool {
562
+
return comments[i].Created.Before(comments[j].Created)
563
+
})
564
+
565
return comments, nil
566
}
567
···
641
return pulls, nil
642
}
643
644
+
func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) {
645
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
646
+
res, err := tx.Exec(
647
query,
648
comment.OwnerDid,
649
comment.RepoAt,
···
659
i, err := res.LastInsertId()
660
if err != nil {
661
return 0, err
662
+
}
663
+
664
+
if err := putReferences(tx, comment.AtUri(), comment.References); err != nil {
665
+
return 0, fmt.Errorf("put reference_links: %w", err)
666
}
667
668
return i, nil
···
709
return err
710
}
711
712
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
713
var conditions []string
714
var args []any
715
···
733
734
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
735
// otherwise submissions are immutable
736
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
737
var conditions []string
738
var args []any
739
···
791
func GetStack(e Execer, stackId string) (models.Stack, error) {
792
unorderedPulls, err := GetPulls(
793
e,
794
+
orm.FilterEq("stack_id", stackId),
795
+
orm.FilterNotEq("state", models.PullDeleted),
796
)
797
if err != nil {
798
return nil, err
···
836
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
837
pulls, err := GetPulls(
838
e,
839
+
orm.FilterEq("stack_id", stackId),
840
+
orm.FilterEq("state", models.PullDeleted),
841
)
842
if err != nil {
843
return nil, err
+2
-1
appview/db/punchcard.go
+2
-1
appview/db/punchcard.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
// this adds to the existing count
···
20
return err
21
}
22
23
-
func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) {
24
punchcard := &models.Punchcard{}
25
now := time.Now()
26
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
// this adds to the existing count
···
21
return err
22
}
23
24
+
func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) {
25
punchcard := &models.Punchcard{}
26
now := time.Now()
27
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+463
appview/db/reference.go
+463
appview/db/reference.go
···
···
1
+
package db
2
+
3
+
import (
4
+
"database/sql"
5
+
"fmt"
6
+
"strings"
7
+
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"tangled.org/core/api/tangled"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
+
)
13
+
14
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
15
+
// It will ignore missing refLinks.
16
+
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17
+
var (
18
+
issueRefs []models.ReferenceLink
19
+
pullRefs []models.ReferenceLink
20
+
)
21
+
for _, ref := range refLinks {
22
+
switch ref.Kind {
23
+
case models.RefKindIssue:
24
+
issueRefs = append(issueRefs, ref)
25
+
case models.RefKindPull:
26
+
pullRefs = append(pullRefs, ref)
27
+
}
28
+
}
29
+
issueUris, err := findIssueReferences(e, issueRefs)
30
+
if err != nil {
31
+
return nil, fmt.Errorf("find issue references: %w", err)
32
+
}
33
+
pullUris, err := findPullReferences(e, pullRefs)
34
+
if err != nil {
35
+
return nil, fmt.Errorf("find pull references: %w", err)
36
+
}
37
+
38
+
return append(issueUris, pullUris...), nil
39
+
}
40
+
41
+
func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
42
+
if len(refLinks) == 0 {
43
+
return nil, nil
44
+
}
45
+
vals := make([]string, len(refLinks))
46
+
args := make([]any, 0, len(refLinks)*4)
47
+
for i, ref := range refLinks {
48
+
vals[i] = "(?, ?, ?, ?)"
49
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
50
+
}
51
+
query := fmt.Sprintf(
52
+
`with input(owner_did, name, issue_id, comment_id) as (
53
+
values %s
54
+
)
55
+
select
56
+
i.did, i.rkey,
57
+
c.did, c.rkey
58
+
from input inp
59
+
join repos r
60
+
on r.did = inp.owner_did
61
+
and r.name = inp.name
62
+
join issues i
63
+
on i.repo_at = r.at_uri
64
+
and i.issue_id = inp.issue_id
65
+
left join issue_comments c
66
+
on inp.comment_id is not null
67
+
and c.issue_at = i.at_uri
68
+
and c.id = inp.comment_id
69
+
`,
70
+
strings.Join(vals, ","),
71
+
)
72
+
rows, err := e.Query(query, args...)
73
+
if err != nil {
74
+
return nil, err
75
+
}
76
+
defer rows.Close()
77
+
78
+
var uris []syntax.ATURI
79
+
80
+
for rows.Next() {
81
+
// Scan rows
82
+
var issueOwner, issueRkey string
83
+
var commentOwner, commentRkey sql.NullString
84
+
var uri syntax.ATURI
85
+
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
86
+
return nil, err
87
+
}
88
+
if commentOwner.Valid && commentRkey.Valid {
89
+
uri = syntax.ATURI(fmt.Sprintf(
90
+
"at://%s/%s/%s",
91
+
commentOwner.String,
92
+
tangled.RepoIssueCommentNSID,
93
+
commentRkey.String,
94
+
))
95
+
} else {
96
+
uri = syntax.ATURI(fmt.Sprintf(
97
+
"at://%s/%s/%s",
98
+
issueOwner,
99
+
tangled.RepoIssueNSID,
100
+
issueRkey,
101
+
))
102
+
}
103
+
uris = append(uris, uri)
104
+
}
105
+
if err := rows.Err(); err != nil {
106
+
return nil, fmt.Errorf("iterate rows: %w", err)
107
+
}
108
+
109
+
return uris, nil
110
+
}
111
+
112
+
func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
113
+
if len(refLinks) == 0 {
114
+
return nil, nil
115
+
}
116
+
vals := make([]string, len(refLinks))
117
+
args := make([]any, 0, len(refLinks)*4)
118
+
for i, ref := range refLinks {
119
+
vals[i] = "(?, ?, ?, ?)"
120
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
121
+
}
122
+
query := fmt.Sprintf(
123
+
`with input(owner_did, name, pull_id, comment_id) as (
124
+
values %s
125
+
)
126
+
select
127
+
p.owner_did, p.rkey,
128
+
c.comment_at
129
+
from input inp
130
+
join repos r
131
+
on r.did = inp.owner_did
132
+
and r.name = inp.name
133
+
join pulls p
134
+
on p.repo_at = r.at_uri
135
+
and p.pull_id = inp.pull_id
136
+
left join pull_comments c
137
+
on inp.comment_id is not null
138
+
and c.repo_at = r.at_uri and c.pull_id = p.pull_id
139
+
and c.id = inp.comment_id
140
+
`,
141
+
strings.Join(vals, ","),
142
+
)
143
+
rows, err := e.Query(query, args...)
144
+
if err != nil {
145
+
return nil, err
146
+
}
147
+
defer rows.Close()
148
+
149
+
var uris []syntax.ATURI
150
+
151
+
for rows.Next() {
152
+
// Scan rows
153
+
var pullOwner, pullRkey string
154
+
var commentUri sql.NullString
155
+
var uri syntax.ATURI
156
+
if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil {
157
+
return nil, err
158
+
}
159
+
if commentUri.Valid {
160
+
// no-op
161
+
uri = syntax.ATURI(commentUri.String)
162
+
} else {
163
+
uri = syntax.ATURI(fmt.Sprintf(
164
+
"at://%s/%s/%s",
165
+
pullOwner,
166
+
tangled.RepoPullNSID,
167
+
pullRkey,
168
+
))
169
+
}
170
+
uris = append(uris, uri)
171
+
}
172
+
return uris, nil
173
+
}
174
+
175
+
func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error {
176
+
err := deleteReferences(tx, fromAt)
177
+
if err != nil {
178
+
return fmt.Errorf("delete old reference_links: %w", err)
179
+
}
180
+
if len(references) == 0 {
181
+
return nil
182
+
}
183
+
184
+
values := make([]string, 0, len(references))
185
+
args := make([]any, 0, len(references)*2)
186
+
for _, ref := range references {
187
+
values = append(values, "(?, ?)")
188
+
args = append(args, fromAt, ref)
189
+
}
190
+
_, err = tx.Exec(
191
+
fmt.Sprintf(
192
+
`insert into reference_links (from_at, to_at)
193
+
values %s`,
194
+
strings.Join(values, ","),
195
+
),
196
+
args...,
197
+
)
198
+
if err != nil {
199
+
return fmt.Errorf("insert new reference_links: %w", err)
200
+
}
201
+
return nil
202
+
}
203
+
204
+
func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error {
205
+
_, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt)
206
+
return err
207
+
}
208
+
209
+
func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
210
+
var (
211
+
conditions []string
212
+
args []any
213
+
)
214
+
for _, filter := range filters {
215
+
conditions = append(conditions, filter.Condition())
216
+
args = append(args, filter.Arg()...)
217
+
}
218
+
219
+
whereClause := ""
220
+
if conditions != nil {
221
+
whereClause = " where " + strings.Join(conditions, " and ")
222
+
}
223
+
224
+
rows, err := e.Query(
225
+
fmt.Sprintf(
226
+
`select from_at, to_at from reference_links %s`,
227
+
whereClause,
228
+
),
229
+
args...,
230
+
)
231
+
if err != nil {
232
+
return nil, fmt.Errorf("query reference_links: %w", err)
233
+
}
234
+
defer rows.Close()
235
+
236
+
result := make(map[syntax.ATURI][]syntax.ATURI)
237
+
238
+
for rows.Next() {
239
+
var from, to syntax.ATURI
240
+
if err := rows.Scan(&from, &to); err != nil {
241
+
return nil, fmt.Errorf("scan row: %w", err)
242
+
}
243
+
244
+
result[from] = append(result[from], to)
245
+
}
246
+
if err := rows.Err(); err != nil {
247
+
return nil, fmt.Errorf("iterate rows: %w", err)
248
+
}
249
+
250
+
return result, nil
251
+
}
252
+
253
+
func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) {
254
+
rows, err := e.Query(
255
+
`select from_at from reference_links
256
+
where to_at = ?`,
257
+
target,
258
+
)
259
+
if err != nil {
260
+
return nil, fmt.Errorf("query backlinks: %w", err)
261
+
}
262
+
defer rows.Close()
263
+
264
+
var (
265
+
backlinks []models.RichReferenceLink
266
+
backlinksMap = make(map[string][]syntax.ATURI)
267
+
)
268
+
for rows.Next() {
269
+
var from syntax.ATURI
270
+
if err := rows.Scan(&from); err != nil {
271
+
return nil, fmt.Errorf("scan row: %w", err)
272
+
}
273
+
nsid := from.Collection().String()
274
+
backlinksMap[nsid] = append(backlinksMap[nsid], from)
275
+
}
276
+
if err := rows.Err(); err != nil {
277
+
return nil, fmt.Errorf("iterate rows: %w", err)
278
+
}
279
+
280
+
var ls []models.RichReferenceLink
281
+
ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID])
282
+
if err != nil {
283
+
return nil, fmt.Errorf("get issue backlinks: %w", err)
284
+
}
285
+
backlinks = append(backlinks, ls...)
286
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
287
+
if err != nil {
288
+
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
289
+
}
290
+
backlinks = append(backlinks, ls...)
291
+
ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID])
292
+
if err != nil {
293
+
return nil, fmt.Errorf("get pull backlinks: %w", err)
294
+
}
295
+
backlinks = append(backlinks, ls...)
296
+
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID])
297
+
if err != nil {
298
+
return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
299
+
}
300
+
backlinks = append(backlinks, ls...)
301
+
302
+
return backlinks, nil
303
+
}
304
+
305
+
func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
306
+
if len(aturis) == 0 {
307
+
return nil, nil
308
+
}
309
+
vals := make([]string, len(aturis))
310
+
args := make([]any, 0, len(aturis)*2)
311
+
for i, aturi := range aturis {
312
+
vals[i] = "(?, ?)"
313
+
did := aturi.Authority().String()
314
+
rkey := aturi.RecordKey().String()
315
+
args = append(args, did, rkey)
316
+
}
317
+
rows, err := e.Query(
318
+
fmt.Sprintf(
319
+
`select r.did, r.name, i.issue_id, i.title, i.open
320
+
from issues i
321
+
join repos r
322
+
on r.at_uri = i.repo_at
323
+
where (i.did, i.rkey) in (%s)`,
324
+
strings.Join(vals, ","),
325
+
),
326
+
args...,
327
+
)
328
+
if err != nil {
329
+
return nil, err
330
+
}
331
+
defer rows.Close()
332
+
var refLinks []models.RichReferenceLink
333
+
for rows.Next() {
334
+
var l models.RichReferenceLink
335
+
l.Kind = models.RefKindIssue
336
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
337
+
return nil, err
338
+
}
339
+
refLinks = append(refLinks, l)
340
+
}
341
+
if err := rows.Err(); err != nil {
342
+
return nil, fmt.Errorf("iterate rows: %w", err)
343
+
}
344
+
return refLinks, nil
345
+
}
346
+
347
+
func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
348
+
if len(aturis) == 0 {
349
+
return nil, nil
350
+
}
351
+
filter := orm.FilterIn("c.at_uri", aturis)
352
+
rows, err := e.Query(
353
+
fmt.Sprintf(
354
+
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
355
+
from issue_comments c
356
+
join issues i
357
+
on i.at_uri = c.issue_at
358
+
join repos r
359
+
on r.at_uri = i.repo_at
360
+
where %s`,
361
+
filter.Condition(),
362
+
),
363
+
filter.Arg()...,
364
+
)
365
+
if err != nil {
366
+
return nil, err
367
+
}
368
+
defer rows.Close()
369
+
var refLinks []models.RichReferenceLink
370
+
for rows.Next() {
371
+
var l models.RichReferenceLink
372
+
l.Kind = models.RefKindIssue
373
+
l.CommentId = new(int)
374
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
375
+
return nil, err
376
+
}
377
+
refLinks = append(refLinks, l)
378
+
}
379
+
if err := rows.Err(); err != nil {
380
+
return nil, fmt.Errorf("iterate rows: %w", err)
381
+
}
382
+
return refLinks, nil
383
+
}
384
+
385
+
func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
386
+
if len(aturis) == 0 {
387
+
return nil, nil
388
+
}
389
+
vals := make([]string, len(aturis))
390
+
args := make([]any, 0, len(aturis)*2)
391
+
for i, aturi := range aturis {
392
+
vals[i] = "(?, ?)"
393
+
did := aturi.Authority().String()
394
+
rkey := aturi.RecordKey().String()
395
+
args = append(args, did, rkey)
396
+
}
397
+
rows, err := e.Query(
398
+
fmt.Sprintf(
399
+
`select r.did, r.name, p.pull_id, p.title, p.state
400
+
from pulls p
401
+
join repos r
402
+
on r.at_uri = p.repo_at
403
+
where (p.owner_did, p.rkey) in (%s)`,
404
+
strings.Join(vals, ","),
405
+
),
406
+
args...,
407
+
)
408
+
if err != nil {
409
+
return nil, err
410
+
}
411
+
defer rows.Close()
412
+
var refLinks []models.RichReferenceLink
413
+
for rows.Next() {
414
+
var l models.RichReferenceLink
415
+
l.Kind = models.RefKindPull
416
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
417
+
return nil, err
418
+
}
419
+
refLinks = append(refLinks, l)
420
+
}
421
+
if err := rows.Err(); err != nil {
422
+
return nil, fmt.Errorf("iterate rows: %w", err)
423
+
}
424
+
return refLinks, nil
425
+
}
426
+
427
+
func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
428
+
if len(aturis) == 0 {
429
+
return nil, nil
430
+
}
431
+
filter := orm.FilterIn("c.comment_at", aturis)
432
+
rows, err := e.Query(
433
+
fmt.Sprintf(
434
+
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
435
+
from repos r
436
+
join pulls p
437
+
on r.at_uri = p.repo_at
438
+
join pull_comments c
439
+
on r.at_uri = c.repo_at and p.pull_id = c.pull_id
440
+
where %s`,
441
+
filter.Condition(),
442
+
),
443
+
filter.Arg()...,
444
+
)
445
+
if err != nil {
446
+
return nil, err
447
+
}
448
+
defer rows.Close()
449
+
var refLinks []models.RichReferenceLink
450
+
for rows.Next() {
451
+
var l models.RichReferenceLink
452
+
l.Kind = models.RefKindPull
453
+
l.CommentId = new(int)
454
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
455
+
return nil, err
456
+
}
457
+
refLinks = append(refLinks, l)
458
+
}
459
+
if err := rows.Err(); err != nil {
460
+
return nil, fmt.Errorf("iterate rows: %w", err)
461
+
}
462
+
return refLinks, nil
463
+
}
+4
-3
appview/db/registration.go
+4
-3
appview/db/registration.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) {
13
var registrations []models.Registration
14
15
var conditions []string
···
69
return registrations, nil
70
}
71
72
-
func MarkRegistered(e Execer, filters ...filter) error {
73
var conditions []string
74
var args []any
75
for _, filter := range filters {
···
94
return err
95
}
96
97
-
func DeleteKnot(e Execer, filters ...filter) error {
98
var conditions []string
99
var args []any
100
for _, filter := range filters {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) {
14
var registrations []models.Registration
15
16
var conditions []string
···
70
return registrations, nil
71
}
72
73
+
func MarkRegistered(e Execer, filters ...orm.Filter) error {
74
var conditions []string
75
var args []any
76
for _, filter := range filters {
···
95
return err
96
}
97
98
+
func DeleteKnot(e Execer, filters ...orm.Filter) error {
99
var conditions []string
100
var args []any
101
for _, filter := range filters {
+17
-33
appview/db/repos.go
+17
-33
appview/db/repos.go
···
10
"time"
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
-
securejoin "github.com/cyphar/filepath-securejoin"
14
-
"tangled.org/core/api/tangled"
15
"tangled.org/core/appview/models"
16
)
17
18
-
type Repo struct {
19
-
Id int64
20
-
Did string
21
-
Name string
22
-
Knot string
23
-
Rkey string
24
-
Created time.Time
25
-
Description string
26
-
Spindle string
27
-
28
-
// optionally, populate this when querying for reverse mappings
29
-
RepoStats *models.RepoStats
30
-
31
-
// optional
32
-
Source string
33
-
}
34
-
35
-
func (r Repo) RepoAt() syntax.ATURI {
36
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
37
-
}
38
-
39
-
func (r Repo) DidSlashRepo() string {
40
-
p, _ := securejoin.SecureJoin(r.Did, r.Name)
41
-
return p
42
-
}
43
-
44
-
func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
45
repoMap := make(map[syntax.ATURI]*models.Repo)
46
47
var conditions []string
···
322
}
323
324
// helper to get exactly one repo
325
-
func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
326
repos, err := GetRepos(e, 0, filters...)
327
if err != nil {
328
return nil, err
···
339
return &repos[0], nil
340
}
341
342
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
343
var conditions []string
344
var args []any
345
for _, filter := range filters {
···
439
return nullableSource.String, nil
440
}
441
442
func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
443
var repos []models.Repo
444
···
559
return err
560
}
561
562
-
func UnsubscribeLabel(e Execer, filters ...filter) error {
563
var conditions []string
564
var args []any
565
for _, filter := range filters {
···
577
return err
578
}
579
580
-
func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
581
var conditions []string
582
var args []any
583
for _, filter := range filters {
···
10
"time"
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
+
func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) {
18
repoMap := make(map[syntax.ATURI]*models.Repo)
19
20
var conditions []string
···
295
}
296
297
// helper to get exactly one repo
298
+
func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
299
repos, err := GetRepos(e, 0, filters...)
300
if err != nil {
301
return nil, err
···
312
return &repos[0], nil
313
}
314
315
+
func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
316
var conditions []string
317
var args []any
318
for _, filter := range filters {
···
412
return nullableSource.String, nil
413
}
414
415
+
func GetRepoSourceRepo(e Execer, repoAt syntax.ATURI) (*models.Repo, error) {
416
+
source, err := GetRepoSource(e, repoAt)
417
+
if source == "" || errors.Is(err, sql.ErrNoRows) {
418
+
return nil, nil
419
+
}
420
+
if err != nil {
421
+
return nil, err
422
+
}
423
+
return GetRepoByAtUri(e, source)
424
+
}
425
+
426
func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
427
var repos []models.Repo
428
···
543
return err
544
}
545
546
+
func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
547
var conditions []string
548
var args []any
549
for _, filter := range filters {
···
561
return err
562
}
563
564
+
func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
565
var conditions []string
566
var args []any
567
for _, filter := range filters {
+6
-5
appview/db/spindle.go
+6
-5
appview/db/spindle.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) {
13
var spindles []models.Spindle
14
15
var conditions []string
···
91
return err
92
}
93
94
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
95
var conditions []string
96
var args []any
97
for _, filter := range filters {
···
114
return res.RowsAffected()
115
}
116
117
-
func DeleteSpindle(e Execer, filters ...filter) error {
118
var conditions []string
119
var args []any
120
for _, filter := range filters {
···
144
return err
145
}
146
147
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
148
var conditions []string
149
var args []any
150
for _, filter := range filters {
···
163
return err
164
}
165
166
-
func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) {
167
var members []models.SpindleMember
168
169
var conditions []string
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) {
14
var spindles []models.Spindle
15
16
var conditions []string
···
92
return err
93
}
94
95
+
func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) {
96
var conditions []string
97
var args []any
98
for _, filter := range filters {
···
115
return res.RowsAffected()
116
}
117
118
+
func DeleteSpindle(e Execer, filters ...orm.Filter) error {
119
var conditions []string
120
var args []any
121
for _, filter := range filters {
···
145
return err
146
}
147
148
+
func RemoveSpindleMember(e Execer, filters ...orm.Filter) error {
149
var conditions []string
150
var args []any
151
for _, filter := range filters {
···
164
return err
165
}
166
167
+
func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) {
168
var members []models.SpindleMember
169
170
var conditions []string
+5
-4
appview/db/star.go
+5
-4
appview/db/star.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
func AddStar(e Execer, star *models.Star) error {
···
133
134
// GetRepoStars return a list of stars each holding target repository.
135
// If there isn't known repo with starred at-uri, those stars will be ignored.
136
-
func GetRepoStars(e Execer, limit int, filters ...filter) ([]models.RepoStar, error) {
137
var conditions []string
138
var args []any
139
for _, filter := range filters {
···
195
return nil, nil
196
}
197
198
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
199
if err != nil {
200
return nil, err
201
}
···
225
return repoStars, nil
226
}
227
228
-
func CountStars(e Execer, filters ...filter) (int64, error) {
229
var conditions []string
230
var args []any
231
for _, filter := range filters {
···
298
}
299
300
// get full repo data
301
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
302
if err != nil {
303
return nil, err
304
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
func AddStar(e Execer, star *models.Star) error {
···
134
135
// GetRepoStars return a list of stars each holding target repository.
136
// If there isn't known repo with starred at-uri, those stars will be ignored.
137
+
func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
138
var conditions []string
139
var args []any
140
for _, filter := range filters {
···
196
return nil, nil
197
}
198
199
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
200
if err != nil {
201
return nil, err
202
}
···
226
return repoStars, nil
227
}
228
229
+
func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
230
var conditions []string
231
var args []any
232
for _, filter := range filters {
···
299
}
300
301
// get full repo data
302
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
303
if err != nil {
304
return nil, err
305
}
+4
-3
appview/db/strings.go
+4
-3
appview/db/strings.go
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
)
12
13
func AddString(e Execer, s models.String) error {
···
44
return err
45
}
46
47
-
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
48
var all []models.String
49
50
var conditions []string
···
127
return all, nil
128
}
129
130
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
131
var conditions []string
132
var args []any
133
for _, filter := range filters {
···
151
return count, nil
152
}
153
154
-
func DeleteString(e Execer, filters ...filter) error {
155
var conditions []string
156
var args []any
157
for _, filter := range filters {
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddString(e Execer, s models.String) error {
···
45
return err
46
}
47
48
+
func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) {
49
var all []models.String
50
51
var conditions []string
···
128
return all, nil
129
}
130
131
+
func CountStrings(e Execer, filters ...orm.Filter) (int64, error) {
132
var conditions []string
133
var args []any
134
for _, filter := range filters {
···
152
return count, nil
153
}
154
155
+
func DeleteString(e Execer, filters ...orm.Filter) error {
156
var conditions []string
157
var args []any
158
for _, filter := range filters {
+9
-8
appview/db/timeline.go
+9
-8
appview/db/timeline.go
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
)
9
10
// TODO: this gathers heterogenous events from different sources and aggregates
···
84
}
85
86
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87
-
filters := make([]filter, 0)
88
if userIsFollowing != nil {
89
-
filters = append(filters, FilterIn("did", userIsFollowing))
90
}
91
92
repos, err := GetRepos(e, limit, filters...)
···
104
105
var origRepos []models.Repo
106
if args != nil {
107
-
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
108
}
109
if err != nil {
110
return nil, err
···
144
}
145
146
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147
-
filters := make([]filter, 0)
148
if userIsFollowing != nil {
149
-
filters = append(filters, FilterIn("did", userIsFollowing))
150
}
151
152
stars, err := GetRepoStars(e, limit, filters...)
···
180
}
181
182
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
183
-
filters := make([]filter, 0)
184
if userIsFollowing != nil {
185
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
186
}
187
188
follows, err := GetFollows(e, limit, filters...)
···
199
return nil, nil
200
}
201
202
-
profiles, err := GetProfiles(e, FilterIn("did", subjects))
203
if err != nil {
204
return nil, err
205
}
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
+
"tangled.org/core/orm"
9
)
10
11
// TODO: this gathers heterogenous events from different sources and aggregates
···
85
}
86
87
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
88
+
filters := make([]orm.Filter, 0)
89
if userIsFollowing != nil {
90
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
91
}
92
93
repos, err := GetRepos(e, limit, filters...)
···
105
106
var origRepos []models.Repo
107
if args != nil {
108
+
origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args))
109
}
110
if err != nil {
111
return nil, err
···
145
}
146
147
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
148
+
filters := make([]orm.Filter, 0)
149
if userIsFollowing != nil {
150
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
151
}
152
153
stars, err := GetRepoStars(e, limit, filters...)
···
181
}
182
183
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
184
+
filters := make([]orm.Filter, 0)
185
if userIsFollowing != nil {
186
+
filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
187
}
188
189
follows, err := GetFollows(e, limit, filters...)
···
200
return nil, nil
201
}
202
203
+
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
204
if err != nil {
205
return nil, err
206
}
+7
-12
appview/email/email.go
+7
-12
appview/email/email.go
···
3
import (
4
"fmt"
5
"net"
6
-
"regexp"
7
"strings"
8
9
"github.com/resend/resend-go/v2"
···
34
}
35
36
func IsValidEmail(email string) bool {
37
-
// Basic length check
38
-
if len(email) < 3 || len(email) > 254 {
39
return false
40
}
41
42
-
// Regular expression for email validation (RFC 5322 compliant)
43
-
pattern := `^[a-zA-Z0-9.!#$%&'*+/=?^_\x60{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
44
-
45
-
// Compile regex
46
-
regex := regexp.MustCompile(pattern)
47
-
48
-
// Check if email matches regex pattern
49
-
if !regex.MatchString(email) {
50
return false
51
}
52
53
// Split email into local and domain parts
54
-
parts := strings.Split(email, "@")
55
domain := parts[1]
56
57
mx, err := net.LookupMX(domain)
···
3
import (
4
"fmt"
5
"net"
6
+
"net/mail"
7
"strings"
8
9
"github.com/resend/resend-go/v2"
···
34
}
35
36
func IsValidEmail(email string) bool {
37
+
// Reject whitespace (ParseAddress normalizes it away)
38
+
if strings.ContainsAny(email, " \t\n\r") {
39
return false
40
}
41
42
+
// Use stdlib RFC 5322 parser
43
+
addr, err := mail.ParseAddress(email)
44
+
if err != nil {
45
return false
46
}
47
48
// Split email into local and domain parts
49
+
parts := strings.Split(addr.Address, "@")
50
domain := parts[1]
51
52
mx, err := net.LookupMX(domain)
+53
appview/email/email_test.go
+53
appview/email/email_test.go
···
···
1
+
package email
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
func TestIsValidEmail(t *testing.T) {
8
+
tests := []struct {
9
+
name string
10
+
email string
11
+
want bool
12
+
}{
13
+
// Valid emails using RFC 2606 reserved domains
14
+
{"standard email", "user@example.com", true},
15
+
{"single char local", "a@example.com", true},
16
+
{"dot in middle", "first.last@example.com", true},
17
+
{"multiple dots", "a.b.c@example.com", true},
18
+
{"plus tag", "user+tag@example.com", true},
19
+
{"numbers", "user123@example.com", true},
20
+
{"example.org", "user@example.org", true},
21
+
{"example.net", "user@example.net", true},
22
+
23
+
// Invalid format - rejected by mail.ParseAddress
24
+
{"empty string", "", false},
25
+
{"no at sign", "userexample.com", false},
26
+
{"no domain", "user@", false},
27
+
{"no local part", "@example.com", false},
28
+
{"double at", "user@@example.com", false},
29
+
{"just at sign", "@", false},
30
+
{"leading dot", ".user@example.com", false},
31
+
{"trailing dot", "user.@example.com", false},
32
+
{"consecutive dots", "user..name@example.com", false},
33
+
34
+
// Whitespace - rejected before parsing
35
+
{"space in local", "user @example.com", false},
36
+
{"space in domain", "user@ example.com", false},
37
+
{"tab", "user\t@example.com", false},
38
+
{"newline", "user\n@example.com", false},
39
+
40
+
// MX lookup - using RFC 2606 reserved TLDs (guaranteed no MX)
41
+
{"invalid TLD", "user@example.invalid", false},
42
+
{"test TLD", "user@mail.test", false},
43
+
}
44
+
45
+
for _, tt := range tests {
46
+
t.Run(tt.name, func(t *testing.T) {
47
+
got := IsValidEmail(tt.email)
48
+
if got != tt.want {
49
+
t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want)
50
+
}
51
+
})
52
+
}
53
+
}
+47
-29
appview/ingester.go
+47
-29
appview/ingester.go
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
)
26
···
253
254
err = db.AddArtifact(i.Db, artifact)
255
case jmodels.CommitOperationDelete:
256
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
257
}
258
259
if err != nil {
···
350
351
err = db.UpsertProfile(tx, &profile)
352
case jmodels.CommitOperationDelete:
353
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
354
}
355
356
if err != nil {
···
424
// get record from db first
425
members, err := db.GetSpindleMembers(
426
ddb,
427
-
db.FilterEq("did", did),
428
-
db.FilterEq("rkey", rkey),
429
)
430
if err != nil || len(members) != 1 {
431
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
440
// remove record by rkey && update enforcer
441
if err = db.RemoveSpindleMember(
442
tx,
443
-
db.FilterEq("did", did),
444
-
db.FilterEq("rkey", rkey),
445
); err != nil {
446
return fmt.Errorf("failed to remove from db: %w", err)
447
}
···
523
// get record from db first
524
spindles, err := db.GetSpindles(
525
ddb,
526
-
db.FilterEq("owner", did),
527
-
db.FilterEq("instance", instance),
528
)
529
if err != nil || len(spindles) != 1 {
530
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
543
// remove spindle members first
544
err = db.RemoveSpindleMember(
545
tx,
546
-
db.FilterEq("owner", did),
547
-
db.FilterEq("instance", instance),
548
)
549
if err != nil {
550
return err
···
552
553
err = db.DeleteSpindle(
554
tx,
555
-
db.FilterEq("owner", did),
556
-
db.FilterEq("instance", instance),
557
)
558
if err != nil {
559
return err
···
621
case jmodels.CommitOperationDelete:
622
if err := db.DeleteString(
623
ddb,
624
-
db.FilterEq("did", did),
625
-
db.FilterEq("rkey", rkey),
626
); err != nil {
627
l.Error("failed to delete", "err", err)
628
return fmt.Errorf("failed to delete string record: %w", err)
···
740
// get record from db first
741
registrations, err := db.GetRegistrations(
742
ddb,
743
-
db.FilterEq("domain", domain),
744
-
db.FilterEq("did", did),
745
)
746
if err != nil {
747
return fmt.Errorf("failed to get registration: %w", err)
···
762
763
err = db.DeleteKnot(
764
tx,
765
-
db.FilterEq("did", did),
766
-
db.FilterEq("domain", domain),
767
)
768
if err != nil {
769
return err
···
841
return nil
842
843
case jmodels.CommitOperationDelete:
844
if err := db.DeleteIssues(
845
-
ddb,
846
-
db.FilterEq("did", did),
847
-
db.FilterEq("rkey", rkey),
848
); err != nil {
849
l.Error("failed to delete", "err", err)
850
return fmt.Errorf("failed to delete issue record: %w", err)
851
}
852
853
return nil
···
888
return fmt.Errorf("failed to validate comment: %w", err)
889
}
890
891
-
_, err = db.AddIssueComment(ddb, *comment)
892
if err != nil {
893
return fmt.Errorf("failed to create issue comment: %w", err)
894
}
895
896
-
return nil
897
898
case jmodels.CommitOperationDelete:
899
if err := db.DeleteIssueComments(
900
ddb,
901
-
db.FilterEq("did", did),
902
-
db.FilterEq("rkey", rkey),
903
); err != nil {
904
return fmt.Errorf("failed to delete issue comment record: %w", err)
905
}
···
952
case jmodels.CommitOperationDelete:
953
if err := db.DeleteLabelDefinition(
954
ddb,
955
-
db.FilterEq("did", did),
956
-
db.FilterEq("rkey", rkey),
957
); err != nil {
958
return fmt.Errorf("failed to delete labeldef record: %w", err)
959
}
···
993
var repo *models.Repo
994
switch collection {
995
case tangled.RepoIssueNSID:
996
-
i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject))
997
if err != nil || len(i) != 1 {
998
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
999
}
···
1002
return fmt.Errorf("unsupport label subject: %s", collection)
1003
}
1004
1005
-
actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels))
1006
if err != nil {
1007
return fmt.Errorf("failed to build label application ctx: %w", err)
1008
}
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
)
27
···
254
255
err = db.AddArtifact(i.Db, artifact)
256
case jmodels.CommitOperationDelete:
257
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
258
}
259
260
if err != nil {
···
351
352
err = db.UpsertProfile(tx, &profile)
353
case jmodels.CommitOperationDelete:
354
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
355
}
356
357
if err != nil {
···
425
// get record from db first
426
members, err := db.GetSpindleMembers(
427
ddb,
428
+
orm.FilterEq("did", did),
429
+
orm.FilterEq("rkey", rkey),
430
)
431
if err != nil || len(members) != 1 {
432
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
441
// remove record by rkey && update enforcer
442
if err = db.RemoveSpindleMember(
443
tx,
444
+
orm.FilterEq("did", did),
445
+
orm.FilterEq("rkey", rkey),
446
); err != nil {
447
return fmt.Errorf("failed to remove from db: %w", err)
448
}
···
524
// get record from db first
525
spindles, err := db.GetSpindles(
526
ddb,
527
+
orm.FilterEq("owner", did),
528
+
orm.FilterEq("instance", instance),
529
)
530
if err != nil || len(spindles) != 1 {
531
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
544
// remove spindle members first
545
err = db.RemoveSpindleMember(
546
tx,
547
+
orm.FilterEq("owner", did),
548
+
orm.FilterEq("instance", instance),
549
)
550
if err != nil {
551
return err
···
553
554
err = db.DeleteSpindle(
555
tx,
556
+
orm.FilterEq("owner", did),
557
+
orm.FilterEq("instance", instance),
558
)
559
if err != nil {
560
return err
···
622
case jmodels.CommitOperationDelete:
623
if err := db.DeleteString(
624
ddb,
625
+
orm.FilterEq("did", did),
626
+
orm.FilterEq("rkey", rkey),
627
); err != nil {
628
l.Error("failed to delete", "err", err)
629
return fmt.Errorf("failed to delete string record: %w", err)
···
741
// get record from db first
742
registrations, err := db.GetRegistrations(
743
ddb,
744
+
orm.FilterEq("domain", domain),
745
+
orm.FilterEq("did", did),
746
)
747
if err != nil {
748
return fmt.Errorf("failed to get registration: %w", err)
···
763
764
err = db.DeleteKnot(
765
tx,
766
+
orm.FilterEq("did", did),
767
+
orm.FilterEq("domain", domain),
768
)
769
if err != nil {
770
return err
···
842
return nil
843
844
case jmodels.CommitOperationDelete:
845
+
tx, err := ddb.BeginTx(ctx, nil)
846
+
if err != nil {
847
+
l.Error("failed to begin transaction", "err", err)
848
+
return err
849
+
}
850
+
defer tx.Rollback()
851
+
852
if err := db.DeleteIssues(
853
+
tx,
854
+
did,
855
+
rkey,
856
); err != nil {
857
l.Error("failed to delete", "err", err)
858
return fmt.Errorf("failed to delete issue record: %w", err)
859
+
}
860
+
if err := tx.Commit(); err != nil {
861
+
l.Error("failed to commit txn", "err", err)
862
+
return err
863
}
864
865
return nil
···
900
return fmt.Errorf("failed to validate comment: %w", err)
901
}
902
903
+
tx, err := ddb.Begin()
904
+
if err != nil {
905
+
return fmt.Errorf("failed to start transaction: %w", err)
906
+
}
907
+
defer tx.Rollback()
908
+
909
+
_, err = db.AddIssueComment(tx, *comment)
910
if err != nil {
911
return fmt.Errorf("failed to create issue comment: %w", err)
912
}
913
914
+
return tx.Commit()
915
916
case jmodels.CommitOperationDelete:
917
if err := db.DeleteIssueComments(
918
ddb,
919
+
orm.FilterEq("did", did),
920
+
orm.FilterEq("rkey", rkey),
921
); err != nil {
922
return fmt.Errorf("failed to delete issue comment record: %w", err)
923
}
···
970
case jmodels.CommitOperationDelete:
971
if err := db.DeleteLabelDefinition(
972
ddb,
973
+
orm.FilterEq("did", did),
974
+
orm.FilterEq("rkey", rkey),
975
); err != nil {
976
return fmt.Errorf("failed to delete labeldef record: %w", err)
977
}
···
1011
var repo *models.Repo
1012
switch collection {
1013
case tangled.RepoIssueNSID:
1014
+
i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject))
1015
if err != nil || len(i) != 1 {
1016
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
1017
}
···
1020
return fmt.Errorf("unsupport label subject: %s", collection)
1021
}
1022
1023
+
actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels))
1024
if err != nil {
1025
return fmt.Errorf("failed to build label application ctx: %w", err)
1026
}
+143
-135
appview/issues/issues.go
+143
-135
appview/issues/issues.go
···
7
"fmt"
8
"log/slog"
9
"net/http"
10
-
"slices"
11
"time"
12
13
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
20
"tangled.org/core/appview/config"
21
"tangled.org/core/appview/db"
22
issues_indexer "tangled.org/core/appview/indexer/issues"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
-
"tangled.org/core/appview/pages/markup"
28
"tangled.org/core/appview/pagination"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
"tangled.org/core/tid"
33
)
34
35
type Issues struct {
36
-
oauth *oauth.OAuth
37
-
repoResolver *reporesolver.RepoResolver
38
-
pages *pages.Pages
39
-
idResolver *idresolver.Resolver
40
-
db *db.DB
41
-
config *config.Config
42
-
notifier notify.Notifier
43
-
logger *slog.Logger
44
-
validator *validator.Validator
45
-
indexer *issues_indexer.Indexer
46
}
47
48
func New(
49
oauth *oauth.OAuth,
50
repoResolver *reporesolver.RepoResolver,
51
pages *pages.Pages,
52
idResolver *idresolver.Resolver,
53
db *db.DB,
54
config *config.Config,
55
notifier notify.Notifier,
···
58
logger *slog.Logger,
59
) *Issues {
60
return &Issues{
61
-
oauth: oauth,
62
-
repoResolver: repoResolver,
63
-
pages: pages,
64
-
idResolver: idResolver,
65
-
db: db,
66
-
config: config,
67
-
notifier: notifier,
68
-
logger: logger,
69
-
validator: validator,
70
-
indexer: indexer,
71
}
72
}
73
···
97
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
98
}
99
100
labelDefs, err := db.GetLabelDefinitions(
101
rp.db,
102
-
db.FilterIn("at_uri", f.Repo.Labels),
103
-
db.FilterContains("scope", tangled.RepoIssueNSID),
104
)
105
if err != nil {
106
l.Error("failed to fetch labels", "err", err)
···
115
116
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
117
LoggedInUser: user,
118
-
RepoInfo: f.RepoInfo(user),
119
Issue: issue,
120
CommentList: issue.CommentList(),
121
OrderedReactionKinds: models.OrderedReactionKinds,
122
Reactions: reactionMap,
123
UserReacted: userReactions,
···
128
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
129
l := rp.logger.With("handler", "EditIssue")
130
user := rp.oauth.GetUser(r)
131
-
f, err := rp.repoResolver.Resolve(r)
132
-
if err != nil {
133
-
l.Error("failed to get repo and knot", "err", err)
134
-
return
135
-
}
136
137
issue, ok := r.Context().Value("issue").(*models.Issue)
138
if !ok {
···
145
case http.MethodGet:
146
rp.pages.EditIssueFragment(w, pages.EditIssueParams{
147
LoggedInUser: user,
148
-
RepoInfo: f.RepoInfo(user),
149
Issue: issue,
150
})
151
case http.MethodPost:
···
153
newIssue := issue
154
newIssue.Title = r.FormValue("title")
155
newIssue.Body = r.FormValue("body")
156
157
if err := rp.validator.ValidateIssue(newIssue); err != nil {
158
l.Error("validation error", "err", err)
···
222
l := rp.logger.With("handler", "DeleteIssue")
223
noticeId := "issue-actions-error"
224
225
-
user := rp.oauth.GetUser(r)
226
-
227
f, err := rp.repoResolver.Resolve(r)
228
if err != nil {
229
l.Error("failed to get repo and knot", "err", err)
···
238
}
239
l = l.With("did", issue.Did, "rkey", issue.Rkey)
240
241
// delete from PDS
242
client, err := rp.oauth.AuthorizedClient(r)
243
if err != nil {
···
258
}
259
260
// delete from db
261
-
if err := db.DeleteIssues(rp.db, db.FilterEq("id", issue.Id)); err != nil {
262
l.Error("failed to delete issue", "err", err)
263
rp.pages.Notice(w, noticeId, "Failed to delete issue.")
264
return
265
}
266
267
rp.notifier.DeleteIssue(r.Context(), issue)
268
269
// return to all issues page
270
-
rp.pages.HxRedirect(w, "/"+f.RepoInfo(user).FullName()+"/issues")
271
}
272
273
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
···
286
return
287
}
288
289
-
collaborators, err := f.Collaborators(r.Context())
290
-
if err != nil {
291
-
l.Error("failed to fetch repo collaborators", "err", err)
292
-
}
293
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
294
-
return user.Did == collab.Did
295
-
})
296
isIssueOwner := user.Did == issue.Did
297
298
// TODO: make this more granular
299
-
if isIssueOwner || isCollaborator {
300
err = db.CloseIssues(
301
rp.db,
302
-
db.FilterEq("id", issue.Id),
303
)
304
if err != nil {
305
l.Error("failed to close issue", "err", err)
···
312
// notify about the issue closure
313
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
314
315
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
316
return
317
} else {
318
l.Error("user is not permitted to close issue")
···
337
return
338
}
339
340
-
collaborators, err := f.Collaborators(r.Context())
341
-
if err != nil {
342
-
l.Error("failed to fetch repo collaborators", "err", err)
343
-
}
344
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
345
-
return user.Did == collab.Did
346
-
})
347
isIssueOwner := user.Did == issue.Did
348
349
-
if isCollaborator || isIssueOwner {
350
err := db.ReopenIssues(
351
rp.db,
352
-
db.FilterEq("id", issue.Id),
353
)
354
if err != nil {
355
l.Error("failed to reopen issue", "err", err)
···
362
// notify about the issue reopen
363
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
364
365
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
366
return
367
} else {
368
l.Error("user is not the owner of the repo")
···
399
replyTo = &replyToUri
400
}
401
402
comment := models.IssueComment{
403
-
Did: user.Did,
404
-
Rkey: tid.TID(),
405
-
IssueAt: issue.AtUri().String(),
406
-
ReplyTo: replyTo,
407
-
Body: body,
408
-
Created: time.Now(),
409
}
410
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
411
l.Error("failed to validate comment", "err", err)
···
442
}
443
}()
444
445
-
commentId, err := db.AddIssueComment(rp.db, comment)
446
if err != nil {
447
l.Error("failed to create comment", "err", err)
448
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
449
return
450
}
451
···
455
// notify about the new comment
456
comment.Id = commentId
457
458
-
rawMentions := markup.FindUserMentions(comment.Body)
459
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
460
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
461
-
var mentions []syntax.DID
462
-
for _, ident := range idents {
463
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
464
-
mentions = append(mentions, ident.DID)
465
-
}
466
-
}
467
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
468
469
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId))
470
}
471
472
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
473
l := rp.logger.With("handler", "IssueComment")
474
user := rp.oauth.GetUser(r)
475
-
f, err := rp.repoResolver.Resolve(r)
476
-
if err != nil {
477
-
l.Error("failed to get repo and knot", "err", err)
478
-
return
479
-
}
480
481
issue, ok := r.Context().Value("issue").(*models.Issue)
482
if !ok {
···
488
commentId := chi.URLParam(r, "commentId")
489
comments, err := db.GetIssueComments(
490
rp.db,
491
-
db.FilterEq("id", commentId),
492
)
493
if err != nil {
494
l.Error("failed to fetch comment", "id", commentId)
···
504
505
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
506
LoggedInUser: user,
507
-
RepoInfo: f.RepoInfo(user),
508
Issue: issue,
509
Comment: &comment,
510
})
···
513
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
514
l := rp.logger.With("handler", "EditIssueComment")
515
user := rp.oauth.GetUser(r)
516
-
f, err := rp.repoResolver.Resolve(r)
517
-
if err != nil {
518
-
l.Error("failed to get repo and knot", "err", err)
519
-
return
520
-
}
521
522
issue, ok := r.Context().Value("issue").(*models.Issue)
523
if !ok {
···
529
commentId := chi.URLParam(r, "commentId")
530
comments, err := db.GetIssueComments(
531
rp.db,
532
-
db.FilterEq("id", commentId),
533
)
534
if err != nil {
535
l.Error("failed to fetch comment", "id", commentId)
···
553
case http.MethodGet:
554
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
555
LoggedInUser: user,
556
-
RepoInfo: f.RepoInfo(user),
557
Issue: issue,
558
Comment: &comment,
559
})
···
571
newComment := comment
572
newComment.Body = newBody
573
newComment.Edited = &now
574
record := newComment.AsRecord()
575
576
-
_, err = db.AddIssueComment(rp.db, newComment)
577
if err != nil {
578
l.Error("failed to perferom update-description query", "err", err)
579
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
580
return
581
}
582
583
// rkey is optional, it was introduced later
584
if newComment.Rkey != "" {
···
607
// return new comment body with htmx
608
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
609
LoggedInUser: user,
610
-
RepoInfo: f.RepoInfo(user),
611
Issue: issue,
612
Comment: &newComment,
613
})
···
617
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
618
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
619
user := rp.oauth.GetUser(r)
620
-
f, err := rp.repoResolver.Resolve(r)
621
-
if err != nil {
622
-
l.Error("failed to get repo and knot", "err", err)
623
-
return
624
-
}
625
626
issue, ok := r.Context().Value("issue").(*models.Issue)
627
if !ok {
···
633
commentId := chi.URLParam(r, "commentId")
634
comments, err := db.GetIssueComments(
635
rp.db,
636
-
db.FilterEq("id", commentId),
637
)
638
if err != nil {
639
l.Error("failed to fetch comment", "id", commentId)
···
649
650
rp.pages.ReplyIssueCommentPlaceholderFragment(w, pages.ReplyIssueCommentPlaceholderParams{
651
LoggedInUser: user,
652
-
RepoInfo: f.RepoInfo(user),
653
Issue: issue,
654
Comment: &comment,
655
})
···
658
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
659
l := rp.logger.With("handler", "ReplyIssueComment")
660
user := rp.oauth.GetUser(r)
661
-
f, err := rp.repoResolver.Resolve(r)
662
-
if err != nil {
663
-
l.Error("failed to get repo and knot", "err", err)
664
-
return
665
-
}
666
667
issue, ok := r.Context().Value("issue").(*models.Issue)
668
if !ok {
···
674
commentId := chi.URLParam(r, "commentId")
675
comments, err := db.GetIssueComments(
676
rp.db,
677
-
db.FilterEq("id", commentId),
678
)
679
if err != nil {
680
l.Error("failed to fetch comment", "id", commentId)
···
690
691
rp.pages.ReplyIssueCommentFragment(w, pages.ReplyIssueCommentParams{
692
LoggedInUser: user,
693
-
RepoInfo: f.RepoInfo(user),
694
Issue: issue,
695
Comment: &comment,
696
})
···
699
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
700
l := rp.logger.With("handler", "DeleteIssueComment")
701
user := rp.oauth.GetUser(r)
702
-
f, err := rp.repoResolver.Resolve(r)
703
-
if err != nil {
704
-
l.Error("failed to get repo and knot", "err", err)
705
-
return
706
-
}
707
708
issue, ok := r.Context().Value("issue").(*models.Issue)
709
if !ok {
···
715
commentId := chi.URLParam(r, "commentId")
716
comments, err := db.GetIssueComments(
717
rp.db,
718
-
db.FilterEq("id", commentId),
719
)
720
if err != nil {
721
l.Error("failed to fetch comment", "id", commentId)
···
742
743
// optimistic deletion
744
deleted := time.Now()
745
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
746
if err != nil {
747
l.Error("failed to delete comment", "err", err)
748
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
774
// htmx fragment of comment after deletion
775
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
776
LoggedInUser: user,
777
-
RepoInfo: f.RepoInfo(user),
778
Issue: issue,
779
Comment: &comment,
780
})
···
831
832
issues, err = db.GetIssues(
833
rp.db,
834
-
db.FilterIn("id", res.Hits),
835
)
836
if err != nil {
837
l.Error("failed to get issues", "err", err)
···
847
issues, err = db.GetIssuesPaginated(
848
rp.db,
849
page,
850
-
db.FilterEq("repo_at", f.RepoAt()),
851
-
db.FilterEq("open", openInt),
852
)
853
if err != nil {
854
l.Error("failed to get issues", "err", err)
···
859
860
labelDefs, err := db.GetLabelDefinitions(
861
rp.db,
862
-
db.FilterIn("at_uri", f.Repo.Labels),
863
-
db.FilterContains("scope", tangled.RepoIssueNSID),
864
)
865
if err != nil {
866
l.Error("failed to fetch labels", "err", err)
···
875
876
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
877
LoggedInUser: rp.oauth.GetUser(r),
878
-
RepoInfo: f.RepoInfo(user),
879
Issues: issues,
880
IssueCount: totalIssues,
881
LabelDefs: defs,
···
899
case http.MethodGet:
900
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
901
LoggedInUser: user,
902
-
RepoInfo: f.RepoInfo(user),
903
})
904
case http.MethodPost:
905
issue := &models.Issue{
906
-
RepoAt: f.RepoAt(),
907
-
Rkey: tid.TID(),
908
-
Title: r.FormValue("title"),
909
-
Body: r.FormValue("body"),
910
-
Open: true,
911
-
Did: user.Did,
912
-
Created: time.Now(),
913
-
Repo: &f.Repo,
914
}
915
916
if err := rp.validator.ValidateIssue(issue); err != nil {
···
978
// everything is successful, do not rollback the atproto record
979
atUri = ""
980
981
-
rawMentions := markup.FindUserMentions(issue.Body)
982
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
983
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
984
-
var mentions []syntax.DID
985
-
for _, ident := range idents {
986
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
987
-
mentions = append(mentions, ident.DID)
988
-
}
989
-
}
990
rp.notifier.NewIssue(r.Context(), issue, mentions)
991
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
992
return
993
}
994
}
···
7
"fmt"
8
"log/slog"
9
"net/http"
10
"time"
11
12
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
+
"tangled.org/core/appview/pages/repoinfo"
28
"tangled.org/core/appview/pagination"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
+
"tangled.org/core/orm"
33
+
"tangled.org/core/rbac"
34
"tangled.org/core/tid"
35
)
36
37
type Issues struct {
38
+
oauth *oauth.OAuth
39
+
repoResolver *reporesolver.RepoResolver
40
+
enforcer *rbac.Enforcer
41
+
pages *pages.Pages
42
+
idResolver *idresolver.Resolver
43
+
mentionsResolver *mentions.Resolver
44
+
db *db.DB
45
+
config *config.Config
46
+
notifier notify.Notifier
47
+
logger *slog.Logger
48
+
validator *validator.Validator
49
+
indexer *issues_indexer.Indexer
50
}
51
52
func New(
53
oauth *oauth.OAuth,
54
repoResolver *reporesolver.RepoResolver,
55
+
enforcer *rbac.Enforcer,
56
pages *pages.Pages,
57
idResolver *idresolver.Resolver,
58
+
mentionsResolver *mentions.Resolver,
59
db *db.DB,
60
config *config.Config,
61
notifier notify.Notifier,
···
64
logger *slog.Logger,
65
) *Issues {
66
return &Issues{
67
+
oauth: oauth,
68
+
repoResolver: repoResolver,
69
+
enforcer: enforcer,
70
+
pages: pages,
71
+
idResolver: idResolver,
72
+
mentionsResolver: mentionsResolver,
73
+
db: db,
74
+
config: config,
75
+
notifier: notifier,
76
+
logger: logger,
77
+
validator: validator,
78
+
indexer: indexer,
79
}
80
}
81
···
105
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
106
}
107
108
+
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
109
+
if err != nil {
110
+
l.Error("failed to fetch backlinks", "err", err)
111
+
rp.pages.Error503(w)
112
+
return
113
+
}
114
+
115
labelDefs, err := db.GetLabelDefinitions(
116
rp.db,
117
+
orm.FilterIn("at_uri", f.Labels),
118
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
119
)
120
if err != nil {
121
l.Error("failed to fetch labels", "err", err)
···
130
131
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
132
LoggedInUser: user,
133
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
134
Issue: issue,
135
CommentList: issue.CommentList(),
136
+
Backlinks: backlinks,
137
OrderedReactionKinds: models.OrderedReactionKinds,
138
Reactions: reactionMap,
139
UserReacted: userReactions,
···
144
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
145
l := rp.logger.With("handler", "EditIssue")
146
user := rp.oauth.GetUser(r)
147
148
issue, ok := r.Context().Value("issue").(*models.Issue)
149
if !ok {
···
156
case http.MethodGet:
157
rp.pages.EditIssueFragment(w, pages.EditIssueParams{
158
LoggedInUser: user,
159
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
160
Issue: issue,
161
})
162
case http.MethodPost:
···
164
newIssue := issue
165
newIssue.Title = r.FormValue("title")
166
newIssue.Body = r.FormValue("body")
167
+
newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body)
168
169
if err := rp.validator.ValidateIssue(newIssue); err != nil {
170
l.Error("validation error", "err", err)
···
234
l := rp.logger.With("handler", "DeleteIssue")
235
noticeId := "issue-actions-error"
236
237
f, err := rp.repoResolver.Resolve(r)
238
if err != nil {
239
l.Error("failed to get repo and knot", "err", err)
···
248
}
249
l = l.With("did", issue.Did, "rkey", issue.Rkey)
250
251
+
tx, err := rp.db.Begin()
252
+
if err != nil {
253
+
l.Error("failed to start transaction", "err", err)
254
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
255
+
return
256
+
}
257
+
defer tx.Rollback()
258
+
259
// delete from PDS
260
client, err := rp.oauth.AuthorizedClient(r)
261
if err != nil {
···
276
}
277
278
// delete from db
279
+
if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil {
280
l.Error("failed to delete issue", "err", err)
281
rp.pages.Notice(w, noticeId, "Failed to delete issue.")
282
return
283
}
284
+
tx.Commit()
285
286
rp.notifier.DeleteIssue(r.Context(), issue)
287
288
// return to all issues page
289
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
290
+
rp.pages.HxRedirect(w, "/"+ownerSlashRepo+"/issues")
291
}
292
293
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
···
306
return
307
}
308
309
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
310
+
isRepoOwner := roles.IsOwner()
311
+
isCollaborator := roles.IsCollaborator()
312
isIssueOwner := user.Did == issue.Did
313
314
// TODO: make this more granular
315
+
if isIssueOwner || isRepoOwner || isCollaborator {
316
err = db.CloseIssues(
317
rp.db,
318
+
orm.FilterEq("id", issue.Id),
319
)
320
if err != nil {
321
l.Error("failed to close issue", "err", err)
···
328
// notify about the issue closure
329
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
330
331
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
333
return
334
} else {
335
l.Error("user is not permitted to close issue")
···
354
return
355
}
356
357
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
358
+
isRepoOwner := roles.IsOwner()
359
+
isCollaborator := roles.IsCollaborator()
360
isIssueOwner := user.Did == issue.Did
361
362
+
if isCollaborator || isRepoOwner || isIssueOwner {
363
err := db.ReopenIssues(
364
rp.db,
365
+
orm.FilterEq("id", issue.Id),
366
)
367
if err != nil {
368
l.Error("failed to reopen issue", "err", err)
···
375
// notify about the issue reopen
376
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
377
378
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
380
return
381
} else {
382
l.Error("user is not the owner of the repo")
···
413
replyTo = &replyToUri
414
}
415
416
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
+
418
comment := models.IssueComment{
419
+
Did: user.Did,
420
+
Rkey: tid.TID(),
421
+
IssueAt: issue.AtUri().String(),
422
+
ReplyTo: replyTo,
423
+
Body: body,
424
+
Created: time.Now(),
425
+
Mentions: mentions,
426
+
References: references,
427
}
428
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
429
l.Error("failed to validate comment", "err", err)
···
460
}
461
}()
462
463
+
tx, err := rp.db.Begin()
464
+
if err != nil {
465
+
l.Error("failed to start transaction", "err", err)
466
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
467
+
return
468
+
}
469
+
defer tx.Rollback()
470
+
471
+
commentId, err := db.AddIssueComment(tx, comment)
472
if err != nil {
473
l.Error("failed to create comment", "err", err)
474
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
475
+
return
476
+
}
477
+
err = tx.Commit()
478
+
if err != nil {
479
+
l.Error("failed to commit transaction", "err", err)
480
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
481
return
482
}
483
···
487
// notify about the new comment
488
comment.Id = commentId
489
490
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
491
492
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
493
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
494
}
495
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
497
l := rp.logger.With("handler", "IssueComment")
498
user := rp.oauth.GetUser(r)
499
500
issue, ok := r.Context().Value("issue").(*models.Issue)
501
if !ok {
···
507
commentId := chi.URLParam(r, "commentId")
508
comments, err := db.GetIssueComments(
509
rp.db,
510
+
orm.FilterEq("id", commentId),
511
)
512
if err != nil {
513
l.Error("failed to fetch comment", "id", commentId)
···
523
524
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
525
LoggedInUser: user,
526
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
527
Issue: issue,
528
Comment: &comment,
529
})
···
532
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
533
l := rp.logger.With("handler", "EditIssueComment")
534
user := rp.oauth.GetUser(r)
535
536
issue, ok := r.Context().Value("issue").(*models.Issue)
537
if !ok {
···
543
commentId := chi.URLParam(r, "commentId")
544
comments, err := db.GetIssueComments(
545
rp.db,
546
+
orm.FilterEq("id", commentId),
547
)
548
if err != nil {
549
l.Error("failed to fetch comment", "id", commentId)
···
567
case http.MethodGet:
568
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
569
LoggedInUser: user,
570
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
571
Issue: issue,
572
Comment: &comment,
573
})
···
585
newComment := comment
586
newComment.Body = newBody
587
newComment.Edited = &now
588
+
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
589
+
590
record := newComment.AsRecord()
591
592
+
tx, err := rp.db.Begin()
593
+
if err != nil {
594
+
l.Error("failed to start transaction", "err", err)
595
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
596
+
return
597
+
}
598
+
defer tx.Rollback()
599
+
600
+
_, err = db.AddIssueComment(tx, newComment)
601
if err != nil {
602
l.Error("failed to perferom update-description query", "err", err)
603
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
604
return
605
}
606
+
tx.Commit()
607
608
// rkey is optional, it was introduced later
609
if newComment.Rkey != "" {
···
632
// return new comment body with htmx
633
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
634
LoggedInUser: user,
635
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
636
Issue: issue,
637
Comment: &newComment,
638
})
···
642
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
643
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
644
user := rp.oauth.GetUser(r)
645
646
issue, ok := r.Context().Value("issue").(*models.Issue)
647
if !ok {
···
653
commentId := chi.URLParam(r, "commentId")
654
comments, err := db.GetIssueComments(
655
rp.db,
656
+
orm.FilterEq("id", commentId),
657
)
658
if err != nil {
659
l.Error("failed to fetch comment", "id", commentId)
···
669
670
rp.pages.ReplyIssueCommentPlaceholderFragment(w, pages.ReplyIssueCommentPlaceholderParams{
671
LoggedInUser: user,
672
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
673
Issue: issue,
674
Comment: &comment,
675
})
···
678
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
679
l := rp.logger.With("handler", "ReplyIssueComment")
680
user := rp.oauth.GetUser(r)
681
682
issue, ok := r.Context().Value("issue").(*models.Issue)
683
if !ok {
···
689
commentId := chi.URLParam(r, "commentId")
690
comments, err := db.GetIssueComments(
691
rp.db,
692
+
orm.FilterEq("id", commentId),
693
)
694
if err != nil {
695
l.Error("failed to fetch comment", "id", commentId)
···
705
706
rp.pages.ReplyIssueCommentFragment(w, pages.ReplyIssueCommentParams{
707
LoggedInUser: user,
708
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
709
Issue: issue,
710
Comment: &comment,
711
})
···
714
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
715
l := rp.logger.With("handler", "DeleteIssueComment")
716
user := rp.oauth.GetUser(r)
717
718
issue, ok := r.Context().Value("issue").(*models.Issue)
719
if !ok {
···
725
commentId := chi.URLParam(r, "commentId")
726
comments, err := db.GetIssueComments(
727
rp.db,
728
+
orm.FilterEq("id", commentId),
729
)
730
if err != nil {
731
l.Error("failed to fetch comment", "id", commentId)
···
752
753
// optimistic deletion
754
deleted := time.Now()
755
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
756
if err != nil {
757
l.Error("failed to delete comment", "err", err)
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
784
// htmx fragment of comment after deletion
785
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
786
LoggedInUser: user,
787
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
788
Issue: issue,
789
Comment: &comment,
790
})
···
841
842
issues, err = db.GetIssues(
843
rp.db,
844
+
orm.FilterIn("id", res.Hits),
845
)
846
if err != nil {
847
l.Error("failed to get issues", "err", err)
···
857
issues, err = db.GetIssuesPaginated(
858
rp.db,
859
page,
860
+
orm.FilterEq("repo_at", f.RepoAt()),
861
+
orm.FilterEq("open", openInt),
862
)
863
if err != nil {
864
l.Error("failed to get issues", "err", err)
···
869
870
labelDefs, err := db.GetLabelDefinitions(
871
rp.db,
872
+
orm.FilterIn("at_uri", f.Labels),
873
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
874
)
875
if err != nil {
876
l.Error("failed to fetch labels", "err", err)
···
885
886
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
887
LoggedInUser: rp.oauth.GetUser(r),
888
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
889
Issues: issues,
890
IssueCount: totalIssues,
891
LabelDefs: defs,
···
909
case http.MethodGet:
910
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
911
LoggedInUser: user,
912
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
913
})
914
case http.MethodPost:
915
+
body := r.FormValue("body")
916
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
917
+
918
issue := &models.Issue{
919
+
RepoAt: f.RepoAt(),
920
+
Rkey: tid.TID(),
921
+
Title: r.FormValue("title"),
922
+
Body: body,
923
+
Open: true,
924
+
Did: user.Did,
925
+
Created: time.Now(),
926
+
Mentions: mentions,
927
+
References: references,
928
+
Repo: f,
929
}
930
931
if err := rp.validator.ValidateIssue(issue); err != nil {
···
993
// everything is successful, do not rollback the atproto record
994
atUri = ""
995
996
rp.notifier.NewIssue(r.Context(), issue, mentions)
997
+
998
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
999
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
1000
return
1001
}
1002
}
+3
-3
appview/issues/opengraph.go
+3
-3
appview/issues/opengraph.go
···
232
233
// Get owner handle for avatar
234
var ownerHandle string
235
-
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Repo.Did)
236
if err != nil {
237
-
ownerHandle = f.Repo.Did
238
} else {
239
ownerHandle = "@" + owner.Handle.String()
240
}
241
242
-
card, err := rp.drawIssueSummaryCard(issue, &f.Repo, commentCount, ownerHandle)
243
if err != nil {
244
log.Println("failed to draw issue summary card", err)
245
http.Error(w, "failed to draw issue summary card", http.StatusInternalServerError)
···
232
233
// Get owner handle for avatar
234
var ownerHandle string
235
+
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Did)
236
if err != nil {
237
+
ownerHandle = f.Did
238
} else {
239
ownerHandle = "@" + owner.Handle.String()
240
}
241
242
+
card, err := rp.drawIssueSummaryCard(issue, f, commentCount, ownerHandle)
243
if err != nil {
244
log.Println("failed to draw issue summary card", err)
245
http.Error(w, "failed to draw issue summary card", http.StatusInternalServerError)
+37
-19
appview/knots/knots.go
+37
-19
appview/knots/knots.go
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
39
Knotstream *eventconsumer.Consumer
40
}
41
42
func (k *Knots) Router() http.Handler {
43
r := chi.NewRouter()
44
···
59
user := k.OAuth.GetUser(r)
60
registrations, err := db.GetRegistrations(
61
k.Db,
62
-
db.FilterEq("did", user.Did),
63
)
64
if err != nil {
65
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
70
k.Pages.Knots(w, pages.KnotsParams{
71
LoggedInUser: user,
72
Registrations: registrations,
73
})
74
}
75
···
87
88
registrations, err := db.GetRegistrations(
89
k.Db,
90
-
db.FilterEq("did", user.Did),
91
-
db.FilterEq("domain", domain),
92
)
93
if err != nil {
94
l.Error("failed to get registrations", "err", err)
···
112
repos, err := db.GetRepos(
113
k.Db,
114
0,
115
-
db.FilterEq("knot", domain),
116
)
117
if err != nil {
118
l.Error("failed to get knot repos", "err", err)
···
132
Members: members,
133
Repos: repoMap,
134
IsOwner: true,
135
})
136
}
137
···
276
// get record from db first
277
registrations, err := db.GetRegistrations(
278
k.Db,
279
-
db.FilterEq("did", user.Did),
280
-
db.FilterEq("domain", domain),
281
)
282
if err != nil {
283
l.Error("failed to get registration", "err", err)
···
304
305
err = db.DeleteKnot(
306
tx,
307
-
db.FilterEq("did", user.Did),
308
-
db.FilterEq("domain", domain),
309
)
310
if err != nil {
311
l.Error("failed to delete registration", "err", err)
···
385
// get record from db first
386
registrations, err := db.GetRegistrations(
387
k.Db,
388
-
db.FilterEq("did", user.Did),
389
-
db.FilterEq("domain", domain),
390
)
391
if err != nil {
392
l.Error("failed to get registration", "err", err)
···
476
// Get updated registration to show
477
registrations, err = db.GetRegistrations(
478
k.Db,
479
-
db.FilterEq("did", user.Did),
480
-
db.FilterEq("domain", domain),
481
)
482
if err != nil {
483
l.Error("failed to get registration", "err", err)
···
512
513
registrations, err := db.GetRegistrations(
514
k.Db,
515
-
db.FilterEq("did", user.Did),
516
-
db.FilterEq("domain", domain),
517
-
db.FilterIsNot("registered", "null"),
518
)
519
if err != nil {
520
l.Error("failed to get registration", "err", err)
···
596
}
597
598
// success
599
-
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
600
}
601
602
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
···
620
621
registrations, err := db.GetRegistrations(
622
k.Db,
623
-
db.FilterEq("did", user.Did),
624
-
db.FilterEq("domain", domain),
625
-
db.FilterIsNot("registered", "null"),
626
)
627
if err != nil {
628
l.Error("failed to get registration", "err", err)
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
"tangled.org/core/tid"
27
···
40
Knotstream *eventconsumer.Consumer
41
}
42
43
+
type tab = map[string]any
44
+
45
+
var (
46
+
knotsTabs []tab = []tab{
47
+
{"Name": "profile", "Icon": "user"},
48
+
{"Name": "keys", "Icon": "key"},
49
+
{"Name": "emails", "Icon": "mail"},
50
+
{"Name": "notifications", "Icon": "bell"},
51
+
{"Name": "knots", "Icon": "volleyball"},
52
+
{"Name": "spindles", "Icon": "spool"},
53
+
}
54
+
)
55
+
56
func (k *Knots) Router() http.Handler {
57
r := chi.NewRouter()
58
···
73
user := k.OAuth.GetUser(r)
74
registrations, err := db.GetRegistrations(
75
k.Db,
76
+
orm.FilterEq("did", user.Did),
77
)
78
if err != nil {
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
84
k.Pages.Knots(w, pages.KnotsParams{
85
LoggedInUser: user,
86
Registrations: registrations,
87
+
Tabs: knotsTabs,
88
+
Tab: "knots",
89
})
90
}
91
···
103
104
registrations, err := db.GetRegistrations(
105
k.Db,
106
+
orm.FilterEq("did", user.Did),
107
+
orm.FilterEq("domain", domain),
108
)
109
if err != nil {
110
l.Error("failed to get registrations", "err", err)
···
128
repos, err := db.GetRepos(
129
k.Db,
130
0,
131
+
orm.FilterEq("knot", domain),
132
)
133
if err != nil {
134
l.Error("failed to get knot repos", "err", err)
···
148
Members: members,
149
Repos: repoMap,
150
IsOwner: true,
151
+
Tabs: knotsTabs,
152
+
Tab: "knots",
153
})
154
}
155
···
294
// get record from db first
295
registrations, err := db.GetRegistrations(
296
k.Db,
297
+
orm.FilterEq("did", user.Did),
298
+
orm.FilterEq("domain", domain),
299
)
300
if err != nil {
301
l.Error("failed to get registration", "err", err)
···
322
323
err = db.DeleteKnot(
324
tx,
325
+
orm.FilterEq("did", user.Did),
326
+
orm.FilterEq("domain", domain),
327
)
328
if err != nil {
329
l.Error("failed to delete registration", "err", err)
···
403
// get record from db first
404
registrations, err := db.GetRegistrations(
405
k.Db,
406
+
orm.FilterEq("did", user.Did),
407
+
orm.FilterEq("domain", domain),
408
)
409
if err != nil {
410
l.Error("failed to get registration", "err", err)
···
494
// Get updated registration to show
495
registrations, err = db.GetRegistrations(
496
k.Db,
497
+
orm.FilterEq("did", user.Did),
498
+
orm.FilterEq("domain", domain),
499
)
500
if err != nil {
501
l.Error("failed to get registration", "err", err)
···
530
531
registrations, err := db.GetRegistrations(
532
k.Db,
533
+
orm.FilterEq("did", user.Did),
534
+
orm.FilterEq("domain", domain),
535
+
orm.FilterIsNot("registered", "null"),
536
)
537
if err != nil {
538
l.Error("failed to get registration", "err", err)
···
614
}
615
616
// success
617
+
k.Pages.HxRedirect(w, fmt.Sprintf("/settings/knots/%s", domain))
618
}
619
620
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
···
638
639
registrations, err := db.GetRegistrations(
640
k.Db,
641
+
orm.FilterEq("did", user.Did),
642
+
orm.FilterEq("domain", domain),
643
+
orm.FilterIsNot("registered", "null"),
644
)
645
if err != nil {
646
l.Error("failed to get registration", "err", err)
+5
-4
appview/labels/labels.go
+5
-4
appview/labels/labels.go
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
"tangled.org/core/rbac"
20
"tangled.org/core/tid"
21
···
88
repoAt := r.Form.Get("repo")
89
subjectUri := r.Form.Get("subject")
90
91
-
repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt))
92
if err != nil {
93
fail("Failed to get repository.", err)
94
return
95
}
96
97
// find all the labels that this repo subscribes to
98
-
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
99
if err != nil {
100
fail("Failed to get labels for this repository.", err)
101
return
···
106
labelAts = append(labelAts, rl.LabelAt.String())
107
}
108
109
-
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
110
if err != nil {
111
fail("Invalid form data.", err)
112
return
113
}
114
115
// calculate the start state by applying already known labels
116
-
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
117
if err != nil {
118
fail("Invalid form data.", err)
119
return
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/tid"
22
···
89
repoAt := r.Form.Get("repo")
90
subjectUri := r.Form.Get("subject")
91
92
+
repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
93
if err != nil {
94
fail("Failed to get repository.", err)
95
return
96
}
97
98
// find all the labels that this repo subscribes to
99
+
repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
100
if err != nil {
101
fail("Failed to get labels for this repository.", err)
102
return
···
107
labelAts = append(labelAts, rl.LabelAt.String())
108
}
109
110
+
actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
111
if err != nil {
112
fail("Invalid form data.", err)
113
return
114
}
115
116
// calculate the start state by applying already known labels
117
+
existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
118
if err != nil {
119
fail("Invalid form data.", err)
120
return
+67
appview/mentions/resolver.go
+67
appview/mentions/resolver.go
···
···
1
+
package mentions
2
+
3
+
import (
4
+
"context"
5
+
"log/slog"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/appview/config"
9
+
"tangled.org/core/appview/db"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/appview/pages/markup"
12
+
"tangled.org/core/idresolver"
13
+
)
14
+
15
+
type Resolver struct {
16
+
config *config.Config
17
+
idResolver *idresolver.Resolver
18
+
execer db.Execer
19
+
logger *slog.Logger
20
+
}
21
+
22
+
func New(
23
+
config *config.Config,
24
+
idResolver *idresolver.Resolver,
25
+
execer db.Execer,
26
+
logger *slog.Logger,
27
+
) *Resolver {
28
+
return &Resolver{
29
+
config,
30
+
idResolver,
31
+
execer,
32
+
logger,
33
+
}
34
+
}
35
+
36
+
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
+
l := r.logger.With("method", "Resolve")
38
+
39
+
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
40
+
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
41
+
42
+
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
43
+
var mentions []syntax.DID
44
+
for _, ident := range idents {
45
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
46
+
mentions = append(mentions, ident.DID)
47
+
}
48
+
}
49
+
l.Debug("found mentions", "mentions", mentions)
50
+
51
+
var resolvedRefs []models.ReferenceLink
52
+
for _, rawRef := range rawRefs {
53
+
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
54
+
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
55
+
continue
56
+
}
57
+
rawRef.Handle = string(ident.DID)
58
+
resolvedRefs = append(resolvedRefs, rawRef)
59
+
}
60
+
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
61
+
if err != nil {
62
+
l.Error("failed running query", "err", err)
63
+
}
64
+
l.Debug("found references", "refs", aturiRefs)
65
+
66
+
return mentions, aturiRefs
67
+
}
+5
-4
appview/middleware/middleware.go
+5
-4
appview/middleware/middleware.go
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
"tangled.org/core/rbac"
22
)
23
···
164
ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
165
if err != nil || !ok {
166
// we need a logged in user
167
-
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
168
http.Error(w, "Forbiden", http.StatusUnauthorized)
169
return
170
}
···
217
218
repo, err := db.GetRepo(
219
mw.db,
220
-
db.FilterEq("did", id.DID.String()),
221
-
db.FilterEq("name", repoName),
222
)
223
if err != nil {
224
log.Println("failed to resolve repo", "err", err)
···
327
return
328
}
329
330
-
fullName := f.OwnerHandle() + "/" + f.Name
331
332
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
333
if r.URL.Query().Get("go-get") == "1" {
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
+
"tangled.org/core/orm"
22
"tangled.org/core/rbac"
23
)
24
···
165
ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166
if err != nil || !ok {
167
// we need a logged in user
168
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
169
http.Error(w, "Forbiden", http.StatusUnauthorized)
170
return
171
}
···
218
219
repo, err := db.GetRepo(
220
mw.db,
221
+
orm.FilterEq("did", id.DID.String()),
222
+
orm.FilterEq("name", repoName),
223
)
224
if err != nil {
225
log.Println("failed to resolve repo", "err", err)
···
328
return
329
}
330
331
+
fullName := reporesolver.GetBaseRepoPath(r, f)
332
333
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
334
if r.URL.Query().Get("go-get") == "1" {
+70
-34
appview/models/issue.go
+70
-34
appview/models/issue.go
···
10
)
11
12
type Issue struct {
13
-
Id int64
14
-
Did string
15
-
Rkey string
16
-
RepoAt syntax.ATURI
17
-
IssueId int
18
-
Created time.Time
19
-
Edited *time.Time
20
-
Deleted *time.Time
21
-
Title string
22
-
Body string
23
-
Open bool
24
25
// optionally, populate this when querying for reverse mappings
26
// like comment counts, parent repo etc.
···
34
}
35
36
func (i *Issue) AsRecord() tangled.RepoIssue {
37
return tangled.RepoIssue{
38
-
Repo: i.RepoAt.String(),
39
-
Title: i.Title,
40
-
Body: &i.Body,
41
-
CreatedAt: i.Created.Format(time.RFC3339),
42
}
43
}
44
···
161
}
162
163
type IssueComment struct {
164
-
Id int64
165
-
Did string
166
-
Rkey string
167
-
IssueAt string
168
-
ReplyTo *string
169
-
Body string
170
-
Created time.Time
171
-
Edited *time.Time
172
-
Deleted *time.Time
173
}
174
175
func (i *IssueComment) AtUri() syntax.ATURI {
···
177
}
178
179
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
180
return tangled.RepoIssueComment{
181
-
Body: i.Body,
182
-
Issue: i.IssueAt,
183
-
CreatedAt: i.Created.Format(time.RFC3339),
184
-
ReplyTo: i.ReplyTo,
185
}
186
}
187
···
205
return nil, err
206
}
207
208
comment := IssueComment{
209
-
Did: ownerDid,
210
-
Rkey: rkey,
211
-
Body: record.Body,
212
-
IssueAt: record.Issue,
213
-
ReplyTo: record.ReplyTo,
214
-
Created: created,
215
}
216
217
return &comment, nil
···
10
)
11
12
type Issue struct {
13
+
Id int64
14
+
Did string
15
+
Rkey string
16
+
RepoAt syntax.ATURI
17
+
IssueId int
18
+
Created time.Time
19
+
Edited *time.Time
20
+
Deleted *time.Time
21
+
Title string
22
+
Body string
23
+
Open bool
24
+
Mentions []syntax.DID
25
+
References []syntax.ATURI
26
27
// optionally, populate this when querying for reverse mappings
28
// like comment counts, parent repo etc.
···
36
}
37
38
func (i *Issue) AsRecord() tangled.RepoIssue {
39
+
mentions := make([]string, len(i.Mentions))
40
+
for i, did := range i.Mentions {
41
+
mentions[i] = string(did)
42
+
}
43
+
references := make([]string, len(i.References))
44
+
for i, uri := range i.References {
45
+
references[i] = string(uri)
46
+
}
47
return tangled.RepoIssue{
48
+
Repo: i.RepoAt.String(),
49
+
Title: i.Title,
50
+
Body: &i.Body,
51
+
Mentions: mentions,
52
+
References: references,
53
+
CreatedAt: i.Created.Format(time.RFC3339),
54
}
55
}
56
···
173
}
174
175
type IssueComment struct {
176
+
Id int64
177
+
Did string
178
+
Rkey string
179
+
IssueAt string
180
+
ReplyTo *string
181
+
Body string
182
+
Created time.Time
183
+
Edited *time.Time
184
+
Deleted *time.Time
185
+
Mentions []syntax.DID
186
+
References []syntax.ATURI
187
}
188
189
func (i *IssueComment) AtUri() syntax.ATURI {
···
191
}
192
193
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
194
+
mentions := make([]string, len(i.Mentions))
195
+
for i, did := range i.Mentions {
196
+
mentions[i] = string(did)
197
+
}
198
+
references := make([]string, len(i.References))
199
+
for i, uri := range i.References {
200
+
references[i] = string(uri)
201
+
}
202
return tangled.RepoIssueComment{
203
+
Body: i.Body,
204
+
Issue: i.IssueAt,
205
+
CreatedAt: i.Created.Format(time.RFC3339),
206
+
ReplyTo: i.ReplyTo,
207
+
Mentions: mentions,
208
+
References: references,
209
}
210
}
211
···
229
return nil, err
230
}
231
232
+
i := record
233
+
mentions := make([]syntax.DID, len(record.Mentions))
234
+
for i, did := range record.Mentions {
235
+
mentions[i] = syntax.DID(did)
236
+
}
237
+
references := make([]syntax.ATURI, len(record.References))
238
+
for i, uri := range i.References {
239
+
references[i] = syntax.ATURI(uri)
240
+
}
241
+
242
comment := IssueComment{
243
+
Did: ownerDid,
244
+
Rkey: rkey,
245
+
Body: record.Body,
246
+
IssueAt: record.Issue,
247
+
ReplyTo: record.ReplyTo,
248
+
Created: created,
249
+
Mentions: mentions,
250
+
References: references,
251
}
252
253
return &comment, nil
+3
-1
appview/models/profile.go
+3
-1
appview/models/profile.go
···
111
}
112
113
type ByMonth struct {
114
RepoEvents []RepoEvent
115
IssueEvents IssueEvents
116
PullEvents PullEvents
···
119
func (b ByMonth) IsEmpty() bool {
120
return len(b.RepoEvents) == 0 &&
121
len(b.IssueEvents.Items) == 0 &&
122
-
len(b.PullEvents.Items) == 0
123
}
124
125
type IssueEvents struct {
···
111
}
112
113
type ByMonth struct {
114
+
Commits int
115
RepoEvents []RepoEvent
116
IssueEvents IssueEvents
117
PullEvents PullEvents
···
120
func (b ByMonth) IsEmpty() bool {
121
return len(b.RepoEvents) == 0 &&
122
len(b.IssueEvents.Items) == 0 &&
123
+
len(b.PullEvents.Items) == 0 &&
124
+
b.Commits == 0
125
}
126
127
type IssueEvents struct {
+41
-3
appview/models/pull.go
+41
-3
appview/models/pull.go
···
66
TargetBranch string
67
State PullState
68
Submissions []*PullSubmission
69
70
// stacking
71
StackId string // nullable string
···
92
source.Repo = &s
93
}
94
}
95
96
record := tangled.RepoPull{
97
-
Title: p.Title,
98
-
Body: &p.Body,
99
-
CreatedAt: p.Created.Format(time.RFC3339),
100
Target: &tangled.RepoPull_Target{
101
Repo: p.RepoAt.String(),
102
Branch: p.TargetBranch,
···
146
147
// content
148
Body string
149
150
// meta
151
Created time.Time
152
}
153
154
func (p *Pull) LastRoundNumber() int {
155
return len(p.Submissions) - 1
···
66
TargetBranch string
67
State PullState
68
Submissions []*PullSubmission
69
+
Mentions []syntax.DID
70
+
References []syntax.ATURI
71
72
// stacking
73
StackId string // nullable string
···
94
source.Repo = &s
95
}
96
}
97
+
mentions := make([]string, len(p.Mentions))
98
+
for i, did := range p.Mentions {
99
+
mentions[i] = string(did)
100
+
}
101
+
references := make([]string, len(p.References))
102
+
for i, uri := range p.References {
103
+
references[i] = string(uri)
104
+
}
105
106
record := tangled.RepoPull{
107
+
Title: p.Title,
108
+
Body: &p.Body,
109
+
Mentions: mentions,
110
+
References: references,
111
+
CreatedAt: p.Created.Format(time.RFC3339),
112
Target: &tangled.RepoPull_Target{
113
Repo: p.RepoAt.String(),
114
Branch: p.TargetBranch,
···
158
159
// content
160
Body string
161
+
162
+
// meta
163
+
Mentions []syntax.DID
164
+
References []syntax.ATURI
165
166
// meta
167
Created time.Time
168
}
169
+
170
+
func (p *PullComment) AtUri() syntax.ATURI {
171
+
return syntax.ATURI(p.CommentAt)
172
+
}
173
+
174
+
// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175
+
// mentions := make([]string, len(p.Mentions))
176
+
// for i, did := range p.Mentions {
177
+
// mentions[i] = string(did)
178
+
// }
179
+
// references := make([]string, len(p.References))
180
+
// for i, uri := range p.References {
181
+
// references[i] = string(uri)
182
+
// }
183
+
// return tangled.RepoPullComment{
184
+
// Pull: p.PullAt,
185
+
// Body: p.Body,
186
+
// Mentions: mentions,
187
+
// References: references,
188
+
// CreatedAt: p.Created.Format(time.RFC3339),
189
+
// }
190
+
// }
191
192
func (p *Pull) LastRoundNumber() int {
193
return len(p.Submissions) - 1
+49
appview/models/reference.go
+49
appview/models/reference.go
···
···
1
+
package models
2
+
3
+
import "fmt"
4
+
5
+
type RefKind int
6
+
7
+
const (
8
+
RefKindIssue RefKind = iota
9
+
RefKindPull
10
+
)
11
+
12
+
func (k RefKind) String() string {
13
+
if k == RefKindIssue {
14
+
return "issues"
15
+
} else {
16
+
return "pulls"
17
+
}
18
+
}
19
+
20
+
// /@alice.com/cool-proj/issues/123
21
+
// /@alice.com/cool-proj/issues/123#comment-321
22
+
type ReferenceLink struct {
23
+
Handle string
24
+
Repo string
25
+
Kind RefKind
26
+
SubjectId int
27
+
CommentId *int
28
+
}
29
+
30
+
func (l ReferenceLink) String() string {
31
+
comment := ""
32
+
if l.CommentId != nil {
33
+
comment = fmt.Sprintf("#comment-%d", *l.CommentId)
34
+
}
35
+
return fmt.Sprintf("/%s/%s/%s/%d%s",
36
+
l.Handle,
37
+
l.Repo,
38
+
l.Kind.String(),
39
+
l.SubjectId,
40
+
comment,
41
+
)
42
+
}
43
+
44
+
type RichReferenceLink struct {
45
+
ReferenceLink
46
+
Title string
47
+
// reusing PullState for both issue & PR
48
+
State PullState
49
+
}
+5
-4
appview/notifications/notifications.go
+5
-4
appview/notifications/notifications.go
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
)
15
16
type Notifications struct {
···
53
54
total, err := db.CountNotifications(
55
n.db,
56
-
db.FilterEq("recipient_did", user.Did),
57
)
58
if err != nil {
59
l.Error("failed to get total notifications", "err", err)
···
64
notifications, err := db.GetNotificationsWithEntities(
65
n.db,
66
page,
67
-
db.FilterEq("recipient_did", user.Did),
68
)
69
if err != nil {
70
l.Error("failed to get notifications", "err", err)
···
96
97
count, err := db.CountNotifications(
98
n.db,
99
-
db.FilterEq("recipient_did", user.Did),
100
-
db.FilterEq("read", 0),
101
)
102
if err != nil {
103
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
type Notifications struct {
···
54
55
total, err := db.CountNotifications(
56
n.db,
57
+
orm.FilterEq("recipient_did", user.Did),
58
)
59
if err != nil {
60
l.Error("failed to get total notifications", "err", err)
···
65
notifications, err := db.GetNotificationsWithEntities(
66
n.db,
67
page,
68
+
orm.FilterEq("recipient_did", user.Did),
69
)
70
if err != nil {
71
l.Error("failed to get notifications", "err", err)
···
97
98
count, err := db.CountNotifications(
99
n.db,
100
+
orm.FilterEq("recipient_did", user.Did),
101
+
orm.FilterEq("read", 0),
102
)
103
if err != nil {
104
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77
-66
appview/notify/db/db.go
+77
-66
appview/notify/db/db.go
···
3
import (
4
"context"
5
"log"
6
-
"maps"
7
"slices"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/notify"
14
"tangled.org/core/idresolver"
15
)
16
17
const (
18
-
maxMentions = 5
19
)
20
21
type databaseNotifier struct {
···
42
return
43
}
44
var err error
45
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
46
if err != nil {
47
log.Printf("NewStar: failed to get repos: %v", err)
48
return
49
}
50
51
actorDid := syntax.DID(star.Did)
52
-
recipients := []syntax.DID{syntax.DID(repo.Did)}
53
eventType := models.NotificationTypeRepoStarred
54
entityType := "repo"
55
entityId := star.RepoAt.String()
···
74
}
75
76
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
77
-
78
-
// build the recipients list
79
-
// - owner of the repo
80
-
// - collaborators in the repo
81
-
var recipients []syntax.DID
82
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
83
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
84
if err != nil {
85
log.Printf("failed to fetch collaborators: %v", err)
86
return
87
}
88
for _, c := range collaborators {
89
-
recipients = append(recipients, c.SubjectDid)
90
}
91
92
actorDid := syntax.DID(issue.Did)
···
108
)
109
n.notifyEvent(
110
actorDid,
111
-
mentions,
112
models.NotificationTypeUserMentioned,
113
entityType,
114
entityId,
···
119
}
120
121
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
122
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
123
if err != nil {
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
return
···
130
}
131
issue := issues[0]
132
133
-
var recipients []syntax.DID
134
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
135
136
if comment.IsReply() {
137
// if this comment is a reply, then notify everybody in that thread
138
parentAtUri := *comment.ReplyTo
139
-
allThreads := issue.CommentList()
140
141
// find the parent thread, and add all DIDs from here to the recipient list
142
-
for _, t := range allThreads {
143
if t.Self.AtUri().String() == parentAtUri {
144
-
recipients = append(recipients, t.Participants()...)
145
}
146
}
147
} else {
148
// not a reply, notify just the issue author
149
-
recipients = append(recipients, syntax.DID(issue.Did))
150
}
151
152
actorDid := syntax.DID(comment.Did)
···
168
)
169
n.notifyEvent(
170
actorDid,
171
-
mentions,
172
models.NotificationTypeUserMentioned,
173
entityType,
174
entityId,
···
184
185
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
186
actorDid := syntax.DID(follow.UserDid)
187
-
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
188
eventType := models.NotificationTypeFollowed
189
entityType := "follow"
190
entityId := follow.UserDid
···
207
}
208
209
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
210
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
211
if err != nil {
212
log.Printf("NewPull: failed to get repos: %v", err)
213
return
214
}
215
-
216
-
// build the recipients list
217
-
// - owner of the repo
218
-
// - collaborators in the repo
219
-
var recipients []syntax.DID
220
-
recipients = append(recipients, syntax.DID(repo.Did))
221
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
222
if err != nil {
223
log.Printf("failed to fetch collaborators: %v", err)
224
return
225
}
226
for _, c := range collaborators {
227
-
recipients = append(recipients, c.SubjectDid)
228
}
229
230
actorDid := syntax.DID(pull.OwnerDid)
···
258
return
259
}
260
261
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
262
if err != nil {
263
log.Printf("NewPullComment: failed to get repos: %v", err)
264
return
···
267
// build up the recipients list:
268
// - repo owner
269
// - all pull participants
270
-
var recipients []syntax.DID
271
-
recipients = append(recipients, syntax.DID(repo.Did))
272
for _, p := range pull.Participants() {
273
-
recipients = append(recipients, syntax.DID(p))
274
}
275
276
actorDid := syntax.DID(comment.OwnerDid)
···
294
)
295
n.notifyEvent(
296
actorDid,
297
-
mentions,
298
models.NotificationTypeUserMentioned,
299
entityType,
300
entityId,
···
321
}
322
323
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
324
-
// build up the recipients list:
325
-
// - repo owner
326
-
// - repo collaborators
327
-
// - all issue participants
328
-
var recipients []syntax.DID
329
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
330
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
331
if err != nil {
332
log.Printf("failed to fetch collaborators: %v", err)
333
return
334
}
335
for _, c := range collaborators {
336
-
recipients = append(recipients, c.SubjectDid)
337
}
338
for _, p := range issue.Participants() {
339
-
recipients = append(recipients, syntax.DID(p))
340
}
341
342
entityType := "pull"
···
366
367
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
368
// Get repo details
369
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
370
if err != nil {
371
log.Printf("NewPullState: failed to get repos: %v", err)
372
return
373
}
374
375
-
// build up the recipients list:
376
-
// - repo owner
377
-
// - all pull participants
378
-
var recipients []syntax.DID
379
-
recipients = append(recipients, syntax.DID(repo.Did))
380
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
381
if err != nil {
382
log.Printf("failed to fetch collaborators: %v", err)
383
return
384
}
385
for _, c := range collaborators {
386
-
recipients = append(recipients, c.SubjectDid)
387
}
388
for _, p := range pull.Participants() {
389
-
recipients = append(recipients, syntax.DID(p))
390
}
391
392
entityType := "pull"
···
422
423
func (n *databaseNotifier) notifyEvent(
424
actorDid syntax.DID,
425
-
recipients []syntax.DID,
426
eventType models.NotificationType,
427
entityType string,
428
entityId string,
···
430
issueId *int64,
431
pullId *int64,
432
) {
433
-
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
434
-
recipients = recipients[:maxMentions]
435
}
436
-
recipientSet := make(map[syntax.DID]struct{})
437
-
for _, did := range recipients {
438
-
// everybody except actor themselves
439
-
if did != actorDid {
440
-
recipientSet[did] = struct{}{}
441
-
}
442
-
}
443
444
prefMap, err := db.GetNotificationPreferences(
445
n.db,
446
-
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
447
)
448
if err != nil {
449
// failed to get prefs for users
···
459
defer tx.Rollback()
460
461
// filter based on preferences
462
-
for recipientDid := range recipientSet {
463
prefs, ok := prefMap[recipientDid]
464
if !ok {
465
prefs = models.DefaultNotificationPreferences(recipientDid)
···
3
import (
4
"context"
5
"log"
6
"slices"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
11
"tangled.org/core/appview/models"
12
"tangled.org/core/appview/notify"
13
"tangled.org/core/idresolver"
14
+
"tangled.org/core/orm"
15
+
"tangled.org/core/sets"
16
)
17
18
const (
19
+
maxMentions = 8
20
)
21
22
type databaseNotifier struct {
···
43
return
44
}
45
var err error
46
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt)))
47
if err != nil {
48
log.Printf("NewStar: failed to get repos: %v", err)
49
return
50
}
51
52
actorDid := syntax.DID(star.Did)
53
+
recipients := sets.Singleton(syntax.DID(repo.Did))
54
eventType := models.NotificationTypeRepoStarred
55
entityType := "repo"
56
entityId := star.RepoAt.String()
···
75
}
76
77
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
78
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
79
if err != nil {
80
log.Printf("failed to fetch collaborators: %v", err)
81
return
82
}
83
+
84
+
// build the recipients list
85
+
// - owner of the repo
86
+
// - collaborators in the repo
87
+
// - remove users already mentioned
88
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
89
for _, c := range collaborators {
90
+
recipients.Insert(c.SubjectDid)
91
+
}
92
+
for _, m := range mentions {
93
+
recipients.Remove(m)
94
}
95
96
actorDid := syntax.DID(issue.Did)
···
112
)
113
n.notifyEvent(
114
actorDid,
115
+
sets.Collect(slices.Values(mentions)),
116
models.NotificationTypeUserMentioned,
117
entityType,
118
entityId,
···
123
}
124
125
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
126
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
127
if err != nil {
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
return
···
134
}
135
issue := issues[0]
136
137
+
// built the recipients list:
138
+
// - the owner of the repo
139
+
// - | if the comment is a reply -> everybody on that thread
140
+
// | if the comment is a top level -> just the issue owner
141
+
// - remove mentioned users from the recipients list
142
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
143
144
if comment.IsReply() {
145
// if this comment is a reply, then notify everybody in that thread
146
parentAtUri := *comment.ReplyTo
147
148
// find the parent thread, and add all DIDs from here to the recipient list
149
+
for _, t := range issue.CommentList() {
150
if t.Self.AtUri().String() == parentAtUri {
151
+
for _, p := range t.Participants() {
152
+
recipients.Insert(p)
153
+
}
154
}
155
}
156
} else {
157
// not a reply, notify just the issue author
158
+
recipients.Insert(syntax.DID(issue.Did))
159
+
}
160
+
161
+
for _, m := range mentions {
162
+
recipients.Remove(m)
163
}
164
165
actorDid := syntax.DID(comment.Did)
···
181
)
182
n.notifyEvent(
183
actorDid,
184
+
sets.Collect(slices.Values(mentions)),
185
models.NotificationTypeUserMentioned,
186
entityType,
187
entityId,
···
197
198
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
199
actorDid := syntax.DID(follow.UserDid)
200
+
recipients := sets.Singleton(syntax.DID(follow.SubjectDid))
201
eventType := models.NotificationTypeFollowed
202
entityType := "follow"
203
entityId := follow.UserDid
···
220
}
221
222
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
223
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
224
if err != nil {
225
log.Printf("NewPull: failed to get repos: %v", err)
226
return
227
}
228
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
229
if err != nil {
230
log.Printf("failed to fetch collaborators: %v", err)
231
return
232
}
233
+
234
+
// build the recipients list
235
+
// - owner of the repo
236
+
// - collaborators in the repo
237
+
recipients := sets.Singleton(syntax.DID(repo.Did))
238
for _, c := range collaborators {
239
+
recipients.Insert(c.SubjectDid)
240
}
241
242
actorDid := syntax.DID(pull.OwnerDid)
···
270
return
271
}
272
273
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
274
if err != nil {
275
log.Printf("NewPullComment: failed to get repos: %v", err)
276
return
···
279
// build up the recipients list:
280
// - repo owner
281
// - all pull participants
282
+
// - remove those already mentioned
283
+
recipients := sets.Singleton(syntax.DID(repo.Did))
284
for _, p := range pull.Participants() {
285
+
recipients.Insert(syntax.DID(p))
286
+
}
287
+
for _, m := range mentions {
288
+
recipients.Remove(m)
289
}
290
291
actorDid := syntax.DID(comment.OwnerDid)
···
309
)
310
n.notifyEvent(
311
actorDid,
312
+
sets.Collect(slices.Values(mentions)),
313
models.NotificationTypeUserMentioned,
314
entityType,
315
entityId,
···
336
}
337
338
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
339
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
340
if err != nil {
341
log.Printf("failed to fetch collaborators: %v", err)
342
return
343
}
344
+
345
+
// build up the recipients list:
346
+
// - repo owner
347
+
// - repo collaborators
348
+
// - all issue participants
349
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
350
for _, c := range collaborators {
351
+
recipients.Insert(c.SubjectDid)
352
}
353
for _, p := range issue.Participants() {
354
+
recipients.Insert(syntax.DID(p))
355
}
356
357
entityType := "pull"
···
381
382
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
383
// Get repo details
384
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
385
if err != nil {
386
log.Printf("NewPullState: failed to get repos: %v", err)
387
return
388
}
389
390
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
391
if err != nil {
392
log.Printf("failed to fetch collaborators: %v", err)
393
return
394
}
395
+
396
+
// build up the recipients list:
397
+
// - repo owner
398
+
// - all pull participants
399
+
recipients := sets.Singleton(syntax.DID(repo.Did))
400
for _, c := range collaborators {
401
+
recipients.Insert(c.SubjectDid)
402
}
403
for _, p := range pull.Participants() {
404
+
recipients.Insert(syntax.DID(p))
405
}
406
407
entityType := "pull"
···
437
438
func (n *databaseNotifier) notifyEvent(
439
actorDid syntax.DID,
440
+
recipients sets.Set[syntax.DID],
441
eventType models.NotificationType,
442
entityType string,
443
entityId string,
···
445
issueId *int64,
446
pullId *int64,
447
) {
448
+
// if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody
449
+
if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions {
450
+
return
451
}
452
+
453
+
recipients.Remove(actorDid)
454
455
prefMap, err := db.GetNotificationPreferences(
456
n.db,
457
+
orm.FilterIn("user_did", slices.Collect(recipients.All())),
458
)
459
if err != nil {
460
// failed to get prefs for users
···
470
defer tx.Rollback()
471
472
// filter based on preferences
473
+
for recipientDid := range recipients.All() {
474
prefs, ok := prefMap[recipientDid]
475
if !ok {
476
prefs = models.DefaultNotificationPreferences(recipientDid)
+3
-2
appview/oauth/handler.go
+3
-2
appview/oauth/handler.go
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
"tangled.org/core/tid"
20
)
21
···
97
// and create an sh.tangled.spindle.member record with that
98
spindleMembers, err := db.GetSpindleMembers(
99
o.Db,
100
-
db.FilterEq("instance", "spindle.tangled.sh"),
101
-
db.FilterEq("subject", did),
102
)
103
if err != nil {
104
l.Error("failed to get spindle members", "err", err)
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/tid"
21
)
22
···
98
// and create an sh.tangled.spindle.member record with that
99
spindleMembers, err := db.GetSpindleMembers(
100
o.Db,
101
+
orm.FilterEq("instance", "spindle.tangled.sh"),
102
+
orm.FilterEq("subject", did),
103
)
104
if err != nil {
105
l.Error("failed to get spindle members", "err", err)
+31
-11
appview/pages/funcmap.go
+31
-11
appview/pages/funcmap.go
···
22
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
23
"github.com/alecthomas/chroma/v2/lexers"
24
"github.com/alecthomas/chroma/v2/styles"
25
-
"github.com/bluesky-social/indigo/atproto/syntax"
26
"github.com/dustin/go-humanize"
27
"github.com/go-enry/go-enry/v2"
28
"github.com/yuin/goldmark"
29
"tangled.org/core/appview/filetree"
30
"tangled.org/core/appview/pages/markup"
31
"tangled.org/core/crypto"
32
)
···
71
}
72
73
return identity.Handle.String()
74
},
75
"truncateAt30": func(s string) string {
76
if len(s) <= 30 {
···
100
"sub": func(a, b int) int {
101
return a - b
102
},
103
-
"mul": func (a, b int) int {
104
return a * b
105
},
106
-
"div": func (a, b int) int {
107
return a / b
108
},
109
"mod": func(a, b int) int {
···
141
142
return b
143
},
144
-
"didOrHandle": func(did, handle string) string {
145
-
if handle != "" && handle != syntax.HandleInvalid.String() {
146
-
return handle
147
-
} else {
148
-
return did
149
-
}
150
-
},
151
"assoc": func(values ...string) ([][]string, error) {
152
if len(values)%2 != 0 {
153
return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
···
158
}
159
return pairs, nil
160
},
161
-
"append": func(s []string, values ...string) []string {
162
s = append(s, values...)
163
return s
164
},
···
379
}
380
}
381
382
func (p *Pages) AvatarUrl(handle, size string) string {
383
handle = strings.TrimPrefix(handle, "@")
384
385
secret := p.avatar.SharedSecret
386
h := hmac.New(sha256.New, []byte(secret))
···
22
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
23
"github.com/alecthomas/chroma/v2/lexers"
24
"github.com/alecthomas/chroma/v2/styles"
25
"github.com/dustin/go-humanize"
26
"github.com/go-enry/go-enry/v2"
27
"github.com/yuin/goldmark"
28
"tangled.org/core/appview/filetree"
29
+
"tangled.org/core/appview/models"
30
"tangled.org/core/appview/pages/markup"
31
"tangled.org/core/crypto"
32
)
···
71
}
72
73
return identity.Handle.String()
74
+
},
75
+
"ownerSlashRepo": func(repo *models.Repo) string {
76
+
ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did)
77
+
if err != nil {
78
+
return repo.DidSlashRepo()
79
+
}
80
+
handle := ownerId.Handle
81
+
if handle != "" && !handle.IsInvalidHandle() {
82
+
return string(handle) + "/" + repo.Name
83
+
}
84
+
return repo.DidSlashRepo()
85
},
86
"truncateAt30": func(s string) string {
87
if len(s) <= 30 {
···
111
"sub": func(a, b int) int {
112
return a - b
113
},
114
+
"mul": func(a, b int) int {
115
return a * b
116
},
117
+
"div": func(a, b int) int {
118
return a / b
119
},
120
"mod": func(a, b int) int {
···
152
153
return b
154
},
155
"assoc": func(values ...string) ([][]string, error) {
156
if len(values)%2 != 0 {
157
return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
···
162
}
163
return pairs, nil
164
},
165
+
"append": func(s []any, values ...any) []any {
166
s = append(s, values...)
167
return s
168
},
···
383
}
384
}
385
386
+
func (p *Pages) resolveDid(did string) string {
387
+
identity, err := p.resolver.ResolveIdent(context.Background(), did)
388
+
389
+
if err != nil {
390
+
return did
391
+
}
392
+
393
+
if identity.Handle.IsInvalidHandle() {
394
+
return "handle.invalid"
395
+
}
396
+
397
+
return identity.Handle.String()
398
+
}
399
+
400
func (p *Pages) AvatarUrl(handle, size string) string {
401
handle = strings.TrimPrefix(handle, "@")
402
+
403
+
handle = p.resolveDid(handle)
404
405
secret := p.avatar.SharedSecret
406
h := hmac.New(sha256.New, []byte(secret))
-26
appview/pages/markup/markdown.go
-26
appview/pages/markup/markdown.go
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
-
treeblood "github.com/wyatt915/goldmark-treeblood"
16
"github.com/yuin/goldmark"
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
"github.com/yuin/goldmark/ast"
···
65
extension.NewFootnote(
66
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
),
68
-
treeblood.MathML(),
69
callout.CalloutExtention,
70
textension.AtExt,
71
),
···
302
}
303
304
return path.Join(rctx.CurrentDir, dst)
305
-
}
306
-
307
-
// FindUserMentions returns Set of user handles from given markup soruce.
308
-
// It doesn't guarntee unique DIDs
309
-
func FindUserMentions(source string) []string {
310
-
var (
311
-
mentions []string
312
-
mentionsSet = make(map[string]struct{})
313
-
md = NewMarkdown()
314
-
sourceBytes = []byte(source)
315
-
root = md.Parser().Parse(text.NewReader(sourceBytes))
316
-
)
317
-
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
318
-
if entering && n.Kind() == textension.KindAt {
319
-
handle := n.(*textension.AtNode).Handle
320
-
mentionsSet[handle] = struct{}{}
321
-
return ast.WalkSkipChildren, nil
322
-
}
323
-
return ast.WalkContinue, nil
324
-
})
325
-
for handle := range mentionsSet {
326
-
mentions = append(mentions, handle)
327
-
}
328
-
return mentions
329
}
330
331
func isAbsoluteUrl(link string) bool {
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
"github.com/yuin/goldmark"
16
highlighting "github.com/yuin/goldmark-highlighting/v2"
17
"github.com/yuin/goldmark/ast"
···
64
extension.NewFootnote(
65
extension.WithFootnoteIDPrefix([]byte("footnote")),
66
),
67
callout.CalloutExtention,
68
textension.AtExt,
69
),
···
300
}
301
302
return path.Join(rctx.CurrentDir, dst)
303
}
304
305
func isAbsoluteUrl(link string) bool {
+124
appview/pages/markup/reference_link.go
+124
appview/pages/markup/reference_link.go
···
···
1
+
package markup
2
+
3
+
import (
4
+
"maps"
5
+
"net/url"
6
+
"path"
7
+
"slices"
8
+
"strconv"
9
+
"strings"
10
+
11
+
"github.com/yuin/goldmark/ast"
12
+
"github.com/yuin/goldmark/text"
13
+
"tangled.org/core/appview/models"
14
+
textension "tangled.org/core/appview/pages/markup/extension"
15
+
)
16
+
17
+
// FindReferences collects all links referencing tangled-related objects
18
+
// like issues, PRs, comments or even @-mentions
19
+
// This funciton doesn't actually check for the existence of records in the DB
20
+
// or the PDS; it merely returns a list of what are presumed to be references.
21
+
func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) {
22
+
var (
23
+
refLinkSet = make(map[models.ReferenceLink]struct{})
24
+
mentionsSet = make(map[string]struct{})
25
+
md = NewMarkdown()
26
+
sourceBytes = []byte(source)
27
+
root = md.Parser().Parse(text.NewReader(sourceBytes))
28
+
)
29
+
// trim url scheme. the SSL shouldn't matter
30
+
baseUrl = strings.TrimPrefix(baseUrl, "https://")
31
+
baseUrl = strings.TrimPrefix(baseUrl, "http://")
32
+
33
+
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
34
+
if !entering {
35
+
return ast.WalkContinue, nil
36
+
}
37
+
switch n.Kind() {
38
+
case textension.KindAt:
39
+
handle := n.(*textension.AtNode).Handle
40
+
mentionsSet[handle] = struct{}{}
41
+
return ast.WalkSkipChildren, nil
42
+
case ast.KindLink:
43
+
dest := string(n.(*ast.Link).Destination)
44
+
ref := parseTangledLink(baseUrl, dest)
45
+
if ref != nil {
46
+
refLinkSet[*ref] = struct{}{}
47
+
}
48
+
return ast.WalkSkipChildren, nil
49
+
case ast.KindAutoLink:
50
+
an := n.(*ast.AutoLink)
51
+
if an.AutoLinkType == ast.AutoLinkURL {
52
+
dest := string(an.URL(sourceBytes))
53
+
ref := parseTangledLink(baseUrl, dest)
54
+
if ref != nil {
55
+
refLinkSet[*ref] = struct{}{}
56
+
}
57
+
}
58
+
return ast.WalkSkipChildren, nil
59
+
}
60
+
return ast.WalkContinue, nil
61
+
})
62
+
mentions := slices.Collect(maps.Keys(mentionsSet))
63
+
references := slices.Collect(maps.Keys(refLinkSet))
64
+
return mentions, references
65
+
}
66
+
67
+
func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
68
+
u, err := url.Parse(urlStr)
69
+
if err != nil {
70
+
return nil
71
+
}
72
+
73
+
if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
74
+
return nil
75
+
}
76
+
77
+
p := path.Clean(u.Path)
78
+
parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
79
+
if len(parts) < 4 {
80
+
// need at least: handle / repo / kind / id
81
+
return nil
82
+
}
83
+
84
+
var (
85
+
handle = parts[0]
86
+
repo = parts[1]
87
+
kindSeg = parts[2]
88
+
subjectSeg = parts[3]
89
+
)
90
+
91
+
handle = strings.TrimPrefix(handle, "@")
92
+
93
+
var kind models.RefKind
94
+
switch kindSeg {
95
+
case "issues":
96
+
kind = models.RefKindIssue
97
+
case "pulls":
98
+
kind = models.RefKindPull
99
+
default:
100
+
return nil
101
+
}
102
+
103
+
subjectId, err := strconv.Atoi(subjectSeg)
104
+
if err != nil {
105
+
return nil
106
+
}
107
+
var commentId *int
108
+
if u.Fragment != "" {
109
+
if strings.HasPrefix(u.Fragment, "comment-") {
110
+
commentIdStr := u.Fragment[len("comment-"):]
111
+
if id, err := strconv.Atoi(commentIdStr); err == nil {
112
+
commentId = &id
113
+
}
114
+
}
115
+
}
116
+
117
+
return &models.ReferenceLink{
118
+
Handle: handle,
119
+
Repo: repo,
120
+
Kind: kind,
121
+
SubjectId: subjectId,
122
+
CommentId: commentId,
123
+
}
124
+
}
+42
appview/pages/markup/reference_link_test.go
+42
appview/pages/markup/reference_link_test.go
···
···
1
+
package markup_test
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"github.com/stretchr/testify/assert"
7
+
"tangled.org/core/appview/models"
8
+
"tangled.org/core/appview/pages/markup"
9
+
)
10
+
11
+
func TestMarkupParsing(t *testing.T) {
12
+
tests := []struct {
13
+
name string
14
+
source string
15
+
wantHandles []string
16
+
wantRefLinks []models.ReferenceLink
17
+
}{
18
+
{
19
+
name: "normal link",
20
+
source: `[link](http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1)`,
21
+
wantHandles: make([]string, 0),
22
+
wantRefLinks: []models.ReferenceLink{
23
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
24
+
},
25
+
},
26
+
{
27
+
name: "commonmark style autolink",
28
+
source: `<http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1>`,
29
+
wantHandles: make([]string, 0),
30
+
wantRefLinks: []models.ReferenceLink{
31
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
32
+
},
33
+
},
34
+
}
35
+
for _, tt := range tests {
36
+
t.Run(tt.name, func(t *testing.T) {
37
+
handles, refLinks := markup.FindReferences("http://127.0.0.1:3000", tt.source)
38
+
assert.ElementsMatch(t, tt.wantHandles, handles)
39
+
assert.ElementsMatch(t, tt.wantRefLinks, refLinks)
40
+
})
41
+
}
42
+
}
+21
-13
appview/pages/pages.go
+21
-13
appview/pages/pages.go
···
31
"github.com/bluesky-social/indigo/atproto/identity"
32
"github.com/bluesky-social/indigo/atproto/syntax"
33
"github.com/go-git/go-git/v5/plumbing"
34
-
"github.com/go-git/go-git/v5/plumbing/object"
35
)
36
37
//go:embed templates/* static legal
···
407
type KnotsParams struct {
408
LoggedInUser *oauth.User
409
Registrations []models.Registration
410
}
411
412
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
419
Members []string
420
Repos map[string][]models.Repo
421
IsOwner bool
422
}
423
424
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
436
type SpindlesParams struct {
437
LoggedInUser *oauth.User
438
Spindles []models.Spindle
439
}
440
441
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
444
445
type SpindleListingParams struct {
446
models.Spindle
447
}
448
449
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
455
Spindle models.Spindle
456
Members []string
457
Repos map[string][]models.Repo
458
}
459
460
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
···
482
483
type ProfileCard struct {
484
UserDid string
485
-
UserHandle string
486
FollowStatus models.FollowStatus
487
Punchcard *models.Punchcard
488
Profile *models.Profile
···
640
RepoInfo repoinfo.RepoInfo
641
Active string
642
TagMap map[string][]string
643
-
CommitsTrunc []*object.Commit
644
TagsTrunc []*types.TagReference
645
BranchesTrunc []types.Branch
646
// ForkInfo *types.ForkInfo
···
831
}
832
833
type Collaborator struct {
834
-
Did string
835
-
Handle string
836
-
Role string
837
}
838
839
type RepoSettingsParams struct {
···
908
RepoInfo repoinfo.RepoInfo
909
Active string
910
Issues []models.Issue
911
-
IssueCount int
912
LabelDefs map[string]*models.LabelDefinition
913
Page pagination.Page
914
FilteringByOpen bool
···
926
Active string
927
Issue *models.Issue
928
CommentList []models.CommentListItem
929
LabelDefs map[string]*models.LabelDefinition
930
931
OrderedReactionKinds []models.ReactionKind
···
1079
Pull *models.Pull
1080
Stack models.Stack
1081
AbandonedPulls []*models.Pull
1082
BranchDeleteStatus *models.BranchDeleteStatus
1083
MergeCheck types.MergeCheckResponse
1084
ResubmitCheck ResubmitResult
···
1250
return p.executePlain("repo/fragments/compareAllowPull", w, params)
1251
}
1252
1253
-
type RepoCompareDiffParams struct {
1254
-
LoggedInUser *oauth.User
1255
-
RepoInfo repoinfo.RepoInfo
1256
-
Diff types.NiceDiff
1257
}
1258
1259
-
func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
1260
-
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
1261
}
1262
1263
type LabelPanelParams struct {
···
31
"github.com/bluesky-social/indigo/atproto/identity"
32
"github.com/bluesky-social/indigo/atproto/syntax"
33
"github.com/go-git/go-git/v5/plumbing"
34
)
35
36
//go:embed templates/* static legal
···
406
type KnotsParams struct {
407
LoggedInUser *oauth.User
408
Registrations []models.Registration
409
+
Tabs []map[string]any
410
+
Tab string
411
}
412
413
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
420
Members []string
421
Repos map[string][]models.Repo
422
IsOwner bool
423
+
Tabs []map[string]any
424
+
Tab string
425
}
426
427
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
439
type SpindlesParams struct {
440
LoggedInUser *oauth.User
441
Spindles []models.Spindle
442
+
Tabs []map[string]any
443
+
Tab string
444
}
445
446
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
449
450
type SpindleListingParams struct {
451
models.Spindle
452
+
Tabs []map[string]any
453
+
Tab string
454
}
455
456
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
462
Spindle models.Spindle
463
Members []string
464
Repos map[string][]models.Repo
465
+
Tabs []map[string]any
466
+
Tab string
467
}
468
469
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
···
491
492
type ProfileCard struct {
493
UserDid string
494
FollowStatus models.FollowStatus
495
Punchcard *models.Punchcard
496
Profile *models.Profile
···
648
RepoInfo repoinfo.RepoInfo
649
Active string
650
TagMap map[string][]string
651
+
CommitsTrunc []types.Commit
652
TagsTrunc []*types.TagReference
653
BranchesTrunc []types.Branch
654
// ForkInfo *types.ForkInfo
···
839
}
840
841
type Collaborator struct {
842
+
Did string
843
+
Role string
844
}
845
846
type RepoSettingsParams struct {
···
915
RepoInfo repoinfo.RepoInfo
916
Active string
917
Issues []models.Issue
918
+
IssueCount int
919
LabelDefs map[string]*models.LabelDefinition
920
Page pagination.Page
921
FilteringByOpen bool
···
933
Active string
934
Issue *models.Issue
935
CommentList []models.CommentListItem
936
+
Backlinks []models.RichReferenceLink
937
LabelDefs map[string]*models.LabelDefinition
938
939
OrderedReactionKinds []models.ReactionKind
···
1087
Pull *models.Pull
1088
Stack models.Stack
1089
AbandonedPulls []*models.Pull
1090
+
Backlinks []models.RichReferenceLink
1091
BranchDeleteStatus *models.BranchDeleteStatus
1092
MergeCheck types.MergeCheckResponse
1093
ResubmitCheck ResubmitResult
···
1259
return p.executePlain("repo/fragments/compareAllowPull", w, params)
1260
}
1261
1262
+
type RepoCompareDiffFragmentParams struct {
1263
+
Diff types.NiceDiff
1264
+
DiffOpts types.DiffOpts
1265
}
1266
1267
+
func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1268
+
return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1269
}
1270
1271
type LabelPanelParams struct {
+25
-22
appview/pages/repoinfo/repoinfo.go
+25
-22
appview/pages/repoinfo/repoinfo.go
···
1
package repoinfo
2
3
import (
4
"path"
5
"slices"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
"tangled.org/core/appview/models"
9
"tangled.org/core/appview/state/userutil"
10
)
11
12
-
func (r RepoInfo) Owner() string {
13
if r.OwnerHandle != "" {
14
return r.OwnerHandle
15
} else {
···
18
}
19
20
func (r RepoInfo) FullName() string {
21
-
return path.Join(r.Owner(), r.Name)
22
}
23
24
-
func (r RepoInfo) OwnerWithoutAt() string {
25
if r.OwnerHandle != "" {
26
return r.OwnerHandle
27
} else {
···
30
}
31
32
func (r RepoInfo) FullNameWithoutAt() string {
33
-
return path.Join(r.OwnerWithoutAt(), r.Name)
34
}
35
36
func (r RepoInfo) GetTabs() [][]string {
···
48
return tabs
49
}
50
51
type RepoInfo struct {
52
-
Name string
53
-
Rkey string
54
-
OwnerDid string
55
-
OwnerHandle string
56
-
Description string
57
-
Website string
58
-
Topics []string
59
-
Knot string
60
-
Spindle string
61
-
RepoAt syntax.ATURI
62
-
IsStarred bool
63
-
Stats models.RepoStats
64
-
Roles RolesInRepo
65
-
Source *models.Repo
66
-
SourceHandle string
67
-
Ref string
68
-
DisableFork bool
69
-
CurrentDir string
70
}
71
72
// each tab on a repo could have some metadata:
···
1
package repoinfo
2
3
import (
4
+
"fmt"
5
"path"
6
"slices"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/models"
11
"tangled.org/core/appview/state/userutil"
12
)
13
14
+
func (r RepoInfo) owner() string {
15
if r.OwnerHandle != "" {
16
return r.OwnerHandle
17
} else {
···
20
}
21
22
func (r RepoInfo) FullName() string {
23
+
return path.Join(r.owner(), r.Name)
24
}
25
26
+
func (r RepoInfo) ownerWithoutAt() string {
27
if r.OwnerHandle != "" {
28
return r.OwnerHandle
29
} else {
···
32
}
33
34
func (r RepoInfo) FullNameWithoutAt() string {
35
+
return path.Join(r.ownerWithoutAt(), r.Name)
36
}
37
38
func (r RepoInfo) GetTabs() [][]string {
···
50
return tabs
51
}
52
53
+
func (r RepoInfo) RepoAt() syntax.ATURI {
54
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.OwnerDid, tangled.RepoNSID, r.Rkey))
55
+
}
56
+
57
type RepoInfo struct {
58
+
Name string
59
+
Rkey string
60
+
OwnerDid string
61
+
OwnerHandle string
62
+
Description string
63
+
Website string
64
+
Topics []string
65
+
Knot string
66
+
Spindle string
67
+
IsStarred bool
68
+
Stats models.RepoStats
69
+
Roles RolesInRepo
70
+
Source *models.Repo
71
+
Ref string
72
+
CurrentDir string
73
}
74
75
// each tab on a repo could have some metadata:
+8
appview/pages/templates/fragments/tabSelector.html
+8
appview/pages/templates/fragments/tabSelector.html
···
2
{{ $name := .Name }}
3
{{ $all := .Values }}
4
{{ $active := .Active }}
5
<div class="flex justify-between divide-x divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
6
{{ $activeTab := "bg-white dark:bg-gray-700 shadow-sm" }}
7
{{ $inactiveTab := "bg-gray-100 dark:bg-gray-800 shadow-inner" }}
8
{{ range $index, $value := $all }}
9
{{ $isActive := eq $value.Key $active }}
10
<a href="?{{ $name }}={{ $value.Key }}"
11
class="p-2 whitespace-nowrap flex justify-center items-center gap-2 text-sm w-full block hover:no-underline text-center {{ if $isActive }} {{$activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}">
12
{{ if $value.Icon }}
13
{{ i $value.Icon "size-4" }}
···
2
{{ $name := .Name }}
3
{{ $all := .Values }}
4
{{ $active := .Active }}
5
+
{{ $include := .Include }}
6
<div class="flex justify-between divide-x divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
7
{{ $activeTab := "bg-white dark:bg-gray-700 shadow-sm" }}
8
{{ $inactiveTab := "bg-gray-100 dark:bg-gray-800 shadow-inner" }}
9
{{ range $index, $value := $all }}
10
{{ $isActive := eq $value.Key $active }}
11
<a href="?{{ $name }}={{ $value.Key }}"
12
+
{{ if $include }}
13
+
hx-get="?{{ $name }}={{ $value.Key }}"
14
+
hx-include="{{ $include }}"
15
+
hx-push-url="true"
16
+
hx-target="body"
17
+
hx-on:htmx:config-request="if(!event.detail.parameters.q) delete event.detail.parameters.q"
18
+
{{ end }}
19
class="p-2 whitespace-nowrap flex justify-center items-center gap-2 text-sm w-full block hover:no-underline text-center {{ if $isActive }} {{$activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}">
20
{{ if $value.Icon }}
21
{{ i $value.Icon "size-4" }}
+22
appview/pages/templates/fragments/tinyAvatarList.html
+22
appview/pages/templates/fragments/tinyAvatarList.html
···
···
1
+
{{ define "fragments/tinyAvatarList" }}
2
+
{{ $all := .all }}
3
+
{{ $classes := .classes }}
4
+
{{ $ps := take $all 5 }}
5
+
<div class="inline-flex items-center -space-x-3">
6
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
7
+
{{ range $i, $p := $ps }}
8
+
<img
9
+
src="{{ tinyAvatar . }}"
10
+
alt=""
11
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
12
+
/>
13
+
{{ end }}
14
+
15
+
{{ if gt (len $all) 5 }}
16
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
17
+
+{{ sub (len $all) 5 }}
18
+
</span>
19
+
{{ end }}
20
+
</div>
21
+
{{ end }}
22
+
+23
-7
appview/pages/templates/knots/dashboard.html
+23
-7
appview/pages/templates/knots/dashboard.html
···
1
-
{{ define "title" }}{{ .Registration.Domain }} · knots{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
5
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</h1>
7
<div id="right-side" class="flex gap-2">
8
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
35
</div>
36
37
{{ if .Members }}
38
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
39
<div class="flex flex-col gap-2">
40
{{ block "member" . }} {{ end }}
41
</div>
···
79
<button
80
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
81
title="Delete knot"
82
-
hx-delete="/knots/{{ .Domain }}"
83
hx-swap="outerHTML"
84
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
85
hx-headers='{"shouldRedirect": "true"}'
···
95
<button
96
class="btn gap-2 group"
97
title="Retry knot verification"
98
-
hx-post="/knots/{{ .Domain }}/retry"
99
hx-swap="none"
100
hx-headers='{"shouldRefresh": "true"}'
101
>
···
113
<button
114
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
115
title="Remove member"
116
-
hx-post="/knots/{{ $root.Registration.Domain }}/remove"
117
hx-swap="none"
118
hx-vals='{"member": "{{$member}}" }'
119
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
···
1
+
{{ define "title" }}{{ .Registration.Domain }} · {{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotDash" }}
20
+
<div>
21
<div class="flex justify-between items-center">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Registration.Domain }}</h2>
23
<div id="right-side" class="flex gap-2">
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
51
</div>
52
53
{{ if .Members }}
54
+
<section class="bg-white dark:bg-gray-800 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
55
<div class="flex flex-col gap-2">
56
{{ block "member" . }} {{ end }}
57
</div>
···
95
<button
96
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
97
title="Delete knot"
98
+
hx-delete="/settings/knots/{{ .Domain }}"
99
hx-swap="outerHTML"
100
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
101
hx-headers='{"shouldRedirect": "true"}'
···
111
<button
112
class="btn gap-2 group"
113
title="Retry knot verification"
114
+
hx-post="/settings/knots/{{ .Domain }}/retry"
115
hx-swap="none"
116
hx-headers='{"shouldRefresh": "true"}'
117
>
···
129
<button
130
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
131
title="Remove member"
132
+
hx-post="/settings/knots/{{ $root.Registration.Domain }}/remove"
133
hx-swap="none"
134
hx-vals='{"member": "{{$member}}" }'
135
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
···
7
8
{{ define "knotLeftSide" }}
9
{{ if .Registered }}
10
-
<a href="/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Domain }}
···
56
<button
57
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
58
title="Delete knot"
59
-
hx-delete="/knots/{{ .Domain }}"
60
hx-swap="outerHTML"
61
hx-target="#knot-{{.Id}}"
62
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
72
<button
73
class="btn gap-2 group"
74
title="Retry knot verification"
75
-
hx-post="/knots/{{ .Domain }}/retry"
76
hx-swap="none"
77
hx-target="#knot-{{.Id}}"
78
>
···
7
8
{{ define "knotLeftSide" }}
9
{{ if .Registered }}
10
+
<a href="/settings/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Domain }}
···
56
<button
57
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
58
title="Delete knot"
59
+
hx-delete="/settings/knots/{{ .Domain }}"
60
hx-swap="outerHTML"
61
hx-target="#knot-{{.Id}}"
62
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
72
<button
73
class="btn gap-2 group"
74
title="Retry knot verification"
75
+
hx-post="/settings/knots/{{ .Domain }}/retry"
76
hx-swap="none"
77
hx-target="#knot-{{.Id}}"
78
>
+42
-11
appview/pages/templates/knots/index.html
+42
-11
appview/pages/templates/knots/index.html
···
1
-
{{ define "title" }}knots{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">docs</a>
9
-
</span>
10
-
</div>
11
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
13
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
{{ block "list" . }} {{ end }}
16
{{ block "register" . }} {{ end }}
17
</div>
···
50
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
51
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
52
<form
53
-
hx-post="/knots/register"
54
class="max-w-2xl mb-2 space-y-4"
55
hx-indicator="#register-button"
56
hx-swap="none"
···
84
85
</section>
86
{{ end }}
···
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotsList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotsList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Knots</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
28
+
</div>
29
30
+
<section>
31
<div class="flex flex-col gap-6">
32
{{ block "list" . }} {{ end }}
33
{{ block "register" . }} {{ end }}
34
</div>
···
67
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
68
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
69
<form
70
+
hx-post="/settings/knots/register"
71
class="max-w-2xl mb-2 space-y-4"
72
hx-indicator="#register-button"
73
hx-swap="none"
···
101
102
</section>
103
{{ end }}
104
+
105
+
{{ define "docsButton" }}
106
+
<a
107
+
class="btn flex items-center gap-2"
108
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
109
+
{{ i "book" "size-4" }}
110
+
docs
111
+
</a>
112
+
<div
113
+
id="add-email-modal"
114
+
popover
115
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
116
+
</div>
117
+
{{ end }}
-2
appview/pages/templates/layouts/fragments/topbar.html
-2
appview/pages/templates/layouts/fragments/topbar.html
+8
-7
appview/pages/templates/layouts/profilebase.html
+8
-7
appview/pages/templates/layouts/profilebase.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
3
{{ define "extrameta" }}
4
-
{{ $avatarUrl := fullAvatar .Card.UserHandle }}
5
-
<meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
6
<meta property="og:type" content="profile" />
7
-
<meta property="og:url" content="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}?tab={{ .Active }}" />
8
-
<meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
9
<meta property="og:image" content="{{ $avatarUrl }}" />
10
<meta property="og:image:width" content="512" />
11
<meta property="og:image:height" content="512" />
12
13
<meta name="twitter:card" content="summary" />
14
-
<meta name="twitter:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
15
-
<meta name="twitter:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
16
<meta name="twitter:image" content="{{ $avatarUrl }}" />
17
{{ end }}
18
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }}{{ end }}
2
3
{{ define "extrameta" }}
4
+
{{ $handle := resolve .Card.UserDid }}
5
+
{{ $avatarUrl := fullAvatar $handle }}
6
+
<meta property="og:title" content="{{ $handle }}" />
7
<meta property="og:type" content="profile" />
8
+
<meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
9
+
<meta property="og:description" content="{{ or .Card.Profile.Description $handle }}" />
10
<meta property="og:image" content="{{ $avatarUrl }}" />
11
<meta property="og:image:width" content="512" />
12
<meta property="og:image:height" content="512" />
13
14
<meta name="twitter:card" content="summary" />
15
+
<meta name="twitter:title" content="{{ $handle }}" />
16
+
<meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" />
17
<meta name="twitter:image" content="{{ $avatarUrl }}" />
18
{{ end }}
19
+35
-10
appview/pages/templates/repo/commit.html
+35
-10
appview/pages/templates/repo/commit.html
···
25
</div>
26
27
<div class="flex flex-wrap items-center space-x-2">
28
-
<p class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-300">
29
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
30
-
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
36
37
<span class="px-1 select-none before:content-['\00B7']"></span>
38
-
{{ template "repo/fragments/time" $commit.Author.When }}
39
<span class="px-1 select-none before:content-['\00B7']"></span>
40
41
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
79
</section>
80
{{end}}
81
82
{{ define "topbarLayout" }}
83
<header class="col-span-full" style="z-index: 20;">
84
{{ template "layouts/fragments/topbar" . }}
···
111
{{ end }}
112
113
{{ define "contentAfter" }}
114
-
{{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff .DiffOpts) }}
115
{{end}}
116
117
{{ define "contentAfterLeft" }}
···
25
</div>
26
27
<div class="flex flex-wrap items-center space-x-2">
28
+
<p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
29
+
{{ template "attribution" . }}
30
31
<span class="px-1 select-none before:content-['\00B7']"></span>
32
+
{{ template "repo/fragments/time" $commit.Committer.When }}
33
<span class="px-1 select-none before:content-['\00B7']"></span>
34
35
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
73
</section>
74
{{end}}
75
76
+
{{ define "attribution" }}
77
+
{{ $commit := .Diff.Commit }}
78
+
{{ $showCommitter := true }}
79
+
{{ if eq $commit.Author.Email $commit.Committer.Email }}
80
+
{{ $showCommitter = false }}
81
+
{{ end }}
82
+
83
+
{{ if $showCommitter }}
84
+
authored by {{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid) }}
85
+
{{ range $commit.CoAuthors }}
86
+
{{ template "attributedUser" (list .Email .Name $.EmailToDid) }}
87
+
{{ end }}
88
+
and committed by {{ template "attributedUser" (list $commit.Committer.Email $commit.Committer.Name $.EmailToDid) }}
89
+
{{ else }}
90
+
{{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid )}}
91
+
{{ end }}
92
+
{{ end }}
93
+
94
+
{{ define "attributedUser" }}
95
+
{{ $email := index . 0 }}
96
+
{{ $name := index . 1 }}
97
+
{{ $map := index . 2 }}
98
+
{{ $did := index $map $email }}
99
+
100
+
{{ if $did }}
101
+
{{ template "user/fragments/picHandleLink" $did }}
102
+
{{ else }}
103
+
<a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a>
104
+
{{ end }}
105
+
{{ end }}
106
+
107
{{ define "topbarLayout" }}
108
<header class="col-span-full" style="z-index: 20;">
109
{{ template "layouts/fragments/topbar" . }}
···
136
{{ end }}
137
138
{{ define "contentAfter" }}
139
+
{{ template "repo/fragments/diff" (list .Diff .DiffOpts) }}
140
{{end}}
141
142
{{ define "contentAfterLeft" }}
+1
-1
appview/pages/templates/repo/compare/compare.html
+1
-1
appview/pages/templates/repo/compare/compare.html
+1
-1
appview/pages/templates/repo/empty.html
+1
-1
appview/pages/templates/repo/empty.html
···
35
36
<p><span class="{{$bullet}}">1</span>First, generate a new <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" class="underline">SSH key pair</a>.</p>
37
<p><span class="{{$bullet}}">2</span>Then add the public key to your account from the <a href="/settings" class="underline">settings</a> page.</p>
38
-
<p><span class="{{$bullet}}">3</span>Configure your remote to <code>git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code></p>
39
<p><span class="{{$bullet}}">4</span>Push!</p>
40
</div>
41
</div>
···
35
36
<p><span class="{{$bullet}}">1</span>First, generate a new <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" class="underline">SSH key pair</a>.</p>
37
<p><span class="{{$bullet}}">2</span>Then add the public key to your account from the <a href="/settings" class="underline">settings</a> page.</p>
38
+
<p><span class="{{$bullet}}">3</span>Configure your remote to <code>git@{{ $knot | stripPort }}:{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code></p>
39
<p><span class="{{$bullet}}">4</span>Push!</p>
40
</div>
41
</div>
+1
-1
appview/pages/templates/repo/fork.html
+1
-1
appview/pages/templates/repo/fork.html
+49
appview/pages/templates/repo/fragments/backlinks.html
+49
appview/pages/templates/repo/fragments/backlinks.html
···
···
1
+
{{ define "repo/fragments/backlinks" }}
2
+
{{ if .Backlinks }}
3
+
<div id="at-uri-panel" class="px-2 md:px-0">
4
+
<div>
5
+
<span class="text-sm py-1 font-bold text-gray-500 dark:text-gray-400">Referenced by</span>
6
+
</div>
7
+
<ul>
8
+
{{ range .Backlinks }}
9
+
<li>
10
+
{{ $repoOwner := resolve .Handle }}
11
+
{{ $repoName := .Repo }}
12
+
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
13
+
<div class="flex flex-col">
14
+
<div class="flex gap-2 items-center">
15
+
{{ if .State.IsClosed }}
16
+
<span class="text-gray-500 dark:text-gray-400">
17
+
{{ i "ban" "w-4 h-4" }}
18
+
</span>
19
+
{{ else if eq .Kind.String "issues" }}
20
+
<span class="text-green-600 dark:text-green-500">
21
+
{{ i "circle-dot" "w-4 h-4" }}
22
+
</span>
23
+
{{ else if .State.IsOpen }}
24
+
<span class="text-green-600 dark:text-green-500">
25
+
{{ i "git-pull-request" "w-4 h-4" }}
26
+
</span>
27
+
{{ else if .State.IsMerged }}
28
+
<span class="text-purple-600 dark:text-purple-500">
29
+
{{ i "git-merge" "w-4 h-4" }}
30
+
</span>
31
+
{{ else }}
32
+
<span class="text-gray-600 dark:text-gray-300">
33
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
34
+
</span>
35
+
{{ end }}
36
+
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
+
</div>
38
+
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
+
<div>
40
+
<span>on <a href="/{{ $repoUrl }}">{{ $repoUrl }}</a></span>
41
+
</div>
42
+
{{ end }}
43
+
</div>
44
+
</li>
45
+
{{ end }}
46
+
</ul>
47
+
</div>
48
+
{{ end }}
49
+
{{ end }}
+3
-2
appview/pages/templates/repo/fragments/cloneDropdown.html
+3
-2
appview/pages/templates/repo/fragments/cloneDropdown.html
···
43
44
<!-- SSH Clone -->
45
<div class="mb-3">
46
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label>
47
<div class="flex items-center border border-gray-300 dark:border-gray-600 rounded">
48
<code
49
class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto"
50
onclick="window.getSelection().selectAllChildren(this)"
51
-
data-url="git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}"
52
-
>git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code>
53
<button
54
onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))"
55
class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600"
···
43
44
<!-- SSH Clone -->
45
<div class="mb-3">
46
+
{{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }}
47
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label>
48
<div class="flex items-center border border-gray-300 dark:border-gray-600 rounded">
49
<code
50
class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto"
51
onclick="window.getSelection().selectAllChildren(this)"
52
+
data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}"
53
+
>git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code>
54
<button
55
onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))"
56
class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600"
+2
-3
appview/pages/templates/repo/fragments/diff.html
+2
-3
appview/pages/templates/repo/fragments/diff.html
+15
-1
appview/pages/templates/repo/fragments/editLabelPanel.html
+15
-1
appview/pages/templates/repo/fragments/editLabelPanel.html
···
170
{{ $fieldName := $def.AtUri }}
171
{{ $valueType := $def.ValueType }}
172
{{ $value := .value }}
173
{{ if $valueType.IsDidFormat }}
174
{{ $value = trimPrefix (resolve .value) "@" }}
175
{{ end }}
176
-
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
177
{{ end }}
178
179
{{ define "nullTypeInput" }}
···
170
{{ $fieldName := $def.AtUri }}
171
{{ $valueType := $def.ValueType }}
172
{{ $value := .value }}
173
+
174
{{ if $valueType.IsDidFormat }}
175
{{ $value = trimPrefix (resolve .value) "@" }}
176
+
<actor-typeahead>
177
+
<input
178
+
autocapitalize="none"
179
+
autocorrect="off"
180
+
autocomplete="off"
181
+
placeholder="user.tngl.sh"
182
+
value="{{$value}}"
183
+
name="{{$fieldName}}"
184
+
type="text"
185
+
class="p-1 w-full text-sm"
186
+
/>
187
+
</actor-typeahead>
188
+
{{ else }}
189
+
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
190
{{ end }}
191
{{ end }}
192
193
{{ define "nullTypeInput" }}
+1
-16
appview/pages/templates/repo/fragments/participants.html
+1
-16
appview/pages/templates/repo/fragments/participants.html
···
6
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
7
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
8
</div>
9
-
<div class="flex items-center -space-x-3 mt-2">
10
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
11
-
{{ range $i, $p := $ps }}
12
-
<img
13
-
src="{{ tinyAvatar . }}"
14
-
alt=""
15
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
16
-
/>
17
-
{{ end }}
18
-
19
-
{{ if gt (len $all) 5 }}
20
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
21
-
+{{ sub (len $all) 5 }}
22
-
</span>
23
-
{{ end }}
24
-
</div>
25
</div>
26
{{ end }}
···
6
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
7
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
8
</div>
9
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }}
10
</div>
11
{{ end }}
+31
-9
appview/pages/templates/repo/index.html
+31
-9
appview/pages/templates/repo/index.html
···
14
{{ end }}
15
<div class="flex items-center justify-between pb-5">
16
{{ block "branchSelector" . }}{{ end }}
17
-
<div class="flex md:hidden items-center gap-2">
18
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
19
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
20
</a>
···
47
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap">
48
{{ range $value := .Languages }}
49
<div
50
-
class="flex flex-grow items-center gap-2 text-xs align-items-center justify-center"
51
>
52
{{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }}
53
<div>{{ or $value.Name "Other" }}
···
66
67
{{ define "branchSelector" }}
68
<div class="flex gap-2 items-center justify-between w-full">
69
-
<div class="flex gap-2 items-center">
70
<select
71
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
72
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
228
<span
229
class="mx-1 before:content-['ยท'] before:select-none"
230
></span>
231
-
<span>
232
-
{{ $did := index $.EmailToDid .Author.Email }}
233
-
<a href="{{ if $did }}/{{ resolve $did }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
234
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
235
-
>{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ .Author.Name }}{{ end }}</a>
236
-
</span>
237
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
238
{{ template "repo/fragments/time" .Committer.When }}
239
···
259
{{ end }}
260
</div>
261
</div>
262
{{ end }}
263
264
{{ define "branchList" }}
···
14
{{ end }}
15
<div class="flex items-center justify-between pb-5">
16
{{ block "branchSelector" . }}{{ end }}
17
+
<div class="flex md:hidden items-center gap-3">
18
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
19
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
20
</a>
···
47
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap">
48
{{ range $value := .Languages }}
49
<div
50
+
class="flex items-center gap-2 text-xs align-items-center justify-center"
51
>
52
{{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }}
53
<div>{{ or $value.Name "Other" }}
···
66
67
{{ define "branchSelector" }}
68
<div class="flex gap-2 items-center justify-between w-full">
69
+
<div class="flex gap-2 items-stretch">
70
<select
71
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
72
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
228
<span
229
class="mx-1 before:content-['ยท'] before:select-none"
230
></span>
231
+
{{ template "attribution" (list . $.EmailToDid) }}
232
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
233
{{ template "repo/fragments/time" .Committer.When }}
234
···
254
{{ end }}
255
</div>
256
</div>
257
+
{{ end }}
258
+
259
+
{{ define "attribution" }}
260
+
{{ $commit := index . 0 }}
261
+
{{ $map := index . 1 }}
262
+
<span class="flex items-center">
263
+
{{ $author := index $map $commit.Author.Email }}
264
+
{{ $coauthors := $commit.CoAuthors }}
265
+
{{ $all := list }}
266
+
267
+
{{ if $author }}
268
+
{{ $all = append $all $author }}
269
+
{{ end }}
270
+
{{ range $coauthors }}
271
+
{{ $co := index $map .Email }}
272
+
{{ if $co }}
273
+
{{ $all = append $all $co }}
274
+
{{ end }}
275
+
{{ end }}
276
+
277
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
278
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
279
+
class="no-underline hover:underline">
280
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
281
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
282
+
</a>
283
+
</span>
284
{{ end }}
285
286
{{ define "branchList" }}
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
19
{{ end }}
20
21
{{ define "timestamp" }}
22
-
<a href="#{{ .Comment.Id }}"
23
class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline"
24
-
id="{{ .Comment.Id }}">
25
{{ if .Comment.Deleted }}
26
{{ template "repo/fragments/shortTimeAgo" .Comment.Deleted }}
27
{{ else if .Comment.Edited }}
···
19
{{ end }}
20
21
{{ define "timestamp" }}
22
+
<a href="#comment-{{ .Comment.Id }}"
23
class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline"
24
+
id="comment-{{ .Comment.Id }}">
25
{{ if .Comment.Deleted }}
26
{{ template "repo/fragments/shortTimeAgo" .Comment.Deleted }}
27
{{ else if .Comment.Edited }}
+3
appview/pages/templates/repo/issues/issue.html
+3
appview/pages/templates/repo/issues/issue.html
···
20
"Subject" $.Issue.AtUri
21
"State" $.Issue.Labels) }}
22
{{ template "repo/fragments/participants" $.Issue.Participants }}
23
+
{{ template "repo/fragments/backlinks"
24
+
(dict "RepoInfo" $.RepoInfo
25
+
"Backlinks" $.Backlinks) }}
26
{{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }}
27
</div>
28
</div>
+2
-1
appview/pages/templates/repo/issues/issues.html
+2
-1
appview/pages/templates/repo/issues/issues.html
···
32
<input type="hidden" name="state" value="{{ if .FilteringByOpen }}open{{ else }}closed{{ end }}">
33
<div class="flex-1 flex relative">
34
<input
35
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
36
type="text"
37
name="q"
···
53
</button>
54
</form>
55
<div class="sm:row-start-1">
56
-
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active) }}
57
</div>
58
<a
59
href="/{{ .RepoInfo.FullName }}/issues/new"
···
32
<input type="hidden" name="state" value="{{ if .FilteringByOpen }}open{{ else }}closed{{ end }}">
33
<div class="flex-1 flex relative">
34
<input
35
+
id="search-q"
36
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
37
type="text"
38
name="q"
···
54
</button>
55
</form>
56
<div class="sm:row-start-1">
57
+
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active "Include" "#search-q") }}
58
</div>
59
<a
60
href="/{{ .RepoInfo.FullName }}/issues/new"
+40
-23
appview/pages/templates/repo/log.html
+40
-23
appview/pages/templates/repo/log.html
···
17
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
18
{{ $grid := "grid grid-cols-14 gap-4" }}
19
<div class="{{ $grid }}">
20
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2">Author</div>
21
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
22
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
23
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-1"></div>
24
<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>
25
</div>
26
{{ range $index, $commit := .Commits }}
27
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
28
<div class="{{ $grid }} py-3">
29
-
<div class="align-top truncate col-span-2">
30
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
36
</div>
37
<div class="align-top font-mono flex items-start col-span-3">
38
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
61
<div class="align-top col-span-6">
62
<div>
63
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
64
{{ if gt (len $messageParts) 1 }}
65
<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>
66
{{ end }}
···
72
</span>
73
{{ end }}
74
{{ end }}
75
</div>
76
77
{{ if gt (len $messageParts) 1 }}
78
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
79
{{ end }}
80
-
</div>
81
-
<div class="align-top col-span-1">
82
-
<!-- ci status -->
83
-
{{ $pipeline := index $.Pipelines .Hash.String }}
84
-
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
85
-
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
86
-
{{ end }}
87
</div>
88
<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>
89
</div>
···
152
</a>
153
</span>
154
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
155
-
<span>
156
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
157
-
<a href="{{ if $did }}/{{ $did }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
158
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
159
-
{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ $commit.Author.Name }}{{ end }}
160
-
</a>
161
-
</span>
162
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
163
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
164
···
176
</div>
177
</section>
178
179
{{ end }}
180
181
{{ define "repoAfter" }}
···
17
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
18
{{ $grid := "grid grid-cols-14 gap-4" }}
19
<div class="{{ $grid }}">
20
+
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
21
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
22
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
23
<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>
24
</div>
25
{{ range $index, $commit := .Commits }}
26
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
27
<div class="{{ $grid }} py-3">
28
+
<div class="align-top col-span-3">
29
+
{{ template "attribution" (list $commit $.EmailToDid) }}
30
</div>
31
<div class="align-top font-mono flex items-start col-span-3">
32
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
55
<div class="align-top col-span-6">
56
<div>
57
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
58
+
59
{{ if gt (len $messageParts) 1 }}
60
<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>
61
{{ end }}
···
67
</span>
68
{{ end }}
69
{{ end }}
70
+
71
+
<!-- ci status -->
72
+
<span class="text-xs">
73
+
{{ $pipeline := index $.Pipelines .Hash.String }}
74
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
75
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
76
+
{{ end }}
77
+
</span>
78
</div>
79
80
{{ if gt (len $messageParts) 1 }}
81
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
82
{{ end }}
83
</div>
84
<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>
85
</div>
···
148
</a>
149
</span>
150
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
151
+
{{ template "attribution" (list $commit $.EmailToDid) }}
152
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
153
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
154
···
166
</div>
167
</section>
168
169
+
{{ end }}
170
+
171
+
{{ define "attribution" }}
172
+
{{ $commit := index . 0 }}
173
+
{{ $map := index . 1 }}
174
+
<span class="flex items-center gap-1">
175
+
{{ $author := index $map $commit.Author.Email }}
176
+
{{ $coauthors := $commit.CoAuthors }}
177
+
{{ $all := list }}
178
+
179
+
{{ if $author }}
180
+
{{ $all = append $all $author }}
181
+
{{ end }}
182
+
{{ range $coauthors }}
183
+
{{ $co := index $map .Email }}
184
+
{{ if $co }}
185
+
{{ $all = append $all $co }}
186
+
{{ end }}
187
+
{{ end }}
188
+
189
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
190
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
191
+
class="no-underline hover:underline">
192
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
193
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
194
+
</a>
195
+
</span>
196
{{ end }}
197
198
{{ define "repoAfter" }}
+1
-1
appview/pages/templates/repo/new.html
+1
-1
appview/pages/templates/repo/new.html
+1
-1
appview/pages/templates/repo/pulls/patch.html
+1
-1
appview/pages/templates/repo/pulls/patch.html
+3
appview/pages/templates/repo/pulls/pull.html
+3
appview/pages/templates/repo/pulls/pull.html
···
21
"Subject" $.Pull.AtUri
22
"State" $.Pull.Labels) }}
23
{{ template "repo/fragments/participants" $.Pull.Participants }}
24
+
{{ template "repo/fragments/backlinks"
25
+
(dict "RepoInfo" $.RepoInfo
26
+
"Backlinks" $.Backlinks) }}
27
{{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }}
28
</div>
29
</div>
+2
-1
appview/pages/templates/repo/pulls/pulls.html
+2
-1
appview/pages/templates/repo/pulls/pulls.html
···
38
<input type="hidden" name="state" value="{{ .FilteringBy.String }}">
39
<div class="flex-1 flex relative">
40
<input
41
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
42
type="text"
43
name="q"
···
59
</button>
60
</form>
61
<div class="sm:row-start-1">
62
-
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active) }}
63
</div>
64
<a
65
href="/{{ .RepoInfo.FullName }}/pulls/new"
···
38
<input type="hidden" name="state" value="{{ .FilteringBy.String }}">
39
<div class="flex-1 flex relative">
40
<input
41
+
id="search-q"
42
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
43
type="text"
44
name="q"
···
60
</button>
61
</form>
62
<div class="sm:row-start-1">
63
+
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active "Include" "#search-q") }}
64
</div>
65
<a
66
href="/{{ .RepoInfo.FullName }}/pulls/new"
+5
-4
appview/pages/templates/repo/settings/access.html
+5
-4
appview/pages/templates/repo/settings/access.html
···
29
{{ template "addCollaboratorButton" . }}
30
{{ end }}
31
{{ range .Collaborators }}
32
<div class="border border-gray-200 dark:border-gray-700 rounded p-4">
33
<div class="flex items-center gap-3">
34
<img
35
-
src="{{ fullAvatar .Handle }}"
36
-
alt="{{ .Handle }}"
37
class="rounded-full h-10 w-10 border border-gray-300 dark:border-gray-600 flex-shrink-0"/>
38
39
<div class="flex-1 min-w-0">
40
-
<a href="/{{ .Handle }}" class="block truncate">
41
-
{{ didOrHandle .Did .Handle }}
42
</a>
43
<p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p>
44
</div>
···
29
{{ template "addCollaboratorButton" . }}
30
{{ end }}
31
{{ range .Collaborators }}
32
+
{{ $handle := resolve .Did }}
33
<div class="border border-gray-200 dark:border-gray-700 rounded p-4">
34
<div class="flex items-center gap-3">
35
<img
36
+
src="{{ fullAvatar $handle }}"
37
+
alt="{{ $handle }}"
38
class="rounded-full h-10 w-10 border border-gray-300 dark:border-gray-600 flex-shrink-0"/>
39
40
<div class="flex-1 min-w-0">
41
+
<a href="/{{ $handle }}" class="block truncate">
42
+
{{ $handle }}
43
</a>
44
<p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p>
45
</div>
+22
-6
appview/pages/templates/spindles/dashboard.html
+22
-6
appview/pages/templates/spindles/dashboard.html
···
1
-
{{ define "title" }}{{.Spindle.Instance}} · spindles{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
5
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Spindle.Instance }}</h1>
7
<div id="right-side" class="flex gap-2">
8
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
71
<button
72
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
73
title="Delete spindle"
74
-
hx-delete="/spindles/{{ .Instance }}"
75
hx-swap="outerHTML"
76
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
77
hx-headers='{"shouldRedirect": "true"}'
···
87
<button
88
class="btn gap-2 group"
89
title="Retry spindle verification"
90
-
hx-post="/spindles/{{ .Instance }}/retry"
91
hx-swap="none"
92
hx-headers='{"shouldRefresh": "true"}'
93
>
···
104
<button
105
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
106
title="Remove member"
107
-
hx-post="/spindles/{{ $root.Spindle.Instance }}/remove"
108
hx-swap="none"
109
hx-vals='{"member": "{{$member}}" }'
110
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
···
1
+
{{ define "title" }}{{.Spindle.Instance}} · {{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleDash" }}
20
+
<div>
21
<div class="flex justify-between items-center">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Spindle.Instance }}</h2>
23
<div id="right-side" class="flex gap-2">
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
87
<button
88
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
89
title="Delete spindle"
90
+
hx-delete="/settings/spindles/{{ .Instance }}"
91
hx-swap="outerHTML"
92
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
93
hx-headers='{"shouldRedirect": "true"}'
···
103
<button
104
class="btn gap-2 group"
105
title="Retry spindle verification"
106
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
107
hx-swap="none"
108
hx-headers='{"shouldRefresh": "true"}'
109
>
···
120
<button
121
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
122
title="Remove member"
123
+
hx-post="/settings/spindles/{{ $root.Spindle.Instance }}/remove"
124
hx-swap="none"
125
hx-vals='{"member": "{{$member}}" }'
126
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
···
7
8
{{ define "spindleLeftSide" }}
9
{{ if .Verified }}
10
-
<a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Instance }}
···
50
<button
51
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
52
title="Delete spindle"
53
-
hx-delete="/spindles/{{ .Instance }}"
54
hx-swap="outerHTML"
55
hx-target="#spindle-{{.Id}}"
56
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
66
<button
67
class="btn gap-2 group"
68
title="Retry spindle verification"
69
-
hx-post="/spindles/{{ .Instance }}/retry"
70
hx-swap="none"
71
hx-target="#spindle-{{.Id}}"
72
>
···
7
8
{{ define "spindleLeftSide" }}
9
{{ if .Verified }}
10
+
<a href="/settings/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Instance }}
···
50
<button
51
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
52
title="Delete spindle"
53
+
hx-delete="/settings/spindles/{{ .Instance }}"
54
hx-swap="outerHTML"
55
hx-target="#spindle-{{.Id}}"
56
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
66
<button
67
class="btn gap-2 group"
68
title="Retry spindle verification"
69
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
70
hx-swap="none"
71
hx-target="#spindle-{{.Id}}"
72
>
+90
-59
appview/pages/templates/spindles/index.html
+90
-59
appview/pages/templates/spindles/index.html
···
1
-
{{ define "title" }}spindles{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">docs</a>
9
-
</span>
10
</div>
11
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
13
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
{{ block "list" . }} {{ end }}
16
{{ block "register" . }} {{ end }}
17
</div>
···
20
21
{{ define "about" }}
22
<section class="rounded flex items-center gap-2">
23
-
<p class="text-gray-500 dark:text-gray-400">
24
-
Spindles are small CI runners.
25
-
</p>
26
</section>
27
{{ end }}
28
29
{{ define "list" }}
30
-
<section class="rounded w-full flex flex-col gap-2">
31
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
32
-
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
33
-
{{ range $spindle := .Spindles }}
34
-
{{ template "spindles/fragments/spindleListing" . }}
35
-
{{ else }}
36
-
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
37
-
no spindles registered yet
38
-
</div>
39
-
{{ end }}
40
</div>
41
-
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
42
-
</section>
43
{{ end }}
44
45
{{ define "register" }}
46
-
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
47
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
48
-
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
49
-
<form
50
-
hx-post="/spindles/register"
51
-
class="max-w-2xl mb-2 space-y-4"
52
-
hx-indicator="#register-button"
53
-
hx-swap="none"
54
-
>
55
-
<div class="flex gap-2">
56
-
<input
57
-
type="text"
58
-
id="instance"
59
-
name="instance"
60
-
placeholder="spindle.example.com"
61
-
required
62
-
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
63
-
>
64
-
<button
65
-
type="submit"
66
-
id="register-button"
67
-
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
68
-
>
69
-
<span class="inline-flex items-center gap-2">
70
-
{{ i "plus" "w-4 h-4" }}
71
-
register
72
-
</span>
73
-
<span class="pl-2 hidden group-[.htmx-request]:inline">
74
-
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
75
-
</span>
76
-
</button>
77
-
</div>
78
79
-
<div id="register-error" class="dark:text-red-400"></div>
80
-
</form>
81
82
-
</section>
83
{{ end }}
···
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
28
</div>
29
30
+
<section>
31
<div class="flex flex-col gap-6">
32
{{ block "list" . }} {{ end }}
33
{{ block "register" . }} {{ end }}
34
</div>
···
37
38
{{ define "about" }}
39
<section class="rounded flex items-center gap-2">
40
+
<p class="text-gray-500 dark:text-gray-400">
41
+
Spindles are small CI runners.
42
+
</p>
43
</section>
44
{{ end }}
45
46
{{ define "list" }}
47
+
<section class="rounded w-full flex flex-col gap-2">
48
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
49
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
50
+
{{ range $spindle := .Spindles }}
51
+
{{ template "spindles/fragments/spindleListing" . }}
52
+
{{ else }}
53
+
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
54
+
no spindles registered yet
55
</div>
56
+
{{ end }}
57
+
</div>
58
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
59
+
</section>
60
{{ end }}
61
62
{{ define "register" }}
63
+
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
64
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
65
+
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
66
+
<form
67
+
hx-post="/settings/spindles/register"
68
+
class="max-w-2xl mb-2 space-y-4"
69
+
hx-indicator="#register-button"
70
+
hx-swap="none"
71
+
>
72
+
<div class="flex gap-2">
73
+
<input
74
+
type="text"
75
+
id="instance"
76
+
name="instance"
77
+
placeholder="spindle.example.com"
78
+
required
79
+
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
80
+
>
81
+
<button
82
+
type="submit"
83
+
id="register-button"
84
+
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
85
+
>
86
+
<span class="inline-flex items-center gap-2">
87
+
{{ i "plus" "w-4 h-4" }}
88
+
register
89
+
</span>
90
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
91
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
92
+
</span>
93
+
</button>
94
+
</div>
95
96
+
<div id="register-error" class="dark:text-red-400"></div>
97
+
</form>
98
+
99
+
</section>
100
+
{{ end }}
101
102
+
{{ define "docsButton" }}
103
+
<a
104
+
class="btn flex items-center gap-2"
105
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
106
+
{{ i "book" "size-4" }}
107
+
docs
108
+
</a>
109
+
<div
110
+
id="add-email-modal"
111
+
popover
112
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
113
+
</div>
114
{{ end }}
+6
-5
appview/pages/templates/strings/dashboard.html
+6
-5
appview/pages/templates/strings/dashboard.html
···
1
-
{{ define "title" }}strings by {{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
3
{{ define "extrameta" }}
4
-
<meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
5
<meta property="og:type" content="profile" />
6
-
<meta property="og:url" content="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}" />
7
-
<meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
8
{{ end }}
9
10
···
35
{{ $s := index . 1 }}
36
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
37
<div class="font-medium dark:text-white flex gap-2 items-center">
38
-
<a href="/strings/{{ or $root.Card.UserHandle $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
39
</div>
40
{{ with $s.Description }}
41
<div class="text-gray-600 dark:text-gray-300 text-sm">
···
1
+
{{ define "title" }}strings by {{ resolve .Card.UserDid }}{{ end }}
2
3
{{ define "extrameta" }}
4
+
{{ $handle := resolve .Card.UserDid }}
5
+
<meta property="og:title" content="{{ $handle }}" />
6
<meta property="og:type" content="profile" />
7
+
<meta property="og:url" content="https://tangled.org/{{ $handle }}" />
8
+
<meta property="og:description" content="{{ or .Card.Profile.Description $handle }}" />
9
{{ end }}
10
11
···
36
{{ $s := index . 1 }}
37
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
38
<div class="font-medium dark:text-white flex gap-2 items-center">
39
+
<a href="/strings/{{ resolve $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
40
</div>
41
{{ with $s.Description }}
42
<div class="text-gray-600 dark:text-gray-300 text-sm">
+3
-3
appview/pages/templates/strings/string.html
+3
-3
appview/pages/templates/strings/string.html
···
1
-
{{ define "title" }}{{ .String.Filename }} ยท by {{ didOrHandle .Owner.DID.String .Owner.Handle.String }}{{ end }}
2
3
{{ define "extrameta" }}
4
-
{{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }}
5
<meta property="og:title" content="{{ .String.Filename }} ยท by {{ $ownerId }}" />
6
<meta property="og:type" content="object" />
7
<meta property="og:url" content="https://tangled.org/strings/{{ $ownerId }}/{{ .String.Rkey }}" />
···
9
{{ end }}
10
11
{{ define "content" }}
12
-
{{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }}
13
<section id="string-header" class="mb-4 py-2 px-6 dark:text-white">
14
<div class="text-lg flex items-center justify-between">
15
<div>
···
1
+
{{ define "title" }}{{ .String.Filename }} ยท by {{ resolve .Owner.DID.String }}{{ end }}
2
3
{{ define "extrameta" }}
4
+
{{ $ownerId := resolve .Owner.DID.String }}
5
<meta property="og:title" content="{{ .String.Filename }} ยท by {{ $ownerId }}" />
6
<meta property="og:type" content="object" />
7
<meta property="og:url" content="https://tangled.org/strings/{{ $ownerId }}/{{ .String.Rkey }}" />
···
9
{{ end }}
10
11
{{ define "content" }}
12
+
{{ $ownerId := resolve .Owner.DID.String }}
13
<section id="string-header" class="mb-4 py-2 px-6 dark:text-white">
14
<div class="text-lg flex items-center justify-between">
15
<div>
+1
-1
appview/pages/templates/timeline/fragments/timeline.html
+1
-1
appview/pages/templates/timeline/fragments/timeline.html
···
14
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
15
{{ if .Repo }}
16
{{ template "timeline/fragments/repoEvent" (list $ .) }}
17
-
{{ else if .Star }}
18
{{ template "timeline/fragments/starEvent" (list $ .) }}
19
{{ else if .Follow }}
20
{{ template "timeline/fragments/followEvent" (list $ .) }}
···
14
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
15
{{ if .Repo }}
16
{{ template "timeline/fragments/repoEvent" (list $ .) }}
17
+
{{ else if .RepoStar }}
18
{{ template "timeline/fragments/starEvent" (list $ .) }}
19
{{ else if .Follow }}
20
{{ template "timeline/fragments/followEvent" (list $ .) }}
+4
-2
appview/pages/templates/user/followers.html
+4
-2
appview/pages/templates/user/followers.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท followers {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-followers" class="md:col-span-8 order-2 md:order-2">
···
19
"FollowersCount" .FollowersCount
20
"FollowingCount" .FollowingCount) }}
21
{{ else }}
22
-
<p class="px-6 dark:text-white">This user does not have any followers yet.</p>
23
{{ end }}
24
</div>
25
{{ end }}
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท followers {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-followers" class="md:col-span-8 order-2 md:order-2">
···
19
"FollowersCount" .FollowersCount
20
"FollowingCount" .FollowingCount) }}
21
{{ else }}
22
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
23
+
<span>This user does not have any followers yet.</span>
24
+
</div>
25
{{ end }}
26
</div>
27
{{ end }}
+4
-2
appview/pages/templates/user/following.html
+4
-2
appview/pages/templates/user/following.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท following {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-following" class="md:col-span-8 order-2 md:order-2">
···
19
"FollowersCount" .FollowersCount
20
"FollowingCount" .FollowingCount) }}
21
{{ else }}
22
-
<p class="px-6 dark:text-white">This user does not follow anyone yet.</p>
23
{{ end }}
24
</div>
25
{{ end }}
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท following {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-following" class="md:col-span-8 order-2 md:order-2">
···
19
"FollowersCount" .FollowersCount
20
"FollowingCount" .FollowingCount) }}
21
{{ else }}
22
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
23
+
<span>This user does not follow anyone yet.</span>
24
+
</div>
25
{{ end }}
26
</div>
27
{{ end }}
+1
-1
appview/pages/templates/user/fragments/profileCard.html
+1
-1
appview/pages/templates/user/fragments/profileCard.html
+22
-4
appview/pages/templates/user/overview.html
+22
-4
appview/pages/templates/user/overview.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-4 order-2 md:order-2">
···
16
<p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p>
17
<div class="flex flex-col gap-4 relative">
18
{{ if .ProfileTimeline.IsEmpty }}
19
-
<p class="dark:text-white">This user does not have any activity yet.</p>
20
{{ end }}
21
22
{{ with .ProfileTimeline }}
···
33
</p>
34
35
<div class="flex flex-col gap-1">
36
{{ block "repoEvents" .RepoEvents }} {{ end }}
37
{{ block "issueEvents" .IssueEvents }} {{ end }}
38
{{ block "pullEvents" .PullEvents }} {{ end }}
···
43
{{ end }}
44
{{ end }}
45
</div>
46
{{ end }}
47
48
{{ define "repoEvents" }}
···
224
{{ define "ownRepos" }}
225
<div>
226
<div class="text-sm font-bold px-2 pb-4 dark:text-white flex items-center gap-2">
227
-
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
228
class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group">
229
<span>PINNED REPOS</span>
230
</a>
···
244
{{ template "user/fragments/repoCard" (list $ . false) }}
245
</div>
246
{{ else }}
247
-
<p class="dark:text-white">This user does not have any pinned repos.</p>
248
{{ end }}
249
</div>
250
</div>
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }}{{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-4 order-2 md:order-2">
···
16
<p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p>
17
<div class="flex flex-col gap-4 relative">
18
{{ if .ProfileTimeline.IsEmpty }}
19
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
20
+
<span class="flex items-center gap-2">
21
+
This user does not have any activity yet.
22
+
</span>
23
+
</div>
24
{{ end }}
25
26
{{ with .ProfileTimeline }}
···
37
</p>
38
39
<div class="flex flex-col gap-1">
40
+
{{ block "commits" .Commits }} {{ end }}
41
{{ block "repoEvents" .RepoEvents }} {{ end }}
42
{{ block "issueEvents" .IssueEvents }} {{ end }}
43
{{ block "pullEvents" .PullEvents }} {{ end }}
···
48
{{ end }}
49
{{ end }}
50
</div>
51
+
{{ end }}
52
+
53
+
{{ define "commits" }}
54
+
{{ if . }}
55
+
<div class="flex flex-wrap items-center gap-1">
56
+
{{ i "git-commit-horizontal" "size-5" }}
57
+
created {{ . }} commits
58
+
</div>
59
+
{{ end }}
60
{{ end }}
61
62
{{ define "repoEvents" }}
···
238
{{ define "ownRepos" }}
239
<div>
240
<div class="text-sm font-bold px-2 pb-4 dark:text-white flex items-center gap-2">
241
+
<a href="/{{ resolve $.Card.UserDid }}?tab=repos"
242
class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group">
243
<span>PINNED REPOS</span>
244
</a>
···
258
{{ template "user/fragments/repoCard" (list $ . false) }}
259
</div>
260
{{ else }}
261
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
262
+
<span class="flex items-center gap-2">
263
+
This user does not have any pinned repos.
264
+
</span>
265
+
</div>
266
{{ end }}
267
</div>
268
</div>
+4
-2
appview/pages/templates/user/repos.html
+4
-2
appview/pages/templates/user/repos.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "user/fragments/repoCard" (list $ . false) }}
14
</div>
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
17
{{ end }}
18
</div>
19
{{ end }}
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท repos {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "user/fragments/repoCard" (list $ . false) }}
14
</div>
15
{{ else }}
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any repos yet.</span>
18
+
</div>
19
{{ end }}
20
</div>
21
{{ end }}
+1
-1
appview/pages/templates/user/settings/notifications.html
+1
-1
appview/pages/templates/user/settings/notifications.html
+9
-6
appview/pages/templates/user/signup.html
+9
-6
appview/pages/templates/user/signup.html
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
-
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
</form>
52
-
<p class="text-sm text-gray-500">
53
-
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
54
-
</p>
55
-
56
-
<p id="signup-msg" class="error w-full"></p>
57
</main>
58
</body>
59
</html>
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
+
<p class="text-sm text-gray-500">
52
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
53
+
</p>
54
+
55
+
<p id="signup-msg" class="error w-full"></p>
56
+
<p class="text-sm text-gray-500 pt-4">
57
+
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>.
58
+
</p>
59
</form>
60
</main>
61
</body>
62
</html>
+4
-2
appview/pages/templates/user/starred.html
+4
-2
appview/pages/templates/user/starred.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "user/fragments/repoCard" (list $ . true) }}
14
</div>
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any starred repos yet.</p>
17
{{ end }}
18
</div>
19
{{ end }}
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท repos {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "user/fragments/repoCard" (list $ . true) }}
14
</div>
15
{{ else }}
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any starred repos yet.</span>
18
+
</div>
19
{{ end }}
20
</div>
21
{{ end }}
+5
-3
appview/pages/templates/user/strings.html
+5
-3
appview/pages/templates/user/strings.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท strings {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-strings" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "singleString" (list $ .) }}
14
</div>
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any strings yet.</p>
17
{{ end }}
18
</div>
19
{{ end }}
···
23
{{ $s := index . 1 }}
24
<div class="py-4 px-6 rounded bg-white dark:bg-gray-800">
25
<div class="font-medium dark:text-white flex gap-2 items-center">
26
-
<a href="/strings/{{ or $root.Card.UserHandle $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
27
</div>
28
{{ with $s.Description }}
29
<div class="text-gray-600 dark:text-gray-300 text-sm">
···
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท strings {{ end }}
2
3
{{ define "profileContent" }}
4
<div id="all-strings" class="md:col-span-8 order-2 md:order-2">
···
13
{{ template "singleString" (list $ .) }}
14
</div>
15
{{ else }}
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any strings yet.</span>
18
+
</div>
19
{{ end }}
20
</div>
21
{{ end }}
···
25
{{ $s := index . 1 }}
26
<div class="py-4 px-6 rounded bg-white dark:bg-gray-800">
27
<div class="font-medium dark:text-white flex gap-2 items-center">
28
+
<a href="/strings/{{ resolve $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
29
</div>
30
{{ with $s.Description }}
31
<div class="text-gray-600 dark:text-gray-300 text-sm">
+16
-22
appview/pipelines/pipelines.go
+16
-22
appview/pipelines/pipelines.go
···
16
"tangled.org/core/appview/reporesolver"
17
"tangled.org/core/eventconsumer"
18
"tangled.org/core/idresolver"
19
"tangled.org/core/rbac"
20
spindlemodel "tangled.org/core/spindle/models"
21
···
78
return
79
}
80
81
-
repoInfo := f.RepoInfo(user)
82
-
83
ps, err := db.GetPipelineStatuses(
84
p.db,
85
30,
86
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
87
-
db.FilterEq("repo_name", repoInfo.Name),
88
-
db.FilterEq("knot", repoInfo.Knot),
89
)
90
if err != nil {
91
l.Error("failed to query db", "err", err)
···
94
95
p.pages.Pipelines(w, pages.PipelinesParams{
96
LoggedInUser: user,
97
-
RepoInfo: repoInfo,
98
Pipelines: ps,
99
})
100
}
···
108
l.Error("failed to get repo and knot", "err", err)
109
return
110
}
111
-
112
-
repoInfo := f.RepoInfo(user)
113
114
pipelineId := chi.URLParam(r, "pipeline")
115
if pipelineId == "" {
···
126
ps, err := db.GetPipelineStatuses(
127
p.db,
128
1,
129
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
130
-
db.FilterEq("repo_name", repoInfo.Name),
131
-
db.FilterEq("knot", repoInfo.Knot),
132
-
db.FilterEq("id", pipelineId),
133
)
134
if err != nil {
135
l.Error("failed to query db", "err", err)
···
145
146
p.pages.Workflow(w, pages.WorkflowParams{
147
LoggedInUser: user,
148
-
RepoInfo: repoInfo,
149
Pipeline: singlePipeline,
150
Workflow: workflow,
151
})
···
176
ctx, cancel := context.WithCancel(r.Context())
177
defer cancel()
178
179
-
user := p.oauth.GetUser(r)
180
f, err := p.repoResolver.Resolve(r)
181
if err != nil {
182
l.Error("failed to get repo and knot", "err", err)
···
184
return
185
}
186
187
-
repoInfo := f.RepoInfo(user)
188
-
189
pipelineId := chi.URLParam(r, "pipeline")
190
workflow := chi.URLParam(r, "workflow")
191
if pipelineId == "" || workflow == "" {
···
196
ps, err := db.GetPipelineStatuses(
197
p.db,
198
1,
199
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
200
-
db.FilterEq("repo_name", repoInfo.Name),
201
-
db.FilterEq("knot", repoInfo.Knot),
202
-
db.FilterEq("id", pipelineId),
203
)
204
if err != nil || len(ps) != 1 {
205
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
208
}
209
210
singlePipeline := ps[0]
211
-
spindle := repoInfo.Spindle
212
-
knot := repoInfo.Knot
213
rkey := singlePipeline.Rkey
214
215
if spindle == "" || knot == "" || rkey == "" {
···
16
"tangled.org/core/appview/reporesolver"
17
"tangled.org/core/eventconsumer"
18
"tangled.org/core/idresolver"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
spindlemodel "tangled.org/core/spindle/models"
22
···
79
return
80
}
81
82
ps, err := db.GetPipelineStatuses(
83
p.db,
84
30,
85
+
orm.FilterEq("repo_owner", f.Did),
86
+
orm.FilterEq("repo_name", f.Name),
87
+
orm.FilterEq("knot", f.Knot),
88
)
89
if err != nil {
90
l.Error("failed to query db", "err", err)
···
93
94
p.pages.Pipelines(w, pages.PipelinesParams{
95
LoggedInUser: user,
96
+
RepoInfo: p.repoResolver.GetRepoInfo(r, user),
97
Pipelines: ps,
98
})
99
}
···
107
l.Error("failed to get repo and knot", "err", err)
108
return
109
}
110
111
pipelineId := chi.URLParam(r, "pipeline")
112
if pipelineId == "" {
···
123
ps, err := db.GetPipelineStatuses(
124
p.db,
125
1,
126
+
orm.FilterEq("repo_owner", f.Did),
127
+
orm.FilterEq("repo_name", f.Name),
128
+
orm.FilterEq("knot", f.Knot),
129
+
orm.FilterEq("id", pipelineId),
130
)
131
if err != nil {
132
l.Error("failed to query db", "err", err)
···
142
143
p.pages.Workflow(w, pages.WorkflowParams{
144
LoggedInUser: user,
145
+
RepoInfo: p.repoResolver.GetRepoInfo(r, user),
146
Pipeline: singlePipeline,
147
Workflow: workflow,
148
})
···
173
ctx, cancel := context.WithCancel(r.Context())
174
defer cancel()
175
176
f, err := p.repoResolver.Resolve(r)
177
if err != nil {
178
l.Error("failed to get repo and knot", "err", err)
···
180
return
181
}
182
183
pipelineId := chi.URLParam(r, "pipeline")
184
workflow := chi.URLParam(r, "workflow")
185
if pipelineId == "" || workflow == "" {
···
190
ps, err := db.GetPipelineStatuses(
191
p.db,
192
1,
193
+
orm.FilterEq("repo_owner", f.Did),
194
+
orm.FilterEq("repo_name", f.Name),
195
+
orm.FilterEq("knot", f.Knot),
196
+
orm.FilterEq("id", pipelineId),
197
)
198
if err != nil || len(ps) != 1 {
199
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
202
}
203
204
singlePipeline := ps[0]
205
+
spindle := f.Spindle
206
+
knot := f.Knot
207
rkey := singlePipeline.Rkey
208
209
if spindle == "" || knot == "" || rkey == "" {
+3
-2
appview/pulls/opengraph.go
+3
-2
appview/pulls/opengraph.go
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
"tangled.org/core/patchutil"
17
"tangled.org/core/types"
18
)
···
276
}
277
278
// Get comment count from database
279
-
comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID))
280
if err != nil {
281
log.Printf("failed to get pull comments: %v", err)
282
}
···
293
filesChanged = niceDiff.Stat.FilesChanged
294
}
295
296
-
card, err := s.drawPullSummaryCard(pull, &f.Repo, commentCount, diffStats, filesChanged)
297
if err != nil {
298
log.Println("failed to draw pull summary card", err)
299
http.Error(w, "failed to draw pull summary card", http.StatusInternalServerError)
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
+
"tangled.org/core/orm"
17
"tangled.org/core/patchutil"
18
"tangled.org/core/types"
19
)
···
277
}
278
279
// Get comment count from database
280
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
281
if err != nil {
282
log.Printf("failed to get pull comments: %v", err)
283
}
···
294
filesChanged = niceDiff.Stat.FilesChanged
295
}
296
297
+
card, err := s.drawPullSummaryCard(pull, f, commentCount, diffStats, filesChanged)
298
if err != nil {
299
log.Println("failed to draw pull summary card", err)
300
http.Error(w, "failed to draw pull summary card", http.StatusInternalServerError)
+138
-142
appview/pulls/pulls.go
+138
-142
appview/pulls/pulls.go
···
1
package pulls
2
3
import (
4
"database/sql"
5
"encoding/json"
6
"errors"
···
18
"tangled.org/core/appview/config"
19
"tangled.org/core/appview/db"
20
pulls_indexer "tangled.org/core/appview/indexer/pulls"
21
"tangled.org/core/appview/models"
22
"tangled.org/core/appview/notify"
23
"tangled.org/core/appview/oauth"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/pages/markup"
26
"tangled.org/core/appview/reporesolver"
27
"tangled.org/core/appview/validator"
28
"tangled.org/core/appview/xrpcclient"
29
"tangled.org/core/idresolver"
30
"tangled.org/core/patchutil"
31
"tangled.org/core/rbac"
32
"tangled.org/core/tid"
···
41
)
42
43
type Pulls struct {
44
-
oauth *oauth.OAuth
45
-
repoResolver *reporesolver.RepoResolver
46
-
pages *pages.Pages
47
-
idResolver *idresolver.Resolver
48
-
db *db.DB
49
-
config *config.Config
50
-
notifier notify.Notifier
51
-
enforcer *rbac.Enforcer
52
-
logger *slog.Logger
53
-
validator *validator.Validator
54
-
indexer *pulls_indexer.Indexer
55
}
56
57
func New(
···
59
repoResolver *reporesolver.RepoResolver,
60
pages *pages.Pages,
61
resolver *idresolver.Resolver,
62
db *db.DB,
63
config *config.Config,
64
notifier notify.Notifier,
···
68
logger *slog.Logger,
69
) *Pulls {
70
return &Pulls{
71
-
oauth: oauth,
72
-
repoResolver: repoResolver,
73
-
pages: pages,
74
-
idResolver: resolver,
75
-
db: db,
76
-
config: config,
77
-
notifier: notifier,
78
-
enforcer: enforcer,
79
-
logger: logger,
80
-
validator: validator,
81
-
indexer: indexer,
82
}
83
}
84
···
123
124
s.pages.PullActionsFragment(w, pages.PullActionsParams{
125
LoggedInUser: user,
126
-
RepoInfo: f.RepoInfo(user),
127
Pull: pull,
128
RoundNumber: roundNumber,
129
MergeCheck: mergeCheckResponse,
···
150
return
151
}
152
153
// can be nil if this pull is not stacked
154
stack, _ := r.Context().Value("stack").(models.Stack)
155
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull)
···
160
if user != nil && user.Did == pull.OwnerDid {
161
resubmitResult = s.resubmitCheck(r, f, pull, stack)
162
}
163
-
164
-
repoInfo := f.RepoInfo(user)
165
166
m := make(map[string]models.Pipeline)
167
···
179
ps, err := db.GetPipelineStatuses(
180
s.db,
181
len(shas),
182
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
183
-
db.FilterEq("repo_name", repoInfo.Name),
184
-
db.FilterEq("knot", repoInfo.Knot),
185
-
db.FilterIn("sha", shas),
186
)
187
if err != nil {
188
log.Printf("failed to fetch pipeline statuses: %s", err)
···
206
207
labelDefs, err := db.GetLabelDefinitions(
208
s.db,
209
-
db.FilterIn("at_uri", f.Repo.Labels),
210
-
db.FilterContains("scope", tangled.RepoPullNSID),
211
)
212
if err != nil {
213
log.Println("failed to fetch labels", err)
···
222
223
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
224
LoggedInUser: user,
225
-
RepoInfo: repoInfo,
226
Pull: pull,
227
Stack: stack,
228
AbandonedPulls: abandonedPulls,
229
BranchDeleteStatus: branchDeleteStatus,
230
MergeCheck: mergeCheckResponse,
231
ResubmitCheck: resubmitResult,
···
239
})
240
}
241
242
-
func (s *Pulls) mergeCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
243
if pull.State == models.PullMerged {
244
return types.MergeCheckResponse{}
245
}
···
268
r.Context(),
269
&xrpcc,
270
&tangled.RepoMergeCheck_Input{
271
-
Did: f.OwnerDid(),
272
Name: f.Name,
273
Branch: pull.TargetBranch,
274
Patch: patch,
···
306
return result
307
}
308
309
-
func (s *Pulls) branchDeleteStatus(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull) *models.BranchDeleteStatus {
310
if pull.State != models.PullMerged {
311
return nil
312
}
···
317
}
318
319
var branch string
320
-
var repo *models.Repo
321
// check if the branch exists
322
// NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates
323
if pull.IsBranchBased() {
324
branch = pull.PullSource.Branch
325
-
repo = &f.Repo
326
} else if pull.IsForkBased() {
327
branch = pull.PullSource.Branch
328
repo = pull.PullSource.Repo
···
361
}
362
}
363
364
-
func (s *Pulls) resubmitCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
365
if pull.State == models.PullMerged || pull.State == models.PullDeleted || pull.PullSource == nil {
366
return pages.Unknown
367
}
···
381
repoName = sourceRepo.Name
382
} else {
383
// pulls within the same repo
384
-
knot = f.Knot
385
-
ownerDid = f.OwnerDid()
386
-
repoName = f.Name
387
}
388
389
scheme := "http"
···
395
Host: host,
396
}
397
398
-
repo := fmt.Sprintf("%s/%s", ownerDid, repoName)
399
-
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, repo)
400
if err != nil {
401
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
402
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
424
425
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
426
user := s.oauth.GetUser(r)
427
-
f, err := s.repoResolver.Resolve(r)
428
-
if err != nil {
429
-
log.Println("failed to get repo and knot", err)
430
-
return
431
-
}
432
433
var diffOpts types.DiffOpts
434
if d := r.URL.Query().Get("diff"); d == "split" {
···
457
458
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
459
LoggedInUser: user,
460
-
RepoInfo: f.RepoInfo(user),
461
Pull: pull,
462
Stack: stack,
463
Round: roundIdInt,
···
471
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
472
user := s.oauth.GetUser(r)
473
474
-
f, err := s.repoResolver.Resolve(r)
475
-
if err != nil {
476
-
log.Println("failed to get repo and knot", err)
477
-
return
478
-
}
479
-
480
var diffOpts types.DiffOpts
481
if d := r.URL.Query().Get("diff"); d == "split" {
482
diffOpts.Split = true
···
521
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
LoggedInUser: s.oauth.GetUser(r),
524
-
RepoInfo: f.RepoInfo(user),
525
Pull: pull,
526
Round: roundIdInt,
527
Interdiff: interdiff,
···
598
599
pulls, err := db.GetPulls(
600
s.db,
601
-
db.FilterIn("id", ids),
602
)
603
if err != nil {
604
log.Println("failed to get pulls", err)
···
646
}
647
pulls = pulls[:n]
648
649
-
repoInfo := f.RepoInfo(user)
650
ps, err := db.GetPipelineStatuses(
651
s.db,
652
len(shas),
653
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
654
-
db.FilterEq("repo_name", repoInfo.Name),
655
-
db.FilterEq("knot", repoInfo.Knot),
656
-
db.FilterIn("sha", shas),
657
)
658
if err != nil {
659
log.Printf("failed to fetch pipeline statuses: %s", err)
···
666
667
labelDefs, err := db.GetLabelDefinitions(
668
s.db,
669
-
db.FilterIn("at_uri", f.Repo.Labels),
670
-
db.FilterContains("scope", tangled.RepoPullNSID),
671
)
672
if err != nil {
673
log.Println("failed to fetch labels", err)
···
682
683
s.pages.RepoPulls(w, pages.RepoPullsParams{
684
LoggedInUser: s.oauth.GetUser(r),
685
-
RepoInfo: f.RepoInfo(user),
686
Pulls: pulls,
687
LabelDefs: defs,
688
FilteringBy: state,
···
693
}
694
695
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
696
-
l := s.logger.With("handler", "PullComment")
697
user := s.oauth.GetUser(r)
698
f, err := s.repoResolver.Resolve(r)
699
if err != nil {
···
720
case http.MethodGet:
721
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
722
LoggedInUser: user,
723
-
RepoInfo: f.RepoInfo(user),
724
Pull: pull,
725
RoundNumber: roundNumber,
726
})
···
732
return
733
}
734
735
// Start a transaction
736
tx, err := s.db.BeginTx(r.Context(), nil)
737
if err != nil {
···
774
Body: body,
775
CommentAt: atResp.Uri,
776
SubmissionId: pull.Submissions[roundNumber].ID,
777
}
778
779
// Create the pull comment in the database with the commentAt field
···
791
return
792
}
793
794
-
rawMentions := markup.FindUserMentions(comment.Body)
795
-
idents := s.idResolver.ResolveIdents(r.Context(), rawMentions)
796
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
797
-
var mentions []syntax.DID
798
-
for _, ident := range idents {
799
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
800
-
mentions = append(mentions, ident.DID)
801
-
}
802
-
}
803
s.notifier.NewPullComment(r.Context(), comment, mentions)
804
805
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
806
return
807
}
808
}
···
826
Host: host,
827
}
828
829
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
830
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
831
if err != nil {
832
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
853
854
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
855
LoggedInUser: user,
856
-
RepoInfo: f.RepoInfo(user),
857
Branches: result.Branches,
858
Strategy: strategy,
859
SourceBranch: sourceBranch,
···
876
}
877
878
// Determine PR type based on input parameters
879
-
isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed()
880
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
881
isForkBased := fromFork != "" && sourceBranch != ""
882
isPatchBased := patch != "" && !isBranchBased && !isForkBased
···
974
func (s *Pulls) handleBranchBasedPull(
975
w http.ResponseWriter,
976
r *http.Request,
977
-
f *reporesolver.ResolvedRepo,
978
user *oauth.User,
979
title,
980
body,
···
986
if !s.config.Core.Dev {
987
scheme = "https"
988
}
989
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
990
xrpcc := &indigoxrpc.Client{
991
Host: host,
992
}
993
994
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
995
-
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, targetBranch, sourceBranch)
996
if err != nil {
997
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
998
log.Println("failed to call XRPC repo.compare", xrpcerr)
···
1029
Sha: comparison.Rev2,
1030
}
1031
1032
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1033
}
1034
1035
-
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
1036
if err := s.validator.ValidatePatch(&patch); err != nil {
1037
s.logger.Error("patch validation failed", "err", err)
1038
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
1039
return
1040
}
1041
1042
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1043
}
1044
1045
-
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1046
repoString := strings.SplitN(forkRepo, "/", 2)
1047
forkOwnerDid := repoString[0]
1048
repoName := repoString[1]
···
1144
Sha: sourceRev,
1145
}
1146
1147
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1148
}
1149
1150
func (s *Pulls) createPullRequest(
1151
w http.ResponseWriter,
1152
r *http.Request,
1153
-
f *reporesolver.ResolvedRepo,
1154
user *oauth.User,
1155
title, body, targetBranch string,
1156
patch string,
···
1165
s.createStackedPullRequest(
1166
w,
1167
r,
1168
-
f,
1169
user,
1170
targetBranch,
1171
patch,
···
1211
}
1212
}
1213
1214
rkey := tid.TID()
1215
initialSubmission := models.PullSubmission{
1216
Patch: patch,
···
1222
Body: body,
1223
TargetBranch: targetBranch,
1224
OwnerDid: user.Did,
1225
-
RepoAt: f.RepoAt(),
1226
Rkey: rkey,
1227
Submissions: []*models.PullSubmission{
1228
&initialSubmission,
1229
},
···
1235
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1236
return
1237
}
1238
-
pullId, err := db.NextPullId(tx, f.RepoAt())
1239
if err != nil {
1240
log.Println("failed to get pull id", err)
1241
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1250
Val: &tangled.RepoPull{
1251
Title: title,
1252
Target: &tangled.RepoPull_Target{
1253
-
Repo: string(f.RepoAt()),
1254
Branch: targetBranch,
1255
},
1256
Patch: patch,
···
1273
1274
s.notifier.NewPull(r.Context(), pull)
1275
1276
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1277
}
1278
1279
func (s *Pulls) createStackedPullRequest(
1280
w http.ResponseWriter,
1281
r *http.Request,
1282
-
f *reporesolver.ResolvedRepo,
1283
user *oauth.User,
1284
targetBranch string,
1285
patch string,
···
1311
1312
// build a stack out of this patch
1313
stackId := uuid.New()
1314
-
stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1315
if err != nil {
1316
log.Println("failed to create stack", err)
1317
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
···
1374
return
1375
}
1376
1377
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1378
}
1379
1380
func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) {
···
1405
1406
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1407
user := s.oauth.GetUser(r)
1408
-
f, err := s.repoResolver.Resolve(r)
1409
-
if err != nil {
1410
-
log.Println("failed to get repo and knot", err)
1411
-
return
1412
-
}
1413
1414
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1415
-
RepoInfo: f.RepoInfo(user),
1416
})
1417
}
1418
···
1433
Host: host,
1434
}
1435
1436
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1437
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1438
if err != nil {
1439
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1466
}
1467
1468
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1469
-
RepoInfo: f.RepoInfo(user),
1470
Branches: withoutDefault,
1471
})
1472
}
1473
1474
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1475
user := s.oauth.GetUser(r)
1476
-
f, err := s.repoResolver.Resolve(r)
1477
-
if err != nil {
1478
-
log.Println("failed to get repo and knot", err)
1479
-
return
1480
-
}
1481
1482
forks, err := db.GetForksByDid(s.db, user.Did)
1483
if err != nil {
···
1486
}
1487
1488
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1489
-
RepoInfo: f.RepoInfo(user),
1490
Forks: forks,
1491
Selected: r.URL.Query().Get("fork"),
1492
})
···
1508
// fork repo
1509
repo, err := db.GetRepo(
1510
s.db,
1511
-
db.FilterEq("did", forkOwnerDid),
1512
-
db.FilterEq("name", forkName),
1513
)
1514
if err != nil {
1515
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
1554
Host: targetHost,
1555
}
1556
1557
-
targetRepo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1558
targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo)
1559
if err != nil {
1560
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1579
})
1580
1581
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1582
-
RepoInfo: f.RepoInfo(user),
1583
SourceBranches: sourceBranches.Branches,
1584
TargetBranches: targetBranches.Branches,
1585
})
···
1587
1588
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1589
user := s.oauth.GetUser(r)
1590
-
f, err := s.repoResolver.Resolve(r)
1591
-
if err != nil {
1592
-
log.Println("failed to get repo and knot", err)
1593
-
return
1594
-
}
1595
1596
pull, ok := r.Context().Value("pull").(*models.Pull)
1597
if !ok {
···
1603
switch r.Method {
1604
case http.MethodGet:
1605
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1606
-
RepoInfo: f.RepoInfo(user),
1607
Pull: pull,
1608
})
1609
return
···
1670
return
1671
}
1672
1673
-
if !f.RepoInfo(user).Roles.IsPushAllowed() {
1674
log.Println("unauthorized user")
1675
w.WriteHeader(http.StatusUnauthorized)
1676
return
···
1685
Host: host,
1686
}
1687
1688
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1689
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch)
1690
if err != nil {
1691
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1812
func (s *Pulls) resubmitPullHelper(
1813
w http.ResponseWriter,
1814
r *http.Request,
1815
-
f *reporesolver.ResolvedRepo,
1816
user *oauth.User,
1817
pull *models.Pull,
1818
patch string,
···
1821
) {
1822
if pull.IsStacked() {
1823
log.Println("resubmitting stacked PR")
1824
-
s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1825
return
1826
}
1827
···
1901
Val: &tangled.RepoPull{
1902
Title: pull.Title,
1903
Target: &tangled.RepoPull_Target{
1904
-
Repo: string(f.RepoAt()),
1905
Branch: pull.TargetBranch,
1906
},
1907
Patch: patch, // new patch
···
1922
return
1923
}
1924
1925
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1926
}
1927
1928
func (s *Pulls) resubmitStackedPullHelper(
1929
w http.ResponseWriter,
1930
r *http.Request,
1931
-
f *reporesolver.ResolvedRepo,
1932
user *oauth.User,
1933
pull *models.Pull,
1934
patch string,
···
1937
targetBranch := pull.TargetBranch
1938
1939
origStack, _ := r.Context().Value("stack").(models.Stack)
1940
-
newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
1941
if err != nil {
1942
log.Println("failed to create resubmitted stack", err)
1943
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
2079
tx,
2080
p.ParentChangeId,
2081
// these should be enough filters to be unique per-stack
2082
-
db.FilterEq("repo_at", p.RepoAt.String()),
2083
-
db.FilterEq("owner_did", p.OwnerDid),
2084
-
db.FilterEq("change_id", p.ChangeId),
2085
)
2086
2087
if err != nil {
···
2115
return
2116
}
2117
2118
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2119
}
2120
2121
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
···
2168
2169
authorName := ident.Handle.String()
2170
mergeInput := &tangled.RepoMerge_Input{
2171
-
Did: f.OwnerDid(),
2172
Name: f.Name,
2173
Branch: pull.TargetBranch,
2174
Patch: patch,
···
2233
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2234
}
2235
2236
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId))
2237
}
2238
2239
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
···
2253
}
2254
2255
// auth filter: only owner or collaborators can close
2256
-
roles := f.RolesInRepo(user)
2257
isOwner := roles.IsOwner()
2258
isCollaborator := roles.IsCollaborator()
2259
isPullAuthor := user.Did == pull.OwnerDid
···
2305
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2306
}
2307
2308
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2309
}
2310
2311
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
···
2326
}
2327
2328
// auth filter: only owner or collaborators can close
2329
-
roles := f.RolesInRepo(user)
2330
isOwner := roles.IsOwner()
2331
isCollaborator := roles.IsCollaborator()
2332
isPullAuthor := user.Did == pull.OwnerDid
···
2378
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2379
}
2380
2381
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2382
}
2383
2384
-
func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2385
formatPatches, err := patchutil.ExtractPatches(patch)
2386
if err != nil {
2387
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2406
body := fp.Body
2407
rkey := tid.TID()
2408
2409
initialSubmission := models.PullSubmission{
2410
Patch: fp.Raw,
2411
SourceRev: fp.SHA,
···
2416
Body: body,
2417
TargetBranch: targetBranch,
2418
OwnerDid: user.Did,
2419
-
RepoAt: f.RepoAt(),
2420
Rkey: rkey,
2421
Submissions: []*models.PullSubmission{
2422
&initialSubmission,
2423
},
···
1
package pulls
2
3
import (
4
+
"context"
5
"database/sql"
6
"encoding/json"
7
"errors"
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
"tangled.org/core/appview/pages/markup"
28
+
"tangled.org/core/appview/pages/repoinfo"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/appview/xrpcclient"
32
"tangled.org/core/idresolver"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/patchutil"
35
"tangled.org/core/rbac"
36
"tangled.org/core/tid"
···
45
)
46
47
type Pulls struct {
48
+
oauth *oauth.OAuth
49
+
repoResolver *reporesolver.RepoResolver
50
+
pages *pages.Pages
51
+
idResolver *idresolver.Resolver
52
+
mentionsResolver *mentions.Resolver
53
+
db *db.DB
54
+
config *config.Config
55
+
notifier notify.Notifier
56
+
enforcer *rbac.Enforcer
57
+
logger *slog.Logger
58
+
validator *validator.Validator
59
+
indexer *pulls_indexer.Indexer
60
}
61
62
func New(
···
64
repoResolver *reporesolver.RepoResolver,
65
pages *pages.Pages,
66
resolver *idresolver.Resolver,
67
+
mentionsResolver *mentions.Resolver,
68
db *db.DB,
69
config *config.Config,
70
notifier notify.Notifier,
···
74
logger *slog.Logger,
75
) *Pulls {
76
return &Pulls{
77
+
oauth: oauth,
78
+
repoResolver: repoResolver,
79
+
pages: pages,
80
+
idResolver: resolver,
81
+
mentionsResolver: mentionsResolver,
82
+
db: db,
83
+
config: config,
84
+
notifier: notifier,
85
+
enforcer: enforcer,
86
+
logger: logger,
87
+
validator: validator,
88
+
indexer: indexer,
89
}
90
}
91
···
130
131
s.pages.PullActionsFragment(w, pages.PullActionsParams{
132
LoggedInUser: user,
133
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
134
Pull: pull,
135
RoundNumber: roundNumber,
136
MergeCheck: mergeCheckResponse,
···
157
return
158
}
159
160
+
backlinks, err := db.GetBacklinks(s.db, pull.AtUri())
161
+
if err != nil {
162
+
log.Println("failed to get pull backlinks", err)
163
+
s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.")
164
+
return
165
+
}
166
+
167
// can be nil if this pull is not stacked
168
stack, _ := r.Context().Value("stack").(models.Stack)
169
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull)
···
174
if user != nil && user.Did == pull.OwnerDid {
175
resubmitResult = s.resubmitCheck(r, f, pull, stack)
176
}
177
178
m := make(map[string]models.Pipeline)
179
···
191
ps, err := db.GetPipelineStatuses(
192
s.db,
193
len(shas),
194
+
orm.FilterEq("repo_owner", f.Did),
195
+
orm.FilterEq("repo_name", f.Name),
196
+
orm.FilterEq("knot", f.Knot),
197
+
orm.FilterIn("sha", shas),
198
)
199
if err != nil {
200
log.Printf("failed to fetch pipeline statuses: %s", err)
···
218
219
labelDefs, err := db.GetLabelDefinitions(
220
s.db,
221
+
orm.FilterIn("at_uri", f.Labels),
222
+
orm.FilterContains("scope", tangled.RepoPullNSID),
223
)
224
if err != nil {
225
log.Println("failed to fetch labels", err)
···
234
235
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
236
LoggedInUser: user,
237
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
238
Pull: pull,
239
Stack: stack,
240
AbandonedPulls: abandonedPulls,
241
+
Backlinks: backlinks,
242
BranchDeleteStatus: branchDeleteStatus,
243
MergeCheck: mergeCheckResponse,
244
ResubmitCheck: resubmitResult,
···
252
})
253
}
254
255
+
func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
256
if pull.State == models.PullMerged {
257
return types.MergeCheckResponse{}
258
}
···
281
r.Context(),
282
&xrpcc,
283
&tangled.RepoMergeCheck_Input{
284
+
Did: f.Did,
285
Name: f.Name,
286
Branch: pull.TargetBranch,
287
Patch: patch,
···
319
return result
320
}
321
322
+
func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *models.BranchDeleteStatus {
323
if pull.State != models.PullMerged {
324
return nil
325
}
···
330
}
331
332
var branch string
333
// check if the branch exists
334
// NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates
335
if pull.IsBranchBased() {
336
branch = pull.PullSource.Branch
337
} else if pull.IsForkBased() {
338
branch = pull.PullSource.Branch
339
repo = pull.PullSource.Repo
···
372
}
373
}
374
375
+
func (s *Pulls) resubmitCheck(r *http.Request, repo *models.Repo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
376
if pull.State == models.PullMerged || pull.State == models.PullDeleted || pull.PullSource == nil {
377
return pages.Unknown
378
}
···
392
repoName = sourceRepo.Name
393
} else {
394
// pulls within the same repo
395
+
knot = repo.Knot
396
+
ownerDid = repo.Did
397
+
repoName = repo.Name
398
}
399
400
scheme := "http"
···
406
Host: host,
407
}
408
409
+
didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName)
410
+
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName)
411
if err != nil {
412
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
413
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
435
436
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
437
user := s.oauth.GetUser(r)
438
439
var diffOpts types.DiffOpts
440
if d := r.URL.Query().Get("diff"); d == "split" {
···
463
464
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
465
LoggedInUser: user,
466
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
467
Pull: pull,
468
Stack: stack,
469
Round: roundIdInt,
···
477
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
478
user := s.oauth.GetUser(r)
479
480
var diffOpts types.DiffOpts
481
if d := r.URL.Query().Get("diff"); d == "split" {
482
diffOpts.Split = true
···
521
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
LoggedInUser: s.oauth.GetUser(r),
524
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
525
Pull: pull,
526
Round: roundIdInt,
527
Interdiff: interdiff,
···
598
599
pulls, err := db.GetPulls(
600
s.db,
601
+
orm.FilterIn("id", ids),
602
)
603
if err != nil {
604
log.Println("failed to get pulls", err)
···
646
}
647
pulls = pulls[:n]
648
649
ps, err := db.GetPipelineStatuses(
650
s.db,
651
len(shas),
652
+
orm.FilterEq("repo_owner", f.Did),
653
+
orm.FilterEq("repo_name", f.Name),
654
+
orm.FilterEq("knot", f.Knot),
655
+
orm.FilterIn("sha", shas),
656
)
657
if err != nil {
658
log.Printf("failed to fetch pipeline statuses: %s", err)
···
665
666
labelDefs, err := db.GetLabelDefinitions(
667
s.db,
668
+
orm.FilterIn("at_uri", f.Labels),
669
+
orm.FilterContains("scope", tangled.RepoPullNSID),
670
)
671
if err != nil {
672
log.Println("failed to fetch labels", err)
···
681
682
s.pages.RepoPulls(w, pages.RepoPullsParams{
683
LoggedInUser: s.oauth.GetUser(r),
684
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
685
Pulls: pulls,
686
LabelDefs: defs,
687
FilteringBy: state,
···
692
}
693
694
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
695
user := s.oauth.GetUser(r)
696
f, err := s.repoResolver.Resolve(r)
697
if err != nil {
···
718
case http.MethodGet:
719
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
720
LoggedInUser: user,
721
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
722
Pull: pull,
723
RoundNumber: roundNumber,
724
})
···
730
return
731
}
732
733
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
734
+
735
// Start a transaction
736
tx, err := s.db.BeginTx(r.Context(), nil)
737
if err != nil {
···
774
Body: body,
775
CommentAt: atResp.Uri,
776
SubmissionId: pull.Submissions[roundNumber].ID,
777
+
Mentions: mentions,
778
+
References: references,
779
}
780
781
// Create the pull comment in the database with the commentAt field
···
793
return
794
}
795
796
s.notifier.NewPullComment(r.Context(), comment, mentions)
797
798
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
799
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId))
800
return
801
}
802
}
···
820
Host: host,
821
}
822
823
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
824
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
825
if err != nil {
826
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
847
848
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
849
LoggedInUser: user,
850
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
851
Branches: result.Branches,
852
Strategy: strategy,
853
SourceBranch: sourceBranch,
···
870
}
871
872
// Determine PR type based on input parameters
873
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
874
+
isPushAllowed := roles.IsPushAllowed()
875
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
876
isForkBased := fromFork != "" && sourceBranch != ""
877
isPatchBased := patch != "" && !isBranchBased && !isForkBased
···
969
func (s *Pulls) handleBranchBasedPull(
970
w http.ResponseWriter,
971
r *http.Request,
972
+
repo *models.Repo,
973
user *oauth.User,
974
title,
975
body,
···
981
if !s.config.Core.Dev {
982
scheme = "https"
983
}
984
+
host := fmt.Sprintf("%s://%s", scheme, repo.Knot)
985
xrpcc := &indigoxrpc.Client{
986
Host: host,
987
}
988
989
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
990
+
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, didSlashRepo, targetBranch, sourceBranch)
991
if err != nil {
992
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
993
log.Println("failed to call XRPC repo.compare", xrpcerr)
···
1024
Sha: comparison.Rev2,
1025
}
1026
1027
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1028
}
1029
1030
+
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
1031
if err := s.validator.ValidatePatch(&patch); err != nil {
1032
s.logger.Error("patch validation failed", "err", err)
1033
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
1034
return
1035
}
1036
1037
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1038
}
1039
1040
+
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1041
repoString := strings.SplitN(forkRepo, "/", 2)
1042
forkOwnerDid := repoString[0]
1043
repoName := repoString[1]
···
1139
Sha: sourceRev,
1140
}
1141
1142
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1143
}
1144
1145
func (s *Pulls) createPullRequest(
1146
w http.ResponseWriter,
1147
r *http.Request,
1148
+
repo *models.Repo,
1149
user *oauth.User,
1150
title, body, targetBranch string,
1151
patch string,
···
1160
s.createStackedPullRequest(
1161
w,
1162
r,
1163
+
repo,
1164
user,
1165
targetBranch,
1166
patch,
···
1206
}
1207
}
1208
1209
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
1210
+
1211
rkey := tid.TID()
1212
initialSubmission := models.PullSubmission{
1213
Patch: patch,
···
1219
Body: body,
1220
TargetBranch: targetBranch,
1221
OwnerDid: user.Did,
1222
+
RepoAt: repo.RepoAt(),
1223
Rkey: rkey,
1224
+
Mentions: mentions,
1225
+
References: references,
1226
Submissions: []*models.PullSubmission{
1227
&initialSubmission,
1228
},
···
1234
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1235
return
1236
}
1237
+
pullId, err := db.NextPullId(tx, repo.RepoAt())
1238
if err != nil {
1239
log.Println("failed to get pull id", err)
1240
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1249
Val: &tangled.RepoPull{
1250
Title: title,
1251
Target: &tangled.RepoPull_Target{
1252
+
Repo: string(repo.RepoAt()),
1253
Branch: targetBranch,
1254
},
1255
Patch: patch,
···
1272
1273
s.notifier.NewPull(r.Context(), pull)
1274
1275
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1276
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId))
1277
}
1278
1279
func (s *Pulls) createStackedPullRequest(
1280
w http.ResponseWriter,
1281
r *http.Request,
1282
+
repo *models.Repo,
1283
user *oauth.User,
1284
targetBranch string,
1285
patch string,
···
1311
1312
// build a stack out of this patch
1313
stackId := uuid.New()
1314
+
stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String())
1315
if err != nil {
1316
log.Println("failed to create stack", err)
1317
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
···
1374
return
1375
}
1376
1377
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1378
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo))
1379
}
1380
1381
func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) {
···
1406
1407
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1408
user := s.oauth.GetUser(r)
1409
1410
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1411
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1412
})
1413
}
1414
···
1429
Host: host,
1430
}
1431
1432
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1433
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1434
if err != nil {
1435
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1462
}
1463
1464
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1465
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1466
Branches: withoutDefault,
1467
})
1468
}
1469
1470
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1471
user := s.oauth.GetUser(r)
1472
1473
forks, err := db.GetForksByDid(s.db, user.Did)
1474
if err != nil {
···
1477
}
1478
1479
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1480
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1481
Forks: forks,
1482
Selected: r.URL.Query().Get("fork"),
1483
})
···
1499
// fork repo
1500
repo, err := db.GetRepo(
1501
s.db,
1502
+
orm.FilterEq("did", forkOwnerDid),
1503
+
orm.FilterEq("name", forkName),
1504
)
1505
if err != nil {
1506
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
1545
Host: targetHost,
1546
}
1547
1548
+
targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1549
targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo)
1550
if err != nil {
1551
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1570
})
1571
1572
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1573
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1574
SourceBranches: sourceBranches.Branches,
1575
TargetBranches: targetBranches.Branches,
1576
})
···
1578
1579
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1580
user := s.oauth.GetUser(r)
1581
1582
pull, ok := r.Context().Value("pull").(*models.Pull)
1583
if !ok {
···
1589
switch r.Method {
1590
case http.MethodGet:
1591
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1592
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1593
Pull: pull,
1594
})
1595
return
···
1656
return
1657
}
1658
1659
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
1660
+
if !roles.IsPushAllowed() {
1661
log.Println("unauthorized user")
1662
w.WriteHeader(http.StatusUnauthorized)
1663
return
···
1672
Host: host,
1673
}
1674
1675
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1676
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch)
1677
if err != nil {
1678
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1799
func (s *Pulls) resubmitPullHelper(
1800
w http.ResponseWriter,
1801
r *http.Request,
1802
+
repo *models.Repo,
1803
user *oauth.User,
1804
pull *models.Pull,
1805
patch string,
···
1808
) {
1809
if pull.IsStacked() {
1810
log.Println("resubmitting stacked PR")
1811
+
s.resubmitStackedPullHelper(w, r, repo, user, pull, patch, pull.StackId)
1812
return
1813
}
1814
···
1888
Val: &tangled.RepoPull{
1889
Title: pull.Title,
1890
Target: &tangled.RepoPull_Target{
1891
+
Repo: string(repo.RepoAt()),
1892
Branch: pull.TargetBranch,
1893
},
1894
Patch: patch, // new patch
···
1909
return
1910
}
1911
1912
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1913
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
1914
}
1915
1916
func (s *Pulls) resubmitStackedPullHelper(
1917
w http.ResponseWriter,
1918
r *http.Request,
1919
+
repo *models.Repo,
1920
user *oauth.User,
1921
pull *models.Pull,
1922
patch string,
···
1925
targetBranch := pull.TargetBranch
1926
1927
origStack, _ := r.Context().Value("stack").(models.Stack)
1928
+
newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId)
1929
if err != nil {
1930
log.Println("failed to create resubmitted stack", err)
1931
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
2067
tx,
2068
p.ParentChangeId,
2069
// these should be enough filters to be unique per-stack
2070
+
orm.FilterEq("repo_at", p.RepoAt.String()),
2071
+
orm.FilterEq("owner_did", p.OwnerDid),
2072
+
orm.FilterEq("change_id", p.ChangeId),
2073
)
2074
2075
if err != nil {
···
2103
return
2104
}
2105
2106
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
2107
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2108
}
2109
2110
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
···
2157
2158
authorName := ident.Handle.String()
2159
mergeInput := &tangled.RepoMerge_Input{
2160
+
Did: f.Did,
2161
Name: f.Name,
2162
Branch: pull.TargetBranch,
2163
Patch: patch,
···
2222
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2223
}
2224
2225
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2226
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2227
}
2228
2229
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
···
2243
}
2244
2245
// auth filter: only owner or collaborators can close
2246
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2247
isOwner := roles.IsOwner()
2248
isCollaborator := roles.IsCollaborator()
2249
isPullAuthor := user.Did == pull.OwnerDid
···
2295
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2296
}
2297
2298
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2299
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2300
}
2301
2302
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
···
2317
}
2318
2319
// auth filter: only owner or collaborators can close
2320
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2321
isOwner := roles.IsOwner()
2322
isCollaborator := roles.IsCollaborator()
2323
isPullAuthor := user.Did == pull.OwnerDid
···
2369
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2370
}
2371
2372
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2373
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2374
}
2375
2376
+
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2377
formatPatches, err := patchutil.ExtractPatches(patch)
2378
if err != nil {
2379
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2398
body := fp.Body
2399
rkey := tid.TID()
2400
2401
+
mentions, references := s.mentionsResolver.Resolve(ctx, body)
2402
+
2403
initialSubmission := models.PullSubmission{
2404
Patch: fp.Raw,
2405
SourceRev: fp.SHA,
···
2410
Body: body,
2411
TargetBranch: targetBranch,
2412
OwnerDid: user.Did,
2413
+
RepoAt: repo.RepoAt(),
2414
Rkey: rkey,
2415
+
Mentions: mentions,
2416
+
References: references,
2417
Submissions: []*models.PullSubmission{
2418
&initialSubmission,
2419
},
+2
-2
appview/repo/archive.go
+2
-2
appview/repo/archive.go
···
31
xrpcc := &indigoxrpc.Client{
32
Host: host,
33
}
34
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
35
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
36
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
rp.pages.Error503(w)
···
31
xrpcc := &indigoxrpc.Client{
32
Host: host,
33
}
34
+
didSlashRepo := f.DidSlashRepo()
35
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo)
36
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
rp.pages.Error503(w)
+21
-14
appview/repo/artifact.go
+21
-14
appview/repo/artifact.go
···
14
"tangled.org/core/appview/db"
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
-
"tangled.org/core/appview/reporesolver"
18
"tangled.org/core/appview/xrpcclient"
19
"tangled.org/core/tid"
20
"tangled.org/core/types"
21
···
131
132
rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
133
LoggedInUser: user,
134
-
RepoInfo: f.RepoInfo(user),
135
Artifact: artifact,
136
})
137
}
···
156
157
artifacts, err := db.GetArtifact(
158
rp.db,
159
-
db.FilterEq("repo_at", f.RepoAt()),
160
-
db.FilterEq("tag", tag.Tag.Hash[:]),
161
-
db.FilterEq("name", filename),
162
)
163
if err != nil {
164
log.Println("failed to get artifacts", err)
···
174
175
artifact := artifacts[0]
176
177
-
ownerPds := f.OwnerId.PDSEndpoint()
178
url, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob", ownerPds))
179
q := url.Query()
180
q.Set("cid", artifact.BlobCid.String())
···
228
229
artifacts, err := db.GetArtifact(
230
rp.db,
231
-
db.FilterEq("repo_at", f.RepoAt()),
232
-
db.FilterEq("tag", tag[:]),
233
-
db.FilterEq("name", filename),
234
)
235
if err != nil {
236
log.Println("failed to get artifacts", err)
···
270
defer tx.Rollback()
271
272
err = db.DeleteArtifact(tx,
273
-
db.FilterEq("repo_at", f.RepoAt()),
274
-
db.FilterEq("tag", artifact.Tag[:]),
275
-
db.FilterEq("name", filename),
276
)
277
if err != nil {
278
log.Println("failed to remove artifact record from db", err)
···
290
w.Write([]byte{})
291
}
292
293
-
func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
294
tagParam, err := url.QueryUnescape(tagParam)
295
if err != nil {
296
return nil, err
···
305
Host: host,
306
}
307
308
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
309
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
310
if err != nil {
311
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
14
"tangled.org/core/appview/db"
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
"tangled.org/core/appview/xrpcclient"
18
+
"tangled.org/core/orm"
19
"tangled.org/core/tid"
20
"tangled.org/core/types"
21
···
131
132
rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
133
LoggedInUser: user,
134
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
135
Artifact: artifact,
136
})
137
}
···
156
157
artifacts, err := db.GetArtifact(
158
rp.db,
159
+
orm.FilterEq("repo_at", f.RepoAt()),
160
+
orm.FilterEq("tag", tag.Tag.Hash[:]),
161
+
orm.FilterEq("name", filename),
162
)
163
if err != nil {
164
log.Println("failed to get artifacts", err)
···
174
175
artifact := artifacts[0]
176
177
+
ownerId, err := rp.idResolver.ResolveIdent(r.Context(), f.Did)
178
+
if err != nil {
179
+
log.Println("failed to resolve repo owner did", f.Did, err)
180
+
http.Error(w, "repository owner not found", http.StatusNotFound)
181
+
return
182
+
}
183
+
184
+
ownerPds := ownerId.PDSEndpoint()
185
url, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob", ownerPds))
186
q := url.Query()
187
q.Set("cid", artifact.BlobCid.String())
···
235
236
artifacts, err := db.GetArtifact(
237
rp.db,
238
+
orm.FilterEq("repo_at", f.RepoAt()),
239
+
orm.FilterEq("tag", tag[:]),
240
+
orm.FilterEq("name", filename),
241
)
242
if err != nil {
243
log.Println("failed to get artifacts", err)
···
277
defer tx.Rollback()
278
279
err = db.DeleteArtifact(tx,
280
+
orm.FilterEq("repo_at", f.RepoAt()),
281
+
orm.FilterEq("tag", artifact.Tag[:]),
282
+
orm.FilterEq("name", filename),
283
)
284
if err != nil {
285
log.Println("failed to remove artifact record from db", err)
···
297
w.Write([]byte{})
298
}
299
300
+
func (rp *Repo) resolveTag(ctx context.Context, f *models.Repo, tagParam string) (*types.TagReference, error) {
301
tagParam, err := url.QueryUnescape(tagParam)
302
if err != nil {
303
return nil, err
···
312
Host: host,
313
}
314
315
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
316
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
317
if err != nil {
318
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+11
-9
appview/repo/blob.go
+11
-9
appview/repo/blob.go
···
54
xrpcc := &indigoxrpc.Client{
55
Host: host,
56
}
57
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
58
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
59
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
60
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
···
62
return
63
}
64
65
// Use XRPC response directly instead of converting to internal types
66
var breadcrumbs [][]string
67
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
68
if filePath != "" {
69
for idx, elem := range strings.Split(filePath, "/") {
70
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
78
79
rp.pages.RepoBlob(w, pages.RepoBlobParams{
80
LoggedInUser: user,
81
-
RepoInfo: f.RepoInfo(user),
82
BreadCrumbs: breadcrumbs,
83
BlobView: blobView,
84
RepoBlob_Output: resp,
···
105
if !rp.config.Core.Dev {
106
scheme = "https"
107
}
108
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
109
baseURL := &url.URL{
110
Scheme: scheme,
111
Host: f.Knot,
···
176
}
177
178
// NewBlobView creates a BlobView from the XRPC response
179
-
func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string, queryParams url.Values) models.BlobView {
180
view := models.BlobView{
181
Contents: "",
182
Lines: 0,
···
198
199
// Determine if binary
200
if resp.IsBinary != nil && *resp.IsBinary {
201
-
view.ContentSrc = generateBlobURL(config, f, ref, filePath)
202
ext := strings.ToLower(filepath.Ext(resp.Path))
203
204
switch ext {
···
250
return view
251
}
252
253
-
func generateBlobURL(config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string) string {
254
scheme := "http"
255
if !config.Core.Dev {
256
scheme = "https"
257
}
258
259
-
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
260
baseURL := &url.URL{
261
Scheme: scheme,
262
-
Host: f.Knot,
263
Path: "/xrpc/sh.tangled.repo.blob",
264
}
265
query := baseURL.Query()
···
54
xrpcc := &indigoxrpc.Client{
55
Host: host,
56
}
57
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
58
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
59
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
60
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
···
62
return
63
}
64
65
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
66
+
67
// Use XRPC response directly instead of converting to internal types
68
var breadcrumbs [][]string
69
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
70
if filePath != "" {
71
for idx, elem := range strings.Split(filePath, "/") {
72
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
80
81
rp.pages.RepoBlob(w, pages.RepoBlobParams{
82
LoggedInUser: user,
83
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
84
BreadCrumbs: breadcrumbs,
85
BlobView: blobView,
86
RepoBlob_Output: resp,
···
107
if !rp.config.Core.Dev {
108
scheme = "https"
109
}
110
+
repo := f.DidSlashRepo()
111
baseURL := &url.URL{
112
Scheme: scheme,
113
Host: f.Knot,
···
178
}
179
180
// NewBlobView creates a BlobView from the XRPC response
181
+
func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, repo *models.Repo, ref, filePath string, queryParams url.Values) models.BlobView {
182
view := models.BlobView{
183
Contents: "",
184
Lines: 0,
···
200
201
// Determine if binary
202
if resp.IsBinary != nil && *resp.IsBinary {
203
+
view.ContentSrc = generateBlobURL(config, repo, ref, filePath)
204
ext := strings.ToLower(filepath.Ext(resp.Path))
205
206
switch ext {
···
252
return view
253
}
254
255
+
func generateBlobURL(config *config.Config, repo *models.Repo, ref, filePath string) string {
256
scheme := "http"
257
if !config.Core.Dev {
258
scheme = "https"
259
}
260
261
+
repoName := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
262
baseURL := &url.URL{
263
Scheme: scheme,
264
+
Host: repo.Knot,
265
Path: "/xrpc/sh.tangled.repo.blob",
266
}
267
query := baseURL.Query()
+2
-2
appview/repo/branches.go
+2
-2
appview/repo/branches.go
···
29
xrpcc := &indigoxrpc.Client{
30
Host: host,
31
}
32
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
33
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
34
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
35
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
46
user := rp.oauth.GetUser(r)
47
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
48
LoggedInUser: user,
49
-
RepoInfo: f.RepoInfo(user),
50
RepoBranchesResponse: result,
51
})
52
}
···
29
xrpcc := &indigoxrpc.Client{
30
Host: host,
31
}
32
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
33
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
34
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
35
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
46
user := rp.oauth.GetUser(r)
47
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
48
LoggedInUser: user,
49
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
50
RepoBranchesResponse: result,
51
})
52
}
+4
-8
appview/repo/compare.go
+4
-8
appview/repo/compare.go
···
36
Host: host,
37
}
38
39
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
40
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
41
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
88
return
89
}
90
91
-
repoinfo := f.RepoInfo(user)
92
-
93
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
94
LoggedInUser: user,
95
-
RepoInfo: repoinfo,
96
Branches: branches,
97
Tags: tags.Tags,
98
Base: base,
···
151
Host: host,
152
}
153
154
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
155
156
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
157
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
202
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
203
}
204
205
-
repoinfo := f.RepoInfo(user)
206
-
207
rp.pages.RepoCompare(w, pages.RepoCompareParams{
208
LoggedInUser: user,
209
-
RepoInfo: repoinfo,
210
Branches: branches.Branches,
211
Tags: tags.Tags,
212
Base: base,
···
36
Host: host,
37
}
38
39
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
40
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
41
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
88
return
89
}
90
91
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
92
LoggedInUser: user,
93
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
94
Branches: branches,
95
Tags: tags.Tags,
96
Base: base,
···
149
Host: host,
150
}
151
152
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
153
154
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
155
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
200
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
201
}
202
203
rp.pages.RepoCompare(w, pages.RepoCompareParams{
204
LoggedInUser: user,
205
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
206
Branches: branches.Branches,
207
Tags: tags.Tags,
208
Base: base,
+24
-17
appview/repo/feed.go
+24
-17
appview/repo/feed.go
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
-
"tangled.org/core/appview/reporesolver"
15
16
"github.com/bluesky-social/indigo/atproto/syntax"
17
"github.com/gorilla/feeds"
18
)
19
20
-
func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) {
21
const feedLimitPerType = 100
22
23
-
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt()))
24
if err != nil {
25
return nil, err
26
}
···
28
issues, err := db.GetIssuesPaginated(
29
rp.db,
30
pagination.Page{Limit: feedLimitPerType},
31
-
db.FilterEq("repo_at", f.RepoAt()),
32
)
33
if err != nil {
34
return nil, err
35
}
36
37
feed := &feeds.Feed{
38
-
Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()),
39
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"},
40
Items: make([]*feeds.Item, 0),
41
Updated: time.UnixMilli(0),
42
}
43
44
for _, pull := range pulls {
45
-
items, err := rp.createPullItems(ctx, pull, f)
46
if err != nil {
47
return nil, err
48
}
···
50
}
51
52
for _, issue := range issues {
53
-
item, err := rp.createIssueItem(ctx, issue, f)
54
if err != nil {
55
return nil, err
56
}
···
71
return feed, nil
72
}
73
74
-
func (rp *Repo) createPullItems(ctx context.Context, pull *models.Pull, f *reporesolver.ResolvedRepo) ([]*feeds.Item, error) {
75
owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid)
76
if err != nil {
77
return nil, err
···
80
var items []*feeds.Item
81
82
state := rp.getPullState(pull)
83
-
description := rp.buildPullDescription(owner.Handle, state, pull, f.OwnerSlashRepo())
84
85
mainItem := &feeds.Item{
86
Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title),
87
Description: description,
88
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)},
89
Created: pull.Created,
90
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
91
}
···
98
99
roundItem := &feeds.Item{
100
Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber),
101
-
Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()),
102
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)},
103
Created: round.Created,
104
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
105
}
···
109
return items, nil
110
}
111
112
-
func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) {
113
owner, err := rp.idResolver.ResolveIdent(ctx, issue.Did)
114
if err != nil {
115
return nil, err
···
122
123
return &feeds.Item{
124
Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title),
125
-
Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()),
126
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)},
127
Created: issue.Created,
128
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
129
}, nil
···
152
log.Println("failed to fully resolve repo:", err)
153
return
154
}
155
156
-
feed, err := rp.getRepoFeed(r.Context(), f)
157
if err != nil {
158
log.Println("failed to get repo feed:", err)
159
rp.pages.Error500(w)
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
16
+
"github.com/bluesky-social/indigo/atproto/identity"
17
"github.com/bluesky-social/indigo/atproto/syntax"
18
"github.com/gorilla/feeds"
19
)
20
21
+
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
22
const feedLimitPerType = 100
23
24
+
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt()))
25
if err != nil {
26
return nil, err
27
}
···
29
issues, err := db.GetIssuesPaginated(
30
rp.db,
31
pagination.Page{Limit: feedLimitPerType},
32
+
orm.FilterEq("repo_at", repo.RepoAt()),
33
)
34
if err != nil {
35
return nil, err
36
}
37
38
feed := &feeds.Feed{
39
+
Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo),
40
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, ownerSlashRepo), Type: "text/html", Rel: "alternate"},
41
Items: make([]*feeds.Item, 0),
42
Updated: time.UnixMilli(0),
43
}
44
45
for _, pull := range pulls {
46
+
items, err := rp.createPullItems(ctx, pull, repo, ownerSlashRepo)
47
if err != nil {
48
return nil, err
49
}
···
51
}
52
53
for _, issue := range issues {
54
+
item, err := rp.createIssueItem(ctx, issue, repo, ownerSlashRepo)
55
if err != nil {
56
return nil, err
57
}
···
72
return feed, nil
73
}
74
75
+
func (rp *Repo) createPullItems(ctx context.Context, pull *models.Pull, repo *models.Repo, ownerSlashRepo string) ([]*feeds.Item, error) {
76
owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid)
77
if err != nil {
78
return nil, err
···
81
var items []*feeds.Item
82
83
state := rp.getPullState(pull)
84
+
description := rp.buildPullDescription(owner.Handle, state, pull, ownerSlashRepo)
85
86
mainItem := &feeds.Item{
87
Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title),
88
Description: description,
89
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId)},
90
Created: pull.Created,
91
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
92
}
···
99
100
roundItem := &feeds.Item{
101
Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber),
102
+
Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo),
103
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId, round.RoundNumber)},
104
Created: round.Created,
105
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
106
}
···
110
return items, nil
111
}
112
113
+
func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, repo *models.Repo, ownerSlashRepo string) (*feeds.Item, error) {
114
owner, err := rp.idResolver.ResolveIdent(ctx, issue.Did)
115
if err != nil {
116
return nil, err
···
123
124
return &feeds.Item{
125
Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title),
126
+
Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo),
127
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, ownerSlashRepo, issue.IssueId)},
128
Created: issue.Created,
129
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
130
}, nil
···
153
log.Println("failed to fully resolve repo:", err)
154
return
155
}
156
+
repoOwnerId, ok := r.Context().Value("resolvedId").(identity.Identity)
157
+
if !ok || repoOwnerId.Handle.IsInvalidHandle() {
158
+
log.Println("failed to get resolved repo owner id")
159
+
return
160
+
}
161
+
ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Name
162
163
+
feed, err := rp.getRepoFeed(r.Context(), f, ownerSlashRepo)
164
if err != nil {
165
log.Println("failed to get repo feed:", err)
166
rp.pages.Error500(w)
+18
-19
appview/repo/index.go
+18
-19
appview/repo/index.go
···
22
"tangled.org/core/appview/db"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
-
"tangled.org/core/appview/reporesolver"
26
"tangled.org/core/appview/xrpcclient"
27
"tangled.org/core/types"
28
29
"github.com/go-chi/chi/v5"
···
52
}
53
54
user := rp.oauth.GetUser(r)
55
-
repoInfo := f.RepoInfo(user)
56
57
// Build index response from multiple XRPC calls
58
result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
···
62
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
63
LoggedInUser: user,
64
NeedsKnotUpgrade: true,
65
-
RepoInfo: repoInfo,
66
})
67
return
68
}
···
124
l.Error("failed to get email to did map", "err", err)
125
}
126
127
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
128
if err != nil {
129
l.Error("failed to GetVerifiedObjectCommits", "err", err)
130
}
···
140
for _, c := range commitsTrunc {
141
shas = append(shas, c.Hash.String())
142
}
143
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
144
if err != nil {
145
l.Error("failed to fetch pipeline statuses", "err", err)
146
// non-fatal
···
148
149
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
150
LoggedInUser: user,
151
-
RepoInfo: repoInfo,
152
TagMap: tagMap,
153
RepoIndexResponse: *result,
154
CommitsTrunc: commitsTrunc,
···
165
func (rp *Repo) getLanguageInfo(
166
ctx context.Context,
167
l *slog.Logger,
168
-
f *reporesolver.ResolvedRepo,
169
xrpcc *indigoxrpc.Client,
170
currentRef string,
171
isDefaultRef bool,
···
173
// first attempt to fetch from db
174
langs, err := db.GetRepoLanguages(
175
rp.db,
176
-
db.FilterEq("repo_at", f.RepoAt()),
177
-
db.FilterEq("ref", currentRef),
178
)
179
180
if err != nil || langs == nil {
181
// non-fatal, fetch langs from ks via XRPC
182
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
183
-
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo)
184
if err != nil {
185
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
186
l.Error("failed to call XRPC repo.languages", "err", xrpcerr)
···
195
196
for _, lang := range ls.Languages {
197
langs = append(langs, models.RepoLanguage{
198
-
RepoAt: f.RepoAt(),
199
Ref: currentRef,
200
IsDefaultRef: isDefaultRef,
201
Language: lang.Name,
···
210
defer tx.Rollback()
211
212
// update appview's cache
213
-
err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs)
214
if err != nil {
215
// non-fatal
216
l.Error("failed to cache lang results", "err", err)
···
255
}
256
257
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
258
-
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) {
259
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
260
261
// first get branches to determine the ref if not specified
262
-
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
263
if err != nil {
264
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
265
}
···
303
wg.Add(1)
304
go func() {
305
defer wg.Done()
306
-
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
307
if err != nil {
308
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
309
return
···
318
wg.Add(1)
319
go func() {
320
defer wg.Done()
321
-
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
322
if err != nil {
323
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
324
return
···
330
wg.Add(1)
331
go func() {
332
defer wg.Done()
333
-
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
334
if err != nil {
335
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
336
return
···
22
"tangled.org/core/appview/db"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/xrpcclient"
26
+
"tangled.org/core/orm"
27
"tangled.org/core/types"
28
29
"github.com/go-chi/chi/v5"
···
52
}
53
54
user := rp.oauth.GetUser(r)
55
56
// Build index response from multiple XRPC calls
57
result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
···
61
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
62
LoggedInUser: user,
63
NeedsKnotUpgrade: true,
64
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
65
})
66
return
67
}
···
123
l.Error("failed to get email to did map", "err", err)
124
}
125
126
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc)
127
if err != nil {
128
l.Error("failed to GetVerifiedObjectCommits", "err", err)
129
}
···
139
for _, c := range commitsTrunc {
140
shas = append(shas, c.Hash.String())
141
}
142
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
143
if err != nil {
144
l.Error("failed to fetch pipeline statuses", "err", err)
145
// non-fatal
···
147
148
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
149
LoggedInUser: user,
150
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
151
TagMap: tagMap,
152
RepoIndexResponse: *result,
153
CommitsTrunc: commitsTrunc,
···
164
func (rp *Repo) getLanguageInfo(
165
ctx context.Context,
166
l *slog.Logger,
167
+
repo *models.Repo,
168
xrpcc *indigoxrpc.Client,
169
currentRef string,
170
isDefaultRef bool,
···
172
// first attempt to fetch from db
173
langs, err := db.GetRepoLanguages(
174
rp.db,
175
+
orm.FilterEq("repo_at", repo.RepoAt()),
176
+
orm.FilterEq("ref", currentRef),
177
)
178
179
if err != nil || langs == nil {
180
// non-fatal, fetch langs from ks via XRPC
181
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
182
+
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, didSlashRepo)
183
if err != nil {
184
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
185
l.Error("failed to call XRPC repo.languages", "err", xrpcerr)
···
194
195
for _, lang := range ls.Languages {
196
langs = append(langs, models.RepoLanguage{
197
+
RepoAt: repo.RepoAt(),
198
Ref: currentRef,
199
IsDefaultRef: isDefaultRef,
200
Language: lang.Name,
···
209
defer tx.Rollback()
210
211
// update appview's cache
212
+
err = db.UpdateRepoLanguages(tx, repo.RepoAt(), currentRef, langs)
213
if err != nil {
214
// non-fatal
215
l.Error("failed to cache lang results", "err", err)
···
254
}
255
256
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
257
+
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) {
258
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
259
260
// first get branches to determine the ref if not specified
261
+
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo)
262
if err != nil {
263
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
264
}
···
302
wg.Add(1)
303
go func() {
304
defer wg.Done()
305
+
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo)
306
if err != nil {
307
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
308
return
···
317
wg.Add(1)
318
go func() {
319
defer wg.Done()
320
+
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo)
321
if err != nil {
322
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
323
return
···
329
wg.Add(1)
330
go func() {
331
defer wg.Done()
332
+
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo)
333
if err != nil {
334
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
335
return
+8
-11
appview/repo/log.go
+8
-11
appview/repo/log.go
···
57
cursor = strconv.Itoa(offset)
58
}
59
60
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
61
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
62
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
63
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
···
116
l.Error("failed to fetch email to did mapping", "err", err)
117
}
118
119
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
if err != nil {
121
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
}
123
-
124
-
repoInfo := f.RepoInfo(user)
125
126
var shas []string
127
for _, c := range xrpcResp.Commits {
128
shas = append(shas, c.Hash.String())
129
}
130
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
131
if err != nil {
132
l.Error("failed to getPipelineStatuses", "err", err)
133
// non-fatal
···
136
rp.pages.RepoLog(w, pages.RepoLogParams{
137
LoggedInUser: user,
138
TagMap: tagMap,
139
-
RepoInfo: repoInfo,
140
RepoLogResponse: xrpcResp,
141
EmailToDid: emailToDidMap,
142
VerifiedCommits: vc,
···
174
Host: host,
175
}
176
177
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
178
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
179
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
180
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
···
194
l.Error("failed to get email to did mapping", "err", err)
195
}
196
197
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
198
if err != nil {
199
l.Error("failed to GetVerifiedCommits", "err", err)
200
}
201
202
user := rp.oauth.GetUser(r)
203
-
repoInfo := f.RepoInfo(user)
204
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
205
if err != nil {
206
l.Error("failed to getPipelineStatuses", "err", err)
207
// non-fatal
···
213
214
rp.pages.RepoCommit(w, pages.RepoCommitParams{
215
LoggedInUser: user,
216
-
RepoInfo: f.RepoInfo(user),
217
RepoCommitResponse: result,
218
EmailToDid: emailToDidMap,
219
VerifiedCommit: vc,
···
57
cursor = strconv.Itoa(offset)
58
}
59
60
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
61
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
62
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
63
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
···
116
l.Error("failed to fetch email to did mapping", "err", err)
117
}
118
119
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
if err != nil {
121
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
}
123
124
var shas []string
125
for _, c := range xrpcResp.Commits {
126
shas = append(shas, c.Hash.String())
127
}
128
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
129
if err != nil {
130
l.Error("failed to getPipelineStatuses", "err", err)
131
// non-fatal
···
134
rp.pages.RepoLog(w, pages.RepoLogParams{
135
LoggedInUser: user,
136
TagMap: tagMap,
137
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
138
RepoLogResponse: xrpcResp,
139
EmailToDid: emailToDidMap,
140
VerifiedCommits: vc,
···
172
Host: host,
173
}
174
175
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
176
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
177
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
178
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
···
192
l.Error("failed to get email to did mapping", "err", err)
193
}
194
195
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit})
196
if err != nil {
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
}
199
200
user := rp.oauth.GetUser(r)
201
+
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
202
if err != nil {
203
l.Error("failed to getPipelineStatuses", "err", err)
204
// non-fatal
···
210
211
rp.pages.RepoCommit(w, pages.RepoCommitParams{
212
LoggedInUser: user,
213
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
214
RepoCommitResponse: result,
215
EmailToDid: emailToDidMap,
216
VerifiedCommit: vc,
+4
-3
appview/repo/opengraph.go
+4
-3
appview/repo/opengraph.go
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
"tangled.org/core/types"
20
)
21
···
338
var languageStats []types.RepoLanguageDetails
339
langs, err := db.GetRepoLanguages(
340
rp.db,
341
-
db.FilterEq("repo_at", f.RepoAt()),
342
-
db.FilterEq("is_default_ref", 1),
343
)
344
if err != nil {
345
log.Printf("failed to get language stats from db: %v", err)
···
374
})
375
}
376
377
-
card, err := rp.drawRepoSummaryCard(&f.Repo, languageStats)
378
if err != nil {
379
log.Println("failed to draw repo summary card", err)
380
http.Error(w, "failed to draw repo summary card", http.StatusInternalServerError)
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/types"
21
)
22
···
339
var languageStats []types.RepoLanguageDetails
340
langs, err := db.GetRepoLanguages(
341
rp.db,
342
+
orm.FilterEq("repo_at", f.RepoAt()),
343
+
orm.FilterEq("is_default_ref", 1),
344
)
345
if err != nil {
346
log.Printf("failed to get language stats from db: %v", err)
···
375
})
376
}
377
378
+
card, err := rp.drawRepoSummaryCard(f, languageStats)
379
if err != nil {
380
log.Println("failed to draw repo summary card", err)
381
http.Error(w, "failed to draw repo summary card", http.StatusInternalServerError)
+35
-35
appview/repo/repo.go
+35
-35
appview/repo/repo.go
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
"tangled.org/core/rbac"
28
"tangled.org/core/tid"
29
"tangled.org/core/xrpc/serviceauth"
···
118
}
119
}
120
121
-
newRepo := f.Repo
122
newRepo.Spindle = newSpindle
123
record := newRepo.AsRecord()
124
···
257
l.Info("wrote label record to PDS")
258
259
// update the repo to subscribe to this label
260
-
newRepo := f.Repo
261
newRepo.Labels = append(newRepo.Labels, aturi)
262
repoRecord := newRepo.AsRecord()
263
···
345
// get form values
346
labelId := r.FormValue("label-id")
347
348
-
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
349
if err != nil {
350
fail("Failed to find label definition.", err)
351
return
···
369
}
370
371
// update repo record to remove the label reference
372
-
newRepo := f.Repo
373
var updated []string
374
removedAt := label.AtUri().String()
375
for _, l := range newRepo.Labels {
···
409
410
err = db.UnsubscribeLabel(
411
tx,
412
-
db.FilterEq("repo_at", f.RepoAt()),
413
-
db.FilterEq("label_at", removedAt),
414
)
415
if err != nil {
416
fail("Failed to unsubscribe label.", err)
417
return
418
}
419
420
-
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
421
if err != nil {
422
fail("Failed to delete label definition.", err)
423
return
···
456
}
457
458
labelAts := r.Form["label"]
459
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
460
if err != nil {
461
fail("Failed to subscribe to label.", err)
462
return
463
}
464
465
-
newRepo := f.Repo
466
newRepo.Labels = append(newRepo.Labels, labelAts...)
467
468
// dedup
···
477
return
478
}
479
480
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
481
if err != nil {
482
fail("Failed to update labels, no record found on PDS.", err)
483
return
···
542
}
543
544
labelAts := r.Form["label"]
545
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
546
if err != nil {
547
fail("Failed to unsubscribe to label.", err)
548
return
549
}
550
551
// update repo record to remove the label reference
552
-
newRepo := f.Repo
553
var updated []string
554
for _, l := range newRepo.Labels {
555
if !slices.Contains(labelAts, l) {
···
565
return
566
}
567
568
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
569
if err != nil {
570
fail("Failed to update labels, no record found on PDS.", err)
571
return
···
582
583
err = db.UnsubscribeLabel(
584
rp.db,
585
-
db.FilterEq("repo_at", f.RepoAt()),
586
-
db.FilterIn("label_at", labelAts),
587
)
588
if err != nil {
589
fail("Failed to unsubscribe label.", err)
···
612
613
labelDefs, err := db.GetLabelDefinitions(
614
rp.db,
615
-
db.FilterIn("at_uri", f.Repo.Labels),
616
-
db.FilterContains("scope", subject.Collection().String()),
617
)
618
if err != nil {
619
l.Error("failed to fetch label defs", "err", err)
···
625
defs[l.AtUri().String()] = &l
626
}
627
628
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
629
if err != nil {
630
l.Error("failed to build label state", "err", err)
631
return
···
635
user := rp.oauth.GetUser(r)
636
rp.pages.LabelPanel(w, pages.LabelPanelParams{
637
LoggedInUser: user,
638
-
RepoInfo: f.RepoInfo(user),
639
Defs: defs,
640
Subject: subject.String(),
641
State: state,
···
660
661
labelDefs, err := db.GetLabelDefinitions(
662
rp.db,
663
-
db.FilterIn("at_uri", f.Repo.Labels),
664
-
db.FilterContains("scope", subject.Collection().String()),
665
)
666
if err != nil {
667
l.Error("failed to fetch labels", "err", err)
···
673
defs[l.AtUri().String()] = &l
674
}
675
676
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
677
if err != nil {
678
l.Error("failed to build label state", "err", err)
679
return
···
683
user := rp.oauth.GetUser(r)
684
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
685
LoggedInUser: user,
686
-
RepoInfo: f.RepoInfo(user),
687
Defs: defs,
688
Subject: subject.String(),
689
State: state,
···
864
r.Context(),
865
client,
866
&tangled.RepoDelete_Input{
867
-
Did: f.OwnerDid(),
868
Name: f.Name,
869
Rkey: f.Rkey,
870
},
···
902
l.Info("removed collaborators")
903
904
// remove repo RBAC
905
-
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
906
if err != nil {
907
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
908
return
909
}
910
911
// remove repo from db
912
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
913
if err != nil {
914
rp.pages.Notice(w, noticeId, "Failed to update appview")
915
return
···
930
return
931
}
932
933
-
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
934
}
935
936
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
959
return
960
}
961
962
-
repoInfo := f.RepoInfo(user)
963
-
if repoInfo.Source == nil {
964
rp.pages.Notice(w, "repo", "This repository is not a fork.")
965
return
966
}
···
971
&tangled.RepoForkSync_Input{
972
Did: user.Did,
973
Name: f.Name,
974
-
Source: repoInfo.Source.RepoAt().String(),
975
Branch: ref,
976
},
977
)
···
1007
rp.pages.ForkRepo(w, pages.ForkRepoParams{
1008
LoggedInUser: user,
1009
Knots: knots,
1010
-
RepoInfo: f.RepoInfo(user),
1011
})
1012
1013
case http.MethodPost:
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
-
db.FilterEq("did", user.Did),
1041
-
db.FilterEq("name", forkName),
1042
)
1043
if err != nil {
1044
if !errors.Is(err, sql.ErrNoRows) {
···
1058
uri = "http"
1059
}
1060
1061
-
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name)
1062
l = l.With("cloneUrl", forkSourceUrl)
1063
1064
sourceAt := f.RepoAt().String()
···
1071
Knot: targetKnot,
1072
Rkey: rkey,
1073
Source: sourceAt,
1074
-
Description: f.Repo.Description,
1075
Created: time.Now(),
1076
Labels: rp.config.Label.DefaultLabelDefs,
1077
}
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
+
"tangled.org/core/orm"
28
"tangled.org/core/rbac"
29
"tangled.org/core/tid"
30
"tangled.org/core/xrpc/serviceauth"
···
119
}
120
}
121
122
+
newRepo := *f
123
newRepo.Spindle = newSpindle
124
record := newRepo.AsRecord()
125
···
258
l.Info("wrote label record to PDS")
259
260
// update the repo to subscribe to this label
261
+
newRepo := *f
262
newRepo.Labels = append(newRepo.Labels, aturi)
263
repoRecord := newRepo.AsRecord()
264
···
346
// get form values
347
labelId := r.FormValue("label-id")
348
349
+
label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId))
350
if err != nil {
351
fail("Failed to find label definition.", err)
352
return
···
370
}
371
372
// update repo record to remove the label reference
373
+
newRepo := *f
374
var updated []string
375
removedAt := label.AtUri().String()
376
for _, l := range newRepo.Labels {
···
410
411
err = db.UnsubscribeLabel(
412
tx,
413
+
orm.FilterEq("repo_at", f.RepoAt()),
414
+
orm.FilterEq("label_at", removedAt),
415
)
416
if err != nil {
417
fail("Failed to unsubscribe label.", err)
418
return
419
}
420
421
+
err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id))
422
if err != nil {
423
fail("Failed to delete label definition.", err)
424
return
···
457
}
458
459
labelAts := r.Form["label"]
460
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
461
if err != nil {
462
fail("Failed to subscribe to label.", err)
463
return
464
}
465
466
+
newRepo := *f
467
newRepo.Labels = append(newRepo.Labels, labelAts...)
468
469
// dedup
···
478
return
479
}
480
481
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
482
if err != nil {
483
fail("Failed to update labels, no record found on PDS.", err)
484
return
···
543
}
544
545
labelAts := r.Form["label"]
546
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
547
if err != nil {
548
fail("Failed to unsubscribe to label.", err)
549
return
550
}
551
552
// update repo record to remove the label reference
553
+
newRepo := *f
554
var updated []string
555
for _, l := range newRepo.Labels {
556
if !slices.Contains(labelAts, l) {
···
566
return
567
}
568
569
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
570
if err != nil {
571
fail("Failed to update labels, no record found on PDS.", err)
572
return
···
583
584
err = db.UnsubscribeLabel(
585
rp.db,
586
+
orm.FilterEq("repo_at", f.RepoAt()),
587
+
orm.FilterIn("label_at", labelAts),
588
)
589
if err != nil {
590
fail("Failed to unsubscribe label.", err)
···
613
614
labelDefs, err := db.GetLabelDefinitions(
615
rp.db,
616
+
orm.FilterIn("at_uri", f.Labels),
617
+
orm.FilterContains("scope", subject.Collection().String()),
618
)
619
if err != nil {
620
l.Error("failed to fetch label defs", "err", err)
···
626
defs[l.AtUri().String()] = &l
627
}
628
629
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
630
if err != nil {
631
l.Error("failed to build label state", "err", err)
632
return
···
636
user := rp.oauth.GetUser(r)
637
rp.pages.LabelPanel(w, pages.LabelPanelParams{
638
LoggedInUser: user,
639
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
640
Defs: defs,
641
Subject: subject.String(),
642
State: state,
···
661
662
labelDefs, err := db.GetLabelDefinitions(
663
rp.db,
664
+
orm.FilterIn("at_uri", f.Labels),
665
+
orm.FilterContains("scope", subject.Collection().String()),
666
)
667
if err != nil {
668
l.Error("failed to fetch labels", "err", err)
···
674
defs[l.AtUri().String()] = &l
675
}
676
677
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
678
if err != nil {
679
l.Error("failed to build label state", "err", err)
680
return
···
684
user := rp.oauth.GetUser(r)
685
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
686
LoggedInUser: user,
687
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
688
Defs: defs,
689
Subject: subject.String(),
690
State: state,
···
865
r.Context(),
866
client,
867
&tangled.RepoDelete_Input{
868
+
Did: f.Did,
869
Name: f.Name,
870
Rkey: f.Rkey,
871
},
···
903
l.Info("removed collaborators")
904
905
// remove repo RBAC
906
+
err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.DidSlashRepo())
907
if err != nil {
908
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
909
return
910
}
911
912
// remove repo from db
913
+
err = db.RemoveRepo(tx, f.Did, f.Name)
914
if err != nil {
915
rp.pages.Notice(w, noticeId, "Failed to update appview")
916
return
···
931
return
932
}
933
934
+
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.Did))
935
}
936
937
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
960
return
961
}
962
963
+
if f.Source == "" {
964
rp.pages.Notice(w, "repo", "This repository is not a fork.")
965
return
966
}
···
971
&tangled.RepoForkSync_Input{
972
Did: user.Did,
973
Name: f.Name,
974
+
Source: f.Source,
975
Branch: ref,
976
},
977
)
···
1007
rp.pages.ForkRepo(w, pages.ForkRepoParams{
1008
LoggedInUser: user,
1009
Knots: knots,
1010
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
1011
})
1012
1013
case http.MethodPost:
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
+
orm.FilterEq("did", user.Did),
1041
+
orm.FilterEq("name", forkName),
1042
)
1043
if err != nil {
1044
if !errors.Is(err, sql.ErrNoRows) {
···
1058
uri = "http"
1059
}
1060
1061
+
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name)
1062
l = l.With("cloneUrl", forkSourceUrl)
1063
1064
sourceAt := f.RepoAt().String()
···
1071
Knot: targetKnot,
1072
Rkey: rkey,
1073
Source: sourceAt,
1074
+
Description: f.Description,
1075
Created: time.Now(),
1076
Labels: rp.config.Label.DefaultLabelDefs,
1077
}
+17
-19
appview/repo/repo_util.go
+17
-19
appview/repo/repo_util.go
···
1
package repo
2
3
import (
4
"slices"
5
"sort"
6
"strings"
7
8
"tangled.org/core/appview/db"
9
"tangled.org/core/appview/models"
10
-
"tangled.org/core/appview/pages/repoinfo"
11
"tangled.org/core/types"
12
-
13
-
"github.com/go-git/go-git/v5/plumbing/object"
14
)
15
16
func sortFiles(files []types.NiceTree) {
···
43
})
44
}
45
46
-
func uniqueEmails(commits []*object.Commit) []string {
47
emails := make(map[string]struct{})
48
for _, commit := range commits {
49
-
if commit.Author.Email != "" {
50
-
emails[commit.Author.Email] = struct{}{}
51
-
}
52
-
if commit.Committer.Email != "" {
53
-
emails[commit.Committer.Email] = struct{}{}
54
}
55
}
56
-
var uniqueEmails []string
57
-
for email := range emails {
58
-
uniqueEmails = append(uniqueEmails, email)
59
-
}
60
-
return uniqueEmails
61
}
62
63
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
93
// golang is so blessed that it requires 35 lines of imperative code for this
94
func getPipelineStatuses(
95
d *db.DB,
96
-
repoInfo repoinfo.RepoInfo,
97
shas []string,
98
) (map[string]models.Pipeline, error) {
99
m := make(map[string]models.Pipeline)
···
105
ps, err := db.GetPipelineStatuses(
106
d,
107
len(shas),
108
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
109
-
db.FilterEq("repo_name", repoInfo.Name),
110
-
db.FilterEq("knot", repoInfo.Knot),
111
-
db.FilterIn("sha", shas),
112
)
113
if err != nil {
114
return nil, err
···
1
package repo
2
3
import (
4
+
"maps"
5
"slices"
6
"sort"
7
"strings"
8
9
"tangled.org/core/appview/db"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
"tangled.org/core/types"
13
)
14
15
func sortFiles(files []types.NiceTree) {
···
42
})
43
}
44
45
+
func uniqueEmails(commits []types.Commit) []string {
46
emails := make(map[string]struct{})
47
for _, commit := range commits {
48
+
emails[commit.Author.Email] = struct{}{}
49
+
emails[commit.Committer.Email] = struct{}{}
50
+
for _, c := range commit.CoAuthors() {
51
+
emails[c.Email] = struct{}{}
52
}
53
}
54
+
55
+
// delete empty emails if any, from the set
56
+
delete(emails, "")
57
+
58
+
return slices.Collect(maps.Keys(emails))
59
}
60
61
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
91
// golang is so blessed that it requires 35 lines of imperative code for this
92
func getPipelineStatuses(
93
d *db.DB,
94
+
repo *models.Repo,
95
shas []string,
96
) (map[string]models.Pipeline, error) {
97
m := make(map[string]models.Pipeline)
···
103
ps, err := db.GetPipelineStatuses(
104
d,
105
len(shas),
106
+
orm.FilterEq("repo_owner", repo.Did),
107
+
orm.FilterEq("repo_name", repo.Name),
108
+
orm.FilterEq("knot", repo.Knot),
109
+
orm.FilterIn("sha", shas),
110
)
111
if err != nil {
112
return nil, err
+40
-11
appview/repo/settings.go
+40
-11
appview/repo/settings.go
···
10
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/appview/db"
13
"tangled.org/core/appview/oauth"
14
"tangled.org/core/appview/pages"
15
xrpcclient "tangled.org/core/appview/xrpcclient"
16
"tangled.org/core/types"
17
18
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
194
Host: host,
195
}
196
197
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
198
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
199
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
200
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
209
return
210
}
211
212
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
213
if err != nil {
214
l.Error("failed to fetch labels", "err", err)
215
rp.pages.Error503(w)
216
return
217
}
218
219
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
220
if err != nil {
221
l.Error("failed to fetch labels", "err", err)
222
rp.pages.Error503(w)
···
237
labels = labels[:n]
238
239
subscribedLabels := make(map[string]struct{})
240
-
for _, l := range f.Repo.Labels {
241
subscribedLabels[l] = struct{}{}
242
}
243
···
254
255
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
256
LoggedInUser: user,
257
-
RepoInfo: f.RepoInfo(user),
258
Branches: result.Branches,
259
Labels: labels,
260
DefaultLabels: defaultLabels,
···
271
f, err := rp.repoResolver.Resolve(r)
272
user := rp.oauth.GetUser(r)
273
274
-
repoCollaborators, err := f.Collaborators(r.Context())
275
if err != nil {
276
l.Error("failed to get collaborators", "err", err)
277
}
278
279
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
280
LoggedInUser: user,
281
-
RepoInfo: f.RepoInfo(user),
282
Tabs: settingsTabs,
283
Tab: "access",
284
-
Collaborators: repoCollaborators,
285
})
286
}
287
···
292
user := rp.oauth.GetUser(r)
293
294
// all spindles that the repo owner is a member of
295
-
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
296
if err != nil {
297
l.Error("failed to fetch spindles", "err", err)
298
return
···
339
340
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
341
LoggedInUser: user,
342
-
RepoInfo: f.RepoInfo(user),
343
Tabs: settingsTabs,
344
Tab: "pipelines",
345
Spindles: spindles,
···
388
}
389
l.Debug("got", "topicsStr", topicStr, "topics", topics)
390
391
-
newRepo := f.Repo
392
newRepo.Description = description
393
newRepo.Website = website
394
newRepo.Topics = topics
···
10
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/appview/db"
13
+
"tangled.org/core/appview/models"
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
+
"tangled.org/core/orm"
18
"tangled.org/core/types"
19
20
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
196
Host: host,
197
}
198
199
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
200
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
201
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
202
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
211
return
212
}
213
214
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
215
if err != nil {
216
l.Error("failed to fetch labels", "err", err)
217
rp.pages.Error503(w)
218
return
219
}
220
221
+
labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels))
222
if err != nil {
223
l.Error("failed to fetch labels", "err", err)
224
rp.pages.Error503(w)
···
239
labels = labels[:n]
240
241
subscribedLabels := make(map[string]struct{})
242
+
for _, l := range f.Labels {
243
subscribedLabels[l] = struct{}{}
244
}
245
···
256
257
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
258
LoggedInUser: user,
259
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
260
Branches: result.Branches,
261
Labels: labels,
262
DefaultLabels: defaultLabels,
···
273
f, err := rp.repoResolver.Resolve(r)
274
user := rp.oauth.GetUser(r)
275
276
+
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
277
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
278
+
if err != nil {
279
+
return nil, err
280
+
}
281
+
var collaborators []pages.Collaborator
282
+
for _, item := range repoCollaborators {
283
+
// currently only two roles: owner and member
284
+
var role string
285
+
switch item[3] {
286
+
case "repo:owner":
287
+
role = "owner"
288
+
case "repo:collaborator":
289
+
role = "collaborator"
290
+
default:
291
+
continue
292
+
}
293
+
294
+
did := item[0]
295
+
296
+
c := pages.Collaborator{
297
+
Did: did,
298
+
Role: role,
299
+
}
300
+
collaborators = append(collaborators, c)
301
+
}
302
+
return collaborators, nil
303
+
}(f)
304
if err != nil {
305
l.Error("failed to get collaborators", "err", err)
306
}
307
308
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
309
LoggedInUser: user,
310
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
311
Tabs: settingsTabs,
312
Tab: "access",
313
+
Collaborators: collaborators,
314
})
315
}
316
···
321
user := rp.oauth.GetUser(r)
322
323
// all spindles that the repo owner is a member of
324
+
spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
325
if err != nil {
326
l.Error("failed to fetch spindles", "err", err)
327
return
···
368
369
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
370
LoggedInUser: user,
371
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
372
Tabs: settingsTabs,
373
Tab: "pipelines",
374
Spindles: spindles,
···
417
}
418
l.Debug("got", "topicsStr", topicStr, "topics", topics)
419
420
+
newRepo := *f
421
newRepo.Description = description
422
newRepo.Website = website
423
newRepo.Topics = topics
+6
-4
appview/repo/tree.go
+6
-4
appview/repo/tree.go
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/appview/pages"
12
xrpcclient "tangled.org/core/appview/xrpcclient"
13
"tangled.org/core/types"
14
···
39
xrpcc := &indigoxrpc.Client{
40
Host: host,
41
}
42
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
43
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
44
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
45
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
···
79
result.ReadmeFileName = xrpcResp.Readme.Filename
80
result.Readme = xrpcResp.Readme.Contents
81
}
82
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
83
// so we can safely redirect to the "parent" (which is the same file).
84
if len(result.Files) == 0 && result.Parent == treePath {
85
-
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
86
http.Redirect(w, r, redirectTo, http.StatusFound)
87
return
88
}
89
user := rp.oauth.GetUser(r)
90
var breadcrumbs [][]string
91
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
92
if treePath != "" {
93
for idx, elem := range strings.Split(treePath, "/") {
94
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
100
LoggedInUser: user,
101
BreadCrumbs: breadcrumbs,
102
TreePath: treePath,
103
-
RepoInfo: f.RepoInfo(user),
104
RepoTreeResponse: result,
105
})
106
}
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/appview/pages"
12
+
"tangled.org/core/appview/reporesolver"
13
xrpcclient "tangled.org/core/appview/xrpcclient"
14
"tangled.org/core/types"
15
···
40
xrpcc := &indigoxrpc.Client{
41
Host: host,
42
}
43
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
44
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
45
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
46
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
···
80
result.ReadmeFileName = xrpcResp.Readme.Filename
81
result.Readme = xrpcResp.Readme.Contents
82
}
83
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
84
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
85
// so we can safely redirect to the "parent" (which is the same file).
86
if len(result.Files) == 0 && result.Parent == treePath {
87
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", ownerSlashRepo, url.PathEscape(ref), result.Parent)
88
http.Redirect(w, r, redirectTo, http.StatusFound)
89
return
90
}
91
user := rp.oauth.GetUser(r)
92
var breadcrumbs [][]string
93
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
94
if treePath != "" {
95
for idx, elem := range strings.Split(treePath, "/") {
96
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
102
LoggedInUser: user,
103
BreadCrumbs: breadcrumbs,
104
TreePath: treePath,
105
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
106
RepoTreeResponse: result,
107
})
108
}
+76
-164
appview/reporesolver/resolver.go
+76
-164
appview/reporesolver/resolver.go
···
1
package reporesolver
2
3
import (
4
-
"context"
5
-
"database/sql"
6
-
"errors"
7
"fmt"
8
"log"
9
"net/http"
···
12
"strings"
13
14
"github.com/bluesky-social/indigo/atproto/identity"
15
-
securejoin "github.com/cyphar/filepath-securejoin"
16
"github.com/go-chi/chi/v5"
17
"tangled.org/core/appview/config"
18
"tangled.org/core/appview/db"
19
"tangled.org/core/appview/models"
20
"tangled.org/core/appview/oauth"
21
-
"tangled.org/core/appview/pages"
22
"tangled.org/core/appview/pages/repoinfo"
23
-
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
)
26
27
-
type ResolvedRepo struct {
28
-
models.Repo
29
-
OwnerId identity.Identity
30
-
CurrentDir string
31
-
Ref string
32
-
33
-
rr *RepoResolver
34
}
35
36
-
type RepoResolver struct {
37
-
config *config.Config
38
-
enforcer *rbac.Enforcer
39
-
idResolver *idresolver.Resolver
40
-
execer db.Execer
41
}
42
43
-
func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Resolver, execer db.Execer) *RepoResolver {
44
-
return &RepoResolver{config: config, enforcer: enforcer, idResolver: resolver, execer: execer}
45
}
46
47
-
func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
48
repo, ok := r.Context().Value("repo").(*models.Repo)
49
if !ok {
50
log.Println("malformed middleware: `repo` not exist in context")
51
return nil, fmt.Errorf("malformed middleware")
52
}
53
-
id, ok := r.Context().Value("resolvedId").(identity.Identity)
54
-
if !ok {
55
-
log.Println("malformed middleware")
56
-
return nil, fmt.Errorf("malformed middleware")
57
-
}
58
59
-
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
60
-
ref := chi.URLParam(r, "ref")
61
-
62
-
return &ResolvedRepo{
63
-
Repo: *repo,
64
-
OwnerId: id,
65
-
CurrentDir: currentDir,
66
-
Ref: ref,
67
-
68
-
rr: rr,
69
-
}, nil
70
-
}
71
-
72
-
func (f *ResolvedRepo) OwnerDid() string {
73
-
return f.OwnerId.DID.String()
74
-
}
75
-
76
-
func (f *ResolvedRepo) OwnerHandle() string {
77
-
return f.OwnerId.Handle.String()
78
}
79
80
-
func (f *ResolvedRepo) OwnerSlashRepo() string {
81
-
handle := f.OwnerId.Handle
82
-
83
-
var p string
84
-
if handle != "" && !handle.IsInvalidHandle() {
85
-
p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name)
86
-
} else {
87
-
p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name)
88
}
89
90
-
return p
91
-
}
92
93
-
func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) {
94
-
repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
95
-
if err != nil {
96
-
return nil, err
97
}
98
99
-
var collaborators []pages.Collaborator
100
-
for _, item := range repoCollaborators {
101
-
// currently only two roles: owner and member
102
-
var role string
103
-
switch item[3] {
104
-
case "repo:owner":
105
-
role = "owner"
106
-
case "repo:collaborator":
107
-
role = "collaborator"
108
-
default:
109
-
continue
110
}
111
-
112
-
did := item[0]
113
-
114
-
c := pages.Collaborator{
115
-
Did: did,
116
-
Handle: "",
117
-
Role: role,
118
}
119
-
collaborators = append(collaborators, c)
120
-
}
121
-
122
-
// populate all collborators with handles
123
-
identsToResolve := make([]string, len(collaborators))
124
-
for i, collab := range collaborators {
125
-
identsToResolve[i] = collab.Did
126
-
}
127
-
128
-
resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve)
129
-
for i, resolved := range resolvedIdents {
130
-
if resolved != nil {
131
-
collaborators[i].Handle = resolved.Handle.String()
132
}
133
-
}
134
-
135
-
return collaborators, nil
136
-
}
137
-
138
-
// this function is a bit weird since it now returns RepoInfo from an entirely different
139
-
// package. we should refactor this or get rid of RepoInfo entirely.
140
-
func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
141
-
repoAt := f.RepoAt()
142
-
isStarred := false
143
-
if user != nil {
144
-
isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt)
145
-
}
146
-
147
-
starCount, err := db.GetStarCount(f.rr.execer, repoAt)
148
-
if err != nil {
149
-
log.Println("failed to get star count for ", repoAt)
150
-
}
151
-
issueCount, err := db.GetIssueCount(f.rr.execer, repoAt)
152
-
if err != nil {
153
-
log.Println("failed to get issue count for ", repoAt)
154
-
}
155
-
pullCount, err := db.GetPullCount(f.rr.execer, repoAt)
156
-
if err != nil {
157
-
log.Println("failed to get issue count for ", repoAt)
158
-
}
159
-
source, err := db.GetRepoSource(f.rr.execer, repoAt)
160
-
if errors.Is(err, sql.ErrNoRows) {
161
-
source = ""
162
-
} else if err != nil {
163
-
log.Println("failed to get repo source for ", repoAt, err)
164
}
165
166
var sourceRepo *models.Repo
167
-
if source != "" {
168
-
sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
169
if err != nil {
170
log.Println("failed to get repo by at uri", err)
171
}
172
}
173
174
-
var sourceHandle *identity.Identity
175
-
if sourceRepo != nil {
176
-
sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did)
177
-
if err != nil {
178
-
log.Println("failed to resolve source repo", err)
179
-
}
180
-
}
181
182
-
knot := f.Knot
183
184
-
repoInfo := repoinfo.RepoInfo{
185
-
OwnerDid: f.OwnerDid(),
186
-
OwnerHandle: f.OwnerHandle(),
187
-
Name: f.Name,
188
-
Rkey: f.Repo.Rkey,
189
-
RepoAt: repoAt,
190
-
Description: f.Description,
191
-
Website: f.Website,
192
-
Topics: f.Topics,
193
-
IsStarred: isStarred,
194
-
Knot: knot,
195
-
Spindle: f.Spindle,
196
-
Roles: f.RolesInRepo(user),
197
-
Stats: models.RepoStats{
198
-
StarCount: starCount,
199
-
IssueCount: issueCount,
200
-
PullCount: pullCount,
201
-
},
202
-
CurrentDir: f.CurrentDir,
203
-
Ref: f.Ref,
204
-
}
205
206
-
if sourceRepo != nil {
207
-
repoInfo.Source = sourceRepo
208
-
repoInfo.SourceHandle = sourceHandle.Handle.String()
209
}
210
211
return repoInfo
212
-
}
213
-
214
-
func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
215
-
if u != nil {
216
-
r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
217
-
return repoinfo.RolesInRepo{Roles: r}
218
-
} else {
219
-
return repoinfo.RolesInRepo{}
220
-
}
221
}
222
223
// extractPathAfterRef gets the actual repository path
···
1
package reporesolver
2
3
import (
4
"fmt"
5
"log"
6
"net/http"
···
9
"strings"
10
11
"github.com/bluesky-social/indigo/atproto/identity"
12
"github.com/go-chi/chi/v5"
13
"tangled.org/core/appview/config"
14
"tangled.org/core/appview/db"
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages/repoinfo"
18
"tangled.org/core/rbac"
19
)
20
21
+
type RepoResolver struct {
22
+
config *config.Config
23
+
enforcer *rbac.Enforcer
24
+
execer db.Execer
25
}
26
27
+
func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer) *RepoResolver {
28
+
return &RepoResolver{config: config, enforcer: enforcer, execer: execer}
29
}
30
31
+
// NOTE: this... should not even be here. the entire package will be removed in future refactor
32
+
func GetBaseRepoPath(r *http.Request, repo *models.Repo) string {
33
+
var (
34
+
user = chi.URLParam(r, "user")
35
+
name = chi.URLParam(r, "repo")
36
+
)
37
+
if user == "" || name == "" {
38
+
return repo.DidSlashRepo()
39
+
}
40
+
return path.Join(user, name)
41
}
42
43
+
// TODO: move this out of `RepoResolver` struct
44
+
func (rr *RepoResolver) Resolve(r *http.Request) (*models.Repo, error) {
45
repo, ok := r.Context().Value("repo").(*models.Repo)
46
if !ok {
47
log.Println("malformed middleware: `repo` not exist in context")
48
return nil, fmt.Errorf("malformed middleware")
49
}
50
51
+
return repo, nil
52
}
53
54
+
// 1. [x] replace `RepoInfo` to `reporesolver.GetRepoInfo(r *http.Request, repo, user)`
55
+
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
56
+
// 3. [x] remove `ResolvedRepo`
57
+
// 4. [ ] replace reporesolver to reposervice
58
+
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo {
59
+
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
60
+
repo, rok := r.Context().Value("repo").(*models.Repo)
61
+
if !ook || !rok {
62
+
log.Println("malformed request, failed to get repo from context")
63
}
64
65
+
// get dir/ref
66
+
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
67
+
ref := chi.URLParam(r, "ref")
68
69
+
repoAt := repo.RepoAt()
70
+
isStarred := false
71
+
roles := repoinfo.RolesInRepo{}
72
+
if user != nil {
73
+
isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt)
74
+
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo())
75
}
76
77
+
stats := repo.RepoStats
78
+
if stats == nil {
79
+
starCount, err := db.GetStarCount(rr.execer, repoAt)
80
+
if err != nil {
81
+
log.Println("failed to get star count for ", repoAt)
82
}
83
+
issueCount, err := db.GetIssueCount(rr.execer, repoAt)
84
+
if err != nil {
85
+
log.Println("failed to get issue count for ", repoAt)
86
}
87
+
pullCount, err := db.GetPullCount(rr.execer, repoAt)
88
+
if err != nil {
89
+
log.Println("failed to get pull count for ", repoAt)
90
}
91
+
stats = &models.RepoStats{
92
+
StarCount: starCount,
93
+
IssueCount: issueCount,
94
+
PullCount: pullCount,
95
+
}
96
}
97
98
var sourceRepo *models.Repo
99
+
var err error
100
+
if repo.Source != "" {
101
+
sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source)
102
if err != nil {
103
log.Println("failed to get repo by at uri", err)
104
}
105
}
106
107
+
repoInfo := repoinfo.RepoInfo{
108
+
// this is basically a models.Repo
109
+
OwnerDid: ownerId.DID.String(),
110
+
OwnerHandle: ownerId.Handle.String(),
111
+
Name: repo.Name,
112
+
Rkey: repo.Rkey,
113
+
Description: repo.Description,
114
+
Website: repo.Website,
115
+
Topics: repo.Topics,
116
+
Knot: repo.Knot,
117
+
Spindle: repo.Spindle,
118
+
Stats: *stats,
119
120
+
// fork repo upstream
121
+
Source: sourceRepo,
122
123
+
// page context
124
+
CurrentDir: currentDir,
125
+
Ref: ref,
126
127
+
// info related to the session
128
+
IsStarred: isStarred,
129
+
Roles: roles,
130
}
131
132
return repoInfo
133
}
134
135
// extractPathAfterRef gets the actual repository path
+5
-4
appview/serververify/verify.go
+5
-4
appview/serververify/verify.go
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
"tangled.org/core/rbac"
13
)
14
···
76
// mark this spindle as verified in the db
77
rowId, err := db.VerifySpindle(
78
tx,
79
-
db.FilterEq("owner", owner),
80
-
db.FilterEq("instance", instance),
81
)
82
if err != nil {
83
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
115
// mark as registered
116
err = db.MarkRegistered(
117
tx,
118
-
db.FilterEq("did", owner),
119
-
db.FilterEq("domain", domain),
120
)
121
if err != nil {
122
return fmt.Errorf("failed to register domain: %w", err)
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
+
"tangled.org/core/orm"
13
"tangled.org/core/rbac"
14
)
15
···
77
// mark this spindle as verified in the db
78
rowId, err := db.VerifySpindle(
79
tx,
80
+
orm.FilterEq("owner", owner),
81
+
orm.FilterEq("instance", instance),
82
)
83
if err != nil {
84
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
116
// mark as registered
117
err = db.MarkRegistered(
118
tx,
119
+
orm.FilterEq("did", owner),
120
+
orm.FilterEq("domain", domain),
121
)
122
if err != nil {
123
return fmt.Errorf("failed to register domain: %w", err)
+2
appview/settings/settings.go
+2
appview/settings/settings.go
+44
-26
appview/spindles/spindles.go
+44
-26
appview/spindles/spindles.go
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
"tangled.org/core/rbac"
24
"tangled.org/core/tid"
25
···
38
Logger *slog.Logger
39
}
40
41
func (s *Spindles) Router() http.Handler {
42
r := chi.NewRouter()
43
···
58
user := s.OAuth.GetUser(r)
59
all, err := db.GetSpindles(
60
s.Db,
61
-
db.FilterEq("owner", user.Did),
62
)
63
if err != nil {
64
s.Logger.Error("failed to fetch spindles", "err", err)
···
69
s.Pages.Spindles(w, pages.SpindlesParams{
70
LoggedInUser: user,
71
Spindles: all,
72
})
73
}
74
···
86
87
spindles, err := db.GetSpindles(
88
s.Db,
89
-
db.FilterEq("instance", instance),
90
-
db.FilterEq("owner", user.Did),
91
-
db.FilterIsNot("verified", "null"),
92
)
93
if err != nil || len(spindles) != 1 {
94
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
108
repos, err := db.GetRepos(
109
s.Db,
110
0,
111
-
db.FilterEq("spindle", instance),
112
)
113
if err != nil {
114
l.Error("failed to get spindle repos", "err", err)
···
127
Spindle: spindle,
128
Members: members,
129
Repos: repoMap,
130
})
131
}
132
···
273
274
spindles, err := db.GetSpindles(
275
s.Db,
276
-
db.FilterEq("owner", user.Did),
277
-
db.FilterEq("instance", instance),
278
)
279
if err != nil || len(spindles) != 1 {
280
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
302
// remove spindle members first
303
err = db.RemoveSpindleMember(
304
tx,
305
-
db.FilterEq("did", user.Did),
306
-
db.FilterEq("instance", instance),
307
)
308
if err != nil {
309
l.Error("failed to remove spindle members", "err", err)
···
313
314
err = db.DeleteSpindle(
315
tx,
316
-
db.FilterEq("owner", user.Did),
317
-
db.FilterEq("instance", instance),
318
)
319
if err != nil {
320
l.Error("failed to delete spindle", "err", err)
···
365
366
shouldRedirect := r.Header.Get("shouldRedirect")
367
if shouldRedirect == "true" {
368
-
s.Pages.HxRedirect(w, "/spindles")
369
return
370
}
371
···
393
394
spindles, err := db.GetSpindles(
395
s.Db,
396
-
db.FilterEq("owner", user.Did),
397
-
db.FilterEq("instance", instance),
398
)
399
if err != nil || len(spindles) != 1 {
400
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
436
437
verifiedSpindle, err := db.GetSpindles(
438
s.Db,
439
-
db.FilterEq("id", rowId),
440
)
441
if err != nil || len(verifiedSpindle) != 1 {
442
l.Error("failed get new spindle", "err", err)
···
469
470
spindles, err := db.GetSpindles(
471
s.Db,
472
-
db.FilterEq("owner", user.Did),
473
-
db.FilterEq("instance", instance),
474
)
475
if err != nil || len(spindles) != 1 {
476
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
581
}
582
583
// success
584
-
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
585
}
586
587
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
···
605
606
spindles, err := db.GetSpindles(
607
s.Db,
608
-
db.FilterEq("owner", user.Did),
609
-
db.FilterEq("instance", instance),
610
)
611
if err != nil || len(spindles) != 1 {
612
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
655
// get the record from the DB first:
656
members, err := db.GetSpindleMembers(
657
s.Db,
658
-
db.FilterEq("did", user.Did),
659
-
db.FilterEq("instance", instance),
660
-
db.FilterEq("subject", memberId.DID),
661
)
662
if err != nil || len(members) != 1 {
663
l.Error("failed to get member", "err", err)
···
668
// remove from db
669
if err = db.RemoveSpindleMember(
670
tx,
671
-
db.FilterEq("did", user.Did),
672
-
db.FilterEq("instance", instance),
673
-
db.FilterEq("subject", memberId.DID),
674
); err != nil {
675
l.Error("failed to remove spindle member", "err", err)
676
fail()
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
+
"tangled.org/core/orm"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
39
Logger *slog.Logger
40
}
41
42
+
type tab = map[string]any
43
+
44
+
var (
45
+
spindlesTabs []tab = []tab{
46
+
{"Name": "profile", "Icon": "user"},
47
+
{"Name": "keys", "Icon": "key"},
48
+
{"Name": "emails", "Icon": "mail"},
49
+
{"Name": "notifications", "Icon": "bell"},
50
+
{"Name": "knots", "Icon": "volleyball"},
51
+
{"Name": "spindles", "Icon": "spool"},
52
+
}
53
+
)
54
+
55
func (s *Spindles) Router() http.Handler {
56
r := chi.NewRouter()
57
···
72
user := s.OAuth.GetUser(r)
73
all, err := db.GetSpindles(
74
s.Db,
75
+
orm.FilterEq("owner", user.Did),
76
)
77
if err != nil {
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
83
s.Pages.Spindles(w, pages.SpindlesParams{
84
LoggedInUser: user,
85
Spindles: all,
86
+
Tabs: spindlesTabs,
87
+
Tab: "spindles",
88
})
89
}
90
···
102
103
spindles, err := db.GetSpindles(
104
s.Db,
105
+
orm.FilterEq("instance", instance),
106
+
orm.FilterEq("owner", user.Did),
107
+
orm.FilterIsNot("verified", "null"),
108
)
109
if err != nil || len(spindles) != 1 {
110
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
124
repos, err := db.GetRepos(
125
s.Db,
126
0,
127
+
orm.FilterEq("spindle", instance),
128
)
129
if err != nil {
130
l.Error("failed to get spindle repos", "err", err)
···
143
Spindle: spindle,
144
Members: members,
145
Repos: repoMap,
146
+
Tabs: spindlesTabs,
147
+
Tab: "spindles",
148
})
149
}
150
···
291
292
spindles, err := db.GetSpindles(
293
s.Db,
294
+
orm.FilterEq("owner", user.Did),
295
+
orm.FilterEq("instance", instance),
296
)
297
if err != nil || len(spindles) != 1 {
298
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
320
// remove spindle members first
321
err = db.RemoveSpindleMember(
322
tx,
323
+
orm.FilterEq("did", user.Did),
324
+
orm.FilterEq("instance", instance),
325
)
326
if err != nil {
327
l.Error("failed to remove spindle members", "err", err)
···
331
332
err = db.DeleteSpindle(
333
tx,
334
+
orm.FilterEq("owner", user.Did),
335
+
orm.FilterEq("instance", instance),
336
)
337
if err != nil {
338
l.Error("failed to delete spindle", "err", err)
···
383
384
shouldRedirect := r.Header.Get("shouldRedirect")
385
if shouldRedirect == "true" {
386
+
s.Pages.HxRedirect(w, "/settings/spindles")
387
return
388
}
389
···
411
412
spindles, err := db.GetSpindles(
413
s.Db,
414
+
orm.FilterEq("owner", user.Did),
415
+
orm.FilterEq("instance", instance),
416
)
417
if err != nil || len(spindles) != 1 {
418
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
454
455
verifiedSpindle, err := db.GetSpindles(
456
s.Db,
457
+
orm.FilterEq("id", rowId),
458
)
459
if err != nil || len(verifiedSpindle) != 1 {
460
l.Error("failed get new spindle", "err", err)
···
487
488
spindles, err := db.GetSpindles(
489
s.Db,
490
+
orm.FilterEq("owner", user.Did),
491
+
orm.FilterEq("instance", instance),
492
)
493
if err != nil || len(spindles) != 1 {
494
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
599
}
600
601
// success
602
+
s.Pages.HxRedirect(w, fmt.Sprintf("/settings/spindles/%s", instance))
603
}
604
605
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
···
623
624
spindles, err := db.GetSpindles(
625
s.Db,
626
+
orm.FilterEq("owner", user.Did),
627
+
orm.FilterEq("instance", instance),
628
)
629
if err != nil || len(spindles) != 1 {
630
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
673
// get the record from the DB first:
674
members, err := db.GetSpindleMembers(
675
s.Db,
676
+
orm.FilterEq("did", user.Did),
677
+
orm.FilterEq("instance", instance),
678
+
orm.FilterEq("subject", memberId.DID),
679
)
680
if err != nil || len(members) != 1 {
681
l.Error("failed to get member", "err", err)
···
686
// remove from db
687
if err = db.RemoveSpindleMember(
688
tx,
689
+
orm.FilterEq("did", user.Did),
690
+
orm.FilterEq("instance", instance),
691
+
orm.FilterEq("subject", memberId.DID),
692
); err != nil {
693
l.Error("failed to remove spindle member", "err", err)
694
fail()
+6
-5
appview/state/gfi.go
+6
-5
appview/state/gfi.go
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
)
15
16
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
20
21
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
22
23
-
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
24
if err != nil {
25
log.Println("failed to get gfi label def", err)
26
s.pages.Error500(w)
27
return
28
}
29
30
-
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
31
if err != nil {
32
log.Println("failed to get repo labels", err)
33
s.pages.Error503(w)
···
55
pagination.Page{
56
Limit: 500,
57
},
58
-
db.FilterIn("repo_at", repoUris),
59
-
db.FilterEq("open", 1),
60
)
61
if err != nil {
62
log.Println("failed to get issues", err)
···
132
}
133
134
if len(uriList) > 0 {
135
-
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
136
if err != nil {
137
log.Println("failed to fetch labels", err)
138
}
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
+
"tangled.org/core/orm"
15
)
16
17
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
21
22
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
23
24
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
25
if err != nil {
26
log.Println("failed to get gfi label def", err)
27
s.pages.Error500(w)
28
return
29
}
30
31
+
repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
32
if err != nil {
33
log.Println("failed to get repo labels", err)
34
s.pages.Error503(w)
···
56
pagination.Page{
57
Limit: 500,
58
},
59
+
orm.FilterIn("repo_at", repoUris),
60
+
orm.FilterEq("open", 1),
61
)
62
if err != nil {
63
log.Println("failed to get issues", err)
···
133
}
134
135
if len(uriList) > 0 {
136
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
137
if err != nil {
138
log.Println("failed to fetch labels", err)
139
}
+6
-5
appview/state/knotstream.go
+6
-5
appview/state/knotstream.go
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
"tangled.org/core/rbac"
20
"tangled.org/core/workflow"
21
···
30
31
knots, err := db.GetRegistrations(
32
d,
33
-
db.FilterIsNot("registered", "null"),
34
)
35
if err != nil {
36
return nil, err
···
143
repos, err := db.GetRepos(
144
d,
145
0,
146
-
db.FilterEq("did", record.RepoDid),
147
-
db.FilterEq("name", record.RepoName),
148
)
149
if err != nil {
150
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
209
repos, err := db.GetRepos(
210
d,
211
0,
212
-
db.FilterEq("did", record.TriggerMetadata.Repo.Did),
213
-
db.FilterEq("name", record.TriggerMetadata.Repo.Repo),
214
)
215
if err != nil {
216
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/workflow"
22
···
31
32
knots, err := db.GetRegistrations(
33
d,
34
+
orm.FilterIsNot("registered", "null"),
35
)
36
if err != nil {
37
return nil, err
···
144
repos, err := db.GetRepos(
145
d,
146
0,
147
+
orm.FilterEq("did", record.RepoDid),
148
+
orm.FilterEq("name", record.RepoName),
149
)
150
if err != nil {
151
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
210
repos, err := db.GetRepos(
211
d,
212
0,
213
+
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
214
+
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
215
)
216
if err != nil {
217
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+27
-18
appview/state/profile.go
+27
-18
appview/state/profile.go
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
)
23
24
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
56
return nil, fmt.Errorf("failed to get profile: %w", err)
57
}
58
59
-
repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did))
60
if err != nil {
61
return nil, fmt.Errorf("failed to get repo count: %w", err)
62
}
63
64
-
stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did))
65
if err != nil {
66
return nil, fmt.Errorf("failed to get string count: %w", err)
67
}
68
69
-
starredCount, err := db.CountStars(s.db, db.FilterEq("did", did))
70
if err != nil {
71
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
72
}
···
86
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
87
punchcard, err := db.MakePunchcard(
88
s.db,
89
-
db.FilterEq("did", did),
90
-
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
91
-
db.FilterLte("date", now.Format(time.DateOnly)),
92
)
93
if err != nil {
94
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
96
97
return &pages.ProfileCard{
98
UserDid: did,
99
-
UserHandle: ident.Handle.String(),
100
Profile: profile,
101
FollowStatus: followStatus,
102
Stats: pages.ProfileStats{
···
119
s.pages.Error500(w)
120
return
121
}
122
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
123
124
repos, err := db.GetRepos(
125
s.db,
126
0,
127
-
db.FilterEq("did", profile.UserDid),
128
)
129
if err != nil {
130
l.Error("failed to fetch repos", "err", err)
···
162
l.Error("failed to create timeline", "err", err)
163
}
164
165
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
166
LoggedInUser: s.oauth.GetUser(r),
167
Card: profile,
···
180
s.pages.Error500(w)
181
return
182
}
183
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
184
185
repos, err := db.GetRepos(
186
s.db,
187
0,
188
-
db.FilterEq("did", profile.UserDid),
189
)
190
if err != nil {
191
l.Error("failed to get repos", "err", err)
···
209
s.pages.Error500(w)
210
return
211
}
212
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
213
214
-
stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid))
215
if err != nil {
216
l.Error("failed to get stars", "err", err)
217
s.pages.Error500(w)
···
238
s.pages.Error500(w)
239
return
240
}
241
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
242
243
-
strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid))
244
if err != nil {
245
l.Error("failed to get strings", "err", err)
246
s.pages.Error500(w)
···
270
if err != nil {
271
return nil, err
272
}
273
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
274
275
loggedInUser := s.oauth.GetUser(r)
276
params := FollowsPageParams{
···
292
followDids = append(followDids, extractDid(follow))
293
}
294
295
-
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
296
if err != nil {
297
l.Error("failed to get profiles", "followDids", followDids, "err", err)
298
return ¶ms, err
···
695
log.Printf("getting profile data for %s: %s", user.Did, err)
696
}
697
698
-
repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did))
699
if err != nil {
700
log.Printf("getting repos for %s: %s", user.Did, err)
701
}
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
+
"tangled.org/core/orm"
23
)
24
25
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
57
return nil, fmt.Errorf("failed to get profile: %w", err)
58
}
59
60
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
61
if err != nil {
62
return nil, fmt.Errorf("failed to get repo count: %w", err)
63
}
64
65
+
stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did))
66
if err != nil {
67
return nil, fmt.Errorf("failed to get string count: %w", err)
68
}
69
70
+
starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did))
71
if err != nil {
72
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
73
}
···
87
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
88
punchcard, err := db.MakePunchcard(
89
s.db,
90
+
orm.FilterEq("did", did),
91
+
orm.FilterGte("date", startOfYear.Format(time.DateOnly)),
92
+
orm.FilterLte("date", now.Format(time.DateOnly)),
93
)
94
if err != nil {
95
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
97
98
return &pages.ProfileCard{
99
UserDid: did,
100
Profile: profile,
101
FollowStatus: followStatus,
102
Stats: pages.ProfileStats{
···
119
s.pages.Error500(w)
120
return
121
}
122
+
l = l.With("profileDid", profile.UserDid)
123
124
repos, err := db.GetRepos(
125
s.db,
126
0,
127
+
orm.FilterEq("did", profile.UserDid),
128
)
129
if err != nil {
130
l.Error("failed to fetch repos", "err", err)
···
162
l.Error("failed to create timeline", "err", err)
163
}
164
165
+
// populate commit counts in the timeline, using the punchcard
166
+
currentMonth := time.Now().Month()
167
+
for _, p := range profile.Punchcard.Punches {
168
+
idx := currentMonth - p.Date.Month()
169
+
if int(idx) < len(timeline.ByMonth) {
170
+
timeline.ByMonth[idx].Commits += p.Count
171
+
}
172
+
}
173
+
174
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
175
LoggedInUser: s.oauth.GetUser(r),
176
Card: profile,
···
189
s.pages.Error500(w)
190
return
191
}
192
+
l = l.With("profileDid", profile.UserDid)
193
194
repos, err := db.GetRepos(
195
s.db,
196
0,
197
+
orm.FilterEq("did", profile.UserDid),
198
)
199
if err != nil {
200
l.Error("failed to get repos", "err", err)
···
218
s.pages.Error500(w)
219
return
220
}
221
+
l = l.With("profileDid", profile.UserDid)
222
223
+
stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid))
224
if err != nil {
225
l.Error("failed to get stars", "err", err)
226
s.pages.Error500(w)
···
247
s.pages.Error500(w)
248
return
249
}
250
+
l = l.With("profileDid", profile.UserDid)
251
252
+
strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid))
253
if err != nil {
254
l.Error("failed to get strings", "err", err)
255
s.pages.Error500(w)
···
279
if err != nil {
280
return nil, err
281
}
282
+
l = l.With("profileDid", profile.UserDid)
283
284
loggedInUser := s.oauth.GetUser(r)
285
params := FollowsPageParams{
···
301
followDids = append(followDids, extractDid(follow))
302
}
303
304
+
profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids))
305
if err != nil {
306
l.Error("failed to get profiles", "followDids", followDids, "err", err)
307
return ¶ms, err
···
704
log.Printf("getting profile data for %s: %s", user.Did, err)
705
}
706
707
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
708
if err != nil {
709
log.Printf("getting repos for %s: %s", user.Did, err)
710
}
+7
-2
appview/state/router.go
+7
-2
appview/state/router.go
···
166
167
r.Mount("/settings", s.SettingsRouter())
168
r.Mount("/strings", s.StringsRouter(mw))
169
-
r.Mount("/knots", s.KnotsRouter())
170
-
r.Mount("/spindles", s.SpindlesRouter())
171
r.Mount("/notifications", s.NotificationsRouter(mw))
172
173
r.Mount("/signup", s.SignupRouter())
···
261
issues := issues.New(
262
s.oauth,
263
s.repoResolver,
264
s.pages,
265
s.idResolver,
266
s.db,
267
s.config,
268
s.notifier,
···
279
s.repoResolver,
280
s.pages,
281
s.idResolver,
282
s.db,
283
s.config,
284
s.notifier,
···
166
167
r.Mount("/settings", s.SettingsRouter())
168
r.Mount("/strings", s.StringsRouter(mw))
169
+
170
+
r.Mount("/settings/knots", s.KnotsRouter())
171
+
r.Mount("/settings/spindles", s.SpindlesRouter())
172
+
173
r.Mount("/notifications", s.NotificationsRouter(mw))
174
175
r.Mount("/signup", s.SignupRouter())
···
263
issues := issues.New(
264
s.oauth,
265
s.repoResolver,
266
+
s.enforcer,
267
s.pages,
268
s.idResolver,
269
+
s.mentionsResolver,
270
s.db,
271
s.config,
272
s.notifier,
···
283
s.repoResolver,
284
s.pages,
285
s.idResolver,
286
+
s.mentionsResolver,
287
s.db,
288
s.config,
289
s.notifier,
+2
-1
appview/state/spindlestream.go
+2
-1
appview/state/spindlestream.go
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
"tangled.org/core/rbac"
21
spindle "tangled.org/core/spindle/models"
22
)
···
27
28
spindles, err := db.GetSpindles(
29
d,
30
-
db.FilterIsNot("verified", "null"),
31
)
32
if err != nil {
33
return nil, err
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/rbac"
22
spindle "tangled.org/core/spindle/models"
23
)
···
28
29
spindles, err := db.GetSpindles(
30
d,
31
+
orm.FilterIsNot("verified", "null"),
32
)
33
if err != nil {
34
return nil, err
+30
-24
appview/state/state.go
+30
-24
appview/state/state.go
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
"tangled.org/core/appview/models"
19
"tangled.org/core/appview/notify"
20
dbnotify "tangled.org/core/appview/notify/db"
···
29
"tangled.org/core/jetstream"
30
"tangled.org/core/log"
31
tlog "tangled.org/core/log"
32
"tangled.org/core/rbac"
33
"tangled.org/core/tid"
34
···
42
)
43
44
type State struct {
45
-
db *db.DB
46
-
notifier notify.Notifier
47
-
indexer *indexer.Indexer
48
-
oauth *oauth.OAuth
49
-
enforcer *rbac.Enforcer
50
-
pages *pages.Pages
51
-
idResolver *idresolver.Resolver
52
-
posthog posthog.Client
53
-
jc *jetstream.JetstreamClient
54
-
config *config.Config
55
-
repoResolver *reporesolver.RepoResolver
56
-
knotstream *eventconsumer.Consumer
57
-
spindlestream *eventconsumer.Consumer
58
-
logger *slog.Logger
59
-
validator *validator.Validator
60
}
61
62
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
96
}
97
validator := validator.New(d, res, enforcer)
98
99
-
repoResolver := reporesolver.New(config, enforcer, res, d)
100
101
wrapper := db.DbWrapper{Execer: d}
102
jc, err := jetstream.NewJetstreamClient(
···
178
enforcer,
179
pages,
180
res,
181
posthog,
182
jc,
183
config,
···
294
return
295
}
296
297
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
298
if err != nil {
299
// non-fatal
300
}
···
318
319
regs, err := db.GetRegistrations(
320
s.db,
321
-
db.FilterEq("did", user.Did),
322
-
db.FilterEq("needs_upgrade", 1),
323
)
324
if err != nil {
325
l.Error("non-fatal: failed to get registrations", "err", err)
···
327
328
spindles, err := db.GetSpindles(
329
s.db,
330
-
db.FilterEq("owner", user.Did),
331
-
db.FilterEq("needs_upgrade", 1),
332
)
333
if err != nil {
334
l.Error("non-fatal: failed to get spindles", "err", err)
···
499
// Check for existing repos
500
existingRepo, err := db.GetRepo(
501
s.db,
502
-
db.FilterEq("did", user.Did),
503
-
db.FilterEq("name", repoName),
504
)
505
if err == nil && existingRepo != nil {
506
l.Info("repo exists")
···
660
}
661
662
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
663
-
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
664
if err != nil {
665
return err
666
}
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
+
"tangled.org/core/appview/mentions"
19
"tangled.org/core/appview/models"
20
"tangled.org/core/appview/notify"
21
dbnotify "tangled.org/core/appview/notify/db"
···
30
"tangled.org/core/jetstream"
31
"tangled.org/core/log"
32
tlog "tangled.org/core/log"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/rbac"
35
"tangled.org/core/tid"
36
···
44
)
45
46
type State struct {
47
+
db *db.DB
48
+
notifier notify.Notifier
49
+
indexer *indexer.Indexer
50
+
oauth *oauth.OAuth
51
+
enforcer *rbac.Enforcer
52
+
pages *pages.Pages
53
+
idResolver *idresolver.Resolver
54
+
mentionsResolver *mentions.Resolver
55
+
posthog posthog.Client
56
+
jc *jetstream.JetstreamClient
57
+
config *config.Config
58
+
repoResolver *reporesolver.RepoResolver
59
+
knotstream *eventconsumer.Consumer
60
+
spindlestream *eventconsumer.Consumer
61
+
logger *slog.Logger
62
+
validator *validator.Validator
63
}
64
65
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
99
}
100
validator := validator.New(d, res, enforcer)
101
102
+
repoResolver := reporesolver.New(config, enforcer, d)
103
+
104
+
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
105
106
wrapper := db.DbWrapper{Execer: d}
107
jc, err := jetstream.NewJetstreamClient(
···
183
enforcer,
184
pages,
185
res,
186
+
mentionsResolver,
187
posthog,
188
jc,
189
config,
···
300
return
301
}
302
303
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
304
if err != nil {
305
// non-fatal
306
}
···
324
325
regs, err := db.GetRegistrations(
326
s.db,
327
+
orm.FilterEq("did", user.Did),
328
+
orm.FilterEq("needs_upgrade", 1),
329
)
330
if err != nil {
331
l.Error("non-fatal: failed to get registrations", "err", err)
···
333
334
spindles, err := db.GetSpindles(
335
s.db,
336
+
orm.FilterEq("owner", user.Did),
337
+
orm.FilterEq("needs_upgrade", 1),
338
)
339
if err != nil {
340
l.Error("non-fatal: failed to get spindles", "err", err)
···
505
// Check for existing repos
506
existingRepo, err := db.GetRepo(
507
s.db,
508
+
orm.FilterEq("did", user.Did),
509
+
orm.FilterEq("name", repoName),
510
)
511
if err == nil && existingRepo != nil {
512
l.Info("repo exists")
···
666
}
667
668
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
669
+
defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults))
670
if err != nil {
671
return err
672
}
+7
-6
appview/strings/strings.go
+7
-6
appview/strings/strings.go
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
"tangled.org/core/tid"
21
22
"github.com/bluesky-social/indigo/api/atproto"
···
108
strings, err := db.GetStrings(
109
s.Db,
110
0,
111
-
db.FilterEq("did", id.DID),
112
-
db.FilterEq("rkey", rkey),
113
)
114
if err != nil {
115
l.Error("failed to fetch string", "err", err)
···
199
all, err := db.GetStrings(
200
s.Db,
201
0,
202
-
db.FilterEq("did", id.DID),
203
-
db.FilterEq("rkey", rkey),
204
)
205
if err != nil {
206
l.Error("failed to fetch string", "err", err)
···
408
409
if err := db.DeleteString(
410
s.Db,
411
-
db.FilterEq("did", user.Did),
412
-
db.FilterEq("rkey", rkey),
413
); err != nil {
414
fail("Failed to delete string.", err)
415
return
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/tid"
22
23
"github.com/bluesky-social/indigo/api/atproto"
···
109
strings, err := db.GetStrings(
110
s.Db,
111
0,
112
+
orm.FilterEq("did", id.DID),
113
+
orm.FilterEq("rkey", rkey),
114
)
115
if err != nil {
116
l.Error("failed to fetch string", "err", err)
···
200
all, err := db.GetStrings(
201
s.Db,
202
0,
203
+
orm.FilterEq("did", id.DID),
204
+
orm.FilterEq("rkey", rkey),
205
)
206
if err != nil {
207
l.Error("failed to fetch string", "err", err)
···
409
410
if err := db.DeleteString(
411
s.Db,
412
+
orm.FilterEq("did", user.Did),
413
+
orm.FilterEq("rkey", rkey),
414
); err != nil {
415
fail("Failed to delete string.", err)
416
return
+2
-1
appview/validator/issue.go
+2
-1
appview/validator/issue.go
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
)
10
11
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
12
// if comments have parents, only ingest ones that are 1 level deep
13
if comment.ReplyTo != nil {
14
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
if err != nil {
16
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
}
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
13
// if comments have parents, only ingest ones that are 1 level deep
14
if comment.ReplyTo != nil {
15
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
16
if err != nil {
17
return fmt.Errorf("failed to fetch parent comment: %w", err)
18
}
+1
-34
crypto/verify.go
+1
-34
crypto/verify.go
···
5
"crypto/sha256"
6
"encoding/base64"
7
"fmt"
8
-
"strings"
9
10
"github.com/hiddeco/sshsig"
11
"golang.org/x/crypto/ssh"
12
-
"tangled.org/core/types"
13
)
14
15
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
28
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
29
// to sha-512 for all key types anyway.
30
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
31
-
return err, err == nil
32
-
}
33
34
-
// VerifyCommitSignature reconstructs the payload used to sign a commit. This is
35
-
// essentially the git cat-file output but without the gpgsig header.
36
-
//
37
-
// Caveats: signature verification will fail on commits with more than one parent,
38
-
// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field
39
-
// and we are unable to reconstruct the payload correctly.
40
-
//
41
-
// Ideally this should directly operate on an *object.Commit.
42
-
func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) {
43
-
signature := commit.Commit.PGPSignature
44
-
45
-
author := bytes.NewBuffer([]byte{})
46
-
committer := bytes.NewBuffer([]byte{})
47
-
commit.Commit.Author.Encode(author)
48
-
commit.Commit.Committer.Encode(committer)
49
-
50
-
payload := strings.Builder{}
51
-
52
-
fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree)
53
-
if commit.Commit.Parent != "" {
54
-
fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent)
55
-
}
56
-
fmt.Fprintf(&payload, "author %s\n", author.String())
57
-
fmt.Fprintf(&payload, "committer %s\n", committer.String())
58
-
if commit.Commit.ChangedId != "" {
59
-
fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId)
60
-
}
61
-
fmt.Fprintf(&payload, "\n%s", commit.Commit.Message)
62
-
63
-
return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String()))
64
}
65
66
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
···
5
"crypto/sha256"
6
"encoding/base64"
7
"fmt"
8
9
"github.com/hiddeco/sshsig"
10
"golang.org/x/crypto/ssh"
11
)
12
13
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
26
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
27
// to sha-512 for all key types anyway.
28
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
29
30
+
return err, err == nil
31
}
32
33
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+3
-3
docs/hacking.md
+3
-3
docs/hacking.md
···
117
# type `poweroff` at the shell to exit the VM
118
```
119
120
-
This starts a knot on port 6000, a spindle on port 6555
121
with `ssh` exposed on port 2222.
122
123
Once the services are running, head to
124
-
http://localhost:3000/knots and hit verify. It should
125
verify the ownership of the services instantly if everything
126
went smoothly.
127
···
146
### running a spindle
147
148
The above VM should already be running a spindle on
149
-
`localhost:6555`. Head to http://localhost:3000/spindles and
150
hit verify. You can then configure each repository to use
151
this spindle and run CI jobs.
152
···
117
# type `poweroff` at the shell to exit the VM
118
```
119
120
+
This starts a knot on port 6444, a spindle on port 6555
121
with `ssh` exposed on port 2222.
122
123
Once the services are running, head to
124
+
http://localhost:3000/settings/knots and hit verify. It should
125
verify the ownership of the services instantly if everything
126
went smoothly.
127
···
146
### running a spindle
147
148
The above VM should already be running a spindle on
149
+
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
150
hit verify. You can then configure each repository to use
151
this spindle and run CI jobs.
152
+1
-1
docs/knot-hosting.md
+1
-1
docs/knot-hosting.md
···
131
132
You should now have a running knot server! You can finalize
133
your registration by hitting the `verify` button on the
134
+
[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
135
a record on your PDS to announce the existence of the knot.
136
137
### custom paths
+3
-3
docs/migrations.md
+3
-3
docs/migrations.md
···
14
For knots:
15
16
- Upgrade to latest tag (v1.9.0 or above)
17
-
- Head to the [knot dashboard](https://tangled.org/knots) and
18
hit the "retry" button to verify your knot
19
20
For spindles:
21
22
- Upgrade to latest tag (v1.9.0 or above)
23
- Head to the [spindle
24
-
dashboard](https://tangled.org/spindles) and hit the
25
"retry" button to verify your spindle
26
27
## Upgrading from v1.7.x
···
41
[settings](https://tangled.org/settings) page.
42
- Restart your knot once you have replaced the environment
43
variable
44
-
- Head to the [knot dashboard](https://tangled.org/knots) and
45
hit the "retry" button to verify your knot. This simply
46
writes a `sh.tangled.knot` record to your PDS.
47
···
14
For knots:
15
16
- Upgrade to latest tag (v1.9.0 or above)
17
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
18
hit the "retry" button to verify your knot
19
20
For spindles:
21
22
- Upgrade to latest tag (v1.9.0 or above)
23
- Head to the [spindle
24
+
dashboard](https://tangled.org/settings/spindles) and hit the
25
"retry" button to verify your spindle
26
27
## Upgrading from v1.7.x
···
41
[settings](https://tangled.org/settings) page.
42
- Restart your knot once you have replaced the environment
43
variable
44
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
45
hit the "retry" button to verify your knot. This simply
46
writes a `sh.tangled.knot` record to your PDS.
47
+3
-3
flake.lock
+3
-3
flake.lock
-2
flake.nix
-2
flake.nix
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
-
inherit (pkgs) gcc;
84
inherit sqlite-lib-src;
85
};
86
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
156
nativeBuildInputs = [
157
pkgs.go
158
pkgs.air
159
-
pkgs.tilt
160
pkgs.gopls
161
pkgs.httpie
162
pkgs.litecli
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
inherit sqlite-lib-src;
84
};
85
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
155
nativeBuildInputs = [
156
pkgs.go
157
pkgs.air
158
pkgs.gopls
159
pkgs.httpie
160
pkgs.litecli
+1
-3
go.mod
+1
-3
go.mod
···
1
module tangled.org/core
2
3
-
go 1.24.4
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
-
github.com/wyatt915/goldmark-treeblood v0.0.1
48
github.com/yuin/goldmark v1.7.13
49
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
50
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
190
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
191
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
192
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
193
-
github.com/wyatt915/treeblood v0.1.16 // indirect
194
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
195
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
196
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
···
1
module tangled.org/core
2
3
+
go 1.25.0
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
github.com/yuin/goldmark v1.7.13
48
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
49
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
189
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
190
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
191
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
192
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
193
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
194
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-4
go.sum
-4
go.sum
···
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
-
github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs=
499
-
github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208=
500
-
github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y=
501
-
github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY=
502
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
503
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
504
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
···
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
499
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
500
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+15
-4
jetstream/jetstream.go
+15
-4
jetstream/jetstream.go
···
72
// existing instances of the closure when j.WantedDids is mutated
73
return func(ctx context.Context, evt *models.Event) error {
74
75
// empty filter => all dids allowed
76
-
if len(j.wantedDids) == 0 {
77
-
return processFunc(ctx, evt)
78
}
79
80
-
if _, ok := j.wantedDids[evt.Did]; ok {
81
return processFunc(ctx, evt)
82
} else {
83
return nil
···
122
123
go func() {
124
if j.waitForDid {
125
-
for len(j.wantedDids) == 0 {
126
time.Sleep(time.Second)
127
}
128
}
···
72
// existing instances of the closure when j.WantedDids is mutated
73
return func(ctx context.Context, evt *models.Event) error {
74
75
+
j.mu.RLock()
76
// empty filter => all dids allowed
77
+
matches := len(j.wantedDids) == 0
78
+
if !matches {
79
+
if _, ok := j.wantedDids[evt.Did]; ok {
80
+
matches = true
81
+
}
82
}
83
+
j.mu.RUnlock()
84
85
+
if matches {
86
return processFunc(ctx, evt)
87
} else {
88
return nil
···
127
128
go func() {
129
if j.waitForDid {
130
+
for {
131
+
j.mu.RLock()
132
+
hasDid := len(j.wantedDids) != 0
133
+
j.mu.RUnlock()
134
+
if hasDid {
135
+
break
136
+
}
137
time.Sleep(time.Second)
138
}
139
}
+81
knotserver/db/db.go
+81
knotserver/db/db.go
···
···
1
+
package db
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"log/slog"
7
+
"strings"
8
+
9
+
_ "github.com/mattn/go-sqlite3"
10
+
"tangled.org/core/log"
11
+
)
12
+
13
+
type DB struct {
14
+
db *sql.DB
15
+
logger *slog.Logger
16
+
}
17
+
18
+
func Setup(ctx context.Context, dbPath string) (*DB, error) {
19
+
// https://github.com/mattn/go-sqlite3#connection-string
20
+
opts := []string{
21
+
"_foreign_keys=1",
22
+
"_journal_mode=WAL",
23
+
"_synchronous=NORMAL",
24
+
"_auto_vacuum=incremental",
25
+
}
26
+
27
+
logger := log.FromContext(ctx)
28
+
logger = log.SubLogger(logger, "db")
29
+
30
+
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
31
+
if err != nil {
32
+
return nil, err
33
+
}
34
+
35
+
conn, err := db.Conn(ctx)
36
+
if err != nil {
37
+
return nil, err
38
+
}
39
+
defer conn.Close()
40
+
41
+
_, err = conn.ExecContext(ctx, `
42
+
create table if not exists known_dids (
43
+
did text primary key
44
+
);
45
+
46
+
create table if not exists public_keys (
47
+
id integer primary key autoincrement,
48
+
did text not null,
49
+
key text not null,
50
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
51
+
unique(did, key),
52
+
foreign key (did) references known_dids(did) on delete cascade
53
+
);
54
+
55
+
create table if not exists _jetstream (
56
+
id integer primary key autoincrement,
57
+
last_time_us integer not null
58
+
);
59
+
60
+
create table if not exists events (
61
+
rkey text not null,
62
+
nsid text not null,
63
+
event text not null, -- json
64
+
created integer not null default (strftime('%s', 'now')),
65
+
primary key (rkey, nsid)
66
+
);
67
+
68
+
create table if not exists migrations (
69
+
id integer primary key autoincrement,
70
+
name text unique
71
+
);
72
+
`)
73
+
if err != nil {
74
+
return nil, err
75
+
}
76
+
77
+
return &DB{
78
+
db: db,
79
+
logger: logger,
80
+
}, nil
81
+
}
-64
knotserver/db/init.go
-64
knotserver/db/init.go
···
1
-
package db
2
-
3
-
import (
4
-
"database/sql"
5
-
"strings"
6
-
7
-
_ "github.com/mattn/go-sqlite3"
8
-
)
9
-
10
-
type DB struct {
11
-
db *sql.DB
12
-
}
13
-
14
-
func Setup(dbPath string) (*DB, error) {
15
-
// https://github.com/mattn/go-sqlite3#connection-string
16
-
opts := []string{
17
-
"_foreign_keys=1",
18
-
"_journal_mode=WAL",
19
-
"_synchronous=NORMAL",
20
-
"_auto_vacuum=incremental",
21
-
}
22
-
23
-
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
24
-
if err != nil {
25
-
return nil, err
26
-
}
27
-
28
-
// NOTE: If any other migration is added here, you MUST
29
-
// copy the pattern in appview: use a single sql.Conn
30
-
// for every migration.
31
-
32
-
_, err = db.Exec(`
33
-
create table if not exists known_dids (
34
-
did text primary key
35
-
);
36
-
37
-
create table if not exists public_keys (
38
-
id integer primary key autoincrement,
39
-
did text not null,
40
-
key text not null,
41
-
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
42
-
unique(did, key),
43
-
foreign key (did) references known_dids(did) on delete cascade
44
-
);
45
-
46
-
create table if not exists _jetstream (
47
-
id integer primary key autoincrement,
48
-
last_time_us integer not null
49
-
);
50
-
51
-
create table if not exists events (
52
-
rkey text not null,
53
-
nsid text not null,
54
-
event text not null, -- json
55
-
created integer not null default (strftime('%s', 'now')),
56
-
primary key (rkey, nsid)
57
-
);
58
-
`)
59
-
if err != nil {
60
-
return nil, err
61
-
}
62
-
63
-
return &DB{db: db}, nil
64
-
}
···
+1
-17
knotserver/git/diff.go
+1
-17
knotserver/git/diff.go
···
77
nd.Diff = append(nd.Diff, ndiff)
78
}
79
80
-
nd.Stat.FilesChanged = len(diffs)
81
-
nd.Commit.This = c.Hash.String()
82
-
nd.Commit.PGPSignature = c.PGPSignature
83
-
nd.Commit.Committer = c.Committer
84
-
nd.Commit.Tree = c.TreeHash.String()
85
-
86
-
if parent.Hash.IsZero() {
87
-
nd.Commit.Parent = ""
88
-
} else {
89
-
nd.Commit.Parent = parent.Hash.String()
90
-
}
91
-
nd.Commit.Author = c.Author
92
-
nd.Commit.Message = c.Message
93
-
94
-
if v, ok := c.ExtraHeaders["change-id"]; ok {
95
-
nd.Commit.ChangedId = string(v)
96
-
}
97
98
return &nd, nil
99
}
+38
-2
knotserver/git/fork.go
+38
-2
knotserver/git/fork.go
···
3
import (
4
"errors"
5
"fmt"
6
"os/exec"
7
8
"github.com/go-git/go-git/v5"
9
"github.com/go-git/go-git/v5/config"
10
)
11
12
-
func Fork(repoPath, source string) error {
13
-
cloneCmd := exec.Command("git", "clone", "--bare", source, repoPath)
14
if err := cloneCmd.Run(); err != nil {
15
return fmt.Errorf("failed to bare clone repository: %w", err)
16
}
···
21
}
22
23
return nil
24
}
25
26
func (g *GitRepo) Sync() error {
···
3
import (
4
"errors"
5
"fmt"
6
+
"log/slog"
7
+
"net/url"
8
"os/exec"
9
+
"path/filepath"
10
11
"github.com/go-git/go-git/v5"
12
"github.com/go-git/go-git/v5/config"
13
+
knotconfig "tangled.org/core/knotserver/config"
14
)
15
16
+
func Fork(repoPath, source string, cfg *knotconfig.Config) error {
17
+
u, err := url.Parse(source)
18
+
if err != nil {
19
+
return fmt.Errorf("failed to parse source URL: %w", err)
20
+
}
21
+
22
+
if o := optimizeClone(u, cfg); o != nil {
23
+
u = o
24
+
}
25
+
26
+
cloneCmd := exec.Command("git", "clone", "--bare", u.String(), repoPath)
27
if err := cloneCmd.Run(); err != nil {
28
return fmt.Errorf("failed to bare clone repository: %w", err)
29
}
···
34
}
35
36
return nil
37
+
}
38
+
39
+
func optimizeClone(u *url.URL, cfg *knotconfig.Config) *url.URL {
40
+
// only optimize if it's the same host
41
+
if u.Host != cfg.Server.Hostname {
42
+
return nil
43
+
}
44
+
45
+
local := filepath.Join(cfg.Repo.ScanPath, u.Path)
46
+
47
+
// sanity check: is there a git repo there?
48
+
if _, err := PlainOpen(local); err != nil {
49
+
return nil
50
+
}
51
+
52
+
// create optimized file:// URL
53
+
optimized := &url.URL{
54
+
Scheme: "file",
55
+
Path: local,
56
+
}
57
+
58
+
slog.Debug("performing local clone", "url", optimized.String())
59
+
return optimized
60
}
61
62
func (g *GitRepo) Sync() error {
+1
-1
knotserver/server.go
+1
-1
knotserver/server.go
+1
-1
knotserver/xrpc/create_repo.go
+1
-1
knotserver/xrpc/create_repo.go
···
84
repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
85
86
if data.Source != nil && *data.Source != "" {
87
-
err = git.Fork(repoPath, *data.Source)
88
if err != nil {
89
l.Error("forking repo", "error", err.Error())
90
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
···
84
repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
85
86
if data.Source != nil && *data.Source != "" {
87
+
err = git.Fork(repoPath, *data.Source, h.Config)
88
if err != nil {
89
l.Error("forking repo", "error", err.Error())
90
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+6
-1
knotserver/xrpc/repo_log.go
+6
-1
knotserver/xrpc/repo_log.go
···
62
return
63
}
64
65
+
tcommits := make([]types.Commit, len(commits))
66
+
for i, c := range commits {
67
+
tcommits[i].FromGoGitCommit(c)
68
+
}
69
+
70
// Create response using existing types.RepoLogResponse
71
response := types.RepoLogResponse{
72
+
Commits: tcommits,
73
Ref: ref,
74
Page: (offset / limit) + 1,
75
PerPage: limit,
+14
lexicons/issue/comment.json
+14
lexicons/issue/comment.json
···
29
"replyTo": {
30
"type": "string",
31
"format": "at-uri"
32
+
},
33
+
"mentions": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "string",
37
+
"format": "did"
38
+
}
39
+
},
40
+
"references": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "string",
44
+
"format": "at-uri"
45
+
}
46
}
47
}
48
}
+14
lexicons/issue/issue.json
+14
lexicons/issue/issue.json
···
24
"createdAt": {
25
"type": "string",
26
"format": "datetime"
27
+
},
28
+
"mentions": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "string",
32
+
"format": "did"
33
+
}
34
+
},
35
+
"references": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "string",
39
+
"format": "at-uri"
40
+
}
41
}
42
}
43
}
+14
lexicons/pulls/comment.json
+14
lexicons/pulls/comment.json
···
25
"createdAt": {
26
"type": "string",
27
"format": "datetime"
28
+
},
29
+
"mentions": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "string",
33
+
"format": "did"
34
+
}
35
+
},
36
+
"references": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "string",
40
+
"format": "at-uri"
41
+
}
42
}
43
}
44
}
+14
lexicons/pulls/pull.json
+14
lexicons/pulls/pull.json
···
36
"createdAt": {
37
"type": "string",
38
"format": "datetime"
39
+
},
40
+
"mentions": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "string",
44
+
"format": "did"
45
+
}
46
+
},
47
+
"references": {
48
+
"type": "array",
49
+
"items": {
50
+
"type": "string",
51
+
"format": "at-uri"
52
+
}
53
}
54
}
55
}
-30
nix/gomod2nix.toml
-30
nix/gomod2nix.toml
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
169
-
version = "v4.4.0"
170
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
171
[mod."github.com/dgraph-io/ristretto"]
172
version = "v0.2.0"
173
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
373
[mod."github.com/klauspost/cpuid/v2"]
374
version = "v2.3.0"
375
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
-
[mod."github.com/lestrrat-go/blackmagic"]
377
-
version = "v1.0.4"
378
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
379
-
[mod."github.com/lestrrat-go/httpcc"]
380
-
version = "v1.0.1"
381
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
382
-
[mod."github.com/lestrrat-go/httprc"]
383
-
version = "v1.0.6"
384
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
385
-
[mod."github.com/lestrrat-go/iter"]
386
-
version = "v1.0.2"
387
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
388
-
[mod."github.com/lestrrat-go/jwx/v2"]
389
-
version = "v2.1.6"
390
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
391
-
[mod."github.com/lestrrat-go/option"]
392
-
version = "v1.0.1"
393
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
394
[mod."github.com/lucasb-eyer/go-colorful"]
395
version = "v1.2.0"
396
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
511
[mod."github.com/ryanuber/go-glob"]
512
version = "v1.0.0"
513
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
514
-
[mod."github.com/segmentio/asm"]
515
-
version = "v1.2.0"
516
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
517
[mod."github.com/sergi/go-diff"]
518
version = "v1.1.0"
519
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
548
[mod."github.com/whyrusleeping/cbor-gen"]
549
version = "v0.3.1"
550
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
551
-
[mod."github.com/wyatt915/goldmark-treeblood"]
552
-
version = "v0.0.1"
553
-
hash = "sha256-hAVFaktO02MiiqZFffr8ZlvFEfwxw4Y84OZ2t7e5G7g="
554
-
[mod."github.com/wyatt915/treeblood"]
555
-
version = "v0.1.16"
556
-
hash = "sha256-T68sa+iVx0qY7dDjXEAJvRWQEGXYIpUsf9tcWwO1tIw="
557
[mod."github.com/xo/terminfo"]
558
version = "v0.0.0-20220910002029-abceb7e1c41e"
559
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
[mod."github.com/dgraph-io/ristretto"]
169
version = "v0.2.0"
170
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
370
[mod."github.com/klauspost/cpuid/v2"]
371
version = "v2.3.0"
372
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
373
[mod."github.com/lucasb-eyer/go-colorful"]
374
version = "v1.2.0"
375
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
490
[mod."github.com/ryanuber/go-glob"]
491
version = "v1.0.0"
492
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
493
[mod."github.com/sergi/go-diff"]
494
version = "v1.1.0"
495
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
524
[mod."github.com/whyrusleeping/cbor-gen"]
525
version = "v0.3.1"
526
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
527
[mod."github.com/xo/terminfo"]
528
version = "v0.0.0-20220910002029-abceb7e1c41e"
529
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
+2
nix/modules/knot.nix
+2
nix/modules/knot.nix
+7
-5
nix/pkgs/sqlite-lib.nix
+7
-5
nix/pkgs/sqlite-lib.nix
···
1
{
2
-
gcc,
3
stdenv,
4
sqlite-lib-src,
5
}:
6
stdenv.mkDerivation {
7
name = "sqlite-lib";
8
src = sqlite-lib-src;
9
-
nativeBuildInputs = [gcc];
10
buildPhase = ''
11
-
gcc -c sqlite3.c
12
-
ar rcs libsqlite3.a sqlite3.o
13
-
ranlib libsqlite3.a
14
mkdir -p $out/include $out/lib
15
cp *.h $out/include
16
cp libsqlite3.a $out/lib
···
1
{
2
stdenv,
3
sqlite-lib-src,
4
}:
5
stdenv.mkDerivation {
6
name = "sqlite-lib";
7
src = sqlite-lib-src;
8
+
9
buildPhase = ''
10
+
$CC -c sqlite3.c
11
+
$AR rcs libsqlite3.a sqlite3.o
12
+
$RANLIB libsqlite3.a
13
+
'';
14
+
15
+
installPhase = ''
16
mkdir -p $out/include $out/lib
17
cp *.h $out/include
18
cp libsqlite3.a $out/lib
+4
-4
nix/vm.nix
+4
-4
nix/vm.nix
···
48
# knot
49
{
50
from = "host";
51
-
host.port = 6000;
52
-
guest.port = 6000;
53
}
54
# spindle
55
{
···
87
motd = "Welcome to the development knot!\n";
88
server = {
89
owner = envVar "TANGLED_VM_KNOT_OWNER";
90
-
hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6000";
91
plcUrl = plcUrl;
92
jetstreamEndpoint = jetstream;
93
-
listenAddr = "0.0.0.0:6000";
94
};
95
};
96
services.tangled.spindle = {
···
48
# knot
49
{
50
from = "host";
51
+
host.port = 6444;
52
+
guest.port = 6444;
53
}
54
# spindle
55
{
···
87
motd = "Welcome to the development knot!\n";
88
server = {
89
owner = envVar "TANGLED_VM_KNOT_OWNER";
90
+
hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6444";
91
plcUrl = plcUrl;
92
jetstreamEndpoint = jetstream;
93
+
listenAddr = "0.0.0.0:6444";
94
};
95
};
96
services.tangled.spindle = {
+122
orm/orm.go
+122
orm/orm.go
···
···
1
+
package orm
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"fmt"
7
+
"log/slog"
8
+
"reflect"
9
+
"strings"
10
+
)
11
+
12
+
type migrationFn = func(*sql.Tx) error
13
+
14
+
func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
15
+
logger = logger.With("migration", name)
16
+
17
+
tx, err := c.BeginTx(context.Background(), nil)
18
+
if err != nil {
19
+
return err
20
+
}
21
+
defer tx.Rollback()
22
+
23
+
var exists bool
24
+
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
25
+
if err != nil {
26
+
return err
27
+
}
28
+
29
+
if !exists {
30
+
// run migration
31
+
err = migrationFn(tx)
32
+
if err != nil {
33
+
logger.Error("failed to run migration", "err", err)
34
+
return err
35
+
}
36
+
37
+
// mark migration as complete
38
+
_, err = tx.Exec("insert into migrations (name) values (?)", name)
39
+
if err != nil {
40
+
logger.Error("failed to mark migration as complete", "err", err)
41
+
return err
42
+
}
43
+
44
+
// commit the transaction
45
+
if err := tx.Commit(); err != nil {
46
+
return err
47
+
}
48
+
49
+
logger.Info("migration applied successfully")
50
+
} else {
51
+
logger.Warn("skipped migration, already applied")
52
+
}
53
+
54
+
return nil
55
+
}
56
+
57
+
type Filter struct {
58
+
Key string
59
+
arg any
60
+
Cmp string
61
+
}
62
+
63
+
func newFilter(key, cmp string, arg any) Filter {
64
+
return Filter{
65
+
Key: key,
66
+
arg: arg,
67
+
Cmp: cmp,
68
+
}
69
+
}
70
+
71
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
72
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
73
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
74
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
75
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
76
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
77
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
78
+
func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
79
+
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
80
+
func FilterContains(key string, arg any) Filter {
81
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
82
+
}
83
+
84
+
func (f Filter) Condition() string {
85
+
rv := reflect.ValueOf(f.arg)
86
+
kind := rv.Kind()
87
+
88
+
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
89
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
90
+
if rv.Len() == 0 {
91
+
// always false
92
+
return "1 = 0"
93
+
}
94
+
95
+
placeholders := make([]string, rv.Len())
96
+
for i := range placeholders {
97
+
placeholders[i] = "?"
98
+
}
99
+
100
+
return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
101
+
}
102
+
103
+
return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
104
+
}
105
+
106
+
func (f Filter) Arg() []any {
107
+
rv := reflect.ValueOf(f.arg)
108
+
kind := rv.Kind()
109
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
110
+
if rv.Len() == 0 {
111
+
return nil
112
+
}
113
+
114
+
out := make([]any, rv.Len())
115
+
for i := range rv.Len() {
116
+
out[i] = rv.Index(i).Interface()
117
+
}
118
+
return out
119
+
}
120
+
121
+
return []any{f.arg}
122
+
}
-1
patchutil/patchutil.go
-1
patchutil/patchutil.go
+8
rbac/rbac.go
+8
rbac/rbac.go
···
285
return e.E.Enforce(user, domain, repo, "repo:delete")
286
}
287
288
+
func (e *Enforcer) IsRepoOwner(user, domain, repo string) (bool, error) {
289
+
return e.E.Enforce(user, domain, repo, "repo:owner")
290
+
}
291
+
292
+
func (e *Enforcer) IsRepoCollaborator(user, domain, repo string) (bool, error) {
293
+
return e.E.Enforce(user, domain, repo, "repo:collaborator")
294
+
}
295
+
296
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
297
return e.E.Enforce(user, domain, repo, "repo:push")
298
}
+31
sets/gen.go
+31
sets/gen.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"math/rand"
5
+
"reflect"
6
+
"testing/quick"
7
+
)
8
+
9
+
func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value {
10
+
s := New[T]()
11
+
12
+
var zero T
13
+
itemType := reflect.TypeOf(zero)
14
+
15
+
for {
16
+
if s.Len() >= size {
17
+
break
18
+
}
19
+
20
+
item, ok := quick.Value(itemType, rand)
21
+
if !ok {
22
+
continue
23
+
}
24
+
25
+
if val, ok := item.Interface().(T); ok {
26
+
s.Insert(val)
27
+
}
28
+
}
29
+
30
+
return reflect.ValueOf(s)
31
+
}
+35
sets/readme.txt
+35
sets/readme.txt
···
···
1
+
sets
2
+
----
3
+
set datastructure for go with generics and iterators. the
4
+
api is supposed to mimic rust's std::collections::HashSet api.
5
+
6
+
s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4}))
7
+
s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6}))
8
+
9
+
union := sets.Collect(s1.Union(s2))
10
+
intersect := sets.Collect(s1.Intersection(s2))
11
+
diff := sets.Collect(s1.Difference(s2))
12
+
symdiff := sets.Collect(s1.SymmetricDifference(s2))
13
+
14
+
s1.Len() // 4
15
+
s1.Contains(1) // true
16
+
s1.IsEmpty() // false
17
+
s1.IsSubset(s2) // true
18
+
s1.IsSuperset(s2) // false
19
+
s1.IsDisjoint(s2) // false
20
+
21
+
if exists := s1.Insert(1); exists {
22
+
// already existed in set
23
+
}
24
+
25
+
if existed := s1.Remove(1); existed {
26
+
// existed in set, now removed
27
+
}
28
+
29
+
30
+
testing
31
+
-------
32
+
includes property-based tests using the wonderful
33
+
testing/quick module!
34
+
35
+
go test -v
+174
sets/set.go
+174
sets/set.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"iter"
5
+
"maps"
6
+
)
7
+
8
+
type Set[T comparable] struct {
9
+
data map[T]struct{}
10
+
}
11
+
12
+
func New[T comparable]() Set[T] {
13
+
return Set[T]{
14
+
data: make(map[T]struct{}),
15
+
}
16
+
}
17
+
18
+
func (s *Set[T]) Insert(item T) bool {
19
+
_, exists := s.data[item]
20
+
s.data[item] = struct{}{}
21
+
return !exists
22
+
}
23
+
24
+
func Singleton[T comparable](item T) Set[T] {
25
+
n := New[T]()
26
+
_ = n.Insert(item)
27
+
return n
28
+
}
29
+
30
+
func (s *Set[T]) Remove(item T) bool {
31
+
_, exists := s.data[item]
32
+
if exists {
33
+
delete(s.data, item)
34
+
}
35
+
return exists
36
+
}
37
+
38
+
func (s Set[T]) Contains(item T) bool {
39
+
_, exists := s.data[item]
40
+
return exists
41
+
}
42
+
43
+
func (s Set[T]) Len() int {
44
+
return len(s.data)
45
+
}
46
+
47
+
func (s Set[T]) IsEmpty() bool {
48
+
return len(s.data) == 0
49
+
}
50
+
51
+
func (s *Set[T]) Clear() {
52
+
s.data = make(map[T]struct{})
53
+
}
54
+
55
+
func (s Set[T]) All() iter.Seq[T] {
56
+
return func(yield func(T) bool) {
57
+
for item := range s.data {
58
+
if !yield(item) {
59
+
return
60
+
}
61
+
}
62
+
}
63
+
}
64
+
65
+
func (s Set[T]) Clone() Set[T] {
66
+
return Set[T]{
67
+
data: maps.Clone(s.data),
68
+
}
69
+
}
70
+
71
+
func (s Set[T]) Union(other Set[T]) iter.Seq[T] {
72
+
if s.Len() >= other.Len() {
73
+
return chain(s.All(), other.Difference(s))
74
+
} else {
75
+
return chain(other.All(), s.Difference(other))
76
+
}
77
+
}
78
+
79
+
func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
80
+
return func(yield func(T) bool) {
81
+
for _, seq := range seqs {
82
+
for item := range seq {
83
+
if !yield(item) {
84
+
return
85
+
}
86
+
}
87
+
}
88
+
}
89
+
}
90
+
91
+
func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] {
92
+
return func(yield func(T) bool) {
93
+
for item := range s.data {
94
+
if other.Contains(item) {
95
+
if !yield(item) {
96
+
return
97
+
}
98
+
}
99
+
}
100
+
}
101
+
}
102
+
103
+
func (s Set[T]) Difference(other Set[T]) iter.Seq[T] {
104
+
return func(yield func(T) bool) {
105
+
for item := range s.data {
106
+
if !other.Contains(item) {
107
+
if !yield(item) {
108
+
return
109
+
}
110
+
}
111
+
}
112
+
}
113
+
}
114
+
115
+
func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] {
116
+
return func(yield func(T) bool) {
117
+
for item := range s.data {
118
+
if !other.Contains(item) {
119
+
if !yield(item) {
120
+
return
121
+
}
122
+
}
123
+
}
124
+
for item := range other.data {
125
+
if !s.Contains(item) {
126
+
if !yield(item) {
127
+
return
128
+
}
129
+
}
130
+
}
131
+
}
132
+
}
133
+
134
+
func (s Set[T]) IsSubset(other Set[T]) bool {
135
+
for item := range s.data {
136
+
if !other.Contains(item) {
137
+
return false
138
+
}
139
+
}
140
+
return true
141
+
}
142
+
143
+
func (s Set[T]) IsSuperset(other Set[T]) bool {
144
+
return other.IsSubset(s)
145
+
}
146
+
147
+
func (s Set[T]) IsDisjoint(other Set[T]) bool {
148
+
for item := range s.data {
149
+
if other.Contains(item) {
150
+
return false
151
+
}
152
+
}
153
+
return true
154
+
}
155
+
156
+
func (s Set[T]) Equal(other Set[T]) bool {
157
+
if s.Len() != other.Len() {
158
+
return false
159
+
}
160
+
for item := range s.data {
161
+
if !other.Contains(item) {
162
+
return false
163
+
}
164
+
}
165
+
return true
166
+
}
167
+
168
+
func Collect[T comparable](seq iter.Seq[T]) Set[T] {
169
+
result := New[T]()
170
+
for item := range seq {
171
+
result.Insert(item)
172
+
}
173
+
return result
174
+
}
+411
sets/set_test.go
+411
sets/set_test.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"slices"
5
+
"testing"
6
+
"testing/quick"
7
+
)
8
+
9
+
func TestNew(t *testing.T) {
10
+
s := New[int]()
11
+
if s.Len() != 0 {
12
+
t.Errorf("New set should be empty, got length %d", s.Len())
13
+
}
14
+
if !s.IsEmpty() {
15
+
t.Error("New set should be empty")
16
+
}
17
+
}
18
+
19
+
func TestFromSlice(t *testing.T) {
20
+
s := Collect(slices.Values([]int{1, 2, 3, 2, 1}))
21
+
if s.Len() != 3 {
22
+
t.Errorf("Expected length 3, got %d", s.Len())
23
+
}
24
+
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
25
+
t.Error("Set should contain all unique elements from slice")
26
+
}
27
+
}
28
+
29
+
func TestInsert(t *testing.T) {
30
+
s := New[string]()
31
+
32
+
if !s.Insert("hello") {
33
+
t.Error("First insert should return true")
34
+
}
35
+
if s.Insert("hello") {
36
+
t.Error("Duplicate insert should return false")
37
+
}
38
+
if s.Len() != 1 {
39
+
t.Errorf("Expected length 1, got %d", s.Len())
40
+
}
41
+
}
42
+
43
+
func TestRemove(t *testing.T) {
44
+
s := Collect(slices.Values([]int{1, 2, 3}))
45
+
46
+
if !s.Remove(2) {
47
+
t.Error("Remove existing element should return true")
48
+
}
49
+
if s.Remove(2) {
50
+
t.Error("Remove non-existing element should return false")
51
+
}
52
+
if s.Contains(2) {
53
+
t.Error("Element should be removed")
54
+
}
55
+
if s.Len() != 2 {
56
+
t.Errorf("Expected length 2, got %d", s.Len())
57
+
}
58
+
}
59
+
60
+
func TestContains(t *testing.T) {
61
+
s := Collect(slices.Values([]int{1, 2, 3}))
62
+
63
+
if !s.Contains(1) {
64
+
t.Error("Should contain 1")
65
+
}
66
+
if s.Contains(4) {
67
+
t.Error("Should not contain 4")
68
+
}
69
+
}
70
+
71
+
func TestClear(t *testing.T) {
72
+
s := Collect(slices.Values([]int{1, 2, 3}))
73
+
s.Clear()
74
+
75
+
if !s.IsEmpty() {
76
+
t.Error("Set should be empty after clear")
77
+
}
78
+
if s.Len() != 0 {
79
+
t.Errorf("Expected length 0, got %d", s.Len())
80
+
}
81
+
}
82
+
83
+
func TestIterator(t *testing.T) {
84
+
s := Collect(slices.Values([]int{1, 2, 3}))
85
+
var items []int
86
+
87
+
for item := range s.All() {
88
+
items = append(items, item)
89
+
}
90
+
91
+
slices.Sort(items)
92
+
expected := []int{1, 2, 3}
93
+
if !slices.Equal(items, expected) {
94
+
t.Errorf("Expected %v, got %v", expected, items)
95
+
}
96
+
}
97
+
98
+
func TestClone(t *testing.T) {
99
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
100
+
s2 := s1.Clone()
101
+
102
+
if !s1.Equal(s2) {
103
+
t.Error("Cloned set should be equal to original")
104
+
}
105
+
106
+
s2.Insert(4)
107
+
if s1.Contains(4) {
108
+
t.Error("Modifying clone should not affect original")
109
+
}
110
+
}
111
+
112
+
func TestUnion(t *testing.T) {
113
+
s1 := Collect(slices.Values([]int{1, 2}))
114
+
s2 := Collect(slices.Values([]int{2, 3}))
115
+
116
+
result := Collect(s1.Union(s2))
117
+
expected := Collect(slices.Values([]int{1, 2, 3}))
118
+
119
+
if !result.Equal(expected) {
120
+
t.Errorf("Expected %v, got %v", expected, result)
121
+
}
122
+
}
123
+
124
+
func TestIntersection(t *testing.T) {
125
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
126
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
127
+
128
+
expected := Collect(slices.Values([]int{2, 3}))
129
+
result := Collect(s1.Intersection(s2))
130
+
131
+
if !result.Equal(expected) {
132
+
t.Errorf("Expected %v, got %v", expected, result)
133
+
}
134
+
}
135
+
136
+
func TestDifference(t *testing.T) {
137
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
138
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
139
+
140
+
expected := Collect(slices.Values([]int{1}))
141
+
result := Collect(s1.Difference(s2))
142
+
143
+
if !result.Equal(expected) {
144
+
t.Errorf("Expected %v, got %v", expected, result)
145
+
}
146
+
}
147
+
148
+
func TestSymmetricDifference(t *testing.T) {
149
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
150
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
151
+
152
+
expected := Collect(slices.Values([]int{1, 4}))
153
+
result := Collect(s1.SymmetricDifference(s2))
154
+
155
+
if !result.Equal(expected) {
156
+
t.Errorf("Expected %v, got %v", expected, result)
157
+
}
158
+
}
159
+
160
+
func TestSymmetricDifferenceCommutativeProperty(t *testing.T) {
161
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
162
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
163
+
164
+
result1 := Collect(s1.SymmetricDifference(s2))
165
+
result2 := Collect(s2.SymmetricDifference(s1))
166
+
167
+
if !result1.Equal(result2) {
168
+
t.Errorf("Expected %v, got %v", result1, result2)
169
+
}
170
+
}
171
+
172
+
func TestIsSubset(t *testing.T) {
173
+
s1 := Collect(slices.Values([]int{1, 2}))
174
+
s2 := Collect(slices.Values([]int{1, 2, 3}))
175
+
176
+
if !s1.IsSubset(s2) {
177
+
t.Error("s1 should be subset of s2")
178
+
}
179
+
if s2.IsSubset(s1) {
180
+
t.Error("s2 should not be subset of s1")
181
+
}
182
+
}
183
+
184
+
func TestIsSuperset(t *testing.T) {
185
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
186
+
s2 := Collect(slices.Values([]int{1, 2}))
187
+
188
+
if !s1.IsSuperset(s2) {
189
+
t.Error("s1 should be superset of s2")
190
+
}
191
+
if s2.IsSuperset(s1) {
192
+
t.Error("s2 should not be superset of s1")
193
+
}
194
+
}
195
+
196
+
func TestIsDisjoint(t *testing.T) {
197
+
s1 := Collect(slices.Values([]int{1, 2}))
198
+
s2 := Collect(slices.Values([]int{3, 4}))
199
+
s3 := Collect(slices.Values([]int{2, 3}))
200
+
201
+
if !s1.IsDisjoint(s2) {
202
+
t.Error("s1 and s2 should be disjoint")
203
+
}
204
+
if s1.IsDisjoint(s3) {
205
+
t.Error("s1 and s3 should not be disjoint")
206
+
}
207
+
}
208
+
209
+
func TestEqual(t *testing.T) {
210
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
211
+
s2 := Collect(slices.Values([]int{3, 2, 1}))
212
+
s3 := Collect(slices.Values([]int{1, 2}))
213
+
214
+
if !s1.Equal(s2) {
215
+
t.Error("s1 and s2 should be equal")
216
+
}
217
+
if s1.Equal(s3) {
218
+
t.Error("s1 and s3 should not be equal")
219
+
}
220
+
}
221
+
222
+
func TestCollect(t *testing.T) {
223
+
s1 := Collect(slices.Values([]int{1, 2}))
224
+
s2 := Collect(slices.Values([]int{2, 3}))
225
+
226
+
unionSet := Collect(s1.Union(s2))
227
+
if unionSet.Len() != 3 {
228
+
t.Errorf("Expected union set length 3, got %d", unionSet.Len())
229
+
}
230
+
if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) {
231
+
t.Error("Union set should contain 1, 2, and 3")
232
+
}
233
+
234
+
diffSet := Collect(s1.Difference(s2))
235
+
if diffSet.Len() != 1 {
236
+
t.Errorf("Expected difference set length 1, got %d", diffSet.Len())
237
+
}
238
+
if !diffSet.Contains(1) {
239
+
t.Error("Difference set should contain 1")
240
+
}
241
+
}
242
+
243
+
func TestPropertySingleonLen(t *testing.T) {
244
+
f := func(item int) bool {
245
+
single := Singleton(item)
246
+
return single.Len() == 1
247
+
}
248
+
249
+
if err := quick.Check(f, nil); err != nil {
250
+
t.Error(err)
251
+
}
252
+
}
253
+
254
+
func TestPropertyInsertIdempotent(t *testing.T) {
255
+
f := func(s Set[int], item int) bool {
256
+
clone := s.Clone()
257
+
258
+
clone.Insert(item)
259
+
firstLen := clone.Len()
260
+
261
+
clone.Insert(item)
262
+
secondLen := clone.Len()
263
+
264
+
return firstLen == secondLen
265
+
}
266
+
267
+
if err := quick.Check(f, nil); err != nil {
268
+
t.Error(err)
269
+
}
270
+
}
271
+
272
+
func TestPropertyUnionCommutative(t *testing.T) {
273
+
f := func(s1 Set[int], s2 Set[int]) bool {
274
+
union1 := Collect(s1.Union(s2))
275
+
union2 := Collect(s2.Union(s1))
276
+
return union1.Equal(union2)
277
+
}
278
+
279
+
if err := quick.Check(f, nil); err != nil {
280
+
t.Error(err)
281
+
}
282
+
}
283
+
284
+
func TestPropertyIntersectionCommutative(t *testing.T) {
285
+
f := func(s1 Set[int], s2 Set[int]) bool {
286
+
inter1 := Collect(s1.Intersection(s2))
287
+
inter2 := Collect(s2.Intersection(s1))
288
+
return inter1.Equal(inter2)
289
+
}
290
+
291
+
if err := quick.Check(f, nil); err != nil {
292
+
t.Error(err)
293
+
}
294
+
}
295
+
296
+
func TestPropertyCloneEquals(t *testing.T) {
297
+
f := func(s Set[int]) bool {
298
+
clone := s.Clone()
299
+
return s.Equal(clone)
300
+
}
301
+
302
+
if err := quick.Check(f, nil); err != nil {
303
+
t.Error(err)
304
+
}
305
+
}
306
+
307
+
func TestPropertyIntersectionIsSubset(t *testing.T) {
308
+
f := func(s1 Set[int], s2 Set[int]) bool {
309
+
inter := Collect(s1.Intersection(s2))
310
+
return inter.IsSubset(s1) && inter.IsSubset(s2)
311
+
}
312
+
313
+
if err := quick.Check(f, nil); err != nil {
314
+
t.Error(err)
315
+
}
316
+
}
317
+
318
+
func TestPropertyUnionIsSuperset(t *testing.T) {
319
+
f := func(s1 Set[int], s2 Set[int]) bool {
320
+
union := Collect(s1.Union(s2))
321
+
return union.IsSuperset(s1) && union.IsSuperset(s2)
322
+
}
323
+
324
+
if err := quick.Check(f, nil); err != nil {
325
+
t.Error(err)
326
+
}
327
+
}
328
+
329
+
func TestPropertyDifferenceDisjoint(t *testing.T) {
330
+
f := func(s1 Set[int], s2 Set[int]) bool {
331
+
diff := Collect(s1.Difference(s2))
332
+
return diff.IsDisjoint(s2)
333
+
}
334
+
335
+
if err := quick.Check(f, nil); err != nil {
336
+
t.Error(err)
337
+
}
338
+
}
339
+
340
+
func TestPropertySymmetricDifferenceCommutative(t *testing.T) {
341
+
f := func(s1 Set[int], s2 Set[int]) bool {
342
+
symDiff1 := Collect(s1.SymmetricDifference(s2))
343
+
symDiff2 := Collect(s2.SymmetricDifference(s1))
344
+
return symDiff1.Equal(symDiff2)
345
+
}
346
+
347
+
if err := quick.Check(f, nil); err != nil {
348
+
t.Error(err)
349
+
}
350
+
}
351
+
352
+
func TestPropertyRemoveWorks(t *testing.T) {
353
+
f := func(s Set[int], item int) bool {
354
+
clone := s.Clone()
355
+
clone.Insert(item)
356
+
clone.Remove(item)
357
+
return !clone.Contains(item)
358
+
}
359
+
360
+
if err := quick.Check(f, nil); err != nil {
361
+
t.Error(err)
362
+
}
363
+
}
364
+
365
+
func TestPropertyClearEmpty(t *testing.T) {
366
+
f := func(s Set[int]) bool {
367
+
s.Clear()
368
+
return s.IsEmpty() && s.Len() == 0
369
+
}
370
+
371
+
if err := quick.Check(f, nil); err != nil {
372
+
t.Error(err)
373
+
}
374
+
}
375
+
376
+
func TestPropertyIsSubsetReflexive(t *testing.T) {
377
+
f := func(s Set[int]) bool {
378
+
return s.IsSubset(s)
379
+
}
380
+
381
+
if err := quick.Check(f, nil); err != nil {
382
+
t.Error(err)
383
+
}
384
+
}
385
+
386
+
func TestPropertyDeMorganUnion(t *testing.T) {
387
+
f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool {
388
+
// create a universe that contains both sets
389
+
u := universe.Clone()
390
+
for item := range s1.All() {
391
+
u.Insert(item)
392
+
}
393
+
for item := range s2.All() {
394
+
u.Insert(item)
395
+
}
396
+
397
+
// (A u B)' = A' n B'
398
+
union := Collect(s1.Union(s2))
399
+
complementUnion := Collect(u.Difference(union))
400
+
401
+
complementS1 := Collect(u.Difference(s1))
402
+
complementS2 := Collect(u.Difference(s2))
403
+
intersectionComplements := Collect(complementS1.Intersection(complementS2))
404
+
405
+
return complementUnion.Equal(intersectionComplements)
406
+
}
407
+
408
+
if err := quick.Check(f, nil); err != nil {
409
+
t.Error(err)
410
+
}
411
+
}
+9
-8
spindle/engines/nixery/engine.go
+9
-8
spindle/engines/nixery/engine.go
···
73
type addlFields struct {
74
image string
75
container string
76
-
env map[string]string
77
}
78
79
func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
···
103
swf.Steps = append(swf.Steps, sstep)
104
}
105
swf.Name = twf.Name
106
-
addl.env = dwf.Environment
107
addl.image = workflowImage(dwf.Dependencies, e.cfg.NixeryPipelines.Nixery)
108
109
setup := &setupSteps{}
···
288
289
func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error {
290
addl := w.Data.(addlFields)
291
-
workflowEnvs := ConstructEnvs(addl.env)
292
// TODO(winter): should SetupWorkflow also have secret access?
293
// IMO yes, but probably worth thinking on.
294
for _, s := range secrets {
295
workflowEnvs.AddEnv(s.Key, s.Value)
296
}
297
298
-
step := w.Steps[idx].(Step)
299
300
select {
301
case <-ctx.Done():
···
304
}
305
306
envs := append(EnvVars(nil), workflowEnvs...)
307
-
for k, v := range step.environment {
308
-
envs.AddEnv(k, v)
309
}
310
envs.AddEnv("HOME", homeDir)
311
312
mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{
313
-
Cmd: []string{"bash", "-c", step.command},
314
AttachStdout: true,
315
AttachStderr: true,
316
Env: envs,
···
333
// Docker doesn't provide an API to kill an exec run
334
// (sure, we could grab the PID and kill it ourselves,
335
// but that's wasted effort)
336
-
e.l.Warn("step timed out", "step", step.Name)
337
338
<-tailDone
339
···
73
type addlFields struct {
74
image string
75
container string
76
}
77
78
func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
···
102
swf.Steps = append(swf.Steps, sstep)
103
}
104
swf.Name = twf.Name
105
+
swf.Environment = dwf.Environment
106
addl.image = workflowImage(dwf.Dependencies, e.cfg.NixeryPipelines.Nixery)
107
108
setup := &setupSteps{}
···
287
288
func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error {
289
addl := w.Data.(addlFields)
290
+
workflowEnvs := ConstructEnvs(w.Environment)
291
// TODO(winter): should SetupWorkflow also have secret access?
292
// IMO yes, but probably worth thinking on.
293
for _, s := range secrets {
294
workflowEnvs.AddEnv(s.Key, s.Value)
295
}
296
297
+
step := w.Steps[idx]
298
299
select {
300
case <-ctx.Done():
···
303
}
304
305
envs := append(EnvVars(nil), workflowEnvs...)
306
+
if nixStep, ok := step.(Step); ok {
307
+
for k, v := range nixStep.environment {
308
+
envs.AddEnv(k, v)
309
+
}
310
}
311
envs.AddEnv("HOME", homeDir)
312
313
mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{
314
+
Cmd: []string{"bash", "-c", step.Command()},
315
AttachStdout: true,
316
AttachStderr: true,
317
Env: envs,
···
334
// Docker doesn't provide an API to kill an exec run
335
// (sure, we could grab the PID and kill it ourselves,
336
// but that's wasted effort)
337
+
e.l.Warn("step timed out", "step", step.Name())
338
339
<-tailDone
340
+6
-7
spindle/models/clone.go
+6
-7
spindle/models/clone.go
···
55
}
56
}
57
58
-
repoURL := buildRepoURL(tr, dev)
59
60
var cloneOpts tangled.Pipeline_CloneOpts
61
if twf.Clone != nil {
···
101
}
102
}
103
104
-
// buildRepoURL constructs the repository URL from trigger metadata
105
-
func buildRepoURL(tr tangled.Pipeline_TriggerMetadata, devMode bool) string {
106
-
if tr.Repo == nil {
107
return ""
108
}
109
110
-
// Determine protocol
111
scheme := "https://"
112
if devMode {
113
scheme = "http://"
114
}
115
116
// Get host from knot
117
-
host := tr.Repo.Knot
118
119
// In dev mode, replace localhost with host.docker.internal for Docker networking
120
if devMode && strings.Contains(host, "localhost") {
···
122
}
123
124
// Build URL: {scheme}{knot}/{did}/{repo}
125
-
return fmt.Sprintf("%s%s/%s/%s", scheme, host, tr.Repo.Did, tr.Repo.Repo)
126
}
127
128
// buildFetchArgs constructs the arguments for git fetch based on clone options
···
55
}
56
}
57
58
+
repoURL := BuildRepoURL(tr.Repo, dev)
59
60
var cloneOpts tangled.Pipeline_CloneOpts
61
if twf.Clone != nil {
···
101
}
102
}
103
104
+
// BuildRepoURL constructs the repository URL from repo metadata.
105
+
func BuildRepoURL(repo *tangled.Pipeline_TriggerRepo, devMode bool) string {
106
+
if repo == nil {
107
return ""
108
}
109
110
scheme := "https://"
111
if devMode {
112
scheme = "http://"
113
}
114
115
// Get host from knot
116
+
host := repo.Knot
117
118
// In dev mode, replace localhost with host.docker.internal for Docker networking
119
if devMode && strings.Contains(host, "localhost") {
···
121
}
122
123
// Build URL: {scheme}{knot}/{did}/{repo}
124
+
return fmt.Sprintf("%s%s/%s/%s", scheme, host, repo.Did, repo.Repo)
125
}
126
127
// buildFetchArgs constructs the arguments for git fetch based on clone options
+4
-3
spindle/models/pipeline.go
+4
-3
spindle/models/pipeline.go
+77
spindle/models/pipeline_env.go
+77
spindle/models/pipeline_env.go
···
···
1
+
package models
2
+
3
+
import (
4
+
"strings"
5
+
6
+
"github.com/go-git/go-git/v5/plumbing"
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
// PipelineEnvVars extracts environment variables from pipeline trigger metadata.
12
+
// These are framework-provided variables that are injected into workflow steps.
13
+
func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId, devMode bool) map[string]string {
14
+
if tr == nil {
15
+
return nil
16
+
}
17
+
18
+
env := make(map[string]string)
19
+
20
+
// Standard CI environment variable
21
+
env["CI"] = "true"
22
+
23
+
env["TANGLED_PIPELINE_ID"] = pipelineId.Rkey
24
+
25
+
// Repo info
26
+
if tr.Repo != nil {
27
+
env["TANGLED_REPO_KNOT"] = tr.Repo.Knot
28
+
env["TANGLED_REPO_DID"] = tr.Repo.Did
29
+
env["TANGLED_REPO_NAME"] = tr.Repo.Repo
30
+
env["TANGLED_REPO_DEFAULT_BRANCH"] = tr.Repo.DefaultBranch
31
+
env["TANGLED_REPO_URL"] = BuildRepoURL(tr.Repo, devMode)
32
+
}
33
+
34
+
switch workflow.TriggerKind(tr.Kind) {
35
+
case workflow.TriggerKindPush:
36
+
if tr.Push != nil {
37
+
refName := plumbing.ReferenceName(tr.Push.Ref)
38
+
refType := "branch"
39
+
if refName.IsTag() {
40
+
refType = "tag"
41
+
}
42
+
43
+
env["TANGLED_REF"] = tr.Push.Ref
44
+
env["TANGLED_REF_NAME"] = refName.Short()
45
+
env["TANGLED_REF_TYPE"] = refType
46
+
env["TANGLED_SHA"] = tr.Push.NewSha
47
+
env["TANGLED_COMMIT_SHA"] = tr.Push.NewSha
48
+
}
49
+
50
+
case workflow.TriggerKindPullRequest:
51
+
if tr.PullRequest != nil {
52
+
// For PRs, the "ref" is the source branch
53
+
env["TANGLED_REF"] = "refs/heads/" + tr.PullRequest.SourceBranch
54
+
env["TANGLED_REF_NAME"] = tr.PullRequest.SourceBranch
55
+
env["TANGLED_REF_TYPE"] = "branch"
56
+
env["TANGLED_SHA"] = tr.PullRequest.SourceSha
57
+
env["TANGLED_COMMIT_SHA"] = tr.PullRequest.SourceSha
58
+
59
+
// PR-specific variables
60
+
env["TANGLED_PR_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch
61
+
env["TANGLED_PR_TARGET_BRANCH"] = tr.PullRequest.TargetBranch
62
+
env["TANGLED_PR_SOURCE_SHA"] = tr.PullRequest.SourceSha
63
+
env["TANGLED_PR_ACTION"] = tr.PullRequest.Action
64
+
}
65
+
66
+
case workflow.TriggerKindManual:
67
+
// Manual triggers may not have ref/sha info
68
+
// Include any manual inputs if present
69
+
if tr.Manual != nil {
70
+
for _, pair := range tr.Manual.Inputs {
71
+
env["TANGLED_INPUT_"+strings.ToUpper(pair.Key)] = pair.Value
72
+
}
73
+
}
74
+
}
75
+
76
+
return env
77
+
}
+260
spindle/models/pipeline_env_test.go
+260
spindle/models/pipeline_env_test.go
···
···
1
+
package models
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"tangled.org/core/api/tangled"
7
+
"tangled.org/core/workflow"
8
+
)
9
+
10
+
func TestPipelineEnvVars_PushBranch(t *testing.T) {
11
+
tr := &tangled.Pipeline_TriggerMetadata{
12
+
Kind: string(workflow.TriggerKindPush),
13
+
Push: &tangled.Pipeline_PushTriggerData{
14
+
NewSha: "abc123def456",
15
+
OldSha: "000000000000",
16
+
Ref: "refs/heads/main",
17
+
},
18
+
Repo: &tangled.Pipeline_TriggerRepo{
19
+
Knot: "example.com",
20
+
Did: "did:plc:user123",
21
+
Repo: "my-repo",
22
+
DefaultBranch: "main",
23
+
},
24
+
}
25
+
id := PipelineId{
26
+
Knot: "example.com",
27
+
Rkey: "123123",
28
+
}
29
+
env := PipelineEnvVars(tr, id, false)
30
+
31
+
// Check standard CI variable
32
+
if env["CI"] != "true" {
33
+
t.Errorf("Expected CI='true', got '%s'", env["CI"])
34
+
}
35
+
36
+
// Check ref variables
37
+
if env["TANGLED_REF"] != "refs/heads/main" {
38
+
t.Errorf("Expected TANGLED_REF='refs/heads/main', got '%s'", env["TANGLED_REF"])
39
+
}
40
+
if env["TANGLED_REF_NAME"] != "main" {
41
+
t.Errorf("Expected TANGLED_REF_NAME='main', got '%s'", env["TANGLED_REF_NAME"])
42
+
}
43
+
if env["TANGLED_REF_TYPE"] != "branch" {
44
+
t.Errorf("Expected TANGLED_REF_TYPE='branch', got '%s'", env["TANGLED_REF_TYPE"])
45
+
}
46
+
47
+
// Check SHA variables
48
+
if env["TANGLED_SHA"] != "abc123def456" {
49
+
t.Errorf("Expected TANGLED_SHA='abc123def456', got '%s'", env["TANGLED_SHA"])
50
+
}
51
+
if env["TANGLED_COMMIT_SHA"] != "abc123def456" {
52
+
t.Errorf("Expected TANGLED_COMMIT_SHA='abc123def456', got '%s'", env["TANGLED_COMMIT_SHA"])
53
+
}
54
+
55
+
// Check repo variables
56
+
if env["TANGLED_REPO_KNOT"] != "example.com" {
57
+
t.Errorf("Expected TANGLED_REPO_KNOT='example.com', got '%s'", env["TANGLED_REPO_KNOT"])
58
+
}
59
+
if env["TANGLED_REPO_DID"] != "did:plc:user123" {
60
+
t.Errorf("Expected TANGLED_REPO_DID='did:plc:user123', got '%s'", env["TANGLED_REPO_DID"])
61
+
}
62
+
if env["TANGLED_REPO_NAME"] != "my-repo" {
63
+
t.Errorf("Expected TANGLED_REPO_NAME='my-repo', got '%s'", env["TANGLED_REPO_NAME"])
64
+
}
65
+
if env["TANGLED_REPO_DEFAULT_BRANCH"] != "main" {
66
+
t.Errorf("Expected TANGLED_REPO_DEFAULT_BRANCH='main', got '%s'", env["TANGLED_REPO_DEFAULT_BRANCH"])
67
+
}
68
+
if env["TANGLED_REPO_URL"] != "https://example.com/did:plc:user123/my-repo" {
69
+
t.Errorf("Expected TANGLED_REPO_URL='https://example.com/did:plc:user123/my-repo', got '%s'", env["TANGLED_REPO_URL"])
70
+
}
71
+
}
72
+
73
+
func TestPipelineEnvVars_PushTag(t *testing.T) {
74
+
tr := &tangled.Pipeline_TriggerMetadata{
75
+
Kind: string(workflow.TriggerKindPush),
76
+
Push: &tangled.Pipeline_PushTriggerData{
77
+
NewSha: "abc123def456",
78
+
OldSha: "000000000000",
79
+
Ref: "refs/tags/v1.2.3",
80
+
},
81
+
Repo: &tangled.Pipeline_TriggerRepo{
82
+
Knot: "example.com",
83
+
Did: "did:plc:user123",
84
+
Repo: "my-repo",
85
+
},
86
+
}
87
+
id := PipelineId{
88
+
Knot: "example.com",
89
+
Rkey: "123123",
90
+
}
91
+
env := PipelineEnvVars(tr, id, false)
92
+
93
+
if env["TANGLED_REF"] != "refs/tags/v1.2.3" {
94
+
t.Errorf("Expected TANGLED_REF='refs/tags/v1.2.3', got '%s'", env["TANGLED_REF"])
95
+
}
96
+
if env["TANGLED_REF_NAME"] != "v1.2.3" {
97
+
t.Errorf("Expected TANGLED_REF_NAME='v1.2.3', got '%s'", env["TANGLED_REF_NAME"])
98
+
}
99
+
if env["TANGLED_REF_TYPE"] != "tag" {
100
+
t.Errorf("Expected TANGLED_REF_TYPE='tag', got '%s'", env["TANGLED_REF_TYPE"])
101
+
}
102
+
}
103
+
104
+
func TestPipelineEnvVars_PullRequest(t *testing.T) {
105
+
tr := &tangled.Pipeline_TriggerMetadata{
106
+
Kind: string(workflow.TriggerKindPullRequest),
107
+
PullRequest: &tangled.Pipeline_PullRequestTriggerData{
108
+
SourceBranch: "feature-branch",
109
+
TargetBranch: "main",
110
+
SourceSha: "pr-sha-789",
111
+
Action: "opened",
112
+
},
113
+
Repo: &tangled.Pipeline_TriggerRepo{
114
+
Knot: "example.com",
115
+
Did: "did:plc:user123",
116
+
Repo: "my-repo",
117
+
},
118
+
}
119
+
id := PipelineId{
120
+
Knot: "example.com",
121
+
Rkey: "123123",
122
+
}
123
+
env := PipelineEnvVars(tr, id, false)
124
+
125
+
// Check ref variables for PR
126
+
if env["TANGLED_REF"] != "refs/heads/feature-branch" {
127
+
t.Errorf("Expected TANGLED_REF='refs/heads/feature-branch', got '%s'", env["TANGLED_REF"])
128
+
}
129
+
if env["TANGLED_REF_NAME"] != "feature-branch" {
130
+
t.Errorf("Expected TANGLED_REF_NAME='feature-branch', got '%s'", env["TANGLED_REF_NAME"])
131
+
}
132
+
if env["TANGLED_REF_TYPE"] != "branch" {
133
+
t.Errorf("Expected TANGLED_REF_TYPE='branch', got '%s'", env["TANGLED_REF_TYPE"])
134
+
}
135
+
136
+
// Check SHA variables
137
+
if env["TANGLED_SHA"] != "pr-sha-789" {
138
+
t.Errorf("Expected TANGLED_SHA='pr-sha-789', got '%s'", env["TANGLED_SHA"])
139
+
}
140
+
if env["TANGLED_COMMIT_SHA"] != "pr-sha-789" {
141
+
t.Errorf("Expected TANGLED_COMMIT_SHA='pr-sha-789', got '%s'", env["TANGLED_COMMIT_SHA"])
142
+
}
143
+
144
+
// Check PR-specific variables
145
+
if env["TANGLED_PR_SOURCE_BRANCH"] != "feature-branch" {
146
+
t.Errorf("Expected TANGLED_PR_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PR_SOURCE_BRANCH"])
147
+
}
148
+
if env["TANGLED_PR_TARGET_BRANCH"] != "main" {
149
+
t.Errorf("Expected TANGLED_PR_TARGET_BRANCH='main', got '%s'", env["TANGLED_PR_TARGET_BRANCH"])
150
+
}
151
+
if env["TANGLED_PR_SOURCE_SHA"] != "pr-sha-789" {
152
+
t.Errorf("Expected TANGLED_PR_SOURCE_SHA='pr-sha-789', got '%s'", env["TANGLED_PR_SOURCE_SHA"])
153
+
}
154
+
if env["TANGLED_PR_ACTION"] != "opened" {
155
+
t.Errorf("Expected TANGLED_PR_ACTION='opened', got '%s'", env["TANGLED_PR_ACTION"])
156
+
}
157
+
}
158
+
159
+
func TestPipelineEnvVars_ManualWithInputs(t *testing.T) {
160
+
tr := &tangled.Pipeline_TriggerMetadata{
161
+
Kind: string(workflow.TriggerKindManual),
162
+
Manual: &tangled.Pipeline_ManualTriggerData{
163
+
Inputs: []*tangled.Pipeline_Pair{
164
+
{Key: "version", Value: "1.0.0"},
165
+
{Key: "environment", Value: "production"},
166
+
},
167
+
},
168
+
Repo: &tangled.Pipeline_TriggerRepo{
169
+
Knot: "example.com",
170
+
Did: "did:plc:user123",
171
+
Repo: "my-repo",
172
+
},
173
+
}
174
+
id := PipelineId{
175
+
Knot: "example.com",
176
+
Rkey: "123123",
177
+
}
178
+
env := PipelineEnvVars(tr, id, false)
179
+
180
+
// Check manual input variables
181
+
if env["TANGLED_INPUT_VERSION"] != "1.0.0" {
182
+
t.Errorf("Expected TANGLED_INPUT_VERSION='1.0.0', got '%s'", env["TANGLED_INPUT_VERSION"])
183
+
}
184
+
if env["TANGLED_INPUT_ENVIRONMENT"] != "production" {
185
+
t.Errorf("Expected TANGLED_INPUT_ENVIRONMENT='production', got '%s'", env["TANGLED_INPUT_ENVIRONMENT"])
186
+
}
187
+
188
+
// Manual triggers shouldn't have ref/sha variables
189
+
if _, ok := env["TANGLED_REF"]; ok {
190
+
t.Error("Manual trigger should not have TANGLED_REF")
191
+
}
192
+
if _, ok := env["TANGLED_SHA"]; ok {
193
+
t.Error("Manual trigger should not have TANGLED_SHA")
194
+
}
195
+
}
196
+
197
+
func TestPipelineEnvVars_DevMode(t *testing.T) {
198
+
tr := &tangled.Pipeline_TriggerMetadata{
199
+
Kind: string(workflow.TriggerKindPush),
200
+
Push: &tangled.Pipeline_PushTriggerData{
201
+
NewSha: "abc123",
202
+
Ref: "refs/heads/main",
203
+
},
204
+
Repo: &tangled.Pipeline_TriggerRepo{
205
+
Knot: "localhost:3000",
206
+
Did: "did:plc:user123",
207
+
Repo: "my-repo",
208
+
},
209
+
}
210
+
id := PipelineId{
211
+
Knot: "example.com",
212
+
Rkey: "123123",
213
+
}
214
+
env := PipelineEnvVars(tr, id, true)
215
+
216
+
// Dev mode should use http:// and replace localhost with host.docker.internal
217
+
expectedURL := "http://host.docker.internal:3000/did:plc:user123/my-repo"
218
+
if env["TANGLED_REPO_URL"] != expectedURL {
219
+
t.Errorf("Expected TANGLED_REPO_URL='%s', got '%s'", expectedURL, env["TANGLED_REPO_URL"])
220
+
}
221
+
}
222
+
223
+
func TestPipelineEnvVars_NilTrigger(t *testing.T) {
224
+
id := PipelineId{
225
+
Knot: "example.com",
226
+
Rkey: "123123",
227
+
}
228
+
env := PipelineEnvVars(nil, id, false)
229
+
230
+
if env != nil {
231
+
t.Error("Expected nil env for nil trigger")
232
+
}
233
+
}
234
+
235
+
func TestPipelineEnvVars_NilPushData(t *testing.T) {
236
+
tr := &tangled.Pipeline_TriggerMetadata{
237
+
Kind: string(workflow.TriggerKindPush),
238
+
Push: nil,
239
+
Repo: &tangled.Pipeline_TriggerRepo{
240
+
Knot: "example.com",
241
+
Did: "did:plc:user123",
242
+
Repo: "my-repo",
243
+
},
244
+
}
245
+
id := PipelineId{
246
+
Knot: "example.com",
247
+
Rkey: "123123",
248
+
}
249
+
env := PipelineEnvVars(tr, id, false)
250
+
251
+
// Should still have repo variables
252
+
if env["TANGLED_REPO_KNOT"] != "example.com" {
253
+
t.Errorf("Expected TANGLED_REPO_KNOT='example.com', got '%s'", env["TANGLED_REPO_KNOT"])
254
+
}
255
+
256
+
// Should not have ref/sha variables
257
+
if _, ok := env["TANGLED_REF"]; ok {
258
+
t.Error("Should not have TANGLED_REF when push data is nil")
259
+
}
260
+
}
+11
spindle/server.go
+11
spindle/server.go
···
6
"encoding/json"
7
"fmt"
8
"log/slog"
9
"net/http"
10
11
"github.com/go-chi/chi/v5"
···
311
312
workflows := make(map[models.Engine][]models.Workflow)
313
314
for _, w := range tpl.Workflows {
315
if w != nil {
316
if _, ok := s.engs[w.Engine]; !ok {
···
335
if err != nil {
336
return err
337
}
338
339
workflows[eng] = append(workflows[eng], *ewf)
340
···
6
"encoding/json"
7
"fmt"
8
"log/slog"
9
+
"maps"
10
"net/http"
11
12
"github.com/go-chi/chi/v5"
···
312
313
workflows := make(map[models.Engine][]models.Workflow)
314
315
+
// Build pipeline environment variables once for all workflows
316
+
pipelineEnv := models.PipelineEnvVars(tpl.TriggerMetadata, pipelineId, s.cfg.Server.Dev)
317
+
318
for _, w := range tpl.Workflows {
319
if w != nil {
320
if _, ok := s.engs[w.Engine]; !ok {
···
339
if err != nil {
340
return err
341
}
342
+
343
+
// inject TANGLED_* env vars after InitWorkflow
344
+
// This prevents user-defined env vars from overriding them
345
+
if ewf.Environment == nil {
346
+
ewf.Environment = make(map[string]string)
347
+
}
348
+
maps.Copy(ewf.Environment, pipelineEnv)
349
350
workflows[eng] = append(workflows[eng], *ewf)
351
+199
types/commit.go
+199
types/commit.go
···
···
1
+
package types
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/json"
6
+
"fmt"
7
+
"maps"
8
+
"regexp"
9
+
"strings"
10
+
11
+
"github.com/go-git/go-git/v5/plumbing"
12
+
"github.com/go-git/go-git/v5/plumbing/object"
13
+
)
14
+
15
+
type Commit struct {
16
+
// hash of the commit object.
17
+
Hash plumbing.Hash `json:"hash,omitempty"`
18
+
19
+
// author is the original author of the commit.
20
+
Author object.Signature `json:"author"`
21
+
22
+
// committer is the one performing the commit, might be different from author.
23
+
Committer object.Signature `json:"committer"`
24
+
25
+
// message is the commit message, contains arbitrary text.
26
+
Message string `json:"message"`
27
+
28
+
// treehash is the hash of the root tree of the commit.
29
+
Tree string `json:"tree"`
30
+
31
+
// parents are the hashes of the parent commits of the commit.
32
+
ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`
33
+
34
+
// pgpsignature is the pgp signature of the commit.
35
+
PGPSignature string `json:"pgp_signature,omitempty"`
36
+
37
+
// mergetag is the embedded tag object when a merge commit is created by
38
+
// merging a signed tag.
39
+
MergeTag string `json:"merge_tag,omitempty"`
40
+
41
+
// changeid is a unique identifier for the change (e.g., gerrit change-id).
42
+
ChangeId string `json:"change_id,omitempty"`
43
+
44
+
// extraheaders contains additional headers not captured by other fields.
45
+
ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`
46
+
47
+
// deprecated: kept for backwards compatibility with old json format.
48
+
This string `json:"this,omitempty"`
49
+
50
+
// deprecated: kept for backwards compatibility with old json format.
51
+
Parent string `json:"parent,omitempty"`
52
+
}
53
+
54
+
// types.Commit is an unify two commit structs:
55
+
// - git.object.Commit from
56
+
// - types.NiceDiff.commit
57
+
//
58
+
// to do this in backwards compatible fashion, we define the base struct
59
+
// to use the same fields as NiceDiff.Commit, and then we also unmarshal
60
+
// the struct fields from go-git structs, this custom unmarshal makes sense
61
+
// of both representations and unifies them to have maximal data in either
62
+
// form.
63
+
func (c *Commit) UnmarshalJSON(data []byte) error {
64
+
type Alias Commit
65
+
66
+
aux := &struct {
67
+
*object.Commit
68
+
*Alias
69
+
}{
70
+
Alias: (*Alias)(c),
71
+
}
72
+
73
+
if err := json.Unmarshal(data, aux); err != nil {
74
+
return err
75
+
}
76
+
77
+
c.FromGoGitCommit(aux.Commit)
78
+
79
+
return nil
80
+
}
81
+
82
+
// fill in as much of Commit as possible from the given go-git commit
83
+
func (c *Commit) FromGoGitCommit(gc *object.Commit) {
84
+
if gc == nil {
85
+
return
86
+
}
87
+
88
+
if c.Hash.IsZero() {
89
+
c.Hash = gc.Hash
90
+
}
91
+
if c.This == "" {
92
+
c.This = gc.Hash.String()
93
+
}
94
+
if isEmptySignature(c.Author) {
95
+
c.Author = gc.Author
96
+
}
97
+
if isEmptySignature(c.Committer) {
98
+
c.Committer = gc.Committer
99
+
}
100
+
if c.Message == "" {
101
+
c.Message = gc.Message
102
+
}
103
+
if c.Tree == "" {
104
+
c.Tree = gc.TreeHash.String()
105
+
}
106
+
if c.PGPSignature == "" {
107
+
c.PGPSignature = gc.PGPSignature
108
+
}
109
+
if c.MergeTag == "" {
110
+
c.MergeTag = gc.MergeTag
111
+
}
112
+
113
+
if len(c.ParentHashes) == 0 {
114
+
c.ParentHashes = gc.ParentHashes
115
+
}
116
+
if c.Parent == "" && len(gc.ParentHashes) > 0 {
117
+
c.Parent = gc.ParentHashes[0].String()
118
+
}
119
+
120
+
if len(c.ExtraHeaders) == 0 {
121
+
c.ExtraHeaders = make(map[string][]byte)
122
+
maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
123
+
}
124
+
125
+
if c.ChangeId == "" {
126
+
if v, ok := gc.ExtraHeaders["change-id"]; ok {
127
+
c.ChangeId = string(v)
128
+
}
129
+
}
130
+
}
131
+
132
+
func isEmptySignature(s object.Signature) bool {
133
+
return s.Email == "" && s.Name == "" && s.When.IsZero()
134
+
}
135
+
136
+
// produce a verifiable payload from this commit's metadata
137
+
func (c *Commit) Payload() string {
138
+
author := bytes.NewBuffer([]byte{})
139
+
c.Author.Encode(author)
140
+
141
+
committer := bytes.NewBuffer([]byte{})
142
+
c.Committer.Encode(committer)
143
+
144
+
payload := strings.Builder{}
145
+
146
+
fmt.Fprintf(&payload, "tree %s\n", c.Tree)
147
+
148
+
if len(c.ParentHashes) > 0 {
149
+
for _, p := range c.ParentHashes {
150
+
fmt.Fprintf(&payload, "parent %s\n", p.String())
151
+
}
152
+
} else {
153
+
// present for backwards compatibility
154
+
fmt.Fprintf(&payload, "parent %s\n", c.Parent)
155
+
}
156
+
157
+
fmt.Fprintf(&payload, "author %s\n", author.String())
158
+
fmt.Fprintf(&payload, "committer %s\n", committer.String())
159
+
160
+
if c.ChangeId != "" {
161
+
fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
162
+
} else if v, ok := c.ExtraHeaders["change-id"]; ok {
163
+
fmt.Fprintf(&payload, "change-id %s\n", string(v))
164
+
}
165
+
166
+
fmt.Fprintf(&payload, "\n%s", c.Message)
167
+
168
+
return payload.String()
169
+
}
170
+
171
+
var (
172
+
coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
173
+
)
174
+
175
+
func (commit Commit) CoAuthors() []object.Signature {
176
+
var coAuthors []object.Signature
177
+
seen := make(map[string]bool)
178
+
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
+
180
+
for _, match := range matches {
181
+
if len(match) >= 3 {
182
+
name := strings.TrimSpace(match[1])
183
+
email := strings.TrimSpace(match[2])
184
+
185
+
if seen[email] {
186
+
continue
187
+
}
188
+
seen[email] = true
189
+
190
+
coAuthors = append(coAuthors, object.Signature{
191
+
Name: name,
192
+
Email: email,
193
+
When: commit.Committer.When,
194
+
})
195
+
}
196
+
}
197
+
198
+
return coAuthors
199
+
}
+2
-12
types/diff.go
+2
-12
types/diff.go
···
2
3
import (
4
"github.com/bluekeyes/go-gitdiff/gitdiff"
5
-
"github.com/go-git/go-git/v5/plumbing/object"
6
)
7
8
type DiffOpts struct {
···
43
44
// A nicer git diff representation.
45
type NiceDiff struct {
46
-
Commit struct {
47
-
Message string `json:"message"`
48
-
Author object.Signature `json:"author"`
49
-
This string `json:"this"`
50
-
Parent string `json:"parent"`
51
-
PGPSignature string `json:"pgp_signature"`
52
-
Committer object.Signature `json:"committer"`
53
-
Tree string `json:"tree"`
54
-
ChangedId string `json:"change_id"`
55
-
} `json:"commit"`
56
-
Stat struct {
57
FilesChanged int `json:"files_changed"`
58
Insertions int `json:"insertions"`
59
Deletions int `json:"deletions"`
···
2
3
import (
4
"github.com/bluekeyes/go-gitdiff/gitdiff"
5
)
6
7
type DiffOpts struct {
···
42
43
// A nicer git diff representation.
44
type NiceDiff struct {
45
+
Commit Commit `json:"commit"`
46
+
Stat struct {
47
FilesChanged int `json:"files_changed"`
48
Insertions int `json:"insertions"`
49
Deletions int `json:"deletions"`
+17
-17
types/repo.go
+17
-17
types/repo.go
···
8
)
9
10
type RepoIndexResponse struct {
11
-
IsEmpty bool `json:"is_empty"`
12
-
Ref string `json:"ref,omitempty"`
13
-
Readme string `json:"readme,omitempty"`
14
-
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
-
Commits []*object.Commit `json:"commits,omitempty"`
16
-
Description string `json:"description,omitempty"`
17
-
Files []NiceTree `json:"files,omitempty"`
18
-
Branches []Branch `json:"branches,omitempty"`
19
-
Tags []*TagReference `json:"tags,omitempty"`
20
-
TotalCommits int `json:"total_commits,omitempty"`
21
}
22
23
type RepoLogResponse struct {
24
-
Commits []*object.Commit `json:"commits,omitempty"`
25
-
Ref string `json:"ref,omitempty"`
26
-
Description string `json:"description,omitempty"`
27
-
Log bool `json:"log,omitempty"`
28
-
Total int `json:"total,omitempty"`
29
-
Page int `json:"page,omitempty"`
30
-
PerPage int `json:"per_page,omitempty"`
31
}
32
33
type RepoCommitResponse struct {
···
8
)
9
10
type RepoIndexResponse struct {
11
+
IsEmpty bool `json:"is_empty"`
12
+
Ref string `json:"ref,omitempty"`
13
+
Readme string `json:"readme,omitempty"`
14
+
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
+
Commits []Commit `json:"commits,omitempty"`
16
+
Description string `json:"description,omitempty"`
17
+
Files []NiceTree `json:"files,omitempty"`
18
+
Branches []Branch `json:"branches,omitempty"`
19
+
Tags []*TagReference `json:"tags,omitempty"`
20
+
TotalCommits int `json:"total_commits,omitempty"`
21
}
22
23
type RepoLogResponse struct {
24
+
Commits []Commit `json:"commits,omitempty"`
25
+
Ref string `json:"ref,omitempty"`
26
+
Description string `json:"description,omitempty"`
27
+
Log bool `json:"log,omitempty"`
28
+
Total int `json:"total,omitempty"`
29
+
Page int `json:"page,omitempty"`
30
+
PerPage int `json:"per_page,omitempty"`
31
}
32
33
type RepoCommitResponse struct {