Milestone 0.1: browse d23464c0ac60
One binary (axum, askama, sqlx on SQLite) linking oxy-core and embedding oxy-server. Repositories are bare and unbound until the first push; pages show trees, files with highlighting and blame, Markdown, history with the dependency graph, changes with diffs, compare, bookmarks, tags, conflicts, the trust policy and verification, and the push log. RetroPlay theme with vendored fonts. JSON API for repositories, bookmarks and tags. End-to-end test with a real oxy-core client.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-27 17:28 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
d23464c0ac60f60775ae362f92fa08a4b1413e8b1d89e146c1699944bc109c19- Depends on
- 93be9ce4653e
- Depended on by
- 1062a0f8e985 fb8f35d025ab 40a43921ef42 f2d2776b2a14 0bc95135954d 64e5964e9f13 306aec1839aa e4f1f52f2929 8297a33b9a4c a22e4aec2e28 47932fa8d6f3 8f1d76f4169a 1c52d940b4a0 b7e1be264c0a 75491d35c258 42c77f6515c2 041a945f55f8 04f9c984439e 27322cd2755a 93f445fa50d8 c26267b3aba6 fac6853c4e46 3d086bc3d69b 8306eca4e4a3 851a26a2a6d1 1ec29691b67a 4dfa4599b3f7 b1860d6090ec 70c3d02998a2 a9088323c390 aeed63c00ad7 217814c239f0 351cf57e5608 046e3352cdfd 7c572789349b 84f09ad45851 12325e1b1020 5d66f27cebe2 0468394734e9 c4bffee5b0d4 951aed50c2f5 5caa65fd51e7 668b2ad7495c 15d3f6f7871b d7e2c2754807 84e3c09656b2 5a9a6e9a15bc b951c58734e8 dd0788b756f2 d061ac87618c 8eae2f0e255c 9bbca80f777a faf5246c900e dc5c64296b0a 1000b40a2629 8501fd5815b9 c2efc0b4674d d2263e299da7 e4203e141e90 a3ba7cc10673 8bb7baeb4557 c43e4f631d99 8f0efa0729b0 234921ea0834 ab4df6aae179 09ffa7b600a9 73c01b3448ee 5d818ec7d5fc 03bfb945f5c1 de4467cb1eca 5714347a6233 b7d2b0b30b4a f4e138301198 439ecd8708a1 6a306cd03fc1 69e4f27c5633 ecac6e8d9b41 657e7d5c9da9 4f8404093d30 456f4197ade3 ba0a55bb5b3c 8ad8e656a3a6 8ac2aa4ca6d1 ca0c443c30cf c419455a82dd ab2d8b40f9f4 adbc38e7ea09 dd327d76062d 5588bb3631d2 737de0fac722 b349e15a3eab 8597fb5ca16c 367f34b8e590 8bf3010760d7 858eca0d03c4 787632570bf9 a549ddd5941f d366306dae7d a0a1601eb5a5 166f6e9bf4f1 c7067c7bda56 cb7b3cd45dc0 3571c519ada6 9c04c779f032 33d98147511b e57765dee61a 0ee5c3fb32eb b9eb70fb9509 a9f1c0bf403a 13b27cb2200c 4a68f1064b08 86a69b53aff0 2a8b621676b4 7c06f5b28ade 0843f23ec51c e97500b70f9a 4866be1c9040 67523e06ed86 16ee5d19d220 85ec74fd82d8 d4a0043fded4 205d8f863e5c b9b74d997cec a10446c68043 8a48dd830178 8dcaeeb848c6 a7aa76ac52d7 05f1a85a2842 4f40eed0588f e5b97ac399e7 965b950f9b96 559c39204234 8aa5ef384ef1 3eb491e9489b 026a76d251ce 53a0fb2e4ac6 119fd17d213f 4cc40ccc6034 d2c1d6a7d12e 97fc47fee9cf f157bcea0e32 529c703db24d a268d4df52bf 76f93eaefe45 19c71c828f86 076c7a7c86b1 ed3a36b6414c 107eaba52c9d e9b9619cd857 dc71378db1dc c92a99490cba c8684d44d549 4e2cad00a484 3b1bc27fe805 d1bf2d698777 5d90e0e045e2 47e374020b59 a2ef53c5db26 2cbf6f348595 b5a0a69b3622 67be8ac00c24 b644e1757711 417184c73f4e cd437c4f5f8d 7b17d570d7ee 62cdf34daec1 0e04dfc20211 0024ccb1b050 3dd6582de060 d74ce22fa80e aa0bfbd11f02 9a777fec4ed9 9a8e3b4419e0 7ebb7052bc7f c0095389312b a2fbad481dd7 2a0c09c09716 11a2e09d6a69 810896d41be7 addec2bd6423 32d97df55ab7 ffb67a81188c 00ef7b83b340 bff9106edcba cd46e56b1709 2e9188ca4f98 d9b3b84605a4 4a4400c5812d e8fa53694f68 7b9f4570daf5 d3b048b27277 1e8a6e8bb91b f4d6ef418a96 93501da2a162 da8f7826a645 46b5e2c59c6b 35fea4237cc9 6ae3fa0af4f6 d7d182467382 6246d1e51f4f 26d0d091deb7 0a3b244b748f 61ecbd299f22 7f283f8e8b97 33bd0bd45f8a df5c69cbd047 17ca6cddc534 f8cf54e99879 1656dc14c2ac 08e221b77658 eb2a3502601d c9aacc0e7dc1 acc85ac0f70b 58f915792b85 73ef9f767132 757195ad7a5d 71e65fcf95ab 3776b70d9b17 a7e6e8e6094c e71583501b1d f677d6270298 750579ecc721 6ca0b971ff07 39442b000e6b a46d15e72101 19355e2283ab 16626c0d69c0 ac713df7b38b 20b8fe85888b 95b525b0b977 501d8b268dd4 44a3d2ebaddd 2fb70d019851 59338826539c 40dd3f86379a 92a797b20912 f3e5e2bc78e9 c8df33a6829f a0dd06b0094e 0650d94ede82 a36bab6b9ffc b1e92a6233ee 8aafd2023dec 7b26e6118bc0 4bae285c6ed8 8a2dbcb56246 a024e800af4e b210727578e8 2e640ffa6c93 3d5c825aebba 617c2840446c 36e5a9c15a84 0b203bbf7cc3 17a3a84b00d1 7edaff01d506 c987c86513df 554ccacfc123 6e7dfa0a42b9 8475176972e8 8ddebaa7e856 1f78655a0466 4d2e44d79b03 62ceb2eec18c 155cab142b63 ed89fae5180b e1dc108bd814 3ea0b8af2d46 206c1fc1b06a ad01845c4bf8 efd29a0033e8 83940d53664f 66fd9d6b1452 d0fbea2eb46e 89e89836cae7 d1727c573f30 4ac01a732552 2a83f5303458 7d2eb590051f e71846aa07b9 3556d08e17f9 7f5f6c056d8f 991b55638aaf a6c241e8eba0 8933faff2043 f28389bd6aab 5ea4872f23e8 b43a9ab0498f b5229b56f49b 8a3334b7a364 745d225de344 967ef74f6fd9 348f8c4baccf c889911bec12 0a56c5313bc0 5dead1f4f26d d7ce41acf22c 9e50caedf4db 4f37afcc3583 a7c05940ebc4 ea85398a51bc 8e96499f2705 4da8b828a822 cb71e18eb6f9 6a1780558353 6ff755541a62 2291f8cc9c96 20c36ed92b6b bf5101d394c0 34f40eb4b205 739a763a558c ced6d3afc5cf 070a7b64b368 d98a93c10a0d 8e820b4ae875 ae138eea7a71 f6346d875ee5 749d9a5fcd9f d305b3b0afdc 60f11b71a740 7974adb0be66 b66e70b7b6c7 6978fd76a066 cc1cd197df1d 74be97962ed8 df71c7892d27 c96b32865000 5533e91a2aa4 99ca93d60bca 129c23effb73 d3e75fb41e61 ff6569530173 046bd71a180f 462f88d161ce a4f01d51c2a7 0b20f1ec4007 67b24adbf35a d91780ee20de df15b74774ca bf1ab3458db1 e9bfea8e26db a1b9869565aa 38833ed78707 9198231cee96 b6b7e4cf6b24 1f1eb06a9009 5c6e92fd8d71 df940d75cd3d 4f6925337848 10708ab8f54d a3c8288b0141 cc19697f8871 2bfe5ef66b0c 92574625ee9a 65be9f9c3074 5e6d6484c28e 9e5ef6c23006 fd958fbc06fb c5f3bde466d1 bf3d07541368 760a7dd30d6f 90326c9601f9 eae4a97475f1 1edac4b68c6d e271a37b629a a0d4bbfbedd1 9008bf6c2b8c 941abdd6bb4c 961c10bee174 25091e5c3bba 380996db84e5 fd3db644df22 6ad368c5958e 83d5052bf76c ef20fdc14e8b 9756c67d3aec decb6304ba97 e1ec9fbf538e 78001759604c 76f262e1a666 b19606f28101 75d611ab99d1 f5c443378333 8fb35df7d66e 883c77eaf2c9 756fe44a0f4d a160b98fb922 eb0c44a9ad37 34beb1a7e376 43e8ca33bc5d 3d1e74cdac76 736f6813543d af9c6ca11c49 42fa95540468 8d2caf122517 6ae41081ccd3 64caed18236a 1ac84c62ba53 1553bfc9ae53 07b1a5bbf481 9df855f29948 101740454617 42a0ba4bac4e c40c2d3edb51 c0fac31d36fd 3f02e5ea4f2d 4edcf24025cf 25ab8c73514a b45384c909c1 3f4b0ea6eabf 145306044896 db72849cd22e b64e2537d702 18e381ea88c2 de505df5cf8d 19bb31ff1f5c e1078e99c560 9ff27da517fe 62d6e969f180 abb98fa9964e 5c6eb1feaa71 baebdd18aaa2 8f3d2ccfe6fc
- In bookmarks
- api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
- In tags
- v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 62 atoms, 77 tree operations, 62 files
Browse the tree at this change
added Cargo.lock +3922 −0
| 1 | + | # This file is automatically @generated by Cargo. |
|
| 2 | + | # It is not intended for manual editing. |
|
| 3 | + | version = 4 |
|
| 4 | + | ||
| 5 | + | [[package]] |
|
| 6 | + | name = "adler2" |
|
| 7 | + | version = "2.0.1" |
|
| 8 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 9 | + | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" |
|
| 10 | + | ||
| 11 | + | [[package]] |
|
| 12 | + | name = "aho-corasick" |
|
| 13 | + | version = "1.1.5" |
|
| 14 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 15 | + | checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" |
|
| 16 | + | dependencies = [ |
|
| 17 | + | "memchr", |
|
| 18 | + | ] |
|
| 19 | + | ||
| 20 | + | [[package]] |
|
| 21 | + | name = "allocator-api2" |
|
| 22 | + | version = "0.2.21" |
|
| 23 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 24 | + | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" |
|
| 25 | + | ||
| 26 | + | [[package]] |
|
| 27 | + | name = "ammonia" |
|
| 28 | + | version = "4.2.0" |
|
| 29 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 30 | + | checksum = "3f7ed3ac4252be4d3e36fb589ed5659356842587b21a31596a9dc89d3b2f5e2a" |
|
| 31 | + | dependencies = [ |
|
| 32 | + | "cssparser", |
|
| 33 | + | "html5ever", |
|
| 34 | + | "maplit", |
|
| 35 | + | "url", |
|
| 36 | + | ] |
|
| 37 | + | ||
| 38 | + | [[package]] |
|
| 39 | + | name = "anstream" |
|
| 40 | + | version = "1.0.0" |
|
| 41 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 42 | + | checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" |
|
| 43 | + | dependencies = [ |
|
| 44 | + | "anstyle", |
|
| 45 | + | "anstyle-parse", |
|
| 46 | + | "anstyle-query", |
|
| 47 | + | "anstyle-wincon", |
|
| 48 | + | "colorchoice", |
|
| 49 | + | "is_terminal_polyfill", |
|
| 50 | + | "utf8parse", |
|
| 51 | + | ] |
|
| 52 | + | ||
| 53 | + | [[package]] |
|
| 54 | + | name = "anstyle" |
|
| 55 | + | version = "1.0.14" |
|
| 56 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 57 | + | checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" |
|
| 58 | + | ||
| 59 | + | [[package]] |
|
| 60 | + | name = "anstyle-parse" |
|
| 61 | + | version = "1.0.0" |
|
| 62 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 63 | + | checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" |
|
| 64 | + | dependencies = [ |
|
| 65 | + | "utf8parse", |
|
| 66 | + | ] |
|
| 67 | + | ||
| 68 | + | [[package]] |
|
| 69 | + | name = "anstyle-query" |
|
| 70 | + | version = "1.1.5" |
|
| 71 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 72 | + | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" |
|
| 73 | + | dependencies = [ |
|
| 74 | + | "windows-sys 0.61.2", |
|
| 75 | + | ] |
|
| 76 | + | ||
| 77 | + | [[package]] |
|
| 78 | + | name = "anstyle-wincon" |
|
| 79 | + | version = "3.0.11" |
|
| 80 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 81 | + | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" |
|
| 82 | + | dependencies = [ |
|
| 83 | + | "anstyle", |
|
| 84 | + | "once_cell_polyfill", |
|
| 85 | + | "windows-sys 0.61.2", |
|
| 86 | + | ] |
|
| 87 | + | ||
| 88 | + | [[package]] |
|
| 89 | + | name = "anyhow" |
|
| 90 | + | version = "1.0.104" |
|
| 91 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 92 | + | checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" |
|
| 93 | + | ||
| 94 | + | [[package]] |
|
| 95 | + | name = "arc-swap" |
|
| 96 | + | version = "1.9.2" |
|
| 97 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 98 | + | checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" |
|
| 99 | + | dependencies = [ |
|
| 100 | + | "rustversion", |
|
| 101 | + | ] |
|
| 102 | + | ||
| 103 | + | [[package]] |
|
| 104 | + | name = "arrayvec" |
|
| 105 | + | version = "0.7.8" |
|
| 106 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 107 | + | checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" |
|
| 108 | + | ||
| 109 | + | [[package]] |
|
| 110 | + | name = "askama" |
|
| 111 | + | version = "0.14.0" |
|
| 112 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 113 | + | checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" |
|
| 114 | + | dependencies = [ |
|
| 115 | + | "askama_derive", |
|
| 116 | + | "itoa", |
|
| 117 | + | "percent-encoding", |
|
| 118 | + | "serde", |
|
| 119 | + | "serde_json", |
|
| 120 | + | ] |
|
| 121 | + | ||
| 122 | + | [[package]] |
|
| 123 | + | name = "askama_derive" |
|
| 124 | + | version = "0.14.0" |
|
| 125 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 126 | + | checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" |
|
| 127 | + | dependencies = [ |
|
| 128 | + | "askama_parser", |
|
| 129 | + | "basic-toml", |
|
| 130 | + | "memchr", |
|
| 131 | + | "proc-macro2", |
|
| 132 | + | "quote", |
|
| 133 | + | "rustc-hash", |
|
| 134 | + | "serde", |
|
| 135 | + | "serde_derive", |
|
| 136 | + | "syn 2.0.119", |
|
| 137 | + | ] |
|
| 138 | + | ||
| 139 | + | [[package]] |
|
| 140 | + | name = "askama_parser" |
|
| 141 | + | version = "0.14.0" |
|
| 142 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 143 | + | checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" |
|
| 144 | + | dependencies = [ |
|
| 145 | + | "memchr", |
|
| 146 | + | "serde", |
|
| 147 | + | "serde_derive", |
|
| 148 | + | "winnow", |
|
| 149 | + | ] |
|
| 150 | + | ||
| 151 | + | [[package]] |
|
| 152 | + | name = "asn1-rs" |
|
| 153 | + | version = "0.7.2" |
|
| 154 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 155 | + | checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" |
|
| 156 | + | dependencies = [ |
|
| 157 | + | "asn1-rs-derive", |
|
| 158 | + | "asn1-rs-impl", |
|
| 159 | + | "displaydoc", |
|
| 160 | + | "nom", |
|
| 161 | + | "num-traits", |
|
| 162 | + | "rusticata-macros", |
|
| 163 | + | "thiserror 2.0.21", |
|
| 164 | + | "time", |
|
| 165 | + | ] |
|
| 166 | + | ||
| 167 | + | [[package]] |
|
| 168 | + | name = "asn1-rs-derive" |
|
| 169 | + | version = "0.6.0" |
|
| 170 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 171 | + | checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" |
|
| 172 | + | dependencies = [ |
|
| 173 | + | "proc-macro2", |
|
| 174 | + | "quote", |
|
| 175 | + | "syn 2.0.119", |
|
| 176 | + | "synstructure 0.13.2", |
|
| 177 | + | ] |
|
| 178 | + | ||
| 179 | + | [[package]] |
|
| 180 | + | name = "asn1-rs-impl" |
|
| 181 | + | version = "0.2.0" |
|
| 182 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 183 | + | checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" |
|
| 184 | + | dependencies = [ |
|
| 185 | + | "proc-macro2", |
|
| 186 | + | "quote", |
|
| 187 | + | "syn 2.0.119", |
|
| 188 | + | ] |
|
| 189 | + | ||
| 190 | + | [[package]] |
|
| 191 | + | name = "async-compression" |
|
| 192 | + | version = "0.4.48" |
|
| 193 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 194 | + | checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" |
|
| 195 | + | dependencies = [ |
|
| 196 | + | "compression-codecs", |
|
| 197 | + | "compression-core", |
|
| 198 | + | "pin-project-lite", |
|
| 199 | + | "tokio", |
|
| 200 | + | ] |
|
| 201 | + | ||
| 202 | + | [[package]] |
|
| 203 | + | name = "atoi" |
|
| 204 | + | version = "2.0.0" |
|
| 205 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 206 | + | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" |
|
| 207 | + | dependencies = [ |
|
| 208 | + | "num-traits", |
|
| 209 | + | ] |
|
| 210 | + | ||
| 211 | + | [[package]] |
|
| 212 | + | name = "atomic-waker" |
|
| 213 | + | version = "1.1.2" |
|
| 214 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 215 | + | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" |
|
| 216 | + | ||
| 217 | + | [[package]] |
|
| 218 | + | name = "autocfg" |
|
| 219 | + | version = "1.5.1" |
|
| 220 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 221 | + | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" |
|
| 222 | + | ||
| 223 | + | [[package]] |
|
| 224 | + | name = "axum" |
|
| 225 | + | version = "0.8.9" |
|
| 226 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 227 | + | checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" |
|
| 228 | + | dependencies = [ |
|
| 229 | + | "axum-core", |
|
| 230 | + | "axum-macros", |
|
| 231 | + | "bytes", |
|
| 232 | + | "form_urlencoded", |
|
| 233 | + | "futures-util", |
|
| 234 | + | "http", |
|
| 235 | + | "http-body", |
|
| 236 | + | "http-body-util", |
|
| 237 | + | "hyper", |
|
| 238 | + | "hyper-util", |
|
| 239 | + | "itoa", |
|
| 240 | + | "matchit", |
|
| 241 | + | "memchr", |
|
| 242 | + | "mime", |
|
| 243 | + | "percent-encoding", |
|
| 244 | + | "pin-project-lite", |
|
| 245 | + | "serde_core", |
|
| 246 | + | "serde_json", |
|
| 247 | + | "serde_path_to_error", |
|
| 248 | + | "serde_urlencoded", |
|
| 249 | + | "sync_wrapper", |
|
| 250 | + | "tokio", |
|
| 251 | + | "tower", |
|
| 252 | + | "tower-layer", |
|
| 253 | + | "tower-service", |
|
| 254 | + | "tracing", |
|
| 255 | + | ] |
|
| 256 | + | ||
| 257 | + | [[package]] |
|
| 258 | + | name = "axum-core" |
|
| 259 | + | version = "0.5.6" |
|
| 260 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 261 | + | checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" |
|
| 262 | + | dependencies = [ |
|
| 263 | + | "bytes", |
|
| 264 | + | "futures-core", |
|
| 265 | + | "http", |
|
| 266 | + | "http-body", |
|
| 267 | + | "http-body-util", |
|
| 268 | + | "mime", |
|
| 269 | + | "pin-project-lite", |
|
| 270 | + | "sync_wrapper", |
|
| 271 | + | "tower-layer", |
|
| 272 | + | "tower-service", |
|
| 273 | + | "tracing", |
|
| 274 | + | ] |
|
| 275 | + | ||
| 276 | + | [[package]] |
|
| 277 | + | name = "axum-macros" |
|
| 278 | + | version = "0.5.1" |
|
| 279 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 280 | + | checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" |
|
| 281 | + | dependencies = [ |
|
| 282 | + | "proc-macro2", |
|
| 283 | + | "quote", |
|
| 284 | + | "syn 2.0.119", |
|
| 285 | + | ] |
|
| 286 | + | ||
| 287 | + | [[package]] |
|
| 288 | + | name = "axum-server" |
|
| 289 | + | version = "0.8.0" |
|
| 290 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 291 | + | checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" |
|
| 292 | + | dependencies = [ |
|
| 293 | + | "arc-swap", |
|
| 294 | + | "bytes", |
|
| 295 | + | "either", |
|
| 296 | + | "fs-err", |
|
| 297 | + | "http", |
|
| 298 | + | "http-body", |
|
| 299 | + | "hyper", |
|
| 300 | + | "hyper-util", |
|
| 301 | + | "pin-project-lite", |
|
| 302 | + | "rustls", |
|
| 303 | + | "rustls-pki-types", |
|
| 304 | + | "tokio", |
|
| 305 | + | "tokio-rustls", |
|
| 306 | + | "tower-service", |
|
| 307 | + | ] |
|
| 308 | + | ||
| 309 | + | [[package]] |
|
| 310 | + | name = "base64" |
|
| 311 | + | version = "0.22.1" |
|
| 312 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 313 | + | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" |
|
| 314 | + | ||
| 315 | + | [[package]] |
|
| 316 | + | name = "base64" |
|
| 317 | + | version = "0.23.1" |
|
| 318 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 319 | + | checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" |
|
| 320 | + | ||
| 321 | + | [[package]] |
|
| 322 | + | name = "base64ct" |
|
| 323 | + | version = "1.8.3" |
|
| 324 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 325 | + | checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" |
|
| 326 | + | ||
| 327 | + | [[package]] |
|
| 328 | + | name = "basic-toml" |
|
| 329 | + | version = "0.1.10" |
|
| 330 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 331 | + | checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" |
|
| 332 | + | dependencies = [ |
|
| 333 | + | "serde", |
|
| 334 | + | ] |
|
| 335 | + | ||
| 336 | + | [[package]] |
|
| 337 | + | name = "bincode" |
|
| 338 | + | version = "1.3.3" |
|
| 339 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 340 | + | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" |
|
| 341 | + | dependencies = [ |
|
| 342 | + | "serde", |
|
| 343 | + | ] |
|
| 344 | + | ||
| 345 | + | [[package]] |
|
| 346 | + | name = "bit-set" |
|
| 347 | + | version = "0.8.0" |
|
| 348 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 349 | + | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" |
|
| 350 | + | dependencies = [ |
|
| 351 | + | "bit-vec 0.8.0", |
|
| 352 | + | ] |
|
| 353 | + | ||
| 354 | + | [[package]] |
|
| 355 | + | name = "bit-vec" |
|
| 356 | + | version = "0.8.0" |
|
| 357 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 358 | + | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" |
|
| 359 | + | ||
| 360 | + | [[package]] |
|
| 361 | + | name = "bit-vec" |
|
| 362 | + | version = "0.9.1" |
|
| 363 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 364 | + | checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" |
|
| 365 | + | dependencies = [ |
|
| 366 | + | "serde", |
|
| 367 | + | ] |
|
| 368 | + | ||
| 369 | + | [[package]] |
|
| 370 | + | name = "bitflags" |
|
| 371 | + | version = "2.13.2" |
|
| 372 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 373 | + | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" |
|
| 374 | + | dependencies = [ |
|
| 375 | + | "serde_core", |
|
| 376 | + | ] |
|
| 377 | + | ||
| 378 | + | [[package]] |
|
| 379 | + | name = "blake3" |
|
| 380 | + | version = "1.8.7" |
|
| 381 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 382 | + | checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" |
|
| 383 | + | dependencies = [ |
|
| 384 | + | "arrayvec", |
|
| 385 | + | "cc", |
|
| 386 | + | "cfg-if", |
|
| 387 | + | "constant_time_eq", |
|
| 388 | + | "cpufeatures 0.3.1", |
|
| 389 | + | ] |
|
| 390 | + | ||
| 391 | + | [[package]] |
|
| 392 | + | name = "block-buffer" |
|
| 393 | + | version = "0.10.4" |
|
| 394 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 395 | + | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" |
|
| 396 | + | dependencies = [ |
|
| 397 | + | "generic-array", |
|
| 398 | + | ] |
|
| 399 | + | ||
| 400 | + | [[package]] |
|
| 401 | + | name = "block-buffer" |
|
| 402 | + | version = "0.12.1" |
|
| 403 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 404 | + | checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" |
|
| 405 | + | dependencies = [ |
|
| 406 | + | "hybrid-array", |
|
| 407 | + | ] |
|
| 408 | + | ||
| 409 | + | [[package]] |
|
| 410 | + | name = "bstr" |
|
| 411 | + | version = "1.13.1" |
|
| 412 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 413 | + | checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" |
|
| 414 | + | dependencies = [ |
|
| 415 | + | "memchr", |
|
| 416 | + | "serde_core", |
|
| 417 | + | ] |
|
| 418 | + | ||
| 419 | + | [[package]] |
|
| 420 | + | name = "bumpalo" |
|
| 421 | + | version = "3.20.3" |
|
| 422 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 423 | + | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" |
|
| 424 | + | ||
| 425 | + | [[package]] |
|
| 426 | + | name = "byteorder" |
|
| 427 | + | version = "1.5.0" |
|
| 428 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 429 | + | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" |
|
| 430 | + | ||
| 431 | + | [[package]] |
|
| 432 | + | name = "bytes" |
|
| 433 | + | version = "1.12.1" |
|
| 434 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 435 | + | checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" |
|
| 436 | + | ||
| 437 | + | [[package]] |
|
| 438 | + | name = "cc" |
|
| 439 | + | version = "1.5.1" |
|
| 440 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 441 | + | checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" |
|
| 442 | + | dependencies = [ |
|
| 443 | + | "find-msvc-tools", |
|
| 444 | + | "jobserver", |
|
| 445 | + | "libc", |
|
| 446 | + | "shlex", |
|
| 447 | + | ] |
|
| 448 | + | ||
| 449 | + | [[package]] |
|
| 450 | + | name = "cfg-if" |
|
| 451 | + | version = "1.0.5" |
|
| 452 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 453 | + | checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" |
|
| 454 | + | ||
| 455 | + | [[package]] |
|
| 456 | + | name = "cfg_aliases" |
|
| 457 | + | version = "0.2.2" |
|
| 458 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 459 | + | checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" |
|
| 460 | + | ||
| 461 | + | [[package]] |
|
| 462 | + | name = "chacha20" |
|
| 463 | + | version = "0.10.2" |
|
| 464 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 465 | + | checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" |
|
| 466 | + | dependencies = [ |
|
| 467 | + | "cfg-if", |
|
| 468 | + | "cpufeatures 0.3.1", |
|
| 469 | + | "rand_core 0.10.1", |
|
| 470 | + | ] |
|
| 471 | + | ||
| 472 | + | [[package]] |
|
| 473 | + | name = "clap" |
|
| 474 | + | version = "4.6.7" |
|
| 475 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 476 | + | checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" |
|
| 477 | + | dependencies = [ |
|
| 478 | + | "clap_builder", |
|
| 479 | + | "clap_derive", |
|
| 480 | + | ] |
|
| 481 | + | ||
| 482 | + | [[package]] |
|
| 483 | + | name = "clap_builder" |
|
| 484 | + | version = "4.6.7" |
|
| 485 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 486 | + | checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" |
|
| 487 | + | dependencies = [ |
|
| 488 | + | "anstream", |
|
| 489 | + | "anstyle", |
|
| 490 | + | "clap_lex", |
|
| 491 | + | "strsim", |
|
| 492 | + | ] |
|
| 493 | + | ||
| 494 | + | [[package]] |
|
| 495 | + | name = "clap_derive" |
|
| 496 | + | version = "4.6.7" |
|
| 497 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 498 | + | checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" |
|
| 499 | + | dependencies = [ |
|
| 500 | + | "heck", |
|
| 501 | + | "proc-macro2", |
|
| 502 | + | "quote", |
|
| 503 | + | "syn 3.0.6", |
|
| 504 | + | ] |
|
| 505 | + | ||
| 506 | + | [[package]] |
|
| 507 | + | name = "clap_lex" |
|
| 508 | + | version = "1.1.1" |
|
| 509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 510 | + | checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" |
|
| 511 | + | ||
| 512 | + | [[package]] |
|
| 513 | + | name = "cmov" |
|
| 514 | + | version = "0.5.4" |
|
| 515 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 516 | + | checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" |
|
| 517 | + | ||
| 518 | + | [[package]] |
|
| 519 | + | name = "colorchoice" |
|
| 520 | + | version = "1.0.5" |
|
| 521 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 522 | + | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" |
|
| 523 | + | ||
| 524 | + | [[package]] |
|
| 525 | + | name = "compression-codecs" |
|
| 526 | + | version = "0.4.43" |
|
| 527 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 528 | + | checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" |
|
| 529 | + | dependencies = [ |
|
| 530 | + | "compression-core", |
|
| 531 | + | "flate2", |
|
| 532 | + | "memchr", |
|
| 533 | + | ] |
|
| 534 | + | ||
| 535 | + | [[package]] |
|
| 536 | + | name = "compression-core" |
|
| 537 | + | version = "0.4.33" |
|
| 538 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 539 | + | checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" |
|
| 540 | + | ||
| 541 | + | [[package]] |
|
| 542 | + | name = "const-oid" |
|
| 543 | + | version = "0.9.6" |
|
| 544 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 545 | + | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" |
|
| 546 | + | ||
| 547 | + | [[package]] |
|
| 548 | + | name = "const-oid" |
|
| 549 | + | version = "0.10.2" |
|
| 550 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 551 | + | checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" |
|
| 552 | + | ||
| 553 | + | [[package]] |
|
| 554 | + | name = "constant_time_eq" |
|
| 555 | + | version = "0.4.2" |
|
| 556 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 557 | + | checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" |
|
| 558 | + | ||
| 559 | + | [[package]] |
|
| 560 | + | name = "cpufeatures" |
|
| 561 | + | version = "0.2.17" |
|
| 562 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 563 | + | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" |
|
| 564 | + | dependencies = [ |
|
| 565 | + | "libc", |
|
| 566 | + | ] |
|
| 567 | + | ||
| 568 | + | [[package]] |
|
| 569 | + | name = "cpufeatures" |
|
| 570 | + | version = "0.3.1" |
|
| 571 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 572 | + | checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" |
|
| 573 | + | dependencies = [ |
|
| 574 | + | "libc", |
|
| 575 | + | ] |
|
| 576 | + | ||
| 577 | + | [[package]] |
|
| 578 | + | name = "crc" |
|
| 579 | + | version = "3.4.0" |
|
| 580 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 581 | + | checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" |
|
| 582 | + | dependencies = [ |
|
| 583 | + | "crc-catalog", |
|
| 584 | + | ] |
|
| 585 | + | ||
| 586 | + | [[package]] |
|
| 587 | + | name = "crc-catalog" |
|
| 588 | + | version = "2.5.0" |
|
| 589 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 590 | + | checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" |
|
| 591 | + | ||
| 592 | + | [[package]] |
|
| 593 | + | name = "crc32fast" |
|
| 594 | + | version = "1.5.2" |
|
| 595 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 596 | + | checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" |
|
| 597 | + | dependencies = [ |
|
| 598 | + | "cfg-if", |
|
| 599 | + | ] |
|
| 600 | + | ||
| 601 | + | [[package]] |
|
| 602 | + | name = "crossbeam-deque" |
|
| 603 | + | version = "0.8.8" |
|
| 604 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 605 | + | checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" |
|
| 606 | + | dependencies = [ |
|
| 607 | + | "crossbeam-epoch", |
|
| 608 | + | "crossbeam-utils", |
|
| 609 | + | ] |
|
| 610 | + | ||
| 611 | + | [[package]] |
|
| 612 | + | name = "crossbeam-epoch" |
|
| 613 | + | version = "0.9.21" |
|
| 614 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 615 | + | checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" |
|
| 616 | + | dependencies = [ |
|
| 617 | + | "crossbeam-utils", |
|
| 618 | + | ] |
|
| 619 | + | ||
| 620 | + | [[package]] |
|
| 621 | + | name = "crossbeam-queue" |
|
| 622 | + | version = "0.3.14" |
|
| 623 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 624 | + | checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" |
|
| 625 | + | dependencies = [ |
|
| 626 | + | "crossbeam-utils", |
|
| 627 | + | ] |
|
| 628 | + | ||
| 629 | + | [[package]] |
|
| 630 | + | name = "crossbeam-utils" |
|
| 631 | + | version = "0.8.23" |
|
| 632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 633 | + | checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" |
|
| 634 | + | ||
| 635 | + | [[package]] |
|
| 636 | + | name = "crypto-common" |
|
| 637 | + | version = "0.1.7" |
|
| 638 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 639 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" |
|
| 640 | + | dependencies = [ |
|
| 641 | + | "generic-array", |
|
| 642 | + | "typenum", |
|
| 643 | + | ] |
|
| 644 | + | ||
| 645 | + | [[package]] |
|
| 646 | + | name = "crypto-common" |
|
| 647 | + | version = "0.2.2" |
|
| 648 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 649 | + | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" |
|
| 650 | + | dependencies = [ |
|
| 651 | + | "getrandom 0.4.3", |
|
| 652 | + | "hybrid-array", |
|
| 653 | + | "rand_core 0.10.1", |
|
| 654 | + | ] |
|
| 655 | + | ||
| 656 | + | [[package]] |
|
| 657 | + | name = "cssparser" |
|
| 658 | + | version = "0.38.0" |
|
| 659 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 660 | + | checksum = "11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59" |
|
| 661 | + | dependencies = [ |
|
| 662 | + | "dtoa-short", |
|
| 663 | + | "itoa", |
|
| 664 | + | "smallvec", |
|
| 665 | + | ] |
|
| 666 | + | ||
| 667 | + | [[package]] |
|
| 668 | + | name = "ctutils" |
|
| 669 | + | version = "0.4.2" |
|
| 670 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 671 | + | checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" |
|
| 672 | + | dependencies = [ |
|
| 673 | + | "cmov", |
|
| 674 | + | ] |
|
| 675 | + | ||
| 676 | + | [[package]] |
|
| 677 | + | name = "curve25519-dalek" |
|
| 678 | + | version = "5.0.0" |
|
| 679 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 680 | + | checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" |
|
| 681 | + | dependencies = [ |
|
| 682 | + | "cfg-if", |
|
| 683 | + | "cpufeatures 0.3.1", |
|
| 684 | + | "curve25519-dalek-derive", |
|
| 685 | + | "digest 0.11.3", |
|
| 686 | + | "fiat-crypto", |
|
| 687 | + | "rustc_version", |
|
| 688 | + | "subtle", |
|
| 689 | + | "zeroize", |
|
| 690 | + | ] |
|
| 691 | + | ||
| 692 | + | [[package]] |
|
| 693 | + | name = "curve25519-dalek-derive" |
|
| 694 | + | version = "0.1.1" |
|
| 695 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 696 | + | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" |
|
| 697 | + | dependencies = [ |
|
| 698 | + | "proc-macro2", |
|
| 699 | + | "quote", |
|
| 700 | + | "syn 2.0.119", |
|
| 701 | + | ] |
|
| 702 | + | ||
| 703 | + | [[package]] |
|
| 704 | + | name = "data-encoding" |
|
| 705 | + | version = "2.11.1" |
|
| 706 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 707 | + | checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" |
|
| 708 | + | ||
| 709 | + | [[package]] |
|
| 710 | + | name = "der" |
|
| 711 | + | version = "0.7.10" |
|
| 712 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 713 | + | checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" |
|
| 714 | + | dependencies = [ |
|
| 715 | + | "const-oid 0.9.6", |
|
| 716 | + | "pem-rfc7468", |
|
| 717 | + | "zeroize", |
|
| 718 | + | ] |
|
| 719 | + | ||
| 720 | + | [[package]] |
|
| 721 | + | name = "der" |
|
| 722 | + | version = "0.8.2" |
|
| 723 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 724 | + | checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" |
|
| 725 | + | dependencies = [ |
|
| 726 | + | "const-oid 0.10.2", |
|
| 727 | + | "zeroize", |
|
| 728 | + | ] |
|
| 729 | + | ||
| 730 | + | [[package]] |
|
| 731 | + | name = "der-parser" |
|
| 732 | + | version = "10.0.0" |
|
| 733 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 734 | + | checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" |
|
| 735 | + | dependencies = [ |
|
| 736 | + | "asn1-rs", |
|
| 737 | + | "displaydoc", |
|
| 738 | + | "nom", |
|
| 739 | + | "num-bigint", |
|
| 740 | + | "num-traits", |
|
| 741 | + | "rusticata-macros", |
|
| 742 | + | ] |
|
| 743 | + | ||
| 744 | + | [[package]] |
|
| 745 | + | name = "deranged" |
|
| 746 | + | version = "0.5.8" |
|
| 747 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 748 | + | checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" |
|
| 749 | + | ||
| 750 | + | [[package]] |
|
| 751 | + | name = "digest" |
|
| 752 | + | version = "0.10.7" |
|
| 753 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 754 | + | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" |
|
| 755 | + | dependencies = [ |
|
| 756 | + | "block-buffer 0.10.4", |
|
| 757 | + | "const-oid 0.9.6", |
|
| 758 | + | "crypto-common 0.1.7", |
|
| 759 | + | "subtle", |
|
| 760 | + | ] |
|
| 761 | + | ||
| 762 | + | [[package]] |
|
| 763 | + | name = "digest" |
|
| 764 | + | version = "0.11.3" |
|
| 765 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 766 | + | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" |
|
| 767 | + | dependencies = [ |
|
| 768 | + | "block-buffer 0.12.1", |
|
| 769 | + | "crypto-common 0.2.2", |
|
| 770 | + | ] |
|
| 771 | + | ||
| 772 | + | [[package]] |
|
| 773 | + | name = "displaydoc" |
|
| 774 | + | version = "0.2.7" |
|
| 775 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 776 | + | checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" |
|
| 777 | + | dependencies = [ |
|
| 778 | + | "proc-macro2", |
|
| 779 | + | "quote", |
|
| 780 | + | "syn 3.0.6", |
|
| 781 | + | ] |
|
| 782 | + | ||
| 783 | + | [[package]] |
|
| 784 | + | name = "dotenvy" |
|
| 785 | + | version = "0.15.7" |
|
| 786 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 787 | + | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" |
|
| 788 | + | ||
| 789 | + | [[package]] |
|
| 790 | + | name = "dtoa" |
|
| 791 | + | version = "1.0.11" |
|
| 792 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 793 | + | checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" |
|
| 794 | + | ||
| 795 | + | [[package]] |
|
| 796 | + | name = "dtoa-short" |
|
| 797 | + | version = "0.3.5" |
|
| 798 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 799 | + | checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" |
|
| 800 | + | dependencies = [ |
|
| 801 | + | "dtoa", |
|
| 802 | + | ] |
|
| 803 | + | ||
| 804 | + | [[package]] |
|
| 805 | + | name = "ed25519" |
|
| 806 | + | version = "3.0.0" |
|
| 807 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 808 | + | checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" |
|
| 809 | + | dependencies = [ |
|
| 810 | + | "signature 3.0.0", |
|
| 811 | + | ] |
|
| 812 | + | ||
| 813 | + | [[package]] |
|
| 814 | + | name = "ed25519-dalek" |
|
| 815 | + | version = "3.0.0" |
|
| 816 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 817 | + | checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" |
|
| 818 | + | dependencies = [ |
|
| 819 | + | "curve25519-dalek", |
|
| 820 | + | "ed25519", |
|
| 821 | + | "sha2 0.11.0", |
|
| 822 | + | "subtle", |
|
| 823 | + | "zeroize", |
|
| 824 | + | ] |
|
| 825 | + | ||
| 826 | + | [[package]] |
|
| 827 | + | name = "either" |
|
| 828 | + | version = "1.18.0" |
|
| 829 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 830 | + | checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" |
|
| 831 | + | dependencies = [ |
|
| 832 | + | "serde", |
|
| 833 | + | ] |
|
| 834 | + | ||
| 835 | + | [[package]] |
|
| 836 | + | name = "equivalent" |
|
| 837 | + | version = "1.0.2" |
|
| 838 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 839 | + | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" |
|
| 840 | + | ||
| 841 | + | [[package]] |
|
| 842 | + | name = "errno" |
|
| 843 | + | version = "0.3.14" |
|
| 844 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 845 | + | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" |
|
| 846 | + | dependencies = [ |
|
| 847 | + | "libc", |
|
| 848 | + | "windows-sys 0.61.2", |
|
| 849 | + | ] |
|
| 850 | + | ||
| 851 | + | [[package]] |
|
| 852 | + | name = "etcetera" |
|
| 853 | + | version = "0.8.0" |
|
| 854 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 855 | + | checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" |
|
| 856 | + | dependencies = [ |
|
| 857 | + | "cfg-if", |
|
| 858 | + | "home", |
|
| 859 | + | "windows-sys 0.48.0", |
|
| 860 | + | ] |
|
| 861 | + | ||
| 862 | + | [[package]] |
|
| 863 | + | name = "event-listener" |
|
| 864 | + | version = "5.4.2" |
|
| 865 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 866 | + | checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" |
|
| 867 | + | dependencies = [ |
|
| 868 | + | "parking", |
|
| 869 | + | "pin-project-lite", |
|
| 870 | + | ] |
|
| 871 | + | ||
| 872 | + | [[package]] |
|
| 873 | + | name = "fancy-regex" |
|
| 874 | + | version = "0.16.2" |
|
| 875 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 876 | + | checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" |
|
| 877 | + | dependencies = [ |
|
| 878 | + | "bit-set", |
|
| 879 | + | "regex-automata", |
|
| 880 | + | "regex-syntax", |
|
| 881 | + | ] |
|
| 882 | + | ||
| 883 | + | [[package]] |
|
| 884 | + | name = "fastrand" |
|
| 885 | + | version = "2.5.0" |
|
| 886 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 887 | + | checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" |
|
| 888 | + | ||
| 889 | + | [[package]] |
|
| 890 | + | name = "fiat-crypto" |
|
| 891 | + | version = "0.3.0" |
|
| 892 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 893 | + | checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" |
|
| 894 | + | ||
| 895 | + | [[package]] |
|
| 896 | + | name = "find-msvc-tools" |
|
| 897 | + | version = "0.1.14" |
|
| 898 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 899 | + | checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" |
|
| 900 | + | ||
| 901 | + | [[package]] |
|
| 902 | + | name = "flate2" |
|
| 903 | + | version = "1.1.10" |
|
| 904 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 905 | + | checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" |
|
| 906 | + | dependencies = [ |
|
| 907 | + | "crc32fast", |
|
| 908 | + | "miniz_oxide", |
|
| 909 | + | "zlib-rs", |
|
| 910 | + | ] |
|
| 911 | + | ||
| 912 | + | [[package]] |
|
| 913 | + | name = "flume" |
|
| 914 | + | version = "0.11.1" |
|
| 915 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 916 | + | checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" |
|
| 917 | + | dependencies = [ |
|
| 918 | + | "futures-core", |
|
| 919 | + | "futures-sink", |
|
| 920 | + | "spin", |
|
| 921 | + | ] |
|
| 922 | + | ||
| 923 | + | [[package]] |
|
| 924 | + | name = "fnv" |
|
| 925 | + | version = "1.0.7" |
|
| 926 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 927 | + | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" |
|
| 928 | + | ||
| 929 | + | [[package]] |
|
| 930 | + | name = "foldhash" |
|
| 931 | + | version = "0.1.5" |
|
| 932 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 933 | + | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" |
|
| 934 | + | ||
| 935 | + | [[package]] |
|
| 936 | + | name = "form_urlencoded" |
|
| 937 | + | version = "1.2.2" |
|
| 938 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 939 | + | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" |
|
| 940 | + | dependencies = [ |
|
| 941 | + | "percent-encoding", |
|
| 942 | + | ] |
|
| 943 | + | ||
| 944 | + | [[package]] |
|
| 945 | + | name = "fs-err" |
|
| 946 | + | version = "3.3.1" |
|
| 947 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 948 | + | checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" |
|
| 949 | + | dependencies = [ |
|
| 950 | + | "autocfg", |
|
| 951 | + | "tokio", |
|
| 952 | + | ] |
|
| 953 | + | ||
| 954 | + | [[package]] |
|
| 955 | + | name = "futures-channel" |
|
| 956 | + | version = "0.3.34" |
|
| 957 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 958 | + | checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" |
|
| 959 | + | dependencies = [ |
|
| 960 | + | "futures-core", |
|
| 961 | + | "futures-sink", |
|
| 962 | + | ] |
|
| 963 | + | ||
| 964 | + | [[package]] |
|
| 965 | + | name = "futures-core" |
|
| 966 | + | version = "0.3.34" |
|
| 967 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 968 | + | checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" |
|
| 969 | + | ||
| 970 | + | [[package]] |
|
| 971 | + | name = "futures-executor" |
|
| 972 | + | version = "0.3.34" |
|
| 973 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 974 | + | checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" |
|
| 975 | + | dependencies = [ |
|
| 976 | + | "futures-core", |
|
| 977 | + | "futures-task", |
|
| 978 | + | "futures-util", |
|
| 979 | + | ] |
|
| 980 | + | ||
| 981 | + | [[package]] |
|
| 982 | + | name = "futures-intrusive" |
|
| 983 | + | version = "0.5.0" |
|
| 984 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 985 | + | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" |
|
| 986 | + | dependencies = [ |
|
| 987 | + | "futures-core", |
|
| 988 | + | "lock_api", |
|
| 989 | + | "parking_lot", |
|
| 990 | + | ] |
|
| 991 | + | ||
| 992 | + | [[package]] |
|
| 993 | + | name = "futures-io" |
|
| 994 | + | version = "0.3.34" |
|
| 995 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 996 | + | checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" |
|
| 997 | + | ||
| 998 | + | [[package]] |
|
| 999 | + | name = "futures-sink" |
|
| 1000 | + | version = "0.3.34" |
|
| 1001 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1002 | + | checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" |
|
| 1003 | + | ||
| 1004 | + | [[package]] |
|
| 1005 | + | name = "futures-task" |
|
| 1006 | + | version = "0.3.34" |
|
| 1007 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1008 | + | checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" |
|
| 1009 | + | ||
| 1010 | + | [[package]] |
|
| 1011 | + | name = "futures-util" |
|
| 1012 | + | version = "0.3.34" |
|
| 1013 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1014 | + | checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" |
|
| 1015 | + | dependencies = [ |
|
| 1016 | + | "futures-core", |
|
| 1017 | + | "futures-io", |
|
| 1018 | + | "futures-sink", |
|
| 1019 | + | "futures-task", |
|
| 1020 | + | "memchr", |
|
| 1021 | + | "pin-project-lite", |
|
| 1022 | + | "slab", |
|
| 1023 | + | ] |
|
| 1024 | + | ||
| 1025 | + | [[package]] |
|
| 1026 | + | name = "generic-array" |
|
| 1027 | + | version = "0.14.7" |
|
| 1028 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1029 | + | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" |
|
| 1030 | + | dependencies = [ |
|
| 1031 | + | "typenum", |
|
| 1032 | + | "version_check", |
|
| 1033 | + | ] |
|
| 1034 | + | ||
| 1035 | + | [[package]] |
|
| 1036 | + | name = "getrandom" |
|
| 1037 | + | version = "0.2.17" |
|
| 1038 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1039 | + | checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" |
|
| 1040 | + | dependencies = [ |
|
| 1041 | + | "cfg-if", |
|
| 1042 | + | "js-sys", |
|
| 1043 | + | "libc", |
|
| 1044 | + | "wasi", |
|
| 1045 | + | "wasm-bindgen", |
|
| 1046 | + | ] |
|
| 1047 | + | ||
| 1048 | + | [[package]] |
|
| 1049 | + | name = "getrandom" |
|
| 1050 | + | version = "0.4.3" |
|
| 1051 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1052 | + | checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" |
|
| 1053 | + | dependencies = [ |
|
| 1054 | + | "cfg-if", |
|
| 1055 | + | "js-sys", |
|
| 1056 | + | "libc", |
|
| 1057 | + | "r-efi", |
|
| 1058 | + | "rand_core 0.10.1", |
|
| 1059 | + | "wasm-bindgen", |
|
| 1060 | + | ] |
|
| 1061 | + | ||
| 1062 | + | [[package]] |
|
| 1063 | + | name = "globset" |
|
| 1064 | + | version = "0.4.20" |
|
| 1065 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1066 | + | checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" |
|
| 1067 | + | dependencies = [ |
|
| 1068 | + | "aho-corasick", |
|
| 1069 | + | "bstr", |
|
| 1070 | + | "log", |
|
| 1071 | + | "regex-automata", |
|
| 1072 | + | "regex-syntax", |
|
| 1073 | + | ] |
|
| 1074 | + | ||
| 1075 | + | [[package]] |
|
| 1076 | + | name = "h2" |
|
| 1077 | + | version = "0.4.19" |
|
| 1078 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1079 | + | checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" |
|
| 1080 | + | dependencies = [ |
|
| 1081 | + | "atomic-waker", |
|
| 1082 | + | "bytes", |
|
| 1083 | + | "fnv", |
|
| 1084 | + | "futures-core", |
|
| 1085 | + | "futures-sink", |
|
| 1086 | + | "http", |
|
| 1087 | + | "indexmap", |
|
| 1088 | + | "slab", |
|
| 1089 | + | "tokio", |
|
| 1090 | + | "tokio-util", |
|
| 1091 | + | "tracing", |
|
| 1092 | + | ] |
|
| 1093 | + | ||
| 1094 | + | [[package]] |
|
| 1095 | + | name = "hashbrown" |
|
| 1096 | + | version = "0.15.5" |
|
| 1097 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1098 | + | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" |
|
| 1099 | + | dependencies = [ |
|
| 1100 | + | "allocator-api2", |
|
| 1101 | + | "equivalent", |
|
| 1102 | + | "foldhash", |
|
| 1103 | + | ] |
|
| 1104 | + | ||
| 1105 | + | [[package]] |
|
| 1106 | + | name = "hashbrown" |
|
| 1107 | + | version = "0.17.1" |
|
| 1108 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1109 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" |
|
| 1110 | + | ||
| 1111 | + | [[package]] |
|
| 1112 | + | name = "hashlink" |
|
| 1113 | + | version = "0.10.0" |
|
| 1114 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1115 | + | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" |
|
| 1116 | + | dependencies = [ |
|
| 1117 | + | "hashbrown 0.15.5", |
|
| 1118 | + | ] |
|
| 1119 | + | ||
| 1120 | + | [[package]] |
|
| 1121 | + | name = "heck" |
|
| 1122 | + | version = "0.5.0" |
|
| 1123 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1124 | + | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" |
|
| 1125 | + | ||
| 1126 | + | [[package]] |
|
| 1127 | + | name = "hex" |
|
| 1128 | + | version = "0.4.3" |
|
| 1129 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1130 | + | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" |
|
| 1131 | + | ||
| 1132 | + | [[package]] |
|
| 1133 | + | name = "hkdf" |
|
| 1134 | + | version = "0.12.4" |
|
| 1135 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1136 | + | checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" |
|
| 1137 | + | dependencies = [ |
|
| 1138 | + | "hmac", |
|
| 1139 | + | ] |
|
| 1140 | + | ||
| 1141 | + | [[package]] |
|
| 1142 | + | name = "hmac" |
|
| 1143 | + | version = "0.12.1" |
|
| 1144 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1145 | + | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" |
|
| 1146 | + | dependencies = [ |
|
| 1147 | + | "digest 0.10.7", |
|
| 1148 | + | ] |
|
| 1149 | + | ||
| 1150 | + | [[package]] |
|
| 1151 | + | name = "home" |
|
| 1152 | + | version = "0.5.12" |
|
| 1153 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1154 | + | checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" |
|
| 1155 | + | dependencies = [ |
|
| 1156 | + | "windows-sys 0.61.2", |
|
| 1157 | + | ] |
|
| 1158 | + | ||
| 1159 | + | [[package]] |
|
| 1160 | + | name = "html5ever" |
|
| 1161 | + | version = "0.40.1" |
|
| 1162 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1163 | + | checksum = "456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130" |
|
| 1164 | + | dependencies = [ |
|
| 1165 | + | "log", |
|
| 1166 | + | "markup5ever", |
|
| 1167 | + | "memchr", |
|
| 1168 | + | ] |
|
| 1169 | + | ||
| 1170 | + | [[package]] |
|
| 1171 | + | name = "http" |
|
| 1172 | + | version = "1.5.0" |
|
| 1173 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1174 | + | checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" |
|
| 1175 | + | dependencies = [ |
|
| 1176 | + | "bytes", |
|
| 1177 | + | "itoa", |
|
| 1178 | + | ] |
|
| 1179 | + | ||
| 1180 | + | [[package]] |
|
| 1181 | + | name = "http-body" |
|
| 1182 | + | version = "1.1.0" |
|
| 1183 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1184 | + | checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" |
|
| 1185 | + | dependencies = [ |
|
| 1186 | + | "bytes", |
|
| 1187 | + | "http", |
|
| 1188 | + | ] |
|
| 1189 | + | ||
| 1190 | + | [[package]] |
|
| 1191 | + | name = "http-body-util" |
|
| 1192 | + | version = "0.1.5" |
|
| 1193 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1194 | + | checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" |
|
| 1195 | + | dependencies = [ |
|
| 1196 | + | "bytes", |
|
| 1197 | + | "futures-core", |
|
| 1198 | + | "http", |
|
| 1199 | + | "http-body", |
|
| 1200 | + | "pin-project-lite", |
|
| 1201 | + | ] |
|
| 1202 | + | ||
| 1203 | + | [[package]] |
|
| 1204 | + | name = "httparse" |
|
| 1205 | + | version = "1.10.1" |
|
| 1206 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1207 | + | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" |
|
| 1208 | + | ||
| 1209 | + | [[package]] |
|
| 1210 | + | name = "httpdate" |
|
| 1211 | + | version = "1.0.3" |
|
| 1212 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1213 | + | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" |
|
| 1214 | + | ||
| 1215 | + | [[package]] |
|
| 1216 | + | name = "hub-api" |
|
| 1217 | + | version = "0.1.0" |
|
| 1218 | + | dependencies = [ |
|
| 1219 | + | "serde", |
|
| 1220 | + | ] |
|
| 1221 | + | ||
| 1222 | + | [[package]] |
|
| 1223 | + | name = "hybrid-array" |
|
| 1224 | + | version = "0.4.15" |
|
| 1225 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1226 | + | checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" |
|
| 1227 | + | dependencies = [ |
|
| 1228 | + | "ctutils", |
|
| 1229 | + | "typenum", |
|
| 1230 | + | ] |
|
| 1231 | + | ||
| 1232 | + | [[package]] |
|
| 1233 | + | name = "hyper" |
|
| 1234 | + | version = "1.11.1" |
|
| 1235 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1236 | + | checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" |
|
| 1237 | + | dependencies = [ |
|
| 1238 | + | "atomic-waker", |
|
| 1239 | + | "bytes", |
|
| 1240 | + | "futures-channel", |
|
| 1241 | + | "futures-core", |
|
| 1242 | + | "h2", |
|
| 1243 | + | "http", |
|
| 1244 | + | "http-body", |
|
| 1245 | + | "httparse", |
|
| 1246 | + | "httpdate", |
|
| 1247 | + | "itoa", |
|
| 1248 | + | "pin-project-lite", |
|
| 1249 | + | "smallvec", |
|
| 1250 | + | "tokio", |
|
| 1251 | + | "want", |
|
| 1252 | + | ] |
|
| 1253 | + | ||
| 1254 | + | [[package]] |
|
| 1255 | + | name = "hyper-rustls" |
|
| 1256 | + | version = "0.27.10" |
|
| 1257 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1258 | + | checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" |
|
| 1259 | + | dependencies = [ |
|
| 1260 | + | "http", |
|
| 1261 | + | "hyper", |
|
| 1262 | + | "hyper-util", |
|
| 1263 | + | "rustls", |
|
| 1264 | + | "tokio", |
|
| 1265 | + | "tokio-rustls", |
|
| 1266 | + | "tower-service", |
|
| 1267 | + | "webpki-roots 1.0.9", |
|
| 1268 | + | ] |
|
| 1269 | + | ||
| 1270 | + | [[package]] |
|
| 1271 | + | name = "hyper-util" |
|
| 1272 | + | version = "0.1.21" |
|
| 1273 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1274 | + | checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" |
|
| 1275 | + | dependencies = [ |
|
| 1276 | + | "base64 0.23.1", |
|
| 1277 | + | "bytes", |
|
| 1278 | + | "futures-channel", |
|
| 1279 | + | "futures-util", |
|
| 1280 | + | "http", |
|
| 1281 | + | "http-body", |
|
| 1282 | + | "httparse", |
|
| 1283 | + | "hyper", |
|
| 1284 | + | "ipnet", |
|
| 1285 | + | "libc", |
|
| 1286 | + | "percent-encoding", |
|
| 1287 | + | "pin-project-lite", |
|
| 1288 | + | "socket2", |
|
| 1289 | + | "tokio", |
|
| 1290 | + | "tower-service", |
|
| 1291 | + | "tracing", |
|
| 1292 | + | ] |
|
| 1293 | + | ||
| 1294 | + | [[package]] |
|
| 1295 | + | name = "icu_collections" |
|
| 1296 | + | version = "2.3.0" |
|
| 1297 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1298 | + | checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" |
|
| 1299 | + | dependencies = [ |
|
| 1300 | + | "displaydoc", |
|
| 1301 | + | "potential_utf", |
|
| 1302 | + | "utf8_iter", |
|
| 1303 | + | "yoke", |
|
| 1304 | + | "zerofrom", |
|
| 1305 | + | "zerovec", |
|
| 1306 | + | ] |
|
| 1307 | + | ||
| 1308 | + | [[package]] |
|
| 1309 | + | name = "icu_locale_core" |
|
| 1310 | + | version = "2.3.0" |
|
| 1311 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1312 | + | checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" |
|
| 1313 | + | dependencies = [ |
|
| 1314 | + | "displaydoc", |
|
| 1315 | + | "litemap", |
|
| 1316 | + | "tinystr", |
|
| 1317 | + | "writeable", |
|
| 1318 | + | "zerovec", |
|
| 1319 | + | ] |
|
| 1320 | + | ||
| 1321 | + | [[package]] |
|
| 1322 | + | name = "icu_normalizer" |
|
| 1323 | + | version = "2.3.0" |
|
| 1324 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1325 | + | checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" |
|
| 1326 | + | dependencies = [ |
|
| 1327 | + | "icu_collections", |
|
| 1328 | + | "icu_normalizer_data", |
|
| 1329 | + | "icu_properties", |
|
| 1330 | + | "icu_provider", |
|
| 1331 | + | "smallvec", |
|
| 1332 | + | "zerovec", |
|
| 1333 | + | ] |
|
| 1334 | + | ||
| 1335 | + | [[package]] |
|
| 1336 | + | name = "icu_normalizer_data" |
|
| 1337 | + | version = "2.3.0" |
|
| 1338 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1339 | + | checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" |
|
| 1340 | + | ||
| 1341 | + | [[package]] |
|
| 1342 | + | name = "icu_properties" |
|
| 1343 | + | version = "2.3.0" |
|
| 1344 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1345 | + | checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" |
|
| 1346 | + | dependencies = [ |
|
| 1347 | + | "displaydoc", |
|
| 1348 | + | "icu_collections", |
|
| 1349 | + | "icu_locale_core", |
|
| 1350 | + | "icu_properties_data", |
|
| 1351 | + | "icu_provider", |
|
| 1352 | + | "zerotrie", |
|
| 1353 | + | "zerovec", |
|
| 1354 | + | ] |
|
| 1355 | + | ||
| 1356 | + | [[package]] |
|
| 1357 | + | name = "icu_properties_data" |
|
| 1358 | + | version = "2.3.0" |
|
| 1359 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1360 | + | checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" |
|
| 1361 | + | ||
| 1362 | + | [[package]] |
|
| 1363 | + | name = "icu_provider" |
|
| 1364 | + | version = "2.3.1" |
|
| 1365 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1366 | + | checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" |
|
| 1367 | + | dependencies = [ |
|
| 1368 | + | "displaydoc", |
|
| 1369 | + | "icu_locale_core", |
|
| 1370 | + | "writeable", |
|
| 1371 | + | "yoke", |
|
| 1372 | + | "zerofrom", |
|
| 1373 | + | "zerotrie", |
|
| 1374 | + | "zerovec", |
|
| 1375 | + | ] |
|
| 1376 | + | ||
| 1377 | + | [[package]] |
|
| 1378 | + | name = "idna" |
|
| 1379 | + | version = "1.1.0" |
|
| 1380 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1381 | + | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" |
|
| 1382 | + | dependencies = [ |
|
| 1383 | + | "idna_adapter", |
|
| 1384 | + | "smallvec", |
|
| 1385 | + | "utf8_iter", |
|
| 1386 | + | ] |
|
| 1387 | + | ||
| 1388 | + | [[package]] |
|
| 1389 | + | name = "idna_adapter" |
|
| 1390 | + | version = "1.2.2" |
|
| 1391 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1392 | + | checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" |
|
| 1393 | + | dependencies = [ |
|
| 1394 | + | "icu_normalizer", |
|
| 1395 | + | "icu_properties", |
|
| 1396 | + | ] |
|
| 1397 | + | ||
| 1398 | + | [[package]] |
|
| 1399 | + | name = "ignore" |
|
| 1400 | + | version = "0.4.33" |
|
| 1401 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1402 | + | checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" |
|
| 1403 | + | dependencies = [ |
|
| 1404 | + | "crossbeam-deque", |
|
| 1405 | + | "globset", |
|
| 1406 | + | "log", |
|
| 1407 | + | "memchr", |
|
| 1408 | + | "regex-automata", |
|
| 1409 | + | "same-file", |
|
| 1410 | + | "walkdir", |
|
| 1411 | + | "winapi-util", |
|
| 1412 | + | ] |
|
| 1413 | + | ||
| 1414 | + | [[package]] |
|
| 1415 | + | name = "indexmap" |
|
| 1416 | + | version = "2.14.2" |
|
| 1417 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1418 | + | checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" |
|
| 1419 | + | dependencies = [ |
|
| 1420 | + | "equivalent", |
|
| 1421 | + | "hashbrown 0.17.1", |
|
| 1422 | + | ] |
|
| 1423 | + | ||
| 1424 | + | [[package]] |
|
| 1425 | + | name = "ipnet" |
|
| 1426 | + | version = "2.12.2" |
|
| 1427 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1428 | + | checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" |
|
| 1429 | + | ||
| 1430 | + | [[package]] |
|
| 1431 | + | name = "is_terminal_polyfill" |
|
| 1432 | + | version = "1.70.2" |
|
| 1433 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1434 | + | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" |
|
| 1435 | + | ||
| 1436 | + | [[package]] |
|
| 1437 | + | name = "itoa" |
|
| 1438 | + | version = "1.0.18" |
|
| 1439 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1440 | + | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" |
|
| 1441 | + | ||
| 1442 | + | [[package]] |
|
| 1443 | + | name = "jobserver" |
|
| 1444 | + | version = "0.1.35" |
|
| 1445 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1446 | + | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" |
|
| 1447 | + | dependencies = [ |
|
| 1448 | + | "getrandom 0.4.3", |
|
| 1449 | + | "libc", |
|
| 1450 | + | ] |
|
| 1451 | + | ||
| 1452 | + | [[package]] |
|
| 1453 | + | name = "js-sys" |
|
| 1454 | + | version = "0.3.106" |
|
| 1455 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1456 | + | checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" |
|
| 1457 | + | dependencies = [ |
|
| 1458 | + | "cfg-if", |
|
| 1459 | + | "futures-util", |
|
| 1460 | + | "wasm-bindgen", |
|
| 1461 | + | ] |
|
| 1462 | + | ||
| 1463 | + | [[package]] |
|
| 1464 | + | name = "keccak" |
|
| 1465 | + | version = "0.2.2" |
|
| 1466 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1467 | + | checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" |
|
| 1468 | + | dependencies = [ |
|
| 1469 | + | "cfg-if", |
|
| 1470 | + | "cpufeatures 0.3.1", |
|
| 1471 | + | ] |
|
| 1472 | + | ||
| 1473 | + | [[package]] |
|
| 1474 | + | name = "lazy_static" |
|
| 1475 | + | version = "1.5.0" |
|
| 1476 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1477 | + | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" |
|
| 1478 | + | dependencies = [ |
|
| 1479 | + | "spin", |
|
| 1480 | + | ] |
|
| 1481 | + | ||
| 1482 | + | [[package]] |
|
| 1483 | + | name = "libc" |
|
| 1484 | + | version = "0.2.189" |
|
| 1485 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1486 | + | checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" |
|
| 1487 | + | ||
| 1488 | + | [[package]] |
|
| 1489 | + | name = "libm" |
|
| 1490 | + | version = "0.2.16" |
|
| 1491 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1492 | + | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" |
|
| 1493 | + | ||
| 1494 | + | [[package]] |
|
| 1495 | + | name = "libredox" |
|
| 1496 | + | version = "0.1.25" |
|
| 1497 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1498 | + | checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" |
|
| 1499 | + | dependencies = [ |
|
| 1500 | + | "bitflags", |
|
| 1501 | + | "libc", |
|
| 1502 | + | "plain", |
|
| 1503 | + | "redox_syscall 0.9.4", |
|
| 1504 | + | ] |
|
| 1505 | + | ||
| 1506 | + | [[package]] |
|
| 1507 | + | name = "libsqlite3-sys" |
|
| 1508 | + | version = "0.30.1" |
|
| 1509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1510 | + | checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" |
|
| 1511 | + | dependencies = [ |
|
| 1512 | + | "cc", |
|
| 1513 | + | "pkg-config", |
|
| 1514 | + | "vcpkg", |
|
| 1515 | + | ] |
|
| 1516 | + | ||
| 1517 | + | [[package]] |
|
| 1518 | + | name = "linked-hash-map" |
|
| 1519 | + | version = "0.5.6" |
|
| 1520 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1521 | + | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" |
|
| 1522 | + | ||
| 1523 | + | [[package]] |
|
| 1524 | + | name = "linux-raw-sys" |
|
| 1525 | + | version = "0.12.1" |
|
| 1526 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1527 | + | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" |
|
| 1528 | + | ||
| 1529 | + | [[package]] |
|
| 1530 | + | name = "litemap" |
|
| 1531 | + | version = "0.8.3" |
|
| 1532 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1533 | + | checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" |
|
| 1534 | + | ||
| 1535 | + | [[package]] |
|
| 1536 | + | name = "lock_api" |
|
| 1537 | + | version = "0.4.14" |
|
| 1538 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1539 | + | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" |
|
| 1540 | + | dependencies = [ |
|
| 1541 | + | "scopeguard", |
|
| 1542 | + | ] |
|
| 1543 | + | ||
| 1544 | + | [[package]] |
|
| 1545 | + | name = "log" |
|
| 1546 | + | version = "0.4.34" |
|
| 1547 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1548 | + | checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" |
|
| 1549 | + | ||
| 1550 | + | [[package]] |
|
| 1551 | + | name = "lru-slab" |
|
| 1552 | + | version = "0.1.3" |
|
| 1553 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1554 | + | checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" |
|
| 1555 | + | ||
| 1556 | + | [[package]] |
|
| 1557 | + | name = "maplit" |
|
| 1558 | + | version = "1.0.2" |
|
| 1559 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1560 | + | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" |
|
| 1561 | + | ||
| 1562 | + | [[package]] |
|
| 1563 | + | name = "markup5ever" |
|
| 1564 | + | version = "0.40.0" |
|
| 1565 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1566 | + | checksum = "0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842" |
|
| 1567 | + | dependencies = [ |
|
| 1568 | + | "log", |
|
| 1569 | + | "tendril", |
|
| 1570 | + | "web_atoms", |
|
| 1571 | + | ] |
|
| 1572 | + | ||
| 1573 | + | [[package]] |
|
| 1574 | + | name = "matchers" |
|
| 1575 | + | version = "0.2.0" |
|
| 1576 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1577 | + | checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" |
|
| 1578 | + | dependencies = [ |
|
| 1579 | + | "regex-automata", |
|
| 1580 | + | ] |
|
| 1581 | + | ||
| 1582 | + | [[package]] |
|
| 1583 | + | name = "matchit" |
|
| 1584 | + | version = "0.8.4" |
|
| 1585 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1586 | + | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" |
|
| 1587 | + | ||
| 1588 | + | [[package]] |
|
| 1589 | + | name = "md-5" |
|
| 1590 | + | version = "0.10.6" |
|
| 1591 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1592 | + | checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" |
|
| 1593 | + | dependencies = [ |
|
| 1594 | + | "cfg-if", |
|
| 1595 | + | "digest 0.10.7", |
|
| 1596 | + | ] |
|
| 1597 | + | ||
| 1598 | + | [[package]] |
|
| 1599 | + | name = "memchr" |
|
| 1600 | + | version = "2.8.3" |
|
| 1601 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1602 | + | checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" |
|
| 1603 | + | ||
| 1604 | + | [[package]] |
|
| 1605 | + | name = "mime" |
|
| 1606 | + | version = "0.3.17" |
|
| 1607 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1608 | + | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" |
|
| 1609 | + | ||
| 1610 | + | [[package]] |
|
| 1611 | + | name = "minimal-lexical" |
|
| 1612 | + | version = "0.2.1" |
|
| 1613 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1614 | + | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" |
|
| 1615 | + | ||
| 1616 | + | [[package]] |
|
| 1617 | + | name = "miniz_oxide" |
|
| 1618 | + | version = "0.9.1" |
|
| 1619 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1620 | + | checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" |
|
| 1621 | + | dependencies = [ |
|
| 1622 | + | "adler2", |
|
| 1623 | + | "simd-adler32", |
|
| 1624 | + | ] |
|
| 1625 | + | ||
| 1626 | + | [[package]] |
|
| 1627 | + | name = "mio" |
|
| 1628 | + | version = "1.2.3" |
|
| 1629 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1630 | + | checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" |
|
| 1631 | + | dependencies = [ |
|
| 1632 | + | "libc", |
|
| 1633 | + | "wasi", |
|
| 1634 | + | "windows-sys 0.61.2", |
|
| 1635 | + | ] |
|
| 1636 | + | ||
| 1637 | + | [[package]] |
|
| 1638 | + | name = "ml-dsa" |
|
| 1639 | + | version = "0.1.1" |
|
| 1640 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1641 | + | checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" |
|
| 1642 | + | dependencies = [ |
|
| 1643 | + | "const-oid 0.10.2", |
|
| 1644 | + | "crypto-common 0.2.2", |
|
| 1645 | + | "ctutils", |
|
| 1646 | + | "hybrid-array", |
|
| 1647 | + | "module-lattice", |
|
| 1648 | + | "pkcs8 0.11.0", |
|
| 1649 | + | "shake", |
|
| 1650 | + | "signature 3.0.0", |
|
| 1651 | + | ] |
|
| 1652 | + | ||
| 1653 | + | [[package]] |
|
| 1654 | + | name = "module-lattice" |
|
| 1655 | + | version = "0.2.3" |
|
| 1656 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1657 | + | checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" |
|
| 1658 | + | dependencies = [ |
|
| 1659 | + | "ctutils", |
|
| 1660 | + | "hybrid-array", |
|
| 1661 | + | "num-traits", |
|
| 1662 | + | ] |
|
| 1663 | + | ||
| 1664 | + | [[package]] |
|
| 1665 | + | name = "new_debug_unreachable" |
|
| 1666 | + | version = "1.0.6" |
|
| 1667 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1668 | + | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" |
|
| 1669 | + | ||
| 1670 | + | [[package]] |
|
| 1671 | + | name = "nom" |
|
| 1672 | + | version = "7.1.3" |
|
| 1673 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1674 | + | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" |
|
| 1675 | + | dependencies = [ |
|
| 1676 | + | "memchr", |
|
| 1677 | + | "minimal-lexical", |
|
| 1678 | + | ] |
|
| 1679 | + | ||
| 1680 | + | [[package]] |
|
| 1681 | + | name = "nu-ansi-term" |
|
| 1682 | + | version = "0.50.3" |
|
| 1683 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1684 | + | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" |
|
| 1685 | + | dependencies = [ |
|
| 1686 | + | "windows-sys 0.61.2", |
|
| 1687 | + | ] |
|
| 1688 | + | ||
| 1689 | + | [[package]] |
|
| 1690 | + | name = "num-bigint" |
|
| 1691 | + | version = "0.4.8" |
|
| 1692 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1693 | + | checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" |
|
| 1694 | + | dependencies = [ |
|
| 1695 | + | "num-integer", |
|
| 1696 | + | "num-traits", |
|
| 1697 | + | ] |
|
| 1698 | + | ||
| 1699 | + | [[package]] |
|
| 1700 | + | name = "num-bigint-dig" |
|
| 1701 | + | version = "0.8.6" |
|
| 1702 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1703 | + | checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" |
|
| 1704 | + | dependencies = [ |
|
| 1705 | + | "lazy_static", |
|
| 1706 | + | "libm", |
|
| 1707 | + | "num-integer", |
|
| 1708 | + | "num-iter", |
|
| 1709 | + | "num-traits", |
|
| 1710 | + | "rand 0.8.8", |
|
| 1711 | + | "smallvec", |
|
| 1712 | + | "zeroize", |
|
| 1713 | + | ] |
|
| 1714 | + | ||
| 1715 | + | [[package]] |
|
| 1716 | + | name = "num-conv" |
|
| 1717 | + | version = "0.2.2" |
|
| 1718 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1719 | + | checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" |
|
| 1720 | + | ||
| 1721 | + | [[package]] |
|
| 1722 | + | name = "num-integer" |
|
| 1723 | + | version = "0.1.47" |
|
| 1724 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1725 | + | checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" |
|
| 1726 | + | dependencies = [ |
|
| 1727 | + | "num-traits", |
|
| 1728 | + | ] |
|
| 1729 | + | ||
| 1730 | + | [[package]] |
|
| 1731 | + | name = "num-iter" |
|
| 1732 | + | version = "0.1.46" |
|
| 1733 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1734 | + | checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" |
|
| 1735 | + | dependencies = [ |
|
| 1736 | + | "num-integer", |
|
| 1737 | + | "num-traits", |
|
| 1738 | + | ] |
|
| 1739 | + | ||
| 1740 | + | [[package]] |
|
| 1741 | + | name = "num-traits" |
|
| 1742 | + | version = "0.2.19" |
|
| 1743 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1744 | + | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" |
|
| 1745 | + | dependencies = [ |
|
| 1746 | + | "autocfg", |
|
| 1747 | + | "libm", |
|
| 1748 | + | ] |
|
| 1749 | + | ||
| 1750 | + | [[package]] |
|
| 1751 | + | name = "oid-registry" |
|
| 1752 | + | version = "0.8.1" |
|
| 1753 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1754 | + | checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" |
|
| 1755 | + | dependencies = [ |
|
| 1756 | + | "asn1-rs", |
|
| 1757 | + | ] |
|
| 1758 | + | ||
| 1759 | + | [[package]] |
|
| 1760 | + | name = "once_cell" |
|
| 1761 | + | version = "1.21.4" |
|
| 1762 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1763 | + | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" |
|
| 1764 | + | ||
| 1765 | + | [[package]] |
|
| 1766 | + | name = "once_cell_polyfill" |
|
| 1767 | + | version = "1.70.2" |
|
| 1768 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1769 | + | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" |
|
| 1770 | + | ||
| 1771 | + | [[package]] |
|
| 1772 | + | name = "oxy-core" |
|
| 1773 | + | version = "0.1.0" |
|
| 1774 | + | dependencies = [ |
|
| 1775 | + | "blake3", |
|
| 1776 | + | "ed25519-dalek", |
|
| 1777 | + | "globset", |
|
| 1778 | + | "hex", |
|
| 1779 | + | "ignore", |
|
| 1780 | + | "ml-dsa", |
|
| 1781 | + | "rand 0.8.8", |
|
| 1782 | + | "rustls", |
|
| 1783 | + | "rustls-pemfile", |
|
| 1784 | + | "similar", |
|
| 1785 | + | "thiserror 1.0.69", |
|
| 1786 | + | "ureq", |
|
| 1787 | + | "webpki-roots 1.0.9", |
|
| 1788 | + | "zstd", |
|
| 1789 | + | ] |
|
| 1790 | + | ||
| 1791 | + | [[package]] |
|
| 1792 | + | name = "oxy-server" |
|
| 1793 | + | version = "0.1.0" |
|
| 1794 | + | dependencies = [ |
|
| 1795 | + | "axum", |
|
| 1796 | + | "axum-server", |
|
| 1797 | + | "oxy-core", |
|
| 1798 | + | "rustls", |
|
| 1799 | + | "tokio", |
|
| 1800 | + | ] |
|
| 1801 | + | ||
| 1802 | + | [[package]] |
|
| 1803 | + | name = "oxygenhub" |
|
| 1804 | + | version = "0.1.0" |
|
| 1805 | + | dependencies = [ |
|
| 1806 | + | "ammonia", |
|
| 1807 | + | "anyhow", |
|
| 1808 | + | "askama", |
|
| 1809 | + | "axum", |
|
| 1810 | + | "axum-server", |
|
| 1811 | + | "clap", |
|
| 1812 | + | "hex", |
|
| 1813 | + | "http-body-util", |
|
| 1814 | + | "hub-api", |
|
| 1815 | + | "oxy-core", |
|
| 1816 | + | "oxy-server", |
|
| 1817 | + | "pulldown-cmark", |
|
| 1818 | + | "rand 0.8.8", |
|
| 1819 | + | "rcgen", |
|
| 1820 | + | "reqwest", |
|
| 1821 | + | "rustls", |
|
| 1822 | + | "serde", |
|
| 1823 | + | "serde_json", |
|
| 1824 | + | "similar", |
|
| 1825 | + | "sqlx", |
|
| 1826 | + | "syntect", |
|
| 1827 | + | "tempfile", |
|
| 1828 | + | "thiserror 2.0.21", |
|
| 1829 | + | "time", |
|
| 1830 | + | "tokio", |
|
| 1831 | + | "toml", |
|
| 1832 | + | "tower", |
|
| 1833 | + | "tower-http", |
|
| 1834 | + | "tracing", |
|
| 1835 | + | "tracing-subscriber", |
|
| 1836 | + | ] |
|
| 1837 | + | ||
| 1838 | + | [[package]] |
|
| 1839 | + | name = "parking" |
|
| 1840 | + | version = "2.2.1" |
|
| 1841 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1842 | + | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" |
|
| 1843 | + | ||
| 1844 | + | [[package]] |
|
| 1845 | + | name = "parking_lot" |
|
| 1846 | + | version = "0.12.5" |
|
| 1847 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1848 | + | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" |
|
| 1849 | + | dependencies = [ |
|
| 1850 | + | "lock_api", |
|
| 1851 | + | "parking_lot_core", |
|
| 1852 | + | ] |
|
| 1853 | + | ||
| 1854 | + | [[package]] |
|
| 1855 | + | name = "parking_lot_core" |
|
| 1856 | + | version = "0.9.12" |
|
| 1857 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1858 | + | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" |
|
| 1859 | + | dependencies = [ |
|
| 1860 | + | "cfg-if", |
|
| 1861 | + | "libc", |
|
| 1862 | + | "redox_syscall 0.5.18", |
|
| 1863 | + | "smallvec", |
|
| 1864 | + | "windows-link", |
|
| 1865 | + | ] |
|
| 1866 | + | ||
| 1867 | + | [[package]] |
|
| 1868 | + | name = "pem" |
|
| 1869 | + | version = "4.0.0" |
|
| 1870 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1871 | + | checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" |
|
| 1872 | + | dependencies = [ |
|
| 1873 | + | "base64 0.23.1", |
|
| 1874 | + | "serde_core", |
|
| 1875 | + | ] |
|
| 1876 | + | ||
| 1877 | + | [[package]] |
|
| 1878 | + | name = "pem-rfc7468" |
|
| 1879 | + | version = "0.7.0" |
|
| 1880 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1881 | + | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" |
|
| 1882 | + | dependencies = [ |
|
| 1883 | + | "base64ct", |
|
| 1884 | + | ] |
|
| 1885 | + | ||
| 1886 | + | [[package]] |
|
| 1887 | + | name = "percent-encoding" |
|
| 1888 | + | version = "2.3.2" |
|
| 1889 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1890 | + | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" |
|
| 1891 | + | ||
| 1892 | + | [[package]] |
|
| 1893 | + | name = "phf" |
|
| 1894 | + | version = "0.14.0" |
|
| 1895 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1896 | + | checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" |
|
| 1897 | + | dependencies = [ |
|
| 1898 | + | "phf_shared", |
|
| 1899 | + | "serde", |
|
| 1900 | + | ] |
|
| 1901 | + | ||
| 1902 | + | [[package]] |
|
| 1903 | + | name = "phf_codegen" |
|
| 1904 | + | version = "0.14.0" |
|
| 1905 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1906 | + | checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" |
|
| 1907 | + | dependencies = [ |
|
| 1908 | + | "phf_generator", |
|
| 1909 | + | "phf_shared", |
|
| 1910 | + | ] |
|
| 1911 | + | ||
| 1912 | + | [[package]] |
|
| 1913 | + | name = "phf_generator" |
|
| 1914 | + | version = "0.14.0" |
|
| 1915 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1916 | + | checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" |
|
| 1917 | + | dependencies = [ |
|
| 1918 | + | "fastrand", |
|
| 1919 | + | "phf_shared", |
|
| 1920 | + | ] |
|
| 1921 | + | ||
| 1922 | + | [[package]] |
|
| 1923 | + | name = "phf_shared" |
|
| 1924 | + | version = "0.14.0" |
|
| 1925 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1926 | + | checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" |
|
| 1927 | + | dependencies = [ |
|
| 1928 | + | "siphasher", |
|
| 1929 | + | ] |
|
| 1930 | + | ||
| 1931 | + | [[package]] |
|
| 1932 | + | name = "pin-project-lite" |
|
| 1933 | + | version = "0.2.17" |
|
| 1934 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1935 | + | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" |
|
| 1936 | + | ||
| 1937 | + | [[package]] |
|
| 1938 | + | name = "pkcs1" |
|
| 1939 | + | version = "0.7.5" |
|
| 1940 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1941 | + | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" |
|
| 1942 | + | dependencies = [ |
|
| 1943 | + | "der 0.7.10", |
|
| 1944 | + | "pkcs8 0.10.2", |
|
| 1945 | + | "spki 0.7.3", |
|
| 1946 | + | ] |
|
| 1947 | + | ||
| 1948 | + | [[package]] |
|
| 1949 | + | name = "pkcs8" |
|
| 1950 | + | version = "0.10.2" |
|
| 1951 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1952 | + | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" |
|
| 1953 | + | dependencies = [ |
|
| 1954 | + | "der 0.7.10", |
|
| 1955 | + | "spki 0.7.3", |
|
| 1956 | + | ] |
|
| 1957 | + | ||
| 1958 | + | [[package]] |
|
| 1959 | + | name = "pkcs8" |
|
| 1960 | + | version = "0.11.0" |
|
| 1961 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1962 | + | checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" |
|
| 1963 | + | dependencies = [ |
|
| 1964 | + | "der 0.8.2", |
|
| 1965 | + | "spki 0.8.0", |
|
| 1966 | + | ] |
|
| 1967 | + | ||
| 1968 | + | [[package]] |
|
| 1969 | + | name = "pkg-config" |
|
| 1970 | + | version = "0.3.34" |
|
| 1971 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1972 | + | checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" |
|
| 1973 | + | ||
| 1974 | + | [[package]] |
|
| 1975 | + | name = "plain" |
|
| 1976 | + | version = "0.2.3" |
|
| 1977 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1978 | + | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" |
|
| 1979 | + | ||
| 1980 | + | [[package]] |
|
| 1981 | + | name = "plist" |
|
| 1982 | + | version = "1.10.1" |
|
| 1983 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1984 | + | checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" |
|
| 1985 | + | dependencies = [ |
|
| 1986 | + | "base64 0.23.1", |
|
| 1987 | + | "indexmap", |
|
| 1988 | + | "quick-xml", |
|
| 1989 | + | "serde", |
|
| 1990 | + | "time", |
|
| 1991 | + | ] |
|
| 1992 | + | ||
| 1993 | + | [[package]] |
|
| 1994 | + | name = "potential_utf" |
|
| 1995 | + | version = "0.1.6" |
|
| 1996 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1997 | + | checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" |
|
| 1998 | + | dependencies = [ |
|
| 1999 | + | "zerovec", |
|
| 2000 | + | ] |
|
| 2001 | + | ||
| 2002 | + | [[package]] |
|
| 2003 | + | name = "powerfmt" |
|
| 2004 | + | version = "0.2.0" |
|
| 2005 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2006 | + | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" |
|
| 2007 | + | ||
| 2008 | + | [[package]] |
|
| 2009 | + | name = "ppv-lite86" |
|
| 2010 | + | version = "0.2.21" |
|
| 2011 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2012 | + | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" |
|
| 2013 | + | dependencies = [ |
|
| 2014 | + | "zerocopy", |
|
| 2015 | + | ] |
|
| 2016 | + | ||
| 2017 | + | [[package]] |
|
| 2018 | + | name = "precomputed-hash" |
|
| 2019 | + | version = "0.1.1" |
|
| 2020 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2021 | + | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" |
|
| 2022 | + | ||
| 2023 | + | [[package]] |
|
| 2024 | + | name = "proc-macro2" |
|
| 2025 | + | version = "1.0.107" |
|
| 2026 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2027 | + | checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" |
|
| 2028 | + | dependencies = [ |
|
| 2029 | + | "unicode-ident", |
|
| 2030 | + | ] |
|
| 2031 | + | ||
| 2032 | + | [[package]] |
|
| 2033 | + | name = "pulldown-cmark" |
|
| 2034 | + | version = "0.13.4" |
|
| 2035 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2036 | + | checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" |
|
| 2037 | + | dependencies = [ |
|
| 2038 | + | "bitflags", |
|
| 2039 | + | "memchr", |
|
| 2040 | + | "pulldown-cmark-escape", |
|
| 2041 | + | "unicase", |
|
| 2042 | + | ] |
|
| 2043 | + | ||
| 2044 | + | [[package]] |
|
| 2045 | + | name = "pulldown-cmark-escape" |
|
| 2046 | + | version = "0.11.0" |
|
| 2047 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2048 | + | checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" |
|
| 2049 | + | ||
| 2050 | + | [[package]] |
|
| 2051 | + | name = "quick-xml" |
|
| 2052 | + | version = "0.42.0" |
|
| 2053 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2054 | + | checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" |
|
| 2055 | + | dependencies = [ |
|
| 2056 | + | "memchr", |
|
| 2057 | + | ] |
|
| 2058 | + | ||
| 2059 | + | [[package]] |
|
| 2060 | + | name = "quinn" |
|
| 2061 | + | version = "0.11.12" |
|
| 2062 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2063 | + | checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" |
|
| 2064 | + | dependencies = [ |
|
| 2065 | + | "bytes", |
|
| 2066 | + | "cfg_aliases", |
|
| 2067 | + | "pin-project-lite", |
|
| 2068 | + | "quinn-proto", |
|
| 2069 | + | "quinn-udp", |
|
| 2070 | + | "rustc-hash", |
|
| 2071 | + | "rustls", |
|
| 2072 | + | "socket2", |
|
| 2073 | + | "thiserror 2.0.21", |
|
| 2074 | + | "tokio", |
|
| 2075 | + | "tracing", |
|
| 2076 | + | "web-time", |
|
| 2077 | + | ] |
|
| 2078 | + | ||
| 2079 | + | [[package]] |
|
| 2080 | + | name = "quinn-proto" |
|
| 2081 | + | version = "0.11.18" |
|
| 2082 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2083 | + | checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" |
|
| 2084 | + | dependencies = [ |
|
| 2085 | + | "bytes", |
|
| 2086 | + | "getrandom 0.4.3", |
|
| 2087 | + | "lru-slab", |
|
| 2088 | + | "rand 0.10.3", |
|
| 2089 | + | "rand_pcg", |
|
| 2090 | + | "ring", |
|
| 2091 | + | "rustc-hash", |
|
| 2092 | + | "rustls", |
|
| 2093 | + | "rustls-pki-types", |
|
| 2094 | + | "slab", |
|
| 2095 | + | "thiserror 2.0.21", |
|
| 2096 | + | "tinyvec", |
|
| 2097 | + | "tracing", |
|
| 2098 | + | "web-time", |
|
| 2099 | + | ] |
|
| 2100 | + | ||
| 2101 | + | [[package]] |
|
| 2102 | + | name = "quinn-udp" |
|
| 2103 | + | version = "0.5.15" |
|
| 2104 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2105 | + | checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" |
|
| 2106 | + | dependencies = [ |
|
| 2107 | + | "cfg_aliases", |
|
| 2108 | + | "libc", |
|
| 2109 | + | "once_cell", |
|
| 2110 | + | "socket2", |
|
| 2111 | + | "tracing", |
|
| 2112 | + | "windows-sys 0.61.2", |
|
| 2113 | + | ] |
|
| 2114 | + | ||
| 2115 | + | [[package]] |
|
| 2116 | + | name = "quote" |
|
| 2117 | + | version = "1.0.47" |
|
| 2118 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2119 | + | checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" |
|
| 2120 | + | dependencies = [ |
|
| 2121 | + | "proc-macro2", |
|
| 2122 | + | ] |
|
| 2123 | + | ||
| 2124 | + | [[package]] |
|
| 2125 | + | name = "r-efi" |
|
| 2126 | + | version = "6.0.0" |
|
| 2127 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2128 | + | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" |
|
| 2129 | + | ||
| 2130 | + | [[package]] |
|
| 2131 | + | name = "rand" |
|
| 2132 | + | version = "0.8.8" |
|
| 2133 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2134 | + | checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" |
|
| 2135 | + | dependencies = [ |
|
| 2136 | + | "libc", |
|
| 2137 | + | "rand_chacha", |
|
| 2138 | + | "rand_core 0.6.4", |
|
| 2139 | + | ] |
|
| 2140 | + | ||
| 2141 | + | [[package]] |
|
| 2142 | + | name = "rand" |
|
| 2143 | + | version = "0.10.3" |
|
| 2144 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2145 | + | checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" |
|
| 2146 | + | dependencies = [ |
|
| 2147 | + | "chacha20", |
|
| 2148 | + | "getrandom 0.4.3", |
|
| 2149 | + | "rand_core 0.10.1", |
|
| 2150 | + | ] |
|
| 2151 | + | ||
| 2152 | + | [[package]] |
|
| 2153 | + | name = "rand_chacha" |
|
| 2154 | + | version = "0.3.1" |
|
| 2155 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2156 | + | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" |
|
| 2157 | + | dependencies = [ |
|
| 2158 | + | "ppv-lite86", |
|
| 2159 | + | "rand_core 0.6.4", |
|
| 2160 | + | ] |
|
| 2161 | + | ||
| 2162 | + | [[package]] |
|
| 2163 | + | name = "rand_core" |
|
| 2164 | + | version = "0.6.4" |
|
| 2165 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2166 | + | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" |
|
| 2167 | + | dependencies = [ |
|
| 2168 | + | "getrandom 0.2.17", |
|
| 2169 | + | ] |
|
| 2170 | + | ||
| 2171 | + | [[package]] |
|
| 2172 | + | name = "rand_core" |
|
| 2173 | + | version = "0.10.1" |
|
| 2174 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2175 | + | checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" |
|
| 2176 | + | ||
| 2177 | + | [[package]] |
|
| 2178 | + | name = "rand_pcg" |
|
| 2179 | + | version = "0.10.2" |
|
| 2180 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2181 | + | checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" |
|
| 2182 | + | dependencies = [ |
|
| 2183 | + | "rand_core 0.10.1", |
|
| 2184 | + | ] |
|
| 2185 | + | ||
| 2186 | + | [[package]] |
|
| 2187 | + | name = "rcgen" |
|
| 2188 | + | version = "0.14.10" |
|
| 2189 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2190 | + | checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" |
|
| 2191 | + | dependencies = [ |
|
| 2192 | + | "pem", |
|
| 2193 | + | "ring", |
|
| 2194 | + | "rustls-pki-types", |
|
| 2195 | + | "time", |
|
| 2196 | + | "x509-parser", |
|
| 2197 | + | "yasna", |
|
| 2198 | + | ] |
|
| 2199 | + | ||
| 2200 | + | [[package]] |
|
| 2201 | + | name = "redox_syscall" |
|
| 2202 | + | version = "0.5.18" |
|
| 2203 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2204 | + | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" |
|
| 2205 | + | dependencies = [ |
|
| 2206 | + | "bitflags", |
|
| 2207 | + | ] |
|
| 2208 | + | ||
| 2209 | + | [[package]] |
|
| 2210 | + | name = "redox_syscall" |
|
| 2211 | + | version = "0.9.4" |
|
| 2212 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2213 | + | checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" |
|
| 2214 | + | dependencies = [ |
|
| 2215 | + | "bitflags", |
|
| 2216 | + | ] |
|
| 2217 | + | ||
| 2218 | + | [[package]] |
|
| 2219 | + | name = "regex-automata" |
|
| 2220 | + | version = "0.4.18" |
|
| 2221 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2222 | + | checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" |
|
| 2223 | + | dependencies = [ |
|
| 2224 | + | "aho-corasick", |
|
| 2225 | + | "memchr", |
|
| 2226 | + | "regex-syntax", |
|
| 2227 | + | ] |
|
| 2228 | + | ||
| 2229 | + | [[package]] |
|
| 2230 | + | name = "regex-syntax" |
|
| 2231 | + | version = "0.8.11" |
|
| 2232 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2233 | + | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" |
|
| 2234 | + | ||
| 2235 | + | [[package]] |
|
| 2236 | + | name = "reqwest" |
|
| 2237 | + | version = "0.12.28" |
|
| 2238 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2239 | + | checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" |
|
| 2240 | + | dependencies = [ |
|
| 2241 | + | "base64 0.22.1", |
|
| 2242 | + | "bytes", |
|
| 2243 | + | "futures-core", |
|
| 2244 | + | "http", |
|
| 2245 | + | "http-body", |
|
| 2246 | + | "http-body-util", |
|
| 2247 | + | "hyper", |
|
| 2248 | + | "hyper-rustls", |
|
| 2249 | + | "hyper-util", |
|
| 2250 | + | "js-sys", |
|
| 2251 | + | "log", |
|
| 2252 | + | "percent-encoding", |
|
| 2253 | + | "pin-project-lite", |
|
| 2254 | + | "quinn", |
|
| 2255 | + | "rustls", |
|
| 2256 | + | "rustls-pki-types", |
|
| 2257 | + | "serde", |
|
| 2258 | + | "serde_json", |
|
| 2259 | + | "serde_urlencoded", |
|
| 2260 | + | "sync_wrapper", |
|
| 2261 | + | "tokio", |
|
| 2262 | + | "tokio-rustls", |
|
| 2263 | + | "tower", |
|
| 2264 | + | "tower-http", |
|
| 2265 | + | "tower-service", |
|
| 2266 | + | "url", |
|
| 2267 | + | "wasm-bindgen", |
|
| 2268 | + | "wasm-bindgen-futures", |
|
| 2269 | + | "web-sys", |
|
| 2270 | + | "webpki-roots 1.0.9", |
|
| 2271 | + | ] |
|
| 2272 | + | ||
| 2273 | + | [[package]] |
|
| 2274 | + | name = "ring" |
|
| 2275 | + | version = "0.17.14" |
|
| 2276 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2277 | + | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" |
|
| 2278 | + | dependencies = [ |
|
| 2279 | + | "cc", |
|
| 2280 | + | "cfg-if", |
|
| 2281 | + | "getrandom 0.2.17", |
|
| 2282 | + | "libc", |
|
| 2283 | + | "untrusted", |
|
| 2284 | + | "windows-sys 0.52.0", |
|
| 2285 | + | ] |
|
| 2286 | + | ||
| 2287 | + | [[package]] |
|
| 2288 | + | name = "rsa" |
|
| 2289 | + | version = "0.9.10" |
|
| 2290 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2291 | + | checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" |
|
| 2292 | + | dependencies = [ |
|
| 2293 | + | "const-oid 0.9.6", |
|
| 2294 | + | "digest 0.10.7", |
|
| 2295 | + | "num-bigint-dig", |
|
| 2296 | + | "num-integer", |
|
| 2297 | + | "num-traits", |
|
| 2298 | + | "pkcs1", |
|
| 2299 | + | "pkcs8 0.10.2", |
|
| 2300 | + | "rand_core 0.6.4", |
|
| 2301 | + | "signature 2.2.0", |
|
| 2302 | + | "spki 0.7.3", |
|
| 2303 | + | "subtle", |
|
| 2304 | + | "zeroize", |
|
| 2305 | + | ] |
|
| 2306 | + | ||
| 2307 | + | [[package]] |
|
| 2308 | + | name = "rustc-hash" |
|
| 2309 | + | version = "2.1.3" |
|
| 2310 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2311 | + | checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" |
|
| 2312 | + | ||
| 2313 | + | [[package]] |
|
| 2314 | + | name = "rustc_version" |
|
| 2315 | + | version = "0.4.1" |
|
| 2316 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2317 | + | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" |
|
| 2318 | + | dependencies = [ |
|
| 2319 | + | "semver", |
|
| 2320 | + | ] |
|
| 2321 | + | ||
| 2322 | + | [[package]] |
|
| 2323 | + | name = "rusticata-macros" |
|
| 2324 | + | version = "4.1.0" |
|
| 2325 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2326 | + | checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" |
|
| 2327 | + | dependencies = [ |
|
| 2328 | + | "nom", |
|
| 2329 | + | ] |
|
| 2330 | + | ||
| 2331 | + | [[package]] |
|
| 2332 | + | name = "rustix" |
|
| 2333 | + | version = "1.1.5" |
|
| 2334 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2335 | + | checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" |
|
| 2336 | + | dependencies = [ |
|
| 2337 | + | "bitflags", |
|
| 2338 | + | "errno", |
|
| 2339 | + | "libc", |
|
| 2340 | + | "linux-raw-sys", |
|
| 2341 | + | "windows-sys 0.61.2", |
|
| 2342 | + | ] |
|
| 2343 | + | ||
| 2344 | + | [[package]] |
|
| 2345 | + | name = "rustls" |
|
| 2346 | + | version = "0.23.45" |
|
| 2347 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2348 | + | checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" |
|
| 2349 | + | dependencies = [ |
|
| 2350 | + | "log", |
|
| 2351 | + | "once_cell", |
|
| 2352 | + | "ring", |
|
| 2353 | + | "rustls-pki-types", |
|
| 2354 | + | "rustls-webpki", |
|
| 2355 | + | "subtle", |
|
| 2356 | + | "zeroize", |
|
| 2357 | + | ] |
|
| 2358 | + | ||
| 2359 | + | [[package]] |
|
| 2360 | + | name = "rustls-pemfile" |
|
| 2361 | + | version = "2.2.0" |
|
| 2362 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2363 | + | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" |
|
| 2364 | + | dependencies = [ |
|
| 2365 | + | "rustls-pki-types", |
|
| 2366 | + | ] |
|
| 2367 | + | ||
| 2368 | + | [[package]] |
|
| 2369 | + | name = "rustls-pki-types" |
|
| 2370 | + | version = "1.15.1" |
|
| 2371 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2372 | + | checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" |
|
| 2373 | + | dependencies = [ |
|
| 2374 | + | "web-time", |
|
| 2375 | + | "zeroize", |
|
| 2376 | + | ] |
|
| 2377 | + | ||
| 2378 | + | [[package]] |
|
| 2379 | + | name = "rustls-webpki" |
|
| 2380 | + | version = "0.103.15" |
|
| 2381 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2382 | + | checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" |
|
| 2383 | + | dependencies = [ |
|
| 2384 | + | "ring", |
|
| 2385 | + | "rustls-pki-types", |
|
| 2386 | + | "untrusted", |
|
| 2387 | + | ] |
|
| 2388 | + | ||
| 2389 | + | [[package]] |
|
| 2390 | + | name = "rustversion" |
|
| 2391 | + | version = "1.0.23" |
|
| 2392 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2393 | + | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" |
|
| 2394 | + | ||
| 2395 | + | [[package]] |
|
| 2396 | + | name = "ryu" |
|
| 2397 | + | version = "1.0.23" |
|
| 2398 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2399 | + | checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" |
|
| 2400 | + | ||
| 2401 | + | [[package]] |
|
| 2402 | + | name = "same-file" |
|
| 2403 | + | version = "1.0.6" |
|
| 2404 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2405 | + | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" |
|
| 2406 | + | dependencies = [ |
|
| 2407 | + | "winapi-util", |
|
| 2408 | + | ] |
|
| 2409 | + | ||
| 2410 | + | [[package]] |
|
| 2411 | + | name = "scopeguard" |
|
| 2412 | + | version = "1.2.0" |
|
| 2413 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2414 | + | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" |
|
| 2415 | + | ||
| 2416 | + | [[package]] |
|
| 2417 | + | name = "semver" |
|
| 2418 | + | version = "1.0.28" |
|
| 2419 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2420 | + | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" |
|
| 2421 | + | ||
| 2422 | + | [[package]] |
|
| 2423 | + | name = "serde" |
|
| 2424 | + | version = "1.0.229" |
|
| 2425 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2426 | + | checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" |
|
| 2427 | + | dependencies = [ |
|
| 2428 | + | "serde_core", |
|
| 2429 | + | "serde_derive", |
|
| 2430 | + | ] |
|
| 2431 | + | ||
| 2432 | + | [[package]] |
|
| 2433 | + | name = "serde_core" |
|
| 2434 | + | version = "1.0.229" |
|
| 2435 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2436 | + | checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" |
|
| 2437 | + | dependencies = [ |
|
| 2438 | + | "serde_derive", |
|
| 2439 | + | ] |
|
| 2440 | + | ||
| 2441 | + | [[package]] |
|
| 2442 | + | name = "serde_derive" |
|
| 2443 | + | version = "1.0.229" |
|
| 2444 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2445 | + | checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" |
|
| 2446 | + | dependencies = [ |
|
| 2447 | + | "proc-macro2", |
|
| 2448 | + | "quote", |
|
| 2449 | + | "syn 3.0.6", |
|
| 2450 | + | ] |
|
| 2451 | + | ||
| 2452 | + | [[package]] |
|
| 2453 | + | name = "serde_json" |
|
| 2454 | + | version = "1.0.151" |
|
| 2455 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2456 | + | checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" |
|
| 2457 | + | dependencies = [ |
|
| 2458 | + | "itoa", |
|
| 2459 | + | "memchr", |
|
| 2460 | + | "serde", |
|
| 2461 | + | "serde_core", |
|
| 2462 | + | "zmij", |
|
| 2463 | + | ] |
|
| 2464 | + | ||
| 2465 | + | [[package]] |
|
| 2466 | + | name = "serde_path_to_error" |
|
| 2467 | + | version = "0.1.20" |
|
| 2468 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2469 | + | checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" |
|
| 2470 | + | dependencies = [ |
|
| 2471 | + | "itoa", |
|
| 2472 | + | "serde", |
|
| 2473 | + | "serde_core", |
|
| 2474 | + | ] |
|
| 2475 | + | ||
| 2476 | + | [[package]] |
|
| 2477 | + | name = "serde_spanned" |
|
| 2478 | + | version = "0.6.9" |
|
| 2479 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2480 | + | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" |
|
| 2481 | + | dependencies = [ |
|
| 2482 | + | "serde", |
|
| 2483 | + | ] |
|
| 2484 | + | ||
| 2485 | + | [[package]] |
|
| 2486 | + | name = "serde_urlencoded" |
|
| 2487 | + | version = "0.7.1" |
|
| 2488 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2489 | + | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" |
|
| 2490 | + | dependencies = [ |
|
| 2491 | + | "form_urlencoded", |
|
| 2492 | + | "itoa", |
|
| 2493 | + | "ryu", |
|
| 2494 | + | "serde", |
|
| 2495 | + | ] |
|
| 2496 | + | ||
| 2497 | + | [[package]] |
|
| 2498 | + | name = "sha1" |
|
| 2499 | + | version = "0.10.7" |
|
| 2500 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2501 | + | checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" |
|
| 2502 | + | dependencies = [ |
|
| 2503 | + | "cfg-if", |
|
| 2504 | + | "cpufeatures 0.2.17", |
|
| 2505 | + | "digest 0.10.7", |
|
| 2506 | + | ] |
|
| 2507 | + | ||
| 2508 | + | [[package]] |
|
| 2509 | + | name = "sha2" |
|
| 2510 | + | version = "0.10.9" |
|
| 2511 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2512 | + | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" |
|
| 2513 | + | dependencies = [ |
|
| 2514 | + | "cfg-if", |
|
| 2515 | + | "cpufeatures 0.2.17", |
|
| 2516 | + | "digest 0.10.7", |
|
| 2517 | + | ] |
|
| 2518 | + | ||
| 2519 | + | [[package]] |
|
| 2520 | + | name = "sha2" |
|
| 2521 | + | version = "0.11.0" |
|
| 2522 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2523 | + | checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" |
|
| 2524 | + | dependencies = [ |
|
| 2525 | + | "cfg-if", |
|
| 2526 | + | "cpufeatures 0.3.1", |
|
| 2527 | + | "digest 0.11.3", |
|
| 2528 | + | ] |
|
| 2529 | + | ||
| 2530 | + | [[package]] |
|
| 2531 | + | name = "shake" |
|
| 2532 | + | version = "0.1.0" |
|
| 2533 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2534 | + | checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" |
|
| 2535 | + | dependencies = [ |
|
| 2536 | + | "digest 0.11.3", |
|
| 2537 | + | "keccak", |
|
| 2538 | + | "sponge-cursor", |
|
| 2539 | + | ] |
|
| 2540 | + | ||
| 2541 | + | [[package]] |
|
| 2542 | + | name = "sharded-slab" |
|
| 2543 | + | version = "0.1.7" |
|
| 2544 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2545 | + | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" |
|
| 2546 | + | dependencies = [ |
|
| 2547 | + | "lazy_static", |
|
| 2548 | + | ] |
|
| 2549 | + | ||
| 2550 | + | [[package]] |
|
| 2551 | + | name = "shlex" |
|
| 2552 | + | version = "2.0.1" |
|
| 2553 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2554 | + | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" |
|
| 2555 | + | ||
| 2556 | + | [[package]] |
|
| 2557 | + | name = "signal-hook-registry" |
|
| 2558 | + | version = "1.4.8" |
|
| 2559 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2560 | + | checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" |
|
| 2561 | + | dependencies = [ |
|
| 2562 | + | "errno", |
|
| 2563 | + | "libc", |
|
| 2564 | + | ] |
|
| 2565 | + | ||
| 2566 | + | [[package]] |
|
| 2567 | + | name = "signature" |
|
| 2568 | + | version = "2.2.0" |
|
| 2569 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2570 | + | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" |
|
| 2571 | + | dependencies = [ |
|
| 2572 | + | "digest 0.10.7", |
|
| 2573 | + | "rand_core 0.6.4", |
|
| 2574 | + | ] |
|
| 2575 | + | ||
| 2576 | + | [[package]] |
|
| 2577 | + | name = "signature" |
|
| 2578 | + | version = "3.0.0" |
|
| 2579 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2580 | + | checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" |
|
| 2581 | + | dependencies = [ |
|
| 2582 | + | "digest 0.11.3", |
|
| 2583 | + | "rand_core 0.10.1", |
|
| 2584 | + | ] |
|
| 2585 | + | ||
| 2586 | + | [[package]] |
|
| 2587 | + | name = "simd-adler32" |
|
| 2588 | + | version = "0.3.10" |
|
| 2589 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2590 | + | checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" |
|
| 2591 | + | ||
| 2592 | + | [[package]] |
|
| 2593 | + | name = "similar" |
|
| 2594 | + | version = "2.7.0" |
|
| 2595 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2596 | + | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" |
|
| 2597 | + | ||
| 2598 | + | [[package]] |
|
| 2599 | + | name = "siphasher" |
|
| 2600 | + | version = "1.0.4" |
|
| 2601 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2602 | + | checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" |
|
| 2603 | + | ||
| 2604 | + | [[package]] |
|
| 2605 | + | name = "slab" |
|
| 2606 | + | version = "0.4.12" |
|
| 2607 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2608 | + | checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" |
|
| 2609 | + | ||
| 2610 | + | [[package]] |
|
| 2611 | + | name = "smallvec" |
|
| 2612 | + | version = "1.16.2" |
|
| 2613 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2614 | + | checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" |
|
| 2615 | + | dependencies = [ |
|
| 2616 | + | "serde", |
|
| 2617 | + | ] |
|
| 2618 | + | ||
| 2619 | + | [[package]] |
|
| 2620 | + | name = "socket2" |
|
| 2621 | + | version = "0.6.5" |
|
| 2622 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2623 | + | checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" |
|
| 2624 | + | dependencies = [ |
|
| 2625 | + | "libc", |
|
| 2626 | + | "windows-sys 0.61.2", |
|
| 2627 | + | ] |
|
| 2628 | + | ||
| 2629 | + | [[package]] |
|
| 2630 | + | name = "spin" |
|
| 2631 | + | version = "0.9.9" |
|
| 2632 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2633 | + | checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" |
|
| 2634 | + | dependencies = [ |
|
| 2635 | + | "lock_api", |
|
| 2636 | + | ] |
|
| 2637 | + | ||
| 2638 | + | [[package]] |
|
| 2639 | + | name = "spki" |
|
| 2640 | + | version = "0.7.3" |
|
| 2641 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2642 | + | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" |
|
| 2643 | + | dependencies = [ |
|
| 2644 | + | "base64ct", |
|
| 2645 | + | "der 0.7.10", |
|
| 2646 | + | ] |
|
| 2647 | + | ||
| 2648 | + | [[package]] |
|
| 2649 | + | name = "spki" |
|
| 2650 | + | version = "0.8.0" |
|
| 2651 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2652 | + | checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" |
|
| 2653 | + | dependencies = [ |
|
| 2654 | + | "base64ct", |
|
| 2655 | + | "der 0.8.2", |
|
| 2656 | + | ] |
|
| 2657 | + | ||
| 2658 | + | [[package]] |
|
| 2659 | + | name = "sponge-cursor" |
|
| 2660 | + | version = "0.1.0" |
|
| 2661 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2662 | + | checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" |
|
| 2663 | + | ||
| 2664 | + | [[package]] |
|
| 2665 | + | name = "sqlx" |
|
| 2666 | + | version = "0.8.6" |
|
| 2667 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2668 | + | checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" |
|
| 2669 | + | dependencies = [ |
|
| 2670 | + | "sqlx-core", |
|
| 2671 | + | "sqlx-macros", |
|
| 2672 | + | "sqlx-mysql", |
|
| 2673 | + | "sqlx-postgres", |
|
| 2674 | + | "sqlx-sqlite", |
|
| 2675 | + | ] |
|
| 2676 | + | ||
| 2677 | + | [[package]] |
|
| 2678 | + | name = "sqlx-core" |
|
| 2679 | + | version = "0.8.6" |
|
| 2680 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2681 | + | checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" |
|
| 2682 | + | dependencies = [ |
|
| 2683 | + | "base64 0.22.1", |
|
| 2684 | + | "bytes", |
|
| 2685 | + | "crc", |
|
| 2686 | + | "crossbeam-queue", |
|
| 2687 | + | "either", |
|
| 2688 | + | "event-listener", |
|
| 2689 | + | "futures-core", |
|
| 2690 | + | "futures-intrusive", |
|
| 2691 | + | "futures-io", |
|
| 2692 | + | "futures-util", |
|
| 2693 | + | "hashbrown 0.15.5", |
|
| 2694 | + | "hashlink", |
|
| 2695 | + | "indexmap", |
|
| 2696 | + | "log", |
|
| 2697 | + | "memchr", |
|
| 2698 | + | "once_cell", |
|
| 2699 | + | "percent-encoding", |
|
| 2700 | + | "serde", |
|
| 2701 | + | "serde_json", |
|
| 2702 | + | "sha2 0.10.9", |
|
| 2703 | + | "smallvec", |
|
| 2704 | + | "thiserror 2.0.21", |
|
| 2705 | + | "tokio", |
|
| 2706 | + | "tokio-stream", |
|
| 2707 | + | "tracing", |
|
| 2708 | + | "url", |
|
| 2709 | + | ] |
|
| 2710 | + | ||
| 2711 | + | [[package]] |
|
| 2712 | + | name = "sqlx-macros" |
|
| 2713 | + | version = "0.8.6" |
|
| 2714 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2715 | + | checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" |
|
| 2716 | + | dependencies = [ |
|
| 2717 | + | "proc-macro2", |
|
| 2718 | + | "quote", |
|
| 2719 | + | "sqlx-core", |
|
| 2720 | + | "sqlx-macros-core", |
|
| 2721 | + | "syn 2.0.119", |
|
| 2722 | + | ] |
|
| 2723 | + | ||
| 2724 | + | [[package]] |
|
| 2725 | + | name = "sqlx-macros-core" |
|
| 2726 | + | version = "0.8.6" |
|
| 2727 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2728 | + | checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" |
|
| 2729 | + | dependencies = [ |
|
| 2730 | + | "dotenvy", |
|
| 2731 | + | "either", |
|
| 2732 | + | "heck", |
|
| 2733 | + | "hex", |
|
| 2734 | + | "once_cell", |
|
| 2735 | + | "proc-macro2", |
|
| 2736 | + | "quote", |
|
| 2737 | + | "serde", |
|
| 2738 | + | "serde_json", |
|
| 2739 | + | "sha2 0.10.9", |
|
| 2740 | + | "sqlx-core", |
|
| 2741 | + | "sqlx-sqlite", |
|
| 2742 | + | "syn 2.0.119", |
|
| 2743 | + | "tokio", |
|
| 2744 | + | "url", |
|
| 2745 | + | ] |
|
| 2746 | + | ||
| 2747 | + | [[package]] |
|
| 2748 | + | name = "sqlx-mysql" |
|
| 2749 | + | version = "0.8.6" |
|
| 2750 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2751 | + | checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" |
|
| 2752 | + | dependencies = [ |
|
| 2753 | + | "atoi", |
|
| 2754 | + | "base64 0.22.1", |
|
| 2755 | + | "bitflags", |
|
| 2756 | + | "byteorder", |
|
| 2757 | + | "bytes", |
|
| 2758 | + | "crc", |
|
| 2759 | + | "digest 0.10.7", |
|
| 2760 | + | "dotenvy", |
|
| 2761 | + | "either", |
|
| 2762 | + | "futures-channel", |
|
| 2763 | + | "futures-core", |
|
| 2764 | + | "futures-io", |
|
| 2765 | + | "futures-util", |
|
| 2766 | + | "generic-array", |
|
| 2767 | + | "hex", |
|
| 2768 | + | "hkdf", |
|
| 2769 | + | "hmac", |
|
| 2770 | + | "itoa", |
|
| 2771 | + | "log", |
|
| 2772 | + | "md-5", |
|
| 2773 | + | "memchr", |
|
| 2774 | + | "once_cell", |
|
| 2775 | + | "percent-encoding", |
|
| 2776 | + | "rand 0.8.8", |
|
| 2777 | + | "rsa", |
|
| 2778 | + | "sha1", |
|
| 2779 | + | "sha2 0.10.9", |
|
| 2780 | + | "smallvec", |
|
| 2781 | + | "sqlx-core", |
|
| 2782 | + | "stringprep", |
|
| 2783 | + | "thiserror 2.0.21", |
|
| 2784 | + | "tracing", |
|
| 2785 | + | "whoami", |
|
| 2786 | + | ] |
|
| 2787 | + | ||
| 2788 | + | [[package]] |
|
| 2789 | + | name = "sqlx-postgres" |
|
| 2790 | + | version = "0.8.6" |
|
| 2791 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2792 | + | checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" |
|
| 2793 | + | dependencies = [ |
|
| 2794 | + | "atoi", |
|
| 2795 | + | "base64 0.22.1", |
|
| 2796 | + | "bitflags", |
|
| 2797 | + | "byteorder", |
|
| 2798 | + | "crc", |
|
| 2799 | + | "dotenvy", |
|
| 2800 | + | "etcetera", |
|
| 2801 | + | "futures-channel", |
|
| 2802 | + | "futures-core", |
|
| 2803 | + | "futures-util", |
|
| 2804 | + | "hex", |
|
| 2805 | + | "hkdf", |
|
| 2806 | + | "hmac", |
|
| 2807 | + | "home", |
|
| 2808 | + | "itoa", |
|
| 2809 | + | "log", |
|
| 2810 | + | "md-5", |
|
| 2811 | + | "memchr", |
|
| 2812 | + | "once_cell", |
|
| 2813 | + | "rand 0.8.8", |
|
| 2814 | + | "serde", |
|
| 2815 | + | "serde_json", |
|
| 2816 | + | "sha2 0.10.9", |
|
| 2817 | + | "smallvec", |
|
| 2818 | + | "sqlx-core", |
|
| 2819 | + | "stringprep", |
|
| 2820 | + | "thiserror 2.0.21", |
|
| 2821 | + | "tracing", |
|
| 2822 | + | "whoami", |
|
| 2823 | + | ] |
|
| 2824 | + | ||
| 2825 | + | [[package]] |
|
| 2826 | + | name = "sqlx-sqlite" |
|
| 2827 | + | version = "0.8.6" |
|
| 2828 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2829 | + | checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" |
|
| 2830 | + | dependencies = [ |
|
| 2831 | + | "atoi", |
|
| 2832 | + | "flume", |
|
| 2833 | + | "futures-channel", |
|
| 2834 | + | "futures-core", |
|
| 2835 | + | "futures-executor", |
|
| 2836 | + | "futures-intrusive", |
|
| 2837 | + | "futures-util", |
|
| 2838 | + | "libsqlite3-sys", |
|
| 2839 | + | "log", |
|
| 2840 | + | "percent-encoding", |
|
| 2841 | + | "serde", |
|
| 2842 | + | "serde_urlencoded", |
|
| 2843 | + | "sqlx-core", |
|
| 2844 | + | "thiserror 2.0.21", |
|
| 2845 | + | "tracing", |
|
| 2846 | + | "url", |
|
| 2847 | + | ] |
|
| 2848 | + | ||
| 2849 | + | [[package]] |
|
| 2850 | + | name = "stable_deref_trait" |
|
| 2851 | + | version = "1.2.1" |
|
| 2852 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2853 | + | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" |
|
| 2854 | + | ||
| 2855 | + | [[package]] |
|
| 2856 | + | name = "string_cache" |
|
| 2857 | + | version = "0.11.0" |
|
| 2858 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2859 | + | checksum = "ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6" |
|
| 2860 | + | dependencies = [ |
|
| 2861 | + | "new_debug_unreachable", |
|
| 2862 | + | "parking_lot", |
|
| 2863 | + | "phf_shared", |
|
| 2864 | + | "precomputed-hash", |
|
| 2865 | + | ] |
|
| 2866 | + | ||
| 2867 | + | [[package]] |
|
| 2868 | + | name = "string_cache_codegen" |
|
| 2869 | + | version = "0.11.2" |
|
| 2870 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2871 | + | checksum = "928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc" |
|
| 2872 | + | dependencies = [ |
|
| 2873 | + | "phf_generator", |
|
| 2874 | + | "phf_shared", |
|
| 2875 | + | "proc-macro2", |
|
| 2876 | + | "quote", |
|
| 2877 | + | ] |
|
| 2878 | + | ||
| 2879 | + | [[package]] |
|
| 2880 | + | name = "stringprep" |
|
| 2881 | + | version = "0.1.5" |
|
| 2882 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2883 | + | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" |
|
| 2884 | + | dependencies = [ |
|
| 2885 | + | "unicode-bidi", |
|
| 2886 | + | "unicode-normalization", |
|
| 2887 | + | "unicode-properties", |
|
| 2888 | + | ] |
|
| 2889 | + | ||
| 2890 | + | [[package]] |
|
| 2891 | + | name = "strsim" |
|
| 2892 | + | version = "0.11.1" |
|
| 2893 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2894 | + | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" |
|
| 2895 | + | ||
| 2896 | + | [[package]] |
|
| 2897 | + | name = "subtle" |
|
| 2898 | + | version = "2.6.1" |
|
| 2899 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2900 | + | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" |
|
| 2901 | + | ||
| 2902 | + | [[package]] |
|
| 2903 | + | name = "syn" |
|
| 2904 | + | version = "2.0.119" |
|
| 2905 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2906 | + | checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" |
|
| 2907 | + | dependencies = [ |
|
| 2908 | + | "proc-macro2", |
|
| 2909 | + | "quote", |
|
| 2910 | + | "unicode-ident", |
|
| 2911 | + | ] |
|
| 2912 | + | ||
| 2913 | + | [[package]] |
|
| 2914 | + | name = "syn" |
|
| 2915 | + | version = "3.0.6" |
|
| 2916 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2917 | + | checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" |
|
| 2918 | + | dependencies = [ |
|
| 2919 | + | "proc-macro2", |
|
| 2920 | + | "quote", |
|
| 2921 | + | "unicode-ident", |
|
| 2922 | + | ] |
|
| 2923 | + | ||
| 2924 | + | [[package]] |
|
| 2925 | + | name = "sync_wrapper" |
|
| 2926 | + | version = "1.0.2" |
|
| 2927 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2928 | + | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" |
|
| 2929 | + | dependencies = [ |
|
| 2930 | + | "futures-core", |
|
| 2931 | + | ] |
|
| 2932 | + | ||
| 2933 | + | [[package]] |
|
| 2934 | + | name = "synstructure" |
|
| 2935 | + | version = "0.13.2" |
|
| 2936 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2937 | + | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" |
|
| 2938 | + | dependencies = [ |
|
| 2939 | + | "proc-macro2", |
|
| 2940 | + | "quote", |
|
| 2941 | + | "syn 2.0.119", |
|
| 2942 | + | ] |
|
| 2943 | + | ||
| 2944 | + | [[package]] |
|
| 2945 | + | name = "synstructure" |
|
| 2946 | + | version = "0.14.0" |
|
| 2947 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2948 | + | checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" |
|
| 2949 | + | dependencies = [ |
|
| 2950 | + | "proc-macro2", |
|
| 2951 | + | "quote", |
|
| 2952 | + | "syn 3.0.6", |
|
| 2953 | + | ] |
|
| 2954 | + | ||
| 2955 | + | [[package]] |
|
| 2956 | + | name = "syntect" |
|
| 2957 | + | version = "5.3.0" |
|
| 2958 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2959 | + | checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" |
|
| 2960 | + | dependencies = [ |
|
| 2961 | + | "bincode", |
|
| 2962 | + | "fancy-regex", |
|
| 2963 | + | "flate2", |
|
| 2964 | + | "fnv", |
|
| 2965 | + | "once_cell", |
|
| 2966 | + | "plist", |
|
| 2967 | + | "regex-syntax", |
|
| 2968 | + | "serde", |
|
| 2969 | + | "serde_derive", |
|
| 2970 | + | "serde_json", |
|
| 2971 | + | "thiserror 2.0.21", |
|
| 2972 | + | "walkdir", |
|
| 2973 | + | "yaml-rust", |
|
| 2974 | + | ] |
|
| 2975 | + | ||
| 2976 | + | [[package]] |
|
| 2977 | + | name = "tempfile" |
|
| 2978 | + | version = "3.27.0" |
|
| 2979 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2980 | + | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" |
|
| 2981 | + | dependencies = [ |
|
| 2982 | + | "fastrand", |
|
| 2983 | + | "getrandom 0.4.3", |
|
| 2984 | + | "once_cell", |
|
| 2985 | + | "rustix", |
|
| 2986 | + | "windows-sys 0.61.2", |
|
| 2987 | + | ] |
|
| 2988 | + | ||
| 2989 | + | [[package]] |
|
| 2990 | + | name = "tendril" |
|
| 2991 | + | version = "0.5.1" |
|
| 2992 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2993 | + | checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" |
|
| 2994 | + | dependencies = [ |
|
| 2995 | + | "new_debug_unreachable", |
|
| 2996 | + | ] |
|
| 2997 | + | ||
| 2998 | + | [[package]] |
|
| 2999 | + | name = "thiserror" |
|
| 3000 | + | version = "1.0.69" |
|
| 3001 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3002 | + | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" |
|
| 3003 | + | dependencies = [ |
|
| 3004 | + | "thiserror-impl 1.0.69", |
|
| 3005 | + | ] |
|
| 3006 | + | ||
| 3007 | + | [[package]] |
|
| 3008 | + | name = "thiserror" |
|
| 3009 | + | version = "2.0.21" |
|
| 3010 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3011 | + | checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" |
|
| 3012 | + | dependencies = [ |
|
| 3013 | + | "thiserror-impl 2.0.21", |
|
| 3014 | + | ] |
|
| 3015 | + | ||
| 3016 | + | [[package]] |
|
| 3017 | + | name = "thiserror-impl" |
|
| 3018 | + | version = "1.0.69" |
|
| 3019 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3020 | + | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" |
|
| 3021 | + | dependencies = [ |
|
| 3022 | + | "proc-macro2", |
|
| 3023 | + | "quote", |
|
| 3024 | + | "syn 2.0.119", |
|
| 3025 | + | ] |
|
| 3026 | + | ||
| 3027 | + | [[package]] |
|
| 3028 | + | name = "thiserror-impl" |
|
| 3029 | + | version = "2.0.21" |
|
| 3030 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3031 | + | checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" |
|
| 3032 | + | dependencies = [ |
|
| 3033 | + | "proc-macro2", |
|
| 3034 | + | "quote", |
|
| 3035 | + | "syn 3.0.6", |
|
| 3036 | + | ] |
|
| 3037 | + | ||
| 3038 | + | [[package]] |
|
| 3039 | + | name = "thread_local" |
|
| 3040 | + | version = "1.1.10" |
|
| 3041 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3042 | + | checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" |
|
| 3043 | + | dependencies = [ |
|
| 3044 | + | "cfg-if", |
|
| 3045 | + | ] |
|
| 3046 | + | ||
| 3047 | + | [[package]] |
|
| 3048 | + | name = "time" |
|
| 3049 | + | version = "0.3.55" |
|
| 3050 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3051 | + | checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" |
|
| 3052 | + | dependencies = [ |
|
| 3053 | + | "deranged", |
|
| 3054 | + | "num-conv", |
|
| 3055 | + | "powerfmt", |
|
| 3056 | + | "serde_core", |
|
| 3057 | + | "time-core", |
|
| 3058 | + | "time-macros", |
|
| 3059 | + | ] |
|
| 3060 | + | ||
| 3061 | + | [[package]] |
|
| 3062 | + | name = "time-core" |
|
| 3063 | + | version = "0.1.9" |
|
| 3064 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3065 | + | checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" |
|
| 3066 | + | ||
| 3067 | + | [[package]] |
|
| 3068 | + | name = "time-macros" |
|
| 3069 | + | version = "0.2.32" |
|
| 3070 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3071 | + | checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" |
|
| 3072 | + | dependencies = [ |
|
| 3073 | + | "num-conv", |
|
| 3074 | + | "time-core", |
|
| 3075 | + | ] |
|
| 3076 | + | ||
| 3077 | + | [[package]] |
|
| 3078 | + | name = "tinystr" |
|
| 3079 | + | version = "0.8.4" |
|
| 3080 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3081 | + | checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" |
|
| 3082 | + | dependencies = [ |
|
| 3083 | + | "displaydoc", |
|
| 3084 | + | "zerovec", |
|
| 3085 | + | ] |
|
| 3086 | + | ||
| 3087 | + | [[package]] |
|
| 3088 | + | name = "tinyvec" |
|
| 3089 | + | version = "1.13.3" |
|
| 3090 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3091 | + | checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" |
|
| 3092 | + | ||
| 3093 | + | [[package]] |
|
| 3094 | + | name = "tokio" |
|
| 3095 | + | version = "1.53.1" |
|
| 3096 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3097 | + | checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" |
|
| 3098 | + | dependencies = [ |
|
| 3099 | + | "bytes", |
|
| 3100 | + | "libc", |
|
| 3101 | + | "mio", |
|
| 3102 | + | "pin-project-lite", |
|
| 3103 | + | "signal-hook-registry", |
|
| 3104 | + | "socket2", |
|
| 3105 | + | "tokio-macros", |
|
| 3106 | + | "windows-sys 0.61.2", |
|
| 3107 | + | ] |
|
| 3108 | + | ||
| 3109 | + | [[package]] |
|
| 3110 | + | name = "tokio-macros" |
|
| 3111 | + | version = "2.7.2" |
|
| 3112 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3113 | + | checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" |
|
| 3114 | + | dependencies = [ |
|
| 3115 | + | "proc-macro2", |
|
| 3116 | + | "quote", |
|
| 3117 | + | "syn 3.0.6", |
|
| 3118 | + | ] |
|
| 3119 | + | ||
| 3120 | + | [[package]] |
|
| 3121 | + | name = "tokio-rustls" |
|
| 3122 | + | version = "0.26.5" |
|
| 3123 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3124 | + | checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" |
|
| 3125 | + | dependencies = [ |
|
| 3126 | + | "rustls", |
|
| 3127 | + | "tokio", |
|
| 3128 | + | ] |
|
| 3129 | + | ||
| 3130 | + | [[package]] |
|
| 3131 | + | name = "tokio-stream" |
|
| 3132 | + | version = "0.1.19" |
|
| 3133 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3134 | + | checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" |
|
| 3135 | + | dependencies = [ |
|
| 3136 | + | "futures-core", |
|
| 3137 | + | "pin-project-lite", |
|
| 3138 | + | "tokio", |
|
| 3139 | + | ] |
|
| 3140 | + | ||
| 3141 | + | [[package]] |
|
| 3142 | + | name = "tokio-util" |
|
| 3143 | + | version = "0.7.19" |
|
| 3144 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3145 | + | checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" |
|
| 3146 | + | dependencies = [ |
|
| 3147 | + | "bytes", |
|
| 3148 | + | "futures-core", |
|
| 3149 | + | "futures-sink", |
|
| 3150 | + | "libc", |
|
| 3151 | + | "pin-project-lite", |
|
| 3152 | + | "tokio", |
|
| 3153 | + | ] |
|
| 3154 | + | ||
| 3155 | + | [[package]] |
|
| 3156 | + | name = "toml" |
|
| 3157 | + | version = "0.8.23" |
|
| 3158 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3159 | + | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" |
|
| 3160 | + | dependencies = [ |
|
| 3161 | + | "serde", |
|
| 3162 | + | "serde_spanned", |
|
| 3163 | + | "toml_datetime", |
|
| 3164 | + | "toml_edit", |
|
| 3165 | + | ] |
|
| 3166 | + | ||
| 3167 | + | [[package]] |
|
| 3168 | + | name = "toml_datetime" |
|
| 3169 | + | version = "0.6.11" |
|
| 3170 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3171 | + | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" |
|
| 3172 | + | dependencies = [ |
|
| 3173 | + | "serde", |
|
| 3174 | + | ] |
|
| 3175 | + | ||
| 3176 | + | [[package]] |
|
| 3177 | + | name = "toml_edit" |
|
| 3178 | + | version = "0.22.27" |
|
| 3179 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3180 | + | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" |
|
| 3181 | + | dependencies = [ |
|
| 3182 | + | "indexmap", |
|
| 3183 | + | "serde", |
|
| 3184 | + | "serde_spanned", |
|
| 3185 | + | "toml_datetime", |
|
| 3186 | + | "toml_write", |
|
| 3187 | + | "winnow", |
|
| 3188 | + | ] |
|
| 3189 | + | ||
| 3190 | + | [[package]] |
|
| 3191 | + | name = "toml_write" |
|
| 3192 | + | version = "0.1.2" |
|
| 3193 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3194 | + | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" |
|
| 3195 | + | ||
| 3196 | + | [[package]] |
|
| 3197 | + | name = "tower" |
|
| 3198 | + | version = "0.5.3" |
|
| 3199 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3200 | + | checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" |
|
| 3201 | + | dependencies = [ |
|
| 3202 | + | "futures-core", |
|
| 3203 | + | "futures-util", |
|
| 3204 | + | "pin-project-lite", |
|
| 3205 | + | "sync_wrapper", |
|
| 3206 | + | "tokio", |
|
| 3207 | + | "tower-layer", |
|
| 3208 | + | "tower-service", |
|
| 3209 | + | "tracing", |
|
| 3210 | + | ] |
|
| 3211 | + | ||
| 3212 | + | [[package]] |
|
| 3213 | + | name = "tower-http" |
|
| 3214 | + | version = "0.6.11" |
|
| 3215 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3216 | + | checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" |
|
| 3217 | + | dependencies = [ |
|
| 3218 | + | "async-compression", |
|
| 3219 | + | "bitflags", |
|
| 3220 | + | "bytes", |
|
| 3221 | + | "futures-core", |
|
| 3222 | + | "futures-util", |
|
| 3223 | + | "http", |
|
| 3224 | + | "http-body", |
|
| 3225 | + | "http-body-util", |
|
| 3226 | + | "pin-project-lite", |
|
| 3227 | + | "tokio", |
|
| 3228 | + | "tokio-util", |
|
| 3229 | + | "tower", |
|
| 3230 | + | "tower-layer", |
|
| 3231 | + | "tower-service", |
|
| 3232 | + | "tracing", |
|
| 3233 | + | "url", |
|
| 3234 | + | ] |
|
| 3235 | + | ||
| 3236 | + | [[package]] |
|
| 3237 | + | name = "tower-layer" |
|
| 3238 | + | version = "0.3.3" |
|
| 3239 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3240 | + | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" |
|
| 3241 | + | ||
| 3242 | + | [[package]] |
|
| 3243 | + | name = "tower-service" |
|
| 3244 | + | version = "0.3.3" |
|
| 3245 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3246 | + | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" |
|
| 3247 | + | ||
| 3248 | + | [[package]] |
|
| 3249 | + | name = "tracing" |
|
| 3250 | + | version = "0.1.44" |
|
| 3251 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3252 | + | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" |
|
| 3253 | + | dependencies = [ |
|
| 3254 | + | "log", |
|
| 3255 | + | "pin-project-lite", |
|
| 3256 | + | "tracing-attributes", |
|
| 3257 | + | "tracing-core", |
|
| 3258 | + | ] |
|
| 3259 | + | ||
| 3260 | + | [[package]] |
|
| 3261 | + | name = "tracing-attributes" |
|
| 3262 | + | version = "0.1.31" |
|
| 3263 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3264 | + | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" |
|
| 3265 | + | dependencies = [ |
|
| 3266 | + | "proc-macro2", |
|
| 3267 | + | "quote", |
|
| 3268 | + | "syn 2.0.119", |
|
| 3269 | + | ] |
|
| 3270 | + | ||
| 3271 | + | [[package]] |
|
| 3272 | + | name = "tracing-core" |
|
| 3273 | + | version = "0.1.36" |
|
| 3274 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3275 | + | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" |
|
| 3276 | + | dependencies = [ |
|
| 3277 | + | "once_cell", |
|
| 3278 | + | "valuable", |
|
| 3279 | + | ] |
|
| 3280 | + | ||
| 3281 | + | [[package]] |
|
| 3282 | + | name = "tracing-log" |
|
| 3283 | + | version = "0.2.0" |
|
| 3284 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3285 | + | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" |
|
| 3286 | + | dependencies = [ |
|
| 3287 | + | "log", |
|
| 3288 | + | "once_cell", |
|
| 3289 | + | "tracing-core", |
|
| 3290 | + | ] |
|
| 3291 | + | ||
| 3292 | + | [[package]] |
|
| 3293 | + | name = "tracing-subscriber" |
|
| 3294 | + | version = "0.3.23" |
|
| 3295 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3296 | + | checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" |
|
| 3297 | + | dependencies = [ |
|
| 3298 | + | "matchers", |
|
| 3299 | + | "nu-ansi-term", |
|
| 3300 | + | "once_cell", |
|
| 3301 | + | "regex-automata", |
|
| 3302 | + | "sharded-slab", |
|
| 3303 | + | "smallvec", |
|
| 3304 | + | "thread_local", |
|
| 3305 | + | "tracing", |
|
| 3306 | + | "tracing-core", |
|
| 3307 | + | "tracing-log", |
|
| 3308 | + | ] |
|
| 3309 | + | ||
| 3310 | + | [[package]] |
|
| 3311 | + | name = "try-lock" |
|
| 3312 | + | version = "0.2.5" |
|
| 3313 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3314 | + | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" |
|
| 3315 | + | ||
| 3316 | + | [[package]] |
|
| 3317 | + | name = "typenum" |
|
| 3318 | + | version = "1.20.1" |
|
| 3319 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3320 | + | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" |
|
| 3321 | + | ||
| 3322 | + | [[package]] |
|
| 3323 | + | name = "unicase" |
|
| 3324 | + | version = "2.9.0" |
|
| 3325 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3326 | + | checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" |
|
| 3327 | + | ||
| 3328 | + | [[package]] |
|
| 3329 | + | name = "unicode-bidi" |
|
| 3330 | + | version = "0.3.18" |
|
| 3331 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3332 | + | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" |
|
| 3333 | + | ||
| 3334 | + | [[package]] |
|
| 3335 | + | name = "unicode-ident" |
|
| 3336 | + | version = "1.0.26" |
|
| 3337 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3338 | + | checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" |
|
| 3339 | + | ||
| 3340 | + | [[package]] |
|
| 3341 | + | name = "unicode-normalization" |
|
| 3342 | + | version = "0.1.25" |
|
| 3343 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3344 | + | checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" |
|
| 3345 | + | dependencies = [ |
|
| 3346 | + | "tinyvec", |
|
| 3347 | + | ] |
|
| 3348 | + | ||
| 3349 | + | [[package]] |
|
| 3350 | + | name = "unicode-properties" |
|
| 3351 | + | version = "0.1.4" |
|
| 3352 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3353 | + | checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" |
|
| 3354 | + | ||
| 3355 | + | [[package]] |
|
| 3356 | + | name = "untrusted" |
|
| 3357 | + | version = "0.9.0" |
|
| 3358 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3359 | + | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" |
|
| 3360 | + | ||
| 3361 | + | [[package]] |
|
| 3362 | + | name = "ureq" |
|
| 3363 | + | version = "2.12.1" |
|
| 3364 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3365 | + | checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" |
|
| 3366 | + | dependencies = [ |
|
| 3367 | + | "base64 0.22.1", |
|
| 3368 | + | "flate2", |
|
| 3369 | + | "log", |
|
| 3370 | + | "once_cell", |
|
| 3371 | + | "rustls", |
|
| 3372 | + | "rustls-pki-types", |
|
| 3373 | + | "url", |
|
| 3374 | + | "webpki-roots 0.26.11", |
|
| 3375 | + | ] |
|
| 3376 | + | ||
| 3377 | + | [[package]] |
|
| 3378 | + | name = "url" |
|
| 3379 | + | version = "2.5.8" |
|
| 3380 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3381 | + | checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" |
|
| 3382 | + | dependencies = [ |
|
| 3383 | + | "form_urlencoded", |
|
| 3384 | + | "idna", |
|
| 3385 | + | "percent-encoding", |
|
| 3386 | + | "serde", |
|
| 3387 | + | ] |
|
| 3388 | + | ||
| 3389 | + | [[package]] |
|
| 3390 | + | name = "utf8_iter" |
|
| 3391 | + | version = "1.0.4" |
|
| 3392 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3393 | + | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" |
|
| 3394 | + | ||
| 3395 | + | [[package]] |
|
| 3396 | + | name = "utf8parse" |
|
| 3397 | + | version = "0.2.2" |
|
| 3398 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3399 | + | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" |
|
| 3400 | + | ||
| 3401 | + | [[package]] |
|
| 3402 | + | name = "valuable" |
|
| 3403 | + | version = "0.1.1" |
|
| 3404 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3405 | + | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" |
|
| 3406 | + | ||
| 3407 | + | [[package]] |
|
| 3408 | + | name = "vcpkg" |
|
| 3409 | + | version = "0.2.15" |
|
| 3410 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3411 | + | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" |
|
| 3412 | + | ||
| 3413 | + | [[package]] |
|
| 3414 | + | name = "version_check" |
|
| 3415 | + | version = "0.9.5" |
|
| 3416 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3417 | + | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" |
|
| 3418 | + | ||
| 3419 | + | [[package]] |
|
| 3420 | + | name = "walkdir" |
|
| 3421 | + | version = "2.5.0" |
|
| 3422 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3423 | + | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" |
|
| 3424 | + | dependencies = [ |
|
| 3425 | + | "same-file", |
|
| 3426 | + | "winapi-util", |
|
| 3427 | + | ] |
|
| 3428 | + | ||
| 3429 | + | [[package]] |
|
| 3430 | + | name = "want" |
|
| 3431 | + | version = "0.3.1" |
|
| 3432 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3433 | + | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" |
|
| 3434 | + | dependencies = [ |
|
| 3435 | + | "try-lock", |
|
| 3436 | + | ] |
|
| 3437 | + | ||
| 3438 | + | [[package]] |
|
| 3439 | + | name = "wasi" |
|
| 3440 | + | version = "0.11.1+wasi-snapshot-preview1" |
|
| 3441 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3442 | + | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" |
|
| 3443 | + | ||
| 3444 | + | [[package]] |
|
| 3445 | + | name = "wasite" |
|
| 3446 | + | version = "0.1.0" |
|
| 3447 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3448 | + | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" |
|
| 3449 | + | ||
| 3450 | + | [[package]] |
|
| 3451 | + | name = "wasm-bindgen" |
|
| 3452 | + | version = "0.2.129" |
|
| 3453 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3454 | + | checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" |
|
| 3455 | + | dependencies = [ |
|
| 3456 | + | "cfg-if", |
|
| 3457 | + | "once_cell", |
|
| 3458 | + | "rustversion", |
|
| 3459 | + | "wasm-bindgen-macro", |
|
| 3460 | + | "wasm-bindgen-shared", |
|
| 3461 | + | ] |
|
| 3462 | + | ||
| 3463 | + | [[package]] |
|
| 3464 | + | name = "wasm-bindgen-futures" |
|
| 3465 | + | version = "0.4.79" |
|
| 3466 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3467 | + | checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" |
|
| 3468 | + | dependencies = [ |
|
| 3469 | + | "js-sys", |
|
| 3470 | + | "tokio", |
|
| 3471 | + | "wasm-bindgen", |
|
| 3472 | + | ] |
|
| 3473 | + | ||
| 3474 | + | [[package]] |
|
| 3475 | + | name = "wasm-bindgen-macro" |
|
| 3476 | + | version = "0.2.129" |
|
| 3477 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3478 | + | checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" |
|
| 3479 | + | dependencies = [ |
|
| 3480 | + | "quote", |
|
| 3481 | + | "wasm-bindgen-macro-support", |
|
| 3482 | + | ] |
|
| 3483 | + | ||
| 3484 | + | [[package]] |
|
| 3485 | + | name = "wasm-bindgen-macro-support" |
|
| 3486 | + | version = "0.2.129" |
|
| 3487 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3488 | + | checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" |
|
| 3489 | + | dependencies = [ |
|
| 3490 | + | "bumpalo", |
|
| 3491 | + | "proc-macro2", |
|
| 3492 | + | "quote", |
|
| 3493 | + | "syn 3.0.6", |
|
| 3494 | + | "wasm-bindgen-shared", |
|
| 3495 | + | ] |
|
| 3496 | + | ||
| 3497 | + | [[package]] |
|
| 3498 | + | name = "wasm-bindgen-shared" |
|
| 3499 | + | version = "0.2.129" |
|
| 3500 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3501 | + | checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" |
|
| 3502 | + | dependencies = [ |
|
| 3503 | + | "unicode-ident", |
|
| 3504 | + | ] |
|
| 3505 | + | ||
| 3506 | + | [[package]] |
|
| 3507 | + | name = "web-sys" |
|
| 3508 | + | version = "0.3.106" |
|
| 3509 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3510 | + | checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" |
|
| 3511 | + | dependencies = [ |
|
| 3512 | + | "js-sys", |
|
| 3513 | + | "wasm-bindgen", |
|
| 3514 | + | ] |
|
| 3515 | + | ||
| 3516 | + | [[package]] |
|
| 3517 | + | name = "web-time" |
|
| 3518 | + | version = "1.1.0" |
|
| 3519 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3520 | + | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" |
|
| 3521 | + | dependencies = [ |
|
| 3522 | + | "js-sys", |
|
| 3523 | + | "wasm-bindgen", |
|
| 3524 | + | ] |
|
| 3525 | + | ||
| 3526 | + | [[package]] |
|
| 3527 | + | name = "web_atoms" |
|
| 3528 | + | version = "0.3.0" |
|
| 3529 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3530 | + | checksum = "7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada" |
|
| 3531 | + | dependencies = [ |
|
| 3532 | + | "phf", |
|
| 3533 | + | "phf_codegen", |
|
| 3534 | + | "string_cache", |
|
| 3535 | + | "string_cache_codegen", |
|
| 3536 | + | ] |
|
| 3537 | + | ||
| 3538 | + | [[package]] |
|
| 3539 | + | name = "webpki-roots" |
|
| 3540 | + | version = "0.26.11" |
|
| 3541 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3542 | + | checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" |
|
| 3543 | + | dependencies = [ |
|
| 3544 | + | "webpki-roots 1.0.9", |
|
| 3545 | + | ] |
|
| 3546 | + | ||
| 3547 | + | [[package]] |
|
| 3548 | + | name = "webpki-roots" |
|
| 3549 | + | version = "1.0.9" |
|
| 3550 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3551 | + | checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" |
|
| 3552 | + | dependencies = [ |
|
| 3553 | + | "rustls-pki-types", |
|
| 3554 | + | ] |
|
| 3555 | + | ||
| 3556 | + | [[package]] |
|
| 3557 | + | name = "whoami" |
|
| 3558 | + | version = "1.6.1" |
|
| 3559 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3560 | + | checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" |
|
| 3561 | + | dependencies = [ |
|
| 3562 | + | "libredox", |
|
| 3563 | + | "wasite", |
|
| 3564 | + | ] |
|
| 3565 | + | ||
| 3566 | + | [[package]] |
|
| 3567 | + | name = "winapi-util" |
|
| 3568 | + | version = "0.1.11" |
|
| 3569 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3570 | + | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" |
|
| 3571 | + | dependencies = [ |
|
| 3572 | + | "windows-sys 0.61.2", |
|
| 3573 | + | ] |
|
| 3574 | + | ||
| 3575 | + | [[package]] |
|
| 3576 | + | name = "windows-link" |
|
| 3577 | + | version = "0.2.1" |
|
| 3578 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3579 | + | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" |
|
| 3580 | + | ||
| 3581 | + | [[package]] |
|
| 3582 | + | name = "windows-sys" |
|
| 3583 | + | version = "0.48.0" |
|
| 3584 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3585 | + | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" |
|
| 3586 | + | dependencies = [ |
|
| 3587 | + | "windows-targets 0.48.5", |
|
| 3588 | + | ] |
|
| 3589 | + | ||
| 3590 | + | [[package]] |
|
| 3591 | + | name = "windows-sys" |
|
| 3592 | + | version = "0.52.0" |
|
| 3593 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3594 | + | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" |
|
| 3595 | + | dependencies = [ |
|
| 3596 | + | "windows-targets 0.52.6", |
|
| 3597 | + | ] |
|
| 3598 | + | ||
| 3599 | + | [[package]] |
|
| 3600 | + | name = "windows-sys" |
|
| 3601 | + | version = "0.61.2" |
|
| 3602 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3603 | + | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" |
|
| 3604 | + | dependencies = [ |
|
| 3605 | + | "windows-link", |
|
| 3606 | + | ] |
|
| 3607 | + | ||
| 3608 | + | [[package]] |
|
| 3609 | + | name = "windows-targets" |
|
| 3610 | + | version = "0.48.5" |
|
| 3611 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3612 | + | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" |
|
| 3613 | + | dependencies = [ |
|
| 3614 | + | "windows_aarch64_gnullvm 0.48.5", |
|
| 3615 | + | "windows_aarch64_msvc 0.48.5", |
|
| 3616 | + | "windows_i686_gnu 0.48.5", |
|
| 3617 | + | "windows_i686_msvc 0.48.5", |
|
| 3618 | + | "windows_x86_64_gnu 0.48.5", |
|
| 3619 | + | "windows_x86_64_gnullvm 0.48.5", |
|
| 3620 | + | "windows_x86_64_msvc 0.48.5", |
|
| 3621 | + | ] |
|
| 3622 | + | ||
| 3623 | + | [[package]] |
|
| 3624 | + | name = "windows-targets" |
|
| 3625 | + | version = "0.52.6" |
|
| 3626 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3627 | + | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" |
|
| 3628 | + | dependencies = [ |
|
| 3629 | + | "windows_aarch64_gnullvm 0.52.6", |
|
| 3630 | + | "windows_aarch64_msvc 0.52.6", |
|
| 3631 | + | "windows_i686_gnu 0.52.6", |
|
| 3632 | + | "windows_i686_gnullvm", |
|
| 3633 | + | "windows_i686_msvc 0.52.6", |
|
| 3634 | + | "windows_x86_64_gnu 0.52.6", |
|
| 3635 | + | "windows_x86_64_gnullvm 0.52.6", |
|
| 3636 | + | "windows_x86_64_msvc 0.52.6", |
|
| 3637 | + | ] |
|
| 3638 | + | ||
| 3639 | + | [[package]] |
|
| 3640 | + | name = "windows_aarch64_gnullvm" |
|
| 3641 | + | version = "0.48.5" |
|
| 3642 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3643 | + | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" |
|
| 3644 | + | ||
| 3645 | + | [[package]] |
|
| 3646 | + | name = "windows_aarch64_gnullvm" |
|
| 3647 | + | version = "0.52.6" |
|
| 3648 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3649 | + | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" |
|
| 3650 | + | ||
| 3651 | + | [[package]] |
|
| 3652 | + | name = "windows_aarch64_msvc" |
|
| 3653 | + | version = "0.48.5" |
|
| 3654 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3655 | + | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" |
|
| 3656 | + | ||
| 3657 | + | [[package]] |
|
| 3658 | + | name = "windows_aarch64_msvc" |
|
| 3659 | + | version = "0.52.6" |
|
| 3660 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3661 | + | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" |
|
| 3662 | + | ||
| 3663 | + | [[package]] |
|
| 3664 | + | name = "windows_i686_gnu" |
|
| 3665 | + | version = "0.48.5" |
|
| 3666 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3667 | + | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" |
|
| 3668 | + | ||
| 3669 | + | [[package]] |
|
| 3670 | + | name = "windows_i686_gnu" |
|
| 3671 | + | version = "0.52.6" |
|
| 3672 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3673 | + | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" |
|
| 3674 | + | ||
| 3675 | + | [[package]] |
|
| 3676 | + | name = "windows_i686_gnullvm" |
|
| 3677 | + | version = "0.52.6" |
|
| 3678 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3679 | + | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" |
|
| 3680 | + | ||
| 3681 | + | [[package]] |
|
| 3682 | + | name = "windows_i686_msvc" |
|
| 3683 | + | version = "0.48.5" |
|
| 3684 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3685 | + | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" |
|
| 3686 | + | ||
| 3687 | + | [[package]] |
|
| 3688 | + | name = "windows_i686_msvc" |
|
| 3689 | + | version = "0.52.6" |
|
| 3690 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3691 | + | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" |
|
| 3692 | + | ||
| 3693 | + | [[package]] |
|
| 3694 | + | name = "windows_x86_64_gnu" |
|
| 3695 | + | version = "0.48.5" |
|
| 3696 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3697 | + | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" |
|
| 3698 | + | ||
| 3699 | + | [[package]] |
|
| 3700 | + | name = "windows_x86_64_gnu" |
|
| 3701 | + | version = "0.52.6" |
|
| 3702 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3703 | + | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" |
|
| 3704 | + | ||
| 3705 | + | [[package]] |
|
| 3706 | + | name = "windows_x86_64_gnullvm" |
|
| 3707 | + | version = "0.48.5" |
|
| 3708 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3709 | + | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" |
|
| 3710 | + | ||
| 3711 | + | [[package]] |
|
| 3712 | + | name = "windows_x86_64_gnullvm" |
|
| 3713 | + | version = "0.52.6" |
|
| 3714 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3715 | + | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" |
|
| 3716 | + | ||
| 3717 | + | [[package]] |
|
| 3718 | + | name = "windows_x86_64_msvc" |
|
| 3719 | + | version = "0.48.5" |
|
| 3720 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3721 | + | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" |
|
| 3722 | + | ||
| 3723 | + | [[package]] |
|
| 3724 | + | name = "windows_x86_64_msvc" |
|
| 3725 | + | version = "0.52.6" |
|
| 3726 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3727 | + | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" |
|
| 3728 | + | ||
| 3729 | + | [[package]] |
|
| 3730 | + | name = "winnow" |
|
| 3731 | + | version = "0.7.15" |
|
| 3732 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3733 | + | checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" |
|
| 3734 | + | dependencies = [ |
|
| 3735 | + | "memchr", |
|
| 3736 | + | ] |
|
| 3737 | + | ||
| 3738 | + | [[package]] |
|
| 3739 | + | name = "writeable" |
|
| 3740 | + | version = "0.6.4" |
|
| 3741 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3742 | + | checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" |
|
| 3743 | + | ||
| 3744 | + | [[package]] |
|
| 3745 | + | name = "x509-parser" |
|
| 3746 | + | version = "0.18.1" |
|
| 3747 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3748 | + | checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" |
|
| 3749 | + | dependencies = [ |
|
| 3750 | + | "asn1-rs", |
|
| 3751 | + | "data-encoding", |
|
| 3752 | + | "der-parser", |
|
| 3753 | + | "lazy_static", |
|
| 3754 | + | "nom", |
|
| 3755 | + | "oid-registry", |
|
| 3756 | + | "ring", |
|
| 3757 | + | "rusticata-macros", |
|
| 3758 | + | "thiserror 2.0.21", |
|
| 3759 | + | "time", |
|
| 3760 | + | ] |
|
| 3761 | + | ||
| 3762 | + | [[package]] |
|
| 3763 | + | name = "yaml-rust" |
|
| 3764 | + | version = "0.4.5" |
|
| 3765 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3766 | + | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" |
|
| 3767 | + | dependencies = [ |
|
| 3768 | + | "linked-hash-map", |
|
| 3769 | + | ] |
|
| 3770 | + | ||
| 3771 | + | [[package]] |
|
| 3772 | + | name = "yasna" |
|
| 3773 | + | version = "0.6.0" |
|
| 3774 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3775 | + | checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" |
|
| 3776 | + | dependencies = [ |
|
| 3777 | + | "bit-vec 0.9.1", |
|
| 3778 | + | "time", |
|
| 3779 | + | ] |
|
| 3780 | + | ||
| 3781 | + | [[package]] |
|
| 3782 | + | name = "yoke" |
|
| 3783 | + | version = "0.8.3" |
|
| 3784 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3785 | + | checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" |
|
| 3786 | + | dependencies = [ |
|
| 3787 | + | "stable_deref_trait", |
|
| 3788 | + | "yoke-derive", |
|
| 3789 | + | "zerofrom", |
|
| 3790 | + | ] |
|
| 3791 | + | ||
| 3792 | + | [[package]] |
|
| 3793 | + | name = "yoke-derive" |
|
| 3794 | + | version = "0.8.3" |
|
| 3795 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3796 | + | checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" |
|
| 3797 | + | dependencies = [ |
|
| 3798 | + | "proc-macro2", |
|
| 3799 | + | "quote", |
|
| 3800 | + | "syn 3.0.6", |
|
| 3801 | + | "synstructure 0.14.0", |
|
| 3802 | + | ] |
|
| 3803 | + | ||
| 3804 | + | [[package]] |
|
| 3805 | + | name = "zerocopy" |
|
| 3806 | + | version = "0.8.59" |
|
| 3807 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3808 | + | checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" |
|
| 3809 | + | dependencies = [ |
|
| 3810 | + | "zerocopy-derive", |
|
| 3811 | + | ] |
|
| 3812 | + | ||
| 3813 | + | [[package]] |
|
| 3814 | + | name = "zerocopy-derive" |
|
| 3815 | + | version = "0.8.59" |
|
| 3816 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3817 | + | checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" |
|
| 3818 | + | dependencies = [ |
|
| 3819 | + | "proc-macro2", |
|
| 3820 | + | "quote", |
|
| 3821 | + | "syn 2.0.119", |
|
| 3822 | + | ] |
|
| 3823 | + | ||
| 3824 | + | [[package]] |
|
| 3825 | + | name = "zerofrom" |
|
| 3826 | + | version = "0.1.8" |
|
| 3827 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3828 | + | checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" |
|
| 3829 | + | dependencies = [ |
|
| 3830 | + | "zerofrom-derive", |
|
| 3831 | + | ] |
|
| 3832 | + | ||
| 3833 | + | [[package]] |
|
| 3834 | + | name = "zerofrom-derive" |
|
| 3835 | + | version = "0.1.8" |
|
| 3836 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3837 | + | checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" |
|
| 3838 | + | dependencies = [ |
|
| 3839 | + | "proc-macro2", |
|
| 3840 | + | "quote", |
|
| 3841 | + | "syn 3.0.6", |
|
| 3842 | + | "synstructure 0.14.0", |
|
| 3843 | + | ] |
|
| 3844 | + | ||
| 3845 | + | [[package]] |
|
| 3846 | + | name = "zeroize" |
|
| 3847 | + | version = "1.9.0" |
|
| 3848 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3849 | + | checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" |
|
| 3850 | + | ||
| 3851 | + | [[package]] |
|
| 3852 | + | name = "zerotrie" |
|
| 3853 | + | version = "0.2.5" |
|
| 3854 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3855 | + | checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" |
|
| 3856 | + | dependencies = [ |
|
| 3857 | + | "displaydoc", |
|
| 3858 | + | "yoke", |
|
| 3859 | + | "zerofrom", |
|
| 3860 | + | ] |
|
| 3861 | + | ||
| 3862 | + | [[package]] |
|
| 3863 | + | name = "zerovec" |
|
| 3864 | + | version = "0.11.8" |
|
| 3865 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3866 | + | checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" |
|
| 3867 | + | dependencies = [ |
|
| 3868 | + | "yoke", |
|
| 3869 | + | "zerofrom", |
|
| 3870 | + | "zerovec-derive", |
|
| 3871 | + | ] |
|
| 3872 | + | ||
| 3873 | + | [[package]] |
|
| 3874 | + | name = "zerovec-derive" |
|
| 3875 | + | version = "0.11.6" |
|
| 3876 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3877 | + | checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" |
|
| 3878 | + | dependencies = [ |
|
| 3879 | + | "proc-macro2", |
|
| 3880 | + | "quote", |
|
| 3881 | + | "syn 3.0.6", |
|
| 3882 | + | ] |
|
| 3883 | + | ||
| 3884 | + | [[package]] |
|
| 3885 | + | name = "zlib-rs" |
|
| 3886 | + | version = "0.6.8" |
|
| 3887 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3888 | + | checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" |
|
| 3889 | + | ||
| 3890 | + | [[package]] |
|
| 3891 | + | name = "zmij" |
|
| 3892 | + | version = "1.0.23" |
|
| 3893 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3894 | + | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" |
|
| 3895 | + | ||
| 3896 | + | [[package]] |
|
| 3897 | + | name = "zstd" |
|
| 3898 | + | version = "0.13.3" |
|
| 3899 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3900 | + | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" |
|
| 3901 | + | dependencies = [ |
|
| 3902 | + | "zstd-safe", |
|
| 3903 | + | ] |
|
| 3904 | + | ||
| 3905 | + | [[package]] |
|
| 3906 | + | name = "zstd-safe" |
|
| 3907 | + | version = "7.3.0" |
|
| 3908 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3909 | + | checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" |
|
| 3910 | + | dependencies = [ |
|
| 3911 | + | "zstd-sys", |
|
| 3912 | + | ] |
|
| 3913 | + | ||
| 3914 | + | [[package]] |
|
| 3915 | + | name = "zstd-sys" |
|
| 3916 | + | version = "2.1.0+zstd.1.5.7" |
|
| 3917 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3918 | + | checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" |
|
| 3919 | + | dependencies = [ |
|
| 3920 | + | "cc", |
|
| 3921 | + | "pkg-config", |
|
| 3922 | + | ] |
added Cargo.toml +44 −0
| 1 | + | [workspace] |
|
| 2 | + | members = ["crates/hub", "crates/hub-api"] |
|
| 3 | + | resolver = "2" |
|
| 4 | + | ||
| 5 | + | [workspace.package] |
|
| 6 | + | version = "0.1.0" |
|
| 7 | + | edition = "2021" |
|
| 8 | + | license = "MIT OR Apache-2.0" |
|
| 9 | + | authors = ["Andy McCall <[email protected]>"] |
|
| 10 | + | repository = "https://earth.andymccall.xyz:7343/andymccall/oxygenhub" |
|
| 11 | + | ||
| 12 | + | [workspace.dependencies] |
|
| 13 | + | oxy-core = { path = "../oxygen/crates/oxy-core" } |
|
| 14 | + | oxy-server = { path = "../oxygen/crates/oxy-server" } |
|
| 15 | + | hub-api = { path = "crates/hub-api" } |
|
| 16 | + | ||
| 17 | + | anyhow = "1" |
|
| 18 | + | thiserror = "2" |
|
| 19 | + | serde = { version = "1", features = ["derive"] } |
|
| 20 | + | serde_json = "1" |
|
| 21 | + | tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time", "signal", "fs"] } |
|
| 22 | + | axum = { version = "0.8", features = ["macros", "form", "query"] } |
|
| 23 | + | axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] } |
|
| 24 | + | rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } |
|
| 25 | + | tower-http = { version = "0.6", features = ["trace", "compression-gzip", "set-header", "limit"] } |
|
| 26 | + | askama = "0.14" |
|
| 27 | + | sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite", "migrate", "macros"] } |
|
| 28 | + | tracing = "0.1" |
|
| 29 | + | tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
|
| 30 | + | clap = { version = "4", features = ["derive", "env"] } |
|
| 31 | + | toml = "0.8" |
|
| 32 | + | time = { version = "0.3", features = ["formatting", "macros"] } |
|
| 33 | + | pulldown-cmark = { version = "0.13", default-features = false, features = ["html"] } |
|
| 34 | + | ammonia = "4" |
|
| 35 | + | syntect = { version = "5", default-features = false, features = ["default-fancy"] } |
|
| 36 | + | rcgen = "0.14" |
|
| 37 | + | hex = "0.4" |
|
| 38 | + | rand = "0.8" |
|
| 39 | + | similar = "2" |
|
| 40 | + | tempfile = "3" |
|
| 41 | + | reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } |
|
| 42 | + | ||
| 43 | + | [profile.release] |
|
| 44 | + | debug = 1 |
added Makefile +23 −0
| 1 | + | .PHONY: build check test fmt clippy run deploy |
|
| 2 | + | ||
| 3 | + | build: |
|
| 4 | + | cargo build --release |
|
| 5 | + | ||
| 6 | + | check: fmt clippy test |
|
| 7 | + | ||
| 8 | + | fmt: |
|
| 9 | + | cargo fmt --all -- --check |
|
| 10 | + | ||
| 11 | + | clippy: |
|
| 12 | + | cargo clippy --workspace --all-targets -- -D warnings |
|
| 13 | + | ||
| 14 | + | test: |
|
| 15 | + | cargo test --workspace |
|
| 16 | + | ||
| 17 | + | run: |
|
| 18 | + | cargo run -- --data data serve |
|
| 19 | + | ||
| 20 | + | # Developer edition on Earth (docs/deploy.md). |
|
| 21 | + | deploy: |
|
| 22 | + | rsync -a --delete --exclude target --exclude node_modules --exclude .oxy/objects/loose ../oxygen ../oxygenhub claude@earth:src/ |
|
| 23 | + | ssh claude@earth 'source ~/.cargo/env && cd src/oxygenhub && cargo build --release && sudo install -m 0755 target/release/oxygenhub /usr/local/bin/oxygenhub && sudo systemctl restart oxygenhub && systemctl is-active oxygenhub' |
added README.md +50 −0
| 1 | + | # Oxygenhub |
|
| 2 | + | ||
| 3 | + | A forge for [Oxygen](../oxygen) repositories: hosting, browsing, review, |
|
| 4 | + | issues and releases, in one Rust binary with one data directory. It works |
|
| 5 | + | like GitHub where GitHub is right and differs where Oxygen's commutative |
|
| 6 | + | model makes something better possible. The specification is |
|
| 7 | + | [docs/spec.md](docs/spec.md); deployment notes are in |
|
| 8 | + | [docs/deploy.md](docs/deploy.md). |
|
| 9 | + | ||
| 10 | + | ## Status |
|
| 11 | + | ||
| 12 | + | Milestone 0.1 (browse): repositories are created by an administrator and |
|
| 13 | + | filled by `oxy push`; the pages show trees, files, blame, history with the |
|
| 14 | + | dependency graph, changes with diffs, comparisons, bookmarks, tags, |
|
| 15 | + | conflicts, the trust policy and the push log. Accounts, proposals, issues |
|
| 16 | + | and the rest follow the milestones in the specification. |
|
| 17 | + | ||
| 18 | + | ## Running |
|
| 19 | + | ||
| 20 | + | ``` |
|
| 21 | + | cargo build --release |
|
| 22 | + | target/release/oxygenhub --data /srv/oxygenhub init --base-url https://hub.example:7343 --self-signed |
|
| 23 | + | target/release/oxygenhub --data /srv/oxygenhub admin repo create andy/oxygen --description "The VCS" |
|
| 24 | + | target/release/oxygenhub --data /srv/oxygenhub serve |
|
| 25 | + | ``` |
|
| 26 | + | ||
| 27 | + | Then from any checkout: `oxy remote add hub https://hub.example:7343/andy/oxygen` |
|
| 28 | + | and `oxy push hub`. With a self-signed certificate, hand clients the CA with |
|
| 29 | + | `OXY_CA=<path>` or `oxy config remote.hub.ca <path>`. |
|
| 30 | + | ||
| 31 | + | Configuration lives in `<data>/hub.toml` (spec §17.1) and every key can be |
|
| 32 | + | overridden with `OXYHUB_<SECTION>_<KEY>`. |
|
| 33 | + | ||
| 34 | + | ## Layout |
|
| 35 | + | ||
| 36 | + | ``` |
|
| 37 | + | crates/hub the binary: config, db (sqlx + migrations), repos (registry, locks, views), |
|
| 38 | + | protocol (embedded oxy-server), web (pages), api (JSON), render (markdown, |
|
| 39 | + | highlighting, diffs, graph) |
|
| 40 | + | crates/hub-api the JSON API's types, shared with clients |
|
| 41 | + | templates/ askama templates and the stylesheet |
|
| 42 | + | assets/ vendored fonts, icons, the small progressive-enhancement script |
|
| 43 | + | migrations/ under crates/hub; applied at start |
|
| 44 | + | ``` |
|
| 45 | + | ||
| 46 | + | ## Developing |
|
| 47 | + | ||
| 48 | + | `make check` runs formatting, clippy with warnings as errors, and the tests. |
|
| 49 | + | The tests use a real `oxy-core` client against a hub on a loopback port and |
|
| 50 | + | never touch your own Oxygen configuration. |
added assets/fonts/Barlow-300.woff2 +0 −0
No line changes.
added assets/fonts/Barlow-400.woff2 +0 −0
No line changes.
added assets/fonts/Barlow-500.woff2 +0 −0
No line changes.
added assets/fonts/Barlow-600.woff2 +0 −0
No line changes.
added assets/fonts/PressStart2P-400.woff2 +0 −0
No line changes.
added assets/hub.js +77 −0
| 1 | + | /* Progressive enhancement only: every page works without this file. */ |
|
| 2 | + | (function () { |
|
| 3 | + | "use strict"; |
|
| 4 | + | /* The band's active tab: the longest link that prefixes the path. */ |
|
| 5 | + | var p = location.pathname, best = null; |
|
| 6 | + | document.querySelectorAll(".band a").forEach(function (a) { |
|
| 7 | + | var h = a.getAttribute("href"); |
|
| 8 | + | if (h !== "/" && (p === h || p.indexOf(h + "/") === 0) && (!best || h.length > best.getAttribute("href").length)) best = a; |
|
| 9 | + | }); |
|
| 10 | + | if (best) best.classList.add("on"); |
|
| 11 | + | ||
| 12 | + | /* Copy buttons. */ |
|
| 13 | + | document.querySelectorAll("[data-copy]").forEach(function (b) { |
|
| 14 | + | b.addEventListener("click", function () { |
|
| 15 | + | navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () { |
|
| 16 | + | var t = b.textContent; b.textContent = "copied"; setTimeout(function () { b.textContent = t; }, 1200); |
|
| 17 | + | }); |
|
| 18 | + | }); |
|
| 19 | + | }); |
|
| 20 | + | ||
| 21 | + | /* Tree rows: the last change per entry, fetched after the page paints. */ |
|
| 22 | + | var tree = document.querySelector("table.tree[data-last]"); |
|
| 23 | + | if (tree) { |
|
| 24 | + | var paths = [].map.call(tree.querySelectorAll("tr[data-path]"), function (r) { return r.getAttribute("data-path"); }); |
|
| 25 | + | if (paths.length) { |
|
| 26 | + | fetch(tree.getAttribute("data-last") + "?paths=" + encodeURIComponent(paths.join(",")), { credentials: "same-origin" }) |
|
| 27 | + | .then(function (r) { return r.json(); }) |
|
| 28 | + | .then(function (rows) { |
|
| 29 | + | rows.forEach(function (c) { |
|
| 30 | + | var tr = tree.querySelector('tr[data-path="' + CSS.escape(c.path) + '"]'); |
|
| 31 | + | if (!tr) return; |
|
| 32 | + | var a = tr.querySelector("td.last a"), t = tr.querySelector("td.age"); |
|
| 33 | + | if (a) { a.textContent = c.title; a.href = tree.getAttribute("data-change") + c.hash; } |
|
| 34 | + | if (t) { t.textContent = c.ago; t.title = c.when; } |
|
| 35 | + | }); |
|
| 36 | + | }).catch(function () {}); |
|
| 37 | + | } |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | /* Line anchors: #L12 or #L12-L20 highlight a range and clicking a number selects. */ |
|
| 41 | + | function mark() { |
|
| 42 | + | var m = location.hash.match(/^#L(\d+)(?:-L(\d+))?$/); |
|
| 43 | + | document.querySelectorAll("tr.hl").forEach(function (r) { r.classList.remove("hl"); }); |
|
| 44 | + | if (!m) return; |
|
| 45 | + | var a = +m[1], b = +(m[2] || m[1]); |
|
| 46 | + | for (var i = Math.min(a, b); i <= Math.max(a, b); i++) { |
|
| 47 | + | var row = document.getElementById("L" + i); |
|
| 48 | + | if (row) row.classList.add("hl"); |
|
| 49 | + | } |
|
| 50 | + | var first = document.getElementById("L" + Math.min(a, b)); |
|
| 51 | + | if (first && !mark.done) { first.scrollIntoView({ block: "center" }); mark.done = true; } |
|
| 52 | + | } |
|
| 53 | + | window.addEventListener("hashchange", mark); |
|
| 54 | + | mark(); |
|
| 55 | + | document.querySelectorAll("table.code td.n").forEach(function (td) { |
|
| 56 | + | td.addEventListener("click", function (e) { |
|
| 57 | + | var n = td.parentNode.id; |
|
| 58 | + | if (e.shiftKey && location.hash.match(/^#L(\d+)/)) location.hash = location.hash.replace(/^#(L\d+).*/, "$1-" + n); |
|
| 59 | + | else location.hash = n; |
|
| 60 | + | }); |
|
| 61 | + | }); |
|
| 62 | + | ||
| 63 | + | /* Keyboard: / focuses search; g c / g p / g i jump; ? shows the sheet. */ |
|
| 64 | + | var pending = null; |
|
| 65 | + | document.addEventListener("keydown", function (e) { |
|
| 66 | + | if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; |
|
| 67 | + | if (e.key === "/") { var s = document.querySelector("input[name=q]"); if (s) { e.preventDefault(); s.focus(); } return; } |
|
| 68 | + | if (e.key === "?") { var k = document.getElementById("keys"); if (k) k.hidden = !k.hidden; return; } |
|
| 69 | + | var repo = document.body.getAttribute("data-repo"); |
|
| 70 | + | if (pending === "g" && repo) { |
|
| 71 | + | var to = { c: "", p: "/proposals", i: "/issues", h: "/changes/" + (document.body.getAttribute("data-ref") || "main"), b: "/bookmarks", t: "/trust" }[e.key]; |
|
| 72 | + | if (to !== undefined) location.href = repo + to; |
|
| 73 | + | pending = null; return; |
|
| 74 | + | } |
|
| 75 | + | pending = e.key === "g" ? "g" : null; |
|
| 76 | + | }); |
|
| 77 | + | })(); |
added assets/icons/oxygenhub.svg +1 −0
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges"><rect width="16" height="16" fill="#141430"/><path fill="#ffc83c" d="M5 3h6v1H5zM4 4h1v1H4zM11 4h1v1h-1zM3 5h1v6H3zM12 5h1v6h-1zM4 11h1v1H4zM11 11h1v1h-1zM5 12h6v1H5z"/><path fill="#ffdd00" d="M7 7h2v2H7z"/></svg> |
added crates/hub-api/Cargo.toml +10 −0
| 1 | + | [package] |
|
| 2 | + | name = "hub-api" |
|
| 3 | + | description = "Types of the Oxygenhub JSON API, shared by the server and its clients" |
|
| 4 | + | version.workspace = true |
|
| 5 | + | edition.workspace = true |
|
| 6 | + | license.workspace = true |
|
| 7 | + | authors.workspace = true |
|
| 8 | + | ||
| 9 | + | [dependencies] |
|
| 10 | + | serde.workspace = true |
added crates/hub-api/src/lib.rs +123 −0
| 1 | + | //! Types of the Oxygenhub JSON API (hub spec §12.1). Everything here is |
|
| 2 | + | //! `serde` data with no behaviour, so clients in other languages can be |
|
| 3 | + | //! generated from it and the server can never drift from them. |
|
| 4 | + | //! |
|
| 5 | + | //! Shapes reuse `oxy --json`'s where one exists (hashes as lowercase hex, |
|
| 6 | + | //! times as milliseconds since the epoch), so tools written against the |
|
| 7 | + | //! command line work against the hub. |
|
| 8 | + | ||
| 9 | + | use serde::{Deserialize, Serialize}; |
|
| 10 | + | ||
| 11 | + | /// A repository as the API lists it. |
|
| 12 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 13 | + | pub struct Repository { |
|
| 14 | + | pub owner: String, |
|
| 15 | + | pub name: String, |
|
| 16 | + | pub description: String, |
|
| 17 | + | pub default_bookmark: String, |
|
| 18 | + | pub visibility: Visibility, |
|
| 19 | + | /// Milliseconds since the epoch. |
|
| 20 | + | pub created_ms: i64, |
|
| 21 | + | pub updated_ms: i64, |
|
| 22 | + | /// `https://…/{owner}/{name}`, what `oxy clone` takes. |
|
| 23 | + | pub clone_url: String, |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 27 | + | #[serde(rename_all = "lowercase")] |
|
| 28 | + | pub enum Visibility { |
|
| 29 | + | Public, |
|
| 30 | + | Internal, |
|
| 31 | + | Private, |
|
| 32 | + | } |
|
| 33 | + | ||
| 34 | + | impl Visibility { |
|
| 35 | + | pub fn as_str(self) -> &'static str { |
|
| 36 | + | match self { |
|
| 37 | + | Visibility::Public => "public", |
|
| 38 | + | Visibility::Internal => "internal", |
|
| 39 | + | Visibility::Private => "private", |
|
| 40 | + | } |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | pub fn parse(s: &str) -> Option<Visibility> { |
|
| 44 | + | Some(match s { |
|
| 45 | + | "public" => Visibility::Public, |
|
| 46 | + | "internal" => Visibility::Internal, |
|
| 47 | + | "private" => Visibility::Private, |
|
| 48 | + | _ => return None, |
|
| 49 | + | }) |
|
| 50 | + | } |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | /// A bookmark: a name for a set of heads (spec §10.1). |
|
| 54 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 55 | + | pub struct Bookmark { |
|
| 56 | + | pub name: String, |
|
| 57 | + | pub heads: Vec<String>, |
|
| 58 | + | /// Number of changes in the bookmark's state. |
|
| 59 | + | pub changes: usize, |
|
| 60 | + | pub default: bool, |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | /// A tag: a name for one state, with its attestation when signed. |
|
| 64 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 65 | + | pub struct Tag { |
|
| 66 | + | pub name: String, |
|
| 67 | + | pub heads: Vec<String>, |
|
| 68 | + | pub message: String, |
|
| 69 | + | pub signer: Option<String>, |
|
| 70 | + | } |
|
| 71 | + | ||
| 72 | + | /// One change in a history listing. |
|
| 73 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 74 | + | pub struct ChangeSummary { |
|
| 75 | + | pub hash: String, |
|
| 76 | + | pub message: String, |
|
| 77 | + | pub author: Author, |
|
| 78 | + | pub when_ms: u64, |
|
| 79 | + | pub deps: Vec<String>, |
|
| 80 | + | pub head: bool, |
|
| 81 | + | pub signature: Signature, |
|
| 82 | + | } |
|
| 83 | + | ||
| 84 | + | /// Who wrote a change: the linked account when the signing key is linked, |
|
| 85 | + | /// else the key alone. |
|
| 86 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 87 | + | pub struct Author { |
|
| 88 | + | pub name: String, |
|
| 89 | + | pub email: String, |
|
| 90 | + | pub key: String, |
|
| 91 | + | /// The hub account this key is linked to, when any. |
|
| 92 | + | pub account: Option<String>, |
|
| 93 | + | } |
|
| 94 | + | ||
| 95 | + | /// Signature status of a change as `oxy verify` reports it. |
|
| 96 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 97 | + | #[serde(tag = "status", rename_all = "lowercase")] |
|
| 98 | + | pub enum Signature { |
|
| 99 | + | Unsigned, |
|
| 100 | + | Valid { key: String, sealed: bool, revoked: bool }, |
|
| 101 | + | Invalid { reason: String }, |
|
| 102 | + | } |
|
| 103 | + | ||
| 104 | + | /// An RFC 7807 problem response. |
|
| 105 | + | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
|
| 106 | + | pub struct Problem { |
|
| 107 | + | pub status: u16, |
|
| 108 | + | pub title: String, |
|
| 109 | + | pub detail: String, |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | #[cfg(test)] |
|
| 113 | + | mod tests { |
|
| 114 | + | use super::*; |
|
| 115 | + | ||
| 116 | + | #[test] |
|
| 117 | + | fn visibility_round_trips() { |
|
| 118 | + | for v in [Visibility::Public, Visibility::Internal, Visibility::Private] { |
|
| 119 | + | assert_eq!(Visibility::parse(v.as_str()), Some(v)); |
|
| 120 | + | } |
|
| 121 | + | assert_eq!(Visibility::parse("secret"), None); |
|
| 122 | + | } |
|
| 123 | + | } |
added crates/hub/Cargo.toml +46 −0
| 1 | + | [package] |
|
| 2 | + | name = "oxygenhub" |
|
| 3 | + | description = "A forge for Oxygen repositories: hosting, browsing, review, issues and releases" |
|
| 4 | + | version.workspace = true |
|
| 5 | + | edition.workspace = true |
|
| 6 | + | license.workspace = true |
|
| 7 | + | authors.workspace = true |
|
| 8 | + | default-run = "oxygenhub" |
|
| 9 | + | ||
| 10 | + | [[bin]] |
|
| 11 | + | name = "oxygenhub" |
|
| 12 | + | path = "src/main.rs" |
|
| 13 | + | ||
| 14 | + | [dependencies] |
|
| 15 | + | oxy-core.workspace = true |
|
| 16 | + | oxy-server.workspace = true |
|
| 17 | + | hub-api.workspace = true |
|
| 18 | + | anyhow.workspace = true |
|
| 19 | + | thiserror.workspace = true |
|
| 20 | + | serde.workspace = true |
|
| 21 | + | serde_json.workspace = true |
|
| 22 | + | tokio.workspace = true |
|
| 23 | + | axum.workspace = true |
|
| 24 | + | axum-server.workspace = true |
|
| 25 | + | rustls.workspace = true |
|
| 26 | + | tower-http.workspace = true |
|
| 27 | + | askama.workspace = true |
|
| 28 | + | sqlx.workspace = true |
|
| 29 | + | tracing.workspace = true |
|
| 30 | + | tracing-subscriber.workspace = true |
|
| 31 | + | clap.workspace = true |
|
| 32 | + | toml.workspace = true |
|
| 33 | + | time.workspace = true |
|
| 34 | + | pulldown-cmark.workspace = true |
|
| 35 | + | ammonia.workspace = true |
|
| 36 | + | syntect.workspace = true |
|
| 37 | + | rcgen.workspace = true |
|
| 38 | + | hex.workspace = true |
|
| 39 | + | rand.workspace = true |
|
| 40 | + | similar.workspace = true |
|
| 41 | + | ||
| 42 | + | [dev-dependencies] |
|
| 43 | + | tempfile.workspace = true |
|
| 44 | + | reqwest.workspace = true |
|
| 45 | + | tower = { version = "0.5", features = ["util"] } |
|
| 46 | + | http-body-util = "0.1" |
added crates/hub/askama.toml +2 −0
| 1 | + | [general] |
|
| 2 | + | dirs = ["../../templates"] |
added crates/hub/migrations/0001_repositories.sql +25 −0
| 1 | + | -- Hub state that Oxygen has no object for (hub spec §1.4). History, |
|
| 2 | + | -- references, signatures and policy live in each repository's own store. |
|
| 3 | + | ||
| 4 | + | CREATE TABLE repositories ( |
|
| 5 | + | id INTEGER PRIMARY KEY, |
|
| 6 | + | owner TEXT NOT NULL, |
|
| 7 | + | name TEXT NOT NULL, |
|
| 8 | + | description TEXT NOT NULL DEFAULT '', |
|
| 9 | + | default_bookmark TEXT NOT NULL DEFAULT 'main', |
|
| 10 | + | visibility TEXT NOT NULL DEFAULT 'public' CHECK (visibility IN ('public', 'internal', 'private')), |
|
| 11 | + | created_ms INTEGER NOT NULL, |
|
| 12 | + | updated_ms INTEGER NOT NULL, |
|
| 13 | + | UNIQUE (owner, name) |
|
| 14 | + | ); |
|
| 15 | + | ||
| 16 | + | -- Every reference update the protocol server accepted or refused (§6.12). |
|
| 17 | + | CREATE TABLE pushes ( |
|
| 18 | + | id INTEGER PRIMARY KEY, |
|
| 19 | + | repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE, |
|
| 20 | + | at_ms INTEGER NOT NULL, |
|
| 21 | + | reference TEXT NOT NULL, |
|
| 22 | + | ok INTEGER NOT NULL, |
|
| 23 | + | reason TEXT NOT NULL DEFAULT '' |
|
| 24 | + | ); |
|
| 25 | + | CREATE INDEX pushes_by_repo ON pushes (repo_id, at_ms DESC); |
added crates/hub/src/api/mod.rs +85 −0
| 1 | + | //! The JSON API (hub spec §12.1) under `/api/v1`. Milestone 0.1 exposes |
|
| 2 | + | //! repositories, bookmarks and tags; the rest follows the pages. |
|
| 3 | + | ||
| 4 | + | use crate::repos::{view, RepoError, RepoName}; |
|
| 5 | + | use crate::{db, Shared}; |
|
| 6 | + | use axum::extract::{Path, State}; |
|
| 7 | + | use axum::http::StatusCode; |
|
| 8 | + | use axum::response::{IntoResponse, Response}; |
|
| 9 | + | use axum::routing::get; |
|
| 10 | + | use axum::{Json, Router}; |
|
| 11 | + | use hub_api::{Bookmark, Problem, Repository, Tag}; |
|
| 12 | + | ||
| 13 | + | pub fn router() -> Router<Shared> { |
|
| 14 | + | Router::new() |
|
| 15 | + | .route("/repos", get(list_repos)) |
|
| 16 | + | .route("/repos/{owner}/{name}", get(get_repo)) |
|
| 17 | + | .route("/repos/{owner}/{name}/bookmarks", get(bookmarks)) |
|
| 18 | + | .route("/repos/{owner}/{name}/tags", get(tags)) |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | pub struct ApiError(StatusCode, String); |
|
| 22 | + | ||
| 23 | + | impl IntoResponse for ApiError { |
|
| 24 | + | fn into_response(self) -> Response { |
|
| 25 | + | let body = Problem { status: self.0.as_u16(), title: self.0.canonical_reason().unwrap_or("error").into(), detail: self.1 }; |
|
| 26 | + | (self.0, [("content-type", "application/problem+json")], Json(body)).into_response() |
|
| 27 | + | } |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | impl From<RepoError> for ApiError { |
|
| 31 | + | fn from(e: RepoError) -> ApiError { |
|
| 32 | + | match e { |
|
| 33 | + | RepoError::NotFound => ApiError(StatusCode::NOT_FOUND, "no such repository".into()), |
|
| 34 | + | other => ApiError(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()), |
|
| 35 | + | } |
|
| 36 | + | } |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | impl From<sqlx::Error> for ApiError { |
|
| 40 | + | fn from(e: sqlx::Error) -> ApiError { |
|
| 41 | + | ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) |
|
| 42 | + | } |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | fn repository(app: &Shared, r: &db::repos::RepoRow) -> Repository { |
|
| 46 | + | Repository { |
|
| 47 | + | owner: r.owner.clone(), |
|
| 48 | + | name: r.name.clone(), |
|
| 49 | + | description: r.description.clone(), |
|
| 50 | + | default_bookmark: r.default_bookmark.clone(), |
|
| 51 | + | visibility: r.visibility(), |
|
| 52 | + | created_ms: r.created_ms, |
|
| 53 | + | updated_ms: r.updated_ms, |
|
| 54 | + | clone_url: format!("{}/{}/{}", app.config.base_url(), r.owner, r.name), |
|
| 55 | + | } |
|
| 56 | + | } |
|
| 57 | + | ||
| 58 | + | async fn public_row(app: &Shared, owner: &str, name: &str) -> Result<(RepoName, db::repos::RepoRow), ApiError> { |
|
| 59 | + | let repo = RepoName::new(owner, name).ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?; |
|
| 60 | + | let row = db::repos::get(&app.db, owner, name).await?.filter(|r| r.visibility() == hub_api::Visibility::Public); |
|
| 61 | + | Ok((repo, row.ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?)) |
|
| 62 | + | } |
|
| 63 | + | ||
| 64 | + | async fn list_repos(State(app): State<Shared>) -> Result<Json<Vec<Repository>>, ApiError> { |
|
| 65 | + | let rows = db::repos::list(&app.db).await?; |
|
| 66 | + | Ok(Json(rows.iter().filter(|r| r.visibility() == hub_api::Visibility::Public).map(|r| repository(&app, r)).collect())) |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | async fn get_repo(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Repository>, ApiError> { |
|
| 70 | + | let (_, row) = public_row(&app, &owner, &name).await?; |
|
| 71 | + | Ok(Json(repository(&app, &row))) |
|
| 72 | + | } |
|
| 73 | + | ||
| 74 | + | async fn bookmarks(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Vec<Bookmark>>, ApiError> { |
|
| 75 | + | let (repo, row) = public_row(&app, &owner, &name).await?; |
|
| 76 | + | let default = row.default_bookmark.clone(); |
|
| 77 | + | let list = app.repos.read(&repo, move |r| view::bookmarks(r, &default)).await?; |
|
| 78 | + | Ok(Json(list.into_iter().map(|b| Bookmark { name: b.name, heads: b.heads, changes: b.changes, default: b.default }).collect())) |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | async fn tags(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Vec<Tag>>, ApiError> { |
|
| 82 | + | let (repo, _) = public_row(&app, &owner, &name).await?; |
|
| 83 | + | let list = app.repos.read(&repo, view::tags).await?; |
|
| 84 | + | Ok(Json(list.into_iter().map(|t| Tag { name: t.name, heads: t.heads, message: t.message, signer: t.signer }).collect())) |
|
| 85 | + | } |
added crates/hub/src/config.rs +277 −0
| 1 | + | //! Configuration (hub spec §17.1): `hub.toml` in the data directory, every |
|
| 2 | + | //! key overridable by `OXYHUB_<SECTION>_<KEY>`. |
|
| 3 | + | ||
| 4 | + | use serde::Deserialize; |
|
| 5 | + | use std::net::SocketAddr; |
|
| 6 | + | use std::path::{Path, PathBuf}; |
|
| 7 | + | ||
| 8 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 9 | + | #[serde(default, deny_unknown_fields)] |
|
| 10 | + | pub struct Config { |
|
| 11 | + | /// Set from the command line, never from the file. |
|
| 12 | + | #[serde(skip)] |
|
| 13 | + | pub data: PathBuf, |
|
| 14 | + | pub site: Site, |
|
| 15 | + | pub server: Server, |
|
| 16 | + | pub tls: Tls, |
|
| 17 | + | pub auth: Auth, |
|
| 18 | + | pub limits: Limits, |
|
| 19 | + | pub retention: Retention, |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 23 | + | #[serde(default, deny_unknown_fields)] |
|
| 24 | + | pub struct Site { |
|
| 25 | + | pub name: String, |
|
| 26 | + | pub description: String, |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 30 | + | #[serde(default, deny_unknown_fields)] |
|
| 31 | + | pub struct Server { |
|
| 32 | + | /// Plain HTTP; empty disables it. |
|
| 33 | + | pub bind: String, |
|
| 34 | + | /// HTTPS; empty disables it. Needs `tls`. |
|
| 35 | + | pub tls_bind: String, |
|
| 36 | + | /// What links and clone URLs start with. |
|
| 37 | + | pub base_url: String, |
|
| 38 | + | /// Trust `X-Forwarded-*` from a terminating proxy. |
|
| 39 | + | pub behind_proxy: bool, |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 43 | + | #[serde(default, deny_unknown_fields)] |
|
| 44 | + | pub struct Tls { |
|
| 45 | + | /// Generate a certificate at `init` (developer edition). |
|
| 46 | + | pub self_signed: bool, |
|
| 47 | + | /// PEM files; relative paths are under the data directory. |
|
| 48 | + | pub cert: String, |
|
| 49 | + | pub key: String, |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | #[derive(Clone, Debug, Default, Deserialize)] |
|
| 53 | + | #[serde(default, deny_unknown_fields)] |
|
| 54 | + | pub struct Auth { |
|
| 55 | + | /// Bearer tokens allowed to push until accounts exist (hub 0.2). Empty |
|
| 56 | + | /// means every push is accepted, as `oxy serve` does. |
|
| 57 | + | pub push_tokens: Vec<String>, |
|
| 58 | + | } |
|
| 59 | + | ||
| 60 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 61 | + | #[serde(default, deny_unknown_fields)] |
|
| 62 | + | pub struct Limits { |
|
| 63 | + | /// Largest file rendered in the browser, e.g. `2MiB`. |
|
| 64 | + | pub render_limit: ByteSize, |
|
| 65 | + | pub asset_limit: ByteSize, |
|
| 66 | + | pub push_body_limit: ByteSize, |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | #[derive(Clone, Debug, Deserialize)] |
|
| 70 | + | #[serde(default, deny_unknown_fields)] |
|
| 71 | + | pub struct Retention { |
|
| 72 | + | pub trash_days: u32, |
|
| 73 | + | pub webhook_log_days: u32, |
|
| 74 | + | } |
|
| 75 | + | ||
| 76 | + | /// A byte count written as `512`, `2MiB`, `4GiB`, `100kB`. |
|
| 77 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 78 | + | pub struct ByteSize(pub u64); |
|
| 79 | + | ||
| 80 | + | impl ByteSize { |
|
| 81 | + | pub fn parse(s: &str) -> Option<ByteSize> { |
|
| 82 | + | let s = s.trim(); |
|
| 83 | + | let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect(); |
|
| 84 | + | let unit = s[digits.len()..].trim(); |
|
| 85 | + | let n: u64 = digits.parse().ok()?; |
|
| 86 | + | let mult = match unit { |
|
| 87 | + | "" | "B" => 1, |
|
| 88 | + | "k" | "kB" | "KB" => 1000, |
|
| 89 | + | "KiB" | "K" => 1 << 10, |
|
| 90 | + | "M" | "MB" => 1_000_000, |
|
| 91 | + | "MiB" => 1 << 20, |
|
| 92 | + | "G" | "GB" => 1_000_000_000, |
|
| 93 | + | "GiB" => 1 << 30, |
|
| 94 | + | _ => return None, |
|
| 95 | + | }; |
|
| 96 | + | Some(ByteSize(n.checked_mul(mult)?)) |
|
| 97 | + | } |
|
| 98 | + | } |
|
| 99 | + | ||
| 100 | + | impl<'de> Deserialize<'de> for ByteSize { |
|
| 101 | + | fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<ByteSize, D::Error> { |
|
| 102 | + | #[derive(Deserialize)] |
|
| 103 | + | #[serde(untagged)] |
|
| 104 | + | enum Raw { |
|
| 105 | + | Number(u64), |
|
| 106 | + | Text(String), |
|
| 107 | + | } |
|
| 108 | + | match Raw::deserialize(d)? { |
|
| 109 | + | Raw::Number(n) => Ok(ByteSize(n)), |
|
| 110 | + | Raw::Text(t) => ByteSize::parse(&t).ok_or_else(|| serde::de::Error::custom(format!("not a byte size: {t:?}"))), |
|
| 111 | + | } |
|
| 112 | + | } |
|
| 113 | + | } |
|
| 114 | + | ||
| 115 | + | impl Default for Site { |
|
| 116 | + | fn default() -> Self { |
|
| 117 | + | Site { name: "Oxygenhub".into(), description: "Repositories, proposals and issues for Oxygen".into() } |
|
| 118 | + | } |
|
| 119 | + | } |
|
| 120 | + | ||
| 121 | + | impl Default for Server { |
|
| 122 | + | fn default() -> Self { |
|
| 123 | + | Server { bind: "127.0.0.1:7300".into(), tls_bind: String::new(), base_url: "http://127.0.0.1:7300".into(), behind_proxy: false } |
|
| 124 | + | } |
|
| 125 | + | } |
|
| 126 | + | ||
| 127 | + | impl Default for Tls { |
|
| 128 | + | fn default() -> Self { |
|
| 129 | + | Tls { self_signed: false, cert: "keys/tls.crt".into(), key: "keys/tls.key".into() } |
|
| 130 | + | } |
|
| 131 | + | } |
|
| 132 | + | ||
| 133 | + | impl Default for Limits { |
|
| 134 | + | fn default() -> Self { |
|
| 135 | + | Limits { render_limit: ByteSize(2 << 20), asset_limit: ByteSize(512 << 20), push_body_limit: ByteSize(4 << 30) } |
|
| 136 | + | } |
|
| 137 | + | } |
|
| 138 | + | ||
| 139 | + | impl Default for Retention { |
|
| 140 | + | fn default() -> Self { |
|
| 141 | + | Retention { trash_days: 30, webhook_log_days: 30 } |
|
| 142 | + | } |
|
| 143 | + | } |
|
| 144 | + | ||
| 145 | + | impl Default for Config { |
|
| 146 | + | fn default() -> Self { |
|
| 147 | + | Config { |
|
| 148 | + | data: PathBuf::from("data"), |
|
| 149 | + | site: Site::default(), |
|
| 150 | + | server: Server::default(), |
|
| 151 | + | tls: Tls::default(), |
|
| 152 | + | auth: Auth::default(), |
|
| 153 | + | limits: Limits::default(), |
|
| 154 | + | retention: Retention::default(), |
|
| 155 | + | } |
|
| 156 | + | } |
|
| 157 | + | } |
|
| 158 | + | ||
| 159 | + | impl Config { |
|
| 160 | + | /// Read `<data>/hub.toml` if present, then apply environment overrides. |
|
| 161 | + | pub fn load(data: &Path) -> anyhow::Result<Config> { |
|
| 162 | + | let path = data.join("hub.toml"); |
|
| 163 | + | let mut cfg = match std::fs::read_to_string(&path) { |
|
| 164 | + | Ok(text) => Config::parse(&text)?, |
|
| 165 | + | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(), |
|
| 166 | + | Err(e) => return Err(anyhow::anyhow!("reading {}: {e}", path.display())), |
|
| 167 | + | }; |
|
| 168 | + | cfg.data = data.to_path_buf(); |
|
| 169 | + | cfg.apply_env(|k| std::env::var(k).ok()); |
|
| 170 | + | Ok(cfg) |
|
| 171 | + | } |
|
| 172 | + | ||
| 173 | + | pub fn parse(text: &str) -> anyhow::Result<Config> { |
|
| 174 | + | Ok(toml::from_str(text)?) |
|
| 175 | + | } |
|
| 176 | + | ||
| 177 | + | /// `OXYHUB_SERVER_BIND`, `OXYHUB_SITE_NAME`, … override the file. |
|
| 178 | + | pub fn apply_env(&mut self, get: impl Fn(&str) -> Option<String>) { |
|
| 179 | + | if let Some(v) = get("OXYHUB_SITE_NAME") { |
|
| 180 | + | self.site.name = v; |
|
| 181 | + | } |
|
| 182 | + | if let Some(v) = get("OXYHUB_SITE_DESCRIPTION") { |
|
| 183 | + | self.site.description = v; |
|
| 184 | + | } |
|
| 185 | + | if let Some(v) = get("OXYHUB_SERVER_BIND") { |
|
| 186 | + | self.server.bind = v; |
|
| 187 | + | } |
|
| 188 | + | if let Some(v) = get("OXYHUB_SERVER_TLS_BIND") { |
|
| 189 | + | self.server.tls_bind = v; |
|
| 190 | + | } |
|
| 191 | + | if let Some(v) = get("OXYHUB_SERVER_BASE_URL") { |
|
| 192 | + | self.server.base_url = v; |
|
| 193 | + | } |
|
| 194 | + | if let Some(v) = get("OXYHUB_SERVER_BEHIND_PROXY") { |
|
| 195 | + | self.server.behind_proxy = matches!(v.as_str(), "1" | "true" | "yes"); |
|
| 196 | + | } |
|
| 197 | + | if let Some(v) = get("OXYHUB_TLS_SELF_SIGNED") { |
|
| 198 | + | self.tls.self_signed = matches!(v.as_str(), "1" | "true" | "yes"); |
|
| 199 | + | } |
|
| 200 | + | if let Some(v) = get("OXYHUB_AUTH_PUSH_TOKENS") { |
|
| 201 | + | self.auth.push_tokens = v.split(',').map(str::trim).filter(|s| !s.is_empty()).map(String::from).collect(); |
|
| 202 | + | } |
|
| 203 | + | if let Some(v) = get("OXYHUB_LIMITS_RENDER_LIMIT").and_then(|v| ByteSize::parse(&v)) { |
|
| 204 | + | self.limits.render_limit = v; |
|
| 205 | + | } |
|
| 206 | + | } |
|
| 207 | + | ||
| 208 | + | pub fn bind_addr(&self) -> anyhow::Result<Option<SocketAddr>> { |
|
| 209 | + | parse_addr(&self.server.bind) |
|
| 210 | + | } |
|
| 211 | + | ||
| 212 | + | pub fn tls_bind_addr(&self) -> anyhow::Result<Option<SocketAddr>> { |
|
| 213 | + | parse_addr(&self.server.tls_bind) |
|
| 214 | + | } |
|
| 215 | + | ||
| 216 | + | pub fn tls_cert(&self) -> PathBuf { |
|
| 217 | + | self.data.join(&self.tls.cert) |
|
| 218 | + | } |
|
| 219 | + | ||
| 220 | + | pub fn tls_key(&self) -> PathBuf { |
|
| 221 | + | self.data.join(&self.tls.key) |
|
| 222 | + | } |
|
| 223 | + | ||
| 224 | + | pub fn repos_dir(&self) -> PathBuf { |
|
| 225 | + | self.data.join("repos") |
|
| 226 | + | } |
|
| 227 | + | ||
| 228 | + | pub fn keys_dir(&self) -> PathBuf { |
|
| 229 | + | self.data.join("keys") |
|
| 230 | + | } |
|
| 231 | + | ||
| 232 | + | pub fn db_path(&self) -> PathBuf { |
|
| 233 | + | self.data.join("hub.db") |
|
| 234 | + | } |
|
| 235 | + | ||
| 236 | + | /// The base URL without a trailing slash. |
|
| 237 | + | pub fn base_url(&self) -> &str { |
|
| 238 | + | self.server.base_url.trim_end_matches('/') |
|
| 239 | + | } |
|
| 240 | + | } |
|
| 241 | + | ||
| 242 | + | fn parse_addr(s: &str) -> anyhow::Result<Option<SocketAddr>> { |
|
| 243 | + | if s.trim().is_empty() { |
|
| 244 | + | return Ok(None); |
|
| 245 | + | } |
|
| 246 | + | Ok(Some(s.parse().map_err(|e| anyhow::anyhow!("bind address {s:?}: {e}"))?)) |
|
| 247 | + | } |
|
| 248 | + | ||
| 249 | + | #[cfg(test)] |
|
| 250 | + | mod tests { |
|
| 251 | + | use super::*; |
|
| 252 | + | ||
| 253 | + | #[test] |
|
| 254 | + | fn byte_sizes() { |
|
| 255 | + | assert_eq!(ByteSize::parse("2MiB"), Some(ByteSize(2 << 20))); |
|
| 256 | + | assert_eq!(ByteSize::parse("512"), Some(ByteSize(512))); |
|
| 257 | + | assert_eq!(ByteSize::parse("4 GiB"), Some(ByteSize(4 << 30))); |
|
| 258 | + | assert_eq!(ByteSize::parse("100kB"), Some(ByteSize(100_000))); |
|
| 259 | + | assert_eq!(ByteSize::parse("lots"), None); |
|
| 260 | + | } |
|
| 261 | + | ||
| 262 | + | #[test] |
|
| 263 | + | fn file_then_environment() { |
|
| 264 | + | let mut cfg = Config::parse("[server]\nbind = \"0.0.0.0:7300\"\n[limits]\nrender_limit = \"1MiB\"\n").unwrap(); |
|
| 265 | + | assert_eq!(cfg.server.bind, "0.0.0.0:7300"); |
|
| 266 | + | assert_eq!(cfg.limits.render_limit, ByteSize(1 << 20)); |
|
| 267 | + | assert_eq!(cfg.site.name, "Oxygenhub"); |
|
| 268 | + | cfg.apply_env(|k| match k { |
|
| 269 | + | "OXYHUB_SITE_NAME" => Some("Earth".into()), |
|
| 270 | + | "OXYHUB_AUTH_PUSH_TOKENS" => Some("a, b".into()), |
|
| 271 | + | _ => None, |
|
| 272 | + | }); |
|
| 273 | + | assert_eq!(cfg.site.name, "Earth"); |
|
| 274 | + | assert_eq!(cfg.auth.push_tokens, ["a", "b"]); |
|
| 275 | + | assert!(Config::parse("[nope]\nx = 1\n").is_err(), "unknown sections are errors, not silence"); |
|
| 276 | + | } |
|
| 277 | + | } |
added crates/hub/src/db/mod.rs +29 −0
| 1 | + | //! SQLite through `sqlx` (hub spec §3.1, §17.2): WAL, foreign keys on, |
|
| 2 | + | //! migrations embedded and applied at start. One module per table group. |
|
| 3 | + | ||
| 4 | + | pub mod repos; |
|
| 5 | + | ||
| 6 | + | use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous}; |
|
| 7 | + | use sqlx::SqlitePool; |
|
| 8 | + | use std::path::Path; |
|
| 9 | + | ||
| 10 | + | pub type Pool = SqlitePool; |
|
| 11 | + | ||
| 12 | + | /// Open (creating if needed) and migrate the database at `path`. |
|
| 13 | + | pub async fn open(path: &Path) -> anyhow::Result<Pool> { |
|
| 14 | + | let options = SqliteConnectOptions::new() |
|
| 15 | + | .filename(path) |
|
| 16 | + | .create_if_missing(true) |
|
| 17 | + | .journal_mode(SqliteJournalMode::Wal) |
|
| 18 | + | .synchronous(SqliteSynchronous::Normal) |
|
| 19 | + | .foreign_keys(true) |
|
| 20 | + | .busy_timeout(std::time::Duration::from_secs(5)); |
|
| 21 | + | let pool = SqlitePoolOptions::new().max_connections(8).connect_with(options).await?; |
|
| 22 | + | sqlx::migrate!("./migrations").run(&pool).await?; |
|
| 23 | + | Ok(pool) |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | /// Milliseconds since the epoch, the hub's timestamp unit (as `oxy`'s). |
|
| 27 | + | pub fn now_ms() -> i64 { |
|
| 28 | + | std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0) |
|
| 29 | + | } |
added crates/hub/src/db/repos.rs +111 −0
| 1 | + | //! The `repositories` and `pushes` tables. |
|
| 2 | + | ||
| 3 | + | use super::{now_ms, Pool}; |
|
| 4 | + | use hub_api::Visibility; |
|
| 5 | + | use sqlx::FromRow; |
|
| 6 | + | ||
| 7 | + | #[derive(Clone, Debug, FromRow)] |
|
| 8 | + | pub struct RepoRow { |
|
| 9 | + | pub id: i64, |
|
| 10 | + | pub owner: String, |
|
| 11 | + | pub name: String, |
|
| 12 | + | pub description: String, |
|
| 13 | + | pub default_bookmark: String, |
|
| 14 | + | pub visibility: String, |
|
| 15 | + | pub created_ms: i64, |
|
| 16 | + | pub updated_ms: i64, |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | impl RepoRow { |
|
| 20 | + | pub fn visibility(&self) -> Visibility { |
|
| 21 | + | Visibility::parse(&self.visibility).unwrap_or(Visibility::Private) |
|
| 22 | + | } |
|
| 23 | + | ||
| 24 | + | pub fn slug(&self) -> String { |
|
| 25 | + | format!("{}/{}", self.owner, self.name) |
|
| 26 | + | } |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | #[derive(Clone, Debug, FromRow)] |
|
| 30 | + | pub struct PushRow { |
|
| 31 | + | pub id: i64, |
|
| 32 | + | pub at_ms: i64, |
|
| 33 | + | pub reference: String, |
|
| 34 | + | pub ok: bool, |
|
| 35 | + | pub reason: String, |
|
| 36 | + | } |
|
| 37 | + | ||
| 38 | + | pub async fn list(pool: &Pool) -> sqlx::Result<Vec<RepoRow>> { |
|
| 39 | + | sqlx::query_as("SELECT * FROM repositories ORDER BY updated_ms DESC, owner, name").fetch_all(pool).await |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | pub async fn list_by_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<RepoRow>> { |
|
| 43 | + | sqlx::query_as("SELECT * FROM repositories WHERE owner = ? ORDER BY updated_ms DESC, name").bind(owner).fetch_all(pool).await |
|
| 44 | + | } |
|
| 45 | + | ||
| 46 | + | pub async fn get(pool: &Pool, owner: &str, name: &str) -> sqlx::Result<Option<RepoRow>> { |
|
| 47 | + | sqlx::query_as("SELECT * FROM repositories WHERE owner = ? AND name = ?").bind(owner).bind(name).fetch_optional(pool).await |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | pub async fn create(pool: &Pool, owner: &str, name: &str, description: &str, visibility: Visibility) -> sqlx::Result<RepoRow> { |
|
| 51 | + | let now = now_ms(); |
|
| 52 | + | sqlx::query_as( |
|
| 53 | + | "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING *", |
|
| 54 | + | ) |
|
| 55 | + | .bind(owner) |
|
| 56 | + | .bind(name) |
|
| 57 | + | .bind(description) |
|
| 58 | + | .bind(visibility.as_str()) |
|
| 59 | + | .bind(now) |
|
| 60 | + | .bind(now) |
|
| 61 | + | .fetch_one(pool) |
|
| 62 | + | .await |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> { |
|
| 66 | + | sqlx::query("DELETE FROM repositories WHERE id = ?").bind(id).execute(pool).await?; |
|
| 67 | + | Ok(()) |
|
| 68 | + | } |
|
| 69 | + | ||
| 70 | + | pub async fn set_description(pool: &Pool, id: i64, description: &str) -> sqlx::Result<()> { |
|
| 71 | + | sqlx::query("UPDATE repositories SET description = ? WHERE id = ?").bind(description).bind(id).execute(pool).await?; |
|
| 72 | + | Ok(()) |
|
| 73 | + | } |
|
| 74 | + | ||
| 75 | + | pub async fn set_default_bookmark(pool: &Pool, id: i64, bookmark: &str) -> sqlx::Result<()> { |
|
| 76 | + | sqlx::query("UPDATE repositories SET default_bookmark = ? WHERE id = ?").bind(bookmark).bind(id).execute(pool).await?; |
|
| 77 | + | Ok(()) |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | pub async fn set_visibility(pool: &Pool, id: i64, visibility: Visibility) -> sqlx::Result<()> { |
|
| 81 | + | sqlx::query("UPDATE repositories SET visibility = ? WHERE id = ?").bind(visibility.as_str()).bind(id).execute(pool).await?; |
|
| 82 | + | Ok(()) |
|
| 83 | + | } |
|
| 84 | + | ||
| 85 | + | /// Record a push's per-reference results and bump the repository's activity. |
|
| 86 | + | pub async fn record_push(pool: &Pool, id: i64, results: &[(String, bool, String)]) -> sqlx::Result<()> { |
|
| 87 | + | let now = now_ms(); |
|
| 88 | + | let mut tx = pool.begin().await?; |
|
| 89 | + | for (reference, ok, reason) in results { |
|
| 90 | + | sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason) VALUES (?, ?, ?, ?, ?)") |
|
| 91 | + | .bind(id) |
|
| 92 | + | .bind(now) |
|
| 93 | + | .bind(reference) |
|
| 94 | + | .bind(ok) |
|
| 95 | + | .bind(reason) |
|
| 96 | + | .execute(&mut *tx) |
|
| 97 | + | .await?; |
|
| 98 | + | } |
|
| 99 | + | if results.iter().any(|(_, ok, _)| *ok) { |
|
| 100 | + | sqlx::query("UPDATE repositories SET updated_ms = ? WHERE id = ?").bind(now).bind(id).execute(&mut *tx).await?; |
|
| 101 | + | } |
|
| 102 | + | tx.commit().await |
|
| 103 | + | } |
|
| 104 | + | ||
| 105 | + | pub async fn pushes(pool: &Pool, id: i64, limit: i64) -> sqlx::Result<Vec<PushRow>> { |
|
| 106 | + | sqlx::query_as("SELECT id, at_ms, reference, ok, reason FROM pushes WHERE repo_id = ? ORDER BY at_ms DESC, id DESC LIMIT ?") |
|
| 107 | + | .bind(id) |
|
| 108 | + | .bind(limit) |
|
| 109 | + | .fetch_all(pool) |
|
| 110 | + | .await |
|
| 111 | + | } |
added crates/hub/src/keys.rs +100 −0
| 1 | + | //! The hub's own material under `<data>/keys/` (hub spec §15.5): its |
|
| 2 | + | //! delegate identity, published at `/.well-known/oxyhub-key`, and the |
|
| 3 | + | //! developer edition's self-signed TLS certificate. |
|
| 4 | + | ||
| 5 | + | use oxy_core::hash::KeyId; |
|
| 6 | + | use oxy_core::identity::{Identity, SecretKey}; |
|
| 7 | + | use std::fs; |
|
| 8 | + | use std::path::Path; |
|
| 9 | + | ||
| 10 | + | const IDENTITY_FILE: &str = "hub.identity"; |
|
| 11 | + | const SECRET_FILE: &str = "hub.secret"; |
|
| 12 | + | ||
| 13 | + | /// The hub identity: public object and key id; the secret stays on disk |
|
| 14 | + | /// until something needs to sign (hub 0.6). |
|
| 15 | + | #[derive(Clone, Debug)] |
|
| 16 | + | pub struct HubKey { |
|
| 17 | + | pub key: KeyId, |
|
| 18 | + | pub identity: Identity, |
|
| 19 | + | pub bytes: Vec<u8>, |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | /// Create the identity if absent, and return it. |
|
| 23 | + | pub fn ensure_identity(keys: &Path, site_name: &str, base_url: &str) -> anyhow::Result<HubKey> { |
|
| 24 | + | fs::create_dir_all(keys)?; |
|
| 25 | + | let id_path = keys.join(IDENTITY_FILE); |
|
| 26 | + | if id_path.exists() { |
|
| 27 | + | return load_identity(keys); |
|
| 28 | + | } |
|
| 29 | + | let secret = SecretKey::generate(); |
|
| 30 | + | let created = crate::db::now_ms() as u64; |
|
| 31 | + | let host = base_url.split("://").nth(1).unwrap_or(base_url).split('/').next().unwrap_or("hub"); |
|
| 32 | + | let identity = secret.identity(&format!("{site_name} (hub delegate)"), &format!("hub@{host}"), created); |
|
| 33 | + | let key = identity.hash(); |
|
| 34 | + | write_private(&keys.join(SECRET_FILE), &secret.encode(&key))?; |
|
| 35 | + | write_private(&id_path, &identity.encode())?; |
|
| 36 | + | Ok(HubKey { key, bytes: identity.encode(), identity }) |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | pub fn load_identity(keys: &Path) -> anyhow::Result<HubKey> { |
|
| 40 | + | let bytes = fs::read(keys.join(IDENTITY_FILE))?; |
|
| 41 | + | let identity = Identity::decode(&bytes)?; |
|
| 42 | + | Ok(HubKey { key: identity.hash(), identity, bytes }) |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | /// Generate a self-signed certificate for the base URL's host (and |
|
| 46 | + | /// localhost) if none exists. Returns whether one was written. |
|
| 47 | + | pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result<bool> { |
|
| 48 | + | if cert.exists() && key.exists() { |
|
| 49 | + | return Ok(false); |
|
| 50 | + | } |
|
| 51 | + | let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string(); |
|
| 52 | + | let mut names = vec![host.clone(), "localhost".to_string()]; |
|
| 53 | + | if let Some(short) = host.split('.').next() { |
|
| 54 | + | if short != host { |
|
| 55 | + | names.push(short.to_string()); |
|
| 56 | + | } |
|
| 57 | + | } |
|
| 58 | + | let ck = rcgen::generate_simple_self_signed(names)?; |
|
| 59 | + | if let Some(dir) = cert.parent() { |
|
| 60 | + | fs::create_dir_all(dir)?; |
|
| 61 | + | } |
|
| 62 | + | fs::write(cert, ck.cert.pem())?; |
|
| 63 | + | write_private(key, ck.signing_key.serialize_pem().as_bytes())?; |
|
| 64 | + | Ok(true) |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> { |
|
| 68 | + | fs::write(path, bytes)?; |
|
| 69 | + | #[cfg(unix)] |
|
| 70 | + | { |
|
| 71 | + | use std::os::unix::fs::PermissionsExt; |
|
| 72 | + | fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; |
|
| 73 | + | } |
|
| 74 | + | Ok(()) |
|
| 75 | + | } |
|
| 76 | + | ||
| 77 | + | #[cfg(test)] |
|
| 78 | + | mod tests { |
|
| 79 | + | use super::*; |
|
| 80 | + | ||
| 81 | + | #[test] |
|
| 82 | + | fn identity_is_created_once_and_reloaded() { |
|
| 83 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 84 | + | let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap(); |
|
| 85 | + | let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap(); |
|
| 86 | + | assert_eq!(a.key, b.key); |
|
| 87 | + | assert_eq!(a.identity.email, "[email protected]:7343"); |
|
| 88 | + | assert!(dir.path().join(SECRET_FILE).exists()); |
|
| 89 | + | } |
|
| 90 | + | ||
| 91 | + | #[test] |
|
| 92 | + | fn self_signed_certificate() { |
|
| 93 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 94 | + | let cert = dir.path().join("keys/tls.crt"); |
|
| 95 | + | let key = dir.path().join("keys/tls.key"); |
|
| 96 | + | assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); |
|
| 97 | + | assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); |
|
| 98 | + | assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE")); |
|
| 99 | + | } |
|
| 100 | + | } |
added crates/hub/src/lib.rs +76 −0
| 1 | + | //! Oxygenhub: a forge for Oxygen repositories (`docs/spec.md`). |
|
| 2 | + | //! |
|
| 3 | + | //! One process: an axum router serving pages, the JSON API and the wire |
|
| 4 | + | //! protocol; SQLite for hub state; repositories as bare Oxygen stores. |
|
| 5 | + | ||
| 6 | + | pub mod api; |
|
| 7 | + | pub mod config; |
|
| 8 | + | pub mod db; |
|
| 9 | + | pub mod keys; |
|
| 10 | + | pub mod protocol; |
|
| 11 | + | pub mod render; |
|
| 12 | + | pub mod repos; |
|
| 13 | + | pub mod web; |
|
| 14 | + | ||
| 15 | + | use crate::config::Config; |
|
| 16 | + | use crate::repos::Repos; |
|
| 17 | + | use std::sync::Arc; |
|
| 18 | + | ||
| 19 | + | /// Everything a request handler can reach. |
|
| 20 | + | pub struct App { |
|
| 21 | + | pub config: Config, |
|
| 22 | + | pub db: db::Pool, |
|
| 23 | + | pub repos: Arc<Repos>, |
|
| 24 | + | pub server: Arc<oxy_server::Server>, |
|
| 25 | + | pub hub_key: keys::HubKey, |
|
| 26 | + | pub started: std::time::Instant, |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | pub type Shared = Arc<App>; |
|
| 30 | + | ||
| 31 | + | /// Open the data directory and build the application state. `init` must |
|
| 32 | + | /// have run once; `serve` and the tests call this. |
|
| 33 | + | pub async fn build(config: Config) -> anyhow::Result<Shared> { |
|
| 34 | + | std::fs::create_dir_all(config.repos_dir())?; |
|
| 35 | + | let db = db::open(&config.db_path()).await?; |
|
| 36 | + | let repos = Arc::new(Repos::new(config.repos_dir())); |
|
| 37 | + | let hub_key = keys::ensure_identity(&config.keys_dir(), &config.site.name, config.base_url())?; |
|
| 38 | + | let backend = protocol::HubBackend::new(repos.clone(), db.clone(), config.auth.push_tokens.clone(), tokio::runtime::Handle::current()); |
|
| 39 | + | let server = oxy_server::Server::new(backend); |
|
| 40 | + | Ok(Arc::new(App { config, db, repos, server, hub_key, started: std::time::Instant::now() })) |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | /// The full router: pages, API, protocol and static assets. |
|
| 44 | + | pub fn router(app: Shared) -> axum::Router { |
|
| 45 | + | web::router(app) |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// Serve on the configured addresses until the process is told to stop. |
|
| 49 | + | pub async fn serve(app: Shared) -> anyhow::Result<()> { |
|
| 50 | + | let router = router(app.clone()); |
|
| 51 | + | let http = app.config.bind_addr()?; |
|
| 52 | + | let https = app.config.tls_bind_addr()?; |
|
| 53 | + | if http.is_none() && https.is_none() { |
|
| 54 | + | anyhow::bail!("nothing to bind: set server.bind or server.tls_bind"); |
|
| 55 | + | } |
|
| 56 | + | let mut tasks = tokio::task::JoinSet::new(); |
|
| 57 | + | if let Some(addr) = http { |
|
| 58 | + | let listener = tokio::net::TcpListener::bind(addr).await?; |
|
| 59 | + | tracing::info!("http on {addr}"); |
|
| 60 | + | let r = router.clone(); |
|
| 61 | + | tasks.spawn(async move { axum::serve(listener, r).await.map_err(anyhow::Error::from) }); |
|
| 62 | + | } |
|
| 63 | + | if let Some(addr) = https { |
|
| 64 | + | let _ = rustls::crypto::ring::default_provider().install_default(); |
|
| 65 | + | let tls = axum_server::tls_rustls::RustlsConfig::from_pem_file(app.config.tls_cert(), app.config.tls_key()) |
|
| 66 | + | .await |
|
| 67 | + | .map_err(|e| anyhow::anyhow!("loading TLS files: {e}"))?; |
|
| 68 | + | tracing::info!("https on {addr}"); |
|
| 69 | + | let r = router.clone(); |
|
| 70 | + | tasks.spawn(async move { axum_server::bind_rustls(addr, tls).serve(r.into_make_service()).await.map_err(anyhow::Error::from) }); |
|
| 71 | + | } |
|
| 72 | + | tokio::select! { |
|
| 73 | + | r = tasks.join_next() => { r.transpose()?.transpose()?; Ok(()) } |
|
| 74 | + | _ = tokio::signal::ctrl_c() => { tracing::info!("stopping"); Ok(()) } |
|
| 75 | + | } |
|
| 76 | + | } |
added crates/hub/src/main.rs +213 −0
| 1 | + | //! `oxygenhub`: `init`, `serve`, `admin`, `check` (hub spec §17.3). |
|
| 2 | + | ||
| 3 | + | use clap::{Parser, Subcommand}; |
|
| 4 | + | use hub_api::Visibility; |
|
| 5 | + | use oxygenhub::config::Config; |
|
| 6 | + | use oxygenhub::repos::RepoName; |
|
| 7 | + | use oxygenhub::{db, keys}; |
|
| 8 | + | use std::path::PathBuf; |
|
| 9 | + | ||
| 10 | + | #[derive(Parser)] |
|
| 11 | + | #[command(name = "oxygenhub", version, about = "A forge for Oxygen repositories")] |
|
| 12 | + | struct Cli { |
|
| 13 | + | /// The data directory (hub spec §3.2) |
|
| 14 | + | #[arg(long, global = true, env = "OXYHUB_DATA", default_value = "data")] |
|
| 15 | + | data: PathBuf, |
|
| 16 | + | #[command(subcommand)] |
|
| 17 | + | cmd: Cmd, |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | #[derive(Subcommand)] |
|
| 21 | + | enum Cmd { |
|
| 22 | + | /// Create the data directory, keys, database and a default hub.toml |
|
| 23 | + | Init { |
|
| 24 | + | /// What links and clone URLs start with |
|
| 25 | + | #[arg(long)] |
|
| 26 | + | base_url: Option<String>, |
|
| 27 | + | /// Generate a self-signed TLS certificate (developer edition) |
|
| 28 | + | #[arg(long)] |
|
| 29 | + | self_signed: bool, |
|
| 30 | + | }, |
|
| 31 | + | /// Run the hub |
|
| 32 | + | Serve, |
|
| 33 | + | /// Offline administration |
|
| 34 | + | Admin { |
|
| 35 | + | #[command(subcommand)] |
|
| 36 | + | cmd: AdminCmd, |
|
| 37 | + | }, |
|
| 38 | + | /// Verify every repository's objects and signatures |
|
| 39 | + | Check, |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | #[derive(Subcommand)] |
|
| 43 | + | enum AdminCmd { |
|
| 44 | + | /// Repositories |
|
| 45 | + | Repo { |
|
| 46 | + | #[command(subcommand)] |
|
| 47 | + | cmd: RepoCmd, |
|
| 48 | + | }, |
|
| 49 | + | } |
|
| 50 | + | ||
| 51 | + | #[derive(Subcommand)] |
|
| 52 | + | enum RepoCmd { |
|
| 53 | + | /// Create an empty repository OWNER/NAME |
|
| 54 | + | Create { |
|
| 55 | + | slug: String, |
|
| 56 | + | #[arg(long, default_value = "")] |
|
| 57 | + | description: String, |
|
| 58 | + | #[arg(long, default_value = "public")] |
|
| 59 | + | visibility: String, |
|
| 60 | + | /// Bind to this repository id now (32 hex characters, from `.oxy/id`); |
|
| 61 | + | /// otherwise the first push binds it |
|
| 62 | + | #[arg(long)] |
|
| 63 | + | id: Option<String>, |
|
| 64 | + | }, |
|
| 65 | + | /// List repositories |
|
| 66 | + | List, |
|
| 67 | + | /// Move a repository to the trash |
|
| 68 | + | Delete { slug: String }, |
|
| 69 | + | /// Change the description, default bookmark or visibility |
|
| 70 | + | Set { |
|
| 71 | + | slug: String, |
|
| 72 | + | #[arg(long)] |
|
| 73 | + | description: Option<String>, |
|
| 74 | + | #[arg(long)] |
|
| 75 | + | default_bookmark: Option<String>, |
|
| 76 | + | #[arg(long)] |
|
| 77 | + | visibility: Option<String>, |
|
| 78 | + | }, |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | #[tokio::main] |
|
| 82 | + | async fn main() -> anyhow::Result<()> { |
|
| 83 | + | tracing_subscriber::fmt() |
|
| 84 | + | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env().add_directive("oxygenhub=info".parse()?)) |
|
| 85 | + | .init(); |
|
| 86 | + | let cli = Cli::parse(); |
|
| 87 | + | match cli.cmd { |
|
| 88 | + | Cmd::Init { base_url, self_signed } => init(&cli.data, base_url, self_signed).await, |
|
| 89 | + | Cmd::Serve => { |
|
| 90 | + | let config = Config::load(&cli.data)?; |
|
| 91 | + | let app = oxygenhub::build(config).await?; |
|
| 92 | + | oxygenhub::serve(app).await |
|
| 93 | + | } |
|
| 94 | + | Cmd::Admin { cmd: AdminCmd::Repo { cmd } } => { |
|
| 95 | + | let config = Config::load(&cli.data)?; |
|
| 96 | + | let app = oxygenhub::build(config).await?; |
|
| 97 | + | repo_admin(&app, cmd).await |
|
| 98 | + | } |
|
| 99 | + | Cmd::Check => { |
|
| 100 | + | let config = Config::load(&cli.data)?; |
|
| 101 | + | let app = oxygenhub::build(config).await?; |
|
| 102 | + | check(&app).await |
|
| 103 | + | } |
|
| 104 | + | } |
|
| 105 | + | } |
|
| 106 | + | ||
| 107 | + | async fn init(data: &PathBuf, base_url: Option<String>, self_signed: bool) -> anyhow::Result<()> { |
|
| 108 | + | std::fs::create_dir_all(data)?; |
|
| 109 | + | let toml_path = data.join("hub.toml"); |
|
| 110 | + | if !toml_path.exists() { |
|
| 111 | + | let base = base_url.clone().unwrap_or_else(|| "http://127.0.0.1:7300".into()); |
|
| 112 | + | let mut text = String::from("# Oxygenhub configuration (docs/spec.md section 17.1)\n\n[site]\nname = \"Oxygenhub\"\n\n[server]\nbind = \"127.0.0.1:7300\"\n"); |
|
| 113 | + | if self_signed { |
|
| 114 | + | text.push_str("tls_bind = \"0.0.0.0:7343\"\n"); |
|
| 115 | + | } |
|
| 116 | + | text.push_str(&format!("base_url = \"{base}\"\n\n[tls]\nself_signed = {self_signed}\n\n[auth]\n# Bearer tokens allowed to push until accounts exist.\npush_tokens = []\n")); |
|
| 117 | + | std::fs::write(&toml_path, text)?; |
|
| 118 | + | println!("wrote {}", toml_path.display()); |
|
| 119 | + | } |
|
| 120 | + | let mut config = Config::load(data)?; |
|
| 121 | + | if let Some(b) = base_url { |
|
| 122 | + | config.server.base_url = b; |
|
| 123 | + | } |
|
| 124 | + | let app = oxygenhub::build(config).await?; |
|
| 125 | + | println!("hub key {}", app.hub_key.key.to_hex()); |
|
| 126 | + | if (self_signed || app.config.tls.self_signed) |
|
| 127 | + | && keys::ensure_self_signed(&app.config.tls_cert(), &app.config.tls_key(), app.config.base_url())? |
|
| 128 | + | { |
|
| 129 | + | println!("wrote self-signed certificate {}", app.config.tls_cert().display()); |
|
| 130 | + | } |
|
| 131 | + | println!("data directory {} is ready", data.display()); |
|
| 132 | + | Ok(()) |
|
| 133 | + | } |
|
| 134 | + | ||
| 135 | + | async fn repo_admin(app: &oxygenhub::Shared, cmd: RepoCmd) -> anyhow::Result<()> { |
|
| 136 | + | match cmd { |
|
| 137 | + | RepoCmd::Create { slug, description, visibility, id } => { |
|
| 138 | + | let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?; |
|
| 139 | + | let visibility = Visibility::parse(&visibility).ok_or_else(|| anyhow::anyhow!("visibility is public, internal or private"))?; |
|
| 140 | + | let id = match id { |
|
| 141 | + | Some(h) => { |
|
| 142 | + | let bytes = hex::decode(h.trim()).ok().and_then(|v| <[u8; 16]>::try_from(v).ok()); |
|
| 143 | + | Some(bytes.ok_or_else(|| anyhow::anyhow!("--id takes 32 hex characters"))?) |
|
| 144 | + | } |
|
| 145 | + | None => None, |
|
| 146 | + | }; |
|
| 147 | + | if db::repos::get(&app.db, &name.owner, &name.name).await?.is_some() { |
|
| 148 | + | anyhow::bail!("{name} already exists"); |
|
| 149 | + | } |
|
| 150 | + | app.repos.create(&name, id)?; |
|
| 151 | + | db::repos::create(&app.db, &name.owner, &name.name, &description, visibility).await?; |
|
| 152 | + | println!("created {name}: oxy clone {}/{name}", app.config.base_url()); |
|
| 153 | + | } |
|
| 154 | + | RepoCmd::List => { |
|
| 155 | + | for r in db::repos::list(&app.db).await? { |
|
| 156 | + | println!("{:<40} {:<8} {}", r.slug(), r.visibility, r.description); |
|
| 157 | + | } |
|
| 158 | + | } |
|
| 159 | + | RepoCmd::Delete { slug } => { |
|
| 160 | + | let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?; |
|
| 161 | + | let row = db::repos::get(&app.db, &name.owner, &name.name).await?.ok_or_else(|| anyhow::anyhow!("{name} does not exist"))?; |
|
| 162 | + | let dest = app.repos.trash(&name)?; |
|
| 163 | + | db::repos::delete(&app.db, row.id).await?; |
|
| 164 | + | println!("moved {name} to {}", dest.display()); |
|
| 165 | + | } |
|
| 166 | + | RepoCmd::Set { slug, description, default_bookmark, visibility } => { |
|
| 167 | + | let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?; |
|
| 168 | + | let row = db::repos::get(&app.db, &name.owner, &name.name).await?.ok_or_else(|| anyhow::anyhow!("{name} does not exist"))?; |
|
| 169 | + | if let Some(d) = description { |
|
| 170 | + | db::repos::set_description(&app.db, row.id, &d).await?; |
|
| 171 | + | } |
|
| 172 | + | if let Some(b) = default_bookmark { |
|
| 173 | + | db::repos::set_default_bookmark(&app.db, row.id, &b).await?; |
|
| 174 | + | } |
|
| 175 | + | if let Some(v) = visibility { |
|
| 176 | + | let v = Visibility::parse(&v).ok_or_else(|| anyhow::anyhow!("visibility is public, internal or private"))?; |
|
| 177 | + | db::repos::set_visibility(&app.db, row.id, v).await?; |
|
| 178 | + | } |
|
| 179 | + | println!("updated {name}"); |
|
| 180 | + | } |
|
| 181 | + | } |
|
| 182 | + | Ok(()) |
|
| 183 | + | } |
|
| 184 | + | ||
| 185 | + | async fn check(app: &oxygenhub::Shared) -> anyhow::Result<()> { |
|
| 186 | + | let mut problems = 0; |
|
| 187 | + | for r in db::repos::list(&app.db).await? { |
|
| 188 | + | let name = RepoName::new(&r.owner, &r.name).unwrap(); |
|
| 189 | + | let report = app.repos.read(&name, oxygenhub::repos::view::verification).await; |
|
| 190 | + | match report { |
|
| 191 | + | Ok(v) => { |
|
| 192 | + | println!( |
|
| 193 | + | "{:<40} {} changes, {} signed, {} sealed, {} unsigned, {} invalid", |
|
| 194 | + | name, |
|
| 195 | + | v.total, |
|
| 196 | + | v.signed, |
|
| 197 | + | v.sealed, |
|
| 198 | + | v.unsigned, |
|
| 199 | + | v.invalid.len() |
|
| 200 | + | ); |
|
| 201 | + | problems += v.invalid.len(); |
|
| 202 | + | } |
|
| 203 | + | Err(e) => { |
|
| 204 | + | println!("{name:<40} ERROR {e}"); |
|
| 205 | + | problems += 1; |
|
| 206 | + | } |
|
| 207 | + | } |
|
| 208 | + | } |
|
| 209 | + | if problems > 0 { |
|
| 210 | + | anyhow::bail!("{problems} problem(s)"); |
|
| 211 | + | } |
|
| 212 | + | Ok(()) |
|
| 213 | + | } |
added crates/hub/src/protocol.rs +115 −0
| 1 | + | //! The wire protocol, embedded (hub spec §3.3, §5.4): `oxy-server`'s |
|
| 2 | + | //! `Server` over a backend that resolves `owner/name`, applies visibility |
|
| 3 | + | //! and push tokens, and records pushes. |
|
| 4 | + | ||
| 5 | + | use crate::repos::{RepoName, Repos}; |
|
| 6 | + | use crate::{db, Shared}; |
|
| 7 | + | use axum::body::Bytes; |
|
| 8 | + | use axum::extract::{Path, State}; |
|
| 9 | + | use axum::http::{HeaderMap, Method, StatusCode}; |
|
| 10 | + | use axum::response::{IntoResponse, Response}; |
|
| 11 | + | use hub_api::Visibility; |
|
| 12 | + | use oxy_core::protocol::RefResult; |
|
| 13 | + | use oxy_core::repo::Repo; |
|
| 14 | + | use oxy_server::{Access, Backend, Endpoint, Failure}; |
|
| 15 | + | use std::sync::Arc; |
|
| 16 | + | ||
| 17 | + | pub struct HubBackend { |
|
| 18 | + | repos: Arc<Repos>, |
|
| 19 | + | pool: db::Pool, |
|
| 20 | + | push_tokens: Vec<String>, |
|
| 21 | + | runtime: tokio::runtime::Handle, |
|
| 22 | + | } |
|
| 23 | + | ||
| 24 | + | impl HubBackend { |
|
| 25 | + | pub fn new(repos: Arc<Repos>, pool: db::Pool, push_tokens: Vec<String>, runtime: tokio::runtime::Handle) -> HubBackend { |
|
| 26 | + | HubBackend { repos, pool, push_tokens, runtime } |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | fn row(&self, name: &RepoName) -> Result<db::repos::RepoRow, Failure> { |
|
| 30 | + | let pool = self.pool.clone(); |
|
| 31 | + | let (owner, repo) = (name.owner.clone(), name.name.clone()); |
|
| 32 | + | let row = tokio::task::block_in_place(|| self.runtime.block_on(db::repos::get(&pool, &owner, &repo))); |
|
| 33 | + | match row { |
|
| 34 | + | Ok(Some(r)) => Ok(r), |
|
| 35 | + | Ok(None) => Err(not_found()), |
|
| 36 | + | Err(e) => Err(Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), |
|
| 37 | + | } |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | fn has_push_token(&self, headers: &HeaderMap) -> bool { |
|
| 41 | + | match oxy_server::bearer_token(headers) { |
|
| 42 | + | Some(t) => self.push_tokens.iter().any(|x| x == t), |
|
| 43 | + | None => false, |
|
| 44 | + | } |
|
| 45 | + | } |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | fn not_found() -> Failure { |
|
| 49 | + | Failure(StatusCode::NOT_FOUND, "no such repository".into()) |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | impl Backend for HubBackend { |
|
| 53 | + | fn open(&self, name: &str) -> Result<Repo, Failure> { |
|
| 54 | + | let name = RepoName::parse(name).ok_or_else(not_found)?; |
|
| 55 | + | self.repos.open(&name).map_err(|e| match e { |
|
| 56 | + | crate::repos::RepoError::NotFound => not_found(), |
|
| 57 | + | other => Failure(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()), |
|
| 58 | + | }) |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | fn authorize(&self, name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> { |
|
| 62 | + | let parsed = RepoName::parse(name).ok_or_else(not_found)?; |
|
| 63 | + | let row = self.row(&parsed)?; |
|
| 64 | + | let token = self.has_push_token(headers); |
|
| 65 | + | // Until accounts exist (hub 0.2) a push token is the only credential: |
|
| 66 | + | // it reads everything and may push everywhere. |
|
| 67 | + | if row.visibility() != Visibility::Public && !token { |
|
| 68 | + | return Err(not_found()); |
|
| 69 | + | } |
|
| 70 | + | match access { |
|
| 71 | + | Access::Read => Ok(()), |
|
| 72 | + | Access::Push if self.push_tokens.is_empty() || token => Ok(()), |
|
| 73 | + | Access::Push => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())), |
|
| 74 | + | } |
|
| 75 | + | } |
|
| 76 | + | ||
| 77 | + | fn after_push(&self, name: &str, results: &[RefResult]) { |
|
| 78 | + | let Some(parsed) = RepoName::parse(name) else { return }; |
|
| 79 | + | let Ok(row) = self.row(&parsed) else { return }; |
|
| 80 | + | let pool = self.pool.clone(); |
|
| 81 | + | let results: Vec<(String, bool, String)> = results.iter().map(|r| (r.name.clone(), r.ok, r.reason.clone())).collect(); |
|
| 82 | + | self.runtime.spawn(async move { |
|
| 83 | + | if let Err(e) = db::repos::record_push(&pool, row.id, &results).await { |
|
| 84 | + | tracing::warn!("recording push to {}: {e}", parsed); |
|
| 85 | + | } |
|
| 86 | + | }); |
|
| 87 | + | } |
|
| 88 | + | } |
|
| 89 | + | ||
| 90 | + | /// `/{owner}/{name}/oxy/v1/{endpoint}`. |
|
| 91 | + | pub async fn dispatch( |
|
| 92 | + | State(app): State<Shared>, |
|
| 93 | + | Path((owner, name, endpoint)): Path<(String, String, String)>, |
|
| 94 | + | method: Method, |
|
| 95 | + | headers: HeaderMap, |
|
| 96 | + | body: Bytes, |
|
| 97 | + | ) -> Response { |
|
| 98 | + | let Some(endpoint) = Endpoint::parse(&endpoint) else { |
|
| 99 | + | return Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(); |
|
| 100 | + | }; |
|
| 101 | + | let Some(repo) = RepoName::new(&owner, &name) else { |
|
| 102 | + | return not_found().into_response(); |
|
| 103 | + | }; |
|
| 104 | + | // Pushes and checkpoints write; everything else reads. |
|
| 105 | + | let _guard = match endpoint { |
|
| 106 | + | Endpoint::Push | Endpoint::Checkpoint => Guard::Write(app.repos.write_guard(&repo).await), |
|
| 107 | + | _ => Guard::Read(app.repos.read_guard(&repo).await), |
|
| 108 | + | }; |
|
| 109 | + | app.server.handle(repo.to_string(), endpoint, &method, &headers, body).await |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | enum Guard { |
|
| 113 | + | Read(#[allow(dead_code)] tokio::sync::OwnedRwLockReadGuard<()>), |
|
| 114 | + | Write(#[allow(dead_code)] tokio::sync::OwnedRwLockWriteGuard<()>), |
|
| 115 | + | } |
added crates/hub/src/render/diff.rs +235 −0
| 1 | + | //! Diff rendering: `oxy`'s per-file hunks become unified rows with |
|
| 2 | + | //! context, and two snapshots become per-file hunks. |
|
| 3 | + | ||
| 4 | + | use oxy_core::repo::{DiffHunk, FileDiff}; |
|
| 5 | + | use oxy_core::state::{FileContent, Snapshot}; |
|
| 6 | + | use similar::{Algorithm, DiffOp}; |
|
| 7 | + | use std::collections::BTreeMap; |
|
| 8 | + | ||
| 9 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 10 | + | pub enum Kind { |
|
| 11 | + | Context, |
|
| 12 | + | Add, |
|
| 13 | + | Del, |
|
| 14 | + | /// Elided lines between hunks. |
|
| 15 | + | Gap, |
|
| 16 | + | } |
|
| 17 | + | ||
| 18 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 19 | + | pub struct Line { |
|
| 20 | + | pub kind: Kind, |
|
| 21 | + | pub old_no: Option<u32>, |
|
| 22 | + | pub new_no: Option<u32>, |
|
| 23 | + | /// Raw text, not escaped. |
|
| 24 | + | pub text: String, |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | impl Kind { |
|
| 28 | + | pub fn class(self) -> &'static str { |
|
| 29 | + | match self { |
|
| 30 | + | Kind::Context => "ctx", |
|
| 31 | + | Kind::Add => "add", |
|
| 32 | + | Kind::Del => "del", |
|
| 33 | + | Kind::Gap => "gap", |
|
| 34 | + | } |
|
| 35 | + | } |
|
| 36 | + | ||
| 37 | + | pub fn sign(self) -> &'static str { |
|
| 38 | + | match self { |
|
| 39 | + | Kind::Context => " ", |
|
| 40 | + | Kind::Add => "+", |
|
| 41 | + | Kind::Del => "-", |
|
| 42 | + | Kind::Gap => "…", |
|
| 43 | + | } |
|
| 44 | + | } |
|
| 45 | + | } |
|
| 46 | + | ||
| 47 | + | fn text(b: &[u8]) -> String { |
|
| 48 | + | String::from_utf8_lossy(b).trim_end_matches(['\n', '\r']).to_string() |
|
| 49 | + | } |
|
| 50 | + | ||
| 51 | + | /// Unified rows for one file: `base` is the file before the change (empty |
|
| 52 | + | /// for an added file) and `hunks` come from `oxy diff`, whose `base_line` |
|
| 53 | + | /// is the 1-based line the additions follow (0 = before the first line). |
|
| 54 | + | pub fn unified(base: &[u8], hunks: &[DiffHunk], context: usize) -> Vec<Line> { |
|
| 55 | + | let base_lines: Vec<String> = oxy_core::diff::split_lines(base).into_iter().map(text).collect(); |
|
| 56 | + | // Turn hunks into a per-old-line plan: removed set, and additions keyed |
|
| 57 | + | // by the base line they follow. |
|
| 58 | + | let mut removed = std::collections::HashSet::new(); |
|
| 59 | + | let mut added: BTreeMap<u32, Vec<String>> = BTreeMap::new(); |
|
| 60 | + | for h in hunks { |
|
| 61 | + | for (line, _) in &h.removed { |
|
| 62 | + | removed.insert(*line); |
|
| 63 | + | } |
|
| 64 | + | added.entry(h.base_line).or_default().extend(h.added.iter().map(|b| text(b))); |
|
| 65 | + | } |
|
| 66 | + | // Walk the base, emitting rows; then trim to context around changes. |
|
| 67 | + | let mut rows: Vec<Line> = Vec::new(); |
|
| 68 | + | let mut new_no: u32 = 0; |
|
| 69 | + | if let Some(lines) = added.get(&0) { |
|
| 70 | + | for l in lines { |
|
| 71 | + | new_no += 1; |
|
| 72 | + | rows.push(Line { kind: Kind::Add, old_no: None, new_no: Some(new_no), text: l.clone() }); |
|
| 73 | + | } |
|
| 74 | + | } |
|
| 75 | + | for (i, l) in base_lines.iter().enumerate() { |
|
| 76 | + | let old_no = i as u32 + 1; |
|
| 77 | + | if removed.contains(&old_no) { |
|
| 78 | + | rows.push(Line { kind: Kind::Del, old_no: Some(old_no), new_no: None, text: l.clone() }); |
|
| 79 | + | } else { |
|
| 80 | + | new_no += 1; |
|
| 81 | + | rows.push(Line { kind: Kind::Context, old_no: Some(old_no), new_no: Some(new_no), text: l.clone() }); |
|
| 82 | + | } |
|
| 83 | + | if let Some(lines) = added.get(&old_no) { |
|
| 84 | + | for a in lines { |
|
| 85 | + | new_no += 1; |
|
| 86 | + | rows.push(Line { kind: Kind::Add, old_no: None, new_no: Some(new_no), text: a.clone() }); |
|
| 87 | + | } |
|
| 88 | + | } |
|
| 89 | + | } |
|
| 90 | + | // Any additions anchored past the end (defensive). |
|
| 91 | + | for (anchor, lines) in &added { |
|
| 92 | + | if *anchor as usize > base_lines.len() { |
|
| 93 | + | for a in lines { |
|
| 94 | + | new_no += 1; |
|
| 95 | + | rows.push(Line { kind: Kind::Add, old_no: None, new_no: Some(new_no), text: a.clone() }); |
|
| 96 | + | } |
|
| 97 | + | } |
|
| 98 | + | } |
|
| 99 | + | with_context(rows, context) |
|
| 100 | + | } |
|
| 101 | + | ||
| 102 | + | /// Keep `context` rows around every change, replacing longer runs with a gap. |
|
| 103 | + | fn with_context(rows: Vec<Line>, context: usize) -> Vec<Line> { |
|
| 104 | + | let changed: Vec<bool> = rows.iter().map(|r| r.kind != Kind::Context).collect(); |
|
| 105 | + | let mut keep = vec![false; rows.len()]; |
|
| 106 | + | for (i, c) in changed.iter().enumerate() { |
|
| 107 | + | if *c { |
|
| 108 | + | let lo = i.saturating_sub(context); |
|
| 109 | + | let hi = (i + context + 1).min(rows.len()); |
|
| 110 | + | for k in keep.iter_mut().take(hi).skip(lo) { |
|
| 111 | + | *k = true; |
|
| 112 | + | } |
|
| 113 | + | } |
|
| 114 | + | } |
|
| 115 | + | let mut out = Vec::new(); |
|
| 116 | + | let mut gap = 0; |
|
| 117 | + | for (row, k) in rows.into_iter().zip(keep) { |
|
| 118 | + | if k { |
|
| 119 | + | if gap > 0 { |
|
| 120 | + | out.push(Line { kind: Kind::Gap, old_no: None, new_no: None, text: format!("{gap} unchanged lines") }); |
|
| 121 | + | gap = 0; |
|
| 122 | + | } |
|
| 123 | + | out.push(row); |
|
| 124 | + | } else { |
|
| 125 | + | gap += 1; |
|
| 126 | + | } |
|
| 127 | + | } |
|
| 128 | + | if gap > 0 { |
|
| 129 | + | out.push(Line { kind: Kind::Gap, old_no: None, new_no: None, text: format!("{gap} unchanged lines") }); |
|
| 130 | + | } |
|
| 131 | + | out |
|
| 132 | + | } |
|
| 133 | + | ||
| 134 | + | /// Hunks between two texts, in `oxy diff`'s shape. |
|
| 135 | + | pub fn hunks_between(old: &[u8], new: &[u8]) -> Vec<DiffHunk> { |
|
| 136 | + | let old_lines = oxy_core::diff::split_lines(old); |
|
| 137 | + | let new_lines = oxy_core::diff::split_lines(new); |
|
| 138 | + | let ops = similar::capture_diff_slices(Algorithm::Myers, &old_lines, &new_lines); |
|
| 139 | + | let mut hunks: Vec<DiffHunk> = Vec::new(); |
|
| 140 | + | for op in ops { |
|
| 141 | + | match op { |
|
| 142 | + | DiffOp::Equal { .. } => {} |
|
| 143 | + | DiffOp::Delete { old_index, old_len, .. } => { |
|
| 144 | + | hunks.push(DiffHunk { |
|
| 145 | + | base_line: (old_index + old_len) as u32, |
|
| 146 | + | added: vec![], |
|
| 147 | + | removed: (old_index..old_index + old_len).map(|i| (i as u32 + 1, old_lines[i].to_vec())).collect(), |
|
| 148 | + | }); |
|
| 149 | + | } |
|
| 150 | + | DiffOp::Insert { old_index, new_index, new_len } => { |
|
| 151 | + | hunks.push(DiffHunk { |
|
| 152 | + | base_line: old_index as u32, |
|
| 153 | + | added: (new_index..new_index + new_len).map(|i| new_lines[i].to_vec()).collect(), |
|
| 154 | + | removed: vec![], |
|
| 155 | + | }); |
|
| 156 | + | } |
|
| 157 | + | DiffOp::Replace { old_index, old_len, new_index, new_len } => { |
|
| 158 | + | hunks.push(DiffHunk { |
|
| 159 | + | base_line: (old_index + old_len) as u32, |
|
| 160 | + | added: (new_index..new_index + new_len).map(|i| new_lines[i].to_vec()).collect(), |
|
| 161 | + | removed: (old_index..old_index + old_len).map(|i| (i as u32 + 1, old_lines[i].to_vec())).collect(), |
|
| 162 | + | }); |
|
| 163 | + | } |
|
| 164 | + | } |
|
| 165 | + | } |
|
| 166 | + | hunks |
|
| 167 | + | } |
|
| 168 | + | ||
| 169 | + | /// Every file that differs between two snapshots, with the base content |
|
| 170 | + | /// of files present in both (for context). |
|
| 171 | + | pub fn snapshot_diff(base: &Snapshot, head: &Snapshot) -> (Vec<FileDiff>, BTreeMap<String, Vec<u8>>) { |
|
| 172 | + | let mut files = Vec::new(); |
|
| 173 | + | let mut bases = BTreeMap::new(); |
|
| 174 | + | let text_of = |f: &oxy_core::state::FileState| match &f.content { |
|
| 175 | + | FileContent::Text { bytes, .. } => Some(bytes.clone()), |
|
| 176 | + | _ => None, |
|
| 177 | + | }; |
|
| 178 | + | for (path, hf) in &head.files { |
|
| 179 | + | if hf.kind == oxy_core::objects::NodeKind::Dir { |
|
| 180 | + | continue; |
|
| 181 | + | } |
|
| 182 | + | match base.files.get(path) { |
|
| 183 | + | None => { |
|
| 184 | + | let new = text_of(hf).unwrap_or_default(); |
|
| 185 | + | files.push(FileDiff { path: path.clone(), kind: "added".into(), hunks: hunks_between(b"", &new), from: None }); |
|
| 186 | + | } |
|
| 187 | + | Some(bf) => { |
|
| 188 | + | let (old, new) = (text_of(bf), text_of(hf)); |
|
| 189 | + | if old != new { |
|
| 190 | + | let old_b = old.clone().unwrap_or_default(); |
|
| 191 | + | let hunks = hunks_between(&old_b, &new.unwrap_or_default()); |
|
| 192 | + | if let Some(o) = old { |
|
| 193 | + | bases.insert(path.clone(), o); |
|
| 194 | + | } |
|
| 195 | + | files.push(FileDiff { path: path.clone(), kind: "modified".into(), hunks, from: None }); |
|
| 196 | + | } else if bf.mode != hf.mode { |
|
| 197 | + | files.push(FileDiff { path: path.clone(), kind: "mode".into(), hunks: vec![], from: None }); |
|
| 198 | + | } |
|
| 199 | + | } |
|
| 200 | + | } |
|
| 201 | + | } |
|
| 202 | + | for (path, bf) in &base.files { |
|
| 203 | + | if bf.kind != oxy_core::objects::NodeKind::Dir && !head.files.contains_key(path) { |
|
| 204 | + | let old = text_of(bf).unwrap_or_default(); |
|
| 205 | + | bases.insert(path.clone(), old.clone()); |
|
| 206 | + | files.push(FileDiff { path: path.clone(), kind: "removed".into(), hunks: hunks_between(&old, b""), from: None }); |
|
| 207 | + | } |
|
| 208 | + | } |
|
| 209 | + | files.sort_by(|a, b| a.path.cmp(&b.path)); |
|
| 210 | + | (files, bases) |
|
| 211 | + | } |
|
| 212 | + | ||
| 213 | + | #[cfg(test)] |
|
| 214 | + | mod tests { |
|
| 215 | + | use super::*; |
|
| 216 | + | ||
| 217 | + | #[test] |
|
| 218 | + | fn unified_rows_with_context() { |
|
| 219 | + | let base = b"a\nb\nc\nd\ne\nf\ng\nh\n"; |
|
| 220 | + | let hunks = hunks_between(base, b"a\nb\nc\nX\ne\nf\ng\nh\nZ\n"); |
|
| 221 | + | let rows = unified(base, &hunks, 1); |
|
| 222 | + | let s: Vec<String> = rows.iter().map(|r| format!("{}{}", r.kind.sign(), r.text)).collect(); |
|
| 223 | + | assert_eq!(s, ["…2 unchanged lines", " c", "-d", "+X", " e", "…2 unchanged lines", " h", "+Z"]); |
|
| 224 | + | assert_eq!(rows[2].old_no, Some(4)); |
|
| 225 | + | assert_eq!(rows[3].new_no, Some(4)); |
|
| 226 | + | assert_eq!(rows.last().unwrap().new_no, Some(9)); |
|
| 227 | + | } |
|
| 228 | + | ||
| 229 | + | #[test] |
|
| 230 | + | fn added_file() { |
|
| 231 | + | let hunks = hunks_between(b"", b"one\ntwo\n"); |
|
| 232 | + | let rows = unified(b"", &hunks, 3); |
|
| 233 | + | assert_eq!(rows.iter().map(|r| r.kind).collect::<Vec<_>>(), [Kind::Add, Kind::Add]); |
|
| 234 | + | } |
|
| 235 | + | } |
added crates/hub/src/render/graph.rs +118 −0
| 1 | + | //! Lane layout for the history graph (hub spec §6.7): each change takes a |
|
| 2 | + | //! lane, its first dependency continues that lane, other dependencies |
|
| 3 | + | //! take free lanes, so edges flow downward from a change to what it |
|
| 4 | + | //! depends on. Ported from the VS Code extension's webview. |
|
| 5 | + | ||
| 6 | + | use std::collections::HashMap; |
|
| 7 | + | ||
| 8 | + | /// A row of the graph: the change's lane and the edges to the rows below. |
|
| 9 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 10 | + | pub struct Row { |
|
| 11 | + | pub lane: usize, |
|
| 12 | + | /// `(from_lane, to_lane)` for each dependency drawn from this row. |
|
| 13 | + | pub edges: Vec<(usize, usize)>, |
|
| 14 | + | /// Lanes that pass through this row without touching it. |
|
| 15 | + | pub through: Vec<usize>, |
|
| 16 | + | } |
|
| 17 | + | ||
| 18 | + | /// `changes` newest first as `(hash, deps)`. Dependencies outside the list |
|
| 19 | + | /// end their lane at the bottom. |
|
| 20 | + | pub fn layout(changes: &[(String, Vec<String>)]) -> Vec<Row> { |
|
| 21 | + | let mut active: Vec<Option<String>> = Vec::new(); |
|
| 22 | + | let mut rows = Vec::with_capacity(changes.len()); |
|
| 23 | + | let known: std::collections::HashSet<&str> = changes.iter().map(|(h, _)| h.as_str()).collect(); |
|
| 24 | + | let mut lanes: HashMap<String, usize> = HashMap::new(); |
|
| 25 | + | for (hash, deps) in changes { |
|
| 26 | + | let lane = match active.iter().position(|a| a.as_deref() == Some(hash)) { |
|
| 27 | + | Some(l) => l, |
|
| 28 | + | None => match active.iter().position(|a| a.is_none()) { |
|
| 29 | + | Some(l) => { |
|
| 30 | + | active[l] = Some(hash.clone()); |
|
| 31 | + | l |
|
| 32 | + | } |
|
| 33 | + | None => { |
|
| 34 | + | active.push(Some(hash.clone())); |
|
| 35 | + | active.len() - 1 |
|
| 36 | + | } |
|
| 37 | + | }, |
|
| 38 | + | }; |
|
| 39 | + | lanes.insert(hash.clone(), lane); |
|
| 40 | + | let mut edges = Vec::new(); |
|
| 41 | + | let deps: Vec<&String> = deps.iter().filter(|d| known.contains(d.as_str())).collect(); |
|
| 42 | + | active[lane] = None; |
|
| 43 | + | for (i, d) in deps.iter().enumerate() { |
|
| 44 | + | let target = match active.iter().position(|a| a.as_deref() == Some(d.as_str())) { |
|
| 45 | + | Some(l) => l, |
|
| 46 | + | None => { |
|
| 47 | + | let l = if i == 0 && active[lane].is_none() { |
|
| 48 | + | lane |
|
| 49 | + | } else { |
|
| 50 | + | match active.iter().position(|a| a.is_none()) { |
|
| 51 | + | Some(l) => l, |
|
| 52 | + | None => { |
|
| 53 | + | active.push(None); |
|
| 54 | + | active.len() - 1 |
|
| 55 | + | } |
|
| 56 | + | } |
|
| 57 | + | }; |
|
| 58 | + | active[l] = Some((*d).clone()); |
|
| 59 | + | l |
|
| 60 | + | } |
|
| 61 | + | }; |
|
| 62 | + | edges.push((lane, target)); |
|
| 63 | + | } |
|
| 64 | + | let through: Vec<usize> = active |
|
| 65 | + | .iter() |
|
| 66 | + | .enumerate() |
|
| 67 | + | .filter(|(l, a)| a.is_some() && *l != lane && !edges.iter().any(|e| e.1 == *l)) |
|
| 68 | + | .map(|(l, _)| l) |
|
| 69 | + | .collect(); |
|
| 70 | + | rows.push(Row { lane, edges, through }); |
|
| 71 | + | // Trim trailing free lanes so the graph stays narrow. |
|
| 72 | + | while active.last().is_some_and(|a| a.is_none()) { |
|
| 73 | + | active.pop(); |
|
| 74 | + | } |
|
| 75 | + | } |
|
| 76 | + | rows |
|
| 77 | + | } |
|
| 78 | + | ||
| 79 | + | pub fn width(rows: &[Row]) -> usize { |
|
| 80 | + | rows.iter() |
|
| 81 | + | .map(|r| r.lane.max(r.edges.iter().map(|e| e.1).max().unwrap_or(0)).max(r.through.iter().copied().max().unwrap_or(0))) |
|
| 82 | + | .max() |
|
| 83 | + | .map(|m| m + 1) |
|
| 84 | + | .unwrap_or(1) |
|
| 85 | + | } |
|
| 86 | + | ||
| 87 | + | #[cfg(test)] |
|
| 88 | + | mod tests { |
|
| 89 | + | use super::*; |
|
| 90 | + | ||
| 91 | + | fn c(h: &str, deps: &[&str]) -> (String, Vec<String>) { |
|
| 92 | + | (h.into(), deps.iter().map(|d| d.to_string()).collect()) |
|
| 93 | + | } |
|
| 94 | + | ||
| 95 | + | #[test] |
|
| 96 | + | fn linear_history_is_one_lane() { |
|
| 97 | + | let rows = layout(&[c("c", &["b"]), c("b", &["a"]), c("a", &[])]); |
|
| 98 | + | assert!(rows.iter().all(|r| r.lane == 0), "{rows:?}"); |
|
| 99 | + | assert_eq!(rows[0].edges, vec![(0, 0)]); |
|
| 100 | + | assert_eq!(width(&rows), 1); |
|
| 101 | + | } |
|
| 102 | + | ||
| 103 | + | #[test] |
|
| 104 | + | fn independent_changes_take_lanes_and_rejoin() { |
|
| 105 | + | // d depends on b and c, which both depend on a: a diamond. |
|
| 106 | + | let rows = layout(&[c("d", &["b", "c"]), c("b", &["a"]), c("c", &["a"]), c("a", &[])]); |
|
| 107 | + | assert_eq!(rows[0].lane, 0); |
|
| 108 | + | assert_eq!(rows[0].edges, vec![(0, 0), (0, 1)]); |
|
| 109 | + | assert_eq!(rows[1].lane, 0); |
|
| 110 | + | assert_eq!(rows[2].lane, 1); |
|
| 111 | + | assert_eq!(rows[2].edges, vec![(1, 0)], "c joins a's lane"); |
|
| 112 | + | assert_eq!(rows[3].lane, 0); |
|
| 113 | + | assert_eq!(width(&rows), 2); |
|
| 114 | + | // Two heads with no common change: two lanes throughout. |
|
| 115 | + | let rows = layout(&[c("x", &[]), c("y", &[])]); |
|
| 116 | + | assert_eq!((rows[0].lane, rows[1].lane), (0, 0), "a lane is reused once its change is drawn"); |
|
| 117 | + | } |
|
| 118 | + | } |
added crates/hub/src/render/highlight.rs +102 −0
| 1 | + | //! Syntax highlighting with syntect, emitted as classed spans so the |
|
| 2 | + | //! stylesheet colours them in both themes (`templates/style.css`). |
|
| 3 | + | ||
| 4 | + | use std::sync::OnceLock; |
|
| 5 | + | use syntect::html::{ClassStyle, ClassedHTMLGenerator}; |
|
| 6 | + | use syntect::parsing::SyntaxSet; |
|
| 7 | + | use syntect::util::LinesWithEndings; |
|
| 8 | + | ||
| 9 | + | fn syntaxes() -> &'static SyntaxSet { |
|
| 10 | + | static SET: OnceLock<SyntaxSet> = OnceLock::new(); |
|
| 11 | + | SET.get_or_init(SyntaxSet::load_defaults_newlines) |
|
| 12 | + | } |
|
| 13 | + | ||
| 14 | + | /// The language name shown for a file, if syntect knows the extension. |
|
| 15 | + | pub fn language(path: &str) -> Option<&'static str> { |
|
| 16 | + | let ext = path.rsplit('.').next().unwrap_or(""); |
|
| 17 | + | let name = path.rsplit('/').next().unwrap_or(path); |
|
| 18 | + | let set = syntaxes(); |
|
| 19 | + | set.find_syntax_by_extension(ext) |
|
| 20 | + | .or_else(|| set.find_syntax_by_extension(name)) |
|
| 21 | + | .or_else(|| match name { |
|
| 22 | + | "Makefile" | "GNUmakefile" => set.find_syntax_by_name("Makefile"), |
|
| 23 | + | "Dockerfile" => set.find_syntax_by_name("Dockerfile"), |
|
| 24 | + | _ => None, |
|
| 25 | + | }) |
|
| 26 | + | .map(|s| s.name.as_str()) |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | /// Highlight `text`, returning one HTML fragment per line (no trailing |
|
| 30 | + | /// newline); plain escaped lines when the language is unknown. |
|
| 31 | + | pub fn lines(path: &str, text: &str) -> Vec<String> { |
|
| 32 | + | let set = syntaxes(); |
|
| 33 | + | let ext = path.rsplit('.').next().unwrap_or(""); |
|
| 34 | + | let name = path.rsplit('/').next().unwrap_or(path); |
|
| 35 | + | let syntax = set.find_syntax_by_extension(ext).or_else(|| set.find_syntax_by_extension(name)).or_else(|| match name { |
|
| 36 | + | "Makefile" | "GNUmakefile" => set.find_syntax_by_name("Makefile"), |
|
| 37 | + | _ => None, |
|
| 38 | + | }); |
|
| 39 | + | let Some(syntax) = syntax else { |
|
| 40 | + | return text.lines().map(super::escape).collect(); |
|
| 41 | + | }; |
|
| 42 | + | let mut generator = ClassedHTMLGenerator::new_with_class_style(syntax, set, ClassStyle::Spaced); |
|
| 43 | + | let mut out = Vec::new(); |
|
| 44 | + | for line in LinesWithEndings::from(text) { |
|
| 45 | + | if generator.parse_html_for_line_which_includes_newline(line).is_err() { |
|
| 46 | + | return text.lines().map(super::escape).collect(); |
|
| 47 | + | } |
|
| 48 | + | } |
|
| 49 | + | let html = generator.finalize(); |
|
| 50 | + | // syntect emits the whole file with spans that may span lines; split |
|
| 51 | + | // on newlines and keep spans balanced per line. |
|
| 52 | + | let mut open: Vec<String> = Vec::new(); |
|
| 53 | + | let mut raw_lines: Vec<&str> = html.split('\n').collect(); |
|
| 54 | + | // syntect closes the outermost spans after the final newline; that |
|
| 55 | + | // segment holds no text and every span is balanced per line below. |
|
| 56 | + | if raw_lines.last().is_some_and(|l| l.replace("</span>", "").trim().is_empty()) { |
|
| 57 | + | raw_lines.pop(); |
|
| 58 | + | } |
|
| 59 | + | for raw in raw_lines { |
|
| 60 | + | let mut line = String::new(); |
|
| 61 | + | for tag in &open { |
|
| 62 | + | line.push_str(tag); |
|
| 63 | + | } |
|
| 64 | + | line.push_str(raw); |
|
| 65 | + | // Track spans opened and closed on this line. |
|
| 66 | + | let mut i = 0; |
|
| 67 | + | while let Some(p) = raw[i..].find('<') { |
|
| 68 | + | let start = i + p; |
|
| 69 | + | let Some(end_rel) = raw[start..].find('>') else { break }; |
|
| 70 | + | let tag = &raw[start..start + end_rel + 1]; |
|
| 71 | + | if tag.starts_with("</") { |
|
| 72 | + | open.pop(); |
|
| 73 | + | } else if tag.starts_with("<span") { |
|
| 74 | + | open.push(tag.to_string()); |
|
| 75 | + | } |
|
| 76 | + | i = start + end_rel + 1; |
|
| 77 | + | } |
|
| 78 | + | for _ in &open { |
|
| 79 | + | line.push_str("</span>"); |
|
| 80 | + | } |
|
| 81 | + | out.push(line); |
|
| 82 | + | } |
|
| 83 | + | out |
|
| 84 | + | } |
|
| 85 | + | ||
| 86 | + | #[cfg(test)] |
|
| 87 | + | mod tests { |
|
| 88 | + | use super::*; |
|
| 89 | + | ||
| 90 | + | #[test] |
|
| 91 | + | fn highlights_rust_and_falls_back() { |
|
| 92 | + | let l = lines("main.rs", "fn main() {\n let x = 1;\n}\n"); |
|
| 93 | + | assert_eq!(l.len(), 3); |
|
| 94 | + | assert!(l[0].contains("<span class=\"source rust\">"), "{}", l[0]); |
|
| 95 | + | assert!(l[0].contains("storage type function"), "{}", l[0]); |
|
| 96 | + | assert!(l[1].ends_with("</span>"), "spans are balanced per line: {}", l[1]); |
|
| 97 | + | assert_eq!(lines("x.unknownext", "a<b\nc"), vec!["a<b", "c"]); |
|
| 98 | + | assert_eq!(language("a/b/lib.rs"), Some("Rust")); |
|
| 99 | + | assert_eq!(language("Makefile"), Some("Makefile")); |
|
| 100 | + | assert_eq!(language("blob.bin"), None); |
|
| 101 | + | } |
|
| 102 | + | } |
added crates/hub/src/render/markdown.rs +62 −0
| 1 | + | //! Markdown to sanitised HTML: pulldown-cmark, then ammonia with an |
|
| 2 | + | //! allow-list (hub spec §15.2). Relative links and images are rebased on |
|
| 3 | + | //! `base`, so a README's `docs/x.md` opens in the hub. |
|
| 4 | + | ||
| 5 | + | use pulldown_cmark::{html, Event, Options, Parser, Tag}; |
|
| 6 | + | ||
| 7 | + | /// `base_file` and `base_raw` are URL prefixes (ending in `/`) for links to |
|
| 8 | + | /// files and for images. |
|
| 9 | + | pub fn render(text: &str, base_file: &str, base_raw: &str) -> String { |
|
| 10 | + | let mut opts = Options::empty(); |
|
| 11 | + | opts.insert(Options::ENABLE_TABLES); |
|
| 12 | + | opts.insert(Options::ENABLE_STRIKETHROUGH); |
|
| 13 | + | opts.insert(Options::ENABLE_TASKLISTS); |
|
| 14 | + | opts.insert(Options::ENABLE_FOOTNOTES); |
|
| 15 | + | opts.insert(Options::ENABLE_HEADING_ATTRIBUTES); |
|
| 16 | + | let parser = Parser::new_ext(text, opts).map(|ev| match ev { |
|
| 17 | + | Event::Start(Tag::Link { link_type, dest_url, title, id }) => { |
|
| 18 | + | Event::Start(Tag::Link { link_type, dest_url: rebase(&dest_url, base_file).into(), title, id }) |
|
| 19 | + | } |
|
| 20 | + | Event::Start(Tag::Image { link_type, dest_url, title, id }) => { |
|
| 21 | + | Event::Start(Tag::Image { link_type, dest_url: rebase(&dest_url, base_raw).into(), title, id }) |
|
| 22 | + | } |
|
| 23 | + | other => other, |
|
| 24 | + | }); |
|
| 25 | + | let mut out = String::with_capacity(text.len() * 2); |
|
| 26 | + | html::push_html(&mut out, parser); |
|
| 27 | + | let mut builder = ammonia::Builder::default(); |
|
| 28 | + | builder.add_generic_attributes(["id", "class"]); |
|
| 29 | + | builder.add_tag_attributes("input", ["type", "checked", "disabled"]); |
|
| 30 | + | builder.add_tags(["input"]); |
|
| 31 | + | builder.link_rel(Some("noopener noreferrer")); |
|
| 32 | + | builder.clean(&out).to_string() |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | /// Leave absolute URLs, fragments and mail links alone; rebase the rest. |
|
| 36 | + | fn rebase(url: &str, base: &str) -> String { |
|
| 37 | + | if url.starts_with('#') || url.contains("://") || url.starts_with("mailto:") || url.starts_with('/') || url.starts_with("data:") { |
|
| 38 | + | return url.to_string(); |
|
| 39 | + | } |
|
| 40 | + | let url = url.strip_prefix("./").unwrap_or(url); |
|
| 41 | + | format!("{base}{url}") |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | #[cfg(test)] |
|
| 45 | + | mod tests { |
|
| 46 | + | use super::*; |
|
| 47 | + | ||
| 48 | + | #[test] |
|
| 49 | + | fn renders_and_sanitises() { |
|
| 50 | + | let html = render( |
|
| 51 | + | "# Hi\n\nSee [docs](docs/x.md) and  <script>alert(1)</script>\n\n- [ ] task\n", |
|
| 52 | + | "/a/b/file/main/", |
|
| 53 | + | "/a/b/file/main/", |
|
| 54 | + | ); |
|
| 55 | + | assert!(html.contains("<h1>Hi</h1>")); |
|
| 56 | + | assert!(html.contains("href=\"/a/b/file/main/docs/x.md\"")); |
|
| 57 | + | assert!(html.contains("src=\"/a/b/file/main/img.png\"")); |
|
| 58 | + | assert!(!html.contains("<script")); |
|
| 59 | + | assert!(html.contains("type=\"checkbox\"")); |
|
| 60 | + | assert!(render("[x](https://e.com) [y](#frag)", "/p/", "/p/").contains("href=\"https://e.com\"")); |
|
| 61 | + | } |
|
| 62 | + | } |
added crates/hub/src/render/mod.rs +54 −0
| 1 | + | //! Rendering helpers with no knowledge of routes: Markdown, syntax |
|
| 2 | + | //! highlighting, diffs, the history graph, and human times. Each is pure |
|
| 3 | + | //! and unit tested. |
|
| 4 | + | ||
| 5 | + | pub mod diff; |
|
| 6 | + | pub mod graph; |
|
| 7 | + | pub mod highlight; |
|
| 8 | + | pub mod markdown; |
|
| 9 | + | pub mod time; |
|
| 10 | + | ||
| 11 | + | /// HTML-escape text for insertion into element content or attributes. |
|
| 12 | + | pub fn escape(s: &str) -> String { |
|
| 13 | + | let mut out = String::with_capacity(s.len()); |
|
| 14 | + | for c in s.chars() { |
|
| 15 | + | match c { |
|
| 16 | + | '&' => out.push_str("&"), |
|
| 17 | + | '<' => out.push_str("<"), |
|
| 18 | + | '>' => out.push_str(">"), |
|
| 19 | + | '"' => out.push_str("""), |
|
| 20 | + | '\'' => out.push_str("'"), |
|
| 21 | + | _ => out.push(c), |
|
| 22 | + | } |
|
| 23 | + | } |
|
| 24 | + | out |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | /// `1.2 KiB`, `3.4 MiB`, `512 B`. |
|
| 28 | + | pub fn bytes(n: u64) -> String { |
|
| 29 | + | const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; |
|
| 30 | + | let mut v = n as f64; |
|
| 31 | + | let mut i = 0; |
|
| 32 | + | while v >= 1024.0 && i < UNITS.len() - 1 { |
|
| 33 | + | v /= 1024.0; |
|
| 34 | + | i += 1; |
|
| 35 | + | } |
|
| 36 | + | if i == 0 { |
|
| 37 | + | format!("{n} B") |
|
| 38 | + | } else { |
|
| 39 | + | format!("{v:.1} {}", UNITS[i]) |
|
| 40 | + | } |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | #[cfg(test)] |
|
| 44 | + | mod tests { |
|
| 45 | + | use super::*; |
|
| 46 | + | ||
| 47 | + | #[test] |
|
| 48 | + | fn escaping_and_sizes() { |
|
| 49 | + | assert_eq!(escape("<a href=\"x\">&'"), "<a href="x">&'"); |
|
| 50 | + | assert_eq!(bytes(512), "512 B"); |
|
| 51 | + | assert_eq!(bytes(1536), "1.5 KiB"); |
|
| 52 | + | assert_eq!(bytes(5 << 20), "5.0 MiB"); |
|
| 53 | + | } |
|
| 54 | + | } |
added crates/hub/src/render/time.rs +65 −0
| 1 | + | //! Times as people read them: relative ("3 hours ago") with the absolute |
|
| 2 | + | //! form for a tooltip. |
|
| 3 | + | ||
| 4 | + | use time::format_description::well_known::Rfc3339; |
|
| 5 | + | use time::OffsetDateTime; |
|
| 6 | + | ||
| 7 | + | /// `just now`, `5 minutes ago`, `3 days ago`, `2 years ago`. |
|
| 8 | + | pub fn ago(when_ms: u64, now_ms: u64) -> String { |
|
| 9 | + | if when_ms == 0 { |
|
| 10 | + | return "unknown".into(); |
|
| 11 | + | } |
|
| 12 | + | if when_ms > now_ms { |
|
| 13 | + | return "in the future".into(); |
|
| 14 | + | } |
|
| 15 | + | let s = (now_ms - when_ms) / 1000; |
|
| 16 | + | let (n, unit) = if s < 45 { |
|
| 17 | + | return "just now".into(); |
|
| 18 | + | } else if s < 90 { |
|
| 19 | + | (1, "minute") |
|
| 20 | + | } else if s < 3600 { |
|
| 21 | + | (s / 60, "minute") |
|
| 22 | + | } else if s < 86_400 { |
|
| 23 | + | (s / 3600, "hour") |
|
| 24 | + | } else if s < 86_400 * 30 { |
|
| 25 | + | (s / 86_400, "day") |
|
| 26 | + | } else if s < 86_400 * 365 { |
|
| 27 | + | (s / (86_400 * 30), "month") |
|
| 28 | + | } else { |
|
| 29 | + | (s / (86_400 * 365), "year") |
|
| 30 | + | }; |
|
| 31 | + | format!("{n} {unit}{} ago", if n == 1 { "" } else { "s" }) |
|
| 32 | + | } |
|
| 33 | + | ||
| 34 | + | /// `2026-09-27 17:02 UTC`. |
|
| 35 | + | pub fn absolute(when_ms: u64) -> String { |
|
| 36 | + | match OffsetDateTime::from_unix_timestamp((when_ms / 1000) as i64) { |
|
| 37 | + | Ok(t) => { |
|
| 38 | + | let f = time::macros::format_description!("[year]-[month]-[day] [hour]:[minute] UTC"); |
|
| 39 | + | t.format(&f).unwrap_or_default() |
|
| 40 | + | } |
|
| 41 | + | Err(_) => String::new(), |
|
| 42 | + | } |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | pub fn rfc3339(when_ms: u64) -> String { |
|
| 46 | + | OffsetDateTime::from_unix_timestamp((when_ms / 1000) as i64).ok().and_then(|t| t.format(&Rfc3339).ok()).unwrap_or_default() |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | #[cfg(test)] |
|
| 50 | + | mod tests { |
|
| 51 | + | use super::*; |
|
| 52 | + | ||
| 53 | + | #[test] |
|
| 54 | + | fn relative() { |
|
| 55 | + | let now = 1_800_000_000_000; |
|
| 56 | + | assert_eq!(ago(now - 10_000, now), "just now"); |
|
| 57 | + | assert_eq!(ago(now - 60_000, now), "1 minute ago"); |
|
| 58 | + | assert_eq!(ago(now - 5 * 60_000, now), "5 minutes ago"); |
|
| 59 | + | assert_eq!(ago(now - 3 * 3_600_000, now), "3 hours ago"); |
|
| 60 | + | assert_eq!(ago(now - 2 * 86_400_000, now), "2 days ago"); |
|
| 61 | + | assert_eq!(ago(now - 400 * 86_400_000, now), "1 year ago"); |
|
| 62 | + | assert_eq!(ago(0, now), "unknown"); |
|
| 63 | + | assert_eq!(absolute(1_758_990_120_000), "2025-09-27 16:22 UTC"); |
|
| 64 | + | } |
|
| 65 | + | } |
added crates/hub/src/repos/mod.rs +149 −0
| 1 | + | //! Repositories on disk (hub spec §4): naming, the registry of open |
|
| 2 | + | //! handles and their locks, and the page-ready views built from `oxy-core`. |
|
| 3 | + | //! |
|
| 4 | + | //! Every `oxy-core` call is synchronous and runs on the blocking pool under |
|
| 5 | + | //! the repository's lock (§16.2): pushes and other writes take the write |
|
| 6 | + | //! half, page reads the read half. |
|
| 7 | + | ||
| 8 | + | pub mod names; |
|
| 9 | + | pub mod view; |
|
| 10 | + | ||
| 11 | + | pub use names::RepoName; |
|
| 12 | + | ||
| 13 | + | use oxy_core::repo::Repo; |
|
| 14 | + | use std::collections::HashMap; |
|
| 15 | + | use std::path::{Path, PathBuf}; |
|
| 16 | + | use std::sync::{Arc, Mutex}; |
|
| 17 | + | use tokio::sync::RwLock; |
|
| 18 | + | ||
| 19 | + | /// Where repositories live and how their access is serialised. |
|
| 20 | + | pub struct Repos { |
|
| 21 | + | root: PathBuf, |
|
| 22 | + | locks: Mutex<HashMap<String, Arc<RwLock<()>>>>, |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | #[derive(Debug, thiserror::Error)] |
|
| 26 | + | pub enum RepoError { |
|
| 27 | + | #[error("no such repository")] |
|
| 28 | + | NotFound, |
|
| 29 | + | #[error("{0} already exists")] |
|
| 30 | + | Exists(String), |
|
| 31 | + | #[error("{0}")] |
|
| 32 | + | Oxy(#[from] oxy_core::repo::RepoError), |
|
| 33 | + | #[error("{0}")] |
|
| 34 | + | Store(#[from] oxy_core::store::StoreError), |
|
| 35 | + | #[error("{0}")] |
|
| 36 | + | Io(#[from] std::io::Error), |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | impl Repos { |
|
| 40 | + | pub fn new(root: PathBuf) -> Repos { |
|
| 41 | + | Repos { root, locks: Mutex::new(HashMap::new()) } |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | pub fn root(&self) -> &Path { |
|
| 45 | + | &self.root |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// `<root>/<owner>/<name>`, holding `.oxy/` and nothing else. |
|
| 49 | + | pub fn dir(&self, name: &RepoName) -> PathBuf { |
|
| 50 | + | self.root.join(&name.owner).join(&name.name) |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | pub fn exists(&self, name: &RepoName) -> bool { |
|
| 54 | + | self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir() |
|
| 55 | + | } |
|
| 56 | + | ||
| 57 | + | /// Open a hosted repository; it is always bare. |
|
| 58 | + | pub fn open(&self, name: &RepoName) -> Result<Repo, RepoError> { |
|
| 59 | + | if !self.exists(name) { |
|
| 60 | + | return Err(RepoError::NotFound); |
|
| 61 | + | } |
|
| 62 | + | Ok(Repo::open(&self.dir(name))?) |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | /// Create an empty bare repository. With no `id` it is **unbound**: the |
|
| 66 | + | /// first push binds it to the pusher's repository id, so an existing |
|
| 67 | + | /// repository keeps its identity when it moves here (hub spec §4.1). |
|
| 68 | + | pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result<Repo, RepoError> { |
|
| 69 | + | let dir = self.dir(name); |
|
| 70 | + | if dir.exists() { |
|
| 71 | + | return Err(RepoError::Exists(name.to_string())); |
|
| 72 | + | } |
|
| 73 | + | Ok(match id { |
|
| 74 | + | Some(id) => Repo::init_bare_with_id(&dir, id)?, |
|
| 75 | + | None => Repo::init_bare_unbound(&dir)?, |
|
| 76 | + | }) |
|
| 77 | + | } |
|
| 78 | + | ||
| 79 | + | /// Move a repository to the trash (hub spec §4.1); a job empties it later. |
|
| 80 | + | pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> { |
|
| 81 | + | let dir = self.dir(name); |
|
| 82 | + | if !dir.exists() { |
|
| 83 | + | return Err(RepoError::NotFound); |
|
| 84 | + | } |
|
| 85 | + | let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner); |
|
| 86 | + | std::fs::create_dir_all(&trash)?; |
|
| 87 | + | let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms())); |
|
| 88 | + | std::fs::rename(&dir, &dest)?; |
|
| 89 | + | Ok(dest) |
|
| 90 | + | } |
|
| 91 | + | ||
| 92 | + | fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> { |
|
| 93 | + | self.locks.lock().unwrap().entry(name.to_string()).or_default().clone() |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | /// Hold the read lock while `f` runs on the blocking pool. |
|
| 97 | + | pub async fn read<T: Send + 'static>( |
|
| 98 | + | &self, |
|
| 99 | + | name: &RepoName, |
|
| 100 | + | f: impl FnOnce(&Repo) -> Result<T, RepoError> + Send + 'static, |
|
| 101 | + | ) -> Result<T, RepoError> { |
|
| 102 | + | let lock = self.lock(name); |
|
| 103 | + | let guard = lock.read_owned().await; |
|
| 104 | + | let dir = self.dir(name); |
|
| 105 | + | let exists = self.exists(name); |
|
| 106 | + | tokio::task::spawn_blocking(move || { |
|
| 107 | + | let _guard = guard; |
|
| 108 | + | if !exists { |
|
| 109 | + | return Err(RepoError::NotFound); |
|
| 110 | + | } |
|
| 111 | + | let repo = Repo::open(&dir)?; |
|
| 112 | + | f(&repo) |
|
| 113 | + | }) |
|
| 114 | + | .await |
|
| 115 | + | .map_err(|e| RepoError::Io(std::io::Error::other(e)))? |
|
| 116 | + | } |
|
| 117 | + | ||
| 118 | + | /// Hold the write lock while `f` runs on the blocking pool. |
|
| 119 | + | pub async fn write<T: Send + 'static>( |
|
| 120 | + | &self, |
|
| 121 | + | name: &RepoName, |
|
| 122 | + | f: impl FnOnce(&Repo) -> Result<T, RepoError> + Send + 'static, |
|
| 123 | + | ) -> Result<T, RepoError> { |
|
| 124 | + | let lock = self.lock(name); |
|
| 125 | + | let guard = lock.write_owned().await; |
|
| 126 | + | let dir = self.dir(name); |
|
| 127 | + | let exists = self.exists(name); |
|
| 128 | + | tokio::task::spawn_blocking(move || { |
|
| 129 | + | let _guard = guard; |
|
| 130 | + | if !exists { |
|
| 131 | + | return Err(RepoError::NotFound); |
|
| 132 | + | } |
|
| 133 | + | let repo = Repo::open(&dir)?; |
|
| 134 | + | f(&repo) |
|
| 135 | + | }) |
|
| 136 | + | .await |
|
| 137 | + | .map_err(|e| RepoError::Io(std::io::Error::other(e)))? |
|
| 138 | + | } |
|
| 139 | + | ||
| 140 | + | /// The write lock alone, for the protocol server, whose handler opens |
|
| 141 | + | /// the repository itself. |
|
| 142 | + | pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> { |
|
| 143 | + | self.lock(name).write_owned().await |
|
| 144 | + | } |
|
| 145 | + | ||
| 146 | + | pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> { |
|
| 147 | + | self.lock(name).read_owned().await |
|
| 148 | + | } |
|
| 149 | + | } |
added crates/hub/src/repos/names.rs +95 −0
| 1 | + | //! Owner and repository names (hub spec §3.2) and the top-level path |
|
| 2 | + | //! segments no owner may take. |
|
| 3 | + | ||
| 4 | + | use std::fmt; |
|
| 5 | + | ||
| 6 | + | /// `owner/name`, both validated. |
|
| 7 | + | #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] |
|
| 8 | + | pub struct RepoName { |
|
| 9 | + | pub owner: String, |
|
| 10 | + | pub name: String, |
|
| 11 | + | } |
|
| 12 | + | ||
| 13 | + | /// Top-level path segments reserved for the hub itself. |
|
| 14 | + | pub const RESERVED: &[&str] = &[ |
|
| 15 | + | "api", |
|
| 16 | + | "static", |
|
| 17 | + | "login", |
|
| 18 | + | "logout", |
|
| 19 | + | "register", |
|
| 20 | + | "settings", |
|
| 21 | + | "explore", |
|
| 22 | + | "search", |
|
| 23 | + | "notifications", |
|
| 24 | + | "new", |
|
| 25 | + | "orgs", |
|
| 26 | + | "admin", |
|
| 27 | + | "style.css", |
|
| 28 | + | "favicon.ico", |
|
| 29 | + | "favicon.svg", |
|
| 30 | + | "health", |
|
| 31 | + | "assets", |
|
| 32 | + | ".well-known", |
|
| 33 | + | ]; |
|
| 34 | + | ||
| 35 | + | /// `[A-Za-z0-9][A-Za-z0-9._-]{0,63}`, not `.`/`..`, not ending in `.git`. |
|
| 36 | + | pub fn valid_segment(s: &str) -> bool { |
|
| 37 | + | let mut chars = s.chars(); |
|
| 38 | + | let Some(first) = chars.next() else { return false }; |
|
| 39 | + | if !first.is_ascii_alphanumeric() || s.len() > 64 { |
|
| 40 | + | return false; |
|
| 41 | + | } |
|
| 42 | + | if !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) { |
|
| 43 | + | return false; |
|
| 44 | + | } |
|
| 45 | + | s != "." && s != ".." && !s.ends_with(".git") |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | pub fn valid_owner(s: &str) -> bool { |
|
| 49 | + | valid_segment(s) && !RESERVED.contains(&s) |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | impl RepoName { |
|
| 53 | + | pub fn new(owner: &str, name: &str) -> Option<RepoName> { |
|
| 54 | + | if valid_owner(owner) && valid_segment(name) { |
|
| 55 | + | Some(RepoName { owner: owner.into(), name: name.into() }) |
|
| 56 | + | } else { |
|
| 57 | + | None |
|
| 58 | + | } |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | /// `owner/name`. |
|
| 62 | + | pub fn parse(slug: &str) -> Option<RepoName> { |
|
| 63 | + | let (owner, name) = slug.split_once('/')?; |
|
| 64 | + | RepoName::new(owner, name) |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | pub fn url(&self) -> String { |
|
| 68 | + | format!("/{}/{}", self.owner, self.name) |
|
| 69 | + | } |
|
| 70 | + | } |
|
| 71 | + | ||
| 72 | + | impl fmt::Display for RepoName { |
|
| 73 | + | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
| 74 | + | write!(f, "{}/{}", self.owner, self.name) |
|
| 75 | + | } |
|
| 76 | + | } |
|
| 77 | + | ||
| 78 | + | #[cfg(test)] |
|
| 79 | + | mod tests { |
|
| 80 | + | use super::*; |
|
| 81 | + | ||
| 82 | + | #[test] |
|
| 83 | + | fn names() { |
|
| 84 | + | assert!(RepoName::parse("andy/oxygen").is_some()); |
|
| 85 | + | assert!(RepoName::parse("andy/oxy.gen-2_x").is_some()); |
|
| 86 | + | assert!(RepoName::parse("api/x").is_none(), "reserved owner"); |
|
| 87 | + | assert!(RepoName::parse("andy/x.git").is_none(), "mirror URL"); |
|
| 88 | + | assert!(RepoName::parse("andy/..").is_none()); |
|
| 89 | + | assert!(RepoName::parse(".andy/x").is_none()); |
|
| 90 | + | assert!(RepoName::parse("andy/a b").is_none()); |
|
| 91 | + | assert!(RepoName::parse("andy").is_none()); |
|
| 92 | + | assert!(RepoName::parse(&format!("andy/{}", "a".repeat(65))).is_none()); |
|
| 93 | + | assert_eq!(RepoName::parse("andy/oxygen").unwrap().url(), "/andy/oxygen"); |
|
| 94 | + | } |
|
| 95 | + | } |
added crates/hub/src/repos/view.rs +885 −0
| 1 | + | //! Page-ready views of a repository, computed from `oxy-core` alone (hub |
|
| 2 | + | //! spec §1.4). Nothing here knows about HTTP or templates; every function |
|
| 3 | + | //! takes an open `Repo` and returns plain data. |
|
| 4 | + | ||
| 5 | + | use super::RepoError; |
|
| 6 | + | use oxy_core::hash::{ChangeHash, KeyId}; |
|
| 7 | + | use oxy_core::objects::{Change, NodeKind}; |
|
| 8 | + | use oxy_core::repo::{BlameLine, FileDiff, Repo, SignatureStatus}; |
|
| 9 | + | use oxy_core::state::{FileContent, Snapshot, State}; |
|
| 10 | + | use oxy_core::tree::TreeConflict; |
|
| 11 | + | use std::collections::{BTreeMap, BTreeSet, HashMap}; |
|
| 12 | + | ||
| 13 | + | type Result<T> = std::result::Result<T, RepoError>; |
|
| 14 | + | ||
| 15 | + | fn fail(msg: impl Into<String>) -> RepoError { |
|
| 16 | + | RepoError::Oxy(oxy_core::repo::RepoError::Msg(msg.into())) |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | // ------------------------------------------------------------------ refs |
|
| 20 | + | ||
| 21 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 22 | + | pub enum RefKind { |
|
| 23 | + | Bookmark, |
|
| 24 | + | Tag, |
|
| 25 | + | Change, |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | /// What a `{ref}` path segment resolved to. |
|
| 29 | + | #[derive(Clone, Debug)] |
|
| 30 | + | pub struct RefTarget { |
|
| 31 | + | /// The name as given (bookmark, tag) or the full hash. |
|
| 32 | + | pub label: String, |
|
| 33 | + | pub kind: RefKind, |
|
| 34 | + | pub heads: Vec<ChangeHash>, |
|
| 35 | + | } |
|
| 36 | + | ||
| 37 | + | /// Resolve a bookmark name, a tag name, or a change hash prefix. |
|
| 38 | + | pub fn resolve(repo: &Repo, spec: &str) -> Result<RefTarget> { |
|
| 39 | + | let view = repo.view()?; |
|
| 40 | + | if let Some(heads) = view.bookmarks.get(spec).map(|st| repo.store().get_state(st).map(|s| s.heads)).transpose()? { |
|
| 41 | + | return Ok(RefTarget { label: spec.into(), kind: RefKind::Bookmark, heads }); |
|
| 42 | + | } |
|
| 43 | + | if let Some(heads) = repo.tag_heads(spec)? { |
|
| 44 | + | return Ok(RefTarget { label: spec.into(), kind: RefKind::Tag, heads }); |
|
| 45 | + | } |
|
| 46 | + | let hash = resolve_change(repo, spec)?; |
|
| 47 | + | Ok(RefTarget { label: hash.to_hex(), kind: RefKind::Change, heads: vec![hash] }) |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | /// A change by full hash or unique hex prefix, searched over every |
|
| 51 | + | /// bookmark's state. |
|
| 52 | + | pub fn resolve_change(repo: &Repo, spec: &str) -> Result<ChangeHash> { |
|
| 53 | + | let spec = spec.trim_start_matches('#'); |
|
| 54 | + | if spec.len() < 4 || !spec.chars().all(|c| c.is_ascii_hexdigit()) { |
|
| 55 | + | return Err(fail(format!("{spec:?} is not a change"))); |
|
| 56 | + | } |
|
| 57 | + | if let Some(h) = ChangeHash::from_hex(spec) { |
|
| 58 | + | if repo.store().has(&h) { |
|
| 59 | + | return Ok(h); |
|
| 60 | + | } |
|
| 61 | + | } |
|
| 62 | + | let heads = all_heads(repo)?; |
|
| 63 | + | let state = repo.load_state(&heads)?; |
|
| 64 | + | let matches: Vec<ChangeHash> = state.changes().map(|(h, _)| *h).filter(|h| h.starts_with_hex(spec)).collect(); |
|
| 65 | + | match matches.len() { |
|
| 66 | + | 1 => Ok(matches[0]), |
|
| 67 | + | 0 => Err(fail(format!("no change matches {spec:?}"))), |
|
| 68 | + | _ => Err(fail(format!("{spec:?} is ambiguous"))), |
|
| 69 | + | } |
|
| 70 | + | } |
|
| 71 | + | ||
| 72 | + | /// The heads of every bookmark, deduplicated. |
|
| 73 | + | pub fn all_heads(repo: &Repo) -> Result<Vec<ChangeHash>> { |
|
| 74 | + | let mut set = BTreeSet::new(); |
|
| 75 | + | for b in repo.bookmarks()? { |
|
| 76 | + | set.extend(b.heads); |
|
| 77 | + | } |
|
| 78 | + | Ok(set.into_iter().collect()) |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | // ---------------------------------------------------------------- authors |
|
| 82 | + | ||
| 83 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 84 | + | pub struct AuthorView { |
|
| 85 | + | pub name: String, |
|
| 86 | + | pub email: String, |
|
| 87 | + | pub key: String, |
|
| 88 | + | /// True when the key is not an identity the store knows. |
|
| 89 | + | pub unknown: bool, |
|
| 90 | + | } |
|
| 91 | + | ||
| 92 | + | pub fn author(repo: &Repo, key: &KeyId, message: &str) -> AuthorView { |
|
| 93 | + | if key.is_zero() { |
|
| 94 | + | // Imported from Git: the trailer carries the author. |
|
| 95 | + | let line = message.lines().rev().find_map(|l| l.strip_prefix("Git-Author: ")).unwrap_or("unknown"); |
|
| 96 | + | let (name, email) = match line.split_once('<') { |
|
| 97 | + | Some((n, e)) => (n.trim().to_string(), e.trim_end_matches('>').trim().to_string()), |
|
| 98 | + | None => (line.trim().to_string(), String::new()), |
|
| 99 | + | }; |
|
| 100 | + | return AuthorView { name, email, key: String::new(), unknown: true }; |
|
| 101 | + | } |
|
| 102 | + | match repo.store().get_identity(key) { |
|
| 103 | + | Ok(id) => AuthorView { name: id.name, email: id.email, key: key.to_hex(), unknown: false }, |
|
| 104 | + | Err(_) => AuthorView { name: key.short(12), email: String::new(), key: key.to_hex(), unknown: true }, |
|
| 105 | + | } |
|
| 106 | + | } |
|
| 107 | + | ||
| 108 | + | // ---------------------------------------------------------------- changes |
|
| 109 | + | ||
| 110 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 111 | + | pub enum SigView { |
|
| 112 | + | Unsigned, |
|
| 113 | + | Valid { key: String, name: String, sealed: bool, revoked: bool }, |
|
| 114 | + | Invalid(String), |
|
| 115 | + | } |
|
| 116 | + | ||
| 117 | + | impl SigView { |
|
| 118 | + | pub fn glyph(&self) -> &'static str { |
|
| 119 | + | match self { |
|
| 120 | + | SigView::Unsigned => "○", |
|
| 121 | + | SigView::Valid { revoked: true, .. } => "⊘", |
|
| 122 | + | SigView::Valid { sealed: true, .. } => "◈", |
|
| 123 | + | SigView::Valid { .. } => "✔", |
|
| 124 | + | SigView::Invalid(_) => "✘", |
|
| 125 | + | } |
|
| 126 | + | } |
|
| 127 | + | ||
| 128 | + | pub fn class(&self) -> &'static str { |
|
| 129 | + | match self { |
|
| 130 | + | SigView::Unsigned => "sig-unsigned", |
|
| 131 | + | SigView::Valid { revoked: true, .. } => "sig-revoked", |
|
| 132 | + | SigView::Valid { .. } => "sig-valid", |
|
| 133 | + | SigView::Invalid(_) => "sig-invalid", |
|
| 134 | + | } |
|
| 135 | + | } |
|
| 136 | + | ||
| 137 | + | pub fn title(&self) -> String { |
|
| 138 | + | match self { |
|
| 139 | + | SigView::Unsigned => "unsigned".into(), |
|
| 140 | + | SigView::Valid { name, sealed: true, revoked: false, .. } => format!("sealed by {name}"), |
|
| 141 | + | SigView::Valid { name, revoked: true, .. } => format!("signed by {name}, key revoked"), |
|
| 142 | + | SigView::Valid { name, .. } => format!("signed by {name}"), |
|
| 143 | + | SigView::Invalid(why) => format!("INVALID: {why}"), |
|
| 144 | + | } |
|
| 145 | + | } |
|
| 146 | + | } |
|
| 147 | + | ||
| 148 | + | pub fn signature(repo: &Repo, hash: &ChangeHash) -> SigView { |
|
| 149 | + | match repo.verify_change(hash) { |
|
| 150 | + | Ok(SignatureStatus::Unsigned) => SigView::Unsigned, |
|
| 151 | + | Ok(SignatureStatus::Valid { key, name, sealed, revoked, .. }) => SigView::Valid { key: key.to_hex(), name, sealed, revoked }, |
|
| 152 | + | Ok(SignatureStatus::Invalid(why)) => SigView::Invalid(why), |
|
| 153 | + | Err(e) => SigView::Invalid(e.to_string()), |
|
| 154 | + | } |
|
| 155 | + | } |
|
| 156 | + | ||
| 157 | + | /// One change as history rows and change pages show it. |
|
| 158 | + | #[derive(Clone, Debug)] |
|
| 159 | + | pub struct ChangeView { |
|
| 160 | + | pub hash: String, |
|
| 161 | + | pub short: String, |
|
| 162 | + | pub title: String, |
|
| 163 | + | pub body: String, |
|
| 164 | + | pub author: AuthorView, |
|
| 165 | + | pub when_ms: u64, |
|
| 166 | + | pub deps: Vec<String>, |
|
| 167 | + | pub head: bool, |
|
| 168 | + | pub resolution: bool, |
|
| 169 | + | pub signature: SigView, |
|
| 170 | + | /// Bookmarks and tags whose state has this change as a head. |
|
| 171 | + | pub labels: Vec<Label>, |
|
| 172 | + | } |
|
| 173 | + | ||
| 174 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 175 | + | pub struct Label { |
|
| 176 | + | pub name: String, |
|
| 177 | + | pub kind: RefKind, |
|
| 178 | + | } |
|
| 179 | + | ||
| 180 | + | pub fn split_message(message: &str) -> (String, String) { |
|
| 181 | + | let mut lines = message.lines(); |
|
| 182 | + | let title = lines.next().unwrap_or("").trim().to_string(); |
|
| 183 | + | let body = lines.collect::<Vec<_>>().join("\n").trim().to_string(); |
|
| 184 | + | (title, body) |
|
| 185 | + | } |
|
| 186 | + | ||
| 187 | + | /// Every bookmark and tag head, for labelling rows. |
|
| 188 | + | pub fn head_labels(repo: &Repo) -> Result<HashMap<ChangeHash, Vec<Label>>> { |
|
| 189 | + | let mut out: HashMap<ChangeHash, Vec<Label>> = HashMap::new(); |
|
| 190 | + | for b in repo.bookmarks()? { |
|
| 191 | + | for h in b.heads { |
|
| 192 | + | out.entry(h).or_default().push(Label { name: b.name.clone(), kind: RefKind::Bookmark }); |
|
| 193 | + | } |
|
| 194 | + | } |
|
| 195 | + | for t in repo.tags()? { |
|
| 196 | + | for h in t.heads { |
|
| 197 | + | out.entry(h).or_default().push(Label { name: t.name.clone(), kind: RefKind::Tag }); |
|
| 198 | + | } |
|
| 199 | + | } |
|
| 200 | + | Ok(out) |
|
| 201 | + | } |
|
| 202 | + | ||
| 203 | + | fn change_view( |
|
| 204 | + | repo: &Repo, |
|
| 205 | + | hash: &ChangeHash, |
|
| 206 | + | meta: &oxy_core::state::ChangeMeta, |
|
| 207 | + | head: bool, |
|
| 208 | + | labels: &HashMap<ChangeHash, Vec<Label>>, |
|
| 209 | + | ) -> ChangeView { |
|
| 210 | + | let (title, body) = split_message(&meta.message); |
|
| 211 | + | ChangeView { |
|
| 212 | + | hash: hash.to_hex(), |
|
| 213 | + | short: hash.short(12), |
|
| 214 | + | title, |
|
| 215 | + | body, |
|
| 216 | + | author: author(repo, &meta.author, &meta.message), |
|
| 217 | + | when_ms: meta.hlc.wall_ms, |
|
| 218 | + | deps: meta.deps.iter().map(|d| d.to_hex()).collect(), |
|
| 219 | + | head, |
|
| 220 | + | resolution: meta.kind == oxy_core::objects::ChangeKind::Resolution, |
|
| 221 | + | signature: signature(repo, hash), |
|
| 222 | + | labels: labels.get(hash).cloned().unwrap_or_default(), |
|
| 223 | + | } |
|
| 224 | + | } |
|
| 225 | + | ||
| 226 | + | // ---------------------------------------------------------------- history |
|
| 227 | + | ||
| 228 | + | #[derive(Clone, Debug)] |
|
| 229 | + | pub struct History { |
|
| 230 | + | pub target: RefTarget, |
|
| 231 | + | pub entries: Vec<ChangeView>, |
|
| 232 | + | pub total: usize, |
|
| 233 | + | pub offset: usize, |
|
| 234 | + | pub has_more: bool, |
|
| 235 | + | pub path: Option<String>, |
|
| 236 | + | } |
|
| 237 | + | ||
| 238 | + | /// A window of the history of a state, newest first. |
|
| 239 | + | pub fn history(repo: &Repo, target: RefTarget, path: Option<&str>, offset: usize, limit: usize) -> Result<History> { |
|
| 240 | + | let entries = match path { |
|
| 241 | + | Some(p) => repo.log_for_path_in(p, &target.heads)?, |
|
| 242 | + | None => repo.log_state(&target.heads)?, |
|
| 243 | + | }; |
|
| 244 | + | let total = entries.len(); |
|
| 245 | + | let labels = head_labels(repo)?; |
|
| 246 | + | let window: Vec<ChangeView> = |
|
| 247 | + | entries.iter().skip(offset).take(limit).map(|e| change_view(repo, &e.hash, &e.change, e.is_head, &labels)).collect(); |
|
| 248 | + | Ok(History { target, entries: window, total, offset, has_more: offset + limit < total, path: path.map(String::from) }) |
|
| 249 | + | } |
|
| 250 | + | ||
| 251 | + | // ----------------------------------------------------------------- change |
|
| 252 | + | ||
| 253 | + | #[derive(Clone, Debug)] |
|
| 254 | + | pub struct ChangeDetail { |
|
| 255 | + | pub view: ChangeView, |
|
| 256 | + | pub dependents: Vec<ChangeView>, |
|
| 257 | + | pub deps: Vec<ChangeView>, |
|
| 258 | + | pub files: Vec<FileDiff>, |
|
| 259 | + | /// Base content of each modified file, for context lines. |
|
| 260 | + | pub bases: BTreeMap<String, Vec<u8>>, |
|
| 261 | + | pub in_bookmarks: Vec<String>, |
|
| 262 | + | pub in_tags: Vec<String>, |
|
| 263 | + | pub atoms: usize, |
|
| 264 | + | pub tree_ops: usize, |
|
| 265 | + | } |
|
| 266 | + | ||
| 267 | + | pub fn change(repo: &Repo, hash: &ChangeHash) -> Result<ChangeDetail> { |
|
| 268 | + | let c: Change = repo.store().get_change(hash)?; |
|
| 269 | + | let labels = head_labels(repo)?; |
|
| 270 | + | let heads = all_heads(repo)?; |
|
| 271 | + | let state = repo.load_state(&heads)?; |
|
| 272 | + | let is_head = state.heads().contains(hash); |
|
| 273 | + | let meta = oxy_core::state::ChangeMeta::of(&c); |
|
| 274 | + | let view = change_view(repo, hash, &meta, is_head, &labels); |
|
| 275 | + | let deps = c.deps.iter().filter_map(|d| state.get(d).map(|m| change_view(repo, d, m, state.heads().contains(d), &labels))).collect(); |
|
| 276 | + | let mut dependents: Vec<ChangeView> = state |
|
| 277 | + | .changes() |
|
| 278 | + | .filter(|(_, m)| m.deps.contains(hash)) |
|
| 279 | + | .map(|(h, m)| change_view(repo, h, m, state.heads().contains(h), &labels)) |
|
| 280 | + | .collect(); |
|
| 281 | + | dependents.sort_by_key(|c| std::cmp::Reverse(c.when_ms)); |
|
| 282 | + | let files = repo.diff_files(Some(hash))?; |
|
| 283 | + | let base_state = repo.load_state(&c.deps)?; |
|
| 284 | + | let base_snapshot = base_state.materialize(); |
|
| 285 | + | let mut bases = BTreeMap::new(); |
|
| 286 | + | for f in &files { |
|
| 287 | + | let old = f.from.as_deref().unwrap_or(&f.path); |
|
| 288 | + | if let Some(FileContent::Text { bytes, .. }) = base_snapshot.files.get(old).map(|s| &s.content) { |
|
| 289 | + | bases.insert(f.path.clone(), bytes.clone()); |
|
| 290 | + | } |
|
| 291 | + | } |
|
| 292 | + | let mut in_bookmarks = Vec::new(); |
|
| 293 | + | for b in repo.bookmarks()? { |
|
| 294 | + | if repo.load_state(&b.heads)?.contains(hash) { |
|
| 295 | + | in_bookmarks.push(b.name); |
|
| 296 | + | } |
|
| 297 | + | } |
|
| 298 | + | let mut in_tags = Vec::new(); |
|
| 299 | + | for t in repo.tags()? { |
|
| 300 | + | if repo.load_state(&t.heads)?.contains(hash) { |
|
| 301 | + | in_tags.push(t.name); |
|
| 302 | + | } |
|
| 303 | + | } |
|
| 304 | + | Ok(ChangeDetail { view, dependents, deps, files, bases, in_bookmarks, in_tags, atoms: c.edits.len(), tree_ops: c.tree_ops.len() }) |
|
| 305 | + | } |
|
| 306 | + | ||
| 307 | + | // ------------------------------------------------------------------- tree |
|
| 308 | + | ||
| 309 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 310 | + | pub enum EntryKind { |
|
| 311 | + | Dir, |
|
| 312 | + | File, |
|
| 313 | + | Symlink, |
|
| 314 | + | Subrepo, |
|
| 315 | + | } |
|
| 316 | + | ||
| 317 | + | #[derive(Clone, Debug)] |
|
| 318 | + | pub struct Entry { |
|
| 319 | + | pub name: String, |
|
| 320 | + | pub path: String, |
|
| 321 | + | pub kind: EntryKind, |
|
| 322 | + | pub exec: bool, |
|
| 323 | + | pub size: Option<u64>, |
|
| 324 | + | pub conflicted: bool, |
|
| 325 | + | } |
|
| 326 | + | ||
| 327 | + | #[derive(Clone, Debug)] |
|
| 328 | + | pub struct Tree { |
|
| 329 | + | pub target: RefTarget, |
|
| 330 | + | pub path: String, |
|
| 331 | + | pub entries: Vec<Entry>, |
|
| 332 | + | /// `(name, path)` from the root down to this directory. |
|
| 333 | + | pub crumbs: Vec<(String, String)>, |
|
| 334 | + | pub readme: Option<Readme>, |
|
| 335 | + | } |
|
| 336 | + | ||
| 337 | + | #[derive(Clone, Debug)] |
|
| 338 | + | pub struct Readme { |
|
| 339 | + | pub path: String, |
|
| 340 | + | pub text: String, |
|
| 341 | + | } |
|
| 342 | + | ||
| 343 | + | pub fn crumbs(path: &str) -> Vec<(String, String)> { |
|
| 344 | + | let mut out = Vec::new(); |
|
| 345 | + | let mut so_far = String::new(); |
|
| 346 | + | for part in path.split('/').filter(|p| !p.is_empty()) { |
|
| 347 | + | if !so_far.is_empty() { |
|
| 348 | + | so_far.push('/'); |
|
| 349 | + | } |
|
| 350 | + | so_far.push_str(part); |
|
| 351 | + | out.push((part.to_string(), so_far.clone())); |
|
| 352 | + | } |
|
| 353 | + | out |
|
| 354 | + | } |
|
| 355 | + | ||
| 356 | + | fn entry_of(path: &str, name: &str, f: &oxy_core::state::FileState) -> Entry { |
|
| 357 | + | let kind = match f.kind { |
|
| 358 | + | NodeKind::Dir => EntryKind::Dir, |
|
| 359 | + | NodeKind::File => EntryKind::File, |
|
| 360 | + | NodeKind::Symlink => EntryKind::Symlink, |
|
| 361 | + | NodeKind::Subrepo => EntryKind::Subrepo, |
|
| 362 | + | }; |
|
| 363 | + | let size = match &f.content { |
|
| 364 | + | FileContent::Text { bytes, .. } => Some(bytes.len() as u64), |
|
| 365 | + | _ => None, |
|
| 366 | + | }; |
|
| 367 | + | Entry { name: name.to_string(), path: path.to_string(), kind, exec: f.mode.exec, size, conflicted: f.is_conflicted() } |
|
| 368 | + | } |
|
| 369 | + | ||
| 370 | + | /// The entries directly under `dir` (`""` for the root). |
|
| 371 | + | pub fn tree(repo: &Repo, target: RefTarget, dir: &str) -> Result<Tree> { |
|
| 372 | + | let state = repo.load_state(&target.heads)?; |
|
| 373 | + | let snapshot = state.materialize(); |
|
| 374 | + | tree_of(repo, target, &snapshot, dir) |
|
| 375 | + | } |
|
| 376 | + | ||
| 377 | + | pub fn tree_of(_repo: &Repo, target: RefTarget, snapshot: &Snapshot, dir: &str) -> Result<Tree> { |
|
| 378 | + | let dir = dir.trim_matches('/'); |
|
| 379 | + | if !dir.is_empty() { |
|
| 380 | + | match snapshot.files.get(dir) { |
|
| 381 | + | Some(f) if f.kind == NodeKind::Dir => {} |
|
| 382 | + | _ => return Err(RepoError::NotFound), |
|
| 383 | + | } |
|
| 384 | + | } |
|
| 385 | + | let prefix = if dir.is_empty() { String::new() } else { format!("{dir}/") }; |
|
| 386 | + | let mut entries: Vec<Entry> = snapshot |
|
| 387 | + | .files |
|
| 388 | + | .iter() |
|
| 389 | + | .filter(|(p, _)| p.starts_with(&prefix) && !p[prefix.len()..].contains('/')) |
|
| 390 | + | .map(|(p, f)| entry_of(p, &p[prefix.len()..], f)) |
|
| 391 | + | .collect(); |
|
| 392 | + | entries.sort_by(|a, b| { |
|
| 393 | + | (a.kind != EntryKind::Dir).cmp(&(b.kind != EntryKind::Dir)).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) |
|
| 394 | + | }); |
|
| 395 | + | let readme = entries |
|
| 396 | + | .iter() |
|
| 397 | + | .find(|e| { |
|
| 398 | + | e.kind == EntryKind::File && matches!(e.name.to_lowercase().as_str(), "readme.md" | "readme" | "readme.txt" | "readme.markdown") |
|
| 399 | + | }) |
|
| 400 | + | .and_then(|e| match &snapshot.files.get(&e.path)?.content { |
|
| 401 | + | FileContent::Text { bytes, .. } => Some(Readme { path: e.path.clone(), text: String::from_utf8_lossy(bytes).into_owned() }), |
|
| 402 | + | _ => None, |
|
| 403 | + | }); |
|
| 404 | + | Ok(Tree { target, path: dir.to_string(), entries, crumbs: crumbs(dir), readme }) |
|
| 405 | + | } |
|
| 406 | + | ||
| 407 | + | // ------------------------------------------------------------------- file |
|
| 408 | + | ||
| 409 | + | #[derive(Clone, Debug)] |
|
| 410 | + | pub enum FileBody { |
|
| 411 | + | Text(String), |
|
| 412 | + | Binary, |
|
| 413 | + | TooLarge, |
|
| 414 | + | } |
|
| 415 | + | ||
| 416 | + | #[derive(Clone, Debug)] |
|
| 417 | + | pub struct FileView { |
|
| 418 | + | pub target: RefTarget, |
|
| 419 | + | pub path: String, |
|
| 420 | + | pub name: String, |
|
| 421 | + | pub crumbs: Vec<(String, String)>, |
|
| 422 | + | pub kind: EntryKind, |
|
| 423 | + | pub exec: bool, |
|
| 424 | + | pub size: u64, |
|
| 425 | + | pub body: FileBody, |
|
| 426 | + | pub conflicted: bool, |
|
| 427 | + | /// Raw bytes, for the `?plain=1` response and images. |
|
| 428 | + | pub bytes: Vec<u8>, |
|
| 429 | + | pub last_change: Option<ChangeView>, |
|
| 430 | + | } |
|
| 431 | + | ||
| 432 | + | pub fn file(repo: &Repo, target: RefTarget, path: &str, render_limit: u64) -> Result<FileView> { |
|
| 433 | + | let state = repo.load_state(&target.heads)?; |
|
| 434 | + | let snapshot = state.materialize(); |
|
| 435 | + | let path = path.trim_matches('/'); |
|
| 436 | + | let Some(f) = snapshot.files.get(path) else { return Err(RepoError::NotFound) }; |
|
| 437 | + | let (kind, bytes) = match (&f.kind, &f.content) { |
|
| 438 | + | (NodeKind::Dir, _) => return Err(RepoError::NotFound), |
|
| 439 | + | (NodeKind::Symlink, FileContent::Text { bytes, .. }) => (EntryKind::Symlink, bytes.clone()), |
|
| 440 | + | (NodeKind::Subrepo, FileContent::Text { bytes, .. }) => (EntryKind::Subrepo, bytes.clone()), |
|
| 441 | + | (_, FileContent::Text { bytes, .. }) => (EntryKind::File, bytes.clone()), |
|
| 442 | + | (_, FileContent::Opaque { blobs }) => { |
|
| 443 | + | let bytes = match blobs.first() { |
|
| 444 | + | Some(b) => repo.read_blob(b)?, |
|
| 445 | + | None => Vec::new(), |
|
| 446 | + | }; |
|
| 447 | + | (EntryKind::File, bytes) |
|
| 448 | + | } |
|
| 449 | + | (_, FileContent::Dir) => return Err(RepoError::NotFound), |
|
| 450 | + | }; |
|
| 451 | + | let size = bytes.len() as u64; |
|
| 452 | + | let body = if size > render_limit { |
|
| 453 | + | FileBody::TooLarge |
|
| 454 | + | } else if is_binary(&bytes) { |
|
| 455 | + | FileBody::Binary |
|
| 456 | + | } else { |
|
| 457 | + | FileBody::Text(String::from_utf8_lossy(&bytes).into_owned()) |
|
| 458 | + | }; |
|
| 459 | + | let labels = head_labels(repo)?; |
|
| 460 | + | let last_change = repo |
|
| 461 | + | .log_for_path_in(path, &target.heads) |
|
| 462 | + | .ok() |
|
| 463 | + | .and_then(|l| l.into_iter().next()) |
|
| 464 | + | .map(|e| change_view(repo, &e.hash, &e.change, e.is_head, &labels)); |
|
| 465 | + | let name = path.rsplit('/').next().unwrap_or(path).to_string(); |
|
| 466 | + | Ok(FileView { |
|
| 467 | + | target, |
|
| 468 | + | path: path.to_string(), |
|
| 469 | + | name, |
|
| 470 | + | crumbs: crumbs(path), |
|
| 471 | + | kind, |
|
| 472 | + | exec: f.mode.exec, |
|
| 473 | + | size, |
|
| 474 | + | body, |
|
| 475 | + | conflicted: f.is_conflicted(), |
|
| 476 | + | bytes, |
|
| 477 | + | last_change, |
|
| 478 | + | }) |
|
| 479 | + | } |
|
| 480 | + | ||
| 481 | + | pub fn is_binary(bytes: &[u8]) -> bool { |
|
| 482 | + | let head = &bytes[..bytes.len().min(8192)]; |
|
| 483 | + | head.contains(&0) |
|
| 484 | + | || std::str::from_utf8(head).is_err() |
|
| 485 | + | && head.iter().filter(|b| **b < 0x20 && !matches!(**b, b'\n' | b'\r' | b'\t')).count() > head.len() / 20 |
|
| 486 | + | } |
|
| 487 | + | ||
| 488 | + | /// Last change per entry of a directory, for tree rows (fetched lazily). |
|
| 489 | + | /// A directory's last change is the newest change touching anything |
|
| 490 | + | /// beneath it, as GitHub shows it. |
|
| 491 | + | pub fn last_changes(repo: &Repo, heads: &[ChangeHash], paths: &[String]) -> Result<Vec<(String, ChangeView)>> { |
|
| 492 | + | let labels = head_labels(repo)?; |
|
| 493 | + | let state = repo.load_state(heads)?; |
|
| 494 | + | let snapshot = state.materialize(); |
|
| 495 | + | // Newest change touching each node, from one pass over the log. |
|
| 496 | + | let mut newest: HashMap<oxy_core::ids::NodeId, (ChangeHash, u64)> = HashMap::new(); |
|
| 497 | + | for e in repo.log_state(heads)? { |
|
| 498 | + | let Ok(c) = repo.store().get_change(&e.hash) else { continue }; |
|
| 499 | + | let when = e.change.hlc.wall_ms; |
|
| 500 | + | for node in c.tree_ops.iter().map(|o| o.node()).chain(c.edits.iter().map(|x| x.node())) { |
|
| 501 | + | newest.entry(node).or_insert((e.hash, when)); |
|
| 502 | + | } |
|
| 503 | + | } |
|
| 504 | + | let mut out = Vec::new(); |
|
| 505 | + | for p in paths { |
|
| 506 | + | let Some(f) = snapshot.files.get(p) else { continue }; |
|
| 507 | + | let best = if f.kind == NodeKind::Dir { |
|
| 508 | + | let prefix = format!("{p}/"); |
|
| 509 | + | snapshot |
|
| 510 | + | .files |
|
| 511 | + | .iter() |
|
| 512 | + | .filter(|(q, _)| q.starts_with(&prefix)) |
|
| 513 | + | .filter_map(|(_, g)| newest.get(&g.node)) |
|
| 514 | + | .chain(newest.get(&f.node)) |
|
| 515 | + | .max_by_key(|(_, when)| *when) |
|
| 516 | + | .copied() |
|
| 517 | + | } else { |
|
| 518 | + | newest.get(&f.node).copied() |
|
| 519 | + | }; |
|
| 520 | + | if let Some((hash, _)) = best { |
|
| 521 | + | if let Some(m) = state.get(&hash) { |
|
| 522 | + | out.push((p.clone(), change_view(repo, &hash, m, state.heads().contains(&hash), &labels))); |
|
| 523 | + | } |
|
| 524 | + | } |
|
| 525 | + | } |
|
| 526 | + | Ok(out) |
|
| 527 | + | } |
|
| 528 | + | ||
| 529 | + | // ------------------------------------------------------------------ blame |
|
| 530 | + | ||
| 531 | + | #[derive(Clone, Debug)] |
|
| 532 | + | pub struct Blame { |
|
| 533 | + | pub file: FileView, |
|
| 534 | + | pub lines: Vec<BlameLine>, |
|
| 535 | + | } |
|
| 536 | + | ||
| 537 | + | pub fn blame(repo: &Repo, target: RefTarget, path: &str, render_limit: u64) -> Result<Blame> { |
|
| 538 | + | let heads = target.heads.clone(); |
|
| 539 | + | let file = file(repo, target, path, render_limit)?; |
|
| 540 | + | let lines = repo.blame_in(path, &heads)?; |
|
| 541 | + | Ok(Blame { file, lines }) |
|
| 542 | + | } |
|
| 543 | + | ||
| 544 | + | // -------------------------------------------------------------- overview |
|
| 545 | + | ||
| 546 | + | #[derive(Clone, Debug)] |
|
| 547 | + | pub struct BookmarkView { |
|
| 548 | + | pub name: String, |
|
| 549 | + | pub heads: Vec<String>, |
|
| 550 | + | pub changes: usize, |
|
| 551 | + | pub default: bool, |
|
| 552 | + | pub conflicted: bool, |
|
| 553 | + | pub latest_ms: u64, |
|
| 554 | + | } |
|
| 555 | + | ||
| 556 | + | #[derive(Clone, Debug)] |
|
| 557 | + | pub struct TagView { |
|
| 558 | + | pub name: String, |
|
| 559 | + | pub heads: Vec<String>, |
|
| 560 | + | pub message: String, |
|
| 561 | + | pub signer: Option<String>, |
|
| 562 | + | } |
|
| 563 | + | ||
| 564 | + | #[derive(Clone, Debug)] |
|
| 565 | + | pub struct Overview { |
|
| 566 | + | pub tree: Tree, |
|
| 567 | + | pub changes: usize, |
|
| 568 | + | pub heads: usize, |
|
| 569 | + | pub bookmarks: Vec<BookmarkView>, |
|
| 570 | + | pub tags: Vec<TagView>, |
|
| 571 | + | pub latest: Option<ChangeView>, |
|
| 572 | + | pub conflicted: bool, |
|
| 573 | + | pub license: Option<String>, |
|
| 574 | + | pub policy: PolicyBadge, |
|
| 575 | + | } |
|
| 576 | + | ||
| 577 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| 578 | + | pub enum PolicyBadge { |
|
| 579 | + | None, |
|
| 580 | + | Active, |
|
| 581 | + | Expired, |
|
| 582 | + | } |
|
| 583 | + | ||
| 584 | + | pub fn bookmarks(repo: &Repo, default: &str) -> Result<Vec<BookmarkView>> { |
|
| 585 | + | let mut out = Vec::new(); |
|
| 586 | + | for b in repo.bookmarks()? { |
|
| 587 | + | let state = repo.load_state(&b.heads)?; |
|
| 588 | + | let latest_ms = state.changes().map(|(_, m)| m.hlc.wall_ms).max().unwrap_or(0); |
|
| 589 | + | let conflicted = state.materialize().is_conflicted(); |
|
| 590 | + | out.push(BookmarkView { |
|
| 591 | + | name: b.name.clone(), |
|
| 592 | + | heads: b.heads.iter().map(|h| h.to_hex()).collect(), |
|
| 593 | + | changes: state.len(), |
|
| 594 | + | default: b.name == default, |
|
| 595 | + | conflicted, |
|
| 596 | + | latest_ms, |
|
| 597 | + | }); |
|
| 598 | + | } |
|
| 599 | + | out.sort_by(|a, b| b.default.cmp(&a.default).then_with(|| a.name.cmp(&b.name))); |
|
| 600 | + | Ok(out) |
|
| 601 | + | } |
|
| 602 | + | ||
| 603 | + | pub fn tags(repo: &Repo) -> Result<Vec<TagView>> { |
|
| 604 | + | let mut out: Vec<TagView> = repo |
|
| 605 | + | .tags()? |
|
| 606 | + | .into_iter() |
|
| 607 | + | .map(|t| TagView { name: t.name, heads: t.heads.iter().map(|h| h.to_hex()).collect(), message: t.message, signer: t.signer }) |
|
| 608 | + | .collect(); |
|
| 609 | + | out.sort_by(|a, b| b.name.cmp(&a.name)); |
|
| 610 | + | Ok(out) |
|
| 611 | + | } |
|
| 612 | + | ||
| 613 | + | pub fn policy_badge(repo: &Repo) -> PolicyBadge { |
|
| 614 | + | match repo.current_policy() { |
|
| 615 | + | Ok(Some((_, sp))) => { |
|
| 616 | + | if sp.policy.expires != 0 && (sp.policy.expires as i64) < crate::db::now_ms() { |
|
| 617 | + | PolicyBadge::Expired |
|
| 618 | + | } else { |
|
| 619 | + | PolicyBadge::Active |
|
| 620 | + | } |
|
| 621 | + | } |
|
| 622 | + | _ => PolicyBadge::None, |
|
| 623 | + | } |
|
| 624 | + | } |
|
| 625 | + | ||
| 626 | + | pub fn overview(repo: &Repo, default_bookmark: &str) -> Result<Overview> { |
|
| 627 | + | let target = match resolve(repo, default_bookmark) { |
|
| 628 | + | Ok(t) => t, |
|
| 629 | + | Err(_) => RefTarget { label: default_bookmark.into(), kind: RefKind::Bookmark, heads: vec![] }, |
|
| 630 | + | }; |
|
| 631 | + | let heads = target.heads.len(); |
|
| 632 | + | // One load of the default state serves the counts, the latest change |
|
| 633 | + | // and the tree (hub spec §16.3). |
|
| 634 | + | let state = repo.load_state(&target.heads)?; |
|
| 635 | + | let snapshot = state.materialize(); |
|
| 636 | + | let changes = state.len(); |
|
| 637 | + | let conflicted = snapshot.is_conflicted(); |
|
| 638 | + | let labels = head_labels(repo)?; |
|
| 639 | + | let latest = state |
|
| 640 | + | .changes() |
|
| 641 | + | .max_by(|(ha, a), (hb, b)| a.hlc.cmp(&b.hlc).then(ha.cmp(hb))) |
|
| 642 | + | .map(|(h, m)| change_view(repo, h, m, state.heads().contains(h), &labels)); |
|
| 643 | + | let tree = tree_of(repo, target, &snapshot, "")?; |
|
| 644 | + | let license = tree |
|
| 645 | + | .entries |
|
| 646 | + | .iter() |
|
| 647 | + | .find(|e| matches!(e.name.to_lowercase().as_str(), "license" | "license.md" | "license.txt" | "licence" | "copying")) |
|
| 648 | + | .map(|e| e.path.clone()); |
|
| 649 | + | Ok(Overview { |
|
| 650 | + | tree, |
|
| 651 | + | changes, |
|
| 652 | + | heads, |
|
| 653 | + | bookmarks: bookmarks(repo, default_bookmark)?, |
|
| 654 | + | tags: tags(repo)?, |
|
| 655 | + | latest, |
|
| 656 | + | conflicted, |
|
| 657 | + | license, |
|
| 658 | + | policy: policy_badge(repo), |
|
| 659 | + | }) |
|
| 660 | + | } |
|
| 661 | + | ||
| 662 | + | // -------------------------------------------------------------- compare |
|
| 663 | + | ||
| 664 | + | #[derive(Clone, Debug)] |
|
| 665 | + | pub struct Compare { |
|
| 666 | + | pub base: RefTarget, |
|
| 667 | + | pub head: RefTarget, |
|
| 668 | + | /// Changes in `head`'s state not in `base`'s. |
|
| 669 | + | pub added: Vec<ChangeView>, |
|
| 670 | + | /// Changes in `base`'s state not in `head`'s. |
|
| 671 | + | pub missing: usize, |
|
| 672 | + | pub files: Vec<FileDiff>, |
|
| 673 | + | pub bases: BTreeMap<String, Vec<u8>>, |
|
| 674 | + | } |
|
| 675 | + | ||
| 676 | + | pub fn compare(repo: &Repo, base: RefTarget, head: RefTarget) -> Result<Compare> { |
|
| 677 | + | let base_state = repo.load_state(&base.heads)?; |
|
| 678 | + | let head_state = repo.load_state(&head.heads)?; |
|
| 679 | + | let labels = head_labels(repo)?; |
|
| 680 | + | let mut added: Vec<ChangeView> = head_state |
|
| 681 | + | .changes() |
|
| 682 | + | .filter(|(h, _)| !base_state.contains(h)) |
|
| 683 | + | .map(|(h, m)| change_view(repo, h, m, head_state.heads().contains(h), &labels)) |
|
| 684 | + | .collect(); |
|
| 685 | + | added.sort_by_key(|c| std::cmp::Reverse(c.when_ms)); |
|
| 686 | + | let missing = base_state.changes().filter(|(h, _)| !head_state.contains(h)).count(); |
|
| 687 | + | let base_snapshot = base_state.materialize(); |
|
| 688 | + | let head_snapshot = head_state.materialize(); |
|
| 689 | + | let (files, bases) = crate::render::diff::snapshot_diff(&base_snapshot, &head_snapshot); |
|
| 690 | + | Ok(Compare { base, head, added, missing, files, bases }) |
|
| 691 | + | } |
|
| 692 | + | ||
| 693 | + | // ------------------------------------------------------------ conflicts |
|
| 694 | + | ||
| 695 | + | #[derive(Clone, Debug)] |
|
| 696 | + | pub struct ConflictView { |
|
| 697 | + | pub bookmark: String, |
|
| 698 | + | pub path: String, |
|
| 699 | + | pub kind: String, |
|
| 700 | + | /// The file rendered with `oxy` conflict markers, when a text file. |
|
| 701 | + | pub marked: Option<String>, |
|
| 702 | + | pub contributors: Vec<ChangeView>, |
|
| 703 | + | } |
|
| 704 | + | ||
| 705 | + | pub fn conflicts(repo: &Repo) -> Result<Vec<ConflictView>> { |
|
| 706 | + | let labels = head_labels(repo)?; |
|
| 707 | + | let mut out = Vec::new(); |
|
| 708 | + | for b in repo.bookmarks()? { |
|
| 709 | + | let state = repo.load_state(&b.heads)?; |
|
| 710 | + | let snapshot = state.materialize(); |
|
| 711 | + | for c in &snapshot.tree.conflicts { |
|
| 712 | + | let (path, kind) = describe_tree_conflict(&snapshot, c); |
|
| 713 | + | out.push(ConflictView { bookmark: b.name.clone(), path, kind, marked: None, contributors: vec![] }); |
|
| 714 | + | } |
|
| 715 | + | for (path, f) in &snapshot.files { |
|
| 716 | + | if !f.is_conflicted() { |
|
| 717 | + | continue; |
|
| 718 | + | } |
|
| 719 | + | let (kind, marked, contributors) = match &f.content { |
|
| 720 | + | FileContent::Text { .. } => { |
|
| 721 | + | let (bytes, _) = state.render_file(&f.node); |
|
| 722 | + | let regions = state.conflict_regions(&f.node); |
|
| 723 | + | let contributors: Vec<ChangeView> = regions |
|
| 724 | + | .contributors |
|
| 725 | + | .iter() |
|
| 726 | + | .filter_map(|h| state.get(h).map(|m| change_view(repo, h, m, state.heads().contains(h), &labels))) |
|
| 727 | + | .collect(); |
|
| 728 | + | ("lines".to_string(), Some(String::from_utf8_lossy(&bytes).into_owned()), contributors) |
|
| 729 | + | } |
|
| 730 | + | FileContent::Opaque { blobs } => (format!("replace ({} candidates)", blobs.len()), None, vec![]), |
|
| 731 | + | FileContent::Dir => continue, |
|
| 732 | + | }; |
|
| 733 | + | out.push(ConflictView { bookmark: b.name.clone(), path: path.clone(), kind, marked, contributors }); |
|
| 734 | + | } |
|
| 735 | + | } |
|
| 736 | + | Ok(out) |
|
| 737 | + | } |
|
| 738 | + | ||
| 739 | + | fn describe_tree_conflict(snapshot: &Snapshot, c: &TreeConflict) -> (String, String) { |
|
| 740 | + | let by_node = snapshot.tree.by_node(); |
|
| 741 | + | let path_of = |n: &oxy_core::ids::NodeId| by_node.get(n).map(|e| e.path.clone()).unwrap_or_else(|| n.short(8)); |
|
| 742 | + | match c { |
|
| 743 | + | TreeConflict::Name { name, nodes, .. } => (name.clone(), format!("name: {} entries want this name", nodes.len())), |
|
| 744 | + | TreeConflict::MultiPlacement { node, placements } => (path_of(node), format!("placement: {} places", placements.len())), |
|
| 745 | + | TreeConflict::Orphan { node, .. } => (path_of(node), "orphan: parent directory was removed".into()), |
|
| 746 | + | TreeConflict::Cycle { node } => (path_of(node), "cycle: directory moved into itself".into()), |
|
| 747 | + | other => (String::new(), format!("{other:?}")), |
|
| 748 | + | } |
|
| 749 | + | } |
|
| 750 | + | ||
| 751 | + | // ------------------------------------------------------------------ trust |
|
| 752 | + | ||
| 753 | + | #[derive(Clone, Debug)] |
|
| 754 | + | pub struct KeyView { |
|
| 755 | + | pub key: String, |
|
| 756 | + | pub short: String, |
|
| 757 | + | pub name: String, |
|
| 758 | + | pub email: String, |
|
| 759 | + | } |
|
| 760 | + | ||
| 761 | + | #[derive(Clone, Debug)] |
|
| 762 | + | pub struct RuleView { |
|
| 763 | + | pub pattern: String, |
|
| 764 | + | pub keys: Vec<KeyView>, |
|
| 765 | + | pub threshold: u32, |
|
| 766 | + | pub require: String, |
|
| 767 | + | pub allow_conflicts: bool, |
|
| 768 | + | } |
|
| 769 | + | ||
| 770 | + | #[derive(Clone, Debug)] |
|
| 771 | + | pub struct PolicyView { |
|
| 772 | + | pub serial: u64, |
|
| 773 | + | pub expires_ms: u64, |
|
| 774 | + | pub expired: bool, |
|
| 775 | + | pub roots: Vec<KeyView>, |
|
| 776 | + | pub root_threshold: u32, |
|
| 777 | + | pub rules: Vec<RuleView>, |
|
| 778 | + | pub revoked: Vec<KeyView>, |
|
| 779 | + | pub signatures: usize, |
|
| 780 | + | pub history: usize, |
|
| 781 | + | } |
|
| 782 | + | ||
| 783 | + | #[derive(Clone, Debug, Default)] |
|
| 784 | + | pub struct Verification { |
|
| 785 | + | pub total: usize, |
|
| 786 | + | pub signed: usize, |
|
| 787 | + | pub sealed: usize, |
|
| 788 | + | pub unsigned: usize, |
|
| 789 | + | pub revoked: usize, |
|
| 790 | + | pub invalid: Vec<(String, String)>, |
|
| 791 | + | } |
|
| 792 | + | ||
| 793 | + | #[derive(Clone, Debug)] |
|
| 794 | + | pub struct Trust { |
|
| 795 | + | pub policy: Option<PolicyView>, |
|
| 796 | + | pub verification: Verification, |
|
| 797 | + | pub hub_key: String, |
|
| 798 | + | pub hub_delegated_by: Vec<KeyView>, |
|
| 799 | + | } |
|
| 800 | + | ||
| 801 | + | fn key_view(repo: &Repo, ids: &[oxy_core::identity::Identity], key: &KeyId) -> KeyView { |
|
| 802 | + | let found = ids.iter().find(|i| i.hash() == *key).cloned().or_else(|| repo.store().get_identity(key).ok()); |
|
| 803 | + | match found { |
|
| 804 | + | Some(i) => KeyView { key: key.to_hex(), short: key.short(12), name: i.name, email: i.email }, |
|
| 805 | + | None => KeyView { key: key.to_hex(), short: key.short(12), name: String::new(), email: String::new() }, |
|
| 806 | + | } |
|
| 807 | + | } |
|
| 808 | + | ||
| 809 | + | pub fn verification(repo: &Repo) -> Result<Verification> { |
|
| 810 | + | let heads = all_heads(repo)?; |
|
| 811 | + | let state = repo.load_state(&heads)?; |
|
| 812 | + | let mut v = Verification::default(); |
|
| 813 | + | for (h, _) in state.changes() { |
|
| 814 | + | v.total += 1; |
|
| 815 | + | match signature(repo, h) { |
|
| 816 | + | SigView::Unsigned => v.unsigned += 1, |
|
| 817 | + | SigView::Valid { sealed, revoked, .. } => { |
|
| 818 | + | if revoked { |
|
| 819 | + | v.revoked += 1; |
|
| 820 | + | } |
|
| 821 | + | if sealed { |
|
| 822 | + | v.sealed += 1 |
|
| 823 | + | } else { |
|
| 824 | + | v.signed += 1 |
|
| 825 | + | } |
|
| 826 | + | } |
|
| 827 | + | SigView::Invalid(why) => v.invalid.push((h.to_hex(), why)), |
|
| 828 | + | } |
|
| 829 | + | } |
|
| 830 | + | Ok(v) |
|
| 831 | + | } |
|
| 832 | + | ||
| 833 | + | pub fn trust(repo: &Repo, hub_key: &KeyId) -> Result<Trust> { |
|
| 834 | + | let policy = match repo.current_policy()? { |
|
| 835 | + | Some((_, sp)) => { |
|
| 836 | + | let p = &sp.policy; |
|
| 837 | + | let ids = &p.identities; |
|
| 838 | + | let require = |r: &oxy_core::policy::Require| match r { |
|
| 839 | + | oxy_core::policy::Require::Signed => "signed", |
|
| 840 | + | oxy_core::policy::Require::Sealed => "sealed", |
|
| 841 | + | oxy_core::policy::Require::Any => "any", |
|
| 842 | + | }; |
|
| 843 | + | Some(PolicyView { |
|
| 844 | + | serial: p.serial, |
|
| 845 | + | expires_ms: p.expires, |
|
| 846 | + | expired: p.expires != 0 && (p.expires as i64) < crate::db::now_ms(), |
|
| 847 | + | roots: p.roots.iter().map(|k| key_view(repo, ids, k)).collect(), |
|
| 848 | + | root_threshold: p.root_threshold, |
|
| 849 | + | rules: p |
|
| 850 | + | .rules |
|
| 851 | + | .iter() |
|
| 852 | + | .map(|r| RuleView { |
|
| 853 | + | pattern: r.pattern.clone(), |
|
| 854 | + | keys: r.keys.iter().map(|k| key_view(repo, ids, k)).collect(), |
|
| 855 | + | threshold: r.threshold, |
|
| 856 | + | require: require(&r.require).into(), |
|
| 857 | + | allow_conflicts: r.allow_conflicts, |
|
| 858 | + | }) |
|
| 859 | + | .collect(), |
|
| 860 | + | revoked: p.revoked.iter().map(|k| key_view(repo, ids, k)).collect(), |
|
| 861 | + | signatures: sp.sigs.len(), |
|
| 862 | + | history: repo.policy_history().map(|h| h.len()).unwrap_or(0), |
|
| 863 | + | }) |
|
| 864 | + | } |
|
| 865 | + | None => None, |
|
| 866 | + | }; |
|
| 867 | + | // Who has delegated to the hub: every identity in the policy or store |
|
| 868 | + | // that lists the hub key. |
|
| 869 | + | let mut delegated = Vec::new(); |
|
| 870 | + | if let Some(p) = repo.current_policy()?.map(|(_, sp)| sp.policy) { |
|
| 871 | + | for i in &p.identities { |
|
| 872 | + | if i.delegates.contains(hub_key) { |
|
| 873 | + | delegated.push(KeyView { key: i.hash().to_hex(), short: i.hash().short(12), name: i.name.clone(), email: i.email.clone() }); |
|
| 874 | + | } |
|
| 875 | + | } |
|
| 876 | + | } |
|
| 877 | + | Ok(Trust { policy, verification: verification(repo)?, hub_key: hub_key.to_hex(), hub_delegated_by: delegated }) |
|
| 878 | + | } |
|
| 879 | + | ||
| 880 | + | // ------------------------------------------------------------- helpers |
|
| 881 | + | ||
| 882 | + | /// The state of a bookmark or any heads, for callers that need it raw. |
|
| 883 | + | pub fn state_of(repo: &Repo, heads: &[ChangeHash]) -> Result<State> { |
|
| 884 | + | Ok(repo.load_state(heads)?) |
|
| 885 | + | } |
added crates/hub/src/web/filters.rs +56 −0
| 1 | + | //! Askama filters used by the templates. Numeric filters accept values or |
|
| 2 | + | //! references, since a template passes fields by reference and method |
|
| 3 | + | //! results by value. |
|
| 4 | + | ||
| 5 | + | use crate::render; |
|
| 6 | + | ||
| 7 | + | /// A number a template hands to a filter, by value or by reference. |
|
| 8 | + | pub trait Num: Copy { |
|
| 9 | + | fn as_u64(self) -> u64; |
|
| 10 | + | } |
|
| 11 | + | ||
| 12 | + | macro_rules! num { |
|
| 13 | + | ($($t:ty),*) => {$( |
|
| 14 | + | impl Num for $t { |
|
| 15 | + | fn as_u64(self) -> u64 { if (self as i128) < 0 { 0 } else { self as u64 } } |
|
| 16 | + | } |
|
| 17 | + | impl Num for &$t { |
|
| 18 | + | fn as_u64(self) -> u64 { (*self).as_u64() } |
|
| 19 | + | } |
|
| 20 | + | )*}; |
|
| 21 | + | } |
|
| 22 | + | num!(u64, i64, u32, i32, usize); |
|
| 23 | + | ||
| 24 | + | pub fn ago(ms: impl Num, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 25 | + | Ok(render::time::ago(ms.as_u64(), crate::db::now_ms() as u64)) |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | pub fn ago_i(ms: impl Num, v: &dyn askama::Values) -> askama::Result<String> { |
|
| 29 | + | ago(ms, v) |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | pub fn when(ms: impl Num, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 33 | + | Ok(render::time::absolute(ms.as_u64())) |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | pub fn when_i(ms: impl Num, v: &dyn askama::Values) -> askama::Result<String> { |
|
| 37 | + | when(ms, v) |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | pub fn bytes(n: impl Num, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 41 | + | Ok(render::bytes(n.as_u64())) |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | pub fn short(hash: &str, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 45 | + | Ok(hash.chars().take(12).collect()) |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// Markdown to HTML (already sanitised), marked safe for the template. |
|
| 49 | + | pub fn markdown(text: &str, _: &dyn askama::Values, base_file: &str, base_raw: &str) -> askama::Result<askama::filters::Safe<String>> { |
|
| 50 | + | Ok(askama::filters::Safe(render::markdown::render(text, base_file, base_raw))) |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | pub fn plural(n: impl Num, _: &dyn askama::Values, one: &str, many: &str) -> askama::Result<String> { |
|
| 54 | + | let n = n.as_u64(); |
|
| 55 | + | Ok(format!("{n} {}", if n == 1 { one } else { many })) |
|
| 56 | + | } |
added crates/hub/src/web/history.rs +148 −0
| 1 | + | //! History, change and compare pages. |
|
| 2 | + | ||
| 3 | + | use super::{filters, lookup, Error, Page, RepoShell, Shell}; |
|
| 4 | + | use crate::render::{self, diff, graph}; |
|
| 5 | + | use crate::repos::view; |
|
| 6 | + | use crate::Shared; |
|
| 7 | + | use askama::Template; |
|
| 8 | + | use axum::extract::{Path, Query, State}; |
|
| 9 | + | use oxy_core::repo::FileDiff; |
|
| 10 | + | use serde::Deserialize; |
|
| 11 | + | use std::collections::BTreeMap; |
|
| 12 | + | ||
| 13 | + | const PAGE: usize = 50; |
|
| 14 | + | ||
| 15 | + | #[derive(Deserialize)] |
|
| 16 | + | pub struct HistoryQuery { |
|
| 17 | + | path: Option<String>, |
|
| 18 | + | offset: Option<usize>, |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | #[derive(Template)] |
|
| 22 | + | #[template(path = "history.html")] |
|
| 23 | + | pub struct HistoryPage { |
|
| 24 | + | shell: Shell, |
|
| 25 | + | repo: RepoShell, |
|
| 26 | + | h: view::History, |
|
| 27 | + | rows: Vec<graph::Row>, |
|
| 28 | + | lanes: usize, |
|
| 29 | + | } |
|
| 30 | + | ||
| 31 | + | pub async fn history( |
|
| 32 | + | State(app): State<Shared>, |
|
| 33 | + | Path((owner, name, r)): Path<(String, String, String)>, |
|
| 34 | + | Query(q): Query<HistoryQuery>, |
|
| 35 | + | ) -> Result<Page<HistoryPage>, Error> { |
|
| 36 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/changes/{r}")); |
|
| 37 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 38 | + | let spec = r.clone(); |
|
| 39 | + | let path = q.path.clone().filter(|p| !p.is_empty()); |
|
| 40 | + | let offset = q.offset.unwrap_or(0); |
|
| 41 | + | let h = app |
|
| 42 | + | .repos |
|
| 43 | + | .read(&repo, move |repo| { |
|
| 44 | + | let target = view::resolve(repo, &spec)?; |
|
| 45 | + | view::history(repo, target, path.as_deref(), offset, PAGE) |
|
| 46 | + | }) |
|
| 47 | + | .await |
|
| 48 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 49 | + | let graph_input: Vec<(String, Vec<String>)> = h.entries.iter().map(|c| (c.hash.clone(), c.deps.clone())).collect(); |
|
| 50 | + | let rows = graph::layout(&graph_input); |
|
| 51 | + | let lanes = graph::width(&rows); |
|
| 52 | + | Ok(Page(HistoryPage { shell, repo: RepoShell::new(&app, &row, "changes"), h, rows, lanes })) |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | /// A file's diff, rendered. |
|
| 56 | + | pub struct FileRows { |
|
| 57 | + | pub path: String, |
|
| 58 | + | pub from: Option<String>, |
|
| 59 | + | pub kind: String, |
|
| 60 | + | pub lines: Vec<diff::Line>, |
|
| 61 | + | pub added: usize, |
|
| 62 | + | pub removed: usize, |
|
| 63 | + | pub anchor: String, |
|
| 64 | + | } |
|
| 65 | + | ||
| 66 | + | pub fn render_files(files: &[FileDiff], bases: &BTreeMap<String, Vec<u8>>) -> Vec<FileRows> { |
|
| 67 | + | files |
|
| 68 | + | .iter() |
|
| 69 | + | .enumerate() |
|
| 70 | + | .map(|(i, f)| { |
|
| 71 | + | let base = bases.get(&f.path).map(Vec::as_slice).unwrap_or(b""); |
|
| 72 | + | let lines = diff::unified(base, &f.hunks, 3); |
|
| 73 | + | FileRows { |
|
| 74 | + | path: f.path.clone(), |
|
| 75 | + | from: f.from.clone(), |
|
| 76 | + | kind: f.kind.clone(), |
|
| 77 | + | added: lines.iter().filter(|l| l.kind == diff::Kind::Add).count(), |
|
| 78 | + | removed: lines.iter().filter(|l| l.kind == diff::Kind::Del).count(), |
|
| 79 | + | lines, |
|
| 80 | + | anchor: format!("diff-{i}"), |
|
| 81 | + | } |
|
| 82 | + | }) |
|
| 83 | + | .collect() |
|
| 84 | + | } |
|
| 85 | + | ||
| 86 | + | #[derive(Template)] |
|
| 87 | + | #[template(path = "change.html")] |
|
| 88 | + | pub struct ChangePage { |
|
| 89 | + | shell: Shell, |
|
| 90 | + | repo: RepoShell, |
|
| 91 | + | c: view::ChangeDetail, |
|
| 92 | + | files: Vec<FileRows>, |
|
| 93 | + | body_html: String, |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | pub async fn change( |
|
| 97 | + | State(app): State<Shared>, |
|
| 98 | + | Path((owner, name, hash)): Path<(String, String, String)>, |
|
| 99 | + | ) -> Result<Page<ChangePage>, Error> { |
|
| 100 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/change/{hash}")); |
|
| 101 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 102 | + | let spec = hash.clone(); |
|
| 103 | + | let c = app |
|
| 104 | + | .repos |
|
| 105 | + | .read(&repo, move |repo| { |
|
| 106 | + | let h = view::resolve_change(repo, &spec)?; |
|
| 107 | + | view::change(repo, &h) |
|
| 108 | + | }) |
|
| 109 | + | .await |
|
| 110 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 111 | + | let files = render_files(&c.files, &c.bases); |
|
| 112 | + | let body_html = |
|
| 113 | + | if c.view.body.is_empty() { String::new() } else { format!("<pre class=\"msg\">{}</pre>", render::escape(&c.view.body)) }; |
|
| 114 | + | Ok(Page(ChangePage { shell, repo: RepoShell::new(&app, &row, "changes"), c, files, body_html })) |
|
| 115 | + | } |
|
| 116 | + | ||
| 117 | + | #[derive(Template)] |
|
| 118 | + | #[template(path = "compare.html")] |
|
| 119 | + | pub struct ComparePage { |
|
| 120 | + | shell: Shell, |
|
| 121 | + | repo: RepoShell, |
|
| 122 | + | cmp: view::Compare, |
|
| 123 | + | files: Vec<FileRows>, |
|
| 124 | + | } |
|
| 125 | + | ||
| 126 | + | /// `/compare/{base}...{head}`. |
|
| 127 | + | pub async fn compare( |
|
| 128 | + | State(app): State<Shared>, |
|
| 129 | + | Path((owner, name, spec)): Path<(String, String, String)>, |
|
| 130 | + | ) -> Result<Page<ComparePage>, Error> { |
|
| 131 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/compare/{spec}")); |
|
| 132 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 133 | + | let Some((a, b)) = spec.split_once("...") else { |
|
| 134 | + | return Err(Error::bad(shell, "compare takes base...head")); |
|
| 135 | + | }; |
|
| 136 | + | let (a, b) = (a.to_string(), b.to_string()); |
|
| 137 | + | let cmp = app |
|
| 138 | + | .repos |
|
| 139 | + | .read(&repo, move |repo| { |
|
| 140 | + | let base = view::resolve(repo, &a)?; |
|
| 141 | + | let head = view::resolve(repo, &b)?; |
|
| 142 | + | view::compare(repo, base, head) |
|
| 143 | + | }) |
|
| 144 | + | .await |
|
| 145 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 146 | + | let files = render_files(&cmp.files, &cmp.bases); |
|
| 147 | + | Ok(Page(ComparePage { shell, repo: RepoShell::new(&app, &row, "changes"), cmp, files })) |
|
| 148 | + | } |
added crates/hub/src/web/home.rs +63 −0
| 1 | + | //! The front page, explore, owner pages and small endpoints. |
|
| 2 | + | ||
| 3 | + | use super::{filters, Error, Page, Shell}; |
|
| 4 | + | use crate::{db, Shared}; |
|
| 5 | + | use askama::Template; |
|
| 6 | + | use axum::extract::{Path, State}; |
|
| 7 | + | use axum::http::header; |
|
| 8 | + | use axum::response::{IntoResponse, Response}; |
|
| 9 | + | use hub_api::Visibility; |
|
| 10 | + | ||
| 11 | + | #[derive(Template)] |
|
| 12 | + | #[template(path = "home.html")] |
|
| 13 | + | pub struct HomePage { |
|
| 14 | + | shell: Shell, |
|
| 15 | + | description: String, |
|
| 16 | + | repos: Vec<db::repos::RepoRow>, |
|
| 17 | + | owner: Option<String>, |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | pub async fn index(State(app): State<Shared>) -> Result<Page<HomePage>, Error> { |
|
| 21 | + | let shell = Shell::new(&app, "/"); |
|
| 22 | + | let repos = db::repos::list(&app.db).await.map_err(|e| Error::from_db(shell.clone(), e))?; |
|
| 23 | + | let repos = repos.into_iter().filter(|r| r.visibility() == Visibility::Public).collect(); |
|
| 24 | + | Ok(Page(HomePage { description: app.config.site.description.clone(), shell, repos, owner: None })) |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | pub async fn explore(State(app): State<Shared>) -> Result<Page<HomePage>, Error> { |
|
| 28 | + | let mut page = index(State(app.clone())).await?; |
|
| 29 | + | page.0.shell = Shell::new(&app, "/explore"); |
|
| 30 | + | Ok(page) |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | pub async fn owner(State(app): State<Shared>, Path(owner): Path<String>) -> Result<Page<HomePage>, Error> { |
|
| 34 | + | let shell = Shell::new(&app, &format!("/{owner}")); |
|
| 35 | + | if !crate::repos::names::valid_owner(&owner) { |
|
| 36 | + | return Err(Error::not_found(shell)); |
|
| 37 | + | } |
|
| 38 | + | let repos = db::repos::list_by_owner(&app.db, &owner).await.map_err(|e| Error::from_db(shell.clone(), e))?; |
|
| 39 | + | let repos: Vec<_> = repos.into_iter().filter(|r| r.visibility() == Visibility::Public).collect(); |
|
| 40 | + | if repos.is_empty() { |
|
| 41 | + | return Err(Error::not_found(shell)); |
|
| 42 | + | } |
|
| 43 | + | Ok(Page(HomePage { description: String::new(), shell, repos, owner: Some(owner) })) |
|
| 44 | + | } |
|
| 45 | + | ||
| 46 | + | pub async fn health(State(app): State<Shared>) -> Response { |
|
| 47 | + | let body = serde_json::json!({ |
|
| 48 | + | "ok": true, |
|
| 49 | + | "version": env!("CARGO_PKG_VERSION"), |
|
| 50 | + | "uptime_s": app.started.elapsed().as_secs(), |
|
| 51 | + | }); |
|
| 52 | + | axum::Json(body).into_response() |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | /// The hub delegate identity object (hub spec §15.5), as `oxy identity |
|
| 56 | + | /// export` writes one, so policy holders can admit it. |
|
| 57 | + | pub async fn hub_key(State(app): State<Shared>) -> Response { |
|
| 58 | + | ( |
|
| 59 | + | [(header::CONTENT_TYPE, "application/oxy-identity"), (header::CONTENT_DISPOSITION, "attachment; filename=\"hub.identity\"")], |
|
| 60 | + | app.hub_key.bytes.clone(), |
|
| 61 | + | ) |
|
| 62 | + | .into_response() |
|
| 63 | + | } |
added crates/hub/src/web/mod.rs +220 −0
| 1 | + | //! Pages (hub spec §6): the router, the shell every template extends, the |
|
| 2 | + | //! error page, and askama filters. Handlers are thin: they resolve names, |
|
| 3 | + | //! call the view layer under the repository lock, and render. |
|
| 4 | + | ||
| 5 | + | pub mod filters; |
|
| 6 | + | mod history; |
|
| 7 | + | mod home; |
|
| 8 | + | mod refs; |
|
| 9 | + | mod repo; |
|
| 10 | + | mod statics; |
|
| 11 | + | ||
| 12 | + | use crate::repos::RepoError; |
|
| 13 | + | use crate::{db, Shared}; |
|
| 14 | + | use askama::Template; |
|
| 15 | + | use axum::extract::DefaultBodyLimit; |
|
| 16 | + | use axum::http::{header, StatusCode}; |
|
| 17 | + | use axum::response::{Html, IntoResponse, Response}; |
|
| 18 | + | use axum::routing::{any, get}; |
|
| 19 | + | use axum::Router; |
|
| 20 | + | use tower_http::compression::CompressionLayer; |
|
| 21 | + | use tower_http::trace::TraceLayer; |
|
| 22 | + | ||
| 23 | + | pub fn router(app: Shared) -> Router { |
|
| 24 | + | let push_limit = app.config.limits.push_body_limit.0 as usize; |
|
| 25 | + | Router::new() |
|
| 26 | + | .route("/", get(home::index)) |
|
| 27 | + | .route("/explore", get(home::explore)) |
|
| 28 | + | .route("/health", get(home::health)) |
|
| 29 | + | .route("/.well-known/oxyhub-key", get(home::hub_key)) |
|
| 30 | + | .route("/style.css", get(statics::style)) |
|
| 31 | + | .route("/favicon.svg", get(statics::favicon)) |
|
| 32 | + | .route("/favicon.ico", get(statics::favicon)) |
|
| 33 | + | .route("/static/{*path}", get(statics::asset)) |
|
| 34 | + | .nest("/api/v1", crate::api::router()) |
|
| 35 | + | .route("/{owner}", get(home::owner)) |
|
| 36 | + | .route("/{owner}/{name}", get(repo::overview)) |
|
| 37 | + | .route("/{owner}/{name}/tree/{r}", get(repo::tree_root)) |
|
| 38 | + | .route("/{owner}/{name}/tree/{r}/{*path}", get(repo::tree)) |
|
| 39 | + | .route("/{owner}/{name}/file/{r}/{*path}", get(repo::file)) |
|
| 40 | + | .route("/{owner}/{name}/last/{r}", get(repo::last_changes)) |
|
| 41 | + | .route("/{owner}/{name}/changes/{r}", get(history::history)) |
|
| 42 | + | .route("/{owner}/{name}/change/{hash}", get(history::change)) |
|
| 43 | + | .route("/{owner}/{name}/compare/{spec}", get(history::compare)) |
|
| 44 | + | .route("/{owner}/{name}/bookmarks", get(refs::bookmarks)) |
|
| 45 | + | .route("/{owner}/{name}/tags", get(refs::tags)) |
|
| 46 | + | .route("/{owner}/{name}/conflicts", get(refs::conflicts)) |
|
| 47 | + | .route("/{owner}/{name}/trust", get(refs::trust)) |
|
| 48 | + | .route("/{owner}/{name}/pushes", get(refs::pushes)) |
|
| 49 | + | .route("/{owner}/{name}/oxy/v1/{endpoint}", any(crate::protocol::dispatch).layer(DefaultBodyLimit::max(push_limit))) |
|
| 50 | + | .fallback(not_found) |
|
| 51 | + | .layer(CompressionLayer::new()) |
|
| 52 | + | .layer(TraceLayer::new_for_http()) |
|
| 53 | + | .with_state(app) |
|
| 54 | + | } |
|
| 55 | + | ||
| 56 | + | /// What every page's chrome needs. |
|
| 57 | + | #[derive(Clone, Debug)] |
|
| 58 | + | pub struct Shell { |
|
| 59 | + | pub site: String, |
|
| 60 | + | pub base_url: String, |
|
| 61 | + | pub path: String, |
|
| 62 | + | pub version: &'static str, |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | impl Shell { |
|
| 66 | + | pub fn new(app: &Shared, path: &str) -> Shell { |
|
| 67 | + | Shell { |
|
| 68 | + | site: app.config.site.name.clone(), |
|
| 69 | + | base_url: app.config.base_url().to_string(), |
|
| 70 | + | path: path.to_string(), |
|
| 71 | + | version: env!("CARGO_PKG_VERSION"), |
|
| 72 | + | } |
|
| 73 | + | } |
|
| 74 | + | } |
|
| 75 | + | ||
| 76 | + | /// The repository chrome: tabs and counts. |
|
| 77 | + | #[derive(Clone, Debug)] |
|
| 78 | + | pub struct RepoShell { |
|
| 79 | + | pub owner: String, |
|
| 80 | + | pub name: String, |
|
| 81 | + | pub url: String, |
|
| 82 | + | pub description: String, |
|
| 83 | + | pub default_bookmark: String, |
|
| 84 | + | pub visibility: String, |
|
| 85 | + | pub tab: &'static str, |
|
| 86 | + | pub clone_url: String, |
|
| 87 | + | } |
|
| 88 | + | ||
| 89 | + | impl RepoShell { |
|
| 90 | + | pub fn new(app: &Shared, row: &db::repos::RepoRow, tab: &'static str) -> RepoShell { |
|
| 91 | + | RepoShell { |
|
| 92 | + | owner: row.owner.clone(), |
|
| 93 | + | name: row.name.clone(), |
|
| 94 | + | url: format!("/{}/{}", row.owner, row.name), |
|
| 95 | + | description: row.description.clone(), |
|
| 96 | + | default_bookmark: row.default_bookmark.clone(), |
|
| 97 | + | visibility: row.visibility.clone(), |
|
| 98 | + | tab, |
|
| 99 | + | clone_url: format!("{}/{}/{}", app.config.base_url(), row.owner, row.name), |
|
| 100 | + | } |
|
| 101 | + | } |
|
| 102 | + | } |
|
| 103 | + | ||
| 104 | + | /// Render a template as an HTML response. |
|
| 105 | + | pub struct Page<T: Template>(pub T); |
|
| 106 | + | ||
| 107 | + | impl<T: Template> IntoResponse for Page<T> { |
|
| 108 | + | fn into_response(self) -> Response { |
|
| 109 | + | match self.0.render() { |
|
| 110 | + | Ok(s) => Html(s).into_response(), |
|
| 111 | + | Err(e) => { |
|
| 112 | + | tracing::error!("template: {e}"); |
|
| 113 | + | (StatusCode::INTERNAL_SERVER_ERROR, "template error").into_response() |
|
| 114 | + | } |
|
| 115 | + | } |
|
| 116 | + | } |
|
| 117 | + | } |
|
| 118 | + | ||
| 119 | + | #[derive(Template)] |
|
| 120 | + | #[template(path = "error.html")] |
|
| 121 | + | struct ErrorPage { |
|
| 122 | + | shell: Shell, |
|
| 123 | + | status: u16, |
|
| 124 | + | title: String, |
|
| 125 | + | detail: String, |
|
| 126 | + | } |
|
| 127 | + | ||
| 128 | + | /// A page-level failure, rendered inside the shell. |
|
| 129 | + | #[derive(Debug)] |
|
| 130 | + | pub struct Error { |
|
| 131 | + | pub status: StatusCode, |
|
| 132 | + | pub detail: String, |
|
| 133 | + | shell: Option<Shell>, |
|
| 134 | + | } |
|
| 135 | + | ||
| 136 | + | impl Error { |
|
| 137 | + | pub fn not_found(shell: Shell) -> Error { |
|
| 138 | + | Error { status: StatusCode::NOT_FOUND, detail: "There is nothing at this address.".into(), shell: Some(shell) } |
|
| 139 | + | } |
|
| 140 | + | ||
| 141 | + | pub fn bad(shell: Shell, detail: impl Into<String>) -> Error { |
|
| 142 | + | Error { status: StatusCode::BAD_REQUEST, detail: detail.into(), shell: Some(shell) } |
|
| 143 | + | } |
|
| 144 | + | ||
| 145 | + | pub fn from_repo(shell: Shell, e: RepoError) -> Error { |
|
| 146 | + | match e { |
|
| 147 | + | RepoError::NotFound => Error::not_found(shell), |
|
| 148 | + | RepoError::Oxy(oxy_core::repo::RepoError::Msg(m)) => Error { status: StatusCode::NOT_FOUND, detail: m, shell: Some(shell) }, |
|
| 149 | + | other => Error { status: StatusCode::INTERNAL_SERVER_ERROR, detail: other.to_string(), shell: Some(shell) }, |
|
| 150 | + | } |
|
| 151 | + | } |
|
| 152 | + | ||
| 153 | + | pub fn from_db(shell: Shell, e: sqlx::Error) -> Error { |
|
| 154 | + | Error { status: StatusCode::INTERNAL_SERVER_ERROR, detail: e.to_string(), shell: Some(shell) } |
|
| 155 | + | } |
|
| 156 | + | } |
|
| 157 | + | ||
| 158 | + | impl IntoResponse for Error { |
|
| 159 | + | fn into_response(self) -> Response { |
|
| 160 | + | let title = match self.status { |
|
| 161 | + | StatusCode::NOT_FOUND => "Not found", |
|
| 162 | + | StatusCode::BAD_REQUEST => "Bad request", |
|
| 163 | + | _ => "Something went wrong", |
|
| 164 | + | }; |
|
| 165 | + | if self.status.is_server_error() { |
|
| 166 | + | tracing::error!("{}", self.detail); |
|
| 167 | + | } |
|
| 168 | + | let shell = self.shell.unwrap_or(Shell { site: "Oxygenhub".into(), base_url: String::new(), path: String::new(), version: "" }); |
|
| 169 | + | let page = ErrorPage { shell, status: self.status.as_u16(), title: title.into(), detail: self.detail }; |
|
| 170 | + | match page.render() { |
|
| 171 | + | Ok(s) => (self.status, Html(s)).into_response(), |
|
| 172 | + | Err(_) => (self.status, title).into_response(), |
|
| 173 | + | } |
|
| 174 | + | } |
|
| 175 | + | } |
|
| 176 | + | ||
| 177 | + | async fn not_found(axum::extract::State(app): axum::extract::State<Shared>, uri: axum::http::Uri) -> Response { |
|
| 178 | + | Error::not_found(Shell::new(&app, uri.path())).into_response() |
|
| 179 | + | } |
|
| 180 | + | ||
| 181 | + | /// A raw file response with a safe content type (hub spec §15.2). |
|
| 182 | + | pub fn raw(bytes: Vec<u8>, name: &str, inline_image: bool) -> Response { |
|
| 183 | + | let mime = mime_of(name); |
|
| 184 | + | let disposition = if inline_image && mime.starts_with("image/") { "inline" } else { "attachment" }; |
|
| 185 | + | ( |
|
| 186 | + | [ |
|
| 187 | + | (header::CONTENT_TYPE, mime.to_string()), |
|
| 188 | + | (header::CONTENT_DISPOSITION, format!("{disposition}; filename=\"{}\"", name.replace('"', ""))), |
|
| 189 | + | (header::X_CONTENT_TYPE_OPTIONS, "nosniff".to_string()), |
|
| 190 | + | ], |
|
| 191 | + | bytes, |
|
| 192 | + | ) |
|
| 193 | + | .into_response() |
|
| 194 | + | } |
|
| 195 | + | ||
| 196 | + | pub fn mime_of(name: &str) -> &'static str { |
|
| 197 | + | match name.rsplit('.').next().map(|e| e.to_ascii_lowercase()).as_deref() { |
|
| 198 | + | Some("png") => "image/png", |
|
| 199 | + | Some("jpg" | "jpeg") => "image/jpeg", |
|
| 200 | + | Some("gif") => "image/gif", |
|
| 201 | + | Some("webp") => "image/webp", |
|
| 202 | + | Some("svg") => "image/svg+xml", |
|
| 203 | + | Some("pdf") => "application/pdf", |
|
| 204 | + | Some( |
|
| 205 | + | "txt" | "md" | "rs" | "toml" | "json" | "yml" | "yaml" | "c" | "h" | "cpp" | "hpp" | "py" | "js" | "ts" | "html" | "css" | "sh", |
|
| 206 | + | ) => "text/plain; charset=utf-8", |
|
| 207 | + | _ => "application/octet-stream", |
|
| 208 | + | } |
|
| 209 | + | } |
|
| 210 | + | ||
| 211 | + | /// Resolve `{owner}/{name}` to its row, or 404. Private repositories are |
|
| 212 | + | /// invisible until accounts exist (hub 0.2). |
|
| 213 | + | pub async fn lookup(app: &Shared, shell: &Shell, owner: &str, name: &str) -> Result<(crate::repos::RepoName, db::repos::RepoRow), Error> { |
|
| 214 | + | let repo = crate::repos::RepoName::new(owner, name).ok_or_else(|| Error::not_found(shell.clone()))?; |
|
| 215 | + | let row = db::repos::get(&app.db, owner, name).await.map_err(|e| Error::from_db(shell.clone(), e))?; |
|
| 216 | + | match row { |
|
| 217 | + | Some(r) if r.visibility() == hub_api::Visibility::Public => Ok((repo, r)), |
|
| 218 | + | _ => Err(Error::not_found(shell.clone())), |
|
| 219 | + | } |
|
| 220 | + | } |
added crates/hub/src/web/refs.rs +84 −0
| 1 | + | //! Bookmarks, tags, conflicts, trust and pushes pages. |
|
| 2 | + | ||
| 3 | + | use super::{filters, lookup, Error, Page, RepoShell, Shell}; |
|
| 4 | + | use crate::repos::view; |
|
| 5 | + | use crate::{db, Shared}; |
|
| 6 | + | use askama::Template; |
|
| 7 | + | use axum::extract::{Path, State}; |
|
| 8 | + | ||
| 9 | + | #[derive(Template)] |
|
| 10 | + | #[template(path = "bookmarks.html")] |
|
| 11 | + | pub struct BookmarksPage { |
|
| 12 | + | shell: Shell, |
|
| 13 | + | repo: RepoShell, |
|
| 14 | + | bookmarks: Vec<view::BookmarkView>, |
|
| 15 | + | } |
|
| 16 | + | ||
| 17 | + | pub async fn bookmarks(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<BookmarksPage>, Error> { |
|
| 18 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/bookmarks")); |
|
| 19 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 20 | + | let default = row.default_bookmark.clone(); |
|
| 21 | + | let bookmarks = app.repos.read(&repo, move |r| view::bookmarks(r, &default)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 22 | + | Ok(Page(BookmarksPage { shell, repo: RepoShell::new(&app, &row, "bookmarks"), bookmarks })) |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | #[derive(Template)] |
|
| 26 | + | #[template(path = "tags.html")] |
|
| 27 | + | pub struct TagsPage { |
|
| 28 | + | shell: Shell, |
|
| 29 | + | repo: RepoShell, |
|
| 30 | + | tags: Vec<view::TagView>, |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | pub async fn tags(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<TagsPage>, Error> { |
|
| 34 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/tags")); |
|
| 35 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 36 | + | let tags = app.repos.read(&repo, view::tags).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 37 | + | Ok(Page(TagsPage { shell, repo: RepoShell::new(&app, &row, "tags"), tags })) |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | #[derive(Template)] |
|
| 41 | + | #[template(path = "conflicts.html")] |
|
| 42 | + | pub struct ConflictsPage { |
|
| 43 | + | shell: Shell, |
|
| 44 | + | repo: RepoShell, |
|
| 45 | + | conflicts: Vec<view::ConflictView>, |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | pub async fn conflicts(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<ConflictsPage>, Error> { |
|
| 49 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/conflicts")); |
|
| 50 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 51 | + | let conflicts = app.repos.read(&repo, view::conflicts).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 52 | + | Ok(Page(ConflictsPage { shell, repo: RepoShell::new(&app, &row, "conflicts"), conflicts })) |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | #[derive(Template)] |
|
| 56 | + | #[template(path = "trust.html")] |
|
| 57 | + | pub struct TrustPage { |
|
| 58 | + | shell: Shell, |
|
| 59 | + | repo: RepoShell, |
|
| 60 | + | t: view::Trust, |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | pub async fn trust(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<TrustPage>, Error> { |
|
| 64 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/trust")); |
|
| 65 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 66 | + | let key = app.hub_key.key; |
|
| 67 | + | let t = app.repos.read(&repo, move |r| view::trust(r, &key)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 68 | + | Ok(Page(TrustPage { shell, repo: RepoShell::new(&app, &row, "trust"), t })) |
|
| 69 | + | } |
|
| 70 | + | ||
| 71 | + | #[derive(Template)] |
|
| 72 | + | #[template(path = "pushes.html")] |
|
| 73 | + | pub struct PushesPage { |
|
| 74 | + | shell: Shell, |
|
| 75 | + | repo: RepoShell, |
|
| 76 | + | pushes: Vec<db::repos::PushRow>, |
|
| 77 | + | } |
|
| 78 | + | ||
| 79 | + | pub async fn pushes(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<PushesPage>, Error> { |
|
| 80 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/pushes")); |
|
| 81 | + | let (_, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 82 | + | let pushes = db::repos::pushes(&app.db, row.id, 200).await.map_err(|e| Error::from_db(shell.clone(), e))?; |
|
| 83 | + | Ok(Page(PushesPage { shell, repo: RepoShell::new(&app, &row, "pushes"), pushes })) |
|
| 84 | + | } |
added crates/hub/src/web/repo.rs +221 −0
| 1 | + | //! Repository overview, tree, file, raw and blame pages. |
|
| 2 | + | ||
| 3 | + | use super::{filters, lookup, Error, Page, RepoShell, Shell}; |
|
| 4 | + | use crate::render::{self, highlight}; |
|
| 5 | + | use crate::repos::view::{self, EntryKind, FileBody}; |
|
| 6 | + | use crate::Shared; |
|
| 7 | + | use askama::Template; |
|
| 8 | + | use axum::extract::{Path, Query, State}; |
|
| 9 | + | use axum::response::{IntoResponse, Response}; |
|
| 10 | + | use serde::Deserialize; |
|
| 11 | + | ||
| 12 | + | #[derive(Template)] |
|
| 13 | + | #[template(path = "overview.html")] |
|
| 14 | + | pub struct OverviewPage { |
|
| 15 | + | shell: Shell, |
|
| 16 | + | repo: RepoShell, |
|
| 17 | + | o: view::Overview, |
|
| 18 | + | readme_html: Option<String>, |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | pub async fn overview(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Page<OverviewPage>, Error> { |
|
| 22 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}")); |
|
| 23 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 24 | + | let default = row.default_bookmark.clone(); |
|
| 25 | + | let o = app.repos.read(&repo, move |r| view::overview(r, &default)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 26 | + | let rs = RepoShell::new(&app, &row, "code"); |
|
| 27 | + | let readme_html = o.readme_html(&rs, &o.tree.target.label); |
|
| 28 | + | Ok(Page(OverviewPage { shell, repo: rs, o, readme_html })) |
|
| 29 | + | } |
|
| 30 | + | ||
| 31 | + | impl view::Overview { |
|
| 32 | + | fn readme_html(&self, rs: &RepoShell, r: &str) -> Option<String> { |
|
| 33 | + | self.tree.readme.as_ref().map(|md| { |
|
| 34 | + | let dir = md.path.rsplit_once('/').map(|(d, _)| format!("{d}/")).unwrap_or_default(); |
|
| 35 | + | render::markdown::render(&md.text, &format!("{}/file/{r}/{dir}", rs.url), &format!("{}/file/{r}/{dir}", rs.url)) |
|
| 36 | + | }) |
|
| 37 | + | } |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | #[derive(Template)] |
|
| 41 | + | #[template(path = "tree.html")] |
|
| 42 | + | pub struct TreePage { |
|
| 43 | + | shell: Shell, |
|
| 44 | + | repo: RepoShell, |
|
| 45 | + | t: view::Tree, |
|
| 46 | + | readme_html: Option<String>, |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | pub async fn tree_root(State(app): State<Shared>, Path((owner, name, r)): Path<(String, String, String)>) -> Result<Page<TreePage>, Error> { |
|
| 50 | + | tree(State(app), Path((owner, name, r, String::new()))).await |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | pub async fn tree( |
|
| 54 | + | State(app): State<Shared>, |
|
| 55 | + | Path((owner, name, r, path)): Path<(String, String, String, String)>, |
|
| 56 | + | ) -> Result<Page<TreePage>, Error> { |
|
| 57 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/tree/{r}/{path}")); |
|
| 58 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 59 | + | let (spec, dir) = (r.clone(), path.clone()); |
|
| 60 | + | let t = app |
|
| 61 | + | .repos |
|
| 62 | + | .read(&repo, move |repo| { |
|
| 63 | + | let target = view::resolve(repo, &spec)?; |
|
| 64 | + | view::tree(repo, target, &dir) |
|
| 65 | + | }) |
|
| 66 | + | .await |
|
| 67 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 68 | + | let rs = RepoShell::new(&app, &row, "code"); |
|
| 69 | + | let readme_html = t.readme.as_ref().map(|md| { |
|
| 70 | + | let dir = md.path.rsplit_once('/').map(|(d, _)| format!("{d}/")).unwrap_or_default(); |
|
| 71 | + | render::markdown::render(&md.text, &format!("{}/file/{r}/{dir}", rs.url), &format!("{}/file/{r}/{dir}", rs.url)) |
|
| 72 | + | }); |
|
| 73 | + | Ok(Page(TreePage { shell, repo: rs, t, readme_html })) |
|
| 74 | + | } |
|
| 75 | + | ||
| 76 | + | #[derive(Deserialize)] |
|
| 77 | + | pub struct FileQuery { |
|
| 78 | + | plain: Option<String>, |
|
| 79 | + | blame: Option<String>, |
|
| 80 | + | /// `source` shows a Markdown file's text instead of rendering it. |
|
| 81 | + | view: Option<String>, |
|
| 82 | + | } |
|
| 83 | + | ||
| 84 | + | #[derive(Template)] |
|
| 85 | + | #[template(path = "file.html")] |
|
| 86 | + | pub struct FilePage { |
|
| 87 | + | shell: Shell, |
|
| 88 | + | repo: RepoShell, |
|
| 89 | + | f: view::FileView, |
|
| 90 | + | /// Highlighted lines when text. |
|
| 91 | + | lines: Vec<String>, |
|
| 92 | + | language: Option<&'static str>, |
|
| 93 | + | markdown_html: Option<String>, |
|
| 94 | + | is_image: bool, |
|
| 95 | + | is_markdown: bool, |
|
| 96 | + | blame: Option<Vec<BlameRow>>, |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | /// A blame gutter entry: shown only on the first line of a run. |
|
| 100 | + | pub struct BlameRow { |
|
| 101 | + | pub first: bool, |
|
| 102 | + | pub hash: String, |
|
| 103 | + | pub short: String, |
|
| 104 | + | pub author: String, |
|
| 105 | + | pub when_ms: u64, |
|
| 106 | + | pub run: usize, |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | pub async fn file( |
|
| 110 | + | State(app): State<Shared>, |
|
| 111 | + | Path((owner, name, r, path)): Path<(String, String, String, String)>, |
|
| 112 | + | Query(q): Query<FileQuery>, |
|
| 113 | + | ) -> Result<Response, Error> { |
|
| 114 | + | let shell = Shell::new(&app, &format!("/{owner}/{name}/file/{r}/{path}")); |
|
| 115 | + | let (repo, row) = lookup(&app, &shell, &owner, &name).await?; |
|
| 116 | + | let limit = app.config.limits.render_limit.0; |
|
| 117 | + | let want_blame = q.blame.is_some(); |
|
| 118 | + | let (spec, p) = (r.clone(), path.clone()); |
|
| 119 | + | let (f, blame) = app |
|
| 120 | + | .repos |
|
| 121 | + | .read(&repo, move |repo| { |
|
| 122 | + | let target = view::resolve(repo, &spec)?; |
|
| 123 | + | if want_blame { |
|
| 124 | + | let b = view::blame(repo, target, &p, limit)?; |
|
| 125 | + | Ok((b.file, Some(b.lines))) |
|
| 126 | + | } else { |
|
| 127 | + | Ok((view::file(repo, target, &p, limit)?, None)) |
|
| 128 | + | } |
|
| 129 | + | }) |
|
| 130 | + | .await |
|
| 131 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 132 | + | if q.plain.is_some() { |
|
| 133 | + | return Ok(super::raw(f.bytes, &f.name, true)); |
|
| 134 | + | } |
|
| 135 | + | let rs = RepoShell::new(&app, &row, "code"); |
|
| 136 | + | let is_image = super::mime_of(&f.name).starts_with("image/"); |
|
| 137 | + | let is_markdown = f.name.to_lowercase().ends_with(".md") || f.name.to_lowercase() == "readme"; |
|
| 138 | + | let want_source = q.view.as_deref() == Some("source") || blame.is_some(); |
|
| 139 | + | let (lines, language, markdown_html) = match &f.body { |
|
| 140 | + | FileBody::Text(text) if !is_image => { |
|
| 141 | + | if is_markdown && !want_source { |
|
| 142 | + | let dir = f.path.rsplit_once('/').map(|(d, _)| format!("{d}/")).unwrap_or_default(); |
|
| 143 | + | let base = format!("{}/file/{r}/{dir}", rs.url); |
|
| 144 | + | (vec![], highlight::language(&f.path), Some(render::markdown::render(text, &base, &base))) |
|
| 145 | + | } else { |
|
| 146 | + | (highlight::lines(&f.path, text), highlight::language(&f.path), None) |
|
| 147 | + | } |
|
| 148 | + | } |
|
| 149 | + | _ => (vec![], None, None), |
|
| 150 | + | }; |
|
| 151 | + | let blame = blame.map(|lines| { |
|
| 152 | + | let mut rows: Vec<BlameRow> = Vec::with_capacity(lines.len()); |
|
| 153 | + | let mut i = 0; |
|
| 154 | + | while i < lines.len() { |
|
| 155 | + | let mut run = 1; |
|
| 156 | + | while i + run < lines.len() && lines[i + run].hash == lines[i].hash { |
|
| 157 | + | run += 1; |
|
| 158 | + | } |
|
| 159 | + | for k in 0..run { |
|
| 160 | + | let l = &lines[i + k]; |
|
| 161 | + | rows.push(BlameRow { |
|
| 162 | + | first: k == 0, |
|
| 163 | + | hash: l.hash.to_hex(), |
|
| 164 | + | short: l.hash.short(8), |
|
| 165 | + | author: l.author.clone(), |
|
| 166 | + | when_ms: l.when_ms, |
|
| 167 | + | run, |
|
| 168 | + | }); |
|
| 169 | + | } |
|
| 170 | + | i += run; |
|
| 171 | + | } |
|
| 172 | + | rows |
|
| 173 | + | }); |
|
| 174 | + | Ok(Page(FilePage { shell, repo: rs, f, lines, language, markdown_html, is_image, is_markdown, blame }).into_response()) |
|
| 175 | + | } |
|
| 176 | + | ||
| 177 | + | #[derive(Deserialize)] |
|
| 178 | + | pub struct LastQuery { |
|
| 179 | + | /// Comma-separated paths. |
|
| 180 | + | paths: String, |
|
| 181 | + | } |
|
| 182 | + | ||
| 183 | + | /// `GET /{owner}/{name}/last/{ref}?paths=a,b` → JSON of last change per |
|
| 184 | + | /// path, fetched by the tree page after it paints (hub spec §6.5). |
|
| 185 | + | pub async fn last_changes( |
|
| 186 | + | State(app): State<Shared>, |
|
| 187 | + | Path((owner, name, r)): Path<(String, String, String)>, |
|
| 188 | + | Query(q): Query<LastQuery>, |
|
| 189 | + | ) -> Result<Response, Error> { |
|
| 190 | + | let shell = Shell::new(&app, ""); |
|
| 191 | + | let (repo, _) = lookup(&app, &shell, &owner, &name).await?; |
|
| 192 | + | let paths: Vec<String> = q.paths.split(',').filter(|p| !p.is_empty()).take(500).map(String::from).collect(); |
|
| 193 | + | let spec = r.clone(); |
|
| 194 | + | let found = app |
|
| 195 | + | .repos |
|
| 196 | + | .read(&repo, move |repo| { |
|
| 197 | + | let target = view::resolve(repo, &spec)?; |
|
| 198 | + | view::last_changes(repo, &target.heads, &paths) |
|
| 199 | + | }) |
|
| 200 | + | .await |
|
| 201 | + | .map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 202 | + | let now = crate::db::now_ms() as u64; |
|
| 203 | + | let body: Vec<serde_json::Value> = found |
|
| 204 | + | .into_iter() |
|
| 205 | + | .map(|(p, c)| { |
|
| 206 | + | serde_json::json!({ "path": p, "hash": c.hash, "short": c.short, "title": c.title, "ago": render::time::ago(c.when_ms, now), "when": render::time::absolute(c.when_ms) }) |
|
| 207 | + | }) |
|
| 208 | + | .collect(); |
|
| 209 | + | Ok(axum::Json(body).into_response()) |
|
| 210 | + | } |
|
| 211 | + | ||
| 212 | + | impl EntryKind { |
|
| 213 | + | pub fn glyph(&self) -> &'static str { |
|
| 214 | + | match self { |
|
| 215 | + | EntryKind::Dir => "▸", |
|
| 216 | + | EntryKind::File => "·", |
|
| 217 | + | EntryKind::Symlink => "↪", |
|
| 218 | + | EntryKind::Subrepo => "◫", |
|
| 219 | + | } |
|
| 220 | + | } |
|
| 221 | + | } |
added crates/hub/src/web/statics.rs +46 −0
| 1 | + | //! Compiled-in static assets: the stylesheet, fonts and icons. |
|
| 2 | + | ||
| 3 | + | use axum::extract::Path; |
|
| 4 | + | use axum::http::{header, StatusCode}; |
|
| 5 | + | use axum::response::{IntoResponse, Response}; |
|
| 6 | + | ||
| 7 | + | const CSS: &str = include_str!("../../../../templates/style.css"); |
|
| 8 | + | const FAVICON: &str = include_str!("../../../../assets/icons/oxygenhub.svg"); |
|
| 9 | + | ||
| 10 | + | struct Asset { |
|
| 11 | + | path: &'static str, |
|
| 12 | + | mime: &'static str, |
|
| 13 | + | bytes: &'static [u8], |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | const ASSETS: &[Asset] = &[ |
|
| 17 | + | Asset { path: "fonts/Barlow-300.woff2", mime: "font/woff2", bytes: include_bytes!("../../../../assets/fonts/Barlow-300.woff2") }, |
|
| 18 | + | Asset { path: "fonts/Barlow-400.woff2", mime: "font/woff2", bytes: include_bytes!("../../../../assets/fonts/Barlow-400.woff2") }, |
|
| 19 | + | Asset { path: "fonts/Barlow-500.woff2", mime: "font/woff2", bytes: include_bytes!("../../../../assets/fonts/Barlow-500.woff2") }, |
|
| 20 | + | Asset { path: "fonts/Barlow-600.woff2", mime: "font/woff2", bytes: include_bytes!("../../../../assets/fonts/Barlow-600.woff2") }, |
|
| 21 | + | Asset { |
|
| 22 | + | path: "fonts/PressStart2P-400.woff2", |
|
| 23 | + | mime: "font/woff2", |
|
| 24 | + | bytes: include_bytes!("../../../../assets/fonts/PressStart2P-400.woff2"), |
|
| 25 | + | }, |
|
| 26 | + | Asset { path: "hub.js", mime: "text/javascript; charset=utf-8", bytes: include_bytes!("../../../../assets/hub.js") }, |
|
| 27 | + | ]; |
|
| 28 | + | ||
| 29 | + | const CACHE: &str = "public, max-age=86400"; |
|
| 30 | + | ||
| 31 | + | pub async fn style() -> Response { |
|
| 32 | + | ([(header::CONTENT_TYPE, "text/css; charset=utf-8"), (header::CACHE_CONTROL, CACHE)], CSS).into_response() |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | pub async fn favicon() -> Response { |
|
| 36 | + | ([(header::CONTENT_TYPE, "image/svg+xml"), (header::CACHE_CONTROL, CACHE)], FAVICON).into_response() |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | pub async fn asset(Path(path): Path<String>) -> Response { |
|
| 40 | + | match ASSETS.iter().find(|a| a.path == path) { |
|
| 41 | + | Some(a) => { |
|
| 42 | + | ([(header::CONTENT_TYPE, a.mime), (header::CACHE_CONTROL, "public, max-age=31536000, immutable")], a.bytes).into_response() |
|
| 43 | + | } |
|
| 44 | + | None => StatusCode::NOT_FOUND.into_response(), |
|
| 45 | + | } |
|
| 46 | + | } |
added crates/hub/tests/browse.rs +221 −0
| 1 | + | //! End to end: a hub in a temporary data directory, a real `oxy` client |
|
| 2 | + | //! pushing over the wire protocol, and every milestone 0.1 page fetched. |
|
| 3 | + | ||
| 4 | + | use axum::body::Body; |
|
| 5 | + | use axum::http::{Request, StatusCode}; |
|
| 6 | + | use http_body_util::BodyExt; |
|
| 7 | + | use oxy_core::repo::Repo; |
|
| 8 | + | use oxygenhub::config::Config; |
|
| 9 | + | use oxygenhub::repos::RepoName; |
|
| 10 | + | use oxygenhub::{db, Shared}; |
|
| 11 | + | use std::path::Path; |
|
| 12 | + | use tower::ServiceExt; |
|
| 13 | + | ||
| 14 | + | /// Point `$OXY_HOME` at a throwaway directory so the client's identity and |
|
| 15 | + | /// configuration never touch the developer's. |
|
| 16 | + | fn hermetic() { |
|
| 17 | + | static ONCE: std::sync::Once = std::sync::Once::new(); |
|
| 18 | + | static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new(); |
|
| 19 | + | ONCE.call_once(|| { |
|
| 20 | + | let d = tempfile::tempdir().unwrap(); |
|
| 21 | + | std::env::set_var("OXY_HOME", d.path()); |
|
| 22 | + | DIR.set(d).ok(); |
|
| 23 | + | }); |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | fn write(root: &Path, rel: &str, content: &[u8]) { |
|
| 27 | + | let p = root.join(rel); |
|
| 28 | + | std::fs::create_dir_all(p.parent().unwrap()).unwrap(); |
|
| 29 | + | std::fs::write(p, content).unwrap(); |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | struct Hub { |
|
| 33 | + | app: Shared, |
|
| 34 | + | base: String, |
|
| 35 | + | _dir: tempfile::TempDir, |
|
| 36 | + | } |
|
| 37 | + | ||
| 38 | + | async fn hub(push_tokens: Vec<String>) -> Hub { |
|
| 39 | + | hermetic(); |
|
| 40 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 41 | + | let mut config = Config::load(dir.path()).unwrap(); |
|
| 42 | + | config.auth.push_tokens = push_tokens; |
|
| 43 | + | let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
|
| 44 | + | let port = listener.local_addr().unwrap().port(); |
|
| 45 | + | config.server.base_url = format!("http://127.0.0.1:{port}"); |
|
| 46 | + | let app = oxygenhub::build(config).await.unwrap(); |
|
| 47 | + | let router = oxygenhub::router(app.clone()); |
|
| 48 | + | tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); |
|
| 49 | + | Hub { app, base: format!("http://127.0.0.1:{port}"), _dir: dir } |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | async fn get(app: &Shared, path: &str) -> (StatusCode, String) { |
|
| 53 | + | let res = oxygenhub::router(app.clone()).oneshot(Request::get(path).body(Body::empty()).unwrap()).await.unwrap(); |
|
| 54 | + | let status = res.status(); |
|
| 55 | + | let bytes = res.into_body().collect().await.unwrap().to_bytes(); |
|
| 56 | + | (status, String::from_utf8_lossy(&bytes).into_owned()) |
|
| 57 | + | } |
|
| 58 | + | ||
| 59 | + | async fn page(app: &Shared, path: &str) -> String { |
|
| 60 | + | let (status, body) = get(app, path).await; |
|
| 61 | + | assert_eq!(status, StatusCode::OK, "{path}: {}", body.lines().find(|l| l.contains("lead")).unwrap_or("")); |
|
| 62 | + | body |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | /// Push a small history from a client: two changes on main, a second |
|
| 66 | + | /// bookmark, a tag. |
|
| 67 | + | fn populate(url: &str, work: &Path) -> (Repo, String, String) { |
|
| 68 | + | // The hosted repository is unbound until the first push, so it cannot |
|
| 69 | + | // be cloned yet: start locally and push, as moving an existing |
|
| 70 | + | // repository to the hub does. |
|
| 71 | + | assert!(Repo::clone(url, &work.join("early")).err().unwrap().to_string().contains("unbound")); |
|
| 72 | + | let mut client = Repo::init(work).unwrap(); |
|
| 73 | + | // A signing identity, so changes and the tag travel signed. |
|
| 74 | + | client.create_identity("Test Author", "[email protected]", oxy_core::repo::IdentityScope::User).unwrap(); |
|
| 75 | + | client.config.set("remote.origin.url", url).unwrap(); |
|
| 76 | + | client.config.set("core.sign", "change").unwrap(); |
|
| 77 | + | client.save_config().unwrap(); |
|
| 78 | + | write(work, "README.md", b"# Demo\n\nSee [docs](docs/guide.md).\n"); |
|
| 79 | + | write(work, "src/main.rs", b"fn main() {\n println!(\"hi\");\n}\n"); |
|
| 80 | + | write(work, "docs/guide.md", b"# Guide\n"); |
|
| 81 | + | client.snapshot("test").unwrap(); |
|
| 82 | + | let c1 = client.commit("Initial import", &[]).unwrap(); |
|
| 83 | + | write(work, "src/main.rs", b"fn main() {\n println!(\"hello\");\n}\n\nfn two() {}\n"); |
|
| 84 | + | client.snapshot("test").unwrap(); |
|
| 85 | + | let c2 = client.commit("Greet properly\n\nAlso adds two().", &[]).unwrap(); |
|
| 86 | + | client.tag_create("v0.1", &[c2], "First tag").unwrap(); |
|
| 87 | + | client.bookmark_create("feature", Some(&[c1])).unwrap(); |
|
| 88 | + | let results = client.push("origin", false).unwrap(); |
|
| 89 | + | assert!(results.iter().all(|r| r.ok), "{results:?}"); |
|
| 90 | + | let results = client.push_bookmark("origin", "feature", false).unwrap(); |
|
| 91 | + | assert!(results.iter().all(|r| r.ok), "{results:?}"); |
|
| 92 | + | // Bound now: a clone shares the id. |
|
| 93 | + | let clone = Repo::clone(url, &work.join("clone")).unwrap(); |
|
| 94 | + | assert_eq!(clone.id().unwrap(), client.id().unwrap()); |
|
| 95 | + | (client, c1.to_hex(), c2.to_hex()) |
|
| 96 | + | } |
|
| 97 | + | ||
| 98 | + | #[tokio::test(flavor = "multi_thread")] |
|
| 99 | + | async fn create_push_and_browse() { |
|
| 100 | + | let h = hub(vec![]).await; |
|
| 101 | + | let name = RepoName::parse("andy/demo").unwrap(); |
|
| 102 | + | h.app.repos.create(&name, None).unwrap(); |
|
| 103 | + | db::repos::create(&h.app.db, "andy", "demo", "A demo", hub_api::Visibility::Public).await.unwrap(); |
|
| 104 | + | let work = tempfile::tempdir().unwrap(); |
|
| 105 | + | let (_client, c1, c2) = populate(&format!("{}/andy/demo", h.base), work.path()); |
|
| 106 | + | ||
| 107 | + | // Front page and owner page list it. |
|
| 108 | + | let home = page(&h.app, "/").await; |
|
| 109 | + | assert!(home.contains("/andy/demo"), "{home}"); |
|
| 110 | + | assert!(page(&h.app, "/andy").await.contains("demo")); |
|
| 111 | + | assert_eq!(get(&h.app, "/nobody").await.0, StatusCode::NOT_FOUND); |
|
| 112 | + | ||
| 113 | + | // Overview: tree, README rendered with rebased links, clone URL, counts. |
|
| 114 | + | let o = page(&h.app, "/andy/demo").await; |
|
| 115 | + | assert!(o.contains("src"), "tree entries"); |
|
| 116 | + | assert!(o.contains("href=\"/andy/demo/file/main/docs/guide.md\""), "README link rebased: {o}"); |
|
| 117 | + | assert!(o.contains(&format!("oxy clone {}/andy/demo", h.base))); |
|
| 118 | + | assert!(o.contains("2 changes")); |
|
| 119 | + | assert!(o.contains("v0.1")); |
|
| 120 | + | assert!(o.contains("feature")); |
|
| 121 | + | ||
| 122 | + | // Tree at a bookmark, a tag and a change; file with highlighting; raw; blame. |
|
| 123 | + | let t = page(&h.app, "/andy/demo/tree/main/src").await; |
|
| 124 | + | assert!(t.contains("main.rs")); |
|
| 125 | + | assert!(page(&h.app, "/andy/demo/tree/v0.1").await.contains("README.md")); |
|
| 126 | + | assert!(page(&h.app, &format!("/andy/demo/tree/{}", &c1[..10])).await.contains("README.md")); |
|
| 127 | + | assert_eq!(get(&h.app, "/andy/demo/tree/main/nope").await.0, StatusCode::NOT_FOUND); |
|
| 128 | + | let f = page(&h.app, "/andy/demo/file/main/src/main.rs").await; |
|
| 129 | + | assert!(f.contains("storage type function"), "highlighted: {f}"); |
|
| 130 | + | assert!(f.contains("id=\"L5\"")); |
|
| 131 | + | assert!(f.contains("Rust")); |
|
| 132 | + | let (status, raw) = get(&h.app, "/andy/demo/file/main/src/main.rs?plain=1").await; |
|
| 133 | + | assert_eq!(status, StatusCode::OK); |
|
| 134 | + | assert_eq!(raw, "fn main() {\n println!(\"hello\");\n}\n\nfn two() {}\n"); |
|
| 135 | + | let b = page(&h.app, "/andy/demo/file/main/src/main.rs?blame=1").await; |
|
| 136 | + | assert!(b.contains(&c1[..8]) && b.contains(&c2[..8]), "both changes blamed: {b}"); |
|
| 137 | + | let md = page(&h.app, "/andy/demo/file/main/README.md").await; |
|
| 138 | + | assert!(md.contains("<h1>Demo</h1>"), "markdown rendered"); |
|
| 139 | + | // Last-change JSON for tree rows. |
|
| 140 | + | let (status, json) = get(&h.app, "/andy/demo/last/main?paths=README.md,src").await; |
|
| 141 | + | assert_eq!(status, StatusCode::OK); |
|
| 142 | + | assert!(json.contains("Initial import") && json.contains("Greet properly"), "{json}"); |
|
| 143 | + | ||
| 144 | + | // History with graph rows, filtered by path; change page with diff; compare. |
|
| 145 | + | let hist = page(&h.app, "/andy/demo/changes/main").await; |
|
| 146 | + | assert!(hist.contains("Greet properly") && hist.contains("Initial import")); |
|
| 147 | + | assert!(hist.contains("<circle"), "graph drawn"); |
|
| 148 | + | assert!(hist.contains("lbl-tag") && hist.contains("v0.1"), "tag label on its head"); |
|
| 149 | + | let by_path = page(&h.app, "/andy/demo/changes/main?path=README.md").await; |
|
| 150 | + | assert!(by_path.contains("Initial import") && !by_path.contains("Greet properly")); |
|
| 151 | + | let c = page(&h.app, &format!("/andy/demo/change/{c2}")).await; |
|
| 152 | + | assert!(c.contains("Also adds two()"), "body shown"); |
|
| 153 | + | assert!(c.contains("+fn two() {}") || c.contains("fn two() {}"), "diff shown: {c}"); |
|
| 154 | + | assert!(c.contains("class=\"add\"") && c.contains("class=\"del\"")); |
|
| 155 | + | assert!(c.contains(&c1[..12]), "dependency listed"); |
|
| 156 | + | assert!(page(&h.app, &format!("/andy/demo/change/{}", &c2[..8])).await.contains("Greet properly")); |
|
| 157 | + | let cmp = page(&h.app, "/andy/demo/compare/feature...main").await; |
|
| 158 | + | assert!(cmp.contains("1 change") && cmp.contains("Greet properly")); |
|
| 159 | + | assert_eq!(get(&h.app, "/andy/demo/compare/feature").await.0, StatusCode::BAD_REQUEST); |
|
| 160 | + | ||
| 161 | + | // Bookmarks, tags, conflicts (none), trust (no policy), pushes. |
|
| 162 | + | let bm = page(&h.app, "/andy/demo/bookmarks").await; |
|
| 163 | + | assert!(bm.contains("feature") && bm.contains("default")); |
|
| 164 | + | let tags = page(&h.app, "/andy/demo/tags").await; |
|
| 165 | + | assert!(tags.contains("v0.1") && tags.contains("First tag")); |
|
| 166 | + | assert!(page(&h.app, "/andy/demo/conflicts").await.contains("No bookmark carries a conflict")); |
|
| 167 | + | let trust = page(&h.app, "/andy/demo/trust").await; |
|
| 168 | + | assert!(trust.contains("No trust policy") && trust.contains(&h.app.hub_key.key.to_hex())); |
|
| 169 | + | assert!(trust.contains("2 signed"), "signed changes verified on the host: {trust}"); |
|
| 170 | + | assert!(c.contains("signed by Test Author"), "change page shows the signer"); |
|
| 171 | + | let pushes = page(&h.app, "/andy/demo/pushes").await; |
|
| 172 | + | assert!(pushes.contains("accepted") && pushes.contains("main"), "{pushes}"); |
|
| 173 | + | ||
| 174 | + | // API and small endpoints. |
|
| 175 | + | let (_, repos) = get(&h.app, "/api/v1/repos").await; |
|
| 176 | + | assert!(repos.contains("\"name\":\"demo\"")); |
|
| 177 | + | let (_, bms) = get(&h.app, "/api/v1/repos/andy/demo/bookmarks").await; |
|
| 178 | + | assert!(bms.contains("\"default\":true")); |
|
| 179 | + | let (status, key) = get(&h.app, "/.well-known/oxyhub-key").await; |
|
| 180 | + | assert_eq!(status, StatusCode::OK); |
|
| 181 | + | assert!(!key.is_empty()); |
|
| 182 | + | assert!(get(&h.app, "/health").await.1.contains("\"ok\":true")); |
|
| 183 | + | assert_eq!(get(&h.app, "/style.css").await.0, StatusCode::OK); |
|
| 184 | + | assert_eq!(get(&h.app, "/static/fonts/Barlow-400.woff2").await.0, StatusCode::OK); |
|
| 185 | + | assert_eq!(get(&h.app, "/andy/demo/oxy/v1/nothing").await.0, StatusCode::NOT_FOUND); |
|
| 186 | + | } |
|
| 187 | + | ||
| 188 | + | #[tokio::test(flavor = "multi_thread")] |
|
| 189 | + | async fn private_repositories_and_push_tokens() { |
|
| 190 | + | let h = hub(vec!["s3cret".into()]).await; |
|
| 191 | + | for (name, vis) in [("pub", hub_api::Visibility::Public), ("priv", hub_api::Visibility::Private)] { |
|
| 192 | + | // `priv` is created bound so it can be cloned while still empty. |
|
| 193 | + | let id = if name == "priv" { Some([7u8; 16]) } else { None }; |
|
| 194 | + | h.app.repos.create(&RepoName::parse(&format!("andy/{name}")).unwrap(), id).unwrap(); |
|
| 195 | + | db::repos::create(&h.app.db, "andy", name, "", vis).await.unwrap(); |
|
| 196 | + | } |
|
| 197 | + | // The private one is invisible on pages and to anonymous clients. |
|
| 198 | + | assert_eq!(get(&h.app, "/andy/priv").await.0, StatusCode::NOT_FOUND); |
|
| 199 | + | assert!(!page(&h.app, "/").await.contains("priv")); |
|
| 200 | + | assert!(Repo::clone(&format!("{}/andy/priv", h.base), &tempfile::tempdir().unwrap().path().join("x")).is_err()); |
|
| 201 | + | // Anonymous push to the public one is refused; with the token it lands. |
|
| 202 | + | let work = tempfile::tempdir().unwrap(); |
|
| 203 | + | let mut client = Repo::init(work.path()).unwrap(); |
|
| 204 | + | client.config.set("remote.origin.url", &format!("{}/andy/pub", h.base)).unwrap(); |
|
| 205 | + | write(work.path(), "a.txt", b"a\n"); |
|
| 206 | + | client.snapshot("test").unwrap(); |
|
| 207 | + | client.commit("a", &[]).unwrap(); |
|
| 208 | + | assert!(client.push("origin", false).is_err()); |
|
| 209 | + | client.config.set("remote.origin.token", "s3cret").unwrap(); |
|
| 210 | + | assert!(client.push("origin", false).unwrap().iter().all(|r| r.ok)); |
|
| 211 | + | // With the token, the private repository can be cloned too. |
|
| 212 | + | let priv_dir = tempfile::tempdir().unwrap(); |
|
| 213 | + | std::env::set_var("OXY_TOKEN", "s3cret"); |
|
| 214 | + | let ok = Repo::clone(&format!("{}/andy/priv", h.base), &priv_dir.path().join("p")).is_ok(); |
|
| 215 | + | std::env::remove_var("OXY_TOKEN"); |
|
| 216 | + | assert!(ok, "a token reads a private repository until accounts exist"); |
|
| 217 | + | // The pushes log shows the accepted update; the front page shows activity. |
|
| 218 | + | tokio::time::sleep(std::time::Duration::from_millis(100)).await; |
|
| 219 | + | assert!(page(&h.app, "/andy/pub/pushes").await.contains("accepted")); |
|
| 220 | + | assert!(page(&h.app, "/andy/pub").await.contains("1 change")); |
|
| 221 | + | } |
added docs/plan.md +30 −0
| 1 | + | # Oxygenhub plan |
|
| 2 | + | ||
| 3 | + | Milestones from `docs/spec.md` §19. Each item is checked when its tests |
|
| 4 | + | pass, clippy is clean, and the change is committed with `oxy`. |
|
| 5 | + | ||
| 6 | + | ## 0.1 Browse (2026-09-27) |
|
| 7 | + | ||
| 8 | + | - [x] Workspace: `crates/hub` (binary), `crates/hub-api` (API types), askama templates, vendored fonts, one stylesheet from the RetroPlay theme. |
|
| 9 | + | - [x] Configuration (`hub.toml` + `OXYHUB_*`), SQLite with embedded migrations (`repositories`, `pushes`). |
|
| 10 | + | - [x] Repository registry: bare, unbound-by-default repositories under `<data>/repos/<owner>/<name>`, per-repository read/write locks, trash on delete. |
|
| 11 | + | - [x] Embedded wire protocol with visibility and push-token authorisation and a push log. |
|
| 12 | + | - [x] Pages: home, owner, overview (tree, README, clone, bookmarks, tags, trust badge, heads and conflict strips), tree, file (highlighting, Markdown, images, raw, blame), history with the dependency graph and path filter, change with diff and dependencies, compare, bookmarks, tags, conflicts, trust, pushes. |
|
| 13 | + | - [x] JSON API: repositories, bookmarks, tags; `/health`; `/.well-known/oxyhub-key`. |
|
| 14 | + | - [x] CLI: `init` (keys, self-signed TLS), `serve` (HTTP and HTTPS), `admin repo create|list|delete|set`, `check`. |
|
| 15 | + | - [x] Tests: unit tests per module and an end-to-end test that pushes with a real `oxy-core` client and fetches every page. |
|
| 16 | + | - [x] Oxygen changes it needed (in the Oxygen plan, phase 4): bare repositories, embeddable server, unbound repositories, adoption of anonymous changes by seals, `oxy seal`. |
|
| 17 | + | - [ ] Developer edition running on Earth with `oxygen`, `oxygen-vscode` and `oxygenhub` hosted (docs/deploy.md). |
|
| 18 | + | ||
| 19 | + | ## 0.2 Accounts and access |
|
| 20 | + | ||
| 21 | + | - [ ] Registration, sessions, TOTP, profile, organisations, teams, roles, visibility enforcement by viewer, personal access tokens with scopes, linked identities with challenge signing (`oxy identity sign-challenge`), audit log, settings pages, pushes attributed to keys. |
|
| 22 | + | ||
| 23 | + | ## Later |
|
| 24 | + | ||
| 25 | + | - 0.3 proposals and review · 0.4 issues and notifications · 0.5 API, webhooks, checks · 0.6 web edits, releases, mirror · 0.7 runner · 1.0 hardening. |
|
| 26 | + | ||
| 27 | + | ## Notes |
|
| 28 | + | ||
| 29 | + | - State caching (spec §16.3) is not started: pages load the state they need per request; the overview loads it once and reuses it. Measured on the `oxygen` repository (88 changes): overview ~0.5 s, tree ~0.14 s, history ~0.13 s, a 2 000-line file ~0.7 s. A per-heads cache is the first performance item for 0.2. |
|
| 30 | + | - `State` holds `Rc` values, so a cross-thread cache needs a `Send` snapshot type in `oxy-core` first. |
added rustfmt.toml +2 −0
| 1 | + | max_width = 140 |
|
| 2 | + | use_small_heuristics = "Max" |
added templates/base.html +47 −0
| 1 | + | <!doctype html> |
|
| 2 | + | <html lang="en"> |
|
| 3 | + | <head> |
|
| 4 | + | <meta charset="utf-8"> |
|
| 5 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
| 6 | + | <title>{% block title %}{{ shell.site }}{% endblock %} · {{ shell.site }}</title> |
|
| 7 | + | <link rel="stylesheet" href="/style.css"> |
|
| 8 | + | <link rel="icon" href="/favicon.svg" type="image/svg+xml"> |
|
| 9 | + | <script>(function(){var m=document.cookie.match(/(?:^|; )theme=(light|dark|auto)/);if(m&&m[1]!=="auto")document.documentElement.dataset.theme=m[1];})();</script> |
|
| 10 | + | </head> |
|
| 11 | + | <body{% block bodyattrs %}{% endblock %}> |
|
| 12 | + | <header> |
|
| 13 | + | <a class="brand px" href="/">OXY<span>◆</span>GEN</a> |
|
| 14 | + | <form class="find" action="/search" method="get" role="search"><input type="search" name="q" placeholder="Search…" aria-label="Search" autocomplete="off"></form> |
|
| 15 | + | <nav> |
|
| 16 | + | <a class="go" href="/explore">EXPLORE</a> |
|
| 17 | + | </nav> |
|
| 18 | + | </header> |
|
| 19 | + | <div class="band"> |
|
| 20 | + | <a href="/">Repositories</a> |
|
| 21 | + | <a href="/explore">Explore</a> |
|
| 22 | + | <span class="grow"></span> |
|
| 23 | + | <a class="dim" href="/{{ shell.version }}" title="Oxygenhub {{ shell.version }}" tabindex="-1">v{{ shell.version }}</a> |
|
| 24 | + | </div> |
|
| 25 | + | {% block tabs %}{% endblock %} |
|
| 26 | + | <main{% block mainclass %}{% endblock %}> |
|
| 27 | + | {% block content %}{% endblock %} |
|
| 28 | + | </main> |
|
| 29 | + | <footer> |
|
| 30 | + | <div class="foot"> |
|
| 31 | + | <div> |
|
| 32 | + | <a class="brand px" href="/">OXY<span>◆</span>GEN</a> |
|
| 33 | + | <p class="small">{{ shell.site }} · a forge for Oxygen repositories. Nothing here is inferred: what a page says, the object store says.</p> |
|
| 34 | + | </div> |
|
| 35 | + | <div class="cols"> |
|
| 36 | + | <div><h3>Use</h3><a href="/explore">Explore</a><a href="/api/v1/repos">API</a><a href="/.well-known/oxyhub-key">Hub key</a></div> |
|
| 37 | + | <div><h3>Keys</h3><span class="small"><kbd>/</kbd> search · <kbd>g</kbd><kbd>c</kbd> code · <kbd>g</kbd><kbd>h</kbd> history · <kbd>?</kbd> help</span></div> |
|
| 38 | + | </div> |
|
| 39 | + | </div> |
|
| 40 | + | </footer> |
|
| 41 | + | <div id="keys" class="sheet" hidden> |
|
| 42 | + | <h2>Keyboard</h2> |
|
| 43 | + | <table><tr><td><kbd>/</kbd></td><td>Search</td></tr><tr><td><kbd>g</kbd> <kbd>c</kbd></td><td>Code</td></tr><tr><td><kbd>g</kbd> <kbd>h</kbd></td><td>History</td></tr><tr><td><kbd>g</kbd> <kbd>b</kbd></td><td>Bookmarks</td></tr><tr><td><kbd>g</kbd> <kbd>t</kbd></td><td>Trust</td></tr><tr><td><kbd>?</kbd></td><td>This sheet</td></tr></table> |
|
| 44 | + | </div> |
|
| 45 | + | <script src="/static/hub.js" defer></script> |
|
| 46 | + | </body> |
|
| 47 | + | </html> |
added templates/bookmarks.html +20 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Bookmarks · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "bookmarks") %}{% endblock %} |
|
| 6 | + | {% block content %} |
|
| 7 | + | <div class="head"><h1>Bookmarks <small>{{ bookmarks.len() }}</small></h1></div> |
|
| 8 | + | <div class="table"><table> |
|
| 9 | + | <tr><th>Bookmark</th><th>Heads</th><th>Changes</th><th>Latest</th><th></th></tr> |
|
| 10 | + | {% for b in bookmarks %} |
|
| 11 | + | <tr> |
|
| 12 | + | <td class="px"><a href="{{ repo.url }}/tree/{{ b.name }}">{{ b.name }}</a>{% if b.default %} <span class="tag">default</span>{% endif %}{% if b.conflicted %} <span class="tag bad">conflicts</span>{% endif %}</td> |
|
| 13 | + | <td>{% for h in b.heads %}<a class="chip" href="{{ repo.url }}/change/{{ h }}">{{ h|short }}</a> {% endfor %}{% if b.heads.len() > 1 %}<span class="tag warn">{{ b.heads.len() }} heads</span>{% endif %}</td> |
|
| 14 | + | <td>{{ b.changes }}</td> |
|
| 15 | + | <td><time title="{{ b.latest_ms|when }}">{{ b.latest_ms|ago }}</time></td> |
|
| 16 | + | <td class="small"><a href="{{ repo.url }}/changes/{{ b.name }}">history</a>{% if !b.default %} · <a href="{{ repo.url }}/compare/{{ repo.default_bookmark }}...{{ b.name }}">compare with {{ repo.default_bookmark }}</a>{% endif %}</td> |
|
| 17 | + | </tr> |
|
| 18 | + | {% endfor %} |
|
| 19 | + | </table></div> |
|
| 20 | + | {% endblock %} |
added templates/change.html +26 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}{{ c.view.title }} · {{ c.view.short }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "changes") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | <article class="change"> |
|
| 9 | + | <div class="head"><h1>{{ c.view.title }} <small class="px">{{ c.view.short }}</small></h1></div> |
|
| 10 | + | {{ body_html|safe }} |
|
| 11 | + | <dl class="facts"> |
|
| 12 | + | <dt>Author</dt><dd>{{ c.view.author.name }}{% if !c.view.author.email.is_empty() %} <span class="mute"><{{ c.view.author.email }}></span>{% endif %}{% if c.view.author.unknown && !c.view.author.key.is_empty() %} <span class="tag">unlinked key</span>{% endif %}</dd> |
|
| 13 | + | <dt>When</dt><dd><time title="{{ c.view.when_ms|when }}">{{ c.view.when_ms|ago }}</time> · {{ c.view.when_ms|when }}</dd> |
|
| 14 | + | <dt>Signature</dt><dd>{% call m::sig(c.view.signature) %} {{ c.view.signature.title() }}</dd> |
|
| 15 | + | <dt>Hash</dt><dd><code class="full">{{ c.view.hash }}</code> <button class="small" data-copy="{{ c.view.hash }}">copy</button></dd> |
|
| 16 | + | <dt>Depends on</dt><dd>{% if c.deps.is_empty() %}<span class="mute">nothing: a root change</span>{% else %}{% for d in c.deps %}<a class="chip" href="{{ repo.url }}/change/{{ d.hash }}" title="{{ d.title }}">{{ d.short }}</a> {% endfor %}{% endif %}</dd> |
|
| 17 | + | <dt>Depended on by</dt><dd>{% if c.dependents.is_empty() %}<span class="mute">nothing yet</span>{% else %}{% for d in c.dependents %}<a class="chip" href="{{ repo.url }}/change/{{ d.hash }}" title="{{ d.title }}">{{ d.short }}</a> {% endfor %}{% endif %}</dd> |
|
| 18 | + | <dt>In bookmarks</dt><dd>{% for b in c.in_bookmarks %}<a class="lbl lbl-bm" href="{{ repo.url }}/tree/{{ b }}">{{ b }}</a> {% endfor %}{% if c.in_bookmarks.is_empty() %}<span class="mute">none</span>{% endif %}</dd> |
|
| 19 | + | {% if !c.in_tags.is_empty() %}<dt>In tags</dt><dd>{% for t in c.in_tags %}<a class="lbl lbl-tag" href="{{ repo.url }}/tree/{{ t }}">{{ t }}</a> {% endfor %}</dd>{% endif %} |
|
| 20 | + | <dt>Edits</dt><dd>{{ c.atoms|plural("atom", "atoms") }}, {{ c.tree_ops|plural("tree operation", "tree operations") }}, {{ files.len()|plural("file", "files") }}</dd> |
|
| 21 | + | </dl> |
|
| 22 | + | <p class="small"><a href="{{ repo.url }}/tree/{{ c.view.hash }}">Browse the tree at this change</a></p> |
|
| 23 | + | <nav class="files small">{% for f in files %}<a href="#{{ f.anchor }}">{{ f.path }}</a>{% endfor %}</nav> |
|
| 24 | + | {% call m::diff(files) %} |
|
| 25 | + | </article> |
|
| 26 | + | {% endblock %} |
added templates/compare.html +12 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Compare {{ cmp.base.label|short }}...{{ cmp.head.label|short }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "changes") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | <div class="head"><h1>Compare <small>{{ cmp.base.label }} … {{ cmp.head.label }}</small></h1></div> |
|
| 9 | + | <p class="lead">{{ cmp.added.len()|plural("change", "changes") }} in <strong>{{ cmp.head.label }}</strong> not in <strong>{{ cmp.base.label }}</strong>{% if cmp.missing > 0 %}; {{ cmp.missing }} the other way{% endif %}. {{ files.len()|plural("file", "files") }} differ between the two states.</p> |
|
| 10 | + | {% for c in cmp.added %}{% call m::change_row(repo, c) %}{% endfor %} |
|
| 11 | + | {% call m::diff(files) %} |
|
| 12 | + | {% endblock %} |
added templates/conflicts.html +19 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Conflicts · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "conflicts") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | <div class="head"><h1>Conflicts <small>{{ conflicts.len() }}</small></h1></div> |
|
| 9 | + | {% if conflicts.is_empty() %}<p class="empty">No bookmark carries a conflict. Conflicts are values in Oxygen: when two changes disagree, the state records both sides and any collaborator can push a resolution.</p>{% else %} |
|
| 10 | + | {% for c in conflicts %} |
|
| 11 | + | <section class="file-diff"> |
|
| 12 | + | <header><span class="lbl lbl-bm">{{ c.bookmark }}</span> <strong>{{ c.path }}</strong> <span class="tag bad">{{ c.kind }}</span></header> |
|
| 13 | + | {% if !c.contributors.is_empty() %}<p class="small">Sides from: {% for x in c.contributors %}<a class="chip" href="{{ repo.url }}/change/{{ x.hash }}" title="{{ x.title }}">{{ x.short }}</a> {% endfor %}</p>{% endif %} |
|
| 14 | + | {% if let Some(text) = c.marked %}<pre class="marked">{{ text }}</pre>{% endif %} |
|
| 15 | + | <p class="small mute">Resolve from a client: <code>oxy bookmark switch {{ c.bookmark }}</code>, edit the file, <code>oxy resolve</code>, <code>oxy commit</code>, <code>oxy push</code>.</p> |
|
| 16 | + | </section> |
|
| 17 | + | {% endfor %} |
|
| 18 | + | {% endif %} |
|
| 19 | + | {% endblock %} |
added templates/error.html +7 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% block title %}{{ title }}{% endblock %} |
|
| 3 | + | {% block content %} |
|
| 4 | + | <div class="head"><h1>{{ status }} <small>{{ title }}</small></h1></div> |
|
| 5 | + | <p class="lead">{{ detail }}</p> |
|
| 6 | + | <p><a class="go" href="/">Back to the front page</a></p> |
|
| 7 | + | {% endblock %} |
added templates/file.html +50 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}{{ f.path }} at {{ f.target.label|short }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}" data-ref="{{ f.target.label }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "code") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | <div class="bar"> |
|
| 9 | + | <span class="ref px">{% if f.target.kind == crate::repos::view::RefKind::Change %}# {{ f.target.label|short }}{% else %}◆ {{ f.target.label }}{% endif %}</span> |
|
| 10 | + | <span class="crumbs"><a href="{{ repo.url }}/tree/{{ f.target.label }}">{{ repo.name }}</a>{% for (n, p) in f.crumbs %} / {% if loop.last %}<strong>{{ n }}</strong>{% else %}<a href="{{ repo.url }}/tree/{{ f.target.label }}/{{ p }}">{{ n }}</a>{% endif %}{% endfor %}</span> |
|
| 11 | + | </div> |
|
| 12 | + | <section class="filebox"> |
|
| 13 | + | <header> |
|
| 14 | + | <span class="small mute">{{ f.size|bytes }}{% if let Some(l) = language %} · {{ l }}{% endif %}{% if f.exec %} · executable{% endif %}{% if f.kind == crate::repos::view::EntryKind::Symlink %} · symlink{% endif %}{% if f.kind == crate::repos::view::EntryKind::Subrepo %} · subrepository pin{% endif %}{% if let Some(c) = f.last_change %} · last changed <a href="{{ repo.url }}/change/{{ c.hash }}">{{ c.title }}</a> {{ c.when_ms|ago }}{% endif %}</span> |
|
| 15 | + | <span class="modes"> |
|
| 16 | + | {% if is_markdown %}<a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}"{% if markdown_html.is_some() %} class="on"{% endif %}>rendered</a><a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?view=source"{% if blame.is_none() && markdown_html.is_none() %} class="on"{% endif %}>source</a> |
|
| 17 | + | {% else %}<a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}"{% if blame.is_none() %} class="on"{% endif %}>source</a>{% endif %} |
|
| 18 | + | <a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?blame=1"{% if blame.is_some() %} class="on"{% endif %}>blame</a> |
|
| 19 | + | <a href="{{ repo.url }}/changes/{{ f.target.label }}?path={{ f.path }}">history</a> |
|
| 20 | + | <a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?plain=1">raw</a> |
|
| 21 | + | </span> |
|
| 22 | + | </header> |
|
| 23 | + | {% if f.conflicted %}<div class="strip bad">This file is in conflict in this state; the markers below are the conflict. <a href="{{ repo.url }}/conflicts">All conflicts</a></div>{% endif %} |
|
| 24 | + | {% if is_image %} |
|
| 25 | + | <p class="center"><img src="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?plain=1" alt="{{ f.name }}"></p> |
|
| 26 | + | {% else %} |
|
| 27 | + | {% match f.body %} |
|
| 28 | + | {% when crate::repos::view::FileBody::TooLarge %}<p class="empty">This file is larger than the render limit. <a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?plain=1">Download it</a>.</p> |
|
| 29 | + | {% when crate::repos::view::FileBody::Binary %}<p class="empty">Binary file, {{ f.size|bytes }}. <a href="{{ repo.url }}/file/{{ f.target.label }}/{{ f.path }}?plain=1">Download it</a>.</p> |
|
| 30 | + | {% when crate::repos::view::FileBody::Text(_) %} |
|
| 31 | + | {% if let Some(html) = markdown_html %} |
|
| 32 | + | <article class="doc" id="rendered">{{ html|safe }}</article> |
|
| 33 | + | {% else %} |
|
| 34 | + | <table class="code{% if blame.is_some() %} blamed{% endif %}"> |
|
| 35 | + | {% for line in lines %} |
|
| 36 | + | {% let n = loop.index %} |
|
| 37 | + | <tr id="L{{ n }}"> |
|
| 38 | + | {% if let Some(b) = blame %}{% let r = b[loop.index0] %} |
|
| 39 | + | {% if r.first %}<td class="blame" rowspan="{{ r.run }}"><a class="px" href="{{ repo.url }}/change/{{ r.hash }}" title="{{ r.hash }}">{{ r.short }}</a> <span class="who">{{ r.author }}</span> <time class="mute" title="{{ r.when_ms|when }}">{{ r.when_ms|ago }}</time></td>{% endif %} |
|
| 40 | + | {% endif %} |
|
| 41 | + | <td class="n">{{ n }}</td> |
|
| 42 | + | <td class="t"><pre>{{ line|safe }}</pre></td> |
|
| 43 | + | </tr> |
|
| 44 | + | {% endfor %} |
|
| 45 | + | </table> |
|
| 46 | + | {% endif %} |
|
| 47 | + | {% endmatch %} |
|
| 48 | + | {% endif %} |
|
| 49 | + | </section> |
|
| 50 | + | {% endblock %} |
added templates/history.html +30 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Changes on {{ h.target.label|short }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}" data-ref="{{ h.target.label }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "changes") %}{% endblock %} |
|
| 6 | + | {% block content %} |
|
| 7 | + | <div class="head"><h1>Changes <small>{% if h.target.kind == crate::repos::view::RefKind::Change %}up to # {{ h.target.label|short }}{% else %}on {{ h.target.label }}{% endif %}{% if let Some(p) = h.path %} touching {{ p }}{% endif %} · {{ h.total }}</small></h1></div> |
|
| 8 | + | {% if h.target.heads.len() > 1 %}<div class="strip warn">{{ h.target.label }} has {{ h.target.heads.len() }} heads. The graph shows their union; heads are marked.</div>{% endif %} |
|
| 9 | + | {% if h.entries.is_empty() %}<p class="empty">No changes.</p>{% else %} |
|
| 10 | + | <div class="hist" style="--lanes: {{ lanes }}"> |
|
| 11 | + | {% for c in h.entries %} |
|
| 12 | + | {% let row = rows[loop.index0] %} |
|
| 13 | + | <div class="hrow"> |
|
| 14 | + | <svg class="lanes" width="{{ lanes * 18 + 4 }}" height="100%" viewBox="0 0 {{ lanes * 18 + 4 }} 60" preserveAspectRatio="none" aria-hidden="true"> |
|
| 15 | + | {% for l in row.through %}<line x1="{{ l * 18 + 10 }}" y1="0" x2="{{ l * 18 + 10 }}" y2="60" class="lane"/>{% endfor %} |
|
| 16 | + | {% for e in row.edges %}<path d="M{{ e.0 * 18 + 10 }} 30 L{{ e.0 * 18 + 10 }} 44 L{{ e.1 * 18 + 10 }} 60" class="lane"/>{% endfor %} |
|
| 17 | + | {% if !c.deps.is_empty() && row.edges.is_empty() %}<line x1="{{ row.lane * 18 + 10 }}" y1="30" x2="{{ row.lane * 18 + 10 }}" y2="60" class="lane dashed"/>{% endif %} |
|
| 18 | + | <line x1="{{ row.lane * 18 + 10 }}" y1="0" x2="{{ row.lane * 18 + 10 }}" y2="30" class="lane{% if c.head %} none{% endif %}"/> |
|
| 19 | + | <circle cx="{{ row.lane * 18 + 10 }}" cy="30" r="4" class="node{% if c.head %} head{% endif %}{% if c.resolution %} res{% endif %}"/> |
|
| 20 | + | </svg> |
|
| 21 | + | {% call m::change_row(repo, c) %} |
|
| 22 | + | </div> |
|
| 23 | + | {% endfor %} |
|
| 24 | + | </div> |
|
| 25 | + | <div class="pager"> |
|
| 26 | + | {% if h.offset > 0 %}<a class="go" href="?offset={{ h.offset.saturating_sub(50) }}{% if let Some(p) = h.path %}&path={{ p }}{% endif %}">newer</a>{% endif %} |
|
| 27 | + | {% if h.has_more %}<a class="go" href="?offset={{ h.offset + 50 }}{% if let Some(p) = h.path %}&path={{ p }}{% endif %}">older</a>{% endif %} |
|
| 28 | + | </div> |
|
| 29 | + | {% endif %} |
|
| 30 | + | {% endblock %} |
added templates/home.html +24 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% block title %}{% if let Some(o) = owner %}{{ o }}{% else %}Repositories{% endif %}{% endblock %} |
|
| 3 | + | {% block content %} |
|
| 4 | + | <div class="head"> |
|
| 5 | + | {% if let Some(o) = owner %}<h1>{{ o }} <small>{{ repos.len()|plural("repository", "repositories") }}</small></h1> |
|
| 6 | + | {% else %}<h1>Repositories <small>{{ description }}</small></h1>{% endif %} |
|
| 7 | + | </div> |
|
| 8 | + | {% if repos.is_empty() %} |
|
| 9 | + | <p class="empty">No repositories yet. Create one with <code>oxygenhub admin repo create OWNER/NAME</code>, then <code>oxy push</code> into it.</p> |
|
| 10 | + | {% else %} |
|
| 11 | + | <ul class="rows repos"> |
|
| 12 | + | {% for r in repos %} |
|
| 13 | + | <li> |
|
| 14 | + | <div> |
|
| 15 | + | <a class="name" href="/{{ r.owner }}/{{ r.name }}"><span class="mute">{{ r.owner }} /</span> <strong>{{ r.name }}</strong></a> |
|
| 16 | + | {% if r.visibility != "public" %}<span class="tag">{{ r.visibility }}</span>{% endif %} |
|
| 17 | + | {% if !r.description.is_empty() %}<p class="small">{{ r.description }}</p>{% endif %} |
|
| 18 | + | </div> |
|
| 19 | + | <div class="right small">updated <time title="{{ r.updated_ms|when_i }}">{{ r.updated_ms|ago_i }}</time></div> |
|
| 20 | + | </li> |
|
| 21 | + | {% endfor %} |
|
| 22 | + | </ul> |
|
| 23 | + | {% endif %} |
|
| 24 | + | {% endblock %} |
added templates/overview.html +40 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}{{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}" data-ref="{{ repo.default_bookmark }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "code") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | {% if o.heads > 1 %}<div class="strip warn">{{ repo.default_bookmark }} has {{ o.heads }} heads; the tree below is their union. <a href="{{ repo.url }}/bookmarks">Bookmarks</a></div>{% endif %} |
|
| 9 | + | {% if o.conflicted %}<div class="strip bad">This state carries conflicts. <a href="{{ repo.url }}/conflicts">See them</a></div>{% endif %} |
|
| 10 | + | <div class="side-layout rev"> |
|
| 11 | + | <div> |
|
| 12 | + | <div class="bar"> |
|
| 13 | + | <span class="ref px"><a href="{{ repo.url }}/bookmarks">◆ {{ repo.default_bookmark }}</a></span> |
|
| 14 | + | <a class="go small" href="{{ repo.url }}/changes/{{ repo.default_bookmark }}">{{ o.changes|plural("change", "changes") }}</a> |
|
| 15 | + | {% if let Some(c) = o.latest %}<span class="small mute">latest <a href="{{ repo.url }}/change/{{ c.hash }}">{{ c.title }}</a> · {{ c.when_ms|ago }}</span>{% endif %} |
|
| 16 | + | </div> |
|
| 17 | + | {% include "tree_table.html" %} |
|
| 18 | + | {% if let Some(html) = readme_html %} |
|
| 19 | + | <article class="doc readme"><h2 class="small mute">{{ o.tree.readme.as_ref().unwrap().path }}</h2>{{ html|safe }}</article> |
|
| 20 | + | {% endif %} |
|
| 21 | + | </div> |
|
| 22 | + | <aside class="side"> |
|
| 23 | + | <h3>About</h3> |
|
| 24 | + | {% if repo.description.is_empty() %}<p class="mute small">No description.</p>{% else %}<p>{{ repo.description }}</p>{% endif %} |
|
| 25 | + | <h3>Clone</h3> |
|
| 26 | + | <div class="clone"><code>oxy clone {{ repo.clone_url }}</code> <button class="small" data-copy="oxy clone {{ repo.clone_url }}">copy</button></div> |
|
| 27 | + | <h3>Bookmarks</h3> |
|
| 28 | + | <ul class="plain">{% for b in o.bookmarks %}<li><a href="{{ repo.url }}/tree/{{ b.name }}">{{ b.name }}</a>{% if b.heads.len() > 1 %} <span class="tag warn">{{ b.heads.len() }} heads</span>{% endif %}{% if b.conflicted %} <span class="tag bad">conflicts</span>{% endif %}</li>{% endfor %}</ul> |
|
| 29 | + | <h3>Tags</h3> |
|
| 30 | + | {% if o.tags.is_empty() %}<p class="mute small">None.</p>{% else %}<ul class="plain">{% for t in o.tags %}<li><a href="{{ repo.url }}/tree/{{ t.name }}">{{ t.name }}</a>{% if t.signer.is_some() %} <span class="sig sig-valid" title="signed">✔</span>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 31 | + | <h3>Trust</h3> |
|
| 32 | + | {% match o.policy %} |
|
| 33 | + | {% when crate::repos::view::PolicyBadge::Active %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-valid">✔</span> policy in force</a></p> |
|
| 34 | + | {% when crate::repos::view::PolicyBadge::Expired %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-invalid">✘</span> policy expired</a></p> |
|
| 35 | + | {% when crate::repos::view::PolicyBadge::None %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-unsigned">○</span> no policy</a></p> |
|
| 36 | + | {% endmatch %} |
|
| 37 | + | {% if let Some(l) = o.license %}<h3>Licence</h3><p><a href="{{ repo.url }}/file/{{ repo.default_bookmark }}/{{ l }}">{{ l }}</a></p>{% endif %} |
|
| 38 | + | </aside> |
|
| 39 | + | </div> |
|
| 40 | + | {% endblock %} |
added templates/pushes.html +16 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Pushes · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "pushes") %}{% endblock %} |
|
| 6 | + | {% block content %} |
|
| 7 | + | <div class="head"><h1>Pushes <small>every reference update the hub accepted or refused</small></h1></div> |
|
| 8 | + | {% if pushes.is_empty() %}<p class="empty">Nothing has been pushed yet.</p>{% else %} |
|
| 9 | + | <div class="table"><table> |
|
| 10 | + | <tr><th>When</th><th>Reference</th><th>Result</th></tr> |
|
| 11 | + | {% for p in pushes %} |
|
| 12 | + | <tr><td><time title="{{ p.at_ms|when_i }}">{{ p.at_ms|ago_i }}</time></td><td class="px">{{ p.reference }}</td><td>{% if p.ok %}<span class="sig sig-valid">✔</span> accepted{% else %}<span class="sig sig-invalid">✘</span> refused: {{ p.reason }}{% endif %}</td></tr> |
|
| 13 | + | {% endfor %} |
|
| 14 | + | </table></div> |
|
| 15 | + | {% endif %} |
|
| 16 | + | {% endblock %} |
added templates/repo_tabs.html +58 −0
| 1 | + | {% macro tabs(repo, tab) %} |
|
| 2 | + | <div class="tabs"> |
|
| 3 | + | <div class="tabs-in"> |
|
| 4 | + | <span class="crumb"><a href="/{{ repo.owner }}">{{ repo.owner }}</a> / <a href="{{ repo.url }}"><strong>{{ repo.name }}</strong></a>{% if repo.visibility != "public" %} <span class="tag">{{ repo.visibility }}</span>{% endif %}</span> |
|
| 5 | + | <a href="{{ repo.url }}"{% if tab == "code" %} class="on"{% endif %}>Code</a> |
|
| 6 | + | <a href="{{ repo.url }}/changes/{{ repo.default_bookmark }}"{% if tab == "changes" %} class="on"{% endif %}>Changes</a> |
|
| 7 | + | <a href="{{ repo.url }}/bookmarks"{% if tab == "bookmarks" %} class="on"{% endif %}>Bookmarks</a> |
|
| 8 | + | <a href="{{ repo.url }}/tags"{% if tab == "tags" %} class="on"{% endif %}>Tags</a> |
|
| 9 | + | <a href="{{ repo.url }}/conflicts"{% if tab == "conflicts" %} class="on"{% endif %}>Conflicts</a> |
|
| 10 | + | <a href="{{ repo.url }}/trust"{% if tab == "trust" %} class="on"{% endif %}>Trust</a> |
|
| 11 | + | <a href="{{ repo.url }}/pushes"{% if tab == "pushes" %} class="on"{% endif %}>Pushes</a> |
|
| 12 | + | </div> |
|
| 13 | + | </div> |
|
| 14 | + | {% endmacro %} |
|
| 15 | + | ||
| 16 | + | {% macro sig(s) %}<span class="sig {{ s.class() }}" title="{{ s.title() }}">{{ s.glyph() }}</span>{% endmacro %} |
|
| 17 | + | ||
| 18 | + | {% macro labels(c) %}{% for l in c.labels %}<span class="lbl {% if l.kind == crate::repos::view::RefKind::Tag %}lbl-tag{% else %}lbl-bm{% endif %}">{{ l.name }}</span>{% endfor %}{% endmacro %} |
|
| 19 | + | ||
| 20 | + | {% macro change_row(repo, c) %} |
|
| 21 | + | <div class="chg"> |
|
| 22 | + | <div class="chg-main"> |
|
| 23 | + | <a class="title" href="{{ repo.url }}/change/{{ c.hash }}">{{ c.title }}</a> |
|
| 24 | + | {% call labels(c) %} |
|
| 25 | + | {% if c.resolution %}<span class="tag">resolution</span>{% endif %} |
|
| 26 | + | {% if c.head %}<span class="tag head">head</span>{% endif %} |
|
| 27 | + | <div class="meta"><span class="who">{{ c.author.name }}</span> · <time title="{{ c.when_ms|when }}">{{ c.when_ms|ago }}</time></div> |
|
| 28 | + | </div> |
|
| 29 | + | <div class="chg-side"> |
|
| 30 | + | {% call sig(c.signature) %} |
|
| 31 | + | <a class="hash px" href="{{ repo.url }}/change/{{ c.hash }}" title="{{ c.hash }}">{{ c.short }}</a> |
|
| 32 | + | </div> |
|
| 33 | + | </div> |
|
| 34 | + | {% endmacro %} |
|
| 35 | + | ||
| 36 | + | {% macro diff(files) %} |
|
| 37 | + | {% for f in files %} |
|
| 38 | + | <section class="file-diff" id="{{ f.anchor }}"> |
|
| 39 | + | <header> |
|
| 40 | + | <span class="kind kind-{{ f.kind }}">{{ f.kind }}</span> |
|
| 41 | + | <strong>{% if let Some(from) = f.from %}{{ from }} → {% endif %}{{ f.path }}</strong> |
|
| 42 | + | <span class="counts"><span class="add">+{{ f.added }}</span> <span class="del">−{{ f.removed }}</span></span> |
|
| 43 | + | </header> |
|
| 44 | + | {% if f.lines.is_empty() %}<p class="empty">No line changes.</p>{% else %} |
|
| 45 | + | <table class="code diff"> |
|
| 46 | + | {% for l in f.lines %} |
|
| 47 | + | <tr class="{{ l.kind.class() }}"> |
|
| 48 | + | <td class="n">{% if let Some(n) = l.old_no %}{{ n }}{% endif %}</td> |
|
| 49 | + | <td class="n">{% if let Some(n) = l.new_no %}{{ n }}{% endif %}</td> |
|
| 50 | + | <td class="s">{{ l.kind.sign() }}</td> |
|
| 51 | + | <td class="t"><pre>{{ l.text }}</pre></td> |
|
| 52 | + | </tr> |
|
| 53 | + | {% endfor %} |
|
| 54 | + | </table> |
|
| 55 | + | {% endif %} |
|
| 56 | + | </section> |
|
| 57 | + | {% endfor %} |
|
| 58 | + | {% endmacro %} |
added templates/style.css +225 −0
| 1 | + | /* Oxygenhub. The RetroPlay Network theme (hub spec §6.1): dark by default |
|
| 2 | + | on the Commander X16 palette, light under [data-theme="light"], Barlow |
|
| 3 | + | for text and Press Start 2P for the wordmark and first columns, square |
|
| 4 | + | corners, thin rules, a band for navigation. Fonts are vendored. */ |
|
| 5 | + | ||
| 6 | + | @font-face { font-family: "Barlow"; font-weight: 300; font-display: swap; src: url(/static/fonts/Barlow-300.woff2) format("woff2"); } |
|
| 7 | + | @font-face { font-family: "Barlow"; font-weight: 400; font-display: swap; src: url(/static/fonts/Barlow-400.woff2) format("woff2"); } |
|
| 8 | + | @font-face { font-family: "Barlow"; font-weight: 500; font-display: swap; src: url(/static/fonts/Barlow-500.woff2) format("woff2"); } |
|
| 9 | + | @font-face { font-family: "Barlow"; font-weight: 600; font-display: swap; src: url(/static/fonts/Barlow-600.woff2) format("woff2"); } |
|
| 10 | + | @font-face { font-family: "Press Start 2P"; font-weight: 400; font-display: swap; src: url(/static/fonts/PressStart2P-400.woff2) format("woff2"); } |
|
| 11 | + | ||
| 12 | + | :root { |
|
| 13 | + | --bg:#0b0b16; --panel:#141430; --soft:#1c1c3c; --ink:#e8e8f0; --mute:#8a8aa0; --line:#2c2c5a; --dim:#4a4a68; |
|
| 14 | + | --acc:#ffc83c; --band:#3a5fd0; --band-ink:#ffffff; --band-line:rgba(11,11,22,.45); --now:#ffc83c; --now-ink:#0b0b16; |
|
| 15 | + | --ok:#7dd87d; --bad:#ff5a5a; --warn:#ffb347; |
|
| 16 | + | --add:#123a22; --del:#3f1520; --add-ink:#9fe8a8; --del-ink:#ffa5a5; |
|
| 17 | + | --m1:#ffdd00; --m2:#d6d6e0; --m3:#d29a55; --m4:#c07a5a; --m5:#9a9ab8; |
|
| 18 | + | /* syntax */ |
|
| 19 | + | --sy-kw:#ff9de6; --sy-str:#a8e39b; --sy-num:#ffd479; --sy-cmt:#6e6e8c; --sy-fn:#8ad8ff; --sy-ty:#ffc83c; --sy-var:#e8e8f0; --sy-const:#ffb347; |
|
| 20 | + | color-scheme: dark; |
|
| 21 | + | } |
|
| 22 | + | :root[data-theme="light"] { |
|
| 23 | + | --bg:#f5f7f2; --panel:#ffffff; --soft:#eef2ea; --ink:#2b2f2a; --mute:#7a807a; --line:#d9ddd6; --dim:#b9bfb5; |
|
| 24 | + | --acc:#5b9c1c; --band:#6aa91f; --band-ink:#ffffff; --band-line:rgba(255,255,255,.25); --now:#3e5f8f; --now-ink:#ffffff; |
|
| 25 | + | --ok:#2e8b3a; --bad:#c8322b; --warn:#b8651b; |
|
| 26 | + | --add:#e3f5e1; --del:#fbe3e1; --add-ink:#1f6b2a; --del-ink:#a12b25; |
|
| 27 | + | --m1:#c9931a; --m2:#7a8290; --m3:#b0693a; --m4:#a25c46; --m5:#8a8f9c; |
|
| 28 | + | --sy-kw:#a626a4; --sy-str:#50a14f; --sy-num:#986801; --sy-cmt:#9a9a9a; --sy-fn:#4078f2; --sy-ty:#c18401; --sy-var:#2b2f2a; --sy-const:#b8651b; |
|
| 29 | + | color-scheme: light; |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | * { box-sizing: border-box; } |
|
| 33 | + | html { background: var(--bg); } |
|
| 34 | + | body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--ink); font: 15px/1.45 "Barlow", "IBM Plex Sans", system-ui, sans-serif; } |
|
| 35 | + | a { color: var(--acc); text-decoration: none; } |
|
| 36 | + | a:hover { text-decoration: underline; } |
|
| 37 | + | code, pre, kbd, .mono { font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; } |
|
| 38 | + | .px { font-family: "Press Start 2P", "Silkscreen", monospace; font-size: 13px; } |
|
| 39 | + | .pxs { font-family: "Press Start 2P", "Silkscreen", monospace; font-size: 11px; } |
|
| 40 | + | h1, h2, h3 { margin: 0; } |
|
| 41 | + | h2 { font-size: 16px; font-weight: 600; margin: 0 0 8px; } |
|
| 42 | + | h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--mute); margin: 14px 0 6px; } |
|
| 43 | + | p { margin: 0 0 10px; } |
|
| 44 | + | .small { font-size: 12px; } |
|
| 45 | + | .mute { color: var(--mute); } |
|
| 46 | + | .dim { color: var(--dim); } |
|
| 47 | + | .center { text-align: center; } |
|
| 48 | + | .right { text-align: right; } |
|
| 49 | + | .empty { color: var(--mute); padding: 22px; text-align: center; border: 1px dashed var(--line); } |
|
| 50 | + | .lead { font-size: 16px; } |
|
| 51 | + | kbd { background: var(--soft); border: 1px solid var(--line); padding: 1px 5px; font-size: 11px; } |
|
| 52 | + | ||
| 53 | + | /* shell */ |
|
| 54 | + | header { background: var(--panel); border-bottom: 1px solid var(--line); padding: 14px 22px; display: flex; align-items: center; gap: 16px; } |
|
| 55 | + | header .brand span, footer .brand span { color: var(--acc); } |
|
| 56 | + | header .brand, footer .brand { color: var(--ink); } |
|
| 57 | + | header nav { margin-left: auto; display: flex; gap: 8px; } |
|
| 58 | + | form.find { margin: 0; flex: 0 1 320px; } |
|
| 59 | + | form.find input { width: 100%; background: var(--bg); border: 1px solid var(--line); color: var(--ink); padding: 6px 10px; font: inherit; } |
|
| 60 | + | form.find input:focus { outline: 2px solid var(--band); outline-offset: 0; } |
|
| 61 | + | .band { background: var(--band); display: flex; align-items: stretch; padding: 0 22px; } |
|
| 62 | + | .band a { color: var(--band-ink); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; padding: 9px 14px; border-right: 1px solid var(--band-line); } |
|
| 63 | + | .band a:first-child { border-left: 1px solid var(--band-line); } |
|
| 64 | + | .band a.on { box-shadow: inset 0 -3px 0 var(--acc); } |
|
| 65 | + | .band a:hover { text-decoration: none; background: rgba(0,0,0,.12); } |
|
| 66 | + | .band .grow { flex: 1; } |
|
| 67 | + | .band a.dim { opacity: .6; border: 0; } |
|
| 68 | + | .tabs { background: var(--panel); border-bottom: 1px solid var(--line); } |
|
| 69 | + | .tabs-in { max-width: 1180px; margin: 0 auto; padding: 0 22px; display: flex; align-items: center; gap: 2px; flex-wrap: wrap; } |
|
| 70 | + | .tabs .crumb { margin-right: 18px; padding: 10px 0; font-size: 16px; } |
|
| 71 | + | .tabs a:not(.crumb a) { padding: 10px 12px; color: var(--ink); font-size: 13px; } |
|
| 72 | + | .tabs a.on { border-bottom: 2px solid var(--acc); font-weight: 600; } |
|
| 73 | + | .tabs a:hover { text-decoration: none; background: var(--soft); } |
|
| 74 | + | main { flex: 1; width: 100%; max-width: 1180px; margin: 0 auto; padding: 14px 22px 28px; } |
|
| 75 | + | main.wide { max-width: none; } |
|
| 76 | + | footer { background: var(--panel); border-top: 1px solid var(--line); margin-top: 28px; } |
|
| 77 | + | .foot { max-width: 1180px; margin: 0 auto; padding: 22px; display: grid; grid-template-columns: 1fr auto; gap: 22px; } |
|
| 78 | + | .foot .cols { display: flex; gap: 32px; } |
|
| 79 | + | .foot .cols a { display: block; font-size: 13px; } |
|
| 80 | + | .head { border-bottom: 2px solid var(--band); margin: 6px 0 16px; padding-bottom: 6px; } |
|
| 81 | + | .head h1 { font-size: 30px; font-weight: 300; color: var(--mute); } |
|
| 82 | + | .head h1 small { display: block; font-size: 13px; color: var(--acc); font-weight: 400; } |
|
| 83 | + | .strip { padding: 8px 12px; margin: 0 0 12px; border: 1px solid; font-size: 13px; } |
|
| 84 | + | .strip.warn { border-color: var(--warn); color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); } |
|
| 85 | + | .strip.bad { border-color: var(--bad); color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); } |
|
| 86 | + | .strip a { color: inherit; text-decoration: underline; } |
|
| 87 | + | .sheet { position: fixed; right: 22px; bottom: 22px; background: var(--panel); border: 1px solid var(--line); padding: 14px 18px; box-shadow: 0 2px 12px rgba(0,0,0,.35); } |
|
| 88 | + | .sheet td { padding: 2px 8px 2px 0; font-size: 13px; } |
|
| 89 | + | ||
| 90 | + | /* buttons, tags, chips */ |
|
| 91 | + | button, a.go, button.go { font: inherit; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; background: var(--panel); color: var(--acc); border: 1px solid var(--acc); padding: 5px 10px; cursor: pointer; } |
|
| 92 | + | button:hover, a.go:hover { background: var(--acc); color: var(--bg); text-decoration: none; } |
|
| 93 | + | button.small, a.go.small { padding: 2px 8px; font-size: 11px; } |
|
| 94 | + | .tag { display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--acc); background: var(--soft); padding: 1px 6px; vertical-align: middle; } |
|
| 95 | + | .tag.bad { color: var(--bad); } |
|
| 96 | + | .tag.warn { color: var(--warn); } |
|
| 97 | + | .tag.head { color: var(--now-ink); background: var(--now); } |
|
| 98 | + | .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; background: var(--soft); padding: 1px 6px; border: 1px solid var(--line); } |
|
| 99 | + | .lbl { display: inline-block; font-size: 11px; padding: 1px 7px; margin-right: 4px; border: 1px solid; vertical-align: middle; } |
|
| 100 | + | .lbl-bm { color: var(--band-ink); background: var(--band); border-color: var(--band); } |
|
| 101 | + | .lbl-tag { color: var(--acc); border-color: var(--acc); } |
|
| 102 | + | .sig { display: inline-block; width: 1.2em; text-align: center; font-weight: 600; } |
|
| 103 | + | .sig-valid { color: var(--ok); } |
|
| 104 | + | .sig-unsigned { color: var(--dim); } |
|
| 105 | + | .sig-invalid { color: var(--bad); } |
|
| 106 | + | .sig-revoked { color: var(--warn); } |
|
| 107 | + | ||
| 108 | + | /* lists, tables, panels */ |
|
| 109 | + | ul.plain { list-style: none; margin: 0; padding: 0; } |
|
| 110 | + | ul.plain li { padding: 2px 0; } |
|
| 111 | + | ul.rows { list-style: none; margin: 0; padding: 0; } |
|
| 112 | + | ul.rows li { display: flex; justify-content: space-between; align-items: center; gap: 14px; background: var(--panel); border: 1px solid var(--line); padding: 12px 14px; margin-bottom: 6px; } |
|
| 113 | + | ul.rows .name { font-size: 16px; } |
|
| 114 | + | ul.rows p { margin: 4px 0 0; color: var(--mute); } |
|
| 115 | + | .table { border: 1px solid var(--line); overflow-x: auto; } |
|
| 116 | + | table { border-collapse: collapse; width: 100%; } |
|
| 117 | + | th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--mute); padding: 8px 10px; border-bottom: 1px solid var(--line); background: var(--soft); } |
|
| 118 | + | td { padding: 7px 10px; background: var(--panel); border-bottom: 1px solid var(--line); vertical-align: top; } |
|
| 119 | + | td.px { color: var(--acc); } |
|
| 120 | + | .panel { background: var(--panel); border: 1px solid var(--line); padding: 14px 16px; margin-bottom: 14px; } |
|
| 121 | + | dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; background: var(--panel); border: 1px solid var(--line); padding: 12px 16px; margin: 0 0 14px; } |
|
| 122 | + | dl.facts dt { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--mute); padding-top: 3px; } |
|
| 123 | + | dl.facts dd { margin: 0; } |
|
| 124 | + | code.full { word-break: break-all; } |
|
| 125 | + | pre.msg { background: var(--panel); border: 1px solid var(--line); padding: 10px 12px; white-space: pre-wrap; font-family: inherit; font-size: 14px; } |
|
| 126 | + | pre.marked { background: var(--panel); border: 1px solid var(--bad); padding: 10px 12px; overflow-x: auto; } |
|
| 127 | + | ||
| 128 | + | /* repository layout */ |
|
| 129 | + | .side-layout { display: grid; grid-template-columns: 1fr 280px; gap: 22px; } |
|
| 130 | + | .side h3:first-child { margin-top: 0; } |
|
| 131 | + | .clone { display: flex; gap: 6px; align-items: center; } |
|
| 132 | + | .clone code { background: var(--bg); border: 1px solid var(--line); padding: 4px 8px; flex: 1; overflow-x: auto; white-space: nowrap; } |
|
| 133 | + | .bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; } |
|
| 134 | + | .ref { color: var(--acc); } |
|
| 135 | + | .crumbs a { color: var(--ink); } |
|
| 136 | + | table.tree td { padding: 6px 10px; } |
|
| 137 | + | table.tree td.g { width: 1.6em; color: var(--acc); text-align: center; } |
|
| 138 | + | table.tree td.last { color: var(--mute); max-width: 520px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 139 | + | table.tree td.last a { color: var(--mute); } |
|
| 140 | + | table.tree td.age { width: 9em; text-align: right; white-space: nowrap; } |
|
| 141 | + | article.doc { max-width: 60rem; background: var(--panel); border: 1px solid var(--line); padding: 18px 24px; margin-top: 14px; } |
|
| 142 | + | article.doc h1 { font-size: 26px; font-weight: 500; margin: 0 0 12px; } |
|
| 143 | + | article.doc h2 { font-size: 20px; margin: 18px 0 8px; } |
|
| 144 | + | article.doc h3 { text-transform: none; letter-spacing: 0; font-size: 16px; color: var(--ink); } |
|
| 145 | + | article.doc pre { background: var(--bg); border: 1px solid var(--line); padding: 10px 12px; overflow-x: auto; } |
|
| 146 | + | article.doc code { background: var(--soft); padding: 0 3px; } |
|
| 147 | + | article.doc pre code { background: none; padding: 0; } |
|
| 148 | + | article.doc table { width: auto; } |
|
| 149 | + | article.doc img { max-width: 100%; } |
|
| 150 | + | article.doc blockquote { border-left: 3px solid var(--band); margin: 0; padding: 2px 12px; color: var(--mute); } |
|
| 151 | + | ||
| 152 | + | /* files and diffs */ |
|
| 153 | + | .filebox { background: var(--panel); border: 1px solid var(--line); } |
|
| 154 | + | .filebox > header { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--soft); } |
|
| 155 | + | .modes a { font-size: 12px; padding: 2px 8px; border: 1px solid var(--line); margin-left: 4px; color: var(--ink); } |
|
| 156 | + | .modes a.on { border-color: var(--acc); color: var(--acc); } |
|
| 157 | + | table.code { width: 100%; } |
|
| 158 | + | table.code td { padding: 0 8px; border: 0; background: transparent; line-height: 1.5; font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 13px; } |
|
| 159 | + | table.code td.n { width: 1%; min-width: 3em; text-align: right; color: var(--dim); user-select: none; cursor: pointer; padding-right: 12px; } |
|
| 160 | + | table.code td.n:hover { color: var(--acc); } |
|
| 161 | + | table.code td.t pre { margin: 0; white-space: pre; } |
|
| 162 | + | table.code tr.hl td { background: color-mix(in srgb, var(--acc) 18%, transparent); } |
|
| 163 | + | table.code td.blame { width: 1%; white-space: nowrap; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--soft); font-family: "Barlow", sans-serif; font-size: 12px; padding: 0 10px; vertical-align: top; line-height: 1.5; } |
|
| 164 | + | table.code td.blame .px { font-size: 9px; } |
|
| 165 | + | table.code td.blame .who { color: var(--ink); } |
|
| 166 | + | table.diff td.s { width: 1%; user-select: none; color: var(--mute); } |
|
| 167 | + | table.diff tr.add td { background: var(--add); color: var(--add-ink); } |
|
| 168 | + | table.diff tr.del td { background: var(--del); color: var(--del-ink); } |
|
| 169 | + | table.diff tr.gap td { background: var(--soft); color: var(--mute); font-family: "Barlow", sans-serif; font-size: 12px; padding: 2px 8px; } |
|
| 170 | + | .file-diff { background: var(--panel); border: 1px solid var(--line); margin: 0 0 14px; } |
|
| 171 | + | .file-diff > header { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--soft); border-bottom: 1px solid var(--line); } |
|
| 172 | + | .file-diff .counts { margin-left: auto; font-family: ui-monospace, monospace; font-size: 12px; } |
|
| 173 | + | .counts .add { color: var(--add-ink); } |
|
| 174 | + | .counts .del { color: var(--del-ink); } |
|
| 175 | + | .kind { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; padding: 1px 6px; border: 1px solid var(--line); color: var(--mute); } |
|
| 176 | + | .kind-added { color: var(--ok); border-color: var(--ok); } |
|
| 177 | + | .kind-removed { color: var(--bad); border-color: var(--bad); } |
|
| 178 | + | .kind-renamed, .kind-modified { color: var(--acc); border-color: var(--acc); } |
|
| 179 | + | nav.files { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 12px; } |
|
| 180 | + | ||
| 181 | + | /* history */ |
|
| 182 | + | .hist { background: var(--panel); border: 1px solid var(--line); } |
|
| 183 | + | .hrow { display: flex; align-items: stretch; border-bottom: 1px solid var(--line); } |
|
| 184 | + | .hrow:last-child { border-bottom: 0; } |
|
| 185 | + | .hrow svg.lanes { flex: 0 0 auto; display: block; } |
|
| 186 | + | .lane { stroke: var(--band); stroke-width: 2; fill: none; } |
|
| 187 | + | .lane.none { stroke: none; } |
|
| 188 | + | .lane.dashed { stroke-dasharray: 3 3; stroke: var(--dim); } |
|
| 189 | + | .node { fill: var(--acc); } |
|
| 190 | + | .node.head { fill: var(--now); stroke: var(--ink); stroke-width: 1.5; } |
|
| 191 | + | .node.res { fill: var(--ok); } |
|
| 192 | + | .chg { display: flex; justify-content: space-between; align-items: center; gap: 14px; padding: 8px 12px; flex: 1; min-width: 0; } |
|
| 193 | + | .chg + .chg, .chg:only-child { border-bottom: 1px solid var(--line); } |
|
| 194 | + | .hrow .chg { border-bottom: 0; } |
|
| 195 | + | .chg .title { color: var(--ink); font-weight: 500; } |
|
| 196 | + | .chg .meta { color: var(--mute); font-size: 12px; margin-top: 2px; } |
|
| 197 | + | .chg-side { display: flex; align-items: center; gap: 10px; white-space: nowrap; } |
|
| 198 | + | .chg .hash { font-size: 9px; } |
|
| 199 | + | .pager { display: flex; gap: 8px; justify-content: center; margin-top: 12px; } |
|
| 200 | + | ||
| 201 | + | @media (max-width: 760px) { |
|
| 202 | + | header { flex-wrap: wrap; } |
|
| 203 | + | .side-layout { grid-template-columns: 1fr; } |
|
| 204 | + | .foot { grid-template-columns: 1fr; } |
|
| 205 | + | main { padding: 10px 14px 20px; } |
|
| 206 | + | .band { padding: 0 8px; overflow-x: auto; } |
|
| 207 | + | .tabs-in { padding: 0 8px; } |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | /* syntax (syntect scopes, spaced classes) */ |
|
| 211 | + | .source .keyword, .source .storage { color: var(--sy-kw); } |
|
| 212 | + | .source .string { color: var(--sy-str); } |
|
| 213 | + | .source .constant.numeric, .source .constant.language { color: var(--sy-num); } |
|
| 214 | + | .source .constant { color: var(--sy-const); } |
|
| 215 | + | .source .comment { color: var(--sy-cmt); font-style: italic; } |
|
| 216 | + | .source .entity.name.function, .source .support.function, .source .meta.function-call .variable.function { color: var(--sy-fn); } |
|
| 217 | + | .source .entity.name.type, .source .storage.type, .source .support.type, .source .entity.name.struct, .source .entity.name.enum, .source .entity.name.class { color: var(--sy-ty); } |
|
| 218 | + | .source .variable { color: var(--sy-var); } |
|
| 219 | + | .source .entity.name.tag { color: var(--sy-kw); } |
|
| 220 | + | .source .entity.other.attribute-name { color: var(--sy-const); } |
|
| 221 | + | .source .markup.heading { color: var(--sy-ty); font-weight: 600; } |
|
| 222 | + | .source .markup.bold { font-weight: 600; } |
|
| 223 | + | .source .markup.italic { font-style: italic; } |
|
| 224 | + | .source .markup.raw { color: var(--sy-str); } |
|
| 225 | + | .source .markup.underline.link { color: var(--sy-fn); } |
added templates/tags.html +21 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Tags · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "tags") %}{% endblock %} |
|
| 6 | + | {% block content %} |
|
| 7 | + | <div class="head"><h1>Tags <small>{{ tags.len() }}</small></h1></div> |
|
| 8 | + | {% if tags.is_empty() %}<p class="empty">No tags. <code>oxy tag create NAME -m "message"</code> then push.</p>{% else %} |
|
| 9 | + | <div class="table"><table> |
|
| 10 | + | <tr><th>Tag</th><th>State</th><th>Message</th><th>Signed</th></tr> |
|
| 11 | + | {% for t in tags %} |
|
| 12 | + | <tr> |
|
| 13 | + | <td class="px"><a href="{{ repo.url }}/tree/{{ t.name }}">{{ t.name }}</a></td> |
|
| 14 | + | <td>{% for h in t.heads %}<a class="chip" href="{{ repo.url }}/change/{{ h }}">{{ h|short }}</a> {% endfor %}</td> |
|
| 15 | + | <td>{{ t.message }}</td> |
|
| 16 | + | <td>{% if let Some(s) = t.signer %}<span class="sig sig-valid">✔</span> {{ s }}{% else %}<span class="sig sig-unsigned">○</span> unsigned{% endif %}</td> |
|
| 17 | + | </tr> |
|
| 18 | + | {% endfor %} |
|
| 19 | + | </table></div> |
|
| 20 | + | {% endif %} |
|
| 21 | + | {% endblock %} |
added templates/tree.html +15 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}{{ t.path }} at {{ t.target.label|short }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}" data-ref="{{ t.target.label }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "code") %}{% endblock %} |
|
| 6 | + | {% block mainclass %} class="wide"{% endblock %} |
|
| 7 | + | {% block content %} |
|
| 8 | + | <div class="bar"> |
|
| 9 | + | <span class="ref px">{% if t.target.kind == crate::repos::view::RefKind::Change %}# {{ t.target.label|short }}{% else %}◆ {{ t.target.label }}{% endif %}</span> |
|
| 10 | + | <span class="crumbs"><a href="{{ repo.url }}/tree/{{ t.target.label }}">{{ repo.name }}</a>{% for (n, p) in t.crumbs %} / <a href="{{ repo.url }}/tree/{{ t.target.label }}/{{ p }}">{{ n }}</a>{% endfor %}</span> |
|
| 11 | + | <a class="go small" href="{{ repo.url }}/changes/{{ t.target.label }}{% if !t.path.is_empty() %}?path={{ t.path }}{% endif %}">history</a> |
|
| 12 | + | </div> |
|
| 13 | + | {% include "tree_rows.html" %} |
|
| 14 | + | {% if let Some(html) = readme_html %}<article class="doc readme">{{ html|safe }}</article>{% endif %} |
|
| 15 | + | {% endblock %} |
added templates/tree_rows.html +22 −0
| 1 | + | {% if t.entries.is_empty() %} |
|
| 2 | + | <p class="empty">This repository is empty. From an existing checkout: <code>oxy remote add hub {{ repo.clone_url }}</code> then <code>oxy push hub</code>. It takes the id of the first repository pushed, so clones made later share it.</p> |
|
| 3 | + | {% else %} |
|
| 4 | + | <table class="tree" data-last="{{ repo.url }}/last/{{ t.target.label }}" data-change="{{ repo.url }}/change/"> |
|
| 5 | + | {% if !t.path.is_empty() %} |
|
| 6 | + | <tr><td class="g">↰</td><td colspan="3"><a href="{{ repo.url }}/tree/{{ t.target.label }}{% if let Some((_, p)) = t.crumbs.iter().rev().nth(1) %}/{{ p }}{% endif %}">..</a></td></tr> |
|
| 7 | + | {% endif %} |
|
| 8 | + | {% for e in t.entries %} |
|
| 9 | + | <tr data-path="{{ e.path }}"> |
|
| 10 | + | <td class="g">{{ e.kind.glyph() }}</td> |
|
| 11 | + | <td class="name"> |
|
| 12 | + | {% if e.kind == crate::repos::view::EntryKind::Dir %}<a href="{{ repo.url }}/tree/{{ t.target.label }}/{{ e.path }}">{{ e.name }}</a> |
|
| 13 | + | {% else %}<a href="{{ repo.url }}/file/{{ t.target.label }}/{{ e.path }}">{{ e.name }}</a>{% endif %} |
|
| 14 | + | {% if e.exec %}<span class="tag">exec</span>{% endif %} |
|
| 15 | + | {% if e.conflicted %}<span class="tag bad">conflict</span>{% endif %} |
|
| 16 | + | </td> |
|
| 17 | + | <td class="last"><a href="{{ repo.url }}/changes/{{ t.target.label }}?path={{ e.path }}" class="mute">…</a></td> |
|
| 18 | + | <td class="age mute"></td> |
|
| 19 | + | </tr> |
|
| 20 | + | {% endfor %} |
|
| 21 | + | </table> |
|
| 22 | + | {% endif %} |
added templates/tree_table.html +2 −0
| 1 | + | {% let t = o.tree %} |
|
| 2 | + | {% include "tree_rows.html" %} |
added templates/trust.html +40 −0
| 1 | + | {% extends "base.html" %} |
|
| 2 | + | {% import "repo_tabs.html" as m %} |
|
| 3 | + | {% block title %}Trust · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
|
| 4 | + | {% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
|
| 5 | + | {% block tabs %}{% call m::tabs(repo, "trust") %}{% endblock %} |
|
| 6 | + | {% block content %} |
|
| 7 | + | <div class="head"><h1>Trust <small>who may update what, and what has been verified</small></h1></div> |
|
| 8 | + | {% if let Some(p) = t.policy %} |
|
| 9 | + | <section class="panel"> |
|
| 10 | + | <h2>Policy serial {{ p.serial }} {% if p.expired %}<span class="tag bad">expired {{ p.expires_ms|when }}</span>{% else if p.expires_ms > 0 %}<span class="tag">expires {{ p.expires_ms|when }}</span>{% endif %} <span class="mute small">{{ p.signatures|plural("signature", "signatures") }}, {{ p.history|plural("version", "versions") }}</span></h2> |
|
| 11 | + | <h3>Roots <span class="mute small">threshold {{ p.root_threshold }}</span></h3> |
|
| 12 | + | <ul class="plain">{% for k in p.roots %}<li><span class="sig sig-valid">◆</span> {% if k.name.is_empty() %}<code>{{ k.short }}</code>{% else %}{{ k.name }} <span class="mute">{{ k.email }}</span> <code class="mute">{{ k.short }}</code>{% endif %}</li>{% endfor %}</ul> |
|
| 13 | + | <h3>Rules</h3> |
|
| 14 | + | <div class="table"><table> |
|
| 15 | + | <tr><th>Pattern</th><th>Keys</th><th>Threshold</th><th>Require</th><th>Conflicts</th></tr> |
|
| 16 | + | {% for r in p.rules %}<tr><td class="px">{{ r.pattern }}</td><td>{% for k in r.keys %}{% if k.name.is_empty() %}<code>{{ k.short }}</code>{% else %}{{ k.name }}{% endif %}{% if !loop.last %}, {% endif %}{% endfor %}</td><td>{{ r.threshold }}</td><td>{{ r.require }}</td><td>{% if r.allow_conflicts %}allowed{% else %}refused{% endif %}</td></tr>{% endfor %} |
|
| 17 | + | </table></div> |
|
| 18 | + | {% if !p.revoked.is_empty() %}<h3>Revoked</h3><ul class="plain">{% for k in p.revoked %}<li><span class="sig sig-revoked">⊘</span> {% if k.name.is_empty() %}<code>{{ k.short }}</code>{% else %}{{ k.name }} <code class="mute">{{ k.short }}</code>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 19 | + | </section> |
|
| 20 | + | {% else %} |
|
| 21 | + | <div class="strip warn">No trust policy: every reference update from anyone who may push is accepted. Create one from a client with <code>oxy policy init</code> and push.</div> |
|
| 22 | + | {% endif %} |
|
| 23 | + | <section class="panel"> |
|
| 24 | + | <h2>Verification <span class="mute small">{{ t.verification.total|plural("change", "changes") }}</span></h2> |
|
| 25 | + | <p> |
|
| 26 | + | <span class="sig sig-valid">✔</span> {{ t.verification.signed }} signed · |
|
| 27 | + | <span class="sig sig-valid">◈</span> {{ t.verification.sealed }} sealed · |
|
| 28 | + | <span class="sig sig-unsigned">○</span> {{ t.verification.unsigned }} unsigned |
|
| 29 | + | {% if t.verification.revoked > 0 %} · <span class="sig sig-revoked">⊘</span> {{ t.verification.revoked }} by revoked keys{% endif %} |
|
| 30 | + | {% if !t.verification.invalid.is_empty() %} · <span class="sig sig-invalid">✘</span> <strong>{{ t.verification.invalid.len() }} INVALID</strong>{% endif %} |
|
| 31 | + | </p> |
|
| 32 | + | {% if !t.verification.invalid.is_empty() %}<ul class="plain">{% for (h, why) in t.verification.invalid %}<li><a class="chip" href="{{ repo.url }}/change/{{ h }}">{{ h|short }}</a> {{ why }}</li>{% endfor %}</ul>{% endif %} |
|
| 33 | + | </section> |
|
| 34 | + | <section class="panel"> |
|
| 35 | + | <h2>Hub delegate key</h2> |
|
| 36 | + | <p class="small">Changes the hub makes for you in the browser (later milestones) are signed with this key on your behalf, once you have run <code>oxy identity delegate add hub.identity</code> with the file from <a href="/.well-known/oxyhub-key">/.well-known/oxyhub-key</a>.</p> |
|
| 37 | + | <p><code class="full">{{ t.hub_key }}</code></p> |
|
| 38 | + | {% if t.hub_delegated_by.is_empty() %}<p class="mute small">Nobody in this repository's policy has delegated to it.</p>{% else %}<p class="small">Delegated by: {% for k in t.hub_delegated_by %}{{ k.name }}{% if !loop.last %}, {% endif %}{% endfor %}</p>{% endif %} |
|
| 39 | + | </section> |
|
| 40 | + | {% endblock %} |
modified .oxyignore +2 −0
| 1 | 1 | /target |
|
| 2 | 2 | /.claude |
|
| 3 | + | /data |
|
| 4 | + | *.vsix |