Forráskód Böngészése

add --all flag to branch command (#1866)

Implements the --all flag for the dulwich branch command, which
lists both local and remote-tracking branches.
 #1847
xifOO 4 hónapja
szülő
commit
72e5a8907d
4 módosított fájl, 271 hozzáadás és 0 törlés
  1. 17 0
      dulwich/cli.py
  2. 57 0
      dulwich/porcelain.py
  3. 37 0
      tests/test_cli.py
  4. 160 0
      tests/test_porcelain.py

+ 17 - 0
dulwich/cli.py

@@ -2049,6 +2049,7 @@ class cmd_branch(Command):
         parser.add_argument(
             "branch",
             type=str,
+            nargs="?",
             help="Name of the branch",
         )
         parser.add_argument(
@@ -2057,7 +2058,23 @@ class cmd_branch(Command):
             action="store_true",
             help="Delete branch",
         )
+        parser.add_argument("--all", action="store_true", help="List all branches")
         args = parser.parse_args(args)
+
+        if args.all:
+            try:
+                branches = porcelain.branch_list(".") + porcelain.branch_remotes_list(
+                    "."
+                )
+
+                for branch in branches:
+                    sys.stdout.write(f"{branch.decode()}\n")
+
+                return 0
+            except porcelain.Error as e:
+                sys.stderr.write(f"{e}")
+                return 1
+
         if not args.branch:
             logger.error("Usage: dulwich branch [-d] BRANCH_NAME")
             return 1

+ 57 - 0
dulwich/porcelain.py

@@ -183,6 +183,7 @@ from .protocol import ZERO_SHA, Protocol
 from .refs import (
     LOCAL_BRANCH_PREFIX,
     LOCAL_NOTES_PREFIX,
+    LOCAL_REMOTE_PREFIX,
     LOCAL_TAG_PREFIX,
     Ref,
     SymrefLoop,
@@ -3250,6 +3251,62 @@ def branch_list(repo: RepoPath) -> list[bytes]:
         return branches
 
 
+def branch_remotes_list(repo: RepoPath) -> list[bytes]:
+    """List the short names of all remote branches.
+
+    Args:
+      repo: Path to the repository
+    Returns:
+      List of branch names (without refs/remotes/ prefix, and without remote name; e.g. 'main' from 'origin/main')
+    """
+    with open_repo_closing(repo) as r:
+        branches = list(r.refs.keys(base=LOCAL_REMOTE_PREFIX))
+
+        config = r.get_config_stack()
+        try:
+            sort_key = config.get((b"branch",), b"sort").decode()
+        except KeyError:
+            # Default is refname (alphabetical)
+            sort_key = "refname"
+
+        # Parse sort key
+        reverse = False
+        if sort_key.startswith("-"):
+            reverse = True
+            sort_key = sort_key[1:]
+
+        # Apply sorting
+        if sort_key == "refname":
+            # Simple alphabetical sort (default)
+            branches.sort(reverse=reverse)
+        elif sort_key in ("committerdate", "authordate"):
+            # Sort by date
+            def get_commit_date(branch_name: bytes) -> int:
+                ref = LOCAL_REMOTE_PREFIX + branch_name
+                sha = r.refs[ref]
+                commit = r.object_store[sha]
+                assert isinstance(commit, Commit)
+                if sort_key == "committerdate":
+                    return commit.commit_time
+                else:  # authordate
+                    return commit.author_time
+
+            # Sort branches by date
+            # Note: Python's sort naturally orders smaller values first (ascending)
+            # For dates, this means oldest first by default
+            # Use a stable sort with branch name as secondary key for consistent ordering
+            if reverse:
+                # For reverse sort, we want newest dates first but alphabetical names second
+                branches.sort(key=lambda b: (-get_commit_date(b), b))
+            else:
+                branches.sort(key=lambda b: (get_commit_date(b), b))
+        else:
+            # Unknown sort key
+            raise ValueError(f"Unknown sort key: {sort_key}")
+
+        return branches
+
+
 def active_branch(repo: RepoPath) -> bytes:
     """Return the active branch in the repository, if any.
 

+ 37 - 0
tests/test_cli.py

@@ -422,6 +422,43 @@ class BranchCommandTest(DulwichCliTestCase):
         result, stdout, stderr = self._run_cli("branch", "-d", "test-branch")
         self.assertNotIn(b"refs/heads/test-branch", self.repo.refs.keys())
 
+    def test_branch_list_all(self):
+        # Create initial commit and local branches
+        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 test branches
+        self._run_cli("branch", "feature-1")
+        self._run_cli("branch", "feature-2")
+
+        # Setup a remote and create remote branches
+        self.repo.refs[b"refs/remotes/origin/master"] = self.repo.refs[
+            b"refs/heads/master"
+        ]
+        self.repo.refs[b"refs/remotes/origin/feature-remote"] = self.repo.refs[
+            b"refs/heads/master"
+        ]
+
+        # Test --all listing
+        result, stdout, stderr = self._run_cli("branch", "--all")
+        self.assertEqual(result, 0)
+
+        expected_branches = {
+            "feature-1",  # local branch
+            "feature-2",  # local branch
+            "master",  # local branch
+            "origin/master",  # remote branch
+            "origin/feature-remote",  # remote branch
+        }
+        lines = [line.strip() for line in stdout.splitlines()]
+
+        # All branches from stdout
+        all_branches = set(line for line in lines)
+        self.assertEqual(all_branches, expected_branches)
+
 
 class CheckoutCommandTest(DulwichCliTestCase):
     """Tests for checkout command."""

+ 160 - 0
tests/test_porcelain.py

@@ -6557,6 +6557,166 @@ class BranchListTests(PorcelainTestCase):
         self.assertEqual([b"alpha", b"beta", b"master", b"zebra"], branches)
 
 
+class BranchRemoteListTests(PorcelainTestCase):
+    def test_no_remote_branches(self) -> None:
+        """Test with no remote branches."""
+        result = porcelain.branch_remotes_list(self.repo)
+        self.assertEqual([], result)
+
+    def test_remote_branches_refname_sort(self) -> None:
+        """Test remote branches sorting with refname (alphabetical)."""
+        # Create some remote branches
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+
+        # Create remote branches is non-alphabetical order
+        self.repo.refs[b"refs/remotes/origin/feature-1"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-2"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-3"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-4"] = c1.id
+
+        # Set branch.sort to refname
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"refname")
+        config.write_to_path()
+
+        # Should return only branch names, sorted alphabetically
+        branches = porcelain.branch_remotes_list(self.repo)
+        expected = [
+            b"origin/feature-1",
+            b"origin/feature-2",
+            b"origin/feature-3",
+            b"origin/feature-4",
+        ]
+        self.assertEqual(expected, branches)
+
+    def test_remote_branches_refname_reverse_sort(self) -> None:
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+
+        self.repo.refs[b"refs/remotes/origin/feature-1"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-2"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-3"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-4"] = c1.id
+
+        # Set branch.sort to -refname
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"-refname")
+        config.write_to_path()
+
+        branches = porcelain.branch_remotes_list(self.repo)
+        expected = [
+            b"origin/feature-4",
+            b"origin/feature-3",
+            b"origin/feature-2",
+            b"origin/feature-1",
+        ]
+        self.assertEqual(expected, branches)
+
+    def test_remote_branches_committerdate_sort(self) -> None:
+        """Test remote branches sorting with committerdate"""
+        # Create commits with different timestamps
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store,
+            [[1], [2], [3]],
+            attrs={
+                1: {"commit_time": 1000},
+                2: {"commit_time": 2000},
+                3: {"commit_time": 3000},
+            },
+        )
+
+        self.repo.refs[b"refs/remotes/origin/oldest"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/middle"] = c2.id
+        self.repo.refs[b"refs/remotes/origin/newest"] = c3.id
+
+        # Set branch.sort to committerdate
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"committerdate")
+        config.write_to_path()
+
+        branches = porcelain.branch_remotes_list(self.repo)
+        # Should be sorted by commit time (oldest first), then alphabetically for same time
+        expected = [b"origin/oldest", b"origin/middle", b"origin/newest"]
+        self.assertEqual(expected, branches)
+
+    def test_remote_branches_committerdate_reverse_sort(self) -> None:
+        """Test remote branches sorting with -committerdate."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store,
+            [[1], [2], [3]],
+            attrs={
+                1: {"commit_time": 1000},
+                2: {"commit_time": 2000},
+                3: {"commit_time": 1500},
+            },
+        )
+
+        self.repo.refs[b"refs/remotes/origin/oldest"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/newest"] = c2.id
+        self.repo.refs[b"refs/remotes/origin/middle"] = c3.id
+
+        # Set branch.sort to -committerdate
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"-committerdate")
+        config.write_to_path()
+
+        branches = porcelain.branch_remotes_list(self.repo)
+        # Should be sorted by commit time (newest first), then alphabetically for same time
+        expected = [b"origin/newest", b"origin/middle", b"origin/oldest"]
+        self.assertEqual(expected, branches)
+
+    def test_remote_branches_authordate_sort(self) -> None:
+        """Test remote branches sorting with authordate."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store,
+            [[1], [2], [3]],
+            attrs={
+                1: {"author_time": 1500},
+                2: {"author_time": 2000},
+                3: {"author_time": 1000},
+            },
+        )
+
+        self.repo.refs[b"refs/remotes/origin/middle"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/newest"] = c2.id
+        self.repo.refs[b"refs/remotes/origin/oldest"] = c3.id
+
+        # Set branch.sort to authordate
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"authordate")
+        config.write_to_path()
+
+        branches = porcelain.branch_remotes_list(self.repo)
+        expected = [b"origin/oldest", b"origin/middle", b"origin/newest"]
+        self.assertEqual(expected, branches)
+
+    def test_unknown_sort_key(self) -> None:
+        """Test that unknown sort key raises ValueError."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo.refs[b"refs/remotes/origin/master"] = c1.id
+
+        # Set branch.sort to unknown key
+        config = self.repo.get_config()
+        config.set((b"branch",), b"sort", b"unknown")
+        config.write_to_path()
+
+        with self.assertRaises(ValueError) as cm:
+            porcelain.branch_remotes_list(self.repo)
+        self.assertIn("Unknown sort key: unknown", str(cm.exception))
+
+    def test_default_sort_no_config(self) -> None:
+        """Test default sorting when no config is set."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+
+        self.repo.refs[b"refs/remotes/origin/feature-1"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-2"] = c1.id
+        self.repo.refs[b"refs/remotes/origin/feature-3"] = c1.id
+
+        # No config set - should default to alphabetical
+        branches = porcelain.branch_remotes_list(self.repo)
+        expected = [b"origin/feature-1", b"origin/feature-2", b"origin/feature-3"]
+        self.assertEqual(expected, branches)
+
+
 class BranchCreateTests(PorcelainTestCase):
     def test_branch_exists(self) -> None:
         [c1] = build_commit_graph(self.repo.object_store, [[1]])