My personal project and infrastructure archive
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
nomicon/apps/servers/octopus/supergit/src/files.rs

247 lines
7.0 KiB

use crate::{Branch, BranchIter, Commit, HashId};
use atomptr::AtomPtr;
use git2::{ObjectType, TreeWalkMode, TreeWalkResult};
use std::collections::BTreeMap;
use std::{path::PathBuf, sync::Arc};
/// A tree of files
pub struct FileTree {
repo: Arc<git2::Repository>,
tree: AtomPtr<BTreeMap<String, Arc<TreeEntry>>>,
}
impl FileTree {
/// Utility function to create a tree, and then parse it too
pub(crate) fn new(repo: &Arc<git2::Repository>, commit: HashId) -> Arc<Self> {
Arc::new(Self {
repo: Arc::clone(repo),
tree: AtomPtr::new(BTreeMap::new()),
})
.parse(commit)
}
/// Parse a tree from a specific commit
pub(crate) fn parse(self: Arc<Self>, commit: HashId) -> Arc<Self> {
let mut new_tree = BTreeMap::new();
let tree = (&self.repo)
.find_commit(commit.to_oid())
.unwrap()
.tree()
.unwrap();
tree.walk(TreeWalkMode::PreOrder, |p, entry| {
let path_segs: Vec<_> = p.split("/").filter(|s| s != &"").collect();
let path = if path_segs.len() == 0 {
None
} else {
Some(path_segs)
};
let te = TreeEntry::generate(path, entry);
new_tree.insert(te.path(), Arc::new(te));
TreeWalkResult::Ok
})
.unwrap();
// Add a special entry for the root of the repo
new_tree.insert(
"".into(),
Arc::new(TreeEntry::Dir(Directory {
id: tree.id().into(),
path: "".into(),
name: "".into(),
})),
);
// This is needed to make borrowchk shut up
drop(tree);
// Atomicly swap new tree into place
self.tree.swap(new_tree);
self
}
fn get_entry(&self, path: &str) -> Option<Arc<TreeEntry>> {
self.tree.get_ref().get(path).map(|e| Arc::clone(&e))
}
/// Load a file entry in this `FileTree` from disk
///
/// When calling this function on a directory, nothing will happen
/// (returns `None`), because directories can't be loaded. If you
/// want to get a list of children for a directory, use
/// [`FileTree::enumerate()`]() instead!
pub fn load(&self, path: &str) -> Option<Yield> {
self.get_entry(path).and_then(|e| e.load(&self.repo))
}
}
/// Data yielded from loading a part of the file tree
///
/// This type is returned when fetching a path via `FileTree::load()`,
/// and can either be a single file read into memory, or an
/// enumeration of direct children of a directory.
///
/// To get all children of a subtree, use `Yield::into_tree()` to
/// create a new, recursive `FileTree` to enumerate.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Yield {
/// Load a single file into a buffer
File(Vec<u8>),
/// Enumerate children in a directory
Dir(Vec<String>),
}
enum TreeEntry {
/// A single file
File(File),
/// A sub-tree
Dir(Directory),
}
impl TreeEntry {
fn generate(path_segments: Option<Vec<&str>>, entry: &git2::TreeEntry) -> Self {
let path = path_segments.map_or("".into(), |p| path_segs_join(p));
let id = entry.id().into();
let name = entry.name().unwrap().into();
match entry.kind() {
Some(ObjectType::Blob) => Self::File(File::new(id, path, name)),
Some(ObjectType::Tree) => Self::Dir(Directory::new(id, path, name)),
_ => unimplemented!(),
}
}
fn load(&self, repo: &Arc<git2::Repository>) -> Option<Yield> {
let id = self.id();
match self {
Self::File(ref f) => repo
.find_blob(id.into())
.ok()
.map(|b| Yield::File(b.content().into())),
Self::Dir(ref d) => repo
.find_tree(id.into())
.ok()
.map(|tree| {
let mut children = vec![];
// Iterate the tree, but only as long as there are no
// additional path segments
tree.walk(TreeWalkMode::PreOrder, |p, entry| {
let path_segs: Vec<_> = p.split("/").filter(|s| s != &"").collect();
if path_segs.len() > 0 {
TreeWalkResult::Skip
} else {
// Take the current tree path, and append the
// name of whatever we're currently iterating
// over is
let path = PathBuf::new().join(self.path()).join(entry.name().unwrap());
children.push(path.as_path().to_str().unwrap().into());
TreeWalkResult::Ok
}
});
children
})
.map(|c| Yield::Dir(c)),
}
}
fn is_file(&self) -> bool {
match self {
Self::File(_) => true,
Self::Dir(_) => false,
}
}
fn id(&self) -> HashId {
match self {
Self::File(ref f) => f.id.clone(),
Self::Dir(ref d) => d.id.clone(),
}
}
/// Get the repo-internal path (including name)
///
/// This is used to index files in a file tree, to allow O(1)
/// access to deeply nested items.
fn path(&self) -> String {
match self {
Self::File(ref f) => PathBuf::new().join(&f.path).join(&f.name),
Self::Dir(ref d) => PathBuf::new().join(&d.path).join(&d.name),
}
.as_path()
.to_str()
.unwrap()
.into()
}
}
struct File {
id: HashId,
path: String,
name: String,
}
impl File {
fn new(id: HashId, path: String, name: String) -> Self {
Self { id, path, name }
}
}
struct Directory {
id: HashId,
path: String,
name: String,
}
impl Directory {
fn new(id: HashId, path: String, name: String) -> Self {
Self { id, path, name }
}
fn enumerate(&self, repo: git2::Repository) -> Vec<String> {
vec![]
}
}
////////////////////////////////
/// Take a vector of path segments, and turn it into a valid offset path
///
/// There are tests to make sure this function works properly.
/// Following are some example transformations.
///
/// * vec![] -> ""
/// * vec!["foo"] -> "foo"
/// * vec!["foo", "bar", "baz"] -> "foo/bar/baz"
fn path_segs_join(segments: Vec<&str>) -> String {
segments
.into_iter()
.fold(PathBuf::new(), |buf, seg| buf.join(seg))
.as_path()
.to_str()
.unwrap()
.to_owned()
}
#[test]
fn empty_path() {
assert_eq!(path_segs_join(vec![]), String::from(""));
}
#[test]
fn one_path() {
assert_eq!(path_segs_join(vec!["foo".into()]), String::from("foo"));
}
#[test]
fn nested_path() {
assert_eq!(
path_segs_join(vec!["foo".into(), "bar".into(), "baz".into()]),
String::from("foo/bar/baz")
);
}