diff --git a/contextzip/api.py b/contextzip/api.py index 54e4561..1de196b 100644 --- a/contextzip/api.py +++ b/contextzip/api.py @@ -354,8 +354,6 @@ def apply_zip( classify every file as new, modified, unchanged, or one with no safe baseline (locally edited since zipping, or never part of the original manifest). Only adds and modifies files — nothing is ever deleted. - Every overwritten file is backed up first, under - ``.contextzip/backups//``. Unlike the CLI, this applies immediately and does not prompt — check ``contextzip.applier.build_plan()`` yourself first if you want to @@ -375,8 +373,7 @@ def apply_zip( Returns ------- ApplyResult - ``written`` (list of relative paths written), ``backup_dir`` - (``Path`` or ``None`` if nothing needed backing up), and + ``written`` (list of relative paths written) and ``applied_zip_path`` (where the consumed zip ended up). Raises @@ -393,8 +390,6 @@ def apply_zip( result = apply_zip() # picks up .contextzip/inbox/*.zip print(f"Wrote {len(result.written)} files") - if result.backup_dir: - print(f"Backup at {result.backup_dir}") """ from contextzip.applier import ( ApplyError, diff --git a/contextzip/applier.py b/contextzip/applier.py index e3eb47e..3842075 100644 --- a/contextzip/applier.py +++ b/contextzip/applier.py @@ -48,7 +48,6 @@ from pathlib import Path _MANIFEST_SUFFIX = ".manifest.json" _INBOX_DIRNAME = "inbox" _APPLIED_DIRNAME = "applied" -_BACKUPS_DIRNAME = "backups" _OUTPUT_DIRNAME = "output" # must match packager._OUTPUT_DIRNAME # Files contextzip itself writes into outgoing ZIPs that should never be @@ -146,7 +145,6 @@ class ApplyPlan: @dataclass class ApplyResult: written: list[str] - backup_dir: Path | None applied_zip_path: Path @@ -647,29 +645,6 @@ def discard_plan(plan: ApplyPlan) -> None: # --------------------------------------------------------------------------- -def _backup_entries( - entries: list[ApplyEntry], project_dir: Path, workspace_root: Path -) -> Path | None: - """ - Copy the current, pre-apply version of every existing file about to be - touched into .contextzip/backups//, preserving relative - paths. Files that don't exist yet locally (new files) have nothing to - back up. Returns the backup directory, or None if nothing needed one. - """ - to_backup = [e for e in entries if (project_dir / e.rel_path).is_file()] - if not to_backup: - return None - - stamp = time.strftime("%Y%m%d-%H%M%S") - backup_dir = workspace_root / _BACKUPS_DIRNAME / stamp - for entry in to_backup: - src = project_dir / entry.rel_path - dst = backup_dir / entry.rel_path - dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src, dst) - return backup_dir - - def _move_zip_to_applied( zip_path: Path, project_dir: Path, workspace_root: Path, retain: int = 1 ) -> Path: @@ -716,9 +691,14 @@ def execute_plan(plan: ApplyPlan, project_dir: Path, retain: int = 1) -> ApplyRe """ Write every non-unchanged entry from *plan* into the project. - Backs up whatever's about to be overwritten first. Assumes the caller - has already decided to proceed — dry-run and confirmation prompts are - handled upstream (see cli.py's `apply-zip` command). + Assumes the caller has already decided to proceed — dry-run and + confirmation prompts are handled upstream (see cli.py's `apply-zip` + command). Files are overwritten directly, with no backup taken first: + `build_plan`'s manifest diff and the confirmation prompt it drives + (see `ApplyPlan.is_risky`) are the safety net here, and the zip you + just applied is itself always recoverable — it's archived to + `.contextzip/inbox/applied/` (or left wherever you pointed at it) + rather than deleted. *retain* caps how many applied zips are kept in .contextzip/inbox/applied/ — typically a project's @@ -727,8 +707,6 @@ def execute_plan(plan: ApplyPlan, project_dir: Path, retain: int = 1) -> ApplyRe workspace_root = _workspace_dir(project_dir) to_write = plan.writable_entries - backup_dir = _backup_entries(to_write, project_dir, workspace_root) - written: list[str] = [] for entry in to_write: dest = project_dir / entry.rel_path @@ -741,6 +719,4 @@ def execute_plan(plan: ApplyPlan, project_dir: Path, retain: int = 1) -> ApplyRe ) shutil.rmtree(plan.extraction_dir, ignore_errors=True) - return ApplyResult( - written=written, backup_dir=backup_dir, applied_zip_path=applied_zip_path - ) + return ApplyResult(written=written, applied_zip_path=applied_zip_path) diff --git a/contextzip/cleanup.py b/contextzip/cleanup.py index f0272c7..58ab455 100644 --- a/contextzip/cleanup.py +++ b/contextzip/cleanup.py @@ -2,21 +2,16 @@ cleanup.py — Automatic workspace housekeeping for contextzip. .contextzip/ accumulates disposable artifacts over time: a zip+manifest+report -set every run, a timestamped backup folder every risky `apply-zip`, and (until -pruned) old archived applied-zips. None of it is precious — every zip is -trivially reproducible by just re-running contextzip, and a backup only -matters until the apply that made it is confirmed good. So cleanup here is -deliberately brutal rather than cautious: it runs automatically after every -successful command (see `_auto_cleanup` in cli.py), with no prompt and no -separate command to remember, and keeps only the most recent -`cleanup.keep_recent` item(s) in each category — everything else is deleted -immediately, every time. +set every run, and (until pruned) old archived applied-zips and loose apply +reports. None of it is precious — every zip is trivially reproducible by just +re-running contextzip. So cleanup here is deliberately brutal rather than +cautious: it runs automatically after every successful command (see +`_auto_cleanup` in cli.py), with no prompt and no separate command to +remember, and keeps only the most recent `cleanup.keep_recent` item(s) in +each category — everything else is deleted immediately, every time. -The heavier caution used elsewhere in contextzip (backing up before overwrite -in apply-zip, never inferring deletions) is reserved for the project's own -files, where losing something can mean losing real work. Nothing here ever -touches the project — only disposable metadata under .contextzip/ that costs -nothing to regenerate. +Nothing here ever touches the project — only disposable metadata under +.contextzip/ that costs nothing to regenerate. Never touched, under any settings: - .contextzip/config.json (project preferences) @@ -28,7 +23,6 @@ Never touched, under any settings: Kept only-the-most-recent-N, everything older deleted immediately: - zip/manifest/report sets under .contextzip/output//, per mode folder - - timestamped folders under .contextzip/backups/ - archived zips under .contextzip/inbox/applied/ (this is also governed by `applied_zip_retention`, already enforced on every apply-zip run — scanning it here too just catches the case where zips piled up before @@ -40,6 +34,12 @@ Kept only-the-most-recent-N, everything older deleted immediately: put it; its report always lands in inbox/ itself), so nothing else here would ever catch them piling up run after run +Removed outright, no matter how recent: + - .contextzip/backups/ in its entirety. apply-zip no longer creates + per-run backups there at all — this only ever catches a leftover + folder from a project that used an older contextzip version, and it's + dead weight the moment that version is gone. + Scanning only ever touches .contextzip/ directory listings and file mtimes — no hashing, no reading file contents — so this stays fast even called on every command, on a large project. @@ -53,7 +53,7 @@ from pathlib import Path _OUTPUT_DIRNAME = "output" -_BACKUPS_DIRNAME = "backups" +_LEGACY_BACKUPS_DIRNAME = "backups" _INBOX_DIRNAME = "inbox" _APPLIED_DIRNAME = "applied" @@ -66,7 +66,7 @@ _APPLIED_DIRNAME = "applied" @dataclass class CleanupItem: path: Path - kind: str # "zip-set" | "backup" | "applied-zip" + kind: str # "zip-set" | "applied-zip" | "apply-report" | "legacy-backups" size_bytes: int @@ -166,14 +166,14 @@ def scan( for f in _zip_set_files(zp): items.append(CleanupItem(path=f, kind="zip-set", size_bytes=_size_of(f))) - # ── backups// ───────────────────────────────────────────── - backups_root = workspace / _BACKUPS_DIRNAME + # ── backups/ — the feature that created these is gone entirely; wipe + # the whole folder outright rather than pruning it, so upgrading + # from an older contextzip version doesn't leave it behind forever ── + backups_root = workspace / _LEGACY_BACKUPS_DIRNAME if backups_root.is_dir(): - backups = _newest_first([p for p in backups_root.iterdir() if p.is_dir()]) - for backup_dir in backups[keep:]: - items.append( - CleanupItem(path=backup_dir, kind="backup", size_bytes=_size_of(backup_dir)) - ) + items.append( + CleanupItem(path=backups_root, kind="legacy-backups", size_bytes=_size_of(backups_root)) + ) # ── inbox/applied/ ─────────────────────────────────────────────────── applied_root = workspace / _INBOX_DIRNAME / _APPLIED_DIRNAME diff --git a/contextzip/cli.py b/contextzip/cli.py index c48f62c..7897f12 100644 --- a/contextzip/cli.py +++ b/contextzip/cli.py @@ -382,8 +382,7 @@ def cmd_apply_zip( every file as new, modified, unchanged, or one needing a closer look (edited locally since zipping, or with no baseline at all) before writing anything. Only adds and modifies files — nothing is ever - deleted. Every overwritten file is backed up first, under - .contextzip/backups//. + deleted. """ project_dir = Path(os.getcwd()).resolve() project_cfg = load_project_config(project_dir) @@ -857,9 +856,11 @@ def _auto_cleanup(project_dir: Path, project_cfg) -> None: to remember. Every zip is trivially reproducible by re-running contextzip, so this is deliberately brutal rather than cautious: it keeps only the `cleanup.keep_recent` most recent zip/manifest/report - set per mode folder, the most recent `cleanup.keep_recent` backup - folder(s), and the most recent `cleanup.keep_recent` archived - applied-zip(s) — everything else is deleted immediately, every run. + set per mode folder, the most recent `cleanup.keep_recent` archived + applied-zip(s), and the most recent `cleanup.keep_recent` loose + apply-report(s) in inbox/ — everything else is deleted immediately, + every run. Also wipes .contextzip/backups/ outright if a leftover one + still exists from an older contextzip version. Gated on `cleanup.enabled` (default True) — set to False in .contextzip/config.json to turn this off entirely. diff --git a/contextzip/cli_display.py b/contextzip/cli_display.py index e733e40..a4bc3c2 100644 --- a/contextzip/cli_display.py +++ b/contextzip/cli_display.py @@ -380,10 +380,8 @@ def print_apply_plan( def print_apply_result(result, *, con: Console = console) -> None: - """Two or three lines: what was written, backed up, and archived.""" + """Two lines: what was written and archived.""" ok(f"Applied {len(result.written)} files", con=con) - if result.backup_dir: - ok("Backed up to", str(result.backup_dir), con=con) ok("Archived zip to", str(result.applied_zip_path), con=con) diff --git a/contextzip/project_config.py b/contextzip/project_config.py index 5938b20..f05c218 100644 --- a/contextzip/project_config.py +++ b/contextzip/project_config.py @@ -110,24 +110,25 @@ built-in default for the newer preference fields. cleanup Automatic housekeeping for `.contextzip/`, which otherwise only - grows over time (a zip+manifest+report set per run, a timestamped - folder under backups/ per risky apply-zip). See cleanup.py — after - every successful contextzip command, the workspace is pruned down - to only the most recent `keep_recent` item(s) per category, no - confirmation, no separate command needed. This is deliberately - brutal rather than cautious: every zip is trivially reproducible - by re-running contextzip, so there's little reason to let old ones - pile up. + grows over time (a zip+manifest+report set per run, archived zips + under inbox/applied/, loose apply-report.txt files in inbox/). See + cleanup.py — after every successful contextzip command, the + workspace is pruned down to only the most recent `keep_recent` + item(s) per category, no confirmation, no separate command needed. + This is deliberately brutal rather than cautious: every zip is + trivially reproducible by re-running contextzip, so there's little + reason to let old ones pile up. enabled — if true (default), auto-cleanup runs after every successful command. Set to false to keep everything contextzip has ever generated. keep_recent — how many most-recent item(s) are kept per category before the rest are deleted: zip+manifest +report sets per mode folder (output/codebase/, - output/git-changes/, output/prompt/), timestamped - folders under backups/, and archived zips under - inbox/applied/. Default 1 — keep only the latest - of each, delete everything older immediately. + output/git-changes/, output/prompt/), archived + zips under inbox/applied/, and loose + apply-report.txt files in inbox/. Default 1 — + keep only the latest of each, delete everything + older immediately. The schema is intentionally a flat, easily-extended dict so future preferences can be added without another migration. diff --git a/contextzip/report.py b/contextzip/report.py index 67feb78..10d6ec0 100644 --- a/contextzip/report.py +++ b/contextzip/report.py @@ -175,7 +175,6 @@ def write_apply_report( if result is not None: w("Result") w(f" written : {len(result.written)}") - w(f" backup : {result.backup_dir or 'none needed'}") w(f" archived: {result.applied_zip_path}") w("")