Source code for semantic_branch.impl

# -*- coding: utf-8 -*-

"""
This module provides a comprehensive system for working with semantic git branch names.
Semantic branches follow a structured naming convention that helps indicate the purpose
and type of work being done on a branch, enabling both developers and CI/CD systems
to understand and process branches appropriately.

Semantic Git Branch Name Format

semantic git branch name format::

    ${semantic_name}-${optional_marker}/${description}/${more_slash_is_ok}

Sample valid semantic git branch name syntax::

    main
    feature
    feature/add-this-feature
    feature-123/description
    release/1.2.3

Usage example::

    import fixa.semantic_branch as sem_branch

    _ = sem_branch.InvalidSemanticNameError
    _ = sem_branch.SemanticBranchEnum
    _ = is_valid_semantic_name
    _ = ensure_is_valid_semantic_name
    _ = sem_branch.is_certain_semantic_branch
    _ = sem_branch.is_main_branch
    _ = sem_branch.is_feature_branch
    _ = sem_branch.is_build_branch
    _ = sem_branch.is_doc_branch
    _ = sem_branch.is_fix_branch
    _ = sem_branch.is_release_branch
    _ = sem_branch.is_cleanup_branch
    _ = sem_branch.is_sandbox_branch
    _ = sem_branch.is_develop_branch
    _ = sem_branch.is_test_branch
    _ = sem_branch.is_int_branch
    _ = sem_branch.is_staging_branch
    _ = sem_branch.is_qa_branch
    _ = sem_branch.is_preprod_branch
    _ = sem_branch.is_prod_branch
    _ = sem_branch.is_blue_branch
    _ = sem_branch.is_green_branch
    _ = sem_branch.SemanticBranchRule
"""

import typing as T
import enum
import string
import dataclasses
from functools import cache


