3장. 히스토리 레슨

분산관리식 시스템을 택한 Git은 개발자들이 history관리를 용이하게 할 수 있게 해줍니다. 그러나 프로그램의 과거를 들춰내려면 조심하세요: 당신이 소유하고 있는 파일들만 다시쓰기 하세요. 세계 각국의 나라들이 누군가 어떤 잘못을 하나하면 누가했는지 끝임없이 따지는 것처럼 만약 한 개발자가 당신이 가지고 있는 파일과 기록 (history)이 다른 파일들을 클론하여 갔을 때 추후 병합시 문제가 생길지도 모릅니다.

어떤 개발자들은 파일의 수정기록들이 절대로 조작되면 안되는 것이라고 믿고 있습니다. 반면에 어떤 개발자들은 수정 기록들이 깨끗하게 정리되어 보여져야 할 것만 선택하여 보여져야 한다고 합니다. Git은 이렇게 다른 성향의 개발자들을 모두 포용할 수 있습니다. 클로닝, branch, 병합과 같은 기능들이 당신이 어떤 개발자 타입이던 당신의 일처리를 도와줄 것입니다. 어떻게 영리하게 사용하는지는 당신에게 달려있죠.

오류 수정합니다

방금 commit을 했는데, 그 commit에 달린 메세지를 바꾸고 싶다고요? 그렇다면:

$ git commit --amend

위 명령어를 사용하면 마지막으로 한 commit의 메세지를 바꿀 수 있습니다. 파일을 더하는 것을 잊어버리고 commit을 했다고요? *git add*를 사용하고서 위의 명령어를 사용하세요.

마지막으로 했던 commit에 편집을 더 하고 싶으신가요? 그렇다면 작업 후에 다음 명령어를 쓰세요.

$ git commit --amend -a

… 더 있습니다

이제 전보다 더 꼬인 상황을 마주했다고 생각합시다. 우선 당신이 긴 시간동안 작업해서 많은 commit을 하였다고 가정해봅시다. 그러나 당신은 그 commit들의 난잡한 구성이 마음에 들지 않습니다. 그리고 몇몇 commit 메세지들을 다시쓰고 싶다면:

$ git rebase -i HEAD~10

위 명령어를 사용한다면 당신의 작업용 에디터에 지난 열 개의 commit이 출력될 것입니다. 샘플을 보자면:

pick 5c6eb73 Added repo.or.cz link
pick a311a64 Reordered analogies in "Work How You Want"
pick 100834f Added push target to Makefile

여기서는 'log’와는 달리 가장 오래된 commit 부터 가장 최근의 commit의 순서로 나열되어 출력되었습니다.여기서는 5c6eb73 가 가장 오래된 commit이고 100834f이 가장 최근 commit 이죠. 그리고:

  • 한 줄을 지움으로써 commit을 삭제합니다. revert 명령어와 비슷하지만 기록에는 남지 않게 지웁니다: 이 방법은 마치 commit이 처음부터 존재하지 않던 것처럼 보여지게 해줍니다.
  • Commit list를 재정렬하며 commit의 순서를 바꾸어 줍니다.
  • 위의 pick 명령어 대신에

    • edit 을 사용하여 개정시킬 commit을 마킹합니다.
    • `reword`를 사용하여 로그메세지를 바꿉니다.
    • squash 를 사용하여 전에 했던 commit과 합병합니다.
    • fixup 를 사용하여 전에 했던 commit과 합병 후 log 메세지를 삭제합니다.

예를들어, 두번째 행의 pick’을 'squash 명령어로 바꾸어 봅니다:

pick 5c6eb73 Added repo.or.cz link
squash a311a64 Reordered analogies in "Work How You Want"
pick 100834f Added push target to Makefile

저장 후 프로그램을 종료하면, Git은 a311a64를 5c6eb73로 병합시킵니다. squash (짓누르기)는 지정된 commit을 바로 다음 commit으로 밀어붙어버린다고 생각하시면 됩니다.

또, Git은 로그메세지들을 합친후 나중에 편집할 수 있게 해주기도 합니다. fixup 명령어를 사용하면 이런 절차를 하지않아도 됩니다; 짓눌려진 (squash 된) 로그메세지들은 간단히 삭제되기 때문입니다.

*edit*을 이용하여 어떤 commit을 마킹해두었다면, Git은 같은 성향의 commit들 중에 가장 오래전에 했던 commit의 작업상태로 당신을 되돌려 보냅니다. 이 상태에서 아까 전 말했듯이 편집작업을 할 수도 있고, 그 상태에 맞는 새로운 commit을 만들수도 있습니다. 모든 수정작업이 만족스럽다면 다음 명령어를 사용해 앞으로 감기를 실행할 수 있습니다.:

$ git rebase --continue

Git은 다음 *edit*까지 아니면 아무런 *edit*이 없다면 현재 작업상태까지 commit을 반복실행 할것입니다.

새로운 rebase를 포기할 수도 있습니다:

$ git rebase --abort

그러니 commit을 부지런하게 자주하십시오: 나중에 rebase를 사용하여 정리할 수 있으니까요.

로컬에서의 수정작업

어떤 프로젝트를 진행하고 있습니다. 당신의 컴퓨터에서 commit을 하며 작업을 하다가 이제 공식적인 프로젝트 파일들이 있는 branch와 동기화 해야합니다. 이런 절차는 메인 branch에 올리기전에 거쳐야 할 과정이지요.

