mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
split deltas store into persistent and current
This commit is contained in:
parent
6299a97e73
commit
d9e13a784a
90
src-tauri/src/deltas/storage/current.rs
Normal file
90
src-tauri/src/deltas/storage/current.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
mod current;
|
||||
mod persistent;
|
||||
mod storage;
|
||||
|
||||
pub use storage::Store;
|
||||
|
88
src-tauri/src/deltas/storage/persistent.rs
Normal file
88
src-tauri/src/deltas/storage/persistent.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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))?;
|
||||
|
@ -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(
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user