Skip to content

Commit 4e576e7

Browse files
committed
fix: add support for gitignore
Fixes: #1887
1 parent 42f313f commit 4e576e7

File tree

2 files changed

+204
-43
lines changed

2 files changed

+204
-43
lines changed

codespell_lib/_codespell.py

Lines changed: 135 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import os
2525
import re
2626
import shlex
27+
import subprocess
2728
import sys
2829
import textwrap
2930
from collections.abc import Iterable, Sequence
@@ -558,6 +559,15 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
558559
'you\'d give "*.eps,*.txt" to this option.',
559560
)
560561

562+
parser.add_argument(
563+
"--use-git-ignore",
564+
action="store_true",
565+
default=False,
566+
dest="use_git_ignore",
567+
help="respect .gitignore by using 'git ls-files' to identify files to check. "
568+
"Only files tracked by git will be checked.",
569+
)
570+
561571
parser.add_argument(
562572
"-x",
563573
"--exclude-file",
@@ -1228,6 +1238,56 @@ def flatten_clean_comma_separated_arguments(
12281238
]
12291239

12301240

1241+
def get_git_tracked_files(paths: list[str]) -> list[str]:
1242+
"""Get list of files tracked by git using git ls-files."""
1243+
try:
1244+
# If no paths specified, use current directory
1245+
if not paths:
1246+
paths = ["."]
1247+
1248+
all_files = []
1249+
for path in paths:
1250+
# If path is a directory, run git ls-files from within it
1251+
if os.path.isdir(path):
1252+
result = subprocess.run(
1253+
["git", "ls-files"],
1254+
capture_output=True,
1255+
text=True,
1256+
check=True,
1257+
cwd=path,
1258+
)
1259+
# Prepend the path to each file
1260+
files = [
1261+
os.path.join(path, f)
1262+
for f in result.stdout.strip().split("\n")
1263+
if f
1264+
]
1265+
all_files.extend(files)
1266+
else:
1267+
# For specific files, check if they're tracked
1268+
# Get the directory and filename
1269+
dirname = os.path.dirname(path) or "."
1270+
basename = os.path.basename(path)
1271+
result = subprocess.run(
1272+
["git", "ls-files", "--", basename],
1273+
capture_output=True,
1274+
text=True,
1275+
check=True,
1276+
cwd=dirname,
1277+
)
1278+
files = result.stdout.strip().split("\n")
1279+
if files and files[0]:
1280+
all_files.append(path)
1281+
1282+
return all_files
1283+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
1284+
print(
1285+
f"ERROR: Failed to run 'git ls-files': {e}",
1286+
file=sys.stderr,
1287+
)
1288+
return []
1289+
1290+
12311291
def _script_main() -> int:
12321292
"""Wrap to main() for setuptools."""
12331293
try:
@@ -1411,52 +1471,21 @@ def main(*args: str) -> int:
14111471
)
14121472

14131473
bad_count = 0
1414-
for filename in sorted(options.files):
1415-
# ignore hidden files
1416-
if is_hidden(filename, options.check_hidden):
1417-
continue
14181474

1419-
if os.path.isdir(filename):
1420-
for root, dirs, files in os.walk(filename):
1421-
if glob_match.match(root): # skip (absolute) directories
1422-
dirs.clear()
1423-
continue
1424-
if is_hidden(root, options.check_hidden): # dir itself hidden
1425-
continue
1426-
for file_ in sorted(files):
1427-
# ignore hidden files in directories
1428-
if is_hidden(file_, options.check_hidden):
1429-
continue
1430-
if glob_match.match(file_): # skip files
1431-
continue
1432-
fname = os.path.join(root, file_)
1433-
if glob_match.match(fname): # skip paths
1434-
continue
1435-
bad_count += parse_file(
1436-
fname,
1437-
colors,
1438-
summary,
1439-
misspellings,
1440-
ignore_words_cased,
1441-
exclude_lines,
1442-
file_opener,
1443-
word_regex,
1444-
ignore_word_regex,
1445-
uri_regex,
1446-
uri_ignore_words,
1447-
context,
1448-
options,
1449-
)
1475+
# Use git ls-files if requested
1476+
if options.use_git_ignore:
1477+
git_files = get_git_tracked_files(options.files)
1478+
if not git_files:
1479+
return EX_OK
14501480

1451-
# skip (relative) directories
1452-
dirs[:] = [
1453-
dir_
1454-
for dir_ in dirs
1455-
if not glob_match.match(dir_)
1456-
and not is_hidden(dir_, options.check_hidden)
1457-
]
1481+
for filename in git_files:
1482+
# Apply skip patterns
1483+
if glob_match.match(filename):
1484+
continue
1485+
# Check if file should be processed (hidden file check)
1486+
if is_hidden(filename, options.check_hidden):
1487+
continue
14581488

