From 5ce0af793b74653e17d478000a72d68f35b791cb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:22:33 +0000 Subject: [PATCH] feat: Make snapshot functionality project-aware This commit refactors the snapshot functionality to be project-aware. Snapshots are now stored in project-specific subdirectories under `.codesage/snapshots/`. The following commands have been updated to support project-specific snapshots: - `snapshot` - `diff` - `governance-plan` - `llm-suggest` - `jules-prompt` These commands now require a `--project` option to specify the project context. The `snapshot create` command can also override the project name with an optional `--project` flag. The `history-*` commands were investigated and found to be using a separate, legacy snapshot system that is already project-aware. Therefore, they were not modified. Additionally, the repository has been cleaned up by removing temporary test files and adding them to the `.gitignore` file. Test fixtures have been moved to the `tests/fixtures` directory. --- .gitignore | 4 + codesage.db | Bin 32768 -> 0 bytes codesage/cli/commands/diff.py | 5 +- codesage/cli/commands/governance_plan.py | 33 +++--- codesage/cli/commands/jules_prompt.py | 35 +++++-- codesage/cli/commands/llm_suggest.py | 17 +-- codesage/cli/commands/snapshot.py | 78 +++++++------- codesage/snapshot/versioning.py | 33 ++++-- command-test.md | 126 +++++++++++++++++++++++ tests/fixtures/project-a/main.py | 5 + tests/fixtures/project-a/utils.py | 0 tests/fixtures/project-b/main.py | 0 tests/fixtures/project-b/utils.py | 0 13 files changed, 263 insertions(+), 73 deletions(-) delete mode 100644 codesage.db create mode 100644 command-test.md create mode 100644 tests/fixtures/project-a/main.py create mode 100644 tests/fixtures/project-a/utils.py create mode 100644 tests/fixtures/project-b/main.py create mode 100644 tests/fixtures/project-b/utils.py diff --git a/.gitignore b/.gitignore index 61a8573..fdfbf27 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,7 @@ dmypy.json .codesage/ report.md report.json +codesage.db +plan.yml +task.yml +enriched_snapshot.yml diff --git a/codesage.db b/codesage.db deleted file mode 100644 index cd330639b3c66b09815319d67b6def4a4136b8d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)+i%lW90zbamnL!IxIo(oO{j-#h*qM^juT*h*+T2GZf&4xGJ!T^xru2FiJjRF zqeAOc;APVOgh`Y3JZbOK-uJqHU}BGZ8)G+aV>?cIav2z*WKFc@choe_as0*SqZ8N1 z!DnrCS$8ya#k8ubgG6eaqG{?ageZzi;YS!hND1Ht_dyCx7GCh2qE24_l;s{!!RQwh z7v#QU@5a8yi%>uS1V8`;KmY_l00ck)1fCVR#Rfx(W5?**mmPJzqLsCpW|TFfq-%EP z^4NS{&K6`;$evx6QRf;o!$kBlTFMpVb8;T#&KFRwxV(GK$x>eGY zD|$sk?`8AzZ)fu}86h?6ZtJMlhUR#z+p2YJz&GB1A-}Ye&0j+A%9qfLUT)Map3lom z=W=e{Ovk>=qP)B)=jGhIyoN}<-Pnqmy)_pIB@Q2^?_?Stx^34ryZJlVWzekaqxEjH zm73~ocD20=u%a89V$`eanni#cuG)3Y(w(i=hN-53RmX|gnpg^av%aj^B}=b4x@n+- ze6iqW{AXUT`a_B7Y5LA&!>h&FS_pKhwW?nLSF_BkTFJB5IC@pH9kp6R3%ExXmR4l9 zJtebR)g5J1wcDQQX7krA)hM-VQ<+SocUZc8O|eU+rFGg?HOJCRHhO36eC}nO+B@6T zt#+$PuiAN&4keBpp>M4=l*n50?CUZjwJ#tt)N1<^;5MkM+mtL#b+odA$J>MmEy#=6 z;&K7a7xTF17ZlfRt&m;0;MK@tZfUhBqZ#*~nq_8R_l+gykI~uc&p$ zY`j*;E-M0A^5a8IV{Fgj1)BST!aqT<9 ztZtDD04Y9|nG=M$l!%11#J?elX$H58=I&GY2MP#)00@8p2!H?xfB*=900@8p2!O!8 z5n%mCBkfB9LEJ8yd+fF@+k6g^E(P8So+dy`MrwtTvA#k`8|VYcUZfnUKlSnI|HDw`!6-;#@$ z@Dx($IKIEMq}jGoRvq>D!X!zV>L+D_q)hgca)_i%^pkRsq#WuetD6t0hyvurcvm14X*rEXj^%#Vzh>2qgeB!lfeBT6!2y=O2aBiegLgk&(i zXM{;cr1y*v$q4tJF-9^%y=MeT##rwe0g@5yJ;P5j0=;MWNQS@n44P#4dd_gi|9H|r zC?Eg=AOHd&00JNY0w4eaAOHd&00R41z#ac{)X!A>Df=Y$IQocr7GxW#U@4?>! zzxwa{exdK-)%&IFY;zGRB2rZ3_G)W3gbd;{?raD=e>2%RcWP2XHC`domtl;Ne+0D8di z0T>T}bVf>##6d9P1t3b9%qV;W!d?I=i9faX0K-2BhP(iHNl1+Xz?cVsct+v}@DUh( z4+wez;1i*d_%j&r0uUup9EFd7-wOaA1PAbEaOmd&pZJ;pboXE=Es4CupN{W+7Z|<( z{j5NHay@(=#Kxxp$Am_Z}w^IujmuzapY0>A+CS|0w4eaAOHd&uIPv|Au|$x3fLz0f2izdH~-93?E>^3m`R+%b-Ia s{cIod0uV;zD&Rp6fYfP87{Eoa;rD<89sutA@FQ^ timedelta(days=manager.retention_days) - ] - - sorted_by_date = sorted(index, key=lambda s: s["timestamp"], reverse=True) - expired_by_count = sorted_by_date[manager.max_versions:] + if not index: + click.echo(f"No snapshots to clean up for project '{project}'.") + return - expired = {s['version']: s for s in expired_by_date + expired_by_count}.values() + now = datetime.now(timezone.utc) + expired_snapshots = manager._get_expired_snapshots(index, now) - if not expired: - click.echo("No snapshots to clean up.") + if not expired_snapshots: + click.echo(f"No snapshots to clean up for project '{project}'.") return click.echo("Snapshots to be deleted:") - for s in expired: + for s in expired_snapshots: click.echo(f"- {s['version']}") else: manager.cleanup_expired_snapshots() - click.echo("Expired snapshots have been cleaned up.") + click.echo(f"Expired snapshots for project '{project}' have been cleaned up.") diff --git a/codesage/snapshot/versioning.py b/codesage/snapshot/versioning.py index 47289e1..2420c69 100644 --- a/codesage/snapshot/versioning.py +++ b/codesage/snapshot/versioning.py @@ -10,9 +10,11 @@ class SnapshotVersionManager: """Manages the versioning and lifecycle of project snapshots.""" - def __init__(self, snapshot_dir: str, config: Dict[str, Any]): - self.snapshot_dir = snapshot_dir - self.index_file = os.path.join(snapshot_dir, "index.json") + def __init__(self, snapshot_dir: str, project_name: str, config: Dict[str, Any]): + self.base_snapshot_dir = snapshot_dir + self.project_name = project_name + self.snapshot_dir = os.path.join(self.base_snapshot_dir, self.project_name) + self.index_file = os.path.join(self.snapshot_dir, "index.json") versioning_config = config.get("versioning", {}) self.max_versions = versioning_config.get("max_versions", 10) self.retention_days = versioning_config.get("retention_days", 30) @@ -80,11 +82,8 @@ def _update_index(self, snapshot_path: str, metadata: SnapshotMetadata): ) self._save_index(index) - def cleanup_expired_snapshots(self): - """Removes expired snapshots based on retention days and max versions.""" - index = self._load_index() - now = datetime.now(timezone.utc) - + def _get_expired_snapshots(self, index: List[Dict[str, Any]], now: datetime) -> List[Dict[str, Any]]: + """Identifies expired snapshots.""" valid_snapshots = [] for s in index: try: @@ -102,7 +101,23 @@ def cleanup_expired_snapshots(self): valid_snapshots, key=lambda s: s["timestamp"], reverse=True )[:self.max_versions] - expired_snapshots = [s for s in index if s not in valid_snapshots] + valid_versions = {s["version"] for s in valid_snapshots} + return [s for s in index if s["version"] not in valid_versions] + + def cleanup_expired_snapshots(self): + """Removes expired snapshots based on retention days and max versions.""" + index = self._load_index() + if not index: + return + + now = datetime.now(timezone.utc) + expired_snapshots = self._get_expired_snapshots(index, now) + + if not expired_snapshots: + return + + expired_versions = {s["version"] for s in expired_snapshots} + valid_snapshots = [s for s in index if s["version"] not in expired_versions] for snapshot_data in expired_snapshots: if os.path.exists(snapshot_data["path"]): diff --git a/command-test.md b/command-test.md new file mode 100644 index 0000000..7834c67 --- /dev/null +++ b/command-test.md @@ -0,0 +1,126 @@ +# CodeSage Command Test Plan + +This document outlines the commands to be tested to ensure the new project-aware snapshot functionality is working correctly and that other commands have not been affected. + +## Test Setup + +1. Create two dummy project directories in `tests/fixtures`: `project-a` and `project-b`. +2. Each project should contain a few dummy source files (e.g., `main.py`, `utils.py`). +3. Create a dummy governance plan file `plan.yml` and a dummy task file `task.yml`. + +## Snapshot Command Tests (`codesage snapshot`) + +### Project A + +1. **Create first snapshot for Project A:** + ```bash + poetry run codesage snapshot create ./project-a + ``` + +2. **Create second snapshot for Project A:** + ```bash + # (after making some changes to a file in project-a) + poetry run codesage snapshot create ./project-a + ``` + +3. **List snapshots for Project A:** + ```bash + poetry run codesage snapshot list --project project-a + ``` + *Expected output: Should list v1 and v2.* + +4. **Show snapshot v1 for Project A:** + ```bash + poetry run codesage snapshot show v1 --project project-a + ``` + +### Project B + +1. **Create first snapshot for Project B:** + ```bash + poetry run codesage snapshot create ./project-b + ``` + +2. **List snapshots for Project B:** + ```bash + poetry run codesage snapshot list --project project-b + ``` + *Expected output: Should list only v1.* + +3. **List snapshots for Project A again:** + ```bash + poetry run codesage snapshot list --project project-a + ``` + *Expected output: Should still list v1 and v2, unaffected by Project B.* + +### Cleanup + +1. **Cleanup snapshots for Project A (dry run):** + ```bash + poetry run codesage snapshot cleanup --project project-a --dry-run + ``` + +2. **Cleanup snapshots for Project A:** + ```bash + poetry run codesage snapshot cleanup --project project-a + ``` + +8. **Create a snapshot with an overridden project name:** + ```bash + poetry run codesage snapshot create ./project-a --project project-c + ``` + +9. **List snapshots for the overridden project name:** + ```bash + poetry run codesage snapshot list --project project-c + ``` + *Expected output: Should list v1.* + + +## Diff Command Tests (`codesage diff`) + +1. **Compare two snapshots within Project A:** + ```bash + poetry run codesage diff v1 v2 --project project-a + ``` + +## Other Commands (Regression Testing) + +These commands were not expected to be changed, but we should run them to ensure they still work. + +1. **Analyze:** + ```bash + poetry run codesage analyze tests/fixtures/project-a + ``` + +2. **Config:** + ```bash + poetry run codesage config show + ``` + +3. **Scan:** + ```bash + poetry run codesage scan tests/fixtures/project-a + ``` + +## New Commands Tests + +1. **Governance Plan:** + ```bash + poetry run codesage governance-plan --snapshot-version v1 --project project-a --output plan.yml + ``` + +2. **LLM Suggest:** + ```bash + poetry run codesage llm-suggest --snapshot-version v1 --project project-a --output enriched_snapshot.yml + ``` + +3. **Jules Prompt (with plan):** + ```bash + poetry run codesage jules-prompt --plan plan.yml --task-id --project project-a --snapshot-version v1 + ``` + +4. **Jules Prompt (with task):** + ```bash + poetry run codesage jules-prompt --task task.yml --project project-a --snapshot-version v1 + ``` diff --git a/tests/fixtures/project-a/main.py b/tests/fixtures/project-a/main.py new file mode 100644 index 0000000..f5e910f --- /dev/null +++ b/tests/fixtures/project-a/main.py @@ -0,0 +1,5 @@ +# New content +# New content +# New content +# New content +# New content diff --git a/tests/fixtures/project-a/utils.py b/tests/fixtures/project-a/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/project-b/main.py b/tests/fixtures/project-b/main.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/project-b/utils.py b/tests/fixtures/project-b/utils.py new file mode 100644 index 0000000..e69de29