-
1. 시작하기
-
2. Git의 기초
- 2.1 Git 저장소 만들기
- 2.2 수정하고 저장소에 저장하기
- 2.3 커밋 히스토리 조회하기
- 2.4 되돌리기
- 2.5 리모트 저장소
- 2.6 태그
- 2.7 Git Alias
- 2.8 요약
-
3. Git 브랜치
-
4. Git 서버
- 4.1 프로토콜
- 4.2 서버에 Git 설치하기
- 4.3 SSH 공개키 만들기
- 4.4 서버 설정하기
- 4.5 Git 데몬
- 4.6 스마트 HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 또 다른 선택지, 호스팅
- 4.10 요약
-
5. 분산 환경에서의 Git
- 5.1 분산 환경에서의 워크플로
- 5.2 프로젝트에 기여하기
- 5.3 프로젝트 관리하기
- 5.4 요약
-
6. GitHub
- 6.1 계정 만들고 설정하기
- 6.2 GitHub 프로젝트에 기여하기
- 6.3 GitHub 프로젝트 관리하기
- 6.4 Organization 관리하기
- 6.5 GitHub 스크립팅
- 6.6 요약
-
7. Git 도구
- 7.1 리비전 조회하기
- 7.2 대화형 명령
- 7.3 Stashing과 Cleaning
- 7.4 내 작업에 서명하기
- 7.5 검색
- 7.6 히스토리 단장하기
- 7.7 Reset 명확히 알고 가기
- 7.8 고급 Merge
- 7.9 Rerere
- 7.10 Git으로 버그 찾기
- 7.11 서브모듈
- 7.12 Bundle
- 7.13 Replace
- 7.14 Credential 저장소
- 7.15 요약
-
8. Git맞춤
- 8.1 Git 설정하기
- 8.2 Git Attributes
- 8.3 Git Hooks
- 8.4 정책 구현하기
- 8.5 요약
-
9. Git과 여타 버전 관리 시스템
- 9.1 Git: 범용 Client
- 9.2 Git으로 옮기기
- 9.3 요약
-
10. Git의 내부
- 10.1 Plumbing 명령과 Porcelain 명령
- 10.2 Git 개체
- 10.3 Git Refs
- 10.4 Packfile
- 10.5 Refspec
- 10.6 데이터 전송 프로토콜
- 10.7 운영 및 데이터 복구
- 10.8 환경변수
- 10.9 요약
-
A1. 부록 A: 다양한 환경에서 Git 사용하기
- A1.1 GUI
- A1.2 Visual Studio
- A1.3 Eclipse
- A1.4 Bash
- A1.5 Zsh
- A1.6 Git in Powershell
- A1.7 요약
-
A2. 부록 B: 애플리케이션에 Git 넣기
-
A3. 부록 C: Git 명령어
- A3.1 설치와 설정
- A3.2 프로젝트 가져오기와 생성하기
- A3.3 스냅샷 다루기
- A3.4 Branch와 Merge
- A3.5 공유하고 업데이트하기
- A3.6 보기와 비교
- A3.7 Debugging
- A3.8 Patch 하기
- A3.9 Email
- A3.10 다른 버전 관리 시스템
- A3.11 관리
- A3.12 Plumbing 명령어
7.7 Git 도구 - Reset 명확히 알고 가기
Reset 명확히 알고 가기
Git의 다른 특별한 도구를 더 살펴보기 보기 전에 reset
과 checkout
에 대해 이야기를 해보자.
이 두 명령은 Git을 처음 사용하는 사람을 가장 헷갈리게 하는 부분이다.
제대로 이해하고 사용할 수 없을 것으로 보일 정도로 많은 기능을 지녔다.
이해하기 쉽게 간단한 비유를 들어 설명해보자.
세 개의 트리
Git을 서로 다른 세 트리를 관리하는 컨텐츠 관리자로 생각하면 reset
과 checkout
을 좀 더 쉽게 이해할 수 있다.
여기서 “트리” 란 실제로는 “파일의 묶음” 이다. 자료구조의 트리가 아니다
세 트리 중 Index는 트리도 아니지만, 이해를 쉽게 하려고 일단 트리라고 한다.
Git은 일반적으로 세 가지 트리를 관리하는 시스템이다.
트리 | 역할 |
---|---|
HEAD |
마지막 커밋 스냅샷, 다음 커밋의 부모 커밋 |
Index |
다음에 커밋할 스냅샷 |
워킹 디렉토리 |
샌드박스 |
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이 실제로 무슨 일을 하는지 볼 때 유용하다.
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는 평평한 구조로 구현되어 있다. 여기에서는 쉽게 이해할 수 있도록 그냥 트리라고 설명한다.
워킹 디렉토리
마지막으로 워킹 디렉토리를 살펴보자.
위의 두 트리는 파일과 그 내용을 효율적인 형태로 .git
디렉토리에 저장한다. 하지만, 사람이 알아보기 어렵다.
워킹 디렉토리는 실제 파일로 존재한다. 바로 눈에 보이기 때문에 사용자가 편집하기 수월하다.
워킹 디렉토리는 샌드박스로 생각하자. 커밋하기 전에는 Index(Staging Area)에 올려놓고 얼마든지 변경할 수 있다.
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
워크플로
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의 내용을 워킹 디렉토리로 복사한다.
Reset 의 역할
위의 트리 세 개를 이해하면 reset
명령이 어떻게 동작하는지 쉽게 알 수 있다.
예로 들어 file.txt
파일 하나를 수정하고 커밋한다. 이것을 세 번 반복한다.
그러면 히스토리는 아래와 같이 된다.
자 이제 reset
명령이 정확히 어떤 일을 하는지 낱낱이 파헤쳐보자.
reset
명령은 이 세 트리를 간단하고 예측 가능한 방법으로 조작한다.
트리를 조작하는 동작은 세 단계 이하로 이루어진다.
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
명령의 결과와 같아진다(마지막 커밋을 수정하기를 참조).
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 단계: 워킹 디렉토리 업데이트 (--hard)
reset
명령은 세 번째로 워킹 디렉토리까지 업데이트한다.
--hard
옵션을 사용하면 reset
명령은 이 단계까지 수행한다.
이 과정은 어떻게 동작하는지 가늠해보자.
reset
명령을 통해 git add
와 git commit
명령으로 생성한 마지막 커밋을 되돌린다. 그리고 워킹 디렉토리의 내용까지도 되돌린다.
이 --hard
옵션은 매우 매우 중요하다. reset
명령을 위험하게 만드는 유일한 옵션이다. Git에는 데이터를 실제로 삭제하는 방법이 별로 없다. 이 삭제하는 방법은 그 중 하나다.
reset
명령을 어떻게 사용하더라도 간단히 결과를 되돌릴 수 있다. 하지만 --hard
옵션은 되돌리는 것이 불가능하다. 이 옵션을 사용하면 워킹 디렉토리의 파일까지 강제로 덮어쓴다.
이 예제는 파일의 v3버전을 아직 Git이 커밋으로 보관하고 있기 때문에 reflog
를 이용해서 다시 복원할 수 있다. 만약 커밋한 적 없다면 Git이 덮어쓴 데이터는 복원할 수 없다.
복습
reset
명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춘다.
-
HEAD가 가리키는 브랜치를 옮긴다. (
--soft
옵션이 붙으면 여기까지) -
Index를 HEAD가 가리키는 상태로 만든다. (
--hard
옵션이 붙지 않았으면 여기까지) -
워킹 디렉토리를 Index의 상태로 만든다.
경로를 주고 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 상태로 만들 수 있다. (더 자세한 내용은 파일 상태를 Unstage로 변경하기를 참고한다.)
특정 커밋을 명시하면 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 상태로 만들거나 내리거나 이전 버전으로 복원시킬 수 있다.
합치기(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 버전은 더는 히스토리에 없다.
Checkout
아마도 checkout
명령과 reset
명령에 어떤 차이가 있는지 궁금할 것이다.
reset
명령과 마찬가지로 checkout
명령도 위의 세 트리를 조작한다. checkout
명령도 파일 경로를 쓰느냐 안 쓰느냐에 따라 동작이 다르다.
경로 없음
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 자체를 옮겼다.
경로 있음
checkout
명령을 실행할 때 파일 경로를 줄 수도 있다. reset
명령과 비슷하게 HEAD는 움직이지 않는다.
동작은 git reset [branch] file
명령과 비슷하다. Index의 내용이 해당 커밋 버전으로 변경될 뿐만 아니라 워킹 디렉토리의 파일도 해당 커밋 버전으로 변경된다.
완전히 git reset --hard [branch] file
명령의 동작이랑 같다. 워킹 디렉토리가 안전하지도 않고 HEAD도 움직이지 않는다.
git reset
이나 git add
명령처럼 checkout
명령도 --patch
옵션을 사용해서 Hunk 단위로 되돌릴 수 있다.
요약
reset
명령이 좀 더 쉬워졌을 거라고 생각한다. 아직 checkout
명령과 정확하게 무엇이 다른지 혼란스럽거나 정확한 사용법을 다 익히지 못했을 수도 있지만 괜찮다.
아래에 어떤 명령이 어떤 트리에 영향을 주는지에 대한 요약표를 준비했다. 명령이 HEAD가 가리키는 브랜치를 움직인다면 “HEAD” 열에 “REF” 라고 적혀 있고 HEAD 자체가 움직인다면 “HEAD” 라고 적혀 있다. 'WD Safe?' 열을 꼭 보자. 여기에 *NO*라고 적혀 있다면 워킹 디렉토리에 저장하지 않은 내용이 안전하지 않기 때문에 해당 명령을 실행하기 전에 한 번쯤 더 생각해보아야 한다.
HEAD | Index | Workdir | WD Safe? | |
---|---|---|---|---|
Commit Level |
||||
|
REF |
NO |
NO |
YES |
|
REF |
YES |
NO |
YES |
|
REF |
YES |
YES |
NO |
|
HEAD |
YES |
YES |
YES |
File Level |
||||
|
NO |
YES |
NO |
YES |
|
NO |
YES |
YES |
NO |