LabHub
배우기 러닝패스 코스

Git in Practice

Rebasing and Resolving Conflicts

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

리베이스가 커밋을 옮기는 것이 아니라 재작성한다는 사실을 해시로 확인하고, 충돌 해결과 중단, 대화형 리베이스의 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. topicmain 위로 리베이스합니다. 리베이스 전에 topic 의 끝 해시를 기록해 두고, /root/gitx3/rebased.txtbefore=<리베이스 전 해시>after=<리베이스 후 해시> 두 줄을 적습니다. 두 값은 서로 달라야 하고, after 는 현재 topic 의 끝과 일치해야 하며, 리베이스가 진행 중인 상태로 남아 있으면 안 됩니다.
  3. main 에서 topic빨리 감기로 병합합니다. 결과: main 의 머지 커밋 0개, main 의 총 커밋 6개 이상, maintopic같은 커밋을 가리킬 것.
  4. /root/gitx3/conflict 에 새 저장소를 만듭니다. mainshared.txt 를 두고, conflict-topic 브랜치에서 그 파일을 고쳐 from-topic 이라는 문자열이 들어가게 커밋합니다. 그다음 main 에서도 같은 자리를 고쳐 from-main 이 들어가게 커밋합니다. 이제 conflict-topicmain 위로 리베이스하면 충돌이 납니다. 두 변경을 모두 남기도록 해결하세요. 완료 조건: 리베이스가 끝나 있을 것, 인덱스에 미해결 경로가 없을 것, <<<<<<< 같은 충돌 표식이 파일에 남아 있지 않을 것, shared.txtfrom-mainfrom-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/four3개, debug: temp log 커밋 없음, debug.txt 없음, d1/d2/d3.txt 모두 존재.
  8. /root/gitx3/rebase.md 를 씁니다. 반드시 포함할 것: --force-with-lease 설명과 --force 와의 차이, 언제 리베이스하면 안 되는지(공유·푸시·다른 사람 중 한 표현 포함), 왜 해시가 바뀌는지(해시·재작성 중 한 표현 포함), 그리고 merge_commits_on_main= 줄에 /root/gitx3/repomain 머지 커밋 개수를 실제로 세어 적습니다.

참고

갈라진 두 브랜치 만들기

/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 위로 재작성

topicmain 위로 리베이스합니다. 리베이스 전에 topic 의 끝 해시를 기록해 두고, /root/gitx3/rebased.txtbefore=<리베이스 전 해시>after=<리베이스 후 해시> 두 줄을 적습니다. 두 값은 서로 달라야 하고, after 는 현재 topic 의 끝과 일치해야 하며, 리베이스가 진행 중인 상태로 남아 있으면 안 됩니다.

리베이스 전에 topic 의 끝 해시를 먼저 기록해 두어야 비교할 수 있습니다. 리베이스가 끝나면 공통 조상이 main 의 끝과 같아지고, 부모가 달라졌으므로 해시도 달라집니다.

빨리 감기로 일직선 만들기

main 에서 topic빨리 감기로 병합합니다. 결과: main 의 머지 커밋 0개, main 의 총 커밋 6개 이상, maintopic같은 커밋을 가리킬 것.

리베이스 뒤에는 main 이 topic 의 조상이므로 머지 커밋 없이 병합됩니다. --ff-only 를 쓰면 조건이 맞지 않을 때 거부해 주므로 안전합니다.

충돌을 양쪽 보존하며 해결

/root/gitx3/conflict 에 새 저장소를 만듭니다. mainshared.txt 를 두고, conflict-topic 브랜치에서 그 파일을 고쳐 from-topic 이라는 문자열이 들어가게 커밋합니다. 그다음 main 에서도 같은 자리를 고쳐 from-main 이 들어가게 커밋합니다. 이제 conflict-topicmain 위로 리베이스하면 충돌이 납니다. 두 변경을 모두 남기도록 해결하세요. 완료 조건: 리베이스가 끝나 있을 것, 인덱스에 미해결 경로가 없을 것, <<<<<<< 같은 충돌 표식이 파일에 남아 있지 않을 것, shared.txtfrom-mainfrom-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/four3개, 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/repomain 머지 커밋 개수를 실제로 세어 적습니다.

/root/gitx3/rebase.md 에 --force-with-lease 와 --force 의 차이, 언제 리베이스하면 안 되는지, 왜 해시가 바뀌는지를 쓰고 merge_commits_on_main= 줄에 3단계 결과를 실제로 세어 적습니다.