Skip to content

Commit 5a212bb

Browse files
committed
feat(hooks): add _hooks gc-pair-sync-refs command for stale ref cleanup
1 parent 244e8fb commit 5a212bb

3 files changed

Lines changed: 94 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ done — log files accumulate in `/tmp` and are not rotated.
279279
Override the log directory with `JCLI_DEBUG_LOG_DIR=/path/to/dir` if `/tmp` is
280280
not writable or you want the logs elsewhere.
281281

282+
If `refs/jcli/pair-sync/*` accumulates over time, clean stale entries with:
283+
284+
j-cli _hooks gc-pair-sync-refs
285+
j-cli _hooks gc-pair-sync-refs --dry-run
286+
282287
## Py:Percent Format
283288

284289
j-cli supports the [py:percent](https://jupytext.readthedocs.io/en/latest/formats-scripts.html#the-percent-format) format — plain Python files with cell markers:

jupyter_jcli/commands/hooks_cmd.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,3 +962,30 @@ def _run_pre_commit_pair_sync(include_globs: tuple[str, ...]) -> None:
962962
f"{', '.join(updated_ipynb)}",
963963
file=sys.stderr,
964964
)
965+
966+
967+
@hooks.command("gc-pair-sync-refs")
968+
@click.option("--dry-run", is_flag=True, default=False, help="Report stale refs without deleting them.")
969+
def gc_pair_sync_refs(dry_run: bool) -> None:
970+
"""Delete stale sticky pair-sync refs under refs/jcli/pair-sync."""
971+
from jupyter_jcli import pair_baseline
972+
973+
repo_root = pair_baseline._git_root(Path.cwd())
974+
if repo_root is None:
975+
print("gc-pair-sync-refs: not in a git repo, skipping", file=sys.stderr)
976+
sys.exit(0)
977+
978+
refs = pair_baseline.list_all_refs(repo_root)
979+
for ref_info in refs:
980+
status, reason = pair_baseline._classify_ref(repo_root, ref_info)
981+
rel_display = ref_info.rel_posix_path or "<unknown>"
982+
if status == "keep":
983+
print(f"keep\t{rel_display}\t{reason}", file=sys.stderr)
984+
elif dry_run:
985+
print(f"would-remove\t{rel_display}\t{reason}", file=sys.stderr)
986+
else:
987+
print(f"remove\t{rel_display}\t{reason}", file=sys.stderr)
988+
989+
removed, kept = pair_baseline.gc_stale_refs(repo_root, dry_run)
990+
print(f"removed {removed}, kept {kept}", file=sys.stderr)
991+
sys.exit(0)

tests/test_hooks_pair_drift.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
import pytest
1313
from click.testing import CliRunner
1414

15-
from jupyter_jcli import pair_baseline
1615
from jupyter_jcli.cli import main
1716
from jupyter_jcli.drift import DriftResult
17+
from jupyter_jcli import pair_baseline
1818

1919

2020
# ---------------------------------------------------------------------------
@@ -678,6 +678,67 @@ def test_pre_merge_updates_ref_so_following_post_does_not_conflict(
678678
assert [cell.source for cell in nb_after.cells if cell.source.strip()] == ["x = 40"]
679679

680680

681+
class TestGcPairSyncRefsCLI:
682+
def test_dry_run_reports_without_deleting(self, git_repo: Path, monkeypatch: pytest.MonkeyPatch):
683+
py, _ipynb = _make_pair(git_repo, ["x = 1"], ["x = 1"])
684+
_git(git_repo, "add", "nb.py")
685+
_git(git_repo, "commit", "-m", "init", env=_git_env(100))
686+
687+
monkeypatch.setenv("GIT_AUTHOR_DATE", "@150 +0000")
688+
monkeypatch.setenv("GIT_COMMITTER_DATE", "@150 +0000")
689+
assert pair_baseline.write_baseline(py, py.read_text(encoding="utf-8")) is True
690+
py.write_text(py.read_text(encoding="utf-8").replace("x = 1", "x = 2"), encoding="utf-8")
691+
_git(git_repo, "add", "nb.py")
692+
_git(git_repo, "commit", "-m", "new head", env=_git_env(200))
693+
694+
runner = CliRunner()
695+
monkeypatch.chdir(git_repo)
696+
result = runner.invoke(main, ["_hooks", "gc-pair-sync-refs", "--dry-run"], catch_exceptions=False)
697+
698+
assert result.exit_code == 0
699+
assert "would-remove" in (result.stderr or result.output)
700+
assert "removed 1, kept 0" in (result.stderr or result.output)
701+
refs = _git(git_repo, "for-each-ref", "refs/jcli/pair-sync/", "--format=%(refname)")
702+
assert refs.stdout.strip() != ""
703+
704+
def test_cli_removes_orphan_ref(self, git_repo: Path, monkeypatch: pytest.MonkeyPatch):
705+
_git(git_repo, "commit", "--allow-empty", "-m", "init", env=_git_env(100))
706+
ghost_path = git_repo / "ghost.py"
707+
monkeypatch.setenv("GIT_AUTHOR_DATE", "@150 +0000")
708+
monkeypatch.setenv("GIT_COMMITTER_DATE", "@150 +0000")
709+
assert pair_baseline.write_baseline(ghost_path, "# %%\nx = 1\n") is True
710+
711+
runner = CliRunner()
712+
monkeypatch.chdir(git_repo)
713+
result = runner.invoke(main, ["_hooks", "gc-pair-sync-refs"], catch_exceptions=False)
714+
715+
assert result.exit_code == 0
716+
assert "remove" in (result.stderr or result.output)
717+
refs = _git(git_repo, "for-each-ref", "refs/jcli/pair-sync/", "--format=%(refname)")
718+
assert refs.stdout.strip() == ""
719+
720+
def test_cli_removes_stale_head_older_ref(self, git_repo: Path, monkeypatch: pytest.MonkeyPatch):
721+
py, _ipynb = _make_pair(git_repo, ["x = 1"], ["x = 1"])
722+
_git(git_repo, "add", "nb.py")
723+
_git(git_repo, "commit", "-m", "init", env=_git_env(100))
724+
725+
monkeypatch.setenv("GIT_AUTHOR_DATE", "@150 +0000")
726+
monkeypatch.setenv("GIT_COMMITTER_DATE", "@150 +0000")
727+
assert pair_baseline.write_baseline(py, py.read_text(encoding="utf-8")) is True
728+
py.write_text(py.read_text(encoding="utf-8").replace("x = 1", "x = 5"), encoding="utf-8")
729+
_git(git_repo, "add", "nb.py")
730+
_git(git_repo, "commit", "-m", "head newer", env=_git_env(200))
731+
732+
runner = CliRunner()
733+
monkeypatch.chdir(git_repo)
734+
result = runner.invoke(main, ["_hooks", "gc-pair-sync-refs"], catch_exceptions=False)
735+
736+
assert result.exit_code == 0
737+
assert "removed 1, kept 0" in (result.stderr or result.output)
738+
refs = _git(git_repo, "for-each-ref", "refs/jcli/pair-sync/", "--format=%(refname)")
739+
assert refs.stdout.strip() == ""
740+
741+
681742
# ---------------------------------------------------------------------------
682743
# --debug smoke tests for pair-drift-guard-pre and pair-drift-guard-post
683744
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)