リベースとコンフリクトの解消
한국어 원문으로 표시합니다.
목표
리베이스가 커밋을 옮기는 것이 아니라 재작성한다는 사실을 해시로 확인하고, 충돌 해결과 중단, 대화형 리베이스의 squash/drop 까지 손으로 해 봅니다. 산출물은 /root/gitx3/ 아래에 둡니다.
왜 중요한가
커밋 객체 안에는 부모 해시가 들어 있습니다. 부모가 달라지면 커밋 해시는 반드시 달라집니다. 이 한 문장에서 나머지가 전부 따라옵니다. 남이 그 커밋 위에 작업을 쌓아 두었다면 재작성은 그 사람의 이력을 깨뜨립니다. 그래서 황금률의 정확한 형태는 "이미 푸시했으면 하지 마라"가 아니라 "다른 사람의 작업이 그 위에 쌓여 있는 커밋은 재작성하지 않는다"입니다. 충돌이 반복되는 이유도 같은 구조에서 나옵니다. 머지는 세 지점만 비교하지만 리베이스는 커밋을 하나씩 다시 적용하며 매번 삼자 병합을 하므로, 리베이스는 한 번의 병합이 아니라 N번의 병합입니다.
단계
전역 git 신원이 없습니다. 새 저장소마다 로컬 user.email / user.name 을 설정하세요.
/root/gitx3/repo에 저장소를 만듭니다.main에 커밋 3개를 쌓고 거기서topic브랜치를 만들어 커밋 2개를 추가한 뒤, 다시main으로 돌아와 커밋 1개를 더 쌓아 갈라진 상태를 만듭니다(topic의 총 커밋은 4개 이상). 그리고/root/gitx3/diverged.txt에 세 줄을 적습니다:main_ahead=1,topic_ahead=2,merge_base=<두 브랜치의 공통 조상 해시>.topic을main위로 리베이스합니다. 리베이스 전에topic의 끝 해시를 기록해 두고,/root/gitx3/rebased.txt에before=<리베이스 전 해시>와after=<리베이스 후 해시>두 줄을 적습니다. 두 값은 서로 달라야 하고,after는 현재topic의 끝과 일치해야 하며, 리베이스가 진행 중인 상태로 남아 있으면 안 됩니다.main에서topic을 빨리 감기로 병합합니다. 결과:main의 머지 커밋 0개,main의 총 커밋 6개 이상,main과topic이 같은 커밋을 가리킬 것./root/gitx3/conflict에 새 저장소를 만듭니다.main에shared.txt를 두고,conflict-topic브랜치에서 그 파일을 고쳐from-topic이라는 문자열이 들어가게 커밋합니다. 그다음main에서도 같은 자리를 고쳐from-main이 들어가게 커밋합니다. 이제conflict-topic을main위로 리베이스하면 충돌이 납니다. 두 변경을 모두 남기도록 해결하세요. 완료 조건: 리베이스가 끝나 있을 것, 인덱스에 미해결 경로가 없을 것,<<<<<<<같은 충돌 표식이 파일에 남아 있지 않을 것,shared.txt에from-main과from-topic이 둘 다 있을 것, 작업 트리가 깨끗할 것./root/gitx3/abort에 새 저장소를 만들고risky브랜치와main이 같은 파일을 서로 다르게 고치도록 커밋합니다. 리베이스를 시작하기 전에risky의 끝 해시를/root/gitx3/abort-before.txt에 저장합니다(해시 문자열만). 그다음 리베이스를 시작해 충돌을 만나고 중단합니다. 완료 조건: 리베이스가 진행 중이 아닐 것,risky의 끝이 기록해 둔 해시와 정확히 같을 것, reflog 에 rebase 기록이 있을 것./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셋 다 존재./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모두 존재./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을 먼저 저장해 두세요.
갈라진 두 브랜치 만들기
/root/gitx3/repo 에 저장소를 만듭니다. main 에 커밋 3개를 쌓고 거기서 topic 브랜치를 만들어 커밋 2개를 추가한 뒤, 다시 main 으로 돌아와 커밋 1개를 더 쌓아 갈라진 상태를 만듭니다(topic 의 총 커밋은 4개 이상). 그리고 /root/gitx3/diverged.txt 에 세 줄을 적습니다: main_ahead=1, topic_ahead=2, merge_base=<두 브랜치의 공통 조상 해시>.
/root/gitx3/repo 에 main 과 topic 이 서로 다른 커밋을 갖도록 만듭니다. 분기 이후 양쪽에 각각 커밋이 있어야 '갈라진' 상태입니다. 앞선 개수는 git rev-list --count A..B 로 셀 수 있습니다.
topic 을 main 위로 재작성
topic 을 main 위로 리베이스합니다. 리베이스 전에 topic 의 끝 해시를 기록해 두고, /root/gitx3/rebased.txt 에 before=<리베이스 전 해시> 와 after=<리베이스 후 해시> 두 줄을 적습니다. 두 값은 서로 달라야 하고, after 는 현재 topic 의 끝과 일치해야 하며, 리베이스가 진행 중인 상태로 남아 있으면 안 됩니다.
리베이스 전에 topic 의 끝 해시를 먼저 기록해 두어야 비교할 수 있습니다. 리베이스가 끝나면 공통 조상이 main 의 끝과 같아지고, 부모가 달라졌으므로 해시도 달라집니다.
빨리 감기로 일직선 만들기
main 에서 topic 을 빨리 감기로 병합합니다. 결과: main 의 머지 커밋 0개, main 의 총 커밋 6개 이상, main 과 topic 이 같은 커밋을 가리킬 것.
리베이스 뒤에는 main 이 topic 의 조상이므로 머지 커밋 없이 병합됩니다. --ff-only 를 쓰면 조건이 맞지 않을 때 거부해 주므로 안전합니다.
충돌을 양쪽 보존하며 해결
/root/gitx3/conflict 에 새 저장소를 만듭니다. main 에 shared.txt 를 두고, conflict-topic 브랜치에서 그 파일을 고쳐 from-topic 이라는 문자열이 들어가게 커밋합니다. 그다음 main 에서도 같은 자리를 고쳐 from-main 이 들어가게 커밋합니다. 이제 conflict-topic 을 main 위로 리베이스하면 충돌이 납니다. 두 변경을 모두 남기도록 해결하세요. 완료 조건: 리베이스가 끝나 있을 것, 인덱스에 미해결 경로가 없을 것, <<<<<<< 같은 충돌 표식이 파일에 남아 있지 않을 것, shared.txt 에 from-main 과 from-topic 이 둘 다 있을 것, 작업 트리가 깨끗할 것.
/root/gitx3/conflict 에서 같은 파일의 같은 자리를 양쪽이 고치게 만든 뒤 리베이스합니다. 해결은 한쪽을 고르는 것이 아니라 두 변경을 모두 남기는 것이고, 충돌 표식까지 지워야 끝난 것입니다.
리베이스 중단하고 원상 복구
/root/gitx3/abort 에 새 저장소를 만들고 risky 브랜치와 main 이 같은 파일을 서로 다르게 고치도록 커밋합니다. 리베이스를 시작하기 전에 risky 의 끝 해시를 /root/gitx3/abort-before.txt 에 저장합니다(해시 문자열만). 그다음 리베이스를 시작해 충돌을 만나고 중단합니다. 완료 조건: 리베이스가 진행 중이 아닐 것, risky 의 끝이 기록해 둔 해시와 정확히 같을 것, reflog 에 rebase 기록이 있을 것.
/root/gitx3/abort 에서 시작 전 브랜치 끝 해시를 파일에 적어 두고 충돌이 나는 리베이스를 시작한 뒤 중단합니다. 중단은 시작 전 상태로 정확히 되돌립니다.
커밋 3개를 1개로 합치기
/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 셋 다 존재.
/root/gitx3/squash 의 feature/three 를 main 기준으로 대화형 리베이스합니다. 합치더라도 변경 내용은 버려지지 않으므로 세 파일이 모두 남아야 합니다. 편집기를 띄우기 어렵다면 GIT_SEQUENCE_EDITOR 로 목록을 바꿀 수 있습니다.
커밋 하나 버리기
/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 모두 존재.
/root/gitx3/drop 의 feature/four 에서 디버그용 커밋만 없앱니다. 커밋을 drop 하면 그 커밋이 추가했던 파일도 함께 사라집니다. 나머지 세 커밋의 파일은 남아 있어야 합니다.
황금률 정리
/root/gitx3/rebase.md 를 씁니다. 반드시 포함할 것: --force-with-lease 설명과 --force 와의 차이, 언제 리베이스하면 안 되는지(공유·푸시·다른 사람 중 한 표현 포함), 왜 해시가 바뀌는지(해시·재작성 중 한 표현 포함), 그리고 merge_commits_on_main= 줄에 /root/gitx3/repo 의 main 머지 커밋 개수를 실제로 세어 적습니다.
/root/gitx3/rebase.md 에 --force-with-lease 와 --force 의 차이, 언제 리베이스하면 안 되는지, 왜 해시가 바뀌는지를 쓰고 merge_commits_on_main= 줄에 3단계 결과를 실제로 세어 적습니다.