half-orm-dev 1.0.0a26__tar.gz → 1.0.0a27__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-1.0.0a26/half_orm_dev.egg-info → half_orm_dev-1.0.0a27}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/bootstrap_manager.py +11 -6
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/migrate.py +1 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/set_git_origin.py +1 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/main.py +2 -2
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/decorators.py +6 -4
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/hgit.py +89 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migration_manager.py +57 -164
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +1 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +1 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/release_manager.py +206 -335
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/repo.py +33 -101
- half_orm_dev-1.0.0a27/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27/half_orm_dev.egg-info}/PKG-INFO +1 -1
- half_orm_dev-1.0.0a26/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/LICENSE +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/README.md +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/revert_migration.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patch_manager.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/py.typed +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/git-hooks/pre-push +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev.egg-info/SOURCES.txt +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev.egg-info/entry_points.txt +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a26 → half_orm_dev-1.0.0a27}/setup.py +0 -0
|
@@ -253,14 +253,14 @@ class BootstrapManager:
|
|
|
253
253
|
'errors': []
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
executed_set = self.get_executed_files()
|
|
257
|
+
all_files = self.get_bootstrap_files(up_to_version, exclude_version=exclude_version, for_version=for_version)
|
|
258
|
+
|
|
256
259
|
if force:
|
|
257
|
-
files_to_execute =
|
|
260
|
+
files_to_execute = all_files
|
|
258
261
|
else:
|
|
259
|
-
files_to_execute =
|
|
260
|
-
|
|
261
|
-
all_files = self.get_bootstrap_files(up_to_version, exclude_version=exclude_version, for_version=for_version)
|
|
262
|
-
executed = self.get_executed_files()
|
|
263
|
-
result['skipped'] = [f.name for f in all_files if f.name in executed]
|
|
262
|
+
files_to_execute = [f for f in all_files if f.name not in executed_set]
|
|
263
|
+
result['skipped'] = [f.name for f in all_files if f.name in executed_set]
|
|
264
264
|
|
|
265
265
|
if not files_to_execute:
|
|
266
266
|
return result
|
|
@@ -278,10 +278,15 @@ class BootstrapManager:
|
|
|
278
278
|
result['executed'].append(filename)
|
|
279
279
|
continue
|
|
280
280
|
|
|
281
|
+
if filename in executed_set:
|
|
282
|
+
result['skipped'].append(filename)
|
|
283
|
+
continue
|
|
284
|
+
|
|
281
285
|
try:
|
|
282
286
|
click.echo(f" • Executing {filename}...")
|
|
283
287
|
self.execute_file(file_path)
|
|
284
288
|
self.record_execution(filename, file_version)
|
|
289
|
+
executed_set.add(filename)
|
|
285
290
|
result['executed'].append(filename)
|
|
286
291
|
except BootstrapManagerError as e:
|
|
287
292
|
result['errors'].append((filename, str(e)))
|
|
@@ -65,7 +65,7 @@ def migrate(verbose: bool) -> None:
|
|
|
65
65
|
# Get current versions
|
|
66
66
|
from half_orm_dev.utils import hop_version
|
|
67
67
|
installed_version = hop_version()
|
|
68
|
-
config_version = repo.
|
|
68
|
+
config_version = repo.config.hop_version
|
|
69
69
|
|
|
70
70
|
# Migration needed (comparison > 0)
|
|
71
71
|
click.echo(f"⚠️ {utils.Color.bold('Migration needed:')}")
|
|
@@ -141,7 +141,7 @@ def create_cli_group():
|
|
|
141
141
|
if hop.repo_checked and hop._Hop__repo.needs_migration():
|
|
142
142
|
from half_orm_dev.utils import hop_version
|
|
143
143
|
installed_version = hop_version()
|
|
144
|
-
config_version = hop._Hop__repo.
|
|
144
|
+
config_version = hop._Hop__repo.repo.config.hop_version
|
|
145
145
|
|
|
146
146
|
@click.command(
|
|
147
147
|
cmd_name,
|
|
@@ -198,7 +198,7 @@ def create_cli_group():
|
|
|
198
198
|
from half_orm_dev.utils import hop_version
|
|
199
199
|
from half_orm_dev.repo import RepoError
|
|
200
200
|
installed_version = hop_version()
|
|
201
|
-
config_version = hop._Hop__repo.
|
|
201
|
+
config_version = hop._Hop__repo.repo.config.hop_version
|
|
202
202
|
current_branch = hop._Hop__repo.hgit.branch if hop._Hop__repo.hgit else 'unknown'
|
|
203
203
|
|
|
204
204
|
click.echo(f"\n{'='*70}")
|
|
@@ -39,6 +39,8 @@ def with_dynamic_branch_lock(branch_getter, timeout_minutes: int = 30):
|
|
|
39
39
|
def decorator(func):
|
|
40
40
|
@wraps(func)
|
|
41
41
|
def wrapper(self, *args, **kwargs):
|
|
42
|
+
# Support both Manager classes (self._repo) and Repo itself (self)
|
|
43
|
+
repo = getattr(self, '_repo', self)
|
|
42
44
|
lock_tag = None
|
|
43
45
|
locked_branch = None
|
|
44
46
|
try:
|
|
@@ -48,20 +50,20 @@ def with_dynamic_branch_lock(branch_getter, timeout_minutes: int = 30):
|
|
|
48
50
|
# 2. All branches are fetched (prune)
|
|
49
51
|
# 3. Repository hop_version is validated against installed version
|
|
50
52
|
# 4. Operation is blocked if version is outdated (prevents dangerous operations)
|
|
51
|
-
|
|
53
|
+
repo.sync_and_validate_ho_prod()
|
|
52
54
|
|
|
53
55
|
# Determine branch name dynamically
|
|
54
56
|
locked_branch = branch_getter(self, *args, **kwargs)
|
|
55
57
|
|
|
56
58
|
# Acquire lock
|
|
57
|
-
lock_tag =
|
|
59
|
+
lock_tag = repo.hgit.acquire_branch_lock(locked_branch, timeout_minutes=timeout_minutes)
|
|
58
60
|
|
|
59
61
|
# Execute the method
|
|
60
62
|
result = func(self, *args, **kwargs)
|
|
61
63
|
|
|
62
64
|
# After success, sync .hop/ from current branch to all other active branches
|
|
63
65
|
try:
|
|
64
|
-
sync_result =
|
|
66
|
+
sync_result = repo.sync_hop_to_active_branches(
|
|
65
67
|
reason=f"{func.__name__}"
|
|
66
68
|
)
|
|
67
69
|
# Log sync errors but don't fail the operation
|
|
@@ -84,7 +86,7 @@ def with_dynamic_branch_lock(branch_getter, timeout_minutes: int = 30):
|
|
|
84
86
|
wrapper, '_interrupted', True) or None)
|
|
85
87
|
wrapper._interrupted = False
|
|
86
88
|
try:
|
|
87
|
-
|
|
89
|
+
repo.hgit.release_branch_lock(lock_tag)
|
|
88
90
|
finally:
|
|
89
91
|
interrupted = wrapper._interrupted
|
|
90
92
|
signal.signal(signal.SIGINT, original_handler)
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import fnmatch
|
|
9
9
|
import git
|
|
10
|
+
from contextlib import contextmanager
|
|
10
11
|
from git.exc import GitCommandError
|
|
11
12
|
from typing import List, Optional
|
|
12
13
|
import time
|
|
@@ -23,6 +24,7 @@ class HGit:
|
|
|
23
24
|
self.__repo = repo
|
|
24
25
|
self.__base_dir = None
|
|
25
26
|
self.__git_repo: git.Repo = None
|
|
27
|
+
self.__snapshot: dict = {}
|
|
26
28
|
if repo:
|
|
27
29
|
self.__origin = repo.git_origin
|
|
28
30
|
self.__base_dir = repo.base_dir
|
|
@@ -78,6 +80,7 @@ class HGit:
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
self.__current_branch = self.branch
|
|
83
|
+
self.__snapshot = self.capture_branches_snapshot()
|
|
81
84
|
|
|
82
85
|
def __str__(self):
|
|
83
86
|
res = ['[Git]']
|
|
@@ -267,6 +270,92 @@ class HGit:
|
|
|
267
270
|
"Proxy to git.commit method"
|
|
268
271
|
return self.__git_repo.git.checkout(*args, **kwargs)
|
|
269
272
|
|
|
273
|
+
@contextmanager
|
|
274
|
+
def on_branch(self, branch: str, silent: bool = False):
|
|
275
|
+
"""Temporarily switch to branch, yield, then return to original branch."""
|
|
276
|
+
original = self.branch
|
|
277
|
+
switched = branch != original
|
|
278
|
+
if switched:
|
|
279
|
+
if not silent:
|
|
280
|
+
print(f" Switching to {branch}...")
|
|
281
|
+
self.__git_repo.git.checkout(branch)
|
|
282
|
+
if not silent:
|
|
283
|
+
print(f" ✓ Now on {branch}")
|
|
284
|
+
try:
|
|
285
|
+
yield
|
|
286
|
+
finally:
|
|
287
|
+
if switched:
|
|
288
|
+
try:
|
|
289
|
+
if not silent:
|
|
290
|
+
print(f" Returning to {original}...")
|
|
291
|
+
self.__git_repo.git.checkout(original)
|
|
292
|
+
if not silent:
|
|
293
|
+
print(f" ✓ Back on {original}")
|
|
294
|
+
except Exception as e:
|
|
295
|
+
if not silent:
|
|
296
|
+
print(f" ⚠ Could not return to {original}: {e}", file=sys.stderr)
|
|
297
|
+
print(f" You are now on {branch}", file=sys.stderr)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def snapshot(self) -> dict:
|
|
301
|
+
"""Return the last stored branches snapshot."""
|
|
302
|
+
return self.__snapshot
|
|
303
|
+
|
|
304
|
+
def update_snapshot(self) -> None:
|
|
305
|
+
"""Refresh the stored snapshot to reflect current branch HEADs."""
|
|
306
|
+
self.__snapshot = self.capture_branches_snapshot()
|
|
307
|
+
|
|
308
|
+
def capture_branches_snapshot(self) -> dict:
|
|
309
|
+
"""Return {branch_name: HEAD_SHA} for all active local branches."""
|
|
310
|
+
snapshot = {}
|
|
311
|
+
try:
|
|
312
|
+
branches_status = self.get_active_branches_status()
|
|
313
|
+
except Exception:
|
|
314
|
+
return snapshot
|
|
315
|
+
|
|
316
|
+
candidates = []
|
|
317
|
+
prod = branches_status.get('prod_branch')
|
|
318
|
+
if prod:
|
|
319
|
+
candidates.append(prod['name'])
|
|
320
|
+
for b in branches_status.get('release_branches', []):
|
|
321
|
+
if b.get('exists_on_remote', True):
|
|
322
|
+
candidates.append(b['name'])
|
|
323
|
+
for b in branches_status.get('patch_branches', []):
|
|
324
|
+
if b.get('exists_on_remote', True):
|
|
325
|
+
candidates.append(b['name'])
|
|
326
|
+
|
|
327
|
+
for name in candidates:
|
|
328
|
+
try:
|
|
329
|
+
snapshot[name] = self.__git_repo.heads[name].commit.hexsha
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
return snapshot
|
|
333
|
+
|
|
334
|
+
def rollback_to_snapshot(self, snapshot: dict = None) -> dict:
|
|
335
|
+
"""Reset each branch to its captured SHA.
|
|
336
|
+
|
|
337
|
+
Uses the stored snapshot when none is provided.
|
|
338
|
+
Returns {'reset': [branch, ...], 'errors': [(branch, msg), ...]}.
|
|
339
|
+
"""
|
|
340
|
+
target = snapshot if snapshot is not None else self.__snapshot
|
|
341
|
+
result = {'reset': [], 'errors': []}
|
|
342
|
+
original = self.branch
|
|
343
|
+
|
|
344
|
+
for branch, sha in target.items():
|
|
345
|
+
try:
|
|
346
|
+
self.__git_repo.heads[branch].checkout()
|
|
347
|
+
self.__git_repo.git.reset('--hard', sha)
|
|
348
|
+
result['reset'].append(branch)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
result['errors'].append((branch, str(e)))
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
self.__git_repo.heads[original].checkout()
|
|
354
|
+
except Exception as e:
|
|
355
|
+
result['errors'].append(('__restore_original__', str(e)))
|
|
356
|
+
|
|
357
|
+
return result
|
|
358
|
+
|
|
270
359
|
def checkout_paths_from_branch(self, branch: str, paths: list) -> None:
|
|
271
360
|
"""
|
|
272
361
|
Checkout specific paths from another branch into working directory.
|
|
@@ -254,26 +254,31 @@ class MigrationManager:
|
|
|
254
254
|
result['sync_files'] = list(dict.fromkeys(result['sync_files']))
|
|
255
255
|
return result
|
|
256
256
|
|
|
257
|
-
@with_dynamic_branch_lock(lambda self, *args, **kwargs: 'ho-prod')
|
|
258
257
|
def run_migrations(self, target_version: str, create_commit: bool = True) -> Dict:
|
|
259
258
|
"""
|
|
260
|
-
Run all pending migrations up to
|
|
261
|
-
|
|
262
|
-
IMPORTANT: This method acquires a lock on ho-prod branch via decorator.
|
|
263
|
-
Should only be called when on ho-prod branch.
|
|
259
|
+
Run all pending migrations from the current repo version up to target_version.
|
|
264
260
|
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
Must be called via Repo.run_migrations_if_needed() (enforced by guard).
|
|
262
|
+
Must be called while on the ho-prod branch.
|
|
267
263
|
|
|
268
264
|
Args:
|
|
269
|
-
target_version: Target version string (e.g., "0.
|
|
270
|
-
create_commit: Whether to create Git commit after migration
|
|
265
|
+
target_version: Target version string (e.g., "1.0.0-a26")
|
|
266
|
+
create_commit: Whether to create a Git commit after migration
|
|
271
267
|
|
|
272
268
|
Returns:
|
|
273
|
-
Dict with
|
|
274
|
-
-
|
|
275
|
-
-
|
|
269
|
+
Dict with keys:
|
|
270
|
+
- target_version: str
|
|
271
|
+
- migrations_applied: List of per-migration result dicts
|
|
272
|
+
- commit_created: bool
|
|
273
|
+
- commit_message: str (only when commit_created is True)
|
|
274
|
+
- sync_result: dict from commit_and_sync_to_active_branches
|
|
275
|
+
- errors: List of non-fatal warning strings
|
|
276
276
|
"""
|
|
277
|
+
if not getattr(self._repo, '_migration_running', False):
|
|
278
|
+
raise MigrationManagerError(
|
|
279
|
+
"run_migrations() must be called via Repo.run_migrations_if_needed()."
|
|
280
|
+
)
|
|
281
|
+
|
|
277
282
|
result = {
|
|
278
283
|
'target_version': target_version,
|
|
279
284
|
'migrations_applied': [],
|
|
@@ -282,165 +287,56 @@ class MigrationManager:
|
|
|
282
287
|
'notified_branches': []
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
|
|
286
|
-
try:
|
|
287
|
-
self._repo.hgit.fetch_from_origin()
|
|
288
|
-
except Exception as e:
|
|
289
|
-
result['errors'].append(f"Failed to fetch from origin: {e}")
|
|
290
|
-
raise MigrationManagerError(f"Cannot run migration: failed to fetch from origin: {e}")
|
|
291
|
-
|
|
292
|
-
# Verify ho-prod is up to date with origin/ho-prod
|
|
293
|
-
current_branch = self._repo.hgit.branch
|
|
294
|
-
if current_branch == 'ho-prod':
|
|
295
|
-
try:
|
|
296
|
-
# Check if ho-prod is synced with origin/ho-prod
|
|
297
|
-
result_check = subprocess.run(
|
|
298
|
-
['git', 'rev-list', '--left-right', '--count', 'ho-prod...origin/ho-prod'],
|
|
299
|
-
cwd=self._repo.base_dir,
|
|
300
|
-
capture_output=True,
|
|
301
|
-
text=True,
|
|
302
|
-
check=True
|
|
303
|
-
)
|
|
304
|
-
ahead, behind = map(int, result_check.stdout.strip().split())
|
|
305
|
-
if behind > 0:
|
|
306
|
-
# Check whether the migration was already applied on origin/ho-prod.
|
|
307
|
-
already_done = False
|
|
308
|
-
try:
|
|
309
|
-
remote_config_text = self._repo.hgit._HGit__git_repo.git.show(
|
|
310
|
-
'origin/ho-prod:.hop/config'
|
|
311
|
-
)
|
|
312
|
-
_cp = ConfigParser()
|
|
313
|
-
_cp.read_string(remote_config_text)
|
|
314
|
-
remote_version = _cp['halfORM'].get('hop_version', '')
|
|
315
|
-
if remote_version == target_version:
|
|
316
|
-
already_done = True
|
|
317
|
-
except Exception:
|
|
318
|
-
pass
|
|
319
|
-
|
|
320
|
-
if already_done:
|
|
321
|
-
# Another developer already ran the migration.
|
|
322
|
-
# Pull and sync all active branches, then return.
|
|
323
|
-
click.echo(
|
|
324
|
-
f" ℹ Migration to {target_version} already applied by another developer.\n"
|
|
325
|
-
f" Pulling and syncing all active branches..."
|
|
326
|
-
)
|
|
327
|
-
self._repo.hgit._HGit__git_repo.remotes.origin.pull('ho-prod')
|
|
328
|
-
self._repo.hgit.sync_active_branches(pattern="ho-*")
|
|
329
|
-
# Reload config so the rest of the tool sees the new version
|
|
330
|
-
self._repo._Repo__config.read()
|
|
331
|
-
result['already_synced'] = True
|
|
332
|
-
return result
|
|
333
|
-
|
|
334
|
-
raise MigrationManagerError(
|
|
335
|
-
f"ho-prod is {behind} commits behind origin/ho-prod. "
|
|
336
|
-
f"Please pull changes first: git pull origin ho-prod"
|
|
337
|
-
)
|
|
338
|
-
if ahead > 0:
|
|
339
|
-
result['errors'].append(f"Warning: ho-prod is {ahead} commits ahead of origin/ho-prod")
|
|
340
|
-
except (MigrationManagerError, subprocess.CalledProcessError):
|
|
341
|
-
raise
|
|
342
|
-
except Exception:
|
|
343
|
-
# Could not compare — maybe origin/ho-prod doesn't exist yet
|
|
344
|
-
pass
|
|
290
|
+
current_version = self._repo.config.hop_version
|
|
345
291
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
) else "0.0.0"
|
|
350
|
-
|
|
351
|
-
# If already at target version, nothing to do
|
|
352
|
-
try:
|
|
353
|
-
comparison = self._repo.compare_versions(current_version, target_version)
|
|
354
|
-
|
|
355
|
-
if comparison >= 0:
|
|
356
|
-
# Already at or past target version (0 = equal, 1 = higher)
|
|
357
|
-
return result
|
|
358
|
-
except Exception as e:
|
|
359
|
-
# If version comparison fails (invalid format), log and continue
|
|
360
|
-
# This allows migration to proceed even if version format is unexpected
|
|
361
|
-
result['errors'].append(
|
|
362
|
-
f"Could not compare versions {current_version} and {target_version}: {e}. "
|
|
363
|
-
f"Continuing with migration attempt."
|
|
364
|
-
)
|
|
292
|
+
comparison = self._repo.compare_versions(current_version, target_version)
|
|
293
|
+
if comparison >= 0:
|
|
294
|
+
return result
|
|
365
295
|
|
|
366
|
-
# Ensure all active branches are in sync with origin before touching anything
|
|
367
|
-
self._ensure_active_branches_synced()
|
|
368
|
-
|
|
369
|
-
# Get pending migrations
|
|
370
296
|
pending = self.get_pending_migrations(current_version, target_version)
|
|
371
297
|
|
|
372
|
-
|
|
373
|
-
if pending:
|
|
298
|
+
try:
|
|
374
299
|
for version_str, migration_dir in pending:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
result['migrations_applied'].append(migration_result)
|
|
300
|
+
migration_result = self.apply_migration(version_str, migration_dir)
|
|
301
|
+
result['migrations_applied'].append(migration_result)
|
|
378
302
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
raise
|
|
303
|
+
# hop_version setter calls write() internally — one write only
|
|
304
|
+
self._repo.config.hop_version = target_version
|
|
382
305
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
self._repo._Repo__config.hop_version = target_version
|
|
388
|
-
self._repo._Repo__config.write()
|
|
389
|
-
|
|
390
|
-
# Update half_orm_dev version in pyproject.toml
|
|
391
|
-
self._update_pyproject_dependency_version(target_version)
|
|
306
|
+
self._update_pyproject_dependency_version(target_version)
|
|
307
|
+
except Exception:
|
|
308
|
+
self._repo.hgit.rollback_to_snapshot()
|
|
309
|
+
raise
|
|
392
310
|
|
|
393
|
-
|
|
394
|
-
all_sync_files = ['pyproject.toml'] # Always sync pyproject.toml
|
|
311
|
+
all_sync_files = ['pyproject.toml']
|
|
395
312
|
for migration in result['migrations_applied']:
|
|
396
313
|
all_sync_files.extend(migration.get('sync_files', []))
|
|
397
|
-
# Remove duplicates while preserving order
|
|
398
314
|
all_sync_files = list(dict.fromkeys(all_sync_files))
|
|
399
315
|
|
|
400
|
-
# Create Git commit if requested
|
|
401
316
|
if create_commit and self._repo.hgit:
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
target_version,
|
|
406
|
-
result['migrations_applied']
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# Commit and sync to active branches (including migration files)
|
|
410
|
-
sync_result = self._repo.commit_and_sync_to_active_branches(
|
|
411
|
-
message=commit_msg,
|
|
412
|
-
reason=f"migration {current_version} → {target_version}",
|
|
413
|
-
files=all_sync_files
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
result['commit_created'] = True
|
|
417
|
-
result['commit_message'] = commit_msg
|
|
418
|
-
result['commit_pushed'] = True
|
|
419
|
-
result['sync_result'] = sync_result
|
|
317
|
+
commit_msg = self._create_migration_commit_message(
|
|
318
|
+
current_version, target_version, result['migrations_applied']
|
|
319
|
+
)
|
|
420
320
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
321
|
+
sync_result = self._repo.commit_and_sync_to_active_branches(
|
|
322
|
+
message=commit_msg,
|
|
323
|
+
reason=f"migration {current_version} → {target_version}",
|
|
324
|
+
files=all_sync_files
|
|
325
|
+
)
|
|
425
326
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
current_version, target_version
|
|
430
|
-
)
|
|
431
|
-
except Exception as regen_err:
|
|
432
|
-
result['errors'].append(
|
|
433
|
-
f"Module regeneration after migration failed: {regen_err}"
|
|
434
|
-
)
|
|
327
|
+
result['commit_created'] = True
|
|
328
|
+
result['commit_message'] = commit_msg
|
|
329
|
+
result['sync_result'] = sync_result
|
|
435
330
|
|
|
331
|
+
try:
|
|
332
|
+
self._create_migration_tag(current_version, target_version, sync_result)
|
|
436
333
|
except Exception as e:
|
|
437
|
-
|
|
438
|
-
result['errors'].append(f"Failed to create commit: {e}")
|
|
334
|
+
result['errors'].append(f"Migration tag creation failed: {e}")
|
|
439
335
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
336
|
+
try:
|
|
337
|
+
self._regenerate_modules_after_migration(current_version, target_version)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
result['errors'].append(f"Module regeneration after migration failed: {e}")
|
|
444
340
|
|
|
445
341
|
return result
|
|
446
342
|
|
|
@@ -624,12 +520,11 @@ class MigrationManager:
|
|
|
624
520
|
b['name'] for b in branches_status.get('release_branches', [])
|
|
625
521
|
if b.get('exists_on_remote', True)
|
|
626
522
|
]
|
|
627
|
-
# ho-patch/* branches
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
all_branches = ['ho-prod'] + release_branches
|
|
523
|
+
# ho-patch/* branches receive .hop/ and pyproject.toml updates but skip
|
|
524
|
+
# module regeneration: their schema may include tables not yet in
|
|
525
|
+
# production, so generate() would delete modules for those new tables.
|
|
526
|
+
# The developer re-runs `hop patch apply` to regenerate with the correct schema.
|
|
527
|
+
all_branches = ['ho-prod'] + release_branches + patch_branches
|
|
633
528
|
|
|
634
529
|
for branch in all_branches:
|
|
635
530
|
try:
|
|
@@ -651,7 +546,8 @@ class MigrationManager:
|
|
|
651
546
|
# ho-prod and ho-patch/*: use production schema
|
|
652
547
|
repo.restore_database_from_schema(skip_bootstrap=True)
|
|
653
548
|
|
|
654
|
-
|
|
549
|
+
if not branch in patch_branches:
|
|
550
|
+
_modules.generate(repo)
|
|
655
551
|
repo.hgit.add(package_dir)
|
|
656
552
|
if not git_repo.git.diff('--cached', '--name-only').strip():
|
|
657
553
|
continue
|
|
@@ -796,10 +692,7 @@ class MigrationManager:
|
|
|
796
692
|
Returns:
|
|
797
693
|
True if migration/update is needed
|
|
798
694
|
"""
|
|
799
|
-
|
|
800
|
-
return False
|
|
801
|
-
|
|
802
|
-
config_version = self._repo._Repo__config.hop_version
|
|
695
|
+
config_version = self._repo.config.hop_version
|
|
803
696
|
|
|
804
697
|
# If no hop_version is configured, no migration needed
|
|
805
698
|
if not config_version:
|
|
@@ -55,7 +55,7 @@ def migrate(repo):
|
|
|
55
55
|
print(f" pyproject.toml not found.")
|
|
56
56
|
if click.confirm(" Create pyproject.toml from template?", default=True):
|
|
57
57
|
try:
|
|
58
|
-
package_name = repo.
|
|
58
|
+
package_name = repo.config.package_name
|
|
59
59
|
template_path = os.path.join(TEMPLATE_DIRS, 'pyproject.toml')
|
|
60
60
|
template = utils.read(template_path)
|
|
61
61
|
|
|
@@ -59,7 +59,7 @@ def migrate(repo):
|
|
|
59
59
|
import click
|
|
60
60
|
|
|
61
61
|
base_dir = Path(repo.base_dir)
|
|
62
|
-
package_name = repo.
|
|
62
|
+
package_name = repo.config.package_name
|
|
63
63
|
|
|
64
64
|
_migrate_init(repo, base_dir, package_name, click)
|
|
65
65
|
_migrate_conftest(repo, base_dir, package_name, click)
|