Study Programming/Git

Git 브랜치 합치기 : Merge, Rebase, Rebase의 활용 및 위험성

네모메모 2021. 8. 17. 13:37
반응형

 


 

Git 에서 특정 한 브랜치에서 다른 브랜치로 합치는 방법으로는 두 가지가 있다.

#1) Merge
#2) Rebase

 

Rebase를 하든지, Merge를 하든지 "최종 결과물은 같고 커밋 히스토리만 다르다는 것이 중요하다!!

 


 

#1 Merge

- $git merge 명령어를 사용

- Merge 의 경우, 두 브랜치의 최종결과만을 가지고 합친다.

- 이외 Merge는 브랜치 포스팅 참조

 

- [Merge 과정]

* 아직 커밋하지 않은 파일이 Checkout 할 브랜치와 충돌 나면 브랜치를 변경할 수 없으므로
  워킹 디렉토리 정리 후 Merge작업을 시작한다.

1) $ git checkout <병합 작업할 브랜치명> : 다른 브랜치(B) 내용을 병합할 브랜치(A)로 이동

2) $ git merge <병합 대상인 브랜치명> : 다른 브랜치(B)를 병합할 브랜치(A)에 합침

 ㄴ> 2-1) B 브랜치가 A 브랜치 이후의 커밋을 가리키고 있으면 (A의 커밋이 B의 조상), 

             브랜치 포인터를 최신 커밋으로 옮기는 'Fast forward 방식'으로 Merge한다.

 ㄴ> 2-2) B 브랜치가 A 브랜치 이후의 커밋이 아니면 (A의 커밋이 B의 조상이 아님), 

             각 브랜치가 가리키는 커밋 두 개와 공통 조상 하나를 사용하여 별도의 커밋으로 만들고,
             A 브랜치가 그 별도의 커밋을 가리키도록 이동시키는 '3-way Merge 방식'으로 Merge한다.

 


 

- Merge Ex1) 'Fast forward 방식'

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

 

ⓑ 'branchA' 브랜치 포인터는 그저 최신 커밋으로 이동한다. ('Fast forward 방식')

 


- Merge Ex2) '3-way Merge 방식'

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

ⓑ 두 브랜치의 마지막 커밋 두 개(C3, C4)와 공통 조상(C2)을 사용하는 3-way Merge로 새로운 커밋을 만들어 낸다.

    이 때 새로운 커밋은 두 개의 부모커밋이 생기는데 이런 커밋을 'Merge커밋'이라고 한다.

    ('3-way Merge 방식')

 

 


 

#2 Rebase

$git rebase <합칠 브랜치>  명령어를 사용

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

- Rebase 명령으로 한 브랜치에서 변경된 사항을 다른 브랜치에 적용할 수 있다.

 

- 사용) 리모트 브랜치에 커밋을 깔끔하게 적용하고 싶을 때

 

- 장점)  merge보다 좀 더 깨끗한 히스토리를 만든다.

           ㄴ> Rebase한 브랜치의 로그를 보면 히스토리가 선형이다. 
           ㄴ> 일을 병렬로 동시에 진행해도 Rebase 하고 나면 모든 작업이 차례대로 수행된 것처럼 보인다.

- 단점) 아래에서 설명할 'Rebase 의 위험성'이 있다

- "Rebase 하는 리모트 브랜치는 직접 관리하는 것이 아니라 그냥 참여하는 브랜치일 것이다."
   ㄴ> 메인 프로젝트에 Patch를 보낼 준비가 되면 하는 것이 Rebase이므로

         브랜치에서 하던 일을 완전히 마치고 origin/main으로 Rebase 한다. 

         이렇게 Rebase 하고 나면 프로젝트 관리자는 어떠한 통합작업도 필요 없다. 
         그냥 main 브랜치를 Fast-forward 시키면 된다.

 

 

- [Rebase과정]

  ⓐ 일단 두 브랜치가 나뉘기 전인 공통 커밋으로 이동
  ⓑ 공통 커밋부터 지금 Checkout 한 브랜치가 가리키는 커밋까지 diff를 차례로 만들어 어딘가에 임시로 저장해 놓는다. 
  ⓒ 'Rebase 할 브랜치'가 '합칠 브랜치'가 가리키는 커밋을 가리키게 하고 
  ⓓ 아까 저장해 놓았던 변경사항을 차례대로 적용한다.
  ⓔ 그리고 나서 main 브랜치를 Fast-forward 시킨다. (=main  브랜치를 최신 커밋으로 옮긴다.)

 

 

 

- Rebase Ex) 

 merge 전 두 개의 브랜치로 나누어진 커밋 히스토리 (※ 브랜치 B에 합칠 예정!!)

