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.
- {half_orm_dev-0.17.5a2/half_orm_dev.egg-info → half_orm_dev-0.17.5a3}/PKG-INFO +1 -1
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/check.py +8 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/patch.py +85 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/release.py +64 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patch_manager.py +146 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/release_manager.py +303 -240
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/repo.py +14 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/git-hooks/pre-commit +3 -2
- half_orm_dev-0.17.5a3/half_orm_dev/version.txt +1 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3/half_orm_dev.egg-info}/PKG-INFO +1 -1
- half_orm_dev-0.17.5a2/half_orm_dev/version.txt +0 -1
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/AUTHORS +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/LICENSE +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/README.md +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/bootstrap_manager.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/__init__.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli/main.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/database.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/hgit.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/migration_manager.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patch_validator.py +0 -0
- {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
- {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
- {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
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/SOURCES.txt +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/pyproject.toml +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/setup.cfg +0 -0
- {half_orm_dev-0.17.5a2 → half_orm_dev-0.17.5a3}/setup.py +0 -0
|
@@ -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).
|