Post

Git Hook을 이용해 코드 포맷팅 체크와 커밋 메시지 검증하기





들어가며

여러 사람과의 버전 관리를 위해 Git을 사용하다 보면 많은 문제가 생기기 마련입니다. 그중에서도 서로 다른 컨벤션, 특히 커밋 메시지 때문에 생기는 문제는 간단하게 해결하기 쉽지만 귀찮은 마음에 넘어가기 쉽습니다. 처음엔 괜찮아도 몇 번의 커밋이 쌓이기 시작하면 너무 먼 길을 왔다는 생각에 이도 저도 아니게 되는 경우도 더러 있는 듯합니다. 본 포스트에서는 Git Hook을 이용해서 커밋 전 코드 포매팅을 체크하고 커밋 메시지를 정해진 양식에 맞춰 작성했는지 자동으로 확인하는 방법을 소개하려고 합니다.

Git Hook은 Git 저장소에서 특정 이벤트가 발생할 때마다 자동으로 실행하는 스크립트를 말합니다. 이를 이용해 많은 귀찮은 작업을 자동으로 수행할 수 있습니다. 여러 Git Hook이 있지만, 이번 포스트에선 커밋 전과 커밋 메시지 작성 시 실행하는 스크립트인 pre-commitcommit-msg를 이용해보겠습니다.

Git Hook은 어디에 있지?

Git Hook은 모든 Git Repository에서 지원합니다. 터미널을 이용해 Git Repository를 들어가서 .git/hooks 폴더를 확인하면 다음과 같은 샘플 파일들이 있습니다.

Git Hook Samples
Git Hook 샘플 파일들

특정 이벤트에 따른 샘플 Hook들이 있고, 확장자인 .sample만 지워주면 바로 사용할 수 있게 됩니다.

코드 포맷팅 체크 (pre-commit)

black

여러 사람이 동일한 컨벤션으로 코드를 작성하기란 쉽지 않습니다. 이럴 대 코드 포매팅 도구를 사용해서 코드 스타일을 통일시키면 굉장히 편하게 여러 사람의 코드에서 일관성을 확보할 수 있습니다. 코드 포매팅 도구 중 최근 많이 쓰이는 것은 black으로 PEP8에 기반을 둔 코드 포매팅 도구입니다. 다음의 명령어를 통해 설치하도록 하겠습니다.

1
$ pip install black

Hook 적용

커밋 전 적용할 Hook의 경우 pre-commit를 설치해 YAML 파일을 통해 간단하게 생성할 수 있습니다. 우선 pip를 이용해 pre-commit을 설치해줍니다.

1
$ pip install pre-commit

올바르게 설치를 했다면 Git Repository 폴더에서 다음의 명렁어를 실행해 환경설정 샘플 YAML 파일을 생성합니다.

1
$ pre-commit sample-config > .pre-commit-config.yaml

안에 내용은 당연히 샘플로 들어가 있기 때문에 black을 사용하도록 수정해야 합니다. 기존 파일을 다음 내용으로 덮어 씌웁니다.

1
2
3
4
5
6
repos:
  - repo: https://github.com/psf/black
    rev: stable
    hooks:
      - id: black
        args: [--line-length=80]

여기까지 됐다면 pre-commit install을 통해 설치합니다. 그리고 git commit을 하면 다음과 같이 적용된 것을 확인할 수 있습니다.

After git commit
pre-commit이 올바르게 적용되었을 때

간혹 다음의 주의 메시지가 출력되는 경우가 있습니다.

1
2
3
4
[WARNING] The 'rev' field of repo 'https://github.com/psf/black' appears to be a mutable reference (moving tag / branch).
Mutable references are never updated after first install and are not supported.
See https://pre-commit.com/#using-the-latest-version-for-a-repository for more details.
Hint: `pre-commit autoupdate` often fixes this.

black 모듈의 버전 정보가 stable로 되어 있을 때 발생하는데, 터미널에서 간단하게 pre-commit autoupdate를 실행해서 버전 정보를 업데이트 해주면 됩니다. 업데이트하게 되면 다음과 같이 수정이 됩니다.

1
2
3
4
5
6
repos:
  - repo: https://github.com/psf/black
    rev: 21.12b0
    hooks:
      - id: black
        args: [--line-length=80]

커밋 메시지 검증 (commit-msg)

좋은 커밋 메시지

좋은 커밋 메시지에 대한 좋은 글들은 이미 많습니다. 저의 모자란 글솜씨 대신 링크로 대신하겠습니다.

커밋 메시지 템플릿

우선 사용할 커밋 메시지 템플릿을 정해야 하는데요. 구글에 검색하면 가장 많이 나오는 형태의 템플릿을 예로 들겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# <type>: <subject>
##### Subject 50 characters ################# -> |


# Body Message
######## Body 72 characters ####################################### -> |

# Issue Tracker Number or URL

