194 lines
4.5 KiB
Rust
194 lines
4.5 KiB
Rust
/*
|
||
* Copyright (c) 2025–2026 Emma Tebibyte <emma@tebibyte.media>
|
||
* 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<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]
|
||
#[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<String>,
|
||
}
|
||
|
||
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<W: Write>(
|
||
&self, dest: &mut W
|
||
) -> Result<(), Box<dyn Error>> {
|
||
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()
|
||
)?)
|
||
}
|
||
}
|