aboutsummaryrefslogtreecommitdiff
path: root/apps/servers/octopus/src/git
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/git
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/git')
-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
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(&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
+}