use core::str::from_utf8;
use std::time::{SystemTime, UNIX_EPOCH};
use std::sync::RwLock;
use lmfu::LiteMap;
use super::internals::{
Result, Error, Mode, Directory, Path, TreeIter, Hash, CommitField, FileType,
ObjectStore, EntryType, Write, ObjectType, get_commit_field_hash,
};
pub struct Repository {
pub(crate) directories: RwLock<LiteMap<Hash, Directory>>,
pub(crate) objects: ObjectStore,
pub(crate) staged: ObjectStore,
pub(crate) upstream_head: Hash,
pub(crate) head: Hash,
pub(crate) root: Option<Hash>,
}
impl Repository {
pub fn new() -> Self {
Self {
directories: RwLock::new(LiteMap::new()),
objects: ObjectStore::new(),
staged: ObjectStore::new(),
upstream_head: Hash::zero(),
head: Hash::zero(),
root: None,
}
}
pub (crate) fn any_store_get(&self, hash: Hash, obj_type: ObjectType) -> Option<&[u8]> {
match self.staged.get_as(hash, obj_type) {
Some(entries) => Some(entries),
None => self.objects.get_as(hash, obj_type),
}
}
pub(crate) fn try_find_dir(&self, hash: Hash) -> Result<Option<Directory>> {
let mut iter = match self.any_store_get(hash, ObjectType::Tree) {
Some(entries) => TreeIter::new(entries),
None => return Ok(None),
};
let mut dir = Directory::new();
while let Some((node, hash, mode)) = iter.next()? {
dir.insert(node.into(), (hash, mode));
}
Ok(Some(dir))
}
pub(crate) fn find_dir(&self, hash: Hash) -> Result<Directory> {
let dir = self.try_find_dir(hash)?;
if dir.is_none() {
log::warn!("Missing directory for hash {}", hash);
}
Ok(dir.unwrap_or(Directory::new()))
}
pub(crate) fn remove_dir(&mut self, dir_hash: Hash) -> Result<Directory> {
let dirs_mut = self.directories.get_mut().unwrap();
match dirs_mut.remove(&dir_hash) {
Some(dir) => Ok(dir),
None => self.find_dir(dir_hash),
}
}
pub(crate) fn fetch_dir(&self, hash: Hash) -> Result<()> {
let present = {
let dirs = self.directories.read().unwrap();
dirs.contains_key(&hash)
};
if !present {
let dir = self.try_find_dir(hash)?.ok_or(Error::MissingObject)?;
let mut dirs_mut = self.directories.write().unwrap();
dirs_mut.insert(hash, dir);
}
Ok(())
}
pub(crate) fn get_commit_root(&self, commit_hash: Hash) -> Result<Option<Hash>> {
match self.objects.get_as(commit_hash, ObjectType::Commit) {
Some(commit) => match get_commit_field_hash(commit, CommitField::Tree)? {
Some(hash) => Ok(Some(hash)),
None => Err(Error::InvalidObject),
},
None => Ok(None),
}
}
pub(crate) fn find_in_dir(&self, dir: Hash, node: &str, filter: EntryType) -> Result<(Hash, Mode)> {
self.fetch_dir(dir)?;
let dirs = self.directories.read().unwrap();
let directory = dirs.get(&dir).unwrap();
match directory.get(node) {
Some((hash, mode)) => match mode.matches(filter) {
true => Ok((*hash, *mode)),
false => {
log::error!("wrong file type for {}: {:?} doesn't match {:?}", node, mode, filter);
Err(Error::PathError)
},
},
None => Err(Error::PathError),
}
}
pub fn for_each_entry<F: FnMut(&str, Mode, Hash)>(&self, path: &str, entry_type: EntryType, mut callback: F) -> Result<()> {
let path = Path::new(path);
let mut current = self.root.ok_or(Error::PathError)?;
for subdir in path.all() {
current = self.find_in_dir(current, subdir, EntryType::Directory)?.0;
}
self.fetch_dir(current)?;
let dirs = self.directories.read().unwrap();
let directory = dirs.get(¤t).unwrap();
for (node, (hash, mode)) in directory.iter() {
if mode.matches(entry_type) {
callback(node.as_str(), *mode, *hash);
}
}
Ok(())
}
pub fn read_file(&self, path: &str) -> Result<&[u8]> {
let path = Path::new(path);
let mut current = self.root.ok_or(Error::PathError)?;
for subdir in path.dirs()? {
current = self.find_in_dir(current, subdir, EntryType::Directory)?.0;
}
let (hash, _mode) = self.find_in_dir(current, path.file()?, EntryType::File)?;
self.any_store_get(hash, ObjectType::Blob).ok_or(Error::MissingObject)
}
pub fn file_exists(&self, path: &str) -> Result<bool> {
match self.read_file(path) {
Ok(_) => Ok(true),
Err(Error::PathError) => Ok(false),
e => e.map(|_| unreachable!()),
}
}
pub fn read_text(&self, path: &str) -> Result<&str> {
match from_utf8(self.read_file(path)?) {
Ok(string) => Ok(string),
Err(_) => Err(Error::InvalidObject),
}
}
pub(crate) fn find_committed_hash_root(&self, mut hash: Hash) -> Option<Hash> {
while let Some(entry) = self.staged.get(hash) {
hash = entry.delta_hint()?;
}
Some(hash)
}
pub(crate) fn update_dir<'a, I: Iterator<Item = &'a str>>(
&mut self,
mut directory: Directory,
steps: &mut I,
file_name: &str,
data: Option<(Vec<u8>, FileType)>,
) -> Result<Option<Directory>> {
let mut result = None;
let step = steps.next();
let node = step.unwrap_or(file_name);
let prev_hash = directory.get(node).map(|(hash, _mode)| *hash);
let delta_hint = prev_hash.and_then(|hash| self.find_committed_hash_root(hash));
if step.is_some() {
let subdir = match prev_hash {
Some(hash) => self.remove_dir(hash)?,
None => Directory::new(),
};
if let Some(subdir) = self.update_dir(subdir, steps, file_name, data)? {
let hash = self.staged.serialize_directory(&subdir, delta_hint);
self.directories.get_mut().unwrap().insert(hash, subdir);
result = Some((hash, Mode::Directory));
}
} else {
if let Some((data, ft)) = data {
let hash = self.staged.insert(ObjectType::Blob, data.into(), delta_hint);
result = Some((hash, ft.into()));
}
}
Ok(if let Some((hash, mode)) = result {
if self.objects.has(hash) {
self.staged.remove(hash);
}
directory.insert(node.into(), (hash, mode));
Some(directory)
} else {
directory.remove(node);
match directory.is_empty() {
true => None,
false => Some(directory),
}
})
}
pub fn stage(&mut self, path: &str, data: Option<(Vec<u8>, FileType)>) -> Result<()> {
let path = Path::new(path);
let root_dir = match self.root {
Some(hash) => self.remove_dir(hash)?,
None => Directory::new(),
};
let file_name = path.file()?;
let mut subdirs = path.dirs()?;
if let Some(root_dir) = self.update_dir(root_dir, &mut subdirs, file_name, data)? {
let prev_hash = self.root.and_then(|h| self.find_committed_hash_root(h));
let hash = self.staged.serialize_directory(&root_dir, prev_hash);
if self.objects.has(hash) {
self.staged.remove(hash);
}
self.directories.get_mut().unwrap().insert(hash, root_dir);
self.root = Some(hash);
} else {
self.root = None;
}
Ok(())
}
pub(crate) fn commit_object(&mut self, hash: Hash) {
if let Some(dir_entry) = self.staged.remove(hash) {
if dir_entry.obj_type() == ObjectType::Tree {
let dir = self.directories.get_mut().unwrap().insert(hash, Directory::new()).unwrap();
for (hash, _mode) in dir.iter_values() {
self.commit_object(*hash);
}
self.directories.get_mut().unwrap().insert(hash, dir).unwrap();
}
self.objects.insert_entry(dir_entry);
}
}
pub fn commit(
&mut self,
message: &str,
author: (&str, &str),
committer: (&str, &str),
timestamp: Option<u64>,
) -> Result<Hash> {
let timestamp = timestamp.unwrap_or_else(|| {
let now = SystemTime::now();
match now.duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_secs(),
_ => 0,
}
});
for string in [author.0, author.1, committer.0, committer.1] {
let has_newline = string.contains('\n');
let has_open = string.contains('<');
let has_close = string.contains('>');
if has_newline || has_open || has_close {
return Err(Error::InvalidObject);
}
}
let mut serialized = Vec::new();
if let Some(root) = self.root {
if Some(root) != self.get_commit_root(self.head).unwrap() {
self.commit_object(root);
}
}
let root = self.root.unwrap_or(Hash::zero());
write!(&mut serialized, "tree {}\n", root).unwrap();
if !self.head.is_zero() {
write!(&mut serialized, "parent {}\n", self.head).unwrap();
}
write!(&mut serialized, "author {} <{}> {} +0000\n", author.0, author.1, timestamp).unwrap();
write!(&mut serialized, "committer {} <{}> {} +0000\n", committer.0, committer.1, timestamp).unwrap();
write!(&mut serialized, "\n{}\n", message).unwrap();
self.head = self.objects.insert(ObjectType::Commit, serialized.into(), None);
Ok(self.head)
}
pub fn discard_commits(&mut self) {
self.head = self.upstream_head;
}
pub fn discard_changes(&mut self) {
self.staged = ObjectStore::new();
self.directories.get_mut().unwrap().clear();
self.root = self.get_commit_root(self.head).unwrap();
}
pub fn discard(&mut self) {
self.discard_commits();
self.discard_changes();
}
}