diff --git a/src/lib.rs b/src/lib.rs index 39e5363..ccd8054 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use std::path::Path; pub mod strategies; -pub fn process_file(path: &Path, cargo_toml_enabled: bool) -> io::Result<()> { +pub fn process_file(path: &Path, features: Vec) -> io::Result<()> { let mut content = fs::read_to_string(path)?; let ends_with_newline = content.ends_with('\n'); if !ends_with_newline { @@ -13,7 +13,7 @@ pub fn process_file(path: &Path, cargo_toml_enabled: bool) -> io::Result<()> { } let lines: Vec<_> = content.split_inclusive('\n').map(String::from).collect(); - let output_lines = process_lines(classify(path, cargo_toml_enabled), lines)?; + let output_lines = process_lines(classify(path, features), lines)?; let mut writer = BufWriter::new(File::create(path)?); for (i, line) in output_lines.iter().enumerate() { @@ -36,6 +36,7 @@ pub enum Strategy { Generic, Bazel, CargoToml, + Gitignore, } pub fn process_lines(strategy: Strategy, lines: Vec) -> io::Result> { @@ -43,15 +44,24 @@ pub fn process_lines(strategy: Strategy, lines: Vec) -> io::Result crate::strategies::generic::process(lines), Strategy::Bazel => crate::strategies::bazel::process(lines), Strategy::CargoToml => crate::strategies::cargo_toml::process(lines), + Strategy::Gitignore => crate::strategies::gitignore::process(lines), } } -fn classify(path: &Path, cargo_toml_enabled: bool) -> Strategy { - match path { - _ if is_bazel(path) => Strategy::Bazel, - _ if cargo_toml_enabled & is_cargo_toml(path) => Strategy::CargoToml, - _ => Strategy::Generic, +fn classify(path: &Path, features: Vec) -> Strategy { + if is_bazel(path) { + return Strategy::Bazel; } + if features.contains(&"cargo_toml".to_string()) && is_cargo_toml(path) { + return Strategy::CargoToml; + } + if features.contains(&"gitignore".to_string()) && is_gitignore(path) { + return Strategy::Gitignore; + } + if features.contains(&"codeowners".to_string()) && is_codeowners(path) { + return Strategy::Gitignore; + } + Strategy::Generic } fn is_bazel(path: &Path) -> bool { @@ -62,6 +72,13 @@ fn is_bazel(path: &Path) -> bool { } fn is_cargo_toml(path: &Path) -> bool { - // Check if the path is a file and its file name is "Cargo.toml" path.is_file() && path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml")) } + +fn is_gitignore(path: &Path) -> bool { + path.is_file() && path.file_name() == Some(std::ffi::OsStr::new(".gitignore")) +} + +fn is_codeowners(path: &Path) -> bool { + path.is_file() && path.file_name() == Some(std::ffi::OsStr::new("CODEOWNERS")) +} diff --git a/src/main.rs b/src/main.rs index fd373a1..17b9227 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,9 +38,7 @@ fn main() -> io::Result<()> { // Check for experimental features let features = args.features.unwrap_or_default(); - let cargo_toml_enabled = features.contains(&"cargo_toml".to_string()); - - process_file(path, cargo_toml_enabled).map_err(|e| { + process_file(path, features).map_err(|e| { eprintln!( "{}: failed to process file {}: {}", env!("CARGO_PKG_NAME"), diff --git a/src/strategies/gitignore.rs b/src/strategies/gitignore.rs new file mode 100644 index 0000000..b1ec523 --- /dev/null +++ b/src/strategies/gitignore.rs @@ -0,0 +1,72 @@ +use std::io; + +pub(crate) fn process(lines: Vec) -> io::Result> { + let mut output_lines = Vec::new(); + let mut block = Vec::new(); + let mut is_sorting_block = false; + + for line in lines { + if !line.trim().is_empty() { + if is_single_line_comment(&line) { + // Skip opening comment. + output_lines.push(line); + } else { + is_sorting_block = true; + block.push(line); + } + } else if is_sorting_block { + is_sorting_block = false; + block = sort(block); + output_lines.append(&mut block); + output_lines.push(line); + } else { + output_lines.push(line); + } + } + + if is_sorting_block { + block = sort(block); + output_lines.append(&mut block); + } + + Ok(output_lines) +} + +#[derive(Default)] +struct Item { + comment: Vec, + code: String, +} + +/// Sorts a block of lines, keeping associated comments with their items. +fn sort(block: Vec) -> Vec { + let n = block.len(); + let mut items = Vec::with_capacity(n); + let mut current_item = Item::default(); + for line in block { + if is_single_line_comment(&line) { + current_item.comment.push(line); + } else { + items.push(Item { + comment: std::mem::take(&mut current_item.comment), + code: line, + }); + } + } + let trailing_comments = std::mem::take(&mut current_item.comment); + + items.sort_by(|a, b| a.code.cmp(&b.code)); + + let mut result = Vec::with_capacity(n); + for item in items { + result.extend(item.comment); + result.push(item.code); + } + result.extend(trailing_comments); + + result +} + +fn is_single_line_comment(line: &str) -> bool { + line.trim().starts_with('#') +} diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index fbda516..68a3039 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -1,3 +1,4 @@ pub mod bazel; pub mod cargo_toml; pub mod generic; +pub mod gitignore; diff --git a/tests/codeowners.rs b/tests/codeowners.rs new file mode 100644 index 0000000..1438d07 --- /dev/null +++ b/tests/codeowners.rs @@ -0,0 +1,79 @@ +#[macro_use] +mod common; + +use keepsorted::Strategy::Gitignore; + +#[test] +fn codeowners_simple_block() { + test_inner!( + Gitignore, + r#" +/.d/ @company/teams/a +/.c/ @company/teams/b +/.b/workflows @company/teams/c @company/teams/d +/.a/CODEOWNERS @company/teams/e + "#, + r#" +/.a/CODEOWNERS @company/teams/e +/.b/workflows @company/teams/c @company/teams/d +/.c/ @company/teams/b +/.d/ @company/teams/a + "# + ); +} + +#[test] +fn codeowners_two_blocks() { + test_inner!( + Gitignore, + r#" +/.d/ @company/teams/a +/.c/ @company/teams/b + +/.b/workflows @company/teams/c @company/teams/d +/.a/CODEOWNERS @company/teams/e + "#, + r#" +/.c/ @company/teams/b +/.d/ @company/teams/a + +/.a/CODEOWNERS @company/teams/e +/.b/workflows @company/teams/c @company/teams/d + "# + ); +} + +#[test] +fn codeowners_1() { + test_inner!( + Gitignore, + r#" + +# [Misc] +/b +/a + +# [Bazel] +/b +/a + +# [Rust Lang] +/b +/a + "#, + r#" + +# [Misc] +/a +/b + +# [Bazel] +/a +/b + +# [Rust Lang] +/a +/b + "# + ); +} diff --git a/tests/e2e-tests.rs b/tests/e2e-tests.rs index 4fa1ea3..28b33f9 100644 --- a/tests/e2e-tests.rs +++ b/tests/e2e-tests.rs @@ -6,8 +6,8 @@ use tempfile::tempdir; fn run_test(input_file_path: &str, expected_file_path: &str, features: &str) { // Read the input and expected output files let input_content = fs::read_to_string(input_file_path).expect("Failed to read input file"); - let expected_content = - fs::read_to_string(expected_file_path).expect("Failed to read expected file"); + let expected_content = fs::read_to_string(expected_file_path) + .unwrap_or_else(|_| panic!("Failed to read expected file: {}", expected_file_path)); // Create a temporary directory let temp_dir = tempdir().expect("Failed to create temporary directory"); @@ -56,56 +56,40 @@ fn run_test(input_file_path: &str, expected_file_path: &str, features: &str) { ); } +fn dir(path: &str) -> String { + format!("./tests/e2e-tests/{path}") +} + #[test] fn test_e2e_bazel_1() { - run_test( - "./tests/e2e-tests/bazel/1_in.bazel", - "./tests/e2e-tests/bazel/1_out.bazel", - "", - ); + run_test(&dir("bazel/1_in.bazel"), &dir("bazel/1_out.bazel"), ""); } #[test] fn test_e2e_bazel_2() { - run_test( - "./tests/e2e-tests/bazel/2_in.bazel", - "./tests/e2e-tests/bazel/2_out.bazel", - "", - ); + run_test(&dir("bazel/2_in.bazel"), &dir("bazel/2_out.bazel"), ""); } #[test] fn test_e2e_generic_1() { - run_test( - "./tests/e2e-tests/generic/1_in.txt", - "./tests/e2e-tests/generic/1_out.txt", - "", - ); + run_test(&dir("generic/1_in.txt"), &dir("generic/1_out.txt"), ""); } #[test] fn test_e2e_generic_2() { - run_test( - "./tests/e2e-tests/generic/2_in.txt", - "./tests/e2e-tests/generic/2_out.txt", - "", - ); + run_test(&dir("generic/2_in.txt"), &dir("generic/2_out.txt"), ""); } #[test] fn test_e2e_generic_3() { - run_test( - "./tests/e2e-tests/generic/3_in.txt", - "./tests/e2e-tests/generic/3_out.txt", - "", - ); + run_test(&dir("generic/3_in.txt"), &dir("generic/3_out.txt"), ""); } #[test] fn test_e2e_cargo_toml_1() { run_test( - "./tests/e2e-tests/cargo_toml/1/Cargo.toml", - "./tests/e2e-tests/cargo_toml/1/Cargo_out.toml", + &dir("cargo_toml/1/Cargo.toml"), + &dir("cargo_toml/1/Cargo_out.toml"), "cargo_toml", ); } @@ -113,8 +97,26 @@ fn test_e2e_cargo_toml_1() { #[test] fn test_e2e_cargo_toml_2() { run_test( - "./tests/e2e-tests/cargo_toml/2/Cargo.toml", - "./tests/e2e-tests/cargo_toml/2/Cargo_out.toml", + &dir("cargo_toml/2/Cargo.toml"), + &dir("cargo_toml/2/Cargo_out.toml"), "cargo_toml", ); } + +#[test] +fn test_e2e_gitignore_1() { + run_test( + &dir("gitignore/.gitignore"), + &dir("gitignore/.gitignore_out"), + "gitignore", + ); +} + +#[test] +fn test_e2e_codeowners_1() { + run_test( + &dir("codeowners/.github/CODEOWNERS"), + &dir("codeowners/.github/CODEOWNERS_out"), + "codeowners", + ); +} diff --git a/tests/e2e-tests/codeowners/.github/CODEOWNERS b/tests/e2e-tests/codeowners/.github/CODEOWNERS new file mode 100644 index 0000000..066ed41 --- /dev/null +++ b/tests/e2e-tests/codeowners/.github/CODEOWNERS @@ -0,0 +1,10 @@ +b +a + +# [Bazel] +b +a + +# [Rust] +b +a diff --git a/tests/e2e-tests/codeowners/.github/CODEOWNERS_out b/tests/e2e-tests/codeowners/.github/CODEOWNERS_out new file mode 100644 index 0000000..5f3158a --- /dev/null +++ b/tests/e2e-tests/codeowners/.github/CODEOWNERS_out @@ -0,0 +1,10 @@ +a +b + +# [Bazel] +a +b + +# [Rust] +a +b diff --git a/tests/e2e-tests/gitignore/.gitignore b/tests/e2e-tests/gitignore/.gitignore new file mode 100644 index 0000000..9830d2a --- /dev/null +++ b/tests/e2e-tests/gitignore/.gitignore @@ -0,0 +1,10 @@ +/b +/a + +# [Bazel] +/b +/a + +# [Rust] +/b +/a diff --git a/tests/e2e-tests/gitignore/.gitignore_out b/tests/e2e-tests/gitignore/.gitignore_out new file mode 100644 index 0000000..a2a0881 --- /dev/null +++ b/tests/e2e-tests/gitignore/.gitignore_out @@ -0,0 +1,10 @@ +/a +/b + +# [Bazel] +/a +/b + +# [Rust] +/a +/b diff --git a/tests/gitignore.rs b/tests/gitignore.rs new file mode 100644 index 0000000..80e032e --- /dev/null +++ b/tests/gitignore.rs @@ -0,0 +1,37 @@ +#[macro_use] +mod common; + +use keepsorted::Strategy::Gitignore; + +#[test] +fn gitignore_1() { + test_inner!( + Gitignore, + r#" + +/b +/a + +# [Bazel] +/b +/a + +# [Rust] +/b +/a + "#, + r#" + +/a +/b + +# [Bazel] +/a +/b + +# [Rust] +/a +/b + "# + ); +}