[첫화면으로]Git/ResetDemystified번역

마지막으로 [b]

원문: http://git-scm.com/blog/2011/07/11/reset.html

Git Reset Demystified - Git Reset 확실히 배우기

reset 명령은 내가 Pro Git 책에서 깊이있게 다루지 않은 주제 중 하나이다. 그 이유는 솔직히 말해서 나 역시도 내가 reset을 필요로 했던 몇 가지 용례 이상으로는 잘 이해하지 못했기 때문이다. 이 명령어가 무엇을 하는지는 알고 있었지만, 어떻게 동작하도록 디자인되었는지는 제대로 알지 못하고 있었다.

그 이후 나는 이 명령어에 좀 더 친숙해졌는데, 여기에는 [Mark Dominus의 글]의 도움이 컸다. 그 글은 이해하기 매우 어려웠던 매뉴얼 페이지의 내용을 쉽게 풀어 설명했다. 이 설명을 읽고 나니 내 자신이 reset을 사용하는 데 훨씬 편해진 것을 느꼈고 다른 사람들도 나처럼 편해지도록 도울 수 있게 되었다.

이 글은 독자가 Git의 브랜치가 동작하는 방식에 대해 기본적인 이해는 하고 있다고 가정한다. 만일 여러분이 HEAD나 Index가 무엇인지 기초적인 정도라도 알고 있지 않다면, 이 글을 읽기 전에 Pro Git 책의 2장과 3장을 먼저 읽는 것이 나을 것이다.

Git의 세 가지 트리

Upload:git_reset_01_trees.png

내가 resetcheckout에 대해 생각할 때, 나는 Git의 근본 개념이 세 가지 서로 다른 트리의 내용을 관리하는 도구라고 바라본다. 여기서 '트리'라는 것은 특별한 자료 구조를 의미하는 게 아니라 "파일들의 모음"을 의미한다. (Git 개발자들 중 일부는 이 지점에서 내게 화를 낼지 모른다. 왜냐하면 인덱스는 트리와 완전히 똑같지는 않기 때문이다. 여기서는 편의상 이렇게 말하는 게 쉬워서 그러는 것이니 양해하기 바란다.)

Git은 시스템 관리자처럼 행동하며 일반적인 동작을 하는 동안 세 가지 트리들을 조작한다. 세 트리 각각에 대해서는 책에서 다루고 있지만, 여기서 다시 살펴보자.

트리 역할
HEAD 마지막 커밋의 스냅샷, 다음 번 부모
Index 다음 커밋 스냅샷으로 제안된 내용
작업 디렉토리 샌드박스

HEAD - 마지막 커밋의 스냅샷, 다음 번 부모

Git의 HEAD는 현재 브랜치의 레퍼런스를 가리키는 포인터이며, 이 브랜치 레퍼런스는 다시, 여러분이 만들었거나 작업 디렉토리에 체크아웃하여 가져온 가장 마지막 커밋을 가리키는 포인터이다. 또한 HEAD는 여러분이 다음 번 커밋을 할 때 그 커밋의 부모 커밋이 될 것이다. 일반적으로는 HEAD를 여러분의 마지막 커밋에 대한 스냅샷이라고 생각하면 간단하다.

사실, HEAD의 스냅샷이 어떻게 생겼는지 살펴보는 것은 매우 쉽다. 아래의 예는 HEAD 스냅샷에서 실제 디렉토리 목록과 각 파일의 SHA 체크썸을 뽑아내는 예문이다.

$ cat .git/HEAD 
ref: refs/heads/master

$ cat .git/refs/heads/master 
e9a570524b63d2a2b3a7c3325acf5b89bbeb131e

$ git cat-file -p e9a570524b63d2a2b3a7c3325acf5b89bbeb131e
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r cfda3bf379e4f8dba8717dee55aab78aef7f4daf
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib

Index - 다음 커밋 스냅샷으로 제안된 내용

인덱스는 여러분의 다음 커밋으로 제안된 것이다. Git은 인덱스를 여러분의 작업 디렉토리에 마지막으로 체크아웃된 파일들의 목록과 그 파일들이 처음 체크아웃되었을 때의 내용들로 구성한다. 이것은 기술적으로 말해서 트리 구조는 아니며 평탄화된 목록이지만, 우리가 이해하는 데에는 별 차이 없다. 여러분이 git commit을 실행하면, 이 명령은 기본적으로 여러분의 인덱스만 살펴보고 작업 디렉토리는 전혀 보지 않는다. 따라서, 인덱스를 여러분의 다음 커밋 스냅샷이라고 생각하는 게 제일 간편하다.

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

