All Articles

Interactive Git Rebase

저번 포스팅에서 rebase 가 무엇이고, 어떻게 활용하는지 알아보았습니다. 간단하게 정리해보자면, 브랜치를 병합하는 merge와는 또 다른 방법으로 각 커밋들의 변경 사항들을 차례대로 basebranch에 반영하여 결과적으로 선형적인 커밋 히스토리를 만들어 내는 기능이었습니다. 이번 포스팅에서는 이러한 작업을 대화형(interactive) 인터페이스를 제공함으로써 좀 더 정교한 작업을 가능하게 해주는 -i 옵션에 대해 알아보도록 합시다.

git rebase -i

git-scm에서는 -i 옵션에 대해 다음과 같이 설명하고 있습니다.

Make a list of the commits which are about to be rebased. Let the user edit that list before rebasing. This mode can also be used to split commits

직역하면 rebase 될 커밋들의 리스트를 사용자가 편집할 수 있게 해 준다는 뜻입니다. 아직 그 개념이 잘 잡히지 않는데, 다음과 같은 Git 환경을 만들고 실습하면서 더 자세히 알아보도록 해봅시다. 현재 Git 프로젝트의 커밋 히스토리가 다음과 같다고 가정해보죠.

현재 커밋 히스토리. Add apple, Add banana, Add cherry 를 순서대로 추가한 모습.
현재 커밋 히스토리. Add apple, Add banana, Add cherry 를 순서대로 추가한 모습.

이제 이 커밋 리스트를 rebase 해보도록 해봅시다. 이때 rebase 할 커밋의 범위를 지정해야 하는데, 이를 일반화한 명령은 다음과 같습니다.

git rebase -i <작업할 커밋의 직전커밋>

이 말의 의미는 git rebase -i 의 파라미터로 주어진 커밋의 바로 다음 커밋부터 HEAD까지 rebase를 진행한다는 뜻입니다. 만약 HEAD~3을 파라미터로 준다면 HEAD~2, HEAD~1, HEAD를 rebase 하겠다는 의미입니다. HEAD~3을 파라미터로 준 결과 다음과 같이 편집기가 실행됩니다.

git rebase -i 명령으로 편집기가 실행된 모습.
git rebase -i 명령으로 편집기가 실행된 모습.

편집기의 상단에 rebase할 커밋들의 리스트가 나타납니다. 아래에는 주석으로 몇 가지 명령어들이 나열돼 있는데, 이 명령들을 활용하여 커밋들을 자신의 입맛대로 rebase 할 수 있습니다. 지금부터 각 명령어들의 기능이 무엇인지 알아보도록 해봅시다.

pick

pick 은 고른다, 선택한다는 의미로 해당 커밋을 히스토리에 반영하겠다는 말인데요. 현재 기본적으로 모든 커밋들이 pick 된 상태로 히스토리에 존재하고 있습니다.

현재 커밋 히스토리
현재 커밋 히스토리

이 상태에서 커밋의 순서를 바꿀 수도 있고, 해쉬값을 이용하여 커밋을 가져올 수도 있습니다. 2번째(Add banana)와 3번째(Add cherry) 커밋 순서를 서로 바꿔보도록 해볼까요?

banana와 cherry 가 바뀐 커밋 리스트
banana와 cherry 의 순서가 서로 바뀐 커밋 리스트

저장하고 종료한 뒤 로그를 살펴보면 변경된 내용으로 커밋 히스토리가 작성되어 있는 걸 확인할 수 있습니다.

순서가 바뀐게 적용된 커밋 히스토리
순서가 바뀐게 적용된 커밋 히스토리

여기서 주목할 것은, 맨 처음 커밋 히스토리와 비교해서 보면 Add bananaAdd cherry커밋 해쉬값이 둘 다 변경된 것을 확인할 수 있습니다. 이는 내용은 같지만 완전히 새로운 커밋이란 걸 의미합니다.

앞에서 커밋의 해쉬값을 이용해 커밋을 가져올 수 있다고 하였는데, 이를 테스트하기 위해 새로운 커밋을 작성하도록 해보겠습니다. Add dragonFruit 이란 커밋을 새롭게 생성해 보았습니다.

