swift

Git Hooks에 SwiftLint 적용하기

kimyounggyun 2023. 5. 15. 22:33

개요

Git 훅은 Git 작업 흐름 중 특정 이벤트가 발생할 때 실행되는 스크립트이다. Git 훅은 클라이언트 훅과 서버 훅으로 나눌 수 있다. 클라이언트 훅은 커밋이나 머지 이벤트가 발생할 때 실행되고 서버 훅은 푸시할 때 실행된다.

다양한 훅

기본적으로 Git은 .git/hooks에 있는 훅 스크립트를 사용한다. 기본 훅 디렉토리에는 Git이 기본으로 제공하는 훅이 있다. 기본 훅들은 sample 확장자를 가진 형태인데 sample확장자를 지우고 sh 명령어로 실행할 수 있다. 

pre-commit.sample

pre-commit 훅에 SwiftLint 적용하기

pre-commit 훅은 커밋 과정 중 가장 먼저 호출되는 훅으로 커밋 전에 실행된다. 이 훅을 사용해 코드 스타일, 코드 포맷, 테스트 등을 자동으로 실행하여 커밋 전 개발자가 실수한 것이 있는지 확인할 수 있다. pre-commit 훅을 조작하여 커밋 전에 스위프트 코드가 린트에 맞게 작성되었는지 확인해 보자. 스크립트는 아래와 같은 흐름으로 작성했다.

pre-commit.sh

swiftlint 찾기

which swiftlint 명령어를 통해 swiftlint의 위치를 찾을 수 있다. 그리고 변수를 하나 만들어 명령어 실행 결과를 저장하자.

#!/bin/bash

# find swiftlint path
SWIFTLINT_PATH=$(which swiftlint)

만약 아래처럼 swiftlint을 추출하여 따로 사용하고 있는 경우에는 다른 방법으로 swiftlint를 찾아야 한다. 왜냐하면 which swiftlint가 brew로 설치한 swiftlint의 위치를 찾기 때문이다.

swiftlint와 같은 디렉토리에 있는 유니크한 파일을 찾아 swiftlint의 위치를 특정하면 된다. 아래 코드는 xcworkspace 파일이 있는 디렉토리에서 swiftlint를 찾는 스크립트이다.

# Constants
readonly WORKSPACE="GaeManda.xcworkspace"

# Variables
swiftlint_path=""
swiftlint_yml_path=""

# Find swiftlint path
path=$(find ~/ -name "$WORKSPACE" -print -quit 2>/dev/null)
if [ -n "$path" ]; then
  directory=$(dirname "$path")
  swiftlint_path="$directory/swiftlint"
  swiftlint_yml_path="$directory/.swiftlint.yml"
  
  if [ -e "$swiftlint_path" ] && [ -e "$swiftlint_yml_path" ]; then
    echo "✅ $WORKSPACE 파일이 있는 디렉토리에서 swiftlint과 .swiftlint.yml을 찾았습니다."
    echo "✅ [swiftlint] : $swiftlint_path"
    printf "✅ [.swiftlint.yml] : $swiftlint_yml_path\n\n"
  else
    echo "❌ $WORKSPACE 파일이 있는 디렉토리에서 swiftlint과 .swiftlint.yml을 찾을 수 없습니다."
    exit 1
  fi
else
  echo "❌ $WORKSPACE 파일을 찾을 수 없습니다."
  exit 1
fi

swiftlint가 실행 가능한지 확인하기

swiftlint를 적용하기 전 찾은 경로에 있는 swiftlint가 실행 가능한지 체크한다. 실행이 안되면 스크립트를 에러 상태로 종료해 커밋을 중단한다.

# ... 생략 

# Check if swiftlint and .swiftlint.yml exist and are executable
printf "🚀 swiftlint, .swiftlint.yml 체크 중...\n\n"
if [ -n "$swiftlint_path" ] && \
   [ -n "$swiftlint_yml_path" ] && \
   [ -x "$swiftlint_path" ]; then
  printf "✅ swiftlint, .swiftlint.yml 체크 성공\n\n"
