diff --git a/docs/assets/images/teaser.png b/docs/assets/images/teaser.png index acd1f382..1dfe6278 100644 Binary files a/docs/assets/images/teaser.png and b/docs/assets/images/teaser.png differ diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index 1dc9cb23..d2a214db 100644 --- a/docs/commands/deploy.md +++ b/docs/commands/deploy.md @@ -18,7 +18,7 @@ backend: value: foo # <- and even one in a list ``` -With the following command GitOps CLI will update both values to `1.1.0` on the `master` branch. +With the following command GitOps CLI will update both values to `1.1.0` on the default branch. ```bash gitopscli deploy \ diff --git a/gitopscli/commands/common/gitops_config_loader.py b/gitopscli/commands/common/gitops_config_loader.py index e904c0b2..e78ab999 100644 --- a/gitopscli/commands/common/gitops_config_loader.py +++ b/gitopscli/commands/common/gitops_config_loader.py @@ -7,7 +7,7 @@ def load_gitops_config(git_api_config: GitApiConfig, organisation: str, repository_name: str) -> GitOpsConfig: git_repo_api = GitRepoApiFactory.create(git_api_config, organisation, repository_name) with GitRepo(git_repo_api) as git_repo: - git_repo.checkout("master") + git_repo.clone() gitops_config_file_path = git_repo.get_full_file_path(".gitops.config.yaml") try: gitops_config_yaml = yaml_file_load(gitops_config_file_path) diff --git a/gitopscli/commands/create_preview.py b/gitopscli/commands/create_preview.py index 60ec265f..beb5cdea 100644 --- a/gitopscli/commands/create_preview.py +++ b/gitopscli/commands/create_preview.py @@ -45,7 +45,7 @@ def execute(self,) -> None: team_config_git_repo_api = self.__create_team_config_git_repo_api(gitops_config) with GitRepo(team_config_git_repo_api) as team_config_git_repo: - team_config_git_repo.checkout("master") + team_config_git_repo.clone() created_new_preview = self.__create_preview_from_template_if_not_existing( team_config_git_repo, gitops_config @@ -58,7 +58,7 @@ def execute(self,) -> None: logging.info("The preview is already up-to-date. I'm done here.") return - self.__commit_and_push_to_master( + self.__commit_and_push( team_config_git_repo, f"{'Create new' if created_new_preview else 'Update'} preview environment for " f"'{gitops_config.application_name}' and git hash '{self.__args.git_hash}'.", @@ -69,9 +69,9 @@ def execute(self,) -> None: else: self.__deployment_updated_callback(route_host) - def __commit_and_push_to_master(self, git_repo: GitRepo, message: str) -> None: + def __commit_and_push(self, git_repo: GitRepo, message: str) -> None: git_repo.commit(self.__args.git_user, self.__args.git_email, message) - git_repo.push("master") + git_repo.push() def __get_gitops_config(self) -> GitOpsConfig: return load_gitops_config(self.__args, self.__args.organisation, self.__args.repository_name) diff --git a/gitopscli/commands/delete_preview.py b/gitopscli/commands/delete_preview.py index 713c2e71..09faf9a5 100644 --- a/gitopscli/commands/delete_preview.py +++ b/gitopscli/commands/delete_preview.py @@ -30,7 +30,7 @@ def execute(self) -> None: team_config_git_repo_api = self.__create_team_config_git_repo_api(gitops_config) with GitRepo(team_config_git_repo_api) as team_config_git_repo: - team_config_git_repo.checkout("master") + team_config_git_repo.clone() preview_namespace = gitops_config.get_preview_namespace(preview_id) logging.info("Preview folder name: %s", preview_namespace) @@ -46,7 +46,7 @@ def execute(self) -> None: ) return - self.__commit_and_push_to_master( + self.__commit_and_push( team_config_git_repo, f"Delete preview environment for '{gitops_config.application_name}' and preview id '{preview_id}'.", ) @@ -57,9 +57,9 @@ def __get_gitops_config(self) -> GitOpsConfig: def __create_team_config_git_repo_api(self, gitops_config: GitOpsConfig) -> GitRepoApi: return GitRepoApiFactory.create(self.__args, gitops_config.team_config_org, gitops_config.team_config_repo) - def __commit_and_push_to_master(self, git_repo: GitRepo, message: str) -> None: + def __commit_and_push(self, git_repo: GitRepo, message: str) -> None: git_repo.commit(self.__args.git_user, self.__args.git_email, message) - git_repo.push("master") + git_repo.push() @staticmethod def __delete_folder_if_exists(git_repo: GitRepo, folder_name: str) -> bool: diff --git a/gitopscli/commands/deploy.py b/gitopscli/commands/deploy.py index d7e7b7f2..869ee86e 100644 --- a/gitopscli/commands/deploy.py +++ b/gitopscli/commands/deploy.py @@ -32,27 +32,26 @@ def __init__(self, args: Args) -> None: def execute(self) -> None: git_repo_api = self.__create_git_repo_api() with GitRepo(git_repo_api) as git_repo: - branch = "master" - git_repo.checkout(branch) + git_repo.clone() if self.__args.create_pr: - branch = f"gitopscli-deploy-{str(uuid.uuid4())[:8]}" - git_repo.new_branch(branch) + pr_branch = f"gitopscli-deploy-{str(uuid.uuid4())[:8]}" + git_repo.new_branch(pr_branch) updated_values = self.__update_values(git_repo) if not updated_values: logging.info("All values already up-to-date. I'm done here.") return - git_repo.push(branch) + git_repo.push() if self.__args.create_pr: title, description = self.__create_pull_request_title_and_description(updated_values) - pr_id = git_repo_api.create_pull_request(branch, "master", title, description).pr_id + pr_id = git_repo_api.create_pull_request_to_default_branch(pr_branch, title, description).pr_id if self.__args.auto_merge: git_repo_api.merge_pull_request(pr_id) - git_repo_api.delete_branch(branch) + git_repo_api.delete_branch(pr_branch) def __create_git_repo_api(self) -> GitRepoApi: return GitRepoApiFactory.create(self.__args, self.__args.organisation, self.__args.repository_name) diff --git a/gitopscli/commands/sync_apps.py b/gitopscli/commands/sync_apps.py index 36f9ed36..7e524bf4 100644 --- a/gitopscli/commands/sync_apps.py +++ b/gitopscli/commands/sync_apps.py @@ -55,7 +55,7 @@ def __sync_apps(team_config_git_repo: GitRepo, root_config_git_repo: GitRepo, gi logging.info("Sync applications in root repository's %s.", apps_config_file_name) merge_yaml_element(apps_config_file, "applications", {repo_app: {} for repo_app in repo_apps}) - __commit_and_push_to_master(team_config_git_repo, root_config_git_repo, git_user, git_email, apps_config_file_name) + __commit_and_push(team_config_git_repo, root_config_git_repo, git_user, git_email, apps_config_file_name) def __find_apps_config_from_repo( @@ -100,16 +100,16 @@ def __get_applications_from_app_config(app_config: Any) -> Set[str]: return set(apps) -def __commit_and_push_to_master( +def __commit_and_push( team_config_git_repo: GitRepo, root_config_git_repo: GitRepo, git_user: str, git_email: str, app_file_name: str ) -> None: author = team_config_git_repo.get_author_from_last_commit() root_config_git_repo.commit(git_user, git_email, f"{author} updated " + app_file_name) - root_config_git_repo.push("master") + root_config_git_repo.push() def __get_bootstrap_entries(root_config_git_repo: GitRepo) -> Any: - root_config_git_repo.checkout("master") + root_config_git_repo.clone() bootstrap_values_file = root_config_git_repo.get_full_file_path("bootstrap/values.yaml") try: bootstrap_yaml = yaml_file_load(bootstrap_values_file) @@ -121,7 +121,7 @@ def __get_bootstrap_entries(root_config_git_repo: GitRepo) -> Any: def __get_repo_apps(team_config_git_repo: GitRepo) -> Set[str]: - team_config_git_repo.checkout("master") + team_config_git_repo.clone() repo_dir = team_config_git_repo.get_full_file_path(".") return { name diff --git a/gitopscli/git/bitbucket_git_repo_api_adapter.py b/gitopscli/git/bitbucket_git_repo_api_adapter.py index f7fb1bd7..163bf6c3 100644 --- a/gitopscli/git/bitbucket_git_repo_api_adapter.py +++ b/gitopscli/git/bitbucket_git_repo_api_adapter.py @@ -51,6 +51,12 @@ def get_clone_url(self) -> str: raise GitOpsException("Couldn't determine repository URL.") return str(repo_url) + def create_pull_request_to_default_branch( + self, from_branch: str, title: str, description: str + ) -> GitRepoApi.PullRequestIdAndUrl: + to_branch = self.__get_default_branch() + return self.create_pull_request(from_branch, to_branch, title, description) + def create_pull_request( self, from_branch: str, to_branch: str, title: str, description: str ) -> GitRepoApi.PullRequestIdAndUrl: @@ -98,3 +104,7 @@ def get_pull_request_branch(self, pr_id: int) -> str: if "errors" in pull_request: raise GitOpsException(pull_request["errors"][0]["message"]) return str(pull_request["fromRef"]["displayId"]) + + def __get_default_branch(self) -> str: + default_branch = self.__bitbucket.get_default_branch(self.__organisation, self.__repository_name) + return str(default_branch["id"]) diff --git a/gitopscli/git/git_repo.py b/gitopscli/git/git_repo.py index ee2bad92..ccda3617 100644 --- a/gitopscli/git/git_repo.py +++ b/gitopscli/git/git_repo.py @@ -36,7 +36,7 @@ def get_full_file_path(self, relative_path: str) -> str: def get_clone_url(self) -> str: return self.__api.get_clone_url() - def checkout(self, branch: str) -> None: + def clone(self) -> None: self.__delete_tmp_dir() self.__tmp_dir = create_tmp_dir() git_options = [] @@ -52,12 +52,6 @@ def checkout(self, branch: str) -> None: except GitError as ex: raise GitOpsException(f"Error cloning '{url}'") from ex - logging.info("Checking out branch: %s", branch) - try: - self.__repo.git.checkout(branch) - except GitError as ex: - raise GitOpsException(f"Error checking out branch '{branch}'") from ex - def new_branch(self, branch: str) -> None: logging.info("Creating new branch: %s", branch) repo = self.__get_repo() @@ -78,9 +72,11 @@ def commit(self, git_user: str, git_email: str, message: str) -> None: except GitError as ex: raise GitOpsException(f"Error creating commit.") from ex - def push(self, branch: str) -> None: - logging.info("Pushing branch: %s", branch) + def push(self, branch: Optional[str] = None) -> None: repo = self.__get_repo() + if not branch: + branch = repo.git.branch("--show-current") + logging.info("Pushing branch: %s", branch) try: repo.git.push("--set-upstream", "origin", branch) except GitCommandError as ex: diff --git a/gitopscli/git/git_repo_api.py b/gitopscli/git/git_repo_api.py index b13e8e63..87104aa1 100644 --- a/gitopscli/git/git_repo_api.py +++ b/gitopscli/git/git_repo_api.py @@ -19,6 +19,12 @@ def get_password(self) -> Optional[str]: def get_clone_url(self) -> str: ... + @abstractmethod + def create_pull_request_to_default_branch( + self, from_branch: str, title: str, description: str + ) -> "PullRequestIdAndUrl": + ... + @abstractmethod def create_pull_request( self, from_branch: str, to_branch: str, title: str, description: str diff --git a/gitopscli/git/git_repo_api_logging_proxy.py b/gitopscli/git/git_repo_api_logging_proxy.py index 84d485b4..b9086f0f 100644 --- a/gitopscli/git/git_repo_api_logging_proxy.py +++ b/gitopscli/git/git_repo_api_logging_proxy.py @@ -16,6 +16,12 @@ def get_password(self) -> Optional[str]: def get_clone_url(self) -> str: return self.__api.get_clone_url() + def create_pull_request_to_default_branch( + self, from_branch: str, title: str, description: str + ) -> GitRepoApi.PullRequestIdAndUrl: + logging.info("Creating pull request from '%s' to default branch with title: %s", from_branch, title) + return self.__api.create_pull_request_to_default_branch(from_branch, title, description) + def create_pull_request( self, from_branch: str, to_branch: str, title: str, description: str ) -> GitRepoApi.PullRequestIdAndUrl: diff --git a/gitopscli/git/github_git_repo_api_adapter.py b/gitopscli/git/github_git_repo_api_adapter.py index 072261f0..e3ad9cda 100644 --- a/gitopscli/git/github_git_repo_api_adapter.py +++ b/gitopscli/git/github_git_repo_api_adapter.py @@ -23,12 +23,18 @@ def get_password(self) -> Optional[str]: def get_clone_url(self) -> str: return self.__get_repo().clone_url + def create_pull_request_to_default_branch( + self, from_branch: str, title: str, description: str + ) -> GitRepoApi.PullRequestIdAndUrl: + to_branch = self.__get_repo().default_branch + return self.create_pull_request(from_branch, to_branch, title, description) + def create_pull_request( self, from_branch: str, to_branch: str, title: str, description: str ) -> GitRepoApi.PullRequestIdAndUrl: repo = self.__get_repo() pull_request = repo.create_pull(title=title, body=description, head=from_branch, base=to_branch) - return GitRepoApi.PullRequestIdAndUrl(pr_id=pull_request.id, url=pull_request.html_url) + return GitRepoApi.PullRequestIdAndUrl(pr_id=pull_request.number, url=pull_request.html_url) def merge_pull_request(self, pr_id: int) -> None: pull_request = self.__get_pull_request(pr_id) diff --git a/tests/commands/common/test_gitops_config_loader.py b/tests/commands/common/test_gitops_config_loader.py index 2a1f8d29..24d6887e 100644 --- a/tests/commands/common/test_gitops_config_loader.py +++ b/tests/commands/common/test_gitops_config_loader.py @@ -32,7 +32,7 @@ def setUp(self): self.git_repo_mock.return_value = self.git_repo_mock self.git_repo_mock.__enter__.return_value = self.git_repo_mock self.git_repo_mock.__exit__.return_value = False - self.git_repo_mock.checkout.return_value = None + self.git_repo_mock.clone.return_value = None self.git_repo_mock.get_full_file_path.side_effect = lambda x: f"/repo-dir/{x}" self.seal_mocks() @@ -47,7 +47,7 @@ def test_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(self.git_api_config, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path(".gitops.config.yaml"), call.yaml_file_load("/repo-dir/.gitops.config.yaml"), call.GitOpsConfig.from_yaml({"dummy": "gitopsconfig"}), @@ -64,7 +64,7 @@ def test_file_not_found(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(self.git_api_config, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path(".gitops.config.yaml"), call.yaml_file_load("/repo-dir/.gitops.config.yaml"), ] diff --git a/tests/commands/test_create_preview.py b/tests/commands/test_create_preview.py index 46909d82..a3bbe1f7 100644 --- a/tests/commands/test_create_preview.py +++ b/tests/commands/test_create_preview.py @@ -64,7 +64,7 @@ def setUp(self): self.git_repo_mock.__enter__.return_value = self.git_repo_mock self.git_repo_mock.__exit__.return_value = False self.git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}" - self.git_repo_mock.checkout.return_value = None + self.git_repo_mock.clone.return_value = None self.git_repo_mock.commit.return_value = None self.git_repo_mock.push.return_value = None @@ -92,7 +92,7 @@ def test_create_new_preview(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"), @@ -125,7 +125,7 @@ def test_create_new_preview(self): "GIT_EMAIL", "Create new preview environment for 'my-app' and git hash '3361723dbd91fcfae7b5b8b8b7d462fbc14187a9'.", ), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_update_existing_preview(self): @@ -149,7 +149,7 @@ def test_update_existing_preview(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"), @@ -172,7 +172,7 @@ def test_update_existing_preview(self): "GIT_EMAIL", "Update preview environment for 'my-app' and git hash '3361723dbd91fcfae7b5b8b8b7d462fbc14187a9'.", ), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_preview_already_up_to_date(self): @@ -198,7 +198,7 @@ def test_preview_already_up_to_date(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"), @@ -233,7 +233,7 @@ def test_create_preview_for_unknown_template(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"), @@ -254,7 +254,7 @@ def test_create_preview_values_yaml_not_found(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"), @@ -279,7 +279,7 @@ def test_create_preview_values_yaml_parse_error(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"), @@ -304,7 +304,7 @@ def test_create_preview_with_invalid_replacement_path(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Use existing folder for preview: %s", "my-app-685912d3-preview"), @@ -334,7 +334,7 @@ def test_create_new_preview_invalid_chart_template(self): call.load_gitops_config(ARGS, "ORGA", "REPO",), call.GitRepoApiFactory.create(ARGS, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO",), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("my-app-685912d3-preview"), call.os.path.isdir("/tmp/created-tmp-dir/my-app-685912d3-preview"), call.logging.info("Create new folder for preview: %s", "my-app-685912d3-preview"), diff --git a/tests/commands/test_delete_preview.py b/tests/commands/test_delete_preview.py index 3d2a167a..34ea43ed 100644 --- a/tests/commands/test_delete_preview.py +++ b/tests/commands/test_delete_preview.py @@ -46,7 +46,7 @@ def setUp(self): self.git_repo_mock.__enter__.return_value = self.git_repo_mock self.git_repo_mock.__exit__.return_value = False self.git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}" - self.git_repo_mock.checkout.return_value = None + self.git_repo_mock.clone.return_value = None self.git_repo_mock.commit.return_value = None self.git_repo_mock.push.return_value = None @@ -70,7 +70,7 @@ def test_delete_existing_happy_flow(self): call.load_gitops_config(args, "ORGA", "REPO"), call.GitRepoApiFactory.create(args, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), call.GitRepo.get_full_file_path("APP-685912d3-preview"), call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), @@ -78,7 +78,7 @@ def test_delete_existing_happy_flow(self): call.GitRepo.commit( "GIT_USER", "GIT_EMAIL", "Delete preview environment for 'APP' and preview id 'PREVIEW_ID'." ), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_delete_missing_happy_flow(self): @@ -101,7 +101,7 @@ def test_delete_missing_happy_flow(self): call.load_gitops_config(args, "ORGA", "REPO"), call.GitRepoApiFactory.create(args, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), call.GitRepo.get_full_file_path("APP-685912d3-preview"), call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), @@ -133,7 +133,7 @@ def test_delete_missing_but_expected_error(self): call.load_gitops_config(args, "ORGA", "REPO"), call.GitRepoApiFactory.create(args, "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), call.GitRepo.get_full_file_path("APP-685912d3-preview"), call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), diff --git a/tests/commands/test_deploy.py b/tests/commands/test_deploy.py index c91b3745..58b496d7 100644 --- a/tests/commands/test_deploy.py +++ b/tests/commands/test_deploy.py @@ -25,7 +25,7 @@ def setUp(self): self.uuid_mock.uuid4.return_value = UUID("b973b5bb-64a6-4735-a840-3113d531b41c") self.git_repo_api_mock = self.create_mock(GitRepoApi) - self.git_repo_api_mock.create_pull_request.return_value = GitRepoApi.PullRequestIdAndUrl( + self.git_repo_api_mock.create_pull_request_to_default_branch.return_value = GitRepoApi.PullRequestIdAndUrl( 42, "" ) self.git_repo_api_mock.merge_pull_request.return_value = None @@ -38,7 +38,7 @@ def setUp(self): self.git_repo_mock.return_value = self.git_repo_mock self.git_repo_mock.__enter__.return_value = self.git_repo_mock self.git_repo_mock.__exit__.return_value = False - self.git_repo_mock.checkout.return_value = None + self.git_repo_mock.clone.return_value = None self.git_repo_mock.new_branch.return_value = None self.git_repo_mock.commit.return_value = None self.git_repo_mock.push.return_value = None @@ -68,7 +68,7 @@ def test_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), @@ -76,7 +76,7 @@ def test_happy_flow(self): call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "changed 'a.b.d' to 'bar' in test/file.yml"), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_create_pr_single_value_change_happy_flow(self): @@ -101,17 +101,16 @@ def test_create_pr_single_value_change_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.uuid.uuid4(), call.GitRepo.new_branch("gitopscli-deploy-b973b5bb"), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "changed 'a.b.c' to 'foo' in test/file.yml"), - call.GitRepo.push("gitopscli-deploy-b973b5bb"), - call.GitRepoApi.create_pull_request( + call.GitRepo.push(), + call.GitRepoApi.create_pull_request_to_default_branch( "gitopscli-deploy-b973b5bb", - "master", "Updated value in test/file.yml", "Updated 1 value in `test/file.yml`:\n```yaml\na.b.c: foo\n```\n", ), @@ -139,7 +138,7 @@ def test_create_pr_multiple_value_changes_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.uuid.uuid4(), call.GitRepo.new_branch("gitopscli-deploy-b973b5bb"), call.GitRepo.get_full_file_path("test/file.yml"), @@ -149,10 +148,9 @@ def test_create_pr_multiple_value_changes_happy_flow(self): call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "changed 'a.b.d' to 'bar' in test/file.yml"), - call.GitRepo.push("gitopscli-deploy-b973b5bb"), - call.GitRepoApi.create_pull_request( + call.GitRepo.push(), + call.GitRepoApi.create_pull_request_to_default_branch( "gitopscli-deploy-b973b5bb", - "master", "Updated values in test/file.yml", "Updated 2 values in `test/file.yml`:\n```yaml\na.b.c: foo\na.b.d: bar\n```\n", ), @@ -180,7 +178,7 @@ def test_create_pr_and_merge_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.uuid.uuid4(), call.GitRepo.new_branch("gitopscli-deploy-b973b5bb"), call.GitRepo.get_full_file_path("test/file.yml"), @@ -190,10 +188,9 @@ def test_create_pr_and_merge_happy_flow(self): call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "changed 'a.b.d' to 'bar' in test/file.yml"), - call.GitRepo.push("gitopscli-deploy-b973b5bb"), - call.GitRepoApi.create_pull_request( + call.GitRepo.push(), + call.GitRepoApi.create_pull_request_to_default_branch( "gitopscli-deploy-b973b5bb", - "master", "Updated values in test/file.yml", "Updated 2 values in `test/file.yml`:\n```yaml\na.b.c: foo\na.b.d: bar\n```\n", ), @@ -223,14 +220,14 @@ def test_single_commit_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "updated 2 values in test/file.yml\n\na.b.c: foo\na.b.d: bar"), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_single_commit_single_value_change_happy_flow(self): @@ -255,12 +252,12 @@ def test_single_commit_single_value_change_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "changed 'a.b.c' to 'foo' in test/file.yml"), - call.GitRepo.push("master"), + call.GitRepo.push(), ] def test_commit_message_multiple_value_changes_happy_flow(self): @@ -285,19 +282,19 @@ def test_commit_message_multiple_value_changes_happy_flow(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), call.GitRepo.commit("GIT_USER", "GIT_EMAIL", "testcommit"), - call.GitRepo.push("master"), + call.GitRepo.push(), ] - def test_checkout_error(self): - checkout_exception = GitOpsException("dummy checkout error") - self.git_repo_mock.checkout.side_effect = checkout_exception + def test_clone_error(self): + clone_exception = GitOpsException("dummy clone error") + self.git_repo_mock.clone.side_effect = clone_exception args = DeployCommand.Args( file="test/file.yml", @@ -317,12 +314,12 @@ def test_checkout_error(self): ) with pytest.raises(GitOpsException) as ex: DeployCommand(args).execute() - self.assertEqual(ex.value, checkout_exception) + self.assertEqual(ex.value, clone_exception) assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), ] def test_file_not_found(self): @@ -351,7 +348,7 @@ def test_file_not_found(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), ] @@ -382,7 +379,7 @@ def test_file_parse_error(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), ] @@ -413,7 +410,7 @@ def test_key_not_found(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), ] @@ -442,7 +439,7 @@ def test_nothing_to_update(self): assert self.mock_manager.method_calls == [ call.GitRepoApiFactory.create(args, "ORGA", "REPO"), call.GitRepo(self.git_repo_api_mock), - call.GitRepo.checkout("master"), + call.GitRepo.clone(), call.GitRepo.get_full_file_path("test/file.yml"), call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), call.logging.info("Yaml property %s already up-to-date", "a.b.c"), diff --git a/tests/commands/test_sync_apps.py b/tests/commands/test_sync_apps.py index 88827532..58d304a6 100644 --- a/tests/commands/test_sync_apps.py +++ b/tests/commands/test_sync_apps.py @@ -41,7 +41,7 @@ def setUp(self): self.team_config_git_repo_mock.__enter__.return_value = self.team_config_git_repo_mock self.team_config_git_repo_mock.__exit__.return_value = False self.team_config_git_repo_mock.get_clone_url.return_value = "https://team.config.repo.git" - self.team_config_git_repo_mock.checkout.return_value = None + self.team_config_git_repo_mock.clone.return_value = None self.team_config_git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/team-config-repo/{x}" self.team_config_git_repo_mock.get_author_from_last_commit.return_value = "author" @@ -50,7 +50,7 @@ def setUp(self): self.root_config_git_repo_mock.__exit__.return_value = False self.root_config_git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/root-config-repo/{x}" self.root_config_git_repo_mock.get_clone_url.return_value = "https://root.config.repo.git" - self.root_config_git_repo_mock.checkout.return_value = None + self.root_config_git_repo_mock.clone.return_value = None self.root_config_git_repo_mock.commit.return_value = None self.root_config_git_repo_mock.push.return_value = None @@ -97,14 +97,14 @@ def test_sync_apps_happy_flow(self): call.logging.info("Team config repository: %s", "https://team.config.repo.git"), call.GitRepo_root.get_clone_url(), call.logging.info("Root config repository: %s", "https://root.config.repo.git"), - call.GitRepo_team.checkout("master"), + call.GitRepo_team.clone(), call.GitRepo_team.get_full_file_path("."), call.os.listdir("/tmp/team-config-repo/."), call.os.path.join("/tmp/team-config-repo/.", "my-app"), call.os.path.isdir("/tmp/team-config-repo/./my-app"), call.logging.info("Found %s app(s) in apps repository: %s", 1, "my-app"), call.logging.info("Searching apps repository in root repository's 'apps/' directory..."), - call.GitRepo_root.checkout("master"), + call.GitRepo_root.clone(), call.GitRepo_root.get_full_file_path("bootstrap/values.yaml"), call.yaml_file_load("/tmp/root-config-repo/bootstrap/values.yaml"), call.GitRepo_team.get_clone_url(), @@ -119,7 +119,7 @@ def test_sync_apps_happy_flow(self): call.merge_yaml_element("/tmp/root-config-repo/apps/team-non-prod.yaml", "applications", {"my-app": {}}), call.GitRepo_team.get_author_from_last_commit(), call.GitRepo_root.commit("GIT_USER", "GIT_EMAIL", "author updated apps/team-non-prod.yaml"), - call.GitRepo_root.push("master"), + call.GitRepo_root.push(), ] def test_sync_apps_already_up_to_date(self): @@ -147,14 +147,14 @@ def test_sync_apps_already_up_to_date(self): call.logging.info("Team config repository: %s", "https://team.config.repo.git"), call.GitRepo_root.get_clone_url(), call.logging.info("Root config repository: %s", "https://root.config.repo.git"), - call.GitRepo_team.checkout("master"), + call.GitRepo_team.clone(), call.GitRepo_team.get_full_file_path("."), call.os.listdir("/tmp/team-config-repo/."), call.os.path.join("/tmp/team-config-repo/.", "my-app"), call.os.path.isdir("/tmp/team-config-repo/./my-app"), call.logging.info("Found %s app(s) in apps repository: %s", 1, "my-app"), call.logging.info("Searching apps repository in root repository's 'apps/' directory..."), - call.GitRepo_root.checkout("master"), + call.GitRepo_root.clone(), call.GitRepo_root.get_full_file_path("bootstrap/values.yaml"), call.yaml_file_load("/tmp/root-config-repo/bootstrap/values.yaml"), call.GitRepo_team.get_clone_url(), diff --git a/tests/git/test_git_repo.py b/tests/git/test_git_repo.py index 9bdc29be..b9830111 100644 --- a/tests/git/test_git_repo.py +++ b/tests/git/test_git_repo.py @@ -2,7 +2,7 @@ import stat import unittest import uuid -from unittest.mock import MagicMock, patch, call +from unittest.mock import MagicMock, patch from git import Repo import pytest @@ -48,12 +48,15 @@ def __create_origin(self): repo.git.add("--all") repo.git.commit("-m", "xyz brach commit") + repo.git.checkout("master") # master = default branch + repo.git.config("receive.denyCurrentBranch", "ignore") + return repo def test_finalize(self): testee = GitRepo(self.__mock_repo_api) - testee.checkout("master") + testee.clone() tmp_dir = testee.get_full_file_path("..") self.assertTrue(path.exists(tmp_dir)) @@ -66,7 +69,7 @@ def test_enter_and_exit_magic_methods(self): self.assertEqual(testee, testee.__enter__()) - testee.checkout("master") + testee.clone() tmp_dir = testee.get_full_file_path("..") self.assertTrue(path.exists(tmp_dir)) @@ -75,25 +78,22 @@ def test_enter_and_exit_magic_methods(self): self.assertFalse(path.exists(tmp_dir)) @patch("gitopscli.git.git_repo.logging") - def test_checkout_without_credentials(self, logging_mock): + def test_clone_without_credentials(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() readme = self.__read_file(testee.get_full_file_path("README.md")) self.assertEqual("master branch readme", readme) self.assertFalse(path.exists(testee.get_full_file_path("../credentials.sh"))) - assert logging_mock.method_calls == [ - call.info("Cloning repository: %s", self.__mock_repo_api.get_clone_url()), - call.info("Checking out branch: %s", "master"), - ] + logging_mock.info.assert_called_once_with("Cloning repository: %s", self.__mock_repo_api.get_clone_url()) @patch("gitopscli.git.git_repo.logging") - def test_checkout_with_credentials(self, logging_mock): + def test_clone_with_credentials(self, logging_mock): self.__mock_repo_api.get_username.return_value = "User" self.__mock_repo_api.get_password.return_value = "Pass" with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() credentials_file = self.__read_file(testee.get_full_file_path("../credentials.sh")) self.assertEqual( @@ -104,53 +104,26 @@ def test_checkout_with_credentials(self, logging_mock): """, credentials_file, ) - assert logging_mock.method_calls == [ - call.info("Cloning repository: %s", self.__mock_repo_api.get_clone_url()), - call.info("Checking out branch: %s", "master"), - ] - - @patch("gitopscli.git.git_repo.logging") - def test_checkout_branch(self, logging_mock): - with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("xyz") - readme = self.__read_file(testee.get_full_file_path("README.md")) - self.assertEqual("xyz branch readme", readme) - assert logging_mock.method_calls == [ - call.info("Cloning repository: %s", self.__mock_repo_api.get_clone_url()), - call.info("Checking out branch: %s", "xyz"), - ] + logging_mock.info.assert_called_once_with("Cloning repository: %s", self.__mock_repo_api.get_clone_url()) @patch("gitopscli.git.git_repo.logging") - def test_checkout_unknown_url(self, logging_mock): + def test_clone_unknown_url(self, logging_mock): self.__mock_repo_api.get_clone_url.return_value = "invalid_url" with GitRepo(self.__mock_repo_api) as testee: with pytest.raises(GitOpsException) as ex: - testee.checkout("master") + testee.clone() self.assertEqual("Error cloning 'invalid_url'", str(ex.value)) - assert logging_mock.method_calls == [ - call.info("Cloning repository: %s", self.__mock_repo_api.get_clone_url()), - ] - - @patch("gitopscli.git.git_repo.logging") - def test_checkout_unknown_branch(self, logging_mock): - with GitRepo(self.__mock_repo_api) as testee: - with pytest.raises(GitOpsException) as ex: - testee.checkout("foo") - self.assertEqual("Error checking out branch 'foo'", str(ex.value)) - assert logging_mock.method_calls == [ - call.info("Cloning repository: %s", self.__mock_repo_api.get_clone_url()), - call.info("Checking out branch: %s", "foo"), - ] + logging_mock.info.assert_called_once_with("Cloning repository: %s", self.__mock_repo_api.get_clone_url()) def test_get_full_file_path(self): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() self.assertRegex(testee.get_full_file_path("foo.bar"), r"^/tmp/gitopscli/[0-9a-f\-]+/repo/foo\.bar$") @patch("gitopscli.git.git_repo.logging") def test_new_branch(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() testee.new_branch("foo") @@ -163,18 +136,18 @@ def test_new_branch(self, logging_mock): @patch("gitopscli.git.git_repo.logging") def test_new_branch_name_collision(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() with pytest.raises(GitOpsException) as ex: - testee.new_branch("xyz") - self.assertEqual("Error creating new branch 'xyz'.", str(ex.value)) - logging_mock.info.assert_called_once_with("Creating new branch: %s", "xyz") + testee.new_branch("master") + self.assertEqual("Error creating new branch 'master'.", str(ex.value)) + logging_mock.info.assert_called_once_with("Creating new branch: %s", "master") @patch("gitopscli.git.git_repo.logging") def test_commit(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() with open(testee.get_full_file_path("foo.md"), "w") as outfile: @@ -196,7 +169,7 @@ def test_commit(self, logging_mock): @patch("gitopscli.git.git_repo.logging") def test_commit_nothing_to_commit(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() testee.commit(git_user="john doe", git_email="john@doe.com", message="empty commit") @@ -210,7 +183,7 @@ def test_commit_nothing_to_commit(self, logging_mock): @patch("gitopscli.git.git_repo.logging") def test_push(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() with open(testee.get_full_file_path("foo.md"), "w") as readme: readme.write("new file") @@ -231,16 +204,26 @@ def test_push(self, logging_mock): @patch("gitopscli.git.git_repo.logging") def test_push_no_changes(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() testee.push("master") logging_mock.info.assert_called_once_with("Pushing branch: %s", "master") + @patch("gitopscli.git.git_repo.logging") + def test_push_current_branch(self, logging_mock): + with GitRepo(self.__mock_repo_api) as testee: + testee.clone() + testee.new_branch("foo") + logging_mock.reset_mock() + + testee.push() # current branch + logging_mock.info.assert_called_once_with("Pushing branch: %s", "foo") + @patch("gitopscli.git.git_repo.logging") def test_push_unknown_branch(self, logging_mock): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() logging_mock.reset_mock() with pytest.raises(GitOpsException) as ex: @@ -256,7 +239,7 @@ def test_push_commit_hook_error_reason_is_shown(self, logging_mock): chmod(f"{repo_dir}/.git/hooks/pre-receive", stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() with open(testee.get_full_file_path("foo.md"), "w") as readme: readme.write("new file") @@ -274,5 +257,5 @@ def test_push_commit_hook_error_reason_is_shown(self, logging_mock): def test_get_author_from_last_commit(self): with GitRepo(self.__mock_repo_api) as testee: - testee.checkout("master") + testee.clone() self.assertEqual("unit tester ", testee.get_author_from_last_commit()) diff --git a/tests/git/test_git_repo_api_logging_proxy.py b/tests/git/test_git_repo_api_logging_proxy.py index d0f7d1c0..0bc9a185 100644 --- a/tests/git/test_git_repo_api_logging_proxy.py +++ b/tests/git/test_git_repo_api_logging_proxy.py @@ -54,6 +54,23 @@ def test_create_pull_request(self, logging_mock): "Creating pull request from '%s' to '%s' with title: %s", "", "", "", ) + @patch("gitopscli.git.git_repo_api_logging_proxy.logging") + def test_create_pull_request_to_default_branch(self, logging_mock): + expected_return_value = GitRepoApi.PullRequestIdAndUrl(42, "<url>") + self.__mock_repo_api.create_pull_request_to_default_branch.return_value = expected_return_value + + actual_return_value = self.__testee.create_pull_request_to_default_branch( + from_branch="<from branch>", title="<title>", description="<description>" + ) + + self.assertEqual(actual_return_value, expected_return_value) + self.__mock_repo_api.create_pull_request_to_default_branch.assert_called_once_with( + "<from branch>", "<title>", "<description>" + ) + logging_mock.info.assert_called_once_with( + "Creating pull request from '%s' to default branch with title: %s", "<from branch>", "<title>", + ) + @patch("gitopscli.git.git_repo_api_logging_proxy.logging") def test_merge_pull_request(self, logging_mock): self.__testee.merge_pull_request(pr_id=42)