커밋 히스토리는 프로젝트의 성장과 진행 과정을 나타내는 기록입니다. 따라서 프로젝트의 규모가 커지고 협업하는 사람이 많아질수록 커밋 히스토리를 잘 관리해야 할 필요성은 더욱 커지게 됩니다. 하지만 처음부터 완벽한 계획을 세우면서 작업을 하는 게 아니다보니, 작업 도중 커밋 히스토리를 수정해야 하는 상황을 자주 맞닥뜨릴 수 있습니다. 구체적으로 예를 들자면 아래와 같은 경우가 있을 것 같습니다.
- 작업할 때는 몰랐는데, 알고보니 과거의 커밋 메시지 하나에 오타가 들어가 있네?
- 테스트 용도으로 작업한 커밋들이 딸려 들어갔네?
- 성격이 비슷한 커밋이 두 개로 분리되어 있는데, 이걸 합칠 수는 없을까?
- 아… 누가 보기 전에 수정하고 싶다!
이런 상황에서 사용할 수 있는 것이 바로 git rebase
명령어입니다. 일반적으로 리베이스라고 하면 두 개의 브랜치를 합치는 용도로 git merge
와 함께 쓰이는 것으로 알고 있으실 텐데요, 현재 작업중인 브랜치의 최신 커밋을 가리키는 HEAD 포인터를 이동시킬 수 있다는 특성을 이용해서 과거 커밋 히스토리를 수정하는 데에도 사용할 수 있습니다. 특히 그 중에서도 대화형으로 실행할 수 있는 옵션인 --interactive
(또는 -i
) 를 이용한다면 더욱 쉽게 수정이 가능하죠.
그래서 오늘은 git rebase --interactive
옵션을 이용해 커밋 히스토리를 수정할 수 있는 활용 방법에 대해 정리해보고자 합니다. 이 포스트를 통해 Git Rebase를 활용한 커밋 로그 수정 방법이 궁금하신 분들께 도움이 되기를 바랍니다.
준비 사항
저는 Github Desktop을 이용해 커밋 로그를 보여드리고자 합니다
저는 미리 다섯 개의 커밋이 들어있는 리포지터리를 생성했습니다. 우선 git rebase -i
의 사용법은 터미널에서 다음과 같이 입력하는 것으로부터 시작됩니다.
git rebase -i ${수정할 커밋의 직전 커밋}
사실 해당 명령어 뒤에 베이스 역할을 하는 브랜치 이름을 붙여 리베이스 절차를 진행할 수도 있지만, 이번 포스트에서는 커밋 히스토리의 수정에 대해서만 집중합니다.
리베이스할 커밋의 직전 커밋에는 내가 수정하고 싶은 커밋의 바로 직전 커밋을 입력하면 됩니다. 즉, 세 번째 커밋을 수정하고 싶다면 두 번째 커밋을 넣으면 됩니다. 이 때 커밋 해시를 넣는 방법도 가능하고, HEAD를 기준으로 입력할 수도 있습니다.
# 커밋 해시를 이용한 방법
git rebase -i 9d9cde8
# HEAD를 이용한 방법
git rebase -i HEAD~3
이렇게 입력하게 되면, 터미널에서 다음처럼 출력되는 vim 에디터를 볼 수 있습니다.
내용을 잠깐 살펴볼까요? 일반적인 로그 메시지가 아래에서부터 위로 출력되는 것에 반해, 여기에서는 위에서부터 아래로 커밋 순서가 출력되어 있는 것을 확인하실 수 있습니다. 그리고 각 라인은 [명령어] [커밋 해시] [커밋 메시지]
순서대로 구성되어 있죠. vim을 종료시킬 때, Git은 각 커밋에 적용시킬 명령어들을 읽은 후, 스크립트를 한 줄씩 실행시킵니다.
아래에는 주석이 있는데 Command, 즉 각 커밋에 사용할 수 있는 명령어들의 목록을 확인할 수 있습니다. 특정 커밋에 명령어를 사용하기 위해서는 직접 vim 에디터로 커밋 해시 앞의 명령어를 수정해야 합니다. 만약 위의 사진에서 네 번째 커밋에 한해 edit
명령어를 적용시키고 싶다면, 아래처럼 수정하면 됩니다.
vim 에디터에서 메시지를 수정하기 위해서는
i
키를 눌러 INSERT 모드로 들어간 후, 메시지를 수정하고:wq
를 입력해 저장 후 종료할 수 있습니다.
지금부터는 각 명령어들이 어떤 역할을 하는지에 대해 알아보도록 할게요.
pick
p, pick
<commit>
= use commit
pick
또는 p
는 해당 커밋을 수정하지 않고 그냥 사용하겠다 라는 명령어입니다. 디폴트로 실행되는 명령어이므로 vim에서 내용을 편집하지 않고 종료한다면 아무런 변경 사항 없이 리베이스가 종료됩니다.
이런 특성을 가진 pick
을 이용해서 커밋 순서을 재정렬하거나, 아예 커밋을 삭제하는 용도로 사용할 수도 있습니다.
세 번째 커밋과 네 번째 커밋의 순서를 변경한 상태로, vim을 종료해보았습니다.
커밋의 순서가 변경된 것을 확인할 수 있습니다.
해당 커밋이 포함된 라인을 지운 후, vim을 종료하면 간단하게 커밋이 삭제됩니다.
해당 커밋이 히스토리에서 사라진 것을 확인할 수 있습니다.
다만 수정한 커밋 히스토리가 서로 의존성을 갖고 있는 경우에는 컨플릭트가 발생할 수 있기 때문에, 이를 위한 별도의 처리가 필요하다는 점 주의해주세요.
reword
r, reword
<commit>
= use commit, but edit the commit message
reword
또는 r
는 커밋 메시지를 수정하기 위한 명령어입니다.
네 번째 커밋의 커밋 메시지를 변경해보도록 하겠습니다. reword
명령어를 입력한 후 vim을 종료하면,
이처럼 커밋 메시지를 vim에서 수정할 수 있게 됩니다.
저는 커밋 제목에 (reword로 수정)
이라는 단어를 더 붙여서 저장해볼게요.
커밋 히스토리에서도 수정된 커밋 메시지가 잘 나타나는 것을 확인할 수 있습니다.
edit
e, edit
<commit>
= use commit, but stop for amending
edit
또는 e
는 커밋의 명령어 뿐만 아니라 작업 내용도 수정할 수 있게 하는 명령어입니다. 아래 예제에서는 커밋 메시지와 작업 내용을 수정하고, 그와 동시에 하나의 커밋을 두 개로 분리하거나 커밋을 끼워넣는 과정을 보여드리도록 하겠습니다.
이전 예시에서 사용한 커밋을 수정해보도록 하겠습니다. 명령어 edit
을 이용해보면…
해당 커밋으로 HEAD가 옮겨진 것을 확인할 수 있습니다. Git에서 커밋 메시지를 수정하려면 git commit --amend
를, 수정을 완료했다면 git rebase --continue
명령어를 입력하라고 친절하게 설명해주고 있네요. 우선 커밋 메시지를 수정하면 어떻게 나오는지 보기 위해 git commit --amend
를 입력해봅니다.
아까와 reword
명령어와 마찬가지로, 커밋 메시지를 수정할 수 있는 창이 뜹니다. 동일한 방식으로 커밋 메시지를 수정하면 되니, 그 이후의 과정은 생략하도록 하겠습니다.
현재 네 번째 커밋에서 작업 중인데, 현재 커밋과 다섯 번째 커밋 사이에 추가적인 작업을 해 보도록 하겠습니다. 우선 저는 4와 ½ 번째 커밋(…)을 새로 만들었습니다.
새 텍스트 파일을 만든 후, git add
와 git commit -m message
명령어를 이용해 새 커밋을 추가했습니다.
새 작업이 네 번째 커밋 뒤에 추가된 것을 확인할 수 있습니다.
이제 네 번째 커밋에서는 할 일이 끝났으니, git rebase --continue
로 진행 중인 리베이스 과정을 종료해봅니다.
네 번째 커밋과 다섯 번째 커밋 사이에 추가된 4와 ½ 번째 커밋을 확인하실 수 있습니다. 같은 과정으로, 네 번째 커밋 역시 수정이 가능합니다.
squash, fixup
s, squash
<commit>
= use commit, but meld into previous commitf, fixup
<commit>
= like “squash”, but discard this commit’s log message
squash
와 s
, fixup
과 f
는 해당 커밋을 이전 커밋과 합치는 명령어입니다. 보통 PR을 머지할 때 스쿼시 머지하는 옵션이 있어서 이 워딩은 좀 익숙하신 분들이 계실 것 같아요.
다만 두 명령어 사이에는 차이점이 있습니다. squash
는 각 커밋들의 메시지가 합쳐지는 반면, fixup
은 이전의 커밋 메시지만 남기는 차이점이 있습니다.
지금부터는 squash
를 이용해 위에서 만들었던 4와 ½ 번째 커밋을 다시 네 번째 커밋으로 합쳐보도록 하겠습니다.
4와 ½ 번째 커밋에 squash
명령어를 적용시켰습니다.
네 번째 커밋과 4와 ½ 번째 커밋의 메시지를 확인하실 수 있습니다. 필요에 따라 커밋 메시지를 수정할 수도 있습니다.
vim을 종료하게 되면, 두 커밋이 합쳐진 것을 확인할 수 있습니다. 4와 ½ 번째 커밋의 메시지는 commit description 으로 들어간 것 역시 확인할 수 있습니다.
exec
x, exec
<command>
= run command (the rest of the line) using shell
exec
또는 x
는 리베이스 도중에 실행할 쉘 커맨드를 입력할 수 있게 해주는 명령어입니다.
예시로, 저는 직전 커밋의 메시지만을 출력하는 커맨드(git log -1 --pretty=format:%B
)를 한 번 각 스크립트 사이에 넣어보았습니다.
Executing
으로 실행할 명령어가 나오고, 결과가 정상적으로 잘 출력되는 것을 확인할 수 있습니다.
break
b, break = stop here (continue rebase later with ‘git rebase —continue’)
break
또는 b
는 그냥 말 그대로 해당 라인에서 리베이스를 일시중지하는 명령어입니다.
너무 간단해서 딱히 설명드릴 게 없네요. 그래도 예시를 보여드리자면, 이렇게 스크립트 사이에 break
를 넣어주면
이렇게 직전 커밋까지 리베이스를 마친 후 일시중지한 상태가 되며
현재 HEAD의 상태를 커밋 히스토리에서도 확인할 수 있습니다. 다시 재개하려면 git rebase --continue
를 입력하면 됩니다. 이건 사실 그렇게 쓸 일이 많을지 잘 모르겠네요.
drop
d, drop
<commit>
= remove commit
drop
또는 d
는 해당 커밋을 명시적으로 삭제하는 명령어입니다. 위에서 pick
명령어로 삭제하는 것과 동일한 결과물이 나옵니다.
네 번째 커밋과 여섯 번째 커밋 앞에 drop
명령어를 붙이면
이렇게 흔적도 없이 커밋이 사라집니다.
merge
m, merge [-C
<commit>
or -c<commit>
]<label>
[#<oneline>
]create a merge commit using the original merge commit’s
message (or the oneline, if no original merge commit was
merge
또는 m
옵션은 머지 커밋을 만들면서 머지하는 명령어입니다.
다른 브랜치에서 작업한 ad6b4ca
커밋을 가져와서, 리베이스 마지막 단계에 머지시켜보도록 하겠습니다.
요렇게 머지 커밋을 작성할 수 있게 vim 에디터가 열리게 되고,
커밋 로그에서도 남게 됩니다. 근데 사실 커밋 히스토리를 수정하는 데 있어서 꼭 필요한 옵션은 아니기 때문에, 이런 게 있구나 라는 정도로만 알아두시면 좋을 거 같습니다.
주의사항
협업하는 과정에서 이미 원격 리포지터리에 푸시된 커밋 히스토리를 로컬에서 리베이스하여 다시 푸시하는 일은 지양해야 합니다. 왜냐하면 리베이스는 기존의 커밋을 재사용하는 것이 아니라, 내용이 같은 커밋을 새로 만들기 때문입니다.
이 때문에 원격 브랜치를 베이스로 작업하고 있던 동료들의 커밋 히스토리가 와장창 깨질 수도 있게 되죠. 특히 함께 협업하는 브랜치에서 --force
옵션으로 푸시해버린다면, 자칫 동료의 작업본을 날려먹을수도 있으니 주의해야겠죠.
이러한 주의사항만 잘 지킨다면, 커밋 히스토리를 넘나들 수 있는 Git 고수에 한 걸음 더 가까워질 수 있을 것 같습니다.