From b19963158f067ff8ae8073c1a1fd87eae112b278 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Wed, 5 Nov 2025 03:28:37 -0500 Subject: [PATCH 1/6] feat: swap to making a new feature branch in demo rather than working from develop always --- scripts/update-demo.py | 27 ++++++++++++++++++++++++++- scripts/util.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index 4d2ad30..5a88532 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,13 +28,28 @@ 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 + develop_branch: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH", "develop") + + current_branch: str = get_current_branch() + current_commit: str = get_current_commit() + + _validate_is_feature_branch(branch=current_branch) + 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) + + last_update_commit: str = get_last_cruft_update_commit(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}'." + ) + + git("checkout", "-b", current_branch) cruft.update( project_dir=demo_path, template_path=REPO_FOLDER, @@ -45,5 +64,11 @@ def update_demo( sys.exit(1) +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..d9e6b1a 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 @@ -17,6 +18,8 @@ import cruft import typer from cookiecutter.utils import work_in +from cruft._commands.utils.cruft import get_cruft_file +from cruft._commands.utils.cruft import json_dumps from dotenv import load_dotenv from typer.models import OptionInfo @@ -106,7 +109,34 @@ 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 + return git("merge-base", "--is-ancestor", ancestor, descendent, ignore_error=True) is not None + + +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 From 8df514fb91a8f6b6a506f8328eff591bb859ed10 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Wed, 5 Nov 2025 03:32:02 -0500 Subject: [PATCH 2/6] chore: small niceties for commit message and not trying a redundant branch creation --- scripts/update-demo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index 5a88532..4ae547f 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -49,14 +49,16 @@ def update_demo( f"'{current_commit}'." ) - git("checkout", "-b", current_branch) + if current_branch != develop_branch: + 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("commit", "-m", f"chore: {last_update_commit} -> {current_commit}", "--no-verify") git("push") except Exception as error: From 2d1d15b9f580a083d39b547a7360b3b3386488e8 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Wed, 5 Nov 2025 20:12:11 -0500 Subject: [PATCH 3/6] fix: swap is_ancestor to use its own error handling due to git merge-base --is-ancestor only showing through status --- scripts/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/util.py b/scripts/util.py index d9e6b1a..ff07a36 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -17,6 +17,7 @@ import cruft import typer +from bandit.plugins.injection_shell import subprocess_popen_with_shell_equals_true from cookiecutter.utils import work_in from cruft._commands.utils.cruft import get_cruft_file from cruft._commands.utils.cruft import json_dumps @@ -109,7 +110,11 @@ 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, ignore_error=True) is not None + try: + git("merge-base", "--is-ancestor", ancestor, descendent) + return True + except subprocess.CalledProcessError: + return False def get_current_branch() -> str: From f99c0b8b35a36aa8b8a06034f43d72d0db943836 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Wed, 5 Nov 2025 20:13:13 -0500 Subject: [PATCH 4/6] fix: remove accidentally added import --- scripts/util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/util.py b/scripts/util.py index ff07a36..ac30f5f 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -17,10 +17,9 @@ import cruft import typer -from bandit.plugins.injection_shell import subprocess_popen_with_shell_equals_true + from cookiecutter.utils import work_in from cruft._commands.utils.cruft import get_cruft_file -from cruft._commands.utils.cruft import json_dumps from dotenv import load_dotenv from typer.models import OptionInfo @@ -46,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 ) From 0ff3bcd7d76c63ddaf3087eb1687419fe389a603 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Wed, 5 Nov 2025 20:26:43 -0500 Subject: [PATCH 5/6] fix: add a few workarounds trying to get POC branching going before refactoring --- scripts/update-demo.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index 4ae547f..e9c539a 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -30,26 +30,23 @@ def update_demo( try: demo_name: str = get_demo_name(add_rust_extension=add_rust_extension) demo_path: Path = demos_cache_folder / demo_name - develop_branch: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH", "develop") current_branch: str = get_current_branch() current_commit: str = get_current_commit() _validate_is_feature_branch(branch=current_branch) - 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) + last_update_commit: str = _get_last_demo_develop_cruft_update(demo_path=demo_path) - last_update_commit: str = get_last_cruft_update_commit(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}'." - ) + 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}'." + ) - if current_branch != develop_branch: + typer.secho(f"Updating demo project at {demo_path=}.", fg="yellow") + with work_in(demo_path): + if current_branch != "develop": git("checkout", "-b", current_branch) cruft.update( @@ -66,6 +63,20 @@ def update_demo( 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/"): From f5aebbec954eef60243c466695b0be656abcaff2 Mon Sep 17 00:00:00 2001 From: Kyle Oliver Date: Thu, 6 Nov 2025 04:24:32 -0500 Subject: [PATCH 6/6] fix: ensure that pushing a new branch for the first time works --- scripts/update-demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-demo.py b/scripts/update-demo.py index e9c539a..55386fb 100644 --- a/scripts/update-demo.py +++ b/scripts/update-demo.py @@ -56,7 +56,7 @@ def update_demo( ) git("add", ".") git("commit", "-m", f"chore: {last_update_commit} -> {current_commit}", "--no-verify") - git("push") + git("push", "-u", "origin", current_branch) except Exception as error: typer.secho(f"error: {error}", fg="red")