Code Monkey home page Code Monkey logo

reviews's Introduction

Reviews

Tests pypi versions

Simplify requests for code review with an all-in-one TUI dashboard providing an overview of open PRs requiring review!

Quick Start

If you want to get up and running with Reviews, you will first first need to create a personal access token. To create a Personal Access Token with the required permissions for reviews, you can generate a new token (classic) and enable the following full scopes for the following:

  • repo
  • admin:org

Note: Whilst reviews only requires read only access, providing only public_repo and read:org permissions is insufficent as it does encompass all types of read-only access for repositories.

To start using reviews, you can run the following:

export GITHUB_USER="your-github-username"
export GITHUB_TOKEN="your personal GitHub token used for interacting with the API"
export REVIEWS_GITHUB_REPOSITORY_CONFIGURATION="apoclyps/reviews"

pip install --upgrade reviews

reviews config --show

reviews dashboard

asciicast

Additional Support

View Configuration

If you wish to view the configuration used by reviews at any time, you can use the following command to show all configuration (with secrets hidden or shown):

reviews config --hide

reviews config --show

Gitlab

If you wish to use reviews with Gitlab, you will need to specify the configuration like so: product id:project name/repository and use the --provider=gitlab flag when running reviews:

export GITLAB_USER=user
export GITLAB_TOKEN=token
export REVIEWS_GITLAB_REPOSITORY_CONFIGURATION="27629846:apoclyps/reviews"

reviews dashboard --provider=gitlab

Getting started with local development

To build and run the CLI on your host, you will need Python 3.9, pip, and virtualenv to build and run review. If you wish to publish a PR with your changes, first create a fork on Github and clone that code.

$ gh repo clone apoclyps/reviews
$ cd reviews
$ python3 -m venv env
$ source env/bin/activate
(env)$ pip install -r requirements_dev.txt
(env)$ pip install -r requirements.txt
(env)$ python -m reviews dashboard

If you wish to keep a copy of Reviews on your host system, you can install and run it using:

$ export REVIEWS_GITHUB_REPOSITORY_CONFIGURATION="apoclyps/reviews"
$ python -m venv env
$ source env/bin/activate
$ python -m pip install -e .
$ reviews -h

You can run the Reviews within Docker:

docker-compose build cli && docker-compose run --rm cli python -m reviews dashboard

To build an image and run that image with all of the necessary dependencies using the following commands:

$ docker-compose build cli
$ docker-compose run --rm cli python -m reviews dashboard

For instructions on setting up a development environment outside of Docker, check out the wiki.

Configuration

Reviews supports both .ini and .env files. Reviews always searches for configuration in this order:

  • Environment variables;
  • Repository: ini or .env file;
  • Configuration Path
  • Review Defaults

The following steps are used to provide the configuration using a .env or .ini file. The configuration can be read from within the module/repository (default location set by decouple) using the .env file or via a location specified by an environmental variable that points to a .ini file located in the root of the project or in a location specified by PATH_TO_CONFIG.

Using an .env file within the repository

cd /home/<your-user>/workspace/apoclyps/reviews
touch .env

echo "REVIEWS_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example" >> .env
python -m reviews config

Using an .ini file within the repository

cd /home/<your-user>/workspace/apoclyps/reviews
touch settings.ini
echo "[settings]\nREVIEWS_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example" >> settings.ini

python -m reviews config

Providing a configuration path

If you wish to set the configuration path to use an ini or .env file when running the application, you can use the configuration of a specific file by supplying the path to the configuration like so:

cd /home/apoclyps/
touch settings.ini
echo "[settings]\nREVIEWS_REPOSITORY_CONFIGURATION=apoclyps/micropython-by-example" >> settings.ini

cd /home/<your-user>/workspace/apoclyps/reviews
export REVIEWS_PATH_TO_CONFIG=/home/<your-user>/

python -m reviews config

If at any time, you want to confirm your configuration reflects the file you have provided, you can use reviews config to view what current configuration of Reviews.

Testing

A test suite has been included to ensure Reviews functions correctly.

To run the entire test suite with verbose output, run the following:

$ pytest -vvv

Alternatively, to run a single set of tests.

$ pytest -vvv tests/test_config.py

All tests can be run within docker by using the following command:

$ docker-compose build pytest && docker-compose run --rm pytest

Linting

To run individual linting steps:

