diff options
-rw-r--r-- | README.md | 19 | ||||
-rw-r--r-- | justfile | 5 | ||||
-rw-r--r-- | kodereviewer/app.py | 4 | ||||
-rw-r--r-- | kodereviewer/data.py | 27 | ||||
-rw-r--r-- | kodereviewer/models/__init__.py | 4 | ||||
-rw-r--r-- | kodereviewer/models/comments.py | 1 | ||||
-rw-r--r-- | kodereviewer/models/pull_request.py | 25 | ||||
-rw-r--r-- | kodereviewer/project.py | 6 | ||||
-rw-r--r-- | kodereviewer/qml/Main.qml | 6 | ||||
-rw-r--r-- | kodereviewer/qml/ProjectListPage.qml | 21 | ||||
-rw-r--r-- | kodereviewer/qml/PullRequestPage.qml | 104 | ||||
-rw-r--r-- | pyproject.toml | 1 |
12 files changed, 190 insertions, 33 deletions
@@ -1,3 +1,22 @@ # Kodereviewer A code review tool + +# Flatpak + +## flatpak-pip-generator +This tool is used to build the pip dependencies manifests for flatpak + + pip install requirements-parser + wget https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/pip/flatpak-pip-generator --directory-prefix venv/bin + chmod +x ./venv/bin/flatpak-pip-generator + +To generate the manifest: + + flatpak-pip-generator pyside6 hatch + +# gammaray + +[Gammaray](https://www.kdab.com/gammaray/) helps to view all the state in Qt/QML. To attach it to the project need to activate a flag for `ptrace` + +echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope @@ -1,9 +1,12 @@ run: python -mkodereviewer -build: +build-python: python -mbuild +build-flatpak: + flatpak-builder --verbose --force-clean flatpak-build-dir org.deprecated.kodereviewer.yml + qmltypes: # If this fails, comment __main__.py code pyside6-qml-stubgen kodereviewer/ --out-dir ./qmltypes/ diff --git a/kodereviewer/app.py b/kodereviewer/app.py index 430b5cf..94a4e55 100644 --- a/kodereviewer/app.py +++ b/kodereviewer/app.py @@ -10,7 +10,7 @@ from PySide6.QtCore import QUrl, QByteArray from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType from kodereviewer.network_manager import NetworkManager -from kodereviewer.models import CommentModel, PullRequestModel, ProjectModel +from kodereviewer.models import CommentModel, LabelModel, LineModel, PullRequestModel, ProjectModel def main(): @@ -35,6 +35,8 @@ def main(): qmlRegisterType(ProjectModel, "org.deprecated.kodereviewer", 1, 0, "ProjectModel") qmlRegisterType(CommentModel, "org.deprecated.kodereviewer", 1, 0, "CommentModel") + qmlRegisterType(LabelModel, "org.deprecated.kodereviewer", 1, 0, "LabelModel") + qmlRegisterType(LineModel, "org.deprecated.kodereviewer", 1, 0, "LineModel") qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel") qmlRegisterType(NetworkManager, "org.deprecated.kodereviewer", 1, 0, "NetworkManager") diff --git a/kodereviewer/data.py b/kodereviewer/data.py index eb86097..82aad94 100644 --- a/kodereviewer/data.py +++ b/kodereviewer/data.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import datetime from enum import Enum import json @@ -63,6 +64,7 @@ class PullRequest(QObject): _body: str | None _created_at: datetime _updated_at: datetime + _draft: bool _user: User _assignee: User | None @@ -70,10 +72,12 @@ class PullRequest(QObject): _comments: list[Comment] + _initial_data: dict[str, Any] + commentsLoaded = Signal() - def __init__(self, data: dict[str, Any]): - super().__init__() + def __init__(self, data: dict[str, Any], *args, **kwargs): + super().__init__(*args, **kwargs) self._number = data['number'] self._state = data['state'] self._title = data['title'] @@ -81,6 +85,7 @@ class PullRequest(QObject): self._body = data['body'] self._created_at = datetime.fromisoformat(data['created_at']) self._updated_at = datetime.fromisoformat(data['updated_at']) + self._draft = data['draft'] self._user = User(data['user']) self._assignee = None @@ -92,6 +97,8 @@ class PullRequest(QObject): # At first this is empty until it's updated with a request self._comments = [] + self._initial_data = data + def __eq__(self, other: object) -> bool: if isinstance(other, PullRequest): return self._number == other._number @@ -110,7 +117,7 @@ class PullRequest(QObject): @Property(str, constant=True) def state(self) -> str: - return self._state.value + return self._state @Property(str, constant=True) def url(self) -> str: @@ -126,12 +133,24 @@ class PullRequest(QObject): def created_at(self) -> datetime: return self._created_at + @Property(bool, constant=True) + def draft(self) -> bool: + return self._draft + @Property(str, constant=True) def username(self) -> str: return self._user.username + @Property(list, constant=True) + def labels(self) -> list[Label]: + return self._labels + def load_comments(self, response: QByteArray) -> None: + print("loading comments") data = json.loads(response.toStdString()) self._comments = [Comment(comment) for comment in data] - print(f'emiting new comments {len(self._comments)}') self.commentsLoaded.emit() + + def copy(self): + """Create a copy of a PullRequest with it's initial parameters.""" + return PullRequest(deepcopy(self._initial_data)) diff --git a/kodereviewer/models/__init__.py b/kodereviewer/models/__init__.py index 853b618..b566981 100644 --- a/kodereviewer/models/__init__.py +++ b/kodereviewer/models/__init__.py @@ -1,9 +1,13 @@ from kodereviewer.models.comments import CommentModel +from kodereviewer.models.line_model import LineModel from kodereviewer.models.project import ProjectModel from kodereviewer.models.pull_request import PullRequestModel +from kodereviewer.models.label import LabelModel __all__ = [ 'CommentModel', + 'LabelModel' + 'LineModel', 'ProjectModel', 'PullRequestModel', ] diff --git a/kodereviewer/models/comments.py b/kodereviewer/models/comments.py index 06de3d7..0d92e23 100644 --- a/kodereviewer/models/comments.py +++ b/kodereviewer/models/comments.py @@ -37,7 +37,6 @@ class CommentModel(QAbstractListModel): if self._pull_request is not None and self._pull_request == pull_request: return - print(f"Setting up pull request to {pull_request}") self.beginResetModel() self._pull_request = pull_request self._pull_request.commentsLoaded.connect(self._reset_model) diff --git a/kodereviewer/models/pull_request.py b/kodereviewer/models/pull_request.py index 7d08efa..99e3495 100644 --- a/kodereviewer/models/pull_request.py +++ b/kodereviewer/models/pull_request.py @@ -1,5 +1,4 @@ -import json -from os import path +from copy import copy from typing import Any, Optional from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property @@ -15,9 +14,17 @@ QML_IMPORT_MAJOR_VERSION = 1 class PullRequestModel(QAbstractListModel): _project: Optional[Project] + _pull_requests: list[PullRequest] + + # This attribute holds a reference to a reference to a newly + # copied PullRequest so Qt don't frees the memory of the pull + # request on _pull_requests + _current_pull_request: PullRequest NumberRole = Qt.ItemDataRole.UserRole + 1 TitleRole = NumberRole + 1 + StateRole = TitleRole + 1 + DraftRole = StateRole + 1 def __init__(self): super().__init__() @@ -33,6 +40,10 @@ class PullRequestModel(QAbstractListModel): return pull_request.number if role == self.TitleRole: return pull_request.title + if role == self.StateRole: + return pull_request.state + if role == self.DraftRole: + return pull_request.draft if role == Qt.ItemDataRole.DisplayRole: return f'{pull_request.number} - {pull_request.title}' return None @@ -46,6 +57,8 @@ class PullRequestModel(QAbstractListModel): return { self.NumberRole: QByteArray(b"number"), self.TitleRole: QByteArray(b"title"), + self.StateRole: QByteArray(b"state"), + self.DraftRole: QByteArray(b"draft"), } def get_project(self) -> Optional[Project]: @@ -64,12 +77,14 @@ class PullRequestModel(QAbstractListModel): @Slot(int, result=PullRequest) def get(self, index: int) -> Optional[PullRequest]: - if self._project is not None: - return self._project.pullRequests[index] - return None + self._current_pull_request = self._pull_requests[index].copy() + + return self._current_pull_request def _reset_model(self) -> None: print("Reseting pull request model") self.beginResetModel() + if self._project is not None: + self._pull_requests = self._project.pullRequests self.endResetModel() diff --git a/kodereviewer/project.py b/kodereviewer/project.py index d96d47b..e880352 100644 --- a/kodereviewer/project.py +++ b/kodereviewer/project.py @@ -43,10 +43,14 @@ class Project(QObject): def pullRequests(self) -> list[PullRequest]: return self._pull_requests + @Slot(int, result=PullRequest) + def pullRequest(self, number: int) -> Optional[PullRequest]: + return self.find_pull_request(number) + def load_pull_requests(self, response: QByteArray) -> None: data = json.loads(response.toStdString()) self._pull_requests = [ - PullRequest(pr) for pr in data + PullRequest(pr, parent=self) for pr in data ] self.pullRequestsChanged.emit() diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml index e7496a6..827cf59 100644 --- a/kodereviewer/qml/Main.qml +++ b/kodereviewer/qml/Main.qml @@ -54,10 +54,8 @@ Kirigami.ApplicationWindow { connection: root.connection project: root.project - onPullRequestSelected: pr => { - print(pr) - pullRequestPageLoader.item.pullRequest = pr - + onPullRequestSelected: number => { + pullRequestPageLoader.item.pullRequest = project.pullRequest(number) } } } diff --git a/kodereviewer/qml/ProjectListPage.qml b/kodereviewer/qml/ProjectListPage.qml index 7ffa8b9..2846db1 100644 --- a/kodereviewer/qml/ProjectListPage.qml +++ b/kodereviewer/qml/ProjectListPage.qml @@ -14,7 +14,7 @@ Kirigami.Page { required property NetworkManager connection required property Project project - signal pullRequestSelected(var pullRequest) + signal pullRequestSelected(int pullRequest) readonly property int currentWidth: _private.currentWidth + 1 @@ -67,18 +67,26 @@ Kirigami.Page { QQC2.ScrollView { ListView { id: view - model: pullRequestFilterModel + model: pullRequestModel clip: true delegate: Delegates.RoundedItemDelegate { required property int number required property string title + required property bool draft required property int index + highlighted: ListView.isCurrentItem text: `${number} - ${title}` - icon.name: "vcs-merge-request" - - onClicked: root.pullRequestSelected(pullRequestModel.get(index)) + icon { + name: "vcs-merge-request" + color: draft ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.positiveTextColor + } + + onClicked: { + view.currentIndex = index + root.pullRequestSelected(number) + } } } } @@ -108,7 +116,7 @@ Kirigami.Page { } if (mouse.x > _lastX) { // _private.currentWidth = _private.currentWidth + (_lastX + mouse.x); - _private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX)) + _private.currentWidth = Math.min(_private.maxWidth, _private.currentWidth + (mouse.x - _lastX)) } else if (mouse.x < _lastX) { const tmpWidth = _private.currentWidth - (_lastX - mouse.x); if (tmpWidth > _private.minWidth) @@ -127,6 +135,7 @@ Kirigami.Page { property int currentWidth: defaultWidth readonly property int defaultWidth: Kirigami.Units.gridUnit * 17 readonly property int minWidth: Kirigami.Units.gridUnit * 2 + readonly property int maxWidth: Kirigami.Units.gridUnit * 25 } } diff --git a/kodereviewer/qml/PullRequestPage.qml b/kodereviewer/qml/PullRequestPage.qml index 5a687e9..e99d81b 100644 --- a/kodereviewer/qml/PullRequestPage.qml +++ b/kodereviewer/qml/PullRequestPage.qml @@ -20,6 +20,41 @@ Kirigami.ScrollablePage { property string currentView: "info" + actions: [ + Kirigami.Action { + id: reviewChangesAction + text: "Review changes" + icon.name: "preview-symbolic" + enabled: !!root.pullRequest + onTriggered: reviewChangesDialog.open() + } + ] + + Kirigami.Dialog { + id: reviewChangesDialog + standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + title: i18nc("@title:window", "Review changes") + padding: Kirigami.Units.largeSpacing + preferredWidth: Kirigami.Units.gridUnit * 20 + ColumnLayout { + MarkdownTextArea { + Layout.fillWidth: true + } + Kirigami.Separator { + Kirigami.FormData.isSection: true + Layout.fillWidth: true + } + QQC2.RadioButton { + text: "Approve" + } + QQC2.RadioButton { + text: "Comment" + } + QQC2.RadioButton { + text: "Request changes" + } + } + } CommentModel { id: commentModel @@ -28,7 +63,6 @@ Kirigami.ScrollablePage { onPullRequestChanged: root.connection.getPullRequestComments(root.pullRequest.number) } - Kirigami.PlaceholderMessage { visible: !root.pullRequest anchors.centerIn: parent @@ -36,11 +70,10 @@ Kirigami.ScrollablePage { text: "Select a pull request" } - ColumnLayout { - id: mainLayout + Kirigami.FormLayout { + id: descriptionLayout visible: !!root.pullRequest && root.currentView == "info" - - spacing: Kirigami.Units.largeSpacing * 2 + anchors.fill: parent Kirigami.Heading { Layout.fillWidth: true @@ -52,9 +85,57 @@ Kirigami.ScrollablePage { wrapMode: Text.WordWrap } - Kirigami.ListSectionHeader { - Layout.fillWidth: true - text: "description" + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + RowLayout { + QQC2.Label { + text: "State: " + elide: Text.ElideRight + } + QQC2.Label { + text: root.pullRequest.state + elide: Text.ElideLeft + } + } + RowLayout { + QQC2.Label { + text: "Draft?: " + elide: Text.ElideRight + } + QQC2.Label { + text: root.pullRequest.draft ? i18n("Yes") : i18n("No") + elide: Text.ElideLeft + } + } + + RowLayout { + Repeater { + model: LabelModel { + pullRequest: root.pullRequest + } + delegate: Rectangle { + required property string name + required property string labelColor + required property string textColor + color: labelColor + width: thelabel.implicitWidth + height: thelabel.implicitHeight + radius: 5 + QQC2.Label { + id: thelabel + padding: Kirigami.Units.smallSpacing + text: name + color: textColor + } + } + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Description" } MarkdownLabel { @@ -62,7 +143,8 @@ Kirigami.ScrollablePage { Layout.fillHeight: false leftPadding: Kirigami.Units.largeSpacing rightPadding: Kirigami.Units.largeSpacing - text: root.pullRequest ? root.pullRequest.body : "" + text: root.pullRequest ? + (root.pullRequest.body != "" ? root.pullRequest.body : "*No description provided.*") : "" } } ColumnLayout { @@ -73,6 +155,10 @@ Kirigami.ScrollablePage { } } + FilesView { + visible: !!root.pullRequest && root.currentView == "files" + } + footer: Kirigami.NavigationTabBar { actions: [ Kirigami.Action { diff --git a/pyproject.toml b/pyproject.toml index 11941f5..9f8a0d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ readme = "README.md" dependencies = [ "PySide6", - "markdown" ] [options] |