다국어 기능을 추가하고 커밋한 이후 이것저것 작업을 했는데 마음에 들지 않아서 커밋 이후의 모든 변경내역을 폐기하고 마지막 커밋한 상태로 깔끔하게 재시작을 하고 싶었다. 그래서 git reset --hard HEAD~
를 했다.
그리고 나의 다국어 기능은 싸그리 날아가버렸다.😇 어려운 로직은 없었지만 타임리프 템플릿에 메시지를 한땀한땀 입력했기 때문에 시간이 꽤 걸렸는데... 메시지 관리 파일에 입력한 내용도 모두...
나는 왜 이런 어처구니 없는 실수를 해 버린 걸까?
중요한 프로젝트가 아니라 조심하지 않은 탓도 있지만, 실수를 한 결정적인 이유는 내가 제대로 몰랐기 때문이다. 그래서 좀 더 알아보고 정리해보기로 했다.
결론부터 말하자면 마지막 커밋 이후의 작업 내역을 모두 폐기하고 깔끔하게 재시작하고 싶다면 두 가지 방법이 있다 :
$ git checkout .
$ git reset HEAD --hard
두 명령어 모두 결과적으로는 직전에 커밋한 상태로 되돌려 주지만 동작의 차이가 있다. 차이점을 알려면 Git이 관리하는 세 가지 영역인 HEAD, Index, Working Tree에 대한 개념을 알아야 한다.
git add
명령을 통해 변경 내역을 올리면 Index가 업데이트 되고, 이어서 git commit
을 하면 HEAD가 업데이트 되는 것이다.조금 더 자세하게 이 세 영역의 관계를 살펴보면 아래와 같다 :
git status
를 해보면 “Changes not staged for commit” 라는 메시지가 뜬다. Working Tree 변경 내역인 V2가 Index에 올라가지 않았다는 뜻이다.git add
를 하면 Working Tree 변경 내역인 V2가 Index에 복사된다(스테이지에 올라간다). Index, Working Tree는 V2 상태이고 HEAD만 V1 상태이다.git status
를 해보면 “Changes to be committed” 라는 메시지가 뜬다. Index에 있는 변경내역 V2가 아직 커밋되지 않았다는 뜻이다.git commit
을 하면 이제 HEAD, Index, Working Tree의 상태가 모두 V2로 동일해졌다. git-checkout - Switch branches or restore working tree files
$ git checkout <branch>
브랜치를 전환할 때 사용한다. 변경 내역이 있을 때 checkout
으로 브랜치 전환을 한 경우 Working tree의 변경 내역은 그대로 보존된다(안전하다).
$ git checkout master (1)
$ git checkout master~2 Makefile (2)
$ rm -f hello.c
$ git checkout -- hello.c (3)
checkout
의 사용 예시이다. (2)를 보면 master 브랜치의 2개 전 커밋에서 Makefile 파일을 가져와 Working Tree에 복구하였다. 개발자가 실수로 hello.c를 삭제했다고 한다면 (3)과 같이 직전 커밋과 같은 상태로 hello.c 파일을 Working Tree에 복구할 수 있다.
--
를 생략할 수 있지만 복구하고자 하는 파일명과 이름이 같은 브랜치가 있는 경우 오류가 발생할 수 있으므로 --
를 붙여주는 것이 좋다.$ git checkout .
따라서 위의 명령은 직전 커밋의 상태로 현재 디렉토리(.
)를 복구하는 명령이 된다. 다시 말해 직전 커밋과 같은 상태로 Working Tree를 되돌려준다. Index도 직전 커밋 상태로 되돌아간다.
git-reset - Reset current HEAD to the specified state
reset
명령 또한 checkout
명령과 마찬가지로 커밋 레벨에서 사용할 수도 있고 파일 경로를 주어 파일 레벨에서 사용할 수도 있다.
$ git reset HEAD~ --soft
위의 명령을 실행하면 HEAD를 최신 커밋의 1개 전 커밋(HEAD~
)으로 이동한다. 만약 직전 커밋의 결과로 HEAD, Index, Working Tree의 상태가 모두 V2라고 했을 때, 위의 명령을 실행하면 HEAD는 이전의 V1 상태이고 Index와 Working Tree는 V2 상태인 것이다.
마지막 커밋을 취소하고 약간의 수정을 한 뒤 커밋을 하고 싶다면 이 명령어를 사용하면 되겠다. 이 용법은 commit --amend
와 동일하다.
최신 커밋을 완료한 뒤 약간의 수정을 마지막 커밋에 밀어넣고 싶거나, 깜빡하고 커밋에 포함시키지 못한 파일이 있는 경우 사용할 수 있다.
수정 내역을 최신 커밋에 반영하고 싶을 때에는 그냥 commit --amend
를 하면 합쳐지고, 커밋에 파일을 추가하고 싶은 경우 git add
를 통해 스테이지에 올린 뒤 commit --amend
를 실행하면 된다. 커밋 메시지를 변경할 필요가 없는 경우 commit --amend --no-edit
와 같이 옵션을 주면 커밋 메시지 편집기가 실행되지 않는다.
$ git reset HEAD~ --mixed
HEAD를 이동한 뒤 Index의 상태를 HEAD와 같은 상태로 변경한다. HEAD를 V2 -> V1으로 변경한 경우 HEAD, Index는 V1 상태이고 Working Tree는 여전히 V2 상태인 것이다. 다르게 표현하면 git commit
도 되돌리고 git add
도 되돌리는 것과 같다.
만약 옵션을 명시하지 않는 경우 기본적으로 --mixed
옵션으로 실행된다.
$ git reset HEAD~ --hard
이 옵션은 위험하다. HEAD를 이동한 뒤 Index, Working Tree의 상태를 모두 HEAD와 같은 상태로 변경한다. reset
을 통해 V2 -> V1으로 변경한 경우 HEAD, Index, Working Tree 모두 V1 상태로 돌아간다. V2 작업 내역은 사라진다. 나의 작업 내역이 싸그리 날라간 이유이다.
즉 reset
명령은 이동한 HEAD의 상태와 같게 주어진 옵션에 따라 한 단계씩 덮어 쓰는 것이다.
$ git reset HEAD --hard
이 명령이 바로 세 영역 모두를 직전 커밋 상태로 되돌리는 방법이다. 그러니까 나는 HEAD~
가 아니라 HEAD
를 썼어야 한 것이다.
reset
명령에 파일 경로를 추가하여 파일 레벨에서 이 명령을 사용할 수 있다.
단 특정 파일로 reset
대상이 한정된 경우는 --soft
옵션이 불가능하다. HEAD는 커밋을 가리키는 포인터이기 때문에 특정 파일에만 HEAD를 변경한다는 것은 말이 안 되기 때문이다.
$ git reset HEAD <file>
--mixed
옵션의 경우에는 해당 파일을 지정한 커밋과 같은 상태로 Index에 해당 파일을 복사한다. 위 명령은 git reset file.txt는 git reset --mixed HEAD file.txt
와 동일한데, 이 명령을 사용했다 하자. 그러면 Index에서 이 파일의 상태가 직전 커밋(HEAD, V1) 상태이고 Working Tree에서는 V2의 상태이다. 즉 해당 파일을 언스테이징 한 것과 같다. git add
의 반대인 것이다.
HEAD~
로 지정하는 경우 1개 전 커밋에서 이 파일이 빠지고 Index에 남아있게 된다(staged 상태). 어느 커밋을 지정하느냐에 따라 파일의 Index 상태와 Working Tree에서의 상태는 다를 수 있다.
git reset --hard HEAD file.txt
를 실행한 경우 해당 파일을 Index에서 제거하고(unstaged) Working Tree에서도 최신 커밋 상태로 되돌린다.
커밋 레벨에서 사용할 때 checkout
과 reset
(--soft
, --mixed
) 모두 Working Tree가 보존된다는 점은 비슷하다. 단 reset --hard
는 Working Tree가 사라지므로 주의한다.
차이점은 명령이 실행된 뒤 HEAD의 위치이다. reset
명령은 HEAD가 가리키는 브랜치의 참조를 변경하지만 checkout
은 HEAD 자체가 이동한다.
명령이 HEAD가 가리키는 브랜치를 움직인다면 "REF", HEAD 자체가 웁직인다면 "HEAD"라고 써 있다. Index와 WorkTree는 명령어에 의해 영향을 받으면 "YES", 받지 않으면 "NO"이다.
'WT Safe?' 열이 중요하다. 이 열에 "NO"라고 적혀있는 것은 Working Tree에 있는 작업 내역이 안전하지 않다는 뜻이므로 사용에 주의한다.
명령어 | HEAD | Index | WorkTree | WT Safe? |
---|---|---|---|---|
Commit Level | ||||
reset --soft [commit] | REF | NO | NO | YES |
reset [commit] | REF | YES | NO | YES |
reset --hard [commit] | REF | YES | YES | NO |
checkout <commit> | HEAD | YES | YES | YES |
File Level | ||||
reset [commit] <paths> | NO | YES | NO | YES |
checkout [commit] <paths> | NO | YES | YES | NO |
checkout
을 브랜치를 변경할 때 사용하거나 커밋 레벨에서 사용할 때는 Working Tree가 안전하지만 파일 레벨에서는 안전하지 않다는 점을 주의하자. git checkout .
명령을 생각해보면 된다. HEAD와 같은 상태로 Index, Working Tree의 파일들(.
)이 변경되기 때문에 마지막 커밋 이후의 변경사항을 폐기할 때 사용되는 것이다. git reset HEAD --hard
도 마찬가지이다.
reset HEAD
대신 reset HEAD~
를 사용하는 바람에 코드를 날려버린 나는 다시 코드 노가다를 할 뻔했지만 울지 않고 챗지피티에게 물어보니 다행히 reflog
라는 것이 있다는 것을 알게 되어 복구에 성공했다. 😄
$ git reflog
$ git reset --hard 7450af2
HEAD is now at 7450af2 #6 feature: 다국어 기능
git reflog
는 HEAD의 이동 기록을 보여준다. 여기서 돌아가고자 하는 커밋(다국어 기능을 추가한 커밋)의 SHA-1 해시값을 찾는다. 그다음 git reset --hard 7450af2
을 하면 "다국어 기능 커밋" 상태로 HEAD, Index, Working Tree 모두 돌아가는 것이다....!!! 이제 HEAD가 다국어 기능 커밋을 가리킨다는 안내가 뜬다.