diff --git a/pyproject.toml b/pyproject.toml index e4e9c489..cb8ec79c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ oca-update-pre-commit-excluded-addons = "tools.update_pre_commit_excluded_addons oca-fix-manifest-website = "tools.fix_manifest_website:main" oca-configure-travis= "tools.configure_travis:main" oca-copier-update = "tools.copier_update:main" +oca-create-branch-from-previous = "tools.create_branch_from_previous:main" [tool.hatch.build.targets.wheel] packages = ["tools"] diff --git a/tests/test_mark_modules_uninstallable.py b/tests/test_mark_modules_uninstallable.py new file mode 100644 index 00000000..80650454 --- /dev/null +++ b/tests/test_mark_modules_uninstallable.py @@ -0,0 +1,50 @@ +import textwrap + +from tools.manifest import mark_modules_uninstallable, mark_manifest_uninstallable + + +def test_mark_module_uninstallable(tmp_path): + (tmp_path / "mod1").mkdir() + (tmp_path / "mod1" / "__manifest__.py").write_text("""{'name': 'mod1'}""") + mark_modules_uninstallable(tmp_path) + assert (tmp_path / "mod1" / "__manifest__.py").read_text() == ( + """{'name': 'mod1',\n 'installable': False,\n}\n""" + ) + + +def test_mark_module_uninstallable_key_exists(tmp_path): + (tmp_path / "mod1").mkdir() + (tmp_path / "mod1" / "__manifest__.py").write_text( + """{'name': 'mod1', "installable": True}""" + ) + mark_modules_uninstallable(tmp_path) + assert (tmp_path / "mod1" / "__manifest__.py").read_text() == ( + """{'name': 'mod1', "installable": False}""" + ) + + +def test_mark_module_uninstallable_curly_braces(tmp_path): + assert mark_manifest_uninstallable( + textwrap.dedent( + """\ + { + 'name': 'mod1', + 'external_dependencies': { + 'python': ['some_package'], + }, + 'license': 'AGPL-3', + } + """ + ) + ) == textwrap.dedent( + """\ + { + 'name': 'mod1', + 'external_dependencies': { + 'python': ['some_package'], + }, + 'license': 'AGPL-3', + 'installable': False, + } + """ + ) diff --git a/tools/create_branch_from_previous.py b/tools/create_branch_from_previous.py new file mode 100644 index 00000000..7ecb603a --- /dev/null +++ b/tools/create_branch_from_previous.py @@ -0,0 +1,125 @@ +import subprocess +from pathlib import Path + +import click + +from .manifest import mark_modules_uninstallable + + +@click.command() +@click.option("--odoo-version", required=True) +@click.option("--new-branch-name") +@click.option( + "--addons-dir", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=".", +) +@click.option( + "--data", + multiple=True, + help="Additional copier data, as key=value", +) +def main( + odoo_version: str, + new_branch_name: str | None, + addons_dir: Path, + data: list[str], +) -> None: + """Create a new branch off an existing branch and set all addons installable=False. + + To use it, go to a git clone of a repo and checkout the branch you want to start from. + """ + if not new_branch_name: + new_branch_name = odoo_version + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + check=True, + text=True, + capture_output=True, + ) + previous_branch = result.stdout.strip() + subprocess.run( + [ + "git", + "checkout", + "-b", + new_branch_name, + ], + check=True, + ) + result = subprocess.run( + [ + "copier", + "recopy", # override local changes when creating a new branch + "--trust", + "--defaults", + "--overwrite", + f"--data=odoo_version={odoo_version}", + *(f"--data={d}" for d in data), + ], + ) + if result.returncode != 0: + raise SystemExit("copier update failed, please fix manually") + result = subprocess.run( + [ + "git", + "diff", + "--diff-filter=U", + "--quiet", + ], + ) + if result.returncode != 0: + raise SystemExit( + "There are merge conflicts after copier update, please fix manually" + ) + mark_modules_uninstallable(addons_dir) + if Path(".pre-commit-config.yaml").exists(): + # First run pre-commit on .pre-commit-config.yaml, to exclude + # addons that are not installable. + subprocess.run( + [ + "pre-commit", + "run", + "--files", + ".pre-commit-config.yaml", + ], + check=False, + ) + # Run pre-commit once to let it apply auto fixes. + subprocess.run( + [ + "pre-commit", + "run", + "--all-files", + ], + check=False, + ) + # Run pre-commit a second time to check that everything is green. + result = subprocess.run( + [ + "pre-commit", + "run", + "--all-files", + ], + check=False, + ) + if result.returncode != 0: + raise SystemExit("pre-commit failed, please fix manually") + subprocess.run( + [ + "git", + "add", + ".", + ], + check=True, + ) + subprocess.run( + [ + "git", + "commit", + "-m", + f"[MIG] Create {new_branch_name} from {previous_branch} " + f"for Odoo {odoo_version}", + ], + check=True, + ) diff --git a/tools/manifest.py b/tools/manifest.py index daea0f36..5c1d4f30 100644 --- a/tools/manifest.py +++ b/tools/manifest.py @@ -3,6 +3,8 @@ import ast import os +import re +from pathlib import Path MANIFEST_NAMES = ("__manifest__.py", "__openerp__.py", "__terp__.py") @@ -41,3 +43,21 @@ def find_addons(addons_dir, installable_only=True): if installable_only and not manifest.get("installable", True): continue yield addon_name, addon_dir, manifest + + +def mark_manifest_uninstallable(manifest_text: str) -> str: + manifest = ast.literal_eval(manifest_text) + if "installable" not in manifest: + src = r",?\s*}\s*$" + dest = ",\n 'installable': False,\n}\n" + else: + src = "[\"']installable[\"']: *True" + dest = '"installable": False' + return re.sub(src, dest, manifest_text, re.DOTALL) + + +def mark_modules_uninstallable(addons_dir: Path) -> None: + for manifest_path in addons_dir.glob("*/__manifest__.py"): + manifest_text = manifest_path.read_text(encoding="utf-8") + new_manifest_text = mark_manifest_uninstallable(manifest_text) + manifest_path.write_text(new_manifest_text, encoding="utf-8")