class BitBucket: def __init__(self, url, username, password): self.bitbucket = AtlassianBitBucketLib( url=url, username=username, password=password, ) async def create_pull_request( self, source_project: str, source_repo: str, dest_project: str, dest_repo: str, branch_name: str, main_branch_name: str, commit_title: str, commit_message: str, ) -> str: pull_request = self.bitbucket.open_pull_request( source_project, source_repo, dest_project, dest_repo, branch_name, main_branch_name, commit_title, commit_message, ) pull_request_url = pull_request["links"]["self"][0]["href"] return pull_request_url
class BitbucketGitRepoApiAdapter(GitRepoApi): def __init__( self, git_provider_url: str, username: Optional[str], password: Optional[str], organisation: str, repository_name: str, ) -> None: self.__bitbucket = Bitbucket(git_provider_url, username, password) self.__git_provider_url = git_provider_url self.__organisation = organisation self.__repository_name = repository_name def get_username(self) -> Optional[str]: return str(self.__bitbucket.username) def get_password(self) -> Optional[str]: return str(self.__bitbucket.password) def get_clone_url(self) -> str: try: repo = self.__bitbucket.get_repo(self.__organisation, self.__repository_name) except requests.exceptions.ConnectionError as ex: raise GitOpsException(f"Error connecting to '{self.__git_provider_url}''") from ex if "errors" in repo: for error in repo["errors"]: exception = error["exceptionName"] if exception == "com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException": raise GitOpsException("Bad credentials") if exception == "com.atlassian.bitbucket.project.NoSuchProjectException": raise GitOpsException(f"Organisation '{self.__organisation}' does not exist") if exception == "com.atlassian.bitbucket.repository.NoSuchRepositoryException": raise GitOpsException(f"Repository '{self.__organisation}/{self.__repository_name}' does not exist") raise GitOpsException(error["message"]) if "links" not in repo: raise GitOpsException(f"Repository '{self.__organisation}/{self.__repository_name}' does not exist") for clone_link in repo["links"]["clone"]: if clone_link["name"] == "http": repo_url = clone_link["href"] if not repo_url: 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: pull_request = self.__bitbucket.open_pull_request( self.__organisation, self.__repository_name, self.__organisation, self.__repository_name, from_branch, to_branch, title, description, ) if "errors" in pull_request: raise GitOpsException(pull_request["errors"][0]["message"]) return GitRepoApi.PullRequestIdAndUrl(pr_id=pull_request["id"], url=pull_request["links"]["self"][0]["href"]) def merge_pull_request(self, pr_id: int, merge_method: Literal["squash", "rebase", "merge"] = "merge") -> None: pull_request = self.__bitbucket.get_pullrequest(self.__organisation, self.__repository_name, pr_id) self.__bitbucket.merge_pull_request( self.__organisation, self.__repository_name, pull_request["id"], pull_request["version"] ) def add_pull_request_comment(self, pr_id: int, text: str, parent_id: Optional[int] = None) -> None: pull_request_comment = self.__bitbucket.add_pull_request_comment( self.__organisation, self.__repository_name, pr_id, text, parent_id ) if "errors" in pull_request_comment: raise GitOpsException(pull_request_comment["errors"][0]["message"]) def delete_branch(self, branch: str) -> None: branch_hash = self.get_branch_head_hash(branch) result = self.__bitbucket.delete_branch(self.__organisation, self.__repository_name, branch, branch_hash) if result and "errors" in result: raise GitOpsException(result["errors"][0]["message"]) def get_branch_head_hash(self, branch: str) -> str: branches = self.__bitbucket.get_branches(self.__organisation, self.__repository_name, filter=branch, limit=1) if not branches: raise GitOpsException(f"Branch '{branch}' not found'") return str(branches[0]["latestCommit"]) def get_pull_request_branch(self, pr_id: int) -> str: pull_request = self.__bitbucket.get_pullrequest(self.__organisation, self.__repository_name, pr_id) 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"])
class Server: """ Class that represents the BitBucket server. Has methods to perform queries on the server. """ def __init__(self, url: str, user: str, password: str): """ C'tor of Server. :param str url: URL of the BitBucket server :param str user: Login username :param str password: Login password or token """ self.server_url = url self.user = user self.password = password self.api = Bitbucket(self.server_url, self.user, self.password) def project_list(self) -> List[str]: """ Get the list of projects from the server. :return: List of project names """ query = self.api.project_list() return query def pr_approved(self, project: str, repo: str, pr: int) -> bool: """ Return True if at least one reviewer approved the pull request, otherwise False. :param: project Project ID of the repository :param: repo Repository slug of the pull request :param: pr Pull request ID :returns: True if one reviewer approved the pull request, otherwise False """ query = self.api.get_pull_request(project, repo, pr) for reviewer in query["reviewers"]: if reviewer["approved"]: return True return False def open_pr_in_repo( self, project: str, repo: str, src_branch: str, dst_branch: str, title: str, desc: str, reviewers: str = None, ): """ Open a new pull request in a repository. :param project: Project name :param repo: Repository name :param src_branch: Source branch name :param dst_branch: Destination branch name :param title: Title of the pull request :param desc: Description text of the pull request :param reviewers: UUIDs of reviewers (default None) """ self.open_pr(project, repo, src_branch, project, repo, dst_branch, title, desc, reviewers) def open_pr( self, src_project: str, src_repo: str, src_branch: str, dst_project: str, dst_repo: str, dst_branch: str, title: str, desc: str, reviewers: str = None, ): """ Open a new pull request. :param src_project: Source project name :param src_repo: Source repository name :param src_branch: Source branch name :param dst_project: Destination project name :param dst_repo: Destination repository name :param dst_branch: Destination branch name :param title: Title of the pull request :param desc: Description text of the pull request :param reviewers: UUIDs of reviewers (default None) """ logging.info("Attempting to open a pull request:") logging.info(title) logging.info(desc) if self._confirm("Open pull request " + src_project + "/" + src_repo + "/" + src_branch + "->" + dst_project + "/" + dst_repo + "/" + dst_branch): self.api.open_pull_request( src_project, src_repo, dst_project, dst_repo, src_branch, dst_branch, title, desc, reviewers, ) print("Success") else: print("Action aborted") @staticmethod def _confirm(question: str) -> bool: """Ask for user decision on question.""" reply = str(input(question + " (y/n): ")).lower().strip() return reply[0] == "y"
class BitBucketGitUtil(AbstractGitUtil): def __init__(self, tmp_dir, git_provider_url, organisation, repository_name, username, password, git_user, git_email): super().__init__(tmp_dir, username, password, git_user, git_email) self._git_provider_url = git_provider_url self._organisation = organisation self._repository_name = repository_name self._bitbucket = Bitbucket(self._git_provider_url, self._username, self._password) def get_clone_url(self): try: repo = self._bitbucket.get_repo(self._organisation, self._repository_name) except requests.exceptions.ConnectionError as ex: raise GitOpsException( f"Error connecting to '{self._git_provider_url}''") from ex if "errors" in repo: for error in repo["errors"]: exception = error["exceptionName"] if exception == "com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException": raise GitOpsException("Bad credentials") if exception == "com.atlassian.bitbucket.project.NoSuchProjectException": raise GitOpsException( f"Organisation '{self._organisation}' does not exist") if exception == "com.atlassian.bitbucket.repository.NoSuchRepositoryException": raise GitOpsException( f"Repository '{self._organisation}/{self._repository_name}' does not exist" ) raise GitOpsException(error["message"]) if "links" not in repo: raise GitOpsException( f"Repository '{self._organisation}/{self._repository_name}' does not exist" ) for clone_link in repo["links"]["clone"]: if clone_link["name"] == "http": repo_url = clone_link["href"] if not repo_url: raise GitOpsException("Couldn't determine repository URL.") return repo_url def create_pull_request(self, from_branch, to_branch, title, description): pull_request = self._bitbucket.open_pull_request( self._organisation, self._repository_name, self._organisation, self._repository_name, from_branch, to_branch, title, description, ) if "errors" in pull_request: raise GitOpsException(pull_request["errors"][0]["message"]) return pull_request def get_pull_request_url(self, pull_request): return pull_request["links"]["self"][0]["href"] def merge_pull_request(self, pull_request): self._bitbucket.merge_pull_request(self._organisation, self._repository_name, pull_request["id"], pull_request["version"]) def add_pull_request_comment(self, pr_id, text, parent_id): pull_request_comment = self._bitbucket.add_pull_request_comment( self._organisation, self._repository_name, pr_id, text, parent_id) if "errors" in pull_request_comment: raise GitOpsException(pull_request_comment["errors"][0]["message"]) return pull_request_comment def delete_branch(self, branch): branches = self._bitbucket.get_branches(self._organisation, self._repository_name, filter=branch, limit=1) if not branches: raise GitOpsException(f"Branch '{branch}' not found'") result = self._bitbucket.delete_branch(self._organisation, self._repository_name, branch, branches[0]["latestCommit"]) if result and "errors" in result: raise GitOpsException(result["errors"][0]["message"]) def get_pull_request_branch(self, pr_id): pull_request = self._bitbucket.get_pullrequest(self._organisation, self._repository_name, pr_id) if "errors" in pull_request: raise GitOpsException(pull_request["errors"][0]["message"]) return pull_request["fromRef"]["displayId"]