🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-09 1:34:09.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-09 1:34:09.0 +01:00:00
commit
8fea2385e33999bf7dd71956f64568cba52d9064 [patch]
tree
74c3e9d68708b13d1b71e60cecd707a988d53272
parent
f3142420736b58f814f89572e30a7ee8961cf059
download
8fea2385e33999bf7dd71956f64568cba52d9064.tar.gz

Implement simple tree browser and file view pages



Diff

 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<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}");
@@ -287,6 +355,37 @@
        .await
        .unwrap()
    }
}

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)]
@@ -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: 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/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<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
+++ 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<Arc<Git>>) -> Html<String> {
    #[derive(Template)]
    #[template(path = "index.html")]
    pub struct View {
        pub repositories: Arc<RepositoryMetadataList>,
    }

    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<RepositoryMetadataList>,
}

pub async fn handle(Extension(git): Extension<Arc<Git>>) -> 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<PathBuf>);

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<ReqBody: Send + 'static>(mut request: Request<ReqBody>) -> 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::<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 @@
    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,21 +109,28 @@
        .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)]
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<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 @@
    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,
    })
}

#[allow(clippy::unused_async)]
#[derive(Template)]
#[template(path = "repo/refs.html")]
pub struct RefsView {
    repo: Repository,
    refs: Arc<Refs>,
}

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 })
}

#[allow(clippy::unused_async)]
#[derive(Template)]
#[template(path = "repo/about.html")]
pub struct AboutView {
    repo: Repository,
    readme: Option<Arc<str>>,
}

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;

    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<Commit>,
}

#[derive(Deserialize)]
@@ -199,20 +229,12 @@
    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 @@
        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 @@
        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 %}
<link rel="stylesheet" type="text/css" href="/highlight.css" />
{% endblock %}

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

{% block content %}
<pre>{{ file.content|safe }}</pre>
{% 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 %}
<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 %}