본문으로 바로가기

Git Rebase 제대로 알고 쓰기 (feat. cherry-pick)

category Git 2020. 3. 3. 12:51

Git에서 한 브랜치에서 다른 브랜치로 합치는 방법으로는 두 가지가 있습니다.
하나는 Merge 이고, 다른 하나는 Rebase 입니다.
이번에는 Rebase가 무엇인지, 어떻게 사용하는지, 좋은 점은 뭐고, 어떤 상황에서 사용하고 어떤 상황에서 사용하지 말아야 하는지 알아보겠습니다.


1. Rebase 기초

예제를 하나 보겠습니다. 두 개의 나누어진 브랜치의 모습을 볼 수 있습니다.

그림 1. 두 개의 브랜치로 나누어진 커밋 히스토리

 

이 두 브랜치를 합치는 가장 쉬운 방법은 merge 명령을 사용하는 것입니다.
두 브랜치의 마지막 커밋 두 개(C3, C4)와 공통 조상(C2)을 사용하는 3-way Merge 로 새로운 커밋을 만들어 냅니다.

그림 2. 나뉜 브랜치를 Merge 하기

 

[그림 2]가 merge 의 결과라면 이번에는 rebase 방법을 살펴보겠습니다.
비슷한 결과를 만드는 다른 방식으로, C3 에서 변경된 사항을 Patch 로 만들고 이를 다시 C4 에 적용시키는 방법이 있습니다. Git 에서는 이런 방식을 Rebase 라고 합니다. rebase 명령으로 한 브랜치에서 변경된 사항을 다른 브랜치에 적용할 수 있습니다.

 

같은 예제를 아래와 같은 명령으로 Rebase 합니다.

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

실제로 일어나는 일을 설명하자면 일단 두 브랜치가 나뉘기 전인 공통 커밋으로 이동하고 나서 그 커밋부터 지금 Checkout 한 브랜치가 가리키는 커밋까지 Diff 를 차례로 만들어 어딘가에 임시로 저장해 놓습니다.

 

Rebase 할 브랜치(예제 - experiment)가 합칠 브랜치(예제 - master)가 가리키는 커밋을 가리키게 하고 아까 저장해 놓았던 변경사항을 차례대로 적용합니다.

그림 3.  C4  의 변경사항을  C3  에 적용하는  Rebase  과정

 

그리고 나서 master 브랜치를 "Fast-forward" 시킵니다.

$ git checkout master
$ git merge experiment

그림 4.  master  브랜치를 "Fast-forward" 시키기

 

C4' 로 표시된 커밋에서의 내용은 Merge 예제에서 살펴본 C5 커밋에서의 내용과 같을 것입니다. Merge 든 Rebase 든 둘 다 합치는 관점에서는 서로 다를 게 없습니다. 하지만, Rebase 가 좀 더 깨끗한 히스토리를 만듭니다. Rebase 한 브랜치의 Log 를 살펴보면 히스토리가 선형입니다. 일을 병렬로 동시에 진행해도 Rebase 하고 나면 모든 작업이 차례대로 수행된 것처럼 보입니다.

 

Rebase는 보통 리모트 브랜치에 커밋을 깔끔하게 적용하고 싶을 때 사용합니다. 아마 이렇게 Rebase 하는 리모트 브랜치는 직접 관리하는 것이 아니라 그냥 참여하는 브랜치일 것입니다.
메인 프로젝트에 Patch 를 보낼 준비가 되면 하는 것이 Rebase 니까 브랜치에서 하던 일을 완전히 마치고 origin/master 로 Rebase 합니다. 이렇게 Rebase 하고 나면 프로젝트 관리자는 어떠한 통합작업도 필요 없습니다. 그냥 master 브랜치를 "Fast-forward" 시키면 됩니다.

 

Rebase 를 하든지, Merge 를 하든지 최종 결과물은 같고 커밋 히스토리만 다르다는 것이 중요합니다. Rebase 의 경우는 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합치고 Merge 의 경우는 두 브랜치의 최종결과만을 가지고 합칩니다.

 

2. Rebase 활용

Rebase 는 단순히 브랜치를 합치는 것만 아니라 다른 용도로도 사용할 수 있습니다.

 

아래와 같이 여러 갈래로 갈라져 나온 브랜치 같은 히스토리가 있다고 하겠습니다.

server 브랜치를 만들어서 서버 기능을 추가하고 그 브랜치에서 다시 client 브랜치를 만들어 클라이언트 기능을 추가합니다. 마지막으로 server 브랜치로 돌아가서 몇 가지 기능을 더 추가합니다.

