🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-06 1:04:51.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-06 1:11:22.0 +01:00:00
commit
afaae25fb783d32ca1a2c3a6b39c0604a6437c77 [patch]
tree
77592f6cb440f8b1c6bc3e566e961b36ed46532f
download
afaae25fb783d32ca1a2c3a6b39c0604a6437c77.tar.gz

Initial commit



Diff

 .gitignore                  |    2 ++
 Cargo.lock                  | 1249 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 Cargo.toml                  |   24 ++++++++++++++++++++++++
 LICENSE                     |   14 ++++++++++++++
 README.md                   |    4 ++++
 src/git.rs                  |  136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main.rs                 |   48 ++++++++++++++++++++++++++++++++++++++++++++++++
 statics/style.css           |   70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 templates/base.html         |   26 ++++++++++++++++++++++++++
 templates/index.html        |   55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/layers/logger.rs        |  134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/layers/mod.rs           |   19 +++++++++++++++++++
 src/methods/index.rs        |   21 +++++++++++++++++++++
 src/methods/mod.rs          |    2 ++
 src/methods/repo.rs         |  174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 templates/repo/about.html   |    6 ++++++
 templates/repo/base.html    |   14 ++++++++++++++
 templates/repo/commit.html  |   38 ++++++++++++++++++++++++++++++++++++++
 templates/repo/diff.html    |    6 ++++++
 templates/repo/log.html     |    6 ++++++
 templates/repo/refs.html    |    6 ++++++
 templates/repo/stats.html   |    6 ++++++
 templates/repo/summary.html |    6 ++++++
 templates/repo/tree.html    |    6 ++++++
 24 files changed, 2072 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d81f12e 100644
--- /dev/null
+++ a/.gitignore
@@ -1,0 +1,2 @@
/target
/.idea
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..3c0b44e 100644
--- /dev/null
+++ a/Cargo.lock
@@ -1,0 +1,1249 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
 "winapi",
]

[[package]]
name = "askama"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139"
dependencies = [
 "askama_derive",
 "askama_escape",
 "askama_shared",
]

[[package]]
name = "askama_derive"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71"
dependencies = [
 "askama_shared",
 "proc-macro2",
 "syn",
]

[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"

[[package]]
name = "askama_shared"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0"
dependencies = [
 "askama_escape",
 "humansize",
 "mime",
 "mime_guess",
 "nom",
 "num-traits",
 "percent-encoding",
 "proc-macro2",
 "quote",
 "serde",
 "syn",
 "toml",
]

[[package]]
name = "async-trait"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
 "hermit-abi",
 "libc",
 "winapi",
]

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "axum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2cc6e8e8c993cb61a005fab8c1e5093a29199b7253b05a6883999312935c1ff"
dependencies = [
 "async-trait",
 "axum-core",
 "bitflags",
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "hyper",
 "itoa",
 "matchit",
 "memchr",
 "mime",
 "percent-encoding",
 "pin-project-lite",
 "serde",
 "serde_json",
 "serde_urlencoded",
 "sync_wrapper",
 "tokio",
 "tower",
 "tower-http",
 "tower-layer",
 "tower-service",
]

[[package]]
name = "axum-core"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4d047478b986f14a13edad31a009e2e05cb241f9805d0d75e4cba4e129ad4d"
dependencies = [
 "async-trait",
 "bytes",
 "futures-util",
 "http",
 "http-body",
 "mime",
]

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"

