Parcourir la source

Add --stat argument to diff

As part of this, move diffstat out of contrib/
Jelmer Vernooij il y a 1 mois
Parent
commit
3c0901e1c0
7 fichiers modifiés avec 62 ajouts et 20 suppressions
  1. 27 4
      dulwich/cli.py
  2. 0 1
      dulwich/contrib/__init__.py
  3. 3 2
      dulwich/diffstat.py
  4. 1 0
      tests/__init__.py
  5. 0 1
      tests/contrib/__init__.py
  6. 19 0
      tests/test_cli.py
  7. 12 12
      tests/test_diffstat.py

+ 27 - 4
dulwich/cli.py

@@ -1468,6 +1468,11 @@ class cmd_diff(Command):
             default="myers",
             help="Choose a diff algorithm",
         )
+        parser.add_argument(
+            "--stat",
+            action="store_true",
+            help="Show diffstat instead of full diff",
+        )
         parser.add_argument(
             "--", dest="separator", action="store_true", help=argparse.SUPPRESS
         )
@@ -1518,7 +1523,17 @@ class cmd_diff(Command):
         with Repo(".") as repo:
             config = repo.get_config_stack()
             with get_pager(config=config, cmd_name="diff") as outstream:
-                output_stream = _create_output_stream(outstream)
+                # For --stat mode, capture the diff in a BytesIO buffer
+                if parsed_args.stat:
+                    import io
+
+                    from .diffstat import diffstat
+
+                    diff_buffer: BinaryIO = io.BytesIO()
+                    output_stream: BinaryIO = diff_buffer
+                else:
+                    output_stream = _create_output_stream(outstream)
+
                 try:
                     if len(parsed_args.committish) == 0:
                         # Show diff for working tree or staged changes
@@ -1559,9 +1574,17 @@ class cmd_diff(Command):
                     sys.stderr.write(f"fatal: {e}\n")
                     sys.exit(1)
 
-                # Flush any remaining output
-                if hasattr(output_stream, "flush"):
-                    output_stream.flush()
+                if parsed_args.stat:
+                    # Generate and output diffstat from captured diff
+                    assert isinstance(diff_buffer, io.BytesIO)
+                    diff_data = diff_buffer.getvalue()
+                    lines = diff_data.split(b"\n")
+                    stat_output = diffstat(lines)
+                    outstream.buffer.write(stat_output + b"\n")
+                else:
+                    # Flush any remaining output
+                    if hasattr(output_stream, "flush"):
+                        output_stream.flush()
 
 
 class cmd_dump_pack(Command):

+ 0 - 1
dulwich/contrib/__init__.py

@@ -26,7 +26,6 @@ functionality. These modules are maintained as part of dulwich but may
 have additional dependencies or more specialized use cases.
 
 Available modules:
-- diffstat: Generate diff statistics similar to git's --stat option
 - greenthreads: Green-threaded support for finding missing objects
 - paramiko_vendor: SSH client implementation using paramiko
 - release_robot: Automated release management utilities

+ 3 - 2
dulwich/contrib/diffstat.py → dulwich/diffstat.py

@@ -46,6 +46,7 @@ statistics about changes, including:
 __all__ = [
     "diffstat",
     "main",
+    "parse_patch",
 ]
 
 import re
@@ -71,7 +72,7 @@ _GIT_UNCHANGED_START = b" "
 # properly interface with diffstat routine
 
 