`C4`의 변경사항을 `C3`에 적용하는 Rebase를 위해 아래와 같은 명령으로 Rebase 한다.

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

Rebase 할 브랜치(역주 - branchA)가 합칠 브랜치(역주 - branchB)가 가리키는 커밋을 가리키게 하고 

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

 

 

 

합칠 브랜치(역주 - branchB)를 Fast-forward 시킨다.

$ git checkout master
$ git merge experiment



- 'C4* 로 표시된 커밋에서의 내용'은 위의 'Merge Ex2 예제 C5 커밋의 내용'과 같을 것이다.



 


 

 

 

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

 

 

 

Rebase 활용


$ git rebase --onto <브랜치A> <브랜치AA> <브랜치AAA>

ㄴ> 브랜치A : 다른 브랜치 사항을 합치려는 브랜치, (= 조상,main,root브랜치)
      브랜치AA : 브랜치 A에서 갈라진 브랜치, (=부모 브랜치)
      브랜치AAA : 합칠 내용이 있는 브랜치 AA에서 갈라진 브랜치, (=자손 브랜치)

 

 

: "브랜치A"부터  "브랜치AA브랜치AAA의 공통 조상"까지커밋 외 사항을 브랜치AAA에서 없애고,

   브랜치AAA에서만 변경된 패치를 만들어 브랜치A에서 브랜치AAA를 기반으로 새로 만들어 적용한다.

   ㄴ>※ 주의) 브랜치A는 이동되지 않는다!!

                   브랜치A'로부터 패치적용해 추가 커밋한 곳으로 '브랜치AAA'가 이동된다!!

 

 


 

$ git rebase  <베이스 브랜치> <토픽 브랜치>

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

- ex1-3 참조

 


ex1)
예제상황 : server 브랜치를 만들어서 서버 기능을 추가하고 그 브랜치에서 다시 client 브랜치를 만들어

              클라이언트 기능을 추가한다. 마지막으로 server 브랜치로 돌아가서 몇 가지 기능을 더 추가한다.

 


ex1-1)
이때 테스트가 덜 된 server 브랜치는 그대로 두고 client 브랜치만 master 로 합치려는 상황
          server 와는 아무 관련이 없는 client 커밋은 C8, C9 이다. 이 두 커밋을 master 브랜치에 적용

 

[$git rebase --onto 적용 전]

그림 39. 다른 토픽 브랜치에서 갈라져 나온 토픽 브랜치

$ git rebase --onto master server client

 

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

 

[$git rebase --onto 적용 후]
  그림 40. 다른 토픽 브랜치에서 갈라져 나온 토픽 브랜치를 Rebase 하기

 

 

ex1-2) (이전 작업에서 master 브랜치는 이동되지 않았다. 위 주의사항 참조)

         이제 master 브랜치로 돌아가서 Fast-forward 시킨다.

$ git checkout master
$ git merge client


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

 

 

 

ex1-3) 이후 server 브랜치의 일이 다 끝나 master 브랜치로 합칠 때,
          Checkout 하지 않고 바로 server 브랜치를 master 브랜치로 Rebase 할 수 있다.

 

$ git rebase master server

ㄴ> 그 결과는 그림 42. master 브랜치에 server 브랜치의 수정 사항을 적용

이후 (위 명령어에서 master 브랜치는 이동되지 않았으므로) master 브랜치를 Fast-forward 시킨다.

$ git checkout master
$ git merge server

 


ex1-4) 이후 모든 것이 master 브랜치에 통합됐기 때문에 더 필요하지 않다면 client 나 server 브랜치는 삭제해도 된다. 
         브랜치를 삭제해도 커밋 히스토리는 최종 커밋 히스토리 같이 여전히 남아 있다. 

 

>> 그림 43. 최종 커밋 히스토리

$ git branch -d client
$ git branch -d server





 

 

Rebase의 위험성

- Rebase의 주의점은 아래와 같다

   ※주의※ 이미 원격 저장소에 Push 한 커밋을 Rebase 하지 말 것!!

 

ㄴ> Rebase는 기존의 커밋을 그대로 사용하는 것이 아니라 내용은 같지만 다른 커밋을 새로 만든다. 
      새 커밋을 서버에 Push 하고 동료 중 누군가가 그 커밋을 Pull 해서 작업을 한다고 하자. 
      그런데 그 커밋을 $ git rebase 로 바꿔서 Push 해버리면 동료가 다시 Push 했을 때 동료는 다시 Merge 해야 한다. 
      그리고 동료가 다시 Merge 한 내용을 Pull 하면 내 코드는 정말 엉망이 된다.
 

 

