Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Blocking check for tracked files whose paths collide when case is ignored.
|
||||
|
||||
Linux is case-sensitive; Windows and macOS (default) are not. Two tracked
|
||||
paths that differ only by case — ``README.md`` and ``readme.md``, or
|
||||
``src/Foo.py`` and ``SRC/foo.py`` — coexist happily in a Linux checkout and
|
||||
silently break every clone on a case-insensitive host: the filesystem can
|
||||
hold only one of them, so checkout either refuses or whichever file is
|
||||
written last wins and clobbers the other. Git itself won't stop the pair
|
||||
from landing — it only warns at checkout time, on a case-insensitive FS,
|
||||
for whichever client happens to do the checkout, and the collision is
|
||||
invisible on Linux. This check is the enforcement point: scan the index,
|
||||
fail the build, name the offenders.
|
||||
|
||||
Usage:
|
||||
# Check the checkout this script lives in (CI + the common local case)
|
||||
python scripts/check-case-collisions.py
|
||||
|
||||
# Check an arbitrary git checkout (tests, other worktrees)
|
||||
python scripts/check-case-collisions.py /path/to/other/repo
|
||||
|
||||
Exit status:
|
||||
0 — no case-colliding tracked paths
|
||||
1 — at least one collision group (paths printed to stdout)
|
||||
2 — not in a git repository / git failed
|
||||
|
||||
Comparison key: the casefolded FULL path (``str.casefold``), not the
|
||||
basename — on a case-insensitive filesystem the entire path is
|
||||
case-insensitive, so ``dir/Foo.txt`` and ``DIR/foo.txt`` collide just like
|
||||
same-directory pairs. ``casefold`` (not ``lower``) is used because it
|
||||
matches how the OSes fold case for non-ASCII text (straße vs strasse,
|
||||
sigma variants); a pair it flags is a genuine collision on macOS/Windows
|
||||
even when Linux disagrees.
|
||||
|
||||
Deliberately out of scope: Unicode NFC/NFD normalization collisions (macOS
|
||||
stores NFD, Linux NFC). git already handles those at checkout via
|
||||
``core.precomposeunicode``; this check is strictly about case.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"root",
|
||||
nargs="?",
|
||||
default=str(REPO_ROOT),
|
||||
help="git checkout to scan (default: the repo this script lives in)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
os.chdir(args.root)
|
||||
except OSError as exc:
|
||||
print(f"::error::cannot enter {args.root}: {exc}")
|
||||
return 2
|
||||
|
||||
proc = subprocess.run(["git", "ls-files", "-z"], capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
msg = proc.stderr.decode("utf-8", errors="replace").strip()
|
||||
print(f"::error::git ls-files failed in {args.root}: {msg}")
|
||||
return 2
|
||||
|
||||
paths = [
|
||||
p.decode("utf-8", errors="surrogateescape")
|
||||
for p in proc.stdout.split(b"\0")
|
||||
if p
|
||||
]
|
||||
|
||||
by_casefold: dict[str, list[str]] = defaultdict(list)
|
||||
for path in paths:
|
||||
by_casefold[path.casefold()].append(path)
|
||||
|
||||
collisions = {key: group for key, group in by_casefold.items() if len(group) > 1}
|
||||
|
||||
if not collisions:
|
||||
print(f"::notice::{len(paths)} tracked files, no case-colliding paths.")
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"::error::Found {len(collisions)} case-collision group(s) among "
|
||||
f"{len(paths)} tracked files."
|
||||
)
|
||||
print(
|
||||
"Paths that differ only by case are ONE file on Windows/macOS but "
|
||||
"several on Linux - the pair breaks every clone on a case-insensitive "
|
||||
"host. Rename one member of each group so the paths differ beyond case."
|
||||
)
|
||||
print()
|
||||
for key, group in sorted(collisions.items()):
|
||||
for path in sorted(group):
|
||||
print(f" {path}")
|
||||
print()
|
||||
print(
|
||||
"Fix: `git mv` one path in each group to a name that doesn't collide. "
|
||||
"On Windows/macOS you may need two steps (`git mv a.txt tmp && git mv "
|
||||
"tmp A.txt`) because the filesystem can't hold both spellings at once."
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user