Просмотр исходного кода

add --list to branch command (#1882)

Implements the --list flag for the dulwich branch command, 
which display list branches matching a pattern.
xifOO 4 месяцев назад
Родитель
Сommit
e32d8b3dc2
4 измененных файлов с 153 добавлено и 49 удалено
  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(
         parser.add_argument(
             "--column", action="store_true", help="Display branch list in columns"
             "--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)
         args = parser.parse_args(args)
 
 
         def print_branches(
         def print_branches(
@@ -2175,61 +2181,42 @@ class cmd_branch(Command):
                 for branch in branches:
                 for branch in branches:
                     sys.stdout.write(f"{branch.decode()}\n")
                     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(
                 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:
         if not args.branch:
             logger.error("Usage: dulwich branch [-d] BRANCH_NAME")
             logger.error("Usage: dulwich branch [-d] BRANCH_NAME")

+ 16 - 1
dulwich/porcelain.py

@@ -87,7 +87,7 @@ import stat
 import sys
 import sys
 import time
 import time
 from collections import namedtuple
 from collections import namedtuple
-from collections.abc import Iterator
+from collections.abc import Iterable, Iterator
 from contextlib import AbstractContextManager, closing, contextmanager
 from contextlib import AbstractContextManager, closing, contextmanager
 from dataclasses import dataclass
 from dataclasses import dataclass
 from io import BytesIO, RawIOBase
 from io import BytesIO, RawIOBase
@@ -3208,6 +3208,21 @@ def branch_create(
                     repo_config.write_to_path()
                     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]:
 def branch_list(repo: RepoPath) -> list[bytes]:
     """List all branches.
     """List all branches.
 
 

+ 25 - 0
tests/test_cli.py

@@ -663,6 +663,31 @@ class BranchCommandTest(DulwichCliTestCase):
         )
         )
         self.assertTrue(multiple_columns)
         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):
 class TestTerminalWidth(TestCase):
     @patch("os.get_terminal_size")
     @patch("os.get_terminal_size")

+ 77 - 0
tests/test_porcelain.py

@@ -7078,6 +7078,83 @@ class BranchContainsTests(PorcelainTestCase):
         self.assertEqual([b"master"], result)
         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):
 class BranchCreateTests(PorcelainTestCase):
     def test_branch_exists(self) -> None:
     def test_branch_exists(self) -> None:
         [c1] = build_commit_graph(self.repo.object_store, [[1]])
         [c1] = build_commit_graph(self.repo.object_store, [[1]])