Jelajahi Sumber

add --list to branch command (#1882)

Implements the --list flag for the dulwich branch command, 
which display list branches matching a pattern.
xifOO 4 bulan lalu
induk
melakukan
e32d8b3dc2
4 mengubah file dengan 153 tambahan dan 49 penghapusan
  1. 35 48
      dulwich/cli.py
  2. 16 1
      dulwich/porcelain.py
  3. 25 0
      tests/test_cli.py
  4. 77 0
      tests/test_porcelain.py

+ 35 - 48
dulwich/cli.py

@@ -2164,6 +2164,12 @@ class cmd_branch(Command):
         parser.add_argument(
             "--column", action="store_true", help="Display branch list in columns"
         )
+        parser.add_argument(
+            "--list",
+            nargs="?",
+            const=None,
+            help="List branches matching a pattern",
+        )
         args = parser.parse_args(args)
 
         def print_branches(
@@ -2175,61 +2181,42 @@ class cmd_branch(Command):
                 for branch in branches:
                     sys.stdout.write(f"{branch.decode()}\n")
 
-        if args.all:
-            try:
+        branches: Union[Iterator[bytes], list[bytes], None] = None
+
+        try:
+            if args.all:
                 branches = porcelain.branch_list(".") + porcelain.branch_remotes_list(
                     "."
                 )
-                print_branches(branches, args.column)
-                return 0
-
-            except porcelain.Error as e:
-                sys.stderr.write(f"{e}")
-                return 1
-
-        if args.merged:
-            try:
-                branches_iter = porcelain.merged_branches(".")
-                print_branches(branches_iter, args.column)
-                return 0
-
-            except porcelain.Error as e:
-                sys.stderr.write(f"{e}")
-                return 1
-
-        if args.no_merged:
-            try:
-                branches_iter = porcelain.no_merged_branches(".")
-                print_branches(branches_iter, args.column)
-                return 0
-
-            except porcelain.Error as e:
-                sys.stderr.write(f"{e}")
-                return 1
-
-        if args.contains:
-            try:
-                branches_iter = porcelain.branches_containing(".", commit=args.contains)
-                print_branches(branches_iter, args.column)
-                return 0
+            elif args.remotes:
+                branches = porcelain.branch_remotes_list(".")
+            elif args.merged:
+                branches = porcelain.merged_branches(".")
+            elif args.no_merged:
+                branches = porcelain.no_merged_branches(".")
+            elif args.contains:
+                try:
+                    branches = list(
+                        porcelain.branches_containing(".", commit=args.contains)
+                    )
 
-            except KeyError as e:
-                sys.stderr.write(f"error: object name {e.args[0].decode()} not found\n")
-                return 1
+                except KeyError as e:
+                    sys.stderr.write(
+                        f"error: object name {e.args[0].decode()} not found\n"
+                    )
+                    return 1
 
-            except porcelain.Error as e:
-                sys.stderr.write(f"{e}")
-                return 1
+        except porcelain.Error as e:
+            sys.stderr.write(f"{e}")
+            return 1
 
-        if args.remotes:
-            try:
-                branches = porcelain.branch_remotes_list(".")
-                print_branches(branches, args.column)
-                return 0
+        pattern = args.list
+        if pattern is not None and branches:
+            branches = porcelain.filter_branches_by_pattern(branches, pattern)
 
-            except porcelain.Error as e:
-                sys.stderr.write(f"{e}")
-                return 1
+        if branches is not None:
+            print_branches(branches, args.column)
+            return 0
 
         if not args.branch:
             logger.error("Usage: dulwich branch [-d] BRANCH_NAME")

+ 16 - 1
dulwich/porcelain.py

@@ -87,7 +87,7 @@ import stat
 import sys
 import time
 from collections import namedtuple
-from collections.abc import Iterator
+from collections.abc import Iterable, Iterator
 from contextlib import AbstractContextManager, closing, contextmanager
 from dataclasses import dataclass
 from io import BytesIO, RawIOBase
@@ -3208,6 +3208,21 @@ def branch_create(
                     repo_config.write_to_path()
 
 
+def filter_branches_by_pattern(branches: Iterable[bytes], pattern: str) -> list[bytes]:
+    """Filter branches by fnmatch pattern.
+
+    Args:
+        branches: Iterable of branch names as bytes
+        pattern: Pattern to match against
+
+    Returns:
+        List of filtered branch names
+    """
+    return [
+        branch for branch in branches if fnmatch.fnmatchcase(branch.decode(), pattern)
+    ]
+
+
 def branch_list(repo: RepoPath) -> list[bytes]:
     """List all branches.
 

+ 25 - 0
tests/test_cli.py

@@ -663,6 +663,31 @@ class BranchCommandTest(DulwichCliTestCase):
         )
         self.assertTrue(multiple_columns)
 
+    def test_branch_list_flag(self):
+        # Create an initial commit
+        test_file = os.path.join(self.repo_path, "test.txt")
+        with open(test_file, "w") as f:
+            f.write("test")
+        self._run_cli("add", "test.txt")
+        self._run_cli("commit", "--message=Initial")
+
+        # Create local branches
+        self._run_cli("branch", "feature-1")
+        self._run_cli("branch", "feature-2")
+        self._run_cli("branch", "branch-1")
+
+        # Run `branch --list` with a pattern "feature-*"
+        result, stdout, stderr = self._run_cli("branch", "--all", "--list", "feature-*")
+        self.assertEqual(result, 0)
+
+        # Collect branches from the output
+        branches = [line.strip() for line in stdout.splitlines()]
+
+        # Expected branches — exactly those matching the pattern
+        expected_branches = ["feature-1", "feature-2"]
+
+        self.assertEqual(branches, expected_branches)
+
 
 class TestTerminalWidth(TestCase):
     @patch("os.get_terminal_size")

+ 77 - 0
tests/test_porcelain.py

@@ -7078,6 +7078,83 @@ class BranchContainsTests(PorcelainTestCase):
         self.assertEqual([b"master"], result)
 
 
+class FilterBranchesByPatternTests(PorcelainTestCase):
+    """Tests for filter_branches_by_pattern function."""
+
+    def test_empty_branches(self) -> None:
+        """Test with empty branches list."""
+        result = porcelain.filter_branches_by_pattern([], "feature-*")
+        self.assertEqual([], result)
+
+    def test_star_pattern(self) -> None:
+        """Test wildcard pattern matching."""
+        branches = [b"main", b"feature-1", b"feature-2", b"develop", b"hotfix-bug"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature-*")
+        expected = [b"feature-1", b"feature-2"]
+        self.assertEqual(expected, result)
+
+    def test_question_mark_pattern(self) -> None:
+        """Test single character wildcard pattern."""
+        branches = [b"main", b"feature-1", b"feature-2", b"feature-a", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature-?")
+        expected = [b"feature-1", b"feature-2", b"feature-a"]
+        self.assertEqual(expected, result)
+
+    def test_specific_pattern(self) -> None:
+        """Test exact match pattern."""
+        branches = [b"main", b"feature-1", b"feature-2", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature-1")
+        expected = [b"feature-1"]
+        self.assertEqual(expected, result)
+
+    def test_no_matches(self) -> None:
+        """Test pattern that matches no branches."""
+        branches = [b"main", b"feature-1", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "release-*")
+        self.assertEqual([], result)
+
+    def test_case_sensitive_pattern(self) -> None:
+        """Test case-sensitive pattern matching."""
+        branches = [b"Main", b"main", b"MAIN", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "main")
+        expected = [b"main"]
+        self.assertEqual(expected, result)
+
+    def test_multiple_patterns_behavior(self) -> None:
+        """Test that pattern works with multiple wildcards."""
+        branches = [
+            b"feature-login",
+            b"feature-signup",
+            b"bugfix-login",
+            b"hotfix-signup",
+        ]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature-*")
+        expected = [b"feature-login", b"feature-signup"]
+        self.assertEqual(expected, result)
+
+    def test_mixed_encoding_branches(self) -> None:
+        """Test with branches that have special characters."""
+        branches = [b"feature-1", b"feature/test", b"feature@prod", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature/*")
+        expected = [b"feature/test"]
+        self.assertEqual(expected, result)
+
+    def test_pattern_with_square_brackets(self) -> None:
+        """Test pattern with character classes."""
+        branches = [b"feature-1", b"feature-2", b"feature-a", b"feature-b", b"develop"]
+
+        result = porcelain.filter_branches_by_pattern(branches, "feature-[12]")
+        expected = [b"feature-1", b"feature-2"]
+        self.assertEqual(expected, result)
+
+
 class BranchCreateTests(PorcelainTestCase):
     def test_branch_exists(self) -> None:
         [c1] = build_commit_graph(self.repo.object_store, [[1]])