half-orm-dev 1.0.0a18__tar.gz → 1.0.0a20__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 (80) hide show
  1. {half_orm_dev-1.0.0a18/half_orm_dev.egg-info → half_orm_dev-1.0.0a20}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/main.py +30 -13
  3. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/hgit.py +12 -2
  4. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patch_manager.py +1 -6
  5. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/release_manager.py +85 -273
  6. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/repo.py +47 -13
  7. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/.gitignore +3 -1
  8. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/git-hooks/pre-commit +12 -0
  9. half_orm_dev-1.0.0a20/half_orm_dev/templates/git-hooks/pre-push +18 -0
  10. half_orm_dev-1.0.0a20/half_orm_dev/templates/git-hooks/reference-transaction +34 -0
  11. half_orm_dev-1.0.0a20/half_orm_dev/version.txt +1 -0
  12. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20/half_orm_dev.egg-info}/PKG-INFO +1 -1
  13. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev.egg-info/SOURCES.txt +3 -1
  14. half_orm_dev-1.0.0a18/half_orm_dev/version.txt +0 -1
  15. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/AUTHORS +0 -0
  16. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/LICENSE +0 -0
  17. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/README.md +0 -0
  18. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/__init__.py +0 -0
  19. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/bootstrap_manager.py +0 -0
  20. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/__init__.py +0 -0
  21. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/__init__.py +0 -0
  22. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/apply.py +0 -0
  23. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  24. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/check.py +0 -0
  25. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/clone.py +0 -0
  26. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/init.py +0 -0
  27. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/migrate.py +0 -0
  28. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/patch.py +0 -0
  29. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/release.py +0 -0
  30. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/restore.py +0 -0
  31. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  32. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  33. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/sync.py +0 -0
  34. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/todo.py +0 -0
  35. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/undo.py +0 -0
  36. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/update.py +0 -0
  37. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli/commands/upgrade.py +0 -0
  38. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/cli_extension.py +0 -0
  39. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/database.py +0 -0
  40. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/decorators.py +0 -0
  41. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/file_executor.py +0 -0
  42. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migration_manager.py +0 -0
  43. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  44. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  45. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  46. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  47. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  48. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  49. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  50. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  51. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  52. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/modules.py +0 -0
  53. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patch_validator.py +0 -0
  54. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  55. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  56. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  57. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patches/log +0 -0
  58. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  59. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/release_file.py +0 -0
  60. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/scripts/repair-metadata.py +0 -0
  61. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/MANIFEST.in +0 -0
  62. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/README +0 -0
  63. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/conftest_template +0 -0
  64. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  65. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/init_module_template +0 -0
  66. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/module_template_1 +0 -0
  67. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/module_template_2 +0 -0
  68. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/module_template_3 +0 -0
  69. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/pyproject.toml +0 -0
  70. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/relation_test +0 -0
  71. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/sql_adapter +0 -0
  72. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/templates/warning +0 -0
  73. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev/utils.py +0 -0
  74. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  75. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev.egg-info/entry_points.txt +0 -0
  76. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev.egg-info/requires.txt +0 -0
  77. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/half_orm_dev.egg-info/top_level.txt +0 -0
  78. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/pyproject.toml +0 -0
  79. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/setup.cfg +0 -0
  80. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a20}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a18
3
+ Version: 1.0.0a20
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
@@ -4,6 +4,8 @@ Main CLI module - Creates and configures the CLI group
4
4
 
5
5
  import click
6
6
  import functools
7
+ import os
8
+ import subprocess
7
9
  import sys
8
10
  from half_orm_dev.repo import Repo, OutdatedHalfORMDevError
9
11
  from half_orm import utils
@@ -39,15 +41,16 @@ class Hop:
39
41
  but will be blocked by the decorator at execution time.
