0. 서론

Git을 사용하다 보면 커밋을 실수하거나 Push를 실수하는 경우가 있다. 이번 포스트에서는 이처럼 원치 않는 이력이 저장되었을 때 이를 되돌리는 방법들에 대해 알아보자.

1. TL;DR

이번 포스트에서는 아래 개념들을 알아 볼 것이다.

  • HEAD, Index, Working Directory의 관계
  • git switch
  • git reset
  • git revert

2. HEAD, Index, Working Directory

지난 포스트에 잠시 등장했던 HEAD 포인터에 대한 추가적인 설명과 함께 git의 핵심 구조이기도 한 HEAD, Index, Working Directory의 의미와 구조, 관계를 알아야 이후 명령어들을 정확히 이해할 수 있다.

2.1. HEAD

HEAD는 지난 포스트에도 언급되었듯 현재 로컬 저장소가 적용된 이력을 가리키는 포인터이다. 그 말은 즉, 현재 저장소에서 어떠한 커밋을 수행한다면 커밋된 이력의 부모 이력이 바로 현재 HEAD가 가리키는 이력이 되는 것이다.


만약 아래와 같은 상황에서

한 번의 커밋을 수행한다 치면 다음처럼 현재 HEAD의 자식 이력으로 커밋이 붙게 된다

직후 아래처럼 HEAD가 자동으로 커밋된 자식 이력으로 이동한다.

이 두 가지 과정(자식 이력의 추가 -> HEAD가 자식으로 이동)이 바로 git commit을 했을 때 일어나는 일이다.

2.2. Index

그렇다면 Index는 무엇일까?

마찬가지로 지난 포스트에서 git add를 설명하며 커밋을 하기 전 변경 이력들을 잘 포장(staging)하는 것이라 하였는데, 이 때 이 staging된 변경 이력들이 모여 있는 공간이 Index이다.

아래 도식으로 치면

git add직후 파일들이 박스 안에 들어가 포장되어 있는데, 저 박스가 바로 Index라 볼 수 있다.

2.3. Working Directory

워킹 디렉토리는 우리가 지금까지 언급했던 로컬 저장소를 의미한다. 위의 두 개념과 달리 직접 파일 탐색기를 통해 상태와 구조를 확인할 수 있고, 이력이 아닌 파일이 저장되어 있다는 점이 차이이다.

2.4. 정리

이 셋의 관계를 그림으로 정리하면 아래와 같다.

3. git switch

앞서 commit과정에서 HEAD가 자식 이력으로 이동하는 것을 볼 수 있었다. 그런데 저 HEAD는 사용자가 직접 움직일 수 없는걸까?


정답은 “가능하다”이다.


HEAD를 원하는 이력으로 이동하는 git 명령어가 존재하는데, 바로 git switch이다.


사실 git 2.23버전 이전까지는 git switch라는 명령어가 없었고 이러한 역할을 git checkout이 담당했는데, git checkout의 역할이 너무 많다 보니 새롭게 git switch라는 명령어가 HEAD/브랜치 등의 이동을 담당하게 되었다.


아무튼, git switch 명령은 아래와 같이 이루어져 있다.

git switch [<options>] [--no-guess] <branch>
git switch [<options>] --detach [<start-point>]
git switch [<options>] (-c|-C) <new-branch> [<start-point>]
git switch [<options>] --orphan <new-branch>

이 중 앞의 options부분은 나중에 git에 숙달되면 세부적으로 설정할 수 있을 것이므로 무시하고, 아래 구조라고만 생각해도 된다.

git switch <branch>
git switch --detach [<start-point>]
git switch -c <new-branch> [<start-point>]
git switch --orphan <new-branch>

각각은 아래와 같은 의미이다.

// 특정 브랜치로 저장소를 checkout한다. HEAD도 함께 이동한다.
git switch <branch> 

//start-point(특정 브랜치 또는 커밋으)로 HEAD만을 이동한다. HEAD와 브랜치 상태가 분리된다
git switch --detach [<start-point>] 

// 새 브랜치를 생성한 후 start-point로 HEAD를 이동한다
git switch -c <new-branch> [<start-point>] 

// 새 고아 브랜치를 생성한다
git switch --orphan <new-branch> 

이 중 브랜치와 브랜치 관련 명령에 대해서는 나중에 또 다시 설명하기로 하고, 커밋을 대상으로 하는 두 번째 명령만을 살펴 보자.


git switch --detach 명령어를 사용하면 HEAD를 우리가 원하는 곳으로 이동시킬 수 있는데, 원하는 곳은 Commit ID를 통해 지정한다.


따라서 아래와 같은 상태일 때

아래와 같이 명령어를 입력하면

git switch --detach 42928a

다음처럼 HEAD만 이동한다.

이처럼 실제 브랜치의 리프 노드와 HEAD가 같은 곳을 가리키지 않을 경우 HEAD를 분리된 헤드(Detached HEAD)라 부른다.


이때 분리된 헤드는 특정 브랜치를 가리키는 것이 아니라 어느 브랜치에도 속하지 않고 특정한 커밋만을 가리키는 것으로 간주되는데, 따라서 HEAD가 분리된 상태에서는 커밋을 진행할 수 없다.