새롭게 Add dragonFruit 커밋이 추가된 커밋 히스토리
새롭게 Add dragonFruit 커밋이 추가된 커밋 히스토리

지금 제가 하고자 하는 것은 방금 생성한 커밋을 git reset 명령으로 되돌린 뒤 rebase의 작업 목록에 해당 커밋 해쉬값을 가져와서 히스토리에 추가하는 것입니다. 아래 명령을 실행해서 방금 추가한 커밋을 삭제해주세요.

git reset --hard HEAD~

로그를 다시 확인해보면 방금 생성한 커밋이 삭제된 걸 확인할 수 있습니다. 이제 git rebase -i HEAD~3 명령으로 편집창을 실행시켜 봅시다. 조금 전에 생성한 커밋의 해쉬값은 d3816e0 이므로 이를 pick 하는 줄을 추가해줍시다(해쉬값이 기억이 안 난다면 git reflog 명령으로 확인할 수 있습니다). 저는 Add bananaAdd cherry 커밋 사이에 추가해보았습니다.

dragonFruit를 추가한 rebase 작업 목록
dragonFruit를 추가한 rebase 작업 목록

이렇게 rebase 작업 목록을 설정하고 저장한 뒤 커밋 히스토리를 확인해보면 삭제했던 커밋이 히스토리에 추가된 것을 확인할 수 있습니다. 또한 커밋 해쉬값 역시 변경된 것을 확인할 수 있죠.

dragonFruit가 추가된 커밋 히스토리
dragonFruit가 추가된 커밋 히스토리

reword

reword는 커밋의 메시지를 변경하는 명령입니다. 메시지를 변경할 커밋의 해쉬값 앞에 reword 키워드를 적어주면 해당 커밋의 메시지를 다시 작성하는 창이 열립니다. 여기서는 Add apple 커밋의 커밋 메시지를 Add apple^.^로 변경해보도록 하겠습니다.

Add apple의 커밋의 rebase 명령어를 reword 로 바꾼 모습
Add apple의 커밋의 rebase 명령어를 reword 로 바꾼 모습

저장하고 종료하면 Add apple 커밋의 메시지를 변경할 수 있는 편집창이 열립니다. 메시지를 변경하고 저장하면 커밋 히스토리에서 변경된 커밋 메시지를 확인할 수 있습니다.

Add apple 커밋의 메시지가 변경된 커밋 히스토리
Add apple 커밋의 메시지가 변경된 커밋 히스토리

단순히 HEAD 커밋의 메시지를 변경하고 싶으면 git commit --amend 명령을 사용해도 됩니다.

edit

reword 가 메시지만 변경하는 작업이었다면 edit 은 커밋의 작업한 내용까지 변경할 수 있는 명령입니다. 여기서 저는 git rebase -i HEAD~4 명령을 실행한 후 dragonFruit 커밋의 명령을 edit으로 바꿔주었습니다.

Add dragonFruit 커밋을 edit 명령하는 모습
Add dragonFruit 커밋을 edit 명령하는 모습

저장하고 종료하면 해당 커밋으로 checkout 되고, 작업을 수행할 수 있게 됩니다. 저는 dragonFruit.txt를 삭제하고 donut.txt를 추가해 주었습니다.

dragonFruit.txt 를 삭제하고 donut.txt 를 추가한 상태
dragonFruit.txt 를 삭제하고 donut.txt 를 추가한 상태

그리고 다음 명령을 실행하여 커밋을 완료해줍시다.

git add .
git commit --amend

커밋 메시지도 변경하고 싶다면 마지막 명령 이후에 열리는 편집창에서 변경하면 됩니다. 저는 Add donut으로 커밋 메시지를 변경해 주었습니다. rebase를 계속하려면 다음 명령을 실행해주면 됩니다.

git rebase --continue

마지막으로 커밋 히스토리를 확인해보면 다음과 같이 Add donut 커밋이 Add dragonFruit 대신 히스토리에 나타나는 것을 확인할 수 있습니다.

Add dragonFruit 커밋이 Add donut 커밋으로 변경된 커밋 히스토리
Add dragonFruit 커밋이 Add donut 커밋으로 변경된 커밋 히스토리

squash

