Skip to content

Commit cd2a92b

Browse files
authored
feat!: add support for .gitignore resolution (#10)
* implement respecting gitignore * skip beta & nightly builds
1 parent 34a2aa1 commit cd2a92b

File tree

6 files changed

+181
-133
lines changed

6 files changed

+181
-133
lines changed

.github/workflows/pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ jobs:
1515
matrix:
1616
toolchain:
1717
- stable
18-
- beta
19-
- nightly
18+
# - beta
19+
# - nightly
2020
steps:
2121
- uses: actions/checkout@v4
2222
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}

Cargo.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ description = "A cli for tidying up json files."
88
homepage = "https://github.com/todor-a/tidy-json"
99
repository = "https://github.com/todor-a/tidy-json"
1010

11-
[[bin]]
12-
name = "generate_test_files"
13-
path = "src/generate_test_files.rs"
14-
1511
[dependencies]
1612
clap = { version = "4.0", features = ["derive"] }
1713
glob = "0.3"
14+
walkdir = "2.3"
15+
ignore = "0.4"
1816
serde = {version = "1.0", features = ["derive"]}
1917
serde_json = {version = "1.0", features = ["preserve_order"]}
2018
thiserror = "1.0"
@@ -26,6 +24,7 @@ rand = "0.8"
2624

2725
[dev-dependencies]
2826
insta = "1.39.0"
27+
tempfile = "3.2"
2928

3029
# The profile that 'cargo dist' will build with
3130
[profile.dist]

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,22 @@ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/todor-a/tidy-json/relea
1212

1313
```sh
1414
tidy-json **/*.json --write
15+
```
16+
17+
## Options
18+
19+
```
20+
Usage: tidy-json [OPTIONS] <INCLUDE>...
21+
22+
Arguments:
23+
<INCLUDE>... File patterns to process (e.g., *.json)
24+
25+
Options:
26+
-e, --exclude <EXCLUDE> File patterns to exclude (e.g., *.json)
27+
-w, --write Write the sorted JSON back to the input files
28+
-b, --backup Create backups before modifying files
29+
-f, --ignore-git-ignore Whether the files specified in .gitignore should also be sorted
30+
-o, --order <ORDER> Specify the sort order [default: asc] [possible values: asc, desc, rand]
31+
-h, --help Print help
32+
-V, --version Print version
1533
```