40
42
  """
41
43
  if self.needs_hop_upgrade:
42
- # Version downgrade detected - return a minimal set of commands
43
- # Commands will be blocked by decorator, but we need them in the list
44
- # so Click doesn't show "No such command" error
45
- return ['check', 'migrate']
44
+ return [] # handled at invocation time in the dev group callback
46
45
 
47
46
  if not self.repo_checked:
48
47
  # Outside hop repository - commands for project initialization
49
48
  return ['init', 'clone']
50
49
 
50
+ # PRODUCTION ENVIRONMENT — read-only, no migrations, no dev commands
51
+ if self.__repo.database.production:
52
+ return ['update', 'upgrade', 'bootstrap']
53
+
51
54
  if self.__repo.needs_migration():
52
55
  return ['migrate']
53
56
 
@@ -56,14 +59,9 @@ class Hop:
56
59
  # Sync-only mode (no metadata)
57
60
  return ['sync-package', 'check']
58
61
 
59
- # Development mode (metadata present)
60
- if self.__repo.database.production:
61
- # PRODUCTION ENVIRONMENT - Release deployment only
62
- return ['update', 'upgrade', 'bootstrap']
63
- else:
64
- # DEVELOPMENT ENVIRONMENT - Patch development
65
- return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
66
- 'revert-migration']
62
+ # DEVELOPMENT ENVIRONMENT - Patch development
63
+ return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
64
+ 'revert-migration']
67
65
 
68
66
  @property
69
67
  def repo_checked(self):
@@ -137,9 +135,28 @@ def create_cli_group():
137
135
 
138
136
  @click.group(cls=VersionCheckGroup, invoke_without_command=True)
139
137
  @click.pass_context
140
- @check_version_before_invoke
141
138
  def dev(ctx):
142
139
  """halfORM development tools - Git-centric patch management and database synchronization"""
140
+ if hop.needs_hop_upgrade:
141
+ error = hop.hop_upgrade_error
142
+ required = error.required_version
143
+ installed = error.installed_version
144
+ click.echo(f"\n Ce repo requiers half-orm-dev {required} "
145
+ f"(installed : {installed}).")
146
+ click.echo(f" Installing the required version...")
147
+ try:
148
+ subprocess.run(
149
+ [sys.executable, '-m', 'pip', 'install', f'half-orm-dev=={required}'],
150
+ check=True,
151
+ )
152
+ except subprocess.CalledProcessError:
153
+ click.echo(f"\n Installation failed.", err=True)
154
+ click.echo(f" Run manually : pip install half-orm-dev=={required}", err=True)
155
+ sys.exit(1)
156
+ click.echo(f" ✓ half-orm-dev {required} installed. Restarting...\n")
157
+ os.execv(sys.argv[0], sys.argv)
158
+ return
159
+
143
160
  if ctx.invoked_subcommand is None:
144
161
  # Show repo state when no subcommand is provided
145
162
  if hop.repo_checked:
@@ -367,7 +367,12 @@ class HGit:
367
367
  # Local git now knows about all remote tags
368
368
  """
369
369
  origin = self.__git_repo.remote('origin')
370
- origin.fetch(tags=True)
370
+ marker = Path(self.__git_repo.working_dir) / '.hop' / '.fetching'
371
+ try:
372
+ marker.touch()
373
+ origin.fetch(tags=True)
374
+ finally:
375
+ marker.unlink(missing_ok=True)
371
376
 
372
377
  def tag_exists(self, tag_name: str) -> bool:
373
378
  """
@@ -443,7 +448,12 @@ class HGit:
443
448
  # Stale remote refs (deleted branches on remote) are removed
444
449
  """
445
450
  origin = self.__git_repo.remote('origin')
446
- origin.fetch(prune=True)
451
+ marker = Path(self.__git_repo.working_dir) / '.hop' / '.fetching'
452
+ try:
453
+ marker.touch()
454
+ origin.fetch(prune=True)
455
+ finally:
456
+ marker.unlink(missing_ok=True)
447
457
 
448
458
  def delete_local_branch(self, branch_name: str) -> None:
449
459
  """
@@ -18,6 +18,7 @@ from typing import List, Dict, Optional, Tuple, Any
18
18
  from dataclasses import dataclass
19
19
  import click
20
20
  from git.exc import GitCommandError
21
+ from packaging.version import Version, InvalidVersion
21
22
 
22
23
  from half_orm import utils
23
24
  from half_orm_dev import modules
@@ -2146,9 +2147,6 @@ class PatchManager:
2146
2147
  - Commit the updated schema
2147
2148
  4. Return to original branch
2148
2149
  """
2149
- from packaging.version import Version
2150
- from half_orm_dev.release_file import ReleaseFile
2151
-
2152
2150
  original_branch = self._repo.hgit.branch
2153
2151
  current_ver = Version(version)
2154
2152
 
@@ -2197,9 +2195,6 @@ class PatchManager:
2197
2195
  Args:
2198
2196
  version: Current release version that was just updated
2199
2197
  """
2200
- from packaging.version import Version
2201
- from half_orm_dev.release_file import ReleaseFile
2202
-
2203
2198
  if not hasattr(self, '_pending_higher_releases') or not self._pending_higher_releases:
2204
2199
  return
2205
2200
 
@@ -13,16 +13,17 @@ import subprocess
13
13
 
14
14
  from pathlib import Path
15
15
  from typing import Optional, Tuple, List, Dict, Literal
16
- from dataclasses import dataclass
17
16
  from datetime import datetime, timezone
18
17
 
19
18
  import click
20
19
 
21
20
  from git.exc import GitCommandError
21
+ from packaging.version import Version, InvalidVersion
22
22
  from half_orm_dev.decorators import with_dynamic_branch_lock
23
23
  from half_orm import utils
24
24
  from half_orm_dev.release_file import ReleaseFile
25
25
 
26
+
26
27
  class ReleaseManagerError(Exception):
27
28
  """Base exception for ReleaseManager operations."""
28
29
  pass
@@ -38,69 +39,6 @@ class ReleaseFileError(ReleaseManagerError):
38
39
  pass
39
40
 
40
41
 
41
- @dataclass
42
- class Version:
43
- """Semantic version with stage information."""
44
- major: int
45
- minor: int
46
- patch: int
47
- stage: Optional[str] = None # None, "stage", "rc1", "rc2", "hotfix1", etc.
48
-
49
- def __str__(self) -> str:
50
- """String representation of version."""
51
- base = f"{self.major}.{self.minor}.{self.patch}"
52
- if self.stage:
53
- return f"{base}-{self.stage}"
54
- return base
55
-
56
- def __lt__(self, other: 'Version') -> bool:
57
- """Compare versions for sorting."""
58
- # Compare base version first
59
- if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
60
- return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
61
-
62
- # If base versions equal, compare stages
63
- # Priority: production (None) > rc > stage > hotfix
64
- stage_priority = {
65
- None: 4, # Production (highest)
66
- 'rc': 3, # Release candidate
67
- 'stage': 2, # Development stage
68
- 'hotfix': 1 # Hotfix (lowest)
69
- }
70
-
71
- # Extract stage type (rc1 → rc, hotfix2 → hotfix)
72
- self_stage_type = self._get_stage_type()
73
- other_stage_type = other._get_stage_type()
74
-
75
- self_priority = stage_priority.get(self_stage_type, 0)
76
- other_priority = stage_priority.get(other_stage_type, 0)
77
-
78
- # If different stage types, compare by priority
79
- if self_priority != other_priority:
80
- return self_priority < other_priority
81
-
82
- # Same stage type - compare stage strings for RC/hotfix numbers
83
- # rc2 > rc1, hotfix2 > hotfix1
84
- if self.stage and other.stage:
85
- return self.stage < other.stage
86
-
87
- return False
88
-
89
- def _get_stage_type(self) -> Optional[str]:
90
- """Extract stage type from stage string."""
91
- if not self.stage:
92
- return None
93
-
94
- if self.stage == 'stage':
95
- return 'stage'
96
- elif self.stage.startswith('rc'):
97
- return 'rc'
98
- elif self.stage.startswith('hotfix'):
99
- return 'hotfix'
100
-
101
- return None
102
-
103
-
104
42
  class ReleaseManager:
105
43
  """
