Git reset과 revert 차이
잘못 만든 Git 커밋을 되돌릴 때 reset과 revert를 자주 사용한다.
두 명령은 모두 이전 변경을 취소하는 데 사용할 수 있지만 커밋 이력을 다루는 방식은 반대에 가깝다.
reset은 현재 브랜치가 가리키는 커밋을 옮겨 기존 이력을 다시 작성한다.revert는 취소할 변경의 반대 내용을 새 커밋으로 기록해 기존 이력을 보존한다.
이미 다른 사람과 공유한 커밋인지가 가장 중요한 선택 기준이다.
커밋 그래프의 차이
현재 main 브랜치가 커밋 C를 가리키는 상태를 보자.
A---B---C main, HEAD
reset 뒤
A---B main, HEAD
\
C 현재 브랜치에서 도달할 수 없음
revert 뒤
A---B---C---R main, HEADreset 뒤에는 main과 HEAD가 B를 가리킨다.C는 현재 브랜치 이력에서 빠지지만 ORIG_HEAD나 reflog 기록을 통해 잠시 다시 찾을 수 있다.
revert는 C를 없애지 않고 반대 변경을 담은 새 커밋 R을 만든다.
로그에는 문제가 생긴 변경과 그 변경을 취소한 기록이 모두 남는다.
그래서 이미 원격에 올라가 다른 사람이 기준으로 삼을 수 있는 커밋에는 revert가 안전한 기본 선택이다.
reset mode의 차이
커밋을 지정하는 형태의 git reset은 현재 브랜치의 끝인 HEAD를 대상 커밋으로 옮긴다.
선택한 mode에 따라 staging area라고 부르는 index와 working tree도 함께 바꾼다.
| mode | HEAD | index | working tree | 마지막 커밋의 변경 |
|---|---|---|---|---|
--soft | 이동 | 유지 | 유지 | staged 상태로 남음 |
--mixed | 이동 | 대상 커밋에 맞춤 | 유지 | unstaged 상태로 남음 |
--hard | 이동 | 대상 커밋에 맞춤 | 대상 커밋에 맞춤 | 파일 변경까지 버림 |
--soft- HEAD
- 이동
- index
- 유지
- working tree
- 유지
- 마지막 커밋의 변경
- staged 상태로 남음
--mixed- HEAD
- 이동
- index
- 대상 커밋에 맞춤
- working tree
- 유지
- 마지막 커밋의 변경
- unstaged 상태로 남음
--hard- HEAD
- 이동
- index
- 대상 커밋에 맞춤
- working tree
- 대상 커밋에 맞춤
- 마지막 커밋의 변경
- 파일 변경까지 버림
mode를 생략하면 기본값은 --mixed다.
같은 HEAD~1을 대상으로 해도 어떤 mode를 선택하는지에 따라 보존되는 작업이 달라진다.
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1--soft는 브랜치 포인터만 옮기므로 마지막 커밋의 변경이 staged 상태로 남는다.
커밋 메시지나 커밋 경계를 다시 구성할 때 유용하다.
--mixed는 index도 대상 커밋에 맞추지만 working tree는 유지한다.
마지막 커밋의 변경을 unstaged 상태로 돌려 다시 나누고 싶을 때 적합하다.
--hard는 index와 working tree까지 대상 커밋에 맞춘다.
파일 변경도 버리므로 세 mode 가운데 데이터 손실 위험이 가장 크다.
파일 경로를 지정하는 reset은 동작이 다르다.git reset -- src/config.js 형태는 브랜치의 HEAD를 옮기지 않고 해당 경로의 index만 갱신한다.
최근 Git에서는 staged 파일을 내리는 의도를 더 분명하게 표현하는 git restore --staged src/config.js도 사용할 수 있다.
reset 전 확인 사항
대상 커밋 이후의 tracked file 변경은 사라진다.
대상 tree를 쓰는 데 방해되는 untracked file이나 directory도 삭제될 수 있다.
커밋한 작업은 reflog에서 다시 찾을 가능성이 있지만 한 번도 커밋하지 않은 working tree 변경은 Git으로 복구하기 어렵다.--hard를 빠른 정리 명령처럼 습관적으로 사용하면 안 되는 이유다.
git status
git diff
git diff --staged
git log --oneline --decorate -5
git branch backup-before-reset마지막 명령으로 만든 브랜치는 reset 전 커밋을 계속 가리킨다.
잘못 판단했을 때 돌아갈 명시적인 기준점이 필요하다면 먼저 이런 임시 브랜치를 남긴다.
이미 staged 변경이 있다면 soft reset 뒤에도 섞여 남으므로 git diff --staged 결과를 확인한다.
revert의 동작과 충돌 처리
git revert <commit>은 지정한 커밋이 도입한 patch의 반대 변경을 적용하고 그 결과를 새 커밋으로 기록한다.
git log --oneline
git revert a1b2c3d
git status
git add <resolved-file>
git revert --continuerevert 대상 커밋은 이력에 그대로 남는다.
새 커밋은 해당 변경을 취소했다는 사실을 보여주고 일반 push로 공유할 수 있다.
revert는 저장소 전체를 과거 시점으로 단순 복사하지 않는다.
대상 커밋 이후에 같은 줄이 바뀌었다면 현재 코드에 반대 patch를 적용하는 과정에서 충돌할 수 있다.
공식 문서는 HEAD 대비 수정이 없는 clean working tree에서 시작하는 흐름을 전제로 한다.
관련 없는 변경을 먼저 커밋하거나 별도로 보관한 뒤 실행하는 편이 안전하다.
충돌이 생기면 파일을 수정하고 stage한 뒤 --continue로 진행한다.
작업 전체를 취소하려면 --abort를 사용한다.
충돌을 해결할 때는 단순히 conflict marker를 지우는 데서 끝내지 않고 취소하려는 기능과 이후 변경이 모두 의도대로 동작하는지 확인해야 한다.
merge commit의 revert
merge commit은 부모가 둘 이상이므로 일반 커밋처럼 바로 revert할 기준이 하나로 정해지지 않는다.
이때는 -m으로 어느 부모를 mainline으로 볼지 지정해야 한다.
git revert -m 1 <merge-commit>숫자 1은 무조건 main 브랜치를 뜻하는 값이 아니라 merge commit의 첫 번째 부모를 뜻한다.
부모 번호를 잘못 선택하면 의도와 다른 쪽의 변경을 취소할 수 있다.
merge revert는 이후 같은 브랜치를 다시 merge할 때 결과에도 영향을 줄 수 있다.git show <merge-commit>과 커밋 그래프를 확인하고 팀의 통합 흐름을 이해한 뒤 실행해야 한다.
merge commit이 만들어지는 방식은 Git merge와 rebase 차이에서 이어서 볼 수 있다.
공유 브랜치의 선택 기준
reset은 로컬 브랜치 포인터를 옮길 뿐 원격 브랜치를 자동으로 바꾸지 않는다.
이미 push한 커밋을 reset한 뒤 같은 브랜치를 push하면 일반적으로 non-fast-forward로 거절된다.
이를 강제 push로 덮어쓰면 다른 사람이 그 커밋을 기준으로 만든 작업과 충돌할 수 있다.--force-with-lease가 무조건 안전을 보장하는 것도 아니므로 공유 브랜치의 잘못된 커밋을 고치는 기본 절차로 삼으면 안 된다.
아직 push하지 않은 개인 브랜치의 마지막 커밋을 다시 구성하려면 reset을 사용할 수 있다.
이미 main이나 여러 사람이 사용하는 브랜치에 올라간 변경을 취소하려면 revert로 새 기록을 남기는 편이 안전하다.
공유 여부가 확실하지 않다면 git fetch origin 뒤에 git log --graph --oneline --decorate --all로 원격 상태를 확인한다.
원격 변경을 확인하고 통합하는 흐름은 Git fetch와 pull 차이에서 자세히 정리했다.
상황별 선택
아직 공유하지 않은 마지막 커밋만 취소하고 변경을 staged 상태로 유지하려면 git reset --soft HEAD~1을 사용한다.
변경을 unstaged 상태로 다시 나누고 싶다면 git reset HEAD~1을 사용한다.
로컬 커밋과 파일 변경을 모두 정말로 버려야 한다면 상태와 대상 커밋을 확인한 뒤에만 git reset --hard를 검토한다.
보존해야 할 변경이 조금이라도 있다면 먼저 브랜치나 커밋으로 기준점을 남긴다.
이미 공유한 커밋의 변경을 취소하면서 이력을 유지하려면 git revert <commit>을 사용한다.
커밋하지 않은 파일 하나를 원래 상태로 되돌리는 목적이라면 revert가 아니라 git restore의 대상과 데이터 손실 가능성을 확인한다.
결정하기 전에 커밋이 원격에 공유되었는지와 working tree에 보존해야 할 변경이 있는지 확인한다.
특히 reset --hard와 강제 push는 되돌릴 대상을 정확히 확인한 뒤에만 사용해야 한다.
참고 자료


























