continuous-refactoring 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/PKG-INFO +32 -4
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/README.md +31 -3
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/pyproject.toml +1 -1
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/cli.py +182 -9
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/config.py +33 -1
- continuous_refactoring-0.2.0/tests/test_cli_init_taste.py +695 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_cli_taste_warning.py +57 -1
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_config.py +168 -1
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_taste_interview.py +31 -0
- continuous_refactoring-0.1.0/tests/test_cli_init_taste.py +0 -288
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/.gitignore +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/LICENSE +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/__init__.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/__main__.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/agent.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/artifacts.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/commit_messages.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/decisions.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/effort.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/failure_report.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/git.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/loop.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/migration_manifest_codec.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/migration_tick.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/migrations.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/phases.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/planning.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/prompts.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/refactor_attempts.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/review_cli.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/routing.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/routing_pipeline.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/scope_candidates.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/scope_expansion.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/targeting.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/conftest.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/fixtures/claude_stream_json/selection.stdout.log +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_claude_stream_json.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_cli_review.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_cli_upgrade.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_commit_messages.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_continuous_refactoring.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_decisions.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_e2e.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_effort.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_failure_report.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_focus_on_live_migrations.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_git.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_loop_migration_tick.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_main_entrypoint.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_migrations.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_no_driver_branching.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_phases.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_planning.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_prompts.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_prompts_scope_selection.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_routing.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_run.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_run_once.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_run_once_regression.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_scope_candidates.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_scope_expansion.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_scope_loop_integration.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_scope_selection.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_targeting.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_taste_refine.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_taste_upgrade.py +0 -0
- {continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/tests/test_wake_up.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: continuous-refactoring
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Continuous refactoring loop for AI coding agents
|
|
5
5
|
Project-URL: Repository, https://github.com/bigH/continuous-refactoring
|
|
6
6
|
Project-URL: Issues, https://github.com/bigH/continuous-refactoring/issues
|
|
@@ -23,16 +23,41 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
# continuous-refactoring
|
|
25
25
|
|
|
26
|
+
[](https://github.com/bigH/continuous-refactoring)
|
|
27
|
+
[](https://pypi.org/project/continuous-refactoring/)
|
|
28
|
+
[](https://github.com/bigH/continuous-refactoring/actions/workflows/test.yml)
|
|
29
|
+
[](https://github.com/bigH/continuous-refactoring/blob/main/LICENSE)
|
|
30
|
+
|
|
26
31
|
Small, test-gated cleanup commits by an AI coding agent.
|
|
27
32
|
|
|
28
33
|
Think of it as a supervised janitor loop: the agent proposes a cleanup, your tests decide if it stays.
|
|
29
34
|
|
|
30
35
|
## Install
|
|
31
36
|
|
|
37
|
+
Try it without installing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx continuous-refactoring --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install it with [uv](https://docs.astral.sh/uv/guides/tools/):
|
|
44
|
+
|
|
32
45
|
```bash
|
|
33
46
|
uv tool install continuous-refactoring
|
|
34
47
|
```
|
|
35
48
|
|
|
49
|
+
Or with [pipx](https://pypa.github.io/pipx/):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pipx install continuous-refactoring
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or with pip:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install continuous-refactoring
|
|
59
|
+
```
|
|
60
|
+
|
|
36
61
|
For a checkout:
|
|
37
62
|
|
|
38
63
|
```bash
|
|
@@ -89,6 +114,8 @@ That keeps sweeping targets until it runs out, hits your caps, or starts failing
|
|
|
89
114
|
```bash
|
|
90
115
|
# 1. Register the repo (creates a project dir under ~/.local/share/continuous-refactoring)
|
|
91
116
|
continuous-refactoring init
|
|
117
|
+
# Or keep project taste in the repo:
|
|
118
|
+
continuous-refactoring init --in-repo-taste
|
|
92
119
|
|
|
93
120
|
# 2. (Optional) Write your refactoring taste — either edit the file, have an agent interview you,
|
|
94
121
|
# or refine an existing draft collaboratively
|
|
@@ -113,7 +140,7 @@ continuous-refactoring run \
|
|
|
113
140
|
|
|
114
141
|
| Command | What it does |
|
|
115
142
|
|---|---|
|
|
116
|
-
| `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir`. |
|
|
143
|
+
| `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir` or `--in-repo-taste`. |
|
|
117
144
|
| `taste` | Prints the active taste file path. Add `--interview` to have an agent author it, `--refine` to iteratively improve an existing taste doc, `--upgrade` to refresh stale taste dimensions, `--global` for the shared file, and `--force` to let `--interview` overwrite custom content after writing a `.bak`. |
|
|
118
145
|
| `run-once` | Single pass on one resolved target. No retry. If there is a diff and validation passes, it commits locally and prints the diffstat. |
|
|
119
146
|
| `run` | The loop. Iterates targets, retries on failure, and commits successful targets locally. |
|
|
@@ -141,6 +168,7 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
|
|
|
141
168
|
### Migrations & taste flags
|
|
142
169
|
|
|
143
170
|
- `init --live-migrations-dir PATH` — enables the larger-refactoring workflow for this project. The path is stored repo-relative in the project registry and created if missing.
|
|
171
|
+
- `init --in-repo-taste [PATH]` — stores this project's taste file in the repo and remembers the repo-relative path. Defaults to `.continuous-refactoring/taste.md`; re-run `init --in-repo-taste ...` to choose a different path.
|
|
144
172
|
- `taste --refine` — opens a collaborative editing session for the taste file. The agent keeps refining until you tell it to write, then the session ends automatically after the settled write.
|
|
145
173
|
- `taste --upgrade` — re-interviews for taste dimensions added since your last version. No-op when already current; use `taste --refine` if you want to rework the doc anyway.
|
|
146
174
|
- `taste --force` — only applies to `--interview`; it allows a customized taste file to be overwritten after backing it up to `taste.md.bak`.
|
|
@@ -191,10 +219,10 @@ The path prints at startup. Grep it when something goes sideways.
|
|
|
191
219
|
|
|
192
220
|
The taste file is a short bullet list of your refactoring preferences. It gets injected into every agent prompt.
|
|
193
221
|
|
|
194
|
-
- Project taste: `~/.local/share/continuous-refactoring/projects/<uuid>/taste.md`
|
|
222
|
+
- Project taste: `~/.local/share/continuous-refactoring/projects/<uuid>/taste.md`, or the repo-local path chosen with `init --in-repo-taste [PATH]`
|
|
195
223
|
- Global taste: `~/.local/share/continuous-refactoring/global/taste.md`
|
|
196
224
|
|
|
197
|
-
Project taste wins over global. Use `taste --interview` to bootstrap one, `taste --refine` to rework it with an agent, or edit the file directly any time.
|
|
225
|
+
Project taste wins over global. Use `taste` to print the active path, `taste --interview` to bootstrap one, `taste --refine` to rework it with an agent, or edit the file directly any time.
|
|
198
226
|
|
|
199
227
|
## Larger refactorings
|
|
200
228
|
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
# continuous-refactoring
|
|
2
2
|
|
|
3
|
+
[](https://github.com/bigH/continuous-refactoring)
|
|
4
|
+
[](https://pypi.org/project/continuous-refactoring/)
|
|
5
|
+
[](https://github.com/bigH/continuous-refactoring/actions/workflows/test.yml)
|
|
6
|
+
[](https://github.com/bigH/continuous-refactoring/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
Small, test-gated cleanup commits by an AI coding agent.
|
|
4
9
|
|
|
5
10
|
Think of it as a supervised janitor loop: the agent proposes a cleanup, your tests decide if it stays.
|
|
6
11
|
|
|
7
12
|
## Install
|
|
8
13
|
|
|
14
|
+
Try it without installing:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvx continuous-refactoring --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install it with [uv](https://docs.astral.sh/uv/guides/tools/):
|
|
21
|
+
|
|
9
22
|
```bash
|
|
10
23
|
uv tool install continuous-refactoring
|
|
11
24
|
```
|
|
12
25
|
|
|
26
|
+
Or with [pipx](https://pypa.github.io/pipx/):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install continuous-refactoring
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with pip:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install continuous-refactoring
|
|
36
|
+
```
|
|
37
|
+
|
|
13
38
|
For a checkout:
|
|
14
39
|
|
|
15
40
|
```bash
|
|
@@ -66,6 +91,8 @@ That keeps sweeping targets until it runs out, hits your caps, or starts failing
|
|
|
66
91
|
```bash
|
|
67
92
|
# 1. Register the repo (creates a project dir under ~/.local/share/continuous-refactoring)
|
|
68
93
|
continuous-refactoring init
|
|
94
|
+
# Or keep project taste in the repo:
|
|
95
|
+
continuous-refactoring init --in-repo-taste
|
|
69
96
|
|
|
70
97
|
# 2. (Optional) Write your refactoring taste — either edit the file, have an agent interview you,
|
|
71
98
|
# or refine an existing draft collaboratively
|
|
@@ -90,7 +117,7 @@ continuous-refactoring run \
|
|
|
90
117
|
|
|
91
118
|
| Command | What it does |
|
|
92
119
|
|---|---|
|
|
93
|
-
| `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir`. |
|
|
120
|
+
| `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir` or `--in-repo-taste`. |
|
|
94
121
|
| `taste` | Prints the active taste file path. Add `--interview` to have an agent author it, `--refine` to iteratively improve an existing taste doc, `--upgrade` to refresh stale taste dimensions, `--global` for the shared file, and `--force` to let `--interview` overwrite custom content after writing a `.bak`. |
|
|
95
122
|
| `run-once` | Single pass on one resolved target. No retry. If there is a diff and validation passes, it commits locally and prints the diffstat. |
|
|
96
123
|
| `run` | The loop. Iterates targets, retries on failure, and commits successful targets locally. |
|
|
@@ -118,6 +145,7 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
|
|
|
118
145
|
### Migrations & taste flags
|
|
119
146
|
|
|
120
147
|
- `init --live-migrations-dir PATH` — enables the larger-refactoring workflow for this project. The path is stored repo-relative in the project registry and created if missing.
|
|
148
|
+
- `init --in-repo-taste [PATH]` — stores this project's taste file in the repo and remembers the repo-relative path. Defaults to `.continuous-refactoring/taste.md`; re-run `init --in-repo-taste ...` to choose a different path.
|
|
121
149
|
- `taste --refine` — opens a collaborative editing session for the taste file. The agent keeps refining until you tell it to write, then the session ends automatically after the settled write.
|
|
122
150
|
- `taste --upgrade` — re-interviews for taste dimensions added since your last version. No-op when already current; use `taste --refine` if you want to rework the doc anyway.
|
|
123
151
|
- `taste --force` — only applies to `--interview`; it allows a customized taste file to be overwritten after backing it up to `taste.md.bak`.
|
|
@@ -168,10 +196,10 @@ The path prints at startup. Grep it when something goes sideways.
|
|
|
168
196
|
|
|
169
197
|
The taste file is a short bullet list of your refactoring preferences. It gets injected into every agent prompt.
|
|
170
198
|
|
|
171
|
-
- Project taste: `~/.local/share/continuous-refactoring/projects/<uuid>/taste.md`
|
|
199
|
+
- Project taste: `~/.local/share/continuous-refactoring/projects/<uuid>/taste.md`, or the repo-local path chosen with `init --in-repo-taste [PATH]`
|
|
172
200
|
- Global taste: `~/.local/share/continuous-refactoring/global/taste.md`
|
|
173
201
|
|
|
174
|
-
Project taste wins over global. Use `taste --interview` to bootstrap one, `taste --refine` to rework it with an agent, or edit the file directly any time.
|
|
202
|
+
Project taste wins over global. Use `taste` to print the active path, `taste --interview` to bootstrap one, `taste --refine` to rework it with an agent, or edit the file directly any time.
|
|
175
203
|
|
|
176
204
|
## Larger refactorings
|
|
177
205
|
|
{continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/cli.py
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import shutil
|
|
4
5
|
import sys
|
|
6
|
+
import uuid
|
|
5
7
|
from collections.abc import Callable
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
@@ -102,6 +104,8 @@ def _add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
|
102
104
|
|
|
103
105
|
|
|
104
106
|
def _add_init_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
107
|
+
from continuous_refactoring.config import DEFAULT_REPO_TASTE_PATH
|
|
108
|
+
|
|
105
109
|
init_parser = subparsers.add_parser(
|
|
106
110
|
"init",
|
|
107
111
|
help="Register a project for continuous refactoring.",
|
|
@@ -119,6 +123,23 @@ def _add_init_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
119
123
|
default=None,
|
|
120
124
|
help="Directory for live migration artifacts (repo-relative path).",
|
|
121
125
|
)
|
|
126
|
+
init_parser.add_argument(
|
|
127
|
+
"--in-repo-taste",
|
|
128
|
+
type=Path,
|
|
129
|
+
nargs="?",
|
|
130
|
+
const=Path(DEFAULT_REPO_TASTE_PATH),
|
|
131
|
+
default=None,
|
|
132
|
+
metavar="PATH",
|
|
133
|
+
help=(
|
|
134
|
+
"Store this project's taste file in the repo. "
|
|
135
|
+
f"Defaults to {DEFAULT_REPO_TASTE_PATH}."
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
init_parser.add_argument(
|
|
139
|
+
"--force",
|
|
140
|
+
action="store_true",
|
|
141
|
+
help="Replace destination state when reconfiguring taste or live migrations.",
|
|
142
|
+
)
|
|
122
143
|
|
|
123
144
|
|
|
124
145
|
def _add_taste_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -273,16 +294,39 @@ def _handle_init(args: argparse.Namespace) -> None:
|
|
|
273
294
|
from continuous_refactoring.config import (
|
|
274
295
|
ensure_taste_file,
|
|
275
296
|
register_project,
|
|
297
|
+
resolve_live_migrations_dir,
|
|
298
|
+
resolve_project,
|
|
299
|
+
resolve_project_taste_path,
|
|
276
300
|
set_live_migrations_dir,
|
|
301
|
+
set_repo_taste_path,
|
|
277
302
|
)
|
|
278
303
|
|
|
279
304
|
path = (args.path or Path.cwd()).resolve()
|
|
305
|
+
in_repo_taste_arg: Path | None = getattr(args, "in_repo_taste", None)
|
|
306
|
+
live_dir_arg: Path | None = getattr(args, "live_migrations_dir", None)
|
|
307
|
+
force = bool(getattr(args, "force", False))
|
|
308
|
+
repo_taste_relative: str | None = None
|
|
309
|
+
repo_taste_resolved: Path | None = None
|
|
310
|
+
resolved_live: Path | None = None
|
|
311
|
+
live_dir_relative: str | None = None
|
|
312
|
+
|
|
280
313
|
try:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
314
|
+
if in_repo_taste_arg is not None:
|
|
315
|
+
repo_taste_resolved = (path / in_repo_taste_arg).resolve()
|
|
316
|
+
if not repo_taste_resolved.is_relative_to(path):
|
|
317
|
+
print(
|
|
318
|
+
f"Error: --in-repo-taste must be inside the repo: {in_repo_taste_arg}",
|
|
319
|
+
file=sys.stderr,
|
|
320
|
+
)
|
|
321
|
+
raise SystemExit(2)
|
|
322
|
+
if repo_taste_resolved.exists() and not repo_taste_resolved.is_file():
|
|
323
|
+
print(
|
|
324
|
+
f"Error: --in-repo-taste must point to a file: {in_repo_taste_arg}",
|
|
325
|
+
file=sys.stderr,
|
|
326
|
+
)
|
|
327
|
+
raise SystemExit(2)
|
|
328
|
+
repo_taste_relative = str(repo_taste_resolved.relative_to(path))
|
|
284
329
|
|
|
285
|
-
live_dir_arg: Path | None = getattr(args, "live_migrations_dir", None)
|
|
286
330
|
if live_dir_arg is not None:
|
|
287
331
|
resolved_live = (path / live_dir_arg).resolve()
|
|
288
332
|
if not resolved_live.is_relative_to(path):
|
|
@@ -291,9 +335,39 @@ def _handle_init(args: argparse.Namespace) -> None:
|
|
|
291
335
|
file=sys.stderr,
|
|
292
336
|
)
|
|
293
337
|
raise SystemExit(2)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
338
|
+
if resolved_live.exists() and not resolved_live.is_dir():
|
|
339
|
+
print(
|
|
340
|
+
f"Error: --live-migrations-dir must point to a directory: {live_dir_arg}",
|
|
341
|
+
file=sys.stderr,
|
|
342
|
+
)
|
|
343
|
+
raise SystemExit(2)
|
|
344
|
+
live_dir_relative = str(resolved_live.relative_to(path))
|
|
345
|
+
|
|
346
|
+
project = register_project(path)
|
|
347
|
+
if repo_taste_relative is not None:
|
|
348
|
+
assert repo_taste_resolved is not None
|
|
349
|
+
_configure_repo_taste(
|
|
350
|
+
current=resolve_project_taste_path(project),
|
|
351
|
+
destination=repo_taste_resolved,
|
|
352
|
+
force=force,
|
|
353
|
+
ensure_taste_file=ensure_taste_file,
|
|
354
|
+
)
|
|
355
|
+
set_repo_taste_path(project.entry.uuid, repo_taste_relative)
|
|
356
|
+
project = resolve_project(path)
|
|
357
|
+
|
|
358
|
+
taste_path = resolve_project_taste_path(project)
|
|
359
|
+
ensure_taste_file(taste_path)
|
|
360
|
+
|
|
361
|
+
if live_dir_arg is not None:
|
|
362
|
+
assert resolved_live is not None
|
|
363
|
+
assert live_dir_relative is not None
|
|
364
|
+
_configure_live_migrations_dir(
|
|
365
|
+
current=resolve_live_migrations_dir(project),
|
|
366
|
+
destination=resolved_live,
|
|
367
|
+
force=force,
|
|
368
|
+
)
|
|
369
|
+
set_live_migrations_dir(project.entry.uuid, live_dir_relative)
|
|
370
|
+
project = resolve_project(path)
|
|
297
371
|
except ContinuousRefactorError as error:
|
|
298
372
|
print(f"Error: {error}", file=sys.stderr)
|
|
299
373
|
raise SystemExit(1) from error
|
|
@@ -302,11 +376,110 @@ def _handle_init(args: argparse.Namespace) -> None:
|
|
|
302
376
|
print(f"Data directory: {project.project_dir}")
|
|
303
377
|
print(f"Taste file: {taste_path}")
|
|
304
378
|
if live_dir_arg is not None:
|
|
379
|
+
assert resolved_live is not None
|
|
305
380
|
print(f"Live migrations dir: {resolved_live}")
|
|
306
381
|
|
|
307
382
|
|
|
383
|
+
def _configure_repo_taste(
|
|
384
|
+
*,
|
|
385
|
+
current: Path,
|
|
386
|
+
destination: Path,
|
|
387
|
+
force: bool,
|
|
388
|
+
ensure_taste_file: Callable[[Path], Path],
|
|
389
|
+
) -> None:
|
|
390
|
+
if not current.exists():
|
|
391
|
+
ensure_taste_file(destination)
|
|
392
|
+
return
|
|
393
|
+
if current.resolve() == destination.resolve():
|
|
394
|
+
return
|
|
395
|
+
if not current.is_file():
|
|
396
|
+
raise ContinuousRefactorError(
|
|
397
|
+
f"Configured taste path is not a file: {current}"
|
|
398
|
+
)
|
|
399
|
+
if destination.exists() and not force:
|
|
400
|
+
raise ContinuousRefactorError(
|
|
401
|
+
"Taste destination already exists: "
|
|
402
|
+
f"{destination}. Re-run init with --force to replace it."
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
406
|
+
try:
|
|
407
|
+
shutil.move(str(current), str(destination))
|
|
408
|
+
except OSError as exc:
|
|
409
|
+
raise ContinuousRefactorError(
|
|
410
|
+
f"Could not move taste file from {current} to {destination}."
|
|
411
|
+
) from exc
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _configure_live_migrations_dir(
|
|
415
|
+
*,
|
|
416
|
+
current: Path | None,
|
|
417
|
+
destination: Path,
|
|
418
|
+
force: bool,
|
|
419
|
+
) -> None:
|
|
420
|
+
if current is None or not current.exists():
|
|
421
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
422
|
+
return
|
|
423
|
+
if not current.is_dir():
|
|
424
|
+
raise ContinuousRefactorError(
|
|
425
|
+
f"Configured live migrations path is not a directory: {current}"
|
|
426
|
+
)
|
|
427
|
+
if current.resolve() == destination.resolve():
|
|
428
|
+
return
|
|
429
|
+
if (
|
|
430
|
+
destination.resolve().is_relative_to(current.resolve())
|
|
431
|
+
or current.resolve().is_relative_to(destination.resolve())
|
|
432
|
+
):
|
|
433
|
+
raise ContinuousRefactorError(
|
|
434
|
+
"Live migrations directory cannot be moved into itself or one of "
|
|
435
|
+
f"its parents: {current} -> {destination}"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
backup_destination: Path | None = None
|
|
439
|
+
removed_empty_destination = False
|
|
440
|
+
if destination.exists():
|
|
441
|
+
if not destination.is_dir():
|
|
442
|
+
raise ContinuousRefactorError(
|
|
443
|
+
f"Live migrations destination is not a directory: {destination}"
|
|
444
|
+
)
|
|
445
|
+
if any(destination.iterdir()) and not force:
|
|
446
|
+
raise ContinuousRefactorError(
|
|
447
|
+
"Live migrations destination already exists and is not empty: "
|
|
448
|
+
f"{destination}. Re-run init with --force to replace it."
|
|
449
|
+
)
|
|
450
|
+
if force:
|
|
451
|
+
backup_name = (
|
|
452
|
+
f".{destination.name}."
|
|
453
|
+
f"continuous-refactoring-replaced-{uuid.uuid4().hex}"
|
|
454
|
+
)
|
|
455
|
+
backup_destination = destination.with_name(backup_name)
|
|
456
|
+
destination.rename(backup_destination)
|
|
457
|
+
else:
|
|
458
|
+
destination.rmdir()
|
|
459
|
+
removed_empty_destination = True
|
|
460
|
+
|
|
461
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
462
|
+
try:
|
|
463
|
+
shutil.move(str(current), str(destination))
|
|
464
|
+
except OSError as exc:
|
|
465
|
+
if backup_destination is not None and not destination.exists():
|
|
466
|
+
backup_destination.rename(destination)
|
|
467
|
+
elif removed_empty_destination:
|
|
468
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
469
|
+
raise ContinuousRefactorError(
|
|
470
|
+
"Could not move live migrations directory from "
|
|
471
|
+
f"{current} to {destination}."
|
|
472
|
+
) from exc
|
|
473
|
+
if backup_destination is not None:
|
|
474
|
+
shutil.rmtree(backup_destination, ignore_errors=True)
|
|
475
|
+
|
|
476
|
+
|
|
308
477
|
def _resolve_taste_path(global_: bool) -> Path:
|
|
309
|
-
from continuous_refactoring.config import
|
|
478
|
+
from continuous_refactoring.config import (
|
|
479
|
+
global_dir,
|
|
480
|
+
resolve_project,
|
|
481
|
+
resolve_project_taste_path,
|
|
482
|
+
)
|
|
310
483
|
|
|
311
484
|
if global_:
|
|
312
485
|
path = global_dir() / "taste.md"
|
|
@@ -323,7 +496,7 @@ def _resolve_taste_path(global_: bool) -> Path:
|
|
|
323
496
|
file=sys.stderr,
|
|
324
497
|
)
|
|
325
498
|
raise SystemExit(1) from error
|
|
326
|
-
return project
|
|
499
|
+
return resolve_project_taste_path(project)
|
|
327
500
|
|
|
328
501
|
|
|
329
502
|
def _taste_settle_path(path: Path) -> Path:
|
{continuous_refactoring-0.1.0 → continuous_refactoring-0.2.0}/src/continuous_refactoring/config.py
RENAMED
|
@@ -14,6 +14,7 @@ from continuous_refactoring.artifacts import ContinuousRefactorError
|
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"CONFIG_CURRENT_VERSION",
|
|
17
|
+
"DEFAULT_REPO_TASTE_PATH",
|
|
17
18
|
"ProjectEntry",
|
|
18
19
|
"ResolvedProject",
|
|
19
20
|
"TASTE_CURRENT_VERSION",
|
|
@@ -31,15 +32,18 @@ __all__ = [
|
|
|
31
32
|
"parse_taste_version",
|
|
32
33
|
"register_project",
|
|
33
34
|
"resolve_live_migrations_dir",
|
|
35
|
+
"resolve_project_taste_path",
|
|
34
36
|
"resolve_project",
|
|
35
37
|
"save_manifest",
|
|
36
38
|
"set_live_migrations_dir",
|
|
39
|
+
"set_repo_taste_path",
|
|
37
40
|
"taste_is_stale",
|
|
38
41
|
"xdg_data_home",
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
CONFIG_CURRENT_VERSION = 1
|
|
42
45
|
TASTE_CURRENT_VERSION = 1
|
|
46
|
+
DEFAULT_REPO_TASTE_PATH = ".continuous-refactoring/taste.md"
|
|
43
47
|
|
|
44
48
|
_DEFAULT_TASTE = """\
|
|
45
49
|
taste-scoping-version: 1
|
|
@@ -69,6 +73,7 @@ class ProjectEntry:
|
|
|
69
73
|
git_remote: str | None
|
|
70
74
|
created_at: str
|
|
71
75
|
live_migrations_dir: str | None = None
|
|
76
|
+
repo_taste_path: str | None = None
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
@dataclass(frozen=True)
|
|
@@ -182,6 +187,12 @@ def _entry_from_object(uid: str, data: object) -> ProjectEntry:
|
|
|
182
187
|
project_id=uid,
|
|
183
188
|
required=False,
|
|
184
189
|
),
|
|
190
|
+
repo_taste_path=_string_field(
|
|
191
|
+
data,
|
|
192
|
+
"repo_taste_path",
|
|
193
|
+
project_id=uid,
|
|
194
|
+
required=False,
|
|
195
|
+
),
|
|
185
196
|
)
|
|
186
197
|
|
|
187
198
|
|
|
@@ -310,6 +321,18 @@ def resolve_live_migrations_dir(project: ResolvedProject) -> Path | None:
|
|
|
310
321
|
return resolved
|
|
311
322
|
|
|
312
323
|
|
|
324
|
+
def resolve_project_taste_path(project: ResolvedProject) -> Path:
|
|
325
|
+
if project.entry.repo_taste_path is None:
|
|
326
|
+
return project.project_dir / "taste.md"
|
|
327
|
+
repo_root = Path(project.entry.path).resolve()
|
|
328
|
+
resolved = (repo_root / project.entry.repo_taste_path).resolve()
|
|
329
|
+
if not resolved.is_relative_to(repo_root):
|
|
330
|
+
raise ContinuousRefactorError(
|
|
331
|
+
f"repo_taste_path escapes repo: {project.entry.repo_taste_path}"
|
|
332
|
+
)
|
|
333
|
+
return resolved
|
|
334
|
+
|
|
335
|
+
|
|
313
336
|
def _get_project(manifest: dict[str, ProjectEntry], project_uuid: str) -> ProjectEntry:
|
|
314
337
|
project = manifest.get(project_uuid)
|
|
315
338
|
if project is None:
|
|
@@ -324,6 +347,13 @@ def set_live_migrations_dir(project_uuid: str, relative_dir: str) -> None:
|
|
|
324
347
|
save_manifest(manifest)
|
|
325
348
|
|
|
326
349
|
|
|
350
|
+
def set_repo_taste_path(project_uuid: str, relative_path: str) -> None:
|
|
351
|
+
manifest = load_manifest()
|
|
352
|
+
old = _get_project(manifest, project_uuid)
|
|
353
|
+
manifest[project_uuid] = replace(old, repo_taste_path=relative_path)
|
|
354
|
+
save_manifest(manifest)
|
|
355
|
+
|
|
356
|
+
|
|
327
357
|
# ---------------------------------------------------------------------------
|
|
328
358
|
# Taste
|
|
329
359
|
# ---------------------------------------------------------------------------
|
|
@@ -349,6 +379,8 @@ def default_taste_text() -> str:
|
|
|
349
379
|
|
|
350
380
|
|
|
351
381
|
def ensure_taste_file(path: Path) -> Path:
|
|
382
|
+
if path.exists() and not path.is_file():
|
|
383
|
+
raise ContinuousRefactorError(f"Taste path is not a file: {path}")
|
|
352
384
|
if not path.exists():
|
|
353
385
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
354
386
|
path.write_text(_DEFAULT_TASTE, encoding="utf-8")
|
|
@@ -366,7 +398,7 @@ def _read_taste_text(path: Path) -> str:
|
|
|
366
398
|
|
|
367
399
|
def load_taste(project: ResolvedProject | None) -> str:
|
|
368
400
|
if project is not None:
|
|
369
|
-
project_taste = project
|
|
401
|
+
project_taste = resolve_project_taste_path(project)
|
|
370
402
|
if project_taste.exists():
|
|
371
403
|
return _read_taste_text(project_taste)
|
|
372
404
|
|