aboutsummaryrefslogtreecommitdiff
path: root/src/git/tree.rs
//! 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
}