mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
define file change listener
This commit is contained in:
parent
614fcbee49
commit
4fefeb26c0
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -1319,6 +1319,7 @@ dependencies = [
|
||||
"timed",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"urlencoding",
|
||||
"uuid 1.3.0",
|
||||
"walkdir",
|
||||
|
@ -46,6 +46,7 @@ futures = "0.3"
|
||||
futures-util = "0.3.8"
|
||||
timed = "0.2.1"
|
||||
serde-jsonlines = "0.4.0"
|
||||
tokio-util = "0.7.7"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
99
src-tauri/src/app/dispatchers/file_change.rs
Normal file
99
src-tauri/src/app/dispatchers/file_change.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::projects;
|
||||
use anyhow::Result;
|
||||
use notify::{Config, RecommendedWatcher, Watcher};
|
||||
use tokio::sync;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dispatcher {
|
||||
watcher: Arc<Mutex<Option<RecommendedWatcher>>>,
|
||||
project_path: String,
|
||||
project_id: String,
|
||||
}
|
||||
|
||||
impl Dispatcher {
|
||||
pub fn new(project: &projects::Project) -> Self {
|
||||
Self {
|
||||
watcher: Arc::new(Mutex::new(None)),
|
||||
project_path: project.path.clone(),
|
||||
project_id: project.id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
if let Some(mut watcher) = self.watcher.lock().unwrap().take() {
|
||||
watcher.unwatch(&std::path::Path::new(&self.project_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&self, rtx: sync::mpsc::Sender<PathBuf>) -> Result<()> {
|
||||
let (mut watcher, mut rx) = async_watcher()?;
|
||||
|
||||
watcher.watch(
|
||||
&std::path::Path::new(&self.project_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)?;
|
||||
self.watcher.lock().unwrap().replace(watcher);
|
||||
|
||||
log::info!("{}: file watcher started", self.project_id);
|
||||
|
||||
while let Some(res) = rx.recv().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
if !is_interesting_event(&event.kind) {
|
||||
continue;
|
||||
}
|
||||
for file_path in event.paths {
|
||||
let relative_file_path =
|
||||
file_path.strip_prefix(&self.project_path).unwrap();
|
||||
if let Err(e) = rtx.send(relative_file_path.to_path_buf()).await {
|
||||
log::error!(
|
||||
"{}: failed to send file change event: {:#}",
|
||||
self.project_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("{}: file watcher error: {:#}", self.project_id, e),
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("{}: file watcher stopped", self.project_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_interesting_event(kind: ¬ify::EventKind) -> bool {
|
||||
match kind {
|
||||
notify::EventKind::Create(notify::event::CreateKind::File) => true,
|
||||
notify::EventKind::Modify(notify::event::ModifyKind::Data(_)) => true,
|
||||
notify::EventKind::Modify(notify::event::ModifyKind::Name(_)) => true,
|
||||
notify::EventKind::Remove(notify::event::RemoveKind::File) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn async_watcher() -> notify::Result<(
|
||||
RecommendedWatcher,
|
||||
sync::mpsc::Receiver<notify::Result<notify::Event>>,
|
||||
)> {
|
||||
let (tx, rx) = sync::mpsc::channel(1);
|
||||
|
||||
let watcher = RecommendedWatcher::new(
|
||||
move |res| {
|
||||
futures::executor::block_on(async {
|
||||
tx.send(res).await.unwrap();
|
||||
})
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
|
||||
Ok((watcher, rx))
|
||||
}
|
2
src-tauri/src/app/dispatchers/mod.rs
Normal file
2
src-tauri/src/app/dispatchers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod file_change;
|
||||
pub mod tick;
|
47
src-tauri/src/app/dispatchers/tick.rs
Normal file
47
src-tauri/src/app/dispatchers/tick.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use crate::projects;
|
||||
use anyhow::Result;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use tokio::{sync, time};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dispatcher {
|
||||
project_id: String,
|
||||
stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Dispatcher {
|
||||
pub fn new(project: &projects::Project) -> Self {
|
||||
Self {
|
||||
project_id: project.id.clone(),
|
||||
stop: AtomicBool::new(false).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(
|
||||
&self,
|
||||
interval: time::Duration,
|
||||
rtx: sync::mpsc::Sender<std::time::Instant>,
|
||||
) -> Result<()> {
|
||||
let mut interval = time::interval(interval);
|
||||
log::info!("{}: ticker started", self.project_id);
|
||||
loop {
|
||||
if self.stop.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let tick = interval.tick().await;
|
||||
|
||||
if let Err(e) = rtx.send(tick.into_std()).await {
|
||||
log::error!("{}: failed to send tick event: {:#}", self.project_id, e);
|
||||
}
|
||||
}
|
||||
log::info!("{}: ticker stopped", self.project_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use super::{reader, writer};
|
||||
use crate::{projects, sessions};
|
||||
use anyhow::{anyhow, Context, Ok, Result};
|
||||
|
||||
@ -7,9 +8,37 @@ pub struct Repository {
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn open(project: &projects::Project) -> Result<Self> {
|
||||
let git_repository = git2::Repository::open(&project.path)
|
||||
.with_context(|| format!("{}: failed to open git repository", project.path))?;
|
||||
pub fn open<P: AsRef<std::path::Path>>(root: P, project: &projects::Project) -> Result<Self> {
|
||||
let root = root.as_ref();
|
||||
let path = root.join(&project.id);
|
||||
let git_repository = if path.exists() {
|
||||
git2::Repository::open(path)
|
||||
.with_context(|| format!("{}: failed to open git repository", project.path))?
|
||||
} else {
|
||||
// TODO: flush first session instead
|
||||
|
||||
let git_repository = git2::Repository::init_opts(
|
||||
&path,
|
||||
&git2::RepositoryInitOptions::new()
|
||||
.initial_head("refs/heads/current")
|
||||
.external_template(false),
|
||||
)
|
||||
.with_context(|| format!("{}: failed to initialize git repository", project.path))?;
|
||||
|
||||
let mut index = git_repository.index()?;
|
||||
let oid = index.write_tree()?;
|
||||
let signature = git2::Signature::now("gitbutler", "gitbutler@localhost").unwrap();
|
||||
git_repository.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
"Initial commit",
|
||||
&git_repository.find_tree(oid)?,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
git_repository
|
||||
};
|
||||
Ok(Self {
|
||||
project_id: project.id.clone(),
|
||||
git_repository,
|
||||
@ -20,6 +49,34 @@ impl Repository {
|
||||
Err(anyhow!("TODO"))
|
||||
}
|
||||
|
||||
pub fn get_wd_writer(&self) -> writer::DirWriter {
|
||||
writer::DirWriter::open(self.root())
|
||||
}
|
||||
|
||||
pub fn get_wd_reader(&self) -> reader::DirReader {
|
||||
reader::DirReader::open(self.root())
|
||||
}
|
||||
|
||||
pub fn get_head_reader(&self) -> Result<reader::CommitReader> {
|
||||
let head = self.git_repository.head().context("failed to get HEAD")?;
|
||||
let commit = head.peel_to_commit().context("failed to get HEAD commit")?;
|
||||
let reader = reader::CommitReader::from_commit(&self.git_repository, commit)?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
pub fn get_current_session(&self) -> Result<Option<sessions::Session>> {
|
||||
let reader = reader::DirReader::open(self.root());
|
||||
match sessions::Session::try_from(reader) {
|
||||
Result::Ok(session) => Ok(Some(session)),
|
||||
Err(sessions::SessionError::NoSession) => Ok(None),
|
||||
Err(sessions::SessionError::Err(err)) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root(&self) -> &std::path::Path {
|
||||
self.git_repository.path().parent().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn session_path(&self) -> std::path::PathBuf {
|
||||
self.git_repository.path().parent().unwrap().join("session")
|
||||
}
|
||||
|
157
src-tauri/src/app/listeners/file_change.rs
Normal file
157
src-tauri/src/app/listeners/file_change.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use crate::{
|
||||
app::{
|
||||
gb_repository, project_repository,
|
||||
reader::{self, Reader},
|
||||
writer::Writer,
|
||||
},
|
||||
deltas::{self, TextDocument},
|
||||
projects,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
pub struct Listener<'listener> {
|
||||
project_id: String,
|
||||
project_repository: &'listener project_repository::Repository,
|
||||
gb_repository: &'listener gb_repository::Repository,
|
||||
}
|
||||
|
||||
impl<'listener> Listener<'listener> {
|
||||
pub fn new(
|
||||
project: &projects::Project,
|
||||
project_repository: &'listener project_repository::Repository,
|
||||
gb_repository: &'listener gb_repository::Repository,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id: project.id.clone(),
|
||||
project_repository,
|
||||
gb_repository,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_file_content(&self, path: &std::path::Path) -> Result<Option<String>> {
|
||||
if self.project_repository.is_path_ignored(path)? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let reader = self.project_repository.get_wd_reader();
|
||||
|
||||
let path = path.to_str().unwrap();
|
||||
if reader.size(path)? > 100_000 {
|
||||
log::warn!("{}: ignoring large file: {}", self.project_id, path);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match reader.read(path)? {
|
||||
reader::Content::UTF8(content) => Ok(Some(content)),
|
||||
reader::Content::Binary(_) => {
|
||||
log::warn!("{}: ignoring non-utf8 file: {}", self.project_id, path);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_latest_file_contents(&self, path: &std::path::Path) -> Result<Option<String>> {
|
||||
let path = path.to_str().unwrap();
|
||||
let gb_head_reader = self
|
||||
.gb_repository
|
||||
.get_head_reader()
|
||||
.with_context(|| "failed to get gb head reader")?;
|
||||
let project_head_reader = self
|
||||
.project_repository
|
||||
.get_head_reader()
|
||||
.with_context(|| "failed to get project head reader")?;
|
||||
let reader = if gb_head_reader.exists(path) {
|
||||
gb_head_reader
|
||||
} else if project_head_reader.exists(path) {
|
||||
project_head_reader
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
if reader.size(path)? > 100_000 {
|
||||
log::warn!("{}: ignoring large file: {}", self.project_id, path);
|
||||
return Ok(None);
|
||||
}
|
||||
match reader.read(path)? {
|
||||
reader::Content::UTF8(content) => Ok(Some(content)),
|
||||
reader::Content::Binary(_) => {
|
||||
log::warn!("{}: ignoring non-utf8 file: {}", self.project_id, path);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_deltas(&self, path: &std::path::Path) -> Result<Option<Vec<deltas::Delta>>> {
|
||||
let reader = self.gb_repository.get_wd_reader();
|
||||
let deltas_path = self.gb_repository.deltas_path().join(path);
|
||||
match reader.read_to_string(deltas_path.to_str().unwrap()) {
|
||||
Ok(content) => Ok(Some(serde_json::from_str(&content)?)),
|
||||
Err(reader::Error::NotFound) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let current_file_content = match self
|
||||
.get_current_file_content(&path)
|
||||
.with_context(|| "failed to get current file content")?
|
||||
{
|
||||
Some(content) => content,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let latest_file_content = self
|
||||
.get_latest_file_contents(&path)
|
||||
.with_context(|| "failed to get latest file content")?;
|
||||
|
||||
let current_deltas = self
|
||||
.get_current_deltas(&path)
|
||||
.with_context(|| "failed to get current deltas")?;
|
||||
|
||||
let mut text_doc = match (latest_file_content, current_deltas) {
|
||||
(Some(latest_contents), Some(deltas)) => {
|
||||
deltas::TextDocument::new(Some(&latest_contents), deltas)?
|
||||
}
|
||||
(Some(latest_contents), None) => {
|
||||
deltas::TextDocument::new(Some(&latest_contents), vec![])?
|
||||
}
|
||||
(None, Some(deltas)) => deltas::TextDocument::new(None, deltas)?,
|
||||
(None, None) => deltas::TextDocument::new(None, vec![])?,
|
||||
};
|
||||
|
||||
if !text_doc.update(¤t_file_content)? {
|
||||
log::debug!(
|
||||
"{}: {} no new deltas, ignoring",
|
||||
self.project_id,
|
||||
path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("{}: {} changed", self.project_id, path.display());
|
||||
|
||||
let writer = self.gb_repository.get_wd_writer();
|
||||
|
||||
// save current deltas
|
||||
writer
|
||||
.write_string(
|
||||
self.gb_repository
|
||||
.deltas_path()
|
||||
.join(path)
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
&serde_json::to_string(&text_doc.get_deltas())?,
|
||||
)
|
||||
.with_context(|| "failed to write deltas")?;
|
||||
|
||||
// save file contents corresponding to the deltas
|
||||
writer
|
||||
.write_string(
|
||||
self.gb_repository.wd_path().join(path).to_str().unwrap(),
|
||||
¤t_file_content,
|
||||
)
|
||||
.with_context(|| "failed to write file content")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
155
src-tauri/src/app/listeners/file_change_tests.rs
Normal file
155
src-tauri/src/app/listeners/file_change_tests.rs
Normal file
@ -0,0 +1,155 @@
|
||||
use crate::{
|
||||
app::{gb_repository, project_repository},
|
||||
deltas, projects,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::file_change::Listener;
|
||||
|
||||
fn commit_all(repository: &git2::Repository) -> Result<git2::Oid> {
|
||||
let mut index = repository.index()?;
|
||||
index.add_all(&["."], git2::IndexAddOption::DEFAULT, None)?;
|
||||
index.write()?;
|
||||
let oid = index.write_tree()?;
|
||||
let signature = git2::Signature::now("test", "test@email.com").unwrap();
|
||||
let commit_oid = repository.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
"some commit",
|
||||
&repository.find_tree(oid)?,
|
||||
&[&repository.find_commit(repository.refname_to_id("HEAD")?)?],
|
||||
)?;
|
||||
Ok(commit_oid)
|
||||
}
|
||||
|
||||
fn test_repository() -> Result<git2::Repository> {
|
||||
let path = tempdir()?.path().to_str().unwrap().to_string();
|
||||
let repository = git2::Repository::init(&path)?;
|
||||
let mut index = repository.index()?;
|
||||
let oid = index.write_tree()?;
|
||||
let signature = git2::Signature::now("test", "test@email.com").unwrap();
|
||||
repository.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
"Initial commit",
|
||||
&repository.find_tree(oid)?,
|
||||
&[],
|
||||
)?;
|
||||
Ok(repository)
|
||||
}
|
||||
|
||||
fn test_project(repository: &git2::Repository) -> Result<projects::Project> {
|
||||
let project = projects::Project::from_path(
|
||||
repository
|
||||
.path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)?;
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_existing_file() -> Result<()> {
|
||||
let repository = test_repository()?;
|
||||
let project = test_project(&repository)?;
|
||||
let project_repo = project_repository::Repository::open(&project)?;
|
||||
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
||||
let gb_repo = gb_repository::Repository::open(gb_repo_path, &project)?;
|
||||
let listener = Listener::new(&project, &project_repo, &gb_repo);
|
||||
|
||||
let file_path = std::path::Path::new("test.txt");
|
||||
std::fs::write(project_repo.root().join(file_path), "test")?;
|
||||
commit_all(&repository)?;
|
||||
|
||||
std::fs::write(project_repo.root().join(file_path), "test2")?;
|
||||
listener.register(file_path)?;
|
||||
|
||||
let raw_deltas = std::fs::read_to_string(gb_repo.deltas_path().join(file_path))?;
|
||||
let deltas: Vec<deltas::Delta> = serde_json::from_str(&raw_deltas)?;
|
||||
assert_eq!(deltas.len(), 1);
|
||||
assert_eq!(deltas[0].operations.len(), 1);
|
||||
assert_eq!(
|
||||
deltas[0].operations[0],
|
||||
deltas::Operation::Insert((4, "2".to_string())),
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(gb_repo.wd_path().join(file_path))?,
|
||||
"test2"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_new_file() -> Result<()> {
|
||||
let repository = test_repository()?;
|
||||
let project = test_project(&repository)?;
|
||||
let project_repo = project_repository::Repository::open(&project)?;
|
||||
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
||||
let gb_repo = gb_repository::Repository::open(gb_repo_path, &project)?;
|
||||
let listener = Listener::new(&project, &project_repo, &gb_repo);
|
||||
|
||||
let file_path = std::path::Path::new("test.txt");
|
||||
std::fs::write(project_repo.root().join(file_path), "test")?;
|
||||
|
||||
listener.register(file_path)?;
|
||||
|
||||
let raw_deltas = std::fs::read_to_string(gb_repo.deltas_path().join(file_path))?;
|
||||
let deltas: Vec<deltas::Delta> = serde_json::from_str(&raw_deltas)?;
|
||||
assert_eq!(deltas.len(), 1);
|
||||
assert_eq!(deltas[0].operations.len(), 1);
|
||||
assert_eq!(
|
||||
deltas[0].operations[0],
|
||||
deltas::Operation::Insert((0, "test".to_string())),
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(gb_repo.wd_path().join(file_path))?,
|
||||
"test"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_new_file_twice() -> Result<()> {
|
||||
let repository = test_repository()?;
|
||||
let project = test_project(&repository)?;
|
||||
let project_repo = project_repository::Repository::open(&project)?;
|
||||
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
||||
let gb_repo = gb_repository::Repository::open(gb_repo_path, &project)?;
|
||||
let listener = Listener::new(&project, &project_repo, &gb_repo);
|
||||
|
||||
let file_path = std::path::Path::new("test.txt");
|
||||
std::fs::write(project_repo.root().join(file_path), "test")?;
|
||||
|
||||
listener.register(file_path)?;
|
||||
|
||||
std::fs::write(project_repo.root().join(file_path), "test2")?;
|
||||
listener.register(file_path)?;
|
||||
|
||||
let raw_deltas = std::fs::read_to_string(gb_repo.deltas_path().join(file_path))?;
|
||||
let deltas: Vec<deltas::Delta> = serde_json::from_str(&raw_deltas)?;
|
||||
assert_eq!(deltas.len(), 2);
|
||||
assert_eq!(deltas[0].operations.len(), 1);
|
||||
assert_eq!(
|
||||
deltas[0].operations[0],
|
||||
deltas::Operation::Insert((0, "test".to_string())),
|
||||
);
|
||||
assert_eq!(deltas[1].operations.len(), 1);
|
||||
assert_eq!(
|
||||
deltas[1].operations[0],
|
||||
deltas::Operation::Insert((4, "2".to_string())),
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(gb_repo.wd_path().join(file_path))?,
|
||||
"test2"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
4
src-tauri/src/app/listeners/mod.rs
Normal file
4
src-tauri/src/app/listeners/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod file_change;
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_change_tests;
|
@ -1,7 +1,13 @@
|
||||
mod gb_repository;
|
||||
mod dispatchers;
|
||||
pub mod gb_repository;
|
||||
mod listeners;
|
||||
mod project_repository;
|
||||
pub mod reader;
|
||||
pub mod session;
|
||||
mod session;
|
||||
mod watcher;
|
||||
mod writer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod reader_tests;
|
||||
|
||||
pub struct App {}
|
||||
|
36
src-tauri/src/app/project_repository.rs
Normal file
36
src-tauri/src/app/project_repository.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use super::reader;
|
||||
use crate::projects;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub struct Repository {
|
||||
git_repository: git2::Repository,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn open(project: &projects::Project) -> Result<Self> {
|
||||
let git_repository = git2::Repository::open(&project.path)
|
||||
.with_context(|| format!("{}: failed to open git repository", project.path))?;
|
||||
Ok(Self { git_repository })
|
||||
}
|
||||
|
||||
pub fn is_path_ignored<P: AsRef<std::path::Path>>(&self, path: P) -> Result<bool> {
|
||||
let path = path.as_ref();
|
||||
let ignored = self.git_repository.is_path_ignored(path)?;
|
||||
Ok(ignored)
|
||||
}
|
||||
|
||||
pub fn get_wd_reader(&self) -> reader::DirReader {
|
||||
reader::DirReader::open(self.root())
|
||||
}
|
||||
|
||||
pub fn get_head_reader(&self) -> Result<reader::CommitReader> {
|
||||
let head = self.git_repository.head()?;
|
||||
let commit = head.peel_to_commit()?;
|
||||
let reader = reader::CommitReader::from_commit(&self.git_repository, commit)?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &std::path::Path {
|
||||
self.git_repository.path().parent().unwrap()
|
||||
}
|
||||
}
|
@ -1,43 +1,81 @@
|
||||
use crate::fs;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::fs;
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Content {
|
||||
UTF8(String),
|
||||
Binary(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("file not found")]
|
||||
NotFound,
|
||||
#[error("io error: {0}")]
|
||||
Other(std::io::Error),
|
||||
}
|
||||
|
||||
pub trait Reader {
|
||||
fn read_to_string(&self, file_path: &str) -> Result<String>;
|
||||
fn read(&self, file_path: &str) -> Result<Content, Error>;
|
||||
fn list_files(&self, dir_path: &str) -> Result<Vec<String>>;
|
||||
}
|
||||
fn exists(&self, file_path: &str) -> bool;
|
||||
fn size(&self, file_path: &str) -> Result<usize>;
|
||||
|
||||
pub struct WdReader<'reader> {
|
||||
git_repository: &'reader git2::Repository,
|
||||
fn read_to_string(&self, file_path: &str) -> Result<String, Error> {
|
||||
match self.read(file_path)? {
|
||||
Content::UTF8(s) => Ok(s),
|
||||
Content::Binary(_) => Err(Error::Other(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"file is not utf8",
|
||||
))),
|
||||
}
|
||||
|
||||
impl WdReader<'_> {
|
||||
pub fn read_to_string(&self, path: &str) -> Result<String> {
|
||||
let contents =
|
||||
std::fs::read_to_string(self.git_repository.path().parent().unwrap().join(path))
|
||||
.with_context(|| format!("{}: not found", path))?;
|
||||
Ok(contents)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader for WdReader<'_> {
|
||||
fn read_to_string(&self, path: &str) -> Result<String> {
|
||||
self.read_to_string(path)
|
||||
pub struct DirReader<'reader> {
|
||||
root: &'reader std::path::Path,
|
||||
}
|
||||
|
||||
impl<'reader> DirReader<'reader> {
|
||||
pub fn open(root: &'reader std::path::Path) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader for DirReader<'_> {
|
||||
fn size(&self, file_path: &str) -> Result<usize> {
|
||||
let path = self.root.join(file_path);
|
||||
if !path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
Ok(metadata.len().try_into()?)
|
||||
}
|
||||
|
||||
fn read(&self, path: &str) -> Result<Content, Error> {
|
||||
let path = self.root.join(path);
|
||||
if !path.exists() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let content = std::fs::read(path).map_err(Error::Other)?;
|
||||
match String::from_utf8_lossy(&content).into_owned() {
|
||||
s if s.as_bytes().eq(&content) => Ok(Content::UTF8(s)),
|
||||
_ => Ok(Content::Binary(content)),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_files(&self, dir_path: &str) -> Result<Vec<String>> {
|
||||
let files: Vec<String> =
|
||||
fs::list_files(self.git_repository.path().parent().unwrap().join(dir_path))?
|
||||
let files: Vec<String> = fs::list_files(self.root.join(dir_path))?
|
||||
.iter()
|
||||
.map(|f| f.to_str().unwrap().to_string())
|
||||
.filter(|f| !f.starts_with(".git"))
|
||||
.collect();
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_working_directory_reader(git_repository: &git2::Repository) -> WdReader {
|
||||
WdReader { git_repository }
|
||||
fn exists(&self, file_path: &str) -> bool {
|
||||
std::path::Path::new(self.root.join(file_path).as_path()).exists()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommitReader<'reader> {
|
||||
@ -46,24 +84,71 @@ pub struct CommitReader<'reader> {
|
||||
tree: git2::Tree<'reader>,
|
||||
}
|
||||
|
||||
impl CommitReader<'_> {
|
||||
impl<'reader> CommitReader<'reader> {
|
||||
pub fn from_commit(
|
||||
repository: &'reader git2::Repository,
|
||||
commit: git2::Commit<'reader>,
|
||||
) -> Result<CommitReader<'reader>> {
|
||||
let tree = commit
|
||||
.tree()
|
||||
.with_context(|| format!("{}: tree not found", commit.id()))?;
|
||||
Ok(CommitReader {
|
||||
repository,
|
||||
tree,
|
||||
commit_oid: commit.id(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
repository: &'reader git2::Repository,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<CommitReader<'reader>> {
|
||||
let commit = repository
|
||||
.find_commit(commit_oid)
|
||||
.with_context(|| format!("{}: commit not found", commit_oid))?;
|
||||
return CommitReader::from_commit(repository, commit);
|
||||
}
|
||||
|
||||
pub fn get_commit_oid(&self) -> git2::Oid {
|
||||
self.commit_oid
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader for CommitReader<'_> {
|
||||
fn read_to_string(&self, path: &str) -> Result<String> {
|
||||
let entry = self
|
||||
fn size(&self, file_path: &str) -> Result<usize> {
|
||||
let entry = match self
|
||||
.tree
|
||||
.get_path(std::path::Path::new(file_path))
|
||||
.with_context(|| format!("{}: tree entry not found", file_path))
|
||||
{
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return Ok(0),
|
||||
};
|
||||
let blob = match self.repository.find_blob(entry.id()) {
|
||||
Ok(blob) => blob,
|
||||
Err(_) => return Ok(0),
|
||||
};
|
||||
Ok(blob.size())
|
||||
}
|
||||
|
||||
fn read(&self, path: &str) -> Result<Content, Error> {
|
||||
let entry = match self
|
||||
.tree
|
||||
.get_path(std::path::Path::new(path))
|
||||
.with_context(|| format!("{}: tree entry not found", path))?;
|
||||
let blob = self
|
||||
.repository
|
||||
.find_blob(entry.id())
|
||||
.with_context(|| format!("{}: blob not found", entry.id()))?;
|
||||
let contents = String::from_utf8_lossy(blob.content()).to_string();
|
||||
Ok(contents)
|
||||
.with_context(|| format!("{}: tree entry not found", path))
|
||||
{
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return Err(Error::NotFound),
|
||||
};
|
||||
let blob = match self.repository.find_blob(entry.id()) {
|
||||
Ok(blob) => blob,
|
||||
Err(_) => return Err(Error::NotFound),
|
||||
};
|
||||
let content = blob.content();
|
||||
match String::from_utf8_lossy(&content).into_owned() {
|
||||
s if s.as_bytes().eq(content) => Ok(Content::UTF8(s)),
|
||||
_ => Ok(Content::Binary(content.to_vec())),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_files(&self, dir_path: &str) -> Result<Vec<String>> {
|
||||
@ -97,21 +182,8 @@ impl Reader for CommitReader<'_> {
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_commit_reader<'reader>(
|
||||
repository: &'reader git2::Repository,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<CommitReader<'reader>> {
|
||||
let commit = repository
|
||||
.find_commit(commit_oid)
|
||||
.with_context(|| format!("{}: commit not found", commit_oid))?;
|
||||
let tree = commit
|
||||
.tree()
|
||||
.with_context(|| format!("{}: tree not found", commit_oid))?;
|
||||
Ok(CommitReader {
|
||||
repository,
|
||||
tree,
|
||||
commit_oid,
|
||||
})
|
||||
fn exists(&self, file_path: &str) -> bool {
|
||||
self.tree.get_path(std::path::Path::new(file_path)).is_ok()
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::reader::Reader;
|
||||
use super::reader::{self, Reader};
|
||||
use anyhow::Result;
|
||||
use tempfile::tempdir;
|
||||
|
||||
@ -43,8 +43,11 @@ fn test_working_directory_reader_read_file() -> Result<()> {
|
||||
let file_path = "test.txt";
|
||||
std::fs::write(&repository.path().parent().unwrap().join(file_path), "test")?;
|
||||
|
||||
let reader = super::reader::get_working_directory_reader(&repository);
|
||||
assert_eq!(reader.read_to_string(&file_path)?, "test");
|
||||
let reader = reader::DirReader::open(&repository.path().parent().unwrap());
|
||||
assert_eq!(
|
||||
reader.read(&file_path)?,
|
||||
reader::Content::UTF8("test".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -63,8 +66,11 @@ fn test_commit_reader_read_file() -> Result<()> {
|
||||
"test2",
|
||||
)?;
|
||||
|
||||
let reader = super::reader::get_commit_reader(&repository, oid)?;
|
||||
assert_eq!(reader.read_to_string(&file_path)?, "test");
|
||||
let reader = reader::CommitReader::open(&repository, oid)?;
|
||||
assert_eq!(
|
||||
reader.read(&file_path)?,
|
||||
reader::Content::UTF8("test".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -88,7 +94,7 @@ fn test_working_directory_reader_list_files() -> Result<()> {
|
||||
"test",
|
||||
)?;
|
||||
|
||||
let reader = super::reader::get_working_directory_reader(&repository);
|
||||
let reader = super::reader::DirReader::open(&repository.path().parent().unwrap());
|
||||
let files = reader.list_files(".")?;
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(files.contains(&"test.txt".to_string()));
|
||||
@ -120,7 +126,7 @@ fn test_commit_reader_list_files() -> Result<()> {
|
||||
|
||||
std::fs::remove_dir_all(&repository.path().parent().unwrap().join("dir"))?;
|
||||
|
||||
let reader = super::reader::get_commit_reader(&repository, oid)?;
|
||||
let reader = super::reader::CommitReader::open(&repository, oid)?;
|
||||
let files = reader.list_files(".")?;
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(files.contains(&"test.txt".to_string()));
|
||||
@ -128,3 +134,39 @@ fn test_commit_reader_list_files() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_working_directory_reader_exists() -> Result<()> {
|
||||
let repository = test_repository()?;
|
||||
|
||||
std::fs::write(
|
||||
&repository.path().parent().unwrap().join("test.txt"),
|
||||
"test",
|
||||
)?;
|
||||
|
||||
let reader = super::reader::DirReader::open(&repository.path().parent().unwrap());
|
||||
assert!(reader.exists("test.txt"));
|
||||
assert!(!reader.exists("test2.txt"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_reader_exists() -> Result<()> {
|
||||
let repository = test_repository()?;
|
||||
|
||||
std::fs::write(
|
||||
&repository.path().parent().unwrap().join("test.txt"),
|
||||
"test",
|
||||
)?;
|
||||
|
||||
let oid = commit(&repository)?;
|
||||
|
||||
std::fs::remove_file(&repository.path().parent().unwrap().join("test.txt"))?;
|
||||
|
||||
let reader = super::reader::CommitReader::open(&repository, oid)?;
|
||||
assert!(reader.exists("test.txt"));
|
||||
assert!(!reader.exists("test2.txt"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{
|
||||
gb_repository as repository, reader,
|
||||
gb_repository as repository,
|
||||
reader::{self, Reader},
|
||||
writer::{self, Writer},
|
||||
};
|
||||
use crate::{deltas, pty, sessions};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct SessionWriter<'writer> {
|
||||
repository: &'writer repository::Repository,
|
||||
@ -17,7 +17,7 @@ impl<'writer> SessionWriter<'writer> {
|
||||
repository: &'writer repository::Repository,
|
||||
session: &'writer sessions::Session,
|
||||
) -> Result<Self> {
|
||||
let reader = reader::get_working_directory_reader(&repository.git_repository);
|
||||
let reader = reader::DirReader::open(repository.root());
|
||||
|
||||
let current_session_id = reader.read_to_string(
|
||||
repository
|
||||
@ -37,7 +37,7 @@ impl<'writer> SessionWriter<'writer> {
|
||||
));
|
||||
}
|
||||
|
||||
let writer = writer::get_working_directory_writer(&repository.git_repository);
|
||||
let writer = writer::DirWriter::open(repository.root());
|
||||
|
||||
writer
|
||||
.write_string(
|
||||
@ -200,7 +200,7 @@ impl<'reader> SessionReader<'reader> {
|
||||
repository: &'reader repository::Repository,
|
||||
session: sessions::Session,
|
||||
) -> Result<Self> {
|
||||
let wd_reader = reader::get_working_directory_reader(&repository.git_repository);
|
||||
let wd_reader = reader::DirReader::open(repository.root());
|
||||
|
||||
let current_session_id = wd_reader.read_to_string(
|
||||
&repository
|
||||
@ -229,7 +229,7 @@ impl<'reader> SessionReader<'reader> {
|
||||
let oid = git2::Oid::from_str(&session_hash)
|
||||
.with_context(|| format!("failed to parse commit hash {}", session_hash))?;
|
||||
|
||||
let commit_reader = reader::get_commit_reader(&repository.git_repository, oid)?;
|
||||
let commit_reader = reader::CommitReader::open(&repository.git_repository, oid)?;
|
||||
Ok(SessionReader {
|
||||
reader: Box::new(commit_reader),
|
||||
repository,
|
||||
|
85
src-tauri/src/app/watcher.rs
Normal file
85
src-tauri/src/app/watcher.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use super::gb_repository;
|
||||
use crate::{
|
||||
app::{
|
||||
dispatchers::{file_change, tick},
|
||||
reader,
|
||||
},
|
||||
git, projects,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use core::time;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync;
|
||||
|
||||
pub struct Watcher<'watcher> {
|
||||
project: &'watcher projects::Project,
|
||||
gb_repository: &'watcher gb_repository::Repository,
|
||||
|
||||
ticker: tick::Dispatcher,
|
||||
file_watcher: file_change::Dispatcher,
|
||||
|
||||
stop: tokio_util::sync::CancellationToken,
|
||||
}
|
||||
|
||||
impl<'watcher> Watcher<'watcher> {
|
||||
pub fn new(
|
||||
gb_repository: &'watcher gb_repository::Repository,
|
||||
project: &'watcher projects::Project
|
||||
) -> Self {
|
||||
Self {
|
||||
gb_repository,
|
||||
project,
|
||||
ticker: tick::Dispatcher::new(project),
|
||||
file_watcher: file_change::Dispatcher::new(project),
|
||||
stop: tokio_util::sync::CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> anyhow::Result<()> {
|
||||
self.stop.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
let (t_tx, mut t_rx) = sync::mpsc::channel(128);
|
||||
let ticker = self.ticker.clone();
|
||||
let project_id = self.project.id.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = ticker.start(time::Duration::from_secs(10), t_tx).await {
|
||||
log::error!("{}: failed to start ticker: {:#}", project_id, e);
|
||||
}
|
||||
});
|
||||
|
||||
let (fw_tx, mut fw_rx) = sync::mpsc::channel(128);
|
||||
let file_watcher = self.file_watcher.clone();
|
||||
let project_id = self.project.id.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = file_watcher.start(fw_tx).await {
|
||||
log::error!("{}: failed to start file watcher: {:#}", project_id, e);
|
||||
}
|
||||
});
|
||||
|
||||
let reader = reader::DirReader::open(&std::path::Path::new(&self.project.path));
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(ts) = t_rx.recv() => {
|
||||
log::info!("{}: ticker ticked: {}", self.project.id, ts.elapsed().as_secs());
|
||||
}
|
||||
Some(path) = fw_rx.recv() => {
|
||||
log::info!("{}: file changed: {}", self.project.id, path.display());
|
||||
},
|
||||
_ = self.stop.cancelled() => {
|
||||
if let Err(e) = self.ticker.stop() {
|
||||
log::error!("{}: failed to stop ticker: {:#}", self.project.id, e);
|
||||
}
|
||||
if let Err(e) = self.file_watcher.stop() {
|
||||
log::error!("{}: failed to stop file watcher: {:#}", self.project.id, e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -6,19 +6,19 @@ pub trait Writer {
|
||||
fn append_string(&self, path: &str, contents: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct WdWriter<'writer> {
|
||||
git_repository: &'writer git2::Repository,
|
||||
pub struct DirWriter<'writer> {
|
||||
root: &'writer std::path::Path,
|
||||
}
|
||||
|
||||
pub fn get_working_directory_writer<'writer>(
|
||||
git_repository: &'writer git2::Repository,
|
||||
) -> WdWriter {
|
||||
WdWriter { git_repository }
|
||||
impl<'writer> DirWriter<'writer> {
|
||||
pub fn open(root: &'writer std::path::Path) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer for WdWriter<'_> {
|
||||
impl Writer for DirWriter<'_> {
|
||||
fn write_string(&self, path: &str, contents: &str) -> Result<()> {
|
||||
let file_path = self.git_repository.path().parent().unwrap().join(path);
|
||||
let file_path = self.root.join(path);
|
||||
let dir_path = file_path.parent().unwrap();
|
||||
std::fs::create_dir_all(dir_path)?;
|
||||
std::fs::write(path, contents)?;
|
||||
@ -26,7 +26,7 @@ impl Writer for WdWriter<'_> {
|
||||
}
|
||||
|
||||
fn append_string(&self, path: &str, contents: &str) -> Result<()> {
|
||||
let file_path = self.git_repository.path().parent().unwrap().join(path);
|
||||
let file_path = self.root.join(path);
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
|
@ -718,6 +718,13 @@ fn init(app_handle: tauri::AppHandle) -> Result<()> {
|
||||
.watch(tx.clone(), &repo)
|
||||
.with_context(|| format!("{}: failed to watch project", project.id))?;
|
||||
|
||||
// let p = project.clone();
|
||||
// tauri::async_runtime::spawn(async move {
|
||||
// let p = p;
|
||||
// let w = app::watchers::Watcher::new(&p);
|
||||
// w.start().await.expect("failed to start watcher");
|
||||
// });
|
||||
|
||||
if let Err(err) = app_state
|
||||
.deltas_searcher
|
||||
.lock()
|
||||
|
@ -1,7 +1,7 @@
|
||||
mod sessions;
|
||||
mod storage;
|
||||
|
||||
pub use sessions::{id_from_commit, Meta, Session};
|
||||
pub use sessions::{id_from_commit, Meta, Session, SessionError};
|
||||
pub use storage::Store;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -2,6 +2,7 @@ use crate::{app::reader, git::activity};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -27,23 +28,40 @@ pub struct Session {
|
||||
pub activity: Vec<activity::Activity>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SessionError {
|
||||
#[error("session does not exist")]
|
||||
NoSession,
|
||||
#[error("{0}")]
|
||||
Err(anyhow::Error),
|
||||
}
|
||||
|
||||
impl<'reader> TryFrom<Box<dyn reader::Reader + 'reader>> for Session {
|
||||
type Error = anyhow::Error;
|
||||
type Error = SessionError;
|
||||
|
||||
fn try_from(reader: Box<dyn reader::Reader + 'reader>) -> Result<Self, Self::Error> {
|
||||
if !reader.exists("session") {
|
||||
return Err(SessionError::NoSession);
|
||||
}
|
||||
|
||||
let id = reader
|
||||
.read_to_string("session/meta/id")
|
||||
.with_context(|| "failed to read session id")?;
|
||||
.with_context(|| "failed to read session id")
|
||||
.map_err(|err| SessionError::Err(err))?;
|
||||
let start_timestamp_ms = reader
|
||||
.read_to_string("session/meta/start")
|
||||
.with_context(|| "failed to read session start timestamp")?
|
||||
.with_context(|| "failed to read session start timestamp")
|
||||
.map_err(|err| SessionError::Err(err))?
|
||||
.parse::<u128>()
|
||||
.with_context(|| "failed to parse session start timestamp")?;
|
||||
.with_context(|| "failed to parse session start timestamp")
|
||||
.map_err(|err| SessionError::Err(err))?;
|
||||
let last_timestamp_ms = reader
|
||||
.read_to_string("session/meta/last")
|
||||
.with_context(|| "failed to read session last timestamp")?
|
||||
.with_context(|| "failed to read session last timestamp")
|
||||
.map_err(|err| SessionError::Err(err))?
|
||||
.parse::<u128>()
|
||||
.with_context(|| "failed to parse session last timestamp")?;
|
||||
.with_context(|| "failed to parse session last timestamp")
|
||||
.map_err(|err| SessionError::Err(err))?;
|
||||
let branch = reader.read_to_string("session/meta/branch");
|
||||
let commit = reader.read_to_string("session/meta/commit");
|
||||
|
||||
@ -69,8 +87,17 @@ impl<'reader> TryFrom<Box<dyn reader::Reader + 'reader>> for Session {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'reader> TryFrom<reader::DirReader<'reader>> for Session {
|
||||
type Error = SessionError;
|
||||
|
||||
fn try_from(reader: reader::DirReader<'reader>) -> Result<Self, Self::Error> {
|
||||
let session = Session::try_from(Box::new(reader) as Box<dyn reader::Reader + 'reader>)?;
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'reader> TryFrom<reader::CommitReader<'reader>> for Session {
|
||||
type Error = anyhow::Error;
|
||||
type Error = SessionError;
|
||||
|
||||
fn try_from(reader: reader::CommitReader<'reader>) -> Result<Self, Self::Error> {
|
||||
let commit_oid = reader.get_commit_oid().to_string();
|
||||
|
@ -227,10 +227,8 @@ fn test_get_persistent() -> Result<()> {
|
||||
|
||||
let commit_oid = git2::Oid::from_str(&created_session.hash.as_ref().unwrap())?;
|
||||
|
||||
let reader = reader::get_commit_reader(&repo, commit_oid)?;
|
||||
let reconstructed = super::sessions::Session::try_from(reader);
|
||||
assert!(reconstructed.is_ok());
|
||||
let reconstructed = reconstructed.unwrap();
|
||||
let reader = reader::CommitReader::open(&repo, commit_oid)?;
|
||||
let reconstructed = super::sessions::Session::try_from(reader)?;
|
||||
|
||||
assert_eq!(reconstructed, created_session);
|
||||
|
||||
|
@ -57,7 +57,7 @@ impl Store {
|
||||
walker.set_sorting(git2::Sort::TIME)?;
|
||||
|
||||
for commit_id in walker {
|
||||
let reader = reader::get_commit_reader(&git_repository, commit_id?)?;
|
||||
let reader = reader::CommitReader::open(&git_repository, commit_id?)?;
|
||||
if reader.read_to_string("session/meta/id")? == session_id {
|
||||
return Ok(Some(sessions::Session::try_from(reader)?));
|
||||
}
|
||||
@ -206,7 +206,7 @@ impl Store {
|
||||
let mut sessions: Vec<sessions::Session> = vec![];
|
||||
for id in walker {
|
||||
let id = id?;
|
||||
let reader = reader::get_commit_reader(&git_repository, id)?;
|
||||
let reader = reader::CommitReader::open(&git_repository, id)?;
|
||||
let session = sessions::Session::try_from(reader)?;
|
||||
sessions.push(session);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user