프로젝트 생산성

[Git] 위험한데, 내 커밋 히스토리가 깔끔해져요! - Rebase

:) :) 2024. 9. 18. 21:20

[이전 포스트]

 

[Git] 협업 필수, 원격 브랜치(remote branch)

Git - 리모트 브랜치“origin” 의 의미 브랜치 이름으로 많이 사용하는 “master” 라는 이름이 괜히 특별한 의미를 가지는 게 아닌 것처럼 “origin” 도 특별한 의미가 있는 것은 아니다. git init 명

300-29-1.tistory.com

 


 

 

 

Git - Rebase 하기

Rebase는 기존의 커밋을 그대로 사용하는 것이 아니라 내용은 같지만 다른 커밋을 새로 만든다. 새 커밋을 서버에 Push 하고 동료 중 누군가가 그 커밋을 Pull 해서 작업을 한다고 하자. 그런데 그 커

git-scm.com

'깃북 3.6 Git 브랜치 - Rebase 하기'에 대한 발췌 및 정리입니다.

 

 


 

 

Rebase

Git에서 여러 브랜치를 합치는 방법에는

두 가지가 있습니다.

 

하나는 바로 Merge이고,

다른 하나는 Rebase 입니다.

 

이번 포스팅에서는 Rebase가

  1. 무엇인지
  2. 어떻게 사용하는지
  3. 좋은 점
  4. 어떤 상황에서 사용해야 할 지
  5. 어떤 상황에서 사용하면 안되는 지

 

 

Rebase 란,

우선 Merge 복습부터.

앞의 Merge 포스팅에서 살펴본 예제로

다시 돌아가 봅시다.

 

두 개의 나누어진 브랜치의 모습을 볼 수 있습니다.

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

 

3-way Merge를 통해

두 브랜치를 가장 쉽게 합칠 수 있습니다.

두 브랜치의 마지막 커밋 C3, C4와

공통 조상 커밋 C2를 사용하는

3-way Merge 입니다.

3-way Merge 결과

 

 

 

 

 

비슷한 결과를 만드는 다른 방식이 있습니다

Rebase 로는,

C3에서 변경된 사항을 일종의 Patch로 만들고,

이를 다시 C4에 적용시키는 방법이 있습니다.

 

이 방식이 바로 Rebase 입니다.

 

위 예제에서는 아래와 같은 명령으로 Rebase 합니다.

$ git checkout experiment
$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: added staged command

더 자세히 설명하자면,

일단 두 브랜치가 나뉘기 전인 공통 커밋(C2)으로 이동하고 나서

그 커밋부터 지금 Checkout 한 브랜치가 가리키는 커밋까지

diff(파일 or 코드 상 차이)를 차례로 만들어 어딘가에 임시로 저장해 놓습니다.

( 공통 커밋 C2에서부터 experiment가 가존에 가리키고 있던 C4 까지의 diff를 차례로 만들어 임시로 저장합니다. )

 

이후 Rebase 할 브랜치(experiment)가 합칠 브랜치(master)가 가리키는 커밋을 가리키게 하고,

아까 저장해 놓았던 변경사항을 차례대로 적용합니다.