106
44
  Manages release files and version lifecycle.
@@ -474,21 +412,16 @@ class ReleaseManager:
474
412
  if not release_files:
475
413
  return None
476
414
 
477
- # Parse all valid versions
478
415
  versions = []
479
- for release_file in release_files:
416
+ for f in release_files:
480
417
  try:
481
- version = self.parse_version_from_filename(release_file.name)
482
- versions.append(version)
483
- except ReleaseVersionError:
484
- # Ignore files with invalid format
418
+ versions.append(Version(f.stem))
419
+ except InvalidVersion:
485
420
  continue
486
421
 
487
422
  if not versions:
488
423
  return None
489
424
 
490
- # Sort versions and return latest
491
- # Version.__lt__ handles sorting with stage priority
492
425
  return max(versions)
493
426
 
494
427
 
@@ -559,83 +492,11 @@ class ReleaseManager:
559
492
  elif increment_type == 'minor':
560
493
  return f"{current_version.major}.{current_version.minor + 1}.0"
561
494
  elif increment_type == 'patch':
562
- return f"{current_version.major}.{current_version.minor}.{current_version.patch + 1}"
495
+ return f"{current_version.major}.{current_version.minor}.{current_version.micro + 1}"
563
496
 
564
497
  # Should never reach here due to validation above
565
498
  raise ReleaseVersionError(f"Unexpected increment type: {increment_type}")
566
499
 
567
- @classmethod
568
- def parse_version_from_filename(cls, filename: str) -> Version:
569
- """
570
- Parse version from release filename.
571
-
572
- Extracts semantic version and stage from release filename.
573
-
574
- Supported formats:
575
- - X.Y.Z.txt → Version(X, Y, Z, stage=None)
576
- - X.Y.Z-stage.txt → Version(X, Y, Z, stage="stage")
577
- - X.Y.Z-rc1.txt → Version(X, Y, Z, stage="rc1")
578
- - X.Y.Z-hotfix1.txt → Version(X, Y, Z, stage="hotfix1")
579
-
580
- Args:
581
- filename: Release filename (e.g., "1.3.5-rc2.txt")
582
-
583
- Returns:
584
- Version: Parsed version object
585
-
586
- Raises:
587
- ReleaseVersionError: If filename format invalid
588
-
589
- Examples:
590
- ver = release_mgr.parse_version_from_filename("1.3.5.txt")
591
- # Version(1, 3, 5, stage=None)
592
-
593
- ver = release_mgr.parse_version_from_filename("1.4.0-stage.txt")
594
- # Version(1, 4, 0, stage="stage")
595
-
596
- ver = release_mgr.parse_version_from_filename("1.3.5-rc2.txt")
597
- # Version(1, 3, 5, stage="rc2")
598
- """
599
- # Extract just filename if path provided
600
- filename = Path(filename).name
601
-
602
- # Validate not empty
603
- if not filename:
604
- raise ReleaseVersionError("Invalid format: empty filename")
605
-
606
- # Must end with .txt
607
- if not filename.endswith('.txt'):
608
- raise ReleaseVersionError(f"Invalid format: missing .txt extension in '{filename}'")
609
-
610
- # Remove .txt extension
611
- version_str = filename[:-4]
612
-
613
- # Pattern: X.Y.Z or X.Y.Z-stage or X.Y.Z-rc1 or X.Y.Z-hotfix1
614
- pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(stage|rc\d+|hotfix\d+))?$'
615
-
616
- match = re.match(pattern, version_str)
617
-
618
- if not match:
619
- raise ReleaseVersionError(
620
- f"Invalid format: '{filename}' does not match X.Y.Z[-stage].txt pattern"
621
- )
622
-
623
- major, minor, patch, stage = match.groups()
624
-
625
- # Convert to integers
626
- try:
627
- major = int(major)
628
- minor = int(minor)
629
- patch = int(patch)
630
- except ValueError:
631
- raise ReleaseVersionError(f"Invalid format: non-numeric version components in '{filename}'")
632
-
633
- # Validate non-negative
634
- if major < 0 or minor < 0 or patch < 0:
635
- raise ReleaseVersionError(f"Invalid format: negative version numbers in '{filename}'")
636
-
637
- return Version(major, minor, patch, stage)
638
-
639
500
  def get_next_release_version(self) -> Optional[str]:
640
501
  """
641
502
  Détermine LA prochaine release à déployer.
@@ -646,8 +507,7 @@ class ReleaseManager:
646
507
  production_str = self._get_production_version()
647
508
 
648
509
  for level in ['patch', 'minor', 'major']:
649
- next_version = self.calculate_next_version(
650
- self.parse_version_from_filename(f"{production_str}.txt"), level)
510
+ next_version = self.calculate_next_version(Version(production_str), level)
651
511
 
652
512
  # Cherche RC ou patches TOML pour cette version
653
513
  rc_pattern = f"{next_version}-rc*.txt"
@@ -662,12 +522,12 @@ class ReleaseManager:
662
522
  """
663
523
  Liste tous les fichiers <label> pour une version, triés par numéro.
664
524
 
665
- Returns:
666
- Liste triée (ex: ["1.3.6-rc1.txt", "1.3.6-rc2.txt"])
525
+ RC uses hyphen separator (1.3.6-rc1.txt).
526
+ Post-releases use dot separator (1.3.6.post1.txt).
667
527
  """
668
- pattern = f"{version}-{label}*.txt"
669
- reg_ex = rf'-{label}(\d+)\.txt$'
670
- label_pattern = re.compile(reg_ex)
528
+ sep = '.' if label == 'post' else '-'
529
+ pattern = f"{version}{sep}{label}*.txt"
530
+ label_pattern = re.compile(rf'[.-]{label}(\d+)\.txt$')
671
531
  files = list(self._releases_dir.glob(pattern))
672
532
 
673
533
  return sorted(files, key=lambda f: int(re.search(label_pattern, f.name).group(1)))
@@ -806,7 +666,7 @@ class ReleaseManager:
806
666
  else:
807
667
  # Production: read from hotfix snapshot if it exists
808
668
  # This handles the case where we're applying a hotfix release
809
- hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
669
+ hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
810
670
  if hotfix_files:
811
671
  # Apply the latest hotfix
812
672
  stage_patches = self.read_release_patches(hotfix_files[-1].name)
@@ -892,7 +752,7 @@ class ReleaseManager:
892
752
  all_patches.extend(self.read_release_patches(base_file))
893
753
 
894
754
  # 2. Hotfix patches in order
895
- hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
755
+ hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
896
756
  for hotfix_file in hotfix_files:
897
757
  all_patches.extend(self.read_release_patches(hotfix_file.name))
898
758
 
@@ -1372,6 +1232,52 @@ class ReleaseManager:
1372
1232
  # Best effort - don't fail if checkout back fails
1373
1233
  pass
1374
1234
 
1235
+ def ensure_ho_current(self) -> bool:
1236
+ """
1237
+ Set up ho-current branch for production servers if not already done.
1238
+
1239
+ Creates ho-current from the current production version's immutable tag
1240
+ and checks it out. Local-only, never pushed to origin.
1241
+
1242
+ Returns:
1243
+ True if ho-current was created and checked out, False otherwise.
1244
+
1245
+ Raises:
1246
+ ReleaseManagerError: If the required version tag is not found locally.
1247
+ """
1248
+ if not (
1249
+ self._repo.production
1250
+ and self._repo.hgit.branch == 'ho-prod'
1251
+ and not self._repo.hgit.branch_exists('ho-current')
1252
+ ):
1253
+ return False
1254
+
1255
+ try:
1256
+ current_version = self._repo.database.last_release_s
1257
+ except Exception as e:
1258
+ raise ReleaseManagerError(
1259
+ f"Cannot read current production version from database: {e}"
1260
+ )
1261
+
1262
+ current_tag = f'v{current_version}'
1263
+ tag_exists = any(
1264
+ t.name == current_tag
1265
+ for t in self._repo.hgit._HGit__git_repo.tags
1266
+ )
1267
+ if not tag_exists:
1268
+ raise ReleaseManagerError(
1269
+ f"Cannot set up ho-current: tag {current_tag} not found locally.\n"
1270
+ f"Run 'hop update' to fetch tags first."
1271
+ )
1272
+
1273
+ self._repo.hgit.create_branch_from_tag('ho-current', current_tag)
1274
+ self._repo.hgit._HGit__git_repo.heads['ho-current'].checkout()
1275
+ click.echo(
1276
+ f" ℹ ho-current branch created from {current_tag} (local only).\n"
1277
+ f" Production servers use ho-current instead of ho-prod."
1278
+ )
1279
+ return True
1280
+
1375
1281
  def update_production(self) -> dict:
1376
1282
  """
1377
1283
  Fetch tags and list available releases for production upgrade (read-only).
@@ -1430,30 +1336,8 @@ class ReleaseManager:
1430
1336
  f"Cannot read current production version from database: {e}"