그러나 당신의 로컬 Git클론은 당신 혼자만 이해할 수 있는 수많은 기록이 뒤죽박죽 섞여있을 것입니다. 아무래도 개인적인 기록이 깨끗하게 정리되어 공식적인 기록만 볼 수 있게 된다면 좋겠지요:

위에서 설명했듯이 git rebase 명령어가 이 작업을 해줄것입니다. --onto 플래그를 사용하여 상호작용을 피할수도 있습니다.

*git help rebase*를 확인해서 좀 더 자세한 예를 한번 봐보세요. Commit을 나눌 수 있다는 걸 알게될 것입니다. 여러 branch들을 재정리할 수도있죠.

*rebase*는 유용한 명령어입니다. 여러가지 실험을 하기전에 *git clone*으로 복사본을 만들어두고 놀아보세요.

기록 다시쓰기

가끔은 어떤 그룹사진에서 포토샵으로 몇 사람지우는 기능과 같은 명령어가 필요할지도 모릅니다. 스탈린식 스타일로 사람을 무자비하게 지우는 명령어 말입니다. 예를들어 이제 어떤 프로젝트를 대중에게 공개할 시간이 왔다고 가정합니다. 그러나 어떤 파일들은 일반 유저들이 보지 못하도록 하고싶습니다. 당신 크레딧카드 번호를 실수로 썻다던지 했다면 더욱 더 그러고 싶겠지요. 이런 경우, 파일을 지우는 것 만으로는 부족합니다. 예전 commit으로 파일을 지워진 파일을 복구하는 것이 가능하기 때문이죠. 당신은 이 파일을 모든 commit으로 부터 없에야 할 것입니다:

$ git filter-branch --tree-filter 'rm top/secret/file' HEAD

*git help filter-branch*를 보세요. 여기서는 본 예시에 대해 설명하고 있고 더 빠른 방법을 제시하여 줄 것입니다. 대체적으로 *filter-branch*은 하나의 명령어만으로도 대량의 파일기록을 변화시킬 수 있을 것입니다.

그리고 +.git/refs/original+ 디렉토리는 이렇게 만든 변화가 오기 전의 기록을 보여줄 것입니다. *filter-branch* 명령어가 어떻게 작용했는지 확인하고, 필요하다면 이 디렉토리를 지우면 됩니다.

마지막으로, 당신의 클론을 새로운 버전의 클론으로 바꾸시면 됩니다.

기록 만들기

어떤 프로젝트를 Git으로 옮겨오고 싶다고요? 다른 VCS에서 옮겨오는 것이라면, 어떤 개발자가 이미 프로젝트의 기록을 Git으로 쉽게 옮겨오는 스크립트를 써두었을지도 모릅니다.

아니라면, 특정 포맷으로 텍스트를 읽어 Git 기록을 처음부터 작성하여 주는 git fast-import 명령어를 확인해 보세요. 대체적으로 한번에 간편하게 프로젝트를 Git에서 사용할수 있게 해줄겁니다.

예를들어, /tmp/history 같은 임시파일에 다음 텍스트를 붙여넣기 해보세요:

commit refs/heads/master
committer Alice <alice@example.com> Thu, 01 Jan 1970 00:00:00 +0000
data <<EOT
Initial commit.
EOT

M 100644 inline hello.c
data <<EOT
#include <stdio.h>

int main() {
  printf("Hello, world!\n");
  return 0;
}
EOT


commit refs/heads/master
committer Bob <bob@example.com> Tue, 14 Mar 2000 01:59:26 -0800
data <<EOT
Replace printf() with write().
EOT

M 100644 inline hello.c
data <<EOT
#include <unistd.h>

int main() {
  write(1, "Hello, world!\n", 14);
  return 0;
}
EOT

그리고 이 임시파일로 Git repository를 만들어보세요:

$ mkdir project; cd project; git init
$ git fast-import --date-format=rfc2822 < /tmp/history

가장 최근 버전을 가져오고 싶다면:

$ git checkout master .

git fast-export 명령어는 아무 Git repository를 결과물이 사람들이 읽을 수 있는 포맷으로 바꾸어 주는 git fast-import 포맷으로 바꾸어 줍니다. 이 명령어들은 텍스트 파일들을 텍스트 파일 전용채널을 통해서 repository로 밀어넣기 해줍니다.

어디서부터 잘못되었을까?

당신은 몇 달전에는 잘 작동되던 프로그램이 갑자기 안 된다는 것을 발견했습니다. 아놔! 이 버그는 어디서부터 생긴 것일까요? 개발을 하면서 테스팅을 종종했더라면 진작에 알아챘을텐데요.

이미 그러기엔 너무 늦었습니다. 그러나 commit이라도 자주했다는 가정하에 Git은 이러한 짐을 덜어줄 수 있습니다:

$ git bisect start
$ git bisect bad HEAD
$ git bisect good 1b6d

Git에서 한 프로젝트를 자체적으로 테스팅을 실행합니다. 그리고 버그가 발견된다면:

$ git bisect bad

버그가 더이상 없다면 위 명령어에서 "bad"를 "good"으로 바꾸세요. Git은 good 버전과 bad 버전 사이로 당신을 데려갈 겁니다. 물론 버그를 찾을 확률은 높아지지요. 몇 번이렇게 반복하다보면 결국엔 버그를 일으킨 commit을 찾아낼 수 있게 도와줄 것입니다. 버그찾기를 완료했다면 다시 처음 작업상태로 (이젠 버그가 없겠지요) 돌아가야 겠지요:

$ git bisect reset

수동으로 테스팅하는 것보단, 다음 명령어로 테스팅을 자동화할 수 있습니다:

$ git bisect run my_script
Git 은 기존 대비할 스크립트에 약간의 변화를 주어서 이것이 좋은 변화인지
안 좋은 변화인지 체크합니다: 좋은 변화는 0으로 무시해야할 변화는 125로
안 좋은 변화는 1과 127사이의 번호로 테스팅을 종료합니다. 마이너스 숫자는
이분화  (bisect)를 강제종료하지요.
당신은 이것보다 더 많은 일을 할 수 있습니다. 도움말은 이분화를 시각화해주는 방법과,
이분화 기록을 다시보는 방법, 이미 확인된 이상없는 변화들은 건너띄어 테스팅을 가속 시켜주는 기능들을 배우실 수 있습니다.

누가 망가뜨렸을까?

다른 VCS들과 같이 Git은 누군가를 탓할 수 있는 기능이 있습니다:

$ git blame bug.c

이 명령어를 사용하면 누가 언제 마지막으로 어느 부분을 작업하였는지 표시하여 줍니다. 다른 VCS들과는 달리 모든 작업은 오프라인에서 진행됩니다.

나의 개인경험담

중앙관리식 VCS에서는 파일들의 기록 변경은 어려울 뿐만아니라 관리자만이 변경할 수 있습니다. 클론, branch 만들기와 병합하기는 네트워크를 통해서만 할 수 있는 작업들입니다. 브라우징 기록보기, commit하기 역시 중앙관리식 VCS에서는 네트워크를 통해야만 합니다. 어떤 시스템에서는 네트워크 연결이 되어야지만 자기 자신이 작업한 기록을 보거나 편집할 수 있습니다.

중앙관리식 VCS는 개발자의 수가 늘어남에 비례해서 더 많은 네트워크 통신을 요구하기 때문에 오프라인에서 작업하는 것보다 비경제적일 수 밖에 없습니다. 그리고 제일 중요한 것은 모든 개발자들이 고급명령어들을 적재적소에 쓰지 않는다면 모든 작업이 어느정도는 무조건 느릴 수 밖에 없다는 것입니다. 극적인 경우에는 아주 기본적인 명령어 역시 잘못하면 느려질 수 밖에 없습니다. 무거운 명령어를 써야한다면 일의 효율성은 나쁜영향을 받을 수 밖에 없습니다.

저는 직접 이런 상황들을 겪어보았습니다. Git은 제가 사용한 가장 먼저 사용한 VCS였죠. 저는 Git의 여러기능들을 당연하다 생각하고 금방 적응하였습니다. 저는 당연히 다른 VCS들 역시 Git이 제공하는 기능들을 가지고 있을 것이라고 생각하였습니다: VCS를 선택하는 것은 텍스트 에디터나 웹 브라우저를 선택하는 것과 같은 맥락일 것이라고 생각하였습니다.

제가 나중에 강제로 중앙관리식 VCS를 사용하게 되었을땐 완전 쇼킹이였죠. 불안정한 인터넷연결은 Git을 사용할 때 중요치 않습니다. 그러나 이러한 인터넷연결은 로컬디스크에서 작업하는 것 만큼은 효율적 일수는 없죠. 그리고 저는 어떤 명령어는 연결 딜레이를 고려함에 따라 습관적으로 쓰지 않고있는 걸 시간이 지나며 깨달았습니다. 이런 습관은 제가 원하는 방식대로 작업을 할 수 없게하는 방해물들이었죠.

어쩔 수 없이 느린 명령어를 사용할 때는 저의 작업효율에 치명타를 입히기 일쑤였죠. 네트워크로 처리되야하는 작업이 완료되길 기다리면서 이메일 확인 및 다른 문서작업을 하며 시간을 때웠습니다. 그러다가 원래하던 작업에 돌아가면 다시 무엇을 했었는지 기억을 해내는데 시간이 많이 허비된 경험이 잦았습니다. 인간은 환경변화에 적응을 할 수는 있으나 그 적응이 결코 빠르진 않죠.

일을 하면서 발생하는 아이러니한 비극도 존재했죠: 네트워크 상황이 원활하지 않을 것이라는 걸 아는 개발자들은 미래에 딜레이를 줄이기위해 지금 하는 작업들이 오히려 현재 트래픽을 더 잡아먹을 수가 있다는 것입니다. 모든 개발자들이 네트워크를 원활하게하는 노력을 할수록 오히려 서로에게 해가 될 수있다는 것입니다. 이게 무슨 아이러니한 일입니까? == Git은 멀티플레이어 ==

제가 과거에 단독 개발자였던 시절부터 Git을 사용해 오고있었습니다. 그 당시엔 여태까지 소개했던 많은 명령어들 중 *pull*과 *clone*정도만 사용하여 같은 프로젝트를 여러 디렉토리에 저장하는데 사용하였습니다.

시간이 지난 후 Git에 제가 만든 코드를 올리고 싶었고 다른 개발자들이 한 작업도 반영하고 싶었습니다. 저는 전 세계의 많은 개발자들을 관리하는 방법을 배워야 했습니다. 다행히도 이런 일을 도와주는 것은 Git의 가장 큰 힘입니다. Git이 존재하는 이유이기도 하지요.

