gitscope
← rgit / Diff · 8fea238 → 8fea238
Changes 8fea238..8fea238
Patch commit
diff --git a/Cargo.lock b/Cargo.lock
index fec37a7..344620e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -769,9 +769,9 @@ dependencies = [
[[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 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[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 @@ dependencies = [
"tower-service",
"tracing",
"tracing-subscriber",
+ "unix_mode",
"uuid",
]
@@ -1716,9 +1717,9 @@ dependencies = [
[[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 @@ dependencies = [
[[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"
@@ -2153,6 +2154,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"
version = "2.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index ce79190..4f78ebb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,4 +29,5 @@ tower-service = "0.3"
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
+++ b/src/git.rs
@@ -14,6 +14,7 @@ use moka::future::Cache;
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 @@ pub struct OpenRepository {
}
impl OpenRepository {
+ pub async fn path(self: Arc<Self>, path: Option<PathBuf>) -> 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<Self>, tag_name: &str) -> DetailedTag {
let reference = format!("refs/tags/{tag_name}");
@@ -289,6 +357,37 @@ impl OpenRepository {
}
}
+pub enum PathDestination {
+ Tree(Vec<TreeItem>),
+ 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)]
pub struct Refs {
pub branch: Vec<Branch>,
@@ -459,6 +558,24 @@ fn fetch_diff_and_stats(
(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))]
fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> String {
let mut diff_output = String::new();
diff --git a/src/main.rs b/src/main.rs
index 4892d22..24621e8 100644
--- a/src/main.rs
+++ b/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 @@ fn static_css(content: &'static [u8]) -> impl Handler<()> {
move || async move {
let mut resp = Response::new(Body::from(content));
resp.headers_mut()
- .insert("Content-Type", HeaderValue::from_static("text/css"));
+ .insert(http::header::CONTENT_TYPE, HeaderValue::from_static("text/css"));
resp
}
}
+
+pub fn into_response<T: Template>(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/src/methods/filters.rs b/src/methods/filters.rs
index 4a98605..2cb33e2 100644
--- a/src/methods/filters.rs
+++ b/src/methods/filters.rs
@@ -1,3 +1,7 @@
pub fn timeago(s: time::OffsetDateTime) -> Result<String, askama::Error> {
Ok(timeago::Formatter::new().convert((time::OffsetDateTime::now_utc() - s).unsigned_abs()))
}
+
+pub fn file_perms(s: &i32) -> Result<String, askama::Error> {
+ 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
+++ b/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};
+use crate::{git::RepositoryMetadataList, Git, into_response};
-#[allow(clippy::unused_async)]
-pub async fn handle(Extension(git): Extension<Arc<Git>>) -> Html<String> {
- #[derive(Template)]
- #[template(path = "index.html")]
- pub struct View {
- pub repositories: Arc<RepositoryMetadataList>,
- }
+#[derive(Template)]
+#[template(path = "index.html")]
+pub struct View {
+ pub repositories: Arc<RepositoryMetadataList>,
+}
+
+pub async fn handle(Extension(git): Extension<Arc<Git>>) -> Response {
+ let repositories = git.fetch_repository_metadata().await;
- Html(
- View {
- repositories: git.fetch_repository_metadata().await,
- }
- .render()
- .unwrap(),
- )
+ 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
+++ b/src/methods/repo.rs
@@ -9,7 +9,7 @@ use axum::{
extract::Query,
handler::Handler,
http::Request,
- response::{Html, IntoResponse, Response},
+ response::{IntoResponse, Response},
Extension,
};
use path_clean::PathClean;
@@ -17,8 +17,8 @@ use serde::Deserialize;
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 @@ impl Deref for Repository {
#[derive(Clone)]
pub struct RepositoryPath(pub PathBuf);
+#[derive(Clone)]
+pub struct ChildPath(pub Option<PathBuf>);
+
impl Deref for RepositoryPath {
type Target = Path;
@@ -42,6 +45,7 @@ impl Deref for RepositoryPath {
}
}
+// this is some wicked, wicked abuse of axum right here...
pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) -> Response {
let mut uri_parts: Vec<&str> = request
.uri()
@@ -51,6 +55,8 @@ pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) ->
.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 @@ pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) ->
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::<PathBuf>().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 @@ pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) ->
let uri = uri_parts.into_iter().collect::<PathBuf>().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,15 +109,15 @@ pub async fn service<ReqBody: Send + 'static>(mut request: Request<ReqBody>) ->
.into_response()
}
-#[allow(clippy::unused_async)]
-pub async fn handle_summary(Extension(repo): Extension<Repository>) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/summary.html")]
- pub struct View {
- repo: Repository,
- }
+#[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<Repository>) -> Response {
+ into_response(&SummaryView { repo })
}
#[derive(Deserialize)]
@@ -96,23 +126,23 @@ pub struct TagQuery {
name: String,
}
+#[derive(Template)]
+#[template(path = "repo/tag.html")]
+pub struct TagView {
+ repo: Repository,
+ tag: DetailedTag,
+}
+
pub async fn handle_tag(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<TagQuery>,
-) -> Html<String> {
- #[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 @@ pub struct LogQuery {
branch: Option<String>,
}
-#[allow(clippy::unused_async)]
+#[derive(Template)]
+#[template(path = "repo/log.html")]
+pub struct LogView {
+ repo: Repository,
+ commits: Vec<Commit>,
+ next_offset: Option<usize>,
+ branch: Option<String>,
+}
+
pub async fn handle_log(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<LogQuery>,
-) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/log.html")]
- pub struct View {
- repo: Repository,
- commits: Vec<Commit>,
- next_offset: Option<usize>,
- branch: Option<String>,
- }
-
+) -> 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,
+ })
+}
+
+#[derive(Template)]
+#[template(path = "repo/refs.html")]
+pub struct RefsView {
+ repo: Repository,
+ refs: Arc<Refs>,
}
-#[allow(clippy::unused_async)]
pub async fn handle_refs(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
-) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/refs.html")]
- pub struct View {
- repo: Repository,
- refs: Arc<Refs>,
- }
-
+) -> 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 })
+}
+
+#[derive(Template)]
+#[template(path = "repo/about.html")]
+pub struct AboutView {
+ repo: Repository,
+ readme: Option<Arc<str>>,
}
-#[allow(clippy::unused_async)]
pub async fn handle_about(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
-) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/about.html")]
- pub struct View {
- repo: Repository,
- readme: Option<Arc<str>>,
- }
-
+) -> Response {
let open_repo = git.clone().repo(repository_path).await;
let readme = open_repo.readme().await;
- Html(View { repo, readme }.render().unwrap())
+ into_response(&AboutView { repo, readme })
+}
+
+#[derive(Template)]
+#[template(path = "repo/commit.html")]
+pub struct CommitView {
+ pub repo: Repository,
+ pub commit: Arc<Commit>,
}
#[derive(Deserialize)]
@@ -199,20 +229,12 @@ pub struct CommitQuery {
id: Option<String>,
}
-#[allow(clippy::unused_async)]
pub async fn handle_commit(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<CommitQuery>,
-) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/commit.html")]
- pub struct View {
- pub repo: Repository,
- pub commit: Arc<Commit>,
- }
-
+) -> 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 @@ pub async fn handle_commit(
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<Repository>) -> Html<String> {
+pub async fn handle_tree(
+ Extension(repo): Extension<Repository>,
+ Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
+ Extension(ChildPath(child_path)): Extension<ChildPath>,
+ Extension(git): Extension<Arc<Git>>,
+) -> Response {
#[derive(Template)]
#[template(path = "repo/tree.html")]
- pub struct View {
+ pub struct TreeView {
+ pub repo: Repository,
+ pub items: Vec<TreeItem>,
+ }
+
+ #[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<Commit>,
}
-#[allow(clippy::unused_async)]
pub async fn handle_diff(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<CommitQuery>,
-) -> Html<String> {
- #[derive(Template)]
- #[template(path = "repo/diff.html")]
- pub struct View {
- pub repo: Repository,
- pub commit: Arc<Commit>,
- }
-
+) -> 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 @@ pub async fn handle_diff(
Arc::new(open_repo.latest_commit().await)
};
- Html(View { repo, commit }.render().unwrap())
+ into_response(&DiffView { repo, commit })
}
diff --git a/statics/style.css b/statics/style.css
index 4cf9841..1018024 100644
--- a/statics/style.css
+++ b/statics/style.css
@@ -110,3 +110,8 @@ pre.h2-first-line::first-line {
background: #ffebe9;
display: block;
}
+
+.nested-tree {
+ color: blue !important;
+ font-weight: bold;
+}
diff --git a/templates/repo/file.html b/templates/repo/file.html
new file mode 100644
index 0000000..b9fc882
--- /dev/null
+++ b/templates/repo/file.html
@@ -0,0 +1,11 @@
+{% extends "repo/base.html" %}
+
+{% block head %}
+<link rel="stylesheet" type="text/css" href="/highlight.css" />
+{% endblock %}
+
+{% block tree_nav_class %}active{% endblock %}
+
+{% block content %}
+<pre>{{ file.content|safe }}</pre>
+{% endblock %}
\ No newline at end of file
diff --git a/templates/repo/tree.html b/templates/repo/tree.html
index 1fac543..cb27281 100644
--- a/templates/repo/tree.html
+++ b/templates/repo/tree.html
@@ -3,4 +3,32 @@
{% block tree_nav_class %}active{% endblock %}
{% block content %}
+<table class="repositories">
+ <thead>
+ <tr>
+ <th style="width: 10rem;">Mode</th>
+ <th>Name</th>
+ <th>Size</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for item in items %}
+ <tr>
+ {% match item %}
+ {% when crate::git::TreeItem::Tree with (tree) %}
+ <td><pre>{{ tree.mode|file_perms }}</pre></td>
+ <td><pre><a class="nested-tree" href="/{{ repo.display() }}/tree/{{ tree.path.display() }}">{{ tree.name }}</a></pre></td>
+ <td></td>
+ <td></td>
+ {% when crate::git::TreeItem::File with (file) %}
+ <td><pre>{{ file.mode|file_perms }}</pre></td>
+ <td><pre><a href="/{{ repo.display() }}/tree/{{ file.path.display() }}">{{ file.name }}</a></pre></td>
+ <td><pre>{{ file.size }}</pre></td>
+ <td></td>
+ {% endmatch %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
{% endblock %}
\ No newline at end of file