어려운 Git 병합 충돌을 단계별로 해결하는 방법

우리 쪽, 상대 쪽, 기본 버전을 읽고 이름 변경, 리베이스, 바이너리 파일, 테스트를 처리하여 어려운 Git 병합 충돌을 해결합니다.

어려운 Git 병합 충돌을 단계별로 해결하는 방법

어려운 Git 병합 충돌은 충돌 표시 자체 때문에 어려운 경우는 드뭅니다. 두 가지 다른 변경 사항의 의도를 동시에 유지해야 하기 때문에 어렵습니다. 한 브랜치에서 함수 이름을 바꾸고 다른 브랜치에서 동작을 변경했습니다. 한 브랜치에서 파일을 이동하고 다른 브랜치에서 편집했습니다. 한 브랜치에서 데이터베이스 마이그레이션 순서를 변경했습니다. Git은 겹치는 부분을 보여줄 수 있지만, 이후 소프트웨어가 무엇을 의미해야 하는지 결정할 수는 없습니다.

병합이 충돌로 중단되면 즉시 표시를 삭제하기 시작하지 마십시오. 먼저 방향을 파악하십시오.

git status

Git은 병합되지 않은 경로를 나열합니다. both modified, deleted by us, deleted by them, both added 또는 이와 유사한 내용이 표시될 수 있습니다. 이러한 문구는 충돌의 형태를 알려줍니다.

병합이 잘못된 것 같거나 해결할 준비가 되지 않은 경우, 더 많은 변경을 하기 전에 중단하십시오:

git merge --abort

리베이스 충돌의 경우 동등한 명령은 다음과 같습니다:

git rebase --abort

이는 실패가 아닙니다. 작업 전 상태로 깔끔하게 재설정하는 것이며, 더 많은 컨텍스트가 필요하다는 것을 깨달았을 때 종종 가장 현명한 방법입니다.

충돌을 세 가지 버전으로 읽기

일반적인 충돌 표시는 다음과 같습니다:

<<<<<<< HEAD
현재 브랜치 버전
=======
들어오는 브랜치 버전
>>>>>>> feature-branch

병합 중에 HEADgit merge를 실행할 때 체크아웃한 브랜치입니다. 아래쪽은 병합 중인 브랜치입니다.

어려운 충돌의 경우 Git이 인덱스에 유지하는 세 가지 단계를 사용하십시오:

git show :1:path/to/file   # 공통 조상
git show :2:path/to/file   # 우리 쪽
git show :3:path/to/file   # 상대 쪽

공통 조상은 두 브랜치가 시작된 버전입니다. 각 브랜치가 실제로 무엇을 변경했는지 보여주기 때문에 유용합니다. 이것 없이 두 최종 버전을 비교하면 그 이유를 놓칠 수 있습니다.

다음을 사용할 수도 있습니다:

git diff
git diff --ours -- path/to/file
git diff --theirs -- path/to/file
git diff --base -- path/to/file

이 부분에서 많은 사람들이 너무 빠르게 진행합니다. 목표는 팀 충성도 투표로 "우리 쪽" 또는 "상대 쪽"을 선택하는 것이 아닙니다. 목표는 올바른 최종 파일을 생성하는 것입니다.

안전한 수동 워크플로우

충돌이 있는 각 파일에 대해 이 루틴을 사용하십시오:

  1. 파일을 열고 모든 충돌 표시를 찾습니다.
  2. 표시된 줄뿐만 아니라 주변 코드를 읽습니다.
  3. 두 브랜치에서 파일을 건드린 커밋을 확인합니다.
  4. 파일을 최종 의도된 버전으로 편집합니다.
  5. 모든 충돌 표시를 제거합니다.
  6. 가장 작은 관련 테스트 또는 빌드 검사를 실행합니다.
  7. 파일을 스테이징합니다.

이 작업을 수행하는 동안 유용한 명령:

git log --oneline --left-right --merge -- path/to/file
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

git diff --check는 남아있는 공백 문제를 잡아냅니다. git grep은 CI에 도달하기 전에 잊혀진 충돌 표시를 잡아냅니다.

하나의 파일을 해결한 후:

git add path/to/file

모든 충돌이 스테이징되면:

git status
git commit

리베이스 중에는 다음을 사용하십시오:

git rebase --continue

두 브랜치가 동일한 함수를 변경한 경우