난 누굴까?

각 commit은 작성자의 이름과 작성자의 이메일주소를 저장합니다. 이 목록은 *git log*를 사용해 조회할 수 있습니다. 기본설정 상 Git은 시스템에 이미 기본으로 세팅이 되어있는 정보를 이용해 작성자의 이름과 이메일주소를 저장합니다. 그러나 수동으로 이름과 이메일주소를 설정하려면:

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

를 사용하여 지정해 주십시오.

현재 사용중인 repository에만 한정적으로 사용할 수 있는 이름이나 이메일을 설정하려면 위 명령어에서 *--global*을 사용하지마세요.

SSH, HTTP를 통한 Git 사용

웹 서버에 관한 SSH 접근권한을 보유하고 있다고 합니다. 그러나 Git은 아직 설치되어 있지않다고 가정합니다. 기존 프로토콜만큼 효율적이진 않겠지만, Git은 HTTP를 통해 데이터 교환이 가능합니다.

우선 기본적으로 컴퓨터에 Git을 다운받아서 설치합니다. 그리고 웹 디렉토리에 저장소를 만듭니다:

$ GIT_DIR=proj.git git init
$ cd proj.git
$ git --bare update-server-info
$ cp hooks/post-update.sample hooks/post-update

Git의 예전 버전에선 복사를 지시하는 명령어가 안 들을 수 있습니다. 그렇다면:

$ chmod a+x hooks/post-update

이제 아무 클론에서 SSH를 통해 당신의 작업을 업로드할 수 있습니다:

$ git push web.server:/path/to/proj.git master

다른 사람들은 당신의 작업을 다운받으려면 다음 명령어를 쓰면 될겁니다:

$ git clone http://web.server/proj.git

모든 것은 Git을 통한다

서버나 인터넷 연결없이 저장소를 동기화시키고 싶다고요? 긴급하게 수정할 것이 발견되었다고요? git fast-export 그리고 git fast-import 명령어들은 repository를 하나의 파일로 묶어주거나 그 하나의 파일을 repository로 되돌려 줄 수 있는 것을 배웠습니다. 다양한 매개체를 통해서 repository의 파일들을 옮길 수 있지만, 정말 효율적인 방법은 git bundle 명령어를 쓰는 것입니다.

보내는 이가 '묶음 (bundle)'을 만듭니다:

$ git bundle create somefile HEAD
그리고 다른 장소로 그 묶음, +somefile+을 어떻게든 옮겨야한다고 가정합시다: 이메일로 보내야할수도 있고, USB드라이브로 보내거나, *xxd* 프린트로 뽑아서 전송하던지, OCR 스캐너로 전송하던지, 전화로 직접 이야기하던지, 연기로 신호를 보내던지 등 어떻게든 보내야합니다. 파일을 받을 사람들은 이 묶음으로부터의 commit을 다음 명령어를 이용하여 간단히 받을 수 있습니다:
$ git pull somefile
파일을 받는 사람들은 빈 repository에서도 이 명령어를 사용할 수 있습니다. 파일의
사이즈가 작아 보임에도 불구하고 +somefile+ 은 저장소의 본 모습을 담고 있습니다.

큰 프로젝트에서는 묶음만들기를 좀 더 효율적으로 하기위해서 버전차이만을 묶어줍니다. 예를 들어서 "1b6d"의 hash가 달린 commit 이 가장 최근에 보내는 이와 받는 이 사이에 공유된 commit이라고 가정해 봅니다:

$ git bundle create somefile HEAD ^1b6d

너무 자주 이렇게 한다면, 어떤 commit이 가장 최근 것인지 기억하지 못할 수 있습니다. 도움말에서는 태그를 이용해 이런 문제점들을 피하라 명시합니다. 풀어서 말하자면 어떤 묶음을 보낸 후에는:

$ git tag -f lastbundle HEAD

그리고 새로운 묶음을 만들어 줍니다:

$ git bundle create newbundle HEAD ^lastbundle

패치: 세계적 통화

패치는 컴퓨터와 인간이 쉽게 알아들을 수 있는 언어로 파일의 변화를 텍스트로 표현할 수 있는 방법입니다. 전 세계 어떤 개발 공간에서나 이런 식으로 파일의 변화를 시도합니다. 어떠한 VCS를 쓰던간에 개발자들에게 패치를 이메일로 보낼 수도 있습니다. 그 개발자들이 그 이메일을 읽을 수만 있다면 그들은 당신이 편집을 한 작업기록을 손쉽게 볼 수 있을겁니다. 즉, 온라인용 Git repository를 만들 필요가 없다는 말이지요.

첫 장에서 본 명령어를 다시 한번 해봅시다:

$ git diff 1b6d > my.patch

위 명령어는 간단한 패치를 생성하여 이메일로 보낼수 있게 도와줍니다. Git 저장소에서 다음을 따라해보세요:

$ git apply < my.patch

위 명령어를 사용하여 패치를 적용시킵니다.

작성자의 이름과 싸인이 기록되어야하는 좀 더 공식적인 환경에서는 그에 상응하는 패치 (1b6d 이후의 작업)를 만들기위해 다음 명령어를 사용합니다:

$ git format-patch 1b6d

이렇게 만든 파일묶음은 *git-send-email*을 사용하여 보낼 수 있습니다. 보내고싶은 commit 묶음을 수동으로 지정해줄 수도 있습니다:

