Repository: getgrit/gritql (rust)

Issue:
`grit apply` only in diff
Similar to the [only_in_json](https://github.com/getgrit/gritql/blob/3581187ffbf6b9d88bd86ee2089a92d148f71e68/crates/cli/src/commands/apply_pattern.rs#L95-L100) flag, we should have a `only-in-diff` flag that can point to a git diff to limit the apply to.

Pull request:
feat: only in diff flag
fixes #142 

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
    - Expanded workflow triggers to include additional branches.
    - Added functionality to rewrite and check ranges only in provided unified diff files.
    - Introduced a new `diff` module for parsing modified line ranges from diff files.
    - New test functions to ensure proper application and checking of content changes based on diff files.
- **Refactor**
    - Improved import statement ordering and syntax across several modules for better readability.
    - Updated code formatting and alignment in specific functions for consistency.
- **Style**
    - Updated string literals in JavaScript fixtures for consistency.
- **Documentation**
    - Fixed a typo in a comment related to OpenSSL usage.
- **Chores**
    - Added an empty line in a test module for clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 08c9cfe6..8aebc8b3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1924,6 +1924,7 @@ dependencies = [
  "ignore",
  "indicatif",
  "indicatif-log-bridge",
+ "insta",
  "lazy_static",
  "log",
  "marzano-auth",
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 3a2dd349..d1efd876 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -71,6 +71,7 @@ buildkite-test-collector = "0.1.1"
 similar = "2.2.1"
 reqwest = { version = "0.11.22", features = ["blocking", "json"] }
 marzano-test-utils = { path = "../test_utils" }
+insta = { version = "1.30.0", features = ["yaml"] }
 
 
 [features]
diff --git a/crates/cli/src/analyze.rs b/crates/cli/src/analyze.rs
index b04645c9..9f8d48e9 100644
--- a/crates/cli/src/analyze.rs
+++ b/crates/cli/src/analyze.rs
@@ -13,13 +13,13 @@ use grit_cache::paths::cache_for_cwd;
 use ignore::Walk;
 use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
 
+#[allow(unused_imports)]
+use marzano_core::pattern::built_in_functions::BuiltIns;
 use marzano_core::pattern::{
     api::{AnalysisLog, DoneFile, MatchResult},
     compiler::CompilationResult,
     Problem,
 };
-#[allow(unused_imports)]
-use marzano_core::pattern::built_in_functions::BuiltIns;
 use marzano_language::target_language::PatternLanguage;
 use marzano_util::cache::GritCache;
 use marzano_util::position::Position;
diff --git a/crates/cli/src/commands/apply_pattern.rs b/crates/cli/src/commands/apply_pattern.rs
index 93448f80..3f493476 100644
--- a/crates/cli/src/commands/apply_pattern.rs
+++ b/crates/cli/src/commands/apply_pattern.rs
@@ -30,9 +30,9 @@ use std::collections::BTreeMap;
 use tokio::fs;
 
 use crate::{
-    analyze::par_apply_pattern, community::parse_eslint_output, error::GoodError,
-    flags::OutputFormat, messenger_variant::create_emitter, result_formatting::get_human_error,
-    updater::Updater,
+    analyze::par_apply_pattern, community::parse_eslint_output, diff::extract_modified_ranges,
+    error::GoodError, flags::OutputFormat, messenger_variant::create_emitter,
+    result_formatting::get_human_error, updater::Updater,
 };
 
 use marzano_messenger::{
@@ -92,10 +92,18 @@ pub struct ApplyPatternArgs {
             hide = true,
         )]
     pub visibility: VisibilityLevels,
+    #[clap(
+        long = "only-in-diff",
+        help = "Only rewrite ranges that are inside the provided unified diff",
+        hide = true,
+        conflicts_with = "only_in_json"
+    )]
+    only_in_diff: Option<PathBuf>,
     #[clap(
         long = "only-in-json",
         help = "Only rewrite ranges that are inside the provided eslint-style JSON file",
-        hide = true
+        hide = true,
+        conflicts_with = "only_in_diff"
     )]
     only_in_json: Option<PathBuf>,
     #[clap(