$ git push --force

 : 서버의 히스토리를 새로 덮어씌우기

 


ㄴ> ex)
ⓐ 원격 저장소를 Clone 하고, 로컬에서 C2, C3를 커밋했다. (그림 44. 저장소를 Clone 하고 일부 수정함)

 ㄴ> @My computer(=local git repository) : 'origin/master브랜치'가 원격의 master브랜치이고, 

      로컬에서 c2,c3 커밋하여 '로컬 main브랜치'가 c3을 바라보는 상태이다.

 

 

ⓑ-1. 팀원 중 누군가 C4,C5커밋, 이 둘을 3-Way Merge해 C6추가하고, 서버에 Push 했다. (그림 45 상단)

ㄴ> @Remote git repository : 원격에 c4,c5 그리고 이 둘을 머지한 c6 커밋된 상태가 push 되었다.

 

 

ⓑ-2. 이후 리모트 브랜치를 내 로컬로 Fetch, Merge 하면 로컬 히스토리는 아래와 같다. (그림 45 하단)

Fetch 후
Fetch후 Merge까지 완료 시

ㄴ> @My computer(=local git repository) : '원격 최신커밋 c6'와 내 '로컬 최신커밋 c3'을 3-way merge해서

        생성된 Merge커밋 'c7'을 로컬 master브랜치가 바라보는 상태 

 


ⓒ-1. 그런데 Push 했던 팀원은 원격에 Merge 한 일을 되돌리고 다시 Rebase 했다. (그림 46 상단)
+) 서버의 히스토리를 새로 덮어씌우려면 $ git push --force 명령을 사용해야 한다. 

ㄴ> @Remote git repository : c4, c5, c6(=c4,c5를 merge한 커밋사항)에서

         c4, c6(=c4,c5 merge 커밋)을 제거하고,

         c4 변경사항을 c5에 적용(merge)한 'c4*'로 커밋하여 rebase작업함.

 

ⓒ-2. 로컬 저장소에서 Fetch 하면 아래 그림과 같은 상태가 된다. (그림 46 하단)

ㄴ> @My computer(=local git repository) :

        이전 Fetch명령으로 받아온 커밋 C4,5,6도 로컬에 존재하면서 (C4,C5의  3-Way merge 결과)

        이번 Fetch명령으로 새로 받아온 'C4*커밋'도 내 브랜치에 같이 존재한다. (C4,C5의 Rebase merge 결과)

 

 --- 이렇게 되면 같은 커밋사항에 대한 '3-Way merge 결과'도 있고 'Rebase merge 결과'도 있는 짬뽕이 되었다 ---

 

 

 $ git pull 명령을 실행 시 서버의 내용을 가져와서 Merge까지 하므로

    같은 내용의 수정사항을 포함한 Merge 커밋이 아래와 같이 만들어진다. (그림 47 하단)

ㄴ> @My computer(=local git repository) : $ git pull 명령으로 '원격 최신 사항(C4*)'와 내 '로컬 최신사항(C7)'을

         Git이 자동으로 merge하여 새로 커밋(C8)한다.
Q. 근데 이게 원격최신(C4*)랑 로컬최신(C7)을 merge할 때 (C4*)랑 합칠 사항이 있나??

     이미 내 로컬에 c4, c5, c6가 다 있어서 내용 변동 없을 듯한데 한 번 해봐야겠다.


ㄴ> C6 : C4, C5를 3-Way merge한 커밋
ㄴ> C4* : C4, C6(=C4, C5를 merge한 커밋사항)를 제거하고 C5에 C4 수정사항을 적용한 Rebase작업으로 추가된 커밋

 


ⓔ 이 상태에서 $ git log 로 히스토리를 확인하면 저자, 커밋 날짜, 메시지가 같은 커밋이 두 개이다 (C4, C4'). 
   이렇게 되면 혼란스럽다 ༼;´༎ຶ ۝ ༎ຶ༽
   게다가 이 히스토리를 서버에 Push 하면 같은 커밋이 두 개 있기 때문에 다른 사람들도 혼란스럽다.(혼란 pushㅠㅠ)  
   'C4'와 'C6'는 포함되지 말았어야 할 커밋이다. 
   애초에 팀원이 서버로 데이터를 보내기 전에 Rebase로 커밋을 정리했어야 했다.

 


 

Rebase 한 것을 다시 Rebase 하기

 

누군가 강제로 내가 한일을 덮어썼을 때 내가 했던 일이 무엇이고 덮어쓴 내용이 무엇인지 알아내야 한다.
이런 상황에 빠질 때 유용한 Git 기능이 하나 있다. 
 


patch-id

 : (커밋 SHA 체크섬 외에도) Git은 커밋에 Patch 할 내용으로 SHA-1 체크섬을 한번 더 구하며, 이 값이 'patch-id' 이다.

덮어쓴 커밋을 받아서 그 커밋을 기준으로 Rebase 할 때 Git은 원래 누가 작성한 코드인지 잘 찾아 낸다. 
그래서 Patch가 원래대로 잘 적용된다.

Ex) 위 ⓒ-2(같은 커밋사항에 대한 '3-Way merge 결과'도 있고 'Rebase merge 결과'도 있는) 상황을 살펴보자

 

