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
"""Scrollable commit log for the pgt TUI."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from pygit2 import Commit, GitError, Repository
from pygit2.enums import SortMode
from pyratatui import Frame, List, ListItem, ListState, Rect
from pygittools.tui.git_colors import current_theme, log_entry_styles
from pygittools.tui.keys import KeyLike, is_down, is_up
_MAX_COMMITS = 500
@dataclass(frozen=True, slots=True)
class _LogEntry:
short_id: str
date: str
subject: str
@dataclass(frozen=True, slots=True)
class LogNavKeyResult:
layout_changed: bool = False
class CommitLogList:
def __init__(self) -> None:
self._entries: list[_LogEntry] = []
self._list_state = ListState()
def is_at_top(self) -> bool:
return (self._list_state.selected or 0) == 0
def refresh(self, repo: Repository) -> None:
self._entries = _load_entries(repo)
selected = self._list_state.selected or 0
self._list_state.select(min(selected, max(0, len(self._entries) - 1)))
def draw(self, frame: Frame, area: Rect, *, highlight: bool) -> None:
if area.height <= 0 or area.width <= 0:
return
theme = current_theme()
if not self._entries:
frame.render_stateful_list(
List([ListItem("No commits", theme.log_date)]),
area,
self._list_state,
)
return
selected = self._list_state.selected or 0
items: list[ListItem] = []
for index, entry in enumerate(self._entries):
focused = highlight and index == selected
hash_style, date_style, subject_style = log_entry_styles(
focused=focused,
highlight=highlight,
)
del date_style, subject_style
label = f"{entry.short_id} {entry.date} {entry.subject}"
items.append(ListItem(label, hash_style if focused else theme.log_subject))
frame.render_stateful_list(
List(items).highlight_style(theme.focus).highlight_symbol(""),
area,
self._list_state,
)
def handle_nav_key(self, key: KeyLike, repo: Repository) -> LogNavKeyResult:
if is_up(key):
selected = self._list_state.selected or 0
self._list_state.select(max(0, selected - 1))
return LogNavKeyResult()
if is_down(key):
selected = self._list_state.selected or 0
self._list_state.select(min(max(0, len(self._entries) - 1), selected + 1))
return LogNavKeyResult()
self.refresh(repo)
return LogNavKeyResult()
def status_hint(self) -> str:
if not self._entries:
return "No commits — j/k move"
entry = self._entries[self._list_state.selected or 0]
return f"{entry.short_id} {entry.date} — j/k move"
def _load_entries(repo: Repository) -> list[_LogEntry]:
try:
head = repo.head.target
except GitError:
return []
entries: list[_LogEntry] = []
for commit in repo.walk(head, SortMode.TIME):
if not isinstance(commit, Commit):
continue
subject = commit.message.splitlines()[0] if commit.message else "(no message)"
entries.append(
_LogEntry(
short_id=str(commit.id)[:7],
date=_format_commit_date(commit),
subject=subject,
),
)
if len(entries) >= _MAX_COMMITS:
break
return entries
def _format_commit_date(commit: Commit) -> str:
return datetime.fromtimestamp(commit.commit_time, tz=UTC).strftime("%Y-%m-%d")