half-orm-dev 1.0.0a20__tar.gz → 1.0.0a22__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 (81) hide show
  1. {half_orm_dev-1.0.0a20/half_orm_dev.egg-info → half_orm_dev-1.0.0a22}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/main.py +35 -3
  3. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/hgit.py +7 -0
  4. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migration_manager.py +67 -47
  5. half_orm_dev-1.0.0a22/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +36 -0
  6. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/repo.py +67 -38
  7. half_orm_dev-1.0.0a22/half_orm_dev/version.txt +1 -0
  8. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22/half_orm_dev.egg-info}/PKG-INFO +1 -1
  9. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  10. half_orm_dev-1.0.0a20/half_orm_dev/version.txt +0 -1
  11. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/AUTHORS +0 -0
  12. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/LICENSE +0 -0
  13. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/README.md +0 -0
  14. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/__init__.py +0 -0
  15. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/bootstrap_manager.py +0 -0
  16. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/__init__.py +0 -0
  17. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/__init__.py +0 -0
  18. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/apply.py +0 -0
  19. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  20. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/check.py +0 -0
  21. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/clone.py +0 -0
  22. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/init.py +0 -0
  23. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/migrate.py +0 -0
  24. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/patch.py +0 -0
  25. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/release.py +0 -0
  26. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/restore.py +0 -0
  27. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  28. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  29. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/sync.py +0 -0
  30. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/todo.py +0 -0
  31. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/undo.py +0 -0
  32. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/update.py +0 -0
  33. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli/commands/upgrade.py +0 -0
  34. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/cli_extension.py +0 -0
  35. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/database.py +0 -0
  36. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/decorators.py +0 -0
  37. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/file_executor.py +0 -0
  38. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  39. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  40. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  41. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  42. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  43. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  44. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  45. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  46. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  47. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/modules.py +0 -0
  48. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patch_manager.py +0 -0
  49. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patch_validator.py +0 -0
  50. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  51. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  52. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  53. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/log +0 -0
  54. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  55. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/release_file.py +0 -0
  56. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/release_manager.py +0 -0
  57. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/scripts/repair-metadata.py +0 -0
  58. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/.gitignore +0 -0
  59. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/MANIFEST.in +0 -0
  60. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/README +0 -0
  61. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/conftest_template +0 -0
  62. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  63. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/pre-push +0 -0
  64. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  65. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
  66. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/init_module_template +0 -0
  67. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_1 +0 -0
  68. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_2 +0 -0
  69. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/module_template_3 +0 -0
  70. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/pyproject.toml +0 -0
  71. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/relation_test +0 -0
  72. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/sql_adapter +0 -0
  73. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/templates/warning +0 -0
  74. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev/utils.py +0 -0
  75. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  76. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/entry_points.txt +0 -0
  77. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/requires.txt +0 -0
  78. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/half_orm_dev.egg-info/top_level.txt +0 -0
  79. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/pyproject.toml +0 -0
  80. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/setup.cfg +0 -0
  81. {half_orm_dev-1.0.0a20 → half_orm_dev-1.0.0a22}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a20
3
+ Version: 1.0.0a22
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
@@ -133,6 +133,38 @@ def create_cli_group():
133
133
  cmd.callback = check_version_before_invoke(cmd.callback)
134
134
  super().add_command(cmd, name)
135
135
 
136
+ def get_command(self, ctx, cmd_name):
137
+ cmd = super().get_command(ctx, cmd_name)
138
+ if cmd is not None:
139
+ return cmd
140
+ # Unknown command — show a specific message when migration is needed.
141
+ if hop.repo_checked and hop._Hop__repo.needs_migration():
142
+ from half_orm_dev.utils import hop_version
143
+ installed_version = hop_version()
144
+ config_version = hop._Hop__repo._Repo__config.hop_version
145
+
146
+ @click.command(
147
+ cmd_name,
148
+ context_settings={
149
+ 'allow_extra_args': True,
150
+ 'ignore_unknown_options': True,
151
+ },
152
+ )
153
+ @click.pass_context
154
+ def _migration_required(ctx):
155
+ click.echo(
156
+ f"\n⚠️ Command '{cmd_name}' is not available: migration required.\n\n"
157
+ f" Repository version: {config_version}\n"
158
+ f" Installed version: {installed_version}\n\n"
159
+ f" Apply migration: half_orm dev migrate\n"
160
+ f" Revert version: pip install half-orm-dev=={config_version}\n",
161
+ err=True,
162
+ )
163
+ sys.exit(1)
164
+
165
+ return _migration_required
166
+ return None
167
+
136
168
  @click.group(cls=VersionCheckGroup, invoke_without_command=True)
