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