gitscope
← rgit / Diff · 4b825dc → afaae25
Changes 4b825dc..afaae25
Patch commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d81f12e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/.idea
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..3c0b44e
--- /dev/null
+++ b/Cargo.lock
@@ -0,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
--- /dev/null
+++ b/Cargo.toml
@@ -0,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
--- /dev/null
+++ b/LICENSE
@@ -0,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
--- /dev/null
+++ b/README.md
@@ -0,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
--- /dev/null
+++ b/src/git.rs
@@ -0,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/layers/logger.rs b/src/layers/logger.rs
new file mode 100644
index 0000000..33c87b9
--- /dev/null
+++ b/src/layers/logger.rs
@@ -0,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
--- /dev/null
+++ b/src/layers/mod.rs
@@ -0,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/main.rs b/src/main.rs
new file mode 100644
index 0000000..fd2a40e
--- /dev/null
+++ b/src/main.rs
@@ -0,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/src/methods/index.rs b/src/methods/index.rs
new file mode 100644
index 0000000..9ca2503
--- /dev/null
+++ b/src/methods/index.rs
@@ -0,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
--- /dev/null
+++ b/src/methods/mod.rs
@@ -0,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
--- /dev/null
+++ b/src/methods/repo.rs
@@ -0,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/statics/style.css b/statics/style.css
new file mode 100644
index 0000000..57cb54c
--- /dev/null
+++ b/statics/style.css
@@ -0,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
--- /dev/null
+++ b/templates/base.html
@@ -0,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>
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..b0fb288
--- /dev/null
+++ b/templates/index.html
@@ -0,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 %}
\ No newline at end of file
diff --git a/templates/repo/about.html b/templates/repo/about.html
new file mode 100644
index 0000000..f4d3acc
--- /dev/null
+++ b/templates/repo/about.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block about_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/base.html b/templates/repo/base.html
new file mode 100644
index 0000000..24e1590
--- /dev/null
+++ b/templates/repo/base.html
@@ -0,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
--- /dev/null
+++ b/templates/repo/commit.html
@@ -0,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 %}
\ No newline at end of file
diff --git a/templates/repo/diff.html b/templates/repo/diff.html
new file mode 100644
index 0000000..aa0b72d
--- /dev/null
+++ b/templates/repo/diff.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block diff_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/log.html b/templates/repo/log.html
new file mode 100644
index 0000000..a50ecab
--- /dev/null
+++ b/templates/repo/log.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block log_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/refs.html b/templates/repo/refs.html
new file mode 100644
index 0000000..efdfb77
--- /dev/null
+++ b/templates/repo/refs.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block refs_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/stats.html b/templates/repo/stats.html
new file mode 100644
index 0000000..e917c8f
--- /dev/null
+++ b/templates/repo/stats.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block stats_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/summary.html b/templates/repo/summary.html
new file mode 100644
index 0000000..d72f5be
--- /dev/null
+++ b/templates/repo/summary.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block summary_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/tree.html b/templates/repo/tree.html
new file mode 100644
index 0000000..1fac543
--- /dev/null
+++ b/templates/repo/tree.html
@@ -0,0 +1,6 @@
+{% extends "repo/base.html" %}
+
+{% block tree_nav_class %}active{% endblock %}
+
+{% block content %}
+{% endblock %}
\ No newline at end of file