split deltas store into persistent and current

This commit is contained in:
Nikita Galaiko 2023-03-20 14:22:58 +01:00
parent 6299a97e73
commit d9e13a784a
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
8 changed files with 216 additions and 153 deletions

View File

@ -0,0 +1,90 @@
use crate::{deltas, fs, projects};
use anyhow::{Context, Result};
use std::{collections::HashMap, path::Path};
#[derive(Clone)]
pub struct Store {
project: projects::Project,
}
impl Store {
pub fn new(project: projects::Project) -> Self {
Self { project }
}
pub fn read<P: AsRef<Path>>(&self, file_path: P) -> Result<Option<Vec<deltas::Delta>>> {
let file_deltas_path = self.project.deltas_path().join(file_path);
if !file_deltas_path.exists() {
return Ok(None);
}
let file_deltas = std::fs::read_to_string(&file_deltas_path).with_context(|| {
format!(
"failed to read file deltas from {}",
file_deltas_path.to_str().unwrap()
)
})?;
let deltas: Vec<deltas::Delta> = serde_json::from_str(&file_deltas).with_context(|| {
format!(
"failed to parse file deltas from {}",
file_deltas_path.to_str().unwrap()
)
})?;
Ok(Some(deltas))
}
pub fn write<P: AsRef<Path>>(&self, file_path: P, deltas: &Vec<deltas::Delta>) -> Result<()> {
let delta_path = self.project.deltas_path().join(file_path);
let delta_dir = delta_path.parent().unwrap();
std::fs::create_dir_all(&delta_dir)?;
log::info!(
"{}: writing deltas to {}",
&self.project.id,
delta_path.to_str().unwrap()
);
let raw_deltas = serde_json::to_string(&deltas)?;
std::fs::write(delta_path.clone(), raw_deltas).with_context(|| {
format!(
"failed to write file deltas to {}",
delta_path.to_str().unwrap()
)
})?;
Ok(())
}
// returns deltas for a current session from .gb/session/deltas tree
pub fn list(&self, paths: Option<Vec<&str>>) -> Result<HashMap<String, Vec<deltas::Delta>>> {
let deltas_path = self.project.deltas_path();
if !deltas_path.exists() {
return Ok(HashMap::new());
}
let file_paths = fs::list_files(&deltas_path).with_context(|| {
format!("Failed to list files in {}", deltas_path.to_str().unwrap())
})?;
let deltas = file_paths
.iter()
.map_while(|file_path| {
if let Some(paths) = &paths {
if !paths.contains(&file_path.to_str().unwrap()) {
return None;
}
}
let file_deltas = self.read(Path::new(file_path));
match file_deltas {
Ok(Some(file_deltas)) => {
Some(Ok((file_path.to_str().unwrap().to_string(), file_deltas)))
}
Ok(None) => None,
Err(err) => Some(Err(err)),
}
})
.collect::<Result<HashMap<String, Vec<deltas::Delta>>>>()?;
Ok(deltas)
}
}

View File

@ -1,3 +1,5 @@
mod current;
mod persistent;
mod storage;
pub use storage::Store;

View File

