half-orm-dev 1.0.0a20__tar.gz → 1.0.0a22__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.0a20/half_orm_dev.egg-info → half_orm_dev-1.0.0a22}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/main.py +35 -3
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/hgit.py +7 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migration_manager.py +67 -47
- half_orm_dev-1.0.0a22/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +36 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/repo.py +67 -38
- half_orm_dev-1.0.0a22/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22/half_orm_dev.egg-info}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/SOURCES.txt +1 -0
- half_orm_dev-1.0.0a20/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/LICENSE +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/README.md +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/bootstrap_manager.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/revert_migration.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patch_manager.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/release_manager.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/pre-push +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/entry_points.txt +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/setup.py +0 -0
|
@@ -133,6 +133,38 @@ def create_cli_group():
|
|
|
133
133
|
cmd.callback = check_version_before_invoke(cmd.callback)
|
|
134
134
|
super().add_command(cmd, name)
|
|
135
135
|
|
|
136
|
+
def get_command(self, ctx, cmd_name):
|
|
137
|
+
cmd = super().get_command(ctx, cmd_name)
|
|
138
|
+
if cmd is not None:
|
|
139
|
+
return cmd
|
|
140
|
+
# Unknown command — show a specific message when migration is needed.
|
|
141
|
+
if hop.repo_checked and hop._Hop__repo.needs_migration():
|
|
142
|
+
from half_orm_dev.utils import hop_version
|
|
143
|
+
installed_version = hop_version()
|
|
144
|
+
config_version = hop._Hop__repo._Repo__config.hop_version
|
|
145
|
+
|
|
146
|
+
@click.command(
|
|
147
|
+
cmd_name,
|
|
148
|
+
context_settings={
|
|
149
|
+
'allow_extra_args': True,
|
|
150
|
+
'ignore_unknown_options': True,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
@click.pass_context
|
|
154
|
+
def _migration_required(ctx):
|
|
155
|
+
click.echo(
|
|
156
|
+
f"\n⚠️ Command '{cmd_name}' is not available: migration required.\n\n"
|
|
157
|
+
f" Repository version: {config_version}\n"
|
|
158
|
+
f" Installed version: {installed_version}\n\n"
|
|
159
|
+
f" Apply migration: half_orm dev migrate\n"
|
|
160
|
+
f" Revert version: pip install half-orm-dev=={config_version}\n",
|
|
161
|
+
err=True,
|
|
162
|
+
)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
return _migration_required
|
|
166
|
+
return None
|
|
167
|
+
|
|
136
168
|
@click.group(cls=VersionCheckGroup, invoke_without_command=True)
|
|
137
169
|
@click.pass_context
|
|
138
170
|
def dev(ctx):
|
|
@@ -141,8 +173,8 @@ def create_cli_group():
|
|
|
141
173
|
error = hop.hop_upgrade_error
|
|
142
174
|
required = error.required_version
|
|
143
175
|
installed = error.installed_version
|
|
144
|
-
click.echo(f"\n
|
|
145
|
-
f"(installed
|
|
176
|
+
click.echo(f"\n This repository requires half-orm-dev {required} "
|
|
177
|
+
f"(installed: {installed}).")
|
|
146
178
|
click.echo(f" Installing the required version...")
|
|
147
179
|
try:
|
|
148
180
|
subprocess.run(
|
|
@@ -151,7 +183,7 @@ def create_cli_group():
|
|
|
151
183
|
)
|
|
152
184
|
except subprocess.CalledProcessError:
|
|
153
185
|
click.echo(f"\n Installation failed.", err=True)
|
|
154
|
-
click.echo(f" Run manually
|
|
186
|
+
click.echo(f" Run manually: pip install half-orm-dev=={required}", err=True)
|
|
155
187
|
sys.exit(1)
|
|
156
188
|
click.echo(f" ✓ half-orm-dev {required} installed. Restarting...\n")
|
|
157
189
|
os.execv(sys.argv[0], sys.argv)
|
|
@@ -1471,8 +1471,15 @@ class HGit:
|
|
|
1471
1471
|
is_local = branch in local_release_branches
|
|
1472
1472
|
release_branch_infos.append(get_branch_info(branch, check_stage=True, is_local=is_local))
|
|
1473
1473
|
|
|
1474
|
+
# ho-prod is always active — include it without special treatment.
|
|
1475
|
+
prod_branch_info = None
|
|
1476
|
+
local_prod = self.branch_exists('ho-prod')
|
|
1477
|
+
if local_prod or 'ho-prod' in remote_branch_names:
|
|
1478
|
+
prod_branch_info = get_branch_info('ho-prod', is_local=local_prod)
|
|
1479
|
+
|
|
1474
1480
|
return {
|
|
1475
1481
|
'current_branch': current_branch,
|
|
1482
|
+
'prod_branch': prod_branch_info,
|
|
1476
1483
|
'patch_branches': patch_branch_infos,
|
|
1477
1484
|
'staged_branches': staged_branch_infos,
|
|
1478
1485
|
'release_branches': release_branch_infos
|
|
@@ -10,10 +10,11 @@ Directory structure:
|
|
|
10
10
|
├── log # List of applied migrations (version format)
|
|
11
11
|
└── major/ # Major version
|
|
12
12
|
└── minor/ # Minor version
|
|
13
|
-
└── patch/ # Patch version
|
|
13
|
+
└── patch/ # Patch version (stable migrations here)
|
|
14
14
|
├── 00_migration_name.py
|
|
15
15
|
├── 01_another_migration.py
|
|
16
|
-
└──
|
|
16
|
+
└── a20/ # Pre-release migrations (4th level, PEP 440)
|
|
17
|
+
└── 01_migration_name.py
|
|
17
18
|
|
|
18
19
|
Each migration file must define:
|
|
19
20
|
- migrate(repo): Execute the migration
|
|
@@ -66,75 +67,92 @@ class MigrationManager:
|
|
|
66
67
|
# Path to migrations directory (in half_orm_dev package)
|
|
67
68
|
self._migrations_root = Path(__file__).parent / 'migrations'
|
|
68
69
|
|
|
69
|
-
def _version_to_path(self,
|
|
70
|
+
def _version_to_path(self, version_str: str) -> Path:
|
|
70
71
|
"""
|
|
71
|
-
Convert version
|
|
72
|
+
Convert a version string to its migration directory path.
|
|
73
|
+
|
|
74
|
+
For stable versions (e.g., "0.17.5") returns major/minor/patch/.
|
|
75
|
+
For pre-release versions (e.g., "1.0.0a20") returns major/minor/patch/pre/
|
|
76
|
+
where pre is the pre-release segment (e.g., "a20").
|
|
72
77
|
|
|
73
78
|
Args:
|
|
74
|
-
|
|
79
|
+
version_str: PEP 440 version string
|
|
75
80
|
|
|
76
81
|
Returns:
|
|
77
82
|
Path to migration directory
|
|
78
83
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
v = version.parse(version_str)
|
|
85
|
+
major, minor, patch = v.release[:3]
|
|
86
|
+
base = self._migrations_root / str(major) / str(minor) / str(patch)
|
|
87
|
+
if v.pre:
|
|
88
|
+
pre_str = ''.join(str(p) for p in v.pre)
|
|
89
|
+
return base / pre_str
|
|
90
|
+
return base
|
|
81
91
|
|
|
82
92
|
def get_pending_migrations(self, current_version: str, target_version: str) -> List[Tuple[str, Path]]:
|
|
83
93
|
"""
|
|
84
94
|
Get list of migrations that need to be applied.
|
|
85
95
|
|
|
86
96
|
Compares current version (from .hop/config) with target version (from hop_version())
|
|
87
|
-
and returns all migrations in between.
|
|
97
|
+
and returns all migrations in between, sorted by PEP 440 version order.
|
|
98
|
+
|
|
99
|
+
Directory structure:
|
|
100
|
+
migrations/major/minor/patch/ ← stable version scripts
|
|
101
|
+
migrations/major/minor/patch/a20/ ← pre-release scripts (4th level)
|
|
88
102
|
|
|
89
103
|
Args:
|
|
90
|
-
current_version: Current version from .hop/config (e.g., "0.17.0")
|
|
91
|
-
target_version: Target version from hop_version() (e.g., "0.17.1")
|
|
104
|
+
current_version: Current version from .hop/config (e.g., "0.17.0" or "1.0.0-a19")
|
|
105
|
+
target_version: Target version from hop_version() (e.g., "0.17.1" or "1.0.0-a20")
|
|
92
106
|
|
|
93
107
|
Returns:
|
|
94
|
-
List of (version_str, migration_dir_path) tuples in order
|
|
108
|
+
List of (version_str, migration_dir_path) tuples in PEP 440 order
|
|
95
109
|
"""
|
|
96
|
-
current = version.parse(current_version)
|
|
97
|
-
target = version.parse(target_version)
|
|
110
|
+
current = version.parse(current_version)
|
|
111
|
+
target = version.parse(target_version)
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
candidates = []
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for major in range(0, target[0] + 1):
|
|
104
|
-
major_dir = self._migrations_root / str(major)
|
|
105
|
-
if not major_dir.exists():
|
|
115
|
+
for major_dir in sorted(self._migrations_root.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
|
|
116
|
+
if not major_dir.is_dir() or not major_dir.name.isdigit():
|
|
106
117
|
continue
|
|
118
|
+
major = int(major_dir.name)
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
minor_dir = major_dir / str(minor)
|
|
111
|
-
if not minor_dir.exists():
|
|
120
|
+
for minor_dir in sorted(major_dir.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
|
|
121
|
+
if not minor_dir.is_dir() or not minor_dir.name.isdigit():
|
|
112
122
|
continue
|
|
123
|
+
minor = int(minor_dir.name)
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
patch_dir = minor_dir / str(patch)
|
|
117
|
-
if not patch_dir.exists():
|
|
118
|
-
continue
|
|
119
|
-
|
|
120
|
-
version_tuple = (major, minor, patch)
|
|
121
|
-
|
|
122
|
-
# Skip if this version is <= current version
|
|
123
|
-
if version_tuple <= current:
|
|
125
|
+
for patch_dir in sorted(minor_dir.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
|
|
126
|
+
if not patch_dir.is_dir() or not patch_dir.name.isdigit():
|
|
124
127
|
continue
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
patch = int(patch_dir.name)
|
|
129
|
+
base_version_str = f"{major}.{minor}.{patch}"
|
|
130
|
+
|
|
131
|
+
# 4th level: pre-release subdirectories (e.g., a20/, b1/, rc2/)
|
|
132
|
+
for pre_dir in sorted(patch_dir.iterdir()):
|
|
133
|
+
if not pre_dir.is_dir() or not re.match(r'^[a-z]+\d+$', pre_dir.name):
|
|
134
|
+
continue
|
|
135
|
+
pre_version_str = f"{base_version_str}{pre_dir.name}"
|
|
136
|
+
try:
|
|
137
|
+
v = version.parse(pre_version_str)
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
if current < v <= target and list(pre_dir.glob('*.py')):
|
|
141
|
+
candidates.append((pre_version_str, pre_dir))
|
|
142
|
+
|
|
143
|
+
# Stable version scripts at patch level
|
|
144
|
+
stable_files = list(patch_dir.glob('*.py'))
|
|
145
|
+
if stable_files:
|
|
146
|
+
try:
|
|
147
|
+
v = version.parse(base_version_str)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
if current < v <= target:
|
|
151
|
+
candidates.append((base_version_str, patch_dir))
|
|
152
|
+
|
|
153
|
+
# Sort by PEP 440 version (pre-releases sort before their stable counterpart)
|
|
154
|
+
candidates.sort(key=lambda x: version.parse(x[0]))
|
|
155
|
+
return candidates
|
|
138
156
|
|
|
139
157
|
def _load_migration_module(self, migration_file: Path):
|
|
140
158
|
"""
|
|
@@ -506,10 +524,12 @@ class MigrationManager:
|
|
|
506
524
|
except Exception:
|
|
507
525
|
return # can't determine status, proceed cautiously
|
|
508
526
|
|
|
527
|
+
prod_info = branches_status.get('prod_branch')
|
|
528
|
+
prod_branches = [prod_info['name']] if prod_info else []
|
|
509
529
|
patch_branches = [b['name'] for b in branches_status.get('patch_branches', [])]
|
|
510
530
|
release_branches = [b['name'] for b in branches_status.get('release_branches', [])]
|
|
511
531
|
# ho-staged/* branches are frozen after merge — excluded from sync checks
|
|
512
|
-
active_branches = release_branches + patch_branches
|
|
532
|
+
active_branches = prod_branches + release_branches + patch_branches
|
|
513
533
|
|
|
514
534
|
blocked = []
|
|
515
535
|
for branch in active_branches:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration 1.0.0a20 — Add production-specific entries to .gitignore
|
|
3
|
+
|
|
4
|
+
Adds .hop/production and .hop/.fetching to .gitignore so that production
|
|
5
|
+
server marker files are not tracked by git.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
GITIGNORE_ENTRIES = ['.hop/production', '.hop/.fetching']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_description():
|
|
12
|
+
return "Add .hop/production and .hop/.fetching to .gitignore"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def migrate(repo):
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
base_dir = repo._Repo__base_dir
|
|
19
|
+
gitignore_path = Path(base_dir) / '.gitignore'
|
|
20
|
+
|
|
21
|
+
if not gitignore_path.exists():
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
content = gitignore_path.read_text()
|
|
25
|
+
lines = content.splitlines()
|
|
26
|
+
missing = [e for e in GITIGNORE_ENTRIES if e not in lines]
|
|
27
|
+
|
|
28
|
+
if not missing:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
with gitignore_path.open('a') as f:
|
|
32
|
+
f.write('\n' + '\n'.join(missing) + '\n')
|
|
33
|
+
|
|
34
|
+
repo.stage_maintenance_file('.gitignore')
|
|
35
|
+
|
|
36
|
+
return {'sync_files': ['.gitignore']}
|
|
@@ -491,7 +491,6 @@ class Repo:
|
|
|
491
491
|
" Run 'hop migrate' on a development machine first, then deploy."
|
|
492
492
|
)
|
|
493
493
|
|
|
494
|
-
self._migration_running = True
|
|
495
494
|
try:
|
|
496
495
|
# Create migration manager
|
|
497
496
|
migration_mgr = MigrationManager(self)
|
|
@@ -506,25 +505,38 @@ class Repo:
|
|
|
506
505
|
|
|
507
506
|
result['migration_needed'] = True
|
|
508
507
|
|
|
509
|
-
# Migration must be run on ho-prod branch
|
|
510
|
-
# If not on ho-prod, switch automatically if working directory is clean
|
|
511
508
|
current_branch = self.hgit.branch if self.hgit else 'unknown'
|
|
512
509
|
switched_branch = False
|
|
513
510
|
|
|
511
|
+
# Dirty check done ONCE here, before any operation.
|
|
512
|
+
# After this point _migration_running disables further dirty checks:
|
|
513
|
+
# all modifications are the migration's responsibility and will be committed.
|
|
514
|
+
if self.hgit and self.hgit.git_repo.is_dirty(untracked_files=False):
|
|
515
|
+
config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
|
|
516
|
+
status = self.hgit.git_repo.git.status('--short')
|
|
517
|
+
raise RepoError(
|
|
518
|
+
f"Repository migration required but working directory has uncommitted changes.\n\n"
|
|
519
|
+
f" Repository version: {config_version}\n"
|
|
520
|
+
f" Installed version: {current_version}\n"
|
|
521
|
+
f" Current branch: {current_branch}\n\n"
|
|
522
|
+
f" Please commit or stash your changes:\n"
|
|
523
|
+
f" git stash\n"
|
|
524
|
+
f" OR\n"
|
|
525
|
+
f" git add . && git commit -m \"your message\"\n"
|
|
526
|
+
f"Dirty files:\n{status}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Verify all active branches (including ho-prod) are in sync before
|
|
530
|
+
# switching branches or touching anything.
|
|
531
|
+
try:
|
|
532
|
+
migration_mgr._ensure_active_branches_synced()
|
|
533
|
+
except Exception as e:
|
|
534
|
+
raise RepoError(str(e)) from e
|
|
535
|
+
|
|
536
|
+
# From here, all modifications are made by the migration itself.
|
|
537
|
+
self._migration_running = True
|
|
538
|
+
|
|
514
539
|
if not self.hgit or self.hgit.branch != 'ho-prod':
|
|
515
|
-
# Check if working directory is clean
|
|
516
|
-
if self.hgit and self.hgit.git_repo.is_dirty(untracked_files=False):
|
|
517
|
-
config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
|
|
518
|
-
raise RepoError(
|
|
519
|
-
f"Repository migration required but working directory has uncommitted changes.\n\n"
|
|
520
|
-
f" Repository version: {config_version}\n"
|
|
521
|
-
f" Installed version: {current_version}\n"
|
|
522
|
-
f" Current branch: {current_branch}\n\n"
|
|
523
|
-
f" Please commit or stash your changes:\n"
|
|
524
|
-
f" git stash\n"
|
|
525
|
-
f" OR\n"
|
|
526
|
-
f" git add . && git commit -m \"your message\"\n"
|
|
527
|
-
)
|
|
528
540
|
|
|
529
541
|
# Working directory is clean, switch to ho-prod
|
|
530
542
|
try:
|
|
@@ -896,17 +908,21 @@ class Repo:
|
|
|
896
908
|
git_repo = self.hgit.git_repo
|
|
897
909
|
current_branch = git_repo.active_branch.name
|
|
898
910
|
|
|
899
|
-
# Check if working directory is clean
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
911
|
+
# Check if working directory is clean.
|
|
912
|
+
# Skipped during migration: _migration_running means the dirty check
|
|
913
|
+
# was already done once at the start of run_migrations_if_needed(),
|
|
914
|
+
# and all subsequent modifications are committed by the migration itself.
|
|
915
|
+
if not getattr(self, '_migration_running', False):
|
|
916
|
+
if git_repo.is_dirty(untracked_files=False):
|
|
917
|
+
status = git_repo.git.status('--short')
|
|
918
|
+
raise RepoError(
|
|
919
|
+
f"Working directory has uncommitted changes.\n"
|
|
920
|
+
f"Please commit or stash your changes before running this command:\n"
|
|
921
|
+
f" git stash\n"
|
|
922
|
+
f" OR\n"
|
|
923
|
+
f" git add . && git commit -m \"your message\"\n"
|
|
924
|
+
f"Dirty files:\n{status}"
|
|
925
|
+
)
|
|
910
926
|
|
|
911
927
|
# Switch to ho-prod temporarily
|
|
912
928
|
git_repo.heads['ho-prod'].checkout()
|
|
@@ -1391,6 +1407,30 @@ class Repo:
|
|
|
1391
1407
|
# step 12: Protect ho-prod from direct commits
|
|
1392
1408
|
self.install_git_hooks()
|
|
1393
1409
|
|
|
1410
|
+
def stage_maintenance_file(self, relative_path: str) -> None:
|
|
1411
|
+
"""Stage a file modified by an automated maintenance operation."""
|
|
1412
|
+
if not hasattr(self, '_maintenance_files'):
|
|
1413
|
+
self._maintenance_files = []
|
|
1414
|
+
abs_path = os.path.join(self.__base_dir, relative_path)
|
|
1415
|
+
if os.path.exists(abs_path):
|
|
1416
|
+
self.hgit.git_repo.index.add([relative_path])
|
|
1417
|
+
if relative_path not in self._maintenance_files:
|
|
1418
|
+
self._maintenance_files.append(relative_path)
|
|
1419
|
+
|
|
1420
|
+
def commit_maintenance_files(self, message: str = 'update maintenance files') -> bool:
|
|
1421
|
+
"""Commit all staged maintenance files in a single [HOP] commit (skip hooks)."""
|
|
1422
|
+
if not getattr(self, '_maintenance_files', None):
|
|
1423
|
+
return False
|
|
1424
|
+
try:
|
|
1425
|
+
self.hgit.git_repo.index.commit(
|
|
1426
|
+
f'[HOP] {message}',
|
|
1427
|
+
skip_hooks=True,
|
|
1428
|
+
)
|
|
1429
|
+
self._maintenance_files = []
|
|
1430
|
+
return True
|
|
1431
|
+
except Exception:
|
|
1432
|
+
return False
|
|
1433
|
+
|
|
1394
1434
|
def install_git_hooks(self, force: bool = False) -> dict:
|
|
1395
1435
|
"""
|
|
1396
1436
|
Install or update Git hooks from templates.
|
|
@@ -1457,17 +1497,6 @@ class Repo:
|
|
|
1457
1497
|
if action == 'installed' or overall_action == 'skipped':
|
|
1458
1498
|
overall_action = action
|
|
1459
1499
|
|
|
1460
|
-
# Ensure production-specific entries are in .gitignore (idempotent).
|
|
1461
|
-
gitignore_path = Path(self.__base_dir) / '.gitignore'
|
|
1462
|
-
if gitignore_path.exists():
|
|
1463
|
-
content = gitignore_path.read_text()
|
|
1464
|
-
lines = content.splitlines()
|
|
1465
|
-
missing = [e for e in ('.hop/production', '.hop/.fetching')
|
|
1466
|
-
if e not in lines]
|
|
1467
|
-
if missing:
|
|
1468
|
-
with gitignore_path.open('a') as f:
|
|
1469
|
-
f.write('\n' + '\n'.join(missing) + '\n')
|
|
1470
|
-
|
|
1471
1500
|
return {
|
|
1472
1501
|
'installed': any_installed,
|
|
1473
1502
|
'action': overall_action
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0-a22
|
|
@@ -52,6 +52,7 @@ half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py
|
|
|
52
52
|
half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py
|
|
53
53
|
half_orm_dev/migrations/0/18/0/00_add_async_support.py
|
|
54
54
|
half_orm_dev/migrations/0/18/0/01_update_default_tests.py
|
|
55
|
+
half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py
|
|
55
56
|
half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md
|
|
56
57
|
half_orm_dev/patches/log
|
|
57
58
|
half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0-a20
|
|
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.0a20 → half_orm_dev-1.0.0a22}/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
|
{half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py
RENAMED
|
File without changes
|
{half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/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
|