Skip to content

Commit

Permalink
adds support for Gitlab
Browse files Browse the repository at this point in the history
  • Loading branch information
apoclyps committed Jun 24, 2021
1 parent 876b2bd commit 312ebe6
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ max-complexity = 20
max-line-length = 119
select = C,E,F,W,B,B950
ignore = E203,E501,D107,D102,W503
# Ignore unused import warnings in __init__.py files
per-file-ignores = __init__.py:F401
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ If you want to get up and running with Reviews immediately, run:
```bash
export GITHUB_USER="your-github-username"
export GITHUB_TOKEN="your personal GitHub token used for interacting with the API"
export REVIEWS_REPOSITORY_CONFIGURATION="apoclyps/reviews"
export REVIEWS_GITHUB_REPOSITORY_CONFIGURATION="apoclyps/reviews"

pip install --upgrade reviews

Expand Down Expand Up @@ -61,7 +61,7 @@ $ source env/bin/activate
If you wish to keep a copy of Reviews on your host system, you can install and run it using:

```bash
$ export REVIEWS_REPOSITORY_CONFIGURATION="apoclyps/reviews"
$ export REVIEWS_GITHUB_REPOSITORY_CONFIGURATION="apoclyps/reviews"
$ python -m venv env
$ source env/bin/activate
$ python -m pip install -e .
Expand Down Expand Up @@ -97,15 +97,15 @@ Create a `settings.ini` next to your configuration module in the form:

```bash
[settings]
REVIEWS_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example
Note: Since ConfigParser supports string interpolation, to represent the character % you need to escape it as %%.
```

#### Env file
Create a `.env` text file on your repository's root directory in the form:

```bash
REVIEWS_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example
```

