git branch 이름과 hook으로 commit message 컨벤션 강제하기
2022 / 04 / 30
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
.git/hooks
디렉토리에 prepare-commit-msg
파일을 생성하면 사용자가 커밋 메시지를 입력하기 전 단계에서 내용을 변경할 수 있습니다. commit-msg
파일을 생성하면 사용자가 커밋 메시지 입력을 완료한 이후에 내용을 변경할 수 있습니다. 자세한 내용은 Customizing Git - Git Hooks를 참고하세요.
#!/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
# ...
postfix
도 prepare-commit-msg
hook에서 추가하면 안될까요? 명령줄에서만 git commit
을 하면 괜찮은데, IntelliJ, WebStrom, GitKraken등의 도구를 사용해서 커밋을 하면 prepare-commit-msg
에서 추가한 문자열들이 무조건 커밋 메시지 앞부분에 붙어버려서 곤란합니다.
commit-msg
hook을 사용하면 사용자가 커밋 메시지 입력을 완료한 이후에 메시지를 변경할 수 있습니다.
#!/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/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을 적용하고 싶은 디렉토리에서 아래 스크립트를 붙여넣으면 됩니다.
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 관련 내용을 추가합니다.