Skip to content

Commit

commit

GitCommitTool — batched atomic commits with pre-commit handling.

GitCommitTool

Bases: AXMTool

Execute one or more atomic commits in a single call.

Each commit in the batch is processed sequentially: stage files, run git commit (pre-commit hooks fire automatically), and capture the result. If a commit fails (e.g. pre-commit rejects), processing stops and the error is returned alongside any commits that already succeeded.

When a pre-commit hook auto-fixes files (e.g. ruff --fix), the tool automatically re-stages and retries the commit once.

Registered as git_commit via axm.tools entry point.

Source code in packages/axm-git/src/axm_git/tools/commit.py
class GitCommitTool(AXMTool):
    """Execute one or more atomic commits in a single call.

    Each commit in the batch is processed sequentially: stage files,
    run ``git commit`` (pre-commit hooks fire automatically), and
    capture the result.  If a commit fails (e.g. pre-commit rejects),
    processing stops and the error is returned alongside any commits
    that already succeeded.

    When a pre-commit hook auto-fixes files (e.g. ruff ``--fix``),
    the tool automatically re-stages and retries the commit once.

    Registered as ``git_commit`` via axm.tools entry point.
    """

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "git_commit"

    def execute(
        self,
        *,
        path: str = ".",
        commits: list[dict[str, Any]] | None = None,
        **kwargs: Any,
    ) -> ToolResult:
        """Execute batched commits.

        Args:
            path: Project root (required).
            commits: List of commit specs, each a dict with keys:
                - ``files`` (list[str]): Files to stage.
                - ``message`` (str): Commit summary line.
                - ``body`` (str, optional): Commit body.

        Returns:
            ToolResult with list of committed results.
        """
        resolved = Path(path).resolve()
        commit_list: list[dict[str, Any]] = commits or []

        if not commit_list:
            return ToolResult(success=False, error="No commits provided")

        # Fail fast with suggestions if not a git repo
        check = run_git(["rev-parse", "--git-dir"], resolved)
        if check.returncode != 0:
            return not_a_repo_error(check.stderr, resolved)

        results: list[dict[str, Any]] = []

        for i, spec in enumerate(commit_list):
            validation_err = _validate_commit_spec(spec, i + 1, results)
            if validation_err:
                return validation_err

            files: list[str] = spec["files"]
            message: str = spec["message"]
            body: str | None = spec.get("body")

            # Stage files
            add_err = _stage_files(files, resolved)
            if add_err:
                return ToolResult(
                    success=False,
                    error=f"Commit {i + 1}: git add failed: {add_err}",
                    data={"results": results, "succeeded": len(results)},
                )

            # Build commit command
            commit_args = ["commit", "-m", message]
            if body:
                commit_args.extend(["-m", body])

            # Attempt commit with auto-retry
            ok, retried, output = _attempt_commit(commit_args, files, resolved)

            if not ok:
                return ToolResult(
                    success=False,
                    error=f"Commit {i + 1}: pre-commit failed",
                    data=_build_failure_data(
                        results,
                        index=i + 1,
                        message=message,
                        output=output,
                        retried=retried,
                        path=resolved,
                    ),
                )

            # Get the SHA of the commit
            log = run_git(["log", "-1", "--format=%H"], resolved)
            sha = log.stdout.strip()[:7]

            results.append(
                {
                    "sha": sha,
                    "message": message,
                    "precommit_passed": True,
                    "retried": retried,
                }
            )

        return ToolResult(
            success=True,
            data={
                "results": results,
                "total": len(results),
                "succeeded": len(results),
            },
        )
name property

Tool name used for MCP registration.

execute(*, path='.', commits=None, **kwargs)

Execute batched commits.

Parameters:

Name Type Description Default
path str

Project root (required).

'.'
commits list[dict[str, Any]] | None

List of commit specs, each a dict with keys: - files (list[str]): Files to stage. - message (str): Commit summary line. - body (str, optional): Commit body.

None

Returns:

Type Description
ToolResult

ToolResult with list of committed results.

Source code in packages/axm-git/src/axm_git/tools/commit.py
def execute(
    self,
    *,
    path: str = ".",
    commits: list[dict[str, Any]] | None = None,
    **kwargs: Any,
) -> ToolResult:
    """Execute batched commits.

    Args:
        path: Project root (required).
        commits: List of commit specs, each a dict with keys:
            - ``files`` (list[str]): Files to stage.
            - ``message`` (str): Commit summary line.
            - ``body`` (str, optional): Commit body.

    Returns:
        ToolResult with list of committed results.
    """
    resolved = Path(path).resolve()
    commit_list: list[dict[str, Any]] = commits or []

    if not commit_list:
        return ToolResult(success=False, error="No commits provided")

    # Fail fast with suggestions if not a git repo
    check = run_git(["rev-parse", "--git-dir"], resolved)
    if check.returncode != 0:
        return not_a_repo_error(check.stderr, resolved)

    results: list[dict[str, Any]] = []

    for i, spec in enumerate(commit_list):
        validation_err = _validate_commit_spec(spec, i + 1, results)
        if validation_err:
            return validation_err

        files: list[str] = spec["files"]
        message: str = spec["message"]
        body: str | None = spec.get("body")

        # Stage files
        add_err = _stage_files(files, resolved)
        if add_err:
            return ToolResult(
                success=False,
                error=f"Commit {i + 1}: git add failed: {add_err}",
                data={"results": results, "succeeded": len(results)},
            )

        # Build commit command
        commit_args = ["commit", "-m", message]
        if body:
            commit_args.extend(["-m", body])

        # Attempt commit with auto-retry
        ok, retried, output = _attempt_commit(commit_args, files, resolved)

        if not ok:
            return ToolResult(
                success=False,
                error=f"Commit {i + 1}: pre-commit failed",
                data=_build_failure_data(
                    results,
                    index=i + 1,
                    message=message,
                    output=output,
                    retried=retried,
                    path=resolved,
                ),
            )

        # Get the SHA of the commit
        log = run_git(["log", "-1", "--format=%H"], resolved)
        sha = log.stdout.strip()[:7]

        results.append(
            {
                "sha": sha,
                "message": message,
                "precommit_passed": True,
                "retried": retried,
            }
        )

    return ToolResult(
        success=True,
        data={
            "results": results,
            "total": len(results),
            "succeeded": len(results),
        },
    )