반응형

작업을 하다보면 어떤 것은 되돌리고(Undo) 싶을 때가 있습니다.
Git을 사용하면 우리가 저지른 실수는 대부분 복구할 수 있지만 되돌린 것은 복구할 수 없다는 것을 주의해야 합니다.

 

1. commit 수정(—amend)

종종 완료한 커밋을 수정해야할 때가 있습니다.

  1. 너무 일찍 commit 했을 때
  2. 어떤 파일을 빼먹었을 때
  3. commit message를 잘못 입력했을 때
  4. 기타 등등

이때 다시 commit 하고 싶으면 파일 수정 작업을 하고 Staging Area에 추가한 다음 --amend 옵션을 사용하여 commit을 재작성 할 수 있습니다.

$ git commit --amend

이 명령은 Staging Area를 사용하여 커밋합니다.

따라서 마지막으로 커밋하고 나서 수정한 것이 없다면(커밋하자마자 바로 이 명령을 실행하는 경우) 조금 전에 한 커밋과 모든 것이 같습니다. 그래서 이때는 commit message만 수정합니다.

 

만약 커밋을 했는데 Stage 하는 것을 깜빡하고 빠트린 파일이 있으면 아래와 같이 고칠 수 있습니다.

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

여기서 실행한 명령어 3개는 모두 커밋 한 개로 기록됩니다.

--amend 를 사용한 두 번째 커밋은 첫 번째 커밋을 덮어씁니다.

 

2. 파일 상태를 Unstage로 변경하기(git reset)

예를 들어 파일을 두 개 수정하고서 따로따로 커밋하려고 했지만, 실수로 git add * 라고 실행해 버렸습니다. 그래서 두 파일 모두 Staging Area에 들어 있습니다.
이 상황에서 둘 중 하나를 어떻게 꺼낼까요? 이럴 때 사용할 수 있는 명령어가 바로 git reset 입니다.

 

만약 README 라는 파일을 꺼내고 싶다면 git reset HEAD README 라고 입력하면 README 파일이 Unstaged 상태가 됩니다.

 

Reset 명확히 알고 가기

resetcheckout 은 Git을 처음 사용하는 사람을 가장 헷갈리게 하는 부분입니다. 제대로 이해하고 사용할 수 없을 것으로 보일 정도로 많은 기능을 지니고 있습니다. 그래서 전부를 알 수는 없더라도 정확하게 구분하여 사용하기 위해 조금 더 살펴보도록 하겠습니다.

세 개의 트리

Git을 서로 다른 세 트리를 관리하는 컨텐츠 관리자로 생각하면 resetcheckout 을 좀 더 쉽게 이해할 수 있습니다. 여기서 "트리"란 실제로는 "파일의 묶음" 입니다. (자료구조의 트리가 아닙니다.)

세 트리 중 Index는 트리로 보기 힘들지만 편의상 트리라 하겠습니다.

 

Git이 관리하는 세 가지 트리

 

트리 역할
HEAD 마지막 커밋 스냅샷, 다음 커밋의 부모 커밋
Index 다음에 커밋할 스냅샷
Working Directory 샌드박스
  • HEAD

    HEAD는 현재 브랜치를 가리키는 포인터이며, 브랜치는 브랜치에 담긴 커밋 중 가장 마지막 커밋을 가리킵니다.

    • 지금의 HEAD가 가리키는 커밋은 바로 다음 커밋의 부모 가 됩니다.
    • 단순하게 생각하면 HEAD는 현재 브랜치 마지막 커밋의 스냅샷 입니다.
  • Index

    Index는 바로 다음에 커밋할 것들입니다.
    이미 앞에서 우리는 이런 개념을 "Staging Area" 라고 배운 바 있습니다.
    Staging Area는 사용자가 git commit 명령을 실행했을 때 Git이 처리할 것들이 있는 곳입니다.

    먼저 Index는 워킹 디렉토리에서 마지막으로 checkout 한 브랜치의 파일 목록과 파일 내용으로 채워집니다. 이후 파일 변경 작업을 하고 변경한 내용으로 Index를 업데이트 할 수 있습니다.
    이렇게 업데이트 하고 git commit 명령을 실행하면 Index는 새 커밋으로 변환됩니다.

    Index는 엄밀히 말해 트리구조는 아닙니다.

  • Working Directory

    마지막으로 워킹 디렉토리입니다. 위의 두 트리는 파일과 그 내용을 효율적인 형태로 .git 디렉토리에 저장합니다. 하지만, 사람이 알아보기는 어렵습니다.

    워킹 디렉토리는 실제 파일이 존재합니다. 바로 눈에 보이기 때문에 사용자가 편집하기 수월합니다.

    워킹 디렉토리는 샌드박스라 생각하면 됩니다.

    커밋하기 전에는 Index(Staging Area)에 올려놓고 얼마든지 변경할 수 있습니다.