이것은 일반적인 경우입니다. 한 브랜치가 유효성 검사를 추가하고 다른 브랜치가 매개변수 이름을 바꾼다고 가정합니다:

<<<<<<< HEAD
function createUser(email) {
  return db.users.insert({ email });
}
=======
function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}
>>>>>>> normalize-email

올바른 답은 둘을 결합하는 것일 수 있습니다:

function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}

그러나 호출자가 rawEmail을 전달하도록 업데이트되었고 정규화가 여전히 필요한 경우에만 가능합니다. 함수를 검색하십시오:

git grep -n 'createUser'

어려운 충돌은 종종 주변 파일을 확인해야 합니다. 한 파일의 함수 시그니처 충돌은 테스트, 라우트, 타입, 모의 객체 또는 문서에서 업데이트가 필요할 수 있습니다.

이름 변경 및 편집 충돌

이름 변경 충돌은 원하는 파일이 예상한 위치에 없을 수 있기 때문에 성가십니다. 상태부터 시작하십시오:

git status --short

그런 다음 이름-상태 정보를 검사하십시오:

git diff --name-status --diff-filter=R

한 브랜치가 src/user.jssrc/account.js로 이름 변경하고 다른 브랜치가 src/user.js를 편집한 경우, 일반적으로 편집된 내용을 새 경로에 적용하려고 합니다. 시각적 병합 도구가 도움이 될 수 있지만 개념은 간단합니다: 이름 변경을 유지하고 의미 있는 편집을 유지하십시오.

최종 경로를 결정한 후 필요하면 이전 경로를 제거하고 최종 경로를 스테이징하십시오:

git rm old/path.js
git add new/path.js

최종 프로젝트에 실제로 두 파일이 모두 포함되어야 하는 경우가 아니라면 두 파일을 모두 스테이징하지 마십시오.

우리 쪽에서 삭제 또는 상대 쪽에서 삭제

삭제/수정 충돌은 한 브랜치가 파일을 삭제하고 다른 브랜치가 변경했음을 의미합니다. Git은 삭제가 변경을 무의미하게 만들었는지 알 수 없습니다.

파일이 삭제된 상태로 유지되어야 하는 경우:

git rm path/to/file

파일이 유지되어야 하는 경우 원하는 버전을 선택하고 스테이징하십시오:

git checkout --theirs path/to/file
git add path/to/file

또는:

git checkout --ours path/to/file
git add path/to/file

리베이스 중에는 --ours--theirs에 주의하십시오. 리베이스에서는 Git이 커밋을 다른 베이스에 재생하기 때문에 레이블이 반전된 것처럼 느껴질 수 있습니다. 확실하지 않으면 단계를 검사하십시오:

git show :2:path/to/file
git show :3:path/to/file

바이너리 파일 충돌

Git은 대부분의 바이너리 파일을 병합할 수 없습니다. 두 브랜치가 동일한 이미지, 아카이브, 문서 또는 컴파일된 자산을 변경한 경우 한 버전을 선택하거나 수동으로 새 파일을 만들어야 합니다.

우리 버전을 사용하려면:

git checkout --ours path/to/file.bin
git add path/to/file.bin

상대 버전을 사용하려면:

git checkout --theirs path/to/file.bin
git add path/to/file.bin

바이너리가 생성된 경우, 텍스트 파일을 해결한 후 소스에서 다시 생성하는 것이 가장 좋은 답일 수 있습니다. 바이너리가 디자인 자산 또는 문서인 경우 다른 쪽을 변경한 사람과 상담하십시오. 추측은 작업을 망칠 수 있습니다.

파일을 읽기 너무 어려울 때 병합 도구 사용

좋은 병합 도구는 네 가지를 보여줍니다: 기본 버전, 사용자 버전, 상대 버전 및 결과. 실제로 좋아하는 도구를 구성하십시오. Visual Studio Code가 일반적입니다:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

그런 다음 실행:

git mergetool

다른 팀은 Meld, KDiff3, Beyond Compare 또는 IDE 통합 도구를 선호합니다. 도구는 세 가지 버전을 이해하는 것보다 덜 중요합니다. 빨간 표시를 없애기 위해 복잡한 충돌에서 "들어오는 것 수락"을 클릭하지 마십시오.

