reorganize code & switch to gix

This commit is contained in:
2026-03-06 18:05:57 -07:00
parent 331387a56a
commit cf3f8b0aea
13 changed files with 1788 additions and 559 deletions

1735
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,10 +8,9 @@ name = "frontend"
[[bin]]
name = "test_render"
path = "src/test-render.rs"
[dependencies]
git2 = { version = "0.20.2", default-features = false }
gix = "0.78.0"
serde = { version = "1.0.219", default-features = false, features = ["derive"] }
#inkjet = "0.11.1"
#markdown = "1.0.0"

69
src/backend/git.rs Normal file
View File

@@ -0,0 +1,69 @@
use std::error::Error;
use gix::{ Repository, traverse::tree::Recorder, prelude::* };
use serde::Serialize;
use tera::Context;
#[derive(Serialize)]
struct Entry {
class: u16,
last_commit: String,
last_commit_message: String,
last_commit_time: String,
path: String,
}
pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
let mut entries = Vec::new();
let branches = r.clone().branch_names()
.iter()
.map(|x| x.to_owned().to_owned())
.collect::<Vec<_>>();
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
/* replace with configurable branch name when we have a database */
let current_branch = r.head()?.referent_name().unwrap().to_string();
//tree.traverse().breadthfirst(&mut rec)?;
for e in tree.iter() {
let entry = e.unwrap();
let last_commit = "TODO".to_string();
let last_commit_message = "TODO".to_string();
let last_commit_time = "1970-01-01T00:00:00".to_string();
entries.push(Entry {
class: entry.kind() as u16,
last_commit,
last_commit_message,
last_commit_time,
path: entry.filename().to_string(),
});
}
let dir = r.git_dir();
let name = dir.file_name().unwrap().to_str().unwrap();
let directory = dir.as_os_str().to_str().unwrap();
let mut ctx = Context::new();
/* 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", "test readme");
ctx.insert("branches", &branches);
ctx.insert("entries", &entries);
ctx.insert("owner", "anon");
ctx.insert("branch", &current_branch);
ctx.insert("repo", name);
ctx.insert("directory", directory);
ctx.insert("entries", &entries);
Ok(ctx)
}

2
src/backend/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod git;
pub mod render;

25
src/backend/test.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::{ env::current_dir, error::Error };
use gix::{ traverse::tree::Recorder, prelude::* };
fn main() -> Result<(), Box<dyn Error>> {
let r = gix::open(current_dir()?)?;
let mut rec = Recorder::default();
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
tree.traverse().breadthfirst(&mut rec)?;
for e in rec.records.iter() {
println!("{}", e.filepath)
}
/*
for e in tree.iter() {
println!("{}", e?.filename())
}
*/
Ok(())
}

62
src/bin/backend/git.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::error::Error;
use gix::{ Repository, traverse::tree::Recorder, prelude::* };
use serde::Serialize;
use tera::Context;
#[derive(Serialize)]
struct Entry {
mode: String,
hash: String,
path: String,
}
pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
let mut entries = Vec::new();
let branches = r.clone().branch_names()
.iter()
.map(|x| x.to_owned().to_owned())
.collect::<Vec<_>>();
let mut rec = Recorder::default();
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
/* replace with configurable branch name when we have a database */
let current_branch = r.head()?.referent_name().unwrap().to_string();
tree.traverse().breadthfirst(&mut rec)?;
for e in rec.records.iter() {
entries.push(Entry {
mode: e.mode.value().to_string(),
hash: e.oid.to_string(),
path: e.filepath.to_string(),
});
}
let dir = r.git_dir();
let name = dir.file_name().unwrap().to_str().unwrap();
let directory = dir.as_os_str().to_str().unwrap();
let mut ctx = Context::new();
/* 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", "test readme");
ctx.insert("branches", &branches);
ctx.insert("entries", &entries);
ctx.insert("owner", "anon");
ctx.insert("branch", &current_branch);
ctx.insert("repo", name);
ctx.insert("directory", directory);
ctx.insert("entries", &entries);
Ok(ctx)
}

65
src/bin/backend/render.rs Normal file
View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025 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 },
path::Path,
};
use tera::{ Context, Tera };
#[non_exhaustive]
pub enum PageKind {
Code,
Dashboard,
Tickets,
User,
}
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 tera = Tera::new(&page_dir)?;
Ok(tera.render(&template, &ctx)?)
}
}
impl Display for PageKind {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
use PageKind::*;
let path = match self {
Code => "repo/code.html",
Dashboard => "dashboard.html",
Tickets => "repo/tickets.html",
User => "user.html",
};
write!(f, "./assets/templates/{}", path)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
* Copyright (c) 20252026 Emma Tebibyte <emma@tebibyte.media>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This file is part of Mintee.
@@ -23,23 +23,14 @@ use std::{
error::Error,
};
use serde::Serialize;
use gix::open;
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,
}
use git::repo_to_context;
/*
impl From<GitRepo> for Context {
fn from(repo: GitRepo) -> Self {
let mut ctx = Context::new();
@@ -85,13 +76,12 @@ impl From<GitRepo> for Context {
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)?
);
let ctx = repo_to_context(open(current_dir()?)?)?;
println!("{}", tera.render("code.html", &ctx)?);

25
src/bin/backend/test.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::{ env::current_dir, error::Error };
use gix::{ traverse::tree::Recorder, prelude::* };
fn main() -> Result<(), Box<dyn Error>> {
let r = gix::open(current_dir()?)?;
let mut rec = Recorder::default();
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
tree.traverse().breadthfirst(&mut rec)?;
for e in rec.records.iter() {
println!("{}", e.filepath)
}
/*
for e in tree.iter() {
println!("{}", e?.filename())
}
*/
Ok(())
}

91
src/bin/test_render.rs Normal file
View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 20252026 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::{
env::{ args, current_dir },
error::Error,
};
use gix::open;
use tera::{ Context, Tera };
use mintee::backend::git::repo_to_context;
/*
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 = repo_to_context(open(current_dir()?)?)?;
println!("{}", tera.render("code.html", &ctx)?);
Ok(())
} else {
eprintln!("Usage: {} template_glob", args().collect::<Vec<_>>()[0]);
std::process::exit(64);
}
}

View File

@@ -1,247 +0,0 @@
/*
* Copyright (c) 2025 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/.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* MIT License
*
* Copyright (c) 2021 Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
use std::{
error::Error,
path::PathBuf,
};
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 {
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 {
type Error = Box<dyn Error>;
fn try_from(commit: Commit) -> Result<Self, Self::Error> {
let hash = commit.id().to_string();
let short_hash = commit.as_object().short_id()?.as_str().map(|f| f.to_owned());
let message = commit.message().map(|f| f.to_owned());
let commit_signature = commit.author();
let name = commit_signature.name().map(|f| f.to_owned());
let address = commit_signature.email().map(|f| f.to_owned());
let author = (name, address);
let time = commit.time().seconds();
let entries = commit
.tree()?
.iter()
.map(|c| -> Result<GitEntry, std::string::FromUtf8Error> {
Ok(GitEntry::try_from(c)?)
}).collect::<Result<Vec<_>, _>>()?;
Ok(GitCommit { author, entries, hash, message, short_hash, time })
}
}
pub struct GitBranch {
pub commits: Vec<GitCommit>,
pub name: String,
}
struct GitBranchWrapper<'a> {
branch: Branch<'a>,
repo: &'a Repository,
}
impl<'a> GitBranchWrapper<'a> {
fn new(branch: Branch<'a>, repo: &'a Repository) -> Self {
Self { branch, repo }
}
}
impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
type Error = Box<dyn Error>;
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().resolve()?.target().unwrap();
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
revwalk.push(branch_oid)?;
let commits = revwalk
.into_iter()
.map(|o| -> Result<GitCommit, Box<dyn Error>> {
Ok(GitCommit::try_from(repo.find_commit(o?)?)?)
}).collect::<Result<Vec<_>, _>>()?;
Ok(GitBranch { commits, name })
}
}
pub struct GitRepo {
pub branches: Vec<GitBranch>,
pub name: String,
pub owner: String,
}
impl GitRepo {
pub fn open(
path: PathBuf, prefix: Option<String>
) -> Result<Self, Box<dyn Error>> {
let repo = Repository::open(path.clone())?;
let branches = repo
.branches(None)?
.map(|b| -> Result<GitBranch, Box<dyn Error>> {
Ok(GitBranch::try_from(GitBranchWrapper::new(b?.0, &repo))?)
}).collect::<Result<Vec<_>, _>>()?;
/*
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
revwalk.push_head()?;
let commits = revwalk
.into_iter()
.map(|o| -> Result<GitCommit, Box<dyn Error>> {
Ok(GitCommit::try_from(repo.find_commit(o?)?)?)
}).collect::<Result<Vec<_>, _>>()?;
*/
let full_path = path.clone().as_path().canonicalize()?;
let relative_path = match prefix {
Some(p) => full_path.strip_prefix(p)?.canonicalize()?,
None => full_path,
};
let owner = String::from_utf8(
relative_path
.parent()
.unwrap()
.to_path_buf()
.as_mut_os_string()
.as_encoded_bytes()
.to_vec()
)?;
let name = String::from_utf8(
relative_path
.file_name()
.unwrap()
.to_owned()
.as_encoded_bytes()
.to_vec()
)?;
Ok(GitRepo { branches, name, owner })
}
pub fn get_remote_repo() { // for AP repos
todo!();
}
}

View File

@@ -1 +1,2 @@
pub mod backend;
pub mod util;