작업 디렉토리 - 샌드박스, 낙서판

마지막으로, 작업 디렉토리가 있다. 여기는 파일의 내용들이 파일 시스템 위에 실제 파일에 담겨 있어서 여러분이 쉽게 편집할 수 있다. 작업 디렉토리는 여러분의 낙서판이고, 파일의 내용을 수정할 때 사용된다.

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

작업 흐름

이렇게, Git은 이 세 가지 트리의 내용이나 파일들의 내용을 조작함으로써 여러분의 프로젝트의 스냅샷을 연속적으로 기록하게 된다.

Upload:git_reset_02_workflow.png

이 과정을 시각화해보자. 파일이 하나 있는 디렉토리에 새로 들어갔다고 하자. 이 파일의 상태를 V1이라고 부르고 파란색으로 나타낼 것이다. 이제 우리는 git init을 실행하고, 그러면 HEAD 레퍼런스가 아직 생성되지 않은 브랜치를 가리키는(다시 말해서, 아무것도 가리키지 않는) 상태로 Git 저장소가 만들어진다.

Upload:git_reset_03_ex2.png

이 시점에서는, 단지 작업 디렉토리에만 내용물이 있다.

이제 우리는 이 파일을 커밋하려고 한다. git add 명령을 사용하여 작업 디렉토리의 내용을 가져와서 인덱스를 채운다.

Upload:git_reset_04_ex3.png

그리고 git commit을 하여 현재 인덱스에서 보이는 내용을 가져와 영구히 스냅샷으로 저장하고 어떤 커밋이 이걸 가리키게 한다. 그리고 HEAD는 이 커밋을 가리키도록 갱신된다.

Upload:git_reset_05_ex4.png

이 시점에서, 세 트리의 내용은 동일하다. 지금 git status를 실행하면, 아무런 차이점을 볼 수 없을 것이다.

이제 파일을 수정하고 커밋하고자 한다. 아까와 동일한 과정을 거칠 것이다. 먼저 작업 디렉토리에 있는 파일의 내용을 수정한다.

Upload:git_reset_06_ex5.png

이제 git status를 실행하면 이 파일이 "changed but not updated"라고 빨갛게 표시되는 것을 볼 수 있다. 이 항목이 인덱스와 작업 디렉토리 간에 서로 다른 상태이기 때문이다. 다음으로 git add를 실행해서 이 파일을 인덱스로 올린다.

Upload:git_reset_07_ex6.png

이 시점에 git status를 실행하면 이 파일이 "Changes to be Commited" 항목 아래 녹색으로 나온다. 이것은 인덱스와 HEAD의 내용이 다르기 때문이다. 다시 말해서 다음 번 커밋할 내용이 가장 최근에 커밋한 내용과 달라져 있다. 이 달라진 내용이 '이번에 커밋될' 내용이다. 마지막으로 git commit을 실행해서 커밋을 완료한다.

Upload:git_reset_08_ex7.png

이제 git status는 아무것도 출력하지 않는다. 세 개의 트리의 내용이 동일하기 때문이다.

브랜치를 전환하거나 복제하는 것도 유사한 과정을 거친다. 여러분이 어떤 브랜치를 체크아웃하면, HEAD가 새로운 커밋을 가리키도록 변경되고, 그 커밋의 스냅샷으로 인덱스를 채우고, 인덱스에 있는 파일들의 내용을 작업 디렉토리로 꺼내온다.

Reset의 역할

이런 맥락에서 보면 reset의 동작이 좀 더 이해하기 쉬워진다. 이 명령은 단순하고 예상할 수 있는 방식으로 세 개의 트리를 조작한다. 이 명령은 기본적인 동작을 최대 세 단계까지 수행한다.

1단계: HEAD를 이동함 killing me --soft ly

reset이 제일 먼저 하는 일은 HEAD가 가리키는 것을 움직이는 것이다. HEAD가 다른 브랜치를 가리키도록 이동시키는 checkout과는 달리, reset은 HEAD가 가리키는 레퍼런스 자체의 SHA 값을 변경한다. 이 말은, 만일 HEAD가 'master' 브랜치를 가리키고 있는 상태라면, git reset 9e5e64a 명령이 제일 먼저 하는 일은 'master'가 9e5e64a 커밋을 가리키도록 변경하는 것이다.

