Post

Git Hook을 이용하여 커밋을 아름답게 관리하기(Husky&commitlint)

들어가며

개발을 위해 주로 사용하는 Git에는 어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행할 수 있는 기능이 있다.
바로 Hook 이라는 기능이다.
훅이 뭔지 알아보고, 커밋의 형식을 강제하고 다듬는 작업을 자동화해보자


Git Hook

훅은 위에서 말했듯이, 어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행할 수 있는 기능을 가진 녀석이다.
주로 활용하는 용도는 검사, 자동화가 대부분이라고 볼 수 있다.
즉, 인력이 필요한 부분을 코드로 치환할 수 있어 아주 편리하다고 볼 수 있다.


Git Hook 종류

Git Hook은 크게 2종류로 나뉜다.
클라이언트 훅은 로컬에서 실행되며 커밋, 푸시 등의 로컬 작업을 제어할 때 사용한다.
서버 훅은 원격 저장소(Git 서버)에서 실행되며 서버에서 적용되기 전에 검증하거나 자동 배포하는 데 사용된다.
보기 쉽게 표로 정리하면 다음과 같다

구분클라이언트 훅서버 훅
실행 위치로컬 저장소원격 저장소 (Git 서버)
주요 용도코드 품질 검증, 메시지 검사, 테스트 실행브랜치 보호, 배포 자동화
예시pre-commit, commit-msg, pre-pushpre-receive, post-receive

전체 총 19개의 종류가 존재하지만, 이 글에서는 클라이언트 훅 중에서 주로 사용되는 것들만 다룬다

git_hook

pre-commit

git commit 실행 직후, 커밋 메시지 입력 전에 실행된다

용도

  • 코드 스타일 검사
  • 테스트 실행
  • 파일 포맷팅(이미지 압축 등)
  • 특정 파일 커밋 방지


prepare-commit-msg

커밋 메시지가 생성되기 직전, 편집기가 열리기 전에 실행된다

용도

  • 커밋 메시지 기본 템플릿 설정
  • 자동으로 브랜치 이름을 메시지에 포함


commit-msg

사용자가 커밋 메시지를 입력한 후, 커밋이 저장되기 전에 실행된다

용도

  • 커밋 메시지 형식 검증
  • 특정 패턴이 포함되었는지 확인


post-commit

커밋이 완료된 직후 실행된다

용도

  • 커밋 후 추가 작업 실행(Slack 알림, 로그 저장 등)
  • 자동 푸시 실행


pre-push

git push 실행 전에 실행된다

용도

  • 원격 저장소로 푸시하기 전 코드 품질 검사
  • 테스트 실행
  • 특정 브랜치에 대한 푸시 제한


커밋 메시지 형식을 강제하고 이모지를 추가하여 가독성 높이기

commit

형식을 강제하는 장치가 없을 때 벌어지는 문제점이 보이는가?
feat라는 커밋 타입을 적으면서 오타가 발생할 수 있고, 정해지지 않은 타입 컨벤션에 의해 의미가 비슷하지만 다른 타입을 써 헷갈리게 만들 수 있다.
우리는 Git Hook을 이용하여 문제를 예방하고 커밋 타입에 따라 이모지도 붙여서 커밋을 한눈에 볼 수 있다. 바로 아래 처럼 말이다

commit2


Husky

Git Hook을 쉽게 관리하고 팀원과 공유할 수 있도록 도와주는 도구다
기존에는 .git/hooks/에 훅 파일들이 있어야 적용이 되고, .git은 로컬로만 관리되기 때문에 훅 설정 공유가 어려웠다.
Husky는 .husky에서 훅파일들을 관리하며, npm install을 통해 쉽게 훅 설정을 할 수 있다.


설치

1
2
3
4
# npm을 기준으로 진행한다.

npm install husky --save-dev # package.json이 설정된다.
npx husky install # .husky 폴더가 생성되고, 훅 경로가 설정된다.


commitlint

커밋 메시지의 형식을 검증하는 도구다
json 파일로 메시지의 형식을 정의하면, commitlint 규칙에 따라 커밋 메시지를 검증하게 된다.


설치

1
npm install --dev @commitlint/config-conventional @commitlint/cli


적용

이제 필요한 도구들도 간단하게 알아봤으니, 실제로 적용해보겠다.
훅의 종류 중 prepare-commit-msgcommit-msg를 사용할 것이다.

사용했던 세팅을 공유한다. 필요에 따라 변경해서 사용하면 되겠다.

커밋 흐름

1
2
3
4
5
6
7
커밋 요청
  ↓
검사 전 커밋 메시지 변환(prepare-commit-msg)
  ↓
commitlint 도구를 사용하여 커밋 형식 검사(commit-msg)
  ↓
커밋 완료

디렉토리 구조

1
2
3
4
5
6
7
project-root
├── .husky
│   ├── commit-msg
│   ├── husky.sh
│   └── prepare-commit-msg
├── .commitlintrc.json
└── package.json


