pre-commit, black, flake8로 파이썬 프로젝트의 git 워크플로우 자동화하기

Created
Jul 15, 2020
Created by
Tags
Python
Property
 
 
 
프로젝트를 셋업할 때 git과 관련된 자동화 스크립트는 보통 .git/hooks에 작성한다. 파이썬 프로젝트일 경우 커밋하기 전에 PEP8 검사를 하도록 pre-commit에 flake8을 실행하는 파이썬 스크립트를 작성한다거나, JIRA이슈 번호가 커밋 메세지에 있는지 체크하는 쉘 스크립트를 추가하는 게 그 예다.
로컬환경의 .git/hooks에 스크립트를 직접 추가할 수도 있지만 리퍼지토리마다 등록된 스크립트의 실행환경이 서로 다를 수 있기 때문에 로컬환경에서 .git/hooks에 직접 작성하는 건 꽤 번거롭다. 따라서 pre-commit 이라는 오픈소스를 사용해서 간단한 명령어만으로 이미 작성된 스크립트들을 git hook에 설정할 수 있도록 할 것이다.
 
구현하려는 hook의 전반적인 플로우는 아래와 같다.
notion image
 
 

1. pre-commit 패키지 설치하기

 
먼저 필요한 패키지를 파이썬 가상환경에 설치한다. pre-commit은 pip로 간단하게 설치 할 수 있다.
$ pip install pre-commit
 
 

2. .pre-commit-config.yaml 작성하기

 
pre-commit은 .pre-commit-config.yaml이라는 파일에 작성된 hook들을 실행하므로 먼저 .pre-commit-config.yaml을 만들어야 한다. pre-commit은 hook으로 사용하려는 소스를 깃헙 리퍼지토리에서 내려받으므로 기본적으로 아래의 속성을 요구한다.
 
  • repo : hook으로 사용할 소스의 리퍼지토리
  • rev : hook으로 사용할 소스의 git 태그 또는 리비전
  • id : hook에서 사용할 소스가 제공하는 커맨드
 
처음부터 위 속성들을 직접 정의하는 것보단 샘플을 참고하는 게 편리하다. 아래의 커맨드를 통해 .pre-commit-config.yaml 샘플을 출력할 수 있다. 출력된 내용을 .pre-commit-config.yaml에 붙여넣는다.
$ pre-commit sample-config # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files
 
4가지 hook이 미리 정의되어 있는 것을 볼 수 있다. 각 hook이 하는 역할은 여기를 참고한다.
 
 

3. black을 hook으로 등록하기

 
보통 작성한 코드가 PEP8 규격에 맞는지 flake8을 사용해서 검사하지만 flake8은 검사만 해줄 뿐 코드를 수정해주진 않는다. 그래서 flake8이 출력하는 메세지를 보고 개발자가 일일이 코드를 수정해야 한다.
black은 PEP8 규격에 맞도록 파이썬 코드를 알아서 수정해준다. black이 코드를 수정해서 diff가 발생할 경우 커밋에 실패하도록 .pre-commit-config.yaml 에 black을 hook으로 추가해보자. 코드가 한줄에 120자를 넘길 경우 line break를 하도록 args—line-length=120이라는 옵션을 추가했다.
- repo: https://github.com/psf/black rev: 19.10b0 hooks: - id: black name: formatting python code with black language_version: python3.7 args: [--line-length=120]
 
  • name : hook이 실행될 때 터미널에 출력될 이름
  • language_version : hook을 실행할 때 사용되는 언어 및 버전으로 pre-commit이 language_version에 맞는 가상환경을 생성한다
  • args : hook을 실행할 때 필요한 파라미터
 
.pre-commit-config.yaml에 정의된 hook이 동작하게 하려면 아래의 커맨드를 실행한다.
$ pre-commit install pre-commit installed at .git/hooks/pre-commit
 
등록한 hook이 잘 작동하는지 확인하기 위해 two blank lines가 없는 아래의 app.py를 커밋하려고 한다.
from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "Hello, World!"
 
그럼 아래와 같이 Failed 메세지가 출력될 것이다. git diff app.py를 통해 black이 two blank lines를 추가한 것을 확인 할 수 있다. 이 때는 수정된 코드를 스테이징에 올리고 다시 커밋을 시도하면 된다.
notion image
 
 

