A pre-commit hook that keeps commits small. It blocks a commit when its staged changes exceed your limits on lines of code added, removed, changed, or in total.
$ git commit -m "Add the new importer"
limit lines of code per commit (cloc)....................................Failed
- hook id: cloc-diff-limit
- exit code: 1
cloc-diff-limit: code lines: total 30 (added 30, removed 0, changed 0)
cloc-diff-limit: commit exceeds limits:
total 30 > 20Counting is done by cloc, so blank lines never count and comments count only if you ask. The hook itself is a small, strictly POSIX sh wrapper.
-
Add the hook to
.pre-commit-config.yaml:repos: - repo: https://github.com/Coacher/cloc-diff-limit rev: v1.0.0 hooks: - id: cloc-diff-limit args: [--max-total, "400", --exclude-dir=tests]
-
Install it into your clone:
pre-commit install
Important
cloc-diff-limit needs cloc 1.86 or later on your PATH, for example brew install cloc or sudo apt install cloc.
If you'd rather not install it, use the cloc-diff-limit-npm hook id instead: pre-commit then installs cloc itself, and only Perl is needed.
| Option | Default | Meaning |
|---|---|---|
--max-total N |
-1 |
Limit on added + removed + changed lines |
--max-added N |
-1 |
Limit on added lines |
--max-removed N |
-1 |
Limit on removed lines |
--max-changed N |
-1 |
Limit on changed (modified) lines |
--include-comments |
off | Count comment lines as well as code lines |
--base REF |
HEAD |
Revision to compare the staged changes against |
-v, --verbose |
off | Print the counts even when the commit passes |
-h, --help |
Print usage | |
| any other option | Passed to cloc unchanged |
N is an integer >= 0, or -1 for no limit. A limit of 0 allows no lines of that kind. Both --max-total 400 and --max-total=400 work.
Every cloc filter works, because options the hook doesn't recognise go straight to cloc:
--exclude-dir=tests,migrations # directory names, at any depth
--exclude-ext=sql,lock # file extensions
--exclude-lang=Markdown,JSON,YAML # cloc language names (see: cloc --show-lang)
--not-match-f='(_test\.go|\.min\.js)$' # regex on file names
--fullpath --not-match-d='/generated(/|$)' # regex on directory paths
--exclude-list-file=.clocignore # one file or directory name per lineWarning
Repeating an option such as --exclude-dir replaces the earlier value instead of adding to it. List several values in one option, separated by commas: --exclude-dir=tests,migrations.
Without --fullpath, --not-match-d only looks at the name of the folder a file is directly in, so --not-match-d=vendor misses vendor/lib/x.py.
Tip
cloc treats Markdown, JSON, YAML and plain text as languages. Add --exclude-lang=Markdown,JSON,YAML,Text if docs and config files shouldn't count against the limit.
Note
pre-commit's own files: and exclude: settings have no effect on this hook, because it checks the whole commit rather than a list of files. Use the cloc options above instead.
Keep every commit under 300 changed lines, ignoring tests and generated SQL
- id: cloc-diff-limit
args: [--max-total, "300", --exclude-dir=tests, --exclude-ext=sql]Separate limits, comments included
- id: cloc-diff-limit
args: [--max-added, "200", --max-removed, "-1", --max-changed, "100", --include-comments]Check a whole branch in CI
The hook works outside pre-commit too. Here it checks everything a branch changes since it split off main (fetch with enough history for the merge base to exist):
./cloc-diff-limit --base "$(git merge-base origin/main HEAD)" --max-total 1000 --verbose- Added, removed and changed follow cloc's diff. An edited line counts once as changed, not as one removal plus one addition the way
git diff --numstatreports it. - Total is added + removed + changed.
- Blank lines never count. Comment lines count only with
--include-comments. - Files cloc doesn't recognise as source code, such as binaries, are ignored.
- Only staged changes count. Unstaged edits in your working tree are ignored.
All counting is one cloc call:
cloc --git --diff HEAD "$(git write-tree)" --quiet --hide-rate --skip-uniqueness [your cloc options]git write-tree turns the staged index into a tree object, the same thing git commit is about to record. cloc compares it with HEAD directly from git's object store, so nothing is checked out to disk. Its report ends with totals:
SUM:
same 0 0 0 1
modified 3 0 1 5
added 1 1 1 5
removed 0 0 0 0
The columns are files, blank, comment and code. The hook reads these rows with the shell's built-in read and compares them with your limits. If the block is missing, it fails with an error instead of silently passing.
- First commit: compared against the empty tree, so every line counts as added.
- Merge commits are skipped, because their diff against
HEADis the whole merged branch. Passing--baseturns the check back on. git commit --amendis compared against the commit being amended, not its parent. A pre-commit hook can't tell that an amend is happening.- Bypassing the check once:
git commit --no-verify, orSKIP=cloc-diff-limit git commitwith pre-commit. - Exit codes:
0within limits,1a limit was exceeded,2invalid options, cloc missing or failing, or not in a git repository.
Install as a plain git hook
Copy cloc-diff-limit into your repository's .git/hooks/ folder and create .git/hooks/pre-commit with your options:
#!/bin/sh
exec "$(dirname "$0")/cloc-diff-limit" --max-total 400 --exclude-dir=tests "$@"Then make both files executable:
chmod +x .git/hooks/pre-commit .git/hooks/cloc-diff-limit| Tested in CI | |
|---|---|
| cloc | Releases 1.86 and 2.10 (every release in between passes too), Ubuntu's package, and the npm package |
| Shells | dash, posh, yash (--posix), busybox sh, bash (--posix), macOS /bin/sh |
| Tools | Only git and cloc; everything else is shell built-ins (--help also uses sed) |
cloc 1.84 and older miscount some cases, such as renamed files.
The wrapper adds a few milliseconds; the rest is cloc. cloc only reads the files changed in the commit, so repository size doesn't matter. Measured with cloc 2.10:
| Staged change | Time |
|---|---|
| 41 new files, 3,080 lines | about 0.25 s |
| 1,000 modified lines in one file, plus 40 edited files | about 0.45 s |
This hook deliberately counts nothing itself. cloc was the only mature tool that does all three things it needs:
| Tool | Knows code from comments | Compares git revisions | Reports changed lines separately |
|---|---|---|---|
| cloc | ✅ | ✅ (--git --diff, any revision or tree) |
✅ |
| scc, tokei, gocloc, loc | ✅ | ❌ totals only | ❌ |
| sccdiff, git-loc | ✅ | net difference only | ❌ |
| rloc (Rust cloc port) | ✅ | directories only, not git | ✅ |
git diff --numstat |
❌ counts blanks and comments | ✅ | ❌ |
You need bats, cloc, ShellCheck and checkbashisms (from Debian's devscripts). The integration tests also need pre-commit and Node.
make lint # ShellCheck and checkbashisms
make test # unit tests under sh (HOOK_SHELL="yash --posix" make test for another shell)
make test-shells # unit tests under every installed POSIX shell
make test-integration # real commits through pre-commit, for both hook idsCI runs all of these on every push and pull request: the unit tests under five shells on Linux and under macOS /bin/sh, with the newest and oldest supported cloc releases plus the Ubuntu and npm packages.
MIT. cloc itself is a separate project under the GPL-2.0 and is not bundled.