diff options
-rw-r--r-- | kodereviewer/app.py | 9 | ||||
-rw-r--r-- | kodereviewer/data.py | 51 | ||||
-rw-r--r-- | kodereviewer/models/__init__.py | 2 | ||||
-rw-r--r-- | kodereviewer/models/file.py | 95 | ||||
-rw-r--r-- | kodereviewer/models/line_model.py | 23 | ||||
-rw-r--r-- | kodereviewer/network_manager.py | 11 | ||||
-rw-r--r-- | kodereviewer/qml/Editor.qml | 3 | ||||
-rw-r--r-- | kodereviewer/qml/FilesView.qml | 29 | ||||
-rw-r--r-- | kodereviewer/qml/Main.qml | 2 | ||||
-rw-r--r-- | kodereviewer/qml/ProjectListPage.qml | 57 | ||||
-rw-r--r-- | kodereviewer/qml/PullRequestPage.qml | 52 |
11 files changed, 256 insertions, 78 deletions
diff --git a/kodereviewer/app.py b/kodereviewer/app.py index 94a4e55..ad019a4 100644 --- a/kodereviewer/app.py +++ b/kodereviewer/app.py @@ -9,8 +9,16 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtCore import QUrl, QByteArray from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType +from kodereviewer.models.file import FileModel from kodereviewer.network_manager import NetworkManager from kodereviewer.models import CommentModel, LabelModel, LineModel, PullRequestModel, ProjectModel +import logging +from rich.logging import RichHandler + +FORMAT = "%(message)s" +logging.basicConfig( + level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] +) def main(): @@ -35,6 +43,7 @@ def main(): qmlRegisterType(ProjectModel, "org.deprecated.kodereviewer", 1, 0, "ProjectModel") qmlRegisterType(CommentModel, "org.deprecated.kodereviewer", 1, 0, "CommentModel") + qmlRegisterType(FileModel, "org.deprecated.kodereviewer", 1, 0, "FileModel") qmlRegisterType(LabelModel, "org.deprecated.kodereviewer", 1, 0, "LabelModel") qmlRegisterType(LineModel, "org.deprecated.kodereviewer", 1, 0, "LineModel") qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel") diff --git a/kodereviewer/data.py b/kodereviewer/data.py index 82aad94..0d1bef6 100644 --- a/kodereviewer/data.py +++ b/kodereviewer/data.py @@ -49,6 +49,44 @@ class Comment(QObject): self.user = User(data['user']) +class ChangedFileStatus(Enum): + ADDED = 'added' + MODIFIED = 'modified' + DELETED = 'deleted' + + +class ChangedFile(QObject): + """ { + "sha": "bbcd538c8e72b8c175046e27cc8f907076331401", + "filename": "file1.txt", + "status": "added", + "additions": 103, + "deletions": 21, + "changes": 124, + "blob_url": "https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt", + "raw_url": "https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e", + "patch": "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test" + }""" + + sha: str + filename: str + status: ChangedFileStatus + additions: int + deletions: int + changes: int + patch: str + + def __init__(self, data: dict[str, Any]): + self.sha = data['sha'] + self.filename = data['filename'] + self.status = data['status'] + self.additions = int(data['additions']) + self.deletions = int(data['deletions']) + self.changes = int(data['changes']) + self.patch = data.get('patch', '') + + class State(Enum): OPEN = 'open' CLOSED = 'closed' @@ -71,10 +109,12 @@ class PullRequest(QObject): _labels: list[Label] _comments: list[Comment] + _files: list[ChangedFile] _initial_data: dict[str, Any] commentsLoaded = Signal() + filesLoaded = Signal() def __init__(self, data: dict[str, Any], *args, **kwargs): super().__init__(*args, **kwargs) @@ -96,6 +136,7 @@ class PullRequest(QObject): # At first this is empty until it's updated with a request self._comments = [] + self._files = [] self._initial_data = data @@ -145,12 +186,20 @@ class PullRequest(QObject): def labels(self) -> list[Label]: return self._labels + @Property(list, constant=True) + def files(self) -> list[ChangedFile]: + return self._files + def load_comments(self, response: QByteArray) -> None: - print("loading comments") data = json.loads(response.toStdString()) self._comments = [Comment(comment) for comment in data] self.commentsLoaded.emit() + def load_files(self, response: QByteArray) -> None: + data = json.loads(response.toStdString()) + self._files = [ChangedFile(file) for file in data] + self.filesLoaded.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 b566981..b9082d1 100644 --- a/kodereviewer/models/__init__.py +++ b/kodereviewer/models/__init__.py @@ -1,4 +1,5 @@ from kodereviewer.models.comments import CommentModel +from kodereviewer.models.file import FileModel from kodereviewer.models.line_model import LineModel from kodereviewer.models.project import ProjectModel from kodereviewer.models.pull_request import PullRequestModel @@ -6,6 +7,7 @@ from kodereviewer.models.label import LabelModel __all__ = [ 'CommentModel', + 'FileModel', 'LabelModel' 'LineModel', 'ProjectModel', diff --git a/kodereviewer/models/file.py b/kodereviewer/models/file.py new file mode 100644 index 0000000..246bd5b --- /dev/null +++ b/kodereviewer/models/file.py @@ -0,0 +1,95 @@ +from copy import copy +from enum import IntEnum, auto +from typing import Any, Optional + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project +from kodereviewer.data import ChangedFile, Label, PullRequest + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class FileModel(QAbstractListModel): + + _pull_request: Optional[PullRequest] + + class Roles(IntEnum): + Sha = Qt.ItemDataRole.UserRole + 1 + Filename = auto() + Status = auto() + Additions = auto() + Deletions = auto() + Changes = auto() + Patch = auto() + + def __init__(self): + super().__init__() + self._pull_request = None + + pullRequestChanged = Signal() + + def get_pull_request(self) -> Optional[PullRequest]: + return self._pull_request + + def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: + if pull_request is None: + return + + if self._pull_request is not None and self._pull_request == pull_request: + return + + self.beginResetModel() + self._pull_request = pull_request + self._pull_request.filesLoaded.connect(self._reset_model) + self.endResetModel() + self.pullRequestChanged.emit() + + pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, + notify=pullRequestChanged) + + def _reset_model(self) -> None: + self.beginResetModel() + self.endResetModel() + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + if self._pull_request is None: + return None + + file: ChangedFile = self._pull_request.files[index.row()] + + if role == self.Roles.Sha: + return file.sha + if role == self.Roles.Filename: + return file.filename + if role == self.Roles.Additions: + return file.additions + if role == self.Roles.Deletions: + return file.deletions + if role == self.Roles.Changes: + return file.changes + if role == self.Roles.Patch: + return file.patch + if role == Qt.ItemDataRole.DisplayRole: + return file.filename + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + if self._pull_request is None: + return 0 + return len(self._pull_request.files) + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.Roles.Sha: QByteArray(b'sha'), + self.Roles.Filename: QByteArray(b'filename'), + self.Roles.Additions: QByteArray(b'additions'), + self.Roles.Deletions: QByteArray(b'deletions'), + self.Roles.Changes: QByteArray(b'changes'), + self.Roles.Patch: QByteArray(b'patch'), + } diff --git a/kodereviewer/models/line_model.py b/kodereviewer/models/line_model.py index 1814829..03d14c6 100644 --- a/kodereviewer/models/line_model.py +++ b/kodereviewer/models/line_model.py @@ -1,7 +1,9 @@ """Model used for line numbers in Editor.qml""" +import logging from enum import auto, IntEnum from typing import Optional +import rich from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property from PySide6.QtQuick import QQuickTextDocument from PySide6.QtQml import QmlElement @@ -9,6 +11,8 @@ from PySide6.QtQml import QmlElement QML_IMPORT_NAME = "org.deprecated.kodereviewer" QML_IMPORT_MAJOR_VERSION = 1 +logger = logging.getLogger(__name__) + @QmlElement class LineModel(QAbstractListModel): @@ -27,15 +31,17 @@ class LineModel(QAbstractListModel): return self._document def set_document(self, document): - print('setting document') + logger.debug('setting document') if document == self._document: + logger.debug(f'Document {document} == {self._document}') return + logger.debug(f'Setting document {document}') self._document = document self.documentChanged.emit() self.resetModel() documentChanged = Signal() - document = Property(QQuickTextDocument, fget=get_document, fset=set_document, + document = Property(QObject, fget=get_document, fset=set_document, notify=documentChanged) def data(self, @@ -43,29 +49,28 @@ class LineModel(QAbstractListModel): role: int = Qt.ItemDataRole.DisplayRole) -> object: if not index.isValid(): - print('index not valid') + logger.debug('index not valid') return if self._document is None: - print('document none') + logger.debug('document none') return row = index.row() if row < 0 or row > self.rowCount(): - print(f'row: {row}') + logger.debug(f'row: {row}') return if role == self.Roles.LineHeight: text_doc = self._document.textDocument() return int(text_doc.documentLayout().blockBoundingRect(text_doc.findBlockByNumber(row)).height()) - print('found role ?') + logger.debug('found role ?') def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: if self._document is None: - print('document is none: row count 0') + logger.debug('rowCount: _document is None') return 0 - print(f'returning {self._document.textDocument().blockCount()}') return self._document.textDocument().blockCount(); def roleNames(self) -> dict[int, QByteArray]: @@ -76,6 +81,6 @@ class LineModel(QAbstractListModel): @Slot() def resetModel(self) -> None: - print('reseting model?') + logger.debug('reseting model?') self.beginResetModel() self.endResetModel() diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py index 62b6047..ebc2797 100644 --- a/kodereviewer/network_manager.py +++ b/kodereviewer/network_manager.py @@ -5,6 +5,7 @@ from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory from PySide6.QtQml import QmlElement +from kodereviewer.data import PullRequest from kodereviewer.project import Project QML_IMPORT_NAME = "org.deprecated.kodereviewer" @@ -12,6 +13,7 @@ QML_IMPORT_MAJOR_VERSION = 1 PULL_REQUEST_LIST_URL = re.compile(r'/pulls$') COMMENT_LIST_URL = re.compile(r'/issues/(\d+)/comments') +FILE_LIST_URL = re.compile(r'/pulls/(\d+)/files') @QmlElement @@ -69,6 +71,11 @@ class NetworkManager(QObject): pull_request = self._project.find_pull_request(pull_request_number) if pull_request is not None: pull_request.load_comments(response_body) + elif (match := FILE_LIST_URL.search(reply.url().toString())): + pull_request_number = int(match.groups()[0]) + pull_request: Optional[PullRequest] = self._project.find_pull_request(pull_request_number) + if pull_request is not None: + pull_request.load_files(response_body) else: print(f"Can't handle {reply.url()}") @@ -79,3 +86,7 @@ class NetworkManager(QObject): @Slot(int) def getPullRequestComments(self, number: int) -> None: self._manager.get(self._request_factory.createRequest(f'/issues/{number}/comments')) + + @Slot(int) + def getFiles(self, pull_request_number) -> None: + self._manager.get(self._request_factory.createRequest(f'/pulls/{pull_request_number}/files')) diff --git a/kodereviewer/qml/Editor.qml b/kodereviewer/qml/Editor.qml index 9cc6cbf..052933d 100644 --- a/kodereviewer/qml/Editor.qml +++ b/kodereviewer/qml/Editor.qml @@ -35,6 +35,7 @@ TextEdit { LineModel { id: lineModel document: root.textDocument + onDocumentChanged: print('Document changed!') } onWidthChanged: lineModel.resetModel() @@ -82,7 +83,7 @@ TextEdit { } } - onTextChanged: print(root.textDocument) + // onTextChanged: lineModel.document = root.textDocument onFileChanged: { repeater.model.resetModel() diff --git a/kodereviewer/qml/FilesView.qml b/kodereviewer/qml/FilesView.qml index 299c353..1442860 100644 --- a/kodereviewer/qml/FilesView.qml +++ b/kodereviewer/qml/FilesView.qml @@ -14,23 +14,7 @@ QQC2.SplitView { anchors.fill: parent padding: 0 spacing: 0 - ListModel { - id: fileModel - ListElement { - filename: "file1" - status: "added" - additions: 100 - deletions: 40 - patch: "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test" - } - ListElement { - filename: "file2" - status: "added" - additions: 100 - deletions: 40 - patch: "@@ -1,9 +1,9 @@ def something @@ -1000,7 +1000,7 @@ module Test" - } - } + required property FileModel fileModel QQC2.ScrollView { @@ -52,10 +36,11 @@ QQC2.SplitView { } } } - - Editor { - id: textArea - text: "" - file: "" + QQC2.ScrollView { + Editor { + id: textArea + text: "" + file: "" + } } } diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml index 827cf59..56248d7 100644 --- a/kodereviewer/qml/Main.qml +++ b/kodereviewer/qml/Main.qml @@ -15,7 +15,7 @@ Kirigami.ApplicationWindow { title: qsTr("Kode Reviewer") minimumWidth: Kirigami.Units.gridUnit * 20 - minimumHeight: Kirigami.Units.gridUnit * 20 + //minimumHeight: Kirigami.Units.gridUnit * 20 width: minimumWidth height: minimumHeight diff --git a/kodereviewer/qml/ProjectListPage.qml b/kodereviewer/qml/ProjectListPage.qml index 2846db1..f156dc1 100644 --- a/kodereviewer/qml/ProjectListPage.qml +++ b/kodereviewer/qml/ProjectListPage.qml @@ -9,7 +9,7 @@ import org.kde.kirigami as Kirigami import org.deprecated.kodereviewer -Kirigami.Page { +Kirigami.ScrollablePage { id: root required property NetworkManager connection @@ -56,43 +56,32 @@ Kirigami.Page { } ] - contentItem: QQC2.StackView { - id: stackView - anchors.fill: parent - - initialItem: pullRequestListView - - Component { - id: pullRequestListView - QQC2.ScrollView { - ListView { - id: view - 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" - color: draft ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.positiveTextColor - } - - onClicked: { - view.currentIndex = index - root.pullRequestSelected(number) - } - } - } + ListView { + id: view + 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" + color: draft ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.positiveTextColor + } + + onClicked: { + view.currentIndex = index + root.pullRequestSelected(number) } } } + MouseArea { anchors.top: parent.top anchors.bottom: parent.bottom diff --git a/kodereviewer/qml/PullRequestPage.qml b/kodereviewer/qml/PullRequestPage.qml index e99d81b..219762a 100644 --- a/kodereviewer/qml/PullRequestPage.qml +++ b/kodereviewer/qml/PullRequestPage.qml @@ -56,11 +56,26 @@ Kirigami.ScrollablePage { } } - CommentModel { - id: commentModel - pullRequest: root.pullRequest + Loader { + id: commentModelLoader + active: !!root.pullRequest + sourceComponent: CommentModel { + id: commentModel + pullRequest: root.pullRequest + + onPullRequestChanged: root.connection.getPullRequestComments(root.pullRequest.number) + } + } - onPullRequestChanged: root.connection.getPullRequestComments(root.pullRequest.number) + Loader { + id: fileModelLoader + active: !!root.pullRequest + sourceComponent: FileModel { + id: fileModel + pullRequest: root.pullRequest + + onPullRequestChanged: root.connection.getFiles(root.pullRequest.number) + } } Kirigami.PlaceholderMessage { @@ -91,11 +106,21 @@ Kirigami.ScrollablePage { RowLayout { QQC2.Label { + text: "Author" + elide: Text.ElideRight + } + QQC2.Label { + text: root.pullRequest ? root.pullRequest.username : "" + elide: Text.ElideLeft + } + } + RowLayout { + QQC2.Label { text: "State: " elide: Text.ElideRight } QQC2.Label { - text: root.pullRequest.state + text: root.pullRequest ? root.pullRequest.state : "" elide: Text.ElideLeft } } @@ -105,16 +130,22 @@ Kirigami.ScrollablePage { elide: Text.ElideRight } QQC2.Label { - text: root.pullRequest.draft ? i18n("Yes") : i18n("No") + text: root.pullRequest ? root.pullRequest.draft ? i18n("Yes") : i18n("No") : "" elide: Text.ElideLeft } } + Loader { + id: labelModelLoader + active: !!root.pullRequest + sourceComponent: LabelModel { + pullRequest: root.pullRequest + } + } + RowLayout { Repeater { - model: LabelModel { - pullRequest: root.pullRequest - } + model: labelModelLoader.item delegate: Rectangle { required property string name required property string labelColor @@ -150,13 +181,14 @@ Kirigami.ScrollablePage { ColumnLayout { visible: !!root.pullRequest && root.currentView == "comments" Repeater { - model: commentModel + model: commentModelLoader.item delegate: CommentDelegate {} } } FilesView { visible: !!root.pullRequest && root.currentView == "files" + fileModel: fileModelLoader.item } footer: Kirigami.NavigationTabBar { |