fix(checkpoints): bare /rollback falls back to a labeled all-directories view (#10505, reapply #10633)

This commit is contained in:
Teknium
2026-08-19 20:11:41 -07:00
parent 7bf66ec39b
commit fa89d87ea6
3 changed files with 115 additions and 2 deletions

View File

@@ -88,8 +88,19 @@ class CLICommandsMixin:
args = filtered
if not args:
# List checkpoints
# List checkpoints — fall back to the cross-project view when the
# current directory has none (#10505, reapply of PR #10633 by
# @nightq). The Aug 2026 QA sweep hit this live: writes landed
# checkpoints under the session cwd (/tmp/qa-repo) while bare
# /rollback searched only TERMINAL_CWD's project and reported
# "No checkpoints found" despite fresh checkpoints existing.
checkpoints = mgr.list_checkpoints(cwd)
if not checkpoints:
all_checkpoints = mgr.list_all_checkpoints()
if all_checkpoints:
print(f" No checkpoints for {cwd} — showing all directories.")
print(format_checkpoint_list(all_checkpoints, "all directories"))
return
print(format_checkpoint_list(checkpoints, cwd))
return

View File

@@ -0,0 +1,70 @@
"""Regression tests for the /rollback all-directories fallback (#10505).
Bare /rollback searched only TERMINAL_CWD's project; checkpoints created
under a different session cwd were invisible ("No checkpoints found for
/home/user" while checkpoints existed seconds earlier). Reapply of PR
#10633 by @nightq onto the v2 single-store layout.
"""
import json
import tools.checkpoint_manager as cm
from tools.checkpoint_manager import CheckpointManager, format_checkpoint_list
def _make_store_with_project(tmp_path, workdir: str):
store = tmp_path / "store"
(store / cm._PROJECTS_DIRNAME).mkdir(parents=True)
(store / "HEAD").write_text("ref: refs/heads/main\n")
dir_hash = cm._project_hash(workdir)
meta = {"workdir": workdir, "created_at": 1, "last_touch": 2}
(store / cm._PROJECTS_DIRNAME / f"{dir_hash}.json").write_text(json.dumps(meta))
return store
class TestListAllCheckpoints:
def test_aggregates_projects_and_labels_workdir(self, tmp_path, monkeypatch):
workdir = str(tmp_path / "proj")
store = _make_store_with_project(tmp_path, workdir)
monkeypatch.setattr(cm, "CHECKPOINT_BASE", tmp_path)
monkeypatch.setattr(cm, "_store_path", lambda base=None: store)
mgr = CheckpointManager(enabled=True)
fake_entries = [
{"hash": "a" * 40, "short_hash": "aaaaaaa", "timestamp": "2026-08-19T10:00:00",
"reason": "before write_file", "files_changed": 1, "insertions": 2, "deletions": 0},
]
monkeypatch.setattr(
CheckpointManager, "list_checkpoints", lambda self, wd: list(fake_entries)
)
results = mgr.list_all_checkpoints()
assert len(results) == 1
assert results[0]["workdir"] == workdir
def test_empty_store_returns_empty(self, tmp_path, monkeypatch):
monkeypatch.setattr(cm, "CHECKPOINT_BASE", tmp_path)
monkeypatch.setattr(cm, "_store_path", lambda base=None: tmp_path / "missing")
assert CheckpointManager(enabled=True).list_all_checkpoints() == []
class TestFormatAllDirectories:
def test_workdir_label_shown_in_all_directories_view(self):
cps = [{
"hash": "b" * 40, "short_hash": "bbbbbbb",
"timestamp": "2026-08-19T10:00:00", "reason": "before patch",
"files_changed": 0, "insertions": 0, "deletions": 0,
"workdir": "/tmp/qa-repo",
}]
out = format_checkpoint_list(cps, "all directories")
assert "[qa-repo]" in out
def test_single_directory_view_unchanged(self):
cps = [{
"hash": "c" * 40, "short_hash": "ccccccc",
"timestamp": "2026-08-19T10:00:00", "reason": "before patch",
"files_changed": 0, "insertions": 0, "deletions": 0,
}]
out = format_checkpoint_list(cps, "/tmp/qa-repo")
assert "[qa-repo]" not in out
assert "ccccccc" in out

View File

@@ -968,6 +968,29 @@ class CheckpointManager:
results.append(entry)
return results
def list_all_checkpoints(self) -> List[Dict]:
"""List checkpoints across every registered project (most recent first).
Surgical reapply of PR #10633 by @nightq (#10505) onto the v2
single-store layout: iterate ``projects/<hash>.json`` metadata via
``_list_projects`` instead of the pre-v2 per-shadow-dir scan. Each
entry carries the extra ``workdir`` key so callers can label which
project a checkpoint belongs to.
"""
store = _store_path(CHECKPOINT_BASE)
if not (store / "HEAD").exists():
return []
results: List[Dict] = []
for meta in _list_projects(store):
workdir = meta.get("workdir") or ""
if not workdir:
continue
for entry in self.list_checkpoints(workdir):
entry["workdir"] = workdir
results.append(entry)
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
return results
@staticmethod
def _parse_shortstat(stat_line: str, entry: Dict) -> None:
"""Parse git --shortstat output into entry dict."""
@@ -1563,7 +1586,16 @@ def format_checkpoint_list(checkpoints: List[Dict], directory: str) -> str:
else:
stat = ""
lines.append(f" {i}. {cp['short_hash']} {ts} {cp['reason']}{stat}")
# Label per-project entries when showing the cross-project view
# (workdir key only present on list_all_checkpoints results).
workdir = cp.get("workdir", "")
if workdir and directory == "all directories":
workdir_short = Path(workdir).name or workdir
lines.append(
f" {i}. {cp['short_hash']} {ts} [{workdir_short}] {cp['reason']}{stat}"
)
else:
lines.append(f" {i}. {cp['short_hash']} {ts} {cp['reason']}{stat}")
lines.append("\n /rollback <N> restore to checkpoint N")
lines.append(" /rollback diff <N> preview changes since checkpoint N")