gitscope
← rgit / Diff · f74037c → f74037c
Changes f74037c..f74037c
Patch commit
diff --git a/build.rs b/build.rs
index cbcc334..b16d138 100644
--- a/build.rs
+++ b/build.rs
@@ -1,9 +1,10 @@
-use anyhow::Context;
use std::{
io::Write,
path::{Path, PathBuf},
};
+use anyhow::Context;
+
#[derive(Copy, Clone)]
pub struct Paths<'a> {
statics_in_dir: &'a Path,
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..d440511
--- /dev/null
+++ b/rustfmt.toml
@@ -0,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/database/indexer.rs b/src/database/indexer.rs
index 0259c49..65187ed 100644
--- a/src/database/indexer.rs
+++ b/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/database/schema/commit.rs b/src/database/schema/commit.rs
index faa80fe..978e6e3 100644
--- a/src/database/schema/commit.rs
+++ b/src/database/schema/commit.rs
@@ -1,12 +1,13 @@
-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> {
#[serde(borrow)]
diff --git a/src/database/schema/prefixes.rs b/src/database/schema/prefixes.rs
index cad0eec..8cfd627 100644
--- a/src/database/schema/prefixes.rs
+++ b/src/database/schema/prefixes.rs
@@ -1,6 +1,7 @@
-use crate::database::schema::repository::RepositoryId;
use std::path::Path;
+use crate::database::schema::repository::RepositoryId;
+
#[repr(u8)]
pub enum TreePrefix {
Repository = 0,
diff --git a/src/database/schema/repository.rs b/src/database/schema/repository.rs
index 0769fca..a8c8c9b 100644
--- a/src/database/schema/repository.rs
+++ b/src/database/schema/repository.rs
@@ -1,18 +1,14 @@
-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> {
/// The ID of the repository, as stored in `sled`
diff --git a/src/database/schema/tag.rs b/src/database/schema/tag.rs
index 9958ee4..942ee5d 100644
--- a/src/database/schema/tag.rs
+++ b/src/database/schema/tag.rs
@@ -1,12 +1,12 @@
-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> {
#[serde(borrow)]
diff --git a/src/git.rs b/src/git.rs
index c95cde7..f1414c4 100644
--- a/src/git.rs
+++ b/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,12 +15,16 @@ use git2::{
};
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>>,
readme_cache: Cache<PathBuf, Option<(ReadmeFormat, Arc<str>)>>,
@@ -109,8 +117,7 @@ impl OpenRepository {
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 @@ impl OpenRepository {
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 @@ impl TryFrom<git2::Commit<'_>> for Commit {
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 @@ fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> Result<String>
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 @@ fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> Result<String>
.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}">"#);
}
diff --git a/src/git_cgi.rs b/src/git_cgi.rs
index b425da9..d46101f 100644
--- a/src/git_cgi.rs
+++ b/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<Response> {
diff --git a/src/main.rs b/src/main.rs
index 8d04be6..baf0d2d 100644
--- a/src/main.rs
+++ b/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/methods/index.rs b/src/methods/index.rs
index 46e2a64..747ee22 100644
--- a/src/methods/index.rs
+++ b/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
--- a/src/methods/repo.rs
+++ /dev/null
@@ -1,509 +0,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()
- }
-}
-
-// this is some wicked, wicked abuse of axum right here...
-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()),
- // 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::<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> {
- // 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<RepositoryPath>,
- body: Bytes,
-) -> Result<Response> {
- // 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<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())
-}
diff --git a/src/methods/repo/about.rs b/src/methods/repo/about.rs
new file mode 100644
index 0000000..7dda1e1
--- /dev/null
+++ b/src/methods/repo/about.rs
@@ -0,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 }))
+}
diff --git a/src/methods/repo/commit.rs b/src/methods/repo/commit.rs
new file mode 100644
index 0000000..e3fea6d
--- /dev/null
+++ b/src/methods/repo/commit.rs
@@ -0,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 }))
+}
diff --git a/src/methods/repo/diff.rs b/src/methods/repo/diff.rs
new file mode 100644
index 0000000..942bc3d
--- /dev/null
+++ b/src/methods/repo/diff.rs
@@ -0,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())
+}
diff --git a/src/methods/repo/log.rs b/src/methods/repo/log.rs
new file mode 100644
index 0000000..096d6de
--- /dev/null
+++ b/src/methods/repo/log.rs
@@ -0,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,
+ }))
+}
diff --git a/src/methods/repo/mod.rs b/src/methods/repo/mod.rs
new file mode 100644
index 0000000..0cbc06a
--- /dev/null
+++ b/src/methods/repo/mod.rs
@@ -0,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<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),
+ // 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::<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)>,
+}
diff --git a/src/methods/repo/refs.rs b/src/methods/repo/refs.rs
new file mode 100644
index 0000000..bf4f256
--- /dev/null
+++ b/src/methods/repo/refs.rs
@@ -0,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 },
+ }))
+}
diff --git a/src/methods/repo/smart_git.rs b/src/methods/repo/smart_git.rs
new file mode 100644
index 0000000..91512b4
--- /dev/null
+++ b/src/methods/repo/smart_git.rs
@@ -0,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> {
+ // 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<RepositoryPath>,
+ body: Bytes,
+) -> Result<Response> {
+ // 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
--- /dev/null
+++ b/src/methods/repo/summary.rs
@@ -0,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,
+ }))
+}
diff --git a/src/methods/repo/tag.rs b/src/methods/repo/tag.rs
new file mode 100644
index 0000000..acdaf0c
--- /dev/null
+++ b/src/methods/repo/tag.rs
@@ -0,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 }))
+}
diff --git a/src/methods/repo/tree.rs b/src/methods/repo/tree.rs
new file mode 100644
index 0000000..e20434d
--- /dev/null
+++ b/src/methods/repo/tree.rs
@@ -0,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 }),
+ },
+ )
+}
diff --git a/src/syntax_highlight.rs b/src/syntax_highlight.rs
index c3e1c1e..f0fab88 100644
--- a/src/syntax_highlight.rs
+++ b/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 @@ impl SyntaxHighlighterAdapter for ComrakSyntectAdapter<'_> {
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!(