1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from pygit2 import Repository, init_repository
from unicurses import KEY_BACKSPACE, KEY_ENTER, KEY_RESIZE, KEY_RIGHT, KEY_UP
from pygittools.tui.app import _apply_result, run_tui
from pygittools.tui.changes_list import ChangesList, SectionId
from pygittools.tui.commit_draft import commit_editmsg_path, load_commit_draft
from pygittools.tui.input_context import InputContext, nav_context
from pygittools.tui.input_context_test import FakeHost
from pygittools.tui.log_list import CommitLogList
from pygittools.tui.pages.branches import BranchPickerPage
from pygittools.tui.pages.help import HelpPage
from pygittools.tui.pages.home import home_page
from pygittools.tui.pages.no_repo import NoRepoPage
from pygittools.tui.pages.projects import ProjectsPage
from pygittools.tui.projects import ProjectEntry
from pygittools.tui.pygitweb_layout import _bool_value, _int_value, _layout_from_mapping
from pygittools.tui.status_bar import _format_line
from pygittools.tui.tabs import _clip_prefix, tab_row_layout
from pygittools.tui.types import PageAction, PageContext, PageResult
from pygittools.tui.worktree import commit_staged, unstage_paths
def test_read_draft_skips_blank_lines(tmp_path: Path) -> None:
commit_editmsg_path(tmp_path).write_text("\n\n# comment\n", encoding="utf-8")
assert load_commit_draft(tmp_path) == ""
def test_draw_text_zero_clip_width(monkeypatch: pytest.MonkeyPatch) -> None:
from pygittools.tui import draw
monkeypatch.setattr(draw, "move", MagicMock())
monkeypatch.setattr(draw, "mvaddstr", MagicMock())
assert draw.draw_text(0, 9, "x", 10) == 9
def test_status_bar_tight_left_space() -> None:
line = _format_line("left", "hint", 5)
assert line == "hint "
def test_tab_row_layout_multiple_tabs() -> None:
layout = tab_row_layout("p", ("A", "B", "C"))
assert len(layout) == 3
def test_clip_prefix_fits_without_ellipsis() -> None:
assert _clip_prefix("x", 80, ("Status", "Log")) == "x"
def test_layout_from_mapping_empty_projects_list() -> None:
layout = _layout_from_mapping({"PROJECTROOT": "/tmp", "PROJECTS_LIST": ""})
assert layout.projects_list == Path("/tmp")
def test_int_and_bool_defaults() -> None:
assert _int_value(object(), 9) == 9
assert _bool_value(object(), False) is False
def test_changes_list_working_tree_clean_hint() -> None:
changes = ChangesList()
assert changes.status_hint() == "Working tree clean"
def test_changes_list_input_resize_and_unknown_key(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
assert changes.handle_input_key(KEY_RESIZE, committed_repo).layout_changed is False
assert changes.handle_input_key(999, committed_repo).layout_changed is False
def test_changes_list_nav_with_no_rows_after_collapse(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes._rows = []
assert changes.handle_nav_key(KEY_UP, committed_repo).layout_changed is False
def test_changes_list_commit_hints_and_backspace(committed_repo: Repository) -> None:
changes = ChangesList()
changes.set_commit_message("msg")
changes.refresh(committed_repo)
changes._cursor = 0
assert "Enter commit" in changes.status_hint()
changes.handle_nav_key(KEY_BACKSPACE, committed_repo)
changes.handle_nav_key(KEY_BACKSPACE, committed_repo)
def test_changes_list_staged_header_unstage_all(committed_repo: Repository) -> None:
path = Path(committed_repo.workdir) / "new.txt"
path.write_text("x\n", encoding="utf-8")
committed_repo.index.add("new.txt")
committed_repo.index.write()
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header" and row.section == SectionId.STAGED:
changes._cursor = index
break
changes.handle_nav_key(KEY_ENTER, committed_repo)
def test_changes_list_unstage_file_row(committed_repo: Repository) -> None:
path = Path(committed_repo.workdir) / "new.txt"
path.write_text("x\n", encoding="utf-8")
committed_repo.index.add("new.txt")
committed_repo.index.write()
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "file" and row.section == SectionId.STAGED:
changes._cursor = index
changes.handle_nav_key(KEY_ENTER, committed_repo)
break
def test_changes_list_header_right_key(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header":
changes._cursor = index
changes.handle_nav_key(KEY_RIGHT, committed_repo)
break
def test_changes_list_failed_commit_nav(committed_repo: Repository) -> None:
changes = ChangesList()
changes.set_commit_message("msg")
changes.refresh(committed_repo)
changes._cursor = 0
changes.handle_nav_key(KEY_ENTER, committed_repo)
assert changes.status_hint()
def test_log_list_scroll_and_refresh(committed_repo: Repository) -> None:
log = CommitLogList()
log.refresh(committed_repo)
log.draw(0, 0, 80, highlight=True)
log._cursor = 100
log.draw(0, 2, 80, highlight=True)
log.handle_nav_key(KEY_UP, committed_repo)
def test_input_context_log_tab_at_top(page_ctx: PageContext) -> None:
host = FakeHost(page_context=page_ctx, changes=ChangesList(), log=CommitLogList())
host.active_tab = "log"
host.log._cursor = 0
nav_context().dispatch_key(host, KEY_UP)
assert host.header_focus == "branch"
def test_input_context_input_without_relinquish(page_ctx: PageContext) -> None:
host = FakeHost(page_context=page_ctx, changes=ChangesList(), log=CommitLogList())
dispatch = InputContext().dispatch_key(host, ord("x"))
assert dispatch.next_mode is None
def test_branch_picker_error_and_short_list(committed_repo: Repository) -> None:
ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
page = BranchPickerPage()
page.on_enter(ctx)
page._error = "bad"
assert "bad" in page.status_text()
page.draw(0, 2, 80)
page._ctx = None
page.draw(0, 10, 80)
page.handle_key(KEY_ENTER)
def test_branch_picker_checkout_error(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
page = BranchPickerPage()
page.on_enter(ctx)
page._filtered = ["ghost"]
page._cursor = 1
monkeypatch.setattr(
"pygittools.tui.pages.branches.checkout_branch",
lambda _repo, _branch: (False, "nope"),
)
page.handle_key(KEY_ENTER)
assert page.status_text() == "nope"
def test_help_page_empty_lines(monkeypatch: pytest.MonkeyPatch) -> None:
page = HelpPage()
monkeypatch.setattr(page, "_lines", [])
assert page.status_text() == "Help"
page.draw(0, 0, 80)
def test_help_page_scroll_past_end() -> None:
page = HelpPage()
page._offset = 999
page.draw(0, 3, 80)
page.handle_key(999)
def test_home_page_relinquish_input(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
page = home_page()
page.on_enter(ctx)
page.handle_key(ord("a"))
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg: (True, ""),
)
page.handle_key(KEY_ENTER)
assert page.status_text()
def test_no_repo_page_truncates_lines(tmp_path: Path) -> None:
page = NoRepoPage(tmp_path)
page.draw(0, 1, 80)
page.handle_key(999)
def test_projects_page_error_and_scroll(tmp_path: Path) -> None:
page = ProjectsPage()
page._projects = [
ProjectEntry(path="x" * 100, worktree=tmp_path, branch="b" * 30),
]
page._projectroot_label = "root"
page._error = "bad"
assert page.status_text() == "bad"
page.draw(0, 5, 40)
page.handle_key(KEY_UP)
page.handle_key(ord("q"))
def test_unstage_paths_empty(committed_repo: Repository) -> None:
unstage_paths(committed_repo, [])
def test_commit_staged_no_workdir(tmp_path: Path) -> None:
bare = init_repository(str(tmp_path / "bare"), bare=True)
ok, error = commit_staged(bare, "msg")
assert ok is False
assert error == "Repository has no working directory"
def test_apply_result_switch_repo_missing(tmp_path: Path, page_ctx: PageContext) -> None:
home = home_page()
home.on_enter(page_ctx)
ctx, stack = _apply_result(
[home],
page_ctx,
PageResult(action=PageAction.POP, switch_repo=tmp_path / "missing"),
)
assert ctx is page_ctx
def test_run_tui_keyboard_interrupt(committed_repo: Repository) -> None:
repo_dir = Path(committed_repo.workdir)
with patch("pygittools.tui.app.initscr") as initscr:
initscr.return_value = MagicMock()
with (
patch("pygittools.tui.app.getmaxyx", return_value=(24, 80)),
patch("pygittools.tui.app.getch", side_effect=KeyboardInterrupt),
patch("pygittools.tui.app.noecho"),
patch("pygittools.tui.app.cbreak"),
patch("pygittools.tui.app.curs_set"),
patch("pygittools.tui.app.keypad"),
patch("pygittools.tui.app.clear"),
patch("pygittools.tui.app.refresh"),
patch("pygittools.tui.app.endwin"),
patch("pygittools.tui.app.init_tui_color_theme"),
patch("pygittools.tui.app.init_tab_colors"),
patch("pygittools.tui.app.draw_status_bar"),
patch("pygittools.tui.app.status_bar_attr", return_value=0),
):
assert run_tui(repo_dir) == 0
def test_run_tui_resize_key(committed_repo: Repository) -> None:
repo_dir = Path(committed_repo.workdir)
with patch("pygittools.tui.app.initscr") as initscr:
initscr.return_value = MagicMock()
with (
patch("pygittools.tui.app.getmaxyx", return_value=(24, 80)),
patch("pygittools.tui.app.getch", side_effect=[KEY_RESIZE, ord("q")]),
patch("pygittools.tui.app.noecho"),
patch("pygittools.tui.app.cbreak"),
patch("pygittools.tui.app.curs_set"),
patch("pygittools.tui.app.keypad"),
patch("pygittools.tui.app.clear"),
patch("pygittools.tui.app.refresh"),
patch("pygittools.tui.app.endwin"),
patch("pygittools.tui.app.init_tui_color_theme"),
patch("pygittools.tui.app.init_tab_colors"),
patch("pygittools.tui.app.draw_status_bar"),
patch("pygittools.tui.app.status_bar_attr", return_value=0),
):
assert run_tui(repo_dir) == 0