squash는 해당 커밋을 이전의 커밋과 하나로 합치는 명령입니다. 즉, 여기서 Add donut 커밋에 squash 명령을 적어주면 이전 커밋인 Add cherry와 하나로 합쳐집니다.

Add donut 커밋을 squash 하는 rebase 작업 목록
Add donut 커밋을 squash 하는 rebase 작업 목록

저장하고 종료하면 커밋 메시지를 변경할 수 있는 편집창이 나타납니다. 그대로 저장하면 기존의 메시지가 그대로 하나의 커밋으로 히스토리에 나타나는 것을 확인할 수 있습니다.

Add cherry와 Add donut의 커밋이 합쳐진 커밋 히스토리
Add cherry와 Add donut의 커밋이 합쳐진 커밋 히스토리

fixup

fixupsquash와 동일하게 이전 커밋과 합치는 명령입니다. squash와 다른 점은 메시지는 합치지 않는다는 것으로, 이전 커밋 메시지만 히스토리에 남게 됩니다. 이 점을 제외하곤 squash와 동일합니다.

exec

exec 명령은 각 커밋이 rebase 된 후 실행될 쉘 명령을 지정할 수 있습니다. 이해를 돕기 위해 저는 다음과 같이 각 커밋 리스트 사이에 exec 명령을 추가해서 커밋의 해쉬값과 메시지를 콘솔에 출력하도록 하였습니다.

각 커밋에 exec 명령을 추가한 커밋 리스트
각 커밋에 exec 명령을 추가한 커밋 리스트

저장하고 종료하면 다음과 같은 결과를 출력합니다.

rebase 되면서 exec 명령이 실행된 모습
rebase 되면서 exec 명령이 실행된 모습

drop

drop 은 커밋 히스토리에서 해당 커밋을 삭제하는 명령입니다. 필자와 같이 편집창에서 커밋 앞에 drop 키워드를 적어주면 해당 커밋은 rebase 된 히스토리에 포함되지 않습니다.

drop 명령을 적어준 커밋 리스트
drop 명령을 적어준 커밋 리스트

저장하고 종료한 뒤 커밋 히스토리를 보면 해당 커밋이 삭제된 걸 확인할 수 있습니다.

cherry 커밋이 삭제된 커밋 히스토리
cherry 커밋이 삭제된 커밋 히스토리

사실 커밋 리스트 편집창에서 특정 커밋이 위치한 줄을 지워줘도 같은 기능을 하게 됩니다.

Recap

지금까지 git rebase -i 를 활용하는 방법과 관련된 명령어들을 알아보았습니다. 이번 포스팅에서 배운 내용을 정리해보죠.

> git rebase -i                # rebase 될 커밋 리스트를 직접 편집할 수 있게 해주는 명령입니다.
> git rebase -i commit_sha     # 주어진 해쉬값의 커밋 이후부터 HEAD까지 rebase 합니다.
> git commit --amend           # HEAD 커밋을 수정합니다. 또한 rebase 중 커밋의 편집을 완료합니다.
> git rebase --continue`       # 커밋을 편집한 뒤 rebase를 계속합니다.

또한 커밋 리스트를 편집하는 여러 rebase 명령어들을 알아보았는데, 각 명령어들의 기능은 다음과 같습니다.

  • pick - 해당 커밋을 히스토리에 넣습니다.
  • reword - 해당 커밋의 메시지를 변경합니다.
  • edit - 해당 커밋의 메시지와 작업 내용을 변경합니다.
  • squash - 해당 커밋을 이전 커밋과 하나로 합칩니다.
  • fixup - 해당 커밋을 이전 커밋과 하나로 합칩니다. 단, 메시지는 이전 커밋의 메시지로 합쳐집니다.
  • exec - 쉘 명령을 실행합니다.
  • drop - 커밋을 히스토리에서 삭제합니다.

처음에는 rebase의 개념이 잘 잡히지 않고 사용하기도 꺼려지지만 조금만 연습해보면 브랜치를 관리할 때 정말 유용하게 사용할 수 있단 것을 알게 됩니다. 로컬에서 작업하고 커밋 히스토리를 정리하고 싶을 때 rebase가 유용하게 쓰이고, -i 옵션을 이용해서 원하는 작업을 수행할 수 있을 것입니다.

References