GitPython 소개 및 활용
GitPython은 파이썬 애플리케이션에서 Git 리포지토리를 손쉽게 제어하고 상호작용할 수 있도록 돕는 라이브러리입니다. 이 도구를 활용하면 Git 커맨드라인 인터페이스를 직접 호출하는 대신, 파이썬 코드를 통해 리포지토리 복제, 최신 변경사항 가져오기, 브랜치 및 태그 관리, 커밋 기록 분석 등 다양한 작업을 자동화할 수 있습니다.
설치
GitPython 라이브러리는 pip를 통해 간단하게 설치할 수 있습니다.
pip install gitpython
기본 리포지토리 작업
GitPython을 사용하여 리포지토리를 복제하거나 기존 리포지토리를 로드하는 방법은 다음과 같습니다.
1. 원격 리포지토리 복제 (Clone)
원격 Git 리포지토리를 로컬 시스템으로 복제합니다. 존재하지 않는 경로의 경우, GitPython이 자동으로 디렉터리를 생성합니다.
import os
from git import Repo
# 리포지토리를 복제할 로컬 경로 지정
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
remote_repo_url = 'https://github.com/gitpython-developers/GitPython.git' # 예시 URL
target_branch = 'main' # 복제할 브랜치
print(f"'{remote_repo_url}' 리포지토리를 '{local_repo_path}'로 복제합니다.")
try:
repo_instance = Repo.clone_from(remote_repo_url, to_path=local_repo_path, branch=target_branch)
print(f"리포지토리 복제 완료. 현재 브랜치: {repo_instance.active_branch}")
except Exception as e:
print(f"리포지토리 복제 중 오류 발생: {e}")
2. 기존 로컬 리포지토리 로드 및 최신 변경사항 가져오기 (Pull)
이미 로컬에 존재하는 Git 리포지토리를 GitPython으로 로드하고, 원격 저장소에서 최신 변경사항을 가져옵니다.
import os
from git import Repo
# 기존 리포지토리의 로컬 경로 지정
# 위의 복제 예제에서 사용된 경로를 재사용할 수 있습니다.
local_existing_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
try:
# 기존 Git 리포지토리를 로드
existing_repo = Repo(local_existing_repo_path)
print(f"로컬 리포지토리 '{local_existing_repo_path}' 로드 완료.")
print(f"현재 활성 브랜치: {existing_repo.active_branch}")
# 원격에서 최신 변경사항을 가져와 병합 (pull)
print("원격 저장소에서 최신 변경사항을 가져옵니다...")
existing_repo.git.pull()
print("최신 변경사항을 성공적으로 가져왔습니다.")
except Exception as e:
print(f"리포지토리 로드 또는 pull 중 오류 발생: {e}")
리포지토리 정보 조회
GitPython을 이용하면 리포지토리의 브랜치, 태그, 커밋 기록 등 다양한 메타데이터를 조회할 수 있습니다.
1. 모든 브랜치 목록 확인
원격 저장소의 모든 브랜치 목록을 가져옵니다.
import os
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
print("--- 원격 브랜치 목록 ---")
remote_branches = repo.remote().refs
for ref in remote_branches:
if ref.remote_head != 'HEAD': # 'HEAD'는 실제 브랜치가 아닌 현재 브랜치를 가리키는 포인터입니다.
print(f"- {ref.remote_head}")
2. 모든 태그 목록 확인
저장소에 정의된 모든 태그 목록을 가져옵니다.
import os
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
print("--- 태그 목록 ---")
for tag in repo.tags:
print(f"- {tag.name}")
3. 커밋 기록 조회
저장소의 커밋 기록을 조회하고, 특정 형식으로 파싱하여 활용할 수 있습니다. 여기서는 JSON 형식으로 출력하도록 설정하고 파싱하는 예시를 보여줍니다.
import os
import json
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
print("--- 최근 커밋 기록 (최대 10개) ---")
# Git log 명령어를 사용하여 커밋 정보를 JSON 형식으로 출력
# %h: 짧은 해시, %an: 작성자 이름, %s: 커밋 메시지 요약, %cd: 커밋 날짜
pretty_format = 'format:{"id":"%h", "author":"%an", "summary":"%s", "date":"%cd"}'
commit_output = repo.git.log(f'--pretty={pretty_format}', max_count=10, date='format:%Y-%m-%d %H:%M:%S')
# 각 줄을 JSON 객체로 파싱
commit_entries = commit_output.strip().split('\n')
parsed_commits = []
for entry in commit_entries:
if entry:
try:
parsed_commits.append(json.loads(entry))
except json.JSONDecodeError as e:
print(f"JSON 파싱 오류: {e} - 원본: {entry}")
for commit in parsed_commits:
print(f" ID: {commit['id']}, 작성자: {commit['author']}, 요약: {commit['summary']}, 날짜: {commit['date']}")
리포지토리 상태 변경
브랜치 전환, 특정 커밋으로 되돌리기 등 리포지토리의 상태를 변경하는 작업도 GitPython으로 가능합니다.
1. 브랜치 전환
지정된 브랜치로 전환합니다.
import os
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
target_branch_name = 'main' # 전환할 브랜치 이름 (실제 존재하는 브랜치로 변경)
print(f"현재 브랜치: {repo.active_branch}")
try:
print(f"'{target_branch_name}' 브랜치로 전환 시도...")
repo.git.checkout(target_branch_name)
print(f"성공적으로 '{target_branch_name}' 브랜치로 전환되었습니다. 현재 브랜치: {repo.active_branch}")
except Exception as e:
print(f"브랜치 전환 중 오류 발생: {e}")
2. 특정 커밋으로 되돌리기 (Hard Reset)
작업 트리를 특정 커밋 상태로 강제 전환합니다. 이 작업은 로컬 변경사항을 덮어쓸 수 있으므로 주의해야 합니다.
import os
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
# 되돌릴 커밋 해시 (예시: 실제 리포지토리의 유효한 커밋 해시로 변경)
target_commit_hash = '854ead2e82dc73b634cbd5afcf1414f5b30e94a8'
print(f"'{target_commit_hash}' 커밋으로 강제 되돌리기 시도...")
try:
repo.git.reset('--hard', target_commit_hash)
print("성공적으로 리포지토리가 지정된 커밋으로 되돌려졌습니다.")
print(f"현재 HEAD 커밋: {repo.head.commit.hexsha}")
except Exception as e:
print(f"커밋 되돌리기 중 오류 발생: {e}")
3. 리포지토리 아카이브 생성
현재 저장소의 스냅샷을 압축 파일 형태로 아카이빙합니다 (예: tar, zip).
import os
from git import Repo
local_repo_path = os.path.join(os.getcwd(), 'my_cloned_project')
repo = Repo(local_repo_path)
archive_output_path = os.path.join(local_repo_path, 'project_archive.tar')
print(f"리포지토리를 '{archive_output_path}'에 아카이빙합니다...")
try:
with open(archive_output_path, 'wb') as fp:
repo.archive(fp) # 기본적으로 tar 형식
print("리포지토리 아카이빙이 완료되었습니다.")
except Exception as e:
print(f"아카이빙 중 오류 발생: {e}")
Git 리포지토리 관리 클래스 예시
자주 사용되는 Git 작업을 클래스로 캡슐화하여, 재사용성을 높이고 코드를 더욱 체계적으로 관리할 수 있습니다.
import os
import json
from git import Repo, InvalidGitRepositoryError, NoSuchPathError
class GitRepositoryManager:
"""
Git 리포지토리의 복제, 업데이트, 정보 조회 등을 관리하는 유틸리티 클래스.
"""
def __init__(self, local_path, remote_url, default_branch='main'):
"""
클래스 초기화 및 리포지토리 로드/복제.
:param local_path: Git 리포지토리가 위치하거나 복제될 로컬 경로.
:param remote_url: 원격 Git 리포지토리의 URL.
:param default_branch: 초기 복제 또는 체크아웃할 기본 브랜치.
"""
self.local_repo_path = local_path
self.remote_repo_url = remote_url
self.repo_instance = None
self._initialize_repository(default_branch)
def _is_valid_git_repo(self, path):
"""지정된 경로가 유효한 Git 저장소인지 확인."""
try:
_ = Repo(path).git_dir
return True
except (InvalidGitRepositoryError, NoSuchPathError):
return False
def _initialize_repository(self, branch):
"""
로컬 경로에 Git 리포지토리가 없으면 복제하고, 있으면 로드합니다.
"""
if not os.path.exists(self.local_repo_path):
os.makedirs(self.local_repo_path, exist_ok=True)
if not self._is_valid_git_repo(self.local_repo_path):
print(f"'{self.local_repo_path}'에 Git 리포지토리가 없어 복제합니다.")
self.repo_instance = Repo.clone_from(self.remote_repo_url, self.local_repo_path, branch=branch)
else:
print(f"'{self.local_repo_path}'의 기존 Git 리포지토리를 로드합니다.")
self.repo_instance = Repo(self.local_repo_path)
# 원격 'origin'이 존재하지 않으면 추가
if 'origin' not in [remote.name for remote in self.repo_instance.remotes]:
self.repo_instance.create_remote('origin', self.remote_repo_url)
def update_from_remote(self):
"""원격 저장소에서 최신 변경사항을 가져와 현재 브랜치에 병합합니다 (git pull)."""
print("원격 저장소에서 최신 변경사항을 가져옵니다...")
self.repo_instance.git.pull()
print("최신 변경사항 업데이트 완료.")
def get_all_remote_branches(self):
"""현재 리포지토리의 모든 원격 브랜치 이름을 리스트로 반환합니다."""
remote_refs = self.repo_instance.remote().refs
return sorted([ref.remote_head for ref in remote_refs if ref.remote_head != 'HEAD'])
def get_all_tags(self):
"""현재 리포지토리의 모든 태그 이름을 리스트로 반환합니다."""
return sorted([tag.name for tag in self.repo_instance.tags])
def get_commit_logs(self, max_count=20):
"""
리포지토리의 커밋 기록을 지정된 개수만큼 JSON 형식의 리스트로 반환합니다.
각 커밋은 해시, 작성자, 요약, 날짜 정보를 포함합니다.
"""
pretty_format = 'format:{"hash":"%h", "author":"%an", "summary":"%s", "date":"%cd"}'
commit_log_raw = self.repo_instance.git.log(f'--pretty={pretty_format}', max_count=max_count, date='format:%Y-%m-%d %H:%M:%S')
parsed_logs = []
for entry in commit_log_raw.strip().split('\n'):
if entry:
try:
parsed_logs.append(json.loads(entry))
except json.JSONDecodeError as e:
print(f"커밋 로그 파싱 오류: {e} - 원본: {entry}")
return parsed_logs
def switch_branch(self, branch_name):
"""지정된 브랜치로 전환합니다."""
print(f"'{branch_name}' 브랜치로 전환합니다.")
self.repo_instance.git.checkout(branch_name)
print(f"현재 활성 브랜치: {self.repo_instance.active_branch}")
def revert_to_commit(self, commit_hash, hard_reset=True):
"""
지정된 커밋으로 리포지토리를 되돌립니다.
:param commit_hash: 되돌릴 커밋의 해시값.
:param hard_reset: True면 --hard 옵션으로 모든 변경사항을 버리고 되돌립니다.
False면 --mixed 옵션으로 인덱스는 초기화하고 작업 디렉토리는 유지합니다.
"""
print(f"'{commit_hash}' 커밋으로 되돌리기 시작 (hard_reset: {hard_reset})...")
if hard_reset:
self.repo_instance.git.reset('--hard', commit_hash)
else:
self.repo_instance.git.reset(commit_hash)
print("리포지토리 상태 업데이트 완료.")
def create_archive_file(self, output_path, archive_format='zip'):
"""
현재 리포지토리의 압축 아카이브를 생성합니다.
:param output_path: 아카이브 파일이 저장될 경로.
:param archive_format: 압축 형식 (예: 'zip', 'tar', 'tar.gz').
"""
print(f"'{output_path}'에 리포지토리 아카이브를 생성합니다 ({archive_format} 형식)...")
with open(output_path, 'wb') as fp:
self.repo_instance.archive(fp, format=archive_format)
print("리포지토리 아카이브 생성 완료.")
# --- GitRepositoryManager 클래스 사용 예시 ---
# if __name__ == "__main__":
# # 예시용 로컬 디렉토리 및 원격 URL
# test_repo_dir = os.path.join(os.getcwd(), 'my_git_project_auto')
# test_remote_url = 'https://github.com/gitpython-developers/GitPython.git' # 실제 존재하는 Git 리포지토리 URL로 변경
# try:
# manager = GitRepositoryManager(test_repo_dir, test_remote_url, default_branch='main')
# manager.update_from_remote()
# print("\n===== 브랜치 목록 =====")
# for branch in manager.get_all_remote_branches():
# print(f"- {branch}")
# print("\n===== 태그 목록 =====")
# for tag in manager.get_all_tags():
# print(f"- {tag}")
# print("\n===== 최근 커밋 기록 (5개) =====")
# for commit in manager.get_commit_logs(max_count=5):
# print(f" ID: {commit['hash']}, 작성자: {commit['author']}, 요약: {commit['summary']}")
# # 특정 브랜치로 전환 (예시: 'develop' 브랜치가 있다면)
# # try:
# # manager.switch_branch('develop')
# # except Exception as e:
# # print(f"브랜치 전환 실패: {e}")
# # 특정 커밋으로 되돌리기 (주의: 로컬 변경사항 손실 가능)
# # recent_commit_id = manager.get_commit_logs(max_count=1)[0]['hash']
# # print(f"\n최신 커밋 '{recent_commit_id}'으로 되돌리기 시도...")
# # manager.revert_to_commit(recent_commit_id)
# # 리포지토리 아카이브 생성
# archive_file = os.path.join(test_repo_dir, 'project_snapshot.zip')
# manager.create_archive_file(archive_file, 'zip')
# except Exception as e:
# print(f"전반적인 작업 중 오류 발생: {e}")