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

빌드노트

merge 커밋을 revert할 때 -m 1은 무엇을 남기는가

merge revert의 -m은 비교 기준 부모를 고른다. 실제 부모 순서와 부모별 diff를 읽고, -m 1과 -m 2가 남기는 파일을 비교한다. revert 뒤 같은 브랜치를 다시 병합해도 취소한 변경이 돌아오지 않는 이유도 확인했다.

merge 커밋에 부모 기준 없이 git revert를 실행하면 no -m option was given으로 멈춘다. 여기서 -m 1을 “1번 부모의 변경을 없앤다”로 읽으면 반대 결과를 만들 수 있다. 1은 변경을 비교할 기준 부모 번호다. Git은 그 부모에서 merge 결과까지 생긴 변경을 반대로 적용해 새 커밋을 만든다. 따라서 숫자를 고르기 전에 실제 부모 순서와 부모별 diff를 확인해야 한다. git revert 공식 문서 (새 탭에서 열림)

부모 번호를 브랜치 이름으로 추측하지 않는다

아래 명령은 Git과 커밋용 사용자 설정이 준비된 PowerShell 7에서 쓴다. 대상 merge가 포함된 브랜치의 작업 폴더에서 시작한다. 다른 merge·rebase·revert가 진행 중이지 않아야 한다. 첫 명령의 저장소 경로를 확인하고, status --short에 변경 파일이 나오면 먼저 정리한 뒤 진행한다. 어떤 명령이든 실패하면 뒤의 명령을 이어 실행하지 않는다.

먼저 대상 커밋과 부모들의 식별자·제목을 읽는다. $merge에는 자신의 merge 커밋 해시를 넣는다. 아래 값은 별도 실습에서 만든 해시이므로 자신의 저장소에 그대로 넣으면 조회되지 않을 수 있다.

git rev-parse --show-toplevel
git status --short
$merge = 'a66cd6413e67ea485961159d8f1cdddd86a778dd'
git show --no-patch --format='%H%n%P%n%s' $merge
git show --no-patch --format='%h %s' "${merge}^1" "${merge}^2"

이하 출력은 Windows 11, Git 2.45.1.windows.1에서 새 독립 저장소로 확인한 결과다. 공통 커밋에서 갈라진 lab-main은 shipping.txt를 shipping=base에서 shipping=fixed로 바꿨다. lab-topic은 다른 파일인 banner.txt를 banner=base에서 banner=feature로 바꿨다. 그다음 lab-main에서 lab-topic을 병합했다. 두 변경은 충돌 없이 합쳐졌다.

두 show 명령의 실제 출력은 다음과 같았다.

a66cd6413e67ea485961159d8f1cdddd86a778dd
cd3b14d446c478149b81a428174a603ece6c31b7 ce4fc0d125730fe5d75159d09d08cde208d56e48
merge topic banner
cd3b14d main fixes shipping
ce4fc0d topic changes banner

첫 show는 merge 해시, 공백으로 구분한 부모 해시, 커밋 제목을 세 줄로 출력한다. %P가 부모 해시이고 %n은 줄바꿈이다. 뒤의 show는 각 부모의 짧은 해시와 제목을 보여 준다. git show의 출력 형식 (새 탭에서 열림)

이 merge의 부모 1은 shipping을 수정한 cd3b14d, 부모 2는 banner를 수정한 ce4fc0d다. ^1과 ^2는 각각 해당 커밋에 기록된 첫째·둘째 부모를 지정한다. 현재 어느 브랜치를 체크아웃했는지에 따라 번호가 바뀌지는 않는다. 부모 지정 문법 (새 탭에서 열림)

^2 조회가 실패하면 숫자부터 바꾸지 않는다. 부모가 하나인 일반 커밋을 골랐는지, 해시가 맞는지, 필요한 커밋 객체가 로컬에 있는지 확인한다. 부모가 셋 이상인 merge도 이 글의 두 부모 예제 범위 밖이다.

두 diff 중 취소할 변경을 고른다

다음 두 명령은 각 부모를 출발점으로 삼아 merge 결과까지 무엇이 달라졌는지 보여 준다. 커밋 두 개를 지정한 git diff는 두 커밋의 파일 상태를 비교한다. git diff 공식 문서 (새 탭에서 열림)

git diff "${merge}^1" $merge --
git diff "${merge}^2" $merge --

실제 출력에서 파일 헤더와 변경 줄만 남기면 다음과 같다. 앞은 부모 1과의 비교, 뒤는 부모 2와의 비교다. - 줄이 부모 쪽 내용이고 + 줄이 merge 결과다.

--- a/banner.txt
+++ b/banner.txt
@@ -1 +1 @@
-banner=base
+banner=feature
--- a/shipping.txt
+++ b/shipping.txt
@@ -1 +1 @@
-shipping=base
+shipping=fixed

부모 1에는 shipping 수정이 이미 있으므로, merge까지 새로 들어온 차이는 banner 변경이다. 부모 2에는 banner 변경이 이미 있으므로, 그 부모를 기준으로 보면 shipping 수정이 새 차이다. revert는 고른 비교의 변경 방향을 뒤집는다.

