half-orm-dev 1.0.0a18__tar.gz → 1.0.0a19__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.0a18/half_orm_dev.egg-info → half_orm_dev-1.0.0a19}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/main.py +30 -13
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patch_manager.py +1 -6
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/release_manager.py +37 -249
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/repo.py +18 -11
- half_orm_dev-1.0.0a19/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19/half_orm_dev.egg-info}/PKG-INFO +1 -1
- half_orm_dev-1.0.0a18/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/LICENSE +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/README.md +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/bootstrap_manager.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/revert_migration.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/hgit.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migration_manager.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/SOURCES.txt +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/entry_points.txt +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/setup.py +0 -0
|
@@ -4,6 +4,8 @@ Main CLI module - Creates and configures the CLI group
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
import functools
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
from half_orm_dev.repo import Repo, OutdatedHalfORMDevError
|
|
9
11
|
from half_orm import utils
|
|
@@ -39,15 +41,16 @@ class Hop:
|
|
|
39
41
|
but will be blocked by the decorator at execution time.
|
|
40
42
|
"""
|
|
41
43
|
if self.needs_hop_upgrade:
|
|
42
|
-
#
|
|
43
|
-
# Commands will be blocked by decorator, but we need them in the list
|
|
44
|
-
# so Click doesn't show "No such command" error
|
|
45
|
-
return ['check', 'migrate']
|
|
44
|
+
return [] # handled at invocation time in the dev group callback
|
|
46
45
|
|
|
47
46
|
if not self.repo_checked:
|
|
48
47
|
# Outside hop repository - commands for project initialization
|
|
49
48
|
return ['init', 'clone']
|
|
50
49
|
|
|
50
|
+
# PRODUCTION ENVIRONMENT — read-only, no migrations, no dev commands
|
|
51
|
+
if self.__repo.database.production:
|
|
52
|
+
return ['update', 'upgrade', 'bootstrap']
|
|
53
|
+
|
|
51
54
|
if self.__repo.needs_migration():
|
|
52
55
|
return ['migrate']
|
|
53
56
|
|
|
@@ -56,14 +59,9 @@ class Hop:
|
|
|
56
59
|
# Sync-only mode (no metadata)
|
|
57
60
|
return ['sync-package', 'check']
|
|
58
61
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return ['update', 'upgrade', 'bootstrap']
|
|
63
|
-
else:
|
|
64
|
-
# DEVELOPMENT ENVIRONMENT - Patch development
|
|
65
|
-
return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
|
|
66
|
-
'revert-migration']
|
|
62
|
+
# DEVELOPMENT ENVIRONMENT - Patch development
|
|
63
|
+
return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
|
|
64
|
+
'revert-migration']
|
|
67
65
|
|
|
68
66
|
@property
|
|
69
67
|
def repo_checked(self):
|
|
@@ -137,9 +135,28 @@ def create_cli_group():
|
|
|
137
135
|
|
|
138
136
|
@click.group(cls=VersionCheckGroup, invoke_without_command=True)
|
|
139
137
|
@click.pass_context
|
|
140
|
-
@check_version_before_invoke
|
|
141
138
|
def dev(ctx):
|
|
142
139
|
"""halfORM development tools - Git-centric patch management and database synchronization"""
|
|
140
|
+
if hop.needs_hop_upgrade:
|
|
141
|
+
error = hop.hop_upgrade_error
|
|
142
|
+
required = error.required_version
|
|
143
|
+
installed = error.installed_version
|
|
144
|
+
click.echo(f"\n Ce repo requiers half-orm-dev {required} "
|
|
145
|
+
f"(installed : {installed}).")
|
|
146
|
+
click.echo(f" Installing the required version...")
|
|
147
|
+
try:
|
|
148
|
+
subprocess.run(
|
|
149
|
+
[sys.executable, '-m', 'pip', 'install', f'half-orm-dev=={required}'],
|
|
150
|
+
check=True,
|
|
151
|
+
)
|
|
152
|
+
except subprocess.CalledProcessError:
|
|
153
|
+
click.echo(f"\n Installation failed.", err=True)
|
|
154
|
+
click.echo(f" Run manually : pip install half-orm-dev=={required}", err=True)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
click.echo(f" ✓ half-orm-dev {required} installed. Restarting...\n")
|
|
157
|
+
os.execv(sys.argv[0], sys.argv)
|
|
158
|
+
return
|
|
159
|
+
|
|
143
160
|
if ctx.invoked_subcommand is None:
|
|
144
161
|
# Show repo state when no subcommand is provided
|
|
145
162
|
if hop.repo_checked:
|
|
@@ -18,6 +18,7 @@ from typing import List, Dict, Optional, Tuple, Any
|
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
import click
|
|
20
20
|
from git.exc import GitCommandError
|
|
21
|
+
from packaging.version import Version, InvalidVersion
|
|
21
22
|
|
|
22
23
|
from half_orm import utils
|
|
23
24
|
from half_orm_dev import modules
|
|
@@ -2146,9 +2147,6 @@ class PatchManager:
|
|
|
2146
2147
|
- Commit the updated schema
|
|
2147
2148
|
4. Return to original branch
|
|
2148
2149
|
"""
|
|
2149
|
-
from packaging.version import Version
|
|
2150
|
-
from half_orm_dev.release_file import ReleaseFile
|
|
2151
|
-
|
|
2152
2150
|
original_branch = self._repo.hgit.branch
|
|
2153
2151
|
current_ver = Version(version)
|
|
2154
2152
|
|
|
@@ -2197,9 +2195,6 @@ class PatchManager:
|
|
|
2197
2195
|
Args:
|
|
2198
2196
|
version: Current release version that was just updated
|
|
2199
2197
|
"""
|
|
2200
|
-
from packaging.version import Version
|
|
2201
|
-
from half_orm_dev.release_file import ReleaseFile
|
|
2202
|
-
|
|
2203
2198
|
if not hasattr(self, '_pending_higher_releases') or not self._pending_higher_releases:
|
|
2204
2199
|
return
|
|
2205
2200
|
|
|
@@ -13,16 +13,17 @@ import subprocess
|
|
|
13
13
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Optional, Tuple, List, Dict, Literal
|
|
16
|
-
from dataclasses import dataclass
|
|
17
16
|
from datetime import datetime, timezone
|
|
18
17
|
|
|
19
18
|
import click
|
|
20
19
|
|
|
21
20
|
from git.exc import GitCommandError
|
|
21
|
+
from packaging.version import Version, InvalidVersion
|
|
22
22
|
from half_orm_dev.decorators import with_dynamic_branch_lock
|
|
23
23
|
from half_orm import utils
|
|
24
24
|
from half_orm_dev.release_file import ReleaseFile
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
class ReleaseManagerError(Exception):
|
|
27
28
|
"""Base exception for ReleaseManager operations."""
|
|
28
29
|
pass
|
|
@@ -38,69 +39,6 @@ class ReleaseFileError(ReleaseManagerError):
|
|
|
38
39
|
pass
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
@dataclass
|
|
42
|
-
class Version:
|
|
43
|
-
"""Semantic version with stage information."""
|
|
44
|
-
major: int
|
|
45
|
-
minor: int
|
|
46
|
-
patch: int
|
|
47
|
-
stage: Optional[str] = None # None, "stage", "rc1", "rc2", "hotfix1", etc.
|
|
48
|
-
|
|
49
|
-
def __str__(self) -> str:
|
|
50
|
-
"""String representation of version."""
|
|
51
|
-
base = f"{self.major}.{self.minor}.{self.patch}"
|
|
52
|
-
if self.stage:
|
|
53
|
-
return f"{base}-{self.stage}"
|
|
54
|
-
return base
|
|
55
|
-
|
|
56
|
-
def __lt__(self, other: 'Version') -> bool:
|
|
57
|
-
"""Compare versions for sorting."""
|
|
58
|
-
# Compare base version first
|
|
59
|
-
if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
|
|
60
|
-
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
|
61
|
-
|
|
62
|
-
# If base versions equal, compare stages
|
|
63
|
-
# Priority: production (None) > rc > stage > hotfix
|
|
64
|
-
stage_priority = {
|
|
65
|
-
None: 4, # Production (highest)
|
|
66
|
-
'rc': 3, # Release candidate
|
|
67
|
-
'stage': 2, # Development stage
|
|
68
|
-
'hotfix': 1 # Hotfix (lowest)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# Extract stage type (rc1 → rc, hotfix2 → hotfix)
|
|
72
|
-
self_stage_type = self._get_stage_type()
|
|
73
|
-
other_stage_type = other._get_stage_type()
|
|
74
|
-
|
|
75
|
-
self_priority = stage_priority.get(self_stage_type, 0)
|
|
76
|
-
other_priority = stage_priority.get(other_stage_type, 0)
|
|
77
|
-
|
|
78
|
-
# If different stage types, compare by priority
|
|
79
|
-
if self_priority != other_priority:
|
|
80
|
-
return self_priority < other_priority
|
|
81
|
-
|
|
82
|
-
# Same stage type - compare stage strings for RC/hotfix numbers
|
|
83
|
-
# rc2 > rc1, hotfix2 > hotfix1
|
|
84
|
-
if self.stage and other.stage:
|
|
85
|
-
return self.stage < other.stage
|
|
86
|
-
|
|
87
|
-
return False
|
|
88
|
-
|
|
89
|
-
def _get_stage_type(self) -> Optional[str]:
|
|
90
|
-
"""Extract stage type from stage string."""
|
|
91
|
-
if not self.stage:
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
if self.stage == 'stage':
|
|
95
|
-
return 'stage'
|
|
96
|
-
elif self.stage.startswith('rc'):
|
|
97
|
-
return 'rc'
|
|
98
|
-
elif self.stage.startswith('hotfix'):
|
|
99
|
-
return 'hotfix'
|
|
100
|
-
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
|
|
104
42
|
class ReleaseManager:
|
|
105
43
|
"""
|
|
106
44
|
Manages release files and version lifecycle.
|
|
@@ -474,21 +412,16 @@ class ReleaseManager:
|
|
|
474
412
|
if not release_files:
|
|
475
413
|
return None
|
|
476
414
|
|
|
477
|
-
# Parse all valid versions
|
|
478
415
|
versions = []
|
|
479
|
-
for
|
|
416
|
+
for f in release_files:
|
|
480
417
|
try:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
except ReleaseVersionError:
|
|
484
|
-
# Ignore files with invalid format
|
|
418
|
+
versions.append(Version(f.stem))
|
|
419
|
+
except InvalidVersion:
|
|
485
420
|
continue
|
|
486
421
|
|
|
487
422
|
if not versions:
|
|
488
423
|
return None
|
|
489
424
|
|
|
490
|
-
# Sort versions and return latest
|
|
491
|
-
# Version.__lt__ handles sorting with stage priority
|
|
492
425
|
return max(versions)
|
|
493
426
|
|
|
494
427
|
|
|
@@ -559,83 +492,11 @@ class ReleaseManager:
|
|
|
559
492
|
elif increment_type == 'minor':
|
|
560
493
|
return f"{current_version.major}.{current_version.minor + 1}.0"
|
|
561
494
|
elif increment_type == 'patch':
|
|
562
|
-
return f"{current_version.major}.{current_version.minor}.{current_version.
|
|
495
|
+
return f"{current_version.major}.{current_version.minor}.{current_version.micro + 1}"
|
|
563
496
|
|
|
564
497
|
# Should never reach here due to validation above
|
|
565
498
|
raise ReleaseVersionError(f"Unexpected increment type: {increment_type}")
|
|
566
499
|
|
|
567
|
-
@classmethod
|
|
568
|
-
def parse_version_from_filename(cls, filename: str) -> Version:
|
|
569
|
-
"""
|
|
570
|
-
Parse version from release filename.
|
|
571
|
-
|
|
572
|
-
Extracts semantic version and stage from release filename.
|
|
573
|
-
|
|
574
|
-
Supported formats:
|
|
575
|
-
- X.Y.Z.txt → Version(X, Y, Z, stage=None)
|
|
576
|
-
- X.Y.Z-stage.txt → Version(X, Y, Z, stage="stage")
|
|
577
|
-
- X.Y.Z-rc1.txt → Version(X, Y, Z, stage="rc1")
|
|
578
|
-
- X.Y.Z-hotfix1.txt → Version(X, Y, Z, stage="hotfix1")
|
|
579
|
-
|
|
580
|
-
Args:
|
|
581
|
-
filename: Release filename (e.g., "1.3.5-rc2.txt")
|
|
582
|
-
|
|
583
|
-
Returns:
|
|
584
|
-
Version: Parsed version object
|
|
585
|
-
|
|
586
|
-
Raises:
|
|
587
|
-
ReleaseVersionError: If filename format invalid
|
|
588
|
-
|
|
589
|
-
Examples:
|
|
590
|
-
ver = release_mgr.parse_version_from_filename("1.3.5.txt")
|
|
591
|
-
# Version(1, 3, 5, stage=None)
|
|
592
|
-
|
|
593
|
-
ver = release_mgr.parse_version_from_filename("1.4.0-stage.txt")
|
|
594
|
-
# Version(1, 4, 0, stage="stage")
|
|
595
|
-
|
|
596
|
-
ver = release_mgr.parse_version_from_filename("1.3.5-rc2.txt")
|
|
597
|
-
# Version(1, 3, 5, stage="rc2")
|
|
598
|
-
"""
|
|
599
|
-
# Extract just filename if path provided
|
|
600
|
-
filename = Path(filename).name
|
|
601
|
-
|
|
602
|
-
# Validate not empty
|
|
603
|
-
if not filename:
|
|
604
|
-
raise ReleaseVersionError("Invalid format: empty filename")
|
|
605
|
-
|
|
606
|
-
# Must end with .txt
|
|
607
|
-
if not filename.endswith('.txt'):
|
|
608
|
-
raise ReleaseVersionError(f"Invalid format: missing .txt extension in '{filename}'")
|
|
609
|
-
|
|
610
|
-
# Remove .txt extension
|
|
611
|
-
version_str = filename[:-4]
|
|
612
|
-
|
|
613
|
-
# Pattern: X.Y.Z or X.Y.Z-stage or X.Y.Z-rc1 or X.Y.Z-hotfix1
|
|
614
|
-
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(stage|rc\d+|hotfix\d+))?$'
|
|
615
|
-
|
|
616
|
-
match = re.match(pattern, version_str)
|
|
617
|
-
|
|
618
|
-
if not match:
|
|
619
|
-
raise ReleaseVersionError(
|
|
620
|
-
f"Invalid format: '{filename}' does not match X.Y.Z[-stage].txt pattern"
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
major, minor, patch, stage = match.groups()
|
|
624
|
-
|
|
625
|
-
# Convert to integers
|
|
626
|
-
try:
|
|
627
|
-
major = int(major)
|
|
628
|
-
minor = int(minor)
|
|
629
|
-
patch = int(patch)
|
|
630
|
-
except ValueError:
|
|
631
|
-
raise ReleaseVersionError(f"Invalid format: non-numeric version components in '{filename}'")
|
|
632
|
-
|
|
633
|
-
# Validate non-negative
|
|
634
|
-
if major < 0 or minor < 0 or patch < 0:
|
|
635
|
-
raise ReleaseVersionError(f"Invalid format: negative version numbers in '{filename}'")
|
|
636
|
-
|
|
637
|
-
return Version(major, minor, patch, stage)
|
|
638
|
-
|
|
639
500
|
def get_next_release_version(self) -> Optional[str]:
|
|
640
501
|
"""
|
|
641
502
|
Détermine LA prochaine release à déployer.
|
|
@@ -646,8 +507,7 @@ class ReleaseManager:
|
|
|
646
507
|
production_str = self._get_production_version()
|
|
647
508
|
|
|
648
509
|
for level in ['patch', 'minor', 'major']:
|
|
649
|
-
next_version = self.calculate_next_version(
|
|
650
|
-
self.parse_version_from_filename(f"{production_str}.txt"), level)
|
|
510
|
+
next_version = self.calculate_next_version(Version(production_str), level)
|
|
651
511
|
|
|
652
512
|
# Cherche RC ou patches TOML pour cette version
|
|
653
513
|
rc_pattern = f"{next_version}-rc*.txt"
|
|
@@ -662,12 +522,12 @@ class ReleaseManager:
|
|
|
662
522
|
"""
|
|
663
523
|
Liste tous les fichiers <label> pour une version, triés par numéro.
|
|
664
524
|
|
|
665
|
-
|
|
666
|
-
|
|
525
|
+
RC uses hyphen separator (1.3.6-rc1.txt).
|
|
526
|
+
Post-releases use dot separator (1.3.6.post1.txt).
|
|
667
527
|
"""
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
label_pattern = re.compile(
|
|
528
|
+
sep = '.' if label == 'post' else '-'
|
|
529
|
+
pattern = f"{version}{sep}{label}*.txt"
|
|
530
|
+
label_pattern = re.compile(rf'[.-]{label}(\d+)\.txt$')
|
|
671
531
|
files = list(self._releases_dir.glob(pattern))
|
|
672
532
|
|
|
673
533
|
return sorted(files, key=lambda f: int(re.search(label_pattern, f.name).group(1)))
|
|
@@ -806,7 +666,7 @@ class ReleaseManager:
|
|
|
806
666
|
else:
|
|
807
667
|
# Production: read from hotfix snapshot if it exists
|
|
808
668
|
# This handles the case where we're applying a hotfix release
|
|
809
|
-
hotfix_files = sorted(self._releases_dir.glob(f"{version}
|
|
669
|
+
hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
|
|
810
670
|
if hotfix_files:
|
|
811
671
|
# Apply the latest hotfix
|
|
812
672
|
stage_patches = self.read_release_patches(hotfix_files[-1].name)
|
|
@@ -892,7 +752,7 @@ class ReleaseManager:
|
|
|
892
752
|
all_patches.extend(self.read_release_patches(base_file))
|
|
893
753
|
|
|
894
754
|
# 2. Hotfix patches in order
|
|
895
|
-
hotfix_files = sorted(self._releases_dir.glob(f"{version}
|
|
755
|
+
hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
|
|
896
756
|
for hotfix_file in hotfix_files:
|
|
897
757
|
all_patches.extend(self.read_release_patches(hotfix_file.name))
|
|
898
758
|
|
|
@@ -1483,7 +1343,7 @@ class ReleaseManager:
|
|
|
1483
1343
|
patches = [line.strip() for line in content.split('\n') if line.strip()]
|
|
1484
1344
|
|
|
1485
1345
|
# Only include releases newer than current version
|
|
1486
|
-
if
|
|
1346
|
+
if Version(version) > Version(current_version):
|
|
1487
1347
|
available_releases.append({
|
|
1488
1348
|
'tag': tag,
|
|
1489
1349
|
'version': version,
|
|
@@ -1550,43 +1410,19 @@ class ReleaseManager:
|
|
|
1550
1410
|
except Exception as e:
|
|
1551
1411
|
raise ReleaseManagerError(f"Failed to read tags from repository: {e}")
|
|
1552
1412
|
|
|
1553
|
-
# Filter for release tags
|
|
1554
|
-
release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d
|
|
1413
|
+
# Filter for release tags: v1.3.5, v1.3.5-rc1, v1.3.5.post1
|
|
1414
|
+
release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d+|\.post\d+)?$')
|
|
1555
1415
|
release_tags = []
|
|
1556
1416
|
|
|
1557
1417
|
for tag in all_tags:
|
|
1558
1418
|
tag_name = tag.name
|
|
1559
1419
|
if release_pattern.match(tag_name):
|
|
1560
|
-
# Filter RC tags unless explicitly allowed
|
|
1561
1420
|
if '-rc' in tag_name and not allow_rc:
|
|
1562
1421
|
continue
|
|
1563
1422
|
release_tags.append(tag_name)
|
|
1564
1423
|
|
|
1565
|
-
# Sort
|
|
1566
|
-
|
|
1567
|
-
"""Extract sortable version tuple from tag name."""
|
|
1568
|
-
# Remove 'v' prefix
|
|
1569
|
-
version_str = tag_name[1:]
|
|
1570
|
-
|
|
1571
|
-
# Split version and suffix
|
|
1572
|
-
if '-rc' in version_str:
|
|
1573
|
-
base_ver, rc_suffix = version_str.split('-rc')
|
|
1574
|
-
rc_num = int(rc_suffix)
|
|
1575
|
-
suffix_weight = (1, rc_num) # RC comes before production
|
|
1576
|
-
elif '-hotfix' in version_str:
|
|
1577
|
-
base_ver, hotfix_suffix = version_str.split('-hotfix')
|
|
1578
|
-
hotfix_num = int(hotfix_suffix)
|
|
1579
|
-
suffix_weight = (2, hotfix_num) # Hotfix comes after production
|
|
1580
|
-
else:
|
|
1581
|
-
base_ver = version_str
|
|
1582
|
-
suffix_weight = (1.5, 0) # Production between RC and hotfix
|
|
1583
|
-
|
|
1584
|
-
# Parse base version
|
|
1585
|
-
major, minor, patch = map(int, base_ver.split('.'))
|
|
1586
|
-
|
|
1587
|
-
return (major, minor, patch, suffix_weight)
|
|
1588
|
-
|
|
1589
|
-
release_tags.sort(key=version_key)
|
|
1424
|
+
# Sort using packaging.version (PEP 440 ordering)
|
|
1425
|
+
release_tags.sort(key=lambda t: Version(t[1:]))
|
|
1590
1426
|
|
|
1591
1427
|
return release_tags
|
|
1592
1428
|
|
|
@@ -1621,77 +1457,31 @@ class ReleaseManager:
|
|
|
1621
1457
|
path = mgr._calculate_upgrade_path("1.4.0", "1.4.0")
|
|
1622
1458
|
# → []
|
|
1623
1459
|
"""
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
target_version = self.parse_version_from_filename(f"{target}.txt")
|
|
1460
|
+
current_version = Version(current)
|
|
1461
|
+
target_version = Version(target)
|
|
1627
1462
|
|
|
1628
|
-
# If same version, no upgrade needed
|
|
1629
1463
|
if current == target:
|
|
1630
1464
|
return []
|
|
1631
1465
|
|
|
1632
|
-
# Get all available release tags (production only)
|
|
1633
1466
|
available_tags = self._get_available_release_tags(allow_rc=False)
|
|
1634
1467
|
|
|
1635
|
-
# Extract versions from tags and parse them
|
|
1636
1468
|
available_versions = []
|
|
1637
1469
|
for tag in available_tags:
|
|
1638
|
-
# Remove 'v' prefix: v1.3.6 → 1.3.6
|
|
1639
1470
|
version_str = tag[1:] if tag.startswith('v') else tag
|
|
1640
|
-
|
|
1641
|
-
# Skip if not a valid production version format
|
|
1642
|
-
if not re.match(r'^\d+\.\d+\.\d+$', version_str):
|
|
1643
|
-
continue
|
|
1644
|
-
|
|
1645
1471
|
try:
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
except (ReleaseManagerError, ValueError):
|
|
1472
|
+
available_versions.append((version_str, Version(version_str)))
|
|
1473
|
+
except InvalidVersion:
|
|
1649
1474
|
continue
|
|
1650
1475
|
|
|
1651
|
-
|
|
1652
|
-
available_versions.sort(key=lambda x: (x[1].major, x[1].minor, x[1].patch))
|
|
1476
|
+
available_versions.sort(key=lambda x: x[1])
|
|
1653
1477
|
|
|
1654
|
-
# Build sequential path from current to target
|
|
1655
1478
|
path = []
|
|
1656
1479
|
for version_str, version in available_versions:
|
|
1657
|
-
|
|
1658
|
-
if (version.major, version.minor, version.patch) <= \
|
|
1659
|
-
(current_version.major, current_version.minor, current_version.patch):
|
|
1660
|
-
continue
|
|
1661
|
-
|
|
1662
|
-
# Add versions <= target
|
|
1663
|
-
if (version.major, version.minor, version.patch) <= \
|
|
1664
|
-
(target_version.major, target_version.minor, target_version.patch):
|
|
1480
|
+
if current_version < version <= target_version:
|
|
1665
1481
|
path.append(version_str)
|
|
1666
1482
|
|
|
1667
1483
|
return path
|
|
1668
1484
|
|
|
1669
|
-
def _version_is_newer(self, version1: str, version2: str) -> bool:
|
|
1670
|
-
"""
|
|
1671
|
-
Compare two version strings to check if version1 is newer than version2.
|
|
1672
|
-
|
|
1673
|
-
Args:
|
|
1674
|
-
version1: First version (e.g., "1.3.6", "1.3.6-rc1")
|
|
1675
|
-
version2: Second version (e.g., "1.3.5")
|
|
1676
|
-
|
|
1677
|
-
Returns:
|
|
1678
|
-
bool: True if version1 > version2
|
|
1679
|
-
|
|
1680
|
-
Examples:
|
|
1681
|
-
_version_is_newer("1.3.6", "1.3.5") # → True
|
|
1682
|
-
_version_is_newer("1.3.5", "1.3.6") # → False
|
|
1683
|
-
_version_is_newer("1.3.6-rc1", "1.3.5") # → True
|
|
1684
|
-
"""
|
|
1685
|
-
# Extract base versions (remove suffix)
|
|
1686
|
-
base1 = version1.split('-')[0]
|
|
1687
|
-
base2 = version2.split('-')[0]
|
|
1688
|
-
|
|
1689
|
-
# Parse versions
|
|
1690
|
-
parts1 = tuple(map(int, base1.split('.')))
|
|
1691
|
-
parts2 = tuple(map(int, base2.split('.')))
|
|
1692
|
-
|
|
1693
|
-
return parts1 > parts2
|
|
1694
|
-
|
|
1695
1485
|
def upgrade_production(
|
|
1696
1486
|
self,
|
|
1697
1487
|
to_version: Optional[str] = None,
|
|
@@ -2431,8 +2221,6 @@ class ReleaseManager:
|
|
|
2431
2221
|
Returns:
|
|
2432
2222
|
Version string of base release, or None if should use prod schema
|
|
2433
2223
|
"""
|
|
2434
|
-
from packaging.version import Version
|
|
2435
|
-
|
|
2436
2224
|
new_ver = Version(new_version)
|
|
2437
2225
|
model_dir = Path(self._repo.model_dir)
|
|
2438
2226
|
|
|
@@ -3114,7 +2902,7 @@ class ReleaseManager:
|
|
|
3114
2902
|
return 1
|
|
3115
2903
|
|
|
3116
2904
|
last_file = files[-1].name
|
|
3117
|
-
reg_ex = rf'
|
|
2905
|
+
reg_ex = rf'[.-]{label}(\d+)\.txt'
|
|
3118
2906
|
match = re.search(reg_ex, last_file)
|
|
3119
2907
|
if match:
|
|
3120
2908
|
last_num = int(match.group(1))
|
|
@@ -3303,26 +3091,26 @@ class ReleaseManager:
|
|
|
3303
3091
|
f" 3. OR move to another release (edit patches file manually)"
|
|
3304
3092
|
)
|
|
3305
3093
|
|
|
3306
|
-
# 3. Determine next
|
|
3307
|
-
|
|
3308
|
-
hotfix_tag = f"v{version}
|
|
3094
|
+
# 3. Determine next post-release number
|
|
3095
|
+
post_num = self._get_latest_label_number(version, 'post')
|
|
3096
|
+
hotfix_tag = f"v{version}.post{post_num}"
|
|
3309
3097
|
|
|
3310
3098
|
# 4. Switch to ho-prod and merge
|
|
3311
3099
|
self._repo.hgit.checkout("ho-prod")
|
|
3312
3100
|
|
|
3313
3101
|
# Merge ho-release/X.Y.Z into ho-prod
|
|
3314
|
-
merge_msg = f"[release] Merge hotfix %{version}
|
|
3102
|
+
merge_msg = f"[release] Merge hotfix %{version}.post{post_num}"
|
|
3315
3103
|
self._repo.hgit.merge(current_branch, message=merge_msg)
|
|
3316
3104
|
|
|
3317
|
-
# 5. Create
|
|
3105
|
+
# 5. Create post-release snapshot file from staged patches
|
|
3318
3106
|
toml_file = self._releases_dir / f"{version}-patches.toml"
|
|
3319
|
-
hotfix_file = self._releases_dir / f"{version}
|
|
3107
|
+
hotfix_file = self._releases_dir / f"{version}.post{post_num}.txt"
|
|
3320
3108
|
|
|
3321
3109
|
if release_file.exists():
|
|
3322
3110
|
# Get staged patches from TOML file
|
|
3323
3111
|
staged_patches = release_file.get_patches(status="staged")
|
|
3324
3112
|
|
|
3325
|
-
# Write snapshot to
|
|
3113
|
+
# Write snapshot to post-release TXT file (production format)
|
|
3326
3114
|
hotfix_file.write_text("\n".join(staged_patches) + "\n" if staged_patches else "", encoding='utf-8')
|
|
3327
3115
|
# Delete TOML patches file (no longer needed)
|
|
3328
3116
|
if toml_file.exists():
|
|
@@ -3333,12 +3121,12 @@ class ReleaseManager:
|
|
|
3333
3121
|
|
|
3334
3122
|
# 7. Commit release file changes and sync to active branches
|
|
3335
3123
|
sync_result = self._repo.commit_and_sync_to_active_branches(
|
|
3336
|
-
message=f"[HOP] Finalize hotfix %{version}
|
|
3337
|
-
reason=f"hotfix {version}
|
|
3124
|
+
message=f"[HOP] Finalize hotfix %{version}.post{post_num} release files",
|
|
3125
|
+
reason=f"hotfix {version}.post{post_num}"
|
|
3338
3126
|
)
|
|
3339
3127
|
|
|
3340
|
-
# 8. Create
|
|
3341
|
-
self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}
|
|
3128
|
+
# 8. Create post-release tag on ho-prod
|
|
3129
|
+
self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}.post{post_num}")
|
|
3342
3130
|
self._repo.hgit.push_tag(hotfix_tag)
|
|
3343
3131
|
|
|
3344
3132
|
deleted_branches = []
|
|
@@ -484,6 +484,14 @@ class Repo:
|
|
|
484
484
|
'errors': []
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
if self.production:
|
|
488
|
+
raise RepoError(
|
|
489
|
+
"Repository migration is not available on a production server.\n\n"
|
|
490
|
+
" Production servers are read-only: no commits or pushes to origin.\n"
|
|
491
|
+
" Run 'hop migrate' on a development machine first, then deploy."
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
self._migration_running = True
|
|
487
495
|
try:
|
|
488
496
|
# Create migration manager
|
|
489
497
|
migration_mgr = MigrationManager(self)
|
|
@@ -584,18 +592,17 @@ class Repo:
|
|
|
584
592
|
print(f" You are now on ho-prod", file=sys.stderr)
|
|
585
593
|
|
|
586
594
|
except RepoError:
|
|
587
|
-
# Re-raise RepoError (for branch check)
|
|
588
595
|
raise
|
|
589
596
|
except MigrationManagerError as e:
|
|
590
|
-
# Log migration errors
|
|
591
597
|
error_msg = f"Migration failed: {e}"
|
|
592
598
|
result['errors'].append(error_msg)
|
|
593
599
|
raise RepoError(error_msg) from e
|
|
594
600
|
except Exception as e:
|
|
595
|
-
# Catch any unexpected errors
|
|
596
601
|
error_msg = f"Unexpected migration error: {e}"
|
|
597
602
|
result['errors'].append(error_msg)
|
|
598
603
|
raise RepoError(error_msg) from e
|
|
604
|
+
finally:
|
|
605
|
+
self._migration_running = False
|
|
599
606
|
|
|
600
607
|
return result
|
|
601
608
|
|
|
@@ -918,9 +925,11 @@ class Repo:
|
|
|
918
925
|
installed_version = hop_version()
|
|
919
926
|
required_version = self.__config.hop_version
|
|
920
927
|
|
|
921
|
-
# Validate version compatibility
|
|
922
|
-
|
|
923
|
-
|
|
928
|
+
# Validate version compatibility — strict: installed must equal required.
|
|
929
|
+
# Skip during migration: the config is being updated, versions are transiently mismatched.
|
|
930
|
+
if not getattr(self, '_migration_running', False):
|
|
931
|
+
if self.compare_versions(installed_version, required_version) != 0:
|
|
932
|
+
raise OutdatedHalfORMDevError(required_version, installed_version)
|
|
924
933
|
|
|
925
934
|
except RepoError:
|
|
926
935
|
# Re-raise RepoError (dirty working directory)
|
|
@@ -928,16 +937,14 @@ class Repo:
|
|
|
928
937
|
except OutdatedHalfORMDevError:
|
|
929
938
|
# Re-raise version error (repository was updated to newer version)
|
|
930
939
|
raise
|
|
931
|
-
except
|
|
932
|
-
#
|
|
933
|
-
# and continue
|
|
940
|
+
except (GitCommandError, IndexError, KeyError, ValueError) as e:
|
|
941
|
+
# Git/network errors (offline mode, missing branch, no remote, etc.)
|
|
942
|
+
# Restore branch if needed and continue — these are non-fatal.
|
|
934
943
|
try:
|
|
935
944
|
if current_branch and git_repo:
|
|
936
945
|
git_repo.heads[current_branch].checkout()
|
|
937
946
|
except (GitCommandError, IndexError, TypeError):
|
|
938
947
|
pass
|
|
939
|
-
# Log but don't fail (offline mode, no remote, etc.)
|
|
940
|
-
# Only critical errors (dirty repo, version mismatch) should block
|
|
941
948
|
|
|
942
949
|
def commit_and_sync_to_active_branches(
|
|
943
950
|
self,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0-a19
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0-a18
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/revert_migration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py
RENAMED
|
File without changes
|
{half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/git-hooks/prepare-commit-msg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|