Skip to content

Commit

Permalink
feat: adds support for GitLab as a Provider type
Browse files Browse the repository at this point in the history
Refs: PSCE-192

Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 committed Jul 27, 2023
1 parent 8f875fb commit b39e90e
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 16 deletions.
35 changes: 34 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ python = '^3.8.1'
gitpython = "^3.1.31"
compliance-trestle = "^2.2.1"
github3-py = "^4.0.1"
python-gitlab = "^3.15.0"

[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
Expand Down
5 changes: 2 additions & 3 deletions tests/trestlebot/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,8 @@ def test_with_target_branch(monkeypatch, valid_args_dict, capsys):
captured = capsys.readouterr()

expected_string = (
"target-branch flag is set with an unsupported git provider. "
"If testing locally with the GitHub API, "
"set the GITHUB_ACTIONS environment variable to true."
"target-branch flag is set with an unset git provider. "
"To test locally, set the GITHUB_ACTIONS or GITLAB_CI environment variable."
)

assert expected_string in captured.err
90 changes: 90 additions & 0 deletions tests/trestlebot/test_gitlab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/python

# Copyright 2023 Red Hat, Inc.
#
# 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.

"""Test for GitLab provider logic"""

from typing import Tuple

import pytest
from git.repo import Repo

from tests.testutils import clean
from trestlebot.gitlab import GitLab
from trestlebot.provider import GitProviderException


@pytest.mark.parametrize(
"repo_url",
[
"https://gitlab.com/owner/repo",
"https://gitlab.com/owner/repo.git",
"gitlab.com/owner/repo.git",
],
)
def test_parse_repository(repo_url: str) -> None:
"""Tests parsing valid GitLab repo urls"""
gl = GitLab("fake")

owner, repo_name = gl.parse_repository(repo_url)

assert owner == "owner"
assert repo_name == "repo"


@pytest.mark.parametrize(
"repo_url",
[
"https://mygitlab.com/owner/repo",
"https://mygitlab.com/owner/repo.git",
"mygitlab.com/owner/repo.git",
],
)
def test_parse_repository_with_server_url(repo_url: str) -> None:
"""Test an invalid url input"""
gl = GitLab("fake", "https://mygitlab.com")

owner, repo_name = gl.parse_repository(repo_url)

assert owner == "owner"
assert repo_name == "repo"


def test_parse_repository_integration(tmp_repo: Tuple[str, Repo]) -> None:
"""Tests integration with git remote get-url"""
repo_path, repo = tmp_repo

repo.create_remote("origin", url="gitlab.com/test/repo.git")

remote = repo.remote()

gl = GitLab("fake")

owner, repo_name = gl.parse_repository(remote.url)

assert owner == "test"
assert repo_name == "repo"

clean(repo_path, repo)


def test_parse_repository_with_incorrect_name() -> None:
"""Test an invalid url input"""
gl = GitLab("fake")
with pytest.raises(
GitProviderException,
match="https://notgitlab.com/owner/repo.git is an invalid repo URL",
):
gl.parse_repository("https://notgitlab.com/owner/repo.git")
30 changes: 21 additions & 9 deletions trestlebot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from trestlebot import bot, const, log
from trestlebot.github import GitHub
from trestlebot.gitlab import GitLab
from trestlebot.provider import GitProvider
from trestlebot.tasks.assemble_task import AssembleTask
from trestlebot.tasks.authored import types
Expand Down Expand Up @@ -229,19 +230,22 @@ def run() -> None:
logger.info("Regeneration task skipped")

if args.target_branch:
if not is_github_actions():
logger.error(
"target-branch flag is set with an unsupported git provider. "
"If testing locally with the GitHub API, set "
"the GITHUB_ACTIONS environment variable to true."
)
sys.exit(const.ERROR_EXIT_CODE)

if not args.with_token:
logger.error("with-token value cannot be empty")
sys.exit(const.ERROR_EXIT_CODE)

git_provider = GitHub(access_token=args.with_token.read().strip())
if is_github_actions():
git_provider = GitHub(access_token=args.with_token.read().strip())
elif is_gitlab_ci():
git_provider = GitLab(api_token=args.with_token.read().strip())
else:
logger.error(
(
"target-branch flag is set with an unset git provider. "
"To test locally, set the GITHUB_ACTIONS or GITLAB_CI environment variable."
)
)
sys.exit(const.ERROR_EXIT_CODE)

exit_code: int = const.SUCCESS_EXIT_CODE

Expand Down Expand Up @@ -287,3 +291,11 @@ def is_github_actions() -> bool:
if var_value and var_value.lower() in ["true", "1"]:
return True
return False


# GitLab ref: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
def is_gitlab_ci() -> bool:
var_value = os.getenv("GITLAB_CI")
if var_value and var_value.lower() in ["true", "1"]:
return True
return False
6 changes: 3 additions & 3 deletions trestlebot/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ def __init__(self, access_token: str):
session: github3.GitHub = github3.GitHub()
session.login(token=access_token)

self.session = session
self._session = session
self.pattern = r"^(?:https?://)?github\.com/([^/]+)/([^/.]+)"

def parse_repository(self, repo_url: str) -> Tuple[str, str]:
"""
Parse repository url
Args:
repo_url: Valid url for GitHub repo
repo_url: Valid url for a GitHub repo
Returns:
Owner and repo name in a tuple, respectively
Expand Down Expand Up @@ -84,7 +84,7 @@ def create_pull_request(
Returns:
Pull request number
"""
repository: Optional[Repository] = self.session.repository(
repository: Optional[Repository] = self._session.repository(
owner=ns, repository=repo_name
)
if repository is None:
Expand Down
101 changes: 101 additions & 0 deletions trestlebot/gitlab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/python

# Copyright 2023 Red Hat, Inc.
#
# 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.

"""GitLab related functions for the Trestle Bot."""

import re
from typing import Tuple

import gitlab

from trestlebot.provider import GitProvider, GitProviderException


class GitLab(GitProvider):
def __init__(self, api_token: str, server_url: str = "https://gitlab.com"):
"""Create GitLab object to interact with the GitLab API"""

self._gitlab_client = gitlab.Gitlab(server_url, private_token=api_token)

stripped_url = re.sub(r"^(https?://)?", "", server_url)
self.pattern = r"^(?:https?://)?{0}/([^/]+)/([^/.]+)".format(
re.escape(stripped_url)
)

def parse_repository(self, repo_url: str) -> Tuple[str, str]:
"""
Parse repository url
Args:
repo_url: Valid url for a GitLab repo
Returns:
Owner and project name in a tuple, respectively
"""
match = re.match(self.pattern, repo_url)

if not match:
raise GitProviderException(f"{repo_url} is an invalid repo URL")

owner = match.group(1)
repo = match.group(2)
return (owner, repo)

def create_pull_request(
self,
ns: str,
repo_name: str,
base_branch: str,
head_branch: str,
title: str,
body: str,
) -> int:
"""
Create a pull (merge request in the GitLab) request in the repository
Args:
ns: Namespace or owner of the repository
repo_name: Name of the repository
base_branch: Branch that changes need to be merged into
head_branch: Branch with changes
title: Text for the title of the pull_request
body: Text for the body of the pull request
Returns:
Pull/Merge request number
"""

try:
project = self._gitlab_client.projects.get(f"{ns}/{repo_name}")
merge_request = project.mergerequests.create(
{
"source_branch": head_branch,
"target_branch": base_branch,
"title": title,
"description": body,
}
)

return merge_request.id

except gitlab.exceptions.GitlabCreateError as e:
raise GitProviderException(
f"Failed to create merge request in {ns}/{repo_name}: {e}"
)
except gitlab.exceptions.GitlabAuthenticationError as e:
raise GitProviderException(
f"Authentication error during merge request creation in {ns}/{repo_name}: {e}"
)

0 comments on commit b39e90e

Please sign in to comment.