Skip to content

Commit b36aebf

Browse files
authored
Add support for --stdin-path flag (#808)
1 parent 6367fb3 commit b36aebf

File tree

2 files changed

+286
-1
lines changed

2 files changed

+286
-1
lines changed

src/main.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use clap::Parser;
44
use ignore::WalkBuilder;
5+
use ignore::gitignore::GitignoreBuilder;
56
use regex::Regex;
67
use rubyfmt::init_logger;
78
use similar::TextDiff;
@@ -61,6 +62,11 @@ struct CommandlineOpts {
6162
#[clap(short, long, name = "in-place")]
6263
in_place: bool,
6364

65+
/// When reading from stdin, treat the input as if it were at this path.
66+
/// This allows .rubyfmtignore and .gitignore patterns to be applied to stdin input.
67+
#[clap(long, name = "stdin-filepath", conflicts_with_all = ["include-paths", "in-place"])]
68+
stdin_filepath: Option<String>,
69+
6470
/// Paths for rubyfmt to analyze. By default the output will be printed to STDOUT. See `--in-place` to write files back in-place.
6571
/// Acceptable paths are:{n}
6672
/// - File paths (i.e lib/foo/bar.rb){n}
@@ -190,6 +196,27 @@ fn rubyfmt_string(
190196
/* Helpers */
191197
/******************************************************/
192198

199+
/// Check if a path should be ignored based on .gitignore and .rubyfmtignore patterns.
200+
/// The path should be relative to the current working directory.
201+
fn is_path_ignored(path: &Path, include_gitignored: bool) -> bool {
202+
let cwd = std::env::current_dir().unwrap();
203+
let mut builder = GitignoreBuilder::new(&cwd);
204+
205+
if !include_gitignored {
206+
builder.add(".gitignore");
207+
}
208+
builder.add(".rubyfmtignore");
209+
210+
if let Ok(gitignore) = builder.build() {
211+
let is_dir = path.is_dir();
212+
gitignore
213+
.matched_path_or_any_parents(path, is_dir)
214+
.is_ignore()
215+
} else {
216+
false
217+
}
218+
}
219+
193220
fn file_walker_builder(include_paths: Vec<&String>, include_gitignored: bool) -> WalkBuilder {
194221
// WalkBuilder does not have an API for adding multiple inputs.
195222
// Must pass the first input to the constructor, and the tail afterwards.
@@ -249,7 +276,22 @@ fn iterate_input_files(opts: &CommandlineOpts, f: &dyn Fn((&Path, &String))) {
249276
io::stdin()
250277
.read_to_string(&mut buffer)
251278
.expect("reading from stdin to not fail");
252-
f((Path::new("stdin"), &buffer))
279+
280+
let path = if let Some(stdin_filepath) = &opts.stdin_filepath {
281+
let path = Path::new(stdin_filepath);
282+
if is_path_ignored(path, opts.include_gitignored) {
283+
// Print unchanged output for ignored files unless we're in check mode
284+
if !opts.check {
285+
puts_stdout(&buffer);
286+
}
287+
return;
288+
}
289+
path
290+
} else {
291+
Path::new("stdin")
292+
};
293+
294+
f((path, &buffer))
253295
} else {
254296
let mut file_paths = Vec::new();
255297
let mut dir_paths = Vec::new();

tests/cli_interface_test.rs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,3 +911,246 @@ fn test_error_uses_rubyfmt_name() {
911911
"Error output should not contain 'rubyfmt-main', got: {stderr}"
912912
);
913913
}
914+
915+
#[test]
916+
fn test_stdin_filepath_respects_rubyfmtignore() {
917+
let dir = tempdir().unwrap();
918+
fs::write(dir.path().join(".rubyfmtignore"), "ignored.rb").unwrap();
919+
920+
// File matching .rubyfmtignore pattern should not be formatted, but output unchanged
921+
Command::cargo_bin("rubyfmt-main")
922+
.unwrap()
923+
.current_dir(dir.path())
924+
.arg("--stdin-filepath")
925+
.arg("ignored.rb")
926+
.write_stdin("a 1,2,3")
927+
.assert()
928+
.stdout("a 1,2,3")
929+
.code(0)
930+
.success();
931+
932+
// File not matching .rubyfmtignore pattern should be formatted
933+
Command::cargo_bin("rubyfmt-main")
934+
.unwrap()
935+
.current_dir(dir.path())
936+
.arg("--stdin-filepath")
937+
.arg("not_ignored.rb")
938+
.write_stdin("a 1,2,3")
939+
.assert()
940+
.stdout("a(1, 2, 3)\n")
941+
.code(0)
942+
.success();
943+
}
944+
945+
#[test]
946+
fn test_stdin_filepath_respects_gitignore() {
947+
let dir = tempdir().unwrap();
948+
// Fake a git repo
949+
create_dir(dir.path().join(".git")).unwrap();
950+
fs::write(dir.path().join(".gitignore"), "ignored.rb").unwrap();
951+
952+
// File matching .gitignore pattern should not be formatted, but output unchanged
953+
Command::cargo_bin("rubyfmt-main")
954+
.unwrap()
955+
.current_dir(dir.path())
956+
.arg("--stdin-filepath")
957+
.arg("ignored.rb")
958+
.write_stdin("a 1,2,3")
959+
.assert()
960+
.stdout("a 1,2,3")
961+
.code(0)
962+
.success();
963+
964+
// File matching .gitignore but with --include-gitignored should be formatted
965+
Command::cargo_bin("rubyfmt-main")
966+
.unwrap()
967+
.current_dir(dir.path())
968+
.arg("--stdin-filepath")
969+
.arg("ignored.rb")
970+
.arg("--include-gitignored")
971+
.write_stdin("a 1,2,3")
972+
.assert()
973+
.stdout("a(1, 2, 3)\n")
974+
.code(0)
975+
.success();
976+
}
977+
978+
#[test]
979+
fn test_stdin_filepath_uses_path_in_check_output() {
980+
let dir = tempdir().unwrap();
981+
982+
Command::cargo_bin("rubyfmt-main")
983+
.unwrap()
984+
.current_dir(dir.path())
985+
.arg("--check")
986+
.arg("--stdin-filepath")
987+
.arg("lib/foo.rb")
988+
.write_stdin("a 1,2,3\n")
989+
.assert()
990+
.stdout(
991+
"--- lib/foo.rb
992+
+++ lib/foo.rb
993+
@@ -1 +1 @@
994+
-a 1,2,3
995+
+a(1, 2, 3)
996+
",
997+
)
998+
.code(5)
999+
.failure();
1000+
}
1001+
1002+
#[test]
1003+
fn test_stdin_filepath_respects_rubyfmtignore_directory_pattern() {
1004+
let dir = tempdir().unwrap();
1005+
fs::write(dir.path().join(".rubyfmtignore"), "ignored/").unwrap();
1006+
1007+
// File in ignored directory should not be formatted, but output unchanged
1008+
Command::cargo_bin("rubyfmt-main")
1009+
.unwrap()
1010+
.current_dir(dir.path())
1011+
.arg("--stdin-filepath")
1012+
.arg("ignored/foo.rb")
1013+
.write_stdin("a 1,2,3")
1014+
.assert()
1015+
.stdout("a 1,2,3")
1016+
.code(0)
1017+
.success();
1018+
1019+
// File not in ignored directory should be formatted
1020+
Command::cargo_bin("rubyfmt-main")
1021+
.unwrap()
1022+
.current_dir(dir.path())
1023+
.arg("--stdin-filepath")
1024+
.arg("not_ignored/foo.rb")
1025+
.write_stdin("a 1,2,3")
1026+
.assert()
1027+
.stdout("a(1, 2, 3)\n")
1028+
.code(0)
1029+
.success();
1030+
}
1031+
1032+
#[test]
1033+
fn test_stdin_filepath_respects_gitignore_directory_pattern() {
1034+
let dir = tempdir().unwrap();
1035+
create_dir(dir.path().join(".git")).unwrap();
1036+
fs::write(dir.path().join(".gitignore"), "ignored/").unwrap();
1037+
1038+
// File in ignored directory should not be formatted, but output unchanged
1039+
Command::cargo_bin("rubyfmt-main")
1040+
.unwrap()
1041+
.current_dir(dir.path())
1042+
.arg("--stdin-filepath")
1043+
.arg("ignored/foo.rb")
1044+
.write_stdin("a 1,2,3")
1045+
.assert()
1046+
.stdout("a 1,2,3")
1047+
.code(0)
1048+
.success();
1049+
1050+
// File in ignored directory with --include-gitignored should be formatted
1051+
Command::cargo_bin("rubyfmt-main")
1052+
.unwrap()
1053+
.current_dir(dir.path())
1054+
.arg("--stdin-filepath")
1055+
.arg("ignored/foo.rb")
1056+
.arg("--include-gitignored")
1057+
.write_stdin("a 1,2,3")
1058+
.assert()
1059+
.stdout("a(1, 2, 3)\n")
1060+
.code(0)
1061+
.success();
1062+
}
1063+
1064+
#[test]
1065+
fn test_stdin_filepath_check_mode_ignored_file() {
1066+
let dir = tempdir().unwrap();
1067+
fs::write(dir.path().join(".rubyfmtignore"), "ignored.rb").unwrap();
1068+
1069+
// Check mode with ignored file should produce no output and exit 0
1070+
Command::cargo_bin("rubyfmt-main")
1071+
.unwrap()
1072+
.current_dir(dir.path())
1073+
.arg("--check")
1074+
.arg("--stdin-filepath")
1075+
.arg("ignored.rb")
1076+
.write_stdin("a 1,2,3")
1077+
.assert()
1078+
.stdout("")
1079+
.code(0)
1080+
.success();
1081+
}
1082+
1083+
#[test]
1084+
fn test_stdin_filepath_conflicts_with_file_paths() {
1085+
let mut file = NamedTempFile::new().unwrap();
1086+
writeln!(file, "a 1,2,3").unwrap();
1087+
1088+
let output = Command::cargo_bin("rubyfmt-main")
1089+
.unwrap()
1090+
.arg("--stdin-filepath")
1091+
.arg("lib/foo.rb")
1092+
.arg(file.path())
1093+
.output()
1094+
.unwrap();
1095+
1096+
assert!(!output.status.success());
1097+
let stderr = String::from_utf8_lossy(&output.stderr);
1098+
assert!(
1099+
stderr.contains("--stdin-filepath") && stderr.contains("cannot be used with"),
1100+
"Expected error about --stdin-filepath conflict, got: {}",
1101+
stderr
1102+
);
1103+
}
1104+
1105+
#[test]
1106+
fn test_stdin_filepath_conflicts_with_in_place() {
1107+
let output = Command::cargo_bin("rubyfmt-main")
1108+
.unwrap()
1109+
.arg("--stdin-filepath")
1110+
.arg("lib/foo.rb")
1111+
.arg("-i")
1112+
.write_stdin("a 1,2,3")
1113+
.output()
1114+
.unwrap();
1115+
1116+
assert!(!output.status.success());
1117+
let stderr = String::from_utf8_lossy(&output.stderr);
1118+
assert!(
1119+
stderr.contains("--stdin-filepath") && stderr.contains("cannot be used with"),
1120+
"Expected error about --stdin-filepath conflict with -i, got: {}",
1121+
stderr
1122+
);
1123+
}
1124+
1125+
#[test]
1126+
fn test_explicit_file_path_ignores_rubyfmtignore() {
1127+
// When a file is passed explicitly (not via directory traversal),
1128+
// ignore patterns should not apply - the user explicitly asked for this file.
1129+
let dir = tempdir().unwrap();
1130+
1131+
let mut file = tempfile::Builder::new()
1132+
.prefix("rubyfmt")
1133+
.suffix(".rb")
1134+
.tempfile_in(dir.path())
1135+
.unwrap();
1136+
writeln!(file, "a 1,2,3").unwrap();
1137+
1138+
fs::write(
1139+
dir.path().join(".rubyfmtignore"),
1140+
file.path().file_name().unwrap().to_str().unwrap(),
1141+
)
1142+
.unwrap();
1143+
1144+
Command::cargo_bin("rubyfmt-main")
1145+
.unwrap()
1146+
.current_dir(dir.path())
1147+
.arg("-i")
1148+
.arg(file.path())
1149+
.assert()
1150+
.stdout("")
1151+
.code(0)
1152+
.success();
1153+
1154+
// File should be formatted despite being in .rubyfmtignore
1155+
assert_eq!("a(1, 2, 3)\n", read_to_string(file.path()).unwrap());
1156+
}

0 commit comments

Comments
 (0)