git branch 이름과 hook으로 commit message 컨벤션 강제하기

git branch 이름과 hook으로 commit message 컨벤션 강제하기
Photo by Yancy Min / Unsplash

Jira와 Conventional Commits

Jira로 티켓 관리를 하면서 Conventional Commits 형태로 커밋 메시지를 작성할 때 브랜치 이름을 잘 정하면 git hook으로 커밋 메시지 컨벤션을 강제할 수 있습니다.

Convetional Commits를 사용하게 되면 커밋 메시지 앞부분에 type: 형태의 접두사가 붙게 됩니다. 여기에 티켓 이름까지 넣어서 type: [KICK-611] 같은 형태로 사용하면 접두사가 너무 길어집니다.

커밋 메시지의 첫 번째 줄은 제목 역할을 하는 중요한 공간이기 때문에 저는 아래처럼 티켓 넘버를 커밋 메시지의 마지막에 추가하는 방식을 선호합니다.

chore: add git hooks to force commit convention

[KICK-611]

하지만 깜빡 잊고 타입이나 티켓 넘버를 빼먹는 경우가 부지기수입니다... 그래서 브랜치 이름을 type/TICKET_NUMBER 형태로 짓고 git hook에서 브랜치 이름을 파싱해서 사용해서 타입과 티켓 넘버를 자동으로 입력하도록 해봤습니다.

예를 들어 KICK-611 티켓에서 에서 chore 타입의 변경사항을 처리해야 한다면 브랜치 이름은 chore/KICK-611이 됩니다. prefix를 추가할 때는 prepare-commit-msg hook을, potsfix를 추가할 때는 commit-msg hook을 사용합니다.

브랜치 이름에서 원하는 정보 추출하기

grep을 사용해서 현재 브랜치 이름을 골라내고, sed를 사용해서 필요 없는 문자열을 제거합니다.

브랜치 이름에서 타입과 티켓 넘버 가져오기

git branch를 입력하면 현재 브랜치에 *표가 붙어있음을 알 수 있습니다.

$ git branch
* chore/KICK-611
  develop

grep으로 *가 있는 행을 골라냅니다

$ git branch | grep '\*'
* chore/KICK-611

sed로 맨 앞의 * 를 제거합니다.

$ git branch | grep '\*' | sed 's/\* //'
chore/KICK-611

첫 번째 / 이전의 문자열만 남길 수 있다면 타입 문자열을 얻게됩니다. 정규표현식으로 /문자를 제외한 모든 문자열로 그룹을 만들어서 이 그룹 이외의 문자열을 모두 지우면 됩니다.

$ git branch | grep '\*' | sed 's/\* //' | sed 's/\([^/]*\).*/\1/'
chore

티켓 번호를 가져오려면 마지막 / 이후의 문자열만 남기면 됩니다.

$ git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///'
KICK-611

가끔 하나의 티켓에 여러 개의 브랜치를 만드는 경우가 있습니다. 생각보다 변경사항이 많아져서 여러 개의 Pull Request를 보내는 경우가 종종 있는데, 이럴 때는 티켓 이름에 -숫자를 추가하는 형태로 브랜치 이름을 만듭니다(e.g. chore/KICK-611-1). 이런 경우에 마지막 / 문자열만 남기면 티켓 번호가 제대로 나오지 않기 때문에 첫 번째 - 앞 뒤의 단어만 남기도록 sed 커맨드를 추가합니다.

$ echo 'chore/KICK-611-1' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/'
KICK-611

prefix는 prepare-commit-msg hook, postfix는 commit-msg hook

.git/hooks 디렉토리에 prepare-commit-msg 파일을 생성하면 사용자가 커밋 메시지를 입력하기 전 단계에서 내용을 변경할 수 있습니다. commit-msg 파일을 생성하면 사용자가 커밋 메시지 입력을 완료한 이후에 내용을 변경할 수 있습니다. 자세한 내용은 Customizing Git - Git Hooks를 참고하세요.

./git/hooks/prepare-commit-msg

#!/bin/sh  
# .git/hooks/prepare-commit-msg  
#  
# Automatically add branch name and branch description to every commit message except merge commit.  
# https://stackoverflow.com/a/18739064  
#  
  
COMMIT_MESSAGE_FILE_PATH=$1  
  
# merge commit에 대해서는 prefix를 생성하지 않는다.  
MERGE=$(grep -c -i 'merge' < "$COMMIT_MESSAGE_FILE_PATH")  
if [ "$MERGE" != "0" ] ; then  
  exit 0  
fi  
  
