Skip to content

Commit

Permalink
feat: add experimental support for .gitignore and CODEOWNERS (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
maksym-arutyunyan authored Aug 8, 2024
1 parent e5d6bfb commit 3bc40cc
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 42 deletions.
33 changes: 25 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> io::Result<()> {
let mut content = fs::read_to_string(path)?;
let ends_with_newline = content.ends_with('\n');
if !ends_with_newline {
Expand All @@ -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() {
Expand All @@ -36,22 +36,32 @@ pub enum Strategy {
Generic,
Bazel,
CargoToml,
Gitignore,
}

pub fn process_lines(strategy: Strategy, lines: Vec<String>) -> io::Result<Vec<String>> {
match strategy {
Strategy::Generic => 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<String>) -> 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 {
Expand All @@ -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"))
}
4 changes: 1 addition & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
72 changes: 72 additions & 0 deletions src/strategies/gitignore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::io;

pub(crate) fn process(lines: Vec<String>) -> io::Result<Vec<String>> {
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<String>,
code: String,
}

/// Sorts a block of lines, keeping associated comments with their items.
fn sort(block: Vec<String>) -> Vec<String> {
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('#')
}
1 change: 1 addition & 0 deletions src/strategies/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod bazel;
pub mod cargo_toml;
pub mod generic;
pub mod gitignore;
79 changes: 79 additions & 0 deletions tests/codeowners.rs
Original file line number Diff line number Diff line change
@@ -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
"#
);
}
64 changes: 33 additions & 31 deletions tests/e2e-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -56,65 +56,67 @@ 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",
);
}

#[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",
);
}
10 changes: 10 additions & 0 deletions tests/e2e-tests/codeowners/.github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
b
a

# [Bazel]
b
a

# [Rust]
b
a
10 changes: 10 additions & 0 deletions tests/e2e-tests/codeowners/.github/CODEOWNERS_out
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
a
b

# [Bazel]
a
b

# [Rust]
a
b
10 changes: 10 additions & 0 deletions tests/e2e-tests/gitignore/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/b
/a

# [Bazel]
/b
/a

# [Rust]
/b
/a
10 changes: 10 additions & 0 deletions tests/e2e-tests/gitignore/.gitignore_out
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/a
/b

# [Bazel]
/a
/b

# [Rust]
/a
/b
Loading

0 comments on commit 3bc40cc

Please sign in to comment.