Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 43 additions & 0 deletions tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,49 @@ 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):
"""``git rev-parse`` exits 0 but says ``false`` when run from inside the
``.git`` directory of a bare repo. ``is_git_project`` is meant to gate
Comment thread
bearomorphism marked this conversation as resolved.
Outdated
work-tree commands, so this case should still 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
):
"""The ``true`` token must be matched as a whole word at the end of the
output, not just any string ending in ``true``. Even though git itself
will never produce these strings, a defensive matcher shouldn't accept
them either."""
Comment thread
bearomorphism marked this conversation as resolved.
Outdated
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