- Writing custom git flow scripts - finishing up without extensive testing
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
43
scripts/git/core/hooks.py
Normal file
43
scripts/git/core/hooks.py
Normal file
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user