1431
1337
  )
1432
1338
 
1433
- # Migration: production servers that still track ho-prod instead of
1434
- # ho-current. Create ho-current from the current version's immutable tag
1435
- # and switch to it — local-only, no push to origin.
1436
- if (
1437
- self._repo.production
1438
- and self._repo.hgit.branch == 'ho-prod'
1439
- and not self._repo.hgit.branch_exists('ho-current')
1440
- ):
1441
- current_tag = f'v{current_version}'
1442
- tag_exists = any(
1443
- t.name == current_tag
1444
- for t in self._repo.hgit._HGit__git_repo.tags
1445
- )
1446
- if not tag_exists:
1447
- raise ReleaseManagerError(
1448
- f"Cannot migrate to ho-current: tag {current_tag} not found locally.\n"
1449
- f"Run 'hop update' again after ensuring tags are fetched."
1450
- )
1451
- self._repo.hgit.create_branch_from_tag('ho-current', current_tag)
1452
- self._repo.hgit._HGit__git_repo.heads['ho-current'].checkout()
1453
- click.echo(
1454
- f" ℹ Migrated to ho-current (created from {current_tag}, local only).\n"
1455
- f" Production servers should now use ho-current instead of ho-prod."
1456
- )
1339
+ # Migration: production servers that still track ho-prod instead of ho-current.
1340
+ self.ensure_ho_current()
1457
1341
 
1458
1342
  # 3. Build list of available releases with details
1459
1343
  available_releases = []
@@ -1483,7 +1367,7 @@ class ReleaseManager:
1483
1367
  patches = [line.strip() for line in content.split('\n') if line.strip()]
1484
1368
 
1485
1369
  # Only include releases newer than current version
1486
- if self._version_is_newer(version, current_version):
1370
+ if Version(version) > Version(current_version):
1487
1371
  available_releases.append({
1488
1372
  'tag': tag,
1489
1373
  'version': version,
@@ -1550,43 +1434,19 @@ class ReleaseManager:
1550
1434
  except Exception as e:
1551
1435
  raise ReleaseManagerError(f"Failed to read tags from repository: {e}")
1552
1436
 
1553
- # Filter for release tags (v*.*.*) with optional -rc or -hotfix suffix
1554
- release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d+|-hotfix\d+)?$')
1437
+ # Filter for release tags: v1.3.5, v1.3.5-rc1, v1.3.5.post1
1438
+ release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d+|\.post\d+)?$')
1555
1439
  release_tags = []
1556
1440
 
1557
1441
  for tag in all_tags:
1558
1442
  tag_name = tag.name
1559
1443
  if release_pattern.match(tag_name):
1560
- # Filter RC tags unless explicitly allowed
1561
1444
  if '-rc' in tag_name and not allow_rc:
1562
1445
  continue
1563
1446
  release_tags.append(tag_name)
1564
1447
 
1565
- # Sort tags by version (semantic versioning)
1566
- def version_key(tag_name):
1567
- """Extract sortable version tuple from tag name."""
1568
- # Remove 'v' prefix
1569
- version_str = tag_name[1:]
1570
-
1571
- # Split version and suffix
1572
- if '-rc' in version_str:
1573
- base_ver, rc_suffix = version_str.split('-rc')
1574
- rc_num = int(rc_suffix)
1575
- suffix_weight = (1, rc_num) # RC comes before production
1576
- elif '-hotfix' in version_str:
1577
- base_ver, hotfix_suffix = version_str.split('-hotfix')
1578
- hotfix_num = int(hotfix_suffix)
1579
- suffix_weight = (2, hotfix_num) # Hotfix comes after production
1580
- else:
1581
- base_ver = version_str
1582
- suffix_weight = (1.5, 0) # Production between RC and hotfix
1583
-
1584
- # Parse base version
1585
- major, minor, patch = map(int, base_ver.split('.'))
1586
-
1587
- return (major, minor, patch, suffix_weight)
1588
-
1589
- release_tags.sort(key=version_key)
1448
+ # Sort using packaging.version (PEP 440 ordering)
1449
+ release_tags.sort(key=lambda t: Version(t[1:]))
1590
1450
 
1591
1451
  return release_tags
1592
1452
 
@@ -1621,77 +1481,31 @@ class ReleaseManager:
1621
1481
  path = mgr._calculate_upgrade_path("1.4.0", "1.4.0")
1622
1482
  # → []
