diff --git a/scripts/update-demo.py b/scripts/update-demo.py index 4d2ad30..55386fb 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -7,7 +7,11 @@ import typer from cookiecutter.utils import work_in +from util import is_ancestor +from util import get_current_branch +from util import get_current_commit from util import get_demo_name +from util import get_last_cruft_update_commit from util import git from util import FolderOption from util import REPO_FOLDER @@ -24,26 +28,60 @@ def update_demo( ) -> None: """Runs precommit in a generated project and matches the template to the results.""" try: - develop_branch: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH", "develop") demo_name: str = get_demo_name(add_rust_extension=add_rust_extension) demo_path: Path = demos_cache_folder / demo_name + + current_branch: str = get_current_branch() + current_commit: str = get_current_commit() + + _validate_is_feature_branch(branch=current_branch) + + last_update_commit: str = _get_last_demo_develop_cruft_update(demo_path=demo_path) + + if not is_ancestor(last_update_commit, current_commit): + raise ValueError( + f"The last update commit '{last_update_commit}' is not an ancestor of the current commit " + f"'{current_commit}'." + ) + typer.secho(f"Updating demo project at {demo_path=}.", fg="yellow") with work_in(demo_path): - require_clean_and_up_to_date_repo() - git("checkout", develop_branch) + if current_branch != "develop": + git("checkout", "-b", current_branch) + cruft.update( project_dir=demo_path, template_path=REPO_FOLDER, extra_context={"project_name": demo_name, "add_rust_extension": add_rust_extension}, ) git("add", ".") - git("commit", "-m", "chore: update demo to the latest cookiecutter-robust-python", "--no-verify") - git("push") + git("commit", "-m", f"chore: {last_update_commit} -> {current_commit}", "--no-verify") + git("push", "-u", "origin", current_branch) except Exception as error: typer.secho(f"error: {error}", fg="red") sys.exit(1) +def _get_last_demo_develop_cruft_update(demo_path: Path) -> str: + """Gets the last cruft update commit for the demo project's develop branch.""" + _prep_demo_develop(demo_path=demo_path) + last_update_commit: str = get_last_cruft_update_commit(demo_path=demo_path) + return last_update_commit + + +def _prep_demo_develop(demo_path: Path) -> None: + """Checks out the demo development branch and validates it is up to date.""" + with work_in(demo_path): + require_clean_and_up_to_date_repo() + git("checkout", "develop") + + +def _validate_is_feature_branch(branch: str) -> None: + """Validates that the cookiecutter has a feature branch checked out.""" + if not branch.startswith("feature/"): + raise ValueError(f"Received branch '{branch}' is not a feature branch.") + + if __name__ == '__main__': cli() diff --git a/scripts/util.py b/scripts/util.py index 5ce7147..ac30f5f 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -1,4 +1,5 @@ """Module containing utility functions used throughout cookiecutter_robust_python scripts.""" +import json import os import shutil import stat @@ -16,7 +17,9 @@ import cruft import typer + from cookiecutter.utils import work_in +from cruft._commands.utils.cruft import get_cruft_file from dotenv import load_dotenv from typer.models import OptionInfo @@ -42,7 +45,6 @@ def _load_env() -> None: # Load environment variables at module import time _load_env() - FolderOption: partial[OptionInfo] = partial( typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path ) @@ -106,7 +108,38 @@ def is_branch_synced_with_remote(branch: str) -> bool: def is_ancestor(ancestor: str, descendent: str) -> bool: """Checks if the branch is synced with its remote.""" - return git("merge-base", "--is-ancestor", ancestor, descendent).returncode == 0 + try: + git("merge-base", "--is-ancestor", ancestor, descendent) + return True + except subprocess.CalledProcessError: + return False + + +def get_current_branch() -> str: + """Returns the current branch name.""" + return git("branch", "--show-current").stdout.strip() + + +def get_current_commit() -> str: + """Returns the current commit reference.""" + return git("rev-parse", "HEAD").stdout.strip() + + +def get_last_cruft_update_commit(demo_path: Path) -> str: + """Returns the commit id for the last time cruft update was ran.""" + existing_cruft_config: dict[str, Any] = _read_cruft_file(demo_path) + last_cookiecutter_commit: Optional[str] = existing_cruft_config.get("commit", None) + if last_cookiecutter_commit is None: + raise ValueError("Could not find last commit id used to generate demo.") + return last_cookiecutter_commit + + +def _read_cruft_file(project_path: Path) -> dict[str, Any]: + """Reads the cruft file for the project path provided and returns the results.""" + cruft_path: Path = get_cruft_file(project_dir_path=project_path) + cruft_text: str = cruft_path.read_text() + cruft_config: dict[str, Any] = json.loads(cruft_text) + return cruft_config @contextmanager