nit-cli 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nit_cli-0.4.0 → nit_cli-0.4.1}/PKG-INFO +1 -1
- {nit_cli-0.4.0 → nit_cli-0.4.1}/pyproject.toml +1 -1
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/app.py +20 -10
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/PKG-INFO +1 -1
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/SOURCES.txt +2 -1
- {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_app.py +16 -1
- {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_comments.py +52 -0
- nit_cli-0.4.1/tests/test_syntax.py +26 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/LICENSE +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/README.md +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/setup.cfg +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/__init__.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/cli.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/comments.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/diff_parser.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/git.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/models.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/syntax.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/dependency_links.txt +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/entry_points.txt +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/requires.txt +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/top_level.txt +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_cli.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_diff_parser.py +0 -0
- {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_git.py +0 -0
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
from collections import Counter
|
|
7
8
|
from pathlib import Path
|
|
@@ -360,9 +361,7 @@ class DiffView(VerticalScroll):
|
|
|
360
361
|
self.diff_lines.append(primary)
|
|
361
362
|
|
|
362
363
|
# Build Rich Text for the row
|
|
363
|
-
text = self._format_sbs_row(
|
|
364
|
-
row, half, lang if row.row_type == "context" else None
|
|
365
|
-
)
|
|
364
|
+
text = self._format_sbs_row(row, half, lang if row.row_type == "context" else None)
|
|
366
365
|
w = DiffLineWidget(text)
|
|
367
366
|
|
|
368
367
|
w.add_class("sbs")
|
|
@@ -383,9 +382,7 @@ class DiffView(VerticalScroll):
|
|
|
383
382
|
block = CommentBlock(f"-- {c.comment}")
|
|
384
383
|
self.mount(block)
|
|
385
384
|
|
|
386
|
-
def _format_sbs_row(
|
|
387
|
-
self, row: SideBySideRow, half: int, language: str | None = None
|
|
388
|
-
) -> Text:
|
|
385
|
+
def _format_sbs_row(self, row: SideBySideRow, half: int, language: str | None = None) -> Text:
|
|
389
386
|
"""Format a side-by-side row as Rich Text with per-side coloring."""
|
|
390
387
|
if row.row_type == "hunk_header":
|
|
391
388
|
left = row.left
|
|
@@ -729,7 +726,7 @@ class NitApp(App):
|
|
|
729
726
|
try:
|
|
730
727
|
self.branch = git.get_current_branch(self.repo_root)
|
|
731
728
|
except Exception:
|
|
732
|
-
self.branch = ""
|
|
729
|
+
self.branch = "(detached HEAD)"
|
|
733
730
|
self.base = git.get_main_branch(self.repo_root)
|
|
734
731
|
if self._cli_args.mode:
|
|
735
732
|
self.diff_mode = self._cli_args.mode
|
|
@@ -934,7 +931,7 @@ class NitApp(App):
|
|
|
934
931
|
action()
|
|
935
932
|
event.prevent_default()
|
|
936
933
|
event.stop()
|
|
937
|
-
|
|
934
|
+
return
|
|
938
935
|
if event.key == "g":
|
|
939
936
|
self._pending_g = True
|
|
940
937
|
event.prevent_default()
|
|
@@ -1016,7 +1013,12 @@ class NitApp(App):
|
|
|
1016
1013
|
diff_mode=self.diff_mode,
|
|
1017
1014
|
)
|
|
1018
1015
|
self.comments.append(comment)
|
|
1019
|
-
|
|
1016
|
+
try:
|
|
1017
|
+
comments_mod.save_comments(
|
|
1018
|
+
self.repo_root, self.comments, self.branch, self.base
|
|
1019
|
+
)
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
self.notify(f"Failed to save comment: {e}", severity="error")
|
|
1020
1022
|
saved_cursor = diff_view.cursor_index
|
|
1021
1023
|
diff_view.hide_comment_input()
|
|
1022
1024
|
if self.current_file:
|
|
@@ -1159,8 +1161,16 @@ class NitApp(App):
|
|
|
1159
1161
|
self.notify("No comments to export", severity="warning")
|
|
1160
1162
|
return
|
|
1161
1163
|
text = comments_mod.export_comments_markdown(self.comments)
|
|
1164
|
+
clip_cmd = None
|
|
1165
|
+
for cmd in ("pbcopy", "xclip", "xsel"):
|
|
1166
|
+
if shutil.which(cmd):
|
|
1167
|
+
clip_cmd = [cmd] if cmd == "pbcopy" else [cmd, "-selection", "clipboard"]
|
|
1168
|
+
break
|
|
1169
|
+
if clip_cmd is None:
|
|
1170
|
+
self.notify("No clipboard command found (pbcopy/xclip/xsel)", severity="error")
|
|
1171
|
+
return
|
|
1162
1172
|
try:
|
|
1163
|
-
subprocess.run(
|
|
1173
|
+
subprocess.run(clip_cmd, input=text, text=True, check=True, timeout=5)
|
|
1164
1174
|
self.notify(f"Copied {len(self.comments)} comments to clipboard")
|
|
1165
1175
|
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
1166
1176
|
self.notify("Clipboard copy failed", severity="error")
|
|
@@ -92,4 +92,19 @@ async def test_app_detached_head(mock_git, mock_comments):
|
|
|
92
92
|
mock_git.get_current_branch.side_effect = Exception("not on branch")
|
|
93
93
|
app = NitApp()
|
|
94
94
|
async with app.run_test():
|
|
95
|
-
assert app.branch == ""
|
|
95
|
+
assert app.branch == "(detached HEAD)"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def test_g_chord_unrecognized_key_not_swallowed(mock_git, mock_comments):
|
|
99
|
+
app = NitApp()
|
|
100
|
+
async with app.run_test() as pilot:
|
|
101
|
+
# Press g — sets pending flag
|
|
102
|
+
await pilot.press("g")
|
|
103
|
+
assert app._pending_g is True
|
|
104
|
+
# Press unrecognized key — flag resets, key not swallowed
|
|
105
|
+
await pilot.press("z")
|
|
106
|
+
assert app._pending_g is False
|
|
107
|
+
# Valid chord still works after: gg = cursor to start
|
|
108
|
+
await pilot.press("g", "g")
|
|
109
|
+
dv = app.query_one("#diff-view")
|
|
110
|
+
assert dv.cursor_index == 0
|
|
@@ -2,6 +2,8 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from nit.comments import (
|
|
4
4
|
comment_matches_line,
|
|
5
|
+
export_comments_json,
|
|
6
|
+
export_comments_markdown,
|
|
5
7
|
load_comments,
|
|
6
8
|
make_comment,
|
|
7
9
|
save_comments,
|
|
@@ -114,3 +116,53 @@ def test_load_missing_comment_fields(tmp_path):
|
|
|
114
116
|
loaded = load_comments(tmp_path)
|
|
115
117
|
assert len(loaded) == 1
|
|
116
118
|
assert loaded[0].file_path == "x.py"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _make_review_comment(**overrides):
|
|
122
|
+
defaults = dict(
|
|
123
|
+
file_path="src/app.py",
|
|
124
|
+
new_line_no=10,
|
|
125
|
+
old_line_no=None,
|
|
126
|
+
line_content="return result",
|
|
127
|
+
comment="Fix this",
|
|
128
|
+
hunk_context=["line1", "return result"],
|
|
129
|
+
timestamp="2025-01-15T10:00:00+00:00",
|
|
130
|
+
diff_mode="branch",
|
|
131
|
+
)
|
|
132
|
+
defaults.update(overrides)
|
|
133
|
+
return ReviewComment(**defaults)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_export_comments_markdown_empty():
|
|
137
|
+
assert export_comments_markdown([]) == ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_export_comments_markdown():
|
|
141
|
+
comments = [
|
|
142
|
+
_make_review_comment(file_path="b.py", new_line_no=5, comment="second file"),
|
|
143
|
+
_make_review_comment(file_path="a.py", new_line_no=10, comment="note here"),
|
|
144
|
+
_make_review_comment(
|
|
145
|
+
file_path="a.py", new_line_no=None, old_line_no=3, comment="old line comment"
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
md = export_comments_markdown(comments)
|
|
149
|
+
assert md.startswith("# Code Review Comments")
|
|
150
|
+
# Files sorted alphabetically
|
|
151
|
+
assert md.index("## a.py") < md.index("## b.py")
|
|
152
|
+
assert "**L10**: note here" in md
|
|
153
|
+
assert "**old L3**: old line comment" in md
|
|
154
|
+
assert "**L5**: second file" in md
|
|
155
|
+
assert "```" in md # code fence for line_content
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_export_comments_json():
|
|
159
|
+
comments = [
|
|
160
|
+
_make_review_comment(comment="test comment"),
|
|
161
|
+
]
|
|
162
|
+
result = export_comments_json(comments)
|
|
163
|
+
parsed = json.loads(result)
|
|
164
|
+
assert isinstance(parsed, list)
|
|
165
|
+
assert len(parsed) == 1
|
|
166
|
+
assert parsed[0]["comment"] == "test comment"
|
|
167
|
+
assert parsed[0]["file"] == "src/app.py"
|
|
168
|
+
assert parsed[0]["line"] == 10
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from rich.text import Text
|
|
2
|
+
|
|
3
|
+
from nit.syntax import detect_language, highlight_line
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_detect_language_python():
|
|
7
|
+
assert detect_language("app.py") == "python"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_detect_language_javascript():
|
|
11
|
+
assert detect_language("index.js") == "javascript"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_detect_language_unknown():
|
|
15
|
+
assert detect_language("file.xyzunknown") is None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_highlight_line_returns_text():
|
|
19
|
+
result = highlight_line("x = 1", "python")
|
|
20
|
+
assert isinstance(result, Text)
|
|
21
|
+
assert str(result).strip() == "x = 1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_highlight_line_empty():
|
|
25
|
+
result = highlight_line("", "python")
|
|
26
|
+
assert isinstance(result, Text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|