$ git format-patch 1b6d..HEAD^^

받는 쪽에서는 이메일을 받을 때는 받은 txt형태의 패치 파일을 저장한 후:

$ git am < email.txt
이 명령어는 새로받은 패치를 적용시키고 작성자의 정보가 포함된 새로운 commit을 만듭니다.

브라우저 상으로 이메일을 수신한다면, 당신의 이메일 클라이언트에서 첨부된 패치의 본 코드의 형태를 확인해야 할수도 있습니다.

mbox-를 기반으로하는 이메일 클라이언트는 약간의 문제점들이 있습니다. 그러나 이런 방식의 클라이언트를 쓸만한 사람이라면 손쉽게 튜토리얼을 읽지않고도 해결할 수 있을것입니다.

죄송합니다. 주소를 옮겼습니다

Repository를 클로닝한 후 *git push*나 *git pull*을 사용하면 원래의 URL에서 해당 명령어를 실행합니다. Git은 어떤 원리로 이렇게 하는 것일까요? 그 비밀은 클론을 만들때 생선된 config 옵션에서 찾을 수 있습니다. 한번 볼까요?:

$ git config --list

remote.origin.url 옵션은 URL 소스를 통제합니다; "origin"은 원래 repository에 붙여진 별명이라고 보면됩니다. Branch에 "master"라고 이름이 붙듯이 말이죠. 그말은 이 이름을 바꾸거나 지울 수 있는데 할 필요는 없다는 것입니다.

제일 처음 사용하던 repository가 옮겨지면, URL을 수정해 주어야 합니다:

$ git config remote.origin.url git://new.url/proj.git

brach.master.merge 옵션은 *git pull*로 당겨올 수 있는 branch를 설정하여 줍니다. 처음으로 클론을 생성하였을때, 그 클론의 branch는 그 클론을 만들어온 repository의 현재 사용중인 repository와 같게 설정이 되어있습니다. 그렇기 때문에 현재 작업 헤드가 다른 branch로 옮겨갔었다고 하더라도, 추후의 당겨오기는 본래의 branch를 따를 수 있게 해줄 것 입니다.

본 옵션은 처음에 +branch.master.remote+옵션에 기록되어 있는 클론의 대상 repository에만 적용됩니다. 다른 저장소에서 당겨오기를 실행한다면, 구체적으로 어떤 나뭇가지에서 당겨오길 원하는지 설정해주어야 합니다:

$ git pull git://example.com/other.git master

이 것은 왜 전에 보여드렸던 push*와 *pull 예제에 다른 argument가 붙지 않았었는지 설명하여 줍니다.

원격 branch들

어떠한 repository를 클론할 때에는 그 클론의 모든 branch를 클론하게 됩니다. Git은 이 사실을 은폐하기에 당신은 클론을 하면서 몰랐을지도 모릅니다: 그러니 당신은 직접 Git에게 물어보아야 합니다. 이 설정은 원격 repository에 있는 branch들은 당신의 branch들과 꼬이게하는 일을 없게 해줍니다. 그래서 초보자들이 Git을 사용할 수 있는 것이고요.

다음 명령어를 이용하여 숨겨진 branch들을 포함해서 모두 나열합니다:

$ git branch -r

당신은 다음과 비슷한 결과물들을 보게될 것입니다:

origin/HEAD
origin/master
origin/experimental

이 결과는 각 행마다 원격 repository의 HEAD와 branch를 보여주며, 다른 Git 명령어들과 함께 사용될 수 있습니다. 예를 들면, 당신은 지금 많은 commit을 하였다고 먼저 가정합니다. 그러고는 가장 최근에 가져온 버젼과 비교를 하고싶다고 생각해봅니다. SHA1 해쉬를 찾아서 확인할 수도 있지만 다음 명령어로 더 간단히 비교할 수 있습니다:

$ git diff origin/HEAD

아니면 "experimental" 나뭇가지가 지금 어떠한 상태인지 알아낼 수도 있습니다.

$ git log origin/experimental

다수의 원격 repository

당신 외의 두명의 개발자가 프로젝트를 공동으로 진행하고 있다고 가정합니다. 그리고 그 둘의 작업상황을 주시하고 싶습니다. 당신은 다음 명령어를 사용함으로써 하나 이상의 repository를 추적할 수 있습니다:

$ git remote add other git://example.com/some_repo.git
$ git pull other some_branch

이제 두번째 repository의 branch로 병합을 시도하였으며 모든 repository의 모든 branch에 대한 접근권한이 생겼습니다.

$ git diff origin/experimental^ other/some_branch~5

그러나 내 작업과 관련없이 버전의 변화를 비교해내는 방법은 무엇일까요? 풀어말하자면 그들의 branch를 보는 동시에 그들의 작업이 내 작업에 영향받지않게 하고싶다는 것입니다. 그렇다면 간단하게 당겨오기 보다는:

$ git fetch        # 태초의 repository로부터 물어옵니다. 디폴트 명령어.
$ git fetch other  # 다른 개발자의 repository를 물어옵니다.

작업기록들만을 가져오는 명령어들입니다. 현재 작업중인 디렉토리는 영향을 받지않을 것이지만, 로컬 사본을 가지고 있기에 우리는 이제 어느 repository의 어떤 branch라도 Git 명령어를 사용하여 활용할 수 있습니다.

