pre-commit-bigfix 2.0.1__py3-none-any.whl

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,821 @@
1
+ Metadata-Version: 2.4
2
+ Name: pre-commit-bigfix
3
+ Version: 2.0.1
4
+ Summary: pre-commit hooks for BigFix content
5
+ Home-page: https://github.com/jgstew/pre-commit-bigfix
6
+ Author: JGStew
7
+ Author-email: noreply@jgstew.com
8
+ Project-URL: Bug Tracker, https://github.com/jgstew/pre-commit-bigfix/issues
9
+ Project-URL: Source, https://github.com/jgstew/pre-commit-bigfix
10
+ Keywords: pre-commit hooks python bigfix bes
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Development Status :: 5 - Production/Stable
21
+ Classifier: Intended Audience :: Developers
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: bigfix_prefetch>=1.1.5
26
+ Requires-Dist: lxml
27
+ Requires-Dist: validate_bes_xml<3,>=2.4.1
28
+ Requires-Dist: bigfix-relevance-analyzer<2,>=1.23.1; python_version >= "3.11"
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest; extra == "test"
31
+ Dynamic: license-file
32
+
33
+ # pre-commit-bigfix
34
+
35
+ pre commit hooks for BigFix content
36
+
37
+ Moved from [pre-commit-jgstew](https://github.com/jgstew/pre-commit-jgstew).
38
+
39
+ ## Usage
40
+
41
+ Add this to your `.pre-commit-config.yaml`:
42
+
43
+ ```yaml
44
+ repos:
45
+ - repo: https://github.com/jgstew/pre-commit-bigfix
46
+ rev: v2.0.0
47
+ hooks:
48
+ - id: bes-schema-validate
49
+ - id: bes-conventions-check
50
+ - id: bes-actionscript-lint-schclass
51
+ - id: bes-actionscript-validate-prefetch
52
+ - id: bes-actionscript-validate-script
53
+ - id: bes-relevance-lint
54
+ - id: bes-relevance-convert-group
55
+ # only in a Git External Custom Site (repository site) repo:
56
+ - id: bes-siterepo-validate-layout
57
+ ```
58
+
59
+ The hooks were renamed to a consistent `bes-<aspect>-<action>` scheme in
60
+ v0.3.0. The deprecated aliases for the previous ids (`validate-bes`,
61
+ `check-bes-conventions`) have since been removed, along with the console
62
+ scripts of the same names: a config using them now fails with pre-commit's
63
+ "hook id not found" error. Use `bes-schema-validate` and
64
+ `bes-conventions-check` instead.
65
+
66
+ ## Run without pre-commit
67
+
68
+ The `pre-commit-bigfix` command runs every hook over a folder, without
69
+ pre-commit or a git commit:
70
+
71
+ ```bash
72
+ uvx --from git+https://github.com/jgstew/pre-commit-bigfix pre-commit-bigfix
73
+ ```
74
+
75
+ Once the package is on PyPI, this is just `uvx pre-commit-bigfix`. Give one or
76
+ more files or folders to check (default: the current folder). Each hook gets
77
+ the files its `files` pattern matches, with its default `args`, and a summary
78
+ line per hook is printed at the end. The exit code is 1 if any hook fails, and
79
+ 2 for a path that does not exist or a bad option.
80
+
81
+ - It is **read-only**. Under pre-commit several hooks fix files in place; here
82
+ each is run with `--auto-fix=no` (or `--check` for
83
+ `bes-relevance-convert-group` and `bes-siterepo-validate-layout`), so
84
+ fixable problems are reported, not fixed. Run a hook directly, or use
85
+ pre-commit, to apply fixes.
86
+ - Hidden folders (a name starting with `.`, or the hidden flag on macOS and
87
+ Windows) are not searched; pass `--include-hidden` to search them.
88
+ `__pycache__` and `node_modules` are never searched. A folder named on the
89
+ command line is always searched.
90
+ - Symlinked files found while searching are skipped, as pre-commit skips
91
+ them. A file named on the command line that no hook checks is reported.
92
+ - `bes-siterepo-validate-layout` runs once in each git repo that holds a given
93
+ path, and is skipped when none does.
94
+ - `--skip HOOK` (repeatable) leaves a hook out.
95
+
96
+ ## Hooks
97
+
98
+ ### bes-schema-validate
99
+
100
+ Validates that BigFix XML files are well-formed and satisfy their schema, via
101
+ the [validate_bes_xml](https://pypi.org/project/validate-bes-xml/) package
102
+ (`>= 2.4.1`). It checks these files:
103
+
104
+ | Files | Root tag | Schema |
105
+ |---|---|---|
106
+ | `*.bes` | `<BES>` | `BES.xsd` |
107
+ | `*.ojo` | any | `BESOJO.xsd` |
108
+ | `siteConfig.xml` (Git External Custom Site) | `<SiteConfig>` | `SiteConfig.xsd` |
109
+ | `site.xml` (Git External Custom Site) | `<Site>` | `Site.xsd` |
110
+ | `digest.xml` (Git External Custom Site) | `<Digest>` | `Digest.xsd` |
111
+ | `Signatures/*.xml` (BigFix Inventory signatures) | `<SoftwareIdentityCatalog>` | `SoftwareIdentityCatalog.xsd` |
112
+
113
+ The file names only decide which files the hook sees. Apart from `.ojo`
114
+ files, which always use `BESOJO.xsd`, the schema is picked by the root tag:
115
+ the bundled schema with the same name, ignoring case. So a `digest.xml` whose
116
+ root is `<SiteConfig>` is checked against `SiteConfig.xsd`, and a `.bes` file
117
+ whose root is `<BESAPI>` against `BESAPI.xsd`.
118
+
119
+ The hook's `files` pattern is:
120
+
121
+ ```text
122
+ \.(bes|ojo)$|(^|/)(siteConfig|site|digest)\.xml$|(^|/)Signatures/[^/]+\.xml$
123
+ ```
124
+
125
+ The `.xml` names are matched case-sensitively, as BigFix documents them, and
126
+ only signature files directly inside a `Signatures/` folder are matched. Keep
127
+ signatures elsewhere? Set `files:` in your own config.
128
+
129
+ Only the schemas bundled with validate_bes_xml are used. A `*.xsd` in the
130
+ repo (a copy of `site.xsd` from RepositorySiteExample, say) is ignored, so a
131
+ `.bes` file whose root tag has no bundled schema fails here as it would
132
+ anywhere else.
133
+
134
+ A `.xml` file that is not a BigFix file, such as a sitemap or a Maven site
135
+ descriptor named `site.xml`, has no bundled schema for its root tag and is
136
+ skipped. An Eclipse update-site `site.xml` is the exception: its root is a
137
+ plain `<site>`, which matches `Site.xsd` (schema names match without regard
138
+ to case), so it is validated and fails. If your repo holds Eclipse update
139
+ sites, exclude them:
140
+
141
+ ```yaml
142
+ - id: bes-schema-validate
143
+ exclude: ^eclipse/.*site\.xml$
144
+ ```
145
+
146
+ Files that pass print nothing. Each error is printed as `path:line: message`,
147
+ or `path: message` when there is no line number, and the exit code is the
148
+ number of files that failed:
149
+
150
+ ```text
151
+ Sites/Foo/siteConfig.xml:4: Element 'BesFileDir': This element is not expected. Expected is one of ( NonClientFilesDir, OtherFilesDir ).
152
+ Sites/Foo/Fixlets/A/digest.xml:4: Element 'DigestRelevanceEvaluationPeriod': [facet 'enumeration'] The value 'PT10M' is not an element of the set {...}.
153
+ unknown.bes: no schema to validate this file against
154
+ ```
155
+
156
+ Renamed from `validate-bes`, whose deprecated alias has been removed.
157
+
158
+ ### bes-conventions-check
159
+
160
+ Picky, opinionated content checks + auto-fixes for BigFix BES files that the
161
+ BES.xsd schema (`bes-schema-validate`) cannot express: ActionScript MIMEType,
162
+ value formats for SourceReleaseDate / x-fixlet-modification-time / DownloadSize /
163
+ action-ui-metadata / CPE-2.3 / CVENames / x-relevance-evaluation-period,
164
+ prefetch-line shape (URL schemes overridable with `--prefetch-url-schemes`)
165
+ and https URLs, CDATA usage, blank-line and
166
+ trailing-whitespace spacing, empty ActionScript, dynamic download statements,
167
+ a UTF-8 XML declaration, Title placeholders / whitespace / TODO markers,
168
+ non-trivial non-empty Relevance with no stray whitespace, unique MIMEField
169
+ names, unique Analysis Property names/IDs, unique Action IDs, action link text
170
+ free of empty-substitution gaps, SuccessCriteria body/Option consistency, a
171
+ SourceSeverity vocabulary (default Low/Moderate/Important/High/Critical,
172
+ overridable with `--severity-values`; `Unspecified` is auto-fixed to empty), description placeholders / empty
173
+ descriptions, a Description `<script>` block free of `//` line comments
174
+ (lost when the file is converted to FXF, since Description newlines are
175
+ collapsed), and Task/Fixlet release-date / modification-time presence and order
176
+ (a SourceReleaseDate later than the modification time warns `W219`), and a
177
+ `DownloadSize` that does not match the prefetches' total (`W220`, only when
178
+ that total is knowable), and a Relevance that waits for a client setting value
179
+ the action never sets (`W221`).
180
+
181
+ Auto-fixes the fixable ones in place and exits 1 when anything was fixed so
182
+ the change is reviewed and re-staged. Only rewrites whose right answer is
183
+ knowable from the file are made: besides the whitespace, CDATA, date-insert
184
+ and XML-declaration fixes, a missing ActionScript MIMEType gets the
185
+ Windows-Shell default (`E200`), an unambiguous year-first SourceReleaseDate
186
+ or a loosely-formatted timestamp is respelled (`E201`/`E202`/`E216` - a wrong
187
+ day-of-week is dropped, not recomputed), a `cve-` prefix is uppercased
188
+ (`E209`, several `<CVENames>` are not merged), an evaluation period is
189
+ zero-padded (`E219`), a case-only SourceSeverity mismatch is respelled
190
+ (`W216`), and a DownloadSize that disagrees with a knowable prefetch total is
191
+ set to it (`W220`). E-codes fail the hook; pass `--strict`
192
+ to also fail on warnings, or `--errors-only` to leave W-codes out of the report
193
+ entirely (the checks and their auto-fixes still run, unlike `--disable`).
194
+ `--check-filename` (off by default) additionally checks that a file's
195
+ basename matches its first content object's Title. Unparsable files are
196
+ skipped (`bes-schema-validate` owns validity).
197
+
198
+ Renamed from `check-bes-conventions`, whose deprecated alias has been
199
+ removed. The file-level opt-out marker moved with it, from `pre-commit-skip:
200
+ bes-conventions` to `pre-commit-skip: bes-conventions-check`.
201
+
202
+ See the docstring in
203
+ [bes_conventions_check.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/bes_conventions_check.py)
204
+ for the full list of check codes, opt-out markers, and options.
205
+
206
+ Auto-fixes are checked against BES.xsd in three groups: all the per-block
207
+ fixes together, the trailing-whitespace strip, and the XML declaration. A
208
+ group that would turn a schema-valid file into an invalid one is not written
209
+ and is reported as `E222`; the other groups still are (`--disable E222` writes
210
+ it anyway). Because the per-block fixes are one group, one bad block fix holds
211
+ back every block fix in the file.
212
+
213
+ ### bes-actionscript-lint-schclass
214
+
215
+ Lints every `<ActionScript>` body in BES files against the BigFix console's
216
+ own lexical grammar: the vendored
217
+ [ExpandedActionScript.schclass](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/schclass_data/ExpandedActionScript.schclass)
218
+ (the lex schema the console's editor uses, 323 command verbs) merged with a
219
+ [small override file](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/schclass_data/bigfix_overrides.schclass)
220
+ of validation corrections the display grammar needs.
221
+
222
+ The rule: the first token of every line must be a known command verb, a `//`
223
+ comment, a `{...}` relevance substitution, a continuation, or the line must be
224
+ blank (E300; the message quotes the whole offending line and, when the
225
+ grammar narrows it to a handful of known commands, offers a `did you mean` --
226
+ e.g. `action log commands` suggests `action log command`). A `{...}`
227
+ substitution must close before line end (E301), and a
228
+ `createfile until <MARKER>` block must reach its bare marker line (E302; the
229
+ block's raw content is excluded from linting). An `override run` / `override
230
+ wait` line opens a block whose following `keyword=value` lines are options
231
+ rather than commands, checked against the
232
+ [documented keywords and values](https://developer.bigfix.com/action-script/reference/execution/override.html)
233
+ (E303) -- a value in `{...}` is a relevance substitution and is accepted
234
+ unchecked. Verbs match case-insensitively but a non-lowercase verb warns
235
+ (W302), an override option keyword or value that is not lowercase warns (W303),
236
+ and an unbalanced `"` warns (W301). ActionScript has no escape character, so
237
+ `"C:\Bes\"` is a closed string; only a `regset`/`regset64` value, which uses
238
+ `.reg`-file syntax, treats `\"` as an escaped quote, and `appendfile` content
239
+ is raw file text that is not checked.
240
+ Only `application/x-Fixlet-Windows-Shell` (or missing-MIMEType) bodies are
241
+ BigFix ActionScript and are linted; `x-sh`, `x-AppleScript`,
242
+ `x-Fixlet-Windows-PowerShell`, and `text/x-uri` bodies are other languages and
243
+ are skipped. E-codes fail the hook; pass `--strict` to also fail on warnings.
244
+ Unparsable files are skipped (`bes-schema-validate` owns validity). Non-`.bes`
245
+ paths passed explicitly are linted as raw ActionScript text, so you can widen the
246
+ hook's `files` pattern to cover standalone ActionScript files.
247
+
248
+ The underlying schclass loader ([schclass.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/schclass.py))
249
+ and tokenizer engine
250
+ ([schclass_tokenizer.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/schclass_tokenizer.py)) are
251
+ generic: point them at any `.schclass` file and they lex that language.
252
+
253
+ This hook's scope is deliberately limited to what the schclass grammar can
254
+ decide - the lexical validity of each line. ActionScript checks that need
255
+ knowledge the grammar does not carry (per-verb argument shapes, `if`/`endif`
256
+ and prefetch-block pairing, the `]]></ActionScript>` closing-tag whitespace
257
+ trap, http->https escalation, and any content-changing auto-fixes) belong in
258
+ a separate ActionScript hook, so this one stays a thin consumer of the grammar
259
+ files and needs no code changes when BigFix ships new command verbs. Its one
260
+ auto-fix changes only case: `--auto-fix` (on by default when files are given,
261
+ as pre-commit does) lowercases a `W302` verb or `W303` override option in
262
+ place, and an auto-fixed file fails the hook so it is reviewed and re-staged;
263
+ `args: ["--auto-fix", "no"]` reports them without rewriting.
264
+
265
+ See the docstring in
266
+ [bes_actionscript_lint_schclass.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/bes_actionscript_lint_schclass.py)
267
+ for the full list of check codes, opt-out markers, and options.
268
+
269
+ The case fixes are checked against BES.xsd together. If they would turn a
270
+ schema-valid file into an invalid one, none of them are written and `E304` is
271
+ reported (`--disable E304` writes them anyway).
272
+
273
+ ### bes-actionscript-validate-prefetch
274
+
275
+ Validates every prefetch line in every `<ActionScript>` body with
276
+ `validate_prefetch()` from the
277
+ [bigfix_prefetch](https://pypi.org/project/bigfix-prefetch/) package, which is
278
+ the reference implementation of what a valid prefetch is. Both spellings are
279
+ covered:
280
+
281
+ ```text
282
+ prefetch <name> sha1:<40> size:<n> <url> sha256:<64>
283
+ add prefetch item name=<name> sha1=<40> size=<n> url=<url> sha256=<64>
284
+ ```
285
+
286
+ A line that fails validation is `E400`, carrying the reason `bigfix_prefetch`
287
+ reported: a missing size or a size that is not > 0, a hash of the wrong
288
+ length, or a line that could not be parsed at all.
289
+
290
+ A missing sha256 is `E401`. `bigfix_prefetch` calls sha256 optional unless
291
+ asked, but in 2026 it is treated as mandatory here. It gets its own code
292
+ rather than being folded into `E400` so that a repo which still wants it
293
+ optional can turn just that off:
294
+
295
+ ```yaml
296
+ - id: bes-actionscript-validate-prefetch
297
+ args: ["--disable", "E401"]
298
+ ```
299
+
300
+ A prefetch block item with no sha1 warns (`W402`); a prefetch *statement* with
301
+ no sha1 warns (`W405`) instead of failing the line - current BigFix clients
302
+ accept a statement with sha256 alone, so this is unusual but valid. A
303
+ declared name whose extension differs from the extension of the URL's file
304
+ warns (`W406`) - e.g. `<sha1>.msi` fetched from `ccsetup531.exe` - since the
305
+ extension decides how the file is run or unpacked. An extension is a trailing
306
+ `.` plus 1-4 characters including a letter; names or URLs without one, and
307
+ server-side script URLs such as `download.php`, are not compared, and a URL
308
+ that is the declared name with one more extension appended (`7z.dll.txt`, a
309
+ way past web filtering) counts as a match. Text the prefetch syntax does not
310
+ define is `E403` - a stray token (an AutoPkg template's
311
+ `vs_SSMS.exe; filename*=UTF-8''vs_SSMS.exe`), a misspelt field (`sha2:`), an
312
+ unencoded space splitting the URL - since the reference parser silently
313
+ ignores it; a space-separated trailing `// comment` is fine. The two shapes
314
+ with one meaning each are auto-fixed offline: `sha2:` before a 64-hex value
315
+ becomes `sha256:`, and a space directly after the URL becomes `%20`; any
316
+ other stray text is left for a human. An
317
+ `add nohash prefetch item` line is reported rather than validated (`W403`),
318
+ since it is hashless by definition and its download cannot be verified.
319
+
320
+ `--auto-fix-network` (off by default) adds the missing sha256 of an `E401` for
321
+ you, which means downloading the file to hash it - there is no other way to
322
+ learn a sha256. The download is handed to `add_sha256_prefetch()` from
323
+ `bigfix_prefetch` (v1.1.5+), which streams the file, checks it against the size
324
+ and sha1 already on the line, and re-emits the prefetch with sha256 added, in
325
+ the same spelling and keeping the line's own download name. A download that
326
+ does not happen or does not match is `W404`: the line is left alone and its
327
+ `E401` stands. Each URL is fetched at most once per run, and each download
328
+ gives up after 60 seconds.
329
+
330
+ The same flag switches an `http://` prefetch URL to `https://` (the fix for
331
+ `bes-conventions-check`'s `W207`), but only once the https copy is proven to be
332
+ the same file: a HEAD request must report a `Content-Length` equal to the
333
+ line's size - checked *before* anything is downloaded - and must not redirect
334
+ off https; then the download's size and every hash on the line must match.
335
+ Only the scheme changes. Anything else is `W407` and the line stays on http;
336
+ `--disable W407` turns this part off.
337
+
338
+ ```yaml
339
+ - id: bes-actionscript-validate-prefetch
340
+ args: ["--auto-fix-network", "yes"]
341
+ ```
342
+
343
+ Because this reaches out to whatever URLs the content names, it is opt-in and
344
+ never the default - think about it before turning it on for a repo whose
345
+ prefetches point at hosts you do not control.
346
+
347
+ A prefetch that downloads the retired `unzip-5.52.exe` from the BigFix redist
348
+ folder is `E402` - `unzip-6.0.exe` is the current one - and it is auto-fixed
349
+ offline. Auto-fix is on by default; the line is rewritten in place to
350
+
351
+ ```text
352
+ add prefetch item name=unzip.exe sha1=84debf12767785cd9b43811022407de7413beb6f size=204800 url=http://software.bigfix.com/download/redist/unzip-6.0.exe sha256=2122557d350fd1c59fb0ef32125330bde673e9331eb9371b454c2ad2d82091ac
353
+ ```
354
+
355
+ or the `prefetch ... sha1:... size:...` statement equivalent, whichever
356
+ spelling the line already used - the replacement string is built by
357
+ `prefetch_from_dictionary()` from `bigfix_prefetch`, so its shape is the
358
+ reference implementation's. The original download's *name* is kept (the rest
359
+ of the ActionScript refers to the file by that name), while the url, size,
360
+ sha1, and sha256 become the current file's. An `add nohash prefetch item` on
361
+ the old URL is reported but not rewritten: swapping in a hashed prefetch would
362
+ change what the line does, not just which file it fetches. To report `E402`
363
+ without rewriting anything:
364
+
365
+ ```yaml
366
+ - id: bes-actionscript-validate-prefetch
367
+ args: ["--auto-fix", "no"]
368
+ ```
369
+
370
+ A file whose prefetches knowingly do not meet these rules opts out of every
371
+ check in this hook with `prefetch-ok` anywhere in it (e.g.
372
+ `<!-- prefetch-ok -->`) - the same marker that opts out of the prefetch-shape
373
+ warning `W206` in `bes-conventions-check`, since it is the same judgement.
374
+ The usual `<!-- pre-commit-skip: bes-actionscript-validate-prefetch -->` skips
375
+ the file too.
376
+
377
+ The scope of the *checks* is internal validity only, and they are offline: no
378
+ check downloads a URL or verifies that the hashes match the real file. The one
379
+ thing that touches the network is `--auto-fix-network`, above, and only when it
380
+ is asked for. Dynamic
381
+ prefetches (a line holding a `{...}` relevance substitution), `//` comments,
382
+ and the raw content of a `createfile until` block are skipped - none of them
383
+ carry a fixed size and hash to check. The line's overall shape and its
384
+ http-vs-https scheme are `W206`/`W207` in `bes-conventions-check`, and its
385
+ lexical validity is `E300` in `bes-actionscript-lint-schclass`: three
386
+ altitudes on the same line, all intentional.
387
+
388
+ `E402`, the two `E403` shapes above, and (with `--auto-fix-network`) `E401`
389
+ and the `W207` https switch are the only auto-fixes, and no others are
390
+ planned - `E400`'s correct size and hashes are properties of the
391
+ real file, and a hook has no way to know which file was meant. E-codes and any
392
+ auto-fix fail the hook; pass `--strict` to also fail on warnings. Unparsable
393
+ files are skipped (`bes-schema-validate` owns validity).
394
+
395
+ See the docstring in
396
+ [bes_actionscript_validate_prefetch.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/bes_actionscript_validate_prefetch.py)
397
+ for the full list of check codes, opt-out markers, and options.
398
+
399
+ Auto-fixes are checked against BES.xsd one pass at a time, in the order
400
+ `E403`, `E402`, `E401`, `W407`. A pass that would turn a schema-valid file
401
+ into an invalid one is not written and is reported as `E404`; the other passes
402
+ still are (`--disable E404` writes it anyway).
403
+
404
+ ### bes-actionscript-validate-script
405
+
406
+ Checks every `<ActionScript>` body of a BES file for balanced `if`/`endif` and
407
+ `begin prefetch block`/`end prefetch block` pairing - the sibling hook named
408
+ in `bes-actionscript-lint-schclass`'s SCOPE note for checks that need pairing,
409
+ ordering, and block-interleaving knowledge the lexical grammar does not
410
+ carry, and meant to grow more per-script checks over time. An unbalanced
411
+ block is a real defect, not a style nit: the BigFix agent fails the action at
412
+ runtime on a dangling `if`, and a missing `endif` silently changes which
413
+ statements are conditional.
414
+
415
+ An unclosed `if` is `E500`; a stray `endif` with no open `if` is `E501`. An
416
+ unclosed `begin prefetch block` is `E502`; a stray `end prefetch block` is
417
+ `E503`; a `begin prefetch block` nested inside another one is `E504`, since
418
+ prefetch blocks do not nest. An `else` or `elseif` outside any open `if` is
419
+ `E505`; an `elseif` after `else`, or a second `else` for the same `if`, is
420
+ `E506`. An `if` opened inside a prefetch block that is still open at that
421
+ block's `end prefetch block` is `E507`, since blocks interleave rather than
422
+ nest.
423
+
424
+ Within a single line, a `{...}` relevance substitution that is never closed
425
+ before end of line is `E508`, and a `}` reached with no substitution open is
426
+ `E509`: a substitution is evaluated per line and cannot span lines. `E508`
427
+ also applies to `createfile until` content, which the agent substitutes too,
428
+ so a literal `{` in an embedded script must be written `{{` (a lone `}` there
429
+ is literal and is not `E509`). `{{` (and
430
+ `}}`) is an escape that passes a literal brace through to the command, so it
431
+ neither opens nor closes a substitution, and a later lone `}` pairs with that
432
+ escape instead of being reported as stray. The `}}` escape holds *inside* an
433
+ open substitution too - a quoted literal like `{ ... "@{'k'='v'}}" ... }`
434
+ (a PowerShell hashtable nested inside a substitution) does not close on the
435
+ first `}` of that `}}`.
436
+
437
+ An `add prefetch item` or `collect prefetch items` outside an open prefetch
438
+ block is `E510`/`E511` - the agent rejects them there. Two prefetch/download
439
+ producers declaring the same download name in the same conditional context is
440
+ `E512` (the second silently overwrites the first); declarations reached
441
+ through different `if`/`elseif`/`else` branch choices are not compared, since
442
+ cross-platform content routinely declares the same `name=` once per OS in a
443
+ separate `if` per platform, and a `__Download\<name>` reference that nothing
444
+ prefetches or downloads warns `W507` - a typo catcher that is skipped entirely
445
+ whenever any producer's names are unknowable (an `extract`/`unarchive`/
446
+ `archive now`/`utility` command, a `download` with no `as <name>` and no
447
+ literal URL basename, or a `{...}` substitution in a name or URL); a
448
+ `delete` or `folder delete` of a `__Download\<name>` is cleanup, not
449
+ consumption, and never counts as a reference. A script can create files under
450
+ `__Download` too, so `copy`/`move` destinations (including from
451
+ `__createfile`) and shell redirection targets (`... > __Download\<name>`)
452
+ count as producers. An
453
+ `if`/`elseif` whose condition is not a `{...}` relevance substitution is
454
+ `E514`, and `else if` written as two words (not a chained branch; use
455
+ `elseif`) is `E524`. A `begin prefetch block` that is not at the top of the script is
456
+ `E515`: only blank lines, `//` comments, `action parameter query` lines, and
457
+ `parameter` assignments may precede it - plain `prefetch` statements are legal
458
+ anywhere in a script and are not placement-checked.
459
+
460
+ Two `parameter "name" = ...` assignments to the same name that can co-execute
461
+ (the same conditional-context rule as `E512`) is `E516` - action parameters
462
+ are write-once, and the second assignment silently overwrites the first. A
463
+ `parameter "name"` reference before that name's assignment elsewhere in the
464
+ body is `E517`; a name never assigned in-script (a secure parameter supplied
465
+ from the Description page, say) is not `E517`, since this hook cannot see it -
466
+ but if the name occurs nowhere else in the whole file (not assigned, queried,
467
+ or mentioned by a Description form) it warns `W508`: that is almost always
468
+ copy-paste from a sibling fixlet, and the substitution fails at runtime. The
469
+ platform-supplied `action issue date` is exempt. A `continue if` or `pause while` condition that is not a `{...}`
470
+ relevance substitution is `E518` - the same rule `E514` applies to `if`/
471
+ `elseif`, extended to these two other condition-bearing verbs.
472
+
473
+ A command referencing `__createfile` or `__appendfile` when the body has no
474
+ matching `createfile until` / `appendfile` line anywhere is `E519` - the
475
+ `W507` rule reapplied to these two scratch-file verbs, with the same
476
+ `delete`/`folder delete` cleanup exemption. Any `__Download`, `__createfile`,
477
+ or `__appendfile` reference whose case does not match exactly warns `W503`:
478
+ Windows tolerates the mismatch, a case-sensitive Linux/macOS filesystem does
479
+ not. It is auto-fixed (see below).
480
+
481
+ A `setting` line that is not the documented
482
+ `setting "name"="value" on "{...}" for client|user|action` shape is `E520` -
483
+ a missing effective-date clause fails at runtime. A
484
+ `regset`/`regset64`/`regdelete`/`regdelete64` key that is not a quoted,
485
+ bracketed `"[HKEY_...]..."` keyname is `E521`. An
486
+ `action uses wow64 redirection` argument that is not `true`, `false`, or a
487
+ `{...}` substitution is `E523`.
488
+
489
+ A `wait`/`run` of cmd.exe that passes a command line but no `/c` warns
490
+ `W505`: without the switch cmd.exe opens an interactive shell and never runs
491
+ the command, so the action reports success having done nothing. `/k` is
492
+ likewise flagged: it does run the command, but leaves the shell open
493
+ afterward, so the action hangs instead of completing.
494
+
495
+ A `move`/`copy` of any source (a `__createfile`/`__appendfile` scratch file,
496
+ a download, ...) onto a destination that is not cleared earlier in the body
497
+ warns `W506` - cleared meaning a `delete` of it (including one that looks the
498
+ same file up by relevance), a shell `rm`/`del` naming it, or a `move` of it
499
+ elsewhere. Both verbs fail when the
500
+ destination already exists, so such an action works the first time and fails
501
+ on every later run; the documented pattern is to `delete` the destination
502
+ first. A destination inside the action's own download folder is exempt, being
503
+ action-scoped rather than a persistent location, and a `folder delete` of an
504
+ ancestor counts as clearing it.
505
+
506
+ An `override wait` / `override run` block not terminated by its own matching
507
+ verb - it hits end of body, is terminated by the *other* verb's command, or
508
+ is reopened by another `override` before any command runs - is `E522`.
509
+ `bes-actionscript-lint-schclass` validates the `keyword=value` option lines
510
+ inside a block (its `E303`); this is the pairing check that block's state
511
+ machine cannot express.
512
+
513
+ A command after an unconditional `exit`, `restart`, or `shutdown` (one outside
514
+ any `if`) can never run and warns `W501` (first unreachable line only); an
515
+ `action parameter query` after the first execution command warns `W502` -
516
+ these are console-time prompts and belong at the top.
517
+
518
+ A `parameter "<name>" = <value>` whose value is not double-quoted is `E527`:
519
+ the agent substitutes `{...}` first and then parses the line, so an unquoted
520
+ value - even a singular `{"a"}` - is invalid `parameter` syntax and the line
521
+ fails at runtime. A trailing `// comment` on a `parameter` line is `E528`:
522
+ only fixed-syntax lines (`if`/`elseif`/`else`/`endif`, the prefetch block,
523
+ `override`) accept one, so the line fails even with a quoted value.
524
+
525
+ `--auto-fix` (`W503`, `W506`, `E525`, `E528`, `E527`, `E524`, `W505`, `E521`), on by default when files are given (as
526
+ pre-commit does) and off when auto-discovering, rewrites every wrong-case
527
+ `__download`/`__createfile`/`__appendfile` reference to its canonical
528
+ spelling, inserts a `delete <destination>` before each `W506` move/copy, and
529
+ quotes each unquoted `folder create`/`folder delete` path (`E525` - quoted even
530
+ when today's value has no spaces, since a user profile or parameter can),
531
+ moves a `parameter` line's trailing comment onto its own line above (`E528`),
532
+ quotes an unquoted `parameter` value (`E527` - `&quot;` in an entity-escaped
533
+ body; a value holding a `"` outside `{...}` or `%22` is reported, not fixed),
534
+ joins `else if` into `elseif` (`E524`, unless a stray `endif` shows a real
535
+ nested `if`), turns a waiting cmd.exe's `/k` into `/c` or inserts a missing
536
+ `/c` after its leading switches (`W505`), and quotes a bracketed-but-unquoted
537
+ registry key (`E521`), in place; an auto-fixed file fails the hook so the
538
+ change is reviewed and re-staged. No other check here has an auto-fix: a hook
539
+ has no way to know where a missing `endif` or `end prefetch block` was meant
540
+ to go, and guessing could silently change what the action does.
541
+
542
+ Only `application/x-Fixlet-Windows-Shell` (matched case-insensitively - it is
543
+ valid BigFix content either way) or missing-MIMEType bodies are checked;
544
+ bodies are extracted with lxml, so entities are decoded, CDATA sections
545
+ merged, and line numbers map back to the file. Lines inside a
546
+ `createfile until` block are file content, not ActionScript, and are not
547
+ scanned; that block's own well-formedness is `bes-actionscript-lint-schclass`'s
548
+ `E302`, not reported here. Unparsable files are `W500` (`bes-schema-validate`
549
+ owns validity).
550
+
551
+ A file opts out of every check here with
552
+ `<!-- pre-commit-skip: bes-actionscript-validate-script -->` anywhere in it,
553
+ or out of one family with `actionscript-if-ok` (`E500`, `E501`, `E505`,
554
+ `E506`, `E514`, `E518`, `E524`), `actionscript-prefetch-block-ok` (`E502`, `E503`,
555
+ `E504`), `actionscript-block-nesting-ok` (`E507`), `actionscript-substitution-ok`
556
+ (`E508`, `E509` - the same marker `bes-actionscript-lint-schclass` uses for
557
+ its `E301`, so one marker covers both hooks),
558
+ `actionscript-prefetch-placement-ok` (`E510`, `E511`, `E515`),
559
+ `actionscript-download-ok` (`E512`, `W507`), `actionscript-parameter-ok`
560
+ (`E516`, `E517`, `E527`, `E528`, `W508`), `actionscript-scratch-ok` (`E519`, `W503`),
561
+ `actionscript-scratch-dest-ok` (`W506`),
562
+ `actionscript-command-shape-ok` (`E520`, `E521`, `E523`, `E525`),
563
+ `actionscript-cmd-ok` (`W505`),
564
+ `actionscript-override-ok` (`E522` - shared with
565
+ `bes-actionscript-lint-schclass`'s `E303`), `actionscript-unreachable-ok`
566
+ (`W501`), or `actionscript-parameter-query-ok` (`W502`). E-codes fail the
567
+ hook; pass
568
+ `--strict` to also fail on warnings.
569
+
570
+ See the docstring in
571
+ [bes_actionscript_validate_script.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/bes_actionscript_validate_script.py)
572
+ for the full list of check codes, opt-out markers, and options.
573
+
574
+ Auto-fixes are checked against BES.xsd one pass at a time: `W503`, `W506`,
575
+ `E525`, `E528`, `E527`, then the `E524`/`W505`/`E521` line rewrites as one pass. A pass that
576
+ would turn a schema-valid file into an invalid one is not written and is
577
+ reported as `E526`; the other passes still are (`--disable E526` writes it
578
+ anyway).
579
+
580
+ ### bes-relevance-lint
581
+
582
+ Every other hook here treats relevance as opaque - `{...}` is a lexer state
583
+ mechanically identical to a string literal, and the most any check proves about
584
+ a condition is that it starts with a brace. This hook is where that stops. It
585
+ hands each statement to
586
+ [bigfix-relevance-analyzer](https://github.com/jgstew/bigfix-relevance-analyzer),
587
+ which parses it, binds `it`, resolves every inspector against the client dumps,
588
+ type-checks the result, and scores what it costs to evaluate.
589
+
590
+ Every site the analyzer's extractor recognizes is checked - `<Relevance>`
591
+ bodies, `<SuccessCriteria Option="CustomRelevance">` bodies, Analysis
592
+ `<Property>` bodies, and the `{...}` substitutions inside `<ActionScript>` - so
593
+ a defect in a substitution is reported at the line it lives on, not at the top
594
+ of the file.
595
+
596
+ | code | meaning |
597
+ | --- | --- |
598
+ | `E600` | the statement could not be parsed |
599
+ | `E601` | the statement contains text that could not be lexed |
600
+ | `E602` | `it` is used where there is no context to bind it to |
601
+ | `E603` | the type checker reported a problem beyond an unbound `it` |
602
+ | `E604` | the complexity score is above the ceiling (raise with `--max-score`) |
603
+ | `E605` | the evaluation cost is above the ceiling (raise with `--max-evaluation-cost`) |
604
+ | `E606` | a directory tree was deeper than `--max-depth`, so it was not fully scanned (auto-discovery only; pre-commit always passes filenames) |
605
+ | `E607` | a path given to the linter does not exist, could not be read, or is not a file type the analyzer recognizes (the hook's `files:` pattern keeps those away under pre-commit) |
606
+ | `E608` | the value does not fit the kind of site it came from: a `<Relevance>` that is not a boolean, or an `if`/`elseif`/`continue if` condition that is not a boolean or string |
607
+ | `E609` | inspectors exclusive to client relevance and to session relevance in one statement, which no engine can answer |
608
+ | `E610` | an ActionScript command word used as a relevance name, usually an ActionScript line pasted or doubled into relevance |
609
+ | `E611` | a plural operand where the engine requires a single value, which it refuses before evaluating anything; a `unique value of` (or singular-aggregate) rewrite is printed as a suggested fix where safe |
610
+ | `E612` | an ActionScript `{` with anything after it on its line and no `}` to close it there, which fails the action |
611
+ | `E613` | a `<?Relevance` with no closing `?>`, so its relevance was not linted |
612
+ | `E614` | a BES file that is not well-formed XML, so nothing in it was linted (**off by default**; `--enable E614`) |
613
+ | `W600` | a name no inspector dump defines |
614
+ | `W601` | a property written singular over an object that may be plural |
615
+ | `W602` | a singular spelling mid-chain (under a `whose` filter, or where a plural is built from it), where the plural reads safer |
616
+ | `W603` | two version-looking strings compared as strings, not as versions |
617
+ | `W604` | a version comparison that truncates to the shorter operand's components |
618
+ | `W605` | an ordinary ActionScript substitution's value is an opaque object with no text form |
619
+ | `W606` | an ordinary ActionScript substitution's value may be more than one value; the client joins them with no separator |
620
+ | `W607` | a relevance-tagged markdown code fence that is never closed, so its relevance was not linted (manual runs on markdown only) |
621
+
622
+ `W600` is a warning rather than an error because a repo running a newer client
623
+ than the analyzer's dump snapshot legitimately uses names it has never heard
624
+ of. `W601` is **disabled by default** in the hook declaration: across 1,108
625
+ `.bes` files of real content it fires 6,127 times, which drowns everything
626
+ else. Switch it back on with `--enable W601`.
627
+
628
+ Unparsable XML is skipped rather than reported (`E614` is off by default) -
629
+ `bes-schema-validate` owns file validity. That does mean a truncated file passes this hook: a clean run
630
+ says the relevance that could be extracted is sound, not that the file parses.
631
+
632
+ A file opts out of every check here with
633
+ `<!-- pre-commit-skip: bes-relevance-lint -->` anywhere in it. There is no
634
+ per-rule marker: `--disable` takes a code repo-wide, and a single file that
635
+ legitimately needs relevance this complex is what `--max-score` is for. Files
636
+ are never rewritten, but where the analyzer can work out a safe rewrite of a
637
+ whole statement (today, the singular spellings behind `W602`) the fixed
638
+ statement is printed once under its first finding as `suggested fix: ...`.
639
+ E-codes fail the hook; pass `--strict` to also fail on warnings.
640
+
641
+ This hook needs **Python 3.11 or newer** - the analyzer requires it, while the
642
+ rest of this package still runs on 3.8. No specific interpreter is pinned, since
643
+ `language_version` names an exact executable and pinning `python3.11` would
644
+ break machines that only have something newer; set it yourself
645
+ (`language_version: python3.12`) if pre-commit's default is too old. The
646
+ dependency carries a matching environment marker, so the other hooks stay
647
+ installable on 3.8-3.10; run this one there anyway and it prints why it cannot
648
+ run and skips (exit 0) - no config on a 3.8-3.10 interpreter can make it pass,
649
+ so failing there would only be an unfixable red build. On 3.11+ a missing
650
+ analyzer is a broken install, and the hook still fails.
651
+
652
+ See the docstring in
653
+ [bes_relevance_lint.py](https://github.com/jgstew/pre-commit-bigfix/blob/main/pre_commit_bigfix/bes_relevance_lint.py) for the full
654
+ list of check codes and options.
655
+
656
+ ### bes-relevance-convert-group
657
+
658
+ The BigFix console can save applicability as a computer-group style
659
+ `<GroupRelevance>`: a list of search components, each already carrying its own
660
+ `<Relevance>`, joined by intersection or union. BES.xsd allows either that or a
661
+ run of `<Relevance>` elements in the same place, and plain relevance is what
662
+ reads and diffs well, so this hook rewrites each `/BES/*/GroupRelevance` in
663
+ place:
664
+
665
+ | `JoinByIntersection` | components | becomes |
666
+ | --- | --- | --- |
667
+ | `true` | any | one `<Relevance>` per component (sibling Relevance must all hold) |
668
+ | `false` | one | `<Relevance>s1</Relevance>` |
669
+ | `false` | two or more | `<Relevance>(s1) OR (s2) OR ...</Relevance>` |
670
+
671
+ Each component's relevance is copied verbatim - CDATA stays CDATA, escaped
672
+ stays escaped - except an OR-join of several, which is decoded, joined, and
673
+ written once: as CDATA when any input was CDATA or the text holds `<`, `>` or
674
+ `&`, and entity-escaped if it contains `]]>`. The splice leaves everything
675
+ outside the replaced block byte-identical, line endings included. A long union
676
+ can become one large statement; `bes-relevance-lint` may then report `E604`.
677
+
678
+ | code | meaning |
679
+ | --- | --- |
680
+ | `E700` | a GroupRelevance was converted (under `--check`: needs converting) |
681
+ | `E701` | a GroupRelevance holds a `SearchComponentGroupReference`; group membership is not converted yet, so it is left unchanged |
682
+ | `E702` | `JoinByIntersection` is not a boolean, or is missing with two or more components (BES.xsd documents no default; a single component converts either way); left unchanged |
683
+ | `E703` | a GroupRelevance has no components, or one with an empty Relevance; left unchanged |
684
+ | `E704` | the converted file would turn a schema-valid file invalid, so nothing is written |
685
+ | `W700` | skipped: file missing or not parseable XML, or a GroupRelevance that could not be located |
686
+
687
+ The hook converts by default and exits 1 when it changed anything, so the
688
+ rewrite shows up for review; `--check` only reports. With no files given it
689
+ checks every `*.bes` below the current folder and never rewrites. A file opts
690
+ out with `<!-- pre-commit-skip: bes-relevance-convert-group -->`.
691
+
692
+ ### bes-siterepo-validate-layout
693
+
694
+ For a BigFix [Git External Custom Site](https://help.hcl-software.com/bigfix/11.0/platform/Platform/Config/c_repository_site_git.html)
695
+ ("repository site", BigFix 11.0.7+) repo. The root server gathers the site
696
+ from git, and one bad file can fail the whole sync, so this hook checks the
697
+ site as a whole: it runs once per commit (`pass_filenames: false`,
698
+ `always_run: true`) on the files `git ls-files` lists, so untracked and
699
+ ignored files do not count.
700
+
701
+ The site root is the repo root, or each `--site-root DIR` (repeatable, for a
702
+ repo of several sites like CommunityContent's `Sites/<name>/`). Its
703
+ `siteConfig.xml`, if tracked, names the folders (`BesFileDir`,
704
+ `NonClientFilesDir`, `OtherFilesDir`); the defaults are `Fixlets`,
705
+ `NonClientFiles` and `OtherFiles`.
706
+
707
+ A root server can run Windows or RHEL, and one repo site may serve both, so
708
+ names and paths must be valid on both: the stricter limit always applies.
709
+ Folder names, the `.bes` extension and `digest.xml` match ignoring case, as
710
+ on Windows, so nothing goes unchecked; one that matches only that way is a
711
+ warning (`W808`), since RHEL is case-sensitive.
712
+
713
+ | code | meaning |
714
+ | --- | --- |
715
+ | `E800` | a `.bes` file in the `.bes` folder whose name does not start with a numeric ID and a space or hyphen; `42- Name.bes` and `42 Name.bes` both work |
716
+ | `E801` | two or more `.bes` files in one site share an ID, compared as integers, so `0042` and `42` collide; each names the others |
717
+ | `E802` | a `.bes` file's server path is too long and the rename cannot fix it: even `<ID> X.bes` is too long (the folders are), it has no ID, or `E805` is disabled. Renaming a folder changes its `.fxf` or digest, so that is left to you |
718
+ | `E803` | fix: zero padding in the ID (`0042- X.bes` -> `42- X.bes`); done even when the ID is a duplicate (`E801`), since the ID does not change |
719
+ | `E804` | fix: non-ASCII characters in the name, folded to ASCII |
720
+ | `E805` | fix: the server path is too long, or the name is over 255 bytes, so the file is renamed `<ID> <Title>.bes` |
721
+ | `E806` | a rename was skipped: the new name is taken (ignoring case), the file has unstaged changes pre-commit set aside, the commit uses a temporary index (`git commit -a`, `git commit <paths>`), or the rename failed |
722
+ | `E807` | another tracked file's server path is too long, inside the site roots or not (the server clones the whole repo). It is not renamed, since actions may refer to it by name |
723
+ | `E808` | a file or folder name over 255 bytes in UTF-8 (Linux `NAME_MAX`) that the rename cannot fix; each folder once |
724
+ | `W800` | a `.bes` file outside the `.bes` folder; the server does not gather it |
725
+ | `W801` | a `.bes` file directly in the `.bes` folder, or deeper than `<BesFileDir>/<fxf>/<digest>/` (how the server handles either is undocumented) |
726
+ | `W802` | a file in the `.bes` folder other than `.bes` or `digest.xml`; an `.xsd` too, since `bes-schema-validate` already has the schemas |
727
+ | `W803` | a folder `siteConfig.xml` renames has no files (a default name spelled out, as the template does, is not checked: those folders are optional) |
728
+ | `W804` | the site has no content: no `.bes` files and no files in the NonClient or Other folders. A site of server files only passes |
729
+ | `W806` | an ID outside 1..2147483647 (the range is undocumented) |
730
+ | `W807` | `siteConfig.xml` is not readable XML; the default folders are used (`bes-schema-validate` reports why) |
731
+ | `W808` | a folder, `.bes` extension or `digest.xml` that matches the expected name only when case is ignored (`fixlets/` for `Fixlets`, `.BES`); a RHEL server may not find it |
732
+
733
+ **Path and name limits.** The server clones the whole repo to
734
+ `<server-root>\RepositorySiteGather\Sites\<repo>_<branch>_<8 hex>\Source\`,
735
+ and each file's path relative to the repo root (not the site root) is added
736
+ to that. The result must be at most 259 characters (`MAX_PATH` is 260 with
737
+ the terminating NUL); paths over it fail on 11.0.7 even with Windows long
738
+ paths enabled. For example, `bigfix-site-reporting-jgstew` on the `main`
739
+ branch is cloned to
740
+ `C:\Program Files (x86)\BigFix Enterprise\BES Server\RepositorySiteGather\Sites\bigfix-site-reporting-jgstew_main_27eb797e\Source\`,
741
+ 129 characters, leaving 130 for the path in the repo. `<repo>` is the git
742
+ repo's name, not the site's name in BigFix. The length is counted in UTF-16
743
+ units, as Windows counts it. Only this Windows layout is confirmed (the
744
+ clone path on a RHEL server is not), so it is the path that is measured. On
745
+ Linux each file and folder name must also be at most 255 bytes in UTF-8
746
+ (`NAME_MAX`); Linux's 4096-byte `PATH_MAX` cannot be reached once the
747
+ Windows limit holds. In a repo of several sites, paths are still measured
748
+ from the repo root: CommunityContent's `Sites/<name>/Fixlets/...` paths
749
+ count in full. The options:
750
+
751
+ | option | default |
752
+ | --- | --- |
753
+ | `--server-root` | `C:\Program Files (x86)\BigFix Enterprise\BES Server` |
754
+ | `--repo-name` | the last part of the `origin` remote URL without `.git`, or the repo folder's name |
755
+ | `--branch` | `main` |
756
+ | `--max-path` | `259` |
757
+
758
+ **Renames.** By default each `.bes` file in the `.bes` folder is renamed at
759
+ most once, combining the fixes it needs: zero padding stripped from the ID
760
+ (`E803`), non-ASCII folded to ASCII (`E804`: the euro sign becomes `EUR`, a
761
+ curly apostrophe `'`, accents are dropped, and `_` stands in when there is no
762
+ equivalent), and, if the path or name is still too long, `<ID> <Title>.bes`
763
+ cut to fit both limits (`E805`). The title comes from the file's `<Title>`,
764
+ or from its current name if the title has no letter or digit; it is made
765
+ ASCII and safe on Windows, cut short, and any trailing space, dot or hyphen
766
+ is dropped. A name that already fits is left alone, even when it differs
767
+ from the title. The ID never changes and no file moves between folders.
768
+
769
+ Tracked files are renamed with `git mv`, so the renames are staged and the
770
+ next commit carries them whole; a plain rename would leave an unstaged
771
+ deletion that `git commit -a` could commit without the new file. Nothing is
772
+ renamed (`E806`) where the rename would be lost:
773
+
774
+ - `git commit -a` and `git commit <paths>` run the hook on a temporary index
775
+ that git throws away when the hook fails. `git add` the changes and run a
776
+ plain `git commit` (or `pre-commit run`) to get the renames.
777
+ - A file with unstaged changes that pre-commit set aside for the run: it
778
+ could not put them back on a renamed file. The hook finds pre-commit's
779
+ stash file by the pid of its parent, grandparent or great-grandparent (a
780
+ Windows `.exe` launcher sits in between); this is verified on macOS only.
781
+ `pre-commit try-repo` keeps its stash in a temporary cache that the hook
782
+ cannot see, so there such a file is renamed and pre-commit then fails to
783
+ restore the changes (they stay in its patch file).
784
+ Any rename makes the hook exit 1, so review the renames and commit again.
785
+ `--check` is a dry run that reports what would be renamed.
786
+
787
+ Files with no ID are reported (`E800`), not given one. An opt-in
788
+ `--assign-ids` is deferred: choosing IDs is a policy decision, best backed
789
+ by a counter file as CommunityContent does, since taking the highest ID plus
790
+ one could reuse a deleted item's ID.
791
+
792
+ Not checked here: more than one object in a `.bes` file, or Baselines
793
+ (repository sites are thought to handle both); schemas, GroupRelevance and
794
+ `//` comments (`bes-schema-validate`, `bes-relevance-convert-group`,
795
+ `bes-conventions-check`); setting up a blank repo. For names that differ
796
+ only by case, and names Windows forbids, add these companions from
797
+ [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks)
798
+ (`>= 5.0.0`):
799
+
800
+ ```yaml
801
+ - repo: https://github.com/pre-commit/pre-commit-hooks
802
+ rev: v6.0.0
803
+ hooks:
804
+ - id: check-case-conflict
805
+ - id: check-illegal-windows-names
806
+ ```
807
+
808
+ A repo with several sites:
809
+
810
+ ```yaml
811
+ - id: bes-siterepo-validate-layout
812
+ args: [--site-root, "Sites/Site A", --site-root, "Sites/Site B"]
813
+ ```
814
+
815
+ ## Development
816
+
817
+ ```bash
818
+ pip install -e .[test]
819
+ python -m pytest
820
+ pre-commit run --all-files
821
+ ```