코드로 우주평화

커밋 메시지를 자동 생성해보자! (feat. auto-commit-msg) 본문

나는 이렇게 성장한다/활동

커밋 메시지를 자동 생성해보자! (feat. auto-commit-msg)

daco2020 2023. 8. 20. 21:06

`auto-commit-msg` 프로젝트 시작

개발자들은 보통 git 이라는 버전 관리 시스템(VCS)을 사용하여 코드를 관리한다. 그리고 git 은 commit 을 통해 코드 변경사항을 기록한다.

 

예를 들어 개발자가 main.py 파일을 추가했다면 git commit -m "feat: Add main.py file" 처럼 명령어를 입력하여 해당 커밋이 무엇을 의미하는지 메시지를 작성한다.

 

예시처럼 간단한 변경사항이라면 커밋 메시지를 작성하는데 어려움은 없겠지만 복잡한 변경사항이라면 커밋 메시지를 짓는 데에도 시간이 걸린다. 만약 리팩터링을 위해 여기저기 파일을 손대고 코드를 수정했다면, 개발자는 커밋 메시지를 짓느라 일정 시간을 사용할 수 밖에 없다.

 

나는 커밋 메시지를 작성하는데 시간을 쓰고 싶지 않았고 이를 GPT로 자동화하면 어떨까?라는 아이디어를 떠올렸다. 평소에 나는 pre-commit 이라는 라이브러리를 통해 린팅, 포맷팅, 정적검사를 자동화하고 있었다. 그렇다면 커밋 메시지도 자동화할 수 있겠다 싶었고 `auto-commit-msg` 라는 이름으로 오픈소스 패키지 개발에 도전해보았다!

 

*pre-commit은 코드를 커밋하기 전에 자동으로 실행되는 훅(hook)을 관리해주는 도구로 주로 코드 포맷팅, 정적 검사, 테스트 실행과 같은 작업을 자동화하기 위해 사용한다.

 

 

정말 가능해?

내 아이디어가 실제 구현 가능한지 사전조사를 하기로 했다. 그러기 위해 먼저 요구사항과 플로우를 작성해 보았다.

 

요구사항

- 사용자는 -m 파라미터 없이 git commit 명령어만으로 커밋 메시지를 얻을 수 있다.

- 사용자는 -m 파라미터를 통해 기존처럼 커밋 메시지를 수동 작성할 수 있다.

- 사용자는 원하는 언어로 커밋 메시지를 얻을 수 있다.

- 사용자는 커밋 컨벤션을 직접 지정할 수 있다.

 

요구사항을 토대로 어떤 동작이 필요한지 파악한 후 동작 플로우를 정리했다.

 

플로우

- 직전 커밋과 현재 코드와의 변경사항을 가져온다.

- ChatGPT를 통해 커밋 메시지를 생성한다.

- 커밋 메시지를 임시 파일로 저장한다.

- 사용자는 커밋 메시지를 확인 후 커밋을 완료한다.

 

코드 변경사항은 git diff HEAD 명령어를 통해 알 수 있었고 ChatGPT 연동은 전에 '톡톡캣' 프로젝트로 해본 적이 있기에 방법을 잘 알고 있었다. 또한 커밋 메시지는 .git/COMMIT_EDITMSG 에 임시 저장되었다가 실제 커밋 시 사용된다는 걸 알아냈다.

 

이렇게 윤곽을 잡아보니 작업이 생각보다 간단해 보였고 쉽게 구현할 수 있을 것 같았다!

 

 

어림도 없죠?

나름 상세하게 사전조사를 했음에도 실제 작업 시에 막혔던 부분들이 많았다. 그중 내가 씨름했던 문제 세 가지를 공유하겠다.

setup.py 누구냐 너!

setup.py 는 패키징 및 배포를 관리하는 스크립트 파일이다. 이 파일은 주로 setuptoolsdistutils 라이브러리를 사용하여 파이썬 패키지 정보와 설정을 하는데 나는 처음 다루다 보니 뭐가 뭔지 이해하는데 시간이 걸렸다.

 

덕분에 setup.py 에 대해 간단히 공부하는 계기가 되었고 이를 따로 정리해 두었다. (setup.py 의 역할과 사용법 참고)

나는 auto-commit-msg 패키지의 setup.py 를 아래와 같이 명시했다.

 

from setuptools import setup

setup(
    name="auto-commit-msg",
    py_modules=["main", "commit_msg_generator", "config"],
    entry_points={
        "console_scripts": ["auto-commit-msg=main:run"],
    },
    install_requires=["openai==0.27.8", 'subprocess32; python_version<"3.0"'],
)

 

entry_points 를 보면 auto-commit-msg 명령어를 입력하면 main.py 의 run 함수를 실행하도록 구성하였다. 그리고 다음처럼 .pre-commit-hooks.yaml 을 작성했다.

 

- id: auto-commit-msg
  name: auto-commit-msg
  description: "Automatically generates commit messages based on diffs"
  entry: auto-commit-msg
  language: python
  require_serial: true
  additional_dependencies: ['openai==0.27.8']

 

 