( 이후 master 브랜치가 가리키던 커밋 C3를 가리키도록 experiment 브랜치를 조정합니다.

그 다음 아까 저장해놓았던 변경사항을 차례대로 적용합니다. 이 예제에서는 C2로부터 C4에 대한 변경사항 하나만 존재하기 때문에, 이를 적용한 커밋 C4` 하나가 새로 생긴 것입니다.)

 

 

이후 master 브랜치를 experiment 에 Fast-forward merge 합니다.

$ git checkout master
$ git merge experiment

C4`의 커밋 내용은, Merge 예제에서 살핀 C5 커밋에서의 내용과 동일합니다!

 

 

 

Rebase의 장점

내용은 동일하지만, Rebase의 히스토리는 선형으로써

좀 더 깨끗한 히스토리를 만들 수 있습니다.

 

일을 병렬로 진행해도, Rebase를 통해 모든 작업이 차례대로 수행된 것처럼 보이게 할 수 있습니다.

 

 

 

 

 

 

이러한 장점을 살려

Rebase는 언제 쓰는가

보통 리모트 브랜치에 커밋을 깔끔하게 적용하고 싶을 때 사용합니다.

 

메인 프로젝트에 Patch를 보낼 준비가 되면

origin/master에 Rebase만 하면 됩니다.

 

작업 브랜치에서 하던 일을 완전히 마치고,

origin/master 로 Rebase하면

우리의 작업이 origin/master 브랜치의 최신 상태에서 시작된 것처럼 보이게 합니다.

 

프로젝트 히스토리가 더없이 깔끔하게 보이고, 유지되는 상태로

이후 push 하면

프로젝트 관리자는 어떠한 통합 작업도 필요 없이

그냥 master 브랜치를 Fast-forward 시키면 됩니다.

 

 

 

 

Rebase가 유용한 상황 - topic 브랜치가 복잡하게 존재할 때

server 토픽 브랜치에서 갈라져 나온 client 토픽 브랜치

 

여러 토픽 브랜치가 존재하는 히스토리가 있다고 합시다.

 

딱봐도 복잡해 보이지 않나요?

다 작업을 마무리 하고, merge를 한다고 해도

커밋 히스토리는 복잡한 거미줄 처럼 엮여 있을 것입니다.

 

이럴 때 이 모든 과정을 선형적인 히스토리로 만들어주는게 바로 Rebase 입니다.

 

위 상황에서, 아직 테스트가 덜 된 server 브랜치는 그대로 두고,

client 브랜치만 master로 합치려는 상황을 생각해 봅시다.

 

server 와는 아무 관련이 없는 커밋만 master 브랜치에 적용하려 하는데,

이를 위해 다음 --onto 옵션을 포함한 명령을 사용할 수 있습니다.

$ git rebase --onto master server client

이 명령을 통해

master 브랜치로부터 server 브랜치와 client 브랜치의 공통 조상까지의 커밋을 client 브랜치에서 없애고 싶을 때 사용합니다.

 

client 브랜치에서만 변경된 패치를 만들어,

master 브랜치에서 client 브랜치를 기반으로 새로 만들어 적용합니다.

C8, C9 커밋이 사라지고, master브랜치에 선형적인 C8, C9 커밋으로 재생성

 

이후 master 브랜치에서 Fast-forward 하면, 깔끔해집니다.

$ git checkout master
$ git merge client

client 브랜치에서 작업한 내용을 master 브랜치와 합치면서, 커밋 히스토리를 선형적으로 유지함

 

이제 server 브랜치의 일이 다 끝났다고 해봅시다.

git rebase <basebranch> <topicbranch> 라는 명령으로 Checkout 하지 않고

바로 server 브랜치를 master 브랜치로 Rebase 할 수 있습니다.

이 명령은 토픽(server) 브랜치를 Checkout 하고 베이스(master) 브랜치에 Rebase 합니다.

 

$ git rebase master server

master 브랜치에 server 브랜치의 수정 사항을 적용

 

이후 master 브랜치를 Fast-forward 합니다.

$ git checkout master
$ git merge server

 

모든 토픽 브랜치를 master 브랜치에 통합했기 때문에

더 이상 필요하지 않다면

client나 server 브랜치는 삭제해도 됩니다.

 

브랜치를 삭제해도 커밋 히스토리는 아래처럼

여전히 남아있습니다.

 

 

 

 

 

이럴 때 Rebase 사용하지 마세요

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

아래 하나만 주의하면 됩니다.

 

이미 공개 저장소에 Push 한 커밋을 Rebase 하지 마세요

Rebase는 기존 커밋을 그대로 사용하는 것이 아니라

내용은 같지만 다른 커밋을 새로 만듭니다.

 

따라서 이미 push한 커밋에 대해 Rebase 해버리면,

동료가 pull 및 push 하는 시점에 따라 소스코드 및 커밋 히스토리가 꼬여버리는 상황이 발생합니다.

 

 

예시

서버 저장소를 Clone 하고, 일부 수정을 한 커밋 히스토리가 위와 같습니다.

 

이제 팀원 중 누군가 커밋, Merge를 하고 나서 서버에 Push한 상황에서,

이 원격 브랜치를 Fetch, Merge하면 아래와 같습니다.

그런데 아까 그 팀원이 Merge를 다시 되돌리고 Rebase 해버렸다고 합시다.

서버의 히스토리를 새로 덮어씌우기 위해,

git push --force 명령을 사용했습니다.

이후 이를 내 로컬에서 Fetch 하면 아래 그림처럼 상당히 복잡해집니다.

서버에는 Rebase가 저장된 상태, 그러나 내 로컬에서는 C4 커밋과 C4` 커밋이 공존한다

 

다른 팀원이 내가 의존하는 커밋을 없애고 Rebase한 커밋을 다시 서버에 Push 하고,

내가 이를 Fetch 한 상황입니다.

이를 git pull로 가져와버려서 merge하면, 아래와 같이 로컬에 C8 커밋이 생기게 됩니다.

이러한 상황에서

git log로 히스토리를 확인해보면

저자, 커밋 날짜, 메세지가 같은 커밋이 두 개 존재합니다 (C4, C4`)

상당히 혼란스럽게 됩니다.

 

게다가 만약 이 히스토리를 서버에 Push하면

같은 커밋이 두 개 존재하기 때문에 다른 사람들도 혼란스러워하게 됩니다.

 

애초에 C4 및 C6는 포함되지 말았어야 할 커밋입니다.

 

애초에 다른 동료가 서버로 데이터를 보내기 전에, Rebase로 커밋을 정리해어야 합니다.

 

 

 

 

이미 잘 못 사용해버렸다면 다음을 이용해보세요

 

만약 이런 상황에 빠졌다면 유용한 Git 기능이 하나 있습니다.

Rebase한 것을 다시 Rebase 하기

어떤 팀원이 강제로 내가 한 일을 덮어썼다고 해봅시다.

 

그러면 내가 했던 작업이 무엇이고, 덮어쓴 내용이 무엇인지부터 알아야 합니다.

 