Upload:git_reset_09_reset-soft.png

커밋을 인자로 준 reset은 어떤 옵션으로 실행해도 제일 먼저 방금의 과정을 수행한다. 만일 --soft 옵션을 주었다면, reset은 이 과정만 수행하고 중단한다.

위의 다이어그램을 다시 보면서 무엇이 이뤄졌는지 살펴보자. 이 과정은 근본적으로 여러분이 만든 마지막 커밋을 취소시켰다. 여러분이 git commit을 하면 Git은 새로운 커밋을 만들고 HEAD가 가리키고 있는 브랜치가 그 커밋을 가리키도록 움직인다. 여러분이 HEAD~(HEAD의 부모 커밋)로 reset을 시키면, 인덱스나 작업 디렉토리는 건드리지 않고 브랜치만 되돌린다. 이제 여러분은 다른 일을 더 한 다음 다시 commit할 수 있고, 이건 기본적으로 git commit --amend로 했을 일을 수행한 것이다.

2단계: 인덱스를 갱신함 having --mixed feelings

지금 시점에서 git status를 하면 인덱스의 내용과 새 HEAD의 내용의 차이가 녹색으로 나오는 것에 유의하라.

reset이 다음으로 할 일은 HEAD가 현재 가리키는 내용을 가지고 인덱스를 갱신해서 둘이 동일해지도록 하는 것이다.

Upload:git_reset_10_reset-mixed.png

--mixed 옵션을 주면, reset은 여기까지 하고 멈춘다. 이것이 디폴트이므로 옵션을 전혀 주지 않을 경우에도 여기에서 멈춘다.

이 다이어그램을 보면서 무엇이 이루어졌는지 살펴보자. 마지막 commit을 취소했을 뿐 아니라, 인덱스에 있던 것들도 다 내렸다(unstage). 여러분이 git addgit commit을 하기 이전 상태로 되돌아갔다.

3단계: 작업 디렉토리를 갱신함 math is --hard, let's go shopping

reset이 세 번째로 할 일은 작업 디렉토리가 인덱스와 동일해지도록 하는 것이다. --hard 옵션을 주면 이 단계까지 진행한다.

Upload:git_reset_11_reset-hard.png

마지막으로 잠시 이 다이어그램을 보면서 무슨 일이 일어났는지 생각해보자. 마지막 커밋이 취소되고, git add로 올린 내용과, 여러분이 작업 디렉토리에서 수행한 작업 모두가 취소되었다.

여기서 유의해야 할 중요한 점은, 이것이 reset 명령이 위험한(다시 말하면 작업 디렉토리가 안전하지 않은) 유일한 경우라는 점이다. 다른 형태로 reset을 수행한 것들은 매우 쉽게 취소할 수 있으나, --hard 옵션은 그렇지 않다. 이것은 작업 디렉토리의 파일들을 (확인도 하지 않고) 덮어써 버리기 때문이다. 지금처럼 특별한 경우에는 우리 파일의 v3 버전이 Git DB 내의 커밋 안에 들어 있고, reflog를 뒤져서 되살릴 수 있다. 그러나 커밋을 하지 않은 상태라 해도 Git은 그것을 덮어써버렸을 것이다.

개괄

기본적으로는 이게 다이다. reset 명령은 이 세 가지 트리를 특정한 순서로 덮어쓰게 되며 여러분이 지시한 지점에서 멈춘다.

이것 말고도 --merge--keep 옵션이 있긴 하지만, 내용을 간단히 하고 싶으니 이것들은 다른 글에서 다루도록 하겠다.

두둥. 여러분은 이제 reset을 통달하였다.

경로를 인자로 준 reset

음, 거짓말이었다. 사실 이게 전부가 아니다. 경로를 지정할 경우, reset은 첫 단계를 건너뛰고 나머지 단계들을 수행하는데 특정 파일 또는 몇몇 파일들에 대해서만 수행한다. 사실 이게 말이 되는 것이, 첫번째 단계는 포인터를 이동시켜 다른 커밋을 가리키게 하는 것인데, 커밋의 일부만 가리키도록 할 수는 없으므로 그냥 이 단계를 아예 수행하지 않는 것이다. 그러나, reset을 써서 인덱스나 작업 디렉토리의 일부분을 기존 커밋의 내용과 동일하게 갱신하는 것은 가능하다.

