-
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 명령어
10.7 Git의 내부 - 운영 및 데이터 복구
운영 및 데이터 복구
언젠가는 저장소를 손수 정리해야 할 날이 올지도 모른다. 저장소를 좀 더 알차게(Compact) 만들고, 다른 VCS에서 임포트하고 나서 그 잔재를 치운다든가, 아니면 문제가 생겨서 복구해야 할 수도 있다. 이 절은 이럴 때 필요한 것을 설명한다.
운영
Git은 때가 되면 자동으로 “auto gc” 명령을 실행한다.
이 명령이 실행되는 경우 대부분은 아무런 일도 일어나지 않는다.
Loose 개체가 너무 많거나, Packfile 자체가 너무 많으면 Git은 그제야 진짜로 git gc
명령이 일하게 한다.
gc
명령은 Garbage를 Collect 하는 명령이다. 이 명령은 Loose 개체를 모아서 Packfile에 저장하거나 작은 Packfile을 모아서 하나의 큰 Packfile에 저장한다. 아무런 커밋도 가리키지 않는 개체가 있고 오랫동안(대략 몇 달쯤) 아무도 쓰지 않는다면 개체를 삭제한다.
Git이 Garbage를 Collect 할 지 말지 자동으로 판단해서 처리하도록 아래와 같이 gc 명령을 실행할 수 있다.
$ git gc --auto
이 명령을 실행해도 보통은 아무 일도 일어나지 않는다.
Loose 개체가 7천 개가 넘거나 Packfile이 50개가 넘지 않으면 Git은 실제로 gc
작업을 실행하지 않는다.
원한다면 gc.auto
나 gc.autopacklimit
설정으로 그 숫자를 조절할 수 있다.
gc
명령이 하는 일 중 하나는 Refs를 파일 하나로 압축하는 일이다.
예를 들어 저장소에 아래와 같은 브랜치와 태그가 있다고 하자.
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
git gc
를 실행하면 refs
에 있는 파일은 사라진다.
대신 Git은 그 파일을 .git/packed-refs
파일로 압축해서 효율을 높인다.
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
이 상태에서 Refs를 수정하면 파일을 수정하는 게 아니라 refs/heads
폴더에 파일을 새로 만든다.
Git은 Refs가 가리키는 SHA-1 값을 찾을 때 먼저 refs
디렉토리에서 찾고 없으면 packed-refs
파일에서 찾는다.
그러니까 어떤 Refs가 있는데 refs
디렉토리에서 못 찾으면 packed-refs
에 있을 것이다.
마지막에 있는 ^
로 시작하는 라인을 살펴보자.
이것은 바로 윗줄의 태그가 Annotated 태그라는 것을 말해준다. 해당 커밋은 윗 태그가 가리키는 커밋이라는 뜻이다.
데이터 복구
Git을 사용하다 보면 커밋을 잃어 버리는 실수를 할 때도 있다. 보통 작업 중인 브랜치를 강제로 삭제하거나, 어떤 커밋을 브랜치 밖으로 끄집어 내버렸거나, 강제로(Hard) Reset 하면 그렇게 될 수 있다. 어쨌든 원치 않게 커밋을 잃어 버리면 어떻게 다시 찾아야 할까?
master
브랜치에서 강제로(Hard) Reset 한 경우를 예로 들어 잃어버린 커밋을 복구해보자.
먼저 연습용 저장소를 만든다.
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
master
브랜치를 예전 커밋으로 Reset 한다.
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
최근 커밋 두 개는 어떤 브랜치도 가리키지 않는다. 잃어 버렸다고 볼 수 있다. 그 두 커밋을 브랜치에 다시 포함하려면 마지막 커밋을 다시 찾아야 한다. SHA-1 값을 외웠을 리도 없고 뭔가 찾아낼 방법이 필요하다.
보통 git reflog
명령을 사용하는 게 가장 쉽다.
HEAD가 가리키는 커밋이 바뀔 때마다 Git은 남몰래 자동으로 그 커밋이 무엇인지 기록한다.
새로 커밋하거나 브랜치를 바꾸면 Reflog도 늘어난다. Git Refs 절에서 배운 git update-ref
명령으로도 Reflog를 남길 수 있다. 이런 상황을 대비할 수 있다는 점이 git update-ref
를 꼭 사용해야 하는 이유 중 하나다.
git reflog
명령만 실행하면 언제나 발자취를 돌아볼 수 있다.
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
reflog
명령으로 확인해보니 Checkout 했었던 커밋 두 개만 보여 준다. 구체적인 정보까지 보여주진 않는다.
좀 더 자세히 보려면 git log -g
명령을 사용해야 한다. 이 명령은 Reflog를 log
명령 형식으로 보여준다.
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
modified repo.rb a bit
두 번째 커밋을 잃어버린 것이니까 그 커밋을 가리키는 브랜치를 만들어 복구한다.
그 커밋(ab1afef)을 가리키는 브랜치 recover-branch
를 만든다.
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
master
브랜치가 가리키던 커밋을 recover-branch
브랜치가 가리키게 했다. 이 커밋 두 개는 다시 도달할 수 있다.
이보다 안 좋은 상황을 가정해보자. 잃어 버린 두 커밋을 Reflog에서 못 찾았다. recover-branch
를 다시 삭제하고 Reflog를 삭제하여 이 상황을 재연하자.
그러면 그 두 커밋은 다시 도달할 수 없게 된다.
$ git branch -D recover-branch
$ rm -Rf .git/logs/
Reflog 데이터는 .git/logs/
디렉토리에 있기 때문에 그 디렉토리를 지우면 Reflog도 다 지워진다.
그러면 커밋을 어떻게 복구할 수 있을까? 한 가지 방법이 있는데 git fsck
명령으로 데이터베이스의 Integrity를 검사할 수 있다.
이 명령에 --full
옵션을 주고 실행하면 길 잃은 개체를 모두 보여준다.
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
이 Dangling 커밋이 잃어버린 커밋이니까 그 SHA-1를 가리키는 브랜치를 만들어 복구할 수 있다.
개체 삭제
Git은 장점이 매우 많다. 물론 단점도 있는데 Clone 할 때 히스토리를 전부 내려받는 것이 문제가 될 때가 있을 수 있다. Git은 모든 파일의 모든 버전을 내려받는다. 사실 파일이 모두 소스코드라면 아무 문제 없다. Git은 최적화를 잘해서 데이터를 잘 압축한다. 하지만, 누군가 매우 큰 바이너리 파일을 넣어버리면 Clone 할 때마다 그 파일을 내려받는다. 다음 커밋에서 그 파일을 삭제해도 히스토리에는 그대로 남아 있기 때문에 Clone 할 때마다 포함된다.
이 문제는 Subversion이나 Perforce 저장소를 Git으로 변환할 때 큰 문제가 된다. Subversion이나 Perforce 시스템은 전체 히스토리를 내려받는 것이 아니므로 해당 파일이 여러 번 추가될 수 있다. 혹은 다른 VCS에서 Git 저장소로 임포트하려고 하는데 Git 저장소의 공간이 충분하지 않으면 너무 큰 개체는 찾아서 삭제해야 한다.
주의: 이 작업을 하다가 커밋 히스토리를 망쳐버릴 수 있다. 삭제하거나 수정할 파일이 들어 있는 커밋 이후에 추가된 커밋은 모두 재작성된다. 프로젝트를 임포트 하자마자 하는 것은 괜찮다. 아직 아무도 새 저장소를 가지고 일하지 않기 때문이다. 그게 아니면 히스토리를 Rebase 한다고 관련된 사람 모두에게 알려야 한다.
시나리오 하나를 살펴보자. 먼저 저장소에 크기가 큰 파일을 넣고 다음 커밋에서는 삭제할 것이다. 그리고 나서 그 파일을 다시 찾아 저장소에서 삭제한다. 먼저 히스토리에 크기가 큰 개체를 추가한다.
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
이런 tar 파일을 버전관리 하자고 넣을 수는 없다. 다음 커밋에서 다시 삭제한다.
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
gc
명령으로 최적화하고 나서 저장소 크기가 얼마나 되는지 확인한다.
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
count-objects
명령은 사용하는 용량이 얼마나 되는지 알려준다.
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
size-pack
항목의 숫자가 Packfile의 크기다. 단위가 킬로바이트라서 이 Packfile의 크기는 약 5MB이다.
큰 파일을 커밋하기 전에는 약 2K였다. 필요없는 파일을 지우고 커밋했지만 히스토리에서 삭제되지 않았다.
어쨌든 큰 파일이 하나 들어 있기 때문에 너무 작은 프로젝트인데도 Clone 하는 사람마다 5MB씩 필요하다.
이제 그 파일을 삭제해 보자.
먼저 파일을 찾는다.
뭐, 지금은 무슨 파일인지 이미 알고 있지만 모른다고 가정한다.
어떤 파일이 용량이 큰지 어떻게 찾아낼까?
게다가 git gc
를 실행됐으면 전부 Packfile 안에 있어서 더 찾기 어렵다. Plumbing 명령어 git verify-pack
로 파일과 그 크기 정보를 수집하고 세 번째 필드를 기준으로 그 결과를 정렬한다. 세 번째 필드가 파일 크기다.
가장 큰 파일 몇 개만 삭제할 것이기 때문에 tail 명령으로 가장 큰 파일 3개만 골라낸다.
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
마지막에 있는 개체가 5MB로 가장 크다.
이제 그 파일이 정확히 무슨 파일인지 알아내야 한다.
커밋 메시지 규칙 만들기 에서 소개했던 rev-list
명령에 --objects
옵션을 추가하면 커밋의 SHA 값과 Blob 개체의 파일이름, SHA-1 값을 보여준다.
그 결과에서 해당 Blob의 이름을 찾는다.
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
히스토리에 있는 모든 Tree 개체에서 이 파일을 삭제한다. 먼저 이 파일을 추가한 커밋을 찾는다.
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
이 파일을 히스토리에서 완전히 삭제하면 6df76
이후 커밋은 모두 재작성된다.
히스토리 단장하기 에서 배운 filter-branch
명령으로 삭제한다.
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
--index-filter
옵션은 히스토리 단장하기 에서 배운 --tree-filter
와 비슷하다. --tree-filter
는 디스크에 Checkout 해서 파일을 수정하지만 --index-filter
는 Staging Area에서 수정한다.
삭제도 rm file
명령이 아니라 git rm --cached
명령으로 삭제한다. 디스크에서 삭제하는 것이 아니라 Index에서 삭제하는 것이다.
이렇게 하는 이유는 속도가 빠르기 때문이다. Filter를 실행할 때마다 각 리비전을 디스크에 Checkout 하지 않기 때문에 이것이 울트라 캡숑 더 빠르다.
--tree-filter
로도 같은 작업을 할 수 있다. 단지 느릴 뿐이다.
그리고 git rm
명령에 --ignore-unmatch
옵션을 주면 파일이 없는 경우에 에러를 출력하지 않는다.
마지막으로 문제가 생긴 것은 7b30847
커밋부터라서 filter-branch
명령에 7b30847
커밋부터 재작성하라고 알려줘야 한다.
그렇지 않으면 첫 커밋부터 시작해서 불필요한 것까지 재작성해 버린다.
히스토리에서는 더는 그 파일을 가리키지 않는다.
하지만, Reflog나 filter-branch를 실행할 때 생기는 Refs가 남아있다. filter-branch
는 .git/refs/original
디렉토리에 실행될 때의 상태를 저장한다. 그래서 이 파일도 삭제하고 데이터베이스를 다시 압축해야 한다.
압축하기 전에 해당 개체를 가리키는 Refs는 모두 없애야 한다.
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
공간이 얼마나 절약됐는지 확인한다.
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
압축된 저장소의 크기는 8K로 내려갔다. 5MB보다 한참 작다.
하지만, size 항목은 아직 압축되지 않는 Loose 개체의 크기를 나타내는데 그 항목이 아직 크다. 즉, 아직 완전히 제거된 것은 아니다. 하지만, 이 개체는 Push 할 수도 Clone 할 수도 없다. 이 점이 중요하다.
정말로 완전히 삭제하려면 git prune --expire
명령으로 삭제해야 한다.
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0