なにができるか
- commit前にcommitされるファイルで差分のあるものだけをチェックする
- eslintの進捗が確認できる
前提知識
静的コード解析を取り入れている現場は多い。
そして、JSだとeslint --fix + lint-stagedで、変更分に対して、静的コード解析+コード修正を行うことが割とスタンダードだ。
そして、huskyを使って、コミット前にコードチェックを強制する光景もよく見る。
「コミット前に Lint を強制するなら lint-staged が便利」はもろにその話。
不満
lint-stagedは、対象のプロジェクト全体に対し、インデックスにのっていない差分まですべてチェックしているようなので、機能を作ってからcommitを分けるスタイルの自分だとcommitのたびに時間がかかる。1
そして、そのときの進捗もわからないので、ちょっと別のことをやったりしていて集中が途切れたりした。
こうしたかった
- commitをされる分だけ、チェックしたい
- ファイル指定でcommitする場合はファイル単位でチェック
- commitがどれくらいで終わるかを確認したい
こんなスクリプトを書いた
const child_process = require('child_process');
const path = require('path');
let exitCode = 0;
const errorLog = (stdout) => {
const error = stdout.toString('utf8');
if (error.length > 0) {
console.log('\x1b[31m', error);
exitCode = 1;
}
};
const getChangedFiles = () => {
const changedFiles = [];
try {
const str = child_process
.execSync('git diff --cached --name-status | grep "\\.jsx\\?$"')
.toString('utf8');
// delimiter newline
const changedLines = str
.split(/(\r?\n)/g)
.filter(line => !(line === '\n' || line === '\r' || line.length < 1));
changedLines.forEach((l) => {
// data = [gitStatus, fileName];
const data = l.split('\t');
// exclude `Delete` status files. To avoid `checkFileChanged` error
if (data[0] !== 'D') {
changedFiles.push(data[1]);
}
});
} catch (e) {
errorLog(e.stdout);
}
return changedFiles;
};
const lintFiles = (files) => {
let progress = 0;
files.forEach((file) => {
progress += 1;
console.log(`${progress}/${files.length} : ${file}`);
try {
child_process
.execSync(
`${path.join(__dirname, 'node_modules/.bin/eslint')} \
-c ${path.join(__dirname, '.eslintrc')} \
--ignore-path ${path.join(__dirname, '.eslintignore')} \
--color --fix ${file}`
)
.toString('utf8');
} catch (e) {
errorLog(e.stdout);
}
});
};
// Stop commit if file changed by `eslint:fix`
const checkFileChanged = (files) => {
try {
const changedFiles = files.join(' ');
const str = child_process
.execSync(`git diff --name-only ${changedFiles}`)
.toString('utf8');
if (str.length > 0) {
errorLog(`eslint fix changed following files : ${str}`);
errorLog('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
errorLog('!!!!!! Please commit again !!!!!!');
errorLog('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
}
} catch (e) {
errorLog(e.stdout);
}
};
const changedFiles = getChangedFiles();
if (changedFiles.length > 0) {
lintFiles(changedFiles);
checkFileChanged(changedFiles);
}
process.exit(exitCode);
huskyを利用しているのであれば、package.jsonに以下のコードを入れるだけ。
"scripts": {
"precommit" : "node ./precommit.js"
}
実行結果
eslint --fix
の実行でファイルが変更された場合、commitが失敗するようになっている。失敗したら、整形後のファイルに対して再度commitが必要。
ローカルにてprettierを利用して、コード整形をしており、fixによる修正があまり発生しない前提で利用している。チェックするファイルが全4ファイルで、残り何ファイルチェックする必要があるかわかる。
% git commit -m 'commit message' .
Found '/Users/amachi/programs/project/.nvmrc' with version <v6.11.5>
Now using node v6.11.5 (npm v3.10.10)
husky > npm run -s precommit (node v6.11.5)
1/4 : app/js/containers/BaseRouter.jsx
...(中略)...
3/4 : app/js/lib/__tests__/getSubDomain.test.js
4/4 : app/js/lib/getSubDomain.js
[index_design fa9d7c2] commit message
4 files changed, 97 insertions(+)
create mode 100644 app/js/lib/__tests__/getModuleName.test.js
create mode 100644 app/js/lib/__tests__/getSubDomain.test.js
create mode 100644 app/js/lib/getSubDomain.js
本当は、precommitの間に変更されたファイルをそのままHEADに反映したいが、綺麗に実装する方法がわからないので、詳しい人に教えて欲しい。
取り入れた結果
体感速度があがったので、コミット中に他の用事をすることが減った。集中も途切れないで済むので、効率は上がったと信じている。
lint-stagedで同じようなことをしたかったが、現在そのようなオプションが見当たらなかったので、本当にないのであれば作ろうかと思っている。
-
挙動からみただけなので正しいかは不明だが、addするまえのステージにないファイルもチェックされる。lint-stagedという名前がついているのに、ステージに乗ってないものもチェックされているのがよく分かっていない... ↩