src/files.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use glob::Pattern;
4+
use ignore::WalkBuilder;
5+
6+
pub fn list_files<P: AsRef<Path>>(
7+
root: P,
8+
include_patterns: Vec<String>,
9+
exclude_patterns: Option<Vec<String>>,
10+
ignore_git_ignore: bool,
11+
) -> Vec<PathBuf> {
12+
include_patterns
13+
.iter()
14+
.flat_map(|pattern| {
15+
WalkBuilder::new(root.as_ref())
16+
.hidden(false)
17+
.git_ignore(!ignore_git_ignore)
18+
.build()
19+
.filter_map(std::result::Result::ok)
20+
.filter(|entry| {
21+
let path = entry.path();
22+
let relative_path = path.strip_prefix(root.as_ref()).unwrap();
23+
is_json_file(&path.to_path_buf())
24+
&& Pattern::new(pattern).unwrap().matches_path(relative_path)
25+
&& !is_excluded(relative_path, &exclude_patterns)
26+
})
27+
.map(|entry| entry.path().to_path_buf())
28+
.collect::<Vec<_>>()
29+
})
30+
.collect()
31+
}
32+
33+
fn is_excluded(path: &Path, exclude_patterns: &Option<Vec<String>>) -> bool {
34+
match exclude_patterns {
35+
Some(patterns) => patterns
36+
.iter()
37+
.any(|pattern| Pattern::new(pattern).unwrap().matches_path(path)),
38+
None => false,
39+
}
40+
}
41+
42+
pub fn is_json_file(path: &PathBuf) -> bool {
43+
path.extension()
44+
.map(|ext| ext == "json" || ext == "jsonc")
45+
.unwrap_or(false)
46+
}
47+
48+
#[cfg(test)]
49+
mod tests {
50+
use super::*;
51+
use std::fs::{self, File};
52+
use tempfile::TempDir;
53+
54+
#[test]
55+
fn test_is_json_file() {
56+
assert!(is_json_file(&PathBuf::from("test.json")));
57+
assert!(is_json_file(&PathBuf::from("test.jsonc")));
58+
assert!(!is_json_file(&PathBuf::from("test.txt")));
59+
assert!(!is_json_file(&PathBuf::from("test")));
60+
}
61+
62+
#[test]
63+
fn test_list_files() {
64+
let temp_dir = TempDir::new().unwrap();
65+
let temp_path = temp_dir.path();
66+
67+
std::process::Command::new("git")
68+
.args(&["init"])
69+
.current_dir(temp_path)
70+
.output()
71+
.expect("Failed to initialize git repo");
72+
73+
fs::write(temp_path.join(".gitignore"), "ignored.json\n").unwrap();
74+
75+
File::create(temp_path.join("test1.json")).unwrap();
76+
File::create(temp_path.join("test2.jsonc")).unwrap();
77+
File::create(temp_path.join("test3.txt")).unwrap();
78+
File::create(temp_path.join("ignored.json")).unwrap();
79+
80+
let subdir = temp_path.join("subdir");
81+
fs::create_dir(&subdir).unwrap();
82+
File::create(subdir.join("test4.json")).unwrap();
83+
File::create(subdir.join("test5.jsonc")).unwrap();
84+
85+
let files = list_files(
86+
temp_path,
87+
vec!["**/*.json".to_string(), "**/*.jsonc".to_string()],
88+
None,
89+
false,
90+
);
91+
92+
assert_eq!(files.len(), 4);
93+
assert!(files.contains(&temp_path.join("test1.json")));
94+
assert!(files.contains(&temp_path.join("test2.jsonc")));
95+
assert!(files.contains(&temp_path.join("subdir/test4.json")));
96+
assert!(files.contains(&temp_path.join("subdir/test5.jsonc")));
97+
assert!(!files.contains(&temp_path.join("ignored.json")));
98+
99+
let files = list_files(
100+
temp_path,
101+
vec!["**/*.json".to_string(), "**/*.jsonc".to_string()],
102+
None,
103+
true,
104+
);
105+
106+
assert_eq!(files.len(), 5);
107+
assert!(files.contains(&temp_path.join("ignored.json")));
108+
109+
let files = list_files(temp_path, vec!["**/test*.json".to_string()], None, false);
110+
assert_eq!(files.len(), 2);
111+
assert!(files.contains(&temp_path.join("test1.json")));
112+
assert!(files.contains(&temp_path.join("subdir/test4.json")));
113+
114+
let files = list_files(temp_path, vec!["**/ignored.json".to_string()], None, false);
115+
assert_eq!(files.len(), 0);
116+
117+
let files = list_files(
118+
temp_path,
119+
vec!["**/*.json".to_string(), "**/*.jsonc".to_string()],
120+
Some(vec!["**/test2*".to_string()]),
121+
false,
122+
);
123+
assert_eq!(files.len(), 3);
124+
assert!(files.contains(&temp_path.join("test1.json")));
125+
assert!(!files.contains(&temp_path.join("test2.jsonc")));
126+
assert!(files.contains(&temp_path.join("subdir/test4.json")));
127+
assert!(files.contains(&temp_path.join("subdir/test5.jsonc")));
128+
assert!(!files.contains(&temp_path.join("ignored.json")));
129+
130+
let files = list_files(
131+
temp_path,
132+
vec!["**/*.json".to_string(), "**/*.jsonc".to_string()],
133+
Some(vec!["**/test2*".to_string()]),
134+
true,
135+
);
136+
assert_eq!(files.len(), 4);
137+
assert!(files.contains(&temp_path.join("test1.json")));
138+
assert!(!files.contains(&temp_path.join("test2.jsonc")));
139+
assert!(files.contains(&temp_path.join("subdir/test4.json")));
140+
assert!(files.contains(&temp_path.join("subdir/test5.jsonc")));
141+
assert!(files.contains(&temp_path.join("ignored.json")));
142+
}
143+
}

