본문 바로가기
CI.CD/GIT

[GIT] 커밋 정밀 조작(Amend, Cherry-pick)과 최후의 복구 도구(Reflog)

반응형

방금 커밋을 남겼는데 치명적인 오타를 발견했거나, 빠뜨린 파일이 뒤늦게 생각난 적이 있으신가요? 혹은 다른 팀원이 작업한 수십 개의 커밋 중 딱 하나의 커밋(특정 기능 추가)만 내 브랜치로 가져오고 싶은 경우가 발생할 수 있습니다. Git은 이처럼 세밀한 단위의 커밋 조작을 가능하게 하는 강력한 도구들을 제공합니다.

이번 포스팅에서는 최신 커밋을 살짝 수정하여 완벽하게 다듬는 git commit --amend와, 다른 브랜치에서 내가 원하는 특정 커밋만 쏙쏙 골라 담는 git cherry-pick의 활용법에 대해 자세히 살펴보겠습니다.

1. 방금 남긴 커밋 수정하기: git commit --amend

git commit --amend는 말 그대로 가장 마지막에 남긴(최신) 커밋을 '고치는' 명령어입니다. 단순히 커밋 메시지에 오타가 나서 수정하고 싶을 때뿐만 아니라, 깜빡하고 스테이징 영역에 올리지 못한 파일을 이전 커밋에 슬쩍 끼워 넣고 싶을 때 매우 유용하게 사용됩니다.

커밋 메시지만 수정할 경우

워킹 디렉토리에 변경된 파일이 아무것도 없는 상태에서 아래 명령어를 입력하면, 기본 설정된 텍스트 에디터(Vim 등)가 열리며 이전 커밋 메시지를 수정할 수 있게 됩니다.

# 최신 커밋의 메시지 수정 창 열기
$ git commit --amend

# 에디터를 열지 않고 명령어 한 줄로 바로 메시지 변경하기
$ git commit --amend -m "fix: 회원가입 버튼 오타 수정 (수정됨)"

누락된 파일을 추가하여 커밋 덮어쓰기

index.htmlstyle.css를 함께 커밋해야 했는데 실수로 index.html만 커밋했다면, 추가로 커밋을 하나 더 만들 필요 없이 --amend를 사용하여 하나의 커밋으로 깔끔하게 합칠 수 있습니다.

# 1. 누락되었던 파일을 스테이징 영역에 올림
$ git add style.css

# 2. 방금 전의 커밋에 파일을 포함하여 덮어쓰기 (메시지 유지 시 --no-edit 옵션 사용)
$ git commit --amend --no-edit

Amend 사용 시 주의사항
`--amend`는 겉보기에는 기존 커밋을 수정하는 것 같지만, 실제로는 기존 커밋을 삭제하고 내용을 변경한 완전히 새로운 커밋(새로운 해시값)을 생성하여 교체하는 작업입니다. 따라서 이전 포스팅에서 다룬 Rebase와 마찬가지로, 이미 원격 저장소에 Push하여 다른 사람과 공유된 커밋은 절대 Amend하면 안 됩니다.

2. 원하는 커밋만 골라서 가져오기: git cherry-pick

다른 브랜치의 작업 내역을 가져올 때 보통 git merge를 사용합니다. 하지만 Merge는 대상 브랜치의 모든 커밋을 통째로 가져오는 방식입니다. 만약 feature-a 브랜치에 A, B, C라는 3개의 커밋이 있는데, 나는 오직 'B'라는 커밋에 담긴 특정 기능(예: 결제 모듈 버그 수정)만 내 master 브랜치로 가져오고 싶다면 어떻게 해야 할까요? 이때 사용하는 것이 체리(원하는 것)만 쏙 뽑아먹는다는 의미의 git cherry-pick입니다.

# 1. 다른 브랜치의 커밋 내역(해시값) 확인
$ git log feature-a --oneline
c3b2a1f (feature-a) C: 로그인 UI 수정
b2a1f0e B: 결제 모듈 버그 수정 (가져올 커밋)
a1f0e9d A: 회원가입 기능 추가

# 2. 내가 커밋을 적용할 브랜치(master)로 이동
$ git switch master

# 3. 원하는 커밋의 해시값을 체리픽
$ git cherry-pick b2a1f0e
[master f8e7d6c] B: 결제 모듈 버그 수정
 1 file changed, 5 insertions(+)

명령어를 실행하면 Git은 지정한 b2a1f0e 커밋의 변경 사항(Patch)을 master 브랜치에 그대로 적용하여 새로운 커밋(위에서는 f8e7d6c)을 만들어 냅니다. 내용과 커밋 메시지는 동일하지만, 완전히 다른 새로운 커밋이 생성된 것입니다.

여러 개의 커밋을 동시에 체리픽하기

하나의 커밋뿐만 아니라 여러 개의 커밋을 띄어쓰기로 구분하여 가져오거나, 연속된 범위를 지정하여 한꺼번에 가져올 수도 있습니다.

# 여러 개의 특정 커밋들을 순서대로 가져오기
$ git cherry-pick 커밋해시1 커밋해시2 커밋해시3

# A 커밋부터 B 커밋까지의 범위(A는 미포함, B는 포함) 가져오기
$ git cherry-pick 커밋해시A..커밋해시B

