Skip to content

fix(display): report one-sided empty files as changes - #1040

Open
YuriNachos wants to merge 1 commit into
Wilfred:masterfrom
YuriNachos:YuriNachos/w4-difftastic-1027
Open

YuriNachos wants to merge 1 commit into
Wilfred:masterfrom
YuriNachos:YuriNachos/w4-difftastic-1027

Conversation

@YuriNachos

Copy link
Copy Markdown
Contributor

Summary

difft foo bar where bar/bar.txt is an empty file that exists only in bar/ printed bar.txt --- Text / No changes., and --skip-unchanged silently dropped it. A file present in only one diffed directory is a change, so it is now reported as one. Empty files present on both sides are still reported as unchanged.

Addresses #1027.

Root cause

Directory diffing pairs files by relative path (relative_paths_in_either, src/files.rs) and compares each pair via diff_file with missing_as_empty = true (src/main.rs, diff_directories). For a file that exists on only one side, read_files_or_die (src/files.rs:24) substitutes empty content for the missing side — the existing behaviour needed for mercurial removals. For an empty file on the present side, both sides then compare equal, so diff_file_content took the early "identical sources" return (has_syntactic_changes: false), printing No changes. and hiding the file under --skip-unchanged. A one-sided non-empty file never hit this because its content differs from the empty substitute.

Changes

  • src/files.rs: read_files_or_die now also returns a MissingFile { Neither, Lhs, Rhs } indicating which side (if any) was missing and substituted as empty. Single-file mode (missing_as_empty = false) behaviour is unchanged: an explicitly missing argument still errors with exit code 2.
  • src/main.rs (diff_file):
    • Text path: when a side was missing, has_syntactic_changes is forced on. A one-sided file is a change even when its content produces no detectable differences — an empty file, content that normalizes to empty under the default --strip-cr=on, or whitespace-only content whose syntax tree matches the empty side's. Output is the existing header + Has changes. / Has syntactic changes.; --skip-unchanged keeps the file and --exit-code reports it.
    • Binary-overridden path: has_byte_changes is only None when the bytes are equal and nothing was missing, so a one-sided empty binary file reports Binary file added (0 B). rather than No changes.
  • src/display/json.rs: with empty hunks, JSON status is now changed when has_syntactic_changes is set (previously always unchanged), keeping the JSON payload consistent with the text display and the --exit-code result. This also corrects --check-only --display=json, which previously reported changed files as unchanged.

Tests

8 new integration tests in tests/cli.rs using new fixtures under sample_files/empty_files_1/ and sample_files/empty_files_2/, all verified red on the pre-fix code and green after:

  • one-sided empty file on the left → Has changes. and exit code 1 with --exit-code
  • one-sided empty file on the right → Has changes.
  • empty file on both sides → still No changes. (regression guard, anchored to the exact header + message)
  • --skip-unchanged shows the one-sided empty file, hides the unchanged ones
  • one-sided CR-only file (content normalizes to empty under --strip-cr=on) → Has changes.
  • one-sided whitespace-only .py (syntax tree identical to the empty side's) → Has syntactic changes.
  • --override-binary one-sided empty file → Binary file added (0 B).
  • JSON output reports the one-sided empty file as "status":"changed"

cargo fmt --check, cargo clippy --all -- -D warnings and cargo test (122 unit + 31 CLI) all pass.

Known limitations, deliberately out of scope here: git's /dev/null form of one-sided files (used by git difftool for added/deleted files) is indistinguishable from an explicit user /dev/null comparison and still reports No changes.; a one-sided empty binary file's added/removed wording keys off byte lengths (the pre-existing TODO: Fix this pedantic case in print_diff_result); JSON uses changed rather than created/deleted for one-sided empty files because the missing side is not currently recorded in DiffResult.

Disclosure

This change was developed with AI assistance (Claude) and human-reviewed; I have read all the code in the diff and can discuss every line.

When diffing directories, a file that exists on only one side is read
as empty content on the missing side (the mercurial-compatible
behaviour in read_files_or_die). A one-sided empty file therefore
compared equal to the missing side and collapsed to "No changes.",
and --skip-unchanged silently dropped it.

read_files_or_die now reports which side was missing, so diff_file can
mark one-sided files as changes: for text files by forcing
has_syntactic_changes (a one-sided file is a change even when its
content produces no detectable differences, e.g. an empty file,
carriage returns stripped by --strip-cr, or whitespace with no syntax
tree nodes), and for binary-overridden files by reporting byte
changes. JSON output now reports "changed" when there are changes
without displayable hunks, consistent with the text display and
--exit-code.

Empty files present on both sides are still reported as unchanged.

Addresses Wilfred#1027.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant