Pro git - 7.7

Updated:

Pro git - Reset 명확히 알고 가기

1. 세 개의 트리

  • 여기서 “트리” 란 실제로는 “파일의 묶음” 이다.
  • 자료구조의 트리가 아니다 (세 트리 중 Index는 트리도 아니지만, 이해를 쉽게 하려고 일단 트리라고 한다).
  • Git은 일반적으로 세 가지 트리를 관리하는 시스템이다.
트리 역할
HEAD 마지막 커밋 스냅샷, 다음 커밋의 부모 커밋
index 다음에 커밋할 스냅샷
워킹 디렉토리 샌드박스

1.1 HEAD

  • HEAD는 현재 브랜치를 가리키는 포인터이며, 브랜치는 브랜치에 담긴 커밋 중 가장 마지막 커밋을 가리킨다.
  • 지금의 HEAD가 가리키는 커밋은 바로 다음 커밋의 부모가 된다.
  • 단순하게 생각하면 HEAD는 현재 브랜치 마지막 커밋의 스냅샷이다.

  • HEAD가 가리키는 스냅샷을 살펴보기는 쉽다.
  • 아래는 HEAD 스냅샷의 디렉토리 리스팅과 각 파일의 SHA-1 체크섬을 보여주는 예제다.
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib
  • cat-filels-tree 명령은 일상적으로는 잘 사용하지 않는 저수준 명령이다.
  • 이런 저수준 명령을 “plumbing” 명령이라고 한다.
  • Git이 실제로 무슨 일을 하는지 볼 때 유용하다.

1.2 Index

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

  • 먼저 Index는 워킹 디렉토리에서 마지막으로 Checkout 한 브랜치의 파일 목록과 파일 내용으로 채워진다.
  • 이후 파일 변경작업을 하고 변경한 내용으로 Index를 업데이트 할 수 있다.
  • 이렇게 업데이트 하고 git commit 명령을 실행하면 Index는 새 커밋으로 변환된다.
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb
  • 또 다른 저수준 git ls-files 명령은 훨씬 더 장막 뒤에 가려져 있는 명령으로 이를 실행하면 현재 Index가 어떤 상태인지를 확인할 수 있다.

  • Index는 엄밀히 말해 트리구조는 아니다.
  • 사실 Index는 평평한 구조로 구현되어 있다.
  • 여기에서는 쉽게 이해할 수 있도록 그냥 트리라고 설명한다.

1.3 워킹 디렉토리

  • 마지막으로 워킹 디렉토리를 살펴보자.
  • 위의 두 트리는 파일과 그 내용을 효율적인 형태로 .git 디렉토리에 저장한다.
  • 하지만, 사람이 알아보기 어렵다. 워킹 디렉토리는 실제 파일로 존재한다.
  • 바로 눈에 보이기 때문에 사용자가 편집하기 수월하다.
  • 워킹 디렉토리는 샌드박스로 생각하자.
  • 커밋하기 전에는 Index(Staging Area)에 올려놓고 얼마든지 변경할 수 있다.
$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