@@ -109,7 +117,7 @@ pub struct ApplyPatternArgs {
     /// Clear cache before running apply
     #[clap(long = "refresh-cache", conflicts_with = "cache")]
     pub refresh_cache: bool,
-    #[clap(long = "language", alias="lang")]
+    #[clap(long = "language", alias = "lang")]
     pub language: Option<PatternLanguage>,
 }
 
@@ -123,6 +131,7 @@ impl Default for ApplyPatternArgs {
             format: Default::default(),
             interactive: Default::default(),
             visibility: VisibilityLevels::Hidden,
+            only_in_diff: Default::default(),
             only_in_json: Default::default(),
             output_file: Default::default(),
             cache: Default::default(),
@@ -188,6 +197,9 @@ pub(crate) async fn run_apply_pattern(
     let filter_range = if let Some(json_path) = arg.only_in_json.clone() {
         let json_ranges = flushable_unwrap!(emitter, parse_eslint_output(json_path));
         Some(json_ranges)
+    } else if let Some(diff_path) = arg.only_in_diff.clone() {
+        let diff_ranges = flushable_unwrap!(emitter, extract_modified_ranges(&diff_path));
+        Some(diff_ranges)
     } else {
         None
     };
@@ -230,7 +242,6 @@ pub(crate) async fn run_apply_pattern(
 
         let pattern_libs = flushable_unwrap!(emitter, get_grit_files_from_cwd().await);
         let (mut lang, pattern_body) = if pattern.ends_with(".grit") || pattern.ends_with(".md") {
-    
             match fs::read_to_string(pattern.clone()).await {
                 Ok(pb) => {
                     if pattern.ends_with(".grit") {
diff --git a/crates/cli/src/commands/check.rs b/crates/cli/src/commands/check.rs
index 407f5ed7..7c317396 100644
--- a/crates/cli/src/commands/check.rs
+++ b/crates/cli/src/commands/check.rs
@@ -33,6 +33,7 @@ use marzano_messenger::emit::{Messager, VisibilityLevels};
 use cli_server::check::CheckMessenger;
 
 use crate::{
+    diff::extract_modified_ranges,
     error::GoodError,
     flags::{GlobalFormatFlags, OutputFormat},
     github::{log_check_annotations, write_check_summary},
@@ -69,6 +70,12 @@ pub struct CheckArg {
     /// Output annotations for a GitHub actions workflow
     #[clap(long = "github-actions")]
     pub github_actions: bool,
+    #[clap(
+        long = "only-in-diff",
+        help = "Only check ranges that are inside the provided unified diff",
+        hide = true
+    )]
+    pub only_in_diff: Option<PathBuf>,
 }
 
 pub(crate) async fn run_check(
@@ -117,6 +124,13 @@ pub(crate) async fn run_check(
         std::env::current_dir()?
     };
 
+    let filter_range = if let Some(diff_path) = arg.only_in_diff.clone() {
+        let diff_ranges = extract_modified_ranges(&diff_path)?;
+        Some(diff_ranges)
+    } else {
+        None
+    };
+
     // Construct a resolver
     let resolver = GritModuleResolver::new(current_dir.to_str().unwrap());
 
@@ -132,7 +146,7 @@ pub(crate) async fn run_check(
                 .make_pattern(&body, Some(p.local_name.to_string()))
                 .unwrap();
             let lang = PatternLanguage::get_language(&p.body);
-            match rich_pattern.compile(&grit_files, lang, None) {
+            match rich_pattern.compile(&grit_files, lang, filter_range.clone()) {
                 Ok(c) => Ok((p.local_name.clone(), c.problem)),
                 Err(e) => {
                     bail!("Unable to compile pattern {}:\n{}", p.local_name, e);
diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs
index 3eafd26e..7f8528bc 100644
--- a/crates/cli/src/commands/mod.rs
+++ b/crates/cli/src/commands/mod.rs
@@ -40,7 +40,7 @@ use crate::{
     flags::{GlobalFormatFlags, OutputFormat},
     updater::Updater,
 };
-use anyhow::{Result};
+use anyhow::Result;
 use apply::ApplyArgs;
 use auth::{Auth, AuthCommands};
 use check::CheckArg;
@@ -52,7 +52,7 @@ use indicatif_log_bridge::LogWrapper;
 use init::InitArgs;
 use install::InstallArgs;
 use list::ListArgs;
-use log::{LevelFilter};
+use log::LevelFilter;
 use lsp::LspArgs;
 use marzano_messenger::emit::ApplyDetails;
 use parse::ParseArgs;
diff --git a/crates/cli/src/commands/plumbing.rs b/crates/cli/src/commands/plumbing.rs
index c5264b64..dfcf3e55 100644
--- a/crates/cli/src/commands/plumbing.rs
+++ b/crates/cli/src/commands/plumbing.rs
@@ -11,14 +11,13 @@ use std::io::{stdin, Read};
 use std::path::Path;
 use std::path::PathBuf;
 
-use crate::analytics::{track_event_line};
+use crate::analytics::track_event_line;
 use crate::flags::GlobalFormatFlags;
 use crate::lister::list_applyables;
 use crate::resolver::{get_grit_files_from, resolve_from, Source};
 use crate::utils::is_pattern_name;
 
-
-use super::super::analytics::{AnalyticsArgs};
+use super::super::analytics::AnalyticsArgs;
 use super::apply_pattern::{run_apply_pattern, ApplyPatternArgs};
 use super::check::{run_check, CheckArg};
 use super::init::{init_config_from_cwd, init_global_grit_modules};
diff --git a/crates/cli/src/diff.rs b/crates/cli/src/diff.rs
new file mode 100644
index 00000000..694c65c9
--- /dev/null
+++ b/crates/cli/src/diff.rs
@@ -0,0 +1,227 @@
+use anyhow::Result;
+use marzano_util::position::{FileRange, Position, RangeWithoutByte, UtilRange};
+use std::{fs::File, io::Read, path::PathBuf, str::FromStr};
+
+pub fn extract_modified_ranges(diff_path: &PathBuf) -> Result<Vec<FileRange>> {
+    let mut file = File::open(diff_path)?;
+    let mut diff = String::new();
+
+    file.read_to_string(&mut diff)?;
+    parse_modified_ranges(&diff)
+}
+
+fn parse_modified_ranges(diff: &str) -> Result<Vec<FileRange>> {
+    let mut results = Vec::new();
+    let lines = diff.lines();
+
+    let mut current_file = String::new();
+    let mut start_pos = Position { line: 0, column: 0 };
+    let mut end_pos = Position { line: 0, column: 0 };
+
+    for line in lines {
+        if line.starts_with("+++") {
+            current_file = line.split_whitespace().nth(1).unwrap_or("").to_string();
+            if current_file.starts_with("b/") {
+                current_file = current_file[2..].to_string();
+            }
+        } else if line.starts_with("@@") {
+            if current_file == "/dev/null" {
+                continue;
+            }
+            let range_part = line.split_whitespace().nth(2).unwrap_or("");
+            let range_parts: Vec<&str> = range_part.split(',').collect();
+            if let Ok(line_num) = u32::from_str(range_parts[0].trim_start_matches('+')) {
+                start_pos.line = line_num;
+                end_pos.line = line_num
+                    + range_parts
+                        .get(1)
+                        .map_or(0, |&x| x.parse::<u32>().unwrap_or(0))
+                        .saturating_sub(1);
+            }
+
+            results.push(FileRange {
+                file_path: current_file.clone(),
+                range: UtilRange::RangeWithoutByte(RangeWithoutByte {
+                    start: start_pos,
+                    end: end_pos,
+                }),
+            });
+        }
+    }
+
+    Ok(results)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use insta::assert_yaml_snapshot;
+
+    #[test]
+    fn parse_one_file_diff() {
+        let diff = r#"diff --git a/crates/cli_bin/fixtures/es6/empty_export_object.js b/crates/cli_bin/fixtures/es6/empty_export_object.js
+index adacd90..71b96e0 100644
+--- a/crates/cli_bin/fixtures/es6/empty_export_object.js
++++ b/crates/cli_bin/fixtures/es6/empty_export_object.js
+@@ -5,7 +5,7 @@ module.exports = {
+    };
+    
+    export async function createTeam() {
+-  console.log('cool');
++  console.log('very cool');
+    }
+    
+    export const addTeamToOrgSubscription = () => console.log('cool');
+"#;
+        let parsed = parse_modified_ranges(diff).unwrap();
+        assert_yaml_snapshot!(parsed);
+    }
+
+    #[test]
+    fn parse_with_multiple_files() {
+        let diff = r#"diff --git a/crates/cli_bin/fixtures/es6/empty_export_object.js b/crates/cli_bin/fixtures/es6/empty_export_object.js
+index adacd90..71b96e0 100644
+--- a/crates/cli_bin/fixtures/es6/empty_export_object.js
++++ b/crates/cli_bin/fixtures/es6/empty_export_object.js
+@@ -5,7 +5,7 @@ module.exports = {
+    };
+    
+    export async function createTeam() {
+-  console.log('cool');
++  console.log('very cool');
+    }
+    
+    export const addTeamToOrgSubscription = () => console.log('cool');
+diff --git a/crates/cli_bin/fixtures/es6/export_object.js b/crates/cli_bin/fixtures/es6/export_object.js
+index f6e1a2c..2c58ad2 100644
+--- a/crates/cli_bin/fixtures/es6/export_object.js
++++ b/crates/cli_bin/fixtures/es6/export_object.js
+@@ -2,7 +2,9 @@ async function createTeam() {
+    console.log('cool');
+    }
+    
+-const addTeamToOrgSubscription = () => console.log('cool');
++const addTeamToOrgSubscription = () => {
++  console.log('cool')
++};
+    
+    module.exports = {
+    createTeam,
+"#;
+        let parsed = parse_modified_ranges(diff).unwrap();
+        assert_yaml_snapshot!(parsed);
+    }
+
+    #[test]
+    fn parse_with_created_file() {
+        let diff = r#"diff --git a/crates/cli_bin/fixtures/es6/empty_export_object.js b/crates/cli_bin/fixtures/es6/empty_export_object.js
+index adacd90..71b96e0 100644
+--- a/crates/cli_bin/fixtures/es6/empty_export_object.js
++++ b/crates/cli_bin/fixtures/es6/empty_export_object.js
+@@ -5,7 +5,7 @@ module.exports = {
+    };
+    
+    export async function createTeam() {
+-  console.log('cool');
++  console.log('very cool');
+    }
+    
+    export const addTeamToOrgSubscription = () => console.log('cool');
+diff --git a/crates/cli_bin/fixtures/es6/export_object.js b/crates/cli_bin/fixtures/es6/export_object.js
+index f6e1a2c..2c58ad2 100644
+--- a/crates/cli_bin/fixtures/es6/export_object.js
++++ b/crates/cli_bin/fixtures/es6/export_object.js
+@@ -2,7 +2,9 @@ async function createTeam() {
+    console.log('cool');
+    }
+    
+-const addTeamToOrgSubscription = () => console.log('cool');
++const addTeamToOrgSubscription = () => {
++  console.log('cool')
++};
+    
+    module.exports = {
+    createTeam,
+diff --git a/crates/cli_bin/fixtures/es6/index.js b/crates/cli_bin/fixtures/es6/index.js
+new file mode 100644
+index 0000000..7b232cd
+--- /dev/null
++++ b/crates/cli_bin/fixtures/es6/index.js
+@@ -0,0 +1,12 @@
++async function createTeam() {
++  console.log("cool");
++}
++
++const addTeamToOrgSubscription = () => {
++  console.log("cool");
++};
++
++module.exports = {
++  createTeam,
++  addTeamToOrgSubscription,
++};
+"#;
+        let parsed = parse_modified_ranges(diff).unwrap();
+        assert_yaml_snapshot!(parsed);
+    }
+
+    #[test]
+    fn parse_with_deleted_file() {
+        let diff = r#"diff --git a/crates/cli_bin/fixtures/es6/empty_export_object.js b/crates/cli_bin/fixtures/es6/empty_export_object.js
+index adacd90..71b96e0 100644
+--- a/crates/cli_bin/fixtures/es6/empty_export_object.js
++++ b/crates/cli_bin/fixtures/es6/empty_export_object.js
+@@ -5,7 +5,7 @@ module.exports = {
+    };
+    
+    export async function createTeam() {
+-  console.log('cool');
++  console.log('very cool');
+    }
+    
+    export const addTeamToOrgSubscription = () => console.log('cool');
+diff --git a/crates/cli_bin/fixtures/es6/export.js b/crates/cli_bin/fixtures/es6/export.js
+deleted file mode 100644
+index 52de8a9..0000000
+--- a/crates/cli_bin/fixtures/es6/export.js
++++ /dev/null
+@@ -1,19 +0,0 @@
+-const king = '9';
+-
+-module.exports = {
+-  king,
+-  queen: '8',
+-};
+-
+-async function createTeam() {
+-  console.log('cool');
+-}
+-
+-const addTeamToOrgSubscription = () => console.log('cool');
+-
+-module.exports = {
+-  createTeam,
+-  addTeamToOrgSubscription,
+-};
+-
+-module.exports.queen = '9';
+diff --git a/crates/cli_bin/fixtures/es6/export_object.js b/crates/cli_bin/fixtures/es6/export_object.js
+index f6e1a2c..2c58ad2 100644
+--- a/crates/cli_bin/fixtures/es6/export_object.js
++++ b/crates/cli_bin/fixtures/es6/export_object.js
+@@ -2,7 +2,9 @@ async function createTeam() {
+    console.log('cool');
+    }
+    
+-const addTeamToOrgSubscription = () => console.log('cool');
++const addTeamToOrgSubscription = () => {
++  console.log('cool')
++};
+    
+    module.exports = {
+    createTeam,
+"#;
+        let parsed = parse_modified_ranges(diff).unwrap();
+        assert_yaml_snapshot!(parsed);
+    }
+}
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
index 493a365a..c3dd72e0 100644
--- a/crates/cli/src/lib.rs
+++ b/crates/cli/src/lib.rs
@@ -2,6 +2,7 @@ mod analytics;
 mod analyze;
 pub mod commands;
 mod community;
+mod diff;
 pub mod error;
 mod flags;
 mod github;
@@ -17,7 +18,7 @@ mod ux;
 #[cfg(feature = "workflows_v2")]
 mod workflows;
 
-// git2 uses openssl, but breaks windows, so we need 
+// git2 uses openssl, but breaks windows, so we need
 // to import openssl and specify the vendored feature in order
 // to prevet git2 from breaking on windows
-use openssl as _;
\ No newline at end of file
+use openssl as _;
diff --git a/crates/cli/src/updater.rs b/crates/cli/src/updater.rs
index 48f1a65e..5b21b9ab 100644
--- a/crates/cli/src/updater.rs
+++ b/crates/cli/src/updater.rs
@@ -785,10 +785,13 @@ mod tests {
         let cli_release_date = updater._get_app_release_date(SupportedApp::Cli)?;
         assert_eq!(
             cli_release_date,
-            DateTime::<Utc>::from_naive_utc_and_offset(NaiveDate::from_ymd_opt(2023, 7, 12)
-                .unwrap()
-                .and_hms_opt(5, 2, 9)
-                .unwrap(), Utc),
+            DateTime::<Utc>::from_naive_utc_and_offset(
+                NaiveDate::from_ymd_opt(2023, 7, 12)
+                    .unwrap()
+                    .and_hms_opt(5, 2, 9)
+                    .unwrap(),
+                Utc
+            ),
         );
 
         Ok(())
diff --git a/crates/cli_bin/fixtures/only_diff/index.js b/crates/cli_bin/fixtures/only_diff/index.js
new file mode 100644
index 00000000..58e40c05
--- /dev/null
+++ b/crates/cli_bin/fixtures/only_diff/index.js
@@ -0,0 +1,15 @@
+export const king = '9';
+
+module.exports = {
+  queen: '8',
+};
+
+export async function createTeam() {
+  console.log('really cool');
+}
+
+export const addTeamToOrgSubscription = () => console.log('cool');
+
+module.exports = {};
+
+export const queen = '9';
diff --git a/crates/cli_bin/fixtures/only_diff_check/.grit/grit.yaml b/crates/cli_bin/fixtures/only_diff_check/.grit/grit.yaml
new file mode 100644
index 00000000..0c101da7
--- /dev/null
+++ b/crates/cli_bin/fixtures/only_diff_check/.grit/grit.yaml
@@ -0,0 +1,6 @@
+version: 0.0.3
+patterns:
+  - name: github.com/getgrit/stdlib#no_console_log
+    level: error
+  - name: github.com/getgrit/stdlib#es6_arrow_functions
+    level: error
diff --git a/crates/cli_bin/fixtures/only_diff_check/index.js b/crates/cli_bin/fixtures/only_diff_check/index.js
new file mode 100644
index 00000000..5ce03a40
--- /dev/null
+++ b/crates/cli_bin/fixtures/only_diff_check/index.js
@@ -0,0 +1,19 @@
+export const king = '9';
+
+module.exports = {
+  queen: '8',
+};
+
+export const wow = () => {
+  return;
+}
+
+export async function createTeam() {
+  console.log('very cool');
+}
+
+export const addTeamToOrgSubscription = () => console.log('cool');
+
+module.exports = {};
+
+export const queen = '9';
diff --git a/crates/util/src/position.rs b/crates/util/src/position.rs
index eb412526..d0398fcd 100644
--- a/crates/util/src/position.rs
+++ b/crates/util/src/position.rs
@@ -324,7 +324,7 @@ impl From<RangeWithoutByte> for UtilRange {
     }
 }
 
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[serde(rename_all = "camelCase")]
 pub struct FileRange {
     pub file_path: String,


Test patch:
diff --git a/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_one_file_diff.snap b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_one_file_diff.snap
new file mode 100644
index 00000000..87003440
--- /dev/null
+++ b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_one_file_diff.snap
@@ -0,0 +1,13 @@
+---
+source: crates/cli/src/diff.rs
+expression: parsed
+---
+- filePath: crates/cli_bin/fixtures/es6/empty_export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 5
+        column: 0
+      end:
+        line: 11
+        column: 0
diff --git a/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_created_file.snap b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_created_file.snap
new file mode 100644
index 00000000..5d10948f
--- /dev/null
+++ b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_created_file.snap
@@ -0,0 +1,31 @@
+---
+source: crates/cli/src/diff.rs
+expression: parsed
+---
+- filePath: crates/cli_bin/fixtures/es6/empty_export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 5
+        column: 0
+      end:
+        line: 11
+        column: 0
+- filePath: crates/cli_bin/fixtures/es6/export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 2
+        column: 0
+      end:
+        line: 10
+        column: 0
+- filePath: crates/cli_bin/fixtures/es6/index.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 1
+        column: 0
+      end:
+        line: 12
+        column: 0
diff --git a/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_deleted_file.snap b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_deleted_file.snap
new file mode 100644
index 00000000..a34821d3
--- /dev/null
+++ b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_deleted_file.snap
@@ -0,0 +1,22 @@
+---
+source: crates/cli/src/diff.rs
+expression: parsed
+---
+- filePath: crates/cli_bin/fixtures/es6/empty_export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 5
+        column: 0
+      end:
+        line: 11
+        column: 0
+- filePath: crates/cli_bin/fixtures/es6/export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 2
+        column: 0
+      end:
+        line: 10
+        column: 0
diff --git a/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_multiple_files.snap b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_multiple_files.snap
new file mode 100644
index 00000000..a34821d3
--- /dev/null
+++ b/crates/cli/src/snapshots/marzano_cli__diff__tests__parse_with_multiple_files.snap
@@ -0,0 +1,22 @@
+---
+source: crates/cli/src/diff.rs
+expression: parsed
+---
+- filePath: crates/cli_bin/fixtures/es6/empty_export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 5
+        column: 0
+      end:
+        line: 11
+        column: 0
+- filePath: crates/cli_bin/fixtures/es6/export_object.js
+  range:
+    RangeWithoutByte:
+      start:
+        line: 2
+        column: 0
+      end:
+        line: 10
+        column: 0
diff --git a/crates/cli_bin/fixtures/only_diff/test.diff b/crates/cli_bin/fixtures/only_diff/test.diff
new file mode 100644
index 00000000..e93405b2
--- /dev/null
+++ b/crates/cli_bin/fixtures/only_diff/test.diff
@@ -0,0 +1,13 @@
+diff --git a/index.js b/index.js
+index adacd90..58e40c0 100644
+--- a/index.js
++++ b/index.js
+@@ -5,7 +5,7 @@ module.exports = {
+ };
+ 
+ export async function createTeam() {
+-  console.log('cool');
++  console.log('really cool');
+ }
+ 
+ export const addTeamToOrgSubscription = () => console.log('cool');
diff --git a/crates/cli_bin/fixtures/only_diff_check/test.diff b/crates/cli_bin/fixtures/only_diff_check/test.diff
new file mode 100644
index 00000000..626c686f
--- /dev/null
+++ b/crates/cli_bin/fixtures/only_diff_check/test.diff
@@ -0,0 +1,18 @@
+diff --git a/index.js b/index.js
+index adacd90..58e40c0 100644
+--- a/index.js
++++ b/index.js
+@@ -4,8 +4,12 @@ module.exports = {
+   queen: '8',
+ };
+ 
++export const wow() => {
++  return;
++}
++
+ export async function createTeam() {
+-  console.log('really cool');
++  console.log('very cool');
+ }
+ 
+ export const addTeamToOrgSubscription = () => console.log('cool');
diff --git a/crates/cli_bin/tests/apply.rs b/crates/cli_bin/tests/apply.rs
index 93c0ea26..7ab16a04 100644
--- a/crates/cli_bin/tests/apply.rs
+++ b/crates/cli_bin/tests/apply.rs
@@ -2239,3 +2239,27 @@ fn invalid_language_option_apply() -> Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn apply_only_in_diff() -> Result<()> {
+    let (_temp_dir, dir) = get_fixture("only_diff", true)?;
+
+    let mut cmd = get_test_cmd()?;
+
+    cmd.arg("apply").arg("no_console_log").arg("--only-in-diff").arg("test.diff").current_dir(dir.clone());
+
+    let output = cmd.output()?;
+
+    assert!(
+        output.status.success(),
+        "Command failed"
+    );
+
+    assert!(String::from_utf8(output.stdout)?.contains("Processed 1 files and found 1 match"));
+    
+    let content = std::fs::read_to_string(dir.join("index.js"))?;
+    assert!(!content.contains("console.log('really cool')"));
+    assert!(content.contains("console.log('cool')"));
+
+    Ok(())
+}
diff --git a/crates/cli_bin/tests/check.rs b/crates/cli_bin/tests/check.rs
index 2a81f41d..c9e053e1 100644
--- a/crates/cli_bin/tests/check.rs
+++ b/crates/cli_bin/tests/check.rs
@@ -183,3 +183,22 @@ fn does_not_attempt_to_check_universal_pattern() -> Result<()> {
     assert!(output.contains("Fix available"));
     Ok(())
 }
+
+#[test]
+fn check_only_in_diff() -> Result<()> {
+    let (_temp_dir, dir) = get_fixture("only_diff_check", true)?;
+
+    let mut cmd = get_test_cmd()?;
+
+    cmd.arg("check").arg("--only-in-diff").arg("test.diff").current_dir(dir.clone());
+
+    let output = cmd.output()?;
+
+    let output = String::from_utf8_lossy(&output.stdout).to_string();
+    let mut lines: Vec<&str> = output.lines().collect();
+    lines.sort();
+    let output = lines.join("\n");
+    assert_snapshot!(output);
+
+    Ok(())
+}
diff --git a/crates/cli_bin/tests/snapshots/check__check_only_in_diff.snap b/crates/cli_bin/tests/snapshots/check__check_only_in_diff.snap
new file mode 100644
index 00000000..98fca75d
--- /dev/null
+++ b/crates/cli_bin/tests/snapshots/check__check_only_in_diff.snap
@@ -0,0 +1,8 @@
+---
+source: crates/cli_bin/tests/check.rs
+expression: output
+---
+  12:3    rewrite    Remove `console.log` statements.    Fix available.    no_console_log
+  7:20    rewrite    Converts function expressions to ES6 arrow functions    Fix available.    es6_arrow_functions
+./index.js
+1 files with rewrites. Run grit check --fix to apply changes.