dev.to

How to resolve merge conflicts in Git without guessing: markers, --ours/--theirs and the rebase flip

추측 없이 Git 병합 충돌 해결하기 — 충돌 표시, --ours/--theirs와 rebase의 반전

Git 충돌 표시가 보여주는 내용과 인덱스의 세 단계, --ours와 --theirs의 의미를 병합과 rebase 상황별로 설명합니다. 충돌 없이 끝난 병합도 동작을 깨뜨릴 수 있으므로 병합 결과를 대상으로 테스트해야 한다고 짚습니다.

AI 요약

rebase 중 충돌이 난 파일에서 내 변경을 남기려 git checkout --ours를 실행했는데, 오히려 내 변경이 사라질 수 있습니다. rebase에서는 --ours가 현재 기준인 main 쪽을 가리키고, 다시 적용 중인 내 커밋은 --theirs이기 때문입니다. 이 글은 충돌 표시를 읽는 법부터 파일 전체를 한쪽 버전으로 선택하는 법, rebase 중단과 병합 후 테스트까지 단계별로 설명합니다. 실습 자료로는 브라우저에서 명령어와 편집을 연습하는 Git Merge Conflict Simulator를 소개합니다. 시뮬레이터는 실제 Git을 실행하지 않고, 각 수업에 필요한 명령과 출력을 모델링합니다.

충돌 표시와 인덱스의 세 버전

병합 충돌이 나면 파일에는 <<<<<<<, =======, >>>>>>> 표시가 들어갑니다. 예시에서는 main이 제한 시간을 30초에서 45초로 올렸고, 기능 브랜치는 60초로 올렸습니다. 표시에는 양쪽 변경이 나오지만, 공통 조상 버전은 기본 충돌 표시에서 빠집니다. 충돌이 해결되지 않은 동안 Git 인덱스에는 파일 버전이 최대 세 개 들어갑니다. stage 1은 공통 조상, stage 2는 ours, stage 3은 theirs입니다. 편집하기 전에 git show :1:config.yaml을 실행하면 기준이 30초였음을 확인할 수 있습니다. 양쪽 모두 값을 올렸으므로, 어느 쪽이 잘못했는지가 아니라 어떤 변경을 채택할지 판단하면 됩니다.

공통 조상 버전도 충돌 안에 표시하고 싶다면 Git 2.35부터 지원하는 git config --global merge.conflictStyle zdiff3를 설정합니다. 그러면 ||||||| 구역에 기준 버전이 함께 나옵니다.

파일을 직접 합치거나 한쪽 버전을 선택하기

두 브랜치가 같은 위치에 서로 다른 패키지를 추가했지만 프로젝트에 둘 다 필요하다면 --ours나 --theirs로 해결할 수 없습니다. 편집기에서 충돌 표시 세 줄을 지우고 두 패키지를 모두 남긴 뒤 저장해야 합니다. 해결한 파일은 git add requirements.txt로 스테이징합니다. 이 명령은 충돌을 해결했다고 Git에 알리며, 인덱스의 세 버전을 스테이징한 파일 하나로 바꿉니다. Git은 파일 내용을 검사하지 않으므로 충돌 표시가 남아 있어도 스테이징을 허용합니다. 커밋 전에 git diff --cached --check를 실행하면 남은 충돌 표시를 확인할 수 있습니다.

바이너리 파일에는 텍스트 충돌 표시가 생기지 않습니다. 예시의 로고 파일은 작업 트리에 main 버전이 남고, 인덱스에는 세 버전이 보관됩니다. 병합 중 --ours는 현재 브랜치, --theirs는 병합해 들어오는 브랜치를 뜻합니다. 들어오는 로고를 택하려면 git checkout --theirs assets/logo.png를 실행한 뒤 스테이징합니다. 한쪽 버전을 통째로 선택하므로 다른 쪽 파일에 있던 변경은 사라집니다.

rebase에서는 ours와 theirs가 반대로 보입니다

