본문 바로가기
빌드노트 검색

빌드노트

rebase 충돌에서 ours를 골랐는데 내 수정이 없어졌다

rebase 충돌의 ours는 upstream부터 재적용한 쪽이다. 같은 파일의 merge·rebase stage 1·2·3을 비교하고, 현재 커밋과 두 버전을 읽어 충돌 밖의 수정까지 보존하는 해결 순서를 확인한다.

topic 브랜치를 main 위로 rebase하다 충돌이 나서 ours를 골랐는데, 방금까지 있던 topic의 수정이 사라질 수 있다. rebase에서 ours는 upstream에서 시작해 지금까지 재적용한 쪽이다. theirs가 현재 재적용하는 topic 커밋 쪽이다. 따라서 ours를 “내가 작업하던 브랜치”라고 읽고 선택하면 의도와 다른 내용을 남긴다. 이 기준은 Git rebase 공식 문서의 --merge 설명 (새 탭에서 열림)에 명시돼 있다.

여기서 upstream은 커밋을 다시 올릴 기준 브랜치인 main이다. 충돌을 해결할 때는 ours/theirs라는 이름보다 두 파일의 내용과 지금 적용 중인 커밋을 먼저 읽는다. theirs도 최종 topic 브랜치 전체를 뜻하지 않는다. 여러 커밋을 재적용하는 중이라면 지금 멈춘 커밋을 기준으로 봐야 한다.

같은 두 브랜치로 merge와 rebase를 비교했다

Windows 11, Git 2.45.1.windows.1에서 새 독립 저장소를 만들어 합성 텍스트 파일 message.txt를 비교했다. 실제 프로젝트에서 수정이 사라진 사건을 재구성한 것이 아니라, 두 명령의 선택 기준을 확인한 실습이다.

공통 커밋의 첫 줄은 message=base다. 이 커밋에서 갈라진 두 브랜치는 같은 줄을 각각 바꿨다.

  • main: message=upstream
  • topic: message=topic

같은 파일의 마지막 줄에는 upstream-only=off도 넣었다. main에서만 이 값을 on으로 바꿨다. 첫 줄과 마지막 줄 사이에는 바뀌지 않는 문맥 여섯 줄을 두어, 마지막 줄은 충돌 없이 합쳐지게 했다.

이 상태를 별도 저장소마다 똑같이 만든 뒤, 모두 topic에서 시작했다. 한쪽은 git merge main, 다른 쪽은 git rebase --merge main을 실행했다. 둘 다 첫 줄에서 충돌했지만, 충돌 중인 인덱스의 내용은 다음처럼 달랐다.

  • stage 1: 양쪽 모두 첫 줄이 message=base였다.
  • stage 2, 즉 ours: merge에서는 message=topic, rebase에서는 message=upstream이었다.
  • stage 3, 즉 theirs: merge에서는 message=upstream, rebase에서는 message=topic이었다.

인덱스는 커밋할 내용을 준비하는 곳이다. 이처럼 양쪽에서 수정한 텍스트 파일이 충돌하면, 해결 전에는 비교 기준과 두 쪽의 파일을 stage 1·2·3으로 보관한다. 일반 merge의 stage 1은 공통 조상, stage 2는 합치는 대상 쪽, stage 3은 들어오는 쪽이다. Git revisions의 인덱스 stage 표기 (새 탭에서 열림)

topic에서 main을 merge할 때 합치는 대상은 topic이다. 반면 rebase는 main 위에 topic의 커밋을 하나씩 다시 적용한다. 그래서 stage 번호의 뜻이 바뀐 것이 아니라, 재적용을 받는 쪽이 달라진다. 첫 커밋에서는 ours가 main의 내용이고, 뒤의 커밋에서는 앞서 재적용한 결과까지 포함할 수 있다. 설치 버전에 가까운 Git 2.45.0 rebase 문서 (새 탭에서 열림)에서도 같은 기준을 확인했다.

해결하기 전에 stage와 현재 커밋을 읽는다

다음 명령은 Git이 설치된 PowerShell 7에서, rebase가 충돌로 멈춘 저장소의 루트에서 사용한다. message.txt는 자신의 충돌 파일 경로로 바꾼다. 먼저 첫 명령이 의도한 저장소를 가리키는지 확인하고 나머지를 실행한다. 아직 git add로 해결을 표시하기 전의 검사이며 파일 내용은 바꾸지 않는다.

git rev-parse --show-toplevel
git status --short
git rebase --show-current-patch
git show ':1:message.txt'
git show ':2:message.txt'
git show ':3:message.txt'

실습의 status 출력은 UU message.txt였다. 이어 현재 패치에는 다음 두 변경 줄이 나왔다. -는 빼는 줄, +는 넣는 줄이다. 여기서는 원래 커밋 정보와 diff 헤더, 바뀌지 않은 문맥을 생략했다.

-message=base
+message=topic

이는 지금 재적용할 수정이 base를 topic으로 바꾸는 것임을 보여 준다. 그 상태에서 stage 2의 첫 줄은 upstream, stage 3의 첫 줄은 topic이었다. git show는 지정한 파일 객체의 내용을 읽고, :2:경로와 :3:경로는 각각 인덱스의 해당 stage를 지정한다. git show 공식 문서 (새 탭에서 열림), 인덱스 파일 지정 문법 (새 탭에서 열림)