위 예제의 처럼 $ git pull 명령으로 Merge 하는 대신 

git rebase origin/main 명령을 실행하면 Git은 아래와 같은 작업을 한다.

 

$git rebase origin/main 명령어 실행 전

현재 브랜치에만 포함된 커밋을 결정한다. (핑크색 테두리로 표시 : C2, C3, C4, C6, C7)
Merge 커밋이 아닌 것을 결정한다. (흰색 배경 표시 : C2, C3, C4)
● 이 중 merge할 브랜치에 덮어쓰이지 않은 커밋을 결정한다. (파란 텍스트색 표시 : C2, C3)

     +) C4는 C4’와 동일한 Patch다

 

● 이렇게 결정된 (현재 브랜치에만 포함되었고 Merge 커밋이 아닌 것 중 merge할 브랜치에 덮어쓰이지 않은 커밋)을

    로컬의 origin/main 브랜치에 새로 적용하고, 나머지 커밋을 제거한다 ▼

$git rebase origin/main 명령어 적용 후

ㄴ> ※ 제약) 동료가 생성했던 C4와 C4' 커밋 내용이 완전히 같을 때만 이렇게 동작된다. 
 커밋 내용이 아예 다르거나 비슷하다면 커밋이 두 개 생긴다(같은 내용이 두 번 커밋될 수 있기 때문에 깔끔하지 않다).

 

 


 

Rebase vs. Merge 둘 중 무엇을 쓰는 게 좋지??

 - Merge 나 Rebase 중 무엇이 나으냐는 질문은 다시 생각해봐도 답이 그리 간단치 않다.

   Git은 매우 강력한 도구고 기능이 많아서 히스토리를 잘 쌓을 수 있지만, 모든 팀과 모든 이가 처한 상황은 모두 다르다. 

 

 

   다만 앞서 언급했듯이, 

"Rebase를 하든지, Merge를 하든지 "최종 결과물은 같고 커밋 히스토리만 다르다는 것이 중요하다"

그러므로 히스토리를 보는 관점은 이 선택에 도움은 될 것이다.

 


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

#1) 히스토리를 '작업한 내용의 기록'으로 보는 것 

- 작업 내용을 기록한 문서이고, 각 기록은 각각 의미를 가지며, 변경할 수 없다. 
  이런 관점에서 커밋 히스토리를 변경한다는 것은 역사를 부정하는 꼴이 된다. 
  언제 무슨 일이 있었는지 기록에 대해 거짓말 을 하게 되는 것이다. 

 


그런데 이렇게 했을 때 지저분하게 수많은 Merge 커밋이 히스토리에 남게 되면 문제가 없을까? 
▼ 

 


 #2) 히스토리를 '프로젝트가 어떻게 진행되었나에 대한 이야기'로 보는 것

  - 소프트웨어를 주의 깊게 편집하는 방법에 메뉴얼이나 세세한 작업내용을 초벌부터 공개하고 싶지 않다. 
    나중에 다른 사람에게 들려주기 좋도록 Rebase 나 filter-branch 같은 도구로 프로젝트의 진행 이야기를 다듬으면 좋다.
  
  



     ≒ 일반적인 해답을 굳이 드리자면 로컬 브랜치에서 작업할 때는 히스토리를 정리하기 위해서 Rebase 할 수도 있지만, 
          리모트 등 어딘가에 Push로 내보낸 커밋에 대해서는 절대 Rebase 하지 말아야 한다.

 

 

 

 

 


 

END!

 

 


스터디 도움 참조 블로그 (References)

- 3.6 Git 브랜치 
https://git-scm.com/book/ko/v2/Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EB%B8%8C%EB%9E%9C%EC%B9%98%EC%99%80-Merge-%EC%9D%98-%EA%B8%B0%EC%B4%88

- Merge방법
https://nemomemo.tistory.com/93 

- 브랜치 명령어
https://backlog.com/git-tutorial/kr/reference/branch.html


- git--distributed-even-if-your-workflow-isnt (Pro Git Book)
https://git-scm.com/book/en/v2

 

 

 

반응형