병합 도구를 사용한 후 .orig와 같은 백업 파일을 확인하십시오:

git status --short

원하지 않는 경우 전역적으로 병합 도구 백업 파일을 비활성화할 수 있습니다:

git config --global mergetool.keepBackup false

전략 옵션은 마법이 아닙니다

다음과 같은 조언을 볼 수 있습니다:

git merge -X theirs feature

이는 "내 브랜치를 feature로 교체"를 의미하지 않습니다. Git의 병합 전략이 충돌하는 덩어리를 볼 때 해당 덩어리에 대해 상대 쪽을 선호해야 함을 의미합니다. 두 브랜치의 충돌하지 않는 변경 사항은 여전히 병합됩니다. 이는 생성된 잠금 파일 또는 기계적 서식 충돌에 유용할 수 있지만 비즈니스 로직에는 위험합니다.

-X ours-X theirs는 전략 옵션입니다. ours 병합 전략은 다릅니다:

git merge -s ours old-branch

이는 현재 트리를 유지하면서 병합을 기록합니다. 내용을 가져오지 않고 브랜치를 병합된 것으로 표시하는 데 자주 사용되는 특수 도구입니다. 매우 확실하지 않은 한 일반 충돌 해결에 사용하지 마십시오.

리베이스 충돌

리베이스 중에 Git은 커밋을 한 번에 하나씩 재생합니다. 즉, 하나의 큰 병합 충돌 대신 여러 개의 작은 충돌을 해결할 수 있습니다.

루프는 다음과 같습니다:

git status
# 파일 편집
git add resolved-file
git rebase --continue

재생 중인 커밋이 새 베이스에 이미 변경 사항이 포함되어 있어 더 이상 필요하지 않은 경우 다음을 사용하십시오:

git rebase --skip

건너뛰기를 신중하게 사용하십시오. 리베이스된 브랜치에서 해당 커밋을 삭제합니다. 먼저 커밋을 읽으십시오:

git show

다시 말하지만, 리베이스에서 --ours--theirs는 혼란스러울 수 있습니다. 의심스러울 때 :2::3:을 검사하십시오.

병합뿐만 아니라 해결 방법 테스트

병합은 구문적으로 해결되었지만 여전히 잘못될 수 있습니다. 파일을 스테이징한 후 변경된 영역을 건드리는 테스트를 실행하십시오. 프론트엔드 충돌의 경우 타입 검사와 집중된 컴포넌트 테스트일 수 있습니다. 백엔드 충돌의 경우 하나의 서비스 테스트 또는 마이그레이션 확인일 수 있습니다. 잠금 파일 충돌의 경우 종속성을 다시 설치하고 패키지 관리자의 확인 명령을 실행하십시오.

최소한:

git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

그런 다음 잘못된 조합을 잡았을 프로젝트별 검사를 실행하십시오.

미래 충돌 줄이기

가장 좋은 충돌은 만들지 않는 충돌입니다. 브랜치를 단기간 유지하고, 정기적으로 메인 브랜치에서 리베이스 또는 병합하고, 기계적 변경과 기능 변경을 혼합하지 마십시오. 서식 전용 PR은 로직도 변경해서는 안 됩니다. 가능하면 파일 이동은 파일을 다시 작성해서는 안 됩니다.

항상 고통스러운 파일의 경우 소유권 또는 구조 변경을 고려하십시오. 대규모 구성 파일, 생성된 스냅샷, 잠금 파일, 마이그레이션 목록 및 중앙 라우트 레지스트리는 모든 사람이 동일한 영역을 편집하기 때문에 반복적인 충돌을 만듭니다. 때로는 해결책이 프로세스입니다. 때로는 해결책이 파일을 분할하거나 더 작은 소스에서 생성하는 것입니다.

특별한 병합 동작이 필요한 파일에 .gitattributes를 사용하십시오. 예를 들어, 일부 생성된 잠금 파일에는 패키지 관리자별 병합 드라이버가 있을 수 있습니다. 가볍게 발명하지 말고, 생태계에 권장 드라이버가 있는지 확인하십시오.

병합 충돌은 부분적으로 기술 작업이고 부분적으로 커뮤니케이션입니다. 다른 브랜치의 의도를 이해하지 못하면 물어보십시오. 작성자와 10분 동안 이야기하는 것이 테스트를 통과하지만 그들이 구축 중인 기능을 제거하는 코드를 조용히 병합하는 것보다 저렴합니다.