사용처에서 pre-commit 을 실행하면 패키지를 내려받게 되고 .pre-commit-hooks.yaml 파일에 명시된 entry auto-commit-msg 를 통해 패키지를 실행하게 된다. 이로써 실제 원격 패키지를 내려받아 스크립트를 실행하는 데 성공했다!

 

 

Git Hooks 에도 단계가 있다고?

pre-commit 을 통해 분명 패키지를 실행했음에도 내가 원하는 결과(커밋 메시지 자동생성)가 나오지 않았다. 의문이 들었던 나는 pre-commit 의 동작방식을 자세히 찾아보았다.

 

앞서 소개한 대로 pre-commit 은 커밋하기 전에 자동으로 실행되는 훅(hook)을 관리해 주는 도구이다. 그렇다면 Git Hooks 이란 무엇일까? (자세히 알고 싶다면 Git Hooks 문서를 참고하라!)

 

문서를 살펴보니 Git Hooks 에도 몇 가지 단계가 있음을 알게 되었다. Git에는 커밋 과정에서 자동으로 실행되는 여러 훅이 있고 이런 훅들은 커밋 과정의 특정 단계에서 실행된다.

 

Git Hooks의 주요 단계를 정리하면 다음과 같다.

pre-commit: 코드 검사, 테스트 실행, 코드 포맷팅 등을 수행할 수 있는 훅

- prepare-commit-msg: 커밋 메시지의 초기 내용을 생성하거나 수정하는 등의 작업을 할 수 있는 훅

commit-msg: 커밋 메시지의 유효성을 검사하거나 특정 규칙을 적용하는 등의 작업을 할 수 있는 훅

post-commit: 커밋이 성공한 후에 실행되는 훅으로, 이메일 알림 보내기, 문서 자동 생성 등의 작업을 수행할 수 있음.

 

즉, 커밋 메시지를 생성하고 수정하는 것은 prepare-commit-msg 단계에서 수행하는데, 나는 pre-commit 단계에서 패키지를 실행하고 있던 것이다. 그러니 커밋 메시지가 생성되어도 이를 반영하지 못했던 것...!

 

이를 해결하기 위해 여러 가지 방법들을 시도해 보았는데 실제 성공한 것은 prepare-commit-msg 스크립트를 추가하는 방법뿐이었다.

 

prepare-commit-msg 단계에서는 .git/hooks/prepare-commit-msg 경로의 스크립트를 실행하게 된다. 그래서 나는 pre-commit 단계에서 두 개의 임시 파일을 생성하도록 로직을 변경했는데 하나는 '커밋 메시지'를 다음 단계에서도 사용할 수 있도록 임시 저장하는 파일이고, 다른 하나는 .git/hooks/ 경로에 추가한 prepare-commit-msg 스크립트 파일이었다.

 

그렇게 생성한 prepare-commit-msg 스크립트 파일은 아래의 코드를 담고 있었다.

#!/bin/bash
commit_msg_file=$1
commit_source=$2
commit_msg=$(cat $commit_msg_file)

if [ "$commit_source" == "message" ] || [ "$commit_source" == "template" ] || [ "$commit_source" == "merge" ] || [ "$commit_source" == "squash" ] || [ "$commit_source" == "commit" ]; then
    exit 0
fi

echo $(cat .git/temp_commit_msg) > $commit_msg_file
rm .git/temp_commit_msg

echo -n > "$0"

코드를 설명해 보자면 다음과 같다.

 

commit_msg_file=$1, commit_source=$2

커밋 메시지 파일의 경로와 커밋 소스 타입을 받아 변수에 저장한다.

 

commit_msg=$(cat $commit_msg_file)

커밋 메시지 파일의 내용을 commit_msg 변수에 저장한다.

 

if [ "$commit_source" == ... ]; then exit 0; fi 

커밋 소스가 "message", "template", "merge", "squash", "commit" 중 하나인 경우, 스크립트를 종료한다. 즉, pre-commit 을 통해 커밋하는 것이 아니라면 이 스크립트는 동작하지 않는다.

 

echo $(cat .git/temp_commit_msg) > $commit_msg_file

위에서 언급한 임시 저장한 커밋 메시지를 commit_msg_file 에 덮어쓰기 한다. 이렇게 해서 임시 커밋 메시지를 실제 커밋 메시지로 사용할 수 있게 된다.

 

rm .git/temp_commit_msg

임시 커밋 메시지 파일을 삭제한다.

 

echo -n > "$0"

현재 스크립트 파일($0이 파일 자기 자신을 가리키는 변수)의 내용을 비워 최종적으로 스크립트를 백지로 되돌린다. (스크립트가 자신의 코드를 삭제하는 이유는 pre-commit 외의 상황에서 해당 스크립트가 사용되는 것을 원치 않기 때문이다)

 

 

