@greenharness/greengate 0.0.0-reserved → 3.0.0

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
@@ -1 +1,110 @@
1
- Reserved for [greengate](https://github.com/greenharness/greengate); real versions are published by its release workflow.
1
+ # greengate
2
+
3
+ **Catch the bad commit before it exists — and take the whole thing back out in one command.**
4
+
5
+ [![CI](https://github.com/greenharness/greengate/actions/workflows/ci.yaml/badge.svg)](https://github.com/greenharness/greengate/actions/workflows/ci.yaml)
6
+ [![Release](https://img.shields.io/github/v/release/greenharness/greengate?label=release)](https://github.com/greenharness/greengate/releases/latest)
7
+ [![License](https://img.shields.io/github/license/greenharness/greengate)](LICENSE)
8
+
9
+ A single Rust binary that checks `git commit` and `git push`. It ships
10
+ thirty-nine built-in checks — commit-message conventions, conflict markers,
11
+ secrets, the linters and test suites of the languages your repository actually
12
+ uses — and each one fires only where the repository has opted into its tool, so
13
+ there is no YAML to write before it is useful. The commit path links no
14
+ external crates, a cloned repository's own checks stay inert until you
15
+ `greengate trust` them, and `greengate uninstall` removes exactly the six shims install
16
+ wrote. [How it compares](docs/similar-projects.md) to pre-commit, lefthook and
17
+ husky.
18
+
19
+ ![greengate catching a commit and letting the fixed one through](docs/assets/amont-demo.gif)
20
+
21
+ > **greengate was called amont until 3.0.0.** Installs place `amont` and
22
+ > `amont-fleet` beside it as copies, so hooks baked to the old name keep
23
+ > working; the `amont-*` crates stay at 2.3.1 and npm's `amont` at 2.3.0 (now
24
+ > `cargo install greengate`, `npm i -D @greenharness/greengate`,
25
+ > `brew install fredericrous/tap/greengate`). Config keys, `amont.conf` and
26
+ > `AMONT_*` variables are unchanged. See [Installing](docs/install.md).
27
+
28
+ `greengate list` says what runs in the repository you are standing in — here, a
29
+ Python one:
30
+
31
+ ```
32
+ pre-commit
33
+ ● ban-terms
34
+ ● ruff
35
+ ● secrets
36
+ …
37
+ pre-push
38
+ ● audit-python
39
+ …
40
+ 15 active here. 23 inert (Go, JavaScript, Kubernetes, Python, Rust) — greengate list --all
41
+ ```
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ # Linux and macOS
47
+ curl -fsSL https://raw.githubusercontent.com/greenharness/greengate/main/install/install.sh | sh
48
+ ```
49
+
50
+ ```powershell
51
+ # Windows
52
+ irm https://raw.githubusercontent.com/greenharness/greengate/main/install/install.ps1 | iex
53
+ ```
54
+
55
+ Either one downloads a release binary, checks it against the published
56
+ `SHA256SUMS`, and **enables nothing**. Homebrew, crates.io, npm, prebuilt
57
+ archives and building from source: [installing](docs/install.md). Needs git
58
+ 2.31 or later.
59
+
60
+ ## Turning hooks on
61
+
62
+ ```sh
63
+ cd <your-repo> && greengate install # this repository only — the default
64
+ greengate-fleet install --root ~/Developer # many repositories at once
65
+ greengate enroll --conventions declared # every future clone on this machine
66
+ ```
67
+
68
+ `enroll` is a standing grant; read [rolling out to a team](docs/team-rollout.md)
69
+ before you use it. `greengate uninstall` removes the shims again —
70
+ [opting out](docs/opting-out.md) covers that and everything short of it.
71
+
72
+ ## Day to day
73
+
74
+ ```sh
75
+ greengate run # would my commit pass? (the staged set)
76
+ greengate run --all-files # does my working tree pass?
77
+ greengate check src/main.rs # these files, in the format editors already parse
78
+ greengate rehearse --wait # the push gate on a snapshot of HEAD, before you push
79
+ greengate land # push this branch, then read the remote back to prove it moved
80
+ greengate trust # review what this repository's amont.conf declares
81
+ git config amont.severity.clippy warn # runs, reports, does not block
82
+ git config hook.skip clippy # does not run at all
83
+ ```
84
+
85
+ ## Documentation
86
+
87
+ Versioned with the code in [`docs/`](docs/) and published as a
88
+ [book](https://greenharness.github.io/greengate/):
89
+
90
+ - [Installing and activating](docs/install.md) ·
91
+ [Rolling out to a team](docs/team-rollout.md) · [The CI backstop](docs/ci.md)
92
+ - [The checks](docs/checks.md) · [Configuration](docs/configuration.md) ·
93
+ [Opting out](docs/opting-out.md) · [Commit conventions](docs/commit-convention.md)
94
+ - [Where the hooks fit in your flow](docs/coding-flow.md) ·
95
+ [The trust model](docs/trust.md) · [Custom checks and packs](docs/custom-checks.md)
96
+ - [Why you can let this near your commits](docs/index.md#why-you-can-let-this-near-your-commits) ·
97
+ [The repositories around it](docs/index.md#the-repositories-around-it) ·
98
+ [SECURITY.md](SECURITY.md) · [design records](docs/SUMMARY.md#design-records)
99
+
100
+ ## Contributing
101
+
102
+ `make check` is the CI-parity target; setup, the zero-dependency rule and the
103
+ house style are in [CONTRIBUTING.md](CONTRIBUTING.md). Questions in
104
+ [Discussions](https://github.com/greenharness/greengate/discussions), bugs in
105
+ [issues](https://github.com/greenharness/greengate/issues). By participating you
106
+ agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
107
+
108
+ ## License
109
+
110
+ [MIT](LICENSE).
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // The hook runtime. See ./native.js for how the platform binary is found.
3
+ require("./native.js").become("greengate");
package/bin/native.js ADDED
@@ -0,0 +1,98 @@
1
+ // Find the native binary npm installed for this platform, and become it.
2
+ //
3
+ // This wrapper exists only because a package manager can link a `bin` that
4
+ // lives inside the package, and the native executable lives in a DIFFERENT
5
+ // package — one of six, selected by npm from `os`/`cpu`/`libc`.
6
+ //
7
+ // It is not on the hook path. `amont init` bakes `current_exe()` — the native
8
+ // binary this spawns, not this file — into `.git/hooks`, so the ~30ms of node
9
+ // start-up below is paid once during `prepare` and never again on a commit.
10
+ // See npm/README.md.
11
+ //
12
+ // Parameterised by binary name rather than copied per binary. Everything below
13
+ // — pnpm's non-hoisting, the libc fallback, spawn-versus-exists, signal exit
14
+ // codes — was learned once and is worth exactly one copy; a second wrapper that
15
+ // duplicated it would be a second place for those lessons to rot.
16
+
17
+ const { spawnSync } = require("node:child_process");
18
+ const { existsSync } = require("node:fs");
19
+
20
+ // The same six targets `release.yaml` builds, spelled the way npm spells them.
21
+ // `libc` is why linux-x64 appears twice: a musl host cannot run the glibc build,
22
+ // and npm will install only the package whose `libc` matches.
23
+ const PACKAGES = {
24
+ "darwin arm64": "@greenharness/greengate-darwin-arm64",
25
+ "darwin x64": "@greenharness/greengate-darwin-x64",
26
+ "linux arm64": "@greenharness/greengate-linux-arm64-gnu",
27
+ "linux x64": ["@greenharness/greengate-linux-x64-gnu", "@greenharness/greengate-linux-x64-musl"],
28
+ "win32 x64": "@greenharness/greengate-win32-x64",
29
+ };
30
+
31
+ function candidates() {
32
+ const found = PACKAGES[`${process.platform} ${process.arch}`];
33
+ if (!found) return [];
34
+ return Array.isArray(found) ? found : [found];
35
+ }
36
+
37
+ // `require.resolve` rather than a hand-built `../amont-<target>/bin/…` path.
38
+ // pnpm does not hoist — the real package sits under `node_modules/.pnpm/` — so a
39
+ // path assembled from `__dirname` is correct under npm and wrong under pnpm and
40
+ // yarn. Node's own resolver knows where the dependency actually is.
41
+ function binaryOf(pkg, name) {
42
+ const exe = process.platform === "win32" ? `${name}.exe` : name;
43
+ try {
44
+ const p = require.resolve(`${pkg}/bin/${exe}`);
45
+ if (existsSync(p)) return p;
46
+ } catch {
47
+ // Not installed: either the wrong libc for this host, or an
48
+ // `--ignore-scripts`-style install that skipped optional deps.
49
+ }
50
+ return null;
51
+ }
52
+
53
+ // Existence is not runnability, so the loop is over SPAWNS, not paths. With a
54
+ // package manager that ignores the `libc` field — yarn classic does, and
55
+ // `npm install --force` is this file's own suggested remedy — BOTH linux-x64
56
+ // builds get installed, gnu listed first, and on a musl host the gnu binary
57
+ // fails at exec on the missing glibc loader while the right one sits a
58
+ // candidate later. Only a spawn-level failure (`result.error`) falls through:
59
+ // a binary that ran and exited non-zero has ANSWERED, and its exit code is the
60
+ // whole product of a hook runner.
61
+ function become(name) {
62
+ const failures = [];
63
+ for (const pkg of candidates()) {
64
+ const binary = binaryOf(pkg, name);
65
+ if (!binary) continue;
66
+ // `spawnSync` with inherited stdio rather than `execFileSync`: this forwards
67
+ // the child's exit CODE. It also keeps the child's stdin, which `amont run`
68
+ // reads.
69
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
70
+ if (result.error) {
71
+ failures.push(` ${binary}: ${result.error.message}`);
72
+ continue;
73
+ }
74
+ // A signalled child has a null status. Report it the way a shell does, so
75
+ // "killed by SIGINT" does not read as a clean exit 0.
76
+ if (result.status === null && result.signal) {
77
+ process.exit(128 + (require("node:os").constants.signals[result.signal] ?? 0));
78
+ }
79
+ process.exit(result.status ?? 1);
80
+ }
81
+
82
+ // Name the platform. "amont binary not found" sends people to reinstall;
83
+ // "no build for linux/ppc64" tells them the actual answer, which is that they
84
+ // want `cargo install amont` or the shell installer.
85
+ const target = `${process.platform}/${process.arch}`;
86
+ process.stderr.write(
87
+ `${name}: no runnable native binary for ${target}.\n` +
88
+ (failures.length ? ` Installed but could not run:\n${failures.join("\n")}\n` : "") +
89
+ ` npm installs one of: ${Object.values(PACKAGES).flat().join(", ")}\n` +
90
+ ` If your platform is not among them, build from source:\n` +
91
+ ` cargo install greengate\n` +
92
+ ` If it is, the optional dependency did not install — try:\n` +
93
+ ` npm install --force @greenharness/greengate\n`,
94
+ );
95
+ process.exit(1);
96
+ }
97
+
98
+ module.exports = { become };
package/package.json CHANGED
@@ -1,8 +1,48 @@
1
1
  {
2
2
  "name": "@greenharness/greengate",
3
- "version": "0.0.0-reserved",
4
- "description": "Reserves the name for greengate (formerly amont); the release workflow publishes real versions. See https://github.com/greenharness/greengate",
3
+ "version": "3.0.0",
4
+ "description": "Opinionated git hooks that judge what you are committing, not what is on disk",
5
+ "keywords": [
6
+ "git",
7
+ "git-hooks",
8
+ "pre-commit",
9
+ "pre-push",
10
+ "hooks",
11
+ "lint",
12
+ "husky"
13
+ ],
14
+ "homepage": "https://github.com/greenharness/greengate#readme",
15
+ "bugs": "https://github.com/greenharness/greengate/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/greenharness/greengate.git"
19
+ },
5
20
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/greenharness/greengate.git" },
7
- "publishConfig": { "access": "public" }
21
+ "author": "Frederic Rousseau",
22
+ "type": "commonjs",
23
+ "_comment_bin": "Both names run the same wrapper: `greengate`, and `amont` for a package.json script or shim written before the rename.",
24
+ "bin": {
25
+ "greengate": "bin/greengate.js",
26
+ "amont": "bin/greengate.js"
27
+ },
28
+ "_comment_access": "Scoped packages default to RESTRICTED; this says public in the manifest too, so a hand-run publish cannot make the package private.",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "bin/greengate.js",
34
+ "bin/native.js",
35
+ "README.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "optionalDependencies": {
41
+ "@greenharness/greengate-darwin-arm64": "3.0.0",
42
+ "@greenharness/greengate-darwin-x64": "3.0.0",
43
+ "@greenharness/greengate-linux-arm64-gnu": "3.0.0",
44
+ "@greenharness/greengate-linux-x64-gnu": "3.0.0",
45
+ "@greenharness/greengate-linux-x64-musl": "3.0.0",
46
+ "@greenharness/greengate-win32-x64": "3.0.0"
47
+ }
8
48
  }