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/git | |
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/git')
-rw-r--r-- | apps/servers/octopus/src/git/log.rs | 117 | ||||
-rw-r--r-- | apps/servers/octopus/src/git/mod.rs | 58 | ||||
-rw-r--r-- | apps/servers/octopus/src/git/tree.rs | 176 |
3 files changed, 351 insertions, 0 deletions
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 +} |