Pull은 간단히 풀어서 설명하면 fetch(물어오기) 후 *merge(병합하기)*를 합친 하나의 고급명령어라고 말할 수 있습니다. 우리는 마지막 으로 한 commit을 현재 작업에 병합하길 원하기 때문에 주로 *pull(당겨오기)*를 사용하게 될 것입니다; 위에 설명한 상황은 특수상황이지요.

*git help remote*에는 원격 repository를 삭제하는 방법, 특정 branch를 무시하는 방법 외에 많은 것을 볼 수 있습니다.

나만의 취향

저는 작업을 할때 다른 개발자들이 제가 당겨오기를 실행할 수 있게 항시 준비해두는 것을 선호합니다. 어떠한 Git 호스팅 서비스는 클릭 한 번만으로도 쉽게 이를 행할 수 있게 도와주는 것도 있습니다.

어떤 파일꾸러미를 물어온 후에는 Git 명령어들을 사용하여 프로젝트가 잘 정리되어 있길 빌며 변화 기록을 조회합니다. 그러고는 저의 작업을 병합합니다. 그 후 제 작업이 맘에 들 경우 메인 저장소에 밀어넣기 합니다.

제가 다른 사람들로부터 많은 도움을 받는 스타일은 아니지만, 이러한 제 작업방식을 추천드리고 싶습니다. 다음 링크를 한 번보세요. Linus Torvalds의 블로그 포스팅.

Git의 세상에 거주하는 것은 패치 파일들을 만들어 배포하는 것보다 더 효율적입니다. Git은 단순한 버전관리 외에도 작업을 행한 사람의 이름, 이메일주소, 작업날짜를 같이 기록하여줍니다. == Git 마스터링 ==

지금까지 배운것만으로도 당신은 git help 페이지를 자유롭게 돌아다니며 거의 모든 것을 이해할 수 있을 것입니다. 그러나 어떠한 문제를 풀기위해 어느 한 가지의 알맞는 명령어를 찾는 것은 아직 성가실 수 있습니다. 그런 문제에 대해 제가 도와줄 수 있을 것 같습니다: 아래는 제가 Git을 사용하며 유용하게 썼던 몇가지 팁들입니다.

소스 배포

제 프로젝트에서 Git은 제가 저장 및 공개하고 싶은 파일들을 정확히 추적하여 줍니다.

$ git archive --format=tar --prefix=proj-1.2.3/ HEAD

바뀐 것은 꼭 commit

Git에게 무엇을 추가, 삭제 및 파일이름을 바꾸었는지 일일히 알려주는 것은 귀찮은 짓일지도 모릅니다. 대신에 당신은 다음 명령어를 쓸 수있습니다:

$ git add .
$ git add -u

Git은 현재 작업중인 디렉토리에 있는 파일들을 자동으로 살피며 자세한 사항들을 기록하여 줍니다. 위의 두번째 명령어 (git add -u) 대신에 'git commit -a’를 사용하여 그 명령어와 commit을 동시에 해낼 수 있습니다. *git help ignore*를 참고하여 특별히 지정된 파일을 무시하는 방법을 알아보십시오.

위의 모든 것을 한 줄의 명령어로 실행할 수 있습니다.

$ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

-z*와 *-0 옵션은 파일이름이 이상한 문자를 포함하고 있을 때 생길 수 있는 여러가지 문제점들을 처리하여 줍니다. 이 옵션들은 무시된 파일들을 포함하여줌으로써 '-x’아니면 '-X’을 같이 써주어야 할 것입니다.

내 commit이 너무 클 경우?

Commit을 한지 시간이 좀 많이 지난 상황입니까? 코딩을 너무 열심히 한 나머지 버전컨트롤하는 것을 깜빡했나요? 프로젝트에 여러가지 연관성없는 수정을 한 상태입니까?

걱정하지말고:

$ git add -p

당신이 만든 모든 수정작업에 대하여 Git은 어떠한 것들이 바뀌였는지 코드로 보여주며 당신에게 다음에 실행할 commit에 부분적인 코드가 포함될 사항인지 물어볼 것입니다. "y"와 "n"를 이용하여 대답할 수 있습니다. 물론 이 대답을 당장하지 않아도 됩니다; "?"로 좀 더 알아보십시요.

모든 수정작업이 마음에 든다면:

$ git commit

위의 간단한 명령어를 사용하여 직접 선택한 작업만을 commit합니다. 이 상황에선 반드시 *-a*옵션을 생략하시길 바랍니다. 그렇지 않으면 Git은 모든 수정작업을 commit할 것입니다.

만약에 여러군데 다른 디렉토리에 많은 수정작업을 해놓았다면 어떻게 할까요? 수정된 사항을 하나씩 검토하는 작업은 정말 귀찮은 짓입니다. 이럴땐 *git add -i*를 사용합니다. 몇 번의 타이핑만으로도 특정 파일의 수정작업을 검토할 수 있게됩니다. 또는 *git commit --interactive*를 사용하여 작업 중 자동으로 작업 후 commit하는 방법도 있을 수 있습니다.

인덱스: Git의 스테이징 구역

여태까지 Git의 중요한 기능인 'index’를 피해왔지만 이제 한 번 살펴본 시간이 온 것 같습니다. 인덱스는 임시적인 스테이징 구역 (번역주:책갈피처럼)으로 보면 됩니다. Git은 당신의 프로젝트와 프로젝트의 기록 사이에 데이터를 직접 옮기는 경우는 드뭅니다. 대신, Git은 인덱스에 파일을 쓰며 그리고 그 파일들을 마지막 목표지점에 카피하여 줍니다.

