diff --git a/src/cache.rs b/src/cache.rs index 5795522..22abc6c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,6 +1,7 @@ use crate::error::{Error, Result}; use std::{ borrow::Cow, + collections::HashMap, fs::{create_dir_all, File, OpenOptions}, io::BufReader, path::Path, @@ -9,49 +10,106 @@ use std::{ /// Enum to indicate the state of the cache pub(crate) enum CacheState<'a> { /// Current head and cached head are the same - Current { count: u64, commits: u64 }, + Current { + count: u64, + commits: u64, + cache: Cache<'a>, + }, /// Cached head is older than current head - Old(Cache<'a>), + Old { + head: String, + cache: Cache<'a>, + }, + NoneForBranch(Cache<'a>), /// No cache was found No, } impl<'a> CacheState<'a> { - pub(crate) fn read_from_file(path: impl AsRef, head: &str) -> Result { + pub(crate) fn read_from_file( + path: impl AsRef, + branch: &str, + head: &str, + ) -> Result> { if path.as_ref().exists() { let cache: Cache = serde_json::from_reader(BufReader::new(File::open(path)?))?; - if cache.head == head { - Ok(CacheState::Current { - count: cache.count, - commits: cache.commits, + Ok(cache + .entries + .get(branch) + .map(|c| { + if c.head == head { + CacheState::Current { + count: c.count, + commits: c.commits, + // TODO: get rid of clone + cache: cache.clone(), + } + } else { + CacheState::Old { + head: c.head.to_string(), + // TODO: get rid of clone + cache: cache.clone(), + } + } }) - } else { - Ok(CacheState::Old(cache)) - } + // TODO: get rid of clone + .unwrap_or_else(|| CacheState::NoneForBranch(cache.clone()))) } else { Ok(CacheState::No) } } - pub(crate) fn calculate_new_cache(self, count: u64, commits: u64, head: Cow<'a, str>) -> Cache { + pub(crate) fn calculate_new_cache( + self, + count: u64, + commits: u64, + head: Cow<'a, str>, + branch: &'a str, + ) -> Cache<'a> { match self { - CacheState::Old(mut cache) => { - cache.head = head; - cache.count += count; - cache.commits += commits; + CacheState::Old { mut cache, .. } => { + if let Some(mut cache) = cache.entries.get_mut(branch) { + cache.head = head; + cache.count += count; + cache.commits += commits; + } cache } - CacheState::No | CacheState::Current { .. } => Cache { - head, - count, - commits, - }, + CacheState::Current { cache, .. } => cache, + CacheState::NoneForBranch(mut cache) => { + cache.entries.insert( + branch.into(), + CacheEntry { + head, + count, + commits, + }, + ); + cache + } + CacheState::No => { + let mut entries = HashMap::with_capacity(1); + entries.insert( + branch.into(), + CacheEntry { + commits, + head, + count, + }, + ); + Cache { entries } + } } } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub(crate) struct Cache<'a> { + pub entries: HashMap, CacheEntry<'a>>, +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct CacheEntry<'a> { /// HEAD commit ref pub head: Cow<'a, str>, /// HoC value diff --git a/src/config.rs b/src/config.rs index 45f806b..1b82cad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,6 @@ pub(crate) struct Opt { pub(crate) async fn init() -> Result<()> { std::env::set_var("RUST_LOG", "actix_web=info,hoc=info"); - // pretty_env_logger::init(); openssl_probe::init_ssl_cert_env_vars(); let stdout = ConsoleAppender::builder().build(); let file = FileAppender::builder() diff --git a/src/error.rs b/src/error.rs index 9fae6fc..76d36a1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,7 +18,7 @@ pub(crate) enum Error { LogBuilder(log4rs::config::Errors), Parse(std::num::ParseIntError), Serial(serde_json::Error), - GitNoMaster, + BranchNotFound, } impl fmt::Display for Error { @@ -33,7 +33,7 @@ impl fmt::Display for Error { Error::LogBuilder(e) => write!(fmt, "LogBuilder({})", e), Error::Parse(e) => write!(fmt, "Parse({})", e), Error::Serial(e) => write!(fmt, "Serial({})", e), - Error::GitNoMaster => write!(fmt, "Repo doesn't have master branch"), + Error::BranchNotFound => write!(fmt, "Repo doesn't have master branch"), } } } @@ -42,7 +42,7 @@ impl ResponseError for Error { fn error_response(&self) -> HttpResponse { let mut buf = Vec::new(); match self { - Error::GitNoMaster => { + Error::BranchNotFound => { templates::p404_no_master( &mut buf, VERSION_INFO, diff --git a/src/main.rs b/src/main.rs index 6351b0a..88c1d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ use actix_web::{ middleware, web, App, HttpResponse, HttpServer, Responder, }; use badge::{Badge, BadgeOptions}; -use git2::Repository; +use git2::{BranchType, Repository}; use number_prefix::NumberPrefix; use std::{ borrow::Cow, @@ -63,10 +63,16 @@ pub(crate) struct State { #[derive(Serialize)] struct JsonResponse<'a> { head: &'a str, + branch: &'a str, count: u64, commits: u64, } +#[derive(Deserialize, Debug)] +struct BranchQuery { + branch: Option, +} + fn pull(path: impl AsRef) -> Result<()> { let repo = Repository::open_bare(path)?; let mut origin = repo.find_remote("origin")?; @@ -74,17 +80,17 @@ fn pull(path: impl AsRef) -> Result<()> { Ok(()) } -fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<(u64, String, u64)> { +fn hoc(repo: &str, repo_dir: &str, cache_dir: &str, branch: &str) -> Result<(u64, String, u64)> { let repo_dir = format!("{}/{}", repo_dir, repo); let cache_dir = format!("{}/{}.json", cache_dir, repo); let cache_dir = Path::new(&cache_dir); let repo = Repository::open_bare(&repo_dir)?; // TODO: do better... - let head = match repo.head() { - Ok(v) => v, - Err(_) => return Err(Error::GitNoMaster), - }; - let head = format!("{}", head.target().ok_or(Error::Internal)?); + let head = repo + .find_branch(branch, BranchType::Local) + .map_err(|_| Error::BranchNotFound)? + .into_reference(); + let head = format!("{}", head.target().ok_or(Error::BranchNotFound)?); let mut arg_commit_count = vec!["rev-list".to_string(), "--count".to_string()]; let mut arg = vec![ "log".to_string(), @@ -98,26 +104,27 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<(u64, String, u64) "-M".to_string(), "--diff-filter=ACDM".to_string(), ]; - let cache = CacheState::read_from_file(&cache_dir, &head)?; + let cache = CacheState::read_from_file(&cache_dir, branch, &head)?; match &cache { - CacheState::Current { count, commits } => { + CacheState::Current { count, commits, .. } => { info!("Using cache for {}", repo_dir); return Ok((*count, head, *commits)); } - CacheState::Old(cache) => { + CacheState::Old { head, .. } => { info!("Updating cache for {}", repo_dir); - arg.push(format!("{}..HEAD", cache.head)); - arg_commit_count.push(format!("{}..HEAD", cache.head)); + arg.push(format!("{}..{}", head, branch)); + arg_commit_count.push(format!("{}..{}", head, branch)); } - CacheState::No => { + CacheState::No | CacheState::NoneForBranch(..) => { info!("Creating cache for {}", repo_dir); - arg_commit_count.push("HEAD".to_string()); + arg.push(branch.to_string()); + arg_commit_count.push(branch.to_string()); } }; arg.push("--".to_string()); arg.push(".".to_string()); let output = Command::new("git") - .args(&arg) + .args(&dbg!(arg)) .current_dir(&repo_dir) .output()? .stdout; @@ -140,10 +147,10 @@ fn hoc(repo: &str, repo_dir: &str, cache_dir: &str) -> Result<(u64, String, u64) }) .sum(); - let cache = cache.calculate_new_cache(count, commits, (&head).into()); + let cache = cache.calculate_new_cache(count, commits, (&head).into(), branch); cache.write_to_file(cache_dir)?; - Ok((cache.count, head, commits)) + Ok((count, head, commits)) } async fn remote_exists(url: &str) -> Result { @@ -206,6 +213,7 @@ where async fn handle_hoc_request( state: web::Data>, data: web::Path<(String, String)>, + branch: &str, mapper: F, ) -> Result where @@ -217,7 +225,6 @@ where let service_url = format!("{}/{}", T::domain(), repo); let path = format!("{}/{}", state.repos, service_url); let url = format!("https://{}", service_url); - error!("{}", url); let remote_exists = remote_exists(&url).await?; let file = Path::new(&path); if !file.exists() { @@ -233,7 +240,7 @@ where REPO_COUNT.fetch_add(1, Ordering::Relaxed); } pull(&path)?; - let (hoc, head, commits) = hoc(&service_url, &state.repos, &state.cache)?; + let (hoc, head, commits) = hoc(&service_url, &state.repos, &state.cache, branch)?; let hoc_pretty = match NumberPrefix::decimal(hoc as f64) { NumberPrefix::Standalone(hoc) => hoc.to_string(), NumberPrefix::Prefixed(prefix, hoc) => format!("{:.1}{}", hoc, prefix), @@ -253,23 +260,27 @@ where pub(crate) async fn json_hoc( state: web::Data>, data: web::Path<(String, String)>, + branch: web::Query, ) -> Result { + let branch = branch.branch.as_deref().unwrap_or("master"); let mapper = |r| match r { HocResult::NotFound => p404(), HocResult::Hoc { hoc, head, commits, .. } => Ok(HttpResponse::Ok().json(JsonResponse { + branch, head: &head, count: hoc, commits, })), }; - handle_hoc_request::(state, data, mapper).await + handle_hoc_request::(state, data, branch, mapper).await } pub(crate) async fn calculate_hoc( state: web::Data>, data: web::Path<(String, String)>, + branch: web::Query, ) -> Result { let mapper = move |r| match r { HocResult::NotFound => p404(), @@ -296,13 +307,16 @@ pub(crate) async fn calculate_hoc( .body(body)) } }; - handle_hoc_request::(state, data, mapper).await + let branch = branch.branch.as_deref().unwrap_or("master"); + handle_hoc_request::(state, data, branch, mapper).await } async fn overview( state: web::Data>, data: web::Path<(String, String)>, + branch: web::Query, ) -> Result { + let branch = branch.branch.as_deref().unwrap_or("master"); let mapper = |r| match r { HocResult::NotFound => p404(), HocResult::Hoc { @@ -324,6 +338,7 @@ async fn overview( hoc_pretty: &hoc_pretty, path: &service_path, url: &url, + branch, }; templates::overview( &mut buf, @@ -335,7 +350,7 @@ async fn overview( Ok(HttpResponse::Ok().content_type("text/html").body(buf)) } }; - handle_hoc_request::(state, data, mapper).await + handle_hoc_request::(state, data, branch, mapper).await } #[get("/")] diff --git a/src/template.rs b/src/template.rs index 163f074..68fd97d 100644 --- a/src/template.rs +++ b/src/template.rs @@ -7,4 +7,5 @@ pub struct RepoInfo<'a> { pub hoc_pretty: &'a str, pub path: &'a str, pub url: &'a str, + pub branch: &'a str, } diff --git a/templates/index.rs.html b/templates/index.rs.html index 075b0c4..8bbcbab 100644 --- a/templates/index.rs.html +++ b/templates/index.rs.html @@ -45,6 +45,12 @@ would render this badge: alt="example badge" /> +

+By default, this service assumes the existence of a branch named master. If no branch with that name exists +in your repository or you want a badge for another branch of your repository, just append +?branch=<branch-name> to the URL. +

+

You can also request the HoC as JSON by appending /json to the request path. This will return a JSON object with three fields: count (the HoC value), commits (the number of commits) and diff --git a/templates/overview.rs.html b/templates/overview.rs.html index 3c6d1e7..8f5338f 100644 --- a/templates/overview.rs.html +++ b/templates/overview.rs.html @@ -9,7 +9,8 @@

The project @repo_info.url has @repo_info.hoc_pretty (exactly @repo_info.hoc) hits of code at -@repo_info.head. The repository contains +@repo_info.head on the +@repo_info.branch branch. The repository contains @repo_info.commits commits.

@@ -18,7 +19,7 @@ To include the badge in your readme, use the following markdown:

-[![Hits-of-Code](https://@repo_info.domain/@repo_info.path)](https://@repo_info.domain/view/@repo_info.path)
+[![Hits-of-Code](https://@repo_info.domain/@repo_info.path?branch=@repo_info.branch)](https://@repo_info.domain/view/@repo_info.path?branch=@repo_info.branch)
 
diff --git a/templates/p404_no_master.rs.html b/templates/p404_no_master.rs.html index dd49d8a..309f5c5 100644 --- a/templates/p404_no_master.rs.html +++ b/templates/p404_no_master.rs.html @@ -3,11 +3,11 @@ @(version_info: VersionInfo, repo_count: usize) -@:base("Master Branch not Found - Hits-of-Code Badges", "404 - Master Branch not Found", { +@:base("Branch not Found - Hits-of-Code Badges", "404 - Branch not Found", {

-Sorry. I couldn't find the master branch of your repositroy. -Currently this service depends on the existence of a master branch. Please go -back to the homepage. +Sorry. I couldn't find the requested branch of your repositroy. Currently this service assumes the +extistence of a branch named master. If you'd like to request a badge for another branch, you can do so by +attaching ?branch=<branch-name> to the request.