Git 실전 · 리베이스와 충돌 · 실습
리베이스와 충돌 해결
목표
리베이스가 커밋을 옮기는 것이 아니라 재작성한다는 사실을 해시로 확인하고, 충돌 해결과 중단, 대화형 리베이스의 squash/drop 까지 손으로 해 봅니다. 산출물은 /root/gitx3/ 아래에 둡니다.
왜 중요한가
커밋 객체 안에는 부모 해시가 들어 있습니다. 부모가 달라지면 커밋 해시는 반드시 달라집니다. 이 한 문장에서 나머지가 전부 따라옵니다. 남이 그 커밋 위에 작업을 쌓아 두었다면 재작성은 그 사람의 이력을 깨뜨립니다. 그래서 황금률의 정확한 형태는 "이미 푸시했으면 하지 마라"가 아니라 "다른 사람의 작업이 그 위에 쌓여 있는 커밋은 재작성하지 않는다"입니다. 충돌이 반복되는 이유도 같은 구조에서 나옵니다. 머지는 세 지점만 비교하지만 리베이스는 커밋을 하나씩 다시 적용하며 매번 삼자 병합을 하므로, 리베이스는 한 번의 병합이 아니라 N번의 병합입니다.
단계
전역 git 신원이 없습니다. 새 저장소마다 로컬 user.email / user.name 을 설정하세요.
1. /root/gitx3/repo 에 저장소를 만듭니다. main 에 커밋 3개를 쌓고 거기서 topic 브랜치를 만들어 커밋 2개를 추가한 뒤, 다시 main 으로 돌아와 커밋 1개를 더 쌓아 갈라진 상태를 만듭니다(topic 의 총 커밋은 4개 이상). 그리고 /root/gitx3/diverged.txt 에 세 줄을 적습니다: main_ahead=1, topic_ahead=2, merge_base=<두 브랜치의 공통 조상 해시>.
2. topic 을 main 위로 리베이스합니다. 리베이스 전에 topic 의 끝 해시를 기록해 두고, /root/gitx3/rebased.txt 에 before=<리베이스 전 해시> 와 after=<리베이스 후 해시> 두 줄을 적습니다. 두 값은 서로 달라야 하고, after 는 현재 topic 의 끝과 일치해야 하며, 리베이스가 진행 중인 상태로 남아 있으면 안 됩니다.
3. main 에서 topic 을 빨리 감기로 병합합니다. 결과: main 의 머지 커밋 0개, main 의 총 커밋 6개 이상, main 과 topic 이 같은 커밋을 가리킬 것.
4. /root/gitx3/conflict 에 새 저장소를 만듭니다. main 에 shared.txt 를 두고, conflict-topic 브랜치에서 그 파일을 고쳐 from-topic 이라는 문자열이 들어가게 커밋합니다. 그다음 main 에서도 같은 자리를 고쳐 from-main 이 들어가게 커밋합니다. 이제 conflict-topic 을 main 위로 리베이스하면 충돌이 납니다. 두 변경을 모두 남기도록 해결하세요. 완료 조건: 리베이스가 끝나 있을 것, 인덱스에 미해결 경로가 없을 것, <<<<<<< 같은 충돌 표식이 파일에 남아 있지 않을 것, shared.txt 에 from-main 과 from-topic 이 둘 다 있을 것, 작업 트리가 깨끗할 것.
5. /root/gitx3/abort 에 새 저장소를 만들고 risky 브랜치와 main 이 같은 파일을 서로 다르게 고치도록 커밋합니다. 리베이스를 시작하기 전에 risky 의 끝 해시를 /root/gitx3/abort-before.txt 에 저장합니다(해시 문자열만). 그다음 리베이스를 시작해 충돌을 만나고 중단합니다. 완료 조건: 리베이스가 진행 중이 아닐 것, risky 의 끝이 기록해 둔 해시와 정확히 같을 것, reflog 에 rebase 기록이 있을 것.
6. /root/gitx3/squash 에 새 저장소를 만들고 feature/three 브랜치에 커밋 3개를 쌓습니다(각각 s1.txt, s2.txt, s3.txt 추가). 대화형 리베이스로 3개를 1개로 합칩니다. 완료 조건: main..feature/three 커밋 수가 1개, 그 커밋 제목에 feat: combined 포함, s1.txt/s2.txt/s3.txt 셋 다 존재.
7. /root/gitx3/drop 에 새 저장소를 만들고 feature/four 브랜치에 커밋 4개를 쌓습니다: d1.txt, d2.txt, 그리고 제목이 debug: temp log 이고 debug.txt 를 추가하는 커밋, 마지막으로 d3.txt. 대화형 리베이스로 디버그 커밋만 버립니다. 완료 조건: main..feature/four 가 3개, debug: temp log 커밋 없음, debug.txt 없음, d1/d2/d3.txt 모두 존재.
8. /root/gitx3/rebase.md 를 씁니다. 반드시 포함할 것: --force-with-lease 설명과 --force 와의 차이, 언제 리베이스하면 안 되는지(공유·푸시·다른 사람 중 한 표현 포함), 왜 해시가 바뀌는지(해시·재작성 중 한 표현 포함), 그리고 merge_commits_on_main= 줄에 /root/gitx3/repo 의 main 머지 커밋 개수를 실제로 세어 적습니다.
참고
- 앞선 개수 세기:
git rev-list --count topic..main(main 이 앞선 수),git rev-list --count main..topic(topic 이 앞선 수), 공통 조상은git merge-base main topic. - 편집기가 없는 환경에서 대화형 리베이스: 목록은
GIT_SEQUENCE_EDITOR='sed -i "2,3s/^pick/fixup/"' git rebase -i main처럼, 커밋 메시지 편집은GIT_EDITOR=true로 우회할 수 있습니다. squash 대신 fixup 으로 합친 뒤git commit --amend -m "feat: combined"로 제목을 정해도 됩니다. - drop 은 목록에서 해당 줄의
pick을drop으로 바꾸거나 줄을 지우면 됩니다. - 충돌 해결 후에는
git add <파일>다음git rebase --continue입니다. 편집기가 뜨면GIT_EDITOR=true를 앞에 붙이세요. - 흔한 실수 1: 충돌을
--ours나--theirs로 한쪽만 골라 넘기는 것. 상대 작업이 조용히 사라집니다. 게다가 리베이스 중에는 재생되는 커밋 쪽이 theirs 라서 의미가 직관과 반대로 느껴집니다. - 흔한 실수 2: 2단계에서 리베이스를 먼저 하고 나서
before해시를 찾으려는 것. 리베이스 후에는git reflog를 봐야 하므로, 시작 전에git rev-parse topic을 먼저 저장해 두세요.
단계 8개
- 갈라진 두 브랜치 만들기
- topic 을 main 위로 재작성
- 빨리 감기로 일직선 만들기
- 충돌을 양쪽 보존하며 해결
- 리베이스 중단하고 원상 복구
- 커밋 3개를 1개로 합치기
- 커밋 하나 버리기
- 황금률 정리