1623
1483
  """
1624
- # Parse versions
1625
- current_version = self.parse_version_from_filename(f"{current}.txt")
1626
- target_version = self.parse_version_from_filename(f"{target}.txt")
1484
+ current_version = Version(current)
1485
+ target_version = Version(target)
1627
1486
 
1628
- # If same version, no upgrade needed
1629
1487
  if current == target:
1630
1488
  return []
1631
1489
 
1632
- # Get all available release tags (production only)
1633
1490
  available_tags = self._get_available_release_tags(allow_rc=False)
1634
1491
 
1635
- # Extract versions from tags and parse them
1636
1492
  available_versions = []
1637
1493
  for tag in available_tags:
1638
- # Remove 'v' prefix: v1.3.6 → 1.3.6
1639
1494
  version_str = tag[1:] if tag.startswith('v') else tag
1640
-
1641
- # Skip if not a valid production version format
1642
- if not re.match(r'^\d+\.\d+\.\d+$', version_str):
1643
- continue
1644
-
1645
1495
  try:
1646
- version = self.parse_version_from_filename(f"{version_str}.txt")
1647
- available_versions.append((version_str, version))
1648
- except (ReleaseManagerError, ValueError):
1496
+ available_versions.append((version_str, Version(version_str)))
1497
+ except InvalidVersion:
1649
1498
  continue
1650
1499
 
1651
- # Sort versions
1652
- available_versions.sort(key=lambda x: (x[1].major, x[1].minor, x[1].patch))
1500
+ available_versions.sort(key=lambda x: x[1])
1653
1501
 
1654
- # Build sequential path from current to target
1655
1502
  path = []
1656
1503
  for version_str, version in available_versions:
1657
- # Skip versions <= current
1658
- if (version.major, version.minor, version.patch) <= \
1659
- (current_version.major, current_version.minor, current_version.patch):
1660
- continue
1661
-
1662
- # Add versions <= target
1663
- if (version.major, version.minor, version.patch) <= \
1664
- (target_version.major, target_version.minor, target_version.patch):
1504
+ if current_version < version <= target_version:
1665
1505
  path.append(version_str)
1666
1506
 
1667
1507
  return path
1668
1508
 
1669
- def _version_is_newer(self, version1: str, version2: str) -> bool:
1670
- """
1671
- Compare two version strings to check if version1 is newer than version2.
1672
-
1673
- Args:
1674
- version1: First version (e.g., "1.3.6", "1.3.6-rc1")
1675
- version2: Second version (e.g., "1.3.5")
1676
-
1677
- Returns:
1678
- bool: True if version1 > version2
1679
-
1680
- Examples:
1681
- _version_is_newer("1.3.6", "1.3.5") # → True
1682
- _version_is_newer("1.3.5", "1.3.6") # → False
1683
- _version_is_newer("1.3.6-rc1", "1.3.5") # → True
1684
- """
1685
- # Extract base versions (remove suffix)
1686
- base1 = version1.split('-')[0]
1687
- base2 = version2.split('-')[0]
1688
-
1689
- # Parse versions
1690
- parts1 = tuple(map(int, base1.split('.')))
1691
- parts2 = tuple(map(int, base2.split('.')))
1692
-
1693
- return parts1 > parts2
1694
-
1695
1509
  def upgrade_production(
1696
1510
  self,
1697
1511
  to_version: Optional[str] = None,
@@ -2431,8 +2245,6 @@ class ReleaseManager:
2431
2245
  Returns:
2432
2246
  Version string of base release, or None if should use prod schema
2433
2247
  """
2434
- from packaging.version import Version
2435
-
2436
2248
  new_ver = Version(new_version)
2437
2249
  model_dir = Path(self._repo.model_dir)
2438
2250
 
@@ -3114,7 +2926,7 @@ class ReleaseManager:
3114
2926
  return 1
3115
2927
 
3116
2928
  last_file = files[-1].name
3117
- reg_ex = rf'-{label}(\d+)\.txt'
2929
+ reg_ex = rf'[.-]{label}(\d+)\.txt'
3118
2930
  match = re.search(reg_ex, last_file)
3119
2931
  if match:
3120
2932
  last_num = int(match.group(1))
@@ -3303,26 +3115,26 @@ class ReleaseManager:
3303
3115
  f" 3. OR move to another release (edit patches file manually)"
3304
3116
  )
3305
3117
 
3306
- # 3. Determine next hotfix number
3307
- hotfix_num = self._get_latest_label_number(version, 'hotfix')
3308
- hotfix_tag = f"v{version}-hotfix{hotfix_num}"
3118
+ # 3. Determine next post-release number
3119
+ post_num = self._get_latest_label_number(version, 'post')
3120
+ hotfix_tag = f"v{version}.post{post_num}"
3309
3121
 
3310
3122
  # 4. Switch to ho-prod and merge
3311
3123
  self._repo.hgit.checkout("ho-prod")
3312
3124
 
3313
3125
  # Merge ho-release/X.Y.Z into ho-prod
3314
- merge_msg = f"[release] Merge hotfix %{version}-hotfix{hotfix_num}"
3126
+ merge_msg = f"[release] Merge hotfix %{version}.post{post_num}"
3315
3127
  self._repo.hgit.merge(current_branch, message=merge_msg)
3316
3128
 
3317
- # 5. Create hotfix snapshot file from staged patches
3129
+ # 5. Create post-release snapshot file from staged patches
3318
3130
  toml_file = self._releases_dir / f"{version}-patches.toml"
3319
- hotfix_file = self._releases_dir / f"{version}-hotfix{hotfix_num}.txt"
3131
+ hotfix_file = self._releases_dir / f"{version}.post{post_num}.txt"
3320
3132
 
3321
3133
  if release_file.exists():
3322
3134
  # Get staged patches from TOML file
3323
3135
  staged_patches = release_file.get_patches(status="staged")
3324
3136
 
3325
- # Write snapshot to hotfix TXT file (production format)
3137
+ # Write snapshot to post-release TXT file (production format)
3326
3138
  hotfix_file.write_text("\n".join(staged_patches) + "\n" if staged_patches else "", encoding='utf-8')
