use super::Result;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use thiserror::Error as ThisError;
use uuid::{uuid, Uuid};
use super::git::git_commit_files;
use crate::editor::EditorCommunicator;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Failed to read file '{path}': {source}")]
FileRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Editor interface error: {message}: {source}")]
EditorInterface {
message: String,
#[source]
source: anyhow::Error,
},
#[error("Failed to write file '{path}': {source}")]
FileWrite {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Git error: {0}")]
Git(#[from] super::git::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Mutex poisoned")]
MutexPoisoned,
}
use std::fmt::Debug;
pub trait FileAccessor: Send + Sync + Debug {
fn read_file(&self, path: &Path) -> Result<String> {
Ok(std::fs::read_to_string(path).map_err(|e| Error::FileRead {
path: path.to_path_buf(),
source: e,
})?)
}
fn lock_file(&self, path: &Path) -> Result<Uuid>;
fn unlock_file(&self, uuid: &Uuid) -> Result<()>;
fn write_file(&self, path: &Path, content: &str, comment: Option<&str>) -> Result<()>;
}
#[derive(Debug)]
struct ProjectFileAccessorMutable {
modified_files: HashSet<PathBuf>,
modified_files_comments: Vec<String>,
}
#[derive(Debug)]
pub struct ProjectFileAccessor {
root_path: PathBuf,
editor_interface: Option<Arc<dyn EditorCommunicator>>,
mutable: Mutex<ProjectFileAccessorMutable>,
}
impl ProjectFileAccessor {
pub fn new(root_path: &Path, editor_interface: Option<Arc<dyn EditorCommunicator>>) -> Self {
ProjectFileAccessor {
root_path: root_path.to_path_buf(),
editor_interface,
mutable: Mutex::new(ProjectFileAccessorMutable {
modified_files: HashSet::new(),
modified_files_comments: Vec::new(),
}),
}
}
pub fn modified_files(&self) -> Result<Vec<PathBuf>> {
Ok(self
.mutable
.lock()
.map_err(|_| Error::MutexPoisoned)?
.modified_files
.iter()
.cloned()
.collect::<Vec<PathBuf>>())
}
pub fn modified_files_comments(&self) -> Result<String> {
Ok(self
.mutable
.lock()
.map_err(|_| Error::MutexPoisoned)?
.modified_files_comments
.join("\n"))
}
pub fn commit(&self, title_message: Option<String>) -> Result<()> {
let mut mutable = self.mutable.lock().map_err(|_| Error::MutexPoisoned)?;
if !mutable.modified_files.is_empty() {
let message_1 = match title_message {
Some(x) => format!("{}\n", x),
None => "".into(),
};
let message_2 = mutable.modified_files_comments.join("\n");
let _ = git_commit_files(
&self.root_path,
&mutable
.modified_files
.iter()
.cloned()
.collect::<Vec<PathBuf>>(),
&format!("{}{}", message_1, message_2),
)?;
}
mutable.modified_files.clear();
mutable.modified_files_comments.clear();
Ok(())
}
}
const DUMMY_ID: Uuid = uuid!("00000000-0000-0000-0000-000000000000");
impl FileAccessor for ProjectFileAccessor {
fn read_file(&self, path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path).map_err(|e| Error::FileRead {
path: path.to_path_buf(),
source: e,
})?;
Ok(content)
}
fn lock_file(&self, path: &Path) -> Result<Uuid> {
match &self.editor_interface {
None => Ok(DUMMY_ID),
Some(x) => Ok(x
.save_and_lock_file(path)
.map_err(|e| Error::EditorInterface {
message: "Failed to save and lock file".to_string(),
source: e,
})?),
}
}
fn unlock_file(&self, uuid: &Uuid) -> Result<()> {
match &self.editor_interface {
None => Ok(()),
Some(x) => Ok(x
.unlock_and_reload_file(*uuid)
.map_err(|e| Error::EditorInterface {
message: "Failed to unlock and reload file".to_string(),
source: e,
})?),
}
}
fn write_file(&self, path: &Path, content: &str, comment: Option<&str>) -> Result<()> {
tracing::debug!("Writing file {:?}", path);
std::fs::write(path, content).map_err(|e| Error::FileWrite {
path: path.to_path_buf(),
source: e,
})?;
let mut mutable = self.mutable.lock().unwrap();
mutable.modified_files.insert(path.into());
if let Some(comment) = comment {
mutable.modified_files_comments.push(comment.into());
}
Ok(())
}
}
pub struct FileLock {
file_access: Arc<dyn FileAccessor>,
lock_id: Option<Uuid>,
}
impl FileLock {
pub fn new(file_access: Arc<dyn FileAccessor>, path: &Path) -> Result<Self> {
let lock_id = file_access.lock_file(path)?;
Ok(Self {
file_access,
lock_id: Some(lock_id),
})
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(lock_id) = self.lock_id.take() {
if let Err(e) = self.file_access.unlock_file(&lock_id) {
tracing::error!("Failed to unlock file with id {}: {}", lock_id, e);
}
}
}
}