half-orm-dev 0.16.0a9__py3-none-any.whl

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.
Files changed (58) hide show
  1. half_orm_dev/__init__.py +1 -0
  2. half_orm_dev/cli/__init__.py +9 -0
  3. half_orm_dev/cli/commands/__init__.py +56 -0
  4. half_orm_dev/cli/commands/apply.py +13 -0
  5. half_orm_dev/cli/commands/clone.py +102 -0
  6. half_orm_dev/cli/commands/init.py +331 -0
  7. half_orm_dev/cli/commands/new.py +15 -0
  8. half_orm_dev/cli/commands/patch.py +317 -0
  9. half_orm_dev/cli/commands/prepare.py +21 -0
  10. half_orm_dev/cli/commands/prepare_release.py +119 -0
  11. half_orm_dev/cli/commands/promote_to.py +127 -0
  12. half_orm_dev/cli/commands/release.py +344 -0
  13. half_orm_dev/cli/commands/restore.py +14 -0
  14. half_orm_dev/cli/commands/sync.py +13 -0
  15. half_orm_dev/cli/commands/todo.py +73 -0
  16. half_orm_dev/cli/commands/undo.py +17 -0
  17. half_orm_dev/cli/commands/update.py +73 -0
  18. half_orm_dev/cli/commands/upgrade.py +191 -0
  19. half_orm_dev/cli/main.py +103 -0
  20. half_orm_dev/cli_extension.py +38 -0
  21. half_orm_dev/database.py +1389 -0
  22. half_orm_dev/hgit.py +1025 -0
  23. half_orm_dev/hop.py +167 -0
  24. half_orm_dev/manifest.py +43 -0
  25. half_orm_dev/modules.py +456 -0
  26. half_orm_dev/patch.py +281 -0
  27. half_orm_dev/patch_manager.py +1694 -0
  28. half_orm_dev/patch_validator.py +335 -0
  29. half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +34 -0
  30. half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +2 -0
  31. half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +3 -0
  32. half_orm_dev/patches/log +2 -0
  33. half_orm_dev/patches/sql/half_orm_meta.sql +208 -0
  34. half_orm_dev/release_manager.py +2841 -0
  35. half_orm_dev/repo.py +1562 -0
  36. half_orm_dev/templates/.gitignore +15 -0
  37. half_orm_dev/templates/MANIFEST.in +1 -0
  38. half_orm_dev/templates/Pipfile +13 -0
  39. half_orm_dev/templates/README +25 -0
  40. half_orm_dev/templates/conftest_template +42 -0
  41. half_orm_dev/templates/init_module_template +10 -0
  42. half_orm_dev/templates/module_template_1 +12 -0
  43. half_orm_dev/templates/module_template_2 +6 -0
  44. half_orm_dev/templates/module_template_3 +3 -0
  45. half_orm_dev/templates/relation_test +23 -0
  46. half_orm_dev/templates/setup.py +81 -0
  47. half_orm_dev/templates/sql_adapter +9 -0
  48. half_orm_dev/templates/warning +12 -0
  49. half_orm_dev/utils.py +49 -0
  50. half_orm_dev/version.txt +1 -0
  51. half_orm_dev-0.16.0a9.dist-info/METADATA +935 -0
  52. half_orm_dev-0.16.0a9.dist-info/RECORD +58 -0
  53. half_orm_dev-0.16.0a9.dist-info/WHEEL +5 -0
  54. half_orm_dev-0.16.0a9.dist-info/licenses/AUTHORS +3 -0
  55. half_orm_dev-0.16.0a9.dist-info/licenses/LICENSE +14 -0
  56. half_orm_dev-0.16.0a9.dist-info/top_level.txt +2 -0
  57. tests/__init__.py +0 -0
  58. tests/conftest.py +329 -0