rebase는 main의 최신 커밋을 체크아웃한 뒤, 내 커밋을 하나씩 그 위에 다시 적용합니다. 따라서 충돌이 난 시점의 HEAD와 --ours는 main과 지금까지 다시 적용한 변경을 가리킵니다. --theirs는 현재 재적용 중인 내 커밋입니다. git checkout 매뉴얼도 git rebase와 git pull --rebase에서 ours와 theirs가 바뀐 것처럼 보일 수 있다고 설명합니다.

글의 예시에서는 main이 분당 200건을 허용하고, 내 브랜치는 제한을 30건으로 강화합니다. rebase 후에도 30을 유지하려면 git checkout --theirs limits.py를 실행하고, 파일을 스테이징한 뒤 git rebase --continue를 실행합니다. 실습에서 충돌 표시의 HEAD 쪽에는 200, 커밋 해시가 붙은 쪽에는 30이 나옵니다. git show :2:limits.py와 git show :3:limits.py로 각 버전을 확인할 수도 있습니다. 잘못 --ours를 실행했지만 아직 스테이징하지 않았다면 git checkout -m limits.py로 충돌 상태를 다시 만들 수 있습니다.

잘못 시작한 병합을 되돌리고, 병합 결과를 테스트하기

엉뚱한 릴리스 브랜치를 병합해 여러 파일에서 충돌이 났다면 git merge --abort로 병합을 취소할 수 있습니다. rebase 중에는 git rebase --abort를 사용합니다. 다만 병합을 시작하기 전 작업 트리에 커밋하지 않은 변경이 있었다면 abort가 이를 복원하지 못할 수 있습니다. 작업을 시작하기 전에 커밋하거나 stash에 보관하라고 안내합니다.

충돌 표시 없이 병합이 성공해도 코드가 정상이라는 보장은 없습니다. 예시에서는 main이 settings.get_timeout()을 get_request_timeout()으로 바꾸고 기존 호출부를 수정했습니다. 오래된 기능 브랜치는 별도 파일에 옛 이름을 쓰는 새 호출부를 추가했습니다. 텍스트 병합은 성공하지만 make test에서 실패하고, 개발자는 worker.py의 한 줄을 고쳐야 합니다. 각 브랜치의 테스트가 통과해도 결합한 코드가 깨질 수 있으므로 CI는 병합 결과를 테스트해야 합니다. 병합 큐(merge queue)를 사용해 코드가 반영되기 전에 결합된 변경을 검증하는 방법도 제시합니다.

dev.to 반응

  • @alexcodebytes — config.yaml의 병합 충돌은 개발자의 인내심을 시험하는 궁극의 과제입니다. 대부분은 'Accept Ours'나 'Accept Theirs'를 무작정 누르고 CI/CD 파이프라인에 불이 나지 않기를 바라는데, :1:config.yaml을 배우면 큰 도움이 됩니다.
  • @mrsaynothing — rebase에서 ours가 뒤집힌다는 점을 강조한 게 좋습니다. --ours를 쓰는 해결 스크립트는 누군가 rebase 도중 실행하는 순간 조용히 잘못 동작합니다. 저희는 스크립트가 해결 작업을 하기 전에 rebase 상태를 확인하는 가드를 넣었습니다. 키워드 하나가 가리키는 쪽이 바뀌는 상황을 막는 저렴한 보험입니다.
  • @marcusykim — 6번 수업에 worker의 timeout 값을 확인하는 테스트도 추가하겠습니다. get_request_timeout()이 있는지만 검사하지 말자는 뜻입니다. 이름이 바뀐 호출부는 쉽게 찾을 수 있지만, 병합 결과에서 worker가 예전 기본값을 쓰는 문제는 간단한 스모크 테스트를 통과할 수 있습니다. 그러면 병합된 코드가 실행되는지, 두 브랜치가 의도한 동작을 계속 지키는지 따로 확인할 수 있습니다.

원문: dev.to / 번역·요약: Trawling