Show revision numbers on change records, not snapshot indicators

Change record headers now carry the revision of the state they produce
(rN); snapshot indicator lines drop it.
This commit is contained in:
LeoVasanko committed 2026-09-28 01:21:11 +00:00
1 parent b8d2222905
commit 5c32e495e9
4 files changed
+28 -16

No files matched your search

+1 -1
View File
@@ -71,7 +71,7 @@ A revision number identifies a *state position between diffs*, not a diff:
- Change records carry no revision of their own; the revision of a state is counted from the preceding snapshot (`snapshot.r + n` records after it), or from the start of the file.
- Revisions are absolute and survive rotation: the leading snapshot of a rotated file carries the revision of its cutoff state, so `r` values keep referring to the same state positions across history rewrites.
`kanta.rev` exposes the current revision at runtime: the number of change records applied since revision 0, advanced in memory as transactions queue records. Reading it before `open()` raises `DataIntegrityError`. The CLI accepts `rN` range bounds to select records by revision (e.g. `-r r10:r20`): a bound `rN` addresses state N together with the record that produced it — the change record resulting in state N, or, when that change is no longer in the file, the snapshot materializing state N. `-r r5` and `-r r5:r6` therefore show the same record and output state 5, while `-r r5..r6` also includes the next change and outputs state 6. Ranges truncate Python-style instead of erroring: on a rotated file whose leading snapshot holds revision 3, `-r r1:r4` starts at the beginning of the file and shows that snapshot. Snapshot indicator lines show the snapshot's revision.
`kanta.rev` exposes the current revision at runtime: the number of change records applied since revision 0, advanced in memory as transactions queue records. Reading it before `open()` raises `DataIntegrityError`. The CLI accepts `rN` range bounds to select records by revision (e.g. `-r r10:r20`): a bound `rN` addresses state N together with the record that produced it — the change record resulting in state N, or, when that change is no longer in the file, the snapshot materializing state N. `-r r5` and `-r r5:r6` therefore show the same record and output state 5, while `-r r5..r6` also includes the next change and outputs state 6. Ranges truncate Python-style instead of erroring: on a rotated file whose leading snapshot holds revision 3, `-r r1:r4` starts at the beginning of the file and shows that snapshot. Change record headers show the revision of the state the record produces as `rN`.
## CLI Range Filtering
+15 -10
View File
@@ -307,6 +307,7 @@ def _print_change_log(
previous: dict[str, Any],
current: dict[str, Any],
kanta: Kanta[Any],
revision: int,
highlight: Any = None,
) -> None:
"""Log a single change record to stderr.
@@ -314,8 +315,10 @@ def _print_change_log(
The record is dispatched as a :class:`LogEvent` through the Kanta
object's logemit handlers; the CLI's own rendering (with the ``l<N>``
label and timestamp) is the fallback when no handler claims the event.
``highlight`` is an optional :class:`kanta.grep.GrepHighlighter` with
the record's matched regions, applied to the fallback rendering.
The fallback header shows the revision of the state the record produces
as ``r<N>``. ``highlight`` is an optional
:class:`kanta.grep.GrepHighlighter` with the record's matched regions,
applied to the fallback rendering.
"""
event = record_change_event(record, previous, current, kanta)
@@ -328,6 +331,7 @@ def _print_change_log(
lines = format_diff(ev.diff, ev.previous, ev.logfmt, highlight=highlight)
else:
header, lines = ev.header, ev.diff_lines
header += str(Line().target(f" r{revision}"))
if not lines:
_print(f"{label} {ts} {header}")
elif len(lines) == 1:
@@ -400,19 +404,16 @@ def _print_snapshot_indicator(
label: str,
snap: Snapshot,
index: int,
revision: int,
serializer: Serializer,
) -> None:
"""Print a snapshot indicator line to stderr.
``snapshot s<N>`` is rendered in bright white; the version, revision,
optional mtime and data size are printed in normal and dark colors
respectively.
``snapshot s<N>`` is rendered in bright white; the version, optional
mtime and data size are printed in normal and dark colors respectively.
"""
ts = _format_ts(snap.ts)
line = Line().snapshot("snapshot").snapshot(f" s{index}")
line.target(f" v{snap.v}")
line.target(f" r{revision}")
if snap.m is not None:
line.target(f" {_format_ts(snap.m)}")
size = len(serializer.encode(snap.state))
@@ -554,7 +555,6 @@ async def _run(args: argparse.Namespace) -> int:
record_label(snap_event.line_number, snap_event.record_index),
snap_event.snap,
snapshot_line_to_index[snap_event.line_number],
snap_event.revision,
kanta._impl.serializer,
)
_print()
@@ -629,13 +629,18 @@ async def _run(args: argparse.Namespace) -> int:
label,
event.snap,
snapshot_line_to_index[event.line_number],
event.revision,
kanta._impl.serializer,
)
else:
assert previous is not None
_print_change_log(
label, event.record, previous, current, kanta, highlight
label,
event.record,
previous,
current,
kanta,
event.revision,
highlight,
)
if items:
_print()
+4 -1
View File
@@ -105,9 +105,12 @@ def test_cli_snapshot_line_format(tmp_path, capsys, monkeypatch):
# Snapshot line: bright white snapshot/sN, white version/mtime, dark size.
assert "\x1b[97msnapshot s0" in err
assert "\x1b[38;5;250m v1 r0 2026-08-12 09:00:00" in err
assert "\x1b[38;5;250m v1 2026-08-12 09:00:00" in err
assert "\x1b[38;5;242m 13 B" in err
# The change record header carries the revision it produces.
assert "\x1b[38;5;250m r1" in err
def test_cli_strips_ansi_without_color_support(tmp_path, capsys, monkeypatch):
"""Without a tty and with NO_COLOR set, output contains no ANSI codes."""
+8 -4
View File
@@ -335,17 +335,19 @@ def test_cli_revision_snapshot_base_file(tmp_path, capsys):
assert _run_cli(path, "-r", "r1") == 2
assert "r1" in capsys.readouterr().err
# r2 prints the snapshot line (it literally carries r: 2) and its state.
# r2 prints the snapshot line (it literally carries r: 2) and its state;
# revisions are shown on change records, not on snapshot indicators.
assert _run_cli(path, "-r", "r2", "-o", "-") == 0
out = capsys.readouterr()
assert "snapshot s0" in out.err
assert "r2" in out.err
assert "r2" not in out.err
assert json.loads(out.out) == {"foo": 1, "bar": 1}
# r3 is the change, replayed on the snapshot base; state includes baz.
assert _run_cli(path, "-r", "r3", "-o", "-") == 0
out = capsys.readouterr()
assert "add_baz" in out.err
assert " r3" in out.err
assert json.loads(out.out) == {"foo": 1, "bar": 1, "baz": 1}
# Single index and half-open range agree.
@@ -380,6 +382,8 @@ def test_cli_revision_ranges(tmp_path, capsys):
assert _run_cli(path, "-r", "r1") == 0
single = capsys.readouterr().err
assert "bootstrap" in single
# Change record headers carry the revision of the state they produce.
assert " r1" in single
assert "inc0" not in single
assert _run_cli(path, "-r", "r1:r2") == 0
@@ -451,7 +455,7 @@ def test_cli_revision_after_rotation(tmp_path, capsys):
assert _run_cli(path, "-r", "r2", "-o", "-") == 0
out = capsys.readouterr()
assert "snapshot s0" in out.err
assert "r2" in out.err
assert "r2" not in out.err
assert "day-20" not in out.err
assert json.loads(out.out) == {"users": {}, "counter": 1}
@@ -465,7 +469,7 @@ def test_cli_revision_after_rotation(tmp_path, capsys):
assert _run_cli(path, "-r", "r1:r3") == 0
err = capsys.readouterr().err
assert "snapshot s0" in err
assert "r2" in err
assert "r2" not in err
assert "day-20" not in err
# Absolute revisions reach the retained change records.