4. flake8을 hook으로 등록하기

 
black으로 코드를 수정한 후 수정한 코드가 PEP8에 만족하는지 flake8으로 검사하도록 .pre-commit-config.yaml에 아래 내용을 추가한다.
- repo: https://github.com/PyCQA/flake8 rev: 3.8.2 hooks: - id: flake8 name: Inspect python code with flake8 language_version: python3.7 args: [--ignore=E501, --max-complexity=10]
 
코드가 한줄에 79자를 넘길 경우 flake8은 line too long error를 출력하므로 이를 무시하기 위해 —ignore=E501 옵션을 추가했다. (120자를 넘기면 line break를 하도록 black에서 이미 옵션을 정의했다.) 그리고 —max-complexity=10 옵션을 추가해서 최대 순환복잡도를 10으로 설정했다.
 
새로 추가한 hook을 반영하도록 아래의 커맨드를 실행한다. -f 옵션은 기존에 등록된 hook을 overwrite 한다.
$ pre-commit install -f pre-commit installed at .git/hooks/pre-commit
 
app.py를 추가로 커밋했을 때 아래와 같이 flake8이 동작하는 것을 확인할 수 있다.
notion image
 
 

5. 커밋메세지 앞에 이슈번호를 붙이는 hook 등록하기

 
커밋메세지를 작성할 때는 보통 깃헙 이슈번호를 메세지 앞에 명시해서 해당 커밋이 어떤 이슈와 관련있는지 확인한다. (회사에서는 JIRA 문서 번호를 커밋메세지에 남긴다.) 하지만 커밋을 자주 하다보면 커밋메세지에 번호 붙이는 것을 까먹을 때도 있고 번호에 오타가 있을 수도 있다.
그러니 hook이 알아서 커밋메세지 앞에 이슈번호를 붙이도록 해보자. 보통 feature#2-current-branch와 같이 브랜치를 네이밍할 때도 브랜치 이름 앞에 이슈번호를 붙이므로 최근에 작업중인 브랜치에서 이슈번호를 가져올 것이다.
파이썬으로 아래의 스크립트 prepare_commit_msg.py를 작성한다.
import re import subprocess import sys BRANCHES_TO_SKIP = ["develop", "master", "test-release", "live-release"] ISSUE_NUMBER_REGEX = "[a-z]+#[0-9]+$" def prepare_commit_msg(commit_editmsg_path): current_branch_name = subprocess.check_output("git branch | grep '*'", shell=True).decode().split()[1] if current_branch_name in BRANCHES_TO_SKIP: return issue_number = current_branch_name.split("-")[0] if not re.match(ISSUE_NUMBER_REGEX, issue_number): return subprocess.check_output("echo {}".format(issue_number), shell=True) with open(commit_editmsg_path, mode="r+") as f: msg = f.read() f.seek(0, 0) f.write("{issue_number} {msg}".format(issue_number=issue_number, msg=msg)) if __name__ == "__main__": prepare_commit_msg(sys.argv[1])
 
커밋을 완료하기 전에 hook이 실행되도록 .pre-commit-config.yaml 에 아래 내용을 추가한다. hook으로 사용하려는 소스는 로컬에서 직접 작성한 prepare_commit_msg.py이므로 repolocal로 지정했다.
- repo: local hooks: - id: prepare-commit-msg name: Insert issue number in commit message language: python entry: python prepare_commit_msg.py stages: [prepare-commit-msg]
 
  • entry : hook으로 실행될 명령어
  • stages : 등록할 hook이 반영될 .git/hooks 스크립트 리스트. 위에서는 prepare-commit-msg로 지정했다. 디폴트 값은 [pre-commit]이다.
 
위에서 정의한 hook을 반영하기 위해 아래 커맨드를 실행한다. -t옵션은 .git/hooks에 있는 특정 스크립트에 반영하려고 할 때 사용한다.
$ pre-commit install -t prepare-commit-msg pre-commit installed at .git/hooks/prepare-commit-msg
 
ftr#2-setup-python-linting이라는 브랜치에서 bye world API 추가 라는 메세지로 커밋을 하면 아래와 같이 ftr#2가 커밋메세지 앞에 붙은 것을 확인할 수 있다.
(같은 hook이 2번 실행되는 것을 볼 수 있는데 관련 이슈는 여기를 참고.)
notion image
 
 
앞서 작성한 .pre-commit-config.yamlprepare_commit_msg.py를 리퍼지토리에 올려놓으면 다른 팀원은 리퍼지토리의 코드를 내려받고 pre-commit 패키지를 설치한 후
$ pre-commit install && pre-commit install -t prepare-commit-msg 커맨드를 실행하기만 하면 되므로 매우 편리하다.
 
 
참고 문서