aboutsummaryrefslogtreecommitdiff
path: root/apps/servers/octopus/src
diff options
context:
space:
mode:
authorKatharina Fey <kookie@spacekookie.de>2020-10-31 18:57:39 +0100
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:10:08 +0100
commit9dacf748651ea7139c0e9f3dee9ae66d949bf73f (patch)
tree7101bc20d5a531218ccd1b4cb9e04c67995f6d0e /apps/servers/octopus/src
parent4e09fe2509904ee64d2470ca8d41006d51e4ffd6 (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')
-rw-r--r--apps/servers/octopus/src/cli.rs79
-rw-r--r--apps/servers/octopus/src/config.rs17
-rw-r--r--apps/servers/octopus/src/git/log.rs117
-rw-r--r--apps/servers/octopus/src/git/mod.rs58
-rw-r--r--apps/servers/octopus/src/git/tree.rs176
-rw-r--r--apps/servers/octopus/src/main.rs29
-rw-r--r--apps/servers/octopus/src/pages/files.rs20
-rw-r--r--apps/servers/octopus/src/pages/mod.rs15
-rw-r--r--apps/servers/octopus/src/pages/overview.rs24
-rw-r--r--apps/servers/octopus/src/pages/p404.rs13
-rw-r--r--apps/servers/octopus/src/pages/repo/about.rs26
-rw-r--r--apps/servers/octopus/src/pages/repo/details.rs38
-rw-r--r--apps/servers/octopus/src/pages/repo/mod.rs7
-rw-r--r--apps/servers/octopus/src/project/mod.rs8
-rw-r--r--apps/servers/octopus/src/repo.rs43
-rw-r--r--apps/servers/octopus/src/state/mod.rs14
-rw-r--r--apps/servers/octopus/src/templ_data/files.rs13
-rw-r--r--apps/servers/octopus/src/templ_data/mod.rs38
-rw-r--r--apps/servers/octopus/src/templ_data/overview.rs11
-rw-r--r--apps/servers/octopus/src/templ_data/repo.rs80
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(&current), 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>,
+ }
+}