커밋 식별자와 제목만 짧게 확인하려면 git show --no-patch --oneline REBASE_HEAD도 쓸 수 있다. 실습에서는 cac9989 topic changes message가 나왔다. 이 해시는 실습 커밋의 값이며 자기 저장소에서는 달라진다. git rebase --show-current-patch는 충돌로 멈췄을 때 git show REBASE_HEAD와 같은 패치를 보여 준다. 현재 패치 확인 옵션 (새 탭에서 열림)

--show-current-patch가 실패한다면 rebase가 충돌로 멈춘 상태인지부터 확인한다. stage 조회가 실패할 때도 곧바로 “그쪽에 내 수정이 없다”고 판단하지 않는다. 이미 해결 내용을 추가했거나, 경로가 다르거나, 파일 추가·삭제 충돌이라 해당 stage가 없을 수 있다. status의 경로와 충돌 종류를 먼저 읽는다. 여기서 비교한 것은 양쪽에서 수정한 기존 텍스트 파일이다.

파일 전체를 고르면 충돌 밖의 수정도 바뀐다

실습의 rebase 충돌에서 git checkout --ours -- message.txt를 실행하자 첫 줄은 upstream으로 바뀌었다. 이 명령은 충돌 줄 하나가 아니라 stage 2의 파일 전체를 작업 파일에 꺼낸다. --theirs는 stage 3의 파일 전체를 꺼낸다. checkout의 --ours·--theirs 설명 (새 탭에서 열림)

ours를 고른 파일을 추가하고 rebase를 계속하자, 최종 파일에는 message=upstream이 남았다. 새 커밋은 생기지 않았고 topic의 끝도 main과 같은 3d65eb5가 됐다. 이번 topic 커밋의 유일한 수정이 해결 결과에 하나도 남지 않았기 때문이다. 원래 커밋 객체를 해시로 읽으면 topic 내용은 여전히 있었지만, rebase 후 topic의 이력에는 그 커밋이 없었다. rebase의 성공 종료만으로 의도한 수정이 보존됐다고 볼 수 없는 경우다.

그렇다고 theirs를 파일 전체의 기본 선택으로 삼을 수도 없다. 별도 저장소에서 같은 충돌에 git checkout --theirs -- message.txt를 실행하자, 첫 줄은 topic이 됐지만 마지막 줄도 upstream-only=off로 돌아갔다. 충돌 직후 작업 파일에는 이미 upstream-only=on이 정상 병합돼 있었다. 그런데 main의 변경이 없는 stage 3 파일 전체로 교체하면서 그 줄까지 되돌린 것이다.

두 변경을 모두 남길 의도라면 충돌 표시가 있는 작업 파일을 직접 편집한다. 실습에서는 첫 줄을 message=upstream+topic으로 정하고, 이미 합쳐진 마지막 줄 upstream-only=on과 여섯 문맥 줄을 유지했다. 이는 두 문구를 모두 남기기로 정한 예제의 해답이며, 실제 코드에서 어느 줄을 보존할지는 기능의 의도에 따라 판단해야 한다.

편집 후에는 다음 두 명령을 실행해, 현재 재적용된 쪽을 기준으로 어떤 변경을 올릴지 읽는다. git add는 파일을 수정하지 않고 현재 내용을 인덱스에 반영해 해결한 것으로 표시한다.

git add -- message.txt
git diff --cached -- message.txt

실제 diff의 변경 줄은 다음과 같았다. 마지막 줄은 그대로 on이므로 변경 목록에 나타나지 않았다.

-message=upstream
+message=upstream+topic

파일의 최종 내용과 diff가 의도에 맞는지 확인한다. 다른 충돌 파일도 모두 해결한 뒤에는 경로 제한 없는 git diff --cached로 전체 준비 내용을 읽고 git rebase --continue를 실행한다. 다음 커밋에서 또 충돌하면 그때의 현재 패치와 stage를 다시 확인한다. 편집기가 열리면 커밋 메시지를 확인하고 저장해 닫는다. 명령이 실패하면 그 오류를 해결하기 전까지 다음 단계로 넘어가지 않는다.

수동 해결 실습은 b25ff24 topic changes message라는 새 커밋으로 끝났다. 부모는 upstream 커밋 3d65eb5였고, 최종 파일에는 message=upstream+topic과 upstream-only=on이 함께 남았다. 내용은 git show 'HEAD:message.txt', 최근 커밋은 git log --oneline -3으로 확인했다. 원래 topic 커밋과 제목은 같아도 해시와 부모, 파일 내용이 달라졌다.

직접 실행한 범위는 한 커밋을 재적용하는 merge 방식 rebase, 기존 텍스트 파일의 수정 충돌, 파일 전체 선택과 수동 해결이다. 본문의 확인·해결 명령은 PowerShell 7.6.6에서도 실행했다. --apply 방식, 여러 커밋의 연속 충돌, 파일 이름 변경·추가·삭제, 바이너리 파일, GUI 충돌 버튼은 시험하지 않았다. 다른 도구의 “내 변경 수락”이라는 라벨에 이 결과를 그대로 대입하지 말고, Git의 현재 패치와 실제 stage 내용을 기준으로 선택한다.

← 전체 글 보기