parent
17bb1561b3
commit
3b65ab3fe1
@ -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(¤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, |
|
||||||
} |
|
||||||
} |
|
@ -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 |
|
||||||
} |
|
Loading…
Reference in new issue