2. 워크플로

  • Git의 주목적은 프로젝트의 스냅샷을 지속적으로 저장하는 것이다.
  • 이 트리 세 개를 사용해 더 나은 상태로 관리한다.

  • 이 과정을 시각화해보자.
  • 파일이 하나 있는 디렉토리로 이동한다.
  • 이걸 파일의 v1이라고 하고 파란색으로 표시한다.
  • git init 명령을 실행하면 Git 저장소가 생기고 HEAD는 아직 없는 브랜치를 가리킨다(master 는 아직 없다).

  • 이 시점에서는 워킹 디렉토리 트리에만 데이터가 있다.
  • 이제 파일을 커밋해보자. git add 명령으로 워킹 디렉토리의 내용을 Index로 복사한다.

  • 그리고 git commit 명령을 실행한다.
  • 그러면 Index의 내용을 스냅샷으로 영구히 저장하고 그 스냅샷을 가리키는 커밋 객체를 만든다.
  • 그리고는 master 가 그 커밋 객체를 가리키도록 한다.

  • 이때 git status 명령을 실행하면 아무런 변경 사항이 없다고 나온다.
  • 세 트리 모두가 같기 때문이다.
  • 다시 파일 내용을 바꾸고 커밋해보자.
  • 위에서 했던 것과 과정은 비슷하다.
  • 먼저 워킹 디렉토리의 파일을 고친다.
  • 이를 이 파일의 v2라고 하자. 이건 빨간색으로 표시한다.

  • git status 명령을 바로 실행하면 “Changes not staged for commit,” 아래에 빨간색으로 된 파일을 볼 수 있다.
  • Index와 워킹 디렉토리가 다른 내용을 담고 있기 때문에 그렇다.
  • git add 명령을 실행해서 변경 사항을 Index에 올려주자.

  • 이 시점에서 git status 명령을 실행하면 “Changes to be committed” 아래에 파일 이름이 녹색으로 변한다.
  • Index와 HEAD의 다른 파일들이 여기에 표시된다.
  • 즉 다음 커밋할 것과 지금 마지막 커밋이 다르다는 말이다.
  • 마지막으로 git commit 명령을 실행해 커밋한다.

  • 이제 git status 명령을 실행하면 아무것도 출력하지 않는다.
  • 세 개의 트리의 내용이 다시 같아졌기 때문이다.
  • 브랜치를 바꾸거나 Clone 명령도 내부에서는 비슷한 절차를 밟는다.
  • 브랜치를 Checkout 하면, HEAD가 새로운 브랜치를 가리키도록 바뀌고, 새로운 커밋의 스냅샷을 Index에 놓는다. 그리고 Index의 내용을 워킹 디렉토리로 복사한다.

3. Reset 의 역할

  • 위의 트리 세 개를 이해하면 reset 명령이 어떻게 동작하는지 쉽게 알 수 있다.
  • 예로 들어 file.txt 파일 하나를 수정하고 커밋한다.
  • 이것을 세 번 반복한다. 그러면 히스토리는 아래와 같이 된다.

  • reset 명령은 이 세 트리를 간단하고 예측 가능한 방법으로 조작한다.
  • 트리를 조작하는 동작은 세 단계 이하로 이루어진다.

3.1 1단계: HEAD 이동

  • reset 명령이 하는 첫 번째 일은 HEAD 브랜치를 이동시킨다.
  • checkout 명령처럼 HEAD가 가리키는 브랜치를 바꾸지는 않는다.
  • HEAD는 계속 현재 브랜치를 가리키고 있고, 현재 브랜치가 가리키는 커밋을 바꾼다.
  • HEAD가 master 브랜치를 가리키고 있다면(즉 master 브랜치를 Checkout 하고 작업하고 있다면) git reset 9e5e6a4 명령은 master 브랜치가 9e5e6a4를 가리키게 한다.

  • reset 명령에 커밋을 넘기고 실행하면 언제나 이런 작업을 수행한다.
  • reset --soft 옵션을 사용하면 딱 여기까지 진행하고 동작을 멈춘다.
  • 이제 위의 다이어그램을 보고 어떤 일이 일어난 것인지 생각해보자.
  • reset 명령은 가장 최근의 git commit 명령을 되돌린다.
  • git commit 명령을 실행하면 Git은 새로운 커밋을 생성하고 HEAD가 가리키는 브랜치가 새로운 커밋을 가리키도록 업데이트한다.
  • reset 명령 뒤에 HEAD~ (HEAD의 부모 커밋)를 주면 Index나 워킹 디렉토리는 그대로 놔두고 브랜치가 가리키는 커밋만 이전으로 되돌린다.
  • Index를 업데이트한 다음에 git commit 명령를 실행하면 git commit --amend 명령의 결과와 같아진다.

3.2 2단계: Index 업데이트 (–mixed)

  • 여기서 git status 명령을 실행하면 Index와 reset 명령으로 이동시킨 HEAD의 다른 점이 녹색으로 출력된다.
  • reset 명령은 여기서 한 발짝 더 나아가 Index를 현재 HEAD가 가리키는 스냅샷으로 업데이트할 수 있다.

  • --mixed 옵션을 주고 실행하면 reset 명령은 여기까지 하고 멈춘다.
  • reset 명령을 실행할 때 아무 옵션도 주지 않으면 기본적으로 --mixed 옵션으로 동작한다(예제와 같이 git reset HEAD~ 처럼 명령을 실행하는 경우).
  • 위의 다이어그램을 보고 어떤 일이 일어날지 한 번 더 생각해보자.
  • 가리키는 대상을 가장 최근의 커밋 으로 되돌리는 것은 같다.
  • 그러고 나서 Staging Area 를 비우기까지 한다.
  • git commit 명령도 되돌리고 git add 명령까지 되돌리는 것이다.

3.3 3 단계: 워킹 디렉토리 업데이트 (–hard)

  • reset 명령은 세 번째로 워킹 디렉토리까지 업데이트한다.
  • --hard 옵션을 사용하면 reset 명령은 이 단계까지 수행한다.

  • 이 과정은 어떻게 동작하는지 가늠해보자.
  • reset 명령을 통해 git addgit commit 명령으로 생성한 마지막 커밋을 되돌린다.
  • 그리고 워킹 디렉토리의 내용까지도 되돌린다.
  • --hard 옵션은 매우 매우 중요하다.
  • reset 명령을 위험하게 만드는 유일한 옵션이다.
  • Git에는 데이터를 실제로 삭제하는 방법이 별로 없다.
  • 이 삭제하는 방법은 그 중 하나다.
  • reset 명령을 어떻게 사용하더라도 간단히 결과를 되돌릴 수 있다.
  • 하지만 --hard 옵션은 되돌리는 것이 불가능하다.
  • 이 옵션을 사용하면 워킹 디렉토리의 파일까지 강제로 덮어쓴다.
  • 이 예제는 파일의 v3버전을 아직 Git이 커밋으로 보관하고 있기 때문에 reflog 를 이용해서 다시 복원할 수 있다.
  • 만약 커밋한 적 없다면 Git이 덮어쓴 데이터는 복원할 수 없다.

3.4 복습

  • reset 명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춘다.
  1. HEAD가 가리키는 브랜치를 옮긴다. (--soft 옵션이 붙으면 여기까지)
  2. Index를 HEAD가 가리키는 상태로 만든다. (--hard 옵션이 붙지 않았으면 여기까지)
  3. 워킹 디렉토리를 Index의 상태로 만든다.

4. 경로를 주고 Reset 하기

  • 지금까지 reset 명령을 실행하는 기본 형태와 사용 방법을 살펴봤다.
  • reset 명령을 실행할 때 경로를 지정하면 1단계를 건너뛰고 정해진 경로의 파일에만 나머지 reset 단계를 적용한다.
  • HEAD는 포인터인데 경로에 따라 파일별로 기준이 되는 커밋을 부분적으로 적용하는 건 불가능하다.
  • 하지만, Index나 워킹 디렉토리는 일부분만 갱신할 수 있다. 따라서 2, 3단계는 가능하다.

  • 예를 들어 git reset file.txt 명령을 실행한다고 가정하자.
  • 이 형식은(커밋의 해시 값이나 브랜치도 표기하지 않고 --soft--hard 도 표기하지 않은) git reset --mixed HEAD file.txt 를 짧게 쓴 것이다.
  1. HEAD의 브랜치를 옮긴다. (건너뜀)
  2. Index를 HEAD가 가리키는 상태로 만든다. (여기서 멈춤)
  • 본질적으로는 file.txt 파일을 HEAD에서 Index로 복사하는 것뿐이다.

  • 이 명령은 해당 파일을 Unstaged 상태로 만든다.
  • 이 명령의 다이어그램과 git add 명령을 비교해보면 정확히 반대인 것을 알 수 있다.

  • 이것이 git status 명령에서 이 명령을 보여주는 이유다.
  • 이 명령으로 파일을 Unstaged 상태로 만들 수 있다.
  • 특정 커밋을 명시하면
  • Git은 “HEAD에서 파일을 가져오는” 것이 아니라 그 커밋에서 파일을 가져온다.
  • git reset eb43bf file.txt 명령과 같이 실행한다.

  • 이 명령을 실행한 것과 같은 결과를 만들려면 워킹 디렉토리의 파일을 v1으로 되돌리고 git add 명령으로 Index를 v1으로 만들고 나서 다시 워킹 디렉토리를 v3로 되돌려야 한다(결과만 같다는 얘기다).
  • 이 상태에서 git commit 명령을 실행하면 v1으로 되돌린 파일 내용을 기록한다. 워킹 디렉토리를 사용하지 않았다.

  • git add 명령처럼 reset 명령도 Hunk 단위로 사용할 수 있다.
  • --patch 옵션을 사용해서 Staging Area에서 Hunk 단위로 Unstaged 상태로 만들 수 있다.
  • 이렇게 선택적으로 Unstaged 상태로 만들거나 내리거나 이전 버전으로 복원시킬 수 있다.

