Split repo methods into their own modules
Diff
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(-)
@@ -1,8 +1,9 @@
use anyhow::Context;
use std::{
io::Write,
path::{Path, PathBuf},
};
use anyhow::Context;
#[derive(Copy, Clone)]
pub struct Paths<'a> {
@@ -1,0 +1,7 @@
edition = "2021"
newline_style = "Unix"
use_field_init_shorthand = true
@@ -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<Oid, Arc<Commit>>,
@@ -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#"<span class="diff-{class}">"#);
}
@@ -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;
pub fn cgi_to_response(buffer: &[u8]) -> Result<Response> {
@@ -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};
@@ -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!(
@@ -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};
@@ -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")]
@@ -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<PathBuf>);
impl Deref for RepositoryPath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Error(anyhow::Error);
impl From<Arc<anyhow::Error>> for Error {
fn from(e: Arc<anyhow::Error>) -> Self {
Self(anyhow::Error::msg(format!("{:?}", e)))
}
}
impl From<anyhow::Error> 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 async fn service<ReqBody: HttpBody + Send + Debug + 'static>(
mut request: Request<ReqBody>,
) -> Response
where
<ReqBody as HttpBody>::Data: Send + Sync,
<ReqBody as HttpBody>::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()),
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);
if uri_parts.iter().any(|v| *v == "tree") {
let mut reconstructed_path = Vec::new();
while let Some(part) = uri_parts.pop() {
if part == "tree" {
break;
}
reconstructed_path.insert(0, part);
}
child_path = Some(reconstructed_path.into_iter().collect::<PathBuf>().clean());
h!(handle_tree)
} else {
h!(handle_summary)
}
}
None => panic!("not found"),
};
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));
service
.call(request)
.await
.unwrap_infallible()
.into_response()
}
pub struct Refs {
heads: BTreeMap<String, YokedCommit>,
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<Repository>,
Extension(db): Extension<sled::Db>,
) -> Result<Response> {
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<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<TagQuery>,
) -> Result<Response> {
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<usize>,
#[serde(rename = "h")]
branch: Option<String>,
}
#[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<usize>,
branch: Option<String>,
}
pub async fn handle_log(
Extension(repo): Extension<Repository>,
Extension(db): Extension<sled::Db>,
Query(query): Query<LogQuery>,
) -> Result<Response> {
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<RepositoryPath>,
Query(query): Query<SmartGitQuery>,
) -> Result<Response> {
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<RepositoryPath>,
body: Bytes,
) -> Result<Response> {
let mut child = std::process::Command::new("git")
.arg("http-backend")
.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<Repository>,
Extension(db): Extension<sled::Db>,
) -> Result<Response> {
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<str>)>,
}
pub async fn handle_about(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
) -> Result<Response> {
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<Commit>,
}
#[derive(Deserialize)]
pub struct CommitQuery {
id: Option<String>,
}
pub async fn handle_commit(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<CommitQuery>,
) -> Result<Response> {
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<String>,
#[serde(rename = "h")]
branch: Option<String>,
}
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<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(ChildPath(child_path)): Extension<ChildPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<TreeQuery>,
) -> Result<Response> {
#[derive(Template)]
#[template(path = "repo/tree.html")]
pub struct TreeView {
pub repo: Repository,
pub items: Vec<TreeItem>,
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<Commit>,
}
pub async fn handle_diff(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<CommitQuery>,
) -> Result<Response> {
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<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<CommitQuery>,
) -> Result<Response> {
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())
}
@@ -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> {
@@ -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 {
@@ -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> {
@@ -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> {
@@ -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<str>)>,
}
pub async fn handle(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
) -> Result<Response> {
let open_repo = git.clone().repo(repository_path).await?;
let readme = open_repo.readme().await?;
Ok(into_response(&View { repo, readme }))
}
@@ -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<Commit>,
}
#[derive(Deserialize)]
pub struct UriQuery {
pub id: Option<String>,
}
pub async fn handle(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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 }))
}
@@ -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<Commit>,
}
pub async fn handle(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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())
}
@@ -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<usize>,
#[serde(rename = "h")]
branch: Option<String>,
}
#[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<usize>,
branch: Option<String>,
}
pub async fn handle(
Extension(repo): Extension<Repository>,
Extension(db): Extension<sled::Db>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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,
}))
}
@@ -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,
};
#[allow(clippy::trait_duplication_in_bounds)]
pub async fn service<ReqBody>(mut request: Request<ReqBody>) -> Response
where
ReqBody: HttpBody + Send + Debug + 'static,
<ReqBody as HttpBody>::Data: Send + Sync,
<ReqBody as HttpBody>::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),
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);
if uri_parts.iter().any(|v| *v == "tree") {
let mut reconstructed_path = Vec::new();
while let Some(part) = uri_parts.pop() {
if part == "tree" {
break;
}
reconstructed_path.insert(0, part);
}
child_path = Some(reconstructed_path.into_iter().collect::<PathBuf>().clean());
h!(handle_tree)
} else {
h!(handle_summary)
}
}
None => panic!("not found"),
};
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));
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<PathBuf>);
impl Deref for RepositoryPath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Error(anyhow::Error);
impl From<Arc<anyhow::Error>> for Error {
fn from(e: Arc<anyhow::Error>) -> Self {
Self(anyhow::Error::msg(format!("{:?}", e)))
}
}
impl From<anyhow::Error> 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<String, YokedCommit>,
tags: Vec<(String, YokedTag)>,
}
@@ -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<Repository>,
Extension(db): Extension<sled::Db>,
) -> Result<Response> {
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 },
}))
}
@@ -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<RepositoryPath>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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<RepositoryPath>,
body: Bytes,
) -> Result<Response> {
let mut child = std::process::Command::new("git")
.arg("http-backend")
.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)?)
}
@@ -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<Repository>,
Extension(db): Extension<sled::Db>,
) -> Result<Response> {
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,
}))
}
@@ -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<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let tag = open_repo.tag_info(&query.name).await?;
Ok(into_response(&View { repo, tag }))
}
@@ -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<String>,
#[serde(rename = "h")]
branch: Option<String>,
}
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<TreeItem>,
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<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(ChildPath(child_path)): Extension<ChildPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
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 }),
},
)
}