husky.sh

이 파일에 스크립트를 추가하여 훅들을 더 깔끔하게 관리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env sh
if [ -z "$husky_skip_init" ]; then
  readonly hook_name="$(basename -- "$0")"
  echo "🚀 Husky(Start) - 시작된 훅 이름 : $hook_name"

  readonly husky_skip_init=1
  export husky_skip_init

  sh "$0" "$@"
  exitCode="$?"

  if [ $exitCode -ne 0 ]; then
    echo "❌ Husky(Error), 이름:{$hook_name} 에러코드:{$exitCode}"
  fi

  echo "✅ Husky(Success) - 완료된 훅 이름 : $hook_name"
  exit $exitCode
fi


prepare-commit-msg

커밋에 이모지를 넣는 작업을 수동으로 할 순 없다
bash 파일로 스크립트를 작성해서 정해진 규칙에 따라 꾸며지도록 설정해보자

prepare-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
  #!/usr/bin/env bash
  `. "$(dirname -- "$0")/husky.sh"

  # 이모지 정의
  emojies=(
    "Feat ✨"
    "Fix 🐛"
    "Style ⭐️"
    "Refactor ♻️"
    "File 📁"
    "Test ✅"
    "Docs 📝"
    "Remove 🔥"
    "Ci 💚"
    "Release 🔖"
    "Chore 🔧"
  )

  COMMIT_MESSAGE_FILE_PATH=$1
  first_line=$(head -n1 "$COMMIT_MESSAGE_FILE_PATH")
  remaining_lines=$(tail -n +2 "$COMMIT_MESSAGE_FILE_PATH")

  # 변환 제외
  if [[ $first_line =~ ^(Merge|Revert|Amend|Reset|Rebase|Tag) ]]; then
    exit 0
  fi

  type=$(echo "$first_line" | grep -o "^[A-Za-z]*")
  start_case_type="$(echo "$type" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')"

  # 맞는 이미지를 찾아 헤더 수정
  for entry in "${emojies[@]}"; do
    key="${entry%% *}"
    value="${entry#* }"
    if [[ "$start_case_type" == "$key" ]]; then
      emoji=$value
      first_line=$(echo "$first_line" | sed "s/^$type/$emoji $start_case_type/")
      break
    fi
  done

  branch_name=$(git rev-parse --abbrev-ref HEAD)
  issue_number=$(echo "$branch_name" | sed -n 's/issue\([0-9]\{1,\}\)-.*/\1/p')

  # 합치기
  echo "$first_line" > "$COMMIT_MESSAGE_FILE_PATH"
  echo "$remaining_lines" >> "$COMMIT_MESSAGE_FILE_PATH"

  # 브랜치 이름에서 이슈번호를 뽑아 footer에 추가, 이슈와 연결됨
  last_line=$(tail -n1 "$COMMIT_MESSAGE_FILE_PATH")
  if [[ -n "$issue_number" ]]; then
      if ! grep -q "#$issue_number" "$COMMIT_MESSAGE_FILE_PATH"; then
        echo "" >> "$COMMIT_MESSAGE_FILE_PATH"
        echo "#$issue_number" >> "$COMMIT_MESSAGE_FILE_PATH"
      fi
  fi`


commit-msg

변환 작업이 끝났으면 규칙에 만족하는지 검사해야 한다.
commitlint 도구를 사용하기 때문에 호출하는 코드만 넣어준다.

1
2
3
4
#!/usr/bin/env sh
. "$(dirname "$0")/husky.sh"

npx --no-install commitlint --edit "$1"


.commitlintrc.json

실제로 형식 검사에 사용되는 규칙이다.
입맛대로 규칙이나 이모지 등을 바꿔서 사용하자

.commitlintrc.json
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
  {
    "extends": ["@commitlint/config-conventional"],
    "parserPreset": {
      "parserOpts": {
        "headerPattern": "^(?<type>.+?)(?:\\((?<scope>.+)\\))?:\\s(?<subject>.+)$",
        "headerCorrespondence": ["type", "scope", "subject"]
      }
    },
    "rules": {
      "subject-empty": [2, "never"],
      "subject-full-stop": [2, "never", "."],
      "subject-case": [2, "never", []],
      "subject-empty": [2, "never"],

      "scope-case": [2, "never", []],

      "body-leading-blank": [1, "always"],
      "body-max-line-length": [2, "always", 100],
      "footer-leading-blank": [1, "always"],
      "footer-max-line-length": [2, "always", 100],

      "type-case": [0],
      "type-empty": [2, "never"],
      "type-enum": [
        2,
        "always",
        [
          "✨ Feat",
          "🐛 Fix",
          "⭐️ Style",
          "♻️ Refactor",
          "📁 File",
          "✅ Test",
          "📝 Docs",
          "🔥 Remove",
          "💚 Ci",
          "🔖 Release",
          "🔧 Chore"
        ]
      ]
    }
  }
This post is licensed under CC BY 4.0 by the author.