[docs] class InvalidSemanticNameError(ValueError): """ Exception raised when a semantic name doesn't meet validation requirements. """
lower_case_charset = set(string.ascii_lowercase) semantic_name_charset = set(string.ascii_lowercase + string.digits)
[docs] def is_valid_semantic_name(name: str) -> bool: """ Check if a name is a valid semantic name. A valid semantic name must: 1. Not be empty 2. Start with a lowercase letter (a-z) 3. Contain only lowercase letters and digits :param name: The name to validate Examples:: # Valid semantic names assert is_valid_semantic_name("feature") is True assert is_valid_semantic_name("feat") is True assert is_valid_semantic_name("test123") is True assert is_valid_semantic_name("build2") is True # Invalid semantic names assert is_valid_semantic_name("") is False # Empty assert is_valid_semantic_name("Feature") is False # Uppercase assert is_valid_semantic_name("123test") is False # Starts with digit assert is_valid_semantic_name("feature-123") is False # Contains dash:return: True if the name is valid, False otherwise """ if len(name) == 0: return False if name[0] not in lower_case_charset: return False return len(set(name).difference(semantic_name_charset)) == 0
[docs] def ensure_is_valid_semantic_name(name: str) -> str: # pragma: no cover """ Validate a semantic name and raise an exception if invalid. This function performs the same validation as :func:`is_valid_semantic_name` but raises an :class:`InvalidSemanticNameError` if the name is invalid, otherwise returns the name unchanged. """ if is_valid_semantic_name(name) is False: raise InvalidSemanticNameError(f"{name!r} is not a valid semantic name") return name
[docs] def is_certain_semantic_branch(name: str, stubs: T.List[str]) -> bool: """ Test if a branch name matches any of the provided semantic stubs. This function extracts the semantic portion from a branch name by removing everything after the first occurrence of separators (``-``, ``_``, ``@``, ``+``, ``/``), then checks if it matches any of the provided semantic stubs. The matching is case-insensitive and handles leading/trailing whitespace. All provided stubs must be valid semantic names (validated automatically). :param name: The git branch name to test :param stubs: List of valid semantic stub names to match against :return: True if the branch name matches any stub, False otherwise :raises InvalidSemanticNameError: If any stub in the list is invalid Examples:: # Basic matching assert is_certain_semantic_branch("feature", ["feat", "feature"]) is True assert is_certain_semantic_branch("feat", ["feat", "feature"]) is True # Case insensitive matching assert is_certain_semantic_branch("FEATURE", ["feat", "feature"]) is True assert is_certain_semantic_branch("Feat", ["feat", "feature"]) is True # Separator handling assert is_certain_semantic_branch("feature-123", ["feat", "feature"]) is True assert is_certain_semantic_branch("feature/add-login", ["feat", "feature"]) is True assert is_certain_semantic_branch("feature_test", ["feat", "feature"]) is True assert is_certain_semantic_branch("feature@urgent", ["feat", "feature"]) is True assert is_certain_semantic_branch("feature+new", ["feat", "feature"]) is True # Combined separators (processed in order) assert is_certain_semantic_branch("feature-123/description", ["feat", "feature"]) is True # Whitespace handling assert is_certain_semantic_branch(" feature ", ["feat", "feature"]) is True # No match assert is_certain_semantic_branch("main", ["feat", "feature"]) is False # Invalid stub raises exception try: is_certain_semantic_branch("feature", ["Feature"]) # Invalid: uppercase except InvalidSemanticNameError: print("Invalid stub provided") """ name = name.lower().strip() name = name.split("/")[0] name = name.split("-")[0] name = name.split("_")[0] name = name.split("@")[0] name = name.split("+")[0] stubs = set([ensure_is_valid_semantic_name(stub.lower().strip()) for stub in stubs]) return name in stubs
[docs] class SemanticStubEnum(str, enum.Enum): """ Enumeration of all supported semantic branch stub names. This enum defines all the standard semantic stub names that can be used to identify different types of branches. Each stub is a short, lowercase string that represents a specific branch purpose or environment. """ # Essential stubs main = "main" master = "master" # Use case based stubs feat = "feat" feature = "feature" build = "build" doc = "doc" fix = "fix" hotfix = "hotfix" rls = "rls" release = "release" clean = "clean" cleanup = "cleanup" # Environment based stubs sbx = "sbx" sandbox = "sandbox" dev = "dev" develop = "develop" tst = "tst" test = "test" int = "int" stg = "stg" stage = "stage" staging = "staging" qa = "qa" preprod = "preprod" prd = "prd" prod = "prod" blue = "blue" green = "green"
[docs] @dataclasses.dataclass class SemanticBranch: """ Data class representing a semantic branch type with its associated stub names. This class encapsulates a semantic branch type by combining a canonical name with a list of stub names that can be used to identify branches of this type. It provides a convenient interface for testing whether a given branch name matches this semantic branch type. :param name: The canonical name of this semantic branch type :param stubs: List of semantic stub names that identify this branch type """ name: str = dataclasses.field() stubs: list[str] = dataclasses.field() def __post_init__(self): self.name = ensure_is_valid_semantic_name(self.name) self.stubs = [ ensure_is_valid_semantic_name(stub) for stub in self.stubs ]
[docs] def is_match(self, git_branch_name: str) -> bool: """ Test if a git branch name matches this semantic branch type. This method uses the :func:`is_certain_semantic_branch` function to determine if the provided branch name matches any of the stubs associated with this semantic branch type. :param git_branch_name: The git branch name to test :return: True if the branch name matches this semantic type, False otherwise :raises InvalidSemanticNameError: If any of the stubs are invalid """ return is_certain_semantic_branch( name=git_branch_name, stubs=self.stubs, )
[docs] class SemanticBranchEnum(enum.Enum): """ Enumeration of all standard semantic branch types. This enum provides pre-configured :class:`SemanticBranch` instances for all standard semantic branch types. Each enum value contains a SemanticBranch object with the appropriate canonical name and list of semantic stubs. This enum serves as the primary interface for semantic branch detection, providing a convenient way to access standardized semantic branch configurations without having to manually create SemanticBranch instances. """ main = SemanticBranch( name="main", stubs=[ SemanticStubEnum.main.value, SemanticStubEnum.master.value, ], ) feature = SemanticBranch( name="feature", stubs=[ SemanticStubEnum.feat.value, SemanticStubEnum.feature.value, ], ) build = SemanticBranch( name="build", stubs=[ SemanticStubEnum.build.value, ], ) doc = SemanticBranch( name="doc", stubs=[ SemanticStubEnum.doc.value, ], ) fix = SemanticBranch( name="fix", stubs=[ SemanticStubEnum.fix.value, SemanticStubEnum.hotfix.value, ], ) release = SemanticBranch( name="release", stubs=[ SemanticStubEnum.rls.value, SemanticStubEnum.release.value, ], ) cleanup = SemanticBranch( name="cleanup", stubs=[ SemanticStubEnum.clean.value, SemanticStubEnum.cleanup.value, ], ) sandbox = SemanticBranch( name="sandbox", stubs=[ SemanticStubEnum.sbx.value, SemanticStubEnum.sandbox.value, ], ) develop = SemanticBranch( name="develop", stubs=[ SemanticStubEnum.dev.value, SemanticStubEnum.develop.value, ], ) test = SemanticBranch( name="test", stubs=[ SemanticStubEnum.tst.value, SemanticStubEnum.test.value, ], ) int = SemanticBranch( name="int", stubs=[ SemanticStubEnum.int.value, ], ) staging = SemanticBranch( name="staging", stubs=[ SemanticStubEnum.stg.value, SemanticStubEnum.stage.value, SemanticStubEnum.staging.value, ], ) qa = SemanticBranch( name="qa", stubs=[ SemanticStubEnum.qa.value, ], ) preprod = SemanticBranch( name="preprod", stubs=[ SemanticStubEnum.preprod.value, ], ) prod = SemanticBranch( name="prod", stubs=[ SemanticStubEnum.prd.value, SemanticStubEnum.prod.value, ], ) blue = SemanticBranch( name="blue", stubs=[ SemanticStubEnum.blue.value, ], ) green = SemanticBranch( name="green", stubs=[ SemanticStubEnum.green.value, ], )