diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 19c4f766d13fe..3e96ebb1558f2 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -527,6 +527,10 @@ pub struct FormatCommand { /// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported. #[clap(long, help_heading = "Editor options", verbatim_doc_comment)] pub range: Option, + + /// Exit with a non-zero status code if any files were modified via format, even if all files were formatted successfully. + #[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")] + pub exit_non_zero_on_format: bool, } #[derive(Copy, Clone, Debug, clap::Parser)] @@ -762,6 +766,7 @@ impl FormatCommand { no_cache: self.no_cache, stdin_filename: self.stdin_filename, range: self.range, + exit_non_zero_on_format: self.exit_non_zero_on_format, }; let cli_overrides = ExplicitConfigOverrides { @@ -1046,6 +1051,7 @@ pub struct FormatArguments { pub files: Vec, pub stdin_filename: Option, pub range: Option, + pub exit_non_zero_on_format: bool, } /// A text range specified by line and column numbers. diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 5a1c4f97875b8..77eb772c51822 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -213,7 +213,11 @@ pub(crate) fn format( match mode { FormatMode::Write => { if errors.is_empty() { - Ok(ExitStatus::Success) + if cli.exit_non_zero_on_format && results.any_formatted() { + Ok(ExitStatus::Failure) + } else { + Ok(ExitStatus::Success) + } } else { Ok(ExitStatus::Error) } diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs index 43428c7327c62..61f8aa2915a28 100644 --- a/crates/ruff/tests/format.rs +++ b/crates/ruff/tests/format.rs @@ -581,6 +581,85 @@ if __name__ == "__main__": Ok(()) } +#[test] +fn exit_non_zero_on_format() -> Result<()> { + let tempdir = TempDir::new()?; + + let contents = r#" +from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#; + + fs::write(tempdir.path().join("main.py"), contents)?; + + let mut cmd = Command::new(get_cargo_bin(BIN_NAME)); + cmd.current_dir(tempdir.path()) + .args([ + "format", + "--no-cache", + "--isolated", + "--exit-non-zero-on-format", + ]) + .arg("main.py"); + + // First format should exit with code 1 since the file needed formatting + assert_cmd_snapshot!(cmd, @r" + success: false + exit_code: 1 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + "); + + // Second format should exit with code 0 since no files needed formatting + assert_cmd_snapshot!(cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + 1 file left unchanged + + ----- stderr ----- + "); + + // Repeat the tests above with the --exit-non-zero-on-fix alias + fs::write(tempdir.path().join("main.py"), contents)?; + + let mut cmd = Command::new(get_cargo_bin(BIN_NAME)); + cmd.current_dir(tempdir.path()) + .args([ + "format", + "--no-cache", + "--isolated", + "--exit-non-zero-on-fix", + ]) + .arg("main.py"); + + // First format should exit with code 1 since the file needed formatting + assert_cmd_snapshot!(cmd, @r" + success: false + exit_code: 1 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + "); + + // Second format should exit with code 0 since no files needed formatting + assert_cmd_snapshot!(cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + 1 file left unchanged + + ----- stderr ----- + "); + + Ok(()) +} + #[test] fn force_exclude() -> Result<()> { let tempdir = TempDir::new()?; diff --git a/docs/configuration.md b/docs/configuration.md index 916942d642dcb..66d5daaafeb66 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -726,6 +726,9 @@ Miscellaneous: Path to the cache directory [env: RUFF_CACHE_DIR=] --stdin-filename The name of the file when passing it through stdin + --exit-non-zero-on-format + Exit with a non-zero status code if any files were modified via + format, even if all files were formatted successfully File selection: --respect-gitignore