else
  echo "❌ swiftlint, .swiftlint.yml 체크 실패."
  exit 1
fi

스테이징 영역에서 스위프트 파일 찾기

커밋을 수행할 때 아래와 같은 경우를 고려해야 한다.

  1. 스테이징 영역에 파일이 없다.
  2. 스테이징 영역에 스위프트 파일이 없다.
  3. 스테이징 영역에 스위프트 파일이 있다.

1의 경우에는 스크립트를 종료해 커밋을 중단한다.

2의 경우에는 스위프트 파일에만 린트를 적용할 것이기 때문에 스크립트를 성공 상태로 종료시켜 커밋을 수행한다. 

3의 경우에는 린트를 적용해야 한다.

위의 경우를 모두 고려하여 스크립트를 작성하면 아래와 같다.

# ...생략

changed_files=$(git diff --stat --cached)
upstream_branch=$(git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)) 
swift_files=$(git diff --stat --cached --diff-filter=d --name-only $upstream_branch | grep -E "\.swift$")

if [ -n "$changed_files" ] && [ -z "$swift_files" ]; then
  echo "🙆🏻‍♂️ 커밋 성공."
  exit 0
elif [ -z "$changed_files" ] || [ -z "$swift_files" ]; then
  echo "🙋🏻‍♂️ 스테이징 영역에 파일이 없습니다."
  exit 1
fi

swiftlint 적용하기

swiftlint로 .swiftlint.yml을 실행시킨 결과를 변수에 저장한다.

# ...생략

lint_result=$("$swiftlint_path" lint --quiet --config "$swiftlint_yml_path")

린트를 적용한 결과가 만약 빈 문자열이라면 스테이징 영역에 있는 스위프트 파일들은 린트에 맞게 작성된 것이므로 커밋을 시키면 된다. 따라서 스크립트를 성공 상태로 종료한다.

만약 결과가 빈 문자열이 아니라면 스크립트를 에러 종료해 커밋을 중단해야 한다. 이때 결과 문자열을 파싱 하여 화면에 보여줄 수도 있다. 결과 문자열은 아래와 같은 형식이다.

# {absolute_path.swift}:{line_number}:{column_number}: {error_type}: {error_message}: {error_description}

/Users/kimyounggyun/Desktop/tca_example/Projects/Core/Services/UserService.swift:23:7: warning: Comment Spacing Violation: Prefer at least one space after slashes for comments (comment_spacing)

/Users/kimyounggyun/Desktop/tca_example/Projects/Core/Services/UserService.swift:22:33: error: Force Cast Violation: Force casts should be avoided (force_cast)

결과 문자열이 :으로 구분되기 때문에 :를 기준으로 파싱 하면 된다.

# ... 생략

if [ -z "$lint_result" ]; then
  echo "❤️  작성한 코드가 swiftlint에 맞습니다."
  echo "🙆🏻‍♂️ 커밋 성공."
else
  echo ""
  printf "💔 swiftlint에 어긋나는 코드가 있습니다. 아래 내용을 확인해주세요.\n\n"

  while IFS=':' read -r file_path \
                        line_number \
                        column_number \
                        error_type \
                        error_message \
                        error_description; do
    icon="🚧"
    if [ "$error_type" = " error" ]; then
      icon="🚨"
    fi

    echo "$icon$error_type"
    echo "┗ $file_path:$line_number:$column_number"
    echo "┗$error_message:$error_description"
    echo ""
    
  done <<< "$lint_result"

  echo "🙅🏻‍♂️ 커밋 실패. swiftlint에 맞게 코드를 변경해주세요."
  exit 1
fi

사용하기

