From f74037c1b1ba30e54d3f1b6008589ff60568e6b2 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 23 Jul 2022 14:05:49 +0100 Subject: [PATCH] Split repo methods into their own modules --- build.rs | 3 ++- rustfmt.toml | 7 +++++++ src/git.rs | 39 +++++++++++++++++++++------------------ src/git_cgi.rs | 12 +++++++----- src/main.rs | 13 ++++++++----- src/syntax_highlight.rs | 13 ++++++++----- src/database/indexer.rs | 7 +++++-- src/methods/index.rs | 8 +++----- src/methods/repo.rs | 509 -------------------------------------------------------------------------------- src/database/schema/commit.rs | 7 ++++--- src/database/schema/prefixes.rs | 3 ++- src/database/schema/repository.rs | 12 ++++-------- src/database/schema/tag.rs | 8 ++++---- src/methods/repo/about.rs | 29 +++++++++++++++++++++++++++++ src/methods/repo/commit.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/methods/repo/diff.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/log.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/mod.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/refs.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/smart_git.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/summary.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/methods/repo/tag.rs | 37 +++++++++++++++++++++++++++++++++++++ src/methods/repo/tree.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 23 files changed, 712 insertions(+), 566 deletions(-) diff --git a/build.rs b/build.rs index cbcc334..b16d138 100644 --- a/build.rs +++ a/build.rs @@ -1,8 +1,9 @@ -use anyhow::Context; use std::{ io::Write, path::{Path, PathBuf}, }; + +use anyhow::Context; #[derive(Copy, Clone)] pub struct Paths<'a> { diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d440511 100644 --- /dev/null +++ a/rustfmt.toml @@ -1,0 +1,7 @@ +edition = "2021" +## not yet supported on stable +#imports_granularity = "Crate" +newline_style = "Unix" +## not yet supported on stable +#group_imports = "StdExternalCrate" +use_field_init_shorthand = true diff --git a/src/git.rs b/src/git.rs index c95cde7..f1414c4 100644 --- a/src/git.rs +++ a/src/git.rs @@ -1,8 +1,12 @@ -use std::ffi::OsStr; -use std::path::Path; -use std::{borrow::Cow, fmt::Write, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + borrow::Cow, + ffi::OsStr, + fmt::Write, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; -use crate::syntax_highlight::ComrakSyntectAdapter; use anyhow::{Context, Result}; use bytes::{Bytes, BytesMut}; use comrak::{ComrakOptions, ComrakPlugins}; @@ -11,11 +15,15 @@ }; use moka::future::Cache; use parking_lot::Mutex; -use syntect::html::{ClassStyle, ClassedHTMLGenerator}; -use syntect::parsing::SyntaxSet; -use syntect::util::LinesWithEndings; +use syntect::{ + html::{ClassStyle, ClassedHTMLGenerator}, + parsing::SyntaxSet, + util::LinesWithEndings, +}; use time::OffsetDateTime; use tracing::instrument; + +use crate::syntax_highlight::ComrakSyntectAdapter; pub struct Git { commits: Cache>, @@ -109,8 +117,7 @@ let extension = path .extension() .or_else(|| path.file_name()) - .map(|v| v.to_string_lossy()) - .unwrap_or_else(|| Cow::Borrowed("")); + .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy); let content = format_file(blob.content(), &extension, &self.git.syntax_set)?; return Ok(PathDestination::File(FileWithContent { @@ -187,8 +194,7 @@ tagger: tag.tagger().map(TryInto::try_into).transpose()?, message: tag .message_bytes() - .map(String::from_utf8_lossy) - .unwrap_or_else(|| Cow::Borrowed("")) + .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy) .into_owned(), tagged_object, }) @@ -440,13 +446,11 @@ parents: commit.parent_ids().map(|v| v.to_string()).collect(), summary: commit .summary_bytes() - .map(String::from_utf8_lossy) - .unwrap_or_else(|| Cow::Borrowed("")) + .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy) .into_owned(), body: commit .body_bytes() - .map(String::from_utf8_lossy) - .unwrap_or_else(|| Cow::Borrowed("")) + .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy) .into_owned(), diff_stats: String::with_capacity(0), diff: String::with_capacity(0), @@ -559,8 +563,7 @@ if let Some(path) = delta.new_file().path() { path.extension() .or_else(|| path.file_name()) - .map(|v| v.to_string_lossy()) - .unwrap_or_else(|| Cow::Borrowed("")) + .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy) } else { Cow::Borrowed("") } @@ -572,7 +575,7 @@ .unwrap_or_else(|| syntax_set.find_syntax_plain_text()); let mut html_generator = ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, ClassStyle::Spaced); - let _ = html_generator.parse_html_for_line_which_includes_newline(&line); + let _res = html_generator.parse_html_for_line_which_includes_newline(&line); if let Some(class) = class { let _ = write!(diff_output, r#""#); } diff --git a/src/git_cgi.rs b/src/git_cgi.rs index b425da9..d46101f 100644 --- a/src/git_cgi.rs +++ a/src/git_cgi.rs @@ -1,10 +1,12 @@ +use std::str::FromStr; + use anyhow::{bail, Context, Result}; -use axum::body::{boxed, Body}; -use axum::http::header::HeaderName; -use axum::http::HeaderValue; -use axum::response::Response; +use axum::{ + body::{boxed, Body}, + http::{header::HeaderName, HeaderValue}, + response::Response, +}; use httparse::Status; -use std::str::FromStr; // https://en.wikipedia.org/wiki/Common_Gateway_Interface pub fn cgi_to_response(buffer: &[u8]) -> Result { diff --git a/src/main.rs b/src/main.rs index 8d04be6..baf0d2d 100644 --- a/src/main.rs +++ a/src/main.rs @@ -1,15 +1,18 @@ #![deny(clippy::pedantic)] +use std::{sync::Arc, time::Duration}; + use askama::Template; -use axum::http::StatusCode; -use axum::response::IntoResponse; use axum::{ - body::Body, handler::Handler, http, http::HeaderValue, response::Response, routing::get, + body::Body, + handler::Handler, + http, + http::{HeaderValue, StatusCode}, + response::{IntoResponse, Response}, + routing::get, Extension, Router, }; use bat::assets::HighlightingAssets; -use std::sync::Arc; -use std::time::Duration; use syntect::html::ClassStyle; use tower_layer::layer_fn; use tracing::{info, instrument}; diff --git a/src/syntax_highlight.rs b/src/syntax_highlight.rs index c3e1c1e..f0fab88 100644 --- a/src/syntax_highlight.rs +++ a/src/syntax_highlight.rs @@ -1,8 +1,11 @@ -use comrak::adapters::SyntaxHighlighterAdapter; use std::collections::HashMap; -use syntect::html::{ClassStyle, ClassedHTMLGenerator}; -use syntect::parsing::SyntaxSet; -use syntect::util::LinesWithEndings; + +use comrak::adapters::SyntaxHighlighterAdapter; +use syntect::{ + html::{ClassStyle, ClassedHTMLGenerator}, + parsing::SyntaxSet, + util::LinesWithEndings, +}; pub struct ComrakSyntectAdapter<'a> { pub(crate) syntax_set: &'a SyntaxSet, @@ -18,7 +21,7 @@ ClassedHTMLGenerator::new_with_class_style(syntax, self.syntax_set, ClassStyle::Spaced); for line in LinesWithEndings::from(code) { - let _ = html_generator.parse_html_for_line_which_includes_newline(line); + let _res = html_generator.parse_html_for_line_which_includes_newline(line); } format!( diff --git a/src/database/indexer.rs b/src/database/indexer.rs index 0259c49..65187ed 100644 --- a/src/database/indexer.rs +++ a/src/database/indexer.rs @@ -1,6 +1,9 @@ +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; + use git2::Sort; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; use time::OffsetDateTime; use tracing::{info, info_span}; diff --git a/src/methods/index.rs b/src/methods/index.rs index 46e2a64..747ee22 100644 --- a/src/methods/index.rs +++ a/src/methods/index.rs @@ -1,13 +1,11 @@ -use anyhow::Context; use std::collections::BTreeMap; +use anyhow::Context; use askama::Template; -use axum::response::Response; -use axum::Extension; +use axum::{response::Response, Extension}; use super::filters; -use crate::database::schema::repository::Repository; -use crate::into_response; +use crate::{database::schema::repository::Repository, into_response}; #[derive(Template)] #[template(path = "index.html")] diff --git a/src/methods/repo.rs b/src/methods/repo.rs deleted file mode 100644 index 47a6f06..0000000 100644 --- a/src/methods/repo.rs +++ /dev/null @@ -1,509 +1,0 @@ -use anyhow::Context; -use std::collections::BTreeMap; -use std::{ - fmt::{Debug, Display, Formatter}, - io::Write, - ops::Deref, - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, -}; - -use askama::Template; -use axum::http::StatusCode; -use axum::{ - body::HttpBody, - extract::Query, - handler::Handler, - http, - http::HeaderValue, - http::Request, - response::{IntoResponse, Response}, - Extension, -}; -use bytes::Bytes; -use path_clean::PathClean; -use serde::Deserialize; -use tower::{util::BoxCloneService, Service}; -use yoke::Yoke; - -use super::filters; -use crate::database::schema::commit::YokedCommit; -use crate::database::schema::tag::YokedTag; -use crate::git::{DetailedTag, FileWithContent, PathDestination, ReadmeFormat, TreeItem}; -use crate::{git::Commit, into_response, layers::UnwrapInfallible, Git}; - -#[derive(Clone)] -pub struct Repository(pub PathBuf); - -impl Deref for Repository { - type Target = Path; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Clone)] -pub struct RepositoryPath(pub PathBuf); - -#[derive(Clone)] -pub struct ChildPath(pub Option); - -impl Deref for RepositoryPath { - type Target = Path; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub type Result = std::result::Result; - -pub struct Error(anyhow::Error); - -impl From> for Error { - fn from(e: Arc) -> Self { - Self(anyhow::Error::msg(format!("{:?}", e))) - } -} - -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self(e) - } -} - -impl IntoResponse for Error { - fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response() - } -} - -// this is some wicked, wicked abuse of axum right here... -pub async fn service( - mut request: Request, -) -> Response -where - ::Data: Send + Sync, - ::Error: std::error::Error + Send + Sync, -{ - let mut uri_parts: Vec<&str> = request - .uri() - .path() - .trim_start_matches('/') - .trim_end_matches('/') - .split('/') - .collect(); - - let mut child_path = None; - - macro_rules! h { - ($handler:ident) => { - BoxCloneService::new($handler.into_service()) - }; - } - - let mut service = match uri_parts.pop() { - Some("about") => BoxCloneService::new(handle_about.into_service()), - // TODO: https://man.archlinux.org/man/git-http-backend.1.en - // TODO: GIT_PROTOCOL - Some("refs") if uri_parts.last() == Some(&"info") => { - uri_parts.pop(); - h!(handle_info_refs) - } - Some("git-upload-pack") => h!(handle_git_upload_pack), - Some("refs") => h!(handle_refs), - Some("log") => h!(handle_log), - Some("tree") => h!(handle_tree), - Some("commit") => h!(handle_commit), - Some("diff") => h!(handle_diff), - Some("patch") => h!(handle_patch), - Some("tag") => h!(handle_tag), - Some(v) => { - uri_parts.push(v); - - // 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()); - - h!(handle_tree) - } else { - h!(handle_summary) - } - } - None => panic!("not found"), - }; - - 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)); - - service - .call(request) - .await - .unwrap_infallible() - .into_response() -} - -pub struct Refs { - heads: BTreeMap, - tags: Vec<(String, YokedTag)>, -} - -#[derive(Template)] -#[template(path = "repo/summary.html")] -pub struct SummaryView<'a> { - repo: Repository, - refs: Refs, - commit_list: Vec<&'a crate::database::schema::commit::Commit<'a>>, -} - -pub async fn handle_summary( - Extension(repo): Extension, - Extension(db): Extension, -) -> Result { - let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? - .context("Repository does not exist")?; - let commit_tree = repository.get().commit_tree(&db, "refs/heads/master")?; - let commits = commit_tree.fetch_latest(11, 0).await; - let commit_list = commits.iter().map(Yoke::get).collect(); - - let mut heads = BTreeMap::new(); - for head in repository.get().heads(&db) { - let commit_tree = repository.get().commit_tree(&db, &head)?; - let name = head.strip_prefix("refs/heads/"); - - if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) { - heads.insert(name.to_string(), commit); - } - } - - let tags = repository - .get() - .tag_tree(&db) - .context("Failed to fetch indexed tags")? - .fetch_all(); - - Ok(into_response(&SummaryView { - repo, - refs: Refs { heads, tags }, - commit_list, - })) -} - -#[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( - Extension(repo): Extension, - Extension(RepositoryPath(repository_path)): Extension, - Extension(git): Extension>, - Query(query): Query, -) -> Result { - let open_repo = git.repo(repository_path).await?; - let tag = open_repo.tag_info(&query.name).await?; - - Ok(into_response(&TagView { repo, tag })) -} - -#[derive(Deserialize)] -pub struct LogQuery { - #[serde(rename = "ofs")] - offset: Option, - #[serde(rename = "h")] - branch: Option, -} - -#[derive(Template)] -#[template(path = "repo/log.html")] -pub struct LogView<'a> { - repo: Repository, - commits: Vec<&'a crate::database::schema::commit::Commit<'a>>, - next_offset: Option, - branch: Option, -} - -pub async fn handle_log( - Extension(repo): Extension, - Extension(db): Extension, - Query(query): Query, -) -> Result { - let offset = query.offset.unwrap_or(0); - - let reference = format!("refs/heads/{}", query.branch.as_deref().unwrap_or("master")); - let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? - .context("Repository does not exist")?; - let commit_tree = repository.get().commit_tree(&db, &reference)?; - let mut commits = commit_tree.fetch_latest(101, offset).await; - - let next_offset = if commits.len() == 101 { - commits.pop(); - Some(offset + 100) - } else { - None - }; - - let commits = commits.iter().map(Yoke::get).collect(); - - Ok(into_response(&LogView { - repo, - commits, - next_offset, - branch: query.branch, - })) -} - -#[derive(Deserialize)] -pub struct SmartGitQuery { - service: String, -} - -pub async fn handle_info_refs( - Extension(RepositoryPath(repository_path)): Extension, - Query(query): Query, -) -> Result { - // todo: tokio command - let out = std::process::Command::new("git") - .arg("http-backend") - .env("REQUEST_METHOD", "GET") - .env("PATH_INFO", "/info/refs") - .env("GIT_PROJECT_ROOT", repository_path) - .env("QUERY_STRING", format!("service={}", query.service)) - .output() - .unwrap(); - - Ok(crate::git_cgi::cgi_to_response(&out.stdout)?) -} - -pub async fn handle_git_upload_pack( - Extension(RepositoryPath(repository_path)): Extension, - body: Bytes, -) -> Result { - // todo: tokio command - let mut child = std::process::Command::new("git") - .arg("http-backend") - // todo: read all this from request - .env("REQUEST_METHOD", "POST") - .env("CONTENT_TYPE", "application/x-git-upload-pack-request") - .env("PATH_INFO", "/git-upload-pack") - .env("GIT_PROJECT_ROOT", repository_path) - .stdout(Stdio::piped()) - .stdin(Stdio::piped()) - .spawn() - .unwrap(); - child.stdin.as_mut().unwrap().write_all(&body).unwrap(); - let out = child.wait_with_output().unwrap(); - - Ok(crate::git_cgi::cgi_to_response(&out.stdout)?) -} - -#[derive(Template)] -#[template(path = "repo/refs.html")] -pub struct RefsView { - repo: Repository, - refs: Refs, -} - -pub async fn handle_refs( - Extension(repo): Extension, - Extension(db): Extension, -) -> Result { - let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? - .context("Repository does not exist")?; - - let mut heads = BTreeMap::new(); - for head in repository.get().heads(&db) { - let commit_tree = repository.get().commit_tree(&db, &head)?; - let name = head.strip_prefix("refs/heads/"); - - if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) { - heads.insert(name.to_string(), commit); - } - } - - let tags = repository - .get() - .tag_tree(&db) - .context("Failed to fetch indexed tags")? - .fetch_all(); - - Ok(into_response(&RefsView { - repo, - refs: Refs { heads, tags }, - })) -} - -#[derive(Template)] -#[template(path = "repo/about.html")] -pub struct AboutView { - repo: Repository, - readme: Option<(ReadmeFormat, Arc)>, -} - -pub async fn handle_about( - Extension(repo): Extension, - Extension(RepositoryPath(repository_path)): Extension, - Extension(git): Extension>, -) -> Result { - let open_repo = git.clone().repo(repository_path).await?; - let readme = open_repo.readme().await?; - - Ok(into_response(&AboutView { repo, readme })) -} - -#[derive(Template)] -#[template(path = "repo/commit.html")] -pub struct CommitView { - pub repo: Repository, - pub commit: Arc, -} - -#[derive(Deserialize)] -pub struct CommitQuery { - id: Option, -} - -pub async fn handle_commit( - Extension(repo): Extension, - Extension(RepositoryPath(repository_path)): Extension, - Extension(git): Extension>, - Query(query): Query, -) -> Result { - let open_repo = git.repo(repository_path).await?; - let commit = if let Some(commit) = query.id { - open_repo.commit(&commit).await? - } else { - Arc::new(open_repo.latest_commit().await?) - }; - - Ok(into_response(&CommitView { repo, commit })) -} - -#[derive(Deserialize)] -pub struct TreeQuery { - id: Option, - #[serde(rename = "h")] - branch: Option, -} - -impl Display for TreeQuery { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut prefix = "?"; - - if let Some(id) = self.id.as_deref() { - write!(f, "{}id={}", prefix, id)?; - prefix = "&"; - } - - if let Some(branch) = self.branch.as_deref() { - write!(f, "{}h={}", prefix, branch)?; - } - - Ok(()) - } -} - -pub async fn handle_tree( - Extension(repo): Extension, - Extension(RepositoryPath(repository_path)): Extension, - Extension(ChildPath(child_path)): Extension, - Extension(git): Extension>, - Query(query): Query, -) -> Result { - #[derive(Template)] - #[template(path = "repo/tree.html")] - pub struct TreeView { - pub repo: Repository, - pub items: Vec, - pub query: TreeQuery, - } - - #[derive(Template)] - #[template(path = "repo/file.html")] - pub struct FileView { - pub repo: Repository, - pub file: FileWithContent, - } - - let open_repo = git.repo(repository_path).await?; - - Ok( - match open_repo - .path(child_path, query.id.as_deref(), query.branch.clone()) - .await? - { - PathDestination::Tree(items) => into_response(&TreeView { repo, items, query }), - PathDestination::File(file) => into_response(&FileView { repo, file }), - }, - ) -} - -#[derive(Template)] -#[template(path = "repo/diff.html")] -pub struct DiffView { - pub repo: Repository, - pub commit: Arc, -} - -pub async fn handle_diff( - Extension(repo): Extension, - Extension(RepositoryPath(repository_path)): Extension, - Extension(git): Extension>, - Query(query): Query, -) -> Result { - let open_repo = git.repo(repository_path).await?; - let commit = if let Some(commit) = query.id { - open_repo.commit(&commit).await? - } else { - Arc::new(open_repo.latest_commit().await?) - }; - - Ok(into_response(&DiffView { repo, commit })) -} - -pub async fn handle_patch( - Extension(RepositoryPath(repository_path)): Extension, - Extension(git): Extension>, - Query(query): Query, -) -> Result { - let open_repo = git.repo(repository_path).await?; - let commit = if let Some(commit) = query.id { - open_repo.commit(&commit).await? - } else { - Arc::new(open_repo.latest_commit().await?) - }; - - let headers = [( - http::header::CONTENT_TYPE, - HeaderValue::from_static("text/plain"), - )]; - - Ok((headers, commit.diff_plain.clone()).into_response()) -} diff --git a/src/database/schema/commit.rs b/src/database/schema/commit.rs index faa80fe..978e6e3 100644 --- a/src/database/schema/commit.rs +++ a/src/database/schema/commit.rs @@ -1,11 +1,12 @@ -use crate::database::schema::Yoked; +use std::{borrow::Cow, ops::Deref}; + use git2::{Oid, Signature}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sled::IVec; -use std::borrow::Cow; -use std::ops::Deref; use time::OffsetDateTime; use yoke::{Yoke, Yokeable}; + +use crate::database::schema::Yoked; #[derive(Serialize, Deserialize, Debug, Yokeable)] pub struct Commit<'a> { diff --git a/src/database/schema/prefixes.rs b/src/database/schema/prefixes.rs index cad0eec..8cfd627 100644 --- a/src/database/schema/prefixes.rs +++ a/src/database/schema/prefixes.rs @@ -1,5 +1,6 @@ -use crate::database::schema::repository::RepositoryId; use std::path::Path; + +use crate::database::schema::repository::RepositoryId; #[repr(u8)] pub enum TreePrefix { diff --git a/src/database/schema/repository.rs b/src/database/schema/repository.rs index 0769fca..a8c8c9b 100644 --- a/src/database/schema/repository.rs +++ a/src/database/schema/repository.rs @@ -1,17 +1,13 @@ -use crate::database::schema::commit::CommitTree; -use crate::database::schema::prefixes::TreePrefix; -use crate::database::schema::tag::TagTree; -use crate::database::schema::Yoked; +use std::{borrow::Cow, collections::BTreeMap, ops::Deref, path::Path}; + use anyhow::{Context, Result}; use nom::AsBytes; use serde::{Deserialize, Serialize}; use sled::IVec; -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::ops::Deref; -use std::path::Path; use time::OffsetDateTime; use yoke::{Yoke, Yokeable}; + +use crate::database::schema::{commit::CommitTree, prefixes::TreePrefix, tag::TagTree, Yoked}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Yokeable)] pub struct Repository<'a> { diff --git a/src/database/schema/tag.rs b/src/database/schema/tag.rs index 9958ee4..942ee5d 100644 --- a/src/database/schema/tag.rs +++ a/src/database/schema/tag.rs @@ -1,11 +1,11 @@ -use crate::database::schema::commit::Author; -use crate::database::schema::Yoked; +use std::{collections::HashSet, ops::Deref}; + use git2::Signature; use serde::{Deserialize, Serialize}; use sled::IVec; -use std::collections::HashSet; -use std::ops::Deref; use yoke::{Yoke, Yokeable}; + +use crate::database::schema::{commit::Author, Yoked}; #[derive(Serialize, Deserialize, Debug, Yokeable)] pub struct Tag<'a> { diff --git a/src/methods/repo/about.rs b/src/methods/repo/about.rs new file mode 100644 index 0000000..7dda1e1 100644 --- /dev/null +++ a/src/methods/repo/about.rs @@ -1,0 +1,29 @@ +use std::sync::Arc; + +use askama::Template; +use axum::{response::Response, Extension}; + +use crate::{ + git::ReadmeFormat, + into_response, + methods::repo::{Repository, RepositoryPath, Result}, + Git, +}; + +#[derive(Template)] +#[template(path = "repo/about.html")] +pub struct View { + repo: Repository, + readme: Option<(ReadmeFormat, Arc)>, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(git): Extension>, +) -> Result { + let open_repo = git.clone().repo(repository_path).await?; + let readme = open_repo.readme().await?; + + Ok(into_response(&View { repo, readme })) +} diff --git a/src/methods/repo/commit.rs b/src/methods/repo/commit.rs new file mode 100644 index 0000000..e3fea6d 100644 --- /dev/null +++ a/src/methods/repo/commit.rs @@ -1,0 +1,40 @@ +use std::sync::Arc; + +use askama::Template; +use axum::{extract::Query, response::Response, Extension}; +use serde::Deserialize; + +use crate::{ + git::Commit, + into_response, + methods::repo::{Repository, RepositoryPath, Result}, + Git, +}; + +#[derive(Template)] +#[template(path = "repo/commit.html")] +pub struct View { + pub repo: Repository, + pub commit: Arc, +} + +#[derive(Deserialize)] +pub struct UriQuery { + pub id: Option, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(git): Extension>, + Query(query): Query, +) -> Result { + let open_repo = git.repo(repository_path).await?; + let commit = if let Some(commit) = query.id { + open_repo.commit(&commit).await? + } else { + Arc::new(open_repo.latest_commit().await?) + }; + + Ok(into_response(&View { repo, commit })) +} diff --git a/src/methods/repo/diff.rs b/src/methods/repo/diff.rs new file mode 100644 index 0000000..942bc3d 100644 --- /dev/null +++ a/src/methods/repo/diff.rs @@ -1,0 +1,59 @@ +use std::sync::Arc; + +use askama::Template; +use axum::{ + extract::Query, + http::HeaderValue, + response::{IntoResponse, Response}, + Extension, +}; + +use crate::{ + git::Commit, + http, into_response, + methods::repo::{commit::UriQuery, Repository, RepositoryPath, Result}, + Git, +}; + +#[derive(Template)] +#[template(path = "repo/diff.html")] +pub struct View { + pub repo: Repository, + pub commit: Arc, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(git): Extension>, + Query(query): Query, +) -> Result { + let open_repo = git.repo(repository_path).await?; + let commit = if let Some(commit) = query.id { + open_repo.commit(&commit).await? + } else { + Arc::new(open_repo.latest_commit().await?) + }; + + Ok(into_response(&View { repo, commit })) +} + +pub async fn handle_plain( + Extension(RepositoryPath(repository_path)): Extension, + Extension(git): Extension>, + Query(query): Query, +) -> Result { + let open_repo = git.repo(repository_path).await?; + let commit = if let Some(commit) = query.id { + open_repo.commit(&commit).await? + } else { + Arc::new(open_repo.latest_commit().await?) + }; + + let headers = [( + http::header::CONTENT_TYPE, + HeaderValue::from_static("text/plain"), + )]; + + Ok((headers, commit.diff_plain.clone()).into_response()) +} diff --git a/src/methods/repo/log.rs b/src/methods/repo/log.rs new file mode 100644 index 0000000..096d6de 100644 --- /dev/null +++ a/src/methods/repo/log.rs @@ -1,0 +1,60 @@ +use anyhow::Context; +use askama::Template; +use axum::{extract::Query, response::Response, Extension}; +use serde::Deserialize; +use yoke::Yoke; + +use crate::{ + into_response, + methods::{ + filters, + repo::{Repository, Result}, + }, +}; + +#[derive(Deserialize)] +pub struct UriQuery { + #[serde(rename = "ofs")] + offset: Option, + #[serde(rename = "h")] + branch: Option, +} + +#[derive(Template)] +#[template(path = "repo/log.html")] +pub struct View<'a> { + repo: Repository, + commits: Vec<&'a crate::database::schema::commit::Commit<'a>>, + next_offset: Option, + branch: Option, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(db): Extension, + Query(query): Query, +) -> Result { + let offset = query.offset.unwrap_or(0); + + let reference = format!("refs/heads/{}", query.branch.as_deref().unwrap_or("master")); + let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? + .context("Repository does not exist")?; + let commit_tree = repository.get().commit_tree(&db, &reference)?; + let mut commits = commit_tree.fetch_latest(101, offset).await; + + let next_offset = if commits.len() == 101 { + commits.pop(); + Some(offset + 100) + } else { + None + }; + + let commits = commits.iter().map(Yoke::get).collect(); + + Ok(into_response(&View { + repo, + commits, + next_offset, + branch: query.branch, + })) +} diff --git a/src/methods/repo/mod.rs b/src/methods/repo/mod.rs new file mode 100644 index 0000000..0cbc06a 100644 --- /dev/null +++ a/src/methods/repo/mod.rs @@ -1,0 +1,176 @@ +mod about; +mod commit; +mod diff; +mod log; +mod refs; +mod smart_git; +mod summary; +mod tag; +mod tree; + +use std::{ + collections::BTreeMap, + fmt::Debug, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, +}; + +use axum::{ + body::HttpBody, + handler::Handler, + http::{Request, StatusCode}, + response::{IntoResponse, Response}, +}; +use path_clean::PathClean; +use tower::{util::BoxCloneService, Service}; + +use self::{ + about::handle as handle_about, + commit::handle as handle_commit, + diff::{handle as handle_diff, handle_plain as handle_patch}, + log::handle as handle_log, + refs::handle as handle_refs, + smart_git::{handle_git_upload_pack, handle_info_refs}, + summary::handle as handle_summary, + tag::handle as handle_tag, + tree::handle as handle_tree, +}; +use crate::{ + database::schema::{commit::YokedCommit, tag::YokedTag}, + layers::UnwrapInfallible, +}; + +// this is some wicked, wicked abuse of axum right here... +#[allow(clippy::trait_duplication_in_bounds)] // clippy seems a bit.. lost +pub async fn service(mut request: Request) -> Response +where + ReqBody: HttpBody + Send + Debug + 'static, + ::Data: Send + Sync, + ::Error: std::error::Error + Send + Sync, +{ + let mut uri_parts: Vec<&str> = request + .uri() + .path() + .trim_start_matches('/') + .trim_end_matches('/') + .split('/') + .collect(); + + let mut child_path = None; + + macro_rules! h { + ($handler:ident) => { + BoxCloneService::new($handler.into_service()) + }; + } + + let mut service = match uri_parts.pop() { + Some("about") => h!(handle_about), + // TODO: https://man.archlinux.org/man/git-http-backend.1.en + // TODO: GIT_PROTOCOL + Some("refs") if uri_parts.last() == Some(&"info") => { + uri_parts.pop(); + h!(handle_info_refs) + } + Some("git-upload-pack") => h!(handle_git_upload_pack), + Some("refs") => h!(handle_refs), + Some("log") => h!(handle_log), + Some("tree") => h!(handle_tree), + Some("commit") => h!(handle_commit), + Some("diff") => h!(handle_diff), + Some("patch") => h!(handle_patch), + Some("tag") => h!(handle_tag), + Some(v) => { + uri_parts.push(v); + + // 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()); + + h!(handle_tree) + } else { + h!(handle_summary) + } + } + None => panic!("not found"), + }; + + 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)); + + service + .call(request) + .await + .unwrap_infallible() + .into_response() +} + +#[derive(Clone)] +pub struct Repository(pub PathBuf); + +impl Deref for Repository { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone)] +pub struct RepositoryPath(pub PathBuf); + +#[derive(Clone)] +pub struct ChildPath(pub Option); + +impl Deref for RepositoryPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub type Result = std::result::Result; + +pub struct Error(anyhow::Error); + +impl From> for Error { + fn from(e: Arc) -> Self { + Self(anyhow::Error::msg(format!("{:?}", e))) + } +} + +impl From for Error { + fn from(e: anyhow::Error) -> Self { + Self(e) + } +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response() + } +} + +pub struct Refs { + heads: BTreeMap, + tags: Vec<(String, YokedTag)>, +} diff --git a/src/methods/repo/refs.rs b/src/methods/repo/refs.rs new file mode 100644 index 0000000..bf4f256 100644 --- /dev/null +++ a/src/methods/repo/refs.rs @@ -1,0 +1,50 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use askama::Template; +use axum::{response::Response, Extension}; + +use crate::{ + into_response, + methods::{ + filters, + repo::{Refs, Repository, Result}, + }, +}; + +#[derive(Template)] +#[template(path = "repo/refs.html")] +pub struct View { + repo: Repository, + refs: Refs, +} + +#[allow(clippy::unused_async)] +pub async fn handle( + Extension(repo): Extension, + Extension(db): Extension, +) -> Result { + let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? + .context("Repository does not exist")?; + + let mut heads = BTreeMap::new(); + for head in repository.get().heads(&db) { + let commit_tree = repository.get().commit_tree(&db, &head)?; + let name = head.strip_prefix("refs/heads/"); + + if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) { + heads.insert(name.to_string(), commit); + } + } + + let tags = repository + .get() + .tag_tree(&db) + .context("Failed to fetch indexed tags")? + .fetch_all(); + + Ok(into_response(&View { + repo, + refs: Refs { heads, tags }, + })) +} diff --git a/src/methods/repo/smart_git.rs b/src/methods/repo/smart_git.rs new file mode 100644 index 0000000..91512b4 100644 --- /dev/null +++ a/src/methods/repo/smart_git.rs @@ -1,0 +1,53 @@ +use std::{io::Write, process::Stdio}; + +use axum::{extract::Query, response::Response, Extension}; +use bytes::Bytes; +use serde::Deserialize; + +use crate::methods::repo::{RepositoryPath, Result}; + +#[derive(Deserialize)] +pub struct UriQuery { + service: String, +} + +#[allow(clippy::unused_async)] +pub async fn handle_info_refs( + Extension(RepositoryPath(repository_path)): Extension, + Query(query): Query, +) -> Result { + // todo: tokio command + let out = std::process::Command::new("git") + .arg("http-backend") + .env("REQUEST_METHOD", "GET") + .env("PATH_INFO", "/info/refs") + .env("GIT_PROJECT_ROOT", repository_path) + .env("QUERY_STRING", format!("service={}", query.service)) + .output() + .unwrap(); + + Ok(crate::git_cgi::cgi_to_response(&out.stdout)?) +} + +#[allow(clippy::unused_async)] +pub async fn handle_git_upload_pack( + Extension(RepositoryPath(repository_path)): Extension, + body: Bytes, +) -> Result { + // todo: tokio command + let mut child = std::process::Command::new("git") + .arg("http-backend") + // todo: read all this from request + .env("REQUEST_METHOD", "POST") + .env("CONTENT_TYPE", "application/x-git-upload-pack-request") + .env("PATH_INFO", "/git-upload-pack") + .env("GIT_PROJECT_ROOT", repository_path) + .stdout(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.as_mut().unwrap().write_all(&body).unwrap(); + let out = child.wait_with_output().unwrap(); + + Ok(crate::git_cgi::cgi_to_response(&out.stdout)?) +} diff --git a/src/methods/repo/summary.rs b/src/methods/repo/summary.rs new file mode 100644 index 0000000..1fa9e9b 100644 --- /dev/null +++ a/src/methods/repo/summary.rs @@ -1,0 +1,55 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use askama::Template; +use axum::{response::Response, Extension}; +use yoke::Yoke; + +use crate::{ + into_response, + methods::{ + filters, + repo::{Refs, Repository, Result}, + }, +}; + +#[derive(Template)] +#[template(path = "repo/summary.html")] +pub struct View<'a> { + repo: Repository, + refs: Refs, + commit_list: Vec<&'a crate::database::schema::commit::Commit<'a>>, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(db): Extension, +) -> Result { + let repository = crate::database::schema::repository::Repository::open(&db, &*repo)? + .context("Repository does not exist")?; + let commit_tree = repository.get().commit_tree(&db, "refs/heads/master")?; + let commits = commit_tree.fetch_latest(11, 0).await; + let commit_list = commits.iter().map(Yoke::get).collect(); + + let mut heads = BTreeMap::new(); + for head in repository.get().heads(&db) { + let commit_tree = repository.get().commit_tree(&db, &head)?; + let name = head.strip_prefix("refs/heads/"); + + if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) { + heads.insert(name.to_string(), commit); + } + } + + let tags = repository + .get() + .tag_tree(&db) + .context("Failed to fetch indexed tags")? + .fetch_all(); + + Ok(into_response(&View { + repo, + refs: Refs { heads, tags }, + commit_list, + })) +} diff --git a/src/methods/repo/tag.rs b/src/methods/repo/tag.rs new file mode 100644 index 0000000..acdaf0c 100644 --- /dev/null +++ a/src/methods/repo/tag.rs @@ -1,0 +1,37 @@ +use std::sync::Arc; + +use askama::Template; +use axum::{extract::Query, response::Response, Extension}; +use serde::Deserialize; + +use crate::{ + git::DetailedTag, + into_response, + methods::repo::{Repository, RepositoryPath, Result}, + Git, +}; + +#[derive(Deserialize)] +pub struct UriQuery { + #[serde(rename = "h")] + name: String, +} + +#[derive(Template)] +#[template(path = "repo/tag.html")] +pub struct View { + repo: Repository, + tag: DetailedTag, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(git): Extension>, + Query(query): Query, +) -> Result { + let open_repo = git.repo(repository_path).await?; + let tag = open_repo.tag_info(&query.name).await?; + + Ok(into_response(&View { repo, tag })) +} diff --git a/src/methods/repo/tree.rs b/src/methods/repo/tree.rs new file mode 100644 index 0000000..e20434d 100644 --- /dev/null +++ a/src/methods/repo/tree.rs @@ -1,0 +1,78 @@ +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; + +use askama::Template; +use axum::{extract::Query, response::Response, Extension}; +use serde::Deserialize; + +use crate::{ + git::{FileWithContent, PathDestination, TreeItem}, + into_response, + methods::{ + filters, + repo::{ChildPath, Repository, RepositoryPath, Result}, + }, + Git, +}; + +#[derive(Deserialize)] +pub struct UriQuery { + id: Option, + #[serde(rename = "h")] + branch: Option, +} + +impl Display for UriQuery { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut prefix = "?"; + + if let Some(id) = self.id.as_deref() { + write!(f, "{}id={}", prefix, id)?; + prefix = "&"; + } + + if let Some(branch) = self.branch.as_deref() { + write!(f, "{}h={}", prefix, branch)?; + } + + Ok(()) + } +} + +#[derive(Template)] +#[template(path = "repo/tree.html")] +#[allow(clippy::module_name_repetitions)] +pub struct TreeView { + pub repo: Repository, + pub items: Vec, + pub query: UriQuery, +} + +#[derive(Template)] +#[template(path = "repo/file.html")] +pub struct FileView { + pub repo: Repository, + pub file: FileWithContent, +} + +pub async fn handle( + Extension(repo): Extension, + Extension(RepositoryPath(repository_path)): Extension, + Extension(ChildPath(child_path)): Extension, + Extension(git): Extension>, + Query(query): Query, +) -> Result { + let open_repo = git.repo(repository_path).await?; + + Ok( + match open_repo + .path(child_path, query.id.as_deref(), query.branch.clone()) + .await? + { + PathDestination::Tree(items) => into_response(&TreeView { repo, items, query }), + PathDestination::File(file) => into_response(&FileView { repo, file }), + }, + ) +} -- rgit 0.1.3