From 8fea2385e33999bf7dd71956f64568cba52d9064 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 09 Jul 2022 01:34:09 +0100 Subject: [PATCH] Implement simple tree browser and file view pages --- Cargo.lock | 23 +++++++++++++++-------- Cargo.toml | 1 + src/git.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 24 ++++++++++++++++++++---- statics/style.css | 5 +++++ src/methods/filters.rs | 4 ++++ src/methods/index.rs | 33 ++++++++++++++++----------------- src/methods/repo.rs | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- templates/repo/file.html | 11 +++++++++++ templates/repo/tree.html | 28 ++++++++++++++++++++++++++++ 10 files changed, 339 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fec37a7..344620e 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -769,9 +769,9 @@ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" [[package]] name = "hermit-abi" @@ -842,9 +842,9 @@ [[package]] name = "hyper" -version = "0.14.19" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -1623,6 +1623,7 @@ "tower-service", "tracing", "tracing-subscriber", + "unix_mode", "uuid", ] @@ -1716,9 +1717,9 @@ [[package]] name = "serde_yaml" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +checksum = "1ec0091e1f5aa338283ce049bd9dfefd55e1f168ac233e85c1ffe0038fb48cbe" dependencies = [ "indexmap", "ryu", @@ -2107,9 +2108,9 @@ [[package]] name = "triomphe" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0abf5a9b5ad4a5ac1393956ae03fb57033749d3983e2cac9afbfd5ae04ec2" +checksum = "7fe1b3800b35f9b936c28dc59dbda91b195371269396784d931fe2a5a2be3d2f" [[package]] name = "try-lock" @@ -2152,6 +2153,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unix_mode" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35abed4630bb800f02451a7428205d1f37b8e125001471bfab259beee6a587ed" [[package]] name = "url" diff --git a/Cargo.toml b/Cargo.toml index ce79190..4f78ebb 100644 --- a/Cargo.toml +++ a/Cargo.toml @@ -29,4 +29,5 @@ tower-layer = "0.3" tracing = "0.1" tracing-subscriber = "0.3" +unix_mode = "0.1" uuid = { version = "1.1", features = ["v4"] } diff --git a/src/git.rs b/src/git.rs index 6551bf1..1ec4f27 100644 --- a/src/git.rs +++ a/src/git.rs @@ -14,6 +14,7 @@ use parking_lot::Mutex; use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; use time::OffsetDateTime; use tracing::instrument; @@ -96,6 +97,73 @@ } impl OpenRepository { + pub async fn path(self: Arc, path: Option) -> PathDestination { + tokio::task::spawn_blocking(move || { + let repo = self.repo.lock(); + + let head = repo.head().unwrap(); + let mut tree = head.peel_to_tree().unwrap(); + + if let Some(path) = path.as_ref() { + let item = tree.get_path(&path).unwrap(); + let object = item.to_object(&repo).unwrap(); + + if let Some(blob) = object.as_blob() { + let name = item.name().unwrap().to_string(); + let path = path.clone().join(&name); + + let extension = path.extension() + .or(path.file_name()) + .unwrap() + .to_string_lossy(); + let content = format_file(blob.content(), &extension, &self.git.syntax_set); + + return PathDestination::File(FileWithContent { + metadata: File { + mode: item.filemode(), + size: blob.size(), + path, + name, + }, + content, + }); + } else if let Ok(new_tree) = object.into_tree() { + tree = new_tree; + } else { + panic!("unknown item kind"); + } + } + + let mut tree_items = Vec::new(); + + for item in tree.iter() { + let object = item.to_object(&repo).unwrap(); + + let name = item.name().unwrap().to_string(); + let path = path.clone().unwrap_or_default().join(&name); + + if let Some(blob) = object.as_blob() { + tree_items.push(TreeItem::File(File { + mode: item.filemode(), + size: blob.size(), + path, + name, + })); + } else if let Some(_tree) = object.as_tree() { + tree_items.push(TreeItem::Tree(Tree { + mode: item.filemode(), + path, + name, + })); + } + } + + PathDestination::Tree(tree_items) + }) + .await + .unwrap() + } + #[instrument(skip(self))] pub async fn tag_info(self: Arc, tag_name: &str) -> DetailedTag { let reference = format!("refs/tags/{tag_name}"); @@ -287,6 +355,37 @@ .await .unwrap() } +} + +pub enum PathDestination { + Tree(Vec), + File(FileWithContent), +} + +pub enum TreeItem { + Tree(Tree), + File(File), +} + +#[derive(Debug)] +pub struct Tree { + pub mode: i32, + pub name: String, + pub path: PathBuf, +} + +#[derive(Debug)] +pub struct File { + pub mode: i32, + pub size: usize, + pub name: String, + pub path: PathBuf, +} + +#[derive(Debug)] +pub struct FileWithContent { + pub metadata: File, + pub content: String, } #[derive(Debug, Default)] @@ -457,6 +556,24 @@ let diff_output = format_diff(&diff, &syntax_set); (diff_output, diff_stats) +} + +fn format_file(content: &[u8], extension: &str, syntax_set: &SyntaxSet) -> String { + let content = std::str::from_utf8(content).unwrap(); + + let syntax = syntax_set + .find_syntax_by_extension(&extension) + .unwrap_or(syntax_set.find_syntax_plain_text()); + let mut html_generator = + ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced); + + for line in LinesWithEndings::from(content) { + html_generator + .parse_html_for_line_which_includes_newline(line) + .unwrap(); + } + + html_generator.finalize() } #[instrument(skip(diff, syntax_set))] diff --git a/src/main.rs b/src/main.rs index 4892d22..24621e8 100644 --- a/src/main.rs +++ a/src/main.rs @@ -1,11 +1,11 @@ #![deny(clippy::pedantic)] -use axum::{ - body::Body, handler::Handler, http::HeaderValue, response::Response, routing::get, Extension, - Router, -}; +use axum::{body::Body, handler::Handler, http::HeaderValue, response::Response, routing::get, Extension, Router, http}; use bat::assets::HighlightingAssets; use std::sync::Arc; +use askama::Template; +use axum::http::StatusCode; +use axum::response::IntoResponse; use syntect::html::ClassStyle; use tower_layer::layer_fn; @@ -55,7 +55,21 @@ move || async move { let mut resp = Response::new(Body::from(content)); resp.headers_mut() - .insert("Content-Type", HeaderValue::from_static("text/css")); + .insert(http::header::CONTENT_TYPE, HeaderValue::from_static("text/css")); resp + } +} + +pub fn into_response(t: &T) -> Response { + match t.render() { + Ok(body) => { + let headers = [( + http::header::CONTENT_TYPE, + HeaderValue::from_static(T::MIME_TYPE), + )]; + + (headers, body).into_response() + } + Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/statics/style.css b/statics/style.css index 4cf9841..1018024 100644 --- a/statics/style.css +++ a/statics/style.css @@ -110,3 +110,8 @@ background: #ffebe9; display: block; } + +.nested-tree { + color: blue !important; + font-weight: bold; +} diff --git a/src/methods/filters.rs b/src/methods/filters.rs index 4a98605..2cb33e2 100644 --- a/src/methods/filters.rs +++ a/src/methods/filters.rs @@ -1,3 +1,7 @@ pub fn timeago(s: time::OffsetDateTime) -> Result { Ok(timeago::Formatter::new().convert((time::OffsetDateTime::now_utc() - s).unsigned_abs())) } + +pub fn file_perms(s: &i32) -> Result { + Ok(unix_mode::to_string(s.unsigned_abs())) +} diff --git a/src/methods/index.rs b/src/methods/index.rs index 7281242..2d02a87 100644 --- a/src/methods/index.rs +++ a/src/methods/index.rs @@ -1,24 +1,21 @@ use askama::Template; -use axum::response::Html; +use axum::response::Response; use axum::Extension; use std::sync::Arc; use super::filters; -use crate::{git::RepositoryMetadataList, Git}; - -#[allow(clippy::unused_async)] -pub async fn handle(Extension(git): Extension>) -> Html { - #[derive(Template)] - #[template(path = "index.html")] - pub struct View { - pub repositories: Arc, - } - - Html( - View { - repositories: git.fetch_repository_metadata().await, - } - .render() - .unwrap(), - ) +use crate::{git::RepositoryMetadataList, Git, into_response}; + +#[derive(Template)] +#[template(path = "index.html")] +pub struct View { + pub repositories: Arc, +} + +pub async fn handle(Extension(git): Extension>) -> Response { + let repositories = git.fetch_repository_metadata().await; + + into_response(&View { + repositories, + }) } diff --git a/src/methods/repo.rs b/src/methods/repo.rs index bbaf83c..ecf9df9 100644 --- a/src/methods/repo.rs +++ a/src/methods/repo.rs @@ -9,7 +9,7 @@ extract::Query, handler::Handler, http::Request, - response::{Html, IntoResponse, Response}, + response::{IntoResponse, Response}, Extension, }; use path_clean::PathClean; @@ -17,8 +17,8 @@ use tower::{util::BoxCloneService, Service}; use super::filters; -use crate::git::{DetailedTag, Refs}; -use crate::{git::Commit, layers::UnwrapInfallible, Git}; +use crate::git::{DetailedTag, FileWithContent, PathDestination, Refs, TreeItem}; +use crate::{git::Commit, layers::UnwrapInfallible, Git, into_response}; #[derive(Clone)] pub struct Repository(pub PathBuf); @@ -34,6 +34,9 @@ #[derive(Clone)] pub struct RepositoryPath(pub PathBuf); +#[derive(Clone)] +pub struct ChildPath(pub Option); + impl Deref for RepositoryPath { type Target = Path; @@ -42,6 +45,7 @@ } } +// this is some wicked, wicked abuse of axum right here... pub async fn service(mut request: Request) -> Response { let mut uri_parts: Vec<&str> = request .uri() @@ -51,6 +55,8 @@ .split('/') .collect(); + let mut child_path = None; + let mut service = match uri_parts.pop() { Some("about") => BoxCloneService::new(handle_about.into_service()), Some("refs") => BoxCloneService::new(handle_refs.into_service()), @@ -61,7 +67,30 @@ Some("tag") => BoxCloneService::new(handle_tag.into_service()), Some(v) => { uri_parts.push(v); - BoxCloneService::new(handle_summary.into_service()) + + // match tree children + if uri_parts.iter().any(|v| *v == "tree") { + // TODO: this needs fixing up so it doesn't accidentally match repos that have + // `tree` in their path + let mut reconstructed_path = Vec::new(); + + while let Some(part) = uri_parts.pop() { + if part == "tree" { + break; + } + + // TODO: FIXME + reconstructed_path.insert(0, part); + } + + child_path = Some(reconstructed_path.into_iter().collect::().clean()); + + eprintln!("repo path: {:?}", child_path); + + BoxCloneService::new(handle_tree.into_service()) + } else { + BoxCloneService::new(handle_summary.into_service()) + } } None => panic!("not found"), }; @@ -69,6 +98,7 @@ let uri = uri_parts.into_iter().collect::().clean(); let path = Path::new("../test-git").canonicalize().unwrap().join(&uri); + request.extensions_mut().insert(ChildPath(child_path)); request.extensions_mut().insert(Repository(uri)); request.extensions_mut().insert(RepositoryPath(path)); @@ -79,21 +109,28 @@ .into_response() } -#[allow(clippy::unused_async)] -pub async fn handle_summary(Extension(repo): Extension) -> Html { - #[derive(Template)] - #[template(path = "repo/summary.html")] - pub struct View { - repo: Repository, - } +#[derive(Template)] +#[template(path = "repo/summary.html")] +pub struct SummaryView { + pub repo: Repository, +} - Html(View { repo }.render().unwrap()) +#[allow(clippy::unused_async)] +pub async fn handle_summary(Extension(repo): Extension) -> Response { + into_response(&SummaryView { repo }) } #[derive(Deserialize)] pub struct TagQuery { #[serde(rename = "h")] name: String, +} + +#[derive(Template)] +#[template(path = "repo/tag.html")] +pub struct TagView { + repo: Repository, + tag: DetailedTag, } pub async fn handle_tag( @@ -101,18 +138,11 @@ Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, Query(query): Query, -) -> Html { - #[derive(Template)] - #[template(path = "repo/tag.html")] - pub struct View { - repo: Repository, - tag: DetailedTag, - } - +) -> Response { let open_repo = git.repo(repository_path).await; let tag = open_repo.tag_info(&query.name).await; - Html(View { repo, tag }.render().unwrap()) + into_response(&TagView { repo, tag }) } #[derive(Deserialize)] @@ -123,75 +153,75 @@ branch: Option, } -#[allow(clippy::unused_async)] +#[derive(Template)] +#[template(path = "repo/log.html")] +pub struct LogView { + repo: Repository, + commits: Vec, + next_offset: Option, + branch: Option, +} + pub async fn handle_log( Extension(repo): Extension, Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, Query(query): Query, -) -> Html { - #[derive(Template)] - #[template(path = "repo/log.html")] - pub struct View { - repo: Repository, - commits: Vec, - next_offset: Option, - branch: Option, - } - +) -> Response { let open_repo = git.repo(repository_path).await; let (commits, next_offset) = open_repo .commits(query.branch.as_deref(), query.offset.unwrap_or(0)) .await; - Html( - View { - repo, - commits, - next_offset, - branch: query.branch, - } - .render() - .unwrap(), - ) + into_response(&LogView { + repo, + commits, + next_offset, + branch: query.branch, + }) } -#[allow(clippy::unused_async)] +#[derive(Template)] +#[template(path = "repo/refs.html")] +pub struct RefsView { + repo: Repository, + refs: Arc, +} + pub async fn handle_refs( Extension(repo): Extension, Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, -) -> Html { - #[derive(Template)] - #[template(path = "repo/refs.html")] - pub struct View { - repo: Repository, - refs: Arc, - } - +) -> Response { let open_repo = git.repo(repository_path).await; let refs = open_repo.refs().await; - Html(View { repo, refs }.render().unwrap()) + into_response(&RefsView { repo, refs }) } -#[allow(clippy::unused_async)] +#[derive(Template)] +#[template(path = "repo/about.html")] +pub struct AboutView { + repo: Repository, + readme: Option>, +} + pub async fn handle_about( Extension(repo): Extension, Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, -) -> Html { - #[derive(Template)] - #[template(path = "repo/about.html")] - pub struct View { - repo: Repository, - readme: Option>, - } - +) -> Response { let open_repo = git.clone().repo(repository_path).await; let readme = open_repo.readme().await; + + into_response(&AboutView { repo, readme }) +} - Html(View { repo, readme }.render().unwrap()) +#[derive(Template)] +#[template(path = "repo/commit.html")] +pub struct CommitView { + pub repo: Repository, + pub commit: Arc, } #[derive(Deserialize)] @@ -199,20 +229,12 @@ id: Option, } -#[allow(clippy::unused_async)] pub async fn handle_commit( Extension(repo): Extension, Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, Query(query): Query, -) -> Html { - #[derive(Template)] - #[template(path = "repo/commit.html")] - pub struct View { - pub repo: Repository, - pub commit: Arc, - } - +) -> Response { let open_repo = git.repo(repository_path).await; let commit = if let Some(commit) = query.id { open_repo.commit(&commit).await @@ -220,34 +242,50 @@ Arc::new(open_repo.latest_commit().await) }; - Html(View { repo, commit }.render().unwrap()) + into_response(&CommitView { repo, commit }) } -#[allow(clippy::unused_async)] -pub async fn handle_tree(Extension(repo): Extension) -> Html { +pub async fn handle_tree( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(ChildPath(child_path)): Extension, + Extension(git): Extension>, +) -> Response { #[derive(Template)] #[template(path = "repo/tree.html")] - pub struct View { + pub struct TreeView { + pub repo: Repository, + pub items: Vec, + } + + #[derive(Template)] + #[template(path = "repo/file.html")] + pub struct FileView { pub repo: Repository, + pub file: FileWithContent, + } + + let open_repo = git.repo(repository_path).await; + + match open_repo.path(child_path).await { + PathDestination::Tree(items) => into_response(&TreeView { repo, items }), + PathDestination::File(file) => into_response(&FileView { repo, file }), } +} - Html(View { repo }.render().unwrap()) +#[derive(Template)] +#[template(path = "repo/diff.html")] +pub struct DiffView { + pub repo: Repository, + pub commit: Arc, } -#[allow(clippy::unused_async)] pub async fn handle_diff( Extension(repo): Extension, Extension(RepositoryPath(repository_path)): Extension, Extension(git): Extension>, Query(query): Query, -) -> Html { - #[derive(Template)] - #[template(path = "repo/diff.html")] - pub struct View { - pub repo: Repository, - pub commit: Arc, - } - +) -> Response { let open_repo = git.repo(repository_path).await; let commit = if let Some(commit) = query.id { open_repo.commit(&commit).await @@ -255,5 +293,5 @@ Arc::new(open_repo.latest_commit().await) }; - Html(View { repo, commit }.render().unwrap()) + into_response(&DiffView { repo, commit }) } diff --git a/templates/repo/file.html b/templates/repo/file.html new file mode 100644 index 0000000..b9fc882 100644 --- /dev/null +++ a/templates/repo/file.html @@ -1,0 +1,11 @@ +{% extends "repo/base.html" %} + +{% block head %} + +{% endblock %} + +{% block tree_nav_class %}active{% endblock %} + +{% block content %} +
{{ file.content|safe }}
+{% endblock %}diff --git a/templates/repo/tree.html b/templates/repo/tree.html index 1fac543..cb27281 100644 --- a/templates/repo/tree.html +++ a/templates/repo/tree.html @@ -1,6 +1,34 @@ {% extends "repo/base.html" %} {% block tree_nav_class %}active{% endblock %} {% block content %} + + + + + + + + + + + {% for item in items %} + + {% match item %} + {% when crate::git::TreeItem::Tree with (tree) %} + + + + + {% when crate::git::TreeItem::File with (file) %} + + + + + {% endmatch %} + + {% endfor %} + +
ModeNameSize
{{ tree.mode|file_perms }}
{{ tree.name }}
{{ file.mode|file_perms }}
{{ file.name }}
{{ file.size }}
{% endblock %}-- rgit 0.1.3