#### Providing a configuration path
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ humanize==3.9.0
keyboard==0.13.5
PyGithub==1.55
python-decouple==3.4
python-gitlab==2.8.0
rich==10.4.0
idna==2.10
52 changes: 42 additions & 10 deletions reviews/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from . import config
from .config import render_config_table
from .controller import PullRequestController
from .controller import GithubPullRequestController, GitlabPullRequestController
from .layout import (
RenderLayoutManager,
generate_layout,
Expand All @@ -29,7 +29,7 @@ def add_log_event(message: str) -> List[Tuple[str, str]]:
return logs


def _render_pull_requests(controller: PullRequestController, configuration: List[Tuple[str, str]]) -> Panel:
def _render_pull_requests(controller: GithubPullRequestController, configuration: List[Tuple[str, str]]) -> Panel:
"""Renders all pull requests for the provided configuration"""

tables = [controller.retrieve_pull_requests(org=org, repository=repo) for (org, repo) in configuration]
Expand All @@ -42,13 +42,35 @@ def _render_pull_requests(controller: PullRequestController, configuration: List
)


def _render_gitlab_pull_requests(
controller: GitlabPullRequestController, configuration: List[Tuple[str, str]]
) -> Panel:
"""Renders all pull requests for the provided configuration"""

tables = [
controller.retrieve_pull_requests(project_id=project_id, namespace=namespace)
for (project_id, namespace) in configuration
]

# filter unrenderable `None` results
return Panel(
RenderGroup(*[t for t in tables if t]),
title="Activity",
border_style="blue",
)


def single_render() -> None:
"""Renders the Terminal UI Dashboard once before closing the application"""

configuration = config.get_configuration()
controller = PullRequestController()
# configuration = config.get_github_configuration()
# controller = GithubPullRequestController()
configuration = config.get_gitlab_configuration()
controller = GitlabPullRequestController()

# body = _render_pull_requests(controller=controller, configuration=configuration)

body = _render_pull_requests(controller=controller, configuration=configuration)
body = _render_gitlab_pull_requests(controller=controller, configuration=configuration)

layout_manager = RenderLayoutManager(layout=generate_layout(log=False, footer=False))
layout_manager.render_layout(
Expand Down Expand Up @@ -79,8 +101,8 @@ def render() -> None:
# initial load should be from database
add_log_event(message="initializing...")

configuration = config.get_configuration()
controller = PullRequestController()
configuration = config.get_github_configuration()
controller = GithubPullRequestController()

layout_manager = RenderLayoutManager(layout=generate_layout())
layout_manager.render_layout(
Expand Down Expand Up @@ -132,19 +154,29 @@ def render_config(show: bool) -> None:
configurations = [
{
"name": "GITHUB_TOKEN",
"value": config.GITHUB_TOKEN if show else "".join("*" for _ in range(0, len(config.GITHUB_TOKEN))),
"value": config.GITHUB_TOKEN if show else "".join("*" for _ in range(len(config.GITHUB_TOKEN))),
},
{"name": "GITHUB_USER", "value": config.GITHUB_USER},
{"name": "GITHUB_URL", "value": config.GITHUB_URL},
{
"name": "GITLAB_TOKEN",
"value": config.GITLAB_TOKEN if show else "".join("*" for _ in range(len(config.GITLAB_TOKEN))),
},
{"name": "GITLAB_USER", "value": config.GITLAB_USER},
{"name": "GITLAB_URL", "value": config.GITLAB_URL},
{"name": "REVIEWS_PATH_TO_CONFIG", "value": f"{config.REVIEWS_PATH_TO_CONFIG}"},
{
"name": "GITHUB_DEFAULT_PAGE_SIZE",
"value": f"{config.GITHUB_DEFAULT_PAGE_SIZE}",
},
{"name": "REVIEWS_DELAY_REFRESH", "value": f"{config.REVIEWS_DELAY_REFRESH}"},
{
"name": "REVIEWS_REPOSITORY_CONFIGURATION",
"value": ", ".join(config.REVIEWS_REPOSITORY_CONFIGURATION),
"name": "REVIEWS_GITHUB_REPOSITORY_CONFIGURATION",
"value": ", ".join(config.REVIEWS_GITHUB_REPOSITORY_CONFIGURATION),
},
{
"name": "REVIEWS_GITLAB_REPOSITORY_CONFIGURATION",
"value": ", ".join(config.REVIEWS_GITLAB_REPOSITORY_CONFIGURATION),
},
{
"name": "REVIEWS_LABEL_CONFIGURATION",
Expand Down
9 changes: 7 additions & 2 deletions reviews/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from ..config.helpers import get_configuration, get_label_colour_map # NOQA: F401
from ..config.helpers import get_github_configuration, get_gitlab_configuration, get_label_colour_map # NOQA: F401
from ..config.render import render_config_table # NOQA: F401
from ..config.settings import ( # NOQA: F401
GITHUB_DEFAULT_PAGE_SIZE,
GITHUB_TOKEN,
GITHUB_URL,
GITHUB_USER,
GITLAB_DEFAULT_PAGE_SIZE,
GITLAB_TOKEN,
GITLAB_URL,
GITLAB_USER,
REVIEWS_DELAY_REFRESH,
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION,
REVIEWS_GITLAB_REPOSITORY_CONFIGURATION,
REVIEWS_LABEL_CONFIGURATION,
REVIEWS_PATH_TO_CONFIG,
REVIEWS_REPOSITORY_CONFIGURATION,
)
24 changes: 21 additions & 3 deletions reviews/config/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

from rich.color import ANSI_COLOR_NAMES

from .settings import REVIEWS_LABEL_CONFIGURATION, REVIEWS_REPOSITORY_CONFIGURATION
from .settings import (
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION,
REVIEWS_GITLAB_REPOSITORY_CONFIGURATION,
REVIEWS_LABEL_CONFIGURATION,
)


def get_configuration() -> List[Tuple[str, str]]:
def get_github_configuration() -> List[Tuple[str, str]]:
"""converts a comma separated list of organizations/repositories into a list
of tuples.
"""
Expand All @@ -15,7 +19,21 @@ def _to_tuple(values: List[str]) -> Tuple[str, str]:

return [
_to_tuple(values=configuration.split(sep="/", maxsplit=1))
for configuration in REVIEWS_REPOSITORY_CONFIGURATION
for configuration in REVIEWS_GITHUB_REPOSITORY_CONFIGURATION
]


def get_gitlab_configuration() -> List[Tuple[str, str]]:
"""converts a comma separated list of organizations/repositories into a list
of tuples.
"""

def _to_tuple(values: List[str]) -> Tuple[str, str]:
return (values[0], values[1])

return [
_to_tuple(values=configuration.split(sep=":", maxsplit=1))
for configuration in REVIEWS_GITLAB_REPOSITORY_CONFIGURATION
]


Expand Down
15 changes: 13 additions & 2 deletions reviews/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,25 @@
GITHUB_URL = config("GITHUB_URL", cast=str, default="https://api.github.com")
GITHUB_DEFAULT_PAGE_SIZE = config("GITHUB_DEFAULT_PAGE_SIZE", cast=int, default=100)

# Gitlab config
GITLAB_TOKEN = config("GITLAB_TOKEN", cast=str, default="")
GITLAB_USER = config("GITLAB_USER", cast=str, default="")
GITLAB_URL = config("GITLAB_URL", cast=str, default="https://gitlab.com")
GITLAB_DEFAULT_PAGE_SIZE = config("GITLAB_DEFAULT_PAGE_SIZE", cast=int, default=100)

# Application Config
REVIEWS_PATH_TO_CONFIG = config("REVIEWS_PATH_TO_CONFIG", cast=str, default=None)
REVIEWS_DELAY_REFRESH = config("REVIEWS_DELAY_REFRESH", cast=int, default=60)
REVIEWS_REPOSITORY_CONFIGURATION = config(
"REVIEWS_REPOSITORY_CONFIGURATION",
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION = config(
"REVIEWS_GITHUB_REPOSITORY_CONFIGURATION",
cast=Csv(),
default="apoclyps/reviews",
)
REVIEWS_GITLAB_REPOSITORY_CONFIGURATION = config(
"REVIEWS_GITLAB_REPOSITORY_CONFIGURATION",
cast=Csv(),
default="27629846:apoclyps/reviews",
)
REVIEWS_LABEL_CONFIGURATION = config(
"REVIEWS_LABEL_CONFIGURATION",
cast=Csv(),
Expand Down
108 changes: 107 additions & 1 deletion reviews/controller.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from datetime import datetime
from typing import Dict, List, Union

from github.PullRequest import PullRequest as ghPullRequest
from gitlab.v4.objects.merge_requests import MergeRequest as GitlabMergeRequest
from rich.table import Table

from reviews.source_control.client import GitlabAPI

from . import config
from .errors import RepositoryDoesNotExist
from .layout import render_pull_request_table, render_repository_does_not_exist
from .source_control import GithubAPI, Label, PullRequest


class PullRequestController:
class GithubPullRequestController:
"""retrieve and store pull requests."""

def __init__(self) -> None:
Expand Down Expand Up @@ -87,3 +91,105 @@ def _get_reviews(pull_request: ghPullRequest) -> Dict[str, str]:
)

return code_review_requests


class GitlabPullRequestController:
"""retrieve and store pull requests."""

def __init__(self) -> None:
self.client = GitlabAPI()

def retrieve_pull_requests(self, project_id: str, namespace: str) -> Union[Table, None]:
"""Renders Terminal UI Dashboard"""
pull_requests = []
try:
pull_requests = self.update_pull_requests(project_id=project_id, namespace=namespace)
except RepositoryDoesNotExist:
# TODO: refactor this function for usage with Gitlab
return render_repository_does_not_exist(
title=f"{namespace} does not exist",
org="",
repository="",
)

if not pull_requests:
return None

# TODO: refactor this function to accept a link
return render_pull_request_table(
title=namespace,
pull_requests=pull_requests,
org="",
repository="",
)

def update_pull_requests(self, project_id: str, namespace: str) -> List[PullRequest]:
"""Updates repository models."""

def _get_reviews(pull_request: GitlabMergeRequest) -> Dict[str, str]:
"""Inner function to retrieve reviews for a pull request"""
reviews = pull_request.approvals.get()

return {reviewer["user"]["username"]: "approved" for reviewer in reviews.approvers}

# ProjectMergeRequest
pull_requests = self.client.get_pull_requests(project_id=project_id, namespace=namespace)

code_review_requests = []
for pull_request in pull_requests:

reviews = _get_reviews(pull_request=pull_request)

if pull_request.author["username"] == config.GITLAB_USER:
approved_by_me = "AUTHOR"
else:
approved_by_me = reviews.get(config.GITLAB_USER, "") # NOQA: R1721

approved_by_others = any(
[True for user, status in reviews.items() if user != config.GITLAB_USER and status == "APPROVED"]
)

def get_labels(labels: List[str]) -> List[Label]:
visible_labels = []
labels_mapping = {label: len(label) for label in labels}

current_size = 0
max_size = 18
hidden_labels = 0
for label, size in labels_mapping.items():
if current_size == 0:
current_size += size
visible_labels.append(label)
continue

if current_size + size < max_size:
current_size += size
visible_labels.append(label)
else:
hidden_labels += 1

labels = [Label(name=label) for label in visible_labels]

if hidden_labels:
labels.append(Label(name=f"+{hidden_labels} others"))

return labels

labels = get_labels(labels=pull_request.labels)

code_review_requests.append(
PullRequest(
number=pull_request.iid,
title=pull_request.title,
draft=pull_request.draft,
additions=0,
deletions=0,
created_at=datetime.strptime(pull_request.created_at, "%Y-%m-%dT%H:%M:%S.%f%z"),
updated_at=datetime.strptime(pull_request.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z"),
approved=approved_by_me,
approved_by_others=approved_by_others,
labels=labels,
)
)

return code_review_requests
4 changes: 4 additions & 0 deletions reviews/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ class RepositoryDoesNotExist(Exception):

class InvalidGithubToken(Exception):
"""Unable to query Github using an empty or invalid Github Token"""


class InvalidGitlabToken(Exception):
"""Unable to query Gitlab using an empty or invalid Gitlab Token"""
Loading

0 comments on commit 312ebe6

Please sign in to comment.