half-orm-dev 0.17.0a13__tar.gz → 0.17.0a15__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.
- {half_orm_dev-0.17.0a13/half_orm_dev.egg-info → half_orm_dev-0.17.0a15}/PKG-INFO +1 -1
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/check.py +90 -42
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/upgrade.py +2 -2
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patch_manager.py +2 -2
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/release_manager.py +206 -150
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/repo.py +134 -146
- half_orm_dev-0.17.0a15/half_orm_dev/version.txt +1 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15/half_orm_dev.egg-info}/PKG-INFO +1 -1
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev.egg-info/SOURCES.txt +0 -2
- half_orm_dev-0.17.0a13/half_orm_dev/hop.py +0 -167
- half_orm_dev-0.17.0a13/half_orm_dev/patch.py +0 -281
- half_orm_dev-0.17.0a13/half_orm_dev/version.txt +0 -1
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/AUTHORS +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/LICENSE +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/README.md +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/new.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli/main.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/database.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/hgit.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/manifest.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/Pipfile +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/pyproject.toml +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/setup.cfg +0 -0
- {half_orm_dev-0.17.0a13 → half_orm_dev-0.17.0a15}/setup.py +0 -0
|
@@ -14,11 +14,6 @@ from half_orm import utils
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@click.command()
|
|
17
|
-
@click.option(
|
|
18
|
-
'--prune-branches', '-p',
|
|
19
|
-
is_flag=True,
|
|
20
|
-
help='Also clean up local branches that no longer exist on remote'
|
|
21
|
-
)
|
|
22
17
|
@click.option(
|
|
23
18
|
'--dry-run',
|
|
24
19
|
is_flag=True,
|
|
@@ -29,7 +24,7 @@ from half_orm import utils
|
|
|
29
24
|
is_flag=True,
|
|
30
25
|
help='Show detailed information'
|
|
31
26
|
)
|
|
32
|
-
def check(
|
|
27
|
+
def check(dry_run: bool, verbose: bool) -> None:
|
|
33
28
|
"""
|
|
34
29
|
Verify and update project configuration.
|
|
35
30
|
|
|
@@ -39,15 +34,12 @@ def check(prune_branches: bool, dry_run: bool, verbose: bool) -> None:
|
|
|
39
34
|
Checks performed:
|
|
40
35
|
• Git hooks are up to date (pre-commit)
|
|
41
36
|
• Repository is properly configured
|
|
42
|
-
•
|
|
37
|
+
• Detect and prompt to clean up stale local branches
|
|
43
38
|
|
|
44
39
|
Examples:
|
|
45
40
|
# Basic check and update
|
|
46
41
|
half_orm dev check
|
|
47
42
|
|
|
48
|
-
# Check and clean up stale branches
|
|
49
|
-
half_orm dev check --prune-branches
|
|
50
|
-
|
|
51
43
|
# Preview what would be done
|
|
52
44
|
half_orm dev check --dry-run
|
|
53
45
|
"""
|
|
@@ -56,13 +48,12 @@ def check(prune_branches: bool, dry_run: bool, verbose: bool) -> None:
|
|
|
56
48
|
|
|
57
49
|
# Perform check (delegates to Repo)
|
|
58
50
|
result = repo.check_and_update(
|
|
59
|
-
prune_branches=prune_branches,
|
|
60
51
|
dry_run=dry_run,
|
|
61
52
|
silent=False # Show messages
|
|
62
53
|
)
|
|
63
54
|
|
|
64
55
|
# Display results
|
|
65
|
-
_display_check_results(result, dry_run,
|
|
56
|
+
_display_check_results(repo, result, dry_run, verbose)
|
|
66
57
|
|
|
67
58
|
except Exception as e:
|
|
68
59
|
click.echo(utils.Color.red(f"❌ Error: {e}"), err=True)
|
|
@@ -72,9 +63,9 @@ def check(prune_branches: bool, dry_run: bool, verbose: bool) -> None:
|
|
|
72
63
|
raise click.Abort()
|
|
73
64
|
|
|
74
65
|
|
|
75
|
-
def _display_check_results(result: dict, dry_run: bool,
|
|
66
|
+
def _display_check_results(repo, result: dict, dry_run: bool, verbose: bool):
|
|
76
67
|
"""Display check results to user."""
|
|
77
|
-
# Version check
|
|
68
|
+
# Version check - display first and potentially interrupt
|
|
78
69
|
version_info = result.get('version')
|
|
79
70
|
if version_info:
|
|
80
71
|
current = version_info.get('current_version')
|
|
@@ -86,8 +77,25 @@ def _display_check_results(result: dict, dry_run: bool, prune_branches: bool, ve
|
|
|
86
77
|
if verbose:
|
|
87
78
|
click.echo(f"ℹ {utils.Color.blue(f'Version check: {error}')}")
|
|
88
79
|
elif update_available and latest:
|
|
89
|
-
|
|
90
|
-
click.echo(
|
|
80
|
+
# Critical update notice - display prominently at the top
|
|
81
|
+
click.echo()
|
|
82
|
+
click.echo(f"{'='*70}")
|
|
83
|
+
click.echo(f"⚠️ {utils.Color.bold('UPDATE AVAILABLE')} ⚠️")
|
|
84
|
+
click.echo(f"{'='*70}")
|
|
85
|
+
click.echo(f"Current version: {utils.Color.bold(current)}")
|
|
86
|
+
click.echo(f"Latest version: {utils.Color.green(utils.Color.bold(latest))}")
|
|
87
|
+
click.echo()
|
|
88
|
+
click.echo(f"To update, run: {utils.Color.bold('pip install --upgrade half_orm_dev')}")
|
|
89
|
+
click.echo(f"{'='*70}")
|
|
90
|
+
click.echo()
|
|
91
|
+
|
|
92
|
+
# Prompt user to update now
|
|
93
|
+
if click.confirm("Do you want to interrupt and update now?", default=False):
|
|
94
|
+
click.echo(f"\nℹ️ Please run the following command:")
|
|
95
|
+
click.echo(f" {utils.Color.bold('pip install --upgrade half_orm_dev')}")
|
|
96
|
+
click.echo()
|
|
97
|
+
raise click.Abort()
|
|
98
|
+
|
|
91
99
|
click.echo()
|
|
92
100
|
elif current:
|
|
93
101
|
click.echo(f"✓ {utils.Color.green(f'half_orm_dev: {current} (latest)')}")
|
|
@@ -139,30 +147,57 @@ def _display_check_results(result: dict, dry_run: bool, prune_branches: bool, ve
|
|
|
139
147
|
if len(stale_release) > 5:
|
|
140
148
|
click.echo(f" ... and {len(stale_release) - 5} more")
|
|
141
149
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
# Stale branches detection and cleanup
|
|
151
|
+
stale_branches = result.get('stale_branches', {})
|
|
152
|
+
candidates = stale_branches.get('candidates', [])
|
|
153
|
+
|
|
154
|
+
if candidates:
|
|
155
|
+
click.echo()
|
|
156
|
+
if dry_run:
|
|
157
|
+
click.echo(f"⚠️ {utils.Color.bold(f'Found {len(candidates)} stale local branch(es)')} (no longer on remote):")
|
|
158
|
+
for branch in candidates[:10]:
|
|
159
|
+
click.echo(f" ○ {branch}")
|
|
160
|
+
if len(candidates) > 10:
|
|
161
|
+
click.echo(f" ... and {len(candidates) - 10} more")
|
|
162
|
+
click.echo(f"\n Run without --dry-run to be prompted for deletion")
|
|
163
|
+
else:
|
|
164
|
+
# Show stale branches and prompt for deletion
|
|
165
|
+
click.echo(f"⚠️ {utils.Color.bold(f'Found {len(candidates)} stale local branch(es)')} (no longer on remote):")
|
|
166
|
+
for branch in candidates[:10]:
|
|
167
|
+
click.echo(f" • {branch}")
|
|
168
|
+
if len(candidates) > 10:
|
|
169
|
+
click.echo(f" ... and {len(candidates) - 10} more")
|
|
148
170
|
click.echo()
|
|
149
|
-
if dry_run:
|
|
150
|
-
click.echo(f"○ {utils.Color.blue(f'Would delete {len(deleted)} stale branch(es)')}")
|
|
151
|
-
else:
|
|
152
|
-
click.echo(f"✓ {utils.Color.green(f'Deleted {len(deleted)} stale branch(es)')}")
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
# Prompt for confirmation
|
|
173
|
+
if click.confirm(f"Delete these {len(candidates)} branch(es)?", default=False):
|
|
174
|
+
# Get repo instance and actually delete the branches
|
|
175
|
+
deleted = []
|
|
176
|
+
errors = []
|
|
177
|
+
try:
|
|
178
|
+
delete_result = repo.hgit.prune_local_branches(
|
|
179
|
+
pattern="ho-*",
|
|
180
|
+
dry_run=False,
|
|
181
|
+
exclude_current=True
|
|
182
|
+
)
|
|
183
|
+
deleted = delete_result.get('deleted', [])
|
|
184
|
+
errors = delete_result.get('errors', [])
|
|
185
|
+
|
|
186
|
+
if deleted:
|
|
187
|
+
click.echo(f"\n✓ {utils.Color.green(f'Deleted {len(deleted)} stale branch(es)')}")
|
|
188
|
+
if verbose:
|
|
189
|
+
for branch in deleted[:10]:
|
|
190
|
+
click.echo(f" ✓ {branch}")
|
|
191
|
+
if len(deleted) > 10:
|
|
192
|
+
click.echo(f" ... and {len(deleted) - 10} more")
|
|
193
|
+
|
|
194
|
+
if errors:
|
|
195
|
+
click.echo(f"\n⚠ {utils.Color.red('Some errors occurred during cleanup')}")
|
|
196
|
+
if verbose:
|
|
197
|
+
for branch, error in errors[:3]:
|
|
198
|
+
click.echo(f" {branch}: {error}")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
click.echo(utils.Color.red(f"\n❌ Error deleting branches: {e}"), err=True)
|
|
166
201
|
|
|
167
202
|
|
|
168
203
|
def _display_release_branches_grouped(branches: list, verbose: bool):
|
|
@@ -263,10 +298,23 @@ def _display_releases_with_patches(releases_info: dict, patch_branches: list, re
|
|
|
263
298
|
# Release header with status
|
|
264
299
|
release_status = ""
|
|
265
300
|
if release_branch_info:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
301
|
+
sync_status = release_branch_info.get('sync_status', 'unknown')
|
|
302
|
+
ahead = release_branch_info.get('ahead', 0)
|
|
303
|
+
behind = release_branch_info.get('behind', 0)
|
|
304
|
+
exists_on_remote = release_branch_info.get('exists_on_remote', False)
|
|
305
|
+
|
|
306
|
+
if not exists_on_remote:
|
|
307
|
+
release_status = f" {utils.Color.bold('⚠️ local only - remote deleted')}"
|
|
308
|
+
elif sync_status == 'remote_only':
|
|
269
309
|
release_status = f" {utils.Color.blue('☁️ on remote only')}"
|
|
310
|
+
elif sync_status == 'synced':
|
|
311
|
+
release_status = f" {utils.Color.green('✓ synced')}"
|
|
312
|
+
elif sync_status == 'ahead':
|
|
313
|
+
release_status = f" {utils.Color.blue(f'↑ {ahead} ahead')}"
|
|
314
|
+
elif sync_status == 'behind':
|
|
315
|
+
release_status = f" {utils.Color.blue(f'↓ {behind} behind')}"
|
|
316
|
+
elif sync_status == 'diverged':
|
|
317
|
+
release_status = f" {utils.Color.red(f'⚠ diverged (↑{ahead} ↓{behind})')}"
|
|
270
318
|
else:
|
|
271
319
|
# Release files exist but no branch at all
|
|
272
320
|
release_status = f" {utils.Color.red('⚠️ branch not found')}"
|
|
@@ -303,7 +351,7 @@ def _display_releases_with_patches(releases_info: dict, patch_branches: list, re
|
|
|
303
351
|
elif sync_status == 'diverged':
|
|
304
352
|
status = utils.Color.red(f"⚠ diverged (↑{ahead} ↓{behind})")
|
|
305
353
|
elif sync_status == 'no_remote':
|
|
306
|
-
status = utils.Color.
|
|
354
|
+
status = utils.Color.bold("⚠️ local only (remote deleted or not pushed - run: git branch -d " + branch_name + ")")
|
|
307
355
|
else:
|
|
308
356
|
status = "?"
|
|
309
357
|
|
|
@@ -132,7 +132,7 @@ def _display_upgrade_results(result):
|
|
|
132
132
|
final = result['final_version']
|
|
133
133
|
click.echo(f"\nWould upgrade: {current} → {utils.Color.green(final)}")
|
|
134
134
|
|
|
135
|
-
click.echo(f"\n{utils.Color.
|
|
135
|
+
click.echo(f"\n{utils.Color.bold('To apply this upgrade, run without --dry-run')}")
|
|
136
136
|
return
|
|
137
137
|
|
|
138
138
|
# === ACTUAL UPGRADE ===
|
|
@@ -147,7 +147,7 @@ def _display_upgrade_results(result):
|
|
|
147
147
|
# Up to date scenario
|
|
148
148
|
pass
|
|
149
149
|
else:
|
|
150
|
-
click.echo(f"⚠️ {utils.Color.
|
|
150
|
+
click.echo(f"⚠️ {utils.Color.bold('No backup created (--skip-backup used)')}")
|
|
151
151
|
|
|
152
152
|
click.echo(f"\nCurrent version: {utils.Color.bold(current)}")
|
|
153
153
|
|
|
@@ -2104,13 +2104,13 @@ class PatchManager:
|
|
|
2104
2104
|
|
|
2105
2105
|
except FileNotFoundError:
|
|
2106
2106
|
# pytest not installed - warn but don't block
|
|
2107
|
-
click.echo(f" • {utils.Color.
|
|
2107
|
+
click.echo(f" • {utils.Color.bold('⚠')} pytest not found (install pytest to run tests)")
|
|
2108
2108
|
except PatchManagerError:
|
|
2109
2109
|
# Re-raise our own exceptions (test failures)
|
|
2110
2110
|
raise
|
|
2111
2111
|
except Exception as e:
|
|
2112
2112
|
# Any other error - warn but don't block (might be environment issue)
|
|
2113
|
-
click.echo(f" • {utils.Color.
|
|
2113
|
+
click.echo(f" • {utils.Color.bold('⚠')} Failed to run tests: {e} (continuing anyway)")
|
|
2114
2114
|
|
|
2115
2115
|
# Note: KeyboardInterrupt (Ctrl+C) is not caught here - it inherits from
|
|
2116
2116
|
# BaseException, not Exception, so it will propagate up to the decorator
|
|
@@ -110,10 +110,10 @@ class ReleaseManager:
|
|
|
110
110
|
- X.Y.Z-hotfix[N].txt: Emergency hotfix (immutable)
|
|
111
111
|
|
|
112
112
|
Examples:
|
|
113
|
-
#
|
|
113
|
+
# Create new release
|
|
114
114
|
release_mgr = ReleaseManager(repo)
|
|
115
|
-
result = release_mgr.
|
|
116
|
-
# Creates
|
|
115
|
+
result = release_mgr.new_release('minor')
|
|
116
|
+
# Creates branch ho-release/1.4.0
|
|
117
117
|
|
|
118
118
|
# Find latest version
|
|
119
119
|
version = release_mgr.find_latest_version()
|
|
@@ -135,148 +135,6 @@ class ReleaseManager:
|
|
|
135
135
|
self._base_dir = str(repo.base_dir)
|
|
136
136
|
self._releases_dir = Path(repo.base_dir) / "releases"
|
|
137
137
|
|
|
138
|
-
def prepare_release(self, increment_type: str) -> dict:
|
|
139
|
-
"""
|
|
140
|
-
Prepare next release stage file.
|
|
141
|
-
|
|
142
|
-
Creates new releases/X.Y.Z-stage.txt file based on latest version
|
|
143
|
-
and increment type. Validates repository state, synchronizes with
|
|
144
|
-
origin, and pushes to reserve version globally.
|
|
145
|
-
|
|
146
|
-
Workflow:
|
|
147
|
-
0. Acquire lock tag
|
|
148
|
-
1. Validate on ho-prod branch
|
|
149
|
-
2. Validate repository is clean
|
|
150
|
-
3. Fetch from origin
|
|
151
|
-
4. Synchronize with origin/ho-prod (pull if behind)
|
|
152
|
-
5. Read production version from model/schema.sql
|
|
153
|
-
6. Calculate next version based on increment type
|
|
154
|
-
7. Verify stage file doesn't already exist
|
|
155
|
-
8. Create empty stage file
|
|
156
|
-
9. Commit with message "Prepare release X.Y.Z-stage"
|
|
157
|
-
10. Push to origin (global reservation)
|
|
158
|
-
11. Release lock tag
|
|
159
|
-
|
|
160
|
-
Branch requirements:
|
|
161
|
-
- Must be on ho-prod branch
|
|
162
|
-
- Repository must be clean (no uncommitted changes)
|
|
163
|
-
- Must be synced with origin/ho-prod (auto-pull if behind)
|
|
164
|
-
|
|
165
|
-
Synchronization behavior:
|
|
166
|
-
- "synced": Continue
|
|
167
|
-
- "behind": Auto-pull with message
|
|
168
|
-
- "ahead": Continue (will push at end)
|
|
169
|
-
- "diverged": Error - manual merge required
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
increment_type: Version increment ("major", "minor", or "patch")
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
dict: Preparation result with keys:
|
|
176
|
-
- version: New version string (e.g., "1.4.0")
|
|
177
|
-
- file: Path to created stage file
|
|
178
|
-
- previous_version: Previous production version
|
|
179
|
-
|
|
180
|
-
Raises:
|
|
181
|
-
ReleaseManagerError: If validation fails
|
|
182
|
-
ReleaseManagerError: If not on ho-prod branch
|
|
183
|
-
ReleaseManagerError: If repository not clean
|
|
184
|
-
ReleaseManagerError: If ho-prod diverged from origin
|
|
185
|
-
ReleaseFileError: If stage file already exists
|
|
186
|
-
ReleaseVersionError: If version calculation fails
|
|
187
|
-
|
|
188
|
-
Examples:
|
|
189
|
-
# Prepare minor release
|
|
190
|
-
result = release_mgr.prepare_release('minor')
|
|
191
|
-
# Production was 1.3.5 → creates releases/1.4.0-stage.txt
|
|
192
|
-
|
|
193
|
-
# Prepare patch release
|
|
194
|
-
result = release_mgr.prepare_release('patch')
|
|
195
|
-
# Production was 1.3.5 → creates releases/1.3.6-stage.txt
|
|
196
|
-
|
|
197
|
-
# Error handling
|
|
198
|
-
try:
|
|
199
|
-
result = release_mgr.prepare_release('major')
|
|
200
|
-
except ReleaseManagerError as e:
|
|
201
|
-
print(f"Failed: {e}")
|
|
202
|
-
"""
|
|
203
|
-
try:
|
|
204
|
-
# 0. ACQUIRE LOCK on ho-prod (with 30 min timeout for stale locks)
|
|
205
|
-
lock_tag = self._repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
|
|
206
|
-
|
|
207
|
-
# 1. Validate on ho-prod branch
|
|
208
|
-
if self._repo.hgit.branch != 'ho-prod':
|
|
209
|
-
raise ReleaseManagerError(
|
|
210
|
-
f"Must be on ho-prod branch to prepare release.\n"
|
|
211
|
-
f"Current branch: {self._repo.hgit.branch}\n"
|
|
212
|
-
f"Switch to ho-prod: git checkout ho-prod"
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# 2. Validate repository is clean
|
|
216
|
-
if not self._repo.hgit.repos_is_clean():
|
|
217
|
-
raise ReleaseManagerError(
|
|
218
|
-
"Repository has uncommitted changes.\n"
|
|
219
|
-
"Commit or stash changes before preparing release:\n"
|
|
220
|
-
" git status\n"
|
|
221
|
-
" git add . && git commit"
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# 3. Fetch from origin
|
|
225
|
-
self._repo.hgit.fetch_from_origin()
|
|
226
|
-
|
|
227
|
-
# 4. Synchronize with origin
|
|
228
|
-
is_synced, status = self._repo.hgit.is_branch_synced("ho-prod")
|
|
229
|
-
|
|
230
|
-
if status == "behind":
|
|
231
|
-
# Pull automatically
|
|
232
|
-
self._repo.hgit.pull()
|
|
233
|
-
elif status == "diverged":
|
|
234
|
-
raise ReleaseManagerError(
|
|
235
|
-
"ho-prod has diverged from origin/ho-prod.\n"
|
|
236
|
-
"Manual resolution required:\n"
|
|
237
|
-
" git pull --rebase origin ho-prod\n"
|
|
238
|
-
" or\n"
|
|
239
|
-
" git merge origin/ho-prod"
|
|
240
|
-
)
|
|
241
|
-
# If "synced" or "ahead", continue
|
|
242
|
-
|
|
243
|
-
# 5. Read production version from model/schema.sql
|
|
244
|
-
prod_version_str = self._get_production_version()
|
|
245
|
-
|
|
246
|
-
# Parse into Version object for calculation
|
|
247
|
-
prod_version = self.parse_version_from_filename(f"{prod_version_str}.txt")
|
|
248
|
-
|
|
249
|
-
# 6. Calculate next version
|
|
250
|
-
next_version = self.calculate_next_version(prod_version, increment_type)
|
|
251
|
-
|
|
252
|
-
# 7. Verify stage file doesn't exist
|
|
253
|
-
stage_file = self._releases_dir / f"{next_version}-stage.txt"
|
|
254
|
-
if stage_file.exists():
|
|
255
|
-
raise ReleaseFileError(
|
|
256
|
-
f"Stage file already exists: {stage_file}\n"
|
|
257
|
-
f"Version {next_version} is already in development.\n"
|
|
258
|
-
f"To continue with this version, use existing stage file."
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
# 8. Create empty stage file
|
|
262
|
-
stage_file.touch()
|
|
263
|
-
|
|
264
|
-
# 9. Commit
|
|
265
|
-
self._repo.hgit.add(str(stage_file))
|
|
266
|
-
self._repo.hgit.commit("-m", f"Prepare release {next_version}-stage")
|
|
267
|
-
|
|
268
|
-
# 10. Push to origin (global reservation)
|
|
269
|
-
self._repo.hgit.push()
|
|
270
|
-
# Return result
|
|
271
|
-
return {
|
|
272
|
-
'version': next_version,
|
|
273
|
-
'file': str(stage_file),
|
|
274
|
-
'previous_version': prod_version_str
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
finally:
|
|
278
|
-
# 11. ALWAYS release lock (even on error)
|
|
279
|
-
self._repo.hgit.release_branch_lock(lock_tag)
|
|
280
138
|
|
|
281
139
|
|
|
282
140
|
def _get_production_version(self) -> str:
|
|
@@ -377,6 +235,199 @@ class ReleaseManager:
|
|
|
377
235
|
|
|
378
236
|
return version
|
|
379
237
|
|
|
238
|
+
def _check_and_init_from_database(self) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Check for legacy project and offer to initialize from database.
|
|
241
|
+
|
|
242
|
+
For projects migrating from half_orm 0.13.x, this method detects when ALL
|
|
243
|
+
three required elements are missing (releases/, model/, ho-prod branch) and
|
|
244
|
+
offers to initialize them from the existing database metadata.
|
|
245
|
+
|
|
246
|
+
Workflow:
|
|
247
|
+
1. Check if releases/, model/, and ho-prod all exist
|
|
248
|
+
2. If ALL THREE are missing, prompt user to initialize from database
|
|
249
|
+
3. Create ho-prod branch from current branch
|
|
250
|
+
4. Read production version from half_orm_meta.view.hop_last_release
|
|
251
|
+
5. Create releases/ and model/ directories
|
|
252
|
+
6. Generate schema and metadata files using Database._generate_schema_sql()
|
|
253
|
+
7. Create releases/X.Y.Z.txt EMPTY (production release)
|
|
254
|
+
8. Commit the initialization
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
bool: True if initialization was performed, False if skipped
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ReleaseManagerError: If database not accessible or initialization fails
|
|
261
|
+
ReleaseManagerError: If project is in inconsistent state (only some parts exist)
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
# Migrating old project
|
|
265
|
+
if release_mgr._check_and_init_from_database():
|
|
266
|
+
print("Initialized from database")
|
|
267
|
+
else:
|
|
268
|
+
print("Already initialized")
|
|
269
|
+
"""
|
|
270
|
+
import click
|
|
271
|
+
from half_orm import utils
|
|
272
|
+
|
|
273
|
+
# Check what exists
|
|
274
|
+
releases_exists = self._releases_dir.exists()
|
|
275
|
+
model_dir = Path(self._base_dir) / "model"
|
|
276
|
+
model_exists = model_dir.exists() and (model_dir / "schema.sql").exists()
|
|
277
|
+
ho_prod_exists = self._repo.hgit.branch_exists("ho-prod")
|
|
278
|
+
|
|
279
|
+
# If all three exist, no initialization needed
|
|
280
|
+
if releases_exists and model_exists and ho_prod_exists:
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
# If only some exist, the project is in an inconsistent state
|
|
284
|
+
if releases_exists or model_exists or ho_prod_exists:
|
|
285
|
+
missing = []
|
|
286
|
+
if not releases_exists:
|
|
287
|
+
missing.append("releases/")
|
|
288
|
+
if not model_exists:
|
|
289
|
+
missing.append("model/")
|
|
290
|
+
if not ho_prod_exists:
|
|
291
|
+
missing.append("ho-prod branch")
|
|
292
|
+
|
|
293
|
+
existing = []
|
|
294
|
+
if releases_exists:
|
|
295
|
+
existing.append("releases/")
|
|
296
|
+
if model_exists:
|
|
297
|
+
existing.append("model/")
|
|
298
|
+
if ho_prod_exists:
|
|
299
|
+
existing.append("ho-prod branch")
|
|
300
|
+
|
|
301
|
+
raise ReleaseManagerError(
|
|
302
|
+
f"Project in inconsistent state.\n\n"
|
|
303
|
+
f"Missing: {', '.join(missing)}\n"
|
|
304
|
+
f"Present: {', '.join(existing)}\n\n"
|
|
305
|
+
f"For legacy migration, all three must be missing.\n"
|
|
306
|
+
f"Please either:\n"
|
|
307
|
+
f" • Complete the setup manually, or\n"
|
|
308
|
+
f" • Remove existing elements to start fresh initialization"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# All three missing - offer to initialize from database
|
|
312
|
+
click.echo(f"\n{utils.Color.bold('⚠ Legacy project detected')}")
|
|
313
|
+
click.echo("The releases/ and model/ directories are missing.")
|
|
314
|
+
click.echo("This appears to be a project from half_orm 0.13.x or earlier.")
|
|
315
|
+
click.echo()
|
|
316
|
+
click.echo("I can initialize the workflow from your existing database:")
|
|
317
|
+
click.echo(" • Create ho-prod branch (if needed)")
|
|
318
|
+
click.echo(" • Read production version from half_orm_meta.view.hop_last_release")
|
|
319
|
+
click.echo(" • Create releases/ and model/ directories")
|
|
320
|
+
click.echo(" • Generate schema and metadata files")
|
|
321
|
+
click.echo(" • Create initial production release file")
|
|
322
|
+
click.echo()
|
|
323
|
+
|
|
324
|
+
if not click.confirm("Initialize from database?", default=True):
|
|
325
|
+
raise ReleaseManagerError(
|
|
326
|
+
"Cannot proceed without releases/ directory.\n"
|
|
327
|
+
"Run this command again and confirm initialization, or\n"
|
|
328
|
+
"manually create the releases/ directory structure."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Ensure ho-prod branch exists
|
|
333
|
+
click.echo(f"\n{utils.Color.bold('Checking ho-prod branch...')}")
|
|
334
|
+
current_branch = self._repo.hgit.branch
|
|
335
|
+
if not self._repo.hgit.branch_exists("ho-prod"):
|
|
336
|
+
click.echo(f" Creating ho-prod branch from {current_branch}...")
|
|
337
|
+
try:
|
|
338
|
+
self._repo.hgit.create_branch("ho-prod", from_branch=current_branch)
|
|
339
|
+
click.echo(f" {utils.Color.green('✓ Created ho-prod branch')}")
|
|
340
|
+
|
|
341
|
+
# Push to remote if exists
|
|
342
|
+
if self._repo.hgit.has_remote():
|
|
343
|
+
self._repo.hgit.push_branch("ho-prod")
|
|
344
|
+
click.echo(f" {utils.Color.green('✓ Pushed ho-prod to origin')}")
|
|
345
|
+
|
|
346
|
+
# Switch to ho-prod
|
|
347
|
+
self._repo.hgit.checkout("ho-prod")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
raise ReleaseManagerError(f"Failed to create ho-prod branch: {e}")
|
|
350
|
+
else:
|
|
351
|
+
click.echo(f" ✓ ho-prod branch already exists")
|
|
352
|
+
# Switch to ho-prod
|
|
353
|
+
if current_branch != "ho-prod":
|
|
354
|
+
self._repo.hgit.checkout("ho-prod")
|
|
355
|
+
|
|
356
|
+
# Read production version from database
|
|
357
|
+
click.echo(f"\n{utils.Color.bold('Reading version from database...')}")
|
|
358
|
+
try:
|
|
359
|
+
version_str = self._repo.database.last_release_s
|
|
360
|
+
click.echo(f" Production version: {utils.Color.green(version_str)}")
|
|
361
|
+
except Exception as e:
|
|
362
|
+
raise ReleaseManagerError(
|
|
363
|
+
f"Failed to read version from database.\n"
|
|
364
|
+
f"Error: {e}\n\n"
|
|
365
|
+
f"Ensure:\n"
|
|
366
|
+
f" • Database is accessible\n"
|
|
367
|
+
f" • half_orm_meta schema exists\n"
|
|
368
|
+
f" • hop_last_release view is populated"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Create directories
|
|
372
|
+
click.echo(f"\n{utils.Color.bold('Creating directories...')}")
|
|
373
|
+
self._releases_dir.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
click.echo(f" ✓ Created {self._releases_dir}")
|
|
375
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
click.echo(f" ✓ Created {model_dir}")
|
|
377
|
+
|
|
378
|
+
# Generate schema and metadata files using existing method
|
|
379
|
+
click.echo(f"\n{utils.Color.bold('Generating schema files...')}")
|
|
380
|
+
try:
|
|
381
|
+
self._repo.database._generate_schema_sql(version_str, model_dir)
|
|
382
|
+
click.echo(f" ✓ Generated schema-{version_str}.sql")
|
|
383
|
+
click.echo(f" ✓ Generated metadata-{version_str}.sql")
|
|
384
|
+
click.echo(f" ✓ Created symlink: schema.sql -> schema-{version_str}.sql")
|
|
385
|
+
except Exception as e:
|
|
386
|
+
raise ReleaseManagerError(f"Failed to generate schema files: {e}")
|
|
387
|
+
|
|
388
|
+
# Create empty production release file
|
|
389
|
+
click.echo(f"\n{utils.Color.bold('Creating release file...')}")
|
|
390
|
+
release_file = self._releases_dir / f"{version_str}.txt"
|
|
391
|
+
release_file.touch()
|
|
392
|
+
click.echo(f" ✓ Created {release_file.name} (empty - production release)")
|
|
393
|
+
|
|
394
|
+
# Commit initialization
|
|
395
|
+
click.echo(f"\n{utils.Color.bold('Committing initialization...')}")
|
|
396
|
+
self._repo.hgit.add(str(self._releases_dir))
|
|
397
|
+
self._repo.hgit.add(str(model_dir))
|
|
398
|
+
self._repo.hgit.commit(
|
|
399
|
+
"-m",
|
|
400
|
+
f"chore: initialize releases/ and model/ from database (version {version_str})"
|
|
401
|
+
)
|
|
402
|
+
click.echo(f" {utils.Color.green('✓ Initialization committed')}")
|
|
403
|
+
|
|
404
|
+
# Create production tag for this version
|
|
405
|
+
click.echo(f"\n{utils.Color.bold('Creating production tag...')}")
|
|
406
|
+
prod_tag = f"v{version_str}"
|
|
407
|
+
self._repo.hgit.create_tag(prod_tag, f"Production release {version_str} (migrated from database)")
|
|
408
|
+
click.echo(f" {utils.Color.green(f'✓ Created tag {prod_tag}')}")
|
|
409
|
+
|
|
410
|
+
# Push if remote exists
|
|
411
|
+
if self._repo.hgit.has_remote():
|
|
412
|
+
click.echo(f"\n{utils.Color.bold('Pushing to origin...')}")
|
|
413
|
+
self._repo.hgit.push()
|
|
414
|
+
self._repo.hgit.push_tag(prod_tag)
|
|
415
|
+
click.echo(f" {utils.Color.green('✓ Pushed to origin')}")
|
|
416
|
+
click.echo(f" {utils.Color.green(f'✓ Pushed tag {prod_tag}')}")
|
|
417
|
+
|
|
418
|
+
click.echo(f"\n{utils.Color.green('✓ Successfully initialized from database!')}")
|
|
419
|
+
click.echo()
|
|
420
|
+
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
# Clean up on error
|
|
425
|
+
if self._releases_dir.exists() and not any(self._releases_dir.iterdir()):
|
|
426
|
+
self._releases_dir.rmdir()
|
|
427
|
+
if model_dir.exists() and not any(model_dir.iterdir()):
|
|
428
|
+
model_dir.rmdir()
|
|
429
|
+
raise ReleaseManagerError(f"Initialization failed: {e}")
|
|
430
|
+
|
|
380
431
|
def find_latest_version(self) -> Optional[Version]:
|
|
381
432
|
"""
|
|
382
433
|
Find latest version across all release stages.
|
|
@@ -2365,6 +2416,7 @@ class ReleaseManager:
|
|
|
2365
2416
|
dependencies between patches.
|
|
2366
2417
|
|
|
2367
2418
|
Workflow:
|
|
2419
|
+
0. Check for legacy project migration (auto-initialize if needed)
|
|
2368
2420
|
1. Calculate next version based on level (major/minor/patch)
|
|
2369
2421
|
2. Create release branch ho-release/{version} from ho-prod
|
|
2370
2422
|
3. Push release branch to remote
|
|
@@ -2393,15 +2445,19 @@ class ReleaseManager:
|
|
|
2393
2445
|
# → Creates empty 0.1.0-stage.txt
|
|
2394
2446
|
# → Switches to ho-release/0.1.0
|
|
2395
2447
|
"""
|
|
2448
|
+
# 0. Check for legacy project migration
|
|
2449
|
+
was_initialized = self._check_and_init_from_database()
|
|
2450
|
+
|
|
2396
2451
|
# Calculate next version
|
|
2397
2452
|
version = self._calculate_next_version(level)
|
|
2398
2453
|
release_branch = f"ho-release/{version}"
|
|
2399
2454
|
|
|
2400
|
-
# Ensure we're on ho-prod
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2455
|
+
# Ensure we're on ho-prod (unless we just initialized and are already there)
|
|
2456
|
+
if not was_initialized or self._repo.hgit.branch != "ho-prod":
|
|
2457
|
+
try:
|
|
2458
|
+
self._repo.hgit.checkout("ho-prod")
|
|
2459
|
+
except Exception as e:
|
|
2460
|
+
raise ReleaseManagerError(f"Failed to checkout ho-prod: {e}")
|
|
2405
2461
|
|
|
2406
2462
|
# Create empty candidates file (NEW)
|
|
2407
2463
|
candidates_file = self._releases_dir / f"{version}-candidates.txt"
|