diff --git a/apps/servers/octopus/Cargo.lock b/apps/servers/octopus/Cargo.lock index 24f6c0c539c..be8b39311b7 100644 --- a/apps/servers/octopus/Cargo.lock +++ b/apps/servers/octopus/Cargo.lock @@ -2256,7 +2256,9 @@ dependencies = [ "askama", "env_logger", "git2", + "log", "markdown", + "supergit", ] [[package]] diff --git a/apps/servers/octopus/Cargo.toml b/apps/servers/octopus/Cargo.toml index afc01fdcab4..c0101294559 100644 --- a/apps/servers/octopus/Cargo.toml +++ b/apps/servers/octopus/Cargo.toml @@ -11,8 +11,10 @@ actix-rt = "1.0.0" actix-web = "2.0.0" askama = "0.8" env_logger = "0.6" +log = "*" git2 = "0.11" markdown = { version = "0.3.0", optional = true } +supergit = { path = "supergit" } [build-dependencies] askama = "0.8" @@ -23,6 +25,6 @@ markdown-readme = ["markdown"] [workspace] members = [ - ".", - "supergit", + ".", + "supergit", ] \ No newline at end of file diff --git a/apps/servers/octopus/src/git.rs b/apps/servers/octopus/src/git.rs new file mode 100644 index 00000000000..f1c8ef9da7e --- /dev/null +++ b/apps/servers/octopus/src/git.rs @@ -0,0 +1,11 @@ +//! Git abstraction module +//! +//! Provide a few utility functions around supergit + +use supergit::Repository; + +pub(crate) fn open() -> Repository { + let path = std::env::var("OCTOPUS_REPOSITORY").unwrap(); + trace!("Loading bare git repo {}", path); + Repository::open(&path).unwrap() +} diff --git a/apps/servers/octopus/src/git/log.rs b/apps/servers/octopus/src/git/log.rs deleted file mode 100644 index c8f4aa37ccf..00000000000 --- a/apps/servers/octopus/src/git/log.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! 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, - /// List of all files, and the commits in which they were touched - file_refs: BTreeMap>, - /// Map of commit IDs to metadata - commit_refs: BTreeMap, -} - -#[derive(Debug)] -pub(crate) struct CommitNode { - id: String, - author: String, - message: String, - touches: BTreeSet, - time: i64, -} - -fn build_diff_log(repo: &Repo, log: Vec<(String, Vec)>) -> Vec { - 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::>(); - 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 deleted file mode 100644 index 244e2f45e6c..00000000000 --- a/apps/servers/octopus/src/git/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! 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, - rev: Option, -} - -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 deleted file mode 100644 index 5343a57c246..00000000000 --- a/apps/servers/octopus/src/git/tree.rs +++ /dev/null @@ -1,176 +0,0 @@ -//! 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, -} - -impl Tree { - /// Insert a node into a subtree with it's full path - fn insert_to_subtree(&mut self, mut path: Vec, 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 { - 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 = 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, - pub name: String, -} - -#[derive(Clone, Debug)] -pub(crate) struct DirNode { - pub path: Vec, - 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 = 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 index 8ed6445dd4a..c8660bb9221 100644 --- a/apps/servers/octopus/src/main.rs +++ b/apps/servers/octopus/src/main.rs @@ -1,26 +1,52 @@ -//mod git; -mod pages; -mod repo; -mod templ_data; +//! Octopus git monorepo explorer +//! +//! This file is the entry-point to the octopus server (previously +//! called `webgit` - some mentions to this name are still in the +//! code). +//! +//! Because of the way that the libgit2 rust abstraction handles +//! state, we can't embed a handle to the repository into the +//! application app state; instead each endpoint (meaning route) will +//! load and index the repository itself. Because all operations in +//! supergit are lazy this does not add too much overhead. If there +//! are operations that take too long, we can start building an +//! explicit cache for them. -mod project; +#[macro_use] +extern crate log; + +pub(crate) mod git; +pub(crate) mod pages; +pub(crate) mod project; +pub(crate) mod repo; +pub(crate) mod state; +pub(crate) mod templ_data; use actix_files as fs; -use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{web, App, 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"); + std::env::set_var("RUST_LOG", "actix_server=info,webgit=trace"); env_logger::init(); let root = PathBuf::new(); + // Check that we know where the repo is. + if std::env::var("OCTOPUS_REPOSITORY").is_err() { + error!("Failed to determine repository path! Please set the `OCTOPUS_REPOSITORY` env variable!"); + std::process::exit(2); + } + + // Start a new server with a few basic routes 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))) + + // Match on tree/* where paths can be arbitrarily recursive + .service(web::resource("/tree/{path:[^{}]*}").route(web::get().to(pages::files))) .default_service(web::resource("").route(web::get().to(pages::p404))) }) .bind("127.0.0.1:8080")? diff --git a/apps/servers/octopus/src/pages/files.rs b/apps/servers/octopus/src/pages/files.rs index 73a86a46918..0f50d6b4722 100644 --- a/apps/servers/octopus/src/pages/files.rs +++ b/apps/servers/octopus/src/pages/files.rs @@ -1,17 +1,30 @@ //! The main file browser -use crate::templ_data::{files::Files, BaseData}; +use crate::{ + git, + templ_data::{files::Files, BaseData}, +}; use actix_web::{web, HttpRequest, HttpResponse, Result}; use askama::Template; -pub async fn render(req: HttpRequest) -> Result { +pub async fn render((req, path): (HttpRequest, web::Path)) -> Result { + let repo = git::open(); + let branch = repo.get_branch("main".into()).unwrap(); // FIXME: this is baaaaad + let head = branch.get_head(); + let tree = head.get_tree(); + + debug!("Loading path: `{}`", path); + let _yield = tree.load(&path).unwrap(); + debug!("{:#?}", _yield); + + let files = Files { base: BaseData { sitename: "dev.spacekookie.de".into(), ..BaseData::default() }, readme: None, - path: "".into(), + path: "/".into(), } .render() .unwrap(); diff --git a/apps/servers/octopus/src/templ_data/mod.rs b/apps/servers/octopus/src/templ_data/mod.rs index 7645e95ef82..41a96b57244 100644 --- a/apps/servers/octopus/src/templ_data/mod.rs +++ b/apps/servers/octopus/src/templ_data/mod.rs @@ -30,7 +30,7 @@ impl Default for BaseData { 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()), + siteurl: env::var("_OCTOPUS_SITE_URL").unwrap_or("http://localhost:8080".to_string()), sitename: env::var("_OCTOPUS_SITE_NAME").unwrap_or("test-octopus".to_string()), has_wiki: true, }