예를 들어 commit -a*는 원래 두단계 과정을 거치는 하나의 명령어입니다. 첫번째 단계에서는 현 작업상황의 스냅샷을 찍어 모든 파일들을 인덱스하는 과정을 거칩니다. 두번째 단계에서는 방금 찍은 스냅샷을 영구적으로 보관하게 됩니다. *-a 옵션을 쓰지않고 commit을 하는 것은 이 두번째 과정만 실행하는 일입니다. 그렇기에 git add 같은 명령어를 쓴 후에 commit을 하는 것이 당연한 이야기가 되겠지요.

대체적으로 인덱스에 관한 컨셉트는 무시하고 파일기록에서 직접적으로 쓰기와 읽기가 실행된다는 개념으로 이해하면 편합니다. 이런 경우에는 우린 인덱스를 제어하는 것 처럼 좀 더 세부한 제어를 하기 원할것입니다. 부분적인 스냅샷을 찍은 후 영구적으로 이 '부분스냅샷’을 보존하는 것이죠.

머리(HEAD)를 잃어버리지 않기

HEAD 태그는 문서작업시 보이는 커서처럼 마지막 commit 포인트를 가르키는 포인터 역할을 합니다. Commit을 실행할 때마다 물론 HEAD도 같이 앞으로 움직이겠지요. 어떤 Git 명령어들은 수동으로 HEAD를 움직일 수 있게 해줍니다. 예를 들면:

$ git reset HEAD~3

위 명령어를 사용한다면 HEAD를 commit을 3번 하기 전으로 옮깁니다. 이 후 모든 Git 명령어는 가지고 있던 파일은 현재상태로 그대로 두되 그 3번의 commit을 하지 않은 것으로 이해하죠.

그러나 어떻게 해야 다시 가장 최근으로 돌아갈 수 있을까요? 예전에 했던 commit들은 미래에 대해서 아무것도 모를텐데 말이지요.

원래의 HEAD의 SHA1을 가지고 있다면:

$ git reset 1b6d

그러나 이 것을 어디에도 써두지 않았었더라도 걱정하지 마십시오: Git은 이런 경우를 대비해서 원래의 HEAD를 ORIG_HEAD로 어딘가에는 저장하여 둡니다. 그러고는 다음명령어를 사용하여 미래로 안전하게 돌아올 수 있지요:

$ git reset ORIG_HEAD

HEAD-헌팅

ORIG_HEAD로 돌아가는 것만으로는 충분하지 않을지도 모릅니다. 당신은 방금 엄청나게 큰 실수를 발견하였고 아주 오래전에 했던 commit으로 돌아가야 할지 모릅니다.

기본적으로 Git은 branch를 수동으로 삭제하였더라도 2주의 시간동안 commit을 무조건 저장하여 둡니다. 문제는 돌아가고 싶은 commit의 hash를 찾는 일입니다. '.git/objects’의 모든 hash 값을 조회하여 얻어걸릴 때까지 해보는 방법이 있긴합니다만, 여기 좀 더 쉬운 방법이 있습니다.

Git은 모든 commit의 hash를 '.git/logs’에 저장해 둡니다. 하위 디렉토리 'refs’은 모든 branch의 활동기록을 저장하여두고 동시에 'HEAD’파일은 모든 hash 값을 저장하고 있습니다. 후자는 실수로 마구 건너 뛴 commit들의 hash도 찾을 수 있게 해줍니다.

reflog 명령어는 당신이 사용하기 쉬운 유저인터페이스로 log파일들을 표현하여 줍니다. 다음 명령어를 사용하여 보십시오.

$ git reflog

hash를 reflog으로부터 자르고 붙여넣기 보다는:

$ git checkout "@{10 minutes ago}"

아니면 5번 째 전에 방문했던 commit으로 돌아갈수도 있습니다:

$ git checkout "@{5}"

좀 더 많은 정보는 *git help rev-parse*의 "재편집 구체화하기" 섹션을 참고하십시오.

아까 언급한 commit의 2주살이 생명을 수동으로 연장하여 줄 수 있습니다. 예를 들어:

$ git config gc.pruneexpire "30 days"

위 명령어는 30일이 지난 후에 지워진 commit들 역시 영구적으로 삭제된다는 의미입니다. 그러고는 *git gc*가 실행되지요.

*git gc*가 자동실행되는 것을 꺼줄 수도 있습니다:

$ git config gc.auto 0

이 경우에는 *git gc*를 수동적으로 실행시켜 commit들을 삭제할 수 있지요.

Git을 좀 더 발전시키는 방법

진정한 UNIX와 같이 Git의 디자인은 다른 프로그램들의 GUI, 웹 인터페이스와 같은 하위파트들과 호환이 됩니다. 어느 Git 명령어들은 유명인사의 어깨위에 서있는 것처럼 Git 그 자체가 스크립팅 언어로 사용될 수도 있습니다. 조금만 시간을 투자하면 Git은 당신의 기호에 더 알맞게 바꿀수 있습니다.

한 가지 트릭을 소개하자면 자주 사용할것 같은 명령어들을 짧게 만들어줄 수 있는 방법이 있습니다:

$ git config --global alias.co checkout
$ git config --global --get-regexp alias  # 현재 설정한 명령어들의 '가명'을 표기해줍니다.
alias.co checkout
$ git co foo                              # 'git checkout foo'와 같은 것입니다.

