index.js의 인사말을 고쳤어요. 이제 main에도 넣고 싶어요. main으로 전환하면 파일은 예전 내용으로 돌아오는데, 내 변경은 어떻게 가져올까요?
앞편에서는 브랜치를 만들고 작업 위치를 바꿨어요. 이번에는 현재 브랜치에 다른 브랜치의 이력을 합치는 병합(merge)을 해봐요. 같은 merge라도 브랜치 위치만 움직일 때와 새 커밋이 생길 때가 있어요.
어느 브랜치로 합칠지 먼저 골라요
main에서 git merge feature/greeting을 실행하면 feature/greeting의 이력을 현재 브랜치인 main으로 합쳐요. 명령에 적은 브랜치로 이동하는 동작은 아니에요. 병합 뒤에도 현재 브랜치는 main이에요.
이번 글은 공통 조상이 있는 두 로컬 브랜치를 병합해요. 원격 저장소나 GitHub PR은 사용하지 않아요. 두 가지 결과를 비교할 거예요.
fast-forward는 새 병합 커밋 없이 브랜치 참조를 앞으로 옮기는 방식이에요. merge commit은 합친 파일 상태와 여러 부모를 기록하는 커밋이에요. 아래 실습에서는 부모가 둘이에요. 이 구분은 Pro Git의 기본 병합 설명을 기준으로 해요.
새 연습 저장소를 준비해요
명령은 Bash 기준이에요. Git 설치와 작성자 설정이 낯설면 첫 커밋 실습을 먼저 읽어봐요. 준비 명령의git init -b main에는 Git 2.28 이상이 필요해요. Node.js는 필요하지 않아요.
앞편의 마지막 상태는 두 브랜치가 index.js의 같은 줄을 서로 다르게 고친 상태예요. 그대로 합치면 충돌하므로, 이번에는 별도 저장소에서 충돌 없이 합쳐지는 두 경우를 만들어요. 앞편의 저장소는 그대로 두세요.
기존 Git 저장소 밖에서 실행해요. 같은 이름의 폴더가 있어
mkdir가 실패하면 멈추고 새 이름을 골라요.
>는 파일을 덮어써요. 방금 만든 빈 연습 폴더에서만 실행해요. 작성자 설정은 이 저장소에만 적용해요. 실습은 별도 hooks, 서명, merge driver나 브랜치별 병합 옵션이 없는 환경을 기준으로 해요.
main이고 status는 working tree clean이어야 해요. 첫 커밋 하나가 보이고 원격 출력은 없어요. 커밋 ID는 실행할 때마다 달라질 수 있어요. 이력이 별도 화면으로 열리면 q로 나와요.
main이 그대로라면 위치만 옮길 수 있어요
1
작업 브랜치에서만 커밋하기
새 브랜치에서 인사말을
hello git으로 바꿔요. main에는 새 커밋을 만들지 않아요.main의 파일은 아직 hello예요. 그래프에서는 feature/greeting이 첫 커밋보다 하나 앞서 있어요. main의 끝 커밋은 그 이력의 부모로 이미 들어 있어요.2
fast-forward로 합치기
실행 전에 예상해봐요. 두 브랜치가 같은 커밋을 가리키게 하려면 새 커밋이 필요할까요?병합 출력에
Fast-forward가 나오고 파일은 hello git으로 바뀌어요. rev-parse가 출력하는 두 ID는 같고, 전체 커밋 수는 여전히 2예요. 새 커밋을 만들지 않고 main을 기존 커밋으로 옮겼어요. 깨끗했던 작업 트리와 인덱스도 그 커밋의 파일 상태로 갱신됐어요.--ff-only는 fast-forward로 합칠 수 있을 때만 진행해요. 이력이 갈라져 있으면 병합을 거절해요. 옵션의 범위는 git merge 공식 문서를 참고해요.feature/greeting은 이동하지 않았어요. main이 같은 위치로 왔을 뿐이에요. 브랜치 이름도 자동으로 삭제되지 않아요.
양쪽에 새 커밋이 있으면 부모 둘을 연결해요
이어서 같은 저장소를 사용해요. 이번에는 작업 브랜치에서index.js를, main에서 README.md를 바꿔요. 공통 조상 이후 서로 다른 파일을 수정해서 이력이 갈라져도 충돌 없이 합쳐지는 경우를 만들어요.
1
작업 브랜치의 인사말 바꾸기
현재
main에서 새 브랜치를 만들어요. 기존 feature/greeting은 그대로 남겨둬요.2
main에도 별도 변경 기록하기
main에서는 사용 안내를 한 줄 추가해요. 아래 >>는 기존 파일 끝에 덧붙여요.main이고 작업 상태는 clean이에요. merge-base는 두 이력의 공통 조상을 찾아요. 이 실습에서는 출력이 feature/greeting의 ID와 같아요. 그 커밋에서 두 갈래로 나뉘었기 때문이에요. git merge-base 공식 문서의 공통 조상 개념에 해당해요.두 끝 커밋의 diff에는 인사말 변경과 안내 줄 삭제가 보여요. 비교 대상인 feature/world에는 main에서 추가한 안내 줄이 없기 때문이에요. 이 diff가 병합 결과를 뜻하지는 않아요. 병합은 공통 조상을 기준으로 양쪽의 변경을 살펴봐요.3
fast-forward가 거절되는지 확인하기
이번에는 이 명령은 의도적으로 실패해요. 검증 환경에서는 아래 오류가 나왔어요. 부가 안내나 번역은 환경에 따라 다를 수 있어요.실패를 확인한 뒤 상태를 읽어요.여전히
main의 새 커밋이 feature/world의 이력에 없어요. 브랜치 위치만 옮겨서는 두 이력을 함께 보존할 수 없어요.main의 마지막 커밋은 Document how to run the script예요. 파일은 hello git 인사말과 추가한 안내 줄을 유지해요. 충돌 해결 단계에 들어간 것이 아니므로 merge --abort를 실행할 필요도 없어요.4
merge commit으로 합치기
현재 브랜치가
main이고 작업 상태가 clean인 것을 확인했어요. 이번에는 fast-forward가 불가능하면 merge commit을 만드는 --ff를 사용해요. 옵션을 명시해 merge.ff 설정에 따른 차이를 줄여요. -m은 병합 메시지, --no-edit는 메시지 편집기를 열지 않는 옵션이에요.index.js에는 hello world가, README.md에는 main에서 추가한 안내가 함께 있어요. 커밋 수는 5이고 main은 새 병합 커밋을 가리켜요. feature/world는 인사말을 고친 기존 커밋에 남아 있어요.A부터 M은 설명용 이름이며 명령에 입력할 커밋 ID가 아니에요.
병합은 이전 커밋을 고쳐 쓰지 않았어요. M이 두 부모를 연결하면서 양쪽 이력이 main에서 이어져 보여요. 이력이 갈라졌다는 이유만으로 충돌하는 것도 아니에요. 이번에는 서로 다른 파일의 변경을 자동으로 합쳤어요.
부모 커밋과 파일을 함께 확인해요
git log의 줄 그림만 보고 판단하기 어렵다면 부모 ID를 직접 읽어봐요.
parents: 뒤에 ID 두 개가 나와요. HEAD^1은 첫 번째 부모, HEAD^2는 두 번째 부모예요. 이번 병합의 첫 번째 부모는 병합 직전 main, 두 번째 부모는 feature/world예요. 따라서 rev-parse의 두 번째와 세 번째 출력이 같아요. 표기법은 gitrevisions 공식 문서를 기준으로 해요.
HEAD~2는 첫 번째 부모를 두 번 따라가요. 두 번째 부모를 뜻하는 HEAD^2와 달라요. 파일 차이를 볼 때도 어느 부모와 비교하는지 골라야 해요.
병합 옵션은 이력과 함께 읽어요
아래는 참고 표예요. 실습에서 병합한 브랜치를 다시 만들 필요는 없어요.
이미 상대 이력이 현재 이력에 포함됐다면 별도 작업 없이 끝날 수 있어요.
--no-ff도 포함된 이력을 합칠 때마다 새 커밋을 만든다는 뜻은 아니에요. 기본 동작은 설정과 병합 대상에 영향을 받을 수 있으므로 여기서는 옵션을 명시했어요. 세 옵션은 git merge의 옵션 설명을 참고해요.
fast-forward는 직선 이력을 유지하고, merge commit은 합친 지점을 부모 관계로 남겨요. 어느 방식이든 파일과 이력을 확인해야 해요. 자동 병합에 성공했다는 사실만으로 프로그램의 동작까지 검증한 것은 아니에요. 실제 프로젝트에서는 병합한 코드에 맞는 테스트도 실행해요.
끝난 상태를 확인해요
Already up to date.로 끝나요. feature/world의 이력이 이미 main에 포함돼 있어요. 새 커밋은 생기지 않아서 전체 커밋 수는 5 그대로예요.
현재 브랜치는
main이고 상태는 clean이에요. 두 미커밋 diff와 원격 출력은 비어 있어요. index.js의 인사말은 hello world이고 README에는 안내 줄이 있어요. 두 작업 브랜치는 남아 있으며, main의 마지막 커밋에는 부모가 둘 있어요.예상과 다른 결과가 나왔다면
충돌로 병합이 진행 중일 때만 아래 명령을 실행해요. 중단하면 병합 도중의 해결 작업도 버리므로, 그 편집을 보존해야 한다면 먼저 별도로 복사해요.
main의 마지막 커밋은 README 안내 추가예요. 인사말은 hello git이고 상태는 clean이에요. 파일과 시작 상태가 다른 경우에는 다음 명령을 이어 실행하지 말고 차이부터 확인해요.
브랜치 이름이 둘이라고 늘 새 병합 커밋이 필요한 것은 아니에요. 현재 커밋이 상대 이력에 포함됐는지를 먼저 읽으면 위치만 옮길 수 있는지 판단할 수 있어요. 현재 위치는 브랜치와 변경 내용 조회로, 합친 결과는 이력과 diff 읽기로 다시 확인해보세요.
같은 줄을 서로 다르게 고쳐 자동으로 합치지 못하면 어떻게 될까요? 병합 충돌 해결에서 파일을 직접 정하고 병합을 완료하거나 중단해봐요.