1459-
elif not glob_match.match(filename): # skip files
14601489
bad_count += parse_file(
14611490
filename,
14621491
colors,
@@ -1472,6 +1501,69 @@ def main(*args: str) -> int:
14721501
context,
14731502
options,
14741503
)
1504+
else:
1505+
# Original directory walking behavior
1506+
for filename in sorted(options.files):
1507+
# ignore hidden files
1508+
if is_hidden(filename, options.check_hidden):
1509+
continue
1510+
1511+
if os.path.isdir(filename):
1512+
for root, dirs, files in os.walk(filename):
1513+
if glob_match.match(root): # skip (absolute) directories
1514+
dirs.clear()
1515+
continue
1516+
if is_hidden(root, options.check_hidden): # dir itself hidden
1517+
continue
1518+
for file_ in sorted(files):
1519+
# ignore hidden files in directories
1520+
if is_hidden(file_, options.check_hidden):
1521+
continue
1522+
if glob_match.match(file_): # skip files
1523+
continue
1524+
fname = os.path.join(root, file_)
1525+
if glob_match.match(fname): # skip paths
1526+
continue
1527+
bad_count += parse_file(
1528+
fname,
1529+
colors,
1530+
summary,
1531+
misspellings,
1532+
ignore_words_cased,
1533+
exclude_lines,
1534+
file_opener,
1535+
word_regex,
1536+
ignore_word_regex,
1537+
uri_regex,
1538+
uri_ignore_words,
1539+
context,
1540+
options,
1541+
)
1542+
1543+
# skip (relative) directories
1544+
dirs[:] = [
1545+
dir_
1546+
for dir_ in dirs
1547+
if not glob_match.match(dir_)
1548+
and not is_hidden(dir_, options.check_hidden)
1549+
]
1550+
1551+
elif not glob_match.match(filename): # skip files
1552+
bad_count += parse_file(
1553+
filename,
1554+
colors,
1555+
summary,
1556+
misspellings,
1557+
ignore_words_cased,
1558+
exclude_lines,
1559+
file_opener,
1560+
word_regex,
1561+
ignore_word_regex,
1562+
uri_regex,
1563+
uri_ignore_words,
1564+
context,
1565+
options,
1566+
)
14751567

14761568
if summary:
14771569
print("\n-------8<-------\nSUMMARY:")

codespell_lib/tests/test_basic.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,3 +1524,72 @@ def test_args_from_file(
15241524
print("Testing with direct call to cs_.main()")
15251525
r = cs_.main(*args[1:])
15261526
print(f"{r=}")
1527+
1528+
1529+
def test_use_git_ignore(
1530+
tmp_path: Path,
1531+
capsys: pytest.CaptureFixture[str],
1532+
) -> None:
1533+
"""Test --use-git-ignore option respects .gitignore."""
1534+
# Initialize a git repo
1535+
subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)
1536+
subprocess.run(
1537+
["git", "config", "user.email", "test@test.com"],
1538+
cwd=tmp_path,
1539+
check=True,
1540+
capture_output=True,
1541+
)
1542+
subprocess.run(
1543+
["git", "config", "user.name", "Test User"],
1544+
cwd=tmp_path,
1545+
check=True,
1546+
capture_output=True,
1547+
)
1548+
# Disable hooks to avoid any pre-commit issues
1549+
subprocess.run(
1550+
["git", "config", "core.hooksPath", "/dev/null"],
1551+
cwd=tmp_path,
1552+
check=True,
1553+
capture_output=True,
1554+
)
1555+
1556+
# Create files
1557+
tracked_file = tmp_path / "tracked.txt"
1558+
ignored_file = tmp_path / "ignored.txt"
1559+
gitignore = tmp_path / ".gitignore"
1560+
1561+
tracked_file.write_text("abandonned\n")
1562+
ignored_file.write_text("abandonned\n")
1563+
gitignore.write_text("ignored.txt\n")
1564+
1565+
# Add and commit tracked file and .gitignore
1566+
subprocess.run(
1567+
["git", "add", "tracked.txt", ".gitignore"],
1568+
cwd=tmp_path,
1569+
check=True,
1570+
capture_output=True,
1571+
)
1572+
result = subprocess.run(
1573+
["git", "commit", "-m", "Initial commit", "--no-gpg-sign"],
1574+
cwd=tmp_path,
1575+
capture_output=True,
1576+
text=True,
1577+
)
1578+
if result.returncode != 0:
1579+
# Print debug info if commit fails
1580+
print(f"Git commit failed: {result.stderr}")
1581+
pytest.skip("Git commit failed, possibly due to hooks")
1582+
1583+
# Run codespell with --use-git-ignore
1584+
# Should only check tracked.txt, not ignored.txt
1585+
code, stdout, stderr = cs.main(
1586+
"--use-git-ignore",
1587+
tmp_path,
1588+
std=True,
1589+
)
1590+
1591+
# Should find error in tracked.txt
1592+
assert "tracked.txt:1: abandonned ==> abandoned" in stdout
1593+
# Should NOT find error in ignored.txt
1594+
assert "ignored.txt" not in stdout
1595+
assert code == 1 # Should have found 1 error

0 commit comments

Comments
 (0)