137
169
  @click.pass_context
138
170
  def dev(ctx):
@@ -141,8 +173,8 @@ def create_cli_group():
141
173
  error = hop.hop_upgrade_error
142
174
  required = error.required_version
143
175
  installed = error.installed_version
144
- click.echo(f"\n Ce repo requiers half-orm-dev {required} "
145
- f"(installed : {installed}).")
176
+ click.echo(f"\n This repository requires half-orm-dev {required} "
177
+ f"(installed: {installed}).")
146
178
  click.echo(f" Installing the required version...")
147
179
  try:
148
180
  subprocess.run(
@@ -151,7 +183,7 @@ def create_cli_group():
151
183
  )
152
184
  except subprocess.CalledProcessError:
153
185
  click.echo(f"\n Installation failed.", err=True)
154
- click.echo(f" Run manually : pip install half-orm-dev=={required}", err=True)
186
+ click.echo(f" Run manually: pip install half-orm-dev=={required}", err=True)
155
187
  sys.exit(1)
156
188
  click.echo(f" ✓ half-orm-dev {required} installed. Restarting...\n")
157
189
  os.execv(sys.argv[0], sys.argv)
@@ -1471,8 +1471,15 @@ class HGit:
1471
1471
  is_local = branch in local_release_branches
1472
1472
  release_branch_infos.append(get_branch_info(branch, check_stage=True, is_local=is_local))
1473
1473
 
