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.
Files changed (25) hide show
  1. {nit_cli-0.4.0 → nit_cli-0.4.1}/PKG-INFO +1 -1
  2. {nit_cli-0.4.0 → nit_cli-0.4.1}/pyproject.toml +1 -1
  3. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/app.py +20 -10
  4. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/PKG-INFO +1 -1
  5. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/SOURCES.txt +2 -1
  6. {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_app.py +16 -1
  7. {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_comments.py +52 -0
  8. nit_cli-0.4.1/tests/test_syntax.py +26 -0
  9. {nit_cli-0.4.0 → nit_cli-0.4.1}/LICENSE +0 -0
  10. {nit_cli-0.4.0 → nit_cli-0.4.1}/README.md +0 -0
  11. {nit_cli-0.4.0 → nit_cli-0.4.1}/setup.cfg +0 -0
  12. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/__init__.py +0 -0
  13. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/cli.py +0 -0
  14. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/comments.py +0 -0
  15. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/diff_parser.py +0 -0
  16. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/git.py +0 -0
  17. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/models.py +0 -0
  18. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit/syntax.py +0 -0
  19. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/dependency_links.txt +0 -0
  20. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/entry_points.txt +0 -0
  21. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/requires.txt +0 -0
  22. {nit_cli-0.4.0 → nit_cli-0.4.1}/src/nit_cli.egg-info/top_level.txt +0 -0
  23. {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_cli.py +0 -0
  24. {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_diff_parser.py +0 -0
  25. {nit_cli-0.4.0 → nit_cli-0.4.1}/tests/test_git.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nit-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Terminal diff viewer with inline review comments
5
5
  Author: Joshua Zink-Duda
6
6
  License-Expression: GPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nit-cli"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Terminal diff viewer with inline review comments"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -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
- return
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
- comments_mod.save_comments(self.repo_root, self.comments, self.branch, self.base)
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(["pbcopy"], input=text, text=True, check=True, timeout=5)
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nit-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Terminal diff viewer with inline review comments
5
5
  Author: Joshua Zink-Duda
6
6
  License-Expression: GPL-3.0-only
@@ -19,4 +19,5 @@ tests/test_app.py
19
19
  tests/test_cli.py
20
20
  tests/test_comments.py
21
21
  tests/test_diff_parser.py
22
- tests/test_git.py
22
+ tests/test_git.py
23
+ tests/test_syntax.py
@@ -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