From fe9fc047ff6d91edc853777687e3bbc07ad1943a Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 11 Dec 2025 09:27:21 +0100 Subject: [PATCH] - 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())