또 다른 트릭은 현재 작업중인 branch의 이름을 작업표시창에 표시하여주는 명령어도 있습니다.

$ git symbolic-ref HEAD

위 명령어는 현재 작업중인 branch 이름을 표기하여 줍니다. 실제로는 "refs/heads/"를 없애고 잠재적으로 일어날 수 있는 에러들을 무시하는걸 추천드립니다:

$ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

contrib 하위 디렉토리는 유용한 Git 툴들이 저장되어있는 장소이기도 합니다. 시간이 지나면 이곳에 있는 툴들은 공식적으로 인정받아 고유명령어가 생기기도 하겠지요. Debian과 Ubuntu에서는 이 디렉토리는 +/usr/share/doc/git-core/contrib+에 위치하고 있습니다.

앞으로 +workdir/git-new-workdir+디렉토리에 방문할 일도 많을 것입니다. 시스템링크 기술을 통해서 이 스크립트는 원래의 repository와 작업기록을 공유하는 새로운 작업 디렉토리를 생성하여 줍니다:

$ git-new-workdir an/existing/repo new/directory

새롭게 생성된 디렉토리는 클론으로 봐도 무방하며 일반클론들과의 한가지 차이점은 어느 한 곳에서 작업을 하던 두 개의 디렉토리는 앞으로 계속 싱크를 진행하며 같은 기록을 가지게 된다는 것입니다. 즉, 병합, 밀어넣기, 당겨오기를 해줄 필요가 없어지는 것이지요.

용감한 스턴트

Git은 요즘 유저들이 데이터를 쉽게 지우지 못하도록 하고 있습니다. 그러나 몇가지의 상용적인 명령어를 통해서 이런 Git만의 방화벽 쯤은 쉽게 뚫어버릴 수 있지요.

Checkout: Commit하지 않은 작업들은 checkout을 할 수없습니다. 방금작업한 모든 것들을 없던 일로하고 그래도 굳이 commit을 진행하고 싶다면:

$ git checkout -f HEAD^

반면에 checkout을 할 위치를 수동으로 설정하여 준다면 Git의 방화벽은 처음부터 작동하지 않을 것입니다. 설정해준 위치는 조용히 덮어씌우게 됩니다. 그러니, 이런 방식으로 checkout을 할 때에는 주의 하십시오.

Reset: 리셋은 commit되지 않은 작업이 있으면 실행되지 않을 것입니다. 그래도 강제로 하고싶다면:

$ git reset --hard 1b6d

Branch: 방금한 작업을 잃어버릴 것같으면 Git은 branch가 지워지지 않게합니다. 그래도 하고싶다면:

$ git branch -D dead_branch  # -d 대신 -D

비슷한 방식으로, commit을 안한 작업이 있어서 move명령어를 통해서 덮어씌우기가 안될경우에는:

$ git branch -M source target  # -m 대신 -M

체크아웃과 리셋과는 다르게 위의 두 명령어는 데이터를 직접 삭제하진 않습니다. 모든 변경기록은 .git 하위 디렉토리에 남게되고 필요한 hash는 '.git/logs’에서 찾을 수 있습니다 (위의 "HEAD-헌팅" 섹션 참고). 기본설정상, 이 기록들은 2주 동안은 삭제되지 않습니다.

Clean: 몇 git 명령어들은 추적되지 않은 파일들을 망쳐놓을까봐 실행이 안되는 경우가 종종 있습니다. 만약에 그 파일들이 삭제되도 된다는 확신이 선다면, 가차없이 다음 명령어를 사용하여 삭제하십시오:

$ git clean -f -d

이 후에는 위 모든 명령어들은 다시 잘 실행되기 시작할 것입니다!

원치않는 commit들을 방지하기

바보같은 실수들은 내 repository를 망쳐놓곤 합니다. 그 중에서도 제일 무서운 것은 *git add*를 쓰지 않아서 작업해놓은 파일들을 잃어버리는 것이지요. 그나마 코드 뒤에 빈 공간을 마구 넣어놓는다던지 병합에서 일어날 수 있는 문제들을 해결해 놓지않는 것은 애교로 보입니다: 별로 문제가 되는 것들은 아니지만 남들이 볼 수 있는 repository에서는 보여주기 싫습니다.

hook 을 사용하는 것과 같이 제가 바보같은 짓을 할 때마다 경고를 해주는 기능이 있다면 얼마나 좋을까요:

$ cd .git/hooks
$ cp pre-commit.sample pre-commit  # 예전 Git 버젼에서는: chmod +x pre-commit

이제는 아까 설명했던 애교스러운 실수들이 발견될 때 Git은 commit을 도중에 그만 둘것입니다.

이 가이드에서는 pre-commit 앞에 밑에 써놓은 코드를 넣음으로써 혹시 있을지도 모르는 바보같은 짓을 방지하였습니다.

if git ls-files -o | grep '\.txt$'; then
  echo FAIL! Untracked .txt files.
  exit 1
fi

많은 git 작업들은 hook과 상호작용합니다; git help hooks*를 참조하십시오. 우리는 "HTTP를 통한 Git"을을 설명할때 *post-update hook을 활성화시켰습니다. HEAD가 옮겨질때마다 같이 실행되지요. Git over HTTP 예제에서는 post-update 스크립트가 통신에 필요한 Git을 업데이트 했었습니다.