@hrg/gitignore 0.3.0 → 0.3.2
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.
- package/README.md +117 -13
- package/js/gitignore.mjs +27 -6
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -127,9 +127,10 @@ Steps 1–6 of [`plans/next-steps.md`](plans/next-steps.md) are implemented:
|
|
|
127
127
|
- the corpus generator is [`conformance/from-git-check-ignore.mjs`](conformance/from-git-check-ignore.mjs), a
|
|
128
128
|
separate step because it needs a checkout CI will not have.
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`package.json`)
|
|
130
|
+
**0.3.0** is released — the same version on npm (`@hrg/gitignore`) and Maven Central (`hr.hrg:gitignore`):
|
|
131
|
+
[Publishing](#publishing) records the release commands. `fs-meta`'s path dependency (`../gitignore` in its
|
|
132
|
+
`build.zig.zon`, `file:` in its `package.json`) can therefore become a version range, the way `hr.hrg:wyhash` is
|
|
133
|
+
consumed. The Java and JS
|
|
133
134
|
ports still consume the vectors and the bench rather than re-deriving them, and a `bench/compare.mjs` that runs
|
|
134
135
|
several ports in one table is a convenience, not a contract.
|
|
135
136
|
|
|
@@ -200,8 +201,8 @@ The package is `package.json` here (`"type": "module"`, `"sideEffects": false`,
|
|
|
200
201
|
```
|
|
201
202
|
|
|
202
203
|
`pom.xml` builds the three matcher classes — `GitIgnoreFile`, `GitIgnoreRules` and the package-private `Glob` —
|
|
203
|
-
with JDK 25. `bun java/run.mjs` remains the zero-build gate for the port.
|
|
204
|
-
|
|
204
|
+
with JDK 25. `bun java/run.mjs` remains the zero-build gate for the port. A checkout can also be installed into the
|
|
205
|
+
local repository once:
|
|
205
206
|
|
|
206
207
|
```text
|
|
207
208
|
cd gitignore
|
|
@@ -259,23 +260,23 @@ The three JS columns are **the same vectors, the same port, three matching strat
|
|
|
259
260
|
which is a pattern with `**` where the regex happened to be fast. That row is the open tuning question, not a settled
|
|
260
261
|
result.
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
Both ports now run the same segment DP as the reference, with the ancestor memoisation that took them from 17x and 11x behind
|
|
264
|
+
Zig to about 1.6x and 2.2x at the time it was measured - and the reference has since become roughly 10x faster than it started,
|
|
265
|
+
so the relative gap moved again. The current numbers, the method, and every improvement (including what was measured and
|
|
266
|
+
refuted) are in [doc/performance/](doc/performance/README.md) and [doc/measurement.md](doc/measurement.md).
|
|
266
267
|
### How these numbers are produced
|
|
267
268
|
|
|
268
269
|
Cold-process runs, with start-up measured separately and subtracted. That is the honest number for a CLI and it is
|
|
269
|
-
also noisy: the same Java build has reported `backtrack` at 3205 ms and 5129 ms. [
|
|
270
|
+
also noisy: the same Java build has reported `backtrack` at 3205 ms and 5129 ms. [doc/measurement.md](doc/measurement.md) records the two measurement modes, the steady-state table (12 rounds inside one process) and every
|
|
270
271
|
strategy tried on the Java port with what it cost. The Java column above predates the literal-segment fast path.
|
|
271
272
|
|
|
272
273
|
### Two caveats
|
|
273
274
|
|
|
274
275
|
- The runner measures a process; the start-up column is a subtraction, and on a loaded machine it is a noisy one. The
|
|
275
276
|
large rows are far outside that noise; the small ones are not.
|
|
276
|
-
- The ports are
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
- The ports are no longer "young" in the sense that mattered: the Java port adopted the segment DP, then literal segments, then
|
|
278
|
+
the ancestor memoisation, and the JS port did the same. What remains is the reference's own head start - it has three indexes
|
|
279
|
+
the ports do not have, and [doc/performance/shared.md](doc/performance/shared.md) is the list a new port should work through.
|
|
279
280
|
## Running the three
|
|
280
281
|
|
|
281
282
|
```text
|
|
@@ -286,3 +287,106 @@ bun bench/perf.mjs --repeat 3 \
|
|
|
286
287
|
--port "js=bun js/decide.mjs --rules {rules} --paths {paths}" \
|
|
287
288
|
--port "java=\"…\bin\java.exe\" -cp java/out hr.hrg.gitignore.Decide --rules {rules} --paths {paths}"
|
|
288
289
|
```
|
|
290
|
+
## Publishing to npm
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
cd <this checkout>
|
|
294
|
+
bun js/conformance.mjs # 34/34, the vectors are the gate
|
|
295
|
+
bun pm pack --dry-run # 4 files: package.json, README.md, js/gitignore.mjs, js/glob.mjs
|
|
296
|
+
npm publish # publishConfig supplies --access public
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**It needs a live token.** Measured on 2026-10-24: `npm whoami` answered `401 Unauthorized` with the token in
|
|
300
|
+
`~/.npmrc`, and `npm publish` reported `404 Not Found - @hrg/gitignore@0.3.1`. npm answers 404 rather than 403 when a
|
|
301
|
+
token cannot publish to a scope, so the 404 is the *symptom* of an expired/revoked token, not of a missing package
|
|
302
|
+
(`npm owner ls @hrg/gitignore` works and names `hrgdavor`, and 0.3.0 is on the registry). Fix with `npm login`, then
|
|
303
|
+
publish; no change to this repository is needed.
|
|
304
|
+
|
|
305
|
+
`fs-meta` pins `"@hrg/gitignore": "0.3.0"` exactly, which is the published version. Bump that one line to `"0.3.1"` when
|
|
306
|
+
npm has it.
|
|
307
|
+
## Publishing the Maven artifact
|
|
308
|
+
|
|
309
|
+
The release is **0.3.0**, the same version as `build.zig.zon` and `package.json`. Publishing is deliberate now:
|
|
310
|
+
the Central publisher lives only in [`pom-release.xml`](pom-release.xml), and `pom.xml` has no publisher at all.
|
|
311
|
+
|
|
312
|
+
### Why the publisher is not in `pom.xml`, and never in a profile
|
|
313
|
+
|
|
314
|
+
Maven reads `<extensions>true</extensions>` only from a POM's **top-level `<build>`**, never from a profile. The
|
|
315
|
+
publisher was once declared in the `publish` profile; it silently never loaded, so `deploy` staged a bundle and
|
|
316
|
+
stopped — no upload, and not a word about "central" in the log. That is the failure this layout prevents.
|
|
317
|
+
|
|
318
|
+
Declaring it in `pom.xml`'s top-level `<build>` does load it, but then *every* build loads it and a bare
|
|
319
|
+
`mvn deploy` publishes to Central whether or not `-Ppublish` was passed. So the publisher is kept out of `pom.xml`
|
|
320
|
+
entirely and exists only in `pom-release.xml`, in that POM's top-level `<build>`.
|
|
321
|
+
|
|
322
|
+
The guard has two halves:
|
|
323
|
+
|
|
324
|
+
- `pom.xml` sets `maven.deploy.skip=true`. `mvn deploy` on it reaches `maven-deploy-plugin` and logs
|
|
325
|
+
`Skipping artifact deployment`; it uploads nothing, and the publisher is not on the build's classpath at all.
|
|
326
|
+
- `pom-release.xml` is a copy of `pom.xml` plus the publisher. It keeps `maven.deploy.skip=true` too: that property
|
|
327
|
+
disables `maven-deploy-plugin`, **not** the Central publisher, which is a separate goal at the same phase. If the
|
|
328
|
+
extension ever failed to load, the release would do nothing rather than fall back to a direct deploy. Verified with
|
|
329
|
+
the plugin: with `maven.deploy.skip=true`, `central-publishing:0.6.0:publish` still runs and logs
|
|
330
|
+
`Skipping Central Publishing at user's request.` when `-DskipPublishing=true` is passed.
|
|
331
|
+
|
|
332
|
+
`pom-release.xml` is a **copy**, not a child: Maven 3.9.9 rejects a jar-packaged POM as a parent
|
|
333
|
+
(`Invalid packaging for parent POM ... must be "pom" but is "jar"`), and Maven has no POM include. Everything is
|
|
334
|
+
duplicated; only the comment block and the publisher plugin are meant to differ. [`scripts/check-poms.mjs`](scripts/check-poms.mjs)
|
|
335
|
+
strips comments, removes the publisher, and fails if anything else differs; it also asserts that both POMs declare the
|
|
336
|
+
same version. Run `bun scripts/check-poms.mjs` after editing either POM, and copy `pom.xml` again in the same commit.
|
|
337
|
+
|
|
338
|
+
### The release command
|
|
339
|
+
|
|
340
|
+
**On this machine `mvn` is `mvnd` (native mode), not the real Maven, and it refuses `-Dmvnd.noDaemon`.** Releasing uses
|
|
341
|
+
the unpacked real Maven in `.tmp/maven/` (Maven 3.9.9), with JDK 25:
|
|
342
|
+
|
|
343
|
+
```text
|
|
344
|
+
$env:JAVA_HOME = 'C:\Program Files\Java\jdk-25'
|
|
345
|
+
$env:MAVEN_GPG_PASSPHRASE = '<your passphrase>'
|
|
346
|
+
.tmp\maven\apache-maven-3.9.9\bin\mvn.cmd -f pom-release.xml -Ppublish -DskipTests package
|
|
347
|
+
.tmp\maven\apache-maven-3.9.9\bin\mvn.cmd -f pom-release.xml -Ppublish -Dgpg.skip=false -DskipTests deploy
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The first command checks that the binary, sources and javadoc jars all appear in `target/`; the second is the release.
|
|
351
|
+
`-Ppublish` adds `maven-source-plugin`, `maven-javadoc-plugin` and `maven-gpg-plugin` (the profile is in both POMs).
|
|
352
|
+
`gpg.skip` defaults to `true`, so nothing signs unless `-Dgpg.skip=false` is passed.
|
|
353
|
+
|
|
354
|
+
To exercise the release path **without uploading**, pass the publisher's own switch:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
.tmp\maven\apache-maven-3.9.9\bin\mvn.cmd -f pom-release.xml -DskipTests -DskipPublishing=true deploy
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
That runs `central-publishing:0.6.0:publish`, stages and bundles locally, logs
|
|
361
|
+
`Skipping Central Publishing at user's request.`, and stops before the upload. **Do not** prove the release path with a
|
|
362
|
+
bare `deploy`.
|
|
363
|
+
|
|
364
|
+
Requirements, learned from a project of this author's that publishes this way (`dia-log`):
|
|
365
|
+
|
|
366
|
+
- `~/.m2/settings.xml` has a server with id **`central`** carrying the Sonatype token.
|
|
367
|
+
- **The signing passphrase comes from `MAVEN_GPG_PASSPHRASE`**, not from `settings.xml`: the plugin deprecates
|
|
368
|
+
`passphraseServerId` (it warns that a passphrase in a file can leak), and the profile passes
|
|
369
|
+
`--pinentry-mode loopback` so gpg reads the environment instead of trying to open a prompt that a batch build cannot
|
|
370
|
+
answer. Measured on this machine: with the variable set, a wrong value produces `gpg: signing failed: Bad passphrase`
|
|
371
|
+
(proving the passphrase reaches the key); with it unset the agent answers `Operation cancelled`, which is what a GUI-less
|
|
372
|
+
run looks like. A passphrase stored in `settings.xml` under the old id is ignored now.
|
|
373
|
+
- `gpg.skip` defaults to `true`, so a local build never signs; the release passes `-Dgpg.skip=false`.
|
|
374
|
+
- The Central publisher takes over the `deploy` phase and suppresses `maven-deploy-plugin`, so the
|
|
375
|
+
`distributionManagement` in both POMs (Sonatype's `maven-releases` endpoint) is not a second upload path. The
|
|
376
|
+
publisher uses `skipPublishing` to stop before the upload and the `central` server for credentials; it does **not**
|
|
377
|
+
use `altDeploymentRepository`.
|
|
378
|
+
- The version comes from the POM (`0.3.0`), and `build.zig.zon`/`package.json` must agree — they do.
|
|
379
|
+
|
|
380
|
+
Afterwards, `fs-meta` resolves `hr.hrg:gitignore` from the repository like any other artifact; it no longer needs the local
|
|
381
|
+
`mvn install`.
|
|
382
|
+
## Checks
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
bun scripts/check-verdicts.mjs # the three implementations agree, byte for byte
|
|
386
|
+
bun scripts/check-verdicts.mjs --json # one NDJSON object per workload
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The 34 conformance vectors say what the matcher must decide; this says that the three implementations decide the *same thing*
|
|
390
|
+
on the six bench workloads — 10,000 verdicts, compared byte for byte, with the first differing line named on failure. A missing
|
|
391
|
+
binary is exit 2 with the build command, never a skip. It exists because twice in this project's history "the vectors pass" was
|
|
392
|
+
mistaken for "the ports agree", and the comparison had been run by hand in scratch files that were never committed.
|
package/js/gitignore.mjs
CHANGED
|
@@ -216,9 +216,16 @@ export function parseGitignore(relativeDirectory, text) {
|
|
|
216
216
|
}
|
|
217
217
|
scoped = relativePath.slice(prefix.length);
|
|
218
218
|
}
|
|
219
|
+
// Split once for every `**` rule of this file, and only if one of them is reached: a file whose patterns all
|
|
220
|
+
// compile to a regex pays no split at all (the `*.log`-shaped rule sets of the small workloads).
|
|
221
|
+
let segments = null;
|
|
219
222
|
for (let index = rules.length - 1; index >= 0; index--) {
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
const rule = rules[index];
|
|
224
|
+
if (rule.segments !== null && segments === null) {
|
|
225
|
+
segments = scoped.split('/');
|
|
226
|
+
}
|
|
227
|
+
if (matches(rule, scoped, isDirectory, segments)) {
|
|
228
|
+
return !rule.negated;
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
231
|
return undefined;
|
|
@@ -286,13 +293,20 @@ export function readGitignore(directoryPath, relativeDirectory) {
|
|
|
286
293
|
*/
|
|
287
294
|
export class GitIgnoreRules {
|
|
288
295
|
#stack = [];
|
|
296
|
+
// A directory's answer, keyed by its path relative to the walk root. A path list visits the same directories over
|
|
297
|
+
// and over (`backtrack` asks about 84,000 ancestor directories that are 42 distinct ones), and the rules are a
|
|
298
|
+
// snapshot, so the answer cannot change while the stack is unchanged. `push`/`pop` clear it: they are the only
|
|
299
|
+
// things that change which files govern a directory, and a cached "excluded" must never outlive them.
|
|
300
|
+
#excluded = new Map();
|
|
289
301
|
|
|
290
302
|
push(file) {
|
|
291
303
|
this.#stack.push(file);
|
|
304
|
+
this.#excluded.clear();
|
|
292
305
|
}
|
|
293
306
|
|
|
294
307
|
pop() {
|
|
295
308
|
this.#stack.pop();
|
|
309
|
+
this.#excluded.clear();
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
get depth() {
|
|
@@ -305,10 +319,17 @@ export class GitIgnoreRules {
|
|
|
305
319
|
return true;
|
|
306
320
|
}
|
|
307
321
|
// An excluded directory is never entered, so a rule for a file inside it cannot re-include that file: ask the
|
|
308
|
-
// ancestors, deepest first. A parent that a later rule re-includes answers false and is skipped.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
// ancestors, deepest first. A parent that a later rule re-includes answers false and is skipped. The ancestor is
|
|
323
|
+
// found by walking the separators backwards, so no ancestor costs a split and a join — one substring, which is
|
|
324
|
+
// also the memo's key.
|
|
325
|
+
for (let slash = relativePath.lastIndexOf('/'); slash >= 0; slash = relativePath.lastIndexOf('/', slash - 1)) {
|
|
326
|
+
const ancestor = relativePath.slice(0, slash);
|
|
327
|
+
let excluded = this.#excluded.get(ancestor);
|
|
328
|
+
if (excluded === undefined) {
|
|
329
|
+
excluded = this.decideDirect(ancestor, true) === true;
|
|
330
|
+
this.#excluded.set(ancestor, excluded);
|
|
331
|
+
}
|
|
332
|
+
if (excluded) {
|
|
312
333
|
return true;
|
|
313
334
|
}
|
|
314
335
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hrg/gitignore",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "git's .gitignore (wildmatch) semantics: one matcher, shared by the Zig, JavaScript and Java ports.",
|
|
5
5
|
"license": "LicenseRef-GPL-3.0-with-Commons-Clause",
|
|
6
6
|
"author": "Davor Hrg",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"conformance": "bun js/conformance.mjs"
|
|
30
|
+
"conformance": "bun js/conformance.mjs",
|
|
31
|
+
"test": "bun test",
|
|
32
|
+
"java": "bun java/run.mjs"
|
|
31
33
|
}
|
|
32
34
|
}
|