@greenharness/greengate 0.0.0-stage → 3.1.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 +109 -2
- package/bin/greengate.js +3 -0
- package/bin/native.js +98 -0
- package/package.json +46 -4
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# greengate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Catch the bad commit before it exists — and take the whole thing back out in one command.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/greenharness/greengate/actions/workflows/ci.yaml)
|
|
6
|
+
[](https://github.com/greenharness/greengate/releases/latest)
|
|
7
|
+
[](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
|
+

|
|
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).
|
package/bin/greengate.js
ADDED
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,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greenharness/greengate",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.1.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
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
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.1.0",
|
|
42
|
+
"@greenharness/greengate-darwin-x64": "3.1.0",
|
|
43
|
+
"@greenharness/greengate-linux-arm64-gnu": "3.1.0",
|
|
44
|
+
"@greenharness/greengate-linux-x64-gnu": "3.1.0",
|
|
45
|
+
"@greenharness/greengate-linux-x64-musl": "3.1.0",
|
|
46
|
+
"@greenharness/greengate-win32-x64": "3.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|