[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
dependencies = [
 "jobserver",
]

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "clap"
version = "3.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
dependencies = [
 "atty",
 "bitflags",
 "clap_lex",
 "indexmap",
 "once_cell",
 "strsim",
 "termcolor",
 "textwrap",
]

[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
 "os_str_bytes",
]

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"

[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
 "matches",
 "percent-encoding",
]

[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-channel"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
 "futures-core",
 "futures-sink",
]

[[package]]
name = "futures-core"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"

[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
 "futures-core",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-io"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"

[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "futures-sink"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"

[[package]]
name = "futures-task"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"

[[package]]
name = "futures-util"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
 "futures-sink",
 "futures-task",
 "memchr",
 "pin-project-lite",
 "pin-utils",
 "slab",
]

[[package]]
name = "getrandom"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "git2"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
dependencies = [
 "bitflags",
 "libc",
 "libgit2-sys",
 "log",
 "openssl-probe",
 "openssl-sys",
 "url",
]

[[package]]
name = "hashbrown"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"

[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
 "libc",
]

[[package]]
name = "http"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
 "bytes",
 "fnv",
 "itoa",
]

[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
 "bytes",
 "http",
 "pin-project-lite",
]

[[package]]
name = "http-range-header"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"

[[package]]
name = "httparse"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"

[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"

[[package]]
name = "humansize"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"

[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"

[[package]]
name = "hyper"
version = "0.14.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
dependencies = [
 "bytes",
 "futures-channel",
 "futures-core",
 "futures-util",
 "http",
 "http-body",
 "httparse",
 "httpdate",
 "itoa",
 "pin-project-lite",
 "socket2",
 "tokio",
 "tower-service",
 "tracing",
 "want",
]

[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
 "matches",
 "unicode-bidi",
 "unicode-normalization",
]

[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
 "autocfg",
 "hashbrown",
]

[[package]]
name = "itoa"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"

[[package]]
name = "jobserver"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
dependencies = [
 "libc",
]

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"

[[package]]
name = "libgit2-sys"
version = "0.13.4+1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1"
dependencies = [
 "cc",
 "libc",
 "libssh2-sys",
 "libz-sys",
 "openssl-sys",
 "pkg-config",
]

[[package]]
name = "libssh2-sys"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca"
dependencies = [
 "cc",
 "libc",
 "libz-sys",
 "openssl-sys",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "libz-sys"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
dependencies = [
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "lock_api"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
dependencies = [
 "autocfg",
 "scopeguard",
]

[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
 "cfg-if",
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"

[[package]]
name = "matchit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"

[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"

[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"

[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
 "mime",
 "unicase",
]

[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"

[[package]]
name = "mio"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [
 "libc",
 "log",
 "wasi",
 "windows-sys",
]

[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
 "memchr",
 "minimal-lexical",
]

[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
 "autocfg",
]

[[package]]
name = "num_cpus"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
 "hermit-abi",
 "libc",
]

[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
 "libc",
]

[[package]]
name = "once_cell"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"

[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"

[[package]]
name = "openssl-sys"
version = "0.9.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
dependencies = [
 "autocfg",
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "os_str_bytes"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"

[[package]]
name = "owning_ref"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce"
dependencies = [
 "stable_deref_trait",
]

[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
 "lock_api",
 "parking_lot_core",
]

[[package]]
name = "parking_lot_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
dependencies = [
 "cfg-if",
 "libc",
 "redox_syscall",
 "smallvec",
 "windows-sys",
]

[[package]]
name = "path-clean"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd"

[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"

[[package]]
name = "pin-project"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260"
dependencies = [
 "pin-project-internal",
]

[[package]]
name = "pin-project-internal"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"

[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"

[[package]]
name = "pkg-config"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"

[[package]]
name = "proc-macro2"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "redox_syscall"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
dependencies = [
 "bitflags",
]

[[package]]
name = "rgit"
version = "0.1.0"
dependencies = [
 "askama",
 "axum",
 "clap",
 "futures",
 "git2",
 "humantime",
 "owning_ref",
 "path-clean",
 "time",
 "tokio",
 "tower",
 "tower-layer",
 "tower-service",
 "tracing",
 "tracing-subscriber",
 "uuid",
]

[[package]]
name = "ryu"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"

[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"

[[package]]
name = "serde"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_json"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
 "form_urlencoded",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
 "lazy_static",
]

[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
 "libc",
]

[[package]]
name = "slab"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"

[[package]]
name = "smallvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"

[[package]]
name = "socket2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [
 "libc",
 "winapi",
]

[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"

[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"

[[package]]
name = "syn"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "sync_wrapper"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"

[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
 "winapi-util",
]

[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"

[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
 "once_cell",
]

[[package]]
name = "time"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
dependencies = [
 "libc",
 "num_threads",
]

[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
 "tinyvec_macros",
]

[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "tokio"
version = "1.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
dependencies = [
 "bytes",
 "libc",
 "memchr",
 "mio",
 "num_cpus",
 "once_cell",
 "parking_lot",
 "pin-project-lite",
 "signal-hook-registry",
 "socket2",
 "tokio-macros",
 "winapi",
]

[[package]]
name = "tokio-macros"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
 "serde",
]

[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
 "futures-core",
 "futures-util",
 "pin-project",
 "pin-project-lite",
 "tokio",
 "tower-layer",
 "tower-service",
 "tracing",
]

[[package]]
name = "tower-http"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
dependencies = [
 "bitflags",
 "bytes",
 "futures-core",
 "futures-util",
 "http",
 "http-body",
 "http-range-header",
 "pin-project-lite",
 "tower",
 "tower-layer",
 "tower-service",
]

[[package]]
name = "tower-layer"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"

[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"

[[package]]
name = "tracing"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [
 "cfg-if",
 "log",
 "pin-project-lite",
 "tracing-attributes",
 "tracing-core",
]

[[package]]
name = "tracing-attributes"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tracing-core"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
dependencies = [
 "once_cell",
 "valuable",
]

[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
 "lazy_static",
 "log",
 "tracing-core",
]

[[package]]
name = "tracing-subscriber"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59"
dependencies = [
 "ansi_term",
 "sharded-slab",
 "smallvec",
 "thread_local",
 "tracing-core",
 "tracing-log",
]

[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"

[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
 "version_check",
]

[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"

[[package]]
name = "unicode-ident"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"

[[package]]
name = "unicode-normalization"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [
 "tinyvec",
]

[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
 "form_urlencoded",
 "idna",
 "matches",
 "percent-encoding",
]

[[package]]
name = "uuid"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
dependencies = [
 "getrandom",
]

[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"

[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
 "log",
 "try-lock",
]

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

[[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.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
 "winapi",
]

[[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-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
 "windows_aarch64_msvc",
 "windows_i686_gnu",
 "windows_i686_msvc",
 "windows_x86_64_gnu",
 "windows_x86_64_msvc",
]

[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"

[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"

[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"

[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"

[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3a8dfe2 100644
--- /dev/null
+++ a/Cargo.toml
@@ -1,0 +1,24 @@
[package]
name = "rgit"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
askama = "0.11"
axum = "0.5"
clap = { version = "3.2", features = ["cargo"] }
futures = "0.3"
git2 = "0.14"
humantime = "2.1"
path-clean = "0.1"
owning_ref = "0.4"
time = "0.3"
tokio = { version = "1.19", features = ["full"] }
tower = "0.4"
tower-service = "0.3"
tower-layer = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1.1", features = ["v4"] }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8c3bdb1 100644
--- /dev/null
+++ a/LICENSE
@@ -1,0 +1,14 @@
           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                   Version 2, December 2004
 
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
 
           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

 0. You just DO WHAT THE FUCK YOU WANT TO.

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..801462c 100644
--- /dev/null
+++ a/README.md
@@ -1,0 +1,4 @@
# rgit

A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, git2 and
Askama. No more CGI, how exciting.
diff --git a/src/git.rs b/src/git.rs
new file mode 100644
index 0000000..d644fdf 100644
--- /dev/null
+++ a/src/git.rs
@@ -1,0 +1,136 @@
use std::{borrow::Cow, collections::BTreeMap, fmt::Display, path::Path, time::Duration};

use git2::{Repository, Signature};
use owning_ref::OwningHandle;
use time::OffsetDateTime;

pub type RepositoryMetadataList = BTreeMap<Option<String>, Vec<RepositoryMetadata>>;

#[derive(Debug)]
pub struct RepositoryMetadata {
    pub name: String,
    pub description: Option<Cow<'static, str>>,
    pub owner: Option<String>,
    pub last_modified: Duration,
}

pub struct CommitUser<'a>(Signature<'a>);

impl CommitUser<'_> {
    pub fn name(&self) -> &str {
        self.0.name().unwrap()
    }

    pub fn email(&self) -> &str {
        self.0.email().unwrap()
    }

    pub fn time(&self) -> String {
        OffsetDateTime::from_unix_timestamp(self.0.when().seconds())
            .unwrap()
            .to_string()
    }
}

pub struct Commit(OwningHandle<Box<Repository>, Box<git2::Commit<'static>>>);

impl Commit {
    pub fn author(&self) -> CommitUser<'_> {
        CommitUser(self.0.author())
    }

    pub fn committer(&self) -> CommitUser<'_> {
        CommitUser(self.0.committer())
    }

    pub fn oid(&self) -> impl Display {
        self.0.id()
    }

    pub fn tree(&self) -> impl Display {
        self.0.tree_id()
    }

    pub fn parents(&self) -> impl Iterator<Item = impl Display + '_> {
        self.0.parent_ids()
    }

    pub fn summary(&self) -> &str {
        self.0.summary().unwrap()
    }

    pub fn body(&self) -> &str {
        self.0.message().unwrap()
    }
}

pub fn get_latest_commit(path: &Path) -> Commit {
    let repo = Repository::open_bare(path).unwrap();

    let commit = OwningHandle::new_with_fn(Box::new(repo), |v| {
        let head = unsafe { (*v).head().unwrap() };
        Box::new(head.peel_to_commit().unwrap())
    });

    // TODO: we can cache this
    Commit(commit)
}

pub fn fetch_repository_metadata() -> RepositoryMetadataList {
    let start = Path::new("../test-git").canonicalize().unwrap();

    let mut repos: RepositoryMetadataList = RepositoryMetadataList::new();
    fetch_repository_metadata_impl(&start, &start, &mut repos);
    repos
}

fn fetch_repository_metadata_impl(
    start: &Path,
    current: &Path,
    repos: &mut RepositoryMetadataList,
) {
    let dirs = std::fs::read_dir(current)
        .unwrap()
        .map(|v| v.unwrap().path())
        .filter(|path| path.is_dir());

    for dir in dirs {
        let repository = match Repository::open_bare(&dir) {
            Ok(v) => v,
            Err(_e) => {
                fetch_repository_metadata_impl(start, &dir, repos);
                continue;
            }
        };

        let repo_path = Some(
            current
                .strip_prefix(start)
                .unwrap()
                .to_string_lossy()
                .into_owned(),
        )
        .filter(|v| !v.is_empty());
        let repos = repos.entry(repo_path).or_default();

        let description = std::fs::read_to_string(dir.join("description"))
            .map(Cow::Owned)
            .ok();
        let last_modified = std::fs::metadata(&dir).unwrap().modified().unwrap();
        let owner = repository.config().unwrap().get_string("gitweb.owner").ok();

        repos.push(RepositoryMetadata {
            name: dir
                .components()
                .last()
                .unwrap()
                .as_os_str()
                .to_string_lossy()
                .into_owned(),
            description,
            owner,
            last_modified: (OffsetDateTime::now_utc() - OffsetDateTime::from(last_modified))
                .unsigned_abs(),
        });
    }
}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..fd2a40e 100644
--- /dev/null
+++ a/src/main.rs
@@ -1,0 +1,48 @@
#![deny(clippy::pedantic)]

use axum::{
    body::Body, handler::Handler, http::HeaderValue, response::Response, routing::get, Router,
};
use tower_layer::layer_fn;

use crate::{
    git::{fetch_repository_metadata, get_latest_commit},
    layers::logger::LoggingMiddleware,
};

mod git;
mod layers;
mod methods;

const CRATE_VERSION: &str = clap::crate_version!();

#[tokio::main]
async fn main() {
    let subscriber = tracing_subscriber::fmt();
    #[cfg(debug_assertions)]
    let subscriber = subscriber.pretty();
    subscriber.init();

    let app = Router::new()
        .route("/", get(methods::index::handle))
        .route(
            "/style.css",
            get(static_css(include_bytes!("../statics/style.css"))),
        )
        .fallback(methods::repo::service.into_service())
        .layer(layer_fn(LoggingMiddleware));

    axum::Server::bind(&"127.0.0.1:3333".parse().unwrap())
        .serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
        .await
        .unwrap();
}

fn static_css(content: &'static [u8]) -> impl Handler<()> {
    move || async move {
        let mut resp = Response::new(Body::from(content));
        resp.headers_mut()
            .insert("Content-Type", HeaderValue::from_static("text/css"));
        resp
    }
}
diff --git a/statics/style.css b/statics/style.css
new file mode 100644
index 0000000..57cb54c 100644
--- /dev/null
+++ a/statics/style.css
@@ -1,0 +1,70 @@
body {
    font-family: sans-serif;
    font-size: 10pt;
}

main {
    padding: 2rem;
    margin: 0;
    border-bottom: solid 3px #ccc;
}

header {
    border-bottom: solid 1px #ccc;
}

footer {
    margin-top: 0.5em;
    text-align: center;
    font-size: 80%;
    color: #ccc;
}

a {
    text-decoration: none;
    color: blue;
}

a:hover {
    text-decoration: underline;
}

table { border-collapse: collapse; }
table.repositories { width: 100%; }
table.repositories a { color: black; }
table.repositories a:hover { color: #00f; }
table th { text-align: left; }
.repo-section { font-style: italic; color: #888; }

table.repositories tbody tr:nth-child(odd) {
    background: #f7f7f7;
}

table tbody tr.has-parent td:first-of-type {
    padding-left: 1rem;
}

table pre { margin: 0; }

table.commit-info td, table.commit-info th { padding: 0.1em 1em 0.1em 0.1em; }

nav {
    margin-top: 2rem;
    border-bottom: solid 3px #ccc;
}

nav a {
    padding: 2px 0.75em;
    color: #777;
    font-size: 110%;
    text-decoration: none;
}

nav a:hover {
    text-decoration: underline;
}

nav a.active {
    color: #000;
    background-color: #ccc;
}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..7f9c081 100644
--- /dev/null
+++ a/templates/base.html
@@ -1,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}rgit{% endblock %}</title>
    {% block head %}{% endblock %}

    <link rel="stylesheet" type="text/css" href="/style.css" />
</head>

<body>
<header>
    <h1>{% block header %}Git repository browser{% endblock %}</h1>
</header>

{% block nav %}{% endblock %}

<main>
    {% block content %}{% endblock %}
</main>

<footer>
    generated by <a href="https://github.com/w4/rgit" target="_blank">rgit</a> v{{ crate::CRATE_VERSION }} at {{ time::OffsetDateTime::now_utc().to_string() }}
</footer>
</body>
</html>
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..b0fb288 100644
--- /dev/null
+++ a/templates/index.html
@@ -1,0 +1,55 @@
{% extends "base.html" %}

{% block title %}Index{% endblock %}

{% block content %}
    <table class="repositories">
        <thead>
        <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Owner</th>
            <th>Idle</th>
        </tr>
        </thead>

        <tbody>
        {% for (path, repositories) in repositories %}
            {% if let Some(path) = path %}
                <tr><td class="repo-section" colspan="4">{{ path }}</td></tr>
            {% endif %}

            {%for repository in repositories %}
                <tr class="{% if path.is_some() %}has-parent{% endif %}">
                    <td>
                        <a href="/{% if let Some(path) = path %}{{ path }}/{% endif %}{{ repository.name }}">
                            {{ repository.name }}
                        </a>
                    </td>
                    <td>
                        <a href="/{% if let Some(path) = path %}{{ path }}/{% endif %}{{ repository.name }}">
                            {% if let Some(description) = repository.description %}
                                {{ description }}
                            {% else %}
                                Unnamed repository; edit this file 'description' to name the repository.
                            {% endif %}
                        </a>
                    </td>
                    <td>
                        <a href="/{% if let Some(path) = path %}{{ path }}/{% endif %}{{ repository.name }}">
                            {% if let Some(owner) = repository.owner %}
                                {{ owner }}
                            {% endif %}
                        </a>
                    </td>
                    <td>
                        <a href="/{% if let Some(path) = path %}{{ path }}/{% endif %}{{ repository.name }}">
                            {{ humantime::format_duration(repository.last_modified.clone()) }}
                        </a>
                    </td>
                </tr>
            {% endfor %}
        {% endfor %}
        </tbody>
    </table>
{% endblock %}
diff --git a/src/layers/logger.rs b/src/layers/logger.rs
new file mode 100644
index 0000000..33c87b9 100644
--- /dev/null
+++ a/src/layers/logger.rs
@@ -1,0 +1,134 @@
//! Logs each and every request out in a format similar to that of Apache's logs.


use std::{
    fmt::Debug,
    net::SocketAddr,
    task::{Context, Poll},
    time::Instant,
};

use axum::{
    extract,
    http::{HeaderValue, Method, Request, Response},
};
use futures::future::{Future, FutureExt, Join, Map, Ready};
use tower_service::Service;
use tracing::{error, info, instrument::Instrumented, Instrument, Span};
use uuid::Uuid;

use super::UnwrapInfallible;

pub trait GenericError: std::error::Error + Debug + Send + Sync {}

#[derive(Clone)]
pub struct LoggingMiddleware<S>(pub S);

impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for LoggingMiddleware<S>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = std::convert::Infallible>
        + Clone
        + Send
        + 'static,
    S::Future: Send + 'static,
    S::Response: Default + Debug,
    ReqBody: Send + Debug + 'static,
    ResBody: Default + Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Map<
        Join<Instrumented<S::Future>, Ready<PendingLogMessage>>,
        fn((<S::Future as Future>::Output, PendingLogMessage)) -> <S::Future as Future>::Output,
    >;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.0.poll_ready(cx)
    }

    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
        let request_id = Uuid::new_v4();
        let span = tracing::info_span!("web", "request_id" = request_id.to_string().as_str());

        let log_message = PendingLogMessage {
            span: span.clone(),
            request_id,
            ip: req
                .extensions()
                .get::<extract::ConnectInfo<std::net::SocketAddr>>()
                .map_or_else(|| "0.0.0.0:0".parse().unwrap(), |v| v.0),
            method: req.method().clone(),
            uri: req.uri().path().to_string(),
            start: Instant::now(),
            user_agent: req.headers().get(axum::http::header::USER_AGENT).cloned(),
        };

        // this is infallible because of the type of S::Error
        futures::future::join(
            self.0.call(req).instrument(span),
            futures::future::ready(log_message),
        )
        .map(|(response, pending_log_message)| {
            let mut response = response.unwrap_infallible();
            pending_log_message.log(&response);
            response.headers_mut().insert(
                "X-Request-ID",
                HeaderValue::try_from(pending_log_message.request_id.to_string()).unwrap(),
            );
            Ok(response)
        })
    }
}

pub struct PendingLogMessage {
    span: Span,
    request_id: Uuid,
    ip: SocketAddr,
    method: Method,
    uri: String,
    start: Instant,
    user_agent: Option<HeaderValue>,
}

impl PendingLogMessage {
    pub fn log<ResBody>(&self, response: &Response<ResBody>) {
        let _enter = self.span.enter();

        if response.status().is_server_error() {
            error!(
                "{ip} - \"{method} {uri}\" {status} {duration:?} \"{user_agent}\" \"{error:?}\"",
                ip = self.ip,
                method = self.method,
                uri = self.uri,
                status = response.status().as_u16(),
                duration = self.start.elapsed(),
                user_agent = self
                    .user_agent
                    .as_ref()
                    .and_then(|v| v.to_str().ok())
                    .unwrap_or("unknown"),
                error = match response.extensions().get::<Box<dyn GenericError>>() {
                    Some(e) => Err(e),
                    None => Ok(()),
                }
            );
        } else {
            info!(
                "{ip} - \"{method} {uri}\" {status} {duration:?} \"{user_agent}\" \"{error:?}\"",
                ip = self.ip,
                method = self.method,
                uri = self.uri,
                status = response.status().as_u16(),
                duration = self.start.elapsed(),
                user_agent = self
                    .user_agent
                    .as_ref()
                    .and_then(|v| v.to_str().ok())
                    .unwrap_or("unknown"),
                error = match response.extensions().get::<Box<dyn GenericError>>() {
                    Some(e) => Err(e),
                    None => Ok(()),
                }
            );
        }
    }
}
diff --git a/src/layers/mod.rs b/src/layers/mod.rs
new file mode 100644
index 0000000..e92435f 100644
--- /dev/null
+++ a/src/layers/mod.rs
@@ -1,0 +1,19 @@
use std::convert::Infallible;

pub mod logger;

pub trait UnwrapInfallible<T> {
    fn unwrap_infallible(self) -> T;
}

impl<T> UnwrapInfallible<T> for Result<T, Infallible> {
    fn unwrap_infallible(self) -> T {
        self.unwrap()
    }
}

impl<T> UnwrapInfallible<T> for Result<T, &Infallible> {
    fn unwrap_infallible(self) -> T {
        self.unwrap()
    }
}
diff --git a/src/methods/index.rs b/src/methods/index.rs
new file mode 100644
index 0000000..9ca2503 100644
--- /dev/null
+++ a/src/methods/index.rs
@@ -1,0 +1,21 @@
use askama::Template;
use axum::response::Html;

use crate::{fetch_repository_metadata, git::RepositoryMetadataList};

#[allow(clippy::unused_async)]
pub async fn handle() -> Html<String> {
    #[derive(Template)]
    #[template(path = "index.html")]
    pub struct View {
        pub repositories: RepositoryMetadataList,
    }

    Html(
        View {
            repositories: fetch_repository_metadata(),
        }
        .render()
        .unwrap(),
    )
}
diff --git a/src/methods/mod.rs b/src/methods/mod.rs
new file mode 100644
index 0000000..5592c5f 100644
--- /dev/null
+++ a/src/methods/mod.rs
@@ -1,0 +1,2 @@
pub mod index;
pub mod repo;
diff --git a/src/methods/repo.rs b/src/methods/repo.rs
new file mode 100644
index 0000000..99e4b43 100644
--- /dev/null
+++ a/src/methods/repo.rs
@@ -1,0 +1,174 @@
use std::{
    ops::Deref,
    path::{Path, PathBuf},
};

use askama::Template;
use axum::{
    handler::Handler,
    http::Request,
    response::{Html, IntoResponse, Response},
    Extension,
};
use path_clean::PathClean;
use tower::{util::BoxCloneService, Service};

use crate::{get_latest_commit, git::Commit, layers::UnwrapInfallible};

#[derive(Clone)]
pub struct Repository(pub PathBuf);

impl Deref for Repository {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Clone)]
pub struct RepositoryPath(pub PathBuf);

impl Deref for RepositoryPath {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) -> Response {
    let mut uri_parts: Vec<&str> = request
        .uri()
        .path()
        .trim_start_matches('/')
        .trim_end_matches('/')
        .split('/')
        .collect();

    let mut service = match uri_parts.pop() {
        Some("about") => BoxCloneService::new(handle_about.into_service()),
        Some("refs") => BoxCloneService::new(handle_refs.into_service()),
        Some("log") => BoxCloneService::new(handle_log.into_service()),
        Some("tree") => BoxCloneService::new(handle_tree.into_service()),
        Some("commit") => BoxCloneService::new(handle_commit.into_service()),
        Some("diff") => BoxCloneService::new(handle_diff.into_service()),
        Some("stats") => BoxCloneService::new(handle_stats.into_service()),
        Some(v) => {
            uri_parts.push(v);
            BoxCloneService::new(handle_summary.into_service())
        }
        None => panic!("not found"),
    };

    let uri = uri_parts.into_iter().collect::<PathBuf>().clean();
    let path = Path::new("../test-git").canonicalize().unwrap().join(&uri);

    request.extensions_mut().insert(Repository(uri));
    request.extensions_mut().insert(RepositoryPath(path));

    service
        .call(request)
        .await
        .unwrap_infallible()
        .into_response()
}

#[allow(clippy::unused_async)]
pub async fn handle_summary(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/summary.html")]
    pub struct View {
        repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_log(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/log.html")]
    pub struct View {
        repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_refs(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/refs.html")]
    pub struct View {
        repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_about(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/about.html")]
    pub struct View {
        repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_commit(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/commit.html")]
    pub struct View {
        pub repo: Repository,
        pub commit: Commit,
    }

    Html(
        View {
            repo,
            commit: get_latest_commit(&repository_path),
        }
        .render()
        .unwrap(),
    )
}

#[allow(clippy::unused_async)]
pub async fn handle_tree(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/tree.html")]
    pub struct View {
        pub repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_diff(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/diff.html")]
    pub struct View {
        pub repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}

#[allow(clippy::unused_async)]
pub async fn handle_stats(Extension(repo): Extension<Repository>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "repo/stats.html")]
    pub struct View {
        pub repo: Repository,
    }

    Html(View { repo }.render().unwrap())
}
diff --git a/templates/repo/about.html b/templates/repo/about.html
new file mode 100644
index 0000000..f4d3acc 100644
--- /dev/null
+++ a/templates/repo/about.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block about_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/base.html b/templates/repo/base.html
new file mode 100644
index 0000000..24e1590 100644
--- /dev/null
+++ a/templates/repo/base.html
@@ -1,0 +1,14 @@
{% extends "../base.html" %}

{% block nav %}
<nav>
    <a href="/{{ repo.display() }}/about" class="{% block about_nav_class %}{% endblock %}">about</a>
    <a href="/{{ repo.display() }}" class="{% block summary_nav_class %}{% endblock %}">summary</a>
    <a href="/{{ repo.display() }}/refs" class="{% block refs_nav_class %}{% endblock %}">refs</a>
    <a href="/{{ repo.display() }}/log" class="{% block log_nav_class %}{% endblock %}">log</a>
    <a href="/{{ repo.display() }}/tree" class="{% block tree_nav_class %}{% endblock %}">tree</a>
    <a href="/{{ repo.display() }}/commit" class="{% block commit_nav_class %}{% endblock %}">commit</a>
    <a href="/{{ repo.display() }}/diff" class="{% block diff_nav_class %}{% endblock %}">diff</a>
    <a href="/{{ repo.display() }}/stats" class="{% block stats_nav_class %}{% endblock %}">stats</a>
</nav>
{% endblock %}
diff --git a/templates/repo/commit.html b/templates/repo/commit.html
new file mode 100644
index 0000000..f4e13b7 100644
--- /dev/null
+++ a/templates/repo/commit.html
@@ -1,0 +1,38 @@
{% extends "repo/base.html" %}

{% block commit_nav_class %}active{% endblock %}

{% block content %}
<table class="commit-info">
    <tbody>
    <tr>
        <th>author</th>
        <td>{{ commit.author().name() }} &lt;{{ commit.author().email() }}&gt;</td>
        <td>{{ commit.author().time() }}</td>
    </tr>
    <tr>
        <th>committer</th>
        <td>{{ commit.committer().name() }} &lt;{{ commit.committer().email() }}&gt;</td>
        <td>{{ commit.committer().time() }}</td>
    </tr>
    <tr>
        <th>commit</th>
        <td colspan="2"><pre>{{ commit.oid() }}</pre></td>
    </tr>
    <tr>
        <th>tree</th>
        <td colspan="2"><pre>{{ commit.tree() }}</pre></td>
    </tr>
    {% for parent in commit.parents() %}
    <tr>
        <th>parent</th>
        <td colspan="2"><pre>{{ parent }}</pre></td>
    </tr>
    {% endfor %}
    </tbody>
</table>

<h2>{{ commit.summary() }}</h2>
<pre>{{ commit.body() }}</pre>

{% endblock %}
diff --git a/templates/repo/diff.html b/templates/repo/diff.html
new file mode 100644
index 0000000..aa0b72d 100644
--- /dev/null
+++ a/templates/repo/diff.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block diff_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/log.html b/templates/repo/log.html
new file mode 100644
index 0000000..a50ecab 100644
--- /dev/null
+++ a/templates/repo/log.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block log_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/refs.html b/templates/repo/refs.html
new file mode 100644
index 0000000..efdfb77 100644
--- /dev/null
+++ a/templates/repo/refs.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block refs_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/stats.html b/templates/repo/stats.html
new file mode 100644
index 0000000..e917c8f 100644
--- /dev/null
+++ a/templates/repo/stats.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block stats_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/summary.html b/templates/repo/summary.html
new file mode 100644
index 0000000..d72f5be 100644
--- /dev/null
+++ a/templates/repo/summary.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block summary_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}
diff --git a/templates/repo/tree.html b/templates/repo/tree.html
new file mode 100644
index 0000000..1fac543 100644
--- /dev/null
+++ a/templates/repo/tree.html
@@ -1,0 +1,6 @@
{% extends "repo/base.html" %}

{% block tree_nav_class %}active{% endblock %}

{% block content %}
{% endblock %}