1474
+ # ho-prod is always active — include it without special treatment.
1475
+ prod_branch_info = None
1476
+ local_prod = self.branch_exists('ho-prod')
1477
+ if local_prod or 'ho-prod' in remote_branch_names:
1478
+ prod_branch_info = get_branch_info('ho-prod', is_local=local_prod)
1479
+
1474
1480
  return {
1475
1481
  'current_branch': current_branch,
1482
+ 'prod_branch': prod_branch_info,
1476
1483
  'patch_branches': patch_branch_infos,
1477
1484
  'staged_branches': staged_branch_infos,
1478
1485
  'release_branches': release_branch_infos
@@ -10,10 +10,11 @@ Directory structure:
10
10
  ├── log # List of applied migrations (version format)
11
11
  └── major/ # Major version
12
12
  └── minor/ # Minor version
13
- └── patch/ # Patch version
13
+ └── patch/ # Patch version (stable migrations here)
14
14
  ├── 00_migration_name.py
15
15
  ├── 01_another_migration.py
16
- └── README.md
16
+ └── a20/ # Pre-release migrations (4th level, PEP 440)
17
+ └── 01_migration_name.py
17
18
 
18
19
  Each migration file must define:
19
20
  - migrate(repo): Execute the migration
@@ -66,75 +67,92 @@ class MigrationManager:
66
67
  # Path to migrations directory (in half_orm_dev package)
67
68
  self._migrations_root = Path(__file__).parent / 'migrations'
68
69
 
69
- def _version_to_path(self, version: Tuple[int, int, int]) -> Path:
70
+ def _version_to_path(self, version_str: str) -> Path:
70
71
  """
71
- Convert version tuple to migration directory path.
72
+ Convert a version string to its migration directory path.
73
+
74
+ For stable versions (e.g., "0.17.5") returns major/minor/patch/.
75
+ For pre-release versions (e.g., "1.0.0a20") returns major/minor/patch/pre/
76
+ where pre is the pre-release segment (e.g., "a20").
72
77
 
73
78
  Args:
74
- version: Tuple of (major, minor, patch)
79
+ version_str: PEP 440 version string
75
80
 
76
81
  Returns:
77
82
  Path to migration directory
78
83
  """
79
- major, minor, patch = version
80
- return self._migrations_root / str(major) / str(minor) / str(patch)
84
+ v = version.parse(version_str)
85
+ major, minor, patch = v.release[:3]
86
+ base = self._migrations_root / str(major) / str(minor) / str(patch)
87
+ if v.pre:
88
+ pre_str = ''.join(str(p) for p in v.pre)
89
+ return base / pre_str
90
+ return base
81
91
 
82
92
  def get_pending_migrations(self, current_version: str, target_version: str) -> List[Tuple[str, Path]]:
83
93
  """
84
94
  Get list of migrations that need to be applied.
85
95
 
86
96
  Compares current version (from .hop/config) with target version (from hop_version())
87
- and returns all migrations in between.
97
+ and returns all migrations in between, sorted by PEP 440 version order.
98
+
99
+ Directory structure:
100
+ migrations/major/minor/patch/ ← stable version scripts
101
+ migrations/major/minor/patch/a20/ ← pre-release scripts (4th level)
88
102
 
89
103
  Args:
90
- current_version: Current version from .hop/config (e.g., "0.17.0")
91
- target_version: Target version from hop_version() (e.g., "0.17.1")
104
+ current_version: Current version from .hop/config (e.g., "0.17.0" or "1.0.0-a19")
105
+ target_version: Target version from hop_version() (e.g., "0.17.1" or "1.0.0-a20")
92
106
 
93
107
  Returns:
94
- List of (version_str, migration_dir_path) tuples in order
108
+ List of (version_str, migration_dir_path) tuples in PEP 440 order
95
109
  """
96
- current = version.parse(current_version).release
97
- target = version.parse(target_version).release
110
+ current = version.parse(current_version)
111
+ target = version.parse(target_version)
98
112
 
99
- pending = []
113
+ candidates = []
100
114
 
101
- # Walk through version directories to find migrations between current and target
102
- # Start from current version + 1 up to target version
103
- for major in range(0, target[0] + 1):
104
- major_dir = self._migrations_root / str(major)
105
- if not major_dir.exists():
115
+ for major_dir in sorted(self._migrations_root.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
116
+ if not major_dir.is_dir() or not major_dir.name.isdigit():
106
117
  continue
118
+ major = int(major_dir.name)
107
119
 
108
- minor_max = target[1] if major == target[0] else 999
109
- for minor in range(0, minor_max + 1):
110
- minor_dir = major_dir / str(minor)
111
- if not minor_dir.exists():
120
+ for minor_dir in sorted(major_dir.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
121
+ if not minor_dir.is_dir() or not minor_dir.name.isdigit():
112
122
  continue
123
+ minor = int(minor_dir.name)
113
124
 
114
- patch_max = target[2] if major == target[0] and minor == target[1] else 999
115
- for patch in range(0, patch_max + 1):
116
- patch_dir = minor_dir / str(patch)
117
- if not patch_dir.exists():
118
- continue
119
-
120
- version_tuple = (major, minor, patch)
121
-
122
- # Skip if this version is <= current version
123
- if version_tuple <= current:
125
+ for patch_dir in sorted(minor_dir.iterdir(), key=lambda p: int(p.name) if p.name.isdigit() else -1):
126
+ if not patch_dir.is_dir() or not patch_dir.name.isdigit():
124
127
  continue
125
-
126
- # Skip if this version is > target version
127
- if version_tuple > target:
128
- continue
129
-
130
- version_str = f"{major}.{minor}.{patch}"
131
-
132
- # Check if this version has any migration files
133
- migration_files = list(patch_dir.glob('*.py'))
134
- if migration_files:
135
- pending.append((version_str, patch_dir))
136
-
137
- return pending
128
+ patch = int(patch_dir.name)
129
+ base_version_str = f"{major}.{minor}.{patch}"
130
+
131
+ # 4th level: pre-release subdirectories (e.g., a20/, b1/, rc2/)
132
+ for pre_dir in sorted(patch_dir.iterdir()):
133
+ if not pre_dir.is_dir() or not re.match(r'^[a-z]+\d+$', pre_dir.name):
134
+ continue
135
+ pre_version_str = f"{base_version_str}{pre_dir.name}"
136
+ try:
137
+ v = version.parse(pre_version_str)
138
+ except Exception:
139
+ continue
140
+ if current < v <= target and list(pre_dir.glob('*.py')):
141
+ candidates.append((pre_version_str, pre_dir))
142
+
143
+ # Stable version scripts at patch level
144
+ stable_files = list(patch_dir.glob('*.py'))
145
+ if stable_files:
146
+ try:
147
+ v = version.parse(base_version_str)
148
+ except Exception:
149
+ continue
150
+ if current < v <= target:
151
+ candidates.append((base_version_str, patch_dir))
152
+
153
+ # Sort by PEP 440 version (pre-releases sort before their stable counterpart)
154
+ candidates.sort(key=lambda x: version.parse(x[0]))
155
+ return candidates
138
156
 
139
157
  def _load_migration_module(self, migration_file: Path):
140
158
  """
@@ -506,10 +524,12 @@ class MigrationManager:
506
524
  except Exception:
507
525
  return # can't determine status, proceed cautiously
508
526
 
527
+ prod_info = branches_status.get('prod_branch')
528
+ prod_branches = [prod_info['name']] if prod_info else []
509
529
  patch_branches = [b['name'] for b in branches_status.get('patch_branches', [])]
510
530
  release_branches = [b['name'] for b in branches_status.get('release_branches', [])]
511
531
  # ho-staged/* branches are frozen after merge — excluded from sync checks
512
- active_branches = release_branches + patch_branches
532
+ active_branches = prod_branches + release_branches + patch_branches
513
533
 
514
534
  blocked = []
515
535
  for branch in active_branches:
@@ -0,0 +1,36 @@
1
+ """
2
+ Migration 1.0.0a20 — Add production-specific entries to .gitignore
3
+
4
+ Adds .hop/production and .hop/.fetching to .gitignore so that production
5
+ server marker files are not tracked by git.
6
+ """
7
+
8
+ GITIGNORE_ENTRIES = ['.hop/production', '.hop/.fetching']
9
+
10
+
11
+ def get_description():
12
+ return "Add .hop/production and .hop/.fetching to .gitignore"
13
+
14
+
15
+ def migrate(repo):
16
+ from pathlib import Path
17
+
18
+ base_dir = repo._Repo__base_dir
19
+ gitignore_path = Path(base_dir) / '.gitignore'
20
+
21
+ if not gitignore_path.exists():
22
+ return {}
23
+
24
+ content = gitignore_path.read_text()
25
+ lines = content.splitlines()
26
+ missing = [e for e in GITIGNORE_ENTRIES if e not in lines]
27
+
28
+ if not missing:
29
+ return {}
30
+
31
+ with gitignore_path.open('a') as f:
32
+ f.write('\n' + '\n'.join(missing) + '\n')
33
+
34
+ repo.stage_maintenance_file('.gitignore')
35
+
36
+ return {'sync_files': ['.gitignore']}
@@ -491,7 +491,6 @@ class Repo:
491
491
  " Run 'hop migrate' on a development machine first, then deploy."
492
492
  )
493
493
 
494
- self._migration_running = True
495
494
  try:
496
495
  # Create migration manager
497
496
  migration_mgr = MigrationManager(self)
@@ -506,25 +505,38 @@ class Repo:
506
505
 
507
506
  result['migration_needed'] = True
508
507
 
509
- # Migration must be run on ho-prod branch
510
- # If not on ho-prod, switch automatically if working directory is clean
511
508
  current_branch = self.hgit.branch if self.hgit else 'unknown'
512
509
  switched_branch = False
513
510
 
511
+ # Dirty check done ONCE here, before any operation.
512
+ # After this point _migration_running disables further dirty checks:
513
+ # all modifications are the migration's responsibility and will be committed.
514
+ if self.hgit and self.hgit.git_repo.is_dirty(untracked_files=False):
515
+ config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
516
+ status = self.hgit.git_repo.git.status('--short')
517
+ raise RepoError(
518
+ f"Repository migration required but working directory has uncommitted changes.\n\n"
519
+ f" Repository version: {config_version}\n"
520
+ f" Installed version: {current_version}\n"
521
+ f" Current branch: {current_branch}\n\n"
522
+ f" Please commit or stash your changes:\n"
523
+ f" git stash\n"
524
+ f" OR\n"
525
+ f" git add . && git commit -m \"your message\"\n"
526
+ f"Dirty files:\n{status}"
527
+ )
528
+
529
+ # Verify all active branches (including ho-prod) are in sync before
530
+ # switching branches or touching anything.
531
+ try:
532
+ migration_mgr._ensure_active_branches_synced()
533
+ except Exception as e:
534
+ raise RepoError(str(e)) from e
535
+
536
+ # From here, all modifications are made by the migration itself.
537
+ self._migration_running = True
538
+
514
539
  if not self.hgit or self.hgit.branch != 'ho-prod':
515
- # Check if working directory is clean
516
- if self.hgit and self.hgit.git_repo.is_dirty(untracked_files=False):
517
- config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
518
- raise RepoError(
519
- f"Repository migration required but working directory has uncommitted changes.\n\n"
520
- f" Repository version: {config_version}\n"
521
- f" Installed version: {current_version}\n"
522
- f" Current branch: {current_branch}\n\n"
523
- f" Please commit or stash your changes:\n"
524
- f" git stash\n"
525
- f" OR\n"
526
- f" git add . && git commit -m \"your message\"\n"
527
- )
528
540
 
529
541
  # Working directory is clean, switch to ho-prod
530
542
  try:
@@ -896,17 +908,21 @@ class Repo:
896
908
  git_repo = self.hgit.git_repo
897
909
  current_branch = git_repo.active_branch.name
898
910
 
899
- # Check if working directory is clean
900
- if git_repo.is_dirty(untracked_files=False):
901
- status = git_repo.git.status('--short')
902
- raise RepoError(
903
- f"Working directory has uncommitted changes.\n"
904
- f"Please commit or stash your changes before running this command:\n"
905
- f" git stash\n"
906
- f" OR\n"
907
- f" git add . && git commit -m \"your message\"\n"
908
- f"Dirty files:\n{status}"
909
- )
911
+ # Check if working directory is clean.
912
+ # Skipped during migration: _migration_running means the dirty check
913
+ # was already done once at the start of run_migrations_if_needed(),
914
+ # and all subsequent modifications are committed by the migration itself.
915
+ if not getattr(self, '_migration_running', False):
916
+ if git_repo.is_dirty(untracked_files=False):
917
+ status = git_repo.git.status('--short')
918
+ raise RepoError(
919
+ f"Working directory has uncommitted changes.\n"
920
+ f"Please commit or stash your changes before running this command:\n"
921
+ f" git stash\n"
922
+ f" OR\n"
923
+ f" git add . && git commit -m \"your message\"\n"
924
+ f"Dirty files:\n{status}"
925
+ )
910
926
 
911
927
  # Switch to ho-prod temporarily
912
928
  git_repo.heads['ho-prod'].checkout()
@@ -1391,6 +1407,30 @@ class Repo:
1391
1407
  # step 12: Protect ho-prod from direct commits
1392
1408
  self.install_git_hooks()
1393
1409
 
1410
+ def stage_maintenance_file(self, relative_path: str) -> None:
1411
+ """Stage a file modified by an automated maintenance operation."""
1412
+ if not hasattr(self, '_maintenance_files'):
1413
+ self._maintenance_files = []
1414
+ abs_path = os.path.join(self.__base_dir, relative_path)
1415
+ if os.path.exists(abs_path):
1416
+ self.hgit.git_repo.index.add([relative_path])
1417
+ if relative_path not in self._maintenance_files:
1418
+ self._maintenance_files.append(relative_path)
1419
+
1420
+ def commit_maintenance_files(self, message: str = 'update maintenance files') -> bool:
1421
+ """Commit all staged maintenance files in a single [HOP] commit (skip hooks)."""
1422
+ if not getattr(self, '_maintenance_files', None):
1423
+ return False
1424
+ try:
1425
+ self.hgit.git_repo.index.commit(
1426
+ f'[HOP] {message}',
1427
+ skip_hooks=True,
1428
+ )
1429
+ self._maintenance_files = []
1430
+ return True
1431
+ except Exception:
1432
+ return False
1433
+
1394
1434
  def install_git_hooks(self, force: bool = False) -> dict:
1395
1435
  """
1396
1436
  Install or update Git hooks from templates.
@@ -1457,17 +1497,6 @@ class Repo:
1457
1497
  if action == 'installed' or overall_action == 'skipped':
1458
1498
  overall_action = action
1459
1499
 
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
-
1471
1500
  return {
1472
1501
  'installed': any_installed,
1473
1502
  'action': overall_action
@@ -0,0 +1 @@
1
+ 1.0.0-a22
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a20
3
+ Version: 1.0.0a22
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
@@ -52,6 +52,7 @@ half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py
52
52
  half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py
53
53
  half_orm_dev/migrations/0/18/0/00_add_async_support.py
54
54
  half_orm_dev/migrations/0/18/0/01_update_default_tests.py
55
+ half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py
55
56
  half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md
56
57
  half_orm_dev/patches/log
57
58
  half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql
@@ -1 +0,0 @@
1
- 1.0.0-a20
File without changes
File without changes