@ -0,0 +1,88 @@
use crate::{deltas, projects, sessions};
use anyhow::Result;
use std::{collections::HashMap, path::Path};
pub struct Store {
project: projects::Project,
git_repository: git2::Repository,
}
impl Clone for Store {
fn clone(&self) -> Self {
Self {
project: self.project.clone(),
git_repository: git2::Repository::open(&self.project.path).unwrap(),
}
}
}
impl Store {
pub fn new(project: projects::Project) -> Result<Self> {
Ok(Self {
git_repository: git2::Repository::open(&project.path)?,
project,
})
}
pub fn list(
&self,
session: &sessions::Session,
paths: Option<Vec<&str>>,
) -> Result<HashMap<String, Vec<deltas::Delta>>> {
if session.hash.is_none() {
return Err(anyhow::anyhow!(format!(
"can not list persistent deltas from current session {}",
session.id
)));
}
let commit_hash = session.hash.as_ref().unwrap();
let commit_id = git2::Oid::from_str(commit_hash)?;
let commit = self.git_repository.find_commit(commit_id)?;
let tree = commit.tree()?;
let mut blobs = HashMap::new();
tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.name().is_none() {
return git2::TreeWalkResult::Ok;
}
let entry_path = Path::new(root).join(entry.name().unwrap());
if !entry_path.starts_with("session/deltas") {
return git2::TreeWalkResult::Ok;
}
if entry.kind() != Some(git2::ObjectType::Blob) {
return git2::TreeWalkResult::Ok;
}
let relative_file_path = entry_path.strip_prefix("session/deltas").unwrap();
if let Some(paths) = &paths {
if !paths.contains(&relative_file_path.to_str().unwrap()) {
return git2::TreeWalkResult::Ok;
}
}
let blob = entry
.to_object(&self.git_repository)
.and_then(|obj| obj.peel_to_blob());
let content = blob.map(|blob| blob.content().to_vec());
match content {
Ok(content) => {
let deltas: Result<Vec<deltas::Delta>> =
serde_json::from_slice(&content).map_err(|e| e.into());
blobs.insert(relative_file_path.to_owned(), deltas);
}
Err(e) => {
log::error!("Could not get blob for {}: {:#}", entry_path.display(), e);
}
}
git2::TreeWalkResult::Ok
})?;
let deltas = blobs
.into_iter()
.map(|(path, deltas)| (path.to_str().unwrap().to_owned(), deltas.unwrap()))
.collect();
Ok(deltas)
}
}

View File

