From fe9fc047ff6d91edc853777687e3bbc07ad1943a Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 11 Dec 2025 09:27:21 +0100 Subject: [PATCH 1/2] - Writing custom git flow scripts - a start --- scripts/__init__.py | 0 scripts/git/__init__.py | 0 scripts/git/commands/__init__.py | 0 scripts/git/commands/bugfix.py | 70 ++++++++++++++++ scripts/git/commands/feature.py | 71 ++++++++++++++++ scripts/git/commands/hotfix.py | 108 +++++++++++++++++++++++++ scripts/git/commands/release.py | 107 ++++++++++++++++++++++++ scripts/git/commands/status.py | 30 +++++++ scripts/git/core/__init__.py | 0 scripts/git/core/config.py | 34 ++++++++ scripts/git/core/git_api.py | 94 +++++++++++++++++++++ scripts/git/core/output.py | 61 ++++++++++++++ scripts/git/gitflow | 10 +++ scripts/git/gitflow.py | 135 +++++++++++++++++++++++++++++++ 14 files changed, 720 insertions(+) create mode 100644 scripts/__init__.py create mode 100644 scripts/git/__init__.py create mode 100644 scripts/git/commands/__init__.py create mode 100644 scripts/git/commands/bugfix.py create mode 100644 scripts/git/commands/feature.py create mode 100644 scripts/git/commands/hotfix.py create mode 100644 scripts/git/commands/release.py create mode 100644 scripts/git/commands/status.py create mode 100644 scripts/git/core/__init__.py create mode 100644 scripts/git/core/config.py create mode 100644 scripts/git/core/git_api.py create mode 100644 scripts/git/core/output.py create mode 100755 scripts/git/gitflow create mode 100644 scripts/git/gitflow.py diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/git/__init__.py b/scripts/git/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/git/commands/__init__.py b/scripts/git/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/git/commands/bugfix.py b/scripts/git/commands/bugfix.py new file mode 100644 index 0000000..a13be64 --- /dev/null +++ b/scripts/git/commands/bugfix.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from ..core import config, git_api, output + + +def _bugfix_branch_name(name: str) -> str: + return f"{config.CONFIG.bugfix_prefix}{name}" + + +def handle_bugfix_start(args) -> int: + name: str = args.name + cfg = config.CONFIG + + try: + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + branch_name = _bugfix_branch_name(name) + output.info(f"Aanmaken van bugfix branch '{branch_name}' vanaf '{cfg.develop_branch}'") + git_api.create_branch(branch_name, cfg.develop_branch) + output.success(f"Bugfix branch '{branch_name}' is aangemaakt en gecheckt out.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + + +def handle_bugfix_finish(args) -> int: + cfg = config.CONFIG + name: str | None = args.name + + try: + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + + if name is None: + current = git_api.get_current_branch() + if not current.startswith(cfg.bugfix_prefix): + raise git_api.GitError( + "Je zit niet op een bugfix branch. Geef de bugfix-naam expliciet door (zonder prefix)." + ) + bugfix_branch = current + else: + bugfix_branch = _bugfix_branch_name(name) + + git_api.ensure_not_behind_remote(bugfix_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + output.info(f"Mergen van '{bugfix_branch}' naar '{cfg.develop_branch}'") + git_api.checkout_branch(cfg.develop_branch) + + merge_args = ["merge"] + if cfg.use_no_ff_for_feature: + merge_args.append("--no-ff") + merge_args.append(bugfix_branch) + + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + output.success(f"Bugfix branch '{bugfix_branch}' is gemerged naar '{cfg.develop_branch}'.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + diff --git a/scripts/git/commands/feature.py b/scripts/git/commands/feature.py new file mode 100644 index 0000000..5cfdaf3 --- /dev/null +++ b/scripts/git/commands/feature.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from ..core import config, git_api, output + + +def _feature_branch_name(name: str) -> str: + return f"{config.CONFIG.feature_prefix}{name}" + + +def handle_feature_start(args) -> int: + name: str = args.name + cfg = config.CONFIG + + try: + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + branch_name = _feature_branch_name(name) + output.info(f"Aanmaken van feature branch '{branch_name}' vanaf '{cfg.develop_branch}'") + git_api.create_branch(branch_name, cfg.develop_branch) + output.success(f"Feature branch '{branch_name}' is aangemaakt en gecheckt out.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + + +def handle_feature_finish(args) -> int: + cfg = config.CONFIG + name: str | None = args.name + + try: + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + + if name is None: + current = git_api.get_current_branch() + if not current.startswith(cfg.feature_prefix): + raise git_api.GitError( + "Je zit niet op een feature branch. Geef de feature-naam expliciet door (zonder prefix)." + ) + feature_branch = current + else: + feature_branch = _feature_branch_name(name) + + # Zorg dat betrokken branches niet achterlopen op remote + git_api.ensure_not_behind_remote(feature_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + output.info(f"Mergen van '{feature_branch}' naar '{cfg.develop_branch}'") + git_api.checkout_branch(cfg.develop_branch) + + merge_args = ["merge"] + if cfg.use_no_ff_for_feature: + merge_args.append("--no-ff") + merge_args.append(feature_branch) + + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + output.success(f"Feature branch '{feature_branch}' is gemerged naar '{cfg.develop_branch}'.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + diff --git a/scripts/git/commands/hotfix.py b/scripts/git/commands/hotfix.py new file mode 100644 index 0000000..d1eba01 --- /dev/null +++ b/scripts/git/commands/hotfix.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from ..core import config, git_api, output + + +def _hotfix_branch_name(name: str) -> str: + return f"{config.CONFIG.hotfix_prefix}{name}" + + +def _ensure_version(name: str | None) -> str: + # Voor hotfix kunnen we dezelfde prompt gebruiken indien geen naam/versie is opgegeven + if name: + return name + output.info("Geen hotfix-naam/versie opgegeven. Gelieve een identificatie in te geven (bijv. 1.2.1 of short-name):") + entered = input("Hotfix: ").strip() + if not entered: + raise git_api.GitError("Geen hotfix-naam/versie opgegeven.") + return entered + + +def handle_hotfix_start(args) -> int: + cfg = config.CONFIG + try: + raw_name: str = args.name + name = _ensure_version(raw_name) + branch_name = _hotfix_branch_name(name) + + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.main_branch, cfg.remote_name) + + output.info(f"Aanmaken van hotfix branch '{branch_name}' vanaf '{cfg.main_branch}'") + git_api.create_branch(branch_name, cfg.main_branch) + output.success(f"Hotfix branch '{branch_name}' is aangemaakt en gecheckt out.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + + +def handle_hotfix_finish(args) -> int: + cfg = config.CONFIG + try: + name_arg = getattr(args, "name", None) + + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + + if name_arg: + name = name_arg + hotfix_branch = _hotfix_branch_name(name) + else: + current = git_api.get_current_branch() + prefix = cfg.hotfix_prefix + if current.startswith(prefix): + name = current[len(prefix) :] + hotfix_branch = current + else: + name = _ensure_version(None) + hotfix_branch = _hotfix_branch_name(name) + + git_api.ensure_not_behind_remote(hotfix_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.main_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + # Voor nu vragen we ook hier de versie/naam en gebruiken die voor de tag. + # In een latere iteratie kunnen we hier automatische patch-bumping doen. + version_for_tag = name + tag_name = cfg.tag_format.format(version=version_for_tag) + + # Merge naar main + output.info(f"Mergen van '{hotfix_branch}' naar '{cfg.main_branch}' en tag '{tag_name}' aanmaken") + git_api.checkout_branch(cfg.main_branch) + merge_args = ["merge"] + if cfg.use_no_ff_for_hotfix: + merge_args.append("--no-ff") + merge_args.append(hotfix_branch) + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge van hotfix naar main is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + git_api._run_git(["tag", tag_name]) # type: ignore[attr-defined] + + # Merge naar develop + output.info(f"Mergen van '{hotfix_branch}' naar '{cfg.develop_branch}'") + git_api.checkout_branch(cfg.develop_branch) + merge_args = ["merge"] + if cfg.use_no_ff_for_hotfix: + merge_args.append("--no-ff") + merge_args.append(hotfix_branch) + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge van hotfix naar develop is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + output.success( + f"Hotfix '{name}' is voltooid: gemerged naar '{cfg.main_branch}' en '{cfg.develop_branch}' en getagd als '{tag_name}'." + ) + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + diff --git a/scripts/git/commands/release.py b/scripts/git/commands/release.py new file mode 100644 index 0000000..cf40f0d --- /dev/null +++ b/scripts/git/commands/release.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from ..core import config, git_api, output + + +def _release_branch_name(version: str) -> str: + return f"{config.CONFIG.release_prefix}{version}" + + +def _ensure_version(version: str | None) -> str: + if version: + return version + # Eenvoudige interactieve prompt; later kunnen we validatie uitbreiden + output.info("Geen versie opgegeven. Gelieve een versie in te geven (bijv. 1.2.0):") + entered = input("Versie: ").strip() + if not entered: + raise git_api.GitError("Geen versie opgegeven.") + return entered + + +def handle_release_start(args) -> int: + cfg = config.CONFIG + try: + version = _ensure_version(getattr(args, "version", None)) + branch_name = _release_branch_name(version) + + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + output.info(f"Aanmaken van release branch '{branch_name}' vanaf '{cfg.develop_branch}'") + git_api.create_branch(branch_name, cfg.develop_branch) + output.success(f"Release branch '{branch_name}' is aangemaakt en gecheckt out.") + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + + +def handle_release_finish(args) -> int: + cfg = config.CONFIG + try: + version_arg = getattr(args, "version", None) + git_api.ensure_clean_working_tree() + git_api.fetch_remote(cfg.remote_name) + + if version_arg: + version = version_arg + release_branch = _release_branch_name(version) + else: + # Probeer huidige branch te gebruiken + current = git_api.get_current_branch() + prefix = cfg.release_prefix + if current.startswith(prefix): + version = current[len(prefix) :] + release_branch = current + else: + # Geen logische branch, vraag versie interactief + version = _ensure_version(None) + release_branch = _release_branch_name(version) + + # Zorg dat betrokken branches niet achterlopen + git_api.ensure_not_behind_remote(release_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.main_branch, cfg.remote_name) + git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) + + tag_name = cfg.tag_format.format(version=version) + + # Merge naar main + output.info(f"Mergen van '{release_branch}' naar '{cfg.main_branch}' en tag '{tag_name}' aanmaken") + git_api.checkout_branch(cfg.main_branch) + merge_args = ["merge"] + if cfg.use_no_ff_for_release: + merge_args.append("--no-ff") + merge_args.append(release_branch) + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge naar main is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + # Tag aanmaken op main + git_api._run_git(["tag", tag_name]) # type: ignore[attr-defined] + + # Merge terug naar develop + output.info(f"Mergen van '{release_branch}' terug naar '{cfg.develop_branch}'") + git_api.checkout_branch(cfg.develop_branch) + merge_args = ["merge"] + if cfg.use_no_ff_for_release: + merge_args.append("--no-ff") + merge_args.append(release_branch) + try: + git_api._run_git(merge_args) # type: ignore[attr-defined] + except git_api.GitError as exc: + raise git_api.GitError( + "Merge naar develop is mislukt (mogelijk conflicten). Los de conflicten op en voltooi de merge handmatig." + ) from exc + + output.success( + f"Release '{version}' is voltooid: gemerged naar '{cfg.main_branch}' en '{cfg.develop_branch}' en getagd als '{tag_name}'." + ) + return 0 + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + diff --git a/scripts/git/commands/status.py b/scripts/git/commands/status.py new file mode 100644 index 0000000..ad84812 --- /dev/null +++ b/scripts/git/commands/status.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from ..core import config, git_api, output + + +def handle_status(args) -> int: # noqa: ARG001 - argparse API + """Toon huidige branch en eenvoudige status-info.""" + + try: + branch = git_api.get_current_branch() + except git_api.GitError as exc: + output.error(str(exc)) + return 1 + + clean = git_api.is_clean_working_tree() + + output.heading("Repo status") + output.plain(f"Huidige branch : {branch}") + output.plain(f"Working tree : {'clean' if clean else 'NIET clean'}") + + # Optionele remote-checks + cfg = config.CONFIG + for important_branch in {cfg.main_branch, cfg.develop_branch, branch}: + try: + git_api.ensure_not_behind_remote(important_branch, cfg.remote_name) + except git_api.GitError as exc: + output.warning(str(exc)) + + return 0 + diff --git a/scripts/git/core/__init__.py b/scripts/git/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/git/core/config.py b/scripts/git/core/config.py new file mode 100644 index 0000000..a133f9b --- /dev/null +++ b/scripts/git/core/config.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class GitFlowConfig: + main_branch: str = "main" + develop_branch: str = "develop" + remote_name: str = "origin" + feature_prefix: str = "feature/" + bugfix_prefix: str = "bugfix/" + hotfix_prefix: str = "hotfix/" + release_prefix: str = "release/" + tag_format: str = "v{version}" + # Merge-strategie kan later per actie configureerbaar worden + use_no_ff_for_feature: bool = True + use_no_ff_for_release: bool = True + use_no_ff_for_hotfix: bool = True + + +CONFIG = GitFlowConfig() + + +def load_config() -> None: + """Laad configuratie. + + Voor nu gebruiken we enkel harde defaults. Later kunnen we hier + een bestand (bv. yaml/toml) inlezen en `CONFIG` overschrijven. + """ + + # TODO: optioneel configuratiebestand ondersteunen. + return None + diff --git a/scripts/git/core/git_api.py b/scripts/git/core/git_api.py new file mode 100644 index 0000000..ed074b6 --- /dev/null +++ b/scripts/git/core/git_api.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import subprocess +from typing import Iterable + +from . import output + + +class GitError(RuntimeError): + pass + + +def _run_git(args: Iterable[str], *, capture_output: bool = False, check: bool = True) -> subprocess.CompletedProcess: + cmd = ["git", *args] + result = subprocess.run( + cmd, + text=True, + capture_output=capture_output, + check=False, + ) + if check and result.returncode != 0: + stderr = result.stderr.strip() if result.stderr else "" + raise GitError(f"git {' '.join(args)} failed ({result.returncode}): {stderr}") + return result + + +def get_current_branch() -> str: + result = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], capture_output=True) + return (result.stdout or "").strip() + + +def is_clean_working_tree() -> bool: + result = _run_git(["status", "--porcelain"], capture_output=True) + return (result.stdout or "").strip() == "" + + +def ensure_clean_working_tree() -> None: + if not is_clean_working_tree(): + raise GitError( + "Working tree is niet clean. Commit of stash je wijzigingen voor je deze actie uitvoert." + ) + + +def ensure_branch_exists(branch: str) -> None: + try: + _run_git(["show-ref", "--verify", f"refs/heads/{branch}"], capture_output=True) + except GitError as exc: + raise GitError(f"Branch '{branch}' bestaat niet lokaal.") from exc + + +def checkout_branch(branch: str) -> None: + _run_git(["checkout", branch]) + + +def create_branch(branch: str, base: str) -> None: + _run_git(["checkout", base]) + _run_git(["checkout", "-b", branch]) + + +def fetch_remote(remote: str) -> None: + _run_git(["fetch", remote]) + + +def ensure_not_behind_remote(branch: str, remote: str) -> None: + """Controleer of branch niet achterloopt op remote. + + We gebruiken `git rev-list --left-right --count local...remote` om + ahead/behind te bepalen. + """ + + remote_ref = f"{remote}/{branch}" + try: + result = _run_git( + ["rev-list", "--left-right", "--count", f"{branch}...{remote_ref}"], + capture_output=True, + ) + except GitError: + # Geen tracking remote; in die gevallen doen we geen check. + return + + output_str = (result.stdout or "").strip() + if not output_str: + return + + ahead_str, behind_str = output_str.split() + ahead = int(ahead_str) + behind = int(behind_str) + + if behind > 0: + raise GitError( + f"Branch '{branch}' loopt {behind} commit(s) achter op {remote_ref}. " + f"Doe eerst een 'git pull --ff-only' of werk te wijzigingen lokaal bij." + ) + diff --git a/scripts/git/core/output.py b/scripts/git/core/output.py new file mode 100644 index 0000000..4862195 --- /dev/null +++ b/scripts/git/core/output.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import sys + + +class _Colors: + RESET = "\033[0m" + BOLD = "\033[1m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + + +def _print(message: str, *, prefix: str = "", color: str | None = None, stream=None) -> None: + if stream is None: + stream = sys.stdout + text = f"{prefix} {message}" if prefix else message + if color: + text = f"{color}{text}{_Colors.RESET}" + print(text, file=stream) + + +def info(message: str) -> None: + _print(message, prefix="ℹ️", color=_Colors.BLUE) + + +def success(message: str) -> None: + _print(message, prefix="✅", color=_Colors.GREEN) + + +def warning(message: str) -> None: + _print(message, prefix="⚠️", color=_Colors.YELLOW, stream=sys.stderr) + + +def error(message: str) -> None: + _print(message, prefix="❌", color=_Colors.RED, stream=sys.stderr) + + +def heading(message: str) -> None: + _print(message, prefix="▶", color=_Colors.BOLD) + + +def plain(message: str) -> None: + _print(message) + + +class Notifier: + """Abstractielaag voor toekomstige auditieve output. + + Voor nu enkel console-notificaties; later kan dit uitgebreid + worden met TTS, systeemmeldingen, ... + """ + + @staticmethod + def notify_event(event: str, detail: str | None = None) -> None: # pragma: no cover - placeholder + if detail: + info(f"[{event}] {detail}") + else: + info(f"[{event}]") + diff --git a/scripts/git/gitflow b/scripts/git/gitflow new file mode 100755 index 0000000..e7936e6 --- /dev/null +++ b/scripts/git/gitflow @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Kleine wrapper zodat je gewoon `scripts/git/gitflow ...` kunt aanroepen +# zonder expliciet `python` te typen. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +cd "${PROJECT_ROOT}" || exit 1 +exec python -m scripts.git.gitflow "$@" diff --git a/scripts/git/gitflow.py b/scripts/git/gitflow.py new file mode 100644 index 0000000..7115cec --- /dev/null +++ b/scripts/git/gitflow.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +"""Git Flow helper CLI for this repository. + +Eerste versie: +- status +- feature start/finish +- bugfix start/finish + +Andere flows (release/hotfix, hooks, enz.) volgen later. +""" + +from __future__ import annotations + +import argparse +import sys + +from .core import config as cfg +from .core import output +from .commands import status as status_cmd +from .commands import feature as feature_cmd +from .commands import bugfix as bugfix_cmd +from .commands import release as release_cmd +from .commands import hotfix as hotfix_cmd + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="gitflow", + description="Git Flow helper voor deze repo", + ) + + subparsers = parser.add_subparsers(dest="command", required=True) + + # status + status_parser = subparsers.add_parser("status", help="Toon huidige branch en repo-status") + status_parser.set_defaults(func=status_cmd.handle_status) + + # feature + feature_parser = subparsers.add_parser("feature", help="Feature branches beheren") + feature_sub = feature_parser.add_subparsers(dest="action", required=True) + + feature_start = feature_sub.add_parser("start", help="Start een nieuwe feature branch vanaf develop") + feature_start.add_argument("name", help="Naam van de feature (zonder prefix)") + feature_start.set_defaults(func=feature_cmd.handle_feature_start) + + feature_finish = feature_sub.add_parser("finish", help="Merge een feature branch terug naar develop") + feature_finish.add_argument( + "name", + nargs="?", + help="Naam van de feature (zonder prefix). Laat leeg om huidige branch te gebruiken.", + ) + feature_finish.set_defaults(func=feature_cmd.handle_feature_finish) + + # bugfix + bugfix_parser = subparsers.add_parser("bugfix", help="Bugfix branches beheren (op develop)") + bugfix_sub = bugfix_parser.add_subparsers(dest="action", required=True) + + bugfix_start = bugfix_sub.add_parser("start", help="Start een nieuwe bugfix branch vanaf develop") + bugfix_start.add_argument("name", help="Naam van de bugfix (zonder prefix)") + bugfix_start.set_defaults(func=bugfix_cmd.handle_bugfix_start) + + bugfix_finish = bugfix_sub.add_parser("finish", help="Merge een bugfix branch terug naar develop") + bugfix_finish.add_argument( + "name", + nargs="?", + help="Naam van de bugfix (zonder prefix). Laat leeg om huidige branch te gebruiken.", + ) + bugfix_finish.set_defaults(func=bugfix_cmd.handle_bugfix_finish) + + # release + release_parser = subparsers.add_parser("release", help="Release branches beheren (main <-> develop)") + release_sub = release_parser.add_subparsers(dest="action", required=True) + + release_start = release_sub.add_parser("start", help="Start een nieuwe release branch vanaf develop") + release_start.add_argument( + "version", + nargs="?", + help="Versienummer (bijv. 1.2.0). Laat leeg om interactief in te geven.", + ) + release_start.set_defaults(func=release_cmd.handle_release_start) + + release_finish = release_sub.add_parser("finish", help="Voltooi een release: merge naar main en develop + tag") + release_finish.add_argument( + "version", + nargs="?", + help="Versienummer (bijv. 1.2.0). Laat leeg om af te leiden van de huidige branch of interactief te vragen.", + ) + release_finish.set_defaults(func=release_cmd.handle_release_finish) + + # hotfix + hotfix_parser = subparsers.add_parser("hotfix", help="Hotfix branches beheren (vanaf main)") + hotfix_sub = hotfix_parser.add_subparsers(dest="action", required=True) + + hotfix_start = hotfix_sub.add_parser("start", help="Start een nieuwe hotfix branch vanaf main") + hotfix_start.add_argument("name", help="Naam of versie van de hotfix (zonder prefix)") + hotfix_start.set_defaults(func=hotfix_cmd.handle_hotfix_start) + + hotfix_finish = hotfix_sub.add_parser("finish", help="Voltooi een hotfix: merge naar main en develop + tag") + hotfix_finish.add_argument( + "name", + nargs="?", + help="Naam of versie van de hotfix (zonder prefix). Laat leeg om huidige branch te gebruiken.", + ) + hotfix_finish.set_defaults(func=hotfix_cmd.handle_hotfix_finish) + + return parser + + +def main(argv: list[str] | None = None) -> int: + if argv is None: + argv = sys.argv[1:] + + cfg.load_config() # voor nu alleen defaults, later evt. bestand + + parser = _build_parser() + + try: + args = parser.parse_args(argv) + except SystemExit as exc: # argparse gebruikt SystemExit + return exc.code + + func = getattr(args, "func", None) + if func is None: + parser.print_help() + return 1 + + try: + return func(args) + except KeyboardInterrupt: + output.error("Afgebroken door gebruiker (Ctrl+C)") + return 130 + + +if __name__ == "__main__": # pragma: no cover - directe CLI entry + raise SystemExit(main()) From 2c8347c91b11b5482153a149b5d8a5ddae43be9f Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 11 Dec 2025 09:47:19 +0100 Subject: [PATCH 2/2] - Writing custom git flow scripts - finishing up without extensive testing --- scripts/git/commands/bugfix.py | 47 +++++++++++- scripts/git/commands/feature.py | 49 +++++++++++- scripts/git/commands/hotfix.py | 60 +++++++++++++-- scripts/git/commands/release.py | 39 +++++++++- scripts/git/core/config.py | 128 +++++++++++++++++++++++++++++++- scripts/git/core/git_api.py | 24 +++++- scripts/git/core/hooks.py | 43 +++++++++++ scripts/git/gitflow.py | 23 +++++- 8 files changed, 389 insertions(+), 24 deletions(-) create mode 100644 scripts/git/core/hooks.py diff --git a/scripts/git/commands/bugfix.py b/scripts/git/commands/bugfix.py index a13be64..21cac3b 100644 --- a/scripts/git/commands/bugfix.py +++ b/scripts/git/commands/bugfix.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core import config, git_api, output +from ..core import config, git_api, hooks, output def _bugfix_branch_name(name: str) -> str: @@ -20,6 +20,15 @@ def handle_bugfix_start(args) -> int: output.info(f"Aanmaken van bugfix branch '{branch_name}' vanaf '{cfg.develop_branch}'") git_api.create_branch(branch_name, cfg.develop_branch) output.success(f"Bugfix branch '{branch_name}' is aangemaakt en gecheckt out.") + + # Hooks na succesvol aanmaken van een bugfix branch + hooks.run_hooks( + "bugfix_start", + { + "branch": branch_name, + "base_branch": cfg.develop_branch, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) @@ -36,11 +45,24 @@ def handle_bugfix_finish(args) -> int: if name is None: current = git_api.get_current_branch() - if not current.startswith(cfg.bugfix_prefix): + if current.startswith(cfg.bugfix_prefix): + bugfix_branch = current + else: + branches = git_api.list_local_branches_with_prefix(cfg.bugfix_prefix) + if not branches: + raise git_api.GitError( + "Er zijn geen lokale bugfix branches gevonden. " + "Maak eerst een bugfix branch aan of geef een naam op." + ) + + output.heading("Beschikbare bugfix branches") + for b in branches: + output.plain(f"- {b}") + raise git_api.GitError( - "Je zit niet op een bugfix branch. Geef de bugfix-naam expliciet door (zonder prefix)." + "Je zit niet op een bugfix branch. Kies een van de bovenstaande namen " + "en voer het commando opnieuw uit, bv.: gitflow bugfix finish ." ) - bugfix_branch = current else: bugfix_branch = _bugfix_branch_name(name) @@ -63,6 +85,23 @@ def handle_bugfix_finish(args) -> int: ) from exc output.success(f"Bugfix branch '{bugfix_branch}' is gemerged naar '{cfg.develop_branch}'.") + + # Optionele cleanup + if cfg.delete_bugfix_after_finish: + output.info(f"Opruimen van lokale bugfix branch '{bugfix_branch}'") + try: + git_api._run_git(["branch", "-d", bugfix_branch]) # type: ignore[attr-defined] + except git_api.GitError as exc: + output.warning(f"Kon bugfix branch '{bugfix_branch}' niet verwijderen: {exc}") + + # Hooks na succesvolle bugfix-finish + hooks.run_hooks( + "bugfix_finish", + { + "branch": bugfix_branch, + "base_branch": cfg.develop_branch, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) diff --git a/scripts/git/commands/feature.py b/scripts/git/commands/feature.py index 5cfdaf3..ba68720 100644 --- a/scripts/git/commands/feature.py +++ b/scripts/git/commands/feature.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core import config, git_api, output +from ..core import config, git_api, hooks, output def _feature_branch_name(name: str) -> str: @@ -20,6 +20,15 @@ def handle_feature_start(args) -> int: output.info(f"Aanmaken van feature branch '{branch_name}' vanaf '{cfg.develop_branch}'") git_api.create_branch(branch_name, cfg.develop_branch) output.success(f"Feature branch '{branch_name}' is aangemaakt en gecheckt out.") + + # Hooks na succesvol aanmaken van een feature branch + hooks.run_hooks( + "feature_start", + { + "branch": branch_name, + "base_branch": cfg.develop_branch, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) @@ -36,11 +45,26 @@ def handle_feature_finish(args) -> int: if name is None: current = git_api.get_current_branch() - if not current.startswith(cfg.feature_prefix): + if current.startswith(cfg.feature_prefix): + feature_branch = current + else: + # Geen naam en we zitten niet op een feature-branch: toon een lijst + # met beschikbare feature-branches om de gebruiker te helpen kiezen. + branches = git_api.list_local_branches_with_prefix(cfg.feature_prefix) + if not branches: + raise git_api.GitError( + "Er zijn geen lokale feature branches gevonden. " + "Maak eerst een feature branch aan of geef een naam op." + ) + + output.heading("Beschikbare feature branches") + for b in branches: + output.plain(f"- {b}") + raise git_api.GitError( - "Je zit niet op een feature branch. Geef de feature-naam expliciet door (zonder prefix)." + "Je zit niet op een feature branch. Kies een van de bovenstaande namen " + "en voer het commando opnieuw uit, bv.: gitflow feature finish ." ) - feature_branch = current else: feature_branch = _feature_branch_name(name) @@ -64,6 +88,23 @@ def handle_feature_finish(args) -> int: ) from exc output.success(f"Feature branch '{feature_branch}' is gemerged naar '{cfg.develop_branch}'.") + + # Optionele cleanup van de feature branch + if cfg.delete_feature_after_finish: + output.info(f"Opruimen van lokale feature branch '{feature_branch}'") + try: + git_api._run_git(["branch", "-d", feature_branch]) # type: ignore[attr-defined] + except git_api.GitError as exc: + output.warning(f"Kon feature branch '{feature_branch}' niet verwijderen: {exc}") + + # Hooks na succesvolle feature-finish + hooks.run_hooks( + "feature_finish", + { + "branch": feature_branch, + "base_branch": cfg.develop_branch, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) diff --git a/scripts/git/commands/hotfix.py b/scripts/git/commands/hotfix.py index d1eba01..352f845 100644 --- a/scripts/git/commands/hotfix.py +++ b/scripts/git/commands/hotfix.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core import config, git_api, output +from ..core import config, git_api, hooks, output def _hotfix_branch_name(name: str) -> str: @@ -32,6 +32,16 @@ def handle_hotfix_start(args) -> int: output.info(f"Aanmaken van hotfix branch '{branch_name}' vanaf '{cfg.main_branch}'") git_api.create_branch(branch_name, cfg.main_branch) output.success(f"Hotfix branch '{branch_name}' is aangemaakt en gecheckt out.") + + # Hooks na succesvol aanmaken van een hotfix branch + hooks.run_hooks( + "hotfix_start", + { + "branch": branch_name, + "base_branch": cfg.main_branch, + "version": name, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) @@ -42,6 +52,7 @@ def handle_hotfix_finish(args) -> int: cfg = config.CONFIG try: name_arg = getattr(args, "name", None) + env_arg = getattr(args, "env", None) git_api.ensure_clean_working_tree() git_api.fetch_remote(cfg.remote_name) @@ -56,17 +67,32 @@ def handle_hotfix_finish(args) -> int: name = current[len(prefix) :] hotfix_branch = current else: - name = _ensure_version(None) - hotfix_branch = _hotfix_branch_name(name) + # Toon lijst van beschikbare hotfix branches + branches = git_api.list_local_branches_with_prefix(cfg.hotfix_prefix) + if not branches: + raise git_api.GitError( + "Er zijn geen lokale hotfix branches gevonden. " + "Maak eerst een hotfix branch aan of geef een naam op." + ) + + output.heading("Beschikbare hotfix branches") + for b in branches: + output.plain(f"- {b}") + + raise git_api.GitError( + "Je zit niet op een hotfix branch. Kies een van de bovenstaande namen " + "en voer het commando opnieuw uit, bv.: gitflow hotfix finish ." + ) git_api.ensure_not_behind_remote(hotfix_branch, cfg.remote_name) git_api.ensure_not_behind_remote(cfg.main_branch, cfg.remote_name) git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) - # Voor nu vragen we ook hier de versie/naam en gebruiken die voor de tag. - # In een latere iteratie kunnen we hier automatische patch-bumping doen. - version_for_tag = name - tag_name = cfg.tag_format.format(version=version_for_tag) + # Bepaal omgeving en tagnaam + env = env_arg or cfg.hotfix_default_env + suffix = cfg.environments.get(env, "") + base_tag = cfg.tag_format.format(version=name) + tag_name = f"{base_tag}{suffix}" # Merge naar main output.info(f"Mergen van '{hotfix_branch}' naar '{cfg.main_branch}' en tag '{tag_name}' aanmaken") @@ -101,6 +127,26 @@ def handle_hotfix_finish(args) -> int: output.success( f"Hotfix '{name}' is voltooid: gemerged naar '{cfg.main_branch}' en '{cfg.develop_branch}' en getagd als '{tag_name}'." ) + + # Optionele cleanup + if cfg.delete_hotfix_after_finish: + output.info(f"Opruimen van lokale hotfix branch '{hotfix_branch}'") + try: + git_api._run_git(["branch", "-d", hotfix_branch]) # type: ignore[attr-defined] + except git_api.GitError as exc: + output.warning(f"Kon hotfix branch '{hotfix_branch}' niet verwijderen: {exc}") + + # Hooks na succesvolle hotfix-finish + hooks.run_hooks( + "hotfix_finish", + { + "branch": hotfix_branch, + "base_branch": cfg.main_branch, + "version": name, + "env": env, + "tag": tag_name, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) diff --git a/scripts/git/commands/release.py b/scripts/git/commands/release.py index cf40f0d..7da7cf0 100644 --- a/scripts/git/commands/release.py +++ b/scripts/git/commands/release.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core import config, git_api, output +from ..core import config, git_api, hooks, output def _release_branch_name(version: str) -> str: @@ -31,6 +31,16 @@ def handle_release_start(args) -> int: output.info(f"Aanmaken van release branch '{branch_name}' vanaf '{cfg.develop_branch}'") git_api.create_branch(branch_name, cfg.develop_branch) output.success(f"Release branch '{branch_name}' is aangemaakt en gecheckt out.") + + # Hooks na succesvol aanmaken van een release branch + hooks.run_hooks( + "release_start", + { + "branch": branch_name, + "base_branch": cfg.develop_branch, + "version": version, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) @@ -41,6 +51,7 @@ def handle_release_finish(args) -> int: cfg = config.CONFIG try: version_arg = getattr(args, "version", None) + env_arg = getattr(args, "env", None) git_api.ensure_clean_working_tree() git_api.fetch_remote(cfg.remote_name) @@ -64,7 +75,11 @@ def handle_release_finish(args) -> int: git_api.ensure_not_behind_remote(cfg.main_branch, cfg.remote_name) git_api.ensure_not_behind_remote(cfg.develop_branch, cfg.remote_name) - tag_name = cfg.tag_format.format(version=version) + # Bepaal omgeving en uiteindelijke tagnaam + env = env_arg or cfg.release_default_env + suffix = cfg.environments.get(env, "") + base_tag = cfg.tag_format.format(version=version) + tag_name = f"{base_tag}{suffix}" # Merge naar main output.info(f"Mergen van '{release_branch}' naar '{cfg.main_branch}' en tag '{tag_name}' aanmaken") @@ -100,6 +115,26 @@ def handle_release_finish(args) -> int: output.success( f"Release '{version}' is voltooid: gemerged naar '{cfg.main_branch}' en '{cfg.develop_branch}' en getagd als '{tag_name}'." ) + + # Optionele cleanup van release branch + if cfg.delete_release_after_finish: + output.info(f"Opruimen van lokale release branch '{release_branch}'") + try: + git_api._run_git(["branch", "-d", release_branch]) # type: ignore[attr-defined] + except git_api.GitError as exc: + output.warning(f"Kon release branch '{release_branch}' niet verwijderen: {exc}") + + # Hooks na succesvolle release-finish + hooks.run_hooks( + "release_finish", + { + "branch": release_branch, + "base_branch": cfg.main_branch, + "version": version, + "env": env, + "tag": tag_name, + }, + ) return 0 except git_api.GitError as exc: output.error(str(exc)) diff --git a/scripts/git/core/config.py b/scripts/git/core/config.py index a133f9b..f4c5113 100644 --- a/scripts/git/core/config.py +++ b/scripts/git/core/config.py @@ -1,6 +1,15 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List + +import os + +try: # yaml is optioneel; bij ontbreken vallen we terug op defaults + import yaml # type: ignore[import] +except Exception: # pragma: no cover - defensieve fallback + yaml = None @dataclass @@ -17,6 +26,29 @@ class GitFlowConfig: use_no_ff_for_feature: bool = True use_no_ff_for_release: bool = True use_no_ff_for_hotfix: bool = True + # Globale dry-run vlag: wanneer True worden muterende git-acties niet + # echt uitgevoerd, maar enkel gelogd. + dry_run: bool = False + + # Hooks per event (feature_start, feature_finish, ...) + hooks: Dict[str, List[str]] = field(default_factory=dict) + + # Cleanup-instellingen: lokale branches verwijderen na succesvolle finish + delete_feature_after_finish: bool = False + delete_bugfix_after_finish: bool = False + delete_release_after_finish: bool = False + delete_hotfix_after_finish: bool = False + + # Omgevingen voor tagging (suffixen) en defaults + environments: Dict[str, str] = field( + default_factory=lambda: { + "test": "-test", + "staging": "-staging", + "production": "", + } + ) + release_default_env: str = "production" + hotfix_default_env: str = "production" CONFIG = GitFlowConfig() @@ -25,10 +57,98 @@ CONFIG = GitFlowConfig() def load_config() -> None: """Laad configuratie. - Voor nu gebruiken we enkel harde defaults. Later kunnen we hier - een bestand (bv. yaml/toml) inlezen en `CONFIG` overschrijven. + We lezen optioneel `scripts/git/gitflow.yaml` in en overschrijven + velden in `CONFIG` op basis van de inhoud. Als het bestand of de + yaml-lib ontbreekt, blijven de defaults gelden. """ - # TODO: optioneel configuratiebestand ondersteunen. + if yaml is None: + return None + + # Zoek het config-bestand relatief t.o.v. de huidige werkdirectory. + # We gaan ervan uit dat gitflow vanuit de projectroot draait (wrapper). + cfg_path = Path("scripts") / "git" / "gitflow.yaml" + if not cfg_path.is_file(): + return None + + with cfg_path.open("r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} + + # Eenvoudige mapping: alleen bekende keys worden overgenomen. + if not isinstance(data, dict): + return None + + # Basisvelden + CONFIG.main_branch = str(data.get("main_branch", CONFIG.main_branch)) + CONFIG.develop_branch = str(data.get("develop_branch", CONFIG.develop_branch)) + CONFIG.remote_name = str(data.get("remote_name", CONFIG.remote_name)) + + # Prefixes + prefixes = data.get("branch_prefixes") or {} + if isinstance(prefixes, dict): + CONFIG.feature_prefix = str(prefixes.get("feature", CONFIG.feature_prefix)) + CONFIG.bugfix_prefix = str(prefixes.get("bugfix", CONFIG.bugfix_prefix)) + CONFIG.hotfix_prefix = str(prefixes.get("hotfix", CONFIG.hotfix_prefix)) + CONFIG.release_prefix = str(prefixes.get("release", CONFIG.release_prefix)) + + # Merge-strategieën + strategies = data.get("merge_strategies") or {} + if isinstance(strategies, dict): + CONFIG.use_no_ff_for_feature = strategies.get( + "feature_finish", "no-ff" if CONFIG.use_no_ff_for_feature else "ff" + ) == "no-ff" + CONFIG.use_no_ff_for_release = strategies.get( + "release_finish", "no-ff" if CONFIG.use_no_ff_for_release else "ff" + ) == "no-ff" + CONFIG.use_no_ff_for_hotfix = strategies.get( + "hotfix_finish", "no-ff" if CONFIG.use_no_ff_for_hotfix else "ff" + ) == "no-ff" + + # Tag-format + if "tag_format" in data: + CONFIG.tag_format = str(data["tag_format"]) + + # Cleanup-instellingen + cleanup = data.get("cleanup") or {} + if isinstance(cleanup, dict): + CONFIG.delete_feature_after_finish = bool( + cleanup.get("delete_feature_after_finish", CONFIG.delete_feature_after_finish) + ) + CONFIG.delete_bugfix_after_finish = bool( + cleanup.get("delete_bugfix_after_finish", CONFIG.delete_bugfix_after_finish) + ) + CONFIG.delete_release_after_finish = bool( + cleanup.get("delete_release_after_finish", CONFIG.delete_release_after_finish) + ) + CONFIG.delete_hotfix_after_finish = bool( + cleanup.get("delete_hotfix_after_finish", CONFIG.delete_hotfix_after_finish) + ) + + # Hooks + hooks = data.get("hooks") or {} + if isinstance(hooks, dict): + normalized: Dict[str, List[str]] = {} + for key, value in hooks.items(): + if isinstance(value, list): + normalized[key] = [str(cmd) for cmd in value] + elif isinstance(value, str): + normalized[key] = [value] + CONFIG.hooks = normalized + + # Omgevingen en defaults + envs = data.get("environments") or {} + if isinstance(envs, dict): + CONFIG.environments = {str(k): str(v) for k, v in envs.items()} + + if "release_default_env" in data: + CONFIG.release_default_env = str(data["release_default_env"]) + if "hotfix_default_env" in data: + CONFIG.hotfix_default_env = str(data["hotfix_default_env"]) + + # Dry-run kan ook via config gezet worden, maar CLI-flag heeft prioriteit; + # daarom overschrijven we hier niet expliciet als het al gezet is. + if "dry_run" in data and not CONFIG.dry_run: + CONFIG.dry_run = bool(data["dry_run"]) + return None diff --git a/scripts/git/core/git_api.py b/scripts/git/core/git_api.py index ed074b6..e7344e3 100644 --- a/scripts/git/core/git_api.py +++ b/scripts/git/core/git_api.py @@ -3,7 +3,7 @@ from __future__ import annotations import subprocess from typing import Iterable -from . import output +from . import config, output class GitError(RuntimeError): @@ -12,6 +12,16 @@ class GitError(RuntimeError): def _run_git(args: Iterable[str], *, capture_output: bool = False, check: bool = True) -> subprocess.CompletedProcess: cmd = ["git", *args] + # Dry-run ondersteuning: voer geen echte git-commando's uit, maar log enkel + # wat er zou gebeuren en geef een "geslaagde" CompletedProcess terug. + if config.CONFIG.dry_run: + output.info(f"[DRY-RUN] zou uitvoeren: {' '.join(cmd)}") + return subprocess.CompletedProcess( + cmd, + 0, + stdout="" if capture_output else None, + stderr="", + ) result = subprocess.run( cmd, text=True, @@ -92,3 +102,15 @@ def ensure_not_behind_remote(branch: str, remote: str) -> None: f"Doe eerst een 'git pull --ff-only' of werk te wijzigingen lokaal bij." ) + +def list_local_branches_with_prefix(prefix: str) -> list[str]: + """Geef een gesorteerde lijst van lokale branches die met het prefix starten.""" + + result = _run_git( + ["for-each-ref", "--format=%(refname:short)", "refs/heads"], + capture_output=True, + ) + lines = (result.stdout or "").splitlines() + branches = sorted(b for b in (ln.strip() for ln in lines) if b.startswith(prefix)) + return branches + diff --git a/scripts/git/core/hooks.py b/scripts/git/core/hooks.py new file mode 100644 index 0000000..4bd9f4e --- /dev/null +++ b/scripts/git/core/hooks.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import os +import shlex +import subprocess +from typing import Dict + +from . import config, output + + +def run_hooks(event: str, context: Dict[str, str] | None = None) -> None: + """Voer alle geconfigureerde hooks voor een gegeven event uit. + + Hooks worden gedefinieerd in `CONFIG.hooks[event]` als een lijst van + shell-commando's. We geven context door via environment-variabelen. + """ + + hooks = config.CONFIG.hooks.get(event) or [] + if not hooks: + return + + base_env = os.environ.copy() + base_env.setdefault("GITFLOW_EVENT", event) + + if context: + for key, value in context.items(): + base_env[f"GITFLOW_{key.upper()}"] = value + + for cmd in hooks: + # Eenvoudige logging + output.info(f"[HOOK {event}] {cmd}") + + if config.CONFIG.dry_run: + output.info("[DRY-RUN] Hook niet echt uitgevoerd.") + continue + + # Gebruik shlex.split zodat eenvoudige strings netjes opgesplitst worden. + args = shlex.split(cmd) + result = subprocess.run(args, env=base_env, text=True) + if result.returncode != 0: + raise RuntimeError( + f"Hook voor event '{event}' faalde met exitcode {result.returncode}: {cmd}" + ) diff --git a/scripts/git/gitflow.py b/scripts/git/gitflow.py index 7115cec..5fb7f94 100644 --- a/scripts/git/gitflow.py +++ b/scripts/git/gitflow.py @@ -29,6 +29,12 @@ def _build_parser() -> argparse.ArgumentParser: description="Git Flow helper voor deze repo", ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Toon welke git-commando's uitgevoerd zouden worden, zonder echt te veranderen", + ) + subparsers = parser.add_subparsers(dest="command", required=True) # status @@ -85,6 +91,11 @@ def _build_parser() -> argparse.ArgumentParser: nargs="?", help="Versienummer (bijv. 1.2.0). Laat leeg om af te leiden van de huidige branch of interactief te vragen.", ) + release_finish.add_argument( + "--env", + dest="env", + help="Omgeving voor tagging (bijv. test, staging, production). Laat leeg voor default uit config.", + ) release_finish.set_defaults(func=release_cmd.handle_release_finish) # hotfix @@ -101,6 +112,11 @@ def _build_parser() -> argparse.ArgumentParser: nargs="?", help="Naam of versie van de hotfix (zonder prefix). Laat leeg om huidige branch te gebruiken.", ) + hotfix_finish.add_argument( + "--env", + dest="env", + help="Omgeving voor tagging (bijv. test, staging, production). Laat leeg voor default uit config.", + ) hotfix_finish.set_defaults(func=hotfix_cmd.handle_hotfix_finish) return parser @@ -110,8 +126,6 @@ def main(argv: list[str] | None = None) -> int: if argv is None: argv = sys.argv[1:] - cfg.load_config() # voor nu alleen defaults, later evt. bestand - parser = _build_parser() try: @@ -119,6 +133,11 @@ def main(argv: list[str] | None = None) -> int: except SystemExit as exc: # argparse gebruikt SystemExit return exc.code + # Dry-run vlag doorgeven aan configuratie *voor* we commands uitvoeren + cfg.CONFIG.dry_run = bool(getattr(args, "dry_run", False)) + + cfg.load_config() # later kan dit config-bestand inladen en overrides toepassen + func = getattr(args, "func", None) if func is None: parser.print_help()