@@ -0,0 +1,317 @@
1
+ """
2
+ Patch command group - Unified patch development and management.
3
+
4
+ Groups all patch-related commands under 'half_orm dev patch':
5
+ - patch new: Create new patch branch and directory
6
+ - patch apply: Apply current patch files to database
7
+ - patch add: Add patch to stage release with validation
8
+
9
+ Replaces legacy commands:
10
+ - create-patch → patch new
11
+ - apply-patch → patch apply
12
+ - add-to-release → patch add
13
+ """
14
+
15
+ import click
16
+ from typing import Optional
17
+
18
+ from half_orm_dev.repo import Repo
19
+ from half_orm_dev.patch_manager import PatchManagerError
20
+ from half_orm_dev.release_manager import ReleaseManagerError
21
+ from half_orm import utils
22
+
23
+
24
+ @click.group()
25
+ def patch():
26
+ """
27
+ Patch development and management commands.
28
+
29
+ Create, apply, and integrate patches into releases with this
30
+ unified command group.
31
+
32
+ \b
33
+ Common workflow:
34
+ 1. half_orm dev patch new <patch_id>
35
+ 2. half_orm dev patch apply
36
+ 3. half_orm dev patch add <patch_id>
37
+ """
38
+ pass
39
+
40
+
41
+ @patch.command('new')
42
+ @click.argument('patch_id', type=str)
43
+ @click.option(
44
+ '--description', '-d',
45
+ type=str,
46
+ default=None,
47
+ help='Optional description for the patch'
48
+ )
49
+ def patch_new(patch_id: str, description: Optional[str] = None) -> None:
50
+ """
51
+ Create new patch branch and directory structure.
52
+
53
+ Creates a new ho-patch/PATCH_ID branch from ho-prod and sets up the
54
+ corresponding Patches/PATCH_ID/ directory structure for schema changes.
55
+
56
+ This command must be run from the ho-prod branch. All business logic
57
+ is delegated to PatchManager.
58
+
59
+ \b
60
+ Args:
61
+ patch_id: Patch identifier (e.g., "456" or "456-user-authentication")
62
+ description: Optional description to include in patch README
63
+
64
+ \b
65
+ Examples:
66
+ Create patch with numeric ID:
67
+ $ half_orm dev patch new 456
68
+
69
+ Create patch with full ID and description:
70
+ $ half_orm dev patch new 456-user-auth -d "Add user authentication"
71
+
72
+ \b
73
+ Raises:
74
+ click.ClickException: If validation fails or creation errors occur
75
+ """
76
+ try:
77
+ # Get repository instance
78
+ repo = Repo()
79
+
80
+ # Delegate to PatchManager
81
+ result = repo.patch_manager.create_patch(patch_id, description)
82
+
83
+ # Display success message
84
+ click.echo(f"✓ Created patch branch: {utils.Color.bold(result['branch_name'])}")
85
+ click.echo(f"✓ Created patch directory: {utils.Color.bold(str(result['patch_dir']))}")
86
+ click.echo(f"✓ Switched to branch: {utils.Color.bold(result['on_branch'])}")
87
+ click.echo()
88
+ click.echo("📝 Next steps:")
89
+ click.echo(f" 1. Add SQL/Python files to {result['patch_dir']}/")
90
+ click.echo(f" 2. Run: {utils.Color.bold('half_orm dev patch apply')}")
91
+ click.echo(" 3. Test your changes")
92
+ click.echo(f" 4. Run: {utils.Color.bold('half_orm dev patch add')} {patch_id}")
93
+
94
+ except PatchManagerError as e:
95
+ raise click.ClickException(str(e))
96
+
97
+
98
+ @patch.command('apply')
99
+ def patch_apply() -> None:
100
+ """
101
+ Apply current patch files to database.
102
+
103
+ Must be run from ho-patch/PATCH_ID branch. Automatically detects
104
+ patch from current branch name and executes complete workflow:
105
+ database restoration, patch application, and code generation.
106
+
107
+ This command has no parameters - patch detection is automatic from
108
+ the current Git branch. All business logic is delegated to
109
+ PatchManager.apply_patch_complete_workflow().
110
+
111
+ \b
112
+ Workflow:
113
+ 1. Validate current branch is ho-patch/*
114
+ 2. Extract patch_id from branch name
115
+ 3. Restore database from model/schema.sql
116
+ 4. Apply patch SQL/Python files in lexicographic order
117
+ 5. Generate halfORM Python code via modules.py
118
+ 6. Display detailed report with next steps
119
+
120
+ \b
121
+ Branch Requirements:
122
+ - Must be on ho-patch/PATCH_ID branch
123
+ - Branch name format: ho-patch/456 or ho-patch/456-description
124
+ - Corresponding Patches/PATCH_ID/ directory must exist
125
+
126
+ \b
127
+ Examples:
128
+ On branch ho-patch/456-user-auth:
129
+ $ half_orm dev patch apply
130
+
131
+ \b
132
+ Output:
133
+ ✓ Current branch: ho-patch/456-user-auth
134
+ ✓ Detected patch: 456-user-auth
135
+ ✓ Database restored from model/schema.sql
136
+ ✓ Applied 2 patch file(s):
137
+ • 01_create_users.sql
138
+ • 02_add_indexes.sql
139
+ ✓ Generated 3 Python file(s):
140
+ • mydb/mydb/public/user.py
141
+ • mydb/mydb/public/user_session.py
142
+ • tests/mydb/public/test_user.py
143
+
144
+ \b
145
+ 📝 Next steps:
146
+ 1. Review generated code in mydb/mydb/
147
+ 2. Implement business logic stubs
148
+ 3. Run: half_orm dev test
149
+ 4. Commit: git add . && git commit -m 'Implement business logic'
150
+
151
+ \b
152
+ Raises:
153
+ click.ClickException: If branch validation fails or application errors occur
154
+ """
155
+ try:
156
+ # Get repository instance
157
+ repo = Repo()
158
+
159
+ # Get current branch
160
+ current_branch = repo.hgit.branch
161
+
162
+ # Validate branch format
163
+ if not current_branch.startswith('ho-patch/'):
164
+ raise click.ClickException(
165
+ f"Must be on ho-patch/* branch. Current branch: {current_branch}\n"
166
+ f"Use: half_orm dev patch new <patch_id>"
167
+ )
168
+
169
+ # Extract patch_id from branch name
170
+ patch_id = current_branch.replace('ho-patch/', '')
171
+
172
+ # Display current context
173
+ click.echo(f"✓ Current branch: {utils.Color.bold(current_branch)}")
174
+ click.echo(f"✓ Detected patch: {utils.Color.bold(patch_id)}")
175
+ click.echo()
176
+
177
+ # Delegate to PatchManager
178
+ click.echo("Applying patch...")
179
+ result = repo.patch_manager.apply_patch_complete_workflow(patch_id)
180
+
181
+ # Display success
182
+ click.echo(f"✓ {utils.Color.green('Patch applied successfully!')}")
183
+ click.echo(f"✓ Database restored from model/schema.sql")
184
+ click.echo()
185
+
186
+ # Display applied files
187
+ applied_files = result.get('applied_release_files', []) + result.get('applied_current_files', [])
188
+ if applied_files:
189
+ click.echo(f"✓ Applied {len(applied_files)} patch file(s):")
190
+ for filename in applied_files:
191
+ click.echo(f" • {filename}")
192
+ click.echo()
193
+ else:
194
+ click.echo("ℹ No patch files to apply (empty patch)")
195
+ click.echo()
196
+
197
+ # Display generated files
198
+ if result['generated_files']:
199
+ click.echo(f"✓ Generated {len(result['generated_files'])} Python file(s):")
200
+ for filepath in result['generated_files']:
201
+ click.echo(f" • {filepath}")
202
+ click.echo()
203
+ else:
204
+ click.echo("ℹ No Python files generated (no schema changes)")
205
+ click.echo()
206
+
207
+ # Display next steps
208
+ click.echo("📝 Next steps:")
209
+ click.echo(" 1. Review generated code")
210
+ click.echo(" 2. Implement business logic stubs")
211
+ click.echo(f" 3. Run: {utils.Color.bold('half_orm dev test')}")
212
+ click.echo(f""" 4. Commit: {utils.Color.bold('git add . && git commit -m "Implement business logic"')}""")
213
+ click.echo()
214
+
215
+ except PatchManagerError as e:
216
+ raise click.ClickException(str(e))
217
+
218
+
219
+ @patch.command('add')
220
+ @click.argument('patch_id', type=str)
221
+ @click.option(
222
+ '--to-version', '-v',
223
+ type=str,
224
+ default=None,
225
+ help='Target release version (required if multiple stage releases exist)'
226
+ )
227
+ def patch_add(patch_id: str, to_version: Optional[str] = None) -> None:
228
+ """
229
+ Add patch to stage release file with validation.
230
+
231
+ Integrates developed patch into a stage release for deployment.
232
+ Must be run from ho-prod branch. All business logic is delegated
233
+ to ReleaseManager with distributed lock for safe concurrent operations.
234
+
235
+ \b
236
+ Complete workflow:
237
+ 1. Acquire exclusive lock on ho-prod (via Git tag)
238
+ 2. Create temporary validation branch
239
+ 3. Apply all release patches + current patch
240
+ 4. Run pytest validation tests
241
+ 5. If tests pass: integrate to ho-prod
242
+ 6. If tests fail: cleanup and exit with error
243
+ 7. Send resync notifications to other patch branches
244
+ 8. Archive patch branch to ho-release/{version}/ namespace
245
+ 9. Cleanup patch branch
246
+ 10. Release lock
247
+
248
+ \b
249
+ Args:
250
+ patch_id: Patch identifier to add (e.g., "456-user-auth")
251
+ to_version: Target release version (auto-detected if single stage exists)
252
+
253
+ \b
254
+ Branch Requirements:
255
+ - Must be on ho-prod branch
256
+ - Repository must be clean (no uncommitted changes)
257
+ - Must be synced with origin/ho-prod
258
+ - Patch branch ho-patch/PATCH_ID must exist
259
+ - At least one stage release file must exist
260
+
261
+ \b
262
+ Examples:
263
+ Add patch to auto-detected stage release:
264
+ $ half_orm dev patch add 456-user-auth
265
+
266
+ Add patch to specific version:
267
+ $ half_orm dev patch add 456-user-auth --to-version 1.3.6
268
+
269
+ \b
270
+ Output:
271
+ ✓ Detected stage release: 1.3.6-stage.txt
272
+ ✓ Validated patch 456-user-auth
273
+ ✓ All tests passed
274
+ ✓ Integrated to ho-prod
275
+ ✓ Archived branch: ho-release/1.3.6/456-user-auth
276
+ ✓ Notified 2 active patch branches
277
+
278
+ 📝 Next steps:
279
+ 1. Other developers: git pull && git rebase ho-prod
280
+ 2. Continue development: half_orm dev patch new <next_patch_id>
281
+ 3. Promote to RC: half_orm dev release promote rc
282
+
283
+ \b
284
+ Raises:
285
+ click.ClickException: If validation fails or integration errors occur
286
+ """
287
+ try:
288
+ # Get repository instance
289
+ repo = Repo()
290
+
291
+ # Display context
292
+ click.echo(f"Adding patch {utils.Color.bold(patch_id)} to stage release...")
293
+ click.echo()
294
+
295
+ # Delegate to ReleaseManager
296
+ result = repo.release_manager.add_patch_to_release(patch_id, to_version)
297
+
298
+ # Display success message
299
+ click.echo(f"✓ {utils.Color.green('Patch added to release successfully!')}")
300
+ click.echo()
301
+ click.echo(f" Stage file: {utils.Color.bold(result['stage_file'])}")
302
+ click.echo(f" Patch added: {utils.Color.bold(result['patch_id'])}")
303
+ click.echo(f" Tests passed: {utils.Color.green('✓')}")
304
+ click.echo(f" Archived branch: {utils.Color.bold(result['archived_branch'])}")
305
+
306
+ if result.get('notified_branches'):
307
+ click.echo(f" Notified: {len(result['notified_branches'])} active branch(es)")
308
+
309
+ click.echo()
310
+ click.echo("📝 Next steps:")
311
+ click.echo(f" • Other developers: {utils.Color.bold('git pull && git rebase ho-prod')}")
312
+ click.echo(f" • Continue development: {utils.Color.bold('half_orm dev patch new <next_patch_id>')}")
313
+ click.echo(f" • Promote to RC: {utils.Color.bold('half_orm dev release promote rc')}")
314
+ click.echo()
315
+
316
+ except ReleaseManagerError as e:
317
+ raise click.ClickException(str(e))
@@ -0,0 +1,21 @@
1
+ """
2
+ Prepare command - Prepares the next release
3
+ """
4
+
5
+ import sys
6
+ import click
7
+ from half_orm_dev.repo import Repo
8
+
9
+
10
+ @click.command()
11
+ @click.option(
12
+ '-l', '--level',
13
+ type=click.Choice(['patch', 'minor', 'major']),
14
+ help="Release level."
15
+ )
16
+ @click.option('-m', '--message', type=str, help="The git commit message")
17
+ def prepare(level, message=None):
18
+ """Prepares the next release."""
19
+ repo = Repo()
20
+ repo.prepare_release(level, message)
21
+ sys.exit()
@@ -0,0 +1,119 @@
1
+ """
2
+ Prepare-release command - Prepares the next release stage file
3
+
4
+ Creates releases/X.Y.Z-stage.txt based on production version and increment level.
5
+ Part of Git-centric release workflow (stage → rc → production).
6
+ """
7
+
8
+ import click
9
+ import sys
10
+ from half_orm_dev.repo import Repo
11
+ from half_orm_dev.release_manager import (
12
+ ReleaseManagerError,
13
+ ReleaseFileError,
14
+ ReleaseVersionError
15
+ )
16
+ from half_orm import utils
17
+
18
+
19
+ @click.command()
20
+ @click.argument(
21
+ 'level',
22
+ type=click.Choice(['patch', 'minor', 'major'], case_sensitive=False)
23
+ )
24
+ def prepare_release(level):
25
+ """
26
+ Prepare next release stage file.
27
+
28
+ Creates releases/X.Y.Z-stage.txt based on production version and
29
+ semantic versioning increment level.
30
+
31
+ LEVEL: Version increment type (patch, minor, or major)
32
+
33
+ \b
34
+ Semantic versioning rules:
35
+ • patch: Bug fixes, minor changes (1.3.5 → 1.3.6)
36
+ • minor: New features, backward compatible (1.3.5 → 1.4.0)
37
+ • major: Breaking changes (1.3.5 → 2.0.0)
38
+
39
+ \b
40
+ Workflow:
41
+ 1. Read production version from model/schema.sql
42
+ 2. Calculate next version (patch/minor/major)
43
+ 3. Create releases/X.Y.Z-stage.txt
44
+ 4. Commit and push to reserve version globally
45
+
46
+ \b
47
+ Requirements:
48
+ • Must be on ho-prod branch
49
+ • Repository must be clean (no uncommitted changes)
50
+ • Must be synced with origin/ho-prod
51
+
52
+ \b
53
+ Examples:
54
+ # Prepare patch release (production 1.3.5 → 1.3.6)
55
+ half_orm dev prepare-release patch
56
+
57
+ # Prepare minor release (production 1.3.5 → 1.4.0)
58
+ half_orm dev prepare-release minor
59
+
60
+ # Prepare major release (production 1.3.5 → 2.0.0)
61
+ half_orm dev prepare-release major
62
+
63
+ \b
64
+ Next steps after prepare-release:
65
+ • Create patches: half_orm dev create-patch <patch_id>
66
+ • Add to release: half_orm dev add-to-release <patch_id>
67
+ • Promote to RC: half_orm dev promote-to rc
68
+ """
69
+ # Normalize level to lowercase
70
+ level = level.lower()
71
+
72
+ try:
73
+ # Get Repo singleton
74
+ repo = Repo()
75
+
76
+ # Get ReleaseManager
77
+ release_mgr = repo.release_manager
78
+
79
+ click.echo(f"Preparing {level} release...")
80
+ click.echo()
81
+
82
+ # Prepare release
83
+ result = release_mgr.prepare_release(level)
84
+
85
+ # Extract result info
86
+ version = result['version']
87
+ stage_file = result['file']
88
+ previous_version = result['previous_version']
89
+
90
+ # Success message
91
+ click.echo(f"✅ {utils.Color.bold('Release prepared successfully!')}")
92
+ click.echo()
93
+ click.echo(f" Previous version: {utils.Color.bold(previous_version)}")
94
+ click.echo(f" New version: {utils.Color.bold(version)}")
95
+ click.echo(f" Stage file: {utils.Color.bold(stage_file)}")
96
+ click.echo()
97
+ click.echo(f"📝 Next steps:")
98
+ click.echo(f" 1. Create patches: {utils.Color.bold(f'half_orm dev create-patch <patch_id>')}")
99
+ click.echo(f" 2. Add to release: {utils.Color.bold(f'half_orm dev add-to-release <patch_id>')}")
100
+ click.echo(f" 3. Promote to RC: {utils.Color.bold('half_orm dev promote-to rc')}")
101
+ click.echo()
102
+
103
+ except ReleaseManagerError as e:
104
+ # Handle validation errors (branch, clean, sync, etc.)
105
+ click.echo(f"❌ {utils.Color.red('Release preparation failed:')}", err=True)
106
+ click.echo(f" {str(e)}", err=True)
107
+ sys.exit(1)
108
+
109
+ except ReleaseFileError as e:
110
+ # Handle file errors (missing schema, stage exists, etc.)
111
+ click.echo(f"❌ {utils.Color.red('File error:')}", err=True)
112
+ click.echo(f" {str(e)}", err=True)
113
+ sys.exit(1)
114
+
115
+ except ReleaseVersionError as e:
116
+ # Handle version errors (invalid format, calculation, etc.)
117
+ click.echo(f"❌ {utils.Color.red('Version error:')}", err=True)
118
+ click.echo(f" {str(e)}", err=True)
119
+ sys.exit(1)
@@ -0,0 +1,127 @@
1
+ """
2
+ Promote-to command implementation.
3
+
4
+ Thin CLI layer that delegates to ReleaseManager for business logic.
5
+ Promotes stage release to target ('rc' or 'prod') with code merge and branch cleanup.
6
+ """
7
+
8
+ import click
9
+ import sys
10
+
11
+ from half_orm_dev.repo import Repo
12
+ from half_orm_dev.release_manager import ReleaseManagerError
13
+ from half_orm import utils
14
+
15
+
16
+ @click.command('promote-to')
17
+ @click.argument('target', type=click.Choice(['rc', 'prod'], case_sensitive=False))
18
+ def promote_to(target: str) -> None:
19
+ """
20
+ Promote stage release to release candidate.
21
+
22
+ Promotes the smallest stage release to RC (rc1, rc2, etc.), merges all
23
+ archived patch code into ho-prod, and deletes patch branches. Must be
24
+ run from ho-prod branch.
25
+
26
+ Complete workflow:
27
+ 1. Detect smallest stage release (sequential promotion)
28
+ 2. Validate single active RC rule
29
+ 3. Acquire distributed lock on ho-prod
30
+ 4. Merge archived patches code into ho-prod
31
+ 5. Rename stage file to RC file (git mv)
32
+ 6. Commit and push promotion
33
+ 7. Send rebase notifications to active branches
34
+ 8. Cleanup patch branches
35
+ 9. Release lock
36
+
37
+ Examples:
38
+ # Promote smallest stage release
39
+ $ half_orm dev promote-to
40
+
41
+ # Output:
42
+ # ✓ Promoted 1.3.5-stage → 1.3.5-rc1
43
+ # ✓ Merged 3 patches into ho-prod
44
+ # ✓ Deleted 3 patch branches
45
+ # ✓ Notified 2 active branches
46
+
47
+ Raises:
48
+ click.ClickException: If validations fail or workflow errors occur
49
+ """
50
+ try:
51
+ # Get repository instance
52
+ repo = Repo()
53
+
54
+ # Delegate to ReleaseManager
55
+ click.echo(f"Promoting stage release to {target.upper()}...")
56
+ click.echo()
57
+
58
+ result = repo.release_manager.promote_to(target.lower())
59
+
60
+ # Display success message
61
+ click.echo(f"✓ {utils.Color.green('Success!')}")
62
+ click.echo()
63
+ click.echo(f"Promoted: {utils.Color.bold(result['from_file'])} → {utils.Color.bold(result['to_file'])}")
64
+ click.echo(f"Version: {utils.Color.bold(result['version'])}")
65
+ if result.get('rc_number'):
66
+ click.echo(f"RC number: {utils.Color.bold(str(result['rc_number']))}")
67
+ click.echo()
68
+
69
+ # Display merged patches
70
+ if result['patches_merged']:
71
+ click.echo(f"✓ Merged {len(result['patches_merged'])} patches into ho-prod:")
72
+ for patch in result['patches_merged']:
73
+ click.echo(f" • {patch}")
74
+ click.echo()
75
+ else:
76
+ click.echo("✓ No patches to merge (empty stage)")
77
+ click.echo()
78
+
79
+ # Display deleted branches
80
+ if result['branches_deleted']:
81
+ click.echo(f"✓ Deleted {len(result['branches_deleted'])} patch branches")
82
+ click.echo()
83
+
84
+ # Display notifications
85
+ if result.get('notifications_sent'):
86
+ click.echo(f"✓ Notified {len(result['notifications_sent'])} active patch branches:")
87
+ for branch in result['notifications_sent']:
88
+ click.echo(f" • {branch}")
89
+ click.echo()
90
+
91
+ # Display commit info
92
+ click.echo(f"Commit: {utils.Color.bold(result['commit_sha'][:8])}")
93
+ click.echo(f"Lock: {result['lock_tag']}")
94
+ click.echo()
95
+
96
+ # Next steps
97
+ click.echo(f"{utils.Color.bold('📝 Next steps:')}")
98
+ click.echo(f""" 1. Test RC: {utils.Color.bold(f'half_orm dev apply-release {result["to_file"].replace(".txt", "")}')}""")
99
+ click.echo(f" 2. If tests pass: {utils.Color.bold('half_orm dev promote-to prod')}")
100
+ click.echo(f" 3. If issues found: Create fix patch and add to new RC")
101
+ click.echo()
102
+
103
+ except ReleaseManagerError as e:
104
+ # Handle expected errors
105
+ click.echo(f"❌ {utils.Color.red('Promotion failed:')}", err=True)
106
+ click.echo(f" {str(e)}", err=True)
107
+ click.echo()
108
+
109
+ # Provide helpful hints based on error type
110
+ error_msg = str(e).lower()
111
+
112
+ if "must be on ho-prod" in error_msg:
113
+ click.echo("💡 Switch to ho-prod: git checkout ho-prod", err=True)
114
+ elif "uncommitted changes" in error_msg:
115
+ click.echo("💡 Commit or stash changes first", err=True)
116
+ elif "no stage releases" in error_msg:
117
+ click.echo("💡 Create a stage release: half_orm dev prepare-release", err=True)
118
+ elif "rc" in error_msg and "must be deployed" in error_msg:
119
+ click.echo("💡 Deploy existing RC to production first", err=True)
120
+ elif "lock held" in error_msg or "lock" in error_msg:
121
+ click.echo("💡 Another operation in progress, retry later", err=True)
122
+ elif "merge conflict" in error_msg:
123
+ click.echo("💡 Resolve conflicts manually and retry", err=True)
124
+ elif "diverged" in error_msg:
125
+ click.echo("💡 Resolve ho-prod divergence: git pull origin ho-prod", err=True)
126
+
127
+ sys.exit(1)