Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion commitizen/git.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import os
import re
from enum import Enum
from functools import lru_cache
from logging import getLogger
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING
Expand All @@ -14,6 +16,15 @@
from collections.abc import Sequence


logger = getLogger("commitizen")


# Match common ANSI control sequences (CSI ``\x1b[...letter`` and SGR resets)
# so wrappers that wrap git's output in colour codes don't break the
# ``true`` / ``false`` parse in :func:`is_git_project` (#1497).
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")


class EOLType(Enum):
"""The EOL type from `git config core.eol`."""

Expand Down Expand Up @@ -312,8 +323,32 @@ def is_staging_clean() -> bool:


def is_git_project() -> bool:
"""Check whether we're inside a git work tree.

Trusts ``git rev-parse``'s exit code as the primary signal: if the command
exits non-zero, we're not in a git context. The textual output (``true`` /
``false``) is then used to distinguish a work tree from a bare-repo
interior. ANSI colour codes (which some shell wrappers inject) are
stripped before the exact-match check, so legitimate-looking strings like
``untrue`` / ``nottrue`` are still rejected (#1497).
"""
c = cmd.run(["git", "rev-parse", "--is-inside-work-tree"])
return c.out.strip() == "true"
if c.return_code != 0:
logger.debug(
"is_git_project: git rev-parse failed (rc=%d) out=%r err=%r",
c.return_code,
c.out,
c.err,
)
return False
cleaned = _ANSI_ESCAPE.sub("", c.out).strip().lower()
inside_work_tree = cleaned == "true"
if not inside_work_tree:
logger.debug(
"is_git_project: git rev-parse said not a work tree: out=%r",
c.out,
)
return inside_work_tree


def get_core_editor() -> str | None:
Expand Down
42 changes: 42 additions & 0 deletions tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,48 @@ def test_is_staging_clean_when_updating_file():
assert git.is_staging_clean() is False


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_is_git_project_inside_work_tree():
assert git.is_git_project() is True


def test_is_git_project_outside_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""When ``git rev-parse`` exits non-zero (no repo above us),
``is_git_project`` returns ``False``."""
monkeypatch.chdir(tmp_path)
assert git.is_git_project() is False


def test_is_git_project_accepts_loose_true_output(mocker: MockFixture):
"""Regression test for #1497: shell wrappers that prepend ANSI colour
codes or trailing whitespace to ``git rev-parse``'s output must not flip
``is_git_project`` to ``False``."""
fake = cmd.Command("\x1b[0mtrue\r\n", "", b"", b"", 0)
mocker.patch("commitizen.cmd.run", return_value=fake)
assert git.is_git_project() is True


def test_is_git_project_returns_false_for_bare_repo(mocker: MockFixture):
"""When ``git rev-parse --is-inside-work-tree`` returns ``false``
(inside ``.git/`` of a normal repo, or at the root of a bare repo),
``is_git_project`` should return ``False``."""
fake = cmd.Command("false\n", "", b"", b"", 0)
mocker.patch("commitizen.cmd.run", return_value=fake)
assert git.is_git_project() is False


@pytest.mark.parametrize("noise", ["untrue", "nottrue", "test true false"])
def test_is_git_project_rejects_strings_containing_true_substring(
mocker: MockFixture, noise: str
):
"""After stripping ANSI escapes and whitespace, only the exact string
``true`` is accepted; outputs like ``untrue``, ``nottrue``, or
``true false`` are rejected."""
fake = cmd.Command(f"{noise}\n", "", b"", b"", 0)
mocker.patch("commitizen.cmd.run", return_value=fake)
assert git.is_git_project() is False


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_get_eol_for_open():
assert git.EOLType.for_open() == os.linesep
Expand Down
Loading