Git을 사용하다 보면 여러 개의 커밋을 생성한다.
이렇게 여러 개의 커밋을 생성하는 도중 특정 커밋 이전 상황으로 돌아가고 싶을 수도 있을 것이다.
예를 들어 A라는 기능을 만들었는데 법적 규제 때문에 A 기능을 사용할 수 없게 되었다. 이때 개발자는 A라는 기능을 개발하기 전 상황으로 돌아가야 할 것이다.
이때 활용하는 Git의 기능이 Reset이다.
(참고로 Git Reset 활용 시 Untracked 파일은 건드리지 않는다)
Soft와 Mixed 모드는 지정한 커밋 상태로 커밋 이력을 되돌리지만 워킹 디렉터리에 작업했던 내용은 그대로 유지시킨다.
그렇다면 동일한 동작 과정인 것 같은데 왜 Soft와 Mixed로 나누어놨을까?
Soft와 Mixed의 차이는 아래와 같다.
Hard Reset은 특정 커밋과 현재 상황을 완전히 일치시킴으로써 깔끔하게 모든 중간 작업물들을 리셋 시키는 것을 의미한다.
다른 git reset 옵션은 명령어를 수행하더라도 워킹 디렉터리에는 작업했던 내용이 남아 있다.
하지만, 유일하게 Hard 옵션은 커밋과 더불어 작업했던 내용까지 워킹 디렉터리에서 초기화시킨다.
이러한 점에서 git reset의 --hard
옵션은 reset
명령을 위험하게 만드는 유일한 옵션이다.
만약 내가 A 파일을 커밋 했었는데 git reset을 통해 A 파일의 작업물을 커밋에서 제외하고 싶다 가정하자.
각각의 옵션으로 git reset을 시키면 아래와 같은 결과가 나온다.
--mixed
: A 파일이 삭제되지는 않지만 Unstaging 상태로 변경됨
git add
를 통해 Staging 할 필요가 있음git reset
의 Default Option--soft
: A 파일이 삭제되지 않고 Staging된 상태로 변경됨
git commit
만 수행하더라도 A 파일은 Staging된 상태이므로 커밋에 반영된다.git reset
을 수행했으므로 현재 HEAD가 가리키는 커밋에는 A 파일이 존재하지 않을 것이다.--hard
: A 파일이 현재 커밋에 존재하지 않고 워킹 디렉터리에서도 삭제되어 있음
구분 | Soft | Mixed(기본) | Hard |
---|---|---|---|
HEAD | 초기화 | 초기화 | 초기화 |
스테이지 | 유지 | 초기화 | 초기화 |
Working Tree 변경사항 | 유지 | 유지 | 초기화 |
Git Log > Reset 후 상태로 만들고 싶은 커밋 선택 > 마우스 오른쪽 클릭 > Reset Current Branch to Here...
을 클릭하면 git reset을 수행할 수 있다.
그럼 아래와 같은 창이 나올 것이다.
이제 하나씩 클릭해서 어떻게 동작하는지 알아보자.
--soft
옵션)일단 현재 main 브랜치의 HEAD가 가리키는 커밋은 Cherrypick으로 가져올 커밋
이라는 메시지를 가진 커밋으로 성공적으로 Reset 되었음을 확인할 수 있다.
(Git Reset 테스트를 위한 파일 커밋
메시지를 가진 커밋이 삭제됨)
이제 reset_test_file.md
파일을 보자.
이미지에서 볼 수 있듯 초록색 글씨로 되어 있는 것을 볼 수 있는데 이는 IntelliJ에서 "Staging된 파일이다"라는 것을 나타낸다.
그렇다면 바로 Commit 할 수 있을까?
한 번 commit 해보자.
딱히 git add
를 통해 Staging 하지도 않았는데 커밋이 가능함을 볼 수 있다.
커밋 시킨 뒤 Mixed reset을 수행해 보자.
--mixed
옵션)마찬가지로 정상적으로 git reset 되었음을 확인할 수 있다.
하지만 큰 차이점이 있는데 바로 reset_test_file.md
파일명이 빨간색 글씨로 되어 있다는 것이다.
IntelliJ에서 빨간색 글씨는 해당 파일이 Staging 되지 않은 Untracked 파일임을 의미한다.
그렇다면 Commit 시 어떤 동작이 수행될까? 커밋 해보자.
--soft
옵션과 다르게 reset_test_file.md
파일이 커밋 후보 파일에 없음을 알 수 있다.
즉, --mixed
옵션을 사용하면 새로 생성된 파일은 Unstaging 됨을 확인할 수 있다.
마지막으로 --hard
옵션을 테스트하기 위해 git add
를 통해 Staging 시킨 뒤 커밋 시키고 다시 git reset을 수행하자.
--hard
옵션)git reset이 정상적으로 수행되었다.
하지만 큰 차이점이 있는데 분명 README.md
파일과 settings.gradle
파일 사이에 있던 reset_test_file.md
파일이 아예 워킹 디렉터리에서 삭제되었음을 볼 수 있다.
이런 상황이 된다면 (원격 저장소에 해당 파일을 Push 하지 않았다면) 삭제된 파일을 복구할 방법이 없다.
따라서 --hard
옵션을 사용할 경우 매우 큰 주의를 기울이며 사용하도록 하자.
# 방법 1 : 커밋 ID 활용
git reset [--hard | --mixed | --soft] <커밋 ID>
# 방법 2 : HEAD 예약어 활용
git reset [--hard | --mixed | --soft] HEAD~<숫자> # 방법 2-1
git reset [--hard | --mixed | --soft] HEAD^<숫자> # 방법 2-2
단순히 내가 Git Reset을 통해 돌아가고 싶은 커밋 ID를 뒤에 입력해 주면 된다.
필자는 이 방법을 선호하는데 아래에서 설명할 HEAD 활용 방법 같은 경우 몇 번째 커밋인지 숫자를 세야 하는 것도 있고 직접 수행하기 전까지 어떤 결과가 나올지 모호한데 커밋 ID를 활용하면 확실하고 안전하게 reset 시킬 수 있기 때문이다.
Git에서는 HEAD에 ^
기호나 ~
기호 뒤에 숫자를 붙임으로써 현재 HEAD가 가리키는 커밋을 기준으로 특정 커밋을 가리킬 수 있다.(상대 참조)
^
와 ~
의 의미는 아래와 같다.
HEAD~<숫자>
: n 번째 부모 커밋
HEAD^
: 현재 커밋의 부모 브랜치
위 2가지 상황에서 부모 커밋은 모두 "현재 브랜치"를 기준으로 찾아야 한다.
벌써부터 어렵다. 이미지를 통해 알아보자.
현재 7번 커밋이 HEAD라고 가정하자.
과연 HEAD^
은 어떤 커밋을 가리킬까?
이를 파악하기 위해선 현재 브랜치가 어디인지를 알아야 한다.
만약 보라색 브랜치가 현재 브랜치라면 4번 커밋을, 노란색 브랜치가 현재 브랜치라면 6번 커밋을 나타낼 것이다.
만약 3번이나 5번 커밋과 같이 부모가 아닌 N번째 조상 커밋을 나타내고 싶다면 ~
를 사용하면 될 것이다.
HEAD~2
를 활용하면 보라색 브랜치가 현재 브랜치일 경우 3번 커밋을, 노란색 브랜치가 현재 브랜치라면 5번 커밋을 나타낼 수 있다.
위 설명을 보면 알겠지만 HEAD를 활용해 특정 커밋을 나타내기엔 너무 어렵고 모호하며 현재 상황에 큰 의존성을 가진다.
그러니 특수한 상황이 아니라면 웬만하면 커밋 ID를 활용하는 것을 추천한다.
위에서 --hard
옵션을 사용하면 파일이 삭제되고 복구할 수 없으니 위험하다는 말은 했지만, 사실 엄밀히 말하자면 복구 방법이 존재한다.
git reset --hard
를 통해 reset시키더라도 로컬 저장소에서 커밋은 삭제되는 것이 아닌 사라진 것처럼 안 보일 뿐이다.
이해하기 쉽게 말하자면(엄밀히 따지자면 다르지만) 컴퓨터에서 특정 파일을 삭제할 경우 영구 삭제되었다고 생각하겠지만 사실 휴지통에는 존재하는 상황이라고 생각하면 편하다.
이런 상황에서 만약 내가 복구하고 싶은 파일명을 알고 있다면 파일을 다시 복구할 수도 있을 것이다
이처럼 만약 내가 복구하고 싶은 파일이 존재하는 커밋 ID를 알고 있다면 해당 커밋 ID를 통해 삭제된 파일을 복구할 수도 있다.
하지만 대부분의 경우 커밋 ID를 외워놓고 있지 않을 것이므로 복구 방법이 존재하지 않는 것과 유사하다고 판단하여 이렇게 설명하는 것이다.