You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 line
8.9KB

  1. # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop)
  2. # @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import importlib
  5. import os
  6. import pathlib
  7. import pkgutil
  8. import inspect
  9. from .config import _AVAILABLE_MIGRATION_STEPS, _MANIFEST_NAMES
  10. from .exception import ConfigException
  11. from .log import logger
  12. from .tools import _execute_shell, _get_latest_version_code
  13. from .module_migration import ModuleMigration
  14. from .base_migration_script import BaseMigrationScript
  15. class Migration:
  16. def __init__(
  17. self, relative_directory_path, init_version_name, target_version_name,
  18. module_names=None, format_patch=False, remote_name='origin',
  19. commit_enabled=True, pre_commit=True, remove_migration_folder=True,
  20. ):
  21. if not module_names:
  22. module_names = []
  23. self._commit_enabled = commit_enabled
  24. self._pre_commit = pre_commit
  25. self._remove_migration_folder = remove_migration_folder
  26. self._migration_steps = []
  27. self._migration_scripts = []
  28. self._module_migrations = []
  29. self._directory_path = False
  30. # Get migration steps that will be runned
  31. found = False
  32. for item in _AVAILABLE_MIGRATION_STEPS:
  33. if not found and item["init_version_name"] != init_version_name:
  34. continue
  35. else:
  36. found = True
  37. self._migration_steps.append(item)
  38. if item["target_version_name"] == target_version_name:
  39. # This is the last step, exiting
  40. break
  41. # Check consistency between format patch and module_names args
  42. if format_patch and len(module_names) != 1:
  43. raise ConfigException(
  44. "Format patch option can only be used for a single module")
  45. logger.debug("Module list: %s" % module_names)
  46. logger.debug("format patch option : %s" % format_patch)
  47. # convert relative or absolute directory into Path Object
  48. if not os.path.exists(relative_directory_path):
  49. raise ConfigException(
  50. "Unable to find directory: %s" % relative_directory_path)
  51. root_path = pathlib.Path(relative_directory_path)
  52. self._directory_path = pathlib.Path(root_path.resolve(strict=True))
  53. # format-patch, if required
  54. if format_patch:
  55. if not (root_path / module_names[0]).is_dir():
  56. self._get_code_from_previous_branch(
  57. module_names[0], remote_name)
  58. else:
  59. logger.warning(
  60. "Ignoring format-patch argument, as the module %s"
  61. " is still present in the repository" % (module_names[0]))
  62. # Guess modules if not provided, and check validity
  63. if not module_names:
  64. module_names = []
  65. # Recover all submodules, if no modules list is provided
  66. child_paths = [x for x in root_path.iterdir() if x.is_dir()]
  67. for child_path in child_paths:
  68. if self._is_module_path(child_path):
  69. module_names.append(child_path.name)
  70. else:
  71. child_paths = [root_path / x for x in module_names]
  72. for child_path in child_paths:
  73. if not self._is_module_path(child_path):
  74. module_names.remove(child_path.name)
  75. logger.warning(
  76. "No valid module found for '%s' in the directory '%s'"
  77. % (child_path.name, root_path.resolve()))
  78. if not module_names:
  79. raise ConfigException("No modules found to migrate. Exiting.")
  80. for module_name in module_names:
  81. self._module_migrations.append(ModuleMigration(self, module_name))
  82. if os.path.exists(".pre-commit-config.yaml") and self._pre_commit:
  83. self._run_pre_commit(module_names)
  84. # get migration scripts, depending to the migration list
  85. self._get_migration_scripts()
  86. def _run_pre_commit(self, module_names):
  87. logger.info("Run pre-commit")
  88. _execute_shell(
  89. "pre-commit run -a", path=self._directory_path, raise_error=False)
  90. if self._commit_enabled:
  91. logger.info("Stage and commit changes done by pre-commit")
  92. _execute_shell("git add -A", path=self._directory_path)
  93. _execute_shell(
  94. "git commit -m '[IMP] %s: pre-commit execution' --no-verify"
  95. % ", ".join(module_names),
  96. path=self._directory_path,
  97. raise_error=False, # Don't fail if there is nothing to commit
  98. )
  99. def _is_module_path(self, module_path):
  100. return any([(module_path / x).exists() for x in _MANIFEST_NAMES])
  101. def _get_code_from_previous_branch(self, module_name, remote_name):
  102. init_version = self._migration_steps[0]["init_version_name"]
  103. target_version = self._migration_steps[-1]["target_version_name"]
  104. branch_name = "%(version)s-mig-%(module_name)s" % {
  105. 'version': target_version,
  106. 'module_name': module_name}
  107. logger.info("Creating new branch '%s' ..." % (branch_name))
  108. _execute_shell(
  109. "git checkout --no-track -b %(branch)s %(remote)s/%(version)s" % {
  110. 'branch': branch_name,
  111. 'remote': remote_name,
  112. 'version': target_version,
  113. }, path=self._directory_path)
  114. logger.info("Getting latest changes from old branch")
  115. # Depth is added just in case you had a shallow git history
  116. _execute_shell(
  117. "git fetch --depth 9999999 %(remote)s %(init)s" % {
  118. 'remote': remote_name,
  119. 'init': init_version,
  120. }, path=self._directory_path
  121. )
  122. _execute_shell(
  123. "git format-patch --keep-subject "
  124. "--stdout %(remote)s/%(target)s..%(remote)s/%(init)s "
  125. "-- %(module)s | git am -3 --keep" % {
  126. 'remote': remote_name,
  127. 'init': init_version,
  128. 'target': target_version,
  129. 'module': module_name,
  130. }, path=self._directory_path)
  131. def _load_migration_script(self, full_name):
  132. module = importlib.import_module(full_name)
  133. result = [x[1]()
  134. for x in inspect.getmembers(module, inspect.isclass)
  135. if x[0] != 'BaseMigrationScript'
  136. and issubclass(x[1], BaseMigrationScript)]
  137. return result
  138. def _get_migration_scripts(self):
  139. # Add the script that will be allways executed
  140. self._migration_scripts.extend(
  141. self._load_migration_script(
  142. "odoo_module_migrate.migration_scripts.migrate_allways"
  143. )
  144. )
  145. if self._remove_migration_folder:
  146. self._migration_scripts.extend(
  147. self._load_migration_script(
  148. "odoo_module_migrate.migration_scripts."
  149. "migrate_remove_migration_folder"
  150. )
  151. )
  152. all_packages = importlib.\
  153. import_module("odoo_module_migrate.migration_scripts")
  154. migration_start = float(self._migration_steps[0]["init_version_code"])
  155. migration_end = float(self._migration_steps[-1]["target_version_code"])
  156. for loader, name, is_pkg in pkgutil.walk_packages(
  157. all_packages.__path__):
  158. # Ignore script that will be allways executed.
  159. # this script will be added at the end.
  160. if name in ('migrate_allways', 'migrate_remove_migration_folder'):
  161. continue
  162. # Filter migration scripts, depending of the configuration
  163. full_name = all_packages.__name__ + '.' + name
  164. if 'allways' in name:
  165. # replace allways by the most recent version
  166. real_name = name.replace("allways", _get_latest_version_code())
  167. else:
  168. real_name = name
  169. splitted_name = real_name.split("_")
  170. script_start = float(splitted_name[1])
  171. script_end = float(splitted_name[2])
  172. # Exclude scripts
  173. if script_start >= migration_end or script_end <= migration_start:
  174. continue
  175. self._migration_scripts.extend(
  176. self._load_migration_script(full_name)
  177. )
  178. logger.debug(
  179. "The following migration script will be"
  180. " executed:\n- %s" % '\n- '.join(
  181. [
  182. inspect.getfile(x.__class__).split('/')[-1]
  183. for x in self._migration_scripts]
  184. )
  185. )
  186. def run(self):
  187. logger.debug(
  188. "Running migration from: %s to: %s in '%s'" % (
  189. self._migration_steps[0]["init_version_name"],
  190. self._migration_steps[-1]["target_version_name"],
  191. self._directory_path.resolve()))
  192. for module_migration in self._module_migrations:
  193. module_migration.run()
上海开阖软件有限公司 沪ICP备12045867号-1