간단히 요약하자면, 이 스크립트는 '임시 커밋 메시지 파일'을 '실제 커밋 메시지 파일'로 복사한 뒤, '임시 커밋 메시지 파일'과 스크립트 자신의 코드를 삭제한다. 

 

이로써 pre-commit 단계에서 동작하는 패키지가 prepare-commit-msg 단계에도 영향을 주어, 자동 생성된 커밋 메시지를 적용할 수 있었다!

 

 

GPT야, 한국어로 답해야지!

나는 사용자가 원하는 언어로 커밋 메시지를 받을 수 있도록 구현하고 싶었다. 즉 한국어를 지정하면 한국어로 된 커밋 메시지를 받을 수 있어야 했다.

 

하지만 GPT에게 해당 언어로 답을 해달라고 요청했음에도 GPT는 영어로 답변했다. 여러 가지 이유가 있겠지만 GPT가 요청 메시지를 제대로 이해하지 못하는 듯했다. GPT-4 모델을 사용하면 원하는 대로 언어를 변경해 답변해 주었지만 GPT-4 는 GPT-3.5 Turbo 모델에 비해서 비용이 20배 이상 비쌌다.

 

Input 은 20배, output 은 30배 차이가 나는 것을 확인할 수 있다.

 

때문에 나는 기본 모델을 GPT-3.5 Turbo 로 지정했고 GPT-3.5 는 사용자가 원하는 언어로 답을 해줄 수 있어야 했다. 나는 이를 해결하기 위해 요청 메시지를 여러 가지 버전으로 바꿔가며 테스트했는데 가장 효과적이었던 것은 요청 메시지 자체를 설정한 언어로 요청하는 것이었다.

 

예를 들어 한국어 커밋 메시지를 얻고 싶다면 요청도 영어가 아니라 한국어로 하는 것이 가장 효과가 좋았다. 마찬가지로 일본어, 중국어 커밋 메시지를 얻고 싶다면 해당 언어로 요청 메시지를 보내는 것이 효과가 좋았다.

 

이모지 접두어 컨벤션과 한국어 설정이 반영된 커밋 메시지

 

결국 나는 지원하는 언어인 영어, 한국어, 일본어, 중국어로 된 요청 메시지를 각각 상수로 두고 이를 기반으로 GPT에게 요청 메시지를 전달했다. 개인적으로 마음에 드는 해결책은 아니지만 나중에 더 좋은 방법을 발견한다면 보완해야겠다.

 

*언어 번역과 관련해서 deepl API 를 사용해 볼까도 생각했지만 gpt 외에 외부 의존성을 추가하고 싶지 않아 제외하였다.

 

 

 

결국 완성!

실제 pre-commti 을 통해 커밋 메시지가 자동 생성되는 모습

 

위에서 언급한 요구사항을 기억하는가?

 

요구사항

- 사용자는 -m 파라미터 없이 git commit 명령어만으로 커밋 메시지를 얻을 수 있다.

- 사용자는 -m 파라미터를 통해 기존처럼 커밋 메시지를 수동 작성할 수 있다.

- 사용자는 원하는 언어로 커밋 메시지를 얻을 수 있다.

- 사용자는 커밋 컨벤션을 직접 지정할 수 있다.

 

문제를 하나씩 해결해 나가면서 나는 위의 모든 요구사항을 만족하는 패키지를 개발할 수 있었다. 특히 이번 프로젝트는 나의 첫 번째 오픈소스 프로젝트였다는 점에서 매우 의미 있는 프로젝트였다!

 

`auto-commit-msg` 의 전체 코드는 github 에서 확인할 수 있다.

 

 

앞으로 나는

내가 `auto-commit-msg` 을 만든 이유는 결국 내가 쓰기 위함이었다. 앞으로 내 프로젝트에서 야무지게 활용할 예정이다.

 

다만 한 가지 아쉬운 점도 있다. 앞서 prepare-commit-msg 파일을 생성하고 지우는 로직이 있는데 만약 사용자가 이미 prepare-commit-msg 파일을 사용하고 있었다면 이를 덮어쓰게 되므로 문제가 될 수 있다. (현재로선 다른 방법이 떠오르지 않는다..)

 

이번 프로젝트를 통해 새롭게 얻은 교훈도 있다. 나는 개발과정에서 막힐 때마다 무지성으로 코드 일부를 수정해가며 문제를 해결하려는 태도를 보였다. 이는 문제 원인을 제대로 알고 해결하는 것이 아닌 '때려 맞추기'식 해결법이었다. 때문에 먼저 문서를 찾아보고 동작원리를 이해했다면 쉽게 해결했을 문제도 오히려 더 오랜 시간 동안 헤매게 만들었다.

 

어떤 방해물을 만났다면 먼저 무엇이 문제인지를 파악하는 것이 가장 빠른 길이라는 걸 알게 되었다. 다음 프로젝트에서는 문제 파악을 최우선으로 삼아야겠다.

 

개발자는 운에 의지하는 사람이 아니라, 문제를 해결하는 사람이라는 걸 잊지 말자!