1. 기본을 넘어서
Git은 풍부한 도구 모음을 가지고 있다. 지금까지는 매일 사용하는 Git의 기본 기능만 다루었다.
Git은 현재 140개가 넘는 명령어를 가지고 있으며, 아마도 이 중에서 상당수는 앞으로도 전혀 사용할 일이 없을 것이다.
예를 들어, Git 내부적으로 문자열이 유효한 브랜치나 태그명인지 판단하려고 사용하는 git check-ref-format과 같은 명령어는, Git의 확장을 만드는 경우가 아니라면 사용할 일이 없다.
하지만, 매일 사용하는 명령어는 아니겠지만, 필요할 때 사용하면 정말로 유용한 명령어도 있다.
* 저장소 이력 압축하기
l 저장소 내보내기
l 브랜치 이력 재정렬하기
l reflog를 이용해서 저장소 고치기
l 저장소를 양분하여 어떤 변경이 버그를 발생시켰는지 찾기
$ git clong git://github.com/tswicegood/mysite-chp8.git Initialized empty Git repository in /work/mysite-chp8/.git/ remote: Counting objects: 56, done. … |
1.1. 저장소 이력 최적화 하기
일상 생활에서 주변을 좋은 상태로 유지하려면 약간의 관리가 필요하다.
자동차 오일도 교환해주어야 하고, 바닥 청소도 해야 한다.
마찬가지로 Git에서는 git gc run을 실행해야 한다.
Git은 모든 것을 저장한다.
모든 것을 저장하기에 불필요한 찌꺼기 데이터를 포함하기도 한다.
예를 들어, git commit에 --amend 매개변수를 지정해서 커밋을 수정해도, Git은 수정되기 이전 리비전을 기억하고 있다.
또 다른 예로, git branch –D로 실험용 브랜치를 지워서 해당 브랜치에 대한 연결고리가 없는데도, Git은 브랜치에 어떤 내용이 들어 있었는지 알고 있다.
이런 경우에 git gc를 이용한다.
100번 커밋한 후나 한 달에 한 번 정도 git gc를 실행하도록 주기를 정해놓으면,
Git이 내부적으로 이력을 저장하는 방식을 최적화하므로 지저분한 내용을 깔끔하게 정리할 수 있다.
이때 git gc를 실행한다고 해서 이력이 변경되지는 않는다. 단지 이력을 저장하는 방식만 바꾼다.
저장소에 따라서 git gc를 실행해서 변경되는 정도에도 차이가 있다.
일단 git gc를 실행하면, 앞으로 최적화가 필요할 정도로 변경하기 전까지, 다시 실행하지 않아도 된다.
그럼 출력 결과를 보고 무엇이 최적화 됐는지 알 수 있을까? 디스크 공간이다.
Git은 속도와 효율 모두를 취하려고 한다. 최적화 일부는 나중에 속도가 문제되지 않을 때 실행할 수 있도록 남겨둔다.
git gc에 –aggressive 매개변수를 지정하면, 한층 더 깊이 최적화한다.
Git은 변경 사항을 델타(delta, 델타는 두 비교 대상간의 차이를 나타내는 단위이다) 단위로 저장한다. git gc를 실행하면 이렇게 저장된 델타를 압축하지만 다시 계산하지는 않는다.
최적화를 수행할 때 --aggressive를 추가하면 이런 변경 사항 델타를 처음부터 다시 계산한다.
1.2. 저장소 내보내기
성공적인 프로젝트라면 만든 소프트웨어를 릴리스해야 한다.
작성한 소프트웨어를 파이선, 루비, php같이 유행하는 스크립트 언어로 작성했다면 최종 사용자가 실행할 수 있도록 소스도 함께 포함시켜야 한다.
소스를 직접 받아서 쓸 수 있도록, 공용 Git 저장소의 접근 권한을 줘도 되겠지만,
프로젝트의 스냅샷에 해당하는 공식 ‘릴리스’를 만들어서 제공하면,
사용자가 소프트웨어를 설정하고 실행할 때 알아야 할 내용을 줄일 수 있다.
Git은 스냅샷을 찍을 수 있는 편리한 도구로 git archive를 제공한다.
git archive는 지정한 저장소의 위치에 해당하는 코드의 복사본을 생성해서 tar나 zip 형식으로 내보낸다.
git archive에는 매개변수 몇 개를 지정해야 한다.
먼저, --format=(파일형식)을 지정해서 어떤 포맷으로 내보낼지 지정해야 한다.
tar, zip이 유요한 값이며 반드시 지정해야 한다.
다음은, 압축 파일을 생성하려는 저장소 이력의 위치이며, 반드시 지정해야 한다. 커밋명 브랜치, 태그를 지정할 수 있다.
이외에 유용한 매개변수로 --prefix가 있는데, 내보낼 파일들의 이름 앞에, 원하는 이름을 추가할 수 있다. --prefix에 디렉터리를 지정하면 내보낼 릴리스를 특정 디렉터리 아래 둘 수 있다.
$ git archive --format=zip \ --prefix=mysite-release/ \ HEAD > mysite-release.zip |
첫째 줄은 앞에서 얘기한 --format 매개변수를 지정했다.
둘째 줄은 --prefix매개변수다. mysite-release/ 이름에서 볼 수 있듯이 마지막에 슬래시 문자를 넣어서 디렉터리를 나타낸다. 슬래시 문자를 넣지 않고 git archive를 실행하면 파일을 디렉터리에 넣는 대신에 모든 파일명 앞에 해당 문자열을 추가한다.
마지막 줄은 2가지를 나타낸다. 먼저, HEAD에서 압축 파일을 생성하도록 지정했다.
그 다음, > 부호를 사용해서 git archive의 출력을 mysite-release.zip으로 내보내도록 지정했다.
tar 파일을 만드는 명령어도 유사하다. 다만, git archive 출력을 직접 파일로 내보내는 대신에 추가적인 명령어를 지정해야 한다. mysite-release.tar.gz 파일을 만드는 명령은 다음과 같다.
$ git archive --format=tar \ --prefix=mysite-release/ \ HEAD | gzip > mysite-release.tar.gz |
HEAD 다음에 파이프(|)를 사용해서, 내용을 gzip 명령어로 전달했음을 알 수 있다.
gzip 명령어에서 mysite-release.tar.gz 파일을 생성한다.
gzip 대신 bzip2로 바꾸면 bzip2를 이용해 압축할 수 있다.
tar, zip 파일 형태로 저장소를 내보내면, 저장소의 스냅샷을 편리하게 배포할 수 있다.
저장소의 전체 이력을 알 필요가 없는 사람이나, 특정 지점만 알고 싶어하는 사람에게 유용하다.
1.3. 브랜치 재정렬하기
브랜치를 이용하는 것이 작업을 조직화하는데 훌륭한 방법이지만,
브랜치 간에 모든 것을 동기화하는 작업은 브랜치의 약점이다.
물론 Git에서 제공하는 합치기 추적을 이용하면 동기화할 때 드는 비용을 급격히 줄일 수 있지만, 여기에서는 합치기 추적이 아닌 다른 방법을 알아본다.
예를 들어, 8.2 ‘릴리스 브랜치 다루기’에서 RB_1.0.1에 코드를 추가했다.
그래서 1.0.1 태그에는 master 브랜치가 모르는 새로운 코드가 들어있다.
RB_1.0.1의 변경 사항을 master의 이력에 합치도록 RB_1.0.1 태그를 이용해서 master를 재정렬할 수 있다. 이렇게 하면 master가 처음부터 해당 변경 사항을 가지고 있던 것처럼 된다.
git rebase를 –i 매개변수 없이 사용하면, 다른 브랜치의 변경 사항을 현재 브랜치의 이력에 재정렬하거나 고쳐 쓸 수 있다.
다른 브랜치의 변경 사항이 현재 브랜치에 원래부터 있던 것처럼 이력을 생성하는 방식으로 볼 수 있다.
git rebase에 재정렬에 이용할 태그를 지정하고 실행한다.
실행하면 바로 오류가 발생한다.
5.4 ‘충돌 다루기’에서 충돌에 대처하는 방법을 다뤘다.
git mergetool을 이용하는 것이 가장 무난한 방법이겠지만 이번에는 직접 수정하는 편이 더 쉽다.
사용자가 직접 합치는 편이 좋은 이유는 2중에서 선택해야 하는 상황이 아니기 때문이다.
1.0.1에서 가져온 새로운 문단과 1.0 태그를 생성한 이후에 추가했던 순서 없는 목록 모두 필요하다.
따라서 파일을 직접 합치는 편이 가장 쉽다.
<<<<<<< HEAD:index.html <p> Fixed </p> ======= <ul> <li><a href=”bio.html”>Biography</a></li> </uul> >>>>>>> add in a bio link:index.html |
그냥 보통 파일처럼 보이도록 충돌 영역을 표시하는 3줄을 제거한다.
파일을 저장하고 다음 명령어를 실행하면, Git에 충돌이 해결됐음을 알리고 계속해서 재정렬을 수행하도록 한다.
$ git add index.html $ git rebase –continue Applying: add in a bio link … |
다른 상황으로는 충돌이 있다.
절차는 거의 동일하다.
이전과 같이 충돌을 수정하고, git add와 git rebase --continue를 호출한다.
물론 git rebase를 호출할 때, --skip을 지정해서 특정 커밋만 건너뛰거나 –abort를 지정해서 재정렬하는 과정을 취소할 수 있다.
--onto 매개변수를 지정하면 이력을 또 다른 방식으로 고쳐 쓸 수 있다.
예를 들어 3개의 브랜치가 있다고 하자.
master 브랜치와 master브랜치로부터 생성된 contact 브랜치, 그리고 contact 브랜치에서 생성된 search 브랜치다.
이 3 브랜치에서 작업했던 내용은 다음과 같다. 연락처 작업 때문에 contact 브랜치의 코드를 이용했지만 도중에 검색 관련 코드를 추가하기로 결정했다.
새로운 검색 코드를 끝마치고서 난 다음에야 contact 브랜치의 모든 변경 사항이 검색 관련 작업에 필요하지 않다는 사실을 깨달았다.
이런 경우에 --onto 매개변수를 이용한다.
--onto에는 재정렬 대상이 되는 하나의 브랜치를 지정한다.
search를 master 브랜치로 재정렬하려는 명령어는 다음과 같다.
$ git rebase –onto master contact search |
search 브랜치를 contact 브랜치에서 떼어내서 master 브랜치로 옮긴다.
search 브랜치를 master 브랜치로 합치려고 하지만, contact 브랜치의 내용이 필요하지 않은 경우에 유용하다.
물론, 재정렬을 시작해서 변경 사항을 반영할 때 합쳐지면서 충돌이 발생하지 않으려면, search 브랜치의 내용은 완전히 독립적이어야 한다.
6.2 ‘리비전 범위 지정하기’에서 알아봤던 방식을 이용해서 --onto와 커밋 범위를 조합하면 다른 일도 할 수 있다.
예를 들어, git rebase를 이용해서 리비전을 지울 수 있다.
2단계 이전 커밋(HEAD^)을 지우는 명령어는 다음과 같다.
$ git rebase --onto HEAD^^ HEAD^ HEAD |
이 명령어는 현재 HEAD로부터 2단계 이전 커밋에 현재 HEAD를 재정렬해서 HEAD^를 지운다.
물론 브랜치의 이력을 고쳐 쓰려면, 먼저 무언가 잘못됐을 때 어떻게 돌아올지에 대한 백업 계획을 준비해두는 게 좋다. 사용하고 있는 브랜치에 바로 반영하지 않고 실험용 브랜치를 만들 수도 있다.
Git에서는 브랜치를 생성하는 비용이 저렴하기 때문에 재정렬을 연습할 때는 사용하고 있는 브랜치 대신 연습용 브랜치를 만드는 것도 좋은 생각이다.
1.4. Reflog 이용하기
리누스 토발스가 Git를 만들면서 정해둔 원칙 중 하나는 안전하다는 것이다.
비 파괴적이라는 말은 저장소의 어떤 시점으로도 다시 돌아갈 수 있어야 함을 의미한다.
그러기 위해서는 상태에 따른 변경을 추적할 수 있는 기법이 필요하다.
Git을 이용하면 대부분의 버전 관리 시스템에서는 할 수 없는 일도 가능하다.
오래된 커밋을 변경하고, 커밋 하나를 여러 개로 쪼개고, 커밋을 제거하고, 중간에 커밋을 추가하는 동작은 Git에서는 가능하지만, 서브버전이나 CVS 같은 버전 관리 시스템에서는 불가능하다.
물론 이런 기능에는 위험이 따른다. git branch –D를 이용해서 실수로 브랜치를 지웠거나 git rebase –i를 잘못 조작했다면 어떻게 해야 할까?
이럴 때 reflog를 이용한다.
reflog는 브랜치가 변경될 때마다 계속해서 추적한다.
5장 ‘브랜치 이해하고 활용하기’의 내용을 보면 브랜치는 단지 최신 커밋을 가리키고 있을 뿐이었다. reflog는 모든 변경을 추적한다. reflog를 이용하면 삭제한 브랜치를 복원할 때 체크아웃 해야 할 커밋을 찾을 수 있다.
예제로 확인할 수 있도록 간단한 저장소를 만든 다음 망쳐버리자.
저장소를 만들어 하나만 커밋하고, 그 다음 브랜치를 만들어 2개의 커밋을 추가한다.
제대로 된 위치에 커밋하기만 하면 되니 아무 내용이나 넣어서 커밋하자.
이제 git rebase나 git rebase –i를 이용해서 두번째 브랜치의 첫번째 커밋을 삭제한다.
git log를 이용해서 저장소의 이력이 어떻게 되었는지 확인해 볼 수 있다.
당연히 git rebase –i가 커밋 하나를 지우고 나면 이력에는 2개의 커밋만 있다.
이제 reflog를 실행해보면 사실 더 많은 커밋이 있음을 확인할 수 있다.
$ git reflog 1b41334…HEAD@{0}: commit: third commit 5e685de…HEAD@{1}: checkout: moving from reflog to 5e685de… 0cb04ad…HEAD@{2}: commit: third commit 71bc515…HEAD@{3}: commit: second commit 5e685de…HEAD@{4}: checkout: moving from master to reflog |
git log의 출력 결과와 마찬가지로 최신 커밋이 가장 먼저 나타나며,
출력 결과에는 ‘third commit’이라는 메시지를 가진 커밋이 2개가 있다.
0cb04ad이 먼저 한 커밋이고, 1b41334은 git rebase로 71bc515를 지운 다음에 한 2번째 커밋이다.
따라서 먼저 했던 ‘third commit’을 복구하려면 단지 해당 커밋을 체크아웃 받으면 된다.
$ git checkout 0cb04ad note: moving to “0cb04ad”which isn’t a local branch if you want to create a new branch from this checkout, you may do so (now or later) by using –b with the checkout command again. Example: git checkout –b <new_branch_name> HEAD is now at 0cb04ad…third commit |
출력 메시지를 보면 알 수 있듯이 지금은 실제 브랜치 위에 있지 않다.
브랜치가 아니라고 해서 이력을 보지 못하는 건 아니지만,
간단히 git checkout –b 를 실행하면 브랜치를 생성할 수 있다.
이제 git log를 실행해보면 2번째 커밋이 복구되어 있다.
$ git log –pretty=format:”%h %s” 0cb04ad third commit 71bc515 second commit 5e685de initial commit |
당연한 얘기지만 브랜치를 생성하려고 체크아웃 받을 필요는 없다.
브랜치를 생성하기 전에 이력을 확인해야 하는 게 아니라면,
그냥 간단히 git branch를 이용해서 브랜치를 바로 생성할 수 있다.
$ git branch reflog-restored 0cb04ad $ git checkout reflog-restored Switched to branch “reflog-restored” |
git gc는 오래된 reflog 항목을 더 이상 유효하지 않게 만들 수 있다.
예를 들어, 방금 복구한 커밋은 보통 사흘 후에는 삭제되며, gc.reflogExpireUnreachable 설정을 바꾸면 변경이 가능하다.
마찬가지로 보통의 reflog 항목들도 gc.reflogExpire를 변경하지 않으면 9일 후에는 유효하지 않게 된다 reflog가 너무 커지지 않도록 하기 위해서이다.
1.5. 저장소 양분하기
사람은 완벽하지 않기 때문에 이따금씩 발생하는 버그에 대비해야 한다.
단위테스트와 같은 도구는 버그의 위치를 찾을 수 있도록 돕는다.
단위 테스트를 통해서도 버그의 위치를 찾지 못했다면, 저장소를 활용해서 버그가 발생한 시점을 찾아볼 수 있다.
git bisect는 버그가 나타난 시점을 찾을 때 이용하는 도구다.
git bisect는 이미 알고 있는 안전하지 않은 커밋과 안전한 커밋을 기반으로 한단계씩 옮겨가는 방식으로 동작한다. 버그를 야기한 커밋을 고립시킬 때까지 안전(good)과 안전하지 않음(bad)으로 표시하면서 이력을 따라간다.
먼저 버그를 확인하는 테스트 케이스를 만든다.
안전하지 않다고 확인된 커밋에서 테스트 케이스가 실패하는지 확인한다.
그리고 안전하다고 확인된 커밋에서 테스트 케이스가 통과하는지도 확인한다.
이제 저장소의 위치를 안전하지 않은 커밋으로 돌려두고 git bisect start를 호출해서 저장소를 쪼개는 과정을 시작하자. 그 다음 git bisect bad를 실행하고, 마지막으로 git bisect good <커밋이나 태그>를 실행한다.
전체 과정은 다음과 같다.
$ git bisect start $ git bisect bad $ git bisect good 1.0 … |
안전하지 않은 지점과 안전한 지점을 표시하면 git bisect는 사용자의 현재 위치를 저장소의 다른 위치로 변경한다.
2지점 사이의 커밋을 취한 다음 이를 절반으로 쪼갠다. 그리고 나서 중간지점을 체크아웃 받는다. 이제 저장소는 다음과 같다.
git bisect는 중간 지점에서 시작한다.
| | 안전한 커밋 | | 양분화 시작 지점 | | 안전하지 않은 커밋 |
| | v | ? | ? | ? | X |
이제 테스트를 다시 실행해서 이 커밋이 안전한지 아닌지를 확인해보자. 이번에는 커밋이 안전하지 않은 것으로 확인됐으니 해당 커밋을 git bisect bad로 안전하지 않다고 표시한다.
이제 안전하지 않은 커밋과 안전한 커밋 사이에는 하나의 커밋만 남아있다.
논리적으로 생각해보면 이전 커밋은 안전하고 이 지점 이후의 커밋은 모두 안전하지 않으니, 이 지점이 안전하다면 다음 커밋이 버그의 원인이고, 이 지점이 안전하지 않다면 이 커밋이 버그의 원인이다.
단위테스트를 해보자. 여기서는 이 커밋에서 버그가 발생했다고 여기자.
이 커밋도 안전하지 않다고 표시하면 Git은 버그를 발생시킨 커밋을 찾았음을 알아채고 이에 대한 로그를 보여준다. 지금까지 이력의 범위를 좁혀서 안전하지 않은 커밋을 파악했다.
이제 남은 일은 커밋에서 정확히 어떤 변경이 문제를 야기했는지 파악하고 문제를 고치는 일이다.
git bisect는 각 커밋을 확인하는 과정에서 사용자의 브랜치를 변경한다.
따라서 변경을 반영하기 전에 다시 이전에 작업하던 브랜치로 돌아가야 한다.
git bisect reset을 실행하면 된다.
$ git bisect reset Switched to branch “master” |
Git의 다른 명령어들과 마찬가지로 git bisect는 이렇게 단순한 형태보다 더 많은 것을 지원한다. git bisect visualize를 이용하면 변경 이력을 시각적으로 보여주므로 이해하기가 쉽다.
git bisect log를 이용하면 텍스트 형태로 어떤 커밋이 안전하다고 표시됐고, 어떤 커밋이 안전하지 않다고 표시됐는지를 볼 수 있다. 이 명령어는 잘못 표시한 커밋을 나중에 찾을 때도 유용하다.
잘못 표시한 커밋을 다시 제대로 표시하려면 git bisect log의 출력을 파일로 저장하고, 파일에서 실수한 시점 이후의 모든 내용을 제거하고 저장한다. 그러고 나서 저장한 파일을 git bisect replay <파일> 명령어의 매개변수로 지정하면 파일에 기록된 지점까지만 수행한다.
git bisect를 이용해서 버그가 발생된 커밋을 찾아가는 작업이 정말 좋기는 하지만, 수많은 작업을 일일이 손으로 해야 한다. 해야 할 일의 양도 양이지만, 사람이 하는 일이므로 오류가 발생할 수도 있다. 자동화된 테스트 스위트(test suite)가 있다면 수작업이 어느 정도는 줄겠지만, git bisect가 테스트 스위트를 알아서 실행하도록 만들면 수작업을 더 줄일 수 있다.
이렇게 하려면 커맨드 라인에서 실행할 수 있는 스크립트가 필요하다.
작성한 스크립트는 테스트가 통과하면 종료코드가 0으로 끝나고, 실패하면 종료코드가 0보다 큰 수로 끝나야 한다. 대개 실패하면 종료코드로 1을 사용한다.
이때, 125를 종료코드로 사용하면 현재 커밋에 표시하지 않고 다음 커밋으로 건너뛰므로 이런 용도가 아니라면 125를 사용하면 안 된다.
안전한지 아닌지를 테스트 할 수 없는 경우가 아니라면 보통은 커밋을 건너뛰려고 하지는 않을 것이다.
git bisect run을 실행하면 자동적으로 작성한 스크립트를 실행한다.
예를 들어, run-tests라는 스크립트를 만들었다면 다음과 같이 실행할 수 있다.
$ git bisect start HEAD 1.0 … $ git bisect run /work/run-tests running /work/run-tests |
/work/run-tests라는 스크립트의 종료 코드를 이용한다는 차이를 제외하고는,
git bisect는 사용자가 직접 하던 대로 계속해서 동작한다.
스크립트에서 0이 아닌 숫자를 반환할 때마다 안전하지 않은 커밋으로 취급한다.
이 명령어에서는 저장소가 밖의 다른 디렉터리에 있는 스크립트 파일을 지정했음을 볼 수 있다. 실행할 스크립트 파일을 저장소 밖에 두면 git bisect가 실행되면서 저장소가 변경돼도 스크립트 파일이 바뀌지 않음을 보장할 수 있다. 꼭 저장소 밖의 파일을 이용해서 테스트를 진행해야 하는 것은 아니지만 불필요한 문제를 피할 수 있다.
출처 : http://youmin3.egloos.com/1990779