커밋들을 자유롭게 넘나드는 방법 - git rebase
git add
, git commit
, git push
제외하고 자주 쓰는 git command는 당연 git rebase
다. 왜냐하면 팀에서 fast-forward merge
가 규칙이기 때문이다. fast-forward merge
가 되면 커밋을 일렬(linear)로 유지할 수 있다. (간단하게 머지 커밋을 남기지 않는 머지 전략이다.)
또 다른 경우는 커밋을 수정하고 싶을 때다. 일련의 커밋들 사이에 마음에 들지 않은 커밋 메시지가 있다든가, 해당 커밋의 작업을 수정하고 싶다든가 할 때 사용한다. 본 글을 읽으면 다음 상황에 대응할 수 있다.
git history
를 일렬로 관리하고 싶다.- 커밋들 일부를 합칠 수 있다.
- 특정 커밋 이름을 변경할 수 있다.
- 특정 커밋 작업을 변경할 수 있다.
- 특정 커밋 작업을 삭제할 수 있다.
git rebase
의 위험성을 알고 있다.
그렇다면 git rebase
의 동작원리와 git rebase --interactive
옵션에 대해 알아보자.
git rebase 동작 원리
표준 모드의 Git 리베이스는 현재 작업 중인 브랜치의 커밋을 자동으로 가져와서 전달된 브랜치의 헤드로 적용합니다.
git rebase <타겟 브랜치>
는 현재 작업 중인 브랜치의 커밋들을 자동으로 가져와서 타겟 브랜치의 헤드로 옮기는 명령어다. 타겟 브랜치를 기반으로 현재 브랜치의 커밋들을 옮긴다.
두개의 브랜치가 있다고 가정하자.
master
feature
: master을 기반으로 만들어진 브랜치
이제 main과 feature 각각 작업이 추가되어서 git graph가 아래 이미지처럼 변경되었다. merge를 깔끔하게 하기 위해서 rebase를 사용하고 싶다. 그러면 어떤 브랜치가 타겟브랜치가 되어야 하는가?
정답은 master
이다. git checkout feature
로 이동한 후, git rebase master
을 실행해야 한다.
feature
브랜치의 커밋들이 master
브랜치 뒤로 옮겨진다. 옮기는 과정중에 merge-confilct처럼 충돌이 발생할 수 있다. 이 때는 충돌을 해결하고 git commit
을 다시 실행한다.
그 후 git rebase --continue
를 실행하면 된다.
만약 master
에서 git rebase feature
를 실행하면 feature
를 기반으로 master
의 커밋들을 옮긴다. master
의 커밋들이 feature
브랜치 뒤로 옮겨진다.
master
의 브랜치가 완전히 꼬여서 리모트 깃 저장소에 올릴 수 없는 상태가 된다.
정석적인 방법은 다음과 같다. 해당 방법으로 깃 히스토리를 일렬로 유지할 수 있다.
merge
와 rebase
의 차이는 다음과 같다. 아래 왼쪽 이미지는 머지(3-way-merge)의 예시다. fast-forward merge
가 아니라면 머지 커밋이 추가되며 머지된다.
이 경우에는 일렬로 히스토리가 유지되지 않는다. 하지만 오른쪽 이미지처럼 rebase
를 사용하면 무조건 fast-forward-merge
가 되어서 일렬로 히스토리가 유지된다.
git rebase --interactive 옵션
interactive
옵션을 사용하면 rebase를 더욱 세밀하게 조정할 수 있다. git rebase -i <수정할 커밋의 직전 커밋>
을 사용하면 된다.
base를 수정할 커밋의 직전 커밋으로 하고 그 이후의 commit들을 내 맘대로 수정할 수 있다. git rebase -i HEAD~3
을 실행하면 아래와 같은 창을 볼 수 있다.
rebase를 베이스로 사용한 커밋의 이후 커밋들이 오래된 순서로 정렬되어 나타난다.
커밋 SHA 앞에 있는 pick 키워드를 변경해서 커밋의 운명을 결정할 수 있다.
p,r,e,s,f,x 등등 여러 키워드가 있는데 rebase 창에서 주석으로 설명이 작성되어 있다.
키워드를 적고 입력창을 나가면 키워드가 p(pick)
이 아닌 커밋들로 이동해서 각 키워드에 맞게 작업을 진행한다.
만약 main1
커밋 메세지를 변경하고 싶다면 다음과 같이 변경해야 한다.
:wq
를 입력해서 창을 나가면, 커밋 메시지를 변경할 수 있는 창이 나타난다. 메시지를 변경하고 :wq
를 입력하면 rebase가 완료된다.
다시 돌아와서 fix: example 앱 빌드에러
와 main1
커밋을 합치고 싶다면 다음과 같이 변경해야 한다.
:wq
를 입력해서 창을 나가면 커밋 메시지를 변경할 수 있는 창이 나타나고 메시지를 적고 창을 나가면 두 커밋이 합쳐진 채로 rebase가 완료된다.
특정 커밋 작업을 삭제하고 싶을 때는 다음과 같이 변경한다. 창을 나가면 git rebase --continue
를 실행하지 않아도 rebase는 완료된다.
다음은 특정 커밋을 메시지도 수정하고 작업도 바꾸고 싶을 때는 아래와 같이 작성한다.
창을 나가면 아래와 같은 메시지가 나타난다.
여기서 세 가지 선택지가 있다.
- 현재 커밋에 새로운 작업 더하기
- 새로운 작업을 추가하고 새로운 커밋을 만들기
- 현재 커밋을 쪼개기
1번은 제일 간단하다. 작업을 한 후 git add
와 git commit --amend
를 실행하면 된다. 그리고 git rebase --continue
를 실행하면 끝난다.
2번은 새로운 작업을 추가하고 git add
와 git commit
을 실행하면 된다. 그리고 git rebase --continue
를 실행하면 끝난다.
마지막으로 현재 커밋을 쪼개려면 reset
명령어를 사용해야 한다. git reset HEAD~1
을 통해서 커밋을 되돌린다. 그 후 작업을 나뉘어서 git add
와 git commit
을 실행한다.
(같은 파일에서 일부만 add
해서 커밋하고 싶다면 git add -p
를 사용하면 된다.)
git rebase의 위험성
git rebase가 위험한 이유는 커밋들을 옮기면서 해시값이 모두 변경되기 때문이다. 깃은 전체 파일들을 보고 커밋 해시값을 결정한다. rebase
를 실행하면 기존 브랜치와는 파일들이 달라지므로 커밋 해시값도 달라지는 것이다.
이를 커밋이 유실된다고 말하기도 한다. rebase
를 실행한 후 원격저장소에 force push
를 해야 하는 것도 이 때문이다.
만약 rebase결과를 다시 돌리고 싶으면 어떻게 할까? git reflog
를 사용하면 된다. git reflog
는 로컬에서 발생한 깃 관련 히스토리들을 볼 수 있는 명령어다.
돌리고 싶은 커밋의 해시값을 찾아서 git reset --hard <해시값>
을 실행하면 된다.
하지만 더 위험한 것은 원격저장소에 코드를 push
했을 때다. 만약 해당 브랜치가 다른 사람과 함께 작업하는 브랜치였다면 골치 아픈 일이 발생한다. 다른 작업자가 git pull
을 실행할 때 충돌이 발생할 수 있다.
충돌하는 과정에서 작업 자체가 변경될 수 있다. 그리고 다른 작업자는 충돌을 해결하고 커밋을 추가해 또다시 push
한다. 이런 과정을 반복하면서 코드가 꼬일 수 있다.
어쩔 수 없이 원격브랜치에 push
할 때는 공동 작업자들과 충분히 협의하고 push
해야 한다.
마치며
사실 입사하고 나서 rebase
를 처음 알았다. 그전까지는 merge
만 사용했었다. 입사 전에는 혼자 개발했기 때문에 커밋 히스토리 관리의 필요성을 느끼지 못했다.
하지만 협업을 하면서 그 필요성을 느끼게 되었다. 커밋 히스토리가 일렬로 유지되지 않는다면 커밋 히스토리가 뚱뚱해져서 히스토리 파악이 어려워진다.
rebase
는 커밋히스토리를 깔끔하게 할 수 있다는 것을 넘어서 --interactive
속성을 통해서 커밋들을 자유롭게 넘나들 수 있게 해준다.
커밋을 쪼갤 수도 있고, 커밋을 삭제할 수 있고, 커밋 명만 변경할도 있다. 커밋을 넘나들 수 있는 무기가 생겼지만 모든 무기는 적절하게 사용해야 한다.
우리가 가진 무기의 위험성을 파악하고 적절한 때에 사용하자.