diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2e584753fc..e329bbf1bdc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,12 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ``` +- Added new `--staged` flag to the `check`, `format` and `lint` subcommands. + + This new option allows users to apply the command _only_ to the files that are staged (the + ones that will be committed), which can be very useful to simplify writing git hook scripts + such as `pre-commit`. Contributed by @castarco + #### Enhancements - Improve support of `.prettierignore` when migrating from Prettier diff --git a/crates/biome_cli/src/changed.rs b/crates/biome_cli/src/changed.rs index 414eec453508..b3eae9f37621 100644 --- a/crates/biome_cli/src/changed.rs +++ b/crates/biome_cli/src/changed.rs @@ -27,3 +27,13 @@ pub(crate) fn get_changed_files( Ok(filtered_changed_files) } + +pub(crate) fn get_staged_files( + fs: &DynRef<'_, dyn FileSystem>, +) -> Result, CliDiagnostic> { + let staged_files = fs.get_staged_files()?; + + let filtered_staged_files = staged_files.iter().map(OsString::from).collect::>(); + + Ok(filtered_staged_files) +} diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index cfc7eeb3b135..23c9321cdfe6 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -1,6 +1,7 @@ -use crate::changed::get_changed_files; use crate::cli_options::CliOptions; -use crate::commands::{get_stdin, resolve_manifest, validate_configuration_diagnostics}; +use crate::commands::{ + get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, +}; use crate::{ execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, }; @@ -26,6 +27,7 @@ pub(crate) struct CheckCommandPayload { pub(crate) formatter_enabled: Option, pub(crate) linter_enabled: Option, pub(crate) organize_imports_enabled: Option, + pub(crate) staged: bool, pub(crate) changed: bool, pub(crate) since: Option, } @@ -46,6 +48,7 @@ pub(crate) fn check( organize_imports_enabled, formatter_enabled, since, + staged, changed, } = payload; setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); @@ -120,13 +123,12 @@ pub(crate) fn check( let stdin = get_stdin(stdin_file_path, &mut *session.app.console, "check")?; - if since.is_some() && !changed { - return Err(CliDiagnostic::incompatible_arguments("since", "changed")); + if let Some(_paths) = + get_files_to_process(since, changed, staged, &session.app.fs, &fs_configuration)? + { + paths = _paths; } - if changed { - paths = get_changed_files(&session.app.fs, &fs_configuration, since)?; - } session .app .workspace diff --git a/crates/biome_cli/src/commands/format.rs b/crates/biome_cli/src/commands/format.rs index a082c3c11963..dc8de990ae82 100644 --- a/crates/biome_cli/src/commands/format.rs +++ b/crates/biome_cli/src/commands/format.rs @@ -1,6 +1,7 @@ -use crate::changed::get_changed_files; use crate::cli_options::CliOptions; -use crate::commands::{get_stdin, resolve_manifest, validate_configuration_diagnostics}; +use crate::commands::{ + get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, +}; use crate::diagnostics::DeprecatedArgument; use crate::{ execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, @@ -30,6 +31,7 @@ pub(crate) struct FormatCommandPayload { pub(crate) write: bool, pub(crate) cli_options: CliOptions, pub(crate) paths: Vec, + pub(crate) staged: bool, pub(crate) changed: bool, pub(crate) since: Option, } @@ -51,6 +53,7 @@ pub(crate) fn format( mut json_formatter, mut css_formatter, since, + staged, changed, } = payload; setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); @@ -156,12 +159,10 @@ pub(crate) fn format( let (vcs_base_path, gitignore_matches) = configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; - if since.is_some() && !changed { - return Err(CliDiagnostic::incompatible_arguments("since", "changed")); - } - - if changed { - paths = get_changed_files(&session.app.fs, &configuration, since)?; + if let Some(_paths) = + get_files_to_process(since, changed, staged, &session.app.fs, &configuration)? + { + paths = _paths; } session diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index 47c84ed82790..c27d3cefd6dc 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -1,6 +1,7 @@ -use crate::changed::get_changed_files; use crate::cli_options::CliOptions; -use crate::commands::{get_stdin, resolve_manifest, validate_configuration_diagnostics}; +use crate::commands::{ + get_files_to_process, get_stdin, resolve_manifest, validate_configuration_diagnostics, +}; use crate::{ execute_mode, setup_cli_subscriber, CliDiagnostic, CliSession, Execution, TraversalMode, }; @@ -24,6 +25,7 @@ pub(crate) struct LintCommandPayload { pub(crate) files_configuration: Option, pub(crate) paths: Vec, pub(crate) stdin_file_path: Option, + pub(crate) staged: bool, pub(crate) changed: bool, pub(crate) since: Option, } @@ -39,6 +41,7 @@ pub(crate) fn lint(session: CliSession, payload: LintCommandPayload) -> Result<( stdin_file_path, vcs_configuration, files_configuration, + staged, changed, since, } = payload; @@ -95,12 +98,10 @@ pub(crate) fn lint(session: CliSession, payload: LintCommandPayload) -> Result<( let (vcs_base_path, gitignore_matches) = fs_configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; - if since.is_some() && !changed { - return Err(CliDiagnostic::incompatible_arguments("since", "changed")); - } - - if changed { - paths = get_changed_files(&session.app.fs, &fs_configuration, since)?; + if let Some(_paths) = + get_files_to_process(since, changed, staged, &session.app.fs, &fs_configuration)? + { + paths = _paths; } let stdin = get_stdin(stdin_file_path, &mut *session.app.console, "lint")?; diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 2a5001121f49..6625f6b87220 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -1,3 +1,4 @@ +use crate::changed::{get_changed_files, get_staged_files}; use crate::cli_options::{cli_options, CliOptions, ColorsArg}; use crate::diagnostics::DeprecatedConfigurationFile; use crate::execute::Stdin; @@ -14,11 +15,11 @@ use biome_configuration::{ use biome_configuration::{ConfigurationDiagnostic, PartialConfiguration}; use biome_console::{markup, Console, ConsoleExt}; use biome_diagnostics::{Diagnostic, PrintDiagnostic}; -use biome_fs::BiomePath; +use biome_fs::{BiomePath, FileSystem}; use biome_service::configuration::LoadedConfiguration; use biome_service::documentation::Doc; use biome_service::workspace::{OpenProjectParams, UpdateProjectParams}; -use biome_service::WorkspaceError; +use biome_service::{DynRef, WorkspaceError}; use bpaf::Bpaf; use std::ffi::OsString; use std::path::PathBuf; @@ -109,6 +110,11 @@ pub enum BiomeCommand { #[bpaf(long("stdin-file-path"), argument("PATH"), hide_usage)] stdin_file_path: Option, + /// When set to true, only the files that have been staged (the ones prepared to be committed) + /// will be linted. + #[bpaf(long("staged"), switch)] + staged: bool, + /// When set to true, only the files that have been changed compared to your `defaultBranch` /// configuration will be linted. #[bpaf(long("changed"), switch)] @@ -150,6 +156,10 @@ pub enum BiomeCommand { /// Example: `echo 'let a;' | biome lint --stdin-file-path=file.js` #[bpaf(long("stdin-file-path"), argument("PATH"), hide_usage)] stdin_file_path: Option, + /// When set to true, only the files that have been staged (the ones prepared to be committed) + /// will be linted. + #[bpaf(long("staged"), switch)] + staged: bool, /// When set to true, only the files that have been changed compared to your `defaultBranch` /// configuration will be linted. #[bpaf(long("changed"), switch)] @@ -197,6 +207,11 @@ pub enum BiomeCommand { #[bpaf(switch)] write: bool, + /// When set to true, only the files that have been staged (the ones prepared to be committed) + /// will be linted. + #[bpaf(long("staged"), switch)] + staged: bool, + /// When set to true, only the files that have been changed compared to your `defaultBranch` /// configuration will be linted. #[bpaf(long("changed"), switch)] @@ -510,6 +525,34 @@ pub(crate) fn get_stdin( Ok(stdin) } +fn get_files_to_process( + since: Option, + changed: bool, + staged: bool, + fs: &DynRef<'_, dyn FileSystem>, + configuration: &PartialConfiguration, +) -> Result>, CliDiagnostic> { + if since.is_some() { + if !changed { + return Err(CliDiagnostic::incompatible_arguments("since", "changed")); + } + if staged { + return Err(CliDiagnostic::incompatible_arguments("since", "staged")); + } + } + + if changed { + if staged { + return Err(CliDiagnostic::incompatible_arguments("changed", "staged")); + } + Ok(Some(get_changed_files(fs, configuration, since)?)) + } else if staged { + Ok(Some(get_staged_files(fs)?)) + } else { + Ok(None) + } +} + /// Tests that all CLI options adhere to the invariants expected by `bpaf`. #[test] fn check_options() { diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index d9f50dea9147..8f80af323645 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -88,6 +88,7 @@ impl<'app> CliSession<'app> { linter_enabled, organize_imports_enabled, formatter_enabled, + staged, changed, since, } => commands::check::check( @@ -102,6 +103,7 @@ impl<'app> CliSession<'app> { linter_enabled, organize_imports_enabled, formatter_enabled, + staged, changed, since, }, @@ -115,6 +117,7 @@ impl<'app> CliSession<'app> { stdin_file_path, vcs_configuration, files_configuration, + staged, changed, since, } => commands::lint::lint( @@ -128,6 +131,7 @@ impl<'app> CliSession<'app> { stdin_file_path, vcs_configuration, files_configuration, + staged, changed, since, }, @@ -165,6 +169,7 @@ impl<'app> CliSession<'app> { files_configuration, json_formatter, css_formatter, + staged, changed, since, } => commands::format::format( @@ -180,6 +185,7 @@ impl<'app> CliSession<'app> { files_configuration, json_formatter, css_formatter, + staged, changed, since, }, diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs index 2d2f63115d2b..ba5253458dbe 100644 --- a/crates/biome_cli/tests/commands/lint.rs +++ b/crates/biome_cli/tests/commands/lint.rs @@ -3006,6 +3006,171 @@ fn should_not_error_for_no_changed_files_with_no_errors_on_unmatched() { )); } +#[test] +fn should_error_if_changed_flag_and_staged_flag_are_active_at_the_same_time() { + let mut console = BufferConsole::default(); + let mut fs = MemoryFileSystem::default(); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from([("lint"), "--staged", "--changed"].as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_error_if_changed_flag_and_staged_flag_are_active_at_the_same_time", + fs, + console, + result, + )); +} + +#[test] +fn should_only_processes_staged_files_when_staged_flag_is_set() { + let mut console = BufferConsole::default(); + let mut fs = MemoryFileSystem::default(); + + fs.set_on_get_staged_files(Box::new(|| vec![String::from("staged.js")])); + fs.set_on_get_changed_files(Box::new(|| vec![String::from("changed.js")])); + + // Staged (prepared to be committed) + fs.insert( + Path::new("staged.js").into(), + r#"console.log('staged');"#.as_bytes(), + ); + + // Changed (already recorded in git history) + fs.insert( + Path::new("changed.js").into(), + r#"console.log('changed');"#.as_bytes(), + ); + + // Unstaged (not yet recorded in git history, and not prepared to be committed) + fs.insert( + Path::new("file2.js").into(), + r#"console.log('file2');"#.as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from([("lint"), "--staged"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_only_processes_staged_files_when_staged_flag_is_set", + fs, + console, + result, + )); +} + +#[test] +fn should_only_process_staged_file_if_its_included() { + let mut console = BufferConsole::default(); + let mut fs = MemoryFileSystem::default(); + + fs.set_on_get_staged_files(Box::new(|| { + vec![String::from("file.js"), String::from("file2.js")] + })); + + let file_path = Path::new("biome.json"); + fs.insert( + file_path.into(), + r#" +{ + "files": { + "include": ["file.js"] + }, + "vcs": { + "defaultBranch": "main" + } +} + "# + .as_bytes(), + ); + + fs.insert( + Path::new("file.js").into(), + r#"console.log('file');"#.as_bytes(), + ); + fs.insert( + Path::new("file2.js").into(), + r#"console.log('file2');"#.as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from([("lint"), "--staged"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_only_process_staged_file_if_its_included", + fs, + console, + result, + )); +} + +#[test] +fn should_not_process_ignored_file_even_if_its_staged() { + let mut console = BufferConsole::default(); + let mut fs = MemoryFileSystem::default(); + + fs.set_on_get_staged_files(Box::new(|| vec![String::from("file.js")])); + + let file_path = Path::new("biome.json"); + fs.insert( + file_path.into(), + r#" +{ + "files": { + "ignore": ["file.js"] + }, + "vcs": { + "defaultBranch": "main" + } +} + "# + .as_bytes(), + ); + + fs.insert( + Path::new("file.js").into(), + r#"console.log('file');"#.as_bytes(), + ); + fs.insert( + Path::new("file2.js").into(), + r#"console.log('file2');"#.as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from([("lint"), "--staged"].as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_not_process_ignored_file_even_if_its_staged", + fs, + console, + result, + )); +} + #[test] fn lint_syntax_rules() { let mut fs = MemoryFileSystem::default(); diff --git a/crates/biome_cli/tests/snapshots/main_commands_check/check_help.snap b/crates/biome_cli/tests/snapshots/main_commands_check/check_help.snap index c079661a20f5..3f43ec668957 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_check/check_help.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_check/check_help.snap @@ -7,7 +7,7 @@ expression: content ```block Runs formatter, linter and import sorting to the requested files. -Usage: check [--apply] [--apply-unsafe] [--changed] [--since=REF] [PATH]... +Usage: check [--apply] [--apply-unsafe] [--staged] [--changed] [--since=REF] [PATH]... The configuration that is contained inside the file `biome.json` --vcs-client-kind= The kind of client. @@ -116,6 +116,8 @@ Available options: The file doesn't need to exist on disk, what matters is the extension of the file. Based on the extension, Biome knows how to check the code. Example: `echo 'let a;' | biome check --stdin-file-path=file.js` + --staged When set to true, only the files that have been staged (the ones prepared + to be committed) will be linted. --changed When set to true, only the files that have been changed compared to your `defaultBranch` configuration will be linted. --since=REF Use this to specify the base branch to compare against when you're using diff --git a/crates/biome_cli/tests/snapshots/main_commands_format/format_help.snap b/crates/biome_cli/tests/snapshots/main_commands_format/format_help.snap index 290125afe454..3d224192b17b 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_format/format_help.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_format/format_help.snap @@ -7,7 +7,7 @@ expression: content ```block Run the formatter on a set of files. -Usage: format [--write] [--changed] [--since=REF] [PATH]... +Usage: format [--write] [--staged] [--changed] [--since=REF] [PATH]... Generic options applied to all files --indent-style= The indent style. @@ -118,6 +118,8 @@ Available options: the file. Based on the extension, Biome knows how to format the code. Example: `echo 'let a;' | biome format --stdin-file-path=file.js` --write Writes formatted files to file system. + --staged When set to true, only the files that have been staged (the ones prepared + to be committed) will be linted. --changed When set to true, only the files that have been changed compared to your `defaultBranch` configuration will be linted. --since=REF Use this to specify the base branch to compare against when you're using diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/lint_help.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/lint_help.snap index 8d6a7557d1c1..5c5b98fcf7a4 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_lint/lint_help.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_lint/lint_help.snap @@ -7,7 +7,7 @@ expression: content ```block Run various checks on a set of files. -Usage: lint [--apply] [--apply-unsafe] [--changed] [--since=REF] [PATH]... +Usage: lint [--apply] [--apply-unsafe] [--staged] [--changed] [--since=REF] [PATH]... Set of properties to integrate Biome with a VCS software. --vcs-client-kind= The kind of client. @@ -66,6 +66,8 @@ Available options: The file doesn't need to exist on disk, what matters is the extension of the file. Based on the extension, Biome knows how to lint the code. Example: `echo 'let a;' | biome lint --stdin-file-path=file.js` + --staged When set to true, only the files that have been staged (the ones prepared + to be committed) will be linted. --changed When set to true, only the files that have been changed compared to your `defaultBranch` configuration will be linted. --since=REF Use this to specify the base branch to compare against when you're using diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/should_error_if_changed_flag_and_staged_flag_are_active_at_the_same_time.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/should_error_if_changed_flag_and_staged_flag_are_active_at_the_same_time.snap new file mode 100644 index 000000000000..993fbd628436 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_commands_lint/should_error_if_changed_flag_and_staged_flag_are_active_at_the_same_time.snap @@ -0,0 +1,14 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: content +--- +# Termination Message + +```block +flags/invalid ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Incompatible arguments changed and staged + + + +``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/should_not_process_ignored_file_even_if_its_staged.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/should_not_process_ignored_file_even_if_its_staged.snap new file mode 100644 index 000000000000..77def02eaddf --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_commands_lint/should_not_process_ignored_file_even_if_its_staged.snap @@ -0,0 +1,45 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: content +--- +## `biome.json` + +```json +{ + "files": { + "ignore": ["file.js"] + }, + "vcs": { + "defaultBranch": "main" + } +} +``` + +## `file.js` + +```js +console.log('file'); +``` + +## `file2.js` + +```js +console.log('file2'); +``` + +# Termination Message + +```block +internalError/io ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × No files were processed in the specified paths. + + + +``` + +# Emitted Messages + +```block +Checked 0 files in