여러 사람이 참여하는 프로젝트에서 코딩 컨벤션을 만들고 하나의 일관된 코딩 스타일에 따라서 코드가 작성되는 것은 중요하다고 생각.
여러 명의 개발자가 각자 본인의 스타일에 맞춰서 코딩을 한다면 소스는 한눈에 알아보기 힘들 것.
개발자가 일일이 신경을 써서 하기에는 리소스 낭비라고 생각되고 이러한 과정을 도와주는 도구들이 있음.
eslint, prettier를 직접 설정해서 사용하거나 gts를 사용하거나 프로젝트 코드 스타일을 강제하는 도구는 여러 가지가 있음. 하지만 이 도구를 사용하는 개발자들이 실수로 적용을 하지 못한 채 Git에 올리는 경우도 있음.
Git에서는 hook을 지원하는데 프로그래밍에서 hook이란 특정 이벤트 또는 함수가 호출되기 전, 후에 호출이 되는 코드를 말함.
클라이언트 훅
과 서버 훅
으로 나눌 수 있음.
클라이언트 훅
은 커밋이나 Merge 할 때 실행되고
서버 훅
은 Push 할 때 서버에서 실행.
만약 Git Repository 서버를 관리할 수 있는 권한이 있다면 서버 훅 을 활용하는 게 더 유용할 수 있음. Git Repository 서버에 있는 모든 프로젝트에 대해 push 정책을 설정할 수 있기 때문.
git hook들을 확인하고 싶다면 git과 연동된 다른 프로젝트 폴더에 들어가서 cd .git/hooks/를 하면 됨.
Mac 환경에서는 ll, Windows 환경에서는 ls를 하면 .sample 확장자의 여러 파일들이 나오는데 이 파일들이 Git에서 지원하는 hook.
클라이언트 훅
은 커밋 워크플로 훅
, 이메일 워크플로 훅
, 그리고 기타 훅
으로 분류할 수 있음.
커밋 워크플로 훅
은 git commit
명령으로 커밋을 할 때 실행하는 훅이고 이메일 워크플로 훅
은 git am
명령으로 이메일을 통해 patch 파일을 적용할 때 실행하는 훅. 기타 훅
은 Rebase, Merge, Push 와 같은 이벤트를 실행할 때 실행하는 훅을 포함.
분류에 따른 훅은 아래 표와 같음.
Git Hooks 는 .git/hooks 디렉토리 안에 저장.
hook 은 실행가능한 스크립트이며, 설정하고자 하는 훅 이름을 확장자 없이 파일명으로 지정하면 Git Hooks 를 적용할 수 있음.
예를 들어 pre-commit 훅을 적용하여 commit 직전에 Hello Gabia! 를 출력하고 싶다면 다음과 같이 파일을 생성하면 됨.
.git/hooks/pre-commit
#!/bin/sh
echo 'Hello Gabia!'
exit 0 # Exit 코드가 0 이 아니면 커밋이 취소됨
위 파일을 저장하고 프로젝트에서 커밋을 실행. 아래 그림과 같이 Hello Gabia! 를 출력하는 것을 확인할 수 있음.
실제로 유용한 훅을 적용. 원본 프로젝트의 Master 로 직접 Push 하려고 하면 push 를 중단시키는 훅.
push 를 실행하는 경우 동작해야 하므로, pre-push 훅을 사용하면 됨. pre-push 훅은 리모트 이름과 주소를 파라미터로 전달받으며 stdin 을 통해 업데이트할 해시 리스트를 전달받음. 우리는 리모트 주소와 브랜치명을 조사하여 push 를 통과시키거나 중단시키면 됨.
스크립트는 다음과 같음.
.git/hooks/pre-push
#!/bin/sh
FORBIDDEN_HTTPS_URL="https://github.com/gabia/git-hooks-study.git" # insert your remote url (https)
FORBIDDEN_SSH_URL="git@github.com:gabia/git-hooks-study.git" # insert your remote url (ssh)
FORBIDDEN_REF="refs/heads/master" # insert branch ref
remote="$1"
url="$2"
if [ "$url" != "$FORBIDDEN_HTTPS_URL" -a "$url" != "$FORBIDDEN_SSH_URL" ]
then
exit 0 # Forked Project 에서는 제한하지 않음
fi
if read local_ref local_sha remote_ref remote_sha
then
if [ "$remote_ref" == "$FORBIDDEN_REF" ]
then
echo "DO NOT PUSH it master"
exit 1 # 금지된 ref 로 push 를 실행하면 에러
fi
fi
exit 0
훅을 적용한 후 원본 프로젝트의 master 로 push 를 시도하면 다음과 같이 에러 메세지와 함께 push 가 실패하는 것을 확인할 수 있음.
Git Hooks 는 .git
디렉토리에 저장한다. 그런데 .git
디렉토리는 버전 관리 대상이 아니므로 Repositories 에 올라가지 않음. 기본적인 Git 체계 하에서는 Git Hooks 를 공유할 수 없다는 뜻.
Git Hooks 를 공유하는 효과적인 방법에는 무엇이 있을까? 다양한 방법이 있겠지만 주로 다음과 같은 방법을 사용함.
방법1. Git Hooks 를 설정하는 스크립트 공유 는 Git Hooks 를 공유하는 방법 중 가장 단순한 접근이다. Git Hooks 를 별도 디렉토리에 넣어 버전 관리를 하고, 이 훅을 .git/hooks 로 복사하는 스크립트를 함께 공유하는 방식이다.
우선 프로젝트 내에 githooks 라는 디렉토리를 만들고 다음과 같이 hooks 를 넣어둔다.
./githooks/
└─ pre-commit
└─ pre-push
그리고 다음 스크립트를 함께 공유한다.
./setup_hooks.sh
#!/bin/sh
cp githoooks/* .git/hooks
이 스크립트는 단순히 githooks 디렉토리에 있는 모든 파일을 .git/hooks 디렉토리로 복사 할 뿐.
이 방법을 사용하면 프로젝트별로 hooks 를 관리하고 공유할 수 있다. 하지만 작업자는 git clone 후에 반드시 ./setup_hooks.sh 를 실행해야 함. 만일 실수로 (혹은 고의로) 설정 스크립트를 실행하지 않으면 훅을 적용할 수 없음.
방법2: Git Template 을 활용 은 git clone
시 --template
옵션을 통해 .git
디렉토리를 초기화할 수 있다는 점을 활용.
우선 프로젝트와 독립된 경로에 다음과 같이 Template 디렉토리를 구성하고 Git Hooks 를 넣어둠.
/home/uzulove/git_templates/
└─ hooks/
└─ pre-commit
└─ pre-push
그리고 프로젝트를 clone 할 때 --template
옵션에 위에서 생성한 Template 디렉토리를 경로로 지정.
$ git clone --template=/home/uzulove/git_templates https://github.com/gabia/git-hooks-study.git
이제 .git/hooks
디렉토리를 확인해보면 Git Hooks 가 정상적으로 설정돼 있는 걸 확인할 수 있음.
.git/hooks/
└─ pre-commit
└─ pre-push
이 방법은 Git Hooks 를 별도 경로 (또는 Repository) 에서 관리하고자 할 때 유용함. 하지만 Template 을 미리 공유해야 하며, --template
옵션을 빠뜨리는 경우 훅을 적용할 수 없다는 문제가 있음.
앞서 설명한 두 가지 방법은 유용하지만 치명적인 문제가 있음. 작업자가 실수로든 고의로든 Git Hooks 을 적용하지 않을 가능성이 크다는 것.
또한 팀에서만 공유하는 프로젝트가 아니라 불특정다수에게 공개한 프로젝트라면 작업자가 Git Hooks 를 적용했는지 관리 감독하기가 더욱 어려워짐.
Git Hooks 을 반드시 적용하게끔 강제할 수는 없을까? 만약 프로젝트가 모듈 의존성을 관리하기 위해 npm 을 사용하고 있다면 husky 은 좋은 선택이 될 수 있음.
husky 는 Git Hooks 를 보다 쉽게 적용할 수 있는 npm 모듈이다. 심지어 Git Hooks 에 대해 자세히 알지 못하더라도 commit, push 정책을 관리하고 공유할 수 있음.
husky 를 사용하여 commit 실행 전 Hello Gabia, woof! 를 출력하는 예제를 만들어봄.
우선 husky 를 설치.
$ npm install --save-dev husky
다음으로 commit 정책을 정의. .huskyrc 파일에 정의하고자 하는 훅과 실행할 명령어를 지정하면 됨.
정책은 package.json 에 정의하여도 무방하다. .huskyrc 에 정의하는 건 순전히 별도 파일로 관리하고 싶어서이다.
.huskyrc
{
"hooks": {
"pre-commit": "echo 'Hello Gabia, woof!'"
}
}
이제 commit 을 실행. 아래 그림과 같이 ‘Hello Gabia!, woof’ 를 출력하는 것을 확인할 수 있음.
이 방법을 통해 사용자는 프로젝트별로 commit, push 등 정책을 관리하고 공유할 수 있음.
또한 작업자가 의존 모듈을 설치하는 것만으로 husky 가 적용됨. 더 이상 설치 스크립트를 실행하지 않거나 옵션을 빠뜨려서 Git Hooks 를 적용하지 못하는 상황은 발생하지 않음.
단지 모듈을 설치하고 정책을 정의했을 뿐인데 Git Hooks 처럼 동작하고 있다. 어떻게 이런 일이 가능할까? husky 구조를 살펴봄.
설치한 husky 모듈의 package.json 를 확인해보면 install 스크립트를 정의하고 있음을 알 수 있음.
./node_modules/husky/package.json
{
...,
"scripts": {
...,
"install": "node husky install",
...,
}
}
install 스크립트는 npm 스크립트 중 하나로서 npm install 명령을 통해 해당 모듈을 설치하면 자동으로 실행하는 스크립트이다. husky 를 설치하면 install 스크립트인 node husky install 를 실행.
node husky install 를 따라가보면 최종적으로는 자체적으로 구현해놓은 Git Hooks 를 .git/hooks 디렉토리에 씀.
실제로 husky 를 설치한 뒤 .git/hooks 디렉토리 안를 살펴보면 hooks 가 설정되어 있음. 각 hooks 는 husky.sh 를 실행하며, husky.sh 는 package.json, .huskyrc 등에 정의한 훅을 husky 모듈로 실행함.
pre-push
#!/bin/sh
# husky
# Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
# At: 2020-5-11 3:14:45 PM
# From: ......./git-hooks-study (https://github.com/typicode/husky#readme)
. "$(dirname "$0")/husky.sh"
husky 를 사용하여 master 로 직접 push 하기를 방지해봄.
huskyhooks 디렉토리를 생성 후 디렉토리 안에 pre-push 스크립트를 작성. 이때 인자를 받는 방식이 기존 pre-push 훅과 다르므로 주의.
huskyhooks/pre-push
#!/bin/sh
FORBIDDEN_HTTPS_URL="https://github.com/gabia/git-hooks-study.git" # insert your remote url (https)
FORBIDDEN_SSH_URL="git@github.com:gabia/git-hooks-study.git" # insert your remote url (ssh)
FORBIDDEN_REF="refs/heads/master" # insert branch ref
ARR_GIT_PARAMS=($(echo $HUSKY_GIT_PARAMS))
ARR_GIT_STDIN=($(echo $HUSKY_GIT_STDIN))
remote=${ARR_GIT_PARAMS[0]}
url=${ARR_GIT_PARAMS[1]}
local_ref=${ARR_GIT_STDIN[0]}
local_sha=${ARR_GIT_STDIN[1]}
remote_ref=${ARR_GIT_STDIN[2]}
remote_sha=${ARR_GIT_STDIN[3]}
if [ "$url" != "$FORBIDDEN_HTTPS_URL" -a "$url" != "$FORBIDDEN_SSH_URL" ]
then
exit 0 # Forked Project 에서는 제한하지 않음
fi
if [ "$remote_ref" == "$FORBIDDEN_REF" ]
then
echo "DO NOT PUSH it master"
exit 1 # 금지된 ref 로 push 를 실행하면 에러
fi
exit 0
.huskyrc
파일에 pre-push 훅이 위에서 정의한 스크립트를 실행하도록 정의.
.huskyrc
{
"hooks": {
"pre-commit": "echo 'Hello Gabia, woof!'",
"pre-push": "./huskyhooks/pre-push"
}
}
훅을 적용한 후 원본 프로젝트의 master 로 push 를 시도하면 다음과 같이 에러 메세지와 함께 push 가 실패하는 것을 확인할 수 있음.