@ -1,10 +1,15 @@
use crate::{deltas, fs, projects, sessions};
use crate::{deltas, projects, sessions};
use anyhow::{anyhow, Context, Result};
use std::{collections::HashMap, path::Path};
use super::{current, persistent};
pub struct Store {
project: projects::Project,
git_repository: git2::Repository,
persistent: persistent::Store,
current: current::Store,
}
impl Clone for Store {
@ -12,39 +17,24 @@ impl Clone for Store {
Self {
project: self.project.clone(),
git_repository: git2::Repository::open(&self.project.path).unwrap(),
current: self.current.clone(),
persistent: self.persistent.clone(),
}
}
}
impl Store {
pub fn new(git_repository: git2::Repository, project: projects::Project) -> Self {
Self {
project,
pub fn new(git_repository: git2::Repository, project: projects::Project) -> Result<Self> {
Ok(Self {
project: project.clone(),
git_repository,
}
current: current::Store::new(project.clone()),
persistent: persistent::Store::new(project)?,
})
}
pub fn read<P: AsRef<Path>>(&self, file_path: P) -> Result<Option<Vec<deltas::Delta>>> {
let file_deltas_path = self.project.deltas_path().join(file_path);
if !file_deltas_path.exists() {
return Ok(None);
}
let file_deltas = std::fs::read_to_string(&file_deltas_path).with_context(|| {
format!(
"failed to read file deltas from {}",
file_deltas_path.to_str().unwrap()
)
})?;
let deltas: Vec<deltas::Delta> = serde_json::from_str(&file_deltas).with_context(|| {
format!(
"failed to parse file deltas from {}",
file_deltas_path.to_str().unwrap()
)
})?;
Ok(Some(deltas))
self.current.read(file_path)
}
pub fn write<P: AsRef<Path>>(
@ -63,61 +53,11 @@ impl Store {
None => sessions::Session::from_head(&self.git_repository, &self.project),
}?;
let delta_path = self.project.deltas_path().join(file_path);
let delta_dir = delta_path.parent().unwrap();
std::fs::create_dir_all(&delta_dir)?;
log::info!(
"{}: writing deltas to {}",
&self.project.id,
delta_path.to_str().unwrap()
);
let raw_deltas = serde_json::to_string(&deltas)?;
std::fs::write(delta_path.clone(), raw_deltas).with_context(|| {
format!(
"failed to write file deltas to {}",
delta_path.to_str().unwrap()
)
})?;
self.current.write(file_path, deltas)?;
Ok(session)
}
// returns deltas for a current session from .gb/session/deltas tree
fn list_current_deltas(
&self,
paths: Option<Vec<&str>>,
) -> Result<HashMap<String, Vec<deltas::Delta>>> {
let deltas_path = self.project.deltas_path();
if !deltas_path.exists() {
return Ok(HashMap::new());
}
let file_paths = fs::list_files(&deltas_path).with_context(|| {
format!("Failed to list files in {}", deltas_path.to_str().unwrap())
})?;
let deltas = file_paths
.iter()
.map_while(|file_path| {
if let Some(paths) = &paths {
if !paths.contains(&file_path.to_str().unwrap()) {
return None;
}
}
let file_deltas = self.read(Path::new(file_path));
match file_deltas {
Ok(Some(file_deltas)) => {
Some(Ok((file_path.to_str().unwrap().to_string(), file_deltas)))
}
Ok(None) => None,
Err(err) => Some(Err(err)),
}
})
.collect::<Result<HashMap<String, Vec<deltas::Delta>>>>()?;
Ok(deltas)
}
pub fn list(
&self,
session_id: &str,
@ -133,67 +73,9 @@ impl Store {
}?;
if session.hash.is_none() {
self.list_current_deltas(paths).with_context(|| {
format!("Failed to list current deltas for session {}", session_id)
})
self.current.list(paths)
} else {
self.list_commit_deltas(&session.hash.unwrap(), paths)
.with_context(|| format!("Failed to list commit deltas for session {}", session_id))
self.persistent.list(&session, paths)
}
}
// returns deltas from gitbutler commit's session/deltas tree
fn list_commit_deltas(
&self,
commit_hash: &str,
paths: Option<Vec<&str>>,
) -> Result<HashMap<String, Vec<deltas::Delta>>> {
let commit_id = git2::Oid::from_str(commit_hash)?;
let commit = self.git_repository.find_commit(commit_id)?;
let tree = commit.tree()?;
let mut blobs = HashMap::new();
tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.name().is_none() {
return git2::TreeWalkResult::Ok;
}
let entry_path = Path::new(root).join(entry.name().unwrap());
if !entry_path.starts_with("session/deltas") {
return git2::TreeWalkResult::Ok;
}
if entry.kind() != Some(git2::ObjectType::Blob) {
return git2::TreeWalkResult::Ok;
}
let relative_file_path = entry_path.strip_prefix("session/deltas").unwrap();
if let Some(paths) = &paths {
if !paths.contains(&relative_file_path.to_str().unwrap()) {
return git2::TreeWalkResult::Ok;
}
}
let blob = entry
.to_object(&self.git_repository)
.and_then(|obj| obj.peel_to_blob());
let content = blob.map(|blob| blob.content().to_vec());
match content {
Ok(content) => {
let deltas: Result<Vec<deltas::Delta>> =
serde_json::from_slice(&content).map_err(|e| e.into());
blobs.insert(relative_file_path.to_owned(), deltas);
}
Err(e) => {
log::error!("Could not get blob for {}: {:#}", entry_path.display(), e);
}
}
git2::TreeWalkResult::Ok
})?;
let deltas = blobs
.into_iter()
.map(|(path, deltas)| (path.to_str().unwrap().to_owned(), deltas.unwrap()))
.collect();
Ok(deltas)
}
}

View File

