casecrash 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hao li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: casecrash
3
+ Version: 0.1.0
4
+ Summary: Find filenames in your git index that would collide on macOS or Windows checkouts
5
+ Author: hao li
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hahahahahahahahah6/casecrash
8
+ Keywords: git,macos,windows,filename,case-sensitivity,unicode,ci
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Version Control :: Git
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # casecrash
22
+
23
+ **Git can track two files your Mac cannot check out.**
24
+
25
+ `README.md` and `readme.md` can sit side by side in a git repo — until
26
+ someone on macOS or Windows clones it and the checkout explodes. The same
27
+ goes for Unicode lookalikes: NFC `café` vs NFD `café` look identical in
28
+ your terminal but are different byte sequences, and macOS treats them as
29
+ the same file.
30
+
31
+ casecrash reads your git index and flags every pair of tracked files that
32
+ would collide on a case-insensitive or Unicode-normalizing filesystem —
33
+ *before* it breaks a teammate's checkout.
34
+
35
+ ## Quickstart
36
+
37
+ ```bash
38
+ pip install casecrash
39
+ cd your-repo
40
+ casecrash
41
+ ```
42
+
43
+ ```
44
+ casecrash: found 2 filename collision(s) in /tmp/demorepo (5 files scanned):
45
+
46
+ [normalization] collide after Unicode normalization (macOS):
47
+ 'cafe\u0301.md' [NFD]
48
+ 'caf\u00e9.md' [NFC]
49
+ -> breaks on: macOS
50
+
51
+ [case] collide on case-insensitive filesystems (macOS, Windows):
52
+ 'README.md'
53
+ 'readme.md'
54
+ -> breaks on: macOS, Windows
55
+
56
+ Fix: rename one of each colliding pair before a macOS/Windows user clones this repo. 'git mv' the odd one out and commit.
57
+ ```
58
+
59
+ No collisions? Exit 0 and a clean bill of health:
60
+
61
+ ```
62
+ casecrash: no filename collisions in the git index (128 files scanned).
63
+ ```
64
+
65
+ ## What it detects
66
+
67
+ | Kind | Example | Breaks on |
68
+ |---|---|---|
69
+ | `case` | `README.md` vs `readme.md`, `É` vs `é` | macOS, Windows |
70
+ | `normalization` | NFC `café` vs NFD `café` | macOS (APFS compares names without regard to normalization) |
71
+ | `exact-duplicate` | identical index entry twice | everywhere (index corruption) |
72
+
73
+ Detection is done on the git index (`git ls-files`), not the working
74
+ tree, so it works on any OS — a Linux CI runner can catch a collision
75
+ that would only bite macOS users. Filenames are handled as raw bytes, so
76
+ undecodable names can never crash the tool. Submodules are skipped (and
77
+ counted).
78
+
79
+ ## CI usage
80
+
81
+ casecrash exits **1** when collisions are found, **0** when clean, **2** on
82
+ errors — so it drops straight into any pipeline:
83
+
84
+ ```yaml
85
+ # .github/workflows/casecrash.yml
86
+ name: casecheck
87
+ on: [push, pull_request]
88
+ jobs:
89
+ casecrash:
90
+ runs-on: ubuntu-latest
91
+ steps:
92
+ - uses: actions/checkout@v4
93
+ - uses: actions/setup-python@v5
94
+ with:
95
+ python-version: "3.12"
96
+ - run: pip install casecrash
97
+ - run: casecrash
98
+ ```
99
+
100
+ JSON output for scripting:
101
+
102
+ ```bash
103
+ casecrash --format json
104
+ ```
105
+
106
+ ## Why this happens
107
+
108
+ Git was born on case-sensitive Linux filesystems and its index happily
109
+ stores `File.txt` and `file.txt` as two separate entries. macOS and
110
+ Windows disagree: checking out the second name silently overwrites the
111
+ first (or the clone fails outright). Normalization collisions are
112
+ sneakier — macOS's HFS+/APFS normalizes filenames, so two
113
+ byte-different, canonically-equivalent names are the same file there.
114
+
115
+ This bites real projects: agents and scripts that generate files with
116
+ slightly different casings, Unicode filenames pasted from different
117
+ platforms, and the occasional CVE (see
118
+ [CVE-2021-21300](https://github.blog/security/vulnerability-research/github-security-lab-cve-2021-21300/),
119
+ where git's own checkout could be tricked on case-insensitive
120
+ filesystems).
121
+
122
+ ## Install
123
+
124
+ ```bash
125
+ pip install casecrash
126
+ ```
127
+
128
+ Requires Python 3.9+ and git. Zero dependencies.
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,112 @@
1
+ # casecrash
2
+
3
+ **Git can track two files your Mac cannot check out.**
4
+
5
+ `README.md` and `readme.md` can sit side by side in a git repo — until
6
+ someone on macOS or Windows clones it and the checkout explodes. The same
7
+ goes for Unicode lookalikes: NFC `café` vs NFD `café` look identical in
8
+ your terminal but are different byte sequences, and macOS treats them as
9
+ the same file.
10
+
11
+ casecrash reads your git index and flags every pair of tracked files that
12
+ would collide on a case-insensitive or Unicode-normalizing filesystem —
13
+ *before* it breaks a teammate's checkout.
14
+
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ pip install casecrash
19
+ cd your-repo
20
+ casecrash
21
+ ```
22
+
23
+ ```
24
+ casecrash: found 2 filename collision(s) in /tmp/demorepo (5 files scanned):
25
+
26
+ [normalization] collide after Unicode normalization (macOS):
27
+ 'cafe\u0301.md' [NFD]
28
+ 'caf\u00e9.md' [NFC]
29
+ -> breaks on: macOS
30
+
31
+ [case] collide on case-insensitive filesystems (macOS, Windows):
32
+ 'README.md'
33
+ 'readme.md'
34
+ -> breaks on: macOS, Windows
35
+
36
+ Fix: rename one of each colliding pair before a macOS/Windows user clones this repo. 'git mv' the odd one out and commit.
37
+ ```
38
+
39
+ No collisions? Exit 0 and a clean bill of health:
40
+
41
+ ```
42
+ casecrash: no filename collisions in the git index (128 files scanned).
43
+ ```
44
+
45
+ ## What it detects
46
+
47
+ | Kind | Example | Breaks on |
48
+ |---|---|---|
49
+ | `case` | `README.md` vs `readme.md`, `É` vs `é` | macOS, Windows |
50
+ | `normalization` | NFC `café` vs NFD `café` | macOS (APFS compares names without regard to normalization) |
51
+ | `exact-duplicate` | identical index entry twice | everywhere (index corruption) |
52
+
53
+ Detection is done on the git index (`git ls-files`), not the working
54
+ tree, so it works on any OS — a Linux CI runner can catch a collision
55
+ that would only bite macOS users. Filenames are handled as raw bytes, so
56
+ undecodable names can never crash the tool. Submodules are skipped (and
57
+ counted).
58
+
59
+ ## CI usage
60
+
61
+ casecrash exits **1** when collisions are found, **0** when clean, **2** on
62
+ errors — so it drops straight into any pipeline:
63
+
64
+ ```yaml
65
+ # .github/workflows/casecrash.yml
66
+ name: casecheck
67
+ on: [push, pull_request]
68
+ jobs:
69
+ casecrash:
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: actions/setup-python@v5
74
+ with:
75
+ python-version: "3.12"
76
+ - run: pip install casecrash
77
+ - run: casecrash
78
+ ```
79
+
80
+ JSON output for scripting:
81
+
82
+ ```bash
83
+ casecrash --format json
84
+ ```
85
+
86
+ ## Why this happens
87
+
88
+ Git was born on case-sensitive Linux filesystems and its index happily
89
+ stores `File.txt` and `file.txt` as two separate entries. macOS and
90
+ Windows disagree: checking out the second name silently overwrites the
91
+ first (or the clone fails outright). Normalization collisions are
92
+ sneakier — macOS's HFS+/APFS normalizes filenames, so two
93
+ byte-different, canonically-equivalent names are the same file there.
94
+
95
+ This bites real projects: agents and scripts that generate files with
96
+ slightly different casings, Unicode filenames pasted from different
97
+ platforms, and the occasional CVE (see
98
+ [CVE-2021-21300](https://github.blog/security/vulnerability-research/github-security-lab-cve-2021-21300/),
99
+ where git's own checkout could be tricked on case-insensitive
100
+ filesystems).
101
+
102
+ ## Install
103
+
104
+ ```bash
105
+ pip install casecrash
106
+ ```
107
+
108
+ Requires Python 3.9+ and git. Zero dependencies.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "casecrash"
7
+ version = "0.1.0"
8
+ description = "Find filenames in your git index that would collide on macOS or Windows checkouts"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "hao li"}]
13
+ keywords = ["git", "macos", "windows", "filename", "case-sensitivity", "unicode", "ci"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Version Control :: Git",
21
+ "Topic :: Utilities",
22
+ ]
23
+
24
+ [project.scripts]
25
+ casecrash = "casecrash.cli:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/hahahahahahahahah6/casecrash"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+ include = ["casecrash*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """casecrash: find filenames in your git index that would collide on
2
+ case-insensitive or Unicode-normalizing filesystems (macOS, Windows)."""
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["__version__"]
@@ -0,0 +1,147 @@
1
+ """Command-line interface for casecrash."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ from . import __version__
11
+ from .core import (
12
+ GitNotFound,
13
+ NotAGitRepo,
14
+ escaped_display,
15
+ normalization_form,
16
+ safe_display,
17
+ scan,
18
+ )
19
+
20
+ EXIT_OK = 0
21
+ EXIT_COLLISIONS = 1
22
+ EXIT_ERROR = 2
23
+
24
+
25
+ def build_parser() -> argparse.ArgumentParser:
26
+ parser = argparse.ArgumentParser(
27
+ prog="casecrash",
28
+ description=(
29
+ "Find filenames in your git index that would collide on "
30
+ "case-insensitive or Unicode-normalizing filesystems "
31
+ "(macOS, Windows) — before they break someone's checkout."
32
+ ),
33
+ )
34
+ parser.add_argument(
35
+ "path",
36
+ nargs="?",
37
+ default=".",
38
+ help="directory inside the git repository to scan (default: .)",
39
+ )
40
+ parser.add_argument(
41
+ "--format",
42
+ choices=["text", "json"],
43
+ default="text",
44
+ help="output format (default: text)",
45
+ )
46
+ parser.add_argument(
47
+ "--version",
48
+ action="version",
49
+ version="casecrash " + __version__,
50
+ )
51
+ return parser
52
+
53
+
54
+ def _platform_note(kind: str) -> str:
55
+ if kind == "case":
56
+ return "collide on case-insensitive filesystems (macOS, Windows)"
57
+ if kind == "normalization":
58
+ return "collide after Unicode normalization (macOS)"
59
+ return "identical index entries (index corruption)"
60
+
61
+
62
+ def _format_name(raw: bytes) -> str:
63
+ form = normalization_form(raw)
64
+ shown = escaped_display(raw)
65
+ if form:
66
+ return "'%s' [%s]" % (shown, form)
67
+ return "'%s'" % shown
68
+
69
+
70
+ def render_text(result) -> str:
71
+ lines = []
72
+ if not result.collisions:
73
+ lines.append(
74
+ "casecrash: no filename collisions in the git index "
75
+ "(%d files scanned%s)." % (
76
+ result.files_scanned,
77
+ ", %d submodule(s) skipped" % result.gitlinks_skipped
78
+ if result.gitlinks_skipped else "",
79
+ )
80
+ )
81
+ return "\n".join(lines) + "\n"
82
+
83
+ lines.append(
84
+ "casecrash: found %d filename collision(s) in %s "
85
+ "(%d files scanned):"
86
+ % (len(result.collisions), result.repo_root, result.files_scanned)
87
+ )
88
+ for collision in result.collisions:
89
+ lines.append("")
90
+ lines.append("[%s] %s:" % (collision.kind, _platform_note(collision.kind)))
91
+ for raw in collision.names:
92
+ lines.append(" %s" % _format_name(raw))
93
+ lines.append(
94
+ " -> breaks on: %s" % ", ".join(collision.platforms)
95
+ )
96
+ if result.gitlinks_skipped:
97
+ lines.append("")
98
+ lines.append(
99
+ "note: %d submodule(s) skipped (not checked)"
100
+ % result.gitlinks_skipped
101
+ )
102
+ lines.append("")
103
+ lines.append(
104
+ "Fix: rename one of each colliding pair before a macOS/Windows "
105
+ "user clones this repo. "
106
+ "'git mv' the odd one out and commit."
107
+ )
108
+ return "\n".join(lines) + "\n"
109
+
110
+
111
+ def render_json(result) -> str:
112
+ payload = {
113
+ "tool": "casecrash",
114
+ "version": __version__,
115
+ "repo_root": result.repo_root,
116
+ "files_scanned": result.files_scanned,
117
+ "gitlinks_skipped": result.gitlinks_skipped,
118
+ "collisions": [
119
+ {
120
+ "kind": collision.kind,
121
+ "platforms": collision.platforms,
122
+ "files": [safe_display(n) for n in collision.names],
123
+ }
124
+ for collision in result.collisions
125
+ ],
126
+ }
127
+ return json.dumps(payload, indent=2, ensure_ascii=True) + "\n"
128
+
129
+
130
+ def main(argv: list[str] | None = None) -> int:
131
+ args = build_parser().parse_args(argv)
132
+ try:
133
+ result = scan(args.path)
134
+ except (NotAGitRepo, GitNotFound) as exc:
135
+ print("casecrash: error: %s" % exc, file=sys.stderr)
136
+ return EXIT_ERROR
137
+
138
+ if args.format == "json":
139
+ sys.stdout.write(render_json(result))
140
+ else:
141
+ sys.stdout.write(render_text(result))
142
+
143
+ return EXIT_COLLISIONS if result.has_collisions else EXIT_OK
144
+
145
+
146
+ if __name__ == "__main__":
147
+ sys.exit(main())