half-orm-dev 0.17.3a8__tar.gz → 0.17.3a9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {half_orm_dev-0.17.3a8/half_orm_dev.egg-info → half_orm_dev-0.17.3a9}/PKG-INFO +1 -1
- half_orm_dev-0.17.3a9/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +204 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patch_manager.py +264 -61
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/release_file.py +32 -18
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/release_manager.py +168 -36
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/repo.py +124 -0
- half_orm_dev-0.17.3a9/half_orm_dev/version.txt +1 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9/half_orm_dev.egg-info}/PKG-INFO +1 -1
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev.egg-info/SOURCES.txt +1 -0
- half_orm_dev-0.17.3a8/half_orm_dev/version.txt +0 -1
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/AUTHORS +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/LICENSE +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/README.md +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli/main.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/database.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/hgit.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/migration_manager.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/Pipfile +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/pyproject.toml +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/setup.cfg +0 -0
- {half_orm_dev-0.17.3a8 → half_orm_dev-0.17.3a9}/setup.py +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration: Convert TOML patches to dict format with merge_commit.
|
|
3
|
+
|
|
4
|
+
This migration converts the old TOML format:
|
|
5
|
+
[patches]
|
|
6
|
+
"1-auth" = "staged"
|
|
7
|
+
"2-api" = "candidate"
|
|
8
|
+
|
|
9
|
+
To the new dict format:
|
|
10
|
+
[patches]
|
|
11
|
+
"1-auth" = { status = "staged", merge_commit = "abc123de" }
|
|
12
|
+
"2-api" = { status = "candidate" }
|
|
13
|
+
|
|
14
|
+
For staged patches, the merge_commit hash is retrieved from git history.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import tomli
|
|
23
|
+
except ImportError:
|
|
24
|
+
import tomllib as tomli
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import tomli_w
|
|
28
|
+
except ImportError:
|
|
29
|
+
raise ImportError(
|
|
30
|
+
"tomli_w is required for this migration. "
|
|
31
|
+
"Install it with: pip install tomli_w"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_description():
|
|
36
|
+
"""Return migration description."""
|
|
37
|
+
return "Convert TOML patches to dict format with merge_commit"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_merge_commit(repo, patch_id: str, version: str) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Find the merge commit hash for a staged patch.
|
|
43
|
+
|
|
44
|
+
Searches git history for the commit that merged the patch branch
|
|
45
|
+
into the release branch.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
repo: Repo instance
|
|
49
|
+
patch_id: Patch identifier (e.g., "456-user-auth")
|
|
50
|
+
version: Release version (e.g., "0.17.0")
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Commit hash (8 characters) or empty string if not found
|
|
54
|
+
"""
|
|
55
|
+
release_branch = f"ho-release/{version}"
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Search for merge commit message pattern
|
|
59
|
+
# Pattern: [HOP] Merge #PATCH_ID into %"VERSION"
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
['git', 'log', '--all', '--grep', f'Merge #{patch_id}',
|
|
62
|
+
'--format=%H', '-n', '1'],
|
|
63
|
+
cwd=repo.base_dir,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
check=True
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
commit_hash = result.stdout.strip()
|
|
70
|
+
if commit_hash:
|
|
71
|
+
return commit_hash[:8]
|
|
72
|
+
|
|
73
|
+
# Fallback: search for the move to stage commit
|
|
74
|
+
# Pattern: [HOP] move patch #PATCH_ID from candidate to stage
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
['git', 'log', '--all', '--grep', f'move patch #{patch_id}',
|
|
77
|
+
'--format=%H', '-n', '1'],
|
|
78
|
+
cwd=repo.base_dir,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
check=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
commit_hash = result.stdout.strip()
|
|
85
|
+
if commit_hash:
|
|
86
|
+
# Get the parent commit (the merge commit is the one before the move)
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
['git', 'rev-parse', f'{commit_hash}^'],
|
|
89
|
+
cwd=repo.base_dir,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
check=True
|
|
93
|
+
)
|
|
94
|
+
parent_hash = result.stdout.strip()
|
|
95
|
+
if parent_hash:
|
|
96
|
+
return parent_hash[:8]
|
|
97
|
+
|
|
98
|
+
except subprocess.CalledProcessError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def migrate(repo):
|
|
105
|
+
"""
|
|
106
|
+
Execute migration: Convert TOML patches to dict format.
|
|
107
|
+
|
|
108
|
+
For each X.Y.Z-patches.toml file:
|
|
109
|
+
1. Read current content
|
|
110
|
+
2. Check if already in dict format
|
|
111
|
+
3. Convert to dict format:
|
|
112
|
+
- candidates: { status = "candidate" }
|
|
113
|
+
- staged: { status = "staged", merge_commit = "..." }
|
|
114
|
+
4. Find merge_commit from git history for staged patches
|
|
115
|
+
5. Write updated TOML file
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
repo: Repo instance
|
|
119
|
+
"""
|
|
120
|
+
print("Migrating TOML patches to dict format with merge_commit...")
|
|
121
|
+
|
|
122
|
+
releases_dir = Path(repo.releases_dir)
|
|
123
|
+
if not releases_dir.exists():
|
|
124
|
+
print(" No releases directory found, skipping migration.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Find all TOML patches files
|
|
128
|
+
toml_files = list(releases_dir.glob("*-patches.toml"))
|
|
129
|
+
|
|
130
|
+
if not toml_files:
|
|
131
|
+
print(" No TOML patches files found, skipping migration.")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
migrated_count = 0
|
|
135
|
+
|
|
136
|
+
for toml_file in toml_files:
|
|
137
|
+
# Extract version from filename
|
|
138
|
+
version = toml_file.stem.replace('-patches', '')
|
|
139
|
+
|
|
140
|
+
print(f" Processing {version}...")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Read current TOML content
|
|
144
|
+
with toml_file.open('rb') as f:
|
|
145
|
+
data = tomli.load(f)
|
|
146
|
+
|
|
147
|
+
patches = data.get("patches", {})
|
|
148
|
+
|
|
149
|
+
if not patches:
|
|
150
|
+
print(f" No patches in {version}, skipping")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Check if already in dict format
|
|
154
|
+
first_value = next(iter(patches.values()))
|
|
155
|
+
if isinstance(first_value, dict):
|
|
156
|
+
print(f" Already in dict format, skipping")
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Convert to dict format
|
|
160
|
+
new_patches = {}
|
|
161
|
+
staged_without_commit = []
|
|
162
|
+
|
|
163
|
+
for patch_id, status in patches.items():
|
|
164
|
+
if status == "candidate":
|
|
165
|
+
new_patches[patch_id] = {"status": "candidate"}
|
|
166
|
+
elif status == "staged":
|
|
167
|
+
# Find merge commit from git history
|
|
168
|
+
merge_commit = find_merge_commit(repo, patch_id, version)
|
|
169
|
+
if merge_commit:
|
|
170
|
+
new_patches[patch_id] = {
|
|
171
|
+
"status": "staged",
|
|
172
|
+
"merge_commit": merge_commit
|
|
173
|
+
}
|
|
174
|
+
else:
|
|
175
|
+
# No merge_commit found, store without it
|
|
176
|
+
new_patches[patch_id] = {"status": "staged"}
|
|
177
|
+
staged_without_commit.append(patch_id)
|
|
178
|
+
else:
|
|
179
|
+
# Unknown status, preserve as-is in dict format
|
|
180
|
+
new_patches[patch_id] = {"status": status}
|
|
181
|
+
|
|
182
|
+
# Update data and write
|
|
183
|
+
data["patches"] = new_patches
|
|
184
|
+
|
|
185
|
+
with toml_file.open('wb') as f:
|
|
186
|
+
tomli_w.dump(data, f)
|
|
187
|
+
|
|
188
|
+
print(f" Converted {len(patches)} patch(es)")
|
|
189
|
+
if staged_without_commit:
|
|
190
|
+
print(f" Warning: No merge_commit found for: {', '.join(staged_without_commit)}",
|
|
191
|
+
file=sys.stderr)
|
|
192
|
+
|
|
193
|
+
migrated_count += 1
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f" Error processing {version}: {e}", file=sys.stderr)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
repo.hgit.add('.hop')
|
|
200
|
+
|
|
201
|
+
if migrated_count > 0:
|
|
202
|
+
print(f"\nMigration complete: {migrated_count} file(s) converted to dict format")
|
|
203
|
+
else:
|
|
204
|
+
print("\nNo files needed migration")
|
|
@@ -488,61 +488,84 @@ class PatchManager:
|
|
|
488
488
|
Apply patch with full release context.
|
|
489
489
|
|
|
490
490
|
Workflow:
|
|
491
|
-
1. Restore DB from
|
|
492
|
-
2. Apply
|
|
493
|
-
3.
|
|
494
|
-
4. If current patch is NOT in release, apply it at the end
|
|
495
|
-
5. Generate Python code
|
|
491
|
+
1. Restore DB from release schema (includes all staged patches)
|
|
492
|
+
2. Apply only the current patch
|
|
493
|
+
3. Generate Python code
|
|
496
494
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
495
|
+
If release schema doesn't exist (backward compatibility), falls back to:
|
|
496
|
+
1. Restore DB from production baseline
|
|
497
|
+
2. Apply all staged patches in order
|
|
498
|
+
3. Apply current patch
|
|
499
|
+
4. Generate Python code
|
|
500
500
|
|
|
501
|
-
|
|
501
|
+
Examples:
|
|
502
|
+
# With release schema (new workflow):
|
|
503
|
+
apply_patch_complete_workflow("999")
|
|
502
504
|
# Execution:
|
|
503
|
-
# 1. Restore DB
|
|
504
|
-
# 2. Apply
|
|
505
|
-
# 3.
|
|
506
|
-
|
|
507
|
-
#
|
|
508
|
-
# 6. Generate code
|
|
509
|
-
|
|
510
|
-
# Current patch: 999 (NOT in release)
|
|
505
|
+
# 1. Restore DB from release-0.17.1.sql (includes staged patches)
|
|
506
|
+
# 2. Apply 999
|
|
507
|
+
# 3. Generate code
|
|
508
|
+
|
|
509
|
+
# Without release schema (backward compat):
|
|
511
510
|
apply_patch_complete_workflow("999")
|
|
512
511
|
# Execution:
|
|
513
|
-
# 1. Restore DB (
|
|
514
|
-
# 2. Apply
|
|
515
|
-
# 3. Apply
|
|
516
|
-
# 4.
|
|
517
|
-
# 5. Apply 234
|
|
518
|
-
# 6. Apply 999 ← At the end
|
|
519
|
-
# 7. Generate code
|
|
512
|
+
# 1. Restore DB from schema.sql (prod)
|
|
513
|
+
# 2. Apply all staged patches
|
|
514
|
+
# 3. Apply 999
|
|
515
|
+
# 4. Generate code
|
|
520
516
|
"""
|
|
521
517
|
|
|
522
518
|
try:
|
|
523
|
-
#
|
|
524
|
-
self.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
519
|
+
# Get release version for this patch
|
|
520
|
+
version = self._find_version_for_candidate(patch_id)
|
|
521
|
+
if not version:
|
|
522
|
+
# Try to find from staged patches
|
|
523
|
+
version = self._repo.release_manager.get_next_release_version()
|
|
528
524
|
|
|
529
525
|
applied_release_files = []
|
|
530
526
|
applied_current_files = []
|
|
531
527
|
patch_was_in_release = False
|
|
532
528
|
|
|
533
|
-
#
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
529
|
+
# Check if release schema exists
|
|
530
|
+
release_schema_path = None
|
|
531
|
+
if version:
|
|
532
|
+
release_schema_path = self._repo.get_release_schema_path(version)
|
|
533
|
+
|
|
534
|
+
if release_schema_path and release_schema_path.exists():
|
|
535
|
+
# New workflow: restore from release schema (includes all staged patches)
|
|
536
|
+
self._repo.restore_database_from_release_schema(version)
|
|
539
537
|
|
|
540
|
-
|
|
541
|
-
if not patch_was_in_release:
|
|
538
|
+
# Apply only the current patch
|
|
542
539
|
files = self.apply_patch_files(patch_id, self._repo.model)
|
|
543
540
|
applied_current_files = files
|
|
541
|
+
else:
|
|
542
|
+
# Backward compatibility: old workflow
|
|
543
|
+
# Also generates release schema for migration of existing projects
|
|
544
|
+
self._repo.restore_database_from_schema()
|
|
544
545
|
|
|
545
|
-
|
|
546
|
+
# Get and apply all staged release patches
|
|
547
|
+
release_patches = self._repo.release_manager.get_all_release_context_patches()
|
|
548
|
+
|
|
549
|
+
for patch in release_patches:
|
|
550
|
+
if patch == patch_id:
|
|
551
|
+
patch_was_in_release = True
|
|
552
|
+
files = self.apply_patch_files(patch, self._repo.model)
|
|
553
|
+
applied_release_files.extend(files)
|
|
554
|
+
|
|
555
|
+
# Generate release schema for existing projects migration
|
|
556
|
+
# This captures the state after all staged patches are applied
|
|
557
|
+
if version:
|
|
558
|
+
try:
|
|
559
|
+
self._repo.generate_release_schema(version)
|
|
560
|
+
except Exception:
|
|
561
|
+
pass # Non-critical, continue with apply
|
|
562
|
+
|
|
563
|
+
# If current patch not in release (candidate), apply it now
|
|
564
|
+
if not patch_was_in_release:
|
|
565
|
+
files = self.apply_patch_files(patch_id, self._repo.model)
|
|
566
|
+
applied_current_files = files
|
|
567
|
+
|
|
568
|
+
# Generate Python code
|
|
546
569
|
# Track generated files
|
|
547
570
|
package_dir = Path(self._base_dir) / self._repo_name
|
|
548
571
|
files_before = set()
|
|
@@ -557,14 +580,14 @@ class PatchManager:
|
|
|
557
580
|
|
|
558
581
|
generated_files = [str(f.relative_to(self._base_dir)) for f in files_after]
|
|
559
582
|
|
|
560
|
-
#
|
|
583
|
+
# Return success
|
|
561
584
|
return {
|
|
562
585
|
'patch_id': patch_id,
|
|
563
|
-
'release_patches': [p for p in release_patches if p != patch_id],
|
|
564
586
|
'applied_release_files': applied_release_files,
|
|
565
587
|
'applied_current_files': applied_current_files,
|
|
566
588
|
'patch_was_in_release': patch_was_in_release,
|
|
567
589
|
'generated_files': generated_files,
|
|
590
|
+
'used_release_schema': release_schema_path is not None and release_schema_path.exists(),
|
|
568
591
|
'status': 'success',
|
|
569
592
|
'error': None
|
|
570
593
|
}
|
|
@@ -619,9 +642,11 @@ class PatchManager:
|
|
|
619
642
|
# Apply files in lexicographic order
|
|
620
643
|
for patch_file in structure.files:
|
|
621
644
|
if patch_file.is_sql:
|
|
645
|
+
print('XXX', patch_file.name)
|
|
622
646
|
self._execute_sql_file(patch_file.path, database_model)
|
|
623
647
|
applied_files.append(patch_file.name)
|
|
624
648
|
elif patch_file.is_python:
|
|
649
|
+
print('XXX', patch_file.name)
|
|
625
650
|
self._execute_python_file(patch_file.path)
|
|
626
651
|
applied_files.append(patch_file.name)
|
|
627
652
|
# Other file types are ignored (not executed)
|
|
@@ -1909,8 +1934,17 @@ class PatchManager:
|
|
|
1909
1934
|
f"You may need to resolve conflicts manually."
|
|
1910
1935
|
)
|
|
1911
1936
|
|
|
1912
|
-
#
|
|
1913
|
-
self.
|
|
1937
|
+
# 5b. Get merge commit hash
|
|
1938
|
+
merge_commit = self._repo.hgit.last_commit()
|
|
1939
|
+
|
|
1940
|
+
# 6. Move from candidates to stage (with merge commit hash)
|
|
1941
|
+
self._move_patch_to_stage(patch_id, version, merge_commit)
|
|
1942
|
+
|
|
1943
|
+
# 6b. Regenerate release schema (DB is already in correct state after validation)
|
|
1944
|
+
try:
|
|
1945
|
+
self._update_release_schemas(version)
|
|
1946
|
+
except Exception as e:
|
|
1947
|
+
raise PatchManagerError(f"Failed to update release schema: {e}")
|
|
1914
1948
|
|
|
1915
1949
|
# 7. Commit changes on release branch (TOML file is in .hop/releases/)
|
|
1916
1950
|
# This also syncs .hop/ to all active branches automatically via decorator
|
|
@@ -1921,6 +1955,9 @@ class PatchManager:
|
|
|
1921
1955
|
except Exception as e:
|
|
1922
1956
|
raise PatchManagerError(f"Failed to commit/push changes: {e}")
|
|
1923
1957
|
|
|
1958
|
+
# 7b. Propagate release schema to higher version releases (now that commit is done)
|
|
1959
|
+
self._propagate_release_schema_to_higher_versions(version)
|
|
1960
|
+
|
|
1924
1961
|
# 8. Delete patch branch (local and remote)
|
|
1925
1962
|
try:
|
|
1926
1963
|
self._repo.hgit.delete_local_branch(patch_branch)
|
|
@@ -1936,6 +1973,129 @@ class PatchManager:
|
|
|
1936
1973
|
'merged_into': release_branch
|
|
1937
1974
|
}
|
|
1938
1975
|
|
|
1976
|
+
def _update_release_schemas(self, version: str) -> None:
|
|
1977
|
+
"""
|
|
1978
|
+
Update release schema for current version and propagate to higher versions.
|
|
1979
|
+
|
|
1980
|
+
After a patch is merged, regenerates the release schema file for the
|
|
1981
|
+
current version and updates all higher version releases that depend on it.
|
|
1982
|
+
|
|
1983
|
+
Args:
|
|
1984
|
+
version: Current release version (e.g., "0.17.1")
|
|
1985
|
+
|
|
1986
|
+
Workflow:
|
|
1987
|
+
1. Add release schema to staging (already generated during validation)
|
|
1988
|
+
2. Find all release branches with higher versions
|
|
1989
|
+
3. For each higher version:
|
|
1990
|
+
- Checkout to that branch
|
|
1991
|
+
- Restore DB from current release schema
|
|
1992
|
+
- Apply all staged patches for that release
|
|
1993
|
+
- Regenerate its release schema
|
|
1994
|
+
- Commit the updated schema
|
|
1995
|
+
4. Return to original branch
|
|
1996
|
+
"""
|
|
1997
|
+
from packaging.version import Version
|
|
1998
|
+
from half_orm_dev.release_file import ReleaseFile
|
|
1999
|
+
|
|
2000
|
+
original_branch = self._repo.hgit.branch
|
|
2001
|
+
current_ver = Version(version)
|
|
2002
|
+
|
|
2003
|
+
# 1. Write and add release schema to staging area (if not hotfix mode)
|
|
2004
|
+
# Schema content was saved during validation with correct DB state
|
|
2005
|
+
release_schema_path = self._repo.get_release_schema_path(version)
|
|
2006
|
+
if hasattr(self, '_pending_release_schema_content') and self._pending_release_schema_content:
|
|
2007
|
+
click.echo(f" • Writing release schema ({len(self._pending_release_schema_content)} bytes)")
|
|
2008
|
+
release_schema_path.write_text(self._pending_release_schema_content, encoding='utf-8')
|
|
2009
|
+
self._pending_release_schema_content = None # Clear after use
|
|
2010
|
+
self._repo.hgit.add(str(release_schema_path))
|
|
2011
|
+
else:
|
|
2012
|
+
# Hotfix mode or no content - skip release schema
|
|
2013
|
+
click.echo(f" • Skipping release schema (hotfix mode)")
|
|
2014
|
+
|
|
2015
|
+
# NOTE: Do NOT checkout other branches here!
|
|
2016
|
+
# The commit will be done later by commit_and_sync_to_active_branches()
|
|
2017
|
+
# Propagation to higher releases is disabled for now as it causes issues
|
|
2018
|
+
# with uncommitted changes being lost during checkout.
|
|
2019
|
+
# TODO: Re-enable propagation after the main commit is done.
|
|
2020
|
+
|
|
2021
|
+
# 2. Find higher version releases (disabled - propagation moved to after commit)
|
|
2022
|
+
releases_dir = Path(self._repo.releases_dir)
|
|
2023
|
+
higher_releases = []
|
|
2024
|
+
|
|
2025
|
+
for toml_file in releases_dir.glob("*-patches.toml"):
|
|
2026
|
+
rel_version = toml_file.stem.replace('-patches', '')
|
|
2027
|
+
try:
|
|
2028
|
+
rel_ver = Version(rel_version)
|
|
2029
|
+
if rel_ver > current_ver:
|
|
2030
|
+
higher_releases.append(rel_version)
|
|
2031
|
+
except Exception:
|
|
2032
|
+
continue
|
|
2033
|
+
|
|
2034
|
+
# Sort by version (ascending)
|
|
2035
|
+
higher_releases.sort(key=lambda v: Version(v))
|
|
2036
|
+
|
|
2037
|
+
# Store higher releases for later propagation (after commit)
|
|
2038
|
+
# This avoids losing uncommitted changes when checking out other branches
|
|
2039
|
+
self._pending_higher_releases = higher_releases if higher_releases else None
|
|
2040
|
+
|
|
2041
|
+
def _propagate_release_schema_to_higher_versions(self, version: str) -> None:
|
|
2042
|
+
"""
|
|
2043
|
+
Propagate release schema changes to higher version releases.
|
|
2044
|
+
|
|
2045
|
+
Called after commit to update release schemas for all releases
|
|
2046
|
+
with version > current version.
|
|
2047
|
+
|
|
2048
|
+
Args:
|
|
2049
|
+
version: Current release version that was just updated
|
|
2050
|
+
"""
|
|
2051
|
+
from packaging.version import Version
|
|
2052
|
+
from half_orm_dev.release_file import ReleaseFile
|
|
2053
|
+
|
|
2054
|
+
if not hasattr(self, '_pending_higher_releases') or not self._pending_higher_releases:
|
|
2055
|
+
return
|
|
2056
|
+
|
|
2057
|
+
higher_releases = self._pending_higher_releases
|
|
2058
|
+
self._pending_higher_releases = None # Clear after use
|
|
2059
|
+
|
|
2060
|
+
original_branch = self._repo.hgit.branch
|
|
2061
|
+
releases_dir = Path(self._repo.releases_dir)
|
|
2062
|
+
|
|
2063
|
+
for higher_version in higher_releases:
|
|
2064
|
+
higher_branch = f"ho-release/{higher_version}"
|
|
2065
|
+
|
|
2066
|
+
if not self._repo.hgit.branch_exists(higher_branch):
|
|
2067
|
+
continue
|
|
2068
|
+
|
|
2069
|
+
click.echo(f" • Propagating to {higher_branch}...")
|
|
2070
|
+
|
|
2071
|
+
try:
|
|
2072
|
+
# Checkout to higher version branch
|
|
2073
|
+
self._repo.hgit.checkout(higher_branch)
|
|
2074
|
+
|
|
2075
|
+
# Restore DB from current release schema (which includes the new patch)
|
|
2076
|
+
self._repo.restore_database_from_release_schema(version)
|
|
2077
|
+
|
|
2078
|
+
# Apply all staged patches for this higher release
|
|
2079
|
+
release_file = ReleaseFile(higher_version, releases_dir)
|
|
2080
|
+
if release_file.exists():
|
|
2081
|
+
staged_patches = release_file.get_patches(status="staged")
|
|
2082
|
+
for pid in staged_patches:
|
|
2083
|
+
patch_dir = Path(self._base_dir) / "Patches" / pid
|
|
2084
|
+
if patch_dir.exists():
|
|
2085
|
+
self.apply_patch_files(pid, self._repo.model)
|
|
2086
|
+
|
|
2087
|
+
# Regenerate release schema for this higher version
|
|
2088
|
+
higher_schema_path = self._repo.generate_release_schema(higher_version)
|
|
2089
|
+
self._repo.hgit.add(str(higher_schema_path))
|
|
2090
|
+
self._repo.hgit.commit('-m', f"[HOP] Update release schema from %{version}")
|
|
2091
|
+
self._repo.hgit.push()
|
|
2092
|
+
|
|
2093
|
+
except Exception as e:
|
|
2094
|
+
click.echo(f" ⚠️ Warning: Failed to propagate to {higher_branch}: {e}")
|
|
2095
|
+
|
|
2096
|
+
# Return to original branch
|
|
2097
|
+
self._repo.hgit.checkout(original_branch)
|
|
2098
|
+
|
|
1939
2099
|
def _validate_patch_before_merge(
|
|
1940
2100
|
self,
|
|
1941
2101
|
patch_id: str,
|
|
@@ -1976,6 +2136,8 @@ class PatchManager:
|
|
|
1976
2136
|
# Save current branch
|
|
1977
2137
|
original_branch = self._repo.hgit.branch
|
|
1978
2138
|
temp_branch = f"ho-validate/{patch_id}"
|
|
2139
|
+
release_schema_content = None
|
|
2140
|
+
release_schema_path = None
|
|
1979
2141
|
|
|
1980
2142
|
try:
|
|
1981
2143
|
click.echo(f"\n🔍 Validating patch {utils.Color.bold(patch_id)} before merge...")
|
|
@@ -1998,24 +2160,36 @@ class PatchManager:
|
|
|
1998
2160
|
# 3. Run patch apply and verify no modifications
|
|
1999
2161
|
click.echo(f" • Running patch apply to verify idempotency...")
|
|
2000
2162
|
try:
|
|
2001
|
-
#
|
|
2002
|
-
|
|
2003
|
-
staged_patches = []
|
|
2004
|
-
if release_file.exists():
|
|
2005
|
-
staged_patches = release_file.get_patches(status="staged")
|
|
2163
|
+
# Check if release schema exists
|
|
2164
|
+
release_schema_path = self._repo.get_release_schema_path(version)
|
|
2006
2165
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2166
|
+
if release_schema_path.exists():
|
|
2167
|
+
# New workflow: restore from release schema (includes all staged patches)
|
|
2168
|
+
self._repo.restore_database_from_release_schema(version)
|
|
2009
2169
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
for pid in all_patches:
|
|
2014
|
-
patch_dir = Path(self._repo.base_dir) / "Patches" / pid
|
|
2170
|
+
# Apply only the current patch
|
|
2171
|
+
patch_dir = Path(self._repo.base_dir) / "Patches" / patch_id
|
|
2015
2172
|
if patch_dir.exists():
|
|
2016
|
-
self.apply_patch_files(
|
|
2173
|
+
self.apply_patch_files(patch_id, self._repo.model)
|
|
2174
|
+
else:
|
|
2175
|
+
# Fallback: old workflow for backward compatibility
|
|
2176
|
+
release_file = ReleaseFile(version, Path(self._repo.releases_dir))
|
|
2177
|
+
staged_patches = []
|
|
2178
|
+
if release_file.exists():
|
|
2179
|
+
staged_patches = release_file.get_patches(status="staged")
|
|
2180
|
+
|
|
2181
|
+
# Apply all staged patches + current patch
|
|
2182
|
+
all_patches = staged_patches + [patch_id]
|
|
2183
|
+
|
|
2184
|
+
# Restore database and apply patches
|
|
2185
|
+
self._repo.restore_database_from_schema()
|
|
2017
2186
|
|
|
2018
|
-
|
|
2187
|
+
for pid in all_patches:
|
|
2188
|
+
patch_dir = Path(self._repo.base_dir) / "Patches" / pid
|
|
2189
|
+
if patch_dir.exists():
|
|
2190
|
+
self.apply_patch_files(pid, self._repo.model)
|
|
2191
|
+
|
|
2192
|
+
# Generate modules
|
|
2019
2193
|
modules.generate(self._repo)
|
|
2020
2194
|
|
|
2021
2195
|
# Check if any files were modified
|
|
@@ -2043,10 +2217,33 @@ class PatchManager:
|
|
|
2043
2217
|
# 4. Run tests (best-effort)
|
|
2044
2218
|
self._run_tests_if_available()
|
|
2045
2219
|
|
|
2220
|
+
# 5. Generate release schema while DB is in correct state
|
|
2221
|
+
# This captures prod + all staged patches + current patch
|
|
2222
|
+
# Skip for hotfix releases (detected by presence of X.Y.Z.txt production file)
|
|
2223
|
+
prod_file = Path(self._repo.releases_dir) / f"{version}.txt"
|
|
2224
|
+
is_hotfix = prod_file.exists()
|
|
2225
|
+
|
|
2226
|
+
if is_hotfix:
|
|
2227
|
+
click.echo(f" • Skipping release schema (hotfix mode)")
|
|
2228
|
+
release_schema_content = None
|
|
2229
|
+
else:
|
|
2230
|
+
click.echo(f" • Generating release schema...")
|
|
2231
|
+
release_schema_path = self._repo.generate_release_schema(version)
|
|
2232
|
+
|
|
2233
|
+
# Save schema content to restore after branch checkout
|
|
2234
|
+
# (the file will be lost when switching branches)
|
|
2235
|
+
release_schema_content = release_schema_path.read_text(encoding='utf-8')
|
|
2236
|
+
|
|
2237
|
+
# Delete the file to avoid checkout conflicts
|
|
2238
|
+
# (content is saved in memory and will be written after checkout)
|
|
2239
|
+
release_schema_path.unlink()
|
|
2240
|
+
|
|
2241
|
+
click.echo(f" • {utils.Color.green('✓')} Release schema generated")
|
|
2242
|
+
|
|
2046
2243
|
click.echo(f" • {utils.Color.green('✓')} Validation passed!\n")
|
|
2047
2244
|
|
|
2048
2245
|
finally:
|
|
2049
|
-
#
|
|
2246
|
+
# 6. Cleanup: Delete temp branch and return to original branch
|
|
2050
2247
|
try:
|
|
2051
2248
|
# Return to original branch
|
|
2052
2249
|
if self._repo.hgit.branch != original_branch:
|
|
@@ -2059,6 +2256,10 @@ class PatchManager:
|
|
|
2059
2256
|
# Cleanup errors are non-critical, just warn
|
|
2060
2257
|
click.echo(f"⚠️ Warning: Failed to cleanup temp branch {temp_branch}: {e}")
|
|
2061
2258
|
|
|
2259
|
+
# Store release schema content for later use in _update_release_schemas
|
|
2260
|
+
# (after merge, when we're on the release branch)
|
|
2261
|
+
self._pending_release_schema_content = release_schema_content
|
|
2262
|
+
|
|
2062
2263
|
def _run_tests_if_available(self) -> None:
|
|
2063
2264
|
"""
|
|
2064
2265
|
Run tests if test configuration is available.
|
|
@@ -2631,7 +2832,7 @@ class PatchManager:
|
|
|
2631
2832
|
|
|
2632
2833
|
return '\n'.join(lines)
|
|
2633
2834
|
|
|
2634
|
-
def _move_patch_to_stage(self, patch_id: str, version: str) -> None:
|
|
2835
|
+
def _move_patch_to_stage(self, patch_id: str, version: str, merge_commit: str) -> None:
|
|
2635
2836
|
"""
|
|
2636
2837
|
Move patch from candidate to staged status.
|
|
2637
2838
|
|
|
@@ -2641,19 +2842,21 @@ class PatchManager:
|
|
|
2641
2842
|
Args:
|
|
2642
2843
|
patch_id: Patch identifier to move
|
|
2643
2844
|
version: Release version
|
|
2845
|
+
merge_commit: Git commit hash of the merge commit
|
|
2644
2846
|
|
|
2645
2847
|
Raises:
|
|
2646
2848
|
PatchManagerError: If operation fails
|
|
2647
2849
|
|
|
2648
2850
|
Examples:
|
|
2649
|
-
self._move_patch_to_stage("456-user-auth", "0.17.0")
|
|
2650
|
-
# Changes "456-user-auth" =
|
|
2851
|
+
self._move_patch_to_stage("456-user-auth", "0.17.0", "abc123de")
|
|
2852
|
+
# Changes "456-user-auth" = {status = "candidate"}
|
|
2853
|
+
# to "456-user-auth" = {status = "staged", merge_commit = "abc123de"}
|
|
2651
2854
|
# Order is preserved!
|
|
2652
2855
|
"""
|
|
2653
2856
|
release_file = ReleaseFile(version, self._releases_dir)
|
|
2654
2857
|
|
|
2655
2858
|
try:
|
|
2656
|
-
release_file.move_to_staged(patch_id)
|
|
2859
|
+
release_file.move_to_staged(patch_id, merge_commit)
|
|
2657
2860
|
|
|
2658
2861
|
# Stage file for commit
|
|
2659
2862
|
self._repo.hgit.add(str(release_file.file_path))
|