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-file
와ls-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 add
와git commit
명령으로 생성한 마지막 커밋을 되돌린다.- 그리고 워킹 디렉토리의 내용까지도 되돌린다.
- 이
--hard
옵션은 매우 매우 중요하다. reset
명령을 위험하게 만드는 유일한 옵션이다.- Git에는 데이터를 실제로 삭제하는 방법이 별로 없다.
- 이 삭제하는 방법은 그 중 하나다.
reset
명령을 어떻게 사용하더라도 간단히 결과를 되돌릴 수 있다.- 하지만
--hard
옵션은 되돌리는 것이 불가능하다. - 이 옵션을 사용하면 워킹 디렉토리의 파일까지 강제로 덮어쓴다.
- 이 예제는 파일의 v3버전을 아직 Git이 커밋으로 보관하고 있기 때문에
reflog
를 이용해서 다시 복원할 수 있다. - 만약 커밋한 적 없다면 Git이 덮어쓴 데이터는 복원할 수 없다.
3.4 복습
reset
명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춘다.
- HEAD가 가리키는 브랜치를 옮긴다. (
--soft
옵션이 붙으면 여기까지) - Index를 HEAD가 가리키는 상태로 만든다. (
--hard
옵션이 붙지 않았으면 여기까지) - 워킹 디렉토리를 Index의 상태로 만든다.
4. 경로를 주고 Reset 하기
- 지금까지
reset
명령을 실행하는 기본 형태와 사용 방법을 살펴봤다. reset
명령을 실행할 때 경로를 지정하면 1단계를 건너뛰고 정해진 경로의 파일에만 나머지reset
단계를 적용한다.- HEAD는 포인터인데 경로에 따라 파일별로 기준이 되는 커밋을 부분적으로 적용하는 건 불가능하다.
-
하지만, Index나 워킹 디렉토리는 일부분만 갱신할 수 있다. 따라서 2, 3단계는 가능하다.
- 예를 들어
git reset file.txt
명령을 실행한다고 가정하자. - 이 형식은(커밋의 해시 값이나 브랜치도 표기하지 않고
--soft
나--hard
도 표기하지 않은)git reset --mixed HEAD file.txt
를 짧게 쓴 것이다.
- HEAD의 브랜치를 옮긴다. (건너뜀)
- 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 자체를 다른 브랜치로 옮긴다. - 예를 들어 각각 다른 커밋을 가리키는
master
와develop
브랜치가 있고 현재 워킹 디렉토리는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라고 적혀 있다면 워킹 디렉토리에 저장하지 않은 내용이 안전하지 않기 때문에 해당 명령을 실행하기 전에 한 번쯤 더 생각해보아야 한다.