이제 우리가 git reset file.txt를 실행했다고 가정하자. 커밋 SHA나 브랜치 이름을 명시하지도 않고 reset 옵션을 명시하지도 않았기 때문에, 이것은 git reset --mixed HEAD file.txt를 짧게 쓴 것이다. 그리고 이것은 다음의 일을 할 것이다:

따라서 이것은 결과적으로 HEAD 안에서 file.txt의 내용을 가져와서 인덱스에 넣게 된다.

Upload:git_reset_12_reset-path1.png

그러면 실용적인 관점에서 무엇을 할 수 있는가? 음, 이것은 파일을 인덱스에서 내린다. 이 다이어그램을 git add의 다이어그램과 비교해보면, 간단히 정반대라는 것을 알 수 있다. git status의 출력에서, 파일을 인덱스에서 내리려면 이 명령을 수행하라고 제안하는데 그 이유가 이것이다.

Upload:git_reset_13_reset-path2.png

git reset eb43bf file.txt와 같은 식으로 파일을 가져올 특정한 커밋을 명시하여 HEAD가 아닌 다른 곳에서 파일을 가져와 인덱스에 넣을 수 있다.

Upload:git_reset_14_reset-path3.png

이건 뭘 의미하는가? 기능적으로 봤을 때 이것은 우리가 파일의 내용을 v1 상태로 되돌리고, git add를 수행하여 인덱스에 올린 후, 파일을 다시 v3로 복구시킨 것과 같다. 만일 git commit을 수행한다면, 파일을 v1으로 되돌린 변경 내역이 기록될 것이다, 실제로 우리 작업 디렉토리에서는 아무 일도 안 했는데 말이다.

또 재미있는 것은 git add --patch와 같이 reset--patch 옵션을 주어서 내용을 조각조각 단위(hunk-by-hunk basis)로 인덱스에서 내릴 수 있다는 점이다. 따라서 내용을 선택적으로 인덱스에서 내리거나 복구할 수 있다.

재미있는 예제

"재미있는"이라는 표현은 대충 쓴 거지만, 재미없게 들린다면 한 잔 하면서 읽기를 바란다. 새로 발견한 능력을 사용하여 흥미로운 일 - 커밋들을 합치기를 해보자.

다음과 같은 변경 내역이 있고, 이제 push를 하려고 하는데, 그 전에 마지막 N개의 커밋을 합쳐서 하나의 놀라운 커밋으로 만들어 여러분이 진짜 똑똑한 사람인 것처럼 보이게 하려고 한다. ("아차", "진행 중", "이 파일을 깜박했네" 이런 메시지들이 달린 다수의 커밋들과 비교하면 말이다) 이 때 reset을 사용하여 빠르고 쉽게 할 수 있다(git rebase -i와 비교해서)

그러니 조금만 더 복잡한 예를 들어보자. 여러분이 어떤 프로젝트를 진행하고 있는데, 첫번째 커밋에는 파일 하나가 있고, 두번째 커밋에서는 파일 하나를 추가하고 첫번째 파일을 수정하였다. 그리고 세번째 커밋에서는 첫번째 파일을 다시 수정하였다. 두번째 커밋은 진행 도중의 상태였고 여러분은 이것을 다른 커밋에 합치려고 한다.

Upload:git_reset_15_squash-r1.png

git reset --soft HEAD~2를 실행하여 HEAD 브랜치를 이전 커밋(여러분이 남겨두려고 했던 첫번째 커밋)을 가리키도록 움직인다:

Upload:git_reset_16_squash-r2.png

그리고는 간단히 git commit을 다시 한다:

Upload:git_reset_17_squash-r3.png

이제 여러분이 push하려고 했던 그 변경 내역을 볼 수 있다. 첫 커밋에는 하나의 파일이 있고, 두번째 커밋에는 새로운 파일 하나가 추가되고 첫번째 파일은 최종 상태로 수정이 되어있다.

Check it out

