half-orm-dev 0.17.0a7__tar.gz → 0.17.0a10__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.
Files changed (65) hide show
  1. {half_orm_dev-0.17.0a7/half_orm_dev.egg-info → half_orm_dev-0.17.0a10}/PKG-INFO +1 -1
  2. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/check.py +24 -6
  3. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/patch.py +2 -102
  4. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/release.py +4 -5
  5. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/decorators.py +16 -11
  6. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/hgit.py +68 -10
  7. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patch_manager.py +91 -24
  8. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/release_manager.py +112 -31
  9. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/repo.py +51 -39
  10. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/utils.py +0 -13
  11. half_orm_dev-0.17.0a10/half_orm_dev/version.txt +1 -0
  12. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10/half_orm_dev.egg-info}/PKG-INFO +1 -1
  13. half_orm_dev-0.17.0a7/half_orm_dev/version.txt +0 -1
  14. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/AUTHORS +0 -0
  15. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/LICENSE +0 -0
  16. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/README.md +0 -0
  17. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/__init__.py +0 -0
  18. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/__init__.py +0 -0
  19. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/__init__.py +0 -0
  20. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/apply.py +0 -0
  21. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/clone.py +0 -0
  22. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/init.py +0 -0
  23. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/new.py +0 -0
  24. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/restore.py +0 -0
  25. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/sync.py +0 -0
  26. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/todo.py +0 -0
  27. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/undo.py +0 -0
  28. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/update.py +0 -0
  29. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/commands/upgrade.py +0 -0
  30. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli/main.py +0 -0
  31. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/cli_extension.py +0 -0
  32. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/database.py +0 -0
  33. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/hop.py +0 -0
  34. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/manifest.py +0 -0
  35. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/modules.py +0 -0
  36. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patch.py +0 -0
  37. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patch_validator.py +0 -0
  38. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  39. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  40. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  41. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patches/log +0 -0
  42. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  43. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/.gitignore +0 -0
  44. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/MANIFEST.in +0 -0
  45. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/Pipfile +0 -0
  46. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/README +0 -0
  47. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/conftest_template +0 -0
  48. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  49. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  50. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/init_module_template +0 -0
  51. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/module_template_1 +0 -0
  52. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/module_template_2 +0 -0
  53. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/module_template_3 +0 -0
  54. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/relation_test +0 -0
  55. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/setup.py +0 -0
  56. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/sql_adapter +0 -0
  57. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev/templates/warning +0 -0
  58. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev.egg-info/SOURCES.txt +0 -0
  59. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  60. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev.egg-info/requires.txt +0 -0
  61. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/half_orm_dev.egg-info/top_level.txt +0 -0
  62. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/setup.cfg +0 -0
  63. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/setup.py +0 -0
  64. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/tests/__init__.py +0 -0
  65. {half_orm_dev-0.17.0a7 → half_orm_dev-0.17.0a10}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.0a7
3
+ Version: 0.17.0a10
4
4
  Summary: half_orm development Framework.
5
5
  Home-page: https://github.com/collorg/halfORM_dev
6
6
  Author: Joël Maïzi
