octopus: add supergit dependency and parse repo path via route param

wip/yesman
Katharina Fey 3 years ago
parent 17bb1561b3
commit 3b65ab3fe1
  1. 2
      apps/servers/octopus/Cargo.lock
  2. 6
      apps/servers/octopus/Cargo.toml
  3. 11
      apps/servers/octopus/src/git.rs
  4. 117
      apps/servers/octopus/src/git/log.rs
  5. 58
      apps/servers/octopus/src/git/mod.rs
  6. 176
      apps/servers/octopus/src/git/tree.rs
  7. 42
      apps/servers/octopus/src/main.rs
  8. 19
      apps/servers/octopus/src/pages/files.rs
  9. 2
      apps/servers/octopus/src/templ_data/mod.rs

@ -2256,7 +2256,9 @@ dependencies = [
"askama",
"env_logger",
"git2",
"log",
"markdown",
"supergit",
]
[[package]]

@ -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",
]

@ -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()
}

@ -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<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,
}
}

@ -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<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(),
)
}
}

@ -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<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
}

@ -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")?

@ -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<HttpResponse> {
pub async fn render((req, path): (HttpRequest, web::Path<String>)) -> Result<HttpResponse> {
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();

@ -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,
}

Loading…
Cancel
Save