끝으로, 여러분 중 일부는 checkoutreset의 차이가 무엇인지 궁금해할지 모르겠다. 음, reset과 같이, checkout도 세 가지 트리를 조작하는데 명령을 내릴 때 파일 경로를 주었는지 여부에 따라 조금 달라진다. 따라서 각각의 예를 구분해서 살펴보자.

git checkout [branch]

git checkout [branch]git reset --hard [branch]와 매우 유사하게 세 가지 트리를 [branch]처럼 보이도록 갱신한다. 그러나 두 가지 중요한 차이점이 있다.

첫째로, reset --hard와 달리, checkout은 이런 형태로 실행할 경우 작업 디렉토리가 안전하다. 이 명령은 변경된 파일들을 날려버리지 않도록 체크한다. 사실 이것은 감지하기 어려운 차이인데, 왜냐하면 체크아웃하는 내용과 이미 있는 내용을 평이하게 merge할 수 있다면 머지를 해 버리고 작업 디렉토리를 갱신해 버리기 때문이다. 이런 경우 reset --hard였다면 아무런 검사 없이 모든 것을 대체해버렸을 것이다.

두번째 중요한 차이점은 HEAD를 갱신하는 방식이다. reset은 HEAD가 가리키는 브랜치를 이동시키지만 checkout은 HEAD 자체를 이동시켜 다른 브랜치를 가리키도록 만든다.

예를 들어, 서로 다른 커밋을 가리키고 있는 두 브랜치 'master'와 'develop'가 있고, 현재 'develop' 브랜치 위에 있다고 해보자.(따라서 HEAD도 이 브랜치를 가리키고 있다) 여기서 git reset master를 실행하면, 'develop' 자체가 이제는 'master'가 가리키는 커밋을 동일하게 가리키게 된다.

반면에, git checkout master를 실행했다면, 'develop'는 이동하지 않고, HEAD 자체가 이동한다. HEAD는 이제 'master'를 가리키게 된다. 따라서 두 경우 다 HEAD가 커밋A를 가리키도록 만들지만 그 과정은 매우 다르다. reset은 HEAD가 가리키는 브랜치를 이동시키고, checkout은 HEAD 자체가 다른 브랜치를 가리키도록 이동시킨다.

Upload:git_reset_18_reset-checkout.png

git checkout [branch] file

checkout을 실행하는 또 하나의 방법은 파일 경로를 주어 실행하는 것이다. 이것은 reset과 같이 HEAD는 움직이지 않으며, git reset [branch] file처럼 그 커밋에 있는 파일의 내용으로 인덱스를 갱신한다. 그리고 여기에 더불어서 작업 디렉토리의 파일까지 덮어쓴다. 이것을 git reset --hard [branch] file와 같다고 생각하라 - 정확히 같은 일을 할 것이며, 작업 디렉토리가 안전하지 않고, HEAD를 이동시키지 않는다는 점까지 동일하다. 유일한 차이점은 reset에 파일명을 인자로 줄 경우 --hard 옵션을 받아들이지 않기 때문에 실제로 이렇게는 실행할 수 없다는 점이다.

또한, git reset이나 git add처럼, checkout 역시 --patch 옵션을 줄 수 있으며, 파일의 내용을 조각 단위로 선택해서 되돌릴 수 있다.

Cheaters Gonna Cheat

바라건대 이제는 여러분도 reset을 이해하고 더 편하게 느낄 수 있으면 좋겠으나, 아마도 여전히 checkout과 정확히 어떻게 다른지 잘 모르겠다거나 여러 가지 형태로 실행할 때의 규칙을 기억하지 못할 수 있다.

따라서 여러분들을 돕기 위해서, 내가 매우 안 좋아하는 것을 만들었는데, 일종의 표이다. 하지만 여러분이 이 글을 다 읽었다면 이 표가 유용한 컨닝 페이퍼가 될 수 있을 것이다. 이 표에는 resetcheckout의 분류와 각 경우에 세 트리 중 어느 것이 갱신되는지가 나와 있다.

'WD Safe?' 열에 각별히 주의하라. 이 항목이 빨간 색이라면, 그 명령을 실행하기 전에 정말 잘 생각하도록 하라.

Upload:git_reset_checkout_cheat.png

Comments

이름:  
Homepage:
내용:
 

컴퓨터분류

마지막 편집일: 2014-7-10 10:43 am (변경사항 [d])
4560 hits | Permalink | 변경내역 보기 [h] | 페이지 소스 보기