Reset 의 역할

위의 트리 세 개를 이해하면 reset 명령이 어떻게 동작하는지 쉽게 알 수 있습니다.

예를 들어 file.txt 파일 하나를 수정하고 커밋하는 것을 세 번 반복하면 히스토리는 아래와 같아집니다.

 

이제 reset 명령이 정확히 어떤 일을 하는지 알아보겠습니다.

reset 명령은 이 세 트리를 간단하고 예측 가능한 방법으로 조작합니다.

 

트리를 조작하는 동작은 세 단계 이하로 이루어집니다.

 

1 단계 : HEAD 이동 (--soft 옵션)


reset 명령이 하는 첫 번째 일은 HEAD를 이동시킵니다.
주의할 점은 checkout 명령처럼 HEAD가 가리키는 브랜치를 바꾸는 것이 아닙니다.
HEAD는 계속 현재 브랜치를 가리키고 있고, 현재 브랜치가 가리키는 커밋을 바꿉니다.


HEAD가 master 브랜치를 가리키고 있었다면 git reset 9e5e6a4 명령은 master 브랜치가 '9e5e6a4' 의 해시값을 갖는 커밋을 가리키게 합니다.

reset 명령에 커밋을 넘기고 실행하면 언제나 이런 작업을 수행합니다.

그리고 reset --soft 옵션을 사용하면 딱 여기 1단계까지만 진행하고 동작을 멈춥니다.

 

이제 위의 다이어그램을 분석해 보겠습니다. reset 명령은 가장 최근의 git commit 명령을 되돌립니다. reset 명령 뒤에 HEAD~ (HEAD의 부모 커밋)를 주면 Index나 워킹 디렉토리는 그대로 놔두고 브랜치가 가리키는 커밋만 이전으로 되돌립니다. 그리고서 Index를 업데이트한 다음에 git commit 명령을 실행하면 git commit --amend 명령의 결과와 같아집니다.

 

2 단계 : Index 업데이트 (--mixed 옵션)


1단계에서 살펴본 예제를 통해 이어서 2단계도 알아보겠습니다.
1단계가 HEAD만 이동한 것이었다면, 2단계는 여기서 한 발짝 더 나아가 Index를 현재 HEAD가 가리키는 스냅샷으로 업데이트할 수 있습니다.

--mixed 옵션을 주고 실행하면 reset 명령은 여기까지 하고 멈춥니다.

reset 명령을 실행할 때 아무 옵션도 주지 않으면 기본적으로 2단계인 --mixed 옵션으로 동작합니다.
(git reset HEAD~ == git reset --mixed HEAD~)

 

위 그림에서 알 수 있듯이 가리키는 대상을 직전 커밋으로 되돌리는 것은 1단계와 같습니다만 --mixed 옵션은 HEAD를 옮기고 나서 Staging Area 도 업데이트를 합니다.

한 마디로, git commit 명령도 되도리고, git add 명령까지도 되돌리는 것입니다.

 