5. 합치기(Squash)

  • 여러 커밋을 커밋 하나로 합치는 재밌는 도구를 알아보자.
  • “oops.” 나 “WIP”, “forgot this file” 같은 깃털같이 가벼운 커밋들이 있다고 해보자.
  • 이럴 때는 reset 명령으로 커밋들을 하나로 합쳐서 남들에게 똑똑한 척할 수 있다. (커밋합치기를 하는 명령어가 따로 있지만, 여기서는 reset 명령을 쓰는 것이 더 간단할 때도 있다는 것을 보여준다.)
  • 이런 프로젝트가 있다고 생각해보자.
  • 첫 번째 커밋은 파일 하나를 추가했고, 두 번째 커밋은 기존 파일을 수정하고 새로운 파일도 추가했다.
  • 세 번째 커밋은 첫 번째 파일을 다시 수정했다.
  • 두 번째 커밋은 아직 작업 중인 커밋으로 이 커밋을 세 번째 커밋과 합치고 싶은 상황이다.

  • git reset --soft HEAD~2 명령을 실행하여 HEAD 포인터를 이전 커밋으로 되돌릴 수 있다. (히스토리에서 그대로 유지할 처음 커밋 말이다).

  • 이 상황에서 git commit 명령을 실행한다.

  • 이제 사람들에게 공개할만한 히스토리가 만들어졌다.
  • file-a.txt 파일이 있는 v1 커밋이 하나 그대로 있고, 두 번째 커밋에는 v3버전의 file-a.txt 파일과 새로 추가된 file-b.txt 파일이 있다.
  • v2 버전은 더는 히스토리에 없다.

6. Checkout

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

6.1 경로 없음

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

  • 첫 번째로 reset --hard 명령과는 달리 checkout 명령은 워킹 디렉토리를 안전하게 다룬다.
  • 저장하지 않은 것이 있는지 확인해서 날려버리지 않는다는 것을 보장한다.
  • 사실 보기보다 좀 더 똑똑하게 동작한다.
  • 워킹 디렉토리에서 Merge 작업을 한번 시도해보고 변경하지 않은 파일만 업데이트한다.
  • 반면 reset --hard 명령은 확인하지 않고 단순히 모든 것을 바꿔버린다.

  • 두 번째 중요한 차이점은 어떻게 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 자체를 옮겼다.

6.2 경로 있음

  • checkout 명령을 실행할 때 파일 경로를 줄 수도 있다.
  • reset 명령과 비슷하게 HEAD는 움직이지 않는다.
  • 동작은 git reset [branch] file 명령과 비슷하다.
  • Index의 내용이 해당 커밋 버전으로 변경될 뿐만 아니라 워킹 디렉토리의 파일도 해당 커밋 버전으로 변경된다.
  • 완전히 git reset --hard [branch] file 명령의 동작이랑 같다.
  • 워킹 디렉토리가 안전하지도 않고 HEAD도 움직이지 않는다.
  • git reset 이나 git add 명령처럼 checkout 명령도 --patch 옵션을 사용해서 Hunk 단위로 되돌릴 수 있다.

7. 요약

  • 명령이 HEAD가 가리키는 브랜치를 움직인다면 “HEAD” 열에 “REF” 라고 적혀 있고 HEAD 자체가 움직인다면 “HEAD” 라고 적혀 있다.
  • WD Safe? 열을 꼭 보자.
  • 여기에 NO라고 적혀 있다면 워킹 디렉토리에 저장하지 않은 내용이 안전하지 않기 때문에 해당 명령을 실행하기 전에 한 번쯤 더 생각해보아야 한다.