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 브랜치는 삭제해도 된다.
브랜치를 삭제해도 커밋 히스토리는 최종 커밋 히스토리 같이 여전히 남아 있다.
$ 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 하단)
ㄴ> @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은 아래와 같은 작업을 한다.
● 현재 브랜치에만 포함된 커밋을 결정한다. (핑크색 테두리로 표시 : C2, C3, C4, C6, C7)
● Merge 커밋이 아닌 것을 결정한다. (흰색 배경 표시 : C2, C3, C4)
● 이 중 merge할 브랜치에 덮어쓰이지 않은 커밋을 결정한다. (파란 텍스트색 표시 : C2, C3)
+) C4는 C4’와 동일한 Patch다
● 이렇게 결정된 (현재 브랜치에만 포함되었고 Merge 커밋이 아닌 것 중 merge할 브랜치에 덮어쓰이지 않은 커밋)을
로컬의 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 |