diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..44d0d5d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @timothebot diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 5713c5e..49a4ef6 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -59,4 +59,4 @@ jobs: - name: doesn't detect powershell if: ${{ matrix.os != 'macos-14' }} shell: bash - run: cargo test --release -- --ignored is_powershell_false \ No newline at end of file + run: cargo test --release -- --ignored is_powershell_false diff --git a/Cargo.lock b/Cargo.lock index 38d711c..19c2e0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "anyhow", "env_logger", "log", + "regex", "serde", "shellexpand", "tempfile", @@ -440,9 +441,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -452,9 +453,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", diff --git a/README.md b/README.md index a0a0d9e..a5da0dc 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,4 @@ executes or lines getting replaced in a target file. - [ ] Path support - [ ] Improve readme - [ ] Variables support + - [ ] Input variables (ask for variable or command or something) diff --git a/book/src/configuration/brick_config.md b/book/src/configuration/brick_config.md index e4c8d1d..4cb706f 100644 --- a/book/src/configuration/brick_config.md +++ b/book/src/configuration/brick_config.md @@ -84,8 +84,8 @@ Allows you to run a command or a script file. [[actions]] action = "run_command" -command = "echo 'hi'" # command or a script file (prefix with file:) +command = "echo 'hi'" ``` This is by far the most simple yet powerful action. -If you need more complex behaviour, you can add a custom script that does what you need. +If you need more complex behavior, you can add a custom script that does what you need. diff --git a/book/src/intro.md b/book/src/intro.md index 60d1efa..86c4c12 100644 --- a/book/src/intro.md +++ b/book/src/intro.md @@ -14,7 +14,7 @@ Instead of having to look up and copy your desired license from the web, you can You can create multiple bricks and combine them behind an alias. -This way, you can easily bootstrap new projects! +This way, you can easily bootstrap new projects! ```shell $ cargo new my_project && cd my_project @@ -26,4 +26,4 @@ $ crane add rust • rustfmt • rustauthor # ... -``` \ No newline at end of file +``` diff --git a/crane/src/cmd/commands.rs b/crane/src/cmd/commands.rs index b00174c..26902b8 100644 --- a/crane/src/cmd/commands.rs +++ b/crane/src/cmd/commands.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::{Parser, Subcommand, ValueHint, command}; +use clap::{Parser, Subcommand, ValueHint}; use clap_verbosity::{InfoLevel, Verbosity}; #[derive(Debug, Parser)] diff --git a/crane_bricks/Cargo.toml b/crane_bricks/Cargo.toml index 468441d..b374a85 100644 --- a/crane_bricks/Cargo.toml +++ b/crane_bricks/Cargo.toml @@ -12,6 +12,7 @@ toml = "0.9.6" anyhow = "1.0.99" log = "0.4.28" shellexpand = "3.1.1" +regex = "1.12.2" [dev-dependencies] tempfile = "3" diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index 36eaedb..5a313f0 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -1,8 +1,10 @@ use anyhow::{Ok, anyhow}; +use regex::Regex; use serde::Deserialize; use crate::{ actions::{ExecuteAction, common::Common}, + brick::BrickFile, file_utils::{file_read_content, file_replace_content}, }; @@ -61,16 +63,65 @@ enum ModifyType { Replace, } +fn modify_content_using_regex( + source_text: String, + selector: &str, + content: String, + modify_type: &ModifyType, +) -> anyhow::Result { + let re = Regex::new(selector)?; + match modify_type { + ModifyType::Append => Ok(re + .replace_all(&source_text, format!("$0{}", content)) + .to_string()), + ModifyType::Prepend => Ok(re + .replace_all(&source_text, format!("{}$0", content)) + .to_string()), + ModifyType::Replace => Ok(re.replace_all(&source_text, content).to_string()), + } +} + impl ModifyFileAction { - pub fn content(&self) -> String { - // TODO: Get content from somewhere else if not set - self.content.clone().unwrap_or_default() + pub fn content(&self, brick: &crate::brick::Brick) -> anyhow::Result { + let content = self.content.clone().unwrap_or_default(); + if let Some(file) = content.strip_prefix("file:") { + let content_file: Vec = brick + .files() + .into_iter() + .filter(|f| f.name() == file) + .collect(); + if content_file.len() != 1 { + return Err(anyhow!( + "The file '{}' that was defined for taking the content from does not exis, or multiple results found. (Found {})", + file, + content_file.len() + )); + } + return Ok(content_file.first().unwrap().content().to_string()); + }; + Ok(content) } - pub fn modify_content(&self, source_text: String) -> anyhow::Result { + pub fn modify_content( + &self, + brick: &crate::brick::Brick, + source_text: String, + ) -> anyhow::Result { // TODO: Handle regex // TODO: insert for all or just one? + let content = &self.content(brick)?; + + if self.selector.starts_with("regex:") { + info!("Modifying content with regex selector."); + return modify_content_using_regex( + source_text, + self.selector.strip_prefix("regex:").unwrap_or_default(), + content.to_string(), + &self.r#type, + ); + } + let locations: Vec<(usize, &str)> = source_text.match_indices(&self.selector).collect(); @@ -92,29 +143,23 @@ impl ModifyFileAction { let modified_index = index + output.len().abs_diff(start_length); match &self.r#type { ModifyType::Append => { - output.insert_str( - (modified_index + selected.len()).max(0), - &self.content(), - ); + output.insert_str((modified_index + selected.len()).max(0), content); } ModifyType::Prepend => { - output.insert_str(modified_index, &self.content()); + output.insert_str(modified_index, content); } ModifyType::Replace => { - // TODO: Something isnt right here but im so tired rn pls - // future tiimo fix this debug!( - "replacing from {} to {} (total chars {})", + "Replacing from {} to {}", modified_index, - (modified_index + selected.len()).max(0), - output.len() + (modified_index + selected.len()).max(0) ); if modified_index > output.len() { - output.insert_str(output.len(), &self.content()); + output.insert_str(output.len(), content); } else { output.replace_range( modified_index..(modified_index + selected.len()), - &self.content(), + content, ); } } @@ -142,12 +187,7 @@ impl ExecuteAction for ModifyFileAction { brick: &crate::brick::Brick, cwd: &std::path::Path, ) -> anyhow::Result<()> { - let mut files: Vec = brick - .files() - .iter() - .map(|brick_file| brick_file.name().to_string()) - .collect(); - files.extend(self.common.sources.clone()); + let files: Vec = self.common.sources.clone(); for file in files { let target_path = cwd.join(file); if !target_path.exists() { @@ -155,7 +195,11 @@ impl ExecuteAction for ModifyFileAction { } info!("Modifying file '{}'", target_path.display()); let content = file_read_content(context, &target_path)?; - file_replace_content(context, &target_path, &self.modify_content(content)?)?; + file_replace_content( + context, + &target_path, + &self.modify_content(brick, content)?, + )?; } Ok(()) } diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index 15cbad3..3376f5b 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use crate::{ actions::{Action, ExecuteAction, insert_file::InsertFileAction}, context::ActionContext, - file_utils::{sub_dirs, sub_paths}, + file_utils::{all_file_paths, sub_dirs}, }; const BRICK_CONFIG_FILE: &str = "brick.toml"; @@ -100,15 +100,17 @@ impl Brick { Ok(()) } - /// Returns a list of all files that + /// Returns a list of all files that are in the brick dir pub fn files(&self) -> Vec { - let Ok(paths) = sub_paths(self.path()) else { + let Ok(paths) = all_file_paths(self.path()) else { return vec![]; }; + + let brick_path = format!("{}/", self.path().display()); paths .iter() .filter_map(|path| { - let name = path.file_name()?.display().to_string(); + let name = path.display().to_string().replace(&brick_path, ""); if !path.is_file() || name == BRICK_CONFIG_FILE { return None; } diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index afdaf90..226c4bf 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -1,5 +1,5 @@ use std::{ - fs::{self, File}, + fs::{self, File, create_dir_all}, io::{self, Write}, path::{Path, PathBuf}, }; @@ -15,7 +15,27 @@ pub fn sub_dirs(dir: &Path) -> anyhow::Result> { .collect::>()) } -/// Get a vec of all files and folders in the given dir if valid +/// Returns the path to every file located in the given dir, including +/// paths in subdirectories +pub fn all_file_paths(dir: &Path) -> anyhow::Result> { + let dir = PathBuf::from(shellexpand::tilde(&dir.display().to_string()).to_string()); + if !dir.exists() || !dir.is_dir() { + return Err(anyhow!("Target does not exist or not a directory")); + } + let mut paths: Vec = Vec::new(); + if let Ok(children) = sub_paths(dir.as_path()) { + for path in children { + if path.is_dir() { + paths.append(&mut all_file_paths(path.as_path()).unwrap()); + } else if path.is_file() { + paths.push(path); + } + } + } + Ok(paths) +} + +/// Returns a vec of all files and folders in the given dir (without subdirs) if valid pub fn sub_paths(dir: &Path) -> anyhow::Result> { let dir = PathBuf::from(shellexpand::tilde(&dir.display().to_string()).to_string()); if !dir.exists() || !dir.is_dir() { @@ -32,11 +52,23 @@ pub fn file_create_new( path: &Path, content: Option, ) -> anyhow::Result<()> { - if !ctx.dry_run { - debug!("Creating new file '{:?}'", path); - let mut file = File::create_new(path)?; - file.write_all(content.unwrap_or_default().as_bytes())?; + if ctx.dry_run { + return Ok(()); + } + debug!("Creating new file '{:?}'", path); + if let Some(parent_dir) = path.parent() { + if !parent_dir.exists() { + debug!("Adding all missing parent dir(s)."); + match create_dir_all(parent_dir) { + Ok(_) => {} + Err(_) => return Err(anyhow!("Couldn't create parent dir(s)!")), + } + } + } else { + debug!("Parent dirs exists"); } + let mut file = File::create_new(path)?; + file.write_all(content.unwrap_or_default().as_bytes())?; Ok(()) } diff --git a/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_C1 b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_C1 new file mode 100644 index 0000000..e2cf5e7 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_C1 @@ -0,0 +1 @@ +C1 diff --git a/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_DONT b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_DONT new file mode 100644 index 0000000..988f732 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/TEST_DONT @@ -0,0 +1 @@ +This file should not exist at target location diff --git a/crane_bricks/tests/bricks/insert_multiple_with_subfolder/brick.toml b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/brick.toml new file mode 100644 index 0000000..44376cd --- /dev/null +++ b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/brick.toml @@ -0,0 +1,9 @@ +name = "test" + +[[actions]] +action = "insert_file" +if_file_exists = "replace" +sources = [ + "TEST_C1", + "sub/TEST_C2" +] diff --git a/crane_bricks/tests/bricks/insert_multiple_with_subfolder/sub/TEST_C2 b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/sub/TEST_C2 new file mode 100644 index 0000000..c4b2d41 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_multiple_with_subfolder/sub/TEST_C2 @@ -0,0 +1 @@ +C2 diff --git a/crane_bricks/tests/bricks/insert_no_config/TEST_B b/crane_bricks/tests/bricks/insert_no_config/TEST_B index 5e1c309..557db03 100644 --- a/crane_bricks/tests/bricks/insert_no_config/TEST_B +++ b/crane_bricks/tests/bricks/insert_no_config/TEST_B @@ -1 +1 @@ -Hello World \ No newline at end of file +Hello World diff --git a/crane_bricks/tests/bricks/insert_with_config/TEST_A b/crane_bricks/tests/bricks/insert_with_config/TEST_A index 3b12464..2a02d41 100644 --- a/crane_bricks/tests/bricks/insert_with_config/TEST_A +++ b/crane_bricks/tests/bricks/insert_with_config/TEST_A @@ -1 +1 @@ -TEST \ No newline at end of file +TEST diff --git a/crane_bricks/tests/bricks/modify_append_regex/brick.toml b/crane_bricks/tests/bricks/modify_append_regex/brick.toml new file mode 100644 index 0000000..acdc1dc --- /dev/null +++ b/crane_bricks/tests/bricks/modify_append_regex/brick.toml @@ -0,0 +1,8 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "append" +content = "\nserde = \"1\"" +selector = "regex:\\[[a-z]+\\]" diff --git a/crane_bricks/tests/bricks/modify_prepend_regex/brick.toml b/crane_bricks/tests/bricks/modify_prepend_regex/brick.toml new file mode 100644 index 0000000..86be927 --- /dev/null +++ b/crane_bricks/tests/bricks/modify_prepend_regex/brick.toml @@ -0,0 +1,8 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "prepend" +content = "# This is group $group\n" +selector = "regex:\\[(?[a-z]+)\\]" diff --git a/crane_bricks/tests/bricks/modify_replace_regex/brick.toml b/crane_bricks/tests/bricks/modify_replace_regex/brick.toml new file mode 100644 index 0000000..223eb83 --- /dev/null +++ b/crane_bricks/tests/bricks/modify_replace_regex/brick.toml @@ -0,0 +1,8 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "replace" +content = "[dev-$group]" +selector = "regex:\\[(?d[a-z]+)\\]" diff --git a/crane_bricks/tests/bricks/modify_with_file_content/brick.toml b/crane_bricks/tests/bricks/modify_with_file_content/brick.toml new file mode 100644 index 0000000..688e9f5 --- /dev/null +++ b/crane_bricks/tests/bricks/modify_with_file_content/brick.toml @@ -0,0 +1,8 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "replace" +content = "file:sub/content" +selector = "[dependencies]" diff --git a/crane_bricks/tests/bricks/modify_with_file_content/sub/content b/crane_bricks/tests/bricks/modify_with_file_content/sub/content new file mode 100644 index 0000000..7644c1e --- /dev/null +++ b/crane_bricks/tests/bricks/modify_with_file_content/sub/content @@ -0,0 +1 @@ +NEW_CONTENT diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs index 053466f..b363584 100644 --- a/crane_bricks/tests/integration_test.rs +++ b/crane_bricks/tests/integration_test.rs @@ -60,6 +60,22 @@ fn test_insert_file() { assert!(!tmpdir.path().join("brick.toml").exists()); } +#[test] +fn test_insert_files_with_sub() { + init_logger(); + + let brick = Brick::try_from(brick_dir("insert_multiple_with_subfolder")).unwrap(); + debug!("{:?}", brick); + + let ctx = ActionContext { dry_run: false }; + let tmpdir = tempfile::tempdir().unwrap(); + brick.execute(&ctx, tmpdir.path()).unwrap(); + assert!(tmpdir.path().join("TEST_C1").exists()); + assert!(tmpdir.path().join("sub/TEST_C2").exists()); + assert!(!tmpdir.path().join("TEST_DONT").exists()); + assert!(!tmpdir.path().join("brick.toml").exists()); +} + #[test] fn test_without_config() { init_logger(); @@ -91,6 +107,22 @@ fn test_modify_append() { assert!(res_content.contains("[dependencies]\nserde = \"1\"\n")) } +#[test] +fn test_modify_append_with_regex() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_append_regex")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(res_content.contains("[dependencies]\nserde = \"1\"\n")) +} + #[test] fn test_modify_prepend() { init_logger(); @@ -107,6 +139,23 @@ fn test_modify_prepend() { assert!(res_content.contains("serde = \"1\"\n[dependencies]")) } +#[test] +fn test_modify_prepend_regex() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_prepend_regex")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(res_content.contains("# This is group dependencies\n[dependencies]")); + assert!(res_content.contains("# This is group package\n[package]")); +} + #[test] fn test_modify_replace() { init_logger(); @@ -124,6 +173,40 @@ fn test_modify_replace() { assert!(res_content.contains("[dev-dependencies]")); } +#[test] +fn test_modify_replace_regex() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_replace")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(!res_content.contains("[dependencies]")); + assert!(res_content.contains("[dev-dependencies]")); +} + +#[test] +fn test_modify_with_file_content() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_with_file_content")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(!res_content.contains("[dependencies]")); + assert!(res_content.contains("NEW_CONTENT\n")); +} + #[test] fn test_command() { init_logger(); diff --git a/example/bricks/serde/brick.toml b/example/bricks/serde/brick.toml index 768563e..a1ee91e 100644 --- a/example/bricks/serde/brick.toml +++ b/example/bricks/serde/brick.toml @@ -17,4 +17,4 @@ type = "replace" # Specify which files this action applies to sources = [ "LICENSE" -] \ No newline at end of file +] diff --git a/example/foo/other_bricks/smile/smile.txt b/example/foo/other_bricks/smile/smile.txt index 93c710d..337c56e 100644 --- a/example/foo/other_bricks/smile/smile.txt +++ b/example/foo/other_bricks/smile/smile.txt @@ -1 +1 @@ -:D \ No newline at end of file +:D diff --git a/rustfmt.toml b/rustfmt.toml index 8518ab0..26ba63e 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -max_width = 90 \ No newline at end of file +max_width = 90