diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-22 22:10:19 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-17 21:02:05 +0800 |
| commit | 5a5a07c7fad31641d032a743e4e87ffb58ade17d (patch) | |
| tree | b6fbd33e8de82e5f9d9e6b99e3cb2102e47fe3ee /rola-vcs | |
Initial commitmain
Diffstat (limited to 'rola-vcs')
| -rw-r--r-- | rola-vcs/Cargo.toml | 9 | ||||
| -rw-r--r-- | rola-vcs/internal_macros/Cargo.toml | 14 | ||||
| -rw-r--r-- | rola-vcs/internal_macros/src/constants.rs | 115 | ||||
| -rw-r--r-- | rola-vcs/internal_macros/src/lib.rs | 39 | ||||
| -rw-r--r-- | rola-vcs/src/abstracts.rs | 2 | ||||
| -rw-r--r-- | rola-vcs/src/abstracts/dir_pointer.rs | 88 | ||||
| -rw-r--r-- | rola-vcs/src/bucket.rs | 18 | ||||
| -rw-r--r-- | rola-vcs/src/err.rs | 105 | ||||
| -rw-r--r-- | rola-vcs/src/err/io.rs | 10 | ||||
| -rw-r--r-- | rola-vcs/src/lib.rs | 24 | ||||
| -rw-r--r-- | rola-vcs/src/tools.rs | 1 | ||||
| -rw-r--r-- | rola-vcs/src/tools/dir_search.rs | 54 | ||||
| -rw-r--r-- | rola-vcs/src/workdraft.rs | 150 |
13 files changed, 629 insertions, 0 deletions
diff --git a/rola-vcs/Cargo.toml b/rola-vcs/Cargo.toml new file mode 100644 index 0000000..6fec629 --- /dev/null +++ b/rola-vcs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rorolala" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +rorolala_internal_macros.workspace = true diff --git a/rola-vcs/internal_macros/Cargo.toml b/rola-vcs/internal_macros/Cargo.toml new file mode 100644 index 0000000..0d6f641 --- /dev/null +++ b/rola-vcs/internal_macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rorolala_internal_macros" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/rola-vcs/internal_macros/src/constants.rs b/rola-vcs/internal_macros/src/constants.rs new file mode 100644 index 0000000..2e76bfe --- /dev/null +++ b/rola-vcs/internal_macros/src/constants.rs @@ -0,0 +1,115 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{Expr, Item, ItemConst, ItemMod, Lit, parse_macro_input, parse_quote}; + +/// Entry point called from lib.rs. +pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_mod = parse_macro_input!(item as ItemMod); + + let (_, items) = match &mut input_mod.content { + Some(content) => content, + None => panic!("#[constants] can only be applied to a module with a body"), + }; + + let mut new_items: Vec<Item> = Vec::with_capacity(items.len()); + + for item in items.iter() { + if let Item::Const(const_item) = item { + let func = transform_const(const_item); + new_items.push(func); + } else { + new_items.push(item.clone()); + } + } + + let mod_ident = &input_mod.ident; + let vis = &input_mod.vis; + + let output = quote! { + #[allow(non_snake_case)] + #vis mod #mod_ident { + #(#new_items)* + } + }; + + output.into() +} + +/// Transforms a single `const` item into a function. +fn transform_const(const_item: &ItemConst) -> Item { + let name = &const_item.ident; + let attrs = &const_item.attrs; + + // Extract the string literal value from the const + let value_str = match &*const_item.expr { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(lit_str) => lit_str.value(), + _ => panic!( + "#[constants] only supports `&str` literals, \ + but `{name}` has a non-string literal" + ), + }, + _ => panic!( + "#[constants] only supports literal expressions, \ + but `{name}` has a non-literal expression" + ), + }; + + let placeholders = extract_placeholders(&value_str); + + if placeholders.is_empty() { + parse_quote! { + #(#attrs)* + pub fn #name() -> String { + #value_str.to_string() + } + } + } else { + let params: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident: impl ::core::convert::AsRef<str> } + }) + .collect(); + + let format_args: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident = #ident.as_ref() } + }) + .collect(); + + parse_quote! { + #(#attrs)* + pub fn #name(#(#params),*) -> String { + ::std::format!(#value_str, #(#format_args),*) + } + } + } +} + +/// Extracts all `{name}` placeholder identifiers from a format string. +fn extract_placeholders(s: &str) -> Vec<String> { + let mut placeholders = Vec::new(); + let mut chars = s.char_indices().peekable(); + + while let Some((_, c)) = chars.next() { + if c == '{' { + let mut name = String::new(); + for (_, c) in &mut chars { + if c == '}' { + break; + } + name.push(c); + } + let trimmed = name.trim().to_string(); + if !trimmed.is_empty() { + placeholders.push(trimmed); + } + } + } + + placeholders +} diff --git a/rola-vcs/internal_macros/src/lib.rs b/rola-vcs/internal_macros/src/lib.rs new file mode 100644 index 0000000..f6c3cb7 --- /dev/null +++ b/rola-vcs/internal_macros/src/lib.rs @@ -0,0 +1,39 @@ +mod constants; + +use proc_macro::TokenStream; + +/// Transforms `pub const` items in a module into equivalent functions. +/// +/// Constants without `{param}` placeholders become `fn NAME() -> String`. +/// Constants with `{param}` placeholders become `fn NAME(param: impl AsRef<str>) -> String`, +/// using `format!()` to fill in the placeholders. +/// +/// The entire module is annotated with `#[allow(non_snake_case)]`. +/// +/// # Example +/// +/// ```ignore +/// #[rorolala_internal_macros::constants] +/// pub mod paths { +/// pub const ROLA_DRAFT_DIR: &str = ".rola"; +/// pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}"; +/// } +/// ``` +/// +/// expands to: +/// +/// ```ignore +/// #[allow(non_snake_case)] +/// pub mod paths { +/// pub fn ROLA_DRAFT_DIR() -> String { +/// ".rola".to_string() +/// } +/// pub fn ROLA_BINDED_BUCKET_FILE(bucket: impl AsRef<str>) -> String { +/// format!(".rola/BIND/{bucket}", bucket = bucket.as_ref()) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn constants(attr: TokenStream, item: TokenStream) -> TokenStream { + constants::expand(attr, item) +} diff --git a/rola-vcs/src/abstracts.rs b/rola-vcs/src/abstracts.rs new file mode 100644 index 0000000..e707ec3 --- /dev/null +++ b/rola-vcs/src/abstracts.rs @@ -0,0 +1,2 @@ +mod dir_pointer; +pub use dir_pointer::*; diff --git a/rola-vcs/src/abstracts/dir_pointer.rs b/rola-vcs/src/abstracts/dir_pointer.rs new file mode 100644 index 0000000..0e59960 --- /dev/null +++ b/rola-vcs/src/abstracts/dir_pointer.rs @@ -0,0 +1,88 @@ +use std::{ + borrow::Borrow, + ops::{Deref, DerefMut}, + path::PathBuf, +}; + +/// Directory Pointer Data +pub trait DirPtrData { + /// Fix the given path + /// + /// Returns Some(path): use the fixed path + /// Returns None: path cannot be fixed, this pointer is invalid + #[doc(hidden)] + fn fix(raw_path: PathBuf) -> Option<PathBuf>; +} + +#[derive(Debug, Default, Clone)] +pub struct DirPtr<Data: DirPtrData> { + /// Whether the current directory pointer is valid + valid: bool, + + /// Data for the directory pointer + data: Data, + + /// Path to the directory + path: PathBuf, +} + +impl<Data: DirPtrData> DirPtr<Data> { + /// Get a reference to the directory pointer's path + pub fn path_ref(&self) -> &PathBuf { + &self.path + } + + /// Get the directory pointer's path + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + /// Returns whether the directory pointer is valid + pub fn is_valid(&self) -> bool { + self.valid + } +} + +impl<Data: DirPtrData + Default> DirPtr<Data> { + /// Create a new directory pointer with the given path and default data + pub fn new(path: impl Into<PathBuf>) -> Self { + let path = path.into(); + let fixed = Data::fix(path.clone()); + Self { + valid: fixed.is_some(), + data: Data::default(), + path: fixed.unwrap_or(path), + } + } +} + +impl<Data: DirPtrData> AsRef<DirPtr<Data>> for DirPtr<Data> { + fn as_ref(&self) -> &DirPtr<Data> { + self + } +} + +impl<Data: DirPtrData> Deref for DirPtr<Data> { + type Target = Data; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<Data: DirPtrData> DerefMut for DirPtr<Data> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl<Data: DirPtrData> Borrow<Data> for DirPtr<Data> { + fn borrow(&self) -> &Data { + &self.data + } +} + +/// Create a new directory pointer with the given path and default data +pub fn dir_ptr<Data: DirPtrData + Default>(path: impl Into<PathBuf>) -> DirPtr<Data> { + DirPtr::new(path) +} diff --git a/rola-vcs/src/bucket.rs b/rola-vcs/src/bucket.rs new file mode 100644 index 0000000..40de6f8 --- /dev/null +++ b/rola-vcs/src/bucket.rs @@ -0,0 +1,18 @@ +//! Bucket - Rorolala Storage Unit + +use crate::{ + DirPtrData, DirSearchPattern, bucket::constants::ROLA_BUCKET_CONFIG_FILE, dir_search_prev, +}; + +pub mod constants { + /// The name of the bucket config file + pub const ROLA_BUCKET_CONFIG_FILE: &str = "rorolala.toml"; +} + +pub struct Bucket; + +impl DirPtrData for Bucket { + fn fix(raw_path: std::path::PathBuf) -> Option<std::path::PathBuf> { + dir_search_prev(raw_path, DirSearchPattern::File(ROLA_BUCKET_CONFIG_FILE)) + } +} diff --git a/rola-vcs/src/err.rs b/rola-vcs/src/err.rs new file mode 100644 index 0000000..71e0a34 --- /dev/null +++ b/rola-vcs/src/err.rs @@ -0,0 +1,105 @@ +//! Error +//! +//! This module is used to create and log Rorolala standard errors + +use std::{ + fmt::{Display, Formatter}, + io::Error as IoError, +}; + +mod io; + +/// Rorolala standard error +#[derive(Default)] +pub struct RolaError { + pub module: RolaModule, + pub data: RolaErrorData, + pub message: String, +} + +/// Rorolala module, used to locate the source of an error +#[derive(Debug, Eq, Default)] +#[repr(u8)] +pub enum RolaModule { + #[default] + Empty, + + Bucket, + Workdraft, +} + +/// Error data +#[derive(Debug, Default)] +pub enum RolaErrorData { + #[default] + Empty, + + /// IO error + IO(IoError), +} + +impl RolaError { + /// Create a new RolaError + pub fn new(module: RolaModule, data: RolaErrorData, message: String) -> Self { + Self { + module, + data, + message, + } + } + + /// Create an empty RolaError + pub fn empty() -> Self { + Self { + module: RolaModule::Empty, + data: RolaErrorData::Empty, + message: String::new(), + } + } + + /// Set the module + pub fn with_module(mut self, module: RolaModule) -> Self { + self.module = module; + self + } + + /// Set the message + pub fn with_message(mut self, message: String) -> Self { + self.message = message; + self + } + + /// Set the error data + pub fn with_data(mut self, data: RolaErrorData) -> Self { + self.data = data; + self + } +} + +impl Display for RolaError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl PartialEq for RolaError { + fn eq(&self, other: &Self) -> bool { + self.message == other.message && self.module == other.module + } +} + +impl Display for RolaModule { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl PartialEq for RolaModule { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (RolaModule::Bucket, RolaModule::Bucket) + | (RolaModule::Workdraft, RolaModule::Workdraft) + ) + } +} diff --git a/rola-vcs/src/err/io.rs b/rola-vcs/src/err/io.rs new file mode 100644 index 0000000..8dd4c1a --- /dev/null +++ b/rola-vcs/src/err/io.rs @@ -0,0 +1,10 @@ +use crate::{RolaError, RolaErrorData, RolaModule}; + +impl From<(RolaModule, std::io::Error)> for RolaError { + fn from(val: (RolaModule, std::io::Error)) -> Self { + let (module, io_err) = val; + let message = io_err.to_string(); + let data = RolaErrorData::IO(io_err); + RolaError::new(module, data, message) + } +} diff --git a/rola-vcs/src/lib.rs b/rola-vcs/src/lib.rs new file mode 100644 index 0000000..7672ff9 --- /dev/null +++ b/rola-vcs/src/lib.rs @@ -0,0 +1,24 @@ +#![allow(dead_code)] + +mod bucket; + +mod abstracts; +pub use abstracts::*; + +mod workdraft; +pub use workdraft::*; + +mod tools; +pub use tools::*; + +mod err; +pub use err::*; + +#[doc(hidden)] +#[macro_export] +macro_rules! include_mod { + ($module:ident) => { + mod $module; + pub use $module::*; + }; +} diff --git a/rola-vcs/src/tools.rs b/rola-vcs/src/tools.rs new file mode 100644 index 0000000..43b4e33 --- /dev/null +++ b/rola-vcs/src/tools.rs @@ -0,0 +1 @@ +crate::include_mod!(dir_search); diff --git a/rola-vcs/src/tools/dir_search.rs b/rola-vcs/src/tools/dir_search.rs new file mode 100644 index 0000000..3d32c70 --- /dev/null +++ b/rola-vcs/src/tools/dir_search.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +pub enum DirSearchPattern<'a> { + File(&'a str), + Dir(&'a str), +} + +/// Searches upward from the given path towards parent directories. +/// If any ancestor directory contains a file or directory matching the pattern, +/// returns that ancestor directory's path. +pub fn dir_search_prev(path: impl Into<PathBuf>, pattern: DirSearchPattern) -> Option<PathBuf> { + let mut current: PathBuf = path.into(); + // Canonicalize the path if possible to ensure absolute traversal + if let Ok(canonical) = current.canonicalize() { + current = canonical; + } else { + // If canonicalization fails (e.g. path does not exist yet), + // try to make it absolute using current dir + if current.is_relative() + && let Ok(cwd) = std::env::current_dir() { + current = cwd.join(¤t); + } + } + + loop { + // Check if the current directory exists and is a directory + if current.is_dir() { + let has_match = match &pattern { + DirSearchPattern::File(name) => { + let mut entry = current.clone(); + entry.push(name); + entry.is_file() + } + DirSearchPattern::Dir(name) => { + let mut entry = current.clone(); + entry.push(name); + entry.is_dir() + } + }; + + if has_match { + return Some(current); + } + } + + // Try to go to the parent directory + if !current.pop() { + // pop() returns false when there's no parent + break; + } + } + + None +} diff --git a/rola-vcs/src/workdraft.rs b/rola-vcs/src/workdraft.rs new file mode 100644 index 0000000..45bb418 --- /dev/null +++ b/rola-vcs/src/workdraft.rs @@ -0,0 +1,150 @@ +//! Work Draft +//! +//! Work Draft is the local workspace of `Rorolala`, used to store files being modified + +use std::path::PathBuf; + +use crate::{ + DirPtrData, DirSearchPattern, RolaError, RolaModule, dir_search_prev, + workdraft::constants::ROLA_DRAFT_DIR, +}; + +#[rorolala_internal_macros::constants] +pub mod constants { + /// The name of the workdraft directory + pub const ROLA_DRAFT_DIR: &str = ".rola"; + + /// The name of the directory containing bucket bindings + pub const ROLA_BINDED_BUCKETS_DIR: &str = ".rola/BIND/"; + + /// The name of the bind file + pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}"; +} + +/// Work Draft Pointer +/// +/// This struct is used to point to an operable local work draft directory on disk +#[derive(Debug, Default, Clone)] +pub struct WorkDraft; + +impl DirPtrData for WorkDraft { + fn fix(raw_path: PathBuf) -> Option<PathBuf> { + let draft_dir = ROLA_DRAFT_DIR(); + dir_search_prev(raw_path, DirSearchPattern::Dir(&draft_dir)) + } +} + +impl WorkDraft { + /// Creates a new work draft directory at the given path + pub fn create(path: PathBuf) -> Result<(), RolaError> { + let dir = path.join(ROLA_DRAFT_DIR()); + std::fs::create_dir_all(dir).map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + Ok(()) + } +} + +/// Module for managing workdraft bucket bindings +pub mod bucket_bind_mgr { + use std::path::PathBuf; + + use crate::{ + DirPtr, RolaError, RolaModule, WorkDraft, constants::ROLA_BINDED_BUCKETS_DIR, + workdraft::constants::ROLA_BINDED_BUCKET_FILE, + }; + + impl DirPtr<WorkDraft> { + /// Returns the path to the bind file for this work draft + pub fn bind_bucket_path(&self, bucket: impl AsRef<str>) -> PathBuf { + self.path().join(ROLA_BINDED_BUCKET_FILE(bucket)) + } + + /// Returns the path to the bind file for this work draft + pub fn bind_bucket(&self, bucket: impl AsRef<str>) -> Result<String, RolaError> { + let bucket_path = self.bind_bucket_path(bucket); + let parent = bucket_path.parent().ok_or_else(|| -> RolaError { + RolaError::from(( + RolaModule::Workdraft, + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid bucket path: {}", bucket_path.to_string_lossy()), + ), + )) + })?; + + if !parent.exists() { + std::fs::create_dir_all(parent) + .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + } + + match std::fs::read_to_string(bucket_path) { + Ok(str) => Ok(str), + Err(err) => Err(RolaError::from((RolaModule::Workdraft, err))), + } + } + + /// Binds the work draft to the specified bucket + pub fn bind_bucket_to(&self, bucket: &str) -> Result<(), RolaError> { + let bucket_path = self.bind_bucket_path(bucket); + let parent = bucket_path.parent().ok_or_else(|| -> RolaError { + RolaError::from(( + RolaModule::Workdraft, + std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid bucket path"), + )) + })?; + if !parent.exists() { + std::fs::create_dir_all(parent) + .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + } + std::fs::write(&bucket_path, bucket) + .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + Ok(()) + } + + /// Returns all bound bucket names for this work draft + pub fn binded_buckets(&self) -> Result<Vec<String>, RolaError> { + let bind_dir = self.path().join(ROLA_BINDED_BUCKETS_DIR()); + if !bind_dir.exists() { + return Ok(Vec::new()); + } + + let mut buckets = Vec::new(); + let entries = std::fs::read_dir(&bind_dir) + .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + + for entry in entries { + let entry = entry.map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + let path = entry.path(); + + if path.is_file() { + // Read the file content which contains the bucket name + if let Ok(content) = std::fs::read_to_string(&path) { + let bucket = content.trim().to_string(); + if !bucket.is_empty() { + buckets.push(bucket); + } + } + } + } + + Ok(buckets) + } + + /// Removes the binding for the specified bucket + pub fn unbind_bucket(&self, bucket: impl AsRef<str>) -> Result<(), RolaError> { + let bucket_path = self.bind_bucket_path(bucket); + if bucket_path.exists() { + std::fs::remove_file(&bucket_path) + .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?; + } + Ok(()) + } + + /// Removes bindings for all specified buckets + pub fn unbind_buckets(&self, buckets: &[impl AsRef<str>]) -> Result<(), RolaError> { + for bucket in buckets { + self.unbind_bucket(bucket)?; + } + Ok(()) + } + } +} |
