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
from __future__ import annotations
from pathlib import Path
import pytest
from pygit2 import Repository
from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_DOWN, KEY_ENTER, KEY_RESIZE, KEY_UP
from pygittools.tui.changes_list import ChangesList, SectionId
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_nav_resize_is_noop(page_ctx: PageContext) -> None:
changes = ChangesList()
assert changes.handle_nav_key(KEY_RESIZE, page_ctx.repo).layout_changed is False
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._cursor = 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._cursor = 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._cursor = index
break
result = changes.handle_nav_key(ord(" "), committed_repo)
assert result.layout_changed is True
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._cursor = 0
changes.handle_nav_key(ord("f"), committed_repo)
assert changes.commit_message == "f"
changes.handle_input_key(KEY_BACKSPACE, committed_repo)
assert changes.commit_message == ""
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg: (True, ""),
)
result = changes.handle_input_key(KEY_ENTER, committed_repo)
assert result.relinquish is True
def test_commit_empty_shows_error(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes._cursor = 0
changes.handle_input_key(KEY_ENTER, committed_repo)
assert changes.status_hint() == "Commit message is empty"
def test_commit_from_nav_row(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.set_commit_message("ship it")
changes.refresh(committed_repo)
changes._cursor = 0
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
def test_enter_on_empty_commit_enters_input(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes._cursor = 0
result = changes.handle_nav_key(KEY_ENTER, committed_repo)
assert result.enter_input is True
def test_status_hints_for_sections(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.STAGED:
changes._cursor = index
assert "stage all" in changes.status_hint()
if row.kind == "file" and row.section == SectionId.UNTRACKED:
changes._cursor = index
assert "Enter stage" in changes.status_hint()
def test_draw_and_scroll(committed_repo: Repository) -> None:
changes = ChangesList()
changes.refresh(committed_repo)
changes.draw(0, 0, 80)
changes._cursor = 50
changes.draw(0, 5, 80, highlight=True, commit_input=True)
def test_persist_commit_draft(committed_repo: Repository) -> None:
changes = ChangesList()
changes.set_commit_message("draft")
changes.persist_commit_draft(committed_repo)
from pygittools.tui.commit_draft import load_commit_draft
assert load_commit_draft(Path(committed_repo.path), committed_repo) == "draft"
def test_unstage_all_from_empty_staged_header(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.STAGED:
changes._cursor = index
break
result = changes.handle_nav_key(KEY_ENTER, committed_repo)
assert result.layout_changed is True
def test_long_commit_message_truncates_in_label(committed_repo: Repository) -> None:
changes = ChangesList()
changes.set_commit_message("x" * 200)
label = changes._commit_label(40, focused=True)
assert len(label) <= 40