@ -25,7 +25,7 @@ fn test_project() -> Result<(git2::Repository, projects::Project)> {
#[test]
fn test_read_none() {
let (repo, project) = test_project().unwrap();
let store = super::Store::new(repo, project);
let store = super::Store::new(repo, project).unwrap();
let file_path = Path::new("test.txt");
let deltas = store.read(file_path);
assert!(deltas.is_ok());
@ -35,7 +35,7 @@ fn test_read_none() {
#[test]
fn test_read_invalid() {
let (repo, project) = test_project().unwrap();
let store = super::Store::new(repo, project.clone());
let store = super::Store::new(repo, project.clone()).unwrap();
let file_path = Path::new("test.txt");
let full_file_path = project.deltas_path().join(file_path);
@ -49,7 +49,7 @@ fn test_read_invalid() {
#[test]
fn test_write_read() {
let (repo, project) = test_project().unwrap();
let store = super::Store::new(repo, project);
let store = super::Store::new(repo, project).unwrap();
let file_path = Path::new("test.txt");
let deltas = vec![Delta {
@ -67,7 +67,7 @@ fn test_write_read() {
#[test]
fn test_write_must_create_session() {
let (repo, project) = test_project().unwrap();
let store = super::Store::new(clone_repo(&repo), project.clone());
let store = super::Store::new(clone_repo(&repo), project.clone()).unwrap();
let file_path = Path::new("test.txt");
let deltas = vec![Delta {
@ -91,7 +91,7 @@ fn clone_repo(repo: &git2::Repository) -> git2::Repository {
#[test]
fn test_write_must_not_override_session() {
let (repo, project) = test_project().unwrap();
let store = super::Store::new(clone_repo(&repo), project.clone());
let store = super::Store::new(clone_repo(&repo), project.clone()).unwrap();
let file_path = Path::new("test.txt");
let session_before_write = sessions::Session::from_head(&repo, &project);

View File

@ -28,7 +28,7 @@ impl Repository {
Ok(Repository {
project: project.clone(),
git_repository,
deltas_storage: deltas::Store::new(git2::Repository::open(&project.path)?, project),
deltas_storage: deltas::Store::new(git2::Repository::open(&project.path)?, project)?,
})
}
@ -186,7 +186,8 @@ impl Repository {
options.recurse_untracked_dirs(true);
// get the status of the repository
let statuses = &self.git_repository
let statuses = &self
.git_repository
.statuses(Some(&mut options))
.with_context(|| "failed to get repository status")?;
@ -305,11 +306,11 @@ impl Repository {
pub fn flush_session(&self, user: &Option<users::User>) -> Result<()> {
// if the reference doesn't exist, we create it by creating a flushing a new session
let mut current_session = match sessions::Session::current(&self.git_repository, &self.project)?
{
Some(session) => session,
None => sessions::Session::from_head(&self.git_repository, &self.project)?,
};
let mut current_session =
match sessions::Session::current(&self.git_repository, &self.project)? {
Some(session) => session,
None => sessions::Session::from_head(&self.git_repository, &self.project)?,
};
current_session
.flush(&self.git_repository, user, &self.project)
.with_context(|| format!("{}: failed to flush session", &self.project.id))?;

View File

@ -35,7 +35,7 @@ fn test_filter_by_timestamp() {
let (repo, project) = test_project().unwrap();
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone());
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone()).unwrap();
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
deltas_storage
.write(
@ -105,7 +105,7 @@ fn test_sorted_by_timestamp() {
let (repo, project) = test_project().unwrap();
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone());
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone()).unwrap();
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
deltas_storage
.write(
@ -149,7 +149,7 @@ fn test_simple() {
let (repo, project) = test_project().unwrap();
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone());
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone()).unwrap();
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
deltas_storage
.write(

View File

@ -33,7 +33,7 @@ fn test_register_file_change_must_create_session() {
let relative_file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(relative_file_path), "test").unwrap();
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone());
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone()).unwrap();
let result =
super::delta::register_file_change(&project, &repo, &deltas_storage, &relative_file_path);
println!("{:?}", result);
@ -52,7 +52,7 @@ fn test_register_file_change_must_not_change_session() {
let relative_file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(relative_file_path), "test").unwrap();
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone());
let deltas_storage = deltas::Store::new(clone_repo(&repo), project.clone()).unwrap();
let result =
super::delta::register_file_change(&project, &repo, &deltas_storage, &relative_file_path);
assert!(result.is_ok());