Git 커밋은 SHA checksum 외에도,

Git은 커밋에 Patch할 내용으로 SHA-1 checksum을 한번 더 구합니다.

이 값을 “patch-id”라고 합니다.

즉, patch-id는 커밋의 실제 변경 내용을 기반으로 구해집니다.

 

덮어쓴 커밋을 받아

이 커밋을 기준으로 Rebase할 때,

Git은 원래 누가 작성한 코드인지 잘 찾아 냅니다.

이래서 Patch가 원래대로 잘 적용되는 것입니다.

 

아래처럼 ‘한 팀원이 다른 팀원이 의존하는 커밋을 없애고 Rebase 한 커밋을 다시 Push’ 한 상태에서

Merge를 하는 대신,

git rebase teamone/master 명령을 실행하면

Git은 아래와 같은 작업을 합니다.

  • 현재 브랜치에만 포함된 커밋을 찾습니다. (C2, C3, C4, C6, C7)
  • 이 중 Merge 커밋이 아닌 것을 선택합니다. (C2, C3, C4)
  • 선택된 커밋 중 teamone/master에 이미 존재하는(patch-id가 같은) 것을 제외합니다(C4, C4는 C4’와 동일한 Patch이다).
  • 남은 커밋을 teamone/master 브랜치에 적용합니다.

 

결과가 아래와 같습니다.

강제로 덮어쓴 브랜치에 Rebase 하기

 

강제로 덮어쓴 커밋과 원래 커밋의 내용이 같다면,

Git은 이를 감지하고 중복을 피합니다.

 

동료가 생성했던 C4와 C4' 커밋 내용이 완전히 같을 때만 이렇게 동작됩니다.

커밋 내용이 아예 다르거나 비슷하다면 커밋이 두 개 생깁니다(같은 내용이 두 번 커밋될 수 있기 때문에 깔끔하지 않습니다).

 

 

 

  • **git pull --rebase**를 사용하면 이 과정을 자동화할 수 있습니다.
  • **git config --global pull.rebase true**로 설정하면 항상 rebase 모드로 pull 할 수 있습니다.

 

 

 

Rebase와 Merge의 차이점

기능 상의 차이

Rebase를 하든지, Merge를 하든지 최종 결과물은 같지만

커밋 히스토리만 다르다는 것이 중요합니다.

 

Rebase 의 경우는 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합치고,

Merge 의 경우는 두 브랜치의 최종결과만을 가지고 합칩니다.

 

둘 중 무엇을 쓰면 좋을까

이 질문에 대한 답을 찾기 전에

히스토리의 의미에 대해 생각해보고,

각자 상황에 맞게 잘 적용해야 합니다.

 

 

히스토리를 보는 관점 두 가지

작업한 내용의 기록

히스토리는 작업 내용을 기록한 문서이고,

각 기록은 각각 의미를 가지며, 변경할 수 없습니다.

이런 관점에서 커밋 히스토리를 변경한다는 것은 역사를 부정하는 꼴이 됩니다.

언제 무슨 일이 있었는지 기록에 대해 거짓말을 하게 되는 것입니다.

 

이런 거짓말을 하지 않기 위해, 모든 Merge 및 커밋을 다 남기면

지저분하게 수많은 Merge 커밋이 히스토리에 남게될 텐데,

문제가 없을까요?

 

물론 역사는 후세를 위해 기록하고 보존해야 합니다.

 

 

프로젝트가 어떻게 진행되었나에 대한 이야기

소프트웨어를 주의 깊게 편집하는 방법에,

메뉴얼이나 세세한 작업 내용을 초반부터 공개하고 싶지 않을 수 있습니다.

 

따라서 Rebase나 filter-branch 같은 도구로 프로젝트의 진행 이야기를 다듬으면

나중에 다른 사람에게 들려주기 좋습니다.

 

모든 팀과 모든 사람이 처한 상황은 모두 다릅니다.

이 두 가지 도구를 어떻게 사용할 지는

각자의 상황과 각자의 판단에 달려있습니다.

 

그래도 일반적인 해법은 존재합니다.

로컬 브랜치에서 작업할 때는 히스토리를 정리하기 위해 Rebase 할 수 있지만,

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

 

 

 

 

 

 

[다음 포스트]

 

[GitHub] 개발자라고? 깃헙 알지? PR 할 줄 알지? - GitHub를 얕게만 아는 사람을 위해

Git - GitHub 프로젝트에 기여하기과거에는 “Fork” 가 좋은 의미로 쓰이지 않았다. 오픈 소스 프로젝트를 “Fork” 한다는 것은 복사해서 조금은 다른 프로젝트를 만드는 것을 의미했고 때때로 원

300-29-1.tistory.com

 

 

 

 

 

 

Ref.

 

Git - Rebase 하기

Rebase는 기존의 커밋을 그대로 사용하는 것이 아니라 내용은 같지만 다른 커밋을 새로 만든다. 새 커밋을 서버에 Push 하고 동료 중 누군가가 그 커밋을 Pull 해서 작업을 한다고 하자. 그런데 그 커

git-scm.com