docker-compose build test
docker-compose run --rm --no-deps test isort .
docker-compose run --rm --no-deps test black --line-length 119 --check .
docker-compose run --rm --no-deps test mypy .
docker-compose run --rm --no-deps test flake8 .
docker-compose run --rm --no-deps test pylint --rcfile=.pylintrc reviews
docker-compose run --rm --no-deps test bandit reviews
docker-compose run --rm --no-deps test vulture --min-confidence 90 reviews
docker-compose run --rm --no-deps test codespell reviews
docker-compose run --rm --no-deps test find . -name '*.py' -exec pyupgrade {} +

You can also set up pre-commit to run the linting steps automatically during the commit phase, the pre-commit pipeline can be set up by running the following command on the project root:

pre-commit install

Contributions

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

reviews's People

Contributors

adam-tokarski avatar apoclyps avatar bphermansson avatar dependabot-preview[bot] avatar dependabot[bot] avatar larsks avatar marz619 avatar mindnumbing avatar paulosgf avatar thewchan avatar yushao2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

reviews's Issues

refactor this function to accept a link

refactor this function to accept a link

# TODO: refactor this function to accept a link

            )

        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: int, 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: int, namespace: str
    ) -> List[PullRequest]:
        """Updates repository models."""

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

            return {
                reviewer["user"]["username"]: True 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

6f50203dc0b1b87523b9510701d6cd5bbcf511c4

[Feature] Add links to pull request titles

Is your feature request related to a problem? Please describe.
I would like to open a pull request by selecting the pull request title in a table listed under Activity.

image

Describe the solution you'd like
When I click on Bump y18n from 3.2.1 to 3.2.2 in /services/client it should open apoclyps/my-dev-space#801" in a browser.

image

Additional context

The title is set here for each pull request

f"[white]{pr.title}",

an example of where the link has been used before can be seen at

tree.add(f"[link=https://www.github.com/{org}/{repo}]{repo}[/link]")

documentation on links is available at https://rich.readthedocs.io/en/latest/markup.html?highlight=link#links

refactor this function to accept a link

refactor this function to accept a link

# TODO: refactor this function to accept a link

            )

        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: int, 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: int, namespace: str) -> List[PullRequest]:
        """Updates repository models."""

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

            return {reviewer["user"]["username"]: True 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"]
            )

            labels = [Label(name=label) for label in 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

70abae0dcb7542d7368b5f7d18bf7860fa6ffd26

[Feature] Show approved (by me) Pull Requests

Is your feature request related to a problem? Please describe.
I would like to see which pull requests have been approved by me and those approved by others.

This will replace the existing status column with 2 new columns.

The first column Approved column should check if the pull request is approved by a user-provided via configuration.

if it has been approved by the config.user in the list of objects returned by pull_request.get_reviews().

The second Ready to Release column should check

if it has been approved by others if the config.user is not in the list of objects returned by pull_request.get_reviews().

Describe the solution you'd like
I would like to have a status column for Approved and Ready to Release.

Additional context

Existing approved status is set here:

approved=any(pull_request.get_reviews()), # NOQA: R1721

this will request changes to the PullRequest data class in

@dataclass
class PullRequest:
"""Models a Pull Request"""
number: int
title: str
created_at: datetime
updated_at: datetime
approved: bool
labels: List[Label] = field(default_factory=list)
to add the additional fields and to remove status.

A configurable user can be set via an environmental variable and used by defining it within:

# Application Config
DELAY_REFRESH = config("DELAY_REFRESH", cast=int, default=60)
ENABLE_NOTIFICATIONS = config("ENABLE_NOTIFICATIONS", cast=bool, default=False)
ENABLE_PERSISTED_DATA = config("ENABLE_PERSISTED_DATA", cast=bool, default=False)
REPOSITORY_CONFIGURATION = config(
"REPOSITORY_CONFIGURATION",
cast=Csv(),
default="apoclyps/code-review-manager",
)

[BUG] Does not seem to work with on premise git enterprise

Describe the bug
When reviews tries to connect to the API of an on premise git instance it fails with the following stack trace:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/__main__.py", line 7, in <module>
    main()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/cli/main.py", line 40, in main
    cli()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/click/core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/click/core.py", line 1062, in main
    rv = self.invoke(ctx)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/click/core.py", line 1668, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/click/core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/cli/main.py", line 33, in dashboard
    render()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/tasks.py", line 120, in render
    body=_render_pull_requests(
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/tasks.py", line 38, in _render_pull_requests
    tables = [
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/tasks.py", line 39, in <listcomp>
    controller.retrieve_pull_requests(org=org, repository=repo)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/controller.py", line 21, in retrieve_pull_requests
    pull_requests = self.update_pull_requests(org=org, repository=repository)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/controller.py", line 48, in update_pull_requests
    pull_requests = self.client.get_pull_requests(org=org, repo=repository)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/source_control/client.py", line 35, in get_pull_requests
    return self._get_pull_requests(repository=repository)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/reviews/source_control/client.py", line 30, in _get_pull_requests
    return list(repository.get_pulls(state=state, sort=sort))
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/PaginatedList.py", line 56, in __iter__
    newElements = self._grow()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/PaginatedList.py", line 67, in _grow
    newElements = self._fetchNextPage()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/PaginatedList.py", line 199, in _fetchNextPage
    headers, data = self.__requester.requestJsonAndCheck(
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/Requester.py", line 354, in requestJsonAndCheck
    *self.requestJson(
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/Requester.py", line 454, in requestJson
    return self.__requestEncode(cnx, verb, url, parameters, headers, input, encode)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/Requester.py", line 519, in __requestEncode
    url = self.__makeAbsoluteUrl(url)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/github/Requester.py", line 591, in __makeAbsoluteUrl
    assert o.hostname in [
AssertionError: None

To Reproduce
Steps to reproduce the behaviour:
Hard to reproduce if you only use github.com, but this is the command I called:

GITHUB_URL=https://git.company.com/api/v3/ GITHUB_USER=username GITHUB_TOKEN=token REPOSITORY_CONFIGURATION=orga/repository python3 -m reviews dashboard

Expected behaviour
A clear and concise description of what you expected to happen.

Dashboard loads and displays repo

Desktop (please complete the following information):

  • OS: [e.g. iOS] macos
  • Python Version: 3.9

[Feature] Add flag to perform a single dashboard update - render cycle

Is your feature request related to a problem? Please describe.
By default the TUI runs continuously in a loop until cancelled by a user, however, I would like to run reviews dashboard with a --no-refresh to display the output in the terminal once.

Describe the solution you'd like
When I type `reviews dashboard --no-refresh the TUI loads as expected, and exits after completing the first render.

Additional context

the --no-refresh command will need to be configured here:

reviews/cli.py

Lines 19 to 21 in 9593f46

@main.command(help="Display a dashboard")
async def dashboard():
"""Dashboard command."""

see Click arguments for implementation: https://click.palletsprojects.com/en/7.x/arguments/

the loop should be modified to loop continuously if no-refresh is not provided.

while True:

[FEATURE] Names of the environment variable are somewhat generic

Is your feature request related to a problem? Please describe.
I think that the currently used env var names (PATH_TO_CONFIG, DEFAULT_PAGE_SIZE, DELAY_REFRESH, REPOSITORY_CONFIGURATION, LABEL_CONFIGURATION) are rather generic names. While I don't think it's likely that they would conflict with some other tool, I do think that this makes it hard to go through the list of environment variables you have set and know why you have set that environment variable.
It's less of a problem in Linux environments where you put those in a bash script that can have comments, but on Windows, you generally set them from this place and there's no place for comments:
image

Describe the solution you'd like
I think that the environment variable names should be prefixed with the name of the project, i.e. REVIEWS_PATH_TO_CONFIG. Perhaps some other prefix would work better here but the idea is to have one so that it's easily identifiable.

Reviews does not appear to find .env file in current directory?

Describe the bug

I have added a .env file to the top level of a local repository. If I run reviews from within that directory, it does not appear to read the .env file.

To Reproduce

$ cd ~/projects/kola
$ git rev-parse --show-toplevel
/home/lars/projects/kola
$ cat > .env <<EOF
REVIEWS_GITHUB_REPOSITORY_CONFIGURATION=larsks/kola
GITHUB_TOKEN=gho_...
GITHUB_USER=larsks
EOF
$ reviews config --show
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Name                                    ┃ Value                                                   ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ GITHUB_TOKEN                            │                                                         │
│ GITHUB_USER                             │                                                         │
│ GITHUB_URL                              │ https://api.github.com                                  │
│ GITLAB_TOKEN                            │                                                         │
│ GITLAB_USER                             │                                                         │
│ GITLAB_URL                              │ https://gitlab.com                                      │
│ REVIEWS_PATH_TO_CONFIG                  │ None                                                    │
│ GITHUB_DEFAULT_PAGE_SIZE                │ 100                                                     │
│ REVIEWS_DELAY_REFRESH                   │ 60                                                      │
│ REVIEWS_GITHUB_REPOSITORY_CONFIGURATION │ apoclyps/reviews                                        │
│ REVIEWS_GITLAB_REPOSITORY_CONFIGURATION │ 27629846:apoclyps/reviews                               │
│ REVIEWS_LABEL_CONFIGURATION             │ blocked/orange, docker/blue, security/red, python/green │
└─────────────────────────────────────────┴─────────────────────────────────────────────────────────┘

However, if I explicitly load the .env file (using e.g. dotenv) it works as expected:

$ dotenv run reviews config --show
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Name                                    ┃ Value                                                   ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ GITHUB_TOKEN                            │ gho_...                                                 │
│ GITHUB_USER                             │ larsks                                                  │
│ GITHUB_URL                              │ https://api.github.com                                  │
│ GITLAB_TOKEN                            │                                                         │
│ GITLAB_USER                             │                                                         │
│ GITLAB_URL                              │ https://gitlab.com                                      │
│ REVIEWS_PATH_TO_CONFIG                  │ None                                                    │
│ GITHUB_DEFAULT_PAGE_SIZE                │ 100                                                     │
│ REVIEWS_DELAY_REFRESH                   │ 60                                                      │
│ REVIEWS_GITHUB_REPOSITORY_CONFIGURATION │ larsks/kola                                             │
│ REVIEWS_GITLAB_REPOSITORY_CONFIGURATION │ 27629846:apoclyps/reviews                               │
│ REVIEWS_LABEL_CONFIGURATION             │ blocked/orange, docker/blue, security/red, python/green │
└─────────────────────────────────────────┴─────────────────────────────────────────────────────────┘

Expected behaviour

I expected reviews to read the .env file.

Desktop (please complete the following information):

  • OS: [e.g. iOS] Fedora (Linux)
  • Version [e.g. 22] 37

[FEATURE] Add support for inline diffs on pull requests

Is your feature request related to a problem? Please describe.
I would like to show the additions and deletions on a pull request to help gauge the size of the pull request.

Describe the solution you'd like
I would like to show additions and diffs in a similar way to Github by rendering additions in green and deletions in red e.g. +10 -5

Additional context
pull_request.addtions will return an interger for additions and pull_request.deletions will return an interger for deletions

refactor this function for usage with Gitlab

refactor this function for usage with Gitlab

# TODO: refactor this function for usage with Gitlab

            )

        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

2688e2421c4c24b0a1edc4470ef9a9b0b3f8c278

[Feature] Add type information to codebase

Is your feature request related to a problem? Please describe.
The codebase is current missing type information within the reviews module.

Describe the solution you'd like
Implement type information where possible for the codebase.

Additional context
Feel free to tackle this file by file, area of the code, submodule, or any other way that helps break up this larger piece of work into smaller more manageable chunks of code.

[Feature] Add command to recreate the SQLite database

Is your feature request related to a problem? Please describe.
Add a command to recreate the database

Describe the solution you'd like
when I run reviews init it should drop the existing database (if it exists) and create the database.

Additional context

You can find an example issue and pull request for adding a new command in #51 and #52

Behaviour
when ENABLE_PERSISTED_DATA is True, the database will be dropped (if it exists) and then created.

we should base the command on ENABLE_PERSISTED_DATA flag being set - if it's true, perform the drop and create the database, otherwise display a helpful error message "ENABLE_PERSISTED_DATA is set to false please enable it before attempting to recreate the database

reviews/cli.py

Lines 25 to 26 in da20303

if config.ENABLE_PERSISTED_DATA:
prepare_database()

existing recreate logic can be found in https://github.com/apoclyps/reviews/blob/main/create.py#L7-L15

[Feature] Implement a command to show configuration

Is your feature request related to a problem? Please describe.
Implement a command that shows configuration e.g. reviews configuration

Describe the solution you'd like
reviews configuration should display the REPOSITORY_CONFIGURATION and the LABEL_CONFIGURATION to show which repositories have been configured, and which label colours are valid colours.

Additional context
See #101 for implementation of LABEL_CONFIGURATION

You can find an example issue and pull request for adding a new command in #51 and #52

Cant get settings file to work

I've tried to follow the documentation for creating a settings file but it won't work. I added a settings.ini in the config folder but it seems like it's ignored. Can you clarify a bit more how it should be done?

Request for PAT requirements documentation

Is your feature request related to a problem? Please describe.

Currently it is difficult to know what the project requires in terms of PAT access. IE if it requires read vs read/write or which specific sections IE PRs / issues etc.

Describe the solution you'd like

Ideally the README would be updated with the required token requirements for the project.

Missing CONTRIBUTING.md file

There is a link to CONTRIBUTING.md in README.md, but this file is either missing or link is broken (but I was not able to find it anyway).

replace with pull_request.web_url

replace with pull_request.web_url

link = f"https://www.github.com/{org}/{repository}"

# TODO: replace with pull_request.web_url

    repository: str,
) -> Table:
    """Renders a list of pull requests as a table"""

    # if pull_requests:
    #     import pdb
    #     pdb.set_trace()
    #     link = pull_requests[0].web_url.split("/-/")[0]
    # else:
    #     link = ""

    link = ""
    show_diff = False

    # TODO: replace with pull_request.web_url
    # link = f"https://www.github.com/{org}/{repository}"

    table = Table(show_header=True, header_style="bold white")
    table.add_column("#", style="dim", width=5)
    table.add_column(
        f"[link={link}]{title}[/link]",
        width=75,
    )
    table.add_column("Labels", width=30)

58ecbaf9f1e101521b44bbefb0f6a5494ca35400

[Feature] Add a version command

Is your feature request related to a problem? Please describe.
I would like to be able to list the installed version of reviews by typing reviews --version to see the installed version.

Describe the solution you'd like
using the latest version of reviews, I would like to type reviews and see --version as an option for displaying the current version.

when executing reviews --version, It should display the currently installed version defined within reviews/version.py.

Additional context
Adding a new command should be included in https://github.com/apoclyps/reviews/blob/main/cli.py

[Feature] Add links to org/repository titles in Activity Screen

Is your feature request related to a problem? Please describe.
I would like to open a repository by selecting the organisation name/repository name in the heading under Activity.

image

Describe the solution you'd like
When I click on apoclyps/my-dev-space it should open https://github.com/apoclyps/my-dev-space" in a browser.

image

Additional context

The title is set here for each pull request

table.add_column(title, width=60)

an example of where the link has been used before can be seen at

tree.add(f"[link=https://www.github.com/{org}/{repo}]{repo}[/link]")

documentation on links is available at https://rich.readthedocs.io/en/latest/markup.html?highlight=link#links

[Feature] Enhance visibility of security tag pull request titles

Is your feature request related to a problem? Please describe.
I would like to highlight [Security] updates in yellow to identify them uniquely within a list of pull requests

Describe the solution you'd like
I would like the pull request title to highlight [Security] in yellow, whilst the rest of the text remains white.

Additional context

You can find a project with security updates required at apoclyps/home_automation

export REPOSITORY_CONFIGURATION="apoclyps/dexi,apoclyps/my-dev-space,apoclyps/home_automation"
reviews dashboard

This will produce results similar to the following

image

Implementation Notes

The colour is currently set to "[white]" by adding this string as a prefix to the pull request title in

for pr in pull_requests:
approved = "[green]Approved" if pr.approved else ""
updated_at = humanize.naturaltime(pr.updated_at)
colour = ""
if (datetime.now() - pr.updated_at).days >= 7:
colour = "[red]"
elif (datetime.now() - pr.updated_at).days >= 1:
colour = "[yellow]"
updated_at = f"{colour}{updated_at}"
labels = ", ".join([label.name for label in pr.labels])
table.add_row(
f"[white]{pr.number} ",
f"[white]{pr.title}",
f"{labels}",
f"{updated_at}",
f"{approved}",
)

The Rich library has support for applying additional formatting to a line of text.

See: https://github.com/willmcgugan/rich/blob/e34eadb3a9354c2469fa2112400ce2690f2663ba/rich/__main__.py#L103-L106 which shows the markup section from https://raw.githubusercontent.com/willmcgugan/rich/master/imgs/features.png

[BUG] No module named cli

Whenever I try to run it in gives the following traceback

Excepted behaviour:

Dashboard would open

Actual behaviour:

Traceback (most recent call last):
  File "c:\users\admin\appdata\local\programs\python\python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\users\admin\appdata\local\programs\python\python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python39\Scripts\reviews.exe\__main__.py", line 4, in <module>
ModuleNotFoundError: No module named 'cli'

[Feature] Provide configuration via config files

Is your feature request related to a problem? Please describe.
Users have requested the ability to set configuration using config files on their file system (instead of using env vars for secrets such as a GITHUB_TOKEN). These files could be encrypted at rest to ensure secrets are not readable outside of the application

Describe the solution you'd like
support for config files on windows, linux, mac to allow a github token to be provided via a config file - instead of an env var.

the order of preference should be env var (if set), otherwise, default to file system for configuration.

Additional context
I'm open to discussing how folks would like to implement this - feedback and suggestions welcome in the comments.

[Feature] Add support for Gitlab

I would like a feature to flip between or include gitlab reps.

Describe the solution you'd like
Either a flag that switches between github and gitlab repos or a way to include both in the same screen.
A clear and concise description of what you want to happen.

Am wiling to help out if you want. Do you think this might be an interesting feature? Could possibly extend to Bitbucket?

[FEATURE] Add support for corporate-hosted github

Currently the path to github is hardcoded.

Many corporations uses a hosted github-environment and it would be great of this tool would take that path as a config-parameter and use that if provided.

I got it to work using the following patch

git diff
diff --git a/reviews/source_control/client.py b/reviews/source_control/client.py
index ca94e5e..649d1a9 100644
--- a/reviews/source_control/client.py
+++ b/reviews/source_control/client.py
@@ -11,7 +11,7 @@ class GithubAPI:
     """Create and execute requests using the Github API"""

     def __init__(self) -> None:
-        self._client = Github(config.GITHUB_TOKEN, per_page=config.DEFAULT_PAGE_SIZE)
+        self._client = Github(config.GITHUB_TOKEN, per_page=config.DEFAULT_PAGE_SIZE, base_url="https://xxx-github.xxx.com/api/v3")

[Feature][Docs]: Setting up pre-commit for linting

My apologies, I just noticed that pre-commit is already setup in this project -- maybe the docs can be updated to show how to set it up such that linting can be performed automatically when trying to make a commit?

Also, noticed that the pre-commit config only has black and isort, will try to add the other linters in (mypy, flake8, etc)

Is your feature request related to a problem? Please describe.
Would be great if a pre-commit hook can be set up to take care of linting before commit is allowed

Describe the solution you'd like
pre-commit can be used (https://pre-commit.com/)

Have seen some open-source projects using it and I think it would be a nice feature to have.

Additional context
Reference for pylint's pre-commit config
https://github.com/PyCQA/pylint/blob/master/.pre-commit-config.yaml

[Refactor] extract pull request render logic into individual field based functions

Is your feature request related to a problem? Please describe.
Refactor the logic to render each of the fields for a pull request into testable methods on the pull request dataclass.

at present, testing and extending this logic is becoming complex and should be separated into functions for each field (number, title, labels, activity, approved, mergeable).

Describe the solution you'd like
Each function should be extracted from the loop within render_pull_request_table and converted into a function e.g.

# format and colourize pull request title
title_colour = ""
if pr.title.startswith("[Security]"):
pr.title = pr.title.removeprefix("[Security]")
title_colour = "[bold red][Security][/]"
if pr.draft:
title_colour = "[bold grey][Draft] [/]"

could be refactored into a render_title property on the PullRequest dataclass allowing

(
f"{title_colour}[white][link=https://www.github.com/"
f"{org}/{repository}/pull/{pr.number}]{pr.title}[/link][/]"
),
to be replaced with pr.render_title

Additional context
logic to refactor can be found within:

title_colour = ""
if pr.title.startswith("[Security]"):
pr.title = pr.title.removeprefix("[Security]")
title_colour = "[bold red][Security][/]"
if pr.draft:
title_colour = "[bold grey][Draft] [/]"
# format pull request last modified at datetime as a human-readable time
updated_at = humanize.naturaltime(pr.updated_at)
colour = ""
if (datetime.now() - pr.updated_at).days >= 7:
colour = "[red]"
elif (datetime.now() - pr.updated_at).days >= 1:
colour = "[yellow]"
updated_at = f"{colour}{updated_at}"
# formats the approval status (approved by me)
approved = ""
if pr.approved == "APPROVED":
approved = "[green]Approved[/]"
elif pr.approved == "CHANGES_REQUESTED":
approved = "[red]Changes Requested[/]"
# format the ready to release status (approved by others)
approved_by_others = "[green]Ready[/]" if pr.approved_by_others else ""
labels = ", ".join([_colourise_label(label.name) for label in pr.labels])
table.add_row(
f"[white]{pr.number} ",
(
f"{title_colour}[white][link=https://www.github.com/"
f"{org}/{repository}/pull/{pr.number}]{pr.title}[/link][/]"
),
f"{labels}",
f"{updated_at}",
f"{approved}",
f"{approved_by_others}",
)

pull request data class can be found within

class PullRequest:
"""Models a Pull Request"""
number: int
title: str
draft: bool
created_at: datetime
updated_at: datetime
approved: str
approved_by_others: bool
labels: List[Label] = field(default_factory=list)

[Feature] Display the users pull requests as "Mine" in approved

Is your feature request related to a problem? Please describe.
When the GITHUB_USER is set, I would like to compare it to the user on a pull request to set the approved field to Mine displayed in yellow - when the pull request is authored by the GITHUB_USER provided.

Describe the solution you'd like
Any pull requests authored by the GITHUB_USER should always display Mine in yellow.

Additionally, we should changed CHANGES REQUESTED to red.

Additional context

For testing purposes, you can set the GitHub user to apoclyps and use this repository to find a pull request by apoclyps.

The change will be scoped to this area of the code as the approved field is already a string:

approved = ""
if pr.approved == "APPROVED":
approved = "[green]Approved"
elif pr.approved == "CHANGES_REQUESTED":
approved = "[yellow]Changes Requested"

image

[Feature] Add flag to dashboard to hide/show authors pull requests

Is your feature request related to a problem? Please describe.
When reviewing pull requests, occasionally I only need to see the pull requests of others to review their code, or pull requests of my own to ship a pull request that is ready.

Describe the solution you'd like
Whilst watching repositories in which I have contributed pull requests, I would like to filter the results to hide all pull requests authored by github user or display only the pull requests authored by my gitub user.

Additional context

refactor this function for usage with Gitlab

refactor this function for usage with Gitlab

# TODO: refactor this function for usage with Gitlab

            )

        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: int, 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: int, namespace: str) -> List[PullRequest]:
        """Updates repository models."""

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

            return {reviewer["user"]["username"]: True 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"]
            )

            labels = [Label(name=label) for label in 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

d90dfb66d0d38412e442383d18ad51680077f10b

[BUG] Bad/missing credentials are not handled graciously

Describe the bug
When I try to open the dashboard without first setting proper credentials, I get an ugly traceback:

λ reviews dashboard
loading dashboard
Traceback (most recent call last):
  File "c:\python38\lib\runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\python38\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Python38\Scripts\reviews.exe\__main__.py", line 7, in <module>
  File "c:\python38\lib\site-packages\reviews\cli\main.py", line 54, in main
    cli()
  File "c:\python38\lib\site-packages\click\core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "c:\python38\lib\site-packages\click\core.py", line 1062, in main
    rv = self.invoke(ctx)
  File "c:\python38\lib\site-packages\click\core.py", line 1668, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "c:\python38\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\python38\lib\site-packages\click\core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "c:\python38\lib\site-packages\reviews\cli\main.py", line 47, in dashboard
    render()
  File "c:\python38\lib\site-packages\reviews\commands.py", line 121, in render
    body=_render_pull_requests(
  File "c:\python38\lib\site-packages\reviews\commands.py", line 39, in _render_pull_requests
    tables = [
  File "c:\python38\lib\site-packages\reviews\commands.py", line 40, in <listcomp>
    controller.retrieve_pull_requests(org=org, repository=repo)
  File "c:\python38\lib\site-packages\reviews\controller.py", line 24, in retrieve_pull_requests
    pull_requests = self.update_pull_requests(org=org, repository=repository)
  File "c:\python38\lib\site-packages\reviews\controller.py", line 57, in update_pull_requests
    pull_requests = self.client.get_pull_requests(org=org, repo=repository)
  File "c:\python38\lib\site-packages\reviews\source_control\client.py", line 39, in get_pull_requests
    repository = self.get_repository(org=org, repo=repo)
  File "c:\python38\lib\site-packages\reviews\source_control\client.py", line 25, in get_repository
    return self._client.get_repo(f"{org}/{repo}")
  File "c:\python38\lib\site-packages\github\MainClass.py", line 330, in get_repo
    headers, data = self.__requester.requestJsonAndCheck("GET", url)
  File "c:\python38\lib\site-packages\github\Requester.py", line 353, in requestJsonAndCheck
    return self.__check(
  File "c:\python38\lib\site-packages\github\Requester.py", line 378, in __check
    raise self.__createException(status, responseHeaders, output)
github.GithubException.BadCredentialsException: 401 {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}

To Reproduce
Steps to reproduce the behaviour:

  1. Install the latest (0.1.10) version of reviews
  2. Run reviews dashboard
  3. See error.

Expected behaviour
A more friendly error message.

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • OS: Windows
  • Version 2004 (OS Build 19041.985)

Additional context
N/A

[Feature] Add coloured labels for common labels

Is your feature request related to a problem? Please describe.
the label section would benefit from having some colors applied to identify common labels e.g. dependabot in blue, python in green, docker in purple. security in red.

Describe the solution you'd like
The Labels section should be updated to colorize common labels with colors.

Additional context
This is very similar to #53

The labels can be found here:

labels = ", ".join([label.name for label in pr.labels])

The Rich library has support for applying additional formatting to a line of text.

See: https://github.com/willmcgugan/rich/blob/e34eadb3a9354c2469fa2112400ce2690f2663ba/rich/__main__.py#L103-L106 which shows the markup section from https://raw.githubusercontent.com/willmcgugan/rich/master/imgs/features.png

refactor this function for usage with Gitlab

refactor this function for usage with Gitlab

# TODO: refactor this function for usage with Gitlab

            )

        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: int, 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: int, namespace: str
    ) -> List[PullRequest]:
        """Updates repository models."""

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

            return {
                reviewer["user"]["username"]: True 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

e0117a920fdf0e13cc081bf7c96bcea99ccac64a

[Feature] Add option for refresh on user input

Is your feature request related to a problem? Please describe.
As an alternative to updating every x seconds, I would like to perform an update and retrieve fresh results from Github upon user input.

Describe the solution you'd like
When a user enters a specific character such as r after the first render, the view should update and display fresh results. The option to refresh results is only available after the first render.

Additional context
Add any other context or screenshots about the feature request here.

[Spike] Investigate replacing raw SQL with SQLAlchemy

Is your feature request related to a problem? Please describe.
I would like to improve how the CLI makes use of persistence; currently using raw SQL for queries and tables is becoming difficult to manage. SQLAlchemy would be the ideal alternative as it can handle table creation, modification, and help simplify queries.

Describe the solution you'd like
I would like to replace the `data source module with SQLAlchemy:

Additional context
An example application using SQLAlchemy within a CLI: https://github.com/Lok3rs/Todo-CLI

Some additional thought will need to be made regarding how upgrades/downgrades will be applied and handled within the application. This may be as simple as resetting the database on every upgrade.

refactor this function to accept a link

refactor this function to accept a link

# TODO: refactor this function to accept a link

            )

        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

bc013d8b7deccf7c89cd5b3f62cfc10fe192e18f

- pass in link

  • pass in link

# TODO - pass in link

    def render_labels(self, colour_map: Dict[str, str]) -> str:
        """Renders the labels as a joined colourised string"""
        return ", ".join(
            [
                f"{colour_map.get(label.name.lower(), '[white]')}{label.name}[/]"
                for label in self.labels
            ]
        )

    def render_title(self, org: str, repository: str) -> str:
        """Renders the title as a colourised string"""
        colour = ""
        title = self.title

        # TODO - pass in link

        if title.startswith("[Security]"):
            title = title.removeprefix("[Security]")
            colour = "[bold red][Security][/]"

38d7a5c0ef29a658457bacfffebc5d606d656072

[BUG] Async isn't actually being used, and prevents ctrl+c from correctly exiting the app

When I first started trying reviews, ^C didn't work very well... I had to bash it several times to get the app to quit.

Looking a bit closer, at the code, render in tasks.py is actually a blocking function.

In cli/main.py all of the async code can be removed:

import asyncclick as click

from ..tasks import render, single_render
from ..version import __version__

CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])


@click.group(context_settings=CONTEXT_SETTINGS)
@click.version_option(__version__, "-v", "--version", message="version %(version)s")
def cli() -> None:
    """Reviews - A terminal UI Dashboard for monitoring code review requests.\n

    For feature requests or bug reports: https://github.com/apoclyps/reviews/issues
    """


@cli.command(help="Visualize code review requests as a Dashboard")
@click.option("-r", "--reload/--no-reload", default=True, is_flag=True)
def dashboard(reload: bool) -> None:
    """
    Command:\n
        reviews dashboard

    Usage:\n
        reviews dashboard --reload \n
        reviews dashboard --no-reload \n
    """

    click.echo("loading dashboard")

    if reload:
        render()
    else:
        single_render()


def main() -> None:
    """Entry point to CLI"""
    cli()

In my few manual tests this runs exactly the same as it did before - but now ^C will actually exit the program.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.