|
| str|None | generate_branch_name_from_message (str message) |
| bool | is_protected_branch (str|None branch, dict|None octrc, *, bool allow_optout=False) |
| tuple[bool, str] | validate_branch_name (str|None branch, dict|None octrc) |
| Profile | _resolve_auto_profile (str|None branch) |
| Profile | resolve_effective_profile (str|None branch, dict|None octrc, str|None cli_override, str|None manifest_profile=None, *, bool allow_optout=False) |
| str|None | resolve_manifest_profile (Path project_root) |
| bool | resolve_manifest_nested_git (Path project_root) |
| Profile | resolve_profile_for_project (Path project_root, str|None branch, dict|None octrc, str|None cli_override) |
| int | apply_profile_policy (str profile, "QualityGateResult" result) |
| tuple[bool, str] | check_protected_branch_commit (str|None branch, dict|None octrc, str profile) |
| tuple[bool, str] | validate_ignore_pattern (str pattern) |
| list[str] | _read_gitignore (Path repo_root) |
| None | _write_gitignore (Path repo_root, list[str] lines) |
| tuple[int, int] | _find_oct_block (list[str] lines) |
| tuple[list[str], list[str]] | list_ignore_patterns (Path repo_root) |
| list[str] | add_ignore_patterns (Path repo_root, list[str] patterns) |
| tuple[bool, str] | remove_ignore_pattern (Path repo_root, str pattern) |
Purpose
-------
Encode the Phase 4B profile-aware enforcement rules for the ``oct git``
quality gate. Maps a combination of profile (``proto`` / ``compact`` /
``strict``), branch context, and raw check findings to a semantic exit
code in the range 0..5 defined by blueprint §5.3.
Responsibilities
----------------
- Resolve the effective lint profile from CLI / octrc / branch context,
honouring the protected-branch override (blueprint §7.2).
- Restrict the protected-branch opt-out to nested-repo subprojects
(Plan 209): an ordinary subproject's ``git.protected_branches`` can
add a branch but never remove ``main``/``master`` from protection.
- Apply the profile enforcement matrix (blueprint §5.3 / §7.1) to a
:class:`~oct.git.quality_gate.QualityGateResult` and return the
matching exit code.
- Enforce the invariant "secrets always blocks" regardless of profile
(blueprint §5.3 final clause).
- Manage the OCT-managed block in ``.gitignore`` - add/remove/list
patterns inside the fenced block, leaving user-managed lines alone.
Diagnostics
-----------
Domain: GIT
Levels:
L1 - workspace-manifest lookup failures (degrading to the
octrc-only chain rather than breaking the gate)
L2 - (none) pure decision helpers; callers own lifecycle logging
L3 - profile resolution and policy decisions
L4 - per-field raw counts during exit-code derivation
Contracts
---------
- No subprocess, no Click. Profile *decisions* are pure; the two
functions that read config off disk (``resolve_manifest_profile`` and
the ``.gitignore`` block helpers) are the documented exceptions.
- ``resolve_effective_profile`` never raises; unknown values collapse
to the documented default. It is the SINGLE resolver - the workspace
fan-out path (``oct.git.oct_git._resolve_subproject_profile``)
delegates to it, so the two paths cannot drift apart (Plan 197).
- ``resolve_manifest_profile`` never raises: a missing, unreadable or
malformed manifest yields ``None``, which degrades to the pre-197
octrc-only behaviour.
- ``apply_profile_policy`` depends only on the ``QualityGateResult``
dataclass defined in :mod:`oct.git.quality_gate`.
- Precedence for the exit code is strictly: 5 > 4 > 3 > 2 > 1 > 0.
- Plan 209: protected-branch opt-out (an explicit
``git.protected_branches`` list) is a ``nested_git``-only privilege -
every other subproject may ADD to but never REMOVE the workspace
defaults from its protected set. ``resolve_manifest_nested_git``
never raises, mirroring ``resolve_manifest_profile``'s degrade-to-
safe-default contract (it returns ``False``, meaning "not exempt",
rather than crashing or guessing).
Dependencies
------------
- oct.core.diagnostics (_dbg structured logger)
- oct.core.workspace (lazy; manifest lookup of the owning subproject's profile and nested_git flag)
- oct.git.conventional (lazy; parse_commit_message - type, scope and subject for branch names)
- oct.git.quality_gate (lazy; QualityGateResult annotation, imported only for type checking)
| int oct.git.policy.apply_profile_policy |
( |
str | profile, |
|
|
"QualityGateResult" | result ) |
Derive the semantic exit code for a quality-gate result.
Exit code table (blueprint §5.3, verbatim):
==== ==========================================================
Code Meaning
==== ==========================================================
0 all checks pass
1 lint violations (blocking under this profile)
2 format violations (blocking under this profile)
3 both lint and format violations
4 test failures (when ``--include-tests``)
5 secrets detected - always blocking
==== ==========================================================
Precedence: ``5 > 4 > 3 > 2 > 1 > 0``. The first rule that matches
wins. Because secrets takes precedence, a profile that would
otherwise return 0 (e.g. ``proto`` with warnings-only lint) still
returns 5 if secrets are present. This is the "secrets always
blocks" invariant.
For profiles where a check is not blocking (``proto`` lint is
``warn``; ``proto`` format is ``skip``), the corresponding violation
counter never contributes to the exit code even if non-zero.
Parameters
----------
profile
One of ``"proto"``, ``"compact"``, ``"strict"``. Unknown values
degrade to :data:`DEFAULT_PROFILE`.
result
A :class:`~oct.git.quality_gate.QualityGateResult` with the raw
counts from a completed gate run.
Definition at line 653 of file policy.py.
| tuple[bool, str] oct.git.policy.check_protected_branch_commit |
( |
str | None | branch, |
|
|
dict | None | octrc, |
|
|
str | profile ) |
Check if committing directly to a protected branch.
Returns ``(blocked, message)``:
- On a **non-protected branch**: ``(False, "")``.
- On a protected branch in ``strict`` profile: ``(True, error_msg)``.
- On a protected branch in other profiles: ``(False, warning_msg)``.
The caller is responsible for printing the message and aborting if
``blocked`` is ``True``.
Definition at line 731 of file policy.py.
| bool oct.git.policy.is_protected_branch |
( |
str | None | branch, |
|
|
dict | None | octrc, |
|
|
* | , |
|
|
bool | allow_optout = False ) |
Return True if ``branch`` is listed in ``git.protected_branches``.
A missing ``octrc`` or a missing ``git.protected_branches`` key
falls back to :data:`DEFAULT_PROTECTED_BRANCHES`. Detached HEAD
(``branch is None``) is never considered protected - detached
workflows are per-definition outside branch policy.
Plan 209: an explicit ``protected_branches`` list is a PRIVILEGE,
not an open door. When the octrc sets one:
- ``allow_optout=False`` (the safe default, every ordinary
subproject): the effective protected set is the UNION of the
configured list and :data:`DEFAULT_PROTECTED_BRANCHES` - a
subproject may ADD a protected branch but can never REMOVE
``main``/``master`` from protection by omitting them.
- ``allow_optout=True`` (reserved for a ``nested_git`` subproject -
a foreign nested repo that owns its own branch policy, e.g.
``oc_explorer``): the configured list is honored verbatim, so an
explicit empty list means genuinely no protected branches.
An unset or malformed (non-list) ``protected_branches`` always
falls back to :data:`DEFAULT_PROTECTED_BRANCHES`, regardless of
*allow_optout* - opting out requires an explicit list.
Definition at line 241 of file policy.py.
| Profile oct.git.policy.resolve_effective_profile |
( |
str | None | branch, |
|
|
dict | None | octrc, |
|
|
str | None | cli_override, |
|
|
str | None | manifest_profile = None, |
|
|
* | , |
|
|
bool | allow_optout = False ) |
Return the profile that should be applied for the current run.
Precedence (highest wins):
1. **Protected-branch override** - if ``branch`` is listed in the
project's protected-branches set, the result is always
``"strict"``. This cannot be overridden by CLI or config.
2. **CLI ``--profile``** - explicit user override from the command
line. Must be one of :data:`VALID_PROFILES`.
3. **Workspace manifest ``subprojects[].profile``** - the profile
the workspace declares for the owning subproject, supplied by
the caller via *manifest_profile* (Plan 197). ``None`` means
"no workspace, or the workspace declares nothing", which
degrades to the pre-197 octrc-only chain below.
4. **``.octrc.json → git.pre_commit_profile``** - the persistent
per-project default.
5. **``.octrc.json → linter.profile``** - if the git-specific key
is absent, fall back to the linter-wide profile.
6. **:data:`DEFAULT_PROFILE`** - final fallback if nothing above
is set.
Unknown profile strings (from any source) are silently discarded in
favour of the next lower level of precedence. Malformed ``octrc``
values are also ignored - never raise.
Plan 197: this is the ONE resolver. Both the single-project path
(``oct git check`` / ``oct git commit`` run from inside a
subproject) and the workspace fan-out path
(``oct.git.oct_git._resolve_subproject_profile``) call it, so a
file's effective profile cannot depend on the directory the gate
was invoked from. Callers that have a project root but not a
manifest profile should use :func:`resolve_profile_for_project`,
which does the lookup for them.
Plan 209: *allow_optout* is forwarded verbatim to
:func:`is_protected_branch` - pass ``True`` only when the caller
has verified the owning subproject is ``nested_git``. Defaults
False, the safe choice for every ordinary subproject.
Definition at line 399 of file policy.py.
| bool oct.git.policy.resolve_manifest_nested_git |
( |
Path | project_root | ) |
|
Return whether *project_root* is declared ``nested_git`` in the manifest.
Mirrors :func:`resolve_manifest_profile`'s walk-up-and-load shape so
the two lookups can never disagree about WHICH subproject owns
*project_root*. Returns ``False`` - never raises - for every
degenerate case (no workspace above *project_root*, an unreadable
or malformed manifest, or *project_root* falling outside every
declared subproject): the safe default is "not exempt", i.e. the
protected-branch opt-out restriction applies.
Plan 209: this is what lets :func:`resolve_profile_for_project`
pass the correct ``allow_optout`` to :func:`is_protected_branch`
without every caller having to look up the manifest itself.
Definition at line 566 of file policy.py.
| str | None oct.git.policy.resolve_manifest_profile |
( |
Path | project_root | ) |
|
Return the workspace-declared profile for *project_root*.
Walks upward from *project_root* to the nearest
``.option_c_workspace.json``, loads it, and returns the ``profile``
the manifest declares for the subproject that OWNS *project_root*
(deepest match wins for nested subprojects).
Returns ``None`` - never raises - when any of the following holds,
so callers degrade cleanly to the pre-197 octrc-only chain:
- *project_root* is not inside a workspace (plain single project);
- the manifest is unreadable or malformed;
- *project_root* is the workspace root itself, or otherwise falls
outside every declared subproject;
- the owning subproject declares no profile, or declares a value
outside :data:`VALID_PROFILES`.
Plan 197: this closes the gap that let ``oc_chat`` declare
``strict`` and be gated at ``compact`` on every feature branch.
Definition at line 498 of file policy.py.
| Profile oct.git.policy.resolve_profile_for_project |
( |
Path | project_root, |
|
|
str | None | branch, |
|
|
dict | None | octrc, |
|
|
str | None | cli_override ) |
Resolve the effective profile for a project on disk.
Thin composition of :func:`resolve_manifest_profile`,
:func:`resolve_manifest_nested_git`, and
:func:`resolve_effective_profile`: the single entry point for
callers that hold a project root. Using it instead of calling
:func:`resolve_effective_profile` directly is what makes the
single-project path agree with the workspace fan-out path
(Plan 197) - including, since Plan 209, on the protected-branch
opt-out restriction.
Definition at line 620 of file policy.py.
| tuple[bool, str] oct.git.policy.validate_branch_name |
( |
str | None | branch, |
|
|
dict | None | octrc ) |
Validate *branch* against the configured naming patterns.
Returns ``(valid, message)``:
- ``git.branch_naming.enabled`` is ``False`` → ``(True, "")``
- ``branch`` is ``None`` (detached HEAD) → ``(True, "")``
- Branch matches at least one pattern → ``(True, "")``
- No pattern matches → ``(False, "Branch name '...' does not ...")``
Patterns default to :data:`DEFAULT_BRANCH_PATTERNS` when not
overridden in ``.octrc.json``.
Definition at line 305 of file policy.py.
| tuple[bool, str] oct.git.policy.validate_ignore_pattern |
( |
str | pattern | ) |
|
Return ``(valid, message)`` for a candidate gitignore pattern.
Rejects empty strings, patterns containing newlines, leading ``!``
(negation - too easy to weaponise via ``oct git ignore``), absolute
paths (leading ``/`` is interpreted as anchored to repo root which
is allowed by gitignore but our policy is to require relative
patterns), and any character outside :data:`_VALID_PATTERN_RE`.
Definition at line 780 of file policy.py.