diff --git a/src/sc/branching/branching.py b/src/sc/branching/branching.py index db8de9c..2ee5a8b 100644 --- a/src/sc/branching/branching.py +++ b/src/sc/branching/branching.py @@ -25,6 +25,7 @@ from .commands.checkout import Checkout from .commands.clean import Clean from .commands.command import Command +from .commands.delete import Delete from .commands.finish import Finish from .commands.group import (GroupCheckout, GroupCmd, GroupFetch, GroupPush, GroupPull, GroupShow, GroupTag) @@ -85,6 +86,20 @@ def checkout( Checkout(top_dir, branch, force=force, verify=verify), project_type ) + + @staticmethod + def delete( + branch_type: BranchType, + name: str | None = None, + remote: bool = False, + run_dir: Path = Path.cwd() + ): + top_dir, project_type = detect_project(run_dir) + branch = create_branch(project_type, top_dir, branch_type, name) + run_command_by_project_type( + Delete(top_dir, branch, remote), + project_type + ) @staticmethod def status( diff --git a/src/sc/branching/commands/delete.py b/src/sc/branching/commands/delete.py new file mode 100644 index 0000000..cedf518 --- /dev/null +++ b/src/sc/branching/commands/delete.py @@ -0,0 +1,58 @@ +# Copyright 2025 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +import logging +from pathlib import Path + +from git import Repo +from git_flow_library import GitFlowLibrary +from sc_manifest_parser import ScManifest + +from ..branch import Branch +from .command import Command + +logger = logging.getLogger(__name__) + +@dataclass +class Delete(Command): + branch: Branch + remote: bool = False + + def run_git_command(self): + self._delete_branch(self.top_dir) + + def run_repo_command(self): + self._error_on_sc_uninitialised() + if self.remote: + logger.info(f"Removing Local & Remote Branch {self.branch.name}") + else: + logger.info(f"Removing Local Branch {self.branch.name}") + + manifest = ScManifest.from_repo_root(self.top_dir / '.repo') + for proj in manifest.projects: + if proj.lock_status == None: + self._delete_branch(self.top_dir / proj.path, proj.remote) + + self._delete_branch(self.top_dir / '.repo' / 'manifests') + + def _delete_branch(self, dir: Path, remote_name: str | None = None): + repo = Repo(dir) + if repo.active_branch.name == self.branch.name: + repo.git.switch(GitFlowLibrary.get_develop_branch(dir)) + repo.git.branch("-D", self.branch.name) + if self.remote: + if not remote_name: + remote_name = repo.remotes[0].name + repo.git.push(remote_name, f":{self.branch.name}") + repo.git.update_ref("-d", f"refs/remotes/m/{self.branch.name}") diff --git a/src/sc/branching_cli.py b/src/sc/branching_cli.py index 254c0c7..b80197c 100644 --- a/src/sc/branching_cli.py +++ b/src/sc/branching_cli.py @@ -68,6 +68,13 @@ def list(): """List feature branches.""" SCBranching.list(BranchType.FEATURE) +@feature.command() +@click.argument('name', required=False) +@click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") +def delete(name, remote): + """Delete feature branch.""" + SCBranching.delete(BranchType.FEATURE, name, remote) + # Develop branch commands @cli.group() def develop(): @@ -167,6 +174,13 @@ def list(): """List release branches.""" SCBranching.list(BranchType.RELEASE) +@release.command() +@click.argument('name', required=False) +@click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") +def delete(name, remote): + """Delete release branch.""" + SCBranching.delete(BranchType.RELEASE, name, remote) + # Hotfix branch commands @cli.group() def hotfix(): @@ -216,6 +230,13 @@ def list(): """List hotfix branches.""" SCBranching.list(BranchType.HOTFIX) +@hotfix.command() +@click.argument('name', required=False) +@click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") +def delete(name, remote): + """Delete hotfix branch.""" + SCBranching.delete(BranchType.HOTFIX, name, remote) + # Support branch commands @cli.group() def support(): diff --git a/tests/branching/test_delete.py b/tests/branching/test_delete.py new file mode 100644 index 0000000..269f275 --- /dev/null +++ b/tests/branching/test_delete.py @@ -0,0 +1,95 @@ +# Copyright 2025 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess +import unittest + +from git import Repo + +from .repo_client_creator import RepoTestClientCreator + +class TestDelete(unittest.TestCase): + def setUp(self): + self.repo_client = RepoTestClientCreator() + + def tearDown(self): + self.repo_client.cleanup() + + def test_feature_delete(self): + self.repo_client.add_branches(["master", "develop", "feature/donut"]) + proj = self.repo_client.add_project() + top_dir = self.repo_client.create("feature/donut") + + subprocess.run(["sc", "feature", "delete", "donut"], cwd=top_dir) + + proj_repo = Repo(top_dir / proj.name) + manifest_repo = Repo(top_dir / ".repo" / "manifests") + self.assertEqual(proj_repo.active_branch.name, "develop") + self.assertFalse("feature/donut" in [b.name for b in proj_repo.branches]) + self.assertTrue(_remote_branch_exists(proj_repo, "feature/donut")) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("feature/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue(_remote_branch_exists(manifest_repo, "feature/donut")) + + def test_release_delete(self): + self.repo_client.add_branches(["master", "develop", "release/donut"]) + proj = self.repo_client.add_project() + top_dir = self.repo_client.create("release/donut") + + subprocess.run(["sc", "release", "delete", "donut"], cwd=top_dir) + + proj_repo = Repo(top_dir / proj.name) + manifest_repo = Repo(top_dir / ".repo" / "manifests") + self.assertEqual(proj_repo.active_branch.name, "develop") + self.assertFalse("release/donut" in [b.name for b in proj_repo.branches]) + self.assertTrue(_remote_branch_exists(proj_repo, "release/donut")) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("release/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue(_remote_branch_exists(manifest_repo, "release/donut")) + + def test_hotfix_delete(self): + self.repo_client.add_branches(["master", "develop", "hotfix/donut"]) + proj = self.repo_client.add_project() + top_dir = self.repo_client.create("hotfix/donut") + + subprocess.run(["sc", "hotfix", "delete", "donut"], cwd=top_dir) + + proj_repo = Repo(top_dir / proj.name) + manifest_repo = Repo(top_dir / ".repo" / "manifests") + self.assertEqual(proj_repo.active_branch.name, "develop") + self.assertFalse("hotfix/donut" in [b.name for b in proj_repo.branches]) + self.assertTrue(_remote_branch_exists(proj_repo, "hotfix/donut")) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("hotfix/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue(_remote_branch_exists(manifest_repo, "hotfix/donut")) + + def test_feature_delete_remote(self): + self.repo_client.add_branches(["master", "develop", "feature/donut"]) + proj = self.repo_client.add_project() + top_dir = self.repo_client.create("feature/donut") + + subprocess.run(["sc", "feature", "delete", "donut", "-r"], cwd=top_dir) + + proj_repo = Repo(top_dir / proj.name) + manifest_repo = Repo(top_dir / ".repo" / "manifests") + self.assertEqual(proj_repo.active_branch.name, "develop") + self.assertFalse("feature/donut" in [b.name for b in proj_repo.branches]) + self.assertFalse(_remote_branch_exists(proj_repo, "feature/donut")) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("feature/donut" in [b.name for b in manifest_repo.branches]) + self.assertFalse(_remote_branch_exists(manifest_repo, "feature/donut")) + +def _remote_branch_exists(repo: Repo, branch: str) -> bool: + out = repo.git.ls_remote("--heads", repo.remotes[0].name, branch) + return bool(out.strip())