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
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
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 changes.status_hint() == "Enter type message — j/k move"
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, monkeypatch: pytest.MonkeyPatch) -> 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)
changes.handle_nav_key(key("f"), committed_repo)
assert changes.commit_message.endswith("f")
changes.handle_input_key(key("Backspace"), committed_repo)
result = changes.handle_input_key(key("Enter"), committed_repo)
# may fail without staged depending on index state; exercise path
assert result is not None
def test_commit_enter_with_message(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.set_commit_message("msg")
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg: (True, ""),
)
result = changes.handle_nav_key(key("Enter"), committed_repo)
assert result.layout_changed is True
assert result.committed is True
def test_commit_enter_failure(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.set_commit_message("msg")
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg: (False, "nope"),
)
changes.handle_nav_key(key("Enter"), committed_repo)
assert "nope" in changes.status_hint()
def test_enter_empty_commit_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_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, tmp_path: Path) -> 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