topmark 0.1.1__tar.gz → 0.2.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.
- {topmark-0.1.1/src/topmark.egg-info → topmark-0.2.0}/PKG-INFO +89 -5
- {topmark-0.1.1 → topmark-0.2.0}/README.md +88 -4
- {topmark-0.1.1 → topmark-0.2.0}/pyproject.toml +3 -1
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/default.py +50 -9
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/main.py +11 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/config/__init__.py +14 -9
- topmark-0.2.0/src/topmark/config/topmark-default.toml +75 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/constants.py +4 -1
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/context.py +2 -2
- {topmark-0.1.1 → topmark-0.2.0/src/topmark.egg-info}/PKG-INFO +89 -5
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/SOURCES.txt +1 -0
- {topmark-0.1.1 → topmark-0.2.0}/LICENSE +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/setup.cfg +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/cli_types.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/dump_config.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/filetypes.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/init_config.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/show_defaults.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/version.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/config_resolver.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/options.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/utils.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/config/logging.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/file_resolver.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/base.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/instances.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/policy.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/registry.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/contracts.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/pipelines.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/base.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/pound.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/slash.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/xml.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/runner.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/builder.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/comparer.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/patcher.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/reader.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/renderer.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/resolver.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/scanner.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/updater.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/py.typed +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/rendering/api.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/rendering/formats.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/utils/__init__.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark/utils/file.py +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/dependency_links.txt +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/entry_points.txt +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/requires.txt +0 -0
- {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: topmark
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Python CLI to inspect, validate, and manage license and copyright headers.
|
|
5
5
|
Author-email: Olivier Biot <olivier.biot@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -127,6 +127,15 @@ Logging verbosity is controlled globally:
|
|
|
127
127
|
All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
|
|
128
128
|
subcommands like `check` or `apply`.
|
|
129
129
|
|
|
130
|
+
### Skipping and filtering helpers
|
|
131
|
+
|
|
132
|
+
These switches help keep CI output meaningful and fast:
|
|
133
|
+
|
|
134
|
+
- `--skip-compliant` — hide files that are already compliant; only show items that need attention.
|
|
135
|
+
- `--skip-unsupported` — hide unsupported file types and files matched by known types that
|
|
136
|
+
intentionally **do not** support headers (e.g., strict JSON). They remain **recognized** but are
|
|
137
|
+
not listed in results.
|
|
138
|
+
|
|
130
139
|
### Examples
|
|
131
140
|
|
|
132
141
|
```bash
|
|
@@ -159,6 +168,12 @@ topmark version
|
|
|
159
168
|
|
|
160
169
|
# Apply changes to files in-place
|
|
161
170
|
topmark --apply src/
|
|
171
|
+
|
|
172
|
+
# CI-friendly summary: only show issues; ignore unsupported types
|
|
173
|
+
topmark --skip-compliant --skip-unsupported src/
|
|
174
|
+
|
|
175
|
+
# Apply fixes for only the changed files passed by pre-commit
|
|
176
|
+
topmark --apply --apply --skip-unsupported --quiet
|
|
162
177
|
```
|
|
163
178
|
|
|
164
179
|
## 📐 Header placement rules
|
|
@@ -249,10 +264,79 @@ The following options can be used with most commands.
|
|
|
249
264
|
|
|
250
265
|
## 🧩 Supported file types
|
|
251
266
|
|
|
252
|
-
| Processor | File types (examples)
|
|
253
|
-
| -------------------- |
|
|
254
|
-
| PoundHeaderProcessor |
|
|
255
|
-
|
|
|
267
|
+
| Processor | File types (examples) |
|
|
268
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
269
|
+
| PoundHeaderProcessor | dockerfile, env, git-meta, ini, julia, makefile, perl, python, python-requirements, r, ruby, shell, toml, yaml |
|
|
270
|
+
| SlashHeaderProcessor | c, cpp, cs, go, java, javascript, kotlin, rust, swift, typescript, vscode-jsonc |
|
|
271
|
+
| XmlHeaderProcessor | html, markdown, svelte, svg, vue, xhtml, xml, xsl, xslt, yaml |
|
|
272
|
+
|
|
273
|
+
Some formats (e.g., strict JSON) are **recognized but intentionally skipped** because they lack a
|
|
274
|
+
safe comment syntax. Use `--skip-unsupported` to hide them from the report while keeping safety.
|
|
275
|
+
|
|
276
|
+
For a complete list, please run:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
topmark filetypes
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### How TopMark resolves file types (specificity & safety)
|
|
283
|
+
|
|
284
|
+
TopMark may have multiple `FileType` definitions that **match** a given path. The resolver now:
|
|
285
|
+
|
|
286
|
+
- evaluates **all** matching file types and scores them by **specificity**,
|
|
287
|
+
- prefers **explicit filenames / tail subpaths** (e.g., `.vscode/settings.json`) over patterns, and
|
|
288
|
+
**patterns** over simple **extensions**,
|
|
289
|
+
- breaks ties in favor of **headerable** types (those without `skip_processing=True`).
|
|
290
|
+
|
|
291
|
+
**Tail subpath matching.** `FileType.filenames` entries that contain a path separator (e.g.,
|
|
292
|
+
`".vscode/settings.json"`) are matched as **path suffixes** against `path.as_posix()`; plain names
|
|
293
|
+
still match the **basename** only.
|
|
294
|
+
|
|
295
|
+
**JSON vs JSONC.** Generic `json` is recognized but marked `skip_processing=True` (no comments in
|
|
296
|
+
strict JSON), while `vscode-jsonc` is a safe, **narrow** opt‑in that uses `//` headers. If you need
|
|
297
|
+
more JSON-with-comments files, add them via a dedicated `FileType` or an explicit allow‑list in
|
|
298
|
+
config.
|
|
299
|
+
|
|
300
|
+
**Shebang‑aware insertion.** The default insertion logic is policy‑driven and shebang‑aware (insert
|
|
301
|
+
after `#!` and optional encoding line). For formats like XML that need character‑precise placement,
|
|
302
|
+
processors provide a text‑offset path; `XmlHeaderProcessor` uses this and signals **no line
|
|
303
|
+
anchor**.
|
|
304
|
+
|
|
305
|
+
## 🪝 Pre-commit integration
|
|
306
|
+
|
|
307
|
+
TopMark provides first-class pre-commit hooks. A minimal consumer configuration:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
# .pre-commit-config.yaml (in a consuming repository)
|
|
311
|
+
repos:
|
|
312
|
+
- repo: https://github.com/shutterfreak/topmark
|
|
313
|
+
rev: v0.2.0 # pin to a released tag
|
|
314
|
+
hooks:
|
|
315
|
+
- id: topmark-check
|
|
316
|
+
# Optional: limit scope to supported text types
|
|
317
|
+
# files: '\\.(py|md|toml|ya?ml|sh|Makefile)$'
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Hooks shipped by this repo:
|
|
321
|
+
|
|
322
|
+
- **`topmark-check`** — non-destructive validation. Recommended on `pre-commit` / `pre-push`.
|
|
323
|
+
- **`topmark-apply`** — destructive, requires `--apply`. Marked `manual` so it only runs when you
|
|
324
|
+
call it explicitly.
|
|
325
|
+
|
|
326
|
+
**Run the manual hook locally:**
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# On the whole repo
|
|
330
|
+
pre-commit run topmark-apply --all-files --hook-stage manual
|
|
331
|
+
|
|
332
|
+
# On specific files
|
|
333
|
+
pre-commit run topmark-apply --files path/to/file1 path/to/file2 --hook-stage manual
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Why does the hook seem to run multiple times?** Pre-commit batches filenames to avoid OS
|
|
337
|
+
argument-length limits. You may see repeated banners (e.g., “Processing N file(s)”) as the hook runs
|
|
338
|
+
once per batch. To run **once** per repo, set `pass_filenames: false` in the hook manifest and let
|
|
339
|
+
TopMark discover files itself.
|
|
256
340
|
|
|
257
341
|
## 🛠 Configuration
|
|
258
342
|
|
|
@@ -78,6 +78,15 @@ Logging verbosity is controlled globally:
|
|
|
78
78
|
All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
|
|
79
79
|
subcommands like `check` or `apply`.
|
|
80
80
|
|
|
81
|
+
### Skipping and filtering helpers
|
|
82
|
+
|
|
83
|
+
These switches help keep CI output meaningful and fast:
|
|
84
|
+
|
|
85
|
+
- `--skip-compliant` — hide files that are already compliant; only show items that need attention.
|
|
86
|
+
- `--skip-unsupported` — hide unsupported file types and files matched by known types that
|
|
87
|
+
intentionally **do not** support headers (e.g., strict JSON). They remain **recognized** but are
|
|
88
|
+
not listed in results.
|
|
89
|
+
|
|
81
90
|
### Examples
|
|
82
91
|
|
|
83
92
|
```bash
|
|
@@ -110,6 +119,12 @@ topmark version
|
|
|
110
119
|
|
|
111
120
|
# Apply changes to files in-place
|
|
112
121
|
topmark --apply src/
|
|
122
|
+
|
|
123
|
+
# CI-friendly summary: only show issues; ignore unsupported types
|
|
124
|
+
topmark --skip-compliant --skip-unsupported src/
|
|
125
|
+
|
|
126
|
+
# Apply fixes for only the changed files passed by pre-commit
|
|
127
|
+
topmark --apply --apply --skip-unsupported --quiet
|
|
113
128
|
```
|
|
114
129
|
|
|
115
130
|
## 📐 Header placement rules
|
|
@@ -200,10 +215,79 @@ The following options can be used with most commands.
|
|
|
200
215
|
|
|
201
216
|
## 🧩 Supported file types
|
|
202
217
|
|
|
203
|
-
| Processor | File types (examples)
|
|
204
|
-
| -------------------- |
|
|
205
|
-
| PoundHeaderProcessor |
|
|
206
|
-
|
|
|
218
|
+
| Processor | File types (examples) |
|
|
219
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| PoundHeaderProcessor | dockerfile, env, git-meta, ini, julia, makefile, perl, python, python-requirements, r, ruby, shell, toml, yaml |
|
|
221
|
+
| SlashHeaderProcessor | c, cpp, cs, go, java, javascript, kotlin, rust, swift, typescript, vscode-jsonc |
|
|
222
|
+
| XmlHeaderProcessor | html, markdown, svelte, svg, vue, xhtml, xml, xsl, xslt, yaml |
|
|
223
|
+
|
|
224
|
+
Some formats (e.g., strict JSON) are **recognized but intentionally skipped** because they lack a
|
|
225
|
+
safe comment syntax. Use `--skip-unsupported` to hide them from the report while keeping safety.
|
|
226
|
+
|
|
227
|
+
For a complete list, please run:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
topmark filetypes
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### How TopMark resolves file types (specificity & safety)
|
|
234
|
+
|
|
235
|
+
TopMark may have multiple `FileType` definitions that **match** a given path. The resolver now:
|
|
236
|
+
|
|
237
|
+
- evaluates **all** matching file types and scores them by **specificity**,
|
|
238
|
+
- prefers **explicit filenames / tail subpaths** (e.g., `.vscode/settings.json`) over patterns, and
|
|
239
|
+
**patterns** over simple **extensions**,
|
|
240
|
+
- breaks ties in favor of **headerable** types (those without `skip_processing=True`).
|
|
241
|
+
|
|
242
|
+
**Tail subpath matching.** `FileType.filenames` entries that contain a path separator (e.g.,
|
|
243
|
+
`".vscode/settings.json"`) are matched as **path suffixes** against `path.as_posix()`; plain names
|
|
244
|
+
still match the **basename** only.
|
|
245
|
+
|
|
246
|
+
**JSON vs JSONC.** Generic `json` is recognized but marked `skip_processing=True` (no comments in
|
|
247
|
+
strict JSON), while `vscode-jsonc` is a safe, **narrow** opt‑in that uses `//` headers. If you need
|
|
248
|
+
more JSON-with-comments files, add them via a dedicated `FileType` or an explicit allow‑list in
|
|
249
|
+
config.
|
|
250
|
+
|
|
251
|
+
**Shebang‑aware insertion.** The default insertion logic is policy‑driven and shebang‑aware (insert
|
|
252
|
+
after `#!` and optional encoding line). For formats like XML that need character‑precise placement,
|
|
253
|
+
processors provide a text‑offset path; `XmlHeaderProcessor` uses this and signals **no line
|
|
254
|
+
anchor**.
|
|
255
|
+
|
|
256
|
+
## 🪝 Pre-commit integration
|
|
257
|
+
|
|
258
|
+
TopMark provides first-class pre-commit hooks. A minimal consumer configuration:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
# .pre-commit-config.yaml (in a consuming repository)
|
|
262
|
+
repos:
|
|
263
|
+
- repo: https://github.com/shutterfreak/topmark
|
|
264
|
+
rev: v0.2.0 # pin to a released tag
|
|
265
|
+
hooks:
|
|
266
|
+
- id: topmark-check
|
|
267
|
+
# Optional: limit scope to supported text types
|
|
268
|
+
# files: '\\.(py|md|toml|ya?ml|sh|Makefile)$'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Hooks shipped by this repo:
|
|
272
|
+
|
|
273
|
+
- **`topmark-check`** — non-destructive validation. Recommended on `pre-commit` / `pre-push`.
|
|
274
|
+
- **`topmark-apply`** — destructive, requires `--apply`. Marked `manual` so it only runs when you
|
|
275
|
+
call it explicitly.
|
|
276
|
+
|
|
277
|
+
**Run the manual hook locally:**
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# On the whole repo
|
|
281
|
+
pre-commit run topmark-apply --all-files --hook-stage manual
|
|
282
|
+
|
|
283
|
+
# On specific files
|
|
284
|
+
pre-commit run topmark-apply --files path/to/file1 path/to/file2 --hook-stage manual
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Why does the hook seem to run multiple times?** Pre-commit batches filenames to avoid OS
|
|
288
|
+
argument-length limits. You may see repeated banners (e.g., “Processing N file(s)”) as the hook runs
|
|
289
|
+
once per batch. To run **once** per repo, set `pass_filenames: false` in the hook manifest and let
|
|
290
|
+
TopMark discover files itself.
|
|
207
291
|
|
|
208
292
|
## 🛠 Configuration
|
|
209
293
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "topmark"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.2.0"
|
|
14
14
|
description = "A Python CLI to inspect, validate, and manage license and copyright headers."
|
|
15
15
|
authors = [{ name = "Olivier Biot", email = "olivier.biot@gmail.com" }]
|
|
16
16
|
license = "MIT"
|
|
@@ -102,9 +102,11 @@ typing = [
|
|
|
102
102
|
|
|
103
103
|
[tool.setuptools]
|
|
104
104
|
package-dir = { "" = "src" }
|
|
105
|
+
include-package-data = true
|
|
105
106
|
|
|
106
107
|
[tool.setuptools.package-data]
|
|
107
108
|
"topmark" = ["py.typed"]
|
|
109
|
+
"topmark.config" = ["topmark-default.toml"]
|
|
108
110
|
|
|
109
111
|
[tool.setuptools.packages.find]
|
|
110
112
|
where = ["src"]
|
|
@@ -29,6 +29,8 @@ Examples:
|
|
|
29
29
|
topmark --apply --diff .
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
+
import logging
|
|
33
|
+
|
|
32
34
|
import click
|
|
33
35
|
from yachalk import chalk
|
|
34
36
|
|
|
@@ -48,7 +50,7 @@ from topmark.cli.utils import (
|
|
|
48
50
|
from topmark.config.logging import get_logger
|
|
49
51
|
from topmark.file_resolver import resolve_file_list
|
|
50
52
|
from topmark.pipeline import runner
|
|
51
|
-
from topmark.pipeline.context import ComparisonStatus, ProcessingContext
|
|
53
|
+
from topmark.pipeline.context import ComparisonStatus, FileStatus, HeaderStatus, ProcessingContext
|
|
52
54
|
from topmark.pipeline.pipelines import get_pipeline
|
|
53
55
|
from topmark.rendering.formats import HeaderOutputFormat
|
|
54
56
|
|
|
@@ -75,6 +77,13 @@ logger = get_logger(__name__)
|
|
|
75
77
|
help="Hide files that are already up-to-date.",
|
|
76
78
|
)
|
|
77
79
|
@underscored_trap_option("--skip_compliant")
|
|
80
|
+
@typed_option(
|
|
81
|
+
"--skip-unsupported",
|
|
82
|
+
"skip_unsupported",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Hide unsupported file types.",
|
|
85
|
+
)
|
|
86
|
+
@underscored_trap_option("--skip_unsupported")
|
|
78
87
|
@typed_option(
|
|
79
88
|
"--format",
|
|
80
89
|
"output_format",
|
|
@@ -98,6 +107,7 @@ def default_command(
|
|
|
98
107
|
diff: bool,
|
|
99
108
|
summary_mode: bool,
|
|
100
109
|
skip_compliant: bool,
|
|
110
|
+
skip_unsupported: bool,
|
|
101
111
|
output_format: OutputFormat | None,
|
|
102
112
|
) -> None:
|
|
103
113
|
"""Run the unified default command (check/apply).
|
|
@@ -121,6 +131,7 @@ def default_command(
|
|
|
121
131
|
diff: If True, show unified diffs of header changes (human output only).
|
|
122
132
|
summary_mode: If True, print outcome counts instead of per-file details.
|
|
123
133
|
skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
|
|
134
|
+
skip_unsupported: If True, suppress unsupported file types.
|
|
124
135
|
output_format: Output format to use (default, json, or ndjson).
|
|
125
136
|
|
|
126
137
|
Raises:
|
|
@@ -182,11 +193,12 @@ def default_command(
|
|
|
182
193
|
stdin_stream = click.get_text_stream("stdin") if stdin else None
|
|
183
194
|
file_list = resolve_file_list(config, stdin_stream=stdin_stream)
|
|
184
195
|
if not file_list:
|
|
185
|
-
click.echo(chalk.blue("\
|
|
196
|
+
click.echo(chalk.blue("\nℹ️ No files to process.\n"))
|
|
186
197
|
raise SystemExit(0)
|
|
187
198
|
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
if logger.isEnabledFor(logging.INFO):
|
|
200
|
+
click.echo(chalk.blue(f"\n🔍 Processing {len(file_list)} file(s):\n"))
|
|
201
|
+
click.echo(chalk.bold.underline("📋 TopMark Results:"))
|
|
190
202
|
|
|
191
203
|
# Use summary pipeline when --summary is set, else full (compare+update+patch)
|
|
192
204
|
pipeline_name = "summary" if summary_mode else "apply"
|
|
@@ -200,16 +212,16 @@ def default_command(
|
|
|
200
212
|
results.append(ctx_obj)
|
|
201
213
|
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
|
|
202
214
|
logger.error("Filesystem error while processing %s: %s", path, e)
|
|
203
|
-
click.echo(chalk.red(f"Filesystem error processing {path}: {e}"))
|
|
215
|
+
click.echo(chalk.red(f"❌ Filesystem error processing {path}: {e}"))
|
|
204
216
|
continue
|
|
205
217
|
except UnicodeDecodeError as e:
|
|
206
218
|
logger.error("Encoding error while reading %s: %s", path, e)
|
|
207
|
-
click.echo(chalk.red(f"Encoding error in {path}: {e}"))
|
|
219
|
+
click.echo(chalk.red(f"🧵 Encoding error in {path}: {e}"))
|
|
208
220
|
continue
|
|
209
221
|
except Exception as e: # pragma: no cover - unexpected path
|
|
210
222
|
logger.exception("Unexpected error processing %s", path)
|
|
211
223
|
click.echo(
|
|
212
|
-
chalk.red(f"Unexpected error processing {path}: {e} (use -vv for traceback)")
|
|
224
|
+
chalk.red(f"⚠️ Unexpected error processing {path}: {e} (use -vv for traceback)")
|
|
213
225
|
)
|
|
214
226
|
continue
|
|
215
227
|
|
|
@@ -219,6 +231,17 @@ def default_command(
|
|
|
219
231
|
else:
|
|
220
232
|
view_results = results
|
|
221
233
|
|
|
234
|
+
if skip_unsupported:
|
|
235
|
+
view_results = [
|
|
236
|
+
r
|
|
237
|
+
for r in view_results
|
|
238
|
+
if r.status.file
|
|
239
|
+
not in [
|
|
240
|
+
FileStatus.SKIPPED_UNSUPPORTED,
|
|
241
|
+
FileStatus.SKIPPED_KNOWN_NO_HEADERS,
|
|
242
|
+
]
|
|
243
|
+
]
|
|
244
|
+
|
|
222
245
|
fmt: OutputFormat = output_format or OutputFormat.DEFAULT
|
|
223
246
|
# Machine formats first
|
|
224
247
|
if fmt is OutputFormat.NDJSON:
|
|
@@ -256,6 +279,18 @@ def default_command(
|
|
|
256
279
|
else:
|
|
257
280
|
for r in view_results:
|
|
258
281
|
click.echo(r.summary)
|
|
282
|
+
if r.status.comparison == ComparisonStatus.CHANGED:
|
|
283
|
+
if apply_changes:
|
|
284
|
+
if r.status.header == HeaderStatus.MISSING:
|
|
285
|
+
click.echo(chalk.yellow(f" ➕ Adding header for '{r.path}'"))
|
|
286
|
+
else:
|
|
287
|
+
click.echo(chalk.yellow(f" ✏️ Updating header for '{r.path}'"))
|
|
288
|
+
else:
|
|
289
|
+
click.echo(
|
|
290
|
+
chalk.yellow(
|
|
291
|
+
f" 🛠️ Run `topmark --apply {r.path}` to update this file."
|
|
292
|
+
)
|
|
293
|
+
)
|
|
259
294
|
if diff:
|
|
260
295
|
for r in view_results:
|
|
261
296
|
if r.header_diff:
|
|
@@ -277,7 +312,11 @@ def default_command(
|
|
|
277
312
|
logger.error("Failed to write %s: %s", r.path, e)
|
|
278
313
|
if fmt is OutputFormat.DEFAULT:
|
|
279
314
|
click.echo(
|
|
280
|
-
|
|
315
|
+
(
|
|
316
|
+
f"\n✅ Applied changes to {written} file(s)."
|
|
317
|
+
if written
|
|
318
|
+
else "\n✅ No changes to apply."
|
|
319
|
+
)
|
|
281
320
|
)
|
|
282
321
|
if failed:
|
|
283
322
|
raise SystemExit(1)
|
|
@@ -291,5 +330,7 @@ def default_command(
|
|
|
291
330
|
if would_change:
|
|
292
331
|
raise SystemExit(2)
|
|
293
332
|
|
|
294
|
-
|
|
333
|
+
if logger.isEnabledFor(logging.INFO):
|
|
334
|
+
click.echo("✅ All done!")
|
|
335
|
+
|
|
295
336
|
return
|
|
@@ -184,6 +184,13 @@ CONTEXT_SETTINGS = dict(
|
|
|
184
184
|
help="Hide files that are already up-to-date.",
|
|
185
185
|
)
|
|
186
186
|
@underscored_trap_option("--skip_compliant")
|
|
187
|
+
@typed_option(
|
|
188
|
+
"--skip-unsupported",
|
|
189
|
+
"skip_unsupported",
|
|
190
|
+
is_flag=True,
|
|
191
|
+
help="Hide unsupported file types.",
|
|
192
|
+
)
|
|
193
|
+
@underscored_trap_option("--skip_unsupported")
|
|
187
194
|
@typed_option(
|
|
188
195
|
"--format",
|
|
189
196
|
"output_format",
|
|
@@ -227,6 +234,7 @@ def cli(
|
|
|
227
234
|
diff: bool,
|
|
228
235
|
summary_mode: bool,
|
|
229
236
|
skip_compliant: bool,
|
|
237
|
+
skip_unsupported: bool,
|
|
230
238
|
output_format: OutputFormat | None,
|
|
231
239
|
) -> None:
|
|
232
240
|
"""
|
|
@@ -257,6 +265,7 @@ def cli(
|
|
|
257
265
|
diff: If True, show unified diffs of header changes (human output only).
|
|
258
266
|
summary_mode: If True, print outcome counts instead of per-file details.
|
|
259
267
|
skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
|
|
268
|
+
skip_unsupported: If True, suppress unsupported file types.
|
|
260
269
|
output_format: Output format to use (default, json, or ndjson).
|
|
261
270
|
|
|
262
271
|
Notes:
|
|
@@ -331,6 +340,7 @@ def cli(
|
|
|
331
340
|
diff=diff,
|
|
332
341
|
summary_mode=summary_mode,
|
|
333
342
|
skip_compliant=skip_compliant,
|
|
343
|
+
skip_unsupported=skip_unsupported,
|
|
334
344
|
output_format=output_format,
|
|
335
345
|
)
|
|
336
346
|
return
|
|
@@ -395,6 +405,7 @@ def ensure_commands_registered() -> None:
|
|
|
395
405
|
diff=p.get("diff", False),
|
|
396
406
|
summary_mode=p.get("summary_mode", False),
|
|
397
407
|
skip_compliant=p.get("skip_compliant", False),
|
|
408
|
+
skip_unsupported=p.get("skip_unsupported", False),
|
|
398
409
|
output_format=p.get("output_format"),
|
|
399
410
|
)
|
|
400
411
|
|
|
@@ -21,17 +21,17 @@ from topmark.toml or pyproject.toml.
|
|
|
21
21
|
import functools
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from datetime import datetime
|
|
24
|
+
from importlib.resources import files
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Any, TypeGuard
|
|
26
27
|
|
|
27
28
|
import toml
|
|
28
29
|
|
|
29
30
|
from topmark.cli.cli_types import ArgsNamespace
|
|
30
|
-
from topmark.
|
|
31
|
+
from topmark.config.logging import get_logger
|
|
32
|
+
from topmark.constants import DEFAULT_TOML_CONFIG_RESOURCE, VALUE_NOT_SET
|
|
31
33
|
from topmark.rendering.formats import HeaderOutputFormat
|
|
32
34
|
|
|
33
|
-
from .logging import get_logger
|
|
34
|
-
|
|
35
35
|
logger = get_logger(__name__)
|
|
36
36
|
|
|
37
37
|
|
|
@@ -103,8 +103,9 @@ class Config:
|
|
|
103
103
|
Returns:
|
|
104
104
|
str: The contents of the default TOML configuration file.
|
|
105
105
|
"""
|
|
106
|
-
logger.debug("Loading defaults from %s",
|
|
107
|
-
|
|
106
|
+
logger.debug("Loading defaults from package resource: %s", DEFAULT_TOML_CONFIG_RESOURCE)
|
|
107
|
+
resource = files("topmark.config") / DEFAULT_TOML_CONFIG_RESOURCE
|
|
108
|
+
return resource.read_text(encoding="utf-8")
|
|
108
109
|
|
|
109
110
|
@classmethod
|
|
110
111
|
def to_cleaned_toml(cls, toml_doc: str) -> str:
|
|
@@ -122,7 +123,7 @@ class Config:
|
|
|
122
123
|
return toml.dumps(parsed)
|
|
123
124
|
|
|
124
125
|
@classmethod
|
|
125
|
-
@functools.cache
|
|
126
|
+
@functools.cache
|
|
126
127
|
def from_defaults(cls) -> "Config":
|
|
127
128
|
"""
|
|
128
129
|
Load the default configuration from the bundled topmark-default.toml file.
|
|
@@ -130,12 +131,16 @@ class Config:
|
|
|
130
131
|
Returns:
|
|
131
132
|
Config: A Config instance populated with default values.
|
|
132
133
|
"""
|
|
133
|
-
|
|
134
|
+
resource = files("topmark.config") / DEFAULT_TOML_CONFIG_RESOURCE
|
|
135
|
+
default_toml_text = resource.read_text(encoding="utf-8")
|
|
136
|
+
default_toml_data = toml.loads(default_toml_text)
|
|
134
137
|
logger.debug("Default Config: %s", default_toml_data)
|
|
135
|
-
|
|
138
|
+
|
|
139
|
+
# Note: `config_file` is set to None because this is a package resource,
|
|
140
|
+
# not a user-specified filesystem path.
|
|
136
141
|
return cls.from_toml_dict(
|
|
137
142
|
default_toml_data,
|
|
138
|
-
config_file=
|
|
143
|
+
config_file=None,
|
|
139
144
|
use_defaults=True, # We ONLY include defaults when loading from defaults!
|
|
140
145
|
)
|
|
141
146
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : topmark-default.toml
|
|
4
|
+
# file_relpath : src/topmark/config/topmark-default.toml
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
#########################
|
|
12
|
+
# TopMark configuration #
|
|
13
|
+
#########################
|
|
14
|
+
# This file is used to define the default configuration for TopMark.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ==========================
|
|
18
|
+
# Header field configuration
|
|
19
|
+
# ==========================
|
|
20
|
+
|
|
21
|
+
[header]
|
|
22
|
+
# Configuration for header fields and metadata to include in generated headers
|
|
23
|
+
|
|
24
|
+
# List of fields to include in the header.
|
|
25
|
+
# Available fields include 'file', 'file_relpath', 'author', 'date', 'license', etc.
|
|
26
|
+
# You can define your own fields as well.
|
|
27
|
+
|
|
28
|
+
# TODO: review the fields: builtin field types and custom fields
|
|
29
|
+
|
|
30
|
+
fields = ["file", "file_relpath"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ========================
|
|
34
|
+
# TopMark field definition
|
|
35
|
+
# ========================
|
|
36
|
+
|
|
37
|
+
[fields]
|
|
38
|
+
|
|
39
|
+
# Project name to include in the header
|
|
40
|
+
# project = "TopMark"
|
|
41
|
+
|
|
42
|
+
# License to include in the header. Example values: 'MIT', 'GPL-3.0', 'Apache-2.0'
|
|
43
|
+
# license = "MIT"
|
|
44
|
+
|
|
45
|
+
# Copyright statement to include in the header.
|
|
46
|
+
# copyright = "(c) 2025 Your Name" # Update according to your needs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ========================
|
|
50
|
+
# Formatting configuration
|
|
51
|
+
# ========================
|
|
52
|
+
|
|
53
|
+
[formatting]
|
|
54
|
+
# Formatting options for header rendering.
|
|
55
|
+
|
|
56
|
+
# Align the fields (colons) in the header for better readability. Values: true or false.
|
|
57
|
+
align_fields = true
|
|
58
|
+
|
|
59
|
+
# File header format (default, native, plain, json)
|
|
60
|
+
# header_format = "native"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ===================
|
|
64
|
+
# Files configuration
|
|
65
|
+
# ===================
|
|
66
|
+
|
|
67
|
+
[files]
|
|
68
|
+
|
|
69
|
+
# Specify (filter) file types:
|
|
70
|
+
# file_types = "markdown"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ============================
|
|
74
|
+
# END of TopMark Configuration
|
|
75
|
+
# ============================
|
|
@@ -16,7 +16,10 @@ from pathlib import Path
|
|
|
16
16
|
TOPMARK_VERSION = get_version("topmark")
|
|
17
17
|
|
|
18
18
|
PYPROJECT_TOML_PATH = Path(__file__).parent.parent / "pyproject.toml"
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
# Name of the bundled default config inside the package `topmark.config`.
|
|
21
|
+
# This is used with importlib.resources in config/__init__.py
|
|
22
|
+
DEFAULT_TOML_CONFIG_RESOURCE: str = "topmark-default.toml"
|
|
20
23
|
|
|
21
24
|
TOPMARK_START_MARKER: str = "topmark:header:start"
|
|
22
25
|
TOPMARK_END_MARKER: str = "topmark:header:end"
|
|
@@ -186,9 +186,9 @@ class ComparisonStatus(BaseStatus):
|
|
|
186
186
|
Callable[[str], str],
|
|
187
187
|
{
|
|
188
188
|
ComparisonStatus.PENDING: chalk.gray,
|
|
189
|
-
ComparisonStatus.CHANGED: chalk.
|
|
189
|
+
ComparisonStatus.CHANGED: chalk.red,
|
|
190
190
|
ComparisonStatus.UNCHANGED: chalk.green,
|
|
191
|
-
ComparisonStatus.CANNOT_COMPARE: chalk.
|
|
191
|
+
ComparisonStatus.CANNOT_COMPARE: chalk.yellow_bright,
|
|
192
192
|
}[self],
|
|
193
193
|
)
|
|
194
194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: topmark
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Python CLI to inspect, validate, and manage license and copyright headers.
|
|
5
5
|
Author-email: Olivier Biot <olivier.biot@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -127,6 +127,15 @@ Logging verbosity is controlled globally:
|
|
|
127
127
|
All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
|
|
128
128
|
subcommands like `check` or `apply`.
|
|
129
129
|
|
|
130
|
+
### Skipping and filtering helpers
|
|
131
|
+
|
|
132
|
+
These switches help keep CI output meaningful and fast:
|
|
133
|
+
|
|
134
|
+
- `--skip-compliant` — hide files that are already compliant; only show items that need attention.
|
|
135
|
+
- `--skip-unsupported` — hide unsupported file types and files matched by known types that
|
|
136
|
+
intentionally **do not** support headers (e.g., strict JSON). They remain **recognized** but are
|
|
137
|
+
not listed in results.
|
|
138
|
+
|
|
130
139
|
### Examples
|
|
131
140
|
|
|
132
141
|
```bash
|
|
@@ -159,6 +168,12 @@ topmark version
|
|
|
159
168
|
|
|
160
169
|
# Apply changes to files in-place
|
|
161
170
|
topmark --apply src/
|
|
171
|
+
|
|
172
|
+
# CI-friendly summary: only show issues; ignore unsupported types
|
|
173
|
+
topmark --skip-compliant --skip-unsupported src/
|
|
174
|
+
|
|
175
|
+
# Apply fixes for only the changed files passed by pre-commit
|
|
176
|
+
topmark --apply --apply --skip-unsupported --quiet
|
|
162
177
|
```
|
|
163
178
|
|
|
164
179
|
## 📐 Header placement rules
|
|
@@ -249,10 +264,79 @@ The following options can be used with most commands.
|
|
|
249
264
|
|
|
250
265
|
## 🧩 Supported file types
|
|
251
266
|
|
|
252
|
-
| Processor | File types (examples)
|
|
253
|
-
| -------------------- |
|
|
254
|
-
| PoundHeaderProcessor |
|
|
255
|
-
|
|
|
267
|
+
| Processor | File types (examples) |
|
|
268
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
269
|
+
| PoundHeaderProcessor | dockerfile, env, git-meta, ini, julia, makefile, perl, python, python-requirements, r, ruby, shell, toml, yaml |
|
|
270
|
+
| SlashHeaderProcessor | c, cpp, cs, go, java, javascript, kotlin, rust, swift, typescript, vscode-jsonc |
|
|
271
|
+
| XmlHeaderProcessor | html, markdown, svelte, svg, vue, xhtml, xml, xsl, xslt, yaml |
|
|
272
|
+
|
|
273
|
+
Some formats (e.g., strict JSON) are **recognized but intentionally skipped** because they lack a
|
|
274
|
+
safe comment syntax. Use `--skip-unsupported` to hide them from the report while keeping safety.
|
|
275
|
+
|
|
276
|
+
For a complete list, please run:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
topmark filetypes
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### How TopMark resolves file types (specificity & safety)
|
|
283
|
+
|
|
284
|
+
TopMark may have multiple `FileType` definitions that **match** a given path. The resolver now:
|
|
285
|
+
|
|
286
|
+
- evaluates **all** matching file types and scores them by **specificity**,
|
|
287
|
+
- prefers **explicit filenames / tail subpaths** (e.g., `.vscode/settings.json`) over patterns, and
|
|
288
|
+
**patterns** over simple **extensions**,
|
|
289
|
+
- breaks ties in favor of **headerable** types (those without `skip_processing=True`).
|
|
290
|
+
|
|
291
|
+
**Tail subpath matching.** `FileType.filenames` entries that contain a path separator (e.g.,
|
|
292
|
+
`".vscode/settings.json"`) are matched as **path suffixes** against `path.as_posix()`; plain names
|
|
293
|
+
still match the **basename** only.
|
|
294
|
+
|
|
295
|
+
**JSON vs JSONC.** Generic `json` is recognized but marked `skip_processing=True` (no comments in
|
|
296
|
+
strict JSON), while `vscode-jsonc` is a safe, **narrow** opt‑in that uses `//` headers. If you need
|
|
297
|
+
more JSON-with-comments files, add them via a dedicated `FileType` or an explicit allow‑list in
|
|
298
|
+
config.
|
|
299
|
+
|
|
300
|
+
**Shebang‑aware insertion.** The default insertion logic is policy‑driven and shebang‑aware (insert
|
|
301
|
+
after `#!` and optional encoding line). For formats like XML that need character‑precise placement,
|
|
302
|
+
processors provide a text‑offset path; `XmlHeaderProcessor` uses this and signals **no line
|
|
303
|
+
anchor**.
|
|
304
|
+
|
|
305
|
+
## 🪝 Pre-commit integration
|
|
306
|
+
|
|
307
|
+
TopMark provides first-class pre-commit hooks. A minimal consumer configuration:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
# .pre-commit-config.yaml (in a consuming repository)
|
|
311
|
+
repos:
|
|
312
|
+
- repo: https://github.com/shutterfreak/topmark
|
|
313
|
+
rev: v0.2.0 # pin to a released tag
|
|
314
|
+
hooks:
|
|
315
|
+
- id: topmark-check
|
|
316
|
+
# Optional: limit scope to supported text types
|
|
317
|
+
# files: '\\.(py|md|toml|ya?ml|sh|Makefile)$'
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Hooks shipped by this repo:
|
|
321
|
+
|
|
322
|
+
- **`topmark-check`** — non-destructive validation. Recommended on `pre-commit` / `pre-push`.
|
|
323
|
+
- **`topmark-apply`** — destructive, requires `--apply`. Marked `manual` so it only runs when you
|
|
324
|
+
call it explicitly.
|
|
325
|
+
|
|
326
|
+
**Run the manual hook locally:**
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# On the whole repo
|
|
330
|
+
pre-commit run topmark-apply --all-files --hook-stage manual
|
|
331
|
+
|
|
332
|
+
# On specific files
|
|
333
|
+
pre-commit run topmark-apply --files path/to/file1 path/to/file2 --hook-stage manual
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Why does the hook seem to run multiple times?** Pre-commit batches filenames to avoid OS
|
|
337
|
+
argument-length limits. You may see repeated banners (e.g., “Processing N file(s)”) as the hook runs
|
|
338
|
+
once per batch. To run **once** per repo, set `pass_filenames: false` in the hook manifest and let
|
|
339
|
+
TopMark discover files itself.
|
|
256
340
|
|
|
257
341
|
## 🛠 Configuration
|
|
258
342
|
|
|
@@ -26,6 +26,7 @@ src/topmark/cli/commands/show_defaults.py
|
|
|
26
26
|
src/topmark/cli/commands/version.py
|
|
27
27
|
src/topmark/config/__init__.py
|
|
28
28
|
src/topmark/config/logging.py
|
|
29
|
+
src/topmark/config/topmark-default.toml
|
|
29
30
|
src/topmark/filetypes/__init__.py
|
|
30
31
|
src/topmark/filetypes/base.py
|
|
31
32
|
src/topmark/filetypes/instances.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|