diff --git a/Cargo.toml b/Cargo.toml index 4fa9049..7d3bcba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,13 @@ edition = "2024" name = "frontend" path = "src/frontend.rs" +[[bin]] +name = "backend" +path = "src/backend.rs" + [dependencies] git2 = "0.20.2" serde = { version = "1.0.219", default-features = false, features = ["derive"] } -#serde_derive = { version = "1.0.219", default-features = false } #inkjet = "0.11.1" #markdown = "1.0.0" tera = { version = "1.20.0", default-features = false, features = ["builtins"] } diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..1c85d0d --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2025 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/. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * MIT License + * + * Copyright (c) 2021 Sergey "Shnatsel" Davidoff + * + * 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, Path }, +}; + +use git2::{ Commit, ErrorClass, ErrorCode, Repository, Sort }; +use serde::Serialize; + +#[derive(Serialize)] +pub struct GitCommit { + hash: String, + message: String, + short_hash: String, +} + +impl TryFrom> for GitCommit { + type Error = git2::Error; + + fn try_from(commit: Commit) -> Result { + let hash = commit.id().to_string(); + let short_hash = commit.as_object().short_id()?.as_str().ok_or( + Self::Error::new(ErrorCode::Invalid, ErrorClass::Object, "Short ID is not valid UTF-8") + )?.to_owned(); + let message = commit.message().ok_or( + Self::Error::new(ErrorCode::NotFound, ErrorClass::None, "No commit message") + )?.to_owned(); + Ok(GitCommit { hash, message, short_hash }) + } +} + +#[derive(Serialize)] +struct GitEntry { + committer: String, + last_commit: String, + last_commit_short: String, + last_commit_time: i64, + path: String, +} + +impl GitEntry { + fn new(commit: &Commit, path: String) -> Result { + let commit_id = commit.id(); + + Ok(GitEntry { + committer: commit + .committer() + .name() + .unwrap_or("") + .to_owned(), + last_commit: commit_id.to_string(), + last_commit_short: commit.as_object().short_id()?.as_str().unwrap_or("").to_owned(), + last_commit_time: commit.time().seconds(), + path, + }) + } +} + + +#[derive(Serialize)] +pub struct GitRepo { + entries: Vec, + last_commit: GitCommit, + name: String, + owner: String, +} + +impl GitRepo { + fn open(path: PathBuf) -> Result> { + let repo = Repository::open(path.clone())?; + let entries = Self::get_entries(&repo)?; + + let full_path = path.clone().as_path().canonicalize()?; + + let relative_path = full_path.strip_prefix("/var/mintee/repos")?; + + 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() + )?; + + let last_commit = GitCommit::try_from(repo.head()?.peel_to_commit()?)?; + + Ok(GitRepo { entries, last_commit, name, owner }) + } + + fn get_remote_repo() { // for AP repos + todo!(); + } + + fn get_entries(repo: &Repository) -> Result, Box> { + let mut entries = Vec::new(); + let mut revwalk = repo.revwalk()?; + + revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?; + revwalk.push_head()?; + + for commit_id in revwalk { + let commit_id = commit_id?; + let commit = repo.find_commit(commit_id)?; + if commit.parent_count() <= 1 { + let tree = commit.tree()?; + let prev_tree = match commit.parent_count() { + 1 => Some(commit.parent(0)?.tree()?), + 0 => None, + _ => unreachable!(), + }; + + let diff = repo.diff_tree_to_tree(prev_tree.as_ref(), Some(&tree), None)?; + for delta in diff.deltas() { + if let Some(file_path) = delta.new_file().path() { + let p = String::from_utf8(file_path + .to_path_buf() + .as_mut_os_string() + .as_encoded_bytes() + .to_vec() + )?; + entries.push(GitEntry::new(&commit, p)?); + } + } + } + } + Ok(entries) + } +} + +fn main() -> Result<(), Box> { + GitRepo::open(Path::new("/var/mintee/repos/silt/pingus").to_path_buf())?; + + Ok(()) +} diff --git a/src/render.rs b/src/render.rs index 34c6c12..1a046b9 100644 --- a/src/render.rs +++ b/src/render.rs @@ -44,30 +44,15 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -use std::{ - env::current_dir, - fs::metadata, - path::PathBuf, -}; +use std::path::PathBuf; -use git2::{ Commit, Repository, Sort }; -use serde::Serialize; +use git2::Repository; use tera::{ Context, Tera }; trait ToContext { fn to_context(self) -> Result; } -#[derive(Serialize)] -struct Entry { - class: String, - committer: String, - last_commit: String, - last_commit_short: String, - last_commit_time: i64, - path: String, -} - impl ToContext for Repository { fn to_context(self) -> Result { let repo = self.commondir(); @@ -99,62 +84,6 @@ impl ToContext for Repository { } } -impl Entry { - fn new(commit: &Commit, path: String) -> Result { - let commit_id = commit.id(); - let ft = metadata(&path).unwrap().file_type(); - let class: String; - - if ft.is_dir() { - class = "directory".to_owned(); - } else { - class = "file".to_owned(); - } - - Ok(Entry { - class, - committer: commit - .committer() - .name() - .unwrap_or("") - .to_owned(), - last_commit: commit_id.to_string(), - last_commit_short: commit.as_object().short_id()?.as_str().unwrap_or("").to_owned(), - last_commit_time: commit.time().seconds(), - path, - }) - } -} - -fn get_entries(repo: &Repository) -> Result, git2::Error> { - let mut entries = Vec::new(); - let mut revwalk = repo.revwalk()?; - - revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?; - revwalk.push_head()?; - - for commit_id in revwalk { - let commit_id = commit_id?; - let commit = repo.find_commit(commit_id)?; - if commit.parent_count() <= 1 { - let tree = commit.tree()?; - let prev_tree = match commit.parent_count() { - 1 => Some(commit.parent(0)?.tree()?), - 0 => None, - _ => unreachable!(), - }; - - let diff = repo.diff_tree_to_tree(prev_tree.as_ref(), Some(&tree), None)?; - for delta in diff.deltas() { - if let Some(file_path) = delta.new_file().path() { - entries.push(Entry::new(&commit, file_path.to_str().unwrap_or("Invalid UTF-8").to_owned())?); - } - } - } - } - Ok(entries) -} - fn render_path(path: PathBuf) -> tera::Result { let tera = Tera::new("./assets/templates/repo/*")?; let repo = Repository::discover(path).unwrap();