잠금 파일, 마이그레이션 및 기타 마찰이 큰 파일

일부 파일은 많은 브랜치가 동일한 작은 영역을 편집하기 때문에 더 자주 충돌합니다. 종속성 잠금 파일이 일반적인 예입니다. 두 브랜치가 패키지를 추가하는 경우 잠금 파일 충돌은 기술적으로 크지만 개념적으로 간단할 수 있습니다: 매니페스트 파일을 해결한 후 패키지 관리자로 다시 생성하십시오.

Node 프로젝트의 경우 package.json을 해결한 다음 잠금 파일을 소유한 패키지 관리자를 실행하는 것을 의미할 수 있습니다:

npm install
# 또는 pnpm install
# 또는 yarn install

그런 다음 매니페스트와 잠금 파일을 모두 스테이징하십시오. 형식을 이해하지 못하면 복잡한 잠금 파일을 수동으로 편집하지 마십시오. 패키지 관리자는 미묘한 종속성 그래프 실수를 할 가능성이 적습니다.

데이터베이스 마이그레이션은 더 많은 주의가 필요합니다. 두 브랜치가 순서 가정으로 마이그레이션을 생성하는 경우 두 파일을 모두 수락하는 것만으로는 충분하지 않을 수 있습니다. 마이그레이션 타임스탬프, 시퀀스 번호, 종속성 및 두 마이그레이션이 동일한 테이블이나 데이터를 수정하는지 확인하십시오. 때로는 올바른 해결 방법이 두 브랜치를 조정하는 새로운 후속 마이그레이션입니다.

생성된 스냅샷 및 골든 파일도 동일한 패턴을 따릅니다: 먼저 소스 변경을 해결하고, 출력을 다시 생성한 다음 생성된 diff를 검토하십시오. 생성된 diff가 방대한 경우 동일한 병합 커밋에 속하는지 물어보십시오. 거대한 생성된 변경은 잘못된 수동 해결을 숨길 수 있습니다.

충돌이 여러 파일에 걸쳐 있는 경우 편집하기 전에 의도된 최종 동작을 기록하십시오. "기능 A의 새 유효성 검사 유지, 기능 B의 이름 변경된 서비스 유지, 클라이언트 타입 다시 생성"과 같은 짧은 메모는 각 파일을 로컬에서 해결하면서 전체 설계를 잃는 것을 방지합니다.

특히 위험한 병합의 경우 시작하기 전에 임시 브랜치를 만드십시오:

git switch -c merge-test/main-with-feature
git merge feature

해결이 지저분해지면 원래 브랜치를 방해하지 않고 임시 브랜치를 포기할 수 있습니다. 이 작은 습관은 항상 깨끗한 돌아갈 방법이 있기 때문에 어려운 충돌을 덜 스트레스로 만듭니다.

최종 병합을 자체 변경 사항으로 검토

충돌 해결은 새로운 작업입니다. 검토에서 그렇게 취급하십시오. 최종 diff는 두 브랜치의 변경 사항뿐만 아니라 함께 작동하도록 작성한 접착제 코드도 보여야 합니다. 병합 커밋이 큰 경우 커밋 메시지 또는 풀 리퀘스트 댓글에서 해결 방법을 설명하십시오. 검토자는 한 쪽이 선택된 이유를 역공학할 필요가 없어야 합니다.

푸시하기 전에 가능하면 두 부모에 대해 최종 결과를 비교하십시오:

git diff HEAD^1..HEAD -- path/to/file
git diff HEAD^2..HEAD -- path/to/file

커밋되지 않은 병합의 경우 스테이징된 변경 사항을 검사하십시오:

git diff --cached

테스트의 우발적 삭제, 더 이상 사용되지 않는 가져오기, 중복된 구성 항목 및 두 브랜치가 다른 이름으로 유사한 로직을 추가한 코드 경로를 찾으십시오. 이것들은 Git이 당신을 위해 식별할 수 없는 실수입니다.

충돌이 동작과 관련된 경우, 잘못된 쪽을 선택했다면 실패할 테스트를 추가하거나 업데이트하십시오. 그 테스트는 오늘의 병합을 증명하는 것 이상을 합니다. 다음 리팩터링에서 결정이 취소되는 것을 보호합니다.