git.rs: cleaning; render.rs: refactor to fit new git.rs library; test_render: initial commit; assets/*: update templates

This commit is contained in:
Emma Tebibyte 2025-08-24 18:29:59 -06:00
parent 7265642b8b
commit 806a9aebca
Signed by: emma
GPG Key ID: 427287A2F16F44FA
6 changed files with 215 additions and 86 deletions

View File

@ -7,6 +7,10 @@ edition = "2024"
name = "frontend"
path = "src/frontend.rs"
[[bin]]
name = "test_render"
path = "src/test-render.rs"
[dependencies]
git2 = { version = "0.20.2", default-features = false }
serde = { version = "1.0.219", default-features = false, features = ["derive"] }

View File

@ -28,7 +28,7 @@
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta charset="UTF-8">
<link rel="stylesheet" href="res/style.css">
<link rel="stylesheet" href="res/martian.css">
<link rel="stylesheet" href="res/mint.css">
<script>
/*
@licstart The following is the entire license notice for the
@ -78,14 +78,13 @@
</div>
<div class=buttonListWrap>
<ul class=buttonList>
<li><a href="./">code</a></li>
<li><a href="./commits">commits</a></li>
<li><a href="./tags">tags</a></li>
<li><a href="./tickets">issues {{ ticket_count }}</a></li>
<li><a href="/">releases</a></li>
<li><a href="/">settings</a></li>
</ul>
<div class="tabList end"><span>code</span>
<a href="./commits">history</a>
<a href="./tags">tags</a>
<a href="./tickets">tickets {{ ticket_count }}</a></li>
<a href="./releases">releases</a>
<a href="./settings">settings</a>
</div>
</div>
</nav>
</div>

View File

@ -15,7 +15,7 @@
<tr>
<td><a class={{ entry.class }} href="/">{{ entry.path }}</a></td>
<td><a class=commit href="./commit/{{ entry.last_commit }}">
{{ entry.last_commit_short }}
{{ entry.last_commit_message }}
</a></td>
<td><time datetime="{{ entry.last_commit_time }}">
{{ entry.last_commit_time | date(format="%Y-%m-%d %H:%M")}}

View File

@ -49,15 +49,77 @@ use std::{
path::PathBuf,
};
use git2::{ Branch, Commit, Repository, Sort, TreeEntry };
use git2::{ Branch, Commit, FileMode, Repository, Sort, TreeEntry };
use tera::Context;
/* TODO: implement From<T> for Context where T: all Git* types */
pub enum GitEntryKind {
Directory,
File,
Link,
}
impl ToString for GitEntryKind {
fn to_string(&self) -> String {
match self {
GitEntryKind::Directory => "directory".to_owned(),
GitEntryKind::File => "file".to_owned(),
GitEntryKind::Link => "link".to_owned(),
}
}
}
pub struct GitEntry {
pub entries: Vec<GitEntry>,
pub kind: GitEntryKind,
pub path: String,
}
impl GitEntry {
fn new(entries: Vec<GitEntry>, kind: GitEntryKind, path: String) -> Self {
GitEntry { entries, kind, path }
}
}
impl TryFrom<TreeEntry<'_>> for GitEntry {
type Error = std::string::FromUtf8Error;
fn try_from(entry: TreeEntry) -> Result<Self, Self::Error> {
let entries = Vec::new();
let path = String::from_utf8(entry.name_bytes().to_vec())?;
let kind = match entry.filemode() {
m if m == <i32>::from(FileMode::Blob) => GitEntryKind::File,
m if m == <i32>::from(FileMode::BlobExecutable) => GitEntryKind::File,
m if m == <i32>::from(FileMode::BlobGroupWritable) => unreachable!(),
m if m == <i32>::from(FileMode::Link) => GitEntryKind::File,
m if m == <i32>::from(FileMode::Tree) => GitEntryKind::Directory,
m if m == <i32>::from(FileMode::Commit) => GitEntryKind::File,
m if m == <i32>::from(FileMode::Unreadable) => todo!(),
_ => GitEntryKind::Directory,
};
Ok(Self::new(entries, kind, path))
}
}
pub struct GitCommit {
author: (Option<String>, Option<String>), // name, e-mail
entries: Vec<GitEntry>,
hash: String,
message: Option<String>,
short_hash: Option<String>,
time: i64, // seconds since Unix epoch
pub author: (Option<String>, Option<String>), // name, e-mail
pub entries: Vec<GitEntry>,
pub hash: String,
pub message: Option<String>,
pub short_hash: Option<String>,
pub time: i64, // seconds since Unix epoch
}
impl From<GitCommit> for Context {
fn from(value: GitCommit) -> Self {
let mut ctx = Context::new();
todo!("proc_macro for populating");
ctx
}
}
impl TryFrom<Commit<'_>> for GitCommit {
@ -83,28 +145,9 @@ impl TryFrom<Commit<'_>> for GitCommit {
}
}
pub struct GitEntry {
path: String,
}
impl GitEntry {
fn new(path: String) -> Self {
GitEntry { path }
}
}
impl TryFrom<TreeEntry<'_>> for GitEntry {
type Error = std::string::FromUtf8Error;
fn try_from(entry: TreeEntry) -> Result<Self, Self::Error> {
let path = String::from_utf8(entry.name_bytes().to_vec())?;
Ok(Self { path })
}
}
pub struct GitBranch {
commits: Vec<GitCommit>,
name: String,
pub commits: Vec<GitCommit>,
pub name: String,
}
struct GitBranchWrapper<'a> {
@ -124,7 +167,7 @@ impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
fn try_from(branch: GitBranchWrapper) -> Result<Self, Self::Error> {
let name = String::from_utf8(branch.branch.name_bytes()?.to_vec())?;
let repo = branch.repo;
let branch_oid = branch.branch.get().target().unwrap();
let branch_oid = branch.branch.get().resolve()?.target().unwrap();
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
@ -142,13 +185,15 @@ impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
}
pub struct GitRepo {
branches: Vec<GitBranch>,
name: String,
owner: String,
pub branches: Vec<GitBranch>,
pub name: String,
pub owner: String,
}
impl GitRepo {
fn open(path: PathBuf) -> Result<Self, Box<dyn Error>> {
pub fn open(
path: PathBuf, prefix: Option<String>
) -> Result<Self, Box<dyn Error>> {
let repo = Repository::open(path.clone())?;
let branches = repo
@ -169,8 +214,10 @@ impl GitRepo {
*/
let full_path = path.clone().as_path().canonicalize()?;
let relative_path = full_path.strip_prefix("/var/mintee/repos")?;
let relative_path = match prefix {
Some(p) => full_path.strip_prefix(p)?.canonicalize()?,
None => full_path,
};
let owner = String::from_utf8(
relative_path
@ -194,7 +241,7 @@ impl GitRepo {
Ok(GitRepo { branches, name, owner })
}
fn get_remote_repo() { // for AP repos
pub fn get_remote_repo() { // for AP repos
todo!();
}
}

View File

@ -16,56 +16,50 @@
*
* You should have received a copy of the GNU Affero General Public License
* along with Mintee. If not, see https://www.gnu.org/licenses/.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*/
use std::path::PathBuf;
use std::{
error::Error,
fmt::{ self, Display, Formatter },
path::Path,
};
use git2::Repository;
use tera::{ Context, Tera };
trait ToContext {
fn to_context(self) -> Result<Context, git2::Error>;
#[non_exhaustive]
pub enum PageKind {
Code,
Dashboard,
Tickets,
User,
}
// TODO: move this data into backend.rs
impl ToContext for Repository {
fn to_context(self) -> Result<Context, git2::Error> {
let repo = self.commondir();
let _index = self.index()?;
let head = self.head()?;
let branch = if head.is_branch() {
head.shorthand().unwrap()
} else { "detached" };
let head_commit = head.peel_to_commit()?;
let entries = get_entries(&self)?;
let committer = head_commit.committer().name().unwrap().to_owned();
let _author = head_commit.author().name().unwrap().to_owned();
impl PageKind {
pub fn render_page(&self, ctx: Context) -> Result<String, Box<dyn Error>> {
let page_dir = self.to_string();
let template = String::from_utf8(Path::new(&page_dir)
.to_path_buf()
.as_mut_os_string()
.as_encoded_bytes()
.to_vec()
)?;
let mut ctx = Context::new();
// stub until we have database
ctx.insert("user", "anon");
ctx.insert("site", "TiB.");
ctx.insert("notif_count", "");
ctx.insert("ticket_count", "(47)");
ctx.insert("owner", &committer);
ctx.insert("repo", repo);
ctx.insert("branch", &branch);
ctx.insert("directory", repo);
ctx.insert("entries", &entries);
ctx.insert("readme_content", "this is a readme");
Ok(ctx)
let tera = Tera::new(&page_dir)?;
Ok(tera.render(&template, &ctx)?)
}
}
fn render_path(path: PathBuf) -> tera::Result<String> {
let tera = Tera::new("./assets/templates/repo/*")?;
let repo = Repository::discover(path).unwrap();
let context = repo.to_context().unwrap();
impl Display for PageKind {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
use PageKind::*;
tera.render("code.html", &context)
let path = match self {
Code => "repo/code.html",
Dashboard => "dashboard.html",
Tickets => "repo/tickets.html",
User => "user.html",
};
write!(f, "./assets/templates/{}", path)
}
}

85
src/test-render.rs Normal file
View File

@ -0,0 +1,85 @@
/* see render.rs for licensing information */
use std::{
env::{ args, current_dir },
error::Error,
};
use serde::Serialize;
use tera::{ Context, Tera };
#[path="git.rs"]
mod git;
use git::GitRepo;
#[derive(Serialize)]
struct SampleEntry {
class: String,
last_commit_message: String,
last_commit: String,
last_commit_time: i64,
path: String,
}
impl From<GitRepo> for Context {
fn from(repo: GitRepo) -> Self {
let mut ctx = Context::new();
let directory = format!("{}/{}", repo.owner, repo.name);
let main_branch = repo.branches
.iter()
.find(|b| b.name == "main")
.unwrap();
let latest_commit = &main_branch.commits[0];
let hash = latest_commit.hash.clone();
let mut entries = Vec::new();
for e in latest_commit.entries.iter() {
let entry = SampleEntry {
class: e.kind.to_string(),
last_commit: hash.clone(),
last_commit_message: latest_commit.message.clone().unwrap(),
last_commit_time: latest_commit.time,
path: e.path.clone(),
};
entries.push(entry);
}
/* stubs til we have a real database */
ctx.insert("user", "anon");
ctx.insert("site", "TiB.");
ctx.insert("notif_count", "");
ctx.insert("ticket_count", "(47)");
ctx.insert("readme_content", "this is a readme");
ctx.insert("owner", &repo.owner);
ctx.insert("branch", &main_branch.name);
ctx.insert("repo", &repo.name);
ctx.insert("directory", &directory);
ctx.insert("entries", &entries);
ctx
}
}
fn main() -> Result<(), Box<dyn Error>> {
if let Some(templates) = args().collect::<Vec<_>>().get(1) {
let tera = Tera::new(templates.as_str())?;
let ctx = Context::from(
GitRepo::open(current_dir()?.to_path_buf(), None)?
);
println!("{}", tera.render("code.html", &ctx)?);
Ok(())
} else {
eprintln!("Usage: {} template_glob", args().collect::<Vec<_>>()[0]);
std::process::exit(64);
}
}