From 28881a4ec582aaa83a40a23edb80f228102d151c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 18:20:42 +0100 Subject: [PATCH 1/4] fix: create all permutations of depth for tree command --- src/tag_engine.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/tag_engine.rs b/src/tag_engine.rs index 89503ff..db8d6d9 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -95,14 +95,12 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec // Start with existing combinations of length-1 for combo in result.iter().filter(|c| c.len() == len - 1) { - // Try to add each remaining tag that comes after the last tag in combo - if let Some(last) = combo.last() { - for tag in tags { - if tag > last && !combo.contains(tag) { - let mut new_combo = combo.clone(); - new_combo.push(tag.clone()); - temp.push(new_combo); - } + // Try to add each remaining tag + for tag in tags { + if !combo.contains(tag) { + let mut new_combo = combo.clone(); + new_combo.push(tag.clone()); + temp.push(new_combo); } } } From d326e05b8a6b5b6f9d52d5f93f7b39fe058e1116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 18:21:07 +0100 Subject: [PATCH 2/4] style: format src --- src/error.rs | 11 ++++----- src/symlink.rs | 61 ++++++++++++++++++++++------------------------- src/tag_engine.rs | 48 ++++++++++++++++--------------------- 3 files changed, 53 insertions(+), 67 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2a30b64..620bbb9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use std::path::PathBuf; +use thiserror::Error; #[derive(Error, Debug)] pub enum FileTagsError { @@ -27,10 +27,7 @@ pub enum FileTagsError { }, #[error("Failed to parse tags in {file}: {source}")] - Parse { - file: PathBuf, - source: ParseError, - }, + Parse { file: PathBuf, source: ParseError }, #[error("Tag error: {0}")] Tag(#[from] TagError), @@ -71,7 +68,7 @@ mod tests { let tag_err = TagError::Empty; let parse_err = ParseError::InvalidTag(tag_err); let file_err = FileTagsError::from(parse_err); - + assert!(matches!(file_err, FileTagsError::Parse { .. })); } @@ -88,7 +85,7 @@ mod tests { path: PathBuf::from("/test"), source: io_err, }; - + assert!(err.source().is_some()); } } diff --git a/src/symlink.rs b/src/symlink.rs index 1ab47fa..c6ec56f 100644 --- a/src/symlink.rs +++ b/src/symlink.rs @@ -1,18 +1,17 @@ -use std::path::{Path, PathBuf}; -use fs_err as fs; use crate::error::FileTagsError; +use fs_err as fs; +use std::path::{Path, PathBuf}; pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), FileTagsError> { for path in paths { // Ensure path is absolute and clean - let abs_path = fs::canonicalize(&path).map_err(|_| { - FileTagsError::InvalidPath(path.clone()) - })?; - + let abs_path = + fs::canonicalize(&path).map_err(|_| FileTagsError::InvalidPath(path.clone()))?; + // Get the file name for the symlink - let file_name = abs_path.file_name().ok_or_else(|| { - FileTagsError::InvalidPath(abs_path.clone()) - })?; + let file_name = abs_path + .file_name() + .ok_or_else(|| FileTagsError::InvalidPath(abs_path.clone()))?; // Create target directory if it doesn't exist fs::create_dir_all(target_dir).map_err(|e| FileTagsError::CreateDir { @@ -23,17 +22,21 @@ pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), // Create the symlink let link_path = target_dir.join(file_name); #[cfg(unix)] - std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, + std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| { + FileTagsError::CreateLink { + from: abs_path, + to: link_path, + source: e, + } })?; #[cfg(windows)] - std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, + std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| { + FileTagsError::CreateLink { + from: abs_path, + to: link_path, + source: e, + } })?; } @@ -49,22 +52,19 @@ mod tests { fn test_create_symlink_tree_basic() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create a test file let test_file = source_dir.path().join("test.txt"); fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree( - vec![test_file.clone()], - target_dir.path() - )?; + create_symlink_tree(vec![test_file.clone()], target_dir.path())?; // Verify symlink exists and points to correct file let symlink = target_dir.path().join("test.txt"); assert!(symlink.exists()); assert!(symlink.is_symlink()); - + #[cfg(unix)] { use std::os::unix::fs::MetadataExt; @@ -81,7 +81,7 @@ mod tests { fn test_create_symlink_tree_nested() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create nested test file let nested_dir = source_dir.path().join("nested"); fs::create_dir_all(&nested_dir)?; @@ -89,10 +89,7 @@ mod tests { fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree( - vec![test_file], - &target_dir.path().join("nested") - )?; + create_symlink_tree(vec![test_file], &target_dir.path().join("nested"))?; // Verify directory and symlink were created let symlink = target_dir.path().join("nested/test.txt"); @@ -105,12 +102,10 @@ mod tests { #[test] fn test_create_symlink_tree_invalid_path() { let target_dir = TempDir::new().unwrap(); - + // Try to create symlink with non-existent source - let result = create_symlink_tree( - vec![PathBuf::from("/nonexistent/path")], - target_dir.path() - ); + let result = + create_symlink_tree(vec![PathBuf::from("/nonexistent/path")], target_dir.path()); assert!(matches!(result, Err(FileTagsError::InvalidPath(_)))); } diff --git a/src/tag_engine.rs b/src/tag_engine.rs index db8d6d9..25cf985 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -25,7 +25,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse .unwrap_or_else(|| filename.to_string()); let parts: Vec<&str> = file_name.split(TAG_DELIMITER).collect(); - + if parts.len() > 2 { return Err(ParseError::MultipleDelimiters); } @@ -37,10 +37,10 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse _ => String::new(), }; let base_name = base_parts.last().unwrap_or(&parts[0]).to_string(); - + let tags = if parts.len() == 2 { let mut tag_part = parts[1].to_string(); - + // Check if the last tag contains an extension if let Some(last_part) = tag_part.split_whitespace().last() { if let Some(dot_pos) = last_part.rfind('.') { @@ -48,7 +48,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse tag_part.truncate(tag_part.len() - extension.len()); } } - + // First parse all tags let parsed_tags: Vec = tag_part .split_whitespace() @@ -78,7 +78,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec> { let mut result = Vec::new(); - + // Handle empty tags or depth 0 if tags.is_empty() || depth == 0 { return result; @@ -92,7 +92,7 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec // Generate combinations up to specified depth for len in 2..=depth.min(tags.len()) { let mut temp = Vec::new(); - + // Start with existing combinations of length-1 for combo in result.iter().filter(|c| c.len() == len - 1) { // Try to add each remaining tag @@ -111,20 +111,21 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec } pub fn filter_tags(current: Vec, remove: &[String]) -> Vec { - current.into_iter() + current + .into_iter() .filter(|tag| !remove.contains(tag)) .collect() } pub fn add_tags(current: Vec, new: Vec) -> Vec { let mut result = current; - + for tag in new { if !result.contains(&tag) { result.push(tag); } } - + result } @@ -135,7 +136,13 @@ pub fn serialize_tags(base: &str, tags: &[String], extension: &str) -> String { if sorted_tags.is_empty() { format!("{}{}", base, extension) } else { - format!("{}{}{}{}", base, TAG_DELIMITER, sorted_tags.join(" "), extension) + format!( + "{}{}{}{}", + base, + TAG_DELIMITER, + sorted_tags.join(" "), + extension + ) } } @@ -317,32 +324,19 @@ mod tests { fn test_create_tag_combinations_depth_limit() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 2); - + // Should contain individual tags and pairs, but no triples assert!(result.iter().all(|combo| combo.len() <= 2)); - assert_eq!(result.len(), 6); // 3 individual + 3 pairs - } - - #[test] - fn test_create_tag_combinations_order() { - let tags = vec!["tag2".to_string(), "tag1".to_string(), "tag3".to_string()]; - let result = create_tag_combinations(&tags, 2); - - // Check that all combinations maintain alphabetical order - for combo in result { - if combo.len() > 1 { - assert!(combo.windows(2).all(|w| w[0] < w[1])); - } - } + assert_eq!(result.len(), 9); // 3 individual + 6 pairs } #[test] fn test_create_tag_combinations_uniqueness() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 3); - + // Convert to set to check for duplicates let result_set: std::collections::HashSet<_> = result.into_iter().collect(); - assert_eq!(result_set.len(), 7); // 3 individual + 3 pairs + 1 triple + assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple } } From 8238d3d0a8e62a42cc309819d8f6655aa7c4cb56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 18:20:42 +0100 Subject: [PATCH 3/4] fix: create all permutations of depth for tree command --- src/tag_engine.rs | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/tag_engine.rs b/src/tag_engine.rs index 89503ff..b9bee0e 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -95,14 +95,12 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec // Start with existing combinations of length-1 for combo in result.iter().filter(|c| c.len() == len - 1) { - // Try to add each remaining tag that comes after the last tag in combo - if let Some(last) = combo.last() { - for tag in tags { - if tag > last && !combo.contains(tag) { - let mut new_combo = combo.clone(); - new_combo.push(tag.clone()); - temp.push(new_combo); - } + // Try to add each remaining tag + for tag in tags { + if !combo.contains(tag) { + let mut new_combo = combo.clone(); + new_combo.push(tag.clone()); + temp.push(new_combo); } } } @@ -322,20 +320,7 @@ mod tests { // Should contain individual tags and pairs, but no triples assert!(result.iter().all(|combo| combo.len() <= 2)); - assert_eq!(result.len(), 6); // 3 individual + 3 pairs - } - - #[test] - fn test_create_tag_combinations_order() { - let tags = vec!["tag2".to_string(), "tag1".to_string(), "tag3".to_string()]; - let result = create_tag_combinations(&tags, 2); - - // Check that all combinations maintain alphabetical order - for combo in result { - if combo.len() > 1 { - assert!(combo.windows(2).all(|w| w[0] < w[1])); - } - } + assert_eq!(result.len(), 9); // 3 individual + 6 pairs } #[test] @@ -345,6 +330,6 @@ mod tests { // Convert to set to check for duplicates let result_set: std::collections::HashSet<_> = result.into_iter().collect(); - assert_eq!(result_set.len(), 7); // 3 individual + 3 pairs + 1 triple + assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple } } From 57d832adbd3f961f5a255800a4f1859deb2f8e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 18:21:07 +0100 Subject: [PATCH 4/4] style: format src --- src/error.rs | 11 ++++----- src/symlink.rs | 61 ++++++++++++++++++++++------------------------- src/tag_engine.rs | 31 ++++++++++++++---------- 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2a30b64..620bbb9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use std::path::PathBuf; +use thiserror::Error; #[derive(Error, Debug)] pub enum FileTagsError { @@ -27,10 +27,7 @@ pub enum FileTagsError { }, #[error("Failed to parse tags in {file}: {source}")] - Parse { - file: PathBuf, - source: ParseError, - }, + Parse { file: PathBuf, source: ParseError }, #[error("Tag error: {0}")] Tag(#[from] TagError), @@ -71,7 +68,7 @@ mod tests { let tag_err = TagError::Empty; let parse_err = ParseError::InvalidTag(tag_err); let file_err = FileTagsError::from(parse_err); - + assert!(matches!(file_err, FileTagsError::Parse { .. })); } @@ -88,7 +85,7 @@ mod tests { path: PathBuf::from("/test"), source: io_err, }; - + assert!(err.source().is_some()); } } diff --git a/src/symlink.rs b/src/symlink.rs index 1ab47fa..c6ec56f 100644 --- a/src/symlink.rs +++ b/src/symlink.rs @@ -1,18 +1,17 @@ -use std::path::{Path, PathBuf}; -use fs_err as fs; use crate::error::FileTagsError; +use fs_err as fs; +use std::path::{Path, PathBuf}; pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), FileTagsError> { for path in paths { // Ensure path is absolute and clean - let abs_path = fs::canonicalize(&path).map_err(|_| { - FileTagsError::InvalidPath(path.clone()) - })?; - + let abs_path = + fs::canonicalize(&path).map_err(|_| FileTagsError::InvalidPath(path.clone()))?; + // Get the file name for the symlink - let file_name = abs_path.file_name().ok_or_else(|| { - FileTagsError::InvalidPath(abs_path.clone()) - })?; + let file_name = abs_path + .file_name() + .ok_or_else(|| FileTagsError::InvalidPath(abs_path.clone()))?; // Create target directory if it doesn't exist fs::create_dir_all(target_dir).map_err(|e| FileTagsError::CreateDir { @@ -23,17 +22,21 @@ pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), // Create the symlink let link_path = target_dir.join(file_name); #[cfg(unix)] - std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, + std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| { + FileTagsError::CreateLink { + from: abs_path, + to: link_path, + source: e, + } })?; #[cfg(windows)] - std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, + std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| { + FileTagsError::CreateLink { + from: abs_path, + to: link_path, + source: e, + } })?; } @@ -49,22 +52,19 @@ mod tests { fn test_create_symlink_tree_basic() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create a test file let test_file = source_dir.path().join("test.txt"); fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree( - vec![test_file.clone()], - target_dir.path() - )?; + create_symlink_tree(vec![test_file.clone()], target_dir.path())?; // Verify symlink exists and points to correct file let symlink = target_dir.path().join("test.txt"); assert!(symlink.exists()); assert!(symlink.is_symlink()); - + #[cfg(unix)] { use std::os::unix::fs::MetadataExt; @@ -81,7 +81,7 @@ mod tests { fn test_create_symlink_tree_nested() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create nested test file let nested_dir = source_dir.path().join("nested"); fs::create_dir_all(&nested_dir)?; @@ -89,10 +89,7 @@ mod tests { fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree( - vec![test_file], - &target_dir.path().join("nested") - )?; + create_symlink_tree(vec![test_file], &target_dir.path().join("nested"))?; // Verify directory and symlink were created let symlink = target_dir.path().join("nested/test.txt"); @@ -105,12 +102,10 @@ mod tests { #[test] fn test_create_symlink_tree_invalid_path() { let target_dir = TempDir::new().unwrap(); - + // Try to create symlink with non-existent source - let result = create_symlink_tree( - vec![PathBuf::from("/nonexistent/path")], - target_dir.path() - ); + let result = + create_symlink_tree(vec![PathBuf::from("/nonexistent/path")], target_dir.path()); assert!(matches!(result, Err(FileTagsError::InvalidPath(_)))); } diff --git a/src/tag_engine.rs b/src/tag_engine.rs index b9bee0e..25cf985 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -25,7 +25,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse .unwrap_or_else(|| filename.to_string()); let parts: Vec<&str> = file_name.split(TAG_DELIMITER).collect(); - + if parts.len() > 2 { return Err(ParseError::MultipleDelimiters); } @@ -37,10 +37,10 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse _ => String::new(), }; let base_name = base_parts.last().unwrap_or(&parts[0]).to_string(); - + let tags = if parts.len() == 2 { let mut tag_part = parts[1].to_string(); - + // Check if the last tag contains an extension if let Some(last_part) = tag_part.split_whitespace().last() { if let Some(dot_pos) = last_part.rfind('.') { @@ -48,7 +48,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse tag_part.truncate(tag_part.len() - extension.len()); } } - + // First parse all tags let parsed_tags: Vec = tag_part .split_whitespace() @@ -78,7 +78,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec> { let mut result = Vec::new(); - + // Handle empty tags or depth 0 if tags.is_empty() || depth == 0 { return result; @@ -92,7 +92,7 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec // Generate combinations up to specified depth for len in 2..=depth.min(tags.len()) { let mut temp = Vec::new(); - + // Start with existing combinations of length-1 for combo in result.iter().filter(|c| c.len() == len - 1) { // Try to add each remaining tag @@ -111,20 +111,21 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec } pub fn filter_tags(current: Vec, remove: &[String]) -> Vec { - current.into_iter() + current + .into_iter() .filter(|tag| !remove.contains(tag)) .collect() } pub fn add_tags(current: Vec, new: Vec) -> Vec { let mut result = current; - + for tag in new { if !result.contains(&tag) { result.push(tag); } } - + result } @@ -135,7 +136,13 @@ pub fn serialize_tags(base: &str, tags: &[String], extension: &str) -> String { if sorted_tags.is_empty() { format!("{}{}", base, extension) } else { - format!("{}{}{}{}", base, TAG_DELIMITER, sorted_tags.join(" "), extension) + format!( + "{}{}{}{}", + base, + TAG_DELIMITER, + sorted_tags.join(" "), + extension + ) } } @@ -317,7 +324,7 @@ mod tests { fn test_create_tag_combinations_depth_limit() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 2); - + // Should contain individual tags and pairs, but no triples assert!(result.iter().all(|combo| combo.len() <= 2)); assert_eq!(result.len(), 9); // 3 individual + 6 pairs @@ -327,7 +334,7 @@ mod tests { fn test_create_tag_combinations_uniqueness() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 3); - + // Convert to set to check for duplicates let result_set: std::collections::HashSet<_> = result.into_iter().collect(); assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple