Git 실전 · 리베이스와 충돌 · 이론
리베이스는 N번의 병합이다
한 줄 요약
커밋 객체 안에는 부모 해시가 들어 있다. 부모가 달라지면 커밋 해시는 반드시 달라진다. 즉 리베이스는 커밋을 이동시키는 명령이 아니라 재작성하는 명령이다.
왜 이게 필요했나
"이미 푸시한 커밋은 리베이스하지 마라"는 조금 부정확하다. 본질은 푸시 여부가 아니라, 다른 사람의 저장소에 그 커밋 객체가 있는지, 그리고 그 위에 누군가의 작업이 얹혀 있는지다. 그래서 황금률의 정확한 형태는 이렇다. 다른 사람의 작업이 그 위에 쌓여 있는 커밋은 재작성하지 않는다.
혼자 쓰는 원격 브랜치를 정리하려고 리베이스하는 것은 문제가 아니다. 문제는 남이 그 위에서 일하고 있을 때다.
어떻게 동작하나
같은 충돌이 리베이스에서 반복되는 이유도 구조에서 나온다. 머지는 양쪽 끝점과 공통 조상 세 지점만 비교하지만, 리베이스는 커밋을 하나씩 다시 적용하며 그때마다 삼자 병합을 새로 한다. 리베이스는 한 번의 병합이 아니라 N번의 병합이다. 그래서 커밋 다섯 개를 리베이스하면 같은 충돌을 다섯 번 볼 수 있다. rerere.enabled true 를 켜 두면 한 번 해결한 충돌 해법을 기억해 다시 적용한다.
충돌 상태란 인덱스에 한 경로의 후보가 셋(1 공통 조상 / 2 ours / 3 theirs) 들어 있는 상태다. merge.conflictStyle zdiff3 를 권하는 이유가 여기 있다. 상대가 무엇을 바꿨는지가 아니라 둘 다 무엇에서 출발했는지를 알아야 올바르게 합칠 수 있다.
대화형 리베이스의 동사는 여섯 개다. pick(유지), reword(메시지만), edit(내용 수정), squash(합치고 메시지도 합침), fixup(합치고 메시지는 버림), drop(삭제).
--force-with-lease 는 원격 추적 참조가 내가 마지막으로 본 값과 같은지 확인한다. 다만 구멍이 있다. IDE 가 방금 fetch 했다면 참조가 이미 갱신돼 있어서, 내가 보지 못한 커밋이 들어왔는데도 검사를 통과한다. push.useForceIfIncludes true 로 보완한다.
현장에서 만나는 모습
리베이스 중 충돌이 났을 때 가장 흔한 실수는 한쪽을 통째로 골라 버리는 것이다. --ours 나 --theirs 로 빠르게 넘어가면 상대의 작업이 조용히 사라진다. 게다가 리베이스 중에는 ours 와 theirs 의 의미가 직관과 반대로 느껴진다. 재생되는 커밋 쪽이 theirs 이기 때문이다.
또 하나는 중단하는 법을 모르는 것이다. git rebase --abort 는 시작 전 상태로 정확히 되돌린다. 막혔을 때 억지로 밀고 나가는 것보다 중단하고 다시 계획하는 편이 거의 항상 낫다.
다음 실습에서 할 것
갈라진 두 브랜치를 만들어 리베이스 전후 해시를 직접 비교하고, 빨리 감기로 일직선 이력을 만들고, 충돌을 일부러 만들어 양쪽 변경을 모두 보존하며 해결하고, 한 번은 일부러 중단해 원상 복구를 확인합니다. 그다음 대화형 리베이스로 커밋 세 개를 하나로 합치고 하나를 버린 뒤, 황금률을 자기 문장으로 정리합니다.