diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 165238e..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Sven van Heugten - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 012eeb9..0000000 --- a/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# mechanicaldiff - -`mechanicaldiff` lets you filter down a diff to _only_ the parts that make a specific mechanical change, so you can commit those changes separately. - -Usage: `git diff | mechanicaldiff | recountdiff` - -It differs from [grepdiff](https://linux.die.net/man/1/grepdiff) in that it doesn’t just search for the lines that contain the given `pattern`, but also checks that the _only_ change in that line is that `pattern` was replaced with the given `replacement`. - -It differs from simply replacing `pattern` with `replacement` in the entire repository in that it filters down an _existing_ diff, which means that the resulting diff doesn’t make the mechanical change everywhere, but only in the lines that your original diff also made it. diff --git a/bin/mechanicaldiff b/bin/mechanicaldiff.py similarity index 78% rename from bin/mechanicaldiff rename to bin/mechanicaldiff.py index 8ac824d..11fc6c5 100755 --- a/bin/mechanicaldiff +++ b/bin/mechanicaldiff.py @@ -4,19 +4,9 @@ import re import sys -def trim_blank_ends(lines: list[str]) -> list[str]: - start = 0 - end = len(lines) - while start < end and lines[start] == "\n": - start += 1 - while end > start and lines[end - 1] == "\n": - end -= 1 - return lines[start:end] - - def filter_change_block( change_lines: list[str], search: str, replace: str -) -> list[str]: +) -> list[str] | None: removed_lines = [] added_lines = [] phase = "minus" @@ -37,16 +27,14 @@ def filter_change_block( transformed_removed = [ re.sub(search, replace, line) for line in removed_lines ] - if trim_blank_ends(transformed_removed) == trim_blank_ends(added_lines): + if transformed_removed == added_lines: return change_lines + if len(removed_lines) != len(added_lines): + return None + kept_lines = [] - max_len = max(len(removed_lines), len(added_lines)) - for idx in range(max_len): - removed = removed_lines[idx] if idx < len(removed_lines) else None - added = added_lines[idx] if idx < len(added_lines) else None - if removed is None: - break + for removed, added in zip(removed_lines, added_lines): if re.sub(search, replace, removed) == added: kept_lines.append(f"-{removed}") kept_lines.append(f"+{added}") @@ -54,16 +42,16 @@ def filter_change_block( kept_lines.append(f" {removed}") if not kept_lines: - return [] + return None return kept_lines def filter_hunk( hunk_lines: list[str], search: str, replace: str -) -> list[str]: +) -> list[str] | None: if not hunk_lines: - return [] + return None, False header = hunk_lines[0] body = hunk_lines[1:] @@ -85,7 +73,7 @@ def filter_hunk( i += 1 if not any(line.startswith(("+", "-")) for line in output_body): - return [] + return None return [header] + output_body @@ -97,8 +85,7 @@ def main() -> None: search = sys.argv[1] replace = sys.argv[2] - input_data = sys.stdin.read() - lines = input_data.splitlines(keepends=True) + lines = sys.stdin.read().splitlines(keepends=True) preamble_lines = [] sections = [] current = None @@ -127,23 +114,29 @@ def main() -> None: output_lines = [] output_lines.extend(preamble_lines) + left_out = False + for section in sections: kept_hunks = [] for hunk_lines in section["hunks"]: filtered = filter_hunk(hunk_lines, search, replace) if filtered: + if filtered != hunk_lines: + left_out = True kept_hunks.append(filtered) + elif hunk_lines: + left_out = True if not kept_hunks: + left_out = True continue output_lines.extend(section["header"]) for hunk_lines in kept_hunks: output_lines.extend(hunk_lines) - output_data = "".join(output_lines) - sys.stdout.write(output_data) - if output_data != input_data: + sys.stdout.write("".join(output_lines)) + if left_out: raise SystemExit(1) diff --git a/default.nix b/default.nix index 54cf645..79ed058 100644 --- a/default.nix +++ b/default.nix @@ -7,7 +7,6 @@ python3Packages, shellcheck-minimal, shfmt, - patchutils_0_4_2, }: let @@ -43,12 +42,11 @@ stdenv.mkDerivation { python3Packages.flake8 shellcheck-minimal shfmt - patchutils_0_4_2 ]; checkPhase = '' runHook preCheck - flake8 bin/mechanicaldiff + flake8 bin/mechanicaldiff.py shellcheck test/mechanicaldiff.bats shfmt -d test/mechanicaldiff.bats bats test @@ -57,7 +55,7 @@ stdenv.mkDerivation { installPhase = '' mkdir -p $out/bin - cp $src/bin/mechanicaldiff $out/bin/mechanicaldiff + cp $src/bin/mechanicaldiff.py $out/bin/mechanicaldiff chmod +x $out/bin/mechanicaldiff ''; } diff --git a/flake.nix b/flake.nix index 387b65c..0593261 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,6 @@ pkgs.python3Packages.flake8 pkgs.shellcheck pkgs.shfmt - pkgs.patchutils_0_4_2 ]; }; } diff --git a/test/mechanicaldiff.bats b/test/mechanicaldiff.bats index 82c7ea1..beb4853 100755 --- a/test/mechanicaldiff.bats +++ b/test/mechanicaldiff.bats @@ -15,7 +15,7 @@ setup() { printf "%s\n" "bar" >"$BATS_TEST_TMPDIR/new" diff_output="$(git diff --no-index "$BATS_TEST_TMPDIR/old" \ "$BATS_TEST_TMPDIR/new" || true)" - run mechanicaldiff foo bar <<<"$diff_output" + run mechanicaldiff.py foo bar <<<"$diff_output" assert_success assert_output "$diff_output" } @@ -25,7 +25,7 @@ setup() { printf "%s\n" "baz" >"$BATS_TEST_TMPDIR/new" git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ >"$BATS_TEST_TMPDIR/diff" || true - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff" + run mechanicaldiff.py foo bar <"$BATS_TEST_TMPDIR/diff" assert_failure assert_output "" } @@ -39,7 +39,7 @@ setup() { diff_output="$(git diff --no-index "$BATS_TEST_TMPDIR/old" \ "$BATS_TEST_TMPDIR/new" || true)" - run mechanicaldiff foo bar <<<"$diff_output" + run mechanicaldiff.py foo bar <<<"$diff_output" assert_failure assert_output "" } @@ -75,7 +75,7 @@ setup() { "$BATS_TEST_TMPDIR/new_kept" \ >"$BATS_TEST_TMPDIR/diff_expected" || true - run mechanicaldiff "beta" "beta_changed" <"$BATS_TEST_TMPDIR/diff_full" + run mechanicaldiff.py "beta" "beta_changed" <"$BATS_TEST_TMPDIR/diff_full" assert_failure assert_output --partial "-beta" assert_output --partial "+beta_changed" @@ -96,15 +96,15 @@ setup() { cat "$BATS_TEST_TMPDIR/diff_one" "$BATS_TEST_TMPDIR/diff_two" \ >"$BATS_TEST_TMPDIR/diff_all" - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff_one" + run mechanicaldiff.py foo bar <"$BATS_TEST_TMPDIR/diff_one" assert_success output_one="$output" - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff_two" + run mechanicaldiff.py foo bar <"$BATS_TEST_TMPDIR/diff_two" assert_failure output_two="$output" - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff_all" + run mechanicaldiff.py foo bar <"$BATS_TEST_TMPDIR/diff_all" assert_failure assert_output "${output_one}${output_two}" } @@ -122,7 +122,7 @@ setup() { git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ >"$BATS_TEST_TMPDIR/diff" || true - run mechanicaldiff 'foo\((\d+)\)' 'foo-\1' <"$BATS_TEST_TMPDIR/diff" + run mechanicaldiff.py 'foo\((\d+)\)' 'foo-\1' <"$BATS_TEST_TMPDIR/diff" assert_success assert_output "$(cat "$BATS_TEST_TMPDIR/diff")" } @@ -138,109 +138,7 @@ setup() { git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ >"$BATS_TEST_TMPDIR/diff" || true - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff" - assert_success - assert_output "$(cat "$BATS_TEST_TMPDIR/diff")" -} - -@test "matches when replacement removes entire line" { - cat >"$BATS_TEST_TMPDIR/old" <<-'EOF' - alpha - foo - omega - EOF - cat >"$BATS_TEST_TMPDIR/new" <<-'EOF' - alpha - omega - EOF - - git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ - >"$BATS_TEST_TMPDIR/diff" || true - - run mechanicaldiff foo "" <"$BATS_TEST_TMPDIR/diff" - assert_success - assert_output "$(cat "$BATS_TEST_TMPDIR/diff")" -} - -@test "keeps matching change with an extra removed line" { - cat >"$BATS_TEST_TMPDIR/old" <<-'EOF' - foo - bar - baz - EOF - cat >"$BATS_TEST_TMPDIR/new" <<-'EOF' - foo_changed - baz - EOF - - git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ - >"$BATS_TEST_TMPDIR/diff" || true - - cat >"$BATS_TEST_TMPDIR/new_kept" <<-'EOF' - foo_changed - bar - baz - EOF - git diff --no-index "$BATS_TEST_TMPDIR/old" \ - "$BATS_TEST_TMPDIR/new_kept" \ - >"$BATS_TEST_TMPDIR/diff_expected" || true - - run mechanicaldiff foo foo_changed <"$BATS_TEST_TMPDIR/diff" - assert_failure - expected="$(sed \ - -e 's/new_kept/new/g' \ - -e '/^index /d' \ - "$BATS_TEST_TMPDIR/diff_expected")" - output_normalized="$(printf '%s\n' "$output" | recountdiff | sed -e '/^index /d')" - assert_equal "$output_normalized" "$expected" -} - -@test "keeps matching change with an extra added line" { - cat >"$BATS_TEST_TMPDIR/old" <<-'EOF' - foo - baz - EOF - cat >"$BATS_TEST_TMPDIR/new" <<-'EOF' - foo_changed - bar - baz - EOF - - git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ - >"$BATS_TEST_TMPDIR/diff" || true - - cat >"$BATS_TEST_TMPDIR/new_kept" <<-'EOF' - foo_changed - baz - EOF - git diff --no-index "$BATS_TEST_TMPDIR/old" \ - "$BATS_TEST_TMPDIR/new_kept" \ - >"$BATS_TEST_TMPDIR/diff_expected" || true - - run mechanicaldiff foo foo_changed <"$BATS_TEST_TMPDIR/diff" - assert_failure - expected="$(sed \ - -e 's/new_kept/new/g' \ - -e '/^index /d' \ - "$BATS_TEST_TMPDIR/diff_expected")" - output_normalized="$(printf '%s\n' "$output" | recountdiff | sed -e '/^index /d')" - assert_equal "$output_normalized" "$expected" -} - -@test "ignores leading and trailing blank lines for comparison" { - cat >"$BATS_TEST_TMPDIR/old" <<-'EOF' - - prefix foo suffix - - EOF - cat >"$BATS_TEST_TMPDIR/new" <<-'EOF' - prefix bar suffix - EOF - - git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ - >"$BATS_TEST_TMPDIR/diff" || true - - run mechanicaldiff foo bar <"$BATS_TEST_TMPDIR/diff" + run mechanicaldiff.py foo bar <"$BATS_TEST_TMPDIR/diff" assert_success assert_output "$(cat "$BATS_TEST_TMPDIR/diff")" } @@ -263,14 +161,14 @@ setup() { git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new_kept" \ >"$BATS_TEST_TMPDIR/diff_expected" || true - run mechanicaldiff foo foo_changed <"$BATS_TEST_TMPDIR/diff_full" + run mechanicaldiff.py foo foo_changed <"$BATS_TEST_TMPDIR/diff_full" assert_failure expected="$(sed \ -e 's/new_kept/new/g' \ -e '/^index /d' \ "$BATS_TEST_TMPDIR/diff_expected")" output_normalized="$(printf '%s\n' "$output" | sed -e '/^index /d')" - assert_equal "$output_normalized" "$expected" + assert_equal "$expected" "$output_normalized" } @test "drops hunk when all pairs become context" { @@ -287,7 +185,7 @@ setup() { git diff --no-index "$BATS_TEST_TMPDIR/old" "$BATS_TEST_TMPDIR/new" \ >"$BATS_TEST_TMPDIR/diff_full" || true - run mechanicaldiff qux quux <"$BATS_TEST_TMPDIR/diff_full" + run mechanicaldiff.py qux quux <"$BATS_TEST_TMPDIR/diff_full" assert_failure assert_output "" }