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" name = "frontend"
path = "src/frontend.rs" path = "src/frontend.rs"
[[bin]]
name = "test_render"
path = "src/test-render.rs"
[dependencies] [dependencies]
git2 = { version = "0.20.2", default-features = false } git2 = { version = "0.20.2", default-features = false }
serde = { version = "1.0.219", default-features = false, features = ["derive"] } 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 content="width=device-width, initial-scale=1" name="viewport">
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="res/style.css"> <link rel="stylesheet" href="res/style.css">
<link rel="stylesheet" href="res/martian.css"> <link rel="stylesheet" href="res/mint.css">
<script> <script>
/* /*
@licstart The following is the entire license notice for the @licstart The following is the entire license notice for the
@ -78,14 +78,13 @@
</div> </div>
<div class=buttonListWrap> <div class=buttonListWrap>
<ul class=buttonList> <div class="tabList end"><span>code</span>
<li><a href="./">code</a></li> <a href="./commits">history</a>
<li><a href="./commits">commits</a></li> <a href="./tags">tags</a>
<li><a href="./tags">tags</a></li> <a href="./tickets">tickets {{ ticket_count }}</a></li>
<li><a href="./tickets">issues {{ ticket_count }}</a></li> <a href="./releases">releases</a>
<li><a href="/">releases</a></li> <a href="./settings">settings</a>
<li><a href="/">settings</a></li> </div>
</ul>
</div> </div>
</nav> </nav>
</div> </div>

View File

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

View File

@ -49,15 +49,77 @@ use std::{
path::PathBuf, 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 { pub struct GitCommit {
author: (Option<String>, Option<String>), // name, e-mail pub author: (Option<String>, Option<String>), // name, e-mail
entries: Vec<GitEntry>, pub entries: Vec<GitEntry>,
hash: String, pub hash: String,
message: Option<String>, pub message: Option<String>,
short_hash: Option<String>, pub short_hash: Option<String>,
time: i64, // seconds since Unix epoch 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 { 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 { pub struct GitBranch {
commits: Vec<GitCommit>, pub commits: Vec<GitCommit>,
name: String, pub name: String,
} }
struct GitBranchWrapper<'a> { struct GitBranchWrapper<'a> {
@ -124,7 +167,7 @@ impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
fn try_from(branch: GitBranchWrapper) -> Result<Self, Self::Error> { fn try_from(branch: GitBranchWrapper) -> Result<Self, Self::Error> {
let name = String::from_utf8(branch.branch.name_bytes()?.to_vec())?; let name = String::from_utf8(branch.branch.name_bytes()?.to_vec())?;
let repo = branch.repo; 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()?; let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?; revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
@ -142,13 +185,15 @@ impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
} }
pub struct GitRepo { pub struct GitRepo {
branches: Vec<GitBranch>, pub branches: Vec<GitBranch>,
name: String, pub name: String,
owner: String, pub owner: String,
} }
impl GitRepo { 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 repo = Repository::open(path.clone())?;
let branches = repo let branches = repo
@ -169,8 +214,10 @@ impl GitRepo {
*/ */
let full_path = path.clone().as_path().canonicalize()?; let full_path = path.clone().as_path().canonicalize()?;
let relative_path = match prefix {
let relative_path = full_path.strip_prefix("/var/mintee/repos")?; Some(p) => full_path.strip_prefix(p)?.canonicalize()?,
None => full_path,
};
let owner = String::from_utf8( let owner = String::from_utf8(
relative_path relative_path
@ -194,7 +241,7 @@ impl GitRepo {
Ok(GitRepo { branches, name, owner }) Ok(GitRepo { branches, name, owner })
} }
fn get_remote_repo() { // for AP repos pub fn get_remote_repo() { // for AP repos
todo!(); todo!();
} }
} }

View File

@ -16,56 +16,50 @@
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with Mintee. If not, see https://www.gnu.org/licenses/. * 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 }; use tera::{ Context, Tera };
trait ToContext { #[non_exhaustive]
fn to_context(self) -> Result<Context, git2::Error>; pub enum PageKind {
Code,
Dashboard,
Tickets,
User,
} }
// TODO: move this data into backend.rs impl PageKind {
impl ToContext for Repository { pub fn render_page(&self, ctx: Context) -> Result<String, Box<dyn Error>> {
fn to_context(self) -> Result<Context, git2::Error> { let page_dir = self.to_string();
let repo = self.commondir(); let template = String::from_utf8(Path::new(&page_dir)
let _index = self.index()?; .to_path_buf()
let head = self.head()?; .as_mut_os_string()
let branch = if head.is_branch() { .as_encoded_bytes()
head.shorthand().unwrap() .to_vec()
} 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();
let mut ctx = Context::new(); let tera = Tera::new(&page_dir)?;
Ok(tera.render(&template, &ctx)?)
// 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)
} }
} }
fn render_path(path: PathBuf) -> tera::Result<String> { impl Display for PageKind {
let tera = Tera::new("./assets/templates/repo/*")?; fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
let repo = Repository::discover(path).unwrap(); use PageKind::*;
let context = repo.to_context().unwrap();
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);
}
}