git rebase: 브랜치 이력을 확인하면서 병합합니다.


이전 블로그 포스팅에서 살펴본 병합과정은 사실 자주 실행하는 작업이 아닙니다. 실제git을 이용한 버전 고나리시스템의 작업흐름은 평소에는 여러개의 branch와 commit 내역을 만들고, 마지막에 작업내역을 확인하고 올바른 작업물만 병합하는 것입니다. 

 git의 특징 중 하나는 commit내역을 수정할수 있다는 것입니다. 하지만 수정할 수 있다고해서 이미 원격 저장소에 push가 끝난 commit내역을 수정하는 것은 특별한 상황이 아닌 이상 절대로 권장할만한 일이 아닙니다.


 push 하기전에 git merge명령을 이용해서 병합하면, 충돌해결 커밋이나, --no-ff로 만든 병합 commit을 남기게 됩니다 .이는 작업 흐름을 일관되게 파악하는데 깔끔하지 않습니다. 따라서 할 수 있다면 로컬 저장소에 있던 commit을 깔끔하게 정리해서 push하는것이 좋습니다.


 그런 정리를 할 수 있는것이바로 git rebase입니다. 여기서는 언제 git rebase를 사용하는것을 고려해야하는지, 그리고 commit내역을 깔끔하게 정리함으로써 얻을 수 있는 장점을 예와함께 살펴보도록 하겠습니다.



아래 그래프는 일반적으로 병합했을때의 모습입니다. 이전 블로그에서 작업했던 저장소의 모습과도 같습니다.


<일반적 작업 흐름의 그래프>




하지만 두개를 넘어서 세개 이상의 브랜치가 master 브랜치에 병합된다고 생각해보겠습니다. myBranch1을 만든 이후 master 브랜치에 어떤 commit내역이 생신 상태로 myBranch2, myBranch3를 만들어서 각각 commit을 했고, myBranch1에도 다른 commit 내역이 있는 상황입니다. 이를 차례대로 master 브랜치에 병합한다고 가정해봅시다.

<세개 이상의 브랜치 병합 시>

.


그럼 이것들을 합쳐야겠지요? 먼저 myBranch1을 master 브랜치와 병합니다.그리고 myBranch2를 maser와 병합합니다. commit 그래프가 꼬이기 시작합니다. 마지막으로 myBranch3을 병합합니다. 아래는 최종형태입니다.


프로젝트 멤버가 세명 이상이면 혹은 동시에 개발중이 기능이 여러개라면 branch가 세개 이상으로 생성되는 일은 매우 흔한 상황입니다. 그럴때마다 각자의 코드를 master브랜치에 반영하면 commit 내역 그래프가 매우 알아보기 어려울 것입니다. 실제로 작업한 내역을 git log --graph 명령을 실행하면, 계속 충돌을 해결해야하는 모습을 볼 수 있습니다. 하지만 git rebase 명령을 사용하면 이를 깔끔하게 정리할 수 있습니다.


실습을 위해 다음과 같이 구성해보도록 하겠습니다. myRebase라는 디렉토리를 만들고 다음과 같이 master와 myBranch123 브랜치들과 커밋내역을 만들도록 합시다.



branch             file               code               message

master         hello.py    print("Hello World")      Hello world

                                print("Hello World2")    Hello world2


myBranch1  hello.py     print("hello World")     Tell your World

                               print("Tell your world")  


myBranch2  hello.py    print("Hello World")     Tell His world

                              print("Hello World2")

  print("Tell His world")


myBranch3  hello.py   print("Hello World")     Tell Her world

                              print("Hello World2")

                             print("Tell Herworld")



$mkdir myRebase

$cd myRebase

$git init


$vim hello.py, 파일내용 추가합니다. print("Hello World")

$git add hello.py

$git commit -a -m "Hello world"


myBranch1을 땁니다.


 git branch myBranch1

$ vim hello.py      Tell your World

$ git add hello.py

$ git commit -a "Tell your World"


다시 master 브랜치로와서 hello world2 print 문 추가해주고 commit해줍시다.

$git commit -a -m "Hello world2"


자 다시 마스터 브랜치에서 브랜치를 땁니다. 2개를 만들어야하네요.

 git branch myBranch2

 git branch myBranch3

그리고 각각 브랜치를 이동하면서 Tell his world, tell her world를 hello.py에 추가하고 commit에도 같이 수정해줍시다.



이런 모양이 되겠지요?


먼저 myBranch1 브랜치부터 정리해보도록 하겠습니다.  master Branch앞으로 myBranch1 브랜치를 이동시키는 짓을 하려고합니다. 따라서 git checout myBranch1 명령을 쳐서, myBranch1으로 브랜치 이동을 합시다. 그리고 git rebase master 명령을 실행해봅니다.


당연히 충돌이 납니다. 이름도 REBASE가 붙은것으로 바뀐것을 확인하실 수 있으십니다.


rebase명령은 아래와 같이 세가지 옵션을 제공합니다.


git rebase --continue: 충돌 상태를 해결한 후 계속 작업을 진행할 수 있도록 해줍니다.

git rebase --skip: 병합 대상 브랜치의 내용으로 강제 병합을 실행합니다. 여기서 명령을 실행하면 master 브랜치를 강제로 병합한 상태가 됩니다. 또한 해당 브랜치에서는 다시 git rebase명령을 실행할 수 없게 됩니다. 