src/generate_test_files.rs

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/main.rs

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use clap::{CommandFactory, Parser, ValueEnum};
22
use colored::*;
3-
use glob::{glob, GlobError};
3+
use glob::GlobError;
44
use log::{debug, error, info};
55
use rand::seq::SliceRandom;
66
use rayon::prelude::*;
@@ -11,6 +11,8 @@ use std::path::PathBuf;
1111
use std::time::Instant;
1212
use thiserror::Error;
1313

14+
mod files;
15+
1416
#[derive(Error, Debug)]
1517
enum CustomError {
1618
#[error("I/O error: {0}")]
@@ -40,7 +42,11 @@ type Result<T> = std::result::Result<T, CustomError>;
4042
struct Args {
4143
/// File patterns to process
4244
#[arg(required = true, help = "File patterns to process (e.g., *.json)")]
43-
patterns: Vec<String>,
45+
include: Vec<String>,
46+
47+
/// File patterns to exclude
48+
#[arg(short, long, help = "File patterns to exclude (e.g., *.json)")]
49+
exclude: Option<Vec<String>>,
4450

4551
/// Write the sorted JSON back to the input files
4652
#[arg(short, long, default_value = "false")]
@@ -50,6 +56,10 @@ struct Args {
5056
#[arg(short, long, default_value = "false")]
5157
backup: bool,
5258

59+
/// Whether the files specified in .gitignore should also be sorted
60+
#[arg(short = 'f', long, default_value = "false")]
61+
ignore_git_ignore: bool,
62+
5363
/// Specify the sort order
5464
#[arg(short = 'o', long, value_enum, default_value = "asc")]
5565
order: SortOrder,
@@ -90,25 +100,13 @@ fn run() -> Result<()> {
90100
let args = Args::parse();
91101
let start_time = Instant::now();
92102

93-
if args.patterns.is_empty() {
103+
if args.include.is_empty() {
94104
return Err(CustomError::CustomError(
95-
"No file patterns provided".to_string(),
105+
"No include file patterns provided".to_string(),
96106
));
97107
}
98108

99-
let files: Vec<PathBuf> = args
100-
.patterns
101-
.iter()
102-
.flat_map(|pattern| {
103-
glob(pattern)
104-
.expect("Failed to read glob pattern")
105-
.filter_map(|entry| match entry {
106-
Ok(path) if is_json_file(&path) => Some(path),
107-
_ => None,
108-
})
109-
.collect::<Vec<_>>()
110-
})
111-
.collect();
109+
let files = files::list_files(".", args.include, args.exclude, args.ignore_git_ignore);
112110

113111
let results: Vec<_> = files
114112
.par_iter()
@@ -154,12 +152,6 @@ fn run() -> Result<()> {
154152
Ok(())
155153
}
156154

157-
fn is_json_file(path: &PathBuf) -> bool {
158-
path.extension()
159-
.map(|ext| ext == "json" || ext == "jsonc")
160-
.unwrap_or(false)
161-
}
162-
163155
fn process_file(path: &PathBuf, write: bool, backup: bool, order: &SortOrder) -> Result<()> {
164156
let data = fs::read_to_string(path)?;
165157
let json: Value = serde_json::from_str(&data)?;
@@ -321,12 +313,4 @@ mod tests {
321313
let json_no_indent = r#"{"key": "value"}"#;
322314
assert_eq!(detect_indent(json_no_indent), None);
323315
}
324-
325-
#[test]
326-
fn test_is_json_file() {
327-
assert!(is_json_file(&PathBuf::from("test.json")));
328-
assert!(is_json_file(&PathBuf::from("test.jsonc")));
329-
assert!(!is_json_file(&PathBuf::from("test.txt")));
330-
assert!(!is_json_file(&PathBuf::from("test")));
331-
}
332316
}

0 commit comments

Comments
 (0)