74.1 병합이 뭐예요?
브랜치에서 따로 작업했으면, 언젠가는 그 결과물을 main으로 다시 가져와 합쳐야겠죠. 이게 병합(merge, 머지)이에요. feature 브랜치에서 만든 기능을 main으로 끌어와 하나로 합치는 작업이에요.
합치는 방식은 상황에 따라 크게 둘로 갈려요. fast-forward(빨리감기)랑 3-way merge(세 갈래 병합)예요. 뭐가 다른지 하나씩 볼게요.
74.2 제일 간단한 병합, fast-forward
main이 가만히 있는 동안 feature 브랜치만 저만치 앞서 나간 경우예요. 이럴 땐 main 입장에서 그냥 feature가 있는 자리까지 따라가기만 하면 되죠. 이게 fast-forward예요.
상황: main에서 갈라진 feature-header에서만 커밋을 했어요.
$ git switch main
$ git merge feature-header
Updating 3bd73f7..d62066b
Fast-forward
config.py | 1 +
1 file changed, 1 insertion(+)
Fast-forward라고 떴죠? 새 병합 커밋을 따로 만들지 않고, main이라는 표지판을 feature 위치로 슥 옮긴 것뿐이에요. 합칠 게 겹치지 않는, 가장 편한 경우예요.
74.3 갈라진 두 갈래를 합치는 3-way 병합
이번엔 양쪽 다 커밋이 쌓인 경우예요. feature-footer에서 footer를 추가하는 동안, main에서도 about.py를 새로 만들어 커밋했다고 해볼게요. 양쪽이 다 앞으로 나간 거죠.
$ git switch main
$ git add about.py
$ git commit -m "feat: add about"
[main d961394] feat: add about
1 file changed, 3 insertions(+)
이러면 그냥 따라갈 수가 없어요. main에도 feature에도 상대에 없는 커밋이 있으니까요. 이땐 Git이 두 갈래를 하나로 묶는 새 병합 커밋을 만들어요. 이게 3-way 병합이에요.
$ git merge feature-footer -m "merge: footer into main"
Merge made by the 'ort' strategy.
config.py | 1 +
1 file changed, 1 insertion(+)
Merge made by the ort strategy는 "Git이 알아서 두 갈래를 합쳐 병합 커밋을 만들었다"는 뜻이에요. ort는 그 합치는 방식의 이름이라 지금은 신경 안 써도 돼요.
결과를 그래프로 보면 두 줄기가 하나로 모이는 게 눈에 보여요.
$ git log --oneline --graph
* c15f833 merge: footer into main
|\
| * 69b2de9 feat: add footer
* | d961394 feat: add about
|/
* d62066b feat: add header
갈라졌다가(\) 다시 만나는(/) 모양이 보이죠? 이렇게 양쪽이 서로 다른 파일이나 다른 줄을 고쳤다면, Git이 알아서 척척 합쳐줘요. 우리가 손댈 게 없어요.
74.4 그럼 충돌은 언제 나요?
문제는 같은 파일의 같은 줄을 양쪽에서 서로 다르게 고쳤을 때예요. 이건 Git도 "둘 중 뭐가 맞는지" 알 수가 없거든요. 이때 나는 게 바로 충돌(conflict, 컨플릭트)이에요.
상황: main에선 인사말을 "Welcome"으로, feature-a에선 "Hi there"로 같은 줄을 고쳤어요.
$ git merge feature-a
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
Automatic merge failed; fix conflicts and then commit the result.
CONFLICT라고 떴어요. 그래도 겁먹을 거 하나 없어요. Git이 "나 여기는 못 정하겠으니 네가 골라줘"라고 부탁하는 것뿐이거든요. 지금 상태를 보면 이렇게 나와요.
$ git status
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
both modified: config.py
both modified, 즉 양쪽 다 고친 config.py가 문제라고 콕 집어줘요. 어느 파일을 손봐야 하는지 Git이 친절히 알려주는 셈이죠. 파일이 여러 개면 여기에 쭉 나열돼서, 뭘 처리해야 할지 목록만 봐도 알 수 있어요.
74.5 충돌난 파일은 어떻게 고쳐요?
충돌난 파일을 열어보면 Git이 이런 표시를 남겨놨어요.
<<<<<<< HEAD
greeting = "Welcome"
=======
greeting = "Hi there"
>>>>>>> feature-a
<<<<<<< HEAD부터 =======까지가 지금 내 브랜치(main)의 내용이고, ======= 아래부터 >>>>>>> feature-a까지가 상대 브랜치의 내용이에요. 우리가 할 일은 이 표시들을 전부 지우고 원하는 최종 모습만 남기는 거예요.
예를 들어 Welcome을 쓰기로 정했다면, 파일을 이 한 줄만 남게 고쳐요.
greeting = "Welcome"
그리고 "다 해결했어"라고 Git에게 알려주려면 git add를 해요. 그다음 커밋하면 병합이 마무리돼요.
$ git add config.py
$ git commit -m "merge: resolve greeting conflict"
이제 그래프를 보면 충돌났던 두 갈래가 무사히 하나로 합쳐진 게 보여요.
$ git log --oneline --graph
* 1a01680 merge: resolve greeting conflict
|\
| * 721736c feat: change greeting to Hi there
* | 92b3c96 feat: change greeting to Welcome
|/
* 6108b12 init: config
74.6 병합하다 꼬였어요, 되돌릴 수 있나요?
충돌이 너무 복잡해서 "아 그냥 없던 걸로 하고 싶다" 싶을 때가 있어요. 아직 병합 커밋을 찍기 전이라면, git merge --abort 한 방으로 병합을 시작하기 직전 상태로 완전히 되돌아가요.
$ git merge --abort
그러면 파일도, 상태도 병합을 걸기 전으로 깨끗하게 돌아가요. 마음 편히 처음부터 다시 시도하면 되죠. 되돌릴 길이 항상 있다는 것만 알아도 충돌이 훨씬 덜 무서워져요.
정리하면, 병합은 fast-forward(따라가기)와 3-way merge(새 병합 커밋)로 나뉘어요. 서로 다른 곳을 고쳤으면 자동으로 합쳐지고, 같은 줄을 다르게 고쳤으면 충돌이 나요. 충돌은 파일 속 표시를 지우고 최종본을 남긴 뒤 git add와 git commit으로 끝내요. 꼬이면 --abort고요. 충돌은 에러가 아니라 "골라달라는 요청"이라는 것만 기억하면, 하나도 안 무서워요.