그림 5. 다른 브랜치에서 갈라져 나온 브랜치들

 

이때 테스트가 덜 된 server 브랜치는 그대로 두고 client 브랜치만 master 로 합치려는 상황을 생각해보겠습니다. server 와는 아무 관련이 없는 client 커밋은 C8, C9 입니다. 이 두 커밋을 master 브랜치에 적용하기 위해서--onto 옵션을 사용하여 아래와 같은 명령을 실행합니다.

$ git rebase --onto master server client

이 명령은 master 브랜치부터 server 브랜치와 client 브랜치의 공통 조상까지의 커밋을 client 브랜치에서 없애고 싶을 때 사용합니다. client 브랜치에서만 변경된 패치를 만들어 master 브랜치에서 client 브랜치를 기반으로 새로 만들어 적용합니다.

그림 6. 다른 브랜치에서 갈라져 나온  client  브랜치를 Rebase 하기

 

이제 master 브랜치로 돌아가서 "Fast-forward" 시킬 수 있습니다.
(참고: [master 브랜치를 client 브랜치 위치로 이동시키기])

$ git checkout master
$ git merge client

그림 7.  master  브랜치를  client  브랜치 위치로 진행 시키기

 

server 브랜치의 일이 다 끝나면 git rebase <base_branch> <topic_branch> 라는 명령으로 Checkout 하지 않고 바로 server 브랜치를 master 브랜치로 Rebase 할 수 있습니다. 이 명령을 토픽 브랜치(예제 - server)를 Checkout 하고 베이스 브랜치(예제 - master) 브랜치에 Rebase 합니다.

$ git rebase master server

server 브랜치의 수정사항을 master 브랜치에 적용했습니다. 그 결과는 아래와 같습니다.

그림 8.  master  브랜치에  server  브랜치의 수정 사항을 적용

 

이제 마지막으로 master 브랜치를 "Fast-forward" 시키겠습니다.
그리고서 더이상 필요하지 않게된 clienserver 브랜치는 삭제해도 됩니다. 브랜치를 삭제해도 커밋 히스토리는 여전히 남아있습니다.

$ git checkout master
$ git merge server
$ git branch -d client
$ git branch -d server

그림 9. 최종 커밋 히스토리

 

3. Rebase 의 위험성

Rebase 가 장점이 많은 기능이지만 단점이 없는 것은 아니니 조심해야 합니다.

주의사항은 한 문장으로 표현하면 아래와 같습니다.

"Do not rebase commits that exist outside your repository and that people may have based work on."
"다른 동료가 작업 중인 외부에 공개 된 저장소 브랜치를 대상으로 리베이스하면 안됩니다."

Rebase는 기존의 커밋을 그대로 사용하는 것이 아니라 내용은 같지만 다른 커밋을 새로 만듭니다.

새 커밋을 서버에 Push 하고 동료 중 누군가가 그 커밋을 Pull 해서 작업을 한다고 해보겠습니다. 그런데 그 커밋을 git rebase 로 바꿔서 Push 해버리면 동료가 다시 Push 했을 때 동료는 다시 Merge 해야 합니다. 그리고 동료가 다시 Merge 한 내용을 Pull 하면 내 코드는 정말 엉망이 됩니다.

 

이미 공개 저장소에 Push 한 커밋을 Rebase 하면 어떤 결과가 초래되는지 예제를 통해 알아보겠습니다. 중앙 저장소에서 Clone 하고 일부 수정을 하면 커밋 히스토리는 아래와 같아 집니다.

그림 10. 저장소를 clone 하고 일부 수정함

 

이제 팀원 중 누군가 commit, Merge 하고 나서 서버에 Push 합니다.
이 리모트 브랜치를 Fetch, Merge 하면 히스토리는 아래와 같이 됩니다.

그림 11. Fetch 한 후 Merge

 

근데 이때 Push 했던 팀원은 Merge 한 일을 되돌리고 다시 Rebase 합니다. 서버의 히스토리를 새로 덮어씌우려면 git push --force 명령을 사용해야 합니다. 이후에 저장소에서 Fetch 하고 나면 아래 그림과 같은 상태가 됩니다.

그림 12. 한 팀원이 내가 의존하는 커밋을 없애고 Rebase 한 커밋을 다시 Push 함

 

이렇게 되면 완전 꼬입니다. git pull 로 서버의 내용을 가져와서 Merge 하면 같은 내용의 수정사항을 포함한 Merge 커밋이 아래와 같이 만들어집니다.

그림 13. 같은 Merge 를 다시 하게 됨

 