1. 최후의 보루: git reflog

우리가 흔히 사용하는 git log는 현재 브랜치가 가리키는 지점에서부터 과거로 거슬러 올라가며 커밋 내역을 보여줍니다. 즉, reset이나 branch -D 등으로 특정 커밋의 연결 고리가 끊어지면 git log 화면에서는 더 이상 그 커밋을 찾을 수 없습니다. 하지만 커밋 데이터 자체가 즉시 삭제되는 것은 아닙니다. Git은 브랜치의 포인터(HEAD)가 이동한 모든 이력을 Reflog(Reference logs)라는 별도의 공간에 차곡차곡 기록해 둡니다.

Reflog를 활용한 복구 과정

예를 들어 master 브랜치에서 중요한 커밋 세 개를 작성한 뒤, 실수로 git reset --hard HEAD~3 명령어를 입력하여 커밋 세 개를 통째로 날렸다고 가정해 보겠습니다. 이 상황을 복구하는 절차는 다음과 같습니다.

# 1. Reflog를 통해 HEAD가 이동했던 모든 이력을 조회
$ git reflog
c4d5e6f (HEAD -> master) HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: 결제 시스템 고도화 완료 (날려버린 가장 최신 커밋)
b2c3d4e HEAD@{2}: commit: 장바구니 버그 수정
d3e4f5g HEAD@{3}: commit: 로그인 버튼 CSS 수정
c4d5e6f HEAD@{4}: clone: from https://github.com/my/project.git

Reflog 출력 결과를 보면 방금 수행한 reset 동작(HEAD@{0})은 물론, 그 이전에 우리가 차례대로 커밋했던 내역들이 고스란히 남아있는 것을 볼 수 있습니다. 우리가 되돌아가야 할 목표 시점은 결제 시스템 고도화를 마쳤던 HEAD@{1} 지점, 즉 해시값 a1b2c3d입니다.

# 2. 날아간 커밋의 해시값을 찾아 다시 reset을 수행하여 복구
$ git reset --hard a1b2c3d
HEAD is now at a1b2c3d 결제 시스템 고도화 완료

# 3. 로그를 확인해보면 마법처럼 커밋들이 되살아남
$ git log --oneline
a1b2c3d (HEAD -> master) 결제 시스템 고도화 완료
b2c3d4e 장바구니 버그 수정
d3e4f5g 로그인 버튼 CSS 수정
c4d5e6f ...

Reflog의 한계
Reflog는 원격 서버가 아닌 오직 내 로컬 컴퓨터의 `.git` 폴더 내부에만 저장됩니다. 따라서 로컬 저장소 폴더 자체를 삭제해버렸거나 원격 저장소를 새로 Clone 받았다면 Reflog를 통한 복구는 불가능합니다. 또한 기본적으로 약 30일~90일이 지난 오래된 Reflog 기록은 Git의 가비지 컬렉터(Garbage Collector)에 의해 영구 삭제되므로 가급적 빨리 복구해야 합니다.

2. 릴리즈 버전 명시: git tag

프로젝트 개발이 진행되면서 v1.0.0, v2.1.0과 같이 특정 소프트웨어 배포 버전을 확정 지어야 할 때가 있습니다. 커밋 메시지에만 버전을 적어두면 나중에 수백 개의 커밋 속에서 찾기가 힘듭니다. 이때 특정 커밋을 영구적인 북마크처럼 지정해 두는 기능이 바로 태그(Tag)입니다. 브랜치(Branch)는 새로운 커밋이 추가될 때마다 포인터가 앞으로 이동하지만, 태그는 한 번 특정 커밋에 부착되면 절대 이동하지 않는 고정된 포인터라는 차이점이 있습니다.

Lightweight 태그와 Annotated 태그

Git의 태그는 크게 두 가지 종류로 나뉩니다. 첫 번째는 단순히 특정 커밋을 가리키기만 하는 가벼운 태그(Lightweight)이고, 두 번째는 태그를 만든 사람의 정보, 생성 날짜, 태그에 대한 설명 메시지까지 모두 포함하는 별도의 객체인 주석 태그(Annotated)입니다. 실무에서는 배포 버전을 명확히 기록하기 위해 주석 태그를 권장합니다.

# 현재 커밋에 Annotated 태그(v1.0.0) 생성 (-a 옵션과 -m 메시지 사용)
$ git tag -a v1.0.0 -m "버전 1.0.0 정식 릴리즈"

# 생성된 태그 목록 확인
$ git tag
v1.0.0

# 특정 태그의 상세 정보(작성자, 날짜, 커밋 내역 등) 조회
$ git show v1.0.0

태그를 원격 저장소에 공유하기

기본적으로 git push 명령어는 커밋 데이터만 원격 저장소로 전송하며 태그 정보는 올리지 않습니다. 태그를 GitHub 등의 원격 서버에 공유하여 팀원들이나 사용자들이 릴리즈 버전을 다운로드할 수 있게 하려면 별도로 Push를 해주어야 합니다.

# 특정 태그(v1.0.0) 하나만 원격 저장소에 업로드
$ git push origin v1.0.0

# 로컬에 있는 모든 태그를 한꺼번에 원격 저장소에 업로드
$ git push origin --tags
반응형