git rebase --abort: git rebase명령을 취소합니다. 다시 git rebase myBranch1명령을 실행할 수 있게 됩니다.


어쨌든 현재 브랜치는 다음과 같이 (myBranch1|REBASE 1/1) 의 상태임을 알 수 있습니다.

PJH@PJH-PC MINGW64 /d/blog/git/myRebase (myBranch1|REBASE 1/1)


그럼 충돌을 해결해보도록 하겠습니다. vim hello.py을 실행해서 충돌내용을 수정합니다. 그리고 git add hello.py 실행합니다. 또한, 마지막으로 아래와 같이 충돌 상태를 해결하는 rebase명령을 내립시다.


$ git rebase --continue


명령을 실행하면 master branch의 공통 부모까지의 myBranch1 Branch commit을 master branch의 뒤에 차례대로 적용합니다. 다음 아래의 그림과 같이 전체 작업 흐름의 위치가 이동한다고 생각해보시면 됩니다. myBranch1 rebase into master라는 것입니다. 현재 myBranch1을 master로 다시 설정하게 된다는 의미로 파악하시면 되겠습니다.

즉 첫번째 git rebase명령을 실행했을때 작업흐름은 다음과 같습니다.

여기까지면 단순하게 myBranch1과 master 브랜치가 따로따로 있는 것에 불과하게 됩니다. master 브랜치는 파랑, myBranch1은 노랑으로 따로따로 있게 된 상태이죠. myBranch1의 base(기반, 밑부분)을 다시 re, 재설정한것과 같은 효과입니다. 병합해야만 비로소 master 브랜치에 myBranch1 브랜치가 반영이 되는것입니다. 지금 master브랜치에는 myBranch1에 반영했던 hello.py 내역이 없습니다. 정말 merge된건 아니라는 말인거죠. myBranch1이 master 브랜치에 rebase만 된것입니다.

 git rebase 명령을 실행하면 무조건 fast-forward가 가능하지만, 이런 경우 병합 commit을 남기는것도 좋습니다. git merge myBranch1 --no-ff라는 명령을 실행해 fast-forward를 하지말라는 옵션을 주어서(사실 no fast-forward는 일반적인 병합시에도 매우 좋습니다. 병합한 흔적을 명시적으로 commit 그래프에 남기는 셈이니깐요.)병합을 실행하면 다음 아래과 같은 그래프가 됩니다.



git merge 명령을 실행했을 때 작업 흐름은 아래와 같습니다.

따라서  아래와 같이 명령을 실행해 master 브랜치로 이동한 후, git merge myBranch1 --no-ff 명령을 실행해 최종 병합해줍니다.


$ git checkout master

$  git merge myBranch1 --no-ff


이제 cat hello.py 를 실행해보면 병합되어져서 master브랜치의 hello.py에 수정병합 내용이 반영된것까지 확인하실 수 있게됩니다.


이제 위의 그림과 같이 myBranch2, 3역시 병합을 해봅시다. 블로그에서 글올린 순서대로 진행하시면 어려움없이 하실 수 있을 것입니다.


그리고 마지막으로 commit graph를 살펴보겠습니다. 아래와 같이 명령을 실행합니다.

$ git log --graph

어느 시점에서 병합했는지 아래와 같이 알 수 있어서 깔끔하고 좋습니다.

아름답게 나오는것을 확인하실 수 있습니다.


git rebase - i: 커밋 내역 합하기 명령입니다.

 git rebase 명령에는 활용도가 높은 옵션 -i가 있답니다. i는 interactive의 약어입니다. 상호작용하면서 rebase할 commit을 선탁핼 수 있습니다. 지금부터 앞서 작업했던 최근 commit 내역 두개를 합쳐보겠습니다. 다음 아래와 같이 명령을 실행합니다.


$ git rebase -i HEAD~~


commit 메세지창이 뜨고, 어떤 접두어로 해야하는지 알려주는 창이 뜨는것을 확인하실 수 있습니다.


수정할때는 다음 원칙을 지켜야합니다.

- 남기는 commit 메시지 앞에는 접두어로 pick을 붙입니다.

- 없애는 commit 메세지앞에는 접두어로 fixup을 붙입니다.

- commit SHA-1 체크섬 값은 꼭 남겨두어야 합니다.

- 기존 commit 메시지를 새롭게 수정할 수는 없습니다.


아래와 같이 커밋 메세지를 수정했습니다.


$ pick SHA값 Tell his world      

$ fixup SHA값 tell her world


아래 그림을 보시면, Tell Her World"라는 commit 내역과 그 사이에 있었던 병합 commit 내역이 모두 Tell His World Commit에 포함된것을알 수 있습니다. 그리고 commit 샤값도 새롭게 바뀐것을 확인하실 수 있습니다. 정리하자면 여러개 commit 중에서 필요한 것을 고른 후에 새롭게 commit 하게 되는 것입니다. 그렇다고 파일내용이 바뀌고 그런게 전혀아닙니다. commit내역을 정리한것입니다. rebase한 내역들의 commit들을 보기좋게 바꾼거라 생각하시면 되겠습니다.


$ git log



$ git log --graph



+ Recent posts