이전에 Git Rebase로 두 브렌치를 합칠 수 있다고 포스팅을 작성하였다. 그런데 Rebase를 사용하여 기존의 커밋들을 수정할 수도 있다. 이것이 어떻게 가능한지에 대해 쉽게 이해해보자.
우선 Rebase가 어떻게 동작하는지부터 알아보자. Rebase는 현재 브렌치(HEAD)의 베이스를 타겟으로 넣어준 브렌치/커밋으로 옮기는 일을 한다. 결과물은 HEAD의 커밋들 중 HEAD와 타겟의 공통 base에 속하지 않은 커밋들을 타겟 뒤에 붙여주는 형태가 된다.
만약 dev 브렌치에서 git rebase main
을 하였다면, main과 dev의 공통되는 base인 C0, C1 커밋을 제외한 C4와 C5 커밋을 main 브렌치 뒤에 붙여준다.
즉 위 그림처럼 결과물이 나온다. 이 과정은 Merge Conflict가 발생하지 않는 이상, 유저의 추가적인 조작없이 자동으로 수행된다. 그렇다면 이번에는 git rebase -i main
을 하면 어떤 일이 발생할까?
rebase의 -i
혹은 --interactive
옵션은 Interactive 모드로 Rebase를 실행해준다. Interactive라는 단어에서 알 수 있듯이 Rebase의 과정을 유저가 interactive하게 상호작용할 수 있게 해준다. 그래서 main 뒤로 이동될 대상 커밋인 C4와 C5에 대해 추가적인 조작이 가능하다. 예를 들어, 커밋 메시지를 수정하거나 몇몇 커밋을 제외하거나 순서를 바꿀 수도 있다.
그런데 아마 여러분이 원하는 커밋 로그 수정은 두 브렌치를 합치는 과정이 아닌 하나의 브렌치 내에서 커밋 로그를 변경하는 것일 것이다. 😎
그렇다면 이번에는 같은 브렌치 내의 커밋에 대해 Rebase를 사용해보자. 위 상태에서 git rebase c1
명령을 입력하였다. 그렇다면 어떤 일이 일어날까?
C1과 HEAD의 공통 Base는 위 그림과 같다. 그렇다면 차이가 나는 커밋인 C2, C3를 C1뒤에 붙인다는 소리인데, 그럼 달라지는게 없다!
실제 로그에서도 차이가 나는게 없어서, 별다른 인터렉션이 이루어지지 않는다.
그러나 Interactive 모드인 i
옵션을 준다면 어떻게 될까?
이제 C2와 C3에 대해 상호작용을 할 수 있게 된다. 여기서 커밋을 수정하고 저장하면, 커밋들에 반영이 된다.
C2 커밋과 C3 커밋의 순서를 한번 바꿔보자. 편집을 한 이후, 저장을 하고 나가주자.
순서가 바뀌었다! 그런데 여기서 알아두어야 할 것은 rebase로 커밋의 정보를 바꾸면, 커밋 객체 자체가 바뀐다. 그래서 이미 원격 레포에 push한 경우는 사용하면 곤란해질 것이다... 자세한 것은 아래에서 추가설명하겠다.
git rebase -i <Commit>
Commit
의 다음 커밋부터 HEAD까지의 커밋들을 선택한다.
git rebase -i <Commit>^
Commit
부터 HEAD까지의 커밋들을 선택한다. (가장 첫번째 커밋(부모 커밋이 없는 커밋)은 선택할 수 없다.)
위처럼 원하는 커밋들을 선택한 후, 해당 커밋들을 수정해줄 수 있다.
사진에서 보이듯 다양한 선택지가 있다. 각 라인 시작부분에 명령어를 넣음으로써, 원하는 동작을 수행할 수 있다.
Pick은 커밋을 수정하지 않고 그대로 사용하겠다는 뜻이다. 단, 위의 예시처럼 라인의 위치를 바꾸면 커밋의 위치가 바뀌며, 라인 자체를 지워버리면 커밋 자체가 지워져버린다.
Reword는 커밋 메시지를 수정하는 명령어이다.
위 사진처럼 c1 커밋의 명령을 reword로 하고 저장 후 종료해보자.
위 사진처럼 편집기가 열리며, 커밋 메시지를 수정할 수 있다.
위처럼 커밋 메시지를 수정하고 종료하면 반영이 된 것을 볼 수 있다.
Edit은 커밋 메시지 뿐만 아니라 작업 내용도 수정을 할 수 있다. 사실 Edit은 타임머신이라고 생각하면 쉽다. 이에 대한 설명은 아래 예시를 진행하면서 적겠다.
위 상태에서 git rebase -i 4fc4
를 입력하였다.
c1 커밋을 Edit 해보자.
출력물을 보면, 현재 c1 커밋에서 멈춰있는 것을 볼 수 있다. 마치 c1 커밋을 딱 할 때의 시점으로 타임머신을 타고 이동하였다고 생각하면 쉽게 이해할 수 있다.
실제로 현재 HEAD가 위치한 곳이 c1 커밋인 것을 볼 수 있다! 이전에 작성한 글에서 rebase의 내부 동작이 타겟에서 이동할 커밋들을 새로 커밋하는 것이라고 말했다. 이 설명대로 타겟(first 커밋
)에서부터 c1부터 차례대로 커밋을 할 것인데, 내가 c1 커밋에 edit을 줬기 때문에 해당 커밋을 한 시점에 멈춘 것이다. 이제 이 시점에서 새로운 커밋을 만들어주거나, 해당 커밋의 로그를 수정할 수도 있다.
git commit --amend
는 가장 최근에 한 커밋의 메시지를 수정하는 명령어이다. 현재 타임머신을 타고 c1 커밋을 한 당시의 시점으로 왔기 때문에 이 명령어로 커밋 로그를 수정할 수 있다.
수정을 끝냈으면, git rease --continue
를 입력하여서 수정사항을 확정하고 다음 차례로 넘어갈 수 있다.
마치 c1 커밋을 딱 할 때의 시점으로 타임머신을 타고 이동하였다고 생각하면 쉽게 이해할 수 있다.
일단 위에서 이런 말로 설명을 했는데 사실 틀린 설명이다. ㅎㅎ
Rebase의 내부 동작은 타겟에서부터 새로 커밋을 하는 것이라 말했다. c1 커밋을 할 때의 시점이 아니라 c1과 동일한 작업을 하는 커밋을 새로 만들 때의 시점에서 멈추는 것이다.
다만, 이 과정에서 c1 커밋과 커밋 시간을 제외하면 완벽히 동일한 내용이기 때문에, 굳이 새로운 커밋을 만드는게 아닌 c1 커밋을 그대로 사용하는 것이다.
실제로 c1과 동일한 작업을 하는 커밋을 새로 만들 때의 시점에서 멈추는 것인지 확인을 해보자.
dev라는 브렌치를 만들고, main 브렌치에서 git rebase -i dev
명령어를 입력한다.
c1 커밋에 edit 명령을 줘보자.
그러면 C1과 동일한 작업을 하는 커밋을 새로 만들 때의 시점에서 멈추게 된다. 이 상태에서 커밋을 추가로 더 하거나, c1 커밋(7ed2f1
)을 수정하는 등의 일을 할 수 있다.
git commit --amend
로 가장 최근 커밋의 메시지를 수정해보았다. 보이는 것과 같이 기존의 c1 커밋(1df9af
)가 변한 것이 아닌, 새로운 c1(7fd2f1
)이 변한 것을 볼 수 있다.
squash와 fixup은 해당 커밋을 이전 커밋과 합친다. 두 명령어의 차이점은 합치는 과정에서 squash는 각 커밋들의 메시지가 합쳐지지만, fixup은 이전 커밋의 메시지만 남는다.
위에서 c2 커밋에 squash 명령을 주면, 이전 커밋인 c1커밋과 합쳐진다.
위처럼 두 커밋이 합쳐지고 커밋 메시지를 수정할 수 있다.
결과물을 보면 정상적으로 합쳐져있는 것을 볼 수 있다.
위 그림처럼 squash 혹은 fixup을 연속해서 사용하면, c1 커밋에 c2와 c3 커밋이 모두 합쳐진다. Rebase의 내부 동작을 생각하면 당연한 결과이다. c1, c2, c3가 차례대로 커밋되므로, c1과 c2가 우선 합쳐지고 c3가 또 합쳐지기에 3개의 커밋이 하나로 합쳐진다.
해당 커밋을 버리는 명령이다. 그냥 해당 커밋이 있는 줄을 없애는 것으로도 커밋을 버릴 수 있지만, 명시적으로 적어줄 수 있다.
이외에도 merge, exec 등 몇개의 명령어가 더 있지만, 이 게시글의 맥락과는 맞지않아 설명하지는 않겠다.
Git - git-rebase Documentation
Git Rebase --Interactive 옵션 알아보기