-
Notifications
You must be signed in to change notification settings - Fork 47
Add delete branch from git #8616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ad8d702
e190dd6
ccf2565
5001ed7
a8f5ad7
d2e160f
54357a1
e0138d8
b474bf0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -536,6 +536,20 @@ def get_branches_from_local(self, include_worktree: bool = True) -> dict[str, Br | |
|
|
||
| return branches | ||
|
|
||
| def origin_has_branch(self, branch_name: str) -> bool: | ||
| """Return True if branch_name exists as a remote branch on origin.""" | ||
| return branch_name in self.get_branches_from_remote() | ||
|
|
||
| async def delete_remote_branch(self, branch_name: str) -> None: | ||
| """Delete branch_name from origin and remove the local tracking ref.""" | ||
| if not self.has_origin: | ||
| return | ||
| repo = self.get_git_repo_main() | ||
| repo.git.push("origin", "--delete", branch_name) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reminds me how much I had gitpython
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@gmazoyer, typo for "hate"? ;) |
||
| local_branches = self.get_branches_from_local(include_worktree=False) | ||
| if branch_name in local_branches: | ||
| repo.delete_head(branch_name, force=True) | ||
|
|
||
| @abstractmethod | ||
| def get_commit_value(self, branch_name: str, remote: bool = False) -> str: | ||
| raise NotImplementedError() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,14 @@ | |
| tags=[WorkflowTag.DATABASE_CHANGE], | ||
| ) | ||
|
|
||
| GIT_REPOSITORIES_DELETE_BRANCH = WorkflowDefinition( | ||
| name="git-repositories-delete-branch", | ||
| type=WorkflowType.CORE, | ||
| module="infrahub.git.tasks", | ||
| function="delete_git_branch", | ||
| tags=[WorkflowTag.DATABASE_CHANGE], | ||
| ) | ||
|
Comment on lines
+195
to
+201
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should add a tag like the workflow above this one. |
||
|
|
||
| GIT_REPOSITORY_ADD = WorkflowDefinition( | ||
| name="git-repository-add-read-write", | ||
| type=WorkflowType.CORE, | ||
|
|
@@ -687,6 +695,7 @@ | |
| GIT_READ_ONLY_REPOSITORY_IMPORT_LAST_COMMIT, | ||
| GIT_REPOSITORIES_CHECK_ARTIFACT_CREATE, | ||
| GIT_REPOSITORIES_CREATE_BRANCH, | ||
| GIT_REPOSITORIES_DELETE_BRANCH, | ||
| GIT_REPOSITORIES_DIFF_NAMES_ONLY, | ||
| GIT_REPOSITORIES_IMPORT_OBJECTS, | ||
| GIT_REPOSITORIES_MERGE, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import pytest | ||
| from git import Repo | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pathlib import Path | ||
|
|
||
| from infrahub.git import InfrahubRepository | ||
|
|
||
|
|
||
| async def test_has_branch_returns_true_for_existing_branch(git_repo_01: InfrahubRepository) -> None: | ||
| assert git_repo_01.origin_has_branch("branch01") is True | ||
|
|
||
|
|
||
| async def test_has_branch_returns_false_for_missing_branch(git_repo_01: InfrahubRepository) -> None: | ||
| assert git_repo_01.origin_has_branch("nonexistent-branch-xyz") is False | ||
|
|
||
|
|
||
| async def test_delete_remote_branch_removes_branch_from_origin( | ||
| git_repo_01: InfrahubRepository, | ||
| git_upstream_repo_01: dict[str, str | Path], | ||
| ) -> None: | ||
| assert git_repo_01.origin_has_branch("clean-branch") is True | ||
|
|
||
| await git_repo_01.delete_remote_branch(branch_name="clean-branch") | ||
|
|
||
| # Fetch to sync remote tracking refs, then verify it's gone | ||
| await git_repo_01.fetch() | ||
| assert git_repo_01.origin_has_branch("clean-branch") is False | ||
|
|
||
| # Verify branch is also gone from the upstream (origin) repo | ||
| upstream = Repo(git_upstream_repo_01["path"]) | ||
| upstream_branches = [b.name for b in upstream.refs if not b.is_remote()] | ||
| assert "clean-branch" not in upstream_branches | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("branch_name", ["branch01", "branch02"]) | ||
| async def test_has_branch_true_for_all_remote_branches(git_repo_01: InfrahubRepository, branch_name: str) -> None: | ||
| assert git_repo_01.origin_has_branch(branch_name) is True |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
| from unittest.mock import ANY, AsyncMock, patch | ||
|
|
||
| import pytest | ||
| from infrahub_sdk.graphql import Mutation | ||
|
|
||
| from infrahub import config | ||
| from infrahub.services.adapters.workflow.local import WorkflowLocalExecution | ||
| from infrahub.workflows.catalogue import GIT_REPOSITORIES_DELETE_BRANCH | ||
| from tests.helpers.schema import CAR_SCHEMA, load_schema | ||
| from tests.helpers.test_app import TestInfrahubApp | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pathlib import Path | ||
|
|
||
| from infrahub_sdk import InfrahubClient | ||
|
|
||
| from infrahub.database import InfrahubDatabase | ||
| from tests.adapters.message_bus import BusSimulator | ||
|
|
||
|
|
||
| class TestDeleteBranchGitWorkflow(TestInfrahubApp): | ||
| @pytest.fixture(scope="class") | ||
| async def initial_dataset( | ||
| self, | ||
| db: InfrahubDatabase, | ||
| initialize_registry: None, | ||
| git_repos_source_dir_module_scope: Path, | ||
| client: InfrahubClient, | ||
| bus_simulator: BusSimulator, | ||
| prefect_test_fixture: None, | ||
| ) -> None: | ||
| await load_schema(db, schema=CAR_SCHEMA) | ||
|
|
||
| async def test_git_deletion_triggered_when_config_enabled_and_sync_with_git( | ||
| self, initial_dataset: None, client: InfrahubClient, delete_git_branch_after_merge_reset_config: None | ||
| ) -> None: | ||
| config.SETTINGS.git.delete_git_branch_after_merge = True | ||
| branch = await client.branch.create(branch_name="git_del_cfg_enabled", sync_with_git=True) | ||
|
|
||
| with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: | ||
| query = Mutation( | ||
| mutation="BranchDelete", | ||
| input_data={"data": {"name": branch.name}}, | ||
| query={"ok": None}, | ||
| ) | ||
| await client.execute_graphql(query=query.render()) | ||
|
|
||
| mock_submit.assert_any_call( | ||
| workflow=GIT_REPOSITORIES_DELETE_BRANCH, | ||
| context=ANY, | ||
| parameters={"branch": branch.name}, | ||
| ) | ||
|
|
||
| async def test_git_deletion_not_triggered_when_config_disabled( | ||
| self, initial_dataset: None, client: InfrahubClient, delete_git_branch_after_merge_reset_config: None | ||
| ) -> None: | ||
| config.SETTINGS.git.delete_git_branch_after_merge = False | ||
| branch = await client.branch.create(branch_name="git_del_cfg_disabled", sync_with_git=True) | ||
|
|
||
| with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: | ||
| query = Mutation( | ||
| mutation="BranchDelete", | ||
| input_data={"data": {"name": branch.name}}, | ||
| query={"ok": None}, | ||
| ) | ||
| await client.execute_graphql(query=query.render()) | ||
|
|
||
| git_del_calls = [ | ||
| c for c in mock_submit.call_args_list if c.kwargs.get("workflow") == GIT_REPOSITORIES_DELETE_BRANCH | ||
| ] | ||
| assert not git_del_calls | ||
|
|
||
| async def test_git_deletion_not_triggered_when_branch_not_sync_with_git( | ||
| self, initial_dataset: None, client: InfrahubClient, delete_git_branch_after_merge_reset_config: None | ||
| ) -> None: | ||
| config.SETTINGS.git.delete_git_branch_after_merge = True | ||
| branch = await client.branch.create(branch_name="git_del_no_sync", sync_with_git=False) | ||
|
|
||
| with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: | ||
| query = Mutation( | ||
| mutation="BranchDelete", | ||
| input_data={"data": {"name": branch.name}}, | ||
| query={"ok": None}, | ||
| ) | ||
| await client.execute_graphql(query=query.render()) | ||
|
|
||
| git_del_calls = [ | ||
| c for c in mock_submit.call_args_list if c.kwargs.get("workflow") == GIT_REPOSITORIES_DELETE_BRANCH | ||
| ] | ||
| assert not git_del_calls | ||
|
|
||
| async def test_git_deletion_triggered_when_delete_from_git_true_and_config_disabled( | ||
| self, initial_dataset: None, client: InfrahubClient, delete_git_branch_after_merge_reset_config: None | ||
| ) -> None: | ||
| config.SETTINGS.git.delete_git_branch_after_merge = False | ||
| branch = await client.branch.create(branch_name="git_del_explicit_true", sync_with_git=True) | ||
|
|
||
| with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: | ||
| query = Mutation( | ||
| mutation="BranchDelete", | ||
| input_data={"data": {"name": branch.name, "delete_from_git": True}}, | ||
| query={"ok": None}, | ||
| ) | ||
| await client.execute_graphql(query=query.render()) | ||
|
|
||
| mock_submit.assert_any_call( | ||
| workflow=GIT_REPOSITORIES_DELETE_BRANCH, | ||
| context=ANY, | ||
| parameters={"branch": branch.name}, | ||
| ) | ||
|
|
||
| async def test_git_deletion_not_triggered_when_delete_from_git_false_and_config_disabled( | ||
| self, initial_dataset: None, client: InfrahubClient, delete_git_branch_after_merge_reset_config: None | ||
| ) -> None: | ||
| config.SETTINGS.git.delete_git_branch_after_merge = False | ||
| branch = await client.branch.create(branch_name="git_del_explicit_false", sync_with_git=True) | ||
|
|
||
| with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: | ||
| query = Mutation( | ||
| mutation="BranchDelete", | ||
| input_data={"data": {"name": branch.name, "delete_from_git": False}}, | ||
| query={"ok": None}, | ||
| ) | ||
| await client.execute_graphql(query=query.render()) | ||
|
|
||
| git_del_calls = [ | ||
| c for c in mock_submit.call_args_list if c.kwargs.get("workflow") == GIT_REPOSITORIES_DELETE_BRANCH | ||
| ] | ||
| assert not git_del_calls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we'd want to catch any errors and write a task log to the repository object if the operation fails perhaps because the token has expired, or there's some network error or the user we use to connect to the git server doesn't have the permissions to delete a branch.