diff --git a/src/backend/git.rs b/src/backend/git.rs index 958b44c..476a07b 100644 --- a/src/backend/git.rs +++ b/src/backend/git.rs @@ -31,14 +31,21 @@ use tera::Context; use time_format::format_iso8601_utc; #[derive(Default, Serialize)] +/// Directory Entry inside git struct Entry { + /// Kind of this entry; file or directory class: String, + /// Latest commit for this entry last_commit: String, + /// Message of the latest commit for this entry last_commit_message: String, + /// Time of the latest commit for this entry last_commit_time: String, + /// Path to this entry path: String, } +/// return a vector of entries from any given git Tree fn get_entries(tree: Tree) -> Result, Box> { let mut entries = Vec::new(); let order = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst); @@ -54,6 +61,10 @@ fn get_entries(tree: Tree) -> Result, Box> { _ => "file", }.to_owned(); + /* TODO: rewrite to not assume we are at the root of the repository, to + * compare entries in each commit to determine the last commit to touch + * the file, and to get the last commit to touch any file in a subtree + * and use that as the commit for that subtree */ for i in tree.repo.head_commit()?.ancestors().sorting(order).all()? { let commit = i?.id().object()?.peel_to_commit()?; let path_slice: &[u8] = entry_t.path.as_ref(); @@ -77,11 +88,14 @@ fn get_entries(tree: Tree) -> Result, Box> { Ok(entries) } +/// Converts a git type to a Tera Context pub trait ToContext { type Error; fn to_context(&self) -> Result; } + +/// Implementation of git Object to Tera Context impl ToContext for Object<'_> { type Error = Box; @@ -94,12 +108,11 @@ impl ToContext for Object<'_> { .shorten() .to_string(); - use gix::object::Kind; + use gix::object::Kind::*; match self.kind { - Kind::Blob => todo!("need sasha to make a file page"), - Kind::Tag => todo!("idk what this needs to look like"), - Kind::Commit => todo!("need sasha to make a commit page"), - Kind::Tree => { + Blob => todo!("need sasha to make a file page"), + Commit | Tag => todo!("need sasha to make a commit page"), + Tree => { let tree = self.clone().peel_to_tree()?; let entries = get_entries(tree)?; let repo = self.repo; diff --git a/src/backend/render.rs b/src/backend/render.rs index 9e20bc7..4d87af6 100644 --- a/src/backend/render.rs +++ b/src/backend/render.rs @@ -22,6 +22,7 @@ use std::{ error::Error, fmt::{ self, Display, Formatter }, io::Write, + path::PathBuf, }; use gix::open; @@ -29,18 +30,63 @@ use tera::Tera; use crate::backend::git::ToContext; +#[derive(Debug, Clone)] +/// A revision for which a repository page may be rendered +pub enum Revision { + Branch(String), + Commit(String), + Tag(String), +} + +#[derive(Debug, Clone)] +/// A type representing the git repository requested from the frontend +pub struct Repo { + /// The user or project this repository is associated with + entity: String, + /// The name of this repository (equivalent to its path under its entity) + name: String, + /// The subtree of the repository the page should be rendered for + path: Option, + /// The revision name that should be checked out when rendering pages + revision: Option, +} + +impl Repo { + /// Instanatiate a new Repo + pub fn new( + entity: String, + name: String, + revision: Option, + path: Option, + ) -> Self { + Self { + entity, + name, + path, + revision, + } + } +} + #[non_exhaustive] #[derive(Debug, Clone)] +/// Type of page pub enum PageKind { - Code, + /// Main homepage of the site Dashboard, + /// Project page Project, - Repo, - RepoSubDir, + /// Repository page + Repo(Repo), + /// Settings page for a user; Should not be used if there is no + /// authenticated user Settings, - Tickets, + /// Tickets page for a repository + Tickets(Repo), + /// User page User, /* TODO: silt exports a generic frontend non-success status trait */ + /// Page does not exist or is not well-formed Invalid, } @@ -49,13 +95,11 @@ impl Display for PageKind { use PageKind::*; let path = match self { - Code => "repo/code.html", Dashboard => "dashboard.html", Project => "project.html", - Repo => "repo/repo.html", - RepoSubDir => "repo/repo.html", + Repo(_) => "repo/repo.html", Settings => "user/settings.html", - Tickets => "repo/tickets.html", + Tickets(_) => "repo/tickets.html", User => "user.html", Invalid => todo!("generate error pages based on error type"), }; @@ -64,43 +108,79 @@ impl Display for PageKind { } } + #[derive(Debug, Clone)] pub struct Page { - pub kind: PageKind, - pub path: String, - pub branch: Option, - pub commit: Option, - pub tag: Option, - pub user: Option, + /// Type of page + kind: PageKind, + /// Currently-authenticated user for whose perspective pages will be + /// rendered + user: Option, } impl Page { + /// Instantiate a new page + pub fn new( + kind: PageKind, + user: Option, + ) -> Self { + Self { + kind, + user, + } + } + + /// Render a page to a Write-able stream pub fn render( &self, mut dest: Box ) -> Result<(), Box> { /* TODO: replace ./assets/ with the actual templates directory, * i.e. /usr/local/share/mintee/ */ let tera = Tera::new("./assets/templates/**/*")?; + let mut page_template = self.kind.to_string(); use PageKind::*; - let ctx = match self.kind { - Code => todo!(), - Dashboard | Project | Settings | Tickets | User | Invalid => { + let ctx = match self.kind.clone() { + Dashboard | Project | Settings | Tickets(_) | User | Invalid => { todo!() }, - Repo => { - open(&self.path)? - /*.rev_parse_single("@")?*/ - .head_tree()? - .id() - .object()? - .to_context()? + Repo(r) => { + let repo = open(format!("{}/{}", r.entity, r.name))?; + /* TODO: handle specifying a commit, tag, or branch */ + let head_tree = repo.head_tree()?; + let object; + + if let Some(dir) = r.path { + let path = PathBuf::from(dir); + + object = head_tree + .lookup_entry(path.iter().map(|c| { + /* unwrap() is okay because self.path is guaranteed + * to be valid UTF-8 */ + String::from_utf8( + c.to_os_string().into_encoded_bytes() + ).unwrap() + }))? + /* replace this expect() with proper error handling */ + .expect("attempted to render nonexistent page") + .to_owned() + .id() + .object()?; + + use gix::object::Kind::*; + page_template = match object.kind { + Blob => "repo/file.html", + Commit | Tag => "repo/commit.html", + Tree => "repo/repo.html", + }.to_string(); + } else { object = head_tree.id().object()?; } + + object.to_context()? }, - RepoSubDir => todo!(), }; Ok(dest.write_all( - tera.render(&self.kind.to_string(), &ctx)?.as_bytes() + tera.render(&page_template, &ctx)?.as_bytes() )?) } } diff --git a/src/bin/test_render.rs b/src/bin/test_render.rs index ed0ee6b..b38e69b 100644 --- a/src/bin/test_render.rs +++ b/src/bin/test_render.rs @@ -24,21 +24,29 @@ use std::{ io::stdout, }; -use mintee::backend::render::{ Page, PageKind }; +use mintee::backend::render::{ Page, PageKind, Repo }; fn main() -> Result<(), Box> { - let path = String::from_utf8( - current_dir()?.into_os_string().into_encoded_bytes() + let path = current_dir()?; + + let name = String::from_utf8( + path.file_name().unwrap().to_os_string().into_encoded_bytes() + )?; + let entity = String::from_utf8( + path.parent().unwrap().as_os_str().to_os_string().into_encoded_bytes() )?; - let page = Page { - kind: PageKind::Repo, - path, - branch: None, - commit: None, - tag: None, - user: None, - }; + let repo = Repo::new( + entity, + name, + None, + Some("src".to_string()), + ); + + let page = Page::new( + PageKind::Repo(repo), + None, + ); page.render(Box::new(stdout())) }