Git hook (Git을 더 똑똑하게 쓰는 방법)
git commit 메시지 관리 및 테스트 수행
개요
Git을 쓰다 보면 이런 생각이 들 때가 있습니다. "커밋 전에 코드 포맷터를 자동으로 돌릴 수 없을까?"
, "push 전에 테스트를 자동으로 돌리고 싶다."
이럴 때 Git Hook을 활용하면 됩니다. 특히 프리랜서나 여러 조직의 사람이 급하게 모인 프로젝트에서 Git Hook을 사용하여 프로젝트 컨벤션과 코드 품질향상을 이룰 수 있었습니다. 이런 Git Hook에 대해서 설명합니다.
Git Hook
Git Hook은 Git의 특정 행동 시점(before/after) 에 실행되는 스크립트입니다.
예를 들어 commit
, push
, merge
같은 작업이 발생할 때 미리 지정한 동작을 자동으로 실행해줍니다. 쉽게 말해, Git의 자동화 트리거라고 볼 수 있습니다.
Hook 종류
git hook은 크게 실행되는 위치에 따라 client-side hooks, server-side hooks로 나뉩니다.
위의 그림과 같이 각 Hook은 client-side hooks, server-side hooks에서 차례대로 수행됩니다.
client-side hooks
로컬 개발 환경에서 실행되며, 커밋, 병합, 푸시 등의 작업에 연관된 이벤트에서 트리거됩니다.
pre-commit
: 커밋을 실행하기 전에 호출되며, 커밋될 코드를 검토하고 유효성 검사를 할 수 있습니다. 예를 들어, 코드 스타일 검사를 수행하거나, 테스팅을 할 수 있습니다.prepare-commit-msg
: 커밋 메시지를 작성하기 전에 호출되며, 커밋 메시지를 미리 작성하거나 수정하는 데 사용할 수 있습니다. 예를 들어, 자동으로 이슈 번호를 추가할 때 유용합니다.commit-msg
: 커밋 메시지를 작성한 후에 호출되며, 메시지의 형식을 검증하거나 규칙을 강제할 때 유용합니다.post-commit
: 커밋이 완료된 후에 호출되며, 커밋 후 후속 작업을 할 수 있습니다. 예를 들어, 알림을 보낼 때 사용할 수 있습니다.pre-push
: 원격 저장소로 푸시하기 전에 호출되며, 푸시 전에 테스트를 실행하거나 코드를 검토할 수 있습니다.pre-rebase
:rebase
명령을 실행하기 전에 호출되며, 재정렬 전에 할 작업이 있을 때 사용합니다.post-checkout
: 새로운 브랜치로 체크아웃하거나 특정 커밋으로 이동한 후에 호출됩니다. 특정 브랜치 전환 시 필요한 작업을 처리할 수 있습니다.post-merge
: 병합이 완료된 후에 호출되며, 병합 후 충돌 해결이나 후속 처리를 자동화할 때 유용합니다.
server-side hooks
Git 서버에서 실행되며, 주로 서버 측에서 푸시된 변경 사항을 검증하거나 후속 작업을 처리하는 데 사용됩니다.
pre-receive
: 원격 저장소에 푸시되기 전에 실행되며, 수신된 모든 커밋을 검토하여 허용할지 여부를 결정합니다. 예를 들어, 특정 브랜치로 푸시가 금지된 경우 이를 막을 수 있습니다.update
:pre-receive
와 비슷하지만, 각 브랜치에 대해 한 번씩 호출됩니다. 브랜치별 검사를 할 때 유용합니다.post-receive
: 원격 저장소에 푸시가 완료된 후 실행되며, 후속 작업을 처리할 수 있습니다. 예를 들어, CI/CD 시스템을 트리거하거나 알림을 보낼 때 사용할 수 있습니다.post-update
: 한 개 이상의 브랜치가 업데이트된 후에 실행되며, 후속 작업을 위한 훅입니다.
Git Hook 사용법
.git/hooks
폴더 확인Git 저장소 안에는 기본적으로
hooks
폴더가 있습니다.1
ls .git/hooks
- Hook 스크립트 작성
- pre-commit 파일을 .git/hooks/에 저장하고 실행 권한을 부여합니다. (아래의 예제코드는 포맷팅을 실행합니다.)
1 2 3
#!/bin/sh echo "코드 포맷팅 중..." npx prettier --write .
- pre-commit 파일을 .git/hooks/에 저장하고 실행 권한을 부여합니다. (아래의 예제코드는 포맷팅을 실행합니다.)
- 커밋 시 자동 실행 확인
- 이제 git commit을 실행하면, 자동으로 포맷터가 돌고 그 후에 커밋이 진행됩니다.
실제로 적용해보기(gradle)
java 프로젝트기준의 예시입니다. 예시에서는 pre-commit, commit-msg만 다뤄서 설명합니다.
또한 프로젝트 clone 후 컴파일 또는 빌드 과정을 통해 자동으로 client-side hooks
을 등록하는 과정을 설명합니다.
git hooks 생성
프로젝트 루트 경로에 hooks 폴더를 생성하고 commit-msg, pre-commit 파일을 생성합니다.
1 2 3
mkdir hooks touch commit-msg touch pre-commit
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
#!/bin/bash # Define valid prefixes VALID_PREFIXES="^(feat|fix|design|hotfix|style|refactor|comment|docs|test|chore|rename|remove):" # Get the commit message file COMMIT_MSG_FILE="$1" # Read the commit message COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") # Check if the commit message is a merge commit if echo "$COMMIT_MSG" | grep -q '^Merge'; then exit 0 fi # Check if the commit message starts with a valid prefix if ! echo "$COMMIT_MSG" | grep -qE "$VALID_PREFIXES"; then echo "Error: 커밋컨벤션을 지켜주세요!!아래 규칙을 따라 커밋을 해야합니다." echo "====================================================" echo " feat : 새로운 기능을 추가" echo " fix : 버그 수정" echo " design : CSS 등 사용자 UI 디자인 변경" echo " hotfix : 급하게 치명적인 버그를 고쳐야하는 경우" echo " style : 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우" echo " refactor : 프로덕션 코드 리팩토링" echo " comment : 필요한 주석 추가 및 변경" echo " docs : 문서 수정" echo " test : 테스트 코드, 리펙토링 테스트 코드 추가, Production Code(실제로 사용하는 코드) 변경 없음" echo " chore : 빌드 업무 수정, 패키지 매니저 수정, 패키지 관리자 구성 등 업데이트, Production Code 변경 없음" echo " rename : 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우" echo " remove : 파일을 삭제하는 작업만 수행한 경우" exit 1 fi exit 0
pre-commit 파일을 아래와 같이 생성합니다.
이 예시에서는 maven or gradle로 test를 진행하게 설정합니다. ( 추가로 lint등과 같은 작업도 가능합니다.)
maven
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/bin/bash if [ ! -f "./mvnw" ]; then echo "Maven Wrapper (mvnw) not found. Please ensure it is present in the project root." exit 1 fi ./mvnw test # 테스트 진행 if [ $? -ne 0 ]; then echo "Tests failed. Commit aborted." exit 1 fi
Gradle
1 2 3 4 5 6 7 8 9 10 11 12
#!/bin/bash if [ ! -f "./gradlew" ]; then echo "gradle Wrapper (gradlew) not found. Please ensure it is present in the project root." exit 1 fi ./gradlew test # 테스트 진행 if [ $? -ne 0 ]; then echo "Tests failed. Commit aborted." exit 1 fi
maven-antrun-plugin를 활용하여 git hooks를 등록합니다.
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
<build> <finalName>portal-pubc</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>install-git-hooks</id> <phase>initialize</phase> <configuration> <target> <copy todir="${basedir}/.git/hooks"> <fileset dir="${basedir}/hooks"/> </copy> <chmod perm="755"> <fileset dir="${basedir}/.git/hooks"> <include name="*"/> </fileset> </chmod> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
위의 과정에서 만들었던 hooks들을 gradle task로 등록합니다.
build.gradle
1 2 3 4 5 6 7 8 9 10 11
// git hook 등록 tasks.register('installLocalGitHook', Copy) { from "${rootProject.rootDir}/hooks" into "${rootProject.rootDir}/.git/hooks" eachFile { mode = 0755 } } build.dependsOn installLocalGitHook
build.gradle.kts
1 2 3 4 5 6 7 8 9 10 11 12
val installLocalGitHook = tasks.register<Copy>("installLocalGitHook") { from("${rootProject.rootDir}/hooks") into(File("${rootProject.rootDir}/.git/hooks")) eachFile { mode = "755".toInt(radix = 8) } } tasks.build { dependsOn(installLocalGitHook) }
Git Template 활용
프로젝트 루트 경로에 git_template 폴더를 만들고, 실제 .git 폴더 구조처럼 hooks 폴더를 생성한 후 내부에 git hook 스크립트 파일을 추가합니다.
1
2
3
4
5
/main/git_templates
└─ /hooks
└─ pre-push
└─ pre-commit
└─ etc...
이후 프로젝트를 clone 할 때 --template
옵션에 사전에 생성한 template 디렉토리를 경로로 지정하여 클론을 받으면 template 옵션에 적힌 디렉토리를 기반으로 .git 디렉토리를 초기화하여 자동으로 Git hook 스크립트가 설정됩니다.
1
git clone --template=/main/git_templates <git-repo-url>
Git hook을 지원 라이브러리
husky
: git 저장소에서 사용되는 Git hooks를 관리하고 실행하는 도구입니다. (링크)
정리
Git Hook은 귀찮은 반복 작업을 자동화하고, 팀원들의 실수를 줄일 수 있는 강력한 도구입니다.
특히 개발 팀이 커질수록 일관성과 자동화는 더욱 중요해지기 때문에, Git Hook은 필수적인 도구라고 볼 수 있습니다.
[출처]
- https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks