Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@
# platform. Both halves are required: the attribute governs checkout, the writer
# governs regeneration.

# Catch-all, not a list: a file with no extension, or an extension added later, must not
# be the one that checks out CRLF. The rules below document intent and pin the cases.
* text=auto eol=lf

# Binaries are named so git's heuristic is not trusted for them.
*.png binary
*.gif binary
*.mp4 binary
*.ico binary
*.jpg binary
*.jpeg binary
*.woff binary
*.woff2 binary
*.pdf binary
.coverage binary

.git/hooks/** text eol=lf
*.sh text eol=lf
*.ps1 text eol=lf
Expand Down
64 changes: 64 additions & 0 deletions tests/test_gitattributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""A tracked text file is never checked out CRLF; a tracked binary is never treated as text."""

import subprocess
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
LF_ENTRY = "i/lf"
BINARY_ENTRY = "i/-text"
BINARY_TEXT_ATTRS = {"unset", "unspecified"}


def _git(*args: str, stdin: str | None = None) -> str:
return subprocess.run(
["git", *args],
cwd=ROOT,
input=stdin,
capture_output=True,
text=True,
check=True,
).stdout


def _tracked_by_index_state() -> dict[str, list[str]]:
by_state: dict[str, list[str]] = {}
for line in _git("ls-files", "--eol", "-z").split("\0"):
if not line:
continue
info, path = line.split("\t", 1)
by_state.setdefault(info.split()[0], []).append(path)
return by_state


def _attr(attr: str, paths: list[str]) -> dict[str, str]:
out = _git("check-attr", "-z", "--stdin", attr, stdin="\0".join(paths) + "\0")
fields = out.split("\0")
return {fields[i]: fields[i + 2] for i in range(0, len(fields) - 2, 3)}


@pytest.fixture(scope="module")
def tracked() -> dict[str, list[str]]:
if not (ROOT / ".git").exists():
pytest.skip("not a git checkout")
return _tracked_by_index_state()


def test_every_tracked_lf_text_file_is_pinned_to_lf(tracked):
paths = tracked.get(LF_ENTRY, [])
assert paths
wrong = {p: v for p, v in _attr("eol", paths).items() if v != "lf"}
assert not wrong, f"tracked text files without eol=lf: {sorted(wrong)}"


def test_every_tracked_binary_file_is_not_text(tracked):
paths = tracked.get(BINARY_ENTRY, [])
assert paths
wrong = {p: v for p, v in _attr("text", paths).items() if v not in BINARY_TEXT_ATTRS}
assert not wrong, f"tracked binaries git may treat as text: {sorted(wrong)}"


def test_no_tracked_file_is_crlf_in_the_index(tracked):
bad = [p for state in ("i/crlf", "i/mixed") for p in tracked.get(state, [])]
assert not bad, f"CRLF in the index: {sorted(bad)}"
Loading