diff --git a/README.md b/README.md index d91e21b24..4e224cbe5 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ rtk read file.rs -l aggressive # Signatures only (strips bodies) rtk smart file.rs # 2-line heuristic code summary rtk find "*.rs" . # Compact find results rtk grep "pattern" . # Grouped search results -rtk diff file1 file2 # Condensed diff +rtk diff file1 file2 # Condensed diff (exit 1 if files differ) ``` ### Git diff --git a/scripts/test-all.sh b/scripts/test-all.sh index f0e2c06b1..08a8df7a5 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -527,7 +527,9 @@ assert_ok "rtk discover" rtk discover section "Diff" -assert_ok "rtk diff two files" rtk diff Cargo.toml LICENSE +assert_ok "rtk diff identical files" rtk diff Cargo.toml Cargo.toml +assert_fails "rtk diff differing files" rtk diff Cargo.toml LICENSE +assert_contains "rtk diff shows changes" "added" rtk diff Cargo.toml LICENSE # ── 37. Wc ──────────────────────────────────────────── diff --git a/src/cmds/git/diff_cmd.rs b/src/cmds/git/diff_cmd.rs index 59c95b100..4f7ae1609 100644 --- a/src/cmds/git/diff_cmd.rs +++ b/src/cmds/git/diff_cmd.rs @@ -5,8 +5,9 @@ use anyhow::Result; use std::fs; use std::path::Path; -/// Ultra-condensed diff - only changed lines, no context -pub fn run(file1: &Path, file2: &Path, verbose: u8) -> Result<()> { +/// Ultra-condensed diff - only changed lines, no context. +/// Returns the diff-convention exit code: 0 if identical, 1 if files differ. +pub fn run(file1: &Path, file2: &Path, verbose: u8) -> Result { let timer = tracking::TimedExecution::start(); if verbose > 0 { @@ -17,39 +18,37 @@ pub fn run(file1: &Path, file2: &Path, verbose: u8) -> Result<()> { let content2 = fs::read_to_string(file2)?; let raw = format!("{}\n---\n{}", content1, content2); + let (rtk, exit_code) = render_file_diff(file1, file2, &content1, &content2); + + print!("{}", rtk); + timer.track( + &format!("diff {} {}", file1.display(), file2.display()), + "rtk diff", + &raw, + &rtk, + ); + Ok(exit_code) +} + +/// Renders the condensed file comparison and returns it with the +/// diff-convention exit code (0 = identical, 1 = differences found). +fn render_file_diff(file1: &Path, file2: &Path, content1: &str, content2: &str) -> (String, i32) { let lines1: Vec<&str> = content1.lines().collect(); let lines2: Vec<&str> = content2.lines().collect(); let diff = compute_diff(&lines1, &lines2); - let mut rtk = String::new(); - if diff.added == 0 && diff.removed == 0 { - rtk.push_str("[ok] Files are identical"); - println!("{}", rtk); - timer.track( - &format!("diff {} {}", file1.display(), file2.display()), - "rtk diff", - &raw, - &rtk, - ); - return Ok(()); + if diff.changes.is_empty() { + return ("[ok] Files are identical\n".to_string(), 0); } + let mut rtk = String::new(); rtk.push_str(&format!("{} → {}\n", file1.display(), file2.display())); rtk.push_str(&format!( " +{} added, -{} removed, ~{} modified\n\n", diff.added, diff.removed, diff.modified )); - rtk.push_str(&format_diff_changes(&diff)); - - print!("{}", rtk); - timer.track( - &format!("diff {} {}", file1.display(), file2.display()), - "rtk diff", - &raw, - &rtk, - ); - Ok(()) + (rtk, 1) } /// Run diff from stdin (piped command output) @@ -307,6 +306,64 @@ mod tests { assert!(result.changes.is_empty()); } + // --- render_file_diff (issue #2364 regression) --- + + #[test] + fn test_render_modified_only_yaml_not_identical() { + // "a: 1" vs "a: 2" is classified as modified (similarity > 0.5); + // the identical check must not ignore modified-only diffs. + let (out, code) = render_file_diff( + Path::new("one.yaml"), + Path::new("two.yaml"), + "a: 1\n", + "a: 2\n", + ); + assert!( + !out.contains("identical"), + "modified-only diff reported as identical:\n{}", + out + ); + assert!(out.contains("~1 modified")); + assert!(out.contains("a: 1")); + assert!(out.contains("a: 2")); + assert_eq!(code, 1, "differing files must exit 1 (diff convention)"); + } + + #[test] + fn test_render_modified_only_json_not_identical() { + let (out, code) = render_file_diff( + Path::new("j1.json"), + Path::new("j2.json"), + "{\"a\": 1}\n", + "{\"a\": 2}\n", + ); + assert!( + !out.contains("identical"), + "modified-only diff reported as identical:\n{}", + out + ); + assert_eq!(code, 1); + } + + #[test] + fn test_render_identical_files_exit_zero() { + let (out, code) = render_file_diff( + Path::new("a.yaml"), + Path::new("b.yaml"), + "a: 1\nb: 2\n", + "a: 1\nb: 2\n", + ); + assert!(out.contains("[ok] Files are identical")); + assert_eq!(code, 0); + } + + #[test] + fn test_render_added_removed_exit_one() { + let (out, code) = render_file_diff(Path::new("t1.txt"), Path::new("t2.txt"), "x\n", "y\n"); + assert!(out.contains("+1 added, -1 removed")); + assert_eq!(code, 1); + } + // --- condense_unified_diff --- #[test] diff --git a/src/main.rs b/src/main.rs index 6d21faf5a..066ebecd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1699,11 +1699,11 @@ fn run_cli() -> Result { Commands::Diff { file1, file2 } => { if let Some(f2) = file2 { - diff_cmd::run(&file1, &f2, cli.verbose)?; + diff_cmd::run(&file1, &f2, cli.verbose)? } else { diff_cmd::run_stdin(cli.verbose)?; + 0 } - 0 } Commands::Log { file } => {