앞선 게시글에서 협업을 위한 기능들을 살펴보았습니다.
모든 부분에서 그렇지만 구두로의 약속은 지키기 어렵습니다.
새로운 사람이 작업에 참여하거나 개인의 실수로 인해서 약속을 지키지 못하는 경우가 생기기 마련입니다.
따라서 시스템적으로 해당 설정들을 일부분 강제해주는 것이 퀄리티를 높이는 가장 좋은 방법이라고 생각합니다.
해당 게시글에서는 git-hook을 이용해서 linting & formatting, commit-message를 형식에 맞추어 commit 하는 방법에 대해 알아보도록 하겠습니다.
Git hook
Git hook이 무엇인가요?
- 커맨드 이벤트 (
add
,commit
,push
등) 전/후에 호출이 되는 코드(스크립트)를 의미합니다. - 컨벤션에 맞지 않는 커밋이나 테스트를 통과하지 못한 코드를 사전에 예방할 수 있습니다.
.git/hooks
에<hook name>.sample
파일들이 존재하며.sample
확장자를 지우면 해당 hook 발생 시 파일을 실행시킵니다.
이때 직접 hook파일을 건드는 방법과 package를 통해 사용하는 방법이 존재합니다.
개인적으로는 직접 hook 파일을 건들지 않고 package를 통해서 사용하는 것을 추천드립니다.
Git hook 관련 package를 사용하는 이유
.git/hooks
폴더에 직접 접근하지 않아도 hook을 사용할 수 있어 편리합니다.git
은 원격에 올라가지 않기 때문에 package를 사용하면 팀 내 공유가 가능합니다- 직접 구현하는 것보다 config 파일을 설정하는 것이 편리합니다.
사용 중인 hook
- pre-commit → commit이 되기 직전에 실행되는 스크립트입니다. linting & formatting을 검사합니다.
- commit-msg → commit-message가 특정 형식에 맞추어져 있는지 확인합니다.
- 더 많은 기능과 정보는 Gitbook을 통해 확인할 수 있습니다
그렇다면 어떻게 세팅해야 하는지 Python환경과 Nodejs 환경을 구분해서 설명하도록 하겠습니다.
Node.js
Node.js의 경우 huksy
라는 pacakge를 통해 hook에 접근할수 있습니다.
yarn add husky --dev
npm install husky --save-dev
package.json 파일에서 hook에서 실행할 command 를 설정합니다
- commit-msg : commit-message를 검사할 스크립트를 실행합니다.
- pre-commit : lint-staged를 실행합니다.
"scripts":{
// 생략
},
"husky": {
"hooks": {
"commit-msg": "node ./commit-msg.js",
"pre-commit": "lint-staged"
}
}
pre-commit
vue 에서 사용하기 위해서 아래 package들을 설치해야 합니다.
yarn add eslint-config-prettier --dev
yarn add eslint-plugin-prettier --dev
yarn add prettier --dev
lint-staged를 통해 staging되어있는 파일만 hook을 실행시킬 수 있습니다.
yarn add lint-staged --dev
npm install lint-staged --save-dev
lint-staged에서 실행 할 command를 package.json에 설정해줍니다.
- 검사할 파일을 명시할 수 있습니다. (
"*.{js,jsx,vue,ts,tsx}"
) 아래 예시는 모든 js 파일을 검사합니다. - eslint 검사 후 수정이 가능하다면 수정합니다.
- prettier 검사 후 수정이 가능하다면 수정합니다.
- 검사이후 자동 수정된 파일이 있다면 git add를 통해 staged상태로 만들어 줍니다.
"scripts":{
// 생략
},
"husky": {
// 생략
}
"lint-staged": {
"*.js": [
"eslint . --fix",
"prettier --write",
"git add"
]
},
commit-msg
commit message를 인자로 받아 convention을 검사할수 있는 파일을 만들어 줍니다
const chalk = require("chalk");
const msgPath = process.env.HUSKY_GIT_PARAMS;
const msg = require("fs").readFileSync(msgPath, "utf-8").trim();
const commitRE = /^(revert: ")?(feat|fix|docs|style|refactor|pref|test|ci|build|chore|)(\(.+\))?!?: .{1,50}/;
if (!commitRE.test(msg)) {
console.error(
` ${chalk.bgRed.white(" ERROR ")} ${chalk.red(
`invalid commit message format.`
)}\n\n` +
chalk.red(
` Proper commit message format is required for automated changelog generation. Examples:\n\n`
) +
` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
` ${chalk.green(
`fix(v-model): handle events on blur (close #28)`
)}\n\n` +
chalk.red(
` You can also use ${chalk.cyan(
`yarn commit`
)} to interactively generate a commit message.\n`
)
);
process.exit(1);
}
혹시 console이 존재할때 commit을 거부하고 싶다면 아래 스크립트를 추가해서 사용하세요.
const chalk = require('chalk');
const childProcessExec = require('child_process').exec;
const util = require('util');
const exec = util.promisify(childProcessExec);
const regex = /(.*)console.log/;
async function getStagedFile() {
const stagedFileOutput = await exec('git diff --cached');
if (stagedFileOutput.stderr) {
throw new Error('error');
}
const stagedFile = stagedFileOutput.stdout;
console.log(stagedFile);
if (regex.test(stagedFile)) {
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`Remove console.log`)}\n`
);
process.exit(1);
}
}
getStagedFile();
commitizen
commit-message 작성을 편하게 해주는 package입니다. (commitizen docs)
yarn add commitizen --dev
npm install commitizen --save-dev
다음 설정을 package.json에 적은 이후에yarn git-cz
, npm git-cz
를 실행하시면 사용할 수 있습니다.
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
추가사항
Python
Python의 경우 pre-commit
pacakge를 통해 hook에 접근할수 있습니다.
pip install pre-commit
해당 글에서는 pre-commit hook만 사용하지만 pre-commit docs Github : pre-commit를 통해서 더 많은 기능을 확인하실 수 있습니다.
pre-commit
pre-commit을 사용하기 위해서 프로젝트에 .pre-commit-config-yaml
파일을 만들어 pre-commit hook 에서 실행할 기능을 추가합니다.
아래 설정은 flake8
black
isort
mypy
을 모두 검사합니다.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.8.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.782"
hooks:
- id: mypy
flake8
black
isort
mypy
들의 세부 설정은 setup.cfg
파일에서 해줍니다.
#setup.cfg
[flake8]
ignore = W503
max-line-length = 88
[isort]
line_length = 88
multi_line_output = 3
atomic = true
include_trailing_comma = true
combine_as_imports = true
force_alphabetical_sort_within_sections = true
[mypy]
python_version=3.8
pretty = True
disallow_untyped_calls=True
disallow_untyped_defs=True
disallow_incomplete_defs= True
warn_incomplete_stub = True
warn_redundant_casts=True
warn_no_return=True
warn_unused_ignores=True
ignore_missing_imports=True
allow_redefinition=True
strict_optional=False
설정이 완료 되었다면 pre-commit run
을 통해서 설정을 적용합니다.
commit-msg
commit message 를 인자로 받아 convention을 검사할수 있는 파일을 만들어 줍니다.
#!/usr/bin/python
import re
import sys
COMMIT_MESSAGE_REGEX = (
r'^((revert: ")?(feat|fix|docs|style|refactor|perf|test|ci|build|chore)'
r"(\(.*\))?!?:\s.{1,50})"
)
def valid_commit_message(message):
"""
Function to validate the commit message
Args:
message (str): The message to validate
Returns:
bool: True for valid messages, False otherwise
"""
if not re.match(COMMIT_MESSAGE_REGEX, message):
print(
"Proper commit message format is required for automated changelog"
"generation. Examples:\n\n"
)
print("feat(compiler): add 'comments' option")
print("fix(v-model): handle events on blur (close #28)\n\n")
print(
"You can also use cz commit to interactively "
"generate a commit message.\n"
)
return False
print("Commit message is valid.")
return True
def main():
"""Main function."""
message_file = sys.argv[1]
try:
txt_file = open(message_file, "r")
commit_message = txt_file.read()
finally:
txt_file.close()
if not valid_commit_message(commit_message):
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
앞선 드린 설명처럼 모든 hook은 .git/hooks
안에 있는 파일들이 실행되는것을 뜻합니다.
따라서 package를 사용하지 않고 위 파일을 실행시키기 위해서는 .git/hooks/commit-msg
을 직접 변경해야 합니다.
저는 스크립트를 통해서 위 파일을 .git/hooks/commit-msg
파일과 symlink 처리해서 commit-msg hook을 사용할 수 있게 만들었습니다.
ln -sf ../../hooks/commit-msg.py ./.git/hooks/commit-msg
# 현재위치에서 ./.git/hooks/commit-msg 파일을 (뒤에 오는 path)
# 파일위치에서 ../../hooks/commit-msg 파일과 연동한다. (앞에 오는 path)
chmod +x .git/hooks/commit-msg
# .git/hooks에 있는 commit-msg가 실행될 수 있도록 권한을 부여합니다.
symlink를 사용할때는 경로가 중요합니다. 위 스크립트를 사용할때 폴더 구조는 다음과 같습니다.
ㄴhooks
ㄴ commit-msg.py
ㄴ.git
ㄴhooks
ㄴcommit-msg
ㄴ.pre-commit-config-yaml
ㄴsetup.cfg
위에서 pre-commit도 설정후 적용시키기 위한 명령어가 필요하기 때문에 pre-commit과 commit-msg hook을 실행시키기 위한 스크립트는 아래 내용이 됩니다.
#!/bin/bash
# pre-commit hook install from pip
pre-commit install
# symlink
ln -sf ../../hooks/commit-msg.py ./.git/hooks/commit-msg
# implement permission
chmod +x .git/hooks/commit-msg
commitizen
python 역시 commitizen이 존재합니다. (commitizen docs)
pip install commtizen
으로 설치합니다.- 이후에
cz commit
또는cz c
명령어를 통해서 commit message를 작성할 수 있습니다.
한계점
nodejs 환경과 다르게 python에서는 한계점이 존재합니다.
그것은 clone 또는 fork이후 반드시 한번은 해당 스크립트를 실행시켜줘야 한다는 점입니다.
따라서 협업하는 사람들에게 해당 사항을 반드시 인지시키거나, 작업간 자주 사용하는 스크립트에 추가하는 등의 방법이 필요합니다.
하지만 해당 방법은 사람이 해야하기 때문에 허점이 존재합니다. 따라서 github actions를 통해서 추가적인 검사를 하는것을 추천드립니다.
추가사항
- 구현은 여기를 확인하세요
Reference
VS Code에서 ESlint와 Prettier 함께 사용하기
Automate Python workflow using pre-commits: black and flake8