commit-sheriff 1.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 +325 -0
- package/bin/commit-sheriff.js +119 -0
- package/package.json +79 -0
- package/templates/commit-msg +80 -0
- package/templates/pre-commit +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# commit-sheriff
|
|
2
|
+
|
|
3
|
+
Shared [Husky](https://typicode.github.io/husky/) git hooks for any project — enforces a consistent **branch naming format** and **commit message format** (ticket + type, optionally module), and runs **lint-staged** / type-check / related tests before every commit.
|
|
4
|
+
|
|
5
|
+
One command installs the same hooks in any repo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx commit-sheriff init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Why](#why)
|
|
14
|
+
- [Requirements](#requirements)
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [What `init` does](#what-init-does)
|
|
18
|
+
- [Commit message format](#commit-message-format)
|
|
19
|
+
- [Branch name format](#branch-name-format)
|
|
20
|
+
- [Configuration (`commitGuard`)](#configuration-commitguard)
|
|
21
|
+
- [`useModules`](#usemodules)
|
|
22
|
+
- [`project`](#project)
|
|
23
|
+
- [`modules`](#modules)
|
|
24
|
+
- [`branchTypes`](#branchtypes)
|
|
25
|
+
- [`types`](#types)
|
|
26
|
+
- [Pre-commit checks](#pre-commit-checks)
|
|
27
|
+
- [lint-staged](#lint-staged)
|
|
28
|
+
- [Examples](#examples)
|
|
29
|
+
- [Updating](#updating)
|
|
30
|
+
- [Skipping / bypassing hooks](#skipping--bypassing-hooks)
|
|
31
|
+
- [Troubleshooting](#troubleshooting)
|
|
32
|
+
- [Releasing this package](#releasing-this-package)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
Different repos tend to drift into different commit-message and branch-naming conventions, which makes changelogs, ticket tracing, and code review harder. `commit-sheriff` gives every project the same two git hooks (`commit-msg`, `pre-commit`) driven by one small config block in `package.json`, so:
|
|
38
|
+
|
|
39
|
+
- Every commit message references a ticket and a change type.
|
|
40
|
+
- Every branch name reflects the same ticket and change type.
|
|
41
|
+
- Lint/format/type-check/tests run automatically before a commit is allowed, but only for the tools the project actually has installed.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Node.js (used to read `package.json` and evaluate the config — no runtime dependencies are installed by `commit-sheriff` itself)
|
|
46
|
+
- Git
|
|
47
|
+
- [`husky`](https://www.npmjs.com/package/husky) v9+ as a devDependency of your project
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install --save-dev commit-sheriff husky
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`lint-staged`, `eslint`, and `prettier` are optional — install them too if you want the pre-commit hook to lint/format staged files:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install --save-dev lint-staged eslint prettier
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx commit-sheriff init
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run this once per repo, from the repo root (where `package.json` lives). This works once `commit-sheriff` is installed as a devDependency (it exposes a `commit-sheriff` binary via `node_modules/.bin`), or directly without installing first via `npx commit-sheriff init`.
|
|
68
|
+
|
|
69
|
+
## What `init` does
|
|
70
|
+
|
|
71
|
+
1. Initializes Husky if it isn't already set up (runs `npx husky init` when `.husky/_/husky.sh` is missing).
|
|
72
|
+
2. Copies the hook scripts into `.husky/commit-msg` and `.husky/pre-commit` (overwriting any existing files with those exact names) and makes them executable.
|
|
73
|
+
3. Adds a default `commitGuard` block to `package.json` **only if one doesn't already exist** — re-running `init` never overwrites your customized config.
|
|
74
|
+
4. Adds a default `lint-staged` block to `package.json` **only if one doesn't already exist**.
|
|
75
|
+
5. Adds a `"prepare": "husky"` npm script if missing, so hooks are (re)installed automatically after `npm install`.
|
|
76
|
+
|
|
77
|
+
Re-running `npx commit-sheriff init` later (e.g. after updating the package) is safe: it refreshes the two hook scripts to the latest version but leaves your `commitGuard` / `lint-staged` config alone.
|
|
78
|
+
|
|
79
|
+
## Commit message format
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
(TICKET) type(scope): message
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
or, when [`useModules`](#usemodules) is `true`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
(TICKET) [MODULE] type(scope): message
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **`TICKET`** — `PROJECT-NUMBER`, e.g. `PROJ-1191` (uppercase letters, a dash, digits)
|
|
92
|
+
- **`MODULE`** — required only if `useModules: true` (see [`modules`](#modules))
|
|
93
|
+
- **`type`** — one of the words configured in [`types`](#types)
|
|
94
|
+
- **`(scope)`** — optional, free text in parentheses
|
|
95
|
+
- **`message`** — free text description
|
|
96
|
+
|
|
97
|
+
Merge/revert/fixup/squash commits (`Merge ...`, `Revert ...`, `fixup! ...`, `squash! ...`) are always allowed through unchanged — no need to reformat what Git itself generates.
|
|
98
|
+
|
|
99
|
+
### Examples
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
(PROJ-1077) feat: implement organization management
|
|
103
|
+
(PROJ-1191) fix(register): reset registration session on menu reopen
|
|
104
|
+
(PROJ-1077) [SET] feat: implement organization management # only with useModules: true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If the commit message doesn't match, the commit is rejected and the hook prints the required format, an example, the ticket pattern, and the allowed types (and modules, if enabled).
|
|
108
|
+
|
|
109
|
+
When `useModules: true`, the hook also checks that the `[MODULE]` in the commit message matches the module encoded in the current branch name (see below) — so you can't accidentally tag a commit for a different module than the branch you're on.
|
|
110
|
+
|
|
111
|
+
## Branch name format
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
<type>/<PROJECT>-<NUMBER>-<description>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
or, when `useModules: true`:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
<type>/<MODULE>-<PROJECT>-<NUMBER>-<description>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **`type`** — one of the words configured in [`branchTypes`](#branchtypes)
|
|
124
|
+
- **`MODULE`** — required only if `useModules: true`
|
|
125
|
+
- **`PROJECT-NUMBER`** — same ticket reference as in commit messages
|
|
126
|
+
- **`description`** — lowercase, starts with a letter, only lowercase letters/digits/dashes after that
|
|
127
|
+
|
|
128
|
+
### Examples
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
bugfix/PROJ-1093-fix-validation
|
|
132
|
+
improvement/PROJ-609-correct-husky-pre-commit-validation
|
|
133
|
+
improvement/PRO-PROJ-609-correct-husky-pre-commit-validation # only with useModules: true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The check is skipped on a detached `HEAD` (e.g. mid-rebase, mid-cherry-pick), so it never blocks those operations.
|
|
137
|
+
|
|
138
|
+
## Configuration (`commitGuard`)
|
|
139
|
+
|
|
140
|
+
Add/edit this block in your project's `package.json` (the `init` command adds a default one for you):
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"commitGuard": {
|
|
145
|
+
"useModules": false,
|
|
146
|
+
"project": "PROJ",
|
|
147
|
+
"branchTypes": [
|
|
148
|
+
"feature",
|
|
149
|
+
"bugfix",
|
|
150
|
+
"hotfix",
|
|
151
|
+
"improvement",
|
|
152
|
+
"refactor",
|
|
153
|
+
"release",
|
|
154
|
+
"chore",
|
|
155
|
+
"docs",
|
|
156
|
+
"test",
|
|
157
|
+
"spike"
|
|
158
|
+
],
|
|
159
|
+
"types": [
|
|
160
|
+
"feat",
|
|
161
|
+
"feature",
|
|
162
|
+
"fix",
|
|
163
|
+
"docs",
|
|
164
|
+
"style",
|
|
165
|
+
"refactor",
|
|
166
|
+
"test",
|
|
167
|
+
"chore",
|
|
168
|
+
"perf",
|
|
169
|
+
"ci",
|
|
170
|
+
"build",
|
|
171
|
+
"revert"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
All keys are optional; anything you omit falls back to the default shown above.
|
|
178
|
+
|
|
179
|
+
### `useModules`
|
|
180
|
+
|
|
181
|
+
`boolean`, default `false`.
|
|
182
|
+
|
|
183
|
+
Turns the `[MODULE]` tag on or off in both the commit message and the branch name. Leave this `false` unless your project is split into named modules/domains that you want tracked per commit.
|
|
184
|
+
|
|
185
|
+
### `project`
|
|
186
|
+
|
|
187
|
+
`string`, default `"PROJ"`.
|
|
188
|
+
|
|
189
|
+
The project key used in the **branch name** check (`<type>/<PROJECT>-<NUMBER>-...`). Note this only constrains the branch name — the commit message ticket itself accepts any uppercase project key (`^\([A-Z]+-[0-9]+\)`), so it doesn't need to be repeated here for the commit-msg hook to work, but keeping it consistent with your Jira/Linear/etc. project key is recommended.
|
|
190
|
+
|
|
191
|
+
### `modules`
|
|
192
|
+
|
|
193
|
+
`string[]`, default: none — falls back to accepting **any** uppercase code (`[A-Z]+`).
|
|
194
|
+
|
|
195
|
+
Only relevant when `useModules: true`. If you want to restrict commits/branches to a specific, known set of module codes, list them explicitly:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
"commitGuard": {
|
|
199
|
+
"useModules": true,
|
|
200
|
+
"project": "PROJ",
|
|
201
|
+
"modules": ["AUTH", "BILLING", "REPORTS"]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If you omit `modules` (or leave it as `[]`), **any** uppercase code is accepted (e.g. `[AUTH]`, `[XYZ]`, `[SET]`) — useful early in a project before the final module list is settled.
|
|
206
|
+
|
|
207
|
+
### `branchTypes`
|
|
208
|
+
|
|
209
|
+
`string[]`, default: `["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"]`.
|
|
210
|
+
|
|
211
|
+
Allowed prefixes for **branch names**. This is intentionally a coarser, workflow-level vocabulary (what kind of branch is this?) — it does not need to match `types` word-for-word, since a single `feature/...` branch will typically contain several kinds of commits (`feat`, `test`, `docs`, `chore`, ...) as the feature is built.
|
|
212
|
+
|
|
213
|
+
### `types`
|
|
214
|
+
|
|
215
|
+
`string[]`, default: `["feat", "feature", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"]`.
|
|
216
|
+
|
|
217
|
+
Allowed `type` words in **commit messages**, following [Conventional Commits](https://www.conventionalcommits.org/) style.
|
|
218
|
+
|
|
219
|
+
## Pre-commit checks
|
|
220
|
+
|
|
221
|
+
In addition to the branch name check, `pre-commit` conditionally runs (skipped when the project doesn't have the relevant tool):
|
|
222
|
+
|
|
223
|
+
- `npm run type-check` — if a `type-check` script exists in `package.json`
|
|
224
|
+
- `npx lint-staged` — if `lint-staged` is listed as a dependency
|
|
225
|
+
- `npx vitest related --run --passWithNoTests <staged .ts/.tsx files>` — if `vitest` is listed as a dependency and staged `.ts`/`.tsx` files exist (bounded to 60s via `timeout`/`gtimeout` when available)
|
|
226
|
+
|
|
227
|
+
Any failure here aborts the commit.
|
|
228
|
+
|
|
229
|
+
## lint-staged
|
|
230
|
+
|
|
231
|
+
The default config added by `init` (only if you don't already have one):
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"lint-staged": {
|
|
236
|
+
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
237
|
+
"*.{json,md,css,scss,html}": ["prettier --write"]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Adjust freely — `commit-sheriff` doesn't overwrite it once present.
|
|
243
|
+
|
|
244
|
+
## Examples
|
|
245
|
+
|
|
246
|
+
**Minimal project (no modules):**
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
"commitGuard": {
|
|
250
|
+
"useModules": false,
|
|
251
|
+
"project": "PROJ",
|
|
252
|
+
"branchTypes": ["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"],
|
|
253
|
+
"types": ["feat", "fix", "docs", "chore", "test"]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
git checkout -b bugfix/PROJ-1093-fix-validation
|
|
259
|
+
git commit -m "(PROJ-1093) fix: correct validation on empty input"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Project split into modules, with a locked list:**
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
"commitGuard": {
|
|
266
|
+
"useModules": true,
|
|
267
|
+
"project": "ACME",
|
|
268
|
+
"modules": ["AUTH", "BILLING", "REPORTS"],
|
|
269
|
+
"branchTypes": ["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"],
|
|
270
|
+
"types": ["feat", "fix", "docs", "chore", "test"]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
git checkout -b feature/AUTH-ACME-42-add-sso
|
|
276
|
+
git commit -m "(ACME-42) [AUTH] feat: add SSO login"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Updating
|
|
280
|
+
|
|
281
|
+
When a new version of `commit-sheriff` is published:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm install commit-sheriff@latest --save-dev
|
|
285
|
+
npx commit-sheriff init
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`npm install` updates the package; `npx commit-sheriff init` re-copies the (possibly changed) `.husky/commit-msg` and `.husky/pre-commit` scripts. It will **not** touch your existing `commitGuard` or `lint-staged` config in `package.json`.
|
|
289
|
+
|
|
290
|
+
## Skipping / bypassing hooks
|
|
291
|
+
|
|
292
|
+
Not recommended as a habit, but for emergencies Git supports:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
git commit --no-verify -m "..."
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Merge, revert, `fixup!`, and `squash!` commits are already exempted from the commit-message format check automatically.
|
|
299
|
+
|
|
300
|
+
## Troubleshooting
|
|
301
|
+
|
|
302
|
+
**"Could not load commitGuard config — is Node.js installed and is this running from the repo root?"**
|
|
303
|
+
The hook runs `node -e "..."` against `./package.json`. Make sure Node.js is on your `PATH` and that you're committing from the repository root (or that your Git client runs hooks with the repo root as the working directory — some GUI clients get this wrong).
|
|
304
|
+
|
|
305
|
+
**A commit is accepted even though the message looks wrong**
|
|
306
|
+
Check which branch/tool you actually committed from — hooks only run for the local Git client that has Husky's `core.hooksPath` configured (`git config core.hooksPath` should print `.husky/_`). GUI clients or CI systems that bypass local hooks (or commit via the GitHub/GitLab API) will not trigger them.
|
|
307
|
+
|
|
308
|
+
**Branch name rejected right after `git checkout -b ...`**
|
|
309
|
+
Detached HEAD is exempt, but a normal new branch is checked immediately on first commit — rename it with `git branch -m <valid-name>` and try again.
|
|
310
|
+
|
|
311
|
+
**`useModules: true` but every module code is accepted**
|
|
312
|
+
That's expected if `commitGuard.modules` is empty/omitted — see [`modules`](#modules). Add an explicit list to restrict it.
|
|
313
|
+
|
|
314
|
+
## Releasing this package
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npm version patch # or minor / major
|
|
318
|
+
npm publish
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
(`npm version` runs on a properly named branch per this repo's own hooks, then fast-forward it into `main`.)
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
ISC
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const command = process.argv[2];
|
|
9
|
+
|
|
10
|
+
const DEFAULT_COMMIT_GUARD = {
|
|
11
|
+
useModules: false,
|
|
12
|
+
project: "PROJ",
|
|
13
|
+
branchTypes: [
|
|
14
|
+
"feature",
|
|
15
|
+
"bugfix",
|
|
16
|
+
"hotfix",
|
|
17
|
+
"improvement",
|
|
18
|
+
"refactor",
|
|
19
|
+
"release",
|
|
20
|
+
"chore",
|
|
21
|
+
"docs",
|
|
22
|
+
"test",
|
|
23
|
+
"spike",
|
|
24
|
+
],
|
|
25
|
+
types: [
|
|
26
|
+
"feat",
|
|
27
|
+
"feature",
|
|
28
|
+
"fix",
|
|
29
|
+
"docs",
|
|
30
|
+
"style",
|
|
31
|
+
"refactor",
|
|
32
|
+
"test",
|
|
33
|
+
"chore",
|
|
34
|
+
"perf",
|
|
35
|
+
"ci",
|
|
36
|
+
"build",
|
|
37
|
+
"revert",
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_LINT_STAGED = {
|
|
42
|
+
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
43
|
+
"*.{json,md,css,scss,html}": ["prettier --write"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function readPackageJson() {
|
|
47
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
48
|
+
if (!fs.existsSync(pkgPath)) {
|
|
49
|
+
console.error(" package.json tapılmadı. Əvvəlcə `npm init` işlədin.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return { pkgPath, pkg: JSON.parse(fs.readFileSync(pkgPath, "utf8")) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureHusky() {
|
|
56
|
+
const huskyDir = path.join(cwd, ".husky");
|
|
57
|
+
const shimPath = path.join(huskyDir, "_", "husky.sh");
|
|
58
|
+
if (!fs.existsSync(shimPath)) {
|
|
59
|
+
console.log("→ husky quraşdırılır (npx husky init)...");
|
|
60
|
+
execSync("npx husky init", { cwd, stdio: "inherit" });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function copyHook(name) {
|
|
65
|
+
const src = path.join(__dirname, "..", "templates", name);
|
|
66
|
+
const dest = path.join(cwd, ".husky", name);
|
|
67
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
68
|
+
fs.copyFileSync(src, dest);
|
|
69
|
+
fs.chmodSync(dest, 0o755);
|
|
70
|
+
console.log(`✔ .husky/${name} yazıldı`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mergeConfig() {
|
|
74
|
+
const { pkgPath, pkg } = readPackageJson();
|
|
75
|
+
let changed = false;
|
|
76
|
+
|
|
77
|
+
if (!pkg.commitGuard) {
|
|
78
|
+
pkg.commitGuard = DEFAULT_COMMIT_GUARD;
|
|
79
|
+
changed = true;
|
|
80
|
+
console.log("✔ package.json → commitGuard konfiqurasiyası əlavə olundu");
|
|
81
|
+
} else {
|
|
82
|
+
console.log("• package.json-da commitGuard artıq var, toxunulmadı");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!pkg["lint-staged"]) {
|
|
86
|
+
pkg["lint-staged"] = DEFAULT_LINT_STAGED;
|
|
87
|
+
changed = true;
|
|
88
|
+
console.log("✔ package.json → lint-staged konfiqurasiyası əlavə olundu");
|
|
89
|
+
} else {
|
|
90
|
+
console.log("• package.json-da lint-staged artıq var, toxunulmadı");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
94
|
+
if (!pkg.scripts.prepare) {
|
|
95
|
+
pkg.scripts.prepare = "husky";
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (changed) {
|
|
100
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function init() {
|
|
105
|
+
ensureHusky();
|
|
106
|
+
copyHook("commit-msg");
|
|
107
|
+
copyHook("pre-commit");
|
|
108
|
+
mergeConfig();
|
|
109
|
+
console.log(
|
|
110
|
+
"\nHazırdır. package.json → commitGuard bölməsində 'project' və lazım olsa 'modules' dəyərlərini tənzimləyin.\n",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (command === "init") {
|
|
115
|
+
init();
|
|
116
|
+
} else {
|
|
117
|
+
console.log("İstifadə: npx commit-sheriff init");
|
|
118
|
+
process.exit(command ? 1 : 0);
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commit-sheriff",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared husky commit-msg / pre-commit hooks enforcing ticket-based commit messages and branch naming",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"commit-sheriff": "bin/commit-sheriff.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
15
|
+
"prepare": "husky"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"husky",
|
|
19
|
+
"git-hooks",
|
|
20
|
+
"commit-msg",
|
|
21
|
+
"pre-commit",
|
|
22
|
+
"commitlint",
|
|
23
|
+
"conventional-commits",
|
|
24
|
+
"lint-staged",
|
|
25
|
+
"branch-naming",
|
|
26
|
+
"ticket"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "ISC",
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^10.0.1",
|
|
33
|
+
"eslint": "^10.8.0",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"globals": "^17.8.0",
|
|
36
|
+
"husky": "^9.1.7",
|
|
37
|
+
"lint-staged": "^17.2.0",
|
|
38
|
+
"prettier": "^3.9.6"
|
|
39
|
+
},
|
|
40
|
+
"lint-staged": {
|
|
41
|
+
"*.{js,jsx,ts,tsx}": [
|
|
42
|
+
"eslint --fix",
|
|
43
|
+
"prettier --write"
|
|
44
|
+
],
|
|
45
|
+
"*.{json,md,css,scss,html}": [
|
|
46
|
+
"prettier --write"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"commitGuard": {
|
|
50
|
+
"useModules": false,
|
|
51
|
+
"project": "PROJ",
|
|
52
|
+
"branchTypes": [
|
|
53
|
+
"feature",
|
|
54
|
+
"bugfix",
|
|
55
|
+
"hotfix",
|
|
56
|
+
"improvement",
|
|
57
|
+
"refactor",
|
|
58
|
+
"release",
|
|
59
|
+
"chore",
|
|
60
|
+
"docs",
|
|
61
|
+
"test",
|
|
62
|
+
"spike"
|
|
63
|
+
],
|
|
64
|
+
"types": [
|
|
65
|
+
"feat",
|
|
66
|
+
"feature",
|
|
67
|
+
"fix",
|
|
68
|
+
"docs",
|
|
69
|
+
"style",
|
|
70
|
+
"refactor",
|
|
71
|
+
"test",
|
|
72
|
+
"chore",
|
|
73
|
+
"perf",
|
|
74
|
+
"ci",
|
|
75
|
+
"build",
|
|
76
|
+
"revert"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
commit_msg_file="$1"
|
|
2
|
+
commit_msg=$(cat "$commit_msg_file")
|
|
3
|
+
|
|
4
|
+
# Skip validation for commits git/tools generate themselves
|
|
5
|
+
case "$commit_msg" in
|
|
6
|
+
"Merge "*|"Revert "*|"fixup! "*|"squash! "*)
|
|
7
|
+
exit 0
|
|
8
|
+
;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
# ── Load config from package.json → "commitGuard" (falls back to sane defaults) ──
|
|
12
|
+
# commitGuard: { "useModules": true, "modules": [...], "types": [...] }
|
|
13
|
+
eval "$(node -e "
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
let cfg = {};
|
|
16
|
+
try { cfg = (JSON.parse(fs.readFileSync('package.json', 'utf8')).commitGuard) || {}; } catch (e) {}
|
|
17
|
+
const useModules = cfg.useModules !== undefined ? cfg.useModules : true;
|
|
18
|
+
const modules = (cfg.modules && cfg.modules.length ? cfg.modules.join('|') : '[A-Z]+');
|
|
19
|
+
const types = (cfg.types || ['feat','feature','fix','docs','style','refactor','test','chore','perf','ci','build','revert']).join('|');
|
|
20
|
+
console.log('USE_MODULES=' + useModules);
|
|
21
|
+
console.log('MODULES=\"' + modules + '\"');
|
|
22
|
+
console.log('TYPES=\"' + types + '\"');
|
|
23
|
+
" 2>/dev/null)"
|
|
24
|
+
|
|
25
|
+
if [ -z "$TYPES" ]; then
|
|
26
|
+
echo ""
|
|
27
|
+
echo " Could not load commitGuard config — is Node.js installed and is this running from the repo root?"
|
|
28
|
+
echo ""
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# ── Commit message format ───────────────────────────────────────────────────
|
|
33
|
+
if [ "$USE_MODULES" = "true" ]; then
|
|
34
|
+
# Format : (TICKET) [MODULE] type(scope): message
|
|
35
|
+
# Example: (PROJ-1077) [SET] feature(organization structure): implement organization management
|
|
36
|
+
valid_commit_regex="^\([A-Z]+-[0-9]+\) \[($MODULES)\] ($TYPES)(\([^)]+\))?: .+"
|
|
37
|
+
else
|
|
38
|
+
# Format : (TICKET) type(scope): message
|
|
39
|
+
# Example: (PROJ-1077) feature(organization structure): implement organization management
|
|
40
|
+
valid_commit_regex="^\([A-Z]+-[0-9]+\) ($TYPES)(\([^)]+\))?: .+"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [[ ! $commit_msg =~ $valid_commit_regex ]]; then
|
|
44
|
+
echo ""
|
|
45
|
+
echo " Invalid commit message: '$commit_msg'"
|
|
46
|
+
echo ""
|
|
47
|
+
if [ "$USE_MODULES" = "true" ]; then
|
|
48
|
+
echo " Required format : (TICKET) [MODULE] type(scope): message"
|
|
49
|
+
echo " Example : (PROJ-1077) [SET] feature(organization structure): implement organization management"
|
|
50
|
+
echo " Example : (PROJ-1191) [SET] fix(register): reset registration session on menu reopen"
|
|
51
|
+
else
|
|
52
|
+
echo " Required format : (TICKET) type(scope): message"
|
|
53
|
+
echo " Example : (PROJ-1077) feature(organization structure): implement organization management"
|
|
54
|
+
echo " Example : (PROJ-1191) fix(register): reset registration session on menu reopen"
|
|
55
|
+
fi
|
|
56
|
+
echo ""
|
|
57
|
+
echo " Ticket : (PROJECT-NUMBER), e.g. (PROJ-1191)"
|
|
58
|
+
[ "$USE_MODULES" = "true" ] && echo " Modules : $(echo $MODULES | tr '|' ' ')"
|
|
59
|
+
echo " Types : $(echo $TYPES | tr '|' ' ')"
|
|
60
|
+
echo " Scope : optional — e.g. fix(register): ..."
|
|
61
|
+
echo ""
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$USE_MODULES" = "true" ]; then
|
|
66
|
+
# ── Keep [MODULE] consistent with the branch ────────────────────────────
|
|
67
|
+
# e.g. bugfix/SET-PROJ-1127-... -> commit must be tagged [SET]
|
|
68
|
+
local_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
|
|
69
|
+
branch_module=$(printf '%s' "$local_branch" | sed -nE "s#^[a-z]+/($MODULES)-.*#\1#p")
|
|
70
|
+
commit_module=$(printf '%s' "$commit_msg" | sed -nE 's/^\([A-Z]+-[0-9]+\) \[([A-Z]+)\].*/\1/p')
|
|
71
|
+
|
|
72
|
+
if [ -n "$branch_module" ] && [ "$branch_module" != "$commit_module" ]; then
|
|
73
|
+
echo ""
|
|
74
|
+
echo " Commit module [$commit_module] does not match the branch module [$branch_module]."
|
|
75
|
+
echo " Branch : $local_branch"
|
|
76
|
+
echo " Use [$branch_module] to match the branch, e.g. (PROJ-1191) [$branch_module] fix(scope): ..."
|
|
77
|
+
echo ""
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
local_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
|
|
2
|
+
|
|
3
|
+
# ── Load config from package.json → "commitGuard" (falls back to sane defaults) ──
|
|
4
|
+
# commitGuard: { "useModules": true, "project": "PROJ", "modules": [...], "branchTypes": [...] }
|
|
5
|
+
eval "$(node -e "
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
let cfg = {};
|
|
8
|
+
try { cfg = (JSON.parse(fs.readFileSync('package.json', 'utf8')).commitGuard) || {}; } catch (e) {}
|
|
9
|
+
const useModules = cfg.useModules !== undefined ? cfg.useModules : true;
|
|
10
|
+
const project = cfg.project || 'PROJ';
|
|
11
|
+
const modules = (cfg.modules && cfg.modules.length ? cfg.modules.join('|') : '[A-Z]+');
|
|
12
|
+
const branchTypes = (cfg.branchTypes || ['feature','bugfix','hotfix','improvement','refactor','release','chore','docs','test','spike']).join('|');
|
|
13
|
+
console.log('USE_MODULES=' + useModules);
|
|
14
|
+
console.log('PROJECT=' + project);
|
|
15
|
+
console.log('MODULES=\"' + modules + '\"');
|
|
16
|
+
console.log('BRANCH_TYPES=\"' + branchTypes + '\"');
|
|
17
|
+
" 2>/dev/null)"
|
|
18
|
+
|
|
19
|
+
# ── Branch format (skipped on detached HEAD, e.g. mid-rebase) ──────────────
|
|
20
|
+
if [ -z "$local_branch" ] || [ "$local_branch" = "HEAD" ]; then
|
|
21
|
+
echo " Detached HEAD — skipping branch name check."
|
|
22
|
+
elif [ -z "$PROJECT" ]; then
|
|
23
|
+
echo ""
|
|
24
|
+
echo " Could not load commitGuard config — is Node.js installed and is this running from the repo root?"
|
|
25
|
+
echo ""
|
|
26
|
+
exit 1
|
|
27
|
+
else
|
|
28
|
+
if [ "$USE_MODULES" = "true" ]; then
|
|
29
|
+
# Format : <type>/<MODULE>-<PROJECT>-<NUMBER>-<description>
|
|
30
|
+
# Example: improvement/PRO-PROJ-609-correct-husky-pre-commit-validation
|
|
31
|
+
valid_branch_regex="^($BRANCH_TYPES)\/($MODULES)-$PROJECT-[0-9]+-[a-z][a-z0-9-]*$"
|
|
32
|
+
else
|
|
33
|
+
# Format : <type>/<PROJECT>-<NUMBER>-<description>
|
|
34
|
+
# Example: bugfix/PROJ-1093-fix-validation
|
|
35
|
+
valid_branch_regex="^($BRANCH_TYPES)\/$PROJECT-[0-9]+-[a-z][a-z0-9-]*$"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [[ ! $local_branch =~ $valid_branch_regex ]]; then
|
|
39
|
+
echo ""
|
|
40
|
+
echo " Invalid branch name: '$local_branch'"
|
|
41
|
+
echo ""
|
|
42
|
+
if [ "$USE_MODULES" = "true" ]; then
|
|
43
|
+
echo " Required format : <type>/<MODULE>-<PROJECT>-<NUMBER>-<description>"
|
|
44
|
+
echo " Example : improvement/PRO-PROJ-609-correct-husky-pre-commit-validation"
|
|
45
|
+
else
|
|
46
|
+
echo " Required format : <type>/<PROJECT>-<NUMBER>-<description>"
|
|
47
|
+
echo " Example : bugfix/PROJ-1093-fix-meeting-validation"
|
|
48
|
+
fi
|
|
49
|
+
echo ""
|
|
50
|
+
echo " Types : $(echo $BRANCH_TYPES | tr '|' ' ')"
|
|
51
|
+
[ "$USE_MODULES" = "true" ] && echo " Modules : $(echo $MODULES | tr '|' ' ')"
|
|
52
|
+
echo " Project : $PROJECT"
|
|
53
|
+
echo ""
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# ── Lint + type-check (only runs if the project actually has them) ─────────
|
|
59
|
+
HAS_TYPE_CHECK=$(node -e "process.stdout.write(((require('./package.json').scripts)||{})['type-check'] ? '1' : '0')" 2>/dev/null)
|
|
60
|
+
if [ "$HAS_TYPE_CHECK" = "1" ]; then
|
|
61
|
+
npm run type-check || exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
HAS_LINT_STAGED=$(node -e "
|
|
65
|
+
const p = require('./package.json');
|
|
66
|
+
const deps = Object.assign({}, p.dependencies, p.devDependencies);
|
|
67
|
+
process.stdout.write(deps['lint-staged'] ? '1' : '0');
|
|
68
|
+
" 2>/dev/null)
|
|
69
|
+
if [ "$HAS_LINT_STAGED" = "1" ]; then
|
|
70
|
+
npx lint-staged || exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ── Related tests (only if vitest is installed) ─────────────────────────────
|
|
74
|
+
HAS_VITEST=$(node -e "
|
|
75
|
+
const p = require('./package.json');
|
|
76
|
+
const deps = Object.assign({}, p.dependencies, p.devDependencies);
|
|
77
|
+
process.stdout.write(deps['vitest'] ? '1' : '0');
|
|
78
|
+
" 2>/dev/null)
|
|
79
|
+
if [ "$HAS_VITEST" = "1" ]; then
|
|
80
|
+
STAGED_TS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx)$' || true)
|
|
81
|
+
if [ -n "$STAGED_TS" ]; then
|
|
82
|
+
if command -v timeout > /dev/null 2>&1; then
|
|
83
|
+
timeout 60 npx vitest related --run --passWithNoTests $STAGED_TS
|
|
84
|
+
elif command -v gtimeout > /dev/null 2>&1; then
|
|
85
|
+
gtimeout 60 npx vitest related --run --passWithNoTests $STAGED_TS
|
|
86
|
+
else
|
|
87
|
+
npx vitest related --run --passWithNoTests $STAGED_TS
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
fi
|