diff options
author | Katharina Fey <kookie@spacekookie.de> | 2020-10-31 18:57:39 +0100 |
---|---|---|
committer | Mx Kookie <kookie@spacekookie.de> | 2020-12-21 05:10:08 +0100 |
commit | 9dacf748651ea7139c0e9f3dee9ae66d949bf73f (patch) | |
tree | 7101bc20d5a531218ccd1b4cb9e04c67995f6d0e /apps/servers/octopus/src | |
parent | 4e09fe2509904ee64d2470ca8d41006d51e4ffd6 (diff) |
Add 'apps/servers/octopus/' from commit '623954d19fdf0dca47db319e5c88ee561aa8d25c'
git-subtree-dir: apps/servers/octopus
git-subtree-mainline: 4e09fe2509904ee64d2470ca8d41006d51e4ffd6
git-subtree-split: 623954d19fdf0dca47db319e5c88ee561aa8d25c
Diffstat (limited to 'apps/servers/octopus/src')
20 files changed, 826 insertions, 0 deletions
diff --git a/apps/servers/octopus/src/cli.rs b/apps/servers/octopus/src/cli.rs new file mode 100644 index 000000000000..500a901954ed --- /dev/null +++ b/apps/servers/octopus/src/cli.rs @@ -0,0 +1,79 @@ +use clap::{App, Arg}; +use colored::Colorize; +use std::{ + env, + fs::File, + path::{Path, PathBuf}, +}; + +pub struct Paths { + pub config: File, + pub data: PathBuf, +} + +/// Initialise the application by getting valid path options +pub fn init() -> Paths { + let app = App::new("webgit") + .about("The friendly and simple git web frontend") + .version("0.0.0") + .arg( + Arg::with_name("CONFIG") + .short("c") + .long("config") + .takes_value(true) + .help( + "Provide the path to the system configuration. Alternatively \ + set WEBGIT_CONFIG_PATH in your env", + ), + ) + .arg( + Arg::with_name("DATA_DIR") + .short("d") + .long("data-dir") + .takes_value(true) + .help( + "Specify where webgit should save git repositories. Alternatively \ + set WEBGIT_DATA_DIR in your env", + ), + ); + + let matches = app.get_matches(); + + Paths { + config: File::open( + env::var_os("WEBGIT_CONFIG_PATH") + .map(|os| match os.into_string() { + Ok(p) => p.to_owned(), + Err(_) => { + eprintln!("{}: Failed to parse provided config path!", "Error:".red()); + std::process::exit(2); + } + }) + .unwrap_or_else(|| match matches.value_of("CONFIG") { + Some(p) => p.to_owned(), + None => { + eprintln!("{}: No config provided!", "Error:".red()); + std::process::exit(2); + } + }), + ) + .expect(&format!("{}: Config file not found!", "Error:".red())), + data: Path::new( + &env::var_os("WEBGIT_DATA_DIR") + .map(|os| { + os.into_string().expect(&format!( + "{}: Failed to parse provided data-dir path!", + "Error".red() + )) + }) + .unwrap_or_else(|| match matches.value_of("CONFIG") { + Some(p) => p.to_owned(), + None => { + eprintln!("{}: No data dir provided!", "Error:".red()); + std::process::exit(2); + } + }), + ) + .into(), + } +} diff --git a/apps/servers/octopus/src/config.rs b/apps/servers/octopus/src/config.rs new file mode 100644 index 000000000000..99e8cffd94b9 --- /dev/null +++ b/apps/servers/octopus/src/config.rs @@ -0,0 +1,17 @@ +//! Configuration to run octopus + +pub struct Config { + app_path: String, + port: u16, + handle_ssl: bool, + cache_path: String, + repos_path: String, + repo_discovery: bool, + repos: Vec<RepoConfig> +} + +pub struct RepoConfig { + name: String, + description: String, + category: String, +} diff --git a/apps/servers/octopus/src/git/log.rs b/apps/servers/octopus/src/git/log.rs new file mode 100644 index 000000000000..c8f4aa37ccf2 --- /dev/null +++ b/apps/servers/octopus/src/git/log.rs @@ -0,0 +1,117 @@ +//! libgit2 log parsing + +use crate::git::{tree::FileNode, Repo}; +use git2::Oid; +use std::collections::{BTreeMap, BTreeSet}; + +/// A file-commit referenced graph thing +/// +/// git is _weird_! It's essentially just a glorified key-value store +/// and it shows. There's no utilities to figure out how thing are +/// related, and all the actual graph things in git are sugar on top +/// of this store. +/// +/// In order to make sense of anything in a repo we need to quite +/// heavily parse the log. This type here is the result of this +/// parsing: you can ask it smart questions like "when did this file +/// change" and it will tell you (sort of). +#[derive(Debug, Default)] +pub(crate) struct CommitGraph { + /// The correct order of commits in the log + order: Vec<String>, + /// List of all files, and the commits in which they were touched + file_refs: BTreeMap<String, Vec<String>>, + /// Map of commit IDs to metadata + commit_refs: BTreeMap<String, CommitNode>, +} + +#[derive(Debug)] +pub(crate) struct CommitNode { + id: String, + author: String, + message: String, + touches: BTreeSet<String>, + time: i64, +} + +fn build_diff_log(repo: &Repo, log: Vec<(String, Vec<FileNode>)>) -> Vec<CommitNode> { + todo!() +} + +/// Walk through all commits from a given ref and build a commit graph +pub(crate) fn create_commit_log(rev: String, repo: &Repo) -> CommitGraph { + let mut walker = repo.get_inner().revwalk().unwrap(); + walker.push(Oid::from_str(rev.as_str()).unwrap()).unwrap(); + let mut commits = walker + .into_iter() + .map(|oid| { + let oid = oid.unwrap(); + repo.get_inner().find_commit(oid).unwrap() + }) + .collect::<Vec<_>>(); + commits.reverse(); + + let mut initial: Vec<(_, _)> = commits + .into_iter() + .map(|commit| { + let id = format!("{}", commit.id()); + (id.clone(), repo.get_tree(id.as_str())) + }) + .collect(); + + // split off rest of the diffs and dissolve the len(1) vec + let log = initial.split_off(1); + let previous = initial.remove(0).1; + + let mut order = vec![]; + let (commit_refs, file_refs) = log.into_iter().fold( + (BTreeMap::new(), BTreeMap::new()), + |(mut cm, mut fm), (cid, current)| { + let commit_id = format!("{}", cid); + + let d = repo + .get_inner() + .diff_tree_to_tree(Some(&previous), Some(¤t), None) + .unwrap(); + + // Store the commit to preserve order + order.push(commit_id.clone()); + + // For each file, store this commit as one that touched it + let touches = d.deltas().fold(BTreeSet::new(), |mut set, delta| { + let file_id = format!("{}", delta.new_file().id()); + fm.entry(file_id.clone()) + .or_insert(vec![]) + .push(commit_id.clone()); + set.insert(file_id); + set + }); + + // From the commit, build a metadata object + let commit_u = repo + .get_inner() + .find_commit(Oid::from_str(cid.as_str()).unwrap()) + .unwrap(); + let author_u = commit_u.author(); + let commit = CommitNode { + id: commit_id, + message: commit_u.message().unwrap().to_owned(), + author: format!("{} {}", author_u.name().unwrap(), author_u.email().unwrap()), + touches, + time: author_u.when().seconds(), + }; + + // Insert the metadata object + cm.insert(cid.clone(), commit); + + // We pass both the modified maps into the next commit + (cm, fm) + }, + ); + + CommitGraph { + order, + file_refs, + commit_refs, + } +} diff --git a/apps/servers/octopus/src/git/mod.rs b/apps/servers/octopus/src/git/mod.rs new file mode 100644 index 000000000000..244e2f45e6c5 --- /dev/null +++ b/apps/servers/octopus/src/git/mod.rs @@ -0,0 +1,58 @@ +//! Wrappers for libgit2 + +pub mod log; +pub mod tree; + +use git2::{self, Repository}; +use log::CommitGraph; +use tree::Tree; + +/// A top-level wrapper API for all libgit2 functions +pub struct Repo { + inner: Repository, + commits: Option<CommitGraph>, + rev: Option<String>, +} + +impl Repo { + pub(crate) fn new(path: &str) -> Self { + Self { + inner: Repository::open(path).expect(&format!("`{}` is not a valid git repo", path)), + commits: None, + rev: None, + } + } + + pub(self) fn get_inner(&self) -> &Repository { + &self.inner + } + + pub(self) fn get_tree<'r>(&'r self, rev: &str) -> git2::Tree<'r> { + self.inner + .revparse_single(rev) + .unwrap() + .peel_to_tree() + .unwrap() + } + + pub(crate) fn clear_cache(&mut self) { + self.rev = None; + self.commits = None; + } + + /// Load and cache commits for a specific rev + pub(crate) fn load_commits(&mut self, rev: String) { + self.rev = Some(rev.clone()); + self.commits = Some(log::create_commit_log(rev, &self)); + } + + /// Load the tree of files for the current rev + /// + /// Will fail if no rev was previously cached + pub(crate) fn get_file_tree(&self) -> Tree { + tree::parse_tree( + self.get_tree(self.rev.as_ref().unwrap().as_str()), + self.get_inner(), + ) + } +} diff --git a/apps/servers/octopus/src/git/tree.rs b/apps/servers/octopus/src/git/tree.rs new file mode 100644 index 000000000000..5343a57c2463 --- /dev/null +++ b/apps/servers/octopus/src/git/tree.rs @@ -0,0 +1,176 @@ +//! Tree handling utilities +//! +//! The way that libgit2 handles trees is super low-level and overkill +//! for what we need. In this module we knock it down a notch or two. +//! +//! This code takes a tree returned by +//! `crate::git::repo::Repo::get_tree()`, and transforms it into a +//! `TreeData` type that the template engine can render. + +use crate::templ_data::repo::{CommitData, FileData, TreeData}; +use git2::{self, ObjectType, TreeWalkMode}; +use std::collections::BTreeMap; + +/// A cache of a repository tree +#[derive(Default, Debug, Clone)] +pub(crate) struct Tree { + inner: BTreeMap<String, TreeNode>, +} + +impl Tree { + /// Insert a node into a subtree with it's full path + fn insert_to_subtree(&mut self, mut path: Vec<String>, name: String, node: TreeNode) { + // If we are given a path, resolve it first + let curr = if path.len() > 0 { + let rest = path.split_off(1); + let mut curr = self.inner.get_mut(&path[0]).unwrap(); + + for dir in rest { + match curr { + TreeNode::Dir(ref mut d) => { + curr = d.children.inner.get_mut(&dir).unwrap(); + } + _ => panic!("Not a tree!"), + } + } + + match curr { + TreeNode::Dir(ref mut d) => &mut d.children, + TreeNode::File(_) => panic!("Not a tree!"), + } + } else { + // If no path was given, we assume the root is meant + self + }; + + curr.inner.insert(name, node); + } + + /// Walk through the tree and only return filenode objects + pub(crate) fn flatten(&self) -> Vec<FileNode> { + self.inner.values().fold(vec![], |mut vec, node| { + match node { + TreeNode::File(f) => vec.push(f.clone()), + TreeNode::Dir(d) => vec.append(&mut d.children.flatten()), + } + + vec + }) + } + + /// Get all the commits that touch a file + pub(crate) fn grab_path_history(&self, mut path: String) -> String { + let mut path: Vec<String> = path + .split("/") + .filter_map(|seg| match seg { + "" => None, + val => Some(val.into()), + }) + .collect(); + + let leaf = if path.len() > 0 { + let rest = path.split_off(1); + let mut curr = self.inner.get(&path[0]).unwrap(); + + for dir in rest { + match curr { + TreeNode::Dir(d) => curr = d.children.inner.get(&dir).unwrap(), + TreeNode::File(_) => break, // we reached the leaf + } + } + + curr + } else { + panic!("No valid path!"); + }; + + match leaf { + TreeNode::File(f) => f.id.clone(), + _ => panic!("Not a leaf!"), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) enum TreeNode { + File(FileNode), + Dir(DirNode), +} + +impl TreeNode { + fn name(&self) -> String { + match self { + Self::File(f) => f.name.clone(), + Self::Dir(d) => d.name.clone(), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct FileNode { + pub id: String, + pub path: Vec<String>, + pub name: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct DirNode { + pub path: Vec<String>, + pub name: String, + pub children: Tree, +} + +impl DirNode { + fn append(&mut self, node: TreeNode) { + self.children.inner.insert(node.name(), node); + } +} + +/// Take a series of path-segments and render a tree at that location +pub(crate) fn parse_tree(tree: git2::Tree, repo: &git2::Repository) -> Tree { + let mut root = Tree::default(); + + tree.walk(TreeWalkMode::PreOrder, |path, entry| { + let path: Vec<String> = path + .split("/") + .filter_map(|seg| match seg { + "" => None, + val => Some(val.into()), + }) + .collect(); + let name = entry.name().unwrap().to_string(); + + match entry.kind() { + // For every tree in the tree we create a new TreeNode with the path we know about + Some(ObjectType::Tree) => { + root.insert_to_subtree( + path.clone(), + name.clone(), + TreeNode::Dir(DirNode { + path, + name, + children: Tree::default(), + }), + ); + } + // If we encounter a blob, this is a file that we can simply insert into the tree + Some(ObjectType::Blob) => { + root.insert_to_subtree( + path.clone(), + name.clone(), + TreeNode::File(FileNode { + id: format!("{}", entry.id()), + path, + name, + }), + ); + } + _ => {} + } + + 0 + }) + .unwrap(); + + root +} diff --git a/apps/servers/octopus/src/main.rs b/apps/servers/octopus/src/main.rs new file mode 100644 index 000000000000..8ed6445dd4a8 --- /dev/null +++ b/apps/servers/octopus/src/main.rs @@ -0,0 +1,29 @@ +//mod git; +mod pages; +mod repo; +mod templ_data; + +mod project; + +use actix_files as fs; +use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +use std::io; +use std::path::PathBuf; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + std::env::set_var("RUST_LOG", "actix_server=info,octopus=debug"); + env_logger::init(); + let root = PathBuf::new(); + + HttpServer::new(move || { + App::new() + .service(fs::Files::new("/static", root.join("static"))) + .service(web::resource("/").route(web::get().to(pages::overview))) + .service(web::resource("/tree").route(web::get().to(pages::files))) + .default_service(web::resource("").route(web::get().to(pages::p404))) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} diff --git a/apps/servers/octopus/src/pages/files.rs b/apps/servers/octopus/src/pages/files.rs new file mode 100644 index 000000000000..73a86a46918e --- /dev/null +++ b/apps/servers/octopus/src/pages/files.rs @@ -0,0 +1,20 @@ +//! The main file browser + +use crate::templ_data::{files::Files, BaseData}; +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use askama::Template; + +pub async fn render(req: HttpRequest) -> Result<HttpResponse> { + let files = Files { + base: BaseData { + sitename: "dev.spacekookie.de".into(), + ..BaseData::default() + }, + readme: None, + path: "".into(), + } + .render() + .unwrap(); + + Ok(HttpResponse::Ok().content_type("text/html").body(files)) +} diff --git a/apps/servers/octopus/src/pages/mod.rs b/apps/servers/octopus/src/pages/mod.rs new file mode 100644 index 000000000000..2f1ed579c9a9 --- /dev/null +++ b/apps/servers/octopus/src/pages/mod.rs @@ -0,0 +1,15 @@ +//! All the pages in webgit +//! +//! A page is defined by it's template type as well as it's route, +//! which is exported from the module and then called by the router + +// pub mod repo; + +mod overview; +pub use overview::render as overview; + +mod p404; +pub use p404::render as p404; + +mod files; +pub use files::render as files; diff --git a/apps/servers/octopus/src/pages/overview.rs b/apps/servers/octopus/src/pages/overview.rs new file mode 100644 index 000000000000..ca8c9b37064c --- /dev/null +++ b/apps/servers/octopus/src/pages/overview.rs @@ -0,0 +1,24 @@ +//! Overview page +//! +//! This is the first page a user sees when they just go to the site +//! root. It renders the `README`, or `README.md` file from the modo +//! repo root, to provide users with a starting point. + +use crate::templ_data::{overview::Index, BaseData}; +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use askama::Template; + +pub async fn render(req: HttpRequest) -> Result<HttpResponse> { + let readme: String = markdown::to_html(include_str!("../../fake-readme.md").into()); + + let index = Index { + base: BaseData { + sitename: "dev.spacekookie.de".into(), + ..BaseData::default() + }, + readme, + } + .render() + .unwrap(); + Ok(HttpResponse::Ok().content_type("text/html").body(index)) +} diff --git a/apps/servers/octopus/src/pages/p404.rs b/apps/servers/octopus/src/pages/p404.rs new file mode 100644 index 000000000000..6427a19c60b7 --- /dev/null +++ b/apps/servers/octopus/src/pages/p404.rs @@ -0,0 +1,13 @@ +use actix_web::{HttpResponse, Result}; +use askama::Template; + +#[derive(Template)] +#[template(path = "404.html")] +struct P404; + +/// Render a simple 404 page +pub async fn render() -> Result<HttpResponse> { + Ok(HttpResponse::NotFound() + .content_type("text/html") + .body(P404.render().unwrap())) +} diff --git a/apps/servers/octopus/src/pages/repo/about.rs b/apps/servers/octopus/src/pages/repo/about.rs new file mode 100644 index 000000000000..1f207e2d56a5 --- /dev/null +++ b/apps/servers/octopus/src/pages/repo/about.rs @@ -0,0 +1,26 @@ +use crate::templ_data::repo::*; +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use askama::Template; + +/// Renders the "repository/about" subpage +pub async fn render(req: HttpRequest, path: web::Path<String>) -> Result<HttpResponse> { + let repo = pages::About { + readme: include_str!("../../../README").to_string(), + repo: RepoData { + owner: "spacekookie".into(), + name: "octopus".into(), + tagline: "A lightweight web frontend for git repositories".into(), + num_commit: 141, + num_branch: 1, + num_tag: 0, + num_contributor: 3, + size: "13.12M".into(), + logo: "fakeavi.png".into(), + }, + base: Default::default(), + } + .render() + .unwrap(); + + Ok(HttpResponse::Ok().content_type("text/html").body(repo)) +} diff --git a/apps/servers/octopus/src/pages/repo/details.rs b/apps/servers/octopus/src/pages/repo/details.rs new file mode 100644 index 000000000000..7298e15af4b8 --- /dev/null +++ b/apps/servers/octopus/src/pages/repo/details.rs @@ -0,0 +1,38 @@ +use crate::templ_data::repo::*; +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use askama::Template; + +/// Renders the "repository/about" subpage +pub async fn render(req: HttpRequest, path: web::Path<String>) -> Result<HttpResponse> { + let last_commit = CommitData { + hash: "84a9a0".into(), + message: "Updating just like... a bunch of shit".into(), + author: "Katharina Fey".into(), + date: "Today".into(), + diff: (125, 55), + }; + + let repo = pages::Details { + branches: vec![BranchData { + name: "develop".into(), + last_commit: last_commit.clone(), + }], + commits: vec![last_commit], + repo: RepoData { + owner: "spacekookie".into(), + name: "octopus".into(), + tagline: "A lightweight web frontend for git repositories".into(), + num_commit: 141, + num_branch: 1, + num_tag: 0, + num_contributor: 3, + size: "13.12M".into(), + logo: "fakeavi.png".into(), + }, + base: Default::default(), + } + .render() + .unwrap(); + + Ok(HttpResponse::Ok().content_type("text/html").body(repo)) +} diff --git a/apps/servers/octopus/src/pages/repo/mod.rs b/apps/servers/octopus/src/pages/repo/mod.rs new file mode 100644 index 000000000000..2b93592624ac --- /dev/null +++ b/apps/servers/octopus/src/pages/repo/mod.rs @@ -0,0 +1,7 @@ +//! The repository page subtree + +mod about; +mod details; + +pub use about::render as about; +pub use details::render as details; diff --git a/apps/servers/octopus/src/project/mod.rs b/apps/servers/octopus/src/project/mod.rs new file mode 100644 index 000000000000..cf81fd406873 --- /dev/null +++ b/apps/servers/octopus/src/project/mod.rs @@ -0,0 +1,8 @@ +//! Octopus project + +use std::{fs::File, path::PathBuf}; + +/// Check if a directory is a valid project +pub(crate) fn is_valid(p: PathBuf) -> bool { + p.join(".octopus").exists() +} diff --git a/apps/servers/octopus/src/repo.rs b/apps/servers/octopus/src/repo.rs new file mode 100644 index 000000000000..ca1f889027e7 --- /dev/null +++ b/apps/servers/octopus/src/repo.rs @@ -0,0 +1,43 @@ +use git2::{Commit, Error, Repository as Backend}; +use std::collections::HashSet; + +/// A structure that represents an existing bare repo on disk +pub struct Repository { + inner: Backend, +} + +impl Repository { + /// Open an existing bare repo from disk storage + pub fn open(path: &'static str) -> Self { + Self { + inner: Backend::open_bare(path).unwrap(), + } + } + + /// Get all commits on head + pub fn head<'a>(&'a self) -> Result<Vec<Commit<'a>>, Error> { + let mut walker = self.inner.revwalk().unwrap(); + walker.push_head()?; + walker + .into_iter() + .map(|oid| { + let oid = oid.unwrap(); + self.inner.find_commit(oid) + }) + .collect() + } + + /// Return the list of contributors + fn contributors(&self) -> Result<Vec<String>, Error> { + let head = self.head()?; + Ok(head + .iter() + .map(|c| c.author()) + .fold(HashSet::new(), |mut set, author| { + set.insert(author.name().unwrap().to_owned()); + set + }) + .into_iter() + .collect()) + } +} diff --git a/apps/servers/octopus/src/state/mod.rs b/apps/servers/octopus/src/state/mod.rs new file mode 100644 index 000000000000..5e7f9276bd97 --- /dev/null +++ b/apps/servers/octopus/src/state/mod.rs @@ -0,0 +1,14 @@ +//! Core octopus state handling + +use std::sync::Arc; + +pub(crate) type StateRef = Arc<OctoState>; + +/// Holds all state handles for the application +pub(crate) struct OctoState {} + +impl OctoState { + pub(crate) fn new() -> StateRef { + Arc::new(Self {}) + } +} diff --git a/apps/servers/octopus/src/templ_data/files.rs b/apps/servers/octopus/src/templ_data/files.rs new file mode 100644 index 000000000000..27a5bde99e5d --- /dev/null +++ b/apps/servers/octopus/src/templ_data/files.rs @@ -0,0 +1,13 @@ +//! File browser template data + +use super::BaseData; +use askama::Template; + +// This struct needs escapng=none to render README files it encounters along the way +#[derive(Template)] +#[template(path = "files.html", escape = "none")] +pub(crate) struct Files { + pub base: BaseData, + pub path: String, + pub readme: Option<String>, +} diff --git a/apps/servers/octopus/src/templ_data/mod.rs b/apps/servers/octopus/src/templ_data/mod.rs new file mode 100644 index 000000000000..7645e95ef824 --- /dev/null +++ b/apps/servers/octopus/src/templ_data/mod.rs @@ -0,0 +1,38 @@ +//! Octopus template data structures +//! +//! All pages are generated by the server via template files that have +//! data inputs. Because the templates follow a well-defined +//! structure (i.e. `core` extended by `<type>/base` extended by +//! `<type>/<page>`, the structure of these template data structures +//! is the same. +//! +//! The actual page initialisation and rendering is nested in the +//! `page` module, which then uses the appropriate template structures +//! defined here. + +pub(crate) mod overview; +pub(crate) mod files; + +use std::env; + +/// A basic application wide template structure +pub(crate) struct BaseData { + pub version: String, + pub source: String, + pub siteurl: String, + pub sitename: String, + pub has_wiki: bool, +} + +impl Default for BaseData { + fn default() -> Self { + Self { + version: env!("CARGO_PKG_VERSION").into(), + source: env::var("_OCTOPUS_SOURCE") + .unwrap_or("https://dev.spacekookie.de/web/octopus".to_string()), + siteurl: env::var("_OCTOPUS_SITE_URL").unwrap_or("localhost:8080".to_string()), + sitename: env::var("_OCTOPUS_SITE_NAME").unwrap_or("test-octopus".to_string()), + has_wiki: true, + } + } +} diff --git a/apps/servers/octopus/src/templ_data/overview.rs b/apps/servers/octopus/src/templ_data/overview.rs new file mode 100644 index 000000000000..693db0a85e77 --- /dev/null +++ b/apps/servers/octopus/src/templ_data/overview.rs @@ -0,0 +1,11 @@ +//! Template data for the main overview + +use super::BaseData; +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html", escape = "none")] +pub(crate) struct Index { + pub base: BaseData, + pub readme: String, +} diff --git a/apps/servers/octopus/src/templ_data/repo.rs b/apps/servers/octopus/src/templ_data/repo.rs new file mode 100644 index 000000000000..989c9b404620 --- /dev/null +++ b/apps/servers/octopus/src/templ_data/repo.rs @@ -0,0 +1,80 @@ +//! Repository specific template data + +use std::collections::BTreeMap; + +/// A simple overview of a repository +/// +/// This type can be generated by the octopus Repository state wrapper +#[derive(Clone)] +pub(crate) struct RepoData { + pub owner: String, + pub name: String, + pub tagline: String, + pub num_commit: usize, + pub num_branch: usize, + pub num_tag: usize, + pub num_contributor: usize, + pub size: String, + pub logo: String, +} + +/// Data about an individual commit +#[derive(Clone)] +pub(crate) struct CommitData { + pub hash: String, + pub message: String, + pub author: String, + pub date: String, + pub diff: (usize, usize), +} + +/// Data about a branch +#[derive(Clone)] +pub(crate) struct BranchData { + pub name: String, + pub last_commit: CommitData, +} + +/// Data about a repository tree +#[derive(Clone)] +pub(crate) struct TreeData { + /// The path segments in the current directory + curr_dir: Vec<String>, + /// The set of children in this tree segment + files: BTreeMap<String, FileData>, +} + +/// Information about a concrete file in a tree +#[derive(Clone)] +pub(crate) struct FileData { + name: String, + last_commit: CommitData, + is_directory: bool, +} + +pub(crate) mod pages { + use super::*; + use crate::templ_data::BaseData; + use askama::Template; + + #[derive(Template)] + #[template(path = "repo/about.html")] + pub(crate) struct About { + pub base: BaseData, + pub repo: RepoData, + + // Template specific + pub readme: String, + } + + #[derive(Template)] + #[template(path = "repo/details.html")] + pub(crate) struct Details { + pub base: BaseData, + pub repo: RepoData, + + // Template specifics + pub branches: Vec<BranchData>, + pub commits: Vec<CommitData>, + } +} |