forked from tangled.org/core
Monorepo for Tangled

Compare changes

Choose any two refs to compare.

Changed files
+4100 -1121
api
appview
crypto
hook
jetstream
knotserver
lexicons
nix
orm
patchutil
sets
spindle
types
+649 -8
api/tangled/cbor_gen.go
··· 6938 6938 } 6939 6939 6940 6940 cw := cbg.NewCborWriter(w) 6941 - fieldCount := 5 6941 + fieldCount := 7 6942 6942 6943 6943 if t.Body == nil { 6944 + fieldCount-- 6945 + } 6946 + 6947 + if t.Mentions == nil { 6948 + fieldCount-- 6949 + } 6950 + 6951 + if t.References == nil { 6944 6952 fieldCount-- 6945 6953 } 6946 6954 ··· 7045 7053 return err 7046 7054 } 7047 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 + 7048 7092 // t.CreatedAt (string) (string) 7049 7093 if len("createdAt") > 1000000 { 7050 7094 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 7067 7111 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 7068 7112 return err 7069 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 + } 7070 7150 return nil 7071 7151 } 7072 7152 ··· 7095 7175 7096 7176 n := extra 7097 7177 7098 - nameBuf := make([]byte, 9) 7178 + nameBuf := make([]byte, 10) 7099 7179 for i := uint64(0); i < n; i++ { 7100 7180 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7101 7181 if err != nil { ··· 7164 7244 } 7165 7245 7166 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 + } 7167 7287 } 7168 7288 // t.CreatedAt (string) (string) 7169 7289 case "createdAt": ··· 7176 7296 7177 7297 t.CreatedAt = string(sval) 7178 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 + } 7179 7339 7180 7340 default: 7181 7341 // Field doesn't exist on this type, so ignore it ··· 7194 7354 } 7195 7355 7196 7356 cw := cbg.NewCborWriter(w) 7197 - fieldCount := 5 7357 + fieldCount := 7 7358 + 7359 + if t.Mentions == nil { 7360 + fieldCount-- 7361 + } 7362 + 7363 + if t.References == nil { 7364 + fieldCount-- 7365 + } 7198 7366 7199 7367 if t.ReplyTo == nil { 7200 7368 fieldCount-- ··· 7301 7469 } 7302 7470 } 7303 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 + 7304 7508 // t.CreatedAt (string) (string) 7305 7509 if len("createdAt") > 1000000 { 7306 7510 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 7323 7527 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 7324 7528 return err 7325 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 + } 7326 7566 return nil 7327 7567 } 7328 7568 ··· 7351 7591 7352 7592 n := extra 7353 7593 7354 - nameBuf := make([]byte, 9) 7594 + nameBuf := make([]byte, 10) 7355 7595 for i := uint64(0); i < n; i++ { 7356 7596 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7357 7597 if err != nil { ··· 7419 7659 } 7420 7660 7421 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 + 7422 7702 } 7423 7703 } 7424 7704 // t.CreatedAt (string) (string) ··· 7431 7711 } 7432 7712 7433 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 + } 7434 7754 } 7435 7755 7436 7756 default: ··· 7614 7934 } 7615 7935 7616 7936 cw := cbg.NewCborWriter(w) 7617 - fieldCount := 7 7937 + fieldCount := 9 7618 7938 7619 7939 if t.Body == nil { 7940 + fieldCount-- 7941 + } 7942 + 7943 + if t.Mentions == nil { 7944 + fieldCount-- 7945 + } 7946 + 7947 + if t.References == nil { 7620 7948 fieldCount-- 7621 7949 } 7622 7950 ··· 7760 8088 return err 7761 8089 } 7762 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 + 7763 8127 // t.CreatedAt (string) (string) 7764 8128 if len("createdAt") > 1000000 { 7765 8129 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 7782 8146 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 7783 8147 return err 7784 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 + } 7785 8185 return nil 7786 8186 } 7787 8187 ··· 7810 8210 7811 8211 n := extra 7812 8212 7813 - nameBuf := make([]byte, 9) 8213 + nameBuf := make([]byte, 10) 7814 8214 for i := uint64(0); i < n; i++ { 7815 8215 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7816 8216 if err != nil { ··· 7919 8319 } 7920 8320 } 7921 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 + } 7922 8362 } 7923 8363 // t.CreatedAt (string) (string) 7924 8364 case "createdAt": ··· 7931 8371 7932 8372 t.CreatedAt = string(sval) 7933 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 + } 7934 8414 7935 8415 default: 7936 8416 // Field doesn't exist on this type, so ignore it ··· 7949 8429 } 7950 8430 7951 8431 cw := cbg.NewCborWriter(w) 8432 + fieldCount := 6 7952 8433 7953 - if _, err := cw.Write([]byte{164}); err != nil { 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 { 7954 8443 return err 7955 8444 } 7956 8445 ··· 8019 8508 return err 8020 8509 } 8021 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 + 8022 8547 // t.CreatedAt (string) (string) 8023 8548 if len("createdAt") > 1000000 { 8024 8549 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 8040 8565 } 8041 8566 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 8042 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 + } 8043 8604 } 8044 8605 return nil 8045 8606 } ··· 8069 8630 8070 8631 n := extra 8071 8632 8072 - nameBuf := make([]byte, 9) 8633 + nameBuf := make([]byte, 10) 8073 8634 for i := uint64(0); i < n; i++ { 8074 8635 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 8075 8636 if err != nil { ··· 8118 8679 8119 8680 t.LexiconTypeID = string(sval) 8120 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 + } 8121 8722 // t.CreatedAt (string) (string) 8122 8723 case "createdAt": 8123 8724 ··· 8128 8729 } 8129 8730 8130 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 + } 8131 8772 } 8132 8773 8133 8774 default:
+7 -5
api/tangled/issuecomment.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoIssueComment 19 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"` 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"` 25 27 }
+6 -4
api/tangled/pullcomment.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPullComment 19 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"` 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"` 24 26 }
+7 -5
api/tangled/repoissue.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoIssue 19 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"` 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"` 25 27 }
+2
api/tangled/repopull.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 23 24 Patch string `json:"patch" cborgen:"patch"` 25 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 24 26 Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 25 27 Target *RepoPull_Target `json:"target" cborgen:"target"` 26 28 Title string `json:"title" cborgen:"title"`
+6 -45
appview/commitverify/verify.go
··· 3 3 import ( 4 4 "log" 5 5 6 - "github.com/go-git/go-git/v5/plumbing/object" 7 6 "tangled.org/core/appview/db" 8 7 "tangled.org/core/appview/models" 9 8 "tangled.org/core/crypto" ··· 35 34 return "" 36 35 } 37 36 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) { 37 + func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) { 47 38 vcs := VerifiedCommits{} 48 39 49 40 didPubkeyCache := make(map[string][]models.PublicKey) 50 41 51 42 for _, commit := range ndCommits { 52 - c := commit.Commit 53 - 54 - committerEmail := c.Committer.Email 43 + committerEmail := commit.Committer.Email 55 44 if did, exists := emailToDid[committerEmail]; exists { 56 45 // check if we've already fetched public keys for this did 57 46 pubKeys, ok := didPubkeyCache[did] ··· 67 56 } 68 57 69 58 // try to verify with any associated pubkeys 59 + payload := commit.Payload() 60 + signature := commit.PGPSignature 70 61 for _, pk := range pubKeys { 71 - if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok { 62 + if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok { 72 63 73 64 fp, err := crypto.SSHFingerprint(pk.Key) 74 65 if err != nil { 75 66 log.Println("error computing ssh fingerprint:", err) 76 67 } 77 68 78 - vc := verifiedCommit{fingerprint: fp, hash: c.This} 69 + vc := verifiedCommit{fingerprint: fp, hash: commit.This} 79 70 vcs[vc] = struct{}{} 80 71 break 81 72 } ··· 86 77 87 78 return vcs, nil 88 79 } 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 -2
appview/db/artifact.go
··· 8 8 "github.com/go-git/go-git/v5/plumbing" 9 9 "github.com/ipfs/go-cid" 10 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 11 12 ) 12 13 13 14 func AddArtifact(e Execer, artifact models.Artifact) error { ··· 37 38 return err 38 39 } 39 40 40 - func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) { 41 + func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) { 41 42 var artifacts []models.Artifact 42 43 43 44 var conditions []string ··· 109 110 return artifacts, nil 110 111 } 111 112 112 - func DeleteArtifact(e Execer, filters ...filter) error { 113 + func DeleteArtifact(e Execer, filters ...orm.Filter) error { 113 114 var conditions []string 114 115 var args []any 115 116 for _, filter := range filters {
+4 -3
appview/db/collaborators.go
··· 6 6 "time" 7 7 8 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 9 10 ) 10 11 11 12 func AddCollaborator(e Execer, c models.Collaborator) error { ··· 16 17 return err 17 18 } 18 19 19 - func DeleteCollaborator(e Execer, filters ...filter) error { 20 + func DeleteCollaborator(e Execer, filters ...orm.Filter) error { 20 21 var conditions []string 21 22 var args []any 22 23 for _, filter := range filters { ··· 58 59 return nil, nil 59 60 } 60 61 61 - return GetRepos(e, 0, FilterIn("at_uri", repoAts)) 62 + return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts)) 62 63 } 63 64 64 - func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) { 65 + func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) { 65 66 var collaborators []models.Collaborator 66 67 var conditions []string 67 68 var args []any
+33 -137
appview/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 - "fmt" 7 6 "log/slog" 8 - "reflect" 9 7 "strings" 10 8 11 9 _ "github.com/mattn/go-sqlite3" 12 10 "tangled.org/core/log" 11 + "tangled.org/core/orm" 13 12 ) 14 13 15 14 type DB struct { ··· 561 560 email_notifications integer not null default 0 562 561 ); 563 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 + 564 570 create table if not exists migrations ( 565 571 id integer primary key autoincrement, 566 572 name text unique ··· 569 575 -- indexes for better performance 570 576 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 571 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); 572 580 `) 573 581 if err != nil { 574 582 return nil, err 575 583 } 576 584 577 585 // run migrations 578 - runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 586 + orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 579 587 tx.Exec(` 580 588 alter table repos add column description text check (length(description) <= 200); 581 589 `) 582 590 return nil 583 591 }) 584 592 585 - runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 593 + orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 586 594 // add unconstrained column 587 595 _, err := tx.Exec(` 588 596 alter table public_keys ··· 605 613 return nil 606 614 }) 607 615 608 - runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 616 + orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 609 617 _, err := tx.Exec(` 610 618 alter table comments drop column comment_at; 611 619 alter table comments add column rkey text; ··· 613 621 return err 614 622 }) 615 623 616 - runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 624 + orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 617 625 _, err := tx.Exec(` 618 626 alter table comments add column deleted text; -- timestamp 619 627 alter table comments add column edited text; -- timestamp ··· 621 629 return err 622 630 }) 623 631 624 - runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 632 + orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 625 633 _, err := tx.Exec(` 626 634 alter table pulls add column source_branch text; 627 635 alter table pulls add column source_repo_at text; ··· 630 638 return err 631 639 }) 632 640 633 - runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 641 + orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 634 642 _, err := tx.Exec(` 635 643 alter table repos add column source text; 636 644 `) ··· 642 650 // 643 651 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 644 652 conn.ExecContext(ctx, "pragma foreign_keys = off;") 645 - runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 653 + orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 646 654 _, err := tx.Exec(` 647 655 create table pulls_new ( 648 656 -- identifiers ··· 699 707 }) 700 708 conn.ExecContext(ctx, "pragma foreign_keys = on;") 701 709 702 - runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 710 + orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 703 711 tx.Exec(` 704 712 alter table repos add column spindle text; 705 713 `) ··· 709 717 // drop all knot secrets, add unique constraint to knots 710 718 // 711 719 // knots will henceforth use service auth for signed requests 712 - runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 720 + orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 713 721 _, err := tx.Exec(` 714 722 create table registrations_new ( 715 723 id integer primary key autoincrement, ··· 732 740 }) 733 741 734 742 // recreate and add rkey + created columns with default constraint 735 - runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 743 + orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 736 744 // create new table 737 745 // - repo_at instead of repo integer 738 746 // - rkey field ··· 786 794 return err 787 795 }) 788 796 789 - runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 797 + orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 790 798 _, err := tx.Exec(` 791 799 alter table issues add column rkey text not null default ''; 792 800 ··· 798 806 }) 799 807 800 808 // repurpose the read-only column to "needs-upgrade" 801 - runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 809 + orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 802 810 _, err := tx.Exec(` 803 811 alter table registrations rename column read_only to needs_upgrade; 804 812 `) ··· 806 814 }) 807 815 808 816 // 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 { 817 + orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 810 818 _, err := tx.Exec(` 811 819 update registrations set needs_upgrade = 1; 812 820 `) ··· 814 822 }) 815 823 816 824 // 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 { 825 + orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 818 826 _, err := tx.Exec(` 819 827 alter table spindles add column needs_upgrade integer not null default 0; 820 828 `) ··· 832 840 // 833 841 // disable foreign-keys for the next migration 834 842 conn.ExecContext(ctx, "pragma foreign_keys = off;") 835 - runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 843 + orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 836 844 _, err := tx.Exec(` 837 845 create table if not exists issues_new ( 838 846 -- identifiers ··· 902 910 // - new columns 903 911 // * column "reply_to" which can be any other comment 904 912 // * column "at-uri" which is a generated column 905 - runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 913 + orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 906 914 _, err := tx.Exec(` 907 915 create table if not exists issue_comments ( 908 916 -- identifiers ··· 962 970 // 963 971 // disable foreign-keys for the next migration 964 972 conn.ExecContext(ctx, "pragma foreign_keys = off;") 965 - runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 973 + orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 966 974 _, err := tx.Exec(` 967 975 create table if not exists pulls_new ( 968 976 -- identifiers ··· 1043 1051 // 1044 1052 // disable foreign-keys for the next migration 1045 1053 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1046 - runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1054 + orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1047 1055 _, err := tx.Exec(` 1048 1056 create table if not exists pull_submissions_new ( 1049 1057 -- identifiers ··· 1097 1105 1098 1106 // knots may report the combined patch for a comparison, we can store that on the appview side 1099 1107 // (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 { 1108 + orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error { 1101 1109 _, err := tx.Exec(` 1102 1110 alter table pull_submissions add column combined text; 1103 1111 `) 1104 1112 return err 1105 1113 }) 1106 1114 1107 - runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1115 + orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1108 1116 _, err := tx.Exec(` 1109 1117 alter table profile add column pronouns text; 1110 1118 `) 1111 1119 return err 1112 1120 }) 1113 1121 1114 - runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1122 + orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1115 1123 _, err := tx.Exec(` 1116 1124 alter table repos add column website text; 1117 1125 alter table repos add column topics text; ··· 1119 1127 return err 1120 1128 }) 1121 1129 1122 - runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1130 + orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1123 1131 _, err := tx.Exec(` 1124 1132 alter table notification_preferences add column user_mentioned integer not null default 1; 1125 1133 `) ··· 1127 1135 }) 1128 1136 1129 1137 // remove the foreign key constraints from stars. 1130 - runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1138 + orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1131 1139 _, err := tx.Exec(` 1132 1140 create table stars_new ( 1133 1141 id integer primary key autoincrement, ··· 1171 1179 }, nil 1172 1180 } 1173 1181 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 1182 func (d *DB) Close() error { 1220 1183 return d.DB.Close() 1221 1184 } 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 - }
+6 -3
appview/db/follow.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 13 func AddFollow(e Execer, follow *models.Follow) error { ··· 134 135 return result, nil 135 136 } 136 137 137 - func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) { 138 + func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) { 138 139 var follows []models.Follow 139 140 140 141 var conditions []string ··· 166 167 if err != nil { 167 168 return nil, err 168 169 } 170 + defer rows.Close() 171 + 169 172 for rows.Next() { 170 173 var follow models.Follow 171 174 var followedAt string ··· 191 194 } 192 195 193 196 func GetFollowers(e Execer, did string) ([]models.Follow, error) { 194 - return GetFollows(e, 0, FilterEq("subject_did", did)) 197 + return GetFollows(e, 0, orm.FilterEq("subject_did", did)) 195 198 } 196 199 197 200 func GetFollowing(e Execer, did string) ([]models.Follow, error) { 198 - return GetFollows(e, 0, FilterEq("user_did", did)) 201 + return GetFollows(e, 0, orm.FilterEq("user_did", did)) 199 202 } 200 203 201 204 func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+93 -36
appview/db/issues.go
··· 10 10 "time" 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.org/core/api/tangled" 13 14 "tangled.org/core/appview/models" 14 15 "tangled.org/core/appview/pagination" 16 + "tangled.org/core/orm" 15 17 ) 16 18 17 19 func PutIssue(tx *sql.Tx, issue *models.Issue) error { ··· 26 28 27 29 issues, err := GetIssues( 28 30 tx, 29 - FilterEq("did", issue.Did), 30 - FilterEq("rkey", issue.Rkey), 31 + orm.FilterEq("did", issue.Did), 32 + orm.FilterEq("rkey", issue.Rkey), 31 33 ) 32 34 switch { 33 35 case err != nil: ··· 69 71 returning rowid, issue_id 70 72 `, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 71 73 72 - return row.Scan(&issue.Id, &issue.IssueId) 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 73 83 } 74 84 75 85 func updateIssue(tx *sql.Tx, issue *models.Issue) error { ··· 79 89 set title = ?, body = ?, edited = ? 80 90 where did = ? and rkey = ? 81 91 `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey) 82 - return err 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 83 100 } 84 101 85 - func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 102 + func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 86 103 issueMap := make(map[string]*models.Issue) // at-uri -> issue 87 104 88 105 var conditions []string ··· 98 115 whereClause = " where " + strings.Join(conditions, " and ") 99 116 } 100 117 101 - pLower := FilterGte("row_num", page.Offset+1) 102 - pUpper := FilterLte("row_num", page.Offset+page.Limit) 118 + pLower := orm.FilterGte("row_num", page.Offset+1) 119 + pUpper := orm.FilterLte("row_num", page.Offset+page.Limit) 103 120 104 121 pageClause := "" 105 122 if page.Limit > 0 { ··· 189 206 repoAts = append(repoAts, string(issue.RepoAt)) 190 207 } 191 208 192 - repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts)) 209 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts)) 193 210 if err != nil { 194 211 return nil, fmt.Errorf("failed to build repo mappings: %w", err) 195 212 } ··· 212 229 // collect comments 213 230 issueAts := slices.Collect(maps.Keys(issueMap)) 214 231 215 - comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts)) 232 + comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) 216 233 if err != nil { 217 234 return nil, fmt.Errorf("failed to query comments: %w", err) 218 235 } ··· 224 241 } 225 242 226 243 // collect allLabels for each issue 227 - allLabels, err := GetLabels(e, FilterIn("subject", issueAts)) 244 + allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts)) 228 245 if err != nil { 229 246 return nil, fmt.Errorf("failed to query labels: %w", err) 230 247 } ··· 234 251 } 235 252 } 236 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 + 237 265 var issues []models.Issue 238 266 for _, i := range issueMap { 239 267 issues = append(issues, *i) ··· 250 278 issues, err := GetIssuesPaginated( 251 279 e, 252 280 pagination.Page{}, 253 - FilterEq("repo_at", repoAt), 254 - FilterEq("issue_id", issueId), 281 + orm.FilterEq("repo_at", repoAt), 282 + orm.FilterEq("issue_id", issueId), 255 283 ) 256 284 if err != nil { 257 285 return nil, err ··· 263 291 return &issues[0], nil 264 292 } 265 293 266 - func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) { 294 + func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) { 267 295 return GetIssuesPaginated(e, pagination.Page{}, filters...) 268 296 } 269 297 ··· 271 299 func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 272 300 var ids []int64 273 301 274 - var filters []filter 302 + var filters []orm.Filter 275 303 openValue := 0 276 304 if opts.IsOpen { 277 305 openValue = 1 278 306 } 279 - filters = append(filters, FilterEq("open", openValue)) 307 + filters = append(filters, orm.FilterEq("open", openValue)) 280 308 if opts.RepoAt != "" { 281 - filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 282 310 } 283 311 284 312 var conditions []string ··· 323 351 return ids, nil 324 352 } 325 353 326 - func AddIssueComment(e Execer, c models.IssueComment) (int64, error) { 327 - result, err := e.Exec( 354 + func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 355 + result, err := tx.Exec( 328 356 `insert into issue_comments ( 329 357 did, 330 358 rkey, ··· 363 391 return 0, err 364 392 } 365 393 394 + if err := putReferences(tx, c.AtUri(), c.References); err != nil { 395 + return 0, fmt.Errorf("put reference_links: %w", err) 396 + } 397 + 366 398 return id, nil 367 399 } 368 400 369 - func DeleteIssueComments(e Execer, filters ...filter) error { 401 + func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 370 402 var conditions []string 371 403 var args []any 372 404 for _, filter := range filters { ··· 385 417 return err 386 418 } 387 419 388 - func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) { 389 - var comments []models.IssueComment 420 + func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 421 + commentMap := make(map[string]*models.IssueComment) 390 422 391 423 var conditions []string 392 424 var args []any ··· 420 452 if err != nil { 421 453 return nil, err 422 454 } 455 + defer rows.Close() 423 456 424 457 for rows.Next() { 425 458 var comment models.IssueComment ··· 465 498 comment.ReplyTo = &replyTo.V 466 499 } 467 500 468 - comments = append(comments, comment) 501 + atUri := comment.AtUri().String() 502 + commentMap[atUri] = &comment 469 503 } 470 504 471 505 if err = rows.Err(); err != nil { 472 506 return nil, err 473 507 } 474 508 509 + // collect references for each comments 510 + commentAts := slices.Collect(maps.Keys(commentMap)) 511 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 512 + if err != nil { 513 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 514 + } 515 + for commentAt, references := range allReferencs { 516 + if comment, ok := commentMap[commentAt.String()]; ok { 517 + comment.References = references 518 + } 519 + } 520 + 521 + var comments []models.IssueComment 522 + for _, c := range commentMap { 523 + comments = append(comments, *c) 524 + } 525 + 526 + sort.Slice(comments, func(i, j int) bool { 527 + return comments[i].Created.After(comments[j].Created) 528 + }) 529 + 475 530 return comments, nil 476 531 } 477 532 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()...) 533 + func DeleteIssues(tx *sql.Tx, did, rkey string) error { 534 + _, err := tx.Exec( 535 + `delete from issues 536 + where did = ? and rkey = ?`, 537 + did, 538 + rkey, 539 + ) 540 + if err != nil { 541 + return fmt.Errorf("delete issue: %w", err) 484 542 } 485 543 486 - whereClause := "" 487 - if conditions != nil { 488 - whereClause = " where " + strings.Join(conditions, " and ") 544 + uri := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey)) 545 + err = deleteReferences(tx, uri) 546 + if err != nil { 547 + return fmt.Errorf("delete reference_links: %w", err) 489 548 } 490 549 491 - query := fmt.Sprintf(`delete from issues %s`, whereClause) 492 - _, err := e.Exec(query, args...) 493 - return err 550 + return nil 494 551 } 495 552 496 - func CloseIssues(e Execer, filters ...filter) error { 553 + func CloseIssues(e Execer, filters ...orm.Filter) error { 497 554 var conditions []string 498 555 var args []any 499 556 for _, filter := range filters { ··· 511 568 return err 512 569 } 513 570 514 - func ReopenIssues(e Execer, filters ...filter) error { 571 + func ReopenIssues(e Execer, filters ...orm.Filter) error { 515 572 var conditions []string 516 573 var args []any 517 574 for _, filter := range filters {
+8 -7
appview/db/label.go
··· 10 10 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 12 "tangled.org/core/appview/models" 13 + "tangled.org/core/orm" 13 14 ) 14 15 15 16 // no updating type for now ··· 59 60 return id, nil 60 61 } 61 62 62 - func DeleteLabelDefinition(e Execer, filters ...filter) error { 63 + func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error { 63 64 var conditions []string 64 65 var args []any 65 66 for _, filter := range filters { ··· 75 76 return err 76 77 } 77 78 78 - func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) { 79 + func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) { 79 80 var labelDefinitions []models.LabelDefinition 80 81 var conditions []string 81 82 var args []any ··· 167 168 } 168 169 169 170 // helper to get exactly one label def 170 - func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) { 171 + func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) { 171 172 labels, err := GetLabelDefinitions(e, filters...) 172 173 if err != nil { 173 174 return nil, err ··· 227 228 return id, nil 228 229 } 229 230 230 - func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) { 231 + func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) { 231 232 var labelOps []models.LabelOp 232 233 var conditions []string 233 234 var args []any ··· 302 303 } 303 304 304 305 // get labels for a given list of subject URIs 305 - func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) { 306 + func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) { 306 307 ops, err := GetLabelOps(e, filters...) 307 308 if err != nil { 308 309 return nil, err ··· 322 323 } 323 324 labelAts := slices.Collect(maps.Keys(labelAtSet)) 324 325 325 - actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts)) 326 + actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts)) 326 327 if err != nil { 327 328 return nil, err 328 329 } ··· 338 339 return results, nil 339 340 } 340 341 341 - func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) { 342 + func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) { 342 343 labels, err := GetLabelDefinitions(e, filters...) 343 344 if err != nil { 344 345 return nil, err
+6 -5
appview/db/language.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) { 13 + func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) { 13 14 var conditions []string 14 15 var args []any 15 16 for _, filter := range filters { ··· 27 28 whereClause, 28 29 ) 29 30 rows, err := e.Query(query, args...) 30 - 31 31 if err != nil { 32 32 return nil, fmt.Errorf("failed to execute query: %w ", err) 33 33 } 34 + defer rows.Close() 34 35 35 36 var langs []models.RepoLanguage 36 37 for rows.Next() { ··· 85 86 return nil 86 87 } 87 88 88 - func DeleteRepoLanguages(e Execer, filters ...filter) error { 89 + func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error { 89 90 var conditions []string 90 91 var args []any 91 92 for _, filter := range filters { ··· 107 108 func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error { 108 109 err := DeleteRepoLanguages( 109 110 tx, 110 - FilterEq("repo_at", repoAt), 111 - FilterEq("ref", ref), 111 + orm.FilterEq("repo_at", repoAt), 112 + orm.FilterEq("ref", ref), 112 113 ) 113 114 if err != nil { 114 115 return fmt.Errorf("failed to delete existing languages: %w", err)
+14 -13
appview/db/notifications.go
··· 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 12 "tangled.org/core/appview/models" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func CreateNotification(e Execer, notification *models.Notification) error { ··· 44 45 } 45 46 46 47 // GetNotificationsPaginated retrieves notifications with filters and pagination 47 - func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) { 48 + func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) { 48 49 var conditions []string 49 50 var args []any 50 51 ··· 113 114 } 114 115 115 116 // GetNotificationsWithEntities retrieves notifications with their related entities 116 - func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) { 117 + func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) { 117 118 var conditions []string 118 119 var args []any 119 120 ··· 256 257 } 257 258 258 259 // GetNotifications retrieves notifications with filters 259 - func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) { 260 + func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) { 260 261 return GetNotificationsPaginated(e, pagination.FirstPage(), filters...) 261 262 } 262 263 263 - func CountNotifications(e Execer, filters ...filter) (int64, error) { 264 + func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) { 264 265 var conditions []string 265 266 var args []any 266 267 for _, filter := range filters { ··· 285 286 } 286 287 287 288 func MarkNotificationRead(e Execer, notificationID int64, userDID string) error { 288 - idFilter := FilterEq("id", notificationID) 289 - recipientFilter := FilterEq("recipient_did", userDID) 289 + idFilter := orm.FilterEq("id", notificationID) 290 + recipientFilter := orm.FilterEq("recipient_did", userDID) 290 291 291 292 query := fmt.Sprintf(` 292 293 UPDATE notifications ··· 314 315 } 315 316 316 317 func MarkAllNotificationsRead(e Execer, userDID string) error { 317 - recipientFilter := FilterEq("recipient_did", userDID) 318 - readFilter := FilterEq("read", 0) 318 + recipientFilter := orm.FilterEq("recipient_did", userDID) 319 + readFilter := orm.FilterEq("read", 0) 319 320 320 321 query := fmt.Sprintf(` 321 322 UPDATE notifications ··· 334 335 } 335 336 336 337 func DeleteNotification(e Execer, notificationID int64, userDID string) error { 337 - idFilter := FilterEq("id", notificationID) 338 - recipientFilter := FilterEq("recipient_did", userDID) 338 + idFilter := orm.FilterEq("id", notificationID) 339 + recipientFilter := orm.FilterEq("recipient_did", userDID) 339 340 340 341 query := fmt.Sprintf(` 341 342 DELETE FROM notifications ··· 362 363 } 363 364 364 365 func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) { 365 - prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid)) 366 + prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid)) 366 367 if err != nil { 367 368 return nil, err 368 369 } ··· 375 376 return p, nil 376 377 } 377 378 378 - func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) { 379 + func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) { 379 380 prefsMap := make(map[syntax.DID]*models.NotificationPreferences) 380 381 381 382 var conditions []string ··· 483 484 484 485 func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error { 485 486 cutoff := time.Now().Add(-olderThan) 486 - createdFilter := FilterLte("created", cutoff) 487 + createdFilter := orm.FilterLte("created", cutoff) 487 488 488 489 query := fmt.Sprintf(` 489 490 DELETE FROM notifications
+6 -5
appview/db/pipeline.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) { 13 + func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) { 13 14 var pipelines []models.Pipeline 14 15 15 16 var conditions []string ··· 168 169 169 170 // this is a mega query, but the most useful one: 170 171 // 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 + func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) { 172 173 var conditions []string 173 174 var args []any 174 175 for _, filter := range filters { 175 - filter.key = "p." + filter.key // the table is aliased in the query to `p` 176 + filter.Key = "p." + filter.Key // the table is aliased in the query to `p` 176 177 conditions = append(conditions, filter.Condition()) 177 178 args = append(args, filter.Arg()...) 178 179 } ··· 264 265 conditions = nil 265 266 args = nil 266 267 for _, p := range pipelines { 267 - knotFilter := FilterEq("pipeline_knot", p.Knot) 268 - rkeyFilter := FilterEq("pipeline_rkey", p.Rkey) 268 + knotFilter := orm.FilterEq("pipeline_knot", p.Knot) 269 + rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey) 269 270 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition())) 270 271 args = append(args, p.Knot) 271 272 args = append(args, p.Rkey)
+11 -5
appview/db/profile.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 const TimeframeMonths = 7 ··· 44 45 45 46 issues, err := GetIssues( 46 47 e, 47 - FilterEq("did", forDid), 48 - FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 48 + orm.FilterEq("did", forDid), 49 + orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 49 50 ) 50 51 if err != nil { 51 52 return nil, fmt.Errorf("error getting issues by owner did: %w", err) ··· 65 66 *items = append(*items, &issue) 66 67 } 67 68 68 - repos, err := GetRepos(e, 0, FilterEq("did", forDid)) 69 + repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid)) 69 70 if err != nil { 70 71 return nil, fmt.Errorf("error getting all repos by did: %w", err) 71 72 } ··· 199 200 return tx.Commit() 200 201 } 201 202 202 - func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) { 203 + func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) { 203 204 var conditions []string 204 205 var args []any 205 206 for _, filter := range filters { ··· 229 230 if err != nil { 230 231 return nil, err 231 232 } 233 + defer rows.Close() 232 234 233 235 profileMap := make(map[string]*models.Profile) 234 236 for rows.Next() { ··· 269 271 if err != nil { 270 272 return nil, err 271 273 } 274 + defer rows.Close() 275 + 272 276 idxs := make(map[string]int) 273 277 for did := range profileMap { 274 278 idxs[did] = 0 ··· 289 293 if err != nil { 290 294 return nil, err 291 295 } 296 + defer rows.Close() 297 + 292 298 idxs = make(map[string]int) 293 299 for did := range profileMap { 294 300 idxs[did] = 0 ··· 441 447 } 442 448 443 449 // ensure all pinned repos are either own repos or collaborating repos 444 - repos, err := GetRepos(e, 0, FilterEq("did", profile.Did)) 450 + repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did)) 445 451 if err != nil { 446 452 log.Printf("getting repos for %s: %s", profile.Did, err) 447 453 }
+69 -24
appview/db/pulls.go
··· 13 13 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 "tangled.org/core/appview/models" 16 + "tangled.org/core/orm" 16 17 ) 17 18 18 19 func NewPull(tx *sql.Tx, pull *models.Pull) error { ··· 93 94 insert into pull_submissions (pull_at, round_number, patch, combined, source_rev) 94 95 values (?, ?, ?, ?, ?) 95 96 `, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev) 96 - return err 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 97 106 } 98 107 99 108 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { ··· 110 119 return pullId - 1, err 111 120 } 112 121 113 - func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) { 122 + func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) { 114 123 pulls := make(map[syntax.ATURI]*models.Pull) 115 124 116 125 var conditions []string ··· 221 230 for _, p := range pulls { 222 231 pullAts = append(pullAts, p.AtUri()) 223 232 } 224 - submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts)) 233 + submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts)) 225 234 if err != nil { 226 235 return nil, fmt.Errorf("failed to get submissions: %w", err) 227 236 } ··· 233 242 } 234 243 235 244 // collect allLabels for each issue 236 - allLabels, err := GetLabels(e, FilterIn("subject", pullAts)) 245 + allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts)) 237 246 if err != nil { 238 247 return nil, fmt.Errorf("failed to query labels: %w", err) 239 248 } ··· 250 259 sourceAts = append(sourceAts, *p.PullSource.RepoAt) 251 260 } 252 261 } 253 - sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts)) 262 + sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts)) 254 263 if err != nil && !errors.Is(err, sql.ErrNoRows) { 255 264 return nil, fmt.Errorf("failed to get source repos: %w", err) 256 265 } ··· 266 275 } 267 276 } 268 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 + 269 288 orderedByPullId := []*models.Pull{} 270 289 for _, p := range pulls { 271 290 orderedByPullId = append(orderedByPullId, p) ··· 277 296 return orderedByPullId, nil 278 297 } 279 298 280 - func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) { 299 + func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 281 300 return GetPullsWithLimit(e, 0, filters...) 282 301 } 283 302 284 303 func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) { 285 304 var ids []int64 286 305 287 - var filters []filter 288 - filters = append(filters, FilterEq("state", opts.State)) 306 + var filters []orm.Filter 307 + filters = append(filters, orm.FilterEq("state", opts.State)) 289 308 if opts.RepoAt != "" { 290 - filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 291 310 } 292 311 293 312 var conditions []string ··· 343 362 } 344 363 345 364 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)) 365 + pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId)) 347 366 if err != nil { 348 367 return nil, err 349 368 } ··· 355 374 } 356 375 357 376 // mapping from pull -> pull submissions 358 - func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 377 + func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 359 378 var conditions []string 360 379 var args []any 361 380 for _, filter := range filters { ··· 430 449 431 450 // Get comments for all submissions using GetPullComments 432 451 submissionIds := slices.Collect(maps.Keys(submissionMap)) 433 - comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds)) 452 + comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds)) 434 453 if err != nil { 435 - return nil, err 454 + return nil, fmt.Errorf("failed to get pull comments: %w", err) 436 455 } 437 456 for _, comment := range comments { 438 457 if submission, ok := submissionMap[comment.SubmissionId]; ok { ··· 456 475 return m, nil 457 476 } 458 477 459 - func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) { 478 + func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) { 460 479 var conditions []string 461 480 var args []any 462 481 for _, filter := range filters { ··· 492 511 } 493 512 defer rows.Close() 494 513 495 - var comments []models.PullComment 514 + commentMap := make(map[string]*models.PullComment) 496 515 for rows.Next() { 497 516 var comment models.PullComment 498 517 var createdAt string ··· 514 533 comment.Created = t 515 534 } 516 535 517 - comments = append(comments, comment) 536 + atUri := comment.AtUri().String() 537 + commentMap[atUri] = &comment 518 538 } 519 539 520 540 if err := rows.Err(); err != nil { 521 541 return nil, err 522 542 } 523 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 + 524 565 return comments, nil 525 566 } 526 567 ··· 600 641 return pulls, nil 601 642 } 602 643 603 - func NewPullComment(e Execer, comment *models.PullComment) (int64, error) { 644 + func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 604 645 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 605 - res, err := e.Exec( 646 + res, err := tx.Exec( 606 647 query, 607 648 comment.OwnerDid, 608 649 comment.RepoAt, ··· 618 659 i, err := res.LastInsertId() 619 660 if err != nil { 620 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) 621 666 } 622 667 623 668 return i, nil ··· 664 709 return err 665 710 } 666 711 667 - func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error { 712 + func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error { 668 713 var conditions []string 669 714 var args []any 670 715 ··· 688 733 689 734 // Only used when stacking to update contents in the event of a rebase (the interdiff should be empty). 690 735 // otherwise submissions are immutable 691 - func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error { 736 + func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error { 692 737 var conditions []string 693 738 var args []any 694 739 ··· 746 791 func GetStack(e Execer, stackId string) (models.Stack, error) { 747 792 unorderedPulls, err := GetPulls( 748 793 e, 749 - FilterEq("stack_id", stackId), 750 - FilterNotEq("state", models.PullDeleted), 794 + orm.FilterEq("stack_id", stackId), 795 + orm.FilterNotEq("state", models.PullDeleted), 751 796 ) 752 797 if err != nil { 753 798 return nil, err ··· 791 836 func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) { 792 837 pulls, err := GetPulls( 793 838 e, 794 - FilterEq("stack_id", stackId), 795 - FilterEq("state", models.PullDeleted), 839 + orm.FilterEq("stack_id", stackId), 840 + orm.FilterEq("state", models.PullDeleted), 796 841 ) 797 842 if err != nil { 798 843 return nil, err
+2 -1
appview/db/punchcard.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 13 // this adds to the existing count ··· 20 21 return err 21 22 } 22 23 23 - func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) { 24 + func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) { 24 25 punchcard := &models.Punchcard{} 25 26 now := time.Now() 26 27 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+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 + }
+5 -3
appview/db/registration.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) { 13 + func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) { 13 14 var registrations []models.Registration 14 15 15 16 var conditions []string ··· 37 38 if err != nil { 38 39 return nil, err 39 40 } 41 + defer rows.Close() 40 42 41 43 for rows.Next() { 42 44 var createdAt string ··· 69 71 return registrations, nil 70 72 } 71 73 72 - func MarkRegistered(e Execer, filters ...filter) error { 74 + func MarkRegistered(e Execer, filters ...orm.Filter) error { 73 75 var conditions []string 74 76 var args []any 75 77 for _, filter := range filters { ··· 94 96 return err 95 97 } 96 98 97 - func DeleteKnot(e Execer, filters ...filter) error { 99 + func DeleteKnot(e Execer, filters ...orm.Filter) error { 98 100 var conditions []string 99 101 var args []any 100 102 for _, filter := range filters {
+17 -6
appview/db/repos.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 - func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { 17 + func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) { 17 18 repoMap := make(map[syntax.ATURI]*models.Repo) 18 19 19 20 var conditions []string ··· 55 56 limitClause, 56 57 ) 57 58 rows, err := e.Query(repoQuery, args...) 58 - 59 59 if err != nil { 60 60 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 61 61 } 62 + defer rows.Close() 62 63 63 64 for rows.Next() { 64 65 var repo models.Repo ··· 127 128 if err != nil { 128 129 return nil, fmt.Errorf("failed to execute labels query: %w ", err) 129 130 } 131 + defer rows.Close() 132 + 130 133 for rows.Next() { 131 134 var repoat, labelat string 132 135 if err := rows.Scan(&repoat, &labelat); err != nil { ··· 164 167 if err != nil { 165 168 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 166 169 } 170 + defer rows.Close() 171 + 167 172 for rows.Next() { 168 173 var repoat, lang string 169 174 if err := rows.Scan(&repoat, &lang); err != nil { ··· 190 195 if err != nil { 191 196 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 192 197 } 198 + defer rows.Close() 199 + 193 200 for rows.Next() { 194 201 var repoat string 195 202 var count int ··· 219 226 if err != nil { 220 227 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 221 228 } 229 + defer rows.Close() 230 + 222 231 for rows.Next() { 223 232 var repoat string 224 233 var open, closed int ··· 260 269 if err != nil { 261 270 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 262 271 } 272 + defer rows.Close() 273 + 263 274 for rows.Next() { 264 275 var repoat string 265 276 var open, merged, closed, deleted int ··· 294 305 } 295 306 296 307 // helper to get exactly one repo 297 - func GetRepo(e Execer, filters ...filter) (*models.Repo, error) { 308 + func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) { 298 309 repos, err := GetRepos(e, 0, filters...) 299 310 if err != nil { 300 311 return nil, err ··· 311 322 return &repos[0], nil 312 323 } 313 324 314 - func CountRepos(e Execer, filters ...filter) (int64, error) { 325 + func CountRepos(e Execer, filters ...orm.Filter) (int64, error) { 315 326 var conditions []string 316 327 var args []any 317 328 for _, filter := range filters { ··· 542 553 return err 543 554 } 544 555 545 - func UnsubscribeLabel(e Execer, filters ...filter) error { 556 + func UnsubscribeLabel(e Execer, filters ...orm.Filter) error { 546 557 var conditions []string 547 558 var args []any 548 559 for _, filter := range filters { ··· 560 571 return err 561 572 } 562 573 563 - func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) { 574 + func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) { 564 575 var conditions []string 565 576 var args []any 566 577 for _, filter := range filters {
+6 -5
appview/db/spindle.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) { 13 + func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) { 13 14 var spindles []models.Spindle 14 15 15 16 var conditions []string ··· 91 92 return err 92 93 } 93 94 94 - func VerifySpindle(e Execer, filters ...filter) (int64, error) { 95 + func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) { 95 96 var conditions []string 96 97 var args []any 97 98 for _, filter := range filters { ··· 114 115 return res.RowsAffected() 115 116 } 116 117 117 - func DeleteSpindle(e Execer, filters ...filter) error { 118 + func DeleteSpindle(e Execer, filters ...orm.Filter) error { 118 119 var conditions []string 119 120 var args []any 120 121 for _, filter := range filters { ··· 144 145 return err 145 146 } 146 147 147 - func RemoveSpindleMember(e Execer, filters ...filter) error { 148 + func RemoveSpindleMember(e Execer, filters ...orm.Filter) error { 148 149 var conditions []string 149 150 var args []any 150 151 for _, filter := range filters { ··· 163 164 return err 164 165 } 165 166 166 - func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) { 167 + func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) { 167 168 var members []models.SpindleMember 168 169 169 170 var conditions []string
+6 -4
appview/db/star.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func AddStar(e Execer, star *models.Star) error { ··· 133 134 134 135 // GetRepoStars return a list of stars each holding target repository. 135 136 // 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 + func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) { 137 138 var conditions []string 138 139 var args []any 139 140 for _, filter := range filters { ··· 164 165 if err != nil { 165 166 return nil, err 166 167 } 168 + defer rows.Close() 167 169 168 170 starMap := make(map[string][]models.Star) 169 171 for rows.Next() { ··· 195 197 return nil, nil 196 198 } 197 199 198 - repos, err := GetRepos(e, 0, FilterIn("at_uri", args)) 200 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args)) 199 201 if err != nil { 200 202 return nil, err 201 203 } ··· 225 227 return repoStars, nil 226 228 } 227 229 228 - func CountStars(e Execer, filters ...filter) (int64, error) { 230 + func CountStars(e Execer, filters ...orm.Filter) (int64, error) { 229 231 var conditions []string 230 232 var args []any 231 233 for _, filter := range filters { ··· 298 300 } 299 301 300 302 // get full repo data 301 - repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 303 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris)) 302 304 if err != nil { 303 305 return nil, err 304 306 }
+4 -3
appview/db/strings.go
··· 8 8 "time" 9 9 10 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 11 12 ) 12 13 13 14 func AddString(e Execer, s models.String) error { ··· 44 45 return err 45 46 } 46 47 47 - func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) { 48 + func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) { 48 49 var all []models.String 49 50 50 51 var conditions []string ··· 127 128 return all, nil 128 129 } 129 130 130 - func CountStrings(e Execer, filters ...filter) (int64, error) { 131 + func CountStrings(e Execer, filters ...orm.Filter) (int64, error) { 131 132 var conditions []string 132 133 var args []any 133 134 for _, filter := range filters { ··· 151 152 return count, nil 152 153 } 153 154 154 - func DeleteString(e Execer, filters ...filter) error { 155 + func DeleteString(e Execer, filters ...orm.Filter) error { 155 156 var conditions []string 156 157 var args []any 157 158 for _, filter := range filters {
+9 -8
appview/db/timeline.go
··· 5 5 6 6 "github.com/bluesky-social/indigo/atproto/syntax" 7 7 "tangled.org/core/appview/models" 8 + "tangled.org/core/orm" 8 9 ) 9 10 10 11 // TODO: this gathers heterogenous events from different sources and aggregates ··· 84 85 } 85 86 86 87 func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 87 - filters := make([]filter, 0) 88 + filters := make([]orm.Filter, 0) 88 89 if userIsFollowing != nil { 89 - filters = append(filters, FilterIn("did", userIsFollowing)) 90 + filters = append(filters, orm.FilterIn("did", userIsFollowing)) 90 91 } 91 92 92 93 repos, err := GetRepos(e, limit, filters...) ··· 104 105 105 106 var origRepos []models.Repo 106 107 if args != nil { 107 - origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args)) 108 + origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args)) 108 109 } 109 110 if err != nil { 110 111 return nil, err ··· 144 145 } 145 146 146 147 func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 147 - filters := make([]filter, 0) 148 + filters := make([]orm.Filter, 0) 148 149 if userIsFollowing != nil { 149 - filters = append(filters, FilterIn("did", userIsFollowing)) 150 + filters = append(filters, orm.FilterIn("did", userIsFollowing)) 150 151 } 151 152 152 153 stars, err := GetRepoStars(e, limit, filters...) ··· 180 181 } 181 182 182 183 func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 183 - filters := make([]filter, 0) 184 + filters := make([]orm.Filter, 0) 184 185 if userIsFollowing != nil { 185 - filters = append(filters, FilterIn("user_did", userIsFollowing)) 186 + filters = append(filters, orm.FilterIn("user_did", userIsFollowing)) 186 187 } 187 188 188 189 follows, err := GetFollows(e, limit, filters...) ··· 199 200 return nil, nil 200 201 } 201 202 202 - profiles, err := GetProfiles(e, FilterIn("did", subjects)) 203 + profiles, err := GetProfiles(e, orm.FilterIn("did", subjects)) 203 204 if err != nil { 204 205 return nil, err 205 206 }
+47 -29
appview/ingester.go
··· 21 21 "tangled.org/core/appview/serververify" 22 22 "tangled.org/core/appview/validator" 23 23 "tangled.org/core/idresolver" 24 + "tangled.org/core/orm" 24 25 "tangled.org/core/rbac" 25 26 ) 26 27 ··· 253 254 254 255 err = db.AddArtifact(i.Db, artifact) 255 256 case jmodels.CommitOperationDelete: 256 - err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 257 + err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 257 258 } 258 259 259 260 if err != nil { ··· 350 351 351 352 err = db.UpsertProfile(tx, &profile) 352 353 case jmodels.CommitOperationDelete: 353 - err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 354 + err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 354 355 } 355 356 356 357 if err != nil { ··· 424 425 // get record from db first 425 426 members, err := db.GetSpindleMembers( 426 427 ddb, 427 - db.FilterEq("did", did), 428 - db.FilterEq("rkey", rkey), 428 + orm.FilterEq("did", did), 429 + orm.FilterEq("rkey", rkey), 429 430 ) 430 431 if err != nil || len(members) != 1 { 431 432 return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members)) ··· 440 441 // remove record by rkey && update enforcer 441 442 if err = db.RemoveSpindleMember( 442 443 tx, 443 - db.FilterEq("did", did), 444 - db.FilterEq("rkey", rkey), 444 + orm.FilterEq("did", did), 445 + orm.FilterEq("rkey", rkey), 445 446 ); err != nil { 446 447 return fmt.Errorf("failed to remove from db: %w", err) 447 448 } ··· 523 524 // get record from db first 524 525 spindles, err := db.GetSpindles( 525 526 ddb, 526 - db.FilterEq("owner", did), 527 - db.FilterEq("instance", instance), 527 + orm.FilterEq("owner", did), 528 + orm.FilterEq("instance", instance), 528 529 ) 529 530 if err != nil || len(spindles) != 1 { 530 531 return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles)) ··· 543 544 // remove spindle members first 544 545 err = db.RemoveSpindleMember( 545 546 tx, 546 - db.FilterEq("owner", did), 547 - db.FilterEq("instance", instance), 547 + orm.FilterEq("owner", did), 548 + orm.FilterEq("instance", instance), 548 549 ) 549 550 if err != nil { 550 551 return err ··· 552 553 553 554 err = db.DeleteSpindle( 554 555 tx, 555 - db.FilterEq("owner", did), 556 - db.FilterEq("instance", instance), 556 + orm.FilterEq("owner", did), 557 + orm.FilterEq("instance", instance), 557 558 ) 558 559 if err != nil { 559 560 return err ··· 621 622 case jmodels.CommitOperationDelete: 622 623 if err := db.DeleteString( 623 624 ddb, 624 - db.FilterEq("did", did), 625 - db.FilterEq("rkey", rkey), 625 + orm.FilterEq("did", did), 626 + orm.FilterEq("rkey", rkey), 626 627 ); err != nil { 627 628 l.Error("failed to delete", "err", err) 628 629 return fmt.Errorf("failed to delete string record: %w", err) ··· 740 741 // get record from db first 741 742 registrations, err := db.GetRegistrations( 742 743 ddb, 743 - db.FilterEq("domain", domain), 744 - db.FilterEq("did", did), 744 + orm.FilterEq("domain", domain), 745 + orm.FilterEq("did", did), 745 746 ) 746 747 if err != nil { 747 748 return fmt.Errorf("failed to get registration: %w", err) ··· 762 763 763 764 err = db.DeleteKnot( 764 765 tx, 765 - db.FilterEq("did", did), 766 - db.FilterEq("domain", domain), 766 + orm.FilterEq("did", did), 767 + orm.FilterEq("domain", domain), 767 768 ) 768 769 if err != nil { 769 770 return err ··· 841 842 return nil 842 843 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 + 844 852 if err := db.DeleteIssues( 845 - ddb, 846 - db.FilterEq("did", did), 847 - db.FilterEq("rkey", rkey), 853 + tx, 854 + did, 855 + rkey, 848 856 ); err != nil { 849 857 l.Error("failed to delete", "err", err) 850 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 851 863 } 852 864 853 865 return nil ··· 888 900 return fmt.Errorf("failed to validate comment: %w", err) 889 901 } 890 902 891 - _, err = db.AddIssueComment(ddb, *comment) 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) 892 910 if err != nil { 893 911 return fmt.Errorf("failed to create issue comment: %w", err) 894 912 } 895 913 896 - return nil 914 + return tx.Commit() 897 915 898 916 case jmodels.CommitOperationDelete: 899 917 if err := db.DeleteIssueComments( 900 918 ddb, 901 - db.FilterEq("did", did), 902 - db.FilterEq("rkey", rkey), 919 + orm.FilterEq("did", did), 920 + orm.FilterEq("rkey", rkey), 903 921 ); err != nil { 904 922 return fmt.Errorf("failed to delete issue comment record: %w", err) 905 923 } ··· 952 970 case jmodels.CommitOperationDelete: 953 971 if err := db.DeleteLabelDefinition( 954 972 ddb, 955 - db.FilterEq("did", did), 956 - db.FilterEq("rkey", rkey), 973 + orm.FilterEq("did", did), 974 + orm.FilterEq("rkey", rkey), 957 975 ); err != nil { 958 976 return fmt.Errorf("failed to delete labeldef record: %w", err) 959 977 } ··· 993 1011 var repo *models.Repo 994 1012 switch collection { 995 1013 case tangled.RepoIssueNSID: 996 - i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject)) 1014 + i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject)) 997 1015 if err != nil || len(i) != 1 { 998 1016 return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i)) 999 1017 } ··· 1002 1020 return fmt.Errorf("unsupport label subject: %s", collection) 1003 1021 } 1004 1022 1005 - actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels)) 1023 + actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels)) 1006 1024 if err != nil { 1007 1025 return fmt.Errorf("failed to build label application ctx: %w", err) 1008 1026 }
+111 -73
appview/issues/issues.go
··· 19 19 "tangled.org/core/appview/config" 20 20 "tangled.org/core/appview/db" 21 21 issues_indexer "tangled.org/core/appview/indexer/issues" 22 + "tangled.org/core/appview/mentions" 22 23 "tangled.org/core/appview/models" 23 24 "tangled.org/core/appview/notify" 24 25 "tangled.org/core/appview/oauth" 25 26 "tangled.org/core/appview/pages" 26 - "tangled.org/core/appview/pages/markup" 27 27 "tangled.org/core/appview/pages/repoinfo" 28 28 "tangled.org/core/appview/pagination" 29 29 "tangled.org/core/appview/reporesolver" 30 30 "tangled.org/core/appview/validator" 31 31 "tangled.org/core/idresolver" 32 + "tangled.org/core/orm" 32 33 "tangled.org/core/rbac" 33 34 "tangled.org/core/tid" 34 35 ) 35 36 36 37 type Issues struct { 37 - oauth *oauth.OAuth 38 - repoResolver *reporesolver.RepoResolver 39 - enforcer *rbac.Enforcer 40 - pages *pages.Pages 41 - idResolver *idresolver.Resolver 42 - db *db.DB 43 - config *config.Config 44 - notifier notify.Notifier 45 - logger *slog.Logger 46 - validator *validator.Validator 47 - indexer *issues_indexer.Indexer 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 48 50 } 49 51 50 52 func New( ··· 53 55 enforcer *rbac.Enforcer, 54 56 pages *pages.Pages, 55 57 idResolver *idresolver.Resolver, 58 + mentionsResolver *mentions.Resolver, 56 59 db *db.DB, 57 60 config *config.Config, 58 61 notifier notify.Notifier, ··· 61 64 logger *slog.Logger, 62 65 ) *Issues { 63 66 return &Issues{ 64 - oauth: oauth, 65 - repoResolver: repoResolver, 66 - enforcer: enforcer, 67 - pages: pages, 68 - idResolver: idResolver, 69 - db: db, 70 - config: config, 71 - notifier: notifier, 72 - logger: logger, 73 - validator: validator, 74 - indexer: indexer, 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, 75 79 } 76 80 } 77 81 ··· 101 105 userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 102 106 } 103 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 + 104 115 labelDefs, err := db.GetLabelDefinitions( 105 116 rp.db, 106 - db.FilterIn("at_uri", f.Labels), 107 - db.FilterContains("scope", tangled.RepoIssueNSID), 117 + orm.FilterIn("at_uri", f.Labels), 118 + orm.FilterContains("scope", tangled.RepoIssueNSID), 108 119 ) 109 120 if err != nil { 110 121 l.Error("failed to fetch labels", "err", err) ··· 122 133 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 123 134 Issue: issue, 124 135 CommentList: issue.CommentList(), 136 + Backlinks: backlinks, 125 137 OrderedReactionKinds: models.OrderedReactionKinds, 126 138 Reactions: reactionMap, 127 139 UserReacted: userReactions, ··· 152 164 newIssue := issue 153 165 newIssue.Title = r.FormValue("title") 154 166 newIssue.Body = r.FormValue("body") 167 + newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body) 155 168 156 169 if err := rp.validator.ValidateIssue(newIssue); err != nil { 157 170 l.Error("validation error", "err", err) ··· 235 248 } 236 249 l = l.With("did", issue.Did, "rkey", issue.Rkey) 237 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 + 238 259 // delete from PDS 239 260 client, err := rp.oauth.AuthorizedClient(r) 240 261 if err != nil { ··· 255 276 } 256 277 257 278 // delete from db 258 - if err := db.DeleteIssues(rp.db, db.FilterEq("id", issue.Id)); err != nil { 279 + if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil { 259 280 l.Error("failed to delete issue", "err", err) 260 281 rp.pages.Notice(w, noticeId, "Failed to delete issue.") 261 282 return 262 283 } 284 + tx.Commit() 263 285 264 286 rp.notifier.DeleteIssue(r.Context(), issue) 265 287 ··· 293 315 if isIssueOwner || isRepoOwner || isCollaborator { 294 316 err = db.CloseIssues( 295 317 rp.db, 296 - db.FilterEq("id", issue.Id), 318 + orm.FilterEq("id", issue.Id), 297 319 ) 298 320 if err != nil { 299 321 l.Error("failed to close issue", "err", err) ··· 340 362 if isCollaborator || isRepoOwner || isIssueOwner { 341 363 err := db.ReopenIssues( 342 364 rp.db, 343 - db.FilterEq("id", issue.Id), 365 + orm.FilterEq("id", issue.Id), 344 366 ) 345 367 if err != nil { 346 368 l.Error("failed to reopen issue", "err", err) ··· 391 413 replyTo = &replyToUri 392 414 } 393 415 416 + mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 + 394 418 comment := models.IssueComment{ 395 - Did: user.Did, 396 - Rkey: tid.TID(), 397 - IssueAt: issue.AtUri().String(), 398 - ReplyTo: replyTo, 399 - Body: body, 400 - Created: time.Now(), 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, 401 427 } 402 428 if err = rp.validator.ValidateIssueComment(&comment); err != nil { 403 429 l.Error("failed to validate comment", "err", err) ··· 434 460 } 435 461 }() 436 462 437 - commentId, err := db.AddIssueComment(rp.db, comment) 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) 438 472 if err != nil { 439 473 l.Error("failed to create comment", "err", err) 440 474 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 441 475 return 442 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 + } 443 483 444 484 // reset atUri to make rollback a no-op 445 485 atUri = "" ··· 447 487 // notify about the new comment 448 488 comment.Id = commentId 449 489 450 - rawMentions := markup.FindUserMentions(comment.Body) 451 - idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 452 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 453 - var mentions []syntax.DID 454 - for _, ident := range idents { 455 - if ident != nil && !ident.Handle.IsInvalidHandle() { 456 - mentions = append(mentions, ident.DID) 457 - } 458 - } 459 490 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 460 491 461 492 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 476 507 commentId := chi.URLParam(r, "commentId") 477 508 comments, err := db.GetIssueComments( 478 509 rp.db, 479 - db.FilterEq("id", commentId), 510 + orm.FilterEq("id", commentId), 480 511 ) 481 512 if err != nil { 482 513 l.Error("failed to fetch comment", "id", commentId) ··· 512 543 commentId := chi.URLParam(r, "commentId") 513 544 comments, err := db.GetIssueComments( 514 545 rp.db, 515 - db.FilterEq("id", commentId), 546 + orm.FilterEq("id", commentId), 516 547 ) 517 548 if err != nil { 518 549 l.Error("failed to fetch comment", "id", commentId) ··· 554 585 newComment := comment 555 586 newComment.Body = newBody 556 587 newComment.Edited = &now 588 + newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 589 + 557 590 record := newComment.AsRecord() 558 591 559 - _, err = db.AddIssueComment(rp.db, newComment) 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) 560 601 if err != nil { 561 602 l.Error("failed to perferom update-description query", "err", err) 562 603 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 563 604 return 564 605 } 606 + tx.Commit() 565 607 566 608 // rkey is optional, it was introduced later 567 609 if newComment.Rkey != "" { ··· 611 653 commentId := chi.URLParam(r, "commentId") 612 654 comments, err := db.GetIssueComments( 613 655 rp.db, 614 - db.FilterEq("id", commentId), 656 + orm.FilterEq("id", commentId), 615 657 ) 616 658 if err != nil { 617 659 l.Error("failed to fetch comment", "id", commentId) ··· 647 689 commentId := chi.URLParam(r, "commentId") 648 690 comments, err := db.GetIssueComments( 649 691 rp.db, 650 - db.FilterEq("id", commentId), 692 + orm.FilterEq("id", commentId), 651 693 ) 652 694 if err != nil { 653 695 l.Error("failed to fetch comment", "id", commentId) ··· 683 725 commentId := chi.URLParam(r, "commentId") 684 726 comments, err := db.GetIssueComments( 685 727 rp.db, 686 - db.FilterEq("id", commentId), 728 + orm.FilterEq("id", commentId), 687 729 ) 688 730 if err != nil { 689 731 l.Error("failed to fetch comment", "id", commentId) ··· 710 752 711 753 // optimistic deletion 712 754 deleted := time.Now() 713 - err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id)) 755 + err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 714 756 if err != nil { 715 757 l.Error("failed to delete comment", "err", err) 716 758 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 799 841 800 842 issues, err = db.GetIssues( 801 843 rp.db, 802 - db.FilterIn("id", res.Hits), 844 + orm.FilterIn("id", res.Hits), 803 845 ) 804 846 if err != nil { 805 847 l.Error("failed to get issues", "err", err) ··· 815 857 issues, err = db.GetIssuesPaginated( 816 858 rp.db, 817 859 page, 818 - db.FilterEq("repo_at", f.RepoAt()), 819 - db.FilterEq("open", openInt), 860 + orm.FilterEq("repo_at", f.RepoAt()), 861 + orm.FilterEq("open", openInt), 820 862 ) 821 863 if err != nil { 822 864 l.Error("failed to get issues", "err", err) ··· 827 869 828 870 labelDefs, err := db.GetLabelDefinitions( 829 871 rp.db, 830 - db.FilterIn("at_uri", f.Labels), 831 - db.FilterContains("scope", tangled.RepoIssueNSID), 872 + orm.FilterIn("at_uri", f.Labels), 873 + orm.FilterContains("scope", tangled.RepoIssueNSID), 832 874 ) 833 875 if err != nil { 834 876 l.Error("failed to fetch labels", "err", err) ··· 870 912 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 871 913 }) 872 914 case http.MethodPost: 915 + body := r.FormValue("body") 916 + mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 917 + 873 918 issue := &models.Issue{ 874 - RepoAt: f.RepoAt(), 875 - Rkey: tid.TID(), 876 - Title: r.FormValue("title"), 877 - Body: r.FormValue("body"), 878 - Open: true, 879 - Did: user.Did, 880 - Created: time.Now(), 881 - Repo: f, 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, 882 929 } 883 930 884 931 if err := rp.validator.ValidateIssue(issue); err != nil { ··· 946 993 // everything is successful, do not rollback the atproto record 947 994 atUri = "" 948 995 949 - rawMentions := markup.FindUserMentions(issue.Body) 950 - idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 951 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 952 - var mentions []syntax.DID 953 - for _, ident := range idents { 954 - if ident != nil && !ident.Handle.IsInvalidHandle() { 955 - mentions = append(mentions, ident.DID) 956 - } 957 - } 958 996 rp.notifier.NewIssue(r.Context(), issue, mentions) 959 997 960 998 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
+19 -18
appview/knots/knots.go
··· 21 21 "tangled.org/core/appview/xrpcclient" 22 22 "tangled.org/core/eventconsumer" 23 23 "tangled.org/core/idresolver" 24 + "tangled.org/core/orm" 24 25 "tangled.org/core/rbac" 25 26 "tangled.org/core/tid" 26 27 ··· 72 73 user := k.OAuth.GetUser(r) 73 74 registrations, err := db.GetRegistrations( 74 75 k.Db, 75 - db.FilterEq("did", user.Did), 76 + orm.FilterEq("did", user.Did), 76 77 ) 77 78 if err != nil { 78 79 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 102 103 103 104 registrations, err := db.GetRegistrations( 104 105 k.Db, 105 - db.FilterEq("did", user.Did), 106 - db.FilterEq("domain", domain), 106 + orm.FilterEq("did", user.Did), 107 + orm.FilterEq("domain", domain), 107 108 ) 108 109 if err != nil { 109 110 l.Error("failed to get registrations", "err", err) ··· 127 128 repos, err := db.GetRepos( 128 129 k.Db, 129 130 0, 130 - db.FilterEq("knot", domain), 131 + orm.FilterEq("knot", domain), 131 132 ) 132 133 if err != nil { 133 134 l.Error("failed to get knot repos", "err", err) ··· 293 294 // get record from db first 294 295 registrations, err := db.GetRegistrations( 295 296 k.Db, 296 - db.FilterEq("did", user.Did), 297 - db.FilterEq("domain", domain), 297 + orm.FilterEq("did", user.Did), 298 + orm.FilterEq("domain", domain), 298 299 ) 299 300 if err != nil { 300 301 l.Error("failed to get registration", "err", err) ··· 321 322 322 323 err = db.DeleteKnot( 323 324 tx, 324 - db.FilterEq("did", user.Did), 325 - db.FilterEq("domain", domain), 325 + orm.FilterEq("did", user.Did), 326 + orm.FilterEq("domain", domain), 326 327 ) 327 328 if err != nil { 328 329 l.Error("failed to delete registration", "err", err) ··· 402 403 // get record from db first 403 404 registrations, err := db.GetRegistrations( 404 405 k.Db, 405 - db.FilterEq("did", user.Did), 406 - db.FilterEq("domain", domain), 406 + orm.FilterEq("did", user.Did), 407 + orm.FilterEq("domain", domain), 407 408 ) 408 409 if err != nil { 409 410 l.Error("failed to get registration", "err", err) ··· 493 494 // Get updated registration to show 494 495 registrations, err = db.GetRegistrations( 495 496 k.Db, 496 - db.FilterEq("did", user.Did), 497 - db.FilterEq("domain", domain), 497 + orm.FilterEq("did", user.Did), 498 + orm.FilterEq("domain", domain), 498 499 ) 499 500 if err != nil { 500 501 l.Error("failed to get registration", "err", err) ··· 529 530 530 531 registrations, err := db.GetRegistrations( 531 532 k.Db, 532 - db.FilterEq("did", user.Did), 533 - db.FilterEq("domain", domain), 534 - db.FilterIsNot("registered", "null"), 533 + orm.FilterEq("did", user.Did), 534 + orm.FilterEq("domain", domain), 535 + orm.FilterIsNot("registered", "null"), 535 536 ) 536 537 if err != nil { 537 538 l.Error("failed to get registration", "err", err) ··· 637 638 638 639 registrations, err := db.GetRegistrations( 639 640 k.Db, 640 - db.FilterEq("did", user.Did), 641 - db.FilterEq("domain", domain), 642 - db.FilterIsNot("registered", "null"), 641 + orm.FilterEq("did", user.Did), 642 + orm.FilterEq("domain", domain), 643 + orm.FilterIsNot("registered", "null"), 643 644 ) 644 645 if err != nil { 645 646 l.Error("failed to get registration", "err", err)
+5 -4
appview/labels/labels.go
··· 16 16 "tangled.org/core/appview/oauth" 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/validator" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 "tangled.org/core/tid" 21 22 ··· 88 89 repoAt := r.Form.Get("repo") 89 90 subjectUri := r.Form.Get("subject") 90 91 91 - repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt)) 92 + repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt)) 92 93 if err != nil { 93 94 fail("Failed to get repository.", err) 94 95 return 95 96 } 96 97 97 98 // find all the labels that this repo subscribes to 98 - repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt)) 99 + repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt)) 99 100 if err != nil { 100 101 fail("Failed to get labels for this repository.", err) 101 102 return ··· 106 107 labelAts = append(labelAts, rl.LabelAt.String()) 107 108 } 108 109 109 - actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts)) 110 + actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts)) 110 111 if err != nil { 111 112 fail("Invalid form data.", err) 112 113 return 113 114 } 114 115 115 116 // calculate the start state by applying already known labels 116 - existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) 117 + existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri)) 117 118 if err != nil { 118 119 fail("Invalid form data.", err) 119 120 return
+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 + }
+3 -2
appview/middleware/middleware.go
··· 18 18 "tangled.org/core/appview/pagination" 19 19 "tangled.org/core/appview/reporesolver" 20 20 "tangled.org/core/idresolver" 21 + "tangled.org/core/orm" 21 22 "tangled.org/core/rbac" 22 23 ) 23 24 ··· 217 218 218 219 repo, err := db.GetRepo( 219 220 mw.db, 220 - db.FilterEq("did", id.DID.String()), 221 - db.FilterEq("name", repoName), 221 + orm.FilterEq("did", id.DID.String()), 222 + orm.FilterEq("name", repoName), 222 223 ) 223 224 if err != nil { 224 225 log.Println("failed to resolve repo", "err", err)
+70 -34
appview/models/issue.go
··· 10 10 ) 11 11 12 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 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 24 26 25 27 // optionally, populate this when querying for reverse mappings 26 28 // like comment counts, parent repo etc. ··· 34 36 } 35 37 36 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 + } 37 47 return tangled.RepoIssue{ 38 - Repo: i.RepoAt.String(), 39 - Title: i.Title, 40 - Body: &i.Body, 41 - CreatedAt: i.Created.Format(time.RFC3339), 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), 42 54 } 43 55 } 44 56 ··· 161 173 } 162 174 163 175 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 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 173 187 } 174 188 175 189 func (i *IssueComment) AtUri() syntax.ATURI { ··· 177 191 } 178 192 179 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 + } 180 202 return tangled.RepoIssueComment{ 181 - Body: i.Body, 182 - Issue: i.IssueAt, 183 - CreatedAt: i.Created.Format(time.RFC3339), 184 - ReplyTo: i.ReplyTo, 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, 185 209 } 186 210 } 187 211 ··· 205 229 return nil, err 206 230 } 207 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 + 208 242 comment := IssueComment{ 209 - Did: ownerDid, 210 - Rkey: rkey, 211 - Body: record.Body, 212 - IssueAt: record.Issue, 213 - ReplyTo: record.ReplyTo, 214 - Created: created, 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, 215 251 } 216 252 217 253 return &comment, nil
+3 -1
appview/models/profile.go
··· 111 111 } 112 112 113 113 type ByMonth struct { 114 + Commits int 114 115 RepoEvents []RepoEvent 115 116 IssueEvents IssueEvents 116 117 PullEvents PullEvents ··· 119 120 func (b ByMonth) IsEmpty() bool { 120 121 return len(b.RepoEvents) == 0 && 121 122 len(b.IssueEvents.Items) == 0 && 122 - len(b.PullEvents.Items) == 0 123 + len(b.PullEvents.Items) == 0 && 124 + b.Commits == 0 123 125 } 124 126 125 127 type IssueEvents struct {
+41 -3
appview/models/pull.go
··· 66 66 TargetBranch string 67 67 State PullState 68 68 Submissions []*PullSubmission 69 + Mentions []syntax.DID 70 + References []syntax.ATURI 69 71 70 72 // stacking 71 73 StackId string // nullable string ··· 92 94 source.Repo = &s 93 95 } 94 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 + } 95 105 96 106 record := tangled.RepoPull{ 97 - Title: p.Title, 98 - Body: &p.Body, 99 - CreatedAt: p.Created.Format(time.RFC3339), 107 + Title: p.Title, 108 + Body: &p.Body, 109 + Mentions: mentions, 110 + References: references, 111 + CreatedAt: p.Created.Format(time.RFC3339), 100 112 Target: &tangled.RepoPull_Target{ 101 113 Repo: p.RepoAt.String(), 102 114 Branch: p.TargetBranch, ··· 146 158 147 159 // content 148 160 Body string 161 + 162 + // meta 163 + Mentions []syntax.DID 164 + References []syntax.ATURI 149 165 150 166 // meta 151 167 Created time.Time 152 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 + // } 153 191 154 192 func (p *Pull) LastRoundNumber() int { 155 193 return len(p.Submissions) - 1
+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
··· 11 11 "tangled.org/core/appview/oauth" 12 12 "tangled.org/core/appview/pages" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 type Notifications struct { ··· 53 54 54 55 total, err := db.CountNotifications( 55 56 n.db, 56 - db.FilterEq("recipient_did", user.Did), 57 + orm.FilterEq("recipient_did", user.Did), 57 58 ) 58 59 if err != nil { 59 60 l.Error("failed to get total notifications", "err", err) ··· 64 65 notifications, err := db.GetNotificationsWithEntities( 65 66 n.db, 66 67 page, 67 - db.FilterEq("recipient_did", user.Did), 68 + orm.FilterEq("recipient_did", user.Did), 68 69 ) 69 70 if err != nil { 70 71 l.Error("failed to get notifications", "err", err) ··· 96 97 97 98 count, err := db.CountNotifications( 98 99 n.db, 99 - db.FilterEq("recipient_did", user.Did), 100 - db.FilterEq("read", 0), 100 + orm.FilterEq("recipient_did", user.Did), 101 + orm.FilterEq("read", 0), 101 102 ) 102 103 if err != nil { 103 104 http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77 -66
appview/notify/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "log" 6 - "maps" 7 6 "slices" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 11 "tangled.org/core/appview/models" 13 12 "tangled.org/core/appview/notify" 14 13 "tangled.org/core/idresolver" 14 + "tangled.org/core/orm" 15 + "tangled.org/core/sets" 15 16 ) 16 17 17 18 const ( 18 - maxMentions = 5 19 + maxMentions = 8 19 20 ) 20 21 21 22 type databaseNotifier struct { ··· 42 43 return 43 44 } 44 45 var err error 45 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt))) 46 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt))) 46 47 if err != nil { 47 48 log.Printf("NewStar: failed to get repos: %v", err) 48 49 return 49 50 } 50 51 51 52 actorDid := syntax.DID(star.Did) 52 - recipients := []syntax.DID{syntax.DID(repo.Did)} 53 + recipients := sets.Singleton(syntax.DID(repo.Did)) 53 54 eventType := models.NotificationTypeRepoStarred 54 55 entityType := "repo" 55 56 entityId := star.RepoAt.String() ··· 74 75 } 75 76 76 77 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())) 78 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 84 79 if err != nil { 85 80 log.Printf("failed to fetch collaborators: %v", err) 86 81 return 87 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)) 88 89 for _, c := range collaborators { 89 - recipients = append(recipients, c.SubjectDid) 90 + recipients.Insert(c.SubjectDid) 91 + } 92 + for _, m := range mentions { 93 + recipients.Remove(m) 90 94 } 91 95 92 96 actorDid := syntax.DID(issue.Did) ··· 108 112 ) 109 113 n.notifyEvent( 110 114 actorDid, 111 - mentions, 115 + sets.Collect(slices.Values(mentions)), 112 116 models.NotificationTypeUserMentioned, 113 117 entityType, 114 118 entityId, ··· 119 123 } 120 124 121 125 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)) 126 + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 123 127 if err != nil { 124 128 log.Printf("NewIssueComment: failed to get issues: %v", err) 125 129 return ··· 130 134 } 131 135 issue := issues[0] 132 136 133 - var recipients []syntax.DID 134 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 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)) 135 143 136 144 if comment.IsReply() { 137 145 // if this comment is a reply, then notify everybody in that thread 138 146 parentAtUri := *comment.ReplyTo 139 - allThreads := issue.CommentList() 140 147 141 148 // find the parent thread, and add all DIDs from here to the recipient list 142 - for _, t := range allThreads { 149 + for _, t := range issue.CommentList() { 143 150 if t.Self.AtUri().String() == parentAtUri { 144 - recipients = append(recipients, t.Participants()...) 151 + for _, p := range t.Participants() { 152 + recipients.Insert(p) 153 + } 145 154 } 146 155 } 147 156 } else { 148 157 // not a reply, notify just the issue author 149 - recipients = append(recipients, syntax.DID(issue.Did)) 158 + recipients.Insert(syntax.DID(issue.Did)) 159 + } 160 + 161 + for _, m := range mentions { 162 + recipients.Remove(m) 150 163 } 151 164 152 165 actorDid := syntax.DID(comment.Did) ··· 168 181 ) 169 182 n.notifyEvent( 170 183 actorDid, 171 - mentions, 184 + sets.Collect(slices.Values(mentions)), 172 185 models.NotificationTypeUserMentioned, 173 186 entityType, 174 187 entityId, ··· 184 197 185 198 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 186 199 actorDid := syntax.DID(follow.UserDid) 187 - recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 200 + recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 188 201 eventType := models.NotificationTypeFollowed 189 202 entityType := "follow" 190 203 entityId := follow.UserDid ··· 207 220 } 208 221 209 222 func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) { 210 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt))) 223 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt))) 211 224 if err != nil { 212 225 log.Printf("NewPull: failed to get repos: %v", err) 213 226 return 214 227 } 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())) 228 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 222 229 if err != nil { 223 230 log.Printf("failed to fetch collaborators: %v", err) 224 231 return 225 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)) 226 238 for _, c := range collaborators { 227 - recipients = append(recipients, c.SubjectDid) 239 + recipients.Insert(c.SubjectDid) 228 240 } 229 241 230 242 actorDid := syntax.DID(pull.OwnerDid) ··· 258 270 return 259 271 } 260 272 261 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt)) 273 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 262 274 if err != nil { 263 275 log.Printf("NewPullComment: failed to get repos: %v", err) 264 276 return ··· 267 279 // build up the recipients list: 268 280 // - repo owner 269 281 // - all pull participants 270 - var recipients []syntax.DID 271 - recipients = append(recipients, syntax.DID(repo.Did)) 282 + // - remove those already mentioned 283 + recipients := sets.Singleton(syntax.DID(repo.Did)) 272 284 for _, p := range pull.Participants() { 273 - recipients = append(recipients, syntax.DID(p)) 285 + recipients.Insert(syntax.DID(p)) 286 + } 287 + for _, m := range mentions { 288 + recipients.Remove(m) 274 289 } 275 290 276 291 actorDid := syntax.DID(comment.OwnerDid) ··· 294 309 ) 295 310 n.notifyEvent( 296 311 actorDid, 297 - mentions, 312 + sets.Collect(slices.Values(mentions)), 298 313 models.NotificationTypeUserMentioned, 299 314 entityType, 300 315 entityId, ··· 321 336 } 322 337 323 338 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())) 339 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 331 340 if err != nil { 332 341 log.Printf("failed to fetch collaborators: %v", err) 333 342 return 334 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)) 335 350 for _, c := range collaborators { 336 - recipients = append(recipients, c.SubjectDid) 351 + recipients.Insert(c.SubjectDid) 337 352 } 338 353 for _, p := range issue.Participants() { 339 - recipients = append(recipients, syntax.DID(p)) 354 + recipients.Insert(syntax.DID(p)) 340 355 } 341 356 342 357 entityType := "pull" ··· 366 381 367 382 func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) { 368 383 // Get repo details 369 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt))) 384 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt))) 370 385 if err != nil { 371 386 log.Printf("NewPullState: failed to get repos: %v", err) 372 387 return 373 388 } 374 389 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())) 390 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 381 391 if err != nil { 382 392 log.Printf("failed to fetch collaborators: %v", err) 383 393 return 384 394 } 395 + 396 + // build up the recipients list: 397 + // - repo owner 398 + // - all pull participants 399 + recipients := sets.Singleton(syntax.DID(repo.Did)) 385 400 for _, c := range collaborators { 386 - recipients = append(recipients, c.SubjectDid) 401 + recipients.Insert(c.SubjectDid) 387 402 } 388 403 for _, p := range pull.Participants() { 389 - recipients = append(recipients, syntax.DID(p)) 404 + recipients.Insert(syntax.DID(p)) 390 405 } 391 406 392 407 entityType := "pull" ··· 422 437 423 438 func (n *databaseNotifier) notifyEvent( 424 439 actorDid syntax.DID, 425 - recipients []syntax.DID, 440 + recipients sets.Set[syntax.DID], 426 441 eventType models.NotificationType, 427 442 entityType string, 428 443 entityId string, ··· 430 445 issueId *int64, 431 446 pullId *int64, 432 447 ) { 433 - if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions { 434 - recipients = recipients[:maxMentions] 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 435 451 } 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 - } 452 + 453 + recipients.Remove(actorDid) 443 454 444 455 prefMap, err := db.GetNotificationPreferences( 445 456 n.db, 446 - db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 457 + orm.FilterIn("user_did", slices.Collect(recipients.All())), 447 458 ) 448 459 if err != nil { 449 460 // failed to get prefs for users ··· 459 470 defer tx.Rollback() 460 471 461 472 // filter based on preferences 462 - for recipientDid := range recipientSet { 473 + for recipientDid := range recipients.All() { 463 474 prefs, ok := prefMap[recipientDid] 464 475 if !ok { 465 476 prefs = models.DefaultNotificationPreferences(recipientDid)
-1
appview/notify/merged_notifier.go
··· 39 39 v.Call(in) 40 40 }(n) 41 41 } 42 - wg.Wait() 43 42 } 44 43 45 44 func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
+3 -2
appview/oauth/handler.go
··· 16 16 "tangled.org/core/api/tangled" 17 17 "tangled.org/core/appview/db" 18 18 "tangled.org/core/consts" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/tid" 20 21 ) 21 22 ··· 97 98 // and create an sh.tangled.spindle.member record with that 98 99 spindleMembers, err := db.GetSpindleMembers( 99 100 o.Db, 100 - db.FilterEq("instance", "spindle.tangled.sh"), 101 - db.FilterEq("subject", did), 101 + orm.FilterEq("instance", "spindle.tangled.sh"), 102 + orm.FilterEq("subject", did), 102 103 ) 103 104 if err != nil { 104 105 l.Error("failed to get spindle members", "err", err)
+7 -2
appview/pages/funcmap.go
··· 25 25 "github.com/dustin/go-humanize" 26 26 "github.com/go-enry/go-enry/v2" 27 27 "github.com/yuin/goldmark" 28 + emoji "github.com/yuin/goldmark-emoji" 28 29 "tangled.org/core/appview/filetree" 29 30 "tangled.org/core/appview/models" 30 31 "tangled.org/core/appview/pages/markup" ··· 162 163 } 163 164 return pairs, nil 164 165 }, 165 - "append": func(s []string, values ...string) []string { 166 + "append": func(s []any, values ...any) []any { 166 167 s = append(s, values...) 167 168 return s 168 169 }, ··· 261 262 }, 262 263 "description": func(text string) template.HTML { 263 264 p.rctx.RendererType = markup.RendererTypeDefault 264 - htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New()) 265 + htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New( 266 + goldmark.WithExtensions( 267 + emoji.Emoji, 268 + ), 269 + )) 265 270 sanitized := p.rctx.SanitizeDescription(htmlString) 266 271 return template.HTML(sanitized) 267 272 },
+2 -26
appview/pages/markup/markdown.go
··· 12 12 13 13 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 14 14 "github.com/alecthomas/chroma/v2/styles" 15 - treeblood "github.com/wyatt915/goldmark-treeblood" 16 15 "github.com/yuin/goldmark" 16 + "github.com/yuin/goldmark-emoji" 17 17 highlighting "github.com/yuin/goldmark-highlighting/v2" 18 18 "github.com/yuin/goldmark/ast" 19 19 "github.com/yuin/goldmark/extension" ··· 65 65 extension.NewFootnote( 66 66 extension.WithFootnoteIDPrefix([]byte("footnote")), 67 67 ), 68 - treeblood.MathML(), 69 68 callout.CalloutExtention, 70 69 textension.AtExt, 70 + emoji.Emoji, 71 71 ), 72 72 goldmark.WithParserOptions( 73 73 parser.WithAutoHeadingID(), ··· 302 302 } 303 303 304 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 305 } 330 306 331 307 func isAbsoluteUrl(link string) bool {
+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 + }
+4 -3
appview/pages/pages.go
··· 31 31 "github.com/bluesky-social/indigo/atproto/identity" 32 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 33 "github.com/go-git/go-git/v5/plumbing" 34 - "github.com/go-git/go-git/v5/plumbing/object" 35 34 ) 36 35 37 36 //go:embed templates/* static legal ··· 641 640 } 642 641 643 642 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 644 - return p.executePlain("fragments/starBtn", w, params) 643 + return p.executePlain("fragments/starBtn-oob", w, params) 645 644 } 646 645 647 646 type RepoIndexParams struct { ··· 649 648 RepoInfo repoinfo.RepoInfo 650 649 Active string 651 650 TagMap map[string][]string 652 - CommitsTrunc []*object.Commit 651 + CommitsTrunc []types.Commit 653 652 TagsTrunc []*types.TagReference 654 653 BranchesTrunc []types.Branch 655 654 // ForkInfo *types.ForkInfo ··· 934 933 Active string 935 934 Issue *models.Issue 936 935 CommentList []models.CommentListItem 936 + Backlinks []models.RichReferenceLink 937 937 LabelDefs map[string]*models.LabelDefinition 938 938 939 939 OrderedReactionKinds []models.ReactionKind ··· 1087 1087 Pull *models.Pull 1088 1088 Stack models.Stack 1089 1089 AbandonedPulls []*models.Pull 1090 + Backlinks []models.RichReferenceLink 1090 1091 BranchDeleteStatus *models.BranchDeleteStatus 1091 1092 MergeCheck types.MergeCheckResponse 1092 1093 ResubmitCheck ResubmitResult
+5
appview/pages/templates/fragments/starBtn-oob.html
··· 1 + {{ define "fragments/starBtn-oob" }} 2 + <div hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'> 3 + {{ template "fragments/starBtn" . }} 4 + </div> 5 + {{ end }}
+1 -3
appview/pages/templates/fragments/starBtn.html
··· 1 1 {{ define "fragments/starBtn" }} 2 + {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 2 3 <button 3 4 id="starBtn" 4 5 class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" ··· 10 11 {{ end }} 11 12 12 13 hx-trigger="click" 13 - hx-target="this" 14 - hx-swap="outerHTML" 15 - hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]' 16 14 hx-disabled-elt="#starBtn" 17 15 > 18 16 {{ if .IsStarred }}
+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 +
+1 -1
appview/pages/templates/knots/index.html
··· 105 105 {{ define "docsButton" }} 106 106 <a 107 107 class="btn flex items-center gap-2" 108 - href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 108 + href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md"> 109 109 {{ i "book" "size-4" }} 110 110 docs 111 111 </a>
+34 -9
appview/pages/templates/repo/commit.html
··· 25 25 </div> 26 26 27 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 }} 28 + <p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300"> 29 + {{ template "attribution" . }} 36 30 37 31 <span class="px-1 select-none before:content-['\00B7']"></span> 38 - {{ template "repo/fragments/time" $commit.Author.When }} 32 + {{ template "repo/fragments/time" $commit.Committer.When }} 39 33 <span class="px-1 select-none before:content-['\00B7']"></span> 40 34 41 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> ··· 78 72 79 73 </section> 80 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 }} 81 106 82 107 {{ define "topbarLayout" }} 83 108 <header class="col-span-full" style="z-index: 20;">
+1 -1
appview/pages/templates/repo/empty.html
··· 26 26 {{ else if (and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) }} 27 27 {{ $knot := .RepoInfo.Knot }} 28 28 {{ if eq $knot "knot1.tangled.sh" }} 29 - {{ $knot = "tangled.sh" }} 29 + {{ $knot = "tangled.org" }} 30 30 {{ end }} 31 31 <div class="w-full flex place-content-center"> 32 32 <div class="py-6 w-fit flex flex-col gap-4">
+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" "size-3" }} 18 + </span> 19 + {{ else if eq .Kind.String "issues" }} 20 + <span class="text-green-600 dark:text-green-500"> 21 + {{ i "circle-dot" "size-3" }} 22 + </span> 23 + {{ else if .State.IsOpen }} 24 + <span class="text-green-600 dark:text-green-500"> 25 + {{ i "git-pull-request" "size-3" }} 26 + </span> 27 + {{ else if .State.IsMerged }} 28 + <span class="text-purple-600 dark:text-purple-500"> 29 + {{ i "git-merge" "size-3" }} 30 + </span> 31 + {{ else }} 32 + <span class="text-gray-600 dark:text-gray-300"> 33 + {{ i "git-pull-request-closed" "size-3" }} 34 + </span> 35 + {{ end }} 36 + <a href="{{ . }}" class="line-clamp-1 text-sm"><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 }}
+1 -16
appview/pages/templates/repo/fragments/participants.html
··· 6 6 <span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span> 7 7 <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span> 8 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> 9 + {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }} 25 10 </div> 26 11 {{ end }}
+31 -9
appview/pages/templates/repo/index.html
··· 14 14 {{ end }} 15 15 <div class="flex items-center justify-between pb-5"> 16 16 {{ block "branchSelector" . }}{{ end }} 17 - <div class="flex md:hidden items-center gap-2"> 17 + <div class="flex md:hidden items-center gap-3"> 18 18 <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold"> 19 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 20 20 </a> ··· 47 47 <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap"> 48 48 {{ range $value := .Languages }} 49 49 <div 50 - class="flex flex-grow items-center gap-2 text-xs align-items-center justify-center" 50 + class="flex items-center gap-2 text-xs align-items-center justify-center" 51 51 > 52 52 {{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }} 53 53 <div>{{ or $value.Name "Other" }} ··· 66 66 67 67 {{ define "branchSelector" }} 68 68 <div class="flex gap-2 items-center justify-between w-full"> 69 - <div class="flex gap-2 items-center"> 69 + <div class="flex gap-2 items-stretch"> 70 70 <select 71 71 onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 72 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 228 <span 229 229 class="mx-1 before:content-['ยท'] before:select-none" 230 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> 231 + {{ template "attribution" (list . $.EmailToDid) }} 237 232 <div class="inline-block px-1 select-none after:content-['ยท']"></div> 238 233 {{ template "repo/fragments/time" .Committer.When }} 239 234 ··· 259 254 {{ end }} 260 255 </div> 261 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> 262 284 {{ end }} 263 285 264 286 {{ define "branchList" }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 19 19 {{ end }} 20 20 21 21 {{ define "timestamp" }} 22 - <a href="#{{ .Comment.Id }}" 22 + <a href="#comment-{{ .Comment.Id }}" 23 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 }}"> 24 + id="comment-{{ .Comment.Id }}"> 25 25 {{ if .Comment.Deleted }} 26 26 {{ template "repo/fragments/shortTimeAgo" .Comment.Deleted }} 27 27 {{ else if .Comment.Edited }}
+3
appview/pages/templates/repo/issues/issue.html
··· 20 20 "Subject" $.Issue.AtUri 21 21 "State" $.Issue.Labels) }} 22 22 {{ template "repo/fragments/participants" $.Issue.Participants }} 23 + {{ template "repo/fragments/backlinks" 24 + (dict "RepoInfo" $.RepoInfo 25 + "Backlinks" $.Backlinks) }} 23 26 {{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }} 24 27 </div> 25 28 </div>
+40 -23
appview/pages/templates/repo/log.html
··· 17 17 <div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700"> 18 18 {{ $grid := "grid grid-cols-14 gap-4" }} 19 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> 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 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 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 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> 25 24 </div> 26 25 {{ range $index, $commit := .Commits }} 27 26 {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 28 27 <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 }} 28 + <div class="align-top col-span-3"> 29 + {{ template "attribution" (list $commit $.EmailToDid) }} 36 30 </div> 37 31 <div class="align-top font-mono flex items-start col-span-3"> 38 32 {{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }} ··· 61 55 <div class="align-top col-span-6"> 62 56 <div> 63 57 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a> 58 + 64 59 {{ if gt (len $messageParts) 1 }} 65 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> 66 61 {{ end }} ··· 72 67 </span> 73 68 {{ end }} 74 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> 75 78 </div> 76 79 77 80 {{ if gt (len $messageParts) 1 }} 78 81 <p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p> 79 82 {{ 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 83 </div> 88 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> 89 85 </div> ··· 152 148 </a> 153 149 </span> 154 150 <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> 151 + {{ template "attribution" (list $commit $.EmailToDid) }} 162 152 <div class="inline-block px-1 select-none after:content-['ยท']"></div> 163 153 <span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span> 164 154 ··· 176 166 </div> 177 167 </section> 178 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> 179 196 {{ end }} 180 197 181 198 {{ define "repoAfter" }}
+3
appview/pages/templates/repo/pulls/pull.html
··· 21 21 "Subject" $.Pull.AtUri 22 22 "State" $.Pull.Labels) }} 23 23 {{ template "repo/fragments/participants" $.Pull.Participants }} 24 + {{ template "repo/fragments/backlinks" 25 + (dict "RepoInfo" $.RepoInfo 26 + "Backlinks" $.Backlinks) }} 24 27 {{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }} 25 28 </div> 26 29 </div>
+1 -1
appview/pages/templates/strings/string.html
··· 17 17 <span class="select-none">/</span> 18 18 <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 19 </div> 20 - <div class="flex gap-2 text-base"> 20 + <div class="flex gap-2 items-stretch text-base"> 21 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 22 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 23 hx-boost="true"
+3 -1
appview/pages/templates/user/followers.html
··· 19 19 "FollowersCount" .FollowersCount 20 20 "FollowingCount" .FollowingCount) }} 21 21 {{ else }} 22 - <p class="px-6 dark:text-white">This user does not have any followers yet.</p> 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> 23 25 {{ end }} 24 26 </div> 25 27 {{ end }}
+3 -1
appview/pages/templates/user/following.html
··· 19 19 "FollowersCount" .FollowersCount 20 20 "FollowingCount" .FollowingCount) }} 21 21 {{ else }} 22 - <p class="px-6 dark:text-white">This user does not follow anyone yet.</p> 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> 23 25 {{ end }} 24 26 </div> 25 27 {{ end }}
+2 -2
appview/pages/templates/user/fragments/followCard.html
··· 6 6 <img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" /> 7 7 </div> 8 8 9 - <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full"> 9 + <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0"> 10 10 <div class="flex-1 min-h-0 justify-around flex flex-col"> 11 11 <a href="/{{ $userIdent }}"> 12 12 <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span> 13 13 </a> 14 14 {{ with .Profile }} 15 - <p class="text-sm pb-2 md:pb-2">{{.Description}}</p> 15 + <p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p> 16 16 {{ end }} 17 17 <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 18 18 <span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+20 -2
appview/pages/templates/user/overview.html
··· 16 16 <p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p> 17 17 <div class="flex flex-col gap-4 relative"> 18 18 {{ if .ProfileTimeline.IsEmpty }} 19 - <p class="dark:text-white">This user does not have any activity yet.</p> 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> 20 24 {{ end }} 21 25 22 26 {{ with .ProfileTimeline }} ··· 33 37 </p> 34 38 35 39 <div class="flex flex-col gap-1"> 40 + {{ block "commits" .Commits }} {{ end }} 36 41 {{ block "repoEvents" .RepoEvents }} {{ end }} 37 42 {{ block "issueEvents" .IssueEvents }} {{ end }} 38 43 {{ block "pullEvents" .PullEvents }} {{ end }} ··· 43 48 {{ end }} 44 49 {{ end }} 45 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 }} 46 60 {{ end }} 47 61 48 62 {{ define "repoEvents" }} ··· 244 258 {{ template "user/fragments/repoCard" (list $ . false) }} 245 259 </div> 246 260 {{ else }} 247 - <p class="dark:text-white">This user does not have any pinned repos.</p> 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> 248 266 {{ end }} 249 267 </div> 250 268 </div>
+3 -1
appview/pages/templates/user/repos.html
··· 13 13 {{ template "user/fragments/repoCard" (list $ . false) }} 14 14 </div> 15 15 {{ else }} 16 - <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 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> 17 19 {{ end }} 18 20 </div> 19 21 {{ end }}
+9 -6
appview/pages/templates/user/signup.html
··· 43 43 page to complete your registration. 44 44 </span> 45 45 <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div> 46 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 47 47 </div> 48 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 49 <span>join now</span> 50 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> 51 59 </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 60 </main> 58 61 </body> 59 62 </html>
+3 -1
appview/pages/templates/user/starred.html
··· 13 13 {{ template "user/fragments/repoCard" (list $ . true) }} 14 14 </div> 15 15 {{ else }} 16 - <p class="px-6 dark:text-white">This user does not have any starred repos yet.</p> 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> 17 19 {{ end }} 18 20 </div> 19 21 {{ end }}
+3 -1
appview/pages/templates/user/strings.html
··· 13 13 {{ template "singleString" (list $ .) }} 14 14 </div> 15 15 {{ else }} 16 - <p class="px-6 dark:text-white">This user does not have any strings yet.</p> 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> 17 19 {{ end }} 18 20 </div> 19 21 {{ end }}
+12 -11
appview/pipelines/pipelines.go
··· 16 16 "tangled.org/core/appview/reporesolver" 17 17 "tangled.org/core/eventconsumer" 18 18 "tangled.org/core/idresolver" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 spindlemodel "tangled.org/core/spindle/models" 21 22 ··· 81 82 ps, err := db.GetPipelineStatuses( 82 83 p.db, 83 84 30, 84 - db.FilterEq("repo_owner", f.Did), 85 - db.FilterEq("repo_name", f.Name), 86 - db.FilterEq("knot", f.Knot), 85 + orm.FilterEq("repo_owner", f.Did), 86 + orm.FilterEq("repo_name", f.Name), 87 + orm.FilterEq("knot", f.Knot), 87 88 ) 88 89 if err != nil { 89 90 l.Error("failed to query db", "err", err) ··· 122 123 ps, err := db.GetPipelineStatuses( 123 124 p.db, 124 125 1, 125 - db.FilterEq("repo_owner", f.Did), 126 - db.FilterEq("repo_name", f.Name), 127 - db.FilterEq("knot", f.Knot), 128 - db.FilterEq("id", pipelineId), 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), 129 130 ) 130 131 if err != nil { 131 132 l.Error("failed to query db", "err", err) ··· 189 190 ps, err := db.GetPipelineStatuses( 190 191 p.db, 191 192 1, 192 - db.FilterEq("repo_owner", f.Did), 193 - db.FilterEq("repo_name", f.Name), 194 - db.FilterEq("knot", f.Knot), 195 - db.FilterEq("id", pipelineId), 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), 196 197 ) 197 198 if err != nil || len(ps) != 1 { 198 199 l.Error("pipeline query failed", "err", err, "count", len(ps))
+2 -1
appview/pulls/opengraph.go
··· 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/models" 15 15 "tangled.org/core/appview/ogcard" 16 + "tangled.org/core/orm" 16 17 "tangled.org/core/patchutil" 17 18 "tangled.org/core/types" 18 19 ) ··· 276 277 } 277 278 278 279 // Get comment count from database 279 - comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID)) 280 + comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID)) 280 281 if err != nil { 281 282 log.Printf("failed to get pull comments: %v", err) 282 283 }
+77 -53
appview/pulls/pulls.go
··· 1 1 package pulls 2 2 3 3 import ( 4 + "context" 4 5 "database/sql" 5 6 "encoding/json" 6 7 "errors" ··· 18 19 "tangled.org/core/appview/config" 19 20 "tangled.org/core/appview/db" 20 21 pulls_indexer "tangled.org/core/appview/indexer/pulls" 22 + "tangled.org/core/appview/mentions" 21 23 "tangled.org/core/appview/models" 22 24 "tangled.org/core/appview/notify" 23 25 "tangled.org/core/appview/oauth" ··· 28 30 "tangled.org/core/appview/validator" 29 31 "tangled.org/core/appview/xrpcclient" 30 32 "tangled.org/core/idresolver" 33 + "tangled.org/core/orm" 31 34 "tangled.org/core/patchutil" 32 35 "tangled.org/core/rbac" 33 36 "tangled.org/core/tid" ··· 42 45 ) 43 46 44 47 type Pulls struct { 45 - oauth *oauth.OAuth 46 - repoResolver *reporesolver.RepoResolver 47 - pages *pages.Pages 48 - idResolver *idresolver.Resolver 49 - db *db.DB 50 - config *config.Config 51 - notifier notify.Notifier 52 - enforcer *rbac.Enforcer 53 - logger *slog.Logger 54 - validator *validator.Validator 55 - indexer *pulls_indexer.Indexer 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 56 60 } 57 61 58 62 func New( ··· 60 64 repoResolver *reporesolver.RepoResolver, 61 65 pages *pages.Pages, 62 66 resolver *idresolver.Resolver, 67 + mentionsResolver *mentions.Resolver, 63 68 db *db.DB, 64 69 config *config.Config, 65 70 notifier notify.Notifier, ··· 69 74 logger *slog.Logger, 70 75 ) *Pulls { 71 76 return &Pulls{ 72 - oauth: oauth, 73 - repoResolver: repoResolver, 74 - pages: pages, 75 - idResolver: resolver, 76 - db: db, 77 - config: config, 78 - notifier: notifier, 79 - enforcer: enforcer, 80 - logger: logger, 81 - validator: validator, 82 - indexer: indexer, 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, 83 89 } 84 90 } 85 91 ··· 151 157 return 152 158 } 153 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 + 154 167 // can be nil if this pull is not stacked 155 168 stack, _ := r.Context().Value("stack").(models.Stack) 156 169 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 178 191 ps, err := db.GetPipelineStatuses( 179 192 s.db, 180 193 len(shas), 181 - db.FilterEq("repo_owner", f.Did), 182 - db.FilterEq("repo_name", f.Name), 183 - db.FilterEq("knot", f.Knot), 184 - db.FilterIn("sha", 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), 185 198 ) 186 199 if err != nil { 187 200 log.Printf("failed to fetch pipeline statuses: %s", err) ··· 205 218 206 219 labelDefs, err := db.GetLabelDefinitions( 207 220 s.db, 208 - db.FilterIn("at_uri", f.Labels), 209 - db.FilterContains("scope", tangled.RepoPullNSID), 221 + orm.FilterIn("at_uri", f.Labels), 222 + orm.FilterContains("scope", tangled.RepoPullNSID), 210 223 ) 211 224 if err != nil { 212 225 log.Println("failed to fetch labels", err) ··· 225 238 Pull: pull, 226 239 Stack: stack, 227 240 AbandonedPulls: abandonedPulls, 241 + Backlinks: backlinks, 228 242 BranchDeleteStatus: branchDeleteStatus, 229 243 MergeCheck: mergeCheckResponse, 230 244 ResubmitCheck: resubmitResult, ··· 584 598 585 599 pulls, err := db.GetPulls( 586 600 s.db, 587 - db.FilterIn("id", ids), 601 + orm.FilterIn("id", ids), 588 602 ) 589 603 if err != nil { 590 604 log.Println("failed to get pulls", err) ··· 635 649 ps, err := db.GetPipelineStatuses( 636 650 s.db, 637 651 len(shas), 638 - db.FilterEq("repo_owner", f.Did), 639 - db.FilterEq("repo_name", f.Name), 640 - db.FilterEq("knot", f.Knot), 641 - db.FilterIn("sha", 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), 642 656 ) 643 657 if err != nil { 644 658 log.Printf("failed to fetch pipeline statuses: %s", err) ··· 651 665 652 666 labelDefs, err := db.GetLabelDefinitions( 653 667 s.db, 654 - db.FilterIn("at_uri", f.Labels), 655 - db.FilterContains("scope", tangled.RepoPullNSID), 668 + orm.FilterIn("at_uri", f.Labels), 669 + orm.FilterContains("scope", tangled.RepoPullNSID), 656 670 ) 657 671 if err != nil { 658 672 log.Println("failed to fetch labels", err) ··· 678 692 } 679 693 680 694 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 681 - l := s.logger.With("handler", "PullComment") 682 695 user := s.oauth.GetUser(r) 683 696 f, err := s.repoResolver.Resolve(r) 684 697 if err != nil { ··· 716 729 s.pages.Notice(w, "pull", "Comment body is required") 717 730 return 718 731 } 732 + 733 + mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 719 734 720 735 // Start a transaction 721 736 tx, err := s.db.BeginTx(r.Context(), nil) ··· 759 774 Body: body, 760 775 CommentAt: atResp.Uri, 761 776 SubmissionId: pull.Submissions[roundNumber].ID, 777 + Mentions: mentions, 778 + References: references, 762 779 } 763 780 764 781 // Create the pull comment in the database with the commentAt field ··· 776 793 return 777 794 } 778 795 779 - rawMentions := markup.FindUserMentions(comment.Body) 780 - idents := s.idResolver.ResolveIdents(r.Context(), rawMentions) 781 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 782 - var mentions []syntax.DID 783 - for _, ident := range idents { 784 - if ident != nil && !ident.Handle.IsInvalidHandle() { 785 - mentions = append(mentions, ident.DID) 786 - } 787 - } 788 796 s.notifier.NewPullComment(r.Context(), comment, mentions) 789 797 790 798 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 1198 1206 } 1199 1207 } 1200 1208 1209 + mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 1210 + 1201 1211 rkey := tid.TID() 1202 1212 initialSubmission := models.PullSubmission{ 1203 1213 Patch: patch, ··· 1211 1221 OwnerDid: user.Did, 1212 1222 RepoAt: repo.RepoAt(), 1213 1223 Rkey: rkey, 1224 + Mentions: mentions, 1225 + References: references, 1214 1226 Submissions: []*models.PullSubmission{ 1215 1227 &initialSubmission, 1216 1228 }, ··· 1299 1311 1300 1312 // build a stack out of this patch 1301 1313 stackId := uuid.New() 1302 - stack, err := newStack(repo, user, targetBranch, patch, pullSource, stackId.String()) 1314 + stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String()) 1303 1315 if err != nil { 1304 1316 log.Println("failed to create stack", err) 1305 1317 s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err)) ··· 1354 1366 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1355 1367 return 1356 1368 } 1369 + 1357 1370 } 1358 1371 1359 1372 if err = tx.Commit(); err != nil { ··· 1362 1375 return 1363 1376 } 1364 1377 1378 + // notify about each pull 1379 + // 1380 + // this is performed after tx.Commit, because it could result in a locked DB otherwise 1381 + for _, p := range stack { 1382 + s.notifier.NewPull(r.Context(), p) 1383 + } 1384 + 1365 1385 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) 1366 1386 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo)) 1367 1387 } ··· 1487 1507 // fork repo 1488 1508 repo, err := db.GetRepo( 1489 1509 s.db, 1490 - db.FilterEq("did", forkOwnerDid), 1491 - db.FilterEq("name", forkName), 1510 + orm.FilterEq("did", forkOwnerDid), 1511 + orm.FilterEq("name", forkName), 1492 1512 ) 1493 1513 if err != nil { 1494 1514 log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err) ··· 1913 1933 targetBranch := pull.TargetBranch 1914 1934 1915 1935 origStack, _ := r.Context().Value("stack").(models.Stack) 1916 - newStack, err := newStack(repo, user, targetBranch, patch, pull.PullSource, stackId) 1936 + newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId) 1917 1937 if err != nil { 1918 1938 log.Println("failed to create resubmitted stack", err) 1919 1939 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2055 2075 tx, 2056 2076 p.ParentChangeId, 2057 2077 // these should be enough filters to be unique per-stack 2058 - db.FilterEq("repo_at", p.RepoAt.String()), 2059 - db.FilterEq("owner_did", p.OwnerDid), 2060 - db.FilterEq("change_id", p.ChangeId), 2078 + orm.FilterEq("repo_at", p.RepoAt.String()), 2079 + orm.FilterEq("owner_did", p.OwnerDid), 2080 + orm.FilterEq("change_id", p.ChangeId), 2061 2081 ) 2062 2082 2063 2083 if err != nil { ··· 2361 2381 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2362 2382 } 2363 2383 2364 - func newStack(repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2384 + func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2365 2385 formatPatches, err := patchutil.ExtractPatches(patch) 2366 2386 if err != nil { 2367 2387 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2386 2406 body := fp.Body 2387 2407 rkey := tid.TID() 2388 2408 2409 + mentions, references := s.mentionsResolver.Resolve(ctx, body) 2410 + 2389 2411 initialSubmission := models.PullSubmission{ 2390 2412 Patch: fp.Raw, 2391 2413 SourceRev: fp.SHA, ··· 2398 2420 OwnerDid: user.Did, 2399 2421 RepoAt: repo.RepoAt(), 2400 2422 Rkey: rkey, 2423 + Mentions: mentions, 2424 + References: references, 2401 2425 Submissions: []*models.PullSubmission{ 2402 2426 &initialSubmission, 2403 2427 },
+10 -9
appview/repo/artifact.go
··· 15 15 "tangled.org/core/appview/models" 16 16 "tangled.org/core/appview/pages" 17 17 "tangled.org/core/appview/xrpcclient" 18 + "tangled.org/core/orm" 18 19 "tangled.org/core/tid" 19 20 "tangled.org/core/types" 20 21 ··· 155 156 156 157 artifacts, err := db.GetArtifact( 157 158 rp.db, 158 - db.FilterEq("repo_at", f.RepoAt()), 159 - db.FilterEq("tag", tag.Tag.Hash[:]), 160 - db.FilterEq("name", filename), 159 + orm.FilterEq("repo_at", f.RepoAt()), 160 + orm.FilterEq("tag", tag.Tag.Hash[:]), 161 + orm.FilterEq("name", filename), 161 162 ) 162 163 if err != nil { 163 164 log.Println("failed to get artifacts", err) ··· 234 235 235 236 artifacts, err := db.GetArtifact( 236 237 rp.db, 237 - db.FilterEq("repo_at", f.RepoAt()), 238 - db.FilterEq("tag", tag[:]), 239 - db.FilterEq("name", filename), 238 + orm.FilterEq("repo_at", f.RepoAt()), 239 + orm.FilterEq("tag", tag[:]), 240 + orm.FilterEq("name", filename), 240 241 ) 241 242 if err != nil { 242 243 log.Println("failed to get artifacts", err) ··· 276 277 defer tx.Rollback() 277 278 278 279 err = db.DeleteArtifact(tx, 279 - db.FilterEq("repo_at", f.RepoAt()), 280 - db.FilterEq("tag", artifact.Tag[:]), 281 - db.FilterEq("name", filename), 280 + orm.FilterEq("repo_at", f.RepoAt()), 281 + orm.FilterEq("tag", artifact.Tag[:]), 282 + orm.FilterEq("name", filename), 282 283 ) 283 284 if err != nil { 284 285 log.Println("failed to remove artifact record from db", err)
+3 -2
appview/repo/feed.go
··· 11 11 "tangled.org/core/appview/db" 12 12 "tangled.org/core/appview/models" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 15 16 "github.com/bluesky-social/indigo/atproto/identity" 16 17 "github.com/bluesky-social/indigo/atproto/syntax" ··· 20 21 func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) { 21 22 const feedLimitPerType = 100 22 23 23 - pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", repo.RepoAt())) 24 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt())) 24 25 if err != nil { 25 26 return nil, err 26 27 } ··· 28 29 issues, err := db.GetIssuesPaginated( 29 30 rp.db, 30 31 pagination.Page{Limit: feedLimitPerType}, 31 - db.FilterEq("repo_at", repo.RepoAt()), 32 + orm.FilterEq("repo_at", repo.RepoAt()), 32 33 ) 33 34 if err != nil { 34 35 return nil, err
+4 -3
appview/repo/index.go
··· 23 23 "tangled.org/core/appview/models" 24 24 "tangled.org/core/appview/pages" 25 25 "tangled.org/core/appview/xrpcclient" 26 + "tangled.org/core/orm" 26 27 "tangled.org/core/types" 27 28 28 29 "github.com/go-chi/chi/v5" ··· 122 123 l.Error("failed to get email to did map", "err", err) 123 124 } 124 125 125 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 126 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc) 126 127 if err != nil { 127 128 l.Error("failed to GetVerifiedObjectCommits", "err", err) 128 129 } ··· 171 172 // first attempt to fetch from db 172 173 langs, err := db.GetRepoLanguages( 173 174 rp.db, 174 - db.FilterEq("repo_at", repo.RepoAt()), 175 - db.FilterEq("ref", currentRef), 175 + orm.FilterEq("repo_at", repo.RepoAt()), 176 + orm.FilterEq("ref", currentRef), 176 177 ) 177 178 178 179 if err != nil || langs == nil {
+2 -2
appview/repo/log.go
··· 116 116 l.Error("failed to fetch email to did mapping", "err", err) 117 117 } 118 118 119 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 119 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits) 120 120 if err != nil { 121 121 l.Error("failed to GetVerifiedObjectCommits", "err", err) 122 122 } ··· 192 192 l.Error("failed to get email to did mapping", "err", err) 193 193 } 194 194 195 - vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 195 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit}) 196 196 if err != nil { 197 197 l.Error("failed to GetVerifiedCommits", "err", err) 198 198 }
+3 -2
appview/repo/opengraph.go
··· 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/models" 18 18 "tangled.org/core/appview/ogcard" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/types" 20 21 ) 21 22 ··· 338 339 var languageStats []types.RepoLanguageDetails 339 340 langs, err := db.GetRepoLanguages( 340 341 rp.db, 341 - db.FilterEq("repo_at", f.RepoAt()), 342 - db.FilterEq("is_default_ref", 1), 342 + orm.FilterEq("repo_at", f.RepoAt()), 343 + orm.FilterEq("is_default_ref", 1), 343 344 ) 344 345 if err != nil { 345 346 log.Printf("failed to get language stats from db: %v", err)
+17 -16
appview/repo/repo.go
··· 24 24 xrpcclient "tangled.org/core/appview/xrpcclient" 25 25 "tangled.org/core/eventconsumer" 26 26 "tangled.org/core/idresolver" 27 + "tangled.org/core/orm" 27 28 "tangled.org/core/rbac" 28 29 "tangled.org/core/tid" 29 30 "tangled.org/core/xrpc/serviceauth" ··· 345 346 // get form values 346 347 labelId := r.FormValue("label-id") 347 348 348 - label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId)) 349 + label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId)) 349 350 if err != nil { 350 351 fail("Failed to find label definition.", err) 351 352 return ··· 409 410 410 411 err = db.UnsubscribeLabel( 411 412 tx, 412 - db.FilterEq("repo_at", f.RepoAt()), 413 - db.FilterEq("label_at", removedAt), 413 + orm.FilterEq("repo_at", f.RepoAt()), 414 + orm.FilterEq("label_at", removedAt), 414 415 ) 415 416 if err != nil { 416 417 fail("Failed to unsubscribe label.", err) 417 418 return 418 419 } 419 420 420 - err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id)) 421 + err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id)) 421 422 if err != nil { 422 423 fail("Failed to delete label definition.", err) 423 424 return ··· 456 457 } 457 458 458 459 labelAts := r.Form["label"] 459 - _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 460 + _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 460 461 if err != nil { 461 462 fail("Failed to subscribe to label.", err) 462 463 return ··· 542 543 } 543 544 544 545 labelAts := r.Form["label"] 545 - _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 546 + _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 546 547 if err != nil { 547 548 fail("Failed to unsubscribe to label.", err) 548 549 return ··· 582 583 583 584 err = db.UnsubscribeLabel( 584 585 rp.db, 585 - db.FilterEq("repo_at", f.RepoAt()), 586 - db.FilterIn("label_at", labelAts), 586 + orm.FilterEq("repo_at", f.RepoAt()), 587 + orm.FilterIn("label_at", labelAts), 587 588 ) 588 589 if err != nil { 589 590 fail("Failed to unsubscribe label.", err) ··· 612 613 613 614 labelDefs, err := db.GetLabelDefinitions( 614 615 rp.db, 615 - db.FilterIn("at_uri", f.Labels), 616 - db.FilterContains("scope", subject.Collection().String()), 616 + orm.FilterIn("at_uri", f.Labels), 617 + orm.FilterContains("scope", subject.Collection().String()), 617 618 ) 618 619 if err != nil { 619 620 l.Error("failed to fetch label defs", "err", err) ··· 625 626 defs[l.AtUri().String()] = &l 626 627 } 627 628 628 - states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 629 + states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 629 630 if err != nil { 630 631 l.Error("failed to build label state", "err", err) 631 632 return ··· 660 661 661 662 labelDefs, err := db.GetLabelDefinitions( 662 663 rp.db, 663 - db.FilterIn("at_uri", f.Labels), 664 - db.FilterContains("scope", subject.Collection().String()), 664 + orm.FilterIn("at_uri", f.Labels), 665 + orm.FilterContains("scope", subject.Collection().String()), 665 666 ) 666 667 if err != nil { 667 668 l.Error("failed to fetch labels", "err", err) ··· 673 674 defs[l.AtUri().String()] = &l 674 675 } 675 676 676 - states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 677 + states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 677 678 if err != nil { 678 679 l.Error("failed to build label state", "err", err) 679 680 return ··· 1036 1037 // in the user's account. 1037 1038 existingRepo, err := db.GetRepo( 1038 1039 rp.db, 1039 - db.FilterEq("did", user.Did), 1040 - db.FilterEq("name", forkName), 1040 + orm.FilterEq("did", user.Did), 1041 + orm.FilterEq("name", forkName), 1041 1042 ) 1042 1043 if err != nil { 1043 1044 if !errors.Is(err, sql.ErrNoRows) {
+16 -17
appview/repo/repo_util.go
··· 1 1 package repo 2 2 3 3 import ( 4 + "maps" 4 5 "slices" 5 6 "sort" 6 7 "strings" 7 8 8 9 "tangled.org/core/appview/db" 9 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 10 12 "tangled.org/core/types" 11 - 12 - "github.com/go-git/go-git/v5/plumbing/object" 13 13 ) 14 14 15 15 func sortFiles(files []types.NiceTree) { ··· 42 42 }) 43 43 } 44 44 45 - func uniqueEmails(commits []*object.Commit) []string { 45 + func uniqueEmails(commits []types.Commit) []string { 46 46 emails := make(map[string]struct{}) 47 47 for _, commit := range commits { 48 - if commit.Author.Email != "" { 49 - emails[commit.Author.Email] = struct{}{} 50 - } 51 - if commit.Committer.Email != "" { 52 - emails[commit.Committer.Email] = struct{}{} 48 + emails[commit.Author.Email] = struct{}{} 49 + emails[commit.Committer.Email] = struct{}{} 50 + for _, c := range commit.CoAuthors() { 51 + emails[c.Email] = struct{}{} 53 52 } 54 53 } 55 - var uniqueEmails []string 56 - for email := range emails { 57 - uniqueEmails = append(uniqueEmails, email) 58 - } 59 - return uniqueEmails 54 + 55 + // delete empty emails if any, from the set 56 + delete(emails, "") 57 + 58 + return slices.Collect(maps.Keys(emails)) 60 59 } 61 60 62 61 func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) { ··· 104 103 ps, err := db.GetPipelineStatuses( 105 104 d, 106 105 len(shas), 107 - db.FilterEq("repo_owner", repo.Did), 108 - db.FilterEq("repo_name", repo.Name), 109 - db.FilterEq("knot", repo.Knot), 110 - db.FilterIn("sha", 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), 111 110 ) 112 111 if err != nil { 113 112 return nil, err
+3 -2
appview/repo/settings.go
··· 14 14 "tangled.org/core/appview/oauth" 15 15 "tangled.org/core/appview/pages" 16 16 xrpcclient "tangled.org/core/appview/xrpcclient" 17 + "tangled.org/core/orm" 17 18 "tangled.org/core/types" 18 19 19 20 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 210 211 return 211 212 } 212 213 213 - defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 214 + defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 214 215 if err != nil { 215 216 l.Error("failed to fetch labels", "err", err) 216 217 rp.pages.Error503(w) 217 218 return 218 219 } 219 220 220 - labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Labels)) 221 + labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels)) 221 222 if err != nil { 222 223 l.Error("failed to fetch labels", "err", err) 223 224 rp.pages.Error503(w)
+2 -1
appview/repo/tags.go
··· 10 10 "tangled.org/core/appview/models" 11 11 "tangled.org/core/appview/pages" 12 12 xrpcclient "tangled.org/core/appview/xrpcclient" 13 + "tangled.org/core/orm" 13 14 "tangled.org/core/types" 14 15 15 16 indigoxrpc "github.com/bluesky-social/indigo/xrpc" ··· 44 45 rp.pages.Error503(w) 45 46 return 46 47 } 47 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 48 + artifacts, err := db.GetArtifact(rp.db, orm.FilterEq("repo_at", f.RepoAt())) 48 49 if err != nil { 49 50 l.Error("failed grab artifacts", "err", err) 50 51 return
+5 -4
appview/serververify/verify.go
··· 9 9 "tangled.org/core/api/tangled" 10 10 "tangled.org/core/appview/db" 11 11 "tangled.org/core/appview/xrpcclient" 12 + "tangled.org/core/orm" 12 13 "tangled.org/core/rbac" 13 14 ) 14 15 ··· 76 77 // mark this spindle as verified in the db 77 78 rowId, err := db.VerifySpindle( 78 79 tx, 79 - db.FilterEq("owner", owner), 80 - db.FilterEq("instance", instance), 80 + orm.FilterEq("owner", owner), 81 + orm.FilterEq("instance", instance), 81 82 ) 82 83 if err != nil { 83 84 return 0, fmt.Errorf("failed to write to DB: %w", err) ··· 115 116 // mark as registered 116 117 err = db.MarkRegistered( 117 118 tx, 118 - db.FilterEq("did", owner), 119 - db.FilterEq("domain", domain), 119 + orm.FilterEq("did", owner), 120 + orm.FilterEq("domain", domain), 120 121 ) 121 122 if err != nil { 122 123 return fmt.Errorf("failed to register domain: %w", err)
+25 -24
appview/spindles/spindles.go
··· 20 20 "tangled.org/core/appview/serververify" 21 21 "tangled.org/core/appview/xrpcclient" 22 22 "tangled.org/core/idresolver" 23 + "tangled.org/core/orm" 23 24 "tangled.org/core/rbac" 24 25 "tangled.org/core/tid" 25 26 ··· 71 72 user := s.OAuth.GetUser(r) 72 73 all, err := db.GetSpindles( 73 74 s.Db, 74 - db.FilterEq("owner", user.Did), 75 + orm.FilterEq("owner", user.Did), 75 76 ) 76 77 if err != nil { 77 78 s.Logger.Error("failed to fetch spindles", "err", err) ··· 101 102 102 103 spindles, err := db.GetSpindles( 103 104 s.Db, 104 - db.FilterEq("instance", instance), 105 - db.FilterEq("owner", user.Did), 106 - db.FilterIsNot("verified", "null"), 105 + orm.FilterEq("instance", instance), 106 + orm.FilterEq("owner", user.Did), 107 + orm.FilterIsNot("verified", "null"), 107 108 ) 108 109 if err != nil || len(spindles) != 1 { 109 110 l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) ··· 123 124 repos, err := db.GetRepos( 124 125 s.Db, 125 126 0, 126 - db.FilterEq("spindle", instance), 127 + orm.FilterEq("spindle", instance), 127 128 ) 128 129 if err != nil { 129 130 l.Error("failed to get spindle repos", "err", err) ··· 290 291 291 292 spindles, err := db.GetSpindles( 292 293 s.Db, 293 - db.FilterEq("owner", user.Did), 294 - db.FilterEq("instance", instance), 294 + orm.FilterEq("owner", user.Did), 295 + orm.FilterEq("instance", instance), 295 296 ) 296 297 if err != nil || len(spindles) != 1 { 297 298 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 319 320 // remove spindle members first 320 321 err = db.RemoveSpindleMember( 321 322 tx, 322 - db.FilterEq("did", user.Did), 323 - db.FilterEq("instance", instance), 323 + orm.FilterEq("did", user.Did), 324 + orm.FilterEq("instance", instance), 324 325 ) 325 326 if err != nil { 326 327 l.Error("failed to remove spindle members", "err", err) ··· 330 331 331 332 err = db.DeleteSpindle( 332 333 tx, 333 - db.FilterEq("owner", user.Did), 334 - db.FilterEq("instance", instance), 334 + orm.FilterEq("owner", user.Did), 335 + orm.FilterEq("instance", instance), 335 336 ) 336 337 if err != nil { 337 338 l.Error("failed to delete spindle", "err", err) ··· 410 411 411 412 spindles, err := db.GetSpindles( 412 413 s.Db, 413 - db.FilterEq("owner", user.Did), 414 - db.FilterEq("instance", instance), 414 + orm.FilterEq("owner", user.Did), 415 + orm.FilterEq("instance", instance), 415 416 ) 416 417 if err != nil || len(spindles) != 1 { 417 418 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 453 454 454 455 verifiedSpindle, err := db.GetSpindles( 455 456 s.Db, 456 - db.FilterEq("id", rowId), 457 + orm.FilterEq("id", rowId), 457 458 ) 458 459 if err != nil || len(verifiedSpindle) != 1 { 459 460 l.Error("failed get new spindle", "err", err) ··· 486 487 487 488 spindles, err := db.GetSpindles( 488 489 s.Db, 489 - db.FilterEq("owner", user.Did), 490 - db.FilterEq("instance", instance), 490 + orm.FilterEq("owner", user.Did), 491 + orm.FilterEq("instance", instance), 491 492 ) 492 493 if err != nil || len(spindles) != 1 { 493 494 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 622 623 623 624 spindles, err := db.GetSpindles( 624 625 s.Db, 625 - db.FilterEq("owner", user.Did), 626 - db.FilterEq("instance", instance), 626 + orm.FilterEq("owner", user.Did), 627 + orm.FilterEq("instance", instance), 627 628 ) 628 629 if err != nil || len(spindles) != 1 { 629 630 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 672 673 // get the record from the DB first: 673 674 members, err := db.GetSpindleMembers( 674 675 s.Db, 675 - db.FilterEq("did", user.Did), 676 - db.FilterEq("instance", instance), 677 - db.FilterEq("subject", memberId.DID), 676 + orm.FilterEq("did", user.Did), 677 + orm.FilterEq("instance", instance), 678 + orm.FilterEq("subject", memberId.DID), 678 679 ) 679 680 if err != nil || len(members) != 1 { 680 681 l.Error("failed to get member", "err", err) ··· 685 686 // remove from db 686 687 if err = db.RemoveSpindleMember( 687 688 tx, 688 - db.FilterEq("did", user.Did), 689 - db.FilterEq("instance", instance), 690 - db.FilterEq("subject", memberId.DID), 689 + orm.FilterEq("did", user.Did), 690 + orm.FilterEq("instance", instance), 691 + orm.FilterEq("subject", memberId.DID), 691 692 ); err != nil { 692 693 l.Error("failed to remove spindle member", "err", err) 693 694 fail()
+6 -5
appview/state/gfi.go
··· 11 11 "tangled.org/core/appview/pages" 12 12 "tangled.org/core/appview/pagination" 13 13 "tangled.org/core/consts" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { ··· 20 21 21 22 goodFirstIssueLabel := s.config.Label.GoodFirstIssue 22 23 23 - gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel)) 24 + gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel)) 24 25 if err != nil { 25 26 log.Println("failed to get gfi label def", err) 26 27 s.pages.Error500(w) 27 28 return 28 29 } 29 30 30 - repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel)) 31 + repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel)) 31 32 if err != nil { 32 33 log.Println("failed to get repo labels", err) 33 34 s.pages.Error503(w) ··· 55 56 pagination.Page{ 56 57 Limit: 500, 57 58 }, 58 - db.FilterIn("repo_at", repoUris), 59 - db.FilterEq("open", 1), 59 + orm.FilterIn("repo_at", repoUris), 60 + orm.FilterEq("open", 1), 60 61 ) 61 62 if err != nil { 62 63 log.Println("failed to get issues", err) ··· 132 133 } 133 134 134 135 if len(uriList) > 0 { 135 - allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList)) 136 + allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList)) 136 137 if err != nil { 137 138 log.Println("failed to fetch labels", err) 138 139 }
+17
appview/state/git_http.go
··· 25 25 26 26 } 27 27 28 + func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 + if !ok { 31 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 + return 33 + } 34 + repo := r.Context().Value("repo").(*models.Repo) 35 + 36 + scheme := "https" 37 + if s.config.Core.Dev { 38 + scheme = "http" 39 + } 40 + 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 + s.proxyRequest(w, r, targetURL) 43 + } 44 + 28 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 46 user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 47 if !ok {
+6 -5
appview/state/knotstream.go
··· 16 16 ec "tangled.org/core/eventconsumer" 17 17 "tangled.org/core/eventconsumer/cursor" 18 18 "tangled.org/core/log" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 "tangled.org/core/workflow" 21 22 ··· 30 31 31 32 knots, err := db.GetRegistrations( 32 33 d, 33 - db.FilterIsNot("registered", "null"), 34 + orm.FilterIsNot("registered", "null"), 34 35 ) 35 36 if err != nil { 36 37 return nil, err ··· 143 144 repos, err := db.GetRepos( 144 145 d, 145 146 0, 146 - db.FilterEq("did", record.RepoDid), 147 - db.FilterEq("name", record.RepoName), 147 + orm.FilterEq("did", record.RepoDid), 148 + orm.FilterEq("name", record.RepoName), 148 149 ) 149 150 if err != nil { 150 151 return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err) ··· 209 210 repos, err := db.GetRepos( 210 211 d, 211 212 0, 212 - db.FilterEq("did", record.TriggerMetadata.Repo.Did), 213 - db.FilterEq("name", record.TriggerMetadata.Repo.Repo), 213 + orm.FilterEq("did", record.TriggerMetadata.Repo.Did), 214 + orm.FilterEq("name", record.TriggerMetadata.Repo.Repo), 214 215 ) 215 216 if err != nil { 216 217 return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+22 -12
appview/state/profile.go
··· 19 19 "tangled.org/core/appview/db" 20 20 "tangled.org/core/appview/models" 21 21 "tangled.org/core/appview/pages" 22 + "tangled.org/core/orm" 22 23 ) 23 24 24 25 func (s *State) Profile(w http.ResponseWriter, r *http.Request) { ··· 56 57 return nil, fmt.Errorf("failed to get profile: %w", err) 57 58 } 58 59 59 - repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did)) 60 + repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did)) 60 61 if err != nil { 61 62 return nil, fmt.Errorf("failed to get repo count: %w", err) 62 63 } 63 64 64 - stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did)) 65 + stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did)) 65 66 if err != nil { 66 67 return nil, fmt.Errorf("failed to get string count: %w", err) 67 68 } 68 69 69 - starredCount, err := db.CountStars(s.db, db.FilterEq("did", did)) 70 + starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did)) 70 71 if err != nil { 71 72 return nil, fmt.Errorf("failed to get starred repo count: %w", err) 72 73 } ··· 86 87 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 87 88 punchcard, err := db.MakePunchcard( 88 89 s.db, 89 - db.FilterEq("did", did), 90 - db.FilterGte("date", startOfYear.Format(time.DateOnly)), 91 - db.FilterLte("date", now.Format(time.DateOnly)), 90 + orm.FilterEq("did", did), 91 + orm.FilterGte("date", startOfYear.Format(time.DateOnly)), 92 + orm.FilterLte("date", now.Format(time.DateOnly)), 92 93 ) 93 94 if err != nil { 94 95 return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) ··· 123 124 repos, err := db.GetRepos( 124 125 s.db, 125 126 0, 126 - db.FilterEq("did", profile.UserDid), 127 + orm.FilterEq("did", profile.UserDid), 127 128 ) 128 129 if err != nil { 129 130 l.Error("failed to fetch repos", "err", err) ··· 161 162 l.Error("failed to create timeline", "err", err) 162 163 } 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 + 164 174 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 165 175 LoggedInUser: s.oauth.GetUser(r), 166 176 Card: profile, ··· 184 194 repos, err := db.GetRepos( 185 195 s.db, 186 196 0, 187 - db.FilterEq("did", profile.UserDid), 197 + orm.FilterEq("did", profile.UserDid), 188 198 ) 189 199 if err != nil { 190 200 l.Error("failed to get repos", "err", err) ··· 210 220 } 211 221 l = l.With("profileDid", profile.UserDid) 212 222 213 - stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid)) 223 + stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid)) 214 224 if err != nil { 215 225 l.Error("failed to get stars", "err", err) 216 226 s.pages.Error500(w) ··· 239 249 } 240 250 l = l.With("profileDid", profile.UserDid) 241 251 242 - strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid)) 252 + strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid)) 243 253 if err != nil { 244 254 l.Error("failed to get strings", "err", err) 245 255 s.pages.Error500(w) ··· 291 301 followDids = append(followDids, extractDid(follow)) 292 302 } 293 303 294 - profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 304 + profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids)) 295 305 if err != nil { 296 306 l.Error("failed to get profiles", "followDids", followDids, "err", err) 297 307 return &params, err ··· 694 704 log.Printf("getting profile data for %s: %s", user.Did, err) 695 705 } 696 706 697 - repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did)) 707 + repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 698 708 if err != nil { 699 709 log.Printf("getting repos for %s: %s", user.Did, err) 700 710 }
+3
appview/state/router.go
··· 101 101 102 102 // These routes get proxied to the knot 103 103 r.Get("/info/refs", s.InfoRefs) 104 + r.Post("/git-upload-archive", s.UploadArchive) 104 105 r.Post("/git-upload-pack", s.UploadPack) 105 106 r.Post("/git-receive-pack", s.ReceivePack) 106 107 ··· 266 267 s.enforcer, 267 268 s.pages, 268 269 s.idResolver, 270 + s.mentionsResolver, 269 271 s.db, 270 272 s.config, 271 273 s.notifier, ··· 282 284 s.repoResolver, 283 285 s.pages, 284 286 s.idResolver, 287 + s.mentionsResolver, 285 288 s.db, 286 289 s.config, 287 290 s.notifier,
+2 -1
appview/state/spindlestream.go
··· 17 17 ec "tangled.org/core/eventconsumer" 18 18 "tangled.org/core/eventconsumer/cursor" 19 19 "tangled.org/core/log" 20 + "tangled.org/core/orm" 20 21 "tangled.org/core/rbac" 21 22 spindle "tangled.org/core/spindle/models" 22 23 ) ··· 27 28 28 29 spindles, err := db.GetSpindles( 29 30 d, 30 - db.FilterIsNot("verified", "null"), 31 + orm.FilterIsNot("verified", "null"), 31 32 ) 32 33 if err != nil { 33 34 return nil, err
+29 -23
appview/state/state.go
··· 15 15 "tangled.org/core/appview/config" 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/indexer" 18 + "tangled.org/core/appview/mentions" 18 19 "tangled.org/core/appview/models" 19 20 "tangled.org/core/appview/notify" 20 21 dbnotify "tangled.org/core/appview/notify/db" ··· 29 30 "tangled.org/core/jetstream" 30 31 "tangled.org/core/log" 31 32 tlog "tangled.org/core/log" 33 + "tangled.org/core/orm" 32 34 "tangled.org/core/rbac" 33 35 "tangled.org/core/tid" 34 36 ··· 42 44 ) 43 45 44 46 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 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 60 63 } 61 64 62 65 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 98 101 99 102 repoResolver := reporesolver.New(config, enforcer, d) 100 103 104 + mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver")) 105 + 101 106 wrapper := db.DbWrapper{Execer: d} 102 107 jc, err := jetstream.NewJetstreamClient( 103 108 config.Jetstream.Endpoint, ··· 178 183 enforcer, 179 184 pages, 180 185 res, 186 + mentionsResolver, 181 187 posthog, 182 188 jc, 183 189 config, ··· 294 300 return 295 301 } 296 302 297 - gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue)) 303 + gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue)) 298 304 if err != nil { 299 305 // non-fatal 300 306 } ··· 318 324 319 325 regs, err := db.GetRegistrations( 320 326 s.db, 321 - db.FilterEq("did", user.Did), 322 - db.FilterEq("needs_upgrade", 1), 327 + orm.FilterEq("did", user.Did), 328 + orm.FilterEq("needs_upgrade", 1), 323 329 ) 324 330 if err != nil { 325 331 l.Error("non-fatal: failed to get registrations", "err", err) ··· 327 333 328 334 spindles, err := db.GetSpindles( 329 335 s.db, 330 - db.FilterEq("owner", user.Did), 331 - db.FilterEq("needs_upgrade", 1), 336 + orm.FilterEq("owner", user.Did), 337 + orm.FilterEq("needs_upgrade", 1), 332 338 ) 333 339 if err != nil { 334 340 l.Error("non-fatal: failed to get spindles", "err", err) ··· 499 505 // Check for existing repos 500 506 existingRepo, err := db.GetRepo( 501 507 s.db, 502 - db.FilterEq("did", user.Did), 503 - db.FilterEq("name", repoName), 508 + orm.FilterEq("did", user.Did), 509 + orm.FilterEq("name", repoName), 504 510 ) 505 511 if err == nil && existingRepo != nil { 506 512 l.Info("repo exists") ··· 660 666 } 661 667 662 668 func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error { 663 - defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults)) 669 + defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults)) 664 670 if err != nil { 665 671 return err 666 672 }
+7 -6
appview/strings/strings.go
··· 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/pages/markup" 19 19 "tangled.org/core/idresolver" 20 + "tangled.org/core/orm" 20 21 "tangled.org/core/tid" 21 22 22 23 "github.com/bluesky-social/indigo/api/atproto" ··· 108 109 strings, err := db.GetStrings( 109 110 s.Db, 110 111 0, 111 - db.FilterEq("did", id.DID), 112 - db.FilterEq("rkey", rkey), 112 + orm.FilterEq("did", id.DID), 113 + orm.FilterEq("rkey", rkey), 113 114 ) 114 115 if err != nil { 115 116 l.Error("failed to fetch string", "err", err) ··· 199 200 all, err := db.GetStrings( 200 201 s.Db, 201 202 0, 202 - db.FilterEq("did", id.DID), 203 - db.FilterEq("rkey", rkey), 203 + orm.FilterEq("did", id.DID), 204 + orm.FilterEq("rkey", rkey), 204 205 ) 205 206 if err != nil { 206 207 l.Error("failed to fetch string", "err", err) ··· 408 409 409 410 if err := db.DeleteString( 410 411 s.Db, 411 - db.FilterEq("did", user.Did), 412 - db.FilterEq("rkey", rkey), 412 + orm.FilterEq("did", user.Did), 413 + orm.FilterEq("rkey", rkey), 413 414 ); err != nil { 414 415 fail("Failed to delete string.", err) 415 416 return
+2 -1
appview/validator/issue.go
··· 6 6 7 7 "tangled.org/core/appview/db" 8 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 9 10 ) 10 11 11 12 func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 12 13 // if comments have parents, only ingest ones that are 1 level deep 13 14 if comment.ReplyTo != nil { 14 - parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) 15 + parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 15 16 if err != nil { 16 17 return fmt.Errorf("failed to fetch parent comment: %w", err) 17 18 }
+1 -34
crypto/verify.go
··· 5 5 "crypto/sha256" 6 6 "encoding/base64" 7 7 "fmt" 8 - "strings" 9 8 10 9 "github.com/hiddeco/sshsig" 11 10 "golang.org/x/crypto/ssh" 12 - "tangled.org/core/types" 13 11 ) 14 12 15 13 func VerifySignature(pubKey, signature, payload []byte) (error, bool) { ··· 28 26 // multiple algorithms but sha-512 is most secure, and git's ssh signing defaults 29 27 // to sha-512 for all key types anyway. 30 28 err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git") 31 - return err, err == nil 32 - } 33 29 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())) 30 + return err, err == nil 64 31 } 65 32 66 33 // SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+9 -9
flake.lock
··· 35 35 "systems": "systems" 36 36 }, 37 37 "locked": { 38 - "lastModified": 1694529238, 39 - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 38 + "lastModified": 1731533236, 39 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 40 40 "owner": "numtide", 41 41 "repo": "flake-utils", 42 - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 42 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 43 43 "type": "github" 44 44 }, 45 45 "original": { ··· 56 56 ] 57 57 }, 58 58 "locked": { 59 - "lastModified": 1754078208, 60 - "narHash": "sha256-YVoIFDCDpYuU3riaDEJ3xiGdPOtsx4sR5eTzHTytPV8=", 59 + "lastModified": 1763982521, 60 + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", 61 61 "owner": "nix-community", 62 62 "repo": "gomod2nix", 63 - "rev": "7f963246a71626c7fc70b431a315c4388a0c95cf", 63 + "rev": "02e63a239d6eabd595db56852535992c898eba72", 64 64 "type": "github" 65 65 }, 66 66 "original": { ··· 150 150 }, 151 151 "nixpkgs": { 152 152 "locked": { 153 - "lastModified": 1751984180, 154 - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", 153 + "lastModified": 1766070988, 154 + "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", 155 155 "owner": "nixos", 156 156 "repo": "nixpkgs", 157 - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", 157 + "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", 158 158 "type": "github" 159 159 }, 160 160 "original": {
-2
flake.nix
··· 80 80 }).buildGoApplication; 81 81 modules = ./nix/gomod2nix.toml; 82 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 - inherit (pkgs) gcc; 84 83 inherit sqlite-lib-src; 85 84 }; 86 85 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 156 155 nativeBuildInputs = [ 157 156 pkgs.go 158 157 pkgs.air 159 - pkgs.tilt 160 158 pkgs.gopls 161 159 pkgs.httpie 162 160 pkgs.litecli
+3 -4
go.mod
··· 1 1 module tangled.org/core 2 2 3 - go 1.24.4 3 + go 1.25.0 4 4 5 5 require ( 6 6 github.com/Blank-Xu/sql-adapter v1.1.1 ··· 44 44 github.com/stretchr/testify v1.10.0 45 45 github.com/urfave/cli/v3 v3.3.3 46 46 github.com/whyrusleeping/cbor-gen v0.3.1 47 - github.com/wyatt915/goldmark-treeblood v0.0.1 48 47 github.com/yuin/goldmark v1.7.13 48 + github.com/yuin/goldmark-emoji v1.0.6 49 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 50 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 51 51 golang.org/x/crypto v0.40.0 52 52 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b 53 53 golang.org/x/image v0.31.0 54 54 golang.org/x/net v0.42.0 55 - golang.org/x/sync v0.17.0 56 55 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 57 56 gopkg.in/yaml.v3 v3.0.1 58 57 ) ··· 190 189 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 191 190 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 192 191 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 193 - github.com/wyatt915/treeblood v0.1.16 // indirect 194 192 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 195 193 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 196 194 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect ··· 205 203 go.uber.org/atomic v1.11.0 // indirect 206 204 go.uber.org/multierr v1.11.0 // indirect 207 205 go.uber.org/zap v1.27.0 // indirect 206 + golang.org/x/sync v0.17.0 // indirect 208 207 golang.org/x/sys v0.34.0 // indirect 209 208 golang.org/x/text v0.29.0 // indirect 210 209 golang.org/x/time v0.12.0 // indirect
+2 -4
go.sum
··· 495 495 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 496 496 github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 497 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 498 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 503 499 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 504 500 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= ··· 509 505 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 510 506 github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 511 507 github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 508 + github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 509 + github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 512 510 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 513 511 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 514 512 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
+4 -4
hook/hook.go
··· 48 48 }, 49 49 Commands: []*cli.Command{ 50 50 { 51 - Name: "post-recieve", 52 - Usage: "sends a post-recieve hook to the knot (waits for stdin)", 53 - Action: postRecieve, 51 + Name: "post-receive", 52 + Usage: "sends a post-receive hook to the knot (waits for stdin)", 53 + Action: postReceive, 54 54 }, 55 55 }, 56 56 } 57 57 } 58 58 59 - func postRecieve(ctx context.Context, cmd *cli.Command) error { 59 + func postReceive(ctx context.Context, cmd *cli.Command) error { 60 60 gitDir := cmd.String("git-dir") 61 61 userDid := cmd.String("user-did") 62 62 userHandle := cmd.String("user-handle")
+1 -1
hook/setup.go
··· 138 138 option_var="GIT_PUSH_OPTION_$i" 139 139 push_options+=(-push-option "${!option_var}") 140 140 done 141 - %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-recieve 141 + %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive 142 142 `, executablePath, config.internalApi) 143 143 144 144 return os.WriteFile(hookPath, []byte(hookContent), 0755)
+15 -4
jetstream/jetstream.go
··· 72 72 // existing instances of the closure when j.WantedDids is mutated 73 73 return func(ctx context.Context, evt *models.Event) error { 74 74 75 + j.mu.RLock() 75 76 // empty filter => all dids allowed 76 - if len(j.wantedDids) == 0 { 77 - return processFunc(ctx, evt) 77 + matches := len(j.wantedDids) == 0 78 + if !matches { 79 + if _, ok := j.wantedDids[evt.Did]; ok { 80 + matches = true 81 + } 78 82 } 83 + j.mu.RUnlock() 79 84 80 - if _, ok := j.wantedDids[evt.Did]; ok { 85 + if matches { 81 86 return processFunc(ctx, evt) 82 87 } else { 83 88 return nil ··· 122 127 123 128 go func() { 124 129 if j.waitForDid { 125 - for len(j.wantedDids) == 0 { 130 + for { 131 + j.mu.RLock() 132 + hasDid := len(j.wantedDids) != 0 133 + j.mu.RUnlock() 134 + if hasDid { 135 + break 136 + } 126 137 time.Sleep(time.Second) 127 138 } 128 139 }
+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
··· 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
··· 77 77 nd.Diff = append(nd.Diff, ndiff) 78 78 } 79 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 - } 80 + nd.Commit.FromGoGitCommit(c) 97 81 98 82 return &nd, nil 99 83 }
+13 -1
knotserver/git/service/service.go
··· 95 95 return c.RunService(cmd) 96 96 } 97 97 98 + func (c *ServiceCommand) UploadArchive() error { 99 + cmd := exec.Command("git", []string{ 100 + "upload-archive", 101 + ".", 102 + }...) 103 + 104 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 105 + cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol)) 106 + cmd.Dir = c.Dir 107 + 108 + return c.RunService(cmd) 109 + } 110 + 98 111 func (c *ServiceCommand) UploadPack() error { 99 112 cmd := exec.Command("git", []string{ 100 - "-c", "uploadpack.allowFilter=true", 101 113 "upload-pack", 102 114 "--stateless-rpc", 103 115 ".",
+47
knotserver/git.go
··· 56 56 } 57 57 } 58 58 59 + func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 60 + did := chi.URLParam(r, "did") 61 + name := chi.URLParam(r, "name") 62 + repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 63 + if err != nil { 64 + gitError(w, err.Error(), http.StatusInternalServerError) 65 + h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 66 + return 67 + } 68 + 69 + const expectedContentType = "application/x-git-upload-archive-request" 70 + contentType := r.Header.Get("Content-Type") 71 + if contentType != expectedContentType { 72 + gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) 73 + } 74 + 75 + var bodyReader io.ReadCloser = r.Body 76 + if r.Header.Get("Content-Encoding") == "gzip" { 77 + gzipReader, err := gzip.NewReader(r.Body) 78 + if err != nil { 79 + gitError(w, err.Error(), http.StatusInternalServerError) 80 + h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err) 81 + return 82 + } 83 + defer gzipReader.Close() 84 + bodyReader = gzipReader 85 + } 86 + 87 + w.Header().Set("Content-Type", "application/x-git-upload-archive-result") 88 + 89 + h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo) 90 + 91 + cmd := service.ServiceCommand{ 92 + GitProtocol: r.Header.Get("Git-Protocol"), 93 + Dir: repo, 94 + Stdout: w, 95 + Stdin: bodyReader, 96 + } 97 + 98 + w.WriteHeader(http.StatusOK) 99 + 100 + if err := cmd.UploadArchive(); err != nil { 101 + h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 102 + return 103 + } 104 + } 105 + 59 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 107 did := chi.URLParam(r, "did") 61 108 name := chi.URLParam(r, "name")
+1
knotserver/router.go
··· 82 82 r.Route("/{name}", func(r chi.Router) { 83 83 // routes for git operations 84 84 r.Get("/info/refs", h.InfoRefs) 85 + r.Post("/git-upload-archive", h.UploadArchive) 85 86 r.Post("/git-upload-pack", h.UploadPack) 86 87 r.Post("/git-receive-pack", h.ReceivePack) 87 88 })
+1 -1
knotserver/server.go
··· 64 64 logger.Info("running in dev mode, signature verification is disabled") 65 65 } 66 66 67 - db, err := db.Setup(c.Server.DBPath) 67 + db, err := db.Setup(ctx, c.Server.DBPath) 68 68 if err != nil { 69 69 return fmt.Errorf("failed to load db: %w", err) 70 70 }
+6 -1
knotserver/xrpc/repo_log.go
··· 62 62 return 63 63 } 64 64 65 + tcommits := make([]types.Commit, len(commits)) 66 + for i, c := range commits { 67 + tcommits[i].FromGoGitCommit(c) 68 + } 69 + 65 70 // Create response using existing types.RepoLogResponse 66 71 response := types.RepoLogResponse{ 67 - Commits: commits, 72 + Commits: tcommits, 68 73 Ref: ref, 69 74 Page: (offset / limit) + 1, 70 75 PerPage: limit,
+14
lexicons/issue/comment.json
··· 29 29 "replyTo": { 30 30 "type": "string", 31 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 + } 32 46 } 33 47 } 34 48 }
+14
lexicons/issue/issue.json
··· 24 24 "createdAt": { 25 25 "type": "string", 26 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 + } 27 41 } 28 42 } 29 43 }
+14
lexicons/pulls/comment.json
··· 25 25 "createdAt": { 26 26 "type": "string", 27 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 + } 28 42 } 29 43 } 30 44 }
+14
lexicons/pulls/pull.json
··· 36 36 "createdAt": { 37 37 "type": "string", 38 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 + } 39 53 } 40 54 } 41 55 }
+3 -30
nix/gomod2nix.toml
··· 165 165 [mod."github.com/davecgh/go-spew"] 166 166 version = "v1.1.2-0.20180830191138-d8f796af33cc" 167 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 168 [mod."github.com/dgraph-io/ristretto"] 172 169 version = "v0.2.0" 173 170 hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw=" ··· 373 370 [mod."github.com/klauspost/cpuid/v2"] 374 371 version = "v2.3.0" 375 372 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 373 [mod."github.com/lucasb-eyer/go-colorful"] 395 374 version = "v1.2.0" 396 375 hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" ··· 511 490 [mod."github.com/ryanuber/go-glob"] 512 491 version = "v1.0.0" 513 492 hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY=" 514 - [mod."github.com/segmentio/asm"] 515 - version = "v1.2.0" 516 - hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs=" 517 493 [mod."github.com/sergi/go-diff"] 518 494 version = "v1.1.0" 519 495 hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY=" ··· 548 524 [mod."github.com/whyrusleeping/cbor-gen"] 549 525 version = "v0.3.1" 550 526 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 527 [mod."github.com/xo/terminfo"] 558 528 version = "v0.0.0-20220910002029-abceb7e1c41e" 559 529 hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=" 560 530 [mod."github.com/yuin/goldmark"] 561 531 version = "v1.7.13" 562 532 hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE=" 533 + [mod."github.com/yuin/goldmark-emoji"] 534 + version = "v1.0.6" 535 + hash = "sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY=" 563 536 [mod."github.com/yuin/goldmark-highlighting/v2"] 564 537 version = "v2.0.0-20230729083705-37449abec8cc" 565 538 hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+2
nix/modules/knot.nix
··· 195 195 Match User ${cfg.gitUser} 196 196 AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 197 197 AuthorizedKeysCommandUser nobody 198 + ChallengeResponseAuthentication no 199 + PasswordAuthentication no 198 200 ''; 199 201 }; 200 202
+7 -5
nix/pkgs/sqlite-lib.nix
··· 1 1 { 2 - gcc, 3 2 stdenv, 4 3 sqlite-lib-src, 5 4 }: 6 5 stdenv.mkDerivation { 7 6 name = "sqlite-lib"; 8 7 src = sqlite-lib-src; 9 - nativeBuildInputs = [gcc]; 8 + 10 9 buildPhase = '' 11 - gcc -c sqlite3.c 12 - ar rcs libsqlite3.a sqlite3.o 13 - ranlib libsqlite3.a 10 + $CC -c sqlite3.c 11 + $AR rcs libsqlite3.a sqlite3.o 12 + $RANLIB libsqlite3.a 13 + ''; 14 + 15 + installPhase = '' 14 16 mkdir -p $out/include $out/lib 15 17 cp *.h $out/include 16 18 cp libsqlite3.a $out/lib
+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
··· 296 296 } 297 297 298 298 nd := types.NiceDiff{} 299 - nd.Commit.Parent = targetBranch 300 299 301 300 for _, d := range diffs { 302 301 ndiff := types.Diff{}
+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
··· 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
··· 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
··· 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 + }
+1
spindle/db/repos.go
··· 16 16 if err != nil { 17 17 return nil, err 18 18 } 19 + defer rows.Close() 19 20 20 21 var knots []string 21 22 for rows.Next() {
+22 -21
spindle/engine/engine.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 - "fmt" 7 6 "log/slog" 7 + "sync" 8 8 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 - "golang.org/x/sync/errgroup" 11 10 "tangled.org/core/notifier" 12 11 "tangled.org/core/spindle/config" 13 12 "tangled.org/core/spindle/db" ··· 31 30 } 32 31 } 33 32 34 - eg, ctx := errgroup.WithContext(ctx) 33 + var wg sync.WaitGroup 35 34 for eng, wfs := range pipeline.Workflows { 36 35 workflowTimeout := eng.WorkflowTimeout() 37 36 l.Info("using workflow timeout", "timeout", workflowTimeout) 38 37 39 38 for _, w := range wfs { 40 - eg.Go(func() error { 39 + wg.Add(1) 40 + go func() { 41 + defer wg.Done() 42 + 41 43 wid := models.WorkflowId{ 42 44 PipelineId: pipelineId, 43 45 Name: w.Name, ··· 45 47 46 48 err := db.StatusRunning(wid, n) 47 49 if err != nil { 48 - return err 50 + l.Error("failed to set workflow status to running", "wid", wid, "err", err) 51 + return 49 52 } 50 53 51 54 err = eng.SetupWorkflow(ctx, wid, &w) ··· 61 64 62 65 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 63 66 if dbErr != nil { 64 - return dbErr 67 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 65 68 } 66 - return err 69 + return 67 70 } 68 71 defer eng.DestroyWorkflow(ctx, wid) 69 72 70 - wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid) 73 + secretValues := make([]string, len(allSecrets)) 74 + for i, s := range allSecrets { 75 + secretValues[i] = s.Value 76 + } 77 + wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid, secretValues) 71 78 if err != nil { 72 79 l.Warn("failed to setup step logger; logs will not be persisted", "error", err) 73 80 wfLogger = nil ··· 99 106 if errors.Is(err, ErrTimedOut) { 100 107 dbErr := db.StatusTimeout(wid, n) 101 108 if dbErr != nil { 102 - return dbErr 109 + l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr) 103 110 } 104 111 } else { 105 112 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 106 113 if dbErr != nil { 107 - return dbErr 114 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 108 115 } 109 116 } 110 - 111 - return fmt.Errorf("starting steps image: %w", err) 117 + return 112 118 } 113 119 } 114 120 115 121 err = db.StatusSuccess(wid, n) 116 122 if err != nil { 117 - return err 123 + l.Error("failed to set workflow status to success", "wid", wid, "err", err) 118 124 } 119 - 120 - return nil 121 - }) 125 + }() 122 126 } 123 127 } 124 128 125 - if err := eg.Wait(); err != nil { 126 - l.Error("failed to run one or more workflows", "err", err) 127 - } else { 128 - l.Info("successfully ran full pipeline") 129 - } 129 + wg.Wait() 130 + l.Info("all workflows completed") 130 131 }
+5 -3
spindle/engines/nixery/engine.go
··· 294 294 workflowEnvs.AddEnv(s.Key, s.Value) 295 295 } 296 296 297 - step := w.Steps[idx].(Step) 297 + step := w.Steps[idx] 298 298 299 299 select { 300 300 case <-ctx.Done(): ··· 303 303 } 304 304 305 305 envs := append(EnvVars(nil), workflowEnvs...) 306 - for k, v := range step.environment { 307 - envs.AddEnv(k, v) 306 + if nixStep, ok := step.(Step); ok { 307 + for k, v := range nixStep.environment { 308 + envs.AddEnv(k, v) 309 + } 308 310 } 309 311 envs.AddEnv("HOME", homeDir) 310 312
+6 -1
spindle/models/logger.go
··· 12 12 type WorkflowLogger struct { 13 13 file *os.File 14 14 encoder *json.Encoder 15 + mask *SecretMask 15 16 } 16 17 17 - func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) { 18 + func NewWorkflowLogger(baseDir string, wid WorkflowId, secretValues []string) (*WorkflowLogger, error) { 18 19 path := LogFilePath(baseDir, wid) 19 20 20 21 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) ··· 25 26 return &WorkflowLogger{ 26 27 file: file, 27 28 encoder: json.NewEncoder(file), 29 + mask: NewSecretMask(secretValues), 28 30 }, nil 29 31 } 30 32 ··· 62 64 63 65 func (w *dataWriter) Write(p []byte) (int, error) { 64 66 line := strings.TrimRight(string(p), "\r\n") 67 + if w.logger.mask != nil { 68 + line = w.logger.mask.Mask(line) 69 + } 65 70 entry := NewDataLogLine(w.idx, line, w.stream) 66 71 if err := w.logger.encoder.Encode(entry); err != nil { 67 72 return 0, err
+51
spindle/models/secret_mask.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/base64" 5 + "strings" 6 + ) 7 + 8 + // SecretMask replaces secret values in strings with "***". 9 + type SecretMask struct { 10 + replacer *strings.Replacer 11 + } 12 + 13 + // NewSecretMask creates a mask for the given secret values. 14 + // Also registers base64-encoded variants of each secret. 15 + func NewSecretMask(values []string) *SecretMask { 16 + var pairs []string 17 + 18 + for _, value := range values { 19 + if value == "" { 20 + continue 21 + } 22 + 23 + pairs = append(pairs, value, "***") 24 + 25 + b64 := base64.StdEncoding.EncodeToString([]byte(value)) 26 + if b64 != value { 27 + pairs = append(pairs, b64, "***") 28 + } 29 + 30 + b64NoPad := strings.TrimRight(b64, "=") 31 + if b64NoPad != b64 && b64NoPad != value { 32 + pairs = append(pairs, b64NoPad, "***") 33 + } 34 + } 35 + 36 + if len(pairs) == 0 { 37 + return nil 38 + } 39 + 40 + return &SecretMask{ 41 + replacer: strings.NewReplacer(pairs...), 42 + } 43 + } 44 + 45 + // Mask replaces all registered secret values with "***". 46 + func (m *SecretMask) Mask(input string) string { 47 + if m == nil || m.replacer == nil { 48 + return input 49 + } 50 + return m.replacer.Replace(input) 51 + }
+135
spindle/models/secret_mask_test.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/base64" 5 + "testing" 6 + ) 7 + 8 + func TestSecretMask_BasicMasking(t *testing.T) { 9 + mask := NewSecretMask([]string{"mysecret123"}) 10 + 11 + input := "The password is mysecret123 in this log" 12 + expected := "The password is *** in this log" 13 + 14 + result := mask.Mask(input) 15 + if result != expected { 16 + t.Errorf("expected %q, got %q", expected, result) 17 + } 18 + } 19 + 20 + func TestSecretMask_Base64Encoded(t *testing.T) { 21 + secret := "mysecret123" 22 + mask := NewSecretMask([]string{secret}) 23 + 24 + b64 := base64.StdEncoding.EncodeToString([]byte(secret)) 25 + input := "Encoded: " + b64 26 + expected := "Encoded: ***" 27 + 28 + result := mask.Mask(input) 29 + if result != expected { 30 + t.Errorf("expected %q, got %q", expected, result) 31 + } 32 + } 33 + 34 + func TestSecretMask_Base64NoPadding(t *testing.T) { 35 + // "test" encodes to "dGVzdA==" with padding 36 + secret := "test" 37 + mask := NewSecretMask([]string{secret}) 38 + 39 + b64NoPad := "dGVzdA" // base64 without padding 40 + input := "Token: " + b64NoPad 41 + expected := "Token: ***" 42 + 43 + result := mask.Mask(input) 44 + if result != expected { 45 + t.Errorf("expected %q, got %q", expected, result) 46 + } 47 + } 48 + 49 + func TestSecretMask_MultipleSecrets(t *testing.T) { 50 + mask := NewSecretMask([]string{"password1", "apikey123"}) 51 + 52 + input := "Using password1 and apikey123 for auth" 53 + expected := "Using *** and *** for auth" 54 + 55 + result := mask.Mask(input) 56 + if result != expected { 57 + t.Errorf("expected %q, got %q", expected, result) 58 + } 59 + } 60 + 61 + func TestSecretMask_MultipleOccurrences(t *testing.T) { 62 + mask := NewSecretMask([]string{"secret"}) 63 + 64 + input := "secret appears twice: secret" 65 + expected := "*** appears twice: ***" 66 + 67 + result := mask.Mask(input) 68 + if result != expected { 69 + t.Errorf("expected %q, got %q", expected, result) 70 + } 71 + } 72 + 73 + func TestSecretMask_ShortValues(t *testing.T) { 74 + mask := NewSecretMask([]string{"abc", "xy", ""}) 75 + 76 + if mask == nil { 77 + t.Fatal("expected non-nil mask") 78 + } 79 + 80 + input := "abc xy test" 81 + expected := "*** *** test" 82 + result := mask.Mask(input) 83 + if result != expected { 84 + t.Errorf("expected %q, got %q", expected, result) 85 + } 86 + } 87 + 88 + func TestSecretMask_NilMask(t *testing.T) { 89 + var mask *SecretMask 90 + 91 + input := "some input text" 92 + result := mask.Mask(input) 93 + if result != input { 94 + t.Errorf("expected %q, got %q", input, result) 95 + } 96 + } 97 + 98 + func TestSecretMask_EmptyInput(t *testing.T) { 99 + mask := NewSecretMask([]string{"secret"}) 100 + 101 + result := mask.Mask("") 102 + if result != "" { 103 + t.Errorf("expected empty string, got %q", result) 104 + } 105 + } 106 + 107 + func TestSecretMask_NoMatch(t *testing.T) { 108 + mask := NewSecretMask([]string{"secretvalue"}) 109 + 110 + input := "nothing to mask here" 111 + result := mask.Mask(input) 112 + if result != input { 113 + t.Errorf("expected %q, got %q", input, result) 114 + } 115 + } 116 + 117 + func TestSecretMask_EmptySecretsList(t *testing.T) { 118 + mask := NewSecretMask([]string{}) 119 + 120 + if mask != nil { 121 + t.Error("expected nil mask for empty secrets list") 122 + } 123 + } 124 + 125 + func TestSecretMask_EmptySecretsFiltered(t *testing.T) { 126 + mask := NewSecretMask([]string{"ab", "validpassword", "", "xyz"}) 127 + 128 + input := "Using validpassword here" 129 + expected := "Using *** here" 130 + 131 + result := mask.Mask(input) 132 + if result != expected { 133 + t.Errorf("expected %q, got %q", expected, result) 134 + } 135 + }
+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 2 3 3 import ( 4 4 "github.com/bluekeyes/go-gitdiff/gitdiff" 5 - "github.com/go-git/go-git/v5/plumbing/object" 6 5 ) 7 6 8 7 type DiffOpts struct { ··· 43 42 44 43 // A nicer git diff representation. 45 44 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 { 45 + Commit Commit `json:"commit"` 46 + Stat struct { 57 47 FilesChanged int `json:"files_changed"` 58 48 Insertions int `json:"insertions"` 59 49 Deletions int `json:"deletions"`
+17 -17
types/repo.go
··· 8 8 ) 9 9 10 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"` 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 21 } 22 22 23 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"` 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 31 } 32 32 33 33 type RepoCommitResponse struct {