3 단계 : Working Directory 업데이트 (--hard 옵션)


마지막 3단계입니다.
reset 명령은 --hard 옵션을 사용하여 HEAD와 Index를 변경한 후에 워킹 디렉토리까지 변경할 수 있습니다.

이 과정은 git addgit commit 명령으로 생성한 마지막 커밋을 되돌리고, 나아가 워킹 디렉토리의 내용까지도 되돌립니다.

--hard 옵션은 매우 매우 중요합니다.
왜냐하면 reset 명령을 위험하게 만드는 유일한 옵션이기 때문입니다.

 

Git에는 데이터를 실제로 삭제하는 방법이 별로 없습니다만, reset --hard 옵션이 그 중 하나입니다. 1~2 단계 reset 명령은 어떻게 사용하더라도 간단히 결과를 되돌릴 수 있습니다. 하지만 --hard 옵션은 되돌리는 것이 불가능합니다. 이 옵션을 사용하면 워킹 디렉토리의 파일까지 강제로 덮어씁니다.
(그나마 Git이 커밋으로 보관하고 있다면 reflog 를 이용해서 다시 복원할 수 있지만, 커밋한 적 없는 데이터는 복원할 수 없습니다.)

요약
reset 명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춥니다.

  1. HEAD를 옮긴다. (—soft 옵션)
  2. Index를 HEAD가 가리키는 상태로 만든다. (—mixed 옵션)(default)
  3. Working Directory를 Index의 상태로 만든다. (—hard 옵션)

경로를 주고 Reset 하기

지금까지 reset 명령을 실행하는 기본 형태와 사용 방법을 살펴봤습니다.

reset 명령을 실행할 때 특정 경로를 지정해줄 수도 있는데, 이 경우 1단계를 건너뛰고 정해진 경로의 파일에만 나머지 reset 단계를 적용하게 됩니다. 즉, HEAD는 일부분에 대해 부분적으로 변경하는 것이 불가능하지만 Index나 워킹 디렉토리는 일부분만 갱신할 수 있습니다.

 

예를 들어, git reset file.txt 명령을 실행한다고 가정해보겠습니다.
(이는 git reset --mixed HEAD file.txt 를 짧게 쓴 것입니다.)

이 명령은 작업중이던 v2 버전의 file.txt 파일을 Unstaged 상태로 만듭니다.
이 명령의 다이어그램과 git add 명령을 비교해보면 정확히 반대인 것을 알 수 있습니다.

파일을 명시할 수도 있지만, 특정 커밋을 명시할 수도 있습니다.

특정 커밋을 명시하면 Git은 HEAD에서 파일을 가져오는 것이 아니라 그 커밋에서 파일을 가져옵니다.

git reset eb43bf file.txt 명령은 다음과 같이 실행합니다.

합치기

reset 을 통해 커밋을 되돌리는 것을 알아봤으니 이번에는 여러 커밋을 하나로 합치는 것도 알아보겠습니다.

이런 프로젝트가 있다고 가정해보겠습니다.

첫 번째 커밋은 파일 하나를 추가했고,
두 번째 커밋은 기존 파일을 수정하고 새로운 파일도 추가했습니다.
세 번째 커밋은 첫 번째 파일을 다시 수정했습니다.


이때 두 번째 커밋과 세 번째 커밋과 합치고 싶은 상황입니다.

git reset --soft HEAD~2 명령을 실행하여 HEAD 포인터를 현재에서 2단계 전 커밋으로 되돌릴 수 있습니다.

이제 이 상황에서 git commit 명령을 실행합니다.

이렇게 되면 file-a.txt 파일이 있는 v1 커밋이 하나 그대로 있고, 두 번째 커밋에는 v3 버전의 file-a.txt 파일과 새로 추가된 file-b.txt 파일이 있게됩니다. v2 버전은 더이상 히스토리에 없습니다.

 

3. Checkout