같은 merge 직후에서 출발하는 별도 브랜치 두 개에 각각 revert를 실행했다. 두 명령을 한 브랜치에서 연달아 실행한 결과가 아니다.

  • git revert --no-edit -m 1 $merge: banner=base, shipping=fixed가 남았다. banner 변경을 취소했다.
  • git revert --no-edit -m 2 $merge: banner=feature, shipping=base가 남았다. shipping 수정을 취소했다.

이 실험에서 -m 1 뒤의 파일 상태는 부모 1과 같았고, -m 2 뒤의 파일 상태는 부모 2와 같았다. 하지만 이것을 “선택한 부모의 파일을 통째로 덮어쓴다”로 이해하면 안 된다. 별도 브랜치에서 merge 이후 later.txt를 추가하고 -m 1로 되돌렸을 때는 그 새 파일이 유지됐다. revert는 현재 커밋 위에서 선택한 차이를 취소한다.

부모별 비교가 필요한 이유는 화면의 diff가 비어 보일 수도 있기 때문이다. 이 merge를 git show --format= --cc $merge --로 조회했을 때 패치 출력은 없었다. combined diff는 모든 부모와 비교해 달라진 파일을 대상으로 한다. 여기서는 각 파일이 한쪽 부모의 내용을 그대로 받아 왔다. 출력이 없다고 되돌릴 변경도 없는 것은 아니다. git show의 combined diff 설명 (새 탭에서 열림)

기준을 정한 뒤 새 브랜치에서 실행한다

이 예제에서 목적이 “shipping 수정은 유지하고, 병합으로 들어온 banner 변경을 취소한다”라면 부모 1을 기준으로 고른다. 아래 명령은 현재 HEAD에서 확인용 브랜치를 만든 뒤 revert하고, 새 커밋이 바꾼 파일과 실제 변경 줄을 읽는다. $merge는 앞에서 확인한 값을 그대로 쓴다. 시작점은 되돌리기를 적용하려는 현재 커밋이며, merge 직후로 강제로 이동하지 않는다. 브랜치 이름이 이미 있으면 새 이름을 정하고 다시 시작한다.

git switch -c check-revert-mainline
git revert --no-edit -m 1 $merge
git show --stat --oneline HEAD
git diff 'HEAD^' HEAD --

PowerShell 7.6.6에서 위 명령을 merge 직후부터 실행하자 Revert "merge topic banner"라는 새 커밋이 생겼다. 마지막 diff에는 -banner=feature, +banner=base가 나왔고 shipping 수정은 유지됐다. 원래 merge 커밋은 새 revert 커밋의 부모로 남았다. 목적이 반대라면 실행 전에 부모 2와의 diff를 읽고 -m 2를 고른다.

이후 커밋이 같은 줄을 수정했다면 충돌할 수 있다. banner를 banner=later로 바꾼 별도 실험에서는 revert가 실패하고 git status --short에 UU banner.txt가 나왔다. 이는 부모 번호가 틀렸다는 증거가 아니다. 그 상태에서 취소하려면 git revert --abort를 쓴다. 실험에서는 시작 전 커밋과 banner=later가 복구됐고 작업 폴더의 변경 표시도 사라졌다. 충돌이 나면 뒤의 성공 확인 명령을 그대로 이어 실행하지 않는다. revert 중단 옵션 (새 탭에서 열림)

revert는 병합했던 조상 관계를 지우지 않는다

부모 1을 기준으로 revert한 뒤, 새 커밋이 없는 원래 lab-topic을 다시 병합했다. 결과는 Already up to date.였고 HEAD도 바뀌지 않았다. banner는 계속 base였다.

이유는 파일 내용과 조상 관계가 다르기 때문이다. revert 커밋의 부모는 여전히 원래 merge이고, 그 merge의 부모 중 하나가 기존 topic 커밋이다. 기존 topic은 계속 현재 커밋의 조상이다. git merge-base --is-ancestor로 그 관계를 검사했을 때도 종료 코드가 0이었다. 이 옵션에서 0은 첫 커밋이 둘째 커밋의 조상이라는 뜻이다. 조상 관계 검사 (새 탭에서 열림)

이어 원래 topic 위에 followup.txt를 추가한 새 커밋을 만들고, revert한 쪽에서 별도로 병합했다. 새 파일은 들어왔지만 기존 banner 변경은 돌아오지 않았다. 공식 문서도 merge를 revert한 뒤의 병합은 이전 merge의 조상이 아닌 커밋에서 생긴 변경을 가져온다고 설명한다. merge를 되돌려도 “그 브랜치를 병합한 적 없는 이력”이 되지는 않는다. merge revert와 이후 병합 (새 탭에서 열림)

직접 확인한 범위는 두 부모의 충돌 없는 텍스트 병합, 두 mainline 선택, 이후의 독립 파일 유지, 같은 줄 수정에 따른 충돌과 중단, 기존 topic 및 새 독립 파일을 추가한 topic의 재병합이다. 충돌을 수동 해결한 merge의 revert, revert 충돌 해결 후 계속 진행, 부모 셋 이상의 병합, 이름 변경·바이너리 파일, topic 이력을 새로 쓴 뒤의 재병합은 시험하지 않았다. 이 글의 결과는 부모 번호를 고르는 기준이며, 취소한 기능을 다시 넣는 모든 이력에 대한 복구 절차는 아니다.

← 목록으로 돌아가기