bile |
| a simple self-hosted git server |
| git clone https://git.wayver.dev/bile |
| README | tree | log | refs |
Commit: 6e060abc1f25c2e1b79fe06bfa8b72cd26952ee1 (tree)
Parent: f3f2b40f0ffae5de2e6d3f661e32b582274bae49 (tree)
Author: wayverd
Date: 2026 M02 19, Thu 17:51:47 -0500
55 files changed; 1661 insertions 1191 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 488a245..0380a46 100644
]
[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "axum-extra"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "axum-response-cache"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
"anyhow",
"askama",
"axum",
+ "axum-extra",
"axum-response-cache",
"clap",
"comrak",
"jiff",
"mimalloc",
"mime",
+ "moka",
"num-conv",
"serde",
"syntect",
]
[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
name = "fancy-regex"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
name = "git2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
+ "serde",
+ "serde_core",
]
[[package]]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "getrandom",
+ "getrandom 0.3.4",
"libc",
]
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "moka"
+version = "0.12.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
+dependencies = [
+ "async-lock",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "equivalent",
+ "event-listener",
+ "futures-util",
+ "parking_lot",
+ "portable-atomic",
+ "smallvec",
+ "tagptr",
+ "uuid",
+]
+
+[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
+name = "onig"
+version = "6.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
"flate2",
"fnv",
"once_cell",
+ "onig",
"plist",
"regex-syntax",
"serde",
]
[[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
+name = "uuid"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
+dependencies = [
+ "getrandom 0.4.1",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
]
[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
[[package]]
name = "writeable"
diff --git a/Cargo.toml b/Cargo.toml
index 418b3c3..f33d8f5 100644
anyhow = "=1.0.101"
askama = "=0.15.4"
axum = { version = "=0.8.8", features = ["tracing"] }
+axum-extra = "=0.12.5"
axum-response-cache = "=0.4.0"
clap = { version = "=4.5.59", features = ["derive", "string"] }
comrak = { version = "=0.50.0", default-features = false }
jiff = "=0.2.20"
mimalloc = "=0.1.48"
mime = "=0.3.17"
+moka = { version = "=0.12.13", features = ["future"] }
num-conv = "=0.2.0"
serde = { version = "=1.0.228", features = ["derive"] }
-syntect = { version = "=5.3.0", default-features = false, features = ["default-fancy"] }
+syntect = { version = "=5.3.0", default-features = false, features = ["default-onig"] }
thiserror = "=2.0.18"
tokio = { version = "=1.49.0", features = ["macros", "rt-multi-thread", "signal", "fs"] }
tower = "=0.5.3"
[profile.release]
codegen-units = 1
lto = true
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
diff --git a/assets/style.css b/assets/style.css
index b403d04..f2deae8 100644
width: 100%;
}
+#log .commit-author-email {
+ text-wrap: nowrap;
+}
#log .commit-files-modified,
#log .commit-lines-added,
#log .commit-lines-removed {
diff --git a/src/config.rs b/src/config.rs
index d626c03..0f28d85 100644
}
impl Config {
- pub fn load() -> crate::utils::error::Result<Self> {
+ pub fn load() -> crate::error::Result<Self> {
let config: Self = Figment::new()
.merge(Serialized::defaults(Self::parse()))
.merge(Toml::file("bile.toml"))
Ok(config)
}
- pub fn finalize(self) -> crate::utils::error::Result<Self> {
+ pub fn finalize(self) -> crate::error::Result<Self> {
Ok(Self {
port: self.port,
project_root: self.project_root.canonicalize()?,
diff --git a/src/utils/error.rs b/src/error.rs
similarity index 91%
rename from src/utils/error.rs
rename to src/error.rs
index a21e323..3d7190b 100644
use std::{convert, fmt};
-use axum::response;
-use http::StatusCode;
use tracing_error::SpanTrace;
pub type Result<T, E = Error> = std::result::Result<T, E>;
}
}
-impl response::IntoResponse for Error {
- fn into_response(self) -> response::Response {
- super::Error::Failure {
- status: StatusCode::INTERNAL_SERVER_ERROR,
- err: self,
- }
- .into_response()
- }
-}
-
pub trait Context<T, E> {
fn context<C>(self, context: C) -> Result<T, Error>
where
diff --git a/src/utils/git/branch.rs b/src/git/branch.rs
similarity index 84%
rename from src/utils/git/branch.rs
rename to src/git/branch.rs
index e591b5d..f0c6751 100644
use git2::{Branch, BranchType, Reference};
-use crate::utils::git::Repository;
+use crate::git::Repository;
impl Repository {
#[tracing::instrument(skip_all)]
- pub fn branches(&self) -> crate::utils::error::Result<Vec<Reference<'_>>> {
+ pub(crate) fn branches(&self) -> crate::error::Result<Vec<Reference<'_>>> {
let references = self.inner.references()?;
let branches = references
}
#[tracing::instrument(skip_all)]
- pub fn branches_of_type(
+ pub(crate) fn branches_of_type(
&self,
typ: BranchType,
- ) -> crate::utils::error::Result<Vec<Branch<'_>>> {
+ ) -> crate::error::Result<Vec<Branch<'_>>> {
let branches = self
.inner
.branches(Some(typ))?
diff --git a/src/utils/git/commit.rs b/src/git/commit.rs
similarity index 83%
rename from src/utils/git/commit.rs
rename to src/git/commit.rs
index 6d79c61..4f2b456 100644
use git2::{Commit, Diff, DiffOptions, DiffStats, Sort, Tree};
-use crate::utils::{
- error::{Context as _, Result},
- git::Repository,
-};
+use crate::{error::Context as _, error::Result, git::Repository, http::extractor::ObjectName};
impl Repository {
#[tracing::instrument(skip_all)]
- pub fn commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
+ pub(crate) fn commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
let obj = match self.inner.revparse_single(spec) {
Ok(obj) => obj,
Err(err) => {
}
#[tracing::instrument(skip_all)]
- pub fn commit_diff(&self, commit: &Commit<'_>) -> Result<Diff<'_>> {
+ pub(crate) fn commit_diff(&self, commit: &Commit<'_>) -> Result<Diff<'_>> {
let mut options = DiffOptions::new();
// This is identical to getting "commit^" and on merges this will be the
}
#[tracing::instrument(skip_all)]
- pub fn commit_stats(&self, commit: &Commit<'_>) -> Result<DiffStats> {
+ pub(crate) fn commit_stats(&self, commit: &Commit<'_>) -> Result<DiffStats> {
let diff = self.commit_diff(commit)?;
let stats = diff.stats()?;
}
#[tracing::instrument(skip_all)]
- pub fn commit_tree(&self, spec: &str) -> Result<Option<(Commit<'_>, Tree<'_>)>> {
+ pub(crate) fn commit_tree(&self, spec: &str) -> Result<Option<(Commit<'_>, Tree<'_>)>> {
let Some(commit) = self.commit(spec)? else {
return Ok(None);
};
}
#[tracing::instrument(skip_all)]
- pub fn commits(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
+ pub(crate) fn commits(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
if self.is_shallow() {
return self.commits_shallow();
}
}
#[tracing::instrument(skip_all)]
- pub fn commits_full(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
+ pub(crate) fn commits_full(
+ &self,
+ spec: &str,
+ amount: usize,
+ ) -> Result<Option<Vec<Commit<'_>>>> {
let mut revwalk = self.inner.revwalk()?;
let Some(commit) = self.commit(spec)? else {
}
#[tracing::instrument(skip_all)]
- pub fn commits_for_obj(
+ pub(crate) fn commits_for_obj(
&self,
spec: &str,
amount: usize,
- obj: Option<&str>,
+ obj: Option<&ObjectName>,
) -> Result<Option<Vec<Commit<'_>>>> {
- tracing::info!("is_shallow");
if self.is_shallow() {
return self
.commits_shallow()
let mut revwalk = self.inner.revwalk().context("failed to create revwalk")?;
let Some(commit) = self.commit(spec).context("failed to get commit")? else {
- tracing::info!("commit None");
return Ok(None);
};
let commits =
revwalk.filter_map(|oid| oid.ok().and_then(|oid| self.inner.find_commit(oid).ok()));
- let Some(Ok(path)) = obj.map(CString::new) else {
+ let Some(Ok(path)) = obj.map(|n| n.0.as_str()).map(CString::new) else {
return Ok(Some(commits.take(amount).collect()));
};
}
#[tracing::instrument(skip_all)]
- pub fn commits_shallow(&self) -> Result<Option<Vec<Commit<'_>>>> {
+ pub(crate) fn commits_shallow(&self) -> Result<Option<Vec<Commit<'_>>>> {
tracing::warn!("repository {:?} is only a shallow clone", self.inner.path());
let commits = self
.inner
}
#[tracing::instrument(skip_all)]
- pub fn file_last_commit<P: git2::IntoCString>(
+ pub(crate) fn file_last_commit<P: git2::IntoCString>(
&self,
spec: &str,
path: P,
diff --git a/src/utils/git/core.rs b/src/git/core.rs
similarity index 86%
rename from src/utils/git/core.rs
rename to src/git/core.rs
index 1ef7db0..43e1520 100644
use std::path::Path;
use git2::{Reference, Time};
+use syntect::parsing::SyntaxSet;
-use crate::utils::{
- error::{Context as _, Result},
- git::Repository,
-};
+use crate::{error::Context as _, error::Result, git::Repository, http::extractor::Ref};
impl Repository {
#[must_use]
- pub fn description(&self) -> String {
+ pub(crate) fn description(&self) -> String {
let content = std::fs::read_to_string(self.path().join("description")).unwrap_or_default();
let first = content.lines().next().unwrap_or_default();
}
#[tracing::instrument(skip_all)]
- pub fn head(&self) -> Result<Reference<'_>> {
+ pub(crate) fn head(&self) -> Result<Reference<'_>> {
let head = self.inner.head()?;
Ok(head)
}
#[tracing::instrument(skip_all)]
- pub fn is_empty(&self) -> Result<bool> {
+ pub(crate) fn is_empty(&self) -> Result<bool> {
Ok(self.inner.is_empty()?)
}
#[must_use]
- pub fn is_shallow(&self) -> bool {
+ pub(crate) fn is_shallow(&self) -> bool {
self.inner.is_shallow()
}
#[tracing::instrument(skip_all)]
- pub fn last_modified(&self) -> Result<Time> {
+ pub(crate) fn last_modified(&self) -> Result<Time> {
let head = self.head()?;
let commit = head.peel_to_commit()?;
let time = commit.committer().when();
Ok(time)
}
- pub fn name(&self) -> Option<&str> {
+ pub(crate) fn name(&self) -> Option<&str> {
self.inner
.workdir()
// use the path for bare repositories
}
#[must_use]
- pub fn owner(&self) -> String {
+ pub(crate) fn owner(&self) -> String {
self.inner
.config()
.and_then(|config| config.get_string("gitweb.owner"))
}
#[must_use]
- pub fn path(&self) -> &Path {
+ pub(crate) fn path(&self) -> &Path {
self.inner.path()
}
#[must_use]
- pub fn readme(&self) -> String {
+ pub(crate) fn readme(&self, syntaxes: &SyntaxSet) -> String {
use askama::filters::Escaper as _;
enum ReadmeFormat {
// already is HTML
ReadmeFormat::Html => text.to_string(),
// render Markdown to HTML
- ReadmeFormat::Markdown => crate::utils::markdown::render(text),
+ ReadmeFormat::Markdown => crate::utils::markdown::render(syntaxes, text),
}
})
.unwrap_or_default()
}
#[tracing::instrument(skip_all)]
- pub fn ref_or_head_shorthand(&self, r#ref: Option<&str>) -> Result<String> {
+ pub(crate) fn ref_or_head_shorthand(&self, r#ref: Option<&Ref>) -> Result<String> {
let head = self.head()?;
let spec = r#ref
- .map(str::to_string)
+ .map(|r| r.0.clone())
.or_else(move || head.shorthand().map(str::to_string))
.context("failed to get repo ref spec")?;
diff --git a/src/utils/git/mod.rs b/src/git/mod.rs
similarity index 88%
rename from src/utils/git/mod.rs
rename to src/git/mod.rs
index 49498a6..fa5b97b 100644
mod tag;
mod tree;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use git2::{Object, Signature};
-use crate::utils::error::{Context as _, Result};
+use crate::{config::Config, error::Context as _, error::Result, http::extractor::RepoName};
-pub struct TagEntry {
+pub(crate) struct TagEntry {
pub link: String,
pub tag: String,
pub message: String,
}
}
-pub struct Repository {
+pub(crate) struct Repository {
inner: git2::Repository,
}
impl Repository {
#[tracing::instrument(skip_all)]
- pub fn open<P>(path: P) -> Result<Option<Self>>
- where
- P: AsRef<Path>,
- {
- let config = crate::config();
-
- let path = path.as_ref();
+ pub(crate) fn open(config: &Config, name: &RepoName) -> Result<Option<Self>> {
+ Self::open_path(config, &PathBuf::from(&name.0))
+ }
+ pub(crate) fn open_path(config: &Config, path: &Path) -> Result<Option<Self>> {
let path = config.project_root.join(path);
if !path.exists() {
}
#[must_use]
- pub const fn as_inner(&self) -> &git2::Repository {
+ pub(crate) const fn as_inner(&self) -> &git2::Repository {
&self.inner
}
}
diff --git a/src/utils/git/tag.rs b/src/git/tag.rs
similarity index 85%
rename from src/utils/git/tag.rs
rename to src/git/tag.rs
index 3152ac6..67b8abd 100644
-use git2::{ObjectType, Tag};
+use git2::ObjectType;
-use crate::utils::{
+use crate::{
error::Result,
git::{Repository, TagEntry},
+ http::extractor::Tag,
};
impl Repository {
#[tracing::instrument(skip_all)]
- pub fn tag_entries(&self) -> Result<Vec<TagEntry>> {
+ pub(crate) fn tag_entries(&self) -> Result<Vec<TagEntry>> {
let mut tags = Vec::new();
self.inner.tag_foreach(|oid, name_bytes| {
}
#[tracing::instrument(skip_all)]
- pub fn tag(&self, spec: &str) -> Result<Tag<'_>> {
- let tag = self.inner.revparse_single(spec)?.peel_to_tag()?;
+ pub(crate) fn tag(&self, spec: &Tag) -> Result<git2::Tag<'_>> {
+ let tag = self.inner.revparse_single(&spec.0)?.peel_to_tag()?;
Ok(tag)
}
diff --git a/src/utils/git/tree.rs b/src/git/tree.rs
similarity index 78%
rename from src/utils/git/tree.rs
rename to src/git/tree.rs
index 7032199..dcf7584 100644
use std::path::Path;
+use anyhow::Context as _;
use git2::{Blob, Object, Tree};
-use crate::utils::{
- error::{Context as _, Result},
- git::Repository,
-};
+use crate::{error::Result, git::Repository};
impl Repository {
#[tracing::instrument(skip_all)]
- pub fn tree_blob(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Blob<'_>>> {
+ pub(crate) fn tree_blob(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Blob<'_>>> {
let Some(obj) = self.tree_object(tree, path)? else {
return Ok(None);
};
}
#[tracing::instrument(skip_all)]
- pub fn tree_object(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Object<'_>>> {
+ pub(crate) fn tree_object(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Object<'_>>> {
let entry = match tree
.get_path(path)
.context("failed to get object from tree")
diff --git a/src/handlers/git.rs b/src/handlers/git.rs
new file mode 100644
index 0000000..c593a78
+use axum::{
+ extract::{Path, State},
+ http::{HeaderValue, StatusCode, Uri, header},
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::RepoName,
+ response::{ErrorPage, Result},
+ },
+};
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(
+ state: State<BileState>,
+ uri: Uri,
+ Path(repo_name): Path<RepoName>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &uri, &repo_name))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+ state: State<BileState>,
+ uri: Uri,
+ Path((repo_name, _)): Path<(RepoName, String)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &uri, &repo_name))
+ .await
+}
+
+fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let path = uri
+ .path()
+ .strip_prefix(&format!("/{repo_name}/"))
+ .unwrap_or_default();
+
+ let path = repo.path().join(path);
+
+ // cant canonicalize if it doesnt exist
+ if !path.exists() {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ }
+
+ let path = path.canonicalize().context("canonicalize new path")?;
+
+ // that path got us outside of the repository structure somehow
+ if !path.starts_with(repo.path()) {
+ tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::FORBIDDEN)
+ .into_response());
+ }
+
+ // Either the requested resource does not exist or it is not
+ // a file, i.e. a directory.
+ if !path.is_file() {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ }
+
+ let body = std::fs::read(&path).context("reading file")?;
+
+ Ok((
+ StatusCode::OK,
+ [(
+ header::CONTENT_TYPE,
+ HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
+ )],
+ body,
+ )
+ .into_response())
+}
diff --git a/src/handlers/git.rs b/src/handlers/git.rs
deleted file mode 100644
index 21df48a..0000000
-use axum::{
- extract::Path,
- http::{HeaderValue, StatusCode, Uri, header},
- response::{IntoResponse as _, Response},
-};
-
-use crate::utils::{
- Error, Result, error::Context as _, extractor::repo_name_checks, git::Repository,
- spawn_blocking,
-};
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(uri: Uri, Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&uri, &repo_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(uri: Uri, Path((repo_name, _)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&uri, &repo_name).into_response()).await
-}
-
-fn inner(uri: &Uri, repo_name: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- let path = uri
- .path()
- .strip_prefix(&format!("/{repo_name}/"))
- .unwrap_or_default();
-
- let path = repo.path().join(path);
-
- // cant canonicalize if it doesnt exist
- if !path.exists() {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "This page does not exist.",
- ));
- }
-
- let path = path.canonicalize().context("canonicalize new path")?;
-
- // that path got us outside of the repository structure somehow
- if !path.starts_with(repo.path()) {
- tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
- return Err(Error::new(
- StatusCode::FORBIDDEN,
- "You do not have access to this file.",
- ));
- }
-
- // Either the requested resource does not exist or it is not
- // a file, i.e. a directory.
- if !path.is_file() {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "This page does not exist.",
- ));
- }
-
- let body = std::fs::read(&path).context("reading file")?;
-
- Ok((
- StatusCode::OK,
- [(
- header::CONTENT_TYPE,
- HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
- )],
- body,
- )
- .into_response())
-}
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
new file mode 100644
index 0000000..af5a9e1
+use std::fs;
+
+use axum::{
+ extract::State,
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::response::{Html, Result},
+ utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "index.html")]
+struct IndexTemplate<'a> {
+ config: &'a Config,
+ repos: Vec<Repository>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(state: State<BileState>) -> Response {
+ state.spawn(move |state| inner(&state)).await
+}
+
+fn inner(state: &BileState) -> Result<Response> {
+ let Ok(read) = fs::read_dir(&state.config.project_root) else {
+ return Ok(Html(IndexTemplate {
+ config: &state.config,
+ repos: Vec::new(),
+ })
+ .into_response());
+ };
+
+ let mut repos = Vec::new();
+
+ for entry in read {
+ let entry = entry.context("failed to open directory entry")?;
+
+ let Some(repo) = Repository::open_path(&state.config, &entry.path())
+ .context("failed to open repository")?
+ else {
+ continue;
+ };
+
+ // check for the export file in the git directory
+ // (the .git subfolder for non-bare repos)
+ if !repo.path().join(&state.config.export_ok).exists() {
+ continue;
+ }
+
+ repos.push(repo);
+ }
+
+ Ok(Html(IndexTemplate {
+ config: &state.config,
+ repos,
+ })
+ .into_response())
+}
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
deleted file mode 100644
index 0f6004d..0000000
-use std::fs;
-
-use axum::response::{IntoResponse as _, Response};
-
-use crate::utils::{
- Result, error::Context as _, filters, git::Repository, response::Html, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "index.html")]
-struct IndexTemplate {
- repos: Vec<Repository>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get() -> Response {
- spawn_blocking(move || inner().into_response()).await
-}
-
-fn inner() -> Result {
- let Ok(read) = fs::read_dir(&crate::config().project_root) else {
- return Ok(Html(IndexTemplate { repos: Vec::new() }).into_response());
- };
-
- let config = crate::config();
-
- let mut repos = Vec::new();
-
- for entry in read {
- let entry = entry.context("failed to open directory entry")?;
-
- let Some(repo) = Repository::open(entry.path()).context("failed to open repository")?
- else {
- continue;
- };
-
- // check for the export file in the git directory
- // (the .git subfolder for non-bare repos)
- if !repo.path().join(&config.export_ok).exists() {
- continue;
- }
-
- repos.push(repo);
- }
-
- Ok(Html(IndexTemplate { repos }).into_response())
-}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
new file mode 100644
index 0000000..8f6333b
+pub(crate) mod git;
+pub(crate) mod index;
+pub(crate) mod repo_commit;
+pub(crate) mod repo_file;
+pub(crate) mod repo_file_raw;
+pub(crate) mod repo_home;
+pub(crate) mod repo_log;
+pub(crate) mod repo_log_feed;
+pub(crate) mod repo_refs;
+pub(crate) mod repo_refs_feed;
+pub(crate) mod repo_tag;
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
deleted file mode 100644
index 59cd646..0000000
-pub mod git;
-pub mod index;
-pub mod repo_commit;
-pub mod repo_file;
-pub mod repo_file_raw;
-pub mod repo_home;
-pub mod repo_log;
-pub mod repo_log_feed;
-pub mod repo_refs;
-pub mod repo_refs_feed;
-pub mod repo_tag;
diff --git a/src/handlers/repo_commit.rs b/src/handlers/repo_commit.rs
index 5ed4344..823424d 100644
use std::fmt::Write as _;
use axum::{
- extract::Path,
+ extract::{Path, State},
http::StatusCode,
response::{IntoResponse as _, Response},
};
-use git2::{
- BranchType, Commit, DescribeFormatOptions, DescribeOptions, Diff, DiffFindOptions, DiffFormat,
-};
+use git2::{BranchType, DescribeFormatOptions, DescribeOptions, DiffFindOptions, DiffFormat};
use syntect::{
html::{ClassStyle, ClassedHTMLGenerator},
+ parsing::SyntaxSet,
util::LinesWithEndings,
};
use crate::{
- SYNTAXES,
- utils::{
- Error, Result,
- error::Context as _,
- extractor::{commit_checks, repo_name_checks},
- filters,
- git::Repository,
- response::Html,
- spawn_blocking,
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{Commit, RepoName},
+ response::{ErrorPage, Html, Result},
},
+ utils::filters,
};
#[derive(askama::Template)]
#[template(path = "commit.html")]
struct RepoCommitTemplate<'a> {
+ config: &'a Config,
+ syntaxes: &'a SyntaxSet,
repo: &'a Repository,
- commit: Commit<'a>,
- diff: &'a Diff<'a>,
+ commit: git2::Commit<'a>,
+ diff: &'a git2::Diff<'a>,
}
impl RepoCommitTemplate<'_> {
});
// highlight the diff
- let syntax = SYNTAXES
+ let syntax = self
+ .syntaxes
.find_syntax_by_name("Diff")
.expect("diff syntax missing");
let mut highlighter =
- ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
+ ClassedHTMLGenerator::new_with_class_style(syntax, self.syntaxes, ClassStyle::Spaced);
LinesWithEndings::from(&buf).for_each(|line| {
if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
}
#[tracing::instrument(skip_all)]
-pub async fn get(Path((repo_name, commit)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&repo_name, &commit).into_response()).await
+pub(crate) async fn get(
+ state: State<BileState>,
+ Path((repo_name, commit)): Path<(RepoName, Commit)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, &commit))
+ .await
}
#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, commit: &str) -> Result {
- repo_name_checks(repo_name)?;
- commit_checks(commit)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName, commit: &Commit) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
};
- let Some(commit) = repo.commit(commit).context("failed to get commit")? else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "commit does not exist in repo",
- ));
+ let Some(commit) = repo.commit(&commit.0).context("failed to get commit")? else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
};
let mut diff = repo
}
Ok(Html(RepoCommitTemplate {
+ config: &state.config,
+ syntaxes: &state.syntax,
repo: &repo,
commit,
diff: &diff,
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
new file mode 100644
index 0000000..e83fe92
+use std::{fmt::Write as _, path};
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse as _, Response},
+};
+use syntect::{
+ html::{ClassStyle, ClassedHTMLGenerator},
+ parsing::SyntaxSet,
+ util::LinesWithEndings,
+};
+
+use crate::{
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{ObjectName, Ref, RepoName},
+ response::{ErrorPage, Html, Redirect, Result},
+ },
+ utils::{blob_mime, filters},
+};
+
+#[derive(askama::Template)]
+#[template(path = "tree.html")]
+struct RepoTreeTemplate<'a> {
+ config: &'a Config,
+ repo: &'a Repository,
+ tree: git2::Tree<'a>,
+ path: &'a path::Path,
+ spec: &'a str,
+ last_commit: git2::Commit<'a>,
+}
+
+#[derive(askama::Template)]
+#[template(path = "file.html")]
+struct RepoFileTemplate<'a> {
+ config: &'a Config,
+ repo: &'a Repository,
+ path: &'a path::Path,
+ file_text: &'a str,
+ spec: &'a str,
+ last_commit: git2::Commit<'a>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, None, None))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+ state: State<BileState>,
+ Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), None))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_3(
+ state: State<BileState>,
+ Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), Some(&object_name)))
+ .await
+}
+
+fn inner(
+ state: &BileState,
+ repo_name: &RepoName,
+ r#ref: Option<&Ref>,
+ object_name: Option<&ObjectName>,
+) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ if repo.is_empty()? {
+ return Ok(Redirect::permanent(&format!("/{repo_name}"))
+ .unwrap_or(Redirect::PERMANENT_ROOT)
+ .into_response());
+ }
+
+ let spec = repo.ref_or_head_shorthand(r#ref)?;
+ let Some((commit, tree)) = repo
+ .commit_tree(&spec)
+ .context("failed to get commit tree")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let (path, tree_obj) = if let Some(path) = object_name {
+ let path = path::Path::new(&path.0);
+
+ (path, repo.tree_object(&tree, path)?)
+ } else {
+ (path::Path::new(""), Some(tree.into_object()))
+ };
+
+ let Some(tree_obj) = tree_obj else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let tree_obj = match tree_obj.into_tree() {
+ // this is a subtree
+ Ok(sub_tree) => {
+ return Ok(Html(RepoTreeTemplate {
+ config: &state.config,
+ repo: &repo,
+ tree: sub_tree,
+ path,
+ spec: &spec,
+ last_commit,
+ })
+ .into_response());
+ }
+ // this is not a subtree, so it should be a blob i.e. file
+ Err(tree_obj) => tree_obj,
+ };
+
+ let Some(blob) = tree_obj.as_blob() else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let output = render(&state.syntax, repo_name, path, &spec, &commit, blob)?;
+
+ Ok(Html(RepoFileTemplate {
+ config: &state.config,
+ repo: &repo,
+ path,
+ file_text: &output,
+ spec: &spec,
+ last_commit,
+ })
+ .into_response())
+}
+
+// TODO: make sure I am escaping html properly here
+// TODO: allow disabling of syntax highlighting
+// TODO: -- dont pull in memory, use iterators if possible
+fn render(
+ syntaxes: &SyntaxSet,
+ repo_name: &RepoName,
+ path: &path::Path,
+ spec: &str,
+ commit: &git2::Commit<'_>,
+ blob: &git2::Blob<'_>,
+) -> crate::error::Result<String> {
+ let extension = path
+ .extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .unwrap_or_default();
+
+ if blob.is_binary() {
+ // this is not a text file, but try to serve the file if the MIME type
+ // can give a hint at how
+ let mime = blob_mime(blob, extension);
+
+ let output = match mime.type_() {
+ mime::TEXT => unreachable!("git detected this file as binary"),
+ mime::IMAGE => format!(
+ "<img src=\"/{}/tree/{}/raw/{}\" />",
+ repo_name,
+ spec,
+ path.display()
+ ),
+ tag @ (mime::AUDIO | mime::VIDEO) => format!(
+ "<{} src=\"/{}/tree/{}/raw/{}\" controls>Your browser does not have support for playing this {0} file.</{0}>",
+ tag,
+ repo_name,
+ spec,
+ path.display()
+ ),
+ _ => "Cannot display binary file.".to_string(),
+ };
+
+ return Ok(output);
+ }
+
+ let syntax = syntaxes
+ .find_syntax_by_extension(extension)
+ .unwrap_or_else(|| syntaxes.find_syntax_plain_text());
+
+ // get file contents from git object
+ let file_string = str::from_utf8(blob.content())?;
+
+ // create a highlighter that uses CSS classes so we can use prefers-color-scheme
+ let mut highlighter =
+ ClassedHTMLGenerator::new_with_class_style(syntax, syntaxes, ClassStyle::Spaced);
+ LinesWithEndings::from(file_string).for_each(|line| {
+ if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
+ tracing::error!(err=?err, "failed to highlight code");
+ }
+ });
+
+ // use oid so it is a permalink
+ let prefix = format!(
+ "/{}/tree/{}/item/{}",
+ repo_name,
+ commit.id(),
+ path.display()
+ );
+
+ let mut output = String::from("<pre>\n");
+ for (n, line) in highlighter.finalize().lines().enumerate() {
+ let _ = writeln!(
+ &mut output,
+ "<a href='{1}#L{0}' id='L{0}' class='line'>{0}</a>{2}",
+ n + 1,
+ prefix,
+ line,
+ );
+ }
+ output.push_str("</pre>\n");
+
+ Ok(output)
+}
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
deleted file mode 100644
index 65db580..0000000
-use std::{fmt::Write as _, path};
-
-use axum::{
- extract::Path,
- http::StatusCode,
- response::{IntoResponse as _, Response},
-};
-use git2::{Blob, Commit, Tree};
-use syntect::{
- html::{ClassStyle, ClassedHTMLGenerator},
- util::LinesWithEndings,
-};
-
-use crate::{
- SYNTAXES,
- utils::{
- Error, Result, blob_mime,
- error::Context as _,
- extractor::repo_name_checks,
- filters,
- git::Repository,
- response::{Html, Redirect},
- spawn_blocking,
- },
-};
-
-#[derive(askama::Template)]
-#[template(path = "tree.html")]
-struct RepoTreeTemplate<'a> {
- repo: &'a Repository,
- tree: Tree<'a>,
- path: &'a path::Path,
- spec: &'a str,
- last_commit: Commit<'a>,
-}
-
-#[derive(askama::Template)]
-#[template(path = "file.html")]
-struct RepoFileTemplate<'a> {
- repo: &'a Repository,
- path: &'a path::Path,
- file_text: &'a str,
- spec: &'a str,
- last_commit: Commit<'a>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name, None, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&repo_name, Some(&r#ref), None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_3(
- Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
- spawn_blocking(move || inner(&repo_name, Some(&r#ref), Some(&object_name)).into_response())
- .await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>, object_name: Option<&str>) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- if repo.is_empty()? {
- return Ok(Redirect::permanent(&format!("/{repo_name}"))
- .unwrap_or(Redirect::PERMANENT_ROOT)
- .into_response());
- }
-
- let spec = repo.ref_or_head_shorthand(r#ref)?;
- let Some((commit, tree)) = repo
- .commit_tree(&spec)
- .context("failed to get commit tree")?
- else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "commit does not exist in repo",
- ));
- };
-
- let (path, tree_obj) = if let Some(path) = object_name {
- let path = path::Path::new(path);
-
- (path, repo.tree_object(&tree, path)?)
- } else {
- (path::Path::new(""), Some(tree.into_object()))
- };
-
- let Some(tree_obj) = tree_obj else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "file does not exist into repo",
- ));
- };
-
- let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "commit does not exist in repo",
- ));
- };
-
- let tree_obj = match tree_obj.into_tree() {
- // this is a subtree
- Ok(sub_tree) => {
- return Ok(Html(RepoTreeTemplate {
- repo: &repo,
- tree: sub_tree,
- path,
- spec: &spec,
- last_commit,
- })
- .into_response());
- }
- // this is not a subtree, so it should be a blob i.e. file
- Err(tree_obj) => tree_obj,
- };
-
- let Some(blob) = tree_obj.as_blob() else {
- return Err(Error::new(StatusCode::NOT_FOUND, "File not found"));
- };
-
- let output = render(repo_name, path, &spec, &commit, blob)?;
-
- Ok(Html(RepoFileTemplate {
- repo: &repo,
- path,
- file_text: &output,
- spec: &spec,
- last_commit,
- })
- .into_response())
-}
-
-// TODO: make sure I am escaping html properly here
-// TODO: allow disabling of syntax highlighting
-// TODO: -- dont pull in memory, use iterators if possible
-fn render(
- repo_name: &str,
- path: &path::Path,
- spec: &str,
- commit: &Commit<'_>,
- blob: &Blob<'_>,
-) -> crate::utils::error::Result<String> {
- let extension = path
- .extension()
- .and_then(std::ffi::OsStr::to_str)
- .unwrap_or_default();
-
- if blob.is_binary() {
- // this is not a text file, but try to serve the file if the MIME type
- // can give a hint at how
- let mime = blob_mime(blob, extension);
-
- let output = match mime.type_() {
- mime::TEXT => unreachable!("git detected this file as binary"),
- mime::IMAGE => format!(
- "<img src=\"/{}/tree/{}/raw/{}\" />",
- repo_name,
- spec,
- path.display()
- ),
- tag @ (mime::AUDIO | mime::VIDEO) => format!(
- "<{} src=\"/{}/tree/{}/raw/{}\" controls>Your browser does not have support for playing this {0} file.</{0}>",
- tag,
- repo_name,
- spec,
- path.display()
- ),
- _ => "Cannot display binary file.".to_string(),
- };
-
- return Ok(output);
- }
-
- let syntax = SYNTAXES
- .find_syntax_by_extension(extension)
- .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
-
- // get file contents from git object
- let file_string = str::from_utf8(blob.content())?;
-
- // create a highlighter that uses CSS classes so we can use prefers-color-scheme
- let mut highlighter =
- ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
- LinesWithEndings::from(file_string).for_each(|line| {
- if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
- tracing::error!(err=?err, "failed to highlight code");
- }
- });
-
- // use oid so it is a permalink
- let prefix = format!(
- "/{}/tree/{}/item/{}",
- repo_name,
- commit.id(),
- path.display()
- );
-
- let mut output = String::from("<pre>\n");
- for (n, line) in highlighter.finalize().lines().enumerate() {
- let _ = writeln!(
- &mut output,
- "<a href='{1}#L{0}' id='L{0}' class='line'>{0}</a>{2}",
- n + 1,
- prefix,
- line,
- );
- }
- output.push_str("</pre>\n");
-
- Ok(output)
-}
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
new file mode 100644
index 0000000..9072050
+use std::path;
+
+use axum::{
+ extract::{Path, State},
+ http::{StatusCode, header},
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{ObjectName, Ref, RepoName},
+ response::{ErrorPage, Result},
+ },
+ utils::blob_mime,
+};
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(
+ state: State<BileState>,
+ Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, &r#ref, &object_name))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(
+ state: &BileState,
+ repo_name: &RepoName,
+ r#ref: &Ref,
+ object_name: &ObjectName,
+) -> Result<Response> {
+ let repo = match Repository::open(&state.config, repo_name).context("opening repository") {
+ Ok(Some(repo)) => repo,
+ Ok(None) => {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ }
+ Err(err) => {
+ tracing::error!(err=?err, "failed to open repository");
+
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ }
+ };
+
+ let path = path::Path::new(&object_name.0);
+
+ let Some((_, tree)) = repo
+ .commit_tree(&r#ref.0)
+ .context("failed to get commit tree")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let Some(blob) = repo.tree_blob(&tree, path)? else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let extension = path
+ .extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .unwrap_or_default();
+
+ let mime = blob_mime(&blob, extension);
+
+ Ok((
+ StatusCode::OK,
+ [(header::CONTENT_TYPE, mime.as_ref())],
+ blob.content().to_vec(),
+ )
+ .into_response())
+}
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
deleted file mode 100644
index 666981d..0000000
-use std::path;
-
-use axum::{
- extract::Path,
- http::{StatusCode, header},
- response::{IntoResponse as _, Response},
-};
-
-use crate::utils::{
- Error, Result, blob_mime, error::Context as _, extractor::repo_name_checks, git::Repository,
- spawn_blocking,
-};
-
-#[tracing::instrument(skip_all)]
-pub async fn get(
- Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
- spawn_blocking(move || inner(&repo_name, &r#ref, &object_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, r#ref: &str, object_name: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- let path = path::Path::new(&object_name);
-
- let Some((_, tree)) = repo
- .commit_tree(r#ref)
- .context("failed to get commit tree")?
- else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "commit does not exist in repo",
- ));
- };
-
- let Some(blob) = repo.tree_blob(&tree, path)? else {
- return Err(Error::new(
- StatusCode::NOT_FOUND,
- "file does not exist into repo",
- ));
- };
-
- let extension = path
- .extension()
- .and_then(std::ffi::OsStr::to_str)
- .unwrap_or_default();
-
- let mime = blob_mime(&blob, extension);
-
- Ok((
- StatusCode::OK,
- [(header::CONTENT_TYPE, mime.as_ref())],
- blob.content().to_vec(),
- )
- .into_response())
-}
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
new file mode 100644
index 0000000..503b851
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::RepoName,
+ response::{ErrorPage, Html, Result},
+ },
+ utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "repo.html")]
+struct RepoHomeTemplate<'a> {
+ config: &'a Config,
+ repo: &'a Repository,
+ commits: Vec<git2::Commit<'a>>,
+ readme_text: String,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state.spawn(move |state| inner(&state, &repo_name)).await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let readme_text = repo.readme(&state.syntax);
+
+ // TODO: let r = req.param("ref").unwrap_or("HEAD");
+ let r = "HEAD";
+ let Some(commits) = repo.commits(r, 3)? else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ Ok(Html(RepoHomeTemplate {
+ config: &state.config,
+ repo: &repo,
+ commits,
+ readme_text,
+ })
+ .into_response())
+}
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
deleted file mode 100644
index fc5504b..0000000
-use axum::{
- extract::Path,
- http::StatusCode,
- response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
- Error, Result, error::Context as _, extractor::repo_name_checks, filters, git::Repository,
- response::Html, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "repo.html")]
-struct RepoHomeTemplate<'a> {
- repo: &'a Repository,
- commits: Vec<Commit<'a>>,
- readme_text: String,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- let readme_text = repo.readme();
-
- // TODO: let r = req.param("ref").unwrap_or("HEAD");
- let r = "HEAD";
- let Some(commits) = repo.commits(r, 3)? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "crepo does not exist"));
- };
-
- Ok(Html(RepoHomeTemplate {
- repo: &repo,
- commits,
- readme_text,
- })
- .into_response())
-}
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
new file mode 100644
index 0000000..d41fb39
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{ObjectName, Ref, RepoName},
+ response::{ErrorPage, Html, Redirect, Result},
+ },
+ utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "log.html")]
+struct RepoLogTemplate<'a> {
+ config: &'a Config,
+ repo: &'a Repository,
+ commits: Vec<git2::Commit<'a>>,
+ branch: String,
+ // the spec the user should be linked to to see the next page of commits
+ next_page: Option<String>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, None, None))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+ state: State<BileState>,
+ Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), None))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_3(
+ state: State<BileState>,
+ Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), Some(&object_name)))
+ .await
+}
+
+fn inner(
+ state: &BileState,
+ repo_name: &RepoName,
+ r#ref: Option<&Ref>,
+ object_name: Option<&ObjectName>,
+) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ if repo.is_empty()? {
+ return Ok(Redirect::permanent(&format!("/{repo_name}"))
+ .unwrap_or(Redirect::PERMANENT_ROOT)
+ .into_response());
+ }
+
+ let r = r#ref.map_or("HEAD", |r| r.0.as_str());
+
+ let next_page_spec = if repo.is_shallow() {
+ String::new()
+ } else if let Some(i) = r.rfind('~') {
+ // there is a tilde, try to find a number too
+ let n = r[i + 1..].parse::<usize>().ok().unwrap_or(1);
+ format!("{}~{}", &r[..i], n + state.config.log_per_page)
+ } else {
+ // there was no tilde
+ format!("{}~{}", r, state.config.log_per_page)
+ };
+
+ let Some(mut commits) = repo
+ .commits_for_obj(r, state.config.log_per_page + 1, object_name)
+ .context("failed to get commits for object")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ // check if there even is a next page
+ let next_page = if commits.len() < state.config.log_per_page + 1 {
+ None
+ } else {
+ // remove additional commit from next page check
+ commits.pop();
+ Some(next_page_spec)
+ };
+
+ let branch = repo.ref_or_head_shorthand(r#ref)?;
+
+ Ok(Html(RepoLogTemplate {
+ config: &state.config,
+ repo: &repo,
+ commits,
+ branch,
+ next_page,
+ })
+ .into_response())
+}
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
deleted file mode 100644
index 28867ef..0000000
-use axum::{
- extract::Path,
- http::StatusCode,
- response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
- Error, Result,
- error::Context as _,
- extractor::repo_name_checks,
- filters,
- git::Repository,
- response::{Html, Redirect},
- spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "log.html")]
-struct RepoLogTemplate<'a> {
- repo: &'a Repository,
- commits: Vec<Commit<'a>>,
- branch: String,
- // the spec the user should be linked to to see the next page of commits
- next_page: Option<String>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name, None, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&repo_name, Some(&r#ref), None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_3(
- Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
- spawn_blocking(move || inner(&repo_name, Some(&r#ref), Some(&object_name)).into_response())
- .await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>, object_name: Option<&str>) -> Result {
- repo_name_checks(repo_name)?;
-
- let config = crate::config();
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- if repo.is_empty()? {
- return Ok(Redirect::permanent(&format!("/{repo_name}"))
- .unwrap_or(Redirect::PERMANENT_ROOT)
- .into_response());
- }
-
- let r = r#ref.unwrap_or("HEAD");
-
- let next_page_spec = if repo.is_shallow() {
- String::new()
- } else if let Some(i) = r.rfind('~') {
- // there is a tilde, try to find a number too
- let n = r[i + 1..].parse::<usize>().ok().unwrap_or(1);
- format!("{}~{}", &r[..i], n + config.log_per_page)
- } else {
- // there was no tilde
- format!("{}~{}", r, config.log_per_page)
- };
-
- let Some(mut commits) = repo
- .commits_for_obj(r, config.log_per_page + 1, object_name)
- .context("failed to get commits for object")?
- else {
- return Err(Error::new(StatusCode::NOT_FOUND, "entry does not exist"));
- };
-
- // check if there even is a next page
- let next_page = if commits.len() < config.log_per_page + 1 {
- None
- } else {
- // remove additional commit from next page check
- commits.pop();
- Some(next_page_spec)
- };
-
- let branch = repo.ref_or_head_shorthand(r#ref)?;
-
- Ok(Html(RepoLogTemplate {
- repo: &repo,
- commits,
- branch,
- next_page,
- })
- .into_response())
-}
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
new file mode 100644
index 0000000..cd306d8
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse as _, Response},
+};
+
+use crate::{
+ BileState,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{Ref, RepoName},
+ response::{ErrorPage, Result, Xml},
+ },
+ utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "log.xml")]
+struct RepoLogFeedTemplate<'a> {
+ repo: &'a Repository,
+ commits: Vec<git2::Commit<'a>>,
+ branch: String,
+ base_url: &'a str,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, None))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+ state: State<BileState>,
+ Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, Some(&r#ref)))
+ .await
+}
+
+fn inner(state: &BileState, repo_name: &RepoName, r#ref: Option<&Ref>) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ if repo.is_empty()? {
+ // show a server error
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::SERVICE_UNAVAILABLE)
+ .into_response());
+ }
+
+ let r = r#ref.map_or("HEAD", |r| r.0.as_str());
+
+ let Some(commits) = repo.commits(r, state.config.log_per_page)? else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let branch = repo.ref_or_head_shorthand(r#ref)?;
+
+ Ok(Xml(RepoLogFeedTemplate {
+ repo: &repo,
+ commits,
+ branch,
+ base_url: &format!("/{repo_name}"),
+ })
+ .into_response())
+}
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
deleted file mode 100644
index f21bbc9..0000000
-use axum::{
- extract::Path,
- http::StatusCode,
- response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
- Error, Result, error::Context as _, extractor::repo_name_checks, filters, git::Repository,
- response::Xml, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "log.xml")]
-struct RepoLogFeedTemplate<'a> {
- repo: &'a Repository,
- commits: Vec<Commit<'a>>,
- branch: String,
- base_url: &'a str,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&repo_name, Some(&r#ref)).into_response()).await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- if repo.is_empty()? {
- // show a server error
- return Err(Error::new(
- StatusCode::SERVICE_UNAVAILABLE,
- "Cannot show feed because there are no commits.",
- ));
- }
-
- let r = r#ref.unwrap_or("HEAD");
-
- let Some(commits) = repo.commits(r, crate::config().log_per_page)? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "crepo does not exist"));
- };
-
- let branch = repo.ref_or_head_shorthand(r#ref)?;
-
- Ok(Xml(RepoLogFeedTemplate {
- repo: &repo,
- commits,
- branch,
- base_url: &format!("/{repo_name}"),
- })
- .into_response())
-}
diff --git a/src/handlers/repo_refs.rs b/src/handlers/repo_refs.rs
index d88c463..efff748 100644
use axum::{
- extract::Path,
+ extract::{Path, State},
http::StatusCode,
response::{IntoResponse as _, Response},
};
-use git2::Reference;
-use crate::utils::{
- Error, Result,
+use crate::{
+ BileState,
+ config::Config,
error::Context as _,
- extractor::repo_name_checks,
- filters,
git::{Repository, TagEntry},
- response::{Html, Redirect},
- spawn_blocking,
+ http::{
+ extractor::RepoName,
+ response::{ErrorPage, Html, Redirect, Result},
+ },
+ utils::filters,
};
#[derive(askama::Template)]
#[template(path = "refs.html")]
struct RepoRefTemplate<'a> {
+ config: &'a Config,
repo: &'a Repository,
- branches: Vec<Reference<'a>>,
+ branches: Vec<git2::Reference<'a>>,
tags: Vec<TagEntry>,
}
#[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name).into_response()).await
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state.spawn(move |state| inner(&state, &repo_name)).await
}
#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
};
if repo.is_empty()? {
tags.sort_unstable_by(|a, b| a.signature.when().cmp(&b.signature.when()).reverse());
Ok(Html(RepoRefTemplate {
+ config: &state.config,
repo: &repo,
branches,
tags,
diff --git a/src/handlers/repo_refs_feed.rs b/src/handlers/repo_refs_feed.rs
index fd04b99..203658a 100644
use axum::{
- extract::Path,
+ extract::{Path, State},
http::StatusCode,
response::{IntoResponse as _, Response},
};
-use crate::utils::{
- Error, Result,
+use crate::{
+ BileState,
error::Context as _,
- extractor::repo_name_checks,
- filters,
git::{Repository, TagEntry},
- response::Xml,
- spawn_blocking,
+ http::{
+ extractor::RepoName,
+ response::{ErrorPage, Result, Xml},
+ },
+ utils::filters,
};
#[derive(askama::Template)]
}
#[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
- spawn_blocking(move || inner(&repo_name).into_response()).await
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+ state.spawn(move |state| inner(&state, &repo_name)).await
}
#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
};
if repo.is_empty()? {
// show a server error
- return Err(Error::new(
- StatusCode::SERVICE_UNAVAILABLE,
- "Cannot show feed because there is nothing here.",
- ));
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::SERVICE_UNAVAILABLE)
+ .into_response());
}
let mut tags = repo.tag_entries()?;
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
new file mode 100644
index 0000000..d78a4ca
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+
+use crate::{
+ BileState,
+ config::Config,
+ error::Context as _,
+ git::Repository,
+ http::{
+ extractor::{RepoName, Tag},
+ response::{ErrorPage, Html, Redirect, Result},
+ },
+ utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "tag.html")]
+struct Template<'a> {
+ config: &'a Config,
+ repo: &'a Repository,
+ tag: git2::Tag<'a>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(
+ state: State<BileState>,
+ Path((repo_name, tag)): Path<(RepoName, Tag)>,
+) -> Response {
+ state
+ .spawn(move |state| inner(&state, &repo_name, &tag))
+ .await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(state: &BileState, repo_name: &RepoName, tag: &Tag) -> Result<Response> {
+ let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+ else {
+ return Ok(ErrorPage::new(&state.config)
+ .with_status(StatusCode::NOT_FOUND)
+ .into_response());
+ };
+
+ let Ok(repo_tag) = repo.tag(tag) else {
+ return Ok(Redirect::permanent(&format!("/{repo_name}/commit/{tag}"))
+ .unwrap_or(Redirect::PERMANENT_ROOT)
+ .into_response());
+ };
+
+ Ok(Html(Template {
+ config: &state.config,
+ repo: &repo,
+ tag: repo_tag,
+ })
+ .into_response())
+}
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
deleted file mode 100644
index 2d7882a..0000000
-use axum::{
- extract::Path,
- http::StatusCode,
- response::{IntoResponse, Response},
-};
-use git2::Tag;
-
-use crate::utils::{
- Error, Result,
- error::Context as _,
- extractor::repo_name_checks,
- filters,
- git::Repository,
- response::{Html, Redirect},
- spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "tag.html")]
-struct Template<'a> {
- repo: &'a Repository,
- tag: Tag<'a>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get(Path((repo_name, tag)): Path<(String, String)>) -> Response {
- spawn_blocking(move || inner(&repo_name, &tag).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, tag: &str) -> Result {
- repo_name_checks(repo_name)?;
-
- let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- };
-
- let Ok(repo_tag) = repo.tag(tag) else {
- return Ok(Redirect::permanent(&format!("/{repo_name}/commit/{tag}"))
- .unwrap_or(Redirect::PERMANENT_ROOT)
- .into_response());
- };
-
- Ok(Html(Template {
- repo: &repo,
- tag: repo_tag,
- })
- .into_response())
-}
diff --git a/src/http/extractor.rs b/src/http/extractor.rs
new file mode 100644
index 0000000..d674c3f
+use std::fmt;
+
+use serde::de::Error as _;
+
+pub(crate) struct Commit(pub String);
+
+impl fmt::Display for Commit {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for Commit {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid commit ref"));
+ }
+
+ for c in value.bytes() {
+ if !c.is_ascii_hexdigit() {
+ return Err(D::Error::custom("invalid commit ref"));
+ }
+ }
+
+ Ok(Self(value))
+ }
+}
+
+pub(crate) struct Obj(pub String);
+
+impl fmt::Display for Obj {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for Obj {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid object ref"));
+ }
+
+ Ok(Self(value))
+ }
+}
+
+pub(crate) struct ObjectName(pub String);
+
+impl fmt::Display for ObjectName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for ObjectName {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid object name"));
+ }
+
+ Ok(Self(value))
+ }
+}
+
+pub(crate) struct Ref(pub String);
+
+impl fmt::Display for Ref {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for Ref {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid ref"));
+ }
+
+ Ok(Self(value))
+ }
+}
+
+pub(crate) struct RepoName(pub String);
+
+impl fmt::Display for RepoName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for RepoName {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid repo name"));
+ }
+
+ Ok(Self(value))
+ }
+}
+
+pub(crate) struct Tag(pub String);
+
+impl fmt::Display for Tag {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for Tag {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ if value.is_empty() {
+ return Err(D::Error::custom("invalid tag"));
+ }
+
+ Ok(Self(value))
+ }
+}
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 0000000..83e27db
+pub(crate) mod extractor;
+pub(crate) mod response;
+
+use std::sync::Arc;
+
+use axum::response::{IntoResponse as _, Response};
+use http::StatusCode;
+use syntect::parsing::SyntaxSet;
+
+use crate::{config::Config, error::Result, http::response::ErrorPage};
+
+#[derive(Clone)]
+pub(crate) struct BileState {
+ pub(crate) config: Arc<Config>,
+ pub(crate) syntax: Arc<SyntaxSet>,
+}
+
+impl BileState {
+ pub(crate) fn new(config: Config, syntax: SyntaxSet) -> Self {
+ Self {
+ config: Arc::new(config),
+ syntax: Arc::new(syntax),
+ }
+ }
+
+ pub(crate) async fn spawn<F>(&self, f: F) -> Response
+ where
+ F: FnOnce(Self) -> Result<Response> + Send + 'static,
+ {
+ let span = tracing::Span::current();
+
+ let this = self.clone();
+
+ spawn_blocking(move || span.in_scope(|| wrap_err(&this.config.clone(), f(this)))).await
+ }
+}
+
+// TODO: https://github.com/rust-lang/rust/issues/110011
+// #[track_caller]
+async fn spawn_blocking<F, R>(f: F) -> R
+where
+ F: FnOnce() -> R + Send + 'static,
+ R: Send + 'static,
+{
+ tokio::task::spawn_blocking(f)
+ .await
+ .expect("failed to join spawn_blocking call, this should only happen due to a panic")
+}
+
+fn wrap_err(config: &Config, res: Result<Response>) -> Response {
+ match res {
+ Ok(res) => res,
+ Err(err) => {
+ tracing::error!(err=?err, "failed to handle response");
+
+ ErrorPage::new(config)
+ .with_status(StatusCode::INTERNAL_SERVER_ERROR)
+ .into_response()
+ }
+ }
+}
diff --git a/src/utils/response.rs b/src/http/response.rs
similarity index 78%
rename from src/utils/response.rs
rename to src/http/response.rs
index eff1bec..f4d8530 100644
use axum::{
http::{HeaderValue, StatusCode, header},
- response::IntoResponse,
+ response::{IntoResponse, Response},
};
-pub struct Css<T>(pub T);
+use crate::config::Config;
+
+pub(crate) type Result<T = Response, E = crate::error::Error> = std::result::Result<T, E>;
+
+pub(crate) struct Css<T>(pub T);
impl<T: IntoResponse> IntoResponse for Css<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(
[
(
}
}
-pub struct Ico<T>(pub T);
+pub(crate) struct Ico<T>(pub T);
impl<T: IntoResponse> IntoResponse for Ico<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(
[
(
}
}
-pub struct Json<T>(pub T);
+pub(crate) struct Json<T>(pub T);
impl<T: IntoResponse> IntoResponse for Json<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(
[
(
}
}
-pub struct Png<T>(pub T);
+pub(crate) struct Png<T>(pub T);
impl<T: IntoResponse> IntoResponse for Png<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(
[
(
}
}
-pub struct Text<T>(pub T);
+pub(crate) struct Text<T>(pub T);
impl<T: IntoResponse> IntoResponse for Text<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(
[
(
}
}
-pub struct Html<T: askama::Template>(pub T);
+pub(crate) struct Html<T: askama::Template>(pub T);
impl<T: askama::Template> IntoResponse for Html<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
match self.0.render() {
Ok(rendered) => (
[
}
}
-pub struct Xml<T: askama::Template>(pub T);
+pub(crate) struct Xml<T: askama::Template>(pub T);
impl<T: askama::Template> IntoResponse for Xml<T> {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
match self.0.render() {
Ok(rendered) => (
[
#[must_use = "needs to be returned from a handler or otherwise turned into a Response to be useful"]
#[derive(Debug, Clone)]
-pub struct Redirect {
+pub(crate) struct Redirect {
status_code: StatusCode,
location: HeaderValue,
}
impl Redirect {
- pub const PERMANENT_ROOT: Self = Self {
+ pub(crate) const PERMANENT_ROOT: Self = Self {
status_code: StatusCode::PERMANENT_REDIRECT,
location: HeaderValue::from_static("/"),
};
- pub const TEMPORARY_ROOT: Self = Self {
+ pub(crate) const TEMPORARY_ROOT: Self = Self {
status_code: StatusCode::TEMPORARY_REDIRECT,
location: HeaderValue::from_static("/"),
};
#[tracing::instrument(skip_all)]
- pub fn to(uri: &str) -> Option<Self> {
+ pub(crate) fn to(uri: &str) -> Option<Self> {
Self::with_status_code(StatusCode::SEE_OTHER, uri)
}
#[tracing::instrument(skip_all)]
- pub fn temporary(uri: &str) -> Option<Self> {
+ pub(crate) fn temporary(uri: &str) -> Option<Self> {
Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri)
}
#[tracing::instrument(skip_all)]
- pub fn permanent(uri: &str) -> Option<Self> {
+ pub(crate) fn permanent(uri: &str) -> Option<Self> {
Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri)
}
}
impl IntoResponse for Redirect {
- fn into_response(self) -> axum::response::Response {
+ fn into_response(self) -> Response {
(self.status_code, [(header::LOCATION, self.location)]).into_response()
}
}
+
+#[derive(askama::Template)]
+#[template(path = "error.html")]
+pub(crate) struct ErrorPage<'c> {
+ config: &'c Config,
+ status: StatusCode,
+}
+
+impl<'c> ErrorPage<'c> {
+ pub(crate) const fn new(config: &'c Config) -> Self {
+ Self {
+ config,
+ status: StatusCode::INTERNAL_SERVER_ERROR,
+ }
+ }
+
+ pub(crate) const fn with_status(self, status: StatusCode) -> Self {
+ Self {
+ config: self.config,
+ status,
+ }
+ }
+}
+
+impl IntoResponse for ErrorPage<'_> {
+ fn into_response(self) -> Response {
+ (self.status, Html(self)).into_response()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index d205dbd..73f9c46 100644
clippy::wildcard_imports,
clippy::zero_sized_map_values
)]
-#![allow(clippy::missing_errors_doc)]
+#![allow(clippy::missing_errors_doc, clippy::redundant_pub_crate)]
-pub mod handlers;
-pub mod utils;
+pub(crate) mod git;
+pub(crate) mod handlers;
+pub(crate) mod http;
+pub(crate) mod utils;
pub mod config;
+pub mod error;
-use std::{
- str,
- sync::{LazyLock, OnceLock},
- time::Duration,
-};
+use std::{str, time::Duration};
use axum::{Router, http::StatusCode, routing::get};
use axum_response_cache::CacheLayer;
-use syntect::parsing::SyntaxSet;
use tower_helmet::HelmetLayer;
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
use crate::{
config::Config,
- utils::response::{Css, Ico, Json, Png, Text},
+ http::BileState,
+ http::response::{Css, Ico, Json, Png, Text},
};
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
-static CONFIG: OnceLock<Config> = OnceLock::new();
-static SYNTAXES: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
-
static APPLE_TOUCH_ICON_PNG: &[u8] = include_bytes!("../assets/apple-touch-icon.png");
static FAVICON_ICO: &[u8] = include_bytes!("../assets/favicon.ico");
static ICON_192_MASKABLE: &[u8] = include_bytes!("../assets/icon-192-maskable.png");
static META_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
-pub(crate) fn config() -> &'static Config {
- CONFIG
- .get()
- .unwrap_or_else(|| unreachable!("failed to get global config, this should not happen"))
-}
-
#[allow(missing_copy_implementations, clippy::empty_structs_with_brackets)]
-pub struct Bile {}
+pub struct Bile {
+ state: BileState,
+}
impl Bile {
/// Create a new Bile 'instance'
/// This will panic if you create multiple Bile instances.
///
/// Which you shouldn't BTW.
+ #[must_use]
pub fn init(config: Config) -> Self {
- CONFIG.set(config).expect("config already set");
-
- Self {}
+ Self {
+ state: BileState::new(config, two_face::syntax::extra_newlines()),
+ }
}
#[rustfmt::skip]
.route("/{repo_name}/tree/{ref}/item/{*object_name}", get(handlers::repo_file::get_3))
.route("/{repo_name}/tree/{ref}/raw/{*object_name}", get(handlers::repo_file_raw::get))
//
+ .with_state(self.state.clone())
+ //
.layer((
TraceLayer::new_for_http(),
TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(10)),
diff --git a/src/main.rs b/src/main.rs
index 41b00eb..af9318f 100644
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
#[tokio::main]
-async fn main() -> bile::utils::error::Result<()> {
+async fn main() -> bile::error::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
new file mode 100644
index 0000000..e69de29
diff --git a/src/utils/extractor.rs b/src/utils/extractor.rs
deleted file mode 100644
index dcb6222..0000000
-use axum::{
- extract,
- http::{self, StatusCode, request},
- response::{self, IntoResponse as _},
-};
-
-use crate::utils::{Error, Result};
-
-fn get_param<'x>(ext: &'x http::Extensions, name: &str) -> Option<&'x str> {
- let raw = ext.get::<extract::RawPathParams>()?;
-
- for (key, value) in raw {
- if key == name {
- return Some(value);
- }
- }
-
- None
-}
-
-pub struct Commit(pub String);
-
-impl<S> extract::FromRequestParts<S> for Commit
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "commit") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "commit does not exist in repo").into_response(),
- );
- }
-
- for c in raw.bytes() {
- if !c.is_ascii_hexdigit() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "commit does not exist in repo")
- .into_response(),
- );
- }
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub struct Obj(pub String);
-
-impl<S> extract::FromRequestParts<S> for Obj
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "obj") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "object does not exist in repo").into_response(),
- );
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub struct ObjectName(pub String);
-
-impl<S> extract::FromRequestParts<S> for ObjectName
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "object_name") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "object does not exist in repo").into_response(),
- );
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub struct Ref(pub String);
-
-impl<S> extract::FromRequestParts<S> for Ref
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "ref") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "ref does not exist in repo").into_response(),
- );
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub struct RepoName(pub String);
-
-impl<S> extract::FromRequestParts<S> for RepoName
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "repo_name") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist").into_response());
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub struct Tag(pub String);
-
-impl<S> extract::FromRequestParts<S> for Tag
-where
- S: Sync,
-{
- type Rejection = response::Response;
-
- async fn from_request_parts(
- parts: &mut request::Parts,
- _state: &S,
- ) -> Result<Self, Self::Rejection> {
- let Some(raw) = get_param(&parts.extensions, "tag") else {
- return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
- };
-
- if raw.is_empty() {
- return Err(
- Error::new(StatusCode::NOT_FOUND, "tag does not exist in repo").into_response(),
- );
- }
-
- Ok(Self(raw.to_string()))
- }
-}
-
-pub fn repo_name_checks(name: &str) -> Result<()> {
- let name = name.trim();
-
- if name.is_empty() {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- }
-
- Ok(())
-}
-
-pub fn commit_checks(name: &str) -> Result<()> {
- let name = name.trim();
-
- if name.is_empty() {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- }
-
- for c in name.bytes() {
- if !c.is_ascii_hexdigit() {
- return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
- }
- }
-
- Ok(())
-}
diff --git a/src/utils/filters.rs b/src/utils/filters.rs
index fbada2b..3643db8 100644
-#![allow(clippy::inline_always, reason = "generated by askama filter_fn")]
+#![allow(
+ unreachable_pub,
+ clippy::inline_always,
+ clippy::unused_self,
+ clippy::unnecessary_wraps,
+ reason = "generated by askama filter_fn"
+)]
use git2::{Commit, Signature, Time};
use jiff::{
};
use num_conv::Truncate as _;
-use crate::utils::git::Repository;
+use crate::git::Repository;
#[askama::filter_fn]
#[tracing::instrument(skip_all)]
-pub fn format_datetime<'f>(
+pub(crate) fn format_datetime<'f>(
time: Time,
_: &dyn askama::Values,
format: &'f str,
}
#[askama::filter_fn]
-pub fn unix_perms(m: i32, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn unix_perms(m: i32, _: &dyn askama::Values) -> askama::Result<String> {
// https://unix.stackexchange.com/questions/450480/file-permission-with-six-bytes-in-git-what-does-it-mean
// Git doesn't store arbitrary modes, only a subset of the values are
// allowed. Since the number of possible values is quite small, it is
}
#[askama::filter_fn]
-pub fn repo_name<'r>(repo: &'r Repository, _: &dyn askama::Values) -> askama::Result<&'r str> {
+pub(crate) fn repo_name<'r>(
+ repo: &'r Repository,
+ _: &dyn askama::Values,
+) -> askama::Result<&'r str> {
repo.name().ok_or(askama::Error::Fmt)
}
#[askama::filter_fn]
-pub fn description(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn description(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
Ok(repo.description())
}
#[askama::filter_fn]
#[tracing::instrument(skip_all)]
-pub fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama::Result<Time> {
+pub(crate) fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama::Result<Time> {
repo.last_modified().map_err(|err| {
tracing::error!(err=?err, "failed to get repo's last modified date");
askama::Error::Fmt
}
#[askama::filter_fn]
-pub fn repo_owner(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn repo_owner(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
Ok(repo.owner())
}
#[askama::filter_fn]
-pub fn signature_email_link(
+pub(crate) fn signature_email_link(
signature: &Signature<'_>,
_: &dyn askama::Values,
) -> askama::Result<String> {
#[askama::filter_fn]
#[tracing::instrument(skip_all)]
-pub fn short_id(commit: &Commit<'_>, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn short_id(commit: &Commit<'_>, _: &dyn askama::Values) -> askama::Result<String> {
let id = match commit.as_object().short_id() {
Ok(id) => id,
Err(err) => {
diff --git a/src/utils/markdown.rs b/src/utils/markdown.rs
index 6e02d11..2b4cb1e 100644
util::LinesWithEndings,
};
-use crate::SYNTAXES;
-
#[tracing::instrument(skip_all)]
-pub fn render(input: &str) -> String {
+pub(crate) fn render(syntaxes: &SyntaxSet, input: &str) -> String {
let adaptor = SyntectAdapter {
- syntax_set: &SYNTAXES,
+ syntax_set: syntaxes,
};
let options = Options::default();
markdown_to_html_with_plugins(input, &options, &plugins)
}
-struct SyntectAdapter {
- syntax_set: &'static SyntaxSet,
+struct SyntectAdapter<'s> {
+ syntax_set: &'s SyntaxSet,
}
-impl SyntectAdapter {
+impl SyntectAdapter<'_> {
fn highlight_html(&self, code: &str, syntax: &SyntaxReference) -> Result<String, Error> {
let mut html_generator =
ClassedHTMLGenerator::new_with_class_style(syntax, self.syntax_set, ClassStyle::Spaced);
}
}
-impl SyntaxHighlighterAdapter for SyntectAdapter {
+impl SyntaxHighlighterAdapter for SyntectAdapter<'_> {
fn write_highlighted(
&self,
output: &mut dyn Write,
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 0000000..4b1ae7e
+pub(crate) mod cache;
+pub(crate) mod filters;
+pub(crate) mod markdown;
+
+#[must_use]
+pub(crate) fn blob_mime(blob: &git2::Blob<'_>, extension: &str) -> mime::Mime {
+ extension.parse().unwrap_or_else(|_| {
+ if blob.is_binary() {
+ mime::APPLICATION_OCTET_STREAM
+ } else {
+ mime::TEXT_PLAIN_UTF_8
+ }
+ })
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
deleted file mode 100644
index 19fef1c..0000000
-pub mod git;
-
-pub mod error;
-pub mod extractor;
-pub mod filters;
-pub mod markdown;
-pub mod response;
-
-use axum::{
- http::StatusCode,
- response::{IntoResponse, Response},
-};
-
-use crate::utils::response::Html;
-
-pub type Result<T = Response, E = Error> = std::result::Result<T, E>;
-
-// TODO: https://github.com/rust-lang/rust/issues/110011
-// #[track_caller]
-pub async fn spawn_blocking<F, R>(f: F) -> R
-where
- F: FnOnce() -> R + Send + 'static,
- R: Send + 'static,
-{
- tokio::task::spawn_blocking(f)
- .await
- .expect("failed to join spawn_blocking call, this should only happen due to a panic")
-}
-
-#[derive(askama::Template)]
-#[template(path = "error.html")]
-pub enum Error {
- Failure {
- status: StatusCode,
- err: error::Error,
- },
- Custom {
- status: StatusCode,
- message: String,
- },
-}
-
-impl Error {
- pub fn new<M: ToString + ?Sized>(status: StatusCode, message: &M) -> Self {
- Self::Custom {
- status,
- message: message.to_string(),
- }
- }
-}
-
-impl IntoResponse for Error {
- fn into_response(self) -> Response {
- let status = match &self {
- Self::Failure { status, err } => {
- tracing::error!(err=?err, "failed to respond to request");
-
- status
- }
- Self::Custom { status, .. } => status,
- };
-
- (*status, Html(self)).into_response()
- }
-}
-
-impl<E> From<E> for Error
-where
- E: Into<error::Error>,
-{
- fn from(err: E) -> Self {
- Self::Failure {
- status: StatusCode::INTERNAL_SERVER_ERROR,
- err: err.into(),
- }
- }
-}
-
-#[must_use]
-pub fn blob_mime(blob: &git2::Blob<'_>, extension: &str) -> mime::Mime {
- extension.parse().unwrap_or_else(|_| {
- if blob.is_binary() {
- mime::APPLICATION_OCTET_STREAM
- } else {
- mime::TEXT_PLAIN_UTF_8
- }
- })
-}
diff --git a/templates/base.html b/templates/base.html
index 30484c2..5796569 100644
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="My self-hosted git repositories">
- <title>{% block title %}{{crate::config().site_name}}{% endblock %}</title>
+ <title>{% block title %}{{config.site_name}}{% endblock %}</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<body>
<div class="page-title">
- <h1><a href="/">{{crate::config().site_name}}</a></h1>
+ <h1><a href="/">{{config.site_name}}</a></h1>
</div>
<hr />
<div id="content">
diff --git a/templates/commit.html b/templates/commit.html
index 49bb891..497e3c4 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} commit {{commit|short_id}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} commit {{commit|short_id}} - {{config.site_name}}{% endblock %}
{% block content %}
{% include "repo-navbar.html" %}
diff --git a/templates/error-404.html b/templates/error-404.html
new file mode 100644
index 0000000..e69de29
diff --git a/templates/error.html b/templates/error-old.html
similarity index 86%
rename from templates/error.html
rename to templates/error-old.html
index 5d0456b..e1c0478 100644
</div>
<br>
<div>
- {%- match self -%}
- {%- when Self::Failure { status, err } -%}
+ {%- match self.kind -%}
+ {%- when ErrorKind::Failure { status, err } -%}
{% let debug = format!("{:?}", err) %}
{% let err = ansi_to_html::convert(&debug) %}
{%- match err -%}
{%- when Err(_) -%}
{{status}} — {{ debug }}
{%- endmatch -%}
- {%- when Self::Custom { status, message } -%}
+ {%- when ErrorKind::Custom { status, message } -%}
{{status}} — {{ message }}
{%- endmatch -%}
</div>
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..70fe79b
+{% extends "base.html" %}
+
+{% block content %}
+ <div class="page-title">
+ <h1>Hmm, that did not work.</h1>
+ </div>
+ <br>
+ <div>
+ </div>
+{% endblock %}<
\ No newline at end of file
diff --git a/templates/file.html b/templates/file.html
index ec9e8fd..744dacf 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} {{path.display()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} {{path.display()}} - {{config.site_name}}{% endblock %}
{% block content %}
{% include "repo-navbar.html" %}
diff --git a/templates/log.html b/templates/log.html
index 4719ab8..6ebc3a6 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} log at {{branch}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} log at {{branch}} - {{config.site_name}}{% endblock %}
{% block head %}
<link rel="alternate" type="application/rss+xml" title="{{repo|repo_name}} {{branch}} commits" href="log.xml">
</tbody>
</table>
{% if next_page.is_some() %}
- <a href="{{next_page.as_ref().unwrap()}}">older commits →</a>
+ <a href="/{{repo|repo_name|urlencode_strict}}/log/{{next_page.as_ref().unwrap()}}">older commits →</a>
{% endif %}
<hr>
<table id="log">
</tbody>
</table>
{% if next_page.is_some() %}
- <a href="{{next_page.as_ref().unwrap()}}">older commits →</a>
+ <a href="/{{repo|repo_name|urlencode_strict}}/log/{{next_page.as_ref().unwrap()}}">older commits →</a>
{% endif %}
{% endblock %}=
\ No newline at end of file
diff --git a/templates/refs.html b/templates/refs.html
index 283e1c9..ed0d0fa 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} refs - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} refs - {{config.site_name}}{% endblock %}
{% block head %}
<link rel="alternate" type="application/rss+xml" title="{{repo|repo_name}} tags" href="refs.xml">
diff --git a/templates/repo-navbar.html b/templates/repo-navbar.html
index bd85681..79f03e0 100644
-<table>
+<table class="repo">
<tbody>
- <tr><td><h1>{{repo|repo_name}}</h1></td></tr>
- <tr><td>{{repo|description}}</td></tr>
- <tr class="clone-url"><td>git clone <a href="{{crate::config().clone_base}}/{{repo|repo_name}}">{{crate::config().clone_base}}/{{repo|repo_name}}</a></td></tr>
+ <tr><td class="repo-link"><h1>{{repo|repo_name}}</h1></td></tr>
+ <tr><td class="repo-description">{{repo|description}}</td></tr>
+ <tr class="clone-url"><td>git clone <a href="{{config.clone_base}}/{{repo|repo_name}}">{{config.clone_base}}/{{repo|repo_name}}</a></td></tr>
<tr class="navbar"><td><a href="/{{repo|repo_name|urlencode_strict}}">README</a> | <a href="/{{repo|repo_name|urlencode_strict}}/tree">tree</a> | <a href="/{{repo|repo_name|urlencode_strict}}/log">log</a> | <a href="/{{repo|repo_name|urlencode_strict}}/refs">refs</a></td></tr>
</tbody>
</table>
diff --git a/templates/repo.html b/templates/repo.html
index c5ff7ec..eca145e 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} - {{config.site_name}}{% endblock %}
{% block content %}
{% include "repo-navbar.html" %}
diff --git a/templates/tag.html b/templates/tag.html
index 1015bc1..e30669a 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} tag {{tag.name().unwrap()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} tag {{tag.name().unwrap()}} - {{config.site_name}}{% endblock %}
{% block content %}
{% include "repo-navbar.html" %}
diff --git a/templates/tree.html b/templates/tree.html
index c47e1a6..aa476f7 100644
{% extends "base.html" %}
-{% block title %}{{repo|repo_name}} {{path.display()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} {{path.display()}} - {{config.site_name}}{% endblock %}
{% block content %}
{% include "repo-navbar.html" %}