Nothing to see here

refactor: move lexicons into lexicon crate

Signed-off-by: tjh <did:plc:65gha4t3avpfpzmvpbwovss7>

tjh.dev 0ff3397f e3a14e1e

verified
+170 -133
Cargo.lock
··· 701 701 "libc", 702 702 "option-ext", 703 703 "redox_users", 704 - "windows-sys 0.61.0", 704 + "windows-sys 0.60.2", 705 705 ] 706 706 707 707 [[package]] ··· 821 821 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 822 822 dependencies = [ 823 823 "libc", 824 - "windows-sys 0.61.0", 824 + "windows-sys 0.60.2", 825 825 ] 826 826 827 827 [[package]] ··· 881 881 checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 882 882 dependencies = [ 883 883 "crc32fast", 884 - "libz-rs-sys", 885 884 "miniz_oxide", 886 885 ] 887 886 ··· 895 896 version = "0.1.5" 896 897 source = "registry+https://github.com/rust-lang/crates.io-index" 897 898 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 899 + 900 + [[package]] 901 + name = "foldhash" 902 + version = "0.2.0" 903 + source = "registry+https://github.com/rust-lang/crates.io-index" 904 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 898 905 899 906 [[package]] 900 907 name = "foreign-types" ··· 1027 1022 1028 1023 [[package]] 1029 1024 name = "gix" 1030 - version = "0.73.0" 1025 + version = "0.74.0" 1031 1026 source = "registry+https://github.com/rust-lang/crates.io-index" 1032 - checksum = "514c29cc879bdc0286b0cbc205585a49b252809eb86c69df4ce4f855ee75f635" 1027 + checksum = "d3fd3d8b30e15352c955ae99f3497a7ee5e346a6fa2662fc9301001691c4fb7e" 1033 1028 dependencies = [ 1034 1029 "gix-actor", 1035 1030 "gix-archive", 1036 1031 "gix-attributes", 1032 + "gix-blame", 1037 1033 "gix-command", 1038 1034 "gix-commitgraph", 1039 1035 "gix-config", ··· 1078 1072 "gix-worktree", 1079 1073 "gix-worktree-state", 1080 1074 "gix-worktree-stream", 1081 - "once_cell", 1082 1075 "parking_lot", 1083 1076 "regex", 1084 1077 "signal-hook", ··· 1087 1082 1088 1083 [[package]] 1089 1084 name = "gix-actor" 1090 - version = "0.35.4" 1085 + version = "0.35.5" 1091 1086 source = "registry+https://github.com/rust-lang/crates.io-index" 1092 - checksum = "2d36dcf9efe32b51b12dfa33cedff8414926124e760a32f9e7a6b5580d280967" 1087 + checksum = "575bb2dafa4e56721c0f7e8b2da647452e6c10c69b6157b118388a132cccb892" 1093 1088 dependencies = [ 1094 1089 "bstr", 1095 1090 "gix-date", ··· 1101 1096 1102 1097 [[package]] 1103 1098 name = "gix-archive" 1104 - version = "0.22.0" 1099 + version = "0.23.0" 1105 1100 source = "registry+https://github.com/rust-lang/crates.io-index" 1106 - checksum = "7be088a0e1b30abe15572ffafb3409172a3d88148e13959734f24f52112a19d6" 1101 + checksum = "015d177dcb7c2d9efc71ab9163cfa9cc83aa3c2409a2cd57d723a3c3cfbcb4c5" 1107 1102 dependencies = [ 1108 1103 "bstr", 1109 1104 "gix-date", ··· 1115 1110 1116 1111 [[package]] 1117 1112 name = "gix-attributes" 1118 - version = "0.27.0" 1113 + version = "0.28.0" 1119 1114 source = "registry+https://github.com/rust-lang/crates.io-index" 1120 - checksum = "45442188216d08a5959af195f659cb1f244a50d7d2d0c3873633b1cd7135f638" 1115 + checksum = "42bbbbe6e01d238f7cf1ce7f604d742e91639006f2ac1685b008cfa55903206d" 1121 1116 dependencies = [ 1122 1117 "bstr", 1123 1118 "gix-glob", ··· 1132 1127 1133 1128 [[package]] 1134 1129 name = "gix-bitmap" 1135 - version = "0.2.14" 1130 + version = "0.2.15" 1136 1131 source = "registry+https://github.com/rust-lang/crates.io-index" 1137 - checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540" 1132 + checksum = "5e150161b8a75b5860521cb876b506879a3376d3adc857ec7a9d35e7c6a5e531" 1138 1133 dependencies = [ 1139 1134 "thiserror", 1140 1135 ] 1141 1136 1142 1137 [[package]] 1143 - name = "gix-chunk" 1144 - version = "0.4.11" 1138 + name = "gix-blame" 1139 + version = "0.4.0" 1145 1140 source = "registry+https://github.com/rust-lang/crates.io-index" 1146 - checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f" 1141 + checksum = "260df64cea7bf3ab6db00e8f8cd8f1f85513d69c19fadd714422a39b8e8a8617" 1142 + dependencies = [ 1143 + "gix-commitgraph", 1144 + "gix-date", 1145 + "gix-diff", 1146 + "gix-hash", 1147 + "gix-object", 1148 + "gix-revwalk", 1149 + "gix-trace", 1150 + "gix-traverse", 1151 + "gix-worktree", 1152 + "smallvec", 1153 + "thiserror", 1154 + ] 1155 + 1156 + [[package]] 1157 + name = "gix-chunk" 1158 + version = "0.4.12" 1159 + source = "registry+https://github.com/rust-lang/crates.io-index" 1160 + checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" 1147 1161 dependencies = [ 1148 1162 "thiserror", 1149 1163 ] 1150 1164 1151 1165 [[package]] 1152 1166 name = "gix-command" 1153 - version = "0.6.2" 1167 + version = "0.6.3" 1154 1168 source = "registry+https://github.com/rust-lang/crates.io-index" 1155 - checksum = "6b31b65ca48a352ae86312b27a514a0c661935f96b481ac8b4371f65815eb196" 1169 + checksum = "095c8367c9dc4872a7706fbc39c7f34271b88b541120a4365ff0e36366f66e62" 1156 1170 dependencies = [ 1157 1171 "bstr", 1158 1172 "gix-path", ··· 1182 1158 1183 1159 [[package]] 1184 1160 name = "gix-commitgraph" 1185 - version = "0.29.0" 1161 + version = "0.30.0" 1186 1162 source = "registry+https://github.com/rust-lang/crates.io-index" 1187 - checksum = "6bb23121e952f43a5b07e3e80890336cb847297467a410475036242732980d06" 1163 + checksum = "5b7c0f17a94a27283afce5c1c5d7b1778cacc0a70bb8affc0e38611f06ffcb2b" 1188 1164 dependencies = [ 1189 1165 "bstr", 1190 1166 "gix-chunk", ··· 1195 1171 1196 1172 [[package]] 1197 1173 name = "gix-config" 1198 - version = "0.46.0" 1174 + version = "0.47.0" 1199 1175 source = "registry+https://github.com/rust-lang/crates.io-index" 1200 - checksum = "5dfb898c5b695fd4acfc3c0ab638525a65545d47706064dcf7b5ead6cdb136c0" 1176 + checksum = "76acbe8307019b17411a3c50e181e4dd97d78a840dead884a4f2dd35a8d38f6a" 1201 1177 dependencies = [ 1202 1178 "bstr", 1203 1179 "gix-config-value", ··· 1207 1183 "gix-ref", 1208 1184 "gix-sec", 1209 1185 "memchr", 1210 - "once_cell", 1211 1186 "smallvec", 1212 1187 "thiserror", 1213 1188 "unicode-bom", ··· 1215 1192 1216 1193 [[package]] 1217 1194 name = "gix-config-value" 1218 - version = "0.15.1" 1195 + version = "0.15.2" 1219 1196 source = "registry+https://github.com/rust-lang/crates.io-index" 1220 - checksum = "9f012703eb67e263c6c1fc96649fec47694dd3e5d2a91abfc65e4a6a6dc85309" 1197 + checksum = "7d837b93598b240624ed9b30f3a7629dc6088db60a025c90a628116a16d26719" 1221 1198 dependencies = [ 1222 1199 "bitflags", 1223 1200 "bstr", ··· 1228 1205 1229 1206 [[package]] 1230 1207 name = "gix-credentials" 1231 - version = "0.30.0" 1208 + version = "0.31.0" 1232 1209 source = "registry+https://github.com/rust-lang/crates.io-index" 1233 - checksum = "0039dd3ac606dd80b16353a41b61fc237ca5cb8b612f67a9f880adfad4be4e05" 1210 + checksum = "86a332d35e543005d61a7d517ccc32812476309e71aac02aef52525a501f1cae" 1234 1211 dependencies = [ 1235 1212 "bstr", 1236 1213 "gix-command", ··· 1246 1223 1247 1224 [[package]] 1248 1225 name = "gix-date" 1249 - version = "0.10.5" 1226 + version = "0.10.6" 1250 1227 source = "registry+https://github.com/rust-lang/crates.io-index" 1251 - checksum = "996b6b90bafb287330af92b274c3e64309dc78359221d8612d11cd10c8b9fe1c" 1228 + checksum = "36911ec97d0343048c6ecbcd2dcbcc6a9adf8272845417593915d1f8514b44ee" 1252 1229 dependencies = [ 1253 1230 "bstr", 1254 1231 "itoa", ··· 1259 1236 1260 1237 [[package]] 1261 1238 name = "gix-diff" 1262 - version = "0.53.0" 1239 + version = "0.54.0" 1263 1240 source = "registry+https://github.com/rust-lang/crates.io-index" 1264 - checksum = "de854852010d44a317f30c92d67a983e691c9478c8a3fb4117c1f48626bcdea8" 1241 + checksum = "b9844abf11a6dc5ab1d9e8670e17d35afbfebbebe216522454883140745bc68b" 1265 1242 dependencies = [ 1266 1243 "bstr", 1267 1244 "gix-attributes", ··· 1283 1260 1284 1261 [[package]] 1285 1262 name = "gix-dir" 1286 - version = "0.15.0" 1263 + version = "0.16.0" 1287 1264 source = "registry+https://github.com/rust-lang/crates.io-index" 1288 - checksum = "dad34e4f373f94902df1ba1d2a1df3a1b29eacd15e316ac5972d842e31422dd7" 1265 + checksum = "f99fb4dcba076453d791949bf3af977c5678a1cbd76740ec2cfe37e29431daf3" 1289 1266 dependencies = [ 1290 1267 "bstr", 1291 1268 "gix-discover", ··· 1303 1280 1304 1281 [[package]] 1305 1282 name = "gix-discover" 1306 - version = "0.41.0" 1283 + version = "0.42.0" 1307 1284 source = "registry+https://github.com/rust-lang/crates.io-index" 1308 - checksum = "ffb180c91ca1a2cf53e828bb63d8d8f8fa7526f49b83b33d7f46cbeb5d79d30a" 1285 + checksum = "9d24547153810634636471af88338240e6ab0831308cd41eb6ebfffea77811c6" 1309 1286 dependencies = [ 1310 1287 "bstr", 1311 1288 "dunce", ··· 1319 1296 1320 1297 [[package]] 1321 1298 name = "gix-features" 1322 - version = "0.43.1" 1299 + version = "0.44.0" 1323 1300 source = "registry+https://github.com/rust-lang/crates.io-index" 1324 - checksum = "cd1543cd9b8abcbcebaa1a666a5c168ee2cda4dea50d3961ee0e6d1c42f81e5b" 1301 + checksum = "4ff90017694214e1445b1baab8ca299e548cdf42c9d4588c02017c352d32652b" 1325 1302 dependencies = [ 1326 1303 "bytes", 1327 1304 "bytesize", 1328 1305 "crc32fast", 1329 1306 "crossbeam-channel", 1330 - "flate2", 1331 1307 "gix-path", 1332 1308 "gix-trace", 1333 1309 "gix-utils", 1334 1310 "libc", 1311 + "libz-rs-sys", 1335 1312 "once_cell", 1336 1313 "parking_lot", 1337 1314 "prodash", ··· 1341 1318 1342 1319 [[package]] 1343 1320 name = "gix-filter" 1344 - version = "0.20.0" 1321 + version = "0.21.0" 1345 1322 source = "registry+https://github.com/rust-lang/crates.io-index" 1346 - checksum = "aa6571a3927e7ab10f64279a088e0dae08e8da05547771796d7389bbe28ad9ff" 1323 + checksum = "1d1253452c9808da01eaaf9b1c4929b9982efec29ef0a668b3326b8046d9b8fb" 1347 1324 dependencies = [ 1348 1325 "bstr", 1349 1326 "encoding_rs", ··· 1362 1339 1363 1340 [[package]] 1364 1341 name = "gix-fs" 1365 - version = "0.16.1" 1342 + version = "0.17.0" 1366 1343 source = "registry+https://github.com/rust-lang/crates.io-index" 1367 - checksum = "9a4d90307d064fa7230e0f87b03231be28f8ba63b913fc15346f489519d0c304" 1344 + checksum = "3f1ecd896258cdc5ccd94d18386d17906b8de265ad2ecf68e3bea6b007f6a28f" 1368 1345 dependencies = [ 1369 1346 "bstr", 1370 1347 "fastrand", ··· 1376 1353 1377 1354 [[package]] 1378 1355 name = "gix-glob" 1379 - version = "0.21.0" 1356 + version = "0.22.0" 1380 1357 source = "registry+https://github.com/rust-lang/crates.io-index" 1381 - checksum = "b947db8366823e7a750c254f6bb29e27e17f27e457bf336ba79b32423db62cd5" 1358 + checksum = "56089c688b74d628bdd7f38c9cadf08d39948c783decc48474cbfdf341ba68c4" 1382 1359 dependencies = [ 1383 1360 "bitflags", 1384 1361 "bstr", ··· 1388 1365 1389 1366 [[package]] 1390 1367 name = "gix-hash" 1391 - version = "0.19.0" 1368 + version = "0.20.0" 1392 1369 source = "registry+https://github.com/rust-lang/crates.io-index" 1393 - checksum = "251fad79796a731a2a7664d9ea95ee29a9e99474de2769e152238d4fdb69d50e" 1370 + checksum = "0732910f9595d1600a6355f37241c1960a851a7250df0e04ad0dad85a20cbf73" 1394 1371 dependencies = [ 1395 1372 "faster-hex", 1396 1373 "gix-features", ··· 1400 1377 1401 1378 [[package]] 1402 1379 name = "gix-hashtable" 1403 - version = "0.9.0" 1380 + version = "0.10.0" 1404 1381 source = "registry+https://github.com/rust-lang/crates.io-index" 1405 - checksum = "c35300b54896153e55d53f4180460931ccd69b7e8d2f6b9d6401122cdedc4f07" 1382 + checksum = "a27d4a3ea9640da504a2657fef3419c517fd71f1767ad8935298bcc805edd195" 1406 1383 dependencies = [ 1407 1384 "gix-hash", 1408 - "hashbrown 0.15.5", 1385 + "hashbrown 0.16.0", 1409 1386 "parking_lot", 1410 1387 ] 1411 1388 1412 1389 [[package]] 1413 1390 name = "gix-ignore" 1414 - version = "0.16.0" 1391 + version = "0.17.0" 1415 1392 source = "registry+https://github.com/rust-lang/crates.io-index" 1416 - checksum = "564d6fddf46e2c981f571b23d6ad40cb08bddcaf6fc7458b1d49727ad23c2870" 1393 + checksum = "cd53028c6adea0b41e0ae35499542f164115c555b6c15b80778e4c22b148a3d0" 1417 1394 dependencies = [ 1418 1395 "bstr", 1419 1396 "gix-glob", ··· 1424 1401 1425 1402 [[package]] 1426 1403 name = "gix-index" 1427 - version = "0.41.0" 1404 + version = "0.42.0" 1428 1405 source = "registry+https://github.com/rust-lang/crates.io-index" 1429 - checksum = "2af39fde3ce4ce11371d9ce826f2936ec347318f2d1972fe98c2e7134e267e25" 1406 + checksum = "91c389a09b9575b6e856bf2199bd717f5bd5e37333c24575a0cf9d08c41d75d6" 1430 1407 dependencies = [ 1431 1408 "bitflags", 1432 1409 "bstr", ··· 1441 1418 "gix-traverse", 1442 1419 "gix-utils", 1443 1420 "gix-validate", 1444 - "hashbrown 0.15.5", 1421 + "hashbrown 0.16.0", 1445 1422 "itoa", 1446 1423 "libc", 1447 1424 "memmap2", ··· 1452 1429 1453 1430 [[package]] 1454 1431 name = "gix-lock" 1455 - version = "18.0.0" 1432 + version = "19.0.0" 1456 1433 source = "registry+https://github.com/rust-lang/crates.io-index" 1457 - checksum = "b9fa71da90365668a621e184eb5b979904471af1b3b09b943a84bc50e8ad42ed" 1434 + checksum = "729d7857429a66023bc0c29d60fa21d0d6ae8862f33c1937ba89e0f74dd5c67f" 1458 1435 dependencies = [ 1459 1436 "gix-tempfile", 1460 1437 "gix-utils", ··· 1463 1440 1464 1441 [[package]] 1465 1442 name = "gix-mailmap" 1466 - version = "0.27.2" 1443 + version = "0.27.3" 1467 1444 source = "registry+https://github.com/rust-lang/crates.io-index" 1468 - checksum = "9a8982e1874a2034d7dd481bcdd6a05579ba444bcda748511eb0f8e50eb10487" 1445 + checksum = "ef3b9794fa2545ab583e7fcd952900cdd8673fe51e57ee2a85b6728abb6bcb22" 1469 1446 dependencies = [ 1470 1447 "bstr", 1471 1448 "gix-actor", ··· 1475 1452 1476 1453 [[package]] 1477 1454 name = "gix-negotiate" 1478 - version = "0.21.0" 1455 + version = "0.22.0" 1479 1456 source = "registry+https://github.com/rust-lang/crates.io-index" 1480 - checksum = "1d58d4c9118885233be971e0d7a589f5cfb1a8bd6cb6e2ecfb0fc6b1b293c83b" 1457 + checksum = "89e16c96e052467d64c8f75a703b78976b33b034b9ff1f1d0c056c584319b0b8" 1481 1458 dependencies = [ 1482 1459 "bitflags", 1483 1460 "gix-commitgraph", ··· 1491 1468 1492 1469 [[package]] 1493 1470 name = "gix-object" 1494 - version = "0.50.2" 1471 + version = "0.51.0" 1495 1472 source = "registry+https://github.com/rust-lang/crates.io-index" 1496 - checksum = "d69ce108ab67b65fbd4fb7e1331502429d78baeb2eee10008bdef55765397c07" 1473 + checksum = "f4d23c75c112f03724792155ff847f12df6396e2528a5f2ed26eca2cf9a11896" 1497 1474 dependencies = [ 1498 1475 "bstr", 1499 1476 "gix-actor", ··· 1512 1489 1513 1490 [[package]] 1514 1491 name = "gix-odb" 1515 - version = "0.70.0" 1492 + version = "0.71.0" 1516 1493 source = "registry+https://github.com/rust-lang/crates.io-index" 1517 - checksum = "9c9d7af10fda9df0bb4f7f9bd507963560b3c66cb15a5b825caf752e0eb109ac" 1494 + checksum = "8c5f6d4ba857982cd2225de0a5d8b61d3b63a314395327f8a15943977203da08" 1518 1495 dependencies = [ 1519 1496 "arc-swap", 1520 1497 "gix-date", ··· 1533 1510 1534 1511 [[package]] 1535 1512 name = "gix-pack" 1536 - version = "0.60.0" 1513 + version = "0.61.0" 1537 1514 source = "registry+https://github.com/rust-lang/crates.io-index" 1538 - checksum = "d8571df89bfca5abb49c3e3372393f7af7e6f8b8dbe2b96303593cef5b263019" 1515 + checksum = "b3afaaa2205a477f5c59a5459bad9205e6de438d218d2af6f0092e19cec03a60" 1539 1516 dependencies = [ 1540 1517 "clru", 1541 1518 "gix-chunk", ··· 1552 1529 1553 1530 [[package]] 1554 1531 name = "gix-packetline" 1555 - version = "0.19.1" 1532 + version = "0.19.2" 1556 1533 source = "registry+https://github.com/rust-lang/crates.io-index" 1557 - checksum = "2592fbd36249a2fea11056f7055cc376301ef38d903d157de41998335bbf1f93" 1534 + checksum = "cd57d692b4625c2ece7634ba5d71939bbfe07bcc70eac92e10875faaf83f5316" 1558 1535 dependencies = [ 1559 1536 "bstr", 1560 1537 "faster-hex", ··· 1564 1541 1565 1542 [[package]] 1566 1543 name = "gix-packetline-blocking" 1567 - version = "0.19.1" 1544 + version = "0.19.2" 1568 1545 source = "registry+https://github.com/rust-lang/crates.io-index" 1569 - checksum = "fc4e706f328cd494cc8f932172e123a72b9a4711b0db5e411681432a89bd4c94" 1546 + checksum = "d10476ed5b3c9e04113e16dbee1fa7a4690279f257a74691e85372c63588c4fd" 1570 1547 dependencies = [ 1571 1548 "bstr", 1572 1549 "faster-hex", ··· 1576 1553 1577 1554 [[package]] 1578 1555 name = "gix-path" 1579 - version = "0.10.20" 1556 + version = "0.10.21" 1580 1557 source = "registry+https://github.com/rust-lang/crates.io-index" 1581 - checksum = "06d37034a4c67bbdda76f7bcd037b2f7bc0fba0c09a6662b19697a5716e7b2fd" 1558 + checksum = "0416b41cd00ff292af9b94b0660880c44bd2ed66828ddca9a2b333535cbb71b8" 1582 1559 dependencies = [ 1583 1560 "bstr", 1584 1561 "gix-trace", 1585 1562 "gix-validate", 1586 1563 "home", 1587 - "once_cell", 1588 1564 "thiserror", 1589 1565 ] 1590 1566 1591 1567 [[package]] 1592 1568 name = "gix-pathspec" 1593 - version = "0.12.0" 1569 + version = "0.13.0" 1594 1570 source = "registry+https://github.com/rust-lang/crates.io-index" 1595 - checksum = "daedead611c9bd1f3640dc90a9012b45f790201788af4d659f28d94071da7fba" 1571 + checksum = "d05e28457dca7c65a2dbe118869aab922a5bd382b7bb10cff5354f366845c128" 1596 1572 dependencies = [ 1597 1573 "bitflags", 1598 1574 "bstr", ··· 1604 1582 1605 1583 [[package]] 1606 1584 name = "gix-prompt" 1607 - version = "0.11.1" 1585 + version = "0.11.2" 1608 1586 source = "registry+https://github.com/rust-lang/crates.io-index" 1609 - checksum = "6ffa1a7a34c81710aaa666a428c142b6c5d640492fcd41267db0740d923c7906" 1587 + checksum = "868e6516dfa16fdcbc5f8c935167d085f2ae65ccd4c9476a4319579d12a69d8d" 1610 1588 dependencies = [ 1611 1589 "gix-command", 1612 1590 "gix-config-value", ··· 1617 1595 1618 1596 [[package]] 1619 1597 name = "gix-protocol" 1620 - version = "0.51.0" 1598 + version = "0.52.0" 1621 1599 source = "registry+https://github.com/rust-lang/crates.io-index" 1622 - checksum = "12b4b807c47ffcf7c1e5b8119585368a56449f3493da93b931e1d4239364e922" 1600 + checksum = "681901ef2f057fd1d856941f43d9f8b84a30df61f46182c0f26297b1a16f1f7f" 1623 1601 dependencies = [ 1624 1602 "bstr", 1625 1603 "gix-date", ··· 1636 1614 1637 1615 [[package]] 1638 1616 name = "gix-quote" 1639 - version = "0.6.0" 1617 + version = "0.6.1" 1640 1618 source = "registry+https://github.com/rust-lang/crates.io-index" 1641 - checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd" 1619 + checksum = "e912ec04b7b1566a85ad486db0cab6b9955e3e32bcd3c3a734542ab3af084c5b" 1642 1620 dependencies = [ 1643 1621 "bstr", 1644 1622 "gix-utils", ··· 1647 1625 1648 1626 [[package]] 1649 1627 name = "gix-ref" 1650 - version = "0.53.1" 1628 + version = "0.54.0" 1651 1629 source = "registry+https://github.com/rust-lang/crates.io-index" 1652 - checksum = "b966f578079a42f4a51413b17bce476544cca1cf605753466669082f94721758" 1630 + checksum = "b3e8559e2f6eb30785f13a689764d571d9aaf2e60603829e7504b97cae5aaaad" 1653 1631 dependencies = [ 1654 1632 "gix-actor", 1655 1633 "gix-features", ··· 1668 1646 1669 1647 [[package]] 1670 1648 name = "gix-refspec" 1671 - version = "0.31.0" 1649 + version = "0.32.0" 1672 1650 source = "registry+https://github.com/rust-lang/crates.io-index" 1673 - checksum = "7d29cae1ae31108826e7156a5e60bffacab405f4413f5bc0375e19772cce0055" 1651 + checksum = "93147960f77695ba89b72019b789679278dd4dad6a0f9a4a5bf2fd07aba56912" 1674 1652 dependencies = [ 1675 1653 "bstr", 1676 1654 "gix-hash", ··· 1682 1660 1683 1661 [[package]] 1684 1662 name = "gix-revision" 1685 - version = "0.35.0" 1663 + version = "0.36.0" 1686 1664 source = "registry+https://github.com/rust-lang/crates.io-index" 1687 - checksum = "f651f2b1742f760bb8161d6743229206e962b73d9c33c41f4e4aefa6586cbd3d" 1665 + checksum = "235c2338044e1e901e98cc44d669574592394212803d3956b6ac025e88963512" 1688 1666 dependencies = [ 1689 1667 "bitflags", 1690 1668 "bstr", ··· 1700 1678 1701 1679 [[package]] 1702 1680 name = "gix-revwalk" 1703 - version = "0.21.0" 1681 + version = "0.22.0" 1704 1682 source = "registry+https://github.com/rust-lang/crates.io-index" 1705 - checksum = "06e74f91709729e099af6721bd0fa7d62f243f2005085152301ca5cdd86ec02c" 1683 + checksum = "02e2de4f91d712b1f6873477f769225fe430ffce2af8c7c85721c3ff955783b3" 1706 1684 dependencies = [ 1707 1685 "gix-commitgraph", 1708 1686 "gix-date", ··· 1715 1693 1716 1694 [[package]] 1717 1695 name = "gix-sec" 1718 - version = "0.12.0" 1696 + version = "0.12.1" 1719 1697 source = "registry+https://github.com/rust-lang/crates.io-index" 1720 - checksum = "09f7053ed7c66633b56c57bc6ed3377be3166eaf3dc2df9f1c5ec446df6fdf2c" 1698 + checksum = "52e45952d0f94824e6ae7e4388b4cf9691cf50ca2ac544e3540858305a316ce7" 1721 1699 dependencies = [ 1722 1700 "bitflags", 1723 1701 "gix-path", 1724 1702 "libc", 1725 - "windows-sys 0.59.0", 1703 + "windows-sys 0.61.2", 1726 1704 ] 1727 1705 1728 1706 [[package]] 1729 1707 name = "gix-shallow" 1730 - version = "0.5.0" 1708 + version = "0.6.0" 1731 1709 source = "registry+https://github.com/rust-lang/crates.io-index" 1732 - checksum = "d936745103243ae4c510f19e0760ce73fb0f08096588fdbe0f0d7fb7ce8944b7" 1710 + checksum = "e2374692db1ee1ffa0eddcb9e86ec218f7c4cdceda800ebc5a9fdf73a8c08223" 1733 1711 dependencies = [ 1734 1712 "bstr", 1735 1713 "gix-hash", ··· 1739 1717 1740 1718 [[package]] 1741 1719 name = "gix-status" 1742 - version = "0.20.0" 1720 + version = "0.21.0" 1743 1721 source = "registry+https://github.com/rust-lang/crates.io-index" 1744 - checksum = "2a4afff9b34eeececa8bdc32b42fb318434b6b1391d9f8d45fe455af08dc2d35" 1722 + checksum = "84e01b3762e781e3d4800dfcd25140878d704b018cf792f4a0c55a1298cfdba1" 1745 1723 dependencies = [ 1746 1724 "bstr", 1747 1725 "filetime", ··· 1762 1740 1763 1741 [[package]] 1764 1742 name = "gix-submodule" 1765 - version = "0.20.0" 1743 + version = "0.21.0" 1766 1744 source = "registry+https://github.com/rust-lang/crates.io-index" 1767 - checksum = "657cc5dd43cbc7a14d9c5aaf02cfbe9c2a15d077cded3f304adb30ef78852d3e" 1745 + checksum = "9bacc06333b50abc4fc06204622c2dd92850de2066bb5d421ac776d2bef7ae55" 1768 1746 dependencies = [ 1769 1747 "bstr", 1770 1748 "gix-config", ··· 1777 1755 1778 1756 [[package]] 1779 1757 name = "gix-tempfile" 1780 - version = "18.0.0" 1758 + version = "19.0.0" 1781 1759 source = "registry+https://github.com/rust-lang/crates.io-index" 1782 - checksum = "666c0041bcdedf5fa05e9bef663c897debab24b7dc1741605742412d1d47da57" 1760 + checksum = "0c413fdfca2dea768903b7ca73e46b0a8c32e4b61662c68af127bcfb7c2f7c20" 1783 1761 dependencies = [ 1784 1762 "dashmap", 1785 1763 "gix-fs", 1786 1764 "libc", 1787 - "once_cell", 1788 1765 "parking_lot", 1789 1766 "signal-hook", 1790 1767 "signal-hook-registry", ··· 1792 1771 1793 1772 [[package]] 1794 1773 name = "gix-trace" 1795 - version = "0.1.13" 1774 + version = "0.1.14" 1796 1775 source = "registry+https://github.com/rust-lang/crates.io-index" 1797 - checksum = "e2ccaf54b0b1743a695b482ca0ab9d7603744d8d10b2e5d1a332fef337bee658" 1776 + checksum = "d35bf1c8c1a883bdbb17ed1fef49be8c751a50dc3a28a2deb4234d826eddb4bb" 1798 1777 1799 1778 [[package]] 1800 1779 name = "gix-transport" 1801 - version = "0.48.0" 1780 + version = "0.49.0" 1802 1781 source = "registry+https://github.com/rust-lang/crates.io-index" 1803 - checksum = "12f7cc0179fc89d53c54e1f9ce51229494864ab4bf136132d69db1b011741ca3" 1782 + checksum = "bd65513315c16ec52f479858b15fc89057e5ff6342cad1ea5573789ca4448f25" 1804 1783 dependencies = [ 1805 1784 "bstr", 1806 1785 "gix-command", ··· 1814 1793 1815 1794 [[package]] 1816 1795 name = "gix-traverse" 1817 - version = "0.47.0" 1796 + version = "0.48.0" 1818 1797 source = "registry+https://github.com/rust-lang/crates.io-index" 1819 - checksum = "c7cdc82509d792ba0ad815f86f6b469c7afe10f94362e96c4494525a6601bdd5" 1798 + checksum = "412126bade03a34f5d4125fd64878852718575b3b360eaae3b29970cb555e2a2" 1820 1799 dependencies = [ 1821 1800 "bitflags", 1822 1801 "gix-commitgraph", ··· 1831 1810 1832 1811 [[package]] 1833 1812 name = "gix-url" 1834 - version = "0.32.0" 1813 + version = "0.33.0" 1835 1814 source = "registry+https://github.com/rust-lang/crates.io-index" 1836 - checksum = "1b76a9d266254ad287ffd44467cd88e7868799b08f4d52e02d942b93e514d16f" 1815 + checksum = "788faa4421a720200c532e4377b17214963583cef7d43f0741a299f81e6b634e" 1837 1816 dependencies = [ 1838 1817 "bstr", 1839 1818 "gix-features", ··· 1845 1824 1846 1825 [[package]] 1847 1826 name = "gix-utils" 1848 - version = "0.3.0" 1827 + version = "0.3.1" 1849 1828 source = "registry+https://github.com/rust-lang/crates.io-index" 1850 - checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" 1829 + checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" 1851 1830 dependencies = [ 1852 1831 "bstr", 1853 1832 "fastrand", ··· 1856 1835 1857 1836 [[package]] 1858 1837 name = "gix-validate" 1859 - version = "0.10.0" 1838 + version = "0.10.1" 1860 1839 source = "registry+https://github.com/rust-lang/crates.io-index" 1861 - checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" 1840 + checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" 1862 1841 dependencies = [ 1863 1842 "bstr", 1864 1843 "thiserror", ··· 1866 1845 1867 1846 [[package]] 1868 1847 name = "gix-worktree" 1869 - version = "0.42.0" 1848 + version = "0.43.0" 1870 1849 source = "registry+https://github.com/rust-lang/crates.io-index" 1871 - checksum = "55f625ac9126c19bef06dbc6d2703cdd7987e21e35b497bb265ac37d383877b1" 1850 + checksum = "c5a40efab3eef5485365fee7b5df60732110bc3521af5e48c5cd1c153c48dae0" 1872 1851 dependencies = [ 1873 1852 "bstr", 1874 1853 "gix-attributes", ··· 1885 1864 1886 1865 [[package]] 1887 1866 name = "gix-worktree-state" 1888 - version = "0.20.0" 1867 + version = "0.21.0" 1889 1868 source = "registry+https://github.com/rust-lang/crates.io-index" 1890 - checksum = "06ba9b17cbacc02b25801197b20100f7f9bd621db1e7fce9d3c8ab3175207bf8" 1869 + checksum = "046efd191ff842cc22ddce61a4e8cea75ef7e3c659772de0838b2ad74b0016ef" 1891 1870 dependencies = [ 1892 1871 "bstr", 1893 1872 "gix-features", ··· 1905 1884 1906 1885 [[package]] 1907 1886 name = "gix-worktree-stream" 1908 - version = "0.22.0" 1887 + version = "0.23.0" 1909 1888 source = "registry+https://github.com/rust-lang/crates.io-index" 1910 - checksum = "f56a737cefbcd90b573cb5393d636f6dc5e0d08a8086356d8c4fcc623b49a0e8" 1889 + checksum = "a629188d528f5ed8abe023cdbdc4d51ef19223552cd7e2808733f96163fbf79d" 1911 1890 dependencies = [ 1912 1891 "gix-attributes", 1913 1892 "gix-features", ··· 1978 1957 source = "registry+https://github.com/rust-lang/crates.io-index" 1979 1958 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1980 1959 dependencies = [ 1981 - "allocator-api2", 1982 - "equivalent", 1983 - "foldhash", 1960 + "foldhash 0.1.5", 1984 1961 ] 1985 1962 1986 1963 [[package]] ··· 1986 1967 version = "0.16.0" 1987 1968 source = "registry+https://github.com/rust-lang/crates.io-index" 1988 1969 checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 1970 + dependencies = [ 1971 + "allocator-api2", 1972 + "equivalent", 1973 + "foldhash 0.2.0", 1974 + ] 1989 1975 1990 1976 [[package]] 1991 1977 name = "heapless" ··· 2529 2505 "http-body-util", 2530 2506 "hyper-util", 2531 2507 "identity", 2508 + "lexicon", 2532 2509 "oauth", 2533 2510 "reqwest", 2534 2511 "rustc-hash", ··· 2565 2540 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2566 2541 dependencies = [ 2567 2542 "spin", 2543 + ] 2544 + 2545 + [[package]] 2546 + name = "lexicon" 2547 + version = "0.0.0" 2548 + dependencies = [ 2549 + "data-encoding", 2550 + "gix-hash", 2551 + "identity", 2552 + "serde", 2553 + "thiserror", 2554 + "time", 2568 2555 ] 2569 2556 2570 2557 [[package]] ··· 3380 3343 "errno", 3381 3344 "libc", 3382 3345 "linux-raw-sys", 3383 - "windows-sys 0.61.0", 3346 + "windows-sys 0.60.2", 3384 3347 ] 3385 3348 3386 3349 [[package]] ··· 3443 3406 source = "registry+https://github.com/rust-lang/crates.io-index" 3444 3407 checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 3445 3408 dependencies = [ 3446 - "windows-sys 0.61.0", 3409 + "windows-sys 0.61.2", 3447 3410 ] 3448 3411 3449 3412 [[package]] ··· 3905 3868 "getrandom 0.3.4", 3906 3869 "once_cell", 3907 3870 "rustix", 3908 - "windows-sys 0.61.0", 3871 + "windows-sys 0.60.2", 3909 3872 ] 3910 3873 3911 3874 [[package]] ··· 4006 3969 "signal-hook-registry", 4007 3970 "socket2 0.6.0", 4008 3971 "tokio-macros", 4009 - "windows-sys 0.61.0", 3972 + "windows-sys 0.61.2", 4010 3973 ] 4011 3974 4012 3975 [[package]] ··· 4527 4490 source = "registry+https://github.com/rust-lang/crates.io-index" 4528 4491 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 4529 4492 dependencies = [ 4530 - "windows-sys 0.61.0", 4493 + "windows-sys 0.60.2", 4531 4494 ] 4532 4495 4533 4496 [[package]] ··· 4544 4507 4545 4508 [[package]] 4546 4509 name = "windows-link" 4547 - version = "0.2.0" 4510 + version = "0.2.1" 4548 4511 source = "registry+https://github.com/rust-lang/crates.io-index" 4549 - checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 4512 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4550 4513 4551 4514 [[package]] 4552 4515 name = "windows-registry" ··· 4615 4578 4616 4579 [[package]] 4617 4580 name = "windows-sys" 4618 - version = "0.61.0" 4581 + version = "0.61.2" 4619 4582 source = "registry+https://github.com/rust-lang/crates.io-index" 4620 - checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" 4583 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 4621 4584 dependencies = [ 4622 - "windows-link 0.2.0", 4585 + "windows-link 0.2.1", 4623 4586 ] 4624 4587 4625 4588 [[package]]
+5 -1
Cargo.toml
··· 4 4 "crates/credential_helper", 5 5 "crates/identity", 6 6 "crates/knot", 7 + "crates/lexicon", 7 8 "crates/oauth", 8 9 "crates/serve_git", 9 10 "crates/xrpc" ··· 21 20 22 21 [workspace.dependencies] 23 22 identity = { path = "crates/identity" } 23 + lexicon = { path = "crates/lexicon" } 24 24 oauth = { path = "crates/oauth" } 25 25 serve_git = { path = "crates/serve_git"} 26 26 xrpc = { path = "crates/xrpc" } 27 27 28 28 anyhow = "1.0.100" 29 29 axum = "0.8.4" 30 - gix = { version = "0.73.0", features = ["max-performance"] } 30 + data-encoding = "2.9.0" 31 + gix = { version = "0.74.0", features = ["max-performance"] } 31 32 reqwest = { version = "0.12.23", features = ["json"] } 32 33 serde = { version = "1.0.226", features = ["derive"] } 33 34 serde_json = "1.0.145" 34 35 thiserror = "2.0.16" 36 + time = { version = "0.3.43", features = ["formatting", "macros", "parsing", "serde"] } 35 37 tracing = "0.1.41" 36 38 url = { version = "2.5.7", features = ["serde"] } 37 39
+3 -2
crates/knot/Cargo.toml
··· 10 10 11 11 [dependencies] 12 12 identity.workspace = true 13 + lexicon.workspace = true 13 14 oauth.workspace = true 14 15 serve_git.workspace = true 15 16 xrpc.workspace = true ··· 28 27 axum-extra = { version = "0.10.1", features = ["async-read-body"] } 29 28 bytes = "1.10.1" 30 29 clap = { version = "4.5.47", features = ["derive", "env", "string"] } 31 - data-encoding = "2.9.0" 30 + data-encoding.workspace = true 32 31 futures-util = "0.3.31" 33 32 hyper-util = { version = "0.1.17", features = ["client"] } 34 33 rustc-hash = "2.1.1" 35 - time = { version = "0.3.43", features = ["formatting", "macros", "parsing", "serde"] } 34 + time.workspace = true 36 35 tokio = { version = "1.47.1", features = ["io-util", "macros", "net", "process", "signal", "rt-multi-thread"] } 37 36 tokio-stream = { version = "0.1.17", features = ["time"] } 38 37 tokio-tungstenite = "0.28.0"
-17
crates/knot/src/convert.rs
··· 1 - //! 2 - //! Utility conversions. 3 - //! 4 - use gix::date::Time; 5 - use time::OffsetDateTime; 6 - use time::UtcOffset; 7 - use time::error::ComponentRange; 8 - 9 - /// Convert a [`gix::date::Time`] to a [`time::OffsetDateTime`]. 10 - /// 11 - #[allow(unused)] 12 - pub fn time_to_offsetdatetime(time: &Time) -> Result<OffsetDateTime, ComponentRange> { 13 - let odt = OffsetDateTime::from_unix_timestamp(time.seconds)? 14 - .to_offset(UtcOffset::from_whole_seconds(time.offset)?); 15 - 16 - Ok(odt) 17 - }
-6
crates/knot/src/lib.rs
··· 1 1 pub(crate) mod atproto; 2 - pub(crate) mod convert; 3 2 pub mod model; 4 3 pub mod public; 5 4 pub mod types; 6 - 7 - mod objectid; 8 - pub use objectid::ObjectId; 9 - 10 - pub mod repospec;
+6 -4
crates/knot/src/main.rs
··· 10 10 rt::TokioExecutor, 11 11 }; 12 12 use identity::Resolver; 13 - use knot::{model::Knot, repospec::RepoSpec}; 13 + use knot::model::Knot; 14 + use lexicon::extra::repopath::RepoPath; 14 15 use reqwest::{ClientBuilder, StatusCode}; 15 16 use serde::Deserialize; 16 17 use std::{ ··· 54 53 55 54 #[derive(Deserialize)] 56 55 struct Repo { 57 - #[serde(with = "knot::repospec::query_param")] 58 - repo: RepoSpec, 56 + #[serde(with = "lexicon::extra::repopath::string")] 57 + repo: RepoPath, 59 58 } 60 59 61 60 fn extract_request_id<B>(request: &Request<B>) -> Option<&str> { ··· 164 163 else => break, 165 164 } 166 165 } 166 + tracing::info!("/events websocket closed"); 167 167 }) 168 168 }, 169 169 ), ··· 189 187 } 190 188 191 189 if let Ok(Query(Repo { repo })) = Query::try_from_uri(uri) { 192 - span.record("repo", format!("{}/{}", repo.owner, repo.name)); 190 + span.record("repo", repo.to_string()); 193 191 } 194 192 195 193 span
+77 -139
crates/knot/src/model.rs
··· 1 + pub mod convert; 2 + pub mod errors; 3 + mod knot_state; 4 + pub mod nicediff; 5 + 1 6 use crate::{ 2 - convert::time_to_offsetdatetime, 3 7 model::errors::{HeadDetached, PathNotFound, RefNotFound, RepoEmpty, RepoError, RepoNotFound}, 4 8 public::{ 5 9 git::{Error, GitAuthorization, NotFound}, 6 10 xrpc::XrpcError, 7 11 }, 8 - repospec::RepoSpec, 9 12 types::sh::tangled::repo::{ 10 - BlobEncoding, BlobParams, BlobResponse, Branch, BranchesParams, BranchesResponse, 11 - DefaultBranchResponse, Diff, DiffParams, DiffResponse, GetDefaultBranchParams, JsonBlob, 12 - LogParams, LogResponse, Readme, Reference, Tag, TagParams, TagsResponse, TreeEntry, 13 - TreeParams, TreeResponse, 13 + blob::BlobResponse, branches::BranchesOutput, diff::DiffResponse, log::LogOutput, 14 + tags::TagsOutput, 14 15 }, 15 16 }; 16 17 use axum::{ 17 18 extract::{FromRef, FromRequestParts}, 18 19 http::request::Parts, 19 20 }; 21 + use convert::convert_entry; 20 22 use gix::{Commit, ObjectId, Repository, bstr::ByteSlice as _, open::Options}; 21 23 use identity::{Did, Resolver}; 24 + use lexicon::{ 25 + extra::repopath::RepoPath, 26 + sh::tangled::repo::{blob, branches, diff, get_default_branch, log, refs, tags, tree}, 27 + }; 22 28 use oauth::public_key::PublicKey; 23 29 use reqwest::ClientBuilder; 24 30 use serve_git::state::GitServiceState; ··· 56 50 b"readme.asciidoc", 57 51 b"index.rst", 58 52 ]; 59 - 60 - pub mod errors; 61 - mod gitoxide; 62 - mod knot_state; 63 53 64 54 #[derive(Clone)] 65 55 pub struct Knot { ··· 153 151 pub trait RepositoryManager { 154 152 type Error; 155 153 156 - fn resolve_repo_path(&self, repo: &RepoSpec) -> PathBuf; 154 + fn resolve_repo_path(&self, repo: &RepoPath) -> PathBuf; 157 155 158 - fn open(&self, repo: &RepoSpec) -> Result<Repository, Self::Error>; 156 + fn open(&self, repo: &RepoPath) -> Result<Repository, Self::Error>; 159 157 160 158 /// Determine whether the specified account has permission to push to the 161 159 /// specified repository. 162 - fn can_push(&self, repo: &RepoSpec, did: &Did) -> impl Future<Output = bool>; 160 + fn can_push(&self, repo: &RepoPath, did: &Did) -> impl Future<Output = bool>; 163 161 } 164 162 165 163 impl RepositoryManager for Knot { 166 164 type Error = Box<gix::open::Error>; 167 165 168 - fn resolve_repo_path(&self, repo: &RepoSpec) -> PathBuf { 166 + fn resolve_repo_path(&self, repo: &RepoPath) -> PathBuf { 169 167 let mut path = self.inner.repository_base.clone(); 170 168 path.push(repo.owner()); 171 169 path.push(repo.name()); 172 170 path 173 171 } 174 172 175 - fn open(&self, repo: &RepoSpec) -> Result<Repository, Self::Error> { 173 + fn open(&self, repo: &RepoPath) -> Result<Repository, Self::Error> { 176 174 let path = self.resolve_repo_path(repo); 177 175 let repository = Options::default() 178 176 .strict_config(true) ··· 186 184 } 187 185 188 186 // Currently defers to the real knotserver's internal "/push-allowed" route. 189 - async fn can_push(&self, repo: &RepoSpec, did: &Did) -> bool { 187 + async fn can_push(&self, repo: &RepoPath, did: &Did) -> bool { 190 188 use reqwest::StatusCode; 191 189 192 190 let url = format!("http://localhost:5444/push-allowed?user={did}&repo={repo}"); ··· 223 221 224 222 pub async fn get_default_branch( 225 223 &self, 226 - params: &GetDefaultBranchParams, 227 - ) -> Result<DefaultBranchResponse, XrpcError> { 224 + params: &get_default_branch::Input, 225 + ) -> Result<get_default_branch::Output, XrpcError> { 228 226 tokio::task::block_in_place(|| { 229 227 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 230 228 ··· 237 235 .shorten() 238 236 .to_string(); 239 237 240 - let hash = head.id().map(|id| id.into()); 238 + let hash = head.id().map(|id| id.detach().into()); 241 239 let when = head 242 - .peel_to_commit_in_place() 240 + .peel_to_commit() 243 241 .ok() 244 242 .and_then(|commit| { 245 243 commit ··· 247 245 .ok() 248 246 .and_then(|committer| committer.time().ok()) 249 247 }) 250 - .and_then(|time| time_to_offsetdatetime(&time).ok()); 248 + .and_then(|time| convert::time_to_offsetdatetime(&time).ok()); 251 249 252 - Ok(DefaultBranchResponse { name, hash, when }) 250 + Ok(get_default_branch::Output { name, hash, when }) 253 251 }) 254 252 } 255 253 256 - pub async fn branches(&self, params: &BranchesParams) -> Result<BranchesResponse, XrpcError> { 254 + pub async fn branches(&self, params: &branches::Input) -> Result<BranchesOutput, XrpcError> { 255 + use crate::types::sh::tangled::repo::branches::Branch; 257 256 tokio::task::block_in_place(|| { 258 257 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 259 258 ··· 268 265 .to_string(); 269 266 270 267 let mut branches = Vec::new(); 271 - for branch in repo 272 - .references()? 273 - .local_branches()? 274 - .skip(params.cursor) 275 - .take(params.limit.into()) 268 + for branch in repo.references()?.local_branches()? 269 + // .skip(params.cursor) 270 + // .take(params.limit.into()) 276 271 { 277 272 let Ok(branch) = branch.inspect_err(|error| tracing::error!(?error)) else { 278 273 continue; ··· 289 288 290 289 let is_default = name == default_name; 291 290 branches.push(Branch { 292 - reference: Reference { 291 + reference: refs::Reference { 293 292 name, 294 293 hash: commit.id.into(), 295 294 }, 296 - commit: commit.try_into()?, 295 + commit: convert::try_convert_commit(commit)?, 297 296 is_default, 298 297 }); 299 298 } 300 299 301 - Ok(BranchesResponse { branches }) 300 + Ok(BranchesOutput { branches }) 302 301 }) 303 302 } 304 303 305 - pub async fn log(&self, params: &LogParams) -> Result<LogResponse, XrpcError> { 304 + pub async fn log(&self, params: &log::Input) -> Result<LogOutput, XrpcError> { 306 305 tokio::task::block_in_place(|| { 307 306 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 308 307 309 - let commit_graph = repo.commit_graph_if_enabled().unwrap(); 308 + let commit_graph = repo.commit_graph_if_enabled()?; 310 309 let total = match &commit_graph { 311 310 Some(cg) => cg 312 311 .num_commits() ··· 335 334 match commit { 336 335 Ok(commit) => { 337 336 let commit = repo.find_commit(commit.id()).map_err(RepoError)?; 338 - commits.push(commit.try_into().map_err(RepoError)?); 337 + commits.push(convert::try_convert_commit(commit).map_err(RepoError)?); 339 338 } 340 339 Err(error) => { 341 340 tracing::error!(?error); ··· 344 343 } 345 344 } 346 345 347 - Ok(LogResponse { 346 + Ok(LogOutput { 348 347 commits, 349 348 log: true, 350 349 total, ··· 354 353 }) 355 354 } 356 355 357 - pub async fn tags(&self, params: &TagParams) -> Result<TagsResponse, XrpcError> { 358 - // @TODO Implement cursor. 356 + pub async fn tags(&self, params: &tags::Input) -> Result<TagsOutput, XrpcError> { 357 + use crate::types::sh::tangled::repo::tags::{Tag, TagsOutput}; 358 + 359 359 tokio::task::block_in_place(|| { 360 360 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 361 361 ··· 380 378 ) 381 379 }); 382 380 383 - tags.truncate(params.limit); 384 - 385 - Ok(TagsResponse { tags }) 381 + Ok(TagsOutput { tags }) 386 382 }) 387 383 } 388 384 389 - pub async fn tree(&self, params: &TreeParams) -> Result<TreeResponse, XrpcError> { 385 + pub async fn tree(&self, params: &tree::Input) -> Result<tree::Output, XrpcError> { 390 386 tokio::task::block_in_place(|| { 391 387 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 392 388 let tip = Self::resolve_rev(&repo, params.rev.as_deref())?; ··· 404 404 .ok_or(PathNotFound(subpath.to_string_lossy()))?; 405 405 406 406 if !entry.mode().is_tree() { 407 - return Ok(TreeResponse { 407 + return Ok(tree::Output { 408 408 files: vec![], 409 409 dotdot, 410 410 parent: params.path.clone(), ··· 418 418 parent = Some(subpath.to_path_buf()); 419 419 } 420 420 421 - let mut files: Vec<TreeEntry> = vec![]; 421 + let mut files: Vec<tree::TreeEntry> = vec![]; 422 422 let mut readme = None; 423 423 for entry in tree.iter() { 424 424 let Ok(entry) = entry else { ··· 431 431 { 432 432 let mut file = repo.find_blob(entry.id())?; 433 433 if let Ok(contents) = String::from_utf8(file.take_data()) { 434 - readme.replace(Readme { 434 + readme.replace(tree::Readme { 435 435 contents, 436 436 filename: entry.filename().to_string(), 437 437 }); 438 438 } 439 439 } 440 440 441 - files.push(entry.into()); 441 + files.push(convert_entry(entry)); 442 442 } 443 443 444 444 let files: Vec<_> = tree 445 445 .iter() 446 446 .filter_map(|entry| { 447 447 let entry = entry.ok()?; 448 - let file: TreeEntry = entry.try_into().ok()?; 448 + let file = convert::convert_entry(entry); 449 449 Some(file) 450 450 }) 451 451 .collect(); 452 452 453 - Ok(TreeResponse { 453 + Ok(tree::Output { 454 454 files, 455 455 dotdot, 456 456 parent, ··· 460 460 }) 461 461 } 462 462 463 - pub async fn blob(&self, params: &BlobParams) -> Result<BlobResponse, XrpcError> { 463 + pub async fn blob(&self, params: &blob::Input) -> Result<BlobResponse, XrpcError> { 464 + use lexicon::sh::tangled::repo::blob::{Encoding, Output}; 464 465 tokio::task::block_in_place(|| { 465 466 let repo = self.open(&params.repo).map_err(RepoNotFound)?; 466 467 let tip = Self::resolve_rev(&repo, params.rev.as_deref())?; ··· 482 481 483 482 let size = data.len(); 484 483 let (content, is_binary, encoding) = match String::from_utf8(data) { 485 - Ok(content) => (content, false, BlobEncoding::Utf8), 484 + Ok(content) => (content, false, blob::Encoding::Utf8), 486 485 Err(error) => ( 487 486 data_encoding::BASE64.encode(&error.into_bytes()), 488 487 true, 489 - BlobEncoding::Base64, 488 + Encoding::Base64, 490 489 ), 491 490 }; 492 491 493 - Ok(BlobResponse::Json(JsonBlob { 492 + Ok(BlobResponse::Json(Output { 493 + rev: params.rev.as_deref().unwrap_or_default().to_owned(), 494 + path: params.path.to_owned(), 494 495 content, 495 496 encoding, 497 + size, 496 498 is_binary, 497 499 mime_type: "".to_string(), 498 - path: params.path.to_owned(), 499 - rev: params.rev.as_deref().unwrap_or_default().to_owned(), 500 - size, 500 + last_commit: None, 501 501 })) 502 502 }) 503 503 } 504 504 505 - pub async fn diff(&self, params: &DiffParams) -> Result<DiffResponse, XrpcError> { 506 - { 507 - tokio::task::block_in_place(|| { 508 - let repo = self.open(&params.repo).map_err(RepoNotFound)?; 509 - 510 - let revision = match ObjectId::from_str(&params.rev) { 511 - Ok(id) => repo.find_commit(id).map_err(RefNotFound)?, 512 - Err(_) => { 513 - // Assume the refspec is a branch or tag. 514 - let mut reference = 515 - repo.find_reference(&params.rev).map_err(RefNotFound)?; 516 - reference.peel_to_commit().map_err(RefNotFound)? 517 - } 518 - }; 519 - 520 - // let parent_tree = match revision.parent_ids().next() { 521 - // Some(id) => repository.find_commit(id)?.tree()?, 522 - // None => repository.empty_tree(), 523 - // }; 524 - 525 - // let mut cache = repository 526 - // .diff_resource_cache( 527 - // gix::diff::blob::pipeline::Mode::ToGit, 528 - // WorktreeRoots::default(), 529 - // ) 530 - // .unwrap(); 531 - 532 - // let this_tree = revision.tree()?; 533 - // this_tree 534 - // .changes()? 535 - // .for_each_to_obtain_tree(&parent_tree, |change| { 536 - // tracing::debug!(?change); 537 - // if change.entry_mode().is_blob() { 538 - // println!("{}:", change.location().to_string()); 539 - // let mut line_diff = change.diff(&mut cache).unwrap(); 540 - // line_diff 541 - // .resource_cache 542 - // .options 543 - // .algorithm 544 - // .replace(Algorithm::Histogram); 545 - 546 - // let out = line_diff.lines(|_| Ok::<_, Er>(()) )?; 547 - 548 - // let input = out.interned_input(); 549 - // let diff = gix::diff::blob::diff( 550 - // Algorithm::Histogram, 551 - // &input, 552 - // UnifiedDiff::new( 553 - // &input, 554 - // String::new(), 555 - // gix::diff::blob::unified_diff::NewlineSeparator::AfterHeaderAndWhenNeeded("\n"), 556 - // ContextSize::symmetrical(3), 557 - // ), 558 - // ).unwrap() 559 - // .finish(); 560 - 561 - // println!("{diff}"); 562 - // } 563 - // Ok::<_, Er>(Action::Continue) 564 - // })?; 565 - 566 - let diff = Diff { 567 - commit: revision.try_into()?, 568 - stat: Default::default(), 569 - deltas: vec![], 570 - }; 571 - 572 - let response = DiffResponse { 573 - rev: params.rev.to_owned(), 574 - diff, 575 - }; 576 - 577 - Ok(response) 578 - }) 579 - } 505 + pub async fn diff(&self, params: diff::Input) -> Result<DiffResponse, XrpcError> { 506 + tokio::task::block_in_place(|| { 507 + let repo = self.open(&params.repo).map_err(RepoNotFound)?; 508 + let this_commit = Self::resolve_rev(&repo, Some(&params.rev))?; 509 + let diff = nicediff::unified_diff_from_parent(this_commit).unwrap(); 510 + let response = DiffResponse { 511 + rev: params.rev, 512 + diff, 513 + }; 514 + Ok(response) 515 + }) 580 516 } 581 517 } 582 518 ··· 538 600 parts: &mut Parts, 539 601 ) -> Result<tokio::process::Command, Self::Rejection> { 540 602 use axum::extract::Path; 541 - let Path(repospec) = Path::<RepoSpec>::from_request_parts(parts, &()).await?; 542 - let repo = self.open(&repospec).map_err(NotFound)?; 603 + let Path(repopath) = Path::<RepoPath>::from_request_parts(parts, &()).await?; 604 + let repo = self.open(&repopath).map_err(NotFound)?; 543 605 let command = serve_git::commands::upload_pack_info_refs("/usr/bin/git", repo.path()); 544 606 Ok(command) 545 607 } ··· 549 611 parts: &mut Parts, 550 612 ) -> Result<tokio::process::Command, Self::Rejection> { 551 613 use axum::extract::Path; 552 - let Path(repospec) = Path::<RepoSpec>::from_request_parts(parts, &()).await?; 553 - let repo = self.open(&repospec).map_err(NotFound)?; 614 + let Path(repopath) = Path::<RepoPath>::from_request_parts(parts, &()).await?; 615 + let repo = self.open(&repopath).map_err(NotFound)?; 554 616 let command = serve_git::commands::upload_pack("/usr/bin/git", repo.path()); 555 617 Ok(command) 556 618 } ··· 568 630 } 569 631 570 632 use axum::extract::Path; 571 - let Path(repospec) = Path::<RepoSpec>::from_request_parts(parts, &()).await?; 572 - if !self.can_push(&repospec, &auth.iss).await { 633 + let Path(repopath) = Path::<RepoPath>::from_request_parts(parts, &()).await?; 634 + if !self.can_push(&repopath, &auth.iss).await { 573 635 tracing::error!(did = %auth.iss, "push denied"); 574 636 return Err(Error::forbidden( 575 637 self, ··· 580 642 ))?; 581 643 } 582 644 583 - let repo = self.open(&repospec).map_err(NotFound)?; 645 + let repo = self.open(&repopath).map_err(NotFound)?; 584 646 let command = serve_git::commands::receive_pack_info_refs("/usr/bin/git", repo.path()); 585 647 Ok(command) 586 648 } ··· 598 660 } 599 661 600 662 use axum::extract::Path; 601 - let Path(repospec) = Path::<RepoSpec>::from_request_parts(parts, &()).await?; 602 - if !self.can_push(&repospec, &auth.iss).await { 663 + let Path(repopath) = Path::<RepoPath>::from_request_parts(parts, &()).await?; 664 + if !self.can_push(&repopath, &auth.iss).await { 603 665 tracing::error!(did = %auth.iss, "push denied"); 604 666 return Err(Error::forbidden( 605 667 self, ··· 610 672 ))?; 611 673 } 612 674 613 - let repo = self.open(&repospec).map_err(NotFound)?; 675 + let repo = self.open(&repopath).map_err(NotFound)?; 614 676 let command = serve_git::commands::receive_pack("/usr/bin/git", repo.path()); 615 677 Ok(command) 616 678 }
+157
crates/knot/src/model/convert.rs
··· 1 + use crate::{public::xrpc::XrpcError, types::sh::tangled::repo::tags}; 2 + use data_encoding::BASE64URL; 3 + use gix::bstr::ByteSlice; 4 + use lexicon::sh::tangled::repo::{refs, tree}; 5 + use reqwest::StatusCode; 6 + use std::{borrow::Cow, collections::HashMap}; 7 + use time::{OffsetDateTime, error::ComponentRange}; 8 + 9 + #[derive(Debug, thiserror::Error)] 10 + pub enum ConversionError { 11 + #[error("Failed to decode object: {0}")] 12 + Decode(#[from] gix::objs::decode::Error), 13 + #[error("Failed to parse git time: {0}")] 14 + TimeParse(#[from] gix::date::parse::Error), 15 + #[error("Failed to convert git time: {0}")] 16 + TimeConversion(#[from] time::error::ComponentRange), 17 + } 18 + 19 + impl From<ConversionError> for XrpcError { 20 + fn from(value: ConversionError) -> Self { 21 + Self { 22 + status: StatusCode::INTERNAL_SERVER_ERROR, 23 + error: "RepositoryError".into(), 24 + message: value.to_string().into(), 25 + } 26 + } 27 + } 28 + 29 + /// Convert a git timestamp to an [`OffsetDateTime`]. 30 + pub fn time_to_offsetdatetime(time: &gix::date::Time) -> Result<OffsetDateTime, ComponentRange> { 31 + use time::UtcOffset; 32 + 33 + let odt = OffsetDateTime::from_unix_timestamp(time.seconds)? 34 + .to_offset(UtcOffset::from_whole_seconds(time.offset)?); 35 + 36 + Ok(odt) 37 + } 38 + 39 + pub fn try_convert_commit(value: gix::Commit<'_>) -> Result<refs::Commit, ConversionError> { 40 + let id = value.id().detach(); 41 + let decoded = value.decode()?; 42 + let mut merge_tag = String::default(); 43 + let mut extra_headers = HashMap::default(); 44 + for (key, value) in decoded.extra_headers { 45 + match key.as_bytes() { 46 + b"mergetag" => merge_tag = value.to_string(), 47 + b"gpgsig" => {} 48 + _ => { 49 + extra_headers.insert(key.to_string(), BASE64URL.encode(value.as_bytes())); 50 + } 51 + } 52 + } 53 + 54 + Ok(refs::Commit { 55 + hash: id.into(), 56 + author: try_convert_signature(decoded.author)?, 57 + committer: try_convert_signature(decoded.committer)?, 58 + merge_tag, 59 + pgp_signature: value.signature()?.map(|(sig, _)| sig.to_string()), 60 + message: decoded.message.to_string(), 61 + tree_hash: value.tree_id()?.detach().into(), 62 + parent_hashes: value.parent_ids().map(|id| id.detach().into()).collect(), 63 + // @TODO Review encoding parameter 64 + encoding: Cow::Borrowed("UTF-8"), 65 + extra_headers, 66 + }) 67 + } 68 + 69 + pub fn convert_entry(value: gix::object::tree::EntryRef<'_, '_>) -> tree::TreeEntry { 70 + use gix::objs::tree::EntryKind; 71 + 72 + let name = value.filename().to_string(); 73 + let mode = value.mode(); 74 + 75 + let is_file = mode.is_blob(); 76 + let is_subtree = mode.is_tree(); 77 + 78 + let mode_string = match mode.kind() { 79 + EntryKind::Tree => "drwxr-xr-x", 80 + EntryKind::Blob => "-rw-r--r--", 81 + EntryKind::BlobExecutable => "-rwxr-xr-x", 82 + EntryKind::Link => "----------", 83 + EntryKind::Commit => "----------", 84 + }; 85 + 86 + tree::TreeEntry { 87 + name, 88 + mode: Cow::Borrowed(mode_string), 89 + is_file, 90 + is_subtree, 91 + ..Default::default() 92 + } 93 + } 94 + 95 + pub fn convert_reference(value: &gix::Reference<'_>) -> refs::Reference { 96 + refs::Reference { 97 + name: value.name().shorten().to_string(), 98 + hash: value.id().detach().into(), 99 + } 100 + } 101 + 102 + pub fn try_convert_signature( 103 + signature: gix::actor::SignatureRef<'_>, 104 + ) -> Result<refs::Signature, ConversionError> { 105 + let signature = signature.trim(); 106 + Ok(refs::Signature { 107 + name: signature.name.to_string(), 108 + email: signature.email.to_string(), 109 + when: time_to_offsetdatetime(&signature.time()?)?, 110 + }) 111 + } 112 + 113 + impl TryFrom<gix::Tag<'_>> for tags::TagAnnotation { 114 + type Error = gix::objs::decode::Error; 115 + 116 + fn try_from(value: gix::Tag<'_>) -> Result<Self, Self::Error> { 117 + use gix::object::Kind; 118 + 119 + let hash = value.id.into(); 120 + let decoded = value.decode()?; 121 + 122 + // cf. <https://github.com/git/git/blob/7014b55638da979331baf8dc31c4e1d697cf2d67/object.h#L97> 123 + let target_type = match decoded.target_kind { 124 + Kind::Commit => 1, 125 + Kind::Tree => 2, 126 + Kind::Blob => 3, 127 + Kind::Tag => 4, 128 + }; 129 + 130 + Ok(tags::TagAnnotation { 131 + hash, 132 + name: decoded.name.to_string(), 133 + tagger: decoded 134 + .tagger 135 + .and_then(|tagger| try_convert_signature(tagger).ok()), 136 + message: decoded.message.to_string(), 137 + pgp_signature: decoded.pgp_signature.map(ToString::to_string), 138 + target_type, 139 + target: decoded.target().into(), 140 + }) 141 + } 142 + } 143 + 144 + impl TryFrom<gix::Reference<'_>> for tags::Tag { 145 + type Error = gix::objs::decode::Error; 146 + 147 + fn try_from(mut value: gix::Reference<'_>) -> Result<Self, Self::Error> { 148 + let r#ref: refs::Reference = convert_reference(&value); 149 + let annotation = value 150 + .peel_to_tag() 151 + .ok() 152 + .map(TryFrom::try_from) 153 + .transpose()?; 154 + 155 + Ok(tags::Tag { r#ref, annotation }) 156 + } 157 + }
-160
crates/knot/src/model/gitoxide.rs
··· 1 - //! 2 - //! [`TryFrom`] impls for converting Gitoxide objects into Tangled objects. 3 - //! 4 - use crate::{ 5 - public::xrpc::XrpcError, 6 - types::sh::tangled::repo::{Commit, Reference, Signature, Tag, TagAnnotation, TreeEntry}, 7 - }; 8 - 9 - use data_encoding::BASE64URL; 10 - use gix::bstr::ByteSlice; 11 - use reqwest::StatusCode; 12 - use std::{borrow::Cow, collections::HashMap}; 13 - 14 - #[derive(Debug, thiserror::Error)] 15 - pub enum ConversionError { 16 - #[error("{0}")] 17 - Decode(#[from] gix::objs::decode::Error), 18 - #[error("{0}")] 19 - TimeParse(#[from] gix::date::parse::Error), 20 - #[error("{0}")] 21 - TimeConversion(#[from] time::error::ComponentRange), 22 - } 23 - 24 - impl From<ConversionError> for XrpcError { 25 - fn from(value: ConversionError) -> Self { 26 - Self { 27 - status: StatusCode::INTERNAL_SERVER_ERROR, 28 - error: "RepositoryError".into(), 29 - message: value.to_string().into(), 30 - } 31 - } 32 - } 33 - 34 - impl TryFrom<gix::actor::SignatureRef<'_>> for Signature { 35 - type Error = ConversionError; 36 - 37 - fn try_from(value: gix::actor::SignatureRef<'_>) -> Result<Self, Self::Error> { 38 - let signature = value.trim(); 39 - Ok(Self { 40 - name: signature.name.to_string(), 41 - email: signature.email.to_string(), 42 - when: crate::convert::time_to_offsetdatetime(&signature.time()?)?, 43 - }) 44 - } 45 - } 46 - 47 - impl TryFrom<gix::Commit<'_>> for Commit { 48 - type Error = ConversionError; 49 - 50 - fn try_from(value: gix::Commit<'_>) -> Result<Self, Self::Error> { 51 - let id = value.id(); 52 - let decoded = value.decode()?; 53 - let signature = value.signature()?.map(|(sig, _)| sig.to_string()); 54 - let mut merge_tag = String::default(); 55 - let mut extra_headers = HashMap::default(); 56 - for (key, value) in decoded.extra_headers { 57 - match key.as_bytes() { 58 - b"mergetag" => merge_tag = value.to_string(), 59 - b"gpgsig" => {} 60 - _ => { 61 - extra_headers.insert(key.to_string(), BASE64URL.encode(value.as_bytes())); 62 - } 63 - } 64 - } 65 - 66 - Ok(Self { 67 - hash: id.into(), 68 - author: decoded.author.try_into()?, 69 - committer: decoded.committer.try_into()?, 70 - merge_tag, 71 - signature, 72 - message: decoded.message.to_string(), 73 - tree_hash: value.tree_id()?.into(), 74 - parent_hashes: value.parent_ids().map(|id| id.into()).collect(), 75 - encoding: Cow::Borrowed("UTF-8"), 76 - extra_headers, 77 - }) 78 - } 79 - } 80 - 81 - impl From<gix::Reference<'_>> for Reference { 82 - fn from(value: gix::Reference<'_>) -> Self { 83 - Self { 84 - name: value.name().shorten().to_string(), 85 - hash: value.id().into(), 86 - } 87 - } 88 - } 89 - 90 - impl TryFrom<gix::Tag<'_>> for TagAnnotation { 91 - type Error = gix::objs::decode::Error; 92 - 93 - fn try_from(value: gix::Tag<'_>) -> Result<Self, Self::Error> { 94 - use gix::object::Kind; 95 - 96 - let hash = value.id.into(); 97 - let decoded = value.decode()?; 98 - 99 - // cf. <https://github.com/git/git/blob/7014b55638da979331baf8dc31c4e1d697cf2d67/object.h#L97> 100 - let target_type = match decoded.target_kind { 101 - Kind::Commit => 1, 102 - Kind::Tree => 2, 103 - Kind::Blob => 3, 104 - Kind::Tag => 4, 105 - }; 106 - 107 - Ok(Self { 108 - hash, 109 - name: decoded.name.to_string(), 110 - tagger: decoded.tagger.and_then(|tagger| tagger.try_into().ok()), 111 - message: decoded.message.to_string(), 112 - signature: decoded.pgp_signature.map(ToString::to_string), 113 - target_type, 114 - target: decoded.target().into(), 115 - }) 116 - } 117 - } 118 - 119 - impl TryFrom<gix::Reference<'_>> for Tag { 120 - type Error = gix::objs::decode::Error; 121 - 122 - fn try_from(mut value: gix::Reference<'_>) -> Result<Self, Self::Error> { 123 - let r#ref: Reference = value.clone().into(); 124 - let annotation = value 125 - .peel_to_tag() 126 - .ok() 127 - .map(TryFrom::try_from) 128 - .transpose()?; 129 - 130 - Ok(Self { r#ref, annotation }) 131 - } 132 - } 133 - 134 - impl From<gix::object::tree::EntryRef<'_, '_>> for TreeEntry { 135 - fn from(value: gix::object::tree::EntryRef<'_, '_>) -> Self { 136 - use gix::objs::tree::EntryKind; 137 - 138 - let name = value.filename().to_string(); 139 - let mode = value.mode(); 140 - 141 - let is_file = mode.is_blob(); 142 - let is_subtree = mode.is_tree(); 143 - 144 - let mode_string = match mode.kind() { 145 - EntryKind::Tree => "drwxr-xr-x", 146 - EntryKind::Blob => "-rw-r--r--", 147 - EntryKind::BlobExecutable => "-rwxr-xr-x", 148 - EntryKind::Link => "----------", 149 - EntryKind::Commit => "----------", 150 - }; 151 - 152 - Self { 153 - name, 154 - mode: Cow::Borrowed(mode_string), 155 - is_file, 156 - is_subtree, 157 - ..Default::default() 158 - } 159 - } 160 - }
+2 -14
crates/knot/src/model/knot_state.rs
··· 1 1 use identity::Did; 2 2 use identity::Resolver; 3 3 use oauth::public_key::PublicKey; 4 - use serde::Deserialize; 5 4 use std::collections::HashMap; 6 5 use std::path::PathBuf; 7 6 use std::sync::Mutex; ··· 51 52 } 52 53 53 54 pub async fn fetch_public_keys(&self, did: &Did) -> anyhow::Result<Vec<PublicKey>> { 55 + use lexicon::sh::tangled::PublicKey as LexiconPublicKey; 54 56 use url::Url; 55 - 56 - #[allow(unused)] 57 - #[derive(Debug, Deserialize)] 58 - #[serde(rename = "sh.tangled.publicKey", rename_all = "camelCase")] 59 - pub struct ShTangledPublicKey { 60 - #[serde(rename = "$type")] 61 - pub r#type: String, 62 - pub name: String, 63 - pub key: String, 64 - #[serde(alias = "created")] 65 - pub created_at: String, 66 - } 67 57 68 58 fn list_records_url(mut pds: Url, collection: &str, repo: &Did) -> Url { 69 59 pds.set_path("/xrpc/com.atproto.repo.listRecords"); ··· 71 83 .ok_or(anyhow::anyhow!("DID document does not declare a pds"))? 72 84 .service_endpoint; 73 85 74 - let response: crate::atproto::Response<ShTangledPublicKey> = self 86 + let response: crate::atproto::Response<LexiconPublicKey> = self 75 87 .public_http 76 88 .get(list_records_url(pds.clone(), "sh.tangled.publicKey", did)) 77 89 .send()
+258
crates/knot/src/model/nicediff.rs
··· 1 + use std::borrow::Cow; 2 + 3 + use crate::types::sh::tangled::repo::diff::{ 4 + Commit, Diff, Line, Name, NiceDiff, Stat, TextFragment, 5 + }; 6 + use gix::{ 7 + Repository, 8 + diff::blob::{ 9 + Algorithm, UnifiedDiff, 10 + pipeline::{Mode, WorktreeRoots}, 11 + unified_diff::{ConsumeHunk, ContextSize, DiffLineKind}, 12 + }, 13 + object::tree::diff::Action, 14 + }; 15 + 16 + #[derive(Debug, thiserror::Error)] 17 + enum Error { 18 + #[error("{0}")] 19 + Io(#[from] std::io::Error), 20 + #[error("{0}")] 21 + Platform(#[from] gix::diff::blob::platform::set_resource::Error), 22 + #[error("{0}")] 23 + Diff(#[from] gix::object::blob::diff::lines::Error<SubError>), 24 + } 25 + 26 + #[derive(Debug, thiserror::Error)] 27 + enum SubError {} 28 + 29 + #[derive(Default)] 30 + struct DiffHunkConsumer { 31 + hunks: Vec<TextFragment>, 32 + } 33 + 34 + impl DiffHunkConsumer { 35 + pub fn with_capacity(capacity: usize) -> Self { 36 + Self { 37 + hunks: Vec::with_capacity(capacity), 38 + } 39 + } 40 + } 41 + 42 + impl ConsumeHunk for DiffHunkConsumer { 43 + type Out = Vec<TextFragment>; 44 + 45 + fn consume_hunk( 46 + &mut self, 47 + header: gix::diff::blob::unified_diff::HunkHeader, 48 + raw_lines: &[(gix::diff::blob::unified_diff::DiffLineKind, &[u8])], 49 + ) -> std::io::Result<()> { 50 + let mut lines_added = 0; 51 + let mut lines_deleted = 0; 52 + let mut leading_context = 0; 53 + let mut trailing_context = 0; 54 + let lines = raw_lines 55 + .iter() 56 + .map(|(kind, data)| { 57 + let line = String::from_utf8_lossy(data).to_string(); 58 + match kind { 59 + DiffLineKind::Context => { 60 + *if lines_added == 0 && lines_deleted == 0 { 61 + &mut leading_context 62 + } else { 63 + &mut trailing_context 64 + } += 1; 65 + Line::Context { line } 66 + } 67 + DiffLineKind::Remove => { 68 + lines_deleted += 1; 69 + Line::Addition { line } 70 + } 71 + DiffLineKind::Add => { 72 + lines_added += 1; 73 + Line::Deletion { line } 74 + } 75 + } 76 + }) 77 + .collect(); 78 + 79 + let hunk = TextFragment { 80 + comment: "".into(), 81 + old_position: header.after_hunk_start, 82 + old_lines: header.after_hunk_len, 83 + new_position: header.before_hunk_start, 84 + new_lines: header.before_hunk_len, 85 + lines, 86 + lines_added, 87 + lines_deleted, 88 + leading_context, 89 + trailing_context, 90 + }; 91 + 92 + self.hunks.push(hunk); 93 + Ok(()) 94 + } 95 + 96 + fn finish(self) -> Self::Out { 97 + self.hunks 98 + } 99 + } 100 + 101 + /// Produce a unified diff from the first parent of the specified commit. 102 + /// 103 + pub fn unified_diff_from_parent<'r>(commit: gix::Commit<'r>) -> anyhow::Result<NiceDiff> { 104 + let current_tree = commit.tree()?; 105 + let parent_tree = match commit.parent_ids().next() { 106 + Some(id) => commit.repo.find_commit(id)?.tree()?, 107 + // If the commit does not have a parent, compute the diff against an empty tree. 108 + None => commit.repo.empty_tree(), 109 + }; 110 + 111 + let (stat, deltas) = unified_diff(&commit.repo, &current_tree, &parent_tree)?; 112 + let decoded = commit.decode()?; 113 + let diff = NiceDiff { 114 + commit: Commit { 115 + message: decoded.message.to_string(), 116 + author: super::convert::try_convert_signature(decoded.author)?, 117 + this: commit.id.into(), 118 + parent: commit.parent_ids().next().map(|id| id.detach().into()), 119 + pgp_signature: commit 120 + .signature()? 121 + .map(|(sig, _)| Cow::Owned(sig.to_string())) 122 + .unwrap_or(Cow::Borrowed("")), 123 + committer: super::convert::try_convert_signature(decoded.committer)?, 124 + tree: current_tree.id.into(), 125 + change_id: decoded 126 + .extra_headers() 127 + .find("change-id") 128 + .map(|v| Cow::Owned(v.to_string())) 129 + .unwrap_or(Cow::Borrowed("")), 130 + }, 131 + stat, 132 + deltas, 133 + }; 134 + 135 + Ok(diff) 136 + } 137 + 138 + /// Produce a unified diff between two trees. 139 + /// 140 + pub fn unified_diff<'r>( 141 + repo: &'r Repository, 142 + this_tree: &gix::Tree<'r>, 143 + parent_tree: &gix::Tree<'r>, 144 + ) -> anyhow::Result<(Stat, Vec<Diff>)> { 145 + let mut cache = repo.diff_resource_cache(Mode::ToGit, WorktreeRoots::default())?; 146 + 147 + // let mut diffs: BTreeMap<_, Vec<DiffHunk>> = BTreeMap::default(); 148 + let mut deltas: Vec<Diff> = Default::default(); 149 + let mut insertions = 0; 150 + let mut deletions = 0; 151 + 152 + let context = 3; 153 + parent_tree 154 + .changes()? 155 + .for_each_to_obtain_tree(&this_tree, |change| { 156 + use gix::object::tree::diff::Change; 157 + let mut delta = match change { 158 + Change::Addition { location, .. } => Diff { 159 + name: Name { 160 + old: location.to_string(), 161 + new: location.to_string(), 162 + }, 163 + text_fragments: vec![], 164 + is_binary: false, 165 + is_new: true, 166 + is_delete: false, 167 + is_copy: false, 168 + is_rename: false, 169 + }, 170 + Change::Deletion { location, .. } => Diff { 171 + name: Name { 172 + old: location.to_string(), 173 + new: location.to_string(), 174 + }, 175 + text_fragments: vec![], 176 + is_binary: false, 177 + is_new: false, 178 + is_delete: true, 179 + is_copy: false, 180 + is_rename: false, 181 + }, 182 + Change::Modification { location, .. } => Diff { 183 + name: Name { 184 + old: location.to_string(), 185 + new: location.to_string(), 186 + }, 187 + text_fragments: vec![], 188 + is_binary: false, 189 + is_new: false, 190 + is_delete: false, 191 + is_copy: false, 192 + is_rename: false, 193 + }, 194 + Change::Rewrite { 195 + source_location, 196 + location, 197 + copy, 198 + .. 199 + } => Diff { 200 + name: Name { 201 + old: source_location.to_string(), 202 + new: location.to_string(), 203 + }, 204 + text_fragments: vec![], 205 + is_binary: false, 206 + is_new: false, 207 + is_delete: false, 208 + is_copy: copy, 209 + is_rename: !copy, 210 + }, 211 + }; 212 + 213 + if change.entry_mode().is_blob() { 214 + let mut line_count = 0; 215 + let mut platform = change.diff(&mut cache)?; 216 + let outcome = platform.lines(|_| { 217 + line_count += 1; 218 + Ok::<_, SubError>(()) 219 + })?; 220 + 221 + if matches!( 222 + outcome.operation, 223 + gix::diff::blob::platform::prepare_diff::Operation::SourceOrDestinationIsBinary 224 + ) { 225 + delta.is_binary = true; 226 + deltas.push(delta); 227 + return Ok(Action::Continue); 228 + } 229 + 230 + let input = outcome.interned_input(); 231 + let hunks = gix::diff::blob::diff( 232 + Algorithm::Histogram, 233 + &input, 234 + UnifiedDiff::new( 235 + &input, 236 + DiffHunkConsumer::with_capacity(line_count), 237 + ContextSize::symmetrical(context), 238 + ), 239 + )?; 240 + 241 + tracing::debug!(prealloc = %line_count, outcome = %hunks.len()); 242 + insertions += hunks.iter().map(|hunk| hunk.lines_added).sum::<u32>(); 243 + deletions += hunks.iter().map(|hunk| hunk.lines_deleted).sum::<u32>(); 244 + delta.text_fragments = hunks; 245 + deltas.push(delta); 246 + } 247 + Ok::<_, Error>(Action::Continue) 248 + })?; 249 + 250 + let stat = Stat { 251 + files_changed: deltas.len() as u32, 252 + insertions, 253 + deletions, 254 + }; 255 + 256 + deltas.sort_by_cached_key(|d| d.name.old.clone()); 257 + Ok((stat, deltas)) 258 + }
+12 -18
crates/knot/src/objectid.rs crates/lexicon/src/extra/objectid.rs
··· 6 6 #[doc(hidden)] 7 7 pub struct Array; 8 8 9 + /// An object ID that can serialized as a hex-string or an array of integers. 10 + /// 11 + /// This only exists because knotserver uses both representations. 12 + /// 9 13 #[derive(Clone, PartialEq, Eq)] 10 14 pub struct ObjectId<E = Hex> { 11 - inner: gix::ObjectId, 15 + inner: gix_hash::ObjectId, 12 16 enc: PhantomData<E>, 13 17 } 14 18 15 19 impl<E> ObjectId<E> { 16 - pub fn id(&self) -> gix::ObjectId { 20 + pub fn id(&self) -> gix_hash::ObjectId { 17 21 self.inner 18 22 } 19 23 } ··· 26 22 #[inline] 27 23 fn default() -> Self { 28 24 Self { 29 - inner: gix::ObjectId::null(gix::hash::Kind::Sha1), 25 + inner: gix_hash::ObjectId::null(gix_hash::Kind::Sha1), 30 26 enc: PhantomData, 31 27 } 32 28 } ··· 38 34 } 39 35 } 40 36 41 - impl<E> From<gix::ObjectId> for ObjectId<E> { 37 + impl<E> From<gix_hash::ObjectId> for ObjectId<E> { 42 38 #[inline] 43 - fn from(value: gix::ObjectId) -> Self { 39 + fn from(value: gix_hash::ObjectId) -> Self { 44 40 Self { 45 41 inner: value, 46 42 enc: PhantomData, ··· 48 44 } 49 45 } 50 46 51 - impl<E> From<ObjectId<E>> for gix::ObjectId { 47 + impl<E> From<ObjectId<E>> for gix_hash::ObjectId { 52 48 #[inline] 53 49 fn from(value: ObjectId<E>) -> Self { 54 50 value.inner 55 - } 56 - } 57 - 58 - impl<E> From<gix::Id<'_>> for ObjectId<E> { 59 - #[inline] 60 - fn from(value: gix::Id<'_>) -> Self { 61 - Self { 62 - inner: value.detach(), 63 - enc: PhantomData, 64 - } 65 51 } 66 52 } 67 53 ··· 82 88 } 83 89 84 90 impl FromStr for ObjectId<Hex> { 85 - type Err = gix::hash::decode::Error; 91 + type Err = gix_hash::decode::Error; 86 92 87 93 fn from_str(s: &str) -> Result<Self, Self::Err> { 88 - let id = gix::ObjectId::from_hex(s.as_bytes())?; 94 + let id = gix_hash::ObjectId::from_hex(s.as_bytes())?; 89 95 Ok(Self { 90 96 inner: id, 91 97 enc: PhantomData,
+4 -64
crates/knot/src/public.rs
··· 1 - use crate::model::Knot; 2 - use axum::{ 3 - Router, 4 - extract::{WebSocketUpgrade, ws::WebSocket}, 5 - }; 6 - use std::time::Duration; 7 - use tokio::time::Instant; 8 - 9 1 pub mod git; 10 2 pub mod xrpc; 11 3 12 - pub fn router() -> Router<Knot> { 13 - Router::new() 4 + use crate::model::Knot; 5 + 6 + pub fn router() -> axum::Router<Knot> { 7 + axum::Router::new() 14 8 .without_v07_checks() 15 - // .route("/events", axum::routing::get(events_handler)) 16 9 .nest("/xrpc", xrpc::router()) 17 10 .nest("/{owner}/{name}", serve_git::router()) 18 - } 19 - 20 - pub async fn events_handler(ws: WebSocketUpgrade) -> axum::response::Response { 21 - ws.on_upgrade(handle_events_socket) 22 - } 23 - 24 - pub async fn handle_events_socket(mut socket: WebSocket) { 25 - use axum::extract::ws::Message; 26 - 27 - let start = Instant::now(); 28 - let mut timeout = tokio::time::interval(Duration::from_secs(45)); 29 - 30 - // The first tick of an interval always resolves immediately. 31 - timeout.tick().await; 32 - 33 - tracing::trace!(?socket, "new websocket connection"); 34 - loop { 35 - let message = tokio::select! { 36 - now = timeout.tick() => { 37 - let diff = now.saturating_duration_since(start); 38 - let payload = format!("{}", diff.as_millis()); 39 - if let Err(error) = socket.send(Message::Ping(payload.into())).await { 40 - tracing::error!(?error); 41 - break; 42 - } 43 - continue; 44 - } 45 - Some(message) = socket.recv() => match message { 46 - Ok(message) => message, 47 - Err(error) => { 48 - tracing::error!(?error); 49 - break; 50 - } 51 - } 52 - }; 53 - 54 - match message { 55 - Message::Pong(payload) => match std::str::from_utf8(&payload) { 56 - Ok(payload) => tracing::debug!(%payload, "received pong"), 57 - Err(error) => { 58 - tracing::error!(?error, ?payload, "received non-utf8 payload in pong") 59 - } 60 - }, 61 - Message::Close(_) => { 62 - tracing::debug!("client closed connection"); 63 - break; 64 - } 65 - Message::Text(payload) => { 66 - tracing::info!(payload = payload.as_str(), "received message from client"); 67 - } 68 - _ => {} // 69 - } 70 - } 71 11 }
-47
crates/knot/src/public/git/helpers.rs
··· 1 - use std::ffi::OsStr; 2 - 3 - trait SetEnv { 4 - fn env<K, V>(&mut self, key: K, val: V) -> &mut Self 5 - where 6 - K: AsRef<OsStr>, 7 - V: AsRef<OsStr>; 8 - } 9 - 10 - macro_rules! impl_set_env { 11 - ($ty:ty) => { 12 - impl SetEnv for $ty { 13 - fn env<K, V>(&mut self, key: K, val: V) -> &mut Self 14 - where 15 - K: AsRef<OsStr>, 16 - V: AsRef<OsStr>, 17 - { 18 - Self::env(self, key, val) 19 - } 20 - } 21 - }; 22 - } 23 - 24 - impl_set_env!(std::process::Command); 25 - impl_set_env!(tokio::process::Command); 26 - 27 - pub trait SetOptionEnv { 28 - /// Inserts or updates an environment variable mapping if, and only if, 29 - /// `val` is [`Some`]. 30 - fn option_env<K, V>(&mut self, key: K, val: Option<V>) -> &mut Self 31 - where 32 - K: AsRef<OsStr>, 33 - V: AsRef<OsStr>; 34 - } 35 - 36 - impl<T: SetEnv> SetOptionEnv for T { 37 - fn option_env<K, V>(&mut self, key: K, val: Option<V>) -> &mut Self 38 - where 39 - K: AsRef<OsStr>, 40 - V: AsRef<OsStr>, 41 - { 42 - if let Some(val) = val { 43 - self.env(key, val); 44 - } 45 - self 46 - } 47 - }
+4 -1
crates/knot/src/public/xrpc.rs
··· 14 14 .merge(sh::tangled::knot::version()) 15 15 .merge(sh::tangled::repo::blob()) 16 16 .merge(sh::tangled::repo::branches()) 17 - // .merge(sh::tangled::repo::diff()) 17 + .merge(sh::tangled::repo::diff()) 18 18 .merge(sh::tangled::repo::get_default_branch()) 19 19 .merge(sh::tangled::repo::languages()) 20 20 .merge(sh::tangled::repo::log()) ··· 115 115 ise!(std::io::Error); 116 116 ise!(gix::diff::blob::platform::set_resource::Error); 117 117 ise!(gix::diff::options::init::Error); 118 + ise!(gix::repository::diff_resource_cache::Error); 118 119 ise!(gix::object::commit::Error); 120 + ise!(gix::objs::decode::Error); 119 121 ise!(gix::object::find::existing::Error); 120 122 ise!(gix::object::find::existing::with_conversion::Error); 121 123 ise!(gix::object::tree::diff::for_each::Error); ··· 125 123 ise!(gix::reference::iter::init::Error); 126 124 ise!(gix::reference::head_tree::Error); 127 125 ise!(gix::reference::find::existing::Error, StatusCode::NOT_FOUND); 126 + ise!(gix::repository::commit_graph_if_enabled::Error);
+7 -4
crates/knot/src/public/xrpc/sh/tangled.rs
··· 1 - use crate::{model::Knot, types::sh::tangled::Owner}; 1 + use crate::model::Knot; 2 2 use axum::{ 3 3 Json, 4 4 extract::{FromRef, State}, 5 5 }; 6 + use lexicon::sh::tangled::OwnerOutput; 6 7 7 8 pub mod knot; 8 9 pub mod repo; 9 10 10 11 #[xrpc::query("sh.tangled.owner")] 11 - #[tracing::instrument] 12 - pub async fn owner(State(knot): State<Knot>) -> Json<Owner> 12 + pub async fn owner(State(knot): State<Knot>) -> Json<OwnerOutput> 13 13 where 14 14 Knot: FromRef<S>, 15 15 { 16 - Owner::new(knot.owner().clone()).into() 16 + OwnerOutput { 17 + owner: knot.owner().clone(), 18 + } 19 + .into() 17 20 }
+4 -9
crates/knot/src/public/xrpc/sh/tangled/knot.rs
··· 1 1 use axum::Json; 2 - 3 - /// Response type for 'sh.tangled.knot.version'. 4 - #[derive(Debug, serde::Serialize)] 5 - pub struct Version { 6 - pub version: &'static str, 7 - } 2 + use lexicon::sh::tangled::knot::VersionOutput; 8 3 9 4 #[xrpc::query("sh.tangled.knot.version")] 10 - pub async fn version() -> Json<Version> { 11 - Json(Version { 12 - version: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), 5 + pub async fn version() -> Json<VersionOutput> { 6 + Json(VersionOutput { 7 + version: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).into(), 13 8 }) 14 9 }
+34 -22
crates/knot/src/public/xrpc/sh/tangled/repo.rs
··· 1 1 use crate::{ 2 2 model::{Knot, errors::InvalidRequest}, 3 3 public::xrpc::XrpcError, 4 - types::sh::tangled::repo::*, 4 + types::sh::tangled::repo::{ 5 + branches::BranchesOutput, diff::DiffResponse, log::LogOutput, tags::TagsOutput, 6 + }, 5 7 }; 6 8 use axum::{ 7 9 Json, 8 10 extract::{FromRef, Query, State}, 9 11 response::{IntoResponse, Response}, 10 12 }; 13 + use lexicon::sh::tangled::repo::{ 14 + blob, branches, diff, get_default_branch, languages, log, tags, tree, 15 + }; 11 16 12 17 type Result<T> = std::result::Result<Json<T>, XrpcError>; 13 18 14 19 #[xrpc::query("sh.tangled.repo.getDefaultBranch")] 15 - #[tracing::instrument] 20 + #[tracing::instrument(skip(knot))] 16 21 pub async fn get_default_branch( 17 22 State(knot): State<Knot>, 18 - Query(params): Query<GetDefaultBranchParams>, 19 - ) -> Result<DefaultBranchResponse> 23 + Query(params): Query<get_default_branch::Input>, 24 + ) -> Result<get_default_branch::Output> 20 25 where 21 26 Knot: FromRef<S>, 22 27 { ··· 29 24 } 30 25 31 26 #[xrpc::query("sh.tangled.repo.branches")] 32 - #[tracing::instrument] 27 + #[tracing::instrument(skip(knot))] 33 28 pub async fn branches( 34 29 State(knot): State<Knot>, 35 - Query(params): Query<BranchesParams>, 36 - ) -> Result<BranchesResponse> 30 + Query(params): Query<branches::Input>, 31 + ) -> Result<BranchesOutput> 37 32 where 38 33 Knot: FromRef<S>, 39 34 { ··· 49 44 } 50 45 51 46 #[xrpc::query("sh.tangled.repo.log")] 52 - #[tracing::instrument] 53 - pub async fn log(State(knot): State<Knot>, Query(params): Query<LogParams>) -> Result<LogResponse> 47 + #[tracing::instrument(skip(knot))] 48 + pub async fn log(State(knot): State<Knot>, Query(params): Query<log::Input>) -> Result<LogOutput> 54 49 where 55 50 Knot: FromRef<S>, 56 51 { ··· 66 61 } 67 62 68 63 #[xrpc::query("sh.tangled.repo.tags")] 69 - #[tracing::instrument] 70 - pub async fn tags(State(knot): State<Knot>, Query(params): Query<TagParams>) -> Result<TagsResponse> 64 + #[tracing::instrument(skip(knot))] 65 + pub async fn tags(State(knot): State<Knot>, Query(params): Query<tags::Input>) -> Result<TagsOutput> 71 66 where 72 67 Knot: FromRef<S>, 73 68 { ··· 75 70 } 76 71 77 72 #[xrpc::query("sh.tangled.repo.tree")] 78 - #[tracing::instrument] 73 + #[tracing::instrument(skip(knot))] 79 74 pub async fn tree( 80 75 State(knot): State<Knot>, 81 - Query(params): Query<TreeParams>, 82 - ) -> Result<TreeResponse> 76 + Query(params): Query<tree::Input>, 77 + ) -> Result<tree::Output> 83 78 where 84 79 Knot: FromRef<S>, 85 80 { ··· 87 82 } 88 83 89 84 #[xrpc::query("sh.tangled.repo.blob")] 90 - #[tracing::instrument] 85 + #[tracing::instrument(skip(knot))] 91 86 pub async fn blob( 92 87 State(knot): State<Knot>, 93 - Query(params): Query<BlobParams>, 88 + Query(params): Query<blob::Input>, 94 89 ) -> std::result::Result<Response, XrpcError> 95 90 where 96 91 Knot: FromRef<S>, 97 92 { 93 + use crate::types::sh::tangled::repo::blob::BlobResponse; 98 94 match knot.blob(&params).await? { 99 95 BlobResponse::Json(blob) => Ok(Json(blob).into_response()), 100 96 BlobResponse::Raw(blob) => Ok(blob.into_response()), ··· 103 97 } 104 98 105 99 #[xrpc::query("sh.tangled.repo.languages")] 106 - #[tracing::instrument] 107 - pub async fn languages() -> Json<Languages> { 100 + #[tracing::instrument(skip(_knot))] 101 + pub async fn languages( 102 + State(_knot): State<Knot>, 103 + Query(params): Query<languages::Input>, 104 + ) -> Json<languages::Output> 105 + where 106 + Knot: FromRef<S>, 107 + { 108 108 Default::default() 109 109 } 110 110 111 111 #[xrpc::query("sh.tangled.repo.diff")] 112 - #[tracing::instrument] 113 - pub async fn diff( 112 + #[tracing::instrument(skip(knot))] 113 + pub async fn diff<S>( 114 114 State(knot): State<Knot>, 115 - Query(params): Query<DiffParams>, 115 + Query(params): Query<diff::Input>, 116 116 ) -> Result<DiffResponse> 117 117 where 118 118 Knot: FromRef<S>, 119 119 { 120 - Ok(knot.diff(&params).await?.into()) 120 + Ok(knot.diff(params).await?.into()) 121 121 }
-250
crates/knot/src/repospec.rs
··· 1 - use serde::{Deserialize, Serialize}; 2 - use std::str::FromStr; 3 - 4 - /// Repository path specifier. 5 - #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] 6 - #[serde(try_from = "UnvalidatedRepoSpec")] 7 - pub struct RepoSpec { 8 - /// DID of the repository owner 9 - pub owner: Box<str>, 10 - 11 - /// Repository name 12 - pub name: Box<str>, 13 - } 14 - 15 - #[derive(Deserialize)] 16 - struct UnvalidatedRepoSpec { 17 - pub owner: Box<str>, 18 - pub name: Box<str>, 19 - } 20 - 21 - impl TryFrom<UnvalidatedRepoSpec> for RepoSpec { 22 - type Error = RepoSpecError; 23 - 24 - #[inline] 25 - fn try_from( 26 - UnvalidatedRepoSpec { owner, name }: UnvalidatedRepoSpec, 27 - ) -> Result<Self, Self::Error> { 28 - Self::from_parts(owner, name) 29 - } 30 - } 31 - 32 - impl RepoSpec { 33 - pub fn from_parts( 34 - owner: impl Into<Box<str>>, 35 - name: impl Into<Box<str>>, 36 - ) -> Result<Self, RepoSpecError> { 37 - let owner = owner.into(); 38 - let name = name.into(); 39 - 40 - validate(owner.as_ref())?; 41 - validate(name.as_ref())?; 42 - 43 - Ok(Self { owner, name }) 44 - } 45 - 46 - pub const fn owner(&self) -> &str { 47 - &self.owner 48 - } 49 - 50 - pub const fn name(&self) -> &str { 51 - &self.name 52 - } 53 - } 54 - 55 - impl std::fmt::Display for RepoSpec { 56 - #[inline] 57 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 58 - write!(f, "{}/{}", self.owner, self.name) 59 - } 60 - } 61 - 62 - impl FromStr for RepoSpec { 63 - type Err = RepoSpecError; 64 - 65 - fn from_str(s: &str) -> Result<Self, Self::Err> { 66 - let Some((owner, name)) = s.split_once('/') else { 67 - return Err(RepoSpecError::Format); 68 - }; 69 - 70 - validate(owner)?; 71 - validate(name)?; 72 - 73 - Ok(Self { 74 - owner: owner.into(), 75 - name: name.into(), 76 - }) 77 - } 78 - } 79 - 80 - pub mod query_param { 81 - //! Serialize/Deserialize a `RepoSpec` from a string. 82 - //! 83 - //! # Example 84 - //! 85 - //! ```rust,no_run 86 - //! # use knot::repospec::RepoSpec; 87 - //! #[derive(serde::Deserialize)] 88 - //! struct Params { 89 - //! #[serde(with = "knot::repospec::query_param")] 90 - //! repo: RepoSpec, 91 - //! } 92 - //! ``` 93 - //! 94 - use super::RepoSpec; 95 - use serde::{Deserialize, Deserializer, Serializer}; 96 - use std::{borrow::Cow, str::FromStr}; 97 - 98 - pub fn deserialize<'de, D>(deserializer: D) -> Result<RepoSpec, D::Error> 99 - where 100 - D: Deserializer<'de>, 101 - { 102 - let s = <Cow<str>>::deserialize(deserializer)?; 103 - let repo = RepoSpec::from_str(&s).map_err(serde::de::Error::custom)?; 104 - Ok(repo) 105 - } 106 - 107 - #[allow(unused)] 108 - pub fn serialize<S>(v: &RepoSpec, serializer: S) -> Result<S::Ok, S::Error> 109 - where 110 - S: Serializer, 111 - { 112 - serializer.serialize_str(&format!("{}/{}", v.owner(), v.name())) 113 - } 114 - } 115 - 116 - fn validate(name: &str) -> Result<(), RepoSpecError> { 117 - static FORBIDDEN_SUBSTRINGS: &[&str] = &[".."]; 118 - static FORBIDDEN_PREFIXES: &[&str] = &[".", "-"]; 119 - static ACCEPTED_SYMBOLS: &[char] = &['.', '-', '_', ':']; 120 - 121 - if name.is_empty() { 122 - return Err(RepoSpecError::Empty); 123 - } 124 - 125 - for value in name.chars() { 126 - if !(value.is_ascii_alphanumeric() || ACCEPTED_SYMBOLS.contains(&value)) { 127 - return Err(RepoSpecError::Character { value }); 128 - } 129 - } 130 - 131 - for substring in FORBIDDEN_SUBSTRINGS { 132 - if name.contains(substring) { 133 - return Err(RepoSpecError::Substring { substring }); 134 - } 135 - } 136 - 137 - for prefix in FORBIDDEN_PREFIXES { 138 - if name.starts_with(prefix) { 139 - return Err(RepoSpecError::Prefix { prefix }); 140 - } 141 - } 142 - 143 - Ok(()) 144 - } 145 - 146 - #[derive(Debug, thiserror::Error)] 147 - pub enum RepoSpecError { 148 - #[error("expected a repository specifier in form '{{owner}}/{{name}}'")] 149 - Format, 150 - #[error("repository name cannot be empty")] 151 - Empty, 152 - #[error("invalid character in repository name: '{value}'")] 153 - Character { value: char }, 154 - #[error("invalid sub-string in repository name: '{substring}'")] 155 - Substring { substring: &'static str }, 156 - #[error("invalid prefix for respository name: '{prefix}'")] 157 - Prefix { prefix: &'static str }, 158 - } 159 - 160 - #[cfg(test)] 161 - mod tests { 162 - use axum::{ 163 - Json, Router, 164 - body::Body, 165 - extract::{Path, Query}, 166 - http::{Request, Response}, 167 - routing, 168 - }; 169 - use http_body_util::BodyExt as _; 170 - use serde::Deserialize; 171 - use tower::ServiceExt as _; 172 - 173 - use super::RepoSpec; 174 - 175 - const SAMPLE_OWNER: &str = "did:plc:65gha4t3avpfpzmvpbwovss7"; 176 - const SAMPLE_NAME: &str = "gordian"; 177 - 178 - async fn check_response(res: Response<Body>) { 179 - let body = res.into_body().collect().await.unwrap(); 180 - let repo: RepoSpec = serde_json::from_slice(&body.to_bytes()).unwrap(); 181 - 182 - assert_eq!(repo.owner.as_ref(), SAMPLE_OWNER); 183 - assert_eq!(repo.name.as_ref(), SAMPLE_NAME); 184 - } 185 - 186 - #[tokio::test] 187 - async fn extract_from_query() { 188 - #[derive(Deserialize)] 189 - struct Params { 190 - #[serde(with = "super::query_param")] 191 - repo: RepoSpec, 192 - } 193 - 194 - let res = Router::new() 195 - .route( 196 - "/something", 197 - routing::get(async |Query(Params { repo }): Query<Params>| Json(repo)), 198 - ) 199 - .oneshot( 200 - Request::builder() 201 - .uri(format!("/something?repo={SAMPLE_OWNER}/{SAMPLE_NAME}")) 202 - .body(Body::empty()) 203 - .unwrap(), 204 - ) 205 - .await 206 - .unwrap(); 207 - 208 - check_response(res).await; 209 - } 210 - 211 - #[tokio::test] 212 - async fn extract_from_path() { 213 - let res = Router::new() 214 - .route( 215 - "/{owner}/{name}", 216 - routing::get(async |Path(repo): Path<RepoSpec>| Json(repo)), 217 - ) 218 - .oneshot( 219 - Request::builder() 220 - .uri(format!("/{SAMPLE_OWNER}/{SAMPLE_NAME}")) 221 - .body(Body::empty()) 222 - .unwrap(), 223 - ) 224 - .await 225 - .unwrap(); 226 - 227 - check_response(res).await; 228 - } 229 - 230 - #[tokio::test] 231 - async fn extract_from_path_backwards() { 232 - // Ensure we are extracting placeholders by name not position. 233 - 234 - let res = Router::new() 235 - .route( 236 - "/{name}/{owner}", 237 - routing::get(async |Path(repo): Path<RepoSpec>| Json(repo)), 238 - ) 239 - .oneshot( 240 - Request::builder() 241 - .uri(format!("/{SAMPLE_NAME}/{SAMPLE_OWNER}")) 242 - .body(Body::empty()) 243 - .unwrap(), 244 - ) 245 - .await 246 - .unwrap(); 247 - 248 - check_response(res).await; 249 - } 250 - }
-23
crates/knot/src/revspec.rs
··· 1 - use axum::{ 2 - extract::{FromRequestParts, Query, rejection::QueryRejection}, 3 - http::request::Parts, 4 - }; 5 - use serde::Deserialize; 6 - 7 - #[derive(Debug, Deserialize)] 8 - pub struct RevSpec { 9 - pub r#ref: String, 10 - } 11 - 12 - impl<S: Send + Sync> FromRequestParts<S> for RevSpec { 13 - type Rejection = QueryRejection; 14 - 15 - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 16 - let Query(revspec) = Query::from_request_parts(parts, state).await?; 17 - Ok(revspec) 18 - } 19 - } 20 - 21 - impl RevSpec { 22 - // 23 - }
+176 -343
crates/knot/src/types.rs
··· 1 1 pub mod sh { 2 2 pub mod tangled { 3 - use identity::Did; 4 - use serde::{Deserialize, Serialize}; 5 - use time::OffsetDateTime; 6 - 7 - /// Response for `sh.tangled.owner`. 8 - #[derive(Debug, Serialize)] 9 - pub struct Owner { 10 - pub owner: Did, 11 - } 12 - 13 - impl Owner { 14 - pub fn new(owner: Did) -> Self { 15 - Self { owner } 16 - } 17 - } 18 - 19 - /// Lexicon definition `sh.tangled.repo` 20 - #[derive(Debug, Deserialize)] 21 - #[serde(rename_all = "camelCase")] 22 - pub struct Repo { 23 - #[serde(rename = "$type")] 24 - pub typ: String, 25 - pub knot: String, 26 - pub name: String, 27 - #[serde(default)] 28 - pub labels: Vec<String>, 29 - #[serde(with = "time::serde::rfc3339")] 30 - pub created_at: OffsetDateTime, 31 - } 32 - 33 3 pub mod repo { 34 - use crate::{ObjectId, objectid::Array, repospec::RepoSpec}; 35 - use serde::{Deserialize, Serialize}; 36 - use std::{borrow::Cow, collections::HashMap, path::PathBuf}; 37 - use time::OffsetDateTime; 4 + pub mod blob { 5 + use lexicon::sh::tangled::repo::blob; 38 6 39 - #[derive(Debug, Deserialize)] 40 - pub struct GetDefaultBranchParams { 41 - #[serde(with = "crate::repospec::query_param")] 42 - pub repo: RepoSpec, 7 + #[derive(Debug)] 8 + pub enum BlobResponse { 9 + Json(blob::Output), 10 + Raw(Vec<u8>), 11 + } 43 12 } 44 13 45 - /// Response type for the `sh.tangled.repo.getDefaultBranch` query. 46 - #[derive(Debug, Default, Serialize)] 47 - #[serde(rename_all = "camelCase")] 48 - pub struct DefaultBranchResponse { 49 - /// Short-name of the default branch. 50 - pub name: String, 14 + pub mod branches { 15 + use lexicon::sh::tangled::repo::refs; 16 + use serde::Serialize; 51 17 52 - /// ID of the most recent commit on the default branch 53 - // 54 - // @NOTE Official knotserver always returns an empty string. 55 - #[serde(skip_serializing_if = "Option::is_none")] 56 - pub hash: Option<ObjectId>, 18 + /// Output of `sh.tangled.repo.branches` query. 19 + #[derive(Debug, Default, Serialize)] 20 + #[serde(rename_all = "camelCase")] 21 + pub struct BranchesOutput { 22 + #[serde(skip_serializing_if = "Vec::is_empty")] 23 + pub branches: Vec<Branch>, 24 + } 57 25 58 - /// Timestamp of the most recent commit on the default branch 59 - // 60 - // @NOTE Official knotserver always returns the unix epoch. 61 - #[serde( 62 - with = "time::serde::rfc3339::option", 63 - skip_serializing_if = "Option::is_none" 64 - )] 65 - pub when: Option<OffsetDateTime>, 26 + #[derive(Debug, Serialize)] 27 + #[serde(rename_all = "camelCase")] 28 + pub struct Branch { 29 + pub reference: refs::Reference, 30 + pub commit: refs::Commit, 31 + #[serde(rename = "is_deafult")] 32 + pub is_default: bool, 33 + } 66 34 } 67 35 68 - #[derive(Debug, Default, Serialize)] 69 - #[serde(rename_all = "camelCase")] 70 - pub struct Reference { 71 - /// Short-name of the reference. 72 - pub name: String, 36 + pub mod diff { 37 + use lexicon::{extra::objectid::ObjectId, sh::tangled::repo::refs}; 38 + use serde::Serialize; 39 + use std::borrow::Cow; 73 40 74 - // @TODO What is this for? 75 - pub hash: ObjectId, 41 + #[derive(Debug, Serialize)] 42 + pub struct DiffResponse { 43 + #[serde(rename = "ref")] 44 + pub rev: Box<str>, 45 + pub diff: NiceDiff, 46 + } 47 + 48 + /// Unified diff replicating Tangled's `NiceDiff` structure. 49 + /// 50 + /// See: <https://tangled.org/@tangled.org/core/blob/master/types/diff.go#L44> 51 + #[derive(Debug, Serialize)] 52 + pub struct NiceDiff { 53 + pub commit: Commit, 54 + pub stat: Stat, 55 + #[serde(rename = "diff")] 56 + pub deltas: Vec<Diff>, 57 + } 58 + 59 + #[derive(Debug, Serialize)] 60 + pub struct Commit { 61 + pub message: String, 62 + pub author: refs::Signature, 63 + pub this: ObjectId, 64 + #[serde(skip_serializing_if = "Option::is_none")] 65 + pub parent: Option<ObjectId>, 66 + #[serde(default)] 67 + pub pgp_signature: Cow<'static, str>, 68 + pub committer: refs::Signature, 69 + pub tree: ObjectId, 70 + #[serde(default)] 71 + pub change_id: Cow<'static, str>, 72 + } 73 + 74 + #[derive(Debug, Default, Serialize)] 75 + pub struct Stat { 76 + pub files_changed: u32, 77 + pub insertions: u32, 78 + pub deletions: u32, 79 + } 80 + 81 + #[derive(Debug, Default, Serialize)] 82 + pub struct Diff { 83 + pub name: Name, 84 + pub text_fragments: Vec<TextFragment>, 85 + pub is_binary: bool, 86 + pub is_new: bool, 87 + pub is_delete: bool, 88 + pub is_copy: bool, 89 + pub is_rename: bool, 90 + } 91 + 92 + #[derive(Debug, Default, Serialize)] 93 + pub struct Name { 94 + pub old: String, 95 + pub new: String, 96 + } 97 + 98 + #[derive(Debug, Default, Serialize)] 99 + #[serde(rename_all = "PascalCase")] 100 + pub struct TextFragment { 101 + #[serde(skip_serializing_if = "str::is_empty")] 102 + pub comment: Cow<'static, str>, 103 + pub old_position: u32, 104 + pub old_lines: u32, 105 + pub new_position: u32, 106 + pub new_lines: u32, 107 + pub lines_added: u32, 108 + pub lines_deleted: u32, 109 + pub leading_context: u32, 110 + pub trailing_context: u32, 111 + #[serde(skip_serializing_if = "Vec::is_empty")] 112 + pub lines: Vec<Line>, 113 + } 114 + 115 + #[derive(Debug)] 116 + pub enum Line { 117 + Context { line: String }, 118 + Addition { line: String }, 119 + Deletion { line: String }, 120 + } 121 + 122 + // Manual impl because this enum is tagged with an integer. 123 + impl Serialize for Line { 124 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 125 + where 126 + S: serde::Serializer, 127 + { 128 + #[derive(Serialize)] 129 + #[serde(rename_all = "PascalCase")] 130 + struct IntTagged<'a> { 131 + op: u8, 132 + line: &'a str, 133 + } 134 + 135 + match self { 136 + Self::Context { line } => IntTagged { op: 0, line }, 137 + Self::Addition { line } => IntTagged { op: 1, line }, 138 + Self::Deletion { line } => IntTagged { op: 2, line }, 139 + } 140 + .serialize(serializer) 141 + } 142 + } 76 143 } 77 144 78 - /// Git commit signature (ie. the author or committer). 79 - #[derive(Debug, Serialize)] 80 - #[serde(rename_all = "PascalCase")] 81 - pub struct Signature { 82 - pub name: String, 83 - pub email: String, 84 - #[serde(with = "time::serde::rfc3339")] 85 - pub when: OffsetDateTime, 145 + pub mod log { 146 + use lexicon::sh::tangled::repo::refs; 147 + use serde::Serialize; 148 + 149 + #[derive(Debug, Serialize)] 150 + pub struct LogOutput { 151 + pub commits: Vec<refs::Commit>, 152 + pub log: bool, 153 + pub total: usize, 154 + pub page: usize, 155 + pub per_page: u16, 156 + } 86 157 } 87 158 88 - #[derive(Debug, Serialize)] 89 - #[serde(rename_all = "PascalCase")] 90 - pub struct Commit { 91 - pub hash: ObjectId<Array>, 92 - pub author: Signature, 93 - pub committer: Signature, 94 - pub merge_tag: String, 95 - #[serde(rename = "PGPSignature")] 96 - pub signature: Option<String>, 97 - pub message: String, 98 - pub tree_hash: ObjectId<Array>, 99 - pub parent_hashes: Vec<ObjectId<Array>>, 100 - pub encoding: Cow<'static, str>, 101 - /// Non-standard object headers. Values are always base64 encoded. 102 - pub extra_headers: HashMap<String, String>, 103 - } 159 + pub mod tags { 160 + use lexicon::{ 161 + extra::objectid::{Array, ObjectId}, 162 + sh::tangled::repo::refs, 163 + }; 164 + use serde::Serialize; 104 165 105 - #[derive(Debug, Serialize)] 106 - #[serde(rename_all = "camelCase")] 107 - pub struct Branch { 108 - pub reference: Reference, 109 - pub commit: Commit, 110 - #[serde(rename = "is_deafult")] 111 - pub is_default: bool, 112 - } 166 + /// Output of `sh.tangled.repo.tags` query. 167 + /// 168 + /// This is not defined in the lexicon, but models what knotserver currently 169 + /// produces. 170 + #[derive(Debug, Serialize)] 171 + pub struct TagsOutput { 172 + pub tags: Vec<Tag>, 173 + } 113 174 114 - #[derive(Debug, Deserialize)] 115 - pub struct BranchesParams { 116 - #[serde(with = "crate::repospec::query_param")] 117 - pub repo: RepoSpec, 118 - #[serde(default = "branches_limit_default")] 119 - pub limit: u16, 120 - #[serde(default)] 121 - pub cursor: usize, 122 - } 175 + #[derive(Debug, Serialize)] 176 + #[serde(rename_all = "PascalCase")] 177 + pub struct TagAnnotation { 178 + pub hash: ObjectId<Array>, 179 + pub name: String, 180 + pub tagger: Option<refs::Signature>, 181 + pub message: String, 182 + #[serde(rename = "PGPSignature")] 183 + pub pgp_signature: Option<String>, 184 + pub target_type: i32, 185 + pub target: ObjectId<Array>, 186 + } 123 187 124 - const fn branches_limit_default() -> u16 { 125 - 50 126 - } 127 - 128 - #[derive(Debug, Default, Serialize)] 129 - #[serde(rename_all = "camelCase")] 130 - pub struct BranchesResponse { 131 - #[serde(skip_serializing_if = "Vec::is_empty")] 132 - pub branches: Vec<Branch>, 133 - } 134 - 135 - #[derive(Debug, serde::Serialize)] 136 - pub struct LogResponse { 137 - pub commits: Vec<Commit>, 138 - pub log: bool, 139 - pub total: usize, 140 - pub page: usize, 141 - pub per_page: u16, 142 - } 143 - 144 - #[derive(Debug, Deserialize)] 145 - pub struct LogParams { 146 - #[serde(with = "crate::repospec::query_param")] 147 - pub repo: RepoSpec, 148 - #[serde(rename = "ref")] 149 - pub rev: Option<String>, 150 - // @NOTE The lexicon states `cursor` is a string containing a pagination 151 - // cursor of a commit id. AppView actually gives us an integer for 152 - // the number of commits to skip. 153 - #[serde(default)] 154 - pub cursor: usize, 155 - #[serde(default = "log_limit_default")] 156 - pub limit: u16, 157 - } 158 - 159 - const fn log_limit_default() -> u16 { 160 - 50 161 - } 162 - 163 - #[derive(Debug, Deserialize)] 164 - pub struct TagParams { 165 - #[serde(with = "crate::repospec::query_param")] 166 - pub repo: RepoSpec, 167 - #[serde(default)] 168 - pub cursor: usize, 169 - #[serde(default = "tag_limit_default")] 170 - pub limit: usize, 171 - } 172 - 173 - const fn tag_limit_default() -> usize { 174 - 50 175 - } 176 - 177 - #[derive(Debug, Serialize)] 178 - #[serde(rename_all = "PascalCase")] 179 - pub struct TagAnnotation { 180 - pub hash: ObjectId<Array>, 181 - pub name: String, 182 - pub tagger: Option<Signature>, 183 - pub message: String, 184 - #[serde(rename = "PGPSignature")] 185 - pub signature: Option<String>, 186 - pub target_type: i32, 187 - pub target: ObjectId<Array>, 188 - } 189 - 190 - #[derive(Debug, Serialize)] 191 - pub struct Tag { 192 - #[serde(flatten)] 193 - pub r#ref: Reference, 194 - #[serde(rename = "tag", skip_serializing_if = "Option::is_none")] 195 - pub annotation: Option<TagAnnotation>, 196 - } 197 - 198 - #[derive(Debug, Serialize)] 199 - pub struct TagsResponse { 200 - pub tags: Vec<Tag>, 201 - } 202 - 203 - #[derive(Debug, Default, Serialize)] 204 - pub struct TreeEntry { 205 - pub name: String, 206 - pub mode: Cow<'static, str>, 207 - pub size: usize, 208 - pub is_file: bool, 209 - pub is_subtree: bool, 210 - } 211 - 212 - #[derive(Debug, Deserialize)] 213 - pub struct TreeParams { 214 - #[serde(with = "crate::repospec::query_param")] 215 - pub repo: RepoSpec, 216 - #[serde(rename = "ref")] 217 - pub rev: Option<String>, 218 - pub path: Option<PathBuf>, 219 - } 220 - 221 - #[derive(Debug, Serialize)] 222 - pub struct Readme { 223 - pub contents: String, 224 - pub filename: String, 225 - } 226 - 227 - /// Return type for `sh.tangled.repo.tree`. 228 - #[derive(Debug, Serialize)] 229 - pub struct TreeResponse { 230 - pub files: Vec<TreeEntry>, 231 - #[serde(skip_serializing_if = "Option::is_none")] 232 - pub dotdot: Option<PathBuf>, 233 - #[serde(skip_serializing_if = "Option::is_none")] 234 - pub parent: Option<PathBuf>, 235 - #[serde(rename = "ref")] 236 - pub rev: String, 237 - #[serde(skip_serializing_if = "Option::is_none")] 238 - pub readme: Option<Readme>, 239 - } 240 - 241 - #[derive(Debug, Deserialize)] 242 - pub struct BlobParams { 243 - #[serde(with = "crate::repospec::query_param")] 244 - pub repo: RepoSpec, 245 - #[serde(rename = "ref")] 246 - pub rev: Option<String>, 247 - pub path: PathBuf, 248 - #[serde(default)] 249 - pub raw: bool, 250 - } 251 - 252 - #[derive(Debug, Serialize)] 253 - pub enum BlobEncoding { 254 - #[serde(rename = "utf-8")] 255 - Utf8, 256 - #[serde(rename = "base64")] 257 - Base64, 258 - } 259 - 260 - #[derive(Debug, Serialize)] 261 - #[serde(rename_all = "camelCase")] 262 - pub struct JsonBlob { 263 - pub content: String, 264 - pub encoding: BlobEncoding, 265 - pub is_binary: bool, 266 - pub mime_type: String, 267 - pub path: PathBuf, 268 - #[serde(rename = "ref")] 269 - pub rev: String, 270 - pub size: usize, 271 - } 272 - 273 - #[derive(Debug)] 274 - pub enum BlobResponse { 275 - Json(JsonBlob), 276 - Raw(Vec<u8>), 277 - } 278 - 279 - #[derive(Debug, Default, Serialize)] 280 - #[serde(rename_all = "PascalCase")] 281 - pub struct DiffLine { 282 - pub op: u8, 283 - pub line: String, 284 - } 285 - 286 - #[derive(Debug, Default, Serialize)] 287 - #[serde(rename_all = "PascalCase")] 288 - pub struct DiffHunk { 289 - // comment: String, 290 - pub old_position: u32, 291 - pub old_lines: u32, 292 - pub new_position: u32, 293 - pub new_lines: u32, 294 - pub lines_added: u32, 295 - pub lines_deleted: u32, 296 - pub leading_context: u32, 297 - pub trailing_context: u32, 298 - pub lines: Vec<DiffLine>, 299 - } 300 - 301 - #[derive(Debug, Default, Serialize)] 302 - pub struct Delta { 303 - pub name: DeltaName, 304 - #[serde(rename = "text_fragments")] 305 - pub hunks: Vec<DiffHunk>, 306 - pub is_binary: bool, 307 - pub is_new: bool, 308 - pub is_delete: bool, 309 - pub is_copy: bool, 310 - pub is_rename: bool, 311 - } 312 - 313 - #[derive(Debug, Default, Serialize)] 314 - pub struct DiffStat { 315 - pub files_changed: usize, 316 - pub insertions: usize, 317 - pub deletions: usize, 318 - } 319 - 320 - #[derive(Debug, Default, Serialize)] 321 - pub struct DeltaName { 322 - pub old: String, 323 - pub new: String, 324 - } 325 - 326 - #[derive(Debug, Serialize)] 327 - pub struct Diff { 328 - pub commit: Commit, 329 - pub stat: DiffStat, 330 - #[serde(rename = "diff")] 331 - pub deltas: Vec<Delta>, 332 - } 333 - 334 - #[derive(Debug, Deserialize)] 335 - pub struct DiffParams { 336 - #[serde(with = "crate::repospec::query_param")] 337 - pub repo: RepoSpec, 338 - #[serde(rename = "ref")] 339 - pub rev: String, 340 - } 341 - 342 - #[derive(Debug, Serialize)] 343 - pub struct DiffResponse { 344 - #[serde(rename = "ref")] 345 - pub rev: String, 346 - pub diff: Diff, 347 - } 348 - 349 - #[derive(Debug, Default, Serialize)] 350 - #[serde(rename_all = "camelCase")] 351 - pub struct Languages { 352 - #[serde(skip_serializing_if = "Vec::is_empty")] 353 - pub languages: Vec<()>, 354 - } 355 - 356 - // Mutations 357 - 358 - #[derive(Debug, Deserialize)] 359 - #[serde(rename_all = "camelCase")] 360 - pub struct CreateParams { 361 - pub rkey: String, 188 + #[derive(Debug, Serialize)] 189 + pub struct Tag { 190 + #[serde(flatten)] 191 + pub r#ref: refs::Reference, 192 + #[serde(rename = "tag", skip_serializing_if = "Option::is_none")] 193 + pub annotation: Option<TagAnnotation>, 194 + } 362 195 } 363 196 } 364 197 }
+18
crates/lexicon/Cargo.toml
··· 1 + [package] 2 + name = "lexicon" 3 + version.workspace = true 4 + authors.workspace = true 5 + repository.workspace = true 6 + license.workspace = true 7 + edition.workspace = true 8 + publish.workspace = true 9 + 10 + [dependencies] 11 + identity.workspace = true 12 + 13 + data-encoding.workspace = true 14 + serde.workspace = true 15 + thiserror.workspace = true 16 + time.workspace = true 17 + 18 + gix-hash = "^0.20.0"
+2
crates/lexicon/src/extra.rs
··· 1 + pub mod objectid; 2 + pub mod repopath;
+157
crates/lexicon/src/extra/repopath.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::str::FromStr; 3 + 4 + /// Repository path specifier. 5 + #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] 6 + #[serde(try_from = "UnvalidatedRepoPath")] 7 + pub struct RepoPath { 8 + /// Repository owner 9 + pub owner: Box<str>, 10 + 11 + /// Repository name 12 + pub name: Box<str>, 13 + } 14 + 15 + #[derive(Deserialize)] 16 + struct UnvalidatedRepoPath { 17 + owner: Box<str>, 18 + name: Box<str>, 19 + } 20 + 21 + impl TryFrom<UnvalidatedRepoPath> for RepoPath { 22 + type Error = RepoPathError; 23 + 24 + #[inline] 25 + fn try_from( 26 + UnvalidatedRepoPath { owner, name }: UnvalidatedRepoPath, 27 + ) -> Result<Self, Self::Error> { 28 + Self::from_parts(owner, name) 29 + } 30 + } 31 + 32 + impl RepoPath { 33 + pub fn from_parts( 34 + owner: impl Into<Box<str>>, 35 + name: impl Into<Box<str>>, 36 + ) -> Result<Self, RepoPathError> { 37 + fn inner(owner: Box<str>, name: Box<str>) -> Result<RepoPath, RepoPathError> { 38 + validate(owner.as_ref())?; 39 + validate(name.as_ref())?; 40 + Ok(RepoPath { owner, name }) 41 + } 42 + inner(owner.into(), name.into()) 43 + } 44 + 45 + pub const fn owner(&self) -> &str { 46 + &self.owner 47 + } 48 + 49 + pub const fn name(&self) -> &str { 50 + &self.name 51 + } 52 + } 53 + 54 + impl std::fmt::Display for RepoPath { 55 + #[inline] 56 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 + write!(f, "{}/{}", self.owner, self.name) 58 + } 59 + } 60 + 61 + impl FromStr for RepoPath { 62 + type Err = RepoPathError; 63 + 64 + fn from_str(s: &str) -> Result<Self, Self::Err> { 65 + let Some((owner, name)) = s.split_once('/') else { 66 + return Err(RepoPathError::Format); 67 + }; 68 + 69 + validate(owner)?; 70 + validate(name)?; 71 + 72 + Ok(Self { 73 + owner: owner.into(), 74 + name: name.into(), 75 + }) 76 + } 77 + } 78 + 79 + pub mod string { 80 + //! Serialize/Deserialize a `RepoSpec` from a string. 81 + //! 82 + //! # Example 83 + //! 84 + //! ```rust,no_run 85 + //! # use lexicon::extra::repopath::RepoPath; 86 + //! #[derive(serde::Deserialize)] 87 + //! struct Params { 88 + //! #[serde(with = "lexicon::extra::repopath::string")] 89 + //! repo: RepoPath, 90 + //! } 91 + //! ``` 92 + //! 93 + use super::RepoPath; 94 + use serde::{Deserialize, Deserializer, Serializer}; 95 + use std::{borrow::Cow, str::FromStr}; 96 + 97 + pub fn deserialize<'de, D>(deserializer: D) -> Result<RepoPath, D::Error> 98 + where 99 + D: Deserializer<'de>, 100 + { 101 + let s = <Cow<str>>::deserialize(deserializer)?; 102 + let repo = RepoPath::from_str(&s).map_err(serde::de::Error::custom)?; 103 + Ok(repo) 104 + } 105 + 106 + #[allow(unused)] 107 + pub fn serialize<S>(v: &RepoPath, serializer: S) -> Result<S::Ok, S::Error> 108 + where 109 + S: Serializer, 110 + { 111 + serializer.serialize_str(&format!("{}/{}", v.owner(), v.name())) 112 + } 113 + } 114 + 115 + fn validate(name: &str) -> Result<(), RepoPathError> { 116 + static FORBIDDEN_SUBSTRINGS: &[&str] = &[".."]; 117 + static FORBIDDEN_PREFIXES: &[&str] = &[".", "-"]; 118 + static ACCEPTED_SYMBOLS: &[char] = &['.', '-', '_', ':']; 119 + 120 + if name.is_empty() { 121 + return Err(RepoPathError::Empty); 122 + } 123 + 124 + for value in name.chars() { 125 + if !(value.is_ascii_alphanumeric() || ACCEPTED_SYMBOLS.contains(&value)) { 126 + return Err(RepoPathError::Character { value }); 127 + } 128 + } 129 + 130 + for substring in FORBIDDEN_SUBSTRINGS { 131 + if name.contains(substring) { 132 + return Err(RepoPathError::Substring { substring }); 133 + } 134 + } 135 + 136 + for prefix in FORBIDDEN_PREFIXES { 137 + if name.starts_with(prefix) { 138 + return Err(RepoPathError::Prefix { prefix }); 139 + } 140 + } 141 + 142 + Ok(()) 143 + } 144 + 145 + #[derive(Debug, thiserror::Error)] 146 + pub enum RepoPathError { 147 + #[error("expected a repository specifier in form '{{owner}}/{{name}}'")] 148 + Format, 149 + #[error("repository name cannot be empty")] 150 + Empty, 151 + #[error("invalid character in repository name: '{value}'")] 152 + Character { value: char }, 153 + #[error("invalid sub-string in repository name: '{substring}'")] 154 + Substring { substring: &'static str }, 155 + #[error("invalid prefix for respository name: '{prefix}'")] 156 + Prefix { prefix: &'static str }, 157 + }
+11
crates/lexicon/src/lib.rs
··· 1 + //! 2 + //! Artisinal, hand-written Lexicon definitions. 3 + //! 4 + //! When you're too lazy to setup codegen, not lazy enough to not write them 5 + //! all out manually. 6 + //! 7 + pub mod sh; 8 + 9 + /// Types which aren't part of the lexicon, but are used for serializing 10 + /// and deserializing. 11 + pub mod extra;
+1
crates/lexicon/src/sh.rs
··· 1 + pub mod tangled;
+32
crates/lexicon/src/sh/tangled.rs
··· 1 + pub mod knot; 2 + pub mod repo; 3 + pub mod spindle; 4 + 5 + use identity::Did; 6 + use serde::{Deserialize, Serialize}; 7 + use time::OffsetDateTime; 8 + 9 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/owner.json> 10 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 11 + #[serde(rename = "sh.tangled.owner")] 12 + pub struct OwnerOutput { 13 + pub owner: Did, 14 + } 15 + 16 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/publicKey.json> 17 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 18 + #[serde(rename = "sh.tangled.publicKey", rename_all = "camelCase")] 19 + pub struct PublicKey { 20 + #[serde(rename = "$type")] 21 + pub r#type: String, 22 + 23 + /// Public key contents 24 + pub key: String, 25 + 26 + /// Human-readable name for this key 27 + pub name: String, 28 + 29 + /// Key upload timestamp 30 + #[serde(alias = "created", with = "time::serde::rfc3339")] 31 + pub created_at: OffsetDateTime, 32 + }
+36
crates/lexicon/src/sh/tangled/knot.rs
··· 1 + use identity::Did; 2 + use serde::{Deserialize, Serialize}; 3 + use std::borrow::Cow; 4 + use time::OffsetDateTime; 5 + 6 + /// Output of `sh.tangled.knot.version` query. 7 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 8 + #[serde(rename_all = "camelCase")] 9 + pub struct VersionOutput { 10 + pub version: Cow<'static, str>, 11 + } 12 + 13 + /// `sh.tangled.knot` record. 14 + /// 15 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/knot/knot.json> 16 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 17 + #[serde(rename = "sh.tangled.knot", rename_all = "camelCase")] 18 + pub struct Knot { 19 + #[serde(with = "time::serde::rfc3339")] 20 + pub created_at: OffsetDateTime, 21 + } 22 + 23 + /// `sh.tangled.knot.member` record. 24 + /// 25 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/knot/member.json> 26 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 27 + #[serde(rename = "sh.tangled.knot.member", rename_all = "camelCase")] 28 + pub struct Member { 29 + pub subject: Did, 30 + 31 + /// Domain that this member now belongs to 32 + pub domain: String, 33 + 34 + #[serde(with = "time::serde::rfc3339")] 35 + pub created_at: OffsetDateTime, 36 + }
+89
crates/lexicon/src/sh/tangled/repo.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use time::OffsetDateTime; 3 + 4 + /// `sh.tangled.repo` record. 5 + /// 6 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/repo.json> 7 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 8 + #[serde(rename = "sh.tangled.repo", rename_all = "camelCase")] 9 + pub struct Repo { 10 + #[serde(rename = "$type")] 11 + pub r#type: String, 12 + 13 + /// Name of the repo 14 + pub name: String, 15 + 16 + /// Knot where the repo was created 17 + pub knot: String, 18 + 19 + /// CI runner to send jobs to and receive results from 20 + #[serde(skip_serializing_if = "Option::is_none")] 21 + pub spindle: Option<String>, 22 + 23 + #[serde(skip_serializing_if = "Option::is_none")] 24 + pub description: Option<String>, 25 + 26 + /// Source of the repo 27 + #[serde(skip_serializing_if = "Option::is_none")] 28 + pub source: Option<String>, 29 + 30 + /// List of labels that this repo subscribes to 31 + // @NOTE These should be at-uris. 32 + #[serde(default, skip_serializing_if = "Vec::is_empty")] 33 + pub labels: Vec<String>, 34 + 35 + #[serde(with = "time::serde::rfc3339")] 36 + pub created_at: OffsetDateTime, 37 + } 38 + 39 + pub mod blob; 40 + pub mod branches; 41 + pub mod diff; 42 + pub mod get_default_branch; 43 + pub mod languages; 44 + pub mod log; 45 + pub mod tags; 46 + pub mod tree; 47 + 48 + /// Lexicon sub-types. 49 + pub mod refs { 50 + use crate::extra::objectid::{Array, ObjectId}; 51 + use serde::Serialize; 52 + use std::{borrow::Cow, collections::HashMap}; 53 + use time::OffsetDateTime; 54 + 55 + #[derive(Debug, Default, Serialize)] 56 + #[serde(rename_all = "camelCase")] 57 + pub struct Reference { 58 + /// Short-name of the reference. 59 + pub name: String, 60 + pub hash: ObjectId, 61 + } 62 + 63 + /// Git commit signature (ie. the author or committer). 64 + #[derive(Debug, Serialize)] 65 + #[serde(rename_all = "PascalCase")] 66 + pub struct Signature { 67 + pub name: String, 68 + pub email: String, 69 + #[serde(with = "time::serde::rfc3339")] 70 + pub when: OffsetDateTime, 71 + } 72 + 73 + #[derive(Debug, Serialize)] 74 + #[serde(rename_all = "PascalCase")] 75 + pub struct Commit { 76 + pub hash: ObjectId<Array>, 77 + pub author: Signature, 78 + pub committer: Signature, 79 + pub merge_tag: String, 80 + #[serde(rename = "PGPSignature")] 81 + pub pgp_signature: Option<String>, 82 + pub message: String, 83 + pub tree_hash: ObjectId<Array>, 84 + pub parent_hashes: Vec<ObjectId<Array>>, 85 + pub encoding: Cow<'static, str>, 86 + /// Non-standard object headers. Values are always base64 encoded. 87 + pub extra_headers: HashMap<String, String>, 88 + } 89 + }
+85
crates/lexicon/src/sh/tangled/repo/blob.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/blob.json> 3 + //! 4 + use crate::extra::{ 5 + objectid::{Hex, ObjectId}, 6 + repopath::RepoPath, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use std::path::PathBuf; 10 + use time::OffsetDateTime; 11 + 12 + use super::refs; 13 + 14 + #[derive(Debug, Deserialize)] 15 + pub struct Input { 16 + /// Repository identifier in format `did:plc:.../repoName`. 17 + #[serde(with = "crate::extra::repopath::string")] 18 + pub repo: RepoPath, 19 + 20 + /// Git reference (branch, tag, or commit SHA). 21 + #[serde(rename = "ref")] 22 + pub rev: Option<String>, 23 + 24 + /// Path to the file within the repository. 25 + pub path: PathBuf, 26 + 27 + /// Return raw file content instead of JSON response. 28 + #[serde(default)] 29 + pub raw: bool, 30 + } 31 + 32 + #[derive(Debug, Serialize)] 33 + #[serde(rename_all = "camelCase")] 34 + pub struct Output { 35 + /// The git reference used. 36 + #[serde(rename = "ref")] 37 + pub rev: String, 38 + 39 + /// This file path. 40 + pub path: PathBuf, 41 + 42 + /// File content (base64 encoded for binary files). 43 + pub content: String, 44 + 45 + /// Content encoding. 46 + pub encoding: Encoding, 47 + 48 + /// File size in bytes. 49 + pub size: usize, 50 + 51 + /// Whether the file is binary. 52 + pub is_binary: bool, 53 + 54 + /// MIME type of the file. 55 + pub mime_type: String, 56 + 57 + pub last_commit: Option<LastCommit>, 58 + } 59 + 60 + #[derive(Debug, Serialize)] 61 + pub enum Encoding { 62 + #[serde(rename = "utf-8")] 63 + Utf8, 64 + #[serde(rename = "base64")] 65 + Base64, 66 + } 67 + 68 + #[derive(Debug, Serialize)] 69 + #[serde(rename_all = "camelCase")] 70 + pub struct LastCommit { 71 + /// Commit hash. 72 + pub hash: ObjectId<Hex>, 73 + 74 + /// Short commit hash. 75 + pub short_hash: Option<Box<str>>, 76 + 77 + /// Commit message. 78 + pub message: Box<str>, 79 + 80 + pub author: Option<refs::Signature>, 81 + 82 + /// Commit timestamp. 83 + #[serde(with = "time::serde::rfc3339")] 84 + pub when: OffsetDateTime, 85 + }
+28
crates/lexicon/src/sh/tangled/repo/branches.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/branches.json> 3 + //! 4 + use crate::extra::repopath::RepoPath; 5 + use serde::Deserialize; 6 + 7 + /// Parameters for the `sh.tangled.repo.branches` query. 8 + /// 9 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/branches.json> 10 + #[derive(Debug, Deserialize)] 11 + pub struct Input { 12 + /// Repository identifier in the format `did:plc:.../repoName` 13 + #[serde(with = "crate::extra::repopath::string")] 14 + pub repo: RepoPath, 15 + 16 + /// Maximum number of branches to return. 17 + /// 18 + /// (1..=100); Default: 50 19 + #[serde(default = "branches_limit_default")] 20 + pub limit: u16, 21 + 22 + /// Pagination cursor 23 + pub cursor: Option<String>, 24 + } 25 + 26 + const fn branches_limit_default() -> u16 { 27 + 50 28 + }
+16
crates/lexicon/src/sh/tangled/repo/diff.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/diff.json> 3 + //! 4 + use crate::extra::repopath::RepoPath; 5 + use serde::Deserialize; 6 + 7 + #[derive(Debug, Deserialize)] 8 + pub struct Input { 9 + /// Repository identifier in the format `did:plc:.../repoName` 10 + #[serde(with = "crate::extra::repopath::string")] 11 + pub repo: RepoPath, 12 + 13 + /// Git reference (branch, tag, or commit SHA) 14 + #[serde(rename = "ref")] 15 + pub rev: Box<str>, 16 + }
+65
crates/lexicon/src/sh/tangled/repo/get_default_branch.rs
··· 1 + use crate::extra::{ 2 + objectid::{Hex, ObjectId}, 3 + repopath::RepoPath, 4 + }; 5 + use serde::{Deserialize, Serialize}; 6 + use time::OffsetDateTime; 7 + 8 + use super::refs; 9 + 10 + #[derive(Debug, Deserialize)] 11 + pub struct Input { 12 + /// Repository identifier in the format `did:plc:.../repoName` 13 + #[serde(with = "crate::extra::repopath::string")] 14 + pub repo: RepoPath, 15 + } 16 + 17 + /// Response type for the `sh.tangled.repo.getDefaultBranch` query. 18 + // 19 + // @NOTE This completely different to what the lexicon describes, 20 + // but it is what knotserver outputs. 21 + #[derive(Debug, Serialize)] 22 + #[serde(rename_all = "camelCase")] 23 + pub struct Output { 24 + /// Short-name of the default branch. 25 + pub name: String, 26 + 27 + /// ID of the most recent commit on the default branch 28 + // 29 + // @NOTE Official knotserver always returns an empty string. 30 + #[serde(skip_serializing_if = "Option::is_none")] 31 + pub hash: Option<ObjectId>, 32 + 33 + /// Timestamp of the most recent commit on the default branch 34 + // 35 + // @NOTE Official knotserver always returns the unix epoch. 36 + #[serde( 37 + with = "time::serde::rfc3339::option", 38 + skip_serializing_if = "Option::is_none" 39 + )] 40 + pub when: Option<OffsetDateTime>, 41 + } 42 + 43 + /// Defined response type for the `sh.tangled.repo.getDefaultBranch` query. 44 + // 45 + #[derive(Debug, Serialize)] 46 + #[serde(rename_all = "camelCase")] 47 + pub struct LexiOutput { 48 + /// Default branch name. 49 + pub name: Box<str>, 50 + 51 + /// Latest commit hash on default branch. 52 + pub hash: ObjectId<Hex>, 53 + 54 + /// Short commit hash. 55 + pub short_hash: Box<str>, 56 + 57 + /// Timestamp of the latest commit. 58 + #[serde(with = "time::serde::rfc3339")] 59 + pub when: OffsetDateTime, 60 + 61 + /// Latest commit message. 62 + pub message: Option<Box<str>>, 63 + 64 + pub author: Option<refs::Signature>, 65 + }
+57
crates/lexicon/src/sh/tangled/repo/languages.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/languages.json> 3 + //! 4 + use crate::extra::repopath::RepoPath; 5 + use serde::{Deserialize, Serialize}; 6 + 7 + #[derive(Debug, Deserialize)] 8 + pub struct Input { 9 + /// Respository identifier in format `did:plc:.../repoName` 10 + #[serde(with = "crate::extra::repopath::string")] 11 + pub repo: RepoPath, 12 + 13 + /// Git reference (branch, tag, or commit SHA) 14 + #[serde(rename = "ref")] 15 + pub rev: String, 16 + } 17 + 18 + #[derive(Debug, Default, Serialize)] 19 + #[serde(rename_all = "camelCase")] 20 + pub struct Output { 21 + /// The git reference used 22 + #[serde(rename = "ref")] 23 + pub rev: String, 24 + 25 + #[serde(skip_serializing_if = "Vec::is_empty")] 26 + pub languages: Vec<Language>, 27 + 28 + /// Total size of all analyzed files in bytes 29 + #[serde(skip_serializing_if = "Option::is_none")] 30 + pub total_size: Option<u64>, 31 + 32 + /// Total number of files analyzed 33 + #[serde(skip_serializing_if = "Option::is_none")] 34 + pub total_files: Option<u64>, 35 + } 36 + 37 + #[derive(Debug, Serialize)] 38 + #[serde(rename_all = "camelCase")] 39 + pub struct Language { 40 + /// Programming language name 41 + pub name: String, 42 + 43 + /// Total size of files in this language (bytes) 44 + pub size: u64, 45 + 46 + /// Percentage of total codebase 0..=100 47 + pub percentage: u8, 48 + 49 + /// Number of files in this language 50 + pub file_count: u64, 51 + 52 + /// Hex color code for this language 53 + pub color: String, 54 + 55 + /// File extensions associated with this language 56 + pub extensions: Vec<String>, 57 + }
+38
crates/lexicon/src/sh/tangled/repo/log.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/log.json> 3 + //! 4 + use crate::extra::repopath::RepoPath; 5 + use serde::Deserialize; 6 + use std::path::PathBuf; 7 + 8 + /// Parameters for the `sh.tangled.repo.log` query. 9 + #[derive(Debug, Deserialize)] 10 + pub struct Input { 11 + /// Repository identifier in the format `did:plc:.../repoName` 12 + #[serde(with = "crate::extra::repopath::string")] 13 + pub repo: RepoPath, 14 + 15 + /// Git reference (branch, tag, or commit SHA) 16 + #[serde(rename = "ref")] 17 + pub rev: Option<String>, 18 + 19 + /// Path to filter commits by 20 + pub path: Option<PathBuf>, 21 + 22 + /// Maximum number of commits to return. 23 + /// 24 + /// (1..=100); Default: 50 25 + #[serde(default = "log_limit_default")] 26 + pub limit: u16, 27 + 28 + /// Pagination cursor (commit SHA) 29 + // 30 + // @NOTE AppView actually gives us an integer for the number of 31 + // commits to skip. 32 + #[serde(default)] 33 + pub cursor: usize, 34 + } 35 + 36 + const fn log_limit_default() -> u16 { 37 + 50 38 + }
+28
crates/lexicon/src/sh/tangled/repo/tags.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/tags.json> 3 + //! 4 + use crate::extra::repopath::RepoPath; 5 + use serde::Deserialize; 6 + 7 + /// Parameters for the `sh.tangled.repo.tags` query. 8 + /// 9 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/tags.json> 10 + #[derive(Debug, Deserialize)] 11 + pub struct Input { 12 + /// Repository identifier in the format `did:plc:.../repoName` 13 + #[serde(with = "crate::extra::repopath::string")] 14 + pub repo: RepoPath, 15 + 16 + /// Maximum number of commits to return. 17 + /// 18 + /// (1..=100); Default: 50 19 + #[serde(default = "tag_limit_default")] 20 + pub limit: u16, 21 + 22 + /// Pagnination cursor 23 + pub cursor: Option<String>, 24 + } 25 + 26 + const fn tag_limit_default() -> u16 { 27 + 50 28 + }
+89
crates/lexicon/src/sh/tangled/repo/tree.rs
··· 1 + //! 2 + //! <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/tree.json> 3 + //! 4 + use std::{borrow::Cow, path::PathBuf}; 5 + 6 + use crate::extra::{objectid::ObjectId, repopath::RepoPath}; 7 + use serde::{Deserialize, Serialize}; 8 + use time::OffsetDateTime; 9 + 10 + /// Parameters for the `sh.tangled.repo.tree` query. 11 + /// 12 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/tree.json> 13 + #[derive(Debug, Deserialize)] 14 + pub struct Input { 15 + /// Repository identifier in the format `did:plc:.../repoName` 16 + #[serde(with = "crate::extra::repopath::string")] 17 + pub repo: RepoPath, 18 + 19 + /// Git reference (branch, tag, or commit SHA) 20 + #[serde(rename = "ref")] 21 + pub rev: Option<String>, 22 + 23 + /// Path within the repository tree 24 + pub path: Option<PathBuf>, 25 + } 26 + 27 + /// Output of `sh.tangled.repo.tree` query. 28 + #[derive(Debug, Serialize)] 29 + pub struct Output { 30 + /// The git reference used 31 + #[serde(rename = "ref")] 32 + pub rev: String, 33 + 34 + /// The parent path in the tree 35 + #[serde(skip_serializing_if = "Option::is_none")] 36 + pub parent: Option<PathBuf>, 37 + 38 + /// Parent directory path 39 + #[serde(skip_serializing_if = "Option::is_none")] 40 + pub dotdot: Option<PathBuf>, 41 + 42 + /// Readme for this file tree 43 + #[serde(skip_serializing_if = "Option::is_none")] 44 + pub readme: Option<Readme>, 45 + 46 + pub files: Vec<TreeEntry>, 47 + } 48 + 49 + #[derive(Debug, Default, Serialize)] 50 + pub struct TreeEntry { 51 + /// Relative file or directory name 52 + pub name: String, 53 + 54 + /// File mode 55 + pub mode: Cow<'static, str>, 56 + 57 + /// File size in bytes 58 + pub size: usize, 59 + 60 + /// Whether this entry is a file 61 + pub is_file: bool, 62 + 63 + /// Whether this entry is a directory/subtree 64 + pub is_subtree: bool, 65 + 66 + pub last_commit: Option<LastCommit>, 67 + } 68 + 69 + #[derive(Debug, Serialize)] 70 + pub struct Readme { 71 + /// Contents of the readme file 72 + pub filename: String, 73 + 74 + /// Name of the readme file 75 + pub contents: String, 76 + } 77 + 78 + #[derive(Debug, Serialize)] 79 + pub struct LastCommit { 80 + /// Commit hash 81 + pub hash: ObjectId, 82 + 83 + /// Commit message 84 + pub message: String, 85 + 86 + /// Commit timestamp 87 + #[serde(with = "time::serde::rfc3339")] 88 + pub when: OffsetDateTime, 89 + }
+28
crates/lexicon/src/sh/tangled/spindle.rs
··· 1 + use identity::Did; 2 + use serde::{Deserialize, Serialize}; 3 + use time::OffsetDateTime; 4 + 5 + /// `sh.tangled.spindle` record. 6 + /// 7 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/spindle/spindle.json> 8 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 9 + #[serde(rename = "sh.tangled.spindle", rename_all = "camelCase")] 10 + pub struct Spindle { 11 + #[serde(with = "time::serde::rfc3339")] 12 + pub created_at: OffsetDateTime, 13 + } 14 + 15 + /// `sh.tangled.spindle.member` record. 16 + /// 17 + /// <https://tangled.org/@tangled.org/core/blob/master/lexicons/spindle/member.json> 18 + #[derive(Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] 19 + #[serde(rename = "sh.tangled.spindle.member", rename_all = "camelCase")] 20 + pub struct Member { 21 + pub subject: Did, 22 + 23 + /// Domain that this member now belongs to 24 + pub domain: String, 25 + 26 + #[serde(with = "time::serde::rfc3339")] 27 + pub created_at: OffsetDateTime, 28 + }