取り消しは二つの問いで決まる
한국어 원문으로 표시합니다.
한 줄 요약
되돌릴 방법은 두 질문으로 결정된다. 되돌리려는 것이 어디에 있는가(워킹 트리 / 인덱스 / 커밋), 그리고 그 커밋이 이미 다른 사람에게 갔는가.
왜 이게 필요했나
Git 되돌리기 명령이 어렵게 느껴지는 이유는 명령이 많아서가 아니라, 명령을 고르는 기준을 배운 적이 없어서다. 첫 번째 질문이 명령의 종류를 정하고, 두 번째 질문이 이력을 고쳐도 되는지를 정한다.
여기에 큰 안심 하나를 얹어 두자. 커밋된 적이 있는 것은 거의 항상 복구되고, 커밋된 적이 없는 것은 거의 복구되지 않는다. 그래서 위험한 순간은 "커밋을 지울 때"가 아니라 "커밋하지 않은 변경을 덮어쓸 때"다.
어떻게 동작하나
reset 의 세 모드는 되돌리는 범위가 다르다.
| 모드 | 되돌리는 범위 | 잃는 것 | 쓰는 때 |
|---|---|---|---|
--soft |
브랜치 참조만 이동 | 없음 | 커밋 여러 개를 하나로 다시 묶을 때 |
--mixed(기본) |
인덱스까지 | 스테이징 상태 | 스테이징을 다시 고르고 싶을 때 |
--hard |
인덱스와 워킹 트리까지 | 커밋되지 않은 모든 변경 | 로컬 실험을 통째로 버릴 때 |
--keep |
인덱스까지, 단 겹치는 로컬 변경이 있으면 중단 | 없음 | 덮어쓰기 전에 거부받고 싶을 때 |
위험도는 명령 이름이 아니라 "이 명령이 커밋되지 않은 변경을 덮어쓰는가"로 매긴다.
커밋이 이미 다른 사람의 저장소에 있다면 그 커밋을 없애는 모든 방법은 다른 사람의 이력을 깨뜨린다. 이력을 고치는 대신 이력을 덧붙이는 것이 답이다. 그래서 revert 를 쓴다. 강제 푸시로 지우면 이미 받아 간 사람들의 로컬은 그대로이고, 그들이 다음에 푸시하면 지웠던 커밋이 되살아난다.
머지 revert 에는 함정이 하나 있다. 머지를 revert 한 뒤 브랜치를 고쳐 다시 머지하면 되돌렸던 변경이 돌아오지 않는다. Git 은 머지를 이력의 도달 가능성으로만 판단하기 때문이다. 정답은 revert 를 revert 하는 것이다.
현장에서 만나는 모습
git reset --hard 로 커밋 두 개를 날린 다음 얼굴이 하얘지는 장면은 흔하다. 대개는 괜찮다. git reflog 를 열어 HEAD@{1} 로 돌아가면 된다. 다만 두 가지를 알아 두어야 한다. 브랜치를 삭제하면 그 브랜치의 참조 로그도 함께 삭제된다(HEAD 의 reflog 에는 남는다). 그리고 기본 만료는 도달 가능 90일, 도달 불가 30일이다.
안전망을 즉시 없애는 명령도 있다. git reflog expire --expire=now --all 과 git gc --prune=now 다. 이 둘을 "정리"라고 부르며 습관적으로 돌리는 팀은 사고가 날 때 복구 수단이 없다.
정말 마지막 수단은 git fsck --full --unreachable --no-reflogs 로 떠도는 객체를 찾고 git cat-file -p <blob> > 파일 로 꺼내는 것이다. 스테이징만 해 둔 내용도 이미 객체이므로 이 방법으로 살아난다.
상황별로 무엇을 쓰나
앞의 두 질문을 실제 상황에 붙이면 이런 표가 된다. 급할 때 이 표만 있으면 명령을 고민하지 않아도 된다.
| 상황 | 쓸 것 | 주의 |
|---|---|---|
| 방금 커밋 메시지를 잘못 썼다 | commit --amend |
이미 푸시했으면 하지 않는다 |
| 방금 커밋에 파일 하나를 빠뜨렸다 | 추가 후 commit --amend --no-edit |
위와 같다 |
| 커밋 세 개를 하나로 묶고 싶다 | reset --soft HEAD~3 후 다시 커밋 |
변경은 그대로 남는다 |
| 스테이징만 되돌리고 싶다 | restore --staged <파일> |
파일 내용은 안 건드린다 |
| 파일 하나를 마지막 커밋 상태로 | restore <파일> |
커밋 안 한 변경이 사라진다 |
| 이미 푸시한 커밋을 취소 | revert <커밋> |
새 커밋이 생기고 이력이 남는다 |
| 로컬 실험을 통째로 버린다 | reset --hard |
되돌릴 수 없는 유일한 자리 |
| 잘못 지웠다 | reflog 로 찾아 reset --hard <해시> |
30~90일 안이면 대개 살아난다 |
표에서 위험한 줄은 딱 둘입니다. restore <파일> 과 reset --hard 입니다. 둘 다 커밋된 적 없는 변경을 덮어쓰기 때문이고, 그 변경은 어디에도 기록되지 않아 reflog 로도 살릴 수 없습니다. 그래서 습관 하나를 권합니다. 버릴지 말지 애매하면 먼저 커밋하거나 git stash 로 밀어 둡니다. 둘 다 객체를 만들기 때문에, 그 순간부터는 되찾을 방법이 생깁니다.
stash 에 대해 한 가지만 덧붙인다. 기본 동작은 추적되지 않는 새 파일을 밀어 두지 않으므로, 새로 만든 파일이 그대로 남아 다음 작업에 섞여 든다. 그것까지 함께 치우려면 -u 를 붙여야 하고, 이것을 모르면 "stash 했는데 왜 작업 트리가 안 깨끗하지" 로 한참 헤매게 된다.
다음 실습에서 할 것
같은 저장소를 여러 벌 만들어야 하므로, 먼저 저장소를 찍어 내는 스크립트를 만듭니다. 그다음 amend, reset 세 모드, revert, reflog 복구를 각각 별도의 저장소에서 실행해 결과 차이를 눈으로 비교하고, 마지막에 상황별 치트시트를 정리합니다.