/* * Copyright (c) 2025–2026 Emma Tebibyte * SPDX-License-Identifier: AGPL-3.0-or-later * * This file is part of Mintee. * * Mintee is free software: you can redistribute it and/or modify it under the * terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * Mintee is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with Mintee. If not, see https://www.gnu.org/licenses/. */ use std::{ error::Error, fmt::{ self, Display, Formatter }, io::Write, path::PathBuf, }; use gix::open; use tera::Tera; use crate::backend::{ MINTEE_DIR, MINTEE_ASSETS, 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 { /// Main homepage of the site Dashboard, /// Project page Project, /// Repository page Repo(Repo), /// Settings page for a user; Should not be used if there is no /// authenticated user Settings, /// 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, } impl Display for PageKind { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { use PageKind::*; let path = match self { Dashboard => "dashboard.html", Project => "project.html", Repo(_) => "repo/repo.html", Settings => "user/settings.html", Tickets(_) => "repo/tickets.html", User => "user.html", Invalid => todo!("generate error pages based on error type"), }; write!(f, "{}", path) } } #[derive(Debug, Clone)] pub struct Page { /// 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, dest: &mut W ) -> Result<(), Box> { let assets = format!("{}/templates/**/*", MINTEE_ASSETS); let tera = Tera::new(&assets)?; let mut page_template = self.kind.to_string(); use PageKind::*; let ctx = match self.kind.clone() { Dashboard | Project | Settings | Tickets(_) | User | Invalid => { todo!() }, Repo(r) => { let mut repo_path = PathBuf::from(MINTEE_DIR.to_string()); repo_path.push(r.entity); repo_path.push(r.name); let repo = open(repo_path)?; /* 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()? }, }; Ok(dest.write_all( tera.render(&page_template, &ctx)?.as_bytes() )?) } }