# --- COMMIT END ---
# Type can be
#   feat    : new feature
#   fix     : bug fix
#   refactor: refactoring production code
#   style   : formatting, missing semi colons, etc; no code change
#   docs    : changes to documentation
#   test    : adding or refactoring tests
#             no productin code change
#   chore   : updating grunt tasks etc
#             no production code change
# ------------------
# Commit rules:
#   Capitalize the subject line
#   Use the imperative mood in the subject line
#   Do not end the subject line with a period
#   Separate subject from body with a blank line
#   Use the body to explain what and why vs. how
#   Can use multiple lines with "-" for bullet points in body
# ------------------

해당 내용이 담긴 파일을 .gitmessage 라는 이름으로 현재 Repository 안에 있는 .git 폴더 안에 넣어줍니다. 그리고 Git 설정값중 커밋 템플릿을 해당 파일로 설정합니다.

1
$ git config --local commit.template .git/.gitmessage

Hook 적용

우선 어떤 형태로 Hook을 적용했는지 알아보도록 하겠습니다. 제가 사용한 방식은 커밋 메시지를 Python으로 읽어서 여러 커밋 메시지 규칙에 부합하는지 확인하여 오류 여부를 체크하는 형태입니다. 커밋 메시지 규칙을 체크하는 Python 파일을 실행하는 스크립트를 commit-msg에 저장하면 됩니다.

우선 제가 작성한 커밋 메시지 검증 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/python3

import re
import sys

type_list = [
    "feat",
    "fix",
    "refactor",
    "style",
    "docs",
    "test",
    "chore",
    "ci",
    "perf",
]
type_regex = (
    r"^(feat|fix|refactor|style|docs|test|chore|ci|perf)(\(.+\))?\:\s(.{3,})"
)


class bcolors:
    FAIL = "\033[91m"
    ENDC = "\033[0m"


def verify_commit_message():
    with open(sys.argv[1]) as commit:
        lines = commit.readlines()

        # Remove comments
        lines = [line for line in lines if not line.startswith("#")]

        # If the last line is whitespace, remove it
        while lines[-1] == "\n":
            lines = lines[:-1]
            if len(lines) == 0:
                break

        # Empty commit message
        if len(lines) == 0:
            sys.stderr.write(
                f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}Empty commit message.\n"
            )
            sys.exit(1)
        # Subject line should be less than 50 characters.
        if len(lines[0]) > 50:
            sys.stderr.write(
                f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}Subject line should be less than 50 characters.\n"
            )
            sys.exit(1)
        # Subject line should follow the rule.
        if re.match(f"({type_regex})", lines[0]) is None:
            sys.stderr.write(
                f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}The commit message subject line does not follow the rule."
            )
            sys.stderr.write("\n<type>: <subject> is required.\n")
            sys.exit(1)
        # The subject should be a title-case.
        if not lines[0].split(":")[1].strip()[0].istitle():
            sys.stderr.write(
                f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}The subject should be title-cased.\n"
            )
            sys.exit(1)
        # If commit message has single line, description might be missing.
        if len(lines) == 1:
            sys.stderr.write(
                f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}Descriptions are missing.\n"
            )
            sys.exit(1)
        # After subject line, line space is required.
        if lines[1] != "\n":
            sys.stderr.write(
                "\nThe line space after the subject line is required.\n"
            )
            sys.exit(1)
        for line in lines[2:]:
            # Every single description should be less than 72 characters.
            if len(line) > 72:
                sys.stderr.write(
                    "\nEvery single description should be less than 72 characters.\n"
                )
                sys.exit(1)
            # Description starts with "-".
            if not line.startswith("-"):
                sys.stderr.write(line)
                sys.stderr.write(
                    f"\n{bcolors.FAIL} Commit failed: {bcolors.ENDC}Description should start with a dash '-'.\n"
                )
                sys.exit(1)
    sys.exit(0)


if __name__ == "__main__":
    verify_commit_message()

파일명은 verify_commit_msg.py로 해당 파일은 .git/hooks 폴더에 위치합니다.

이제 여기에 commit-msg 스크립트 파일을 아래와 같이 작성해서 동일한 .git/hooks 폴더에 넣어놓으면 됩니다.

1
2
3
4
#!/bin/sh

exec < /dev/tty
./.git/hooks/verify_commit_msg.py $1

여기까지 잘 따라오셨다면 이제 git commit을 통해 잘못된 커밋 메시지를 입력해보셔서 테스트 하시면 됩니다. 아래 이미지는 커밋 제목이 Title-cased가 아니여서 실패한 케이스입니다.

Commit failed
잘못된 커밋 메시지를 입력하였을 때

마치며

이렇게 한 번 잘 설정해놓으면 커밋 메시지의 퀄리티에 대해 신경을 크게 쓰지도 않아 매우 편합니다. 게다가 나중에 다른 Git Repository를 작업하더라도 위에서 생성한 파일들만 그대로 넣어놓으면 바로 사용할 수도 있구요. 사소하다 생각해서 매번 놓치는 부분일 수도 있지만 사소한 것에서 큰 차이가 난다는 말도 있으니까요. 한 번 시도해보시는건 어떨까요? 😃



This post is licensed under CC BY 4.0 by the author.