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

Compare changes

Choose any two refs to compare.

Changed files
+8220 -8251
.air
.tangled
.zed
api
appview
cache
session
config
db
issues
knots
middleware
oauth
pages
pulls
repo
reporesolver
serververify
settings
spindles
state
strings
xrpcclient
cmd
genjwks
punchcardPopulate
docs
eventconsumer
cursor
jetstream
knotclient
knotserver
lexicons
log
nix
rbac
spindle
workflow
xrpc
errors
+1 -1
.air/appview.toml
··· 5 5 6 6 exclude_regex = [".*_templ.go"] 7 7 include_ext = ["go", "templ", "html", "css"] 8 - exclude_dir = ["target", "atrium"] 8 + exclude_dir = ["target", "atrium", "nix"]
+3
.gitignore
··· 15 15 .env 16 16 *.rdb 17 17 .envrc 18 + # Created if following hacking.md 19 + genjwks.out 20 + /nix/vm-data
+12
.prettierrc.json
··· 1 + { 2 + "overrides": [ 3 + { 4 + "files": ["*.html"], 5 + "options": { 6 + "parser": "go-template" 7 + } 8 + } 9 + ], 10 + "bracketSameLine": true, 11 + "htmlWhitespaceSensitivity": "ignore" 12 + }
+2
.tangled/workflows/build.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 + engine: nixery 6 + 5 7 dependencies: 6 8 nixpkgs: 7 9 - go
+3 -12
.tangled/workflows/fmt.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 - dependencies: 6 - nixpkgs: 7 - - go 8 - - alejandra 5 + engine: nixery 9 6 10 7 steps: 11 - - name: "nix fmt" 8 + - name: "Check formatting" 12 9 command: | 13 - alejandra -c nix/**/*.nix flake.nix 14 - 15 - - name: "go fmt" 16 - command: | 17 - unformatted=$(gofmt -l .) 18 - test -z "$unformatted" || (echo "$unformatted" && exit 1) 19 - 10 + nix run .#fmt -- --ci
+2
.tangled/workflows/test.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 + engine: nixery 6 + 5 7 dependencies: 6 8 nixpkgs: 7 9 - go
-16
.zed/settings.json
··· 1 - // Folder-specific settings 2 - // 3 - // For a full list of overridable settings, and general information on folder-specific settings, 4 - // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 - { 6 - "languages": { 7 - "HTML": { 8 - "prettier": { 9 - "format_on_save": false, 10 - "allowed": true, 11 - "parser": "go-template", 12 - "plugins": ["prettier-plugin-go-template"] 13 - } 14 - } 15 - } 16 - }
+1772 -2319
api/tangled/cbor_gen.go
··· 866 866 867 867 return nil 868 868 } 869 - func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) MarshalCBOR(w io.Writer) error { 869 + func (t *GitRefUpdate) MarshalCBOR(w io.Writer) error { 870 870 if t == nil { 871 871 _, err := w.Write(cbg.CborNull) 872 872 return err ··· 874 874 875 875 cw := cbg.NewCborWriter(w) 876 876 877 - if _, err := cw.Write([]byte{162}); err != nil { 877 + if _, err := cw.Write([]byte{168}); err != nil { 878 878 return err 879 879 } 880 880 881 - // t.Count (int64) (int64) 882 - if len("count") > 1000000 { 883 - return xerrors.Errorf("Value in field \"count\" was too long") 881 + // t.Ref (string) (string) 882 + if len("ref") > 1000000 { 883 + return xerrors.Errorf("Value in field \"ref\" was too long") 884 884 } 885 885 886 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("count"))); err != nil { 886 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("ref"))); err != nil { 887 887 return err 888 888 } 889 - if _, err := cw.WriteString(string("count")); err != nil { 889 + if _, err := cw.WriteString(string("ref")); err != nil { 890 890 return err 891 891 } 892 892 893 - if t.Count >= 0 { 894 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Count)); err != nil { 895 - return err 896 - } 897 - } else { 898 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Count-1)); err != nil { 899 - return err 900 - } 893 + if len(t.Ref) > 1000000 { 894 + return xerrors.Errorf("Value in field t.Ref was too long") 901 895 } 902 896 903 - // t.Email (string) (string) 904 - if len("email") > 1000000 { 905 - return xerrors.Errorf("Value in field \"email\" was too long") 897 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Ref))); err != nil { 898 + return err 899 + } 900 + if _, err := cw.WriteString(string(t.Ref)); err != nil { 901 + return err 906 902 } 907 903 908 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("email"))); err != nil { 904 + // t.Meta (tangled.GitRefUpdate_Meta) (struct) 905 + if len("meta") > 1000000 { 906 + return xerrors.Errorf("Value in field \"meta\" was too long") 907 + } 908 + 909 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("meta"))); err != nil { 909 910 return err 910 911 } 911 - if _, err := cw.WriteString(string("email")); err != nil { 912 + if _, err := cw.WriteString(string("meta")); err != nil { 912 913 return err 913 914 } 914 915 915 - if len(t.Email) > 1000000 { 916 - return xerrors.Errorf("Value in field t.Email was too long") 916 + if err := t.Meta.MarshalCBOR(cw); err != nil { 917 + return err 917 918 } 918 919 919 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Email))); err != nil { 920 + // t.LexiconTypeID (string) (string) 921 + if len("$type") > 1000000 { 922 + return xerrors.Errorf("Value in field \"$type\" was too long") 923 + } 924 + 925 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 920 926 return err 921 927 } 922 - if _, err := cw.WriteString(string(t.Email)); err != nil { 928 + if _, err := cw.WriteString(string("$type")); err != nil { 929 + return err 930 + } 931 + 932 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.git.refUpdate"))); err != nil { 933 + return err 934 + } 935 + if _, err := cw.WriteString(string("sh.tangled.git.refUpdate")); err != nil { 936 + return err 937 + } 938 + 939 + // t.NewSha (string) (string) 940 + if len("newSha") > 1000000 { 941 + return xerrors.Errorf("Value in field \"newSha\" was too long") 942 + } 943 + 944 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("newSha"))); err != nil { 945 + return err 946 + } 947 + if _, err := cw.WriteString(string("newSha")); err != nil { 948 + return err 949 + } 950 + 951 + if len(t.NewSha) > 1000000 { 952 + return xerrors.Errorf("Value in field t.NewSha was too long") 953 + } 954 + 955 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.NewSha))); err != nil { 956 + return err 957 + } 958 + if _, err := cw.WriteString(string(t.NewSha)); err != nil { 959 + return err 960 + } 961 + 962 + // t.OldSha (string) (string) 963 + if len("oldSha") > 1000000 { 964 + return xerrors.Errorf("Value in field \"oldSha\" was too long") 965 + } 966 + 967 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("oldSha"))); err != nil { 968 + return err 969 + } 970 + if _, err := cw.WriteString(string("oldSha")); err != nil { 971 + return err 972 + } 973 + 974 + if len(t.OldSha) > 1000000 { 975 + return xerrors.Errorf("Value in field t.OldSha was too long") 976 + } 977 + 978 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.OldSha))); err != nil { 979 + return err 980 + } 981 + if _, err := cw.WriteString(string(t.OldSha)); err != nil { 982 + return err 983 + } 984 + 985 + // t.RepoDid (string) (string) 986 + if len("repoDid") > 1000000 { 987 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 988 + } 989 + 990 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 991 + return err 992 + } 993 + if _, err := cw.WriteString(string("repoDid")); err != nil { 994 + return err 995 + } 996 + 997 + if len(t.RepoDid) > 1000000 { 998 + return xerrors.Errorf("Value in field t.RepoDid was too long") 999 + } 1000 + 1001 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.RepoDid))); err != nil { 1002 + return err 1003 + } 1004 + if _, err := cw.WriteString(string(t.RepoDid)); err != nil { 1005 + return err 1006 + } 1007 + 1008 + // t.RepoName (string) (string) 1009 + if len("repoName") > 1000000 { 1010 + return xerrors.Errorf("Value in field \"repoName\" was too long") 1011 + } 1012 + 1013 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoName"))); err != nil { 1014 + return err 1015 + } 1016 + if _, err := cw.WriteString(string("repoName")); err != nil { 1017 + return err 1018 + } 1019 + 1020 + if len(t.RepoName) > 1000000 { 1021 + return xerrors.Errorf("Value in field t.RepoName was too long") 1022 + } 1023 + 1024 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.RepoName))); err != nil { 1025 + return err 1026 + } 1027 + if _, err := cw.WriteString(string(t.RepoName)); err != nil { 1028 + return err 1029 + } 1030 + 1031 + // t.CommitterDid (string) (string) 1032 + if len("committerDid") > 1000000 { 1033 + return xerrors.Errorf("Value in field \"committerDid\" was too long") 1034 + } 1035 + 1036 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("committerDid"))); err != nil { 1037 + return err 1038 + } 1039 + if _, err := cw.WriteString(string("committerDid")); err != nil { 1040 + return err 1041 + } 1042 + 1043 + if len(t.CommitterDid) > 1000000 { 1044 + return xerrors.Errorf("Value in field t.CommitterDid was too long") 1045 + } 1046 + 1047 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CommitterDid))); err != nil { 1048 + return err 1049 + } 1050 + if _, err := cw.WriteString(string(t.CommitterDid)); err != nil { 923 1051 return err 924 1052 } 925 1053 return nil 926 1054 } 927 1055 928 - func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) UnmarshalCBOR(r io.Reader) (err error) { 929 - *t = GitRefUpdate_Meta_CommitCount_ByEmail_Elem{} 1056 + func (t *GitRefUpdate) UnmarshalCBOR(r io.Reader) (err error) { 1057 + *t = GitRefUpdate{} 930 1058 931 1059 cr := cbg.NewCborReader(r) 932 1060 ··· 945 1073 } 946 1074 947 1075 if extra > cbg.MaxLength { 948 - return fmt.Errorf("GitRefUpdate_Meta_CommitCount_ByEmail_Elem: map struct too large (%d)", extra) 1076 + return fmt.Errorf("GitRefUpdate: map struct too large (%d)", extra) 949 1077 } 950 1078 951 1079 n := extra 952 1080 953 - nameBuf := make([]byte, 5) 1081 + nameBuf := make([]byte, 12) 954 1082 for i := uint64(0); i < n; i++ { 955 1083 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 956 1084 if err != nil { ··· 966 1094 } 967 1095 968 1096 switch string(nameBuf[:nameLen]) { 969 - // t.Count (int64) (int64) 970 - case "count": 1097 + // t.Ref (string) (string) 1098 + case "ref": 1099 + 971 1100 { 972 - maj, extra, err := cr.ReadHeader() 1101 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 973 1102 if err != nil { 974 1103 return err 975 1104 } 976 - var extraI int64 977 - switch maj { 978 - case cbg.MajUnsignedInt: 979 - extraI = int64(extra) 980 - if extraI < 0 { 981 - return fmt.Errorf("int64 positive overflow") 1105 + 1106 + t.Ref = string(sval) 1107 + } 1108 + // t.Meta (tangled.GitRefUpdate_Meta) (struct) 1109 + case "meta": 1110 + 1111 + { 1112 + 1113 + b, err := cr.ReadByte() 1114 + if err != nil { 1115 + return err 1116 + } 1117 + if b != cbg.CborNull[0] { 1118 + if err := cr.UnreadByte(); err != nil { 1119 + return err 982 1120 } 983 - case cbg.MajNegativeInt: 984 - extraI = int64(extra) 985 - if extraI < 0 { 986 - return fmt.Errorf("int64 negative overflow") 1121 + t.Meta = new(GitRefUpdate_Meta) 1122 + if err := t.Meta.UnmarshalCBOR(cr); err != nil { 1123 + return xerrors.Errorf("unmarshaling t.Meta pointer: %w", err) 987 1124 } 988 - extraI = -1 - extraI 989 - default: 990 - return fmt.Errorf("wrong type for int64 field: %d", maj) 991 1125 } 992 1126 993 - t.Count = int64(extraI) 994 1127 } 995 - // t.Email (string) (string) 996 - case "email": 1128 + // t.LexiconTypeID (string) (string) 1129 + case "$type": 997 1130 998 1131 { 999 1132 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 1001 1134 return err 1002 1135 } 1003 1136 1004 - t.Email = string(sval) 1137 + t.LexiconTypeID = string(sval) 1138 + } 1139 + // t.NewSha (string) (string) 1140 + case "newSha": 1141 + 1142 + { 1143 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1144 + if err != nil { 1145 + return err 1146 + } 1147 + 1148 + t.NewSha = string(sval) 1149 + } 1150 + // t.OldSha (string) (string) 1151 + case "oldSha": 1152 + 1153 + { 1154 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1155 + if err != nil { 1156 + return err 1157 + } 1158 + 1159 + t.OldSha = string(sval) 1160 + } 1161 + // t.RepoDid (string) (string) 1162 + case "repoDid": 1163 + 1164 + { 1165 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1166 + if err != nil { 1167 + return err 1168 + } 1169 + 1170 + t.RepoDid = string(sval) 1171 + } 1172 + // t.RepoName (string) (string) 1173 + case "repoName": 1174 + 1175 + { 1176 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1177 + if err != nil { 1178 + return err 1179 + } 1180 + 1181 + t.RepoName = string(sval) 1182 + } 1183 + // t.CommitterDid (string) (string) 1184 + case "committerDid": 1185 + 1186 + { 1187 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1188 + if err != nil { 1189 + return err 1190 + } 1191 + 1192 + t.CommitterDid = string(sval) 1005 1193 } 1006 1194 1007 1195 default: ··· 1014 1202 1015 1203 return nil 1016 1204 } 1017 - func (t *GitRefUpdate_Meta_CommitCount) MarshalCBOR(w io.Writer) error { 1205 + func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 1018 1206 if t == nil { 1019 1207 _, err := w.Write(cbg.CborNull) 1020 1208 return err 1021 1209 } 1022 1210 1023 1211 cw := cbg.NewCborWriter(w) 1024 - fieldCount := 1 1212 + fieldCount := 3 1025 1213 1026 - if t.ByEmail == nil { 1214 + if t.LangBreakdown == nil { 1027 1215 fieldCount-- 1028 1216 } 1029 1217 ··· 1031 1219 return err 1032 1220 } 1033 1221 1034 - // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1035 - if t.ByEmail != nil { 1222 + // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1223 + if len("commitCount") > 1000000 { 1224 + return xerrors.Errorf("Value in field \"commitCount\" was too long") 1225 + } 1036 1226 1037 - if len("byEmail") > 1000000 { 1038 - return xerrors.Errorf("Value in field \"byEmail\" was too long") 1227 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commitCount"))); err != nil { 1228 + return err 1229 + } 1230 + if _, err := cw.WriteString(string("commitCount")); err != nil { 1231 + return err 1232 + } 1233 + 1234 + if err := t.CommitCount.MarshalCBOR(cw); err != nil { 1235 + return err 1236 + } 1237 + 1238 + // t.IsDefaultRef (bool) (bool) 1239 + if len("isDefaultRef") > 1000000 { 1240 + return xerrors.Errorf("Value in field \"isDefaultRef\" was too long") 1241 + } 1242 + 1243 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("isDefaultRef"))); err != nil { 1244 + return err 1245 + } 1246 + if _, err := cw.WriteString(string("isDefaultRef")); err != nil { 1247 + return err 1248 + } 1249 + 1250 + if err := cbg.WriteBool(w, t.IsDefaultRef); err != nil { 1251 + return err 1252 + } 1253 + 1254 + // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1255 + if t.LangBreakdown != nil { 1256 + 1257 + if len("langBreakdown") > 1000000 { 1258 + return xerrors.Errorf("Value in field \"langBreakdown\" was too long") 1039 1259 } 1040 1260 1041 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("byEmail"))); err != nil { 1261 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("langBreakdown"))); err != nil { 1042 1262 return err 1043 1263 } 1044 - if _, err := cw.WriteString(string("byEmail")); err != nil { 1264 + if _, err := cw.WriteString(string("langBreakdown")); err != nil { 1045 1265 return err 1046 1266 } 1047 1267 1048 - if len(t.ByEmail) > 8192 { 1049 - return xerrors.Errorf("Slice value in field t.ByEmail was too long") 1050 - } 1051 - 1052 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ByEmail))); err != nil { 1268 + if err := t.LangBreakdown.MarshalCBOR(cw); err != nil { 1053 1269 return err 1054 1270 } 1055 - for _, v := range t.ByEmail { 1056 - if err := v.MarshalCBOR(cw); err != nil { 1057 - return err 1058 - } 1059 - 1060 - } 1061 1271 } 1062 1272 return nil 1063 1273 } 1064 1274 1065 - func (t *GitRefUpdate_Meta_CommitCount) UnmarshalCBOR(r io.Reader) (err error) { 1066 - *t = GitRefUpdate_Meta_CommitCount{} 1275 + func (t *GitRefUpdate_Meta) UnmarshalCBOR(r io.Reader) (err error) { 1276 + *t = GitRefUpdate_Meta{} 1067 1277 1068 1278 cr := cbg.NewCborReader(r) 1069 1279 ··· 1082 1292 } 1083 1293 1084 1294 if extra > cbg.MaxLength { 1085 - return fmt.Errorf("GitRefUpdate_Meta_CommitCount: map struct too large (%d)", extra) 1295 + return fmt.Errorf("GitRefUpdate_Meta: map struct too large (%d)", extra) 1086 1296 } 1087 1297 1088 1298 n := extra 1089 1299 1090 - nameBuf := make([]byte, 7) 1300 + nameBuf := make([]byte, 13) 1091 1301 for i := uint64(0); i < n; i++ { 1092 1302 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1093 1303 if err != nil { ··· 1103 1313 } 1104 1314 1105 1315 switch string(nameBuf[:nameLen]) { 1106 - // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1107 - case "byEmail": 1316 + // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1317 + case "commitCount": 1318 + 1319 + { 1320 + 1321 + b, err := cr.ReadByte() 1322 + if err != nil { 1323 + return err 1324 + } 1325 + if b != cbg.CborNull[0] { 1326 + if err := cr.UnreadByte(); err != nil { 1327 + return err 1328 + } 1329 + t.CommitCount = new(GitRefUpdate_Meta_CommitCount) 1330 + if err := t.CommitCount.UnmarshalCBOR(cr); err != nil { 1331 + return xerrors.Errorf("unmarshaling t.CommitCount pointer: %w", err) 1332 + } 1333 + } 1334 + 1335 + } 1336 + // t.IsDefaultRef (bool) (bool) 1337 + case "isDefaultRef": 1108 1338 1109 1339 maj, extra, err = cr.ReadHeader() 1110 1340 if err != nil { 1111 1341 return err 1112 1342 } 1113 - 1114 - if extra > 8192 { 1115 - return fmt.Errorf("t.ByEmail: array too large (%d)", extra) 1116 - } 1117 - 1118 - if maj != cbg.MajArray { 1119 - return fmt.Errorf("expected cbor array") 1343 + if maj != cbg.MajOther { 1344 + return fmt.Errorf("booleans must be major type 7") 1120 1345 } 1121 - 1122 - if extra > 0 { 1123 - t.ByEmail = make([]*GitRefUpdate_Meta_CommitCount_ByEmail_Elem, extra) 1346 + switch extra { 1347 + case 20: 1348 + t.IsDefaultRef = false 1349 + case 21: 1350 + t.IsDefaultRef = true 1351 + default: 1352 + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 1124 1353 } 1125 - 1126 - for i := 0; i < int(extra); i++ { 1127 - { 1128 - var maj byte 1129 - var extra uint64 1130 - var err error 1131 - _ = maj 1132 - _ = extra 1133 - _ = err 1134 - 1135 - { 1354 + // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1355 + case "langBreakdown": 1136 1356 1137 - b, err := cr.ReadByte() 1138 - if err != nil { 1139 - return err 1140 - } 1141 - if b != cbg.CborNull[0] { 1142 - if err := cr.UnreadByte(); err != nil { 1143 - return err 1144 - } 1145 - t.ByEmail[i] = new(GitRefUpdate_Meta_CommitCount_ByEmail_Elem) 1146 - if err := t.ByEmail[i].UnmarshalCBOR(cr); err != nil { 1147 - return xerrors.Errorf("unmarshaling t.ByEmail[i] pointer: %w", err) 1148 - } 1149 - } 1357 + { 1150 1358 1359 + b, err := cr.ReadByte() 1360 + if err != nil { 1361 + return err 1362 + } 1363 + if b != cbg.CborNull[0] { 1364 + if err := cr.UnreadByte(); err != nil { 1365 + return err 1151 1366 } 1152 - 1367 + t.LangBreakdown = new(GitRefUpdate_Meta_LangBreakdown) 1368 + if err := t.LangBreakdown.UnmarshalCBOR(cr); err != nil { 1369 + return xerrors.Errorf("unmarshaling t.LangBreakdown pointer: %w", err) 1370 + } 1153 1371 } 1372 + 1154 1373 } 1155 1374 1156 1375 default: ··· 1163 1382 1164 1383 return nil 1165 1384 } 1166 - func (t *GitRefUpdate_Meta_LangBreakdown) MarshalCBOR(w io.Writer) error { 1385 + func (t *GitRefUpdate_Meta_CommitCount) MarshalCBOR(w io.Writer) error { 1167 1386 if t == nil { 1168 1387 _, err := w.Write(cbg.CborNull) 1169 1388 return err ··· 1172 1391 cw := cbg.NewCborWriter(w) 1173 1392 fieldCount := 1 1174 1393 1175 - if t.Inputs == nil { 1394 + if t.ByEmail == nil { 1176 1395 fieldCount-- 1177 1396 } 1178 1397 ··· 1180 1399 return err 1181 1400 } 1182 1401 1183 - // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1184 - if t.Inputs != nil { 1402 + // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1403 + if t.ByEmail != nil { 1185 1404 1186 - if len("inputs") > 1000000 { 1187 - return xerrors.Errorf("Value in field \"inputs\" was too long") 1405 + if len("byEmail") > 1000000 { 1406 + return xerrors.Errorf("Value in field \"byEmail\" was too long") 1188 1407 } 1189 1408 1190 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil { 1409 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("byEmail"))); err != nil { 1191 1410 return err 1192 1411 } 1193 - if _, err := cw.WriteString(string("inputs")); err != nil { 1412 + if _, err := cw.WriteString(string("byEmail")); err != nil { 1194 1413 return err 1195 1414 } 1196 1415 1197 - if len(t.Inputs) > 8192 { 1198 - return xerrors.Errorf("Slice value in field t.Inputs was too long") 1416 + if len(t.ByEmail) > 8192 { 1417 + return xerrors.Errorf("Slice value in field t.ByEmail was too long") 1199 1418 } 1200 1419 1201 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil { 1420 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ByEmail))); err != nil { 1202 1421 return err 1203 1422 } 1204 - for _, v := range t.Inputs { 1423 + for _, v := range t.ByEmail { 1205 1424 if err := v.MarshalCBOR(cw); err != nil { 1206 1425 return err 1207 1426 } ··· 1211 1430 return nil 1212 1431 } 1213 1432 1214 - func (t *GitRefUpdate_Meta_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) { 1215 - *t = GitRefUpdate_Meta_LangBreakdown{} 1433 + func (t *GitRefUpdate_Meta_CommitCount) UnmarshalCBOR(r io.Reader) (err error) { 1434 + *t = GitRefUpdate_Meta_CommitCount{} 1216 1435 1217 1436 cr := cbg.NewCborReader(r) 1218 1437 ··· 1231 1450 } 1232 1451 1233 1452 if extra > cbg.MaxLength { 1234 - return fmt.Errorf("GitRefUpdate_Meta_LangBreakdown: map struct too large (%d)", extra) 1453 + return fmt.Errorf("GitRefUpdate_Meta_CommitCount: map struct too large (%d)", extra) 1235 1454 } 1236 1455 1237 1456 n := extra 1238 1457 1239 - nameBuf := make([]byte, 6) 1458 + nameBuf := make([]byte, 7) 1240 1459 for i := uint64(0); i < n; i++ { 1241 1460 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1242 1461 if err != nil { ··· 1252 1471 } 1253 1472 1254 1473 switch string(nameBuf[:nameLen]) { 1255 - // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1256 - case "inputs": 1474 + // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1475 + case "byEmail": 1257 1476 1258 1477 maj, extra, err = cr.ReadHeader() 1259 1478 if err != nil { ··· 1261 1480 } 1262 1481 1263 1482 if extra > 8192 { 1264 - return fmt.Errorf("t.Inputs: array too large (%d)", extra) 1483 + return fmt.Errorf("t.ByEmail: array too large (%d)", extra) 1265 1484 } 1266 1485 1267 1486 if maj != cbg.MajArray { ··· 1269 1488 } 1270 1489 1271 1490 if extra > 0 { 1272 - t.Inputs = make([]*GitRefUpdate_Pair, extra) 1491 + t.ByEmail = make([]*GitRefUpdate_Meta_CommitCount_ByEmail_Elem, extra) 1273 1492 } 1274 1493 1275 1494 for i := 0; i < int(extra); i++ { ··· 1291 1510 if err := cr.UnreadByte(); err != nil { 1292 1511 return err 1293 1512 } 1294 - t.Inputs[i] = new(GitRefUpdate_Pair) 1295 - if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil { 1296 - return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err) 1513 + t.ByEmail[i] = new(GitRefUpdate_Meta_CommitCount_ByEmail_Elem) 1514 + if err := t.ByEmail[i].UnmarshalCBOR(cr); err != nil { 1515 + return xerrors.Errorf("unmarshaling t.ByEmail[i] pointer: %w", err) 1297 1516 } 1298 1517 } 1299 1518 ··· 1312 1531 1313 1532 return nil 1314 1533 } 1315 - func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 1534 + func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) MarshalCBOR(w io.Writer) error { 1316 1535 if t == nil { 1317 1536 _, err := w.Write(cbg.CborNull) 1318 1537 return err 1319 1538 } 1320 1539 1321 1540 cw := cbg.NewCborWriter(w) 1322 - fieldCount := 3 1541 + 1542 + if _, err := cw.Write([]byte{162}); err != nil { 1543 + return err 1544 + } 1545 + 1546 + // t.Count (int64) (int64) 1547 + if len("count") > 1000000 { 1548 + return xerrors.Errorf("Value in field \"count\" was too long") 1549 + } 1550 + 1551 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("count"))); err != nil { 1552 + return err 1553 + } 1554 + if _, err := cw.WriteString(string("count")); err != nil { 1555 + return err 1556 + } 1557 + 1558 + if t.Count >= 0 { 1559 + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Count)); err != nil { 1560 + return err 1561 + } 1562 + } else { 1563 + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Count-1)); err != nil { 1564 + return err 1565 + } 1566 + } 1323 1567 1324 - if t.LangBreakdown == nil { 1325 - fieldCount-- 1568 + // t.Email (string) (string) 1569 + if len("email") > 1000000 { 1570 + return xerrors.Errorf("Value in field \"email\" was too long") 1326 1571 } 1327 1572 1328 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1573 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("email"))); err != nil { 1574 + return err 1575 + } 1576 + if _, err := cw.WriteString(string("email")); err != nil { 1329 1577 return err 1330 1578 } 1331 1579 1332 - // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1333 - if len("commitCount") > 1000000 { 1334 - return xerrors.Errorf("Value in field \"commitCount\" was too long") 1580 + if len(t.Email) > 1000000 { 1581 + return xerrors.Errorf("Value in field t.Email was too long") 1335 1582 } 1336 1583 1337 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commitCount"))); err != nil { 1584 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Email))); err != nil { 1338 1585 return err 1339 1586 } 1340 - if _, err := cw.WriteString(string("commitCount")); err != nil { 1587 + if _, err := cw.WriteString(string(t.Email)); err != nil { 1341 1588 return err 1342 1589 } 1590 + return nil 1591 + } 1343 1592 1344 - if err := t.CommitCount.MarshalCBOR(cw); err != nil { 1593 + func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) UnmarshalCBOR(r io.Reader) (err error) { 1594 + *t = GitRefUpdate_Meta_CommitCount_ByEmail_Elem{} 1595 + 1596 + cr := cbg.NewCborReader(r) 1597 + 1598 + maj, extra, err := cr.ReadHeader() 1599 + if err != nil { 1345 1600 return err 1346 1601 } 1602 + defer func() { 1603 + if err == io.EOF { 1604 + err = io.ErrUnexpectedEOF 1605 + } 1606 + }() 1347 1607 1348 - // t.IsDefaultRef (bool) (bool) 1349 - if len("isDefaultRef") > 1000000 { 1350 - return xerrors.Errorf("Value in field \"isDefaultRef\" was too long") 1608 + if maj != cbg.MajMap { 1609 + return fmt.Errorf("cbor input should be of type map") 1351 1610 } 1352 1611 1353 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("isDefaultRef"))); err != nil { 1354 - return err 1612 + if extra > cbg.MaxLength { 1613 + return fmt.Errorf("GitRefUpdate_Meta_CommitCount_ByEmail_Elem: map struct too large (%d)", extra) 1355 1614 } 1356 - if _, err := cw.WriteString(string("isDefaultRef")); err != nil { 1615 + 1616 + n := extra 1617 + 1618 + nameBuf := make([]byte, 5) 1619 + for i := uint64(0); i < n; i++ { 1620 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1621 + if err != nil { 1622 + return err 1623 + } 1624 + 1625 + if !ok { 1626 + // Field doesn't exist on this type, so ignore it 1627 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 1628 + return err 1629 + } 1630 + continue 1631 + } 1632 + 1633 + switch string(nameBuf[:nameLen]) { 1634 + // t.Count (int64) (int64) 1635 + case "count": 1636 + { 1637 + maj, extra, err := cr.ReadHeader() 1638 + if err != nil { 1639 + return err 1640 + } 1641 + var extraI int64 1642 + switch maj { 1643 + case cbg.MajUnsignedInt: 1644 + extraI = int64(extra) 1645 + if extraI < 0 { 1646 + return fmt.Errorf("int64 positive overflow") 1647 + } 1648 + case cbg.MajNegativeInt: 1649 + extraI = int64(extra) 1650 + if extraI < 0 { 1651 + return fmt.Errorf("int64 negative overflow") 1652 + } 1653 + extraI = -1 - extraI 1654 + default: 1655 + return fmt.Errorf("wrong type for int64 field: %d", maj) 1656 + } 1657 + 1658 + t.Count = int64(extraI) 1659 + } 1660 + // t.Email (string) (string) 1661 + case "email": 1662 + 1663 + { 1664 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1665 + if err != nil { 1666 + return err 1667 + } 1668 + 1669 + t.Email = string(sval) 1670 + } 1671 + 1672 + default: 1673 + // Field doesn't exist on this type, so ignore it 1674 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1675 + return err 1676 + } 1677 + } 1678 + } 1679 + 1680 + return nil 1681 + } 1682 + func (t *GitRefUpdate_Meta_LangBreakdown) MarshalCBOR(w io.Writer) error { 1683 + if t == nil { 1684 + _, err := w.Write(cbg.CborNull) 1357 1685 return err 1358 1686 } 1359 1687 1360 - if err := cbg.WriteBool(w, t.IsDefaultRef); err != nil { 1688 + cw := cbg.NewCborWriter(w) 1689 + fieldCount := 1 1690 + 1691 + if t.Inputs == nil { 1692 + fieldCount-- 1693 + } 1694 + 1695 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1361 1696 return err 1362 1697 } 1363 1698 1364 - // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1365 - if t.LangBreakdown != nil { 1699 + // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1700 + if t.Inputs != nil { 1366 1701 1367 - if len("langBreakdown") > 1000000 { 1368 - return xerrors.Errorf("Value in field \"langBreakdown\" was too long") 1702 + if len("inputs") > 1000000 { 1703 + return xerrors.Errorf("Value in field \"inputs\" was too long") 1369 1704 } 1370 1705 1371 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("langBreakdown"))); err != nil { 1706 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil { 1372 1707 return err 1373 1708 } 1374 - if _, err := cw.WriteString(string("langBreakdown")); err != nil { 1709 + if _, err := cw.WriteString(string("inputs")); err != nil { 1375 1710 return err 1376 1711 } 1377 1712 1378 - if err := t.LangBreakdown.MarshalCBOR(cw); err != nil { 1713 + if len(t.Inputs) > 8192 { 1714 + return xerrors.Errorf("Slice value in field t.Inputs was too long") 1715 + } 1716 + 1717 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil { 1379 1718 return err 1380 1719 } 1720 + for _, v := range t.Inputs { 1721 + if err := v.MarshalCBOR(cw); err != nil { 1722 + return err 1723 + } 1724 + 1725 + } 1381 1726 } 1382 1727 return nil 1383 1728 } 1384 1729 1385 - func (t *GitRefUpdate_Meta) UnmarshalCBOR(r io.Reader) (err error) { 1386 - *t = GitRefUpdate_Meta{} 1730 + func (t *GitRefUpdate_Meta_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) { 1731 + *t = GitRefUpdate_Meta_LangBreakdown{} 1387 1732 1388 1733 cr := cbg.NewCborReader(r) 1389 1734 ··· 1402 1747 } 1403 1748 1404 1749 if extra > cbg.MaxLength { 1405 - return fmt.Errorf("GitRefUpdate_Meta: map struct too large (%d)", extra) 1750 + return fmt.Errorf("GitRefUpdate_Meta_LangBreakdown: map struct too large (%d)", extra) 1406 1751 } 1407 1752 1408 1753 n := extra 1409 1754 1410 - nameBuf := make([]byte, 13) 1755 + nameBuf := make([]byte, 6) 1411 1756 for i := uint64(0); i < n; i++ { 1412 1757 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1413 1758 if err != nil { ··· 1423 1768 } 1424 1769 1425 1770 switch string(nameBuf[:nameLen]) { 1426 - // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1427 - case "commitCount": 1428 - 1429 - { 1430 - 1431 - b, err := cr.ReadByte() 1432 - if err != nil { 1433 - return err 1434 - } 1435 - if b != cbg.CborNull[0] { 1436 - if err := cr.UnreadByte(); err != nil { 1437 - return err 1438 - } 1439 - t.CommitCount = new(GitRefUpdate_Meta_CommitCount) 1440 - if err := t.CommitCount.UnmarshalCBOR(cr); err != nil { 1441 - return xerrors.Errorf("unmarshaling t.CommitCount pointer: %w", err) 1442 - } 1443 - } 1444 - 1445 - } 1446 - // t.IsDefaultRef (bool) (bool) 1447 - case "isDefaultRef": 1771 + // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1772 + case "inputs": 1448 1773 1449 1774 maj, extra, err = cr.ReadHeader() 1450 1775 if err != nil { 1451 1776 return err 1452 1777 } 1453 - if maj != cbg.MajOther { 1454 - return fmt.Errorf("booleans must be major type 7") 1778 + 1779 + if extra > 8192 { 1780 + return fmt.Errorf("t.Inputs: array too large (%d)", extra) 1455 1781 } 1456 - switch extra { 1457 - case 20: 1458 - t.IsDefaultRef = false 1459 - case 21: 1460 - t.IsDefaultRef = true 1461 - default: 1462 - return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 1782 + 1783 + if maj != cbg.MajArray { 1784 + return fmt.Errorf("expected cbor array") 1463 1785 } 1464 - // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1465 - case "langBreakdown": 1786 + 1787 + if extra > 0 { 1788 + t.Inputs = make([]*GitRefUpdate_Pair, extra) 1789 + } 1790 + 1791 + for i := 0; i < int(extra); i++ { 1792 + { 1793 + var maj byte 1794 + var extra uint64 1795 + var err error 1796 + _ = maj 1797 + _ = extra 1798 + _ = err 1799 + 1800 + { 1466 1801 1467 - { 1802 + b, err := cr.ReadByte() 1803 + if err != nil { 1804 + return err 1805 + } 1806 + if b != cbg.CborNull[0] { 1807 + if err := cr.UnreadByte(); err != nil { 1808 + return err 1809 + } 1810 + t.Inputs[i] = new(GitRefUpdate_Pair) 1811 + if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil { 1812 + return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err) 1813 + } 1814 + } 1468 1815 1469 - b, err := cr.ReadByte() 1470 - if err != nil { 1471 - return err 1472 - } 1473 - if b != cbg.CborNull[0] { 1474 - if err := cr.UnreadByte(); err != nil { 1475 - return err 1476 1816 } 1477 - t.LangBreakdown = new(GitRefUpdate_Meta_LangBreakdown) 1478 - if err := t.LangBreakdown.UnmarshalCBOR(cr); err != nil { 1479 - return xerrors.Errorf("unmarshaling t.LangBreakdown pointer: %w", err) 1480 - } 1817 + 1481 1818 } 1482 - 1483 1819 } 1484 1820 1485 1821 default: ··· 1641 1977 1642 1978 return nil 1643 1979 } 1644 - func (t *GitRefUpdate) MarshalCBOR(w io.Writer) error { 1980 + func (t *GraphFollow) MarshalCBOR(w io.Writer) error { 1645 1981 if t == nil { 1646 1982 _, err := w.Write(cbg.CborNull) 1647 1983 return err ··· 1649 1985 1650 1986 cw := cbg.NewCborWriter(w) 1651 1987 1652 - if _, err := cw.Write([]byte{168}); err != nil { 1653 - return err 1654 - } 1655 - 1656 - // t.Ref (string) (string) 1657 - if len("ref") > 1000000 { 1658 - return xerrors.Errorf("Value in field \"ref\" was too long") 1659 - } 1660 - 1661 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("ref"))); err != nil { 1662 - return err 1663 - } 1664 - if _, err := cw.WriteString(string("ref")); err != nil { 1665 - return err 1666 - } 1667 - 1668 - if len(t.Ref) > 1000000 { 1669 - return xerrors.Errorf("Value in field t.Ref was too long") 1670 - } 1671 - 1672 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Ref))); err != nil { 1673 - return err 1674 - } 1675 - if _, err := cw.WriteString(string(t.Ref)); err != nil { 1676 - return err 1677 - } 1678 - 1679 - // t.Meta (tangled.GitRefUpdate_Meta) (struct) 1680 - if len("meta") > 1000000 { 1681 - return xerrors.Errorf("Value in field \"meta\" was too long") 1682 - } 1683 - 1684 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("meta"))); err != nil { 1685 - return err 1686 - } 1687 - if _, err := cw.WriteString(string("meta")); err != nil { 1688 - return err 1689 - } 1690 - 1691 - if err := t.Meta.MarshalCBOR(cw); err != nil { 1988 + if _, err := cw.Write([]byte{163}); err != nil { 1692 1989 return err 1693 1990 } 1694 1991 ··· 1704 2001 return err 1705 2002 } 1706 2003 1707 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.git.refUpdate"))); err != nil { 1708 - return err 1709 - } 1710 - if _, err := cw.WriteString(string("sh.tangled.git.refUpdate")); err != nil { 1711 - return err 1712 - } 1713 - 1714 - // t.NewSha (string) (string) 1715 - if len("newSha") > 1000000 { 1716 - return xerrors.Errorf("Value in field \"newSha\" was too long") 1717 - } 1718 - 1719 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("newSha"))); err != nil { 1720 - return err 1721 - } 1722 - if _, err := cw.WriteString(string("newSha")); err != nil { 1723 - return err 1724 - } 1725 - 1726 - if len(t.NewSha) > 1000000 { 1727 - return xerrors.Errorf("Value in field t.NewSha was too long") 1728 - } 1729 - 1730 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.NewSha))); err != nil { 1731 - return err 1732 - } 1733 - if _, err := cw.WriteString(string(t.NewSha)); err != nil { 1734 - return err 1735 - } 1736 - 1737 - // t.OldSha (string) (string) 1738 - if len("oldSha") > 1000000 { 1739 - return xerrors.Errorf("Value in field \"oldSha\" was too long") 1740 - } 1741 - 1742 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("oldSha"))); err != nil { 1743 - return err 1744 - } 1745 - if _, err := cw.WriteString(string("oldSha")); err != nil { 1746 - return err 1747 - } 1748 - 1749 - if len(t.OldSha) > 1000000 { 1750 - return xerrors.Errorf("Value in field t.OldSha was too long") 1751 - } 1752 - 1753 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.OldSha))); err != nil { 1754 - return err 1755 - } 1756 - if _, err := cw.WriteString(string(t.OldSha)); err != nil { 1757 - return err 1758 - } 1759 - 1760 - // t.RepoDid (string) (string) 1761 - if len("repoDid") > 1000000 { 1762 - return xerrors.Errorf("Value in field \"repoDid\" was too long") 1763 - } 1764 - 1765 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 2004 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.graph.follow"))); err != nil { 1766 2005 return err 1767 2006 } 1768 - if _, err := cw.WriteString(string("repoDid")); err != nil { 2007 + if _, err := cw.WriteString(string("sh.tangled.graph.follow")); err != nil { 1769 2008 return err 1770 2009 } 1771 2010 1772 - if len(t.RepoDid) > 1000000 { 1773 - return xerrors.Errorf("Value in field t.RepoDid was too long") 2011 + // t.Subject (string) (string) 2012 + if len("subject") > 1000000 { 2013 + return xerrors.Errorf("Value in field \"subject\" was too long") 1774 2014 } 1775 2015 1776 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.RepoDid))); err != nil { 2016 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 1777 2017 return err 1778 2018 } 1779 - if _, err := cw.WriteString(string(t.RepoDid)); err != nil { 2019 + if _, err := cw.WriteString(string("subject")); err != nil { 1780 2020 return err 1781 2021 } 1782 2022 1783 - // t.RepoName (string) (string) 1784 - if len("repoName") > 1000000 { 1785 - return xerrors.Errorf("Value in field \"repoName\" was too long") 2023 + if len(t.Subject) > 1000000 { 2024 + return xerrors.Errorf("Value in field t.Subject was too long") 1786 2025 } 1787 2026 1788 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoName"))); err != nil { 2027 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 1789 2028 return err 1790 2029 } 1791 - if _, err := cw.WriteString(string("repoName")); err != nil { 2030 + if _, err := cw.WriteString(string(t.Subject)); err != nil { 1792 2031 return err 1793 2032 } 1794 2033 1795 - if len(t.RepoName) > 1000000 { 1796 - return xerrors.Errorf("Value in field t.RepoName was too long") 2034 + // t.CreatedAt (string) (string) 2035 + if len("createdAt") > 1000000 { 2036 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 1797 2037 } 1798 2038 1799 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.RepoName))); err != nil { 2039 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 1800 2040 return err 1801 2041 } 1802 - if _, err := cw.WriteString(string(t.RepoName)); err != nil { 2042 + if _, err := cw.WriteString(string("createdAt")); err != nil { 1803 2043 return err 1804 2044 } 1805 2045 1806 - // t.CommitterDid (string) (string) 1807 - if len("committerDid") > 1000000 { 1808 - return xerrors.Errorf("Value in field \"committerDid\" was too long") 2046 + if len(t.CreatedAt) > 1000000 { 2047 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 1809 2048 } 1810 2049 1811 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("committerDid"))); err != nil { 2050 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 1812 2051 return err 1813 2052 } 1814 - if _, err := cw.WriteString(string("committerDid")); err != nil { 1815 - return err 1816 - } 1817 - 1818 - if len(t.CommitterDid) > 1000000 { 1819 - return xerrors.Errorf("Value in field t.CommitterDid was too long") 1820 - } 1821 - 1822 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CommitterDid))); err != nil { 1823 - return err 1824 - } 1825 - if _, err := cw.WriteString(string(t.CommitterDid)); err != nil { 2053 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 1826 2054 return err 1827 2055 } 1828 2056 return nil 1829 2057 } 1830 2058 1831 - func (t *GitRefUpdate) UnmarshalCBOR(r io.Reader) (err error) { 1832 - *t = GitRefUpdate{} 2059 + func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { 2060 + *t = GraphFollow{} 1833 2061 1834 2062 cr := cbg.NewCborReader(r) 1835 2063 ··· 1848 2076 } 1849 2077 1850 2078 if extra > cbg.MaxLength { 1851 - return fmt.Errorf("GitRefUpdate: map struct too large (%d)", extra) 2079 + return fmt.Errorf("GraphFollow: map struct too large (%d)", extra) 1852 2080 } 1853 2081 1854 2082 n := extra 1855 2083 1856 - nameBuf := make([]byte, 12) 2084 + nameBuf := make([]byte, 9) 1857 2085 for i := uint64(0); i < n; i++ { 1858 2086 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1859 2087 if err != nil { ··· 1869 2097 } 1870 2098 1871 2099 switch string(nameBuf[:nameLen]) { 1872 - // t.Ref (string) (string) 1873 - case "ref": 1874 - 1875 - { 1876 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1877 - if err != nil { 1878 - return err 1879 - } 1880 - 1881 - t.Ref = string(sval) 1882 - } 1883 - // t.Meta (tangled.GitRefUpdate_Meta) (struct) 1884 - case "meta": 1885 - 1886 - { 1887 - 1888 - b, err := cr.ReadByte() 1889 - if err != nil { 1890 - return err 1891 - } 1892 - if b != cbg.CborNull[0] { 1893 - if err := cr.UnreadByte(); err != nil { 1894 - return err 1895 - } 1896 - t.Meta = new(GitRefUpdate_Meta) 1897 - if err := t.Meta.UnmarshalCBOR(cr); err != nil { 1898 - return xerrors.Errorf("unmarshaling t.Meta pointer: %w", err) 1899 - } 1900 - } 1901 - 1902 - } 1903 - // t.LexiconTypeID (string) (string) 2100 + // t.LexiconTypeID (string) (string) 1904 2101 case "$type": 1905 2102 1906 2103 { ··· 1911 2108 1912 2109 t.LexiconTypeID = string(sval) 1913 2110 } 1914 - // t.NewSha (string) (string) 1915 - case "newSha": 2111 + // t.Subject (string) (string) 2112 + case "subject": 1916 2113 1917 2114 { 1918 2115 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 1920 2117 return err 1921 2118 } 1922 2119 1923 - t.NewSha = string(sval) 2120 + t.Subject = string(sval) 1924 2121 } 1925 - // t.OldSha (string) (string) 1926 - case "oldSha": 2122 + // t.CreatedAt (string) (string) 2123 + case "createdAt": 1927 2124 1928 2125 { 1929 2126 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 1931 2128 return err 1932 2129 } 1933 2130 1934 - t.OldSha = string(sval) 1935 - } 1936 - // t.RepoDid (string) (string) 1937 - case "repoDid": 1938 - 1939 - { 1940 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1941 - if err != nil { 1942 - return err 1943 - } 1944 - 1945 - t.RepoDid = string(sval) 1946 - } 1947 - // t.RepoName (string) (string) 1948 - case "repoName": 1949 - 1950 - { 1951 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1952 - if err != nil { 1953 - return err 1954 - } 1955 - 1956 - t.RepoName = string(sval) 1957 - } 1958 - // t.CommitterDid (string) (string) 1959 - case "committerDid": 1960 - 1961 - { 1962 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1963 - if err != nil { 1964 - return err 1965 - } 1966 - 1967 - t.CommitterDid = string(sval) 2131 + t.CreatedAt = string(sval) 1968 2132 } 1969 2133 1970 2134 default: ··· 1977 2141 1978 2142 return nil 1979 2143 } 1980 - func (t *GraphFollow) MarshalCBOR(w io.Writer) error { 2144 + func (t *Knot) MarshalCBOR(w io.Writer) error { 1981 2145 if t == nil { 1982 2146 _, err := w.Write(cbg.CborNull) 1983 2147 return err ··· 1985 2149 1986 2150 cw := cbg.NewCborWriter(w) 1987 2151 1988 - if _, err := cw.Write([]byte{163}); err != nil { 2152 + if _, err := cw.Write([]byte{162}); err != nil { 1989 2153 return err 1990 2154 } 1991 2155 ··· 2001 2165 return err 2002 2166 } 2003 2167 2004 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.graph.follow"))); err != nil { 2005 - return err 2006 - } 2007 - if _, err := cw.WriteString(string("sh.tangled.graph.follow")); err != nil { 2008 - return err 2009 - } 2010 - 2011 - // t.Subject (string) (string) 2012 - if len("subject") > 1000000 { 2013 - return xerrors.Errorf("Value in field \"subject\" was too long") 2014 - } 2015 - 2016 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 2168 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.knot"))); err != nil { 2017 2169 return err 2018 2170 } 2019 - if _, err := cw.WriteString(string("subject")); err != nil { 2020 - return err 2021 - } 2022 - 2023 - if len(t.Subject) > 1000000 { 2024 - return xerrors.Errorf("Value in field t.Subject was too long") 2025 - } 2026 - 2027 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 2028 - return err 2029 - } 2030 - if _, err := cw.WriteString(string(t.Subject)); err != nil { 2171 + if _, err := cw.WriteString(string("sh.tangled.knot")); err != nil { 2031 2172 return err 2032 2173 } 2033 2174 ··· 2056 2197 return nil 2057 2198 } 2058 2199 2059 - func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { 2060 - *t = GraphFollow{} 2200 + func (t *Knot) UnmarshalCBOR(r io.Reader) (err error) { 2201 + *t = Knot{} 2061 2202 2062 2203 cr := cbg.NewCborReader(r) 2063 2204 ··· 2076 2217 } 2077 2218 2078 2219 if extra > cbg.MaxLength { 2079 - return fmt.Errorf("GraphFollow: map struct too large (%d)", extra) 2220 + return fmt.Errorf("Knot: map struct too large (%d)", extra) 2080 2221 } 2081 2222 2082 2223 n := extra ··· 2107 2248 } 2108 2249 2109 2250 t.LexiconTypeID = string(sval) 2110 - } 2111 - // t.Subject (string) (string) 2112 - case "subject": 2113 - 2114 - { 2115 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2116 - if err != nil { 2117 - return err 2118 - } 2119 - 2120 - t.Subject = string(sval) 2121 2251 } 2122 2252 // t.CreatedAt (string) (string) 2123 2253 case "createdAt": ··· 2339 2469 2340 2470 return nil 2341 2471 } 2342 - func (t *Knot) MarshalCBOR(w io.Writer) error { 2343 - if t == nil { 2344 - _, err := w.Write(cbg.CborNull) 2345 - return err 2346 - } 2347 - 2348 - cw := cbg.NewCborWriter(w) 2349 - 2350 - if _, err := cw.Write([]byte{162}); err != nil { 2351 - return err 2352 - } 2353 - 2354 - // t.LexiconTypeID (string) (string) 2355 - if len("$type") > 1000000 { 2356 - return xerrors.Errorf("Value in field \"$type\" was too long") 2357 - } 2358 - 2359 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 2360 - return err 2361 - } 2362 - if _, err := cw.WriteString(string("$type")); err != nil { 2363 - return err 2364 - } 2365 - 2366 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.knot"))); err != nil { 2367 - return err 2368 - } 2369 - if _, err := cw.WriteString(string("sh.tangled.knot")); err != nil { 2370 - return err 2371 - } 2372 - 2373 - // t.CreatedAt (string) (string) 2374 - if len("createdAt") > 1000000 { 2375 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 2376 - } 2377 - 2378 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2379 - return err 2380 - } 2381 - if _, err := cw.WriteString(string("createdAt")); err != nil { 2382 - return err 2383 - } 2384 - 2385 - if len(t.CreatedAt) > 1000000 { 2386 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 2387 - } 2388 - 2389 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 2390 - return err 2391 - } 2392 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 2393 - return err 2394 - } 2395 - return nil 2396 - } 2397 - 2398 - func (t *Knot) UnmarshalCBOR(r io.Reader) (err error) { 2399 - *t = Knot{} 2400 - 2401 - cr := cbg.NewCborReader(r) 2402 - 2403 - maj, extra, err := cr.ReadHeader() 2404 - if err != nil { 2405 - return err 2406 - } 2407 - defer func() { 2408 - if err == io.EOF { 2409 - err = io.ErrUnexpectedEOF 2410 - } 2411 - }() 2412 - 2413 - if maj != cbg.MajMap { 2414 - return fmt.Errorf("cbor input should be of type map") 2415 - } 2416 - 2417 - if extra > cbg.MaxLength { 2418 - return fmt.Errorf("Knot: map struct too large (%d)", extra) 2419 - } 2420 - 2421 - n := extra 2422 - 2423 - nameBuf := make([]byte, 9) 2424 - for i := uint64(0); i < n; i++ { 2425 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2426 - if err != nil { 2427 - return err 2428 - } 2429 - 2430 - if !ok { 2431 - // Field doesn't exist on this type, so ignore it 2432 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 2433 - return err 2434 - } 2435 - continue 2436 - } 2437 - 2438 - switch string(nameBuf[:nameLen]) { 2439 - // t.LexiconTypeID (string) (string) 2440 - case "$type": 2441 - 2442 - { 2443 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2444 - if err != nil { 2445 - return err 2446 - } 2447 - 2448 - t.LexiconTypeID = string(sval) 2449 - } 2450 - // t.CreatedAt (string) (string) 2451 - case "createdAt": 2452 - 2453 - { 2454 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2455 - if err != nil { 2456 - return err 2457 - } 2458 - 2459 - t.CreatedAt = string(sval) 2460 - } 2461 - 2462 - default: 2463 - // Field doesn't exist on this type, so ignore it 2464 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 2465 - return err 2466 - } 2467 - } 2468 - } 2469 - 2470 - return nil 2471 - } 2472 - func (t *PipelineStatus) MarshalCBOR(w io.Writer) error { 2472 + func (t *Pipeline) MarshalCBOR(w io.Writer) error { 2473 2473 if t == nil { 2474 2474 _, err := w.Write(cbg.CborNull) 2475 2475 return err 2476 2476 } 2477 2477 2478 2478 cw := cbg.NewCborWriter(w) 2479 - fieldCount := 7 2480 2479 2481 - if t.Error == nil { 2482 - fieldCount-- 2483 - } 2484 - 2485 - if t.ExitCode == nil { 2486 - fieldCount-- 2487 - } 2488 - 2489 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2480 + if _, err := cw.Write([]byte{163}); err != nil { 2490 2481 return err 2491 2482 } 2492 2483 ··· 2502 2493 return err 2503 2494 } 2504 2495 2505 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.pipeline.status"))); err != nil { 2496 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.pipeline"))); err != nil { 2506 2497 return err 2507 2498 } 2508 - if _, err := cw.WriteString(string("sh.tangled.pipeline.status")); err != nil { 2499 + if _, err := cw.WriteString(string("sh.tangled.pipeline")); err != nil { 2509 2500 return err 2510 2501 } 2511 2502 2512 - // t.Error (string) (string) 2513 - if t.Error != nil { 2514 - 2515 - if len("error") > 1000000 { 2516 - return xerrors.Errorf("Value in field \"error\" was too long") 2517 - } 2518 - 2519 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("error"))); err != nil { 2520 - return err 2521 - } 2522 - if _, err := cw.WriteString(string("error")); err != nil { 2523 - return err 2524 - } 2525 - 2526 - if t.Error == nil { 2527 - if _, err := cw.Write(cbg.CborNull); err != nil { 2528 - return err 2529 - } 2530 - } else { 2531 - if len(*t.Error) > 1000000 { 2532 - return xerrors.Errorf("Value in field t.Error was too long") 2533 - } 2534 - 2535 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Error))); err != nil { 2536 - return err 2537 - } 2538 - if _, err := cw.WriteString(string(*t.Error)); err != nil { 2539 - return err 2540 - } 2541 - } 2542 - } 2543 - 2544 - // t.Status (string) (string) 2545 - if len("status") > 1000000 { 2546 - return xerrors.Errorf("Value in field \"status\" was too long") 2503 + // t.Workflows ([]*tangled.Pipeline_Workflow) (slice) 2504 + if len("workflows") > 1000000 { 2505 + return xerrors.Errorf("Value in field \"workflows\" was too long") 2547 2506 } 2548 2507 2549 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 2508 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("workflows"))); err != nil { 2550 2509 return err 2551 2510 } 2552 - if _, err := cw.WriteString(string("status")); err != nil { 2511 + if _, err := cw.WriteString(string("workflows")); err != nil { 2553 2512 return err 2554 2513 } 2555 2514 2556 - if len(t.Status) > 1000000 { 2557 - return xerrors.Errorf("Value in field t.Status was too long") 2515 + if len(t.Workflows) > 8192 { 2516 + return xerrors.Errorf("Slice value in field t.Workflows was too long") 2558 2517 } 2559 2518 2560 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 2561 - return err 2562 - } 2563 - if _, err := cw.WriteString(string(t.Status)); err != nil { 2519 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Workflows))); err != nil { 2564 2520 return err 2565 2521 } 2566 - 2567 - // t.ExitCode (int64) (int64) 2568 - if t.ExitCode != nil { 2569 - 2570 - if len("exitCode") > 1000000 { 2571 - return xerrors.Errorf("Value in field \"exitCode\" was too long") 2572 - } 2573 - 2574 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("exitCode"))); err != nil { 2575 - return err 2576 - } 2577 - if _, err := cw.WriteString(string("exitCode")); err != nil { 2522 + for _, v := range t.Workflows { 2523 + if err := v.MarshalCBOR(cw); err != nil { 2578 2524 return err 2579 2525 } 2580 2526 2581 - if t.ExitCode == nil { 2582 - if _, err := cw.Write(cbg.CborNull); err != nil { 2583 - return err 2584 - } 2585 - } else { 2586 - if *t.ExitCode >= 0 { 2587 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.ExitCode)); err != nil { 2588 - return err 2589 - } 2590 - } else { 2591 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.ExitCode-1)); err != nil { 2592 - return err 2593 - } 2594 - } 2595 - } 2596 - 2597 2527 } 2598 2528 2599 - // t.Pipeline (string) (string) 2600 - if len("pipeline") > 1000000 { 2601 - return xerrors.Errorf("Value in field \"pipeline\" was too long") 2602 - } 2603 - 2604 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pipeline"))); err != nil { 2605 - return err 2606 - } 2607 - if _, err := cw.WriteString(string("pipeline")); err != nil { 2608 - return err 2609 - } 2610 - 2611 - if len(t.Pipeline) > 1000000 { 2612 - return xerrors.Errorf("Value in field t.Pipeline was too long") 2613 - } 2614 - 2615 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pipeline))); err != nil { 2616 - return err 2617 - } 2618 - if _, err := cw.WriteString(string(t.Pipeline)); err != nil { 2619 - return err 2620 - } 2621 - 2622 - // t.Workflow (string) (string) 2623 - if len("workflow") > 1000000 { 2624 - return xerrors.Errorf("Value in field \"workflow\" was too long") 2625 - } 2626 - 2627 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("workflow"))); err != nil { 2628 - return err 2629 - } 2630 - if _, err := cw.WriteString(string("workflow")); err != nil { 2631 - return err 2632 - } 2633 - 2634 - if len(t.Workflow) > 1000000 { 2635 - return xerrors.Errorf("Value in field t.Workflow was too long") 2636 - } 2637 - 2638 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Workflow))); err != nil { 2639 - return err 2640 - } 2641 - if _, err := cw.WriteString(string(t.Workflow)); err != nil { 2642 - return err 2643 - } 2644 - 2645 - // t.CreatedAt (string) (string) 2646 - if len("createdAt") > 1000000 { 2647 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 2529 + // t.TriggerMetadata (tangled.Pipeline_TriggerMetadata) (struct) 2530 + if len("triggerMetadata") > 1000000 { 2531 + return xerrors.Errorf("Value in field \"triggerMetadata\" was too long") 2648 2532 } 2649 2533 2650 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2534 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("triggerMetadata"))); err != nil { 2651 2535 return err 2652 2536 } 2653 - if _, err := cw.WriteString(string("createdAt")); err != nil { 2537 + if _, err := cw.WriteString(string("triggerMetadata")); err != nil { 2654 2538 return err 2655 2539 } 2656 2540 2657 - if len(t.CreatedAt) > 1000000 { 2658 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 2659 - } 2660 - 2661 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 2662 - return err 2663 - } 2664 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 2541 + if err := t.TriggerMetadata.MarshalCBOR(cw); err != nil { 2665 2542 return err 2666 2543 } 2667 2544 return nil 2668 2545 } 2669 2546 2670 - func (t *PipelineStatus) UnmarshalCBOR(r io.Reader) (err error) { 2671 - *t = PipelineStatus{} 2547 + func (t *Pipeline) UnmarshalCBOR(r io.Reader) (err error) { 2548 + *t = Pipeline{} 2672 2549 2673 2550 cr := cbg.NewCborReader(r) 2674 2551 ··· 2687 2564 } 2688 2565 2689 2566 if extra > cbg.MaxLength { 2690 - return fmt.Errorf("PipelineStatus: map struct too large (%d)", extra) 2567 + return fmt.Errorf("Pipeline: map struct too large (%d)", extra) 2691 2568 } 2692 2569 2693 2570 n := extra 2694 2571 2695 - nameBuf := make([]byte, 9) 2572 + nameBuf := make([]byte, 15) 2696 2573 for i := uint64(0); i < n; i++ { 2697 2574 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2698 2575 if err != nil { ··· 2719 2596 2720 2597 t.LexiconTypeID = string(sval) 2721 2598 } 2722 - // t.Error (string) (string) 2723 - case "error": 2724 - 2725 - { 2726 - b, err := cr.ReadByte() 2727 - if err != nil { 2728 - return err 2729 - } 2730 - if b != cbg.CborNull[0] { 2731 - if err := cr.UnreadByte(); err != nil { 2732 - return err 2733 - } 2599 + // t.Workflows ([]*tangled.Pipeline_Workflow) (slice) 2600 + case "workflows": 2734 2601 2735 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2736 - if err != nil { 2737 - return err 2738 - } 2602 + maj, extra, err = cr.ReadHeader() 2603 + if err != nil { 2604 + return err 2605 + } 2739 2606 2740 - t.Error = (*string)(&sval) 2741 - } 2607 + if extra > 8192 { 2608 + return fmt.Errorf("t.Workflows: array too large (%d)", extra) 2742 2609 } 2743 - // t.Status (string) (string) 2744 - case "status": 2745 2610 2746 - { 2747 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2748 - if err != nil { 2749 - return err 2750 - } 2611 + if maj != cbg.MajArray { 2612 + return fmt.Errorf("expected cbor array") 2613 + } 2751 2614 2752 - t.Status = string(sval) 2615 + if extra > 0 { 2616 + t.Workflows = make([]*Pipeline_Workflow, extra) 2753 2617 } 2754 - // t.ExitCode (int64) (int64) 2755 - case "exitCode": 2756 - { 2757 2618 2758 - b, err := cr.ReadByte() 2759 - if err != nil { 2760 - return err 2761 - } 2762 - if b != cbg.CborNull[0] { 2763 - if err := cr.UnreadByte(); err != nil { 2764 - return err 2765 - } 2766 - maj, extra, err := cr.ReadHeader() 2767 - if err != nil { 2768 - return err 2769 - } 2770 - var extraI int64 2771 - switch maj { 2772 - case cbg.MajUnsignedInt: 2773 - extraI = int64(extra) 2774 - if extraI < 0 { 2775 - return fmt.Errorf("int64 positive overflow") 2619 + for i := 0; i < int(extra); i++ { 2620 + { 2621 + var maj byte 2622 + var extra uint64 2623 + var err error 2624 + _ = maj 2625 + _ = extra 2626 + _ = err 2627 + 2628 + { 2629 + 2630 + b, err := cr.ReadByte() 2631 + if err != nil { 2632 + return err 2776 2633 } 2777 - case cbg.MajNegativeInt: 2778 - extraI = int64(extra) 2779 - if extraI < 0 { 2780 - return fmt.Errorf("int64 negative overflow") 2634 + if b != cbg.CborNull[0] { 2635 + if err := cr.UnreadByte(); err != nil { 2636 + return err 2637 + } 2638 + t.Workflows[i] = new(Pipeline_Workflow) 2639 + if err := t.Workflows[i].UnmarshalCBOR(cr); err != nil { 2640 + return xerrors.Errorf("unmarshaling t.Workflows[i] pointer: %w", err) 2641 + } 2781 2642 } 2782 - extraI = -1 - extraI 2783 - default: 2784 - return fmt.Errorf("wrong type for int64 field: %d", maj) 2643 + 2785 2644 } 2786 2645 2787 - t.ExitCode = (*int64)(&extraI) 2788 2646 } 2789 2647 } 2790 - // t.Pipeline (string) (string) 2791 - case "pipeline": 2648 + // t.TriggerMetadata (tangled.Pipeline_TriggerMetadata) (struct) 2649 + case "triggerMetadata": 2792 2650 2793 2651 { 2794 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2795 - if err != nil { 2796 - return err 2797 - } 2798 2652 2799 - t.Pipeline = string(sval) 2800 - } 2801 - // t.Workflow (string) (string) 2802 - case "workflow": 2803 - 2804 - { 2805 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2653 + b, err := cr.ReadByte() 2806 2654 if err != nil { 2807 2655 return err 2808 2656 } 2809 - 2810 - t.Workflow = string(sval) 2811 - } 2812 - // t.CreatedAt (string) (string) 2813 - case "createdAt": 2814 - 2815 - { 2816 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2817 - if err != nil { 2818 - return err 2657 + if b != cbg.CborNull[0] { 2658 + if err := cr.UnreadByte(); err != nil { 2659 + return err 2660 + } 2661 + t.TriggerMetadata = new(Pipeline_TriggerMetadata) 2662 + if err := t.TriggerMetadata.UnmarshalCBOR(cr); err != nil { 2663 + return xerrors.Errorf("unmarshaling t.TriggerMetadata pointer: %w", err) 2664 + } 2819 2665 } 2820 2666 2821 - t.CreatedAt = string(sval) 2822 2667 } 2823 2668 2824 2669 default: ··· 3013 2858 3014 2859 return nil 3015 2860 } 3016 - func (t *Pipeline_Dependency) MarshalCBOR(w io.Writer) error { 3017 - if t == nil { 3018 - _, err := w.Write(cbg.CborNull) 3019 - return err 3020 - } 3021 - 3022 - cw := cbg.NewCborWriter(w) 3023 - 3024 - if _, err := cw.Write([]byte{162}); err != nil { 3025 - return err 3026 - } 3027 - 3028 - // t.Packages ([]string) (slice) 3029 - if len("packages") > 1000000 { 3030 - return xerrors.Errorf("Value in field \"packages\" was too long") 3031 - } 3032 - 3033 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("packages"))); err != nil { 3034 - return err 3035 - } 3036 - if _, err := cw.WriteString(string("packages")); err != nil { 3037 - return err 3038 - } 3039 - 3040 - if len(t.Packages) > 8192 { 3041 - return xerrors.Errorf("Slice value in field t.Packages was too long") 3042 - } 3043 - 3044 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Packages))); err != nil { 3045 - return err 3046 - } 3047 - for _, v := range t.Packages { 3048 - if len(v) > 1000000 { 3049 - return xerrors.Errorf("Value in field v was too long") 3050 - } 3051 - 3052 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 3053 - return err 3054 - } 3055 - if _, err := cw.WriteString(string(v)); err != nil { 3056 - return err 3057 - } 3058 - 3059 - } 3060 - 3061 - // t.Registry (string) (string) 3062 - if len("registry") > 1000000 { 3063 - return xerrors.Errorf("Value in field \"registry\" was too long") 3064 - } 3065 - 3066 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("registry"))); err != nil { 3067 - return err 3068 - } 3069 - if _, err := cw.WriteString(string("registry")); err != nil { 3070 - return err 3071 - } 3072 - 3073 - if len(t.Registry) > 1000000 { 3074 - return xerrors.Errorf("Value in field t.Registry was too long") 3075 - } 3076 - 3077 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Registry))); err != nil { 3078 - return err 3079 - } 3080 - if _, err := cw.WriteString(string(t.Registry)); err != nil { 3081 - return err 3082 - } 3083 - return nil 3084 - } 3085 - 3086 - func (t *Pipeline_Dependency) UnmarshalCBOR(r io.Reader) (err error) { 3087 - *t = Pipeline_Dependency{} 3088 - 3089 - cr := cbg.NewCborReader(r) 3090 - 3091 - maj, extra, err := cr.ReadHeader() 3092 - if err != nil { 3093 - return err 3094 - } 3095 - defer func() { 3096 - if err == io.EOF { 3097 - err = io.ErrUnexpectedEOF 3098 - } 3099 - }() 3100 - 3101 - if maj != cbg.MajMap { 3102 - return fmt.Errorf("cbor input should be of type map") 3103 - } 3104 - 3105 - if extra > cbg.MaxLength { 3106 - return fmt.Errorf("Pipeline_Dependency: map struct too large (%d)", extra) 3107 - } 3108 - 3109 - n := extra 3110 - 3111 - nameBuf := make([]byte, 8) 3112 - for i := uint64(0); i < n; i++ { 3113 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 3114 - if err != nil { 3115 - return err 3116 - } 3117 - 3118 - if !ok { 3119 - // Field doesn't exist on this type, so ignore it 3120 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 3121 - return err 3122 - } 3123 - continue 3124 - } 3125 - 3126 - switch string(nameBuf[:nameLen]) { 3127 - // t.Packages ([]string) (slice) 3128 - case "packages": 3129 - 3130 - maj, extra, err = cr.ReadHeader() 3131 - if err != nil { 3132 - return err 3133 - } 3134 - 3135 - if extra > 8192 { 3136 - return fmt.Errorf("t.Packages: array too large (%d)", extra) 3137 - } 3138 - 3139 - if maj != cbg.MajArray { 3140 - return fmt.Errorf("expected cbor array") 3141 - } 3142 - 3143 - if extra > 0 { 3144 - t.Packages = make([]string, extra) 3145 - } 3146 - 3147 - for i := 0; i < int(extra); i++ { 3148 - { 3149 - var maj byte 3150 - var extra uint64 3151 - var err error 3152 - _ = maj 3153 - _ = extra 3154 - _ = err 3155 - 3156 - { 3157 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 3158 - if err != nil { 3159 - return err 3160 - } 3161 - 3162 - t.Packages[i] = string(sval) 3163 - } 3164 - 3165 - } 3166 - } 3167 - // t.Registry (string) (string) 3168 - case "registry": 3169 - 3170 - { 3171 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 3172 - if err != nil { 3173 - return err 3174 - } 3175 - 3176 - t.Registry = string(sval) 3177 - } 3178 - 3179 - default: 3180 - // Field doesn't exist on this type, so ignore it 3181 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 3182 - return err 3183 - } 3184 - } 3185 - } 3186 - 3187 - return nil 3188 - } 3189 2861 func (t *Pipeline_ManualTriggerData) MarshalCBOR(w io.Writer) error { 3190 2862 if t == nil { 3191 2863 _, err := w.Write(cbg.CborNull) ··· 3839 3511 3840 3512 return nil 3841 3513 } 3842 - func (t *Pipeline_Step) MarshalCBOR(w io.Writer) error { 3514 + func (t *PipelineStatus) MarshalCBOR(w io.Writer) error { 3843 3515 if t == nil { 3844 3516 _, err := w.Write(cbg.CborNull) 3845 3517 return err 3846 3518 } 3847 3519 3848 3520 cw := cbg.NewCborWriter(w) 3849 - fieldCount := 3 3521 + fieldCount := 7 3850 3522 3851 - if t.Environment == nil { 3523 + if t.Error == nil { 3524 + fieldCount-- 3525 + } 3526 + 3527 + if t.ExitCode == nil { 3852 3528 fieldCount-- 3853 3529 } 3854 3530 ··· 3856 3532 return err 3857 3533 } 3858 3534 3859 - // t.Name (string) (string) 3860 - if len("name") > 1000000 { 3861 - return xerrors.Errorf("Value in field \"name\" was too long") 3535 + // t.LexiconTypeID (string) (string) 3536 + if len("$type") > 1000000 { 3537 + return xerrors.Errorf("Value in field \"$type\" was too long") 3862 3538 } 3863 3539 3864 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { 3540 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 3865 3541 return err 3866 3542 } 3867 - if _, err := cw.WriteString(string("name")); err != nil { 3543 + if _, err := cw.WriteString(string("$type")); err != nil { 3868 3544 return err 3869 3545 } 3870 3546 3871 - if len(t.Name) > 1000000 { 3872 - return xerrors.Errorf("Value in field t.Name was too long") 3873 - } 3874 - 3875 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { 3547 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.pipeline.status"))); err != nil { 3876 3548 return err 3877 3549 } 3878 - if _, err := cw.WriteString(string(t.Name)); err != nil { 3550 + if _, err := cw.WriteString(string("sh.tangled.pipeline.status")); err != nil { 3879 3551 return err 3880 3552 } 3881 3553 3882 - // t.Command (string) (string) 3883 - if len("command") > 1000000 { 3884 - return xerrors.Errorf("Value in field \"command\" was too long") 3554 + // t.Error (string) (string) 3555 + if t.Error != nil { 3556 + 3557 + if len("error") > 1000000 { 3558 + return xerrors.Errorf("Value in field \"error\" was too long") 3559 + } 3560 + 3561 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("error"))); err != nil { 3562 + return err 3563 + } 3564 + if _, err := cw.WriteString(string("error")); err != nil { 3565 + return err 3566 + } 3567 + 3568 + if t.Error == nil { 3569 + if _, err := cw.Write(cbg.CborNull); err != nil { 3570 + return err 3571 + } 3572 + } else { 3573 + if len(*t.Error) > 1000000 { 3574 + return xerrors.Errorf("Value in field t.Error was too long") 3575 + } 3576 + 3577 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Error))); err != nil { 3578 + return err 3579 + } 3580 + if _, err := cw.WriteString(string(*t.Error)); err != nil { 3581 + return err 3582 + } 3583 + } 3885 3584 } 3886 3585 3887 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("command"))); err != nil { 3586 + // t.Status (string) (string) 3587 + if len("status") > 1000000 { 3588 + return xerrors.Errorf("Value in field \"status\" was too long") 3589 + } 3590 + 3591 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 3888 3592 return err 3889 3593 } 3890 - if _, err := cw.WriteString(string("command")); err != nil { 3594 + if _, err := cw.WriteString(string("status")); err != nil { 3891 3595 return err 3892 3596 } 3893 3597 3894 - if len(t.Command) > 1000000 { 3895 - return xerrors.Errorf("Value in field t.Command was too long") 3598 + if len(t.Status) > 1000000 { 3599 + return xerrors.Errorf("Value in field t.Status was too long") 3896 3600 } 3897 3601 3898 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Command))); err != nil { 3602 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 3899 3603 return err 3900 3604 } 3901 - if _, err := cw.WriteString(string(t.Command)); err != nil { 3605 + if _, err := cw.WriteString(string(t.Status)); err != nil { 3902 3606 return err 3903 3607 } 3904 3608 3905 - // t.Environment ([]*tangled.Pipeline_Pair) (slice) 3906 - if t.Environment != nil { 3609 + // t.ExitCode (int64) (int64) 3610 + if t.ExitCode != nil { 3907 3611 3908 - if len("environment") > 1000000 { 3909 - return xerrors.Errorf("Value in field \"environment\" was too long") 3612 + if len("exitCode") > 1000000 { 3613 + return xerrors.Errorf("Value in field \"exitCode\" was too long") 3910 3614 } 3911 3615 3912 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("environment"))); err != nil { 3616 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("exitCode"))); err != nil { 3913 3617 return err 3914 3618 } 3915 - if _, err := cw.WriteString(string("environment")); err != nil { 3619 + if _, err := cw.WriteString(string("exitCode")); err != nil { 3916 3620 return err 3917 3621 } 3918 3622 3919 - if len(t.Environment) > 8192 { 3920 - return xerrors.Errorf("Slice value in field t.Environment was too long") 3623 + if t.ExitCode == nil { 3624 + if _, err := cw.Write(cbg.CborNull); err != nil { 3625 + return err 3626 + } 3627 + } else { 3628 + if *t.ExitCode >= 0 { 3629 + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.ExitCode)); err != nil { 3630 + return err 3631 + } 3632 + } else { 3633 + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.ExitCode-1)); err != nil { 3634 + return err 3635 + } 3636 + } 3921 3637 } 3922 3638 3923 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Environment))); err != nil { 3924 - return err 3925 - } 3926 - for _, v := range t.Environment { 3927 - if err := v.MarshalCBOR(cw); err != nil { 3928 - return err 3929 - } 3639 + } 3640 + 3641 + // t.Pipeline (string) (string) 3642 + if len("pipeline") > 1000000 { 3643 + return xerrors.Errorf("Value in field \"pipeline\" was too long") 3644 + } 3645 + 3646 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pipeline"))); err != nil { 3647 + return err 3648 + } 3649 + if _, err := cw.WriteString(string("pipeline")); err != nil { 3650 + return err 3651 + } 3652 + 3653 + if len(t.Pipeline) > 1000000 { 3654 + return xerrors.Errorf("Value in field t.Pipeline was too long") 3655 + } 3656 + 3657 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pipeline))); err != nil { 3658 + return err 3659 + } 3660 + if _, err := cw.WriteString(string(t.Pipeline)); err != nil { 3661 + return err 3662 + } 3663 + 3664 + // t.Workflow (string) (string) 3665 + if len("workflow") > 1000000 { 3666 + return xerrors.Errorf("Value in field \"workflow\" was too long") 3667 + } 3668 + 3669 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("workflow"))); err != nil { 3670 + return err 3671 + } 3672 + if _, err := cw.WriteString(string("workflow")); err != nil { 3673 + return err 3674 + } 3675 + 3676 + if len(t.Workflow) > 1000000 { 3677 + return xerrors.Errorf("Value in field t.Workflow was too long") 3678 + } 3679 + 3680 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Workflow))); err != nil { 3681 + return err 3682 + } 3683 + if _, err := cw.WriteString(string(t.Workflow)); err != nil { 3684 + return err 3685 + } 3686 + 3687 + // t.CreatedAt (string) (string) 3688 + if len("createdAt") > 1000000 { 3689 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 3690 + } 3691 + 3692 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 3693 + return err 3694 + } 3695 + if _, err := cw.WriteString(string("createdAt")); err != nil { 3696 + return err 3697 + } 3698 + 3699 + if len(t.CreatedAt) > 1000000 { 3700 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 3701 + } 3930 3702 3931 - } 3703 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 3704 + return err 3705 + } 3706 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 3707 + return err 3932 3708 } 3933 3709 return nil 3934 3710 } 3935 3711 3936 - func (t *Pipeline_Step) UnmarshalCBOR(r io.Reader) (err error) { 3937 - *t = Pipeline_Step{} 3712 + func (t *PipelineStatus) UnmarshalCBOR(r io.Reader) (err error) { 3713 + *t = PipelineStatus{} 3938 3714 3939 3715 cr := cbg.NewCborReader(r) 3940 3716 ··· 3953 3729 } 3954 3730 3955 3731 if extra > cbg.MaxLength { 3956 - return fmt.Errorf("Pipeline_Step: map struct too large (%d)", extra) 3732 + return fmt.Errorf("PipelineStatus: map struct too large (%d)", extra) 3957 3733 } 3958 3734 3959 3735 n := extra 3960 3736 3961 - nameBuf := make([]byte, 11) 3737 + nameBuf := make([]byte, 9) 3962 3738 for i := uint64(0); i < n; i++ { 3963 3739 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 3964 3740 if err != nil { ··· 3974 3750 } 3975 3751 3976 3752 switch string(nameBuf[:nameLen]) { 3977 - // t.Name (string) (string) 3978 - case "name": 3753 + // t.LexiconTypeID (string) (string) 3754 + case "$type": 3979 3755 3980 3756 { 3981 3757 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 3983 3759 return err 3984 3760 } 3985 3761 3986 - t.Name = string(sval) 3762 + t.LexiconTypeID = string(sval) 3987 3763 } 3988 - // t.Command (string) (string) 3989 - case "command": 3764 + // t.Error (string) (string) 3765 + case "error": 3990 3766 3991 3767 { 3992 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 3768 + b, err := cr.ReadByte() 3993 3769 if err != nil { 3994 3770 return err 3995 3771 } 3772 + if b != cbg.CborNull[0] { 3773 + if err := cr.UnreadByte(); err != nil { 3774 + return err 3775 + } 3996 3776 3997 - t.Command = string(sval) 3998 - } 3999 - // t.Environment ([]*tangled.Pipeline_Pair) (slice) 4000 - case "environment": 3777 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3778 + if err != nil { 3779 + return err 3780 + } 4001 3781 4002 - maj, extra, err = cr.ReadHeader() 4003 - if err != nil { 4004 - return err 3782 + t.Error = (*string)(&sval) 3783 + } 4005 3784 } 3785 + // t.Status (string) (string) 3786 + case "status": 4006 3787 4007 - if extra > 8192 { 4008 - return fmt.Errorf("t.Environment: array too large (%d)", extra) 4009 - } 3788 + { 3789 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3790 + if err != nil { 3791 + return err 3792 + } 4010 3793 4011 - if maj != cbg.MajArray { 4012 - return fmt.Errorf("expected cbor array") 3794 + t.Status = string(sval) 4013 3795 } 3796 + // t.ExitCode (int64) (int64) 3797 + case "exitCode": 3798 + { 4014 3799 4015 - if extra > 0 { 4016 - t.Environment = make([]*Pipeline_Pair, extra) 3800 + b, err := cr.ReadByte() 3801 + if err != nil { 3802 + return err 3803 + } 3804 + if b != cbg.CborNull[0] { 3805 + if err := cr.UnreadByte(); err != nil { 3806 + return err 3807 + } 3808 + maj, extra, err := cr.ReadHeader() 3809 + if err != nil { 3810 + return err 3811 + } 3812 + var extraI int64 3813 + switch maj { 3814 + case cbg.MajUnsignedInt: 3815 + extraI = int64(extra) 3816 + if extraI < 0 { 3817 + return fmt.Errorf("int64 positive overflow") 3818 + } 3819 + case cbg.MajNegativeInt: 3820 + extraI = int64(extra) 3821 + if extraI < 0 { 3822 + return fmt.Errorf("int64 negative overflow") 3823 + } 3824 + extraI = -1 - extraI 3825 + default: 3826 + return fmt.Errorf("wrong type for int64 field: %d", maj) 3827 + } 3828 + 3829 + t.ExitCode = (*int64)(&extraI) 3830 + } 4017 3831 } 3832 + // t.Pipeline (string) (string) 3833 + case "pipeline": 4018 3834 4019 - for i := 0; i < int(extra); i++ { 4020 - { 4021 - var maj byte 4022 - var extra uint64 4023 - var err error 4024 - _ = maj 4025 - _ = extra 4026 - _ = err 3835 + { 3836 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3837 + if err != nil { 3838 + return err 3839 + } 4027 3840 4028 - { 3841 + t.Pipeline = string(sval) 3842 + } 3843 + // t.Workflow (string) (string) 3844 + case "workflow": 4029 3845 4030 - b, err := cr.ReadByte() 4031 - if err != nil { 4032 - return err 4033 - } 4034 - if b != cbg.CborNull[0] { 4035 - if err := cr.UnreadByte(); err != nil { 4036 - return err 4037 - } 4038 - t.Environment[i] = new(Pipeline_Pair) 4039 - if err := t.Environment[i].UnmarshalCBOR(cr); err != nil { 4040 - return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err) 4041 - } 4042 - } 3846 + { 3847 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3848 + if err != nil { 3849 + return err 3850 + } 4043 3851 4044 - } 3852 + t.Workflow = string(sval) 3853 + } 3854 + // t.CreatedAt (string) (string) 3855 + case "createdAt": 4045 3856 3857 + { 3858 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3859 + if err != nil { 3860 + return err 4046 3861 } 3862 + 3863 + t.CreatedAt = string(sval) 4047 3864 } 4048 3865 4049 3866 default: ··· 4532 4349 4533 4350 cw := cbg.NewCborWriter(w) 4534 4351 4535 - if _, err := cw.Write([]byte{165}); err != nil { 4352 + if _, err := cw.Write([]byte{164}); err != nil { 4353 + return err 4354 + } 4355 + 4356 + // t.Raw (string) (string) 4357 + if len("raw") > 1000000 { 4358 + return xerrors.Errorf("Value in field \"raw\" was too long") 4359 + } 4360 + 4361 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("raw"))); err != nil { 4362 + return err 4363 + } 4364 + if _, err := cw.WriteString(string("raw")); err != nil { 4365 + return err 4366 + } 4367 + 4368 + if len(t.Raw) > 1000000 { 4369 + return xerrors.Errorf("Value in field t.Raw was too long") 4370 + } 4371 + 4372 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Raw))); err != nil { 4373 + return err 4374 + } 4375 + if _, err := cw.WriteString(string(t.Raw)); err != nil { 4536 4376 return err 4537 4377 } 4538 4378 ··· 4575 4415 return err 4576 4416 } 4577 4417 4578 - // t.Steps ([]*tangled.Pipeline_Step) (slice) 4579 - if len("steps") > 1000000 { 4580 - return xerrors.Errorf("Value in field \"steps\" was too long") 4418 + // t.Engine (string) (string) 4419 + if len("engine") > 1000000 { 4420 + return xerrors.Errorf("Value in field \"engine\" was too long") 4581 4421 } 4582 4422 4583 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("steps"))); err != nil { 4423 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("engine"))); err != nil { 4584 4424 return err 4585 4425 } 4586 - if _, err := cw.WriteString(string("steps")); err != nil { 4426 + if _, err := cw.WriteString(string("engine")); err != nil { 4587 4427 return err 4588 4428 } 4589 4429 4590 - if len(t.Steps) > 8192 { 4591 - return xerrors.Errorf("Slice value in field t.Steps was too long") 4430 + if len(t.Engine) > 1000000 { 4431 + return xerrors.Errorf("Value in field t.Engine was too long") 4592 4432 } 4593 4433 4594 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Steps))); err != nil { 4434 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Engine))); err != nil { 4595 4435 return err 4596 4436 } 4597 - for _, v := range t.Steps { 4598 - if err := v.MarshalCBOR(cw); err != nil { 4599 - return err 4600 - } 4601 - 4602 - } 4603 - 4604 - // t.Environment ([]*tangled.Pipeline_Pair) (slice) 4605 - if len("environment") > 1000000 { 4606 - return xerrors.Errorf("Value in field \"environment\" was too long") 4607 - } 4608 - 4609 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("environment"))); err != nil { 4437 + if _, err := cw.WriteString(string(t.Engine)); err != nil { 4610 4438 return err 4611 4439 } 4612 - if _, err := cw.WriteString(string("environment")); err != nil { 4613 - return err 4614 - } 4615 - 4616 - if len(t.Environment) > 8192 { 4617 - return xerrors.Errorf("Slice value in field t.Environment was too long") 4618 - } 4619 - 4620 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Environment))); err != nil { 4621 - return err 4622 - } 4623 - for _, v := range t.Environment { 4624 - if err := v.MarshalCBOR(cw); err != nil { 4625 - return err 4626 - } 4627 - 4628 - } 4629 - 4630 - // t.Dependencies ([]*tangled.Pipeline_Dependency) (slice) 4631 - if len("dependencies") > 1000000 { 4632 - return xerrors.Errorf("Value in field \"dependencies\" was too long") 4633 - } 4634 - 4635 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dependencies"))); err != nil { 4636 - return err 4637 - } 4638 - if _, err := cw.WriteString(string("dependencies")); err != nil { 4639 - return err 4640 - } 4641 - 4642 - if len(t.Dependencies) > 8192 { 4643 - return xerrors.Errorf("Slice value in field t.Dependencies was too long") 4644 - } 4645 - 4646 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Dependencies))); err != nil { 4647 - return err 4648 - } 4649 - for _, v := range t.Dependencies { 4650 - if err := v.MarshalCBOR(cw); err != nil { 4651 - return err 4652 - } 4653 - 4654 - } 4655 4440 return nil 4656 4441 } 4657 4442 ··· 4680 4465 4681 4466 n := extra 4682 4467 4683 - nameBuf := make([]byte, 12) 4468 + nameBuf := make([]byte, 6) 4684 4469 for i := uint64(0); i < n; i++ { 4685 4470 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 4686 4471 if err != nil { ··· 4696 4481 } 4697 4482 4698 4483 switch string(nameBuf[:nameLen]) { 4699 - // t.Name (string) (string) 4484 + // t.Raw (string) (string) 4485 + case "raw": 4486 + 4487 + { 4488 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 4489 + if err != nil { 4490 + return err 4491 + } 4492 + 4493 + t.Raw = string(sval) 4494 + } 4495 + // t.Name (string) (string) 4700 4496 case "name": 4701 4497 4702 4498 { ··· 4727 4523 } 4728 4524 4729 4525 } 4730 - // t.Steps ([]*tangled.Pipeline_Step) (slice) 4731 - case "steps": 4732 - 4733 - maj, extra, err = cr.ReadHeader() 4734 - if err != nil { 4735 - return err 4736 - } 4737 - 4738 - if extra > 8192 { 4739 - return fmt.Errorf("t.Steps: array too large (%d)", extra) 4740 - } 4741 - 4742 - if maj != cbg.MajArray { 4743 - return fmt.Errorf("expected cbor array") 4744 - } 4745 - 4746 - if extra > 0 { 4747 - t.Steps = make([]*Pipeline_Step, extra) 4748 - } 4749 - 4750 - for i := 0; i < int(extra); i++ { 4751 - { 4752 - var maj byte 4753 - var extra uint64 4754 - var err error 4755 - _ = maj 4756 - _ = extra 4757 - _ = err 4758 - 4759 - { 4760 - 4761 - b, err := cr.ReadByte() 4762 - if err != nil { 4763 - return err 4764 - } 4765 - if b != cbg.CborNull[0] { 4766 - if err := cr.UnreadByte(); err != nil { 4767 - return err 4768 - } 4769 - t.Steps[i] = new(Pipeline_Step) 4770 - if err := t.Steps[i].UnmarshalCBOR(cr); err != nil { 4771 - return xerrors.Errorf("unmarshaling t.Steps[i] pointer: %w", err) 4772 - } 4773 - } 4774 - 4775 - } 4776 - 4777 - } 4778 - } 4779 - // t.Environment ([]*tangled.Pipeline_Pair) (slice) 4780 - case "environment": 4781 - 4782 - maj, extra, err = cr.ReadHeader() 4783 - if err != nil { 4784 - return err 4785 - } 4786 - 4787 - if extra > 8192 { 4788 - return fmt.Errorf("t.Environment: array too large (%d)", extra) 4789 - } 4790 - 4791 - if maj != cbg.MajArray { 4792 - return fmt.Errorf("expected cbor array") 4793 - } 4794 - 4795 - if extra > 0 { 4796 - t.Environment = make([]*Pipeline_Pair, extra) 4797 - } 4798 - 4799 - for i := 0; i < int(extra); i++ { 4800 - { 4801 - var maj byte 4802 - var extra uint64 4803 - var err error 4804 - _ = maj 4805 - _ = extra 4806 - _ = err 4807 - 4808 - { 4809 - 4810 - b, err := cr.ReadByte() 4811 - if err != nil { 4812 - return err 4813 - } 4814 - if b != cbg.CborNull[0] { 4815 - if err := cr.UnreadByte(); err != nil { 4816 - return err 4817 - } 4818 - t.Environment[i] = new(Pipeline_Pair) 4819 - if err := t.Environment[i].UnmarshalCBOR(cr); err != nil { 4820 - return xerrors.Errorf("unmarshaling t.Environment[i] pointer: %w", err) 4821 - } 4822 - } 4823 - 4824 - } 4526 + // t.Engine (string) (string) 4527 + case "engine": 4825 4528 4529 + { 4530 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 4531 + if err != nil { 4532 + return err 4826 4533 } 4827 - } 4828 - // t.Dependencies ([]*tangled.Pipeline_Dependency) (slice) 4829 - case "dependencies": 4830 4534 4831 - maj, extra, err = cr.ReadHeader() 4832 - if err != nil { 4833 - return err 4834 - } 4835 - 4836 - if extra > 8192 { 4837 - return fmt.Errorf("t.Dependencies: array too large (%d)", extra) 4838 - } 4839 - 4840 - if maj != cbg.MajArray { 4841 - return fmt.Errorf("expected cbor array") 4842 - } 4843 - 4844 - if extra > 0 { 4845 - t.Dependencies = make([]*Pipeline_Dependency, extra) 4846 - } 4847 - 4848 - for i := 0; i < int(extra); i++ { 4849 - { 4850 - var maj byte 4851 - var extra uint64 4852 - var err error 4853 - _ = maj 4854 - _ = extra 4855 - _ = err 4856 - 4857 - { 4858 - 4859 - b, err := cr.ReadByte() 4860 - if err != nil { 4861 - return err 4862 - } 4863 - if b != cbg.CborNull[0] { 4864 - if err := cr.UnreadByte(); err != nil { 4865 - return err 4866 - } 4867 - t.Dependencies[i] = new(Pipeline_Dependency) 4868 - if err := t.Dependencies[i].UnmarshalCBOR(cr); err != nil { 4869 - return xerrors.Errorf("unmarshaling t.Dependencies[i] pointer: %w", err) 4870 - } 4871 - } 4872 - 4873 - } 4874 - 4875 - } 4535 + t.Engine = string(sval) 4876 4536 } 4877 4537 4878 4538 default: ··· 4885 4545 4886 4546 return nil 4887 4547 } 4888 - func (t *Pipeline) MarshalCBOR(w io.Writer) error { 4548 + func (t *PublicKey) MarshalCBOR(w io.Writer) error { 4889 4549 if t == nil { 4890 4550 _, err := w.Write(cbg.CborNull) 4891 4551 return err ··· 4893 4553 4894 4554 cw := cbg.NewCborWriter(w) 4895 4555 4896 - if _, err := cw.Write([]byte{163}); err != nil { 4556 + if _, err := cw.Write([]byte{164}); err != nil { 4897 4557 return err 4898 4558 } 4899 4559 4900 - // t.LexiconTypeID (string) (string) 4901 - if len("$type") > 1000000 { 4902 - return xerrors.Errorf("Value in field \"$type\" was too long") 4560 + // t.Key (string) (string) 4561 + if len("key") > 1000000 { 4562 + return xerrors.Errorf("Value in field \"key\" was too long") 4903 4563 } 4904 4564 4905 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 4565 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil { 4906 4566 return err 4907 4567 } 4908 - if _, err := cw.WriteString(string("$type")); err != nil { 4568 + if _, err := cw.WriteString(string("key")); err != nil { 4909 4569 return err 4910 4570 } 4911 4571 4912 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.pipeline"))); err != nil { 4572 + if len(t.Key) > 1000000 { 4573 + return xerrors.Errorf("Value in field t.Key was too long") 4574 + } 4575 + 4576 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil { 4913 4577 return err 4914 4578 } 4915 - if _, err := cw.WriteString(string("sh.tangled.pipeline")); err != nil { 4579 + if _, err := cw.WriteString(string(t.Key)); err != nil { 4916 4580 return err 4917 4581 } 4918 4582 4919 - // t.Workflows ([]*tangled.Pipeline_Workflow) (slice) 4920 - if len("workflows") > 1000000 { 4921 - return xerrors.Errorf("Value in field \"workflows\" was too long") 4583 + // t.Name (string) (string) 4584 + if len("name") > 1000000 { 4585 + return xerrors.Errorf("Value in field \"name\" was too long") 4922 4586 } 4923 4587 4924 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("workflows"))); err != nil { 4588 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { 4925 4589 return err 4926 4590 } 4927 - if _, err := cw.WriteString(string("workflows")); err != nil { 4591 + if _, err := cw.WriteString(string("name")); err != nil { 4928 4592 return err 4929 4593 } 4930 4594 4931 - if len(t.Workflows) > 8192 { 4932 - return xerrors.Errorf("Slice value in field t.Workflows was too long") 4595 + if len(t.Name) > 1000000 { 4596 + return xerrors.Errorf("Value in field t.Name was too long") 4933 4597 } 4934 4598 4935 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Workflows))); err != nil { 4599 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { 4936 4600 return err 4937 4601 } 4938 - for _, v := range t.Workflows { 4939 - if err := v.MarshalCBOR(cw); err != nil { 4940 - return err 4941 - } 4602 + if _, err := cw.WriteString(string(t.Name)); err != nil { 4603 + return err 4604 + } 4942 4605 4606 + // t.LexiconTypeID (string) (string) 4607 + if len("$type") > 1000000 { 4608 + return xerrors.Errorf("Value in field \"$type\" was too long") 4943 4609 } 4944 4610 4945 - // t.TriggerMetadata (tangled.Pipeline_TriggerMetadata) (struct) 4946 - if len("triggerMetadata") > 1000000 { 4947 - return xerrors.Errorf("Value in field \"triggerMetadata\" was too long") 4611 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 4612 + return err 4613 + } 4614 + if _, err := cw.WriteString(string("$type")); err != nil { 4615 + return err 4948 4616 } 4949 4617 4950 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("triggerMetadata"))); err != nil { 4618 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.publicKey"))); err != nil { 4951 4619 return err 4952 4620 } 4953 - if _, err := cw.WriteString(string("triggerMetadata")); err != nil { 4621 + if _, err := cw.WriteString(string("sh.tangled.publicKey")); err != nil { 4954 4622 return err 4955 4623 } 4956 4624 4957 - if err := t.TriggerMetadata.MarshalCBOR(cw); err != nil { 4625 + // t.CreatedAt (string) (string) 4626 + if len("createdAt") > 1000000 { 4627 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 4628 + } 4629 + 4630 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 4631 + return err 4632 + } 4633 + if _, err := cw.WriteString(string("createdAt")); err != nil { 4634 + return err 4635 + } 4636 + 4637 + if len(t.CreatedAt) > 1000000 { 4638 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 4639 + } 4640 + 4641 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 4642 + return err 4643 + } 4644 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 4958 4645 return err 4959 4646 } 4960 4647 return nil 4961 4648 } 4962 4649 4963 - func (t *Pipeline) UnmarshalCBOR(r io.Reader) (err error) { 4964 - *t = Pipeline{} 4650 + func (t *PublicKey) UnmarshalCBOR(r io.Reader) (err error) { 4651 + *t = PublicKey{} 4965 4652 4966 4653 cr := cbg.NewCborReader(r) 4967 4654 ··· 4980 4667 } 4981 4668 4982 4669 if extra > cbg.MaxLength { 4983 - return fmt.Errorf("Pipeline: map struct too large (%d)", extra) 4670 + return fmt.Errorf("PublicKey: map struct too large (%d)", extra) 4984 4671 } 4985 4672 4986 4673 n := extra 4987 4674 4988 - nameBuf := make([]byte, 15) 4675 + nameBuf := make([]byte, 9) 4989 4676 for i := uint64(0); i < n; i++ { 4990 4677 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 4991 4678 if err != nil { ··· 5001 4688 } 5002 4689 5003 4690 switch string(nameBuf[:nameLen]) { 5004 - // t.LexiconTypeID (string) (string) 5005 - case "$type": 4691 + // t.Key (string) (string) 4692 + case "key": 5006 4693 5007 4694 { 5008 4695 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 5010 4697 return err 5011 4698 } 5012 4699 5013 - t.LexiconTypeID = string(sval) 5014 - } 5015 - // t.Workflows ([]*tangled.Pipeline_Workflow) (slice) 5016 - case "workflows": 5017 - 5018 - maj, extra, err = cr.ReadHeader() 5019 - if err != nil { 5020 - return err 5021 - } 5022 - 5023 - if extra > 8192 { 5024 - return fmt.Errorf("t.Workflows: array too large (%d)", extra) 4700 + t.Key = string(sval) 5025 4701 } 4702 + // t.Name (string) (string) 4703 + case "name": 5026 4704 5027 - if maj != cbg.MajArray { 5028 - return fmt.Errorf("expected cbor array") 5029 - } 4705 + { 4706 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 4707 + if err != nil { 4708 + return err 4709 + } 5030 4710 5031 - if extra > 0 { 5032 - t.Workflows = make([]*Pipeline_Workflow, extra) 4711 + t.Name = string(sval) 5033 4712 } 5034 - 5035 - for i := 0; i < int(extra); i++ { 5036 - { 5037 - var maj byte 5038 - var extra uint64 5039 - var err error 5040 - _ = maj 5041 - _ = extra 5042 - _ = err 5043 - 5044 - { 5045 - 5046 - b, err := cr.ReadByte() 5047 - if err != nil { 5048 - return err 5049 - } 5050 - if b != cbg.CborNull[0] { 5051 - if err := cr.UnreadByte(); err != nil { 5052 - return err 5053 - } 5054 - t.Workflows[i] = new(Pipeline_Workflow) 5055 - if err := t.Workflows[i].UnmarshalCBOR(cr); err != nil { 5056 - return xerrors.Errorf("unmarshaling t.Workflows[i] pointer: %w", err) 5057 - } 5058 - } 5059 - 5060 - } 4713 + // t.LexiconTypeID (string) (string) 4714 + case "$type": 5061 4715 4716 + { 4717 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 4718 + if err != nil { 4719 + return err 5062 4720 } 4721 + 4722 + t.LexiconTypeID = string(sval) 5063 4723 } 5064 - // t.TriggerMetadata (tangled.Pipeline_TriggerMetadata) (struct) 5065 - case "triggerMetadata": 4724 + // t.CreatedAt (string) (string) 4725 + case "createdAt": 5066 4726 5067 4727 { 5068 - 5069 - b, err := cr.ReadByte() 4728 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5070 4729 if err != nil { 5071 4730 return err 5072 4731 } 5073 - if b != cbg.CborNull[0] { 5074 - if err := cr.UnreadByte(); err != nil { 5075 - return err 5076 - } 5077 - t.TriggerMetadata = new(Pipeline_TriggerMetadata) 5078 - if err := t.TriggerMetadata.UnmarshalCBOR(cr); err != nil { 5079 - return xerrors.Errorf("unmarshaling t.TriggerMetadata pointer: %w", err) 5080 - } 5081 - } 5082 4732 4733 + t.CreatedAt = string(sval) 5083 4734 } 5084 4735 5085 4736 default: ··· 5092 4743 5093 4744 return nil 5094 4745 } 5095 - func (t *PublicKey) MarshalCBOR(w io.Writer) error { 4746 + func (t *Repo) MarshalCBOR(w io.Writer) error { 5096 4747 if t == nil { 5097 4748 _, err := w.Write(cbg.CborNull) 5098 4749 return err 5099 4750 } 5100 4751 5101 4752 cw := cbg.NewCborWriter(w) 4753 + fieldCount := 8 5102 4754 5103 - if _, err := cw.Write([]byte{164}); err != nil { 4755 + if t.Description == nil { 4756 + fieldCount-- 4757 + } 4758 + 4759 + if t.Source == nil { 4760 + fieldCount-- 4761 + } 4762 + 4763 + if t.Spindle == nil { 4764 + fieldCount-- 4765 + } 4766 + 4767 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 5104 4768 return err 5105 4769 } 5106 4770 5107 - // t.Key (string) (string) 5108 - if len("key") > 1000000 { 5109 - return xerrors.Errorf("Value in field \"key\" was too long") 4771 + // t.Knot (string) (string) 4772 + if len("knot") > 1000000 { 4773 + return xerrors.Errorf("Value in field \"knot\" was too long") 5110 4774 } 5111 4775 5112 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil { 4776 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("knot"))); err != nil { 5113 4777 return err 5114 4778 } 5115 - if _, err := cw.WriteString(string("key")); err != nil { 4779 + if _, err := cw.WriteString(string("knot")); err != nil { 5116 4780 return err 5117 4781 } 5118 4782 5119 - if len(t.Key) > 1000000 { 5120 - return xerrors.Errorf("Value in field t.Key was too long") 4783 + if len(t.Knot) > 1000000 { 4784 + return xerrors.Errorf("Value in field t.Knot was too long") 5121 4785 } 5122 4786 5123 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil { 4787 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Knot))); err != nil { 5124 4788 return err 5125 4789 } 5126 - if _, err := cw.WriteString(string(t.Key)); err != nil { 4790 + if _, err := cw.WriteString(string(t.Knot)); err != nil { 5127 4791 return err 5128 4792 } 5129 4793 ··· 5162 4826 return err 5163 4827 } 5164 4828 5165 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.publicKey"))); err != nil { 4829 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo"))); err != nil { 5166 4830 return err 5167 4831 } 5168 - if _, err := cw.WriteString(string("sh.tangled.publicKey")); err != nil { 4832 + if _, err := cw.WriteString(string("sh.tangled.repo")); err != nil { 5169 4833 return err 5170 4834 } 5171 4835 4836 + // t.Owner (string) (string) 4837 + if len("owner") > 1000000 { 4838 + return xerrors.Errorf("Value in field \"owner\" was too long") 4839 + } 4840 + 4841 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 4842 + return err 4843 + } 4844 + if _, err := cw.WriteString(string("owner")); err != nil { 4845 + return err 4846 + } 4847 + 4848 + if len(t.Owner) > 1000000 { 4849 + return xerrors.Errorf("Value in field t.Owner was too long") 4850 + } 4851 + 4852 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil { 4853 + return err 4854 + } 4855 + if _, err := cw.WriteString(string(t.Owner)); err != nil { 4856 + return err 4857 + } 4858 + 4859 + // t.Source (string) (string) 4860 + if t.Source != nil { 4861 + 4862 + if len("source") > 1000000 { 4863 + return xerrors.Errorf("Value in field \"source\" was too long") 4864 + } 4865 + 4866 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { 4867 + return err 4868 + } 4869 + if _, err := cw.WriteString(string("source")); err != nil { 4870 + return err 4871 + } 4872 + 4873 + if t.Source == nil { 4874 + if _, err := cw.Write(cbg.CborNull); err != nil { 4875 + return err 4876 + } 4877 + } else { 4878 + if len(*t.Source) > 1000000 { 4879 + return xerrors.Errorf("Value in field t.Source was too long") 4880 + } 4881 + 4882 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Source))); err != nil { 4883 + return err 4884 + } 4885 + if _, err := cw.WriteString(string(*t.Source)); err != nil { 4886 + return err 4887 + } 4888 + } 4889 + } 4890 + 4891 + // t.Spindle (string) (string) 4892 + if t.Spindle != nil { 4893 + 4894 + if len("spindle") > 1000000 { 4895 + return xerrors.Errorf("Value in field \"spindle\" was too long") 4896 + } 4897 + 4898 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("spindle"))); err != nil { 4899 + return err 4900 + } 4901 + if _, err := cw.WriteString(string("spindle")); err != nil { 4902 + return err 4903 + } 4904 + 4905 + if t.Spindle == nil { 4906 + if _, err := cw.Write(cbg.CborNull); err != nil { 4907 + return err 4908 + } 4909 + } else { 4910 + if len(*t.Spindle) > 1000000 { 4911 + return xerrors.Errorf("Value in field t.Spindle was too long") 4912 + } 4913 + 4914 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Spindle))); err != nil { 4915 + return err 4916 + } 4917 + if _, err := cw.WriteString(string(*t.Spindle)); err != nil { 4918 + return err 4919 + } 4920 + } 4921 + } 4922 + 5172 4923 // t.CreatedAt (string) (string) 5173 4924 if len("createdAt") > 1000000 { 5174 4925 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 5191 4942 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 5192 4943 return err 5193 4944 } 4945 + 4946 + // t.Description (string) (string) 4947 + if t.Description != nil { 4948 + 4949 + if len("description") > 1000000 { 4950 + return xerrors.Errorf("Value in field \"description\" was too long") 4951 + } 4952 + 4953 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { 4954 + return err 4955 + } 4956 + if _, err := cw.WriteString(string("description")); err != nil { 4957 + return err 4958 + } 4959 + 4960 + if t.Description == nil { 4961 + if _, err := cw.Write(cbg.CborNull); err != nil { 4962 + return err 4963 + } 4964 + } else { 4965 + if len(*t.Description) > 1000000 { 4966 + return xerrors.Errorf("Value in field t.Description was too long") 4967 + } 4968 + 4969 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { 4970 + return err 4971 + } 4972 + if _, err := cw.WriteString(string(*t.Description)); err != nil { 4973 + return err 4974 + } 4975 + } 4976 + } 5194 4977 return nil 5195 4978 } 5196 4979 5197 - func (t *PublicKey) UnmarshalCBOR(r io.Reader) (err error) { 5198 - *t = PublicKey{} 4980 + func (t *Repo) UnmarshalCBOR(r io.Reader) (err error) { 4981 + *t = Repo{} 5199 4982 5200 4983 cr := cbg.NewCborReader(r) 5201 4984 ··· 5214 4997 } 5215 4998 5216 4999 if extra > cbg.MaxLength { 5217 - return fmt.Errorf("PublicKey: map struct too large (%d)", extra) 5000 + return fmt.Errorf("Repo: map struct too large (%d)", extra) 5218 5001 } 5219 5002 5220 5003 n := extra 5221 5004 5222 - nameBuf := make([]byte, 9) 5005 + nameBuf := make([]byte, 11) 5223 5006 for i := uint64(0); i < n; i++ { 5224 5007 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 5225 5008 if err != nil { ··· 5235 5018 } 5236 5019 5237 5020 switch string(nameBuf[:nameLen]) { 5238 - // t.Key (string) (string) 5239 - case "key": 5021 + // t.Knot (string) (string) 5022 + case "knot": 5240 5023 5241 5024 { 5242 5025 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 5244 5027 return err 5245 5028 } 5246 5029 5247 - t.Key = string(sval) 5030 + t.Knot = string(sval) 5248 5031 } 5249 5032 // t.Name (string) (string) 5250 5033 case "name": ··· 5268 5051 5269 5052 t.LexiconTypeID = string(sval) 5270 5053 } 5054 + // t.Owner (string) (string) 5055 + case "owner": 5056 + 5057 + { 5058 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5059 + if err != nil { 5060 + return err 5061 + } 5062 + 5063 + t.Owner = string(sval) 5064 + } 5065 + // t.Source (string) (string) 5066 + case "source": 5067 + 5068 + { 5069 + b, err := cr.ReadByte() 5070 + if err != nil { 5071 + return err 5072 + } 5073 + if b != cbg.CborNull[0] { 5074 + if err := cr.UnreadByte(); err != nil { 5075 + return err 5076 + } 5077 + 5078 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5079 + if err != nil { 5080 + return err 5081 + } 5082 + 5083 + t.Source = (*string)(&sval) 5084 + } 5085 + } 5086 + // t.Spindle (string) (string) 5087 + case "spindle": 5088 + 5089 + { 5090 + b, err := cr.ReadByte() 5091 + if err != nil { 5092 + return err 5093 + } 5094 + if b != cbg.CborNull[0] { 5095 + if err := cr.UnreadByte(); err != nil { 5096 + return err 5097 + } 5098 + 5099 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5100 + if err != nil { 5101 + return err 5102 + } 5103 + 5104 + t.Spindle = (*string)(&sval) 5105 + } 5106 + } 5271 5107 // t.CreatedAt (string) (string) 5272 5108 case "createdAt": 5273 5109 ··· 5278 5114 } 5279 5115 5280 5116 t.CreatedAt = string(sval) 5117 + } 5118 + // t.Description (string) (string) 5119 + case "description": 5120 + 5121 + { 5122 + b, err := cr.ReadByte() 5123 + if err != nil { 5124 + return err 5125 + } 5126 + if b != cbg.CborNull[0] { 5127 + if err := cr.UnreadByte(); err != nil { 5128 + return err 5129 + } 5130 + 5131 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5132 + if err != nil { 5133 + return err 5134 + } 5135 + 5136 + t.Description = (*string)(&sval) 5137 + } 5281 5138 } 5282 5139 5283 5140 default: ··· 5778 5635 5779 5636 return nil 5780 5637 } 5638 + func (t *RepoIssue) MarshalCBOR(w io.Writer) error { 5639 + if t == nil { 5640 + _, err := w.Write(cbg.CborNull) 5641 + return err 5642 + } 5643 + 5644 + cw := cbg.NewCborWriter(w) 5645 + fieldCount := 7 5646 + 5647 + if t.Body == nil { 5648 + fieldCount-- 5649 + } 5650 + 5651 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 5652 + return err 5653 + } 5654 + 5655 + // t.Body (string) (string) 5656 + if t.Body != nil { 5657 + 5658 + if len("body") > 1000000 { 5659 + return xerrors.Errorf("Value in field \"body\" was too long") 5660 + } 5661 + 5662 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 5663 + return err 5664 + } 5665 + if _, err := cw.WriteString(string("body")); err != nil { 5666 + return err 5667 + } 5668 + 5669 + if t.Body == nil { 5670 + if _, err := cw.Write(cbg.CborNull); err != nil { 5671 + return err 5672 + } 5673 + } else { 5674 + if len(*t.Body) > 1000000 { 5675 + return xerrors.Errorf("Value in field t.Body was too long") 5676 + } 5677 + 5678 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { 5679 + return err 5680 + } 5681 + if _, err := cw.WriteString(string(*t.Body)); err != nil { 5682 + return err 5683 + } 5684 + } 5685 + } 5686 + 5687 + // t.Repo (string) (string) 5688 + if len("repo") > 1000000 { 5689 + return xerrors.Errorf("Value in field \"repo\" was too long") 5690 + } 5691 + 5692 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 5693 + return err 5694 + } 5695 + if _, err := cw.WriteString(string("repo")); err != nil { 5696 + return err 5697 + } 5698 + 5699 + if len(t.Repo) > 1000000 { 5700 + return xerrors.Errorf("Value in field t.Repo was too long") 5701 + } 5702 + 5703 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 5704 + return err 5705 + } 5706 + if _, err := cw.WriteString(string(t.Repo)); err != nil { 5707 + return err 5708 + } 5709 + 5710 + // t.LexiconTypeID (string) (string) 5711 + if len("$type") > 1000000 { 5712 + return xerrors.Errorf("Value in field \"$type\" was too long") 5713 + } 5714 + 5715 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 5716 + return err 5717 + } 5718 + if _, err := cw.WriteString(string("$type")); err != nil { 5719 + return err 5720 + } 5721 + 5722 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.issue"))); err != nil { 5723 + return err 5724 + } 5725 + if _, err := cw.WriteString(string("sh.tangled.repo.issue")); err != nil { 5726 + return err 5727 + } 5728 + 5729 + // t.Owner (string) (string) 5730 + if len("owner") > 1000000 { 5731 + return xerrors.Errorf("Value in field \"owner\" was too long") 5732 + } 5733 + 5734 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 5735 + return err 5736 + } 5737 + if _, err := cw.WriteString(string("owner")); err != nil { 5738 + return err 5739 + } 5740 + 5741 + if len(t.Owner) > 1000000 { 5742 + return xerrors.Errorf("Value in field t.Owner was too long") 5743 + } 5744 + 5745 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil { 5746 + return err 5747 + } 5748 + if _, err := cw.WriteString(string(t.Owner)); err != nil { 5749 + return err 5750 + } 5751 + 5752 + // t.Title (string) (string) 5753 + if len("title") > 1000000 { 5754 + return xerrors.Errorf("Value in field \"title\" was too long") 5755 + } 5756 + 5757 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil { 5758 + return err 5759 + } 5760 + if _, err := cw.WriteString(string("title")); err != nil { 5761 + return err 5762 + } 5763 + 5764 + if len(t.Title) > 1000000 { 5765 + return xerrors.Errorf("Value in field t.Title was too long") 5766 + } 5767 + 5768 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil { 5769 + return err 5770 + } 5771 + if _, err := cw.WriteString(string(t.Title)); err != nil { 5772 + return err 5773 + } 5774 + 5775 + // t.IssueId (int64) (int64) 5776 + if len("issueId") > 1000000 { 5777 + return xerrors.Errorf("Value in field \"issueId\" was too long") 5778 + } 5779 + 5780 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issueId"))); err != nil { 5781 + return err 5782 + } 5783 + if _, err := cw.WriteString(string("issueId")); err != nil { 5784 + return err 5785 + } 5786 + 5787 + if t.IssueId >= 0 { 5788 + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.IssueId)); err != nil { 5789 + return err 5790 + } 5791 + } else { 5792 + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.IssueId-1)); err != nil { 5793 + return err 5794 + } 5795 + } 5796 + 5797 + // t.CreatedAt (string) (string) 5798 + if len("createdAt") > 1000000 { 5799 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 5800 + } 5801 + 5802 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 5803 + return err 5804 + } 5805 + if _, err := cw.WriteString(string("createdAt")); err != nil { 5806 + return err 5807 + } 5808 + 5809 + if len(t.CreatedAt) > 1000000 { 5810 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 5811 + } 5812 + 5813 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 5814 + return err 5815 + } 5816 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 5817 + return err 5818 + } 5819 + return nil 5820 + } 5821 + 5822 + func (t *RepoIssue) UnmarshalCBOR(r io.Reader) (err error) { 5823 + *t = RepoIssue{} 5824 + 5825 + cr := cbg.NewCborReader(r) 5826 + 5827 + maj, extra, err := cr.ReadHeader() 5828 + if err != nil { 5829 + return err 5830 + } 5831 + defer func() { 5832 + if err == io.EOF { 5833 + err = io.ErrUnexpectedEOF 5834 + } 5835 + }() 5836 + 5837 + if maj != cbg.MajMap { 5838 + return fmt.Errorf("cbor input should be of type map") 5839 + } 5840 + 5841 + if extra > cbg.MaxLength { 5842 + return fmt.Errorf("RepoIssue: map struct too large (%d)", extra) 5843 + } 5844 + 5845 + n := extra 5846 + 5847 + nameBuf := make([]byte, 9) 5848 + for i := uint64(0); i < n; i++ { 5849 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 5850 + if err != nil { 5851 + return err 5852 + } 5853 + 5854 + if !ok { 5855 + // Field doesn't exist on this type, so ignore it 5856 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 5857 + return err 5858 + } 5859 + continue 5860 + } 5861 + 5862 + switch string(nameBuf[:nameLen]) { 5863 + // t.Body (string) (string) 5864 + case "body": 5865 + 5866 + { 5867 + b, err := cr.ReadByte() 5868 + if err != nil { 5869 + return err 5870 + } 5871 + if b != cbg.CborNull[0] { 5872 + if err := cr.UnreadByte(); err != nil { 5873 + return err 5874 + } 5875 + 5876 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5877 + if err != nil { 5878 + return err 5879 + } 5880 + 5881 + t.Body = (*string)(&sval) 5882 + } 5883 + } 5884 + // t.Repo (string) (string) 5885 + case "repo": 5886 + 5887 + { 5888 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5889 + if err != nil { 5890 + return err 5891 + } 5892 + 5893 + t.Repo = string(sval) 5894 + } 5895 + // t.LexiconTypeID (string) (string) 5896 + case "$type": 5897 + 5898 + { 5899 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5900 + if err != nil { 5901 + return err 5902 + } 5903 + 5904 + t.LexiconTypeID = string(sval) 5905 + } 5906 + // t.Owner (string) (string) 5907 + case "owner": 5908 + 5909 + { 5910 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5911 + if err != nil { 5912 + return err 5913 + } 5914 + 5915 + t.Owner = string(sval) 5916 + } 5917 + // t.Title (string) (string) 5918 + case "title": 5919 + 5920 + { 5921 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5922 + if err != nil { 5923 + return err 5924 + } 5925 + 5926 + t.Title = string(sval) 5927 + } 5928 + // t.IssueId (int64) (int64) 5929 + case "issueId": 5930 + { 5931 + maj, extra, err := cr.ReadHeader() 5932 + if err != nil { 5933 + return err 5934 + } 5935 + var extraI int64 5936 + switch maj { 5937 + case cbg.MajUnsignedInt: 5938 + extraI = int64(extra) 5939 + if extraI < 0 { 5940 + return fmt.Errorf("int64 positive overflow") 5941 + } 5942 + case cbg.MajNegativeInt: 5943 + extraI = int64(extra) 5944 + if extraI < 0 { 5945 + return fmt.Errorf("int64 negative overflow") 5946 + } 5947 + extraI = -1 - extraI 5948 + default: 5949 + return fmt.Errorf("wrong type for int64 field: %d", maj) 5950 + } 5951 + 5952 + t.IssueId = int64(extraI) 5953 + } 5954 + // t.CreatedAt (string) (string) 5955 + case "createdAt": 5956 + 5957 + { 5958 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5959 + if err != nil { 5960 + return err 5961 + } 5962 + 5963 + t.CreatedAt = string(sval) 5964 + } 5965 + 5966 + default: 5967 + // Field doesn't exist on this type, so ignore it 5968 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 5969 + return err 5970 + } 5971 + } 5972 + } 5973 + 5974 + return nil 5975 + } 5781 5976 func (t *RepoIssueComment) MarshalCBOR(w io.Writer) error { 5782 5977 if t == nil { 5783 5978 _, err := w.Write(cbg.CborNull) ··· 6327 6522 6328 6523 return nil 6329 6524 } 6330 - func (t *RepoIssue) MarshalCBOR(w io.Writer) error { 6525 + func (t *RepoPull) MarshalCBOR(w io.Writer) error { 6331 6526 if t == nil { 6332 6527 _, err := w.Write(cbg.CborNull) 6333 6528 return err 6334 6529 } 6335 6530 6336 6531 cw := cbg.NewCborWriter(w) 6337 - fieldCount := 7 6532 + fieldCount := 9 6338 6533 6339 6534 if t.Body == nil { 6535 + fieldCount-- 6536 + } 6537 + 6538 + if t.Source == nil { 6340 6539 fieldCount-- 6341 6540 } 6342 6541 ··· 6376 6575 } 6377 6576 } 6378 6577 6379 - // t.Repo (string) (string) 6380 - if len("repo") > 1000000 { 6381 - return xerrors.Errorf("Value in field \"repo\" was too long") 6382 - } 6383 - 6384 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 6385 - return err 6386 - } 6387 - if _, err := cw.WriteString(string("repo")); err != nil { 6388 - return err 6389 - } 6390 - 6391 - if len(t.Repo) > 1000000 { 6392 - return xerrors.Errorf("Value in field t.Repo was too long") 6393 - } 6394 - 6395 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 6396 - return err 6397 - } 6398 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 6399 - return err 6400 - } 6401 - 6402 6578 // t.LexiconTypeID (string) (string) 6403 6579 if len("$type") > 1000000 { 6404 6580 return xerrors.Errorf("Value in field \"$type\" was too long") ··· 6411 6587 return err 6412 6588 } 6413 6589 6414 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.issue"))); err != nil { 6590 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull"))); err != nil { 6415 6591 return err 6416 6592 } 6417 - if _, err := cw.WriteString(string("sh.tangled.repo.issue")); err != nil { 6593 + if _, err := cw.WriteString(string("sh.tangled.repo.pull")); err != nil { 6418 6594 return err 6419 6595 } 6420 6596 6421 - // t.Owner (string) (string) 6422 - if len("owner") > 1000000 { 6423 - return xerrors.Errorf("Value in field \"owner\" was too long") 6597 + // t.Patch (string) (string) 6598 + if len("patch") > 1000000 { 6599 + return xerrors.Errorf("Value in field \"patch\" was too long") 6424 6600 } 6425 6601 6426 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 6602 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 6427 6603 return err 6428 6604 } 6429 - if _, err := cw.WriteString(string("owner")); err != nil { 6605 + if _, err := cw.WriteString(string("patch")); err != nil { 6430 6606 return err 6431 6607 } 6432 6608 6433 - if len(t.Owner) > 1000000 { 6434 - return xerrors.Errorf("Value in field t.Owner was too long") 6609 + if len(t.Patch) > 1000000 { 6610 + return xerrors.Errorf("Value in field t.Patch was too long") 6435 6611 } 6436 6612 6437 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil { 6613 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 6438 6614 return err 6439 6615 } 6440 - if _, err := cw.WriteString(string(t.Owner)); err != nil { 6616 + if _, err := cw.WriteString(string(t.Patch)); err != nil { 6441 6617 return err 6442 6618 } 6443 6619 ··· 6464 6640 return err 6465 6641 } 6466 6642 6467 - // t.IssueId (int64) (int64) 6468 - if len("issueId") > 1000000 { 6469 - return xerrors.Errorf("Value in field \"issueId\" was too long") 6643 + // t.PullId (int64) (int64) 6644 + if len("pullId") > 1000000 { 6645 + return xerrors.Errorf("Value in field \"pullId\" was too long") 6470 6646 } 6471 6647 6472 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issueId"))); err != nil { 6648 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pullId"))); err != nil { 6473 6649 return err 6474 6650 } 6475 - if _, err := cw.WriteString(string("issueId")); err != nil { 6651 + if _, err := cw.WriteString(string("pullId")); err != nil { 6476 6652 return err 6477 6653 } 6478 6654 6479 - if t.IssueId >= 0 { 6480 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.IssueId)); err != nil { 6655 + if t.PullId >= 0 { 6656 + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.PullId)); err != nil { 6481 6657 return err 6482 6658 } 6483 6659 } else { 6484 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.IssueId-1)); err != nil { 6660 + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.PullId-1)); err != nil { 6661 + return err 6662 + } 6663 + } 6664 + 6665 + // t.Source (tangled.RepoPull_Source) (struct) 6666 + if t.Source != nil { 6667 + 6668 + if len("source") > 1000000 { 6669 + return xerrors.Errorf("Value in field \"source\" was too long") 6670 + } 6671 + 6672 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { 6673 + return err 6674 + } 6675 + if _, err := cw.WriteString(string("source")); err != nil { 6676 + return err 6677 + } 6678 + 6679 + if err := t.Source.MarshalCBOR(cw); err != nil { 6485 6680 return err 6486 6681 } 6487 6682 } ··· 6508 6703 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 6509 6704 return err 6510 6705 } 6706 + 6707 + // t.TargetRepo (string) (string) 6708 + if len("targetRepo") > 1000000 { 6709 + return xerrors.Errorf("Value in field \"targetRepo\" was too long") 6710 + } 6711 + 6712 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetRepo"))); err != nil { 6713 + return err 6714 + } 6715 + if _, err := cw.WriteString(string("targetRepo")); err != nil { 6716 + return err 6717 + } 6718 + 6719 + if len(t.TargetRepo) > 1000000 { 6720 + return xerrors.Errorf("Value in field t.TargetRepo was too long") 6721 + } 6722 + 6723 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetRepo))); err != nil { 6724 + return err 6725 + } 6726 + if _, err := cw.WriteString(string(t.TargetRepo)); err != nil { 6727 + return err 6728 + } 6729 + 6730 + // t.TargetBranch (string) (string) 6731 + if len("targetBranch") > 1000000 { 6732 + return xerrors.Errorf("Value in field \"targetBranch\" was too long") 6733 + } 6734 + 6735 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetBranch"))); err != nil { 6736 + return err 6737 + } 6738 + if _, err := cw.WriteString(string("targetBranch")); err != nil { 6739 + return err 6740 + } 6741 + 6742 + if len(t.TargetBranch) > 1000000 { 6743 + return xerrors.Errorf("Value in field t.TargetBranch was too long") 6744 + } 6745 + 6746 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetBranch))); err != nil { 6747 + return err 6748 + } 6749 + if _, err := cw.WriteString(string(t.TargetBranch)); err != nil { 6750 + return err 6751 + } 6511 6752 return nil 6512 6753 } 6513 6754 6514 - func (t *RepoIssue) UnmarshalCBOR(r io.Reader) (err error) { 6515 - *t = RepoIssue{} 6755 + func (t *RepoPull) UnmarshalCBOR(r io.Reader) (err error) { 6756 + *t = RepoPull{} 6516 6757 6517 6758 cr := cbg.NewCborReader(r) 6518 6759 ··· 6531 6772 } 6532 6773 6533 6774 if extra > cbg.MaxLength { 6534 - return fmt.Errorf("RepoIssue: map struct too large (%d)", extra) 6775 + return fmt.Errorf("RepoPull: map struct too large (%d)", extra) 6535 6776 } 6536 6777 6537 6778 n := extra 6538 6779 6539 - nameBuf := make([]byte, 9) 6780 + nameBuf := make([]byte, 12) 6540 6781 for i := uint64(0); i < n; i++ { 6541 6782 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 6542 6783 if err != nil { ··· 6572 6813 6573 6814 t.Body = (*string)(&sval) 6574 6815 } 6575 - } 6576 - // t.Repo (string) (string) 6577 - case "repo": 6578 - 6579 - { 6580 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 6581 - if err != nil { 6582 - return err 6583 - } 6584 - 6585 - t.Repo = string(sval) 6586 6816 } 6587 6817 // t.LexiconTypeID (string) (string) 6588 6818 case "$type": ··· 6595 6825 6596 6826 t.LexiconTypeID = string(sval) 6597 6827 } 6598 - // t.Owner (string) (string) 6599 - case "owner": 6828 + // t.Patch (string) (string) 6829 + case "patch": 6600 6830 6601 6831 { 6602 6832 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 6604 6834 return err 6605 6835 } 6606 6836 6607 - t.Owner = string(sval) 6837 + t.Patch = string(sval) 6608 6838 } 6609 6839 // t.Title (string) (string) 6610 6840 case "title": ··· 6617 6847 6618 6848 t.Title = string(sval) 6619 6849 } 6620 - // t.IssueId (int64) (int64) 6621 - case "issueId": 6850 + // t.PullId (int64) (int64) 6851 + case "pullId": 6622 6852 { 6623 6853 maj, extra, err := cr.ReadHeader() 6624 6854 if err != nil { ··· 6641 6871 return fmt.Errorf("wrong type for int64 field: %d", maj) 6642 6872 } 6643 6873 6644 - t.IssueId = int64(extraI) 6874 + t.PullId = int64(extraI) 6875 + } 6876 + // t.Source (tangled.RepoPull_Source) (struct) 6877 + case "source": 6878 + 6879 + { 6880 + 6881 + b, err := cr.ReadByte() 6882 + if err != nil { 6883 + return err 6884 + } 6885 + if b != cbg.CborNull[0] { 6886 + if err := cr.UnreadByte(); err != nil { 6887 + return err 6888 + } 6889 + t.Source = new(RepoPull_Source) 6890 + if err := t.Source.UnmarshalCBOR(cr); err != nil { 6891 + return xerrors.Errorf("unmarshaling t.Source pointer: %w", err) 6892 + } 6893 + } 6894 + 6645 6895 } 6646 6896 // t.CreatedAt (string) (string) 6647 6897 case "createdAt": ··· 6653 6903 } 6654 6904 6655 6905 t.CreatedAt = string(sval) 6906 + } 6907 + // t.TargetRepo (string) (string) 6908 + case "targetRepo": 6909 + 6910 + { 6911 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6912 + if err != nil { 6913 + return err 6914 + } 6915 + 6916 + t.TargetRepo = string(sval) 6917 + } 6918 + // t.TargetBranch (string) (string) 6919 + case "targetBranch": 6920 + 6921 + { 6922 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6923 + if err != nil { 6924 + return err 6925 + } 6926 + 6927 + t.TargetBranch = string(sval) 6656 6928 } 6657 6929 6658 6930 default: ··· 7050 7322 7051 7323 return nil 7052 7324 } 7053 - func (t *RepoPullStatus) MarshalCBOR(w io.Writer) error { 7054 - if t == nil { 7055 - _, err := w.Write(cbg.CborNull) 7056 - return err 7057 - } 7058 - 7059 - cw := cbg.NewCborWriter(w) 7060 - 7061 - if _, err := cw.Write([]byte{163}); err != nil { 7062 - return err 7063 - } 7064 - 7065 - // t.Pull (string) (string) 7066 - if len("pull") > 1000000 { 7067 - return xerrors.Errorf("Value in field \"pull\" was too long") 7068 - } 7069 - 7070 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { 7071 - return err 7072 - } 7073 - if _, err := cw.WriteString(string("pull")); err != nil { 7074 - return err 7075 - } 7076 - 7077 - if len(t.Pull) > 1000000 { 7078 - return xerrors.Errorf("Value in field t.Pull was too long") 7079 - } 7080 - 7081 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pull))); err != nil { 7082 - return err 7083 - } 7084 - if _, err := cw.WriteString(string(t.Pull)); err != nil { 7085 - return err 7086 - } 7087 - 7088 - // t.LexiconTypeID (string) (string) 7089 - if len("$type") > 1000000 { 7090 - return xerrors.Errorf("Value in field \"$type\" was too long") 7091 - } 7092 - 7093 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 7094 - return err 7095 - } 7096 - if _, err := cw.WriteString(string("$type")); err != nil { 7097 - return err 7098 - } 7099 - 7100 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull.status"))); err != nil { 7101 - return err 7102 - } 7103 - if _, err := cw.WriteString(string("sh.tangled.repo.pull.status")); err != nil { 7104 - return err 7105 - } 7106 - 7107 - // t.Status (string) (string) 7108 - if len("status") > 1000000 { 7109 - return xerrors.Errorf("Value in field \"status\" was too long") 7110 - } 7111 - 7112 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 7113 - return err 7114 - } 7115 - if _, err := cw.WriteString(string("status")); err != nil { 7116 - return err 7117 - } 7118 - 7119 - if len(t.Status) > 1000000 { 7120 - return xerrors.Errorf("Value in field t.Status was too long") 7121 - } 7122 - 7123 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 7124 - return err 7125 - } 7126 - if _, err := cw.WriteString(string(t.Status)); err != nil { 7127 - return err 7128 - } 7129 - return nil 7130 - } 7131 - 7132 - func (t *RepoPullStatus) UnmarshalCBOR(r io.Reader) (err error) { 7133 - *t = RepoPullStatus{} 7134 - 7135 - cr := cbg.NewCborReader(r) 7136 - 7137 - maj, extra, err := cr.ReadHeader() 7138 - if err != nil { 7139 - return err 7140 - } 7141 - defer func() { 7142 - if err == io.EOF { 7143 - err = io.ErrUnexpectedEOF 7144 - } 7145 - }() 7146 - 7147 - if maj != cbg.MajMap { 7148 - return fmt.Errorf("cbor input should be of type map") 7149 - } 7150 - 7151 - if extra > cbg.MaxLength { 7152 - return fmt.Errorf("RepoPullStatus: map struct too large (%d)", extra) 7153 - } 7154 - 7155 - n := extra 7156 - 7157 - nameBuf := make([]byte, 6) 7158 - for i := uint64(0); i < n; i++ { 7159 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7160 - if err != nil { 7161 - return err 7162 - } 7163 - 7164 - if !ok { 7165 - // Field doesn't exist on this type, so ignore it 7166 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 7167 - return err 7168 - } 7169 - continue 7170 - } 7171 - 7172 - switch string(nameBuf[:nameLen]) { 7173 - // t.Pull (string) (string) 7174 - case "pull": 7175 - 7176 - { 7177 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7178 - if err != nil { 7179 - return err 7180 - } 7181 - 7182 - t.Pull = string(sval) 7183 - } 7184 - // t.LexiconTypeID (string) (string) 7185 - case "$type": 7186 - 7187 - { 7188 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7189 - if err != nil { 7190 - return err 7191 - } 7192 - 7193 - t.LexiconTypeID = string(sval) 7194 - } 7195 - // t.Status (string) (string) 7196 - case "status": 7197 - 7198 - { 7199 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7200 - if err != nil { 7201 - return err 7202 - } 7203 - 7204 - t.Status = string(sval) 7205 - } 7206 - 7207 - default: 7208 - // Field doesn't exist on this type, so ignore it 7209 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 7210 - return err 7211 - } 7212 - } 7213 - } 7214 - 7215 - return nil 7216 - } 7217 7325 func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error { 7218 7326 if t == nil { 7219 7327 _, err := w.Write(cbg.CborNull) ··· 7406 7514 7407 7515 return nil 7408 7516 } 7409 - func (t *RepoPull) MarshalCBOR(w io.Writer) error { 7517 + func (t *RepoPullStatus) MarshalCBOR(w io.Writer) error { 7410 7518 if t == nil { 7411 7519 _, err := w.Write(cbg.CborNull) 7412 7520 return err 7413 7521 } 7414 7522 7415 7523 cw := cbg.NewCborWriter(w) 7416 - fieldCount := 9 7417 - 7418 - if t.Body == nil { 7419 - fieldCount-- 7420 - } 7421 7524 7422 - if t.Source == nil { 7423 - fieldCount-- 7424 - } 7425 - 7426 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 7525 + if _, err := cw.Write([]byte{163}); err != nil { 7427 7526 return err 7428 7527 } 7429 7528 7430 - // t.Body (string) (string) 7431 - if t.Body != nil { 7432 - 7433 - if len("body") > 1000000 { 7434 - return xerrors.Errorf("Value in field \"body\" was too long") 7435 - } 7436 - 7437 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 7438 - return err 7439 - } 7440 - if _, err := cw.WriteString(string("body")); err != nil { 7441 - return err 7442 - } 7443 - 7444 - if t.Body == nil { 7445 - if _, err := cw.Write(cbg.CborNull); err != nil { 7446 - return err 7447 - } 7448 - } else { 7449 - if len(*t.Body) > 1000000 { 7450 - return xerrors.Errorf("Value in field t.Body was too long") 7451 - } 7452 - 7453 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { 7454 - return err 7455 - } 7456 - if _, err := cw.WriteString(string(*t.Body)); err != nil { 7457 - return err 7458 - } 7459 - } 7460 - } 7461 - 7462 - // t.LexiconTypeID (string) (string) 7463 - if len("$type") > 1000000 { 7464 - return xerrors.Errorf("Value in field \"$type\" was too long") 7529 + // t.Pull (string) (string) 7530 + if len("pull") > 1000000 { 7531 + return xerrors.Errorf("Value in field \"pull\" was too long") 7465 7532 } 7466 7533 7467 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 7534 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { 7468 7535 return err 7469 7536 } 7470 - if _, err := cw.WriteString(string("$type")); err != nil { 7537 + if _, err := cw.WriteString(string("pull")); err != nil { 7471 7538 return err 7472 7539 } 7473 7540 7474 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull"))); err != nil { 7475 - return err 7476 - } 7477 - if _, err := cw.WriteString(string("sh.tangled.repo.pull")); err != nil { 7478 - return err 7479 - } 7480 - 7481 - // t.Patch (string) (string) 7482 - if len("patch") > 1000000 { 7483 - return xerrors.Errorf("Value in field \"patch\" was too long") 7541 + if len(t.Pull) > 1000000 { 7542 + return xerrors.Errorf("Value in field t.Pull was too long") 7484 7543 } 7485 7544 7486 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 7545 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pull))); err != nil { 7487 7546 return err 7488 7547 } 7489 - if _, err := cw.WriteString(string("patch")); err != nil { 7548 + if _, err := cw.WriteString(string(t.Pull)); err != nil { 7490 7549 return err 7491 7550 } 7492 7551 7493 - if len(t.Patch) > 1000000 { 7494 - return xerrors.Errorf("Value in field t.Patch was too long") 7552 + // t.LexiconTypeID (string) (string) 7553 + if len("$type") > 1000000 { 7554 + return xerrors.Errorf("Value in field \"$type\" was too long") 7495 7555 } 7496 7556 7497 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 7557 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 7498 7558 return err 7499 7559 } 7500 - if _, err := cw.WriteString(string(t.Patch)); err != nil { 7560 + if _, err := cw.WriteString(string("$type")); err != nil { 7501 7561 return err 7502 7562 } 7503 7563 7504 - // t.Title (string) (string) 7505 - if len("title") > 1000000 { 7506 - return xerrors.Errorf("Value in field \"title\" was too long") 7507 - } 7508 - 7509 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil { 7564 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull.status"))); err != nil { 7510 7565 return err 7511 7566 } 7512 - if _, err := cw.WriteString(string("title")); err != nil { 7567 + if _, err := cw.WriteString(string("sh.tangled.repo.pull.status")); err != nil { 7513 7568 return err 7514 7569 } 7515 7570 7516 - if len(t.Title) > 1000000 { 7517 - return xerrors.Errorf("Value in field t.Title was too long") 7571 + // t.Status (string) (string) 7572 + if len("status") > 1000000 { 7573 + return xerrors.Errorf("Value in field \"status\" was too long") 7518 7574 } 7519 7575 7520 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil { 7576 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 7521 7577 return err 7522 7578 } 7523 - if _, err := cw.WriteString(string(t.Title)); err != nil { 7579 + if _, err := cw.WriteString(string("status")); err != nil { 7524 7580 return err 7525 7581 } 7526 7582 7527 - // t.PullId (int64) (int64) 7528 - if len("pullId") > 1000000 { 7529 - return xerrors.Errorf("Value in field \"pullId\" was too long") 7583 + if len(t.Status) > 1000000 { 7584 + return xerrors.Errorf("Value in field t.Status was too long") 7530 7585 } 7531 7586 7532 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pullId"))); err != nil { 7587 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 7533 7588 return err 7534 7589 } 7535 - if _, err := cw.WriteString(string("pullId")); err != nil { 7536 - return err 7537 - } 7538 - 7539 - if t.PullId >= 0 { 7540 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.PullId)); err != nil { 7541 - return err 7542 - } 7543 - } else { 7544 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.PullId-1)); err != nil { 7545 - return err 7546 - } 7547 - } 7548 - 7549 - // t.Source (tangled.RepoPull_Source) (struct) 7550 - if t.Source != nil { 7551 - 7552 - if len("source") > 1000000 { 7553 - return xerrors.Errorf("Value in field \"source\" was too long") 7554 - } 7555 - 7556 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { 7557 - return err 7558 - } 7559 - if _, err := cw.WriteString(string("source")); err != nil { 7560 - return err 7561 - } 7562 - 7563 - if err := t.Source.MarshalCBOR(cw); err != nil { 7564 - return err 7565 - } 7566 - } 7567 - 7568 - // t.CreatedAt (string) (string) 7569 - if len("createdAt") > 1000000 { 7570 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 7571 - } 7572 - 7573 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 7574 - return err 7575 - } 7576 - if _, err := cw.WriteString(string("createdAt")); err != nil { 7577 - return err 7578 - } 7579 - 7580 - if len(t.CreatedAt) > 1000000 { 7581 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 7582 - } 7583 - 7584 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 7585 - return err 7586 - } 7587 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 7588 - return err 7589 - } 7590 - 7591 - // t.TargetRepo (string) (string) 7592 - if len("targetRepo") > 1000000 { 7593 - return xerrors.Errorf("Value in field \"targetRepo\" was too long") 7594 - } 7595 - 7596 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetRepo"))); err != nil { 7597 - return err 7598 - } 7599 - if _, err := cw.WriteString(string("targetRepo")); err != nil { 7600 - return err 7601 - } 7602 - 7603 - if len(t.TargetRepo) > 1000000 { 7604 - return xerrors.Errorf("Value in field t.TargetRepo was too long") 7605 - } 7606 - 7607 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetRepo))); err != nil { 7608 - return err 7609 - } 7610 - if _, err := cw.WriteString(string(t.TargetRepo)); err != nil { 7611 - return err 7612 - } 7613 - 7614 - // t.TargetBranch (string) (string) 7615 - if len("targetBranch") > 1000000 { 7616 - return xerrors.Errorf("Value in field \"targetBranch\" was too long") 7617 - } 7618 - 7619 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetBranch"))); err != nil { 7620 - return err 7621 - } 7622 - if _, err := cw.WriteString(string("targetBranch")); err != nil { 7623 - return err 7624 - } 7625 - 7626 - if len(t.TargetBranch) > 1000000 { 7627 - return xerrors.Errorf("Value in field t.TargetBranch was too long") 7628 - } 7629 - 7630 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetBranch))); err != nil { 7631 - return err 7632 - } 7633 - if _, err := cw.WriteString(string(t.TargetBranch)); err != nil { 7590 + if _, err := cw.WriteString(string(t.Status)); err != nil { 7634 7591 return err 7635 7592 } 7636 7593 return nil 7637 7594 } 7638 7595 7639 - func (t *RepoPull) UnmarshalCBOR(r io.Reader) (err error) { 7640 - *t = RepoPull{} 7596 + func (t *RepoPullStatus) UnmarshalCBOR(r io.Reader) (err error) { 7597 + *t = RepoPullStatus{} 7641 7598 7642 7599 cr := cbg.NewCborReader(r) 7643 7600 ··· 7656 7613 } 7657 7614 7658 7615 if extra > cbg.MaxLength { 7659 - return fmt.Errorf("RepoPull: map struct too large (%d)", extra) 7616 + return fmt.Errorf("RepoPullStatus: map struct too large (%d)", extra) 7660 7617 } 7661 7618 7662 7619 n := extra 7663 7620 7664 - nameBuf := make([]byte, 12) 7621 + nameBuf := make([]byte, 6) 7665 7622 for i := uint64(0); i < n; i++ { 7666 7623 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7667 7624 if err != nil { ··· 7677 7634 } 7678 7635 7679 7636 switch string(nameBuf[:nameLen]) { 7680 - // t.Body (string) (string) 7681 - case "body": 7637 + // t.Pull (string) (string) 7638 + case "pull": 7682 7639 7683 7640 { 7684 - b, err := cr.ReadByte() 7641 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7685 7642 if err != nil { 7686 7643 return err 7687 7644 } 7688 - if b != cbg.CborNull[0] { 7689 - if err := cr.UnreadByte(); err != nil { 7690 - return err 7691 - } 7692 7645 7693 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7694 - if err != nil { 7695 - return err 7696 - } 7697 - 7698 - t.Body = (*string)(&sval) 7699 - } 7646 + t.Pull = string(sval) 7700 7647 } 7701 7648 // t.LexiconTypeID (string) (string) 7702 7649 case "$type": ··· 7709 7656 7710 7657 t.LexiconTypeID = string(sval) 7711 7658 } 7712 - // t.Patch (string) (string) 7713 - case "patch": 7714 - 7715 - { 7716 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7717 - if err != nil { 7718 - return err 7719 - } 7720 - 7721 - t.Patch = string(sval) 7722 - } 7723 - // t.Title (string) (string) 7724 - case "title": 7725 - 7726 - { 7727 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7728 - if err != nil { 7729 - return err 7730 - } 7731 - 7732 - t.Title = string(sval) 7733 - } 7734 - // t.PullId (int64) (int64) 7735 - case "pullId": 7736 - { 7737 - maj, extra, err := cr.ReadHeader() 7738 - if err != nil { 7739 - return err 7740 - } 7741 - var extraI int64 7742 - switch maj { 7743 - case cbg.MajUnsignedInt: 7744 - extraI = int64(extra) 7745 - if extraI < 0 { 7746 - return fmt.Errorf("int64 positive overflow") 7747 - } 7748 - case cbg.MajNegativeInt: 7749 - extraI = int64(extra) 7750 - if extraI < 0 { 7751 - return fmt.Errorf("int64 negative overflow") 7752 - } 7753 - extraI = -1 - extraI 7754 - default: 7755 - return fmt.Errorf("wrong type for int64 field: %d", maj) 7756 - } 7757 - 7758 - t.PullId = int64(extraI) 7759 - } 7760 - // t.Source (tangled.RepoPull_Source) (struct) 7761 - case "source": 7762 - 7763 - { 7764 - 7765 - b, err := cr.ReadByte() 7766 - if err != nil { 7767 - return err 7768 - } 7769 - if b != cbg.CborNull[0] { 7770 - if err := cr.UnreadByte(); err != nil { 7771 - return err 7772 - } 7773 - t.Source = new(RepoPull_Source) 7774 - if err := t.Source.UnmarshalCBOR(cr); err != nil { 7775 - return xerrors.Errorf("unmarshaling t.Source pointer: %w", err) 7776 - } 7777 - } 7778 - 7779 - } 7780 - // t.CreatedAt (string) (string) 7781 - case "createdAt": 7782 - 7783 - { 7784 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7785 - if err != nil { 7786 - return err 7787 - } 7788 - 7789 - t.CreatedAt = string(sval) 7790 - } 7791 - // t.TargetRepo (string) (string) 7792 - case "targetRepo": 7793 - 7794 - { 7795 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7796 - if err != nil { 7797 - return err 7798 - } 7799 - 7800 - t.TargetRepo = string(sval) 7801 - } 7802 - // t.TargetBranch (string) (string) 7803 - case "targetBranch": 7659 + // t.Status (string) (string) 7660 + case "status": 7804 7661 7805 7662 { 7806 7663 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 7808 7665 return err 7809 7666 } 7810 7667 7811 - t.TargetBranch = string(sval) 7668 + t.Status = string(sval) 7812 7669 } 7813 7670 7814 7671 default: ··· 7821 7678 7822 7679 return nil 7823 7680 } 7824 - func (t *Repo) MarshalCBOR(w io.Writer) error { 7681 + func (t *Spindle) MarshalCBOR(w io.Writer) error { 7825 7682 if t == nil { 7826 7683 _, err := w.Write(cbg.CborNull) 7827 7684 return err 7828 7685 } 7829 7686 7830 7687 cw := cbg.NewCborWriter(w) 7831 - fieldCount := 8 7832 - 7833 - if t.Description == nil { 7834 - fieldCount-- 7835 - } 7836 7688 7837 - if t.Source == nil { 7838 - fieldCount-- 7839 - } 7840 - 7841 - if t.Spindle == nil { 7842 - fieldCount-- 7843 - } 7844 - 7845 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 7846 - return err 7847 - } 7848 - 7849 - // t.Knot (string) (string) 7850 - if len("knot") > 1000000 { 7851 - return xerrors.Errorf("Value in field \"knot\" was too long") 7852 - } 7853 - 7854 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("knot"))); err != nil { 7855 - return err 7856 - } 7857 - if _, err := cw.WriteString(string("knot")); err != nil { 7858 - return err 7859 - } 7860 - 7861 - if len(t.Knot) > 1000000 { 7862 - return xerrors.Errorf("Value in field t.Knot was too long") 7863 - } 7864 - 7865 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Knot))); err != nil { 7866 - return err 7867 - } 7868 - if _, err := cw.WriteString(string(t.Knot)); err != nil { 7869 - return err 7870 - } 7871 - 7872 - // t.Name (string) (string) 7873 - if len("name") > 1000000 { 7874 - return xerrors.Errorf("Value in field \"name\" was too long") 7875 - } 7876 - 7877 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { 7878 - return err 7879 - } 7880 - if _, err := cw.WriteString(string("name")); err != nil { 7881 - return err 7882 - } 7883 - 7884 - if len(t.Name) > 1000000 { 7885 - return xerrors.Errorf("Value in field t.Name was too long") 7886 - } 7887 - 7888 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { 7889 - return err 7890 - } 7891 - if _, err := cw.WriteString(string(t.Name)); err != nil { 7689 + if _, err := cw.Write([]byte{162}); err != nil { 7892 7690 return err 7893 7691 } 7894 7692 ··· 7904 7702 return err 7905 7703 } 7906 7704 7907 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo"))); err != nil { 7908 - return err 7909 - } 7910 - if _, err := cw.WriteString(string("sh.tangled.repo")); err != nil { 7911 - return err 7912 - } 7913 - 7914 - // t.Owner (string) (string) 7915 - if len("owner") > 1000000 { 7916 - return xerrors.Errorf("Value in field \"owner\" was too long") 7917 - } 7918 - 7919 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 7920 - return err 7921 - } 7922 - if _, err := cw.WriteString(string("owner")); err != nil { 7923 - return err 7924 - } 7925 - 7926 - if len(t.Owner) > 1000000 { 7927 - return xerrors.Errorf("Value in field t.Owner was too long") 7928 - } 7929 - 7930 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil { 7705 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.spindle"))); err != nil { 7931 7706 return err 7932 7707 } 7933 - if _, err := cw.WriteString(string(t.Owner)); err != nil { 7708 + if _, err := cw.WriteString(string("sh.tangled.spindle")); err != nil { 7934 7709 return err 7935 7710 } 7936 7711 7937 - // t.Source (string) (string) 7938 - if t.Source != nil { 7939 - 7940 - if len("source") > 1000000 { 7941 - return xerrors.Errorf("Value in field \"source\" was too long") 7942 - } 7943 - 7944 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { 7945 - return err 7946 - } 7947 - if _, err := cw.WriteString(string("source")); err != nil { 7948 - return err 7949 - } 7950 - 7951 - if t.Source == nil { 7952 - if _, err := cw.Write(cbg.CborNull); err != nil { 7953 - return err 7954 - } 7955 - } else { 7956 - if len(*t.Source) > 1000000 { 7957 - return xerrors.Errorf("Value in field t.Source was too long") 7958 - } 7959 - 7960 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Source))); err != nil { 7961 - return err 7962 - } 7963 - if _, err := cw.WriteString(string(*t.Source)); err != nil { 7964 - return err 7965 - } 7966 - } 7967 - } 7968 - 7969 - // t.Spindle (string) (string) 7970 - if t.Spindle != nil { 7971 - 7972 - if len("spindle") > 1000000 { 7973 - return xerrors.Errorf("Value in field \"spindle\" was too long") 7974 - } 7975 - 7976 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("spindle"))); err != nil { 7977 - return err 7978 - } 7979 - if _, err := cw.WriteString(string("spindle")); err != nil { 7980 - return err 7981 - } 7982 - 7983 - if t.Spindle == nil { 7984 - if _, err := cw.Write(cbg.CborNull); err != nil { 7985 - return err 7986 - } 7987 - } else { 7988 - if len(*t.Spindle) > 1000000 { 7989 - return xerrors.Errorf("Value in field t.Spindle was too long") 7990 - } 7991 - 7992 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Spindle))); err != nil { 7993 - return err 7994 - } 7995 - if _, err := cw.WriteString(string(*t.Spindle)); err != nil { 7996 - return err 7997 - } 7998 - } 7999 - } 8000 - 8001 7712 // t.CreatedAt (string) (string) 8002 7713 if len("createdAt") > 1000000 { 8003 7714 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 8020 7731 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 8021 7732 return err 8022 7733 } 8023 - 8024 - // t.Description (string) (string) 8025 - if t.Description != nil { 8026 - 8027 - if len("description") > 1000000 { 8028 - return xerrors.Errorf("Value in field \"description\" was too long") 8029 - } 8030 - 8031 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { 8032 - return err 8033 - } 8034 - if _, err := cw.WriteString(string("description")); err != nil { 8035 - return err 8036 - } 8037 - 8038 - if t.Description == nil { 8039 - if _, err := cw.Write(cbg.CborNull); err != nil { 8040 - return err 8041 - } 8042 - } else { 8043 - if len(*t.Description) > 1000000 { 8044 - return xerrors.Errorf("Value in field t.Description was too long") 8045 - } 8046 - 8047 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { 8048 - return err 8049 - } 8050 - if _, err := cw.WriteString(string(*t.Description)); err != nil { 8051 - return err 8052 - } 8053 - } 8054 - } 8055 7734 return nil 8056 7735 } 8057 7736 8058 - func (t *Repo) UnmarshalCBOR(r io.Reader) (err error) { 8059 - *t = Repo{} 7737 + func (t *Spindle) UnmarshalCBOR(r io.Reader) (err error) { 7738 + *t = Spindle{} 8060 7739 8061 7740 cr := cbg.NewCborReader(r) 8062 7741 ··· 8075 7754 } 8076 7755 8077 7756 if extra > cbg.MaxLength { 8078 - return fmt.Errorf("Repo: map struct too large (%d)", extra) 7757 + return fmt.Errorf("Spindle: map struct too large (%d)", extra) 8079 7758 } 8080 7759 8081 7760 n := extra 8082 7761 8083 - nameBuf := make([]byte, 11) 7762 + nameBuf := make([]byte, 9) 8084 7763 for i := uint64(0); i < n; i++ { 8085 7764 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 8086 7765 if err != nil { ··· 8096 7775 } 8097 7776 8098 7777 switch string(nameBuf[:nameLen]) { 8099 - // t.Knot (string) (string) 8100 - case "knot": 8101 - 8102 - { 8103 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8104 - if err != nil { 8105 - return err 8106 - } 8107 - 8108 - t.Knot = string(sval) 8109 - } 8110 - // t.Name (string) (string) 8111 - case "name": 8112 - 8113 - { 8114 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8115 - if err != nil { 8116 - return err 8117 - } 8118 - 8119 - t.Name = string(sval) 8120 - } 8121 - // t.LexiconTypeID (string) (string) 7778 + // t.LexiconTypeID (string) (string) 8122 7779 case "$type": 8123 7780 8124 7781 { ··· 8129 7786 8130 7787 t.LexiconTypeID = string(sval) 8131 7788 } 8132 - // t.Owner (string) (string) 8133 - case "owner": 8134 - 8135 - { 8136 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8137 - if err != nil { 8138 - return err 8139 - } 8140 - 8141 - t.Owner = string(sval) 8142 - } 8143 - // t.Source (string) (string) 8144 - case "source": 8145 - 8146 - { 8147 - b, err := cr.ReadByte() 8148 - if err != nil { 8149 - return err 8150 - } 8151 - if b != cbg.CborNull[0] { 8152 - if err := cr.UnreadByte(); err != nil { 8153 - return err 8154 - } 8155 - 8156 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8157 - if err != nil { 8158 - return err 8159 - } 8160 - 8161 - t.Source = (*string)(&sval) 8162 - } 8163 - } 8164 - // t.Spindle (string) (string) 8165 - case "spindle": 8166 - 8167 - { 8168 - b, err := cr.ReadByte() 8169 - if err != nil { 8170 - return err 8171 - } 8172 - if b != cbg.CborNull[0] { 8173 - if err := cr.UnreadByte(); err != nil { 8174 - return err 8175 - } 8176 - 8177 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8178 - if err != nil { 8179 - return err 8180 - } 8181 - 8182 - t.Spindle = (*string)(&sval) 8183 - } 8184 - } 8185 7789 // t.CreatedAt (string) (string) 8186 7790 case "createdAt": 8187 7791 ··· 8192 7796 } 8193 7797 8194 7798 t.CreatedAt = string(sval) 8195 - } 8196 - // t.Description (string) (string) 8197 - case "description": 8198 - 8199 - { 8200 - b, err := cr.ReadByte() 8201 - if err != nil { 8202 - return err 8203 - } 8204 - if b != cbg.CborNull[0] { 8205 - if err := cr.UnreadByte(); err != nil { 8206 - return err 8207 - } 8208 - 8209 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8210 - if err != nil { 8211 - return err 8212 - } 8213 - 8214 - t.Description = (*string)(&sval) 8215 - } 8216 7799 } 8217 7800 8218 7801 default: ··· 8400 7983 } 8401 7984 8402 7985 t.Instance = string(sval) 8403 - } 8404 - // t.CreatedAt (string) (string) 8405 - case "createdAt": 8406 - 8407 - { 8408 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8409 - if err != nil { 8410 - return err 8411 - } 8412 - 8413 - t.CreatedAt = string(sval) 8414 - } 8415 - 8416 - default: 8417 - // Field doesn't exist on this type, so ignore it 8418 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 8419 - return err 8420 - } 8421 - } 8422 - } 8423 - 8424 - return nil 8425 - } 8426 - func (t *Spindle) MarshalCBOR(w io.Writer) error { 8427 - if t == nil { 8428 - _, err := w.Write(cbg.CborNull) 8429 - return err 8430 - } 8431 - 8432 - cw := cbg.NewCborWriter(w) 8433 - 8434 - if _, err := cw.Write([]byte{162}); err != nil { 8435 - return err 8436 - } 8437 - 8438 - // t.LexiconTypeID (string) (string) 8439 - if len("$type") > 1000000 { 8440 - return xerrors.Errorf("Value in field \"$type\" was too long") 8441 - } 8442 - 8443 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 8444 - return err 8445 - } 8446 - if _, err := cw.WriteString(string("$type")); err != nil { 8447 - return err 8448 - } 8449 - 8450 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.spindle"))); err != nil { 8451 - return err 8452 - } 8453 - if _, err := cw.WriteString(string("sh.tangled.spindle")); err != nil { 8454 - return err 8455 - } 8456 - 8457 - // t.CreatedAt (string) (string) 8458 - if len("createdAt") > 1000000 { 8459 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 8460 - } 8461 - 8462 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 8463 - return err 8464 - } 8465 - if _, err := cw.WriteString(string("createdAt")); err != nil { 8466 - return err 8467 - } 8468 - 8469 - if len(t.CreatedAt) > 1000000 { 8470 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 8471 - } 8472 - 8473 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 8474 - return err 8475 - } 8476 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 8477 - return err 8478 - } 8479 - return nil 8480 - } 8481 - 8482 - func (t *Spindle) UnmarshalCBOR(r io.Reader) (err error) { 8483 - *t = Spindle{} 8484 - 8485 - cr := cbg.NewCborReader(r) 8486 - 8487 - maj, extra, err := cr.ReadHeader() 8488 - if err != nil { 8489 - return err 8490 - } 8491 - defer func() { 8492 - if err == io.EOF { 8493 - err = io.ErrUnexpectedEOF 8494 - } 8495 - }() 8496 - 8497 - if maj != cbg.MajMap { 8498 - return fmt.Errorf("cbor input should be of type map") 8499 - } 8500 - 8501 - if extra > cbg.MaxLength { 8502 - return fmt.Errorf("Spindle: map struct too large (%d)", extra) 8503 - } 8504 - 8505 - n := extra 8506 - 8507 - nameBuf := make([]byte, 9) 8508 - for i := uint64(0); i < n; i++ { 8509 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 8510 - if err != nil { 8511 - return err 8512 - } 8513 - 8514 - if !ok { 8515 - // Field doesn't exist on this type, so ignore it 8516 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 8517 - return err 8518 - } 8519 - continue 8520 - } 8521 - 8522 - switch string(nameBuf[:nameLen]) { 8523 - // t.LexiconTypeID (string) (string) 8524 - case "$type": 8525 - 8526 - { 8527 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8528 - if err != nil { 8529 - return err 8530 - } 8531 - 8532 - t.LexiconTypeID = string(sval) 8533 7986 } 8534 7987 // t.CreatedAt (string) (string) 8535 7988 case "createdAt":
-35
api/tangled/knothealth.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.knot.health 6 - 7 - import ( 8 - "context" 9 - 10 - "github.com/bluesky-social/indigo/lex/util" 11 - ) 12 - 13 - const ( 14 - KnotHealthNSID = "sh.tangled.knot.health" 15 - ) 16 - 17 - // KnotHealth_Output is the output of a sh.tangled.knot.health call. 18 - type KnotHealth_Output struct { 19 - // status: Health status of the knot 20 - Status string `json:"status" cborgen:"status"` 21 - // timestamp: Timestamp of the health check 22 - Timestamp *string `json:"timestamp,omitempty" cborgen:"timestamp,omitempty"` 23 - // version: Version of the knot server 24 - Version *string `json:"version,omitempty" cborgen:"version,omitempty"` 25 - } 26 - 27 - // KnotHealth calls the XRPC method "sh.tangled.knot.health". 28 - func KnotHealth(ctx context.Context, c util.LexClient) (*KnotHealth_Output, error) { 29 - var out KnotHealth_Output 30 - if err := c.LexDo(ctx, util.Query, "", "sh.tangled.knot.health", nil, nil, &out); err != nil { 31 - return nil, err 32 - } 33 - 34 - return &out, nil 35 - }
+6 -6
api/tangled/repocreate.go
··· 16 16 17 17 // RepoCreate_Input is the input argument to a sh.tangled.repo.create call. 18 18 type RepoCreate_Input struct { 19 - // default_branch: Default branch name 20 - Default_branch *string `json:"default_branch,omitempty" cborgen:"default_branch,omitempty"` 21 - // did: DID of the user creating the repository 22 - Did string `json:"did" cborgen:"did"` 23 - // name: Name of the repository 24 - Name string `json:"name" cborgen:"name"` 19 + // defaultBranch: Default branch to push to 20 + DefaultBranch *string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"` 21 + // rkey: Rkey of the repository record 22 + Rkey string `json:"rkey" cborgen:"rkey"` 23 + // source: A source URL to clone from, populate this when forking or importing a repository. 24 + Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 25 25 } 26 26 27 27 // RepoCreate calls the XRPC method "sh.tangled.repo.create".
+2
api/tangled/repodelete.go
··· 20 20 Did string `json:"did" cborgen:"did"` 21 21 // name: Name of the repository to delete 22 22 Name string `json:"name" cborgen:"name"` 23 + // rkey: Rkey of the repository record 24 + Rkey string `json:"rkey" cborgen:"rkey"` 23 25 } 24 26 25 27 // RepoDelete calls the XRPC method "sh.tangled.repo.delete".
-34
api/tangled/repofork.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.repo.fork 6 - 7 - import ( 8 - "context" 9 - 10 - "github.com/bluesky-social/indigo/lex/util" 11 - ) 12 - 13 - const ( 14 - RepoForkNSID = "sh.tangled.repo.fork" 15 - ) 16 - 17 - // RepoFork_Input is the input argument to a sh.tangled.repo.fork call. 18 - type RepoFork_Input struct { 19 - // did: DID of the user creating the fork 20 - Did string `json:"did" cborgen:"did"` 21 - // name: Name for the forked repository (defaults to basename of source) 22 - Name *string `json:"name,omitempty" cborgen:"name,omitempty"` 23 - // source: Source repository URL to fork from 24 - Source string `json:"source" cborgen:"source"` 25 - } 26 - 27 - // RepoFork calls the XRPC method "sh.tangled.repo.fork". 28 - func RepoFork(ctx context.Context, c util.LexClient, input *RepoFork_Input) error { 29 - if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.fork", nil, input, nil); err != nil { 30 - return err 31 - } 32 - 33 - return nil 34 - }
+4 -18
api/tangled/tangledpipeline.go
··· 29 29 Submodules bool `json:"submodules" cborgen:"submodules"` 30 30 } 31 31 32 - // Pipeline_Dependency is a "dependency" in the sh.tangled.pipeline schema. 33 - type Pipeline_Dependency struct { 34 - Packages []string `json:"packages" cborgen:"packages"` 35 - Registry string `json:"registry" cborgen:"registry"` 36 - } 37 - 38 32 // Pipeline_ManualTriggerData is a "manualTriggerData" in the sh.tangled.pipeline schema. 39 33 type Pipeline_ManualTriggerData struct { 40 34 Inputs []*Pipeline_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"` ··· 61 55 Ref string `json:"ref" cborgen:"ref"` 62 56 } 63 57 64 - // Pipeline_Step is a "step" in the sh.tangled.pipeline schema. 65 - type Pipeline_Step struct { 66 - Command string `json:"command" cborgen:"command"` 67 - Environment []*Pipeline_Pair `json:"environment,omitempty" cborgen:"environment,omitempty"` 68 - Name string `json:"name" cborgen:"name"` 69 - } 70 - 71 58 // Pipeline_TriggerMetadata is a "triggerMetadata" in the sh.tangled.pipeline schema. 72 59 type Pipeline_TriggerMetadata struct { 73 60 Kind string `json:"kind" cborgen:"kind"` ··· 87 74 88 75 // Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema. 89 76 type Pipeline_Workflow struct { 90 - Clone *Pipeline_CloneOpts `json:"clone" cborgen:"clone"` 91 - Dependencies []*Pipeline_Dependency `json:"dependencies" cborgen:"dependencies"` 92 - Environment []*Pipeline_Pair `json:"environment" cborgen:"environment"` 93 - Name string `json:"name" cborgen:"name"` 94 - Steps []*Pipeline_Step `json:"steps" cborgen:"steps"` 77 + Clone *Pipeline_CloneOpts `json:"clone" cborgen:"clone"` 78 + Engine string `json:"engine" cborgen:"engine"` 79 + Name string `json:"name" cborgen:"name"` 80 + Raw string `json:"raw" cborgen:"raw"` 95 81 }
+1
appview/cache/session/store.go
··· 31 31 PkceVerifier string 32 32 DpopAuthserverNonce string 33 33 DpopPrivateJwk string 34 + ReturnUrl string 34 35 } 35 36 36 37 type SessionStore struct {
+1 -1
appview/config/config.go
··· 17 17 Dev bool `env:"DEV, default=false"` 18 18 DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"` 19 19 20 - // temporarily, to add users to default spindle 20 + // temporarily, to add users to default knot and spindle 21 21 AppPassword string `env:"APP_PASSWORD"` 22 22 } 23 23
+57 -31
appview/db/db.go
··· 27 27 } 28 28 29 29 func Make(dbPath string) (*DB, error) { 30 - db, err := sql.Open("sqlite3", dbPath) 30 + // https://github.com/mattn/go-sqlite3#connection-string 31 + opts := []string{ 32 + "_foreign_keys=1", 33 + "_journal_mode=WAL", 34 + "_synchronous=NORMAL", 35 + "_auto_vacuum=incremental", 36 + } 37 + 38 + db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 31 39 if err != nil { 32 40 return nil, err 33 41 } 34 - _, err = db.Exec(` 35 - pragma journal_mode = WAL; 36 - pragma synchronous = normal; 37 - pragma foreign_keys = on; 38 - pragma temp_store = memory; 39 - pragma mmap_size = 30000000000; 40 - pragma page_size = 32768; 41 - pragma auto_vacuum = incremental; 42 - pragma busy_timeout = 5000; 42 + 43 + ctx := context.Background() 43 44 45 + conn, err := db.Conn(ctx) 46 + if err != nil { 47 + return nil, err 48 + } 49 + defer conn.Close() 50 + 51 + _, err = conn.ExecContext(ctx, ` 44 52 create table if not exists registrations ( 45 53 id integer primary key autoincrement, 46 54 domain text not null unique, ··· 462 470 id integer primary key autoincrement, 463 471 name text unique 464 472 ); 473 + 474 + -- indexes for better star query performance 475 + create index if not exists idx_stars_created on stars(created); 476 + create index if not exists idx_stars_repo_at_created on stars(repo_at, created); 465 477 `) 466 478 if err != nil { 467 479 return nil, err 468 480 } 469 481 470 482 // run migrations 471 - runMigration(db, "add-description-to-repos", func(tx *sql.Tx) error { 483 + runMigration(conn, "add-description-to-repos", func(tx *sql.Tx) error { 472 484 tx.Exec(` 473 485 alter table repos add column description text check (length(description) <= 200); 474 486 `) 475 487 return nil 476 488 }) 477 489 478 - runMigration(db, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 490 + runMigration(conn, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 479 491 // add unconstrained column 480 492 _, err := tx.Exec(` 481 493 alter table public_keys ··· 498 510 return nil 499 511 }) 500 512 501 - runMigration(db, "add-rkey-to-comments", func(tx *sql.Tx) error { 513 + runMigration(conn, "add-rkey-to-comments", func(tx *sql.Tx) error { 502 514 _, err := tx.Exec(` 503 515 alter table comments drop column comment_at; 504 516 alter table comments add column rkey text; ··· 506 518 return err 507 519 }) 508 520 509 - runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 521 + runMigration(conn, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 510 522 _, err := tx.Exec(` 511 523 alter table comments add column deleted text; -- timestamp 512 524 alter table comments add column edited text; -- timestamp ··· 514 526 return err 515 527 }) 516 528 517 - runMigration(db, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 529 + runMigration(conn, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 518 530 _, err := tx.Exec(` 519 531 alter table pulls add column source_branch text; 520 532 alter table pulls add column source_repo_at text; ··· 523 535 return err 524 536 }) 525 537 526 - runMigration(db, "add-source-to-repos", func(tx *sql.Tx) error { 538 + runMigration(conn, "add-source-to-repos", func(tx *sql.Tx) error { 527 539 _, err := tx.Exec(` 528 540 alter table repos add column source text; 529 541 `) ··· 534 546 // NOTE: this cannot be done in a transaction, so it is run outside [0] 535 547 // 536 548 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 537 - db.Exec("pragma foreign_keys = off;") 538 - runMigration(db, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 549 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 550 + runMigration(conn, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 539 551 _, err := tx.Exec(` 540 552 create table pulls_new ( 541 553 -- identifiers ··· 590 602 `) 591 603 return err 592 604 }) 593 - db.Exec("pragma foreign_keys = on;") 605 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 594 606 595 607 // run migrations 596 - runMigration(db, "add-spindle-to-repos", func(tx *sql.Tx) error { 608 + runMigration(conn, "add-spindle-to-repos", func(tx *sql.Tx) error { 597 609 tx.Exec(` 598 610 alter table repos add column spindle text; 599 611 `) 600 612 return nil 601 613 }) 602 614 603 - // make registrations.secret nullable for unified registration flow 604 - runMigration(db, "make-registrations-secret-nullable", func(tx *sql.Tx) error { 605 - // sqlite doesn't support ALTER COLUMN, so we need to recreate the table 615 + // drop all knot secrets, add unique constraint to knots 616 + // 617 + // knots will henceforth use service auth for signed requests 618 + runMigration(conn, "no-more-secrets", func(tx *sql.Tx) error { 606 619 _, err := tx.Exec(` 607 620 create table registrations_new ( 608 621 id integer primary key autoincrement, 609 - domain text not null unique, 622 + domain text not null, 610 623 did text not null, 611 - secret text, 612 624 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 613 - registered text 625 + registered text, 626 + read_only integer not null default 0, 627 + unique(domain, did) 614 628 ); 615 629 616 - insert into registrations_new (id, domain, did, secret, created, registered) 617 - select id, domain, did, secret, created, registered from registrations; 630 + insert into registrations_new (id, domain, did, created, registered, read_only) 631 + select id, domain, did, created, registered, 1 from registrations 632 + where registered is not null; 618 633 619 634 drop table registrations; 620 635 alter table registrations_new rename to registrations; ··· 623 638 }) 624 639 625 640 // recreate and add rkey + created columns with default constraint 626 - runMigration(db, "rework-collaborators-table", func(tx *sql.Tx) error { 641 + runMigration(conn, "rework-collaborators-table", func(tx *sql.Tx) error { 627 642 // create new table 628 643 // - repo_at instead of repo integer 629 644 // - rkey field ··· 677 692 return err 678 693 }) 679 694 695 + runMigration(conn, "add-rkey-to-issues", func(tx *sql.Tx) error { 696 + _, err := tx.Exec(` 697 + alter table issues add column rkey text not null default ''; 698 + 699 + -- get last url section from issue_at and save to rkey column 700 + update issues 701 + set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), ''); 702 + `) 703 + return err 704 + }) 705 + 680 706 return &DB{db}, nil 681 707 } 682 708 683 709 type migrationFn = func(*sql.Tx) error 684 710 685 - func runMigration(d *sql.DB, name string, migrationFn migrationFn) error { 686 - tx, err := d.Begin() 711 + func runMigration(c *sql.Conn, name string, migrationFn migrationFn) error { 712 + tx, err := c.BeginTx(context.Background(), nil) 687 713 if err != nil { 688 714 return err 689 715 }
+144 -41
appview/db/follow.go
··· 1 1 package db 2 2 3 3 import ( 4 + "fmt" 4 5 "log" 6 + "strings" 5 7 "time" 6 8 ) 7 9 ··· 53 55 return err 54 56 } 55 57 56 - func GetFollowerFollowing(e Execer, did string) (int, int, error) { 58 + type FollowStats struct { 59 + Followers int 60 + Following int 61 + } 62 + 63 + func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) { 57 64 followers, following := 0, 0 58 65 err := e.QueryRow( 59 - `SELECT 66 + `SELECT 60 67 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, 61 68 COUNT(CASE WHEN user_did = ? THEN 1 END) AS following 62 69 FROM follows;`, did, did).Scan(&followers, &following) 63 70 if err != nil { 64 - return 0, 0, err 71 + return FollowStats{}, err 65 72 } 66 - return followers, following, nil 73 + return FollowStats{ 74 + Followers: followers, 75 + Following: following, 76 + }, nil 67 77 } 68 78 69 - type FollowStatus int 79 + func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]FollowStats, error) { 80 + if len(dids) == 0 { 81 + return nil, nil 82 + } 83 + 84 + placeholders := make([]string, len(dids)) 85 + for i := range placeholders { 86 + placeholders[i] = "?" 87 + } 88 + placeholderStr := strings.Join(placeholders, ",") 89 + 90 + args := make([]any, len(dids)*2) 91 + for i, did := range dids { 92 + args[i] = did 93 + args[i+len(dids)] = did 94 + } 95 + 96 + query := fmt.Sprintf(` 97 + select 98 + coalesce(f.did, g.did) as did, 99 + coalesce(f.followers, 0) as followers, 100 + coalesce(g.following, 0) as following 101 + from ( 102 + select subject_did as did, count(*) as followers 103 + from follows 104 + where subject_did in (%s) 105 + group by subject_did 106 + ) f 107 + full outer join ( 108 + select user_did as did, count(*) as following 109 + from follows 110 + where user_did in (%s) 111 + group by user_did 112 + ) g on f.did = g.did`, 113 + placeholderStr, placeholderStr) 70 114 71 - const ( 72 - IsNotFollowing FollowStatus = iota 73 - IsFollowing 74 - IsSelf 75 - ) 115 + result := make(map[string]FollowStats) 76 116 77 - func (s FollowStatus) String() string { 78 - switch s { 79 - case IsNotFollowing: 80 - return "IsNotFollowing" 81 - case IsFollowing: 82 - return "IsFollowing" 83 - case IsSelf: 84 - return "IsSelf" 85 - default: 86 - return "IsNotFollowing" 117 + rows, err := e.Query(query, args...) 118 + if err != nil { 119 + return nil, err 87 120 } 88 - } 121 + defer rows.Close() 122 + 123 + for rows.Next() { 124 + var did string 125 + var followers, following int 126 + if err := rows.Scan(&did, &followers, &following); err != nil { 127 + return nil, err 128 + } 129 + result[did] = FollowStats{ 130 + Followers: followers, 131 + Following: following, 132 + } 133 + } 89 134 90 - func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { 91 - if userDid == subjectDid { 92 - return IsSelf 93 - } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { 94 - return IsNotFollowing 95 - } else { 96 - return IsFollowing 135 + for _, did := range dids { 136 + if _, exists := result[did]; !exists { 137 + result[did] = FollowStats{ 138 + Followers: 0, 139 + Following: 0, 140 + } 141 + } 97 142 } 143 + 144 + return result, nil 98 145 } 99 146 100 - func GetAllFollows(e Execer, limit int) ([]Follow, error) { 147 + func GetFollows(e Execer, limit int, filters ...filter) ([]Follow, error) { 101 148 var follows []Follow 102 149 103 - rows, err := e.Query(` 104 - select user_did, subject_did, followed_at, rkey 150 + var conditions []string 151 + var args []any 152 + for _, filter := range filters { 153 + conditions = append(conditions, filter.Condition()) 154 + args = append(args, filter.Arg()...) 155 + } 156 + 157 + whereClause := "" 158 + if conditions != nil { 159 + whereClause = " where " + strings.Join(conditions, " and ") 160 + } 161 + limitClause := "" 162 + if limit > 0 { 163 + limitClause = " limit ?" 164 + args = append(args, limit) 165 + } 166 + 167 + query := fmt.Sprintf( 168 + `select user_did, subject_did, followed_at, rkey 105 169 from follows 170 + %s 106 171 order by followed_at desc 107 - limit ?`, limit, 108 - ) 172 + %s 173 + `, whereClause, limitClause) 174 + 175 + rows, err := e.Query(query, args...) 109 176 if err != nil { 110 177 return nil, err 111 178 } 112 - defer rows.Close() 113 - 114 179 for rows.Next() { 115 180 var follow Follow 116 181 var followedAt string 117 - if err := rows.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey); err != nil { 182 + err := rows.Scan( 183 + &follow.UserDid, 184 + &follow.SubjectDid, 185 + &followedAt, 186 + &follow.Rkey, 187 + ) 188 + if err != nil { 118 189 return nil, err 119 190 } 120 - 121 191 followedAtTime, err := time.Parse(time.RFC3339, followedAt) 122 192 if err != nil { 123 193 log.Println("unable to determine followed at time") ··· 125 195 } else { 126 196 follow.FollowedAt = followedAtTime 127 197 } 128 - 129 198 follows = append(follows, follow) 130 199 } 200 + return follows, nil 201 + } 202 + 203 + func GetFollowers(e Execer, did string) ([]Follow, error) { 204 + return GetFollows(e, 0, FilterEq("subject_did", did)) 205 + } 131 206 132 - if err := rows.Err(); err != nil { 133 - return nil, err 207 + func GetFollowing(e Execer, did string) ([]Follow, error) { 208 + return GetFollows(e, 0, FilterEq("user_did", did)) 209 + } 210 + 211 + type FollowStatus int 212 + 213 + const ( 214 + IsNotFollowing FollowStatus = iota 215 + IsFollowing 216 + IsSelf 217 + ) 218 + 219 + func (s FollowStatus) String() string { 220 + switch s { 221 + case IsNotFollowing: 222 + return "IsNotFollowing" 223 + case IsFollowing: 224 + return "IsFollowing" 225 + case IsSelf: 226 + return "IsSelf" 227 + default: 228 + return "IsNotFollowing" 134 229 } 230 + } 135 231 136 - return follows, nil 232 + func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { 233 + if userDid == subjectDid { 234 + return IsSelf 235 + } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { 236 + return IsNotFollowing 237 + } else { 238 + return IsFollowing 239 + } 137 240 }
+103 -17
appview/db/issues.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "fmt" 6 + "strings" 5 7 "time" 6 8 7 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.sh/tangled.sh/core/api/tangled" 8 11 "tangled.sh/tangled.sh/core/appview/pagination" 9 12 ) 10 13 ··· 13 16 RepoAt syntax.ATURI 14 17 OwnerDid string 15 18 IssueId int 16 - IssueAt string 19 + Rkey string 17 20 Created time.Time 18 21 Title string 19 22 Body string ··· 42 45 Edited *time.Time 43 46 } 44 47 48 + func (i *Issue) AtUri() syntax.ATURI { 49 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 50 + } 51 + 45 52 func NewIssue(tx *sql.Tx, issue *Issue) error { 46 53 defer tx.Rollback() 47 54 ··· 67 74 issue.IssueId = nextId 68 75 69 76 res, err := tx.Exec(` 70 - insert into issues (repo_at, owner_did, issue_id, title, body) 71 - values (?, ?, ?, ?, ?) 72 - `, issue.RepoAt, issue.OwnerDid, issue.IssueId, issue.Title, issue.Body) 77 + insert into issues (repo_at, owner_did, rkey, issue_at, issue_id, title, body) 78 + values (?, ?, ?, ?, ?, ?, ?) 79 + `, issue.RepoAt, issue.OwnerDid, issue.Rkey, issue.AtUri(), issue.IssueId, issue.Title, issue.Body) 73 80 if err != nil { 74 81 return err 75 82 } ··· 87 94 return nil 88 95 } 89 96 90 - func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error { 91 - _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) 92 - return err 93 - } 94 - 95 97 func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 96 98 var issueAt string 97 99 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) ··· 104 106 return ownerDid, err 105 107 } 106 108 107 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 109 + func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 108 110 var issues []Issue 109 111 openValue := 0 110 112 if isOpen { ··· 117 119 select 118 120 i.id, 119 121 i.owner_did, 122 + i.rkey, 120 123 i.issue_id, 121 124 i.created, 122 125 i.title, ··· 136 139 select 137 140 id, 138 141 owner_did, 142 + rkey, 139 143 issue_id, 140 144 created, 141 145 title, 142 146 body, 143 147 open, 144 148 comment_count 145 - from 149 + from 146 150 numbered_issue 147 - where 151 + where 148 152 row_num between ? and ?`, 149 153 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 150 154 if err != nil { ··· 156 160 var issue Issue 157 161 var createdAt string 158 162 var metadata IssueMetadata 159 - err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 163 + err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 160 164 if err != nil { 161 165 return nil, err 162 166 } ··· 178 182 return issues, nil 179 183 } 180 184 185 + func GetIssuesWithLimit(e Execer, limit int, filters ...filter) ([]Issue, error) { 186 + issues := make([]Issue, 0, limit) 187 + 188 + var conditions []string 189 + var args []any 190 + for _, filter := range filters { 191 + conditions = append(conditions, filter.Condition()) 192 + args = append(args, filter.Arg()...) 193 + } 194 + 195 + whereClause := "" 196 + if conditions != nil { 197 + whereClause = " where " + strings.Join(conditions, " and ") 198 + } 199 + limitClause := "" 200 + if limit != 0 { 201 + limitClause = fmt.Sprintf(" limit %d ", limit) 202 + } 203 + 204 + query := fmt.Sprintf( 205 + `select 206 + i.id, 207 + i.owner_did, 208 + i.repo_at, 209 + i.issue_id, 210 + i.created, 211 + i.title, 212 + i.body, 213 + i.open 214 + from 215 + issues i 216 + %s 217 + order by 218 + i.created desc 219 + %s`, 220 + whereClause, limitClause) 221 + 222 + rows, err := e.Query(query, args...) 223 + if err != nil { 224 + return nil, err 225 + } 226 + defer rows.Close() 227 + 228 + for rows.Next() { 229 + var issue Issue 230 + var issueCreatedAt string 231 + err := rows.Scan( 232 + &issue.ID, 233 + &issue.OwnerDid, 234 + &issue.RepoAt, 235 + &issue.IssueId, 236 + &issueCreatedAt, 237 + &issue.Title, 238 + &issue.Body, 239 + &issue.Open, 240 + ) 241 + if err != nil { 242 + return nil, err 243 + } 244 + 245 + issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 246 + if err != nil { 247 + return nil, err 248 + } 249 + issue.Created = issueCreatedTime 250 + 251 + issues = append(issues, issue) 252 + } 253 + 254 + if err := rows.Err(); err != nil { 255 + return nil, err 256 + } 257 + 258 + return issues, nil 259 + } 260 + 261 + func GetIssues(e Execer, filters ...filter) ([]Issue, error) { 262 + return GetIssuesWithLimit(e, 0, filters...) 263 + } 264 + 181 265 // timeframe here is directly passed into the sql query filter, and any 182 266 // timeframe in the past should be negative; e.g.: "-3 months" 183 267 func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { ··· 187 271 `select 188 272 i.id, 189 273 i.owner_did, 274 + i.rkey, 190 275 i.repo_at, 191 276 i.issue_id, 192 277 i.created, ··· 219 304 err := rows.Scan( 220 305 &issue.ID, 221 306 &issue.OwnerDid, 307 + &issue.Rkey, 222 308 &issue.RepoAt, 223 309 &issue.IssueId, 224 310 &issueCreatedAt, ··· 262 348 } 263 349 264 350 func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 265 - query := `select id, owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 351 + query := `select id, owner_did, rkey, created, title, body, open from issues where repo_at = ? and issue_id = ?` 266 352 row := e.QueryRow(query, repoAt, issueId) 267 353 268 354 var issue Issue 269 355 var createdAt string 270 - err := row.Scan(&issue.ID, &issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open) 356 + err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open) 271 357 if err != nil { 272 358 return nil, err 273 359 } ··· 282 368 } 283 369 284 370 func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 285 - query := `select id, owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 371 + query := `select id, owner_did, rkey, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 286 372 row := e.QueryRow(query, repoAt, issueId) 287 373 288 374 var issue Issue 289 375 var createdAt string 290 - err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 376 + err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) 291 377 if err != nil { 292 378 return nil, nil, err 293 379 }
+2 -7
appview/db/profile.go
··· 348 348 return tx.Commit() 349 349 } 350 350 351 - func GetProfiles(e Execer, filters ...filter) ([]Profile, error) { 351 + func GetProfiles(e Execer, filters ...filter) (map[string]*Profile, error) { 352 352 var conditions []string 353 353 var args []any 354 354 for _, filter := range filters { ··· 448 448 idxs[did] = idx + 1 449 449 } 450 450 451 - var profiles []Profile 452 - for _, p := range profileMap { 453 - profiles = append(profiles, *p) 454 - } 455 - 456 - return profiles, nil 451 + return profileMap, nil 457 452 } 458 453 459 454 func GetProfile(e Execer, did string) (*Profile, error) {
+22 -3
appview/db/pulls.go
··· 310 310 return pullId - 1, err 311 311 } 312 312 313 - func GetPulls(e Execer, filters ...filter) ([]*Pull, error) { 313 + func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*Pull, error) { 314 314 pulls := make(map[int]*Pull) 315 315 316 316 var conditions []string ··· 323 323 whereClause := "" 324 324 if conditions != nil { 325 325 whereClause = " where " + strings.Join(conditions, " and ") 326 + } 327 + limitClause := "" 328 + if limit != 0 { 329 + limitClause = fmt.Sprintf(" limit %d ", limit) 326 330 } 327 331 328 332 query := fmt.Sprintf(` ··· 344 348 from 345 349 pulls 346 350 %s 347 - `, whereClause) 351 + order by 352 + created desc 353 + %s 354 + `, whereClause, limitClause) 348 355 349 356 rows, err := e.Query(query, args...) 350 357 if err != nil { ··· 412 419 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 413 420 submissionsQuery := fmt.Sprintf(` 414 421 select 415 - id, pull_id, round_number, patch, source_rev 422 + id, pull_id, round_number, patch, created, source_rev 416 423 from 417 424 pull_submissions 418 425 where ··· 438 445 for submissionsRows.Next() { 439 446 var s PullSubmission 440 447 var sourceRev sql.NullString 448 + var createdAt string 441 449 err := submissionsRows.Scan( 442 450 &s.ID, 443 451 &s.PullId, 444 452 &s.RoundNumber, 445 453 &s.Patch, 454 + &createdAt, 446 455 &sourceRev, 447 456 ) 448 457 if err != nil { 449 458 return nil, err 450 459 } 460 + 461 + createdTime, err := time.Parse(time.RFC3339, createdAt) 462 + if err != nil { 463 + return nil, err 464 + } 465 + s.Created = createdTime 451 466 452 467 if sourceRev.Valid { 453 468 s.SourceRev = sourceRev.String ··· 511 526 }) 512 527 513 528 return orderedByPullId, nil 529 + } 530 + 531 + func GetPulls(e Execer, filters ...filter) ([]*Pull, error) { 532 + return GetPullsWithLimit(e, 0, filters...) 514 533 } 515 534 516 535 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
+7 -7
appview/db/reaction.go
··· 11 11 12 12 const ( 13 13 Like ReactionKind = "๐Ÿ‘" 14 - Unlike = "๐Ÿ‘Ž" 15 - Laugh = "๐Ÿ˜†" 16 - Celebration = "๐ŸŽ‰" 17 - Confused = "๐Ÿซค" 18 - Heart = "โค๏ธ" 19 - Rocket = "๐Ÿš€" 20 - Eyes = "๐Ÿ‘€" 14 + Unlike ReactionKind = "๐Ÿ‘Ž" 15 + Laugh ReactionKind = "๐Ÿ˜†" 16 + Celebration ReactionKind = "๐ŸŽ‰" 17 + Confused ReactionKind = "๐Ÿซค" 18 + Heart ReactionKind = "โค๏ธ" 19 + Rocket ReactionKind = "๐Ÿš€" 20 + Eyes ReactionKind = "๐Ÿ‘€" 21 21 ) 22 22 23 23 func (rk ReactionKind) String() string {
+67 -133
appview/db/registration.go
··· 1 1 package db 2 2 3 3 import ( 4 - "crypto/rand" 5 4 "database/sql" 6 - "encoding/hex" 7 5 "fmt" 8 - "log" 9 6 "strings" 10 7 "time" 11 8 ) ··· 18 15 ByDid string 19 16 Created *time.Time 20 17 Registered *time.Time 18 + ReadOnly bool 21 19 } 22 20 23 21 func (r *Registration) Status() Status { 24 - if r.Registered != nil { 22 + if r.ReadOnly { 23 + return ReadOnly 24 + } else if r.Registered != nil { 25 25 return Registered 26 26 } else { 27 27 return Pending 28 28 } 29 29 } 30 30 31 + func (r *Registration) IsRegistered() bool { 32 + return r.Status() == Registered 33 + } 34 + 35 + func (r *Registration) IsReadOnly() bool { 36 + return r.Status() == ReadOnly 37 + } 38 + 39 + func (r *Registration) IsPending() bool { 40 + return r.Status() == Pending 41 + } 42 + 31 43 type Status uint32 32 44 33 45 const ( 34 46 Registered Status = iota 35 47 Pending 48 + ReadOnly 36 49 ) 37 50 38 - // returns registered status, did of owner, error 39 - func RegistrationsByDid(e Execer, did string) ([]Registration, error) { 51 + func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { 40 52 var registrations []Registration 41 53 42 - rows, err := e.Query(` 43 - select id, domain, did, created, registered from registrations 44 - where did = ? 45 - `, did) 46 - if err != nil { 47 - return nil, err 54 + var conditions []string 55 + var args []any 56 + for _, filter := range filters { 57 + conditions = append(conditions, filter.Condition()) 58 + args = append(args, filter.Arg()...) 48 59 } 49 60 50 - for rows.Next() { 51 - var createdAt *string 52 - var registeredAt *string 53 - var registration Registration 54 - err = rows.Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 55 - 56 - if err != nil { 57 - log.Println(err) 58 - } else { 59 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 60 - var registeredAtTime *time.Time 61 - if registeredAt != nil { 62 - x, _ := time.Parse(time.RFC3339, *registeredAt) 63 - registeredAtTime = &x 64 - } 65 - 66 - registration.Created = &createdAtTime 67 - registration.Registered = registeredAtTime 68 - registrations = append(registrations, registration) 69 - } 61 + whereClause := "" 62 + if conditions != nil { 63 + whereClause = " where " + strings.Join(conditions, " and ") 70 64 } 71 65 72 - return registrations, nil 73 - } 74 - 75 - // returns registered status, did of owner, error 76 - func RegistrationByDomain(e Execer, domain string) (*Registration, error) { 77 - var createdAt *string 78 - var registeredAt *string 79 - var registration Registration 80 - 81 - err := e.QueryRow(` 82 - select id, domain, did, created, registered from registrations 83 - where domain = ? 84 - `, domain).Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 66 + query := fmt.Sprintf(` 67 + select id, domain, did, created, registered, read_only 68 + from registrations 69 + %s 70 + order by created 71 + `, 72 + whereClause, 73 + ) 85 74 75 + rows, err := e.Query(query, args...) 86 76 if err != nil { 87 - if err == sql.ErrNoRows { 88 - return nil, nil 89 - } else { 90 - return nil, err 91 - } 77 + return nil, err 92 78 } 93 79 94 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 95 - var registeredAtTime *time.Time 96 - if registeredAt != nil { 97 - x, _ := time.Parse(time.RFC3339, *registeredAt) 98 - registeredAtTime = &x 99 - } 100 - 101 - registration.Created = &createdAtTime 102 - registration.Registered = registeredAtTime 103 - 104 - return &registration, nil 105 - } 106 - 107 - func genSecret() string { 108 - key := make([]byte, 32) 109 - rand.Read(key) 110 - return hex.EncodeToString(key) 111 - } 80 + for rows.Next() { 81 + var createdAt string 82 + var registeredAt sql.Null[string] 83 + var readOnly int 84 + var reg Registration 112 85 113 - func GenerateRegistrationKey(e Execer, domain, did string) (string, error) { 114 - // sanity check: does this domain already have a registration? 115 - reg, err := RegistrationByDomain(e, domain) 116 - if err != nil { 117 - return "", err 118 - } 119 - 120 - // registration is open 121 - if reg != nil { 122 - switch reg.Status() { 123 - case Registered: 124 - // already registered by `owner` 125 - return "", fmt.Errorf("%s already registered by %s", domain, reg.ByDid) 126 - case Pending: 127 - // TODO: be loud about this 128 - log.Printf("%s registered by %s, status pending", domain, reg.ByDid) 86 + err = rows.Scan(&reg.Id, &reg.Domain, &reg.ByDid, &createdAt, &registeredAt, &readOnly) 87 + if err != nil { 88 + return nil, err 129 89 } 130 - } 131 90 132 - secret := genSecret() 91 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 92 + reg.Created = &t 93 + } 133 94 134 - _, err = e.Exec(` 135 - insert into registrations (domain, did, secret) 136 - values (?, ?, ?) 137 - on conflict(domain) do update set did = excluded.did, secret = excluded.secret, created = excluded.created 138 - `, domain, did, secret) 139 - 140 - if err != nil { 141 - return "", err 142 - } 143 - 144 - return secret, nil 145 - } 95 + if registeredAt.Valid { 96 + if t, err := time.Parse(time.RFC3339, registeredAt.V); err == nil { 97 + reg.Registered = &t 98 + } 99 + } 146 100 147 - func GetRegistrationKey(e Execer, domain string) (string, error) { 148 - res := e.QueryRow(`select secret from registrations where domain = ?`, domain) 101 + if readOnly != 0 { 102 + reg.ReadOnly = true 103 + } 149 104 150 - var secret string 151 - err := res.Scan(&secret) 152 - if err != nil || secret == "" { 153 - return "", err 105 + registrations = append(registrations, reg) 154 106 } 155 107 156 - return secret, nil 108 + return registrations, nil 157 109 } 158 110 159 - func GetCompletedRegistrations(e Execer) ([]string, error) { 160 - rows, err := e.Query(`select domain from registrations where registered not null`) 161 - if err != nil { 162 - return nil, err 163 - } 164 - 165 - var domains []string 166 - for rows.Next() { 167 - var domain string 168 - err = rows.Scan(&domain) 169 - 170 - if err != nil { 171 - log.Println(err) 172 - } else { 173 - domains = append(domains, domain) 174 - } 111 + func MarkRegistered(e Execer, filters ...filter) error { 112 + var conditions []string 113 + var args []any 114 + for _, filter := range filters { 115 + conditions = append(conditions, filter.Condition()) 116 + args = append(args, filter.Arg()...) 175 117 } 176 118 177 - if err = rows.Err(); err != nil { 178 - return nil, err 119 + query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0" 120 + if len(conditions) > 0 { 121 + query += " where " + strings.Join(conditions, " and ") 179 122 } 180 123 181 - return domains, nil 182 - } 183 - 184 - func Register(e Execer, domain string) error { 185 - _, err := e.Exec(` 186 - update registrations 187 - set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 188 - where domain = ?; 189 - `, domain) 190 - 124 + _, err := e.Exec(query, args...) 191 125 return err 192 126 } 193 127
+9 -10
appview/db/repos.go
··· 19 19 Knot string 20 20 Rkey string 21 21 Created time.Time 22 - AtUri string 23 22 Description string 24 23 Spindle string 25 24 ··· 391 390 var description, spindle sql.NullString 392 391 393 392 row := e.QueryRow(` 394 - select did, name, knot, created, at_uri, description, spindle 393 + select did, name, knot, created, description, spindle, rkey 395 394 from repos 396 395 where did = ? and name = ? 397 396 `, ··· 400 399 ) 401 400 402 401 var createdAt string 403 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 402 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil { 404 403 return nil, err 405 404 } 406 405 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 421 420 var repo Repo 422 421 var nullableDescription sql.NullString 423 422 424 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 423 + row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) 425 424 426 425 var createdAt string 427 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 426 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { 428 427 return nil, err 429 428 } 430 429 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 444 443 `insert into repos 445 444 (did, name, knot, rkey, at_uri, description, source) 446 445 values (?, ?, ?, ?, ?, ?, ?)`, 447 - repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 446 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, 448 447 ) 449 448 return err 450 449 } ··· 467 466 var repos []Repo 468 467 469 468 rows, err := e.Query( 470 - `select did, name, knot, rkey, description, created, at_uri, source 469 + `select did, name, knot, rkey, description, created, source 471 470 from repos 472 471 where did = ? and source is not null and source != '' 473 472 order by created desc`, ··· 484 483 var nullableDescription sql.NullString 485 484 var nullableSource sql.NullString 486 485 487 - err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 486 + err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 488 487 if err != nil { 489 488 return nil, err 490 489 } ··· 521 520 var nullableSource sql.NullString 522 521 523 522 row := e.QueryRow( 524 - `select did, name, knot, rkey, description, created, at_uri, source 523 + `select did, name, knot, rkey, description, created, source 525 524 from repos 526 525 where did = ? and name = ? and source is not null and source != ''`, 527 526 did, name, 528 527 ) 529 528 530 - err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 529 + err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 531 530 if err != nil { 532 531 return nil, err 533 532 }
+73 -6
appview/db/star.go
··· 47 47 // Get a star record 48 48 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 49 49 query := ` 50 - select starred_by_did, repo_at, created, rkey 50 + select starred_by_did, repo_at, created, rkey 51 51 from stars 52 52 where starred_by_did = ? and repo_at = ?` 53 53 row := e.QueryRow(query, starredByDid, repoAt) ··· 119 119 } 120 120 121 121 repoQuery := fmt.Sprintf( 122 - `select starred_by_did, repo_at, created, rkey 122 + `select starred_by_did, repo_at, created, rkey 123 123 from stars 124 124 %s 125 125 order by created desc ··· 187 187 var stars []Star 188 188 189 189 rows, err := e.Query(` 190 - select 190 + select 191 191 s.starred_by_did, 192 192 s.repo_at, 193 193 s.rkey, ··· 196 196 r.name, 197 197 r.knot, 198 198 r.rkey, 199 - r.created, 200 - r.at_uri 199 + r.created 201 200 from stars s 202 201 join repos r on s.repo_at = r.at_uri 203 202 `) ··· 222 221 &repo.Knot, 223 222 &repo.Rkey, 224 223 &repoCreatedAt, 225 - &repo.AtUri, 226 224 ); err != nil { 227 225 return nil, err 228 226 } ··· 246 244 247 245 return stars, nil 248 246 } 247 + 248 + // GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week 249 + func GetTopStarredReposLastWeek(e Execer) ([]Repo, error) { 250 + // first, get the top repo URIs by star count from the last week 251 + query := ` 252 + with recent_starred_repos as ( 253 + select distinct repo_at 254 + from stars 255 + where created >= datetime('now', '-7 days') 256 + ), 257 + repo_star_counts as ( 258 + select 259 + s.repo_at, 260 + count(*) as star_count 261 + from stars s 262 + join recent_starred_repos rsr on s.repo_at = rsr.repo_at 263 + group by s.repo_at 264 + ) 265 + select rsc.repo_at 266 + from repo_star_counts rsc 267 + order by rsc.star_count desc 268 + limit 8 269 + ` 270 + 271 + rows, err := e.Query(query) 272 + if err != nil { 273 + return nil, err 274 + } 275 + defer rows.Close() 276 + 277 + var repoUris []string 278 + for rows.Next() { 279 + var repoUri string 280 + err := rows.Scan(&repoUri) 281 + if err != nil { 282 + return nil, err 283 + } 284 + repoUris = append(repoUris, repoUri) 285 + } 286 + 287 + if err := rows.Err(); err != nil { 288 + return nil, err 289 + } 290 + 291 + if len(repoUris) == 0 { 292 + return []Repo{}, nil 293 + } 294 + 295 + // get full repo data 296 + repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 297 + if err != nil { 298 + return nil, err 299 + } 300 + 301 + // sort repos by the original trending order 302 + repoMap := make(map[string]Repo) 303 + for _, repo := range repos { 304 + repoMap[repo.RepoAt().String()] = repo 305 + } 306 + 307 + orderedRepos := make([]Repo, 0, len(repoUris)) 308 + for _, uri := range repoUris { 309 + if repo, exists := repoMap[uri]; exists { 310 + orderedRepos = append(orderedRepos, repo) 311 + } 312 + } 313 + 314 + return orderedRepos, nil 315 + }
+12 -11
appview/db/strings.go
··· 50 50 func (s String) Validate() error { 51 51 var err error 52 52 53 - if !strings.Contains(s.Filename, ".") { 54 - err = errors.Join(err, fmt.Errorf("missing filename extension")) 55 - } 56 - 57 - if strings.HasSuffix(s.Filename, ".") { 58 - err = errors.Join(err, fmt.Errorf("filename ends with `.`")) 59 - } 60 - 61 53 if utf8.RuneCountInString(s.Filename) > 140 { 62 54 err = errors.Join(err, fmt.Errorf("filename too long")) 63 55 } ··· 113 105 filename = excluded.filename, 114 106 description = excluded.description, 115 107 content = excluded.content, 116 - edited = case 108 + edited = case 117 109 when 118 110 strings.content != excluded.content 119 111 or strings.filename != excluded.filename ··· 131 123 return err 132 124 } 133 125 134 - func GetStrings(e Execer, filters ...filter) ([]String, error) { 126 + func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) { 135 127 var all []String 136 128 137 129 var conditions []string ··· 146 138 whereClause = " where " + strings.Join(conditions, " and ") 147 139 } 148 140 141 + limitClause := "" 142 + if limit != 0 { 143 + limitClause = fmt.Sprintf(" limit %d ", limit) 144 + } 145 + 149 146 query := fmt.Sprintf(`select 150 147 did, 151 148 rkey, ··· 154 151 content, 155 152 created, 156 153 edited 157 - from strings %s`, 154 + from strings 155 + %s 156 + order by created desc 157 + %s`, 158 158 whereClause, 159 + limitClause, 159 160 ) 160 161 161 162 rows, err := e.Query(query, args...)
+6 -22
appview/db/timeline.go
··· 20 20 *FollowStats 21 21 } 22 22 23 - type FollowStats struct { 24 - Followers int 25 - Following int 26 - } 27 - 28 23 const Limit = 50 29 24 30 25 // TODO: this gathers heterogenous events from different sources and aggregates ··· 137 132 } 138 133 139 134 func getTimelineFollows(e Execer) ([]TimelineEvent, error) { 140 - follows, err := GetAllFollows(e, Limit) 135 + follows, err := GetFollows(e, Limit) 141 136 if err != nil { 142 137 return nil, err 143 138 } ··· 151 146 return nil, nil 152 147 } 153 148 154 - profileMap := make(map[string]Profile) 155 149 profiles, err := GetProfiles(e, FilterIn("did", subjects)) 156 150 if err != nil { 157 151 return nil, err 158 152 } 159 - for _, p := range profiles { 160 - profileMap[p.Did] = p 161 - } 162 153 163 - followStatMap := make(map[string]FollowStats) 164 - for _, s := range subjects { 165 - followers, following, err := GetFollowerFollowing(e, s) 166 - if err != nil { 167 - return nil, err 168 - } 169 - followStatMap[s] = FollowStats{ 170 - Followers: followers, 171 - Following: following, 172 - } 154 + followStatMap, err := GetFollowerFollowingCounts(e, subjects) 155 + if err != nil { 156 + return nil, err 173 157 } 174 158 175 159 var events []TimelineEvent 176 160 for _, f := range follows { 177 - profile, _ := profileMap[f.SubjectDid] 161 + profile, _ := profiles[f.SubjectDid] 178 162 followStatMap, _ := followStatMap[f.SubjectDid] 179 163 180 164 events = append(events, TimelineEvent{ 181 165 Follow: &f, 182 - Profile: &profile, 166 + Profile: profile, 183 167 FollowStats: &followStatMap, 184 168 EventAt: f.FollowedAt, 185 169 })
+11 -8
appview/ingester.go
··· 75 75 } 76 76 77 77 if err != nil { 78 - l.Error("error ingesting record", "err", err) 78 + l.Debug("error ingesting record", "err", err) 79 79 } 80 80 81 - return err 81 + return nil 82 82 } 83 83 } 84 84 ··· 714 714 715 715 ddb, ok := i.Db.Execer.(*db.DB) 716 716 if !ok { 717 - return fmt.Errorf("failed to index profile record, invalid db cast") 717 + return fmt.Errorf("failed to index knot record, invalid db cast") 718 718 } 719 719 720 720 // get record from db first 721 - registration, err := db.RegistrationByDomain(ddb, domain) 721 + registrations, err := db.GetRegistrations( 722 + ddb, 723 + db.FilterEq("domain", domain), 724 + db.FilterEq("did", did), 725 + ) 722 726 if err != nil { 723 727 return fmt.Errorf("failed to get registration: %w", err) 724 728 } 725 - 726 - // only allow deletion by the owner 727 - if registration.ByDid != did { 728 - return fmt.Errorf("unauthorized deletion attempt") 729 + if len(registrations) != 1 { 730 + return fmt.Errorf("got incorret number of registrations: %d, expected 1", len(registrations)) 729 731 } 732 + registration := registrations[0] 730 733 731 734 tx, err := ddb.Begin() 732 735 if err != nil {
+37 -86
appview/issues/issues.go
··· 7 7 "net/http" 8 8 "slices" 9 9 "strconv" 10 + "strings" 10 11 "time" 11 12 12 13 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 14 "github.com/bluesky-social/indigo/atproto/data" 14 - "github.com/bluesky-social/indigo/atproto/syntax" 15 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 16 "github.com/go-chi/chi/v5" 17 17 ··· 21 21 "tangled.sh/tangled.sh/core/appview/notify" 22 22 "tangled.sh/tangled.sh/core/appview/oauth" 23 23 "tangled.sh/tangled.sh/core/appview/pages" 24 + "tangled.sh/tangled.sh/core/appview/pages/markup" 24 25 "tangled.sh/tangled.sh/core/appview/pagination" 25 26 "tangled.sh/tangled.sh/core/appview/reporesolver" 26 27 "tangled.sh/tangled.sh/core/idresolver" ··· 73 74 return 74 75 } 75 76 76 - issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) 77 + issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt(), issueIdInt) 77 78 if err != nil { 78 79 log.Println("failed to get issue and comments", err) 79 80 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 80 81 return 81 82 } 82 83 83 - reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt)) 84 + reactionCountMap, err := db.GetReactionCountMap(rp.db, issue.AtUri()) 84 85 if err != nil { 85 86 log.Println("failed to get issue reactions") 86 87 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 88 89 89 90 userReactions := map[db.ReactionKind]bool{} 90 91 if user != nil { 91 - userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt)) 92 + userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 92 93 } 93 94 94 95 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) ··· 96 97 log.Println("failed to resolve issue owner", err) 97 98 } 98 99 99 - identsToResolve := make([]string, len(comments)) 100 - for i, comment := range comments { 101 - identsToResolve[i] = comment.OwnerDid 102 - } 103 - resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 104 - didHandleMap := make(map[string]string) 105 - for _, identity := range resolvedIds { 106 - if !identity.Handle.IsInvalidHandle() { 107 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 108 - } else { 109 - didHandleMap[identity.DID.String()] = identity.DID.String() 110 - } 111 - } 112 - 113 100 rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 114 101 LoggedInUser: user, 115 102 RepoInfo: f.RepoInfo(user), 116 - Issue: *issue, 103 + Issue: issue, 117 104 Comments: comments, 118 105 119 106 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 120 - DidHandleMap: didHandleMap, 121 107 122 108 OrderedReactionKinds: db.OrderedReactionKinds, 123 109 Reactions: reactionCountMap, ··· 142 128 return 143 129 } 144 130 145 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 131 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 146 132 if err != nil { 147 133 log.Println("failed to get issue", err) 148 134 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 174 160 Rkey: tid.TID(), 175 161 Record: &lexutil.LexiconTypeDecoder{ 176 162 Val: &tangled.RepoIssueState{ 177 - Issue: issue.IssueAt, 163 + Issue: issue.AtUri().String(), 178 164 State: closed, 179 165 }, 180 166 }, ··· 186 172 return 187 173 } 188 174 189 - err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 175 + err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt) 190 176 if err != nil { 191 177 log.Println("failed to close issue", err) 192 178 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 218 204 return 219 205 } 220 206 221 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 207 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 222 208 if err != nil { 223 209 log.Println("failed to get issue", err) 224 210 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 235 221 isIssueOwner := user.Did == issue.OwnerDid 236 222 237 223 if isCollaborator || isIssueOwner { 238 - err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 224 + err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt) 239 225 if err != nil { 240 226 log.Println("failed to reopen issue", err) 241 227 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") ··· 279 265 280 266 err := db.NewIssueComment(rp.db, &db.Comment{ 281 267 OwnerDid: user.Did, 282 - RepoAt: f.RepoAt, 268 + RepoAt: f.RepoAt(), 283 269 Issue: issueIdInt, 284 270 CommentId: commentId, 285 271 Body: body, ··· 294 280 createdAt := time.Now().Format(time.RFC3339) 295 281 commentIdInt64 := int64(commentId) 296 282 ownerDid := user.Did 297 - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 283 + issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) 298 284 if err != nil { 299 285 log.Println("failed to get issue at", err) 300 286 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 301 287 return 302 288 } 303 289 304 - atUri := f.RepoAt.String() 290 + atUri := f.RepoAt().String() 305 291 client, err := rp.oauth.AuthorizedClient(r) 306 292 if err != nil { 307 293 log.Println("failed to get authorized client", err) ··· 358 344 return 359 345 } 360 346 361 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 347 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 362 348 if err != nil { 363 349 log.Println("failed to get issue", err) 364 350 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 365 351 return 366 352 } 367 353 368 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 354 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 369 355 if err != nil { 370 356 http.Error(w, "bad comment id", http.StatusBadRequest) 371 357 return 372 358 } 373 359 374 - identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid) 375 - if err != nil { 376 - log.Println("failed to resolve did") 377 - return 378 - } 379 - 380 - didHandleMap := make(map[string]string) 381 - if !identity.Handle.IsInvalidHandle() { 382 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 383 - } else { 384 - didHandleMap[identity.DID.String()] = identity.DID.String() 385 - } 386 - 387 360 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 388 361 LoggedInUser: user, 389 362 RepoInfo: f.RepoInfo(user), 390 - DidHandleMap: didHandleMap, 391 363 Issue: issue, 392 364 Comment: comment, 393 365 }) ··· 417 389 return 418 390 } 419 391 420 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 392 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 421 393 if err != nil { 422 394 log.Println("failed to get issue", err) 423 395 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 424 396 return 425 397 } 426 398 427 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 399 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 428 400 if err != nil { 429 401 http.Error(w, "bad comment id", http.StatusBadRequest) 430 402 return ··· 503 475 } 504 476 505 477 // optimistic update for htmx 506 - didHandleMap := map[string]string{ 507 - user.Did: user.Handle, 508 - } 509 478 comment.Body = newBody 510 479 comment.Edited = &edited 511 480 ··· 513 482 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 514 483 LoggedInUser: user, 515 484 RepoInfo: f.RepoInfo(user), 516 - DidHandleMap: didHandleMap, 517 485 Issue: issue, 518 486 Comment: comment, 519 487 }) ··· 539 507 return 540 508 } 541 509 542 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 510 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 543 511 if err != nil { 544 512 log.Println("failed to get issue", err) 545 513 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 554 522 return 555 523 } 556 524 557 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 525 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 558 526 if err != nil { 559 527 http.Error(w, "bad comment id", http.StatusBadRequest) 560 528 return ··· 572 540 573 541 // optimistic deletion 574 542 deleted := time.Now() 575 - err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 543 + err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 576 544 if err != nil { 577 545 log.Println("failed to delete comment") 578 546 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 598 566 } 599 567 600 568 // optimistic update for htmx 601 - didHandleMap := map[string]string{ 602 - user.Did: user.Handle, 603 - } 604 569 comment.Body = "" 605 570 comment.Deleted = &deleted 606 571 ··· 608 573 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 609 574 LoggedInUser: user, 610 575 RepoInfo: f.RepoInfo(user), 611 - DidHandleMap: didHandleMap, 612 576 Issue: issue, 613 577 Comment: comment, 614 578 }) 615 - return 616 579 } 617 580 618 581 func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { ··· 641 604 return 642 605 } 643 606 644 - issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 607 + issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) 645 608 if err != nil { 646 609 log.Println("failed to get issues", err) 647 610 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 648 611 return 649 612 } 650 613 651 - identsToResolve := make([]string, len(issues)) 652 - for i, issue := range issues { 653 - identsToResolve[i] = issue.OwnerDid 654 - } 655 - resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 656 - didHandleMap := make(map[string]string) 657 - for _, identity := range resolvedIds { 658 - if !identity.Handle.IsInvalidHandle() { 659 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 660 - } else { 661 - didHandleMap[identity.DID.String()] = identity.DID.String() 662 - } 663 - } 664 - 665 614 rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 666 615 LoggedInUser: rp.oauth.GetUser(r), 667 616 RepoInfo: f.RepoInfo(user), 668 617 Issues: issues, 669 - DidHandleMap: didHandleMap, 670 618 FilteringByOpen: isOpen, 671 619 Page: page, 672 620 }) 673 - return 674 621 } 675 622 676 623 func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { ··· 697 644 return 698 645 } 699 646 647 + sanitizer := markup.NewSanitizer() 648 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(title)); st == "" { 649 + rp.pages.Notice(w, "issues", "Title is empty after HTML sanitization") 650 + return 651 + } 652 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(body)); sb == "" { 653 + rp.pages.Notice(w, "issues", "Body is empty after HTML sanitization") 654 + return 655 + } 656 + 700 657 tx, err := rp.db.BeginTx(r.Context(), nil) 701 658 if err != nil { 702 659 rp.pages.Notice(w, "issues", "Failed to create issue, try again later") ··· 704 661 } 705 662 706 663 issue := &db.Issue{ 707 - RepoAt: f.RepoAt, 664 + RepoAt: f.RepoAt(), 665 + Rkey: tid.TID(), 708 666 Title: title, 709 667 Body: body, 710 668 OwnerDid: user.Did, ··· 722 680 rp.pages.Notice(w, "issues", "Failed to create issue.") 723 681 return 724 682 } 725 - atUri := f.RepoAt.String() 726 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 683 + atUri := f.RepoAt().String() 684 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 727 685 Collection: tangled.RepoIssueNSID, 728 686 Repo: user.Did, 729 - Rkey: tid.TID(), 687 + Rkey: issue.Rkey, 730 688 Record: &lexutil.LexiconTypeDecoder{ 731 689 Val: &tangled.RepoIssue{ 732 690 Repo: atUri, ··· 739 697 }) 740 698 if err != nil { 741 699 log.Println("failed to create issue", err) 742 - rp.pages.Notice(w, "issues", "Failed to create issue.") 743 - return 744 - } 745 - 746 - err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) 747 - if err != nil { 748 - log.Println("failed to set issue at", err) 749 700 rp.pages.Notice(w, "issues", "Failed to create issue.") 750 701 return 751 702 }
+135 -61
appview/knots/knots.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "log" 6 7 "log/slog" 7 8 "net/http" 8 9 "slices" ··· 49 50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember) 50 51 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember) 51 52 53 + r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner) 54 + 52 55 return r 53 56 } 54 57 55 58 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 56 59 user := k.OAuth.GetUser(r) 57 - registrations, err := db.RegistrationsByDid(k.Db, user.Did) 60 + registrations, err := db.GetRegistrations( 61 + k.Db, 62 + db.FilterEq("did", user.Did), 63 + ) 58 64 if err != nil { 59 65 k.Logger.Error("failed to fetch knot registrations", "err", err) 60 66 w.WriteHeader(http.StatusInternalServerError) ··· 79 85 } 80 86 l = l.With("domain", domain) 81 87 82 - registrations, err := db.RegistrationsByDid(k.Db, user.Did) 88 + registrations, err := db.GetRegistrations( 89 + k.Db, 90 + db.FilterEq("did", user.Did), 91 + db.FilterEq("domain", domain), 92 + ) 83 93 if err != nil { 84 94 l.Error("failed to get registrations", "err", err) 85 95 http.Error(w, "Not found", http.StatusNotFound) 86 96 return 87 97 } 88 - 89 - // Find the specific registration for this domain 90 - var registration *db.Registration 91 - for _, reg := range registrations { 92 - if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil { 93 - registration = &reg 94 - break 95 - } 96 - } 97 - 98 - if registration == nil { 99 - l.Error("registration not found or not verified") 100 - http.Error(w, "Not found", http.StatusNotFound) 98 + if len(registrations) != 1 { 99 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 101 100 return 102 101 } 102 + registration := registrations[0] 103 103 104 104 members, err := k.Enforcer.GetUserByRole("server:member", domain) 105 105 if err != nil { ··· 120 120 return 121 121 } 122 122 123 - identsToResolve := make([]string, len(members)) 124 - copy(identsToResolve, members) 125 - resolvedIds := k.IdResolver.ResolveIdents(r.Context(), identsToResolve) 126 - didHandleMap := make(map[string]string) 127 - for _, identity := range resolvedIds { 128 - if !identity.Handle.IsInvalidHandle() { 129 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 130 - } else { 131 - didHandleMap[identity.DID.String()] = identity.DID.String() 132 - } 133 - } 134 - 135 123 // organize repos by did 136 124 repoMap := make(map[string][]db.Repo) 137 125 for _, r := range repos { ··· 140 128 141 129 k.Pages.Knot(w, pages.KnotParams{ 142 130 LoggedInUser: user, 143 - Registration: registration, 131 + Registration: &registration, 144 132 Members: members, 145 133 Repos: repoMap, 146 - DidHandleMap: didHandleMap, 147 134 IsOwner: true, 148 135 }) 149 136 } ··· 280 267 return 281 268 } 282 269 283 - registration, err := db.RegistrationByDomain(k.Db, domain) 270 + // get record from db first 271 + registrations, err := db.GetRegistrations( 272 + k.Db, 273 + db.FilterEq("did", user.Did), 274 + db.FilterEq("domain", domain), 275 + ) 284 276 if err != nil { 285 - l.Error("failed to retrieve domain registration", "err", err) 277 + l.Error("failed to get registration", "err", err) 286 278 fail() 287 279 return 288 280 } 289 - 290 - if registration.ByDid != user.Did { 291 - l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 292 - k.Pages.Notice(w, noticeId, "Failed to delete knot, unauthorized deletion attempt.") 281 + if len(registrations) != 1 { 282 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 283 + fail() 293 284 return 294 285 } 286 + registration := registrations[0] 295 287 296 288 tx, err := k.Db.Begin() 297 289 if err != nil { ··· 384 376 l = l.With("domain", domain) 385 377 l = l.With("user", user.Did) 386 378 387 - registration, err := db.RegistrationByDomain(k.Db, domain) 379 + // get record from db first 380 + registrations, err := db.GetRegistrations( 381 + k.Db, 382 + db.FilterEq("did", user.Did), 383 + db.FilterEq("domain", domain), 384 + ) 388 385 if err != nil { 389 - l.Error("failed to retrieve domain registration", "err", err) 386 + l.Error("failed to get registration", "err", err) 390 387 fail() 391 388 return 392 389 } 393 - 394 - if registration.ByDid != user.Did { 395 - l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 396 - k.Pages.Notice(w, noticeId, "Failed to verify knot, unauthorized verification attempt.") 390 + if len(registrations) != 1 { 391 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 392 + fail() 397 393 return 398 394 } 395 + registration := registrations[0] 399 396 400 397 // begin verification 401 398 err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) ··· 423 420 return 424 421 } 425 422 423 + // if this knot was previously read-only, then emit a record too 424 + // 425 + // this is part of migrating from the old knot system to the new one 426 + if registration.ReadOnly { 427 + // re-announce by registering under same rkey 428 + client, err := k.OAuth.AuthorizedClient(r) 429 + if err != nil { 430 + l.Error("failed to authorize client", "err", err) 431 + fail() 432 + return 433 + } 434 + 435 + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 436 + var exCid *string 437 + if ex != nil { 438 + exCid = ex.Cid 439 + } 440 + 441 + // ignore the error here 442 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 443 + Collection: tangled.KnotNSID, 444 + Repo: user.Did, 445 + Rkey: domain, 446 + Record: &lexutil.LexiconTypeDecoder{ 447 + Val: &tangled.Knot{ 448 + CreatedAt: time.Now().Format(time.RFC3339), 449 + }, 450 + }, 451 + SwapRecord: exCid, 452 + }) 453 + if err != nil { 454 + l.Error("non-fatal: failed to reannouce knot", "err", err) 455 + } 456 + } 457 + 426 458 // add this knot to knotstream 427 459 go k.Knotstream.AddSource( 428 460 r.Context(), ··· 436 468 } 437 469 438 470 // Get updated registration to show 439 - updatedRegistration, err := db.RegistrationByDomain(k.Db, domain) 471 + registrations, err = db.GetRegistrations( 472 + k.Db, 473 + db.FilterEq("did", user.Did), 474 + db.FilterEq("domain", domain), 475 + ) 440 476 if err != nil { 441 - l.Error("failed get updated registration", "err", err) 442 - k.Pages.HxRefresh(w) 477 + l.Error("failed to get registration", "err", err) 478 + fail() 479 + return 480 + } 481 + if len(registrations) != 1 { 482 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 483 + fail() 443 484 return 444 485 } 486 + updatedRegistration := registrations[0] 487 + 488 + log.Println(updatedRegistration) 445 489 446 490 w.Header().Set("HX-Reswap", "outerHTML") 447 491 k.Pages.KnotListing(w, pages.KnotListingParams{ 448 - Registration: *updatedRegistration, 492 + Registration: &updatedRegistration, 449 493 }) 450 494 } 451 495 ··· 462 506 l = l.With("domain", domain) 463 507 l = l.With("user", user.Did) 464 508 465 - registration, err := db.RegistrationByDomain(k.Db, domain) 509 + registrations, err := db.GetRegistrations( 510 + k.Db, 511 + db.FilterEq("did", user.Did), 512 + db.FilterEq("domain", domain), 513 + db.FilterIsNot("registered", "null"), 514 + ) 466 515 if err != nil { 467 - l.Error("failed to retrieve domain registration", "err", err) 468 - http.Error(w, "Not found", http.StatusNotFound) 516 + l.Error("failed to get registration", "err", err) 469 517 return 470 518 } 519 + if len(registrations) != 1 { 520 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 521 + return 522 + } 523 + registration := registrations[0] 471 524 472 525 noticeId := fmt.Sprintf("add-member-error-%d", registration.Id) 473 526 defaultErr := "Failed to add member. Try again later." 474 527 fail := func() { 475 528 k.Pages.Notice(w, noticeId, defaultErr) 476 - } 477 - 478 - if registration.ByDid != user.Did { 479 - l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 480 - k.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 481 - return 482 529 } 483 530 484 531 member := r.FormValue("member") ··· 566 613 l = l.With("domain", domain) 567 614 l = l.With("user", user.Did) 568 615 569 - registration, err := db.RegistrationByDomain(k.Db, domain) 616 + registrations, err := db.GetRegistrations( 617 + k.Db, 618 + db.FilterEq("did", user.Did), 619 + db.FilterEq("domain", domain), 620 + db.FilterIsNot("registered", "null"), 621 + ) 570 622 if err != nil { 571 - l.Error("failed to retrieve domain registration", "err", err) 572 - fail() 623 + l.Error("failed to get registration", "err", err) 573 624 return 574 625 } 575 - 576 - if registration.ByDid != user.Did { 577 - l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 578 - k.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 626 + if len(registrations) != 1 { 627 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 579 628 return 580 629 } 581 630 ··· 629 678 // ok 630 679 k.Pages.HxRefresh(w) 631 680 } 681 + 682 + func (k *Knots) banner(w http.ResponseWriter, r *http.Request) { 683 + user := k.OAuth.GetUser(r) 684 + l := k.Logger.With("handler", "removeMember") 685 + l = l.With("did", user.Did) 686 + l = l.With("handle", user.Handle) 687 + 688 + registrations, err := db.GetRegistrations( 689 + k.Db, 690 + db.FilterEq("did", user.Did), 691 + db.FilterEq("read_only", 1), 692 + ) 693 + if err != nil { 694 + l.Error("non-fatal: failed to get registrations") 695 + return 696 + } 697 + 698 + if registrations == nil { 699 + return 700 + } 701 + 702 + k.Pages.KnotBanner(w, pages.KnotBannerParams{ 703 + Registrations: registrations, 704 + }) 705 + }
+16 -13
appview/middleware/middleware.go
··· 5 5 "fmt" 6 6 "log" 7 7 "net/http" 8 + "net/url" 8 9 "slices" 9 10 "strconv" 10 11 "strings" 11 - "time" 12 12 13 13 "github.com/bluesky-social/indigo/atproto/identity" 14 14 "github.com/go-chi/chi/v5" ··· 46 46 func AuthMiddleware(a *oauth.OAuth) middlewareFunc { 47 47 return func(next http.Handler) http.Handler { 48 48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 + returnURL := "/" 50 + if u, err := url.Parse(r.Header.Get("Referer")); err == nil { 51 + returnURL = u.RequestURI() 52 + } 53 + 54 + loginURL := fmt.Sprintf("/login?return_url=%s", url.QueryEscape(returnURL)) 55 + 49 56 redirectFunc := func(w http.ResponseWriter, r *http.Request) { 50 - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 57 + http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) 51 58 } 52 59 if r.Header.Get("HX-Request") == "true" { 53 60 redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 54 - w.Header().Set("HX-Redirect", "/login") 61 + w.Header().Set("HX-Redirect", loginURL) 55 62 w.WriteHeader(http.StatusOK) 56 63 } 57 64 } ··· 210 217 if err != nil { 211 218 // invalid did or handle 212 219 log.Println("failed to resolve repo") 213 - mw.pages.Error404(w) 220 + mw.pages.ErrorKnot404(w) 214 221 return 215 222 } 216 223 217 - ctx := context.WithValue(req.Context(), "knot", repo.Knot) 218 - ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 219 - ctx = context.WithValue(ctx, "repoDescription", repo.Description) 220 - ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 221 - ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 224 + ctx := context.WithValue(req.Context(), "repo", repo) 222 225 next.ServeHTTP(w, req.WithContext(ctx)) 223 226 }) 224 227 } ··· 231 234 f, err := mw.repoResolver.Resolve(r) 232 235 if err != nil { 233 236 log.Println("failed to fully resolve repo", err) 234 - http.Error(w, "invalid repo url", http.StatusNotFound) 237 + mw.pages.ErrorKnot404(w) 235 238 return 236 239 } 237 240 ··· 243 246 return 244 247 } 245 248 246 - pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) 249 + pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 247 250 if err != nil { 248 251 log.Println("failed to get pull and comments", err) 249 252 return ··· 280 283 f, err := mw.repoResolver.Resolve(r) 281 284 if err != nil { 282 285 log.Println("failed to fully resolve repo", err) 283 - http.Error(w, "invalid repo url", http.StatusNotFound) 286 + mw.pages.ErrorKnot404(w) 284 287 return 285 288 } 286 289 287 - fullName := f.OwnerHandle() + "/" + f.RepoName 290 + fullName := f.OwnerHandle() + "/" + f.Name 288 291 289 292 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 290 293 if r.URL.Query().Get("go-get") == "1" {
+115 -84
appview/oauth/handler/handler.go
··· 8 8 "log" 9 9 "net/http" 10 10 "net/url" 11 + "slices" 11 12 "strings" 12 13 "time" 13 14 ··· 25 26 "tangled.sh/tangled.sh/core/appview/oauth/client" 26 27 "tangled.sh/tangled.sh/core/appview/pages" 27 28 "tangled.sh/tangled.sh/core/idresolver" 28 - "tangled.sh/tangled.sh/core/knotclient" 29 29 "tangled.sh/tangled.sh/core/rbac" 30 30 "tangled.sh/tangled.sh/core/tid" 31 31 ) ··· 109 109 func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { 110 110 switch r.Method { 111 111 case http.MethodGet: 112 - o.pages.Login(w, pages.LoginParams{}) 112 + returnURL := r.URL.Query().Get("return_url") 113 + o.pages.Login(w, pages.LoginParams{ 114 + ReturnUrl: returnURL, 115 + }) 113 116 case http.MethodPost: 114 117 handle := r.FormValue("handle") 115 118 ··· 194 197 DpopAuthserverNonce: parResp.DpopAuthserverNonce, 195 198 DpopPrivateJwk: string(dpopKeyJson), 196 199 State: parResp.State, 200 + ReturnUrl: r.FormValue("return_url"), 197 201 }) 198 202 if err != nil { 199 203 log.Println("failed to save oauth request:", err) ··· 249 253 return 250 254 } 251 255 256 + if iss != oauthRequest.AuthserverIss { 257 + log.Println("mismatched iss:", iss, "!=", oauthRequest.AuthserverIss, "for state:", state) 258 + o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 259 + return 260 + } 261 + 252 262 self := o.oauth.ClientMetadata() 253 263 254 264 oauthClient, err := client.NewClient( ··· 311 321 } 312 322 } 313 323 314 - http.Redirect(w, r, "/", http.StatusFound) 324 + returnUrl := oauthRequest.ReturnUrl 325 + if returnUrl == "" { 326 + returnUrl = "/" 327 + } 328 + 329 + http.Redirect(w, r, returnUrl, http.StatusFound) 315 330 } 316 331 317 332 func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) { ··· 338 353 return pubKey, nil 339 354 } 340 355 356 + var ( 357 + tangledHandle = "tangled.sh" 358 + tangledDid = "did:plc:wshs7t2adsemcrrd4snkeqli" 359 + defaultSpindle = "spindle.tangled.sh" 360 + defaultKnot = "knot1.tangled.sh" 361 + ) 362 + 341 363 func (o *OAuthHandler) addToDefaultSpindle(did string) { 342 364 // use the tangled.sh app password to get an accessJwt 343 365 // and create an sh.tangled.spindle.member record with that 344 - 345 - defaultSpindle := "spindle.tangled.sh" 346 - appPassword := o.config.Core.AppPassword 347 - 348 366 spindleMembers, err := db.GetSpindleMembers( 349 367 o.db, 350 368 db.FilterEq("instance", "spindle.tangled.sh"), ··· 360 378 return 361 379 } 362 380 363 - // TODO: hardcoded tangled handle and did for now 364 - tangledHandle := "tangled.sh" 365 - tangledDid := "did:plc:wshs7t2adsemcrrd4snkeqli" 381 + log.Printf("adding %s to default spindle", did) 382 + session, err := o.createAppPasswordSession() 383 + if err != nil { 384 + log.Printf("failed to create session: %s", err) 385 + return 386 + } 366 387 367 - if appPassword == "" { 368 - log.Println("no app password configured, skipping spindle member addition") 388 + record := tangled.SpindleMember{ 389 + LexiconTypeID: "sh.tangled.spindle.member", 390 + Subject: did, 391 + Instance: defaultSpindle, 392 + CreatedAt: time.Now().Format(time.RFC3339), 393 + } 394 + 395 + if err := session.putRecord(record); err != nil { 396 + log.Printf("failed to add member to default knot: %s", err) 369 397 return 370 398 } 371 399 372 - log.Printf("adding %s to default spindle", did) 400 + log.Printf("successfully added %s to default spindle", did) 401 + } 402 + 403 + func (o *OAuthHandler) addToDefaultKnot(did string) { 404 + // use the tangled.sh app password to get an accessJwt 405 + // and create an sh.tangled.spindle.member record with that 406 + 407 + allKnots, err := o.enforcer.GetKnotsForUser(did) 408 + if err != nil { 409 + log.Printf("failed to get knot members for did %s: %v", did, err) 410 + return 411 + } 412 + 413 + if slices.Contains(allKnots, defaultKnot) { 414 + log.Printf("did %s is already a member of the default knot", did) 415 + return 416 + } 417 + 418 + log.Printf("adding %s to default knot", did) 419 + session, err := o.createAppPasswordSession() 420 + if err != nil { 421 + log.Printf("failed to create session: %s", err) 422 + return 423 + } 424 + 425 + record := tangled.KnotMember{ 426 + LexiconTypeID: "sh.tangled.knot.member", 427 + Subject: did, 428 + Domain: defaultKnot, 429 + CreatedAt: time.Now().Format(time.RFC3339), 430 + } 431 + 432 + if err := session.putRecord(record); err != nil { 433 + log.Printf("failed to add member to default knot: %s", err) 434 + return 435 + } 436 + 437 + log.Printf("successfully added %s to default Knot", did) 438 + } 439 + 440 + // create a session using apppasswords 441 + type session struct { 442 + AccessJwt string `json:"accessJwt"` 443 + PdsEndpoint string 444 + } 445 + 446 + func (o *OAuthHandler) createAppPasswordSession() (*session, error) { 447 + appPassword := o.config.Core.AppPassword 448 + if appPassword == "" { 449 + return nil, fmt.Errorf("no app password configured, skipping member addition") 450 + } 373 451 374 452 resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid) 375 453 if err != nil { 376 - log.Printf("failed to resolve tangled.sh DID %s: %v", tangledDid, err) 377 - return 454 + return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", tangledDid, err) 378 455 } 379 456 380 457 pdsEndpoint := resolved.PDSEndpoint() 381 458 if pdsEndpoint == "" { 382 - log.Printf("no PDS endpoint found for tangled.sh DID %s", tangledDid) 383 - return 459 + return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", tangledDid) 384 460 } 385 461 386 462 sessionPayload := map[string]string{ ··· 389 465 } 390 466 sessionBytes, err := json.Marshal(sessionPayload) 391 467 if err != nil { 392 - log.Printf("failed to marshal session payload: %v", err) 393 - return 468 + return nil, fmt.Errorf("failed to marshal session payload: %v", err) 394 469 } 395 470 396 471 sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 397 472 sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 398 473 if err != nil { 399 - log.Printf("failed to create session request: %v", err) 400 - return 474 + return nil, fmt.Errorf("failed to create session request: %v", err) 401 475 } 402 476 sessionReq.Header.Set("Content-Type", "application/json") 403 477 404 478 client := &http.Client{Timeout: 30 * time.Second} 405 479 sessionResp, err := client.Do(sessionReq) 406 480 if err != nil { 407 - log.Printf("failed to create session: %v", err) 408 - return 481 + return nil, fmt.Errorf("failed to create session: %v", err) 409 482 } 410 483 defer sessionResp.Body.Close() 411 484 412 485 if sessionResp.StatusCode != http.StatusOK { 413 - log.Printf("failed to create session: HTTP %d", sessionResp.StatusCode) 414 - return 486 + return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) 415 487 } 416 488 417 - var session struct { 418 - AccessJwt string `json:"accessJwt"` 419 - } 489 + var session session 420 490 if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 421 - log.Printf("failed to decode session response: %v", err) 422 - return 491 + return nil, fmt.Errorf("failed to decode session response: %v", err) 423 492 } 424 493 425 - record := tangled.SpindleMember{ 426 - LexiconTypeID: "sh.tangled.spindle.member", 427 - Subject: did, 428 - Instance: defaultSpindle, 429 - CreatedAt: time.Now().Format(time.RFC3339), 430 - } 494 + session.PdsEndpoint = pdsEndpoint 495 + 496 + return &session, nil 497 + } 431 498 499 + func (s *session) putRecord(record any) error { 432 500 recordBytes, err := json.Marshal(record) 433 501 if err != nil { 434 - log.Printf("failed to marshal spindle member record: %v", err) 435 - return 502 + return fmt.Errorf("failed to marshal knot member record: %w", err) 436 503 } 437 504 438 - payload := map[string]interface{}{ 505 + payload := map[string]any{ 439 506 "repo": tangledDid, 440 - "collection": tangled.SpindleMemberNSID, 507 + "collection": tangled.KnotMemberNSID, 441 508 "rkey": tid.TID(), 442 509 "record": json.RawMessage(recordBytes), 443 510 } 444 511 445 512 payloadBytes, err := json.Marshal(payload) 446 513 if err != nil { 447 - log.Printf("failed to marshal request payload: %v", err) 448 - return 514 + return fmt.Errorf("failed to marshal request payload: %w", err) 449 515 } 450 516 451 - url := pdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 517 + url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 452 518 req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 453 519 if err != nil { 454 - log.Printf("failed to create HTTP request: %v", err) 455 - return 520 + return fmt.Errorf("failed to create HTTP request: %w", err) 456 521 } 457 522 458 523 req.Header.Set("Content-Type", "application/json") 459 - req.Header.Set("Authorization", "Bearer "+session.AccessJwt) 524 + req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 460 525 526 + client := &http.Client{Timeout: 30 * time.Second} 461 527 resp, err := client.Do(req) 462 528 if err != nil { 463 - log.Printf("failed to add user to default spindle: %v", err) 464 - return 529 + return fmt.Errorf("failed to add user to default Knot: %w", err) 465 530 } 466 531 defer resp.Body.Close() 467 532 468 533 if resp.StatusCode != http.StatusOK { 469 - log.Printf("failed to add user to default spindle: HTTP %d", resp.StatusCode) 470 - return 534 + return fmt.Errorf("failed to add user to default Knot: HTTP %d", resp.StatusCode) 471 535 } 472 536 473 - log.Printf("successfully added %s to default spindle", did) 474 - } 475 - 476 - func (o *OAuthHandler) addToDefaultKnot(did string) { 477 - defaultKnot := "knot1.tangled.sh" 478 - 479 - log.Printf("adding %s to default knot", did) 480 - err := o.enforcer.AddKnotMember(defaultKnot, did) 481 - if err != nil { 482 - log.Println("failed to add user to knot1.tangled.sh: ", err) 483 - return 484 - } 485 - err = o.enforcer.E.SavePolicy() 486 - if err != nil { 487 - log.Println("failed to add user to knot1.tangled.sh: ", err) 488 - return 489 - } 490 - 491 - secret, err := db.GetRegistrationKey(o.db, defaultKnot) 492 - if err != nil { 493 - log.Println("failed to get registration key for knot1.tangled.sh") 494 - return 495 - } 496 - signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.config.Core.Dev) 497 - resp, err := signedClient.AddMember(did) 498 - if err != nil { 499 - log.Println("failed to add user to knot1.tangled.sh: ", err) 500 - return 501 - } 502 - 503 - if resp.StatusCode != http.StatusNoContent { 504 - log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 505 - return 506 - } 537 + return nil 507 538 }
+16 -3
appview/oauth/oauth.go
··· 103 103 if err != nil { 104 104 return nil, false, fmt.Errorf("error parsing expiry time: %w", err) 105 105 } 106 - if expiry.Sub(time.Now()) <= 5*time.Minute { 106 + if time.Until(expiry) <= 5*time.Minute { 107 107 privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) 108 108 if err != nil { 109 109 return nil, false, err ··· 224 224 s.service = service 225 225 } 226 226 } 227 + 228 + // Specify the Duration in seconds for the expiry of this token 229 + // 230 + // The time of expiry is calculated as time.Now().Unix() + exp 227 231 func WithExp(exp int64) ServiceClientOpt { 228 232 return func(s *ServiceClientOpts) { 229 - s.exp = exp 233 + s.exp = time.Now().Unix() + exp 230 234 } 231 235 } 232 236 ··· 266 270 return nil, err 267 271 } 268 272 273 + // force expiry to atleast 60 seconds in the future 274 + sixty := time.Now().Unix() + 60 275 + if opts.exp < sixty { 276 + opts.exp = sixty 277 + } 278 + 269 279 resp, err := authorizedClient.ServerGetServiceAuth(r.Context(), opts.Audience(), opts.exp, opts.lxm) 270 280 if err != nil { 271 281 return nil, err ··· 276 286 AccessJwt: resp.Token, 277 287 }, 278 288 Host: opts.Host(), 289 + Client: &http.Client{ 290 + Timeout: time.Second * 5, 291 + }, 279 292 }, nil 280 293 } 281 294 ··· 305 318 redirectURIs := makeRedirectURIs(clientURI) 306 319 307 320 if o.config.Core.Dev { 308 - clientURI = fmt.Sprintf("http://127.0.0.1:3000") 321 + clientURI = "http://127.0.0.1:3000" 309 322 redirectURIs = makeRedirectURIs(clientURI) 310 323 311 324 query := url.Values{}
+39 -6
appview/pages/funcmap.go
··· 1 1 package pages 2 2 3 3 import ( 4 + "context" 4 5 "crypto/hmac" 5 6 "crypto/sha256" 6 7 "encoding/hex" ··· 18 19 19 20 "github.com/dustin/go-humanize" 20 21 "github.com/go-enry/go-enry/v2" 21 - "github.com/microcosm-cc/bluemonday" 22 22 "tangled.sh/tangled.sh/core/appview/filetree" 23 23 "tangled.sh/tangled.sh/core/appview/pages/markup" 24 + "tangled.sh/tangled.sh/core/crypto" 24 25 ) 25 26 26 27 func (p *Pages) funcMap() template.FuncMap { ··· 28 29 "split": func(s string) []string { 29 30 return strings.Split(s, "\n") 30 31 }, 32 + "resolve": func(s string) string { 33 + identity, err := p.resolver.ResolveIdent(context.Background(), s) 34 + 35 + if err != nil { 36 + return s 37 + } 38 + 39 + if identity.Handle.IsInvalidHandle() { 40 + return "handle.invalid" 41 + } 42 + 43 + return "@" + identity.Handle.String() 44 + }, 31 45 "truncateAt30": func(s string) string { 32 46 if len(s) <= 30 { 33 47 return s ··· 74 88 "negf64": func(a float64) float64 { 75 89 return -a 76 90 }, 77 - "cond": func(cond interface{}, a, b string) string { 91 + "cond": func(cond any, a, b string) string { 78 92 if cond == nil { 79 93 return b 80 94 } ··· 167 181 return html.UnescapeString(s) 168 182 }, 169 183 "nl2br": func(text string) template.HTML { 170 - return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1)) 184 + return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>")) 171 185 }, 172 186 "unwrapText": func(text string) string { 173 187 paragraphs := strings.Split(text, "\n\n") ··· 193 207 } 194 208 return v.Slice(0, min(n, v.Len())).Interface() 195 209 }, 196 - 197 210 "markdown": func(text string) template.HTML { 198 - rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 199 - return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text))) 211 + p.rctx.RendererType = markup.RendererTypeDefault 212 + htmlString := p.rctx.RenderMarkdown(text) 213 + sanitized := p.rctx.SanitizeDefault(htmlString) 214 + return template.HTML(sanitized) 215 + }, 216 + "description": func(text string) template.HTML { 217 + p.rctx.RendererType = markup.RendererTypeDefault 218 + htmlString := p.rctx.RenderMarkdown(text) 219 + sanitized := p.rctx.SanitizeDescription(htmlString) 220 + return template.HTML(sanitized) 200 221 }, 201 222 "isNil": func(t any) bool { 202 223 // returns false for other "zero" values ··· 256 277 }, 257 278 "layoutCenter": func() string { 258 279 return "col-span-1 md:col-span-8 lg:col-span-6" 280 + }, 281 + 282 + "normalizeForHtmlId": func(s string) string { 283 + // TODO: extend this to handle other cases? 284 + return strings.ReplaceAll(s, ":", "_") 285 + }, 286 + "sshFingerprint": func(pubKey string) string { 287 + fp, err := crypto.SSHFingerprint(pubKey) 288 + if err != nil { 289 + return "error" 290 + } 291 + return fp 259 292 }, 260 293 } 261 294 }
+61 -31
appview/pages/markup/markdown.go
··· 9 9 "path" 10 10 "strings" 11 11 12 - "github.com/microcosm-cc/bluemonday" 12 + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 13 + "github.com/alecthomas/chroma/v2/styles" 13 14 "github.com/yuin/goldmark" 15 + highlighting "github.com/yuin/goldmark-highlighting/v2" 14 16 "github.com/yuin/goldmark/ast" 15 17 "github.com/yuin/goldmark/extension" 16 18 "github.com/yuin/goldmark/parser" ··· 40 42 repoinfo.RepoInfo 41 43 IsDev bool 42 44 RendererType RendererType 45 + Sanitizer Sanitizer 43 46 } 44 47 45 48 func (rctx *RenderContext) RenderMarkdown(source string) string { 46 49 md := goldmark.New( 47 - goldmark.WithExtensions(extension.GFM), 50 + goldmark.WithExtensions( 51 + extension.GFM, 52 + highlighting.NewHighlighting( 53 + highlighting.WithFormatOptions( 54 + chromahtml.Standalone(false), 55 + chromahtml.WithClasses(true), 56 + ), 57 + highlighting.WithCustomStyle(styles.Get("catppuccin-latte")), 58 + ), 59 + extension.NewFootnote( 60 + extension.WithFootnoteIDPrefix([]byte("footnote")), 61 + ), 62 + ), 48 63 goldmark.WithParserOptions( 49 64 parser.WithAutoHeadingID(), 50 65 ), ··· 145 160 } 146 161 } 147 162 148 - func (rctx *RenderContext) Sanitize(html string) string { 149 - policy := bluemonday.UGCPolicy() 150 - 151 - // video 152 - policy.AllowElements("video") 153 - policy.AllowAttrs("controls").OnElements("video") 154 - policy.AllowElements("source") 155 - policy.AllowAttrs("src", "type").OnElements("source") 156 - 157 - // centering content 158 - policy.AllowElements("center") 163 + func (rctx *RenderContext) SanitizeDefault(html string) string { 164 + return rctx.Sanitizer.SanitizeDefault(html) 165 + } 159 166 160 - policy.AllowAttrs("align", "style", "width", "height").Globally() 161 - policy.AllowStyles( 162 - "margin", 163 - "padding", 164 - "text-align", 165 - "font-weight", 166 - "text-decoration", 167 - "padding-left", 168 - "padding-right", 169 - "padding-top", 170 - "padding-bottom", 171 - "margin-left", 172 - "margin-right", 173 - "margin-top", 174 - "margin-bottom", 175 - ) 176 - return policy.Sanitize(html) 167 + func (rctx *RenderContext) SanitizeDescription(html string) string { 168 + return rctx.Sanitizer.SanitizeDescription(html) 177 169 } 178 170 179 171 type MarkdownTransformer struct { ··· 189 181 switch a.rctx.RendererType { 190 182 case RendererTypeRepoMarkdown: 191 183 switch n := n.(type) { 184 + case *ast.Heading: 185 + a.rctx.anchorHeadingTransformer(n) 192 186 case *ast.Link: 193 187 a.rctx.relativeLinkTransformer(n) 194 188 case *ast.Image: ··· 197 191 } 198 192 case RendererTypeDefault: 199 193 switch n := n.(type) { 194 + case *ast.Heading: 195 + a.rctx.anchorHeadingTransformer(n) 200 196 case *ast.Image: 201 197 a.rctx.imageFromKnotAstTransformer(n) 202 198 a.rctx.camoImageLinkAstTransformer(n) ··· 211 207 212 208 dst := string(link.Destination) 213 209 214 - if isAbsoluteUrl(dst) { 210 + if isAbsoluteUrl(dst) || isFragment(dst) || isMail(dst) { 215 211 return 216 212 } 217 213 ··· 252 248 img.Destination = []byte(rctx.imageFromKnotTransformer(dst)) 253 249 } 254 250 251 + func (rctx *RenderContext) anchorHeadingTransformer(h *ast.Heading) { 252 + idGeneric, exists := h.AttributeString("id") 253 + if !exists { 254 + return // no id, nothing to do 255 + } 256 + id, ok := idGeneric.([]byte) 257 + if !ok { 258 + return 259 + } 260 + 261 + // create anchor link 262 + anchor := ast.NewLink() 263 + anchor.Destination = fmt.Appendf(nil, "#%s", string(id)) 264 + anchor.SetAttribute([]byte("class"), []byte("anchor")) 265 + 266 + // create icon text 267 + iconText := ast.NewString([]byte("#")) 268 + anchor.AppendChild(anchor, iconText) 269 + 270 + // set class on heading 271 + h.SetAttribute([]byte("class"), []byte("heading")) 272 + 273 + // append anchor to heading 274 + h.AppendChild(h, anchor) 275 + } 276 + 255 277 // actualPath decides when to join the file path with the 256 278 // current repository directory (essentially only when the link 257 279 // destination is relative. if it's absolute then we assume the ··· 271 293 } 272 294 return parsed.IsAbs() 273 295 } 296 + 297 + func isFragment(link string) bool { 298 + return strings.HasPrefix(link, "#") 299 + } 300 + 301 + func isMail(link string) bool { 302 + return strings.HasPrefix(link, "mailto:") 303 + }
+117
appview/pages/markup/sanitizer.go
··· 1 + package markup 2 + 3 + import ( 4 + "maps" 5 + "regexp" 6 + "slices" 7 + "strings" 8 + 9 + "github.com/alecthomas/chroma/v2" 10 + "github.com/microcosm-cc/bluemonday" 11 + ) 12 + 13 + type Sanitizer struct { 14 + defaultPolicy *bluemonday.Policy 15 + descriptionPolicy *bluemonday.Policy 16 + } 17 + 18 + func NewSanitizer() Sanitizer { 19 + return Sanitizer{ 20 + defaultPolicy: defaultPolicy(), 21 + descriptionPolicy: descriptionPolicy(), 22 + } 23 + } 24 + 25 + func (s *Sanitizer) SanitizeDefault(html string) string { 26 + return s.defaultPolicy.Sanitize(html) 27 + } 28 + func (s *Sanitizer) SanitizeDescription(html string) string { 29 + return s.descriptionPolicy.Sanitize(html) 30 + } 31 + 32 + func defaultPolicy() *bluemonday.Policy { 33 + policy := bluemonday.UGCPolicy() 34 + 35 + // Allow generally safe attributes 36 + generalSafeAttrs := []string{ 37 + "abbr", "accept", "accept-charset", 38 + "accesskey", "action", "align", "alt", 39 + "aria-describedby", "aria-hidden", "aria-label", "aria-labelledby", 40 + "axis", "border", "cellpadding", "cellspacing", "char", 41 + "charoff", "charset", "checked", 42 + "clear", "cols", "colspan", "color", 43 + "compact", "coords", "datetime", "dir", 44 + "disabled", "enctype", "for", "frame", 45 + "headers", "height", "hreflang", 46 + "hspace", "ismap", "label", "lang", 47 + "maxlength", "media", "method", 48 + "multiple", "name", "nohref", "noshade", 49 + "nowrap", "open", "prompt", "readonly", "rel", "rev", 50 + "rows", "rowspan", "rules", "scope", 51 + "selected", "shape", "size", "span", 52 + "start", "summary", "tabindex", "target", 53 + "title", "type", "usemap", "valign", "value", 54 + "vspace", "width", "itemprop", 55 + } 56 + 57 + generalSafeElements := []string{ 58 + "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt", 59 + "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label", 60 + "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary", 61 + "details", "caption", "figure", "figcaption", 62 + "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr", 63 + } 64 + 65 + policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) 66 + 67 + // video 68 + policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") 69 + 70 + // checkboxes 71 + policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") 72 + policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") 73 + 74 + // for code blocks 75 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma`)).OnElements("pre") 76 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a") 77 + policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8") 78 + policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span") 79 + 80 + // centering content 81 + policy.AllowElements("center") 82 + 83 + policy.AllowAttrs("align", "style", "width", "height").Globally() 84 + policy.AllowStyles( 85 + "margin", 86 + "padding", 87 + "text-align", 88 + "font-weight", 89 + "text-decoration", 90 + "padding-left", 91 + "padding-right", 92 + "padding-top", 93 + "padding-bottom", 94 + "margin-left", 95 + "margin-right", 96 + "margin-top", 97 + "margin-bottom", 98 + ) 99 + 100 + return policy 101 + } 102 + 103 + func descriptionPolicy() *bluemonday.Policy { 104 + policy := bluemonday.NewPolicy() 105 + policy.AllowStandardURLs() 106 + 107 + // allow italics and bold. 108 + policy.AllowElements("i", "b", "em", "strong") 109 + 110 + // allow code. 111 + policy.AllowElements("code") 112 + 113 + // allow links 114 + policy.AllowAttrs("href", "target", "rel").OnElements("a") 115 + 116 + return policy 117 + }
+133 -68
appview/pages/pages.go
··· 24 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 + "tangled.sh/tangled.sh/core/idresolver" 27 28 "tangled.sh/tangled.sh/core/patchutil" 28 29 "tangled.sh/tangled.sh/core/types" 29 30 ··· 45 46 t map[string]*template.Template 46 47 47 48 avatar config.AvatarConfig 49 + resolver *idresolver.Resolver 48 50 dev bool 49 51 embedFS embed.FS 50 52 templateDir string // Path to templates on disk for dev mode 51 53 rctx *markup.RenderContext 52 54 } 53 55 54 - func NewPages(config *config.Config) *Pages { 56 + func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { 55 57 // initialized with safe defaults, can be overriden per use 56 58 rctx := &markup.RenderContext{ 57 59 IsDev: config.Core.Dev, 58 60 CamoUrl: config.Camo.Host, 59 61 CamoSecret: config.Camo.SharedSecret, 62 + Sanitizer: markup.NewSanitizer(), 60 63 } 61 64 62 65 p := &Pages{ ··· 66 69 avatar: config.Avatar, 67 70 embedFS: Files, 68 71 rctx: rctx, 72 + resolver: res, 69 73 templateDir: "appview/pages", 70 74 } 71 75 ··· 256 260 return p.executeOrReload(name, w, "layouts/repobase", params) 257 261 } 258 262 263 + func (p *Pages) Favicon(w io.Writer) error { 264 + return p.executePlain("favicon", w, nil) 265 + } 266 + 259 267 type LoginParams struct { 268 + ReturnUrl string 260 269 } 261 270 262 271 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 290 299 type TimelineParams struct { 291 300 LoggedInUser *oauth.User 292 301 Timeline []db.TimelineEvent 293 - DidHandleMap map[string]string 302 + Repos []db.Repo 294 303 } 295 304 296 305 func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 297 - return p.execute("timeline", w, params) 306 + return p.execute("timeline/timeline", w, params) 298 307 } 299 308 300 - type SettingsParams struct { 309 + type UserProfileSettingsParams struct { 310 + LoggedInUser *oauth.User 311 + Tabs []map[string]any 312 + Tab string 313 + } 314 + 315 + func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 316 + return p.execute("user/settings/profile", w, params) 317 + } 318 + 319 + type UserKeysSettingsParams struct { 301 320 LoggedInUser *oauth.User 302 321 PubKeys []db.PublicKey 322 + Tabs []map[string]any 323 + Tab string 324 + } 325 + 326 + func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 327 + return p.execute("user/settings/keys", w, params) 328 + } 329 + 330 + type UserEmailsSettingsParams struct { 331 + LoggedInUser *oauth.User 303 332 Emails []db.Email 333 + Tabs []map[string]any 334 + Tab string 304 335 } 305 336 306 - func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 307 - return p.execute("settings", w, params) 337 + func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 338 + return p.execute("user/settings/emails", w, params) 339 + } 340 + 341 + type KnotBannerParams struct { 342 + Registrations []db.Registration 343 + } 344 + 345 + func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { 346 + return p.executePlain("knots/fragments/banner", w, params) 308 347 } 309 348 310 349 type KnotsParams struct { ··· 318 357 319 358 type KnotParams struct { 320 359 LoggedInUser *oauth.User 321 - DidHandleMap map[string]string 322 360 Registration *db.Registration 323 361 Members []string 324 362 Repos map[string][]db.Repo ··· 330 368 } 331 369 332 370 type KnotListingParams struct { 333 - db.Registration 371 + *db.Registration 334 372 } 335 373 336 374 func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { ··· 359 397 Spindle db.Spindle 360 398 Members []string 361 399 Repos map[string][]db.Repo 362 - DidHandleMap map[string]string 363 400 } 364 401 365 402 func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { ··· 385 422 return p.execute("repo/fork", w, params) 386 423 } 387 424 388 - type ProfilePageParams struct { 425 + type ProfileHomePageParams struct { 389 426 LoggedInUser *oauth.User 390 427 Repos []db.Repo 391 428 CollaboratingRepos []db.Repo 392 429 ProfileTimeline *db.ProfileTimeline 393 430 Card ProfileCard 394 431 Punchcard db.Punchcard 395 - 396 - DidHandleMap map[string]string 397 432 } 398 433 399 434 type ProfileCard struct { 400 - UserDid string 401 - UserHandle string 402 - FollowStatus db.FollowStatus 403 - Followers int 404 - Following int 435 + UserDid string 436 + UserHandle string 437 + FollowStatus db.FollowStatus 438 + FollowersCount int 439 + FollowingCount int 405 440 406 441 Profile *db.Profile 407 442 } 408 443 409 - func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 444 + func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 410 445 return p.execute("user/profile", w, params) 411 446 } 412 447 ··· 414 449 LoggedInUser *oauth.User 415 450 Repos []db.Repo 416 451 Card ProfileCard 417 - 418 - DidHandleMap map[string]string 419 452 } 420 453 421 454 func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 422 455 return p.execute("user/repos", w, params) 456 + } 457 + 458 + type FollowCard struct { 459 + UserDid string 460 + FollowStatus db.FollowStatus 461 + FollowersCount int 462 + FollowingCount int 463 + Profile *db.Profile 464 + } 465 + 466 + type FollowersPageParams struct { 467 + LoggedInUser *oauth.User 468 + Followers []FollowCard 469 + Card ProfileCard 470 + } 471 + 472 + func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 473 + return p.execute("user/followers", w, params) 474 + } 475 + 476 + type FollowingPageParams struct { 477 + LoggedInUser *oauth.User 478 + Following []FollowCard 479 + Card ProfileCard 480 + } 481 + 482 + func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 483 + return p.execute("user/following", w, params) 423 484 } 424 485 425 486 type FollowFragmentParams struct { ··· 444 505 LoggedInUser *oauth.User 445 506 Profile *db.Profile 446 507 AllRepos []PinnedRepo 447 - DidHandleMap map[string]string 448 508 } 449 509 450 510 type PinnedRepo struct { ··· 479 539 } 480 540 481 541 type RepoIndexParams struct { 482 - LoggedInUser *oauth.User 483 - RepoInfo repoinfo.RepoInfo 484 - Active string 485 - TagMap map[string][]string 486 - CommitsTrunc []*object.Commit 487 - TagsTrunc []*types.TagReference 488 - BranchesTrunc []types.Branch 489 - ForkInfo *types.ForkInfo 542 + LoggedInUser *oauth.User 543 + RepoInfo repoinfo.RepoInfo 544 + Active string 545 + TagMap map[string][]string 546 + CommitsTrunc []*object.Commit 547 + TagsTrunc []*types.TagReference 548 + BranchesTrunc []types.Branch 549 + // ForkInfo *types.ForkInfo 490 550 HTMLReadme template.HTML 491 551 Raw bool 492 552 EmailToDidOrHandle map[string]string ··· 503 563 } 504 564 505 565 p.rctx.RepoInfo = params.RepoInfo 566 + p.rctx.RepoInfo.Ref = params.Ref 506 567 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 507 568 508 569 if params.ReadmeFileName != "" { 509 - var htmlString string 510 570 ext := filepath.Ext(params.ReadmeFileName) 511 571 switch ext { 512 572 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 513 - htmlString = p.rctx.Sanitize(htmlString) 514 - htmlString = p.rctx.RenderMarkdown(params.Readme) 515 573 params.Raw = false 516 - params.HTMLReadme = template.HTML(htmlString) 574 + htmlString := p.rctx.RenderMarkdown(params.Readme) 575 + sanitized := p.rctx.SanitizeDefault(htmlString) 576 + params.HTMLReadme = template.HTML(sanitized) 517 577 default: 518 578 params.Raw = true 519 579 } ··· 652 712 p.rctx.RepoInfo = params.RepoInfo 653 713 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 654 714 htmlString := p.rctx.RenderMarkdown(params.Contents) 655 - params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 715 + sanitized := p.rctx.SanitizeDefault(htmlString) 716 + params.RenderedContents = template.HTML(sanitized) 656 717 } 657 718 } 658 719 659 - if params.Lines < 5000 { 660 - c := params.Contents 661 - formatter := chromahtml.New( 662 - chromahtml.InlineCode(false), 663 - chromahtml.WithLineNumbers(true), 664 - chromahtml.WithLinkableLineNumbers(true, "L"), 665 - chromahtml.Standalone(false), 666 - chromahtml.WithClasses(true), 667 - ) 668 - 669 - lexer := lexers.Get(filepath.Base(params.Path)) 670 - if lexer == nil { 671 - lexer = lexers.Fallback 672 - } 720 + c := params.Contents 721 + formatter := chromahtml.New( 722 + chromahtml.InlineCode(false), 723 + chromahtml.WithLineNumbers(true), 724 + chromahtml.WithLinkableLineNumbers(true, "L"), 725 + chromahtml.Standalone(false), 726 + chromahtml.WithClasses(true), 727 + ) 673 728 674 - iterator, err := lexer.Tokenise(nil, c) 675 - if err != nil { 676 - return fmt.Errorf("chroma tokenize: %w", err) 677 - } 729 + lexer := lexers.Get(filepath.Base(params.Path)) 730 + if lexer == nil { 731 + lexer = lexers.Fallback 732 + } 678 733 679 - var code bytes.Buffer 680 - err = formatter.Format(&code, style, iterator) 681 - if err != nil { 682 - return fmt.Errorf("chroma format: %w", err) 683 - } 734 + iterator, err := lexer.Tokenise(nil, c) 735 + if err != nil { 736 + return fmt.Errorf("chroma tokenize: %w", err) 737 + } 684 738 685 - params.Contents = code.String() 739 + var code bytes.Buffer 740 + err = formatter.Format(&code, style, iterator) 741 + if err != nil { 742 + return fmt.Errorf("chroma format: %w", err) 686 743 } 687 744 745 + params.Contents = code.String() 688 746 params.Active = "overview" 689 747 return p.executeRepo("repo/blob", w, params) 690 748 } ··· 763 821 RepoInfo repoinfo.RepoInfo 764 822 Active string 765 823 Issues []db.Issue 766 - DidHandleMap map[string]string 767 824 Page pagination.Page 768 825 FilteringByOpen bool 769 826 } ··· 777 834 LoggedInUser *oauth.User 778 835 RepoInfo repoinfo.RepoInfo 779 836 Active string 780 - Issue db.Issue 837 + Issue *db.Issue 781 838 Comments []db.Comment 782 839 IssueOwnerHandle string 783 - DidHandleMap map[string]string 784 840 785 841 OrderedReactionKinds []db.ReactionKind 786 842 Reactions map[db.ReactionKind]int ··· 834 890 835 891 type SingleIssueCommentParams struct { 836 892 LoggedInUser *oauth.User 837 - DidHandleMap map[string]string 838 893 RepoInfo repoinfo.RepoInfo 839 894 Issue *db.Issue 840 895 Comment *db.Comment ··· 866 921 RepoInfo repoinfo.RepoInfo 867 922 Pulls []*db.Pull 868 923 Active string 869 - DidHandleMap map[string]string 870 924 FilteringBy db.PullState 871 925 Stacks map[string]db.Stack 872 926 Pipelines map[string]db.Pipeline ··· 899 953 LoggedInUser *oauth.User 900 954 RepoInfo repoinfo.RepoInfo 901 955 Active string 902 - DidHandleMap map[string]string 903 956 Pull *db.Pull 904 957 Stack db.Stack 905 958 AbandonedPulls []*db.Pull ··· 919 972 920 973 type RepoPullPatchParams struct { 921 974 LoggedInUser *oauth.User 922 - DidHandleMap map[string]string 923 975 RepoInfo repoinfo.RepoInfo 924 976 Pull *db.Pull 925 977 Stack db.Stack ··· 937 989 938 990 type RepoPullInterdiffParams struct { 939 991 LoggedInUser *oauth.User 940 - DidHandleMap map[string]string 941 992 RepoInfo repoinfo.RepoInfo 942 993 Pull *db.Pull 943 994 Round int ··· 1150 1201 return p.execute("strings/dashboard", w, params) 1151 1202 } 1152 1203 1204 + type StringTimelineParams struct { 1205 + LoggedInUser *oauth.User 1206 + Strings []db.String 1207 + } 1208 + 1209 + func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1210 + return p.execute("strings/timeline", w, params) 1211 + } 1212 + 1153 1213 type SingleStringParams struct { 1154 1214 LoggedInUser *oauth.User 1155 1215 ShowRendered bool ··· 1166 1226 if params.ShowRendered { 1167 1227 switch markup.GetFormat(params.String.Filename) { 1168 1228 case markup.FormatMarkdown: 1169 - p.rctx.RendererType = markup.RendererTypeDefault 1229 + p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1170 1230 htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1171 - params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 1231 + sanitized := p.rctx.SanitizeDefault(htmlString) 1232 + params.RenderedContents = template.HTML(sanitized) 1172 1233 } 1173 1234 } 1174 1235 ··· 1251 1312 1252 1313 func (p *Pages) Error404(w io.Writer) error { 1253 1314 return p.execute("errors/404", w, nil) 1315 + } 1316 + 1317 + func (p *Pages) ErrorKnot404(w io.Writer) error { 1318 + return p.execute("errors/knot404", w, nil) 1254 1319 } 1255 1320 1256 1321 func (p *Pages) Error503(w io.Writer) error {
+24 -4
appview/pages/templates/errors/404.html
··· 1 1 {{ define "title" }}404 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <h1>404 &mdash; nothing like that here!</h1> 5 - <p> 6 - It seems we couldn't find what you were looking for. Sorry about that! 7 - </p> 4 + <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 + <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 + <div class="mb-6"> 7 + <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center"> 8 + {{ i "search-x" "w-8 h-8 text-gray-400 dark:text-gray-500" }} 9 + </div> 10 + </div> 11 + 12 + <div class="space-y-4"> 13 + <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 + 404 &mdash; page not found 15 + </h1> 16 + <p class="text-gray-600 dark:text-gray-300"> 17 + The page you're looking for doesn't exist. It may have been moved, deleted, or you have the wrong URL. 18 + </p> 19 + <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 + <a href="javascript:history.back()" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 21 + {{ i "arrow-left" "w-4 h-4" }} 22 + go back 23 + </a> 24 + </div> 25 + </div> 26 + </div> 27 + </div> 8 28 {{ end }}
+36 -3
appview/pages/templates/errors/500.html
··· 1 1 {{ define "title" }}500 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <h1>500 &mdash; something broke!</h1> 5 - <p>We're working on getting service back up. Hang tight!</p> 6 - {{ end }} 4 + <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 + <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 + <div class="mb-6"> 7 + <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"> 8 + {{ i "alert-triangle" "w-8 h-8 text-red-500 dark:text-red-400" }} 9 + </div> 10 + </div> 11 + 12 + <div class="space-y-4"> 13 + <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 + 500 &mdash; internal server error 15 + </h1> 16 + <p class="text-gray-600 dark:text-gray-300"> 17 + Something went wrong on our end. We've been notified and are working to fix the issue. 18 + </p> 19 + <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded p-3 text-sm text-yellow-800 dark:text-yellow-200"> 20 + <div class="flex items-center gap-2"> 21 + {{ i "info" "w-4 h-4" }} 22 + <span class="font-medium">we're on it!</span> 23 + </div> 24 + <p class="mt-1">Our team has been automatically notified about this error.</p> 25 + </div> 26 + <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 27 + <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white"> 28 + {{ i "refresh-cw" "w-4 h-4" }} 29 + try again 30 + </button> 31 + <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 32 + {{ i "home" "w-4 h-4" }} 33 + back to home 34 + </a> 35 + </div> 36 + </div> 37 + </div> 38 + </div> 39 + {{ end }}
+28 -5
appview/pages/templates/errors/503.html
··· 1 1 {{ define "title" }}503 &middot; tangled{{ end }} 2 2 3 3 {{ define "content" }} 4 - <h1>503 &mdash; unable to reach knot</h1> 5 - <p> 6 - We were unable to reach the knot hosting this repository. Try again 7 - later. 8 - </p> 4 + <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 + <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 + <div class="mb-6"> 7 + <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center"> 8 + {{ i "server-off" "w-8 h-8 text-blue-500 dark:text-blue-400" }} 9 + </div> 10 + </div> 11 + 12 + <div class="space-y-4"> 13 + <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 + 503 &mdash; service unavailable 15 + </h1> 16 + <p class="text-gray-600 dark:text-gray-300"> 17 + We were unable to reach the knot hosting this repository. The service may be temporarily unavailable. 18 + </p> 19 + <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 + <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white"> 21 + {{ i "refresh-cw" "w-4 h-4" }} 22 + try again 23 + </button> 24 + <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"> 25 + {{ i "arrow-left" "w-4 h-4" }} 26 + back to timeline 27 + </a> 28 + </div> 29 + </div> 30 + </div> 31 + </div> 9 32 {{ end }}
+28
appview/pages/templates/errors/knot404.html
··· 1 + {{ define "title" }}404 &middot; tangled{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="flex flex-col items-center justify-center min-h-[60vh] text-center"> 5 + <div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto"> 6 + <div class="mb-6"> 7 + <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center"> 8 + {{ i "book-x" "w-8 h-8 text-orange-500 dark:text-orange-400" }} 9 + </div> 10 + </div> 11 + 12 + <div class="space-y-4"> 13 + <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white"> 14 + 404 &mdash; repository not found 15 + </h1> 16 + <p class="text-gray-600 dark:text-gray-300"> 17 + The repository you were looking for could not be found. The knot serving the repository may be unavailable. 18 + </p> 19 + <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6"> 20 + <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline"> 21 + {{ i "arrow-left" "w-4 h-4" }} 22 + back to timeline 23 + </a> 24 + </div> 25 + </div> 26 + </div> 27 + </div> 28 + {{ end }}
+26
appview/pages/templates/favicon.html
··· 1 + {{ define "favicon" }} 2 + <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"> 3 + <style> 4 + .favicon-text { 5 + fill: #000000; 6 + stroke: none; 7 + } 8 + 9 + @media (prefers-color-scheme: dark) { 10 + .favicon-text { 11 + fill: #ffffff; 12 + stroke: none; 13 + } 14 + } 15 + </style> 16 + 17 + <g style="display:inline"> 18 + <path d="M0-2.117h62.177v25.135H0z" style="display:inline;fill:none;fill-opacity:1;stroke-width:.396875" transform="translate(11.01 6.9)"/> 19 + <path d="M3.64 22.787c-1.697 0-2.943-.45-3.74-1.35-.77-.9-1.156-2.094-1.156-3.585 0-.36.013-.72.038-1.08.052-.385.129-.873.232-1.464L.44 6.826h-5.089l.733-4.394h3.2c.822 0 1.439-.168 1.85-.502.437-.334.72-.938.848-1.812l.771-4.703h5.243L6.84 2.432h7.787l-.733 4.394H6.107L4.257 17.93l.77.27 6.015-4.742 2.775 3.161-2.313 2.005c-.822.694-1.568 1.31-2.236 1.85-.668.515-1.31.952-1.927 1.311a7.406 7.406 0 0 1-1.774.733c-.59.18-1.233.27-1.927.27z" 20 + aria-label="tangled.sh" 21 + class="favicon-text" 22 + style="font-size:16.2278px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Normal';display:inline;fill-opacity:1" 23 + transform="translate(11.01 6.9)"/> 24 + </g> 25 + </svg> 26 + {{ end }}
+13 -6
appview/pages/templates/knots/dashboard.html
··· 7 7 <div id="right-side" class="flex gap-2"> 8 8 {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }} 9 9 {{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }} 10 - {{ if .Registration.Registered }} 10 + {{ if .Registration.IsRegistered }} 11 11 <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span> 12 12 {{ if $isOwner }} 13 13 {{ template "knots/fragments/addMemberModal" .Registration }} 14 + {{ end }} 15 + {{ else if .Registration.IsReadOnly }} 16 + <span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}"> 17 + {{ i "shield-alert" "w-4 h-4" }} read-only 18 + </span> 19 + {{ if $isOwner }} 20 + {{ block "retryButton" .Registration }} {{ end }} 14 21 {{ end }} 15 22 {{ else }} 16 23 <span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}">{{ i "shield-off" "w-4 h-4" }} unverified</span> ··· 42 49 <div> 43 50 <div class="flex justify-between items-center"> 44 51 <div class="flex items-center gap-2"> 45 - {{ i "user" "size-4" }} 46 - {{ $user := index $.DidHandleMap . }} 47 - <a href="/{{ $user }}">{{ $user }}</a> 52 + {{ template "user/fragments/picHandleLink" . }} 53 + <span class="ml-2 font-mono text-gray-500">{{.}}</span> 48 54 </div> 49 55 {{ if ne $.LoggedInUser.Did . }} 50 56 {{ block "removeMemberButton" (list $ . ) }} {{ end }} ··· 55 61 {{ range $repos }} 56 62 <div class="flex gap-2 items-center"> 57 63 {{ i "book-marked" "size-4" }} 58 - <a href="/{{ .Did }}/{{ .Name }}"> 64 + <a href="/{{ resolve .Did }}/{{ .Name }}"> 59 65 {{ .Name }} 60 66 </a> 61 67 </div> ··· 103 109 {{ define "removeMemberButton" }} 104 110 {{ $root := index . 0 }} 105 111 {{ $member := index . 1 }} 112 + {{ $memberHandle := resolve $member }} 106 113 <button 107 114 class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group" 108 115 title="Remove member" 109 116 hx-post="/knots/{{ $root.Registration.Domain }}/remove" 110 117 hx-swap="none" 111 118 hx-vals='{"member": "{{$member}}" }' 112 - hx-confirm="Are you sure you want to remove {{ index $root.DidHandleMap $member }} from this knot?" 119 + hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?" 113 120 > 114 121 {{ i "user-minus" "w-4 h-4" }} 115 122 remove
+9
appview/pages/templates/knots/fragments/banner.html
··· 1 + {{ define "knots/fragments/banner" }} 2 + <div class="w-full px-6 py-2 -z-15 bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm"> 3 + A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }}) 4 + that you administer is presently read-only. Consider upgrading this knot to 5 + continue creating repositories on it. 6 + <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/migrations/knot-1.7.0.md">Click to read the upgrade guide</a>. 7 + </div> 8 + {{ end }} 9 +
+18 -5
appview/pages/templates/knots/fragments/knotListing.html
··· 9 9 {{ if .Registered }} 10 10 <a href="/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]"> 11 11 {{ i "hard-drive" "w-4 h-4" }} 12 - {{ .Domain }} 12 + <span class="hover:underline"> 13 + {{ .Domain }} 14 + </span> 13 15 <span class="text-gray-500"> 14 16 {{ template "repo/fragments/shortTimeAgo" .Created }} 15 17 </span> ··· 28 30 {{ define "knotRightSide" }} 29 31 <div id="right-side" class="flex gap-2"> 30 32 {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }} 31 - {{ if .Registered }} 32 - <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span> 33 + {{ if .IsRegistered }} 34 + <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}"> 35 + {{ i "shield-check" "w-4 h-4" }} verified 36 + </span> 33 37 {{ template "knots/fragments/addMemberModal" . }} 38 + {{ block "knotDeleteButton" . }} {{ end }} 39 + {{ else if .IsReadOnly }} 40 + <span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}"> 41 + {{ i "shield-alert" "w-4 h-4" }} read-only 42 + </span> 43 + {{ block "knotRetryButton" . }} {{ end }} 44 + {{ block "knotDeleteButton" . }} {{ end }} 34 45 {{ else }} 35 - <span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}">{{ i "shield-off" "w-4 h-4" }} unverified</span> 46 + <span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}"> 47 + {{ i "shield-off" "w-4 h-4" }} unverified 48 + </span> 36 49 {{ block "knotRetryButton" . }} {{ end }} 50 + {{ block "knotDeleteButton" . }} {{ end }} 37 51 {{ end }} 38 - {{ block "knotDeleteButton" . }} {{ end }} 39 52 </div> 40 53 {{ end }} 41 54
+1 -1
appview/pages/templates/knots/index.html
··· 77 77 </button> 78 78 </div> 79 79 80 - <div id="registration-error" class="error dark:text-red-400"></div> 80 + <div id="register-error" class="error dark:text-red-400"></div> 81 81 </form> 82 82 83 83 </section>
-12
appview/pages/templates/layouts/base.html
··· 24 24 {{ block "mainLayout" . }} 25 25 <div class="px-1 col-span-1 md:col-start-3 md:col-span-8 flex flex-col gap-4"> 26 26 {{ block "contentLayout" . }} 27 - <div class="col-span-1 md:col-span-2"> 28 - {{ block "contentLeft" . }} {{ end }} 29 - </div> 30 27 <main class="col-span-1 md:col-span-8"> 31 28 {{ block "content" . }}{{ end }} 32 29 </main> 33 - <div class="col-span-1 md:col-span-2"> 34 - {{ block "contentRight" . }} {{ end }} 35 - </div> 36 30 {{ end }} 37 31 38 32 {{ block "contentAfterLayout" . }} 39 - <div class="col-span-1 md:col-span-2"> 40 - {{ block "contentAfterLeft" . }} {{ end }} 41 - </div> 42 33 <main class="col-span-1 md:col-span-8"> 43 34 {{ block "contentAfter" . }}{{ end }} 44 35 </main> 45 - <div class="col-span-1 md:col-span-2"> 46 - {{ block "contentAfterRight" . }} {{ end }} 47 - </div> 48 36 {{ end }} 49 37 </div> 50 38 {{ end }}
+16 -21
appview/pages/templates/layouts/repobase.html
··· 5 5 {{ if .RepoInfo.Source }} 6 6 <p class="text-sm"> 7 7 <div class="flex items-center"> 8 - {{ i "git-fork" "w-3 h-3 mr-1"}} 8 + {{ i "git-fork" "w-3 h-3 mr-1 shrink-0" }} 9 9 forked from 10 10 {{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }} 11 11 <a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a> ··· 20 20 </div> 21 21 22 22 <div class="flex items-center gap-2 z-auto"> 23 + <a 24 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 25 + href="/{{ .RepoInfo.FullName }}/feed.atom" 26 + > 27 + {{ i "rss" "size-4" }} 28 + </a> 23 29 {{ template "repo/fragments/repoStar" .RepoInfo }} 24 - {{ if .RepoInfo.DisableFork }} 25 - <button 26 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" 27 - disabled 28 - title="Empty repositories cannot be forked" 29 - > 30 - {{ i "git-fork" "w-4 h-4" }} 31 - fork 32 - </button> 33 - {{ else }} 34 - <a 35 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 36 - hx-boost="true" 37 - href="/{{ .RepoInfo.FullName }}/fork" 38 - > 39 - {{ i "git-fork" "w-4 h-4" }} 40 - fork 41 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 42 - </a> 43 - {{ end }} 30 + <a 31 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 32 + hx-boost="true" 33 + href="/{{ .RepoInfo.FullName }}/fork" 34 + > 35 + {{ i "git-fork" "w-4 h-4" }} 36 + fork 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 38 + </a> 44 39 </div> 45 40 </div> 46 41 {{ template "repo/fragments/repoDescription" . }}
+8 -1
appview/pages/templates/layouts/topbar.html
··· 2 2 <nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm"> 3 3 <div class="flex justify-between p-0 items-center"> 4 4 <div id="left-items"> 5 - <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic"> 5 + <a href="/" hx-boost="true" class="flex gap-2 font-bold italic"> 6 6 tangled<sub>alpha</sub> 7 7 </a> 8 8 </div> ··· 21 21 </div> 22 22 </div> 23 23 </nav> 24 + {{ if .LoggedInUser }} 25 + <div id="upgrade-banner" 26 + hx-get="/knots/upgradeBanner" 27 + hx-trigger="load" 28 + hx-swap="innerHTML"> 29 + </div> 30 + {{ end }} 24 31 {{ end }} 25 32 26 33 {{ define "newButton" }}
+1 -1
appview/pages/templates/repo/commit.html
··· 118 118 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 119 119 {{ template "repo/fragments/diffOpts" .DiffOpts }} 120 120 </div> 121 - <div class="sticky top-0 flex-grow max-h-screen"> 121 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 122 122 {{ template "repo/fragments/diffChangedFiles" .Diff }} 123 123 </div> 124 124 {{end}}
+1 -1
appview/pages/templates/repo/compare/compare.html
··· 49 49 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 50 50 {{ template "repo/fragments/diffOpts" .DiffOpts }} 51 51 </div> 52 - <div class="sticky top-0 flex-grow max-h-screen"> 52 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 53 53 {{ template "repo/fragments/diffChangedFiles" .Diff }} 54 54 </div> 55 55 {{end}}
-4
appview/pages/templates/repo/empty.html
··· 44 44 {{ end }} 45 45 </main> 46 46 {{ end }} 47 - 48 - {{ define "repoAfter" }} 49 - {{ template "repo/fragments/cloneInstructions" . }} 50 - {{ end }}
+8 -2
appview/pages/templates/repo/fork.html
··· 5 5 <p class="text-xl font-bold dark:text-white">Fork {{ .RepoInfo.FullName }}</p> 6 6 </div> 7 7 <div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded"> 8 - <form hx-post="/{{ .RepoInfo.FullName }}/fork" class="space-y-12" hx-swap="none"> 8 + <form hx-post="/{{ .RepoInfo.FullName }}/fork" class="space-y-12" hx-swap="none" hx-indicator="#spinner"> 9 9 <fieldset class="space-y-3"> 10 10 <legend class="dark:text-white">Select a knot to fork into</legend> 11 11 <div class="space-y-2"> ··· 30 30 </fieldset> 31 31 32 32 <div class="space-y-2"> 33 - <button type="submit" class="btn">fork repo</button> 33 + <button type="submit" class="btn-create flex items-center gap-2"> 34 + {{ i "git-fork" "w-4 h-4" }} 35 + fork repo 36 + <span id="spinner" class="group"> 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 38 + </span> 39 + </button> 34 40 <div id="repo" class="error"></div> 35 41 </div> 36 42 </form>
+104
appview/pages/templates/repo/fragments/cloneDropdown.html
··· 1 + {{ define "repo/fragments/cloneDropdown" }} 2 + {{ $knot := .RepoInfo.Knot }} 3 + {{ if eq $knot "knot1.tangled.sh" }} 4 + {{ $knot = "tangled.sh" }} 5 + {{ end }} 6 + 7 + <details id="clone-dropdown" class="relative inline-block text-left group"> 8 + <summary class="btn-create cursor-pointer list-none flex items-center gap-2"> 9 + {{ i "download" "w-4 h-4" }} 10 + <span class="hidden md:inline">code</span> 11 + <span class="group-open:hidden"> 12 + {{ i "chevron-down" "w-4 h-4" }} 13 + </span> 14 + <span class="hidden group-open:flex"> 15 + {{ i "chevron-up" "w-4 h-4" }} 16 + </span> 17 + </summary> 18 + 19 + <div class="absolute right-0 mt-2 w-96 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 drop-shadow-sm dark:text-white z-[9999]"> 20 + <div class="p-4"> 21 + <div class="mb-3"> 22 + <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Clone this repository</h3> 23 + </div> 24 + 25 + <!-- HTTPS Clone --> 26 + <div class="mb-3"> 27 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">HTTPS</label> 28 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 29 + <code 30 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 31 + onclick="window.getSelection().selectAllChildren(this)" 32 + data-url="https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}" 33 + >https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code> 34 + <button 35 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 36 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 37 + title="Copy to clipboard" 38 + > 39 + {{ i "copy" "w-4 h-4" }} 40 + </button> 41 + </div> 42 + </div> 43 + 44 + <!-- SSH Clone --> 45 + <div class="mb-3"> 46 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label> 47 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 48 + <code 49 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 50 + onclick="window.getSelection().selectAllChildren(this)" 51 + data-url="git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}" 52 + >git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code> 53 + <button 54 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 55 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 56 + title="Copy to clipboard" 57 + > 58 + {{ i "copy" "w-4 h-4" }} 59 + </button> 60 + </div> 61 + </div> 62 + 63 + <!-- Note for self-hosted --> 64 + <p class="text-xs text-gray-500 dark:text-gray-400"> 65 + For self-hosted knots, clone URLs may differ based on your setup. 66 + </p> 67 + 68 + <!-- Download Archive --> 69 + <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700"> 70 + <a 71 + href="/{{ .RepoInfo.FullName }}/archive/{{ .Ref | urlquery }}" 72 + class="flex items-center gap-2 px-3 py-2 text-sm" 73 + > 74 + {{ i "download" "w-4 h-4" }} 75 + Download tar.gz 76 + </a> 77 + </div> 78 + 79 + </div> 80 + </div> 81 + </details> 82 + 83 + <script> 84 + function copyToClipboard(button, text) { 85 + navigator.clipboard.writeText(text).then(() => { 86 + const originalContent = button.innerHTML; 87 + button.innerHTML = `{{ i "check" "w-4 h-4" }}`; 88 + setTimeout(() => { 89 + button.innerHTML = originalContent; 90 + }, 2000); 91 + }); 92 + } 93 + 94 + // Close clone dropdown when clicking outside 95 + document.addEventListener('click', function(event) { 96 + const cloneDropdown = document.getElementById('clone-dropdown'); 97 + if (cloneDropdown && cloneDropdown.hasAttribute('open')) { 98 + if (!cloneDropdown.contains(event.target)) { 99 + cloneDropdown.removeAttribute('open'); 100 + } 101 + } 102 + }); 103 + </script> 104 + {{ end }}
-55
appview/pages/templates/repo/fragments/cloneInstructions.html
··· 1 - {{ define "repo/fragments/cloneInstructions" }} 2 - {{ $knot := .RepoInfo.Knot }} 3 - {{ if eq $knot "knot1.tangled.sh" }} 4 - {{ $knot = "tangled.sh" }} 5 - {{ end }} 6 - <section 7 - class="mt-4 p-6 rounded drop-shadow-sm bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4" 8 - > 9 - <div class="flex flex-col gap-2"> 10 - <strong>push</strong> 11 - <div class="md:pl-4 overflow-x-auto whitespace-nowrap"> 12 - <code class="dark:text-gray-100" 13 - >git remote add origin 14 - git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 15 - > 16 - </div> 17 - </div> 18 - 19 - <div class="flex flex-col gap-2"> 20 - <strong>clone</strong> 21 - <div class="md:pl-4 flex flex-col gap-2"> 22 - <div class="flex items-center gap-3"> 23 - <span 24 - class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white" 25 - >HTTP</span 26 - > 27 - <div class="overflow-x-auto whitespace-nowrap flex-1"> 28 - <code class="dark:text-gray-100" 29 - >git clone 30 - https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code 31 - > 32 - </div> 33 - </div> 34 - 35 - <div class="flex items-center gap-3"> 36 - <span 37 - class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white" 38 - >SSH</span 39 - > 40 - <div class="overflow-x-auto whitespace-nowrap flex-1"> 41 - <code class="dark:text-gray-100" 42 - >git clone 43 - git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 44 - > 45 - </div> 46 - </div> 47 - </div> 48 - </div> 49 - 50 - <p class="py-2 text-gray-500 dark:text-gray-400"> 51 - Note that for self-hosted knots, clone URLs may be different based 52 - on your setup. 53 - </p> 54 - </section> 55 - {{ end }}
+4 -4
appview/pages/templates/repo/fragments/fileTree.html
··· 3 3 <details open> 4 4 <summary class="cursor-pointer list-none pt-1"> 5 5 <span class="tree-directory inline-flex items-center gap-2 "> 6 - {{ i "folder" "size-4 fill-current" }} 7 - <span class="filename text-black dark:text-white">{{ .Name }}</span> 6 + {{ i "folder" "flex-shrink-0 size-4 fill-current" }} 7 + <span class="filename truncate text-black dark:text-white">{{ .Name }}</span> 8 8 </span> 9 9 </summary> 10 10 <div class="ml-1 pl-2 border-l border-gray-200 dark:border-gray-700"> ··· 15 15 </details> 16 16 {{ else if .Name }} 17 17 <div class="tree-file flex items-center gap-2 pt-1"> 18 - {{ i "file" "size-4" }} 19 - <a href="#file-{{ .Path }}" class="filename text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 18 + {{ i "file" "flex-shrink-0 size-4" }} 19 + <a href="#file-{{ .Path }}" class="filename truncate text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 20 20 </div> 21 21 {{ else }} 22 22 {{ range $child := .Children }}
+1 -1
appview/pages/templates/repo/fragments/interdiffFiles.html
··· 1 1 {{ define "repo/fragments/interdiffFiles" }} 2 2 {{ $fileTree := fileTree .AffectedFiles }} 3 - <section class="mt-4 px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 3 + <section class="px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 4 4 <div class="diff-stat"> 5 5 <div class="flex gap-2 items-center"> 6 6 <strong class="text-sm uppercase dark:text-gray-200">files</strong>
+1 -1
appview/pages/templates/repo/fragments/repoDescription.html
··· 1 1 {{ define "repo/fragments/repoDescription" }} 2 2 <span id="repo-description" class="flex flex-wrap items-center gap-2 text-sm" hx-target="this" hx-swap="outerHTML"> 3 3 {{ if .RepoInfo.Description }} 4 - {{ .RepoInfo.Description }} 4 + {{ .RepoInfo.Description | description }} 5 5 {{ else }} 6 6 <span class="italic">this repo has no description</span> 7 7 {{ end }}
+91 -109
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-4"> 18 - <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1"> 17 + <div class="flex md:hidden items-center gap-2"> 18 + <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold"> 19 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 20 20 </a> 21 - <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex items-center text-sm gap-1"> 21 + <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex items-center text-sm gap-1 font-bold"> 22 22 {{ i "git-branch" "w-4" "h-4" }} {{ len .Branches }} 23 23 </a> 24 - <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex items-center text-sm gap-1"> 24 + <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex items-center text-sm gap-1 font-bold"> 25 25 {{ i "tags" "w-4" "h-4" }} {{ len .Tags }} 26 26 </a> 27 + {{ template "repo/fragments/cloneDropdown" . }} 27 28 </div> 28 29 </div> 29 30 <div class="grid grid-cols-1 md:grid-cols-2 gap-2"> ··· 47 48 48 49 49 50 {{ define "branchSelector" }} 50 - <div class="flex gap-2 items-center items-stretch justify-center"> 51 - <select 52 - onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 53 - class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 54 - > 55 - <optgroup label="branches ({{len .Branches}})" class="bold text-sm"> 56 - {{ range .Branches }} 57 - <option 58 - value="{{ .Reference.Name }}" 59 - class="py-1" 60 - {{ if eq .Reference.Name $.Ref }} 61 - selected 62 - {{ end }} 63 - > 64 - {{ .Reference.Name }} 65 - </option> 66 - {{ end }} 67 - </optgroup> 68 - <optgroup label="tags ({{len .Tags}})" class="bold text-sm"> 69 - {{ range .Tags }} 70 - <option 71 - value="{{ .Reference.Name }}" 72 - class="py-1" 73 - {{ if eq .Reference.Name $.Ref }} 74 - selected 75 - {{ end }} 76 - > 77 - {{ .Reference.Name }} 78 - </option> 79 - {{ else }} 80 - <option class="py-1" disabled>no tags found</option> 81 - {{ end }} 82 - </optgroup> 83 - </select> 84 - <div class="flex items-center gap-2"> 85 - {{ $isOwner := and .LoggedInUser .RepoInfo.Roles.IsOwner }} 86 - {{ $isCollaborator := and .LoggedInUser .RepoInfo.Roles.IsCollaborator }} 87 - {{ if and (or $isOwner $isCollaborator) .ForkInfo .ForkInfo.IsFork }} 88 - {{ $disabled := "" }} 89 - {{ $title := "" }} 90 - {{ if eq .ForkInfo.Status 0 }} 91 - {{ $disabled = "disabled" }} 92 - {{ $title = "This branch is not behind the upstream" }} 93 - {{ else if eq .ForkInfo.Status 2 }} 94 - {{ $disabled = "disabled" }} 95 - {{ $title = "This branch has conflicts that must be resolved" }} 96 - {{ else if eq .ForkInfo.Status 3 }} 97 - {{ $disabled = "disabled" }} 98 - {{ $title = "This branch does not exist on the upstream" }} 99 - {{ end }} 51 + <div class="flex gap-2 items-center justify-between w-full"> 52 + <div class="flex gap-2 items-center"> 53 + <select 54 + onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 55 + class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 56 + > 57 + <optgroup label="branches ({{len .Branches}})" class="bold text-sm"> 58 + {{ range .Branches }} 59 + <option 60 + value="{{ .Reference.Name }}" 61 + class="py-1" 62 + {{ if eq .Reference.Name $.Ref }} 63 + selected 64 + {{ end }} 65 + > 66 + {{ .Reference.Name }} 67 + </option> 68 + {{ end }} 69 + </optgroup> 70 + <optgroup label="tags ({{len .Tags}})" class="bold text-sm"> 71 + {{ range .Tags }} 72 + <option 73 + value="{{ .Reference.Name }}" 74 + class="py-1" 75 + {{ if eq .Reference.Name $.Ref }} 76 + selected 77 + {{ end }} 78 + > 79 + {{ .Reference.Name }} 80 + </option> 81 + {{ else }} 82 + <option class="py-1" disabled>no tags found</option> 83 + {{ end }} 84 + </optgroup> 85 + </select> 86 + <div class="flex items-center gap-2"> 87 + <a 88 + href="/{{ .RepoInfo.FullName }}/compare?base={{ $.Ref | urlquery }}" 89 + class="btn flex items-center gap-2 no-underline hover:no-underline" 90 + title="Compare branches or tags" 91 + > 92 + {{ i "git-compare" "w-4 h-4" }} 93 + </a> 94 + </div> 95 + </div> 100 96 101 - <button 102 - id="syncBtn" 103 - {{ $disabled }} 104 - {{ if $title }}title="{{ $title }}"{{ end }} 105 - class="btn flex gap-2 items-center disabled:opacity-50 disabled:cursor-not-allowed" 106 - hx-post="/{{ .RepoInfo.FullName }}/fork/sync" 107 - hx-trigger="click" 108 - hx-swap="none" 109 - > 110 - {{ if $disabled }} 111 - {{ i "refresh-cw-off" "w-4 h-4" }} 112 - {{ else }} 113 - {{ i "refresh-cw" "w-4 h-4" }} 114 - {{ end }} 115 - <span>sync</span> 116 - </button> 117 - {{ end }} 118 - <a 119 - href="/{{ .RepoInfo.FullName }}/compare?base={{ $.Ref | urlquery }}" 120 - class="btn flex items-center gap-2 no-underline hover:no-underline" 121 - title="Compare branches or tags" 122 - > 123 - {{ i "git-compare" "w-4 h-4" }} 124 - </a> 97 + <!-- Clone dropdown in top right --> 98 + <div class="hidden md:flex items-center "> 99 + {{ template "repo/fragments/cloneDropdown" . }} 125 100 </div> 126 - </div> 101 + </div> 127 102 {{ end }} 128 103 129 104 {{ define "fileTree" }} ··· 131 106 {{ $linkstyle := "no-underline hover:underline dark:text-white" }} 132 107 133 108 {{ range .Files }} 134 - <div class="grid grid-cols-2 gap-4 items-center py-1"> 135 - <div class="col-span-1"> 109 + <div class="grid grid-cols-3 gap-4 items-center py-1"> 110 + <div class="col-span-2"> 136 111 {{ $link := printf "/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) .Name }} 137 112 {{ $icon := "folder" }} 138 113 {{ $iconStyle := "size-4 fill-current" }} ··· 144 119 {{ end }} 145 120 <a href="{{ $link }}" class="{{ $linkstyle }}"> 146 121 <div class="flex items-center gap-2"> 147 - {{ i $icon $iconStyle }}{{ .Name }} 122 + {{ i $icon $iconStyle "flex-shrink-0" }} 123 + <span class="truncate">{{ .Name }}</span> 148 124 </div> 149 125 </a> 150 126 </div> 151 127 152 - <div class="text-xs col-span-1 text-right"> 128 + <div class="text-sm col-span-1 text-right"> 153 129 {{ with .LastCommit }} 154 130 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash }}" class="text-gray-500 dark:text-gray-400">{{ template "repo/fragments/time" .When }}</a> 155 131 {{ end }} ··· 210 186 </div> 211 187 212 188 <!-- commit info bar --> 213 - <div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center"> 189 + <div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap"> 214 190 {{ $verified := $.VerifiedCommits.IsVerified .Hash.String }} 215 191 {{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }} 216 192 {{ if $verified }} ··· 280 256 </a> 281 257 <div class="flex flex-col gap-1"> 282 258 {{ range .BranchesTrunc }} 283 - <div class="text-base flex items-center justify-between"> 284 - <div class="flex items-center gap-2"> 259 + <div class="text-base flex items-center justify-between overflow-hidden"> 260 + <div class="flex items-center gap-2 min-w-0 flex-1"> 285 261 <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Reference.Name | urlquery }}" 286 - class="inline no-underline hover:underline dark:text-white"> 262 + class="inline-block truncate no-underline hover:underline dark:text-white"> 287 263 {{ .Reference.Name }} 288 264 </a> 289 265 {{ if .Commit }} 290 - <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 291 - <span class="text-xs text-gray-500 dark:text-gray-400">{{ template "repo/fragments/time" .Commit.Committer.When }}</span> 266 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท'] shrink-0"></span> 267 + <span class="whitespace-nowrap text-xs text-gray-500 dark:text-gray-400 shrink-0">{{ template "repo/fragments/time" .Commit.Committer.When }}</span> 292 268 {{ end }} 293 269 {{ if .IsDefault }} 294 - <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 295 - <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-xs font-mono">default</span> 270 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท'] shrink-0"></span> 271 + <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-xs font-mono shrink-0">default</span> 296 272 {{ end }} 297 273 </div> 298 274 {{ if ne $.Ref .Reference.Name }} 299 275 <a href="/{{ $.RepoInfo.FullName }}/compare/{{ $.Ref | urlquery }}...{{ .Reference.Name | urlquery }}" 300 - class="text-xs flex gap-2 items-center" 276 + class="text-xs flex gap-2 items-center shrink-0 ml-2" 301 277 title="Compare branches or tags"> 302 278 {{ i "git-compare" "w-3 h-3" }} compare 303 279 </a> 304 - {{end}} 280 + {{ end }} 305 281 </div> 306 282 {{ end }} 307 283 </div> ··· 347 323 348 324 {{ define "repoAfter" }} 349 325 {{- if or .HTMLReadme .Readme -}} 350 - <section 351 - class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto overflow-auto {{ if not .Raw }} 352 - prose dark:prose-invert dark:[&_pre]:bg-gray-900 353 - dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 354 - dark:[&_pre]:border dark:[&_pre]:border-gray-700 355 - {{ end }}" 356 - > 357 - <article class="{{ if .Raw }}whitespace-pre{{ end }}">{{- if .Raw -}}<pre class="dark:bg-gray-800 dark:text-white overflow-x-auto"> 358 - {{- .Readme -}} 359 - </pre> 360 - {{- else -}} 361 - {{ .HTMLReadme }} 362 - {{- end -}}</article> 363 - </section> 326 + <div class="mt-4 rounded bg-white dark:bg-gray-800 drop-shadow-sm w-full mx-auto overflow-hidden"> 327 + {{- if .ReadmeFileName -}} 328 + <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2"> 329 + {{ i "file-text" "w-4 h-4" "text-gray-600 dark:text-gray-400" }} 330 + <span class="font-mono text-sm text-gray-800 dark:text-gray-200">{{ .ReadmeFileName }}</span> 331 + </div> 332 + {{- end -}} 333 + <section 334 + class="p-6 overflow-auto {{ if not .Raw }} 335 + prose dark:prose-invert dark:[&_pre]:bg-gray-900 336 + dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 337 + dark:[&_pre]:border dark:[&_pre]:border-gray-700 338 + {{ end }}" 339 + > 340 + <article class="{{ if .Raw }}whitespace-pre{{ end }}">{{- if .Raw -}}<pre class="dark:bg-gray-800 dark:text-white overflow-x-auto"> 341 + {{- .Readme -}} 342 + </pre> 343 + {{- else -}} 344 + {{ .HTMLReadme }} 345 + {{- end -}}</article> 346 + </section> 347 + </div> 364 348 {{- end -}} 365 - 366 - {{ template "repo/fragments/cloneInstructions" . }} 367 349 {{ end }}
+1 -2
appview/pages/templates/repo/issues/fragments/issueComment.html
··· 2 2 {{ with .Comment }} 3 3 <div id="comment-container-{{.CommentId}}"> 4 4 <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm flex-wrap"> 5 - {{ $owner := index $.DidHandleMap .OwnerDid }} 6 - {{ template "user/fragments/picHandleLink" $owner }} 5 + {{ template "user/fragments/picHandleLink" .OwnerDid }} 7 6 8 7 <!-- show user "hats" --> 9 8 {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
+3 -3
appview/pages/templates/repo/issues/issue.html
··· 11 11 {{ define "repoContent" }} 12 12 <header class="pb-4"> 13 13 <h1 class="text-2xl"> 14 - {{ .Issue.Title }} 14 + {{ .Issue.Title | description }} 15 15 <span class="text-gray-500 dark:text-gray-400">#{{ .Issue.IssueId }}</span> 16 16 </h1> 17 17 </header> ··· 54 54 "Kind" $kind 55 55 "Count" (index $.Reactions $kind) 56 56 "IsReacted" (index $.UserReacted $kind) 57 - "ThreadAt" $.Issue.IssueAt) 57 + "ThreadAt" $.Issue.AtUri) 58 58 }} 59 59 {{ end }} 60 60 </div> ··· 70 70 {{ if gt $index 0 }} 71 71 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 72 72 {{ end }} 73 - {{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}} 73 + {{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "Issue" $.Issue "Comment" .)}} 74 74 </div> 75 75 {{ end }} 76 76 </section>
+2 -3
appview/pages/templates/repo/issues/issues.html
··· 45 45 href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" 46 46 class="no-underline hover:underline" 47 47 > 48 - {{ .Title }} 48 + {{ .Title | description }} 49 49 <span class="text-gray-500">#{{ .IssueId }}</span> 50 50 </a> 51 51 </div> ··· 65 65 </span> 66 66 67 67 <span class="ml-1"> 68 - {{ $owner := index $.DidHandleMap .OwnerDid }} 69 - {{ template "user/fragments/picHandleLink" $owner }} 68 + {{ template "user/fragments/picHandleLink" .OwnerDid }} 70 69 </span> 71 70 72 71 <span class="before:content-['ยท']">
+1 -1
appview/pages/templates/repo/new.html
··· 63 63 <button type="submit" class="btn-create flex items-center gap-2"> 64 64 {{ i "book-plus" "w-4 h-4" }} 65 65 create repo 66 - <span id="create-pull-spinner" class="group"> 66 + <span id="spinner" class="group"> 67 67 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 68 68 </span> 69 69 </button>
+2 -2
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
··· 23 23 </div> 24 24 {{ else if $allFail }} 25 25 <div class="flex gap-1 items-center"> 26 - {{ i "x" "size-4 text-red-600" }} 26 + {{ i "x" "size-4 text-red-500" }} 27 27 <span>0/{{ $total }}</span> 28 28 </div> 29 29 {{ else if $allTimeout }} 30 30 <div class="flex gap-1 items-center"> 31 - {{ i "clock-alert" "size-4 text-orange-400" }} 31 + {{ i "clock-alert" "size-4 text-orange-500" }} 32 32 <span>0/{{ $total }}</span> 33 33 </div> 34 34 {{ else }}
+1 -1
appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html
··· 19 19 {{ $color = "text-gray-600 dark:text-gray-500" }} 20 20 {{ else if eq $kind "timeout" }} 21 21 {{ $icon = "clock-alert" }} 22 - {{ $color = "text-orange-400 dark:text-orange-300" }} 22 + {{ $color = "text-orange-400 dark:text-orange-500" }} 23 23 {{ else }} 24 24 {{ $icon = "x" }} 25 25 {{ $color = "text-red-600 dark:text-red-500" }}
+3 -3
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 1 1 {{ define "repo/pulls/fragments/pullHeader" }} 2 2 <header class="pb-4"> 3 3 <h1 class="text-2xl dark:text-white"> 4 - {{ .Pull.Title }} 4 + {{ .Pull.Title | description }} 5 5 <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span> 6 6 </h1> 7 7 </header> ··· 17 17 {{ $icon = "git-merge" }} 18 18 {{ end }} 19 19 20 + {{ $owner := resolve .Pull.OwnerDid }} 20 21 <section class="mt-2"> 21 22 <div class="flex items-center gap-2"> 22 23 <div ··· 28 29 </div> 29 30 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 30 31 opened by 31 - {{ $owner := index $.DidHandleMap .Pull.OwnerDid }} 32 - {{ template "user/fragments/picHandleLink" $owner }} 32 + {{ template "user/fragments/picHandleLink" .Pull.OwnerDid }} 33 33 <span class="select-none before:content-['\00B7']"></span> 34 34 {{ template "repo/fragments/time" .Pull.Created }} 35 35
+1 -1
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 9 9 </div> 10 10 <span class="truncate text-sm text-gray-800 dark:text-gray-200"> 11 11 <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 12 - {{ .Title }} 12 + {{ .Title | description }} 13 13 </span> 14 14 </div> 15 15
+1 -1
appview/pages/templates/repo/pulls/interdiff.html
··· 68 68 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 69 69 {{ template "repo/fragments/diffOpts" .DiffOpts }} 70 70 </div> 71 - <div class="sticky top-0 flex-grow max-h-screen"> 71 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 72 72 {{ template "repo/fragments/interdiffFiles" .Interdiff }} 73 73 </div> 74 74 {{end}}
+1 -1
appview/pages/templates/repo/pulls/patch.html
··· 73 73 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 74 74 {{ template "repo/fragments/diffOpts" .DiffOpts }} 75 75 </div> 76 - <div class="sticky top-0 flex-grow max-h-screen"> 76 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 77 77 {{ template "repo/fragments/diffChangedFiles" .Diff }} 78 78 </div> 79 79 {{end}}
+4 -5
appview/pages/templates/repo/pulls/pull.html
··· 47 47 <!-- round summary --> 48 48 <div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400"> 49 49 <span class="gap-1 flex items-center"> 50 - {{ $owner := index $.DidHandleMap $.Pull.OwnerDid }} 50 + {{ $owner := resolve $.Pull.OwnerDid }} 51 51 {{ $re := "re" }} 52 52 {{ if eq .RoundNumber 0 }} 53 53 {{ $re = "" }} 54 54 {{ end }} 55 55 <span class="hidden md:inline">{{$re}}submitted</span> 56 - by {{ template "user/fragments/picHandleLink" $owner }} 56 + by {{ template "user/fragments/picHandleLink" $.Pull.OwnerDid }} 57 57 <span class="select-none before:content-['\00B7']"></span> 58 58 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}">{{ template "repo/fragments/shortTime" .Created }}</a> 59 59 <span class="select-none before:content-['ยท']"></span> ··· 122 122 {{ end }} 123 123 </div> 124 124 <div class="flex items-center"> 125 - <span>{{ .Title }}</span> 125 + <span>{{ .Title | description }}</span> 126 126 {{ if gt (len .Body) 0 }} 127 127 <button 128 128 class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" ··· 151 151 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 152 152 {{ end }} 153 153 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 154 - {{ $owner := index $.DidHandleMap $c.OwnerDid }} 155 - {{ template "user/fragments/picHandleLink" $owner }} 154 + {{ template "user/fragments/picHandleLink" $c.OwnerDid }} 156 155 <span class="before:content-['ยท']"></span> 157 156 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a> 158 157 </div>
+2 -3
appview/pages/templates/repo/pulls/pulls.html
··· 50 50 <div class="px-6 py-4 z-5"> 51 51 <div class="pb-2"> 52 52 <a href="/{{ $.RepoInfo.FullName }}/pulls/{{ .PullId }}" class="dark:text-white"> 53 - {{ .Title }} 53 + {{ .Title | description }} 54 54 <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 55 55 </a> 56 56 </div> 57 57 <div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1"> 58 - {{ $owner := index $.DidHandleMap .OwnerDid }} 59 58 {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} 60 59 {{ $icon := "ban" }} 61 60 ··· 76 75 </span> 77 76 78 77 <span class="ml-1"> 79 - {{ template "user/fragments/picHandleLink" $owner }} 78 + {{ template "user/fragments/picHandleLink" .OwnerDid }} 80 79 </span> 81 80 82 81 <span class="before:content-['ยท']">
+3 -1
appview/pages/templates/repo/settings/general.html
··· 8 8 <div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2"> 9 9 {{ template "branchSettings" . }} 10 10 {{ template "deleteRepo" . }} 11 + <div id="operation-error" class="text-red-500 dark:text-red-400"></div> 11 12 </div> 12 13 </section> 13 14 {{ end }} ··· 22 23 unless you specify a different branch. 23 24 </p> 24 25 </div> 25 - <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 26 + <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" hx-swap="none" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 26 27 <select id="branch" name="branch" required class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 27 28 <option value="" disabled selected > 28 29 Choose a default branch ··· 54 55 <button 55 56 class="btn group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center" 56 57 type="button" 58 + hx-swap="none" 57 59 hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete" 58 60 hx-confirm="Are you sure you want to delete {{ $.RepoInfo.FullName }}?"> 59 61 {{ i "trash-2" "size-4" }}
+5 -5
appview/pages/templates/repo/tree.html
··· 54 54 55 55 {{ range .Files }} 56 56 <div class="grid grid-cols-12 gap-4 items-center py-1"> 57 - <div class="col-span-6 md:col-span-3"> 57 + <div class="col-span-8 md:col-span-4"> 58 58 {{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) $.TreePath .Name }} 59 59 {{ $icon := "folder" }} 60 60 {{ $iconStyle := "size-4 fill-current" }} 61 61 62 62 {{ if .IsFile }} 63 63 {{ $icon = "file" }} 64 - {{ $iconStyle = "flex-shrink-0 size-4" }} 64 + {{ $iconStyle = "size-4" }} 65 65 {{ end }} 66 66 <a href="{{ $link }}" class="{{ $linkstyle }}"> 67 67 <div class="flex items-center gap-2"> 68 - {{ i $icon $iconStyle }} 68 + {{ i $icon $iconStyle "flex-shrink-0" }} 69 69 <span class="truncate">{{ .Name }}</span> 70 70 </div> 71 71 </a> 72 72 </div> 73 73 74 - <div class="col-span-0 md:col-span-7 hidden md:block overflow-hidden"> 74 + <div class="col-span-0 md:col-span-6 hidden md:block overflow-hidden"> 75 75 {{ with .LastCommit }} 76 76 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash }}" class="text-gray-500 dark:text-gray-400 block truncate">{{ .Message }}</a> 77 77 {{ end }} 78 78 </div> 79 79 80 - <div class="col-span-6 md:col-span-2 text-right"> 80 + <div class="col-span-4 md:col-span-2 text-sm text-right"> 81 81 {{ with .LastCommit }} 82 82 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash }}" class="text-gray-500 dark:text-gray-400">{{ template "repo/fragments/time" .When }}</a> 83 83 {{ end }}
-192
appview/pages/templates/settings.html
··· 1 - {{ define "title" }}settings{{ end }} 2 - 3 - {{ define "content" }} 4 - <div class="p-6"> 5 - <p class="text-xl font-bold dark:text-white">Settings</p> 6 - </div> 7 - <div class="flex flex-col"> 8 - {{ block "profile" . }} {{ end }} 9 - {{ block "keys" . }} {{ end }} 10 - {{ block "emails" . }} {{ end }} 11 - </div> 12 - {{ end }} 13 - 14 - {{ define "profile" }} 15 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">profile</h2> 16 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 17 - <dl class="grid grid-cols-[auto_1fr] gap-x-4 dark:text-gray-200"> 18 - {{ if .LoggedInUser.Handle }} 19 - <dt class="font-bold">handle</dt> 20 - <dd>@{{ .LoggedInUser.Handle }}</dd> 21 - {{ end }} 22 - <dt class="font-bold">did</dt> 23 - <dd>{{ .LoggedInUser.Did }}</dd> 24 - <dt class="font-bold">pds</dt> 25 - <dd>{{ .LoggedInUser.Pds }}</dd> 26 - </dl> 27 - </section> 28 - {{ end }} 29 - 30 - {{ define "keys" }} 31 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">ssh keys</h2> 32 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 33 - <p class="mb-8 dark:text-gray-300">SSH public keys added here will be broadcasted to knots that you are a member of, <br> allowing you to push to repositories there.</p> 34 - <div id="key-list" class="flex flex-col gap-6 mb-8"> 35 - {{ range $index, $key := .PubKeys }} 36 - <div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> 37 - <div class="flex flex-col gap-1"> 38 - <div class="inline-flex items-center gap-4"> 39 - {{ i "key" "w-3 h-3 dark:text-gray-300" }} 40 - <p class="font-bold dark:text-white">{{ .Name }}</p> 41 - </div> 42 - <p class="text-sm text-gray-500 dark:text-gray-400">added {{ template "repo/fragments/time" .Created }}</p> 43 - <div class="overflow-x-auto whitespace-nowrap flex-1 max-w-full"> 44 - <code class="text-sm text-gray-500 dark:text-gray-400">{{ .Key }}</code> 45 - </div> 46 - </div> 47 - <button 48 - class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group" 49 - title="Delete key" 50 - hx-delete="/settings/keys?name={{urlquery .Name}}&rkey={{urlquery .Rkey}}&key={{urlquery .Key}}" 51 - hx-confirm="Are you sure you want to delete the key '{{ .Name }}'?" 52 - > 53 - {{ i "trash-2" "w-5 h-5" }} 54 - <span class="hidden md:inline">delete</span> 55 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 56 - </button> 57 - </div> 58 - {{ end }} 59 - </div> 60 - <form 61 - hx-put="/settings/keys" 62 - hx-indicator="#add-sshkey-spinner" 63 - hx-swap="none" 64 - class="max-w-2xl mb-8 space-y-4" 65 - > 66 - <input 67 - type="text" 68 - id="name" 69 - name="name" 70 - placeholder="key name" 71 - required 72 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"/> 73 - 74 - <input 75 - id="key" 76 - name="key" 77 - placeholder="ssh-rsa AAAAAA..." 78 - required 79 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"/> 80 - 81 - <button class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center" type="submit"> 82 - <span>add key</span> 83 - <span id="add-sshkey-spinner" class="group"> 84 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 85 - </span> 86 - </button> 87 - 88 - <div id="settings-keys" class="error dark:text-red-400"></div> 89 - </form> 90 - </section> 91 - {{ end }} 92 - 93 - {{ define "emails" }} 94 - <h2 class="text-sm font-bold py-2 px-6 uppercase dark:text-gray-300">email addresses</h2> 95 - <section class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 96 - <p class="mb-8 dark:text-gray-300">Commits authored using emails listed here will be associated with your Tangled profile.</p> 97 - <div id="email-list" class="flex flex-col gap-6 mb-8"> 98 - {{ range $index, $email := .Emails }} 99 - <div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> 100 - <div class="flex flex-col gap-2"> 101 - <div class="inline-flex items-center gap-4"> 102 - {{ i "mail" "w-3 h-3 dark:text-gray-300" }} 103 - <p class="font-bold dark:text-white">{{ .Address }}</p> 104 - <div class="inline-flex items-center gap-1"> 105 - {{ if .Verified }} 106 - <span class="text-xs bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 py-1 rounded">verified</span> 107 - {{ else }} 108 - <span class="text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 px-2 py-1 rounded">unverified</span> 109 - {{ end }} 110 - {{ if .Primary }} 111 - <span class="text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded">primary</span> 112 - {{ end }} 113 - </div> 114 - </div> 115 - <p class="text-sm text-gray-500 dark:text-gray-400">added {{ template "repo/fragments/time" .CreatedAt }}</p> 116 - </div> 117 - <div class="flex gap-2 items-center"> 118 - {{ if not .Verified }} 119 - <button 120 - class="btn flex gap-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" 121 - hx-post="/settings/emails/verify/resend" 122 - hx-swap="none" 123 - href="#" 124 - hx-vals='{"email": "{{ .Address }}"}'> 125 - {{ i "rotate-cw" "w-5 h-5" }} 126 - <span class="hidden md:inline">resend</span> 127 - </button> 128 - {{ end }} 129 - {{ if and (not .Primary) .Verified }} 130 - <a 131 - class="text-sm dark:text-blue-400 dark:hover:text-blue-300" 132 - hx-post="/settings/emails/primary" 133 - hx-swap="none" 134 - href="#" 135 - hx-vals='{"email": "{{ .Address }}"}'> 136 - set as primary 137 - </a> 138 - {{ end }} 139 - {{ if not .Primary }} 140 - <form 141 - hx-delete="/settings/emails" 142 - hx-confirm="Are you sure you wish to delete the email '{{ .Address }}'?" 143 - hx-indicator="#delete-email-{{ $index }}-spinner" 144 - > 145 - <input type="hidden" name="email" value="{{ .Address }}"> 146 - <button 147 - class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center" 148 - title="Delete email" 149 - type="submit" 150 - > 151 - {{ i "trash-2" "w-5 h-5" }} 152 - <span class="hidden md:inline">delete</span> 153 - <span id="delete-email-{{ $index }}-spinner" class="group"> 154 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 155 - </span> 156 - </button> 157 - </form> 158 - {{ end }} 159 - </div> 160 - </div> 161 - {{ end }} 162 - </div> 163 - <form 164 - hx-put="/settings/emails" 165 - hx-swap="none" 166 - class="max-w-2xl mb-8 space-y-4" 167 - hx-indicator="#add-email-spinner" 168 - > 169 - <input 170 - type="email" 171 - id="email" 172 - name="email" 173 - placeholder="your@email.com" 174 - required 175 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 176 - > 177 - 178 - <button 179 - class="btn dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 flex gap-2 items-center" 180 - type="submit" 181 - > 182 - <span>add email</span> 183 - <span id="add-email-spinner" class="group"> 184 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 185 - </span> 186 - </button> 187 - 188 - <div id="settings-emails-error" class="error dark:text-red-400"></div> 189 - <div id="settings-emails-success" class="success dark:text-green-400"></div> 190 - </form> 191 - </section> 192 - {{ end }}
+2 -4
appview/pages/templates/spindles/dashboard.html
··· 42 42 <div> 43 43 <div class="flex justify-between items-center"> 44 44 <div class="flex items-center gap-2"> 45 - {{ i "user" "size-4" }} 46 - {{ $user := index $.DidHandleMap . }} 47 - <a href="/{{ $user }}">{{ $user }}</a> 45 + {{ template "user/fragments/picHandleLink" . }} 48 46 </div> 49 47 {{ if ne $.LoggedInUser.Did . }} 50 48 {{ block "removeMemberButton" (list $ . ) }} {{ end }} ··· 109 107 hx-post="/spindles/{{ $root.Spindle.Instance }}/remove" 110 108 hx-swap="none" 111 109 hx-vals='{"member": "{{$member}}" }' 112 - hx-confirm="Are you sure you want to remove {{ index $root.DidHandleMap $member }} from this instance?" 110 + hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?" 113 111 > 114 112 {{ i "user-minus" "w-4 h-4" }} 115 113 remove
+3 -1
appview/pages/templates/spindles/fragments/spindleListing.html
··· 9 9 {{ if .Verified }} 10 10 <a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]"> 11 11 {{ i "hard-drive" "w-4 h-4" }} 12 - {{ .Instance }} 12 + <span class="hover:underline"> 13 + {{ .Instance }} 14 + </span> 13 15 <span class="text-gray-500"> 14 16 {{ template "repo/fragments/shortTimeAgo" .Created }} 15 17 </span>
+3 -2
appview/pages/templates/strings/fragments/form.html
··· 13 13 type="text" 14 14 id="filename" 15 15 name="filename" 16 - placeholder="Filename with extension" 16 + placeholder="Filename" 17 17 required 18 18 value="{{ .String.Filename }}" 19 19 class="md:max-w-64 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded" ··· 31 31 name="content" 32 32 id="content-textarea" 33 33 wrap="off" 34 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 34 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 font-mono" 35 35 rows="20" 36 + spellcheck="false" 36 37 placeholder="Paste your string here!" 37 38 required>{{ .String.Contents }}</textarea> 38 39 <div class="flex justify-between items-center">
+2 -2
appview/pages/templates/strings/string.html
··· 35 35 title="Delete string" 36 36 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 37 37 hx-swap="none" 38 - hx-confirm="Are you sure you want to delete the gist `{{ .String.Filename }}`?" 38 + hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 39 39 > 40 40 {{ i "trash-2" "size-4" }} 41 41 <span class="hidden md:inline">delete</span> ··· 77 77 {{ end }} 78 78 </div> 79 79 </div> 80 - <div class="overflow-auto relative"> 80 + <div class="overflow-x-auto overflow-y-hidden relative"> 81 81 {{ if .ShowRendered }} 82 82 <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 83 83 {{ else }}
+65
appview/pages/templates/strings/timeline.html
··· 1 + {{ define "title" }} all strings {{ end }} 2 + 3 + {{ define "topbar" }} 4 + {{ template "layouts/topbar" $ }} 5 + {{ end }} 6 + 7 + {{ define "content" }} 8 + {{ block "timeline" $ }}{{ end }} 9 + {{ end }} 10 + 11 + {{ define "timeline" }} 12 + <div> 13 + <div class="p-6"> 14 + <p class="text-xl font-bold dark:text-white">All strings</p> 15 + </div> 16 + 17 + <div class="flex flex-col gap-4"> 18 + {{ range $i, $s := .Strings }} 19 + <div class="relative"> 20 + {{ if ne $i 0 }} 21 + <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 22 + {{ end }} 23 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 24 + {{ template "stringCard" $s }} 25 + </div> 26 + </div> 27 + {{ end }} 28 + </div> 29 + </div> 30 + {{ end }} 31 + 32 + {{ define "stringCard" }} 33 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"> 34 + <div class="font-medium dark:text-white flex gap-2 items-center"> 35 + <a href="/strings/{{ resolve .Did.String }}/{{ .Rkey }}">{{ .Filename }}</a> 36 + </div> 37 + {{ with .Description }} 38 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 39 + {{ . }} 40 + </div> 41 + {{ end }} 42 + 43 + {{ template "stringCardInfo" . }} 44 + </div> 45 + {{ end }} 46 + 47 + {{ define "stringCardInfo" }} 48 + {{ $stat := .Stats }} 49 + {{ $resolved := resolve .Did.String }} 50 + <div class="text-gray-400 pt-4 text-sm font-mono inline-flex items-center gap-2 mt-auto"> 51 + <a href="/strings/{{ $resolved }}" class="flex items-center"> 52 + {{ template "user/fragments/picHandle" $resolved }} 53 + </a> 54 + <span class="select-none [&:before]:content-['ยท']"></span> 55 + <span>{{ $stat.LineCount }} line{{if ne $stat.LineCount 1}}s{{end}}</span> 56 + <span class="select-none [&:before]:content-['ยท']"></span> 57 + {{ with .Edited }} 58 + <span>edited {{ template "repo/fragments/shortTimeAgo" . }}</span> 59 + {{ else }} 60 + {{ template "repo/fragments/shortTimeAgo" .Created }} 61 + {{ end }} 62 + </div> 63 + {{ end }} 64 + 65 +
+183
appview/pages/templates/timeline/timeline.html
··· 1 + {{ define "title" }}timeline{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + <meta property="og:title" content="timeline ยท tangled" /> 5 + <meta property="og:type" content="object" /> 6 + <meta property="og:url" content="https://tangled.sh" /> 7 + <meta property="og:description" content="tightly-knit social coding" /> 8 + {{ end }} 9 + 10 + {{ define "content" }} 11 + {{ if .LoggedInUser }} 12 + {{ else }} 13 + {{ block "hero" $ }}{{ end }} 14 + {{ end }} 15 + 16 + {{ block "trending" $ }}{{ end }} 17 + {{ block "timeline" $ }}{{ end }} 18 + {{ end }} 19 + 20 + {{ define "hero" }} 21 + <div class="flex flex-col text-black dark:text-white p-6 gap-6 max-w-xl"> 22 + <div class="font-bold text-4xl">tightly-knit<br>social coding.</div> 23 + 24 + <p class="text-lg"> 25 + tangled is new social-enabled git collaboration platform built on <a class="underline" href="https://atproto.com/">atproto</a>. 26 + </p> 27 + <p class="text-lg"> 28 + we envision a place where developers have complete ownership of their 29 + code, open source communities can freely self-govern and most 30 + importantly, coding can be social and fun again. 31 + </p> 32 + 33 + <div class="flex gap-6 items-center"> 34 + <a href="/signup" class="no-underline hover:no-underline "> 35 + <button class="btn-create flex gap-2 px-4 items-center"> 36 + join now {{ i "arrow-right" "size-4" }} 37 + </button> 38 + </a> 39 + </div> 40 + </div> 41 + {{ end }} 42 + 43 + {{ define "trending" }} 44 + <div class="w-full md:mx-0 py-4"> 45 + <div class="px-6 pb-4"> 46 + <h3 class="text-xl font-bold dark:text-white flex items-center gap-2"> 47 + Trending 48 + {{ i "trending-up" "size-4 flex-shrink-0" }} 49 + </h3> 50 + </div> 51 + <div class="flex gap-4 overflow-x-auto scrollbar-hide items-stretch"> 52 + {{ range $index, $repo := .Repos }} 53 + <div class="flex-none h-full border border-gray-200 dark:border-gray-700 rounded-sm w-96"> 54 + {{ template "user/fragments/repoCard" (list $ $repo true) }} 55 + </div> 56 + {{ else }} 57 + <div class="py-8 px-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm"> 58 + <div class="text-sm text-gray-500 dark:text-gray-400 text-center"> 59 + No trending repositories this week 60 + </div> 61 + </div> 62 + {{ end }} 63 + </div> 64 + </div> 65 + {{ end }} 66 + 67 + {{ define "timeline" }} 68 + <div class="py-4"> 69 + <div class="px-6 pb-4"> 70 + <p class="text-xl font-bold dark:text-white">Timeline</p> 71 + </div> 72 + 73 + <div class="flex flex-col gap-4"> 74 + {{ range $i, $e := .Timeline }} 75 + <div class="relative"> 76 + {{ if ne $i 0 }} 77 + <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 78 + {{ end }} 79 + {{ with $e }} 80 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 81 + {{ if .Repo }} 82 + {{ block "repoEvent" (list $ .Repo .Source) }} {{ end }} 83 + {{ else if .Star }} 84 + {{ block "starEvent" (list $ .Star) }} {{ end }} 85 + {{ else if .Follow }} 86 + {{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }} 87 + {{ end }} 88 + </div> 89 + {{ end }} 90 + </div> 91 + {{ end }} 92 + </div> 93 + </div> 94 + {{ end }} 95 + 96 + {{ define "repoEvent" }} 97 + {{ $root := index . 0 }} 98 + {{ $repo := index . 1 }} 99 + {{ $source := index . 2 }} 100 + {{ $userHandle := resolve $repo.Did }} 101 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 102 + {{ template "user/fragments/picHandleLink" $repo.Did }} 103 + {{ with $source }} 104 + {{ $sourceDid := resolve .Did }} 105 + forked 106 + <a href="/{{ $sourceDid }}/{{ .Name }}"class="no-underline hover:underline"> 107 + {{ $sourceDid }}/{{ .Name }} 108 + </a> 109 + to 110 + <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a> 111 + {{ else }} 112 + created 113 + <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline"> 114 + {{ $repo.Name }} 115 + </a> 116 + {{ end }} 117 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span> 118 + </div> 119 + {{ with $repo }} 120 + {{ template "user/fragments/repoCard" (list $root . true) }} 121 + {{ end }} 122 + {{ end }} 123 + 124 + {{ define "starEvent" }} 125 + {{ $root := index . 0 }} 126 + {{ $star := index . 1 }} 127 + {{ with $star }} 128 + {{ $starrerHandle := resolve .StarredByDid }} 129 + {{ $repoOwnerHandle := resolve .Repo.Did }} 130 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 131 + {{ template "user/fragments/picHandleLink" $starrerHandle }} 132 + starred 133 + <a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 134 + {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }} 135 + </a> 136 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span> 137 + </div> 138 + {{ with .Repo }} 139 + {{ template "user/fragments/repoCard" (list $root . true) }} 140 + {{ end }} 141 + {{ end }} 142 + {{ end }} 143 + 144 + 145 + {{ define "followEvent" }} 146 + {{ $root := index . 0 }} 147 + {{ $follow := index . 1 }} 148 + {{ $profile := index . 2 }} 149 + {{ $stat := index . 3 }} 150 + 151 + {{ $userHandle := resolve $follow.UserDid }} 152 + {{ $subjectHandle := resolve $follow.SubjectDid }} 153 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 154 + {{ template "user/fragments/picHandleLink" $userHandle }} 155 + followed 156 + {{ template "user/fragments/picHandleLink" $subjectHandle }} 157 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 158 + </div> 159 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 160 + <div class="flex-shrink-0 max-h-full w-24 h-24"> 161 + <img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 162 + </div> 163 + 164 + <div class="flex-1 min-h-0 justify-around flex flex-col"> 165 + <a href="/{{ $subjectHandle }}"> 166 + <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 167 + </a> 168 + {{ with $profile }} 169 + {{ with .Description }} 170 + <p class="text-sm pb-2 md:pb-2">{{.}}</p> 171 + {{ end }} 172 + {{ end }} 173 + {{ with $stat }} 174 + <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 175 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 176 + <span id="followers"><a href="/{{ $subjectHandle }}?tab=followers">{{ .Followers }} followers</a></span> 177 + <span class="select-none after:content-['ยท']"></span> 178 + <span id="following"><a href="/{{ $subjectHandle }}?tab=following">{{ .Following }} following</a></span> 179 + </div> 180 + {{ end }} 181 + </div> 182 + </div> 183 + {{ end }}
-161
appview/pages/templates/timeline.html
··· 1 - {{ define "title" }}timeline{{ end }} 2 - 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="timeline ยท tangled" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh" /> 7 - <meta property="og:description" content="see what's tangling" /> 8 - {{ end }} 9 - 10 - {{ define "topbar" }} 11 - {{ template "layouts/topbar" $ }} 12 - {{ end }} 13 - 14 - {{ define "content" }} 15 - {{ with .LoggedInUser }} 16 - {{ block "timeline" $ }}{{ end }} 17 - {{ else }} 18 - {{ block "hero" $ }}{{ end }} 19 - {{ block "timeline" $ }}{{ end }} 20 - {{ end }} 21 - {{ end }} 22 - 23 - {{ define "hero" }} 24 - <div class="flex flex-col text-black dark:text-white p-6 gap-6 max-w-xl"> 25 - <div class="font-bold text-4xl">tightly-knit<br>social coding.</div> 26 - 27 - <p class="text-lg"> 28 - tangled is new social-enabled git collaboration platform built on <a class="underline" href="https://atproto.com/">atproto</a>. 29 - </p> 30 - <p class="text-lg"> 31 - we envision a place where developers have complete ownership of their 32 - code, open source communities can freely self-govern and most 33 - importantly, coding can be social and fun again. 34 - </p> 35 - 36 - <div class="flex gap-6 items-center"> 37 - <a href="/signup" class="no-underline hover:no-underline "> 38 - <button class="btn-create flex gap-2 px-4 items-center"> 39 - join now {{ i "arrow-right" "size-4" }} 40 - </button> 41 - </a> 42 - </div> 43 - </div> 44 - {{ end }} 45 - 46 - {{ define "timeline" }} 47 - <div> 48 - <div class="p-6"> 49 - <p class="text-xl font-bold dark:text-white">Timeline</p> 50 - </div> 51 - 52 - <div class="flex flex-col gap-4"> 53 - {{ range $i, $e := .Timeline }} 54 - <div class="relative"> 55 - {{ if ne $i 0 }} 56 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 57 - {{ end }} 58 - {{ with $e }} 59 - <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 60 - {{ if .Repo }} 61 - {{ block "repoEvent" (list $ .Repo .Source) }} {{ end }} 62 - {{ else if .Star }} 63 - {{ block "starEvent" (list $ .Star) }} {{ end }} 64 - {{ else if .Follow }} 65 - {{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }} 66 - {{ end }} 67 - </div> 68 - {{ end }} 69 - </div> 70 - {{ end }} 71 - </div> 72 - </div> 73 - {{ end }} 74 - 75 - {{ define "repoEvent" }} 76 - {{ $root := index . 0 }} 77 - {{ $repo := index . 1 }} 78 - {{ $source := index . 2 }} 79 - {{ $userHandle := index $root.DidHandleMap $repo.Did }} 80 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 81 - {{ template "user/fragments/picHandleLink" $userHandle }} 82 - {{ with $source }} 83 - forked 84 - <a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}"class="no-underline hover:underline"> 85 - {{ index $root.DidHandleMap .Did }}/{{ .Name }} 86 - </a> 87 - to 88 - <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a> 89 - {{ else }} 90 - created 91 - <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline"> 92 - {{ $repo.Name }} 93 - </a> 94 - {{ end }} 95 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span> 96 - </div> 97 - {{ with $repo }} 98 - {{ template "user/fragments/repoCard" (list $root . true) }} 99 - {{ end }} 100 - {{ end }} 101 - 102 - {{ define "starEvent" }} 103 - {{ $root := index . 0 }} 104 - {{ $star := index . 1 }} 105 - {{ with $star }} 106 - {{ $starrerHandle := index $root.DidHandleMap .StarredByDid }} 107 - {{ $repoOwnerHandle := index $root.DidHandleMap .Repo.Did }} 108 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 109 - {{ template "user/fragments/picHandleLink" $starrerHandle }} 110 - starred 111 - <a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 112 - {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }} 113 - </a> 114 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span> 115 - </div> 116 - {{ with .Repo }} 117 - {{ template "user/fragments/repoCard" (list $root . true) }} 118 - {{ end }} 119 - {{ end }} 120 - {{ end }} 121 - 122 - 123 - {{ define "followEvent" }} 124 - {{ $root := index . 0 }} 125 - {{ $follow := index . 1 }} 126 - {{ $profile := index . 2 }} 127 - {{ $stat := index . 3 }} 128 - 129 - {{ $userHandle := index $root.DidHandleMap $follow.UserDid }} 130 - {{ $subjectHandle := index $root.DidHandleMap $follow.SubjectDid }} 131 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 132 - {{ template "user/fragments/picHandleLink" $userHandle }} 133 - followed 134 - {{ template "user/fragments/picHandleLink" $subjectHandle }} 135 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 136 - </div> 137 - <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 138 - <div class="flex-shrink-0 max-h-full w-24 h-24"> 139 - <img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 140 - </div> 141 - 142 - <div class="flex-1 min-h-0 justify-around flex flex-col"> 143 - <a href="/{{ $subjectHandle }}"> 144 - <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 145 - </a> 146 - {{ with $profile }} 147 - {{ with .Description }} 148 - <p class="text-sm pb-2 md:pb-2">{{.}}</p> 149 - {{ end }} 150 - {{ end }} 151 - {{ with $stat }} 152 - <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 153 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 154 - <span id="followers">{{ .Followers }} followers</span> 155 - <span class="select-none after:content-['ยท']"></span> 156 - <span id="following">{{ .Following }} following</span> 157 - </div> 158 - {{ end }} 159 - </div> 160 - </div> 161 - {{ end }}
+30
appview/pages/templates/user/followers.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท followers {{ end }} 2 + 3 + {{ define "extrameta" }} 4 + <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s followers" /> 5 + <meta property="og:type" content="object" /> 6 + <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=followers" /> 7 + <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 + {{ end }} 9 + 10 + {{ define "content" }} 11 + <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 + <div class="md:col-span-3 order-1 md:order-1"> 13 + {{ template "user/fragments/profileCard" .Card }} 14 + </div> 15 + <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 16 + {{ block "followers" . }}{{ end }} 17 + </div> 18 + </div> 19 + {{ end }} 20 + 21 + {{ define "followers" }} 22 + <p class="text-sm font-bold p-2 dark:text-white">ALL FOLLOWERS</p> 23 + <div id="followers" class="grid grid-cols-1 gap-4 mb-6"> 24 + {{ range .Followers }} 25 + {{ template "user/fragments/followCard" . }} 26 + {{ else }} 27 + <p class="px-6 dark:text-white">This user does not have any followers yet.</p> 28 + {{ end }} 29 + </div> 30 + {{ end }}
+30
appview/pages/templates/user/following.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท following {{ end }} 2 + 3 + {{ define "extrameta" }} 4 + <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s following" /> 5 + <meta property="og:type" content="object" /> 6 + <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=following" /> 7 + <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 + {{ end }} 9 + 10 + {{ define "content" }} 11 + <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 + <div class="md:col-span-3 order-1 md:order-1"> 13 + {{ template "user/fragments/profileCard" .Card }} 14 + </div> 15 + <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 16 + {{ block "following" . }}{{ end }} 17 + </div> 18 + </div> 19 + {{ end }} 20 + 21 + {{ define "following" }} 22 + <p class="text-sm font-bold p-2 dark:text-white">ALL FOLLOWING</p> 23 + <div id="following" class="grid grid-cols-1 gap-4 mb-6"> 24 + {{ range .Following }} 25 + {{ template "user/fragments/followCard" . }} 26 + {{ else }} 27 + <p class="px-6 dark:text-white">This user does not follow anyone yet.</p> 28 + {{ end }} 29 + </div> 30 + {{ end }}
+1 -1
appview/pages/templates/user/fragments/editPins.html
··· 27 27 <input type="checkbox" id="repo-{{$idx}}" name="pinnedRepo{{$idx}}" value="{{.RepoAt}}" {{if .IsPinned}}checked{{end}}> 28 28 <label for="repo-{{$idx}}" class="my-0 py-0 normal-case font-normal w-full"> 29 29 <div class="flex justify-between items-center w-full"> 30 - <span class="flex-shrink-0 overflow-hidden text-ellipsis ">{{ index $.DidHandleMap .Did }}/{{.Name}}</span> 30 + <span class="flex-shrink-0 overflow-hidden text-ellipsis ">{{ resolve .Did }}/{{.Name}}</span> 31 31 <div class="flex gap-1 items-center"> 32 32 {{ i "star" "size-4 fill-current" }} 33 33 <span>{{ .RepoStats.StarCount }}</span>
+2 -2
appview/pages/templates/user/fragments/follow.html
··· 1 1 {{ define "user/fragments/follow" }} 2 - <button id="followBtn" 2 + <button id="{{ normalizeForHtmlId .UserDid }}" 3 3 class="btn mt-2 w-full flex gap-2 items-center group" 4 4 5 5 {{ if eq .FollowStatus.String "IsNotFollowing" }} ··· 9 9 {{ end }} 10 10 11 11 hx-trigger="click" 12 - hx-target="#followBtn" 12 + hx-target="#{{ normalizeForHtmlId .UserDid }}" 13 13 hx-swap="outerHTML" 14 14 > 15 15 {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
+29
appview/pages/templates/user/fragments/followCard.html
··· 1 + {{ define "user/fragments/followCard" }} 2 + {{ $userIdent := resolve .UserDid }} 3 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 4 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 5 + <div class="flex-shrink-0 max-h-full w-24 h-24"> 6 + <img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" /> 7 + </div> 8 + 9 + <div class="flex-1 min-h-0 justify-around flex flex-col"> 10 + <a href="/{{ $userIdent }}"> 11 + <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span> 12 + </a> 13 + <p class="text-sm pb-2 md:pb-2">{{.Profile.Description}}</p> 14 + <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 15 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 16 + <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 17 + <span class="select-none after:content-['ยท']"></span> 18 + <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 19 + </div> 20 + </div> 21 + 22 + {{ if ne .FollowStatus.String "IsSelf" }} 23 + <div class="max-w-24"> 24 + {{ template "user/fragments/follow" . }} 25 + </div> 26 + {{ end }} 27 + </div> 28 + </div> 29 + {{ end }}
+3 -2
appview/pages/templates/user/fragments/picHandleLink.html
··· 1 1 {{ define "user/fragments/picHandleLink" }} 2 - <a href="/{{ . }}" class="flex items-center"> 3 - {{ template "user/fragments/picHandle" . }} 2 + {{ $resolved := resolve . }} 3 + <a href="/{{ $resolved }}" class="flex items-center"> 4 + {{ template "user/fragments/picHandle" $resolved }} 4 5 </a> 5 6 {{ end }}
+17 -14
appview/pages/templates/user/fragments/profileCard.html
··· 1 1 {{ define "user/fragments/profileCard" }} 2 + {{ $userIdent := didOrHandle .UserDid .UserHandle }} 2 3 <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit"> 3 4 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 5 <div id="avatar" class="col-span-1 flex justify-center items-center"> ··· 8 9 </div> 9 10 <div class="col-span-2"> 10 11 <div class="flex items-center flex-row flex-nowrap gap-2"> 11 - <p title="{{ didOrHandle .UserDid .UserHandle }}" 12 + <p title="{{ $userIdent }}" 12 13 class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"> 13 - {{ didOrHandle .UserDid .UserHandle }} 14 + {{ $userIdent }} 14 15 </p> 15 - <a href="/{{ didOrHandle .UserDid .UserHandle }}/feed.atom">{{ i "rss" "size-4" }}</a> 16 + <a href="/{{ $userIdent }}/feed.atom">{{ i "rss" "size-4" }}</a> 16 17 </div> 17 18 18 19 <div class="md:hidden"> 19 - {{ block "followerFollowing" (list .Followers .Following) }} {{ end }} 20 + {{ block "followerFollowing" (list . $userIdent) }} {{ end }} 20 21 </div> 21 22 </div> 22 23 <div class="col-span-3 md:col-span-full"> ··· 29 30 {{ end }} 30 31 31 32 <div class="hidden md:block"> 32 - {{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }} 33 + {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 33 34 </div> 34 35 35 36 <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> ··· 42 43 {{ if .IncludeBluesky }} 43 44 <div class="flex items-center gap-2"> 44 45 <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 45 - <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ didOrHandle $.UserDid $.UserHandle }}</a> 46 + <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a> 46 47 </div> 47 48 {{ end }} 48 49 {{ range $link := .Links }} ··· 88 89 {{ end }} 89 90 90 91 {{ define "followerFollowing" }} 91 - {{ $followers := index . 0 }} 92 - {{ $following := index . 1 }} 93 - <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 94 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 95 - <span id="followers">{{ $followers }} followers</span> 96 - <span class="select-none after:content-['ยท']"></span> 97 - <span id="following">{{ $following }} following</span> 98 - </div> 92 + {{ $root := index . 0 }} 93 + {{ $userIdent := index . 1 }} 94 + {{ with $root }} 95 + <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 96 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 97 + <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 98 + <span class="select-none after:content-['ยท']"></span> 99 + <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 100 + </div> 101 + {{ end }} 99 102 {{ end }} 100 103
+36 -31
appview/pages/templates/user/fragments/repoCard.html
··· 4 4 {{ $fullName := index . 2 }} 5 5 6 6 {{ with $repo }} 7 - <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"> 8 - <div class="font-medium dark:text-white flex gap-2 items-center"> 9 - {{- if $fullName -}} 10 - <a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}">{{ index $root.DidHandleMap .Did }}/{{ .Name }}</a> 11 - {{- else -}} 12 - <a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}">{{ .Name }}</a> 13 - {{- end -}} 14 - </div> 15 - {{ with .Description }} 16 - <div class="text-gray-600 dark:text-gray-300 text-sm"> 17 - {{ . }} 18 - </div> 7 + <div class="py-4 px-6 gap-1 flex flex-col drop-shadow-sm rounded bg-white dark:bg-gray-800 min-h-32"> 8 + <div class="font-medium dark:text-white flex items-center"> 9 + {{ if .Source }} 10 + {{ i "git-fork" "w-4 h-4 mr-1.5 shrink-0" }} 11 + {{ else }} 12 + {{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }} 19 13 {{ end }} 20 14 21 - {{ if .RepoStats }} 22 - {{ block "repoStats" .RepoStats }} {{ end }} 23 - {{ end }} 15 + {{ $repoOwner := resolve .Did }} 16 + {{- if $fullName -}} 17 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ $repoOwner }}/{{ .Name }}</a> 18 + {{- else -}} 19 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ .Name }}</a> 20 + {{- end -}} 21 + </div> 22 + {{ with .Description }} 23 + <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2"> 24 + {{ . | description }} 25 + </div> 26 + {{ end }} 27 + 28 + {{ if .RepoStats }} 29 + {{ block "repoStats" .RepoStats }}{{ end }} 30 + {{ end }} 24 31 </div> 25 32 {{ end }} 26 33 {{ end }} 27 34 28 35 {{ define "repoStats" }} 29 - <div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-4 mt-auto"> 36 + <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> 30 37 {{ with .Language }} 31 38 <div class="flex gap-2 items-center text-sm"> 32 - <div class="size-2 rounded-full" 39 + <div class="size-2 rounded-full" 33 40 style="background: radial-gradient(circle at 35% 35%, color-mix(in srgb, {{ langColor . }} 70%, white), {{ langColor . }} 30%, color-mix(in srgb, {{ langColor . }} 85%, black));"></div> 34 41 <span>{{ . }}</span> 35 42 </div> 36 43 {{ end }} 37 44 {{ with .StarCount }} 38 - <div class="flex gap-1 items-center text-sm"> 39 - {{ i "star" "w-3 h-3 fill-current" }} 40 - <span>{{ . }}</span> 41 - </div> 45 + <div class="flex gap-1 items-center text-sm"> 46 + {{ i "star" "w-3 h-3 fill-current" }} 47 + <span>{{ . }}</span> 48 + </div> 42 49 {{ end }} 43 50 {{ with .IssueCount.Open }} 44 - <div class="flex gap-1 items-center text-sm"> 45 - {{ i "circle-dot" "w-3 h-3" }} 46 - <span>{{ . }}</span> 47 - </div> 51 + <div class="flex gap-1 items-center text-sm"> 52 + {{ i "circle-dot" "w-3 h-3" }} 53 + <span>{{ . }}</span> 54 + </div> 48 55 {{ end }} 49 56 {{ with .PullCount.Open }} 50 - <div class="flex gap-1 items-center text-sm"> 51 - {{ i "git-pull-request" "w-3 h-3" }} 52 - <span>{{ . }}</span> 53 - </div> 57 + <div class="flex gap-1 items-center text-sm"> 58 + {{ i "git-pull-request" "w-3 h-3" }} 59 + <span>{{ . }}</span> 60 + </div> 54 61 {{ end }} 55 62 </div> 56 63 {{ end }} 57 - 58 -
+1
appview/pages/templates/user/login.html
··· 41 41 your Tangled (<code>.tngl.sh</code>) or <a href="https://bsky.app">Bluesky</a> (<code>.bsky.social</code>) account. 42 42 </span> 43 43 </div> 44 + <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 44 45 45 46 <button 46 47 class="btn w-full my-2 mt-6 text-base "
+13 -20
appview/pages/templates/user/profile.html
··· 50 50 </div> 51 51 {{ else }} 52 52 <div class="flex flex-col gap-1"> 53 - {{ block "repoEvents" (list .RepoEvents $.DidHandleMap) }} {{ end }} 54 - {{ block "issueEvents" (list .IssueEvents $.DidHandleMap) }} {{ end }} 55 - {{ block "pullEvents" (list .PullEvents $.DidHandleMap) }} {{ end }} 53 + {{ block "repoEvents" .RepoEvents }} {{ end }} 54 + {{ block "issueEvents" .IssueEvents }} {{ end }} 55 + {{ block "pullEvents" .PullEvents }} {{ end }} 56 56 </div> 57 57 {{ end }} 58 58 </div> ··· 66 66 {{ end }} 67 67 68 68 {{ define "repoEvents" }} 69 - {{ $items := index . 0 }} 70 - {{ $handleMap := index . 1 }} 71 - 72 - {{ if gt (len $items) 0 }} 69 + {{ if gt (len .) 0 }} 73 70 <details> 74 71 <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 75 72 <div class="flex flex-wrap items-center gap-2"> 76 73 {{ i "book-plus" "w-4 h-4" }} 77 - created {{ len $items }} {{if eq (len $items) 1 }}repository{{else}}repositories{{end}} 74 + created {{ len . }} {{if eq (len .) 1 }}repository{{else}}repositories{{end}} 78 75 </div> 79 76 </summary> 80 77 <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 81 - {{ range $items }} 78 + {{ range . }} 82 79 <div class="flex flex-wrap items-center gap-2"> 83 80 <span class="text-gray-500 dark:text-gray-400"> 84 81 {{ if .Source }} ··· 87 84 {{ i "book-plus" "w-4 h-4" }} 88 85 {{ end }} 89 86 </span> 90 - <a href="/{{ index $handleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 87 + <a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 91 88 {{- .Repo.Name -}} 92 89 </a> 93 90 </div> ··· 98 95 {{ end }} 99 96 100 97 {{ define "issueEvents" }} 101 - {{ $i := index . 0 }} 102 - {{ $items := $i.Items }} 103 - {{ $stats := $i.Stats }} 104 - {{ $handleMap := index . 1 }} 98 + {{ $items := .Items }} 99 + {{ $stats := .Stats }} 105 100 106 101 {{ if gt (len $items) 0 }} 107 102 <details> ··· 129 124 </summary> 130 125 <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 131 126 {{ range $items }} 132 - {{ $repoOwner := index $handleMap .Metadata.Repo.Did }} 127 + {{ $repoOwner := resolve .Metadata.Repo.Did }} 133 128 {{ $repoName := .Metadata.Repo.Name }} 134 129 {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 135 130 ··· 163 158 {{ end }} 164 159 165 160 {{ define "pullEvents" }} 166 - {{ $i := index . 0 }} 167 - {{ $items := $i.Items }} 168 - {{ $stats := $i.Stats }} 169 - {{ $handleMap := index . 1 }} 161 + {{ $items := .Items }} 162 + {{ $stats := .Stats }} 170 163 {{ if gt (len $items) 0 }} 171 164 <details> 172 165 <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> ··· 200 193 </summary> 201 194 <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 202 195 {{ range $items }} 203 - {{ $repoOwner := index $handleMap .Repo.Did }} 196 + {{ $repoOwner := resolve .Repo.Did }} 204 197 {{ $repoName := .Repo.Name }} 205 198 {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 206 199
+1 -1
appview/pages/templates/user/repos.html
··· 3 3 {{ define "extrameta" }} 4 4 <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s repos" /> 5 5 <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}/repos" /> 6 + <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=repos" /> 7 7 <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 8 {{ end }} 9 9
+94
appview/pages/templates/user/settings/emails.html
··· 1 + {{ define "title" }}{{ .Tab }} settings{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="p-6"> 5 + <p class="text-xl font-bold dark:text-white">Settings</p> 6 + </div> 7 + <div class="bg-white dark:bg-gray-800"> 8 + <section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6 p-6"> 9 + <div class="col-span-1"> 10 + {{ template "user/settings/fragments/sidebar" . }} 11 + </div> 12 + <div class="col-span-1 md:col-span-3 flex flex-col gap-6"> 13 + {{ template "emailSettings" . }} 14 + </div> 15 + </section> 16 + </div> 17 + {{ end }} 18 + 19 + {{ define "emailSettings" }} 20 + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center"> 21 + <div class="col-span-1 md:col-span-2"> 22 + <h2 class="text-sm pb-2 uppercase font-bold">Email Addresses</h2> 23 + <p class="text-gray-500 dark:text-gray-400"> 24 + Commits authored using emails listed here will be associated with your Tangled profile. 25 + </p> 26 + </div> 27 + <div class="col-span-1 md:col-span-1 md:justify-self-end"> 28 + {{ template "addEmailButton" . }} 29 + </div> 30 + </div> 31 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full"> 32 + {{ range .Emails }} 33 + {{ template "user/settings/fragments/emailListing" (list $ .) }} 34 + {{ else }} 35 + <div class="flex items-center justify-center p-2 text-gray-500"> 36 + no emails added yet 37 + </div> 38 + {{ end }} 39 + </div> 40 + {{ end }} 41 + 42 + {{ define "addEmailButton" }} 43 + <button 44 + class="btn flex items-center gap-2" 45 + popovertarget="add-email-modal" 46 + popovertargetaction="toggle"> 47 + {{ i "plus" "size-4" }} 48 + add email 49 + </button> 50 + <div 51 + id="add-email-modal" 52 + popover 53 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 54 + {{ template "addEmailModal" . }} 55 + </div> 56 + {{ end}} 57 + 58 + {{ define "addEmailModal" }} 59 + <form 60 + hx-put="/settings/emails" 61 + hx-indicator="#spinner" 62 + hx-swap="none" 63 + class="flex flex-col gap-2" 64 + > 65 + <p class="uppercase p-0">ADD EMAIL</p> 66 + <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 67 + <input 68 + type="email" 69 + id="email-address" 70 + name="email" 71 + required 72 + placeholder="your@email.com" 73 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 74 + /> 75 + <div class="flex gap-2 pt-2"> 76 + <button 77 + type="button" 78 + popovertarget="add-email-modal" 79 + popovertargetaction="hide" 80 + class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" 81 + > 82 + {{ i "x" "size-4" }} cancel 83 + </button> 84 + <button type="submit" class="btn w-1/2 flex items-center"> 85 + <span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span> 86 + <span id="spinner" class="group"> 87 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 + </span> 89 + </button> 90 + </div> 91 + <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 92 + <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 93 + </form> 94 + {{ end }}
+62
appview/pages/templates/user/settings/fragments/emailListing.html
··· 1 + {{ define "user/settings/fragments/emailListing" }} 2 + {{ $root := index . 0 }} 3 + {{ $email := index . 1 }} 4 + <div id="email-{{$email.Address}}" class="flex items-center justify-between p-2"> 5 + <div class="hover:no-underline flex flex-col gap-1 min-w-0 max-w-[80%]"> 6 + <div class="flex items-center gap-2"> 7 + {{ i "mail" "w-4 h-4 text-gray-500 dark:text-gray-400" }} 8 + <span class="font-bold"> 9 + {{ $email.Address }} 10 + </span> 11 + <div class="inline-flex items-center gap-1"> 12 + {{ if $email.Verified }} 13 + <span class="text-xs bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 py-1 rounded">verified</span> 14 + {{ else }} 15 + <span class="text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 px-2 py-1 rounded">unverified</span> 16 + {{ end }} 17 + {{ if $email.Primary }} 18 + <span class="text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded">primary</span> 19 + {{ end }} 20 + </div> 21 + </div> 22 + <div class="flex text-sm flex-wrap text items-center gap-1 text-gray-500 dark:text-gray-400"> 23 + <span>added {{ template "repo/fragments/time" $email.CreatedAt }}</span> 24 + </div> 25 + </div> 26 + <div class="flex gap-2 items-center"> 27 + {{ if not $email.Verified }} 28 + <button 29 + class="btn flex gap-2 text-sm px-2 py-1" 30 + hx-post="/settings/emails/verify/resend" 31 + hx-swap="none" 32 + hx-vals='{"email": "{{ $email.Address }}"}'> 33 + {{ i "rotate-cw" "w-4 h-4" }} 34 + <span class="hidden md:inline">resend</span> 35 + </button> 36 + {{ end }} 37 + {{ if and (not $email.Primary) $email.Verified }} 38 + <button 39 + class="btn text-sm px-2 py-1 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" 40 + hx-post="/settings/emails/primary" 41 + hx-swap="none" 42 + hx-vals='{"email": "{{ $email.Address }}"}'> 43 + set as primary 44 + </button> 45 + {{ end }} 46 + {{ if not $email.Primary }} 47 + <button 48 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group" 49 + title="Delete email" 50 + hx-delete="/settings/emails" 51 + hx-swap="none" 52 + hx-vals='{"email": "{{ $email.Address }}"}' 53 + hx-confirm="Are you sure you want to delete the email {{ $email.Address }}?" 54 + > 55 + {{ i "trash-2" "w-5 h-5" }} 56 + <span class="hidden md:inline">delete</span> 57 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 + </button> 59 + {{ end }} 60 + </div> 61 + </div> 62 + {{ end }}
+31
appview/pages/templates/user/settings/fragments/keyListing.html
··· 1 + {{ define "user/settings/fragments/keyListing" }} 2 + {{ $root := index . 0 }} 3 + {{ $key := index . 1 }} 4 + <div id="key-{{$key.Name}}" class="flex items-center justify-between p-2"> 5 + <div class="hover:no-underline flex flex-col gap-1 text min-w-0 max-w-[80%]"> 6 + <div class="flex items-center gap-2"> 7 + <span>{{ i "key" "w-4" "h-4" }}</span> 8 + <span class="font-bold"> 9 + {{ $key.Name }} 10 + </span> 11 + </div> 12 + <span class="font-mono text-sm text-gray-500 dark:text-gray-400"> 13 + {{ sshFingerprint $key.Key }} 14 + </span> 15 + <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 16 + <span>added {{ template "repo/fragments/time" $key.Created }}</span> 17 + </div> 18 + </div> 19 + <button 20 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group" 21 + title="Delete key" 22 + hx-delete="/settings/keys?name={{urlquery $key.Name}}&rkey={{urlquery $key.Rkey}}&key={{urlquery $key.Key}}" 23 + hx-swap="none" 24 + hx-confirm="Are you sure you want to delete the key {{ $key.Name }}?" 25 + > 26 + {{ i "trash-2" "w-5 h-5" }} 27 + <span class="hidden md:inline">delete</span> 28 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 29 + </button> 30 + </div> 31 + {{ end }}
+16
appview/pages/templates/user/settings/fragments/sidebar.html
··· 1 + {{ define "user/settings/fragments/sidebar" }} 2 + {{ $active := .Tab }} 3 + {{ $tabs := .Tabs }} 4 + <div class="sticky top-2 grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 shadow-inner"> 5 + {{ $activeTab := "bg-white dark:bg-gray-700 drop-shadow-sm" }} 6 + {{ $inactiveTab := "bg-gray-100 dark:bg-gray-800" }} 7 + {{ range $tabs }} 8 + <a href="/settings/{{.Name}}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 9 + <div class="flex gap-3 items-center p-2 {{ if eq .Name $active }} {{ $activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}"> 10 + {{ i .Icon "size-4" }} 11 + {{ .Name }} 12 + </div> 13 + </a> 14 + {{ end }} 15 + </div> 16 + {{ end }}
+101
appview/pages/templates/user/settings/keys.html
··· 1 + {{ define "title" }}{{ .Tab }} settings{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="p-6"> 5 + <p class="text-xl font-bold dark:text-white">Settings</p> 6 + </div> 7 + <div class="bg-white dark:bg-gray-800"> 8 + <section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6 p-6"> 9 + <div class="col-span-1"> 10 + {{ template "user/settings/fragments/sidebar" . }} 11 + </div> 12 + <div class="col-span-1 md:col-span-3 flex flex-col gap-6"> 13 + {{ template "sshKeysSettings" . }} 14 + </div> 15 + </section> 16 + </div> 17 + {{ end }} 18 + 19 + {{ define "sshKeysSettings" }} 20 + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center"> 21 + <div class="col-span-1 md:col-span-2"> 22 + <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 23 + <p class="text-gray-500 dark:text-gray-400"> 24 + SSH public keys added here will be broadcasted to knots that you are a member of, 25 + allowing you to push to repositories there. 26 + </p> 27 + </div> 28 + <div class="col-span-1 md:col-span-1 md:justify-self-end"> 29 + {{ template "addKeyButton" . }} 30 + </div> 31 + </div> 32 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full"> 33 + {{ range .PubKeys }} 34 + {{ template "user/settings/fragments/keyListing" (list $ .) }} 35 + {{ else }} 36 + <div class="flex items-center justify-center p-2 text-gray-500"> 37 + no keys added yet 38 + </div> 39 + {{ end }} 40 + </div> 41 + {{ end }} 42 + 43 + {{ define "addKeyButton" }} 44 + <button 45 + class="btn flex items-center gap-2" 46 + popovertarget="add-key-modal" 47 + popovertargetaction="toggle"> 48 + {{ i "plus" "size-4" }} 49 + add key 50 + </button> 51 + <div 52 + id="add-key-modal" 53 + popover 54 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 55 + {{ template "addKeyModal" . }} 56 + </div> 57 + {{ end}} 58 + 59 + {{ define "addKeyModal" }} 60 + <form 61 + hx-put="/settings/keys" 62 + hx-indicator="#spinner" 63 + hx-swap="none" 64 + class="flex flex-col gap-2" 65 + > 66 + <p class="uppercase p-0">ADD SSH KEY</p> 67 + <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p> 68 + <input 69 + type="text" 70 + id="key-name" 71 + name="name" 72 + required 73 + placeholder="key name" 74 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 75 + /> 76 + <textarea 77 + type="text" 78 + id="key-value" 79 + name="key" 80 + required 81 + placeholder="ssh-rsa AAAAB3NzaC1yc2E..." 82 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"></textarea> 83 + <div class="flex gap-2 pt-2"> 84 + <button 85 + type="button" 86 + popovertarget="add-key-modal" 87 + popovertargetaction="hide" 88 + class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" 89 + > 90 + {{ i "x" "size-4" }} cancel 91 + </button> 92 + <button type="submit" class="btn w-1/2 flex items-center"> 93 + <span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span> 94 + <span id="spinner" class="group"> 95 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 96 + </span> 97 + </button> 98 + </div> 99 + <div id="settings-keys" class="text-red-500 dark:text-red-400"></div> 100 + </form> 101 + {{ end }}
+64
appview/pages/templates/user/settings/profile.html
··· 1 + {{ define "title" }}{{ .Tab }} settings{{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="p-6"> 5 + <p class="text-xl font-bold dark:text-white">Settings</p> 6 + </div> 7 + <div class="bg-white dark:bg-gray-800"> 8 + <section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6 p-6"> 9 + <div class="col-span-1"> 10 + {{ template "user/settings/fragments/sidebar" . }} 11 + </div> 12 + <div class="col-span-1 md:col-span-3 flex flex-col gap-6"> 13 + {{ template "profileInfo" . }} 14 + </div> 15 + </section> 16 + </div> 17 + {{ end }} 18 + 19 + {{ define "profileInfo" }} 20 + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center"> 21 + <div class="col-span-1 md:col-span-2"> 22 + <h2 class="text-sm pb-2 uppercase font-bold">Profile</h2> 23 + <p class="text-gray-500 dark:text-gray-400"> 24 + Your account information from your AT Protocol identity. 25 + </p> 26 + </div> 27 + <div class="col-span-1 md:col-span-1 md:justify-self-end"> 28 + </div> 29 + </div> 30 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full"> 31 + <div class="flex items-center justify-between p-4"> 32 + <div class="hover:no-underline flex flex-col gap-1 min-w-0 max-w-[80%]"> 33 + <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 34 + <span>Handle</span> 35 + </div> 36 + {{ if .LoggedInUser.Handle }} 37 + <span class="font-bold"> 38 + @{{ .LoggedInUser.Handle }} 39 + </span> 40 + {{ end }} 41 + </div> 42 + </div> 43 + <div class="flex items-center justify-between p-4"> 44 + <div class="hover:no-underline flex flex-col gap-1 min-w-0 max-w-[80%]"> 45 + <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 46 + <span>Decentralized Identifier (DID)</span> 47 + </div> 48 + <span class="font-mono font-bold"> 49 + {{ .LoggedInUser.Did }} 50 + </span> 51 + </div> 52 + </div> 53 + <div class="flex items-center justify-between p-4"> 54 + <div class="hover:no-underline flex flex-col gap-1 min-w-0 max-w-[80%]"> 55 + <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 56 + <span>Personal Data Server (PDS)</span> 57 + </div> 58 + <span class="font-bold"> 59 + {{ .LoggedInUser.Pds }} 60 + </span> 61 + </div> 62 + </div> 63 + </div> 64 + {{ end }}
+80 -157
appview/pulls/pulls.go
··· 17 17 "tangled.sh/tangled.sh/core/appview/notify" 18 18 "tangled.sh/tangled.sh/core/appview/oauth" 19 19 "tangled.sh/tangled.sh/core/appview/pages" 20 + "tangled.sh/tangled.sh/core/appview/pages/markup" 20 21 "tangled.sh/tangled.sh/core/appview/reporesolver" 22 + "tangled.sh/tangled.sh/core/appview/xrpcclient" 21 23 "tangled.sh/tangled.sh/core/idresolver" 22 24 "tangled.sh/tangled.sh/core/knotclient" 23 25 "tangled.sh/tangled.sh/core/patchutil" 24 26 "tangled.sh/tangled.sh/core/tid" 25 27 "tangled.sh/tangled.sh/core/types" 26 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 27 28 28 29 "github.com/bluekeyes/go-gitdiff/gitdiff" 29 30 comatproto "github.com/bluesky-social/indigo/api/atproto" 30 - "github.com/bluesky-social/indigo/atproto/syntax" 31 31 lexutil "github.com/bluesky-social/indigo/lex/util" 32 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 32 33 "github.com/go-chi/chi/v5" 33 34 "github.com/google/uuid" 34 35 ) ··· 150 151 } 151 152 } 152 153 153 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 154 - didHandleMap := make(map[string]string) 155 - for _, identity := range resolvedIds { 156 - if !identity.Handle.IsInvalidHandle() { 157 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 158 - } else { 159 - didHandleMap[identity.DID.String()] = identity.DID.String() 160 - } 161 - } 162 - 163 154 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 164 155 resubmitResult := pages.Unknown 165 156 if user != nil && user.Did == pull.OwnerDid { ··· 211 202 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 212 203 LoggedInUser: user, 213 204 RepoInfo: repoInfo, 214 - DidHandleMap: didHandleMap, 215 205 Pull: pull, 216 206 Stack: stack, 217 207 AbandonedPulls: abandonedPulls, ··· 230 220 return types.MergeCheckResponse{} 231 221 } 232 222 233 - client, err := s.oauth.ServiceClient( 234 - r, 235 - oauth.WithService(f.Knot), 236 - oauth.WithLxm(tangled.RepoMergeCheckNSID), 237 - oauth.WithDev(s.config.Core.Dev), 238 - ) 239 - if err != nil { 240 - log.Printf("failed to connect to knot server: %v", err) 241 - return types.MergeCheckResponse{ 242 - Error: "failed to check merge status: could not connect to knot server", 243 - } 223 + scheme := "https" 224 + if s.config.Core.Dev { 225 + scheme = "http" 226 + } 227 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 228 + 229 + xrpcc := indigoxrpc.Client{ 230 + Host: host, 244 231 } 245 232 246 233 patch := pull.LatestPatch() ··· 253 240 patch = mergeable.CombinedPatch() 254 241 } 255 242 256 - resp, err := tangled.RepoMergeCheck( 243 + resp, xe := tangled.RepoMergeCheck( 257 244 r.Context(), 258 - client, 245 + &xrpcc, 259 246 &tangled.RepoMergeCheck_Input{ 260 247 Did: f.OwnerDid(), 261 - Name: f.RepoName, 248 + Name: f.Name, 262 249 Branch: pull.TargetBranch, 263 250 Patch: patch, 264 251 }, 265 252 ) 266 - if err != nil { 267 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 268 - if parseErr != nil { 269 - log.Printf("failed to check for mergeability: %v", err) 270 - return types.MergeCheckResponse{ 271 - Error: "failed to check merge status", 272 - } 273 - } 274 - log.Printf("failed to check for mergeability: %s", xe.Error()) 253 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 254 + log.Println("failed to check for mergeability", "err", err) 275 255 return types.MergeCheckResponse{ 276 - Error: fmt.Sprintf("failed to check merge status: %s", xe.Message), 256 + Error: fmt.Sprintf("failed to check merge status: %s", err.Error()), 277 257 } 278 258 } 279 259 ··· 324 304 // pulls within the same repo 325 305 knot = f.Knot 326 306 ownerDid = f.OwnerDid() 327 - repoName = f.RepoName 307 + repoName = f.Name 328 308 } 329 309 330 310 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) ··· 383 363 return 384 364 } 385 365 386 - identsToResolve := []string{pull.OwnerDid} 387 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 388 - didHandleMap := make(map[string]string) 389 - for _, identity := range resolvedIds { 390 - if !identity.Handle.IsInvalidHandle() { 391 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 392 - } else { 393 - didHandleMap[identity.DID.String()] = identity.DID.String() 394 - } 395 - } 396 - 397 366 patch := pull.Submissions[roundIdInt].Patch 398 367 diff := patchutil.AsNiceDiff(patch, pull.TargetBranch) 399 368 400 369 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 401 370 LoggedInUser: user, 402 - DidHandleMap: didHandleMap, 403 371 RepoInfo: f.RepoInfo(user), 404 372 Pull: pull, 405 373 Stack: stack, ··· 446 414 return 447 415 } 448 416 449 - identsToResolve := []string{pull.OwnerDid} 450 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 451 - didHandleMap := make(map[string]string) 452 - for _, identity := range resolvedIds { 453 - if !identity.Handle.IsInvalidHandle() { 454 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 455 - } else { 456 - didHandleMap[identity.DID.String()] = identity.DID.String() 457 - } 458 - } 459 - 460 417 currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].Patch) 461 418 if err != nil { 462 419 log.Println("failed to interdiff; current patch malformed") ··· 478 435 RepoInfo: f.RepoInfo(user), 479 436 Pull: pull, 480 437 Round: roundIdInt, 481 - DidHandleMap: didHandleMap, 482 438 Interdiff: interdiff, 483 439 DiffOpts: diffOpts, 484 440 }) ··· 500 456 return 501 457 } 502 458 503 - identsToResolve := []string{pull.OwnerDid} 504 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 505 - didHandleMap := make(map[string]string) 506 - for _, identity := range resolvedIds { 507 - if !identity.Handle.IsInvalidHandle() { 508 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 509 - } else { 510 - didHandleMap[identity.DID.String()] = identity.DID.String() 511 - } 512 - } 513 - 514 459 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 515 460 w.Write([]byte(pull.Submissions[roundIdInt].Patch)) 516 461 } ··· 535 480 536 481 pulls, err := db.GetPulls( 537 482 s.db, 538 - db.FilterEq("repo_at", f.RepoAt), 483 + db.FilterEq("repo_at", f.RepoAt()), 539 484 db.FilterEq("state", state), 540 485 ) 541 486 if err != nil { ··· 601 546 m[p.Sha] = p 602 547 } 603 548 604 - identsToResolve := make([]string, len(pulls)) 605 - for i, pull := range pulls { 606 - identsToResolve[i] = pull.OwnerDid 607 - } 608 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 609 - didHandleMap := make(map[string]string) 610 - for _, identity := range resolvedIds { 611 - if !identity.Handle.IsInvalidHandle() { 612 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 613 - } else { 614 - didHandleMap[identity.DID.String()] = identity.DID.String() 615 - } 616 - } 617 - 618 549 s.pages.RepoPulls(w, pages.RepoPullsParams{ 619 550 LoggedInUser: s.oauth.GetUser(r), 620 551 RepoInfo: f.RepoInfo(user), 621 552 Pulls: pulls, 622 - DidHandleMap: didHandleMap, 623 553 FilteringBy: state, 624 554 Stacks: stacks, 625 555 Pipelines: m, ··· 677 607 createdAt := time.Now().Format(time.RFC3339) 678 608 ownerDid := user.Did 679 609 680 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 610 + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 681 611 if err != nil { 682 612 log.Println("failed to get pull at", err) 683 613 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 684 614 return 685 615 } 686 616 687 - atUri := f.RepoAt.String() 617 + atUri := f.RepoAt().String() 688 618 client, err := s.oauth.AuthorizedClient(r) 689 619 if err != nil { 690 620 log.Println("failed to get authorized client", err) ··· 713 643 714 644 comment := &db.PullComment{ 715 645 OwnerDid: user.Did, 716 - RepoAt: f.RepoAt.String(), 646 + RepoAt: f.RepoAt().String(), 717 647 PullId: pull.PullId, 718 648 Body: body, 719 649 CommentAt: atResp.Uri, ··· 759 689 return 760 690 } 761 691 762 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 692 + result, err := us.Branches(f.OwnerDid(), f.Name) 763 693 if err != nil { 764 694 log.Println("failed to fetch branches", err) 765 695 return ··· 807 737 s.pages.Notice(w, "pull", "Title is required for git-diff patches.") 808 738 return 809 739 } 740 + sanitizer := markup.NewSanitizer() 741 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(title)); (st) == "" { 742 + s.pages.Notice(w, "pull", "Title is empty after HTML sanitization") 743 + return 744 + } 810 745 } 811 746 812 747 // Validate we have at least one valid PR creation method ··· 883 818 return 884 819 } 885 820 886 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 821 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) 887 822 if err != nil { 888 823 log.Println("failed to compare", err) 889 824 s.pages.Notice(w, "pull", err.Error()) ··· 954 889 &tangled.RepoHiddenRef_Input{ 955 890 ForkRef: sourceBranch, 956 891 RemoteRef: targetBranch, 957 - Repo: fork.AtUri, 892 + Repo: fork.RepoAt().String(), 958 893 }, 959 894 ) 960 - if err != nil { 961 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 962 - if parseErr != nil { 963 - log.Printf("failed to create hidden ref: %v", err) 964 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 965 - } else { 966 - log.Printf("failed to create hidden ref: %s", xe.Error()) 967 - if xe.Tag == "AccessControl" { 968 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 969 - } else { 970 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create pull request: %s", xe.Message)) 971 - } 972 - } 895 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 896 + s.pages.Notice(w, "pull", err.Error()) 973 897 return 974 898 } 975 899 ··· 1003 927 return 1004 928 } 1005 929 1006 - forkAtUri, err := syntax.ParseATURI(fork.AtUri) 1007 - if err != nil { 1008 - log.Println("failed to parse fork AT URI", err) 1009 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1010 - return 1011 - } 930 + forkAtUri := fork.RepoAt() 931 + forkAtUriStr := forkAtUri.String() 1012 932 1013 933 pullSource := &db.PullSource{ 1014 934 Branch: sourceBranch, ··· 1016 936 } 1017 937 recordPullSource := &tangled.RepoPull_Source{ 1018 938 Branch: sourceBranch, 1019 - Repo: &fork.AtUri, 939 + Repo: &forkAtUriStr, 1020 940 Sha: sourceRev, 1021 941 } 1022 942 ··· 1092 1012 Body: body, 1093 1013 TargetBranch: targetBranch, 1094 1014 OwnerDid: user.Did, 1095 - RepoAt: f.RepoAt, 1015 + RepoAt: f.RepoAt(), 1096 1016 Rkey: rkey, 1097 1017 Submissions: []*db.PullSubmission{ 1098 1018 &initialSubmission, ··· 1105 1025 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1106 1026 return 1107 1027 } 1108 - pullId, err := db.NextPullId(tx, f.RepoAt) 1028 + pullId, err := db.NextPullId(tx, f.RepoAt()) 1109 1029 if err != nil { 1110 1030 log.Println("failed to get pull id", err) 1111 1031 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1120 1040 Val: &tangled.RepoPull{ 1121 1041 Title: title, 1122 1042 PullId: int64(pullId), 1123 - TargetRepo: string(f.RepoAt), 1043 + TargetRepo: string(f.RepoAt()), 1124 1044 TargetBranch: targetBranch, 1125 1045 Patch: patch, 1126 1046 Source: recordPullSource, ··· 1298 1218 return 1299 1219 } 1300 1220 1301 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1221 + result, err := us.Branches(f.OwnerDid(), f.Name) 1302 1222 if err != nil { 1303 1223 log.Println("failed to reach knotserver", err) 1304 1224 return ··· 1382 1302 return 1383 1303 } 1384 1304 1385 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1305 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) 1386 1306 if err != nil { 1387 1307 log.Println("failed to reach knotserver for target branches", err) 1388 1308 return ··· 1498 1418 return 1499 1419 } 1500 1420 1501 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1421 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) 1502 1422 if err != nil { 1503 1423 log.Printf("compare request failed: %s", err) 1504 1424 s.pages.Notice(w, "resubmit-error", err.Error()) ··· 1566 1486 &tangled.RepoHiddenRef_Input{ 1567 1487 ForkRef: pull.PullSource.Branch, 1568 1488 RemoteRef: pull.TargetBranch, 1569 - Repo: forkRepo.AtUri, 1489 + Repo: forkRepo.RepoAt().String(), 1570 1490 }, 1571 1491 ) 1572 - if err != nil || !resp.Success { 1573 - if err != nil { 1574 - log.Printf("failed to update tracking branch: %s", err) 1575 - } else { 1576 - log.Printf("failed to update tracking branch: success=false") 1577 - } 1578 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1492 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1493 + s.pages.Notice(w, "resubmit-error", err.Error()) 1494 + return 1495 + } 1496 + if !resp.Success { 1497 + log.Println("Failed to update tracking ref.", "err", resp.Error) 1498 + s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 1579 1499 return 1580 1500 } 1581 1501 ··· 1691 1611 Val: &tangled.RepoPull{ 1692 1612 Title: pull.Title, 1693 1613 PullId: int64(pull.PullId), 1694 - TargetRepo: string(f.RepoAt), 1614 + TargetRepo: string(f.RepoAt()), 1695 1615 TargetBranch: pull.TargetBranch, 1696 1616 Patch: patch, // new patch 1697 1617 Source: recordPullSource, ··· 1807 1727 1808 1728 // deleted pulls are marked as deleted in the DB 1809 1729 for _, p := range deletions { 1730 + // do not do delete already merged PRs 1731 + if p.State == db.PullMerged { 1732 + continue 1733 + } 1734 + 1810 1735 err := db.DeletePull(tx, p.RepoAt, p.PullId) 1811 1736 if err != nil { 1812 1737 log.Println("failed to delete pull", err, p.PullId) ··· 1847 1772 op, _ := origById[id] 1848 1773 np, _ := newById[id] 1849 1774 1775 + // do not update already merged PRs 1776 + if op.State == db.PullMerged { 1777 + continue 1778 + } 1779 + 1850 1780 submission := np.Submissions[np.LastRoundNumber()] 1851 1781 1852 1782 // resubmit the old pull ··· 1991 1921 1992 1922 patch := pullsToMerge.CombinedPatch() 1993 1923 1994 - client, err := s.oauth.ServiceClient( 1995 - r, 1996 - oauth.WithService(f.Knot), 1997 - oauth.WithLxm(tangled.RepoMergeNSID), 1998 - oauth.WithDev(s.config.Core.Dev), 1999 - ) 2000 - if err != nil { 2001 - log.Printf("failed to connect to knot server: %v", err) 2002 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2003 - return 2004 - } 2005 - 2006 1924 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 2007 1925 if err != nil { 2008 1926 log.Printf("resolving identity: %s", err) ··· 2018 1936 authorName := ident.Handle.String() 2019 1937 mergeInput := &tangled.RepoMerge_Input{ 2020 1938 Did: f.OwnerDid(), 2021 - Name: f.RepoName, 1939 + Name: f.Name, 2022 1940 Branch: pull.TargetBranch, 2023 1941 Patch: patch, 2024 1942 CommitMessage: &pull.Title, ··· 2033 1951 mergeInput.AuthorEmail = &email.Address 2034 1952 } 2035 1953 1954 + client, err := s.oauth.ServiceClient( 1955 + r, 1956 + oauth.WithService(f.Knot), 1957 + oauth.WithLxm(tangled.RepoMergeNSID), 1958 + oauth.WithDev(s.config.Core.Dev), 1959 + ) 1960 + if err != nil { 1961 + log.Printf("failed to connect to knot server: %v", err) 1962 + s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1963 + return 1964 + } 1965 + 2036 1966 err = tangled.RepoMerge(r.Context(), client, mergeInput) 2037 - if err != nil { 2038 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 2039 - if parseErr != nil { 2040 - log.Printf("failed to merge pull request: %v", err) 2041 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 2042 - } else { 2043 - log.Printf("failed to merge pull request: %s", xe.Error()) 2044 - s.pages.Notice(w, "pull-merge-error", fmt.Sprintf("Failed to merge pull request: %s", xe.Message)) 2045 - } 1967 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1968 + s.pages.Notice(w, "pull-merge-error", err.Error()) 2046 1969 return 2047 1970 } 2048 1971 ··· 2055 1978 defer tx.Rollback() 2056 1979 2057 1980 for _, p := range pullsToMerge { 2058 - err := db.MergePull(tx, f.RepoAt, p.PullId) 1981 + err := db.MergePull(tx, f.RepoAt(), p.PullId) 2059 1982 if err != nil { 2060 1983 log.Printf("failed to update pull request status in database: %s", err) 2061 1984 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 2071 1994 return 2072 1995 } 2073 1996 2074 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1997 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 2075 1998 } 2076 1999 2077 2000 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { ··· 2123 2046 2124 2047 for _, p := range pullsToClose { 2125 2048 // Close the pull in the database 2126 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 2049 + err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2127 2050 if err != nil { 2128 2051 log.Println("failed to close pull", err) 2129 2052 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2191 2114 2192 2115 for _, p := range pullsToReopen { 2193 2116 // Close the pull in the database 2194 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2117 + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2195 2118 if err != nil { 2196 2119 log.Println("failed to close pull", err) 2197 2120 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2243 2166 Body: body, 2244 2167 TargetBranch: targetBranch, 2245 2168 OwnerDid: user.Did, 2246 - RepoAt: f.RepoAt, 2169 + RepoAt: f.RepoAt(), 2247 2170 Rkey: rkey, 2248 2171 Submissions: []*db.PullSubmission{ 2249 2172 &initialSubmission,
+6 -6
appview/repo/artifact.go
··· 76 76 Artifact: uploadBlobResp.Blob, 77 77 CreatedAt: createdAt.Format(time.RFC3339), 78 78 Name: handler.Filename, 79 - Repo: f.RepoAt.String(), 79 + Repo: f.RepoAt().String(), 80 80 Tag: tag.Tag.Hash[:], 81 81 }, 82 82 }, ··· 100 100 artifact := db.Artifact{ 101 101 Did: user.Did, 102 102 Rkey: rkey, 103 - RepoAt: f.RepoAt, 103 + RepoAt: f.RepoAt(), 104 104 Tag: tag.Tag.Hash, 105 105 CreatedAt: createdAt, 106 106 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 155 155 156 156 artifacts, err := db.GetArtifact( 157 157 rp.db, 158 - db.FilterEq("repo_at", f.RepoAt), 158 + db.FilterEq("repo_at", f.RepoAt()), 159 159 db.FilterEq("tag", tag.Tag.Hash[:]), 160 160 db.FilterEq("name", filename), 161 161 ) ··· 197 197 198 198 artifacts, err := db.GetArtifact( 199 199 rp.db, 200 - db.FilterEq("repo_at", f.RepoAt), 200 + db.FilterEq("repo_at", f.RepoAt()), 201 201 db.FilterEq("tag", tag[:]), 202 202 db.FilterEq("name", filename), 203 203 ) ··· 239 239 defer tx.Rollback() 240 240 241 241 err = db.DeleteArtifact(tx, 242 - db.FilterEq("repo_at", f.RepoAt), 242 + db.FilterEq("repo_at", f.RepoAt()), 243 243 db.FilterEq("tag", artifact.Tag[:]), 244 244 db.FilterEq("name", filename), 245 245 ) ··· 270 270 return nil, err 271 271 } 272 272 273 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 273 + result, err := us.Tags(f.OwnerDid(), f.Name) 274 274 if err != nil { 275 275 log.Println("failed to reach knotserver", err) 276 276 return nil, err
+165
appview/repo/feed.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "slices" 9 + "time" 10 + 11 + "tangled.sh/tangled.sh/core/appview/db" 12 + "tangled.sh/tangled.sh/core/appview/reporesolver" 13 + 14 + "github.com/bluesky-social/indigo/atproto/syntax" 15 + "github.com/gorilla/feeds" 16 + ) 17 + 18 + func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) { 19 + const feedLimitPerType = 100 20 + 21 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 22 + if err != nil { 23 + return nil, err 24 + } 25 + 26 + issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + feed := &feeds.Feed{ 32 + Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()), 33 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"}, 34 + Items: make([]*feeds.Item, 0), 35 + Updated: time.UnixMilli(0), 36 + } 37 + 38 + for _, pull := range pulls { 39 + items, err := rp.createPullItems(ctx, pull, f) 40 + if err != nil { 41 + return nil, err 42 + } 43 + feed.Items = append(feed.Items, items...) 44 + } 45 + 46 + for _, issue := range issues { 47 + item, err := rp.createIssueItem(ctx, issue, f) 48 + if err != nil { 49 + return nil, err 50 + } 51 + feed.Items = append(feed.Items, item) 52 + } 53 + 54 + slices.SortFunc(feed.Items, func(a, b *feeds.Item) int { 55 + if a.Created.After(b.Created) { 56 + return -1 57 + } 58 + return 1 59 + }) 60 + 61 + if len(feed.Items) > 0 { 62 + feed.Updated = feed.Items[0].Created 63 + } 64 + 65 + return feed, nil 66 + } 67 + 68 + func (rp *Repo) createPullItems(ctx context.Context, pull *db.Pull, f *reporesolver.ResolvedRepo) ([]*feeds.Item, error) { 69 + owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) 70 + if err != nil { 71 + return nil, err 72 + } 73 + 74 + var items []*feeds.Item 75 + 76 + state := rp.getPullState(pull) 77 + description := rp.buildPullDescription(owner.Handle, state, pull, f.OwnerSlashRepo()) 78 + 79 + mainItem := &feeds.Item{ 80 + Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 81 + Description: description, 82 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, 83 + Created: pull.Created, 84 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 85 + } 86 + items = append(items, mainItem) 87 + 88 + for _, round := range pull.Submissions { 89 + if round == nil || round.RoundNumber == 0 { 90 + continue 91 + } 92 + 93 + roundItem := &feeds.Item{ 94 + Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 95 + Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()), 96 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)}, 97 + Created: round.Created, 98 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 99 + } 100 + items = append(items, roundItem) 101 + } 102 + 103 + return items, nil 104 + } 105 + 106 + func (rp *Repo) createIssueItem(ctx context.Context, issue db.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) { 107 + owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) 108 + if err != nil { 109 + return nil, err 110 + } 111 + 112 + state := "closed" 113 + if issue.Open { 114 + state = "opened" 115 + } 116 + 117 + return &feeds.Item{ 118 + Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 119 + Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()), 120 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, 121 + Created: issue.Created, 122 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 123 + }, nil 124 + } 125 + 126 + func (rp *Repo) getPullState(pull *db.Pull) string { 127 + if pull.State == db.PullOpen { 128 + return "opened" 129 + } 130 + return pull.State.String() 131 + } 132 + 133 + func (rp *Repo) buildPullDescription(handle syntax.Handle, state string, pull *db.Pull, repoName string) string { 134 + base := fmt.Sprintf("@%s %s pull request #%d", handle, state, pull.PullId) 135 + 136 + if pull.State == db.PullMerged { 137 + return fmt.Sprintf("%s (on round #%d) in %s", base, pull.LastRoundNumber(), repoName) 138 + } 139 + 140 + return fmt.Sprintf("%s in %s", base, repoName) 141 + } 142 + 143 + func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { 144 + f, err := rp.repoResolver.Resolve(r) 145 + if err != nil { 146 + log.Println("failed to fully resolve repo:", err) 147 + return 148 + } 149 + 150 + feed, err := rp.getRepoFeed(r.Context(), f) 151 + if err != nil { 152 + log.Println("failed to get repo feed:", err) 153 + rp.pages.Error500(w) 154 + return 155 + } 156 + 157 + atom, err := feed.ToAtom() 158 + if err != nil { 159 + rp.pages.Error500(w) 160 + return 161 + } 162 + 163 + w.Header().Set("content-type", "application/atom+xml") 164 + w.Write([]byte(atom)) 165 + }
+17 -129
appview/repo/index.go
··· 1 1 package repo 2 2 3 3 import ( 4 - "encoding/json" 5 - "fmt" 6 4 "log" 7 5 "net/http" 8 6 "slices" 9 7 "sort" 10 8 "strings" 11 9 12 - "tangled.sh/tangled.sh/core/api/tangled" 13 10 "tangled.sh/tangled.sh/core/appview/commitverify" 14 11 "tangled.sh/tangled.sh/core/appview/db" 15 - "tangled.sh/tangled.sh/core/appview/oauth" 16 12 "tangled.sh/tangled.sh/core/appview/pages" 17 - "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 18 13 "tangled.sh/tangled.sh/core/appview/reporesolver" 19 14 "tangled.sh/tangled.sh/core/knotclient" 20 15 "tangled.sh/tangled.sh/core/types" ··· 25 20 26 21 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 27 22 ref := chi.URLParam(r, "ref") 23 + 28 24 f, err := rp.repoResolver.Resolve(r) 29 25 if err != nil { 30 26 log.Println("failed to fully resolve repo", err) ··· 38 34 return 39 35 } 40 36 41 - result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 37 + result, err := us.Index(f.OwnerDid(), f.Name, ref) 42 38 if err != nil { 43 39 rp.pages.Error503(w) 44 40 log.Println("failed to reach knotserver", err) ··· 105 101 user := rp.oauth.GetUser(r) 106 102 repoInfo := f.RepoInfo(user) 107 103 108 - secret, err := db.GetRegistrationKey(rp.db, f.Knot) 109 - if err != nil { 110 - log.Printf("failed to get registration key for %s: %s", f.Knot, err) 111 - rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 112 - } 113 - 114 - signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 115 - if err != nil { 116 - log.Printf("failed to create signed client for %s: %s", f.Knot, err) 117 - return 118 - } 119 - 120 - var forkInfo *types.ForkInfo 121 - if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 122 - forkInfo, err = getForkInfo(r, repoInfo, rp, f, user, signedClient) 123 - if err != nil { 124 - log.Printf("Failed to fetch fork information: %v", err) 125 - return 126 - } 127 - } 128 - 129 104 // TODO: a bit dirty 130 - languageInfo, err := rp.getLanguageInfo(f, signedClient, chi.URLParam(r, "ref") == "") 105 + languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "") 131 106 if err != nil { 132 107 log.Printf("failed to compute language percentages: %s", err) 133 108 // non-fatal ··· 144 119 } 145 120 146 121 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 147 - LoggedInUser: user, 148 - RepoInfo: repoInfo, 149 - TagMap: tagMap, 150 - RepoIndexResponse: *result, 151 - CommitsTrunc: commitsTrunc, 152 - TagsTrunc: tagsTrunc, 153 - ForkInfo: forkInfo, 122 + LoggedInUser: user, 123 + RepoInfo: repoInfo, 124 + TagMap: tagMap, 125 + RepoIndexResponse: *result, 126 + CommitsTrunc: commitsTrunc, 127 + TagsTrunc: tagsTrunc, 128 + // ForkInfo: forkInfo, // TODO: reinstate this after xrpc properly lands 154 129 BranchesTrunc: branchesTrunc, 155 130 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 156 131 VerifiedCommits: vc, ··· 161 136 162 137 func (rp *Repo) getLanguageInfo( 163 138 f *reporesolver.ResolvedRepo, 164 - signedClient *knotclient.SignedClient, 139 + us *knotclient.UnsignedClient, 140 + currentRef string, 165 141 isDefaultRef bool, 166 142 ) ([]types.RepoLanguageDetails, error) { 167 143 // first attempt to fetch from db 168 144 langs, err := db.GetRepoLanguages( 169 145 rp.db, 170 - db.FilterEq("repo_at", f.RepoAt), 171 - db.FilterEq("ref", f.Ref), 146 + db.FilterEq("repo_at", f.RepoAt()), 147 + db.FilterEq("ref", currentRef), 172 148 ) 173 149 174 150 if err != nil || langs == nil { 175 151 // non-fatal, fetch langs from ks 176 - ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref) 152 + ls, err := us.RepoLanguages(f.OwnerDid(), f.Name, currentRef) 177 153 if err != nil { 178 154 return nil, err 179 155 } ··· 183 159 184 160 for l, s := range ls.Languages { 185 161 langs = append(langs, db.RepoLanguage{ 186 - RepoAt: f.RepoAt, 187 - Ref: f.Ref, 162 + RepoAt: f.RepoAt(), 163 + Ref: currentRef, 188 164 IsDefaultRef: isDefaultRef, 189 165 Language: l, 190 166 Bytes: s, ··· 230 206 231 207 return languageStats, nil 232 208 } 233 - 234 - func getForkInfo( 235 - r *http.Request, 236 - repoInfo repoinfo.RepoInfo, 237 - rp *Repo, 238 - f *reporesolver.ResolvedRepo, 239 - user *oauth.User, 240 - signedClient *knotclient.SignedClient, 241 - ) (*types.ForkInfo, error) { 242 - if user == nil { 243 - return nil, nil 244 - } 245 - 246 - forkInfo := types.ForkInfo{ 247 - IsFork: repoInfo.Source != nil, 248 - Status: types.UpToDate, 249 - } 250 - 251 - if !forkInfo.IsFork { 252 - forkInfo.IsFork = false 253 - return &forkInfo, nil 254 - } 255 - 256 - us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev) 257 - if err != nil { 258 - log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 259 - return nil, err 260 - } 261 - 262 - result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name) 263 - if err != nil { 264 - log.Println("failed to reach knotserver", err) 265 - return nil, err 266 - } 267 - 268 - if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 269 - return branch.Name == f.Ref 270 - }) { 271 - forkInfo.Status = types.MissingBranch 272 - return &forkInfo, nil 273 - } 274 - 275 - client, err := rp.oauth.ServiceClient( 276 - r, 277 - oauth.WithService(f.Knot), 278 - oauth.WithLxm(tangled.RepoHiddenRefNSID), 279 - oauth.WithDev(rp.config.Core.Dev), 280 - ) 281 - if err != nil { 282 - log.Printf("failed to connect to knot server: %v", err) 283 - return nil, err 284 - } 285 - 286 - resp, err := tangled.RepoHiddenRef( 287 - r.Context(), 288 - client, 289 - &tangled.RepoHiddenRef_Input{ 290 - ForkRef: f.Ref, 291 - RemoteRef: f.Ref, 292 - Repo: string(f.RepoAt), 293 - }, 294 - ) 295 - if err != nil || !resp.Success { 296 - if err != nil { 297 - log.Printf("failed to update tracking branch: %s", err) 298 - } else { 299 - log.Printf("failed to update tracking branch: success=false") 300 - } 301 - return nil, fmt.Errorf("failed to update tracking branch") 302 - } 303 - 304 - hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 305 - 306 - var status types.AncestorCheckResponse 307 - forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 308 - if err != nil { 309 - log.Printf("failed to check if fork is ahead/behind: %s", err) 310 - return nil, err 311 - } 312 - 313 - if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 314 - log.Printf("failed to decode fork status: %s", err) 315 - return nil, err 316 - } 317 - 318 - forkInfo.Status = status.Status 319 - return &forkInfo, nil 320 - }
+237 -236
appview/repo/repo.go
··· 28 28 "tangled.sh/tangled.sh/core/appview/pages" 29 29 "tangled.sh/tangled.sh/core/appview/pages/markup" 30 30 "tangled.sh/tangled.sh/core/appview/reporesolver" 31 + xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient" 31 32 "tangled.sh/tangled.sh/core/eventconsumer" 32 33 "tangled.sh/tangled.sh/core/idresolver" 33 34 "tangled.sh/tangled.sh/core/knotclient" ··· 35 36 "tangled.sh/tangled.sh/core/rbac" 36 37 "tangled.sh/tangled.sh/core/tid" 37 38 "tangled.sh/tangled.sh/core/types" 38 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 39 39 "tangled.sh/tangled.sh/core/xrpc/serviceauth" 40 40 41 41 securejoin "github.com/cyphar/filepath-securejoin" ··· 98 98 } else { 99 99 uri = "https" 100 100 } 101 - url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.RepoName, url.PathEscape(refParam)) 101 + url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam)) 102 102 103 103 http.Redirect(w, r, url, http.StatusFound) 104 104 } ··· 126 126 return 127 127 } 128 128 129 - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 129 + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 130 130 if err != nil { 131 + rp.pages.Error503(w) 131 132 log.Println("failed to reach knotserver", err) 132 133 return 133 134 } 134 135 135 - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) 136 + tagResult, err := us.Tags(f.OwnerDid(), f.Name) 136 137 if err != nil { 138 + rp.pages.Error503(w) 137 139 log.Println("failed to reach knotserver", err) 138 140 return 139 141 } ··· 147 149 tagMap[hash] = append(tagMap[hash], tag.Name) 148 150 } 149 151 150 - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) 152 + branchResult, err := us.Branches(f.OwnerDid(), f.Name) 151 153 if err != nil { 154 + rp.pages.Error503(w) 152 155 log.Println("failed to reach knotserver", err) 153 156 return 154 157 } ··· 215 218 return 216 219 } 217 220 218 - repoAt := f.RepoAt 221 + repoAt := f.RepoAt() 219 222 rkey := repoAt.RecordKey().String() 220 223 if rkey == "" { 221 224 log.Println("invalid aturi for repo", err) ··· 265 268 Record: &lexutil.LexiconTypeDecoder{ 266 269 Val: &tangled.Repo{ 267 270 Knot: f.Knot, 268 - Name: f.RepoName, 271 + Name: f.Name, 269 272 Owner: user.Did, 270 - CreatedAt: f.CreatedAt, 273 + CreatedAt: f.Created.Format(time.RFC3339), 271 274 Description: &newDescription, 272 275 Spindle: &f.Spindle, 273 276 }, ··· 313 316 return 314 317 } 315 318 316 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 319 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 317 320 if err != nil { 321 + rp.pages.Error503(w) 318 322 log.Println("failed to reach knotserver", err) 319 323 return 320 324 } ··· 378 382 if !rp.config.Core.Dev { 379 383 protocol = "https" 380 384 } 381 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 385 + 386 + // if the tree path has a trailing slash, let's strip it 387 + // so we don't 404 388 + treePath = strings.TrimSuffix(treePath, "/") 389 + 390 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 382 391 if err != nil { 392 + rp.pages.Error503(w) 383 393 log.Println("failed to reach knotserver", err) 384 394 return 385 395 } 386 396 397 + // uhhh so knotserver returns a 500 if the entry isn't found in 398 + // the requested tree path, so let's stick to not-OK here. 399 + // we can fix this once we build out the xrpc apis for these operations. 400 + if resp.StatusCode != http.StatusOK { 401 + rp.pages.Error404(w) 402 + return 403 + } 404 + 387 405 body, err := io.ReadAll(resp.Body) 388 406 if err != nil { 389 407 log.Printf("Error reading response body: %v", err) ··· 408 426 user := rp.oauth.GetUser(r) 409 427 410 428 var breadcrumbs [][]string 411 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 429 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 412 430 if treePath != "" { 413 431 for idx, elem := range strings.Split(treePath, "/") { 414 432 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 439 457 return 440 458 } 441 459 442 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 460 + result, err := us.Tags(f.OwnerDid(), f.Name) 443 461 if err != nil { 462 + rp.pages.Error503(w) 444 463 log.Println("failed to reach knotserver", err) 445 464 return 446 465 } 447 466 448 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 467 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 449 468 if err != nil { 450 469 log.Println("failed grab artifacts", err) 451 470 return ··· 496 515 return 497 516 } 498 517 499 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 518 + result, err := us.Branches(f.OwnerDid(), f.Name) 500 519 if err != nil { 520 + rp.pages.Error503(w) 501 521 log.Println("failed to reach knotserver", err) 502 522 return 503 523 } ··· 525 545 if !rp.config.Core.Dev { 526 546 protocol = "https" 527 547 } 528 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 548 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 529 549 if err != nil { 550 + rp.pages.Error503(w) 530 551 log.Println("failed to reach knotserver", err) 531 552 return 532 553 } 533 554 555 + if resp.StatusCode == http.StatusNotFound { 556 + rp.pages.Error404(w) 557 + return 558 + } 559 + 534 560 body, err := io.ReadAll(resp.Body) 535 561 if err != nil { 536 562 log.Printf("Error reading response body: %v", err) ··· 545 571 } 546 572 547 573 var breadcrumbs [][]string 548 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 574 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 549 575 if filePath != "" { 550 576 for idx, elem := range strings.Split(filePath, "/") { 551 577 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 578 604 579 605 // fetch the actual binary content like in RepoBlobRaw 580 606 581 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 607 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) 582 608 contentSrc = blobURL 583 609 if !rp.config.Core.Dev { 584 610 contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) ··· 615 641 if !rp.config.Core.Dev { 616 642 protocol = "https" 617 643 } 618 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 619 - resp, err := http.Get(blobURL) 644 + 645 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 646 + 647 + req, err := http.NewRequest("GET", blobURL, nil) 620 648 if err != nil { 621 - log.Println("failed to reach knotserver:", err) 649 + log.Println("failed to create request", err) 650 + return 651 + } 652 + 653 + // forward the If-None-Match header 654 + if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 655 + req.Header.Set("If-None-Match", clientETag) 656 + } 657 + 658 + client := &http.Client{} 659 + resp, err := client.Do(req) 660 + if err != nil { 661 + log.Println("failed to reach knotserver", err) 622 662 rp.pages.Error503(w) 623 663 return 624 664 } 625 665 defer resp.Body.Close() 626 666 667 + // forward 304 not modified 668 + if resp.StatusCode == http.StatusNotModified { 669 + w.WriteHeader(http.StatusNotModified) 670 + return 671 + } 672 + 627 673 if resp.StatusCode != http.StatusOK { 628 674 log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) 629 675 w.WriteHeader(resp.StatusCode) ··· 671 717 return 672 718 } 673 719 674 - repoAt := f.RepoAt 720 + repoAt := f.RepoAt() 675 721 rkey := repoAt.RecordKey().String() 676 722 if rkey == "" { 677 723 fail("Failed to resolve repo. Try again later", err) ··· 725 771 Record: &lexutil.LexiconTypeDecoder{ 726 772 Val: &tangled.Repo{ 727 773 Knot: f.Knot, 728 - Name: f.RepoName, 774 + Name: f.Name, 729 775 Owner: user.Did, 730 - CreatedAt: f.CreatedAt, 776 + CreatedAt: f.Created.Format(time.RFC3339), 731 777 Description: &f.Description, 732 778 Spindle: spindlePtr, 733 779 }, ··· 808 854 Record: &lexutil.LexiconTypeDecoder{ 809 855 Val: &tangled.RepoCollaborator{ 810 856 Subject: collaboratorIdent.DID.String(), 811 - Repo: string(f.RepoAt), 857 + Repo: string(f.RepoAt()), 812 858 CreatedAt: createdAt.Format(time.RFC3339), 813 859 }}, 814 860 }) ··· 817 863 fail("Failed to write record to PDS.", err) 818 864 return 819 865 } 820 - l = l.With("at-uri", resp.Uri) 866 + 867 + aturi := resp.Uri 868 + l = l.With("at-uri", aturi) 821 869 l.Info("wrote record to PDS") 822 870 823 - l.Info("adding to knot") 824 - secret, err := db.GetRegistrationKey(rp.db, f.Knot) 871 + tx, err := rp.db.BeginTx(r.Context(), nil) 825 872 if err != nil { 826 - fail("Failed to add to knot.", err) 873 + fail("Failed to add collaborator.", err) 827 874 return 828 875 } 829 876 830 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 831 - if err != nil { 832 - fail("Failed to add to knot.", err) 833 - return 834 - } 877 + rollback := func() { 878 + err1 := tx.Rollback() 879 + err2 := rp.enforcer.E.LoadPolicy() 880 + err3 := rollbackRecord(context.Background(), aturi, client) 835 881 836 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 837 - if err != nil { 838 - fail("Knot was unreachable.", err) 839 - return 840 - } 882 + // ignore txn complete errors, this is okay 883 + if errors.Is(err1, sql.ErrTxDone) { 884 + err1 = nil 885 + } 841 886 842 - if ksResp.StatusCode != http.StatusNoContent { 843 - fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil) 844 - return 845 - } 846 - 847 - tx, err := rp.db.BeginTx(r.Context(), nil) 848 - if err != nil { 849 - fail("Failed to add collaborator.", err) 850 - return 851 - } 852 - defer func() { 853 - tx.Rollback() 854 - err = rp.enforcer.E.LoadPolicy() 855 - if err != nil { 856 - fail("Failed to add collaborator.", err) 887 + if errs := errors.Join(err1, err2, err3); errs != nil { 888 + l.Error("failed to rollback changes", "errs", errs) 889 + return 857 890 } 858 - }() 891 + } 892 + defer rollback() 859 893 860 894 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 861 895 if err != nil { ··· 867 901 Did: syntax.DID(currentUser.Did), 868 902 Rkey: rkey, 869 903 SubjectDid: collaboratorIdent.DID, 870 - RepoAt: f.RepoAt, 904 + RepoAt: f.RepoAt(), 871 905 Created: createdAt, 872 906 }) 873 907 if err != nil { ··· 887 921 return 888 922 } 889 923 924 + // clear aturi to when everything is successful 925 + aturi = "" 926 + 890 927 rp.pages.HxRefresh(w) 891 928 } 892 929 893 930 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 894 931 user := rp.oauth.GetUser(r) 895 932 933 + noticeId := "operation-error" 896 934 f, err := rp.repoResolver.Resolve(r) 897 935 if err != nil { 898 936 log.Println("failed to get repo and knot", err) ··· 905 943 log.Println("failed to get authorized client", err) 906 944 return 907 945 } 908 - repoRkey := f.RepoAt.RecordKey().String() 909 946 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 910 947 Collection: tangled.RepoNSID, 911 948 Repo: user.Did, 912 - Rkey: repoRkey, 949 + Rkey: f.Rkey, 913 950 }) 914 951 if err != nil { 915 952 log.Printf("failed to delete record: %s", err) 916 - rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 953 + rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 917 954 return 918 955 } 919 - log.Println("removed repo record ", f.RepoAt.String()) 956 + log.Println("removed repo record ", f.RepoAt().String()) 920 957 921 958 client, err := rp.oauth.ServiceClient( 922 959 r, ··· 934 971 client, 935 972 &tangled.RepoDelete_Input{ 936 973 Did: f.OwnerDid(), 937 - Name: f.RepoName, 974 + Name: f.Name, 975 + Rkey: f.Rkey, 938 976 }, 939 977 ) 940 - if err != nil { 941 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 942 - if parseErr != nil { 943 - log.Printf("failed to delete repo from knot %s: %s", f.Knot, err) 944 - } else { 945 - log.Printf("failed to delete repo from knot %s: %s", f.Knot, xe.Error()) 946 - } 947 - // Continue anyway since we want to clean up local state 948 - } else { 949 - log.Println("removed repo from knot ", f.Knot) 978 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 979 + rp.pages.Notice(w, noticeId, err.Error()) 980 + return 950 981 } 982 + log.Println("deleted repo from knot") 951 983 952 984 tx, err := rp.db.BeginTx(r.Context(), nil) 953 985 if err != nil { ··· 966 998 // remove collaborator RBAC 967 999 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 968 1000 if err != nil { 969 - rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 1001 + rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 970 1002 return 971 1003 } 972 1004 for _, c := range repoCollaborators { ··· 978 1010 // remove repo RBAC 979 1011 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 980 1012 if err != nil { 981 - rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 1013 + rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 982 1014 return 983 1015 } 984 1016 985 1017 // remove repo from db 986 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 1018 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 987 1019 if err != nil { 988 - rp.pages.Notice(w, "settings-delete", "Failed to update appview") 1020 + rp.pages.Notice(w, noticeId, "Failed to update appview") 989 1021 return 990 1022 } 991 1023 log.Println("removed repo from db") ··· 1014 1046 return 1015 1047 } 1016 1048 1049 + noticeId := "operation-error" 1017 1050 branch := r.FormValue("branch") 1018 1051 if branch == "" { 1019 1052 http.Error(w, "malformed form", http.StatusBadRequest) ··· 1028 1061 ) 1029 1062 if err != nil { 1030 1063 log.Println("failed to connect to knot server:", err) 1031 - rp.pages.Notice(w, "repo-settings", "Failed to connect to knot server.") 1064 + rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 1032 1065 return 1033 1066 } 1034 1067 1035 - err = tangled.RepoSetDefaultBranch( 1068 + xe := tangled.RepoSetDefaultBranch( 1036 1069 r.Context(), 1037 1070 client, 1038 1071 &tangled.RepoSetDefaultBranch_Input{ 1039 - Repo: fmt.Sprintf("%s/%s", f.OwnerDid(), f.RepoName), 1072 + Repo: f.RepoAt().String(), 1040 1073 DefaultBranch: branch, 1041 1074 }, 1042 1075 ) 1043 - if err != nil { 1044 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 1045 - if parseErr != nil { 1046 - log.Printf("failed to set default branch: %s", err) 1047 - rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 1048 - } else { 1049 - log.Printf("failed to set default branch: %s", xe.Error()) 1050 - rp.pages.Notice(w, "repo-settings", fmt.Sprintf("Failed to set default branch: %s", xe.Message)) 1051 - } 1076 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 1077 + log.Println("xrpc failed", "err", xe) 1078 + rp.pages.Notice(w, noticeId, err.Error()) 1052 1079 return 1053 1080 } 1054 1081 1055 - w.Write(fmt.Append(nil, "default branch set to: ", branch)) 1082 + rp.pages.HxRefresh(w) 1056 1083 } 1057 1084 1058 1085 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { ··· 1081 1108 r, 1082 1109 oauth.WithService(f.Spindle), 1083 1110 oauth.WithLxm(lxm), 1111 + oauth.WithExp(60), 1084 1112 oauth.WithDev(rp.config.Core.Dev), 1085 1113 ) 1086 1114 if err != nil { ··· 1108 1136 r.Context(), 1109 1137 spindleClient, 1110 1138 &tangled.RepoAddSecret_Input{ 1111 - Repo: f.RepoAt.String(), 1139 + Repo: f.RepoAt().String(), 1112 1140 Key: key, 1113 1141 Value: value, 1114 1142 }, ··· 1126 1154 r.Context(), 1127 1155 spindleClient, 1128 1156 &tangled.RepoRemoveSecret_Input{ 1129 - Repo: f.RepoAt.String(), 1157 + Repo: f.RepoAt().String(), 1130 1158 Key: key, 1131 1159 }, 1132 1160 ) ··· 1167 1195 case "pipelines": 1168 1196 rp.pipelineSettings(w, r) 1169 1197 } 1170 - 1171 - // user := rp.oauth.GetUser(r) 1172 - // repoCollaborators, err := f.Collaborators(r.Context()) 1173 - // if err != nil { 1174 - // log.Println("failed to get collaborators", err) 1175 - // } 1176 - 1177 - // isCollaboratorInviteAllowed := false 1178 - // if user != nil { 1179 - // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 1180 - // if err == nil && ok { 1181 - // isCollaboratorInviteAllowed = true 1182 - // } 1183 - // } 1184 - 1185 - // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1186 - // if err != nil { 1187 - // log.Println("failed to create unsigned client", err) 1188 - // return 1189 - // } 1190 - 1191 - // result, err := us.Branches(f.OwnerDid(), f.RepoName) 1192 - // if err != nil { 1193 - // log.Println("failed to reach knotserver", err) 1194 - // return 1195 - // } 1196 - 1197 - // // all spindles that this user is a member of 1198 - // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1199 - // if err != nil { 1200 - // log.Println("failed to fetch spindles", err) 1201 - // return 1202 - // } 1203 - 1204 - // var secrets []*tangled.RepoListSecrets_Secret 1205 - // if f.Spindle != "" { 1206 - // if spindleClient, err := rp.oauth.ServiceClient( 1207 - // r, 1208 - // oauth.WithService(f.Spindle), 1209 - // oauth.WithLxm(tangled.RepoListSecretsNSID), 1210 - // oauth.WithDev(rp.config.Core.Dev), 1211 - // ); err != nil { 1212 - // log.Println("failed to create spindle client", err) 1213 - // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1214 - // log.Println("failed to fetch secrets", err) 1215 - // } else { 1216 - // secrets = resp.Secrets 1217 - // } 1218 - // } 1219 - 1220 - // rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1221 - // LoggedInUser: user, 1222 - // RepoInfo: f.RepoInfo(user), 1223 - // Collaborators: repoCollaborators, 1224 - // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1225 - // Branches: result.Branches, 1226 - // Spindles: spindles, 1227 - // CurrentSpindle: f.Spindle, 1228 - // Secrets: secrets, 1229 - // }) 1230 1198 } 1231 1199 1232 1200 func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { ··· 1239 1207 return 1240 1208 } 1241 1209 1242 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1210 + result, err := us.Branches(f.OwnerDid(), f.Name) 1243 1211 if err != nil { 1212 + rp.pages.Error503(w) 1244 1213 log.Println("failed to reach knotserver", err) 1245 1214 return 1246 1215 } ··· 1289 1258 r, 1290 1259 oauth.WithService(f.Spindle), 1291 1260 oauth.WithLxm(tangled.RepoListSecretsNSID), 1261 + oauth.WithExp(60), 1292 1262 oauth.WithDev(rp.config.Core.Dev), 1293 1263 ); err != nil { 1294 1264 log.Println("failed to create spindle client", err) 1295 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1265 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1296 1266 log.Println("failed to fetch secrets", err) 1297 1267 } else { 1298 1268 secrets = resp.Secrets ··· 1333 1303 } 1334 1304 1335 1305 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1306 + ref := chi.URLParam(r, "ref") 1307 + 1336 1308 user := rp.oauth.GetUser(r) 1337 1309 f, err := rp.repoResolver.Resolve(r) 1338 1310 if err != nil { ··· 1364 1336 client, 1365 1337 &tangled.RepoForkSync_Input{ 1366 1338 Did: user.Did, 1367 - Name: f.RepoName, 1368 - Source: repoInfo.Source.AtUri, 1369 - Branch: f.Ref, 1339 + Name: f.Name, 1340 + Source: repoInfo.Source.RepoAt().String(), 1341 + Branch: ref, 1370 1342 }, 1371 1343 ) 1372 - if err != nil { 1373 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 1374 - if parseErr != nil { 1375 - log.Printf("failed to sync repository fork: %s", err) 1376 - rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1377 - } else { 1378 - log.Printf("failed to sync repository fork: %s", xe.Error()) 1379 - rp.pages.Notice(w, "repo", fmt.Sprintf("Failed to sync repository fork: %s", xe.Message)) 1380 - } 1344 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1345 + rp.pages.Notice(w, "repo", err.Error()) 1381 1346 return 1382 1347 } 1383 1348 ··· 1410 1375 }) 1411 1376 1412 1377 case http.MethodPost: 1378 + l := rp.logger.With("handler", "ForkRepo") 1413 1379 1414 - knot := r.FormValue("knot") 1415 - if knot == "" { 1380 + targetKnot := r.FormValue("knot") 1381 + if targetKnot == "" { 1416 1382 rp.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 1417 1383 return 1418 1384 } 1385 + l = l.With("targetKnot", targetKnot) 1419 1386 1420 - ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1387 + ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create") 1421 1388 if err != nil || !ok { 1422 1389 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1423 1390 return 1424 1391 } 1425 1392 1426 - forkName := fmt.Sprintf("%s", f.RepoName) 1427 - 1393 + // choose a name for a fork 1394 + forkName := f.Name 1428 1395 // this check is *only* to see if the forked repo name already exists 1429 1396 // in the user's account. 1430 - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1397 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) 1431 1398 if err != nil { 1432 1399 if errors.Is(err, sql.ErrNoRows) { 1433 1400 // no existing repo with this name found, we can use the name as is ··· 1440 1407 // repo with this name already exists, append random string 1441 1408 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1442 1409 } 1443 - client, err := rp.oauth.ServiceClient( 1444 - r, 1445 - oauth.WithService(knot), 1446 - oauth.WithLxm(tangled.RepoForkNSID), 1447 - oauth.WithDev(rp.config.Core.Dev), 1448 - ) 1449 - 1450 - if err != nil { 1451 - log.Printf("error creating client for knot server: %v", err) 1452 - rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1453 - return 1454 - } 1410 + l = l.With("forkName", forkName) 1455 1411 1456 - var uri string 1412 + uri := "https" 1457 1413 if rp.config.Core.Dev { 1458 1414 uri = "http" 1459 - } else { 1460 - uri = "https" 1461 1415 } 1462 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1463 - sourceAt := f.RepoAt.String() 1416 + 1417 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1418 + l = l.With("cloneUrl", forkSourceUrl) 1419 + 1420 + sourceAt := f.RepoAt().String() 1464 1421 1422 + // create an atproto record for this fork 1465 1423 rkey := tid.TID() 1466 1424 repo := &db.Repo{ 1467 1425 Did: user.Did, 1468 1426 Name: forkName, 1469 - Knot: knot, 1427 + Knot: targetKnot, 1470 1428 Rkey: rkey, 1471 1429 Source: sourceAt, 1472 1430 } 1473 1431 1474 - tx, err := rp.db.BeginTx(r.Context(), nil) 1475 - if err != nil { 1476 - log.Println(err) 1477 - rp.pages.Notice(w, "repo", "Failed to save repository information.") 1478 - return 1479 - } 1480 - defer func() { 1481 - tx.Rollback() 1482 - err = rp.enforcer.E.LoadPolicy() 1483 - if err != nil { 1484 - log.Println("failed to rollback policies") 1485 - } 1486 - }() 1487 - 1488 - err = tangled.RepoFork( 1489 - r.Context(), 1490 - client, 1491 - &tangled.RepoFork_Input{ 1492 - Did: user.Did, 1493 - Name: &forkName, 1494 - Source: forkSourceUrl, 1495 - }, 1496 - ) 1497 - 1498 - if err != nil { 1499 - xe, err := xrpcerr.Unmarshal(err.Error()) 1500 - if err != nil { 1501 - log.Println(err) 1502 - rp.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1503 - return 1504 - } 1505 - 1506 - log.Println(xe.Error()) 1507 - rp.pages.Notice(w, "repo", fmt.Sprintf("Failed to create repository on knot server: %s.", xe.Message)) 1508 - return 1509 - } 1510 - 1511 1432 xrpcClient, err := rp.oauth.AuthorizedClient(r) 1512 1433 if err != nil { 1513 - log.Println("failed to get authorized client", err) 1514 - rp.pages.Notice(w, "repo", "Failed to create repository.") 1434 + l.Error("failed to create xrpcclient", "err", err) 1435 + rp.pages.Notice(w, "repo", "Failed to fork repository.") 1515 1436 return 1516 1437 } 1517 1438 ··· 1530 1451 }}, 1531 1452 }) 1532 1453 if err != nil { 1533 - log.Printf("failed to create record: %s", err) 1454 + l.Error("failed to write to PDS", "err", err) 1534 1455 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1535 1456 return 1536 1457 } 1537 - log.Println("created repo record: ", atresp.Uri) 1458 + 1459 + aturi := atresp.Uri 1460 + l = l.With("aturi", aturi) 1461 + l.Info("wrote to PDS") 1462 + 1463 + tx, err := rp.db.BeginTx(r.Context(), nil) 1464 + if err != nil { 1465 + l.Info("txn failed", "err", err) 1466 + rp.pages.Notice(w, "repo", "Failed to save repository information.") 1467 + return 1468 + } 1469 + 1470 + // The rollback function reverts a few things on failure: 1471 + // - the pending txn 1472 + // - the ACLs 1473 + // - the atproto record created 1474 + rollback := func() { 1475 + err1 := tx.Rollback() 1476 + err2 := rp.enforcer.E.LoadPolicy() 1477 + err3 := rollbackRecord(context.Background(), aturi, xrpcClient) 1478 + 1479 + // ignore txn complete errors, this is okay 1480 + if errors.Is(err1, sql.ErrTxDone) { 1481 + err1 = nil 1482 + } 1538 1483 1539 - repo.AtUri = atresp.Uri 1484 + if errs := errors.Join(err1, err2, err3); errs != nil { 1485 + l.Error("failed to rollback changes", "errs", errs) 1486 + return 1487 + } 1488 + } 1489 + defer rollback() 1490 + 1491 + client, err := rp.oauth.ServiceClient( 1492 + r, 1493 + oauth.WithService(targetKnot), 1494 + oauth.WithLxm(tangled.RepoCreateNSID), 1495 + oauth.WithDev(rp.config.Core.Dev), 1496 + ) 1497 + if err != nil { 1498 + l.Error("could not create service client", "err", err) 1499 + rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1500 + return 1501 + } 1502 + 1503 + err = tangled.RepoCreate( 1504 + r.Context(), 1505 + client, 1506 + &tangled.RepoCreate_Input{ 1507 + Rkey: rkey, 1508 + Source: &forkSourceUrl, 1509 + }, 1510 + ) 1511 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1512 + rp.pages.Notice(w, "repo", err.Error()) 1513 + return 1514 + } 1515 + 1540 1516 err = db.AddRepo(tx, repo) 1541 1517 if err != nil { 1542 1518 log.Println(err) ··· 1546 1522 1547 1523 // acls 1548 1524 p, _ := securejoin.SecureJoin(user.Did, forkName) 1549 - err = rp.enforcer.AddRepo(user.Did, knot, p) 1525 + err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 1550 1526 if err != nil { 1551 1527 log.Println(err) 1552 1528 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1567 1543 return 1568 1544 } 1569 1545 1546 + // reset the ATURI because the transaction completed successfully 1547 + aturi = "" 1548 + 1549 + rp.notifier.NewRepo(r.Context(), repo) 1570 1550 rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 1571 - return 1551 + } 1552 + } 1553 + 1554 + // this is used to rollback changes made to the PDS 1555 + // 1556 + // it is a no-op if the provided ATURI is empty 1557 + func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 1558 + if aturi == "" { 1559 + return nil 1572 1560 } 1561 + 1562 + parsed := syntax.ATURI(aturi) 1563 + 1564 + collection := parsed.Collection().String() 1565 + repo := parsed.Authority().String() 1566 + rkey := parsed.RecordKey().String() 1567 + 1568 + _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 1569 + Collection: collection, 1570 + Repo: repo, 1571 + Rkey: rkey, 1572 + }) 1573 + return err 1573 1574 } 1574 1575 1575 1576 func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { ··· 1587 1588 return 1588 1589 } 1589 1590 1590 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1591 + result, err := us.Branches(f.OwnerDid(), f.Name) 1591 1592 if err != nil { 1592 1593 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1593 1594 log.Println("failed to reach knotserver", err) ··· 1617 1618 head = queryHead 1618 1619 } 1619 1620 1620 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1621 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1621 1622 if err != nil { 1622 1623 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1623 1624 log.Println("failed to reach knotserver", err) ··· 1679 1680 return 1680 1681 } 1681 1682 1682 - branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1683 + branches, err := us.Branches(f.OwnerDid(), f.Name) 1683 1684 if err != nil { 1684 1685 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1685 1686 log.Println("failed to reach knotserver", err) 1686 1687 return 1687 1688 } 1688 1689 1689 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1690 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1690 1691 if err != nil { 1691 1692 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1692 1693 log.Println("failed to reach knotserver", err) 1693 1694 return 1694 1695 } 1695 1696 1696 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1697 + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1697 1698 if err != nil { 1698 1699 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1699 1700 log.Println("failed to compare", err)
+1
appview/repo/router.go
··· 10 10 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 11 r := chi.NewRouter() 12 12 r.Get("/", rp.RepoIndex) 13 + r.Get("/feed.atom", rp.RepoAtomFeed) 13 14 r.Get("/commits/{ref}", rp.RepoLog) 14 15 r.Route("/tree/{ref}", func(r chi.Router) { 15 16 r.Get("/", rp.RepoIndex)
+37 -104
appview/reporesolver/resolver.go
··· 7 7 "fmt" 8 8 "log" 9 9 "net/http" 10 - "net/url" 11 10 "path" 11 + "regexp" 12 12 "strings" 13 13 14 14 "github.com/bluesky-social/indigo/atproto/identity" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 15 securejoin "github.com/cyphar/filepath-securejoin" 17 16 "github.com/go-chi/chi/v5" 18 17 "tangled.sh/tangled.sh/core/appview/config" ··· 21 20 "tangled.sh/tangled.sh/core/appview/pages" 22 21 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 22 "tangled.sh/tangled.sh/core/idresolver" 24 - "tangled.sh/tangled.sh/core/knotclient" 25 23 "tangled.sh/tangled.sh/core/rbac" 26 24 ) 27 25 28 26 type ResolvedRepo struct { 29 - Knot string 30 - OwnerId identity.Identity 31 - RepoName string 32 - RepoAt syntax.ATURI 33 - Description string 34 - Spindle string 35 - CreatedAt string 36 - Ref string 37 - CurrentDir string 27 + db.Repo 28 + OwnerId identity.Identity 29 + CurrentDir string 30 + Ref string 38 31 39 32 rr *RepoResolver 40 33 } ··· 51 44 } 52 45 53 46 func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { 54 - repoName := chi.URLParam(r, "repo") 55 - knot, ok := r.Context().Value("knot").(string) 47 + repo, ok := r.Context().Value("repo").(*db.Repo) 56 48 if !ok { 57 - log.Println("malformed middleware") 49 + log.Println("malformed middleware: `repo` not exist in context") 58 50 return nil, fmt.Errorf("malformed middleware") 59 51 } 60 52 id, ok := r.Context().Value("resolvedId").(identity.Identity) ··· 63 55 return nil, fmt.Errorf("malformed middleware") 64 56 } 65 57 66 - repoAt, ok := r.Context().Value("repoAt").(string) 67 - if !ok { 68 - log.Println("malformed middleware") 69 - return nil, fmt.Errorf("malformed middleware") 70 - } 71 - 72 - parsedRepoAt, err := syntax.ParseATURI(repoAt) 73 - if err != nil { 74 - log.Println("malformed repo at-uri") 75 - return nil, fmt.Errorf("malformed middleware") 76 - } 77 - 58 + currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 78 59 ref := chi.URLParam(r, "ref") 79 60 80 - if ref == "" { 81 - us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev) 82 - if err != nil { 83 - return nil, err 84 - } 85 - 86 - defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 87 - if err != nil { 88 - return nil, err 89 - } 90 - 91 - ref = defaultBranch.Branch 92 - } 93 - 94 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 95 - 96 - // pass through values from the middleware 97 - description, ok := r.Context().Value("repoDescription").(string) 98 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 99 - spindle, ok := r.Context().Value("repoSpindle").(string) 100 - 101 61 return &ResolvedRepo{ 102 - Knot: knot, 103 - OwnerId: id, 104 - RepoName: repoName, 105 - RepoAt: parsedRepoAt, 106 - Description: description, 107 - CreatedAt: addedAt, 108 - Ref: ref, 109 - CurrentDir: currentDir, 110 - Spindle: spindle, 62 + Repo: *repo, 63 + OwnerId: id, 64 + CurrentDir: currentDir, 65 + Ref: ref, 111 66 112 67 rr: rr, 113 68 }, nil ··· 126 81 127 82 var p string 128 83 if handle != "" && !handle.IsInvalidHandle() { 129 - p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 84 + p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) 130 85 } else { 131 - p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 86 + p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) 132 87 } 133 88 134 - return p 135 - } 136 - 137 - func (f *ResolvedRepo) DidSlashRepo() string { 138 - p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 139 89 return p 140 90 } 141 91 ··· 187 137 // this function is a bit weird since it now returns RepoInfo from an entirely different 188 138 // package. we should refactor this or get rid of RepoInfo entirely. 189 139 func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 140 + repoAt := f.RepoAt() 190 141 isStarred := false 191 142 if user != nil { 192 - isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 143 + isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 193 144 } 194 145 195 - starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 146 + starCount, err := db.GetStarCount(f.rr.execer, repoAt) 196 147 if err != nil { 197 - log.Println("failed to get star count for ", f.RepoAt) 148 + log.Println("failed to get star count for ", repoAt) 198 149 } 199 - issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 150 + issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 200 151 if err != nil { 201 - log.Println("failed to get issue count for ", f.RepoAt) 152 + log.Println("failed to get issue count for ", repoAt) 202 153 } 203 - pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 154 + pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 204 155 if err != nil { 205 - log.Println("failed to get issue count for ", f.RepoAt) 156 + log.Println("failed to get issue count for ", repoAt) 206 157 } 207 - source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 158 + source, err := db.GetRepoSource(f.rr.execer, repoAt) 208 159 if errors.Is(err, sql.ErrNoRows) { 209 160 source = "" 210 161 } else if err != nil { 211 - log.Println("failed to get repo source for ", f.RepoAt, err) 162 + log.Println("failed to get repo source for ", repoAt, err) 212 163 } 213 164 214 165 var sourceRepo *db.Repo ··· 228 179 } 229 180 230 181 knot := f.Knot 231 - var disableFork bool 232 - us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev) 233 - if err != nil { 234 - log.Printf("failed to create unsigned client for %s: %v", knot, err) 235 - } else { 236 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 237 - if err != nil { 238 - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 239 - } 240 - 241 - if len(result.Branches) == 0 { 242 - disableFork = true 243 - } 244 - } 245 182 246 183 repoInfo := repoinfo.RepoInfo{ 247 184 OwnerDid: f.OwnerDid(), 248 185 OwnerHandle: f.OwnerHandle(), 249 - Name: f.RepoName, 250 - RepoAt: f.RepoAt, 186 + Name: f.Name, 187 + RepoAt: repoAt, 251 188 Description: f.Description, 252 - Ref: f.Ref, 253 189 IsStarred: isStarred, 254 190 Knot: knot, 255 191 Spindle: f.Spindle, ··· 259 195 IssueCount: issueCount, 260 196 PullCount: pullCount, 261 197 }, 262 - DisableFork: disableFork, 263 - CurrentDir: f.CurrentDir, 198 + CurrentDir: f.CurrentDir, 199 + Ref: f.Ref, 264 200 } 265 201 266 202 if sourceRepo != nil { ··· 284 220 // after the ref. for example: 285 221 // 286 222 // /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 287 - func extractPathAfterRef(fullPath, ref string) string { 223 + func extractPathAfterRef(fullPath string) string { 288 224 fullPath = strings.TrimPrefix(fullPath, "/") 289 225 290 - ref = url.PathEscape(ref) 226 + // match blob/, tree/, or raw/ followed by any ref and then a slash 227 + // 228 + // captures everything after the final slash 229 + pattern := `(?:blob|tree|raw)/[^/]+/(.*)$` 291 230 292 - prefixes := []string{ 293 - fmt.Sprintf("blob/%s/", ref), 294 - fmt.Sprintf("tree/%s/", ref), 295 - fmt.Sprintf("raw/%s/", ref), 296 - } 231 + re := regexp.MustCompile(pattern) 232 + matches := re.FindStringSubmatch(fullPath) 297 233 298 - for _, prefix := range prefixes { 299 - idx := strings.Index(fullPath, prefix) 300 - if idx != -1 { 301 - return fullPath[idx+len(prefix):] 302 - } 234 + if len(matches) > 1 { 235 + return matches[1] 303 236 } 304 237 305 238 return ""
+5 -1
appview/serververify/verify.go
··· 129 129 }() 130 130 131 131 // mark as registered 132 - err = db.Register(tx, domain) 132 + err = db.MarkRegistered( 133 + tx, 134 + db.FilterEq("did", owner), 135 + db.FilterEq("domain", domain), 136 + ) 133 137 if err != nil { 134 138 return fmt.Errorf("failed to register domain: %w", err) 135 139 }
+44 -9
appview/settings/settings.go
··· 33 33 Config *config.Config 34 34 } 35 35 36 + type tab = map[string]any 37 + 38 + var ( 39 + settingsTabs []tab = []tab{ 40 + {"Name": "profile", "Icon": "user"}, 41 + {"Name": "keys", "Icon": "key"}, 42 + {"Name": "emails", "Icon": "mail"}, 43 + } 44 + ) 45 + 36 46 func (s *Settings) Router() http.Handler { 37 47 r := chi.NewRouter() 38 48 39 49 r.Use(middleware.AuthMiddleware(s.OAuth)) 40 50 41 - r.Get("/", s.settings) 51 + // settings pages 52 + r.Get("/", s.profileSettings) 53 + r.Get("/profile", s.profileSettings) 42 54 43 55 r.Route("/keys", func(r chi.Router) { 56 + r.Get("/", s.keysSettings) 44 57 r.Put("/", s.keys) 45 58 r.Delete("/", s.keys) 46 59 }) 47 60 48 61 r.Route("/emails", func(r chi.Router) { 62 + r.Get("/", s.emailsSettings) 49 63 r.Put("/", s.emails) 50 64 r.Delete("/", s.emails) 51 65 r.Get("/verify", s.emailsVerify) ··· 56 70 return r 57 71 } 58 72 59 - func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { 73 + func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 74 + user := s.OAuth.GetUser(r) 75 + 76 + s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 77 + LoggedInUser: user, 78 + Tabs: settingsTabs, 79 + Tab: "profile", 80 + }) 81 + } 82 + 83 + func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { 60 84 user := s.OAuth.GetUser(r) 61 85 pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did) 62 86 if err != nil { 63 87 log.Println(err) 64 88 } 65 89 90 + s.Pages.UserKeysSettings(w, pages.UserKeysSettingsParams{ 91 + LoggedInUser: user, 92 + PubKeys: pubKeys, 93 + Tabs: settingsTabs, 94 + Tab: "keys", 95 + }) 96 + } 97 + 98 + func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) { 99 + user := s.OAuth.GetUser(r) 66 100 emails, err := db.GetAllEmails(s.Db, user.Did) 67 101 if err != nil { 68 102 log.Println(err) 69 103 } 70 104 71 - s.Pages.Settings(w, pages.SettingsParams{ 105 + s.Pages.UserEmailsSettings(w, pages.UserEmailsSettingsParams{ 72 106 LoggedInUser: user, 73 - PubKeys: pubKeys, 74 107 Emails: emails, 108 + Tabs: settingsTabs, 109 + Tab: "emails", 75 110 }) 76 111 } 77 112 ··· 201 236 return 202 237 } 203 238 204 - s.Pages.HxLocation(w, "/settings") 239 + s.Pages.HxLocation(w, "/settings/emails") 205 240 return 206 241 } 207 242 } ··· 244 279 return 245 280 } 246 281 247 - http.Redirect(w, r, "/settings", http.StatusSeeOther) 282 + http.Redirect(w, r, "/settings/emails", http.StatusSeeOther) 248 283 } 249 284 250 285 func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) { ··· 339 374 return 340 375 } 341 376 342 - s.Pages.HxLocation(w, "/settings") 377 + s.Pages.HxLocation(w, "/settings/emails") 343 378 } 344 379 345 380 func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { ··· 410 445 return 411 446 } 412 447 413 - s.Pages.HxLocation(w, "/settings") 448 + s.Pages.HxLocation(w, "/settings/keys") 414 449 return 415 450 416 451 case http.MethodDelete: ··· 455 490 } 456 491 log.Println("deleted successfully") 457 492 458 - s.Pages.HxLocation(w, "/settings") 493 + s.Pages.HxLocation(w, "/settings/keys") 459 494 return 460 495 } 461 496 }
-13
appview/spindles/spindles.go
··· 113 113 return 114 114 } 115 115 116 - identsToResolve := make([]string, len(members)) 117 - copy(identsToResolve, members) 118 - resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve) 119 - didHandleMap := make(map[string]string) 120 - for _, identity := range resolvedIds { 121 - if !identity.Handle.IsInvalidHandle() { 122 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 123 - } else { 124 - didHandleMap[identity.DID.String()] = identity.DID.String() 125 - } 126 - } 127 - 128 116 // organize repos by did 129 117 repoMap := make(map[string][]db.Repo) 130 118 for _, r := range repos { ··· 136 124 Spindle: spindle, 137 125 Members: members, 138 126 Repos: repoMap, 139 - DidHandleMap: didHandleMap, 140 127 }) 141 128 } 142 129
+9 -12
appview/state/git_http.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 + "maps" 6 7 "net/http" 7 8 8 9 "github.com/bluesky-social/indigo/atproto/identity" 9 10 "github.com/go-chi/chi/v5" 11 + "tangled.sh/tangled.sh/core/appview/db" 10 12 ) 11 13 12 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 13 15 user := r.Context().Value("resolvedId").(identity.Identity) 14 - knot := r.Context().Value("knot").(string) 15 - repo := chi.URLParam(r, "repo") 16 + repo := r.Context().Value("repo").(*db.Repo) 16 17 17 18 scheme := "https" 18 19 if s.config.Core.Dev { 19 20 scheme = "http" 20 21 } 21 22 22 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 23 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 23 24 s.proxyRequest(w, r, targetURL) 24 25 25 26 } ··· 30 31 http.Error(w, "failed to resolve user", http.StatusInternalServerError) 31 32 return 32 33 } 33 - knot := r.Context().Value("knot").(string) 34 - repo := chi.URLParam(r, "repo") 34 + repo := r.Context().Value("repo").(*db.Repo) 35 35 36 36 scheme := "https" 37 37 if s.config.Core.Dev { 38 38 scheme = "http" 39 39 } 40 40 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 42 s.proxyRequest(w, r, targetURL) 43 43 } 44 44 ··· 48 48 http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 49 return 50 50 } 51 - knot := r.Context().Value("knot").(string) 52 - repo := chi.URLParam(r, "repo") 51 + repo := r.Context().Value("repo").(*db.Repo) 53 52 54 53 scheme := "https" 55 54 if s.config.Core.Dev { 56 55 scheme = "http" 57 56 } 58 57 59 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 58 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 60 59 s.proxyRequest(w, r, targetURL) 61 60 } 62 61 ··· 85 84 defer resp.Body.Close() 86 85 87 86 // Copy response headers 88 - for k, v := range resp.Header { 89 - w.Header()[k] = v 90 - } 87 + maps.Copy(w.Header(), resp.Header) 91 88 92 89 // Set response status code 93 90 w.WriteHeader(resp.StatusCode)
+5 -2
appview/state/knotstream.go
··· 24 24 ) 25 25 26 26 func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) { 27 - knots, err := db.GetCompletedRegistrations(d) 27 + knots, err := db.GetRegistrations( 28 + d, 29 + db.FilterIsNot("registered", "null"), 30 + ) 28 31 if err != nil { 29 32 return nil, err 30 33 } 31 34 32 35 srcs := make(map[ec.Source]struct{}) 33 36 for _, k := range knots { 34 - s := ec.NewKnotSource(k) 37 + s := ec.NewKnotSource(k.Domain) 35 38 srcs[s] = struct{}{} 36 39 } 37 40
+291 -155
appview/state/profile.go
··· 17 17 "github.com/gorilla/feeds" 18 18 "tangled.sh/tangled.sh/core/api/tangled" 19 19 "tangled.sh/tangled.sh/core/appview/db" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 20 21 "tangled.sh/tangled.sh/core/appview/pages" 21 22 ) 22 23 ··· 24 25 tabVal := r.URL.Query().Get("tab") 25 26 switch tabVal { 26 27 case "": 27 - s.profilePage(w, r) 28 + s.profileHomePage(w, r) 28 29 case "repos": 29 30 s.reposPage(w, r) 31 + case "followers": 32 + s.followersPage(w, r) 33 + case "following": 34 + s.followingPage(w, r) 30 35 } 31 36 } 32 37 33 - func (s *State) profilePage(w http.ResponseWriter, r *http.Request) { 38 + type ProfilePageParams struct { 39 + Id identity.Identity 40 + LoggedInUser *oauth.User 41 + Card pages.ProfileCard 42 + } 43 + 44 + func (s *State) profilePage(w http.ResponseWriter, r *http.Request) *ProfilePageParams { 34 45 didOrHandle := chi.URLParam(r, "user") 35 46 if didOrHandle == "" { 36 - http.Error(w, "Bad request", http.StatusBadRequest) 37 - return 47 + http.Error(w, "bad request", http.StatusBadRequest) 48 + return nil 38 49 } 39 50 40 51 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 41 52 if !ok { 42 - s.pages.Error404(w) 43 - return 53 + log.Printf("malformed middleware") 54 + w.WriteHeader(http.StatusInternalServerError) 55 + return nil 44 56 } 57 + did := ident.DID.String() 45 58 46 - profile, err := db.GetProfile(s.db, ident.DID.String()) 59 + profile, err := db.GetProfile(s.db, did) 47 60 if err != nil { 48 - log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 61 + log.Printf("getting profile data for %s: %s", did, err) 62 + s.pages.Error500(w) 63 + return nil 49 64 } 50 65 66 + followStats, err := db.GetFollowerFollowingCount(s.db, did) 67 + if err != nil { 68 + log.Printf("getting follow stats for %s: %s", did, err) 69 + } 70 + 71 + loggedInUser := s.oauth.GetUser(r) 72 + followStatus := db.IsNotFollowing 73 + if loggedInUser != nil { 74 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 75 + } 76 + 77 + return &ProfilePageParams{ 78 + Id: ident, 79 + LoggedInUser: loggedInUser, 80 + Card: pages.ProfileCard{ 81 + UserDid: did, 82 + UserHandle: ident.Handle.String(), 83 + Profile: profile, 84 + FollowStatus: followStatus, 85 + FollowersCount: followStats.Followers, 86 + FollowingCount: followStats.Following, 87 + }, 88 + } 89 + } 90 + 91 + func (s *State) profileHomePage(w http.ResponseWriter, r *http.Request) { 92 + pageWithProfile := s.profilePage(w, r) 93 + if pageWithProfile == nil { 94 + return 95 + } 96 + 97 + id := pageWithProfile.Id 51 98 repos, err := db.GetRepos( 52 99 s.db, 53 100 0, 54 - db.FilterEq("did", ident.DID.String()), 101 + db.FilterEq("did", id.DID), 55 102 ) 56 103 if err != nil { 57 - log.Printf("getting repos for %s: %s", ident.DID.String(), err) 104 + log.Printf("getting repos for %s: %s", id.DID, err) 58 105 } 59 106 107 + profile := pageWithProfile.Card.Profile 60 108 // filter out ones that are pinned 61 109 pinnedRepos := []db.Repo{} 62 110 for i, r := range repos { ··· 71 119 } 72 120 } 73 121 74 - collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 122 + collaboratingRepos, err := db.CollaboratingIn(s.db, id.DID.String()) 75 123 if err != nil { 76 - log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 124 + log.Printf("getting collaborating repos for %s: %s", id.DID, err) 77 125 } 78 126 79 127 pinnedCollaboratingRepos := []db.Repo{} ··· 84 132 } 85 133 } 86 134 87 - timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 135 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 88 136 if err != nil { 89 - log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 137 + log.Printf("failed to create profile timeline for %s: %s", id.DID, err) 90 138 } 91 139 92 140 var didsToResolve []string ··· 108 156 } 109 157 } 110 158 111 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve) 112 - didHandleMap := make(map[string]string) 113 - for _, identity := range resolvedIds { 114 - if !identity.Handle.IsInvalidHandle() { 115 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 116 - } else { 117 - didHandleMap[identity.DID.String()] = identity.DID.String() 118 - } 119 - } 120 - 121 - followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 122 - if err != nil { 123 - log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 124 - } 125 - 126 - loggedInUser := s.oauth.GetUser(r) 127 - followStatus := db.IsNotFollowing 128 - if loggedInUser != nil { 129 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 130 - } 131 - 132 159 now := time.Now() 133 160 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 134 161 punchcard, err := db.MakePunchcard( 135 162 s.db, 136 - db.FilterEq("did", ident.DID.String()), 163 + db.FilterEq("did", id.DID), 137 164 db.FilterGte("date", startOfYear.Format(time.DateOnly)), 138 165 db.FilterLte("date", now.Format(time.DateOnly)), 139 166 ) 140 167 if err != nil { 141 - log.Println("failed to get punchcard for did", "did", ident.DID.String(), "err", err) 168 + log.Println("failed to get punchcard for did", "did", id.DID, "err", err) 142 169 } 143 170 144 - s.pages.ProfilePage(w, pages.ProfilePageParams{ 145 - LoggedInUser: loggedInUser, 171 + s.pages.ProfileHomePage(w, pages.ProfileHomePageParams{ 172 + LoggedInUser: pageWithProfile.LoggedInUser, 146 173 Repos: pinnedRepos, 147 174 CollaboratingRepos: pinnedCollaboratingRepos, 148 - DidHandleMap: didHandleMap, 149 - Card: pages.ProfileCard{ 150 - UserDid: ident.DID.String(), 151 - UserHandle: ident.Handle.String(), 152 - Profile: profile, 153 - FollowStatus: followStatus, 154 - Followers: followers, 155 - Following: following, 156 - }, 157 - Punchcard: punchcard, 158 - ProfileTimeline: timeline, 175 + Card: pageWithProfile.Card, 176 + Punchcard: punchcard, 177 + ProfileTimeline: timeline, 159 178 }) 160 179 } 161 180 162 181 func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 163 - ident, ok := r.Context().Value("resolvedId").(identity.Identity) 164 - if !ok { 165 - s.pages.Error404(w) 182 + pageWithProfile := s.profilePage(w, r) 183 + if pageWithProfile == nil { 166 184 return 167 185 } 168 186 169 - profile, err := db.GetProfile(s.db, ident.DID.String()) 170 - if err != nil { 171 - log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 172 - } 173 - 187 + id := pageWithProfile.Id 174 188 repos, err := db.GetRepos( 175 189 s.db, 176 190 0, 177 - db.FilterEq("did", ident.DID.String()), 191 + db.FilterEq("did", id.DID), 178 192 ) 179 193 if err != nil { 180 - log.Printf("getting repos for %s: %s", ident.DID.String(), err) 194 + log.Printf("getting repos for %s: %s", id.DID, err) 195 + } 196 + 197 + s.pages.ReposPage(w, pages.ReposPageParams{ 198 + LoggedInUser: pageWithProfile.LoggedInUser, 199 + Repos: repos, 200 + Card: pageWithProfile.Card, 201 + }) 202 + } 203 + 204 + type FollowsPageParams struct { 205 + LoggedInUser *oauth.User 206 + Follows []pages.FollowCard 207 + Card pages.ProfileCard 208 + } 209 + 210 + func (s *State) followPage(w http.ResponseWriter, r *http.Request, fetchFollows func(db.Execer, string) ([]db.Follow, error), extractDid func(db.Follow) string) (FollowsPageParams, error) { 211 + pageWithProfile := s.profilePage(w, r) 212 + if pageWithProfile == nil { 213 + return FollowsPageParams{}, nil 214 + } 215 + 216 + id := pageWithProfile.Id 217 + loggedInUser := pageWithProfile.LoggedInUser 218 + 219 + follows, err := fetchFollows(s.db, id.DID.String()) 220 + if err != nil { 221 + log.Printf("getting followers for %s: %s", id.DID, err) 222 + return FollowsPageParams{}, err 223 + } 224 + 225 + if len(follows) == 0 { 226 + return FollowsPageParams{ 227 + LoggedInUser: loggedInUser, 228 + Follows: []pages.FollowCard{}, 229 + Card: pageWithProfile.Card, 230 + }, nil 181 231 } 182 232 183 - loggedInUser := s.oauth.GetUser(r) 184 - followStatus := db.IsNotFollowing 185 - if loggedInUser != nil { 186 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 233 + followDids := make([]string, 0, len(follows)) 234 + for _, follow := range follows { 235 + followDids = append(followDids, extractDid(follow)) 187 236 } 188 237 189 - followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 238 + profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 190 239 if err != nil { 191 - log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 240 + log.Printf("getting profile for %s: %s", followDids, err) 241 + return FollowsPageParams{}, err 242 + } 243 + 244 + followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) 245 + if err != nil { 246 + log.Printf("getting follow counts for %s: %s", followDids, err) 247 + } 248 + 249 + var loggedInUserFollowing map[string]struct{} 250 + if loggedInUser != nil { 251 + following, err := db.GetFollowing(s.db, loggedInUser.Did) 252 + if err != nil { 253 + return FollowsPageParams{}, err 254 + } 255 + if len(following) > 0 { 256 + loggedInUserFollowing = make(map[string]struct{}, len(following)) 257 + for _, follow := range following { 258 + loggedInUserFollowing[follow.SubjectDid] = struct{}{} 259 + } 260 + } 192 261 } 193 262 194 - s.pages.ReposPage(w, pages.ReposPageParams{ 263 + followCards := make([]pages.FollowCard, 0, len(follows)) 264 + for _, did := range followDids { 265 + followStats, exists := followStatsMap[did] 266 + if !exists { 267 + followStats = db.FollowStats{} 268 + } 269 + followStatus := db.IsNotFollowing 270 + if loggedInUserFollowing != nil { 271 + if _, exists := loggedInUserFollowing[did]; exists { 272 + followStatus = db.IsFollowing 273 + } else if loggedInUser.Did == did { 274 + followStatus = db.IsSelf 275 + } 276 + } 277 + var profile *db.Profile 278 + if p, exists := profiles[did]; exists { 279 + profile = p 280 + } else { 281 + profile = &db.Profile{} 282 + profile.Did = did 283 + } 284 + followCards = append(followCards, pages.FollowCard{ 285 + UserDid: did, 286 + FollowStatus: followStatus, 287 + FollowersCount: followStats.Followers, 288 + FollowingCount: followStats.Following, 289 + Profile: profile, 290 + }) 291 + } 292 + 293 + return FollowsPageParams{ 195 294 LoggedInUser: loggedInUser, 196 - Repos: repos, 197 - DidHandleMap: map[string]string{ident.DID.String(): ident.Handle.String()}, 198 - Card: pages.ProfileCard{ 199 - UserDid: ident.DID.String(), 200 - UserHandle: ident.Handle.String(), 201 - Profile: profile, 202 - FollowStatus: followStatus, 203 - Followers: followers, 204 - Following: following, 205 - }, 295 + Follows: followCards, 296 + Card: pageWithProfile.Card, 297 + }, nil 298 + } 299 + 300 + func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 301 + followPage, err := s.followPage(w, r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 302 + if err != nil { 303 + s.pages.Notice(w, "all-followers", "Failed to load followers") 304 + return 305 + } 306 + 307 + s.pages.FollowersPage(w, pages.FollowersPageParams{ 308 + LoggedInUser: followPage.LoggedInUser, 309 + Followers: followPage.Follows, 310 + Card: followPage.Card, 206 311 }) 207 312 } 208 313 209 - func (s *State) feedFromRequest(w http.ResponseWriter, r *http.Request) *feeds.Feed { 314 + func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 315 + followPage, err := s.followPage(w, r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 316 + if err != nil { 317 + s.pages.Notice(w, "all-following", "Failed to load following") 318 + return 319 + } 320 + 321 + s.pages.FollowingPage(w, pages.FollowingPageParams{ 322 + LoggedInUser: followPage.LoggedInUser, 323 + Following: followPage.Follows, 324 + Card: followPage.Card, 325 + }) 326 + } 327 + 328 + func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 210 329 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 211 330 if !ok { 212 331 s.pages.Error404(w) 213 - return nil 332 + return 214 333 } 215 334 216 - feed, err := s.GetProfileFeed(r.Context(), ident.Handle.String(), ident.DID.String()) 335 + feed, err := s.getProfileFeed(r.Context(), &ident) 217 336 if err != nil { 218 337 s.pages.Error500(w) 219 - return nil 338 + return 220 339 } 221 340 222 - return feed 223 - } 224 - 225 - func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 226 - feed := s.feedFromRequest(w, r) 227 341 if feed == nil { 228 342 return 229 343 } ··· 238 352 w.Write([]byte(atom)) 239 353 } 240 354 241 - func (s *State) GetProfileFeed(ctx context.Context, handle string, did string) (*feeds.Feed, error) { 242 - timeline, err := db.MakeProfileTimeline(s.db, did) 355 + func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 356 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 243 357 if err != nil { 244 358 return nil, err 245 359 } 246 360 247 361 author := &feeds.Author{ 248 - Name: fmt.Sprintf("@%s", handle), 362 + Name: fmt.Sprintf("@%s", id.Handle), 249 363 } 250 - feed := &feeds.Feed{ 251 - Title: fmt.Sprintf("timeline feed for %s", author.Name), 252 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, handle), Type: "text/html", Rel: "alternate"}, 364 + 365 + feed := feeds.Feed{ 366 + Title: fmt.Sprintf("%s's timeline", author.Name), 367 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 253 368 Items: make([]*feeds.Item, 0), 254 369 Updated: time.UnixMilli(0), 255 370 Author: author, 256 371 } 372 + 257 373 for _, byMonth := range timeline.ByMonth { 258 - for _, pull := range byMonth.PullEvents.Items { 259 - owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 260 - if err != nil { 261 - return nil, err 262 - } 263 - feed.Items = append(feed.Items, &feeds.Item{ 264 - Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 265 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 266 - Created: pull.Created, 267 - Author: author, 268 - }) 269 - for _, submission := range pull.Submissions { 270 - feed.Items = append(feed.Items, &feeds.Item{ 271 - Title: fmt.Sprintf("%s submitted pull request '%s' (round #%d) in @%s/%s", author.Name, pull.Title, submission.RoundNumber, owner.Handle, pull.Repo.Name), 272 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 273 - Created: submission.Created, 274 - Author: author, 275 - }) 276 - } 374 + if err := s.addPullRequestItems(ctx, &feed, byMonth.PullEvents.Items, author); err != nil { 375 + return nil, err 277 376 } 278 - for _, issue := range byMonth.IssueEvents.Items { 279 - owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 280 - if err != nil { 281 - return nil, err 282 - } 283 - feed.Items = append(feed.Items, &feeds.Item{ 284 - Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 285 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 286 - Created: issue.Created, 287 - Author: author, 288 - }) 377 + if err := s.addIssueItems(ctx, &feed, byMonth.IssueEvents.Items, author); err != nil { 378 + return nil, err 289 379 } 290 - for _, repo := range byMonth.RepoEvents { 291 - var title string 292 - if repo.Source != nil { 293 - id, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 294 - if err != nil { 295 - return nil, err 296 - } 297 - title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, id.Handle, repo.Source.Name, repo.Repo.Name) 298 - } else { 299 - title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 300 - } 301 - feed.Items = append(feed.Items, &feeds.Item{ 302 - Title: title, 303 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, handle, repo.Repo.Name), Type: "text/html", Rel: "alternate"}, 304 - Created: repo.Repo.Created, 305 - Author: author, 306 - }) 380 + if err := s.addRepoItems(ctx, &feed, byMonth.RepoEvents, author); err != nil { 381 + return nil, err 307 382 } 308 383 } 384 + 309 385 slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 310 386 return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 311 387 }) 388 + 312 389 if len(feed.Items) > 0 { 313 390 feed.Updated = feed.Items[0].Created 314 391 } 315 392 316 - return feed, nil 393 + return &feed, nil 394 + } 395 + 396 + func (s *State) addPullRequestItems(ctx context.Context, feed *feeds.Feed, pulls []*db.Pull, author *feeds.Author) error { 397 + for _, pull := range pulls { 398 + owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 399 + if err != nil { 400 + return err 401 + } 402 + 403 + // Add pull request creation item 404 + feed.Items = append(feed.Items, s.createPullRequestItem(pull, owner, author)) 405 + } 406 + return nil 407 + } 408 + 409 + func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*db.Issue, author *feeds.Author) error { 410 + for _, issue := range issues { 411 + owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 412 + if err != nil { 413 + return err 414 + } 415 + 416 + feed.Items = append(feed.Items, s.createIssueItem(issue, owner, author)) 417 + } 418 + return nil 419 + } 420 + 421 + func (s *State) addRepoItems(ctx context.Context, feed *feeds.Feed, repos []db.RepoEvent, author *feeds.Author) error { 422 + for _, repo := range repos { 423 + item, err := s.createRepoItem(ctx, repo, author) 424 + if err != nil { 425 + return err 426 + } 427 + feed.Items = append(feed.Items, item) 428 + } 429 + return nil 430 + } 431 + 432 + func (s *State) createPullRequestItem(pull *db.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 433 + return &feeds.Item{ 434 + Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 435 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 436 + Created: pull.Created, 437 + Author: author, 438 + } 439 + } 440 + 441 + func (s *State) createIssueItem(issue *db.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 442 + return &feeds.Item{ 443 + Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 444 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 445 + Created: issue.Created, 446 + Author: author, 447 + } 448 + } 449 + 450 + func (s *State) createRepoItem(ctx context.Context, repo db.RepoEvent, author *feeds.Author) (*feeds.Item, error) { 451 + var title string 452 + if repo.Source != nil { 453 + sourceOwner, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 454 + if err != nil { 455 + return nil, err 456 + } 457 + title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, sourceOwner.Handle, repo.Source.Name, repo.Repo.Name) 458 + } else { 459 + title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 460 + } 461 + 462 + return &feeds.Item{ 463 + Title: title, 464 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 465 + Created: repo.Repo.Created, 466 + Author: author, 467 + }, nil 317 468 } 318 469 319 470 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { ··· 518 669 }) 519 670 } 520 671 521 - var didsToResolve []string 522 - for _, r := range allRepos { 523 - didsToResolve = append(didsToResolve, r.Did) 524 - } 525 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve) 526 - didHandleMap := make(map[string]string) 527 - for _, identity := range resolvedIds { 528 - if !identity.Handle.IsInvalidHandle() { 529 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 530 - } else { 531 - didHandleMap[identity.DID.String()] = identity.DID.String() 532 - } 533 - } 534 - 535 672 s.pages.EditPinsFragment(w, pages.EditPinsParams{ 536 673 LoggedInUser: user, 537 674 Profile: profile, 538 675 AllRepos: allRepos, 539 - DidHandleMap: didHandleMap, 540 676 }) 541 677 }
+14 -3
appview/state/router.go
··· 32 32 s.pages, 33 33 ) 34 34 35 + router.Get("/favicon.svg", s.Favicon) 36 + router.Get("/favicon.ico", s.Favicon) 37 + 38 + userRouter := s.UserRouter(&middleware) 39 + standardRouter := s.StandardRouter(&middleware) 40 + 35 41 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 36 42 pat := chi.URLParam(r, "*") 37 43 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 38 - s.UserRouter(&middleware).ServeHTTP(w, r) 44 + userRouter.ServeHTTP(w, r) 39 45 } else { 40 46 // Check if the first path element is a valid handle without '@' or a flattened DID 41 47 pathParts := strings.SplitN(pat, "/", 2) ··· 58 64 return 59 65 } 60 66 } 61 - s.StandardRouter(&middleware).ServeHTTP(w, r) 67 + standardRouter.ServeHTTP(w, r) 62 68 } 63 69 }) 64 70 ··· 72 78 r.Get("/", s.Profile) 73 79 r.Get("/feed.atom", s.AtomFeedPage) 74 80 81 + // redirect /@handle/repo.git -> /@handle/repo 82 + r.Get("/{repo}.git", func(w http.ResponseWriter, r *http.Request) { 83 + nonDotGitPath := strings.TrimSuffix(r.URL.Path, ".git") 84 + http.Redirect(w, r, nonDotGitPath, http.StatusMovedPermanently) 85 + }) 86 + 75 87 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 76 88 r.Use(mw.GoImport()) 77 - 78 89 r.Mount("/", s.RepoRouter(mw)) 79 90 r.Mount("/issues", s.IssuesRouter(mw)) 80 91 r.Mount("/pulls", s.PullsRouter(mw))
+119 -73
appview/state/state.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "database/sql" 6 + "errors" 5 7 "fmt" 6 8 "log" 7 9 "log/slog" ··· 10 12 "time" 11 13 12 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 + "github.com/bluesky-social/indigo/atproto/syntax" 13 16 lexutil "github.com/bluesky-social/indigo/lex/util" 14 17 securejoin "github.com/cyphar/filepath-securejoin" 15 18 "github.com/go-chi/chi/v5" ··· 25 28 "tangled.sh/tangled.sh/core/appview/pages" 26 29 posthogService "tangled.sh/tangled.sh/core/appview/posthog" 27 30 "tangled.sh/tangled.sh/core/appview/reporesolver" 31 + xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient" 28 32 "tangled.sh/tangled.sh/core/eventconsumer" 29 33 "tangled.sh/tangled.sh/core/idresolver" 30 34 "tangled.sh/tangled.sh/core/jetstream" 31 35 tlog "tangled.sh/tangled.sh/core/log" 32 36 "tangled.sh/tangled.sh/core/rbac" 33 37 "tangled.sh/tangled.sh/core/tid" 34 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 38 + // xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 35 39 ) 36 40 37 41 type State struct { ··· 48 52 repoResolver *reporesolver.RepoResolver 49 53 knotstream *eventconsumer.Consumer 50 54 spindlestream *eventconsumer.Consumer 55 + logger *slog.Logger 51 56 } 52 57 53 58 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 61 66 return nil, fmt.Errorf("failed to create enforcer: %w", err) 62 67 } 63 68 64 - pgs := pages.NewPages(config) 65 - 66 69 res, err := idresolver.RedisResolver(config.Redis.ToURL()) 67 70 if err != nil { 68 71 log.Printf("failed to create redis resolver: %v", err) 69 72 res = idresolver.DefaultResolver() 70 73 } 74 + 75 + pgs := pages.NewPages(config, res) 71 76 72 77 cache := cache.New(config.Redis.Addr) 73 78 sess := session.New(cache) ··· 152 157 repoResolver, 153 158 knotstream, 154 159 spindlestream, 160 + slog.Default(), 155 161 } 156 162 157 163 return state, nil 158 164 } 159 165 166 + func (s *State) Favicon(w http.ResponseWriter, r *http.Request) { 167 + w.Header().Set("Content-Type", "image/svg+xml") 168 + w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 169 + w.Header().Set("ETag", `"favicon-svg-v1"`) 170 + 171 + if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { 172 + w.WriteHeader(http.StatusNotModified) 173 + return 174 + } 175 + 176 + s.pages.Favicon(w) 177 + } 178 + 160 179 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) { 161 180 user := s.oauth.GetUser(r) 162 181 s.pages.TermsOfService(w, pages.TermsOfServiceParams{ ··· 180 199 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 181 200 } 182 201 183 - var didsToResolve []string 184 - for _, ev := range timeline { 185 - if ev.Repo != nil { 186 - didsToResolve = append(didsToResolve, ev.Repo.Did) 187 - if ev.Source != nil { 188 - didsToResolve = append(didsToResolve, ev.Source.Did) 189 - } 190 - } 191 - if ev.Follow != nil { 192 - didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid) 193 - } 194 - if ev.Star != nil { 195 - didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 196 - } 197 - } 198 - 199 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve) 200 - didHandleMap := make(map[string]string) 201 - for _, identity := range resolvedIds { 202 - if !identity.Handle.IsInvalidHandle() { 203 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 204 - } else { 205 - didHandleMap[identity.DID.String()] = identity.DID.String() 206 - } 202 + repos, err := db.GetTopStarredReposLastWeek(s.db) 203 + if err != nil { 204 + log.Println(err) 205 + s.pages.Notice(w, "topstarredrepos", "Unable to load.") 206 + return 207 207 } 208 208 209 209 s.pages.Timeline(w, pages.TimelineParams{ 210 210 LoggedInUser: user, 211 211 Timeline: timeline, 212 - DidHandleMap: didHandleMap, 212 + Repos: repos, 213 213 }) 214 - 215 - return 216 214 } 217 215 218 216 func (s *State) Keys(w http.ResponseWriter, r *http.Request) { ··· 279 277 return nil 280 278 } 281 279 280 + func stripGitExt(name string) string { 281 + return strings.TrimSuffix(name, ".git") 282 + } 283 + 282 284 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 283 285 switch r.Method { 284 286 case http.MethodGet: ··· 295 297 }) 296 298 297 299 case http.MethodPost: 300 + l := s.logger.With("handler", "NewRepo") 301 + 298 302 user := s.oauth.GetUser(r) 303 + l = l.With("did", user.Did) 304 + l = l.With("handle", user.Handle) 299 305 306 + // form validation 300 307 domain := r.FormValue("domain") 301 308 if domain == "" { 302 309 s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 303 310 return 304 311 } 312 + l = l.With("knot", domain) 305 313 306 314 repoName := r.FormValue("name") 307 315 if repoName == "" { ··· 313 321 s.pages.Notice(w, "repo", err.Error()) 314 322 return 315 323 } 324 + repoName = stripGitExt(repoName) 325 + l = l.With("repoName", repoName) 316 326 317 327 defaultBranch := r.FormValue("branch") 318 328 if defaultBranch == "" { 319 329 defaultBranch = "main" 320 330 } 331 + l = l.With("defaultBranch", defaultBranch) 321 332 322 333 description := r.FormValue("description") 323 334 335 + // ACL validation 324 336 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 325 337 if err != nil || !ok { 338 + l.Info("unauthorized") 326 339 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 327 340 return 328 341 } 329 342 343 + // Check for existing repos 330 344 existingRepo, err := db.GetRepo(s.db, user.Did, repoName) 331 345 if err == nil && existingRepo != nil { 346 + l.Info("repo exists") 332 347 s.pages.Notice(w, "repo", fmt.Sprintf("You already have a repository by this name on %s", existingRepo.Knot)) 333 348 return 334 349 } 335 350 336 - client, err := s.oauth.ServiceClient( 337 - r, 338 - oauth.WithService(domain), 339 - oauth.WithLxm(tangled.RepoCreateNSID), 340 - oauth.WithDev(s.config.Core.Dev), 341 - ) 342 - 343 - if err != nil { 344 - s.pages.Notice(w, "repo", "Failed to connect to knot server.") 345 - return 346 - } 347 - 351 + // create atproto record for this repo 348 352 rkey := tid.TID() 349 353 repo := &db.Repo{ 350 354 Did: user.Did, ··· 356 360 357 361 xrpcClient, err := s.oauth.AuthorizedClient(r) 358 362 if err != nil { 363 + l.Info("PDS write failed", "err", err) 359 364 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 360 365 return 361 366 } ··· 374 379 }}, 375 380 }) 376 381 if err != nil { 377 - log.Printf("failed to create record: %s", err) 382 + l.Info("PDS write failed", "err", err) 378 383 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 379 384 return 380 385 } 381 - log.Println("created repo record: ", atresp.Uri) 386 + 387 + aturi := atresp.Uri 388 + l = l.With("aturi", aturi) 389 + l.Info("wrote to PDS") 382 390 383 391 tx, err := s.db.BeginTx(r.Context(), nil) 384 392 if err != nil { 385 - log.Println(err) 393 + l.Info("txn failed", "err", err) 386 394 s.pages.Notice(w, "repo", "Failed to save repository information.") 387 395 return 388 396 } 389 - defer func() { 390 - tx.Rollback() 391 - err = s.enforcer.E.LoadPolicy() 392 - if err != nil { 393 - log.Println("failed to rollback policies") 397 + 398 + // The rollback function reverts a few things on failure: 399 + // - the pending txn 400 + // - the ACLs 401 + // - the atproto record created 402 + rollback := func() { 403 + err1 := tx.Rollback() 404 + err2 := s.enforcer.E.LoadPolicy() 405 + err3 := rollbackRecord(context.Background(), aturi, xrpcClient) 406 + 407 + // ignore txn complete errors, this is okay 408 + if errors.Is(err1, sql.ErrTxDone) { 409 + err1 = nil 394 410 } 395 - }() 396 411 397 - err = tangled.RepoCreate( 412 + if errs := errors.Join(err1, err2, err3); errs != nil { 413 + l.Error("failed to rollback changes", "errs", errs) 414 + return 415 + } 416 + } 417 + defer rollback() 418 + 419 + client, err := s.oauth.ServiceClient( 420 + r, 421 + oauth.WithService(domain), 422 + oauth.WithLxm(tangled.RepoCreateNSID), 423 + oauth.WithDev(s.config.Core.Dev), 424 + ) 425 + if err != nil { 426 + l.Error("service auth failed", "err", err) 427 + s.pages.Notice(w, "repo", "Failed to reach PDS.") 428 + return 429 + } 430 + 431 + xe := tangled.RepoCreate( 398 432 r.Context(), 399 433 client, 400 434 &tangled.RepoCreate_Input{ 401 - Default_branch: &defaultBranch, 402 - Did: user.Did, 403 - Name: repoName, 435 + Rkey: rkey, 404 436 }, 405 437 ) 406 - 407 - if err != nil { 408 - xe, err := xrpcerr.Unmarshal(err.Error()) 409 - if err != nil { 410 - log.Println(err) 411 - s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 412 - return 413 - } 414 - 415 - log.Println(xe.Error()) 416 - s.pages.Notice(w, "repo", fmt.Sprintf("Failed to create repository on knot server: %s.", xe.Message)) 438 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 439 + l.Error("xrpc error", "xe", xe) 440 + s.pages.Notice(w, "repo", err.Error()) 417 441 return 418 442 } 419 443 420 - repo.AtUri = atresp.Uri 421 444 err = db.AddRepo(tx, repo) 422 445 if err != nil { 423 - log.Println(err) 446 + l.Error("db write failed", "err", err) 424 447 s.pages.Notice(w, "repo", "Failed to save repository information.") 425 448 return 426 449 } ··· 429 452 p, _ := securejoin.SecureJoin(user.Did, repoName) 430 453 err = s.enforcer.AddRepo(user.Did, domain, p) 431 454 if err != nil { 432 - log.Println(err) 455 + l.Error("acl setup failed", "err", err) 433 456 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 434 457 return 435 458 } 436 459 437 460 err = tx.Commit() 438 461 if err != nil { 439 - log.Println("failed to commit changes", err) 462 + l.Error("txn commit failed", "err", err) 440 463 http.Error(w, err.Error(), http.StatusInternalServerError) 441 464 return 442 465 } 443 466 444 467 err = s.enforcer.E.SavePolicy() 445 468 if err != nil { 446 - log.Println("failed to update ACLs", err) 469 + l.Error("acl save failed", "err", err) 447 470 http.Error(w, err.Error(), http.StatusInternalServerError) 448 471 return 449 472 } 450 473 451 - s.notifier.NewRepo(r.Context(), repo) 474 + // reset the ATURI because the transaction completed successfully 475 + aturi = "" 452 476 477 + s.notifier.NewRepo(r.Context(), repo) 453 478 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 454 - return 455 479 } 456 480 } 481 + 482 + // this is used to rollback changes made to the PDS 483 + // 484 + // it is a no-op if the provided ATURI is empty 485 + func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 486 + if aturi == "" { 487 + return nil 488 + } 489 + 490 + parsed := syntax.ATURI(aturi) 491 + 492 + collection := parsed.Collection().String() 493 + repo := parsed.Authority().String() 494 + rkey := parsed.RecordKey().String() 495 + 496 + _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 497 + Collection: collection, 498 + Repo: repo, 499 + Rkey: rkey, 500 + }) 501 + return err 502 + }
+30 -19
appview/strings/strings.go
··· 7 7 "path" 8 8 "slices" 9 9 "strconv" 10 - "strings" 11 10 "time" 12 11 13 12 "tangled.sh/tangled.sh/core/api/tangled" ··· 44 43 r := chi.NewRouter() 45 44 46 45 r. 46 + Get("/", s.timeline) 47 + 48 + r. 47 49 With(mw.ResolveIdent()). 48 50 Route("/{user}", func(r chi.Router) { 49 51 r.Get("/", s.dashboard) ··· 70 72 return r 71 73 } 72 74 75 + func (s *Strings) timeline(w http.ResponseWriter, r *http.Request) { 76 + l := s.Logger.With("handler", "timeline") 77 + 78 + strings, err := db.GetStrings(s.Db, 50) 79 + if err != nil { 80 + l.Error("failed to fetch string", "err", err) 81 + w.WriteHeader(http.StatusInternalServerError) 82 + return 83 + } 84 + 85 + s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 86 + LoggedInUser: s.OAuth.GetUser(r), 87 + Strings: strings, 88 + }) 89 + } 90 + 73 91 func (s *Strings) contents(w http.ResponseWriter, r *http.Request) { 74 92 l := s.Logger.With("handler", "contents") 75 93 ··· 91 109 92 110 strings, err := db.GetStrings( 93 111 s.Db, 112 + 0, 94 113 db.FilterEq("did", id.DID), 95 114 db.FilterEq("rkey", rkey), 96 115 ) ··· 154 173 155 174 all, err := db.GetStrings( 156 175 s.Db, 176 + 0, 157 177 db.FilterEq("did", id.DID), 158 178 ) 159 179 if err != nil { ··· 182 202 followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String()) 183 203 } 184 204 185 - followers, following, err := db.GetFollowerFollowing(s.Db, id.DID.String()) 205 + followStats, err := db.GetFollowerFollowingCount(s.Db, id.DID.String()) 186 206 if err != nil { 187 207 l.Error("failed to get follow stats", "err", err) 188 208 } ··· 190 210 s.Pages.StringsDashboard(w, pages.StringsDashboardParams{ 191 211 LoggedInUser: s.OAuth.GetUser(r), 192 212 Card: pages.ProfileCard{ 193 - UserDid: id.DID.String(), 194 - UserHandle: id.Handle.String(), 195 - Profile: profile, 196 - FollowStatus: followStatus, 197 - Followers: followers, 198 - Following: following, 213 + UserDid: id.DID.String(), 214 + UserHandle: id.Handle.String(), 215 + Profile: profile, 216 + FollowStatus: followStatus, 217 + FollowersCount: followStats.Followers, 218 + FollowingCount: followStats.Following, 199 219 }, 200 220 Strings: all, 201 221 }) ··· 225 245 // get the string currently being edited 226 246 all, err := db.GetStrings( 227 247 s.Db, 248 + 0, 228 249 db.FilterEq("did", id.DID), 229 250 db.FilterEq("rkey", rkey), 230 251 ) ··· 266 287 fail("Empty filename.", nil) 267 288 return 268 289 } 269 - if !strings.Contains(filename, ".") { 270 - // TODO: make this a htmx form validation 271 - fail("No extension provided for filename.", nil) 272 - return 273 - } 274 290 275 291 content := r.FormValue("content") 276 292 if content == "" { ··· 353 369 fail("Empty filename.", nil) 354 370 return 355 371 } 356 - if !strings.Contains(filename, ".") { 357 - // TODO: make this a htmx form validation 358 - fail("No extension provided for filename.", nil) 359 - return 360 - } 361 372 362 373 content := r.FormValue("content") 363 374 if content == "" { ··· 434 445 } 435 446 436 447 if user.Did != id.DID.String() { 437 - fail("You cannot delete this gist", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 448 + fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 438 449 return 439 450 } 440 451
+25
appview/xrpcclient/xrpc.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 7 + "fmt" 6 8 "io" 9 + "net/http" 7 10 8 11 "github.com/bluesky-social/indigo/api/atproto" 9 12 "github.com/bluesky-social/indigo/xrpc" 13 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 10 14 oauth "tangled.sh/icyphox.sh/atproto-oauth" 11 15 ) 12 16 ··· 102 106 103 107 return &out, nil 104 108 } 109 + 110 + // produces a more manageable error 111 + func HandleXrpcErr(err error) error { 112 + if err == nil { 113 + return nil 114 + } 115 + 116 + var xrpcerr *indigoxrpc.Error 117 + if ok := errors.As(err, &xrpcerr); !ok { 118 + return fmt.Errorf("Recieved invalid XRPC error response.") 119 + } 120 + 121 + switch xrpcerr.StatusCode { 122 + case http.StatusNotFound: 123 + return fmt.Errorf("XRPC is unsupported on this knot, consider upgrading your knot.") 124 + case http.StatusUnauthorized: 125 + return fmt.Errorf("Unauthorized XRPC request.") 126 + default: 127 + return fmt.Errorf("Failed to perform operation. Try again later.") 128 + } 129 + }
+11 -13
cmd/gen.go
··· 17 17 tangled.ActorProfile{}, 18 18 tangled.FeedReaction{}, 19 19 tangled.FeedStar{}, 20 - tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem{}, 20 + tangled.GitRefUpdate{}, 21 + tangled.GitRefUpdate_Meta{}, 21 22 tangled.GitRefUpdate_Meta_CommitCount{}, 23 + tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem{}, 22 24 tangled.GitRefUpdate_Meta_LangBreakdown{}, 23 - tangled.GitRefUpdate_Meta{}, 24 25 tangled.GitRefUpdate_Pair{}, 25 - tangled.GitRefUpdate{}, 26 26 tangled.GraphFollow{}, 27 - tangled.KnotMember{}, 28 27 tangled.Knot{}, 29 - tangled.PipelineStatus{}, 28 + tangled.KnotMember{}, 29 + tangled.Pipeline{}, 30 30 tangled.Pipeline_CloneOpts{}, 31 - tangled.Pipeline_Dependency{}, 32 31 tangled.Pipeline_ManualTriggerData{}, 33 32 tangled.Pipeline_Pair{}, 34 33 tangled.Pipeline_PullRequestTriggerData{}, 35 34 tangled.Pipeline_PushTriggerData{}, 36 - tangled.Pipeline_Step{}, 35 + tangled.PipelineStatus{}, 37 36 tangled.Pipeline_TriggerMetadata{}, 38 37 tangled.Pipeline_TriggerRepo{}, 39 38 tangled.Pipeline_Workflow{}, 40 - tangled.Pipeline{}, 41 39 tangled.PublicKey{}, 40 + tangled.Repo{}, 42 41 tangled.RepoArtifact{}, 43 42 tangled.RepoCollaborator{}, 43 + tangled.RepoIssue{}, 44 44 tangled.RepoIssueComment{}, 45 45 tangled.RepoIssueState{}, 46 - tangled.RepoIssue{}, 46 + tangled.RepoPull{}, 47 47 tangled.RepoPullComment{}, 48 - tangled.RepoPullStatus{}, 49 48 tangled.RepoPull_Source{}, 50 - tangled.RepoPull{}, 51 - tangled.Repo{}, 49 + tangled.RepoPullStatus{}, 50 + tangled.Spindle{}, 52 51 tangled.SpindleMember{}, 53 - tangled.Spindle{}, 54 52 tangled.String{}, 55 53 ); err != nil { 56 54 panic(err)
+4
cmd/genjwks/main.go
··· 30 30 panic(err) 31 31 } 32 32 33 + if err := key.Set("use", "sig"); err != nil { 34 + panic(err) 35 + } 36 + 33 37 b, err := json.Marshal(key) 34 38 if err != nil { 35 39 panic(err)
+1 -1
cmd/punchcardPopulate/main.go
··· 11 11 ) 12 12 13 13 func main() { 14 - db, err := sql.Open("sqlite3", "./appview.db") 14 + db, err := sql.Open("sqlite3", "./appview.db?_foreign_keys=1") 15 15 if err != nil { 16 16 log.Fatal("Failed to open database:", err) 17 17 }
+6
docs/contributing.md
··· 55 55 - Avoid noisy commit messages like "wip" or "final fix"โ€”rewrite history 56 56 before submitting if necessary. 57 57 58 + ## code formatting 59 + 60 + We use a variety of tools to format our code, and multiplex them with 61 + [`treefmt`](https://treefmt.com): all you need to do to format your changes 62 + is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 63 + 58 64 ## proposals for bigger changes 59 65 60 66 Small fixes like typos, minor bugs, or trivial refactors can be
+23 -18
docs/hacking.md
··· 55 55 quite cumbersome. So the nix flake provides a 56 56 `nixosConfiguration` to do so. 57 57 58 - To begin, head to `http://localhost:3000/knots` in the browser 59 - and create a knot with hostname `localhost:6000`. This will 60 - generate a knot secret. Set `$TANGLED_VM_KNOT_SECRET` to it, 61 - ideally in a `.envrc` with [direnv](https://direnv.net) so you 62 - don't lose it. 58 + To begin, grab your DID from http://localhost:3000/settings. 59 + Then, set `TANGLED_VM_KNOT_OWNER` and 60 + `TANGLED_VM_SPINDLE_OWNER` to your DID. 61 + 62 + If you don't want to [set up a spindle](#running-a-spindle), 63 + you can use any placeholder value. 63 64 64 - You can now start a lightweight NixOS VM using 65 - `nixos-shell` like so: 65 + You can now start a lightweight NixOS VM like so: 66 66 67 67 ```bash 68 - nix run .#vm 69 - # or nixos-shell --flake .#vm 68 + nix run --impure .#vm 70 69 71 - # hit Ctrl-a + c + q to exit the VM 70 + # type `poweroff` at the shell to exit the VM 72 71 ``` 73 72 74 73 This starts a knot on port 6000, a spindle on port 6555 75 - with `ssh` exposed on port 2222. You can push repositories 76 - to this VM with this ssh config block on your main machine: 74 + with `ssh` exposed on port 2222. 75 + 76 + Once the services are running, head to 77 + http://localhost:3000/knots and hit verify (and similarly, 78 + http://localhost:3000/spindles to verify your spindle). It 79 + should verify the ownership of the services instantly if 80 + everything went smoothly. 81 + 82 + You can push repositories to this VM with this ssh config 83 + block on your main machine: 77 84 78 85 ```bash 79 86 Host nixos-shell ··· 92 99 93 100 ## running a spindle 94 101 95 - Be sure to set `$TANGLED_VM_SPINDLE_OWNER` to your own DID. 96 - The above VM should already be running a spindle on `localhost:6555`. 97 - You can head to the spindle dashboard on `http://localhost:3000/spindles`, 98 - and register a spindle with hostname `localhost:6555`. It should instantly 99 - be verified. You can then configure each repository to use this spindle 100 - and run CI jobs. 102 + The above VM should already be running a spindle on 103 + `localhost:6555`. Head to http://localhost:3000/spindles and 104 + hit verify. You can then configure each repository to use 105 + this spindle and run CI jobs. 101 106 102 107 Of interest when debugging spindles: 103 108
+7 -5
docs/knot-hosting.md
··· 73 73 ``` 74 74 75 75 Create `/home/git/.knot.env` with the following, updating the values as 76 - necessary. The `KNOT_SERVER_SECRET` can be obtained from the 77 - [/knots](https://tangled.sh/knots) page on Tangled. 76 + necessary. The `KNOT_SERVER_OWNER` should be set to your 77 + DID, you can find your DID in the [Settings](https://tangled.sh/settings) page. 78 78 79 79 ``` 80 80 KNOT_REPO_SCAN_PATH=/home/git 81 81 KNOT_SERVER_HOSTNAME=knot.example.com 82 82 APPVIEW_ENDPOINT=https://tangled.sh 83 - KNOT_SERVER_SECRET=secret 83 + KNOT_SERVER_OWNER=did:plc:foobar 84 84 KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 85 85 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 86 86 ``` ··· 128 128 Remember to use Let's Encrypt or similar to procure a certificate for your 129 129 knot domain. 130 130 131 - You should now have a running knot server! You can finalize your registration by hitting the 132 - `initialize` button on the [/knots](https://tangled.sh/knots) page. 131 + You should now have a running knot server! You can finalize 132 + your registration by hitting the `verify` button on the 133 + [/knots](https://tangled.sh/knots) page. This simply creates 134 + a record on your PDS to announce the existence of the knot. 133 135 134 136 ### custom paths 135 137
+35
docs/migrations/knot-1.7.0.md
··· 1 + # Upgrading from v1.7.0 2 + 3 + After v1.7.0, knot secrets have been deprecated. You no 4 + longer need a secret from the appview to run a knot. All 5 + authorized commands to knots are managed via [Inter-Service 6 + Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 7 + Knots will be read-only until upgraded. 8 + 9 + Upgrading is quite easy, in essence: 10 + 11 + - `KNOT_SERVER_SECRET` is no more, you can remove this 12 + environment variable entirely 13 + - `KNOT_SERVER_OWNER` is now required on boot, set this to 14 + your DID. You can find your DID in the 15 + [settings](https://tangled.sh/settings) page. 16 + - Restart your knot once you have replaced the environment 17 + variable 18 + - Head to the [knot dashboard](https://tangled.sh/knots) and 19 + hit the "retry" button to verify your knot. This simply 20 + writes a `sh.tangled.knot` record to your PDS. 21 + 22 + ## Nix 23 + 24 + If you use the nix module, simply bump the flake to the 25 + latest revision, and change your config block like so: 26 + 27 + ```diff 28 + services.tangled-knot = { 29 + enable = true; 30 + server = { 31 + - secretFile = /path/to/secret; 32 + + owner = "did:plc:foo"; 33 + }; 34 + }; 35 + ```
+26 -3
docs/spindle/pipeline.md
··· 4 4 repo. Generally: 5 5 6 6 * Pipelines are defined in YAML. 7 - * Dependencies can be specified from 8 - [Nixpkgs](https://search.nixos.org) or custom registries. 9 - * Environment variables can be set globally or per-step. 7 + * Workflows can run using different *engines*. 8 + 9 + The most barebones workflow looks like this: 10 + 11 + ```yaml 12 + when: 13 + - event: ["push"] 14 + branch: ["main"] 15 + 16 + engine: "nixery" 17 + 18 + # optional 19 + clone: 20 + skip: false 21 + depth: 50 22 + submodules: true 23 + ``` 24 + 25 + The `when` and `engine` fields are required, while every other aspect 26 + of how the definition is parsed is up to the engine. Currently, a spindle 27 + provides at least one of these built-in engines: 28 + 29 + ## `nixery` 30 + 31 + The Nixery engine uses an instance of [Nixery](https://nixery.dev) to run 32 + steps that use dependencies from [Nixpkgs](https://github.com/NixOS/nixpkgs). 10 33 11 34 Here's an example that uses all fields: 12 35
+1 -1
eventconsumer/cursor/sqlite.go
··· 21 21 } 22 22 23 23 func NewSQLiteStore(dbPath string, opts ...SqliteStoreOpt) (*SqliteStore, error) { 24 - db, err := sql.Open("sqlite3", dbPath) 24 + db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=1") 25 25 if err != nil { 26 26 return nil, fmt.Errorf("failed to open sqlite database: %w", err) 27 27 }
+55 -29
flake.nix
··· 106 106 pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot; 107 107 pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped; 108 108 pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle; 109 + 110 + treefmt-wrapper = pkgs.treefmt.withConfig { 111 + settings.formatter = { 112 + alejandra = { 113 + command = pkgs.lib.getExe pkgs.alejandra; 114 + includes = ["*.nix"]; 115 + }; 116 + 117 + gofmt = { 118 + command = pkgs.lib.getExe' pkgs.go "gofmt"; 119 + options = ["-w"]; 120 + includes = ["*.go"]; 121 + }; 122 + 123 + # prettier = let 124 + # wrapper = pkgs.runCommandLocal "prettier-wrapper" {nativeBuildInputs = [pkgs.makeWrapper];} '' 125 + # makeWrapper ${pkgs.prettier}/bin/prettier "$out" --add-flags "--plugin=${pkgs.prettier-plugin-go-template}/lib/node_modules/prettier-plugin-go-template/lib/index.js" 126 + # ''; 127 + # in { 128 + # command = wrapper; 129 + # options = ["-w"]; 130 + # includes = ["*.html"]; 131 + # # causes Go template plugin errors: https://github.com/NiklasPor/prettier-plugin-go-template/issues/120 132 + # excludes = ["appview/pages/templates/layouts/repobase.html" "appview/pages/templates/repo/tags.html"]; 133 + # }; 134 + }; 135 + }; 109 136 }); 110 137 defaultPackage = forAllSystems (system: self.packages.${system}.appview); 111 - formatter = forAllSystems (system: nixpkgsFor.${system}.alejandra); 112 138 devShells = forAllSystems (system: let 113 139 pkgs = nixpkgsFor.${system}; 114 140 packages' = self.packages.${system}; ··· 129 155 pkgs.redis 130 156 pkgs.coreutils # for those of us who are on systems that use busybox (alpine) 131 157 packages'.lexgen 158 + packages'.treefmt-wrapper 132 159 ]; 133 160 shellHook = '' 134 161 mkdir -p appview/pages/static ··· 158 185 ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 159 186 ''; 160 187 in { 188 + fmt = { 189 + type = "app"; 190 + program = pkgs.lib.getExe packages'.treefmt-wrapper; 191 + }; 161 192 watch-appview = { 162 193 type = "app"; 163 194 program = toString (pkgs.writeShellScript "watch-appview" '' ··· 175 206 program = ''${tailwind-watcher}/bin/run''; 176 207 }; 177 208 vm = let 178 - system = 209 + guestSystem = 179 210 if pkgs.stdenv.hostPlatform.isAarch64 180 - then "aarch64" 181 - else "x86_64"; 182 - 183 - nixos-shell = pkgs.nixos-shell.overrideAttrs (old: { 184 - patches = 185 - (old.patches or []) 186 - ++ [ 187 - # https://github.com/Mic92/nixos-shell/pull/94 188 - (pkgs.fetchpatch { 189 - name = "fix-foreign-vm.patch"; 190 - url = "https://github.com/Mic92/nixos-shell/commit/113e4cc55ae236b5b0b1fbd8b321e9b67c77580e.patch"; 191 - hash = "sha256-eauetBK0wXAOcd9PYbExokNCiwz2QyFnZ4FnwGi9VCo="; 192 - }) 193 - ]; 194 - }); 211 + then "aarch64-linux" 212 + else "x86_64-linux"; 195 213 in { 196 214 type = "app"; 197 - program = toString (pkgs.writeShellScript "vm" '' 198 - ${nixos-shell}/bin/nixos-shell --flake .#vm-${system} --guest-system ${system}-linux 199 - ''); 215 + program = 216 + (pkgs.writeShellApplication { 217 + name = "launch-vm"; 218 + text = '' 219 + rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 220 + cd "$rootDir" 221 + 222 + mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs} 223 + 224 + export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data" 225 + exec ${pkgs.lib.getExe 226 + (import ./nix/vm.nix { 227 + inherit nixpkgs self; 228 + system = guestSystem; 229 + hostSystem = system; 230 + }).config.system.build.vm} 231 + ''; 232 + }) 233 + + /bin/launch-vm; 200 234 }; 201 235 gomod2nix = { 202 236 type = "app"; ··· 218 252 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 219 253 cd "$rootDir" 220 254 221 - rm api/tangled/* 255 + rm -f api/tangled/* 222 256 lexgen --build-file lexicon-build-config.json lexicons 223 257 sed -i.bak 's/\tutil/\/\/\tutil/' api/tangled/* 224 258 ${pkgs.gotools}/bin/goimports -w api/tangled/* ··· 257 291 imports = [./nix/modules/spindle.nix]; 258 292 259 293 services.tangled-spindle.package = lib.mkDefault self.packages.${pkgs.system}.spindle; 260 - }; 261 - nixosConfigurations.vm-x86_64 = import ./nix/vm.nix { 262 - inherit self nixpkgs; 263 - system = "x86_64-linux"; 264 - }; 265 - nixosConfigurations.vm-aarch64 = import ./nix/vm.nix { 266 - inherit self nixpkgs; 267 - system = "aarch64-linux"; 268 294 }; 269 295 }; 270 296 }
+3 -2
go.mod
··· 22 22 github.com/go-enry/go-enry/v2 v2.9.2 23 23 github.com/go-git/go-git/v5 v5.14.0 24 24 github.com/google/uuid v1.6.0 25 + github.com/gorilla/feeds v1.2.0 25 26 github.com/gorilla/sessions v1.4.0 26 27 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 27 28 github.com/hiddeco/sshsig v0.2.0 ··· 38 39 github.com/stretchr/testify v1.10.0 39 40 github.com/urfave/cli/v3 v3.3.3 40 41 github.com/whyrusleeping/cbor-gen v0.3.1 41 - github.com/yuin/goldmark v1.4.13 42 + github.com/yuin/goldmark v1.4.15 43 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 42 44 golang.org/x/crypto v0.40.0 43 45 golang.org/x/net v0.42.0 44 46 golang.org/x/sync v0.16.0 ··· 88 90 github.com/golang/mock v1.6.0 // indirect 89 91 github.com/google/go-querystring v1.1.0 // indirect 90 92 github.com/gorilla/css v1.0.1 // indirect 91 - github.com/gorilla/feeds v1.2.0 // indirect 92 93 github.com/gorilla/securecookie v1.1.2 // indirect 93 94 github.com/hashicorp/errwrap v1.1.0 // indirect 94 95 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+5 -1
go.sum
··· 79 79 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 80 80 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 81 81 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 82 + github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 82 83 github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 83 84 github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 84 85 github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= ··· 429 430 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 430 431 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 431 432 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 432 - github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 433 433 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 434 + github.com/yuin/goldmark v1.4.15 h1:CFa84T0goNn/UIXYS+dmjjVxMyTAvpOmzld40N/nfK0= 435 + github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 436 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 437 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 434 438 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 435 439 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 436 440 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
+72 -7
input.css
··· 13 13 @font-face { 14 14 font-family: "InterVariable"; 15 15 src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2"); 16 - font-weight: 400; 16 + font-weight: normal; 17 17 font-style: italic; 18 18 font-display: swap; 19 19 } 20 20 21 21 @font-face { 22 22 font-family: "InterVariable"; 23 - src: url("/static/fonts/InterVariable.woff2") format("woff2"); 24 - font-weight: 600; 23 + src: url("/static/fonts/InterDisplay-Bold.woff2") format("woff2"); 24 + font-weight: bold; 25 25 font-style: normal; 26 26 font-display: swap; 27 27 } 28 28 29 29 @font-face { 30 + font-family: "InterVariable"; 31 + src: url("/static/fonts/InterDisplay-BoldItalic.woff2") format("woff2"); 32 + font-weight: bold; 33 + font-style: italic; 34 + font-display: swap; 35 + } 36 + 37 + @font-face { 30 38 font-family: "IBMPlexMono"; 31 39 src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"); 32 40 font-weight: normal; 41 + font-style: normal; 42 + font-display: swap; 43 + } 44 + 45 + @font-face { 46 + font-family: "IBMPlexMono"; 47 + src: url("/static/fonts/IBMPlexMono-Italic.woff2") format("woff2"); 48 + font-weight: normal; 49 + font-style: italic; 50 + font-display: swap; 51 + } 52 + 53 + @font-face { 54 + font-family: "IBMPlexMono"; 55 + src: url("/static/fonts/IBMPlexMono-Bold.woff2") format("woff2"); 56 + font-weight: bold; 57 + font-style: normal; 58 + font-display: swap; 59 + } 60 + 61 + @font-face { 62 + font-family: "IBMPlexMono"; 63 + src: url("/static/fonts/IBMPlexMono-BoldItalic.woff2") format("woff2"); 64 + font-weight: bold; 33 65 font-style: italic; 34 66 font-display: swap; 35 67 } ··· 46 78 @supports (font-variation-settings: normal) { 47 79 html { 48 80 font-feature-settings: 49 - "ss01" 1, 50 81 "kern" 1, 51 82 "liga" 1, 52 83 "cv05" 1, ··· 72 103 } 73 104 74 105 code { 75 - @apply px-1 font-mono rounded bg-gray-100 dark:bg-gray-700; 106 + @apply font-mono rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white; 76 107 } 77 108 } 78 109 ··· 102 133 disabled:before:bg-green-400 dark:disabled:before:bg-green-600; 103 134 } 104 135 136 + .prose hr { 137 + @apply my-2; 138 + } 139 + 140 + .prose li:has(input) { 141 + @apply list-none; 142 + } 143 + 144 + .prose ul:has(input) { 145 + @apply pl-2; 146 + } 147 + 148 + .prose .heading .anchor { 149 + @apply no-underline mx-2 opacity-0; 150 + } 151 + 152 + .prose .heading:hover .anchor { 153 + @apply opacity-70; 154 + } 155 + 156 + .prose .heading .anchor:hover { 157 + @apply opacity-70; 158 + } 159 + 160 + .prose a.footnote-backref { 161 + @apply no-underline; 162 + } 163 + 164 + .prose li { 165 + @apply my-0 py-0; 166 + } 167 + 168 + .prose ul, .prose ol { 169 + @apply my-1 py-0; 170 + } 171 + 105 172 .prose img { 106 173 display: inline; 107 174 margin: 0; ··· 134 201 /* PreWrapper */ 135 202 .chroma { 136 203 color: #4c4f69; 137 - background-color: #eff1f5; 138 204 } 139 205 /* Error */ 140 206 .chroma .err { ··· 471 537 /* PreWrapper */ 472 538 .chroma { 473 539 color: #cad3f5; 474 - background-color: #24273a; 475 540 } 476 541 /* Error */ 477 542 .chroma .err {
+6 -4
jetstream/jetstream.go
··· 68 68 type processor func(context.Context, *models.Event) error 69 69 70 70 func (j *JetstreamClient) withDidFilter(processFunc processor) processor { 71 - // empty filter => all dids allowed 72 - if len(j.wantedDids) == 0 { 73 - return processFunc 74 - } 75 71 // since this closure references j.WantedDids; it should auto-update 76 72 // existing instances of the closure when j.WantedDids is mutated 77 73 return func(ctx context.Context, evt *models.Event) error { 74 + 75 + // empty filter => all dids allowed 76 + if len(j.wantedDids) == 0 { 77 + return processFunc(ctx, evt) 78 + } 79 + 78 80 if _, ok := j.wantedDids[evt.Did]; ok { 79 81 return processFunc(ctx, evt) 80 82 } else {
-336
knotclient/signer.go
··· 1 - package knotclient 2 - 3 - import ( 4 - "bytes" 5 - "crypto/hmac" 6 - "crypto/sha256" 7 - "encoding/hex" 8 - "encoding/json" 9 - "fmt" 10 - "io" 11 - "log" 12 - "net/http" 13 - "net/url" 14 - "time" 15 - 16 - "tangled.sh/tangled.sh/core/types" 17 - ) 18 - 19 - type SignerTransport struct { 20 - Secret string 21 - } 22 - 23 - func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { 24 - timestamp := time.Now().Format(time.RFC3339) 25 - mac := hmac.New(sha256.New, []byte(s.Secret)) 26 - message := req.Method + req.URL.Path + timestamp 27 - mac.Write([]byte(message)) 28 - signature := hex.EncodeToString(mac.Sum(nil)) 29 - req.Header.Set("X-Signature", signature) 30 - req.Header.Set("X-Timestamp", timestamp) 31 - return http.DefaultTransport.RoundTrip(req) 32 - } 33 - 34 - type SignedClient struct { 35 - Secret string 36 - Url *url.URL 37 - client *http.Client 38 - } 39 - 40 - func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) { 41 - client := &http.Client{ 42 - Timeout: 5 * time.Second, 43 - Transport: SignerTransport{ 44 - Secret: secret, 45 - }, 46 - } 47 - 48 - scheme := "https" 49 - if dev { 50 - scheme = "http" 51 - } 52 - url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 53 - if err != nil { 54 - return nil, err 55 - } 56 - 57 - signedClient := &SignedClient{ 58 - Secret: secret, 59 - client: client, 60 - Url: url, 61 - } 62 - 63 - return signedClient, nil 64 - } 65 - 66 - func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 67 - return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 68 - } 69 - 70 - func (s *SignedClient) Init(did string) (*http.Response, error) { 71 - const ( 72 - Method = "POST" 73 - Endpoint = "/init" 74 - ) 75 - 76 - body, _ := json.Marshal(map[string]any{ 77 - "did": did, 78 - }) 79 - 80 - req, err := s.newRequest(Method, Endpoint, body) 81 - if err != nil { 82 - return nil, err 83 - } 84 - 85 - return s.client.Do(req) 86 - } 87 - 88 - func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) { 89 - const ( 90 - Method = "PUT" 91 - Endpoint = "/repo/new" 92 - ) 93 - 94 - body, _ := json.Marshal(map[string]any{ 95 - "did": did, 96 - "name": repoName, 97 - "default_branch": defaultBranch, 98 - }) 99 - 100 - req, err := s.newRequest(Method, Endpoint, body) 101 - if err != nil { 102 - return nil, err 103 - } 104 - 105 - return s.client.Do(req) 106 - } 107 - 108 - func (s *SignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) { 109 - const ( 110 - Method = "GET" 111 - ) 112 - endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref)) 113 - 114 - req, err := s.newRequest(Method, endpoint, nil) 115 - if err != nil { 116 - return nil, err 117 - } 118 - 119 - resp, err := s.client.Do(req) 120 - if err != nil { 121 - return nil, err 122 - } 123 - 124 - var result types.RepoLanguageResponse 125 - if resp.StatusCode != http.StatusOK { 126 - log.Println("failed to calculate languages", resp.Status) 127 - return &types.RepoLanguageResponse{}, nil 128 - } 129 - 130 - body, err := io.ReadAll(resp.Body) 131 - if err != nil { 132 - return nil, err 133 - } 134 - 135 - err = json.Unmarshal(body, &result) 136 - if err != nil { 137 - return nil, err 138 - } 139 - 140 - return &result, nil 141 - } 142 - 143 - func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) { 144 - const ( 145 - Method = "GET" 146 - ) 147 - endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 148 - 149 - body, _ := json.Marshal(map[string]any{ 150 - "did": ownerDid, 151 - "source": source, 152 - "name": name, 153 - "hiddenref": hiddenRef, 154 - }) 155 - 156 - req, err := s.newRequest(Method, endpoint, body) 157 - if err != nil { 158 - return nil, err 159 - } 160 - 161 - return s.client.Do(req) 162 - } 163 - 164 - func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { 165 - const ( 166 - Method = "POST" 167 - ) 168 - endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 169 - 170 - body, _ := json.Marshal(map[string]any{ 171 - "did": ownerDid, 172 - "source": source, 173 - "name": name, 174 - }) 175 - 176 - req, err := s.newRequest(Method, endpoint, body) 177 - if err != nil { 178 - return nil, err 179 - } 180 - 181 - return s.client.Do(req) 182 - } 183 - 184 - func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 185 - const ( 186 - Method = "POST" 187 - Endpoint = "/repo/fork" 188 - ) 189 - 190 - body, _ := json.Marshal(map[string]any{ 191 - "did": ownerDid, 192 - "source": source, 193 - "name": name, 194 - }) 195 - 196 - req, err := s.newRequest(Method, Endpoint, body) 197 - if err != nil { 198 - return nil, err 199 - } 200 - 201 - return s.client.Do(req) 202 - } 203 - 204 - func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 205 - const ( 206 - Method = "DELETE" 207 - Endpoint = "/repo" 208 - ) 209 - 210 - body, _ := json.Marshal(map[string]any{ 211 - "did": did, 212 - "name": repoName, 213 - }) 214 - 215 - req, err := s.newRequest(Method, Endpoint, body) 216 - if err != nil { 217 - return nil, err 218 - } 219 - 220 - return s.client.Do(req) 221 - } 222 - 223 - func (s *SignedClient) AddMember(did string) (*http.Response, error) { 224 - const ( 225 - Method = "PUT" 226 - Endpoint = "/member/add" 227 - ) 228 - 229 - body, _ := json.Marshal(map[string]any{ 230 - "did": did, 231 - }) 232 - 233 - req, err := s.newRequest(Method, Endpoint, body) 234 - if err != nil { 235 - return nil, err 236 - } 237 - 238 - return s.client.Do(req) 239 - } 240 - 241 - func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 242 - const ( 243 - Method = "PUT" 244 - ) 245 - endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 246 - 247 - body, _ := json.Marshal(map[string]any{ 248 - "branch": branch, 249 - }) 250 - 251 - req, err := s.newRequest(Method, endpoint, body) 252 - if err != nil { 253 - return nil, err 254 - } 255 - 256 - return s.client.Do(req) 257 - } 258 - 259 - func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 260 - const ( 261 - Method = "POST" 262 - ) 263 - endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 264 - 265 - body, _ := json.Marshal(map[string]any{ 266 - "did": memberDid, 267 - }) 268 - 269 - req, err := s.newRequest(Method, endpoint, body) 270 - if err != nil { 271 - return nil, err 272 - } 273 - 274 - return s.client.Do(req) 275 - } 276 - 277 - func (s *SignedClient) Merge( 278 - patch []byte, 279 - ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 280 - ) (*http.Response, error) { 281 - const ( 282 - Method = "POST" 283 - ) 284 - endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 285 - 286 - mr := types.MergeRequest{ 287 - Branch: branch, 288 - CommitMessage: commitMessage, 289 - CommitBody: commitBody, 290 - AuthorName: authorName, 291 - AuthorEmail: authorEmail, 292 - Patch: string(patch), 293 - } 294 - 295 - body, _ := json.Marshal(mr) 296 - 297 - req, err := s.newRequest(Method, endpoint, body) 298 - if err != nil { 299 - return nil, err 300 - } 301 - 302 - return s.client.Do(req) 303 - } 304 - 305 - func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 306 - const ( 307 - Method = "POST" 308 - ) 309 - endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 310 - 311 - body, _ := json.Marshal(map[string]any{ 312 - "patch": string(patch), 313 - "branch": branch, 314 - }) 315 - 316 - req, err := s.newRequest(Method, endpoint, body) 317 - if err != nil { 318 - return nil, err 319 - } 320 - 321 - return s.client.Do(req) 322 - } 323 - 324 - func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 325 - const ( 326 - Method = "POST" 327 - ) 328 - endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch)) 329 - 330 - req, err := s.newRequest(Method, endpoint, nil) 331 - if err != nil { 332 - return nil, err 333 - } 334 - 335 - return s.client.Do(req) 336 - }
+35
knotclient/unsigned.go
··· 248 248 249 249 return &formatPatchResponse, nil 250 250 } 251 + 252 + func (s *UnsignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) { 253 + const ( 254 + Method = "GET" 255 + ) 256 + endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref)) 257 + 258 + req, err := s.newRequest(Method, endpoint, nil, nil) 259 + if err != nil { 260 + return nil, err 261 + } 262 + 263 + resp, err := s.client.Do(req) 264 + if err != nil { 265 + return nil, err 266 + } 267 + 268 + var result types.RepoLanguageResponse 269 + if resp.StatusCode != http.StatusOK { 270 + log.Println("failed to calculate languages", resp.Status) 271 + return &types.RepoLanguageResponse{}, nil 272 + } 273 + 274 + body, err := io.ReadAll(resp.Body) 275 + if err != nil { 276 + return nil, err 277 + } 278 + 279 + err = json.Unmarshal(body, &result) 280 + if err != nil { 281 + return nil, err 282 + } 283 + 284 + return &result, nil 285 + }
-1
knotserver/config/config.go
··· 17 17 type Server struct { 18 18 ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:5555"` 19 19 InternalListenAddr string `env:"INTERNAL_LISTEN_ADDR, default=127.0.0.1:5444"` 20 - Secret string `env:"SECRET, required"` 21 20 DBPath string `env:"DB_PATH, default=knotserver.db"` 22 21 Hostname string `env:"HOSTNAME, required"` 23 22 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
+14 -10
knotserver/db/init.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "strings" 5 6 6 7 _ "github.com/mattn/go-sqlite3" 7 8 ) ··· 11 12 } 12 13 13 14 func Setup(dbPath string) (*DB, error) { 14 - db, err := sql.Open("sqlite3", dbPath) 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, "&")) 15 24 if err != nil { 16 25 return nil, err 17 26 } 18 27 19 - _, err = db.Exec(` 20 - pragma journal_mode = WAL; 21 - pragma synchronous = normal; 22 - pragma foreign_keys = on; 23 - pragma temp_store = memory; 24 - pragma mmap_size = 30000000000; 25 - pragma page_size = 32768; 26 - pragma auto_vacuum = incremental; 27 - pragma busy_timeout = 5000; 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. 28 31 32 + _, err = db.Exec(` 29 33 create table if not exists known_dids ( 30 34 did text primary key 31 35 );
+8 -10
knotserver/git/fork.go
··· 10 10 ) 11 11 12 12 func Fork(repoPath, source string) error { 13 - _, err := git.PlainClone(repoPath, true, &git.CloneOptions{ 14 - URL: source, 15 - SingleBranch: false, 16 - }) 17 - 18 - if err != nil { 13 + cloneCmd := exec.Command("git", "clone", "--bare", source, repoPath) 14 + if err := cloneCmd.Run(); err != nil { 19 15 return fmt.Errorf("failed to bare clone repository: %w", err) 20 16 } 21 17 22 - err = exec.Command("git", "-C", repoPath, "config", "receive.hideRefs", "refs/hidden").Run() 23 - if err != nil { 18 + configureCmd := exec.Command("git", "-C", repoPath, "config", "receive.hideRefs", "refs/hidden") 19 + if err := configureCmd.Run(); err != nil { 24 20 return fmt.Errorf("failed to configure hidden refs: %w", err) 25 21 } 26 22 27 23 return nil 28 24 } 29 25 30 - func (g *GitRepo) Sync(branch string) error { 26 + func (g *GitRepo) Sync() error { 27 + branch := g.h.String() 28 + 31 29 fetchOpts := &git.FetchOptions{ 32 30 RefSpecs: []config.RefSpec{ 33 - config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branch, branch)), 31 + config.RefSpec("+" + branch + ":" + branch), // +refs/heads/master:refs/heads/master 34 32 }, 35 33 } 36 34
+5
knotserver/git.go
··· 129 129 // If the appview gave us the repository owner's handle we can attempt to 130 130 // construct the correct ssh url. 131 131 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle") 132 + ownerHandle = strings.TrimPrefix(ownerHandle, "@") 132 133 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") { 133 134 hostname := d.c.Server.Hostname 134 135 if strings.Contains(hostname, ":") { 135 136 hostname = strings.Split(hostname, ":")[0] 137 + } 138 + 139 + if hostname == "knot1.tangled.sh" { 140 + hostname = "tangled.sh" 136 141 } 137 142 138 143 fmt.Fprintf(w, " Try:\ngit remote set-url --push origin git@%s:%s/%s\n\n... and push again.", hostname, ownerHandle, unqualifiedRepoName)
+994 -167
knotserver/handler.go
··· 1 1 package knotserver 2 2 3 3 import ( 4 + "compress/gzip" 4 5 "context" 6 + "crypto/sha256" 7 + "encoding/json" 8 + "errors" 5 9 "fmt" 6 - "log/slog" 10 + "log" 7 11 "net/http" 8 - "runtime/debug" 12 + "net/url" 13 + "path/filepath" 14 + "strconv" 15 + "strings" 16 + "sync" 17 + "time" 9 18 19 + securejoin "github.com/cyphar/filepath-securejoin" 20 + "github.com/gliderlabs/ssh" 10 21 "github.com/go-chi/chi/v5" 11 - "tangled.sh/tangled.sh/core/idresolver" 12 - "tangled.sh/tangled.sh/core/jetstream" 13 - "tangled.sh/tangled.sh/core/knotserver/config" 22 + "github.com/go-git/go-git/v5/plumbing" 23 + "github.com/go-git/go-git/v5/plumbing/object" 14 24 "tangled.sh/tangled.sh/core/knotserver/db" 15 - "tangled.sh/tangled.sh/core/knotserver/xrpc" 16 - tlog "tangled.sh/tangled.sh/core/log" 17 - "tangled.sh/tangled.sh/core/notifier" 18 - "tangled.sh/tangled.sh/core/rbac" 19 - "tangled.sh/tangled.sh/core/xrpc/serviceauth" 25 + "tangled.sh/tangled.sh/core/knotserver/git" 26 + "tangled.sh/tangled.sh/core/types" 20 27 ) 21 28 22 - type Handle struct { 23 - c *config.Config 24 - db *db.DB 25 - jc *jetstream.JetstreamClient 26 - e *rbac.Enforcer 27 - l *slog.Logger 28 - n *notifier.Notifier 29 - resolver *idresolver.Resolver 29 + func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 30 + w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 31 + } 32 + 33 + func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) { 34 + w.Header().Set("Content-Type", "application/json") 35 + 36 + capabilities := map[string]any{ 37 + "pull_requests": map[string]any{ 38 + "format_patch": true, 39 + "patch_submissions": true, 40 + "branch_submissions": true, 41 + "fork_submissions": true, 42 + }, 43 + "xrpc": true, 44 + } 45 + 46 + jsonData, err := json.Marshal(capabilities) 47 + if err != nil { 48 + http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError) 49 + return 50 + } 51 + 52 + w.Write(jsonData) 53 + } 54 + 55 + func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 56 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 57 + l := h.l.With("path", path, "handler", "RepoIndex") 58 + ref := chi.URLParam(r, "ref") 59 + ref, _ = url.PathUnescape(ref) 60 + 61 + gr, err := git.Open(path, ref) 62 + if err != nil { 63 + plain, err2 := git.PlainOpen(path) 64 + if err2 != nil { 65 + l.Error("opening repo", "error", err2.Error()) 66 + notFound(w) 67 + return 68 + } 69 + branches, _ := plain.Branches() 70 + 71 + log.Println(err) 72 + 73 + if errors.Is(err, plumbing.ErrReferenceNotFound) { 74 + resp := types.RepoIndexResponse{ 75 + IsEmpty: true, 76 + Branches: branches, 77 + } 78 + writeJSON(w, resp) 79 + return 80 + } else { 81 + l.Error("opening repo", "error", err.Error()) 82 + notFound(w) 83 + return 84 + } 85 + } 86 + 87 + var ( 88 + commits []*object.Commit 89 + total int 90 + branches []types.Branch 91 + files []types.NiceTree 92 + tags []object.Tag 93 + ) 94 + 95 + var wg sync.WaitGroup 96 + errorsCh := make(chan error, 5) 97 + 98 + wg.Add(1) 99 + go func() { 100 + defer wg.Done() 101 + cs, err := gr.Commits(0, 60) 102 + if err != nil { 103 + errorsCh <- fmt.Errorf("commits: %w", err) 104 + return 105 + } 106 + commits = cs 107 + }() 108 + 109 + wg.Add(1) 110 + go func() { 111 + defer wg.Done() 112 + t, err := gr.TotalCommits() 113 + if err != nil { 114 + errorsCh <- fmt.Errorf("calculating total: %w", err) 115 + return 116 + } 117 + total = t 118 + }() 119 + 120 + wg.Add(1) 121 + go func() { 122 + defer wg.Done() 123 + bs, err := gr.Branches() 124 + if err != nil { 125 + errorsCh <- fmt.Errorf("fetching branches: %w", err) 126 + return 127 + } 128 + branches = bs 129 + }() 130 + 131 + wg.Add(1) 132 + go func() { 133 + defer wg.Done() 134 + ts, err := gr.Tags() 135 + if err != nil { 136 + errorsCh <- fmt.Errorf("fetching tags: %w", err) 137 + return 138 + } 139 + tags = ts 140 + }() 141 + 142 + wg.Add(1) 143 + go func() { 144 + defer wg.Done() 145 + fs, err := gr.FileTree(r.Context(), "") 146 + if err != nil { 147 + errorsCh <- fmt.Errorf("fetching filetree: %w", err) 148 + return 149 + } 150 + files = fs 151 + }() 152 + 153 + wg.Wait() 154 + close(errorsCh) 155 + 156 + // show any errors 157 + for err := range errorsCh { 158 + l.Error("loading repo", "error", err.Error()) 159 + writeError(w, err.Error(), http.StatusInternalServerError) 160 + return 161 + } 162 + 163 + rtags := []*types.TagReference{} 164 + for _, tag := range tags { 165 + var target *object.Tag 166 + if tag.Target != plumbing.ZeroHash { 167 + target = &tag 168 + } 169 + tr := types.TagReference{ 170 + Tag: target, 171 + } 172 + 173 + tr.Reference = types.Reference{ 174 + Name: tag.Name, 175 + Hash: tag.Hash.String(), 176 + } 177 + 178 + if tag.Message != "" { 179 + tr.Message = tag.Message 180 + } 181 + 182 + rtags = append(rtags, &tr) 183 + } 184 + 185 + var readmeContent string 186 + var readmeFile string 187 + for _, readme := range h.c.Repo.Readme { 188 + content, _ := gr.FileContent(readme) 189 + if len(content) > 0 { 190 + readmeContent = string(content) 191 + readmeFile = readme 192 + } 193 + } 194 + 195 + if ref == "" { 196 + mainBranch, err := gr.FindMainBranch() 197 + if err != nil { 198 + writeError(w, err.Error(), http.StatusInternalServerError) 199 + l.Error("finding main branch", "error", err.Error()) 200 + return 201 + } 202 + ref = mainBranch 203 + } 204 + 205 + resp := types.RepoIndexResponse{ 206 + IsEmpty: false, 207 + Ref: ref, 208 + Commits: commits, 209 + Description: getDescription(path), 210 + Readme: readmeContent, 211 + ReadmeFileName: readmeFile, 212 + Files: files, 213 + Branches: branches, 214 + Tags: rtags, 215 + TotalCommits: total, 216 + } 217 + 218 + writeJSON(w, resp) 30 219 } 31 220 32 - func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 33 - r := chi.NewRouter() 221 + func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 222 + treePath := chi.URLParam(r, "*") 223 + ref := chi.URLParam(r, "ref") 224 + ref, _ = url.PathUnescape(ref) 34 225 35 - h := Handle{ 36 - c: c, 37 - db: db, 38 - e: e, 39 - l: l, 40 - jc: jc, 41 - n: n, 42 - resolver: idresolver.DefaultResolver(), 226 + l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 227 + 228 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 229 + gr, err := git.Open(path, ref) 230 + if err != nil { 231 + notFound(w) 232 + return 43 233 } 44 234 45 - err := e.AddKnot(rbac.ThisServer) 235 + files, err := gr.FileTree(r.Context(), treePath) 46 236 if err != nil { 47 - return nil, fmt.Errorf("failed to setup enforcer: %w", err) 237 + writeError(w, err.Error(), http.StatusInternalServerError) 238 + l.Error("file tree", "error", err.Error()) 239 + return 48 240 } 49 241 50 - err = h.configureOwner() 242 + resp := types.RepoTreeResponse{ 243 + Ref: ref, 244 + Parent: treePath, 245 + Description: getDescription(path), 246 + DotDot: filepath.Dir(treePath), 247 + Files: files, 248 + } 249 + 250 + writeJSON(w, resp) 251 + } 252 + 253 + func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 254 + treePath := chi.URLParam(r, "*") 255 + ref := chi.URLParam(r, "ref") 256 + ref, _ = url.PathUnescape(ref) 257 + 258 + l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 259 + 260 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 261 + gr, err := git.Open(path, ref) 51 262 if err != nil { 52 - return nil, err 263 + notFound(w) 264 + return 53 265 } 54 - h.l.Info("owner set", "did", h.c.Server.Owner) 55 266 56 - err = h.jc.StartJetstream(ctx, h.processMessages) 267 + contents, err := gr.RawContent(treePath) 57 268 if err != nil { 58 - return nil, fmt.Errorf("failed to start jetstream: %w", err) 269 + writeError(w, err.Error(), http.StatusBadRequest) 270 + l.Error("file content", "error", err.Error()) 271 + return 272 + } 273 + 274 + mimeType := http.DetectContentType(contents) 275 + 276 + // exception for svg 277 + if filepath.Ext(treePath) == ".svg" { 278 + mimeType = "image/svg+xml" 279 + } 280 + 281 + contentHash := sha256.Sum256(contents) 282 + eTag := fmt.Sprintf("\"%x\"", contentHash) 283 + 284 + // allow image, video, and text/plain files to be served directly 285 + switch { 286 + case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 287 + if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 288 + w.WriteHeader(http.StatusNotModified) 289 + return 290 + } 291 + w.Header().Set("ETag", eTag) 292 + 293 + case strings.HasPrefix(mimeType, "text/plain"): 294 + w.Header().Set("Cache-Control", "public, no-cache") 295 + 296 + default: 297 + l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 298 + writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 299 + return 59 300 } 60 301 61 - h.jc.AddDid(h.c.Server.Owner) 302 + w.Header().Set("Content-Type", mimeType) 303 + w.Write(contents) 304 + } 62 305 63 - // check if the knot knows about any dids 64 - dids, err := h.db.GetAllDids() 306 + func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 307 + treePath := chi.URLParam(r, "*") 308 + ref := chi.URLParam(r, "ref") 309 + ref, _ = url.PathUnescape(ref) 310 + 311 + l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 312 + 313 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 314 + gr, err := git.Open(path, ref) 65 315 if err != nil { 66 - return nil, fmt.Errorf("failed to get all dids: %w", err) 316 + notFound(w) 317 + return 67 318 } 68 - for _, d := range dids { 69 - jc.AddDid(d) 319 + 320 + var isBinaryFile bool = false 321 + contents, err := gr.FileContent(treePath) 322 + if errors.Is(err, git.ErrBinaryFile) { 323 + isBinaryFile = true 324 + } else if errors.Is(err, object.ErrFileNotFound) { 325 + notFound(w) 326 + return 327 + } else if err != nil { 328 + writeError(w, err.Error(), http.StatusInternalServerError) 329 + return 70 330 } 71 331 72 - r.Get("/", h.Index) 73 - r.Get("/capabilities", h.Capabilities) 74 - r.Get("/version", h.Version) 75 - r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { 76 - w.Write([]byte(h.c.Server.Owner)) 77 - }) 78 - r.Route("/{did}", func(r chi.Router) { 79 - // Repo routes 80 - r.Route("/{name}", func(r chi.Router) { 81 - r.Route("/collaborator", func(r chi.Router) { 82 - r.Use(h.VerifySignature) 83 - r.Post("/add", h.AddRepoCollaborator) 84 - }) 332 + bytes := []byte(contents) 333 + // safe := string(sanitize(bytes)) 334 + sizeHint := len(bytes) 85 335 86 - r.Route("/languages", func(r chi.Router) { 87 - r.Get("/", h.RepoLanguages) 88 - r.Get("/{ref}", h.RepoLanguages) 89 - }) 336 + resp := types.RepoBlobResponse{ 337 + Ref: ref, 338 + Contents: string(bytes), 339 + Path: treePath, 340 + IsBinary: isBinaryFile, 341 + SizeHint: uint64(sizeHint), 342 + } 90 343 91 - r.Get("/", h.RepoIndex) 92 - r.Get("/info/refs", h.InfoRefs) 93 - r.Post("/git-upload-pack", h.UploadPack) 94 - r.Post("/git-receive-pack", h.ReceivePack) 95 - r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 344 + h.showFile(resp, w, l) 345 + } 96 346 97 - r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef) 347 + func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 348 + name := chi.URLParam(r, "name") 349 + file := chi.URLParam(r, "file") 98 350 99 - r.Route("/merge", func(r chi.Router) { 100 - r.With(h.VerifySignature) 101 - r.Post("/", h.Merge) 102 - r.Post("/check", h.MergeCheck) 103 - }) 351 + l := h.l.With("handler", "Archive", "name", name, "file", file) 104 352 105 - r.Route("/tree/{ref}", func(r chi.Router) { 106 - r.Get("/", h.RepoIndex) 107 - r.Get("/*", h.RepoTree) 108 - }) 353 + // TODO: extend this to add more files compression (e.g.: xz) 354 + if !strings.HasSuffix(file, ".tar.gz") { 355 + notFound(w) 356 + return 357 + } 109 358 110 - r.Route("/blob/{ref}", func(r chi.Router) { 111 - r.Get("/*", h.Blob) 112 - }) 359 + ref := strings.TrimSuffix(file, ".tar.gz") 113 360 114 - r.Route("/raw/{ref}", func(r chi.Router) { 115 - r.Get("/*", h.BlobRaw) 116 - }) 361 + unescapedRef, err := url.PathUnescape(ref) 362 + if err != nil { 363 + notFound(w) 364 + return 365 + } 117 366 118 - r.Get("/log/{ref}", h.Log) 119 - r.Get("/archive/{file}", h.Archive) 120 - r.Get("/commit/{ref}", h.Diff) 121 - r.Get("/tags", h.Tags) 122 - r.Route("/branches", func(r chi.Router) { 123 - r.Get("/", h.Branches) 124 - r.Get("/{branch}", h.Branch) 125 - r.Route("/default", func(r chi.Router) { 126 - r.Get("/", h.DefaultBranch) 127 - r.With(h.VerifySignature).Put("/", h.SetDefaultBranch) 128 - }) 129 - }) 130 - }) 131 - }) 367 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 132 368 133 - // xrpc apis 134 - r.Mount("/xrpc", h.XrpcRouter()) 369 + // This allows the browser to use a proper name for the file when 370 + // downloading 371 + filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 372 + setContentDisposition(w, filename) 373 + setGZipMIME(w) 135 374 136 - // Create a new repository. 137 - r.Route("/repo", func(r chi.Router) { 138 - r.Use(h.VerifySignature) 139 - r.Delete("/", h.RemoveRepo) 140 - r.Route("/fork", func(r chi.Router) { 141 - r.Post("/", h.RepoFork) 142 - r.Post("/sync/{branch}", h.RepoForkSync) 143 - r.Get("/sync/{branch}", h.RepoForkAheadBehind) 144 - }) 145 - }) 375 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 376 + gr, err := git.Open(path, unescapedRef) 377 + if err != nil { 378 + notFound(w) 379 + return 380 + } 381 + 382 + gw := gzip.NewWriter(w) 383 + defer gw.Close() 384 + 385 + prefix := fmt.Sprintf("%s-%s", name, safeRefFilename) 386 + err = gr.WriteTar(gw, prefix) 387 + if err != nil { 388 + // once we start writing to the body we can't report error anymore 389 + // so we are only left with printing the error. 390 + l.Error("writing tar file", "error", err.Error()) 391 + return 392 + } 393 + 394 + err = gw.Flush() 395 + if err != nil { 396 + // once we start writing to the body we can't report error anymore 397 + // so we are only left with printing the error. 398 + l.Error("flushing?", "error", err.Error()) 399 + return 400 + } 401 + } 402 + 403 + func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 404 + ref := chi.URLParam(r, "ref") 405 + ref, _ = url.PathUnescape(ref) 406 + 407 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 408 + 409 + l := h.l.With("handler", "Log", "ref", ref, "path", path) 410 + 411 + gr, err := git.Open(path, ref) 412 + if err != nil { 413 + notFound(w) 414 + return 415 + } 146 416 147 - r.Route("/member", func(r chi.Router) { 148 - r.Use(h.VerifySignature) 149 - r.Put("/add", h.AddMember) 150 - }) 417 + // Get page parameters 418 + page := 1 419 + pageSize := 30 151 420 152 - // Socket that streams git oplogs 153 - r.Get("/events", h.Events) 421 + if pageParam := r.URL.Query().Get("page"); pageParam != "" { 422 + if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 423 + page = p 424 + } 425 + } 426 + 427 + if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 428 + if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 429 + pageSize = ps 430 + } 431 + } 432 + 433 + // convert to offset/limit 434 + offset := (page - 1) * pageSize 435 + limit := pageSize 436 + 437 + commits, err := gr.Commits(offset, limit) 438 + if err != nil { 439 + writeError(w, err.Error(), http.StatusInternalServerError) 440 + l.Error("fetching commits", "error", err.Error()) 441 + return 442 + } 154 443 155 - // Health check. Used for two-way verification with appview. 156 - r.With(h.VerifySignature).Get("/health", h.Health) 444 + total := len(commits) 157 445 158 - // All public keys on the knot. 159 - r.Get("/keys", h.Keys) 446 + resp := types.RepoLogResponse{ 447 + Commits: commits, 448 + Ref: ref, 449 + Description: getDescription(path), 450 + Log: true, 451 + Total: total, 452 + Page: page, 453 + PerPage: pageSize, 454 + } 160 455 161 - return r, nil 456 + writeJSON(w, resp) 162 457 } 163 458 164 - func (h *Handle) XrpcRouter() http.Handler { 165 - logger := tlog.New("knots") 459 + func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 460 + ref := chi.URLParam(r, "ref") 461 + ref, _ = url.PathUnescape(ref) 166 462 167 - serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 463 + l := h.l.With("handler", "Diff", "ref", ref) 168 464 169 - xrpc := &xrpc.Xrpc{ 170 - Config: h.c, 171 - Db: h.db, 172 - Ingester: h.jc, 173 - Enforcer: h.e, 174 - Logger: logger, 175 - Notifier: h.n, 176 - Resolver: h.resolver, 177 - ServiceAuth: serviceAuth, 465 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 466 + gr, err := git.Open(path, ref) 467 + if err != nil { 468 + notFound(w) 469 + return 178 470 } 179 - return xrpc.Router() 471 + 472 + diff, err := gr.Diff() 473 + if err != nil { 474 + writeError(w, err.Error(), http.StatusInternalServerError) 475 + l.Error("getting diff", "error", err.Error()) 476 + return 477 + } 478 + 479 + resp := types.RepoCommitResponse{ 480 + Ref: ref, 481 + Diff: diff, 482 + } 483 + 484 + writeJSON(w, resp) 180 485 } 181 486 182 - // version is set during build time. 183 - var version string 487 + func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 488 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 489 + l := h.l.With("handler", "Refs") 184 490 185 - func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { 186 - if version == "" { 187 - info, ok := debug.ReadBuildInfo() 188 - if !ok { 189 - http.Error(w, "failed to read build info", http.StatusInternalServerError) 190 - return 491 + gr, err := git.Open(path, "") 492 + if err != nil { 493 + notFound(w) 494 + return 495 + } 496 + 497 + tags, err := gr.Tags() 498 + if err != nil { 499 + // Non-fatal, we *should* have at least one branch to show. 500 + l.Warn("getting tags", "error", err.Error()) 501 + } 502 + 503 + rtags := []*types.TagReference{} 504 + for _, tag := range tags { 505 + var target *object.Tag 506 + if tag.Target != plumbing.ZeroHash { 507 + target = &tag 508 + } 509 + tr := types.TagReference{ 510 + Tag: target, 191 511 } 192 512 193 - var modVer string 194 - for _, mod := range info.Deps { 195 - if mod.Path == "tangled.sh/tangled.sh/knotserver" { 196 - version = mod.Version 197 - break 198 - } 513 + tr.Reference = types.Reference{ 514 + Name: tag.Name, 515 + Hash: tag.Hash.String(), 199 516 } 200 517 201 - if modVer == "" { 202 - version = "unknown" 518 + if tag.Message != "" { 519 + tr.Message = tag.Message 203 520 } 521 + 522 + rtags = append(rtags, &tr) 204 523 } 205 524 206 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 207 - fmt.Fprintf(w, "knotserver/%s", version) 525 + resp := types.RepoTagsResponse{ 526 + Tags: rtags, 527 + } 528 + 529 + writeJSON(w, resp) 530 + } 531 + 532 + func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 533 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 534 + 535 + gr, err := git.PlainOpen(path) 536 + if err != nil { 537 + notFound(w) 538 + return 539 + } 540 + 541 + branches, _ := gr.Branches() 542 + 543 + resp := types.RepoBranchesResponse{ 544 + Branches: branches, 545 + } 546 + 547 + writeJSON(w, resp) 208 548 } 209 549 210 - func (h *Handle) configureOwner() error { 211 - cfgOwner := h.c.Server.Owner 550 + func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 551 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 552 + branchName := chi.URLParam(r, "branch") 553 + branchName, _ = url.PathUnescape(branchName) 554 + 555 + l := h.l.With("handler", "Branch") 556 + 557 + gr, err := git.PlainOpen(path) 558 + if err != nil { 559 + notFound(w) 560 + return 561 + } 562 + 563 + ref, err := gr.Branch(branchName) 564 + if err != nil { 565 + l.Error("getting branch", "error", err.Error()) 566 + writeError(w, err.Error(), http.StatusInternalServerError) 567 + return 568 + } 212 569 213 - rbacDomain := "thisserver" 570 + commit, err := gr.Commit(ref.Hash()) 571 + if err != nil { 572 + l.Error("getting commit object", "error", err.Error()) 573 + writeError(w, err.Error(), http.StatusInternalServerError) 574 + return 575 + } 214 576 215 - existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 577 + defaultBranch, err := gr.FindMainBranch() 578 + isDefault := false 216 579 if err != nil { 217 - return err 580 + l.Error("getting default branch", "error", err.Error()) 581 + // do not quit though 582 + } else if defaultBranch == branchName { 583 + isDefault = true 218 584 } 219 585 220 - switch len(existing) { 221 - case 0: 222 - // no owner configured, continue 223 - case 1: 224 - // find existing owner 225 - existingOwner := existing[0] 586 + resp := types.RepoBranchResponse{ 587 + Branch: types.Branch{ 588 + Reference: types.Reference{ 589 + Name: ref.Name().Short(), 590 + Hash: ref.Hash().String(), 591 + }, 592 + Commit: commit, 593 + IsDefault: isDefault, 594 + }, 595 + } 226 596 227 - // no ownership change, this is okay 228 - if existingOwner == h.c.Server.Owner { 229 - break 597 + writeJSON(w, resp) 598 + } 599 + 600 + func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 601 + l := h.l.With("handler", "Keys") 602 + 603 + switch r.Method { 604 + case http.MethodGet: 605 + keys, err := h.db.GetAllPublicKeys() 606 + if err != nil { 607 + writeError(w, err.Error(), http.StatusInternalServerError) 608 + l.Error("getting public keys", "error", err.Error()) 609 + return 230 610 } 231 611 232 - // remove existing owner 233 - err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) 612 + data := make([]map[string]any, 0) 613 + for _, key := range keys { 614 + j := key.JSON() 615 + data = append(data, j) 616 + } 617 + writeJSON(w, data) 618 + return 619 + 620 + case http.MethodPut: 621 + pk := db.PublicKey{} 622 + if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 623 + writeError(w, "invalid request body", http.StatusBadRequest) 624 + return 625 + } 626 + 627 + _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 234 628 if err != nil { 235 - return nil 629 + writeError(w, "invalid pubkey", http.StatusBadRequest) 236 630 } 237 - default: 238 - return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 631 + 632 + if err := h.db.AddPublicKey(pk); err != nil { 633 + writeError(w, err.Error(), http.StatusInternalServerError) 634 + l.Error("adding public key", "error", err.Error()) 635 + return 636 + } 637 + 638 + w.WriteHeader(http.StatusNoContent) 639 + return 239 640 } 641 + } 240 642 241 - return h.e.AddKnotOwner(rbacDomain, cfgOwner) 643 + // func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 644 + // l := h.l.With("handler", "RepoForkSync") 645 + // 646 + // data := struct { 647 + // Did string `json:"did"` 648 + // Source string `json:"source"` 649 + // Name string `json:"name,omitempty"` 650 + // HiddenRef string `json:"hiddenref"` 651 + // }{} 652 + // 653 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 654 + // writeError(w, "invalid request body", http.StatusBadRequest) 655 + // return 656 + // } 657 + // 658 + // did := data.Did 659 + // source := data.Source 660 + // 661 + // if did == "" || source == "" { 662 + // l.Error("invalid request body, empty did or name") 663 + // w.WriteHeader(http.StatusBadRequest) 664 + // return 665 + // } 666 + // 667 + // var name string 668 + // if data.Name != "" { 669 + // name = data.Name 670 + // } else { 671 + // name = filepath.Base(source) 672 + // } 673 + // 674 + // branch := chi.URLParam(r, "branch") 675 + // branch, _ = url.PathUnescape(branch) 676 + // 677 + // relativeRepoPath := filepath.Join(did, name) 678 + // repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 679 + // 680 + // gr, err := git.PlainOpen(repoPath) 681 + // if err != nil { 682 + // log.Println(err) 683 + // notFound(w) 684 + // return 685 + // } 686 + // 687 + // forkCommit, err := gr.ResolveRevision(branch) 688 + // if err != nil { 689 + // l.Error("error resolving ref revision", "msg", err.Error()) 690 + // writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest) 691 + // return 692 + // } 693 + // 694 + // sourceCommit, err := gr.ResolveRevision(data.HiddenRef) 695 + // if err != nil { 696 + // l.Error("error resolving hidden ref revision", "msg", err.Error()) 697 + // writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest) 698 + // return 699 + // } 700 + // 701 + // status := types.UpToDate 702 + // if forkCommit.Hash.String() != sourceCommit.Hash.String() { 703 + // isAncestor, err := forkCommit.IsAncestor(sourceCommit) 704 + // if err != nil { 705 + // log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 706 + // return 707 + // } 708 + // 709 + // if isAncestor { 710 + // status = types.FastForwardable 711 + // } else { 712 + // status = types.Conflict 713 + // } 714 + // } 715 + // 716 + // w.Header().Set("Content-Type", "application/json") 717 + // json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 718 + // } 719 + 720 + func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) { 721 + repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 722 + ref := chi.URLParam(r, "ref") 723 + ref, _ = url.PathUnescape(ref) 724 + 725 + l := h.l.With("handler", "RepoLanguages") 726 + 727 + gr, err := git.Open(repoPath, ref) 728 + if err != nil { 729 + l.Error("opening repo", "error", err.Error()) 730 + notFound(w) 731 + return 732 + } 733 + 734 + ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) 735 + defer cancel() 736 + 737 + sizes, err := gr.AnalyzeLanguages(ctx) 738 + if err != nil { 739 + l.Error("failed to analyze languages", "error", err.Error()) 740 + writeError(w, err.Error(), http.StatusNoContent) 741 + return 742 + } 743 + 744 + resp := types.RepoLanguageResponse{Languages: sizes} 745 + 746 + writeJSON(w, resp) 747 + } 748 + 749 + // func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 750 + // l := h.l.With("handler", "RepoForkSync") 751 + // 752 + // data := struct { 753 + // Did string `json:"did"` 754 + // Source string `json:"source"` 755 + // Name string `json:"name,omitempty"` 756 + // }{} 757 + // 758 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 759 + // writeError(w, "invalid request body", http.StatusBadRequest) 760 + // return 761 + // } 762 + // 763 + // did := data.Did 764 + // source := data.Source 765 + // 766 + // if did == "" || source == "" { 767 + // l.Error("invalid request body, empty did or name") 768 + // w.WriteHeader(http.StatusBadRequest) 769 + // return 770 + // } 771 + // 772 + // var name string 773 + // if data.Name != "" { 774 + // name = data.Name 775 + // } else { 776 + // name = filepath.Base(source) 777 + // } 778 + // 779 + // branch := chi.URLParam(r, "branch") 780 + // branch, _ = url.PathUnescape(branch) 781 + // 782 + // relativeRepoPath := filepath.Join(did, name) 783 + // repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 784 + // 785 + // gr, err := git.Open(repoPath, branch) 786 + // if err != nil { 787 + // log.Println(err) 788 + // notFound(w) 789 + // return 790 + // } 791 + // 792 + // err = gr.Sync() 793 + // if err != nil { 794 + // l.Error("error syncing repo fork", "error", err.Error()) 795 + // writeError(w, err.Error(), http.StatusInternalServerError) 796 + // return 797 + // } 798 + // 799 + // w.WriteHeader(http.StatusNoContent) 800 + // } 801 + 802 + // func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 803 + // l := h.l.With("handler", "RepoFork") 804 + // 805 + // data := struct { 806 + // Did string `json:"did"` 807 + // Source string `json:"source"` 808 + // Name string `json:"name,omitempty"` 809 + // }{} 810 + // 811 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 812 + // writeError(w, "invalid request body", http.StatusBadRequest) 813 + // return 814 + // } 815 + // 816 + // did := data.Did 817 + // source := data.Source 818 + // 819 + // if did == "" || source == "" { 820 + // l.Error("invalid request body, empty did or name") 821 + // w.WriteHeader(http.StatusBadRequest) 822 + // return 823 + // } 824 + // 825 + // var name string 826 + // if data.Name != "" { 827 + // name = data.Name 828 + // } else { 829 + // name = filepath.Base(source) 830 + // } 831 + // 832 + // relativeRepoPath := filepath.Join(did, name) 833 + // repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 834 + // 835 + // err := git.Fork(repoPath, source) 836 + // if err != nil { 837 + // l.Error("forking repo", "error", err.Error()) 838 + // writeError(w, err.Error(), http.StatusInternalServerError) 839 + // return 840 + // } 841 + // 842 + // // add perms for this user to access the repo 843 + // err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath) 844 + // if err != nil { 845 + // l.Error("adding repo permissions", "error", err.Error()) 846 + // writeError(w, err.Error(), http.StatusInternalServerError) 847 + // return 848 + // } 849 + // 850 + // hook.SetupRepo( 851 + // hook.Config( 852 + // hook.WithScanPath(h.c.Repo.ScanPath), 853 + // hook.WithInternalApi(h.c.Server.InternalListenAddr), 854 + // ), 855 + // repoPath, 856 + // ) 857 + // 858 + // w.WriteHeader(http.StatusNoContent) 859 + // } 860 + 861 + // func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 862 + // l := h.l.With("handler", "RemoveRepo") 863 + // 864 + // data := struct { 865 + // Did string `json:"did"` 866 + // Name string `json:"name"` 867 + // }{} 868 + // 869 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 870 + // writeError(w, "invalid request body", http.StatusBadRequest) 871 + // return 872 + // } 873 + // 874 + // did := data.Did 875 + // name := data.Name 876 + // 877 + // if did == "" || name == "" { 878 + // l.Error("invalid request body, empty did or name") 879 + // w.WriteHeader(http.StatusBadRequest) 880 + // return 881 + // } 882 + // 883 + // relativeRepoPath := filepath.Join(did, name) 884 + // repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 885 + // err := os.RemoveAll(repoPath) 886 + // if err != nil { 887 + // l.Error("removing repo", "error", err.Error()) 888 + // writeError(w, err.Error(), http.StatusInternalServerError) 889 + // return 890 + // } 891 + // 892 + // w.WriteHeader(http.StatusNoContent) 893 + // 894 + // } 895 + 896 + // func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 897 + // path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 898 + // 899 + // data := types.MergeRequest{} 900 + // 901 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 902 + // writeError(w, err.Error(), http.StatusBadRequest) 903 + // h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 904 + // return 905 + // } 906 + // 907 + // mo := &git.MergeOptions{ 908 + // AuthorName: data.AuthorName, 909 + // AuthorEmail: data.AuthorEmail, 910 + // CommitBody: data.CommitBody, 911 + // CommitMessage: data.CommitMessage, 912 + // } 913 + // 914 + // patch := data.Patch 915 + // branch := data.Branch 916 + // gr, err := git.Open(path, branch) 917 + // if err != nil { 918 + // notFound(w) 919 + // return 920 + // } 921 + // 922 + // mo.FormatPatch = patchutil.IsFormatPatch(patch) 923 + // 924 + // if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 925 + // var mergeErr *git.ErrMerge 926 + // if errors.As(err, &mergeErr) { 927 + // conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 928 + // for i, conflict := range mergeErr.Conflicts { 929 + // conflicts[i] = types.ConflictInfo{ 930 + // Filename: conflict.Filename, 931 + // Reason: conflict.Reason, 932 + // } 933 + // } 934 + // response := types.MergeCheckResponse{ 935 + // IsConflicted: true, 936 + // Conflicts: conflicts, 937 + // Message: mergeErr.Message, 938 + // } 939 + // writeConflict(w, response) 940 + // h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 941 + // } else { 942 + // writeError(w, err.Error(), http.StatusBadRequest) 943 + // h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 944 + // } 945 + // return 946 + // } 947 + // 948 + // w.WriteHeader(http.StatusOK) 949 + // } 950 + 951 + // func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 952 + // path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 953 + // 954 + // var data struct { 955 + // Patch string `json:"patch"` 956 + // Branch string `json:"branch"` 957 + // } 958 + // 959 + // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 960 + // writeError(w, err.Error(), http.StatusBadRequest) 961 + // h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 962 + // return 963 + // } 964 + // 965 + // patch := data.Patch 966 + // branch := data.Branch 967 + // gr, err := git.Open(path, branch) 968 + // if err != nil { 969 + // notFound(w) 970 + // return 971 + // } 972 + // 973 + // err = gr.MergeCheck([]byte(patch), branch) 974 + // if err == nil { 975 + // response := types.MergeCheckResponse{ 976 + // IsConflicted: false, 977 + // } 978 + // writeJSON(w, response) 979 + // return 980 + // } 981 + // 982 + // var mergeErr *git.ErrMerge 983 + // if errors.As(err, &mergeErr) { 984 + // conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 985 + // for i, conflict := range mergeErr.Conflicts { 986 + // conflicts[i] = types.ConflictInfo{ 987 + // Filename: conflict.Filename, 988 + // Reason: conflict.Reason, 989 + // } 990 + // } 991 + // response := types.MergeCheckResponse{ 992 + // IsConflicted: true, 993 + // Conflicts: conflicts, 994 + // Message: mergeErr.Message, 995 + // } 996 + // writeConflict(w, response) 997 + // h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 998 + // return 999 + // } 1000 + // writeError(w, err.Error(), http.StatusInternalServerError) 1001 + // h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1002 + // } 1003 + 1004 + func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1005 + rev1 := chi.URLParam(r, "rev1") 1006 + rev1, _ = url.PathUnescape(rev1) 1007 + 1008 + rev2 := chi.URLParam(r, "rev2") 1009 + rev2, _ = url.PathUnescape(rev2) 1010 + 1011 + l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1012 + 1013 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1014 + gr, err := git.PlainOpen(path) 1015 + if err != nil { 1016 + notFound(w) 1017 + return 1018 + } 1019 + 1020 + commit1, err := gr.ResolveRevision(rev1) 1021 + if err != nil { 1022 + l.Error("error resolving revision 1", "msg", err.Error()) 1023 + writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1024 + return 1025 + } 1026 + 1027 + commit2, err := gr.ResolveRevision(rev2) 1028 + if err != nil { 1029 + l.Error("error resolving revision 2", "msg", err.Error()) 1030 + writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1031 + return 1032 + } 1033 + 1034 + rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1035 + if err != nil { 1036 + l.Error("error comparing revisions", "msg", err.Error()) 1037 + writeError(w, "error comparing revisions", http.StatusBadRequest) 1038 + return 1039 + } 1040 + 1041 + writeJSON(w, types.RepoFormatPatchResponse{ 1042 + Rev1: commit1.Hash.String(), 1043 + Rev2: commit2.Hash.String(), 1044 + FormatPatch: formatPatch, 1045 + Patch: rawPatch, 1046 + }) 1047 + } 1048 + 1049 + func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1050 + l := h.l.With("handler", "DefaultBranch") 1051 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1052 + 1053 + gr, err := git.Open(path, "") 1054 + if err != nil { 1055 + notFound(w) 1056 + return 1057 + } 1058 + 1059 + branch, err := gr.FindMainBranch() 1060 + if err != nil { 1061 + writeError(w, err.Error(), http.StatusInternalServerError) 1062 + l.Error("getting default branch", "error", err.Error()) 1063 + return 1064 + } 1065 + 1066 + writeJSON(w, types.RepoDefaultBranchResponse{ 1067 + Branch: branch, 1068 + }) 242 1069 }
-10
knotserver/http_util.go
··· 20 20 func notFound(w http.ResponseWriter) { 21 21 writeError(w, "not found", http.StatusNotFound) 22 22 } 23 - 24 - func writeMsg(w http.ResponseWriter, msg string) { 25 - writeJSON(w, map[string]string{"msg": msg}) 26 - } 27 - 28 - func writeConflict(w http.ResponseWriter, data interface{}) { 29 - w.Header().Set("Content-Type", "application/json") 30 - w.WriteHeader(http.StatusConflict) 31 - json.NewEncoder(w).Encode(data) 32 - }
+126 -215
knotserver/ingester.go
··· 8 8 "net/http" 9 9 "net/url" 10 10 "path/filepath" 11 - "slices" 12 11 "strings" 13 12 14 13 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 25 24 "tangled.sh/tangled.sh/core/workflow" 26 25 ) 27 26 28 - func (h *Handle) processPublicKey(ctx context.Context, did string, operation string, record tangled.PublicKey) error { 27 + func (h *Handle) processPublicKey(ctx context.Context, event *models.Event) error { 29 28 l := log.FromContext(ctx) 29 + raw := json.RawMessage(event.Commit.Record) 30 + did := event.Did 30 31 31 - switch operation { 32 - case models.CommitOperationCreate, models.CommitOperationUpdate: 33 - pk := db.PublicKey{ 34 - Did: did, 35 - PublicKey: record, 36 - } 37 - if err := h.db.AddPublicKey(pk); err != nil { 38 - l.Error("failed to add public key", "error", err) 39 - return fmt.Errorf("failed to add public key: %w", err) 40 - } 41 - l.Info("added public key from firehose", "did", did) 32 + var record tangled.PublicKey 33 + if err := json.Unmarshal(raw, &record); err != nil { 34 + return fmt.Errorf("failed to unmarshal record: %w", err) 35 + } 42 36 43 - case models.CommitOperationDelete: 44 - if err := h.db.RemovePublicKey(did); err != nil { 45 - l.Error("failed to remove public key", "error", err) 46 - return fmt.Errorf("failed to remove public key: %w", err) 47 - } 48 - l.Info("removed public key (delete triggered from firehose)", "did", did) 37 + pk := db.PublicKey{ 38 + Did: did, 39 + PublicKey: record, 40 + } 41 + if err := h.db.AddPublicKey(pk); err != nil { 42 + l.Error("failed to add public key", "error", err) 43 + return fmt.Errorf("failed to add public key: %w", err) 49 44 } 50 - 45 + l.Info("added public key from firehose", "did", did) 51 46 return nil 52 47 } 53 48 54 - func (h *Handle) processKnotMember(ctx context.Context, did string, operation string, record tangled.KnotMember) error { 49 + func (h *Handle) processKnotMember(ctx context.Context, event *models.Event) error { 55 50 l := log.FromContext(ctx) 51 + raw := json.RawMessage(event.Commit.Record) 52 + did := event.Did 56 53 57 - switch operation { 58 - case models.CommitOperationCreate, models.CommitOperationUpdate: 59 - if record.Domain != h.c.Server.Hostname { 60 - l.Error("domain mismatch", "domain", record.Domain, "expected", h.c.Server.Hostname) 61 - return fmt.Errorf("domain mismatch: %s != %s", record.Domain, h.c.Server.Hostname) 62 - } 54 + var record tangled.KnotMember 55 + if err := json.Unmarshal(raw, &record); err != nil { 56 + return fmt.Errorf("failed to unmarshal record: %w", err) 57 + } 63 58 64 - ok, err := h.e.E.Enforce(did, rbac.ThisServer, rbac.ThisServer, "server:invite") 65 - if err != nil || !ok { 66 - l.Error("failed to add member", "did", did) 67 - return fmt.Errorf("failed to enforce permissions: %w", err) 68 - } 59 + if record.Domain != h.c.Server.Hostname { 60 + l.Error("domain mismatch", "domain", record.Domain, "expected", h.c.Server.Hostname) 61 + return fmt.Errorf("domain mismatch: %s != %s", record.Domain, h.c.Server.Hostname) 62 + } 69 63 70 - if err := h.e.AddKnotMember(rbac.ThisServer, record.Subject); err != nil { 71 - l.Error("failed to add member", "error", err) 72 - return fmt.Errorf("failed to add member: %w", err) 73 - } 74 - l.Info("added member from firehose", "member", record.Subject) 64 + ok, err := h.e.E.Enforce(did, rbac.ThisServer, rbac.ThisServer, "server:invite") 65 + if err != nil || !ok { 66 + l.Error("failed to add member", "did", did) 67 + return fmt.Errorf("failed to enforce permissions: %w", err) 68 + } 75 69 76 - if err := h.db.AddDid(did); err != nil { 77 - l.Error("failed to add did", "error", err) 78 - return fmt.Errorf("failed to add did: %w", err) 79 - } 80 - h.jc.AddDid(did) 70 + if err := h.e.AddKnotMember(rbac.ThisServer, record.Subject); err != nil { 71 + l.Error("failed to add member", "error", err) 72 + return fmt.Errorf("failed to add member: %w", err) 73 + } 74 + l.Info("added member from firehose", "member", record.Subject) 81 75 82 - if err := h.fetchAndAddKeys(ctx, did); err != nil { 83 - return fmt.Errorf("failed to fetch and add keys: %w", err) 84 - } 76 + if err := h.db.AddDid(record.Subject); err != nil { 77 + l.Error("failed to add did", "error", err) 78 + return fmt.Errorf("failed to add did: %w", err) 79 + } 80 + h.jc.AddDid(record.Subject) 85 81 86 - case models.CommitOperationDelete: 87 - if err := h.e.RemoveKnotMember(rbac.ThisServer, record.Subject); err != nil { 88 - l.Error("failed to remove member", "error", err) 89 - return fmt.Errorf("failed to remove member: %w", err) 90 - } 91 - l.Info("removed member (delete triggered from firehose)", "member", record.Subject) 92 - 93 - if err := h.db.RemoveDid(record.Subject); err != nil { 94 - l.Error("failed to remove did", "error", err) 95 - return fmt.Errorf("failed to remove did: %w", err) 96 - } 97 - h.jc.RemoveDid(record.Subject) 82 + if err := h.fetchAndAddKeys(ctx, record.Subject); err != nil { 83 + return fmt.Errorf("failed to fetch and add keys: %w", err) 98 84 } 99 85 100 86 return nil 101 87 } 102 88 103 - func (h *Handle) processPull(ctx context.Context, did string, record tangled.RepoPull) error { 89 + func (h *Handle) processPull(ctx context.Context, event *models.Event) error { 90 + raw := json.RawMessage(event.Commit.Record) 91 + did := event.Did 92 + 93 + var record tangled.RepoPull 94 + if err := json.Unmarshal(raw, &record); err != nil { 95 + return fmt.Errorf("failed to unmarshal record: %w", err) 96 + } 97 + 104 98 l := log.FromContext(ctx) 105 99 l = l.With("handler", "processPull") 106 100 l = l.With("did", did) ··· 108 102 l = l.With("target_branch", record.TargetBranch) 109 103 110 104 if record.Source == nil { 111 - reason := "not a branch-based pull request" 112 - l.Info("ignoring pull record", "reason", reason) 113 - return fmt.Errorf("ignoring pull record: %s", reason) 105 + return fmt.Errorf("ignoring pull record: not a branch-based pull request") 114 106 } 115 107 116 108 if record.Source.Repo != nil { 117 - reason := "fork based pull" 118 - l.Info("ignoring pull record", "reason", reason) 119 - return fmt.Errorf("ignoring pull record: %s", reason) 120 - } 121 - 122 - allDids, err := h.db.GetAllDids() 123 - if err != nil { 124 - return err 125 - } 126 - 127 - // presently: we only process PRs from collaborators for pipelines 128 - if !slices.Contains(allDids, did) { 129 - reason := "not a known did" 130 - l.Info("rejecting pull record", "reason", reason) 131 - return fmt.Errorf("rejected pull record: %s, %s", reason, did) 109 + return fmt.Errorf("ignoring pull record: fork based pull") 132 110 } 133 111 134 112 repoAt, err := syntax.ParseATURI(record.TargetRepo) 135 113 if err != nil { 136 - return err 114 + return fmt.Errorf("failed to parse ATURI: %w", err) 137 115 } 138 116 139 117 // resolve this aturi to extract the repo record ··· 149 127 150 128 resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 151 129 if err != nil { 152 - return err 130 + return fmt.Errorf("failed to resolver repo: %w", err) 153 131 } 154 132 155 133 repo := resp.Value.Val.(*tangled.Repo) 156 134 157 135 if repo.Knot != h.c.Server.Hostname { 158 - reason := "not this knot" 159 - l.Info("rejecting pull record", "reason", reason) 160 - return fmt.Errorf("rejected pull record: %s", reason) 136 + return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) 161 137 } 162 138 163 139 didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name) 164 140 if err != nil { 165 - return err 141 + return fmt.Errorf("failed to construct relative repo path: %w", err) 166 142 } 167 143 168 144 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 169 145 if err != nil { 170 - return err 146 + return fmt.Errorf("failed to construct absolute repo path: %w", err) 171 147 } 172 148 173 149 gr, err := git.Open(repoPath, record.Source.Branch) 174 150 if err != nil { 175 - return err 151 + return fmt.Errorf("failed to open git repository: %w", err) 176 152 } 177 153 178 154 workflowDir, err := gr.FileTree(ctx, workflow.WorkflowDir) 179 155 if err != nil { 180 - return err 156 + return fmt.Errorf("failed to open workflow directory: %w", err) 181 157 } 182 158 183 - var pipeline workflow.Pipeline 159 + var pipeline workflow.RawPipeline 184 160 for _, e := range workflowDir { 185 161 if !e.IsFile { 186 162 continue ··· 192 168 continue 193 169 } 194 170 195 - wf, err := workflow.FromFile(e.Name, contents) 196 - if err != nil { 197 - // TODO: log here, respond to client that is pushing 198 - h.l.Error("failed to parse workflow", "err", err, "path", fpath) 199 - continue 200 - } 201 - 202 - pipeline = append(pipeline, wf) 171 + pipeline = append(pipeline, workflow.RawWorkflow{ 172 + Name: e.Name, 173 + Contents: contents, 174 + }) 203 175 } 204 176 205 177 trigger := tangled.Pipeline_PullRequestTriggerData{ ··· 221 193 }, 222 194 } 223 195 224 - cp := compiler.Compile(pipeline) 196 + cp := compiler.Compile(compiler.Parse(pipeline)) 225 197 eventJson, err := json.Marshal(cp) 226 198 if err != nil { 227 - return err 199 + return fmt.Errorf("failed to marshal pipeline event: %w", err) 228 200 } 229 201 230 202 // do not run empty pipelines ··· 232 204 return nil 233 205 } 234 206 235 - event := db.Event{ 207 + ev := db.Event{ 236 208 Rkey: TID(), 237 209 Nsid: tangled.PipelineNSID, 238 210 EventJson: string(eventJson), 239 211 } 240 212 241 - return h.db.InsertEvent(event, h.n) 213 + return h.db.InsertEvent(ev, h.n) 242 214 } 243 215 244 216 // duplicated from add collaborator 245 - func (h *Handle) processCollaborator(ctx context.Context, did string, operation string, record tangled.RepoCollaborator) error { 246 - l := log.FromContext(ctx) 247 - l = l.With("handler", "processCollaborator", "did", did) 217 + func (h *Handle) processCollaborator(ctx context.Context, event *models.Event) error { 218 + raw := json.RawMessage(event.Commit.Record) 219 + did := event.Did 248 220 249 - switch operation { 250 - case models.CommitOperationCreate, models.CommitOperationUpdate: 251 - repoAt, err := syntax.ParseATURI(record.Repo) 252 - if err != nil { 253 - return err 254 - } 221 + var record tangled.RepoCollaborator 222 + if err := json.Unmarshal(raw, &record); err != nil { 223 + return fmt.Errorf("failed to unmarshal record: %w", err) 224 + } 255 225 256 - resolver := h.resolver 226 + repoAt, err := syntax.ParseATURI(record.Repo) 227 + if err != nil { 228 + return err 229 + } 257 230 258 - subjectId, err := resolver.ResolveIdent(ctx, record.Subject) 259 - if err != nil || subjectId.Handle.IsInvalidHandle() { 260 - return err 261 - } 231 + resolver := idresolver.DefaultResolver() 262 232 263 - // TODO: fix this for good, we need to fetch the record here unfortunately 264 - // resolve this aturi to extract the repo record 265 - owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String()) 266 - if err != nil || owner.Handle.IsInvalidHandle() { 267 - return fmt.Errorf("failed to resolve handle: %w", err) 268 - } 233 + subjectId, err := resolver.ResolveIdent(ctx, record.Subject) 234 + if err != nil || subjectId.Handle.IsInvalidHandle() { 235 + return err 236 + } 269 237 270 - xrpcc := xrpc.Client{ 271 - Host: owner.PDSEndpoint(), 272 - } 238 + // TODO: fix this for good, we need to fetch the record here unfortunately 239 + // resolve this aturi to extract the repo record 240 + owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String()) 241 + if err != nil || owner.Handle.IsInvalidHandle() { 242 + return fmt.Errorf("failed to resolve handle: %w", err) 243 + } 273 244 274 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 275 - if err != nil { 276 - return err 277 - } 278 - 279 - repo := resp.Value.Val.(*tangled.Repo) 280 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 281 - 282 - // check perms for this user 283 - if ok, err := h.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil { 284 - return fmt.Errorf("insufficient permissions: %w", err) 285 - } 286 - 287 - if err := h.db.AddDid(subjectId.DID.String()); err != nil { 288 - return err 289 - } 290 - h.jc.AddDid(subjectId.DID.String()) 291 - 292 - if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 293 - return err 294 - } 295 - 296 - l.Info("added collaborator from firehose", "subject", record.Subject, "repo", record.Repo) 297 - 298 - return h.fetchAndAddKeys(ctx, subjectId.DID.String()) 299 - 300 - case models.CommitOperationDelete: 301 - repoAt, err := syntax.ParseATURI(record.Repo) 302 - if err != nil { 303 - return err 304 - } 305 - 306 - resolver := h.resolver 307 - 308 - subjectId, err := resolver.ResolveIdent(ctx, record.Subject) 309 - if err != nil || subjectId.Handle.IsInvalidHandle() { 310 - return err 311 - } 312 - 313 - owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String()) 314 - if err != nil || owner.Handle.IsInvalidHandle() { 315 - return fmt.Errorf("failed to resolve handle: %w", err) 316 - } 245 + xrpcc := xrpc.Client{ 246 + Host: owner.PDSEndpoint(), 247 + } 317 248 318 - xrpcc := xrpc.Client{ 319 - Host: owner.PDSEndpoint(), 320 - } 249 + resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 250 + if err != nil { 251 + return err 252 + } 321 253 322 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 323 - if err != nil { 324 - return err 325 - } 254 + repo := resp.Value.Val.(*tangled.Repo) 255 + didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 326 256 327 - repo := resp.Value.Val.(*tangled.Repo) 328 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 257 + // check perms for this user 258 + ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) 259 + if err != nil { 260 + return fmt.Errorf("failed to check permissions: %w", err) 261 + } 262 + if !ok { 263 + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) 264 + } 329 265 330 - if err := h.e.RemoveCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 331 - l.Error("failed to remove collaborator", "error", err) 332 - return fmt.Errorf("failed to remove collaborator: %w", err) 333 - } 266 + if err := h.db.AddDid(subjectId.DID.String()); err != nil { 267 + return err 268 + } 269 + h.jc.AddDid(subjectId.DID.String()) 334 270 335 - l.Info("removed collaborator from firehose", "subject", record.Subject, "repo", record.Repo) 271 + if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 272 + return err 336 273 } 337 274 338 - return nil 275 + return h.fetchAndAddKeys(ctx, subjectId.DID.String()) 339 276 } 340 277 341 278 func (h *Handle) fetchAndAddKeys(ctx context.Context, did string) error { ··· 365 302 return fmt.Errorf("error reading response body: %w", err) 366 303 } 367 304 368 - for _, key := range strings.Split(string(plaintext), "\n") { 305 + for key := range strings.SplitSeq(string(plaintext), "\n") { 369 306 if key == "" { 370 307 continue 371 308 } ··· 382 319 } 383 320 384 321 func (h *Handle) processMessages(ctx context.Context, event *models.Event) error { 385 - did := event.Did 386 322 if event.Kind != models.EventKindCommit { 387 323 return nil 388 324 } ··· 396 332 } 397 333 }() 398 334 399 - raw := json.RawMessage(event.Commit.Record) 400 - 401 335 switch event.Commit.Collection { 402 336 case tangled.PublicKeyNSID: 403 - var record tangled.PublicKey 404 - if err := json.Unmarshal(raw, &record); err != nil { 405 - return fmt.Errorf("failed to unmarshal record: %w", err) 406 - } 407 - if err := h.processPublicKey(ctx, did, event.Commit.Operation, record); err != nil { 408 - return fmt.Errorf("failed to process public key: %w", err) 409 - } 410 - 337 + err = h.processPublicKey(ctx, event) 411 338 case tangled.KnotMemberNSID: 412 - var record tangled.KnotMember 413 - if err := json.Unmarshal(raw, &record); err != nil { 414 - return fmt.Errorf("failed to unmarshal record: %w", err) 415 - } 416 - if err := h.processKnotMember(ctx, did, event.Commit.Operation, record); err != nil { 417 - return fmt.Errorf("failed to process knot member: %w", err) 418 - } 419 - 339 + err = h.processKnotMember(ctx, event) 420 340 case tangled.RepoPullNSID: 421 - var record tangled.RepoPull 422 - if err := json.Unmarshal(raw, &record); err != nil { 423 - return fmt.Errorf("failed to unmarshal record: %w", err) 424 - } 425 - if err := h.processPull(ctx, did, record); err != nil { 426 - return fmt.Errorf("failed to process knot member: %w", err) 427 - } 341 + err = h.processPull(ctx, event) 342 + case tangled.RepoCollaboratorNSID: 343 + err = h.processCollaborator(ctx, event) 344 + } 428 345 429 - case tangled.RepoCollaboratorNSID: 430 - var record tangled.RepoCollaborator 431 - if err := json.Unmarshal(raw, &record); err != nil { 432 - return fmt.Errorf("failed to unmarshal record: %w", err) 433 - } 434 - if err := h.processCollaborator(ctx, did, event.Commit.Operation, record); err != nil { 435 - return fmt.Errorf("failed to process collaborator: %w", err) 436 - } 346 + if err != nil { 347 + h.l.Debug("failed to process event", "nsid", event.Commit.Collection, "err", err) 437 348 } 438 349 439 - return err 350 + return nil 440 351 }
+15 -37
knotserver/internal.go
··· 47 47 } 48 48 49 49 w.WriteHeader(http.StatusNoContent) 50 - return 51 50 } 52 51 53 52 func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) { ··· 63 62 data = append(data, j) 64 63 } 65 64 writeJSON(w, data) 66 - return 67 65 } 68 66 69 67 type PushOptions struct { ··· 200 198 return err 201 199 } 202 200 203 - pipelineParseErrors := []string{} 204 - 205 - var pipeline workflow.Pipeline 201 + var pipeline workflow.RawPipeline 206 202 for _, e := range workflowDir { 207 203 if !e.IsFile { 208 204 continue ··· 214 210 continue 215 211 } 216 212 217 - wf, err := workflow.FromFile(e.Name, contents) 218 - if err != nil { 219 - h.l.Error("failed to parse workflow", "err", err, "path", fpath) 220 - pipelineParseErrors = append(pipelineParseErrors, fmt.Sprintf("- at %s: %s\n", fpath, err)) 221 - continue 222 - } 223 - 224 - pipeline = append(pipeline, wf) 213 + pipeline = append(pipeline, workflow.RawWorkflow{ 214 + Name: e.Name, 215 + Contents: contents, 216 + }) 225 217 } 226 218 227 219 trigger := tangled.Pipeline_PushTriggerData{ ··· 242 234 }, 243 235 } 244 236 245 - cp := compiler.Compile(pipeline) 237 + cp := compiler.Compile(compiler.Parse(pipeline)) 246 238 eventJson, err := json.Marshal(cp) 247 239 if err != nil { 248 240 return err 249 241 } 250 242 243 + for _, e := range compiler.Diagnostics.Errors { 244 + *clientMsgs = append(*clientMsgs, e.String()) 245 + } 246 + 251 247 if pushOptions.verboseCi { 252 - hasDiagnostics := false 253 - if len(pipelineParseErrors) > 0 { 254 - hasDiagnostics = true 255 - *clientMsgs = append(*clientMsgs, "error: failed to parse workflow(s):") 256 - for _, error := range pipelineParseErrors { 257 - *clientMsgs = append(*clientMsgs, error) 258 - } 248 + if compiler.Diagnostics.IsEmpty() { 249 + *clientMsgs = append(*clientMsgs, "success: pipeline compiled with no diagnostics") 259 250 } 260 - if len(compiler.Diagnostics.Errors) > 0 { 261 - hasDiagnostics = true 262 - *clientMsgs = append(*clientMsgs, "error(s) on pipeline:") 263 - for _, error := range compiler.Diagnostics.Errors { 264 - *clientMsgs = append(*clientMsgs, fmt.Sprintf("- %s:", error)) 265 - } 266 - } 267 - if len(compiler.Diagnostics.Warnings) > 0 { 268 - hasDiagnostics = true 269 - *clientMsgs = append(*clientMsgs, "warning(s) on pipeline:") 270 - for _, warning := range compiler.Diagnostics.Warnings { 271 - *clientMsgs = append(*clientMsgs, fmt.Sprintf("- at %s: %s: %s", warning.Path, warning.Type, warning.Reason)) 272 - } 273 - } 274 - if !hasDiagnostics { 275 - *clientMsgs = append(*clientMsgs, "success: pipeline compiled with no diagnostics") 251 + 252 + for _, w := range compiler.Diagnostics.Warnings { 253 + *clientMsgs = append(*clientMsgs, w.String()) 276 254 } 277 255 } 278 256
-53
knotserver/middleware.go
··· 1 - package knotserver 2 - 3 - import ( 4 - "crypto/hmac" 5 - "crypto/sha256" 6 - "encoding/hex" 7 - "net/http" 8 - "time" 9 - ) 10 - 11 - func (h *Handle) VerifySignature(next http.Handler) http.Handler { 12 - if h.c.Server.Dev { 13 - return next 14 - } 15 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 - signature := r.Header.Get("X-Signature") 17 - if signature == "" || !h.verifyHMAC(signature, r) { 18 - writeError(w, "signature verification failed", http.StatusForbidden) 19 - return 20 - } 21 - next.ServeHTTP(w, r) 22 - }) 23 - } 24 - 25 - func (h *Handle) verifyHMAC(signature string, r *http.Request) bool { 26 - secret := h.c.Server.Secret 27 - timestamp := r.Header.Get("X-Timestamp") 28 - if timestamp == "" { 29 - return false 30 - } 31 - 32 - // Verify that the timestamp is not older than a minute 33 - reqTime, err := time.Parse(time.RFC3339, timestamp) 34 - if err != nil { 35 - return false 36 - } 37 - if time.Since(reqTime) > time.Minute { 38 - return false 39 - } 40 - 41 - message := r.Method + r.URL.Path + timestamp 42 - 43 - mac := hmac.New(sha256.New, []byte(secret)) 44 - mac.Write([]byte(message)) 45 - expectedMAC := mac.Sum(nil) 46 - 47 - signatureBytes, err := hex.DecodeString(signature) 48 - if err != nil { 49 - return false 50 - } 51 - 52 - return hmac.Equal(signatureBytes, expectedMAC) 53 - }
+138 -1171
knotserver/routes.go
··· 1 1 package knotserver 2 2 3 3 import ( 4 - "compress/gzip" 5 4 "context" 6 - "crypto/sha256" 7 - "encoding/json" 8 - "errors" 9 5 "fmt" 10 - "log" 6 + "log/slog" 11 7 "net/http" 12 - "net/url" 13 - "os" 14 - "path/filepath" 15 - "strconv" 16 - "strings" 17 - "sync" 18 - "time" 8 + "runtime/debug" 19 9 20 - securejoin "github.com/cyphar/filepath-securejoin" 21 - "github.com/gliderlabs/ssh" 22 10 "github.com/go-chi/chi/v5" 23 - "github.com/go-git/go-git/v5/plumbing" 24 - "github.com/go-git/go-git/v5/plumbing/object" 25 - "tangled.sh/tangled.sh/core/hook" 11 + "tangled.sh/tangled.sh/core/idresolver" 12 + "tangled.sh/tangled.sh/core/jetstream" 13 + "tangled.sh/tangled.sh/core/knotserver/config" 26 14 "tangled.sh/tangled.sh/core/knotserver/db" 27 - "tangled.sh/tangled.sh/core/knotserver/git" 28 - "tangled.sh/tangled.sh/core/patchutil" 15 + "tangled.sh/tangled.sh/core/knotserver/xrpc" 16 + tlog "tangled.sh/tangled.sh/core/log" 17 + "tangled.sh/tangled.sh/core/notifier" 29 18 "tangled.sh/tangled.sh/core/rbac" 30 - "tangled.sh/tangled.sh/core/types" 19 + "tangled.sh/tangled.sh/core/xrpc/serviceauth" 31 20 ) 32 21 33 - func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 34 - w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 22 + type Handle struct { 23 + c *config.Config 24 + db *db.DB 25 + jc *jetstream.JetstreamClient 26 + e *rbac.Enforcer 27 + l *slog.Logger 28 + n *notifier.Notifier 29 + resolver *idresolver.Resolver 35 30 } 36 31 37 - func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) { 38 - w.Header().Set("Content-Type", "application/json") 32 + func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 33 + r := chi.NewRouter() 39 34 40 - capabilities := map[string]any{ 41 - "pull_requests": map[string]any{ 42 - "format_patch": true, 43 - "patch_submissions": true, 44 - "branch_submissions": true, 45 - "fork_submissions": true, 46 - }, 35 + h := Handle{ 36 + c: c, 37 + db: db, 38 + e: e, 39 + l: l, 40 + jc: jc, 41 + n: n, 42 + resolver: idresolver.DefaultResolver(), 47 43 } 48 44 49 - jsonData, err := json.Marshal(capabilities) 45 + err := e.AddKnot(rbac.ThisServer) 50 46 if err != nil { 51 - http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError) 52 - return 53 - } 54 - 55 - w.Write(jsonData) 56 - } 57 - 58 - func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 59 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 60 - l := h.l.With("path", path, "handler", "RepoIndex") 61 - ref := chi.URLParam(r, "ref") 62 - ref, _ = url.PathUnescape(ref) 63 - 64 - gr, err := git.Open(path, ref) 65 - if err != nil { 66 - plain, err2 := git.PlainOpen(path) 67 - if err2 != nil { 68 - l.Error("opening repo", "error", err2.Error()) 69 - notFound(w) 70 - return 71 - } 72 - branches, _ := plain.Branches() 73 - 74 - log.Println(err) 75 - 76 - if errors.Is(err, plumbing.ErrReferenceNotFound) { 77 - resp := types.RepoIndexResponse{ 78 - IsEmpty: true, 79 - Branches: branches, 80 - } 81 - writeJSON(w, resp) 82 - return 83 - } else { 84 - l.Error("opening repo", "error", err.Error()) 85 - notFound(w) 86 - return 87 - } 88 - } 89 - 90 - var ( 91 - commits []*object.Commit 92 - total int 93 - branches []types.Branch 94 - files []types.NiceTree 95 - tags []object.Tag 96 - ) 97 - 98 - var wg sync.WaitGroup 99 - errorsCh := make(chan error, 5) 100 - 101 - wg.Add(1) 102 - go func() { 103 - defer wg.Done() 104 - cs, err := gr.Commits(0, 60) 105 - if err != nil { 106 - errorsCh <- fmt.Errorf("commits: %w", err) 107 - return 108 - } 109 - commits = cs 110 - }() 111 - 112 - wg.Add(1) 113 - go func() { 114 - defer wg.Done() 115 - t, err := gr.TotalCommits() 116 - if err != nil { 117 - errorsCh <- fmt.Errorf("calculating total: %w", err) 118 - return 119 - } 120 - total = t 121 - }() 122 - 123 - wg.Add(1) 124 - go func() { 125 - defer wg.Done() 126 - bs, err := gr.Branches() 127 - if err != nil { 128 - errorsCh <- fmt.Errorf("fetching branches: %w", err) 129 - return 130 - } 131 - branches = bs 132 - }() 133 - 134 - wg.Add(1) 135 - go func() { 136 - defer wg.Done() 137 - ts, err := gr.Tags() 138 - if err != nil { 139 - errorsCh <- fmt.Errorf("fetching tags: %w", err) 140 - return 141 - } 142 - tags = ts 143 - }() 144 - 145 - wg.Add(1) 146 - go func() { 147 - defer wg.Done() 148 - fs, err := gr.FileTree(r.Context(), "") 149 - if err != nil { 150 - errorsCh <- fmt.Errorf("fetching filetree: %w", err) 151 - return 152 - } 153 - files = fs 154 - }() 155 - 156 - wg.Wait() 157 - close(errorsCh) 158 - 159 - // show any errors 160 - for err := range errorsCh { 161 - l.Error("loading repo", "error", err.Error()) 162 - writeError(w, err.Error(), http.StatusInternalServerError) 163 - return 164 - } 165 - 166 - rtags := []*types.TagReference{} 167 - for _, tag := range tags { 168 - var target *object.Tag 169 - if tag.Target != plumbing.ZeroHash { 170 - target = &tag 171 - } 172 - tr := types.TagReference{ 173 - Tag: target, 174 - } 175 - 176 - tr.Reference = types.Reference{ 177 - Name: tag.Name, 178 - Hash: tag.Hash.String(), 179 - } 180 - 181 - if tag.Message != "" { 182 - tr.Message = tag.Message 183 - } 184 - 185 - rtags = append(rtags, &tr) 186 - } 187 - 188 - var readmeContent string 189 - var readmeFile string 190 - for _, readme := range h.c.Repo.Readme { 191 - content, _ := gr.FileContent(readme) 192 - if len(content) > 0 { 193 - readmeContent = string(content) 194 - readmeFile = readme 195 - } 47 + return nil, fmt.Errorf("failed to setup enforcer: %w", err) 196 48 } 197 49 198 - if ref == "" { 199 - mainBranch, err := gr.FindMainBranch() 200 - if err != nil { 201 - writeError(w, err.Error(), http.StatusInternalServerError) 202 - l.Error("finding main branch", "error", err.Error()) 203 - return 204 - } 205 - ref = mainBranch 206 - } 207 - 208 - resp := types.RepoIndexResponse{ 209 - IsEmpty: false, 210 - Ref: ref, 211 - Commits: commits, 212 - Description: getDescription(path), 213 - Readme: readmeContent, 214 - ReadmeFileName: readmeFile, 215 - Files: files, 216 - Branches: branches, 217 - Tags: rtags, 218 - TotalCommits: total, 219 - } 220 - 221 - writeJSON(w, resp) 222 - return 223 - } 224 - 225 - func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 226 - treePath := chi.URLParam(r, "*") 227 - ref := chi.URLParam(r, "ref") 228 - ref, _ = url.PathUnescape(ref) 229 - 230 - l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 231 - 232 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 233 - gr, err := git.Open(path, ref) 234 - if err != nil { 235 - notFound(w) 236 - return 50 + // configure owner 51 + if err = h.configureOwner(); err != nil { 52 + return nil, err 237 53 } 54 + h.l.Info("owner set", "did", h.c.Server.Owner) 55 + h.jc.AddDid(h.c.Server.Owner) 238 56 239 - files, err := gr.FileTree(r.Context(), treePath) 57 + // configure known-dids in jetstream consumer 58 + dids, err := h.db.GetAllDids() 240 59 if err != nil { 241 - writeError(w, err.Error(), http.StatusInternalServerError) 242 - l.Error("file tree", "error", err.Error()) 243 - return 60 + return nil, fmt.Errorf("failed to get all dids: %w", err) 244 61 } 245 - 246 - resp := types.RepoTreeResponse{ 247 - Ref: ref, 248 - Parent: treePath, 249 - Description: getDescription(path), 250 - DotDot: filepath.Dir(treePath), 251 - Files: files, 62 + for _, d := range dids { 63 + jc.AddDid(d) 252 64 } 253 65 254 - writeJSON(w, resp) 255 - return 256 - } 257 - 258 - func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 259 - treePath := chi.URLParam(r, "*") 260 - ref := chi.URLParam(r, "ref") 261 - ref, _ = url.PathUnescape(ref) 262 - 263 - l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 264 - 265 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 266 - gr, err := git.Open(path, ref) 66 + err = h.jc.StartJetstream(ctx, h.processMessages) 267 67 if err != nil { 268 - notFound(w) 269 - return 68 + return nil, fmt.Errorf("failed to start jetstream: %w", err) 270 69 } 271 70 272 - contents, err := gr.RawContent(treePath) 273 - if err != nil { 274 - writeError(w, err.Error(), http.StatusBadRequest) 275 - l.Error("file content", "error", err.Error()) 276 - return 277 - } 71 + r.Get("/", h.Index) 72 + r.Get("/capabilities", h.Capabilities) 73 + r.Get("/version", h.Version) 74 + r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { 75 + w.Write([]byte(h.c.Server.Owner)) 76 + }) 77 + r.Route("/{did}", func(r chi.Router) { 78 + // Repo routes 79 + r.Route("/{name}", func(r chi.Router) { 278 80 279 - mimeType := http.DetectContentType(contents) 81 + r.Route("/languages", func(r chi.Router) { 82 + r.Get("/", h.RepoLanguages) 83 + r.Get("/{ref}", h.RepoLanguages) 84 + }) 280 85 281 - // exception for svg 282 - if filepath.Ext(treePath) == ".svg" { 283 - mimeType = "image/svg+xml" 284 - } 86 + r.Get("/", h.RepoIndex) 87 + r.Get("/info/refs", h.InfoRefs) 88 + r.Post("/git-upload-pack", h.UploadPack) 89 + r.Post("/git-receive-pack", h.ReceivePack) 90 + r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 285 91 286 - // allow image, video, and text/plain files to be served directly 287 - switch { 288 - case strings.HasPrefix(mimeType, "image/"): 289 - // allowed 290 - case strings.HasPrefix(mimeType, "video/"): 291 - // allowed 292 - case strings.HasPrefix(mimeType, "text/plain"): 293 - // allowed 294 - default: 295 - l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 296 - writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 297 - return 298 - } 92 + r.Route("/tree/{ref}", func(r chi.Router) { 93 + r.Get("/", h.RepoIndex) 94 + r.Get("/*", h.RepoTree) 95 + }) 299 96 300 - w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 301 - w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 302 - w.Header().Set("Content-Type", mimeType) 303 - w.Write(contents) 304 - } 97 + r.Route("/blob/{ref}", func(r chi.Router) { 98 + r.Get("/*", h.Blob) 99 + }) 305 100 306 - func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 307 - treePath := chi.URLParam(r, "*") 308 - ref := chi.URLParam(r, "ref") 309 - ref, _ = url.PathUnescape(ref) 101 + r.Route("/raw/{ref}", func(r chi.Router) { 102 + r.Get("/*", h.BlobRaw) 103 + }) 310 104 311 - l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 312 - 313 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 314 - gr, err := git.Open(path, ref) 315 - if err != nil { 316 - notFound(w) 317 - return 318 - } 105 + r.Get("/log/{ref}", h.Log) 106 + r.Get("/archive/{file}", h.Archive) 107 + r.Get("/commit/{ref}", h.Diff) 108 + r.Get("/tags", h.Tags) 109 + r.Route("/branches", func(r chi.Router) { 110 + r.Get("/", h.Branches) 111 + r.Get("/{branch}", h.Branch) 112 + r.Get("/default", h.DefaultBranch) 113 + }) 114 + }) 115 + }) 319 116 320 - var isBinaryFile bool = false 321 - contents, err := gr.FileContent(treePath) 322 - if errors.Is(err, git.ErrBinaryFile) { 323 - isBinaryFile = true 324 - } else if errors.Is(err, object.ErrFileNotFound) { 325 - notFound(w) 326 - return 327 - } else if err != nil { 328 - writeError(w, err.Error(), http.StatusInternalServerError) 329 - return 330 - } 117 + // xrpc apis 118 + r.Mount("/xrpc", h.XrpcRouter()) 331 119 332 - bytes := []byte(contents) 333 - // safe := string(sanitize(bytes)) 334 - sizeHint := len(bytes) 120 + // Socket that streams git oplogs 121 + r.Get("/events", h.Events) 335 122 336 - resp := types.RepoBlobResponse{ 337 - Ref: ref, 338 - Contents: string(bytes), 339 - Path: treePath, 340 - IsBinary: isBinaryFile, 341 - SizeHint: uint64(sizeHint), 342 - } 123 + // All public keys on the knot. 124 + r.Get("/keys", h.Keys) 343 125 344 - h.showFile(resp, w, l) 126 + return r, nil 345 127 } 346 128 347 - func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 348 - name := chi.URLParam(r, "name") 349 - file := chi.URLParam(r, "file") 350 - 351 - l := h.l.With("handler", "Archive", "name", name, "file", file) 352 - 353 - // TODO: extend this to add more files compression (e.g.: xz) 354 - if !strings.HasSuffix(file, ".tar.gz") { 355 - notFound(w) 356 - return 357 - } 358 - 359 - ref := strings.TrimSuffix(file, ".tar.gz") 360 - 361 - unescapedRef, err := url.PathUnescape(ref) 362 - if err != nil { 363 - notFound(w) 364 - return 365 - } 129 + func (h *Handle) XrpcRouter() http.Handler { 130 + logger := tlog.New("knots") 366 131 367 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 132 + serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 368 133 369 - // This allows the browser to use a proper name for the file when 370 - // downloading 371 - filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 372 - setContentDisposition(w, filename) 373 - setGZipMIME(w) 374 - 375 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 376 - gr, err := git.Open(path, unescapedRef) 377 - if err != nil { 378 - notFound(w) 379 - return 380 - } 381 - 382 - gw := gzip.NewWriter(w) 383 - defer gw.Close() 384 - 385 - prefix := fmt.Sprintf("%s-%s", name, safeRefFilename) 386 - err = gr.WriteTar(gw, prefix) 387 - if err != nil { 388 - // once we start writing to the body we can't report error anymore 389 - // so we are only left with printing the error. 390 - l.Error("writing tar file", "error", err.Error()) 391 - return 392 - } 393 - 394 - err = gw.Flush() 395 - if err != nil { 396 - // once we start writing to the body we can't report error anymore 397 - // so we are only left with printing the error. 398 - l.Error("flushing?", "error", err.Error()) 399 - return 134 + xrpc := &xrpc.Xrpc{ 135 + Config: h.c, 136 + Db: h.db, 137 + Ingester: h.jc, 138 + Enforcer: h.e, 139 + Logger: logger, 140 + Notifier: h.n, 141 + Resolver: h.resolver, 142 + ServiceAuth: serviceAuth, 400 143 } 144 + return xrpc.Router() 401 145 } 402 146 403 - func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 404 - ref := chi.URLParam(r, "ref") 405 - ref, _ = url.PathUnescape(ref) 147 + // version is set during build time. 148 + var version string 406 149 407 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 408 - 409 - l := h.l.With("handler", "Log", "ref", ref, "path", path) 410 - 411 - gr, err := git.Open(path, ref) 412 - if err != nil { 413 - notFound(w) 414 - return 415 - } 416 - 417 - // Get page parameters 418 - page := 1 419 - pageSize := 30 420 - 421 - if pageParam := r.URL.Query().Get("page"); pageParam != "" { 422 - if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 423 - page = p 150 + func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { 151 + if version == "" { 152 + info, ok := debug.ReadBuildInfo() 153 + if !ok { 154 + http.Error(w, "failed to read build info", http.StatusInternalServerError) 155 + return 424 156 } 425 - } 426 157 427 - if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 428 - if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 429 - pageSize = ps 158 + var modVer string 159 + for _, mod := range info.Deps { 160 + if mod.Path == "tangled.sh/tangled.sh/knotserver" { 161 + version = mod.Version 162 + break 163 + } 430 164 } 431 - } 432 165 433 - // convert to offset/limit 434 - offset := (page - 1) * pageSize 435 - limit := pageSize 436 - 437 - commits, err := gr.Commits(offset, limit) 438 - if err != nil { 439 - writeError(w, err.Error(), http.StatusInternalServerError) 440 - l.Error("fetching commits", "error", err.Error()) 441 - return 442 - } 443 - 444 - total := len(commits) 445 - 446 - resp := types.RepoLogResponse{ 447 - Commits: commits, 448 - Ref: ref, 449 - Description: getDescription(path), 450 - Log: true, 451 - Total: total, 452 - Page: page, 453 - PerPage: pageSize, 454 - } 455 - 456 - writeJSON(w, resp) 457 - return 458 - } 459 - 460 - func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 461 - ref := chi.URLParam(r, "ref") 462 - ref, _ = url.PathUnescape(ref) 463 - 464 - l := h.l.With("handler", "Diff", "ref", ref) 465 - 466 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 467 - gr, err := git.Open(path, ref) 468 - if err != nil { 469 - notFound(w) 470 - return 471 - } 472 - 473 - diff, err := gr.Diff() 474 - if err != nil { 475 - writeError(w, err.Error(), http.StatusInternalServerError) 476 - l.Error("getting diff", "error", err.Error()) 477 - return 478 - } 479 - 480 - resp := types.RepoCommitResponse{ 481 - Ref: ref, 482 - Diff: diff, 483 - } 484 - 485 - writeJSON(w, resp) 486 - return 487 - } 488 - 489 - func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 490 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 491 - l := h.l.With("handler", "Refs") 492 - 493 - gr, err := git.Open(path, "") 494 - if err != nil { 495 - notFound(w) 496 - return 497 - } 498 - 499 - tags, err := gr.Tags() 500 - if err != nil { 501 - // Non-fatal, we *should* have at least one branch to show. 502 - l.Warn("getting tags", "error", err.Error()) 503 - } 504 - 505 - rtags := []*types.TagReference{} 506 - for _, tag := range tags { 507 - var target *object.Tag 508 - if tag.Target != plumbing.ZeroHash { 509 - target = &tag 166 + if modVer == "" { 167 + version = "unknown" 510 168 } 511 - tr := types.TagReference{ 512 - Tag: target, 513 - } 514 - 515 - tr.Reference = types.Reference{ 516 - Name: tag.Name, 517 - Hash: tag.Hash.String(), 518 - } 519 - 520 - if tag.Message != "" { 521 - tr.Message = tag.Message 522 - } 523 - 524 - rtags = append(rtags, &tr) 525 169 } 526 170 527 - resp := types.RepoTagsResponse{ 528 - Tags: rtags, 529 - } 530 - 531 - writeJSON(w, resp) 532 - return 171 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 172 + fmt.Fprintf(w, "knotserver/%s", version) 533 173 } 534 174 535 - func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 536 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 537 - 538 - gr, err := git.PlainOpen(path) 539 - if err != nil { 540 - notFound(w) 541 - return 542 - } 543 - 544 - branches, _ := gr.Branches() 175 + func (h *Handle) configureOwner() error { 176 + cfgOwner := h.c.Server.Owner 545 177 546 - resp := types.RepoBranchesResponse{ 547 - Branches: branches, 548 - } 178 + rbacDomain := "thisserver" 549 179 550 - writeJSON(w, resp) 551 - return 552 - } 553 - 554 - func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 555 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 556 - branchName := chi.URLParam(r, "branch") 557 - branchName, _ = url.PathUnescape(branchName) 558 - 559 - l := h.l.With("handler", "Branch") 560 - 561 - gr, err := git.PlainOpen(path) 180 + existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 562 181 if err != nil { 563 - notFound(w) 564 - return 565 - } 566 - 567 - ref, err := gr.Branch(branchName) 568 - if err != nil { 569 - l.Error("getting branch", "error", err.Error()) 570 - writeError(w, err.Error(), http.StatusInternalServerError) 571 - return 182 + return err 572 183 } 573 184 574 - commit, err := gr.Commit(ref.Hash()) 575 - if err != nil { 576 - l.Error("getting commit object", "error", err.Error()) 577 - writeError(w, err.Error(), http.StatusInternalServerError) 578 - return 579 - } 185 + switch len(existing) { 186 + case 0: 187 + // no owner configured, continue 188 + case 1: 189 + // find existing owner 190 + existingOwner := existing[0] 580 191 581 - defaultBranch, err := gr.FindMainBranch() 582 - isDefault := false 583 - if err != nil { 584 - l.Error("getting default branch", "error", err.Error()) 585 - // do not quit though 586 - } else if defaultBranch == branchName { 587 - isDefault = true 588 - } 589 - 590 - resp := types.RepoBranchResponse{ 591 - Branch: types.Branch{ 592 - Reference: types.Reference{ 593 - Name: ref.Name().Short(), 594 - Hash: ref.Hash().String(), 595 - }, 596 - Commit: commit, 597 - IsDefault: isDefault, 598 - }, 599 - } 600 - 601 - writeJSON(w, resp) 602 - return 603 - } 604 - 605 - func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 606 - l := h.l.With("handler", "Keys") 607 - 608 - switch r.Method { 609 - case http.MethodGet: 610 - keys, err := h.db.GetAllPublicKeys() 611 - if err != nil { 612 - writeError(w, err.Error(), http.StatusInternalServerError) 613 - l.Error("getting public keys", "error", err.Error()) 614 - return 192 + // no ownership change, this is okay 193 + if existingOwner == h.c.Server.Owner { 194 + break 615 195 } 616 196 617 - data := make([]map[string]any, 0) 618 - for _, key := range keys { 619 - j := key.JSON() 620 - data = append(data, j) 621 - } 622 - writeJSON(w, data) 623 - return 624 - 625 - case http.MethodPut: 626 - pk := db.PublicKey{} 627 - if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 628 - writeError(w, "invalid request body", http.StatusBadRequest) 629 - return 630 - } 631 - 632 - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 633 - if err != nil { 634 - writeError(w, "invalid pubkey", http.StatusBadRequest) 635 - } 636 - 637 - if err := h.db.AddPublicKey(pk); err != nil { 638 - writeError(w, err.Error(), http.StatusInternalServerError) 639 - l.Error("adding public key", "error", err.Error()) 640 - return 641 - } 642 - 643 - w.WriteHeader(http.StatusNoContent) 644 - return 645 - } 646 - } 647 - 648 - func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 649 - l := h.l.With("handler", "RepoForkSync") 650 - 651 - data := struct { 652 - Did string `json:"did"` 653 - Source string `json:"source"` 654 - Name string `json:"name,omitempty"` 655 - HiddenRef string `json:"hiddenref"` 656 - }{} 657 - 658 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 659 - writeError(w, "invalid request body", http.StatusBadRequest) 660 - return 661 - } 662 - 663 - did := data.Did 664 - source := data.Source 665 - 666 - if did == "" || source == "" { 667 - l.Error("invalid request body, empty did or name") 668 - w.WriteHeader(http.StatusBadRequest) 669 - return 670 - } 671 - 672 - var name string 673 - if data.Name != "" { 674 - name = data.Name 675 - } else { 676 - name = filepath.Base(source) 677 - } 678 - 679 - branch := chi.URLParam(r, "branch") 680 - branch, _ = url.PathUnescape(branch) 681 - 682 - relativeRepoPath := filepath.Join(did, name) 683 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 684 - 685 - gr, err := git.PlainOpen(repoPath) 686 - if err != nil { 687 - log.Println(err) 688 - notFound(w) 689 - return 690 - } 691 - 692 - forkCommit, err := gr.ResolveRevision(branch) 693 - if err != nil { 694 - l.Error("error resolving ref revision", "msg", err.Error()) 695 - writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest) 696 - return 697 - } 698 - 699 - sourceCommit, err := gr.ResolveRevision(data.HiddenRef) 700 - if err != nil { 701 - l.Error("error resolving hidden ref revision", "msg", err.Error()) 702 - writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest) 703 - return 704 - } 705 - 706 - status := types.UpToDate 707 - if forkCommit.Hash.String() != sourceCommit.Hash.String() { 708 - isAncestor, err := forkCommit.IsAncestor(sourceCommit) 197 + // remove existing owner 198 + err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) 709 199 if err != nil { 710 - log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 711 - return 712 - } 713 - 714 - if isAncestor { 715 - status = types.FastForwardable 716 - } else { 717 - status = types.Conflict 718 - } 719 - } 720 - 721 - w.Header().Set("Content-Type", "application/json") 722 - json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 723 - } 724 - 725 - func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) { 726 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 727 - ref := chi.URLParam(r, "ref") 728 - ref, _ = url.PathUnescape(ref) 729 - 730 - l := h.l.With("handler", "RepoLanguages") 731 - 732 - gr, err := git.Open(repoPath, ref) 733 - if err != nil { 734 - l.Error("opening repo", "error", err.Error()) 735 - notFound(w) 736 - return 737 - } 738 - 739 - ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) 740 - defer cancel() 741 - 742 - sizes, err := gr.AnalyzeLanguages(ctx) 743 - if err != nil { 744 - l.Error("failed to analyze languages", "error", err.Error()) 745 - writeError(w, err.Error(), http.StatusNoContent) 746 - return 747 - } 748 - 749 - resp := types.RepoLanguageResponse{Languages: sizes} 750 - 751 - writeJSON(w, resp) 752 - } 753 - 754 - func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 755 - l := h.l.With("handler", "RepoForkSync") 756 - 757 - data := struct { 758 - Did string `json:"did"` 759 - Source string `json:"source"` 760 - Name string `json:"name,omitempty"` 761 - }{} 762 - 763 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 764 - writeError(w, "invalid request body", http.StatusBadRequest) 765 - return 766 - } 767 - 768 - did := data.Did 769 - source := data.Source 770 - 771 - if did == "" || source == "" { 772 - l.Error("invalid request body, empty did or name") 773 - w.WriteHeader(http.StatusBadRequest) 774 - return 775 - } 776 - 777 - var name string 778 - if data.Name != "" { 779 - name = data.Name 780 - } else { 781 - name = filepath.Base(source) 782 - } 783 - 784 - branch := chi.URLParam(r, "branch") 785 - branch, _ = url.PathUnescape(branch) 786 - 787 - relativeRepoPath := filepath.Join(did, name) 788 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 789 - 790 - gr, err := git.PlainOpen(repoPath) 791 - if err != nil { 792 - log.Println(err) 793 - notFound(w) 794 - return 795 - } 796 - 797 - err = gr.Sync(branch) 798 - if err != nil { 799 - l.Error("error syncing repo fork", "error", err.Error()) 800 - writeError(w, err.Error(), http.StatusInternalServerError) 801 - return 802 - } 803 - 804 - w.WriteHeader(http.StatusNoContent) 805 - } 806 - 807 - func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 808 - l := h.l.With("handler", "RepoFork") 809 - 810 - data := struct { 811 - Did string `json:"did"` 812 - Source string `json:"source"` 813 - Name string `json:"name,omitempty"` 814 - }{} 815 - 816 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 817 - writeError(w, "invalid request body", http.StatusBadRequest) 818 - return 819 - } 820 - 821 - did := data.Did 822 - source := data.Source 823 - 824 - if did == "" || source == "" { 825 - l.Error("invalid request body, empty did or name") 826 - w.WriteHeader(http.StatusBadRequest) 827 - return 828 - } 829 - 830 - var name string 831 - if data.Name != "" { 832 - name = data.Name 833 - } else { 834 - name = filepath.Base(source) 835 - } 836 - 837 - relativeRepoPath := filepath.Join(did, name) 838 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 839 - 840 - err := git.Fork(repoPath, source) 841 - if err != nil { 842 - l.Error("forking repo", "error", err.Error()) 843 - writeError(w, err.Error(), http.StatusInternalServerError) 844 - return 845 - } 846 - 847 - // add perms for this user to access the repo 848 - err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath) 849 - if err != nil { 850 - l.Error("adding repo permissions", "error", err.Error()) 851 - writeError(w, err.Error(), http.StatusInternalServerError) 852 - return 853 - } 854 - 855 - hook.SetupRepo( 856 - hook.Config( 857 - hook.WithScanPath(h.c.Repo.ScanPath), 858 - hook.WithInternalApi(h.c.Server.InternalListenAddr), 859 - ), 860 - repoPath, 861 - ) 862 - 863 - w.WriteHeader(http.StatusNoContent) 864 - } 865 - 866 - func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 867 - l := h.l.With("handler", "RemoveRepo") 868 - 869 - data := struct { 870 - Did string `json:"did"` 871 - Name string `json:"name"` 872 - }{} 873 - 874 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 875 - writeError(w, "invalid request body", http.StatusBadRequest) 876 - return 877 - } 878 - 879 - did := data.Did 880 - name := data.Name 881 - 882 - if did == "" || name == "" { 883 - l.Error("invalid request body, empty did or name") 884 - w.WriteHeader(http.StatusBadRequest) 885 - return 886 - } 887 - 888 - relativeRepoPath := filepath.Join(did, name) 889 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 890 - err := os.RemoveAll(repoPath) 891 - if err != nil { 892 - l.Error("removing repo", "error", err.Error()) 893 - writeError(w, err.Error(), http.StatusInternalServerError) 894 - return 895 - } 896 - 897 - w.WriteHeader(http.StatusNoContent) 898 - 899 - } 900 - func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 901 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 902 - 903 - data := types.MergeRequest{} 904 - 905 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 906 - writeError(w, err.Error(), http.StatusBadRequest) 907 - h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 908 - return 909 - } 910 - 911 - mo := &git.MergeOptions{ 912 - AuthorName: data.AuthorName, 913 - AuthorEmail: data.AuthorEmail, 914 - CommitBody: data.CommitBody, 915 - CommitMessage: data.CommitMessage, 916 - } 917 - 918 - patch := data.Patch 919 - branch := data.Branch 920 - gr, err := git.Open(path, branch) 921 - if err != nil { 922 - notFound(w) 923 - return 924 - } 925 - 926 - mo.FormatPatch = patchutil.IsFormatPatch(patch) 927 - 928 - if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 929 - var mergeErr *git.ErrMerge 930 - if errors.As(err, &mergeErr) { 931 - conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 932 - for i, conflict := range mergeErr.Conflicts { 933 - conflicts[i] = types.ConflictInfo{ 934 - Filename: conflict.Filename, 935 - Reason: conflict.Reason, 936 - } 937 - } 938 - response := types.MergeCheckResponse{ 939 - IsConflicted: true, 940 - Conflicts: conflicts, 941 - Message: mergeErr.Message, 942 - } 943 - writeConflict(w, response) 944 - h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 945 - } else { 946 - writeError(w, err.Error(), http.StatusBadRequest) 947 - h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 948 - } 949 - return 950 - } 951 - 952 - w.WriteHeader(http.StatusOK) 953 - } 954 - 955 - func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 956 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 957 - 958 - var data struct { 959 - Patch string `json:"patch"` 960 - Branch string `json:"branch"` 961 - } 962 - 963 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 964 - writeError(w, err.Error(), http.StatusBadRequest) 965 - h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 966 - return 967 - } 968 - 969 - patch := data.Patch 970 - branch := data.Branch 971 - gr, err := git.Open(path, branch) 972 - if err != nil { 973 - notFound(w) 974 - return 975 - } 976 - 977 - err = gr.MergeCheck([]byte(patch), branch) 978 - if err == nil { 979 - response := types.MergeCheckResponse{ 980 - IsConflicted: false, 981 - } 982 - writeJSON(w, response) 983 - return 984 - } 985 - 986 - var mergeErr *git.ErrMerge 987 - if errors.As(err, &mergeErr) { 988 - conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 989 - for i, conflict := range mergeErr.Conflicts { 990 - conflicts[i] = types.ConflictInfo{ 991 - Filename: conflict.Filename, 992 - Reason: conflict.Reason, 993 - } 994 - } 995 - response := types.MergeCheckResponse{ 996 - IsConflicted: true, 997 - Conflicts: conflicts, 998 - Message: mergeErr.Message, 200 + return nil 999 201 } 1000 - writeConflict(w, response) 1001 - h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1002 - return 1003 - } 1004 - writeError(w, err.Error(), http.StatusInternalServerError) 1005 - h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1006 - } 1007 - 1008 - func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1009 - rev1 := chi.URLParam(r, "rev1") 1010 - rev1, _ = url.PathUnescape(rev1) 1011 - 1012 - rev2 := chi.URLParam(r, "rev2") 1013 - rev2, _ = url.PathUnescape(rev2) 1014 - 1015 - l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1016 - 1017 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1018 - gr, err := git.PlainOpen(path) 1019 - if err != nil { 1020 - notFound(w) 1021 - return 1022 - } 1023 - 1024 - commit1, err := gr.ResolveRevision(rev1) 1025 - if err != nil { 1026 - l.Error("error resolving revision 1", "msg", err.Error()) 1027 - writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1028 - return 1029 - } 1030 - 1031 - commit2, err := gr.ResolveRevision(rev2) 1032 - if err != nil { 1033 - l.Error("error resolving revision 2", "msg", err.Error()) 1034 - writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1035 - return 1036 - } 1037 - 1038 - rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1039 - if err != nil { 1040 - l.Error("error comparing revisions", "msg", err.Error()) 1041 - writeError(w, "error comparing revisions", http.StatusBadRequest) 1042 - return 202 + default: 203 + return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 1043 204 } 1044 205 1045 - writeJSON(w, types.RepoFormatPatchResponse{ 1046 - Rev1: commit1.Hash.String(), 1047 - Rev2: commit2.Hash.String(), 1048 - FormatPatch: formatPatch, 1049 - Patch: rawPatch, 1050 - }) 1051 - return 1052 - } 1053 - 1054 - func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1055 - l := h.l.With("handler", "NewHiddenRef") 1056 - 1057 - forkRef := chi.URLParam(r, "forkRef") 1058 - forkRef, _ = url.PathUnescape(forkRef) 1059 - 1060 - remoteRef := chi.URLParam(r, "remoteRef") 1061 - remoteRef, _ = url.PathUnescape(remoteRef) 1062 - 1063 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1064 - gr, err := git.PlainOpen(path) 1065 - if err != nil { 1066 - notFound(w) 1067 - return 1068 - } 1069 - 1070 - err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1071 - if err != nil { 1072 - l.Error("error tracking hidden remote ref", "msg", err.Error()) 1073 - writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1074 - return 1075 - } 1076 - 1077 - w.WriteHeader(http.StatusNoContent) 1078 - return 1079 - } 1080 - 1081 - func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1082 - l := h.l.With("handler", "AddMember") 1083 - 1084 - data := struct { 1085 - Did string `json:"did"` 1086 - }{} 1087 - 1088 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1089 - writeError(w, "invalid request body", http.StatusBadRequest) 1090 - return 1091 - } 1092 - 1093 - did := data.Did 1094 - 1095 - if err := h.db.AddDid(did); err != nil { 1096 - l.Error("adding did", "error", err.Error()) 1097 - writeError(w, err.Error(), http.StatusInternalServerError) 1098 - return 1099 - } 1100 - h.jc.AddDid(did) 1101 - 1102 - if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil { 1103 - l.Error("adding member", "error", err.Error()) 1104 - writeError(w, err.Error(), http.StatusInternalServerError) 1105 - return 1106 - } 1107 - 1108 - if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1109 - l.Error("fetching and adding keys", "error", err.Error()) 1110 - writeError(w, err.Error(), http.StatusInternalServerError) 1111 - return 1112 - } 1113 - 1114 - w.WriteHeader(http.StatusNoContent) 1115 - } 1116 - 1117 - func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1118 - l := h.l.With("handler", "AddRepoCollaborator") 1119 - 1120 - data := struct { 1121 - Did string `json:"did"` 1122 - }{} 1123 - 1124 - ownerDid := chi.URLParam(r, "did") 1125 - repo := chi.URLParam(r, "name") 1126 - 1127 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1128 - writeError(w, "invalid request body", http.StatusBadRequest) 1129 - return 1130 - } 1131 - 1132 - if err := h.db.AddDid(data.Did); err != nil { 1133 - l.Error("adding did", "error", err.Error()) 1134 - writeError(w, err.Error(), http.StatusInternalServerError) 1135 - return 1136 - } 1137 - h.jc.AddDid(data.Did) 1138 - 1139 - repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1140 - if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil { 1141 - l.Error("adding repo collaborator", "error", err.Error()) 1142 - writeError(w, err.Error(), http.StatusInternalServerError) 1143 - return 1144 - } 1145 - 1146 - if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1147 - l.Error("fetching and adding keys", "error", err.Error()) 1148 - writeError(w, err.Error(), http.StatusInternalServerError) 1149 - return 1150 - } 1151 - 1152 - w.WriteHeader(http.StatusNoContent) 1153 - } 1154 - 1155 - func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1156 - l := h.l.With("handler", "DefaultBranch") 1157 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1158 - 1159 - gr, err := git.Open(path, "") 1160 - if err != nil { 1161 - notFound(w) 1162 - return 1163 - } 1164 - 1165 - branch, err := gr.FindMainBranch() 1166 - if err != nil { 1167 - writeError(w, err.Error(), http.StatusInternalServerError) 1168 - l.Error("getting default branch", "error", err.Error()) 1169 - return 1170 - } 1171 - 1172 - writeJSON(w, types.RepoDefaultBranchResponse{ 1173 - Branch: branch, 1174 - }) 1175 - } 1176 - 1177 - func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1178 - l := h.l.With("handler", "SetDefaultBranch") 1179 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1180 - 1181 - data := struct { 1182 - Branch string `json:"branch"` 1183 - }{} 1184 - 1185 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1186 - writeError(w, err.Error(), http.StatusBadRequest) 1187 - return 1188 - } 1189 - 1190 - gr, err := git.PlainOpen(path) 1191 - if err != nil { 1192 - notFound(w) 1193 - return 1194 - } 1195 - 1196 - err = gr.SetDefaultBranch(data.Branch) 1197 - if err != nil { 1198 - writeError(w, err.Error(), http.StatusInternalServerError) 1199 - l.Error("setting default branch", "error", err.Error()) 1200 - return 1201 - } 1202 - 1203 - w.WriteHeader(http.StatusNoContent) 1204 - } 1205 - 1206 - func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1207 - w.Write([]byte("ok")) 1208 - } 1209 - 1210 - func validateRepoName(name string) error { 1211 - // check for path traversal attempts 1212 - if name == "." || name == ".." || 1213 - strings.Contains(name, "/") || strings.Contains(name, "\\") { 1214 - return fmt.Errorf("Repository name contains invalid path characters") 1215 - } 1216 - 1217 - // check for sequences that could be used for traversal when normalized 1218 - if strings.Contains(name, "./") || strings.Contains(name, "../") || 1219 - strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1220 - return fmt.Errorf("Repository name contains invalid path sequence") 1221 - } 1222 - 1223 - // then continue with character validation 1224 - for _, char := range name { 1225 - if !((char >= 'a' && char <= 'z') || 1226 - (char >= 'A' && char <= 'Z') || 1227 - (char >= '0' && char <= '9') || 1228 - char == '-' || char == '_' || char == '.') { 1229 - return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1230 - } 1231 - } 1232 - 1233 - // additional check to prevent multiple sequential dots 1234 - if strings.Contains(name, "..") { 1235 - return fmt.Errorf("Repository name cannot contain sequential dots") 1236 - } 1237 - 1238 - // if all checks pass 1239 - return nil 206 + return h.e.AddKnotOwner(rbacDomain, cfgOwner) 1240 207 }
+45 -16
knotserver/xrpc/create_repo.go
··· 8 8 "path/filepath" 9 9 "strings" 10 10 11 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/bluesky-social/indigo/xrpc" 12 14 securejoin "github.com/cyphar/filepath-securejoin" 13 15 gogit "github.com/go-git/go-git/v5" 14 16 "tangled.sh/tangled.sh/core/api/tangled" ··· 31 33 return 32 34 } 33 35 34 - isMember, err := h.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer) 36 + isMember, err := h.Enforcer.IsRepoCreateAllowed(actorDid.String(), rbac.ThisServer) 35 37 if err != nil { 36 38 fail(xrpcerr.GenericError(err)) 37 39 return ··· 47 49 return 48 50 } 49 51 52 + rkey := data.Rkey 53 + 54 + ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 + if err != nil || ident.Handle.IsInvalidHandle() { 56 + fail(xrpcerr.GenericError(err)) 57 + return 58 + } 59 + 60 + xrpcc := xrpc.Client{ 61 + Host: ident.PDSEndpoint(), 62 + } 63 + 64 + resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 + if err != nil { 66 + fail(xrpcerr.GenericError(err)) 67 + return 68 + } 69 + 70 + repo := resp.Value.Val.(*tangled.Repo) 71 + 50 72 defaultBranch := h.Config.Repo.MainBranch 51 - if data.Default_branch != nil && *data.Default_branch != "" { 52 - defaultBranch = *data.Default_branch 73 + if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 + defaultBranch = *data.DefaultBranch 53 75 } 54 76 55 - did := data.Did 56 - name := data.Name 57 - 58 - if err := validateRepoName(name); err != nil { 77 + if err := validateRepoName(repo.Name); err != nil { 59 78 l.Error("creating repo", "error", err.Error()) 60 79 fail(xrpcerr.GenericError(err)) 61 80 return 62 81 } 63 82 64 - relativeRepoPath := filepath.Join(did, name) 83 + relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) 65 84 repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) 66 - err = git.InitBare(repoPath, defaultBranch) 67 - if err != nil { 68 - l.Error("initializing bare repo", "error", err.Error()) 69 - if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 70 - fail(xrpcerr.RepoExistsError("repository already exists")) 71 - return 72 - } else { 85 + 86 + if data.Source != nil && *data.Source != "" { 87 + err = git.Fork(repoPath, *data.Source) 88 + if err != nil { 89 + l.Error("forking repo", "error", err.Error()) 73 90 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 74 91 return 75 92 } 93 + } else { 94 + err = git.InitBare(repoPath, defaultBranch) 95 + if err != nil { 96 + l.Error("initializing bare repo", "error", err.Error()) 97 + if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 + fail(xrpcerr.RepoExistsError("repository already exists")) 99 + return 100 + } else { 101 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 102 + return 103 + } 104 + } 76 105 } 77 106 78 107 // add perms for this user to access the repo 79 - err = h.Enforcer.AddRepo(did, rbac.ThisServer, relativeRepoPath) 108 + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 80 109 if err != nil { 81 110 l.Error("adding repo permissions", "error", err.Error()) 82 111 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+27 -13
knotserver/xrpc/delete_repo.go
··· 7 7 "os" 8 8 "path/filepath" 9 9 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/bluesky-social/indigo/xrpc" 11 13 securejoin "github.com/cyphar/filepath-securejoin" 12 14 "tangled.sh/tangled.sh/core/api/tangled" 13 15 "tangled.sh/tangled.sh/core/rbac" ··· 27 29 return 28 30 } 29 31 30 - isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer) 31 - if err != nil { 32 - fail(xrpcerr.GenericError(err)) 33 - return 34 - } 35 - if !isMember { 36 - fail(xrpcerr.AccessControlError(actorDid.String())) 37 - return 38 - } 39 - 40 32 var data tangled.RepoDelete_Input 41 33 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 42 34 fail(xrpcerr.GenericError(err)) ··· 45 37 46 38 did := data.Did 47 39 name := data.Name 40 + rkey := data.Rkey 48 41 49 42 if did == "" || name == "" { 50 43 fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 51 44 return 52 45 } 53 46 47 + ident, err := x.Resolver.ResolveIdent(r.Context(), actorDid.String()) 48 + if err != nil || ident.Handle.IsInvalidHandle() { 49 + fail(xrpcerr.GenericError(err)) 50 + return 51 + } 52 + 53 + xrpcc := xrpc.Client{ 54 + Host: ident.PDSEndpoint(), 55 + } 56 + 57 + // ensure that the record does not exists 58 + _, err = comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 59 + if err == nil { 60 + fail(xrpcerr.RecordExistsError(rkey)) 61 + return 62 + } 63 + 54 64 relativeRepoPath := filepath.Join(did, name) 55 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 56 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 57 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 65 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 66 + if err != nil { 67 + fail(xrpcerr.GenericError(err)) 68 + return 69 + } 70 + if !isDeleteAllowed { 71 + fail(xrpcerr.AccessControlError(actorDid.String())) 58 72 return 59 73 } 60 74
-93
knotserver/xrpc/fork_repo.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "net/http" 7 - "path/filepath" 8 - 9 - "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 - "tangled.sh/tangled.sh/core/api/tangled" 12 - "tangled.sh/tangled.sh/core/hook" 13 - "tangled.sh/tangled.sh/core/knotserver/git" 14 - "tangled.sh/tangled.sh/core/rbac" 15 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16 - ) 17 - 18 - func (x *Xrpc) ForkRepo(w http.ResponseWriter, r *http.Request) { 19 - l := x.Logger.With("handler", "ForkRepo") 20 - fail := func(e xrpcerr.XrpcError) { 21 - l.Error("failed", "kind", e.Tag, "error", e.Message) 22 - writeError(w, e, http.StatusBadRequest) 23 - } 24 - 25 - actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 26 - if !ok { 27 - fail(xrpcerr.MissingActorDidError) 28 - return 29 - } 30 - 31 - isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer) 32 - if err != nil { 33 - fail(xrpcerr.GenericError(err)) 34 - return 35 - } 36 - if !isMember { 37 - fail(xrpcerr.AccessControlError(actorDid.String())) 38 - return 39 - } 40 - 41 - var data tangled.RepoFork_Input 42 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 43 - fail(xrpcerr.GenericError(err)) 44 - return 45 - } 46 - 47 - did := data.Did 48 - source := data.Source 49 - 50 - if did == "" || source == "" { 51 - fail(xrpcerr.GenericError(fmt.Errorf("did and source are required"))) 52 - return 53 - } 54 - 55 - var name string 56 - if data.Name != nil && *data.Name != "" { 57 - name = *data.Name 58 - } else { 59 - name = filepath.Base(source) 60 - } 61 - 62 - relativeRepoPath := filepath.Join(did, name) 63 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 64 - if err != nil { 65 - fail(xrpcerr.GenericError(err)) 66 - return 67 - } 68 - 69 - err = git.Fork(repoPath, source) 70 - if err != nil { 71 - l.Error("forking repo", "error", err.Error()) 72 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 73 - return 74 - } 75 - 76 - // add perms for this user to access the repo 77 - err = x.Enforcer.AddRepo(did, rbac.ThisServer, relativeRepoPath) 78 - if err != nil { 79 - l.Error("adding repo permissions", "error", err.Error()) 80 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 81 - return 82 - } 83 - 84 - hook.SetupRepo( 85 - hook.Config( 86 - hook.WithScanPath(x.Config.Repo.ScanPath), 87 - hook.WithInternalApi(x.Config.Server.InternalListenAddr), 88 - ), 89 - repoPath, 90 - ) 91 - 92 - w.WriteHeader(http.StatusOK) 93 - }
+4 -4
knotserver/xrpc/fork_sync.go
··· 37 37 name := data.Name 38 38 branch := data.Branch 39 39 40 - if did == "" || name == "" || branch == "" { 41 - fail(xrpcerr.GenericError(fmt.Errorf("did, name, and branch are required"))) 40 + if did == "" || name == "" { 41 + fail(xrpcerr.GenericError(fmt.Errorf("did, name are required"))) 42 42 return 43 43 } 44 44 ··· 56 56 return 57 57 } 58 58 59 - gr, err := git.PlainOpen(repoPath) 59 + gr, err := git.Open(repoPath, branch) 60 60 if err != nil { 61 61 fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 62 62 return 63 63 } 64 64 65 - err = gr.Sync(branch) 65 + err = gr.Sync() 66 66 if err != nil { 67 67 l.Error("error syncing repo fork", "error", err.Error()) 68 68 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
-18
knotserver/xrpc/merge_check.go
··· 6 6 "fmt" 7 7 "net/http" 8 8 9 - "github.com/bluesky-social/indigo/atproto/syntax" 10 9 securejoin "github.com/cyphar/filepath-securejoin" 11 10 "tangled.sh/tangled.sh/core/api/tangled" 12 11 "tangled.sh/tangled.sh/core/knotserver/git" 13 - "tangled.sh/tangled.sh/core/rbac" 14 12 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 15 13 ) 16 14 ··· 19 17 fail := func(e xrpcerr.XrpcError) { 20 18 l.Error("failed", "kind", e.Tag, "error", e.Message) 21 19 writeError(w, e, http.StatusBadRequest) 22 - } 23 - 24 - actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 25 - if !ok { 26 - fail(xrpcerr.MissingActorDidError) 27 - return 28 - } 29 - 30 - isMember, err := x.Enforcer.IsKnotMember(actorDid.String(), rbac.ThisServer) 31 - if err != nil { 32 - fail(xrpcerr.GenericError(err)) 33 - return 34 - } 35 - if !isMember { 36 - fail(xrpcerr.AccessControlError(actorDid.String())) 37 - return 38 20 } 39 21 40 22 var data tangled.RepoMergeCheck_Input
-56
knotserver/xrpc/router.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "encoding/json" 5 - "log/slog" 6 - "net/http" 7 - 8 - "tangled.sh/tangled.sh/core/api/tangled" 9 - "tangled.sh/tangled.sh/core/idresolver" 10 - "tangled.sh/tangled.sh/core/jetstream" 11 - "tangled.sh/tangled.sh/core/knotserver/config" 12 - "tangled.sh/tangled.sh/core/knotserver/db" 13 - "tangled.sh/tangled.sh/core/notifier" 14 - "tangled.sh/tangled.sh/core/rbac" 15 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16 - "tangled.sh/tangled.sh/core/xrpc/serviceauth" 17 - 18 - "github.com/go-chi/chi/v5" 19 - ) 20 - 21 - type Xrpc struct { 22 - Config *config.Config 23 - Db *db.DB 24 - Ingester *jetstream.JetstreamClient 25 - Enforcer *rbac.Enforcer 26 - Logger *slog.Logger 27 - Notifier *notifier.Notifier 28 - Resolver *idresolver.Resolver 29 - ServiceAuth *serviceauth.ServiceAuth 30 - } 31 - 32 - func (x *Xrpc) Router() http.Handler { 33 - r := chi.NewRouter() 34 - r.Group(func(r chi.Router) { 35 - r.Use(x.ServiceAuth.VerifyServiceAuth) 36 - 37 - r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 38 - r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) 39 - r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) 40 - r.Post("/"+tangled.RepoForkNSID, x.ForkRepo) 41 - r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) 42 - r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync) 43 - 44 - r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef) 45 - 46 - r.Post("/"+tangled.RepoMergeNSID, x.Merge) 47 - r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck) 48 - }) 49 - return r 50 - } 51 - 52 - func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 53 - w.Header().Set("Content-Type", "application/json") 54 - w.WriteHeader(status) 55 - json.NewEncoder(w).Encode(e) 56 - }
+60
knotserver/xrpc/xrpc.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "encoding/json" 5 + "log/slog" 6 + "net/http" 7 + 8 + "tangled.sh/tangled.sh/core/api/tangled" 9 + "tangled.sh/tangled.sh/core/idresolver" 10 + "tangled.sh/tangled.sh/core/jetstream" 11 + "tangled.sh/tangled.sh/core/knotserver/config" 12 + "tangled.sh/tangled.sh/core/knotserver/db" 13 + "tangled.sh/tangled.sh/core/notifier" 14 + "tangled.sh/tangled.sh/core/rbac" 15 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16 + "tangled.sh/tangled.sh/core/xrpc/serviceauth" 17 + 18 + "github.com/go-chi/chi/v5" 19 + ) 20 + 21 + type Xrpc struct { 22 + Config *config.Config 23 + Db *db.DB 24 + Ingester *jetstream.JetstreamClient 25 + Enforcer *rbac.Enforcer 26 + Logger *slog.Logger 27 + Notifier *notifier.Notifier 28 + Resolver *idresolver.Resolver 29 + ServiceAuth *serviceauth.ServiceAuth 30 + } 31 + 32 + func (x *Xrpc) Router() http.Handler { 33 + r := chi.NewRouter() 34 + 35 + r.Group(func(r chi.Router) { 36 + r.Use(x.ServiceAuth.VerifyServiceAuth) 37 + 38 + r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 39 + r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) 40 + r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) 41 + r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) 42 + r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync) 43 + r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef) 44 + r.Post("/"+tangled.RepoMergeNSID, x.Merge) 45 + }) 46 + 47 + // merge check is an open endpoint 48 + // 49 + // TODO: should we constrain this more? 50 + // - we can calculate on PR submit/resubmit/gitRefUpdate etc. 51 + // - use ETags on clients to keep requests to a minimum 52 + r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck) 53 + return r 54 + } 55 + 56 + func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 57 + w.Header().Set("Content-Type", "application/json") 58 + w.WriteHeader(status) 59 + json.NewEncoder(w).Encode(e) 60 + }
+24
lexicons/knot/knot.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.knot", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "any", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "createdAt": { 17 + "type": "string", 18 + "format": "datetime" 19 + } 20 + } 21 + } 22 + } 23 + } 24 + }
-22
lexicons/knot.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.knot", 4 - "needsCbor": true, 5 - "needsType": true, 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "key": "any", 10 - "record": { 11 - "type": "object", 12 - "required": ["createdAt"], 13 - "properties": { 14 - "createdAt": { 15 - "type": "string", 16 - "format": "datetime" 17 - } 18 - } 19 - } 20 - } 21 - } 22 - }
+7 -63
lexicons/pipeline/pipeline.json
··· 149 149 "type": "object", 150 150 "required": [ 151 151 "name", 152 - "dependencies", 153 - "steps", 154 - "environment", 155 - "clone" 152 + "engine", 153 + "clone", 154 + "raw" 156 155 ], 157 156 "properties": { 158 157 "name": { 159 158 "type": "string" 160 159 }, 161 - "dependencies": { 162 - "type": "array", 163 - "items": { 164 - "type": "ref", 165 - "ref": "#dependency" 166 - } 167 - }, 168 - "steps": { 169 - "type": "array", 170 - "items": { 171 - "type": "ref", 172 - "ref": "#step" 173 - } 174 - }, 175 - "environment": { 176 - "type": "array", 177 - "items": { 178 - "type": "ref", 179 - "ref": "#pair" 180 - } 160 + "engine": { 161 + "type": "string" 181 162 }, 182 163 "clone": { 183 164 "type": "ref", 184 165 "ref": "#cloneOpts" 185 - } 186 - } 187 - }, 188 - "dependency": { 189 - "type": "object", 190 - "required": [ 191 - "registry", 192 - "packages" 193 - ], 194 - "properties": { 195 - "registry": { 166 + }, 167 + "raw": { 196 168 "type": "string" 197 - }, 198 - "packages": { 199 - "type": "array", 200 - "items": { 201 - "type": "string" 202 - } 203 169 } 204 170 } 205 171 }, ··· 219 185 }, 220 186 "submodules": { 221 187 "type": "boolean" 222 - } 223 - } 224 - }, 225 - "step": { 226 - "type": "object", 227 - "required": [ 228 - "name", 229 - "command" 230 - ], 231 - "properties": { 232 - "name": { 233 - "type": "string" 234 - }, 235 - "command": { 236 - "type": "string" 237 - }, 238 - "environment": { 239 - "type": "array", 240 - "items": { 241 - "type": "ref", 242 - "ref": "#pair" 243 - } 244 188 } 245 189 } 246 190 },
+9 -8
lexicons/repo/create.json
··· 9 9 "encoding": "application/json", 10 10 "schema": { 11 11 "type": "object", 12 - "required": ["did", "name"], 12 + "required": [ 13 + "rkey" 14 + ], 13 15 "properties": { 14 - "did": { 16 + "rkey": { 15 17 "type": "string", 16 - "format": "did", 17 - "description": "DID of the user creating the repository" 18 + "description": "Rkey of the repository record" 18 19 }, 19 - "name": { 20 + "defaultBranch": { 20 21 "type": "string", 21 - "description": "Name of the repository" 22 + "description": "Default branch to push to" 22 23 }, 23 - "default_branch": { 24 + "source": { 24 25 "type": "string", 25 - "description": "Default branch name" 26 + "description": "A source URL to clone from, populate this when forking or importing a repository." 26 27 } 27 28 } 28 29 }
+5 -1
lexicons/repo/delete.json
··· 9 9 "encoding": "application/json", 10 10 "schema": { 11 11 "type": "object", 12 - "required": ["did", "name"], 12 + "required": ["did", "name", "rkey"], 13 13 "properties": { 14 14 "did": { 15 15 "type": "string", ··· 19 19 "name": { 20 20 "type": "string", 21 21 "description": "Name of the repository to delete" 22 + }, 23 + "rkey": { 24 + "type": "string", 25 + "description": "Rkey of the repository record" 22 26 } 23 27 } 24 28 }
-32
lexicons/repo/fork.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.repo.fork", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Fork a repository from a source repository", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": ["did", "source"], 13 - "properties": { 14 - "did": { 15 - "type": "string", 16 - "format": "did", 17 - "description": "DID of the user creating the fork" 18 - }, 19 - "source": { 20 - "type": "string", 21 - "description": "Source repository URL to fork from" 22 - }, 23 - "name": { 24 - "type": "string", 25 - "description": "Name for the forked repository (defaults to basename of source)" 26 - } 27 - } 28 - } 29 - } 30 - } 31 - } 32 - }
+3 -1
log/log.go
··· 9 9 // NewHandler sets up a new slog.Handler with the service name 10 10 // as an attribute 11 11 func NewHandler(name string) slog.Handler { 12 - handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) 12 + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 13 + Level: slog.LevelDebug, 14 + }) 13 15 14 16 var attrs []slog.Attr 15 17 attrs = append(attrs, slog.Attr{Key: "service", Value: slog.StringValue(name)})
+5 -2
nix/gomod2nix.toml
··· 426 426 version = "v0.3.1" 427 427 hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc=" 428 428 [mod."github.com/yuin/goldmark"] 429 - version = "v1.4.13" 430 - hash = "sha256-GVwFKZY6moIS6I0ZGuio/WtDif+lkZRfqWS6b4AAJyI=" 429 + version = "v1.4.15" 430 + hash = "sha256-MvSOT6dwf5hVYkIg4MnqMpsy5ZtWZ7amAE7Zo9HkEa0=" 431 + [mod."github.com/yuin/goldmark-highlighting/v2"] 432 + version = "v2.0.0-20230729083705-37449abec8cc" 433 + hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg=" 431 434 [mod."gitlab.com/yawning/secp256k1-voi"] 432 435 version = "v0.0.0-20230925100816-f2616030848b" 433 436 hash = "sha256-X8INg01LTg13iOuwPI3uOhPN7r01sPZtmtwJ2sudjCA="
+32 -29
nix/modules/knot.nix
··· 93 93 description = "Internal address for inter-service communication"; 94 94 }; 95 95 96 - secretFile = mkOption { 97 - type = lib.types.path; 98 - example = "KNOT_SERVER_SECRET=<hash>"; 99 - description = "File containing secret key provided by appview (required)"; 96 + owner = mkOption { 97 + type = types.str; 98 + example = "did:plc:qfpnj4og54vl56wngdriaxug"; 99 + description = "DID of owner (required)"; 100 100 }; 101 101 102 102 dbPath = mkOption { ··· 126 126 cfg.package 127 127 ]; 128 128 129 - system.activationScripts.gitConfig = let 130 - setMotd = 131 - if cfg.motdFile != null && cfg.motd != null 132 - then throw "motdFile and motd cannot be both set" 133 - else '' 134 - ${optionalString (cfg.motdFile != null) "cat ${cfg.motdFile} > ${cfg.stateDir}/motd"} 135 - ${optionalString (cfg.motd != null) ''printf "${cfg.motd}" > ${cfg.stateDir}/motd''} 136 - ''; 137 - in '' 138 - mkdir -p "${cfg.repo.scanPath}" 139 - chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}" 140 - 141 - mkdir -p "${cfg.stateDir}/.config/git" 142 - cat > "${cfg.stateDir}/.config/git/config" << EOF 143 - [user] 144 - name = Git User 145 - email = git@example.com 146 - [receive] 147 - advertisePushOptions = true 148 - EOF 149 - ${setMotd} 150 - chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}" 151 - ''; 152 - 153 129 users.users.${cfg.gitUser} = { 154 130 isSystemUser = true; 155 131 useDefaultShell = true; ··· 185 161 description = "knot service"; 186 162 after = ["network.target" "sshd.service"]; 187 163 wantedBy = ["multi-user.target"]; 164 + enableStrictShellChecks = true; 165 + 166 + preStart = let 167 + setMotd = 168 + if cfg.motdFile != null && cfg.motd != null 169 + then throw "motdFile and motd cannot be both set" 170 + else '' 171 + ${optionalString (cfg.motdFile != null) "cat ${cfg.motdFile} > ${cfg.stateDir}/motd"} 172 + ${optionalString (cfg.motd != null) ''printf "${cfg.motd}" > ${cfg.stateDir}/motd''} 173 + ''; 174 + in '' 175 + mkdir -p "${cfg.repo.scanPath}" 176 + chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}" 177 + 178 + mkdir -p "${cfg.stateDir}/.config/git" 179 + cat > "${cfg.stateDir}/.config/git/config" << EOF 180 + [user] 181 + name = Git User 182 + email = git@example.com 183 + [receive] 184 + advertisePushOptions = true 185 + EOF 186 + ${setMotd} 187 + chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}" 188 + ''; 189 + 188 190 serviceConfig = { 189 191 User = cfg.gitUser; 192 + PermissionsStartOnly = true; 190 193 WorkingDirectory = cfg.stateDir; 191 194 Environment = [ 192 195 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" ··· 196 199 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 197 200 "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 198 201 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 202 + "KNOT_SERVER_OWNER=${cfg.server.owner}" 199 203 ]; 200 - EnvironmentFile = cfg.server.secretFile; 201 204 ExecStart = "${cfg.package}/bin/knot server"; 202 205 Restart = "always"; 203 206 };
+2 -2
nix/modules/spindle.nix
··· 111 111 "SPINDLE_SERVER_SECRETS_PROVIDER=${cfg.server.secrets.provider}" 112 112 "SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=${cfg.server.secrets.openbao.proxyAddr}" 113 113 "SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=${cfg.server.secrets.openbao.mount}" 114 - "SPINDLE_PIPELINES_NIXERY=${cfg.pipelines.nixery}" 115 - "SPINDLE_PIPELINES_WORKFLOW_TIMEOUT=${cfg.pipelines.workflowTimeout}" 114 + "SPINDLE_NIXERY_PIPELINES_NIXERY=${cfg.pipelines.nixery}" 115 + "SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT=${cfg.pipelines.workflowTimeout}" 116 116 ]; 117 117 ExecStart = "${cfg.package}/bin/spindle"; 118 118 Restart = "always";
+1 -1
nix/pkgs/appview-static-files.nix
··· 22 22 cp -rf ${lucide-src}/*.svg icons/ 23 23 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 fonts/ 24 24 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 fonts/ 25 - cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 fonts/ 25 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 26 26 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 27 27 # for whatever reason (produces broken css), so we are doing this instead 28 28 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+55 -16
nix/vm.nix
··· 1 1 { 2 2 nixpkgs, 3 3 system, 4 + hostSystem, 4 5 self, 5 6 }: let 6 7 envVar = name: let ··· 16 17 self.nixosModules.knot 17 18 self.nixosModules.spindle 18 19 ({ 20 + lib, 19 21 config, 20 22 pkgs, 21 23 ... 22 24 }: { 23 - nixos-shell = { 24 - inheritPath = false; 25 - mounts = { 26 - mountHome = false; 27 - mountNixProfile = false; 28 - }; 29 - }; 30 - virtualisation = { 25 + virtualisation.vmVariant.virtualisation = { 26 + host.pkgs = import nixpkgs {system = hostSystem;}; 27 + 28 + graphics = false; 31 29 memorySize = 2048; 32 30 diskSize = 10 * 1024; 33 31 cores = 2; ··· 51 49 guest.port = 6555; 52 50 } 53 51 ]; 52 + sharedDirectories = { 53 + # We can't use the 9p mounts directly for most of these 54 + # as SQLite is incompatible with them. So instead we 55 + # mount the shared directories to a different location 56 + # and copy the contents around on service start/stop. 57 + knotData = { 58 + source = "$TANGLED_VM_DATA_DIR/knot"; 59 + target = "/mnt/knot-data"; 60 + }; 61 + spindleData = { 62 + source = "$TANGLED_VM_DATA_DIR/spindle"; 63 + target = "/mnt/spindle-data"; 64 + }; 65 + spindleLogs = { 66 + source = "$TANGLED_VM_DATA_DIR/spindle-logs"; 67 + target = "/var/log/spindle"; 68 + }; 69 + }; 54 70 }; 71 + # This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall 72 + networking.firewall.enable = false; 73 + time.timeZone = "Europe/London"; 55 74 services.getty.autologinUser = "root"; 56 75 environment.systemPackages = with pkgs; [curl vim git sqlite litecli]; 57 - systemd.tmpfiles.rules = let 58 - u = config.services.tangled-knot.gitUser; 59 - g = config.services.tangled-knot.gitUser; 60 - in [ 61 - "d /var/lib/knot 0770 ${u} ${g} - -" # Create the directory first 62 - "f+ /var/lib/knot/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=${envVar "TANGLED_VM_KNOT_SECRET"}" 63 - ]; 64 76 services.tangled-knot = { 65 77 enable = true; 66 78 motd = "Welcome to the development knot!\n"; 67 79 server = { 68 - secretFile = "/var/lib/knot/secret"; 80 + owner = envVar "TANGLED_VM_KNOT_OWNER"; 69 81 hostname = "localhost:6000"; 70 82 listenAddr = "0.0.0.0:6000"; 71 83 }; ··· 81 93 provider = "sqlite"; 82 94 }; 83 95 }; 96 + }; 97 + users = { 98 + # So we don't have to deal with permission clashing between 99 + # blank disk VMs and existing state 100 + users.${config.services.tangled-knot.gitUser}.uid = 666; 101 + groups.${config.services.tangled-knot.gitUser}.gid = 666; 102 + 103 + # TODO: separate spindle user 104 + }; 105 + systemd.services = let 106 + mkDataSyncScripts = source: target: { 107 + enableStrictShellChecks = true; 108 + 109 + preStart = lib.mkBefore '' 110 + mkdir -p ${target} 111 + ${lib.getExe pkgs.rsync} -a ${source}/ ${target} 112 + ''; 113 + 114 + postStop = lib.mkAfter '' 115 + ${lib.getExe pkgs.rsync} -a ${target}/ ${source} 116 + ''; 117 + 118 + serviceConfig.PermissionsStartOnly = true; 119 + }; 120 + in { 121 + knot = mkDataSyncScripts "/mnt/knot-data" config.services.tangled-knot.stateDir; 122 + spindle = mkDataSyncScripts "/mnt/spindle-data" (builtins.dirOf config.services.tangled-spindle.server.dbPath); 84 123 }; 85 124 }) 86 125 ];
+9 -1
rbac/rbac.go
··· 43 43 return nil, err 44 44 } 45 45 46 - db, err := sql.Open("sqlite3", path) 46 + db, err := sql.Open("sqlite3", path+"?_foreign_keys=1") 47 47 if err != nil { 48 48 return nil, err 49 49 } ··· 275 275 276 276 func (e *Enforcer) IsSpindleInviteAllowed(user, domain string) (bool, error) { 277 277 return e.isInviteAllowed(user, intoSpindle(domain)) 278 + } 279 + 280 + func (e *Enforcer) IsRepoCreateAllowed(user, domain string) (bool, error) { 281 + return e.E.Enforce(user, domain, domain, "repo:create") 282 + } 283 + 284 + func (e *Enforcer) IsRepoDeleteAllowed(user, domain, repo string) (bool, error) { 285 + return e.E.Enforce(user, domain, repo, "repo:delete") 278 286 } 279 287 280 288 func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
+1 -1
rbac/rbac_test.go
··· 14 14 ) 15 15 16 16 func setup(t *testing.T) *rbac.Enforcer { 17 - db, err := sql.Open("sqlite3", ":memory:") 17 + db, err := sql.Open("sqlite3", ":memory:?_foreign_keys=1") 18 18 assert.NoError(t, err) 19 19 20 20 a, err := adapter.NewAdapter(db, "sqlite3", "acl")
+4 -4
spindle/config/config.go
··· 16 16 Dev bool `env:"DEV, default=false"` 17 17 Owner string `env:"OWNER, required"` 18 18 Secrets Secrets `env:",prefix=SECRETS_"` 19 + LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 19 20 } 20 21 21 22 func (s Server) Did() syntax.DID { ··· 32 33 Mount string `env:"MOUNT, default=spindle"` 33 34 } 34 35 35 - type Pipelines struct { 36 + type NixeryPipelines struct { 36 37 Nixery string `env:"NIXERY, default=nixery.tangled.sh"` 37 38 WorkflowTimeout string `env:"WORKFLOW_TIMEOUT, default=5m"` 38 - LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 39 39 } 40 40 41 41 type Config struct { 42 - Server Server `env:",prefix=SPINDLE_SERVER_"` 43 - Pipelines Pipelines `env:",prefix=SPINDLE_PIPELINES_"` 42 + Server Server `env:",prefix=SPINDLE_SERVER_"` 43 + NixeryPipelines NixeryPipelines `env:",prefix=SPINDLE_NIXERY_PIPELINES_"` 44 44 } 45 45 46 46 func Load(ctx context.Context) (*Config, error) {
+14 -10
spindle/db/db.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "strings" 5 6 6 7 _ "github.com/mattn/go-sqlite3" 7 8 ) ··· 11 12 } 12 13 13 14 func Make(dbPath string) (*DB, error) { 14 - db, err := sql.Open("sqlite3", dbPath) 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, "&")) 15 24 if err != nil { 16 25 return nil, err 17 26 } 18 27 19 - _, err = db.Exec(` 20 - pragma journal_mode = WAL; 21 - pragma synchronous = normal; 22 - pragma foreign_keys = on; 23 - pragma temp_store = memory; 24 - pragma mmap_size = 30000000000; 25 - pragma page_size = 32768; 26 - pragma auto_vacuum = incremental; 27 - pragma busy_timeout = 5000; 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. 28 31 32 + _, err = db.Exec(` 29 33 create table if not exists _jetstream ( 30 34 id integer primary key autoincrement, 31 35 last_time_us integer not null
-21
spindle/engine/ansi_stripper.go
··· 1 - package engine 2 - 3 - import ( 4 - "io" 5 - 6 - "regexp" 7 - ) 8 - 9 - // regex to match ANSI escape codes (e.g., color codes, cursor moves) 10 - const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 11 - 12 - var re = regexp.MustCompile(ansi) 13 - 14 - type ansiStrippingWriter struct { 15 - underlying io.Writer 16 - } 17 - 18 - func (w *ansiStrippingWriter) Write(p []byte) (int, error) { 19 - clean := re.ReplaceAll(p, []byte{}) 20 - return w.underlying.Write(clean) 21 - }
+68 -415
spindle/engine/engine.go
··· 4 4 "context" 5 5 "errors" 6 6 "fmt" 7 - "io" 8 7 "log/slog" 9 - "os" 10 - "strings" 11 - "sync" 12 - "time" 13 8 14 9 securejoin "github.com/cyphar/filepath-securejoin" 15 - "github.com/docker/docker/api/types/container" 16 - "github.com/docker/docker/api/types/image" 17 - "github.com/docker/docker/api/types/mount" 18 - "github.com/docker/docker/api/types/network" 19 - "github.com/docker/docker/api/types/volume" 20 - "github.com/docker/docker/client" 21 - "github.com/docker/docker/pkg/stdcopy" 22 10 "golang.org/x/sync/errgroup" 23 - "tangled.sh/tangled.sh/core/log" 24 11 "tangled.sh/tangled.sh/core/notifier" 25 12 "tangled.sh/tangled.sh/core/spindle/config" 26 13 "tangled.sh/tangled.sh/core/spindle/db" ··· 28 15 "tangled.sh/tangled.sh/core/spindle/secrets" 29 16 ) 30 17 31 - const ( 32 - workspaceDir = "/tangled/workspace" 18 + var ( 19 + ErrTimedOut = errors.New("timed out") 20 + ErrWorkflowFailed = errors.New("workflow failed") 33 21 ) 34 22 35 - type cleanupFunc func(context.Context) error 36 - 37 - type Engine struct { 38 - docker client.APIClient 39 - l *slog.Logger 40 - db *db.DB 41 - n *notifier.Notifier 42 - cfg *config.Config 43 - vault secrets.Manager 44 - 45 - cleanupMu sync.Mutex 46 - cleanup map[string][]cleanupFunc 47 - } 48 - 49 - func New(ctx context.Context, cfg *config.Config, db *db.DB, n *notifier.Notifier, vault secrets.Manager) (*Engine, error) { 50 - dcli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 51 - if err != nil { 52 - return nil, err 53 - } 54 - 55 - l := log.FromContext(ctx).With("component", "spindle") 56 - 57 - e := &Engine{ 58 - docker: dcli, 59 - l: l, 60 - db: db, 61 - n: n, 62 - cfg: cfg, 63 - vault: vault, 64 - } 65 - 66 - e.cleanup = make(map[string][]cleanupFunc) 67 - 68 - return e, nil 69 - } 70 - 71 - func (e *Engine) StartWorkflows(ctx context.Context, pipeline *models.Pipeline, pipelineId models.PipelineId) { 72 - e.l.Info("starting all workflows in parallel", "pipeline", pipelineId) 23 + func StartWorkflows(l *slog.Logger, vault secrets.Manager, cfg *config.Config, db *db.DB, n *notifier.Notifier, ctx context.Context, pipeline *models.Pipeline, pipelineId models.PipelineId) { 24 + l.Info("starting all workflows in parallel", "pipeline", pipelineId) 73 25 74 26 // extract secrets 75 27 var allSecrets []secrets.UnlockedSecret 76 28 if didSlashRepo, err := securejoin.SecureJoin(pipeline.RepoOwner, pipeline.RepoName); err == nil { 77 - if res, err := e.vault.GetSecretsUnlocked(ctx, secrets.DidSlashRepo(didSlashRepo)); err == nil { 29 + if res, err := vault.GetSecretsUnlocked(ctx, secrets.DidSlashRepo(didSlashRepo)); err == nil { 78 30 allSecrets = res 79 31 } 80 32 } 81 33 82 - workflowTimeoutStr := e.cfg.Pipelines.WorkflowTimeout 83 - workflowTimeout, err := time.ParseDuration(workflowTimeoutStr) 84 - if err != nil { 85 - e.l.Error("failed to parse workflow timeout", "error", err, "timeout", workflowTimeoutStr) 86 - workflowTimeout = 5 * time.Minute 87 - } 88 - e.l.Info("using workflow timeout", "timeout", workflowTimeout) 89 - 90 34 eg, ctx := errgroup.WithContext(ctx) 91 - for _, w := range pipeline.Workflows { 92 - eg.Go(func() error { 93 - wid := models.WorkflowId{ 94 - PipelineId: pipelineId, 95 - Name: w.Name, 96 - } 97 - 98 - err := e.db.StatusRunning(wid, e.n) 99 - if err != nil { 100 - return err 101 - } 35 + for eng, wfs := range pipeline.Workflows { 36 + workflowTimeout := eng.WorkflowTimeout() 37 + l.Info("using workflow timeout", "timeout", workflowTimeout) 102 38 103 - err = e.SetupWorkflow(ctx, wid) 104 - if err != nil { 105 - e.l.Error("setting up worklow", "wid", wid, "err", err) 106 - return err 107 - } 108 - defer e.DestroyWorkflow(ctx, wid) 109 - 110 - reader, err := e.docker.ImagePull(ctx, w.Image, image.PullOptions{}) 111 - if err != nil { 112 - e.l.Error("pipeline image pull failed!", "image", w.Image, "workflowId", wid, "error", err.Error()) 39 + for _, w := range wfs { 40 + eg.Go(func() error { 41 + wid := models.WorkflowId{ 42 + PipelineId: pipelineId, 43 + Name: w.Name, 44 + } 113 45 114 - err := e.db.StatusFailed(wid, err.Error(), -1, e.n) 46 + err := db.StatusRunning(wid, n) 115 47 if err != nil { 116 48 return err 117 49 } 118 50 119 - return fmt.Errorf("pulling image: %w", err) 120 - } 121 - defer reader.Close() 122 - io.Copy(os.Stdout, reader) 123 - 124 - ctx, cancel := context.WithTimeout(ctx, workflowTimeout) 125 - defer cancel() 51 + err = eng.SetupWorkflow(ctx, wid, &w) 52 + if err != nil { 53 + // TODO(winter): Should this always set StatusFailed? 54 + // In the original, we only do in a subset of cases. 55 + l.Error("setting up worklow", "wid", wid, "err", err) 126 56 127 - err = e.StartSteps(ctx, wid, w, allSecrets) 128 - if err != nil { 129 - if errors.Is(err, ErrTimedOut) { 130 - dbErr := e.db.StatusTimeout(wid, e.n) 131 - if dbErr != nil { 132 - return dbErr 57 + destroyErr := eng.DestroyWorkflow(ctx, wid) 58 + if destroyErr != nil { 59 + l.Error("failed to destroy workflow after setup failure", "error", destroyErr) 133 60 } 134 - } else { 135 - dbErr := e.db.StatusFailed(wid, err.Error(), -1, e.n) 61 + 62 + dbErr := db.StatusFailed(wid, err.Error(), -1, n) 136 63 if dbErr != nil { 137 64 return dbErr 138 65 } 66 + return err 139 67 } 68 + defer eng.DestroyWorkflow(ctx, wid) 140 69 141 - return fmt.Errorf("starting steps image: %w", err) 142 - } 70 + wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid) 71 + if err != nil { 72 + l.Warn("failed to setup step logger; logs will not be persisted", "error", err) 73 + wfLogger = nil 74 + } else { 75 + defer wfLogger.Close() 76 + } 143 77 144 - err = e.db.StatusSuccess(wid, e.n) 145 - if err != nil { 146 - return err 147 - } 78 + ctx, cancel := context.WithTimeout(ctx, workflowTimeout) 79 + defer cancel() 148 80 149 - return nil 150 - }) 151 - } 81 + for stepIdx, step := range w.Steps { 82 + if wfLogger != nil { 83 + ctl := wfLogger.ControlWriter(stepIdx, step) 84 + ctl.Write([]byte(step.Name())) 85 + } 152 86 153 - if err = eg.Wait(); err != nil { 154 - e.l.Error("failed to run one or more workflows", "err", err) 155 - } else { 156 - e.l.Error("successfully ran full pipeline") 157 - } 158 - } 87 + err = eng.RunStep(ctx, wid, &w, stepIdx, allSecrets, wfLogger) 88 + if err != nil { 89 + if errors.Is(err, ErrTimedOut) { 90 + dbErr := db.StatusTimeout(wid, n) 91 + if dbErr != nil { 92 + return dbErr 93 + } 94 + } else { 95 + dbErr := db.StatusFailed(wid, err.Error(), -1, n) 96 + if dbErr != nil { 97 + return dbErr 98 + } 99 + } 159 100 160 - // SetupWorkflow sets up a new network for the workflow and volumes for 161 - // the workspace and Nix store. These are persisted across steps and are 162 - // destroyed at the end of the workflow. 163 - func (e *Engine) SetupWorkflow(ctx context.Context, wid models.WorkflowId) error { 164 - e.l.Info("setting up workflow", "workflow", wid) 101 + return fmt.Errorf("starting steps image: %w", err) 102 + } 103 + } 165 104 166 - _, err := e.docker.VolumeCreate(ctx, volume.CreateOptions{ 167 - Name: workspaceVolume(wid), 168 - Driver: "local", 169 - }) 170 - if err != nil { 171 - return err 172 - } 173 - e.registerCleanup(wid, func(ctx context.Context) error { 174 - return e.docker.VolumeRemove(ctx, workspaceVolume(wid), true) 175 - }) 176 - 177 - _, err = e.docker.VolumeCreate(ctx, volume.CreateOptions{ 178 - Name: nixVolume(wid), 179 - Driver: "local", 180 - }) 181 - if err != nil { 182 - return err 183 - } 184 - e.registerCleanup(wid, func(ctx context.Context) error { 185 - return e.docker.VolumeRemove(ctx, nixVolume(wid), true) 186 - }) 187 - 188 - _, err = e.docker.NetworkCreate(ctx, networkName(wid), network.CreateOptions{ 189 - Driver: "bridge", 190 - }) 191 - if err != nil { 192 - return err 193 - } 194 - e.registerCleanup(wid, func(ctx context.Context) error { 195 - return e.docker.NetworkRemove(ctx, networkName(wid)) 196 - }) 105 + err = db.StatusSuccess(wid, n) 106 + if err != nil { 107 + return err 108 + } 197 109 198 - return nil 199 - } 200 - 201 - // StartSteps starts all steps sequentially with the same base image. 202 - // ONLY marks pipeline as failed if container's exit code is non-zero. 203 - // All other errors are bubbled up. 204 - // Fixed version of the step execution logic 205 - func (e *Engine) StartSteps(ctx context.Context, wid models.WorkflowId, w models.Workflow, secrets []secrets.UnlockedSecret) error { 206 - workflowEnvs := ConstructEnvs(w.Environment) 207 - for _, s := range secrets { 208 - workflowEnvs.AddEnv(s.Key, s.Value) 209 - } 210 - 211 - for stepIdx, step := range w.Steps { 212 - select { 213 - case <-ctx.Done(): 214 - return ctx.Err() 215 - default: 216 - } 217 - 218 - envs := append(EnvVars(nil), workflowEnvs...) 219 - for k, v := range step.Environment { 220 - envs.AddEnv(k, v) 221 - } 222 - envs.AddEnv("HOME", workspaceDir) 223 - e.l.Debug("envs for step", "step", step.Name, "envs", envs.Slice()) 224 - 225 - hostConfig := hostConfig(wid) 226 - resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 227 - Image: w.Image, 228 - Cmd: []string{"bash", "-c", step.Command}, 229 - WorkingDir: workspaceDir, 230 - Tty: false, 231 - Hostname: "spindle", 232 - Env: envs.Slice(), 233 - }, hostConfig, nil, nil, "") 234 - defer e.DestroyStep(ctx, resp.ID) 235 - if err != nil { 236 - return fmt.Errorf("creating container: %w", err) 237 - } 238 - 239 - err = e.docker.NetworkConnect(ctx, networkName(wid), resp.ID, nil) 240 - if err != nil { 241 - return fmt.Errorf("connecting network: %w", err) 242 - } 243 - 244 - err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 245 - if err != nil { 246 - return err 247 - } 248 - e.l.Info("started container", "name", resp.ID, "step", step.Name) 249 - 250 - // start tailing logs in background 251 - tailDone := make(chan error, 1) 252 - go func() { 253 - tailDone <- e.TailStep(ctx, resp.ID, wid, stepIdx, step) 254 - }() 255 - 256 - // wait for container completion or timeout 257 - waitDone := make(chan struct{}) 258 - var state *container.State 259 - var waitErr error 260 - 261 - go func() { 262 - defer close(waitDone) 263 - state, waitErr = e.WaitStep(ctx, resp.ID) 264 - }() 265 - 266 - select { 267 - case <-waitDone: 268 - 269 - // wait for tailing to complete 270 - <-tailDone 271 - 272 - case <-ctx.Done(): 273 - e.l.Warn("step timed out; killing container", "container", resp.ID, "step", step.Name) 274 - err = e.DestroyStep(context.Background(), resp.ID) 275 - if err != nil { 276 - e.l.Error("failed to destroy step", "container", resp.ID, "error", err) 277 - } 278 - 279 - // wait for both goroutines to finish 280 - <-waitDone 281 - <-tailDone 282 - 283 - return ErrTimedOut 284 - } 285 - 286 - select { 287 - case <-ctx.Done(): 288 - return ctx.Err() 289 - default: 290 - } 291 - 292 - if waitErr != nil { 293 - return waitErr 294 - } 295 - 296 - err = e.DestroyStep(ctx, resp.ID) 297 - if err != nil { 298 - return err 299 - } 300 - 301 - if state.ExitCode != 0 { 302 - e.l.Error("workflow failed!", "workflow_id", wid.String(), "error", state.Error, "exit_code", state.ExitCode, "oom_killed", state.OOMKilled) 303 - if state.OOMKilled { 304 - return ErrOOMKilled 305 - } 306 - return ErrWorkflowFailed 110 + return nil 111 + }) 307 112 } 308 113 } 309 114 310 - return nil 311 - } 312 - 313 - func (e *Engine) WaitStep(ctx context.Context, containerID string) (*container.State, error) { 314 - wait, errCh := e.docker.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) 315 - select { 316 - case err := <-errCh: 317 - if err != nil { 318 - return nil, err 319 - } 320 - case <-wait: 321 - } 322 - 323 - e.l.Info("waited for container", "name", containerID) 324 - 325 - info, err := e.docker.ContainerInspect(ctx, containerID) 326 - if err != nil { 327 - return nil, err 328 - } 329 - 330 - return info.State, nil 331 - } 332 - 333 - func (e *Engine) TailStep(ctx context.Context, containerID string, wid models.WorkflowId, stepIdx int, step models.Step) error { 334 - wfLogger, err := NewWorkflowLogger(e.cfg.Pipelines.LogDir, wid) 335 - if err != nil { 336 - e.l.Warn("failed to setup step logger; logs will not be persisted", "error", err) 337 - return err 115 + if err := eg.Wait(); err != nil { 116 + l.Error("failed to run one or more workflows", "err", err) 117 + } else { 118 + l.Error("successfully ran full pipeline") 338 119 } 339 - defer wfLogger.Close() 340 - 341 - ctl := wfLogger.ControlWriter(stepIdx, step) 342 - ctl.Write([]byte(step.Name)) 343 - 344 - logs, err := e.docker.ContainerLogs(ctx, containerID, container.LogsOptions{ 345 - Follow: true, 346 - ShowStdout: true, 347 - ShowStderr: true, 348 - Details: false, 349 - Timestamps: false, 350 - }) 351 - if err != nil { 352 - return err 353 - } 354 - 355 - _, err = stdcopy.StdCopy( 356 - wfLogger.DataWriter("stdout"), 357 - wfLogger.DataWriter("stderr"), 358 - logs, 359 - ) 360 - if err != nil && err != io.EOF && !errors.Is(err, context.DeadlineExceeded) { 361 - return fmt.Errorf("failed to copy logs: %w", err) 362 - } 363 - 364 - return nil 365 - } 366 - 367 - func (e *Engine) DestroyStep(ctx context.Context, containerID string) error { 368 - err := e.docker.ContainerKill(ctx, containerID, "9") // SIGKILL 369 - if err != nil && !isErrContainerNotFoundOrNotRunning(err) { 370 - return err 371 - } 372 - 373 - if err := e.docker.ContainerRemove(ctx, containerID, container.RemoveOptions{ 374 - RemoveVolumes: true, 375 - RemoveLinks: false, 376 - Force: false, 377 - }); err != nil && !isErrContainerNotFoundOrNotRunning(err) { 378 - return err 379 - } 380 - 381 - return nil 382 - } 383 - 384 - func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 385 - e.cleanupMu.Lock() 386 - key := wid.String() 387 - 388 - fns := e.cleanup[key] 389 - delete(e.cleanup, key) 390 - e.cleanupMu.Unlock() 391 - 392 - for _, fn := range fns { 393 - if err := fn(ctx); err != nil { 394 - e.l.Error("failed to cleanup workflow resource", "workflowId", wid, "error", err) 395 - } 396 - } 397 - return nil 398 - } 399 - 400 - func (e *Engine) registerCleanup(wid models.WorkflowId, fn cleanupFunc) { 401 - e.cleanupMu.Lock() 402 - defer e.cleanupMu.Unlock() 403 - 404 - key := wid.String() 405 - e.cleanup[key] = append(e.cleanup[key], fn) 406 - } 407 - 408 - func workspaceVolume(wid models.WorkflowId) string { 409 - return fmt.Sprintf("workspace-%s", wid) 410 - } 411 - 412 - func nixVolume(wid models.WorkflowId) string { 413 - return fmt.Sprintf("nix-%s", wid) 414 - } 415 - 416 - func networkName(wid models.WorkflowId) string { 417 - return fmt.Sprintf("workflow-network-%s", wid) 418 - } 419 - 420 - func hostConfig(wid models.WorkflowId) *container.HostConfig { 421 - hostConfig := &container.HostConfig{ 422 - Mounts: []mount.Mount{ 423 - { 424 - Type: mount.TypeVolume, 425 - Source: workspaceVolume(wid), 426 - Target: workspaceDir, 427 - }, 428 - { 429 - Type: mount.TypeVolume, 430 - Source: nixVolume(wid), 431 - Target: "/nix", 432 - }, 433 - { 434 - Type: mount.TypeTmpfs, 435 - Target: "/tmp", 436 - ReadOnly: false, 437 - TmpfsOptions: &mount.TmpfsOptions{ 438 - Mode: 0o1777, // world-writeable sticky bit 439 - Options: [][]string{ 440 - {"exec"}, 441 - }, 442 - }, 443 - }, 444 - { 445 - Type: mount.TypeVolume, 446 - Source: "etc-nix-" + wid.String(), 447 - Target: "/etc/nix", 448 - }, 449 - }, 450 - ReadonlyRootfs: false, 451 - CapDrop: []string{"ALL"}, 452 - CapAdd: []string{"CAP_DAC_OVERRIDE"}, 453 - SecurityOpt: []string{"no-new-privileges"}, 454 - ExtraHosts: []string{"host.docker.internal:host-gateway"}, 455 - } 456 - 457 - return hostConfig 458 - } 459 - 460 - // thanks woodpecker 461 - func isErrContainerNotFoundOrNotRunning(err error) bool { 462 - // Error response from daemon: Cannot kill container: ...: No such container: ... 463 - // Error response from daemon: Cannot kill container: ...: Container ... is not running" 464 - // Error response from podman daemon: can only kill running containers. ... is in state exited 465 - // Error: No such container: ... 466 - return err != nil && (strings.Contains(err.Error(), "No such container") || strings.Contains(err.Error(), "is not running") || strings.Contains(err.Error(), "can only kill running containers")) 467 120 }
-28
spindle/engine/envs.go
··· 1 - package engine 2 - 3 - import ( 4 - "fmt" 5 - ) 6 - 7 - type EnvVars []string 8 - 9 - // ConstructEnvs converts a tangled.Pipeline_Step_Environment_Elem.{Key,Value} 10 - // representation into a docker-friendly []string{"KEY=value", ...} slice. 11 - func ConstructEnvs(envs map[string]string) EnvVars { 12 - var dockerEnvs EnvVars 13 - for k, v := range envs { 14 - ev := fmt.Sprintf("%s=%s", k, v) 15 - dockerEnvs = append(dockerEnvs, ev) 16 - } 17 - return dockerEnvs 18 - } 19 - 20 - // Slice returns the EnvVar as a []string slice. 21 - func (ev EnvVars) Slice() []string { 22 - return ev 23 - } 24 - 25 - // AddEnv adds a key=value string to the EnvVar. 26 - func (ev *EnvVars) AddEnv(key, value string) { 27 - *ev = append(*ev, fmt.Sprintf("%s=%s", key, value)) 28 - }
-48
spindle/engine/envs_test.go
··· 1 - package engine 2 - 3 - import ( 4 - "testing" 5 - 6 - "github.com/stretchr/testify/assert" 7 - ) 8 - 9 - func TestConstructEnvs(t *testing.T) { 10 - tests := []struct { 11 - name string 12 - in map[string]string 13 - want EnvVars 14 - }{ 15 - { 16 - name: "empty input", 17 - in: make(map[string]string), 18 - want: EnvVars{}, 19 - }, 20 - { 21 - name: "single env var", 22 - in: map[string]string{"FOO": "bar"}, 23 - want: EnvVars{"FOO=bar"}, 24 - }, 25 - { 26 - name: "multiple env vars", 27 - in: map[string]string{"FOO": "bar", "BAZ": "qux"}, 28 - want: EnvVars{"FOO=bar", "BAZ=qux"}, 29 - }, 30 - } 31 - for _, tt := range tests { 32 - t.Run(tt.name, func(t *testing.T) { 33 - got := ConstructEnvs(tt.in) 34 - if got == nil { 35 - got = EnvVars{} 36 - } 37 - assert.ElementsMatch(t, tt.want, got) 38 - }) 39 - } 40 - } 41 - 42 - func TestAddEnv(t *testing.T) { 43 - ev := EnvVars{} 44 - ev.AddEnv("FOO", "bar") 45 - ev.AddEnv("BAZ", "qux") 46 - want := EnvVars{"FOO=bar", "BAZ=qux"} 47 - assert.ElementsMatch(t, want, ev) 48 - }
-9
spindle/engine/errors.go
··· 1 - package engine 2 - 3 - import "errors" 4 - 5 - var ( 6 - ErrOOMKilled = errors.New("oom killed") 7 - ErrTimedOut = errors.New("timed out") 8 - ErrWorkflowFailed = errors.New("workflow failed") 9 - )
-84
spindle/engine/logger.go
··· 1 - package engine 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "io" 7 - "os" 8 - "path/filepath" 9 - "strings" 10 - 11 - "tangled.sh/tangled.sh/core/spindle/models" 12 - ) 13 - 14 - type WorkflowLogger struct { 15 - file *os.File 16 - encoder *json.Encoder 17 - } 18 - 19 - func NewWorkflowLogger(baseDir string, wid models.WorkflowId) (*WorkflowLogger, error) { 20 - path := LogFilePath(baseDir, wid) 21 - 22 - file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 23 - if err != nil { 24 - return nil, fmt.Errorf("creating log file: %w", err) 25 - } 26 - 27 - return &WorkflowLogger{ 28 - file: file, 29 - encoder: json.NewEncoder(file), 30 - }, nil 31 - } 32 - 33 - func LogFilePath(baseDir string, workflowID models.WorkflowId) string { 34 - logFilePath := filepath.Join(baseDir, fmt.Sprintf("%s.log", workflowID.String())) 35 - return logFilePath 36 - } 37 - 38 - func (l *WorkflowLogger) Close() error { 39 - return l.file.Close() 40 - } 41 - 42 - func (l *WorkflowLogger) DataWriter(stream string) io.Writer { 43 - // TODO: emit stream 44 - return &dataWriter{ 45 - logger: l, 46 - stream: stream, 47 - } 48 - } 49 - 50 - func (l *WorkflowLogger) ControlWriter(idx int, step models.Step) io.Writer { 51 - return &controlWriter{ 52 - logger: l, 53 - idx: idx, 54 - step: step, 55 - } 56 - } 57 - 58 - type dataWriter struct { 59 - logger *WorkflowLogger 60 - stream string 61 - } 62 - 63 - func (w *dataWriter) Write(p []byte) (int, error) { 64 - line := strings.TrimRight(string(p), "\r\n") 65 - entry := models.NewDataLogLine(line, w.stream) 66 - if err := w.logger.encoder.Encode(entry); err != nil { 67 - return 0, err 68 - } 69 - return len(p), nil 70 - } 71 - 72 - type controlWriter struct { 73 - logger *WorkflowLogger 74 - idx int 75 - step models.Step 76 - } 77 - 78 - func (w *controlWriter) Write(_ []byte) (int, error) { 79 - entry := models.NewControlLogLine(w.idx, w.step) 80 - if err := w.logger.encoder.Encode(entry); err != nil { 81 - return 0, err 82 - } 83 - return len(w.step.Name), nil 84 - }
+21
spindle/engines/nixery/ansi_stripper.go
··· 1 + package nixery 2 + 3 + import ( 4 + "io" 5 + 6 + "regexp" 7 + ) 8 + 9 + // regex to match ANSI escape codes (e.g., color codes, cursor moves) 10 + const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 11 + 12 + var re = regexp.MustCompile(ansi) 13 + 14 + type ansiStrippingWriter struct { 15 + underlying io.Writer 16 + } 17 + 18 + func (w *ansiStrippingWriter) Write(p []byte) (int, error) { 19 + clean := re.ReplaceAll(p, []byte{}) 20 + return w.underlying.Write(clean) 21 + }
+421
spindle/engines/nixery/engine.go
··· 1 + package nixery 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "log/slog" 9 + "os" 10 + "path" 11 + "runtime" 12 + "sync" 13 + "time" 14 + 15 + "github.com/docker/docker/api/types/container" 16 + "github.com/docker/docker/api/types/image" 17 + "github.com/docker/docker/api/types/mount" 18 + "github.com/docker/docker/api/types/network" 19 + "github.com/docker/docker/client" 20 + "github.com/docker/docker/pkg/stdcopy" 21 + "gopkg.in/yaml.v3" 22 + "tangled.sh/tangled.sh/core/api/tangled" 23 + "tangled.sh/tangled.sh/core/log" 24 + "tangled.sh/tangled.sh/core/spindle/config" 25 + "tangled.sh/tangled.sh/core/spindle/engine" 26 + "tangled.sh/tangled.sh/core/spindle/models" 27 + "tangled.sh/tangled.sh/core/spindle/secrets" 28 + ) 29 + 30 + const ( 31 + workspaceDir = "/tangled/workspace" 32 + homeDir = "/tangled/home" 33 + ) 34 + 35 + type cleanupFunc func(context.Context) error 36 + 37 + type Engine struct { 38 + docker client.APIClient 39 + l *slog.Logger 40 + cfg *config.Config 41 + 42 + cleanupMu sync.Mutex 43 + cleanup map[string][]cleanupFunc 44 + } 45 + 46 + type Step struct { 47 + name string 48 + kind models.StepKind 49 + command string 50 + environment map[string]string 51 + } 52 + 53 + func (s Step) Name() string { 54 + return s.name 55 + } 56 + 57 + func (s Step) Command() string { 58 + return s.command 59 + } 60 + 61 + func (s Step) Kind() models.StepKind { 62 + return s.kind 63 + } 64 + 65 + // setupSteps get added to start of Steps 66 + type setupSteps []models.Step 67 + 68 + // addStep adds a step to the beginning of the workflow's steps. 69 + func (ss *setupSteps) addStep(step models.Step) { 70 + *ss = append(*ss, step) 71 + } 72 + 73 + type addlFields struct { 74 + image string 75 + container string 76 + env map[string]string 77 + } 78 + 79 + func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) { 80 + swf := &models.Workflow{} 81 + addl := addlFields{} 82 + 83 + dwf := &struct { 84 + Steps []struct { 85 + Command string `yaml:"command"` 86 + Name string `yaml:"name"` 87 + Environment map[string]string `yaml:"environment"` 88 + } `yaml:"steps"` 89 + Dependencies map[string][]string `yaml:"dependencies"` 90 + Environment map[string]string `yaml:"environment"` 91 + }{} 92 + err := yaml.Unmarshal([]byte(twf.Raw), &dwf) 93 + if err != nil { 94 + return nil, err 95 + } 96 + 97 + for _, dstep := range dwf.Steps { 98 + sstep := Step{} 99 + sstep.environment = dstep.Environment 100 + sstep.command = dstep.Command 101 + sstep.name = dstep.Name 102 + sstep.kind = models.StepKindUser 103 + swf.Steps = append(swf.Steps, sstep) 104 + } 105 + swf.Name = twf.Name 106 + addl.env = dwf.Environment 107 + addl.image = workflowImage(dwf.Dependencies, e.cfg.NixeryPipelines.Nixery) 108 + 109 + setup := &setupSteps{} 110 + 111 + setup.addStep(nixConfStep()) 112 + setup.addStep(cloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev)) 113 + // this step could be empty 114 + if s := dependencyStep(dwf.Dependencies); s != nil { 115 + setup.addStep(*s) 116 + } 117 + 118 + // append setup steps in order to the start of workflow steps 119 + swf.Steps = append(*setup, swf.Steps...) 120 + swf.Data = addl 121 + 122 + return swf, nil 123 + } 124 + 125 + func (e *Engine) WorkflowTimeout() time.Duration { 126 + workflowTimeoutStr := e.cfg.NixeryPipelines.WorkflowTimeout 127 + workflowTimeout, err := time.ParseDuration(workflowTimeoutStr) 128 + if err != nil { 129 + e.l.Error("failed to parse workflow timeout", "error", err, "timeout", workflowTimeoutStr) 130 + workflowTimeout = 5 * time.Minute 131 + } 132 + 133 + return workflowTimeout 134 + } 135 + 136 + func workflowImage(deps map[string][]string, nixery string) string { 137 + var dependencies string 138 + for reg, ds := range deps { 139 + if reg == "nixpkgs" { 140 + dependencies = path.Join(ds...) 141 + } 142 + } 143 + 144 + // load defaults from somewhere else 145 + dependencies = path.Join(dependencies, "bash", "git", "coreutils", "nix") 146 + 147 + if runtime.GOARCH == "arm64" { 148 + dependencies = path.Join("arm64", dependencies) 149 + } 150 + 151 + return path.Join(nixery, dependencies) 152 + } 153 + 154 + func New(ctx context.Context, cfg *config.Config) (*Engine, error) { 155 + dcli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 156 + if err != nil { 157 + return nil, err 158 + } 159 + 160 + l := log.FromContext(ctx).With("component", "spindle") 161 + 162 + e := &Engine{ 163 + docker: dcli, 164 + l: l, 165 + cfg: cfg, 166 + } 167 + 168 + e.cleanup = make(map[string][]cleanupFunc) 169 + 170 + return e, nil 171 + } 172 + 173 + func (e *Engine) SetupWorkflow(ctx context.Context, wid models.WorkflowId, wf *models.Workflow) error { 174 + e.l.Info("setting up workflow", "workflow", wid) 175 + 176 + _, err := e.docker.NetworkCreate(ctx, networkName(wid), network.CreateOptions{ 177 + Driver: "bridge", 178 + }) 179 + if err != nil { 180 + return err 181 + } 182 + e.registerCleanup(wid, func(ctx context.Context) error { 183 + return e.docker.NetworkRemove(ctx, networkName(wid)) 184 + }) 185 + 186 + addl := wf.Data.(addlFields) 187 + 188 + reader, err := e.docker.ImagePull(ctx, addl.image, image.PullOptions{}) 189 + if err != nil { 190 + e.l.Error("pipeline image pull failed!", "image", addl.image, "workflowId", wid, "error", err.Error()) 191 + 192 + return fmt.Errorf("pulling image: %w", err) 193 + } 194 + defer reader.Close() 195 + io.Copy(os.Stdout, reader) 196 + 197 + resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 198 + Image: addl.image, 199 + Cmd: []string{"cat"}, 200 + OpenStdin: true, // so cat stays alive :3 201 + Tty: false, 202 + Hostname: "spindle", 203 + WorkingDir: workspaceDir, 204 + Labels: map[string]string{ 205 + "sh.tangled.pipeline/workflow_id": wid.String(), 206 + }, 207 + // TODO(winter): investigate whether environment variables passed here 208 + // get propagated to ContainerExec processes 209 + }, &container.HostConfig{ 210 + Mounts: []mount.Mount{ 211 + { 212 + Type: mount.TypeTmpfs, 213 + Target: "/tmp", 214 + ReadOnly: false, 215 + TmpfsOptions: &mount.TmpfsOptions{ 216 + Mode: 0o1777, // world-writeable sticky bit 217 + Options: [][]string{ 218 + {"exec"}, 219 + }, 220 + }, 221 + }, 222 + }, 223 + ReadonlyRootfs: false, 224 + CapDrop: []string{"ALL"}, 225 + CapAdd: []string{"CAP_DAC_OVERRIDE"}, 226 + SecurityOpt: []string{"no-new-privileges"}, 227 + ExtraHosts: []string{"host.docker.internal:host-gateway"}, 228 + }, nil, nil, "") 229 + if err != nil { 230 + return fmt.Errorf("creating container: %w", err) 231 + } 232 + e.registerCleanup(wid, func(ctx context.Context) error { 233 + err = e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{}) 234 + if err != nil { 235 + return err 236 + } 237 + 238 + return e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{ 239 + RemoveVolumes: true, 240 + RemoveLinks: false, 241 + Force: false, 242 + }) 243 + }) 244 + 245 + err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 246 + if err != nil { 247 + return fmt.Errorf("starting container: %w", err) 248 + } 249 + 250 + mkExecResp, err := e.docker.ContainerExecCreate(ctx, resp.ID, container.ExecOptions{ 251 + Cmd: []string{"mkdir", "-p", workspaceDir, homeDir}, 252 + AttachStdout: true, // NOTE(winter): pretty sure this will make it so that when stdout read is done below, mkdir is done. maybe?? 253 + AttachStderr: true, // for good measure, backed up by docker/cli ("If -d is not set, attach to everything by default") 254 + }) 255 + if err != nil { 256 + return err 257 + } 258 + 259 + // This actually *starts* the command. Thanks, Docker! 260 + execResp, err := e.docker.ContainerExecAttach(ctx, mkExecResp.ID, container.ExecAttachOptions{}) 261 + if err != nil { 262 + return err 263 + } 264 + defer execResp.Close() 265 + 266 + // This is apparently best way to wait for the command to complete. 267 + _, err = io.ReadAll(execResp.Reader) 268 + if err != nil { 269 + return err 270 + } 271 + 272 + execInspectResp, err := e.docker.ContainerExecInspect(ctx, mkExecResp.ID) 273 + if err != nil { 274 + return err 275 + } 276 + 277 + if execInspectResp.ExitCode != 0 { 278 + return fmt.Errorf("mkdir exited with exit code %d", execInspectResp.ExitCode) 279 + } else if execInspectResp.Running { 280 + return errors.New("mkdir is somehow still running??") 281 + } 282 + 283 + addl.container = resp.ID 284 + wf.Data = addl 285 + 286 + return nil 287 + } 288 + 289 + func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error { 290 + addl := w.Data.(addlFields) 291 + workflowEnvs := ConstructEnvs(addl.env) 292 + // TODO(winter): should SetupWorkflow also have secret access? 293 + // IMO yes, but probably worth thinking on. 294 + for _, s := range secrets { 295 + workflowEnvs.AddEnv(s.Key, s.Value) 296 + } 297 + 298 + step := w.Steps[idx].(Step) 299 + 300 + select { 301 + case <-ctx.Done(): 302 + return ctx.Err() 303 + default: 304 + } 305 + 306 + envs := append(EnvVars(nil), workflowEnvs...) 307 + for k, v := range step.environment { 308 + envs.AddEnv(k, v) 309 + } 310 + envs.AddEnv("HOME", homeDir) 311 + 312 + mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{ 313 + Cmd: []string{"bash", "-c", step.command}, 314 + AttachStdout: true, 315 + AttachStderr: true, 316 + Env: envs, 317 + }) 318 + if err != nil { 319 + return fmt.Errorf("creating exec: %w", err) 320 + } 321 + 322 + // start tailing logs in background 323 + tailDone := make(chan error, 1) 324 + go func() { 325 + tailDone <- e.tailStep(ctx, wfLogger, mkExecResp.ID, wid, idx, step) 326 + }() 327 + 328 + select { 329 + case <-tailDone: 330 + 331 + case <-ctx.Done(): 332 + // cleanup will be handled by DestroyWorkflow, since 333 + // Docker doesn't provide an API to kill an exec run 334 + // (sure, we could grab the PID and kill it ourselves, 335 + // but that's wasted effort) 336 + e.l.Warn("step timed out", "step", step.Name) 337 + 338 + <-tailDone 339 + 340 + return engine.ErrTimedOut 341 + } 342 + 343 + select { 344 + case <-ctx.Done(): 345 + return ctx.Err() 346 + default: 347 + } 348 + 349 + execInspectResp, err := e.docker.ContainerExecInspect(ctx, mkExecResp.ID) 350 + if err != nil { 351 + return err 352 + } 353 + 354 + if execInspectResp.ExitCode != 0 { 355 + inspectResp, err := e.docker.ContainerInspect(ctx, addl.container) 356 + if err != nil { 357 + return err 358 + } 359 + 360 + e.l.Error("workflow failed!", "workflow_id", wid.String(), "exit_code", execInspectResp.ExitCode, "oom_killed", inspectResp.State.OOMKilled) 361 + 362 + if inspectResp.State.OOMKilled { 363 + return ErrOOMKilled 364 + } 365 + return engine.ErrWorkflowFailed 366 + } 367 + 368 + return nil 369 + } 370 + 371 + func (e *Engine) tailStep(ctx context.Context, wfLogger *models.WorkflowLogger, execID string, wid models.WorkflowId, stepIdx int, step models.Step) error { 372 + if wfLogger == nil { 373 + return nil 374 + } 375 + 376 + // This actually *starts* the command. Thanks, Docker! 377 + logs, err := e.docker.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{}) 378 + if err != nil { 379 + return err 380 + } 381 + defer logs.Close() 382 + 383 + _, err = stdcopy.StdCopy( 384 + wfLogger.DataWriter("stdout"), 385 + wfLogger.DataWriter("stderr"), 386 + logs.Reader, 387 + ) 388 + if err != nil && err != io.EOF && !errors.Is(err, context.DeadlineExceeded) { 389 + return fmt.Errorf("failed to copy logs: %w", err) 390 + } 391 + 392 + return nil 393 + } 394 + 395 + func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 396 + e.cleanupMu.Lock() 397 + key := wid.String() 398 + 399 + fns := e.cleanup[key] 400 + delete(e.cleanup, key) 401 + e.cleanupMu.Unlock() 402 + 403 + for _, fn := range fns { 404 + if err := fn(ctx); err != nil { 405 + e.l.Error("failed to cleanup workflow resource", "workflowId", wid, "error", err) 406 + } 407 + } 408 + return nil 409 + } 410 + 411 + func (e *Engine) registerCleanup(wid models.WorkflowId, fn cleanupFunc) { 412 + e.cleanupMu.Lock() 413 + defer e.cleanupMu.Unlock() 414 + 415 + key := wid.String() 416 + e.cleanup[key] = append(e.cleanup[key], fn) 417 + } 418 + 419 + func networkName(wid models.WorkflowId) string { 420 + return fmt.Sprintf("workflow-network-%s", wid) 421 + }
+28
spindle/engines/nixery/envs.go
··· 1 + package nixery 2 + 3 + import ( 4 + "fmt" 5 + ) 6 + 7 + type EnvVars []string 8 + 9 + // ConstructEnvs converts a tangled.Pipeline_Step_Environment_Elem.{Key,Value} 10 + // representation into a docker-friendly []string{"KEY=value", ...} slice. 11 + func ConstructEnvs(envs map[string]string) EnvVars { 12 + var dockerEnvs EnvVars 13 + for k, v := range envs { 14 + ev := fmt.Sprintf("%s=%s", k, v) 15 + dockerEnvs = append(dockerEnvs, ev) 16 + } 17 + return dockerEnvs 18 + } 19 + 20 + // Slice returns the EnvVar as a []string slice. 21 + func (ev EnvVars) Slice() []string { 22 + return ev 23 + } 24 + 25 + // AddEnv adds a key=value string to the EnvVar. 26 + func (ev *EnvVars) AddEnv(key, value string) { 27 + *ev = append(*ev, fmt.Sprintf("%s=%s", key, value)) 28 + }
+48
spindle/engines/nixery/envs_test.go
··· 1 + package nixery 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/stretchr/testify/assert" 7 + ) 8 + 9 + func TestConstructEnvs(t *testing.T) { 10 + tests := []struct { 11 + name string 12 + in map[string]string 13 + want EnvVars 14 + }{ 15 + { 16 + name: "empty input", 17 + in: make(map[string]string), 18 + want: EnvVars{}, 19 + }, 20 + { 21 + name: "single env var", 22 + in: map[string]string{"FOO": "bar"}, 23 + want: EnvVars{"FOO=bar"}, 24 + }, 25 + { 26 + name: "multiple env vars", 27 + in: map[string]string{"FOO": "bar", "BAZ": "qux"}, 28 + want: EnvVars{"FOO=bar", "BAZ=qux"}, 29 + }, 30 + } 31 + for _, tt := range tests { 32 + t.Run(tt.name, func(t *testing.T) { 33 + got := ConstructEnvs(tt.in) 34 + if got == nil { 35 + got = EnvVars{} 36 + } 37 + assert.ElementsMatch(t, tt.want, got) 38 + }) 39 + } 40 + } 41 + 42 + func TestAddEnv(t *testing.T) { 43 + ev := EnvVars{} 44 + ev.AddEnv("FOO", "bar") 45 + ev.AddEnv("BAZ", "qux") 46 + want := EnvVars{"FOO=bar", "BAZ=qux"} 47 + assert.ElementsMatch(t, want, ev) 48 + }
+7
spindle/engines/nixery/errors.go
··· 1 + package nixery 2 + 3 + import "errors" 4 + 5 + var ( 6 + ErrOOMKilled = errors.New("oom killed") 7 + )
+126
spindle/engines/nixery/setup_steps.go
··· 1 + package nixery 2 + 3 + import ( 4 + "fmt" 5 + "path" 6 + "strings" 7 + 8 + "tangled.sh/tangled.sh/core/api/tangled" 9 + "tangled.sh/tangled.sh/core/workflow" 10 + ) 11 + 12 + func nixConfStep() Step { 13 + setupCmd := `mkdir -p /etc/nix 14 + echo 'extra-experimental-features = nix-command flakes' >> /etc/nix/nix.conf 15 + echo 'build-users-group = ' >> /etc/nix/nix.conf` 16 + return Step{ 17 + command: setupCmd, 18 + name: "Configure Nix", 19 + } 20 + } 21 + 22 + // cloneOptsAsSteps processes clone options and adds corresponding steps 23 + // to the beginning of the workflow's step list if cloning is not skipped. 24 + // 25 + // the steps to do here are: 26 + // - git init 27 + // - git remote add origin <url> 28 + // - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha> 29 + // - git checkout FETCH_HEAD 30 + func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step { 31 + if twf.Clone.Skip { 32 + return Step{} 33 + } 34 + 35 + var commands []string 36 + 37 + // initialize git repo in workspace 38 + commands = append(commands, "git init") 39 + 40 + // add repo as git remote 41 + scheme := "https://" 42 + if dev { 43 + scheme = "http://" 44 + tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal") 45 + } 46 + url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo) 47 + commands = append(commands, fmt.Sprintf("git remote add origin %s", url)) 48 + 49 + // run git fetch 50 + { 51 + var fetchArgs []string 52 + 53 + // default clone depth is 1 54 + depth := 1 55 + if twf.Clone.Depth > 1 { 56 + depth = int(twf.Clone.Depth) 57 + } 58 + fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth)) 59 + 60 + // optionally recurse submodules 61 + if twf.Clone.Submodules { 62 + fetchArgs = append(fetchArgs, "--recurse-submodules=yes") 63 + } 64 + 65 + // set remote to fetch from 66 + fetchArgs = append(fetchArgs, "origin") 67 + 68 + // set revision to checkout 69 + switch workflow.TriggerKind(tr.Kind) { 70 + case workflow.TriggerKindManual: 71 + // TODO: unimplemented 72 + case workflow.TriggerKindPush: 73 + fetchArgs = append(fetchArgs, tr.Push.NewSha) 74 + case workflow.TriggerKindPullRequest: 75 + fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha) 76 + } 77 + 78 + commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " "))) 79 + } 80 + 81 + // run git checkout 82 + commands = append(commands, "git checkout FETCH_HEAD") 83 + 84 + cloneStep := Step{ 85 + command: strings.Join(commands, "\n"), 86 + name: "Clone repository into workspace", 87 + } 88 + return cloneStep 89 + } 90 + 91 + // dependencyStep processes dependencies defined in the workflow. 92 + // For dependencies using a custom registry (i.e. not nixpkgs), it collects 93 + // all packages and adds a single 'nix profile install' step to the 94 + // beginning of the workflow's step list. 95 + func dependencyStep(deps map[string][]string) *Step { 96 + var customPackages []string 97 + 98 + for registry, packages := range deps { 99 + if registry == "nixpkgs" { 100 + continue 101 + } 102 + 103 + if len(packages) == 0 { 104 + customPackages = append(customPackages, registry) 105 + } 106 + // collect packages from custom registries 107 + for _, pkg := range packages { 108 + customPackages = append(customPackages, fmt.Sprintf("'%s#%s'", registry, pkg)) 109 + } 110 + } 111 + 112 + if len(customPackages) > 0 { 113 + installCmd := "nix --extra-experimental-features nix-command --extra-experimental-features flakes profile install" 114 + cmd := fmt.Sprintf("%s %s", installCmd, strings.Join(customPackages, " ")) 115 + installStep := Step{ 116 + command: cmd, 117 + name: "Install custom dependencies", 118 + environment: map[string]string{ 119 + "NIX_NO_COLOR": "1", 120 + "NIX_SHOW_DOWNLOAD_PROGRESS": "0", 121 + }, 122 + } 123 + return &installStep 124 + } 125 + return nil 126 + }
+8 -4
spindle/ingester.go
··· 40 40 41 41 switch e.Commit.Collection { 42 42 case tangled.SpindleMemberNSID: 43 - s.ingestMember(ctx, e) 43 + err = s.ingestMember(ctx, e) 44 44 case tangled.RepoNSID: 45 - s.ingestRepo(ctx, e) 45 + err = s.ingestRepo(ctx, e) 46 46 case tangled.RepoCollaboratorNSID: 47 - s.ingestCollaborator(ctx, e) 47 + err = s.ingestCollaborator(ctx, e) 48 48 } 49 49 50 - return err 50 + if err != nil { 51 + s.l.Debug("failed to process message", "nsid", e.Commit.Collection, "err", err) 52 + } 53 + 54 + return nil 51 55 } 52 56 } 53 57
+17
spindle/models/engine.go
··· 1 + package models 2 + 3 + import ( 4 + "context" 5 + "time" 6 + 7 + "tangled.sh/tangled.sh/core/api/tangled" 8 + "tangled.sh/tangled.sh/core/spindle/secrets" 9 + ) 10 + 11 + type Engine interface { 12 + InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*Workflow, error) 13 + SetupWorkflow(ctx context.Context, wid WorkflowId, wf *Workflow) error 14 + WorkflowTimeout() time.Duration 15 + DestroyWorkflow(ctx context.Context, wid WorkflowId) error 16 + RunStep(ctx context.Context, wid WorkflowId, w *Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *WorkflowLogger) error 17 + }
+82
spindle/models/logger.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "os" 8 + "path/filepath" 9 + "strings" 10 + ) 11 + 12 + type WorkflowLogger struct { 13 + file *os.File 14 + encoder *json.Encoder 15 + } 16 + 17 + func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) { 18 + path := LogFilePath(baseDir, wid) 19 + 20 + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 21 + if err != nil { 22 + return nil, fmt.Errorf("creating log file: %w", err) 23 + } 24 + 25 + return &WorkflowLogger{ 26 + file: file, 27 + encoder: json.NewEncoder(file), 28 + }, nil 29 + } 30 + 31 + func LogFilePath(baseDir string, workflowID WorkflowId) string { 32 + logFilePath := filepath.Join(baseDir, fmt.Sprintf("%s.log", workflowID.String())) 33 + return logFilePath 34 + } 35 + 36 + func (l *WorkflowLogger) Close() error { 37 + return l.file.Close() 38 + } 39 + 40 + func (l *WorkflowLogger) DataWriter(stream string) io.Writer { 41 + // TODO: emit stream 42 + return &dataWriter{ 43 + logger: l, 44 + stream: stream, 45 + } 46 + } 47 + 48 + func (l *WorkflowLogger) ControlWriter(idx int, step Step) io.Writer { 49 + return &controlWriter{ 50 + logger: l, 51 + idx: idx, 52 + step: step, 53 + } 54 + } 55 + 56 + type dataWriter struct { 57 + logger *WorkflowLogger 58 + stream string 59 + } 60 + 61 + func (w *dataWriter) Write(p []byte) (int, error) { 62 + line := strings.TrimRight(string(p), "\r\n") 63 + entry := NewDataLogLine(line, w.stream) 64 + if err := w.logger.encoder.Encode(entry); err != nil { 65 + return 0, err 66 + } 67 + return len(p), nil 68 + } 69 + 70 + type controlWriter struct { 71 + logger *WorkflowLogger 72 + idx int 73 + step Step 74 + } 75 + 76 + func (w *controlWriter) Write(_ []byte) (int, error) { 77 + entry := NewControlLogLine(w.idx, w.step) 78 + if err := w.logger.encoder.Encode(entry); err != nil { 79 + return 0, err 80 + } 81 + return len(w.step.Name()), nil 82 + }
+3 -3
spindle/models/models.go
··· 104 104 func NewControlLogLine(idx int, step Step) LogLine { 105 105 return LogLine{ 106 106 Kind: LogKindControl, 107 - Content: step.Name, 107 + Content: step.Name(), 108 108 StepId: idx, 109 - StepKind: step.Kind, 110 - StepCommand: step.Command, 109 + StepKind: step.Kind(), 110 + StepCommand: step.Command(), 111 111 } 112 112 }
+8 -103
spindle/models/pipeline.go
··· 1 1 package models 2 2 3 - import ( 4 - "path" 5 - 6 - "tangled.sh/tangled.sh/core/api/tangled" 7 - "tangled.sh/tangled.sh/core/spindle/config" 8 - ) 9 - 10 3 type Pipeline struct { 11 4 RepoOwner string 12 5 RepoName string 13 - Workflows []Workflow 6 + Workflows map[Engine][]Workflow 14 7 } 15 8 16 - type Step struct { 17 - Command string 18 - Name string 19 - Environment map[string]string 20 - Kind StepKind 9 + type Step interface { 10 + Name() string 11 + Command() string 12 + Kind() StepKind 21 13 } 22 14 23 15 type StepKind int ··· 30 22 ) 31 23 32 24 type Workflow struct { 33 - Steps []Step 34 - Environment map[string]string 35 - Name string 36 - Image string 37 - } 38 - 39 - // setupSteps get added to start of Steps 40 - type setupSteps []Step 41 - 42 - // addStep adds a step to the beginning of the workflow's steps. 43 - func (ss *setupSteps) addStep(step Step) { 44 - *ss = append(*ss, step) 45 - } 46 - 47 - // ToPipeline converts a tangled.Pipeline into a model.Pipeline. 48 - // In the process, dependencies are resolved: nixpkgs deps 49 - // are constructed atop nixery and set as the Workflow.Image, 50 - // and ones from custom registries 51 - func ToPipeline(pl tangled.Pipeline, cfg config.Config) *Pipeline { 52 - workflows := []Workflow{} 53 - 54 - for _, twf := range pl.Workflows { 55 - swf := &Workflow{} 56 - for _, tstep := range twf.Steps { 57 - sstep := Step{} 58 - sstep.Environment = stepEnvToMap(tstep.Environment) 59 - sstep.Command = tstep.Command 60 - sstep.Name = tstep.Name 61 - sstep.Kind = StepKindUser 62 - swf.Steps = append(swf.Steps, sstep) 63 - } 64 - swf.Name = twf.Name 65 - swf.Environment = workflowEnvToMap(twf.Environment) 66 - swf.Image = workflowImage(twf.Dependencies, cfg.Pipelines.Nixery) 67 - 68 - setup := &setupSteps{} 69 - 70 - setup.addStep(nixConfStep()) 71 - setup.addStep(cloneStep(*twf, *pl.TriggerMetadata, cfg.Server.Dev)) 72 - // this step could be empty 73 - if s := dependencyStep(*twf); s != nil { 74 - setup.addStep(*s) 75 - } 76 - 77 - // append setup steps in order to the start of workflow steps 78 - swf.Steps = append(*setup, swf.Steps...) 79 - 80 - workflows = append(workflows, *swf) 81 - } 82 - repoOwner := pl.TriggerMetadata.Repo.Did 83 - repoName := pl.TriggerMetadata.Repo.Repo 84 - return &Pipeline{ 85 - RepoOwner: repoOwner, 86 - RepoName: repoName, 87 - Workflows: workflows, 88 - } 89 - } 90 - 91 - func workflowEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string { 92 - envMap := map[string]string{} 93 - for _, env := range envs { 94 - if env != nil { 95 - envMap[env.Key] = env.Value 96 - } 97 - } 98 - return envMap 99 - } 100 - 101 - func stepEnvToMap(envs []*tangled.Pipeline_Pair) map[string]string { 102 - envMap := map[string]string{} 103 - for _, env := range envs { 104 - if env != nil { 105 - envMap[env.Key] = env.Value 106 - } 107 - } 108 - return envMap 109 - } 110 - 111 - func workflowImage(deps []*tangled.Pipeline_Dependency, nixery string) string { 112 - var dependencies string 113 - for _, d := range deps { 114 - if d.Registry == "nixpkgs" { 115 - dependencies = path.Join(d.Packages...) 116 - } 117 - } 118 - 119 - // load defaults from somewhere else 120 - dependencies = path.Join(dependencies, "bash", "git", "coreutils", "nix") 121 - 122 - return path.Join(nixery, dependencies) 25 + Steps []Step 26 + Name string 27 + Data any 123 28 }
-128
spindle/models/setup_steps.go
··· 1 - package models 2 - 3 - import ( 4 - "fmt" 5 - "path" 6 - "strings" 7 - 8 - "tangled.sh/tangled.sh/core/api/tangled" 9 - "tangled.sh/tangled.sh/core/workflow" 10 - ) 11 - 12 - func nixConfStep() Step { 13 - setupCmd := `echo 'extra-experimental-features = nix-command flakes' >> /etc/nix/nix.conf 14 - echo 'build-users-group = ' >> /etc/nix/nix.conf` 15 - return Step{ 16 - Command: setupCmd, 17 - Name: "Configure Nix", 18 - } 19 - } 20 - 21 - // cloneOptsAsSteps processes clone options and adds corresponding steps 22 - // to the beginning of the workflow's step list if cloning is not skipped. 23 - // 24 - // the steps to do here are: 25 - // - git init 26 - // - git remote add origin <url> 27 - // - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha> 28 - // - git checkout FETCH_HEAD 29 - func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step { 30 - if twf.Clone.Skip { 31 - return Step{} 32 - } 33 - 34 - var commands []string 35 - 36 - // initialize git repo in workspace 37 - commands = append(commands, "git init") 38 - 39 - // add repo as git remote 40 - scheme := "https://" 41 - if dev { 42 - scheme = "http://" 43 - tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal") 44 - } 45 - url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo) 46 - commands = append(commands, fmt.Sprintf("git remote add origin %s", url)) 47 - 48 - // run git fetch 49 - { 50 - var fetchArgs []string 51 - 52 - // default clone depth is 1 53 - depth := 1 54 - if twf.Clone.Depth > 1 { 55 - depth = int(twf.Clone.Depth) 56 - } 57 - fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth)) 58 - 59 - // optionally recurse submodules 60 - if twf.Clone.Submodules { 61 - fetchArgs = append(fetchArgs, "--recurse-submodules=yes") 62 - } 63 - 64 - // set remote to fetch from 65 - fetchArgs = append(fetchArgs, "origin") 66 - 67 - // set revision to checkout 68 - switch workflow.TriggerKind(tr.Kind) { 69 - case workflow.TriggerKindManual: 70 - // TODO: unimplemented 71 - case workflow.TriggerKindPush: 72 - fetchArgs = append(fetchArgs, tr.Push.NewSha) 73 - case workflow.TriggerKindPullRequest: 74 - fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha) 75 - } 76 - 77 - commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " "))) 78 - } 79 - 80 - // run git checkout 81 - commands = append(commands, "git checkout FETCH_HEAD") 82 - 83 - cloneStep := Step{ 84 - Command: strings.Join(commands, "\n"), 85 - Name: "Clone repository into workspace", 86 - } 87 - return cloneStep 88 - } 89 - 90 - // dependencyStep processes dependencies defined in the workflow. 91 - // For dependencies using a custom registry (i.e. not nixpkgs), it collects 92 - // all packages and adds a single 'nix profile install' step to the 93 - // beginning of the workflow's step list. 94 - func dependencyStep(twf tangled.Pipeline_Workflow) *Step { 95 - var customPackages []string 96 - 97 - for _, d := range twf.Dependencies { 98 - registry := d.Registry 99 - packages := d.Packages 100 - 101 - if registry == "nixpkgs" { 102 - continue 103 - } 104 - 105 - if len(packages) == 0 { 106 - customPackages = append(customPackages, registry) 107 - } 108 - // collect packages from custom registries 109 - for _, pkg := range packages { 110 - customPackages = append(customPackages, fmt.Sprintf("'%s#%s'", registry, pkg)) 111 - } 112 - } 113 - 114 - if len(customPackages) > 0 { 115 - installCmd := "nix --extra-experimental-features nix-command --extra-experimental-features flakes profile install" 116 - cmd := fmt.Sprintf("%s %s", installCmd, strings.Join(customPackages, " ")) 117 - installStep := Step{ 118 - Command: cmd, 119 - Name: "Install custom dependencies", 120 - Environment: map[string]string{ 121 - "NIX_NO_COLOR": "1", 122 - "NIX_SHOW_DOWNLOAD_PROGRESS": "0", 123 - }, 124 - } 125 - return &installStep 126 - } 127 - return nil 128 - }
+1 -1
spindle/secrets/sqlite.go
··· 24 24 } 25 25 26 26 func NewSQLiteManager(dbPath string, opts ...SqliteManagerOpt) (*SqliteManager, error) { 27 - db, err := sql.Open("sqlite3", dbPath) 27 + db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=1") 28 28 if err != nil { 29 29 return nil, fmt.Errorf("failed to open sqlite database: %w", err) 30 30 }
+38 -8
spindle/server.go
··· 20 20 "tangled.sh/tangled.sh/core/spindle/config" 21 21 "tangled.sh/tangled.sh/core/spindle/db" 22 22 "tangled.sh/tangled.sh/core/spindle/engine" 23 + "tangled.sh/tangled.sh/core/spindle/engines/nixery" 23 24 "tangled.sh/tangled.sh/core/spindle/models" 24 25 "tangled.sh/tangled.sh/core/spindle/queue" 25 26 "tangled.sh/tangled.sh/core/spindle/secrets" ··· 40 41 e *rbac.Enforcer 41 42 l *slog.Logger 42 43 n *notifier.Notifier 43 - eng *engine.Engine 44 + engs map[string]models.Engine 44 45 jq *queue.Queue 45 46 cfg *config.Config 46 47 ks *eventconsumer.Consumer ··· 94 95 return fmt.Errorf("unknown secrets provider: %s", cfg.Server.Secrets.Provider) 95 96 } 96 97 97 - eng, err := engine.New(ctx, cfg, d, &n, vault) 98 + nixeryEng, err := nixery.New(ctx, cfg) 98 99 if err != nil { 99 100 return err 100 101 } ··· 129 130 db: d, 130 131 l: logger, 131 132 n: &n, 132 - eng: eng, 133 + engs: map[string]models.Engine{"nixery": nixeryEng}, 133 134 jq: jq, 134 135 cfg: cfg, 135 136 res: resolver, ··· 219 220 Logger: logger, 220 221 Db: s.db, 221 222 Enforcer: s.e, 222 - Engine: s.eng, 223 + Engines: s.engs, 223 224 Config: s.cfg, 224 225 Resolver: s.res, 225 226 Vault: s.vault, ··· 265 266 Rkey: msg.Rkey, 266 267 } 267 268 269 + workflows := make(map[models.Engine][]models.Workflow) 270 + 268 271 for _, w := range tpl.Workflows { 269 272 if w != nil { 270 - err := s.db.StatusPending(models.WorkflowId{ 273 + if _, ok := s.engs[w.Engine]; !ok { 274 + err = s.db.StatusFailed(models.WorkflowId{ 275 + PipelineId: pipelineId, 276 + Name: w.Name, 277 + }, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n) 278 + if err != nil { 279 + return err 280 + } 281 + 282 + continue 283 + } 284 + 285 + eng := s.engs[w.Engine] 286 + 287 + if _, ok := workflows[eng]; !ok { 288 + workflows[eng] = []models.Workflow{} 289 + } 290 + 291 + ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl) 292 + if err != nil { 293 + return err 294 + } 295 + 296 + workflows[eng] = append(workflows[eng], *ewf) 297 + 298 + err = s.db.StatusPending(models.WorkflowId{ 271 299 PipelineId: pipelineId, 272 300 Name: w.Name, 273 301 }, s.n) ··· 277 305 } 278 306 } 279 307 280 - spl := models.ToPipeline(tpl, *s.cfg) 281 - 282 308 ok := s.jq.Enqueue(queue.Job{ 283 309 Run: func() error { 284 - s.eng.StartWorkflows(ctx, spl, pipelineId) 310 + engine.StartWorkflows(s.l, s.vault, s.cfg, s.db, s.n, ctx, &models.Pipeline{ 311 + RepoOwner: tpl.TriggerMetadata.Repo.Did, 312 + RepoName: tpl.TriggerMetadata.Repo.Repo, 313 + Workflows: workflows, 314 + }, pipelineId) 285 315 return nil 286 316 }, 287 317 OnFail: func(jobError error) {
+32 -2
spindle/stream.go
··· 6 6 "fmt" 7 7 "io" 8 8 "net/http" 9 + "os" 9 10 "strconv" 10 11 "time" 11 12 12 - "tangled.sh/tangled.sh/core/spindle/engine" 13 13 "tangled.sh/tangled.sh/core/spindle/models" 14 14 15 15 "github.com/go-chi/chi/v5" ··· 143 143 } 144 144 isFinished := models.StatusKind(status.Status).IsFinish() 145 145 146 - filePath := engine.LogFilePath(s.cfg.Pipelines.LogDir, wid) 146 + filePath := models.LogFilePath(s.cfg.Server.LogDir, wid) 147 + 148 + if status.Status == models.StatusKindFailed.String() && status.Error != nil { 149 + if _, err := os.Stat(filePath); os.IsNotExist(err) { 150 + msgs := []models.LogLine{ 151 + { 152 + Kind: models.LogKindControl, 153 + Content: "", 154 + StepId: 0, 155 + StepKind: models.StepKindUser, 156 + }, 157 + { 158 + Kind: models.LogKindData, 159 + Content: *status.Error, 160 + }, 161 + } 162 + 163 + for _, msg := range msgs { 164 + b, err := json.Marshal(msg) 165 + if err != nil { 166 + return err 167 + } 168 + 169 + if err := conn.WriteMessage(websocket.TextMessage, b); err != nil { 170 + return fmt.Errorf("failed to write to websocket: %w", err) 171 + } 172 + } 173 + 174 + return nil 175 + } 176 + } 147 177 148 178 config := tail.Config{ 149 179 Follow: !isFinished,
+2 -2
spindle/xrpc/xrpc.go
··· 13 13 "tangled.sh/tangled.sh/core/rbac" 14 14 "tangled.sh/tangled.sh/core/spindle/config" 15 15 "tangled.sh/tangled.sh/core/spindle/db" 16 - "tangled.sh/tangled.sh/core/spindle/engine" 16 + "tangled.sh/tangled.sh/core/spindle/models" 17 17 "tangled.sh/tangled.sh/core/spindle/secrets" 18 18 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 19 19 "tangled.sh/tangled.sh/core/xrpc/serviceauth" ··· 25 25 Logger *slog.Logger 26 26 Db *db.DB 27 27 Enforcer *rbac.Enforcer 28 - Engine *engine.Engine 28 + Engines map[string]models.Engine 29 29 Config *config.Config 30 30 Resolver *idresolver.Resolver 31 31 Vault secrets.Manager
+1 -9
tailwind.config.js
··· 36 36 css: { 37 37 maxWidth: "none", 38 38 pre: { 39 - backgroundColor: colors.gray[100], 40 - color: colors.black, 41 - "@apply font-normal text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": {}, 42 - }, 43 - li: { 44 - "@apply inline-block w-full my-0 py-0": {}, 45 - }, 46 - "ul, ol": { 47 - "@apply my-1 py-0": {}, 39 + "@apply font-normal text-black bg-gray-50 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 border": {}, 48 40 }, 49 41 code: { 50 42 "@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {},
+62 -41
workflow/compile.go
··· 1 1 package workflow 2 2 3 3 import ( 4 + "errors" 4 5 "fmt" 5 6 6 7 "tangled.sh/tangled.sh/core/api/tangled" 7 8 ) 8 9 10 + type RawWorkflow struct { 11 + Name string 12 + Contents []byte 13 + } 14 + 15 + type RawPipeline = []RawWorkflow 16 + 9 17 type Compiler struct { 10 18 Trigger tangled.Pipeline_TriggerMetadata 11 19 Diagnostics Diagnostics 12 20 } 13 21 14 22 type Diagnostics struct { 15 - Errors []error 23 + Errors []Error 16 24 Warnings []Warning 17 25 } 18 26 27 + func (d *Diagnostics) IsEmpty() bool { 28 + return len(d.Errors) == 0 && len(d.Warnings) == 0 29 + } 30 + 19 31 func (d *Diagnostics) Combine(o Diagnostics) { 20 32 d.Errors = append(d.Errors, o.Errors...) 21 33 d.Warnings = append(d.Warnings, o.Warnings...) ··· 25 37 d.Warnings = append(d.Warnings, Warning{path, kind, reason}) 26 38 } 27 39 28 - func (d *Diagnostics) AddError(err error) { 29 - d.Errors = append(d.Errors, err) 40 + func (d *Diagnostics) AddError(path string, err error) { 41 + d.Errors = append(d.Errors, Error{path, err}) 30 42 } 31 43 32 44 func (d Diagnostics) IsErr() bool { 33 45 return len(d.Errors) != 0 34 46 } 35 47 48 + type Error struct { 49 + Path string 50 + Error error 51 + } 52 + 53 + func (e Error) String() string { 54 + return fmt.Sprintf("error: %s: %s", e.Path, e.Error.Error()) 55 + } 56 + 36 57 type Warning struct { 37 58 Path string 38 59 Type WarningKind 39 60 Reason string 40 61 } 41 62 63 + func (w Warning) String() string { 64 + return fmt.Sprintf("warning: %s: %s: %s", w.Path, w.Type, w.Reason) 65 + } 66 + 67 + var ( 68 + MissingEngine error = errors.New("missing engine") 69 + ) 70 + 42 71 type WarningKind string 43 72 44 73 var ( ··· 46 75 InvalidConfiguration WarningKind = "invalid configuration" 47 76 ) 48 77 78 + func (compiler *Compiler) Parse(p RawPipeline) Pipeline { 79 + var pp Pipeline 80 + 81 + for _, w := range p { 82 + wf, err := FromFile(w.Name, w.Contents) 83 + if err != nil { 84 + compiler.Diagnostics.AddError(w.Name, err) 85 + continue 86 + } 87 + 88 + pp = append(pp, wf) 89 + } 90 + 91 + return pp 92 + } 93 + 49 94 // convert a repositories' workflow files into a fully compiled pipeline that runners accept 50 95 func (compiler *Compiler) Compile(p Pipeline) tangled.Pipeline { 51 96 cp := tangled.Pipeline{ 52 97 TriggerMetadata: &compiler.Trigger, 53 98 } 54 99 55 - for _, w := range p { 56 - cw := compiler.compileWorkflow(w) 100 + for _, wf := range p { 101 + cw := compiler.compileWorkflow(wf) 57 102 58 - // empty workflows are not added to the pipeline 59 - if len(cw.Steps) == 0 { 103 + if cw == nil { 60 104 continue 61 105 } 62 106 63 - cp.Workflows = append(cp.Workflows, &cw) 107 + cp.Workflows = append(cp.Workflows, cw) 64 108 } 65 109 66 110 return cp 67 111 } 68 112 69 - func (compiler *Compiler) compileWorkflow(w Workflow) tangled.Pipeline_Workflow { 70 - cw := tangled.Pipeline_Workflow{} 113 + func (compiler *Compiler) compileWorkflow(w Workflow) *tangled.Pipeline_Workflow { 114 + cw := &tangled.Pipeline_Workflow{} 71 115 72 116 if !w.Match(compiler.Trigger) { 73 117 compiler.Diagnostics.AddWarning( ··· 75 119 WorkflowSkipped, 76 120 fmt.Sprintf("did not match trigger %s", compiler.Trigger.Kind), 77 121 ) 78 - return cw 79 - } 80 - 81 - if len(w.Steps) == 0 { 82 - compiler.Diagnostics.AddWarning( 83 - w.Name, 84 - WorkflowSkipped, 85 - "empty workflow", 86 - ) 87 - return cw 122 + return nil 88 123 } 89 124 90 125 // validate clone options 91 126 compiler.analyzeCloneOptions(w) 92 127 93 128 cw.Name = w.Name 94 - cw.Dependencies = w.Dependencies.AsRecord() 95 - for _, s := range w.Steps { 96 - step := tangled.Pipeline_Step{ 97 - Command: s.Command, 98 - Name: s.Name, 99 - } 100 - for k, v := range s.Environment { 101 - e := &tangled.Pipeline_Pair{ 102 - Key: k, 103 - Value: v, 104 - } 105 - step.Environment = append(step.Environment, e) 106 - } 107 - cw.Steps = append(cw.Steps, &step) 129 + 130 + if w.Engine == "" { 131 + compiler.Diagnostics.AddError(w.Name, MissingEngine) 132 + return nil 108 133 } 109 - for k, v := range w.Environment { 110 - e := &tangled.Pipeline_Pair{ 111 - Key: k, 112 - Value: v, 113 - } 114 - cw.Environment = append(cw.Environment, e) 115 - } 134 + 135 + cw.Engine = w.Engine 136 + cw.Raw = w.Raw 116 137 117 138 o := w.CloneOpts.AsRecord() 118 139 cw.Clone = &o
+23 -29
workflow/compile_test.go
··· 26 26 27 27 func TestCompileWorkflow_MatchingWorkflowWithSteps(t *testing.T) { 28 28 wf := Workflow{ 29 - Name: ".tangled/workflows/test.yml", 30 - When: when, 31 - Steps: []Step{ 32 - {Name: "Test", Command: "go test ./..."}, 33 - }, 29 + Name: ".tangled/workflows/test.yml", 30 + Engine: "nixery", 31 + When: when, 34 32 CloneOpts: CloneOpts{}, // default true 35 33 } 36 34 ··· 43 41 assert.False(t, c.Diagnostics.IsErr()) 44 42 } 45 43 46 - func TestCompileWorkflow_EmptySteps(t *testing.T) { 47 - wf := Workflow{ 48 - Name: ".tangled/workflows/empty.yml", 49 - When: when, 50 - Steps: []Step{}, // no steps 51 - } 52 - 53 - c := Compiler{Trigger: trigger} 54 - cp := c.Compile([]Workflow{wf}) 55 - 56 - assert.Len(t, cp.Workflows, 0) 57 - assert.Len(t, c.Diagnostics.Warnings, 1) 58 - assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type) 59 - } 60 - 61 44 func TestCompileWorkflow_TriggerMismatch(t *testing.T) { 62 45 wf := Workflow{ 63 - Name: ".tangled/workflows/mismatch.yml", 46 + Name: ".tangled/workflows/mismatch.yml", 47 + Engine: "nixery", 64 48 When: []Constraint{ 65 49 { 66 50 Event: []string{"push"}, 67 51 Branch: []string{"master"}, // different branch 68 52 }, 69 53 }, 70 - Steps: []Step{ 71 - {Name: "Lint", Command: "golint ./..."}, 72 - }, 73 54 } 74 55 75 56 c := Compiler{Trigger: trigger} ··· 82 63 83 64 func TestCompileWorkflow_CloneFalseWithShallowTrue(t *testing.T) { 84 65 wf := Workflow{ 85 - Name: ".tangled/workflows/clone_skip.yml", 86 - When: when, 87 - Steps: []Step{ 88 - {Name: "Skip", Command: "echo skip"}, 89 - }, 66 + Name: ".tangled/workflows/clone_skip.yml", 67 + Engine: "nixery", 68 + When: when, 90 69 CloneOpts: CloneOpts{ 91 70 Skip: true, 92 71 Depth: 1, ··· 101 80 assert.Len(t, c.Diagnostics.Warnings, 1) 102 81 assert.Equal(t, InvalidConfiguration, c.Diagnostics.Warnings[0].Type) 103 82 } 83 + 84 + func TestCompileWorkflow_MissingEngine(t *testing.T) { 85 + wf := Workflow{ 86 + Name: ".tangled/workflows/missing_engine.yml", 87 + When: when, 88 + Engine: "", 89 + } 90 + 91 + c := Compiler{Trigger: trigger} 92 + cp := c.Compile([]Workflow{wf}) 93 + 94 + assert.Len(t, cp.Workflows, 0) 95 + assert.Len(t, c.Diagnostics.Errors, 1) 96 + assert.Equal(t, MissingEngine, c.Diagnostics.Errors[0].Error) 97 + }
+6 -33
workflow/def.go
··· 24 24 25 25 // this is simply a structural representation of the workflow file 26 26 Workflow struct { 27 - Name string `yaml:"-"` // name of the workflow file 28 - When []Constraint `yaml:"when"` 29 - Dependencies Dependencies `yaml:"dependencies"` 30 - Steps []Step `yaml:"steps"` 31 - Environment map[string]string `yaml:"environment"` 32 - CloneOpts CloneOpts `yaml:"clone"` 27 + Name string `yaml:"-"` // name of the workflow file 28 + Engine string `yaml:"engine"` 29 + When []Constraint `yaml:"when"` 30 + CloneOpts CloneOpts `yaml:"clone"` 31 + Raw string `yaml:"-"` 33 32 } 34 33 35 34 Constraint struct { 36 35 Event StringList `yaml:"event"` 37 36 Branch StringList `yaml:"branch"` // this is optional, and only applied on "push" events 38 37 } 39 - 40 - Dependencies map[string][]string 41 38 42 39 CloneOpts struct { 43 40 Skip bool `yaml:"skip"` 44 41 Depth int `yaml:"depth"` 45 42 IncludeSubmodules bool `yaml:"submodules"` 46 - } 47 - 48 - Step struct { 49 - Name string `yaml:"name"` 50 - Command string `yaml:"command"` 51 - Environment map[string]string `yaml:"environment"` 52 43 } 53 44 54 45 StringList []string ··· 77 68 } 78 69 79 70 wf.Name = name 71 + wf.Raw = string(contents) 80 72 81 73 return wf, nil 82 74 } ··· 173 165 } 174 166 175 167 return errors.New("failed to unmarshal StringOrSlice") 176 - } 177 - 178 - // conversion utilities to atproto records 179 - func (d Dependencies) AsRecord() []*tangled.Pipeline_Dependency { 180 - var deps []*tangled.Pipeline_Dependency 181 - for registry, packages := range d { 182 - deps = append(deps, &tangled.Pipeline_Dependency{ 183 - Registry: registry, 184 - Packages: packages, 185 - }) 186 - } 187 - return deps 188 - } 189 - 190 - func (s Step) AsRecord() tangled.Pipeline_Step { 191 - return tangled.Pipeline_Step{ 192 - Command: s.Command, 193 - Name: s.Name, 194 - } 195 168 } 196 169 197 170 func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts {
+1 -86
workflow/def_test.go
··· 10 10 yamlData := ` 11 11 when: 12 12 - event: ["push", "pull_request"] 13 - branch: ["main", "develop"] 14 - 15 - dependencies: 16 - nixpkgs: 17 - - go 18 - - git 19 - - curl 20 - 21 - steps: 22 - - name: "Test" 23 - command: | 24 - go test ./...` 13 + branch: ["main", "develop"]` 25 14 26 15 wf, err := FromFile("test.yml", []byte(yamlData)) 27 16 assert.NoError(t, err, "YAML should unmarshal without error") ··· 30 19 assert.ElementsMatch(t, []string{"main", "develop"}, wf.When[0].Branch) 31 20 assert.ElementsMatch(t, []string{"push", "pull_request"}, wf.When[0].Event) 32 21 33 - assert.Len(t, wf.Steps, 1) 34 - assert.Equal(t, "Test", wf.Steps[0].Name) 35 - assert.Equal(t, "go test ./...", wf.Steps[0].Command) 36 - 37 - pkgs, ok := wf.Dependencies["nixpkgs"] 38 - assert.True(t, ok, "`nixpkgs` should be present in dependencies") 39 - assert.ElementsMatch(t, []string{"go", "git", "curl"}, pkgs) 40 - 41 22 assert.False(t, wf.CloneOpts.Skip, "Skip should default to false") 42 23 } 43 24 44 - func TestUnmarshalCustomRegistry(t *testing.T) { 45 - yamlData := ` 46 - when: 47 - - event: push 48 - branch: main 49 - 50 - dependencies: 51 - git+https://tangled.sh/@oppi.li/tbsp: 52 - - tbsp 53 - git+https://git.peppe.rs/languages/statix: 54 - - statix 55 - 56 - steps: 57 - - name: "Check" 58 - command: | 59 - statix check` 60 - 61 - wf, err := FromFile("test.yml", []byte(yamlData)) 62 - assert.NoError(t, err, "YAML should unmarshal without error") 63 - 64 - assert.ElementsMatch(t, []string{"push"}, wf.When[0].Event) 65 - assert.ElementsMatch(t, []string{"main"}, wf.When[0].Branch) 66 - 67 - assert.ElementsMatch(t, []string{"tbsp"}, wf.Dependencies["git+https://tangled.sh/@oppi.li/tbsp"]) 68 - assert.ElementsMatch(t, []string{"statix"}, wf.Dependencies["git+https://git.peppe.rs/languages/statix"]) 69 - } 70 - 71 25 func TestUnmarshalCloneFalse(t *testing.T) { 72 26 yamlData := ` 73 27 when: ··· 75 29 76 30 clone: 77 31 skip: true 78 - 79 - dependencies: 80 - nixpkgs: 81 - - python3 82 - 83 - steps: 84 - - name: Notify 85 - command: | 86 - python3 ./notify.py 87 32 ` 88 33 89 34 wf, err := FromFile("test.yml", []byte(yamlData)) ··· 93 38 94 39 assert.True(t, wf.CloneOpts.Skip, "Skip should be false") 95 40 } 96 - 97 - func TestUnmarshalEnv(t *testing.T) { 98 - yamlData := ` 99 - when: 100 - - event: ["pull_request_close"] 101 - 102 - clone: 103 - skip: false 104 - 105 - environment: 106 - HOME: /home/foo bar/baz 107 - CGO_ENABLED: 1 108 - 109 - steps: 110 - - name: Something 111 - command: echo "hello" 112 - environment: 113 - FOO: bar 114 - BAZ: qux 115 - ` 116 - 117 - wf, err := FromFile("test.yml", []byte(yamlData)) 118 - assert.NoError(t, err) 119 - 120 - assert.Len(t, wf.Environment, 2) 121 - assert.Equal(t, "/home/foo bar/baz", wf.Environment["HOME"]) 122 - assert.Equal(t, "1", wf.Environment["CGO_ENABLED"]) 123 - assert.Equal(t, "bar", wf.Steps[0].Environment["FOO"]) 124 - assert.Equal(t, "qux", wf.Steps[0].Environment["BAZ"]) 125 - }
+7
xrpc/errors/errors.go
··· 86 86 ) 87 87 } 88 88 89 + var RecordExistsError = func(r string) XrpcError { 90 + return NewXrpcError( 91 + WithTag("RecordExists"), 92 + WithError(fmt.Errorf("repo already exists: %s", r)), 93 + ) 94 + } 95 + 89 96 func GenericError(err error) XrpcError { 90 97 return NewXrpcError( 91 98 WithTag("Generic"),