src/backend/git.rs, src/backend/render.rs: improvements to API; test_render(1): modifications to fit new API

This commit is contained in:
2026-05-06 11:00:02 +00:00
parent 5940e203b9
commit b11134c754
3 changed files with 143 additions and 42 deletions

View File

@@ -31,14 +31,21 @@ use tera::Context;
use time_format::format_iso8601_utc; use time_format::format_iso8601_utc;
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
/// Directory Entry inside git
struct Entry { struct Entry {
/// Kind of this entry; file or directory
class: String, class: String,
/// Latest commit for this entry
last_commit: String, last_commit: String,
/// Message of the latest commit for this entry
last_commit_message: String, last_commit_message: String,
/// Time of the latest commit for this entry
last_commit_time: String, last_commit_time: String,
/// Path to this entry
path: String, path: String,
} }
/// return a vector of entries from any given git Tree
fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> { fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> {
let mut entries = Vec::new(); let mut entries = Vec::new();
let order = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst); let order = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst);
@@ -54,6 +61,10 @@ fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> {
_ => "file", _ => "file",
}.to_owned(); }.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()? { for i in tree.repo.head_commit()?.ancestors().sorting(order).all()? {
let commit = i?.id().object()?.peel_to_commit()?; let commit = i?.id().object()?.peel_to_commit()?;
let path_slice: &[u8] = entry_t.path.as_ref(); let path_slice: &[u8] = entry_t.path.as_ref();
@@ -77,11 +88,14 @@ fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> {
Ok(entries) Ok(entries)
} }
/// Converts a git type to a Tera Context
pub trait ToContext { pub trait ToContext {
type Error; type Error;
fn to_context(&self) -> Result<Context, Self::Error>; fn to_context(&self) -> Result<Context, Self::Error>;
} }
/// Implementation of git Object to Tera Context
impl ToContext for Object<'_> { impl ToContext for Object<'_> {
type Error = Box<dyn Error>; type Error = Box<dyn Error>;
@@ -94,12 +108,11 @@ impl ToContext for Object<'_> {
.shorten() .shorten()
.to_string(); .to_string();
use gix::object::Kind; use gix::object::Kind::*;
match self.kind { match self.kind {
Kind::Blob => todo!("need sasha to make a file page"), Blob => todo!("need sasha to make a file page"),
Kind::Tag => todo!("idk what this needs to look like"), Commit | Tag => todo!("need sasha to make a commit page"),
Kind::Commit => todo!("need sasha to make a commit page"), Tree => {
Kind::Tree => {
let tree = self.clone().peel_to_tree()?; let tree = self.clone().peel_to_tree()?;
let entries = get_entries(tree)?; let entries = get_entries(tree)?;
let repo = self.repo; let repo = self.repo;

View File

@@ -22,6 +22,7 @@ use std::{
error::Error, error::Error,
fmt::{ self, Display, Formatter }, fmt::{ self, Display, Formatter },
io::Write, io::Write,
path::PathBuf,
}; };
use gix::open; use gix::open;
@@ -29,18 +30,63 @@ use tera::Tera;
use crate::backend::git::ToContext; 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<String>,
/// The revision name that should be checked out when rendering pages
revision: Option<Revision>,
}
impl Repo {
/// Instanatiate a new Repo
pub fn new(
entity: String,
name: String,
revision: Option<Revision>,
path: Option<String>,
) -> Self {
Self {
entity,
name,
path,
revision,
}
}
}
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Type of page
pub enum PageKind { pub enum PageKind {
Code, /// Main homepage of the site
Dashboard, Dashboard,
/// Project page
Project, Project,
Repo, /// Repository page
RepoSubDir, Repo(Repo),
/// Settings page for a user; Should not be used if there is no
/// authenticated user
Settings, Settings,
Tickets, /// Tickets page for a repository
Tickets(Repo),
/// User page
User, User,
/* TODO: silt exports a generic frontend non-success status trait */ /* TODO: silt exports a generic frontend non-success status trait */
/// Page does not exist or is not well-formed
Invalid, Invalid,
} }
@@ -49,13 +95,11 @@ impl Display for PageKind {
use PageKind::*; use PageKind::*;
let path = match self { let path = match self {
Code => "repo/code.html",
Dashboard => "dashboard.html", Dashboard => "dashboard.html",
Project => "project.html", Project => "project.html",
Repo => "repo/repo.html", Repo(_) => "repo/repo.html",
RepoSubDir => "repo/repo.html",
Settings => "user/settings.html", Settings => "user/settings.html",
Tickets => "repo/tickets.html", Tickets(_) => "repo/tickets.html",
User => "user.html", User => "user.html",
Invalid => todo!("generate error pages based on error type"), Invalid => todo!("generate error pages based on error type"),
}; };
@@ -64,43 +108,79 @@ impl Display for PageKind {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Page { pub struct Page {
pub kind: PageKind, /// Type of page
pub path: String, kind: PageKind,
pub branch: Option<String>, /// Currently-authenticated user for whose perspective pages will be
pub commit: Option<String>, /// rendered
pub tag: Option<String>, user: Option<String>,
pub user: Option<String>,
} }
impl Page { impl Page {
/// Instantiate a new page
pub fn new(
kind: PageKind,
user: Option<String>,
) -> Self {
Self {
kind,
user,
}
}
/// Render a page to a Write-able stream
pub fn render( pub fn render(
&self, mut dest: Box<dyn Write> &self, mut dest: Box<dyn Write>
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
/* TODO: replace ./assets/ with the actual templates directory, /* TODO: replace ./assets/ with the actual templates directory,
* i.e. /usr/local/share/mintee/ */ * i.e. /usr/local/share/mintee/ */
let tera = Tera::new("./assets/templates/**/*")?; let tera = Tera::new("./assets/templates/**/*")?;
let mut page_template = self.kind.to_string();
use PageKind::*; use PageKind::*;
let ctx = match self.kind { let ctx = match self.kind.clone() {
Code => todo!(), Dashboard | Project | Settings | Tickets(_) | User | Invalid => {
Dashboard | Project | Settings | Tickets | User | Invalid => {
todo!() todo!()
}, },
Repo => { Repo(r) => {
open(&self.path)? let repo = open(format!("{}/{}", r.entity, r.name))?;
/*.rev_parse_single("@")?*/ /* TODO: handle specifying a commit, tag, or branch */
.head_tree()? let head_tree = repo.head_tree()?;
.id() let object;
.object()?
.to_context()? 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( Ok(dest.write_all(
tera.render(&self.kind.to_string(), &ctx)?.as_bytes() tera.render(&page_template, &ctx)?.as_bytes()
)?) )?)
} }
} }

View File

@@ -24,21 +24,29 @@ use std::{
io::stdout, io::stdout,
}; };
use mintee::backend::render::{ Page, PageKind }; use mintee::backend::render::{ Page, PageKind, Repo };
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let path = String::from_utf8( let path = current_dir()?;
current_dir()?.into_os_string().into_encoded_bytes()
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 { let repo = Repo::new(
kind: PageKind::Repo, entity,
path, name,
branch: None, None,
commit: None, Some("src".to_string()),
tag: None, );
user: None,
}; let page = Page::new(
PageKind::Repo(repo),
None,
);
page.render(Box::new(stdout())) page.render(Box::new(stdout()))
} }