From dc39a279e62666de741a32d6b3ddb75f7c1e727d Mon Sep 17 00:00:00 2001 From: Benjamin Milan Date: Wed, 13 May 2026 13:43:34 +0100 Subject: [PATCH 1/2] gh57 sc delete --- src/sc/branching/branching.py | 16 +++++ src/sc/branching/commands/delete.py | 65 ++++++++++++++++++++ src/sc/branching_cli.py | 24 ++++++++ tests/branching/test_delete.py | 95 +++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 src/sc/branching/commands/delete.py create mode 100644 tests/branching/test_delete.py diff --git a/src/sc/branching/branching.py b/src/sc/branching/branching.py index db8de9c..f6fe7e6 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,21 @@ 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, + force: 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, force), + 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..a2897e6 --- /dev/null +++ b/src/sc/branching/commands/delete.py @@ -0,0 +1,65 @@ +# 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 +import subprocess +import sys + +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 + force: bool = False + + def run_git_command(self): + _git_flow_delete(self.top_dir, self.branch, self.remote, self.force) + GitFlowLibrary.delete( + self.top_dir, self.branch.type, self.branch.suffix, self.remote) + + 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: + _git_flow_delete( + self.top_dir / proj.path, self.branch, self.remote, self.force) + + _git_flow_delete( + self.top_dir / '.repo' / 'manifests', self.branch, self.remote, self.force) + +def _git_flow_delete(dir: Path, branch: Branch, remote: bool = False, force: bool = False): + try: + GitFlowLibrary.delete( + dir, + branch.type, + branch.suffix, + remote=remote, + force=force + ) + except subprocess.CalledProcessError as e: + logger.error(e) + sys.exit(1) diff --git a/src/sc/branching_cli.py b/src/sc/branching_cli.py index 254c0c7..6d44b83 100644 --- a/src/sc/branching_cli.py +++ b/src/sc/branching_cli.py @@ -68,6 +68,14 @@ 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.") +@click.option("-f", "--force", is_flag=True, help="Force deletion.") +def delete(name, remote, force): + """Delete feature branch.""" + SCBranching.delete(BranchType.FEATURE, name, remote, force) + # Develop branch commands @cli.group() def develop(): @@ -167,6 +175,14 @@ 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.") +@click.option("-f", "--force", is_flag=True, help="Force deletion.") +def delete(name, remote, force): + """Delete release branch.""" + SCBranching.delete(BranchType.RELEASE, name, remote, force) + # Hotfix branch commands @cli.group() def hotfix(): @@ -216,6 +232,14 @@ 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.") +@click.option("-f", "--force", is_flag=True, help="Force deletion.") +def delete(name, remote, force): + """Delete hotfix branch.""" + SCBranching.delete(BranchType.HOTFIX, name, remote, force) + # 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..5879ba5 --- /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", "-f"], 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("feature/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("feature/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue("feature/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + + 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", "-f"], 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("release/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) + self.assertEqual(manifest_repo.active_branch.name, "develop") + self.assertFalse("release/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue("release/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + + 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", "-f"], cwd=top_dir) + + proj_repo = Repo(top_dir / proj.name) + manifest_repo = Repo(top_dir / ".repo" / "manifests") + self.assertEqual(proj_repo.active_branch.name, "master") + self.assertFalse("hotfix/donut" in [b.name for b in proj_repo.branches]) + self.assertTrue("hotfix/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) + self.assertEqual(manifest_repo.active_branch.name, "master") + self.assertFalse("hotfix/donut" in [b.name for b in manifest_repo.branches]) + self.assertTrue("hotfix/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + + 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", "-f", "-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("feature/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + +def _remote_branch_exists(repo: Repo, branch: str) -> bool: + return repo.git.ls_remote("--heads", repo.remotes[0].name, branch) + return bool(out.strip()) From 422ef48db51c644f67f039efa4887cdcdebde41b Mon Sep 17 00:00:00 2001 From: Benjamin Milan Date: Thu, 14 May 2026 09:36:19 +0100 Subject: [PATCH 2/2] gh57 sc delete --- src/sc/branching/branching.py | 3 +-- src/sc/branching/commands/delete.py | 37 ++++++++++++----------------- src/sc/branching_cli.py | 15 +++++------- tests/branching/test_delete.py | 28 +++++++++++----------- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/sc/branching/branching.py b/src/sc/branching/branching.py index f6fe7e6..2ee5a8b 100644 --- a/src/sc/branching/branching.py +++ b/src/sc/branching/branching.py @@ -92,13 +92,12 @@ def delete( branch_type: BranchType, name: str | None = None, remote: bool = False, - force: 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, force), + Delete(top_dir, branch, remote), project_type ) diff --git a/src/sc/branching/commands/delete.py b/src/sc/branching/commands/delete.py index a2897e6..cedf518 100644 --- a/src/sc/branching/commands/delete.py +++ b/src/sc/branching/commands/delete.py @@ -14,9 +14,8 @@ from dataclasses import dataclass import logging from pathlib import Path -import subprocess -import sys +from git import Repo from git_flow_library import GitFlowLibrary from sc_manifest_parser import ScManifest @@ -29,12 +28,9 @@ class Delete(Command): branch: Branch remote: bool = False - force: bool = False def run_git_command(self): - _git_flow_delete(self.top_dir, self.branch, self.remote, self.force) - GitFlowLibrary.delete( - self.top_dir, self.branch.type, self.branch.suffix, self.remote) + self._delete_branch(self.top_dir) def run_repo_command(self): self._error_on_sc_uninitialised() @@ -45,21 +41,18 @@ def run_repo_command(self): manifest = ScManifest.from_repo_root(self.top_dir / '.repo') for proj in manifest.projects: - _git_flow_delete( - self.top_dir / proj.path, self.branch, self.remote, self.force) + if proj.lock_status == None: + self._delete_branch(self.top_dir / proj.path, proj.remote) - _git_flow_delete( - self.top_dir / '.repo' / 'manifests', self.branch, self.remote, self.force) + self._delete_branch(self.top_dir / '.repo' / 'manifests') -def _git_flow_delete(dir: Path, branch: Branch, remote: bool = False, force: bool = False): - try: - GitFlowLibrary.delete( - dir, - branch.type, - branch.suffix, - remote=remote, - force=force - ) - except subprocess.CalledProcessError as e: - logger.error(e) - sys.exit(1) + 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 6d44b83..b80197c 100644 --- a/src/sc/branching_cli.py +++ b/src/sc/branching_cli.py @@ -71,10 +71,9 @@ def list(): @feature.command() @click.argument('name', required=False) @click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") -@click.option("-f", "--force", is_flag=True, help="Force deletion.") -def delete(name, remote, force): +def delete(name, remote): """Delete feature branch.""" - SCBranching.delete(BranchType.FEATURE, name, remote, force) + SCBranching.delete(BranchType.FEATURE, name, remote) # Develop branch commands @cli.group() @@ -178,10 +177,9 @@ def list(): @release.command() @click.argument('name', required=False) @click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") -@click.option("-f", "--force", is_flag=True, help="Force deletion.") -def delete(name, remote, force): +def delete(name, remote): """Delete release branch.""" - SCBranching.delete(BranchType.RELEASE, name, remote, force) + SCBranching.delete(BranchType.RELEASE, name, remote) # Hotfix branch commands @cli.group() @@ -235,10 +233,9 @@ def list(): @hotfix.command() @click.argument('name', required=False) @click.option("-r", "--remote", is_flag=True, help="Delete remote branches.") -@click.option("-f", "--force", is_flag=True, help="Force deletion.") -def delete(name, remote, force): +def delete(name, remote): """Delete hotfix branch.""" - SCBranching.delete(BranchType.HOTFIX, name, remote, force) + SCBranching.delete(BranchType.HOTFIX, name, remote) # Support branch commands @cli.group() diff --git a/tests/branching/test_delete.py b/tests/branching/test_delete.py index 5879ba5..269f275 100644 --- a/tests/branching/test_delete.py +++ b/tests/branching/test_delete.py @@ -31,55 +31,55 @@ def test_feature_delete(self): proj = self.repo_client.add_project() top_dir = self.repo_client.create("feature/donut") - subprocess.run(["sc", "feature", "delete", "donut", "-f"], cwd=top_dir) + 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("feature/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) + 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("feature/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + 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", "-f"], cwd=top_dir) + 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("release/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) + 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("release/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + 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", "-f"], cwd=top_dir) + 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, "master") + self.assertEqual(proj_repo.active_branch.name, "develop") self.assertFalse("hotfix/donut" in [b.name for b in proj_repo.branches]) - self.assertTrue("hotfix/donut" in [r.remote_head for r in proj_repo.remotes[0].refs]) - self.assertEqual(manifest_repo.active_branch.name, "master") + 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("hotfix/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + 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", "-f", "-r"], cwd=top_dir) + subprocess.run(["sc", "feature", "delete", "donut", "-r"], cwd=top_dir) proj_repo = Repo(top_dir / proj.name) manifest_repo = Repo(top_dir / ".repo" / "manifests") @@ -88,8 +88,8 @@ def test_feature_delete_remote(self): 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("feature/donut" in [r.remote_head for r in manifest_repo.remotes[0].refs]) + self.assertFalse(_remote_branch_exists(manifest_repo, "feature/donut")) def _remote_branch_exists(repo: Repo, branch: str) -> bool: - return repo.git.ls_remote("--heads", repo.remotes[0].name, branch) + out = repo.git.ls_remote("--heads", repo.remotes[0].name, branch) return bool(out.strip())