@hrg/gitignore 0.3.0 → 0.3.1

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 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
- What remains is **publishing** (step 7), scheduled for **0.3.0**: [Publishing](#publishing) is the checklist, and once
131
- the version is on the registry, `fs-meta`'s path dependency (`../gitignore` in its `build.zig.zon`, `file:` in its
132
- `package.json`) becomes a version range, the way `hr.hrg:wyhash` is consumed. The Java and JS
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. Until `0.3.0` is published, a machine
204
- needs the checkout installed once into the local repository:
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
@@ -286,3 +287,95 @@ 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`.
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
- if (matches(rules[index], scoped, isDirectory)) {
221
- return !rules[index].negated;
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
- const segments = relativePath.split('/');
310
- for (let count = segments.length - 1; count >= 1; count -= 1) {
311
- if (this.decideDirect(segments.slice(0, count).join('/'), true) === true) {
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.0",
3
+ "version": "0.3.1",
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
  }