Przeglądaj źródła

add --merged to branch command (#1873)

Implements the --merged flag for the dulwich branch command,
which lists branches that have been merged into the current branch.
https://github.com/jelmer/dulwich/issues/1847
xifOO 4 miesięcy temu
rodzic
commit
c1087d3ceb
4 zmienionych plików z 145 dodań i 0 usunięć
  1. 15 0
      dulwich/cli.py
  2. 22 0
      dulwich/porcelain.py
  3. 36 0
      tests/test_cli.py
  4. 72 0
      tests/test_porcelain.py

+ 15 - 0
dulwich/cli.py

@@ -2059,6 +2059,9 @@ class cmd_branch(Command):
             help="Delete branch",
         )
         parser.add_argument("--all", action="store_true", help="List all branches")
+        parser.add_argument(
+            "--merged", action="store_true", help="List merged branches"
+        )
         parser.add_argument(
             "--remotes", action="store_true", help="List remotes branches"
         )
@@ -2078,6 +2081,18 @@ class cmd_branch(Command):
                 sys.stderr.write(f"{e}")
                 return 1
 
+        if args.merged:
+            try:
+                branches_iter = porcelain.merged_branches(".")
+
+                for branch in branches_iter:
+                    sys.stdout.write(f"{branch.decode()}\n")
+
+                return 0
+            except porcelain.Error as e:
+                sys.stderr.write(f"{e}")
+                return 1
+
         if args.remotes:
             try:
                 branches = porcelain.branch_remotes_list(".")

+ 22 - 0
dulwich/porcelain.py

@@ -3307,6 +3307,28 @@ def branch_remotes_list(repo: RepoPath) -> list[bytes]:
         return branches
 
 
+def merged_branches(repo: RepoPath) -> Iterator[bytes]:
+    """List branches that have been merged into the current branch.
+
+    Args:
+      repo: Path to the repository
+    Yields:
+      Branch names (without refs/heads/ prefix) that are merged
+      into the current HEAD
+    """
+    with open_repo_closing(repo) as r:
+        current_sha = r.refs[b"HEAD"]
+
+        for branch_ref in r.refs.keys(base=b"refs/heads/"):
+            full_ref = b"refs/heads/" + branch_ref
+
+            branch_sha = r.refs[full_ref]
+
+            # Check if branch is an ancestor of HEAD (fully merged)
+            if can_fast_forward(r, branch_sha, current_sha):
+                yield branch_ref
+
+
 def active_branch(repo: RepoPath) -> bytes:
     """Return the active branch in the repository, if any.
 

+ 36 - 0
tests/test_cli.py

@@ -459,6 +459,42 @@ class BranchCommandTest(DulwichCliTestCase):
         all_branches = set(line for line in lines)
         self.assertEqual(all_branches, expected_branches)
 
+    def test_branch_list_merged(self):
+        # Create 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")
+
+        master_sha = self.repo.refs[b"refs/heads/master"]
+
+        # Create a merged branch (points to same commit as master)
+        self.repo.refs[b"refs/heads/merged-branch"] = master_sha
+
+        # Create a new branch with different content (not merged)
+        test_file2 = os.path.join(self.repo_path, "test2.txt")
+        with open(test_file2, "w") as f:
+            f.write("test2")
+        self._run_cli("add", "test2.txt")
+        self._run_cli("commit", "--message=New branch commit")
+
+        new_branch_sha = self.repo.refs[b"HEAD"]
+
+        # Switch back to master
+        self.repo.refs[b"HEAD"] = master_sha
+
+        # Create a non-merged branch that points to the new branch commit
+        self.repo.refs[b"refs/heads/non-merged-branch"] = new_branch_sha
+
+        # Test --merged listing
+        result, stdout, stderr = self._run_cli("branch", "--merged")
+        self.assertEqual(result, 0)
+
+        branches = [line.strip() for line in stdout.splitlines()]
+        expected_branches = {"master", "merged-branch"}
+        self.assertEqual(set(branches), expected_branches)
+
     def test_branch_list_remotes(self):
         # Create initial commit
         test_file = os.path.join(self.repo_path, "test.txt")

+ 72 - 0
tests/test_porcelain.py

@@ -6717,6 +6717,78 @@ class BranchRemoteListTests(PorcelainTestCase):
         self.assertEqual(expected, branches)
 
 
+class BranchMergedTests(PorcelainTestCase):
+    def test_no_merged_branches(self) -> None:
+        """Test with no merged branches."""
+        # Create complete graph: c1 → c2 (master), c1 → c3 (feature)
+        [c1, c2, c3] = build_commit_graph(
+            self.repo.object_store,
+            [
+                [1],  # c1
+                [2, 1],  # c2 → c1 (master line)
+                [3, 1],  # c3 → c1 (diverged feature branch)
+            ],
+        )
+
+        self.repo.refs[b"HEAD"] = c2.id
+        self.repo.refs[b"refs/heads/master"] = c2.id
+        self.repo.refs[b"refs/heads/feature"] = c3.id
+
+        result = list(porcelain.merged_branches(self.repo))
+        self.assertEqual([b"master"], result)
+
+    def test_all_branches_merged(self) -> None:
+        """Test when all branches are merged into current."""
+        # Create linear history: c1 → c2 → c3 (HEAD)
+        [c1, c2, c3] = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 2]])
+
+        self.repo.refs[b"HEAD"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/feature-1"] = c2.id  # Merged (ancestor)
+        self.repo.refs[b"refs/heads/feature-2"] = c1.id  # Merged (ancestor)
+
+        branches = list(porcelain.merged_branches(self.repo))
+        expected = [b"feature-1", b"master", b"feature-2"]
+        expected.sort()
+        branches.sort()
+        self.assertEqual(expected, branches)
+
+    def test_some_branches_merged(self) -> None:
+        """Test when some branches are merged, some are not."""
+        # c1 → c2 → c3 (HEAD/master)
+        # c1 → c4 → c5 (feature-1 - diverged)
+        [c1, c2, c3, c4, c5] = build_commit_graph(
+            self.repo.object_store,
+            [
+                [1],  # c1
+                [2, 1],  # c2 → c1 (master line)
+                [3, 2],  # c3 → c2 (HEAD)
+                [4, 1],  # c4 → c1 (feature branch)
+                [5, 4],  # c5 → c4 (feature branch)
+            ],
+        )
+
+        self.repo.refs[b"HEAD"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/feature-1"] = c5.id  # Not merged (diverged)
+        self.repo.refs[b"refs/heads/feature-2"] = c2.id  # Merged (ancestor)
+
+        branches = list(porcelain.merged_branches(self.repo))
+        expected = [b"feature-2", b"master"]
+        expected.sort()
+        branches.sort()
+        self.assertEqual(expected, branches)
+
+    def test_only_current_branch_merged(self) -> None:
+        """Test when only current branch exists."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo.refs[b"HEAD"] = c1.id
+        self.repo.refs[b"refs/heads/master"] = c1.id
+
+        result = list(porcelain.merged_branches(self.repo))
+        self.assertEqual([b"master"], result)
+
+
 class BranchCreateTests(PorcelainTestCase):
     def test_branch_exists(self) -> None:
         [c1] = build_commit_graph(self.repo.object_store, [[1]])