summaryrefslogtreecommitdiff
path: root/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/display.rs486
-rw-r--r--src/utils/env.rs94
-rw-r--r--src/utils/fs.rs40
-rw-r--r--src/utils/globber.rs276
-rw-r--r--src/utils/input.rs139
-rw-r--r--src/utils/levenshtein_distance.rs34
-rw-r--r--src/utils/logger.rs87
-rw-r--r--src/utils/push_version.rs30
-rw-r--r--src/utils/socket_addr_helper.rs194
-rw-r--r--src/utils/workspace_reader.rs377
10 files changed, 0 insertions, 1757 deletions
diff --git a/src/utils/display.rs b/src/utils/display.rs
deleted file mode 100644
index 835313b..0000000
--- a/src/utils/display.rs
+++ /dev/null
@@ -1,486 +0,0 @@
-use colored::*;
-use just_enough_vcs::vcs::data::sheet::SheetMappingMetadata;
-use std::{
- collections::{BTreeMap, HashMap, VecDeque},
- path::PathBuf,
-};
-
-pub struct SimpleTable {
- items: Vec<String>,
- line: Vec<Vec<String>>,
- length: Vec<usize>,
- padding: usize,
-}
-
-impl SimpleTable {
- /// Create a new Table
- pub fn new(items: Vec<impl Into<String>>) -> Self {
- Self::new_with_padding(items, 2)
- }
-
- /// Create a new Table with padding
- pub fn new_with_padding(items: Vec<impl Into<String>>, padding: usize) -> Self {
- let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
- let mut length = Vec::with_capacity(items.len());
-
- for item in &items {
- length.push(display_width(item));
- }
-
- SimpleTable {
- items,
- padding,
- line: Vec::new(),
- length,
- }
- }
-
- /// Push a new row of items to the table
- pub fn push_item(&mut self, items: Vec<impl Into<String>>) {
- let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
-
- let mut processed_items = Vec::with_capacity(self.items.len());
-
- for i in 0..self.items.len() {
- if i < items.len() {
- processed_items.push(items[i].clone());
- } else {
- processed_items.push(String::new());
- }
- }
-
- for (i, d) in processed_items.iter().enumerate() {
- let d_len = display_width(d);
- if d_len > self.length[i] {
- self.length[i] = d_len;
- }
- }
-
- self.line.push(processed_items);
- }
-
- /// Insert a new row of items at the specified index
- pub fn insert_item(&mut self, index: usize, items: Vec<impl Into<String>>) {
- let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
-
- let mut processed_items = Vec::with_capacity(self.items.len());
-
- for i in 0..self.items.len() {
- if i < items.len() {
- processed_items.push(items[i].clone());
- } else {
- processed_items.push(String::new());
- }
- }
-
- for (i, d) in processed_items.iter().enumerate() {
- let d_len = display_width(d);
- if d_len > self.length[i] {
- self.length[i] = d_len;
- }
- }
-
- self.line.insert(index, processed_items);
- }
-
- /// Get the current maximum column widths
- fn get_column_widths(&self) -> &[usize] {
- &self.length
- }
-}
-
-impl std::fmt::Display for SimpleTable {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let column_widths = self.get_column_widths();
-
- // Build the header row
- let header: Vec<String> = self
- .items
- .iter()
- .enumerate()
- .map(|(i, item)| {
- let target_width = column_widths[i] + self.padding;
- let current_width = display_width(item);
- let space_count = target_width - current_width;
- let space = " ".repeat(space_count);
- let result = format!("{}{}", item, space);
- result
- })
- .collect();
- writeln!(f, "{}", header.join(""))?;
-
- // Build each data row
- for row in &self.line {
- let formatted_row: Vec<String> = row
- .iter()
- .enumerate()
- .map(|(i, cell)| {
- let target_width = column_widths[i] + self.padding;
- let current_width = display_width(cell);
- let space_count = target_width - current_width;
- let spaces = " ".repeat(space_count);
- let result = format!("{}{}", cell, spaces);
- result
- })
- .collect();
- writeln!(f, "{}", formatted_row.join(""))?;
- }
-
- Ok(())
- }
-}
-
-pub fn display_width(s: &str) -> usize {
- // Filter out ANSI escape sequences before calculating width
- let filtered_bytes = strip_ansi_escapes::strip(s);
- let filtered_str = match std::str::from_utf8(&filtered_bytes) {
- Ok(s) => s,
- Err(_) => s, // Fallback to original string if UTF-8 conversion fails
- };
-
- let mut width = 0;
- for c in filtered_str.chars() {
- if c.is_ascii() {
- width += 1;
- } else {
- width += 2;
- }
- }
- width
-}
-
-/// Convert byte size to a human-readable string format
-///
-/// Automatically selects the appropriate unit (B, KB, MB, GB, TB) based on the byte size
-/// and formats it as a string with two decimal places
-pub fn size_str(total_size: usize) -> String {
- if total_size < 1024 {
- format!("{} B", total_size)
- } else if total_size < 1024 * 1024 {
- format!("{:.2} KB", total_size as f64 / 1024.0)
- } else if total_size < 1024 * 1024 * 1024 {
- format!("{:.2} MB", total_size as f64 / (1024.0 * 1024.0))
- } else if total_size < 1024 * 1024 * 1024 * 1024 {
- format!("{:.2} GB", total_size as f64 / (1024.0 * 1024.0 * 1024.0))
- } else {
- format!(
- "{:.2} TB",
- total_size as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0)
- )
- }
-}
-
-// Convert the Markdown formatted text into a format supported by the command line
-pub fn md(text: impl AsRef<str>) -> String {
- let text = text.as_ref().trim();
- let mut result = String::new();
- let mut color_stack: VecDeque<String> = VecDeque::new();
-
- let mut i = 0;
- let chars: Vec<char> = text.chars().collect();
-
- while i < chars.len() {
- // Check for escape character \
- if chars[i] == '\\' && i + 1 < chars.len() {
- let escaped_char = chars[i + 1];
- // Only escape specific characters
- if matches!(escaped_char, '*' | '<' | '>' | '`') {
- let mut escaped_text = escaped_char.to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- escaped_text = apply_color(&escaped_text, color);
- }
-
- result.push_str(&escaped_text);
- i += 2;
- continue;
- }
- }
-
- // Check for color tag start [[color]]
- if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' {
- let mut j = i + 2;
- while j < chars.len()
- && !(chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == ']')
- {
- j += 1;
- }
-
- if j + 1 < chars.len() {
- let tag_content: String = chars[i + 2..j].iter().collect();
-
- // Check if it's a closing tag [[/]]
- if tag_content == "/" {
- color_stack.pop_back();
- i = j + 2;
- continue;
- }
-
- // It's a color tag
- color_stack.push_back(tag_content.clone());
- i = j + 2;
- continue;
- }
- }
-
- // Check for bold **text**
- if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
- let mut j = i + 2;
- while j + 1 < chars.len() && !(chars[j] == '*' && chars[j + 1] == '*') {
- j += 1;
- }
-
- if j + 1 < chars.len() {
- let bold_text: String = chars[i + 2..j].iter().collect();
- let mut formatted_text = bold_text.bold().to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- formatted_text = apply_color(&formatted_text, color);
- }
-
- result.push_str(&formatted_text);
- i = j + 2;
- continue;
- }
- }
-
- // Check for italic *text*
- if chars[i] == '*' {
- let mut j = i + 1;
- while j < chars.len() && chars[j] != '*' {
- j += 1;
- }
-
- if j < chars.len() {
- let italic_text: String = chars[i + 1..j].iter().collect();
- let mut formatted_text = italic_text.italic().to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- formatted_text = apply_color(&formatted_text, color);
- }
-
- result.push_str(&formatted_text);
- i = j + 1;
- continue;
- }
- }
-
- // Check for angle-bracketed content <text>
- if chars[i] == '<' {
- let mut j = i + 1;
- while j < chars.len() && chars[j] != '>' {
- j += 1;
- }
-
- if j < chars.len() {
- // Include the angle brackets in the output
- let angle_text: String = chars[i..=j].iter().collect();
- let mut formatted_text = angle_text.cyan().to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- formatted_text = apply_color(&formatted_text, color);
- }
-
- result.push_str(&formatted_text);
- i = j + 1;
- continue;
- }
- }
-
- // Check for inline code `text`
- if chars[i] == '`' {
- let mut j = i + 1;
- while j < chars.len() && chars[j] != '`' {
- j += 1;
- }
-
- if j < chars.len() {
- // Include the backticks in the output
- let code_text: String = chars[i..=j].iter().collect();
- let mut formatted_text = code_text.green().to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- formatted_text = apply_color(&formatted_text, color);
- }
-
- result.push_str(&formatted_text);
- i = j + 1;
- continue;
- }
- }
-
- // Regular character
- let mut current_char = chars[i].to_string();
-
- // Apply current color stack
- for color in color_stack.iter().rev() {
- current_char = apply_color(&current_char, color);
- }
-
- result.push_str(&current_char);
- i += 1;
- }
-
- result
-}
-
-// Helper function to apply color to text
-fn apply_color(text: &str, color_name: &str) -> String {
- match color_name {
- // Normal colors
- "black" => text.black().to_string(),
- "red" => text.red().to_string(),
- "green" => text.green().to_string(),
- "yellow" => text.yellow().to_string(),
- "blue" => text.blue().to_string(),
- "magenta" => text.magenta().to_string(),
- "cyan" => text.cyan().to_string(),
- "white" => text.white().to_string(),
- "bright_black" => text.bright_black().to_string(),
- "bright_red" => text.bright_red().to_string(),
- "bright_green" => text.bright_green().to_string(),
- "bright_yellow" => text.bright_yellow().to_string(),
- "bright_blue" => text.bright_blue().to_string(),
- "bright_magenta" => text.bright_magenta().to_string(),
- "bright_cyan" => text.bright_cyan().to_string(),
- "bright_white" => text.bright_white().to_string(),
-
- // Short aliases for bright colors
- "b_black" => text.bright_black().to_string(),
- "b_red" => text.bright_red().to_string(),
- "b_green" => text.bright_green().to_string(),
- "b_yellow" => text.bright_yellow().to_string(),
- "b_blue" => text.bright_blue().to_string(),
- "b_magenta" => text.bright_magenta().to_string(),
- "b_cyan" => text.bright_cyan().to_string(),
- "b_white" => text.bright_white().to_string(),
-
- // Gray colors using truecolor
- "gray" | "grey" => text.truecolor(128, 128, 128).to_string(),
- "bright_gray" | "bright_grey" => text.truecolor(192, 192, 192).to_string(),
- "b_gray" | "b_grey" => text.truecolor(192, 192, 192).to_string(),
-
- // Default to white if color not recognized
- _ => text.to_string(),
- }
-}
-
-/// Render a HashMap of PathBuf to SheetMappingMetadata as a tree string.
-pub fn render_share_path_tree(paths: &HashMap<PathBuf, SheetMappingMetadata>) -> String {
- if paths.is_empty() {
- return String::new();
- }
-
- // Collect all path components into a tree structure
- let mut root = TreeNode::new("".to_string());
-
- for (path, metadata) in paths {
- let mut current = &mut root;
- let components: Vec<String> = path
- .components()
- .filter_map(|comp| match comp {
- std::path::Component::Normal(s) => s.to_str().map(|s| s.to_string()),
- _ => None,
- })
- .collect();
-
- for (i, comp) in components.iter().enumerate() {
- let is_leaf = i == components.len() - 1;
- let child = current
- .children
- .entry(comp.clone())
- .or_insert_with(|| TreeNode::new(comp.clone()));
-
- // If this is the leaf node, store the metadata
- if is_leaf {
- child.metadata = Some((metadata.id.clone(), metadata.version.clone()));
- }
-
- current = child;
- }
- }
-
- // Convert tree to string representation
- let mut result = String::new();
- let is_root = true;
- let prefix = String::new();
- let last_stack = vec![true]; // Root is always "last"
-
- add_tree_node_to_string(&root, &mut result, is_root, &prefix, &last_stack);
-
- result
-}
-
-/// Internal tree node structure for building the path tree
-#[derive(Debug)]
-struct TreeNode {
- name: String,
- children: BTreeMap<String, TreeNode>, // Use BTreeMap for sorted output
- metadata: Option<(String, String)>, // Store (id, version) for leaf nodes
-}
-
-impl TreeNode {
- fn new(name: String) -> Self {
- Self {
- name,
- children: BTreeMap::new(),
- metadata: None,
- }
- }
-}
-
-/// Recursively add tree node to string representation
-fn add_tree_node_to_string(
- node: &TreeNode,
- result: &mut String,
- is_root: bool,
- prefix: &str,
- last_stack: &[bool],
-) {
- if !is_root {
- // Add the tree prefix for this node
- for &is_last in &last_stack[1..] {
- if is_last {
- result.push_str(" ");
- } else {
- result.push_str("│ ");
- }
- }
-
- // Add the connector for this node
- if let Some(&is_last) = last_stack.last() {
- if is_last {
- result.push_str("└── ");
- } else {
- result.push_str("├── ");
- }
- }
-
- // Add node name
- result.push_str(&node.name);
-
- // Add metadata for leaf nodes
- if let Some((id, version)) = &node.metadata {
- // Truncate id to first 11 characters
- let truncated_id = if id.len() > 11 { &id[..11] } else { id };
- result.push_str(&format!(" [{}|{}]", truncated_id, version));
- }
-
- result.push('\n');
- }
-
- // Process children
- let child_count = node.children.len();
- for (i, (_, child)) in node.children.iter().enumerate() {
- let is_last_child = i == child_count - 1;
- let mut new_last_stack = last_stack.to_vec();
- new_last_stack.push(is_last_child);
-
- add_tree_node_to_string(child, result, false, prefix, &new_last_stack);
- }
-}
diff --git a/src/utils/env.rs b/src/utils/env.rs
deleted file mode 100644
index 81dfbd7..0000000
--- a/src/utils/env.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-/// Returns the current locale string based on environment variables.
-///
-/// The function checks for locale settings in the following order:
-/// 1. JV_LANG environment variable
-/// 2. APP_LANG environment variable
-/// 3. LANG environment variable (extracts base language before dot and replaces underscores with hyphens)
-/// 4. Defaults to "en" if no locale environment variables are found
-///
-/// # Returns
-/// A String containing the detected locale code
-pub fn current_locales() -> String {
- if let Ok(lang) = std::env::var("JV_LANG") {
- return lang;
- }
-
- if let Ok(lang) = std::env::var("APP_LANG") {
- return lang;
- }
-
- if let Ok(lang) = std::env::var("LANG") {
- if let Some(base_lang) = lang.split('.').next() {
- return base_lang.replace('_', "-");
- }
- return lang;
- }
-
- "en".to_string()
-}
-
-/// Checks if auto update is enabled based on environment variables.
-///
-/// The function checks the JV_AUTO_UPDATE environment variable and compares
-/// its value (after trimming and converting to lowercase) against known
-/// positive and negative values.
-///
-/// # Returns
-/// `true` if the value matches "yes", "y", or "true"
-/// `false` if the value matches "no", "n", or "false", or if the variable is not set
-pub fn enable_auto_update() -> bool {
- if let Ok(auto_update) = std::env::var("JV_AUTO_UPDATE") {
- let normalized = auto_update.trim().to_lowercase();
- match normalized.as_str() {
- "yes" | "y" | "true" => return true,
- "no" | "n" | "false" => return false,
- _ => {}
- }
- }
- false
-}
-
-/// Gets the auto update expiration time based on environment variables.
-///
-/// The function checks the JV_OUTDATED_MINUTES environment variable.
-/// Requires JV_AUTO_UPDATE to be enabled.
-/// Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed.
-///
-/// # Returns
-/// - When the set number is < 0, timeout-based update is disabled
-/// - When the set number = 0, update runs every time (not recommended)
-/// - When the set number > 0, update according to the specified time
-/// - If not set or conversion error occurs, the default is -1
-pub fn auto_update_outdate() -> i64 {
- if !enable_auto_update() {
- return -1;
- }
-
- match std::env::var("JV_OUTDATED_MINUTES") {
- Ok(value) => match value.trim().parse::<i64>() {
- Ok(num) => num,
- Err(_) => -1,
- },
- Err(_) => -1,
- }
-}
-
-/// Gets the default text editor based on environment variables.
-///
-/// The function checks the JV_TEXT_EDITOR and EDITOR environment variables
-/// and returns their values if they are set. If neither variable is set,
-/// it returns "jvii" as the default editor.
-///
-/// # Returns
-/// A String containing the default text editor
-pub async fn get_default_editor() -> String {
- if let Ok(editor) = std::env::var("JV_TEXT_EDITOR") {
- return editor;
- }
-
- if let Ok(editor) = std::env::var("EDITOR") {
- return editor;
- }
-
- "jvii".to_string()
-}
diff --git a/src/utils/fs.rs b/src/utils/fs.rs
deleted file mode 100644
index 0050cf1..0000000
--- a/src/utils/fs.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-pub async fn move_across_partitions(
- source_path: impl AsRef<std::path::Path>,
- dest_path: impl AsRef<std::path::Path>,
-) -> Result<(), std::io::Error> {
- let source_path = source_path.as_ref();
- let dest_path = dest_path.as_ref();
- if !source_path.exists() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "Source file does not exist",
- ));
- }
-
- if let Ok(()) = std::fs::rename(source_path, dest_path) {
- return Ok(());
- }
-
- std::fs::copy(source_path, dest_path)?;
- std::fs::remove_file(source_path)?;
-
- Ok(())
-}
-
-pub async fn copy_across_partitions(
- source_path: impl AsRef<std::path::Path>,
- dest_path: impl AsRef<std::path::Path>,
-) -> Result<(), std::io::Error> {
- let source_path = source_path.as_ref();
- let dest_path = dest_path.as_ref();
- if !source_path.exists() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "Source file does not exist",
- ));
- }
-
- std::fs::copy(source_path, dest_path)?;
-
- Ok(())
-}
diff --git a/src/utils/globber.rs b/src/utils/globber.rs
deleted file mode 100644
index dbb77ea..0000000
--- a/src/utils/globber.rs
+++ /dev/null
@@ -1,276 +0,0 @@
-use std::{io::Error, path::PathBuf, str::FromStr};
-
-use just_enough_vcs::utils::string_proc::format_path::format_path_str;
-
-use crate::utils::globber::constants::{SPLIT_STR, get_base_dir_current};
-
-pub struct Globber {
- pattern: String,
- base: PathBuf,
- names: Vec<String>,
-}
-
-#[allow(dead_code)]
-impl Globber {
- pub fn new(pattern: String, base: PathBuf) -> Self {
- Self {
- pattern,
- base,
- names: Vec::new(),
- }
- }
-
- pub fn names(&self) -> Vec<&String> {
- self.names.iter().collect()
- }
-
- pub fn base(&self) -> &PathBuf {
- &self.base
- }
-
- pub fn into_names(self) -> Vec<String> {
- self.names
- }
-
- pub fn paths(&self) -> Vec<PathBuf> {
- self.names.iter().map(|n| self.base.join(n)).collect()
- }
-
- pub fn glob<F>(mut self, get_names: F) -> Result<Self, std::io::Error>
- where
- F: Fn(PathBuf) -> Vec<GlobItem>,
- {
- let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern);
-
- let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) {
- let (path_part, pattern_part) = full_path.split_at(last_split);
- let mut path = path_part.to_string();
- if !path.ends_with(SPLIT_STR) {
- path.push_str(SPLIT_STR);
- }
- (
- format_path_str(path)?,
- pattern_part[SPLIT_STR.len()..].to_string(),
- )
- } else {
- (String::default(), full_path)
- };
-
- self.base = match PathBuf::from_str(&path) {
- Ok(r) => r,
- Err(_) => {
- return Err(Error::new(
- std::io::ErrorKind::InvalidInput,
- format!("Invalid path: \"{}\"", &path),
- ));
- }
- };
-
- let pattern = if pattern.is_empty() {
- "*".to_string()
- } else if pattern == "." {
- "*".to_string()
- } else if pattern.ends_with(SPLIT_STR) {
- format!("{}*", pattern)
- } else {
- pattern
- };
-
- if !pattern.contains('*') && !pattern.contains('?') {
- self.names = vec![pattern];
- return Ok(self);
- }
-
- let mut collected = Vec::new();
-
- collect_files(&path.into(), "./".to_string(), &mut collected, &get_names);
- fn collect_files<F>(
- base: &PathBuf,
- current: String,
- file_names: &mut Vec<String>,
- get_names: &F,
- ) where
- F: Fn(PathBuf) -> Vec<GlobItem>,
- {
- let current_path = if current.is_empty() {
- base.clone()
- } else {
- base.join(&current)
- };
-
- let items = get_names(current_path);
- for item in items {
- match item {
- GlobItem::File(file_name) => {
- let relative_path = {
- format_path_str(format!("{}{}{}", current, SPLIT_STR, file_name))
- .unwrap_or_default()
- };
- file_names.push(relative_path)
- }
- GlobItem::Directory(dir_name) => {
- let new_current = {
- format_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name))
- .unwrap_or_default()
- };
- collect_files(base, new_current, file_names, get_names);
- }
- }
- }
- }
-
- self.names = collected
- .iter()
- .filter_map(|name| match_pattern(name, &pattern))
- .collect();
-
- Ok(self)
- }
-}
-
-fn match_pattern(name: &str, pattern: &str) -> Option<String> {
- if pattern.is_empty() {
- return None;
- }
-
- let name_chars: Vec<char> = name.chars().collect();
- let pattern_chars: Vec<char> = pattern.chars().collect();
-
- let mut name_idx = 0;
- let mut pattern_idx = 0;
- let mut star_idx = -1;
- let mut match_idx = -1;
-
- while name_idx < name_chars.len() {
- if pattern_idx < pattern_chars.len()
- && (pattern_chars[pattern_idx] == '?'
- || pattern_chars[pattern_idx] == name_chars[name_idx])
- {
- name_idx += 1;
- pattern_idx += 1;
- } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
- star_idx = pattern_idx as i32;
- match_idx = name_idx as i32;
- pattern_idx += 1;
- } else if star_idx != -1 {
- pattern_idx = (star_idx + 1) as usize;
- match_idx += 1;
- name_idx = match_idx as usize;
- } else {
- return None;
- }
- }
-
- while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
- pattern_idx += 1;
- }
-
- if pattern_idx == pattern_chars.len() {
- Some(name.to_string())
- } else {
- None
- }
-}
-
-impl<T: AsRef<str>> From<T> for Globber {
- fn from(pattern: T) -> Self {
- let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string());
- Self::new(pattern, base_dir)
- }
-}
-
-#[derive(Debug, Clone, Hash)]
-pub enum GlobItem {
- File(String),
- Directory(String),
-}
-
-impl PartialEq for GlobItem {
- fn eq(&self, other: &Self) -> bool {
- match (self, other) {
- (GlobItem::File(a), GlobItem::File(b)) => a == b,
- (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b,
- _ => false,
- }
- }
-}
-
-impl std::fmt::Display for GlobItem {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- GlobItem::File(name) => write!(f, "{}", name),
- GlobItem::Directory(name) => write!(f, "{}", name),
- }
- }
-}
-
-impl Eq for GlobItem {}
-
-pub mod constants {
- use std::{env::current_dir, path::PathBuf};
-
- #[cfg(unix)]
- pub(crate) const CURRENT_DIR_PREFIX: &str = "./";
- #[cfg(windows)]
- pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\";
-
- #[cfg(unix)]
- pub(crate) const USER_DIR_PREFIX: &str = "~";
- #[cfg(windows)]
- pub(crate) const USER_DIR_PREFIX: &str = "~\\";
-
- #[cfg(unix)]
- pub(crate) const ROOT_DIR_PREFIX: &str = "/";
- #[cfg(windows)]
- pub(crate) const ROOT_DIR_PREFIX: &str = "\\";
-
- #[cfg(unix)]
- pub(crate) const SPLIT_STR: &str = "/";
- #[cfg(windows)]
- pub(crate) const SPLIT_STR: &str = "\\";
-
- pub fn get_base_dir_current(input: String) -> (PathBuf, String) {
- get_base_dir(input, current_dir().unwrap_or_default())
- }
-
- pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) {
- if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) {
- (current_dir, remaining.to_string())
- } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) {
- (dirs::home_dir().unwrap_or_default(), remaining.to_string())
- } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) {
- {
- #[cfg(unix)]
- {
- (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string())
- }
- #[cfg(windows)]
- {
- let current_drive = current_dir
- .components()
- .find_map(|comp| {
- if let std::path::Component::Prefix(prefix_component) = comp {
- Some(prefix_component)
- } else {
- None
- }
- })
- .and_then(|prefix_component| match prefix_component.kind() {
- std::path::Prefix::Disk(drive_letter)
- | std::path::Prefix::VerbatimDisk(drive_letter) => {
- Some((drive_letter as char).to_string())
- }
- _ => None,
- })
- .unwrap_or_else(|| "C".to_string());
- (
- PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)),
- remaining.to_string(),
- )
- }
- }
- } else {
- (current_dir, input)
- }
- }
-}
diff --git a/src/utils/input.rs b/src/utils/input.rs
deleted file mode 100644
index 1edcc5d..0000000
--- a/src/utils/input.rs
+++ /dev/null
@@ -1,139 +0,0 @@
-use tokio::{fs, process::Command};
-
-use crate::utils::env::get_default_editor;
-
-/// Confirm the current operation
-/// Waits for user input of 'y' or 'n'
-pub async fn confirm_hint(text: impl Into<String>) -> bool {
- use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
-
- let prompt = text.into().trim().to_string();
-
- let mut stdout = io::stdout();
- let mut stdin = BufReader::new(io::stdin());
-
- stdout
- .write_all(prompt.as_bytes())
- .await
- .expect("Failed to write prompt");
- stdout.flush().await.expect("Failed to flush stdout");
-
- let mut input = String::new();
- stdin
- .read_line(&mut input)
- .await
- .expect("Failed to read input");
-
- input.trim().eq_ignore_ascii_case("y")
-}
-
-/// Confirm the current operation, or execute a closure if rejected
-/// Waits for user input of 'y' or 'n'
-/// If 'n' is entered, executes the provided closure and returns false
-pub async fn confirm_hint_or<F>(text: impl Into<String>, on_reject: F) -> bool
-where
- F: FnOnce(),
-{
- let confirmed = confirm_hint(text).await;
- if !confirmed {
- on_reject();
- }
- confirmed
-}
-
-/// Confirm the current operation, and execute a closure if confirmed
-/// Waits for user input of 'y' or 'n'
-/// If 'y' is entered, executes the provided closure and returns true
-pub async fn confirm_hint_then<F>(text: impl Into<String>, on_confirm: F) -> bool
-where
- F: FnOnce(),
-{
- let confirmed = confirm_hint(text).await;
- if confirmed {
- on_confirm();
- }
- confirmed
-}
-
-/// Input text using the system editor
-/// Opens the system editor (from EDITOR environment variable) with default text in a cache file,
-/// then reads back the modified content after the editor closes, removing comment lines
-pub async fn input_with_editor(
- default_text: impl AsRef<str>,
- cache_file: impl AsRef<std::path::Path>,
- comment_char: impl AsRef<str>,
-) -> Result<String, std::io::Error> {
- let cache_path = cache_file.as_ref();
- let default_content = default_text.as_ref();
- let comment_prefix = comment_char.as_ref();
-
- // Write default text to cache file
- fs::write(cache_path, default_content).await?;
-
- // Get editor from environment variable
- let editor = get_default_editor().await;
-
- // Open editor with cache file
- let status = Command::new(editor).arg(cache_path).status().await?;
-
- if !status.success() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- "Editor exited with non-zero status",
- ));
- }
-
- // Read the modified content
- let content = fs::read_to_string(cache_path).await?;
-
- // Remove comment lines and trim
- let processed_content: String = content
- .lines()
- .filter_map(|line| {
- let trimmed = line.trim();
- if trimmed.starts_with(comment_prefix) {
- None
- } else {
- Some(line)
- }
- })
- .collect::<Vec<&str>>()
- .join("\n");
-
- // Delete the cache file
- let _ = fs::remove_file(cache_path).await;
-
- Ok(processed_content)
-}
-
-/// Show text using the system pager (less)
-/// Opens the system pager (less) with the given text content written to the specified file
-/// If less is not found, directly outputs the content to stdout
-pub async fn show_in_pager(
- content: impl AsRef<str>,
- cache_file: impl AsRef<std::path::Path>,
-) -> Result<(), std::io::Error> {
- let content_str = content.as_ref();
- let cache_path = cache_file.as_ref();
-
- // Write content to cache file
- fs::write(cache_path, content_str).await?;
-
- // Try to use less first
- let status = Command::new("less").arg(cache_path).status().await;
-
- match status {
- Ok(status) if status.success() => Ok(()),
- _ => {
- // If less failed, output directly to stdout
- use tokio::io::{self, AsyncWriteExt};
- let mut stdout = io::stdout();
- stdout
- .write_all(content_str.as_bytes())
- .await
- .expect("Failed to write content");
- stdout.flush().await.expect("Failed to flush stdout");
- Ok(())
- }
- }
-}
diff --git a/src/utils/levenshtein_distance.rs b/src/utils/levenshtein_distance.rs
deleted file mode 100644
index 6bdb7e7..0000000
--- a/src/utils/levenshtein_distance.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use std::cmp::min;
-
-pub fn levenshtein_distance(a: &str, b: &str) -> usize {
- let a_chars: Vec<char> = a.chars().collect();
- let b_chars: Vec<char> = b.chars().collect();
- let a_len = a_chars.len();
- let b_len = b_chars.len();
-
- if a_len == 0 {
- return b_len;
- }
- if b_len == 0 {
- return a_len;
- }
-
- let mut dp = vec![vec![0; b_len + 1]; a_len + 1];
-
- for (i, row) in dp.iter_mut().enumerate() {
- row[0] = i;
- }
-
- for (j, cell) in dp[0].iter_mut().enumerate() {
- *cell = j;
- }
-
- for (i, a_char) in a_chars.iter().enumerate() {
- for (j, b_char) in b_chars.iter().enumerate() {
- let cost = if a_char == b_char { 0 } else { 1 };
- dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost));
- }
- }
-
- dp[a_len][b_len]
-}
diff --git a/src/utils/logger.rs b/src/utils/logger.rs
deleted file mode 100644
index 7dd4f62..0000000
--- a/src/utils/logger.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use std::path::Path;
-
-use colored::Colorize;
-use env_logger::{Builder, Target};
-use just_enough_vcs::{
- utils::string_proc::format_path::format_path, vcs::data::vault::vault_config::LoggerLevel,
-};
-use log::{Level, LevelFilter};
-
-pub fn build_env_logger(log_path: impl AsRef<Path>, logger_level: LoggerLevel) {
- use std::io::{self, Write};
-
- struct MultiWriter<A, B> {
- a: A,
- b: B,
- }
-
- impl<A: Write, B: Write> MultiWriter<A, B> {
- fn new(a: A, b: B) -> Self {
- Self { a, b }
- }
- }
-
- impl<A: Write, B: Write> Write for MultiWriter<A, B> {
- fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
- let _ = self.a.write(buf);
- self.b.write(buf)
- }
-
- fn flush(&mut self) -> io::Result<()> {
- let _ = self.a.flush();
- self.b.flush()
- }
- }
-
- let log_path = {
- let path = log_path.as_ref();
- let Ok(path) = format_path(path) else {
- eprintln!(
- "Build logger failed: {} is not a vaild path.",
- path.display()
- );
- return;
- };
- path
- };
-
- let mut builder = Builder::new();
-
- let log_format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record| {
- let now = chrono::Local::now();
-
- let level_style = match record.level() {
- Level::Error => record.args().to_string().red().bold(),
- Level::Warn => record.args().to_string().yellow().bold(),
- Level::Info => record.args().to_string().white(),
- Level::Debug => record.args().to_string().white(),
- Level::Trace => record.args().to_string().cyan(),
- };
-
- writeln!(
- buf,
- "{} {}",
- now.format("%H:%M:%S")
- .to_string()
- .truecolor(105, 105, 105)
- .bold(),
- level_style
- )
- };
-
- let log_file = std::fs::File::create(log_path).expect("Failed to create log file");
- let combined_target = Target::Pipe(Box::new(MultiWriter::new(std::io::stdout(), log_file)));
-
- let level = match logger_level {
- LoggerLevel::Debug => LevelFilter::Debug,
- LoggerLevel::Trace => LevelFilter::Trace,
- LoggerLevel::Info => LevelFilter::Info,
- };
-
- builder
- .format(log_format)
- .filter(None, level.clone())
- .filter_module("just_enough_vcs", level)
- .target(combined_target)
- .init();
-}
diff --git a/src/utils/push_version.rs b/src/utils/push_version.rs
deleted file mode 100644
index 6da9039..0000000
--- a/src/utils/push_version.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-pub fn push_version(current_version: impl Into<String>) -> Option<String> {
- let version_str = current_version.into();
- let parts: Vec<&str> = version_str.split('.').collect();
-
- if parts.len() != 3 {
- return None;
- }
-
- let major: Result<u32, _> = parts[0].parse();
- let minor: Result<u32, _> = parts[1].parse();
- let patch: Result<u32, _> = parts[2].parse();
-
- if let (Ok(mut major), Ok(mut minor), Ok(mut patch)) = (major, minor, patch) {
- patch += 1;
-
- if patch > 99 {
- patch = 0;
- minor += 1;
-
- if minor > 99 {
- minor = 0;
- major += 1;
- }
- }
-
- Some(format!("{}.{}.{}", major, minor, patch))
- } else {
- None
- }
-}
diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs
deleted file mode 100644
index 29ccd9f..0000000
--- a/src/utils/socket_addr_helper.rs
+++ /dev/null
@@ -1,194 +0,0 @@
-use std::net::SocketAddr;
-use tokio::net::lookup_host;
-
-/// Helper function to parse a string into a SocketAddr with optional default port
-pub async fn get_socket_addr(
- address_str: impl AsRef<str>,
- default_port: u16,
-) -> Result<SocketAddr, std::io::Error> {
- let address = address_str.as_ref().trim();
-
- // Return error if input is empty after trimming
- if address.is_empty() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::InvalidInput,
- "Empty address string",
- ));
- }
-
- // Check if the address contains a port
- if let Some((host, port_str)) = parse_host_and_port(address) {
- let port = port_str.parse::<u16>().map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidInput,
- format!("Invalid port number '{}': {}", port_str, e),
- )
- })?;
-
- return resolve_to_socket_addr(host, port).await;
- }
-
- // No port specified, use default port
- resolve_to_socket_addr(address, default_port).await
-}
-
-/// Parse host and port from address string
-fn parse_host_and_port(address: &str) -> Option<(&str, &str)> {
- if address.starts_with('[')
- && let Some(close_bracket) = address.find(']')
- && close_bracket + 1 < address.len()
- && address.as_bytes()[close_bracket + 1] == b':'
- {
- let host = &address[1..close_bracket];
- let port = &address[close_bracket + 2..];
- return Some((host, port));
- }
-
- // Handle IPv4 addresses and hostnames with ports
- if let Some(colon_pos) = address.rfind(':') {
- // Check if this is not part of an IPv6 address without brackets
- if !address.contains('[') && !address.contains(']') {
- let host = &address[..colon_pos];
- let port = &address[colon_pos + 1..];
-
- // Basic validation to avoid false positives
- if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) {
- return Some((host, port));
- }
- }
- }
-
- None
-}
-
-/// Resolve host to SocketAddr, handling both IP addresses and domain names
-async fn resolve_to_socket_addr(host: &str, port: u16) -> Result<SocketAddr, std::io::Error> {
- // First try to parse as IP address (IPv4 or IPv6)
- if let Ok(ip_addr) = host.parse() {
- return Ok(SocketAddr::new(ip_addr, port));
- }
-
- // If it's not a valid IP address, treat it as a domain name and perform DNS lookup
- let lookup_addr = format!("{}:{}", host, port);
- let mut addrs = lookup_host(&lookup_addr).await?;
-
- if let Some(addr) = addrs.next() {
- Ok(addr)
- } else {
- Err(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- format!("Could not resolve host '{}'", host),
- ))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[tokio::test]
- async fn test_ipv4_with_port() {
- let result = get_socket_addr("127.0.0.1:8080", 80).await;
- assert!(result.is_ok());
- let addr = result.unwrap();
- assert_eq!(addr.port(), 8080);
- assert_eq!(addr.ip().to_string(), "127.0.0.1");
- }
-
- #[tokio::test]
- async fn test_ipv4_without_port() {
- let result = get_socket_addr("192.168.1.1", 443).await;
- assert!(result.is_ok());
- let addr = result.unwrap();
- assert_eq!(addr.port(), 443);
- assert_eq!(addr.ip().to_string(), "192.168.1.1");
- }
-
- #[tokio::test]
- async fn test_ipv6_with_port() {
- let result = get_socket_addr("[::1]:8080", 80).await;
- assert!(result.is_ok());
- let addr = result.unwrap();
- assert_eq!(addr.port(), 8080);
- assert_eq!(addr.ip().to_string(), "::1");
- }
-
- #[tokio::test]
- async fn test_ipv6_without_port() {
- let result = get_socket_addr("[::1]", 443).await;
- assert!(result.is_ok());
- let addr = result.unwrap();
- assert_eq!(addr.port(), 443);
- assert_eq!(addr.ip().to_string(), "::1");
- }
-
- #[tokio::test]
- async fn test_invalid_port() {
- let result = get_socket_addr("127.0.0.1:99999", 80).await;
- assert!(result.is_err());
- }
-
- #[tokio::test]
- async fn test_empty_string() {
- let result = get_socket_addr("", 80).await;
- assert!(result.is_err());
- }
-
- #[tokio::test]
- async fn test_whitespace_trimming() {
- let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await;
- assert!(result.is_ok());
- let addr = result.unwrap();
- assert_eq!(addr.port(), 8080);
- }
-
- #[tokio::test]
- async fn test_domain_name_with_port() {
- // This test will only pass if localhost resolves
- let result = get_socket_addr("localhost:8080", 80).await;
- if result.is_ok() {
- let addr = result.unwrap();
- assert_eq!(addr.port(), 8080);
- // localhost should resolve to 127.0.0.1 or ::1
- assert!(addr.ip().is_loopback());
- }
- }
-
- #[tokio::test]
- async fn test_domain_name_without_port() {
- // This test will only pass if localhost resolves
- let result = get_socket_addr("localhost", 443).await;
- if result.is_ok() {
- let addr = result.unwrap();
- assert_eq!(addr.port(), 443);
- // localhost should resolve to 127.0.0.1 or ::1
- assert!(addr.ip().is_loopback());
- }
- }
-
- #[tokio::test]
- async fn test_parse_host_and_port() {
- // IPv4 with port
- assert_eq!(
- parse_host_and_port("192.168.1.1:8080"),
- Some(("192.168.1.1", "8080"))
- );
-
- // IPv6 with port
- assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080")));
-
- // Hostname with port
- assert_eq!(
- parse_host_and_port("example.com:443"),
- Some(("example.com", "443"))
- );
-
- // No port
- assert_eq!(parse_host_and_port("192.168.1.1"), None);
- assert_eq!(parse_host_and_port("example.com"), None);
-
- // Invalid cases
- assert_eq!(parse_host_and_port(":"), None);
- assert_eq!(parse_host_and_port("192.168.1.1:"), None);
- }
-}
diff --git a/src/utils/workspace_reader.rs b/src/utils/workspace_reader.rs
deleted file mode 100644
index c001330..0000000
--- a/src/utils/workspace_reader.rs
+++ /dev/null
@@ -1,377 +0,0 @@
-use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
-
-use just_enough_vcs::{
- utils::cfg_file::config::ConfigFile,
- vcs::{
- data::{
- local::{
- LocalWorkspace,
- latest_file_data::LatestFileData,
- latest_info::LatestInfo,
- local_sheet::{LocalSheet, LocalSheetData},
- workspace_analyzer::{AnalyzeResult, AnalyzeResultPure},
- workspace_config::LocalConfig,
- },
- member::MemberId,
- sheet::{SheetData, SheetName},
- vault::vault_config::VaultUuid,
- },
- env::current_local_path,
- },
-};
-
-use crate::systems::cmd::errors::CmdPrepareError;
-
-/// Temporarily enter a directory to execute a block of code, then return to the original directory
-macro_rules! entry_dir {
- ($path:expr, $block:block) => {{
- let original_dir = std::env::current_dir().unwrap();
- std::env::set_current_dir($path).unwrap();
- let result = $block;
- std::env::set_current_dir(original_dir).unwrap();
- result
- }};
-}
-
-#[derive(Default)]
-pub struct LocalWorkspaceReader {
- cached_sheet: HashMap<SheetName, SheetData>,
- current_dir: Option<PathBuf>,
- workspace_dir: Option<PathBuf>,
- latest_file_data: HashMap<MemberId, LatestFileData>,
- latest_info: Option<LatestInfo>,
- local_config: Option<LocalConfig>,
- local_sheet_data: HashMap<(MemberId, SheetName), LocalSheetData>,
- local_workspace: Option<LocalWorkspace>,
-}
-
-impl LocalWorkspaceReader {
- // Get the current directory
- pub fn current_dir(&mut self) -> Result<&PathBuf, CmdPrepareError> {
- if self.current_dir.is_none() {
- let current_dir = std::env::current_dir()?;
- self.current_dir = Some(current_dir);
- }
- Ok(self.current_dir.as_ref().unwrap())
- }
-
- // Get the workspace directory
- pub fn workspace_dir(&mut self) -> Result<&PathBuf, CmdPrepareError> {
- if self.workspace_dir.is_none() {
- let workspace_dir = current_local_path();
- self.workspace_dir = workspace_dir;
- return match &self.workspace_dir {
- Some(d) => Ok(d),
- None => Err(CmdPrepareError::LocalWorkspaceNotFound),
- };
- }
- Ok(self.workspace_dir.as_ref().unwrap())
- }
-
- // Get the local configuration
- pub async fn local_config(&mut self) -> Result<&LocalConfig, CmdPrepareError> {
- if self.local_config.is_none() {
- let workspace_dir = self.workspace_dir()?;
- let local_config = entry_dir!(workspace_dir, {
- LocalConfig::read()
- .await
- .map_err(|_| CmdPrepareError::LocalConfigNotFound)?
- });
- self.local_config = Some(local_config)
- }
- Ok(self.local_config.as_ref().unwrap())
- }
-
- // Current account
- pub async fn current_account(&mut self) -> Result<MemberId, CmdPrepareError> {
- Ok(self.local_config().await?.current_account())
- }
-
- // Whether it is in host mode
- pub async fn is_host_mode(&mut self) -> Result<bool, CmdPrepareError> {
- Ok(self.local_config().await?.is_host_mode())
- }
-
- // Whether the workspace is stained
- pub async fn workspace_stained(&mut self) -> Result<bool, CmdPrepareError> {
- Ok(self.local_config().await?.stained())
- }
-
- // Stain UUID
- pub async fn stained_uuid(&mut self) -> Result<Option<VaultUuid>, CmdPrepareError> {
- Ok(self.local_config().await?.stained_uuid())
- }
-
- // Upstream address
- pub async fn upstream_addr(&mut self) -> Result<SocketAddr, CmdPrepareError> {
- Ok(self.local_config().await?.upstream_addr())
- }
-
- // Currently used sheet
- pub async fn sheet_in_use(&mut self) -> Result<&Option<SheetName>, CmdPrepareError> {
- Ok(self.local_config().await?.sheet_in_use())
- }
-
- // Get the sheet name in use, or error if none
- pub async fn sheet_name(&mut self) -> Result<SheetName, CmdPrepareError> {
- match self.local_config().await?.sheet_in_use() {
- Some(name) => Ok(name.clone()),
- None => Err(CmdPrepareError::NoSheetInUse),
- }
- }
-
- // Current draft folder
- pub async fn current_draft_folder(&mut self) -> Result<Option<PathBuf>, CmdPrepareError> {
- Ok(self.local_config().await?.current_draft_folder())
- }
-
- // Get the local workspace
- pub async fn local_workspace(&mut self) -> Result<&LocalWorkspace, CmdPrepareError> {
- if self.local_workspace.is_none() {
- let workspace_dir = self.workspace_dir()?.clone();
- let local_config = self.local_config().await?.clone();
- let Some(local_workspace) = entry_dir!(&workspace_dir, {
- LocalWorkspace::init_current_dir(local_config)
- }) else {
- return Err(CmdPrepareError::LocalWorkspaceNotFound);
- };
- self.local_workspace = Some(local_workspace);
- }
- Ok(self.local_workspace.as_ref().unwrap())
- }
-
- // Get the latest information
- pub async fn latest_info(&mut self) -> Result<&LatestInfo, CmdPrepareError> {
- if self.latest_info.is_none() {
- let local_dir = self.workspace_dir()?.clone();
- let local_config = self.local_config().await?.clone();
- let latest_info = entry_dir!(&local_dir, {
- match LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- {
- Ok(info) => info,
- Err(_) => {
- return Err(CmdPrepareError::LatestInfoNotFound);
- }
- }
- });
- self.latest_info = Some(latest_info);
- }
- Ok(self.latest_info.as_ref().unwrap())
- }
-
- // Get the latest file data
- pub async fn latest_file_data(
- &mut self,
- account: &MemberId,
- ) -> Result<&LatestFileData, CmdPrepareError> {
- if !self.latest_file_data.contains_key(account) {
- let local_dir = self.workspace_dir()?;
- let latest_file_data_path =
- match entry_dir!(&local_dir, { LatestFileData::data_path(account) }) {
- Ok(p) => p,
- Err(_) => return Err(CmdPrepareError::LatestFileDataNotExist(account.clone())),
- };
- let latest_file_data = LatestFileData::read_from(&latest_file_data_path).await?;
- self.latest_file_data
- .insert(account.clone(), latest_file_data);
- }
-
- Ok(self.latest_file_data.get(account).unwrap())
- }
-
- // Get the cached sheet
- pub async fn cached_sheet(
- &mut self,
- sheet_name: &SheetName,
- ) -> Result<&SheetData, CmdPrepareError> {
- if !self.cached_sheet.contains_key(sheet_name) {
- let workspace_dir = self.workspace_dir()?;
- let cached_sheet = entry_dir!(&workspace_dir, {
- match just_enough_vcs::vcs::data::local::cached_sheet::CachedSheet::cached_sheet_data(sheet_name).await {
- Ok(data) => data,
- Err(_) => return Err(CmdPrepareError::CachedSheetNotFound(sheet_name.clone())),
- }
- });
- self.cached_sheet.insert(sheet_name.clone(), cached_sheet);
- }
-
- Ok(self.cached_sheet.get(sheet_name).unwrap())
- }
-
- // Get the local sheet data
- pub async fn local_sheet_data(
- &mut self,
- account: &MemberId,
- sheet_name: &SheetName,
- ) -> Result<&LocalSheetData, CmdPrepareError> {
- let key = (account.clone(), sheet_name.clone());
- if !self.local_sheet_data.contains_key(&key) {
- let workspace_dir = self.workspace_dir()?.clone();
- let local_workspace = self.local_workspace().await?;
- let path = entry_dir!(&workspace_dir, {
- local_workspace.local_sheet_path(account, sheet_name)
- });
- let local_sheet_data = match LocalSheetData::read_from(path).await {
- Ok(data) => data,
- Err(_) => {
- return Err(CmdPrepareError::LocalSheetNotFound(
- account.clone(),
- sheet_name.clone(),
- ));
- }
- };
- self.local_sheet_data.insert(key.clone(), local_sheet_data);
- }
-
- Ok(self.local_sheet_data.get(&key).unwrap())
- }
-
- // Clone and get the local sheet
- pub async fn local_sheet_cloned(
- &mut self,
- account: &MemberId,
- sheet_name: &SheetName,
- ) -> Result<LocalSheet<'_>, CmdPrepareError> {
- let local_sheet_data = self.local_sheet_data(account, sheet_name).await?.clone();
- Ok(LocalSheet::new(
- self.local_workspace().await?,
- account.clone(),
- sheet_name.clone(),
- local_sheet_data,
- ))
- }
-
- // Analyze local status
- pub async fn analyze_local_status(&mut self) -> Result<AnalyzeResultPure, CmdPrepareError> {
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(self.local_workspace().await?).await
- else {
- return Err(CmdPrepareError::LocalStatusAnalyzeFailed);
- };
- Ok(analyzed.into())
- }
-
- // Pop the local configuration (take ownership if cached)
- pub async fn pop_local_config(&mut self) -> Result<LocalConfig, CmdPrepareError> {
- if let Some(local_config) = self.local_config.take() {
- Ok(local_config)
- } else {
- let workspace_dir = self.workspace_dir()?;
- let local_config = entry_dir!(workspace_dir, {
- LocalConfig::read()
- .await
- .map_err(|_| CmdPrepareError::LocalConfigNotFound)?
- });
- Ok(local_config)
- }
- }
-
- // Pop the local workspace (take ownership if cached)
- pub async fn pop_local_workspace(&mut self) -> Result<LocalWorkspace, CmdPrepareError> {
- if let Some(local_workspace) = self.local_workspace.take() {
- Ok(local_workspace)
- } else {
- let workspace_dir = self.workspace_dir()?.clone();
- let local_config = self.local_config().await?.clone();
- let Some(local_workspace) = entry_dir!(&workspace_dir, {
- LocalWorkspace::init_current_dir(local_config)
- }) else {
- return Err(CmdPrepareError::LocalWorkspaceNotFound);
- };
- Ok(local_workspace)
- }
- }
-
- // Pop the latest information (take ownership if cached)
- pub async fn pop_latest_info(&mut self) -> Result<LatestInfo, CmdPrepareError> {
- if let Some(latest_info) = self.latest_info.take() {
- Ok(latest_info)
- } else {
- let local_dir = self.workspace_dir()?.clone();
- let local_config = self.local_config().await?.clone();
- let latest_info = entry_dir!(&local_dir, {
- match LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- {
- Ok(info) => info,
- Err(_) => {
- return Err(CmdPrepareError::LatestInfoNotFound);
- }
- }
- });
- Ok(latest_info)
- }
- }
-
- // Pop the latest file data for a specific account (take ownership if cached)
- pub async fn pop_latest_file_data(
- &mut self,
- account: &MemberId,
- ) -> Result<LatestFileData, CmdPrepareError> {
- if let Some(latest_file_data) = self.latest_file_data.remove(account) {
- Ok(latest_file_data)
- } else {
- let local_dir = self.workspace_dir()?;
- let latest_file_data_path =
- match entry_dir!(&local_dir, { LatestFileData::data_path(account) }) {
- Ok(p) => p,
- Err(_) => return Err(CmdPrepareError::LatestFileDataNotExist(account.clone())),
- };
- let latest_file_data = LatestFileData::read_from(&latest_file_data_path).await?;
- Ok(latest_file_data)
- }
- }
-
- // Pop the cached sheet for a specific sheet name (take ownership if cached)
- pub async fn pop_cached_sheet(
- &mut self,
- sheet_name: &SheetName,
- ) -> Result<SheetData, CmdPrepareError> {
- if let Some(cached_sheet) = self.cached_sheet.remove(sheet_name) {
- Ok(cached_sheet)
- } else {
- let workspace_dir = self.workspace_dir()?;
- let cached_sheet = entry_dir!(&workspace_dir, {
- match just_enough_vcs::vcs::data::local::cached_sheet::CachedSheet::cached_sheet_data(sheet_name).await {
- Ok(data) => data,
- Err(_) => return Err(CmdPrepareError::CachedSheetNotFound(sheet_name.clone())),
- }
- });
- Ok(cached_sheet)
- }
- }
-
- // Pop the local sheet data for a specific account and sheet name (take ownership if cached)
- pub async fn pop_local_sheet_data(
- &mut self,
- account: &MemberId,
- sheet_name: &SheetName,
- ) -> Result<LocalSheetData, CmdPrepareError> {
- let key = (account.clone(), sheet_name.clone());
- if let Some(local_sheet_data) = self.local_sheet_data.remove(&key) {
- Ok(local_sheet_data)
- } else {
- let workspace_dir = self.workspace_dir()?.clone();
- let local_workspace = self.local_workspace().await?;
- let path = entry_dir!(&workspace_dir, {
- local_workspace.local_sheet_path(account, sheet_name)
- });
- let local_sheet_data = match LocalSheetData::read_from(path).await {
- Ok(data) => data,
- Err(_) => {
- return Err(CmdPrepareError::LocalSheetNotFound(
- account.clone(),
- sheet_name.clone(),
- ));
- }
- };
- Ok(local_sheet_data)
- }
- }
-}