[Git] 마지막 커밋 이후 변경사항 폐기하기

dondonee·2023년 11월 4일
2
post-thumbnail

마지막 커밋 이후 변경사항 폐기하기

다국어 기능을 추가하고 커밋한 이후 이것저것 작업을 했는데 마음에 들지 않아서 커밋 이후의 모든 변경내역을 폐기하고 마지막 커밋한 상태로 깔끔하게 재시작을 하고 싶었다. 그래서 git reset --hard HEAD~를 했다.

그리고 나의 다국어 기능은 싸그리 날아가버렸다.😇 어려운 로직은 없었지만 타임리프 템플릿에 메시지를 한땀한땀 입력했기 때문에 시간이 꽤 걸렸는데... 메시지 관리 파일에 입력한 내용도 모두...

나는 왜 이런 어처구니 없는 실수를 해 버린 걸까?
중요한 프로젝트가 아니라 조심하지 않은 탓도 있지만, 실수를 한 결정적인 이유는 내가 제대로 몰랐기 때문이다. 그래서 좀 더 알아보고 정리해보기로 했다.


결론부터 말하자면 마지막 커밋 이후의 작업 내역을 모두 폐기하고 깔끔하게 재시작하고 싶다면 두 가지 방법이 있다 :

$ git checkout .
$ git reset HEAD --hard

HEAD, Index, Working Tree

두 명령어 모두 결과적으로는 직전에 커밋한 상태로 되돌려 주지만 동작의 차이가 있다. 차이점을 알려면 Git이 관리하는 세 가지 영역인 HEAD, Index, Working Tree에 대한 개념을 알아야 한다.

  • HEAD: 현재 브랜치의 최신 커밋을 가리키는 포인터이다.
  • Index: 곧 커밋될 내용이다. "Staging Area"라고도 한다.
  • Working Tree: 개발자가 작업하는 로컬 디렉토리이다.
    • "Working Directory"라고도 한다. 여기서 git add 명령을 통해 변경 내역을 올리면 Index가 업데이트 되고, 이어서 git commit을 하면 HEAD가 업데이트 되는 것이다.
    • HEAD, Index는 .git 파일에 효율적인 형태로 저장되어 관리되지만 Working Tree는 실제 파일들의 묶음이다.

조금 더 자세하게 이 세 영역의 관계를 살펴보면 아래와 같다 :

  1. 최초의 커밋을 한 직후에는 HEAD, Index, Working Tree 모두 같은 상태이다. 이 상태를 V1이라 하자.
  2. 첫 커밋 이후 개발자가 작업을 했다면 Working Tree에 변경 내역이 있기 때문에 V2 상태이다. 나머지는 여전히 V1 상태이다.
    • 이 때 git status를 해보면 “Changes not staged for commit” 라는 메시지가 뜬다. Working Tree 변경 내역인 V2가 Index에 올라가지 않았다는 뜻이다.
  3. git add를 하면 Working Tree 변경 내역인 V2가 Index에 복사된다(스테이지에 올라간다). Index, Working Tree는 V2 상태이고 HEAD만 V1 상태이다.
    • git status를 해보면 “Changes to be committed” 라는 메시지가 뜬다. Index에 있는 변경내역 V2가 아직 커밋되지 않았다는 뜻이다.
  4. git commit을 하면 이제 HEAD, Index, Working Tree의 상태가 모두 V2로 동일해졌다.

git checkout

git-checkout - Switch branches or restore working tree files

용법 1. 브랜치 변경

$ git checkout <branch>

브랜치를 전환할 때 사용한다. 변경 내역이 있을 때 checkout으로 브랜치 전환을 한 경우 Working tree의 변경 내역은 그대로 보존된다(안전하다).

용법 2. 파일 복구

$ 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

git-reset - Reset current HEAD to the specified state

reset 명령 또한 checkout 명령과 마찬가지로 커밋 레벨에서 사용할 수도 있고 파일 경로를 주어 파일 레벨에서 사용할 수도 있다.

--soft 옵션

$ git reset HEAD~ --soft

위의 명령을 실행하면 HEAD를 최신 커밋의 1개 전 커밋(HEAD~)으로 이동한다. 만약 직전 커밋의 결과로 HEAD, Index, Working Tree의 상태가 모두 V2라고 했을 때, 위의 명령을 실행하면 HEAD는 이전의 V1 상태이고 Index와 Working Tree는 V2 상태인 것이다.
마지막 커밋을 취소하고 약간의 수정을 한 뒤 커밋을 하고 싶다면 이 명령어를 사용하면 되겠다. 이 용법은 commit --amend와 동일하다.

참고) commit --amend

최신 커밋을 완료한 뒤 약간의 수정을 마지막 커밋에 밀어넣고 싶거나, 깜빡하고 커밋에 포함시키지 못한 파일이 있는 경우 사용할 수 있다.
수정 내역을 최신 커밋에 반영하고 싶을 때에는 그냥 commit --amend를 하면 합쳐지고, 커밋에 파일을 추가하고 싶은 경우 git add를 통해 스테이지에 올린 뒤 commit --amend를 실행하면 된다. 커밋 메시지를 변경할 필요가 없는 경우 commit --amend --no-edit와 같이 옵션을 주면 커밋 메시지 편집기가 실행되지 않는다.

--mixed 옵션

$ git reset HEAD~ --mixed

HEAD를 이동한 뒤 Index의 상태를 HEAD와 같은 상태로 변경한다. HEAD를 V2 -> V1으로 변경한 경우 HEAD, Index는 V1 상태이고 Working Tree는 여전히 V2 상태인 것이다. 다르게 표현하면 git commit도 되돌리고 git add도 되돌리는 것과 같다.
만약 옵션을 명시하지 않는 경우 기본적으로 --mixed 옵션으로 실행된다.

--hard 옵션

$ 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

단 특정 파일로 reset 대상이 한정된 경우는 --soft 옵션이 불가능하다. HEAD는 커밋을 가리키는 포인터이기 때문에 특정 파일에만 HEAD를 변경한다는 것은 말이 안 되기 때문이다.

--mixed (특정 파일 unstaged 상태로 되돌리기)

$ 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에서의 상태는 다를 수 있다.

--hard

git reset --hard HEAD file.txt를 실행한 경우 해당 파일을 Index에서 제거하고(unstaged) Working Tree에서도 최신 커밋 상태로 되돌린다.


checkout / reset 차이

동일한 점

커밋 레벨에서 사용할 때 checkoutreset(--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에 있는 작업 내역이 안전하지 않다는 뜻이므로 사용에 주의한다.

명령어HEADIndexWorkTreeWT Safe?
Commit Level
reset --soft [commit]REFNONOYES
reset [commit]REFYESNOYES
reset --hard [commit]REFYESYESNO
checkout <commit>HEADYESYESYES
File Level
reset [commit] <paths>NOYESNOYES
checkout [commit] <paths>NOYESYESNO

checkout을 브랜치를 변경할 때 사용하거나 커밋 레벨에서 사용할 때는 Working Tree가 안전하지만 파일 레벨에서는 안전하지 않다는 점을 주의하자. git checkout . 명령을 생각해보면 된다. HEAD와 같은 상태로 Index, Working Tree의 파일들(.)이 변경되기 때문에 마지막 커밋 이후의 변경사항을 폐기할 때 사용되는 것이다. git reset HEAD --hard도 마찬가지이다.


사라진 코드 복구 - git reflog

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가 다국어 기능 커밋을 가리킨다는 안내가 뜬다.


🔗 References

0개의 댓글