use crate::error::{ChangesetError, ChangesetResult};
use crate::types::{ArchiveResult, ArchivedChangeset, Changeset, ReleaseInfo};
use async_trait::async_trait;
#[async_trait]
pub trait ChangesetStorage: Send + Sync {
async fn save(&self, changeset: &Changeset) -> ChangesetResult<()>;
async fn load(&self, branch: &str) -> ChangesetResult<Changeset>;
async fn exists(&self, branch: &str) -> ChangesetResult<bool>;
async fn delete(&self, branch: &str) -> ChangesetResult<()>;
async fn list_pending(&self) -> ChangesetResult<Vec<Changeset>>;
async fn archive(
&self,
changeset: &Changeset,
release_info: ReleaseInfo,
) -> ChangesetResult<ArchiveResult>;
async fn load_archived(&self, branch: &str) -> ChangesetResult<ArchivedChangeset>;
async fn list_archived(&self) -> ChangesetResult<Vec<ArchivedChangeset>>;
}
pub struct FileBasedChangesetStorage<F>
where
F: sublime_standard_tools::filesystem::AsyncFileSystem,
{
root_path: std::path::PathBuf,
changeset_dir: String,
history_dir: String,
fs: F,
}
impl<F> FileBasedChangesetStorage<F>
where
F: sublime_standard_tools::filesystem::AsyncFileSystem,
{
pub fn new(
root_path: std::path::PathBuf,
changeset_dir: String,
history_dir: String,
fs: F,
) -> Self {
Self { root_path, changeset_dir, history_dir, fs }
}
fn changeset_path(&self, branch: &str) -> std::path::PathBuf {
let filename = Self::sanitize_branch_name(branch);
self.root_path.join(&self.changeset_dir).join(format!("{}.json", filename))
}
fn archive_path(&self, branch: &str) -> std::path::PathBuf {
let filename = Self::sanitize_branch_name(branch);
self.root_path.join(&self.history_dir).join(format!("{}.json", filename))
}
fn sanitize_branch_name(branch: &str) -> String {
branch
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
_ => c,
})
.collect()
}
#[must_use]
pub fn workspace_root(&self) -> &std::path::Path {
&self.root_path
}
}
#[async_trait]
impl<F> ChangesetStorage for FileBasedChangesetStorage<F>
where
F: sublime_standard_tools::filesystem::AsyncFileSystem,
{
async fn save(&self, changeset: &Changeset) -> ChangesetResult<()> {
let path = self.changeset_path(&changeset.branch);
if let Some(parent) = path.parent() {
self.fs.create_dir_all(parent).await.map_err(|e| ChangesetError::StorageError {
path: parent.to_path_buf(),
reason: format!("Failed to create changeset directory: {}", e),
})?;
}
let json = serde_json::to_string_pretty(changeset).map_err(|e| {
ChangesetError::SerializationError {
operation: "serialize".to_string(),
reason: format!("Failed to serialize changeset: {}", e),
}
})?;
self.fs.write_file_string(&path, &json).await.map_err(|e| {
ChangesetError::StorageError {
path: path.clone(),
reason: format!("Failed to write changeset file: {}", e),
}
})?;
Ok(())
}
async fn load(&self, branch: &str) -> ChangesetResult<Changeset> {
let path = self.changeset_path(branch);
let exists = self.fs.exists(&path).await;
if !exists {
return Err(ChangesetError::NotFound { branch: branch.to_string() });
}
let contents =
self.fs.read_file_string(&path).await.map_err(|e| ChangesetError::StorageError {
path: path.clone(),
reason: format!("Failed to read changeset file: {}", e),
})?;
let changeset =
serde_json::from_str(&contents).map_err(|e| ChangesetError::SerializationError {
operation: "deserialize".to_string(),
reason: format!("Failed to deserialize changeset: {}", e),
})?;
Ok(changeset)
}
async fn exists(&self, branch: &str) -> ChangesetResult<bool> {
let path = self.changeset_path(branch);
Ok(self.fs.exists(&path).await)
}
async fn delete(&self, branch: &str) -> ChangesetResult<()> {
let path = self.changeset_path(branch);
let exists = self.fs.exists(&path).await;
if !exists {
return Ok(());
}
self.fs.remove(&path).await.map_err(|e| ChangesetError::StorageError {
path: path.clone(),
reason: format!("Failed to delete changeset file: {}", e),
})?;
Ok(())
}
async fn list_pending(&self) -> ChangesetResult<Vec<Changeset>> {
let dir_path = self.root_path.join(&self.changeset_dir);
let exists = self.fs.exists(&dir_path).await;
if !exists {
return Ok(Vec::new());
}
let entries =
self.fs.read_dir(&dir_path).await.map_err(|e| ChangesetError::StorageError {
path: dir_path.clone(),
reason: format!("Failed to read changeset directory: {}", e),
})?;
let mut changesets = Vec::new();
for entry in entries {
if entry.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
if let Some(_filename) = entry.file_stem().and_then(|s| s.to_str()) {
match self.load_from_path(&entry).await {
Ok(changeset) => changesets.push(changeset),
Err(e) => {
log::warn!("Failed to load changeset from {:?}: {}", entry, e);
}
}
}
}
Ok(changesets)
}
async fn archive(
&self,
changeset: &Changeset,
release_info: ReleaseInfo,
) -> ChangesetResult<ArchiveResult> {
let pending_path = self.changeset_path(&changeset.branch);
let archive_path = self.archive_path(&changeset.branch);
let archive_exists = self.fs.exists(&archive_path).await;
if archive_exists {
return Err(ChangesetError::AlreadyExists {
branch: changeset.branch.clone(),
path: archive_path,
});
}
let archived = ArchivedChangeset::new(changeset.clone(), release_info);
if let Some(parent) = archive_path.parent() {
self.fs.create_dir_all(parent).await.map_err(|e| ChangesetError::StorageError {
path: parent.to_path_buf(),
reason: format!("Failed to create history directory: {}", e),
})?;
}
let json = serde_json::to_string_pretty(&archived).map_err(|e| {
ChangesetError::SerializationError {
operation: "serialize".to_string(),
reason: format!("Failed to serialize archived changeset: {}", e),
}
})?;
self.fs.write_file_string(&archive_path, &json).await.map_err(|e| {
ChangesetError::ArchiveError {
branch: changeset.branch.clone(),
reason: format!("Failed to write archived changeset: {}", e),
}
})?;
let exists = self.fs.exists(&pending_path).await;
if exists {
self.fs.remove(&pending_path).await.map_err(|e| ChangesetError::ArchiveError {
branch: changeset.branch.clone(),
reason: format!("Failed to delete pending changeset: {}", e),
})?;
}
Ok(ArchiveResult::new(pending_path, archive_path))
}
async fn load_archived(&self, branch: &str) -> ChangesetResult<ArchivedChangeset> {
let path = self.archive_path(branch);
let exists = self.fs.exists(&path).await;
if !exists {
return Err(ChangesetError::NotFound { branch: branch.to_string() });
}
let contents =
self.fs.read_file_string(&path).await.map_err(|e| ChangesetError::StorageError {
path: path.clone(),
reason: format!("Failed to read archived changeset file: {}", e),
})?;
let archived =
serde_json::from_str(&contents).map_err(|e| ChangesetError::SerializationError {
operation: "deserialize".to_string(),
reason: format!("Failed to deserialize archived changeset: {}", e),
})?;
Ok(archived)
}
async fn list_archived(&self) -> ChangesetResult<Vec<ArchivedChangeset>> {
let dir_path = self.root_path.join(&self.history_dir);
let exists = self.fs.exists(&dir_path).await;
if !exists {
return Ok(Vec::new());
}
let entries =
self.fs.read_dir(&dir_path).await.map_err(|e| ChangesetError::StorageError {
path: dir_path.clone(),
reason: format!("Failed to read history directory: {}", e),
})?;
let mut archived_changesets = Vec::new();
for entry in entries {
if entry.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
match self.load_archived_from_path(&entry).await {
Ok(archived) => archived_changesets.push(archived),
Err(e) => {
log::warn!("Failed to load archived changeset from {:?}: {}", entry, e);
}
}
}
Ok(archived_changesets)
}
}
impl<F> FileBasedChangesetStorage<F>
where
F: sublime_standard_tools::filesystem::AsyncFileSystem,
{
async fn load_from_path(&self, path: &std::path::Path) -> ChangesetResult<Changeset> {
let contents =
self.fs.read_file_string(path).await.map_err(|e| ChangesetError::StorageError {
path: path.to_path_buf(),
reason: format!("Failed to read changeset file: {}", e),
})?;
let changeset =
serde_json::from_str(&contents).map_err(|e| ChangesetError::SerializationError {
operation: "deserialize".to_string(),
reason: format!("Failed to deserialize changeset: {}", e),
})?;
Ok(changeset)
}
async fn load_archived_from_path(
&self,
path: &std::path::Path,
) -> ChangesetResult<ArchivedChangeset> {
let contents =
self.fs.read_file_string(path).await.map_err(|e| ChangesetError::StorageError {
path: path.to_path_buf(),
reason: format!("Failed to read archived changeset file: {}", e),
})?;
let archived =
serde_json::from_str(&contents).map_err(|e| ChangesetError::SerializationError {
operation: "deserialize".to_string(),
reason: format!("Failed to deserialize archived changeset: {}", e),
})?;
Ok(archived)
}
}