diff --git a/src/cache.rs b/src/cache.rs index 5ae46d3..8318b98 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,21 +1,22 @@ use crate::Error; use std::{ + borrow::Cow, fs::{create_dir_all, File, OpenOptions}, io::BufReader, path::Path, }; /// Enum to indicate the state of the cache -pub(crate) enum CacheState { +pub(crate) enum CacheState<'a> { /// Current head and cached head are the same Current(u64), /// Cached head is older than current head - Old(Cache), + Old(Cache<'a>), /// No cache was found No, } -impl CacheState { +impl<'a> CacheState<'a> { pub(crate) fn read_from_file(path: impl AsRef, head: &str) -> Result { if path.as_ref().exists() { let cache: Cache = serde_json::from_reader(BufReader::new(File::open(path)?))?; @@ -29,7 +30,7 @@ impl CacheState { } } - pub(crate) fn calculate_new_cache(self, count: u64, head: String) -> Cache { + pub(crate) fn calculate_new_cache(self, count: u64, head: Cow<'a, str>) -> Cache { match self { CacheState::Old(mut cache) => { cache.head = head; @@ -42,12 +43,12 @@ impl CacheState { } #[derive(Serialize, Deserialize)] -pub(crate) struct Cache { - pub head: String, +pub(crate) struct Cache<'a> { + pub head: Cow<'a, str>, pub count: u64, } -impl Cache { +impl<'a> Cache<'a> { pub(crate) fn write_to_file(&self, path: impl AsRef) -> Result<(), Error> { create_dir_all(path.as_ref().parent().ok_or(Error::Internal)?)?; serde_json::to_writer( diff --git a/src/main.rs b/src/main.rs index 67310db..cd138ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,14 +9,16 @@ extern crate serde_derive; mod cache; mod error; +mod service; -use crate::{cache::CacheState, error::Error}; +use crate::{ + cache::CacheState, + error::Error, + service::{Bitbucket, GitHub, Gitlab, Service}, +}; use actix_web::{ error::ErrorBadRequest, - http::{ - self, - header::{CacheControl, CacheDirective, Expires}, - }, + http::header::{CacheControl, CacheDirective, Expires}, middleware, web, App, HttpResponse, HttpServer, }; use badge::{Badge, BadgeOptions}; @@ -100,7 +102,7 @@ fn pull(path: impl AsRef) -> Result<(), Error> { Ok(()) } -fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result { +fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<(u64, String), Error> { let repo_dir = format!("{}/{}", repo_dir, repo); let cache_dir = format!("{}/{}.json", cache_dir, repo); let cache_dir = Path::new(&cache_dir); @@ -125,7 +127,7 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result { ]; let cache = CacheState::read_from_file(&cache_dir, &head)?; match &cache { - CacheState::Current(res) => return Ok(*res), + CacheState::Current(res) => return Ok((*res, head)), CacheState::Old(cache) => { arg.push(format!("{}..HEAD", cache.head)); } @@ -150,22 +152,21 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result { }) .sum(); - let cache = cache.calculate_new_cache(count, head); + let cache = cache.calculate_new_cache(count, (&head).into()); cache.write_to_file(cache_dir)?; - Ok(cache.count) + Ok((cache.count, head)) } fn remote_exists(url: &str) -> Result { Ok(CLIENT.head(url).send()?.status() == reqwest::StatusCode::OK) } -fn calculate_hoc( - service: &str, +fn calculate_hoc( state: web::Data>, data: web::Path<(String, String)>, ) -> Result { - let service_path = format!("{}/{}/{}", service, data.0, data.1); + let service_path = format!("{}/{}/{}", T::domain(), data.0, data.1); let path = format!("{}/{}", state.repos, service_path); let file = Path::new(&path); if !file.exists() { @@ -179,7 +180,7 @@ fn calculate_hoc( repo.remote_set_url("origin", &url)?; } pull(&path)?; - let hoc = hoc(&service_path, &state.repos, &state.cache)?; + let (hoc, _) = hoc(&service_path, &state.repos, &state.cache)?; let badge_opt = BadgeOptions { subject: "Hits-of-Code".to_string(), color: "#007ec6".to_string(), @@ -203,31 +204,46 @@ fn calculate_hoc( .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))) } -fn github( +fn overview( state: web::Data>, data: web::Path<(String, String)>, ) -> Result { - calculate_hoc("github.com", state, data) -} + let repo = format!("{}/{}", data.0, data.1); + let service_path = format!("{}/{}", T::domain(), repo); + let path = format!("{}/{}", state.repos, service_path); + let file = Path::new(&path); + let url = format!("https://{}", service_path); + if !file.exists() { + if !remote_exists(&url)? { + return Ok(p404()); + } + create_dir_all(file)?; + let repo = Repository::init_bare(file)?; + repo.remote_add_fetch("origin", "refs/heads/*:refs/heads/*")?; + repo.remote_set_url("origin", &url)?; + } + pull(&path)?; + let (hoc, head) = hoc(&service_path, &state.repos, &state.cache)?; + let mut buf = Vec::new(); + let req_path = format!("{}/{}/{}", T::url_path(), data.0, data.1); + templates::overview( + &mut buf, + COMMIT, + VERSION, + &OPT.domain, + &req_path, + &url, + hoc, + &head, + &T::commit_url(&repo, &head), + )?; -fn gitlab( - state: web::Data>, - data: web::Path<(String, String)>, -) -> Result { - calculate_hoc("gitlab.com", state, data) -} + let (tx, rx_body) = mpsc::unbounded(); + let _ = tx.unbounded_send(Bytes::from(buf)); -fn bitbucket( - state: web::Data>, - data: web::Path<(String, String)>, -) -> Result { - calculate_hoc("bitbucket.org", state, data) -} - -fn overview(_: web::Path<(String, String)>) -> HttpResponse { - HttpResponse::TemporaryRedirect() - .header(http::header::LOCATION, "/") - .finish() + Ok(HttpResponse::Ok() + .content_type("text/html") + .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))) } #[get("/")] @@ -263,12 +279,12 @@ fn main() -> std::io::Result<()> { .wrap(middleware::Logger::default()) .service(index) .service(css) - .service(web::resource("/github/{user}/{repo}").to(github)) - .service(web::resource("/gitlab/{user}/{repo}").to(gitlab)) - .service(web::resource("/bitbucket/{user}/{repo}").to(bitbucket)) - .service(web::resource("/view/github/{user}/{repo}").to(overview)) - .service(web::resource("/view/gitlab/{user}/{repo}").to(overview)) - .service(web::resource("/view/github/{user}/{repo}").to(overview)) + .service(web::resource("/github/{user}/{repo}").to(calculate_hoc::)) + .service(web::resource("/gitlab/{user}/{repo}").to(calculate_hoc::)) + .service(web::resource("/bitbucket/{user}/{repo}").to(calculate_hoc::)) + .service(web::resource("/view/github/{user}/{repo}").to(overview::)) + .service(web::resource("/view/gitlab/{user}/{repo}").to(overview::)) + .service(web::resource("/view/bitbucket/{user}/{repo}").to(overview::)) .default_service(web::resource("").route(web::get().to(p404))) }) .bind(interface)? diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..f234ff3 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,47 @@ +pub(crate) trait Service { + fn domain() -> &'static str; + fn url_path() -> &'static str; + fn commit_url(repo: &str, commit_ref: &str) -> String; +} + +pub(crate) struct GitHub; + +impl Service for GitHub { + fn domain() -> &'static str { + "github.com" + } + fn url_path() -> &'static str { + "github" + } + fn commit_url(repo: &str, commit_ref: &str) -> String { + format!("https://{}/{}/commit/{}", Self::domain(), repo, commit_ref) + } +} + +pub(crate) struct Gitlab; + +impl Service for Gitlab { + fn domain() -> &'static str { + "gitlab.com" + } + fn url_path() -> &'static str { + "gitlab" + } + fn commit_url(repo: &str, commit_ref: &str) -> String { + format!("https://{}/{}/commit/{}", Self::domain(), repo, commit_ref) + } +} + +pub(crate) struct Bitbucket; + +impl Service for Bitbucket { + fn domain() -> &'static str { + "bitbucket.org" + } + fn url_path() -> &'static str { + "bitbucket" + } + fn commit_url(repo: &str, commit_ref: &str) -> String { + format!("https://{}/{}/commits/{}", Self::domain(), repo, commit_ref) + } +} diff --git a/templates/overview.rs.html b/templates/overview.rs.html new file mode 100644 index 0000000..c01ff0a --- /dev/null +++ b/templates/overview.rs.html @@ -0,0 +1,18 @@ +@use super::base; + +@(commit: &str, version: &str, domain: &str, path: &str, url: &str, hoc: u64, head: &str, commit_url: &str) + +@:base("Hits-of-Code Badges", "Overview", { + +

+The project at @url has @hoc hits of code at @head. +

+ +

+To include the badge in your readme, use the following markdown: +

+ +
+[![Hits-of-Code](https://@domain/@path)](https://@domain/view/@path)
+
+}, commit, version)