baobab-ai-dev-git 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- baobab_ai_dev_git/__init__.py +75 -0
- baobab_ai_dev_git/branch/__init__.py +1 -0
- baobab_ai_dev_git/branch/branch_hierarchy_validator.py +83 -0
- baobab_ai_dev_git/branch/branch_name.py +31 -0
- baobab_ai_dev_git/branch/branch_name_builder.py +120 -0
- baobab_ai_dev_git/branch/branch_name_parser.py +62 -0
- baobab_ai_dev_git/branch/branch_name_validator.py +77 -0
- baobab_ai_dev_git/branch/branch_service.py +116 -0
- baobab_ai_dev_git/branch/branch_type.py +20 -0
- baobab_ai_dev_git/domain/__init__.py +7 -0
- baobab_ai_dev_git/exceptions/__init__.py +29 -0
- baobab_ai_dev_git/exceptions/branch_already_exists_error.py +16 -0
- baobab_ai_dev_git/exceptions/branch_not_found_error.py +16 -0
- baobab_ai_dev_git/exceptions/git_operation_error.py +15 -0
- baobab_ai_dev_git/exceptions/github_integration_error.py +15 -0
- baobab_ai_dev_git/exceptions/github_operation_error.py +15 -0
- baobab_ai_dev_git/exceptions/invalid_branch_hierarchy_error.py +23 -0
- baobab_ai_dev_git/exceptions/invalid_branch_name_error.py +15 -0
- baobab_ai_dev_git/exceptions/invalid_pull_request_target_error.py +17 -0
- baobab_ai_dev_git/exceptions/merge_not_allowed_error.py +23 -0
- baobab_ai_dev_git/exceptions/repository_dirty_error.py +13 -0
- baobab_ai_dev_git/github/__init__.py +21 -0
- baobab_ai_dev_git/github/github_check_run_info.py +20 -0
- baobab_ai_dev_git/github/github_client_port.py +71 -0
- baobab_ai_dev_git/github/github_pull_request_adapter.py +216 -0
- baobab_ai_dev_git/github/github_pull_request_api_client.py +69 -0
- baobab_ai_dev_git/github/github_pull_request_info.py +32 -0
- baobab_ai_dev_git/github/github_repository_adapter.py +130 -0
- baobab_ai_dev_git/github/github_repository_api_client.py +31 -0
- baobab_ai_dev_git/github/github_repository_info.py +23 -0
- baobab_ai_dev_git/policy/__init__.py +1 -0
- baobab_ai_dev_git/policy/branch_policy.py +25 -0
- baobab_ai_dev_git/policy/branch_policy_service.py +56 -0
- baobab_ai_dev_git/policy/merge_policy_service.py +123 -0
- baobab_ai_dev_git/policy/pull_request_policy.py +19 -0
- baobab_ai_dev_git/policy/pull_request_policy_service.py +44 -0
- baobab_ai_dev_git/pull_request/__init__.py +1 -0
- baobab_ai_dev_git/pull_request/pull_request_service.py +150 -0
- baobab_ai_dev_git/pull_request/pull_request_target_resolver.py +149 -0
- baobab_ai_dev_git/pull_request/pull_request_validator.py +73 -0
- baobab_ai_dev_git/report/__init__.py +9 -0
- baobab_ai_dev_git/report/git_compliance_report.py +52 -0
- baobab_ai_dev_git/report/git_compliance_report_builder.py +175 -0
- baobab_ai_dev_git/repository/__init__.py +1 -0
- baobab_ai_dev_git/repository/git_command_runner.py +61 -0
- baobab_ai_dev_git/repository/git_repository_service.py +137 -0
- baobab_ai_dev_git/repository/git_repository_state_reader.py +164 -0
- baobab_ai_dev_git/result/__init__.py +15 -0
- baobab_ai_dev_git/result/branch_validation_result.py +39 -0
- baobab_ai_dev_git/result/git_operation_result.py +52 -0
- baobab_ai_dev_git/result/merge_eligibility_result.py +41 -0
- baobab_ai_dev_git/result/pull_request_creation_result.py +48 -0
- baobab_ai_dev_git/result/pull_request_validation_result.py +44 -0
- baobab_ai_dev_git/result/serializable_result.py +14 -0
- baobab_ai_dev_git-0.1.0.dist-info/METADATA +290 -0
- baobab_ai_dev_git-0.1.0.dist-info/RECORD +59 -0
- baobab_ai_dev_git-0.1.0.dist-info/WHEEL +5 -0
- baobab_ai_dev_git-0.1.0.dist-info/licenses/LICENSE +21 -0
- baobab_ai_dev_git-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Librairie de gestion Git pour l'écosystème Baobab AI Dev."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.branch.branch_name_builder import BranchNameBuilder
|
|
4
|
+
from baobab_ai_dev_git.branch.branch_name_validator import BranchNameValidator
|
|
5
|
+
from baobab_ai_dev_git.branch.branch_service import BranchService
|
|
6
|
+
from baobab_ai_dev_git.exceptions import (
|
|
7
|
+
BranchAlreadyExistsError,
|
|
8
|
+
BranchNotFoundError,
|
|
9
|
+
GithubIntegrationError,
|
|
10
|
+
GithubOperationError,
|
|
11
|
+
GitOperationError,
|
|
12
|
+
InvalidBranchHierarchyError,
|
|
13
|
+
InvalidBranchNameError,
|
|
14
|
+
InvalidPullRequestTargetError,
|
|
15
|
+
MergeNotAllowedError,
|
|
16
|
+
RepositoryDirtyError,
|
|
17
|
+
)
|
|
18
|
+
from baobab_ai_dev_git.github import (
|
|
19
|
+
GithubCheckRunInfo,
|
|
20
|
+
GithubClientPort,
|
|
21
|
+
GithubPullRequestAdapter,
|
|
22
|
+
GithubPullRequestApiClient,
|
|
23
|
+
GithubPullRequestInfo,
|
|
24
|
+
GithubRepositoryAdapter,
|
|
25
|
+
GithubRepositoryApiClient,
|
|
26
|
+
GithubRepositoryInfo,
|
|
27
|
+
)
|
|
28
|
+
from baobab_ai_dev_git.policy.merge_policy_service import MergePolicyService
|
|
29
|
+
from baobab_ai_dev_git.pull_request.pull_request_service import PullRequestService
|
|
30
|
+
from baobab_ai_dev_git.pull_request.pull_request_validator import PullRequestValidator
|
|
31
|
+
from baobab_ai_dev_git.report import GitComplianceReport, GitComplianceReportBuilder
|
|
32
|
+
from baobab_ai_dev_git.result import (
|
|
33
|
+
BranchValidationResult,
|
|
34
|
+
GitOperationResult,
|
|
35
|
+
MergeEligibilityResult,
|
|
36
|
+
PullRequestValidationResult,
|
|
37
|
+
SerializableResult,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BranchAlreadyExistsError",
|
|
44
|
+
"BranchNameBuilder",
|
|
45
|
+
"BranchNameValidator",
|
|
46
|
+
"BranchNotFoundError",
|
|
47
|
+
"BranchService",
|
|
48
|
+
"BranchValidationResult",
|
|
49
|
+
"GitComplianceReport",
|
|
50
|
+
"GitComplianceReportBuilder",
|
|
51
|
+
"GitOperationError",
|
|
52
|
+
"GitOperationResult",
|
|
53
|
+
"GithubCheckRunInfo",
|
|
54
|
+
"GithubClientPort",
|
|
55
|
+
"GithubIntegrationError",
|
|
56
|
+
"GithubOperationError",
|
|
57
|
+
"GithubPullRequestAdapter",
|
|
58
|
+
"GithubPullRequestApiClient",
|
|
59
|
+
"GithubPullRequestInfo",
|
|
60
|
+
"GithubRepositoryAdapter",
|
|
61
|
+
"GithubRepositoryApiClient",
|
|
62
|
+
"GithubRepositoryInfo",
|
|
63
|
+
"InvalidBranchHierarchyError",
|
|
64
|
+
"InvalidBranchNameError",
|
|
65
|
+
"InvalidPullRequestTargetError",
|
|
66
|
+
"MergeEligibilityResult",
|
|
67
|
+
"MergeNotAllowedError",
|
|
68
|
+
"MergePolicyService",
|
|
69
|
+
"PullRequestService",
|
|
70
|
+
"PullRequestValidationResult",
|
|
71
|
+
"PullRequestValidator",
|
|
72
|
+
"RepositoryDirtyError",
|
|
73
|
+
"SerializableResult",
|
|
74
|
+
"__version__",
|
|
75
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Construction de noms de branches Git conformes."""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Valide la hiérarchie parent/enfant entre branches Git."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.branch.branch_name import BranchName
|
|
4
|
+
from baobab_ai_dev_git.branch.branch_name_parser import BranchNameParser
|
|
5
|
+
from baobab_ai_dev_git.branch.branch_type import BranchType
|
|
6
|
+
from baobab_ai_dev_git.exceptions.invalid_branch_hierarchy_error import (
|
|
7
|
+
InvalidBranchHierarchyError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BranchHierarchyValidator:
|
|
12
|
+
"""Vérifie qu'une branche enfant respecte la hiérarchie US → FEAT → BL."""
|
|
13
|
+
|
|
14
|
+
_ALLOWED_CHILDREN: dict[BranchType, frozenset[BranchType]] = {
|
|
15
|
+
BranchType.MAIN: frozenset({BranchType.USER_STORY}),
|
|
16
|
+
BranchType.USER_STORY: frozenset({BranchType.FEATURE}),
|
|
17
|
+
BranchType.FEATURE: frozenset({BranchType.BACKLOG}),
|
|
18
|
+
BranchType.BACKLOG: frozenset(),
|
|
19
|
+
BranchType.UNKNOWN: frozenset(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def __init__(self, parser: BranchNameParser | None = None) -> None:
|
|
23
|
+
"""Initialise le validateur.
|
|
24
|
+
|
|
25
|
+
:param parser: Parseur injecté ; instance par défaut si absent.
|
|
26
|
+
:type parser: BranchNameParser | None
|
|
27
|
+
"""
|
|
28
|
+
self._parser: BranchNameParser = parser or BranchNameParser()
|
|
29
|
+
|
|
30
|
+
def validate(self, parent_raw_name: str, child_raw_name: str) -> None:
|
|
31
|
+
"""Valide la relation hiérarchique entre deux branches.
|
|
32
|
+
|
|
33
|
+
:param parent_raw_name: Nom de la branche parente.
|
|
34
|
+
:type parent_raw_name: str
|
|
35
|
+
:param child_raw_name: Nom de la branche enfant.
|
|
36
|
+
:type child_raw_name: str
|
|
37
|
+
:raises InvalidBranchHierarchyError: Si la hiérarchie est invalide.
|
|
38
|
+
"""
|
|
39
|
+
parent = self._parser.parse(parent_raw_name)
|
|
40
|
+
child = self._parser.parse(child_raw_name)
|
|
41
|
+
|
|
42
|
+
if parent.branch_type == BranchType.UNKNOWN:
|
|
43
|
+
msg = f"Branche parente non reconnue : '{parent.raw_name}'."
|
|
44
|
+
raise InvalidBranchHierarchyError(msg, parent.raw_name, child.raw_name)
|
|
45
|
+
|
|
46
|
+
if child.branch_type == BranchType.UNKNOWN:
|
|
47
|
+
msg = f"Branche enfant non reconnue : '{child.raw_name}'."
|
|
48
|
+
raise InvalidBranchHierarchyError(msg, parent.raw_name, child.raw_name)
|
|
49
|
+
|
|
50
|
+
allowed = self._ALLOWED_CHILDREN[parent.branch_type]
|
|
51
|
+
if child.branch_type not in allowed:
|
|
52
|
+
msg = (
|
|
53
|
+
f"Hiérarchie invalide : '{child.raw_name}' ne peut pas être créée "
|
|
54
|
+
f"depuis '{parent.raw_name}'."
|
|
55
|
+
)
|
|
56
|
+
raise InvalidBranchHierarchyError(msg, parent.raw_name, child.raw_name)
|
|
57
|
+
|
|
58
|
+
self._validate_identifier_consistency(parent, child)
|
|
59
|
+
|
|
60
|
+
def _validate_identifier_consistency(self, parent: BranchName, child: BranchName) -> None:
|
|
61
|
+
"""Vérifie la cohérence des identifiants entre parent et enfant.
|
|
62
|
+
|
|
63
|
+
:param parent: Branche parente parsée.
|
|
64
|
+
:type parent: BranchName
|
|
65
|
+
:param child: Branche enfant parsée.
|
|
66
|
+
:type child: BranchName
|
|
67
|
+
:raises InvalidBranchHierarchyError: Si les identifiants sont incohérents.
|
|
68
|
+
"""
|
|
69
|
+
if child.branch_type == BranchType.FEATURE:
|
|
70
|
+
if child.user_story_id != parent.user_story_id:
|
|
71
|
+
msg = (
|
|
72
|
+
f"La feature '{child.raw_name}' doit appartenir à "
|
|
73
|
+
f"la user story '{parent.user_story_id}'."
|
|
74
|
+
)
|
|
75
|
+
raise InvalidBranchHierarchyError(msg, parent.raw_name, child.raw_name)
|
|
76
|
+
|
|
77
|
+
if child.branch_type == BranchType.BACKLOG:
|
|
78
|
+
if child.feature_id != parent.feature_id:
|
|
79
|
+
msg = (
|
|
80
|
+
f"Le backlog '{child.raw_name}' doit appartenir à "
|
|
81
|
+
f"la feature '{parent.feature_id}'."
|
|
82
|
+
)
|
|
83
|
+
raise InvalidBranchHierarchyError(msg, parent.raw_name, child.raw_name)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Représentation structurée d'un nom de branche Git."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from baobab_ai_dev_git.branch.branch_type import BranchType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class BranchName:
|
|
10
|
+
"""Nom de branche Git parsé et typé.
|
|
11
|
+
|
|
12
|
+
:param raw_name: Chaîne brute telle que fournie par Git.
|
|
13
|
+
:type raw_name: str
|
|
14
|
+
:param branch_type: Type de branche détecté.
|
|
15
|
+
:type branch_type: BranchType
|
|
16
|
+
:param user_story_id: Identifiant US extrait, le cas échéant.
|
|
17
|
+
:type user_story_id: str | None
|
|
18
|
+
:param feature_id: Identifiant feature extrait, le cas échéant.
|
|
19
|
+
:type feature_id: str | None
|
|
20
|
+
:param backlog_id: Identifiant backlog extrait, le cas échéant.
|
|
21
|
+
:type backlog_id: str | None
|
|
22
|
+
:param slug: Partie descriptive normalisée, le cas échéant.
|
|
23
|
+
:type slug: str | None
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
raw_name: str
|
|
27
|
+
branch_type: BranchType
|
|
28
|
+
user_story_id: str | None = None
|
|
29
|
+
feature_id: str | None = None
|
|
30
|
+
backlog_id: str | None = None
|
|
31
|
+
slug: str | None = None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Génère des noms de branches Git conformes aux conventions du projet."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
5
|
+
|
|
6
|
+
from baobab_ai_dev_core.domain.entities.backlog import Backlog
|
|
7
|
+
from baobab_ai_dev_core.domain.entities.feature import Feature
|
|
8
|
+
from baobab_ai_dev_core.domain.entities.user_story import UserStory
|
|
9
|
+
|
|
10
|
+
_ID_PATTERN: re.Pattern[str] = re.compile(r"^[A-Z]+-\d+$")
|
|
11
|
+
_FORBIDDEN_SLUG_CHARS: re.Pattern[str] = re.compile(r"[^a-z0-9-]+")
|
|
12
|
+
_MULTI_HYPHEN: re.Pattern[str] = re.compile(r"-{2,}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BranchNameBuilder:
|
|
16
|
+
"""Construit des noms de branches Git à partir d'objets métier.
|
|
17
|
+
|
|
18
|
+
Les slugs produits sont déterministes : minuscules, sans accents,
|
|
19
|
+
caractères spéciaux supprimés, espaces convertis en tirets.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def build_user_story_branch(self, user_story: UserStory) -> str:
|
|
23
|
+
"""Construit le nom de branche d'une user story.
|
|
24
|
+
|
|
25
|
+
:param user_story: User story source.
|
|
26
|
+
:type user_story: UserStory
|
|
27
|
+
:return: Nom de branche (ex. ``us/US-001-initialiser-module-git``).
|
|
28
|
+
:rtype: str
|
|
29
|
+
:raises ValueError: Si l'identifiant ou le titre est absent ou invalide.
|
|
30
|
+
"""
|
|
31
|
+
identifier = self._require_identifier(str(user_story.code), "user_story")
|
|
32
|
+
slug = self.normalize_slug(str(user_story.title))
|
|
33
|
+
return f"us/{identifier}-{slug}"
|
|
34
|
+
|
|
35
|
+
def build_feature_branch(self, feature: Feature) -> str:
|
|
36
|
+
"""Construit le nom de branche d'une feature.
|
|
37
|
+
|
|
38
|
+
:param feature: Feature source.
|
|
39
|
+
:type feature: Feature
|
|
40
|
+
:return: Nom de branche (ex. ``feat/US-001/FEAT-001-gestion-branches``).
|
|
41
|
+
:rtype: str
|
|
42
|
+
:raises ValueError: Si un identifiant ou le titre est absent ou invalide.
|
|
43
|
+
"""
|
|
44
|
+
feature_id = self._require_identifier(str(feature.code), "feature")
|
|
45
|
+
user_story_code = feature.branch_name.user_story_code
|
|
46
|
+
if user_story_code is None:
|
|
47
|
+
msg = "Le code user story parent est introuvable sur la branche de la feature."
|
|
48
|
+
raise ValueError(msg)
|
|
49
|
+
user_story_id = self._require_identifier(str(user_story_code), "user_story")
|
|
50
|
+
slug = self.normalize_slug(str(feature.title))
|
|
51
|
+
return f"feat/{user_story_id}/{feature_id}-{slug}"
|
|
52
|
+
|
|
53
|
+
def build_backlog_branch(self, backlog: Backlog) -> str:
|
|
54
|
+
"""Construit le nom de branche d'un backlog.
|
|
55
|
+
|
|
56
|
+
:param backlog: Backlog source.
|
|
57
|
+
:type backlog: Backlog
|
|
58
|
+
:return: Nom de branche (ex. ``bl/FEAT-001/BL-001-validator``).
|
|
59
|
+
:rtype: str
|
|
60
|
+
:raises ValueError: Si un identifiant ou le titre est absent ou invalide.
|
|
61
|
+
"""
|
|
62
|
+
backlog_id = self._require_identifier(str(backlog.code), "backlog")
|
|
63
|
+
feature_code = backlog.branch_name.feature_code
|
|
64
|
+
if feature_code is None:
|
|
65
|
+
msg = "Le code feature parent est introuvable sur la branche du backlog."
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
feature_id = self._require_identifier(str(feature_code), "feature")
|
|
68
|
+
slug = self.normalize_slug(str(backlog.title))
|
|
69
|
+
return f"bl/{feature_id}/{backlog_id}-{slug}"
|
|
70
|
+
|
|
71
|
+
def normalize_slug(self, title: str) -> str:
|
|
72
|
+
"""Normalise un titre en slug Git conforme.
|
|
73
|
+
|
|
74
|
+
:param title: Titre brut à normaliser.
|
|
75
|
+
:type title: str
|
|
76
|
+
:return: Slug en minuscules sans caractères interdits.
|
|
77
|
+
:rtype: str
|
|
78
|
+
:raises ValueError: Si le titre est vide ou ne produit aucun slug valide.
|
|
79
|
+
"""
|
|
80
|
+
if not title or not title.strip():
|
|
81
|
+
msg = "Le titre ne peut pas être vide."
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
normalized = unicodedata.normalize("NFKD", title.strip())
|
|
85
|
+
ascii_title = normalized.encode("ascii", "ignore").decode("ascii")
|
|
86
|
+
lowered = ascii_title.lower()
|
|
87
|
+
with_hyphens = lowered.replace(" ", "-").replace("_", "-")
|
|
88
|
+
cleaned = _FORBIDDEN_SLUG_CHARS.sub("", with_hyphens)
|
|
89
|
+
collapsed = _MULTI_HYPHEN.sub("-", cleaned).strip("-")
|
|
90
|
+
|
|
91
|
+
if not collapsed:
|
|
92
|
+
msg = "Le titre ne produit aucun slug valide après normalisation."
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
|
|
95
|
+
return collapsed
|
|
96
|
+
|
|
97
|
+
def _require_identifier(self, value: str, field_name: str) -> str:
|
|
98
|
+
"""Valide et retourne un identifiant métier.
|
|
99
|
+
|
|
100
|
+
:param value: Identifiant brut.
|
|
101
|
+
:type value: str
|
|
102
|
+
:param field_name: Nom du champ pour le message d'erreur.
|
|
103
|
+
:type field_name: str
|
|
104
|
+
:return: Identifiant validé.
|
|
105
|
+
:rtype: str
|
|
106
|
+
:raises ValueError: Si l'identifiant est absent ou mal formé.
|
|
107
|
+
"""
|
|
108
|
+
if not value or not value.strip():
|
|
109
|
+
msg = f"L'identifiant {field_name} est obligatoire."
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
candidate = value.strip().upper()
|
|
113
|
+
if not _ID_PATTERN.match(candidate):
|
|
114
|
+
msg = (
|
|
115
|
+
f"L'identifiant {field_name} '{value}' est invalide "
|
|
116
|
+
f"(format attendu : PREFIX-NUM, ex. US-001)."
|
|
117
|
+
)
|
|
118
|
+
raise ValueError(msg)
|
|
119
|
+
|
|
120
|
+
return candidate
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Analyse un nom de branche Git brut en objet structuré."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from baobab_ai_dev_git.branch.branch_name import BranchName
|
|
6
|
+
from baobab_ai_dev_git.branch.branch_type import BranchType
|
|
7
|
+
|
|
8
|
+
_ID: str = r"[A-Z]+-\d+"
|
|
9
|
+
_RE_USER_STORY: re.Pattern[str] = re.compile(rf"^us/({_ID})-(.+)$")
|
|
10
|
+
_RE_FEATURE: re.Pattern[str] = re.compile(rf"^feat/({_ID})/({_ID})-(.+)$")
|
|
11
|
+
_RE_BACKLOG: re.Pattern[str] = re.compile(rf"^bl/({_ID})/({_ID})-(.+)$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BranchNameParser:
|
|
15
|
+
"""Parse une chaîne de branche Git en ``BranchName`` structuré."""
|
|
16
|
+
|
|
17
|
+
def parse(self, raw_name: str) -> BranchName:
|
|
18
|
+
"""Analyse un nom de branche brut.
|
|
19
|
+
|
|
20
|
+
:param raw_name: Nom de branche tel que retourné par Git.
|
|
21
|
+
:type raw_name: str
|
|
22
|
+
:return: Objet ``BranchName`` avec type et identifiants extraits.
|
|
23
|
+
:rtype: BranchName
|
|
24
|
+
"""
|
|
25
|
+
normalized = raw_name.strip()
|
|
26
|
+
|
|
27
|
+
if normalized == "main":
|
|
28
|
+
return BranchName(raw_name=normalized, branch_type=BranchType.MAIN)
|
|
29
|
+
|
|
30
|
+
backlog_match = _RE_BACKLOG.match(normalized)
|
|
31
|
+
if backlog_match is not None:
|
|
32
|
+
feature_id, backlog_id, slug = backlog_match.groups()
|
|
33
|
+
return BranchName(
|
|
34
|
+
raw_name=normalized,
|
|
35
|
+
branch_type=BranchType.BACKLOG,
|
|
36
|
+
feature_id=feature_id,
|
|
37
|
+
backlog_id=backlog_id,
|
|
38
|
+
slug=slug,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
feature_match = _RE_FEATURE.match(normalized)
|
|
42
|
+
if feature_match is not None:
|
|
43
|
+
user_story_id, feature_id, slug = feature_match.groups()
|
|
44
|
+
return BranchName(
|
|
45
|
+
raw_name=normalized,
|
|
46
|
+
branch_type=BranchType.FEATURE,
|
|
47
|
+
user_story_id=user_story_id,
|
|
48
|
+
feature_id=feature_id,
|
|
49
|
+
slug=slug,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
user_story_match = _RE_USER_STORY.match(normalized)
|
|
53
|
+
if user_story_match is not None:
|
|
54
|
+
user_story_id, slug = user_story_match.groups()
|
|
55
|
+
return BranchName(
|
|
56
|
+
raw_name=normalized,
|
|
57
|
+
branch_type=BranchType.USER_STORY,
|
|
58
|
+
user_story_id=user_story_id,
|
|
59
|
+
slug=slug,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return BranchName(raw_name=normalized, branch_type=BranchType.UNKNOWN)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Valide strictement les noms de branches Git."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.branch.branch_name import BranchName
|
|
4
|
+
from baobab_ai_dev_git.branch.branch_name_parser import BranchNameParser
|
|
5
|
+
from baobab_ai_dev_git.branch.branch_type import BranchType
|
|
6
|
+
from baobab_ai_dev_git.exceptions.invalid_branch_name_error import InvalidBranchNameError
|
|
7
|
+
from baobab_ai_dev_git.result.branch_validation_result import BranchValidationResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BranchNameValidator:
|
|
11
|
+
"""Valide un nom de branche à l'aide du parseur métier."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, parser: BranchNameParser | None = None) -> None:
|
|
14
|
+
"""Initialise le validateur.
|
|
15
|
+
|
|
16
|
+
:param parser: Parseur injecté ; instance par défaut si absent.
|
|
17
|
+
:type parser: BranchNameParser | None
|
|
18
|
+
"""
|
|
19
|
+
self._parser: BranchNameParser = parser or BranchNameParser()
|
|
20
|
+
|
|
21
|
+
def validate(self, raw_name: str) -> BranchValidationResult:
|
|
22
|
+
"""Valide un nom de branche sans lever d'exception.
|
|
23
|
+
|
|
24
|
+
:param raw_name: Nom brut à valider.
|
|
25
|
+
:type raw_name: str
|
|
26
|
+
:return: Résultat structuré de validation.
|
|
27
|
+
:rtype: BranchValidationResult
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
self.validate_or_raise(raw_name)
|
|
31
|
+
except InvalidBranchNameError as error:
|
|
32
|
+
return BranchValidationResult(
|
|
33
|
+
is_valid=False,
|
|
34
|
+
errors=[str(error)],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parsed = self._parser.parse(raw_name)
|
|
38
|
+
return BranchValidationResult(is_valid=True, branch_name=parsed)
|
|
39
|
+
|
|
40
|
+
def validate_or_raise(self, raw_name: str) -> BranchName:
|
|
41
|
+
"""Valide un nom de branche et lève une exception si invalide.
|
|
42
|
+
|
|
43
|
+
:param raw_name: Nom brut à valider.
|
|
44
|
+
:type raw_name: str
|
|
45
|
+
:return: Nom parsé conforme.
|
|
46
|
+
:rtype: BranchName
|
|
47
|
+
:raises InvalidBranchNameError: Si le format est invalide.
|
|
48
|
+
"""
|
|
49
|
+
if not raw_name or not raw_name.strip():
|
|
50
|
+
msg = "Le nom de branche ne peut pas être vide."
|
|
51
|
+
raise InvalidBranchNameError(msg, branch_name=raw_name)
|
|
52
|
+
|
|
53
|
+
parsed = self._parser.parse(raw_name.strip())
|
|
54
|
+
|
|
55
|
+
if parsed.branch_type == BranchType.UNKNOWN:
|
|
56
|
+
msg = f"Format de branche non reconnu : '{parsed.raw_name}'."
|
|
57
|
+
raise InvalidBranchNameError(msg, branch_name=parsed.raw_name)
|
|
58
|
+
|
|
59
|
+
if parsed.branch_type != BranchType.MAIN and not parsed.slug:
|
|
60
|
+
msg = f"Le slug est obligatoire pour la branche '{parsed.raw_name}'."
|
|
61
|
+
raise InvalidBranchNameError(msg, branch_name=parsed.raw_name)
|
|
62
|
+
|
|
63
|
+
if parsed.branch_type == BranchType.USER_STORY and not parsed.user_story_id:
|
|
64
|
+
msg = f"Identifiant user story manquant pour '{parsed.raw_name}'."
|
|
65
|
+
raise InvalidBranchNameError(msg, branch_name=parsed.raw_name)
|
|
66
|
+
|
|
67
|
+
if parsed.branch_type == BranchType.FEATURE:
|
|
68
|
+
if not parsed.user_story_id or not parsed.feature_id:
|
|
69
|
+
msg = f"Identifiants US/FEAT manquants pour '{parsed.raw_name}'."
|
|
70
|
+
raise InvalidBranchNameError(msg, branch_name=parsed.raw_name)
|
|
71
|
+
|
|
72
|
+
if parsed.branch_type == BranchType.BACKLOG:
|
|
73
|
+
if not parsed.feature_id or not parsed.backlog_id:
|
|
74
|
+
msg = f"Identifiants FEAT/BL manquants pour '{parsed.raw_name}'."
|
|
75
|
+
raise InvalidBranchNameError(msg, branch_name=parsed.raw_name)
|
|
76
|
+
|
|
77
|
+
return parsed
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Coordonne la création de branches conformes à la hiérarchie Git."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.branch.branch_hierarchy_validator import BranchHierarchyValidator
|
|
4
|
+
from baobab_ai_dev_git.branch.branch_name_builder import BranchNameBuilder
|
|
5
|
+
from baobab_ai_dev_git.domain import Backlog, Feature, UserStory
|
|
6
|
+
from baobab_ai_dev_git.repository.git_repository_service import GitRepositoryService
|
|
7
|
+
from baobab_ai_dev_git.result.git_operation_result import GitOperationResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BranchService:
|
|
11
|
+
"""Orchestre builder, validateur hiérarchique et service Git."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
builder: BranchNameBuilder | None = None,
|
|
16
|
+
hierarchy_validator: BranchHierarchyValidator | None = None,
|
|
17
|
+
git_repository: GitRepositoryService | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Initialise le service.
|
|
20
|
+
|
|
21
|
+
:param builder: Générateur de noms de branches.
|
|
22
|
+
:type builder: BranchNameBuilder | None
|
|
23
|
+
:param hierarchy_validator: Validateur de hiérarchie parent/enfant.
|
|
24
|
+
:type hierarchy_validator: BranchHierarchyValidator | None
|
|
25
|
+
:param git_repository: Service Git injecté.
|
|
26
|
+
:type git_repository: GitRepositoryService | None
|
|
27
|
+
"""
|
|
28
|
+
self._builder: BranchNameBuilder = builder or BranchNameBuilder()
|
|
29
|
+
self._hierarchy: BranchHierarchyValidator = (
|
|
30
|
+
hierarchy_validator or BranchHierarchyValidator()
|
|
31
|
+
)
|
|
32
|
+
self._git: GitRepositoryService = git_repository or GitRepositoryService()
|
|
33
|
+
|
|
34
|
+
def create_user_story_branch(
|
|
35
|
+
self,
|
|
36
|
+
user_story: UserStory,
|
|
37
|
+
parent_branch: str = "main",
|
|
38
|
+
cwd: str | None = None,
|
|
39
|
+
) -> GitOperationResult:
|
|
40
|
+
"""Crée une branche user story depuis ``main``.
|
|
41
|
+
|
|
42
|
+
:param user_story: User story source.
|
|
43
|
+
:type user_story: UserStory
|
|
44
|
+
:param parent_branch: Branche parente (``main`` par défaut).
|
|
45
|
+
:type parent_branch: str
|
|
46
|
+
:param cwd: Répertoire du dépôt.
|
|
47
|
+
:type cwd: str | None
|
|
48
|
+
:return: Résultat de la création Git.
|
|
49
|
+
:rtype: GitOperationResult
|
|
50
|
+
"""
|
|
51
|
+
branch_name = self._builder.build_user_story_branch(user_story)
|
|
52
|
+
return self._create_from_parent(parent_branch, branch_name, cwd=cwd)
|
|
53
|
+
|
|
54
|
+
def create_feature_branch(
|
|
55
|
+
self,
|
|
56
|
+
feature: Feature,
|
|
57
|
+
parent_branch: str,
|
|
58
|
+
cwd: str | None = None,
|
|
59
|
+
) -> GitOperationResult:
|
|
60
|
+
"""Crée une branche feature depuis sa user story parente.
|
|
61
|
+
|
|
62
|
+
:param feature: Feature source.
|
|
63
|
+
:type feature: Feature
|
|
64
|
+
:param parent_branch: Branche user story parente.
|
|
65
|
+
:type parent_branch: str
|
|
66
|
+
:param cwd: Répertoire du dépôt.
|
|
67
|
+
:type cwd: str | None
|
|
68
|
+
:return: Résultat de la création Git.
|
|
69
|
+
:rtype: GitOperationResult
|
|
70
|
+
"""
|
|
71
|
+
branch_name = self._builder.build_feature_branch(feature)
|
|
72
|
+
return self._create_from_parent(parent_branch, branch_name, cwd=cwd)
|
|
73
|
+
|
|
74
|
+
def create_backlog_branch(
|
|
75
|
+
self,
|
|
76
|
+
backlog: Backlog,
|
|
77
|
+
parent_branch: str,
|
|
78
|
+
cwd: str | None = None,
|
|
79
|
+
) -> GitOperationResult:
|
|
80
|
+
"""Crée une branche backlog depuis sa feature parente.
|
|
81
|
+
|
|
82
|
+
:param backlog: Backlog source.
|
|
83
|
+
:type backlog: Backlog
|
|
84
|
+
:param parent_branch: Branche feature parente.
|
|
85
|
+
:type parent_branch: str
|
|
86
|
+
:param cwd: Répertoire du dépôt.
|
|
87
|
+
:type cwd: str | None
|
|
88
|
+
:return: Résultat de la création Git.
|
|
89
|
+
:rtype: GitOperationResult
|
|
90
|
+
"""
|
|
91
|
+
branch_name = self._builder.build_backlog_branch(backlog)
|
|
92
|
+
self._hierarchy.validate(parent_branch, branch_name)
|
|
93
|
+
return self._git.prepare_backlog_branch(branch_name, parent_branch, cwd=cwd)
|
|
94
|
+
|
|
95
|
+
def _create_from_parent(
|
|
96
|
+
self,
|
|
97
|
+
parent_branch: str,
|
|
98
|
+
branch_name: str,
|
|
99
|
+
*,
|
|
100
|
+
cwd: str | None,
|
|
101
|
+
) -> GitOperationResult:
|
|
102
|
+
"""Crée une branche enfant après validation hiérarchique.
|
|
103
|
+
|
|
104
|
+
:param parent_branch: Branche parente.
|
|
105
|
+
:type parent_branch: str
|
|
106
|
+
:param branch_name: Nom de la branche à créer.
|
|
107
|
+
:type branch_name: str
|
|
108
|
+
:param cwd: Répertoire du dépôt.
|
|
109
|
+
:type cwd: str | None
|
|
110
|
+
:return: Résultat de création.
|
|
111
|
+
:rtype: GitOperationResult
|
|
112
|
+
"""
|
|
113
|
+
self._hierarchy.validate(parent_branch, branch_name)
|
|
114
|
+
self._git.checkout_branch(parent_branch, cwd=cwd)
|
|
115
|
+
self._git.ensure_current_branch(parent_branch, cwd=cwd)
|
|
116
|
+
return self._git.create_branch(branch_name, cwd=cwd)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Types de branches Git reconnus par la librairie."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BranchType(Enum):
|
|
7
|
+
"""Énumération des types de branches supportés.
|
|
8
|
+
|
|
9
|
+
:cvar MAIN: Branche principale ``main``.
|
|
10
|
+
:cvar USER_STORY: Branche user story ``us/*``.
|
|
11
|
+
:cvar FEATURE: Branche feature ``feat/<US-ID>/*``.
|
|
12
|
+
:cvar BACKLOG: Branche backlog ``bl/<FEAT-ID>/*``.
|
|
13
|
+
:cvar UNKNOWN: Format non reconnu.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
MAIN = "MAIN"
|
|
17
|
+
USER_STORY = "USER_STORY"
|
|
18
|
+
FEATURE = "FEATURE"
|
|
19
|
+
BACKLOG = "BACKLOG"
|
|
20
|
+
UNKNOWN = "UNKNOWN"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Entités métier officielles réexportées depuis baobab-ai-dev-core."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_core.domain.entities.backlog import Backlog
|
|
4
|
+
from baobab_ai_dev_core.domain.entities.feature import Feature
|
|
5
|
+
from baobab_ai_dev_core.domain.entities.user_story import UserStory
|
|
6
|
+
|
|
7
|
+
__all__ = ["Backlog", "Feature", "UserStory"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Catalogue public des exceptions métier Git consolidées."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.exceptions.branch_already_exists_error import BranchAlreadyExistsError
|
|
4
|
+
from baobab_ai_dev_git.exceptions.branch_not_found_error import BranchNotFoundError
|
|
5
|
+
from baobab_ai_dev_git.exceptions.git_operation_error import GitOperationError
|
|
6
|
+
from baobab_ai_dev_git.exceptions.github_integration_error import GithubIntegrationError
|
|
7
|
+
from baobab_ai_dev_git.exceptions.github_operation_error import GithubOperationError
|
|
8
|
+
from baobab_ai_dev_git.exceptions.invalid_branch_hierarchy_error import (
|
|
9
|
+
InvalidBranchHierarchyError,
|
|
10
|
+
)
|
|
11
|
+
from baobab_ai_dev_git.exceptions.invalid_branch_name_error import InvalidBranchNameError
|
|
12
|
+
from baobab_ai_dev_git.exceptions.invalid_pull_request_target_error import (
|
|
13
|
+
InvalidPullRequestTargetError,
|
|
14
|
+
)
|
|
15
|
+
from baobab_ai_dev_git.exceptions.merge_not_allowed_error import MergeNotAllowedError
|
|
16
|
+
from baobab_ai_dev_git.exceptions.repository_dirty_error import RepositoryDirtyError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BranchAlreadyExistsError",
|
|
20
|
+
"BranchNotFoundError",
|
|
21
|
+
"GitOperationError",
|
|
22
|
+
"GithubIntegrationError",
|
|
23
|
+
"GithubOperationError",
|
|
24
|
+
"InvalidBranchHierarchyError",
|
|
25
|
+
"InvalidBranchNameError",
|
|
26
|
+
"InvalidPullRequestTargetError",
|
|
27
|
+
"MergeNotAllowedError",
|
|
28
|
+
"RepositoryDirtyError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Exception levée lorsqu'une branche existe déjà."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.exceptions.git_operation_error import GitOperationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BranchAlreadyExistsError(GitOperationError):
|
|
7
|
+
"""Signale une tentative de création sur une branche existante.
|
|
8
|
+
|
|
9
|
+
:param branch_name: Nom de la branche en conflit.
|
|
10
|
+
:type branch_name: str | None
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, branch_name: str, message: str | None = None) -> None:
|
|
14
|
+
self.branch_name: str = branch_name
|
|
15
|
+
text = message or f"La branche '{branch_name}' existe déjà."
|
|
16
|
+
super().__init__(text, operation="branch-already-exists")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Exception levée lorsqu'une branche attendue est introuvable."""
|
|
2
|
+
|
|
3
|
+
from baobab_ai_dev_git.exceptions.git_operation_error import GitOperationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BranchNotFoundError(GitOperationError):
|
|
7
|
+
"""Signale qu'une branche locale recherchée n'existe pas.
|
|
8
|
+
|
|
9
|
+
:param branch_name: Nom de la branche introuvable.
|
|
10
|
+
:type branch_name: str
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, branch_name: str, message: str | None = None) -> None:
|
|
14
|
+
self.branch_name: str = branch_name
|
|
15
|
+
text = message or f"La branche '{branch_name}' est introuvable."
|
|
16
|
+
super().__init__(text, operation="branch-not-found")
|