3327
3139
  # Delete TOML patches file (no longer needed)
3328
3140
  if toml_file.exists():
@@ -3333,12 +3145,12 @@ class ReleaseManager:
3333
3145
 
3334
3146
  # 7. Commit release file changes and sync to active branches
3335
3147
  sync_result = self._repo.commit_and_sync_to_active_branches(
3336
- message=f"[HOP] Finalize hotfix %{version}-hotfix{hotfix_num} release files",
3337
- reason=f"hotfix {version}-hotfix{hotfix_num}"
3148
+ message=f"[HOP] Finalize hotfix %{version}.post{post_num} release files",
3149
+ reason=f"hotfix {version}.post{post_num}"
3338
3150
  )
3339
3151
 
3340
- # 8. Create hotfix tag on ho-prod
3341
- self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}-hotfix{hotfix_num}")
3152
+ # 8. Create post-release tag on ho-prod
3153
+ self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}.post{post_num}")
3342
3154
  self._repo.hgit.push_tag(hotfix_tag)
3343
3155
 
3344
3156
  deleted_branches = []
@@ -484,6 +484,14 @@ class Repo:
484
484
  'errors': []
485
485
  }
486
486
 
487
+ if self.production:
488
+ raise RepoError(
489
+ "Repository migration is not available on a production server.\n\n"
490
+ " Production servers are read-only: no commits or pushes to origin.\n"
491
+ " Run 'hop migrate' on a development machine first, then deploy."
492
+ )
493
+
494
+ self._migration_running = True
487
495
  try:
488
496
  # Create migration manager
489
497
  migration_mgr = MigrationManager(self)
@@ -584,18 +592,17 @@ class Repo:
584
592
  print(f" You are now on ho-prod", file=sys.stderr)
585
593
 
586
594
  except RepoError:
587
- # Re-raise RepoError (for branch check)
588
595
  raise
589
596
  except MigrationManagerError as e:
590
- # Log migration errors
591
597
  error_msg = f"Migration failed: {e}"
592
598
  result['errors'].append(error_msg)
593
599
  raise RepoError(error_msg) from e
594
600
  except Exception as e:
595
- # Catch any unexpected errors
596
601
  error_msg = f"Unexpected migration error: {e}"
597
602
  result['errors'].append(error_msg)
598
603
  raise RepoError(error_msg) from e
604
+ finally:
605
+ self._migration_running = False
599
606
 
600
607
  return result
601
608
 
@@ -918,9 +925,11 @@ class Repo:
918
925
  installed_version = hop_version()
919
926
  required_version = self.__config.hop_version
920
927
 
921
- # Validate version compatibility
922
- if not _is_version_compatible(installed_version, required_version):
923
- raise OutdatedHalfORMDevError(required_version, installed_version)
928
+ # Validate version compatibility — strict: installed must equal required.
929
+ # Skip during migration: the config is being updated, versions are transiently mismatched.
930
+ if not getattr(self, '_migration_running', False):
931
+ if self.compare_versions(installed_version, required_version) != 0:
932
+ raise OutdatedHalfORMDevError(required_version, installed_version)
924
933
 
925
934
  except RepoError:
926
935
  # Re-raise RepoError (dirty working directory)
@@ -928,16 +937,14 @@ class Repo:
928
937
  except OutdatedHalfORMDevError:
929
938
  # Re-raise version error (repository was updated to newer version)
930
939
  raise
931
- except Exception as e:
932
- # If we're in detached HEAD or any error, try to return to original state
933
- # and continue (offline mode, no remote, etc.)
940
+ except (GitCommandError, IndexError, KeyError, ValueError) as e:
941
+ # Git/network errors (offline mode, missing branch, no remote, etc.)
942
+ # Restore branch if needed and continue these are non-fatal.
934
943
  try:
935
944
  if current_branch and git_repo:
936
945
  git_repo.heads[current_branch].checkout()
937
946
  except (GitCommandError, IndexError, TypeError):
938
947
  pass
939
- # Log but don't fail (offline mode, no remote, etc.)
940
- # Only critical errors (dirty repo, version mismatch) should block
941
948
 