@@ -97,7 +97,7 @@ def _display_check_results(result: dict, dry_run: bool, prune_branches: bool, ve
97
97
  # Show releases with candidates and staged patches
98
98
  releases_info = result.get('releases_info', {})
99
99
  if releases_info:
100
- _display_releases_with_patches(releases_info, patch_branches, verbose)
100
+ _display_releases_with_patches(releases_info, patch_branches, release_branches, verbose)
101
101
  elif verbose:
102
102
  click.echo(f"\n📦 {utils.Color.bold('Active releases:')} None")
103
103
 
@@ -220,12 +220,13 @@ def _display_branch_info(branch_info: dict, verbose: bool, indent: str = " ", s
220
220
  click.echo(f"{indent}{marker}• {display_name} - {status}")
221
221
 
222
222
 
223
- def _display_releases_with_patches(releases_info: dict, patch_branches: list, verbose: bool):
223
+ def _display_releases_with_patches(releases_info: dict, patch_branches: list, release_branches: list, verbose: bool):
224
224
  """Display releases grouped by version with candidates and staged patches.
225
225
 
226
226
  Args:
227
227
  releases_info: Dict of {version: {candidates: [], staged: [], ...}}
228
228
  patch_branches: List of patch branch info dicts
229
+ release_branches: List of release branch info dicts
229
230
  verbose: Show verbose output
230
231
  """
231
232
  # Sort versions
@@ -236,9 +237,22 @@ def _display_releases_with_patches(releases_info: dict, patch_branches: list, ve
236
237
  candidates = info.get('candidates', [])
237
238
  staged = info.get('staged', [])
238
239
 
239
- # Release header
240
- total_patches = len(candidates) + len(staged)
241
- click.echo(f"\n📦 {utils.Color.bold(f'Release {version}')} (ho-release/{version}):")
240
+ # Check if release branch exists
241
+ release_branch_name = f"ho-release/{version}"
242
+ release_branch_info = next((b for b in release_branches if b['name'] == release_branch_name), None)
243
+
244
+ # Release header with status
245
+ release_status = ""
246
+ if release_branch_info:
247
+ if not release_branch_info.get('exists_on_remote', False) and release_branch_info.get('exists_locally', False):
248
+ release_status = f" {utils.Color.yellow('⚠️ local only - remote deleted')}"
249
+ elif release_branch_info.get('sync_status') == 'remote_only':
250
+ release_status = f" {utils.Color.blue('☁️ on remote only')}"
251
+ else:
252
+ # Release files exist but no branch at all
253
+ release_status = f" {utils.Color.red('⚠️ branch not found')}"
254
+
255
+ click.echo(f"\n📦 {utils.Color.bold(f'Release {version}')} (ho-release/{version}):{release_status}")
242
256
 
243
257
  # Show staged patches
244
258
  if staged:
@@ -261,18 +275,22 @@ def _display_releases_with_patches(releases_info: dict, patch_branches: list, ve
261
275
 
262
276
  if sync_status == 'synced':
263
277
  status = utils.Color.green("✓ synced")
278
+ elif sync_status == 'remote_only':
279
+ status = utils.Color.blue("☁️ on remote only (run: git checkout -b ho-patch/" + patch_id + " origin/ho-patch/" + patch_id + ")")
264
280
  elif sync_status == 'behind':
265
281
  status = utils.Color.blue(f"⚠️ {behind} commits behind")
266
282
  elif sync_status == 'ahead':
267
283
  status = utils.Color.blue(f"↑ {ahead} ahead")
268
284
  elif sync_status == 'diverged':
269
285
  status = utils.Color.red(f"⚠ diverged (↑{ahead} ↓{behind})")
286
+ elif sync_status == 'no_remote':
287
+ status = utils.Color.yellow("⚠️ local only (remote deleted or not pushed - run: git branch -d " + branch_name + ")")
270
288
  else:
271
289
  status = "?"
272
290
 
273
291
  click.echo(f" • {patch_id} - {status}")
274
292
  else:
275
- # Branch doesn't exist locally
293
+ # Branch doesn't exist anywhere
276
294
  click.echo(f" • {patch_id} {utils.Color.red('⚠ branch not found')}")
277
295
 
278
296
  if not staged and not candidates:
@@ -4,7 +4,7 @@ Patch command group - Unified patch development and management.
4
4
  Groups all patch-related commands under 'half_orm dev patch':
5
5
  - patch new: Create new patch branch and directory
6
6
  - patch apply: Apply current patch files to database
7
- - patch add: Add patch to stage release with validation
7
+ - patch close: Add patch to stage release with validation
8
8
 
9
9
  Replaces legacy commands:
10
10
  - create-patch → patch new
@@ -33,7 +33,7 @@ def patch():
33
33
  Common workflow:
34
34
  1. half_orm dev patch new <patch_id>
35
35
  2. half_orm dev patch apply
36
- 3. half_orm dev patch add <patch_id>
36
+ 3. half_orm dev patch close <patch_id>
37
37
  """
38
38
  pass
39
39
 
@@ -288,103 +288,3 @@ def patch_close(patch_id: str) -> None:
288
288
 
289
289
  except PatchManagerError as e:
290
290
  raise click.ClickException(str(e))
291
-
292
-
293
- @patch.command('add')
294
- @click.argument('patch_id', type=str)
295
- @click.option(
296
- '--to-version', '-v',
297
- type=str,
298
- default=None,
299
- help='Target release version (required if multiple stage releases exist)'
300
- )
301
- def patch_add(patch_id: str, to_version: Optional[str] = None) -> None:
302
- """
303
- Add patch to stage release file with validation.
304
-
305
- Integrates developed patch into a stage release for deployment.
306
- Must be run from ho-prod branch. All business logic is delegated
307
- to ReleaseManager with distributed lock for safe concurrent operations.
308
-
309
- \b
310
- Complete workflow:
311
- 1. Acquire exclusive lock on ho-prod (via Git tag)
312
- 2. Create temporary validation branch
313
- 3. Apply all release patches + current patch
314
- 4. Run pytest validation tests
315
- 5. If tests pass: integrate to ho-prod
316
- 6. If tests fail: cleanup and exit with error
317
- 7. Send resync notifications to other patch branches
318
- 8. Archive patch branch to ho-release/{version}/ namespace
319
- 9. Cleanup patch branch
320
- 10. Release lock
321
-
322
- \b
323
- Args:
324
- patch_id: Patch identifier to add (e.g., "456-user-auth")
325
- to_version: Target release version (auto-detected if single stage exists)
326
-
327
- \b
328
- Branch Requirements:
329
- - Must be on ho-prod branch
330
- - Repository must be clean (no uncommitted changes)
331
- - Must be synced with origin/ho-prod
332
- - Patch branch ho-patch/PATCH_ID must exist
333
- - At least one stage release file must exist
334
-
335
- \b
336
- Examples:
337
- Add patch to auto-detected stage release:
338
- $ half_orm dev patch add 456-user-auth
339
-
340
- Add patch to specific version:
341
- $ half_orm dev patch add 456-user-auth --to-version 1.3.6
342
-
343
- \b
344
- Output:
345
- ✓ Detected stage release: 1.3.6-stage.txt
346
- ✓ Validated patch 456-user-auth
347
- ✓ All tests passed
348
- ✓ Integrated to ho-prod
349
- ✓ Archived branch: ho-release/1.3.6/456-user-auth
350
- ✓ Notified 2 active patch branches
351
-
352
- 📝 Next steps:
353
- 1. Other developers: git pull && git rebase ho-prod
354
- 2. Continue development: half_orm dev patch new <next_patch_id>
355
- 3. Promote to RC: half_orm dev release promote rc
356
-
357
- \b
358
- Raises:
359
- click.ClickException: If validation fails or integration errors occur
360
- """
361
- try:
362
- # Get repository instance
363
- repo = Repo()
364
-
365
- # Display context
366
- click.echo(f"Adding patch {utils.Color.bold(patch_id)} to stage release...")
367
- click.echo()
368
-
369
- # Delegate to ReleaseManager
370
- result = repo.release_manager.add_patch_to_release(patch_id, to_version)
371
-
372
- # Display success message
373
- click.echo(f"✓ {utils.Color.green('Patch added to release successfully!')}")
374
- click.echo()
375
- click.echo(f" Stage file: {utils.Color.bold(result['stage_file'])}")
376
- click.echo(f" Patch added: {utils.Color.bold(result['patch_id'])}")
377
- click.echo(f" Tests passed: {utils.Color.green('✓')}")
378
-
379
- if result.get('notified_branches'):
380
- click.echo(f" Notified: {len(result['notified_branches'])} active branch(es)")
381
-
382
- click.echo()
383
- click.echo("📝 Next steps:")
384
- click.echo(f" • Other developers: {utils.Color.bold('git pull && git rebase ho-prod')}")
385
- click.echo(f" • Continue development: {utils.Color.bold('half_orm dev patch new <next_patch_id>')}")
386
- click.echo(f" • Promote to RC: {utils.Color.bold('half_orm dev release promote rc')}")
387
- click.echo()
388
-
389
- except ReleaseManagerError as e:
390
- raise click.ClickException(str(e))
@@ -345,13 +345,12 @@ def release_hotfix(version: Optional[str] = None) -> None:
345
345
 
346
346
  # Display context
347
347
  if version:
348
- click.echo(f"Reopening version {utils.Color.bold(version)} for hotfix...")
349
- else:
350
- click.echo("Reopening current production version for hotfix...")
348
+ click.echo(f"⚠️ Version parameter is ignored - will reopen current production version")
349
+ click.echo("Reopening current production version for hotfix...")
351
350
  click.echo()
352
351
 
353
- # Delegate to ReleaseManager
354
- result = repo.release_manager.reopen_for_hotfix(version)
352
+ # Delegate to ReleaseManager (auto-detects version)
353
+ result = repo.release_manager.reopen_for_hotfix()
355
354
 
356
355
  # Display success message
357
356
  click.echo(f"✓ {utils.Color.green('Version reopened for hotfix successfully!')}")
@@ -7,34 +7,39 @@ Provides common decorators for ReleaseManager and PatchManager.
7
7
  from functools import wraps
8
8
 
9
9
 
10
- def with_ho_prod_lock(branch: str = "ho-prod", timeout_minutes: int = 30):
10
+ def with_dynamic_branch_lock(branch_getter, timeout_minutes: int = 30):
11
11
  """
12
- Decorator to protect methods that modify ho-prod with a lock tag.
12
+ Decorator to protect methods with a dynamic branch lock.
13
13
 
14
- The lock tag allows the pre-commit hook to permit commits on ho-prod
15
- during the execution of the decorated method.
14
+ Unlike with_branch_lock which uses a static branch name, this decorator
15
+ calls a function to determine the branch name at runtime.
16
16
 
17
17
  Args:
18
- branch: Branch to lock (default: "ho-prod")
18
+ branch_getter: Callable that takes (self, *args, **kwargs) and returns branch name
19
19
  timeout_minutes: Lock timeout in minutes (default: 30)
20
20
 
21
21
  Usage:
22
- @with_ho_prod_lock()
23
- def my_method(self, ...):
24
- # Can commit to ho-prod here
22
+ def _get_release_branch(self, patch_id, *args, **kwargs):
23
+ # Logic to determine release branch from patch_id
24
+ return f"ho-release/{version}"
25
+
26
+ @with_dynamic_branch_lock(_get_release_branch)
27
+ def close_patch(self, patch_id):
28
+ # Will lock the release branch determined by _get_release_branch
25
29
  ...
26
30
 
27
31
  Notes:
28
- - The decorator assumes `self._repo.hgit` has `acquire_branch_lock()`
29
- and `release_branch_lock()` methods
32
+ - branch_getter is called with the same arguments as the decorated function
30
33
  - The lock is ALWAYS released in the finally block, even on error
31
- - If lock acquisition fails, the method is not executed
32
34
  """
33
35
  def decorator(func):
34
36
  @wraps(func)
35
37
  def wrapper(self, *args, **kwargs):
36
38
  lock_tag = None
37
39
  try:
40
+ # Determine branch name dynamically
41
+ branch = branch_getter(self, *args, **kwargs)
42
+
38
43
  # Acquire lock
39
44
  lock_tag = self._repo.hgit.acquire_branch_lock(branch, timeout_minutes=timeout_minutes)
40
45
 
@@ -255,6 +255,32 @@ class HGit:
255
255
  "Proxy to git.commit method"
256
256
  return self.__git_repo.git.checkout(*args, **kwargs)
257
257
 
258
+ def checkout_paths_from_branch(self, branch: str, paths: list) -> None:
259
+ """
260
+ Checkout specific paths from another branch into working directory.
261
+
262
+ This allows selective extraction of files/directories from a branch
263
+ without merging the entire branch. Used to sync metadata files
264
+ (like releases/*.txt) between branches.
265
+
266
+ Args:
267
+ branch: Source branch name (e.g., "ho-release/0.1.0")
268
+ paths: List of paths to checkout (e.g., ["releases/", "docs/"])
269
+
270
+ Examples:
271
+ # Copy only releases/ directory from release branch to current branch
272
+ hgit.checkout("ho-prod")
273
+ hgit.checkout_paths_from_branch("ho-release/0.1.0", ["releases/"])
274
+ # Now releases/ contains files from ho-release/0.1.0
275
+ hgit.commit("-m", "sync releases/ from ho-release/0.1.0")
276
+
277
+ Notes:
278
+ - Changes are staged automatically
279
+ - Current branch must be checked out first
280
+ - Paths are relative to repository root
281
+ """
282
+ self.__git_repo.git.checkout(branch, '--', *paths)
283
+
258
284
  def pull(self, *args, **kwargs):
259
285
  "Proxy to git.pull method"
260
286
  return self.__git_repo.git.pull(*args, **kwargs)
@@ -411,12 +437,13 @@ class HGit:
411
437
 
412
438
  def fetch_from_origin(self) -> None:
413
439
  """
414
- Fetch all references from origin remote.
440
+ Fetch all references from origin remote with pruning.
415
441
 
416
442
  Updates local knowledge of all remote references including:
417
443
  - All remote branches
418
444
  - All remote tags
419
445
  - Other remote refs
446
+ - Removes stale remote-tracking references (--prune)
420
447
 
421
448
  This is more comprehensive than fetch_tags() which only fetches tags.
422
449
  Used before patch creation to ensure up-to-date view of remote state.
@@ -427,10 +454,11 @@ class HGit:
427
454
  Examples:
428
455
  hgit.fetch_from_origin()
429
456
  # Local git now has complete up-to-date view of origin
457
+ # Stale remote refs (deleted branches on remote) are removed
430
458
  """
431
459
  try:
432
460
  origin = self.__git_repo.remote('origin')
433
- origin.fetch()
461
+ origin.fetch(prune=True)
434
462
  except Exception as e:
435
463
  from git.exc import GitCommandError
436
464
  if isinstance(e, GitCommandError):
@@ -1447,11 +1475,19 @@ class HGit:
1447
1475
  remote_branches = self.get_remote_branches()
1448
1476
  remote_branch_names = {b.replace('origin/', '') for b in remote_branches}
1449
1477
 
1450
- # Get ho-patch branches
1451
- patch_branches = self.get_local_branches(pattern="ho-patch/*")
1478
+ # Get ho-patch branches (local + remote)
1479
+ local_patch_branches = self.get_local_branches(pattern="ho-patch/*")
1480
+ remote_patch_branches = [b.replace('origin/', '') for b in remote_branches
1481
+ if b.startswith('origin/ho-patch/')]
1482
+ # Combine local and remote, avoiding duplicates
1483
+ all_patch_branches = list(set(local_patch_branches + remote_patch_branches))
1452
1484
 
1453
- # Get ho-release branches
1454
- release_branches = self.get_local_branches(pattern="ho-release/*")
1485
+ # Get ho-release branches (local + remote)
1486
+ local_release_branches = self.get_local_branches(pattern="ho-release/*")
1487
+ remote_release_branches = [b.replace('origin/', '') for b in remote_branches
1488
+ if b.startswith('origin/ho-release/')]
1489
+ # Combine local and remote, avoiding duplicates
1490
+ all_release_branches = list(set(local_release_branches + remote_release_branches))
1455
1491
 
1456
1492
  # Parse stage files to identify active release branches and their order
1457
1493
  active_release_patches = set()
@@ -1477,11 +1513,18 @@ class HGit:
1477
1513
  except Exception:
1478
1514
  pass
1479
1515
 
1480
- def get_branch_info(branch: str, check_stage: bool = False) -> dict:
1481
- """Helper to get branch status."""
1516
+ def get_branch_info(branch: str, check_stage: bool = False, is_local: bool = True) -> dict:
1517
+ """Helper to get branch status.
1518
+
1519
+ Args:
1520
+ branch: Branch name
1521
+ check_stage: Whether to check stage file membership
1522
+ is_local: Whether branch exists locally
1523
+ """
1482
1524
  info = {
1483
1525
  'name': branch,
1484
1526
  'is_current': branch == current_branch,
1527
+ 'exists_locally': is_local,
1485
1528
  'exists_on_remote': branch in remote_branch_names,
1486
1529
  'sync_status': 'unknown',
1487
1530
  'ahead': 0,
@@ -1508,9 +1551,13 @@ class HGit:
1508
1551
  info['in_stage_file'] = False
1509
1552
  info['order'] = 999
1510
1553
 
1554
+ # Determine sync status
1511
1555
  if not info['exists_on_remote']:
1512
1556
  info['sync_status'] = 'no_remote'
1557
+ elif not info['exists_locally']:
1558
+ info['sync_status'] = 'remote_only'
1513
1559
  else:
1560
+ # Both local and remote exist - check sync
1514
1561
  try:
1515
1562
  is_synced, status = self.is_branch_synced(branch, remote='origin')
1516
1563
  info['sync_status'] = status
@@ -1535,8 +1582,19 @@ class HGit:
1535
1582
 
1536
1583
  return info
1537
1584
 
1585
+ # Build branch info with local/remote indication
1586
+ patch_branch_infos = []
1587
+ for branch in all_patch_branches:
1588
+ is_local = branch in local_patch_branches
1589
+ patch_branch_infos.append(get_branch_info(branch, is_local=is_local))
1590
+
1591
+ release_branch_infos = []
1592
+ for branch in all_release_branches:
1593
+ is_local = branch in local_release_branches
1594
+ release_branch_infos.append(get_branch_info(branch, check_stage=True, is_local=is_local))
1595
+
1538
1596
  return {
1539
1597
  'current_branch': current_branch,
1540
- 'patch_branches': [get_branch_info(b) for b in patch_branches],
1541
- 'release_branches': [get_branch_info(b, check_stage=True) for b in release_branches]
1598
+ 'patch_branches': patch_branch_infos,
1599
+ 'release_branches': release_branch_infos
1542
1600
  }
@@ -20,6 +20,7 @@ from git.exc import GitCommandError
20
20
  from half_orm import utils
21
21
  from half_orm_dev import modules
22
22
  from .patch_validator import PatchValidator, PatchInfo
23
+ from .decorators import with_dynamic_branch_lock
23
24
 
24
25
 
25
26
  class PatchManagerError(Exception):
@@ -1227,6 +1228,7 @@ class PatchManager:
1227
1228
  # Directory deletion may fail (permissions, etc.) - continue
1228
1229
  pass
1229
1230
 
1231
+ @with_dynamic_branch_lock(lambda self, patch_id, description=None: "ho-prod")
1230
1232
  def create_patch(self, patch_id: str, description: Optional[str] = None) -> dict:
1231
1233
  """
1232
1234
  Create new patch with atomic tag-first reservation strategy.
@@ -1286,7 +1288,8 @@ class PatchManager:
1286
1288
  # With description for README and commits
1287
1289
  """
1288
1290
  # Step 1-3: Validate context
1289
- release_version = self._validate_on_ho_release() # NEW: returns version
1291
+ release_branch = self._validate_on_ho_release()
1292
+ release_version = release_branch.replace("ho-release/", "")
1290
1293
  self._validate_repo_clean()
1291
1294
  self._validate_has_remote()
1292
1295
 
@@ -1299,16 +1302,12 @@ class PatchManager:
1299
1302
  lock_tag = None
1300
1303
  # Save initial state for rollback
1301
1304
  initial_branch = self._repo.hgit.branch # Should be ho-release/X.Y.Z
1302
- release_branch = f"ho-release/{release_version}"
1303
1305
  patch_dir = None
1304
1306
  commit_created = False
1305
1307
  tag_pushed = False
1306
1308
  modifications_started = False # Track if we started making changes
1307
1309
  branch_name = f"ho-patch/{normalized_id}"
1308
1310
  try:
1309
- # Step 5: ACQUIRE LOCK on ho-release/X.Y.Z (with 30 min timeout for stale locks)
1310
- lock_tag = self._repo.hgit.acquire_branch_lock(release_branch, timeout_minutes=30)
1311
-
1312
1311
  # Step 6: Fetch all references from remote (branches + tags) - with lock held
1313
1312
  self._fetch_from_remote()
1314
1313
 
@@ -1347,6 +1346,9 @@ class PatchManager:
1347
1346
  tag_pushed = True # Tag pushed = point of no return
1348
1347
  # ✅ If we reach here: patch number globally reserved!
1349
1348
 
1349
+ # Step 11b: Sync release files to ho-prod (non-critical)
1350
+ self._sync_release_files_to_ho_prod(release_version, release_branch, critical=False)
1351
+
1350
1352
  # === BRANCH CREATION (after reservation) ===
1351
1353
 
1352
1354
  # Step 11: Create branch FROM current commit (after tag push)
@@ -1363,13 +1365,7 @@ class PatchManager:
1363
1365
  click.echo(f"⚠️ Push branch manually: git push -u origin {branch_name}")
1364
1366
  # Don't raise - tag pushed means success
1365
1367
 
1366
- # Step 13: Checkout to new branch (non-critical, warn if fails)
1367
- try:
1368
- self._checkout_branch(branch_name)
1369
- except Exception as e:
1370
- import click
1371
- click.echo(f"⚠️ Checkout failed but patch created successfully")
1372
- click.echo(f"Run: git checkout {branch_name}")
1368
+ # Note: No need to checkout - already on branch after _create_git_branch()
1373
1369
 
1374
1370
  except Exception as e:
1375
1371
  # Only rollback if tag NOT pushed yet AND modifications started
@@ -1385,10 +1381,6 @@ class PatchManager:
1385
1381
  )
1386
1382
  raise PatchManagerError(f"Patch creation failed: {e}")
1387
1383
 
1388
- finally:
1389
- # ALWAYS release lock (even on error)
1390
- self._repo.hgit.release_branch_lock(lock_tag)
1391
-
1392
1384
  # Return result
1393
1385
  return {
1394
1386
  'patch_id': normalized_id,
@@ -1398,6 +1390,80 @@ class PatchManager:
1398
1390
  'version': release_version # NEW: include release version
1399
1391
  }
1400
1392
 
1393
+ def _sync_release_files_to_ho_prod(self, version: str, release_branch: str, critical: bool = False) -> None:
1394
+ """
1395
+ Sync release files for a specific version to ho-prod.
1396
+
1397
+ This ensures that ho-prod always has the latest state of release metadata
1398
+ files, making it the single source of truth for release information.
1399
+
1400
+ Args:
1401
+ version: Release version (e.g., "0.17.0")
1402
+ release_branch: Source release branch (e.g., "ho-release/0.17.0")
1403
+ critical: If True, raise exception on failure. If False, just warn.
1404
+
1405
+ Raises:
1406
+ PatchManagerError: If critical=True and sync fails
1407
+
1408
+ Examples:
1409
+ # After modifying candidates.txt on ho-release/0.17.0
1410
+ self._sync_release_files_to_ho_prod("0.17.0", "ho-release/0.17.0")
1411
+ """
1412
+ try:
1413
+ # Save current branch
1414
+ current_branch = release_branch
1415
+
1416
+ # Checkout ho-prod
1417
+ self._repo.hgit.checkout("ho-prod")
1418
+
1419
+ # Copy ALL files for this specific version from release branch
1420
+ # This includes: -candidates.txt, -stage.txt, -rc*.txt, .txt, -hotfix*.txt
1421
+ # Using wildcard pattern to get all files matching the version
1422
+ self._repo.hgit.checkout_paths_from_branch(release_branch, [f"releases/{version}*.txt"])
1423
+
1424
+ # Commit on ho-prod
1425
+ self._repo.hgit.commit("-m", f"[HOP] sync release {version} files from {release_branch}")
1426
+ self._repo.hgit.push_branch("ho-prod")
1427
+
1428
+ # Return to release branch
1429
+ self._repo.hgit.checkout(current_branch)
1430
+ except Exception as e:
1431
+ # Try to return to release branch even on error
1432
+ try:
1433
+ self._repo.hgit.checkout(release_branch)
1434
+ except:
1435
+ pass
1436
+
1437
+ if critical:
1438
+ raise PatchManagerError(f"Failed to sync release files to ho-prod: {e}")
1439
+ else:
1440
+ # Non-critical - just warn
1441
+ import click
1442
+ click.echo(f"⚠️ Warning: Failed to sync release files to ho-prod: {e}")
1443
+ click.echo(f"⚠️ You may need to manually sync releases/ to ho-prod")
1444
+
1445
+ def _get_release_branch_for_patch(self, patch_id: str, *args, **kwargs) -> str:
1446
+ """
1447
+ Helper to determine release branch for close_patch lock.
1448
+
1449
+ Args:
1450
+ patch_id: Patch identifier
1451
+
1452
+ Returns:
1453
+ Release branch name (e.g., "ho-release/0.17.0")
1454
+
1455
+ Raises:
1456
+ PatchManagerError: If patch not found in candidates
1457
+ """
1458
+ version = self._find_version_for_candidate(patch_id)
1459
+ if not version:
1460
+ raise PatchManagerError(
1461
+ f"Patch {patch_id} not found in any candidates file.\n"
1462
+ f"Available candidates:\n{self._list_all_candidates()}"
1463
+ )
1464
+ return f"ho-release/{version}"
1465
+
1466
+ @with_dynamic_branch_lock(lambda self, patch_id: "ho-prod")
1401
1467
  def close_patch(self, patch_id: str) -> dict:
1402
1468
  """
1403
1469
  Close a patch by merging it into the release branch.
@@ -1463,7 +1529,7 @@ class PatchManager:
1463
1529
 
1464
1530
  # 5. Merge patch branch into release branch
1465
1531
  try:
1466
- self._repo.hgit.merge(patch_branch, message=f"[HOP] Merge #{patch_id} into %{version}")
1532
+ self._repo.hgit.merge(patch_branch, message=f'''[HOP] Merge #{patch_id} into %"{version}"''')
1467
1533
  except Exception as e:
1468
1534
  raise PatchManagerError(
1469
1535
  f"Failed to merge {patch_branch} into {release_branch}: {e}\n"
@@ -1473,13 +1539,16 @@ class PatchManager:
1473
1539
  # 6. Move from candidates to stage
1474
1540
  self._move_patch_to_stage(patch_id, version)
1475
1541
 
1476
- # 7. Commit changes
1542
+ # 7. Commit changes on release branch
1477
1543
  try:
1478
1544
  self._repo.hgit.commit("-m", f"[HOP] move patch #{patch_id} from candidate to stage %{version}")
1479
1545
  self._repo.hgit.push_branch(release_branch)
1480
1546
  except Exception as e:
1481
1547
  raise PatchManagerError(f"Failed to commit/push changes: {e}")
1482
1548
 
1549
+ # 7b. Sync release files to ho-prod (metadata only, not code)
1550
+ self._sync_release_files_to_ho_prod(version, release_branch, critical=True)
1551
+
1483
1552
  # 8. Delete patch branch (local and remote)
1484
1553
  try:
1485
1554
  self._repo.hgit.delete_local_branch(patch_branch)
@@ -1638,14 +1707,14 @@ class PatchManager:
1638
1707
  to allow patches to see each other's changes and support dependencies.
1639
1708
 
1640
1709
  Returns:
1641
- Version string extracted from branch name (e.g., "0.17.0")
1710
+ branch name (e.g., "ho-release/0.17.0")
1642
1711
 
1643
1712
  Raises:
1644
1713
  PatchManagerError: If not on ho-release/* branch
1645
1714
 
1646
1715
  Examples:
1647
- version = self._validate_on_ho_release()
1648
- # On ho-release/0.17.0 → returns "0.17.0"
1716
+ branch_name = self._validate_on_ho_release()
1717
+ # On ho-release/X.Y.Z → returns "ho-release/X.Y.Z"
1649
1718
  # On main → raises PatchManagerError
1650
1719
  """
1651
1720
  current_branch = self._repo.hgit.branch
@@ -1656,9 +1725,7 @@ class PatchManager:
1656
1725
  f"Hint: Run 'half_orm dev release new <level>' first to create a release"
1657
1726
  )
1658
1727
 
1659
- # Extract version from branch name (ho-release/X.Y.Z → X.Y.Z)
1660
- version = current_branch.replace("ho-release/", "")
1661
- return version
1728
+ return current_branch
1662
1729
 
1663
1730
  def _validate_on_ho_prod(self) -> None:
1664
1731
  """
@@ -16,7 +16,7 @@ from typing import Optional, Tuple, List, Dict
16
16
  from dataclasses import dataclass
17
17
 
18
18
  from git.exc import GitCommandError
19
- from half_orm_dev.decorators import with_ho_prod_lock
19
+ from half_orm_dev.decorators import with_dynamic_branch_lock
20
20
 
21
21
  class ReleaseManagerError(Exception):
22
22
  """Base exception for ReleaseManager operations."""
@@ -2285,7 +2285,7 @@ class ReleaseManager:
2285
2285
  version_tags.sort(key=version_key, reverse=True)
2286
2286
  return version_tags[0]
2287
2287
 
2288
- @with_ho_prod_lock()
2288
+ @with_dynamic_branch_lock(lambda self, level: "ho-prod")
2289
2289
  def new_release(self, level: str) -> dict:
2290
2290
  """
2291
2291
  Create a new release with integration branch.
@@ -2380,7 +2380,7 @@ class ReleaseManager:
2380
2380
  'stage_file': str(stage_file)
2381
2381
  }
2382
2382
 
2383
- @with_ho_prod_lock()
2383
+ @with_dynamic_branch_lock(lambda self, patch_id, version: "ho-prod")
2384
2384
  def add_patch_to_release(self, patch_id: str, version: str) -> dict:
2385
2385
  """
2386
2386
  Add a patch to a release by merging into the release branch.
@@ -2526,8 +2526,8 @@ class ReleaseManager:
2526
2526
  stage_files.sort(key=version_key)
2527
2527
  return stage_files[0].stem.replace('-stage', '')
2528
2528
 
2529
- @with_ho_prod_lock()
2530
- def promote_to_rc(self, version: str = None) -> dict:
2529
+ @with_dynamic_branch_lock(lambda self: "ho-prod")
2530
+ def promote_to_rc(self) -> dict:
2531
2531
  """
2532
2532
  Promote a stage release to RC by tagging the release branch.
2533
2533
 
@@ -2548,7 +2548,7 @@ class ReleaseManager:
2548
2548
  ReleaseManagerError: If promotion fails
2549
2549
 
2550
2550
  Examples:
2551
- rel_mgr.promote_to_rc("0.1.0")
2551
+ rel_mgr.promote_to_rc()
2552
2552
  # → Creates tag "0.1.0-rcN" on ho-release/0.1.0
2553
2553
  # → Renames 0.1.0-stage.txt to 0.1.0-rcN.txt
2554
2554
 
@@ -2556,8 +2556,7 @@ class ReleaseManager:
2556
2556
  # → Promotes the smallest stage release
2557
2557
  """
2558
2558
  # Auto-detect version if not provided
2559
- if version is None:
2560
- version = self._detect_version_to_promote('rc')
2559
+ version = self._detect_version_to_promote('rc')
2561
2560
 
2562
2561
  stage_file = self._releases_dir / f"{version}-stage.txt"
2563
2562
  if not stage_file.exists():
@@ -2597,6 +2596,13 @@ class ReleaseManager:
2597
2596
  self._repo.hgit.commit("-m", f"[HOP] Promote release %{version} to RC {rc_number}")
2598
2597
  self._repo.hgit.push_branch(release_branch)
2599
2598
 
2599
+ # Sync RC and stage files to ho-prod (single source of truth)
2600
+ self._repo.patch_manager._sync_release_files_to_ho_prod(
2601
+ version,
2602
+ release_branch,
2603
+ critical=False
2604
+ )
2605
+
2600
2606
  return {
2601
2607
  'version': version,
2602
2608
  'tag': rc_tag,
@@ -2606,8 +2612,8 @@ class ReleaseManager:
2606
2612
  except Exception as e:
2607
2613
  raise ReleaseManagerError(f"Failed to promote to RC: {e}")
2608
2614
 
2609
- @with_ho_prod_lock()
2610
- def promote_to_prod(self, version: str = None) -> dict:
2615
+ @with_dynamic_branch_lock(lambda self: "ho-prod")
2616
+ def promote_to_prod(self) -> dict:
2611
2617
  """
2612
2618
  Promote stage release to production.
2613
2619
 
@@ -2635,7 +2641,7 @@ class ReleaseManager:
2635
2641
  ReleaseManagerError: If promotion fails
2636
2642
 
2637
2643
  Examples:
2638
- rel_mgr.promote_to_prod("0.1.0")
2644
+ rel_mgr.promote_to_prod()
2639
2645
  # → Merges ho-release/0.1.0 into ho-prod
2640
2646
  # → Creates tag "0.1.0"
2641
2647
  # → Deletes candidates.txt, renames stage.txt to 0.1.0.txt
@@ -2644,9 +2650,8 @@ class ReleaseManager:
2644
2650
  rel_mgr.promote_to_prod() # Auto-detect version
2645
2651
  # → Promotes the smallest RC release
2646
2652
  """
2647
- # Auto-detect version if not provided
2648
- if version is None:
2649
- version = self._detect_version_to_promote('prod')
2653
+ # Auto-detect version
2654
+ version = self._detect_version_to_promote('prod')
2650
2655
 
2651
2656
  stage_file = self._releases_dir / f"{version}-stage.txt"
2652
2657
  if not stage_file.exists():
@@ -2702,10 +2707,31 @@ class ReleaseManager:
2702
2707
  "ho-prod has been restored to its previous state."
2703
2708
  )
2704
2709
 
2705
- # 3. Apply patches and generate schema
2706
- self._apply_release_patches(version)
2710
+ # 3. Apply only NEW patches from stage (if any) and generate schema
2711
+ # Note: RC patches have already been applied during promote_to_rc
2712
+ stage_patches = self.read_release_patches(f"{version}-stage.txt")
2713
+
2714
+ if stage_patches:
2715
+ # There are new patches after RC - apply them
2716
+ # First, restore from latest RC schema
2717
+ latest_rc = self._get_latest_rc_number(version)
2718
+ rc_schema_version = f"{version}-rc{latest_rc}" if latest_rc > 0 else "0.0.0"
2719
+
2720
+ # Restore from RC schema if it exists, otherwise from baseline
2721
+ try:
2722
+ self._repo.restore_database_from_schema(rc_schema_version)
2723
+ except:
2724
+ self._repo.restore_database_from_schema()
2725
+
2726
+ # Apply new stage patches
2727
+ for patch_id in stage_patches:
2728
+ self._repo.patch_manager.apply_patch_files(patch_id, self._repo.model)
2729
+ else:
2730
+ # No new patches - just restore from latest RC
2731
+ # The database should already be in the correct state from RC
2732
+ pass
2707
2733
 
2708
- # Generate schema dump for this version
2734
+ # Generate schema dump for this production version
2709
2735
  model_dir = Path(self._repo.base_dir) / "model"
2710
2736
  self._repo.database._generate_schema_sql(version, model_dir)
2711
2737
 
@@ -2761,6 +2787,39 @@ class ReleaseManager:
2761
2787
  except Exception as e:
2762
2788
  raise ReleaseManagerError(f"Failed to promote to production: {e}")
2763
2789
 
2790
+ def _get_latest_rc_number(self, version: str) -> int:
2791
+ """
2792
+ Get the latest (highest) RC number for a version.
2793
+
2794
+ Args:
2795
+ version: Version string (e.g., "1.3.5")
2796
+
2797
+ Returns:
2798
+ Latest RC number (0 if no RCs exist, 1 for rc1, 2 for rc2, etc.)
2799
+
2800
+ Examples:
2801
+ # No RCs
2802
+ latest = mgr._get_latest_rc_number("1.3.5")
2803
+ # → 0
2804
+
2805
+ # rc1, rc2 exist
2806
+ latest = mgr._get_latest_rc_number("1.3.5")
2807
+ # → 2
2808
+ """
2809
+ rc_files = self.get_rc_files(version)
2810
+ if not rc_files:
2811
+ return 0
2812
+
2813
+ # Extract RC numbers from filenames
2814
+ rc_numbers = []
2815
+ for rc_file in rc_files:
2816
+ # Format: "1.3.5-rc2.txt" → extract "2"
2817
+ match = re.search(r'-rc(\d+)\.txt$', rc_file.name)
2818
+ if match:
2819
+ rc_numbers.append(int(match.group(1)))
2820
+
2821
+ return max(rc_numbers) if rc_numbers else 0
2822
+
2764
2823
  def _determine_rc_number(self, version: str) -> int:
2765
2824
  """
2766
2825
  Determine next RC number for version.
@@ -2812,8 +2871,8 @@ class ReleaseManager:
2812
2871
  # Fallback (shouldn't happen with valid RC files)
2813
2872
  return len(rc_files) + 1
2814
2873
 
2815
- @with_ho_prod_lock()
2816
- def reopen_for_hotfix(self, version: str = None) -> dict:
2874
+ @with_dynamic_branch_lock(lambda self: "ho-prod")
2875
+ def reopen_for_hotfix(self) -> dict:
2817
2876
  """
2818
2877
  Reopen a production version for hotfix development.
2819
2878
 
@@ -2855,10 +2914,8 @@ class ReleaseManager:
2855
2914
  8. Switch to branch
2856
2915
  """
2857
2916
  try:
2858
- # 1. Determine version to reopen
2859
- if version is None:
2860
- # Get current production version from model/schema.sql
2861
- version = self._get_production_version()
2917
+ # Get current production version from model/schema.sql
2918
+ version = self._get_production_version()
2862
2919
 
2863
2920
  # Validate version format
2864
2921
  if not re.match(r'^\d+\.\d+\.\d+$', version):
@@ -2936,6 +2993,7 @@ class ReleaseManager:
2936
2993
  except Exception as e:
2937
2994
  raise ReleaseManagerError(f"Failed to reopen version for hotfix: {e}")
2938
2995
 
2996
+ @with_dynamic_branch_lock(lambda self: "ho-prod")
2939
2997
  def promote_to_hotfix(self) -> dict:
2940
2998
  """
2941
2999
  Promote hotfix release to production with hotfix tag.
@@ -2943,14 +3001,16 @@ class ReleaseManager:
2943
3001
  Similar to promote_to_prod() but:
2944
3002
  - Works from ho-release/X.Y.Z branch (not ho-prod)
2945
3003
  - Creates hotfix tag vX.Y.Z-hotfixN (not vX.Y.Z)
3004
+ - Renames stage.txt to X.Y.Z-hotfixN.txt
2946
3005
  - Merges ho-release/X.Y.Z into ho-prod
2947
- - Keeps ho-release/X.Y.Z branch for potential additional hotfixes
3006
+ - Deletes ho-release/X.Y.Z branch after promotion
2948
3007
 
2949
3008
  Returns:
2950
3009
  dict with:
2951
3010
  - version: Base version (e.g., "1.3.5")
2952
3011
  - hotfix_tag: Tag created (e.g., "v1.3.5-hotfix1")
2953
3012
  - branch: Branch used (e.g., "ho-release/1.3.5")
3013
+ - deleted_branches: List of deleted branches
2954
3014
 
2955
3015
  Raises:
2956
3016
  ReleaseManagerError: If validation fails or promotion errors occur
@@ -2960,9 +3020,12 @@ class ReleaseManager:
2960
3020
  2. Verify candidates.txt is empty
2961
3021
  3. Determine next hotfix number
2962
3022
  4. Merge ho-release/X.Y.Z into ho-prod
2963
- 5. Generate SQL dumps on ho-prod
2964
- 6. Create hotfix tag vX.Y.Z-hotfixN on ho-prod
2965
- 7. Return to ho-release/X.Y.Z
3023
+ 5. Rename stage.txt to X.Y.Z-hotfixN.txt and delete candidates.txt
3024
+ 6. Apply release patches and generate SQL dumps on ho-prod
3025
+ 7. Commit release file changes
3026
+ 8. Create hotfix tag vX.Y.Z-hotfixN on ho-prod
3027
+ 9. Push ho-prod
3028
+ 10. Delete ho-release/X.Y.Z branch
2966
3029
  """
2967
3030
  try:
2968
3031
  # 1. Verify on ho-release/* branch
@@ -3009,19 +3072,37 @@ class ReleaseManager:
3009
3072
  merge_msg = f"[release] Merge hotfix %{version}-hotfix{hotfix_num}"
3010
3073
  self._repo.hgit.merge(current_branch, message=merge_msg)
3011
3074
 
3012
- # 5. Apply release patches and generate SQL dumps
3075
+ # 5. Rename stage file to hotfix file and delete candidates
3076
+ stage_file = self._releases_dir / f"{version}-stage.txt"
3077
+ hotfix_file = self._releases_dir / f"{version}-hotfix{hotfix_num}.txt"
3078
+ candidates_file = self._releases_dir / f"{version}-candidates.txt"
3079
+
3080
+ if stage_file.exists():
3081
+ stage_file.rename(hotfix_file)
3082
+ self._repo.hgit.add(str(stage_file)) # Old path (deleted)
3083
+ self._repo.hgit.add(str(hotfix_file)) # New path (created)
3084
+
3085
+ # Delete candidates file if it exists
3086
+ if candidates_file.exists():
3087
+ candidates_file.unlink()
3088
+ self._repo.hgit.add(str(candidates_file)) # Mark as deleted
3089
+
3090
+ # 6. Apply release patches and generate SQL dumps
3013
3091
  self._apply_release_patches(version, True)
3014
3092
 
3015
- # 6. Create hotfix tag on ho-prod
3093
+ # 7. Commit release file changes
3094
+ self._repo.hgit.commit("-m", f"[HOP] Finalize hotfix %{version}-hotfix{hotfix_num} release files")
3095
+
3096
+ # 8. Create hotfix tag on ho-prod
3016
3097
  self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}-hotfix{hotfix_num}")
3017
3098
  self._repo.hgit.push_tag(hotfix_tag)
3018
3099
 
3019
- # 7. Push ho-prod
3100
+ # 9. Push ho-prod
3020
3101
  self._repo.hgit.push_branch("ho-prod")
3021
3102
 
3022
3103
  deleted_branches = []
3023
3104
 
3024
- # 7. Delete release branch (force=True because Git may not recognize the merge)
3105
+ # 10. Delete release branch (force=True because Git may not recognize the merge)
3025
3106
  try:
3026
3107
  self._repo.hgit.delete_branch(current_branch, force=True)
3027
3108
  self._repo.hgit.delete_remote_branch(current_branch)
@@ -773,34 +773,59 @@ class Repo:
773
773
 
774
774
  # 2. Get active branches status and release files
775
775
  try:
776
- # Find release files (candidates and stage)
776
+ # Find release files (candidates and stage) from ho-prod
777
777
  releases_info = {}
778
778
  if hasattr(self, 'release_manager'):
779
- releases_dir = Path(self.__base_dir) / 'releases'
780
- if releases_dir.exists():
781
- # Group files by version
782
- from collections import defaultdict
783
- by_version = defaultdict(dict)
784
-
785
- for candidates_file in releases_dir.glob('*-candidates.txt'):
786
- version = candidates_file.stem.replace('-candidates', '')
787
- by_version[version]['candidates_file'] = str(candidates_file)
788
- # Read candidates
789
- candidates_content = candidates_file.read_text(encoding='utf-8').strip()
790
- by_version[version]['candidates'] = [
791
- c.strip() for c in candidates_content.split('\n') if c.strip()
792
- ]
793
-
794
- for stage_file in releases_dir.glob('*-stage.txt'):
795
- version = stage_file.stem.replace('-stage', '')
796
- by_version[version]['stage_file'] = str(stage_file)
797
- # Read staged patches
798
- stage_content = stage_file.read_text(encoding='utf-8').strip()
799
- by_version[version]['staged'] = [
800
- s.strip() for s in stage_content.split('\n') if s.strip()
801
- ]
802
-
803
- releases_info = dict(by_version)
779
+ # Save current branch
780
+ try:
781
+ current_branch = str(self.hgit._HGit__git_repo.active_branch)
782
+ except:
783
+ current_branch = None
784
+
785
+ # Switch to ho-prod to read release files (source of truth)
786
+ try:
787
+ self.hgit.checkout('ho-prod')
788
+
789
+ # Pull latest changes from origin to ensure we have up-to-date metadata
790
+ try:
791
+ self.hgit.pull()
792
+ except Exception:
793
+ pass # Best effort - may fail if no remote or no changes
794
+
795
+ releases_dir = Path(self.__base_dir) / 'releases'
796
+ if releases_dir.exists():
797
+ # Group files by version
798
+ from collections import defaultdict
799
+ by_version = defaultdict(dict)
800
+
801
+ for candidates_file in releases_dir.glob('*-candidates.txt'):
802
+ version = candidates_file.stem.replace('-candidates', '')
803
+ by_version[version]['candidates_file'] = str(candidates_file)
804
+ # Read candidates (skip comments)
805
+ candidates_content = candidates_file.read_text(encoding='utf-8').strip()
806
+ by_version[version]['candidates'] = [
807
+ c.strip() for c in candidates_content.split('\n')
808
+ if c.strip() and not c.startswith('#')
809
+ ]
810
+
811
+ for stage_file in releases_dir.glob('*-stage.txt'):
812
+ version = stage_file.stem.replace('-stage', '')
813
+ by_version[version]['stage_file'] = str(stage_file)
814
+ # Read staged patches (skip comments)
815
+ stage_content = stage_file.read_text(encoding='utf-8').strip()
816
+ by_version[version]['staged'] = [
817
+ s.strip() for s in stage_content.split('\n')
818
+ if s.strip() and not s.startswith('#')
819
+ ]
820
+
821
+ releases_info = dict(by_version)
822
+ finally:
823
+ # Return to original branch
824
+ if current_branch:
825
+ try:
826
+ self.hgit.checkout(current_branch)
827
+ except:
828
+ pass # Best effort
804
829
 
805
830
  result['active_branches'] = self.hgit.get_active_branches_status(
806
831
  stage_files=[info.get('stage_file') for info in releases_info.values()
@@ -1798,16 +1823,3 @@ See docs/half_orm_dev.md for complete documentation.
1798
1823
  raise RepoError(
1799
1824
  f"Failed to restore database from schema: {e}"
1800
1825
  ) from e
1801
-
1802
- def temp_lock(func):
1803
- def wrapper(*args, **kwargs):
1804
- repo = Repo()
1805
- lock_tag = None
1806
- try:
1807
- lock_tag = repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
1808
- result = func(*args, **kwargs)
1809
- result['lock_tag'] = lock_tag
1810
- finally:
1811
- repo.hgit.release_branch_lock(lock_tag)
1812
- return result
1813
- return wrapper
@@ -47,16 +47,3 @@ def resolve_database_config_name(base_dir):
47
47
 
48
48
  # Priority 3: directory name
49
49
  return base_path.name
50
-
51
- def lock_tag(repo: 'Repo'):
52
- def temp_lock(func):
53
- def wrapper(*args, **kwargs):
54
- lock_tag = None
55
- try:
56
- lock_tag = repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
57
- result = func(*args, **kwargs)
58
- finally:
59
- if lock_tag:
60
- repo.hgit.release_branch_lock(lock_tag)
61
- return result
62
- return wrapper
@@ -0,0 +1 @@
1
+ 0.17.0-a10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.0a7
3
+ Version: 0.17.0a10
4
4
  Summary: half_orm development Framework.
5
5
  Home-page: https://github.com/collorg/halfORM_dev
6
6
  Author: Joël Maïzi
@@ -1 +0,0 @@
1
- 0.17.0-a7