about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-05-12 12:39:10 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-05-12 12:39:10 +0200
commit1e4dff1995833538f436b381bc0450a7c0080bad (patch)
tree2dc620ac9ea683cbee412b8d5818b3992462677c
downloadback-1e4dff1995833538f436b381bc0450a7c0080bad.zip
chore: Initial commit
Based on the version that was previously in `vhack.eu/nixos-server/pkgs/by-name/ba/back`.
-rw-r--r--.editorconfig27
-rw-r--r--.envrc22
-rw-r--r--.gitignore17
-rw-r--r--.reuse/templates/default.jinja230
-rw-r--r--Cargo.lock3044
-rw-r--r--Cargo.toml104
-rw-r--r--README.md82
-rw-r--r--assets/style.css383
-rw-r--r--cog.toml36
-rw-r--r--contrib/config.json6
-rw-r--r--contrib/config.json.license11
-rw-r--r--flake.lock139
-rw-r--r--flake.lock.license10
-rw-r--r--flake.nix136
-rw-r--r--nix/module.nix102
-rw-r--r--nix/package.nix38
-rw-r--r--rustfmt.toml79
-rwxr-xr-xscripts/optimize_images.sh136
-rw-r--r--src/cli.rs25
-rw-r--r--src/config/mod.rs137
-rw-r--r--src/error/mod.rs135
-rw-r--r--src/main.rs54
-rw-r--r--src/web/generate/mod.rs236
-rw-r--r--src/web/mod.rs138
-rw-r--r--src/web/responses.rs61
-rw-r--r--templates/issue.html74
-rw-r--r--templates/issues.html77
-rw-r--r--templates/repos.html62
-rw-r--r--tests/base.nix216
-rw-r--r--treefmt.nix88
-rwxr-xr-xupdate.sh17
31 files changed, 5722 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..74297ea
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,27 @@
+# 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>.
+
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# Matches multiple files with brace expansion notation
+# Set default charset
+[*.{nix}]
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..bd488c8
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,22 @@
+#!/usr/bin/env sh
+
+# Back - An extremely simple git bug visualization system. Inspired by TVL's
+# panettone.
+#
+# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# 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 flake || use nix
+watch_file flake.nix
+
+PATH_add ./scripts
+
+if on_git_branch; then
+    echo && git status --short --branch
+fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..702b917
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+# 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>.
+
+# build
+/target
+/result
+
+# dev env
+.direnv
diff --git a/.reuse/templates/default.jinja2 b/.reuse/templates/default.jinja2
new file mode 100644
index 0000000..81706dc
--- /dev/null
+++ b/.reuse/templates/default.jinja2
@@ -0,0 +1,30 @@
+{#
+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>.
+#}
+
+Back - An extremely simple git bug visualization system. Inspired by TVL's
+panettone.
+
+{% for copyright_line in copyright_lines %}
+{{ copyright_line }}
+{% endfor %}
+{% for contributor_line in contributor_lines %}
+SPDX-FileContributor: {{ contributor_line }}
+{% endfor %}
+{% for expression in spdx_expressions %}
+SPDX-License-Identifier: {{ expression }}
+{% endfor %}
+
+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>.
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..61a5656
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3044 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+dependencies = [
+ "anstyle",
+ "once_cell",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "atom_syndication"
+version = "0.12.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3"
+dependencies = [
+ "chrono",
+ "derive_builder",
+ "diligent-date-parser",
+ "never",
+ "quick-xml",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "back"
+version = "0.1.0"
+dependencies = [
+ "bytes",
+ "chrono",
+ "clap",
+ "gix",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "markdown",
+ "rinja",
+ "rss",
+ "serde",
+ "serde_json",
+ "sha2",
+ "stderrlog",
+ "thiserror",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "bytesize"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
+
+[[package]]
+name = "cc"
+version = "1.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "clru"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "diligent-date-parser"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9"
+dependencies = [
+ "chrono",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "faster-hex"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
+dependencies = [
+ "heapless",
+ "serde",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
+dependencies = [
+ "crc32fast",
+ "libz-rs-sys",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "gix"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01237e8d3d78581f71642be8b0c2ae8c0b2b5c251c9c5d9ebbea3c1ea280dce8"
+dependencies = [
+ "gix-actor",
+ "gix-archive",
+ "gix-attributes",
+ "gix-command",
+ "gix-commitgraph",
+ "gix-config",
+ "gix-credentials",
+ "gix-date",
+ "gix-diff",
+ "gix-dir",
+ "gix-discover",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-ignore",
+ "gix-index",
+ "gix-lock",
+ "gix-mailmap",
+ "gix-negotiate",
+ "gix-object",
+ "gix-odb",
+ "gix-pack",
+ "gix-path",
+ "gix-pathspec",
+ "gix-prompt",
+ "gix-protocol",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-sec",
+ "gix-shallow",
+ "gix-status",
+ "gix-submodule",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-url",
+ "gix-utils",
+ "gix-validate",
+ "gix-worktree",
+ "gix-worktree-state",
+ "gix-worktree-stream",
+ "once_cell",
+ "parking_lot",
+ "regex",
+ "signal-hook",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-actor"
+version = "0.35.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b300e6e4f31f3f6bd2de5e2b0caab192ced00dc0fcd0f7cc56e28c575c8e1ff"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-utils",
+ "itoa",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-archive"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e59cc5867c40065122324265d99416ca0ddeb51d007870868b5f835423fb6c"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-object",
+ "gix-worktree-stream",
+ "jiff",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-attributes"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e26b3ac280ddb25bb6980d34f4a82ee326f78bf2c6d4ea45eef2d940048b8e"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "kstring",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-bitmap"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "gix-chunk"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "gix-command"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f47f3fb4ba33644061e8e0e1030ef2a937d42dc969553118c320a205a9fb28"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "shell-words",
+]
+
+[[package]]
+name = "gix-commitgraph"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05050fd6caa6c731fe3bd7f9485b3b520be062d3d139cb2626e052d6c127951"
+dependencies = [
+ "bstr",
+ "gix-chunk",
+ "gix-hash",
+ "memmap2",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-config"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f3c8f357ae049bfb77493c2ec9010f58cfc924ae485e1116c3718fc0f0d881"
+dependencies = [
+ "bstr",
+ "gix-config-value",
+ "gix-features",
+ "gix-glob",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+ "winnow",
+]
+
+[[package]]
+name = "gix-config-value"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439d62e241dae2dffd55bfeeabe551275cf9d9f084c5ebc6b48bad49d03285b7"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-path",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-credentials"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1c7307e36026b6088e5b12014ffe6d4f509c911ee453e22a7be4003a159c9b"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-config-value",
+ "gix-path",
+ "gix-prompt",
+ "gix-sec",
+ "gix-trace",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-date"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139d1d52b21741e3f0c72b0fc65e1ff34d4eaceb100ef529d182725d2e09b8cb"
+dependencies = [
+ "bstr",
+ "itoa",
+ "jiff",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-diff"
+version = "0.52.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e9b43e95fe352da82a969f0c84ff860c2de3e724d93f6681fedbcd6c917f252"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-worktree",
+ "imara-diff",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-dir"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01e6e2dc5b8917142d0ffe272209d1671e45b771e433f90186bc71c016792e87"
+dependencies = [
+ "bstr",
+ "gix-discover",
+ "gix-fs",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-trace",
+ "gix-utils",
+ "gix-worktree",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-discover"
+version = "0.40.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dccfe3e25b4ea46083916c56db3ba9d1e6ef6dce54da485f0463f9fc0fe1837c"
+dependencies = [
+ "bstr",
+ "dunce",
+ "gix-fs",
+ "gix-hash",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-features"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed"
+dependencies = [
+ "bytes",
+ "bytesize",
+ "crc32fast",
+ "crossbeam-channel",
+ "flate2",
+ "gix-path",
+ "gix-trace",
+ "gix-utils",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "prodash",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "gix-filter"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90c21f0d61778f518bbb7c431b00247bf4534b2153c3e85bcf383876c55ca6c"
+dependencies = [
+ "bstr",
+ "encoding_rs",
+ "gix-attributes",
+ "gix-command",
+ "gix-hash",
+ "gix-object",
+ "gix-packetline-blocking",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "gix-utils",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-fs"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7"
+dependencies = [
+ "bstr",
+ "fastrand",
+ "gix-features",
+ "gix-path",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-glob"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2926b03666e83b8d01c10cf06e5733521aacbd2d97179a4c9b1fdddabb9e937d"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-features",
+ "gix-path",
+]
+
+[[package]]
+name = "gix-hash"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8"
+dependencies = [
+ "faster-hex",
+ "gix-features",
+ "sha1-checked",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-hashtable"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904"
+dependencies = [
+ "gix-hash",
+ "hashbrown 0.14.5",
+ "parking_lot",
+]
+
+[[package]]
+name = "gix-ignore"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae358c3c96660b10abc7da63c06788dfded603e717edbd19e38c6477911b71c8"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-trace",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-index"
+version = "0.40.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b38e919efd59cb8275d23ad2394b2ab9d002007b27620e145d866d546403b665"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "filetime",
+ "fnv",
+ "gix-bitmap",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-traverse",
+ "gix-utils",
+ "gix-validate",
+ "hashbrown 0.14.5",
+ "itoa",
+ "libc",
+ "memmap2",
+ "rustix",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-lock"
+version = "17.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796"
+dependencies = [
+ "gix-tempfile",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-mailmap"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e7c52eb13d84ad26030d07a2c2975ba639dd1400a7996e6966c5aef617ed829"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-negotiate"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e1ea901acc4d5b44553132a29e8697210cb0e739b2d9752d713072e9391e3c9"
+dependencies = [
+ "bitflags",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-object"
+version = "0.49.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d957ca3640c555d48bb27f8278c67169fa1380ed94f6452c5590742524c40fbb"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-path",
+ "gix-utils",
+ "gix-validate",
+ "itoa",
+ "smallvec",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-odb"
+version = "0.69.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "868f703905fdbcfc1bd750942f82419903ecb7039f5288adb5206d6de405e0c9"
+dependencies = [
+ "arc-swap",
+ "gix-date",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-pack",
+ "gix-path",
+ "gix-quote",
+ "parking_lot",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pack"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d49c55d69c8449f2a0a5a77eb9cbacfebb6b0e2f1215f0fc23a4cb60528a450"
+dependencies = [
+ "clru",
+ "gix-chunk",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-path",
+ "memmap2",
+ "smallvec",
+ "thiserror",
+ "uluru",
+]
+
+[[package]]
+name = "gix-packetline"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddc034bc67c848e4ef7596ab5528cd8fd439d310858dbe1ce8b324f25deb91c"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-packetline-blocking"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c44880f028ba46d6cf37a66d27a300310c6b51b8ed0e44918f93df061168e2f3"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-path"
+version = "0.10.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567f65fec4ef10dfab97ae71f26a27fd4d7fe7b8e3f90c8a58551c41ff3fb65b"
+dependencies = [
+ "bstr",
+ "gix-trace",
+ "gix-validate",
+ "home",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-pathspec"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce061c50e5f8f7c830cacb3da3e999ae935e283ce8522249f0ce2256d110979d"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-attributes",
+ "gix-config-value",
+ "gix-glob",
+ "gix-path",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-prompt"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d024a3fe3993bbc17733396d2cefb169c7a9d14b5b71dafb7f96e3962b7c3128"
+dependencies = [
+ "gix-command",
+ "gix-config-value",
+ "parking_lot",
+ "rustix",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-protocol"
+version = "0.50.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5c17d78bb0414f8d60b5f952196dc2e47ec320dca885de9128ecdb4a0e38401"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-ref",
+ "gix-shallow",
+ "gix-transport",
+ "gix-utils",
+ "maybe-async",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-quote"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd"
+dependencies = [
+ "bstr",
+ "gix-utils",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-ref"
+version = "0.52.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1b7985657029684d759f656b09abc3e2c73085596d5cdb494428823970a7762"
+dependencies = [
+ "gix-actor",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-utils",
+ "gix-validate",
+ "memmap2",
+ "thiserror",
+ "winnow",
+]
+
+[[package]]
+name = "gix-refspec"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445ed14e3db78e8e79980085e3723df94e1c8163b3ae5bc8ed6a8fe6cf983b42"
+dependencies = [
+ "bstr",
+ "gix-hash",
+ "gix-revision",
+ "gix-validate",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-revision"
+version = "0.34.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78d0b8e5cbd1c329e25383e088cb8f17439414021a643b30afa5146b71e3c65d"
+dependencies = [
+ "bitflags",
+ "bstr",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-revwalk"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc756b73225bf005ddeb871d1ca7b3c33e2417d0d53e56effa5a36765b52b28"
+dependencies = [
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-sec"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0dabbc78c759ecc006b970339394951b2c8e1e38a37b072c105b80b84c308fd"
+dependencies = [
+ "bitflags",
+ "gix-path",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "gix-shallow"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9a6f6e34d6ede08f522d89e5c7990b4f60524b8ae6ebf8e850963828119ad4"
+dependencies = [
+ "bstr",
+ "gix-hash",
+ "gix-lock",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-status"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "072099c2415cfa5397df7d47eacbcb6016d2cd17e0d674c74965e6ad1b17289f"
+dependencies = [
+ "bstr",
+ "filetime",
+ "gix-diff",
+ "gix-dir",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-worktree",
+ "portable-atomic",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-submodule"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f51472f05a450cc61bc91ed2f62fb06e31e2bbb31c420bc4be8793f26c8b0c1"
+dependencies = [
+ "bstr",
+ "gix-config",
+ "gix-path",
+ "gix-pathspec",
+ "gix-refspec",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-tempfile"
+version = "17.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa"
+dependencies = [
+ "dashmap",
+ "gix-fs",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-registry",
+ "tempfile",
+]
+
+[[package]]
+name = "gix-trace"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7"
+
+[[package]]
+name = "gix-transport"
+version = "0.47.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfe22ba26d4b65c17879f12b9882eafe65d3c8611c933b272fce2c10f546f59"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-features",
+ "gix-packetline",
+ "gix-quote",
+ "gix-sec",
+ "gix-url",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-traverse"
+version = "0.46.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8648172f85aca3d6e919c06504b7ac26baef54e04c55eb0100fa588c102cc33"
+dependencies = [
+ "bitflags",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-url"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42a1ad0b04a5718b5cb233e6888e52a9b627846296161d81dcc5eb9203ec84b8"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-path",
+ "percent-encoding",
+ "thiserror",
+ "url",
+]
+
+[[package]]
+name = "gix-utils"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5"
+dependencies = [
+ "bstr",
+ "fastrand",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "gix-validate"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d"
+dependencies = [
+ "bstr",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-worktree"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54f1916f8d928268300c977d773dd70a8746b646873b77add0a34876a8c847e9"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-features",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-validate",
+]
+
+[[package]]
+name = "gix-worktree-state"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f81e31496d034dbdac87535b0b9d4659dbbeabaae1045a0dce7c69b5d16ea7d6"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-worktree",
+ "io-close",
+ "thiserror",
+]
+
+[[package]]
+name = "gix-worktree-stream"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7731f9e7ffc45f1f79d6601a37169be0697d4e2bd015495b4f53dffd80b42e"
+dependencies = [
+ "gix-attributes",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-traverse",
+ "parking_lot",
+ "thiserror",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "human_format"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "imara-diff"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
+dependencies = [
+ "hashbrown 0.15.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.3",
+]
+
+[[package]]
+name = "io-close"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jiff"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
+dependencies = [
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags",
+ "libc",
+ "redox_syscall",
+]
+
+[[package]]
+name = "libz-rs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
+dependencies = [
+ "zlib-rs",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "markdown"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
+dependencies = [
+ "unicode-id",
+]
+
+[[package]]
+name = "maybe-async"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "never"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prodash"
+version = "29.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc"
+dependencies = [
+ "bytesize",
+ "human_format",
+ "log",
+ "parking_lot",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "encoding_rs",
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+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"
+checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf"
+dependencies = [
+ "atom_syndication",
+ "derive_builder",
+ "never",
+ "quick-xml",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha1-checked"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423"
+dependencies = [
+ "digest",
+ "sha1",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+[[package]]
+name = "socket2"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "stderrlog"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
+dependencies = [
+ "chrono",
+ "is-terminal",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "uluru"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "unicode-bom"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
+
+[[package]]
+name = "unicode-id"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
+[[package]]
+name = "windows-result"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a898572
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,104 @@
+# 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>.
+
+[package]
+name = "back"
+description = "An extremely simple git bug visualization system. Inspired by TVL's panettone."
+version = "0.1.0"
+edition = "2021"
+license = "AGPL-3.0-or-later"
+homepage = ""
+repository = "https://git.foss-syndicate.org/vhack.eu/git_bug/back"
+# TODO
+# categories = [""]
+# keywords = ["", ""]
+
+
+[dependencies]
+bytes = "1.10.1"
+chrono = "0.4.41"
+clap = { version = "4.5.38", features = ["derive"] }
+gix = "0.72.1"
+http = "1.3.1"
+http-body-util = "0.1.3"
+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"] }
+
+[profile.release]
+lto = true
+codegen-units = 1
+panic = "abort"
+split-debuginfo = "off"
+
+[lints.rust]
+# rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html
+warnings = "warn"
+future_incompatible = { level = "warn", priority = -1 }
+let_underscore = { level = "warn", priority = -1 }
+nonstandard_style = { level = "warn", priority = -1 }
+rust_2018_compatibility = { level = "warn", priority = -1 }
+rust_2018_idioms = { level = "warn", priority = -1 }
+rust_2021_compatibility = { level = "warn", priority = -1 }
+unused = { level = "warn", priority = -1 }
+
+# rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
+# missing_docs = "warn"
+macro_use_extern_crate = "warn"
+meta_variable_misuse = "warn"
+missing_abi = "warn"
+missing_copy_implementations = "warn"
+missing_debug_implementations = "warn"
+non_ascii_idents = "warn"
+noop_method_call = "warn"
+single_use_lifetimes = "warn"
+trivial_casts = "warn"
+trivial_numeric_casts = "warn"
+unreachable_pub = "warn"
+unsafe_op_in_unsafe_fn = "warn"
+unused_crate_dependencies = "warn"
+unused_import_braces = "warn"
+unused_lifetimes = "warn"
+unused_qualifications = "warn"
+variant_size_differences = "warn"
+
+[lints.rustdoc]
+# rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html
+broken_intra_doc_links = "warn"
+private_intra_doc_links = "warn"
+missing_crate_level_docs = "warn"
+private_doc_tests = "warn"
+invalid_codeblock_attributes = "warn"
+invalid_rust_codeblocks = "warn"
+bare_urls = "warn"
+
+[lints.clippy]
+# clippy allowed by default
+dbg_macro = "warn"
+
+# clippy categories https://doc.rust-lang.org/clippy/
+all = { level = "warn", priority = -1 }
+correctness = { level = "warn", priority = -1 }
+suspicious = { level = "warn", priority = -1 }
+style = { level = "warn", priority = -1 }
+complexity = { level = "warn", priority = -1 }
+perf = { level = "warn", priority = -1 }
+pedantic = { level = "warn", priority = -1 }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..afb70a1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,82 @@
+<!--
+Back - An extremely simple git bug visualization system. Inspired by TVL's
+panettone.
+
+Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+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>.
+-->
+
+# Back
+
+> An extremely simple git issue tracking system. Inspired by tvix's panettone.
+
+## Usage
+
+`back` is modelled after `cgit`, only for `git-bug` initialized repositories.
+The server is than started at `http://127.0.0.1:8000` and provides access to the
+issues (bugs) tracked via `git-bug`, via multiple routes:
+
+### `/`
+
+The default index is a list of all repositories that have `git-bug` data in
+them.
+
+### `<repo_path>/issues/<state>`
+
+This path displays all issues in `<state>` (i.e., open or closed) for the
+repository at `<repo_path>`.
+
+### `<repo_path>/issue/<issue_id>`
+
+Displays the actual issue with `id` `<issue_id>`. Beware, that the `<isuse_id>`
+is sourced from the actual git object associated with the issue create commit.
+As such, it is not the same ID, as displayed by the `git-bug` CLI.
+
+### `<repo_path>/issues/feed`
+
+An RSS feed usable to subscribe to. This includes all issues and all comments of
+issues.
+
+## Configuration file
+
+The config file is passed to `back` via the first command line argument. It is
+written in JSON. An example configuration file is available at
+[`./contrib/config.json`](./contrib/config.json).
+
+Following keys are required:
+
+### `source_code_repository_url`
+
+The URL to the source code of this instance of `back`.
+
+### `root_url`
+
+The root URL this instance of `back` is hosted at. For example:
+`https://issues.foss-syndicate.org`. This is required by the RSS feed to
+generate links to the various issues/comments.
+
+### `scan_path`
+
+The path under which to search for the repositories as specified by the
+`projects.list` file. This is semantically the same as `cgit`'s `scan-path`.
+
+### `project_list`
+
+The path to the file specifying the repositories to search. A repository path
+per line. This is semantically the same as `cgit`'s `project-list`.
+
+### Note
+
+`back` needs write access to the repository, because of internal `gix` and `git`
+object reasons.
+
+## Licensing
+
+This project complies with the REUSE v3.3 specification. This means that every
+file clearly states its copyright.
diff --git a/assets/style.css b/assets/style.css
new file mode 100644
index 0000000..e7ce376
--- /dev/null
+++ b/assets/style.css
@@ -0,0 +1,383 @@
+/*
+ * Back - An extremely simple git bug visualization system. Inspired by TVL's
+ * panettone.
+ *
+ * Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+ * 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>.
+ */
+
+/*
+* This has been taken from the tvix depot from panettone.
+* Fetched via `suckit https://b.tvlfyi`.
+* It was originally licensed under the MIT license.
+*/
+
+input[type="text"],
+input[type="password"],
+textarea {
+  width: 100%;
+  padding: 0.5rem;
+  outline: none;
+  border-top: none;
+  border-left: none;
+  border-right: none;
+  border-bottom: 1px solid var(--gray);
+  margin-bottom: 1rem;
+}
+
+textarea {
+  resize: vertical;
+}
+
+input[type="submit"] {
+  -webkit-appearance: none;
+  border: none;
+  cursor: pointer;
+  font-size: 1rem;
+}
+
+input[type="submit"] {
+  background-color: var(--success);
+  padding: 0.5rem;
+  text-decoration: none;
+  -moz-transition: box-shadow 0.15s ease-in-out;
+  -o-transition: box-shadow 0.15s ease-in-out;
+  -webkit-transition: box-shadow 0.15s ease-in-out;
+  -ms-transition: box-shadow 0.15s ease-in-out;
+  transition: box-shadow 0.15s ease-in-out;
+}
+
+input[type="submit"]:hover {
+  -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+}
+
+input[type="submit"]:active,
+input[type="submit"]:focus {
+  -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  outline: none;
+  border: none;
+}
+
+.form-link input[type="submit"] {
+  background-color: initial;
+  color: inherit;
+  padding: 0;
+  text-decoration: underline;
+}
+
+.form-link input[type="submit"]:hover,
+.form-link input[type="submit"]:active,
+.form-link input[type="submit"]:focus {
+  -moz-box-shadow: 0 0 0 0;
+  -o-box-shadow: 0 0 0 0;
+  -webkit-box-shadow: 0 0 0 0;
+  -ms-box-shadow: 0 0 0 0;
+  box-shadow: 0 0 0 0;
+}
+
+.form-group {
+  margin-top: 1rem;
+}
+
+label.checkbox {
+  cursor: pointer;
+}
+
+.issue-list {
+  list-style-type: none;
+  padding-left: 0;
+}
+
+.issue-list .issue-subject {
+  font-weight: bold;
+}
+
+.issue-list li {
+  padding-bottom: 1rem;
+}
+
+.issue-list li + li {
+  border-top: 1px solid var(--gray);
+}
+
+.issue-list a {
+  text-decoration: none;
+  display: block;
+}
+
+.issue-list a:hover {
+  outline: none;
+}
+
+.issue-list a:hover .issue-subject {
+  color: var(--primary);
+}
+
+.comment-count {
+  color: var(--gray);
+}
+
+.issue-links {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+
+.issue-search input[type="search"] {
+  padding: 0.5rem;
+  background-image: url("static/search.png");
+  background-position: 10px 10px;
+  background-repeat: no-repeat;
+  background-size: 1rem;
+  padding-left: 2rem;
+  border: 1px solid var(--gray);
+}
+
+.issue-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.issue-info .edit-issue {
+  background-color: var(--success);
+  padding: 0.5rem;
+  text-decoration: none;
+  -moz-transition: box-shadow 0.15s ease-in-out;
+  -o-transition: box-shadow 0.15s ease-in-out;
+  -webkit-transition: box-shadow 0.15s ease-in-out;
+  -ms-transition: box-shadow 0.15s ease-in-out;
+  transition: box-shadow 0.15s ease-in-out;
+}
+
+.issue-info .edit-issue:hover {
+  -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+}
+
+.issue-info .edit-issue:active,
+.issue-info .edit-issue:focus {
+  -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  outline: none;
+  border: none;
+}
+
+.issue-info .created-by-at {
+  flex: 1;
+}
+
+.issue-info .edit-issue {
+  background-color: var(--light) -gray;
+  flex: 0;
+  margin-right: 0.5rem;
+}
+
+.issue-info .close-issue {
+  background-color: var(--failure);
+}
+
+.issue-history {
+  list-style: none;
+  border-top: 1px solid var(--gray);
+  padding-top: 1rem;
+  padding-left: 2rem;
+}
+
+.issue-history .comment-info {
+  color: var(--gray);
+  margin: 0;
+  padding-top: 1rem;
+}
+
+.issue-history .comment-info a {
+  text-decoration: none;
+}
+
+.issue-history .comment-info a:hover {
+  text-decoration: underline;
+}
+
+.issue-history .comment,
+.issue-history .event {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+  border-bottom: 1px solid var(--gray);
+}
+
+.issue-history .comment p,
+.issue-history .event p {
+  margin: 0;
+}
+
+.issue-history .comment:target,
+.issue-history .event:target {
+  border-color: var(--primary);
+  border-bottom-width: 3px;
+}
+
+.issue-history .event {
+  color: var(--gray);
+}
+
+blockquote {
+  border-left: 5px solid var(--light) -gray;
+  padding-left: 1rem;
+  margin-left: 0rem;
+}
+
+pre {
+  overflow-x: auto;
+}
+
+body {
+  font-family: sans-serif;
+  color: var(--text);
+  background: var(--bg);
+  --text: rgb(24, 24, 24);
+  --bg: white;
+  --gray: #8d8d8d;
+  --primary: rgb(106, 154, 255);
+  --primary-light: rgb(150, 166, 200);
+  --success: rgb(168, 249, 166);
+  --failure: rgb(247, 167, 167);
+  --light-gray: #eee;
+}
+
+@media (prefers-color-scheme: dark) {
+  body {
+    --text: rgb(240, 240, 240);
+    --bg: black;
+    --gray: #8d8d8d;
+    --primary: rgb(106, 154, 255);
+    --primary-light: rgb(150, 166, 200);
+    --success: rgb(14, 130, 11);
+    --failure: rgb(124, 14, 14);
+    --light-gray: #222;
+  }
+}
+
+a {
+  color: inherit;
+}
+
+.content {
+  max-width: 800px;
+  margin: 0 auto;
+}
+
+header {
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid var(--text);
+  margin-bottom: 1rem;
+}
+
+header h1 {
+  padding: 0;
+  flex: 1;
+}
+
+header .issue-number {
+  color: var(--gray);
+  font-size: 1.5rem;
+}
+
+nav {
+  display: flex;
+  color: var(--gray);
+  justify-content: space-between;
+}
+
+nav .nav-group {
+  display: flex;
+}
+
+nav .nav-group > * {
+  margin-left: 0.5rem;
+}
+
+footer {
+  border-top: 1px solid var(--gray);
+  padding-top: 1rem;
+  margin-top: 1rem;
+  color: var(--gray);
+}
+
+.new-issue {
+  background-color: var(--success);
+  padding: 0.5rem;
+  text-decoration: none;
+  -moz-transition: box-shadow 0.15s ease-in-out;
+  -o-transition: box-shadow 0.15s ease-in-out;
+  -webkit-transition: box-shadow 0.15s ease-in-out;
+  -ms-transition: box-shadow 0.15s ease-in-out;
+  transition: box-shadow 0.15s ease-in-out;
+}
+
+.new-issue:hover {
+  -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+  box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+}
+
+.new-issue:active,
+.new-issue:focus {
+  -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+  outline: none;
+  border: none;
+}
+
+.alert {
+  padding: 0.5rem;
+  margin-bottom: 1rem;
+  background-color: var(--failure);
+}
+
+.login-form {
+  max-width: 300px;
+  margin: 0 auto;
+}
+
+.created-by-at {
+  color: var(--gray);
+}
+
+.sr-only {
+  border: 0;
+  clip: rect(0 0 0 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px;
+}
diff --git a/cog.toml b/cog.toml
new file mode 100644
index 0000000..f4f71b7
--- /dev/null
+++ b/cog.toml
@@ -0,0 +1,36 @@
+# 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>.
+
+tag_prefix = "v"
+branch_whitelist = ["main", "prime"]
+ignore_merge_commits = false
+
+pre_bump_hooks = [
+  "reuse lint",                    # Check licensing status.
+  "nix flake check",               # Verify the project builds.
+  "cargo set-version {{version}}", # Bump version in Cargo.toml.
+  "nix fmt",                       # Format.
+]
+post_bump_hooks = [
+  "git push",
+  "cargo publish",
+  "git push origin v{{version}}", # push the new tag to origin
+]
+
+[bump_profiles]
+
+[changelog]
+path = "NEWS.md"
+template = "remote"
+remote = "git.foss-syndicate.org"
+repository = "git_bug/back"
+owner = "vhack.eu"
+authors = [{ signature = "Benedikt Peetz", username = "vhack.eu" }]
diff --git a/contrib/config.json b/contrib/config.json
new file mode 100644
index 0000000..a1d70d0
--- /dev/null
+++ b/contrib/config.json
@@ -0,0 +1,6 @@
+{
+  "source_code_repository_url": "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back",
+  "root_url": "https://issues.foss-syndicate.org",
+  "scan_path": "/path/to/the/scan/path",
+  "project_list": "/path/to/the/projects.list"
+}
diff --git a/contrib/config.json.license b/contrib/config.json.license
new file mode 100644
index 0000000..6cb8d56
--- /dev/null
+++ b/contrib/config.json.license
@@ -0,0 +1,11 @@
+Back - An extremely simple git bug visualization system. Inspired by TVL's
+panettone.
+
+Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+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>.
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..78982af
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,139 @@
+{
+  "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": [
+          "systems"
+        ]
+      },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1746576598,
+        "narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "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,
+        "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
+        "owner": "nix-systems",
+        "repo": "x86_64-linux",
+        "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "x86_64-linux",
+        "type": "github"
+      }
+    },
+    "treefmt-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1746989248,
+        "narHash": "sha256-uoQ21EWsAhyskNo8QxrTVZGjG/dV4x5NM1oSgrmNDJY=",
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "rev": "708ec80ca82e2bbafa93402ccb66a35ff87900c5",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.lock.license b/flake.lock.license
new file mode 100644
index 0000000..deb57aa
--- /dev/null
+++ b/flake.lock.license
@@ -0,0 +1,10 @@
+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>.
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..7389429
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,136 @@
+# 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>.
+{
+  description = "An extremely simple git bug visualization system. Inspired by TVL's panettone.";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
+    treefmt-nix = {
+      url = "github:numtide/treefmt-nix";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+      };
+    };
+
+    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 = {
+        systems.follows = "systems";
+      };
+    };
+  };
+
+  outputs = {
+    self,
+    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};
+        '';
+      };
+
+      treefmtEval = import ./treefmt.nix {
+        inherit treefmt-nix pkgs;
+        rustfmt = rust_default;
+      };
+    in {
+      packages.default = pkgs.symlinkJoin {
+        inherit (cargo_toml.package) name;
+
+        paths = [manual craneBuild];
+      };
+
+      checks = {
+        inherit craneBuild;
+        formatting = treefmtEval.config.build.check self;
+      };
+
+      formatter = treefmtEval.config.build.wrapper;
+
+      devShells.default = pkgs.mkShell {
+        packages = with pkgs; [
+          cocogitto
+          git-bug
+
+          rust_default
+          cargo-edit
+
+          reuse
+        ];
+      };
+    });
+}
+# vim: ts=2
+
diff --git a/nix/module.nix b/nix/module.nix
new file mode 100644
index 0000000..4a22467
--- /dev/null
+++ b/nix/module.nix
@@ -0,0 +1,102 @@
+# 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>.
+{
+  config,
+  lib,
+  vhackPackages,
+  pkgs,
+  ...
+}: let
+  cfg = config.vhack.back;
+in {
+  options.vhack.back = {
+    enable = lib.mkEnableOption "Back issue tracker (inspired by tvix's panettone)";
+
+    domain = lib.mkOption {
+      type = lib.types.str;
+      description = "The domain to host this `back` instance on.";
+    };
+
+    settings = {
+      scan_path = lib.mkOption {
+        type = lib.types.path;
+        description = "The path to the directory under which all the repositories reside";
+      };
+      project_list = lib.mkOption {
+        type = lib.types.path;
+        description = "The path to the `projects.list` file.";
+      };
+
+      source_code_repository_url = lib.mkOption {
+        description = "The url to the source code of this instance of back";
+        default = "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back";
+        type = lib.types.str;
+      };
+
+      root_url = lib.mkOption {
+        type = lib.types.str;
+        description = "The url to this instance of back.";
+        default = "https://${cfg.domain}";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services."back" = {
+      description = "Back issue tracking system.";
+      requires = ["network-online.target"];
+      after = ["network-online.target"];
+      wantedBy = ["default.target"];
+
+      serviceConfig = {
+        ExecStart = "${lib.getExe vhackPackages.back} ${(pkgs.formats.json {}).generate "config.json" cfg.settings}";
+
+        # Ensure that the service can read the repository
+        # FIXME(@bpeetz): This has the implied assumption, that all the exposed git
+        # repositories are readable for the git group. This should not be necessary. <2024-12-23>
+        User = "git";
+        Group = "git";
+
+        DynamicUser = true;
+        Restart = "always";
+
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"];
+      };
+    };
+    services.nginx.virtualHosts."${cfg.domain}" = {
+      locations."/".proxyPass = "http://127.0.0.1:8000";
+
+      enableACME = true;
+      forceSSL = true;
+    };
+  };
+}
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 0000000..ef00195
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,38 @@
+# Back - An extremely simple git bug visualization system. Inspired by TVL's
+# panettone.
+#
+# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# 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>.
+{
+  rustPlatform,
+  lib,
+}:
+rustPlatform.buildRustPackage {
+  pname = "back";
+  version = "1.0.0";
+
+  src = lib.cleanSourceWith {
+    src = lib.cleanSource ./.;
+    filter = name: type:
+      (type == "directory")
+      || (builtins.elem (builtins.baseNameOf name) ["Cargo.toml" "Cargo.lock" "style.css"])
+      || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name))
+      || (lib.strings.hasSuffix ".html" (builtins.baseNameOf name));
+  };
+
+  doCheck = true;
+
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+
+  meta = {
+    mainProgram = "back";
+  };
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..5c3f6ba
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,79 @@
+max_width = 100
+hard_tabs = false
+tab_spaces = 4
+newline_style = "Auto"
+indent_style = "Block"
+use_small_heuristics = "Default"
+fn_call_width = 60
+attr_fn_like_width = 70
+struct_lit_width = 18
+struct_variant_width = 35
+array_width = 60
+chain_width = 60
+single_line_if_else_max_width = 50
+single_line_let_else_max_width = 50
+wrap_comments = false
+format_code_in_doc_comments = true
+doc_comment_code_block_width = 100
+comment_width = 100
+normalize_comments = false
+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
+fn_single_line = false
+where_single_line = false
+imports_indent = "Block"
+imports_layout = "Mixed"
+imports_granularity = "Crate"
+group_imports = "StdExternalCrate"
+reorder_imports = true
+reorder_modules = true
+reorder_impl_items = true
+type_punctuation_density = "Wide"
+space_before_colon = false
+space_after_colon = true
+spaces_around_ranges = false
+binop_separator = "Front"
+remove_nested_parens = true
+combine_control_expr = true
+short_array_element_width_threshold = 10
+overflow_delimited_expr = false
+struct_field_align_threshold = 0
+enum_discrim_align_threshold = 0
+match_arm_blocks = true
+match_arm_leading_pipes = "Never"
+force_multiline_blocks = false
+fn_params_layout = "Tall"
+brace_style = "SameLineWhere"
+control_brace_style = "AlwaysSameLine"
+trailing_semicolon = true
+trailing_comma = "Vertical"
+match_block_trailing_comma = false
+blank_lines_upper_bound = 1
+blank_lines_lower_bound = 0
+edition = "2024"
+style_edition = "2024"
+inline_attribute_width = 0
+format_generated_files = true
+generated_marker_line_search_limit = 5
+merge_derives = true
+use_try_shorthand = false
+use_field_init_shorthand = true
+force_explicit_abi = true
+condense_wildcard_suffixes = false
+color = "Auto"
+required_version = "1.8.0"
+unstable_features = true
+disable_all_formatting = false
+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/scripts/optimize_images.sh b/scripts/optimize_images.sh
new file mode 100755
index 0000000..2d923ac
--- /dev/null
+++ b/scripts/optimize_images.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env nix
+#! nix shell nixpkgs#optipng nixpkgs#jpegoptim nixpkgs#nodePackages.svgo nixpkgs#dash --command dash
+
+# 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>.
+
+# shellcheck shell=dash
+
+# source: https://github.com/stride-tasks/stride/blob/148d513297c8ae66d79fc287769adfe5e711c93c/scripts/optimize-images
+
+PROJECT_DIR="$(git rev-parse --show-toplevel)"
+
+cd "$PROJECT_DIR" || {
+    echo "No '$PROJECT_DIR' ?!"
+    exit 1
+}
+
+PNG_OPITMIZE_COMMAND='optipng --quiet -o7 -preserve'
+JPG_OPITMIZE_COMMAND='jpegoptim --quiet --strip-all -m76'
+SVG_OPITMIZE_COMMAND='svgo --multipass --quiet --input'
+
+# The extension is the part after the first dot in the filename:
+#
+# For example:
+# file.png    => png
+# file.tar.gz => tar.gz
+find_files_by_extension() {
+    wanted_extension="$1"
+    tmp="$(mktemp)"
+
+    git ls-files --cached --modified --other --exclude-standard --deduplicate | while IFS= read -r file; do
+        file_basename="$(basename "$file")"
+        extension="${file_basename#*.}"
+        if [ "$extension" = "$wanted_extension" ]; then
+            echo "$file"
+        else
+            :
+            # echo "'$file' with extension: '$extension' does not match filter: '$wanted_extension'" 1>&2
+        fi
+    done >"$tmp"
+
+    echo "$tmp"
+}
+
+size_of() {
+    du -b "$1" | cut -f1
+}
+
+bytes_human() {
+    number="$1"
+
+    numfmt --to=iec-i --suffix=B --format="%9.2f" "$number"
+}
+
+# https://stackoverflow.com/questions/44695878/how-to-calculate-percentage-in-shell-script
+# Native POSIX solution using string manipulation (assumes integer inputs).
+percent() {
+    DP="$1"
+    SDC="$2"
+
+    # Special case when DP is zero.
+    [ "$DP" = "0" ] && echo "0.00" && return
+
+    #                                  # e.g. round down   e.g. round up
+    #                                  # DP=1 SDC=3        DP=2 SDC=3
+    Percent=$((DP * 100000 / SDC + 5)) # Percent=33338     Percent=66671
+    Whole=${Percent%???}               # Whole=33          Whole=66
+    Percent=${Percent#"$Whole"}        # Percent=338       Percent=671
+    Percent=$Whole.${Percent%?}        # Percent=33.33     Percent=66.67
+    echo "$Percent"
+}
+
+TOTAL=0
+TOTAL_SAVED=0
+
+optimize_files() {
+    FILTER="$1"
+    PROGRAM="$2"
+
+    printf "%s" "Processing $FILTER files:"
+
+    FILES="$(find_files_by_extension "$FILTER")"
+    COUNT=$(wc -l <"$FILES")
+
+    if [ "$COUNT" -eq 0 ]; then
+        echo " no files found!"
+        return
+    fi
+
+    echo ""
+
+    I=1
+
+    TOTAL_INNER=0
+    TOTAL_SAVED_INNER=0
+
+    while IFS= read -r f; do
+        printf "%s/${COUNT} $f ... " "$I"
+
+        SIZE="$(size_of "$f")"
+
+        $PROGRAM "$f"
+
+        NEW_SIZE="$(size_of "$f")"
+        DIFF=$((SIZE - NEW_SIZE))
+
+        echo "saved: $(bytes_human "$DIFF") ($(percent $DIFF "$SIZE")%)"
+
+        TOTAL_INNER=$((TOTAL_INNER + SIZE))
+        TOTAL_SAVED_INNER=$((TOTAL_SAVED_INNER + DIFF))
+
+        I=$((I + 1))
+    done <"$FILES"
+    rm "$FILES"
+
+    echo "Total saved for $FILTER: $(bytes_human "$TOTAL_SAVED_INNER") ($(percent $TOTAL_SAVED_INNER $TOTAL_INNER)%)"
+    echo ""
+
+    TOTAL=$((TOTAL + TOTAL_INNER))
+    TOTAL_SAVED=$((TOTAL_SAVED + TOTAL_SAVED_INNER))
+}
+
+optimize_files 'png' "$PNG_OPITMIZE_COMMAND"
+optimize_files 'jpg' "$JPG_OPITMIZE_COMMAND"
+optimize_files 'jpeg' "$JPG_OPITMIZE_COMMAND"
+optimize_files 'svg' "$SVG_OPITMIZE_COMMAND"
+
+echo "Total saved: $(bytes_human "$TOTAL_SAVED") ($(percent $TOTAL_SAVED $TOTAL)%)"
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..eeeed2b
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,25 @@
+// Back - An extremely simple git bug visualization system. Inspired by TVL's
+// panettone.
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// 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::path::PathBuf;
+
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+#[allow(clippy::module_name_repetitions)]
+/// An extremely simple git issue tracking system.
+/// Inspired by tvix's panettone
+pub 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
new file mode 100644
index 0000000..6c90fce
--- /dev/null
+++ b/src/config/mod.rs
@@ -0,0 +1,137 @@
+// Back - An extremely simple git bug visualization system. Inspired by TVL's
+// panettone.
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// 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, PathBuf},
+};
+
+use gix::ThreadSafeRepository;
+use serde::Deserialize;
+use url::Url;
+
+use crate::{
+    error::{self, Error},
+    git_bug::dag::is_git_bug,
+};
+
+#[derive(Deserialize)]
+pub struct BackConfig {
+    /// The url to the source code of back. This is needed, because back is licensed under the
+    /// AGPL.
+    pub source_code_repository_url: Url,
+
+    /// The root url this instance of back is hosted on.
+    /// For example:
+    ///     `issues.foss-syndicate.org`
+    pub root_url: Url,
+
+    project_list: PathBuf,
+
+    /// The path that is the common parent of all the repositories.
+    pub scan_path: PathBuf,
+}
+
+pub struct BackRepositories {
+    repositories: Vec<BackRepository>,
+}
+
+impl BackRepositories {
+    pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
+        self.into_iter()
+    }
+}
+
+impl<'a> IntoIterator for &'a BackRepositories {
+    type IntoIter = <&'a Vec<BackRepository> as IntoIterator>::IntoIter;
+    type Item = <&'a Vec<BackRepository> as IntoIterator>::Item;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.repositories.iter()
+    }
+}
+
+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> {
+        self.repositories
+            .iter()
+            .find(|p| p.repo_path == path)
+            .ok_or(error::Error::RepoFind {
+                repository_path: path.to_owned(),
+            })
+    }
+}
+
+pub struct BackRepository {
+    repo_path: PathBuf,
+}
+
+impl BackRepository {
+    pub fn open(&self, scan_path: &Path) -> Result<ThreadSafeRepository, error::Error> {
+        let path = {
+            let base = scan_path.join(&self.repo_path);
+            if base.is_dir() {
+                base
+            } else {
+                PathBuf::from(base.display().to_string() + ".git")
+            }
+        };
+        let repo = ThreadSafeRepository::open(path).map_err(|err| Error::RepoOpen {
+            repository_path: self.repo_path.to_owned(),
+            error: Box::new(err),
+        })?;
+        if is_git_bug(&repo.to_thread_local())? {
+            Ok(repo)
+        } else {
+            Err(error::Error::NotGitBug {
+                path: self.repo_path.clone(),
+            })
+        }
+    }
+
+    pub fn path(&self) -> &Path {
+        &self.repo_path
+    }
+}
+
+impl BackConfig {
+    pub fn repositories(&self) -> error::Result<BackRepositories> {
+        let repositories = fs::read_to_string(&self.project_list)
+            .map_err(|err| error::Error::ProjectListRead {
+                error: err,
+                file: self.project_list.to_owned(),
+            })?
+            .lines()
+            .try_fold(vec![], |mut acc, path| {
+                acc.push(BackRepository {
+                    repo_path: PathBuf::from(path),
+                });
+
+                Ok::<_, error::Error>(acc)
+            })?;
+        Ok(BackRepositories { repositories })
+    }
+
+    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(),
+            error: err,
+        })?;
+
+        serde_json::from_str(&value).map_err(|err| Error::ConfigParse {
+            file: path.to_owned(),
+            error: err,
+        })
+    }
+}
diff --git a/src/error/mod.rs b/src/error/mod.rs
new file mode 100644
index 0000000..026cc58
--- /dev/null
+++ b/src/error/mod.rs
@@ -0,0 +1,135 @@
+// Back - An extremely simple git bug visualization system. Inspired by TVL's
+// panettone.
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// 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::{fmt::Display, io, net::SocketAddr, path::PathBuf};
+
+use gix::hash::Prefix;
+use thiserror::Error;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    ConfigParse {
+        file: PathBuf,
+        error: serde_json::Error,
+    },
+
+    ProjectListRead {
+        file: PathBuf,
+        error: io::Error,
+    },
+    ConfigRead {
+        file: PathBuf,
+        error: io::Error,
+    },
+    NotGitBug {
+        path: PathBuf,
+    },
+    RepoOpen {
+        repository_path: PathBuf,
+        error: Box<gix::open::Error>,
+    },
+    RepoFind {
+        repository_path: PathBuf,
+    },
+    RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error),
+    RepoRefsPrefixed {
+        error: io::Error,
+    },
+
+    TcpBind {
+        addr: SocketAddr,
+        err: io::Error,
+    },
+    TcpAccept {
+        err: io::Error,
+    },
+
+    IssuesPrefixMissing {
+        prefix: Prefix,
+    },
+    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}.")
+            }
+        }
+    }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..49ffe5c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,54 @@
+// Back - An extremely simple git bug visualization system. Inspired by TVL's
+// panettone.
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// 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::{process, sync::Arc};
+
+use clap::Parser;
+
+use crate::config::BackConfig;
+
+mod cli;
+pub mod config;
+mod error;
+pub mod git_bug;
+mod web;
+
+fn main() -> Result<(), String> {
+    if let Err(err) = server_main() {
+        eprintln!("Error {err}");
+        process::exit(1);
+    } else {
+        Ok(())
+    }
+}
+
+#[tokio::main]
+async fn server_main() -> Result<(), error::Error> {
+    let args = cli::Cli::parse();
+
+    stderrlog::new()
+        .module(module_path!())
+        .modules(["hyper", "http"])
+        .quiet(false)
+        .show_module_names(false)
+        .color(stderrlog::ColorChoice::Auto)
+        .verbosity(2)
+        .timestamp(stderrlog::Timestamp::Off)
+        .init()
+        .expect("Let's just hope that this does not panic");
+
+    let config = BackConfig::from_config_file(&args.config_file)?;
+
+    web::main(Arc::new(config)).await?;
+
+    Ok(())
+}
diff --git a/src/web/generate/mod.rs b/src/web/generate/mod.rs
new file mode 100644
index 0000000..06bab17
--- /dev/null
+++ b/src/web/generate/mod.rs
@@ -0,0 +1,236 @@
+// 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},
+    },
+};
+
+#[derive(Template)]
+#[template(path = "./issues.html")]
+struct IssuesTemplate {
+    wanted_status: Status,
+    counter_status: Status,
+    issues: Vec<CollapsedIssue>,
+
+    /// The path to the repository
+    repo_path: String,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+pub fn issues(
+    config: &BackConfig,
+    wanted_status: Status,
+    counter_status: Status,
+    repo_path: &Path,
+) -> 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())?
+        .into_iter()
+        .map(|issue| issue.collapse())
+        .filter(|issue| issue.status == wanted_status)
+        .collect::<Vec<CollapsedIssue>>();
+
+    // 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"))
+}
+
+use crate::git_bug::format::HtmlString;
+#[derive(Template)]
+#[template(path = "./issue.html")]
+struct IssueTemplate {
+    issue: CollapsedIssue,
+
+    /// The path to the repository
+    repo_path: String,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result<String> {
+    let repository = config
+        .repositories()?
+        .get(repo_path)?
+        .open(&config.scan_path)?
+        .to_thread_local();
+
+    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 }),
+    }
+}
+
+#[derive(Template)]
+#[template(path = "./repos.html")]
+struct ReposTemplate {
+    repos: Vec<RepoValue>,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+struct RepoValue {
+    description: String,
+    owner: String,
+    path: String,
+}
+pub 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();
+
+                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());
+
+                let description = fs::read_to_string(repo.git_dir().join("description"))
+                    .unwrap_or("<No description>".to_owned());
+
+                Some(RepoValue {
+                    description,
+                    owner,
+                    path,
+                })
+            }
+            Err(err) => {
+                info!(
+                    "Repo '{}' could not be opened: '{err}'",
+                    raw_repo.path().display()
+                );
+                None
+            }
+        })
+        .collect();
+
+    Ok(ReposTemplate {
+        repos,
+        source_code_repository_url: config.source_code_repository_url.clone(),
+    }
+    .render()
+    .expect("this should work"))
+}
+
+pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> {
+    use rss::{ChannelBuilder, Item, ItemBuilder};
+
+    let repository = config
+        .repositories()?
+        .get(repo_path)?
+        .open(&config.scan_path)?
+        .to_thread_local();
+
+    let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
+        .into_iter()
+        .map(|issue| issue.collapse())
+        .collect();
+
+    // 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())
+                .link(format!(
+                    "/{}/{}/issue/{}",
+                    repo_path.display(),
+                    &config.root_url,
+                    issue.id
+                ))
+                .build()
+        })
+        .collect();
+
+    // Append all comments after converting them to rss items
+    items.extend(
+        issues
+            .iter()
+            .filter(|issue| !issue.comments.is_empty())
+            .flat_map(|issue| {
+                issue
+                    .comments
+                    .iter()
+                    .map(|comment| {
+                        ItemBuilder::default()
+                            .title(issue.title.to_string())
+                            .author(comment.author.to_string())
+                            .description(comment.message.to_string())
+                            .pub_date(comment.timestamp.to_string())
+                            .link(format!(
+                                "/{}/{}/issue/{}",
+                                repo_path.display(),
+                                &config.root_url,
+                                issue.id
+                            ))
+                            .build()
+                    })
+                    .collect::<Vec<Item>>()
+            })
+            .collect::<Vec<Item>>(),
+    );
+
+    let channel = ChannelBuilder::default()
+        .title("Issues")
+        .link(config.root_url.to_string())
+        .description(format!("The rss feed for issues on {}.", &config.root_url))
+        .items(items)
+        .build();
+    Ok(channel.to_string())
+}
diff --git a/src/web/mod.rs b/src/web/mod.rs
new file mode 100644
index 0000000..8e2e9b0
--- /dev/null
+++ b/src/web/mod.rs
@@ -0,0 +1,138 @@
+// 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::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc};
+
+use bytes::Bytes;
+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 tokio::net::TcpListener;
+
+use crate::{config::BackConfig, error, git_bug::issue::Status};
+
+mod generate;
+mod responses;
+
+async fn match_uri(
+    config: Arc<BackConfig>,
+    req: Request<hyper::body::Incoming>,
+) -> Result<Response<BoxBody<Bytes, Infallible>>, hyper::Error> {
+    if req.method() != Method::GET {
+        return Ok(html_response_status(
+            "Only get requests are supported",
+            StatusCode::NOT_ACCEPTABLE,
+        ));
+    }
+
+    let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> {
+        match req.uri().path().trim_end_matches("/") {
+            "" => Ok(html_response(generate::repos(&config)?)),
+
+            "/style.css" => Ok(responses::html_response_status_content_type(
+                include_str!("../../assets/style.css"),
+                StatusCode::OK,
+                "text/css",
+            )),
+
+            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"),
+                );
+
+                let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?;
+                Ok(html_response(issues))
+            }
+            path if path.ends_with("/issues/feed") => {
+                let repo_path = PathBuf::from(
+                    path.strip_suffix("/issues/feed")
+                        .expect("This suffix exists")
+                        .strip_prefix("/")
+                        .expect("This also exists"),
+                );
+
+                let feed = generate::feed(&config, &repo_path)?;
+                Ok(html_response_status_content_type(
+                    feed,
+                    StatusCode::OK,
+                    "text/xml",
+                ))
+            }
+
+            path if path.contains("/issue/") => {
+                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 repo_path =
+                        PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists"));
+
+                    (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",
+            )),
+        }
+    };
+    match output() {
+        Ok(response) => Ok(response),
+        Err(err) => Ok(err.into_response()),
+    }
+}
+
+pub 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);
+    loop {
+        let (stream, _) = listener
+            .accept()
+            .await
+            .map_err(|err| error::Error::TcpAccept { err })?;
+        let io = TokioIo::new(stream);
+
+        let local_config = Arc::clone(&config);
+
+        let service = service_fn(move |req| match_uri(Arc::clone(&local_config), req));
+
+        tokio::task::spawn(async move {
+            if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
+                error!("Error serving connection: {:?}", err);
+            }
+        });
+    }
+}
diff --git a/src/web/responses.rs b/src/web/responses.rs
new file mode 100644
index 0000000..bcdcc0a
--- /dev/null
+++ b/src/web/responses.rs
@@ -0,0 +1,61 @@
+// 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::convert::Infallible;
+
+use bytes::Bytes;
+use http::{Response, StatusCode, Version};
+use http_body_util::{BodyExt, Full, combinators::BoxBody};
+
+use crate::{error, git_bug::format::HtmlString};
+
+pub(super) fn html_response<T: Into<Bytes>>(html_text: T) -> Response<BoxBody<Bytes, Infallible>> {
+    html_response_status(html_text, StatusCode::OK)
+}
+
+pub(super) fn html_response_status<T: Into<Bytes>>(
+    html_text: T,
+    status: StatusCode,
+) -> Response<BoxBody<Bytes, Infallible>> {
+    html_response_status_content_type(html_text, status, "text/html")
+}
+
+pub(super) fn html_response_status_content_type<T: Into<Bytes>>(
+    html_text: T,
+    status: StatusCode,
+    content_type: &str,
+) -> Response<BoxBody<Bytes, Infallible>> {
+    Response::builder()
+        .status(status)
+        .version(Version::HTTP_2)
+        .header("Content-Type", format!("{}; charset=utf-8", content_type))
+        .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> {
+    Full::new(chunk.into()).boxed()
+}
+
+// FIXME: Not all errors should return `INTERNAL_SERVER_ERROR`. <2025-03-08>
+impl error::Error {
+    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())
+            ),
+            StatusCode::INTERNAL_SERVER_ERROR,
+        )
+    }
+}
diff --git a/templates/issue.html b/templates/issue.html
new file mode 100644
index 0000000..c7e4efc
--- /dev/null
+++ b/templates/issue.html
@@ -0,0 +1,74 @@
+<!--
+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">&lt;{{issue.author.email|safe}}&gt;</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
new file mode 100644
index 0000000..564827c
--- /dev/null
+++ b/templates/issues.html
@@ -0,0 +1,77 @@
+<!--
+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"
+                  >&lt;{{issue.author.email|safe}}&gt;</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
new file mode 100644
index 0000000..8aa71c4
--- /dev/null
+++ b/templates/repos.html
@@ -0,0 +1,62 @@
+<!--
+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/tests/base.nix b/tests/base.nix
new file mode 100644
index 0000000..5aebe6a
--- /dev/null
+++ b/tests/base.nix
@@ -0,0 +1,216 @@
+# 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>.
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  domain = "server";
+
+  sshKeys =
+    import ../../gi/git-server/ssh_keys.nix {inherit pkgs;};
+
+  gitoliteAdminConfSnippet = pkgs.writeText "gitolite-admin-conf-snippet" ''
+    repo CREATOR/[a-zA-Z0-9].*
+      C     = @all
+      RW+   = CREATOR
+      RW    = WRITERS
+      R     = READERS
+      option user-configs = cgit\.owner cgit\.desc cgit\.section cgit\.homepage
+  '';
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "back";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      server = {config, ...}: {
+        environment.systemPackages = [pkgs.git];
+
+        imports =
+          extraModules
+          ++ [
+            ../../../../modules
+          ];
+
+        vhack = {
+          persist.enable = true;
+          openssh.enable = true;
+          nginx = {
+            enable = true;
+            selfsign = true;
+          };
+          git-server = {
+            enable = true;
+            domain = "git.${domain}";
+            gitolite.adminPubkey = sshKeys.admin.pub;
+          };
+          back = {
+            enable = true;
+            domain = "issues.${domain}";
+
+            settings = {
+              scan_path = "${config.services.gitolite.dataDir}/repositories";
+              project_list = "${config.services.gitolite.dataDir}/projects.list";
+            };
+          };
+        };
+      };
+
+      client = {nodes, ...}: {
+        environment.systemPackages = [pkgs.git pkgs.curl pkgs.git-bug pkgs.gawk];
+        programs.ssh.extraConfig = ''
+          Host *
+            UserKnownHostsFile /dev/null
+            StrictHostKeyChecking no
+            # there's nobody around that can input password
+            PreferredAuthentications publickey
+        '';
+        users.users.alice = {isNormalUser = true;};
+        networking.hosts = {
+          "${nodes.server.networking.primaryIPAddress}" = [
+            "git.${domain}"
+            "issues.${domain}"
+            "${domain}"
+          ];
+        };
+      };
+    };
+
+    testScript = {nodes, ...}:
+    /*
+    python
+    */
+    ''
+      start_all()
+
+      with subtest("can setup ssh keys on client"):
+        client.succeed(
+            "mkdir -p ~root/.ssh",
+            "cp ${sshKeys.admin.priv} ~root/.ssh/id_ed25519",
+            "chmod 600 ~root/.ssh/id_ed25519",
+        )
+        client.succeed(
+            "sudo -u alice mkdir -p ~alice/.ssh",
+            "sudo -u alice cp ${sshKeys.alice.priv} ~alice/.ssh/id_ed25519",
+            "sudo -u alice chmod 600 ~alice/.ssh/id_ed25519",
+        )
+
+      with subtest("gitolite server starts"):
+        server.wait_for_unit("gitolite-init.service")
+        server.wait_for_unit("sshd.service")
+        client.succeed("ssh -n git@git.${domain} info")
+
+
+      with subtest("admin can clone and configure gitolite-admin.git"):
+        client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" ''
+        set -xe
+
+        git clone git@git.${domain}:gitolite-admin.git
+        git config --global user.name 'System Administrator'
+        git config --global user.email root\@domain.example
+
+        cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub
+
+        (cd gitolite-admin && git switch -c master && git branch -D main)
+
+        (cd gitolite-admin && git add . && git commit -m 'Add keys for alice' && git push -u origin master)
+        cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf
+        (cd gitolite-admin && git add . && git commit -m 'Add support for wild repos' && git push)
+        (cd gitolite-admin && git push -d origin main)
+      ''}")
+
+      with subtest("alice can create a repo"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "alice-create-repo" ''
+        set -xe
+
+        mkdir --parents ./alice/repo1 && cd alice/repo1;
+
+        git init --initial-branch main
+        echo "# Alice's Repo" > README.md
+        git add README.md
+        git -c user.name=Alice -c user.email=alice@domain.example commit -m 'Add readme'
+
+        git remote add origin git@git.${domain}:alice/repo1.git
+        git push --set-upstream origin main
+      ''}")
+
+      with subtest("can setup git-bug issues in alice/repo1"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "setup-git-repo" ''
+        set -ex
+
+        cd alice/repo1
+
+        git bug user create --avatar "" --email "alice@server.org" --name "alice" --non-interactive
+
+        git bug add \
+        --title "Some bug title" \
+        --message "A long description of the bug. Probably has some code segments, maybe even *markdown* mark_up_ or other things" \
+        --non-interactive
+
+        git bug add \
+        --title "Second bug title" \
+        --message "" \
+        --non-interactive
+
+        git bug add \
+        --title "Third bug title" \
+        --message "" \
+        --non-interactive
+
+        git bug select "$(git bug ls --format plain | awk '{print $1}' | head -n 1)"
+
+        git bug comment add --message "Some comment message" --non-interactive
+        git bug comment add --message "Second comment message" --non-interactive
+        git bug label add "Test"
+
+        # TODO: This should use `git bug push`, but their ssh implementation is just
+        # too special to work in a VM test <2025-03-08>
+        git push origin +refs/bugs/*
+        git push origin +refs/identities/*
+
+        ssh git@${domain} -- config alice/repo1 --add cgit.owner Alice
+        ssh git@${domain} -- perms alice/repo1 + READERS @all
+      ''}")
+
+      with subtest("back server starts"):
+        server.wait_for_unit("back.service")
+
+      with subtest("client can access the server"):
+        client.succeed("${pkgs.writeShellScript "curl-back" ''
+        set -xe
+
+        curl --insecure --fail --show-error "https://issues.${domain}/alice/repo1.git/issues/open" --output /root/issues.html
+        grep -- 'Second bug title' /root/issues.html
+
+        curl --insecure --fail --show-error "https://issues.${domain}/" --output /root/repos.html
+        grep -- 'repo' /root/repos.html
+        grep -- "&#60;No description&#62;" /root/repos.html
+        grep -- '<span class="user-name">Alice</span>' /root/repos.html
+      ''} >&2")
+
+      client.copy_from_vm("/root/issues.html", "");
+      client.copy_from_vm("/root/repos.html", "");
+    '';
+  }
diff --git a/treefmt.nix b/treefmt.nix
new file mode 100644
index 0000000..ebe4157
--- /dev/null
+++ b/treefmt.nix
@@ -0,0 +1,88 @@
+# 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>.
+{
+  treefmt-nix,
+  pkgs,
+  rustfmt,
+}:
+treefmt-nix.lib.evalModule pkgs (
+  {pkgs, ...}: {
+    # Used to find the project root
+    projectRootFile = "flake.nix";
+
+    programs = {
+      alejandra.enable = true;
+      rustfmt = {
+        enable = true;
+        package = rustfmt;
+        edition = "2024";
+      };
+      clang-format.enable = true;
+      mdformat.enable = true;
+      shfmt = {
+        enable = true;
+        indent_size = 4;
+      };
+      shellcheck.enable = true;
+      prettier = {
+        enable = true;
+        settings = {
+          arrowParens = "always";
+          bracketSameLine = false;
+          bracketSpacing = true;
+          editorconfig = true;
+          embeddedLanguageFormatting = "auto";
+          endOfLine = "lf";
+          # experimentalTernaries = false;
+          htmlWhitespaceSensitivity = "css";
+          insertPragma = false;
+          jsxSingleQuote = true;
+          printWidth = 80;
+          proseWrap = "always";
+          quoteProps = "consistent";
+          requirePragma = false;
+          semi = true;
+          singleAttributePerLine = true;
+          singleQuote = false;
+          trailingComma = "all";
+          useTabs = false;
+          vueIndentScriptAndStyle = false;
+
+          tabWidth = 2;
+        };
+      };
+      stylua.enable = true;
+      ruff = {
+        enable = true;
+        format = true;
+      };
+      taplo.enable = true;
+    };
+
+    settings = {
+      global.excludes = [
+        "CHANGELOG.md"
+        "NEWS.md"
+      ];
+      formatter = {
+        clang-format = {
+          options = ["--style" "GNU"];
+        };
+        rustfmt = {
+          options = ["--config-path" "${./rustfmt.toml}"];
+        };
+        shfmt = {
+          includes = ["*.bash"];
+        };
+      };
+    };
+  }
+)
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..d3304c2
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env sh
+
+# 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>.
+
+nix flake update
+
+[ "$1" = "upgrade" ] && cargo upgrade --incompatible
+cargo update