942
949
  def commit_and_sync_to_active_branches(
943
950
  self,
@@ -1450,6 +1457,17 @@ class Repo:
1450
1457
  if action == 'installed' or overall_action == 'skipped':
1451
1458
  overall_action = action
1452
1459
 
1460
+ # Ensure production-specific entries are in .gitignore (idempotent).
1461
+ gitignore_path = Path(self.__base_dir) / '.gitignore'
1462
+ if gitignore_path.exists():
1463
+ content = gitignore_path.read_text()
1464
+ lines = content.splitlines()
1465
+ missing = [e for e in ('.hop/production', '.hop/.fetching')
1466
+ if e not in lines]
1467
+ if missing:
1468
+ with gitignore_path.open('a') as f:
1469
+ f.write('\n' + '\n'.join(missing) + '\n')
1470
+
1453
1471
  return {
1454
1472
  'installed': any_installed,
1455
1473
  'action': overall_action
@@ -2934,9 +2952,14 @@ Each script is executed only once unless `--force` is used.
2934
2952
  )
2935
2953
 
2936
2954
  # Step 3: Clone repository
2955
+ # Production clones fetch ho-prod only — dev branches are irrelevant on production.
2956
+ clone_cmd = ["git", "clone"]
2957
+ if connection_options.get('production'):
2958
+ clone_cmd += ["--single-branch", "--branch", "ho-prod"]
2959
+ clone_cmd += [git_origin, str(dest_path)]
2937
2960
  try:
2938
2961
  result = subprocess.run(
2939
- ["git", "clone", git_origin, str(dest_path)],
2962
+ clone_cmd,
2940
2963
  capture_output=True,
2941
2964
  text=True,
2942
2965
  check=True,
@@ -3009,5 +3032,16 @@ Each script is executed only once unless `--force` is used.
3009
3032
  f"Failed to restore database from schema: {e}"
3010
3033
  ) from e
3011
3034
 
3012
- # Step 9: Install Git hooks
3035
+ # Step 9: Set up ho-current for production servers
3036
+ if connection_options.get('production'):
3037
+ # Mark this clone as production (read-only): blocks git commit/push via hooks.
3038
+ production_marker = Path(dest_path) / '.hop' / 'production'
3039
+ production_marker.parent.mkdir(parents=True, exist_ok=True)
3040
+ production_marker.touch()
3041
+ try:
3042
+ repo.release_manager.ensure_ho_current()
3043
+ except Exception as e:
3044
+ print(f" ⚠️ Warning: Could not set up ho-current: {e}", file=sys.stderr)
3045
+
3046
+ # Step 10: Install Git hooks
3013
3047
  repo.install_git_hooks()
@@ -14,4 +14,6 @@ Backups
14
14
  __pycache__
15
15
  .hop/alt_config
16
16
  .hop/local_config
17
- .hop/backups/
17
+ .hop/backups/
18
+ .hop/production
19
+ .hop/.fetching
@@ -1,11 +1,23 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
3
  # Half-ORM pre-commit hook
4
+ # 0. Blocks commits on production servers (read-only)
4
5
  # 1. Checks if current ho-* branch exists on remote origin
5
6
  # 2. Protects ho-prod branch from direct commits
6
7
  # 3. Optionally calls pre-commit-custom if it exists
7
8
  # Generated by half_orm_dev
8
9
 
10
+ # =============================================================================
11
+ # PRODUCTION SERVER GUARD
12
+ # =============================================================================
13
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
14
+ if [ -f "${REPO_ROOT}/.hop/production" ]; then
15
+ echo "ERROR: git commit is not allowed on a production server." >&2
16
+ echo " This repository is read-only (production mode)." >&2
17
+ echo " Changes are deployed via 'hop upgrade', never committed directly." >&2
18
+ exit 1
19
+ fi
20
+
9
21
  # Get current branch
10
22
  CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
11
23
 
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+
3
+ # Half-ORM pre-push hook
4
+ # Blocks git push on production servers (read-only mode).
5
+ # Generated by half_orm_dev
6
+
7
+ # =============================================================================
8
+ # PRODUCTION SERVER GUARD
9
+ # =============================================================================
10
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
11
+ if [ -f "${REPO_ROOT}/.hop/production" ]; then
12
+ echo "ERROR: git push is not allowed on a production server." >&2
13
+ echo " This repository is read-only (production mode)." >&2
14
+ echo " Changes are deployed via 'hop upgrade', never pushed directly." >&2
15
+ exit 1
16
+ fi
17
+
18
+ exit 0
@@ -0,0 +1,34 @@
1
+ #!/bin/sh
2
+
3
+ # Half-ORM reference-transaction hook
4
+ # Blocks tag creation on production servers (read-only mode).
5
+ # Generated by half_orm_dev
6
+
7
+ # Only act on the "prepared" phase (before the transaction is committed).
8
+ if [ "$1" != "prepared" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
13
+ if [ ! -f "${REPO_ROOT}/.hop/production" ]; then
14
+ exit 0
15
+ fi
16
+
17
+ # Allow tag updates that originate from an internal hop fetch operation.
18
+ # hop sets .hop/.fetching before calling git fetch and removes it after.
19
+ if [ -f "${REPO_ROOT}/.hop/.fetching" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ # Block any local tag creation.
24
+ while read -r _ _ ref_name; do
25
+ case "$ref_name" in
26
+ refs/tags/*)
27
+ echo "ERROR: git tag is not allowed on a production server." >&2
28
+ echo " This repository is read-only (production mode)." >&2
29
+ exit 1
30
+ ;;
31
+ esac
32
+ done
33
+
34
+ exit 0
@@ -0,0 +1 @@
1
+ 1.0.0-a20
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a18
3
+ Version: 1.0.0a20
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
@@ -72,4 +72,6 @@ half_orm_dev/templates/relation_test
72
72
  half_orm_dev/templates/sql_adapter
73
73
  half_orm_dev/templates/warning
74
74
  half_orm_dev/templates/git-hooks/pre-commit
75
- half_orm_dev/templates/git-hooks/prepare-commit-msg
75
+ half_orm_dev/templates/git-hooks/pre-push
76
+ half_orm_dev/templates/git-hooks/prepare-commit-msg
77
+ half_orm_dev/templates/git-hooks/reference-transaction
@@ -1 +0,0 @@
1
- 1.0.0-a18
File without changes
File without changes