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
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from pygit2 import Repository
from pyratatui import Rect
from pygittools.tui.changes_list import ChangesList, SectionId
from pygittools.tui.keys import key, set_vim_enabled
from pygittools.tui.types import PageContext
from pygittools.tui.worktree import categorize_status
def test_refresh_builds_sections(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
assert changes.status_hint() != ""
def test_refresh_clean_repo(page_ctx: PageContext) -> None:
changes = ChangesList()
changes.refresh(page_ctx.repo)
assert "Enter edit" in changes.status_hint()
def test_nav_up_down(page_ctx: PageContext) -> None:
changes = ChangesList()
changes.refresh(page_ctx.repo)
changes.handle_nav_key(key("Down"), page_ctx.repo)
changes.handle_nav_key(key("Up"), page_ctx.repo)
assert changes.is_at_top()
def test_stage_and_unstage_file(committed_repo: Repository) -> None:
path = Path(committed_repo.workdir) / "tracked.txt"
path.write_text("v2\n", encoding="utf-8")
changes = ChangesList()
changes.refresh(committed_repo)
changes._list_state.select(3)
changes.handle_nav_key(key("Enter"), committed_repo)
changes.refresh(committed_repo)
staged, unstaged, _ = categorize_status(committed_repo)
assert "tracked.txt" in staged or "tracked.txt" in unstaged
def test_toggle_section_stages_untracked(committed_repo: Repository) -> None:
(Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header" and row.section == SectionId.UNTRACKED:
changes._list_state.select(index)
break
result = changes.handle_nav_key(key("Enter"), committed_repo)
assert result.layout_changed is True
def test_collapse_section_with_space(committed_repo: Repository) -> None:
(Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header" and row.section == SectionId.UNSTAGED:
changes._list_state.select(index)
break
result = changes.handle_nav_key(key(" "), committed_repo)
assert result.layout_changed is True
def test_collapse_does_not_reread_status(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header":
changes._list_state.select(index)
break
with patch("pygittools.tui.changes_list.categorize_status") as status:
changes.handle_nav_key(key(" "), committed_repo)
status.assert_not_called()
assert any(row.kind == "header" and row.label.startswith("[>") for row in changes._rows)
def test_commit_input_flow(committed_repo: Repository) -> None:
(Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
committed_repo.index.add("new.txt")
committed_repo.index.write()
changes = ChangesList()
changes.refresh(committed_repo)
changes._list_state.select(0)
assert changes.handle_nav_key(key("Enter"), committed_repo).enter_input is True
changes.handle_input_key(key("f"), committed_repo)
assert changes.commit_message.endswith("f")
changes.handle_input_key(key("Enter"), committed_repo)
assert "\n" in changes.commit_message
changes.handle_input_key(key("Tab"), committed_repo)
result = changes.handle_input_key(key("Enter"), committed_repo)
assert result.committed is True
assert result.relinquish is True
def test_commit_button_with_message(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.set_commit_message("msg")
changes.begin_input()
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg, *, amend=False: (True, ""),
)
changes.handle_input_key(key("Tab"), committed_repo)
result = changes.handle_input_key(key("Enter"), committed_repo)
assert result.layout_changed is True
assert result.committed is True
def test_amend_button(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.set_commit_message("fix")
changes.begin_input()
seen: dict[str, bool] = {}
def _commit(_repo: Repository, _msg: str, *, amend: bool = False) -> tuple[bool, str]:
seen["amend"] = amend
return True, ""
monkeypatch.setattr("pygittools.tui.changes_list.commit_staged", _commit)
changes.handle_input_key(key("Tab"), committed_repo)
changes.handle_input_key(key("Right"), committed_repo)
result = changes.handle_input_key(key("Enter"), committed_repo)
assert result.committed is True
assert seen["amend"] is True
def test_commit_button_failure(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.set_commit_message("msg")
changes.begin_input()
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg, *, amend=False: (False, "nope"),
)
changes.handle_input_key(key("Tab"), committed_repo)
changes.handle_input_key(key("Enter"), committed_repo)
assert "nope" in changes.input_status_hint()
def test_enter_commit_row_starts_input(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes._list_state.select(0)
result = changes.handle_nav_key(key("Enter"), committed_repo)
assert result.enter_input is True
def test_vim_i_starts_input(committed_repo: Repository) -> None:
set_vim_enabled(True)
try:
changes = ChangesList()
changes.refresh(committed_repo)
changes._list_state.select(0)
assert "Enter/i" in changes.status_hint()
result = changes.handle_nav_key(key("i"), committed_repo)
assert result.enter_input is True
finally:
set_vim_enabled(False)
def test_stage_file_hint(committed_repo: Repository) -> None:
path = Path(committed_repo.workdir) / "tracked.txt"
path.write_text("v2\n", encoding="utf-8")
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "file" and row.section == SectionId.UNSTAGED:
changes._list_state.select(index)
break
assert "Enter stage" in changes.status_hint()
def test_draw_renders(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
frame = MagicMock()
area = Rect(0, 0, 80, 20)
changes.draw(frame, area, highlight=True, commit_input=True)
changes.draw(frame, area, highlight=True, commit_input=False)
def test_draw_commit_input_does_not_highlight_shifted_row(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes._list_state.select(0)
frame = MagicMock()
changes.draw(frame, Rect(0, 0, 80, 20), highlight=True, commit_input=True)
assert frame.render_stateful_list.called
list_state = frame.render_stateful_list.call_args.args[2]
assert list_state.selected is None
assert (changes._list_state.selected or 0) == 0
def test_persist_commit_draft(page_ctx: PageContext) -> None:
changes = ChangesList()
changes.set_commit_message("draft")
changes.persist_commit_draft(page_ctx.repo)
def test_toggle_staged_header_when_empty(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
for index, row in enumerate(changes._rows):
if row.kind == "header" and row.section == SectionId.STAGED:
changes._list_state.select(index)
break
result = changes.handle_nav_key(key("Enter"), committed_repo)
assert result.layout_changed is True