TYPE=$(git branch | grep '\*' | sed 's/\* //' | sed 's/\([^/]*\).*/\1/')  
DESCRIPTION=$(git config branch."$TYPE".description)  
echo "$TYPE: $(cat "$COMMIT_MESSAGE_FILE_PATH")" > "$COMMIT_MESSAGE_FILE_PATH"  
if [ -n "$DESCRIPTION" ]  
then  
   echo "" >> "$COMMIT_MESSAGE_FILE_PATH"  
   echo "$DESCRIPTION" >> "$COMMIT_MESSAGE_FILE_PATH"  
fi

이전에 작성했던 [Git] Commit 메세지에 자동으로 issue number 추가하기 글에서 스크립트를 가져와 조금 변형했습니다. chore/KICK-611 브랜치에서 git commit을 하면 아래와 같이 chore: 가 입력된 상태로 커밋 메시지가 준비됩니다.

chore: 
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch chore/KICK-611
# ...

postfixprepare-commit-msg hook에서 추가하면 안될까요? 명령줄에서만 git commit을 하면 괜찮은데, IntelliJ, WebStrom, GitKraken등의 도구를 사용해서 커밋을 하면 prepare-commit-msg에서 추가한 문자열들이 무조건 커밋 메시지 앞부분에 붙어버려서 곤란합니다.

commit-msg hook을 사용하면 사용자가 커밋 메시지 입력을 완료한 이후에 메시지를 변경할 수 있습니다.

./git/hooks/commit-msg

#!/bin/bash  
  
COMMIT_MESSAGE_FILE_PATH=$1  
MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")  
  
# 커밋 메시지가 있을 때만 티켓 넘버를 추가한다.  
# 커밋 메시지가 비이었으면 'Aborting commit due to empty commit message.' 와 함께 커밋이 실패해야 하는데  
# 이 상황에서 티켓 넘버를 메시지에 추가해버리면 커밋이 성공해버린다. 이를 방지하기 위해 커밋 메시지가 있을 때만 티켓 넘버를 추가한다.  
if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then  
  exit 0  
fi  
  
# 브랜치 이름에서 마지막 '/' 이후의 문자열만 남긴다. '/'가 없다면 브랜치 전체 이름이 POSTFIX 된다.  
# POSTFIX의 첫 번째 '-' 앞뒤의 문자열만 포함한다. '-'가 없다면 변경은 없다  
# e.g.)  
# | branch name      | postfix    |  
# |------------------|------------|  
# | chore/KICK-611   | [KICK-611] |  
# | chore/KICK-611-1 | [KICK-611] |  
# | KICK-611         | [KICK-611] |  
# | NODASH           | [NODASH]   |  
POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')  
  
printf "%s\n\n[%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"

아래에서 커밋 메시지에 chore: 가 prefix로 붙고 커밋을 완료한 이후에 로그를 확인하면 [KICK-611]가 postfix로 붙는 것을 확인할 수 있습니다.

IDE나 GitKraken에서 커밋을 할 때는 prefix, postfix 없이 메시지만 적고 커밋을 하면 됩니다.

git으로 hook 관리하기

.git/hooks 디렉토리에 정해진 이름의 파일을 넣으면 hook을 사용할 수 있지만, .git 디렉토리 내부는 형상관리를 할 수 없기 때문에 git clone을 할 때마다 매번 hook을 다시 설정해야 합니다.

.githooks 디렉토리에 hook파일을 넣고 .githooks 디렉토리를 hook 디렉토리로 사용하게 설정하면 형상관리를 할 수 있습니다.

git config core.hooksPath .githooks

맥과 리눅스에 웬만하면 기본으로 설치되어있는 Makefile을 활용해서 위 설정을 입력하도록 만듭니다. ./Makefile 파일에 아래 내용을 추가하고 make를 입력합니다.

# ./Makefile
init:  
   git config core.hooksPath .githooks
$ make
git config core.hooksPath .githooks 

입력 한 번으로 hook 설정하기

위 내용을 기존의 저장소에 한 번에 적용할 수 있는 스크립트를 만들어봤습니다. 위 hook을 적용하고 싶은 디렉토리에서 아래 스크립트를 붙여넣으면 됩니다.

curl -L https://github.com/myeongjae-kim/git-conventions-by-hooks/archive/main.tar.gz | tar -xzv \
  && rsync -axvP git-conventions-by-hooks-main/ ./ \
  && rm -rf git-conventions-by-hooks-main \
  && bash setup.sh

https://github.com/myeongjae-kim/git-conventions-by-hooks 저장소의 내용을 현재 디렉토리로 가져온 뒤 hook을 설치하고 README.md 에 hook 관련 내용을 추가합니다.

(원문보기)

-->