aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatias Linares <matias.linares@comprandoengrupo.net>2025-02-25 17:05:51 -0300
committerMatias Linares <matias.linares@comprandoengrupo.net>2025-02-27 11:57:57 -0300
commit265a1db778542623e42f1b35857579c25160df88 (patch)
tree8696da64c52d8b15b21c3a442b9fc8c4747134aa
parentc5f854946a40b1ec278dfa999b3aaba9109dfc42 (diff)
downloadkodereviewer-265a1db778542623e42f1b35857579c25160df88.tar.gz
Add reviewer dialog and begin new diff views
-rw-r--r--.gitignore5
-rw-r--r--kodereviewer/diff_parser.py146
-rw-r--r--kodereviewer/models/diff_file.py87
-rw-r--r--kodereviewer/models/file.py1
-rw-r--r--kodereviewer/network_manager.py39
-rw-r--r--kodereviewer/qml/FilesChangedPage.qml21
-rw-r--r--kodereviewer/qml/FilesDrawer.qml1
-rw-r--r--kodereviewer/qml/PullRequestDescription.qml2
-rw-r--r--kodereviewer/qml/ReviewWindow.qml76
-rw-r--r--pyproject.toml3
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/diff_parser_tests.py22
12 files changed, 395 insertions, 8 deletions
diff --git a/.gitignore b/.gitignore
index 597fc20..5a67436 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.json
qmltypes
-tests/
+mytests/
+tmp/
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -163,4 +164,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/ \ No newline at end of file
+#.idea/
diff --git a/kodereviewer/diff_parser.py b/kodereviewer/diff_parser.py
new file mode 100644
index 0000000..378c795
--- /dev/null
+++ b/kodereviewer/diff_parser.py
@@ -0,0 +1,146 @@
+import logging
+import re
+from collections.abc import Sequence
+
+from rich.logging import RichHandler
+
+HEADER_RE = re.compile(r'@@ -(?P<original_start_line>\d+),(?P<original_line_count>\d+)'
+ r' \+(?P<new_start_line>\d+),(?P<new_line_count>\d+)'
+ r' @@ ?(?P<context>.*)')
+
+logging.basicConfig(
+ level="INFO", format="%(message)s", datefmt="[%X]", handlers=[RichHandler()]
+)
+logger = logging.getLogger(__name__)
+
+
+class SideBySideDiffLine:
+ line: int
+ original: str
+ new: str
+
+ def __init__(self, line: int, original: str, new: str):
+ self.line = line
+ self.original = original
+ self.new = new
+
+
+class Hunk:
+ header: str
+ lines: list[str]
+
+ original_start_line: int
+ original_line_count: int
+ new_start_line: int
+ new_line_count: int
+ context: str
+
+ original: list[str]
+ new: list[str]
+
+ def __init__(self, hunk_str: str):
+ header, *body = hunk_str.splitlines()
+ self.header = header
+ self.lines = body
+
+ self.parse_header(header)
+ self.parse_body(body)
+
+ def parse_header(self, header: str) -> None:
+ """Parses a header.
+ The format is: @@ -l,s +l,s @@ optional section heading
+ where
+ l: starting line
+ s: number of lines
+
+ example: @@ -13,10 +13,15 @@ on:
+ """
+ match = HEADER_RE.match(header)
+ if match:
+ self.original_start_line = int(match.group('original_start_line'))
+ self.original_line_count = int(match.group('original_line_count'))
+ self.new_start_line = int(match.group('new_start_line'))
+ self.new_line_count = int(match.group('new_line_count'))
+ self.context = match.group('context')
+ else:
+ raise ValueError(f'Error parsing {header}')
+
+ def parse_body(self, lines: list[str]) -> None:
+ self.original = []
+ self.new = []
+
+ for line in lines:
+ if line.startswith('-'):
+ self.original.append(line)
+ elif line.startswith('+'):
+ self.new.append(line)
+ else:
+ self.original.append(line)
+ self.new.append(line)
+
+ def side_by_side(self) -> Sequence[SideBySideDiffLine]:
+ start = min(self.original_start_line, self.new_start_line)
+ end = max(self.original_start_line + self.original_line_count, self.new_start_line, self.new_line_count)
+ retval: list[SideBySideDiffLine] = []
+ original_line_counter = 0
+ new_line_counter = 0
+
+ for line in range(start, end):
+ original = ''
+ new = ''
+ if line in range(self.original_start_line, self.original_start_line + self.original_line_count):
+ original = self.original[original_line_counter]
+ original_line_counter += 1
+
+ if line in range(self.new_start_line, self.new_start_line + self.new_line_count):
+ new = self.new[new_line_counter]
+ new_line_counter += 1
+
+ retval.append(SideBySideDiffLine(line, original, new))
+
+ return retval
+
+
+class GithubDiffParser:
+ diff: str
+
+ def __init__(self, diff: str):
+ self.diff = diff
+
+ def hunks(self) -> list[Hunk]:
+ retval: list[Hunk] = []
+
+ hunk_str = ''
+ for line in self.diff.splitlines():
+ if line.startswith('@@'):
+ logger.info('adding %s', line)
+ if hunk_str != '':
+ retval.append(Hunk(hunk_str))
+ hunk_str = ''
+ hunk_str += line
+ hunk_str += '\n'
+ if hunk_str != '':
+ retval.append(Hunk(hunk_str))
+ return retval
+
+if __name__ == '__main__':
+ from rich.console import Console
+ from rich.table import Table
+
+ import sys
+ filename = sys.argv[-1]
+
+ with open(filename) as fp:
+ parser = GithubDiffParser(fp.read())
+ for hunk in parser.hunks():
+
+ table = Table(title=hunk.context)
+ table.add_column('line')
+ table.add_column('original')
+ table.add_column('new')
+
+ for diff_line in hunk.side_by_side():
+ table.add_row(str(diff_line.line), diff_line.original, diff_line.new)
+
+ console = Console()
+ console.print(table)
diff --git a/kodereviewer/models/diff_file.py b/kodereviewer/models/diff_file.py
new file mode 100644
index 0000000..c84eeca
--- /dev/null
+++ b/kodereviewer/models/diff_file.py
@@ -0,0 +1,87 @@
+"""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
+
+from kodereviewer.diff_parser import Diff, DiffBlock
+
+QML_IMPORT_NAME = "org.deprecated.kodereviewer"
+QML_IMPORT_MAJOR_VERSION = 1
+
+logger = logging.getLogger(__name__)
+
+
+@QmlElement
+class DiffFileModel(QAbstractListModel):
+
+ _diff_str: str
+ _diff: Diff
+ blocks: list[DiffBlock] = []
+
+ class Roles(IntEnum):
+ Content = Qt.ItemDataRole.UserRole + 1
+ LineCount = auto()
+
+ def __init__(self, *args, **kwargs):
+ self._diff_str = ''
+ super().__init__(*args, **kwargs)
+
+
+ def get_diff(self):
+ return self._diff_str
+
+ def set_diff(self, diff):
+ if diff == self._diff_str:
+ return
+ self._diff_str = diff
+ self._diff = Diff(diff)
+ self.diffChanged.emit()
+ self.resetModel()
+
+ diffChanged = Signal()
+ diff = Property(QObject, fget=get_diff, fset=set_diff,
+ notify=diffChanged)
+
+ def data(self,
+ index: QModelIndex | QPersistentModelIndex,
+ role: int = Qt.ItemDataRole.DisplayRole) -> object:
+
+ if not index.isValid():
+ logger.debug('index not valid')
+ return
+
+ if self._diff_str is None:
+ return
+
+ row = index.row()
+ if row < 0 or row > self.rowCount():
+ logger.debug(f'row: {row}')
+ return
+
+ block = self.blocks[row]
+
+ if role == self.Roles.Content:
+ return block.content
+ if role == self.Roles.LineCount:
+ return len(block.content.split("\n"))
+
+ def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
+ return len(self.blocks)
+
+ def roleNames(self) -> dict[int, QByteArray]:
+ return {
+ self.Roles.Content: QByteArray(b'content'),
+ }
+
+
+ @Slot()
+ def resetModel(self) -> None:
+ logger.debug('reseting model?')
+ self.beginResetModel()
+ self.endResetModel()
diff --git a/kodereviewer/models/file.py b/kodereviewer/models/file.py
index c8e4da6..cc3f8be 100644
--- a/kodereviewer/models/file.py
+++ b/kodereviewer/models/file.py
@@ -27,6 +27,7 @@ class FileItem:
name: str
parent: Self | None
patch: str | None = None
+ sha: str | None = None
children: list[Self] = field(default_factory=list)
def append(self, child: Self):
diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py
index cbe5160..faee87e 100644
--- a/kodereviewer/network_manager.py
+++ b/kodereviewer/network_manager.py
@@ -51,7 +51,6 @@ class NetworkManager(QObject):
headers.append(QHttpHeaders.WellKnownHeader.Authorization,
f'Bearer {github_token}')
headers.append(QHttpHeaders.WellKnownHeader.UserAgent, "kodereviewer")
- print(f'Authorization: Bearer {github_token}')
self._request_factory.setCommonHeaders(headers)
self._manager.finished.connect(self.reply_finished)
@@ -66,6 +65,7 @@ class NetworkManager(QObject):
self.projectChanged.emit()
base_url = f'https://api.github.com/repos/{self._project.owner}/{self._project.name}'
+ logger.info(f'got base url: "{base_url}"')
self._request_factory.setBaseUrl(base_url)
project = Property(Project, fget=project, fset=set_project)
@@ -102,7 +102,13 @@ class NetworkManager(QObject):
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_reviews(response_body)
+
+ if reply.operation() == QNetworkAccessManager.Operation.GetOperation:
+ pull_request.load_reviews(response_body)
+ elif reply.operation() == QNetworkAccessManager.Operation.PostOperation:
+ logger.info(response_body)
+ else:
+ logger.info(f'Unknown operation: {reply.operation()}')
else:
logger.info(f"Can't handle {reply.url()}")
@@ -134,6 +140,8 @@ class NetworkManager(QObject):
)
)
+ # Reviews
+
@Slot(int)
def getPullRequestReviews(self, pull_request_number: int) -> None:
self._manager.get(
@@ -157,3 +165,30 @@ class NetworkManager(QObject):
'comments': []
}
self._manager.post(request, json.dumps(data).encode())
+
+ @Slot(int, str, str, str, int, int)
+ def createReviewComment(
+ self,
+ pull_request_number: int,
+ body: str,
+ path: str,
+ commit_id: str,
+ line: int,
+ start_line: int,
+ ) -> None:
+ request = self._request_factory.createRequest(
+ f'/pulls/{pull_request_number}/comments'
+ )
+ data = {
+ 'body': body,
+ 'commit_id': commit_id,
+ 'path': path,
+ 'start_line': start_line,
+ 'start_side': 'RIGHT',
+ 'line': line,
+ 'side': 'RIGHT'
+
+ }
+ logger.info('Creating review comment')
+ logger.info(data)
+ self._manager.post(request, json.dumps(data).encode())
diff --git a/kodereviewer/qml/FilesChangedPage.qml b/kodereviewer/qml/FilesChangedPage.qml
index fe2aaf6..25c1164 100644
--- a/kodereviewer/qml/FilesChangedPage.qml
+++ b/kodereviewer/qml/FilesChangedPage.qml
@@ -30,6 +30,7 @@ Kirigami.ScrollablePage {
onPullRequestChanged: {
root.currentFile = ""
root.currentText = ""
+ root.currentSha = ""
}
Component.onCompleted: {
@@ -85,7 +86,7 @@ Kirigami.ScrollablePage {
id: editor
Layout.fillWidth: true
Layout.fillHeight: true
- file: root.currentFile
+ file: root.currentFile + '.diff'
text: root.currentText
MouseArea {
@@ -108,7 +109,21 @@ Kirigami.ScrollablePage {
onTriggered: {
console.log("triggered review")
console.log(`start line: ${editor.selectionStartLine()} to ${editor.selectionEndLine()}`)
- popup.open()
+
+ const component = Qt.createComponent("ReviewWindow.qml")
+ print(applicationWindow().project)
+ const params = {
+ project: applicationWindow().project,
+ pullRequestNumber: root.pullRequest.number,
+ diffText: root.currentText,
+ path: root.currentFile,
+ startLine: editor.selectionStartLine(),
+ endLine: editor.selectionEndLine(),
+ commitId: root.pullRequest.last_commit
+ }
+ component.createObject(applicationWindow(), params)
+
+ //popup.open()
}
}
}
@@ -120,7 +135,7 @@ Kirigami.ScrollablePage {
target: contextDrawer
function onFileSelected(filename, text) {
console.log("file changed!")
- root.currentFile = filename + '.diff'
+ root.currentFile = filename
root.currentText = text
}
}
diff --git a/kodereviewer/qml/FilesDrawer.qml b/kodereviewer/qml/FilesDrawer.qml
index 3f98290..c4cfddf 100644
--- a/kodereviewer/qml/FilesDrawer.qml
+++ b/kodereviewer/qml/FilesDrawer.qml
@@ -1,3 +1,4 @@
+pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
diff --git a/kodereviewer/qml/PullRequestDescription.qml b/kodereviewer/qml/PullRequestDescription.qml
index 329d288..7a91d14 100644
--- a/kodereviewer/qml/PullRequestDescription.qml
+++ b/kodereviewer/qml/PullRequestDescription.qml
@@ -126,7 +126,7 @@ Kirigami.FormLayout {
Kirigami.Separator {
Kirigami.FormData.isSection: true
- Kirigami.FormData.label: "Reviewers"
+ Kirigami.FormData.label: i18n("Assignees")
}
Loader {
diff --git a/kodereviewer/qml/ReviewWindow.qml b/kodereviewer/qml/ReviewWindow.qml
new file mode 100644
index 0000000..b3d58cc
--- /dev/null
+++ b/kodereviewer/qml/ReviewWindow.qml
@@ -0,0 +1,76 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtCore
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+import org.kde.kirigamiaddons.components as KirigamiComponents
+
+import org.deprecated.kodereviewer 1.0
+
+Kirigami.ApplicationWindow {
+ id: root
+ height: Kirigami.Units.gridUnit * 30
+ width: Kirigami.Units.gridUnit * 30
+ minimumWidth: Kirigami.Units.gridUnit * 15
+ minimumHeight: Kirigami.Units.gridUnit * 20
+
+ property Project project
+
+ property NetworkManager connection: NetworkManager {
+ project: root.project
+ }
+
+ property int pullRequestNumber
+ property string diffText: ''
+ property string path: '/src/bla.c++'
+ property int startLine: 1
+ property int endLine: 2
+ property string commitId: ''
+
+ title: i18n("New review")
+
+
+ pageStack.initialPage: Kirigami.ScrollablePage {
+ id: page
+ title: root.path
+
+ Kirigami.FlexColumn {
+ Editor {
+ Layout.fillWidth: true
+
+ text: root.diffText
+ }
+ MarkdownTextArea {
+ id: commentTextArea
+ placeholderText: "Say something"
+ Layout.fillWidth: true
+ }
+ }
+
+ footer: RowLayout {
+ Item {
+ Layout.fillWidth: true
+ }
+ QQC2.Button {
+ text: i18n("Comment now")
+ icon.name: "document-send-symbolic"
+ onClicked: {
+ root.submit()
+ }
+ }
+ QQC2.Button {
+ text: i18n("Add review (TODO)")
+ enabled: false
+
+ }
+ }
+ }
+
+ function submit() {
+ connection.createReviewComment(pullRequestNumber, commentTextArea.text,
+ path, commitId, endLine, startLine)
+ }
+}
diff --git a/pyproject.toml b/pyproject.toml
index 657ab63..ff00bc3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,3 +44,6 @@ kodereviewer = "kodereviewer.app:main"
[tool.pyright]
venv = "venv"
venvPath = "."
+
+[tool.pytest.ini_options]
+python_files = ["*_tests.py"]
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/diff_parser_tests.py b/tests/diff_parser_tests.py
new file mode 100644
index 0000000..023430e
--- /dev/null
+++ b/tests/diff_parser_tests.py
@@ -0,0 +1,22 @@
+from kodereviewer.diff_parser import Hunk
+
+
+class TestHunk:
+ def test_parse_header(self):
+ hunk = Hunk('@@ -13,10 +13,15 @@ on:')
+
+ assert hunk.original_start_line == 13
+ assert hunk.original_line_count == 10
+ assert hunk.new_start_line == 13
+ assert hunk.new_line_count == 15
+ assert hunk.context == 'on:'
+
+ def test_parse_hunk_different_lines(self):
+ hunk = Hunk('@@ -68,5 +81,10 @@ jobs:')
+
+ assert hunk.original_start_line == 68
+ assert hunk.original_line_count == 5
+
+ assert hunk.new_start_line == 81
+ assert hunk.new_line_count == 10
+