Merge branch 'feature/overview'

Closes #8
This commit is contained in:
Valentin Brandl 2019-04-29 20:42:56 +02:00
commit e7473fbd13
No known key found for this signature in database
GPG Key ID: 30D341DD34118D7D
4 changed files with 128 additions and 46 deletions

View File

@ -1,21 +1,22 @@
use crate::Error; use crate::Error;
use std::{ use std::{
borrow::Cow,
fs::{create_dir_all, File, OpenOptions}, fs::{create_dir_all, File, OpenOptions},
io::BufReader, io::BufReader,
path::Path, path::Path,
}; };
/// Enum to indicate the state of the cache /// 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 head and cached head are the same
Current(u64), Current(u64),
/// Cached head is older than current head /// Cached head is older than current head
Old(Cache), Old(Cache<'a>),
/// No cache was found /// No cache was found
No, No,
} }
impl CacheState { impl<'a> CacheState<'a> {
pub(crate) fn read_from_file(path: impl AsRef<Path>, head: &str) -> Result<CacheState, Error> { pub(crate) fn read_from_file(path: impl AsRef<Path>, head: &str) -> Result<CacheState, Error> {
if path.as_ref().exists() { if path.as_ref().exists() {
let cache: Cache = serde_json::from_reader(BufReader::new(File::open(path)?))?; 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 { match self {
CacheState::Old(mut cache) => { CacheState::Old(mut cache) => {
cache.head = head; cache.head = head;
@ -42,12 +43,12 @@ impl CacheState {
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub(crate) struct Cache { pub(crate) struct Cache<'a> {
pub head: String, pub head: Cow<'a, str>,
pub count: u64, pub count: u64,
} }
impl Cache { impl<'a> Cache<'a> {
pub(crate) fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), Error> { pub(crate) fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
create_dir_all(path.as_ref().parent().ok_or(Error::Internal)?)?; create_dir_all(path.as_ref().parent().ok_or(Error::Internal)?)?;
serde_json::to_writer( serde_json::to_writer(

View File

@ -9,14 +9,16 @@ extern crate serde_derive;
mod cache; mod cache;
mod error; mod error;
mod service;
use crate::{cache::CacheState, error::Error}; use crate::{
cache::CacheState,
error::Error,
service::{Bitbucket, GitHub, Gitlab, Service},
};
use actix_web::{ use actix_web::{
error::ErrorBadRequest, error::ErrorBadRequest,
http::{ http::header::{CacheControl, CacheDirective, Expires},
self,
header::{CacheControl, CacheDirective, Expires},
},
middleware, web, App, HttpResponse, HttpServer, middleware, web, App, HttpResponse, HttpServer,
}; };
use badge::{Badge, BadgeOptions}; use badge::{Badge, BadgeOptions};
@ -100,7 +102,7 @@ fn pull(path: impl AsRef<Path>) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<u64, Error> { fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<(u64, String), Error> {
let repo_dir = format!("{}/{}", repo_dir, repo); let repo_dir = format!("{}/{}", repo_dir, repo);
let cache_dir = format!("{}/{}.json", cache_dir, repo); let cache_dir = format!("{}/{}.json", cache_dir, repo);
let cache_dir = Path::new(&cache_dir); let cache_dir = Path::new(&cache_dir);
@ -125,7 +127,7 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<u64, Error> {
]; ];
let cache = CacheState::read_from_file(&cache_dir, &head)?; let cache = CacheState::read_from_file(&cache_dir, &head)?;
match &cache { match &cache {
CacheState::Current(res) => return Ok(*res), CacheState::Current(res) => return Ok((*res, head)),
CacheState::Old(cache) => { CacheState::Old(cache) => {
arg.push(format!("{}..HEAD", cache.head)); arg.push(format!("{}..HEAD", cache.head));
} }
@ -150,22 +152,21 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<u64, Error> {
}) })
.sum(); .sum();
let cache = cache.calculate_new_cache(count, head); let cache = cache.calculate_new_cache(count, (&head).into());
cache.write_to_file(cache_dir)?; cache.write_to_file(cache_dir)?;
Ok(cache.count) Ok((cache.count, head))
} }
fn remote_exists(url: &str) -> Result<bool, Error> { fn remote_exists(url: &str) -> Result<bool, Error> {
Ok(CLIENT.head(url).send()?.status() == reqwest::StatusCode::OK) Ok(CLIENT.head(url).send()?.status() == reqwest::StatusCode::OK)
} }
fn calculate_hoc( fn calculate_hoc<T: Service>(
service: &str,
state: web::Data<Arc<State>>, state: web::Data<Arc<State>>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
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 path = format!("{}/{}", state.repos, service_path);
let file = Path::new(&path); let file = Path::new(&path);
if !file.exists() { if !file.exists() {
@ -179,7 +180,7 @@ fn calculate_hoc(
repo.remote_set_url("origin", &url)?; repo.remote_set_url("origin", &url)?;
} }
pull(&path)?; pull(&path)?;
let hoc = hoc(&service_path, &state.repos, &state.cache)?; let (hoc, _) = hoc(&service_path, &state.repos, &state.cache)?;
let badge_opt = BadgeOptions { let badge_opt = BadgeOptions {
subject: "Hits-of-Code".to_string(), subject: "Hits-of-Code".to_string(),
color: "#007ec6".to_string(), color: "#007ec6".to_string(),
@ -203,31 +204,46 @@ fn calculate_hoc(
.streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))) .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request"))))
} }
fn github( fn overview<T: Service>(
state: web::Data<Arc<State>>, state: web::Data<Arc<State>>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
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)?;
fn gitlab( let repo = Repository::init_bare(file)?;
state: web::Data<Arc<State>>, repo.remote_add_fetch("origin", "refs/heads/*:refs/heads/*")?;
data: web::Path<(String, String)>, repo.remote_set_url("origin", &url)?;
) -> Result<HttpResponse, Error> {
calculate_hoc("gitlab.com", state, data)
} }
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 bitbucket( let (tx, rx_body) = mpsc::unbounded();
state: web::Data<Arc<State>>, let _ = tx.unbounded_send(Bytes::from(buf));
data: web::Path<(String, String)>,
) -> Result<HttpResponse, Error> {
calculate_hoc("bitbucket.org", state, data)
}
fn overview(_: web::Path<(String, String)>) -> HttpResponse { Ok(HttpResponse::Ok()
HttpResponse::TemporaryRedirect() .content_type("text/html")
.header(http::header::LOCATION, "/") .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request"))))
.finish()
} }
#[get("/")] #[get("/")]
@ -263,12 +279,12 @@ fn main() -> std::io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(index) .service(index)
.service(css) .service(css)
.service(web::resource("/github/{user}/{repo}").to(github)) .service(web::resource("/github/{user}/{repo}").to(calculate_hoc::<GitHub>))
.service(web::resource("/gitlab/{user}/{repo}").to(gitlab)) .service(web::resource("/gitlab/{user}/{repo}").to(calculate_hoc::<Gitlab>))
.service(web::resource("/bitbucket/{user}/{repo}").to(bitbucket)) .service(web::resource("/bitbucket/{user}/{repo}").to(calculate_hoc::<Bitbucket>))
.service(web::resource("/view/github/{user}/{repo}").to(overview)) .service(web::resource("/view/github/{user}/{repo}").to(overview::<GitHub>))
.service(web::resource("/view/gitlab/{user}/{repo}").to(overview)) .service(web::resource("/view/gitlab/{user}/{repo}").to(overview::<Gitlab>))
.service(web::resource("/view/github/{user}/{repo}").to(overview)) .service(web::resource("/view/bitbucket/{user}/{repo}").to(overview::<Bitbucket>))
.default_service(web::resource("").route(web::get().to(p404))) .default_service(web::resource("").route(web::get().to(p404)))
}) })
.bind(interface)? .bind(interface)?

47
src/service.rs Normal file
View File

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

View File

@ -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", {
<p>
The project at <a href="@url">@url</a> has <strong>@hoc</strong> hits of code at <a href="@commit_url">@head</a>.
</p>
<p>
To include the badge in your readme, use the following markdown:
</p>
<pre>
[![Hits-of-Code](https://@domain/@path)](https://@domain/view/@path)
</pre>
}, commit, version)