git log 로 히스토리를 확인해보면 저자, 커밋 날짜, 메시지가 같은 커밋이 두 개(C4, C4')가 있게 됩니다.
이렇게 되면 혼란을 야기합니다. 게다가 이 히스토리를 서버에 Push 하면 같은 커밋이 두 개 있기 때문에 다른 사람들도 혼란스러워 할 수 있습니다. [그림 13] 에서 C4C6 은 포함되지 말아야할 커밋입니다. 애초에 서버로 데이터를 보내기 전에 Rebase로 커밋을 정리했어야 합니다.

 

그렇다고 방법이 없는 것은 아닙니다.
위 경우 팀원이 내가 의존하고 있는 커밋을 없애고 Rebase 한 커밋을 다시 Push 한 상황에서 Merge 가 아니라 git rebase teamone/master 명령을 실행한다면 아래처럼 제대로 된 결과를 얻을 수 있습니다.

그림 14. Rebase 한 것을 다시 Rebase 하기

 

그렇다 하더라도 리모트 등 어딘가에 Push로 내보낸 커밋에 대해서는 절대 Rebase 하지 말아야 합니다.

 

4. Rebase 와 Cherry-Pick

히스토리를 한 줄로 관리하려고 Merge 보다 Rebase 나 Cherry-Pick을 더 선호하는 개발자들이 많이 있습니다.

 

작업 브랜치에서 작업을 마친 후 master 브랜치에 Merge 할 때 master 브랜치를 기반으로 Rebase 합니다. 그러면 커밋이 다시 만들어 집니다. master 대신 develop 등의 브랜치에도 가능합니다. 문제가 없으면 master 브랜치를 "Fast-forward" 시킵니다. 이렇게 히스토리를 한 줄로 유지할 수 있습니다.

 

한 브랜치에서 다른 브랜치로 작업한 내용을 옮기는 또 다른 방식으로 Cherry-pick 이란 것도 있습니다.
Git의 cherry-pick 은 커밋 하나만 Rebase 하는 것입니다. 커밋 하나로 Patch 내용을 만들어 현재 브랜치에 적용을 하는 것입니다. 토픽 브랜치에 있는 커밋 중에서 하나만 고르거나 토픽 브랜치에 커밋이 하나밖에 없을 때 Rebase 보다 유용합니다. 아래와 같은 예를 들어보겠습니다.

그림 15.  cherry-pick  하기 전의 저장

 

e43a6 커밋 하나만 master 브랜치에 적용하려면 아래와 같은 명령을 실행합니다.

$ git cherry-pick e43a6
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
 3 files changed, 17 insertions(+), 3 deletions(-)

위 명령을 실행하면 e43a6 커밋에서 변경된 내용을 현재 브랜치에 똑같이 적용을 합니다. 하지만, 변경을 적용한 시점이 다르므로 새 커밋의 SHA-1 해시값은 달라집니다. 명령을 실행하고 나면 아래와 같이 됩니다.

그림 16.  cherry-pick  방식으로 커밋 하나를 적용한 후의 저장소

 

Rebase 나 Cherry-pick 방식으로 토픽 브랜치를 합치고 나면 필요없는 토픽 브랜치나 커밋은 삭제합니다.

5. Merge 되돌리기

지금까지 Merge 하는 방법을 배웠습니다. 그러나 Merge 할 때 실수할 수도 있습니다.
Git에서는 실수해도 됩니다. 실수해도 되돌릴 수 있습니다.

Merge 커밋도 예외는 아닙니다. 토픽 브랜치에서 일을 하다가 master 로 잘못 Merge 했다고 생각해보겠습니다. 커밋 히스토리는 아래와 같습니다.

그림 17. 우발적인 Merge

 

접근 방식은 원하는 결과에 따라 두 가지로 나눌 수 있습니다.

  • Refs 수정

    실수로 생긴 Merge 커밋이 로컬 저장소에만 있을 때는 브랜치를 원하는 커밋을 가리키도록 옮기는 것이 쉽고 빠릅니다. 이에 대해서는 git reset --hard HEAD~ 명령으로 브랜치를 되돌리면 됩니다.
    (참고 : Undoing Things )

  • 커밋 되돌리기

    브랜치를 옮기는 것을 할 수 없는 경우는 모든 변경사항을 취소하고 새로운 커밋을 만들 수도 있습니다.
    Git에서 이 기능을 "revert" 라 부릅니다. git revert 명령은 git cherry-pick 명령의 반대로 볼 수 있습니다.


출처 - 저서 ProGit


댓글을 달아 주세요