omerta 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.
omerta-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergii Shcherbak
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.
omerta-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: omerta
3
+ Version: 0.1.0
4
+ Summary: Fail when a value of your env files, e.g. an API key in .env, is in git: staged, tracked, or anywhere in the history
5
+ Keywords: secrets,env,dotenv,.env,git,pre-commit,leak,api-key,security,secret-scanning
6
+ Author: Sergii Shcherbak
7
+ Author-email: Sergii Shcherbak <sergii@shcherbak.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Operating System :: Microsoft :: Windows
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: Software Development :: Version Control :: Git
24
+ Classifier: Environment :: Console
25
+ Classifier: Typing :: Typed
26
+ Requires-Dist: hyperscan==0.9.1
27
+ Requires-Dist: python-dotenv==1.2.4
28
+ Requires-Python: >=3.11
29
+ Project-URL: Homepage, https://github.com/shcherbak-ai/omerta
30
+ Project-URL: Repository, https://github.com/shcherbak-ai/omerta
31
+ Project-URL: Issues, https://github.com/shcherbak-ai/omerta/issues
32
+ Project-URL: Changelog, https://github.com/shcherbak-ai/omerta/blob/main/CHANGELOG.md
33
+ Description-Content-Type: text/markdown
34
+
35
+ # omerta
36
+
37
+ Fail when a value of your env files, e.g. an API key in `.env`, is in git: in the staged changes, in a tracked file, or anywhere in the history.
38
+
39
+ ```bash
40
+ uvx omerta check --all # every tracked file
41
+ uvx omerta history # every commit, tag and message
42
+ ```
43
+
44
+ - Matches your actual values, as written and as encoded in URLs, JSON and base64, so it catches secrets of any format; it complements pattern scanners, which find what looks like a key.
45
+ - Reports the file, the line and the variable's name, never the value; a tracked env file is a leak in itself.
46
+ - Runs entirely offline.
47
+
48
+ As a pre-commit hook:
49
+
50
+ ```yaml
51
+ repos:
52
+ - repo: https://github.com/shcherbak-ai/omerta
53
+ rev: v0.1.0
54
+ hooks:
55
+ - id: omerta
56
+ ```
57
+
58
+ With Husky, Lefthook or a plain git hook: `uvx omerta@0.1.0 check`.
59
+
60
+ See the [repository](https://github.com/shcherbak-ai/omerta#readme) for the options, CI and configuration.
61
+
62
+ Licensed under [MIT](https://github.com/shcherbak-ai/omerta/blob/main/LICENSE).
omerta-0.1.0/PYPI.md ADDED
@@ -0,0 +1,28 @@
1
+ # omerta
2
+
3
+ Fail when a value of your env files, e.g. an API key in `.env`, is in git: in the staged changes, in a tracked file, or anywhere in the history.
4
+
5
+ ```bash
6
+ uvx omerta check --all # every tracked file
7
+ uvx omerta history # every commit, tag and message
8
+ ```
9
+
10
+ - Matches your actual values, as written and as encoded in URLs, JSON and base64, so it catches secrets of any format; it complements pattern scanners, which find what looks like a key.
11
+ - Reports the file, the line and the variable's name, never the value; a tracked env file is a leak in itself.
12
+ - Runs entirely offline.
13
+
14
+ As a pre-commit hook:
15
+
16
+ ```yaml
17
+ repos:
18
+ - repo: https://github.com/shcherbak-ai/omerta
19
+ rev: v0.1.0
20
+ hooks:
21
+ - id: omerta
22
+ ```
23
+
24
+ With Husky, Lefthook or a plain git hook: `uvx omerta@0.1.0 check`.
25
+
26
+ See the [repository](https://github.com/shcherbak-ai/omerta#readme) for the options, CI and configuration.
27
+
28
+ Licensed under [MIT](https://github.com/shcherbak-ai/omerta/blob/main/LICENSE).
@@ -0,0 +1,193 @@
1
+ [project]
2
+ name = "omerta"
3
+ version = "0.1.0"
4
+ description = "Fail when a value of your env files, e.g. an API key in .env, is in git: staged, tracked, or anywhere in the history"
5
+ readme = "PYPI.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "secrets",
11
+ "env",
12
+ "dotenv",
13
+ ".env",
14
+ "git",
15
+ "pre-commit",
16
+ "leak",
17
+ "api-key",
18
+ "security",
19
+ "secret-scanning",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Programming Language :: Python :: 3.15",
30
+ "Operating System :: POSIX :: Linux",
31
+ "Operating System :: MacOS",
32
+ "Operating System :: Microsoft :: Windows",
33
+ "Topic :: Security",
34
+ "Topic :: Software Development :: Quality Assurance",
35
+ "Topic :: Software Development :: Version Control :: Git",
36
+ "Environment :: Console",
37
+ "Typing :: Typed",
38
+ ]
39
+ dependencies = [
40
+ "hyperscan==0.9.1",
41
+ "python-dotenv==1.2.4",
42
+ ]
43
+
44
+ [[project.authors]]
45
+ name = "Sergii Shcherbak"
46
+ email = "sergii@shcherbak.ai"
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/shcherbak-ai/omerta"
50
+ Repository = "https://github.com/shcherbak-ai/omerta"
51
+ Issues = "https://github.com/shcherbak-ai/omerta/issues"
52
+ Changelog = "https://github.com/shcherbak-ai/omerta/blob/main/CHANGELOG.md"
53
+
54
+ [project.scripts]
55
+ omerta = "omerta.cli:main"
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "pytest>=8.0",
60
+ "pytest-cov>=6.0",
61
+ "pytest-xdist>=3.6",
62
+ "coverage[toml]>=7.0",
63
+ "tethered>=1.0.0",
64
+ "pre-commit>=4.0",
65
+ "pre-commit-hooks>=6.0",
66
+ "ruff>=0.9",
67
+ "bandit>=1.8",
68
+ "ty>=0.0.33",
69
+ "interrogate>=1.7",
70
+ "vulture>=2.16",
71
+ "deptry>=0.25.1",
72
+ "commitizen>=4.13",
73
+ "invoke>=2.2",
74
+ "licenseal>=1.0.1",
75
+ ]
76
+
77
+ [tool.uv]
78
+ exclude-newer = "7 days"
79
+
80
+ [tool.uv.exclude-newer-package]
81
+ certifi = "2026-10-20T02:25:00Z"
82
+ virtualenv = "2026-10-22T20:11:00Z"
83
+
84
+ [tool.pytest.ini_options]
85
+ testpaths = ["tests"]
86
+ filterwarnings = ["error"]
87
+
88
+ [tool.coverage.run]
89
+ source = ["omerta"]
90
+ branch = true
91
+
92
+ [tool.coverage.report]
93
+ show_missing = true
94
+ fail_under = 100
95
+ exclude_lines = [
96
+ "pragma: no cover",
97
+ "if TYPE_CHECKING:",
98
+ "if __name__",
99
+ ]
100
+
101
+ [tool.coverage.paths]
102
+ source = ["src"]
103
+
104
+ [tool.ruff]
105
+ target-version = "py311"
106
+ line-length = 100
107
+ src = [
108
+ "src",
109
+ "tests",
110
+ ]
111
+
112
+ [tool.ruff.lint]
113
+ select = [
114
+ "E",
115
+ "W",
116
+ "F",
117
+ "I",
118
+ "N",
119
+ "UP",
120
+ "B",
121
+ "S",
122
+ "T20",
123
+ "SIM",
124
+ "FA",
125
+ ]
126
+ ignore = ["S101"]
127
+
128
+ [tool.ruff.lint.isort]
129
+ required-imports = ["from __future__ import annotations"]
130
+ known-first-party = ["omerta"]
131
+
132
+ [tool.ruff.lint.per-file-ignores]
133
+ "tests/**/*.py" = [
134
+ "S",
135
+ "T20",
136
+ ]
137
+ "scripts/**/*.py" = ["T20"]
138
+ "tasks.py" = ["T20"]
139
+
140
+ [tool.ty.src]
141
+ include = ["src"]
142
+ exclude = ["tests"]
143
+
144
+ [tool.deptry]
145
+ known_first_party = ["omerta"]
146
+
147
+ [tool.deptry.per_rule_ignores]
148
+ DEP004 = ["invoke"]
149
+
150
+ [tool.bandit]
151
+ exclude_dirs = ["tests"]
152
+ skips = ["B101"]
153
+
154
+ [tool.vulture]
155
+ paths = ["src"]
156
+ min_confidence = 60
157
+ ignore_names = ["main"]
158
+
159
+ [tool.interrogate]
160
+ ignore-init-method = true
161
+ ignore-init-module = true
162
+ ignore-magic = true
163
+ ignore-private = true
164
+ ignore-semiprivate = true
165
+ fail-under = 100
166
+ exclude = [
167
+ "tests",
168
+ "node_modules",
169
+ ]
170
+
171
+ [tool.commitizen]
172
+ name = "cz_conventional_commits"
173
+ version = "0.1.0"
174
+ version_files = [
175
+ "src/omerta/__init__.py:__version__",
176
+ "pyproject.toml:^version",
177
+ "CITATION.cff:^version:",
178
+ "README.md:omerta@|rev: v",
179
+ "PYPI.md:omerta@|rev: v",
180
+ ]
181
+ tag_format = "v$version"
182
+ annotated_tag = true
183
+ major_version_zero = true
184
+ pre_bump_hooks = [
185
+ "uv lock --no-upgrade",
186
+ "git add uv.lock",
187
+ "uv run python scripts/date_release.py",
188
+ "uv run pytest tests/test_changelog.py -q -p no:cacheprovider",
189
+ ]
190
+
191
+ [build-system]
192
+ requires = ["uv_build>=0.13.0,<0.14"]
193
+ build-backend = "uv_build"
@@ -0,0 +1,202 @@
1
+ [project]
2
+ name = "omerta"
3
+ version = "0.1.0"
4
+ description = "Fail when a value of your env files, e.g. an API key in .env, is in git: staged, tracked, or anywhere in the history"
5
+ readme = "PYPI.md"
6
+ authors = [
7
+ { name = "Sergii Shcherbak", email = "sergii@shcherbak.ai" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ keywords = ["secrets", "env", "dotenv", ".env", "git", "pre-commit", "leak", "api-key", "security", "secret-scanning"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: 3.15",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Operating System :: MacOS",
24
+ "Operating System :: Microsoft :: Windows",
25
+ "Topic :: Security",
26
+ "Topic :: Software Development :: Quality Assurance",
27
+ "Topic :: Software Development :: Version Control :: Git",
28
+ "Environment :: Console",
29
+ "Typing :: Typed",
30
+ ]
31
+ # Pinned exactly, as uv.lock has them, which `uv lock --check` holds: both see
32
+ # the values of the env files in cleartext, and pre-commit and uvx install from
33
+ # these requirements, not from uv.lock. A release of either reaches users only
34
+ # through a release of omerta, after the cooldown and the advisories check.
35
+ dependencies = [
36
+ # Every form of every value is searched for in one pass over each blob, as
37
+ # literals of one Hyperscan database. Its wheels cover every platform but
38
+ # Windows on ARM.
39
+ "hyperscan==0.9.1",
40
+ # Findings depend on its parser: 1.2.4 parses `KEY= # comment` as empty,
41
+ # and 1.2.3 stopped a quoted value ending in an escaped backslash from
42
+ # swallowing the next lines.
43
+ "python-dotenv==1.2.4",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/shcherbak-ai/omerta"
48
+ Repository = "https://github.com/shcherbak-ai/omerta"
49
+ Issues = "https://github.com/shcherbak-ai/omerta/issues"
50
+ Changelog = "https://github.com/shcherbak-ai/omerta/blob/main/CHANGELOG.md"
51
+
52
+ [project.scripts]
53
+ omerta = "omerta.cli:main"
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "pytest>=8.0",
58
+ "pytest-cov>=6.0",
59
+ "pytest-xdist>=3.6",
60
+ "coverage[toml]>=7.0",
61
+ "tethered>=1.0.0",
62
+ "pre-commit>=4.0",
63
+ "pre-commit-hooks>=6.0",
64
+ "ruff>=0.9",
65
+ "bandit>=1.8",
66
+ "ty>=0.0.33",
67
+ "interrogate>=1.7",
68
+ "vulture>=2.16",
69
+ "deptry>=0.25.1",
70
+ "commitizen>=4.13",
71
+ "invoke>=2.2",
72
+ "licenseal>=1.0.1",
73
+ ]
74
+
75
+ [tool.uv]
76
+ # A release enters uv.lock only a week after its upload, when a compromised one
77
+ # has been found and removed.
78
+ exclude-newer = "7 days"
79
+
80
+ # Security fixes taken inside the cooldown, up to their uploads: the advisories
81
+ # hook says when an entry is no longer needed.
82
+ [tool.uv.exclude-newer-package]
83
+ # 2026.10.19 drops the root CAs whose intermediate keys were compromised
84
+ # (GHSA-6q48-6c7w-9gw2)
85
+ certifi = "2026-10-20T02:25:00Z"
86
+ # 21.14.13 escapes the --prompt value in the Bash activator (GHSA-cx6f-vjhg-g3cc)
87
+ virtualenv = "2026-10-22T20:11:00Z"
88
+
89
+ [tool.pytest.ini_options]
90
+ testpaths = ["tests"]
91
+ filterwarnings = ["error"]
92
+
93
+ [tool.coverage.run]
94
+ source = ["omerta"]
95
+ branch = true
96
+
97
+ [tool.coverage.report]
98
+ show_missing = true
99
+ fail_under = 100
100
+ exclude_lines = [
101
+ "pragma: no cover",
102
+ "if TYPE_CHECKING:",
103
+ "if __name__",
104
+ ]
105
+
106
+ [tool.coverage.paths]
107
+ source = ["src"]
108
+
109
+ [tool.ruff]
110
+ target-version = "py311"
111
+ line-length = 100
112
+ src = ["src", "tests"]
113
+
114
+ [tool.ruff.lint]
115
+ select = [
116
+ "E", # pycodestyle errors
117
+ "W", # pycodestyle warnings
118
+ "F", # pyflakes
119
+ "I", # isort
120
+ "N", # pep8-naming
121
+ "UP", # pyupgrade
122
+ "B", # flake8-bugbear
123
+ "S", # flake8-bandit
124
+ "T20", # flake8-print
125
+ "SIM", # flake8-simplify
126
+ "FA", # flake8-future-annotations (FA100: require from __future__ import annotations)
127
+ ]
128
+ ignore = [
129
+ "S101", # assert used (fine in tests)
130
+ ]
131
+
132
+ [tool.ruff.lint.isort]
133
+ required-imports = ["from __future__ import annotations"]
134
+ known-first-party = ["omerta"]
135
+
136
+ [tool.ruff.lint.per-file-ignores]
137
+ "tests/**/*.py" = ["S", "T20"]
138
+ "scripts/**/*.py" = ["T20"]
139
+ "tasks.py" = ["T20"]
140
+
141
+ [tool.ty.src]
142
+ include = ["src"]
143
+ exclude = ["tests"]
144
+
145
+ [tool.deptry]
146
+ known_first_party = ["omerta"]
147
+
148
+ [tool.deptry.per_rule_ignores]
149
+ DEP004 = [
150
+ "invoke", # tasks.py, the development tasks
151
+ ]
152
+
153
+ [tool.bandit]
154
+ exclude_dirs = ["tests"]
155
+ skips = ["B101"]
156
+
157
+ [tool.vulture]
158
+ paths = ["src"]
159
+ min_confidence = 60
160
+ # The command's entry point, which the console script calls.
161
+ ignore_names = ["main"]
162
+
163
+ [tool.interrogate]
164
+ ignore-init-method = true
165
+ ignore-init-module = true
166
+ ignore-magic = true
167
+ ignore-private = true
168
+ ignore-semiprivate = true
169
+ fail-under = 100
170
+ exclude = ["tests", "node_modules"]
171
+
172
+ [tool.commitizen]
173
+ name = "cz_conventional_commits"
174
+ version = "0.1.0"
175
+ # Commitizen replaces the version on each line that the pattern after the colon
176
+ # finds: anchored, so that `cff-version: 1.2.0` (the format of CITATION.cff)
177
+ # keeps its own. The README and the package description pin a release in their
178
+ # examples (`omerta@X.Y.Z`, `rev: vX.Y.Z`), which a bump moves too.
179
+ version_files = [
180
+ "src/omerta/__init__.py:__version__",
181
+ "pyproject.toml:^version",
182
+ "CITATION.cff:^version:",
183
+ "README.md:omerta@|rev: v",
184
+ "PYPI.md:omerta@|rev: v",
185
+ ]
186
+ tag_format = "v$version"
187
+ annotated_tag = true
188
+ major_version_zero = true
189
+ # The hooks run after the version fields are rewritten and before the commit,
190
+ # which takes in every changed file (`git commit -a`). scripts/date_release.py
191
+ # names the `## [Unreleased]` section of the changelog after the new version,
192
+ # with the day, and writes the day into CITATION.cff; the last hook checks both.
193
+ pre_bump_hooks = [
194
+ "uv lock --no-upgrade",
195
+ "git add uv.lock",
196
+ "uv run python scripts/date_release.py",
197
+ "uv run pytest tests/test_changelog.py -q -p no:cacheprovider",
198
+ ]
199
+
200
+ [build-system]
201
+ requires = ["uv_build>=0.13.0,<0.14"]
202
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ """Fail when a value of your env files is in git."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,125 @@
1
+ """``omerta check``: the index content of the staged changes, of given paths, or of every entry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from omerta.errors import OmertaError
8
+ from omerta.git import GITLINK, Entry, Git
9
+ from omerta.output import Finding
10
+ from omerta.patterns import Pattern, is_env_file
11
+ from omerta.search import Matcher, Target, line_of
12
+
13
+
14
+ def select(git: Git, index: list[Entry], paths: list[str], everything: bool) -> list[Entry]:
15
+ """The index entries that ``check`` searches.
16
+
17
+ A PATH selects the entry at that path, or with a folder the entries under
18
+ it, matched exactly, as literal pathspecs are. The paths are matched here,
19
+ not given to git, so that none goes on git's command line.
20
+
21
+ Args:
22
+ git: git, in the repository.
23
+ index: Every index entry.
24
+ paths: The PATH arguments, relative to the folder omerta runs in.
25
+ everything: ``--all``: every entry.
26
+
27
+ Raises:
28
+ OmertaError: A path is outside the repository.
29
+ """
30
+ if everything:
31
+ return index
32
+ if paths:
33
+ selected = [_from_root(git, path) for path in paths]
34
+ return [
35
+ entry
36
+ for entry in index
37
+ if any(
38
+ not chosen or entry.path == chosen or entry.path.startswith(chosen + b"/")
39
+ for chosen in selected
40
+ )
41
+ ]
42
+ staged = git.staged_paths()
43
+ return [entry for entry in index if entry.path in staged]
44
+
45
+
46
+ def _from_root(git: Git, path: str) -> bytes:
47
+ """A PATH argument as an index path: from the root, with ``/``; empty for the root."""
48
+ full = os.path.abspath(os.path.join(git.folder, path))
49
+ try:
50
+ relative = os.path.relpath(full, os.path.abspath(git.top))
51
+ except ValueError: # on another drive
52
+ relative = os.pardir
53
+ if relative == os.pardir or relative.startswith(os.pardir + os.sep):
54
+ raise OmertaError(f"{path} is outside the repository")
55
+ return b"" if relative == os.curdir else os.fsencode(relative.replace(os.sep, "/"))
56
+
57
+
58
+ def run(
59
+ git: Git,
60
+ entries: list[Entry],
61
+ targets: list[Target],
62
+ patterns: tuple[Pattern, ...],
63
+ findings: list[Finding],
64
+ ) -> None:
65
+ """Search the entries, adding what is found to ``findings``.
66
+
67
+ An entry that matches the patterns is a tracked env file, and a path that
68
+ holds a value is a finding of its own. A gitlink (a submodule) has no
69
+ content to search. An unmerged entry is searched in each of its stages,
70
+ and a value is reported at its first occurrence in the lowest stage that
71
+ holds it.
72
+
73
+ Raises:
74
+ OmertaError: An entry's object cannot be read.
75
+ """
76
+ matcher = Matcher(targets)
77
+ for path in sorted({entry.path for entry in entries}):
78
+ for index, hit in matcher.search(path).items():
79
+ findings.append(_found(targets[index], hit.form, path=path, in_path=True))
80
+ entries = sorted(
81
+ (entry for entry in entries if entry.mode != GITLINK),
82
+ key=lambda entry: (entry.path, entry.stage),
83
+ )
84
+ tracked: set[bytes] = set()
85
+ for entry in entries:
86
+ if entry.path not in tracked and is_env_file(entry.path, patterns):
87
+ tracked.add(entry.path)
88
+ findings.append(Finding(path=entry.path))
89
+ if not targets:
90
+ return
91
+ by_oid: dict[str, list[Entry]] = {}
92
+ for entry in entries:
93
+ by_oid.setdefault(entry.oid, []).append(entry)
94
+ found: dict[tuple[bytes, int], tuple[int, Finding]] = {}
95
+ try:
96
+ for oid, _, data in git.objects(list(by_oid)):
97
+ hits = matcher.search(data)
98
+ for entry in by_oid[oid]:
99
+ for index, hit in hits.items():
100
+ key = (entry.path, index)
101
+ if key in found and found[key][0] <= entry.stage:
102
+ continue
103
+ line = line_of(data, hit.position)
104
+ found[key] = (
105
+ entry.stage,
106
+ _found(targets[index], hit.form, path=entry.path, line=line),
107
+ )
108
+ finally:
109
+ # What was found before an error is reported with it.
110
+ findings.extend(finding for _, finding in found.values())
111
+
112
+
113
+ def _found(
114
+ target: Target, form: str, *, path: bytes, line: int | None = None, in_path: bool = False
115
+ ) -> Finding:
116
+ """A finding of a target in an entry: in its content at a line, or in its path."""
117
+ return Finding(
118
+ path=path,
119
+ line=line,
120
+ in_path=in_path,
121
+ variable=target.variable,
122
+ env_file=target.env_file,
123
+ env_order=target.env_order,
124
+ form=form,
125
+ )