깃 커밋 되돌리기 상황별 정리, 머지를 revert했다면 다시 머지하기 전에 볼 것
깃 커밋 되돌리기는 푸시 전후로 갈리지만 머지 커밋은 따로 봐야 합니다. -m 1, 재머지 함정, force-with-lease까지 정리했습니다.
커밋을 잘못 올렸고, 지금 당장 되돌려야 하는 상황일 겁니다. reset 과 revert 의 차이는 대부분 알고 계실 테니 그 부분은 표 하나로 끝내겠습니다.
이 글이 공을 들이는 곳은 그다음입니다. 머지 커밋을 revert하면 나중에 같은 브랜치를 다시 머지할 때 변경 일부가 조용히 빠집니다. 오류 메시지도 없어서 배포한 뒤에야 알게 되는 경우가 많아요. 명령어는 깃 2.54 공식 문서 기준입니다.
상황별로 고르는 명령
푸시하지 않은 커밋은 reset 으로 지우고, 이미 푸시한 커밋은 revert 로 되돌리는 새 커밋을 만듭니다. 판단 기준은 “다른 사람이 이 커밋을 받아 갔을 수 있는가” 하나예요.
| 상황 | 명령 |
|---|---|
| 마지막 커밋 취소, 수정은 스테이지에 남김 | git reset --soft HEAD~1 |
| 마지막 커밋 취소, 수정은 파일에만 남김 | git reset HEAD~1 |
| 마지막 커밋과 수정 전부 버림 | git reset --hard HEAD~1 |
| 커밋 메시지만 고침 (푸시 전) | git commit --amend |
| 파일 하나만 특정 커밋 시점으로 | git restore --source <해시> <파일> |
| 푸시한 일반 커밋 되돌림 | git revert <해시> |
| 푸시한 커밋 여러 개를 한 커밋으로 되돌림 | git revert --no-commit <시작>^..<끝> 후 git commit |
| 푸시한 머지 커밋 되돌림 | git revert -m 1 <머지 해시> |
마지막 줄이 이 글의 본론입니다. revert 도중 충돌이 나면 해결한 뒤 git revert --continue, 그만두려면 git revert --abort 로 시작 전 상태로 돌아갑니다.
머지 커밋은 -m 1 을 붙여야 되돌아갑니다
머지 커밋에 그냥 git revert 를 하면 is a merge but no -m option was given 오류가 나고 아무것도 바뀌지 않습니다. 머지 커밋은 부모가 둘이라 어느 쪽을 기준으로 되돌릴지 깃이 정할 수 없기 때문이에요.
-m 뒤의 숫자는 남길 쪽 부모의 번호입니다. 확인은 git show <머지 해시> 로 합니다. 출력 맨 위에 Merge: a1b2c3d e4f5a6b 처럼 해시 두 개가 나오는데, 앞쪽이 1번이고 뒤쪽이 2번이에요.
- main 에서
git merge feature를 했다면 1번이 main, 2번이 feature 입니다 - GitHub 에서 PR 을 일반 머지(Create a merge commit)했어도 1번이 기준 브랜치입니다
그래서 대부분은 -m 1 이 맞습니다. 숫자를 반대로 넣으면 feature 쪽을 남기고 main 쪽 변경을 되돌리는 엉뚱한 결과가 나오니, 자신이 없으면 git revert --no-commit -m 1 <해시> 로 먼저 적용만 해 보고 git diff --staged 로 무엇이 지워지는지 확인한 뒤 커밋하세요.
PR 을 Squash and merge 로 합쳤다면 머지 커밋이 아니라 일반 커밋 하나가 생깁니다. 이 경우엔 -m 없이 그냥 revert 하면 됩니다. Rebase and merge 였다면 커밋이 여러 개 그대로 올라가 있으니 위 표의 범위 revert 를 씁니다.
revert한 머지를 다시 머지하면 변경이 빠집니다
핵심머지를 revert한 뒤 feature 브랜치에서 버그를 고치고 다시 머지하면, 고친 커밋만 들어오고 처음 머지 때 있던 변경은 들어오지 않습니다. 충돌도 경고도 없어서 그대로 배포되기 쉬워요.
이유는 revert 가 내용만 되돌리고 기록은 되돌리지 않기 때문입니다. 깃 공식 문서는 머지 커밋을 revert하는 것을 “그 머지가 가져온 트리 변경을 앞으로도 원하지 않는다는 선언”으로 설명합니다. 깃 입장에서 처음 머지는 여전히 이뤄진 상태고, feature 의 예전 커밋들은 이미 main 의 조상이에요. 그래서 다음 머지는 그 뒤에 생긴 커밋만 가져옵니다.
해결은 깃 문서의 how-to(Revert a faulty merge)가 안내하는 그대로입니다.
- revert 커밋 찾기revert 커밋의 해시를 찾습니다.
git log --oneline --grep="Revert"가 빠릅니다 - revert 를 revert
git revert <revert 커밋 해시>로 revert 를 revert 합니다. 처음 머지의 내용이 다시 살아나요 - 그다음 머지그다음 feature 를 머지합니다. 이제 고친 커밋까지 모두 반영됩니다
순서를 바꿔 머지부터 하면 2번에서 충돌이 날 수 있으니 이 순서를 지키는 편이 덜 고생스럽습니다. 팀에서 “머지 revert 했음”을 PR 이나 메신저에 남겨 두세요. 나중에 다른 사람이 같은 브랜치를 머지하면서 같은 함정에 빠지는 일이 꽤 흔합니다.
GitHub 의 Revert 버튼은 이렇게 동작합니다
GitHub 에서 머지된 PR 페이지 아래쪽에 Revert 버튼이 있습니다. 누르면 원래 머지를 되돌리는 새 PR 이 만들어지고, 그걸 머지해야 실제로 되돌아갑니다. 버튼을 누르는 것만으로 main 이 바뀌지는 않아요.
- 저장소 쓰기 권한이 있어야 버튼이 보입니다
- 되돌리다 충돌이 나거나, 원래 PR 이 GitHub 에서 머지된 게 아니면 버튼 대신 로컬에서
git revert를 해야 합니다 - 버튼으로 되돌렸어도 위의 재머지 함정은 그대로입니다. 다시 올릴 때는 그 Revert PR 을 한 번 더 Revert 하는 방식으로 처리하세요
푸시한 걸 reset 했다면 force-with-lease 로 올립니다
개인 브랜치처럼 나 말고 받아 간 사람이 없다면 푸시한 커밋도 reset 하고 강제 푸시할 수 있습니다. 이때 --force 대신 --force-with-lease 를 써야 합니다. 원격 브랜치가 내가 마지막으로 본 상태 그대로일 때만 덮어써서, 그사이 누가 올린 커밋을 지우지 않거든요.
다만 공식 문서도 짚는 약점이 있습니다. 편집기나 Git 클라이언트가 백그라운드에서 git fetch 를 돌리면 “내가 마지막으로 본 상태”가 몰래 최신으로 바뀌어 보호가 사라져요. 그래서 둘을 같이 쓰는 걸 권합니다.
git push --force-with-lease --force-if-includes origin feature
--force-if-includes 는 원격의 최신 커밋을 내가 로컬에서 실제로 반영한 적이 있는지까지 확인합니다. 매번 치기 번거롭다면 git config --global push.useForceIfIncludes true 로 기본값을 켜 두면 --force-with-lease 만 써도 함께 적용돼요.
main 같은 공용 브랜치는 이 방법을 쓰지 않는 게 맞습니다. 애초에 GitHub 는 보호 브랜치에 대한 강제 푸시를 기본으로 막아 둡니다. protected branch hook declined 류의 거부가 뜬다면 우회할 게 아니라 revert 로 가라는 신호입니다.
reset 전에 알아 둘 세 가지
주의--hard 는 추적하지 않는 파일도 덮어쓸 수 있습니다. 공식 문서 표현 그대로 untracked 파일을 덮어쓸 수 있고, 되돌릴 커밋에 없는 추적 파일은 삭제합니다. 커밋하지 않은 수정이 하나라도 있다면 git stash -u 로 먼저 치워 두세요.
로컬 수정을 지키고 싶다면 --keep 입니다. git reset --keep HEAD~1 은 --hard 처럼 커밋을 지우지만, 되돌리면서 바뀌어야 할 파일에 내 수정이 있으면 실행 자체를 멈춥니다. 실수로 작업 중인 내용을 날리는 일이 없어요.
방금 한 reset·merge·pull 은 ORIG_HEAD 로 바로 취소됩니다. 이 명령들은 실행 직전 브랜치 끝을 ORIG_HEAD 에 적어 두거든요. git reset --hard ORIG_HEAD 한 줄이면 직전 상태로 돌아갑니다. 그보다 이전 상태가 필요하면 git reflog 에서 HEAD@{2} 같은 위치를 찾아 쓰면 되고, reflog 는 기본 90일(어느 브랜치에서도 닿지 않는 항목은 30일) 동안 남습니다. 단 reflog 는 내 컴퓨터에만 있어서, 다른 사람 PC 에서 날린 커밋은 그 사람의 reflog 로 찾아야 합니다.
커밋을 엉뚱한 브랜치에 했을 때
main 에 바로 커밋해 버렸는데 아직 푸시 전이라면, 지금 위치에 브랜치를 하나 만들고 main 만 뒤로 돌리면 됩니다.
git branch feature-fix
git reset --keep HEAD~1
git switch feature-fix
커밋은 feature-fix 에 그대로 남고 main 은 한 칸 뒤로 갑니다. 이미 푸시했다면 main 에서는 revert 하고, 해당 커밋은 git cherry-pick <해시> 로 원래 브랜치에 옮기세요.
자주 묻는 질문
git revert 했는데 ‘is a merge but no -m option was given’ 오류가 납니다. 되돌리려는 커밋이 머지 커밋이라서 그렇습니다. 어느 쪽 부모를 기준으로 되돌릴지 정해 줘야 해요. 보통 main 에서 머지했다면 git revert -m 1 <해시> 입니다.
머지를 revert한 뒤 브랜치를 고쳐서 다시 머지했는데 예전 변경이 안 들어옵니다. 정상 동작입니다. 깃은 그 머지가 이미 이뤄졌다고 기억하고 있어서, 처음 머지 때 들어온 커밋은 다시 가져오지 않습니다. 다시 머지하기 전에 revert 커밋을 한 번 더 revert해야 합니다.
reset –hard 로 날린 커밋을 살릴 수 있나요? 커밋까지 했던 내용이면 대개 살릴 수 있습니다. git reflog 에서 날리기 전 위치를 찾아 그 해시로 다시 reset 하면 됩니다. 커밋하지 않은 수정은 reflog 에 남지 않아 돌아오지 않습니다.
force-with-lease 면 안전한가요? –force 보다는 낫지만 완전하지 않습니다. 편집기나 도구가 백그라운드에서 fetch 를 돌리면 보호가 무력화됩니다. –force-if-includes 를 함께 쓰세요.