작성한 pre-commit 스크립트는 .git/hooks 디렉토리에 있어 다른 개발자들과 공유할 수 없다. 기본적으로 Git은 .git/hooks 디렉토리에 있는 훅 스크립트를 사용하지만 git config core.hooksPath 명령어를 사용해 훅 스크립트의 경로를 변경할 수 있다. 명령어를 사용해 훅 스크립트 경로를 바꿔 사용하면 된다.

> git config core.hooksPath .githooks

이제 스테이징 영역에 파일을 올리고 커밋을 해보면 커밋 전 린트가 잘 작동하는 것을 확인할 수 있다.

⬇️ 전체 스크립트 코드 ⬇️

더보기
#!/bin/bash

# Constants
readonly WORKSPACE="GaeManda.xcworkspace"

# Variables
swiftlint_path=""
swiftlint_yml_path=""

# Find swiftlint path
path=$(find ~/ -name "$WORKSPACE" -print -quit 2>/dev/null)
if [ -n "$path" ]; then
  directory=$(dirname "$path")
  swiftlint_path="$directory/swiftlint"
  swiftlint_yml_path="$directory/.swiftlint.yml"
  if [ -e "$swiftlint_path" ] && [ -e "$swiftlint_yml_path" ]; then
    echo "✅ $WORKSPACE 파일이 있는 디렉토리에서 swiftlint과 .swiftlint.yml을 찾았습니다."
    echo "✅ [swiftlint] : $swiftlint_path"
    printf "✅ [.swiftlint.yml] : $swiftlint_yml_path\n\n"
  else
    echo "❌ $WORKSPACE 파일이 있는 디렉토리에서 swiftlint과 .swiftlint.yml을 찾을 수 없습니다."
    exit 1
  fi
else
  echo "❌ $WORKSPACE 파일을 찾을 수 없습니다."
  exit 1
fi

# Check if swiftlint and .swiftlint.yml exist and are executable
printf "🚀 swiftlint, .swiftlint.yml 체크 중...\n\n"
if [ -n "$swiftlint_path" ] && [ -n "$swiftlint_yml_path" ] && [ -x "$swiftlint_path" ]; then
  printf "✅ swiftlint, .swiftlint.yml 체크 성공\n\n"
else
  echo "❌ swiftlint, .swiftlint.yml 체크 실패."
  exit 1
fi

# Find Swift files to apply swiftlint to
changed_files=$(git diff --stat --cached)
upstream_branch=$(git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)) 
swift_files=$(git diff --stat --cached --diff-filter=d --name-only $upstream_branch | grep -E "\.swift$")

if [ -n "$changed_files" ] && [ -z "$swift_files" ]; then
  echo "🙆🏻‍♂️ 커밋 성공."
  exit 0
elif [ -z "$changed_files" ] || [ -z "$swift_files" ]; then
  echo "🙋🏻‍♂️ 스테이징 영역에 파일이 없습니다."
  exit 1
fi

# Apply swiftlint
printf "🚀 swiftlint 적용 중...\n\n"
lint_result=$("$swiftlint_path" lint --quiet --config "$swiftlint_yml_path")
printf "✅ swiftlint 적용 완료.\n\n"

if [ -z "$lint_result" ]; then
  echo "❤️  작성한 코드가 swiftlint에 맞습니다."
  echo "🙆🏻‍♂️ 커밋 성공."
else
  printf "💔 swiftlint에 어긋나는 코드가 있습니다. 아래 내용을 확인해주세요.\n\n"
  while IFS=':' read -r file_path line_number column_number error_type error_message error_description; do
    icon="🚧"
    if [ "$error_type" = " error" ]; then
      icon="🚨"
    fi
    echo "$icon$error_type"
    echo "┗ $file_path:$line_number:$column_number"
    echo "┗$error_message:$error_description"
    echo ""
  done <<< "$lint_result"

  echo "🙅🏻‍♂️ 커밋 실패. swiftlint에 맞게 코드를 변경해주세요."
  exit 1
fi

참고