half-orm-dev 0.17.5a2__tar.gz → 0.17.5a3__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 (72) hide show
  1. {half_orm_dev-0.17.5a2/half_orm_dev.egg-info → half_orm_dev-0.17.5a3}/PKG-INFO +1 -1
  2. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/check.py +8 -0
  3. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/patch.py +85 -0
  4. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/release.py +64 -0
  5. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patch_manager.py +146 -0
  6. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/release_manager.py +303 -240
  7. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/repo.py +14 -0
  8. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/git-hooks/pre-commit +3 -2
  9. half_orm_dev-0.17.5a3/half_orm_dev/version.txt +1 -0
  10. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3/half_orm_dev.egg-info}/PKG-INFO +1 -1
  11. half_orm_dev-0.17.5a2/half_orm_dev/version.txt +0 -1
  12. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/AUTHORS +0 -0
  13. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/LICENSE +0 -0
  14. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/README.md +0 -0
  15. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/__init__.py +0 -0
  16. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/bootstrap_manager.py +0 -0
  17. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/__init__.py +0 -0
  18. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/__init__.py +0 -0
  19. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/apply.py +0 -0
  20. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  21. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/clone.py +0 -0
  22. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/init.py +0 -0
  23. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/migrate.py +0 -0
  24. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/restore.py +0 -0
  25. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/sync.py +0 -0
  26. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/todo.py +0 -0
  27. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/undo.py +0 -0
  28. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/update.py +0 -0
  29. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/upgrade.py +0 -0
  30. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/main.py +0 -0
  31. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli_extension.py +0 -0
  32. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/database.py +0 -0
  33. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/decorators.py +0 -0
  34. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/file_executor.py +0 -0
  35. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/hgit.py +0 -0
  36. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migration_manager.py +0 -0
  37. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  38. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  39. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  40. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  41. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  42. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  43. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/modules.py +0 -0
  44. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patch_validator.py +0 -0
  45. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  46. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  47. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  48. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/log +0 -0
  49. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  50. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/release_file.py +0 -0
  51. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/scripts/repair-metadata.py +0 -0
  52. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/.gitignore +0 -0
  53. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/MANIFEST.in +0 -0
  54. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/README +0 -0
  55. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/conftest_template +0 -0
  56. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  57. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/init_module_template +0 -0
  58. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_1 +0 -0
  59. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_2 +0 -0
  60. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_3 +0 -0
  61. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/pyproject.toml +0 -0
  62. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/relation_test +0 -0
  63. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/sql_adapter +0 -0
  64. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/warning +0 -0
  65. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/utils.py +0 -0
  66. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/SOURCES.txt +0 -0
  67. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  68. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/requires.txt +0 -0
  69. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/top_level.txt +0 -0
  70. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/pyproject.toml +0 -0
  71. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/setup.cfg +0 -0
  72. {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.5a2
3
+ Version: 0.17.5a3
4
4
  Summary: half_orm development Framework.
5
5
  Author-email: Joël Maïzi <joel.maizi@collorg.org>
6
6
  License-Expression: GPL-3.0-or-later
@@ -153,6 +153,14 @@ def _display_check_results(repo, result: dict, dry_run: bool, verbose: bool):
153
153
  elif verbose:
154
154
  click.echo(f"\n📦 {utils.Color.bold('Active releases:')} None")
155
155
 
156
+ # Show orphaned patches
157
+ orphaned_patches = result.get('orphaned_patches', [])
158
+ if orphaned_patches:
159
+ click.echo(f"\n🔧 {utils.Color.bold('Orphaned patches')} ({len(orphaned_patches)}):")
160
+ for patch_id in sorted(orphaned_patches):
161
+ click.echo(f" • {patch_id}")
162
+ click.echo(f" {utils.Color.blue('(Use \"half_orm dev release attach-patch <id>\" to reattach)')}")
163
+
156
164
  # Show standalone patch branches (not in candidates/stage)
157
165
  standalone_patches = [b for b in patch_branches
158
166
  if not _is_patch_in_releases(b['name'], releases_info)]
@@ -5,6 +5,7 @@ Groups all patch-related commands under 'half_orm dev patch':
5
5
  - patch create: Create new patch branch and directory
6
6
  - patch apply: Apply current patch files to database
7
7
  - patch merge: Add patch to stage release with validation
8
+ - patch detach: Detach a candidate patch from its release
8
9
 
9
10
  Replaces legacy commands:
10
11
  - create-patch → patch create
@@ -365,3 +366,87 @@ def patch_merge(force: bool) -> None:
365
366
 
366
367
  except PatchManagerError as e:
367
368
  raise click.ClickException(str(e))
369
+
370
+
371
+ @patch.command('detach')
372
+ @click.argument('patch_id', type=str, required=False)
373
+ @click.option('--force', '-f', is_flag=True, help='Skip confirmation prompt')
374
+ def patch_detach(patch_id: Optional[str], force: bool) -> None:
375
+ """
376
+ Detach a candidate patch from its release.
377
+
378
+ Moves the patch to Patches/orphaned/ directory.
379
+ The git branch is preserved for future reattachment.
380
+
381
+ If PATCH_ID is not provided, uses the current branch's patch.
382
+
383
+ Examples:
384
+ Detach current patch (from ho-patch/* branch):
385
+ $ half_orm dev patch detach
386
+
387
+ Detach specific patch:
388
+ $ half_orm dev patch detach 123-feature
389
+
390
+ Detach without confirmation:
391
+ $ half_orm dev patch detach --force
392
+ """
393
+ try:
394
+ repo = Repo()
395
+
396
+ # Auto-detect patch_id from current branch if not provided
397
+ if not patch_id:
398
+ branch = repo.hgit.branch
399
+ if not branch.startswith('ho-patch/'):
400
+ raise click.UsageError(
401
+ "Not on a patch branch. Provide PATCH_ID or checkout ho-patch/* branch."
402
+ )
403
+ patch_id = branch.replace('ho-patch/', '')
404
+
405
+ # Get patch info for confirmation
406
+ status_map = repo.patch_manager.get_patch_status_map()
407
+ if patch_id not in status_map:
408
+ click.echo(utils.Color.red(f"Patch '{patch_id}' not found"))
409
+ raise click.Abort()
410
+
411
+ patch_info = status_map[patch_id]
412
+ version = patch_info.get('version', 'unknown')
413
+ status = patch_info.get('status', 'unknown')
414
+
415
+ # Check if patch can be detached
416
+ if status == 'staged':
417
+ click.echo(utils.Color.red(
418
+ f"Cannot detach staged patch '{patch_id}'. "
419
+ "Only candidate patches can be detached."
420
+ ))
421
+ raise click.Abort()
422
+
423
+ if status == 'orphaned':
424
+ click.echo(utils.Color.red(f"Patch '{patch_id}' is already orphaned."))
425
+ raise click.Abort()
426
+
427
+ # Confirmation
428
+ if not force:
429
+ click.echo(f"Detaching patch '{utils.Color.bold(patch_id)}' from release {utils.Color.bold(version)}")
430
+ click.echo()
431
+ click.echo("This will:")
432
+ click.echo(f" • Remove patch from {version}-patches.toml")
433
+ click.echo(f" • Move directory to Patches/orphaned/{patch_id}/")
434
+ click.echo(f" • Keep git branch ho-patch/{patch_id}")
435
+ click.echo()
436
+ if not click.confirm("Continue?", default=True):
437
+ click.echo("Cancelled.")
438
+ return
439
+
440
+ # Execute detach
441
+ result = repo.patch_manager.detach_patch(patch_id)
442
+
443
+ click.echo()
444
+ click.echo(utils.Color.green(f"✓ Patch '{patch_id}' detached from release {version}"))
445
+ click.echo(f" Directory moved to: {result['orphaned_path']}")
446
+ click.echo(f" Branch preserved: ho-patch/{patch_id}")
447
+ click.echo()
448
+ click.echo("To reattach later: half_orm dev release attach-patch <patch_id>")
449
+
450
+ except PatchManagerError as e:
451
+ click.echo(utils.Color.red(f"Error: {e}"), err=True)
452
+ raise click.Abort()
@@ -4,6 +4,7 @@ Release command group - Unified release management.
4
4
  Groups all release-related commands under 'half_orm dev release':
5
5
  - release create: Prepare next release stage file
6
6
  - release promote: Promote stage to rc or production
7
+ - release attach-patch: Reattach orphaned patch to a release
7
8
  """
8
9
 
9
10
  import click
@@ -16,6 +17,7 @@ from half_orm_dev.release_manager import (
16
17
  ReleaseFileError,
17
18
  ReleaseVersionError
18
19
  )
20
+ from half_orm_dev.patch_manager import PatchManagerError
19
21
  from half_orm import utils
20
22
 
21
23
 
@@ -484,3 +486,65 @@ def release_apply(skip_tests: bool) -> None:
484
486
  click.echo(f"❌ {utils.Color.red('Release apply failed:')}", err=True)
485
487
  click.echo(f" {str(e)}", err=True)
486
488
  sys.exit(1)
489
+
490
+
491
+ @release.command('attach-patch')
492
+ @click.argument('patch_id', type=str)
493
+ @click.option('--force', '-f', is_flag=True, help='Skip confirmation prompt')
494
+ def release_attach_patch(patch_id: str, force: bool) -> None:
495
+ """
496
+ Reattach an orphaned patch to the current release.
497
+
498
+ Moves the patch from Patches/orphaned/ back to Patches/ and adds it
499
+ as a candidate in the release TOML file.
500
+
501
+ Must be run from a ho-release/X.Y.Z branch.
502
+
503
+ \b
504
+ EXAMPLES:
505
+ # Reattach an orphaned patch
506
+ half_orm dev release attach-patch 123-feature
507
+
508
+ # Skip confirmation
509
+ half_orm dev release attach-patch 123-feature --force
510
+ """
511
+ try:
512
+ repo = Repo()
513
+
514
+ # Must be on ho-release/* branch
515
+ current_branch = repo.hgit.branch
516
+ if not current_branch.startswith('ho-release/'):
517
+ click.echo(
518
+ f"❌ {utils.Color.red('Must be on ho-release/X.Y.Z branch.')}",
519
+ err=True
520
+ )
521
+ click.echo(
522
+ f" Current branch: {current_branch}",
523
+ err=True
524
+ )
525
+ sys.exit(1)
526
+
527
+ version = current_branch.replace('ho-release/', '')
528
+
529
+ # Confirmation
530
+ if not force:
531
+ click.echo(f"Attaching patch '{patch_id}' to release {version}")
532
+ click.echo("This will:")
533
+ click.echo(f" • Add patch to {version}-patches.toml as candidate")
534
+ click.echo(f" • Move directory from Patches/orphaned/{patch_id}/ to Patches/{patch_id}/")
535
+ if not click.confirm("Continue?", default=True):
536
+ click.echo("Cancelled.")
537
+ return
538
+
539
+ # Execute attach
540
+ result = repo.patch_manager.attach_patch(patch_id, version)
541
+
542
+ click.echo(utils.Color.green(
543
+ f"✓ Patch '{patch_id}' attached to release {version}"
544
+ ))
545
+ click.echo(f" Status: candidate")
546
+ click.echo(f" Directory: {result['patch_path']}")
547
+
548
+ except PatchManagerError as e:
549
+ click.echo(f"❌ {utils.Color.red(str(e))}", err=True)
550
+ sys.exit(1)
@@ -3005,6 +3005,152 @@ class PatchManager:
3005
3005
  f"Failed to move patch to staged: {e}"
3006
3006
  )
3007
3007
 
3008
+ def detach_patch(self, patch_id: str) -> dict:
3009
+ """
3010
+ Detach a candidate patch from its release (move to orphaned/).
3011
+
3012
+ Workflow:
3013
+ 1. Validate patch is candidate (not staged)
3014
+ 2. Remove patch from TOML file
3015
+ 3. Move directory to Patches/orphaned/
3016
+ 4. Keep git branch as-is (for future reattachment)
3017
+ 5. Update cache
3018
+ 6. Commit and sync changes
3019
+
3020
+ Args:
3021
+ patch_id: Patch identifier to detach
3022
+
3023
+ Returns:
3024
+ dict with 'patch_id', 'version', 'orphaned_path'
3025
+
3026
+ Raises:
3027
+ PatchManagerError: If patch not found, not candidate, or operation fails
3028
+ """
3029
+ # 1. Get patch status
3030
+ status_map = self.get_patch_status_map()
3031
+ if patch_id not in status_map:
3032
+ raise PatchManagerError(f"Patch '{patch_id}' not found")
3033
+
3034
+ patch_info = status_map[patch_id]
3035
+ status = patch_info.get("status")
3036
+ version = patch_info.get("version")
3037
+
3038
+ # 2. Validate: only candidates can be detached
3039
+ if status == "staged":
3040
+ raise PatchManagerError(
3041
+ f"Cannot detach staged patch '{patch_id}'. "
3042
+ "Only candidate patches can be detached."
3043
+ )
3044
+ if status == "orphaned":
3045
+ raise PatchManagerError(f"Patch '{patch_id}' is already orphaned.")
3046
+
3047
+ # 3. Remove from TOML
3048
+ release_file = ReleaseFile(version, self._releases_dir)
3049
+ release_file.remove_patch(patch_id)
3050
+ self._repo.hgit.add(str(release_file.file_path))
3051
+
3052
+ # 4. Move directory to orphaned/
3053
+ old_path = self.get_patch_directory_path(patch_id)
3054
+ orphaned_dir = self._schema_patches_dir / "orphaned"
3055
+ new_path = orphaned_dir / patch_id
3056
+
3057
+ if old_path.exists():
3058
+ orphaned_dir.mkdir(exist_ok=True)
3059
+ self._repo.hgit.mv(str(old_path), str(new_path))
3060
+
3061
+ # 5. Update cache
3062
+ self._update_patch_status_cache(patch_id, "orphaned")
3063
+
3064
+ # 6. Commit changes (include Patches/ directory in sync)
3065
+ self._repo.commit_and_sync_to_active_branches(
3066
+ message=f"[HOP] detach patch #{patch_id} from release %{version}",
3067
+ files=['Patches/']
3068
+ )
3069
+
3070
+ return {
3071
+ 'patch_id': patch_id,
3072
+ 'version': version,
3073
+ 'orphaned_path': str(new_path)
3074
+ }
3075
+
3076
+ def attach_patch(self, patch_id: str, version: str) -> dict:
3077
+ """
3078
+ Attach an orphaned patch to a release (move from orphaned/ to root).
3079
+
3080
+ Reverse of detach_patch(). Workflow:
3081
+ 1. Validate patch is orphaned
3082
+ 2. Validate target release exists (TOML file)
3083
+ 3. Add patch to TOML as candidate
3084
+ 4. Move directory from Patches/orphaned/ to Patches/
3085
+ 5. Update cache
3086
+ 6. Commit and sync changes
3087
+
3088
+ Args:
3089
+ patch_id: Patch identifier to attach
3090
+ version: Target release version (e.g., "0.1.0")
3091
+
3092
+ Returns:
3093
+ dict with 'patch_id', 'version', 'patch_path'
3094
+
3095
+ Raises:
3096
+ PatchManagerError: If patch not found, not orphaned, or release doesn't exist
3097
+ """
3098
+ # 1. Get patch status
3099
+ status_map = self.get_patch_status_map()
3100
+ if patch_id not in status_map:
3101
+ raise PatchManagerError(f"Patch '{patch_id}' not found")
3102
+
3103
+ patch_info = status_map[patch_id]
3104
+ status = patch_info.get("status")
3105
+
3106
+ # 2. Validate: only orphaned patches can be attached
3107
+ if status == "candidate":
3108
+ raise PatchManagerError(
3109
+ f"Patch '{patch_id}' is already a candidate in release "
3110
+ f"{patch_info.get('version')}."
3111
+ )
3112
+ if status == "staged":
3113
+ raise PatchManagerError(
3114
+ f"Cannot attach staged patch '{patch_id}'. "
3115
+ "Staged patches are already integrated."
3116
+ )
3117
+ if status != "orphaned":
3118
+ raise PatchManagerError(
3119
+ f"Patch '{patch_id}' has unexpected status '{status}'."
3120
+ )
3121
+
3122
+ # 3. Validate release exists and add to TOML
3123
+ release_file = ReleaseFile(version, self._releases_dir)
3124
+ if not release_file.file_path.exists():
3125
+ raise PatchManagerError(
3126
+ f"Release {version} not found. "
3127
+ f"No file {release_file.file_path.name} exists."
3128
+ )
3129
+ release_file.add_patch(patch_id)
3130
+ self._repo.hgit.add(str(release_file.file_path))
3131
+
3132
+ # 4. Move directory from orphaned/ to root
3133
+ orphaned_path = self._schema_patches_dir / "orphaned" / patch_id
3134
+ new_path = self._schema_patches_dir / patch_id
3135
+
3136
+ if orphaned_path.exists():
3137
+ self._repo.hgit.mv(str(orphaned_path), str(new_path))
3138
+
3139
+ # 5. Update cache
3140
+ self._update_patch_status_cache(patch_id, "candidate")
3141
+
3142
+ # 6. Commit changes (include Patches/ directory in sync)
3143
+ self._repo.commit_and_sync_to_active_branches(
3144
+ message=f"[HOP] attach patch #{patch_id} to release %{version}",
3145
+ files=['Patches/']
3146
+ )
3147
+
3148
+ return {
3149
+ 'patch_id': patch_id,
3150
+ 'version': version,
3151
+ 'patch_path': str(new_path)
3152
+ }
3153
+
3008
3154
  def _get_other_candidates(self, version: str, exclude_patch: str) -> List[str]:
3009
3155
  """
3010
3156
  Get list of other candidate patches for a release (excluding one).