Skip to content

Commit spec

commit_spec

Shared commit spec validation and Git hook autofix-retry plumbing.

AutofixRetry dataclass

Outcome of an autofix-aware commit retry.

Attributes:

Name Type Description
result _GitResultLike

The final GitResult-like object (returncode/stdout/stderr).

retried bool

Whether a re-stage + retry was actually performed.

auto_fixed list[str]

Files the commit hook modified, captured before re-staging (the subsequent git add would empty the diff). Empty when no auto-fix occurred.

Source code in packages/axm-git/src/axm_git/core/commit_spec.py
Python
@dataclass
class AutofixRetry:
    """Outcome of an autofix-aware commit retry.

    Attributes:
        result: The final GitResult-like object (returncode/stdout/stderr).
        retried: Whether a re-stage + retry was actually performed.
        auto_fixed: Files the commit hook modified, captured *before*
            re-staging (the subsequent ``git add`` would empty the diff).
            Empty when no auto-fix occurred.
    """

    result: _GitResultLike
    retried: bool
    auto_fixed: list[str]

attempt_commit_with_autofix_retry(cmd, files, git_root, first_result, *, working_dir=None)

Re-stage + retry cmd once when a commit hook auto-fixed files.

Detection is on the combined stdout+stderr of first_result: when the canonical "files were modified" marker is present, the modified files are captured (git diff --name-only before re-staging), the spec files are re-staged via the subdir-aware resolver, and cmd is retried once. Otherwise first_result is returned unchanged.

Source code in packages/axm-git/src/axm_git/core/commit_spec.py
Python
def attempt_commit_with_autofix_retry(
    cmd: list[str],
    files: list[str],
    git_root: Path,
    first_result: _GitResultLike,
    *,
    working_dir: Path | None = None,
) -> AutofixRetry:
    """Re-stage + retry *cmd* once when a commit hook auto-fixed files.

    Detection is on the combined stdout+stderr of *first_result*: when the
    canonical ``"files were modified"`` marker is present, the modified
    files are captured (``git diff --name-only`` *before* re-staging), the
    spec *files* are re-staged via the subdir-aware resolver, and *cmd* is
    retried once.  Otherwise *first_result* is returned unchanged.
    """
    if first_result.returncode == 0:
        return AutofixRetry(result=first_result, retried=False, auto_fixed=[])

    output = first_result.stdout + first_result.stderr
    if AUTOFIX_MARKER not in output:
        return AutofixRetry(result=first_result, retried=False, auto_fixed=[])

    logger.warning("Commit hook auto-fixed files, re-staging and retrying")
    diff = run_git(["diff", "--name-only"], git_root)
    auto_fixed = [f for f in diff.stdout.strip().splitlines() if f.strip()]

    restage_err = stage_spec_files(files, git_root, working_dir=working_dir)
    if restage_err:
        failed = cast(
            "_GitResultLike",
            SimpleNamespace(returncode=1, stdout="", stderr=restage_err),
        )
        return AutofixRetry(result=failed, retried=True, auto_fixed=auto_fixed)

    head_before = _head_sha(git_root)
    retried = run_git(cmd, git_root)
    reconciled = _reconcile_with_repo_state(retried, git_root, head_before)
    return AutofixRetry(result=reconciled, retried=True, auto_fixed=auto_fixed)

validate_commit_spec(spec)

Validate a commit_spec dict (pure; stricter merged contract).

Requires a non-empty message AND a non-empty files list — the stricter of the two prior per-surface validators. Returns (spec, error_message) where spec is None when an error is set; each surface wraps the error string in its own result type.

Source code in packages/axm-git/src/axm_git/core/commit_spec.py
Python
def validate_commit_spec(
    spec: dict[str, object] | None,
) -> tuple[dict[str, object] | None, str | None]:
    """Validate a ``commit_spec`` dict (pure; stricter merged contract).

    Requires a non-empty ``message`` AND a non-empty ``files`` list — the
    stricter of the two prior per-surface validators.  Returns
    ``(spec, error_message)`` where *spec* is ``None`` when an error is set;
    each surface wraps the error string in its own result type.
    """
    if not spec:
        return None, "from_outputs=True but no commit_spec in context"
    if not isinstance(spec, dict):
        return None, "commit_spec must be a dict"
    missing = _REQUIRED_SPEC_KEYS - set(spec)
    if missing:
        return None, (
            f"commit_spec missing {', '.join(repr(k) for k in sorted(missing))}"
        )
    if not spec.get("files"):
        return None, "empty files list"
    if not spec.get("message"):
        return None, "empty message"
    return spec, None