diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-06-06 15:45:11 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-06-06 15:45:11 +0200 |
commit | a6baea06697f6c76c695dc4198099deb8ba916e0 (patch) | |
tree | 476a3865f6b4bef04751ba20534813a58892811b | |
parent | chore: Initial commit (diff) | |
download | back-a6baea06697f6c76c695dc4198099deb8ba916e0.zip |
feat(treewide): Prepare for first release
This commit contains many changes, as they were developed alongside `git-bug-rs` and unfortunately not separately committed. A toplevel summary would include: - Appropriate redirects, - The templating moved to `vy` (as this works with rustfmt formatting), - Search support (via `git-bug-rs`), - And better layout in the link section.
-rw-r--r-- | Cargo.lock | 325 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | assets/search.png | bin | 0 -> 711 bytes | |||
-rw-r--r-- | assets/style.css | 3 | ||||
-rw-r--r-- | contrib/projects.list | 3 | ||||
-rw-r--r-- | flake.lock | 54 | ||||
-rw-r--r-- | flake.nix | 91 | ||||
-rw-r--r-- | rustfmt_config.toml (renamed from rustfmt.toml) | 2 | ||||
-rw-r--r-- | src/cli.rs | 2 | ||||
-rw-r--r-- | src/config/mod.rs | 66 | ||||
-rw-r--r-- | src/error/mod.rs | 165 | ||||
-rw-r--r-- | src/main.rs | 9 | ||||
-rw-r--r-- | src/web/generate/mod.rs | 522 | ||||
-rw-r--r-- | src/web/generate/templates.rs | 44 | ||||
-rw-r--r-- | src/web/mod.rs | 125 | ||||
-rw-r--r-- | src/web/responses.rs | 135 | ||||
-rw-r--r-- | templates/issue.html | 74 | ||||
-rw-r--r-- | templates/issues.html | 77 | ||||
-rw-r--r-- | templates/repos.html | 62 | ||||
-rw-r--r-- | treefmt.nix | 21 |
20 files changed, 1070 insertions, 717 deletions
diff --git a/Cargo.lock b/Cargo.lock index 61a5656..59d9fb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,12 +100,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -151,8 +151,8 @@ name = "back" version = "0.1.0" dependencies = [ "bytes", - "chrono", "clap", + "git-bug", "gix", "http", "http-body-util", @@ -160,15 +160,14 @@ dependencies = [ "hyper-util", "log", "markdown", - "rinja", "rss", "serde", "serde_json", - "sha2", "stderrlog", "thiserror", "tokio", "url", + "vy", ] [[package]] @@ -187,10 +186,19 @@ dependencies = [ ] [[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -238,9 +246,9 @@ checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ "shlex", ] @@ -267,9 +275,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -277,9 +285,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -312,6 +320,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -483,6 +497,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -499,9 +525,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -547,6 +573,15 @@ dependencies = [ ] [[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -623,9 +658,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -635,6 +672,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] +name = "git-bug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfe9a5393ead84bdd88d054a0e2bb71296490bf90846b07d90eba8ec86fa6d" +dependencies = [ + "base64", + "chrono", + "faster-hex", + "gix", + "log", + "postcard", + "redb", + "serde", + "sha2", + "simd-json", + "thiserror", + "url", +] + +[[package]] name = "gix" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -679,6 +736,7 @@ dependencies = [ "gix-submodule", "gix-tempfile", "gix-trace", + "gix-transport", "gix-traverse", "gix-url", "gix-utils", @@ -689,6 +747,7 @@ dependencies = [ "once_cell", "parking_lot", "regex", + "serde", "signal-hook", "smallvec", "thiserror", @@ -704,15 +763,16 @@ dependencies = [ "gix-date", "gix-utils", "itoa", + "serde", "thiserror", "winnow", ] [[package]] name = "gix-archive" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e59cc5867c40065122324265d99416ca0ddeb51d007870868b5f835423fb6c" +checksum = "8826f20d84822a9abd9e81d001c61763a25f4ec96caa073fb0b5457fe766af21" dependencies = [ "bstr", "gix-date", @@ -724,9 +784,9 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e26b3ac280ddb25bb6980d34f4a82ee326f78bf2c6d4ea45eef2d940048b8e" +checksum = "6f50d813d5c2ce9463ba0c29eea90060df08e38ad8f34b8a192259f8bce5c078" dependencies = [ "bstr", "gix-glob", @@ -734,6 +794,7 @@ dependencies = [ "gix-quote", "gix-trace", "kstring", + "serde", "smallvec", "thiserror", "unicode-bom", @@ -759,9 +820,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f47f3fb4ba33644061e8e0e1030ef2a937d42dc969553118c320a205a9fb28" +checksum = "d05dd813ef6bb798570308aa7f1245cefa350ec9f30dc53308335eb22b9d0f8b" dependencies = [ "bstr", "gix-path", @@ -780,6 +841,7 @@ dependencies = [ "gix-chunk", "gix-hash", "memmap2", + "serde", "thiserror", ] @@ -831,6 +893,7 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", + "serde", "thiserror", ] @@ -843,6 +906,7 @@ dependencies = [ "bstr", "itoa", "jiff", + "serde", "smallvec", "thiserror", ] @@ -931,9 +995,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90c21f0d61778f518bbb7c431b00247bf4534b2153c3e85bcf383876c55ca6c" +checksum = "ecf004912949bbcf308d71aac4458321748ecb59f4d046830d25214208c471f1" dependencies = [ "bstr", "encoding_rs", @@ -966,14 +1030,15 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2926b03666e83b8d01c10cf06e5733521aacbd2d97179a4c9b1fdddabb9e937d" +checksum = "90181472925b587f6079698f79065ff64786e6d6c14089517a1972bca99fb6e9" dependencies = [ "bitflags", "bstr", "gix-features", "gix-path", + "serde", ] [[package]] @@ -984,6 +1049,7 @@ checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" dependencies = [ "faster-hex", "gix-features", + "serde", "sha1-checked", "thiserror", ] @@ -1009,6 +1075,7 @@ dependencies = [ "gix-glob", "gix-path", "gix-trace", + "serde", "unicode-bom", ] @@ -1036,6 +1103,7 @@ dependencies = [ "libc", "memmap2", "rustix", + "serde", "smallvec", "thiserror", ] @@ -1060,6 +1128,7 @@ dependencies = [ "bstr", "gix-actor", "gix-date", + "serde", "thiserror", ] @@ -1095,6 +1164,7 @@ dependencies = [ "gix-utils", "gix-validate", "itoa", + "serde", "smallvec", "thiserror", "winnow", @@ -1117,6 +1187,7 @@ dependencies = [ "gix-path", "gix-quote", "parking_lot", + "serde", "tempfile", "thiserror", ] @@ -1135,6 +1206,7 @@ dependencies = [ "gix-object", "gix-path", "memmap2", + "serde", "smallvec", "thiserror", "uluru", @@ -1221,6 +1293,7 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", + "serde", "thiserror", "winnow", ] @@ -1253,6 +1326,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", + "serde", "thiserror", "winnow", ] @@ -1286,6 +1360,7 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", + "serde", "thiserror", ] @@ -1313,6 +1388,7 @@ dependencies = [ "bitflags", "gix-path", "libc", + "serde", "windows-sys 0.59.0", ] @@ -1325,6 +1401,7 @@ dependencies = [ "bstr", "gix-hash", "gix-lock", + "serde", "thiserror", ] @@ -1401,6 +1478,7 @@ dependencies = [ "gix-quote", "gix-sec", "gix-url", + "serde", "thiserror", ] @@ -1431,6 +1509,7 @@ dependencies = [ "gix-features", "gix-path", "percent-encoding", + "serde", "thiserror", "url", ] @@ -1473,6 +1552,7 @@ dependencies = [ "gix-object", "gix-path", "gix-validate", + "serde", ] [[package]] @@ -1497,9 +1577,9 @@ dependencies = [ [[package]] name = "gix-worktree-stream" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7731f9e7ffc45f1f79d6601a37169be0697d4e2bd015495b4f53dffd80b42e" +checksum = "5acc0f942392e0cae6607bfd5fe39e56e751591271bdc8795c6ec34847d17948" dependencies = [ "gix-attributes", "gix-features", @@ -1533,6 +1613,15 @@ dependencies = [ ] [[package]] +name = "halfbrown" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2c385c6df70fd180bbb673d93039dbd2cd34e41d782600bdf6e1ca7bce39aa" +dependencies = [ + "hashbrown 0.15.3", +] + +[[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1557,6 +1646,8 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1665,12 +1756,12 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "hyper", @@ -1751,9 +1842,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -1767,9 +1858,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -1868,10 +1959,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] +name = "itoap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" + +[[package]] name = "jiff" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1884,9 +1981,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", @@ -1924,6 +2021,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ + "serde", "static_assertions", ] @@ -2027,13 +2125,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2067,6 +2165,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2123,6 +2227,18 @@ dependencies = [ ] [[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2178,6 +2294,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] +name = "redb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef6a6d3a65ea334d6cdfb31fa2525c20184b7aa7bd1ad1e2e37502610d4609f" +dependencies = [ + "libc", +] + +[[package]] name = "redox_syscall" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2187,6 +2312,26 @@ dependencies = [ ] [[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2216,12 +2361,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "rinja" -version = "0.4.0+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67047e6d76ddf18132ef41c8a3465dceb48cf05edd3eb38189456ebe83875ea8" - -[[package]] name = "rss" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2254,9 +2393,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -2375,6 +2514,25 @@ dependencies = [ ] [[package]] +name = "simd-json" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c962f626b54771990066e5435ec8331d1462576cd2d1e62f24076ae014f92112" +dependencies = [ + "getrandom", + "halfbrown", + "ref-cast", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2388,12 +2546,15 @@ name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2531,9 +2692,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -2655,12 +2816,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] +name = "value-trait" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0508fce11ad19e0aab49ce20b6bec7f8f82902ded31df1c9fc61b90f0eb396b8" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] +name = "vy" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9415823937514bbba8aeddee84871f1b137f107878a0fdebf6c8ec2846b3a6" +dependencies = [ + "vy-core", + "vy-macros", +] + +[[package]] +name = "vy-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c86e70f659b92a9bb20120384e1af5a29eac7f926c484b51fad034e45c8acf" +dependencies = [ + "itoap", + "ryu", +] + +[[package]] +name = "vy-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567c0fb4cea81da221859fbcffa37f614c0460f8804f058c0973b76b571997db" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "vy-core", +] + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2776,9 +2981,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", @@ -2817,18 +3022,18 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index a898572..ac8b303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ name = "back" description = "An extremely simple git bug visualization system. Inspired by TVL's panettone." version = "0.1.0" -edition = "2021" +edition = "2024" license = "AGPL-3.0-or-later" homepage = "" repository = "https://git.foss-syndicate.org/vhack.eu/git_bug/back" @@ -23,8 +23,8 @@ repository = "https://git.foss-syndicate.org/vhack.eu/git_bug/back" [dependencies] +git-bug = { version = "0.2.3" } bytes = "1.10.1" -chrono = "0.4.41" clap = { version = "4.5.38", features = ["derive"] } gix = "0.72.1" http = "1.3.1" @@ -33,15 +33,14 @@ hyper = { version = "1.6.0", features = ["http1", "http2", "server"] } hyper-util = { version = "0.1.11", features = ["tokio"] } log = "0.4.27" markdown = "1.0.0" -rinja = "0.4.0" rss = "2.0.12" serde = "1.0.219" serde_json = "1.0.140" -sha2 = "0.10.9" stderrlog = "0.6.0" thiserror = "2.0.12" tokio = { version = "1.45.0", features = ["macros", "net", "rt-multi-thread"] } url = { version = "2.5.4", features = ["serde"] } +vy = "0.2.0" [profile.release] lto = true diff --git a/assets/search.png b/assets/search.png new file mode 100644 index 0000000..0fd78c6 --- /dev/null +++ b/assets/search.png Binary files differdiff --git a/assets/style.css b/assets/style.css index e7ce376..2f7414f 100644 --- a/assets/style.css +++ b/assets/style.css @@ -141,7 +141,7 @@ label.checkbox { .issue-search input[type="search"] { padding: 0.5rem; - background-image: url("static/search.png"); + background-image: url("/search.png"); background-position: 10px 10px; background-repeat: no-repeat; background-size: 1rem; @@ -309,6 +309,7 @@ nav { display: flex; color: var(--gray); justify-content: space-between; + align-items: center; } nav .nav-group { diff --git a/contrib/projects.list b/contrib/projects.list new file mode 100644 index 0000000..dd4a060 --- /dev/null +++ b/contrib/projects.list @@ -0,0 +1,3 @@ +owner1/project_one.git +owner2/project_two +all_the_issues_cache diff --git a/flake.lock b/flake.lock index 78982af..d984446 100644 --- a/flake.lock +++ b/flake.lock @@ -1,36 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1746291859, - "narHash": "sha256-DdWJLA+D5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q=", - "owner": "ipetkov", - "repo": "crane", - "rev": "dfd9a8dfd09db9aad544c4d3b6c47b12562544a5", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": [ @@ -69,35 +38,12 @@ }, "root": { "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay", "systems": "systems", "treefmt-nix": "treefmt-nix" } }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1747017456, - "narHash": "sha256-C/U12fcO+HEF071b5mK65lt4XtAIZyJSSJAg9hdlvTk=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "5b07506ae89b025b14de91f697eba23b48654c52", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1680978846, diff --git a/flake.nix b/flake.nix index 7389429..b6df3c8 100644 --- a/flake.nix +++ b/flake.nix @@ -21,25 +21,9 @@ }; }; - crane = { - url = "github:ipetkov/crane"; - inputs = {}; - }; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; - }; - - # inputs for following systems = { url = "github:nix-systems/x86_64-linux"; # only evaluate for this system }; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; flake-utils = { url = "github:numtide/flake-utils"; inputs = { @@ -53,81 +37,42 @@ nixpkgs, flake-utils, treefmt-nix, - crane, - rust-overlay, ... }: flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { - inherit system; - overlays = [(import rust-overlay)]; - }; - - nightly = true; - rust_minimal = - if nightly - then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal) - else pkgs.rust-bin.stable.latest.minimal; - rust_default = - if nightly - then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default) - else pkgs.rust-bin.stable.latest.default; - - cargo_toml = craneLib.cleanCargoToml {cargoToml = ./Cargo.toml;}; - pname = cargo_toml.package.name; - - craneLib = (crane.mkLib pkgs).overrideToolchain rust_minimal; - craneBuild = craneLib.buildPackage { - src = craneLib.cleanCargoSource ./.; - - doCheck = true; - }; - - manual = pkgs.stdenv.mkDerivation { - name = "${pname}-manual"; - inherit (cargo_toml.package) version; - - src = ./docs; - nativeBuildInputs = with pkgs; [pandoc]; - - buildPhase = '' - mkdir --parents $out/docs; - - pandoc "./${pname}.1.md" -s -t man > $out/docs/${pname}.1 - ''; - - installPhase = '' - install -D $out/docs/${pname}.1 $out/share/man/man1/${pname}; - ''; - }; + pkgs = nixpkgs.legacyPackages."x86_64-linux"; treefmtEval = import ./treefmt.nix { inherit treefmt-nix pkgs; - rustfmt = rust_default; }; - in { - packages.default = pkgs.symlinkJoin { - inherit (cargo_toml.package) name; - paths = [manual craneBuild]; - }; + rustfmt = pkgs.writeShellScriptBin "rustfmt" '' + # Avoid the duplicated edition flag, that rust-analyzer passes. + if [ "$1" = "--edition" ] && [ "$2" == "2024" ]; then + shift 2 + fi + ${pkgs.lib.getExe pkgs.rustfmt} ${builtins.concatStringsSep " " treefmtEval.config.settings.formatter.rustfmt.options} "$@" + ''; + in { checks = { - inherit craneBuild; formatting = treefmtEval.config.build.check self; }; formatter = treefmtEval.config.build.wrapper; devShells.default = pkgs.mkShell { - packages = with pkgs; [ - cocogitto - git-bug + packages = [ + pkgs.cocogitto + pkgs.git-bug - rust_default - cargo-edit + pkgs.rustc + pkgs.cargo + pkgs.clippy + rustfmt + pkgs.cargo-edit - reuse + pkgs.reuse ]; }; }); diff --git a/rustfmt.toml b/rustfmt_config.toml index 5c3f6ba..044f5fb 100644 --- a/rustfmt.toml +++ b/rustfmt_config.toml @@ -21,7 +21,6 @@ normalize_doc_attributes = true format_strings = true format_macro_matchers = true format_macro_bodies = true -skip_macro_invocations = [] hex_literal_case = "Upper" empty_item_single_line = true struct_lit_single_line = true @@ -74,6 +73,5 @@ skip_children = false show_parse_errors = true error_on_line_overflow = true error_on_unformatted = true -ignore = [] emit_mode = "Files" make_backup = false diff --git a/src/cli.rs b/src/cli.rs index eeeed2b..f217b00 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,7 @@ use clap::Parser; #[allow(clippy::module_name_repetitions)] /// An extremely simple git issue tracking system. /// Inspired by tvix's panettone -pub struct Cli { +pub(crate) struct Cli { /// The path to the configuration file. The file should be written in JSON. pub config_file: PathBuf, } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6c90fce..07c6c29 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,16 +15,13 @@ use std::{ path::{Path, PathBuf}, }; -use gix::ThreadSafeRepository; +use git_bug::{entities::issue::Issue, replica::Replica}; use serde::Deserialize; use url::Url; -use crate::{ - error::{self, Error}, - git_bug::dag::is_git_bug, -}; +use crate::error::{self, Error}; -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct BackConfig { /// The url to the source code of back. This is needed, because back is licensed under the /// AGPL. @@ -35,17 +32,24 @@ pub struct BackConfig { /// `issues.foss-syndicate.org` pub root_url: Url, - project_list: PathBuf, + /// The file that list all the projects. + /// + /// This is `cgit`'s `project-list` setting. + pub project_list: PathBuf, /// The path that is the common parent of all the repositories. + /// + /// This is `cgit`'s `scan-path` setting. pub scan_path: PathBuf, } +#[derive(Debug)] pub struct BackRepositories { repositories: Vec<BackRepository>, } impl BackRepositories { + #[must_use] pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter { self.into_iter() } @@ -62,23 +66,31 @@ impl<'a> IntoIterator for &'a BackRepositories { impl BackRepositories { /// Try to get the repository at path `path`. - /// If no repository was registered/found at `path`, returns an error. - pub fn get(&self, path: &Path) -> Result<&BackRepository, error::Error> { + /// + /// # Errors + /// - If no repository was registered/found at `path` + pub fn get(&self, path: &Path) -> Result<&BackRepository, Error> { self.repositories .iter() .find(|p| p.repo_path == path) - .ok_or(error::Error::RepoFind { + .ok_or(Error::RepoFind { repository_path: path.to_owned(), }) } } +#[derive(Debug)] pub struct BackRepository { repo_path: PathBuf, } impl BackRepository { - pub fn open(&self, scan_path: &Path) -> Result<ThreadSafeRepository, error::Error> { + /// Open this repository. + /// + /// # Errors + /// + /// This function will return an error if the repository could not be opened. + pub fn open(&self, scan_path: &Path) -> Result<Replica, Error> { let path = { let base = scan_path.join(&self.repo_path); if base.is_dir() { @@ -87,30 +99,43 @@ impl BackRepository { PathBuf::from(base.display().to_string() + ".git") } }; - let repo = ThreadSafeRepository::open(path).map_err(|err| Error::RepoOpen { - repository_path: self.repo_path.to_owned(), + let repo = Replica::from_path(path).map_err(|err| Error::RepoOpen { + repository_path: self.repo_path.clone(), error: Box::new(err), })?; - if is_git_bug(&repo.to_thread_local())? { + + // We could also check that Identity data is in the Replica, + // but Issue existent is paramount. + if repo.contains::<Issue>()? { Ok(repo) } else { - Err(error::Error::NotGitBug { + Err(Error::NotGitBug { path: self.repo_path.clone(), }) } } + #[must_use] pub fn path(&self) -> &Path { &self.repo_path } } impl BackConfig { + /// Returns the repositories of this [`BackConfig`]. + /// + /// # Note + /// This will always re-read the `projects.list` file, to pick up repositories that were added + /// in the mean time. + /// + /// # Errors + /// + /// This function will return an error if the associated IO operations fail. pub fn repositories(&self) -> error::Result<BackRepositories> { let repositories = fs::read_to_string(&self.project_list) - .map_err(|err| error::Error::ProjectListRead { + .map_err(|err| Error::ProjectListRead { error: err, - file: self.project_list.to_owned(), + file: self.project_list.clone(), })? .lines() .try_fold(vec![], |mut acc, path| { @@ -118,11 +143,16 @@ impl BackConfig { repo_path: PathBuf::from(path), }); - Ok::<_, error::Error>(acc) + Ok::<_, Error>(acc) })?; Ok(BackRepositories { repositories }) } + /// Construct this [`BackConfig`] from a config file. + /// + /// # Errors + /// + /// This function will return an error if the associated IO operations fail. pub fn from_config_file(path: &Path) -> error::Result<Self> { let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead { file: path.to_owned(), diff --git a/src/error/mod.rs b/src/error/mod.rs index 026cc58..f109e11 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -10,126 +10,87 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/agpl.txt>. -use std::{fmt::Display, io, net::SocketAddr, path::PathBuf}; +use std::{io, net::SocketAddr, path::PathBuf}; -use gix::hash::Prefix; +use git_bug::{ + entities::{identity::Identity, issue::Issue}, + replica::{ + self, + entity::{ + id::prefix::{self, IdPrefix}, + snapshot::Snapshot, + }, + }, +}; use thiserror::Error; -pub type Result<T> = std::result::Result<T, Error>; +pub(crate) type Result<T> = std::result::Result<T, Error>; #[derive(Error, Debug)] pub enum Error { + #[error("while trying to parse the config file ({file}): {error}")] ConfigParse { file: PathBuf, error: serde_json::Error, }, - ProjectListRead { - file: PathBuf, - error: io::Error, - }, - ConfigRead { - file: PathBuf, - error: io::Error, - }, - NotGitBug { - path: PathBuf, - }, + #[error("while trying to read the project.list file ({file}): {error}")] + ProjectListRead { file: PathBuf, error: io::Error }, + + #[error("while trying to read the config file ({file}): {error}")] + ConfigRead { file: PathBuf, error: io::Error }, + + #[error("Repository ('{path}') has no initialized git-bug data")] + NotGitBug { path: PathBuf }, + + #[error("while trying to open the repository ({repository_path}): {error}")] RepoOpen { repository_path: PathBuf, - error: Box<gix::open::Error>, - }, - RepoFind { - repository_path: PathBuf, + error: Box<replica::open::Error>, }, + + #[error("while trying to get an entity reference from a replica: {0}")] + RepoGetReferences(#[from] replica::get::Error), + + #[error("while trying to read an issues's data from a replica: {0}")] + RepoIssueRead(#[from] replica::entity::read::Error<Issue>), + + #[error("while trying to read an identity's data from a replica: {0}")] + RepoIdentityRead(#[from] replica::entity::read::Error<Identity>), + + #[error("failed to find the repository at path: '{repository_path}'")] + RepoFind { repository_path: PathBuf }, + + #[error("while iteration over the refs in a repository: {0}")] RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error), - RepoRefsPrefixed { - error: io::Error, - }, - TcpBind { - addr: SocketAddr, - err: io::Error, - }, - TcpAccept { - err: io::Error, - }, + #[error("while prefixing the refs with a path: {error}")] + RepoRefsPrefixed { error: io::Error }, + + #[error("while trying to open tcp {addr} for listening: {err}.")] + TcpBind { addr: SocketAddr, err: io::Error }, + #[error("while trying to accept a tcp connection: {err}.")] + TcpAccept { err: io::Error }, + + #[error("There is no 'issue' associated with the prefix '{prefix}': {err}")] IssuesPrefixMissing { - prefix: Prefix, + prefix: Box<IdPrefix>, + err: prefix::resolve::Error, }, - IssuesPrefixParse(#[from] gix::hash::prefix::from_hex::Error), -} -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::ConfigParse { file, error } => { - write!( - f, - "while trying to parse the config file ({}): {error}", - file.display() - ) - } - Error::ProjectListRead { file, error } => { - write!( - f, - "while trying to read the project.list file ({}): {error}", - file.display() - ) - } - Error::ConfigRead { file, error } => { - write!( - f, - "while trying to read the config file ({}): {error}", - file.display() - ) - } - Error::RepoOpen { - repository_path, - error, - } => { - write!( - f, - "while trying to open the repository ({}): {error}", - repository_path.display() - ) - } - Error::NotGitBug { path } => { - write!( - f, - "Repository ('{}') has no initialized git-bug data", - path.display() - ) - } - Error::RepoFind { repository_path } => { - write!( - f, - "failed to find the repository at path: '{}'", - repository_path.display() - ) - } - Error::RepoRefsIter(error) => { - write!(f, "while iteration over the refs in a repository: {error}",) - } - Error::RepoRefsPrefixed { error, .. } => { - write!(f, "while prefixing the refs with a path: {error}") - } - Error::IssuesPrefixMissing { prefix } => { - write!( - f, - "There is no 'issue' associated with the prefix: {prefix}" - ) - } - Error::IssuesPrefixParse(error) => { - write!(f, "The given prefix can not be parsed as prefix: {error}") - } - Error::TcpBind { addr, err } => { - write!(f, "while trying to open tcp {addr} for listening: {err}.") - } - Error::TcpAccept { err } => { - write!(f, "while trying to accept a tcp connection: {err}.") - } - } - } + #[error("The given prefix ('{prefix}') can not be parsed as prefix: {error}")] + IssuesPrefixParse { + prefix: String, + error: prefix::decode::Error, + }, + + #[error("Route '{0}' was unknown.")] + NotFound(PathBuf), + + #[error("Query '{query}' was not valid: {err}")] + InvalidQuery { + err: git_bug::query::parse::parser::Error<Snapshot<Issue>>, + query: String, + }, } diff --git a/src/main.rs b/src/main.rs index 49ffe5c..28ca543 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,15 +19,12 @@ use crate::config::BackConfig; mod cli; pub mod config; mod error; -pub mod git_bug; mod web; -fn main() -> Result<(), String> { +fn main() { if let Err(err) = server_main() { eprintln!("Error {err}"); process::exit(1); - } else { - Ok(()) } } @@ -37,9 +34,9 @@ async fn server_main() -> Result<(), error::Error> { stderrlog::new() .module(module_path!()) - .modules(["hyper", "http"]) + .modules(["hyper", "http", "git_bug"]) .quiet(false) - .show_module_names(false) + .show_module_names(true) .color(stderrlog::ColorChoice::Auto) .verbosity(2) .timestamp(stderrlog::Timestamp::Off) diff --git a/src/web/generate/mod.rs b/src/web/generate/mod.rs index 06bab17..e68bddf 100644 --- a/src/web/generate/mod.rs +++ b/src/web/generate/mod.rs @@ -1,144 +1,353 @@ -// Back - An extremely simple git bug visualization system. Inspired by TVL's -// panettone. -// -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This file is part of Back. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/agpl.txt>. - use std::{fs, path::Path}; -use gix::hash::Prefix; -use log::info; -use rinja::Template; -use url::Url; - -use crate::{ - config::BackConfig, - error, - git_bug::{ - dag::issues_from_repository, - issue::{CollapsedIssue, Status}, +use git_bug::{ + entities::{ + identity::Identity, + issue::{ + Issue, data::status::Status, query::MatchKeyValue, snapshot::timeline::IssueTimeline, + }, + }, + query::{Matcher, Query}, + replica::{ + Replica, + entity::{ + Entity, + id::prefix::IdPrefix, + snapshot::{ + Snapshot, + timeline::{Timeline, history_step::HistoryStep}, + }, + }, }, }; +use log::info; +use templates::to_markdown; +use vy::{IntoHtml, a, div, footer, form, h1, header, input, li, main, nav, ol, p, span}; -#[derive(Template)] -#[template(path = "./issues.html")] -struct IssuesTemplate { - wanted_status: Status, - counter_status: Status, - issues: Vec<CollapsedIssue>, +use crate::{config::BackConfig, error, web::generate::templates::make_page}; - /// The path to the repository - repo_path: String, +pub(crate) mod templates; - /// The URL to `back`'s source code - source_code_repository_url: Url, +fn get_other_status(current: Status) -> Status { + match current { + Status::Open => Status::Closed, + Status::Closed => Status::Open, + } } -pub fn issues( + +fn set_query_status( + mut root_matcher: Matcher<Snapshot<Issue>>, + status: Status, +) -> Matcher<Snapshot<Issue>> { + fn change_status(matcher: &mut Matcher<Snapshot<Issue>>, status: Status) { + match matcher { + Matcher::Or { lhs, rhs } | Matcher::And { lhs, rhs } => { + change_status(lhs, status); + change_status(rhs, status); + } + Matcher::Match { key_value } => { + if let MatchKeyValue::Status(found_status) = key_value { + *found_status = status; + } + } + } + } + + change_status(&mut root_matcher, status); + root_matcher +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn issues( config: &BackConfig, - wanted_status: Status, - counter_status: Status, repo_path: &Path, + replica: &Replica, + query: &Query<Snapshot<Issue>>, ) -> error::Result<String> { - let repository = config - .repositories()? - .get(repo_path)? - .open(&config.scan_path)?; - - let mut issue_list = issues_from_repository(&repository.to_thread_local())? + let mut issue_list = replica + .get_all_with_query::<Issue>(query)? + .collect::<Result<Result<Vec<Issue>, _>, _>>()?? .into_iter() - .map(|issue| issue.collapse()) - .filter(|issue| issue.status == wanted_status) - .collect::<Vec<CollapsedIssue>>(); + .map(|i| i.snapshot()) + .map(|i| -> error::Result<_> { + let author = i.author(); + Ok((i, author.resolve(replica)?.snapshot())) + }) + .collect::<error::Result<Vec<_>>>()?; // Sort by date descending. // SAFETY: // The time stamp is only used for sorting, so a malicious attacker could only affect the issue // sorting. - issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); - issue_list.reverse(); - - Ok(IssuesTemplate { - wanted_status, - counter_status, - source_code_repository_url: config.source_code_repository_url.clone(), - issues: issue_list, - repo_path: repo_path.display().to_string(), - } - .render() - .expect("This should always work")) -} + issue_list.sort_by_key(|(issue, _)| unsafe { + issue + .timeline() + .history() + .first() + .expect("Is some") + .at() + .to_unsafe() + }); + + let root_matcher = query.as_matcher().unwrap_or(&Matcher::Match { + key_value: MatchKeyValue::Status(Status::Open), + }); + + let status = { + fn find_status(matcher: &Matcher<Snapshot<Issue>>) -> Option<Status> { + fn merge(status1: Option<Status>, status2: Option<Status>) -> Option<Status> { + match (status1, status2) { + (None, None) => None, + (None, Some(b)) => Some(b), + #[allow(clippy::match_same_arms)] + (Some(a), None) => Some(a), + // TODO(@bpeetz): Should we warn the user somehow? <2025-05-28> + (Some(a), Some(_)) => Some(a), + } + } -use crate::git_bug::format::HtmlString; -#[derive(Template)] -#[template(path = "./issue.html")] -struct IssueTemplate { - issue: CollapsedIssue, + match matcher { + Matcher::Or { lhs, rhs } | Matcher::And { lhs, rhs } => { + merge(find_status(lhs), find_status(rhs)) + } + Matcher::Match { key_value } => match key_value { + MatchKeyValue::Status(status) => Some(*status), + _ => None, + }, + } + } - /// The path to the repository - repo_path: String, + find_status(root_matcher) + }; - /// The URL to `back`'s source code - source_code_repository_url: Url, + // TODO(@bpeetz): Be less verbose. <2025-06-06> + let query_string = query.to_string(); + + Ok(make_page( + ( + header!(h1!(( + status.map_or(String::new(), |a| a.to_string()), + " Issues" + ))), + main!( + div!( + class = "issue-links", + if let Some(status) = status { + a!( + href = format!( + "/{}/issues/?query={}", + repo_path.display(), + Query::from_matcher(set_query_status( + root_matcher.clone(), + get_other_status(status) + )) + .to_string() + ), + "View ", + get_other_status(status).to_string(), + " issues" + ) + }, + a!(href = "/", "View repos",), + form!( + class = "issue-search", + method = "get", + input!( + name = "query", + value = &query_string, + title = "Issue search query", + "type" = "search", + placeholder = "status:open title:\"Test\"", + size = query_string.chars().count().clamp(20, 40), + ), + input!( + class = "sr-only", + "type" = "submit", + value = "Search Issues" + ) + ) + ), + ol!( + class = "issue-list", + issue_list.iter().map(|(issue, identity)| { + li!(a!( + href = format!("/{}/issue/{}", repo_path.display(), issue.id()), + p!(span!( + class = "issue-subject", + to_markdown(issue.title(), true) + )), + span!( + class = "issue-number", + issue.id().as_id().shorten().to_string() + ), + " ", + display_issue_open(identity, issue.timeline()), + if !issue.comments().is_empty() { + span!( + class = "comment-count", + " - ", + issue.comments().len(), + " ", + { + if issue.comments().len() > 1 { + "comments" + } else { + "comment" + } + } + ) + } + )) + }) + ) + ), + footer!(nav!(a!( + href = config.source_code_repository_url.to_string(), + "Source Code" + ))), + ), + "issue list page", + None, + ) + .into_string()) } -pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result<String> { - let repository = config + +pub(crate) fn issue( + config: &BackConfig, + repo_path: &Path, + prefix: IdPrefix, +) -> error::Result<String> { + let replica = config .repositories()? .get(repo_path)? - .open(&config.scan_path)? - .to_thread_local(); + .open(&config.scan_path)?; - let maybe_issue = issues_from_repository(&repository)? - .into_iter() - .map(|val| val.collapse()) - .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); - - match maybe_issue { - Some(issue) => Ok(IssueTemplate { - issue, - repo_path: repo_path.display().to_string(), - source_code_repository_url: config.source_code_repository_url.clone(), - } - .render() - .expect("This should always work")), - None => Err(error::Error::IssuesPrefixMissing { prefix }), - } -} + let issue = replica + .get(prefix.resolve::<Issue>(replica.repo()).map_err(|err| { + error::Error::IssuesPrefixMissing { + prefix: Box::new(prefix), + err, + } + })?)? + .snapshot(); + + let identity = issue.author().resolve(&replica)?.snapshot(); -#[derive(Template)] -#[template(path = "./repos.html")] -struct ReposTemplate { - repos: Vec<RepoValue>, + Ok(make_page( + div!( + class = "content", + nav!( + a!( + href = format!("/{}/issues/?query=status:open", repo_path.display()), + "Open Issues" + ), + a!( + href = format!("/{}/issues/?query=status:closed", repo_path.display()), + "Closed Issues" + ), + ), + header!( + h1!(to_markdown(issue.title(), true)), + div!( + class = "issue-number", + issue.id().as_id().shorten().to_string() + ) + ), + main!( + div!( + class = "issue-info", + display_issue_open(&identity, issue.timeline()) + ), + to_markdown(issue.body(), false), + if !issue.comments().is_empty() { + ol!( + class = "issue-history", + issue + .comments() + .into_iter() + .map(|comment| { + Ok(li!( + class = "comment", + id = comment.combined_id.shorten().to_string(), + to_markdown(&comment.message, false), + p!( + class = "comment-info", + span!( + class = "user-name", + comment + .author + .resolve(&replica)? + .snapshot() + .name() + .to_owned(), + " at ", + comment.timestamp.to_string() + ) + ) + )) + }) + .collect::<error::Result<Vec<_>>>()? + ) + } + ), + footer!(nav!(a!( + href = config.source_code_repository_url.to_string(), + "Source code" + ),)) + ), + "issue detail page", + Some(format!("{} | Back", issue.title()).as_str()), + ) + .into_string()) +} - /// The URL to `back`'s source code - source_code_repository_url: Url, +fn display_issue_open( + identity: &Snapshot<Identity>, + issue_timeline: &IssueTimeline, +) -> impl IntoHtml { + span!( + class = "created-by-at", + "Opened by ", + span!(class = "user-name", identity.name()), + " ", + identity + .email() + .map(|email| span!(class = "user-email", "<", email, "> ",)), + "at ", + span!(class = "timestamp", { + { + issue_timeline + .history() + .last() + .expect("Exists") + .at() + .to_string() + } + }) + ) } + struct RepoValue { description: String, owner: String, path: String, } -pub fn repos(config: &BackConfig) -> error::Result<String> { + +pub(crate) fn repos(config: &BackConfig) -> error::Result<String> { let repos: Vec<RepoValue> = config .repositories()? .iter() .filter_map(|raw_repo| match raw_repo.open(&config.scan_path) { - Ok(repo) => { - let repo = repo.to_thread_local(); - let git_config = repo.config_snapshot(); + Ok(replica) => { + let git_config = replica.repo().config_snapshot(); let path = raw_repo.path().to_string_lossy().to_string(); let owner = git_config .string("cgit.owner") - .map(|v| v.to_string()) - .unwrap_or("<No owner>".to_owned()); + .map_or("<No owner>".to_owned(), |v| v.to_string()); - let description = fs::read_to_string(repo.git_dir().join("description")) + let description = fs::read_to_string(replica.repo().git_dir().join("description")) .unwrap_or("<No description>".to_owned()); Some(RepoValue { @@ -157,73 +366,124 @@ pub fn repos(config: &BackConfig) -> error::Result<String> { }) .collect(); - Ok(ReposTemplate { - repos, - source_code_repository_url: config.source_code_repository_url.clone(), - } - .render() - .expect("this should work")) + Ok(make_page( + div!( + class = "content", + header!(h1!("Repositiories")), + main!( + div!( + class = "issue-links", + // TODO(@bpeetz): Add a search. <2025-05-21> + ), + if !repos.is_empty() { + ol!( + class = "issue-list", + repos.into_iter().map(|repo| { + li!(a!( + href = format!("/{}/issues/?query=status:open", repo.path), + p!(span!(class = "issue-subject", repo.path)), + span!( + class = "created-by-at", + span!(class = "timestamp", repo.description), + " - ", + span!(class = "user-name", repo.owner) + ) + )) + }) + ) + } + ), + footer!(nav!(a!( + href = config.source_code_repository_url.to_string(), + "Source Code" + ))), + ), + "repository listing page", + Some("Repos | Back"), + ) + .into_string()) } -pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> { +pub(crate) fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> { use rss::{ChannelBuilder, Item, ItemBuilder}; - let repository = config + let replica = config .repositories()? .get(repo_path)? - .open(&config.scan_path)? - .to_thread_local(); + .open(&config.scan_path)?; - let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)? + let issues: Vec<Snapshot<Issue>> = replica + .get_all::<Issue>()? + .collect::<Result<Result<Vec<Issue>, _>, _>>()?? .into_iter() - .map(|issue| issue.collapse()) - .collect(); + .map(|i| i.snapshot()) + .collect::<Vec<_>>(); // Collect all Items as rss items let mut items: Vec<Item> = issues .iter() - .map(|issue| { - ItemBuilder::default() - .title(issue.title.to_string()) - .author(issue.author.to_string()) - .description(issue.message.to_string()) - .pub_date(issue.timestamp.to_string()) + .map(|issue| -> error::Result<_> { + Ok(ItemBuilder::default() + .title(issue.title().to_string()) + .author( + replica + .get(issue.author().id())? + .snapshot() + .name() + .to_owned(), + ) + .description(issue.body().to_string()) + .pub_date( + issue + .timeline() + .history() + .last() + .expect("Exists") + .at() + .to_string(), + ) .link(format!( "/{}/{}/issue/{}", repo_path.display(), &config.root_url, - issue.id + issue.id() )) - .build() + .build()) }) - .collect(); + .collect::<error::Result<Vec<Item>>>()?; // Append all comments after converting them to rss items items.extend( issues .iter() - .filter(|issue| !issue.comments.is_empty()) + .filter(|issue| !issue.comments().is_empty()) .flat_map(|issue| { issue - .comments + .comments() .iter() - .map(|comment| { - ItemBuilder::default() - .title(issue.title.to_string()) - .author(comment.author.to_string()) + .map(|comment| -> error::Result<_> { + Ok(ItemBuilder::default() + .title(issue.title().to_string()) + .author( + replica + .get(comment.author.id())? + .snapshot() + .name() + .to_owned(), + ) .description(comment.message.to_string()) .pub_date(comment.timestamp.to_string()) .link(format!( "/{}/{}/issue/{}", repo_path.display(), &config.root_url, - issue.id + comment.combined_id.shorten() )) - .build() + .build()) }) - .collect::<Vec<Item>>() + .collect::<Vec<_>>() }) - .collect::<Vec<Item>>(), + .collect::<error::Result<Vec<Item>>>()?, ); let channel = ChannelBuilder::default() diff --git a/src/web/generate/templates.rs b/src/web/generate/templates.rs new file mode 100644 index 0000000..693b862 --- /dev/null +++ b/src/web/generate/templates.rs @@ -0,0 +1,44 @@ +use vy::{DOCTYPE, IntoHtml, PreEscaped, body, div, head, html, link, meta, title}; + +pub(crate) fn make_page( + content: impl IntoHtml, + description: &str, + title: Option<&str>, +) -> impl IntoHtml { + ( + DOCTYPE, + html!( + lang = "en", + head!( + title!(title.unwrap_or("Back")), + link!(rel = "icon", href = "/favicon.ico"), + link!(rel = "stylesheet", "type" = "text/css", href = "/style.css"), + meta!(charset = "UTF-8"), + meta!( + name = "viewport", + content = "width=device-width,initial-scale=1" + ), + meta!(name = "description", content = description), + ), + body!(div!(class = "content", content)) + ), + ) +} + +pub(super) fn to_markdown(input: &str, is_title: bool) -> PreEscaped<String> { + let markdown = markdown::to_html(input.trim()); + + // If the markdown contains only one line line, assuming that it is a title is okay. + if input.lines().count() == 1 && markdown.starts_with("<p>") && is_title { + PreEscaped( + markdown + .strip_prefix("<p>") + .expect("We checked") + .strip_suffix("</p>") + .expect("markdown crate produces no invalid html") + .to_owned(), + ) + } else { + PreEscaped(markdown) + } +} diff --git a/src/web/mod.rs b/src/web/mod.rs index 8e2e9b0..0f9835a 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -9,21 +9,28 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/agpl.txt>. -use std::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc}; +use std::{cell::OnceCell, convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc}; use bytes::Bytes; +use git_bug::{ + entities::issue::{Issue, data::status::Status, query::MatchKeyValue}, + query::{Matcher, ParseMode, Query}, + replica::entity::{id::prefix::IdPrefix, snapshot::Snapshot}, +}; use http_body_util::combinators::BoxBody; use hyper::{Method, Request, Response, StatusCode, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; -use log::{error, info}; -use responses::{html_response, html_response_status, html_response_status_content_type}; +use log::{error, info, warn}; +use responses::{html_response, html_response_status, html_response_status_content_type, redirect}; use tokio::net::TcpListener; +use url::Url; -use crate::{config::BackConfig, error, git_bug::issue::Status}; +use crate::{config::BackConfig, error}; mod generate; mod responses; +#[allow(clippy::unused_async, clippy::too_many_lines)] async fn match_uri( config: Arc<BackConfig>, req: Request<hyper::body::Incoming>, @@ -36,36 +43,69 @@ async fn match_uri( } let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> { - match req.uri().path().trim_end_matches("/") { + match req.uri().path().trim_matches('/') { "" => Ok(html_response(generate::repos(&config)?)), - "/style.css" => Ok(responses::html_response_status_content_type( + "style.css" => Ok(html_response_status_content_type( include_str!("../../assets/style.css"), StatusCode::OK, "text/css", )), + "search.png" => Ok(html_response_status_content_type( + &include_bytes!("../../assets/search.png")[..], + StatusCode::OK, + "image/png", + )), - path if path.ends_with("/issues/open") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/open") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); - - let issues = generate::issues(&config, Status::Open, Status::Closed, &repo_path)?; - Ok(html_response(issues)) - } - path if path.ends_with("/issues/closed") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/closed") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); + path if path.ends_with("/issues") => { + let repo_path = + PathBuf::from(path.strip_suffix("/issues").expect("This suffix exists")); + + let replica = config + .repositories()? + .get(&repo_path)? + .open(&config.scan_path)?; + + let query = { + // HACK(@bpeetz): We cannot use the uri directly, because that would require us + // to parse the query string. As such, we need to turn into into a url::Url, + // which provides this. Unfortunately, this re-parsing is the “officially” + // sanctioned way of doing this. <2025-05-28> + let url: Url = format!("https://{}{}", config.root_url, req.uri()) + .parse() + .expect("Was a url before"); + + let mut query = OnceCell::new(); + for (name, value) in url.query_pairs() { + if name == "query" && query.get().is_none() { + let val = + Query::from_continuous_str(&replica, &value, ParseMode::Relaxed) + .map_err(|err| error::Error::InvalidQuery { + err, + query: value.to_string(), + })?; + query.set(val).expect("We checked for initialized"); + } else { + warn!("Unknown url query key: {name}"); + } + } + + query.take() + }; - let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?; - Ok(html_response(issues)) + if let Some(query) = query { + let issues = generate::issues(&config, &repo_path, &replica, &query)?; + Ok(html_response(issues)) + } else { + let query = Query::<Snapshot<Issue>>::from_matcher(Matcher::Match { + key_value: MatchKeyValue::Status(Status::Open), + }); + Ok(redirect(&format!( + "/{}/issues/?query={}", + repo_path.display(), + query + ))) + } } path if path.ends_with("/issues/feed") => { let repo_path = PathBuf::from( @@ -87,22 +127,30 @@ async fn match_uri( let (repo_path, prefix) = { let split: Vec<&str> = path.split("/issue/").collect(); - let prefix = - gix::hash::Prefix::from_hex(split[1]).map_err(error::Error::from)?; + let prefix = IdPrefix::from_hex_bytes(split[1].as_bytes()).map_err(|err| { + error::Error::IssuesPrefixParse { + prefix: split[1].to_owned(), + error: err, + } + })?; - let repo_path = - PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists")); + let repo_path = PathBuf::from(split[0]); (repo_path, prefix) }; Ok(html_response(generate::issue(&config, &repo_path, prefix)?)) } - other => Ok(responses::html_response_status_content_type( - format!("'{}' not found", other), - StatusCode::NOT_FOUND, - "text/plain", - )), + other if config.repositories()?.get(&PathBuf::from(other)).is_ok() => { + Ok(redirect(&format!("/{other}/issues/?query=status:open"))) + } + other if config.repositories()?.get(&PathBuf::from(other)).is_err() => { + Err(error::Error::NotGitBug { + path: PathBuf::from(other), + }) + } + + other => Err(error::Error::NotFound(PathBuf::from(other))), } }; match output() { @@ -111,13 +159,14 @@ async fn match_uri( } } -pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { +pub(crate) async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { let addr: SocketAddr = ([127, 0, 0, 1], 8000).into(); let listener = TcpListener::bind(addr) .await .map_err(|err| error::Error::TcpBind { addr, err })?; - info!("Listening on http://{}", addr); + info!("Listening on http://{addr}"); + loop { let (stream, _) = listener .accept() @@ -131,7 +180,7 @@ pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, service).await { - error!("Error serving connection: {:?}", err); + error!("Error serving connection: {err:?}"); } }); } diff --git a/src/web/responses.rs b/src/web/responses.rs index bcdcc0a..00af35a 100644 --- a/src/web/responses.rs +++ b/src/web/responses.rs @@ -12,10 +12,13 @@ use std::convert::Infallible; use bytes::Bytes; +use git_bug::replica::entity::id::prefix::IdPrefix; use http::{Response, StatusCode, Version}; use http_body_util::{BodyExt, Full, combinators::BoxBody}; +use vy::{IntoHtml, h1, p, pre}; -use crate::{error, git_bug::format::HtmlString}; +use super::generate::templates::make_page; +use crate::error; pub(super) fn html_response<T: Into<Bytes>>(html_text: T) -> Response<BoxBody<Bytes, Infallible>> { html_response_status(html_text, StatusCode::OK) @@ -28,6 +31,15 @@ pub(super) fn html_response_status<T: Into<Bytes>>( html_response_status_content_type(html_text, status, "text/html") } +pub(super) fn redirect(target: &str) -> Response<BoxBody<Bytes, Infallible>> { + Response::builder() + .status(StatusCode::MOVED_PERMANENTLY) + .version(Version::HTTP_2) + .header("Location", target) + .body(full("")) + .expect("This is hardcoded and will build") +} + pub(super) fn html_response_status_content_type<T: Into<Bytes>>( html_text: T, status: StatusCode, @@ -36,26 +48,129 @@ pub(super) fn html_response_status_content_type<T: Into<Bytes>>( Response::builder() .status(status) .version(Version::HTTP_2) - .header("Content-Type", format!("{}; charset=utf-8", content_type)) + .header("Content-Type", format!("{content_type}; charset=utf-8")) .header("x-content-type-options", "nosniff") .header("x-frame-options", "SAMEORIGIN") .body(full(html_text)) .expect("This will always build") } -fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> { +pub(super) fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> { Full::new(chunk.into()).boxed() } -// FIXME: Not all errors should return `INTERNAL_SERVER_ERROR`. <2025-03-08> impl error::Error { + #[allow(clippy::too_many_lines)] pub fn into_response(self) -> Response<BoxBody<Bytes, Infallible>> { - html_response_status( - format!( - "<h1> Internal server error. </h1> <pre>Error: {}</pre>", - HtmlString::from(self.to_string()) + match self { + error::Error::ConfigParse { .. } + | error::Error::ProjectListRead { .. } + | error::Error::ConfigRead { .. } + | error::Error::RepoGetReferences(_) + | error::Error::RepoIssueRead(_) + | error::Error::RepoIdentityRead(_) + | error::Error::RepoFind { .. } + | error::Error::RepoRefsIter(_) + | error::Error::RepoRefsPrefixed { .. } + | error::Error::TcpBind { .. } + | error::Error::RepoOpen { .. } + | error::Error::TcpAccept { .. } => html_response_status( + make_page( + ( + h1!("Internal server error"), + pre!(format!("Error {}", self.to_string())), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ), + error::Error::NotGitBug { path } => html_response_status( + make_page( + ( + h1!("Expectation Failed"), + p!( + "Repository '", + path.display().to_string(), + "' has no git bug data to show." + ), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::EXPECTATION_FAILED, + ), + error::Error::IssuesPrefixMissing { prefix, err } => html_response_status( + make_page( + ( + h1!("Expectation Failed"), + p!( + "There is no issue associated with the prefix ", + "'", + prefix.to_string(), + "': ", + err.to_string() + ), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::EXPECTATION_FAILED, + ), + error::Error::IssuesPrefixParse { prefix, error } => html_response_status( + make_page( + ( + h1!("Expectation Failed"), + p!( + "The prefix '", + prefix, + "' cannot be interperted as a prefix", + ), + p!(error.to_string()), + p!( + "The prefix is composed of ", + IdPrefix::REQUIRED_LENGTH, + " or more hex chars." + ), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::EXPECTATION_FAILED, + ), + error::Error::NotFound(path_buf) => html_response_status( + make_page( + ( + h1!("Expectation Failed"), + p!( + "The path '", + path_buf.display().to_string(), + "' was unkonwn", + ), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::EXPECTATION_FAILED, + ), + error::Error::InvalidQuery { err, query } => html_response_status( + make_page( + ( + h1!("Expectation Failed"), + p!("The query '", query, "' was invalid",), + p!(err.to_string()), + ), + "Error page", + Some("Error | Back"), + ) + .into_string(), + StatusCode::EXPECTATION_FAILED, ), - StatusCode::INTERNAL_SERVER_ERROR, - ) + } } } diff --git a/templates/issue.html b/templates/issue.html deleted file mode 100644 index c7e4efc..0000000 --- a/templates/issue.html +++ /dev/null @@ -1,74 +0,0 @@ -<!-- -Back - An extremely simple git bug visualization system. Inspired by TVL's -panettone. - -Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -SPDX-License-Identifier: AGPL-3.0-or-later - -This file is part of Back. - -You should have received a copy of the License along with this program. -If not, see <https://www.gnu.org/licenses/agpl.txt>. ---> - -<!doctype html> -<html lang="en"> - <head> - <title>{{ HtmlString::from(issue.title.clone()) }} | Back</title> - <link - href="/style.css" - rel="stylesheet" - type="text/css" - /> - <meta - content="width=device-width,initial-scale=1" - name="viewport" - /> - </head> - <body> - <div class="content"> - <nav> - <a href="/{{repo_path}}/issues/open">Open Issues</a> - <a href="/{{repo_path}}/issues/closed">Closed Issues</a> - </nav> - <header> - <h1>{{issue.title|safe}}</h1> - <div class="issue-number">{{issue.id}}</div> - </header> - <main> - <div class="issue-info"> - <span class="created-by-at" - >Opened by - <span class="user-name">{{issue.author.name|safe}}</span> - <span class="user-email"><{{issue.author.email|safe}}></span> - at <span class="timestamp">{{issue.timestamp}}</span></span - > - </div> - {{issue.message|safe}} {% if !issue.comments.is_empty() %} - <ol class="issue-history"> - {% for comment in issue.comments %} - <li - class="comment" - id="{{comment.id}}" - > - {{comment.message|safe}} - <p class="comment-info"> - <span class="user-name" - >{{comment.author.name|safe}} at {{comment.timestamp}}</span - > - </p> - </li> - {% endfor %} - </ol> - {% endif %} - </main> - <footer> - <nav> - <a href="/{{repo_path}}/issues/open">Open Issues</a> - <a href="{{source_code_repository_url}}">Source code</a> - <a href="/{{repo_path}}/issues/closed">Closed Issues</a> - </nav> - </footer> - </div> - </body> -</html> diff --git a/templates/issues.html b/templates/issues.html deleted file mode 100644 index 564827c..0000000 --- a/templates/issues.html +++ /dev/null @@ -1,77 +0,0 @@ -<!-- -Back - An extremely simple git bug visualization system. Inspired by TVL's -panettone. - -Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -SPDX-License-Identifier: AGPL-3.0-or-later - -This file is part of Back. - -You should have received a copy of the License along with this program. -If not, see <https://www.gnu.org/licenses/agpl.txt>. ---> - -<!doctype html> -<html lang="en"> - <head> - <title>Back</title> - <link - href="/style.css" - rel="stylesheet" - type="text/css" - /> - <meta - content="width=device-width,initial-scale=1" - name="viewport" - /> - </head> - <body> - <div class="content"> - <header> - <h1>{{wanted_status}} Issues</h1> - </header> - <main> - <div class="issue-links"> - <a href="/{{repo_path}}/issues/{{counter_status|lowercase}}/" - >View {{counter_status}} issues</a - > - <a href="{{source_code_repository_url}}">Source code</a> - <!-- - <form class="issue-search" method="get"> - <input name="search" title="Issue search query" type="search"> - <input class="sr-only" type="submit" value="Search Issues"> - </form> - --> - </div> - <ol class="issue-list"> - {% for issue in issues -%} - <li> - <a href="/{{repo_path}}/issue/{{issue.id}}"> - <p> - <span class="issue-subject">{{issue.title|safe}}</span> - </p> - <span class="issue-number">{{issue.id}}</span> - <span class="created-by-at" - >Opened by {{ " " }} - <span class="user-name">{{issue.author.name|safe}}</span> - {{ " " }} - <span class="user-email" - ><{{issue.author.email|safe}}></span - > - {{ "at" }} - <span class="timestamp">{{issue.timestamp}}</span> - </span> - {% if !issue.comments.is_empty() +%} - <span class="comment-count"> - - {{issue.comments.len()}} - comment{{issue.comments.len()|pluralize}}</span - > - {%+ endif %} - </a> - </li> - {%- endfor %} - </ol> - </main> - </div> - </body> -</html> diff --git a/templates/repos.html b/templates/repos.html deleted file mode 100644 index 8aa71c4..0000000 --- a/templates/repos.html +++ /dev/null @@ -1,62 +0,0 @@ -<!-- -Back - An extremely simple git bug visualization system. Inspired by TVL's -panettone. - -Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -SPDX-License-Identifier: AGPL-3.0-or-later - -This file is part of Back. - -You should have received a copy of the License along with this program. -If not, see <https://www.gnu.org/licenses/agpl.txt>. ---> - -<!doctype html> -<html lang="en"> - <head> - <title>Back</title> - <link - href="/style.css" - rel="stylesheet" - type="text/css" - /> - <meta - content="width=device-width,initial-scale=1" - name="viewport" - /> - </head> - <body> - <div class="content"> - <header> - <h1>Repositories</h1> - </header> - <main> - <div class="issue-links"> - <a href="{{source_code_repository_url}}">Source code</a> - <!-- - <form class="issue-search" method="get"> - <input name="search" title="Issue search query" type="search"> - <input class="sr-only" type="submit" value="Search Issues"> - </form> - --> - </div> - <ol class="issue-list"> - {% for repo in repos -%} - <li> - <a href="/{{repo.path}}/issues/open"> - <p> - <span class="issue-subject">{{repo.path}}</span> - </p> - <span class="created-by-at"> - <span class="timestamp">{{repo.description}}</span> - {{ "-" }} - <span class="user-name">{{repo.owner}}</span> - </span> - </a> - </li> - {%- endfor %} - </ol> - </main> - </div> - </body> -</html> diff --git a/treefmt.nix b/treefmt.nix index ebe4157..1768b65 100644 --- a/treefmt.nix +++ b/treefmt.nix @@ -11,7 +11,6 @@ { treefmt-nix, pkgs, - rustfmt, }: treefmt-nix.lib.evalModule pkgs ( {pkgs, ...}: { @@ -22,7 +21,6 @@ treefmt-nix.lib.evalModule pkgs ( alejandra.enable = true; rustfmt = { enable = true; - package = rustfmt; edition = "2024"; }; clang-format.enable = true; @@ -76,8 +74,23 @@ treefmt-nix.lib.evalModule pkgs ( clang-format = { options = ["--style" "GNU"]; }; - rustfmt = { - options = ["--config-path" "${./rustfmt.toml}"]; + rustfmt = let + config = builtins.fromTOML (builtins.readFile ./rustfmt_config.toml); + toValue = value: + if builtins.isString value + then value + else if builtins.isInt value + then builtins.toString value + else if builtins.isBool value + then + if value + then "true" + else "false" + else builtins.throw "Unknown value: ${value}"; + options = pkgs.lib.mapAttrsToList (name: value: "--config=${name}=${toValue value}") config; + in { + inherit options; + excludes = ["vendored/*"]; }; shfmt = { includes = ["*.bash"]; |