mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-26 02:51:57 +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",
|
"timed",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
|
"tokio-util",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
@ -46,6 +46,7 @@ futures = "0.3"
|
|||||||
futures-util = "0.3.8"
|
futures-util = "0.3.8"
|
||||||
timed = "0.2.1"
|
timed = "0.2.1"
|
||||||
serde-jsonlines = "0.4.0"
|
serde-jsonlines = "0.4.0"
|
||||||
|
tokio-util = "0.7.7"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# 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 crate::{projects, sessions};
|
||||||
use anyhow::{anyhow, Context, Ok, Result};
|
use anyhow::{anyhow, Context, Ok, Result};
|
||||||
|
|
||||||
@ -7,9 +8,37 @@ pub struct Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
pub fn open(project: &projects::Project) -> Result<Self> {
|
pub fn open<P: AsRef<std::path::Path>>(root: P, project: &projects::Project) -> Result<Self> {
|
||||||
let git_repository = git2::Repository::open(&project.path)
|
let root = root.as_ref();
|
||||||
.with_context(|| format!("{}: failed to open git repository", project.path))?;
|
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 {
|
Ok(Self {
|
||||||
project_id: project.id.clone(),
|
project_id: project.id.clone(),
|
||||||
git_repository,
|
git_repository,
|
||||||
@ -20,6 +49,34 @@ impl Repository {
|
|||||||
Err(anyhow!("TODO"))
|
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 {
|
pub(crate) fn session_path(&self) -> std::path::PathBuf {
|
||||||
self.git_repository.path().parent().unwrap().join("session")
|
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 reader;
|
||||||
pub mod session;
|
mod session;
|
||||||
|
mod watcher;
|
||||||
mod writer;
|
mod writer;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod reader_tests;
|
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 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 {
|
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 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> {
|
fn read_to_string(&self, file_path: &str) -> Result<String, Error> {
|
||||||
git_repository: &'reader git2::Repository,
|
match self.read(file_path)? {
|
||||||
}
|
Content::UTF8(s) => Ok(s),
|
||||||
|
Content::Binary(_) => Err(Error::Other(std::io::Error::new(
|
||||||
impl WdReader<'_> {
|
std::io::ErrorKind::InvalidData,
|
||||||
pub fn read_to_string(&self, path: &str) -> Result<String> {
|
"file is not utf8",
|
||||||
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<'_> {
|
pub struct DirReader<'reader> {
|
||||||
fn read_to_string(&self, path: &str) -> Result<String> {
|
root: &'reader std::path::Path,
|
||||||
self.read_to_string(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>> {
|
fn list_files(&self, dir_path: &str) -> Result<Vec<String>> {
|
||||||
let files: Vec<String> =
|
let files: Vec<String> = fs::list_files(self.root.join(dir_path))?
|
||||||
fs::list_files(self.git_repository.path().parent().unwrap().join(dir_path))?
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| f.to_str().unwrap().to_string())
|
.map(|f| f.to_str().unwrap().to_string())
|
||||||
.filter(|f| !f.starts_with(".git"))
|
.filter(|f| !f.starts_with(".git"))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_working_directory_reader(git_repository: &git2::Repository) -> WdReader {
|
fn exists(&self, file_path: &str) -> bool {
|
||||||
WdReader { git_repository }
|
std::path::Path::new(self.root.join(file_path).as_path()).exists()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommitReader<'reader> {
|
pub struct CommitReader<'reader> {
|
||||||
@ -46,24 +84,71 @@ pub struct CommitReader<'reader> {
|
|||||||
tree: git2::Tree<'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 {
|
pub fn get_commit_oid(&self) -> git2::Oid {
|
||||||
self.commit_oid
|
self.commit_oid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reader for CommitReader<'_> {
|
impl Reader for CommitReader<'_> {
|
||||||
fn read_to_string(&self, path: &str) -> Result<String> {
|
fn size(&self, file_path: &str) -> Result<usize> {
|
||||||
let entry = self
|
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
|
.tree
|
||||||
.get_path(std::path::Path::new(path))
|
.get_path(std::path::Path::new(path))
|
||||||
.with_context(|| format!("{}: tree entry not found", path))?;
|
.with_context(|| format!("{}: tree entry not found", path))
|
||||||
let blob = self
|
{
|
||||||
.repository
|
Ok(entry) => entry,
|
||||||
.find_blob(entry.id())
|
Err(_) => return Err(Error::NotFound),
|
||||||
.with_context(|| format!("{}: blob not found", entry.id()))?;
|
};
|
||||||
let contents = String::from_utf8_lossy(blob.content()).to_string();
|
let blob = match self.repository.find_blob(entry.id()) {
|
||||||
Ok(contents)
|
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>> {
|
fn list_files(&self, dir_path: &str) -> Result<Vec<String>> {
|
||||||
@ -97,21 +182,8 @@ impl Reader for CommitReader<'_> {
|
|||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_commit_reader<'reader>(
|
fn exists(&self, file_path: &str) -> bool {
|
||||||
repository: &'reader git2::Repository,
|
self.tree.get_path(std::path::Path::new(file_path)).is_ok()
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::reader::Reader;
|
use super::reader::{self, Reader};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
@ -43,8 +43,11 @@ fn test_working_directory_reader_read_file() -> Result<()> {
|
|||||||
let file_path = "test.txt";
|
let file_path = "test.txt";
|
||||||
std::fs::write(&repository.path().parent().unwrap().join(file_path), "test")?;
|
std::fs::write(&repository.path().parent().unwrap().join(file_path), "test")?;
|
||||||
|
|
||||||
let reader = super::reader::get_working_directory_reader(&repository);
|
let reader = reader::DirReader::open(&repository.path().parent().unwrap());
|
||||||
assert_eq!(reader.read_to_string(&file_path)?, "test");
|
assert_eq!(
|
||||||
|
reader.read(&file_path)?,
|
||||||
|
reader::Content::UTF8("test".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -63,8 +66,11 @@ fn test_commit_reader_read_file() -> Result<()> {
|
|||||||
"test2",
|
"test2",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let reader = super::reader::get_commit_reader(&repository, oid)?;
|
let reader = reader::CommitReader::open(&repository, oid)?;
|
||||||
assert_eq!(reader.read_to_string(&file_path)?, "test");
|
assert_eq!(
|
||||||
|
reader.read(&file_path)?,
|
||||||
|
reader::Content::UTF8("test".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -88,7 +94,7 @@ fn test_working_directory_reader_list_files() -> Result<()> {
|
|||||||
"test",
|
"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(".")?;
|
let files = reader.list_files(".")?;
|
||||||
assert_eq!(files.len(), 2);
|
assert_eq!(files.len(), 2);
|
||||||
assert!(files.contains(&"test.txt".to_string()));
|
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"))?;
|
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(".")?;
|
let files = reader.list_files(".")?;
|
||||||
assert_eq!(files.len(), 2);
|
assert_eq!(files.len(), 2);
|
||||||
assert!(files.contains(&"test.txt".to_string()));
|
assert!(files.contains(&"test.txt".to_string()));
|
||||||
@ -128,3 +134,39 @@ fn test_commit_reader_list_files() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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::{
|
use super::{
|
||||||
gb_repository as repository, reader,
|
gb_repository as repository,
|
||||||
|
reader::{self, Reader},
|
||||||
writer::{self, Writer},
|
writer::{self, Writer},
|
||||||
};
|
};
|
||||||
use crate::{deltas, pty, sessions};
|
use crate::{deltas, pty, sessions};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct SessionWriter<'writer> {
|
pub struct SessionWriter<'writer> {
|
||||||
repository: &'writer repository::Repository,
|
repository: &'writer repository::Repository,
|
||||||
@ -17,7 +17,7 @@ impl<'writer> SessionWriter<'writer> {
|
|||||||
repository: &'writer repository::Repository,
|
repository: &'writer repository::Repository,
|
||||||
session: &'writer sessions::Session,
|
session: &'writer sessions::Session,
|
||||||
) -> Result<Self> {
|
) -> 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(
|
let current_session_id = reader.read_to_string(
|
||||||
repository
|
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
|
writer
|
||||||
.write_string(
|
.write_string(
|
||||||
@ -200,7 +200,7 @@ impl<'reader> SessionReader<'reader> {
|
|||||||
repository: &'reader repository::Repository,
|
repository: &'reader repository::Repository,
|
||||||
session: sessions::Session,
|
session: sessions::Session,
|
||||||
) -> Result<Self> {
|
) -> 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(
|
let current_session_id = wd_reader.read_to_string(
|
||||||
&repository
|
&repository
|
||||||
@ -229,7 +229,7 @@ impl<'reader> SessionReader<'reader> {
|
|||||||
let oid = git2::Oid::from_str(&session_hash)
|
let oid = git2::Oid::from_str(&session_hash)
|
||||||
.with_context(|| format!("failed to parse commit hash {}", 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 {
|
Ok(SessionReader {
|
||||||
reader: Box::new(commit_reader),
|
reader: Box::new(commit_reader),
|
||||||
repository,
|
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<()>;
|
fn append_string(&self, path: &str, contents: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WdWriter<'writer> {
|
pub struct DirWriter<'writer> {
|
||||||
git_repository: &'writer git2::Repository,
|
root: &'writer std::path::Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_working_directory_writer<'writer>(
|
impl<'writer> DirWriter<'writer> {
|
||||||
git_repository: &'writer git2::Repository,
|
pub fn open(root: &'writer std::path::Path) -> Self {
|
||||||
) -> WdWriter {
|
Self { root }
|
||||||
WdWriter { git_repository }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writer for WdWriter<'_> {
|
impl Writer for DirWriter<'_> {
|
||||||
fn write_string(&self, path: &str, contents: &str) -> Result<()> {
|
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();
|
let dir_path = file_path.parent().unwrap();
|
||||||
std::fs::create_dir_all(dir_path)?;
|
std::fs::create_dir_all(dir_path)?;
|
||||||
std::fs::write(path, contents)?;
|
std::fs::write(path, contents)?;
|
||||||
@ -26,7 +26,7 @@ impl Writer for WdWriter<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn append_string(&self, path: &str, contents: &str) -> Result<()> {
|
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()
|
let mut file = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
|
@ -718,6 +718,13 @@ fn init(app_handle: tauri::AppHandle) -> Result<()> {
|
|||||||
.watch(tx.clone(), &repo)
|
.watch(tx.clone(), &repo)
|
||||||
.with_context(|| format!("{}: failed to watch project", project.id))?;
|
.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
|
if let Err(err) = app_state
|
||||||
.deltas_searcher
|
.deltas_searcher
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
mod sessions;
|
mod sessions;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
|
||||||
pub use sessions::{id_from_commit, Meta, Session};
|
pub use sessions::{id_from_commit, Meta, Session, SessionError};
|
||||||
pub use storage::Store;
|
pub use storage::Store;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -2,6 +2,7 @@ use crate::{app::reader, git::activity};
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -27,23 +28,40 @@ pub struct Session {
|
|||||||
pub activity: Vec<activity::Activity>,
|
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 {
|
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> {
|
fn try_from(reader: Box<dyn reader::Reader + 'reader>) -> Result<Self, Self::Error> {
|
||||||
|
if !reader.exists("session") {
|
||||||
|
return Err(SessionError::NoSession);
|
||||||
|
}
|
||||||
|
|
||||||
let id = reader
|
let id = reader
|
||||||
.read_to_string("session/meta/id")
|
.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
|
let start_timestamp_ms = reader
|
||||||
.read_to_string("session/meta/start")
|
.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>()
|
.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
|
let last_timestamp_ms = reader
|
||||||
.read_to_string("session/meta/last")
|
.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>()
|
.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 branch = reader.read_to_string("session/meta/branch");
|
||||||
let commit = reader.read_to_string("session/meta/commit");
|
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 {
|
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> {
|
fn try_from(reader: reader::CommitReader<'reader>) -> Result<Self, Self::Error> {
|
||||||
let commit_oid = reader.get_commit_oid().to_string();
|
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 commit_oid = git2::Oid::from_str(&created_session.hash.as_ref().unwrap())?;
|
||||||
|
|
||||||
let reader = reader::get_commit_reader(&repo, commit_oid)?;
|
let reader = reader::CommitReader::open(&repo, commit_oid)?;
|
||||||
let reconstructed = super::sessions::Session::try_from(reader);
|
let reconstructed = super::sessions::Session::try_from(reader)?;
|
||||||
assert!(reconstructed.is_ok());
|
|
||||||
let reconstructed = reconstructed.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(reconstructed, created_session);
|
assert_eq!(reconstructed, created_session);
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ impl Store {
|
|||||||
walker.set_sorting(git2::Sort::TIME)?;
|
walker.set_sorting(git2::Sort::TIME)?;
|
||||||
|
|
||||||
for commit_id in walker {
|
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 {
|
if reader.read_to_string("session/meta/id")? == session_id {
|
||||||
return Ok(Some(sessions::Session::try_from(reader)?));
|
return Ok(Some(sessions::Session::try_from(reader)?));
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ impl Store {
|
|||||||
let mut sessions: Vec<sessions::Session> = vec![];
|
let mut sessions: Vec<sessions::Session> = vec![];
|
||||||
for id in walker {
|
for id in walker {
|
||||||
let id = id?;
|
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)?;
|
let session = sessions::Session::try_from(reader)?;
|
||||||
sessions.push(session);
|
sessions.push(session);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user