그렇다면 HEAD 이동은 어떤 상황에서 사용할까?

바로 이전 이력의 작업 상태를 확인해야 할 일이 있거나, 과거 시점에서 새 브랜치를 생성해야 할 때 이용한다.


예를 들어, 열심히 프로그래밍을 하던 도중 버그가 일어난 것을 발견했는데 해당 버그가 과거 시점에 작성한 코드에 의한 버그라고 가정해 보자. 그런데 이 코드가 문제가 있을 것으로 미처 생각하지 못하고 해당 코드에 의존해서 추가 기능들을 잔뜩 작성해 놓은 상태라면? 이미 커질대로 커져버린 코드 베이스 때문에 함부로 건드리기 어려울 것이다.


이 경우 문제가 있는 코드가 최초 작성된 시점으로 돌아가 버그 수정용 브랜치를 생성해 그 안에서 버그를 깔끔히 제거한 후 기존 브랜치와 합치는 방식의 작업을 고려해 볼 수 있다.


즉, 아래와 같은 상황에서

먼저 HEAD를 버그 코드가 맨 처음 작성된 시점으로 옮긴다.

그 다음 bugfix 브랜치를 생성후 이동해 버그를 고친 후 해당 브랜치에 커밋하고

고쳐진 코드를 기존 작업 코드와 합치면 된다!

4. git reset

HEAD를 되돌리는 것으로 이전 작업 상태를 '’확인’‘할 수 있는 것은 알겠는데, 만약 커밋을 잘못하는 등의 실수를 해결하기 위해 브랜치 상황 자체를 이전 상태로 되돌리는 방법은 없을까?


이러한 역할을 하는 것이 바로 git resetgit revert이다.


그 중 git reset은 아주 간단하게 설명하면 특정 시점 이후의 해당 브랜치의 모든 이력을 지워버리는 명령어이다.


git reset의 명령어는 아래처럼 구성되는데,

git reset [<commit>] [--soft | --mixed [-N] | --hard | --merge | --keep]

눈여겨 살펴 보아야 할 것은 --soft, --mixed, --hard 세 가지이다(나머지는 브랜치와 관련된 옵션이므로 이 포스트에서는 설명하지 않는다).


만약 아래와 같은 상황일 때

HEAD를 C2커밋으로 옮기고(꼭 HEAD를 기준으로 reset할 필요는 없지만, 커밋 ID를 이용해 reset할 경우 HEAD를 해당 커밋으로 switch하는 추가 작업이 필요해진다)

git reset 명령어를 이용하면 아래처럼 된다.

이 때 이전에 커밋했던 C3, C4는 어떻게 되었을까? 이를 결정하는 게 바로 --soft, --mixed, --hard의 역할이다.

이전의 커밋 내역들은 옵션에 따라 다음처럼 다루어진다.

Option Description
–soft 모든 이전 커밋(변경 이력)들이 staging 상태로 존재함. 즉, Index에 저장되어 있음
–mixed 모든 이전 커밋(변경 이력)들이 unstaged 상태로 존재함. 즉, Working Directory에만 적용되어 있음
–hard 모든 이전 커밋(변경 이력)들이 제거됨. Working Directory에도 reset한 시점 파일들이 적용되어 있음

즉, 아래와 같다.

실제 터미널에서의 과정을 스크린샷으로 살펴보면

위처럼 C1, C2, C3 커밋을 조성해 놓았을 때

--soft 옵션을 적용한 경우 C2로 리셋이 되었지만 C3 변경점이 Index에 add되어 있는 상태임을 알 수 있다.

--mixed옵션(혹은 적지 않음)을 적용한 경우 C2로 리셋이 되어 있고, C3 변경점이 unstaged되어 있는 상태임을 알 수 있다.

마지막으로, --hard옵션의 경우 “HEAD가 이제 C2를 가리킨다”는 알림과 함께 C3 변경점이 완전히 삭제된 것을 볼 수 있다.


따라서 위 세 가지 상황 중 soft, mixed저장소에는 C3변경점인 output.txt파일이 존재하지만 hard 옵션 저장소에는 삭제되어 존재하지 않게 된다.

5. git revert

git revert는 git reset과 달리 되돌리려는 이전 시점의 커밋을 추가로 커밋한다. 엄밀히 말하면 되돌리려는 시점까지의 변경 이력을 정 반대로 하는 커밋을 추가함으로써 시점을 되돌리는 것과 같은 효과를 만들어낸다.

즉 아래와 같다.

git revert는 다음과 같은 명령으로 사용할 수 있다.

git revert [--no-commit] <commit>

커밋 아이디 앞에 --no-commit 옵션을 추가하면 revert명령을 해도 새 이력이 바로 커밋되지 않고 staging만 일어나게 된다.


revert기능을 이용하면 언제 어느 시점으로 되돌려졌는지의 기록도 커밋에 추가되기 때문에 단순히 이력을 되돌리는 것을 목적으로 한다면 커밋 히스토리 관리 측면에서 revert가 더 유용하다.


실제 터미널에서는 다음과 같은 일이 일어난다.

First, Second, Third 커밋이 존재할 때 Second 커밋으로 revert할 경우

Third커밋의 자식 노드로 Revert Second Commit이 추가되는 것을 볼 수 있다.