이제는 checkout 명령과 reset 명령에 어떤 차이가 있는지 궁금할 것입니다. reset 명령과 마찬가지로 checkout 명령도 위의 세 트리를 조작합니다. checkout 명령도 파일 경로를 쓰느냐 안 쓰느냐에 따라 동작이 다릅니다.

경로를 지정하지 않은 경우

git checkout [branch] 명령은 git reset --hard [branch] 명령과 비슷하게 [branch] 스냅샷을 기준으로 세 트리를 조작합니다. 하지만, 두 가지 사항이 다릅니다.

  1. reset --hard 명령과는 달리 checkout 명령은 워킹 디렉토리를 안전하게 다룹니다. 저장하지 않은 것이 있는지 확인해서 날려버리지 않는다는 것을 보장합니다.
    그리고 똑똑하게도 워킹 디렉토리에서 Merge 작업을 한 번 시도해보고 변경하지 않은 파일만 업데이트 합니다. 반면에 reset --hard 명령은 확인하지 않고 단순히 모든 것을 바꿔버립니다.
  2. 또 중요한 차이점은, 어떻게 checkout 명령이 HEAD를 업데이트 하는가 입니다. reset 명령은 HEAD가 가리키는 브랜치를 움직이지만(정확히는 브랜치 Refs), checkout 명령은 HEAD 자체를 다른 브랜치로 옮깁니다.

예를 들어 각각 다른 커밋을 가리키는 masterdevelop 브랜치가 있고 현재 워킹 디렉토리는 develop 브랜치라고 가정해보겠습니다.(즉 HEAD는 develop 브랜치를 가리킵니다).

 

git reset master 명령을 실행하면 develop 브랜치는 master 브랜치가 가리키는 커밋과 같은 커밋을 가리키게 됩니다. 반면, git checkout master 명령을 실행하면 develop 브랜치가 가리키는 커밋은 바뀌지 않고 HEAD가 master 브랜치를 가리키도록 업데이트된다. 이제 HEAD는 master 브랜치를 가리키게 된다.

 

그래서 위 두 경우 모두 HEAD는 결과적으로 A 커밋을 가리키게 되지만 방식은 완전히 다릅니다.
reset 명령은 HEAD가 가리키는 브랜치의 포인터를 옮겼고 checkout 명령은 HEAD 자체를 옮겼습니다.

경로를 지정한 경우

checkout 명령을 실행할 때 파일 경로를 줄 수도 있습니다. reset 명령과 비슷하게 HEAD는 움직이지 않습니다. 동작은 git reset [branch] file 명령과 비슷합니다.

Index의 내용이 해당 커밋 버전으로 변경될 뿐만 아니라 워킹 디렉토리의 파일도 해당 커밋 버전으로 변경됩니다. 한 마디로 git reset --hard [branch] file 명령의 동작이랑 같습니다. 워킹 디렉토리가 안전하지도 않고 HEAD도 움직이지 않습니다.

 

 

요약

reset 명령을 위주로 살펴보면서 checkout 까지 함께 살펴보았습니다.

아래에는 어떤 명령이 어떤 트리에 영향을 주는지에 대한 요약표입니다.

명령이 HEAD가 가리키는 브랜치를 움직인다면 "HEAD" 열에 "REF" 라고 적혀 있고, HEAD 자체가 움직인다면 "HEAD" 라고 적혀 있습니다.

 

각 명령이 트리에 주는 영향

Command HEAD Index Working Directory WD Safe?
Commit Level
reset --soft [commit] REF NO NO YES
reset [--mixed] [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

꼭 봐야하는 것은 WD Safe? 입니다.

여기에 "NO" 라고 적혀 있다면 워킹 디렉토리에 저장하지 않은 내용이 안전하지 않기 때문에 해당 명령을 실행하기 전에 한 번 더 생각해보아야 합니다.(되돌리기 어렵습니다.)

 


출처 - 저서 ProGit

반응형
반응형