-def _parse_patch(
+def parse_patch(
     lines: Sequence[bytes],
 ) -> tuple[list[bytes], list[bool], list[tuple[int, int]]]:
     """Parse a git style diff or patch to generate diff stats.
@@ -136,7 +137,7 @@ def diffstat(lines: Sequence[bytes], max_width: int = 80) -> bytes:
     Returns: A byte string that lists the changed files with change
              counts and histogram.
     """
-    names, nametypes, counts = _parse_patch(lines)
+    names, nametypes, counts = parse_patch(lines)
     insert = []
     delete = []
     namelen = 0

+ 1 - 0
tests/__init__.py

@@ -141,6 +141,7 @@ def self_test_suite() -> unittest.TestSuite:
         "credentials",
         "diff",
         "diff_tree",
+        "diffstat",
         "dumb",
         "fastexport",
         "file",

+ 0 - 1
tests/contrib/__init__.py

@@ -24,7 +24,6 @@ import unittest
 
 def test_suite() -> unittest.TestSuite:
     names = [
-        "diffstat",
         "greenthreads",
         "paramiko_vendor",
         "release_robot",

+ 19 - 0
tests/test_cli.py

@@ -1193,6 +1193,25 @@ class DiffCommandTest(DulwichCliTestCase):
         self.assertIn("+newer1", stdout)
         self.assertNotIn("file2.txt", stdout)
 
+    def test_diff_stat(self):
+        # Create and commit a file
+        test_file = os.path.join(self.repo_path, "test.txt")
+        with open(test_file, "w") as f:
+            f.write("initial content\n")
+        self._run_cli("add", "test.txt")
+        self._run_cli("commit", "--message=Initial")
+
+        # Modify the file
+        with open(test_file, "w") as f:
+            f.write("initial content\nmodified\n")
+
+        # Test --stat output
+        _result, stdout, _stderr = self._run_cli("diff", "--stat")
+        self.assertEqual(
+            stdout,
+            " test.txt | 1 +\n 1 files changed, 1 insertions(+), 0 deletions(-)\n",
+        )
+
 
 class FilterBranchCommandTest(DulwichCliTestCase):
     """Tests for filter-branch command."""

+ 12 - 12
tests/contrib/test_diffstat.py → tests/test_diffstat.py

@@ -3,25 +3,25 @@
 # Copyright (c) 2025 Test Contributor
 # All rights reserved.
 
-"""Tests for dulwich.contrib.diffstat."""
+"""Tests for dulwich.diffstat."""
 
 import os
 import tempfile
 import unittest
 
-from dulwich.contrib.diffstat import (
-    _parse_patch,
+from dulwich.diffstat import (
     diffstat,
     main,
+    parse_patch,
 )
 
 
 class ParsePatchTests(unittest.TestCase):
-    """Tests for _parse_patch function."""
+    """Tests for parse_patch function."""
 
     def test_empty_input(self):
         """Test parsing an empty list of lines."""
-        names, nametypes, counts = _parse_patch([])
+        names, nametypes, counts = parse_patch([])
         self.assertEqual(names, [])
         self.assertEqual(nametypes, [])
         self.assertEqual(counts, [])
@@ -42,7 +42,7 @@ class ParsePatchTests(unittest.TestCase):
             b"+third added line",
             b" unchanged line",
         ]
-        names, nametypes, counts = _parse_patch(diff)
+        names, nametypes, counts = parse_patch(diff)
         self.assertEqual(names, [b"file.txt"])
         self.assertEqual(nametypes, [False])  # Not a binary file
         self.assertEqual(counts, [(3, 2)])  # 3 additions, 2 deletions
@@ -73,7 +73,7 @@ class ParsePatchTests(unittest.TestCase):
             b"+added in file2",
             b" another unchanged in file2",
         ]
-        names, nametypes, counts = _parse_patch(diff)
+        names, nametypes, counts = parse_patch(diff)
         self.assertEqual(names, [b"file.txt", b"file2.txt"])
         self.assertEqual(nametypes, [False, False])
         self.assertEqual(
@@ -87,7 +87,7 @@ class ParsePatchTests(unittest.TestCase):
             b"index 1234567..abcdefg 100644",
             b"Binary files a/image.png and b/image.png differ",
         ]
-        names, nametypes, counts = _parse_patch(diff)
+        names, nametypes, counts = parse_patch(diff)
         self.assertEqual(names, [b"image.png"])
         self.assertEqual(nametypes, [True])  # Is a binary file
         self.assertEqual(counts, [(0, 0)])  # No additions/deletions counted
@@ -108,7 +108,7 @@ class ParsePatchTests(unittest.TestCase):
             b"+added line",
             b" third unchanged line",
         ]
-        names, nametypes, counts = _parse_patch(diff)
+        names, nametypes, counts = parse_patch(diff)
         # The name should include both old and new names
         self.assertEqual(names, [b"oldname.txt => newname.txt"])
         self.assertEqual(nametypes, [False])  # Not a binary file
@@ -137,7 +137,7 @@ class ParsePatchTests(unittest.TestCase):
             b"-deleted",
             b" unchanged",
         ]
-        names, nametypes, counts = _parse_patch(diff)
+        names, nametypes, counts = parse_patch(diff)
         self.assertEqual(names, [b"file1.txt", b"file2.txt"])
         self.assertEqual(nametypes, [False, False])
         self.assertEqual(
@@ -471,7 +471,7 @@ index 1234567..abcdefg 100644
         import io
         import sys
 
-        from dulwich.contrib.diffstat import diffstat as real_diffstat
+        from dulwich.diffstat import diffstat as real_diffstat
 
         # Save original sys.argv, diffstat function, and stdout
         orig_argv = sys.argv
@@ -488,7 +488,7 @@ index 1234567..abcdefg 100644
 
             # Mock the diffstat function to return a wrong result
             # This will trigger the self-test failure path
-            from dulwich.contrib import diffstat as diffstat_module
+            from dulwich import diffstat as diffstat_module
 
             diffstat_module.diffstat = lambda lines, max_width=80: b"WRONG OUTPUT"