[Git/Github] Git 기본 명령어4 — merge(브랜치 병합하기)
merge는 브랜치와 브랜치를 병합할 수 있는 명령이다. 두 브랜치를 병합한다는 것은 다른 브랜치의 변경사항을 현재 브랜치에 적용시키는 것을 의미한다.
이번 포스트에서는 특정 브랜치의 변경 사항을 현재 브랜치에 적용시킬 수 있는 merge 의 기본적인 내용에 대해서 알아보자.
먼저 병합을 위한 명령어에 대해 알아보자.
- ‘git merge <병합할 브랜치명>’
명령어는 다른 브랜치의 변경사항을 현재 브랜치에 적용시키는 명령어이다. 정말 간단한 명령어지만, 상황에 따라 다른 처리를 해줘야 할 수 있다. 다음과 같은 상황을 가정해보자.
만약 현재 브랜치에서 a.txt 라는 파일의 첫 번째 줄을 수정했는데, 병합하려는 브랜치에서도 a.txt 파일의 첫 번째 줄을 수정했다면?
위와 같은 상황을 ‘충돌(conflict)이 발생했다.’ 라고 한다. 충돌을 해결할 수 있는 방법은 잠시 후에 알아보고, 우선 merge 할 때 어떤 상황들이 있는지 알아보자.
- merge 상황으로는 fast-forward merge(ff merge) 와 3-way merge가 있다.
병합하는 브랜치 사이의 관계에 따른 merge 종류
다음과 같은 상황을 가정해보자.
master branch tip에서 파생된 branch_a, branch_b가 각각 두 개의 커밋을 생성하여 Fig1 과 같은 상황이 됐다.
fast-forward merge(ff merge)
먼저 merge는 하나의 브랜치의 변경 사항을 다른 branch에 적용시키는 것이라고 했다. 현재 HEAD가 가리키는 브랜치가 branch_a라고 하자. 이 상태의 branch_a에서 master를 병합하려고 하면 ‘Already up to date.’ 라는 문구를 볼 수 있다. 이미 master의 모든 변경 사항을 branch_a가 포함하고 있기 때문에 적용할 변경 사항이 없다는 뜻이다.
우리가 실제로 master 와 branch_a를 합치려고 하면 Fig2와 같은 그림을 원할 것이다. (Fast-forward 의 이해를 위해 Fig1에서 branch_b 만의 커밋들은 제외했다.)
이를 원한다면 먼저 master 브랜치로 checkout 한 후에 branch_a를 master로 merge해야 한다.
위와 같이 ‘git merge <변경사항을 적용시킬 브랜치명>’ 이라는 명령어로 branch_a의 적용사항을 현재 브랜치(master)에 적용할 수 있다. 이 때 ‘Fast-forward’라는 문구를 볼 수 있다. (Fig3 의 ff0323d 와 같은 커밋 id 값은 예시이므로, Fig2 와 다를 수 있다는 점은 참고해주세요!)
현재 브랜치가 가리키는 커밋이 병합하려는 브랜치가 가리키는 커밋의 조상 커밋일 때 두 브랜치는 Fast-forward 관계에 있다고 한다. 그저 현재 브랜치가 병합하려는 브랜치 팁으로 이동하면 되므로, 빨리 감기라는 단어가 어울린다.
- Fast-forward 관계에 있는 두 브랜치 : 현재 브랜치가 가리키는 커밋이 병합하려는 브랜치가 가리키는 커밋의 조상 커밋일 때
- git merge <변경사항을 적용시킬 브랜치명> : ‘변경사항을 적용시킬 브랜치명’의 이름을 갖는 브랜치의 변경사항을 현재 브랜치에 적용한다.
기본적으로 설정되어 있는 옵션은 병합하려는 두 브랜치가 Fast-forward 관계일 때는 Fast-forward 방식으로 병합한다. --ff라는 옵션 명령어를 달아 명시적으로 표기할 수 있고(기본값), Fast-forward 방식으로 병합이 불가능할 땐 다른 방법으로 병합한다. --ff-only 라는 옵션 명령어를 사용하면 Fast-forward 방식으로만 병합을 진행하며, 불가능할 때는 “fatal: Not possible to fast-forward, aborting.” 이라는 메세지를 보이며 병합하지 않는다. --no-ff 옵션을 사용하면 명시적으로 Fast-forward 병합을 하지 않는다. 만약 두 브랜치가 Fast-forward 관계에 있다고 하더라도, Fast-forward 병합을 하지 않고 각각 브랜치의 변경 이력을 가지면서 병합한다.
- --ff : Fast-forward 병합 옵션의 기본 값. Fast-forward 병합이 가능할 때는 Fast-forward 병합을 수행하고, 불가능할 때는 다른 방법으로 병합한다.
- --ff-only : Fast-forward 방식으로만 병합한다. Fast-forward 병합이 불가능할 경우는 병합하지 않는다.
- --no-ff : Fast-forward 방식을 사용하지 않고 병합한다. 설령 두 브랜치가 Fast-forward 관계에 있다고 하더라도, Fast- forward 병합을 하지 않고 각각 브랜치의 변경 이력을 가지면서 병합한다.
3-way merge
현재 브랜치가 가리키는 커밋이 병합하려는 브랜치가 가리키는 커밋의 조상 커밋이 아니고, 자식 커밋이나 자기 자신도 아닐 때 3-way merge를 수행할 수 있다.
3-way 의 3은 병합하려는 두 브랜치 각각의 브랜치 팁과 두 브랜치가 파생된 공통 base commit 세 커밋을 의미한다. Fig5 에서는 d5a82 커밋, b239 커밋과 공통 base commit 인 f30ab 커밋 세 커밋을 의미한다고 볼 수 있다.
3-way merge 는 Fast-forward merge와 다르게 충돌(conflict)이 발생할 수 있다. 같은 커밋에서 시작하여 다른 변경 사항을 만든 후 병합하는 것이기 때문에, 같은 파일의 같은 부분에 다른 수정을 수행한 경우가 생길 수 있기 때문이다.
병합 완료 시 “Merge made by the ‘recursive’ strategy.” 혹은 “Merge made by the ‘ort’ strategy.” 라는 메세지를 보여준다. 여기서 strategy 는 base commit 을 찾는 merge strategy 를 뜻하는데, default strategy는 git 버전 2.33 까지는 recursive, 그 이후는 ort 이다. base commit 을 찾는 방법이 어떻게 여러가지가 될 수 있는지 의문을 가질 수 있다. 하지만 여러가지가 될 수 있는 상황은 Fig4 에서와 같이 명확히 base commit 이 하나일 때가 아닌, 브랜치 서로가 서로를 병합하는 등 좀 더 복잡한 상황에서 사용될 수 있다.
merge strategy에는 recursive, resolve, octopus, ours, subtree 등 여러가지가 있으나, 병합을 이해하기 위한 기본적인 내용은 아니라고 생각하여 본 포스트에서는 다루지 않는다.
Fig6 은 아무 옵션도 없이 merge 로 3-way merge 했을 때 결과를 나타내는 예시이다. HEAD 가 branch_b를 가리키는 상태에서 branch_a 를 merge 했을 때를 나타낸다. 결과를 보면 branch_b 는 새로운 커밋이 생긴 것을 볼 수 있다. 하지만 branch_a 는 변함이 없다.
또 한 가지 눈 여겨 볼 점은 branch_b 가 가리키고 있는 커밋은 가리키고 있는 부모 커밋이 두 개가 됐다는 점이다. 따라서 branch_b 의 커밋 이력을 보면 branch_b 의 커밋 뿐만 아니라 branch_a 의 커밋들도 전부 볼 수 있다.
어떤 경우에는 병합할 때 branch_a 의 커밋 이력을 전부 보여주는 것이 더 불편한 경우도 있을 것이다. 그런 경우에는 merge에 옵션을 주어 squash merge 와 같은 방법을 사용할 수 있으나, 커밋 이력 관리를 쉽게 해주는 merge의 옵션이나 전략에 관해서는 본 포스트에서 다루지 않는다.
충돌 해결 (conflict resolve)
3-way merge 상황에서 merge 하는 경우 포스트 상단에서도 언급한 ‘충돌(conflict)’이 발생할 수 있다. Fig4 에서 Fig5 와 같이 branch_a를 branch_b로병합하는 경우를 가정해보자. 이 때 만약 개발자 a 가 branch_a 에서 ‘text.txt’ 파일의 첫 번째 줄을 수정하고, 개발자 b 는 branch_b 에서 같은 파일인 ‘text.txt’ 파일의 첫 번째 줄을 수정했다면, 같은 파일의 같은 줄이 각각 다른 브랜치에서 수정됐기 때문에 충돌이 발생한다.
이 때, 충돌이 발생한 파일은 어떻게 처리되며, 충돌을 어떻게 해결할 수 있는지 알아보자.
만약 충돌이 있다면, Fig4 에서 merge 명령어를 사용했을 때 바로 merge commit이 생기면서 merge 되지 않는다. 충돌이 발생한 파일을 열어보면 충돌 부분이
- <<<<<<< 현재 브랜치명
- 현재 브랜치에서의 변경 내용
- ======= (구분자)
- 병합할 브랜치의 변경 내용
- >>>>>>> 병합할 브랜치명
로 표시되고, modified 된 상태로 unstaged 상태에 있다. 예를 들어 ‘text.txt’ 파일을 열어보면 아래와 같다.
<<<<<<< HEAD
branch_b 의 변경 내용
=======
branch_a 의 변경 내용
>>>>>>> branch_b
충돌을 해결하는 방법은 ‘<<<<<<< 현재 브랜치명’ , ‘=======’, ‘>>>>>>> 병합할 브랜치명’ 을 지우고 원하는 변경 방향을 정하여 설정한 뒤 저장하고, git add 로 충돌이 발생한 파일을 stage 위에 올리고, commit 하면 Fig5 와 같이 병합이 완료된 상태로 만들 수 있다.
그러나 충돌이 발생한 상태에서 커밋하기 전에, 병합을 취소하고 싶으면 ‘git merge --abort’ 명령어를 사용하여 취소할 수 있다.
이번 포스트에서는 다른 브랜치의 변경 사항을 현재 브랜치에 적용할 수 있는 merge의 기본적인 내용에 대해 알아보았다. merge 가 발생할 때의 상황, 충돌이 발생했을 때 해결 방법, 그리고 충돌 발생 시 merge 를 취소할 수 있는 방법을 잘 숙지하고, 유용하게 사용해보자.
Reference
- [Git official — Book] — https://git-scm.com/book/en/v2