Similar documents
git CLI 로간단하게조작하기! by 윤선지

github_introduction.key


슬라이드 1

리눅스기초

PowerPoint 프레젠테이션

<3833C8A35FB0F8C7D05FC6AEB7BBB5E55F F466C6F77B8A65FC8B0BFEBC7D15FC8BFB0FAC0FBC0CE5FBCD2BDBA5FC7FCBBF35FB0FCB8AE5F F322E687770>

PowerPoint Presentation

PowerPoint 프레젠테이션

PowerPoint 프레젠테이션

untitled

PowerPoint 프레젠테이션

SourceTree 를이용한 Git 사용법 1

슬라이드 1

svn 을능숙하게다루던능력자들처음 git 을만나면대게이런표정이죠.

<31332DB9E9C6AEB7A2C7D8C5B72D3131C0E528BACEB7CF292E687770>

GIT/GITHUB 사용 1 Git & GitHub 튜토리얼 출처 : [Studio Rini ] Git 을보통어떻게사용하는지간략한 Flow 를보겠습니다. 1. 새프로젝트를생성, 프로젝트폴더에 g

슬라이드 1

Microsoft PowerPoint - chap01-C언어개요.pptx

슬라이드 1

Microsoft Word - ntasFrameBuilderInstallGuide2.5.doc

1) 인증서만들기 ssl]# cat > // 설명 : 발급받은인증서 / 개인키파일을한파일로저장합니다. ( 저장방법 : cat [ 개인키

YUM(Yellowdog Updater,Modified) : RPM 패키지가저장된서버 ( 저장소 ) 로부터원하는패키지를자동으로설치한다. : YUM 도구는 RPM 의패키지의존성문제를해결

1) 인증서만들기 ssl]# cat > // 설명 : 발급받은인증서 / 개인키파일을한파일로저장합니다. ( 저장방법 : cat [ 개인키

SIGIL 완벽입문

Software Verification Team 오준 임국현 주영진 김슬기

Microsoft PowerPoint - chap02-C프로그램시작하기.pptx

/chroot/lib/ /chroot/etc/

[ 컴퓨터시스템 ] 3 주차 1 차시. 디렉토리사이의이동 3 주차 1 차시디렉토리사이의이동 학습목표 1. pwd 명령을사용하여현재디렉토리를확인할수있다. 2. cd 명령을사용하여다른디렉토리로이동할수있다. 3. ls 명령을사용하여디렉토리내의파일목록을옵션에따라다양하게확인할수

Microsoft Word - windows server 2003 수동설치_non pro support_.doc

View Licenses and Services (customer)

슬라이드 1

소프트웨어설치 1. 소프트웨어설치및제거 ( 소스코드 ) 소스코드컴파일을이용한 S/W 설치 1. 소스코드다운로드 - 예 ) httpd tar.gz - 압축해제 : #tar xzvf httpd tar.gz - INSTALL 또는 README파일참조

Microsoft PowerPoint - 08_(Linux)_(Fundamental)_Version_Control_Systems

System Recovery 사용자 매뉴얼

단계

Microsoft PowerPoint 웹 연동 기술.pptx

RHEV 2.2 인증서 만료 확인 및 갱신

1. 안드로이드개발환경설정 안드로이드개발을위해선툴체인을비롯한다양한소프트웨어패키지가필요합니다 툴체인 (Cross-Compiler) 설치 안드로이드 2.2 프로요부터는소스에기본툴체인이 prebuilt 라는이름으로포함되어있지만, 리눅스 나부트로더 (U-boot)

Windows 8에서 BioStar 1 설치하기

문서의 제목 나눔고딕B, 54pt

PowerPoint 프레젠테이션

Microsoft Word - 3부A windows 환경 IVF + visual studio.doc

Tablespace On-Offline 테이블스페이스 온라인/오프라인

ISP and CodeVisionAVR C Compiler.hwp

슬라이드 1

리눅스설치가이드 3. 3Rabbitz Book 을리눅스에서설치하기위한절차는다음과같습니다. 설치에대한예시는우분투서버 기준으로진행됩니다. 1. Java Development Kit (JDK) 또는 Java Runtime Environment (JRE) 를설치합니다. 2.

28 THE ASIAN JOURNAL OF TEX [2] ko.tex [5]

아이콘의 정의 본 사용자 설명서에서는 다음 아이콘을 사용합니다. 참고 참고는 발생할 수 있는 상황에 대처하는 방법을 알려 주거나 다른 기능과 함께 작동하는 방법에 대한 요령을 제공합니다. 상표 Brother 로고는 Brother Industries, Ltd.의 등록 상

Microsoft Word - Armjtag_문서1.doc

슬라이드 1

4. Compass 명령어를알아보자. compass <command> [<option>, <option>, <option>.. <option>] command : 명령어. clean - Remove generated files and the sass cache. com

Secure Programming Lecture1 : Introduction

PowerPoint Template

문서의 제목 나눔고딕B, 54pt

Microsoft PowerPoint Android-SDK설치.HelloAndroid(1.0h).pptx

작성자 : 기술지원부 김 삼 수

Microsoft Word - CAE 클러스터 환경 구축-ABAQUS.doc

Microsoft PowerPoint 통신 및 압축 명령어.ppt

2) 활동하기 활동개요 활동과정 [ 예제 10-1]main.xml 1 <LinearLayout xmlns:android=" 2 xmlns:tools="

세계 비지니스 정보

메일서버등록제(SPF) 인증기능적용안내서 (HP-UX - postfix) OS Mail Server SPF 적용모듈 (Perl 기반) 작성기준 HP-UX 11.11i postfix spf-filter 년 6 월

다. 최신 버전의 rpm 패키지 버전을 다운로드해 다음과 같이 설 치한다. 단 debuginfo의 rpm 패키지는 설치할 필요가 없다. 하기 위한 옵션이고, init는 저장소를 초기화하기 위한 cvs 명령 어이다. - 새로 설치한 경우 : rpm -ivh cvs* -

커알못의 커널 탐방기 이 세상의 모든 커알못을 위해서

항목

Network Security - Wired Sniffing 실습 ICNS Lab. Kyung Hee University

네이버 오픈소스 세미나 key

Title Layout

HLS(HTTP Live Streaming) 이용가이드 1. HLS 소개 Apple iphone, ipad, ipod의운영체제인 ios에서사용하는표준 HTTP 기반스트리밍프로토콜입니다. 2. HLS 지원대상 - 디바이스 : iphone/ipad/ipod - 운영체제 :

Discrete Mathematics

Contributors: Myung Su Seok and SeokJae Yoo Last Update: 09/25/ Introduction 2015년 8월현재전자기학분야에서가장많이쓰이고있는 simulation software는다음과같은알고리즘을사용하고있다.

메일서버등록제(SPF) 인증기능적용안내서 (AIX - sendmail) OS Mail Server SPF 적용모듈 (Perl 기반) 작성기준 AIX 5.3 sendmail spf-filter 년 6 월

User Guide

표준프레임워크 Nexus 및 CI 환경구축가이드 Version 3.8 Page 1

임베디드디바이스개발시커널로리눅스를많이사용하는데, 그때그커널과함께리눅스명령어도필요하게된다. 모든명령어를지원하기위해서는다수의개별적인패키지들이필요하다. 한프로젝트에서그많은명령어를한번에지원해준다면자원제약적인임베디드환경에서공간효율성이극대화될것이다. 상대적으로경량화된리눅스명령어세

Raspbian 설치 라즈비안 OS (Raspbian OS) 라즈베리파이 3 Model B USB 마우스 USB 키보드 마이크로 SD 카드 마이크로 SD 카드리더기 HDM I 케이블모니터

Snort Install Manual Ad2m VMware libnet tar.gz DebianOS libpcap tar.gz Putty snort tar.gz WinSCP snort rules 1. 첫번째로네트워크설정 1) ifconf

JVM 메모리구조

지난시간에... 우리는 kernel compile을위하여 cross compile 환경을구축했음. UBUNTU 12.04에서 arm-2009q3를사용하여 간단한 c source를빌드함. 한번은 intel CPU를위한 gcc로, 한번은 ARM CPU를위한 gcc로. AR

PowerPoint 프레젠테이션

메일서버등록제(SPF) 인증기능적용안내서 (HP-UX - qmail) OS Mail Server SPF 적용모듈 (Perl 기반) 작성기준 HP-UX 11.11i qmail 1.03 spf-filter 년 6 월

리눅스 프로세스 관리

슬라이드 1

경우 1) 80GB( 원본 ) => 2TB( 복사본 ), 원본 80GB 는 MBR 로디스크초기화하고 NTFS 로포맷한경우 복사본 HDD 도 MBR 로디스크초기화되고 80GB 만큼포맷되고나머지영역 (80GB~ 나머지부분 ) 은할당되지않음 으로나온다. A. Window P

PowerPoint 프레젠테이션

iii. Design Tab 을 Click 하여 WindowBuilder 가자동으로생성한 GUI 프로그래밍환경을확인한다.

쉽게 풀어쓴 C 프로그래밍

Install stm32cubemx and st-link utility

Windows Server 2012

본교재는수업용으로제작된게시물입니다. 영리목적으로사용할경우저작권법제 30 조항에의거법적처벌을받을수있습니다. [ 실습 ] 스위치장비초기화 1. NVRAM 에저장되어있는 'startup-config' 파일이있다면, 삭제를실시한다. SWx>enable SWx#erase sta

SQL Developer Connect to TimesTen 유니원아이앤씨 DB 기술지원팀 2010 년 07 월 28 일 문서정보 프로젝트명 SQL Developer Connect to TimesTen 서브시스템명 버전 1.0 문서명 작성일 작성자

슬라이드 1

Microsoft PowerPoint UNIX Shell.pptx

EndNote X2 초급 분당차병원도서실사서최근영 ( )

Chapter 05. 파일접근권한관리하기

Microsoft PowerPoint - comp_prac_081223_2.pptx

Microsoft PowerPoint UNIX Shell.ppt

목차 윈도우드라이버 1. 매뉴얼안내 운영체제 (OS) 환경 윈도우드라이버준비 윈도우드라이버설치 Windows XP/Server 2003 에서설치 Serial 또는 Parallel 포트의경우.

저작자표시 - 비영리 - 동일조건변경허락 2.0 대한민국 이용자는아래의조건을따르는경우에한하여자유롭게 이저작물을복제, 배포, 전송, 전시, 공연및방송할수있습니다. 이차적저작물을작성할수있습니다. 다음과같은조건을따라야합니다 : 저작자표시. 귀하는원저작자를표시하여야합니다. 비

C# Programming Guide - Types

Transcription:

Pro Git Scott Chacon * 2013-12-19 * 지금보시는문서는 Pro Git 책에대한 PDF 파일입니다. 본문서는 Creative Commons 저작자표시-비영리조건-동일조건변경허락 3.0(Creative Commons Attribution-Non Commercial-Share Alike 3.0) 라이센스를따릅니다. 여러분이 Git을이해하는데도움이되기를희망합니다. 종이로출판된책을 Amazon 웹사이트 http://tinyurl.com/amazonprogit 에서구입하여저를비롯한 Apress에도도움을주시기를기대하겠습니다. 한국어번역판도인사이트를통해종이로출판됐습니다. 한국어번역에대한자세한정보는 https://github.com/ progit/progit/tree/master/ko에서확인바람니다.

목차 1 시작하기 1 1.1 버전관리란?.................................... 1 1.1.1 로컬버전관리시스템............................ 1 1.1.2 중앙집중식버전관리시스템........................ 1 1.1.3 분산버전관리시스템............................ 2 1.2 짧게보는 Git의역사................................ 3 1.3 Git 기초....................................... 4 1.3.1 델타가아니라스냅샷............................ 4 1.3.2 거의모든명령을로컬에서실행....................... 4 1.3.3 Git의무결성................................ 5 1.3.4 Git은데이터를추가할뿐.......................... 5 1.3.5 세가지상태................................. 5 1.4 Git 설치....................................... 6 1.4.1 소스코드로설치하기............................ 6 1.4.2 리눅스에설치................................ 7 1.4.3 Mac에설치하기.............................. 7 1.4.4 윈도에설치................................. 8 1.5 Git 최초설정.................................... 8 1.5.1 사용자정보................................. 9 1.5.2 편집기................................... 9 1.5.3 Diff 도구.................................. 9 1.5.4 설정확인.................................. 9 1.6 도움말보기..................................... 10 1.7 요약........................................ 10 2 Git의기초 11 2.1 Git 저장소만들기.................................. 11 2.1.1 기존디렉토리를 Git 저장소로만들기.................... 11 2.1.2 기존저장소를 Clone하기.......................... 12 2.2 수정하고저장소에저장하기............................. 12 2.2.1 파일의상태확인하기............................ 12 2.2.2 파일을새로추적하기............................ 13 2.2.3 Modified 상태의파일을 Stage하기..................... 14 2.2.4 파일무시하기................................ 16 2.2.5 Staged와 Unstaged 상태의변경내용을보기............... 17 2.2.6 변경사항커밋하기............................. 19 iii

2.2.7 Staging Area 생략하기........................... 20 2.2.8 파일을삭제하기............................... 21 2.2.9 파일이름변경하기............................. 22 2.3 커밋히스토리조회하기............................... 23 2.3.1 조회제한조건................................ 28 2.3.2 GUI 도구로히스토리를시각화하기..................... 30 2.4 되돌리기...................................... 30 2.4.1 커밋수정하기................................ 30 2.4.2 파일상태를 Unstage로변경하기...................... 31 2.4.3 Modified 파일되돌리기.......................... 32 2.5 리모트저장소.................................... 32 2.5.1 리모트저장소확인하기........................... 33 2.5.2 리모트저장소추가하기........................... 34 2.5.3 리모트저장소를 Pull 하거나 Fetch 하기................... 34 2.5.4 리모트저장소에 Push하기......................... 35 2.5.5 리모트저장소살펴보기........................... 35 2.5.6 리모트저장소이름을바꾸거나리모트저장소를삭제하기.......... 36 2.6 태그........................................ 37 2.6.1 태그조회하기................................ 37 2.6.2 태그붙이기................................. 37 2.6.3 Annotated 태그.............................. 37 2.6.4 태그에서명하기............................... 38 2.6.5 Lightweight 태그............................. 39 2.6.6 태그검증하기................................ 40 2.6.7 나중에태그하기............................... 40 2.6.8 태그공유하기................................ 41 2.7 팁과트릭...................................... 42 2.7.1 자동완성.................................. 42 2.7.2 Git Alias.................................. 43 2.8 요약........................................ 44 3 Git 브랜치 45 3.1 브랜치란무엇인가?................................. 45 3.2 브랜치와 Merge의기초............................... 49 3.2.1 브랜치의기초................................ 50 3.2.2 Merge의기초............................... 53 3.2.3 충돌의기초................................. 54 3.3 브랜치관리..................................... 57 3.4 브랜치 Workflow.................................. 58 3.4.1 Long-Running 브랜치........................... 58 3.4.2 토픽브랜치................................. 59 3.5 리모트브랜치.................................... 60 3.5.1 Push하기.................................. 61 3.5.2 브랜치추적................................. 64 3.5.3 리모트브랜치삭제............................. 64 3.6 Rebase하기.................................... 65 iv

3.6.1 Rebase의기초............................... 65 3.6.2 좀더 Rebase................................ 67 3.6.3 Rebase의위험성.............................. 69 3.7 요약........................................ 71 4 Git 서버 73 4.1 프로토콜...................................... 73 4.1.1 로컬프로토콜................................ 73 장점.................................... 74 단점.................................... 74 4.1.2 SSH 프로토콜................................ 75 장점.................................... 75 단점.................................... 75 4.1.3 Git 프로토콜................................ 75 장점.................................... 76 단점.................................... 76 4.1.4 HTTP/S 프로토콜............................. 76 장점.................................... 77 단점.................................... 77 4.2 서버에 Git 설치하기................................. 77 4.2.1 서버에 Bare 저장소넣기.......................... 78 4.2.2 바로설정하기................................ 78 SSH 접근.................................. 78 4.3 SSH 공개키만들기................................. 79 4.4 서버에설정하기................................... 80 4.5 공개하기...................................... 82 4.6 GitWeb...................................... 83 4.7 Gitosis....................................... 85 4.8 Gitolite....................................... 89 4.8.1 설치하기.................................. 89 4.8.2 자신에게맞게설치하기........................... 90 4.8.3 설정파일과접근제어규칙.......................... 90 4.8.4 deny 규칙을꼼꼼하게제어하기...................... 92 4.8.5 파일단위로 Push를제어하기........................ 92 4.8.6 Personal 브랜치.............................. 92 4.8.7 와일드카드 저장소............................ 93 4.8.8 그밖의기능들............................... 93 4.9 Git 데몬....................................... 93 4.10 Hosted Git..................................... 95 4.10.1 GitHub................................... 96 4.10.2 계정설정하기................................ 96 4.10.3 저장소만들기................................ 96 4.10.4 Subversion으로부터코드가져오기 (Import)................ 99 4.10.5 동료추가하기................................ 99 4.10.6 내프로젝트................................. 99 4.10.7 프로젝트 Fork............................... 100 v

4.10.8 GitHub 요약................................ 101 4.11 요약........................................ 101 5 분산환경에서의 Git 103 5.1 분산환경에서의 Workflow............................. 103 5.1.1 중앙집중식 Workflow........................... 103 5.1.2 Integration-Manager Workflow..................... 104 5.1.3 Dictator and Lieutenants Workflow................... 105 5.2 프로젝트에기여하기................................ 105 5.2.1 커밋가이드라인............................... 106 5.2.2 비공개소규모팀.............................. 107 5.2.3 비공개대규모팀.............................. 112 5.2.4 공개소규모팀............................... 117 5.2.5 대규모공개프로젝트............................ 120 5.2.6 요약.................................... 123 5.3 프로젝트운영하기................................. 123 5.3.1 토픽브랜치에서일하기........................... 124 5.3.2 이메일로받은 Patch를적용하기...................... 124 apply 명령을사용하는방법......................... 124 am 명령을사용하는방법.......................... 125 5.3.3 리모트브랜치로부터통합하기........................ 127 5.3.4 무슨내용인지확인하기........................... 128 5.3.5 기여물통합하기............................... 129 Merge Workflow............................. 129 대규모 Merge Workflow.......................... 130 Rebase와 Cherry-Pick Workflow..................... 132 5.3.6 릴리즈버전에태그달기........................... 133 5.3.7 빌드넘버만들기............................... 134 5.3.8 릴리즈준비하기............................... 135 5.3.9 Shortlog 보기............................... 135 5.4 요약........................................ 136 6 Git 도구 137 6.1 리비전조회하기................................... 137 6.1.1 리비전하나가리키기............................ 137 6.1.2 짧은 SHA-1................................ 137 6.1.3 SHA-1 해시값에대한단상......................... 138 6.1.4 브랜치로가리키기............................. 139 6.1.5 RefLog로가리키기............................. 139 6.1.6 계통관계로가리키기............................ 140 6.1.7 범위로커밋가리키기............................ 142 Double Dot................................ 142 세개이상의레퍼런스............................ 143 Triple Dot................................. 143 6.2 대화형명령어.................................... 144 6.2.1 Staging Area에파일추가하고추가취소하기................ 145 vi

6.2.2 파일의일부분만 Staging Area에추가하기................. 147 6.3 Stashing...................................... 148 6.3.1 하던일을 Stash하기............................ 148 6.3.2 Stash 되돌리기............................... 151 6.3.3 Stash를적용한브랜치만들기........................ 151 6.4 히스토리단장하기................................. 152 6.4.1 마지막커밋을수정하기........................... 152 6.4.2 커밋메시지를여러개수정하기....................... 152 6.4.3 커밋순서바꾸기.............................. 154 6.4.4 커밋합치기................................. 155 6.4.5 커밋분리하기................................ 156 6.4.6 filter-branch는포크레인......................... 157 모든커밋에서파일을제거하기....................... 157 하위디렉토리를루트디렉토리로만들기................... 157 모든커밋의이메일주소를수정하기..................... 157 6.5 Git으로버그찾기.................................. 158 6.5.1 파일어노테이션............................... 158 6.5.2 이진탐색.................................. 159 6.6 서브모듈...................................... 161 6.6.1 서브모듈시작하기............................. 161 6.6.2 서브모듈이있는프로젝트 Clone하기.................... 163 6.6.3 슈퍼프로젝트................................ 166 6.6.4 서브모듈사용할때주의할점들....................... 166 6.7 Subtree Merge.................................. 168 6.8 요약........................................ 169 7 Git맞춤 171 7.1 Git 설정하기.................................... 171 7.1.1 클라이언트설정............................... 171 core.editor................................ 172 commit.template............................. 172 core.pager................................. 173 user.signingkey.............................. 173 core.excludesfile............................. 173 help.autocorrect............................. 174 7.1.2 컬러터미널................................. 174 color.ui.................................. 174 color.*................................... 174 7.1.3 다른 Merge, Diff 도구사용하기....................... 175 7.1.4 소스포맷과공백.............................. 177 core.autocrlf............................... 177 core.whitespace............................. 178 7.1.5 서버설정.................................. 179 receive.fsckobjects............................ 179 receive.denynonfastforwards..................... 179 receive.denydeletes........................... 179 vii

7.2 Git Attribute.................................... 179 7.2.1 바이너리파일................................ 180 바이너리파일이라고알려주기........................ 180 바이너리파일 Diff하기........................... 180 MS Word 파일........................... 181 OpenDocument 파일....................... 182 이미지파일............................. 183 7.2.2 키워드치환................................. 184 7.2.3 저장소익스포트하기............................ 186 export-ignore............................... 186 export-subst............................... 187 7.2.4 Merge 전략................................. 187 7.3 Git 훅........................................ 188 7.3.1 훅설치하기................................. 188 7.3.2 클라이언트훅................................ 188 커밋 Workflow 훅............................. 188 E-mail Workflow 훅............................ 189 기타훅................................... 189 7.3.3 서버훅................................... 189 pre-receive와 post-receive....................... 189 update................................... 190 7.4 정책구현하기.................................... 190 7.4.1 서버훅................................... 190 커밋메시지규칙만들기........................... 191 ACL로사용자마다다른규칙적용하기.................... 192 Fast-Forward Push만허용하기...................... 194 7.4.2 클라이언트훅................................ 196 7.5 요약........................................ 199 8 Git으로이전하기 201 8.1 Git과 Subversion................................. 201 8.1.1 git svn................................... 201 8.1.2 설정하기.................................. 202 8.1.3 시작하기.................................. 203 8.1.4 Subversion 서버에커밋하기........................ 204 8.1.5 새로운변경사항받아오기.......................... 205 8.1.6 Git 브랜치문제............................... 207 8.1.7 Subversion의브랜치............................ 208 SVN 브랜치만들기............................. 208 8.1.8 Subversion 브랜치넘나들기........................ 208 8.1.9 Subversion 명령.............................. 209 SVN 형식의히스토리............................ 209 SVN 어노테이션.............................. 209 SVN 서버정보............................... 210 Subversion에서무시하는것무시하기................... 210 8.1.10 Git-Svn 요약................................ 211 viii

8.2 Git으로옮기기................................... 211 8.2.1 가져오기.................................. 211 8.2.2 Subversion................................ 211 8.2.3 Perforce.................................. 213 8.2.4 직접 Importer 만들기............................ 215 8.3 요약........................................ 221 9 Git의내부 223 9.1 Plumbing 명령과 Porcelain 명령.......................... 223 9.2 Git 개체....................................... 224 9.2.1 Tree 개체.................................. 226 9.2.2 커밋개체.................................. 228 9.2.3 개체저장소................................. 231 9.3 Git 레퍼런스.................................... 232 9.3.1 HEAD................................... 234 9.3.2 태그.................................... 235 9.3.3 리모트레퍼런스............................... 236 9.4 Packfile...................................... 236 9.5 Refspec...................................... 240 9.5.1 Refspec Push하기............................. 241 9.5.2 레퍼런스삭제하기............................. 242 9.6 데이터전송프로토콜................................ 242 9.6.1 Dumb 프로토콜.............................. 242 9.6.2 스마트프로토콜............................... 245 데이터업로드................................ 245 데이터다운로드............................... 246 9.7 운영및데이터복구................................. 247 9.7.1 운영.................................... 247 9.7.2 데이터복구................................. 248 9.7.3 개체삭제.................................. 250 9.8 요약........................................ 253 ix

1 장 시작하기 이장에서설명하는것은 Git을처음접하는사람에게필요한내용이다. 먼저버전관리도구에대한이해와 Git을설치하는방법을설명하고마지막으로 Git 서버를설정하고사용하는방법을설명한다. 이장을다읽고나면 Git 탄생배경, Git을사용하는이유, Git을설정하고사용하는방법을터득하게될것이다. 1.1 버전관리란? 버전관리는무엇이고우리는왜이것을알아야할까? 버전관리시스템은파일변화를시간에따라기록했다가나중에특정시점의버전을다시꺼내올수있는시스템이다. 이책에서는버전관리하는예제로소스코드만보여주지만, 실제로거의모든컴퓨터파일의버전을관리할수있다. 그래픽디자이너나웹디자이너도버전관리시스템 (VCS - Version Control System) 을사용할수있다. VCS로이미지나레이아웃의버전 ( 변경이력혹은수정내용 ) 을관리하는것은매우현명하다. VCS를사용하면각파일을이전상태로되돌릴수있고, 프로젝트를통째로이전상태로되돌릴수있고, 시간에따라수정내용을비교해볼수있고, 누가문제를일으켰는지도추적할수있고, 누가언제만들어낸이슈인지도알수있다. VCS를사용하면파일을잃어버리거나잘못고쳤을때도쉽게복구할수있다. 이런모든장점을큰노력없이이용할수있다. 1.1.1 로컬버전관리시스템많은사람은버전을관리하기위해디렉토리로파일을복사하는방법을쓴다 ( 똑똑한사람이라면디렉토리이름에시간을넣을거다 ). 이방법은간단하므로자주사용한다. 그렇지만, 정말뭔가가잘못되기쉽다. 작업하던디렉토리를지워버리거나, 실수로파일을잘못고칠수도있고, 잘못복사할수도있다. 이런이유로프로그래머들은오래전에로컬 VCS라는걸만들었다. 이 VCS는아주간단한데이터베이스를사용해서파일의변경정보를관리했다. 많이쓰는 VCS 도구중에 rcs라고부르는것이있는데오늘날까지도아직많은회사가사용하고있다. Mac OS X 운영체제에서도개발도구를설치하면 RCS가함께설치된다. RCS는기본적으로 Patch Set( 파일에서변경되는부분 ) 을관리한다. 이 Patch Set은특별한형식의파일로저장한다. 그리고일련의 Patch Set을적용해서모든파일을특정시점으로되돌릴수있다. 1.1.2 중앙집중식버전관리시스템 프로젝트를진행하다보면다른개발자와함께작업해야하는경우가많다. 이럴때생기는문제를해 결하기위해 CVCS( 중앙집중식 VCS) 가개발됐다. CVS, Subversion, Perforce 같은시스템은파일 1

1 장시작하기 Scott Chacon Pro Git 그림 1.1: 로컬버전관리다이어그램 을관리하는서버가별도로있고클라이언트가중앙서버에서파일을받아서사용 (Checkout) 한다. 수 년동안이러한시스템들이많은사랑을받았다. 그림 1.2: 중앙집중식버전관리 (CVCS) 다이어그램 CVCS 환경은로컬 VCS에비해장점이많다. 프로젝트에참여한사람이면누가무엇을하고있는지알수있다. 관리자는누가무엇을할수있는지꼼꼼하게관리할수있다. 모든클라이언트의로컬데이터베이스를관리하는것보다 VCS 하나를관리하기가훨씬쉽다. 그러나 CVCS 환경은몇가지치명적인결점이있다. 가장대표적인것이중앙서버에발생한문제다. 만약서버가한시간동안다운되면그동안아무도다른사람과협업할수없고사람들이하는일을백업할방법도없다. 그리고중앙데이터베이스가있는하드디스크에문제가생기면프로젝트의모든히스토리를잃는다. 물론사람마다하나씩가진스냅샷은괜찮다. 로컬 VCS 시스템도이와비슷한결점이있고이런문제가발생하면모든것을잃는다. 1.1.3 분산버전관리시스템 DVCS( 분산버전관리시스템 ) 을설명할차례다. Git, Mecurial, Bazaar, Darcs 같은 DVCS에서는클라이언트가파일의마지막스냅샷을 Checkout하지않는다. 그냥저장소를전부복제한다. 서버에문제가생기면이복제물로다시작업을시작할수있다. 클라이언트중에서아무거나골라도서버를복원할수있다. 모든 Checkout은모든데이터를가진진정한백업이다. 2

Scott Chacon Pro Git 1.2 절짧게보는 Git 의역사 그림 1.3: 분산버전관리시스템 (DVCS) 다이어그램 게다가대부분의 DVCS 환경에서는리모트저장소가존재한다. 리모트저장소가많을수도있다. 그래 서사람들은동시에다양한그룹과다양한방법으로협업할수있다. 계층모델같은중앙집중식시스템 으로는할수없는몇가지워크플로우를사용할수있다. 1.2 짧게보는 Git 의역사 인생을살다보면여러가지일들이벌어지듯이 Git의삶또한창조적인파괴와모순속에서시작되었다. 리눅스커널은굉장히규모가큰오픈소스프로젝트다. 리눅스커널의일생에서대부분시절은패치와단순압축파일로만관리했다. 2002년에드디어리눅스커널은 BitKeeper라고불리는상용 DVCS 를사용하기시작했다. 2005년에커뮤니티가만드는리눅스커널과이익을추구하는회사가개발한 BitKeeper의관계는틀어졌다. BitKeeper의무료사용이제고된것이다. 이사건은리눅스개발커뮤니티 ( 특히리눅스창시자리누스토발즈 ) 가자체도구를만드는계기가됐다. Git은 BitKeeper를사용하면서배운교훈을기초로아래와같은목표를세웠다 : 빠른속도 단순한구조 비선형적인개발 ( 수천개의동시다발적인브랜치 ) 완벽한분산 리눅스커널같은대형프로젝트에도유용할것 ( 속도나데이터크기면에서 ) Git은 2005년탄생하고나서아직도초기목표를그대로유지하고있다. 그러면서도사용하기쉽게진화하고성숙했다. Git은미친듯이빨라서대형프로젝트에사용하기도좋다. Git은동시다발적인브랜치에도끄떡없는슈퍼울트라브랜칭시스템이다 (3장참고 ). 3

1 장시작하기 Scott Chacon Pro Git 1.3 Git 기초 Git의핵심은뭘까? 이질문은 Git을이해하는데굉장히중요하다. Git이무엇이고어떻게동작하는지이해한다면쉽게 Git을효과적으로사용할수있다. Git을배우려면 Subversion이나 Perforce 같은다른 VCS를사용하던경험을지워버려야한다. Git은미묘하게달라서다른 VCS에서쓰던개념으로는헷갈릴거다. 사용자인터페이스는매우비슷하지만, 정보를취급하는방식이다르다. 이런차이점을이해하면 Git을사용하는것이어렵지않다. 1.3.1 델타가아니라스냅샷 Subversion과 Subversion 비슷한놈들과 Git의가장큰차이점은데이터를다루는방법에있다. 큰틀에서봤을때대부분의 VCS 시스템이관리하는정보는파일들의목록이다. CVS, Subversion, Perforce, Bazaar 등의시스템은파일의집합으로정보를관리한다. 각파일의변화를그림 1-4처럼시간순으로관리한다. 그림 1.4: 각파일에대한변화 ( 델타 ) 를저장하는시스템들 Git은이런식으로데이터를저장하지도취급하지도않는다. 대신 Git의데이터는파일시스템의스냅샷이라할수있으며크기가아주작다. Git은커밋하거나프로젝트의상태를저장할때마다파일이존재하는그순간을중요하게여긴다. 파일이달라지지않았으면 Git은성능을위해서파일을저장하지않는다. 단지이전상태의파일에대한링크만저장한다. Git은그림 1-5처럼동작한다. 그림 1.5: Git 은시간순으로프로젝트의스냅샷을저장한다 이것이 Git이다른 VCS와구분되는점이다. 이점때문에 Git는다른시스템들이과거로부터답습해왔던버전컨트롤의개념과다르다는것이고많은부분을새로운관점에서바라본다. Git은강력한도구를지원하는작은파일시스템이다. Git은단순한 VCS가아니다. 이제3장에서설명할 Git 브랜치를사용하면얻게되는이득이무엇인지설명한다. 1.3.2 거의모든명령을로컬에서실행거의모든명령이로컬파일과데이터만사용하기때문에네트워크에있는다른컴퓨터는필요없다. 대부분의명령어가네트워크의속도에영향을받는 CVCS에익숙하다면 Git이매우놀라울것이다. Git 4

Scott Chacon Pro Git 1.3 절 Git 기초 의이런특징에서나오는미칠듯한속도는오직 Git느님만이구사할수있는초인적인능력이다. 프로젝트의모든히스토리가로컬디스크에있기때문에모든명령을순식간에실행된다. 예를들어 Git은프로젝트의히스토리를조회할때서버없이조회한다. 그냥로컬데이터베이스에서히스토리를읽어서보여준다. 그래서눈깜짝할사이에히스토리를조회할수있다. 어떤파일의현재버전과한달전의상태를비교해보고싶을때도 Git은그냥한달전의파일과지금의파일을로컬에서찾는다. 파일을비교하기위해리모트에있는서버에접근하고나서예전버전을가져올필요가없다. 즉오프라인상태에서도비교할수있다. 비행기나기차등에서작업하고네트워크에접속하고있지않아도커밋할수있다. 다른 VCS 시스템에서는불가능한일이다. Perforce는서버에연결할수없을때할수있는일이별로없다. Subversion이나 CVS에서도마찬가지다. 데이터베이스에접근할수없어서파일을편집할수는있지만, 커밋할수없다. 매우사소해보이지만실제로이상황에부닥쳐보면느껴지는차이가매우크다. 1.3.3 Git의무결성 Git은모든데이터를저장하기전에체크섬 ( 또는해시 ) 을구하고그체크섬으로데이터를관리한다. 체크섬없이어떠한파일이나디렉토리도변경할수없다. 체크섬은 Git에서사용하는가장기본적인 (Atomic) 데이터단위이자 Git의기본철학이다. Git 없이는체크섬을다룰수없어서파일의상태도알수없고심지어데이터를잃어버릴수도없다. Git은 SHA-1 해시를사용하여체크섬을만든다. 만든체크섬은 40자길이의 16진수문자열이다. 파일의내용이나디렉토리구조를이용하여체크섬을구한다. SHA-1은아래처럼생겼다 : 24b9da6552252987aa493b52f8696cd6d3b00373 Git 은모든것을해시로식별하기때문에이런값은여기저기서보인다. 실제로 Git 은파일을이름으로 저장하지않고해당파일의해시로저장한다. 1.3.4 Git은데이터를추가할뿐 Git으로무얼하든데이터를추가한다. 되돌리거나데이터를삭제할방법이없다. 다른 VCS처럼 Git 도커밋하지않으면변경사항을잃어버릴수있다. 하지만, 일단스냅샷을커밋하고나면데이터를잃어버리기어렵다. Git을사용하면프로젝트가심각하게망가질걱정없이매우즐겁게여러가지실험을해볼수있다. 9 장을보면 Git이데이터를어떻게저장하고손실을어떻게복구해야할지알수있다. 1.3.5 세가지상태이부분은중요하기에집중해서읽어야한다. Git을공부하기위해반드시짚고넘어가야할부분이다. Git은파일을 Committed, Modified, Staged 이렇게세가지상태로관리한다. Committed란데이터가로컬데이터베이스에안전하게저장됐다는것을의미한다. Modified는수정한파일을아직로컬데이터베이스에커밋하지않은것을말한다. Staged란현재수정한파일을곧커밋할것이라고표시한상태를의미한다. 이세가지상태는 Git 프로젝트의세가지단계와연결돼있다. Git 디렉토리, 워킹디렉토리, Staging Area 이렇게세가지단계를이해하고넘어가자. Git 디렉토리는 Git이프로젝트의메타데이터와객체데이터베이스를저장하는곳을말한다. Git 디렉토리가 Git의핵심이다. 다른컴퓨터에있는저장소를 Clone 할때 Git 디렉토리가만들어진다. 5

1 장시작하기 Scott Chacon Pro Git 그림 1.6: 워킹디렉토리, Staging Area, Git 디렉토리 워킹디렉토리는프로젝트의특정버전을 Checkout한것이다. Git 디렉토리는지금작업하는디스크에있고그디렉토리에압축된데이터베이스에서파일을가져와서워킹디렉토리를만든다. Staging Area는 Git 디렉토리에있다. 단순한파일이고곧커밋할파일에대한정보를저장한다. 종종인덱스라고불리기도하지만, Staging Area라는명칭이표준이되어가고있다. Git으로하는일은기본적으로아래와같다 : 워킹디렉토리에서파일을수정한다. Staging Area에파일을 Stage해서커밋할스냅샷을만든다. Staging Area에있는파일들을커밋해서 Git 디렉토리에영구적인스냅샷으로저장한다. Git 디렉토리에있는파일들은 Committed 상태이다. 파일을수정하고 Staging Area에추가했다면 Staged이다. 그리고 Checkout하고나서수정했지만, 아직 Staging Area에추가하지않았으면 Modified이다. 2장에서이상태에대해좀더자세히배운다. 특히 Staging Area를어떻게이용하는지혹은아예생략하는방법도설명한다. 1.4 Git 설치 Git을사용하려면우선설치해야한다. 다양한방법으로 Git을설치할수있지만두가지방법이가장일반적이다. 하나는소스코드로컴파일하여설치하는방법이고다른하나는각운영체제 ( 혹은플랫폼 ) 의패키지를사용하여설치하는방법이다. 1.4.1 소스코드로설치하기소스코드로설치하면 Git의가장최신버전을설치할수있기때문에컴파일하여설치할시간이있으면소스코드로 Git을설치하는것이좋다. Git은계속 UI를개선하고있기때문에최신버전을사용하면좋은기능을빨리사용할수있다. 리눅스패키지는보통최신버전이아니고예전버전이다. 그래서 Backport를사용하거나소스코드로설치하는것도좋은대안이다. Git을설치하려면아래와같은라이브러리들이필요하다. Git은 curl, zlib, openssl, expat, libiconv 를필요로한다. 예를들어 Fedora처럼 yum을사용하는시스템이나 apt-get이있는데비안류시스템이면아래명령어를실행하여의존패키지를설치할수있다 : 6

Scott Chacon Pro Git 1.4 절 Git 설치 $ yum install curl-devel expat-devel gettext-devel \ openssl-devel zlib-devel $ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \ libz-dev libssl-dev 필요한라이브러리를모두설치하고다음단계를진행한다. Git 웹사이트에서최신스냅샷을가져온다 : http://git-scm.com/download 그리고컴파일하고설치한다 : $ tar -zxf git-1.7.2.2.tar.gz $ cd git-1.7.2.2 $ make prefix=/usr/local all $ sudo make prefix=/usr/local install 설치한다음부터는 Git 을사용하여 Git 소스코드를수정할수있다 : $ git clone git://git.kernel.org/pub/scm/git/git.git 1.4.2 리눅스에설치 리눅스에서패키지로 Git 을설치할때에는보통각배포판에서사용하는패키지관리도구를사용하여 설치한다. Fedora 에서는아래와같이한다 : $ yum install git-core Ubuntu 같은데비안류배포판에서는 apt-get 을사용한다 : $ apt-get install git 1.4.3 Mac에설치하기 Mac에 Git을쉽게설치하는방법은두가지가있다. GUI 인스톨러가가장쉽게사용할수있다. Google Code 페이지에서내려받는다 : http://code.google.com/p/git-osx-installer MacPorts(http://www.macports.org) 를사용하는방법도있다. MacPorts가설치돼있으면아래와같이 Git을설치한다 : 7

1 장시작하기 Scott Chacon Pro Git 그림 1.7: OS X Git 인스톨러 $ sudo port install git-core +svn +doc +bash_completion +gitweb 이제설치는했다. 만약 Subversion 저장소를 Git 과함께사용해야하면 svn 도필요하다. 1.4.4 윈도에설치윈도에서도 Git을쉽게설치할수있다. 그저구글코드페이지에서 msysgit 인스톨러를내려받고실행하면된다 : http://msysgit.github.com/ 설치가완료되면 CLI 프로그램과 GUI 프로그램을둘다사용할수있다. CLI 프로그램에는 SSH 클라이언트가포함돼있기때문에유용하다. Windows 사용자필독 : 이책에서소개하는다양한명령어를사용하려면유닉스스타일의 msysgit 쉘을사용하는것이좋다. 어쩔수없이 Windows에포함된기본쉘 (Command Prompt, 명령프롬프트 ) 을꼭써야하면공백이포함된파라미터를 Git 명령어에넘길때작은따옴표 ( ) 대신큰따옴표 ( ) 를사용해야한다. 파라미터끝에 기호가있을때도큰따옴표로파라미터를감싸야한다. Windows 쉘에서 기호는다음줄로명령어가이어짐을나타낸다. 1.5 Git 최초설정 Git을설치하고나면 Git의사용환경을적절하게설정해주어야한다. 한번만설정하면된다. 설정한내용은 Git을업그레이드해도유지된다. 언제든지다시바꿀수있는명령어가있다. git config 라는도구로설정내용을확인하고변경할수있다. Git은이설정에따라동작한다. 이때사용하는설정파일은세가지나된다. /etc/gitconfig 파일 : 시스템의모든사용자와모든저장소에적용되는설정이다. git config -- system 옵션으로이파일을읽고쓸수있다. ~/.gitconfig 파일 : 특정사용자에게만적용되는설정이다. git config --global 옵션으로이파일을읽고쓸수있다..git/config: 이파일은 Git 디렉토리에있고특정저장소 ( 혹은현재작업중인프로젝트 ) 에만적용된다. 각설정은역순으로우선시된다. 그래서.git/config가 /etc/gitconfig보다우선한다. 8

Scott Chacon Pro Git 1.5 절 Git 최초설정 윈도용 Git은 $HOME 디렉토리 (%USERPROFILE% 환경변수 ) 에있는.gitconfig 파일을찾는다. 보통 C: \Documents and Settings\$USER 또는 C:\Users\$USER 이다 ( 윈도우에서는 $USER 대신 %USERNAME% 를사용한다 ). 그리고 msysgit도 /etc/gitconfig를가지고있다. 경로는 MSys 루트에따른상대경로다. 인스톨러로 msysgit을설치할때설치경로를선택할수있다. 1.5.1 사용자정보 Git 을설치하고나서가장먼저해야하는것은사용자이름과이메일주소를설정하는것이다. Git 은 커밋할때마다이정보를사용한다. 한번커밋한후에는정보를변경할수없다 : $ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com 다시말하자면 --global 옵션으로설정한것은딱한번만하면된다. 해당시스템에서해당사용자가 사용할때에는이정보를사용한다. 만약프로젝트마다다른이름과이메일주소를사용하고싶으면 -- global 옵션을빼고명령을실행한다. 1.5.2 편집기사용자정보를설정하고나면 Git에서사용할텍스트편집기를고른다. 기본적으로 Git은시스템의기본편집기를사용하고보통 Vi나 Vim이다. 하지만, Emacs 같은다른텍스트편집기를사용할수있고아래와같이실행하면된다 : $ git config --global core.editor emacs 1.5.3 Diff 도구 Merge 충돌을해결하기위해사용하는 Diff 도구를설정할수있다. vimdiff 를사용하고싶으면아래 와같이실행한다 : $ git config --global merge.tool vimdiff 이렇게 kdiff3, tkdiff, meld, xxdif, emerge, vimdiff, gvimdiff, ecmerge, opendiff 를사용할수 있다. 물론다른도구도사용할수있다. 자세한내용은 7 장에서다룬다. 1.5.4 설정확인 git config --list 명령을실행하면설정한모든것을보여준다 : 9

1 장시작하기 Scott Chacon Pro Git $ git config --list user.name=scott Chacon user.email=schacon@gmail.com color.status=auto color.branch=auto color.interactive=auto color.diff=auto... Git 은같은키를여러파일 (/etc/gitconfig 와 ~/.gitconfig 같은 ) 에서읽기때문에같은키가여러개있 을수도있다. 이러면 Git 은나중값을사용한다. git config {key} 명령으로 Git 이특정 Key 에대해어떤값을사용하는지확인할수있다 : $ git config user.name Scott Chacon 1.6 도움말보기 명령어에대한도움말이필요할때도움말을보는방법은세가지다 : $ git help <verb> $ git <verb> --help $ man git-<verb> 예를들어아래와같이실행하면 config 명령에대한도움말을볼수있다 : $ git help config 도움말은언제어디서나볼수있다. 오프라인으로도볼수있다. 도움말과이책으로부족하면다른사람의도움을받는것이필요하다. Freenode IRC 서버 (irc.freenode.net) 에있는 #git이나 #github 채널로찾아가라. 이채널에는보통수백명의사람이접속해있다. 이사람들은모두 Git에대해잘알고있다. 기꺼이도와줄것이다. 1.7 요약 우리는 Git 이무엇이고지금까지사용해온다른 CVCS 와어떻게다른지배웠다. 시스템에 Git 을설치 하고사용자정보도설정했다. 다음장에서는 Git 의사용법을배운다. 10

2 장 Git 의기초 Git을사용하는방법을알고싶은데한챕터밖에읽을시간이없다면2장을읽어야한다. Git에서자주사용하는명령어는모두2장에등장한다.2장을다읽으면저장소를만들고설정하는방법, 파일을추적하거나 (Track) 추적을그만두는방법, 변경내용을 Stage하고커밋하는방법을알게된다. 그리고또파일이나파일패턴을무시하도록 Git을설정하는방법, 실수를쉽고빠르게만회하는방법, 프로젝트히스토리를조회하고커밋을비교하는방법, 리모트저장소에 Push하고 Pull하는방법을살펴본다. 2.1 Git 저장소만들기 Git 저장소를만드는방법은두가지다. 기존프로젝트를 Git 저장소로만드는방법이있고다른서버 에있는저장소를 Clone 하는방법이있다. 2.1.1 기존디렉토리를 Git 저장소로만들기 기존프로젝트를 Git 으로관리하고싶을때, 프로젝트의디렉토리로이동해서아래과같은명령을실 행한다. $ git init 이명령은.git이라는하위디렉토리를만든다..git 디렉토리에는저장소에필요한뼈대파일 (Skeleton) 이들어있다 (.git 디렉토리가막만들어진직후에어떤파일이있는지에대한내용은 9 장에서다룬다 ). 이명령만으로는아직프로젝트의어떤파일도관리하지않는다. Git이파일을관리하게하려면저장소에파일을추가하고커밋해야한다. git add 명령으로파일을추가하고커밋한다 : $ git add *.c $ git add README $ git commit -m 'initial project version' 매우짧은시간에명령어를몇개실행해서 Git 저장소를만들고파일이관리되게했다. 11

2 장 Git 의기초 Scott Chacon Pro Git 2.1.2 기존저장소를 Clone하기다른프로젝트에참여하거나 (Contribute) Git 저장소를복사하고싶을때 git clone 명령을사용한다. 이미 Subversion 같은 VCS에익숙한사용자에게는 checkout이아니라 clone이라는점이도드라져보일것이다. Git이 Subversion과다른가장큰차이점은서버에있는모든데이터를복사한다는것이다. git clone을실행하면프로젝트히스토리를전부받아온다. 실제로서버의디스크가망가져도클라이언트저장소중에서아무거나하나가져다가복구하면된다 ( 서버에만적용했던설정은복구하지못하지만모든데이터는복구된다 - 4장에서좀더자세히다룬다 ). git clone [url] 명령으로저장소를 Clone한다. Ruby용 Git 라이브러리인 Grit을 Clone하려면아래과같이실행한다 : $ git clone git://github.com/schacon/grit.git 이명령은 grit 이라는디렉토리를만들고그안에.git 디렉토리를만든다. 그리고저장소의데이터를모두가져와서자동으로가장최신버전을 Checkout해놓는다. grit 디렉토리로이동하면 Checkout 으로생성한파일을볼수있고당장하고자하는일을시작할수있다. 아래과같은명령을사용하여저장소를 Clone하면 grit 이아니라다른디렉토리이름으로 Clone할수있다 : $ git clone git://github.com/schacon/grit.git mygrit 디렉토리이름이 mygrit이라는것만빼면이명령의결과와앞선명령의결과는같다. Git은다양한프로토콜을지원한다. 이제까지는 git:// 프로토콜을사용했지만 http(s):// 를사용할수도있고 user@server:/path.git처럼 SSH 프로토콜을사용할수도있다. 자세한내용은 4장에서다룬다. 4장에서는각프로토콜의장단점과 Git 저장소에접근하는방법을설명한다. 2.2 수정하고저장소에저장하기 만질수있는 Git 저장소를하나만들었고워킹디렉토리에 Checkout도했다. 이제는파일을수정하고파일의스냅샷을커밋해보자. 파일을수정하다가저장하고싶으면스냅샷을커밋한다. 워킹디렉토리의모든파일은크게 Tracked( 관리대상임 ) 와 Untracked( 관리대상이아님 ) 로나눈다. Tracked 파일은이미스냅샷에포함돼있던파일이다. Tracked 파일은또 Unmodified( 수정하지않음 ) 와 Modified( 수정함 ) 그리고 Staged( 커밋하면저장소에기록되는 ) 상태중하나이다. 그리고나머지파일은모두 Untracked 파일이다. Untracked 파일은워킹디렉토리에있는모든파일이스냅샷에포함돼있는것은아니고 Staging Area에있는것도아니다. 처음저장소를 Clone하면모든파일은 Tracked이면서 Unmodified 상태가된다. 파일을 Checkout하고나서아무것도수정하지않았기때문에그렇다. 마지막커밋이후아직아무것도수정하지않은상태에서어떤파일이수정되면 Git은그즉시파일을 Modified 상태로인식한다. 그리고이수정한파일을 Stage하고 Staged 상태인파일을커밋한다. 이라이프사이클을그림 2-1처럼계속반복한다. 2.2.1 파일의상태확인하기파일의상태를확인하려면보통 git status 명령을사용한다. Clone한후에바로이명령을실행하면아래과같은메시지를볼수있다 : 12

Scott Chacon Pro Git 2.2 절수정하고저장소에저장하기 그림 2.1: 파일의라이프사이클 $ git status # On branch master nothing to commit (working directory clean) 위의내용은파일을하나도수정하지않았다는것을말해준다. Tracked나 Modified 상태인파일이없다는의미다. Untracked 파일은아직없어서목록에나타나지않는다. 그리고현재작업중인브랜치를알려준다. 기본브랜치가 master이기때문에현재 master로나오는것이다. 브랜치관련내용은차차알아가자. 다음장에서브랜치와레퍼런스에대해자세히다룬다. 프로젝트에 README 파일을만들어보자. README 파일은새로만든파일이기때문에 git status를실행하면 Untracked files 에들어있다 : $ vim README $ git status # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # # README nothing added to commit but untracked files present (use "git add" to track) README 파일은 Untracked files 부분에속해있는데이것은 README 파일이 Untracked 상태라는것을말한다. Git은 Untracked 파일을아직스냅샷 ( 커밋 ) 에넣어지지않은파일이라고본다. 파일이 Tracked 상태가되기전까지는 Git은절대그파일을커밋하지않는다. 그래서일하면서생성하는바이너리파일같은것을커밋하는실수는하지않게된다. README 파일을추가해서직접 Tracked 상태로만들어보자. 2.2.2 파일을새로추적하기 git add 명령으로파일을새로추적할수있다. 아래명령을실행하면 Git 은 README 파일을추적한다 : 13

2 장 Git 의기초 Scott Chacon Pro Git $ git add README git status 명령을다시실행하면 README 파일이 Tracked 상태이면서 Staged 상태라는것을확인 할수있다 : $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # Changes to be committed 에들어있는파일은 Staged 상태라는것을의미한다. 커밋하면 git add를실행한시점의파일이커밋되어저장소히스토리에남는다. 앞에서 git init 명령을실행했을때, 그다음 git add (files) 명령을실행했던걸기억할것이다. 이것은작업디렉토리에있는파일들을추적하기시작하게하였다. git add 명령은파일또는디렉토리의경로명을아규먼트로받는다 ; 만일디렉토리를아규먼트로줄경우, 그디렉토리아래에있는모든파일들을재귀적으로추가한다. 2.2.3 Modified 상태의파일을 Stage 하기 이미 Tracked 상태인파일을수정하는법을알아보자. benchmarks.rb 라는파일을수정하고나서 git status 명령을다시실행하면결과는아래와같다 : $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: benchmarks.rb # 이 benchmarks.rb 파일은 Changes not staged for commit에있다. 이것은수정한파일이 Tracked 상태이지만아직 Staged 상태는아니라는것이다. Staged 상태로만들려면 git add 명령을실행해야한다. git add는파일을새로추적할때도사용하고수정한파일을 Staged 상태로만들때도사용한다. git add를실행하여 benchmarks.rb 파일을 Staged 상태로만들고 git status 명령으로결과를확인해보자 : 14

Scott Chacon Pro Git 2.2 절수정하고저장소에저장하기 $ git add benchmarks.rb $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # modified: benchmarks.rb # 두파일모두 Staged 상태이므로다음커밋에포함된다. 하지만, 아직더수정해야한다는것을알게되어바로커밋하지못하는상황이되었다고하자. 이상황에서 benchmark.rb 파일을열고수정한다. 아마당신은커밋할준비가다됐다고생각할테지만, Git은그렇지않다. git status 명령으로파일의상태를다시확인해보자 : $ vim benchmarks.rb $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # modified: benchmarks.rb # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: benchmarks.rb # 헉! benchmarks.rb가 Staged 상태이면서동시에 Unstaged 상태로나온다. 어떻게이런일이가능할까? git add 명령을실행하면 Git은파일을바로 Staged 상태로만든다. 지금이시점에서커밋을하면 git commit 명령을실행하는시점의버전이커밋되는것이아니라마지막으로 git add 명령을실행했을때의버전이커밋된다. 그러니까 git add 명령을실행한후에또파일을수정하면 git add 명령을다시실행해서최신버전을 Staged 상태로만들어야한다 : $ git add benchmarks.rb $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) 15

2 장 Git 의기초 Scott Chacon Pro Git # # new file: README # modified: benchmarks.rb # 2.2.4 파일무시하기어떤파일은 Git이자동으로추가하거나 Untracked 파일이라고보여줄필요가없다. 보통로그파일이나빌드시스템이자동으로생성한파일이그렇다. 그런파일을무시하려면.gitignore 파일을만들고그안에무시할파일패턴을적는다. 아래는.gitignore 파일의예이다 : $ cat.gitignore *.[oa] *~ 첫번째줄은확장자가.o 나.a인파일을 Git이무시하라는것이고둘째줄은 ~ 로끝나는모든파일을무시하라는것이다..o와.a는각각빌드시스템이만들어내는오브젝트와아카이브파일이고 ~ 로끝나는파일은 Emacs나 VI 같은텍스트편집기가임시로만들어내는파일이다. 또 log, tmp, pid 같은디렉토리나, 자동으로생성하는문서같은것들도추가할수있다..gitignore 파일은보통처음에만들어두는것이편리하다. 그래서 Git 저장소에커밋하고싶지않은파일을실수로커밋하는일을방지할수있다..gitignore 파일에입력하는패턴은아래규칙을따른다 : 아무것도없는줄이나, # 로시작하는줄은무시한다. 표준 Glob 패턴을사용한다. 디렉토리는슬래시 (/) 를끝에사용하는것으로표현한다. 느낌표 (!) 로시작하는패턴의파일은무시하지않는다. Glob 패턴은정규표현식을단순하게만든것으로생각하면되고보통쉘에서많이사용한다. 애스터리스크 (*) 는문자가하나도없거나하나이상을의미하고, [abc] 는중괄호안에있는문자중하나를의미한다 ( 그러니까이경우에는 a, b, c). 물음표 (?) 는문자하나를말하고, [0-9] 처럼중괄호안의캐릭터사이에하이픈 (-) 을사용하면그캐릭터사이에있는문자하나를말한다. 다음은.gitignore 파일의예이다 : # a comment - 이줄은무시한다. # 확장자가.a인파일무시 *.a # 윗줄에서확장자가.a인파일은무시하게했지만 lib.a는무시하지않는다.!lib.a # 루트디렉토리에있는 TODO파일은무시하고 subdir/todo처럼하위디렉토리에있는파일은무시하지않는다. /TODO # build/ 디렉토리에있는모든파일은무시한다. 16

Scott Chacon Pro Git 2.2 절수정하고저장소에저장하기 build/ # `doc/notes.txt`같은파일은무시하고 doc/server/arch.txt같은파일은무시하지않는다. doc/*.txt # `doc` 디렉토리아래의모든.txt 파일을무시한다. doc/**/*.txt **/ 스타일의문법은 Git 1.8.2 버전부터사용할수있다. 2.2.5 Staged와 Unstaged 상태의변경내용을보기단순히파일이변경됐다는사실이아니라어떤내용이변경됐는지살펴보기엔 git status 명령이아니라 git diff 명령을사용해야한다. 보통우리는 수정했지만, 아직 Staged 파일이아닌것? 과 어떤파일이 Staged 상태인지? 가궁금하기때문에 git status 명령으로도충분하다. git diff는 Patch처럼어떤라인을추가했고삭제했는지가궁금할때에사용한다. git diff는나중에더자세히다룬다. README 파일을수정해서 Staged 상태로만들고 benchmarks.rb 파일은그냥수정만해둔다. 이상태에서 git status 명령을실행하면아래와같은메시지를볼수있다 : $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: benchmarks.rb # git diff 명령을실행하면수정했지만아직 staged 상태가아닌파일을비교해볼수있다 : $ git diff diff --git a/benchmarks.rb b/benchmarks.rb index 3cb747f..da65585 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -36,6 +36,10 @@ def main @commit.parents[0].parents[0].parents[0] end + run_code(x, 'commits 1') do 17

2 장 Git 의기초 Scott Chacon Pro Git + git.commits.size + end + run_code(x, 'commits 2') do log = git.commits('master', 15) log.size 이명령은워킹디렉토리에있는것과 Staging Area에있는것을비교한다. 그래서수정하고아직 Stage하지않은것을보여준다. 만약커밋하려고 Staging Area에넣은파일의변경부분을보고싶으면 git diff --cached 옵션을사용한다 (Git 버전 1.6.1부터는좀더기억하기쉽게 git diff --staged로도사용할수있다 ). 이명령은저장소에커밋한것과 Staging Area에있는것을비교한다 : $ git diff --cached diff --git a/readme b/readme new file mode 100644 index 0000000..03902a1 --- /dev/null +++ b/readme2 @@ -0,0 +1,5 @@ +grit + by Tom Preston-Werner, Chris Wanstrath + http://github.com/mojombo/grit + +Grit is a Ruby library for extracting information from a Git repository 꼭잊지말아야할것이있는데 git diff 명령은마지막으로커밋한후에수정한것을보여주지않는다. git diff는 Unstaged 상태인것들만보여준다. 이부분이조금헷갈릴수있다. 수정한파일을모두 Staging Area에넣었다면 git diff 명령은아무것도출력하지않는다. benchmarks.rb 파일을 Stage한후에다시수정해도 git diff 명령을사용할수있다. 이때는 Staged 상태인것과 Unstaged 상태인것을비교한다 : $ git add benchmarks.rb $ echo '# test line' >> benchmarks.rb $ git status # On branch master # # Changes to be committed: # # modified: benchmarks.rb # # Changes not staged for commit: 18

Scott Chacon Pro Git 2.2 절수정하고저장소에저장하기 # # modified: benchmarks.rb # git diff 명령으로 Unstaged 상태인변경부분을확인해볼수있다 : $ git diff diff --git a/benchmarks.rb b/benchmarks.rb index e445e28..86b2f7c 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -127,3 +127,4 @@ end main() ##pp Grit::GitRuby.cache_client.stats +# test line Staged 상태인파일은 git diff --cached 옵션으로확인한다 : $ git diff --cached diff --git a/benchmarks.rb b/benchmarks.rb index 3cb747f..e445e28 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -36,6 +36,10 @@ def main @commit.parents[0].parents[0].parents[0] end + run_code(x, 'commits 1') do + git.commits.size + end + run_code(x, 'commits 2') do log = git.commits('master', 15) log.size 2.2.6 변경사항커밋하기수정한것을커밋하기위해 Staging Area에파일을정리했다. Unstaged 상태의파일은커밋되지않는다는것을기억해야한다. Git은생성하거나수정하고나서 git add 명령으로추가하지않은파일은커밋하지않는다. 그파일은여전히 Modified 상태로남아있다. 커밋하기전에 git status 명령으로모든것이 Staged 상태인지확인할수있다. 그리고 git commit을실행하여커밋한다 : 19

2 장 Git 의기초 Scott Chacon Pro Git $ git commit Git 설정에지정된편집기가실행되고, 아래와같은텍스트가자동으로포함된다 ( 아래예제는 Vim 편집기의화면이다 ). 이편집기는쉘의 $EDITOR 환경변수에등록된편집기이고보통은 Vim이나 Emacs을사용한다. 또 1장에서설명했듯이 git config --global core.editor 명령으로어떤편집기를사용할지설정할수있다 : 편집기는아래와같은내용을표시한다 ( 아래예제는 Vim 편집기 ): # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # modified: benchmarks.rb ~ ~ ~ ".git/commit_editmsg" 10L, 283C 자동으로생성되는커밋메시지의첫줄은비어있고둘째줄부터 git status 명령의결과가채워진다. 커밋한내용을쉽게기억할수있도록이메시지를포함할수도있고메시지를전부지우고새로작성할수있다 ( 수정한내용을좀더구체적으로남겨둘수있다. git commit에 -v 옵션을추가하면편집기에 diff 메시지도추가된다 ). 메시지를인라인으로첨부할수도있다. commit 명령을실행할때아래와같이 -m 옵션을사용한다 : $ git commit -m "Story 182: Fix benchmarks for speed" [master]: created 463dc4f: "Fix benchmarks for speed" 2 files changed, 3 insertions(+), 0 deletions(-) create mode 100644 README commit 명령은몇가지정보를출력하는데위예제는 master 브랜치에커밋했고체크섬은 463dc4f이라고알려준다. 그리고수정한파일이몇개이고삭제됐거나추가된줄이몇줄인지알려준다. Git은 Staging Area에속한스냅샷을커밋한다는것을기억해야한다. 수정은했지만, 아직 Staging Area에넣지않은것은다음에커밋할수있다. 커밋할때마다프로젝트의스냅샷을기록하기때문에나중에스냅샷끼리비교하거나예전스냅샷으로되돌릴수있다. 2.2.7 Staging Area 생략하기 Staging Area는커밋할파일을정리한다는점에서매우유용하지만복잡하기만하고필요하지않은때도있다. 아주쉽게 Staging Area를생략할수있다. git commit 명령을실행할때 -a 옵션을추가하 20

Scott Chacon Pro Git 2.2 절수정하고저장소에저장하기 면 Git 은 Tracked 상태의파일을자동으로 Staging Area 에넣는다. 그래서 git add 명령을실행하는 수고를덜수있다 : $ git status # On branch master # # Changes not staged for commit: # # modified: benchmarks.rb # $ git commit -a -m 'added new benchmarks' [master 83e38c7] added new benchmarks 1 files changed, 5 insertions(+), 0 deletions(-) 이예제에서는커밋하기전에 git add 명령으로 benchmarks.rb 파일을추가하지않았다는점을눈여 겨보자. 2.2.8 파일을삭제하기 Git에서파일을제거하려면 git rm 명령으로 Tracked 상태의파일을삭제한후에 ( 정확하게는 Staging Area에서삭제하는것 ) 커밋해야한다. 이명령은워킹디렉토리에있는파일도삭제하기때문에실제로지워진다. 만약 Git없이그냥파일을삭제하고 git status 명령으로상태를확인하면 Changes not staged for commit( 즉, Unstaged) 에속한다는것을확인할수있다 : $ rm grit.gemspec $ git status # On branch master # # Changes not staged for commit: # (use "git add/rm <file>..." to update what will be committed) # # deleted: grit.gemspec # 그리고 git rm 명령을실행하면삭제한파일은 staged 상태가된다 : $ git rm grit.gemspec rm 'grit.gemspec' $ git status # On branch master # 21

2 장 Git 의기초 Scott Chacon Pro Git # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # deleted: grit.gemspec # 커밋하면파일은삭제되고 Git은이파일을더는추적하지않는다. 이미파일을수정했거나 Index에 ( 역주, Staging Area을 Git Index라고도부른다 ) 추가했다면 -f옵션을주어강제로삭제해야한다. 이점은실수로데이터를삭제하지못하도록하는안전장치다. 한번도커밋한적없는데이터는 Git으로복구할수없다. 또 Staging Area에서만제거하고워킹디렉토리에있는파일은지우지않고남겨둘수있다. 다시말해서하드디스크에있는파일은그대로두고 Git만추적하지않게한다. 이것은.gitignore 파일에추가하는것을빼먹었거나대용량로그파일이나컴파일된파일인.a 파일같은것을실수로추가했을때쓴다. --cached 옵션을사용하여명령을실행한다 : $ git rm --cached readme.txt 여러개의파일이나디렉토리를한꺼번에삭제할수도있다. 아래와같이 git rm 명령에 file-glob 패 턴을사용한다 : $ git rm log/\*.log * 앞에 \ 을사용한것을기억하자. 파일명확장기능은쉘에만있는것이아니라 Git 자체에도있기때 문에필요하다. Windows 기본쉘을쓸때는 \ 기호를붙이지않는다. 이명령은 log/ 디렉토리에있는.log 파일을모두삭제한다. 아래의예제처럼할수도있다 : $ git rm \*~ 이명령은 ~ 로끝나는파일을모두삭제한다. 2.2.9 파일이름변경하기 Git은다른 VCS 시스템과는달리파일이름의변경이나파일의이동을명시적으로관리하지않는다. 다시말해서파일이름이변경됐다는별도의정보를저장하지않는다. Git은똑똑해서굳이파일이름이변경되었다는것을추적하지않아도아는방법이있다. 파일의이름이변경된것을 Git이어떻게알아내는지살펴보자. 이렇게말하고 Git에 mv 명령이있는게좀이상하겠지만, 아래와같이파일이름을변경할수있다 : 22

Scott Chacon Pro Git 2.3 절커밋히스토리조회하기 $ git mv file_from file_to 잘동작한다. 이명령을실행하고 Git 의상태를확인해보면 Git 은이름이바뀐사실을알고있다 : $ git mv README.txt README $ git status # On branch master # Your branch is ahead of 'origin/master' by 1 commit. # # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # renamed: README.txt -> README # 사실 git mv 명령은아래명령어들을수행한것과완전히똑같다 : $ mv README.txt README $ git rm README.txt $ git add README git mv 는일종의단축명령어이다. 이명령으로파일이름을바꿔도되고 mv 명령으로파일이름을직접 바꿔도된다. 단지 Git 의 mv 명령은편리하게명령을세번실행해주는것뿐이다. 어떤도구로이름을바 꿔도상관없다. 중요한것은이름을변경하고나서꼭 rm/add 명령을실행해야한다는것뿐이다. 2.3 커밋히스토리조회하기 새로저장소를만들어서몇번커밋을했을수도있고, 커밋히스토리가있는저장소를 Clone했을수도있다. 어쨌든가끔저장소의히스토리를보고싶을때가있다. Git에는히스토리를조회하는명령어인 git log가있다. 이예제에서는 simplegit이라는매우단순한프로젝트를사용한다. simplegit은 Git을설명하는데자주사용하는예제다. 아래와같이이프로젝트를 Clone한다 : git clone git://github.com/schacon/simplegit-progit.git 이프로젝트디렉토리에서 git log 명령을실행하면아래와같이출력된다 : 23

2 장 Git 의기초 Scott Chacon Pro Git $ git log commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008-0700 changed the version number commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <schacon@gee-mail.com> Date: Sat Mar 15 16:40:33 2008-0700 removed unnecessary test code commit a11bef06a3f659402fe7563abf99ad00de2209e6 Author: Scott Chacon <schacon@gee-mail.com> Date: Sat Mar 15 10:31:28 2008-0700 first commit 특별한아규먼트없이 git log 명령을실행하면저장소의커밋히스토리를시간순으로보여준다. 즉, 가장최근의커밋이가장먼저나온다. 그리고이어서각커밋의 SHA-1 체크섬, 저자이름, 저자이메일, 커밋한날짜, 커밋메시지를보여준다. 원하는히스토리를검색할수있도록 git log 명령은매우다양한옵션을지원한다. 여기에서는자주사용하는옵션을설명한다. -p가가장유용한옵션중하나다. -p는각커밋의 diff 결과를보여준다. 게다가 -2는최근두개의결과만보여주는옵션이다 : $ git log -p -2 commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008-0700 changed the version number diff --git a/rakefile b/rakefile index a874b73..8f94139 100644 --- a/rakefile +++ b/rakefile @@ -5,7 +5,5 @@ require 'rake/gempackagetask' spec = Gem::Specification.new do s s.name = "simplegit" - s.version = "0.1.0" + s.version = "0.1.1" 24

Scott Chacon Pro Git 2.3 절커밋히스토리조회하기 s.author = "Scott Chacon" s.email = "schacon@gee-mail.com" commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <schacon@gee-mail.com> Date: Sat Mar 15 16:40:33 2008-0700 removed unnecessary test code diff --git a/lib/simplegit.rb b/lib/simplegit.rb index a0a60ae..47c6340 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -18,8 +18,3 @@ class SimpleGit end end - -if $0 == FILE - git = SimpleGit.new - puts git.show -end \ No newline at end of file 이옵션은직접 diff를실행한것과같은결과를출력하기때문에동료가무엇을커밋했는지리뷰하고빨리조회하는데유용하다. 가끔은 diff 결과를줄단위로보기보다는단어단위로보는것이좋을때도있다. git log -p와같은명령에 --word-diff 옵션을사용하면줄단위대신단어단위로변경사항을보여준다. 단어단위로다른부분을확인하는것은소스코드에는별로유용하지않다. 책이나에세이같이문장이긴글을쓸때는단어단위로보는것이편하다. --word-diff 옵션은다음과같이사용한다 : $ git log -U1 --word-diff commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008-0700 changed the version number diff --git a/rakefile b/rakefile index a874b73..8f94139 100644 --- a/rakefile +++ b/rakefile @@ -7,3 +7,3 @@ spec = Gem::Specification.new do s 25

2 장 Git 의기초 Scott Chacon Pro Git s.name = "simplegit" s.version = [-"0.1.0"-]{+"0.1.1"+} s.author = "Scott Chacon" 위의예제는줄단위로보여주는일반적인 diff와좀다르다. 줄안에서변경한부분을단어단위로표시한다. 추가한단어는 {+ +} 기호가둘러싸고삭제한단어는 [- -] 기호가둘러싼다. diff는기본적으로다른줄과위아래줄을포함해서 3줄을보여준다. 줄단위가아니라단어단위로비교해서볼때는굳이 3줄을다볼필요가없다. 예제에서처럼 -U1 옵션을주면해당줄만보여준다. 또 git log 명령에는히스토리의통계를보여주는옵션도있다. --stat 옵션으로각커밋의통계정보를조회할수있다 : $ git log --stat commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008-0700 changed the version number Rakefile 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <schacon@gee-mail.com> Date: Sat Mar 15 16:40:33 2008-0700 removed unnecessary test code lib/simplegit.rb 5 ----- 1 files changed, 0 insertions(+), 5 deletions(-) commit a11bef06a3f659402fe7563abf99ad00de2209e6 Author: Scott Chacon <schacon@gee-mail.com> Date: Sat Mar 15 10:31:28 2008-0700 first commit README 6 ++++++ Rakefile 23 +++++++++++++++++++++++ lib/simplegit.rb 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 0 deletions(-) 이결과에서 --stat 옵션은어떤파일이수정됐는지, 얼마나많은파일이변경됐는지, 또얼마나많은 줄을추가하거나삭제했는지보여준다. 요약정보는가장뒤쪽에보여준다. 26

Scott Chacon Pro Git 2.3 절커밋히스토리조회하기 다른또유용한옵션은 --pretty 옵션이다. 이옵션을통해 log의내용을보여줄때기본형식이외에여러가지중에하나를선택할수있다. oneline 옵션은각커밋을한줄로보여준다. 이옵션은많은커밋을한번에조회할때유용하다. 추가로 short, full, fuller 옵션도있는데이것은정보를조금씩가감해서보여준다 : $ git log --pretty=oneline ca82a6dff817ec66f44342007202690a93763949 changed the version number 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code a11bef06a3f659402fe7563abf99ad00de2209e6 first commit 가장재밌는옵션은 format 옵션이다. 나만의포맷으로결과를출력하고싶을때사용한다. 특히결과 를다른프로그램으로파싱하고자할때유용하다. 이옵션을사용하면포맷을정확하게일치시킬수있 기때문에 Git 을새버전으로바꿔도결과포맷이바뀌지않는다 : $ git log --pretty=format:"%h - %an, %ar : %s" ca82a6d - Scott Chacon, 11 months ago : changed the version number 085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code a11bef0 - Scott Chacon, 11 months ago : first commit 표 2-1 형식에서사용하는유용한옵션들. Option Description of Output %H Commit hash %h Abbreviated commit hash %T Tree hash %t Abbreviated tree hash %P Parent hashes %p Abbreviated parent hashes %an %ae %ad %ar %cn %ce %cd %cr Author name Author e-mail Author date (format respects the date= option) Author date, relative Committer name Committer email Committer date Committer date, relative %s Subject 27

2 장 Git 의기초 Scott Chacon Pro Git 저자 (Author) 와커미터 (Committer) 를구분하는것이조금이상해보일수있다. 저자는원래작업을수행한원작자이고커밋터는마지막으로이작업을적용한사람이다. 만약당신이어떤프로젝트에패치를보냈고그프로젝트의담당자가패치를적용했다면두명의정보를모두알필요가있다. 그래서이경우당신이저자고그담당자가커미터다. 5장에서이주제에대해자세히다룰것이다. oneline과 format 옵션은 --graph 옵션과함께사용할때더빛난다. 이명령은브랜치와머지히스토리를보여주는아스키그래프를출력한다. 이명령을 Grit 프로젝트저장소에서사용해보면아래와같다 : $ git log --pretty=format:"%h %s" --graph * 2d3acf9 ignore errors from SIGCHLD on trap * 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit \ * 420eac9 Added a method for getting the current branch. * 30e367c timeout code and tests * 5a09431 add timeout protection to grit * e1193f8 support for heads with slashes in them / * d6016bc require time for xmlschema * 11d191e Merge branch 'defunkt' into local git log 명령의기본적인옵션과출력물의형식에관련된옵션을살펴보았다. git log 명령은앞서살 펴본것보다더많은옵션을지원한다. 표 2-2 는지금설명한것과함께유용하게사용할수있는옵션 이다. 각옵션으로어떻게 log 명령을제어할수있는지보여준다. 옵션설명 -p 각커밋에적용된패치를보여준다. --word-diff diff 결과를단어단위로보여준다. --stat 각커밋에서수정된파일의통계정보를보여준다. --shortstat `--stat` 명령의결과중에서수정한파일, 추가된줄, 삭제된줄만보여준다. --name-only 커밋정보중에서수정된파일의목록만보여준다. --name-status 수정된파일의목록을보여줄뿐만아니라파일을추가한것인지, 수정한것인지, 삭제한것인지도보여준다. --abbrev-commit 40자짜리 SHA-1 체크섬을전부보여주는것이아니라처음몇자만보여준다. --relative-date 정확한시간을보여주는것이아니라 `2 주전`처럼상대적인형식으로보여준다. --graph 브랜치와머지히스토리정보까지아스키그래프로보여준다. --pretty 지정한형식으로보여준다. 이옵션에는 oneline, short, full, fuller, format이있다. format은원하는형식으로출력하고자할때사용한다. --oneline `--pretty=oneline --abbrev-commit` 옵션을함께사용한것과동일하다. 2.3.1 조회제한조건출력형식과관련된옵션을살펴봤지만 git log 명령은조회범위를제한하는옵션들도있다. 히스토리전부가아니라부분만조회한다. 이미최근두개만조회하는 -2 옵션은살펴봤다. 실제사용법은 - 28

Scott Chacon Pro Git 2.3 절커밋히스토리조회하기 <n> 이고 n은최근 n개의커밋을의미한다. 사실이옵션은잘쓰이지않는다. Git은기본적으로출력을 pager류의프로그램을거쳐서내보내므로한번에한페이지씩보여준다. 반면 --since나 --until같은시간을기준으로조회하는옵션은매우유용하다. 지난 2주동안만들어진커밋들만조회하는명령은아래와같다 : $ git log --since=2.weeks 이옵션은다양한형식을지원한다. 2008-01-15같이정확한날짜도사용할수있고 2 years 1 day 3 minutes ago같이상대적인기간을사용할수도있다. 또다른기준도있다. --author 옵션으로저자를지정하여검색할수도있고 --grep 옵션으로커밋메시지에서키워드를검색할수도있다 (author와 grep 옵션을나눠서지정하고싶지않으면 --all-match 옵션으로한번에검색할수있다 ). 마지막으로파일경로로검색하는옵션이있는데이것도정말유용하다. 디렉토리나파일이름을사용하여그파일이변경된 log의결과를검색할수있다. 이옵션은 --와함께경로이름을사용하는데명령어끝부분에쓴다 ( 역주, git log -- path1 path2). 표 2-3은조회범위를제한하는옵션들이다. 옵션 설명 -(n) 최근 n 개의커밋만조회한다. --since, --after 명시한날짜이후의커밋만검색한다. --until, --before 명시한날짜이전의커밋만조회한다. --author 입력한저자의커밋만보여준다. --committer 입력한커미터의커밋만보여준다. 아래예제는 2008 년 10 월에 Junio Hamano 가커밋한히스토리를조회하는것이다. 그중에서테스 트파일을수정한커밋중에서머지커밋이아닌것들만조회한다 : $ git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \ --before="2008-11-01" --no-merges -- t/ 5610e3b - Fix testcase failure when extended attribute acd3b9e - Enhance hold_lock_file_for_{update,append}() f563754 - demonstrate breakage of detached checkout wi d1a43f2 - reset --hard/read-tree --reset -u: remove un 51a94af - Fix "checkout --track -b newbranch" on detac b0ad11e - pull: allow "git pull origin $something:$cur 총 2 만여개의커밋히스토리에서이명령의검색조건에만족하는것은단 6 개였다. 29

2 장 Git 의기초 Scott Chacon Pro Git 2.3.2 GUI 도구로히스토리를시각화하기 GUI 도구로커밋히스토리를시각화하고싶다면 gitk를사용할수있다. gitk는 Tcl/Tk 프로그램이고 git log 명령을시각화해주는도구다. gitk는 git log 명령이지원하는필터링옵션을거의모두지원한다. 프로젝트디렉토리에서 gitk를실행하면그림 2-2처럼보일것이다. 그림 2.2: gitk 의히스토리 위쪽반을차지하는윈도에서는히스토리를그래프로예쁘게보여준다. 아래쪽반을차지하는윈도는 diff 결과를보여주는데위쪽윈도에서선택한커밋에대한 diff 결과를보여준다. 2.4 되돌리기 일을하다보면모든단계에서어떤것은되돌리고 (Undo) 싶을때가있다. 이번에는우리가한일을되 돌리는방법을살펴볼것이다. 한번되돌리면복구할수없어서주의해야한다. Git 을사용하면우리가 한실수를복구하지못할것은거의없지만되돌리기는복구할수없다. 2.4.1 커밋수정하기 종종완료한커밋을수정해야할때가있다. 너무일찍커밋했거나어떤파일을빼먹었을때그리고커 밋메시지를잘못적었을때하게된다. 다시커밋하고싶으면 --amend 옵션을사용한다 : $ git commit --amend 이명령은 Staging Area를사용하여커밋한다. 만약마지막으로커밋하고나서수정한것이없다면 ( 커밋하자마자바로이명령을실행하는경우 ) 조금전에한커밋과모든것이같다. 이때는커밋메시지만수정한다. 편집기가실행되면이전커밋메시지가자동으로포함된다. 메시지를수정하지않고그대로커밋해도기존의커밋을덮어쓴다. 커밋을했는데 Stage하는것을깜빡하고빠트린파일이있으면아래와같이고칠수있다 : 30

Scott Chacon Pro Git 2.4 절되돌리기 $ git commit -m 'initial commit' $ git add forgotten_file $ git commit --amend 여기서실행한명령어 3 개는모두하나의커밋으로기록된다. 두번째커밋은첫번째커밋을덮어쓴다. 2.4.2 파일상태를 Unstage로변경하기다음은 Staging Area와워킹디렉토리사이를넘나드는방법을설명한다. 두영역의상태를확인할때마다변경된상태를되돌리는방법을알려주기때문에매우편리하다. 예를들어파일을두개수정하고서따로따로커밋하려고했지만, 실수로 git add * 라고실행해버렸다. 두파일모두 Staging Area 에들어있다. 이제둘중하나를어떻게꺼낼까? 우선 git status 명령으로확인해보자 : $ git add. $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: README.txt # modified: benchmarks.rb # Changes to be commited 밑에 git reset HEAD <file>... 이라는문장을볼수있다. 이명령으로 Unstage 상태로변경할수있다. benchmarks.rb 파일을 Unstage 상태로변경해보자 : $ git reset HEAD benchmarks.rb benchmarks.rb: locally modified $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: README.txt # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: benchmarks.rb # 31

2 장 Git 의기초 Scott Chacon Pro Git 명령어가낮설게느껴질수도있지만잘동작한다. benchmarks.rb 파일은 Unstage 상태가됐다. 2.4.3 Modified 파일되돌리기어떻게해야 benchmarks.rb 파일을수정하고나서다시되돌릴수있을까? 그러니까최근커밋된버전으로 ( 아니면처음 Clone했을때처럼워킹디렉토리에처음 Checkout 한그내용으로 ) 되돌리는방법이무얼까? git status 명령이친절하게알려준다. 바로위에있는예제에서 Unstaged 부분을보자 : # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: benchmarks.rb # 위의메시지는수정한파일을되돌리는방법을꽤정확하게알려준다 ( 적어도 Git 1.6.1 이후버전부터 는그렇다. 만약예전것을아직사용하고있으면업그레드하는것이좋다. 편의성이많이개선됐다 ). 알려주는대로한번해보자 : $ git checkout -- benchmarks.rb $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: README.txt # 정상적으로복원된것을알수있다. 하지만이명령은꽤위험한명령이라는것을알아야한다. 수정이전의파일로덮어썼기때문에수정했던내용은전부사라진다. 수정한내용이진짜마음에들지않을때에만사용하자. 정말이렇게삭제해야한다면 Stash와 Branch를사용하자. 다음장에서다루는이방법들이훨씬낫다. Git으로커밋한모든것은언제나복구할수있다. 삭제한브랜치에있었던것도 --amend 옵션으로다시커밋한것도복구할수있다 ( 자세한것은 9장에서다룬다 ). 하지만, 커밋하지않고잃어버린것은절대로되돌릴수없다. 2.5 리모트저장소 리모트저장소를관리할줄알아야다른사람과함께일할수있다. 리모트저장소는인터넷이나네트워크어딘가에있는저장소를말한다. 저장소는여러개가있을수있는데어떤저장소는읽고쓰기모두할수있고어떤저장소는읽기권한만있을수도있다. 간단히말해서다른사람들과함께일한다는것은리모트저장소를관리하면서데이터를거기에 Push하고 Pull하는것이다. 리모트저장소를관리 32

Scott Chacon Pro Git 2.5 절리모트저장소 한다는것은저장소를추가, 삭제하는것뿐만아니라브랜치를관리하고추적할지말지등을관리하는 것을말한다. 이번에는리모트저장소를관리하는방법에대해설명한다. 2.5.1 리모트저장소확인하기 git remote 명령으로현재프로젝트에등록된리모트저장소를확인할수있다. 이명령은리모트저장소의단축이름을보여준다. 저장소를 Clone하면 origin이라는리모트저장소가자동으로등록되기때문에 origin이라는이름을볼수있다 : $ git clone git://github.com/schacon/ticgit.git Initialized empty Git repository in /private/tmp/ticgit/.git/ remote: Counting objects: 595, done. remote: Compressing objects: 100% (269/269), done. remote: Total 595 (delta 255), reused 589 (delta 253) Receiving objects: 100% (595/595), 73.31 KiB 1 KiB/s, done. Resolving deltas: 100% (255/255), done. $ cd ticgit $ git remote origin -v 옵션을주어단축이름과 URL 을함께볼수있다 : $ git remote -v origin git://github.com/schacon/ticgit.git (fetch) origin git://github.com/schacon/ticgit.git (push) 리모트저장소가여러개있다면이명령은전부보여준다. 내 Grit 저장소에서실행하면아래와같이 출력한다 : $ cd grit $ git remote -v bakkdoor git://github.com/bakkdoor/grit.git cho45 git://github.com/cho45/grit.git defunkt git://github.com/defunkt/grit.git koke git://github.com/koke/grit.git origin git@github.com:mojombo/grit.git 이렇게리모트저장소가여러개가등록되어있으면다른사람이기여한내용 (Contributions) 을쉽게가져올수있다. 그리고 origin만 SSH URL이기때문에 origin에만 Push할수있다 (4장에서좀더자세히다룬다 ). 33

2 장 Git 의기초 Scott Chacon Pro Git 2.5.2 리모트저장소추가하기이전절에서도리모트저장소를추가하는것에대해설명했었지만수박겉핥기식으로살펴봤을뿐이었다. 여기에서는리모트저장소를추가하는방법을자세하게설명한다. 쉽게새리모트저장소를추가할수있는데 git remote add [ 단축이름 ] [url] 명령을실행한다 : $ git remote origin $ git remote add pb git://github.com/paulboone/ticgit.git $ git remote -v origin git://github.com/schacon/ticgit.git pb git://github.com/paulboone/ticgit.git 이제 URL 대신에스트링 pb 를사용할수있다. 예를들어로컬저장소에는없지만 Paul 의저장소에있 는것을가져오려면아래과같이실행한다 : $ git fetch pb remote: Counting objects: 58, done. remote: Compressing objects: 100% (41/41), done. remote: Total 44 (delta 24), reused 1 (delta 0) Unpacking objects: 100% (44/44), done. From git://github.com/paulboone/ticgit * [new branch] master -> pb/master * [new branch] ticgit -> pb/ticgit 로컬에서 pb/master 가 Paul 의 master 브랜치이다. 이것을로컬브랜치중하나에머지하거나체크아 웃하여브랜치내용을자세히확인할수있다. 2.5.3 리모트저장소를 Pull 하거나 Fetch 하기 앞서설명했듯이리모트저장소에서데이터를가져오려면간단히아래와같이실행한다 : $ git fetch [remote-name] 이명령은로컬에는없지만, 리모트저장소에는있는데이터를모두가져온다. 그리고나면리모트저장소의모든브랜치를로컬에서접근할수있어서언제든지머지를하거나내용을살펴볼수있다 ( 우리는 3장에서브랜치를사용하는방법에대해좀더자세히설명할것이다 ). 저장소를 Clone하면명령은자동으로리모트저장소를 origin이라는이름으로추가한다. 그래서나중에 git fetch origin을실행하면 Clone한이후에 ( 혹은마지막으로가져온이후에 ) 수정된것을모두가져온다. fetch 명령은리모트저장소의데이터를모두로컬로가져오지만, 자동으로머지하지않는다. 그래서당신이로컬에서하던작업을정리하고나서수동으로머지해야한다. 34

Scott Chacon Pro Git 2.5 절리모트저장소 그냥쉽게 git pull 명령으로리모트저장소브랜치에서데이터를가져올뿐만아니라자동으로로컬브랜치와머지시킬수있다. 먼저 git clone 명령은자동으로로컬의 master 브랜치가리모트저장소의 master 브랜치를추적하도록한다 ( 물론리모트저장소에 master 브랜치가있다고가정에서 ). 그리고 git pull 명령은 Clone한서버에서데이터를가져오고그데이터를자동으로현재작업하는코드와머지시킨다. 2.5.4 리모트저장소에 Push하기프로젝트를공유하고싶을때리모트저장소에 Push할수있다. 이명령은 git push [ 리모트저장소이름 ] [ 브랜치이름 ] 으로단순하다. master 브랜치를 origin 서버에 Push하려면 ( 다시말하지만 Clone 하면보통자동으로 origin 이름이생성된다 ) 아래와같이서버에 Push한다 : $ git push origin master 이명령은 Clone한리모트저장소에쓰기권한이있고, Clone하고난이후아무도리모트저장소에 Push하지않았을때만사용할수있다. 다시말해서 Clone한사람이여러명있을때, 다른사람이 Push한후에 Push하려고하면 Push할수없다. 먼저다른사람이작업한것을가져와서머지한후에 Push할수있다. 3장에서서버에 Push하는방법에대해자세히설명할것이다. 2.5.5 리모트저장소살펴보기 ( 역주, 이절은최신버전의 Git이출력하는메시지와조금다르다.) git remote show [ 리모트저장소이름 ] 명령으로리모트저장소의구체적인정보를확인할수있다. origin 같은단축이름으로이명령을실행하면아래와같은정보를볼수있다 : $ git remote show origin * remote origin URL: git://github.com/schacon/ticgit.git Remote branch merged with 'git pull' while on branch master master Tracked remote branches master ticgit 리모트저장소의 URL과추적하는브랜치를출력한다. 이명령은 git pull 명령을실행할때 master 브랜치와머지할브랜치가무엇인지보여준다. git pull 명령은리모트저장소브랜치의데이터를모두가져오고나서자동으로머지할것이다. 그리고가져온모든리모트저장소정보도출력한다. 좀더 Git을열심히사용하게되면 git remote show 명령은더많은정보를보여줄것이다. 여러분도언젠가는아래와같은메시지 ( 역주, 다수의브랜치를사용하는메시지 ) 를볼날이올것이다. $ git remote show origin * remote origin 35

2 장 Git 의기초 Scott Chacon Pro Git URL: git@github.com:defunkt/github.git Remote branch merged with 'git pull' while on branch issues issues Remote branch merged with 'git pull' while on branch master master New remote branches (next fetch will store in remotes/origin) caching Stale tracking branches (use 'git remote prune') libwalker walker2 Tracked remote branches acl apiv2 dashboard2 issues master postgres Local branch pushed with 'git push' master:master 브랜치명을생략하고 git push 명령을실행할때어떤브랜치가어떤브랜치로 Push되는지보여준다. 또아직로컬로가져오지않은리모트저장소의브랜치는어떤것들이있는지, 서버에서는삭제됐지만아직가지고있는브랜치는어떤것인지, git pull 명령을실행했을때자동으로머지할브랜치는어떤것이있는지보여준다. 2.5.6 리모트저장소이름을바꾸거나리모트저장소를삭제하기 git remote rename 명령으로리모트저장소의이름을변경할수있다. 예를들어 pb 를 paul 로변경하려 면 git remote rename 명령을사용한다 : $ git remote rename pb paul $ git remote origin paul 리모트저장소의브랜치이름도바뀐다. 여태까지 pb/master로리모트저장소브랜치를사용했으면이제는 paul/master라고사용해야한다. 리모트저장소를삭제해야한다면 git remote rm 명령을사용한다. 서버정보가바뀌었을때, 더는별도의미러가필요하지않을때, 더는기여자가활동하지않을때필요하다 : $ git remote rm paul $ git remote 36

Scott Chacon Pro Git 2.6 절태그 origin 2.6 태그 다른 VCS 처럼 Git 도태그를지원한다. 사람들은보통릴리즈할때사용한다 (v1.0, 등등 ). 이번에는 태그를조회하고생성하는법과태그의종류를설명한다. 2.6.1 태그조회하기 우선 git tag 명령으로이미만들어진태그가있는지확인할수있다 : $ git tag v0.1 v1.3 이명령은알파벳순서로태그을보여준다. 사실순서는별로중요한게아니다. 검색패턴을사용하여태그를검색할수있다. Git 소스저장소는 240 여개의태그가있다. 만약 1.4.2 버전의태그들만검색하고싶으면아래와같이실행한다 : $ git tag -l 'v1.4.2.*' v1.4.2.1 v1.4.2.2 v1.4.2.3 v1.4.2.4 2.6.2 태그붙이기 Git의태그는 Lightweight 태그와 Annotated 태그로두종류가있다. Lightweight 태그는브랜치와비슷한데브랜치처럼가리키는지점을최신커밋으로이동시키지않는다. 단순히특정커밋에대한포인터일뿐이다. 한편, Annotated 태그는 Git 데이터베이스에태그를만든사람의이름, 이메일과태그를만든날짜, 그리고태그메시지도저장한다. 또 GPG(GNU Privacy Guard) 로서명할수도있다. 이모든정보를저장해둬야할때에만 Annotated 태그를추천한다. 그냥다른정보를저장하지않는단순한태그가필요하다면 Lightweight 태그를사용하는것이좋다. 2.6.3 Annotated 태그 Annotated 태그를만드는방법은간단하다. tag 명령을실행할때 -a 옵션을추가한다 : 37

2 장 Git 의기초 Scott Chacon Pro Git $ git tag -a v1.4 -m 'my version 1.4' $ git tag v0.1 v1.3 v1.4 -m 옵션으로태그를저장할때메시지를함께저장할수있다. 명령을실행할때메시지를입력하지않 으면 Git 은편집기를실행시킨다. git show 명령으로태그정보와커밋정보를모두확인할수있다 : $ git show v1.4 tag v1.4 Tagger: Scott Chacon <schacon@gee-mail.com> Date: Mon Feb 9 14:45:11 2009-0800 my version 1.4 commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <schacon@gee-mail.com> Date: Sun Feb 8 19:02:46 2009-0800 Merge branch 'experiment' 커밋정보를보여주기전에먼저태그를만든사람이누구인지, 언제태그를만들었는지, 그리고태그 메시지가무엇인지보여준다. 2.6.4 태그에서명하기 GPG 개인키가있으면태그에서명할수있다. 이때에는 -a 옵션대신 -s 를사용한다 : $ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Scott Chacon <schacon@gee-mail.com>" 1024-bit DSA key, ID F721C45A, created 2009-02-09 이태그에 git show 를실행하면 GPG 서명도볼수있다 : $ git show v1.5 tag v1.5 Tagger: Scott Chacon <schacon@gee-mail.com> 38

Scott Chacon Pro Git 2.6 절태그 Date: Mon Feb 9 15:22:20 2009-0800 my signed 1.5 tag -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.8 (Darwin) ieyeabecaayfakmquriacgkqon3dxfchxfr5caceimn+zxlkggjqf0qyiqbwgysn Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/ =WryJ -----END PGP SIGNATURE----- commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <schacon@gee-mail.com> Date: Sun Feb 8 19:02:46 2009-0800 Merge branch 'experiment' 잠시후에서명한태그를검증하는방법도설명한다. 2.6.5 Lightweight 태그 Lightweight 태그는기본적으로파일에커밋체크섬을저장하는것뿐이다. 다른정보는저장하지않 는다. Lightweight 태그를만들때에는 -a, -s, -m 옵션을사용하지않는다 : $ git tag v1.4-lw $ git tag v0.1 v1.3 v1.4 v1.4-lw v1.5 이태그에 git show 를실행하면별도의태그정보를확인할수없다. 이명령은단순히커밋정보만을 보여준다 : $ git show v1.4-lw commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <schacon@gee-mail.com> Date: Sun Feb 8 19:02:46 2009-0800 Merge branch 'experiment' 39

2 장 Git 의기초 Scott Chacon Pro Git 2.6.6 태그검증하기 git tag -v [ 태그이름 ] 명령으로서명한태그를검증한다. 이명령은 GPG를사용하여서명을검증한다. 그래서서명자의 GPG 공개키가필요하다. 이공개키가 Keyring에있어야만이명령이성공적으로실행된다 : $ git tag -v v1.4.2.1 object 883653babd8ee7ea23e6a5c392bb739348b1eb61 type commit tag v1.4.2.1 tagger Junio C Hamano <junkio@cox.net> 1158138501-0700 GIT 1.4.2.1 Minor fixes since 1.4.2, including git-mv and git-http with alternates. gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Good signature from "Junio C Hamano <junkio@cox.net>" gpg: aka "[jpeg image of size 1513]" Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A 만약서명자의공개키가없으면아래와같은메시지를출력한다 : gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Can't check signature: public key not found error: could not verify the tag 'v1.4.2.1' 2.6.7 나중에태그하기 예전커밋에대해서도태그할수있다. 커밋히스토리는아래와같고 : $ git log --pretty=oneline 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment' a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support 0d52aaab4479697da7686c15f77a3d64d9165190 one more thing 6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment' 0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function 4682c3261057305bdd616e23b64b0857d832627b added a todo file 166ae0c4d3f420721acbb115cc33848dfcc2121a started write support 9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile 964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo 8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme 40

Scott Chacon Pro Git 2.6 절태그 updated rakefile 커밋을 v1.2 로태그하지못했다고해도차후에태그를붙일수있다. 특정커밋 에태그하기위해서명령의끝에커밋체크섬을명시한다 ( 긴체크섬을전부사용할필요는없다 ): $ git tag -a v1.2 -m 'version 1.2' 9fceb02 이제아래와같이만든태그를확인한다 : $ git tag v0.1 v1.2 v1.3 v1.4 v1.4-lw v1.5 $ git show v1.2 tag v1.2 Tagger: Scott Chacon <schacon@gee-mail.com> Date: Mon Feb 9 15:32:16 2009-0800 version 1.2 commit 9fceb02d0ae598e95dc970b74767f19372d61af8 Author: Magnus Chacon <mchacon@gee-mail.com> Date: Sun Apr 27 20:43:35 2008-0700... updated rakefile 2.6.8 태그공유하기 git push 명령은자동으로리모트서버에태그를전송하지않는다. 태그를만들었으면서버에별도로 Push해야한다. 브랜치를공유하는것과같은방법으로할수있다. git push origin [ 태그이름 ] 을실행한다 : $ git push origin v1.5 Counting objects: 50, done. Compressing objects: 100% (38/38), done. Writing objects: 100% (44/44), 4.56 KiB, done. Total 44 (delta 18), reused 8 (delta 1) To git@github.com:schacon/simplegit.git * [new tag] v1.5 -> v1.5 41

2 장 Git 의기초 Scott Chacon Pro Git 만약한번에태그를여러개 Push 하고싶으면 --tags 옵션을추가하여 git push 명령을실행한다. 이 명령으로리모트서버에없는태그를모두전송할수있다 : $ git push origin --tags Counting objects: 50, done. Compressing objects: 100% (38/38), done. Writing objects: 100% (44/44), 4.56 KiB, done. Total 44 (delta 18), reused 8 (delta 1) To git@github.com:schacon/simplegit.git * [new tag] v0.1 -> v0.1 * [new tag] v1.2 -> v1.2 * [new tag] v1.4 -> v1.4 * [new tag] v1.4-lw -> v1.4-lw * [new tag] v1.5 -> v1.5 누군가저장소에서 Clone 하거나 Pull 을하면모든태그정보도함께전송된다. 2.7 팁과트릭 Git의기초를마치기전에 Git을좀더쉽고편안하게쓸수있게만들어줄몇가지팁과트릭도설명한다. 이런팁없이 Git을사용하는사람들도많다. 우리는이책에서이팁을다시거론하지않고이런팁을알고있다고가정한다. 그래서알고있는것이좋다. 2.7.1 자동완성 Bash 쉘을쓰고있다면멋진자동완성 (Auto-completion) 기능을사용할수있다. https:// github.com/git/git/blob/master/contrib/completion/git-completion.bash 에서바로다운받는다. 그파일을홈디렉토리에카피하고.bashrc 파일에아래와같은내용을추가하자 : source ~/git-completion.bash 또모든사용자가사용할수있게설정할수있다. Mac 시스템이라면이스크립트를 /opt/local/etc/ bash_completion.d 디렉토리에복사하고리눅스라면 /etc/bash_completion.d/ 에복사한다. 이디렉토리는 Bash가자동완성을지원하기위해사용하는디렉토리다. 윈도에 msysgit을설치해서 Git Bash를사용하는경우에는자동완성이미리설정되어있다. Git 명령을입력할때 <Tab> 키를누르면 Git이제안하는명령어가출력된다 : $ git co<tab><tab> commit config 이경우 git co 를입력하고 Tab 키를두번누르면 commit 과 config 를제안한다. 이때 m<tab> 을입력 하면자동으로 git commit 명령을완성한다. 42

Scott Chacon Pro Git 2.7 절팁과트릭 옵션에도이기능이되고더유용하다. 예를들어 git log 명령을실행하는데옵션이전혀기억나지않 는다면아래와같이입력하고 Tap 키를누르면아래와같은옵션을제안한다 : $ git log --s<tab> --shortstat --since= --src-prefix= --stat --summary 이건상당히멋진팁이다. 아마문서를찾아보는등의시간을절약해줄것이다. 2.7.2 Git Alias 명령을완벽하게입력하지않으면 Git 은알아듣지못한다. Git 의명령을전부입력하는것이귀찮다면 git config 를사용하여각명령의 Alias 을쉽게만들수있다. 아래는 Alias 을만드는예이다 : $ git config --global alias.co checkout $ git config --global alias.br branch $ git config --global alias.ci commit $ git config --global alias.st status 이제 git commit 대신 git ci만으로도커밋할수있다. Git을계속사용한다면다른명령어도자주사용하게될것이다. 자주사용하는명령은 Alias을만들어편하게사용한다. 이미있는명령을편리하고새로운명령으로만들어사용할수있다. 예를들어파일을 Unstage 상태로변경하는명령을만들어서불편함을덜수있다. 아래와같이 unstage 라는 Alias을만든다 : $ git config --global alias.unstage 'reset HEAD --' 아래두명령은동일한명령이다 : $ git unstage filea $ git reset HEAD filea 한결간결해졌다. 추가로 last 명령을만들어보자 : $ git config --global alias.last 'log -1 HEAD' 이제최근커밋을좀더쉽게확인할수있다 : 43

2 장 Git 의기초 Scott Chacon Pro Git $ git last commit 66938dae3329c7aebe598c2246a8e6af90d04646 Author: Josh Goebel <dreamer3@example.com> Date: Tue Aug 26 19:48:51 2008 +0800 test for current head Signed-off-by: Scott Chacon <schacon@example.com> 이것으로쉽게새로운명령을만들수있다. 그리고 Git 의명령어뿐만아니라외부명령어도실행할수 있다.! 를제일앞에추가하면외부명령을실행한다. 아래명령은 git visual 이라고입력하면 gitk 가 실행된다 : $ git config --global alias.visual '!gitk' 2.8 요약 이제우리는로컬에서사용할수있는 Git 명령에대한기본지식은갖추었다. 저장소를만들고 Clone 하는방법, 수정하고나서 Stage하고커밋하는방법, 저장소의히스토리를조회하는방법등을살펴보았다. 이어지는장에서는 Git의가장강력한기능인브랜치모델을살펴볼것이다. 44

3 장 Git 브랜치 모든버전관리시스템은브랜치를지원한다. 개발을하다보면코드를여러개로복사해야하는일이자주생긴다. 코드를통째로복사하고나서원래코드와는상관없이독립적으로개발을진행할수있는데, 이렇게독립적으로개발하는것이브랜치다. 버전관리시스템에서브랜치를만드는과정은고생스럽다. 개발자가수동으로소스코드디렉토리를복사해서브랜치를만들어야하고소스코드의양이많으면브린채를만드는시간도오래걸린다. 사람들은브랜치모델이 Git의최고의장점이라고, Git이다른것들과구분되는특징이라고말한다. 당최어떤점이그렇게특별한것일까? Git의브랜치는매우가볍다. 순식간에브랜치를새로만들고브랜치사이를이동할수있다. 다른버전관리시스템과는달리 Git은브랜치를만들어작업하고나중에 Merge하는방법을권장한다. 심지어하루에수십번씩해도괜찮다. Git 브랜치에능숙해지면개발방식이완전히바뀌고다른도구를사용할수없게된다. 3.1 브랜치란무엇인가? Git이브랜치하는과정을이해하려면우선 Git이데이터를어떻게저장하는지알아야한다. Git은데이터를 Change Set이나변경사항 (Diff) 으로기록하지않고일련의스냅샷으로기록한다는것을 1장에서보여줬다. 커밋하면 Git은현 Staging Area에있는데이터의스냅샷에대한포인터, 저자나커밋메시지같은메타데이터, 이전커밋에대한포인터등을포함하는커밋개체 ( 커밋 Object) 를저장한다. 이전커밋포인터가있어서현재커밋이무엇을기준으로바뀌었는지를알수있다. 최초커밋을제외한나머지커밋은이전커밋포인터가적어도하나씩있고브랜치를합친 Merge 커밋같은경우에는이전커밋포인터가여러개있다. 예제를보자. 파일이 3개있는디렉토리가하나있고이파일을 Staging Area에저장하고커밋해보자. 파일을 Stage하면 Git 저장소에파일을저장하고 (Git은이것을 Blob이라고부른다 ) Staging Area에해당파일의체크섬을저장한다 (1장에서살펴본 SHA-1을사용한다 ). $ git add README test.rb LICENSE $ git commit -m 'initial commit of my project' git commit 으로커밋하면먼저루트디렉토리와각하위디렉토리의트리개체를체크섬과함께저 장소에저장한다. 그다음에커밋개체를만들고메타데이터와루트디렉토리트리개체를가리키는포 인터정보를커밋개체에넣어저장한다. 그래서필요하면언제든지스냅샷을다시만들수있다. 45

3 장 Git 브랜치 Scott Chacon Pro Git 이작업을마치고나면 Git 저장소에는다섯개의데이터개체가생긴다. 각파일에대한 Blob 세개, 파 일과디렉토리구조가들어있는트리개체하나, 메타데이터와루트트리를가리키는포인터가담긴커 밋개체하나이다. 이것을그림으로그리면그림 3-1 과같다. 그림 3.1: 저장소의커밋데이터 다시파일을수정하고커밋하면이전커밋이무엇인지도저장한다. 커밋을두번더하면그림 3-2 과 같이저장된다. 그림 3.2: Git 커밋의개체데이터 Git 의브랜치는커밋사이를가볍게이동할수있는어떤포인터같은것이다. 기본적으로 Git 은 master 브랜치를만든다. 최초로커밋하면 Git 은 master 라는이름의브랜치를만들어서자동으로가장마지 막커밋을가리키게한다. 그림 3.3: 가장최근커밋정보를가리키는브랜치 브랜치를하나새로만들면어떨까? 브랜치를하나만들어서놀자. 다음과같이 git branch 명령으로 testing 브랜치를만든다. 46

Scott Chacon Pro Git 3.1 절브랜치란무엇인가? $ git branch testing 새로만든브랜치도지금작업하고있던마지막커밋을가리킨다 ( 그림 3-4). 그림 3.4: 커밋개체를가리키는두브랜치 지금작업중인브랜치가무엇인지 Git은어떻게파악할까? 다른버전관리시스템과는달리 Git은 HEAD 라는특수한포인터가있다. 이포인터는지금작업하는로컬브랜치를가리킨다. 브랜치를새로만들었지만, Git은아직 master 브랜치를가리키고있다. git branch 명령은브랜치를만들기만하고브랜치를옮기지않는다. 그림 3.5: HEAD 는현재작업중인브랜치를가리킴 git checkout 명령으로새로만든브랜치로이동할수있다. testing 브랜치로이동하려면다음과같이 한다 : $ git checkout testing 이렇게하면 HEAD 는 testing 브랜치를가리킨다. 자, 이제핵심이보일거다! 커밋을새로한번해보면 : $ vim test.rb $ git commit -a -m 'made a change' 47

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.6: HEAD 는옮겨간다른브랜치를가리킨다 그림 3.7: HEAD 가가리키는 testing 브랜치가새커밋을가리킨다 결과는그림 3-7 과같다. 이부분이흥미롭다. 새로커밋해서 testing 브랜치는앞으로이동했다. 하지만, master 브랜치는여전 히이전커밋을가리킨다. master 브랜치로되돌아가면 : $ git checkout master 결과는그림 3-8 과같다. 그림 3.8: HEAD 가 Checkout 한브랜치로이동함 방금실행한명령이한일은두가지다. master 브랜치가가리키는커밋을 HEAD 가가리키게하고워 킹디렉토리의파일도그시점으로되돌려놓았다. 앞으로커밋을하면다른브랜치의작업들과별개로 48

Scott Chacon Pro Git 3.2 절브랜치와 Merge 의기초 진행되기때문에 testing 브랜치에서임시로작업하고원래 master 브랜치로돌아와서하던일을계속 할수있다. 파일을수정하고다시커밋을해보자 : $ vim test.rb $ git commit -a -m 'made other changes' 프로젝트히스토리는분리돼진행한다 ( 그림 3-9). 우리는브랜치를하나만들어그브랜치에서일을좀하고, 다시원래브랜치로되돌아와서다른일을했다. 두작업내용은서로독립적으로각브랜치에존재한다. 커밋사이를자유롭게이동하다가때가되면두브랜치를 Merge한다. 간단히 branch와 checkout 명령을써서말이다. 그림 3.9: 브랜치히스토리가서로독립적임 실제로 Git의브랜치는어떤한커밋을가리키는 40글자의 SHA-1 체크섬파일에불과하기때문에만들기도쉽고지우기도쉽다. 새로브랜치를하나만드는것은 41바이트크기의파일을 (40자와줄바꿈문자 ) 하나만드는것에불과하다. 브랜치를만들어야하면프로젝트를통째로복사해야하는다른버전관리도구와 Git의차이는극명하다. 통째로복사하는작업은프로젝트크기에따라다르겠지만수십초에서수십분까지걸린다. 그에비해 Git은순식간이다. 게다가커밋을할때마다이전커밋의정보를저장하기때문에 Merge할때어디서부터 (Merge Base) 합쳐야하는지안다. 이런특징은개발자들이수시로브랜치를만들어사용하게한다. 이제왜그렇게브랜치를수시로만들고사용해야하는지알아보자. 3.2 브랜치와 Merge 의기초 실제개발과정에서겪을만한예제를하나살펴보자. 브랜치와 Merge 는보통이런식으로진행한다 : 1. 작업중인웹사이트가있다. 2. 새로운이슈를처리할새 Branch를하나생성. 3. 새로만든 Branch에서작업중. 49

3 장 Git 브랜치 Scott Chacon Pro Git 이때중요한문제가생겨서그것을해결하는 Hotfix를먼저만들어야한다. 그러면다음과같이할수있다 : 1. 새로운이슈를처리하기이전의운영 (Production) 브랜치로복원. 2. Hotfix 브랜치를새로하나생성. 3. 수정한 Hotfix 테스트를마치고운영브랜치로 Merge. 4. 다시작업하던브랜치로옮겨가서하던일진행. 3.2.1 브랜치의기초 먼저커밋을몇번했다고가정하자. 그림 3.10: 현재커밋히스토리 이슈관리시스템에등록된 53 번이슈를처리한다고하면이이슈에집중할수있는브랜치를새로하 나만든다. Git 은어떤이슈관리시스템에도종속돼있지않다. 브랜치를만들면서 Checkout 까지한 번에하려면 git checkout 명령에 -b 라는옵션을준다. $ git checkout -b iss53 Switched to a new branch 'iss53' 위명령은아래명령을줄여놓은것이다 : $ git branch iss53 $ git checkout iss53 그림 3-11 은위명령의결과를나타낸다. 그림 3.11: 브랜치포인터를새로만듦 iss53 브랜치를 Checkout 했기때문에 ( 즉, HEAD 는 iss53 브랜치를가리킨다 ) 뭔가일을하고커밋 하면 iss53 브랜치가앞으로진행한다 : 50

Scott Chacon Pro Git 3.2 절브랜치와 Merge 의기초 $ vim index.html $ git commit -a -m 'added a new footer [issue 53]' 그림 3.12: 진행중인 iss53 브랜치 다른상황을가정해보자. 만드는사이트에문제가생겨서즉시고쳐야한다. 버그를해결한 Hotfix에 iss53 이섞이는것을방지하기위해 iss53 와관련된코드를어딘가에저장해두고원래운영환경의소스로복구해야한다. Git을사용하면이런노력을들일필요없이그냥 master 브랜치로옮기면된다. 그렇지만, 브랜치를이동하려면해야할일이있다. 아직커밋하지않은파일이 Checkout할브랜치와충돌나면브랜치를변경할수없다. 브랜치를변경할때에는워킹디렉토리를정리하는것이좋다. 이런문제를다루는방법은 ( 주로, Stash이나커밋 Amend에대해 ) 나중에다룰것이다. 지금은작업하던것을모두커밋하고 master 브랜치로옮긴다 : $ git checkout master Switched to branch 'master' 이때워킹디렉토리는 53번이슈를시작하기이전모습으로되돌려지기때문에새로운문제에집중할수있는환경이만들어진다. Git은자동으로워킹디렉토리에파일들을추가하고, 지우고, 수정해서 Checkout한브랜치의스냅샷으로되돌려놓는다는것을기억해야한다. hotfix라는브랜치를만들고새로운이슈를해결할때까지사용한다 : $ git checkout -b hotfix Switched to a new branch 'hotfix' $ vim index.html $ git commit -a -m 'fixed the broken email address' [hotfix]: created 3a0874c: 'fixed the broken email address' 1 files changed, 0 insertions(+), 1 deletions(-) 운영환경에적용하려면문제를제대로고쳤는지테스트하고 master 브랜치에합쳐야한다. git merge 명령으로다음과같이한다 : $ git checkout master 51

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.13: master 브랜치에서갈라져나온 hotfix 브랜치 $ git merge hotfix Updating f42c576..3a0874c Fast forward README 1-1 files changed, 0 insertions(+), 1 deletions(-) Merge 메시지에서 Fast forward 가보이는가? Merge할브랜치가가리키고있던커밋이현브랜치가가리키는것보다 앞으로진행한 커밋이기때문에 master 브랜치포인터는최신커밋으로이동한다. 이런 Merge 방식을 Fast forward 라고부른다. 다시말해서 A 브랜치에서다른 B 브랜치를 Merge할때 B가 A 이후의커밋을가리키고있으면그저 A가 B의커밋을가리키게할뿐이다. 이제 hotfix는 master 브랜치에포함됐고운영환경에적용할수있다 ( 그림 3-14). 그림 3.14: Merge 후 hotfix 브랜치와같은것을가리키는 master 브랜치 문제를급히해결하고 master 브랜치에적용하고나면다시일하던브랜치로돌아가야한다. 하지만, 그전에필요없는 hotfix 브랜치를삭제한다. git branch 명령에 -d 옵션을주고브랜치를삭제한다. $ git branch -d hotfix Deleted branch hotfix (3a0874c). 자이제이슈 53 번을처리하던환경으로되돌아가서하던일을계속하자 ( 그림 3-15): 52

Scott Chacon Pro Git 3.2 절브랜치와 Merge 의기초 $ git checkout iss53 Switched to branch 'iss53' $ vim index.html $ git commit -a -m 'finished the new footer [issue 53]' [iss53]: created ad82d7a: 'finished the new footer [issue 53]' 1 files changed, 1 insertions(+), 0 deletions(-) 그림 3.15: master 와별개로진행하는 iss53 브랜치 위에서작업한 hotfix가 iss53 브랜치에영향을끼치지않는다는점을이해하는것이중요하다. git merge master 명령으로 master 브랜치를 iss53 브랜치에 Merge하면 iss53 브랜치에 hotfix가적용된다. 아니면 iss53 브랜치가 master에 Merge할수있는수준이될때까지기다렸다가 Merge하면 hotfix와 iss53가합쳐진다. 3.2.2 Merge의기초 53번이슈를다구현하고 master 브랜치에 Merge하는과정을살펴보자. master 브랜치에 Merge 하는것은앞서살펴본 hotfix 브랜치를 Merge하는것과비슷하다. git merge 명령으로합칠브랜치에서합쳐질브랜치를 Merge하면된다 : $ git checkout master $ git merge iss53 Merge made by recursive. README 1 + 1 files changed, 1 insertions(+), 0 deletions(-) hotfix를 Merge했을때와메시지가다르다. 현브랜치가가리키는커밋이 Merge할브랜치의조상이아니므로 Git은 Fast-forward 로 Merge하지않는다. 이러면 Git은각브랜치가가리키는커밋두개와공통조상하나를사용하여 3-way Merge를한다. 그림 3-16에이 Merge에서사용하는커밋세개가표시된다. 단순히브랜치포인터를최신커밋으로옮기는게아니라 3-way Merge의결과를별도의커밋으로만들고나서해당브랜치가그커밋을가리키도록이동시킨다 ( 그림 3-17). 그래서이런커밋은부모가여러개고 Merge 커밋이라고부른다. 53

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.16: Git 은 Merge 에필요한공통커밋을자동으로찾음 Git 은 Merge 하는데필요한최적의공통조상을자동으로찾는다. 이런기능도 Git 이다른버전관리 시스템보다나은점이다. CVS 나 Subversion 같은버전관리시스템은개발자가직접공통조상을찾 아서 Merge 해야한다. Git 은다른시스템보다 Merge 가대단히쉽다. 그림 3.17: Git 은 Merge 할때 Merge 에대한정보가들어있는커밋를하나만든다. iss53 브랜치를 master 에 Merge 하고나면더는 iss53 브랜치가필요없다. 다음명령으로브랜치를 삭제하고이슈의상태를처리완료로표시한다 : $ git branch -d iss53 3.2.3 충돌의기초가끔씩 3-way Merge가실패할때도있다. Merge하는두브랜치에서같은파일의한부분을동시에수정하고 Merge하면 Git은해당부분을 Merge하지못한다. 예를들어, 53번이슈와 hotfix가같은부분을수정했다면 Git은 Merge하지못하고다음과같은충돌 (Conflict) 메시지를출력한다 : $ git merge iss53 Auto-merging index.html 54

Scott Chacon Pro Git 3.2 절브랜치와 Merge 의기초 CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. Git 은자동으로 Merge 하지못해서새커밋이생기지않는다. 변경사항의충돌을개발자가해결하지 않는한 Merge 과정을진행할수없다. Merge 충돌이일어났을때 Git 이어떤파일을 Merge 할수없 었는지살펴보려면 git status 명령을이용한다 : [master*]$ git status index.html: needs merge # On branch master # Changes not staged for commit: # (use 'git add <file>...' to update what will be committed) # (use 'git checkout -- <file>...' to discard changes in working directory) # # unmerged: index.html # 충돌이일어난파일은 unmerged 상태로표시된다. Git 은충돌이난부분을표준형식에따라표시해 준다. 그러면개발자는해당부분을수동으로해결한다. 충돌난부분은다음과같이표시된다. <<<<<<< HEAD:index.html <div id='footer'>contact : email.support@github.com</div> ======= <div id='footer'> please contact us at support@github.com </div> >>>>>>> iss53:index.html ======= 위쪽의내용은 HEAD 버전 (merge 명령을실행할때작업하던 master 브랜치 ) 의내용이고 아래쪽은 iss53 브랜치의내용이다. 충돌을해결하려면위쪽이나아래쪽내용중에서고르거나새로작 성하여 Merge 한다. 다음은아예새로작성하여충돌을해결하는예제다 : <div id='footer'> please contact us at email.support@github.com </div> 충돌한양쪽에서조금씩가져와서새로수정했다. 그리고 <<<<<<<, =======, >>>>>>> 가포함된행을삭제하였다. 이렇게충돌한부분을해결하고 git add 명령으로다시 Git에저장한다. 충돌을쉽게해결하기위해다른 Merge 도구도이용할수있다. git mergetool 명령으로실행한다 : 55

3 장 Git 브랜치 Scott Chacon Pro Git $ git mergetool merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff Merging the files: index.html Normal merge conflict for 'index.html': {local}: modified {remote}: modified Hit return to start merge resolution tool (opendiff): Mac에서는 opendiff가실행된다. 기본도구말고사용할수있는다른 Merge 도구도있는데, merge tool candidates 부분에보여준다. 여기에표시된도구중하나를고를수있다. Merge 도구를변경하는방법은 7장에서다룬다. Merge 도구를종료하면 Git은잘 Merge했는지물어본다. 잘마쳤다고입력하면자동으로 git add가수행되고해당파일이 Staging Area에저장된다. git status 명령으로충돌이해결된상태인지다시한번확인해볼수있다. $ git status # On branch master # Changes to be committed: # (use 'git reset HEAD <file>...' to unstage) # # modified: index.html # 충돌을해결하고나서해당파일이 Staging Area 에저장됐는지확인했으면 git commit 명령으로 Merge 한것을커밋한다. 충돌을해결하고 Merge 할때에는커밋메시지가아래와같다. Merge branch 'iss53' Conflicts: index.html # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file #.git/merge_head # and try again. # 어떻게충돌을해결했고좀더확인해야하는부분은무엇을어떻게했는지자세하게기록한다. 자세한 기록은나중에이 Merge 커밋을이해하는데도움을줄것이다. 56

Scott Chacon Pro Git 3.3 절브랜치관리 3.3 브랜치관리 지금까지브랜치를만들고, Merge하고, 삭제하는방법에대해서살펴봤다. 브랜치를관리하는데필요한다른명령도살펴보자. git branch 명령은단순히브랜치를만들고삭제해주기만하는것이아니다. 아무런옵션없이실행하면브랜치의목록을보여준다 : $ git branch iss53 * master testing * 기호가붙어있는 master 브랜치는현재 Checkout 해서작업하는브랜치를나타낸다. 즉, 지금수정 한내용을커밋하면 master 브랜치에커밋되고포인터가앞으로한단계나아간다. git branch -v 명 령을실행하면브랜치마다마지막커밋메시지도함께보여준다 : $ git branch -v iss53 93b412c fix javascript issue * master 7a98805 Merge branch 'iss53' testing 782fd34 add scott to the author list in the readmes 각브랜치가지금어떤상태인지확인하기에좋은옵션도있다. 현재 Checkout 한브랜치를기준으로 Merge 된브랜치인지그렇지않은지필터링해볼수있다. --merged 와 --no-merged 옵션을사용하여해 당목록을볼수있다. git branch --merged 명령으로이미 Merge 한브랜치목록을확인한다 : $ git branch --merged iss53 * master iss53 브랜치는앞에서이미 Merge했기때문에목록에나타난다. * 기호가붙어있지않은브랜치는 git branch -d 명령으로삭제해도되는브랜치다. 이미다른브랜치와 Merge 했기때문에삭제해도정보를잃지않는다. 반대로현재 Checkout한브랜치에 Merge하지않은브랜치를살펴보려면 git branch --no-merged 명령을사용한다 : $ git branch --no-merged testing 위에는없었던다른브랜치가보인다. 아직 Merge 하지않은커밋을담고있기때문에 git branch -d 명령으로삭제되지않는다 : 57

3 장 Git 브랜치 Scott Chacon Pro Git $ git branch -d testing error: The branch 'testing' is not an ancestor of your current HEAD. If you are sure you want to delete it, run 'git branch -D testing'. Merge 하지않은브랜치를강제로삭제하려면 -D 옵션으로삭제한다. 3.4 브랜치 Workflow 브랜치를만들고 Merge 하는것을어디에써먹어야할까? 이절에서는 Git 의브랜치가유용한몇가 지 Workflow 를살펴본다. 여기서설명하는 Workflow 를개발에적용하면도움이될것이다. 3.4.1 Long-Running 브랜치 Git은꼼꼼하게 3-way Merge를사용하기때문에장기간에걸쳐서한브랜치를다른브랜치와여러번 Merge하는것도어렵지않다. 그래서개발과정에서필요한용도에따라브랜치를만들어두고계속사용할수있다. 그리고정기적으로브랜치를다른브랜치로 Merge한다 : 이런접근법에따라서 Git 개발자가많이선호하는 Workflow가하나있다. 배포했거나배포할코드만 master 브랜치에 Merge해서안정버전의코드만 master 브랜치에둔다. 개발을진행하고안정화하는브랜치는 develop이나 next라는이름으로추가로만들어사용한다. 이브랜치는언젠가안정상태가되겠지만, 항상안정상태를유지해야하는것이아니다. 테스트를거쳐서안정적이라고판단되면 master 브랜치에 Merge한다. 토픽브랜치 ( 앞서살펴본 iss53 브랜치같은짧은호흡브랜치 ) 에도적용할수있는데, 해당토픽을처리하고테스트해서버그도없고안정적이면그때 Merge한다. 사실우리가얘기하는것은커밋을가리키는포인터에대한얘기다. 개발브랜치는공격적으로히스토리를만들어나아가고안정브랜치는이미만든히스토리를뒤따르며나아간다. 그림 3.18: 안정적인브랜치일수록커밋히스토리가뒤쳐진다 실험실에서충분히테스트하고실전에배치하는과정으로보면이해하기쉽다 ( 그림 3-19). 그림 3.19: 각브랜치를하나의실험실로생각하라 58

Scott Chacon Pro Git 3.4 절브랜치 Workflow 코드를여러단계로나누어안정성을높여가며운영할수있다. 큰규모의프로젝트라면 proposed 혹은 pu(proposed updates) 라는이름의브랜치를두어 next나 master 브랜치에아직 Merge할준비가되지않은것을일단 Merge시킨다. 중요한개념은브랜치를이용해여러단계에걸쳐서안정화해나아가면서충분히안정화가됐을때안정브랜치로 Merge한다는점이다. 다시말해서반드시 Long-Running의브랜치를여러개만들어야하는것은아니지만정말유용하다. 특히규모가크고복잡한프로젝트일수록그유용성이반짝반짝빛난다. 3.4.2 토픽브랜치토픽브랜치는프로젝트크기에상관없이유용하다. 토픽브랜치는어떤한가지주제나작업을위해만든짧은호흡의브랜치다. 다른버전관리시스템에서이런브랜치를본적이없을것이다. Git이아닌다른버전관리도구에서는브랜치를하나만드는데큰비용이든다. Git에서는매우일상적으로브랜치를만들고 Merge하고삭제한다. 앞서사용한 iss53이나 hotfix 브랜치가토픽브랜치다. 우리는브랜치를새로만들고어느정도커밋하고나서다시 master 브랜치에 Merge하고브랜치삭제도해보았다. 보통주제별로브랜치를만들고각각은독립돼있기때문에매우쉽게컨텍스트사이를옮겨다닐수있다. 묶음별로나눠서일하면내용별로검토하기에도, 테스트하기에도더편하다. 각작업을하루든한달이든유지하다가 master 브랜치에 Merge할시점이되면순서에관계없이그때 Merge하면된다. master 브랜치를 checkout한상태에서어떤작업을한다고해보자. 한이슈를처리하기위해서 iss91라는브랜치를만들고해당작업을한다. 같은이슈를다른방법으로해결해보고싶을때도있다. iss91v2라는브랜치를만들고다른방법을시도해본다. 확신할수없는아이디어를적용해보기위해다시 master 브랜치로되돌아가서 dumbidea 브랜치를하나더만든다. 지금까지말했던커밋히스토리는그림 3-20과같다. 그림 3.20: 여러토픽브랜치에대한커밋히스토리 이슈를처리했던방법중두번째방법인 iss91v2 브랜치가괜찮아서적용하기로결정을내렸다. 그리고아이디어를확신할수없었던 dumbidea 브랜치를같이일하는다른개발자에게보여줬더니썩괜찮다는반응을얻었다. iss91 브랜치는 (C5, C6 커밋도함께 ) 버리고다른두브랜치를 Merge하면그림 3-21과같이된다. 59

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.21: dumbidea 와 iss91v2 브랜치를 Merge 하고난후의모습 지금까지한작업은전부로컬에서만처리한다는것을꼭기억하자. 로컬저장소에서만브랜치를만들 고 Merge 했으며서버와통신을주고받는일은없었다. 3.5 리모트브랜치 리모트브랜치란리모트저장소에있는브랜치를말한다. 사실리모트브랜치도로컬에있지만멋대로옮기거나할수없고리모트저장소와통신하면자동으로업데이트된다. 리모트브랜치는브랜치상태를알려주는책갈피라고볼수있다. 이책갈피로리모트저장소에서마지막으로데이터를가져온시점의상태를알수있다. 리모트브랜치의이름은 (remote)/(branch) 형식으로되어있다. 예를들어리모트저장소 origin의 master 브랜치를보고싶다면 origin/master라는이름으로브랜치를확인하면된다. 다른팀원과함께어떤이슈를구현할때그팀원이 iss53 브랜치를서버로 Push했고당신도로컬에 iss53 브랜치가있다고가정하자. 이때서버가가리키는 iss53 브랜치는로컬에서 origin/iss53이가리키는커밋이다. 다소헷갈릴수있으니예제를좀더살펴보자. git.ourcompany.com이라는 Git 서버가있고이서버의저장소를하나 Clone하면 Git은자동으로 origin이라는이름을붙인다. origin으로부터저장소데이터를모두내려받고 master 브랜치를가리키는포인터를만든다. 이포인터는 origin/master라고부르고멋대로조종할수없다. 그리고 Git은로컬의 master 브랜치가 origin/master를가리키게한다. 이제이 master 브랜치에서작업을시작할수있다. 로컬저장소에서어떤작업을하고있는데동시에다른팀원이 git.ourcompany.com 서버에 Push하고 master 브랜치를업데이트한다. 그러면이제팀원간의히스토리는서로달라진다. 서버저장소로부터어떤데이터도주고받지않아서 origin/master 포인터는그대로다. 리모트서버로부터저장소정보를동기화하려면 git fetch origin 명령을사용한다. 명령을실행하면우선 origin 서버의주소정보 ( 이예에서는 git.ourcompany.com) 를찾아서, 현재로컬의저장소가갖고있지않은새로운정보가있으면모두내려받고, 받은데이터를로컬저장소에업데이트하고나서, origin/master 포인터의위치를최신커밋으로이동시킨다. 리모트저장소를여러개운영하는상황을이해할수있도록개발용으로사용할 Git 저장소를팀내부 60

Scott Chacon Pro Git 3.5 절리모트브랜치 그림 3.22: 저장소를 Clone 하면로컬 master 브랜치, 리모트저장소의 master 브랜치를가리키는 origin/master 브랜치가생김 그림 3.23: 로컬과서버의커밋히스토리는독립적임 에하나추가해보자. 이저장소의주소가 git.team1.ourcompany.com 이면 2장에서살펴본 git remote add 명령으로현재작업중인프로젝트에팀의저장소를추가한다. 이름을 teamone으로짓고긴서버주소대신사용한다. 서버를추가하고나면 git fetch teamone 명령으로 teamone 서버의데이터를내려받는다. 명령을실행해도 teamone 서버의데이터는모두 origin 서버에도있는것들이라서아무것도내려받지않는다. 하지만, 이명령은 teamone/master 브랜치가 teamone 서버의 master 브랜치가가리키는커밋을가리키게한다. 3.5.1 Push하기로컬의브랜치를서버로전송하려면쓰기권한이있는리모트저장소에 Push해야한다. 로컬저장소의브랜치는자동으로리모트저장소로전송되지않는다. 명시적으로브랜치를 Push해야정보가전송된다. 따라서리모트저장소에전송하지않고로컬브랜치에만두는비공개브랜치를만들수있다. 또 61

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.24: Git 의 Fetch 명령은리모트브랜치정보를업데이트한다 그림 3.25: 서버를리모트저장소로추가하기 다른사람과협업하기위해토픽브랜치만전송할수도있다. serverfix 라는브랜치를다른사람과공유할때에도브랜치를처음 Push 하는것과같은방법으로 Push 한다. 다음과같이 git push (remote) (branch) 명령을사용한다 : $ git push origin serverfix Counting objects: 20, done. Compressing objects: 100% (14/14), done. Writing objects: 100% (15/15), 1.74 KiB, done. Total 15 (delta 5), reused 0 (delta 0) To git@github.com:schacon/simplegit.git 62

Scott Chacon Pro Git 3.5 절리모트브랜치 그림 3.26: 로컬저장소에만들어진 teamone 의 master 브랜치를가리키는포인터 * [new branch] serverfix -> serverfix 이메시지에는숨겨진내용이많다. Git은 serverfix라는브랜치이름을 refs/heads/serverfix:refs/heads/serverfix로확장한다. 이것은 serverfix라는로컬브랜치를서버로 Push하는데리모트의 serverfix 브랜치로업데이트한다는것을의미한다. 나중에 9장에서 refs/heads/ 의뜻을자세히알아볼것이기때문에일단넘어가도록한다. git push origin serverfix:serverfix라고 Push하는것도같은의미인데이것은 로컬의 serverfix 브랜치를리모트저장소의 serverfix 브랜치로 Push하라 라는뜻이다. 로컬브랜치의이름과리모트서버의브랜치이름이다를때필요하다. 리모트저장소에 serverfix라는이름대신다른이름을사용하려면 git push origin serverfix:awesomebranch처럼사용한다. 나중에누군가저장소를 Fetch하고나서서버에있는 serverfix 브랜치에접근할때 origin/serverfix 라는이름으로접근할수있다 : $ git fetch origin remote: Counting objects: 20, done. remote: Compressing objects: 100% (14/14), done. remote: Total 15 (delta 5), reused 0 (delta 0) Unpacking objects: 100% (15/15), done. From git@github.com:schacon/simplegit * [new branch] serverfix -> origin/serverfix 여기서짚고넘어가야할게있다. Fetch 명령으로리모트브랜치를내려받는다고해서로컬저장소에수정할수있는브랜치가새로생기는것이아니다. 다시말해서 serverfix라는브랜치가생기는것이아니라그저수정못하는 origin/serverfix 브랜치포인터가생기는것이다. 새로받은브랜치의내용을 Merge하려면 git merge origin/serverfix 명령을사용한다. Merge하지않고리모트브랜치에서시작하는새브랜치를만들려면아래와같은명령을사용한다. 63

3 장 Git 브랜치 Scott Chacon Pro Git $ git checkout -b serverfix origin/serverfix Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. Switched to a new branch 'serverfix' 그러면 origin/serverfix 에서시작하고수정할수있는 serverfix 라는로컬브랜치가만들어진다. 3.5.2 브랜치추적리모트브랜치를로컬브랜치로 Checkout하면자동으로트래킹 (Tracking) 브랜치가만들어진다. 트래킹브랜치는리모트브랜치와직접적인연결고리가있는로컬브랜치이다. 트래킹브랜치에서 git push 명령을내려도 Git은연결고리가있어서어떤리모트저장소에 Push해야하는지알수있다. 또한 git pull 명령을내리면리모트저장소로부터데이터를내려받아연결된리모트브랜치와자동으로 Merge한다. 서버로부터저장소를 Clone해올때도 Git은자동으로 master 브랜치를 origin/master 브랜치의트래킹브랜치로만든다. 그래서 git push, git pull 명령이추가적인아규먼트없이도동작한다. 트래킹브랜치를직접만들수있는데 origin/master뿐만아니라다른저장소의다른브랜치도추적하게 (Tracking) 할수있다. git checkout -b [branch] [remotename]/[branch] 명령으로간단히트래킹브랜치를만들수있다. Git 1.6.2 버전이상을사용하는경우에는 track 옵션도사용할수있다. $ git checkout --track origin/serverfix Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. Switched to a new branch 'serverfix' 리모트브랜치와다른이름으로브랜치를만들려면로컬브랜치의이름을아래와같이다르게지정한 다 : $ git checkout -b sf origin/serverfix Branch sf set up to track remote branch refs/remotes/origin/serverfix. Switched to a new branch 'sf' 이제 sf 브랜치에서 Push 나 Pull 하면자동으로 origin/serverfix 에데이터를보내거나가져온다. 3.5.3 리모트브랜치삭제동료와협업하기위해리모트브랜치를만들었다가작업을마치고 master 브랜치로 Merge했다. 협업하는데사용했던그리모트브랜치는이제안정화됐으므로삭제할수있다. git push [remotename] :[branch] 라고실행해서삭제할수있는데이명령은좀특이하게생겼다. serverfix라는리모트브랜치를삭제하려면다음과같이실행한다 : 64

Scott Chacon Pro Git 3.6 절 Rebase 하기 $ git push origin :serverfix To git@github.com:schacon/simplegit.git - [deleted] serverfix 위명령을실행하고나면서버의브랜치는삭제된다. 이명령을잊어버릴경우를대비해서페이지귀퉁이를접어놓고필요할때펴보는게좋을지도모르겠다. 이명령은앞서살펴본 git push [remotename] [localbranch]:[remotebranch] 형식으로기억하는것이좋다. [localbranch] 부분에비워둔채로실행하면 로컬에서빈내용을리모트의 [remotebranch] 에채워넣어라 라는뜻이되기때문이다. 3.6 Rebase 하기 Git 에서한브랜치에서다른브랜치로합치는방법은두가지가있다. 하나는 Merge 이고다른하나는 Rebase 다. 이절에서는 Rebase 가무엇인지, 어떻게사용하는지, 좋은점은뭐고, 어떤상황에서사용 하고어떤상황에서사용하지말아야하는지알아본다. 3.6.1 Rebase 의기초 앞의 Merge 절에서살펴본예제로다시돌아가보자 ( 그림 3-27). 두개의나누어진브랜치의모습을 볼수있다. 그림 3.27: 두개의브랜치로나누어진커밋히스토리 이두브랜치를합치는가장쉬운방법은앞에서살펴본대로 Merge 명령을사용하는것이다. 두브랜 치의마지막커밋두개 (C3, C4) 와공통조상 (C2) 을사용하는 3-way Merge 로그림 3-28 처럼새로 운커밋을만들어낸다. 그림 3.28: 나뉜브랜치를 Merge 하기 65

3 장 Git 브랜치 Scott Chacon Pro Git 비슷한결과를만드는다른방식으로, C3에서변경된사항을패치 (Patch) 로만들고이를다시 C4에적용시키는방법이있다. Git에서는이런방식을 Rebase 라고한다. Rebase 명령으로한브랜치에서변경된사항을다른브랜치에적용할수있다. 위의예제는다음과같은명령으로 Rebase한다 : $ git checkout experiment $ git rebase master First, rewinding head to replay your work on top of it... Applying: added staged command 실제로일어나는일을설명하자면일단두브랜치가나뉘기전인공통커밋으로이동하고나서그커밋부터지금 Checkout한브랜치가가리키는커밋까지 diff를차례로만들어어딘가에임시로저장해놓는다. Rebase할브랜치 ( 역주 - experiment) 가합칠브랜치 ( 역주 - master) 가가리키는커밋을가리키게하고아까저장해놓았던변경사항을차례대로적용한다. 그림 3-29는이러한과정을나타내고있다. 그림 3.29: C3 의변경사항을 C4 에적용하는 Rebase 과정 그리고나서 master 브랜치를 Fast-forward 시킨다. 그림 3.30: master 브랜치를 Fast-forward 시키기 C3 로표시된커밋에서의내용은 Merge 예제에서살펴본 C5 커밋에서의내용과같을것이다. Merge 이든 Rebase든둘다합치는관점에서는서로다를게없다. 하지만, Rebase가좀더깨끗한히스토리를만든다. Rebase한브랜치의 Log를살펴보면히스토리가선형적이다. 일을병렬로동시에진행해도 Rebase하고나면모든작업이차례대로수행된것처럼보인다. Rebase는보통리모트브랜치에커밋을깔끔하게적용하고싶을때사용한다. 아마이렇게 Rebase하는리모트브랜치는직접관리하는것이아니라그냥참여하는브랜치일것이다. 메인프로젝트에패치를보낼준비가되면하는것이 Rebase이니까브랜치에서하던일을완전히마치고 origin/master로 Rebase한다. 프로젝트관리자는어떠한통합작업도필요없다. 그냥 master 브랜치를 Fast-forward 시키면된다. Rebase를하든지, Merge를하든지최종결과물은같고커밋히스토리만다르다는것이중요하다. Rebase의경우는브랜치의변경사항을순서대로다른브랜치에적용하면서합치고 Merge의경우는 66

Scott Chacon Pro Git 3.6 절 Rebase 하기 두브랜치의최종결과만을가지고합친다. 3.6.2 좀더 Rebase Rebase는단순히브랜치를합치는것만아니라다른용도로도사용할수있다. 그림 3-31과같은히스토리가있다고하자. server 브랜치를만들어서서버기능을추가하고그브랜치에서다시 client 브랜치를만들어클라이언트기능을추가한다. 마지막으로 server 브랜치로돌아가서몇가지기능을더추가한다. 그림 3.31: 다른토픽브랜치에서갈라져나온토픽브랜치 이때테스트가덜된 server 브랜치는그대로두고 client 브랜치만 master 로합치려는상황을생각해 보자. server 와는아무관련이없는 client 커밋은 C8, C9 이다. 이두커밋을 master 브랜치에적용하 기위해서 --onto 옵션을사용하여아래와같은명령을실행한다 : $ git rebase --onto master server client 이명령은 client 브랜치를 Checkout 하고 server 와 client 의공통조상이후의패치를만들어 master 에적용한다. 조금복잡하긴해도꽤쓸모있다. 그림 3-32 를보자. 이제 master 브랜치로돌아가서 Fast-forward 시킬수있다 : $ git checkout master $ git merge client server 브랜치의일이다끝나면 git rebase [basebranch] [topicbranch] 라는명령으로 Checkout하지않고바로 server 브랜치를 master 브랜치로 rebase할수있다. 이명령은토픽 (server) 브랜치를 Checkout하고베이스 (master) 브랜치에 Rebase한다 : 67

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.32: 다른토픽브랜치에서갈라져나온토픽브랜치를 Rebase 하기 그림 3.33: master 브랜치를 client 브랜치위치로진행시키기 $ git rebase master server server 브랜치의수정사항을 master 브랜치에적용했다. 그결과는그림 3-34 와같다. 그림 3.34: master 브랜치에 server 브랜치의수정사항을적용 그리고나서 master 브랜치를 Fast-forward 시킨다 : $ git checkout master $ git merge server 모든것이 master 브랜치에통합됐기때문에더필요하지않다면 client 나 server 브랜치는삭제해도 된다. 브랜치를삭제해도커밋히스토리는그림 3-35 와같이여전히남아있다 : 68

Scott Chacon Pro Git 3.6 절 Rebase 하기 $ git branch -d client $ git branch -d server 그림 3.35: 최종커밋히스토리 3.6.3 Rebase의위험성 Rebase가장점이많은기능이지만단점이없는것은아니니조심해야한다. 그주의사항은다음한문장으로표현할수있다 : 이미공개저장소에 Push한커밋을 Rebase하지마라이지침만지키면 Rebase를하는데문제될게없다. 하지만, 이주의사항을지키지않으면사람들에게욕을먹을것이다 ( 역주 - 아마도가카의호연지기가필요해질것이다 ). Rebase는기존의커밋을그대로사용하는것이아니라내용은같지만다른커밋을새로만든다. 새커밋을서버에 Push하고동료중누군가가그커밋을 Pull해서작업을한다고하자. 그런데그커밋을 git rebase로바꿔서 Push해버리면동료가다시 Push했을때동료는다시 Merge해야한다. 그리고동료가다시 Merge한내용을 Pull하면내코드는정말엉망이된다. 이미공개저장소에 Push한커밋을 Rebase하면어떤결과가초래되는지예제를통해알아보자. 중앙저장소에서 Clone하고일부수정을하면커밋히스토리는그림 3-36과같아진다. 그림 3.36: 저장소를 Clone 하고일부수정함 이제팀원중누군가커밋, Merge하고나서서버에 Push 한다. 이리모트브랜치를 Fetch, Merge하면그림 3-37과같이된다. 그런데 Push했던팀원은 Merge한일을되돌리고다시 Rebase한다. 서버의히스토리를새로덮어씌우려면 git push --force 명령을사용해야한다. 이후에저장소에서 Fetch하고나면아래그림과같은상태가된다 : 69

3 장 Git 브랜치 Scott Chacon Pro Git 그림 3.37: Fetch 한후 Merge 함 그림 3.38: 한팀원이다른팀원이의존하는커밋을없애고 Rebase 한커밋을다시 Push 함 기존커밋이사라졌기때문에이미처리한일이라고해도다시 Merge 해야한다. Rebase 는커밋의 SHA-1 해시를바꾸기때문에 Git 은새로운커밋으로생각한다. 사실 C4 는이미히스토리에적용되어 있지만, Git 은모른다. 그림 3.39: 같은 Merge 를다시한다 70

Scott Chacon Pro Git 3.7 절요약 다른개발자와계속같이일하려면이런 Merge도해야만한다. Merge하면 C4와 C4 커밋둘다히스토리에남게된다. 실제내용과메시지가같지만 SHA-1 해시값이전혀다르다. git log로히스토리를확인해보면저자, 커밋날짜, 메시지가같은커밋이두개있을것이다. 이렇게되면혼란스럽다. 게다가이히스토리를서버에 Push하면같은커밋이두개있기때문에다른사람들도혼란스러워한다. Push하기전에정리하려고 Rebase하는것은괜찮다. 또절대공개하지않고혼자 Rebase하는경우도괜찮다. 하지만, 이미공개하여사람들이사용하는커밋을 Rebase하면틀림없이문제가생길것이다. 3.7 요약 우리는이장에서 Git으로브랜치를만들고 Merge 기능의기본적인명령을다루었다. 이제브랜치를만들고옮겨다니고 Merge하는것에익숙해졌을것으로생각한다. 브랜치를 Push하여공유하거나 Push하기전에브랜치를 Rebase하는것정도는어렵지않게할수있을것이다. 71

4 장 Git 서버 이글을읽는독자라면이미하루업무의대부분을 Git으로처리할수있을거라고생각한다. 이제는다른사람과협업하는방법을고민해보자. 다른사람과협업하려면리모트저장소가필요하다. 물론혼자서저장소를만들고거기에 Push하고 Pull할수도있지만이렇게하는것은아무의미가없다. 이런방식으로는다른사람이무슨일을하고있는지알려면항상지켜보고있어야간신히알수있을터이다. 당신컴퓨터가오프라인일때에도동료가저장소를사용할수있도록언제나이용할수있는저장소가필요하다. 즉, 공동으로사용할수있는저장소를만들고모두이저장소에접근하여 Push, Pull할수있어야한다. 우리는이저장소를 Git 서버 라고부른다. Git 저장소를운영하는데자원이많이필요하지도않아서별도로 Git 서버를준비하지않아도된다. Git 서버를운영하는것은어렵지않다. 우선사용할전송프로토콜부터정한다. 이장의앞부분에서는어떤프로토콜이있는지그리고각장단점은무엇인지살펴본다. 그다음엔각프로토콜을사용하는방법과그프로토콜을사용할수있도록서버를구성하는방법을살펴본다. 마지막으로다른사람의서버에내코드를맡기긴싫고고생스럽게서버를설치하고관리하고싶지도않을때고를수있는선택지가어떤것들이있는지살펴본다. 서버를직접설치해서운영할생각이없으면이장의마지막절만읽어도된다. 마지막절에서는 Git 호스팅서비스에계정을만들고사용하는방법에대해설명한다. 그리고다음장에서는분산환경에서소스를관리하는다양한패턴에대해논의할것이다. 리모트저장소는일반적으로워킹디렉토리가없는 Bare 저장소이다. 이저장소는협업용이기때문에체크아웃이필요없다. 그냥 Git 데이터만있으면된다. 다시말해서 Bare 저장소는일반프로젝트에서.git 디렉토리만있는저장소다. 4.1 프로토콜 Git 은 Local, SSH, Git, HTTP 이렇게네가지의네트워크프로토콜을사용할수있다. 이절에서는각 각어떤경우에유용한지살펴볼것이다. HTTP 프로토콜를제외한나머지들은모두 Git 이서버에설치돼있어야한다. 4.1.1 로컬프로토콜가장기본적인것이로컬프로토콜이다. 리모트저장소가단순히디스크의다른디렉토리에있을때사용한다. 팀원들이전부한시스템에로그인하여개발하거나아니면 NFS같은것으로파일시스템을공유하고있을때사용한다. 전자는문제가될수있다. 모든저장소가한시스템에있기때문에한순간에찌질해질수있다. 73

4 장 Git 서버 Scott Chacon Pro Git 공유파일시스템을마운트했을때는로컬저장소를사용하는것처럼 Clone 하고 Push 하고 Pull 하면 된다. 일단저장소를 Clone 하거나프로젝트에리모트저장소로추가한다. 추가할때 URL 자리에저장 소의경로를사용한다. 예를들어아래와같이로컬저장소를 Clone 한다 : $ git clone /opt/git/project.git 아래처럼도가능하다 : $ git clone file:///opt/git/project.git Git은파일경로를직접쓸때와 file:// 로시작하는 URL을사용할때에약간다르게처리한다. 디렉토리경로를그대로사용하면 Git은필요한파일을직접복사하거나하드링크를사용한다. 하지만 file:// 로시작하면 Git은네트워크를통해서데이터를전송할때처럼프로세스를별도로생성하여처리한다. 이프로세스로데이터를전송하는것은효율이좀떨어지지만그래도 file:// 를사용하는이유가있다. 보통은다른버전관리시스템들에서임포트한후에이렇게사용하는데, 외부레퍼런스나개체들이포함된저장소의복사본을깨끗한상태로남겨두고자할때사용한다 (9장에서자세히다룬다 ). 여기서는속도가빠른디렉토리경로를사용한다. 이미있는 Git 프로젝트에서아래와같이로컬저장소를추가한다 : $ git remote add local_proj /opt/git/project.git 그러면네트워크에있는리모트저장소처럼 Push 하고 Pull 할수있다. 장점파일기반저장소는단순한것이장점이다. 기존에있던네트워크나파일의권한을그대로사용하기때문에설정하기쉽다. 이미팀전체가접근가능한파일시스템이있으면저장소를아주쉽게구성할수있다. 디렉토리를공유하듯이동료가모두읽고쓸수있는공유디렉토리에 Bare 저장소를만든다. 다음절인 서버에 Git 설치하기 에서 Bare 저장소를만드는방법을살펴볼것이다. 또한, 동료가작업하는저장소에서한일을바로가져오기에도좋다. 만약함께프로젝트를하는동료가자신이한일을당신이확인해줬으면한다. 이럴때그동료가서버에 Push하고당신이다시 Pull할필요없이 git pull /home/john/project 라는명령어를바로실행시켜서매우쉽게동료의코드를가져올수있다. 단점다양한상황에서접근할수있도록디렉토리를공유하는것자체가일반적으로어렵다. 집에있을때 Push하려면리모트저장소가있는디스크를마운트해야하는데이것은다른프로토콜을이용하는방법보다느리고어렵다. 게다가네트워크파일시스템을마운트해서사용하는중이라면별로빠르지도않다. 로컬저장소는데이터를빠르게읽을수있을때만빠르다. NFS에있는저장소에 Git을사용하는것은보통같은서버에 SSH로접근하는것보다느리다. 74

Scott Chacon Pro Git 4.1 절프로토콜 4.1.2 SSH 프로토콜 Git의대표프로토콜은 SSH이다. 대부분서버는 SSH로접근할수있도록설정돼있다. 뭐, 설정돼있지않더라도쉽게설정할수있다. 그리고 SSH는읽기 / 쓰기접근을쉽게할수있는유일한네트워크프로토콜이다. 다른네트워크프로토콜인 HTTP와 Git은일반적으로읽기만가능하다. 그래서초보자 (unwashed masses) 라고해도쓰기명령을이용하려면 SSH가필요하다. SSH는또한인증도지원한다. SSH는보통유비쿼터스적이면서도, 사용하기도, 설치하기도쉽다. SSH를통해 Git 저장소를 Clone하려면 ssh:// 로시작하는 URL을사용한다 : $ git clone ssh://user@server/project.git 아니면 scp 명령어처럼사용할수있다. 이게조금더짧다 : $ git clone user@server:project.git 사용자계정을생략할수도있는데계정을생략하면 Git 은현재로그인한사용자의계정을사용한다. 장점 SSH는장점이매우많은프로토콜이다. 첫째, 누가리모트에서저장소에접근하는지알고싶다면 SSH 를사용해야한다. 둘째, SSH는상대적으로설정하기쉽다. SSH 데몬은정말흔하다. 네트워크관리자은 SSH 데몬을다루어본경험이있고대부분의 OS 배포판에는 SSH 데몬과관리도구가모두들어있다. 셋째, SSH를통해접근하면보안에안전하다. 모든데이터는암호화되어인증된상태로전송된다. 마지막으로 SSH는전송시데이터를가능한압축하기때문에효율적이다. 단점 SSH의단점은익명으로접근할수없다는것이다. 심지어읽기전용인경우에도익명으로시스템에접근할수없다. 회사에서만사용할것이라면 SSH가가장적합한프로토콜일것이지만오픈소스프로젝트는 SSH만으로는부족하다. 만약사람들이프로젝트에익명으로접근할수있게하려면, 자신이 Push할때사용할 SSH를설치하는것과별개로다른사람들이 Pull할때사용할다른프로토콜을추가해야한다. 4.1.3 Git 프로토콜 Git 프로토콜은 Git에포함된데몬을사용하는방법이다. 포트는 9418이며 SSH 프로토콜과비슷한서비스를제공하지만, 인증메커니즘이없다. 저장소에 git-export-daemon-ok 파일을만들면 Git 프로토콜로서비스할수있지만, 보안은없다. 이파일이없는저장소는 Git 프로토콜로서비스할수없다. 이저장소는누구나 Clone할수있거나아무도 Clone할수없거나둘중의하나만선택할수있다. 그래서이프로토콜로는 Push 가능하게설정할수없다. 엄밀히말해서 Push할수있도록설정할수있지만, 인증하도록할수없다. 그러니까당신이 Push할수있으면이프로젝트의 URL을아는사람은누구나 Push할수있다. 그냥이런것도있지만잘안쓴다고알고있으면된다. 75

4 장 Git 서버 Scott Chacon Pro Git 장점 Git 프로토콜은전송속도가가장빠르다. 전송량이많은공개프로젝트나별도의인증이필요없고읽기만허용하는프로젝트를서비스할때유용하다. 암호화와인증을빼면 SSH 프로토콜과전송메커니즘이별반다르지않다. 단점 Git 프로토콜은인증메커니즘이없는게단점이다. Git 프로토콜만사용하는프로젝트는바람직하지못하다. 일반적으로 SSH 프로토콜과함께사용한다. 소수의개발자만 Push할수있고대다수사람은 git:// 을사용하여읽을수만있게한다. 어쩌면가장설치하기어려운방법일수도있다. 별도의데몬이필요하고프로젝트에맞게설정해야한다. 이장의 Gitosis 절에서설정하는법을살펴볼것이다. 자원을아낄수있도록 xinetd 같은것도설정해야하고방화벽을통과할수있도록 9418 포트도열어야한다. 이포트는일반적으로회사들이허용하는표준포트가아니다. 규모가큰회사라면당연히방화벽에서이포트를막아놓는다. 4.1.4 HTTP/S 프로토콜마지막으로, HTTP 프로토콜이있다. HTTP와 HTTPS 프로토콜의미학은설정이간단하다는점이다. HTTP 도큐먼트루트밑에 Bare 저장소를두고 post-update 훅을설정하는것이기본적으로해야하는일의전부다 (7장에서 Git 훅에대해자세히다룰것이다 ). 저장소가있는웹서버에접근할수있다면그저장소를 Clone할수도있다. HTTP를통해서저장소를읽을수있게하려면아래와같이한다 : $ cd /var/www/htdocs/ $ git clone --bare /path/to/git_project gitproject.git $ cd gitproject.git $ mv hooks/post-update.sample hooks/post-update $ chmod a+x hooks/post-update post-update 훅은 Git 에포함되어있으며 git update-server-info 라는명령어를실행시킨다. 이명 령어는 HTTP 로 Fetch 와 Clone 명령이잘동작하게한다. SSH 를통해서저장소에 Push 할때실행되 며, 사람들은아래와같이 Clone 한다 : $ git clone http://example.com/gitproject.git 여기서는 Apache 서버가기본으로사용하는 /var/www/htdocs을루트디렉토리로사용하지만다른웹서버를사용해도된다. 단순히 Bare 저장소를 HTTP 문서루트에넣으면된다. Git 데이터는일반적인정적파일처럼취급된다 (9장에서정확히어떻게처리하는지다룰것이다 ). HTTP를통해서 Push하는것도가능하다. 단지이방법은잘사용하지않는 WebDAV 환경을완벽하게구축해야한다. 잘사용하지않기때문에이책에서도다루지않는다. HTTP 프로토콜로 Push하고싶으면 http://www.kernel.org/pub/software/scm/git/docs/howto/setup-git-server-over-http.txt 읽고저장소를만들면된다. HTTP를통해서 Push하는방법의좋은점은 WebDAV 서버를아무거나골라쓸수있다는것이다. 그래서 WebDAV를지원하는웹호스팅업체를이용하면이기능을사용할수있다. 76

Scott Chacon Pro Git 4.2 절서버에 Git 설치하기 장점 HTTP 프로토콜은설정하기쉽다는것이장점이다. 몇개의필수명령어만실행하면세계어디에서나당신의저장소에접근할수있게만들수있다. 이렇게하는데몇분이면충분하다. HTTP 프로토콜은서버의리소스를많이잡아먹지도않는다. 보통은정적 HTTP 서버만으로도충분하기때문에흔한 Apache 서버로초당수천개의파일을처리할수있다. 작은서버로도충분히감당할수있다. 또 HTTPS를사용해서서비스할수도있기때문에전송하는데이터를암호화할수있다. 그리고클라이언트가서명된 SSL 인증서를사용하게할수도있다. 이렇게하더라도 SSH 공개키를사용하는방식보다쉽다. 서명한 SSL 인증서를사용하는게나을때도있고단순히 HTTPS위에서 HTTP기반인증을사용하는게나을때도있다. HTTP는매우보편적인프로토콜이라서거의모든회사가트래픽이방화벽을통과하도록허용한다는장점도있다. 단점클라이언트에서는 HTTP가좀비효율적이다. 저장소에서 Fetch하거나 Clone할때좀더오래걸린다. 다른프로토콜의네트워크오버헤드보다 HTTP의오버헤드가좀더크다. 지능적으로정말필요한데이터만전송하지않기때문에 HTTP 프로토콜은멍청한프로토콜 (Dumb Protocol) 이라고도부른다. 효율적으로전송하고자서버는아무것도하지않는다. HTTP와다른프로토콜의성능차이는 9 장에서자세히설명한다. 4.2 서버에 Git 설치하기 어떤서버를설치하더라도일단저장소를 Bare 저장소로만들어야한다. 다시말하지만, Bare 저장소 는워킹디렉토리가없는저장소이다. --bare 옵션을주고 Clone 하면새로운 Bare 저장소가만들어진 다. Bare 저장소디렉토리는관례에따라. git 확장자로끝난다 : $ git clone --bare my_project my_project.git Initialized empty Git repository in /opt/projects/my_project.git/ 이명령이출력하는메시지가조금이상해보일수도있다. 사실 git clone 명령은 git init을하고나서 git fetch를실행한다. 그런데빈디렉토리밖에만들지않는 git init 명령의메시지만보여준다. 개체전송에관련된메시지는아무것도보여주지않는다. 전송메시지를보여주지않지만 my_project.git 디렉토리를보면 Git 데이터가들어있다. 아래와같이실행한것과비슷하다 : $ cp -Rf my_project/.git my_project.git 물론설정상의미세한차이가있지만, 저장소의내용만고려한다면같다고볼수있다. 워킹디렉토리 가없는 Git 저장소인데다가별도의디렉토리도하나만들었다는점에서는같다. 77

4 장 Git 서버 Scott Chacon Pro Git 4.2.1 서버에 Bare 저장소넣기 Bare 저장소는이제만들었으니까서버에넣고프로토콜을설정한다. git.example.com라는이름의서버를하나준비하자. 그리고그서버에 SSH로접속할수있게하고 /opt/git에 Git 저장소를만든다. 아래와같이 Bare 저장소를복사한다 : $ scp -r my_project.git user@git.example.com:/opt/git 이제다른사용자들은 SSH 로서버에접근해서저장소를 Clone 할수있다. 사용자는 /opt/git 디렉토 리에읽기권한이있어야한다 : $ git clone user@git.example.com:/opt/git/my_project.git 이서버에 SSH 로접근할수있는사용자가 /opt/git/my_project.git 디렉토리에쓰기권한까지가지고 있으면바로 Push 할수있다. git init 명령에 --shared 옵션을추가하면 Git 은자동으로그룹쓰기권 한을추가한다 : $ ssh user@git.example.com $ cd /opt/git/my_project.git $ git init --bare --shared Git 저장소를만드는것이얼마나쉬운지살펴보았다. Bare 저장소를만들어 SSH로접근할수있는서버에올리면동료와함께일할준비가끝난다. 그러니까 Git 서버를구축하는데사람이할일은정말별로없다. SSH로접속할수있도록서버에계정을만들고 Bare 저장소를사람들이읽고쓸수있는곳에넣어두기만하면된다. 다른것은아무것도필요없다. 다음절에서는좀더정교하게설정하는법을살펴본다. 사용자에게계정을만들어주는법, 저장소를읽고쓸수있게하는법, Web UI를설정하는법, Gitosis를사용하는법, 등등은여기에서설명하지않는다. 꼭기억해야할것은동료와함께개발할때꼭필요한것이 SSH 서버와 Bare 저장소뿐이라는것이다. 4.2.2 바로설정하기만약창업을준비하고있거나회사에서 Git을막도입하려고할때처럼사용할개발자의수가많지않을때에는설정할게별로없다. Git 서버설정에서사용자관리가가장골치아프다. 사람이많으면어떤사용자는읽기만가능하게하고어떤사용자는읽고쓰기둘다가능하게하는것이좀까다롭다. SSH 접근만약모든개발자가 SSH로접속할수있는서버가있으면너무쉽게저장소를만들수있다. 앞서말했듯이할일이별로없다. 저장소의권한을꼼꼼하게관리해야하면그냥운영체제의파일시스템권한관리를이용한다. 동료가저장소에쓰기접근을해야하는데아직 SSH로접속할수있는서버가없으면 78

Scott Chacon Pro Git 4.3 절 SSH 공개키만들기 하나마련해야한다. 아마독자에게서버가있다면그서버에는이미 SSH 서버가설치돼있어서이미 SSH로접속하고있을것이다. 동료가접속하도록하는방법은몇가지가있다. 첫째로모두에게계정을만들어주는방법이있다. 이방법이제일단순하지만다소귀찮은방법이다. 팀원마다 adduser를실행시키고임시암호를부여해야하기때문에보통이방법을쓰고싶어하지않는다. 둘째로서버마다 git이라는계정을하나씩만드는방법이있다. 쓰기권한이필요한사용자의 SSH 공개키를모두모아서 git 계정의 ~/.ssh/authorized_keys파일에모든키를입력한다. 그러면모두 git 계정으로그서버에접속할수있다. 이 git 계정은커밋데이터에는아무런영향을주지않는다. 다시말해서접속하는데사용한 SSH 계정과커밋에저장되는사용자는아무상관없다. 이미 LDAP 서버같은중앙집중식인증소스를가지고있으면 SSH 서버가해당인증을이용하도록할수도있다. SSH 인증메커니즘중아무거나하나이용할수있으면그서버에접속이가능하다. 4.3 SSH 공개키만들기 이미말했듯이많은 Git 서버들은 SSH 공개키로인증한다. 공개키를사용하려면일단공개키를만들어야한다. 공개키를만드는방법은모든운영체제가비슷하다. 먼저키가있는지부터확인하자. 사용자의 SSH 키들은기본적으로사용자의 ~/.ssh 디렉토리에저장한다. 그래서만약디렉토리의파일을살펴보면공개키가있는지확인할수있다 : $ cd ~/.ssh $ ls authorized_keys2 id_dsa known_hosts config id_dsa.pub something, something.pub이라는형식으로된파일을볼수있다. something은보통 id_dsa나 id_rsa라고돼있다. 그중.pub파일이공개키이고다른파일은개인키이다. 만약이파일이없거나.ssh 디렉토리도없으면 ssh-keygen이라는프로그램으로키를생성해야한다. ssh-keygen 프로그램은리눅스나 Mac의 SSH 패키지에포함돼있고윈도는 MSysGit 패키지안에들어있다 : $ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/Users/schacon/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /Users/schacon/.ssh/id_rsa. Your public key has been saved in /Users/schacon/.ssh/id_rsa.pub. The key fingerprint is: 43:c5:5b:5f:b1:f1:50:43:ad:20:a6:92:6a:1f:9a:3a schacon@agadorlaptop.local 먼저키를어디에저장할지경로를 (.ssh/id_rsa) 입력하고암호를두번입력한다. 이때암호를비워두면키를사용할때암호를묻지않는다. 사용자는그다음에자신의공개기를 Git 서버관리자에게보내야한다. 사용자는.pub 파일의내용을복사하여메일을보내기만하면된다. 공개키는아래와같이생겼다 : 79

4 장 Git 서버 Scott Chacon Pro Git $ cat ~/.ssh/id_rsa.pub ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3 Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA t3faojoasncm1q9x5+3v0ww68/eifmb1zuufljqjkprrx88xypndvjynby6vw/pb0rwert/en mz+aw4ozpntpi89zpmvmluayrd2ce86z/il8b+gw3r3+1nkatmikjn2so1d01qratlmqvssbx NrRFi9wrf+M7Q== schacon@agadorlaptop.local 다양한운영체제에서 SSH 키를만드는방법이궁금하면에있는 Github 설명서를찾아보는게좋다. 4.4 서버에설정하기 서버에설정하는일을살펴보자. 일단 Ubuntu같은표준리눅스배포판을사용한다고가정한다. 사용자는아마도 authorized_keys 파일로인증할것이다. 먼저 git 계정을만들고사용자홈디렉토리에.ssh 디렉토리를만든다 : $ sudo adduser git $ su git $ cd $ mkdir.ssh authorized_keys 파일에 SSH 공개키를추가해야사용자가접근할수있다. 추가하기전에이미이메일 로공개키를몇개받아서가지고있다고가정하자. 공개키가어떻게생겼는지다시한번확인한다 : $ cat /tmp/id_rsa.john.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L ojg6rs6hpb09j9r/t17/x4lhja0f3fr1rp6kybrswj2athgw6hxlm9/5zytk6ztg3rpkk+4k Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq dav8jggjicuvax2t9va5 gsg-keypair authorized_keys 파일에추가한다 : $ cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys $ cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys $ cat /tmp/id_rsa.jessica.pub >> ~/.ssh/authorized_keys --bare 옵션을주고 git init 을실행해서워킹디렉토리가없는빈저장소를하나만든다 : 80

Scott Chacon Pro Git 4.4 절서버에설정하기 $ cd /opt/git $ mkdir project.git $ cd project.git $ git --bare init 이제 John씨, Josie씨, Jessica씨는이저장소를리모트저장소로등록하면브랜치를 Push할수있다. 프로젝트마다적어도한명은서버에접속하여 Bare 저장소를만들어야한다. git 계정과저장소를만든서버의호스트이름이 gitserver라고하자. 만약이서버가내부망에있으면 gitserver가그서버를가리키도록 DNS에설정한다. 그러면명령을아래와같이사용할수있다 : # on Johns computer $ cd myproject $ git init $ git add. $ git commit -m 'initial commit' $ git remote add origin git@gitserver:/opt/git/project.git $ git push origin master 이제이프로젝트를 Clone 하고나서수정하고 Push 한다 : $ git clone git@gitserver:/opt/git/project.git $ cd project $ vim README $ git commit -am 'fix for the README file' $ git push origin master 개발자들이읽고쓸수있는 Git 서버를간단하게만들었다. 그리고 git-shell이라는걸사용해서보안을강화할수있다. 이쉘로 git 계정을사용하는사용자들이 Git 말고다른것을할수없도록제한하는것이다. git 계정의로그인쉘을이것으로설정하면 git 사용자는일반적인쉘을사용할수없다. 통상의 bash, csh 대신에 git-shell을로그인쉘로설정하기만하면된다. 이것을하려면 /etc/passwd 파일을편집한다 : $ sudo vim /etc/passwd 그리고아래와같은줄을찾는다 : git:x:1000:1000::/home/git:/bin/sh 81

4 장 Git 서버 Scott Chacon Pro Git /bin/sh 를 /usr/bin/git-shell 로 (which git-shell 명령으로어디에설치됐는지확인하는게좋다 ) 변경 한다 : git:x:1000:1000::/home/git:/usr/bin/git-shell 이제 git 계정은 Git 저장소에 Push 하고 Pull 하는것만가능하고서버의쉘에는접근할수없다. 실제 로로그인을해보면아래와같은메시지로로그인이거절된다 : $ ssh git@gitserver fatal: What do you think I am? A shell? Connection to gitserver closed. 4.5 공개하기 익명의사용자에게읽기접근을허용하고싶을때는어떻게해야할까? 프로젝트를비공개가아니라오픈소스프로젝트로공개한다거나자동빌드서버나 CI(Continuous Integration) 서버가많아서계정마다하나하나설정해야할수있다. 아니면그냥매번 SSH 키를생성하는게귀찮을수도있다. 그러니까그냥간단하게익명의사용자도읽을수있도록하고싶을때는어떻게해야할까? 분명웹서버를설치하는것이가장쉬운방법이다. 이장의첫부분에설명했듯이웹서버를설치하고 Git 저장소를문서루트디렉토리에두고 post-update 훅을켜기만하면된다. 먼저설명했던예제를따라해보자. /opt/git 디렉토리에저장소가있고서버에 Apache가설치돼있다고가정하자. 아무웹서버나다사용할수있지만, 이예제에서는 Apache를사용한다. 여기에서는이해하는것이목적이므로아주기본적인 Apache 설정만을보여줄것이다. 먼저이훅을설정해야한다 : $ cd project.git $ mv hooks/post-update.sample hooks/post-update $ chmod a+x hooks/post-update post-update 훅은무슨일을할까? 기본적으로다음과같다 : $ cat.git/hooks/post-update #!/bin/sh exec git-update-server-info SSH를통해서서버에 Push하면 Git은이명령어를실행하여 HTTP를통해서도 Fetch할수있도록파일를갱신한다. 그다음 Apache 설정에 VirtualHost 항목을추가한다. 이항목에서문서루트가 Git 저장소의루트디렉토리가되도록한다. 그리고 *.gitserver로접속하는사람들이모두이서버에접속하도록한다. 와일드카드를이용하여 VirtualHost항목을아래와같이설정한다 : 82

Scott Chacon Pro Git 4.6 절 GitWeb <VirtualHost *:80> ServerName git.gitserver DocumentRoot /opt/git <Directory /opt/git/> Order allow, deny allow from all </Directory> </VirtualHost> 그리고 Apache 서버는 www-data 권한으로 CGI 스크립트를실행시키기때문에 /opt/git 디렉토리의 그룹소유권한을 www-data 로수정해주어야웹서버로접근하는사용자들이읽을수있다. $ chgrp -R www-data /opt/git Apache 를재시작하면아래와같은 URL 로저장소를 Clone 할수있다 : $ git clone http://git.gitserver/project.git 이렇게사용자들이 HTTP 로프로젝트에접근하도록설정하는데몇분밖에걸리지않는다. 그리고 Git 데몬으로도똑같이인증없이접속하게할수있다. 프로세스를데몬으로만들어야한다는단점이있지 만가능하다. 이것은다음절에서살펴볼것이다. 4.6 GitWeb 프로젝트저장소를단순히읽거나쓰는것에대한설정은다뤘다. 이제는웹기반인터페이스를설정해보자. Git에는 GitWeb이라는 CGI 스크립트를제공해서쉽게웹에서저장소를조회하도록할수있다. 같은사이트에서 GitWeb을구경할수있다 ( 그림 4-1). 그림 4.1: Git 웹용 UI, GitWeb 83

4 장 Git 서버 Scott Chacon Pro Git Git은 GitWeb을쉽게사용해볼수있도록서버를잠시띄우는명령을제공한다. 시스템에 lighttpd나 webrick 같은경량웹서버가설치돼있어야이명령을사용할수있다. 리눅스에서는 lighttpd가설치돼있을확률이높고프로젝트디렉토리에서그냥 git instaweb을실행하면바로실행된다. Mac의 Leopard 버전은 Ruby가미리설치돼있기때문에 webrick이더낫다. lighttpd이아니라면아래와같이 --httpd 옵션을사용해야한다 : $ git instaweb --httpd=webrick [2009-02-21 10:02:21] INFO WEBrick 1.3.1 [2009-02-21 10:02:21] INFO ruby 1.8.6 (2008-03-03) [universal-darwin9.0] 1234 포트로 HTTPD 서버를시작하고이페이지를여는웹브라우저를자동으로실행시킨다. 꽤편리 하다. 필요한일을모두마치고나서같은명령어에 --stop 옵션을추가하여서버를중지한다 : $ git instaweb --httpd=webrick --stop 항상접속가능한웹인터페이스를운영하려면먼저웹서버에이 CGI 스크립트를설치해야한다. apt 나 yum 으로도 gitweb 을설치할수있지만, 여기에서는수동으로설치한다. 먼저 GitWeb 이포함된 Git 소 스코드를구한다음 CGI 스크립트를빌드한다 : $ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/ $ make GITWEB_PROJECTROOT="/opt/git" \ prefix=/usr gitweb/gitweb.cgi $ sudo cp -Rf gitweb /var/www/ 빌드할때 GITWEB_PROJECTROOT 변수로 Git 저장소의위치를알려줘야한다. 이제 Apache 가이스크립 트를사용하도록 VirtualHost 항목을설정한다 : <VirtualHost *:80> ServerName gitserver DocumentRoot /var/www/gitweb <Directory /var/www/gitweb> Options ExecCGI +FollowSymLinks +SymLinksIfOwnerMatch AllowOverride All order allow,deny Allow from all AddHandler cgi-script cgi DirectoryIndex gitweb.cgi </Directory> </VirtualHost> 84

Scott Chacon Pro Git 4.7 절 Gitosis 다시말해서 GitWeb 은 CGI 를지원하는웹서버라면아무거나사용할수있다. 이제에접속하여온라 인으로저장소를확인할수있을뿐만아니라를통해서 HTTP 프로토콜로저장소를 Clone 하고 Fetch 할수있다. 4.7 Gitosis 처음에는모든사용자의공개키를 authorized_keys에저장하는방법으로도불편하지않을것이다. 하지만, 사용자가수백명이넘으면관리하기가매우고통스럽다. 사용자를추가할때마다매번서버에접속할수도없고권한관리도안된다. authorized_keys에등록된모든사용자는누구나프로젝트를읽고쓸수있다. 이문제는매우널리사용되고있는 Gitosis라는소프트웨어로해결할수있다. Gitosis는기본적으로 authorized_keys 파일을관리하고접근제어를돕는스크립트패키지다. 사용자를추가하고권한을관리하는 UI가웹인터페이스가아니라일종의 Git 저장소라는점이재미있다. 프로젝트설정을 Push하면그설정이 Gitosis에적용된다. 신비롭다! Gitosis를설치하기가쉽지는않지만그렇다고어렵지도않다. Gitosis는리눅스에설치하는것이가장쉽다. 여기서는 Ubuntu 8.10 서버를사용한다. Gitosis는 Python이필요하기때문에먼저 Python setuptools 패키지를설치해야한다. Ubuntu 에서는아래와같이설치한다 : $ apt-get install python-setuptools 그리고 Gitosis 프로젝트사이트에서 Gitosis 를 Clone 한후설치한다 : $ git clone git://eagain.net/gitosis.git $ cd gitosis $ sudo python setup.py install Gitosis 가설치되면 Gitosis 는저장소디렉토리로 /home/git 를사용하려고한다. 이대로사용해도괜 찮지만, 우리의저장소는이미 /opt/git 에있다. 다시설정하지말고아래와같이간단하게심볼릭링크 를만들자 : $ ln -s /opt/git /home/git/repositories Gitosis 가키들을관리할것이기때문에현재파일은삭제하고다시추가해야한다. 이제부터는 Gitosis 가 authorized_keys 파일을자동으로관리할것이다. authorized_keys 파일을백업해두자 : $ mv /home/git/.ssh/authorized_keys /home/git/.ssh/ak.bak 그리고 git 계정의쉘을 git-shell 로변경했었다면원래대로복원해야한다. Gitosis 가대신이일을맡 아줄것이기때문에복원해도사람들은여전히로그인할수없다. /etc/passwd 파일의다음줄을 : 85

4 장 Git 서버 Scott Chacon Pro Git git:x:1000:1000::/home/git:/usr/bin/git-shell 아래와같이변경한다 : git:x:1000:1000::/home/git:/bin/sh 이제 Gitosis 를초기화할차례다. gitosis-init 명령을공개키와함께실행한다. 만약공개키가서버에 없으면공개키를서버로복사해와야한다 : $ sudo -H -u git gitosis-init < /tmp/id_dsa.pub Initialized empty Git repository in /opt/git/gitosis-admin.git/ Reinitialized existing Git repository in /opt/git/gitosis-admin.git/ 이명령으로등록하는키의사용자는 Gitosis 를제어하는파일들이있는 Gitosis 설정저장소를수정 할수있게된다. 그리고수동으로 post-update 스크립트에실행권한을부여한다 : $ sudo chmod 755 /opt/git/gitosis-admin.git/hooks/post-update 모든준비가끝났다. 설정이잘됐으면추가한공개키의사용자로 SSH 서버에접속했을때아래와같 은메시지를보게된다 : $ ssh git@gitserver PTY allocation request failed on channel 0 fatal: unrecognized command 'gitosis-serve schacon@quaternion' Connection to gitserver closed. 이것은접속을시도한사용자가누구인지식별할수는있지만, Git 명령이아니어서거절한다는뜻이 다. 그러니까실제 Git 명령어를실행시켜보자. Gitosis 제어저장소를 Clone 한다 : # on your local computer $ git clone git@gitserver:gitosis-admin.git gitosis-admin 이라는디렉토리가생긴다. 디렉토리내용은크게두가지로나눌수있다 : 86

Scott Chacon Pro Git 4.7 절 Gitosis $ cd gitosis-admin $ find../gitosis.conf./keydir./keydir/scott.pub gitoiss.conf 파일은사용자, 저장소, 권한등을명시하는설정파일이다. keydir 디렉토리에는저장소에접근할수있는사용자의공개키가저장된다. 사용자마다공개키가하나씩있고이공개키로서버에접근한다. 이예제에서는 scott.pub이지만 keydir 안에있는파일의이름은사용자마다다르다. 파일이름은 gitosis-init 스크립트로공개키를추가할때결정되는데공개키끝부분에있는이름이사용된다. 이제 gitosis.conf 파일을열어보자. 지금막 Clone한 gitosis-admin 프로젝트에대한정보만들어있다 : $ cat gitosis.conf [gitosis] [group gitosis-admin] writable = gitosis-admin members = scott scott이라는사용자는 Gitosis를초기화할때사용한공개키의사용자이다. 이사용자만 gitosis-admin 프로젝트에접근할수있다. 이제프로젝트를새로추가해보자. mobile 단락을추가하고그프로젝트에속한개발자나프로젝트에접근해야하는사용자를추가한다. 현재는 scott이외에다른사용자가없으니 scott만추가한다. 그리고 iphone_project 프로젝트를새로추가한다 : [group mobile] writable = iphone_project members = scott gitosis-admin 프로젝트를수정하면커밋하고서버에 Push 해야수정한설정이적용된다 : $ git commit -am 'add iphone_project and mobile group' [master]: created 8962da8: "changed name" 1 files changed, 4 insertions(+), 0 deletions(-) $ git push Counting objects: 5, done. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 272 bytes, done. 87

4 장 Git 서버 Scott Chacon Pro Git Total 3 (delta 1), reused 0 (delta 0) To git@gitserver:/opt/git/gitosis-admin.git fb27aec..8962da8 master -> master 로컬에있는 iphone_project 프로젝트에이서버를리모트저장소로추가하고 Push 하면서버에새로 운저장소가추가된다. 서버에프로젝트를새로만들때이제는수동으로 Bare 저장소를만들필요가 없다. 처음 Push 할때 Gitosis 가알아서생성해준다 : $ git remote add origin git@gitserver:iphone_project.git $ git push origin master Initialized empty Git repository in /opt/git/iphone_project.git/ Counting objects: 3, done. Writing objects: 100% (3/3), 230 bytes, done. Total 3 (delta 0), reused 0 (delta 0) To git@gitserver:iphone_project.git * [new branch] master -> master Gitosis를이용할때에는저장소경로를명시할필요도없고사용할수도없다. 단지콜론뒤에프로젝트이름만적어도 Gitosis가알아서찾아준다. 동료와이프로젝트를공유하려면동료의공개키도모두추가해야한다. ~/.ssh/authorized_keys 파일에수동으로추가하는게아니라 keydir 디렉토리에하나의공개키를하나의파일로추가한다. 이공개키의파일이름이 gitosis.conf 파일에서사용하는사용자이름을결정한다. John, Josie, Jessica의공개키를추가해보자 : $ cp /tmp/id_rsa.john.pub keydir/john.pub $ cp /tmp/id_rsa.josie.pub keydir/josie.pub $ cp /tmp/id_rsa.jessica.pub keydir/jessica.pub 이세사람을모두 mobile 팀으로추가하여 iphone_project 에대한읽기, 쓰기를허용한다 : [group mobile] writable = iphone_project members = scott john josie jessica 이파일을커밋하고 Push 하고나면네명모두 iphone_project 를읽고쓸수있게된다. Gitosis 의접근제어방법은매우단순하다. 만약이프로젝트에대해서 John 은읽기만가능하도록설 정하려면아래와같이한다 : 88

Scott Chacon Pro Git 4.8 절 Gitolite [group mobile] writable = iphone_project members = scott josie jessica [group mobile_ro] readonly = iphone_project members = john 이제 John 은프로젝트를 Clone 하거나 Fetch 할수는있지만, 프로젝트에 Push 할수는없다. 다양한 사용자와프로젝트가있어도필요한만큼그룹을만들어사용하면된다. 그리고 members 항목에사 용자대신그룹명을사용할수도있다. 그룹명앞에 @ 를붙이면그그룹의사용자을그대로상속한다 : [group mobile_committers] members = scott josie jessica [group mobile] writable = iphone_project members = @mobile_committers [group mobile_2] writable = another_iphone_project members = @mobile_committers john [gitosis] 절에 loglevel=debug라고적으면문제가생겼을때해결하는데도움이된다. 그리고설정이꼬여버려서 Push할수없게되면서버에있는파일을수동으로고쳐도된다. Gitosis는 /home/ git/.gitosis.conf 파일의정보를읽기때문에이파일을고친다. gitosis.conf는 Push할때그위치로복사되기때문에수동으로고친파일은 gitosis-admin 프로젝트가다음에 Push될때까지유지된다. 4.8 Gitolite 이절에서는 Gitolite가뭐고기본적으로어떻게설치하는지를살펴본다. 물론 Gitolite에들어있는 Gitolite 문서는양이많아서이절에서모두다룰수없다. Gitolite는계속진화하고있기때문에이책의내용과다를수있다. 최신내용은여기에서확인해야한다. Gitolite은간단히말해 Git 위에서운영하는권한제어 (Authorization) 도구다. 사용자인증 (Authentication) 은 sshd와 httpd를사용한다. 접속한접속하는사용자가누구인지가려내는것이인증 (Authentication) 이고리소스에대한접근권한을가려내는일은권한제어 (Authorization) 이다. Gitolite는저장소뿐만아니라저장소의브랜치나태그에도권한을명시할수있다. 즉, 어떤사람은 refs( 브랜치나태그 ) 에 Push할수있고어떤사람은할수없게하는것이가능하다. 4.8.1 설치하기별도문서를읽지않아도유닉스계정만하나있으면 Gitolite를쉽게설치할수있다. 이글은여러가지리눅스들과솔라리스 10에서테스트를마쳤다. Git, Perl, OpenSSH가호환되는 SSH 서버가설치 89

4 장 Git 서버 Scott Chacon Pro Git 돼있으면 root 권한도필요없다. 앞서사용했던 gitserver라는서버와그서버에 git 계정을만들어사용한다. Gitolite는보통의서버소프트웨어와는달리 SSH를통해서접근한다. 서버의모든계정은근본적으로 Gitolite 호스트 가될수있다. 이책에서는가장간단한설치방법으로설명한다. 자세한설명문서는 Gitolite의문서를참고한다. 먼저서버에 git 계정을만들고 git 계정으로로그인한다. 사용자의 SSH 공개키 (ssh-keygen으로생성한 SSH 공개키는기본적으로 ~/.ssh/id_rsa.pub에위치함 ) 를복사하여 < 이름 >.pub 파일로저장한다 ( 이책의예제에서는 scott.pub로저장 ). 그리고다음과같은명령을실행한다 : $ git clone git://github.com/sitaramc/gitolite $ gitolite/install -ln # $HOME/bin가이미 $PATH에등록돼있다고가정 $ gitolite setup -pk $HOME/scott.pub 마지막명령은 gitolite-admin라는새 Git 저장소를서버에만든다. 다시작업하던환경으로돌아가서 git clone git@gitserver:gitolite-admin 명령으로서버의저장소를 Clone 했을떄문제없이 Clone 되면 Gitolite가정상적으로설치된것이다. 이 gitolite-admin 저장소의내용을수정하고 Push하여 Gitolite을설치를마치도록한다. 4.8.2 자신에게맞게설치하기보통은기본설정으로빠르게설치하는것으로충분하지만, 자신에게맞게고쳐서설치할수있다. 일부설정은주석이잘달려있는 rc 파일을간단히고쳐서쓸수있지만, 자세한설정을위해서는 Gitolite가제공하는문서를살펴보도록한다. 4.8.3 설정파일과접근제어규칙 설치가완료되면홈디렉토리에 Clone 한 gitolite-admin 디렉토리로이동해서어떤것들이있는지한 번살펴보자 : $ cd ~/gitolite-admin/ $ ls conf/ keydir/ $ find conf keydir -type f conf/gitolite.conf keydir/scott.pub $ cat conf/gitolite.conf repo gitolite-admin RW+ = scott repo testing RW+ = @all 90

Scott Chacon Pro Git 4.8 절 Gitolite gitolite setup 명령을실행했을때주었던공개키파일의이름인 scott은 gitolite-admin 저장소에대한읽기와쓰기권한을갖도록공개키가등록돼있다. 사용자를새로추가하기도쉽다. alice 라는사용자를새로등록하려면우선등록할사람의공개키파일을얻어서 alice.pub라는이름으로 gitolite-admin 디렉토리아래에 keydir 디렉토리에저장한다. 새로추가한이파일을 Add하고커밋한후 Push를하면 alice라는사용자가등록된것이다. Gitolite의설정파일인 conf/example.conf에대한내용은 Gitolite 문서에설명이잘되어있다. 이책에서는주요일부설정에대해서만간단히살펴본다. 저장소의사용자그룹을쉽게만들수있다. 이그룹은매크로와비슷하다. 그룹을만들때는그그룹이프로젝트의그룹인지사용자의그룹인지구분하지않지만사용할때에는다르다. @oss_repos @secret_repos = linux perl rakudo git gitolite = fenestra pear @admins @interns @engineers @staff = scott = ashok = sitaram dilbert wally alice = @admins @engineers @interns 그리고 ref 단위로권한을제어한다. 다음예제를보자. 인턴 (interns) 은 int 브랜치만 Push 할수있 고 engineers 는 eng- 로시작하는브랜치와 rc 뒤에숫자가붙는태그만 Push 할수있다. 그리고관리 자는모든 ref 에무엇이든지 ( 되돌리기도포함됨 ) 할수있다. repo @oss_repos RW int$ = @interns RW eng- = @engineers RW refs/tags/rc[0-9] = @engineers RW+ = @admins RW나 RW+ 뒤에나오는표현식은정규표현식 (regex) 이고 Push하는 ref 이름의패턴을의미한다. 그래서우리는 refex라고부른다. 물론 refex는여기에보여준것보다훨씬더강력하다. 하지만, 펄의정규표현식에익숙하지않은독자도있으니여기서는무리하지않았다. 그리고이미예상했겠지만 Gitolite는 refs/heads/ 라고시작하지않는 refex에대해서는암묵적으로 refs/heads/ 가생략된것으로판단한다. 특정저장소에사용하는규칙을한곳에모아놓지않아도괜찮다. 위에보여준 oss_repos 저장소설정과다른저장소설정이마구섞여있어도괜찮다. 아래와같이목적이분명하고제한적인규칙을아무데나추가해도좋다 : repo gitolite RW+ = sitaram 이규칙은 gitolite 저장소를위해지금막추가한규칙이다. 91

4 장 Git 서버 Scott Chacon Pro Git 이제는접근제어규칙이실제로어떻게적용되는지설명한다. 이제부터그내용을살펴보자. Gitolite는접근제어를두단계로한다. 첫단계가저장소단계인데접근하는저장소의 ref 중에서하나라도읽고쓸수있으면실제로그저장소전부에대해읽기, 쓰기권한이있는것이다. 두번째단계는브랜치나태그단위로제어하는것으로오직 쓰기 접근만제어할수있다. 어느사용자가특정 ref 이름으로접근을시도하면 (W나 + 같은 ) 설정파일에정의된순서대로접근제어규칙이적용된다. 그순서대로사용자이름과 ref 이름을비교하는데 ref 이름의경우단순히문자열을비교하는것이아니라정규표현식으로비교한다. 해당되는것을찾으면정상적으로 Push되지만찾지못하면거절된다. 4.8.4 deny 규칙을꼼꼼하게제어하기지금까지는 R, RW, RW+ 권한에대해서만다뤘다. Gitolite는 deny 규칙을위해서 - 권한도지원한다. 이것으로복잡도를낮출수있다. -로거절도할수있기때문에규칙의순서가중요하다. 다시말해서 engineers가 master와 integ 브랜치이외의모든브랜치를되돌릴수있게하고싶으면아래와같이한다 : RW master integ = @engineers - master integ = @engineers RW+ = @engineers 즉, 접근제어규칙을순서대로찾기때문에순서대로정의해야한다. 첫번째규칙은 master나 integ 브랜치에대해서읽기, 쓰기만허용하고되돌리기는허용하지않는다. master나 integ 브랜치를되돌리는 Push는첫번째규칙에어긋나기때문에바로두번째규칙으로넘어간다. 그리고거기서거절된다. master나 integ 브랜치이외다른 ref에대한 Push는첫번째규칙과두번째규칙에는만족하지않고마지막규칙으로허용된다. 4.8.5 파일단위로 Push를제어하기브랜치단위로 Push를제어할수있지만수정된파일단위로도제어할수있다. 예를들어 Makefile을보자. Makefile 파일에의존하는파일은매우많고보통꼼꼼하게수정하지않으면문제가생긴다. 그래서아무나 Makefile을수정하게둘수없다. 그러면아래와같이설정한다 : repo foo RW = @junior_devs @senior_devs - VREF/NAME/Makefile = @junior_devs 예전버전의 Gitolite 에서버전을올리려는사용자는설정이많이달라진것을알게될것이다. Gitolite 의버전업가이드를필히참고하자. 4.8.6 Personal 브랜치 Gitolite는또 Personal 브랜치 라고부르는기능을지원한다. 이기능은실제로 Personal 브랜치네임스페이스 라고부르는것이더적절하다. 이기능은기업에서매우유용하다. 92

Scott Chacon Pro Git 4.9 절 Git 데몬 Git을사용하다보면코드를공유하려고 Pull 해주세요 라고말해야하는일이자주생긴다. 그런데기업에서는인증하지않은접근을절대허용하지도않는데다가아예다른사람의컴퓨터에접근할수없다. 그래서공유하려면중앙서버에 Push하고나서 Pull해야한다고다른사람에게말해야만한다. 중앙집중식 VCS에서이렇게마구사용하면브랜치이름이충돌할확률이높다. 그때마다관리자는추가로권한을관리해줘야하기때문에관리자의노력이쓸데없이낭비된다. Gitolite는모든개발자가 personal 이나 scratch 네임스페이스를가질수있도록허용한다. 이네임스페이스는 refs/personal/<devname>/* 라고표현한다. 자세한내용은 Gitolite 문서를참고한다. 4.8.7 와일드카드 저장소 Gitolite는펄정규표현식으로저장소이름을표현하기때문에와일드카드를사용할수있다. 그래서 assignments/s[0-9][0-9]/a[0-9][0-9] 같은정규표현식을사용할수있다. 사용자가새로운저장소를만들수있는새로운권한모드인 C 모드를사용할수도있다. 저장소를새로만든사람에게는자동으로접근권한이부여된다. 다른사용자에게접근권한을주려면 R 또는 RW 권한을설정한다. 다시한번말하지만문서에모든내용이다있으므로꼭보기를바란다. 4.8.8 그밖의기능들마지막으로알고있으면유용한것들이있다. Gitolite에는많은기능이있고자세한내용은 Faq, Tip, 등등 의다른문서에잘설명돼있다. 로깅 : 누군가성공적으로접근하면 Gitolite는무조건로그를남긴다. 관리자가한눈파는사이에되돌리기 (RW+) 권한을가진망나니가 master 브랜치를날려버릴수있다. 이경우로그파일이구원해준다. 이로그파일을참고하여버려진 SHA를빠르고쉽게찾을수있다. 접근권한보여주기 : 만약어떤서버에서작업을시작하려고할때필요한것이무엇일까? Gitolite는해당서버에대해접근할수있는저장소가무엇인지, 어떤권한을가졌는지보여준다 : hello scott, this is git@git running gitolite3 v3.01-18-g9609868 on git 1.7.4.4 R anu-wsd R entrans R W git-notes R W gitolite R W gitolite-admin R indic_web_input R shreelipi_converter 권한위임 : 조직규모가크면저장소에대한책임을여러사람이나눠가지는게좋다. 여러사람이각자맡은바를관리하도록한다. 그래서주요관리자의업무가줄어들기에병목현상이적어진다. 이기능에대해서는 doc/ 디렉토리에포함된 Gitolite 문서를참고하라. 미러링 : Gitolite의미러는여러개만들수있어서주서버가다운돼도변경하면된다. 4.9 Git 데몬 공개된프로젝트는누가읽기접근을시도하는지알필요가없다. 그래서 HTTP 프로토콜을사용하거나 Git 프로토콜을사용해야한다. Git 프로토콜이 HTTP 프로토콜보다효율적이기때문에속도가빠 93

4 장 Git 서버 Scott Chacon Pro Git 르다. 따라서사용자의시간을절약해준다. 다시강조하지만, 이것은불특정다수에게읽기접근을허용할때에만쓸만하다. 만약서버가외부에그냥노출돼있으면우선방화벽으로보호하고프로젝트만외부에서접근할수있게만든다. 서버를방화벽으로보호하고있으면 CI 서버나빌드서버같은컴퓨터나사람이읽기접근이가능하도록설정한다. 모두 SSH 키를일일이추가하고싶지않을때이방법을사용한다. 어쨌든 Git 프로토콜은상대적으로설치하기쉽다. 그냥데몬을실행한다 : git daemon --reuseaddr --base-path=/opt/git/ /opt/git/ --reuseaddr는서버가기존의연결이타임아웃될때까지기다리지말고바로재시작하게하는옵션이다. --base-path 옵션을사용하면사람들이프로젝트를 Clone할때전체경로를사용하지않아도된다. 그리고마지막에있는경로는노출할저장소의위치다. 마지막으로방화벽을사용하고있으면 9418 포트를열어서지금작업하는서버의숨통을틔워준다. 운영체제에따라 Git 데몬을실행시키는방법은다르다. 우분투에서는 Upstart 스크립트를사용한다. 아래와같이파일을만들고 : /etc/event.d/local-git-daemon 다음의내용을입력한다 : start on startup stop on shutdown exec /usr/bin/git daemon \ --user=git --group=git \ --reuseaddr \ --base-path=/opt/git/ \ /opt/git/ respawn 저장소를읽을수만있는사용자로데몬을실행시킬것을보안을위해강력하게권고한다. git-ro라는계정을새로만들고그계정으로데몬을실행시킨다. 여기에서는쉽게설명하려고그냥 Gitosis를실행했던 git 계정으로실행시킨다. 서버가재시작할때 Git 데몬이자동으로실행되고데몬이죽어도자동으로재시작된다. 서버는놔두고 Git 데몬만재시작할수있다 : initctl start local-git-daemon 다른시스템에서는 sysvinit 시스템의 xinetd 스크립트를사용하거나자신만의방법으로해야한다. Git 데몬을통해서아무나읽을수있다는것을 Gitosis 서버에알려주어야한다. Git 데몬으로읽기접근을허용하는저장소가무엇인지설정에추가해야한다. 만약 iphone_project에 Git 프로토콜을허용했다면아래와같은것을 gitosis.conf 파일의하단에추가한다 : 94

Scott Chacon Pro Git 4.10 절 Hosted Git [repo iphone_project] daemon = yes 차례대로커밋과 Push하고나면지금실행중인데몬이 9418 포트로접근하는사람에게서비스하기시작한다. Gitosis 없이도 Git 데몬을설치할수있지만그러려면서비스하고자하는프로젝트마다아래와같이 git-daemon-export-ok 파일을넣어주어야한다 : $ cd /path/to/project.git $ touch git-daemon-export-ok 이파일이있으면 Git 데몬은인증없이프로젝트를노출하는것으로판단한다. 또한, Gitweb 으로노출하는프로젝트도 Gitosis 로제어할수있다. 먼저 /etc/gitweb.conf 파일에아 래와같은내용을추가해야한다 : $projects_list = "/home/git/gitosis/projects.list"; $projectroot = "/home/git/repositories"; $export_ok = "git-daemon-export-ok"; @git_base_url_list = ('git://gitserver'); Gitosis 설정파일에 gitweb 설정을넣거나빼면사용자는 GitWeb 을통해프로젝트를볼수도있고못 볼수도있다. [repo iphone_project] daemon = yes gitweb = yes 이제이것을커밋하고 Push 하면 GitWeb 을통해 iphone_project 를볼수있다. 4.10 Hosted Git Git 서버를설치하는등의일을하고싶지않으면전문호스팅사이트를이용하면된다. 호스팅사이트는몇가지장점이있다. 설정이쉬워서바로프로젝트를시작할수있을뿐만아니라직접서버를관리하고모니터링하지않아도된다. 내부적으로 Git 서버를직접설치하고운영하고있어도오픈소스프로젝트는호스팅사이트를이용하는것이좋다. 이렇게하면보통오픈소스커뮤니티로부터좀더쉽게도움받을수있다. 요즘은이용할수있는호스팅사이트들이많다. 각각장단점이있기때문에다음페이지에서최신정보를확인해보자 : 95

4 장 Git 서버 Scott Chacon Pro Git https://git.wiki.kernel.org/index.php/githosting 이절에서전부설명할수는없고 ( 필자는저회사중한군데에서일한다 ) GitHub에계정과프로젝트를만드는방법을설명한다. GitHub은가장큰오픈소스 Git 호스팅사이트이고공개 (Public) 프로젝트와비공개 (Private) 프로젝트에대한호스팅서비스를제공하는보기드문사이트다. 그래서상업용비공개코드와공개코드를같은곳에둘수있다. 실제로이책도 GitHub에서비공개로작성했다. 4.10.1 GitHub GitHub는프로젝트네임스페이스가다른코드호스팅사이트들과다르다. GitHub는프로젝트가아니라사용자가중심이다. GitHub에 grit 프로젝트를호스팅하고싶으면 github.com/grit이아니라 github.com/schacon/grit으로접속해야한다. 그리고처음프로젝트를시작한사람이그프로젝트를잊어버려도누구나프로젝트를이어갈수있다. 프로젝트주저장소라고해서특별한게아니다. GitHub은이윤을목적으로하는회사이기때문에비공개저장소를만들려면돈을내야한다. 하지만, 누구나손쉽게무료계정을만들어오픈소스프로젝트를시작할수있다. 어떻게사용하는지간략하게설명한다. 4.10.2 계정설정하기 먼저무료계정을하나만든다. 가격정책에대해알려주며가입을시작할수있는에방문하여 Sign up 버튼을클릭한다. 그러면가입페이지로이동한다. 그림 4.2: GitHub 가격정책페이지 아직등록되지않은사용자이름을입력하고 e-mail 주소와암호를입력한다 ( 그림 4-3). 그리고 SSH 공개키가있으면바로등록한다. SSH 키를만드는방법은 바로설정하기 절에서이미설명했다. 그공개키파일의내용을복사해서 SSH 공개키입력박스에붙여넣는다. explain ssh keys 링크를클릭하면 key를생성하는방법을자세히설명해준다. 주요운영체제에서하는방법이모두설명돼있다. I agree, sign me up 버튼을클릭하면자신만의대쉬보드페이지가나타난다. 그리고저장소를만들자. 4.10.3 저장소만들기 Your Repositories옆에있는 create a new one 링크를클릭하면저장소를만드는입력폼을볼수있다 ( 그림 4-5). 이폼에프로젝트이름과프로젝트설명을적는다. 다적은후에 Create Repository 버튼을클릭하면 GitHub에저장소가생긴다. 96

Scott Chacon Pro Git 4.10 절 Hosted Git 그림 4.3: GitHub 가입폼 그림 4.4: GitHub 사용자대쉬보드 그림 4.5: GitHub 의저장소를생성하는폼 그림 4.6: GitHub 프로젝트정보 이저장소에는아직코드가없어서 GitHub 은프로젝트를새로만드는방법, 이미있는 Git 프로젝트를 Push 하는법, 공개된 Subversion 저장소에서프로젝트를가져오는 (Import) 방법등을보여준다. 여기설명하는내용은이미우리가배웠다. 프로젝트가없을때는아래와같이프로젝트를초기화한다 : 97

4 장 Git 서버 Scott Chacon Pro Git 그림 4.7: 새저장소를위한사용설명서 $ git init $ git add. $ git commit -m 'initial commit' 만약이미로컬에 Git 저장소가있으면 GitHub 저장소를리모트저장소로등록하고 master 브랜치를 Push 한다 : $ git remote add origin git@github.com:testinguser/iphone_project.git $ git push origin master 이제프로젝트가 GitHub 에서서비스되니공유하고싶은사람에게 URL 을알려주면된다. URL 은이 다. 그리고이저장소의정보를잘살펴보면 Git URL 이두개라는것을발견할수있다. 그림 4.8: 프로젝트의공개 URL 과비공개 URL Public Clone URL은말그대로누구나프로젝트를 Clone할수있도록모두에게읽기전용으로공개하는것이다. 이 URL을다른사람에알려주거나웹사이트같은데공개하는것을부담스러워하지않아도된다. Your Clone URL은읽고쓸수있는 SSH 기반 URL이다. 사용자계정에등록한공개키와한짝인개인키로만접속할수있다. 다른사용자로이프로젝트에방문하면이 URL은볼수없고공개 URL만볼수있다. 98

Scott Chacon Pro Git 4.10 절 Hosted Git 4.10.4 Subversion으로부터코드가져오기 (Import) GitHub은공개중인 Subversion 프로젝트를 Git 프로젝트로만들어준다. 사용설명서하단에있는 Subversion에서 Import하기 링크를클릭하면임포트폼을볼수있고거기에 Subversion 프로젝트의 URL을넣는다 ( 그림 4-9). 그림 4.9: Subversion 프로젝트를 Import 하는화면 프로젝트가비표준방식을사용하거나규모가너무크고비공개라면이기능을사용할수없다. 7 장에 서수동으로임포트하는방법에대해좀더자세히배운다. 4.10.5 동료추가하기동료를추가하자. 먼저 John씨, Josie씨, Jessica씨를모두 GitHub에가입시키고나서그들을동료로추가하고저장소에 Push할수있는권한을준다. 프로젝트페이지에있는 Admin 버튼을클릭해서관리페이지로이동한다 ( 그림 4-10). 그림 4.10: GitHub 의프로젝트관리페이지 다른사람에게쓰기권한을주려면 Add another collaborator 링크를클릭한다. 그러면텍스트박스가새로나타나는데거기에사용자이름을입력한다. 사용자이름을입력하기시작하면자동으로시스템에존재하는사용자를찾아서보여준다. 원하는사용자를찾으면 Add 버튼을클릭해서그사용자를동료로만든다. 추가한사람은동료목록박스에서모두확인할수있다 ( 그림 4-12). 그리고만약다시혼자작업하고싶어지면 revoke 링크를클릭하여쫓아낸다. 쫓겨나면더는 Push 할수없다. 또기존프로젝트에등록된동료를그룹으로묶어추가할수도있다. 4.10.6 내프로젝트 Subversion에서 Import했거나로컬의프로젝트를 Push하고나면프로젝트메인페이지가그림 4-13같이바뀐다. 사람들이이프로젝트에방문하면이페이지가제일처음보인다. 이페이지는몇가지탭으로구성된다. Commits 탭은지금까지의커밋을 git log 명령을실행시킨것처럼최신것부터보여준다. Network 99

4 장 Git 서버 Scott Chacon Pro Git 그림 4.11: 프로젝트에동료추가하기 그림 4.12: 프로젝트동료들 그림 4.13: GitHub 의프로젝트메인페이지 탭은프로젝트를복제한사람들과기여한사람들을모두보여준다. Downloads 탭에는바이너리파일이나프로젝트의태그버전을압축해서올릴수있다. Wiki 탭은프로젝트에대한정보나문서를쓰는곳이다. Graphs 탭은사람들의활동을그림과통계로보여준다. 메인탭인 Source 탭은프로젝트의메인디렉토리를보여주고 README 파일이있으면자동으로화면에출력해준다. 그리고마지막커밋내용도함께보여준다. 4.10.7 프로젝트 Fork 권한이없는프로젝트에참여하고싶으면 GitHub는프로젝트를 Fork하도록권고한다. 마침매우흥미롭게보이는프로젝트를발견했다고하자. 그프로젝트를조금뜯어고치려면프로젝트페이지상단에있는 fork 버튼을클릭한다. 그러면 GitHub는접속한사용자의계정으로프로젝트를 Fork해준다. 사용자는이프로젝트에마음대로 Push할수있다. 굳이 Push할수있도록사람들을동료로추가하지않아도된다. 사람들은마음껏프로젝트를 Fork하 100

Scott Chacon Pro Git 4.11 절요약 고 Push할수있다. 그리고원래프로젝트의관리자는다른사람의프로젝트를리모트저장소로추가하고그작업물을가져와서 Merge한다. 프로젝트페이지에들어가서상단의 fork 버튼을클릭하여프로젝트를복제한다 ( 그림 4-14) 그림 4-14의예는 mojombo/chronic 프로젝트페이지이다 그림 4.14: 어떤저장소든지 fork 버튼을클릭하면 Push 할수있는저장소를얻을수있다 클릭하는순간, 이프로젝트를즉시 Fork 한다 ( 그림 4-15). 그림 4.15: Fork 한프로젝트 4.10.8 GitHub 요약빨리한번전체를훑어보는것이중요하기때문에여기에서는 GitHub에대해이정도로만설명했다. 몇분만에계정과프로젝트를만들고 Push까지할수있다. GitHub에있는개발자커뮤니티규모는매우크기때문에만약 GitHub에오픈소스프로젝트를만들면다른개발자들이당신의프로젝트를복제하고당신을도울것이다. GitHub는 Git을빨리사용해볼수있도록돕는다. 4.11 요약 리모트저장소를만들고다른사람과협업하거나작업물을공개하는방법은여러가지다. 서버를직접구축하는것은할일이많은데다가방화벽도필요하다. 그리고이렇게서버를만들고관리하는일은시간이많이든다. 호스팅사이트를이용하면쉽게시작할수있다. 하지만, 코드를타인의서버에보관해야하기때문에사용하지않는조직들이많다. 자신의조직에서어떤방법으로협업해야할지고민해야하는시점이되었다. 101

5 장 분산환경에서의 Git 앞장에서다른개발자와코드를공유하는리모트저장소를만드는법을배웠다. 로컬에서작업하는데필요한기본적인명령어에는어느정도익숙해졌다. 이제는분산환경에서 Git이제공하는기능을어떻게효율적으로사용할지를배운다. 이번장에서는분산환경에서 Git을어떻게사용할수있을지살펴본다. 프로젝트기여자입장과여러수정사항을취합하는관리자입장에서두루살펴본다. 즉, 프로젝트기여자또는관리자로서작업물을프로젝트에어떻게포함시킬지와수많은개발자가수행한일을취합하고프로젝트를운영하는방법을배운다. 5.1 분산환경에서의 Workflow 중앙집중형버전관리시스템과는달리 Git은분산형이다. Git의구조가훨씬더유연하기때문에여러개발자가함께작업하는방식을더다양하게구성할수있다. 중앙집중형버전관리시스템에서각개발자는중앙저장소를중심으로하는하나의노드일뿐이다. 하지만, Git에서는각개발자의저장소가하나의노드이기도하고중앙저장소같은역할도할수있다. 즉, 모든개발자는다른개발자의저장소에일한내용을전송하거나, 다른개발자들이참여할수있도록자신이운영하는저장소위치를공개할수도있다. 이런특징은프로젝트나팀이코드를운영할때다양한 Workflow을만들수있도록해준다. 이런유연성을살려저장소를운영하는몇가지방식을소개한다. 각방식의장단점을살펴보고그방식중하나를고르거나여러가지를적절히섞어쓰면된다. 5.1.1 중앙집중식 Workflow 중앙집중식시스템에서는보통중앙집중식협업모델이라는한가지방식밖에없다. 중앙저장소는딱하나있고변경사항은모두이중앙저장소에집중된다. 개발자는이중앙저장소를중심으로작업한다 ( 그림 5-1). 중앙집중식에서개발자두명이중앙저장소를 Clone하고각자수정하는상황을생각해보자. 한개발자가자신이한일을커밋하고나서아무문제없이서버에 Push한다. 그러면다른개발자는자신의일을커밋하고 Push하기전에첫번째개발자가한일을먼저 Merge해야한다. Merge를해야첫번째개발자가작업한내용을덮어쓰지않는다. 이런개념은 Subversion과같은중앙집중식버전관리시스템에서사용하는방식이고 Git에서도당연히이런 Workflow를사용할수있다. 팀이작거나이미중앙집중식에적응한상황이라면이 Workflow에따라 Git을도입하여사용할수있다. 중앙저장소를하나만들고개발자모두에게 Push 권한을부여한다. 모두에게 Push 권한을부여해도 Git은한개발자가다른개발자의작업내용을덮어쓰도록허용하지않는다. 한개발자가 Clone하고나서수정하는사이에이미다른개발자가중앙저장소에무언가 Push했다면이개발자는 Push할수 103

5 장분산환경에서의 Git Scott Chacon Pro Git 그림 5.1: 중앙집중식 Workflow 없다. Git 은개발자에게지금 Push 하려는커밋으로 Fast-forward 할수없으니 Fetch 하고 Merge 해 야서버로 Push 할수있다고알려준다. 이런개념은개발자에게익숙해서거부감없이도입할수있다. 5.1.2 Integration-Manager Workflow Git을사용하면리모트저장소를여러개운영할수있다. 다른개발자는읽기만가능하고자신은쓰기도가능한공개저장소를만드는 Workflow도가능하다. 이 Worlflow에는보통프로젝트를대표하는하나의공식저장소가있다. 기여자는우선공식저장소를하나 Clone하고수정하고나서자신의저장소에 Push한다. 그다음에프로젝트 Integration-Manager에게새저장소에서 Pull하라고요청한다. 그러면그 Integration-Manager는기여자의저장소를리모트저장소로등록하고, 로컬에서기여물을테스트하고, 프로젝트메인브랜치에 Merge하고, 그내용을다시프로젝트메인저장소에 Push한다. 이런과정은아래와같다 ( 그림 5-2). 1. 프로젝트 Integration-Manager는프로젝트메인저장소에 Push를한다. 2. 프로젝트기여자는메인저장소를 Clone하고수정한다. 3. 기여자는자신의저장소에 Push하고 Integration-Manager가접근할수있도록공개해놓는다. 4. 기여자는 Integration-Manager에게변경사항을적용해줄것을 E-mail 같은것으로요청한다. 5. Integration-Manager는기여자의저장소를리모트저장소로등록하고수정사항을 Merge하여테스트한다. 6. Integration-Manager는 Merge한사항을메인저장소에 Push한다. 그림 5.2: Integration-Manager Workflow 이방식은 GitHub 같은사이트에서주로사용하는방식이다. GitHub는프로젝트를 Fork하고수정사항을반영하여다시모두에게공개하기좋은구조로되어있다. 이방식의장점은기여자와 Integration-Manager가각자의사정에맞춰프로젝트를유지할수있다는점이다. 기여자는자신의 104

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 저장소와브랜치에서수정작업을계속해나갈수있고수정사항이프로젝트에반영되도록기다릴필요 가없다. 관리자는여유를가지고기여자가 Push 해놓은커밋을적절한시점에 Merge 한다. 5.1.3 Dictator and Lieutenants Workflow 이방식은저장소를여러개운영하는방식을변형한구조이다. 보통수백명의개발자가참여하는아주큰프로젝트를운영할때이방식을사용한다. 리눅스커널프로젝트가대표적이다. 여러명의 Integration-Manager가저장소에서자신이맡은부분만을담당하는데이들을 Lieutenants라고부른다. 모든 Lieutenant는최종관리자아래에있으며이최종관리자를 Dictator라고부른다 ( 그림 5-3). 1. 개발자는코드를수정하고 master 브랜치를기준으로자신의토픽브랜치를 Rebase한다. 여기서 master 브랜치란 Dictator의브랜치를말한다. 2. Lieutenant들은개발자들의수정사항을자신이관리하는 master 브랜치에 Merge한다. 3. Dictator는 Lieutenant의 master 브랜치를자신의 master 브랜치로 Merge한다. 4. Dictator는 Merge한자신의 master 브랜치를 Push하여다른모든개발자가 Rebase할수있는기준으로만든다. 그림 5.3: Benevolent dictator Workflow 이방식이일반적이지않지만깊은계층구조를가지는환경이나규모가큰프로젝트에서는매우쓸모있다. 프로젝트리더가모든코드를통합하기전에코드를부분부분통합하도록여러명의 Lieutenant 에게위임한다. 이세가지 Workflow가 Git 같은분산버전관리시스템에서주로사용하는것들이다. 사실이런 Workflow뿐만아니라다양한변종 Workflow가실재로사용된다. 어떤방식을선택하고혹은조합해야하는지살짝감이잡힐것이다. 이장에서는몇가지구체적사례를들고우리가다양한환경에서각역할을어떻게수행하는지살펴본다. 5.2 프로젝트에기여하기 이미다른장에서기본적인 Git 사용법에대해서배웠고몇가지 Workflow도살펴보았다. 이제이절에서는 Git으로프로젝트에기여하는방법에대해배운다. 프로젝트에기여하는방식이매우다양하다는점을설명하기란정말어렵다. Git이워낙유연하게설계됐기때문에사람들은여러가지방식으로사용할수있다. 게다가프로젝트마다환경이달라서프로 105

5 장분산환경에서의 Git Scott Chacon Pro Git 젝트에기여하는방식을쉽게설명하기란정말어렵다. 기여하는방식에영향을끼치는몇가지변수가있다. 활발히기여하는개발자의수가얼마인지, 선택한 Workflow가무엇인지, 각개발자에게접근권한을어떻게부여했는지, 외부에서도기여할수있는지등이변수다. 첫번째로살펴볼변수는활발히활동하는개발자의수이다. 얼마나많은개발자가얼마나자주코드를쏟아내는가하는점이활발한개발자의기준이다. 대부분둘, 셋정도의개발자가하루에몇번커밋을하고활발하지않은프로젝트는더띄엄띄엄할것이다. 하지만, 아주큰프로젝트는수백, 수천명의개발자가하루에도수십, 수백개의커밋을만들어낸다. 개발자가많으면많을수록코드를깔끔하게적용하거나 Merge하기어려워진다. 어떤것은다른개발자가기여한것으로불필요해지기도하고때론서로충돌이일어난다. 어떻게해야코드를최신으로유지하면서원하는대로수정할수있을까? 두번째변수는프로젝트에서선택한저장소운영방식이다. 메인저장소에개발자모두가쓰기권한을가지는중앙집중형방식인가? 프로젝트에모든 Patch를검사하고통합하는관리자가따로있는가? 모든수정사항을개발자끼리검토하고승인하는가? 자신도기여이상의역할을하고있는지? 중간관리자가있어서그들에게먼저알려야하는가? 세번째변수는접근권한이다. 프로젝트에쓰기권한이있어서직접쓸수있는가? 아니면읽기만가능한가? 에따라서프로젝트에기여하는방식이매우달라진다. 쓰기권한이없다면어떻게수정사항을프로젝트에반영할수있을까? 수정사항을적용하는정책이프로젝트에있는가? 얼마나많은시간을프로젝트에할애하는가? 얼마나자주기여하는가? 이런질문에따라프로젝트에기여하는방법과 Workflow 등이달라진다. 간단한것부터복잡한것까지각상황을살펴보면실제프로젝트에필요한방식을선택할수있게된다. 5.2.1 커밋가이드라인다른것보다먼저커밋메시지에대한주의사항을알아보자. 커밋메시지를잘작성하는가이드라인을알아두면다른개발자와함께일하는데도움이많이된다. Git 프로젝트에보면커밋메시지를작성하는데참고할만한좋은팁이많다. Git 프로젝트의 Documentation/SubmittingPatches 문서를참고하자. 공백문자를깨끗하게정리하고커밋해야한다. Git은공백문자를검사해볼수있는간단한명령을제공한다. 커밋을하기전에 git diff --check 명령으로공백문자에대한오류를확인할수있다. 아래예제를보면잘못사용한공백을 X 문자로바꾸어표시해준다 : $ git diff --check lib/simplegit.rb:5: trailing whitespace. + @git_dir = File.expand_path(git_dir)XX lib/simplegit.rb:7: trailing whitespace. + XXXXXXXXXXX lib/simplegit.rb:26: trailing whitespace. + def command(git_cmd)xxxx 커밋을하기전에공백문자에대해검사를하면공백으로불필요하게커밋되는것을막고이런커밋으로인해불필요하게다른개발자들이신경쓰는일을방지할수있다. 그리고각커밋은논리적으로구분되는 Changeset이다. 최대한수정사항을하나의주제로요약할수있어야하고여러가지이슈에대한수정사항을하나의커밋에담지않아야한다. 여러가지이슈를한꺼번에수정했다고하더라도 Staging Area을이용하여한커밋에하나의이슈만담기도록한다. 작업내용을분할하고, 각커밋마다적절한메시지를작성한다. 같은파일의다른부분을수정하는경우에는 106

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 git add -patch 명령을써서한부분씩나누어 Staging Area에저장해야한다 ( 관련내용은 6장에서다룬다 ). 결과적으로최종프로젝트의모습은한번에커밋을하든다섯번에나누어커밋을하든똑같다. 하지만, 여러번나누어커밋하는것이좋다. 다른동료가수정한부분을확인할때나각커밋의시점으로복원해서검토할때이해하기훨씬쉽다. 6장에서이미저장된커밋을다시수정하거나파일을단계적으로 Staging Area에저장하는방법을살펴본다. 다양한도구를이용해서간단하고이해하기쉬운커밋을쌓아가야한다. 마지막으로명심해야할점은커밋메시지자체다. 좋은커밋메시지를작성하는습관은 Git을사용하는데도움이많이된다. 일반적으로커밋메시지를작성할때사용하는규칙이있다. 메시지의첫줄에 50자가넘지않는아주간략한메시지를적어해당커밋을요약한다. 다음한줄은비우고그다음줄부터커밋을자세히설명한다. 예를들어 Git 개발프로젝트에서는개발동기와구현상황의제약조건이나상황등을자세하게요구한다. 이런점은따를만한좋은가이드라인이다. 그리고현재형표현을사용하는것이좋다. 예를들어 I added tests for ( 테스트를추가함 ) 보다는 Add tests for ( 테스트추가 ) 와같은메시지를작성한다. 아래예제는 Pope at tpope.net이작성한커밋메시지이다. 영문 50 글자이하의간략한수정요약 자세한설명. 영문 72글자이상이되면줄바꿈을하고이어지는내용을작성한다. 특정상황에서는첫번째줄이이메일메시지의제목이되고나머지는메일내용이된다. 간략하게요약하고넣는빈줄은자세한설명을아예쓰지않는한매우중요하다. 이어지는내용도한줄띄우고쓴다. - 목록표시도사용할수있다. - 보통 '-' 나 '*' 표시를사용해서목록을표현하고표시앞에공백 하나, 각목록사이에는빈줄을하나를넣는데상황에따라다르다. 메시지를이렇게작성하면함께일하는사람은물론이고자신에게도매우유용하다. Git 개발프로젝트에는잘쓰인커밋메시지가많으므로프로젝트를내려받아서 git log --no-merges 명령으로꼭살펴보기를권한다. 이책에서설명하는예제의커밋메시지는시간관계상위와같이아주멋지게쓰지않았다. git commit 명령에서 -m 옵션을사용하여간단하게적는다. 하지만! 저자처럼하지말고시키는대로하셔야한다. 5.2.2 비공개소규모팀두세명으로이루어진비공개프로젝트가가장간단한프로젝트일것이다. 비공개라고함은소스코드가공개되지않은것을말하는것이지외부에서접근할수없는것을말하지않는다. 모든개발자는공유하는저장소에쓰기권한이있어야한다. 이런환경에서는보통 Subversion 같은중앙집중형버전관리시스템에서사용하던방식을사용한다. 물론 Git이가진오프라인커밋기능이나브랜치 Merge 기능을이용하긴하지만크게다르지않다. 가장큰차이점은서버가아닌클라이언트쪽에서 Merge한다는점이다. 두개발자가저장소를공유하는시나리오를살펴보자. 개발자 John씨는저장소를 Clone하고파일을수정하고나서로컬에커밋한다 (Git이출력하는메시지는... 으로줄이고생략한다 ). 107

5 장분산환경에서의 Git Scott Chacon Pro Git # John's Machine $ git clone john@githost:simplegit.git Initialized empty Git repository in /home/john/simplegit/.git/... $ cd simplegit/ $ vim lib/simplegit.rb $ git commit -am 'removed invalid default value' [master 738ee87] removed invalid default value 1 files changed, 1 insertions(+), 1 deletions(-) 개발자 Jessica 씨도저장소를 Clone 하고나서파일을하나새로추가하고커밋한다 : # Jessica's Machine $ git clone jessica@githost:simplegit.git Initialized empty Git repository in /home/jessica/simplegit/.git/... $ cd simplegit/ $ vim TODO $ git commit -am 'add reset task' [master fbff5bc] add reset task 1 files changed, 1 insertions(+), 0 deletions(-) Jessica 씨는서버에커밋을 Push 한다 : # Jessica's Machine $ git push origin master... To jessica@githost:simplegit.git 1edee6b..fbff5bc master -> master John 씨도서버로커밋을 Push 하려고한다 : # John's Machine $ git push origin master To john@githost:simplegit.git! [rejected] master -> master (non-fast forward) error: failed to push some refs to 'john@githost:simplegit.git' Jessica 씨의 Push 는성공했지만, John 씨의커밋은서버에서거절된다. Subversion 을사용했던사 람은이부분을이해하는것이중요하다. 같은파일을수정한것도아닌데왜 Push 가거절되는걸까? 108

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 Subversion 에서는서로다른파일을수정하는이런 Merge 작업은자동으로서버가처리한다. 하 지만, Git 은로컬에서먼저 Merge 해야한다. John 씨는 Push 하기전에 Jessica 씨가수정한커밋을 Fetch 하고 Merge 한다 : $ git fetch origin... From john@githost:simplegit + 049d078...fbff5bc master -> origin/master Fetch 하고나면 John 씨의로컬저장소는그림 5-4 와같이된다. 그림 5.4: Fetch 하고난 John 씨의저장소 John 씨는 Jessica 씨가저장소로 Push 했던커밋과를로컬저장소에가져왔다. 하지만, Push 하기전 에 Fetch 한브랜치를 Merge 해야한다 : $ git merge origin/master Merge made by recursive. TODO 1 + 1 files changed, 1 insertions(+), 0 deletions(-) Merge 가잘이루어지면 John 씨의브랜치는그림 5-5 와같은상태가된다. John 씨는 Merge 하고나서자신이작업한코드가제대로동작하는지확인한다. 그후에공유하는저 장소에 Push 한다 : $ git push origin master... To john@githost:simplegit.git fbff5bc..72bbc59 master -> master 109

5 장분산환경에서의 Git Scott Chacon Pro Git 그림 5.5: origin/master 브랜치를 Merge 하고난후, John 씨의저장소 이제 John 씨의저장소는그림 5-6 처럼되었다. 그림 5.6: Push 하고난후, John 씨의저장소 동시에 Jessica 씨는토픽브랜치를하나만든다. issue54 브랜치를만들고세번에걸쳐서커밋한다. 아직 John 씨의커밋을 Fetch 하지않은상황이기때문에그림 5-7 과같은상황이된다. 그림 5.7: Jessica 씨의저장소 Jessica 씨는 John 씨의작업을적용하려면 Fetch 를해야한다 : # Jessica's Machine $ git fetch origin... From jessica@githost:simplegit fbff5bc..72bbc59 master -> origin/master 위명령으로 John 씨가 Push 한커밋을모두내려받는다. 그러면 Jessica 씨의저장소는그림 5-8 과 같은상태가된다. 110

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 그림 5.8: John 씨의커밋을 Fetch 한후 Jessica 씨의저장소 이제 orgin/master 와 Merge 할차례다. Jessica 씨는토픽브랜치작업을마치고어떤내용이 Merge 되는지 git log 명령으로확인한다 : $ git log --no-merges origin/master ^issue54 commit 738ee872852dfaa9d6634e0dea7a324040193016 Author: John Smith <jsmith@example.com> Date: Fri May 29 16:01:27 2009-0700 removed invalid default value Merge 할내용을확인한 Jessica 씨는자신이작업한내용과 John 씨가 Push 한작업 (origin/master) 을 master 브랜치에 Merge 하고 Push 한다. 모든내용을합치기전에우선 master 브랜치를 Checkout 한다 : $ git checkout master Switched to branch "master" Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded. origin/master, issue54 모두 master 보다 Fast-forward 된브랜치이기때문에둘중에무엇을먼 저 Merge 하든상관이없다. 물론어떤것을먼저 Merge 하느냐에따라히스토리순서는달라지지만, 최종결과는똑같다. Jessica 씨는먼저 issue54 브랜치를 Merge 한다 : $ git merge issue54 Updating fbff5bc..4af4298 Fast forward README 1 + lib/simplegit.rb 6 +++++- 2 files changed, 6 insertions(+), 1 deletions(-) 보다시피 Fast-forward Merge 이기때문에별문제없이실행된다. 다음은 John 씨의커밋 (origin/ master) 을 Merge 한다 : 111

5 장분산환경에서의 Git Scott Chacon Pro Git $ git merge origin/master Auto-merging lib/simplegit.rb Merge made by recursive. lib/simplegit.rb 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) 위와같이 Merge 가잘되면그림 5-9 와같은상태가된다. 그림 5.9: Merge 이후 Jessica 씨의저장소 origin/master 브랜치가 Jessica 씨의 master 브랜치로나아갈 (reachable) 수있기때문에 Push 는성공한다 ( 물론 John 씨가그사이에 Push 를하지않았다면 ): $ git push origin master... To jessica@githost:simplegit.git 72bbc59..8059c15 master -> master 두개발자의커밋과 Merge 가성공적으로이루어지고난후의결과는 5-10 과같다. 그림 5.10: Jessica 씨가서버로 Push 하고난후의저장소 여기서살펴본예제가가장간단한상황이다. 토픽브랜치에서수정하고로컬의 master 브랜치에 Merge한다. 작업한내용을프로젝트의공유저장소에 Push하고자할때에는우선 origin/master 브랜치를 Fetch하고 Merge한다. 그리고나서 Merge한결과를다시서버로 Push한다. 이런 Workflow 가일반적이고그림 5-11로나타낼수있다. 5.2.3 비공개대규모팀이제비공개대규모팀에서의역할을살펴보자. 이런상황에는보통팀을여러개로나눈다. 그래서각각의작은팀이서로어떻게하나로 Merge하는지를살펴본다. 112

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 그림 5.11: 여러개발자가 Git 을사용하는 Workflow John씨와 Jessica씨는한팀이고프로젝트에서어떤한부분을담당한다. 또한, Jessica씨와 Josie 씨도다른부분을담당하는한팀이다. 이런상황이라면회사는 Integration-manager Workflow를선택하는게좋다. 작은팀이수행한결과물은 Integration-Manager가 Merge하고공유저장소의 master 브랜치를업데이트한다. 팀마다브랜치를하나씩만들고 Integration-Manager는그브랜치를 Pull해서 Merge한다. 두팀에모두속한 Jessica씨의작업순서를살펴보자. 우선 Jessica씨는저장소를 Clone하고 featurea 작업을먼저한다. featurea 브랜치를만들고수정을하고커밋을한다 : # Jessica's Machine $ git checkout -b featurea Switched to a new branch "featurea" $ vim lib/simplegit.rb $ git commit -am 'add limit to log function' [featurea 3300904] add limit to log function 1 files changed, 1 insertions(+), 1 deletions(-) 이수정한부분을 John 씨와공유해야한다. 공유하려면우선 featurea 브랜치를서버로 Push 한다. Integration-Manager 만 master 브랜치를업데이트할수있기때문에 master 브랜치로 Push 를할 수없고다른브랜치로 John 과공유한다 : $ git push origin featurea... 113

5 장분산환경에서의 Git Scott Chacon Pro Git To jessica@githost:simplegit.git * [new branch] featurea -> featurea Jessica 씨는자신이한일을 featurea 라는브랜치로 Push 했다는이메일을 John 씨에게보낸다. John 씨의피드백을기다리는동안 Jessica 씨는 Josie 씨와함께하는 featureb 작업을하기로한다. 서버의 master 브랜치를기반으로새로운브랜치를하나만든다 : # Jessica's Machine $ git fetch origin $ git checkout -b featureb origin/master Switched to a new branch "featureb" 몇가지작업을하고 featureb 브랜치에커밋한다 : $ vim lib/simplegit.rb $ git commit -am 'made the ls-tree function recursive' [featureb e5b0fdc] made the ls-tree function recursive 1 files changed, 1 insertions(+), 1 deletions(-) $ vim lib/simplegit.rb $ git commit -am 'add ls-files' [featureb 8512791] add ls-files 1 files changed, 5 insertions(+), 0 deletions(-) 그럼 Jessica 씨의저장소는그림 5-12 과같다. 그림 5.12: Jessica 씨의저장소 작업을마치고 Push하려고하는데 Jesie씨가이미일부작업을하고서버에 featurebee 브랜치로 Push했다는이메일을보내왔다. Jessica씨는 Jesie씨의작업을먼저 Merge해야만 Push할수있다. Merge하기위해서우선 git fetch로 Fetch한다 : 114

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 $ git fetch origin... From jessica@githost:simplegit * [new branch] featurebee -> origin/featurebee Fetch 해온브랜치를 git merge 명령으로 Merge 한다 : $ git merge origin/featurebee Auto-merging lib/simplegit.rb Merge made by recursive. lib/simplegit.rb 4 ++++ 1 files changed, 4 insertions(+), 0 deletions(-) Push 하려고하는데작은문제가생겼다. Jessica 씨는 featureb 브랜치에서작업을했는데서버에 는브랜치가 featurebee 라는이름으로되어있다. 그래서 git push 명령으로 Push 할때로컬브랜치 featureb 뒤에콜론 (:) 과함께서버브랜치이름을직접지정해준다 : $ git push origin featureb:featurebee... To jessica@githost:simplegit.git fba9af8..cd685d1 featureb -> featurebee 이것은 refspec 이란것을사용하는것인데 9 장에서자세하게설명한다. John 씨가몇가지작업을하고나서 featurea 에 Push 했고확인해달라는내용의이메일을보내왔다. Jessica 씨는 git fetch 로 Push 한작업을 Fetch 한다 : $ git fetch origin... From jessica@githost:simplegit 3300904..aad881d featurea -> origin/featurea 어떤것이업데이트됐는지 git log 명령으로확인한다 : $ git log origin/featurea ^featurea commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6 Author: John Smith <jsmith@example.com> Date: Fri May 29 19:57:33 2009-0700 changed log output to 30 from 25 115

5 장분산환경에서의 Git Scott Chacon Pro Git 확인을마치면로컬의 featurea 브랜치로 Merge 한다 : $ git checkout featurea Switched to branch "featurea" $ git merge origin/featurea Updating 3300904..aad881d Fast forward lib/simplegit.rb 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-) Jessica 씨는일부수정하고, 수정한내용을다시서버로 Push 한다 : $ git commit -am 'small tweak' [featurea 774b3ed] small tweak 1 files changed, 1 insertions(+), 1 deletions(-) $ git push origin featurea... To jessica@githost:simplegit.git 3300904..774b3ed featurea -> featurea 위와같은작업을마치고나면 Jessica 씨의저장소는그림 5-13 과같은모습이된다. 그림 5.13: 마지막 Push 하고난후의 Jessica 씨의저장소 그럼 featurea와 featurebee 브랜치가프로젝트의메인브랜치로 Merge할준비가되었다고 Integration-Manager에게알려준다. Integration-Manager가두브랜치를모두 Merge하고난후에메인브랜치를 Fetch하면그림 5-14와같은모양이된다. 수많은팀의작업을동시에진행하고나중에 Merge하는기능을사용하려고다른버전관리시스템에서 Git으로바꾸는조직들이많아지고있다. 팀은자신의브랜치로작업하지만, 메인브랜치에영향을끼치지않는다는점이 Git의장점이다. 그림 5-15는이런 Workflow을나타내고있다. 116

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 그림 5.14: 두브랜치가메인브랜치에 Merge 된후의저장소 그림 5.15: 대규모팀의 Workflow 5.2.4 공개소규모팀비공개팀을운영하는것과공개팀을운영하는것은약간다르다. 공개팀을운영할때에는모든개발자가프로젝트의공유저장소에직접적으로쓰기권한을가지지는않는다. 그래서프로젝트의관리자는몇가지일을더해줘야한다. Fork를지원하는 Git 호스팅에서 Fork를통해프로젝트에기여하는법을예제를통해살펴본다. repo.or.cz나 Github 같은 Git 호스팅사이트는 Fork 기능을지원하며프로젝트관리자는보통 Fork하는것으로프로젝트를운영한다. 다른방식으로이메일과 Patch를사용하는방식도있는데뒤이어살펴본다. 우선처음할일은메인저장소를 Clone 하는것이다. 그리고나서토픽브랜치를만들고일정부분기여한다. 그순서는아래와같다 : $ git clone (url) $ cd project 117

5 장분산환경에서의 Git Scott Chacon Pro Git $ git checkout -b featurea $ (work) $ git commit $ (work) $ git commit rebase -i 명령을사용하면여러커밋을하나의커밋으로합치거나프로젝트의관리자가수정사항을쉽게이해하도록커밋을정리할수있다. 6장에서대화식으로 Rebase하는방법을살펴본다. 일단프로젝트의웹사이트로가서 Fork 버튼을누르면원래프로젝트저장소에서갈라져나온, 쓰기권한이있는저장소가하나만들어진다. 그러면로컬에서수정한커밋을외부에있는그저장소에 Push 할수있다. 그저장소를로컬저장소의리모트저장소로등록한다. 예를들어 myfork로등록한다 : $ git remote add myfork (url) 자이제등록한리모트저장소에 Push한다. 작업하던것을로컬저장소의 master 브랜치에 Merge 한후 Push하는것보다리모트브랜치에바로 Push를하는방식이훨씬간단하다. 이렇게하는이유는관리자가토픽브랜치를프로젝트에포함시키고싶지않을때토픽브랜치를 Merge하기이전상태로 master 브랜치를되돌릴필요가없기때문이다. 관리자가토픽브랜치를 Merge하든 Rebase하든 cherry-pick하든지간에결국다시관리자의저장소를 Pull할때에는토픽브랜치의내용이들어있을것이다 : $ git push myfork featurea Fork한저장소에 Push하고나면프로젝트관리자에게이내용을알려야한다. 이것을 Pull Request 라고한다. git 호스팅사이트에서관리자에게보낼메시지를생성하거나 git request-pull 명령으로이메일을수동으로만들수있다. GitHub의 pull request 버튼은자동으로메시지를만들어준다. request-pull 명령은아규먼트를두개입력받는다. 첫번째아규먼트는작업한토픽브랜치의 Base 브랜치이다. 두번째는토픽브랜치가위치한저장소 URL인데위에서등록한리모트저장소이름을적을수있다. 이명령은토픽브랜치수정사항을요약한내용을결과로보여준다. 예를들어 Jessica씨가 John씨에게 Pull 요청을보내는상황을살펴보자. Jessica씨는토픽브랜치에두번커밋을하고 Fork 한저장소에 Push했다. 그리고아래와같이실행한다 : $ git request-pull origin/master myfork The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40: John Smith (1): added a new function are available in the git repository at: 118

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 git://githost/simplegit.git featurea Jessica Smith (2): add limit to log function change log output to 30 from 25 lib/simplegit.rb 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-) 관리자에게이내용을보낸다. 이내용에는토픽브랜치가어느시점에갈라져나온것인지, 어떤커밋이있는지, Pull하려면어떤저장소에접근해야하는지에대한내용이들어있다. 프로젝트관리자가아니라고해도보통 origin/master를추적하는 master 브랜치는가지고있다. 그래도토픽브랜치를만들고일을하면관리자가수정내용을거부할때쉽게버릴수있다. 토픽별로브랜치를분리해서일을하면그동안주저장소의 master 브랜치가수정됐기때문에깨끗한커밋을남길수없을수도있지만 Rebase로깨끗하게 Merge할수있다. 그리고토픽브랜치를새로만들때앞서 Push한토픽브랜치에서시작하지말고주저장소의 master 브랜치로부터만들어야한다 : $ git checkout -b featureb origin/master $ (work) $ git commit $ git push myfork featureb $ (email maintainer) $ git fetch origin 그림 5-16 처럼각토픽은일종의실험실이라고할수있다. 각토픽은서로방해하지않고독립적으로 수정하고 Rebase 할수있다 : 그림 5.16: featureb 수정작업이끝난직후저장소의모습 프로젝트관리자가사람들의수정사항을 Merge 하고나서 Jessica 씨의브랜치를 Merge 하려고할때 충돌이날수도있다. 그러면 Jessica 씨가자신의브랜치를 origin/master 에 Rebase 해서충돌을해 결하고다시 Pull Request 을보낸다 : 119

5 장분산환경에서의 Git Scott Chacon Pro Git $ git checkout featurea $ git rebase origin/master $ git push -f myfork featurea 위명령들을실행하고나면그림 5-17 과같아진다. 그림 5.17: FeatureA 에대한 Rebase 가적용된후의모습 브랜치를 Rebase해버렸기때문에 Push할때 -f 옵션을주고강제로기존에서버에있던브랜치의내용을덮어써야한다. 아니면새로운브랜치를 ( 예를들어 featureav2) 서버에 Push해도된다. 또다른시나리오를하나더살펴보자. 프로젝트관리자는 featureb 브랜치의내용은좋지만, 상세구현은다르게하고싶다. 관리자는 featureb 담당자에게상세구현을다르게해달라고요청한다. featureb 담당자는하는김에 featureb 브랜치를프로젝트의최신 master 브랜치기반으로옮긴다. 먼저 origin/master 브랜치에서 featurebv2 브랜치를새로하나만들고, featureb의커밋들을모두 Squash해서 Merge하고, 만약충돌이나면해결하고, 상세구현을수정하고, 새브랜치를 Push한다 : $ git checkout -b featurebv2 origin/master $ git merge --no-commit --squash featureb $ (change implementation) $ git commit $ git push myfork featurebv2 squash 옵션은현재브랜치에 Merge할때해당브랜치의커밋을모두하나의커밋으로합쳐서 Merge한다. no-commit 옵션을주면 Git은 Merge하고나서자동으로커밋하지않는다. 다른브랜치의수정사항을통째로새로운브랜치에 Merge하고나서좀더수정하고커밋하나를새로만든다. 수정을마치면관리자에게 featurebv2 브랜치를확인해보라고메시지를보낸다 ( 그림 5-18 참고 ). 5.2.5 대규모공개프로젝트대규모프로젝트은보통수정사항이나 Patch를수용하는자신만의규칙을마련해놓고있다. 프로젝트마다규칙은서로다를수있으므로각프로젝트의규칙을미리알아둘필요가있다. 대규모프로젝트는대부분메일링리스트를통해서 Patch를받아들이는데예제를통해살펴본다. 토픽브랜치를만들어수정하는작업은앞서살펴본바와거의비슷하지만, Patch를제출하는방식이다르다. 프로젝트를 Fork 하여 Push하는것이아니라커밋내용을메일로만들어개발자메일링리스트에제출한다 : 120

Scott Chacon Pro Git 5.2 절프로젝트에기여하기 그림 5.18: featurebv2 브랜치를커밋한이후저장소모습 $ git checkout -b topica $ (work) $ git commit $ (work) $ git commit 커밋을두번하고메일링리스트에보내보자. git format-patch 명령으로메일링리스트에보낼 mbox 형식의파일을생성한다. 각커밋은하나씩메일메시지로생성되는데커밋메시지의첫번째줄이제목이되고 Merge 메시지내용과 Patch 자체가메일메시지의본문이된다. 이방식은수신한이메일에들어있는 Patch를바로적용할수있어서좋다. 메일속에는커밋의모든내용이포함된다. 메일에포함된 Patch를적용하는것은다음절에서살펴본다. $ git format-patch -M origin/master 0001-add-limit-to-log-function.patch 0002-changed-log-output-to-30-from-25.patch format-patch 명령을실행하면생성한파일이름을보여준다. -M 옵션은이름이변경된파일이있 는지살펴보라는옵션이다. 각파일의내용은아래와같다 : $ cat 0001-add-limit-to-log-function.patch From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <jessica@example.com> Date: Sun, 6 Apr 2008 10:17:23-0700 Subject: [PATCH 1/2] add limit to log function Limit log functionality to the first 20 --- lib/simplegit.rb 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) 121

5 장분산환경에서의 Git Scott Chacon Pro Git diff --git a/lib/simplegit.rb b/lib/simplegit.rb index 76f47bc..f9815f1 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -14,7 +14,7 @@ class SimpleGit end def log(treeish = 'master') - command("git log #{treeish}") + command("git log -n 20 #{treeish}") end -- def ls_tree(treeish = 'master') 1.6.2.rc1.20.g8c5b.dirty 메일링리스트에메일을보내기전에각 Patch 메일파일의내용을손으로고칠수있다. --- 줄과 Patch 가시작되는줄 (lib/simplegit.rb로시작하는줄 ) 사이에내용을추가하면개발자는읽을수있지만, 나중에 Patch에적용되지는않는다. 특정메일프로그램을사용하거나이메일을보내는명령어로메일링리스트에보낼수있다. 붙여넣기로위의내용이그대로들어가지않는메일프로그램도있다. 사용자편의를위해공백이나줄바꿈문자등을넣어주는메일프로그램은원본그대로들어가지않는다. 다행히 Git에는 Patch 메일을그대로보낼수있는도구가있다. IMAP 프로토콜로보낸다. 저자가사용하는방법으로 Gmail을사용하여 Patch 메일을전송하는방법을살펴보자. 추가로 Git 프로젝트의 Documentation/SubmittingPatches 문서의마지막부분을살펴보면다양한메일프로그램으로메일을보내는방법을설명한다. 메일을보내려면먼저 ~/.gitconfig 파일에서이메일부분설정한다. git config 명령으로추가할수도있고직접파일을열어서추가할수도있다. 아무튼, 아래와같이설정을한다 : [imap] folder = "[Gmail]/Drafts" host = imaps://imap.gmail.com user = user@gmail.com pass = p4ssw0rd port = 993 sslverify = false IMAP 서버가 SSL 을사용하지않으면마지막두줄은필요없고 host 에서 imaps:// 대신 imap:// 로한 다. 이렇게설정하면 git send-email 명령으로메일을전송할수있다 : $ git send-email *.patch 0001-added-limit-to-log-function.patch 122

Scott Chacon Pro Git 5.3 절프로젝트운영하기 0002-changed-log-output-to-30-from-25.patch Who should the emails appear to be from? [Jessica Smith <jessica@example.com>] Email s will be sent from: Jessica Smith <jessica@example.com> Who should the emails be sent to? jessica@example.com Message-ID to be used as In-Reply-To for the first email? y Git 으로메일을보내면아래와같은로그메시지가출력된다 : (mbox) Adding cc: Jessica Smith <jessica@example.com> from \line 'From: Jessica Smith <jessica@example.com>' OK. Log says: Sendmail: /usr/sbin/sendmail -i jessica@example.com From: Jessica Smith <jessica@example.com> To: jessica@example.com Subject: [PATCH 1/2] added limit to log function Date: Sat, 30 May 2009 13:29:15-0700 Message-Id: <1243715356-61726-1-git-send-email-jessica@example.com> X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty In-Reply-To: <y> References: <y> Result: OK 이후 Gmail 의 Draft 폴더로가서 To 부분을메일링리스트의주소로변경하고 CC 부분에해당메일을 참고해야하는관리자나개발자의메일주소를적고실제로전송한다. 5.2.6 요약이번절에서는다양한 Workflow에따라 Git을어떻게사용하는지살펴보고그에필요한도구들을설명했다. 다음절에서는동전의뒷면인프로젝트를운영하는방법에대하여살펴본다. 즉친절한 Dictator나 Integration-Manager가되어보는것이다. 5.3 프로젝트운영하기 효율적으로기여하는방법뿐만아니라효율적으로운영하는방법도알아야한다. 언젠가는단순히프로젝트에기여하는것이아니라프로젝트를직접운영해야할수도있다. 프로젝트를운영하는것은크게두가지로이루어진다. 하나는 format-patch 명령으로생성한 Patch를이메일로받아서프로젝트에 Patch를적용하는것이다. 다른하나는프로젝트의다른리모트저장소로부터변경내용을 Merge 하는것이다. 저장소를아주깔끔하고정돈된상태로운영하고 Patch를적용하거나수정사항을확인하기쉬운상태를유지하려면좋은운영방식을터득해야한다. 좋은운영방식은다른사람들이이해하기쉽고프로젝트가오랫동안운영돼도흐트러짐이없어야한다. 123

5 장분산환경에서의 Git Scott Chacon Pro Git 5.3.1 토픽브랜치에서일하기메인브랜치에통합하기전에임시로토픽브랜치를하나만들고거기에통합해보고나서다시메인브랜치에통합하는것이좋다. 이렇게하면 Patch를적용할때이리저리수정해보기도하고좀더고민해봐야하면 Patch를적용해둔채로나중으로미룰수도있다. 무슨 Patch인지브랜치이름에간단히적어주면다른작업을하다가나중에이브랜치로돌아왔을때기억해내기훨씬수월하다. 프로젝트의관리자라면이런토픽브랜치의이름을잘지어야한다. 예를들어 sc라는사람이작업한 Patch라면 sc/ ruby_client 처럼앞에닉네임을붙여서브랜치를만들수있다. master 브랜치에서새토픽브랜치를아래와같이만든다 : $ git branch sc/ruby_client master checkout -b 명령으로브랜치를만들고 Checkout 까지한번에할수있다 : $ git checkout -b sc/ruby_client master 이렇게토픽브랜치를만들고 Patch 를적용해보고적용한내용을다시 Long-Running 브랜치로 Merge 한다. 5.3.2 이메일로받은 Patch 를적용하기 이메일로받은 Patch 를프로젝트에적용하기전에우선토픽브랜치에 Patch 를적용해본다. Patch 를적용하는방법은 git apply 명령을사용하는것과 git am 명령을사용하는것두가지가있다. apply 명령을사용하는방법 git diff 나 Unix 의 diff 명령으로만든 Patch 파일을적용할때에는 git apply 명령을사용한다. Patch 파일이 /tmp/patch-ruby-client.patch 라고하면아래와같은명령으로 Patch 를적용할수있다 : $ git apply /tmp/patch-ruby-client.patch 위명령을실행하면 Patch 파일내용에따라현재디렉토리의파일들을변경한다. 위명령은 patch -p1 명령과거의같다. 하지만, 이명령이 patch 명령보다훨씬더꼼꼼하게비교한다. git diff로생성한 Patch 파일에파일을추가하거나, 파일을삭제하고, 파일의이름을변경하는내용이들어있으면그대로적용된다. 이런것은 patch 명령으로할수없다. 그리고 git apply는 모두적용, 아니면모두취소 모델을사용하기때문에 Patch를적용하는데실패하면 Patch를적용하기이전상태로전부되돌려놓는다. Patch 명령은여러파일에적용하다가중간에실패하면거기서그대로중단하기때문에깔끔하지못하다. git apply는 Patch보다훨씬결벽증적이다. 이명령은자동으로커밋해주지않기때문에변경된파일을직접 Staging Area에추가하고커밋해야한다. 실제로 Patch를적용하기전에 Patch가잘적용되는지한번시험해보려면 git apply --check 명령을사용한다 : 124

Scott Chacon Pro Git 5.3 절프로젝트운영하기 $ git apply --check 0001-seeing-if-this-helps-the-gem.patch error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply 화면에아무런내용도뜨지않으면 Patch 가깔끔하게적용된다는것이다. 이명령은 Patch 를적용해 보고에러가발생하면 0 이아닌값을반환하기때문에쉘스크립트에서도사용할수있다. am 명령을사용하는방법프로젝트기여자가 Git의 format-patch 명령을잘사용하면관리자의작업은훨씬쉬워진다. format-patch 명령으로만든 Patch 파일은기여자의정보와커밋정보가포함되어있기때문이다. 그래서기여자가 diff보다 format-patch를사용하도록권해야한다. git apply는기존의 Patch파일에만사용한다. format-patch 명령으로생성한 Patch 파일은 git am 명령으로적용한다. git am은메일여러통이들어있는 mbox파일을읽어서 Patch한다. mbox파일은간단한텍스트파일이고그내용은아래와같다 : From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <jessica@example.com> Date: Sun, 6 Apr 2008 10:17:23-0700 Subject: [PATCH 1/2] add limit to log function Limit log functionality to the first 20 이내용은 format-patch 명령으로생성한파일의앞부분이다. 이파일은 mbox 형식이다. 받은메일이 git send-email로만든메일이라면 mbox 형식으로저장하고이 mbox 파일을 git am 명령으로적용한다. 사용하는메일클라이언트가여러메일을하나의 mbox 파일로저장할수있다면메일여러개를한번에 Patch할수있다. 이메일로받은것이아니라이슈트래킹시스템같은데올라온파일이라면먼저내려받고서 git am 명령으로 Patch한다 : $ git am 0001-limit-log-function.patch Applying: add limit to log function Patch 가성공하면자동으로새로운커밋이하나만들어진다. Patch 파일에들어있는기여자의이메 일, 작성시간, 커밋메시지를뽑아서커밋에함께저장한다. 예를들어위의 mbox 예제파일을적용해 서생성되는커밋은아래와같다 : $ git log --pretty=fuller -1 commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 125

5 장분산환경에서의 Git Scott Chacon Pro Git Author: Jessica Smith <jessica@example.com> AuthorDate: Sun Apr 6 10:17:23 2008-0700 Commit: Scott Chacon <schacon@gmail.com> CommitDate: Thu Apr 9 09:19:06 2009-0700 add limit to log function Limit log functionality to the first 20 커밋정보는누가언제 Patch했는지알려준다. Author 정보는실제로누가언제 Patch파일을만들었는지알려준다. Patch에실패할수도있다. 보통 Patch가생성된시점보다해당브랜치가너무업데이트됐을때나아직적용되지않은다른 Patch가필요한경우에일어난다. 이러면 git am 명령은 Patch를중단하고사용자에게어떻게처리할지물어온다 : $ git am 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply Patch failed at 0001. When you have resolved this problem run "git am --resolved". ( 충돌을해결하면 "git am --resolved" 입력 ) If you would prefer to skip this patch, instead run "git am --skip". (Patch 적용을생략하려면 "git am --skip" 입력 ) To restore the original branch and stop patching run "git am --abort". (Patch 적용을중단하려면 "git am --abort" 입력 ) 성공적으로 Patch하지못하면 git은 Merge나 Rebase의경우처럼문제를일으킨파일에충돌표시를해놓는다. Merge나 Rebase할때충돌을해결하는것처럼 Patch의충돌도해결할수있다. 충돌한파일을열어서충돌부분을수정하고나서 Staging Area에추가하고 git am --resolved 명령을입력한다 : $ (fix the file) $ git add ticgit.gemspec $ git am --resolved Applying: seeing if this helps the gem 충돌이났을때 Git에게좀더머리를써서 Patch를적용하도록하려면 -3 옵션을사용한다. 이옵션은 Git에게 3-way Patch를적용해보라고하는것이다. Patch가어느시점에서갈라져나온것인지알수없기때문에이옵션은기본적으로비활성화돼있다. 하지만, 같은프로젝트의커밋이라면기본옵션보다훨씬똑똑하게충돌상황을해결한다. 126

Scott Chacon Pro Git 5.3 절프로젝트운영하기 $ git am -3 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... No changes -- Patch already applied. 위의경우는이미 Patch한것을다시 Patch하는상황이다. -3 옵션이없었으면충돌을알아서해결하지못했을것이다. 하나의 mbox 파일에들어있는여러 Patch를적용할때대화형방식을사용할수있다. 이방식을사용하면 Patch를적용하기전에 Patch를적용할때마다묻는다 : $ git am -3 -i mbox Commit Body is: -------------------------- seeing if this helps the gem -------------------------- Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all 이옵션은 Patch를여러개적용할때유용하다. 적용하려는 Patch의내용을미리꼭기억해두지않아도되고적용하기전에이미적용된 Patch인지알수있다. 모든 Patch를토픽브랜치에적용하고커밋까지마치면 Long-Running 브랜치에어떻게통합할지를결정해야한다. 5.3.3 리모트브랜치로부터통합하기프로젝트기여자가자신의저장소를만들고커밋을몇번하고저장소의 URL과변경내용을메일로보내왔다면 URL을리모트저장소로등록하고 Merge할수있다. 예를들어 Jessica씨는 ruby-client 브랜치에엄청난기능을만들어놨다고메일을보내왔다. 이리모트브랜치를등록하고 Checkout해서테스트한다 : $ git remote add jessica git://github.com/jessica/myproject.git $ git fetch jessica $ git checkout -b rubyclient jessica/ruby-client 후에 Jessiaca씨가이메일로또다른엄청난기능을개발한브랜치를보내오면이미저장소를등록했기때문에간단히 Fetch하고 Checkout할수있다. 다른개발자들과함께지속적으로개발할때는이방식이가장사용하기좋다. 물론기여하는사람이간단한 Patch를이따금씩만만들어내면이메일로 Patch 파일을받는것이낫다. 기여자가저장소서버를만들어커밋하고관리자가리모트저장소로등록해서 Patch를합치는작업보다시간과노력이덜 127

5 장분산환경에서의 Git Scott Chacon Pro Git 든다. 물론 Patch 한두개를보내는사람들까지도모두리모트저장소로등록해서사용해도된다. 스크립트나호스팅서비스를사용하면좀더쉽게관리할수있다. 어쨌든어떤방식이좋을지는우리가어떻게개발하고어떻게기여할지에달렸다. 리모트저장소로등록하면커밋의히스토리도알수있다. Merge할때어디서부터커밋이갈라졌는지알수있기때문에 -3 옵션을주지않아도자동으로 3-way Merge가적용된다. 리모트저장소로등록하지않고도 Merge할수있다. 계속함께일할개발자가아닐때사용하면좋다. 아래는리모트저장소로등록하지않고 URL을직접사용하여 Merge를하는예이다 : $ git pull git://github.com/onetimeguy/project.git From git://github.com/onetimeguy/project * branch HEAD -> FETCH_HEAD Merge made by recursive. 5.3.4 무슨내용인지확인하기기여물이포함된토픽브랜치가있으니이제그기여물을 Merge할지말지결정야한다. 이번절에서는메인브랜치에 Merge할때필요한명령어를살펴본다. 주로토픽브랜치를검토하는데필요한명령이다. 먼저지금작업하는브랜치에서 master 브랜치에속하지않는커밋만살펴보는것이좋다. not 옵션으로히스토리에서 master 브랜치에속한커밋은제외하고살펴본다. 예를들어 contrib 브랜치에 Patch를두개 Merge했으면아래와같은명령어로그결과를살펴볼수있다 : $ git log contrib --not master commit 5b6235bd297351589efc4d73316f0a68d484f118 Author: Scott Chacon <schacon@gmail.com> Date: Fri Oct 24 09:53:59 2008-0700 seeing if this helps the gem commit 7482e0d16d04bea79d0dba8988cc78df655f16a0 Author: Scott Chacon <schacon@gmail.com> Date: Mon Oct 22 19:38:36 2008-0700 updated the gemspec to hopefully work better git log 명령에 -p 옵션을주면각커밋에서실제로무슨내용이변경됐는지살펴볼수있다. 이옵션은각 commit의뒤에 diff의내용을출력해준다. 토픽브랜치를다른브랜치에 Merge하기전에어떤부분이변경될지미리살펴볼수있다. 이때는색다른명령을사용해야한다. 물론아래와같은명령을사용할수도있다 : $ git diff master 128

Scott Chacon Pro Git 5.3 절프로젝트운영하기 이명령은 diff 내용을보여주긴하지만잘못된것을보여줄수도있다. 토픽브랜치에서작업하는동안 master 브랜치에새로운커밋이추가될수도있다. 그래서기대하는 diff 결과가아닐수있다. 지금이명령은각브랜치의마지막스냅샷을비교한다. master 브랜치에한줄을추가되면토픽브랜치에서한줄삭제한것으로보여준다. master 브랜치가가리키는커밋이토픽브랜치의조상이라면아무문제없다. 하지만, 그렇지않은경우라면이 diff 도구는토픽브랜치에만있는내용은추가하는것이고 master 브랜치에만있는내용은삭제하는것으로간주한다. 정말보고싶은것은토픽브랜치에추가한것이고결국에는이것을 master 브랜치에추가하려는것이다. 그러니까 master 브랜치와토픽브랜치의공통조상인커밋을찾아서토픽브랜치가현재가리키는커밋과비교해야한다. 아래와같은명령으로공통조상인커밋을찾고이조상커밋에서변경된내용을살펴본다 : $ git merge-base contrib master 36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 $ git diff 36c7db 이방법으로원하는결과를얻을수있지만, 사용법이불편하다. Git 은 Triple-Dot 으로간단하게위와 같이비교하는방법을지원한다. diff 명령을사용할때두브랜치사이에 를쓰면, 두브랜치의공통 조상과브랜치의마지막커밋을비교한다 : $ git diff master...contrib 이명령은 master 브랜치로부터현재토픽브랜치의다른것들만보여주기때문에기억해두면매우유 용하게사용할수있을것이다. 5.3.5 기여물통합하기기여물을토픽브랜치에다적용하고 Long-Running 브랜치나 master 브랜치로통합할준비가되었다면이제어떻게해야할까? 프로젝트를운영하는데쓰는작업방식은어떤것이있을까? 앞으로그예제몇가지를살펴본다. Merge Workflow 바로 master 브랜치에 Merge하는것이가장간단하다. 이 Workflow에서는 master 브랜치가안전한코드라고가정한다. 토픽브랜치를검증하고 master 브랜치로 Merge할때마다토픽브랜치를삭제한다. 그림 5-19처럼 ruby_client 브랜치와 php_client 브랜치가있을때 ruby_client 브랜치를 master 브랜치로 Merge한후 php_client 브랜치를 Merge하면그림 5-20과같아진다 : 이 Workflow은간단하지만, 프로젝트의규모가커지면문제가생길수있다. 개발자가많고규모가큰프로젝트에서는두단계로 Merge하는것이좋다. 그래서 Long-Running 브랜치를두개로유지해야한다. master 브랜치는아주안정적인버전을릴리즈하기위해서사용한다. develop 브랜치는새로수정된코드를통합할때사용한다. 그리고두브랜치를모두저장소에 Push한다. 우선 develop 브랜치에토픽브랜치 ( 그림 5-21) 를그림 5-22과같이 Merge한다. 그후에릴리즈해도될만한수준이되면 master 브랜치를 develop 브랜치까지 Fast-forward시킨다 ( 그림 5-23). 129

5 장분산환경에서의 Git Scott Chacon Pro Git 그림 5.19: 저장소의두브랜치 그림 5.20: Merge 한후의저장소 이 Workflow을사용하면프로젝트저장소를 Clone하고나서개발자가안정버전이필요하면 master 브랜치를빌드하고안정적이지않더라도좀더최신버전이필요하면 develop 브랜치를 Checkout하여빌드한다. 이개념을좀더확장해서사용할수있다. 토픽브랜치를검증하기위한 integrate 브랜치를만들어 Merge하고토픽브랜치가검증되면 develop 브랜치에머지한다. 그리고 develop 브랜치에서충분히안정하다는것이증명되면그때 master 브랜치에 Merge한다. 대규모 Merge Workflow Git을개발하는프로젝트는 Long-Running의브랜치를 4개운영한다. 각브랜치이름은 master, next, pu (Proposed Updates), maint 이다. maint는마지막으로릴리즈한버전을지원하는브랜치다. 기여자가새로운기능을제안하면관리자는그림 5-24처럼자신의저장소에토픽브랜치를만들어관리한다. 그리고토픽에부족한점은없는지, 안정적인지계속테스트한다. 안정화되면 next로 130

Scott Chacon Pro Git 5.3 절프로젝트운영하기 그림 5.21: 토픽브랜치를 Merge 하기전 그림 5.22: 토픽브랜치를 Merge 한후 그림 5.23: 토픽브랜치를릴리즈한후 Merge하고저장소에 Push한다. 그러면모두가잘통합됐는지확인할수있다. 토픽브랜치가좀더개선돼야하면 next가아니라 pu에 Merge한다. 그후에충분히검증을마치면 pu에서 next로옮기고 next를기반으로 pu를다시만든다. next에는아직 master에넣기에모자라보이는것들이들어있다. 즉 next 브랜치는정말가끔 Rebase하고 pu는자주 Rebase하지만 master 는항상 Fast-forward한다 ( 그림 5-25). 토픽브랜치가결국 master 브랜치로 Merge되면저장소에서삭제한다. 그리고이전릴리즈버전에 Patch가필요하면 maint 브랜치를이용해대응한다. Git을개발하는프로젝트를 Clone하면브랜치가 4개있고각브랜치를이용하여진행사항을확인해볼수있다. 그래서새로운기능을추가하려면적당 131

5 장분산환경에서의 Git Scott Chacon Pro Git 그림 5.24: 토픽브랜치를동시에여러개관리하는것은복잡하다 그림 5.25: 토픽브랜치를 Long-Running 브랜치로 Merge 하기 한브랜치를보고고른다. 이 Workflow 는잘구조화돼있어서코드가새로추가돼도테스트하기쉽다. Rebase와 Cherry-Pick Workflow 히스토리를평평하게관리하려고 Merge보다 Rebase나 Cherry-Pick을더선호하는관리자들도있다. 토픽브랜치에서작업을마친후 master에통합할때 master 브랜치를기반으로 Rebase한다. 그러면커밋이다시만들어진다. master 대신 develop 등의브랜치에도가능하다. 문제가없으면 master 브랜치를 Fast-forward시킨다. 이렇게평평한히스토리를유지할수있다. 한브랜치에서다른브랜치로작업한내용을옮기는또다른방식으로 Cherry-pick이란것도있다. Git의 Cherry-pick은커밋하나만 Rebase하는것이다. 커밋하나로 Patch 내용을만들어현재브랜치에적용을하는것이다. 토픽브랜치에있는커밋중에서하나만고르거나토픽브랜치에커밋이하나밖에없을때 Rebase보다유용하다. 그림 5-26의예를들어보자. e43a6 커밋하나만현재브랜치에적용하려면아래와같은명령을실행한다 : $ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf Finished one cherry-pick. [master]: created a0a41a9: "More friendly message when locking the index fails." 3 files changed, 17 insertions(+), 3 deletions(-) 위명령을실행하면 e43a6 커밋에서변경된내용을현재브랜치에똑같이적용을한다. 하지만, 변경 132

Scott Chacon Pro Git 5.3 절프로젝트운영하기 그림 5.26: Cherry-pick 을실행하기전의저장소 을적용한시점이다르므로새커밋의 SHA-1 해시값은달라진다. 명령을실행하고나면그림 5-27 과 같이될것이다. 그림 5.27: Cherry-pick 방식으로커밋하나를적용한후의저장소 Rebase 나 Cherry-pick 방식으로토픽브랜치를합치고나면필요없는토픽브랜치나커밋은삭제한 다. 5.3.6 릴리즈버전에태그달기 적당한때가되면릴리즈해야한다. 그리고언제든지그시점으로되돌릴수있게태그를다는것이좋 다. 2 장에서살펴본대로태그를달면된다. 서명된태그를달면아래와같이출력된다 : $ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Scott Chacon <schacon@gmail.com>" 1024-bit DSA key, ID F721C45A, created 2009-02-09 태그에서명하면서명에사용한 PGP 공개키도배포해야한다. Git 개발프로젝트는관리자의 PGP 공개키를 Blob 형식으로 Git 저장소에함께배포한다. 이 Blob파일을사용하여태그에서명했다. 아래와같은명령으로어떤 PGP 공개키를포함할지확인한다 : 133

5 장분산환경에서의 Git Scott Chacon Pro Git $ gpg --list-keys /Users/schacon/.gnupg/pubring.gpg --------------------------------- pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09] uid Scott Chacon <schacon@gmail.com> sub 2048g/45D02282 2009-02-09 [expires: 2010-02-09] git hash-object 라는명령으로공개키를바로 Git 저장소에넣을수있다. 이명령은 Git 저장소안에 Blob 형식으로공개키를저장해주고그 Blob 의 SHA-1 값을알려준다 : $ gpg -a --export F721C45A git hash-object -w --stdin 659ef797d181633c87ec71ac3f9ba29fe5775b92 이 SHA-1 해시값으로 PGP 공개키를가리키는태그를만든다 : $ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92 git push --tags 명령으로앞서만든 maintainer-pgp-pub 태그를공유한다. 다른사람이태그의서 명을확인하려면우선 Git 저장소에저장된 PGP 공개키를꺼내서 GPG 키데이터베이스에저장해야한 다 : $ git show maintainer-pgp-pub gpg --import 사람들은이렇게공개키를얻어서서명된태그를확인한다. 또한, 관리자가태그메시지에서명을확인 하는방법을적어놓으면좋다. git show <tag> 으로어떻게서명된태그를확인하는지설명한다. 5.3.7 빌드넘버만들기 Git은 v123 처럼숫자형태로커밋이름을만들지않기때문에사람이기억하기어렵다. 하지만 git describe 명령으로좀더사람이기억하기쉬운이름을얻을수있다. Git은가장가까운태그의이름과, 태그에서얼마나더커밋이쌓였는지, 그리고해당커밋의 SHA-1 값을조금가져다가이름을만든다 : $ git describe master v1.6.2-rc1-20-g8c5b85c 이렇게사람이읽을수있는이름으로스냅샷이나빌드를만든다. 만약저장소에서 Clone한후소스코드로 Git을설치하면 git --version 명령은이렇게생긴빌드넘버를보여준다. 태그가달린커밋에 git describe명령을사용하면다른정보없이태그이름만사용한다. 134

Scott Chacon Pro Git 5.3 절프로젝트운영하기 git describe 명령은 -a나 -s 옵션을주고만든 Annotated 태그가필요하다. 릴리즈태그는 git describe 명령으로만드니까꼭이름이적당한지사전에확인해야한다. 그리고이값은 Checkout이나 Show 명령에도사용할수있지만, 전적으로이름뒤에붙은 SHA-1 값을사용한다. 그래서이값으로는해당커밋을못찾을수도있다. 최근 Linux Kernel은충돌때문에축약된 SHA-1를 8자에서 10자로늘렸다. 이제는 8자일때생성한값은사용할수없다. 5.3.8 릴리즈준비하기 먼저 Git 을사용하지않는사람을위해소스코드스냅샷을압축한다. 쉽게압축할수있도록 Git 은 git archive 명령을지원한다 : $ git archive master --prefix='project/' gzip > `git describe master`.tar.gz $ ls *.tar.gz v1.6.2-rc1-20-g8c5b85c.tar.gz 이압축파일을풀면프로젝트의가장마지막스냅샷이나온다. ZIP 형식으로압축파일을만들려면 -- format=zip 옵션을사용한다 : $ git archive master --prefix='project/' --format=zip > `git describe master`.zip 이렇게압축한스냅샷파일은 Website 나이메일로사람들에게배포할수있다. 5.3.9 Shortlog 보기이메일로프로젝트의변경사항을사람들에게알려야할때, git shortlog 명령을사용하면지난릴리즈이후의변경사항목록을쉽게얻어올수있다. git shortlog 명령은주어진범위에있는커밋을요약해준다. 아래는최근릴리즈버전인 v1.0.1 이후의커밋을요약해주는예제이다 : $ git shortlog --no-merges master --not v1.0.1 Chris Wanstrath (8): Add support for annotated tags to Grit::Tag Add packed-refs annotated tag support. Add Grit::Commit#to_patch Update version and History.txt Remove stray `puts` Make ls_tree ignore nils Tom Preston-Werner (4): fix dates in history dynamic version method Version bump to 1.0.2 Regenerated gemspec for version 1.0.2 135

5 장분산환경에서의 Git Scott Chacon Pro Git 이렇게 Author 를기준으로정리한커밋을이메일로전송한다. 5.4 요약 이제 Git 프로젝트에기여하고, 자신의프로젝트를운영하고, 다른사람이기여한내용을통합하는것정도는쉽게할수있을것이다. 일단쓸만한 Git 개발자가된것을축하한다. 다음장에서복잡한상황을다루는방법과강력한도구들을배우고나면 Git 장인이라고불릴수있을것이다. 136

6 장 Git 도구 지금까지일상적으로자주사용하는명령어들과몇가지 Workflow를배웠다. 파일을추적하고커밋하는등의기본적인명령어뿐만아니라 Staging Area가왜좋은지도배웠고가볍게토픽브랜치를만들고 Merge하는방법도다뤘다. 이제는소스코드관리를 Git 저장소로충분히해낼수있을것이다. 이장에서는일상적으로사용하지는않지만위급한상황에서반드시필요한 Git 도구를살펴본다. 6.1 리비전조회하기 리비전하나를조회할수도있고범위를주고여러개를조회할수도있다. 잘쓰진않지만알아두는게 좋다. 6.1.1 리비전하나가리키기 사람은커밋을나타내는 SHA-1 해시값을쉽게기억할수없다. 이절에서는커밋을표현하는방법을 몇가지설명한다. 좀더사람이기억하기쉬운방법들이다. 6.1.2 짧은 SHA-1 해시값의앞몇글자만으로도어떤커밋인지충분히식별할수있다. 중복되지않으면해시값의앞 4 자만사용해도된다. 유일하기만하면짧은 SHA-1 값이라도괜찮다. 먼저 git log 명령으로어떤커밋이있는지조회한다 : $ git log commit 734713bc047d87bf7eac9674765ae793478c50d3 Author: Scott Chacon <schacon@gmail.com> Date: Fri Jan 2 18:32:33 2009-0800 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon <schacon@gmail.com> Date: Thu Dec 11 15:08:43 2008-0800 137

6 장 Git 도구 Scott Chacon Pro Git Merge commit 'phedders/rdocs' commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon <schacon@gmail.com> Date: Thu Dec 11 14:58:32 2008-0800 added some blame and merge stuff git show 명령으로 1c002dd... 로시작하는커밋을조회한다면아래와같이조회할수있다. 다음명령 어는모두같다 ( 단짧은해시값이다른커밋과중복되지않다고가정 ): $ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d git log 명령어에 --abbrev-commit 옵션을추가하면짧은해시값을보여준다. 기본으로 7 자를보여주 고해시값이중복되면더긴해시값을보여준다 : $ git log --abbrev-commit --pretty=oneline ca82a6d changed the version number 085bb3b removed unnecessary test code a11bef0 first commit 보통은 8 자에서 10 자내외로도충분하다. 이정도로도중복되지않는다. 대규모프로젝트인리눅스커 널도커밋을가리키는데해시값 40 자중에서 12 자만사용한다. 6.1.3 SHA-1 해시값에대한단상 Git을쓰는사람중에서가능성이낮긴하지만언젠가 SHA-1 값이중복될까봐걱정하는사람도있다. 정말그렇게되면어떤일이벌어질까? 이미있는 SHA-1 값을 Git 데이터베이스에커밋하면새로운개체라고해도이미커밋한것으로간주된다. 그래서해당 SHA-1 값의커밋을 Checkout하면항상처음에저장한커밋만 Checkout된다. 그러나해시값이중복되는일은일어나기어렵다. SHA-1 값의크기는 20 바이트 (160비트) 이다. 해시값이중복될확률이 50% 가되는데필요한개체의수는 2 80 이다. 이수는 1.2 자 ( 자 는 경 의 억 배 - 10 24) 이다 ( 충돌확률을구하는공식은 p=(n(n-1)/2) * (1/2 160) 이다 ). 즉, 지구에존재하는모래알의수에 1200을곱한수와맞먹는다. 아직도 SHA-1 해시값이중복될까봐걱정하는사람들을위해좀더덧붙이겠다. 지구에서약 6.5억명의인구가개발하고각자매초리눅스커널히스토리전체와 (100만개 ) 맞먹는개체를쏟아내고바로 Push한다고가정하자. 이런상황에서해시값의충돌날확률이 50% 가되기까지는 5년이걸린다. 그냥어느날동료가전부한순간에늑대에게물려죽을확률이훨씬더높다. 138

Scott Chacon Pro Git 6.1 절리비전조회하기 6.1.4 브랜치로가리키기브랜치를사용하는것이커밋을나타내는가장쉬운방법이다. 커밋개체나 SHA-1 값이필요한곳이면브랜치이름을사용할수있다. 만약 topic1 브랜치의최근커밋을보고싶으면아래와같이실행한다. topic1 브랜치가 ca82a6d를가리키고있기때문에두명령의결과는같다 : $ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1 브랜치가가리키는개체의 SHA-1 값에대한궁금증은 rev-parse 이라는 Plumbing 도구가해결해준 다. 9 장에서이도구에대해좀더자세히설명한다. 기본적으로 rev-parse 은저수준명령어이기때문에 평소에는전혀필요하지않지만그래도한번사용해보고어떤결과가나오는지알아두자 : $ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949 6.1.5 RefLog로가리키기 Git은자동으로브랜치와 HEAD가지난몇달동안에가리켰었던커밋을모두기록하는데이로그를 Reflog라고부른다. git reflog를실행하면 Reflog를볼수있다 : $ git reflog 734713b... HEAD@{0}: commit: fixed refs handling, added gc auto, updated d921970... HEAD@{1}: merge phedders/rdocs: Merge made by recursive. 1c002dd... HEAD@{2}: commit: added some blame and merge stuff 1c36188... HEAD@{3}: rebase -i (squash): updating HEAD 95df984... HEAD@{4}: commit: # This is a combination of two commits. 1c36188... HEAD@{5}: rebase -i (squash): updating HEAD 7e05da5... HEAD@{6}: rebase -i (pick): updating HEAD Git 은브랜치가가리키는것이변경될때마다그정보를임시영역에저장한다. 그래서예전에뭘가리 켰었는지확인할수있다. @{n} 규칙을사용하면아래와같이 HEAD 가 5 번전에가리켰던것을알수있 다 : $ git show HEAD@{5} 순서뿐아니라시간도가능하다. 어제날짜의 master 브랜치를보고싶으면아래와같이한다 : 139

6 장 Git 도구 Scott Chacon Pro Git $ git show master@{yesterday} 이명령은어제 master 브랜치가가리키고있던것이무엇인지보여준다. Reflog 에남아있는것만조 회할수있기때문에너무오래된커밋은조회할수없다. git log -g 명령을사용하면 git reflog 결과를 git log 명령과같은형태로볼수있다 : $ git log -g master commit 734713bc047d87bf7eac9674765ae793478c50d3 Reflog: master@{0} (Scott Chacon <schacon@gmail.com>) Reflog message: commit: fixed refs handling, added gc auto, updated Author: Scott Chacon <schacon@gmail.com> Date: Fri Jan 2 18:32:33 2009-0800 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Reflog: master@{1} (Scott Chacon <schacon@gmail.com>) Reflog message: merge phedders/rdocs: Merge made by recursive. Author: Scott Chacon <schacon@gmail.com> Date: Thu Dec 11 15:08:43 2008-0800 Merge commit 'phedders/rdocs' reflog의일은모두로컬의일이기때문에내 reflog가동료의저장소에는있을수없다. 이제막 Clone 한저장소에도아무것도한게없어서 reflog가하나도없다. git show HEAD@{2.months.ago} 같은명령은적어도두달전에 Clone한저장소에서나사용할수있다. 그러니까이명령을 5분전에 Clone한저장소에사용하면아무것도나오지않는다. 6.1.6 계통관계로가리키기 계통관계로도커밋을표현할수있다. 이름끝에 를붙이면 Git 은해당커밋의부모를찾는다. 프로 젝트히스토리가아래와같을때 : $ git log --pretty=format:'%h %s' --graph * 734713b fixed refs handling, added gc auto, updated tests * d921970 Merge commit 'phedders/rdocs' \ * 35cfb2b Some rdoc changes * 1c002dd added some blame and merge stuff / * 1c36188 ignore *.gem * 9b29157 add open3_detach to gemspec file list 140

Scott Chacon Pro Git 6.1 절리비전조회하기 HEAD 는바로 HEAD 의부모 를의미하므로바로이전커밋을보여준다 : $ git show HEAD^ commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon <schacon@gmail.com> Date: Thu Dec 11 15:08:43 2008-0800 Merge commit 'phedders/rdocs' 뒤에숫자도사용할수있다. 예를들어 d921970 2는 d921970의두번째부모 를의미하기에두번째부모가있는 Merge 커밋에만사용할수있다. 첫번째부모는 Merge할때 Checkout했던브랜치를말하고두번째부모는 Merge한대상브랜치를의미한다. $ git show d921970^ commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon <schacon@gmail.com> Date: Thu Dec 11 14:58:32 2008-0800 added some blame and merge stuff $ git show d921970^2 commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548 Author: Paul Hedderly <paul+git@mjr.org> Date: Wed Dec 10 22:22:03 2008 +0000 Some rdoc changes 계통을표현하는방법으로 ~ 라는것도있다. HEAD~ 와 HEAD 는똑같이첫번째부모를가리킨다. 하지 만그뒤에숫자를사용하면달라진다. HEAD~2 는명령을실행할시점의 첫번째부모의첫번째부모, 즉 조부모 를가리킨다. 위의예제에서 HEAD~3 은아래와같다 : $ git show HEAD~3 commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner <tom@mojombo.com> Date: Fri Nov 7 13:47:59 2008-0500 ignore *.gem 141

6 장 Git 도구 Scott Chacon Pro Git 이것은 HEAD 와같은표현이다. 다시말해서첫번째부모의첫번째부모의첫번째부모를말한 다 : $ git show HEAD^^^ commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner <tom@mojombo.com> Date: Fri Nov 7 13:47:59 2008-0500 ignore *.gem 이두표현을같이사용할수도있다. 위의예제에서 HEAD~3 2 를사용하면증조부모의 Merge 커밋의 두번째부모를조회한다. 6.1.7 범위로커밋가리키기커밋을하나씩조회할수도있지만, 범위를주고여러커밋을한꺼번에조회할수도있다. 범위을주고조회하면브랜치를관리할때유용하다. 브랜치가상당히많고 왜아직도주브랜치에 Merge가안된브랜치들은무엇에대한브랜치일까? 라는의문이들면범위를주고어떤브랜치인지쉽게알아볼수있다. Double Dot 범위를표현하는문법으로 Double Dot(..) 을많이쓴다. Double Dot은한쪽에는있고다른쪽에는없는커밋이무엇인지 Git에게물어보는것이다. 예들들어그림 6-1과같은커밋히스토리가있다고가정하자. 그림 6.1: 범위를설명하는데사용할예제 experiment 브랜치의커밋들중에서아직 master 브랜치에 Merge 하지않은것만보고싶으면 master..experiment 라고사용한다. 이표현은 master 에는없지만, experiment 에는있는커밋 을의 미한다. 여기에서는설명을쉽게하고자실제조회결과가아니라그림 6-1 의문자를사용한다 : $ git log master..experiment D C 반대로 experiment 에는없고 master 에만있는커밋이궁금하면브랜치순서를거꾸로사용한다. experiment..master 는 experiment 에는없고 master 에만있는것을알려준다 : 142

Scott Chacon Pro Git 6.1 절리비전조회하기 $ git log experiment..master F E experiment 브랜치를 Merge 하기전에무엇이변경됐는지궁금하다. 그리고리모트저장소에 Push 할 때에도마찬가지로차이가궁금하다. 이렇게궁금한상황에서굉장히유용하다 : $ git log origin/master..head 이명령은 origin 저장소의 master 브랜치에는없고현재 Checkout중인브랜치에만있는커밋을보여준다. Checkout한브랜치가 origin/master라면 git log origin/master..head가보여주는커밋이 Push하면서버에전송될커밋이다. 그리고한쪽의레퍼런스를생략하면 Git은 HEAD라고가정한다. git log origin/master.. 는 git log origin/master..head와같다. 세개이상의레퍼런스 Double Dot은간단하고유용하다. 하지만, 두개이상의브랜치에는사용할수없다. 그러니까현재작업중인브랜치에는있지만다른여러브랜치에는없는커밋이보고싶으면.. 으로는확인할수없다. 과 --not 옵션뒤에브랜치이름을넣으면그브랜치에없는커밋을찾아준다. 다음명령어는모두같은명령이다 : $ git log refa..refb $ git log ^refa refb $ git log refb --not refa Double Dot 으로는세개이상의레퍼런스에사용할수없지만이옵션은가능하다. 예를들어 refa 나 refb 에는있지만 refc 에는없는커밋을보려면다음중하나를사용한다 : $ git log refa refb ^refc $ git log refa refb --not refc 이조건을잘응용하면작업중인브랜치와다른브랜치을매우상세하게비교할수있다. Triple Dot Triple Dot은양쪽에있는두레퍼런스사이에서공통으로가지는것을제외하고서로다른커밋만보여준다. 그림 6-1의커밋히스토리를다시보자. 만약 master와 experiment의공통부분은빼고다른커밋만보고싶으면아래와같이하면된다 : 143

6 장 Git 도구 Scott Chacon Pro Git $ git log master...experiment F E D C 우리가아는 log 명령의결과를최근날짜순으로보여준다. 이예제에서는커밋을네개보여준다. 그리고 log 명령에 --left-right 옵션을추가하면각커밋이어느브랜치에속하는지도보여주기때문 에좀더이해하기쉽다 : $ git log --left-right master...experiment < F < E > D > C 위와같은명령들을사용하면원하는커밋을좀더꼼꼼하게살펴볼수있다. 6.2 대화형명령어 Git은대화형명령어도제공해서좀더쉽게사용할수있다. 여기서소개하는몇가지대화형명령어를이용하면바로전문가처럼능숙하게커밋할수있다. 대화형으로커밋할파일을고를수도있고수정된파일의일부분만커밋할수도있다. 수정한파일이매우많고통째로커밋하지않고이슈별로나눠서커밋할때유용하다. 이슈별로나눠서커밋하면동료가쉽게검토할수있다. git add 명령에 -i나 -- interactive 옵션을주고실행하면 Git은아래와같은대화형모드로들어간다 : $ git add -i staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 이명령어는 Staging Area의현재상태가어떻고할수있는일이무엇인지보여준다. 기본적으로 git status 명령이보여주는것과같지만좀더간결하고정돈돼있다. 왼쪽에는 Staged 상태인파일들을보여주고오른쪽에는 Unstaged인파일들을보여준다. 144

Scott Chacon Pro Git 6.2 절대화형명령어 그리고마지막 Commands 부분에서는할수일이무엇인지보여준다. 파일을 Stage 하고 Unstage 하는 것, Untracked 상태의파일을추가하는것, Stage 한파일을 diff 해보는것을할수있다. 게다가수정 한파일의일부분만 Staging Area 에추가할수도있다. 6.2.1 Staging Area 에파일추가하고추가취소하기 What now> 프롬프트에서 2 나 u 를 (update) 입력하면 Staging Area 에추가할수있는파일을전부보 여준다 : What now> 2 staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> TODO 와 index.html 파일을 Stage 하려면아래와같이입력한다 : Update>> 1,2 staged unstaged path * 1: unchanged +0/-1 TODO * 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> * 표시가붙은파일은 stage 하도록선택한것이다. 선택하고 Update>> 프롬프트에아무것도입력하지 않고엔터를치면 Git 은선택한파일을 Staging Area 로추가한다 : Update>> updated 2 paths *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb 이제 TODO 와 index.html 파일은 Stage 했고 simplegit.rb 파일만아직 Unstaged 상태로남아있 다. 이제 TODO 파일을다시 Unstage 하고싶으면 3 이나 r 을 (Revert) 입력한다 : 145

6 장 Git 도구 Scott Chacon Pro Git *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 3 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> 1 staged unstaged path * 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> [enter] reverted one path 다시 Status 를선택하면 TODO 파일이 Unstaged 상태인것을알수있다 : *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: unchanged +0/-1 TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Staged 파일의변경내용을보려면 6 이나 d 를 (diff) 입력한다. 그러면먼저 Staged 상태인파일을보 여준다. 그리고그중에서파일하나를선택한다. 그결과는명령줄에서 git diff --cached 라고실행한 결과와같다 : *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 6 staged unstaged path 1: +1/-1 nothing index.html Review diff>> 1 diff --git a/index.html b/index.html index 4d07108..4335f49 100644 --- a/index.html 146

Scott Chacon Pro Git 6.2 절대화형명령어 +++ b/index.html @@ -16,7 +16,7 @@ Date Finder <p id="out">...</p> -<div id="footer">contact : support@github.com</div> +<div id="footer">contact : email.support@github.com</div> <script type="text/javascript"> 대화형모드를사용하면 Staging Area 에파일을좀더쉽게추가할수있다. 6.2.2 파일의일부분만 Staging Area에추가하기파일의일부분만 Staging Area에추가하는것도가능하다. 예를들어 simplegit.rb 파일은고친부분이두군데이다. 그중하나를추가하고나머지는그대로두고싶다. Git에서는이런작업도매우쉽게할수있다. 대화형프롬프트에서 5, p를 (patch) 입력한다. 그러면 Git은부분적으로 Staging Area에추가할파일이있는지묻는다. 파일을선택하면파일의특정부분을 Staging Area에추가할것인지부분별로구분하여묻는다 : diff --git a/lib/simplegit.rb b/lib/simplegit.rb index dd5ecc4..57399e0 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -22,7 +22,7 @@ class SimpleGit end def log(treeish = 'master') - command("git log -n 25 #{treeish}") + command("git log -n 30 #{treeish}") end def blame(path) Stage this hunk [y,n,a,d,/,j,j,g,e,?]? 여기에서? 를입력하면선택가능한명령어를설명해준다 : Stage this hunk [y,n,a,d,/,j,j,g,e,?]?? y - stage this hunk n - do not stage this hunk a - stage this and all the remaining hunks in the file d - do not stage this hunk nor any of the remaining hunks in the file 147

6 장 Git 도구 Scott Chacon Pro Git g - select a hunk to go to / - search for a hunk matching the given regex j - leave this hunk undecided, see next undecided hunk J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks e - manually edit the current hunk? - print help y 나 n 을입력하면각부분을 Stage 할지말지결정할수있다. 하지만, 파일을통째로 stage 하거나필 요할때까지아예그대로남겨두는것이다음에더유용할지도모른다. 어쨌든파일의한부분은 Stage 하고다른부분은 unstaged 상태로남겨놓고 status 명령으로확인해보면결과는아래와같다 : What now> 1 staged unstaged path 1: unchanged +0/-1 TODO 2: +1/-1 nothing index.html 3: +1/-1 +4/-0 lib/simplegit.rb simplegit.rb 파일의상태를보자. 어떤줄은 Staged 상태이고어떤줄은 Unstaged라고알려줄것이다. 이파일은부분적으로 Stage하였다. 이제대화형모드를종료하고일부분만 Stage한파일을커밋할수있다. 끝으로대화형스크립트로만파일일부분을 Stage할수있는것은아니다. git add -p나 git add -- patch로도같은일을할수있다. 6.3 Stashing 당신이어떤프로젝트에서한부분을담당하고있다고하자. 그리고여기에서뭔가작업하던일이있고다른요청이들어와서잠시브랜치를변경해야할일이생겼다고치자. 아직완료하지않은일을커밋하는것은좀껄끄럽다. 이런상황에서는커밋하지않고나중에다시돌아와서작업을다시하고싶을것이다. 이문제는 git stash라는명령으로해결할수있다. Stash 명령을사용하면워킹디렉토리에서수정한파일만저장한다. Stash는 Modified이면서 Tracked 상태인파일과 Staging Area에있는파일들을보관해두는장소다. 아직끝나지않은수정사항을스택에잠시저장했다가나중에다시적용할수있다. 6.3.1 하던일을 Stash 하기 예제프로젝트를하나살펴보자. 파일을두개수정하고그중하나는 Staging Area 에추가한다. 그리 고 git status 명령을실행하면아래와같은결과를볼수있다 : 148

Scott Chacon Pro Git 6.3 절 Stashing $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: lib/simplegit.rb # 이제브랜치를변경한다. 아직작업중인파일은커밋할게아니라서모두 Stash 한다. git stash 를실 행하면스택에새로운 Stash 가만들어진다 : $ git stash Saved working directory and index state \ "WIP on master: 049d078 added the index file" HEAD is now at 049d078 added the index file (To restore them type "git stash apply") 대신워킹디렉토리는깨끗해졌다 : $ git status # On branch master nothing to commit (working directory clean) 이제아무브랜치나골라서바꿀수있다. 수정하던것은스택에저장했다. 아래와같이 git stash list 를사용하여저장한 Stash 를확인한다 : $ git stash list stash@{0}: WIP on master: 049d078 added the index file stash@{1}: WIP on master: c264051... Revert "added file_size" stash@{2}: WIP on master: 21d80a5... added number to log Stash 두개는원래있었던것이다. 그래서현재총세개의 Stash를사용할수있다. 이제 git stash apply를사용하여 Stash를적용할수있다. git stash 명령을실행하면이명령에대한도움말을보여주기때문에편리하다. 다른 Stash를고르고싶으면 Stash 이름을입력해야한다. 이름이없으면 Git 은가장최근의 Stash를적용한다 : 149

6 장 Git 도구 Scott Chacon Pro Git $ git stash apply # On branch master # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: index.html # modified: lib/simplegit.rb # Git은 Stash에저장할때수정하던파일을복원해준다. 복원할때의워킹디렉토리는 Stash할때의그브랜치이고워킹디렉토리도깨끗한상태였다. 하지만, 꼭깨끗한워킹디렉토리나 Stash할때와같은브랜치에적용해야하는것은아니다. 어떤브랜치에서 Stash하고다른브랜치로옮기고서거기에 Stash를복원할수있다. 그리고꼭워킹디렉토리가깨끗한상태일필요도없다. 워킹디렉토리에수정하고커밋하지않은파일들이있을때에도 Stash를적용할수있다. 만약충돌이나면알려준다. Git은 Stash를적용할때 Staged 상태였던파일을자동으로다시 Staged 상태로만들어주지않는다. 그래서 git stash apply 명령을실행할때 --index 옵션을주어야 Staged 상태까지복원한다. 그럼원래작업하던상태로돌아올수있다 : $ git stash apply --index # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: lib/simplegit.rb # apply 옵션은단순히 Stash 를적용하는것뿐이다. Stash 는여전히스택에남아있다. git stash drop 명령을사용하여해당 Stash 를제거한다 : $ git stash list stash@{0}: WIP on master: 049d078 added the index file stash@{1}: WIP on master: c264051... Revert "added file_size" stash@{2}: WIP on master: 21d80a5... added number to log $ git stash drop stash@{0} Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43) 150

Scott Chacon Pro Git 6.3 절 Stashing 그리고 git stash pop 이라는명령도있는데이명령은 Stash 를적용하고나서바로스택에서제거해준 다. 6.3.2 Stash 되돌리기 Stash 를적용하고나서아차싶을때에는다시되돌려놓아야한다. Git 은 stash unapply 같은명령을 제공하지는않는다. 하지만, Stash 를이용해서패치를만들고그것을거꾸로적용할수있다 : $ git stash show -p stash@{0} git apply -R Stash 를명시하지않으면 Git 은가장최근의 Stash 를사용한다 : $ git stash show -p git apply -R stash-unapply 라는 alias 를만들고편리하게할수도있다 : $ git config --global alias.stash-unapply '!git stash show -p git apply -R' $ git stash $ #... work work work $ git stash-unapply 6.3.3 Stash를적용한브랜치만들기보통 Stash에저장하면한동안그대로유지하고그브랜치에서는계속새로운일을한다. 그러면저장한 Stash를적용하는것이문제가될수있다. 수정한파일에 Stash를적용하면충돌이날수있다. 충돌이나면충돌을해결해야한다. 그리고 Stash한것은다시테스트해야한다. git stash branch 명령을실행하면 Stash할당시의커밋을 Checkout한후새로운브랜치를만들고여기에적용한다. 이모든것이성공하면 Stash를삭제한다 : $ git stash branch testchanges Switched to a new branch "testchanges" # On branch testchanges # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # 151

6 장 Git 도구 Scott Chacon Pro Git # modified: lib/simplegit.rb # Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359) 이명령은브랜치를새로만들고 Stash 를복원해주는매우편리한도구다. 6.4 히스토리단장하기 Git으로일하다보면어떤이유로든커밋히스토리를수정해야할때가있다. 결정을나중으로미룰수있던것은 Git의장점이다. Staging Area가있어서커밋할파일을고르는일을커밋하는순간으로미룰수있고 Stash 명령으로하던일을미룰수있다. 게다가이미커밋한내용을수정할수있다. 거의모든것을수정할수있다. 커밋순서도변경할수있고커밋메시지와커밋한파일도변경할수있다. 여러개의커밋을하나로합치거나반대로하나의커밋을여러개로분리할수도있다. 아니면커밋전체를삭제할수도있다. 하지만, 이모든것은다른사람과코드를공유하기전에해야한다. 이절에서는사람들과코드를공유하기전에커밋히스토리를예쁘게단장하는방법에대해서설명한다. 6.4.1 마지막커밋을수정하기히스토리를단장하는일중에서는마지막커밋을수정하는것이가장자주하는일이다. 기본적으로두가지로나눌수있는데하나는커밋메시지를수정하는것이고다른하나는파일목록을수정하는것이다. 커밋메시지를수정하는방법은매우간단하다 : $ git commit --amend 이명령은자동으로텍스트편집기를실행시켜서마지막커밋메시지를열어준다. 여기에메시지를수정하고편집기를닫으면편집기는수정한메시지로마지막커밋을수정한다. 커밋하고나서새로만들었거나다시수정한파일을마지막커밋에포함할수있다. 기본적으로방법은같다. 파일을수정하고 git add 명령으로 Staging Area에넣거나 git rm 명령으로파일삭제한다. 그리고 git commit --amend 명령으로커밋하면된다. 이명령은현 Staging Area의내용을이용해서수정한다. 이때 SHA-1 값이바뀌기때문에과거의커밋을변경할때주의해야한다. rebase처럼이미 Push한커밋은수정하면안된다. 6.4.2 커밋메시지를여러개수정하기최근커밋이아니라예전커밋을수정하려면다른도구가필요하다. 히스토리수정용도구는없지만 rebase 명령을이용하여수정할수있다. 현재작업하는브랜치에서각커밋을하나하나수정하는것이아니라어느시점부터 HEAD까지의커밋을한번에 Rebase한다. 대화형 Rebase 도구를사용하면커밋을처리할때마다멈춘다. 그러면각커밋의메시지를수정하거나파일을추가하고변경하는등의일을진행할수있다. git rebase 명령에 -i 옵션을추가하면대화형모드로 Rebase할수있다. 어떤시점부터 HEAD까지 Rebase할것인지아규먼트로넘기면된다. 152

Scott Chacon Pro Git 6.4 절히스토리단장하기 마지막커밋메시지세개를모두수정하거나그중몇개를수정하는시나리오를살펴보자. git rebase -i의아규먼트로편집하려는마지막커밋의부모를 HEAD~2 나 HEAD~3로해서넘긴다. 마지막세개의커밋을수정하는것이기때문에 ~3이좀더기억하기쉽다. 그렇지만, 실질적으로가리키게되는것은수정하려는커밋의부모인네번째이전커밋이다. $ git rebase -i HEAD~3 이명령은 rebase하는것이기때문에메시지의수정여부에관계없이 HEAD~3..HEAD 범위에있는모든커밋을수정한다. 다시강조하지만이미중앙서버에 Push한커밋은절대고치지말아야한다. Push한커밋을 Rebase하면결국같은내용을두번 Push하는것이기때문에다른개발자들이혼란스러워한다. 실행하면텍스트편집기가열리고그안에는수정하려는커밋목록이첨부된다 : pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file # Rebase 710f0f8..a5f4a0d onto 710f0f8 # # Commands: # p, pick = use commit # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # 이커밋은모두 log 명령과는정반대의순서로나열된다. log 명령을실행하면아래와같은결과를볼 수있다 : $ git log --pretty=format:"%h %s" HEAD~3..HEAD a5f4a0d added cat-file 310154e updated README formatting and added blame f7f3f6d changed my name a bit 위결과의역순임을기억하자. 대화형 rebase는스크립트에적혀있는순서대로 HEAD~3부터적용하기시작하고위에서아래로각각의커밋을순서대로수정한다. 순서대로적용하는것이기때문에제일위에있는것이최신이아니라가장오래된것이다. 특정커밋에서실행을멈추게하려면스크립트를수정해야한다. pick이라는단어를 edit로수정하면그커밋에서멈춘다. 가장오래된커밋메시지를수정하려면아래와같이편집한다 : 153

6 장 Git 도구 Scott Chacon Pro Git edit f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file 저장하고편집기를종료하면 Git 은목록에있는커밋중에서가장오래된커밋으로이동하고, 아래와 같은메시지를보여주고, 명령프롬프트를보여준다 : $ git rebase -i HEAD~3 Stopped at 7482e0d... updated the gemspec to hopefully work better You can amend the commit now, with git commit --amend Once you re satisfied with your changes, run git rebase --continue 정확히뭘해야하는지알려준다. 아래와같은명령을실행하고 : $ git commit --amend 커밋메시지를수정하고텍스트편집기를종료한다. 그리고아래명령어를실행한다 : $ git rebase --continue 이렇게나머지두개의커밋에적용하면끝이다. 다른것도 pick 을 edit 로수정해서이작업을몇번이 든반복할수있다. Git 이멈출때마다커밋을수정할수있고완료할때까지계속할수있다. 6.4.3 커밋순서바꾸기 대화형 Rebase 도구로커밋전체를삭제하고순서도바꿀수있다. added cat-file 커밋을삭제하 고다른두커밋의순서를변경하려면이 rebase 스크립트를 : pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file 아래와같이수정한다 : 154

Scott Chacon Pro Git 6.4 절히스토리단장하기 pick 310154e updated README formatting and added blame pick f7f3f6d changed my name a bit 수정한내용을저장하고편집기를종료하면 Git 은브랜치를이커밋들의부모로이동시키고서 310154e 와 f7f3f6d 를순서대로적용한다. 그러면커밋순서가변경됐고 added cat-file 커밋이제거된것을확 인할수있다. 6.4.4 커밋합치기 대화형 Rebase 명령을이용하여여러개의커밋을꾹꾹눌러서하나의커밋으로만들어버릴수있다. Rebase 스크립트에자동으로포함된도움말에설명돼있다 : # # Commands: # p, pick = use commit # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # pick 이나 edit 말고 squash 를입력하면 Git 은해당커밋과바로이전커밋을합칠것이고커밋 메시지도 Merge 한다. 그래서 3 개의커밋을모두합치려면스크립트를아래와같이수정한다 : pick f7f3f6d changed my name a bit squash 310154e updated README formatting and added blame squash a5f4a0d added cat-file 저장하고나서편집기를종료하면 Git 은 3 개의커밋메시지를 Merge 할수있도록에디터를바로실행 해준다 : # This is a combination of 3 commits. # The first commit's message is: changed my name a bit # This is the 2nd commit message: updated README formatting and added blame 155

6 장 Git 도구 Scott Chacon Pro Git # This is the 3rd commit message: added cat-file 이메시지를저장하면 3 개의커밋이모두합쳐진하나의커밋만남는다. 6.4.5 커밋분리하기커밋을분리한다는것은기존커밋을 Reset하고 ( 혹은되돌려놓고 ) Stage를여러개로분리하고나서그것을원하는횟수만큼다시커밋하는것이다. 예로들었던커밋세개중에서가운데것을분리해보자. 이커밋은 updated README formatting and added blame 라는커밋인데 updated README formatting 과 added blame 으로분리해보자. rebase -i 스크립트에서해당커밋을 edit 로변경한다 : pick f7f3f6d changed my name a bit edit 310154e updated README formatting and added blame pick a5f4a0d added cat-file 위와같이수정하고나서저장하고편집기를종료하면 Git은제일오래된커밋의부모로이동하고서 f7f3f6d과 310154e을처리하고콘솔프롬프트를보여준다. 여기서커밋을해제하는 git reset HEAD 라는명령으로커밋을해제한다. 그러면수정했던파일은 Unstaged 상태가된다. 그다음에파일들을 Stage한후커밋하는일을원하는만큼반복하고나서 git rebase --continue라는명령을실행하면남은 Rebase작업이끝난다 : $ git reset HEAD^ $ git add README $ git commit -m 'updated README formatting' $ git add lib/simplegit.rb $ git commit -m 'added blame' $ git rebase --continue 나머지 a5f4a0d 커밋도처리되면히스토리는아래와같다 : $ git log -4 --pretty=format:"%h %s" 1c002dd added cat-file 9b29157 added blame 35cfb2b updated README formatting f3cc40e changed my name a bit 다시강조하지만, 목록에있는모든커밋의 SHA-1 값은변경된다. 그래서이미서버에 Push 한커밋 을수정하면안된다. 156

Scott Chacon Pro Git 6.4 절히스토리단장하기 6.4.6 filter-branch는포크레인수정해야하는커밋이너무많아서 rebase 스크립트로수정하기어려울것같으면다른방법을사용하는것이좋다. 모든커밋의이메일주소를변경하거나어떤파일을삭제하는경우를살펴보자. filter-branch라는명령으로수정할수있는데 rebase가삽이라면이명령은포크레인이라고할수있다. filter-branch도역시수정하려는커밋이이미공개돼서다른사람과함께공유하는중이라면사용하지말아야한다. 하지만, 잘쓰면꽤유용하다. filter-branch가어떤경우에유용할지예를들어서설명한다. 모든커밋에서파일을제거하기갑자기누군가생각없이 git add. 같은명령어를실행해버려서공룡똥덩어리가커밋됐거나실수로암호가포함된파일을커밋해서이런파일들을다시삭제해야하는상황을살펴보자. 이런상황은생각보다자주발생한다. filter-branch는히스토리전체에서필요한것만골라내는데사용하는도구다. filter-branch의 --tree-filter라는옵션을사용하면히스토리에서 passwords.txt라는파일을아예제거할수있다 : $ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) Ref 'refs/heads/master' was rewritten --tree-filter 옵션은프로젝트를 Checkout한후에각커밋에명시한명령어를실행시키고그결과를다시커밋한다. 이예제에서는각스냅샷에 passwords.txt라는파일이있으면그파일을삭제한다. 실수로편집기의백업파일을커밋했으면 git filter-branch --tree-filter "find * -type f -name '*~' -delete" HEAD라고실행해서삭제할수있다. 이명령은모든파일과커밋을정리하고브랜치포인터를다시복원해준다. 테스팅브랜치에서사용할명령을점검하고나서 master 브랜치를정리한다. 그리고 filter-branch 명령에 --all 옵션을추가하면모든브랜치에적용된다. 하위디렉토리를루트디렉토리로만들기다른 VCS에서코드를임포트하면그 VCS만을위한디렉토리가있을수있다. SVN에서코드를임포트하면 trunk, tags, branch 디렉토리가포함된다. 모든커밋에대해 trunk 디렉토리를프로젝트루트디렉토리로만들때에도 filter-branch 명령이유용하다 : $ git filter-branch --subdirectory-filter trunk HEAD Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12) Ref 'refs/heads/master' was rewritten 이제 trunk 디렉토리를루트디렉토리로만들었다. Git 은입력한디렉토리와관련이없는커밋을자동 으로삭제한다. 모든커밋의이메일주소를수정하기프로젝트를오픈소스로공개할때에도회사이메일주소로커밋된것을개인이메일주소로변경해야한다. 아니면아예 git config로이름과이메일주소를설정하는것을잊었을수도있다. 어쨌든 filter- 157

6 장 Git 도구 Scott Chacon Pro Git branch 명령의 --commit-filter 옵션을사용하여각커밋에등록된이메일주소를수정할수있다. 이메 일주소를변경할때는조심해야한다. $ git filter-branch --commit-filter ' if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ]; then GIT_AUTHOR_NAME="Scott Chacon"; GIT_AUTHOR_EMAIL="schacon@example.com"; git commit-tree "$@"; else git commit-tree "$@"; fi' HEAD 이메일주소를새주소로변경했다. 모든커밋은부모의 SHA-1 값을가지고있기때문에조건에만족 하는커밋의 SHA-1 값만바뀌는것이아니라모든커밋의 SHA-1 값이바뀐다. 6.5 Git 으로버그찾기 Git 은굉장히유연해서어떤프로젝트에나사용할수있다. 게다가문제를일으킨범인이나버그도쉽 게찾을수있도록도와준다. 6.5.1 파일어노테이션버그를찾을때먼저그코드가왜, 언제추가했는지알고싶을것이다. 이때는파일어노테이션을활용한다. 한줄한줄마지막으로커밋한사람이누구인지, 언제마지막으로커밋했는지볼수있다. 어떤메소드에버그가있으면 git blame 명령으로그메소드의각줄을누가언제마지막으로고쳤는지찾아낼수있다 : $ git blame -L 12,22 simplegit.rb ^4832fe2 (Scott Chacon 2008-03-15 10:31:28-0700 12) def show(tree = 'master') ^4832fe2 (Scott Chacon 2008-03-15 10:31:28-0700 13) command("git show #{tree}") ^4832fe2 (Scott Chacon 2008-03-15 10:31:28-0700 14) end ^4832fe2 (Scott Chacon 2008-03-15 10:31:28-0700 15) 9f6560e4 (Scott Chacon 2008-03-17 21:52:20-0700 16) def log(tree = 'master') 79eaf55d (Scott Chacon 2008-04-06 10:15:08-0700 17) command("git log #{tree}") 9f6560e4 (Scott Chacon 2008-03-17 21:52:20-0700 18) end 9f6560e4 (Scott Chacon 2008-03-17 21:52:20-0700 19) 42cf2861 (Magnus Chacon 2008-04-13 10:45:01-0700 20) def blame(path) 42cf2861 (Magnus Chacon 2008-04-13 10:45:01-0700 21) command("git blame #{path}") 42cf2861 (Magnus Chacon 2008-04-13 10:45:01-0700 22) end 첫항목은그줄을마지막에수정한커밋의 SHA-1 값이다. 그다음두항목은누가, 언제그줄을커밋 했는지보여준다. 그래서누가, 언제커밋했는지쉽게찾을수있다. 그뒤에파일의줄번호와내용을보 158

Scott Chacon Pro Git 6.5 절 Git 으로버그찾기 여준다. 그리고 4832fe2 커밋이궁금할텐데이표시가붙어있으면해당줄이처음커밋한것을의미한다. 그러니까해당줄은 4832fe2에서커밋된후변경된적이없다. 지금까지커밋을수정하는것을배우면서 을적어도세곳에서사용한다고배웠기때문에약간헷갈릴수있으니혼동하지말자. Git은파일이름을변경한이력을별도로기록해두지않는다. 하지만, 원래이정보들은각스냅샷에저장되고이정보를이용하여변경이력을만들어낼수있다. 그러니까파일에생긴변화는무엇이든지알아낼수있다. Git은파일어노테이션을분석하여코드들이원래어떤파일에서커밋된것인지찾아준다. 예를들어보자. GITServerHandler.m을여러개의파일로리팩토링했는데그중한파일이 GITPackUpload.m이라는파일이라고하자. -C 옵션으로 GITPackUpload.m 파일을추적해보면각코드가원래어떤파일로커밋된것인지알수있다 : $ git blame -C -L 141,153 GITPackUpload.m f344f58d GITServerHandler.m (Scott 2009-01-04 141) f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherobjectshasfromc f344f58d GITServerHandler.m (Scott 2009-01-04 143) { 70befddd GITServerHandler.m (Scott 2009-03-22 144) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 145) ad11ac80 GITPackUpload.m (Scott 2009-03-24 146) NSString *parentsha; ad11ac80 GITPackUpload.m (Scott 2009-03-24 147) GITCommit *commit = [g ad11ac80 GITPackUpload.m (Scott 2009-03-24 148) ad11ac80 GITPackUpload.m (Scott 2009-03-24 149) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 150) 56ef2caf GITServerHandler.m (Scott 2009-01-05 151) if(commit) { 56ef2caf GITServerHandler.m (Scott 2009-01-05 152) [refdict setob 56ef2caf GITServerHandler.m (Scott 2009-01-05 153) 언제나코드가커밋될당시의파일이름을알수있기때문에코드를어떻게리팩토링해도추적할수있 다. 그리고어떤파일에적용해봐도각줄을커밋할당시의파일이름을알수있다. 버그를찾을때정말 유용하다. 6.5.2 이진탐색파일어노테이션은특정이슈와관련된커밋을찾는데에도좋다. 문제가생겼을때의심스러운커밋이수십, 수백개에이르면도대체어디서부터시작해야할지모를수있다. 이때는 git bisect 명령이유용하다. bisect 명령은커밋히스토리를이진탐색방법으로좁혀주기때문에이슈와관련된커밋을최대한빠르게찾아낼수있도록도와준다. 코드를운용환경에배포하고난후에개발할때발견하지못한버그가있다고보고받았다. 그런데왜그런현상이발생하는지아직이해하지못하는상황을가정해보자. 해당이슈를다시만들고작업하기시작했는데뭐가잘못됐는지알아낼수없다. 이럴때 bisect를사용하여코드를뒤져보는게좋다. 먼저 git bisect start 명령으로이진탐색을시작하고 git bisect bad를실행하여현재커밋에문제가있다고표시를남기고나서문제가없는마지막커밋을 git bisect good [good_commit] 명령으로표시한다. $ git bisect start 159

6 장 Git 도구 Scott Chacon Pro Git $ git bisect bad $ git bisect good v1.0 Bisecting: 6 revisions left to test after this [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo 이예제에서마지막으로괜찮았던커밋 (v1.0) 과현재문제가있는커밋사이에있는커밋은전부 12개이고 Git은그중간에있는커밋을 Checkout해준다. 여기에서해당이슈가구현됐는지테스트해보고만약이슈가있으면그중간커밋이전으로범위를좁히고이슈가없으면그중간커밋이후로범위를좁힌다. 이슈를발견하지못했으면 git bisect good으로이슈가아직없음을알리고계속진행한다 : $ git bisect good Bisecting: 3 revisions left to test after this [b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing 현재문제가있는커밋과지금테스트한커밋사이에서중간에있는커밋이 Checkout 됐다. 다시테스 트해보고이슈가있으면 git bisect bad 로이슈가있다고알린다 : $ git bisect bad Bisecting: 1 revisions left to test after this [f71ce38690acf49c1f3c9bea38e09d82a5ce6014] drop exceptions table 이제이슈를처음구현한커밋을찾았다. 이 SHA-1 값을포함한이커밋의정보를확인하고수정된파 일이무엇인지확인한다. 이문제가발생한시점에도대체무슨일이있었는지아래와같이살펴본다 : $ git bisect good b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04 Author: PJ Hyett <pjhyett@example.com> Date: Tue Jan 27 14:48:32 2009-0800 secure this thing :040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730 f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M config 이제찾았으니까 git bisect reset 명령을실행시켜서이진탐색을시작하기전으로 HEAD 를돌려놓 는다 : 160

Scott Chacon Pro Git 6.6 절서브모듈 $ git bisect reset 수백개의커밋들중에서버그가만들어진커밋을찾는데몇분밖에걸리지않는다. 프로젝트가정상적으로수행되면 0을반환하고문제가있을경우 1을반환하는스크립트를만들면이 git bisect 과정을완전히자동화할수있다. 먼저 bisect start 명령으로 bisect를사용할범위를알려준다. 위에서한것처럼문제가있다고아는커밋과문제가없다고아는커밋을넘기면된다 : $ git bisect start HEAD v1.0 $ git bisect run test-error.sh 문제가생긴첫커밋을찾을때까지 Checkout 할때마다 test-error.sh 를실행한다. make 든지 make tests 든지어쨌든이슈를찾는테스트를실행하여찾는다. 6.6 서브모듈 프로젝트를수행하다보면다른프로젝트를사용해야하는경우가종종있다. 보통사용할프로젝트들은독립적으로개발된라이브러리들이다. 이런상황에서자주생기는이슈는, 두프로젝트를서로별개로다루면서도그중하나를다른하나안에서사용할수있어야한다는것이다. Atom 피드를제공하는웹사이트를만든다고가장하자. Atom 피드를생성하는코드는직접작성하지않고라이브러리를가져다쓰기로했다. 그러면 CPAN이나 Ruby gem 같은라이브러리관리도구를사용하거나해당소스코드를프로젝트로복사해야한다. 사실라이브러리를수정하는것은어렵다. 하지만수정한라이브러리를모든사용자가이용할수있도록배포하는것은더어렵다. 그래서프로젝트에라이브러리코드를포함시켜서수정하는방법도사용한다. 이렇게라이브러리코드를포함시키면원래라이브러리프로젝트의코드와 Merge하기어렵게된다. Git의서브모듈은이런문제를해결해준다. 서브모듈은 Git 저장소안에다른 Git 저장소를둘수있게해준다. 이렇게해도두 Git 저장소모두여전히독립적으로관리된다. 6.6.1 서브모듈시작하기한번 Ruby 웹서버게이트웨이인터페이스인 Rack 라이브러리를프로젝트에추가해보자. 추가하고나서도앞으로여전히해당저장소에서관리할수있기때문에마음놓고코드를수정할수있다. 먼저 git submodule add 명령으로프로젝트를서브모듈로추가한다 : $ git submodule add git://github.com/chneukirchen/rack.git rack Initialized empty Git repository in /opt/subtest/rack/.git/ remote: Counting objects: 3181, done. remote: Compressing objects: 100% (1534/1534), done. remote: Total 3181 (delta 1951), reused 2623 (delta 1603) Receiving objects: 100% (3181/3181), 675.42 KiB 422 KiB/s, done. Resolving deltas: 100% (1951/1951), done. 161

6 장 Git 도구 Scott Chacon Pro Git 이제프로젝트디렉토리를보면 rack이라는디렉토리가생겼을것이다. 그디렉토리가 Rack 프로젝트이다. rack 디렉토리안에서수정하고 Push할권한이있는저장소를하나추가하고나서그저장소에 Push한다. 물론원래프로젝트저장소에서도 Fetch하고 Merge할수있다. 서브모듈을추가한직후바로 git status라는명령을실행하면아래와같이두파일이생긴것을알수있다 : $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file:.gitmodules # new file: rack #.gitmodules 파일을살펴보자. 이것은로컬디렉토리와프로젝트 URL 의매핑정보가저장된설정파일 이다 : $ cat.gitmodules [submodule "rack"] path = rack url = git://github.com/chneukirchen/rack.git 서브모듈개수만큼이항목이생긴다. 이파일도.gitignore 파일처럼버전관리된다. 다른파일처럼 Push하고풀한다. 이프로젝트를 Clone하는사람은.gitmodules 파일을보고어떤서브모듈프로젝트가있는지알수있다..gitmodules은살펴봤고이제 rack 항목에대해살펴보자. git diff 명령을실행시키면흥미로운점을발견할수있다 : $ git diff --cached rack diff --git a/rack b/rack new file mode 160000 index 0000000..08d709f --- /dev/null +++ b/rack @@ -0,0 +1 @@ +Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 Git은 rack 디렉토리를서브모듈로취급하기때문에파일들을직접추적하지않고커밋하나만저장한다. rack 디렉토리에서수정을하고커밋하면다른사람이같은환경을만들수있도록 HEAD가가리키는커밋이슈퍼프로젝트에저장된다. master처럼브랜치이름같은레퍼런스가저장되는것이아니라커밋의 SHA-1 값이저장된다. 슈퍼프로젝트도커밋해야된다 : 162

Scott Chacon Pro Git 6.6 절서브모듈 $ git commit -m 'first commit with submodule rack' [master 0550271] first commit with submodule rack 2 files changed, 4 insertions(+), 0 deletions(-) create mode 100644.gitmodules create mode 160000 rack rack 디렉토리의모드는 160000이다. 160000 모드는일반적인파일이나디렉토리가아니라는의미다. 하위프로젝트의마지막커밋이바뀔때마다슈퍼프로젝트에저장된커밋도바꿔준다. rack 디렉토리를별도의프로젝트로취급하기때문에모든 Git 명령은독립적으로동작한다 : $ git log -1 commit 0550271328a0038865aad6331e620cd7238601bb Author: Scott Chacon <schacon@gmail.com> Date: Thu Apr 9 09:03:56 2009-0700 first commit with submodule rack $ cd rack/ $ git log -1 commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 Author: Christian Neukirchen <chneukirchen@gmail.com> Date: Wed Mar 25 14:49:04 2009 +0100 Document version change 6.6.2 서브모듈이있는프로젝트 Clone 하기 서브모듈을사용하는프로젝트를 Clone 하면해당서브모듈디렉토리는빈디렉터리다 : $ git clone git://github.com/schacon/myproject.git Initialized empty Git repository in /opt/myproject/.git/ remote: Counting objects: 6, done. remote: Compressing objects: 100% (4/4), done. remote: Total 6 (delta 0), reused 0 (delta 0) Receiving objects: 100% (6/6), done. $ cd myproject $ ls -l total 8 -rw-r-r-- 1 schacon admin 3 Apr 9 09:11 README drwxr-xr-x 2 schacon admin 68 Apr 9 09:11 rack $ ls rack/ 163

6 장 Git 도구 Scott Chacon Pro Git $ 분명히 rack 디렉토리가있지만비워져있다. 먼저 git submodule init 명령으로서브모듈을초기화하 고 git submodule update 명령으로서버에서데이터를가져온다. 데이터를전부가져오면슈퍼프로젝 트에저장된커밋으로 Checkout 된다 : $ git submodule init Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack' $ git submodule update Initialized empty Git repository in /opt/myproject/rack/.git/ remote: Counting objects: 3181, done. remote: Compressing objects: 100% (1534/1534), done. remote: Total 3181 (delta 1951), reused 2623 (delta 1603) Receiving objects: 100% (3181/3181), 675.42 KiB 173 KiB/s, done. Resolving deltas: 100% (1951/1951), done. Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433' rack 디렉토리는이제복원했다. 그리고누군가 rack 을수정하면그코드를가져다 Merge 한다 : $ git merge origin/master Updating 0550271..85a3eee Fast forward rack 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) [master*]$ git status # On branch master # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: rack # Merge 해서서브모듈의 HEAD 값이변경됐다. 슈퍼프로젝트가아는커밋과서브모듈의 HEAD 가달 라서아직워킹디렉토리의상태는깨끗한상태가아니다 : $ git diff diff --git a/rack b/rack index 6c5e70b..08d709f 160000 --- a/rack 164

Scott Chacon Pro Git 6.6 절서브모듈 +++ b/rack @@ -1 +1 @@ -Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 +Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 이럴때 git submodule update 명령을실행해서해결한다 : $ git submodule update remote: Counting objects: 5, done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 1), reused 2 (delta 0) Unpacking objects: 100% (3/3), done. From git@github.com:schacon/rack 08d709f..6c5e70b master -> origin/master Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0' 서브모듈프로젝트를풀할때마다 git submodule update 명령을실행해야한다. 뭔가속는것같지만잘된다. 개발자들이흔히저지르는실수로서브모듈의코드를수정하고나서서버에 Push하지않는경우가있다. 슈퍼프로젝트는 Push했지만프로젝트가아는커밋은아직 Push하지않고개발자 PC에만있다. 만약다른개발자가 git submodule update를실행하면슈퍼프로젝트에저장된커밋을서브모듈프로젝트에서찾을수없어서에러가발생한다 : $ git submodule update fatal: reference isn t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack' 누가마지막으로서브모듈을수정했는지확인하고 : $ git log -1 rack commit 85a3eee996800fcfa91e2119372dd4172bf76678 Author: Scott Chacon <schacon@gmail.com> Date: Thu Apr 9 09:19:14 2009-0700 added a submodule reference I will never make public. hahahahaha! 그개발자에게이메일을보내거나전화를건다. 165

6 장 Git 도구 Scott Chacon Pro Git 6.6.3 슈퍼프로젝트프로젝트규모가크면 CVS나 Subversion에서는모듈프로젝트을간단히하위디렉토리로만들었다. 가끔 Git에서도이런 Workflow을사용하려는개발자들이있다. Git에서는각하위디렉토리를별도의 Git 저장소로만들어야한다. 그리고그저장소을포함하는상위저장소를만든다. 슈퍼프로젝트의태그와브랜치를이용해서각프로젝트의관계를구체적으로정의할수있다는것은 Git만의장점이다. 6.6.4 서브모듈사용할때주의할점들전체적으로서브모듈은어렵지않게사용할수있지만, 서브모듈의코드를수정하는경우에는주의가필요하다. git submodule update 명령을실행시키면특정브랜치가아니라슈퍼프로젝트에저장된커밋을 Checkout해버린다. 그러면 detached HEAD라고부르는상태가된다. detached HEAD는 HEAD가브랜치나태그같은간접레퍼런스를가리키지않고커밋을가리키는것을말한다. 데이터를잃어버릴수도있기때문에일반적으로 detached HEAD 상태는피해야한다. submodule update를실행하고나서별도의작업용브랜치를만들지않고서브모듈코드를수정하고커밋한다. 그리고나중에커밋한것을잊은채로슈퍼프로젝트에서다시 git submodule update를실행시키면 Git은아무말없이 Checkout해버린다. 엄밀히말해서커밋을없어진것은아니지만브랜치에속하지않는커밋을찾아내기란정말어렵다. git checkout -b work 같은명령으로작업할때마다 work 브랜치를만들면이문제를피할수있다. 실수로 submodule update 명령을실행해버려서하던일을놓쳐버려도포인터가있어서언제든지되찾을수있다. 그리고서브모듈이있는슈퍼프로젝트의브랜치를오갈때는약간의추가작업이필요하다. 브랜치를만들고서브모듈을추가한다. 그다음에서브모듈이없는브랜치로돌아간다. 그렇지만, 이미추가한서브모듈디렉토리가 untracked 상태로보인다 : $ git checkout -b rack Switched to a new branch "rack" $ git submodule add git@github.com:schacon/rack.git rack Initialized empty Git repository in /opt/myproj/rack/.git/... Receiving objects: 100% (3184/3184), 677.42 KiB 34 KiB/s, done. Resolving deltas: 100% (1952/1952), done. $ git commit -am 'added rack submodule' [rack cc49a69] added rack submodule 2 files changed, 4 insertions(+), 0 deletions(-) create mode 100644.gitmodules create mode 160000 rack $ git checkout master Switched to branch "master" $ git status # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # 166

Scott Chacon Pro Git 6.6 절서브모듈 # rack/ 서브모듈디렉토리를다른곳에옮겨두거나삭제해야한다. 삭제할경우는원래브랜치로돌아왔을때서브모듈을다시 Clone해야하고, 이경우아직 Push하지않았던변경사항이나브랜치를잃을수있다. rack이라는디렉토리가있고이것을서브모듈로바꾸려고한다고가정하자. 먼저 rack 디렉토리를삭제하고 submodule add를실행하면 Git은아래와같은에러를뱉는다 : $ rm -Rf rack/ $ git submodule add git@github.com:schacon/rack.git rack 'rack' already exists in the index rack 디렉토리를 Staging Area 에서제거하면서브모듈을추가할수있다. $ git rm -r rack $ git submodule add git@github.com:schacon/rack.git rack Initialized empty Git repository in /opt/testsub/rack/.git/ remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB 88 KiB/s, done. Resolving deltas: 100% (1952/1952), done. 한브랜치에서는해결했다. 아직해당디렉토리를서브모듈로만들지않은브랜치를 Checkout 하려고 하면아래와같은에러가난다 : $ git checkout master error: Untracked working tree file 'rack/authors' would be overwritten by merge. 다른브랜치로바꾸기전에 rack 서브모듈디렉토리를다른곳으로옮겨둔다 : $ mv rack /tmp/ $ git checkout master Switched to branch "master" $ ls README rack 그리고나서다시서브모듈이있는브랜치로돌아가면 rack 디렉토리는텅비어있다. git submodule update 명령으로다시 Clone 하거나 /tmp/rack/ 에복사해둔파일을다시복사한다. 167

6 장 Git 도구 Scott Chacon Pro Git 6.7 Subtree Merge 서브모듈시스템이무엇이고어디에쓰는지배웠다. 그런데같은문제를해결하는방법이또하나있다. Git은 Merge하는시점에무엇을 Merge할지, 어떤전략을사용할지결정해야한다. Git은브랜치두개를 Merge할때에는 Recursive 전략을사용하고세개이상의브랜치를 Merge할때에는 Octopus 전략을사용한다. 이전략은자동으로선택된다. Merge할브랜치가두개면 Recursive 전략이선택된다. Recursive 전략은 Merge하려는두커밋과공통조상커밋을이용하는 three-way merge를사용하기때문에단두개의브랜치에만적용할수있다. Octopus 전략은브랜치가여러개라도 Merge할수있지만비교적충돌이쉽게일어난다. 다른전략도있는데그중하나가 Subtree Merge다. 이 Merge는하위프로젝트문제를해결하는데에도사용한다. 위에서사용했던 Rack 예제를적용해보자. Subtree Merge는마치하위프로젝트가아예합쳐진것처럼보일정도로한프로젝트를다른프로젝트의하위디렉토리에연결해준다. 정말놀라운기능이다. Rack 프로젝트를리모트저장소로추가시키고브랜치를 Checkout한다 : $ git remote add rack_remote git@github.com:schacon/rack.git $ git fetch rack_remote warning: no common commits remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB 4 KiB/s, done. Resolving deltas: 100% (1952/1952), done. From git@github.com:schacon/rack * [new branch] build -> rack_remote/build * [new branch] master -> rack_remote/master * [new branch] rack-0.4 -> rack_remote/rack-0.4 * [new branch] rack-0.9 -> rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch" Checkout 한 rack_branch 의루트디렉토리와 origin 프로젝트의 master 브랜치의루트디렉토리는다 르다. 브랜치를바꿔가며어떻게다른지확인한다 : $ ls AUTHORS KNOWN-ISSUES Rakefile contrib lib COPYING README bin example test $ git checkout master Switched to branch "master" $ ls README 168

Scott Chacon Pro Git 6.8 절요약 여기에서 Rack 프로젝트를 master 브랜치의하위디렉토리에넣으려면 git read-tree 명령어를사용한다. 9장에서 read-read 류의명령어를좀더자세히다룬다. 여기에서는워킹디렉토리와 Staging Area 로어떤브랜치를통째로넣을수있다는것만알면된다. master 브랜치로되돌아가서 rack_branch를 rack 디렉토리에넣는다 : $ git read-tree --prefix=rack/ -u rack_branch 그리고나서커밋을하면 rack 디렉토리는 rack 프로젝트의파일들을직접복사해넣은것과똑같다. 복사한것과다른점은브랜치를자유롭게바꿀수있고최신버전의 Rack 프로젝트의코드를쉽게끌 어올수있다는점이다 : $ git checkout rack_branch $ git pull 그리고 git merge -s subtree 라는명령어를사용하여 master 브랜치와 Merge 할수있고원하든원하 지않든간에히스토리도함께 Merge 된다. 수정내용만 Merge 하거나커밋메시지를다시작성하려면 -s subtree 옵션에다가 --squash, --no-commit 를함께사용해야한다 : $ git checkout master $ git merge --squash -s subtree --no-commit rack_branch Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested Rack 프로젝트의최신코드를가져다가 Merge했고이제커밋하면된다. 물론반대로하는것도가능하다. rack 디렉토리로이동해서코드를수정하고 rack_branch 브랜치로 Merge한다. 그리고 Rack 프로젝트저장소에 Push할수있다. rack 디렉토리와 rack_branch 브랜치와의차이점도비교할수있다. 일반적인 diff 명령은사용할수없고 git diff-tree 명령을사용해야한다 : $ git diff-tree -p rack_branch 또 rack 디렉토리와저장소의 master 브랜치와비교할수있다 : $ git diff-tree -p rack_remote/master 6.8 요약 커밋과저장소를꼼꼼하게관리하는도구를살펴보았다. 문제가생기면바로누가, 언제, 무엇을했는지찾아내야한다. 그리고프로젝트를쪼개고싶을때사용하는방법들도배웠다. 이제 Git 명령어는거 169

6 장 Git 도구 Scott Chacon Pro Git 의모두배운것이다. 독자들이하루빨리익숙해져서자유롭게사용했으면좋겠다. 170

7 장 Git 맞춤 지금까지 Git이어떻게동작하고 Git을어떻게사용하는지설명했다. 이제 Git을좀더쉽고편하게사용할수있도록도와주는도구를살펴본다. 이장에서는먼저많이쓰이는설정그리고훅시스템을먼저설명한다. 그후에 Git을내게맞추어 (Customize) 본다. Git을자신의프로젝트에맞추고편하게사용하자. 7.1 Git 설정하기 1 장에서설명했지만, 제일먼저해야하는것은 git config 명령으로이름과 e-mail 주소를설정하는 것이다 : $ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com 이렇게설정하는것들중에서중요한것을몇가지설명한다. 아주기초적인설정은1장에서도설명했지만, 이번장에서다시한번복습한다. Git은내장된기본규칙따르지만, 설정된것이있으면그에따른다. Git은먼저 /etc/gitconfig 파일을찾는다. 이파일은해당시스템에있는모든사용자와모든저장소에적용되는설정파일이다. git config 명령에 --system 옵션을주면이파일을사용한다. 다음으로 ~/.gitconfig 파일을찾는다. 이파일은해당사용자에게만적용되는설정파일이다. --global 옵션을주면 Git은이파일을사용한다. 마지막으로현재작업중인저장소의 Git 디렉토리에있는.git/config 파일을찾는다. 이파일은해당저장소에만적용된다. 각설정파일에중복된설정이있으면설명한순서대로덮어쓴다. 예를들어.git/config와 /etc/gitconfig에같은설정이들어있다면.git/config에있는설정을사용한다. 설정파일은손으로직접편집해도되지만보통 git config 명령을사용하는것이더편하다. 7.1.1 클라이언트설정설정은클라이언트와서버로나뉜다. 대부분은개인작업환경과관련된클라이언트설정이다. Git에는설정거리가매우많은데, 여기서는 Workflow를관리하는데필요한것과잘사용하는것만설명한다. 한번도겪지못할상황에서나유용한옵션까지다포함하면설정할게너무많다. Git 버전마다옵션이조금씩다른데, 아래와같이실행하면설치한버전에서사용할수있는옵션을모두보여준다 : 171

7 장 Git 맞춤 Scott Chacon Pro Git $ git config --help 어떤옵션을사용할수있는지 git config 의에자세히설명돼있다. core.editor Git 은편집기를설정하지않았거나설정한편집기를찾을수없으면 Vi 를실행한다. 커밋할때나 tag 메시지를편집할때설정한편집기를실행한다. code.editor 설정으로편집기를설정한다 : $ git config --global core.editor emacs 이렇게설정하면메시지를편집할때환경변수에설정한편집기가아니라 Emacs 를실행한다. commit.template 커밋할때 Git 이보여주는커밋메시지는이옵션에설정한템플릿파일이다. 예를들어 $HOME/.gitmessage.txt 파일을아래와같이만든다 : subject line what happened [ticket: X] 이파일을 commit.template 에설정하면 Git 은 git commit 명령이실행하는편집기에이메시지를기본 으로넣어준다 : $ git config --global commit.template $HOME/.gitmessage.txt $ git commit 그러면 commit 할때아래와같은메시지를편집기에자동으로채워준다 : subject line what happened [ticket: X] # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. 172

Scott Chacon Pro Git 7.1 절 Git 설정하기 # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: lib/test.rb # ~ ~ ".git/commit_editmsg" 14L, 297C 소속팀에커밋메시지규칙이있으면그규칙에맞는템플릿파일을만든다. Git 이그파일을사용하도 록설정하면규칙을따르기가쉬워진다. core.pager Git 은 log 나 diff 같은명령의메시지를출력할때페이지로나누어보여준다. 기본으로사용하는명령 은 less 다. more 를더좋아하면 more 라고설정한다. 페이지를나누고싶지않으면빈문자열로설정한다 : $ git config --global core.pager '' 이명령을실행하면 Git 은길든지짧든지결과를한번에다보여준다. user.signingkey 2 장에서설명했던 Annotated Tag 를만들때유용하다. 사용할 GPG 키를설정해둘수있다. 아래처 럼 GPG 키를설정하면서명할때편리하다 : $ git config --global user.signingkey <gpg-key-id> git tag 명령을실행할때키를생략하고서명할수있다 : $ git tag -s <tag-name> core.excludesfile Git이무시하는 untracked 파일은.gitignore에해당패턴을적으면된다고 2장에서설명했다. 해당패턴의파일은 git add 명령으로추가해도 Stage되지않는다..gitignore 파일을저장소밖에두고관리하고싶으면 core.excludesfile에해당파일의경로를설정한다. 이파일을작성하는방법은.gitignore 파일을작성하는방법과같다. 그리고 core.excludesfile에설정한파일과저장소안에있는.gitignore 파일은둘다사용된다. 173

7 장 Git 맞춤 Scott Chacon Pro Git help.autocorrect 이옵션은 Git 1.6.1 버전부터사용할수있다. 명령어를잘못입력하면 Git 은메시지를아래와같이보 여준다 : $ git com git: 'com' is not a git-command. See 'git --help'. Did you mean this? commit 그러나 help.autocorrect 를 1 로설정하면명령어를잘못입력해도 Git 이자동으로해당명령어를찾아 서실행해준다. 단, 해당명령어가딱하나찾았을때에만실행한다. 7.1.2 컬러터미널 사람이쉽게인식할수있도록터미널에결과를컬러로출력할수있다. 터미널컬러와관련된옵션은 매우다양하기때문에꼼꼼하게설정할수있다. color.ui color.ui 를 true 로설정하면 Git 이알아서결과에색칠한다. 물론무엇을어떤색으로칠할지꼼꼼하게 설정할수있지만, 이옵션을켜면터미널을그냥기본컬러로칠한다. $ git config --global color.ui true 이옵션을켜면 Git은터미널에컬러로결과를출력한다. 이값을 false로설정하면절대컬러로출력하지않는다. 결과를파일로리다이렉트하거나다른프로그램으로보낼 (Piping. 파이프라인 ) 때도그렇다. color.ui = always라고설정하면결과를리다이렉트할때에도컬러코드가출력된다. 이렇게까지설정해야하는경우는매우드물다. 대신 Git 명령에는 --color 옵션이있어서어떻게출력할지그때그때정해줄수있다. 보통은 color.ui = true 만으로도충분하다. color.* Git 은좀더꼼꼼하게컬러를설정하는방법을제공한다. 아래와같은설정들이있다. 모두 true, false, always 중하나를고를수있다 : color.branch color.diff color.interactive color.status 174

Scott Chacon Pro Git 7.1 절 Git 설정하기 또한, 각옵션의컬러를직접지정할수도있다. 아래처럼설정하면 diff 명령에서 meta 정보의포그라 운드는 blue, 백그라운드는 black, 테스트는 bold 로바뀐다 : $ git config --global color.diff.meta "blue black bold" 컬러는 normal, black, red, green, yellow, blue, magenta, cyan, white 중에서고를수있고텍 스트속성은 bold, dim, ul, blink, reverse 중에서고를수있다. git config 맨페이지를보면어떤설정거리가있는지자세히나온다. 7.1.3 다른 Merge, Diff 도구사용하기 Git에들어있는 diff 말고다른도구로바꿀수있다. 화려한 GUI 도구로바꿔서좀더편리하게충돌을해결할수있다. 여기서는 Perforce의 Merge 도구인 P4Merge로설정하는것을보여준다. P4Merge 는무료인데다꽤괜찮다. P4Merge는중요플랫폼을모두지원하기때문에웬만한환경이면사용할수있다. 여기서는 Mac과 Linux 시스템에설치하는것을보여준다. 윈도에서사용하려면 /usr/local/bin 경로만윈도경로로바꿔준다. 다음페이지에서 P4Merge를내려받는다 : http://www.perforce.com/perforce/downloads/component.html 먼저 P4Merge에쓸 Wrapper 스크립트를만든다. 필자는 Mac 사용자라서 Mac 경로를사용한다. 어떤시스템이든 p4merge가설치된경로를사용하면된다. extmerge라는 Merge용 Wrapper 스크립트를만들고이스크립트로넘어오는모든아규먼트를 p4merge 프로그램으로넘긴다 : $ cat /usr/local/bin/extmerge #!/bin/sh /Applications/p4merge.app/Contents/MacOS/p4merge $* 그리고 diff 용 Wrapper 도만든다. 이스크립트로넘어오는아규먼트는총 7 개지만그중 2 개만 Merge Wrapper 로넘긴다. Git 이 diff 프로그램에넘겨주는아규먼트는아래와같다 : path old-file old-hex old-mode new-file new-hex new-mode 이중에서 old-file 과 new-file 만사용하는 wrapper script 를만든다 : $ cat /usr/local/bin/extdiff #!/bin/sh [ $# -eq 7 ] && /usr/local/bin/extmerge "$2" "$5" 이두스크립트에실행권한을부여한다 : 175

7 장 Git 맞춤 Scott Chacon Pro Git $ sudo chmod +x /usr/local/bin/extmerge $ sudo chmod +x /usr/local/bin/extdiff Git config 파일에이스크립트를모두추가한다. 설정해야하는옵션이좀많다. merge.tool로무슨 Merge 도구를사용할지, mergetool.*.cmd로실제로어떻게명령어를실행할지, mergetool.trustexitcode로 Merge 도구가반환하는 exit 코드가 merge의성공여부를나타내는지, diff.external은 diff할때실행할명령어가무엇인지를설정할때사용한다. 모두 git config 명령으로설정한다 : $ git config --global merge.tool extmerge $ git config --global mergetool.extmerge.cmd \ 'extmerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"' $ git config --global mergetool.trustexitcode false $ git config --global diff.external extdiff ~/.gitconfig/ 파일을직접편집해도된다 : [merge] tool = extmerge [mergetool "extmerge"] cmd = extmerge \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\" trustexitcode = false [diff] external = extdiff 설정을완료하고나서아래와같이 diff 명령어를실행한다 : $ git diff 32d1776b1^ 32d1776b1 diff 결과가터미널에출력되는대신 P4Merge가실행된다. 그리고그림 7-1처럼그프로그램안에서보여준다 : 브랜치를 Merge할때충돌이나면 git mergetool 명령을실행한다. 이명령을실행하면 GUI 도구로충돌을해결할수있도록 P4Merge를실행해준다. Wrapper를만들어설정해두면다른 diff, Merge 도구로바꾸기도쉽다. 예를들어, KDiff3를사용하도록 extdiff와 extmerge 스크립트를수정한다 : $ cat /usr/local/bin/extmerge #!/bin/sh /Applications/kdiff3.app/Contents/MacOS/kdiff3 $* 176

Scott Chacon Pro Git 7.1 절 Git 설정하기 그림 7.1: P4Merge 이제부터 Git은 diff 결과를보여주거나충돌을해결할때 KDiff3 도구를사용한다. 어떤 Merge 도구는 Git에미리 cmd 설정이들어있다. 그래서 cmd 설정없이사용할수있는것도있다. kdiff3, opendiff, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff는 cmd 설정없이 Merge 도구로사용할수있다. diff 도구로는다른것을사용하지만, Merge 도구로는 KDiff3를사용하고싶은경우에는 kdiff3 명령을실행경로로넣고아래와같이설정하기만하면된다 : $ git config --global merge.tool kdiff3 extmerge 와 extdiff 파일을사용하지않고이렇게 Merge 도구만 kdiff3 로설정하고 diff 도구는 Git 에원래들어있는것을사용할수있다. 7.1.4 소스포맷과공백협업할때겪는소스포맷 (Formatting) 과공백문제는미묘하고난해하다. 동료사이에사용하는플랫폼이다를때는특히더심하다. 다른사람이보내온 Patch는공백문자패턴이미묘하게다를확률이높다. 편집기가몰래공백문자를추가해버릴수도있고크로스-플랫폼프로젝트에서윈도개발자가줄끝에 CR(Carriage-Return) 문자를추가해버렸을수도있다. Git에는이이슈를돕는몇가지설정이있다. core.autocrlf 윈도에서개발하는동료와함께일하면줄바꿈 (New Line) 문자에문제가생긴다. 윈도는줄바꿈문자로 CR(Carriage-Return) 과 LF(Line Feed) 문자를둘다사용하지만, Mac과 Linux는 LF 문자만사용한다. 아무것도아닌것같지만, 크로스플랫폼프로젝트에서는꽤성가신문제다. Git은커밋할때자동으로 CRLF를 LF로변환해주고반대로 Checkout할때 LF를 CRLF로변환해주는기능이있다. core.autocrlf 설정으로이기능을켤수있다. 윈도에서이값을 true로설정하면 Checkout할때 LF 문자가 CRLR 문자로변환된다 : $ git config --global core.autocrlf true 177

7 장 Git 맞춤 Scott Chacon Pro Git 줄바꿈문자로 LF 를사용하는 Linux 와 Mac 에서는 Checkout 할때 Git 이 LF 를 CRLF 로변환할필요 가없다. 게다가우연히 CRLF 가들어간파일이저장소에들어있어도 Git 이알아서고쳐주면좋을것이 다. core.autocrlf 값을 input 으로설정하면커밋할때만 CRLF 를 LF 로변환한다 : $ git config --global core.autocrlf input 이설정을이용하면윈도에서는 CRLF 를사용하고 Mac, Linux, 저장소에서는 LF 를사용할수있다. 윈도플랫폼에서만개발하면이기능이필요없다. 이옵션을 false 라고설정하면이기능이꺼지고 CR 문자도저장소에도저장된다 : $ git config --global core.autocrlf false core.whitespace Git에는공백문자를다루는방법으로네가지가미리정의돼있다. 두가지는기본적으로켜져있지만끌수있고나머지두가지는꺼져있지만켤수있다. 먼저기본적으로켜져있는것을살펴보자. trailing-space는각줄끝에공백이있는지찾고 spacebefore-tab은모든줄처음에 tab보다공백이먼저나오는지찾는다. 기본적으로꺼져있는나머지두개는 indent-with-non-tab과 cr-at-eol이다. intent-with-non-tab은 tab이아니라공백 8자이상으로시작하는줄이있는지찾고 cr-at-eol은줄끝에 CR 문자가있어도괜찮다고 Git에알리는것이다. core.whitespace 옵션으로이네가지방법을켜고끌수있다. 설정에서해당옵션을빼버리거나이름이 -로시작하면기능이꺼진다. 예를들어, 다른건다켜고 cr-at-eol 옵션만끄려면아래와같이설정한다 : $ git config --global core.whitespace \ trailing-space,space-before-tab,indent-with-non-tab git diff 명령을실행하면 Git 은이설정에따라검사해서컬러로표시해준다. 그래서좀더쉽게검토 해서커밋할수있다. git apply 명령으로 Patch 를적용할때도이설정을이용할수있다. 아래처럼명 령어를실행하면해당 Patch 가공백문자정책에들어맞는지확인할수있다 : $ git apply --whitespace=warn <patch> 아니면 Git 이자동으로고치도록할수있다 : $ git apply --whitespace=fix <patch> 이옵션은 git rebase 명령에서도사용할수있다. 공백문제가있는커밋을서버로 Push 하기전에 -- whitespace=fix 옵션을주고 Rebase 하면 Git 은다시 Patch 를적용하면서공백을설정한대로고친다. 178

Scott Chacon Pro Git 7.2 절 Git Attribute 7.1.5 서버설정 서버설정은많지않지만, 꼭짚고넘어가야하는것이몇개있다. receive.fsckobjects Git은 Push할때기본적으로개체를검증하지 (check for consistency) 않는다. 하지만, Push할때마다각개체가 SHA-1 체크섬에맞는지, 잘못된개체가가리키고있는지검사하게할수있다. 개체를점검하는것은상대적으로느려서 Push하는시간이늘어난다. 얼마나늘어나는지는저장소크기와 Push하는양에달렸다. receive.fsckobjects 값을 true로설정하면 Git이 Push할때마다검증한다. $ git config --system receive.fsckobjects true 이렇게설정하면 Push 할때마다검증하기때문에클라이언트는잘못된데이터를 Push 하지못한다. receive.denynonfastforwards 이미 Push한커밋을 Rebase해서다시 Push하지못하게할수있다. 브랜치를 Push할때해당리모트브랜치가가리키는커밋이 Push하려는브랜치에없을때 Push하지못하게할수있다. 보통은이런정책이좋고 git push 명령에 -f 옵션을주면강제로 Push할수있다. 하지만, 강제로 Push하지못하게할수도있다. receive.denynonfastforwards 옵션을켜면 Fastforward로 Push할수없는브랜치는아예 Push하지못한다 : $ git config --system receive.denynonfastforwards true 사용자마다다른정책을적용하고싶으면서버훅을사용해야한다. 서버의 receive 훅으로할수있고 이훅도이장에서설명한다. receive.denydeletes receive.denynonfastforwards 와비슷한정책으로 receive.denydeletes 라는것이있다. 이설정을켜면 브랜치를삭제하는 Push 가거절된다. Git 1.6.1 부터 receive.denydeletes 를사용할수있다 : $ git config --system receive.denydeletes true 이제브랜치나 Tag 를삭제하는 Push 는거절된다. 아무도삭제할수없다. 리모트브랜치를삭제하려 면직접손으로 server 의 ref 파일을삭제해야한다. 그리고사용자마다다른정책을적용시키는 ACL 을만드는방법도있다. 이방법은이장끝부분에서다룬다. 7.2 Git Attribute 디렉토리와파일단위로다른설정을적용할수도있다. 이렇게경로별로설정하는것을 Git Attribute 라고부른다. 이설정은.gitattributes라는파일에저장하고아무디렉토리에나둘수있지만, 179

7 장 Git 맞춤 Scott Chacon Pro Git 보통은프로젝트최상위디렉토리에둔다. 그리고이파일을커밋하고싶지않으면.gitattributes가아니라.git/info/attributes로파일을만든다. 이 Attribute로 Merge는어떻게할지, 텍스트가아닌파일은어떻게 Diff할지, checkin/checkout 할때어떻게필터링할지정해줄수있다. 이절에서는설정할수있는 Attribute가어떤것이있는지, 그리고어떻게설정하는지배우고예제를살펴본다. 7.2.1 바이너리파일이 Attribute로어떤파일이바이너리파일인지 Git에게알려줄수있다. 기본적으로 Git은어떤파일이바이너리파일인지알지못한다. 하지만, Git에는파일을어떻게다뤄야하는지알려주는방법이있다. 텍스트파일중에서프로그램이생성하는파일에는바이너리파일과진배없는파일이있다. 이런파일은 diff할수없으니바이너리파일이라고알려줘야한다. 반대로바이너리파일중에서취급방법을 Git에알려주면 diff할수있는파일도있다. 바이너리파일이라고알려주기사실텍스트파일이지만만든목적과의도를보면바이너리파일인것이있다. 예를들어 Mac의 Xcode 는.pbxproj 파일을만든다. 이파일은 IDE 설정등을디스크에저장하는파일로 JSON 포맷이다. 모든것이 ASCII인텍스트파일이지만실제로는간단한데이터베이스이기때문에텍스트파일처럼취급할수없다. 그래서여러명이이파일을동시에수정하고 Merge할때 diff가도움이안된다. 이파일은프로그램이읽고쓰는파일이기때문에바이너리파일처럼취급하는것이옳다. 모든 pbxproj 파일을바이너리로파일로취급하는설정은아래와같다..gitattributes 파일에넣으면된다 : *.pbxproj -crlf -diff 이제 pbxproj 파일은 CRLF 변환이적용되지않는다. git show 나 git diff 같은명령을실행할때에도 통계를계산하거나 diff 를출력하지않는다. Git 1.6 부터는 -crlf -diff 를한마디로줄여서표현할수 있다 : *.pbxproj binary 바이너리파일 Diff하기 Git은바이너리파일도 diff할수있다. Git Attribute를통해 Git이바이너리파일을텍스트포맷으로변환하고그결과를 diff로비교하도록하는것이다. 그래서문제는어떻게바이너리를텍스트로변환해야할까에있다. 바이너리를텍스트로변환해주는도구중에서내가필요한바이너리파일에꼭맞는도구를찾는게가장좋다. 사람이읽을수있는텍스트로표현된바이너리포맷은극히드물다 ( 오디오데이터를텍스트로변환한다고생각해보라 ). 파일내용을텍스트로변환할방법을찾지못했을때는파일의설명이나메타데이터를텍스트로변환하는방법을찾아보자. 이런방법이가능한경우가많다. 메타데이터는파일내용을완벽하게알려주지않지만전혀비교하지못하는것보다이렇게라도하는게훨씬낫다. 여기서설명한두가지방법을많이사용하는바이너리파일에적용해볼거다. 180

Scott Chacon Pro Git 7.2 절 Git Attribute 댓글 : 전용변환기는없지만텍스트가들어있는바이너리포맷들이있다. 이런포맷은 strings 프로그램으로바이너리파일에서텍스트를추출한다. 이런종류의바이너리파일중에서 UTF-16 인코딩이나다른 codepages 로된파일들도있다. 그런인코딩으로된파일에서 strings으로추출할수있는텍스트는제한적이다. 상황에따라다르게추출된다. 그래도 strings는 Mac과 Linux 시스템에서쉽게사용할수있기때문에다양한바이너리파일에쉽게적용할수있다. MS Word 파일먼저이기술을인류에게알려진가장귀찮은문제중하나인 Word 문서를버전관리하는상황을살펴보자. 모든사람이 Word가가장끔찍한편집기라고말하지만애석하게도모두 Word 를사용한다. Git 저장소에넣고이따금커밋하는것만으로도 Word 문서의버전을관리할수있다. 그렇지만 git diff를실행하면다음과같은메시지를볼수있을뿐이다 : $ git diff diff --git a/chapter1.doc b/chapter1.doc index 88839c4..4afcb7c 100644 Binary files a/chapter1.doc and b/chapter1.doc differ 직접파일을하나하나까보지않으면두버전이뭐가다른지알수없다. Git Attribute 를사용하면이 를더좋게개선할수있다..gitattributes 파일에아래와같은내용을추가한다 : *.doc diff=word 이것은 *.doc 파일의두버전이무엇이다른지 diff할때 word 필터를사용하라고설정하는것이다. 그럼 word 필터는뭘까? 이 word 필터도정의해야한다. Word 문서에서사람이읽을수있는텍스트를추출해주는 catdoc 프로그램을 word 필터로사용한다. 그러면 Word 문서를 diff할수있다. (catdoc 프로그램은 MS Word 문서에특화된텍스트추출기다. http://www.wagner.pp.ru/~vitus/ software/catdoc/ 에서구할수있다 ): $ git config diff.word.textconv catdoc 위의명령은아래와같은내용을.git/config 파일에추가한다 : [diff "word"] textconv = catdoc 이제 Git은확장자가.doc인파일의스냅샷을 diff할때 word 필터로정의한 catdoc 프로그램을사용한다. 이프로그램은 Word 파일을텍스트파일로변환해주기때문에 diff할수있다. 이책의 1장을 Word 파일로만들어서 Git에넣고나서단락하나를수정하고저장하는예를살펴보자. git diff를실행하면어디가달려졌는지확인할수있다 : 181

7 장 Git 맞춤 Scott Chacon Pro Git $ git diff diff --git a/chapter1.doc b/chapter1.doc index c1c8a0a..b93c9e4 100644 --- a/chapter1.doc +++ b/chapter1.doc @@ -128,7 +128,7 @@ and data size) Since its birth in 2005, Git has evolved and matured to be easy to use and yet retain these initial qualities. It s incredibly fast, it s very efficient with large projects, and it has an incredible branching -system for non-linear development. +system for non-linear development (See Chapter 3). Git 은 (See Chapter 3) 가추가됐다는것을정확하게찾아준다. OpenDocument 파일 MS Word(*.doc) 파일에사용한방법은 OpenOffice.org( 혹은 LibreOffice.org) 파일형식인 OpenDocument(*.odt) 파일에도적용할수있다. 아래의내용을.gitattributes 파일에추가한다 : *.odt diff=odt.git/config 파일에 odt diff 필터를설정한다 : [diff "odt"] binary = true textconv = /usr/local/bin/odt-to-txt OpenDocument 파일은사실여러파일 (XML, 스타일, 이미지등등 ) 을 Zip 으로압축한형식이다. OpenDocument 파일에서텍스트만추출하는스크립트를하나작성한다. 아래와같은내용을 /usr/ local/bin/odt-to-txt 파일로 ( 다른위치에저장해도상관없다 ) 저장한다 : #! /usr/bin/env perl # Simplistic OpenDocument Text (.odt) to plain text converter. # Author: Philipp Kempgen if (! defined($argv[0])) { print STDERR "No filename given!\n"; print STDERR "Usage: $0 filename\n"; exit 1; } 182

Scott Chacon Pro Git 7.2 절 Git Attribute my $content = ''; open my $fh, '- ', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!; { local $/ = undef; # slurp mode $content = <$fh>; } close $fh; $_ = $content; s/<text:span\b[^>]*>//g; # remove spans s/<text:h\b[^>]*>/\n\n***** /g; # headers s/<text:list-item\b[^>]*>\s*<text:p\b[^>]*>/\n -- /g; # list items s/<text:list\b[^>]*>/\n\n/g; # lists s/<text:p\b[^>]*>/\n /g; # paragraphs s/<[^>]+>//g; # remove all XML tags s/\n{2,}/\n\n/g; # remove multiple blank lines s/\a\n+//; # remove leading blank lines print "\n", $_, "\n\n"; 그리고실행가능하도록만든다 : chmod +x /usr/local/bin/odt-to-txt 이제 git diff 명령으로.odt 파일에대한변화를살펴볼수있다. 이미지파일이방법으로이미지파일도 diff할수있다. 필터로 EXIF 정보를추출해서 PNG 파일을비교한다. EXIF 정보는대부분의이미지파일에들어있는메타데이터다. exiftool이라는프로그램을설치하고이미지파일에서메타데이터텍스트를추출한다. 그리고그결과를 diff해서무엇이달라졌는지본다 : $ echo '*.pngdiff=exif' >>.gitattributes $ git config diff.exif.textconv exiftool 프로젝트에들어있는이미지파일을변경하고 git diff 를실행하면아래와같이보여준다 : diff --git a/image.pngb/image.png index 88839c4..4afcb7c 100644 --- a/image.png +++ b/image.png @@ -1,12 +1,12 @@ 183

7 장 Git 맞춤 Scott Chacon Pro Git ExifTool Version Number : 7.74 -File Size : 70 kb -File Modification Date/Time : 2009:04:17 10:12:35-07:00 +File Size : 94 kb +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha 이미지파일의크기와해상도가달라진것을쉽게알수있다 : 7.2.2 키워드치환 SVN이나 CVS에익숙한사람들은해당시스템에서사용하던키워드치환 (Keyword Expansion) 기능을찾는다. Git에서는이것이쉽지않다. Git은먼저체크섬을계산하고커밋하기때문에그커밋에대한정보를가지고파일을수정할수없다. 하지만, Checkout할때그정보가자동으로파일에삽입되도록했다가다시커밋할때삭제되도록할수있다. 파일안에 $Id$ 필드를넣으면 Blob의 SHA-1 체크섬을자동으로삽입한다. 이필드를파일에넣으면 Git은앞으로 Checkout할때해당 Blob의 SHA-1 값으로교체한다. 여기서꼭기억해야할것이있다. 교체되는체크섬은커밋의것이아니라 Blob 그자체의 SHA-1 체크섬이다 : $ echo '*.txt ident' >>.gitattributes $ echo '$Id$' > test.txt Git 은이파일을 Checkout 할때마다 SHA 값을삽입해준다 : $ rm test.txt $ git checkout -- test.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $ 하지만이것은별로유용하지않다. CVS나 SVN의키워드치환 (Keyword Substitution) 을써봤으면날짜 (Datestamp) 도가능했다는것을알고있을것이다. SHA는그냥해시이고식별할수있을뿐이지다른것을알려주진않는다. SHA만으로는예전것보다새것인지오래된것인지는알수없다. Commit/Checkout할때사용하는필터를직접만들어쓸수있다. 방향에따라 clean 필터와 smudge 필터라고부른다..gitattributes 파일에설정하고파일경로마다다른필터를설정할수 184

Scott Chacon Pro Git 7.2 절 Git Attribute 있다. Checkout 할때파일을처리하는것이 smudge 필터이고 ( 그림 7-2) 커밋할때처리하는필 터가 clean 필터이다. 이필터로할수있는일은무궁무진하다. 그림 7.2: smudge 필터는 Checkout 할때실행된다 그림 7.3: clean 필터는파일을 Stage 할때실행된다 커밋하기전에 indent 프로그램으로 C 코드전부를필터링하지만커밋메시지는단순한예제를보자. *.c 파일은 indent 필터를사용하도록.gitattributes 파일에설정한다 : *.c filter=indent 아래처럼 indent 필터의 smudge 와 clean 이무엇인지설정한다 : $ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat *.c 파일을커밋하면 indent 프로그램을통해서커밋되고 Checkout하면 cat 프로그램을통해 Checkout된다. cat은입력된데이터를그대로다시내보내는, 사실아무것도안하는프로그램이다. 이렇게설정하면모든 C 소스파일은 indent 프로그램을통해커밋된다. 이제 RCS 처럼 $Date$ 를치환하는예제을살펴보자. 이것를하려면간단한스크립트가하나필요하다. 이스크립트는 $Date$ 필드를프로젝트의마지막커밋일자로치환한다. 표준입력을읽어서 $Date$ 필드를치환한다. 아래는 Ruby로구현한스크립트다 : 185

7 장 Git 맞춤 Scott Chacon Pro Git #! /usr/bin/env ruby data = STDIN.read last_date = `git log --pretty=format:"%ad" -1` puts data.gsub('$date$', '$Date: ' + last_date.to_s + '$') git log 명령으로마지막커밋정보를얻고표준입력 (STDIN) 에서 $Date$ 스트링을찾아서치환한다. 스크립트는자신이편한언어로만든다. 이스크립트의이름을 expand_date라고짓고실행경로에넣는다. 그리고 dater라는 Git 필터를정의한다. Checkout시실행하는 smudge 필터로 expand_date를사용하고커밋할때실행하는 clean 필터는 Perl을사용한다 : $ git config filter.dater.smudge expand_date $ git config filter.dater.clean 'perl -pe "s/\\\$date[^\\\$]*\\\$/\\\$date\\\$/"' 이 Perl 코드는 $Date$ 스트링에있는문자를제거해서원래대로복원한다. 이제필터가준비됐으니 $Date$ 키워드가들어있는파일을만들고 Git Attribute 를설정한다. 새필터를시험해보자 : $ echo '# $Date$' > date_test.txt $ echo 'date*.txt filter=dater' >>.gitattributes 커밋하고파일을다시 Checkout 하면해당키워드가적절히치환된것을볼수있다 : $ git add date_test.txt.gitattributes $ git commit -m "Testing date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009-0700$ 이것은매우강력해서두루두루사용할수있다..gitattributes 파일은커밋하는파일이기때문에드 라이버 ( 여기서는 dater) 가없는사람에게도배포된다. 그리고 dater 가없으면에러가난다. 필터를만 들때이런예외상황도고려해서항상잘동작하게해야한다. 7.2.3 저장소익스포트하기 프로젝트를익스포트해서아카이브를만들때에도 Git Attribute 가유용하다. export-ignore 아카이브를만들때제외할파일이나디렉토리가무엇인지설정할수있다. 특정디렉토리나파일을프로젝트에는포함하고아카이브에는포함하고싶지않을때 export-ignore Attribute를사용한다. 186

Scott Chacon Pro Git 7.2 절 Git Attribute 예를들어 test/ 디렉토리에테스트파일이있다고하자. 보통 tar 파일로묶어서익스포트할때테스트 파일은포함하지않는다. Git Attribute 파일에다음라인을추가하면테스트파일은무시된다 : test/ export-ignore git archive 명령으로 tar 파일을만들면 test 디렉토리는아카이브에포함되지않는다. export-subst 아카이브를만들때에도키워드치환을할수있다. 파일을하나만들고거기에 $Format:$ 스트링을넣으면 Git이치환해준다. 이스트링에 --pretty=format 옵션에사용하는것과같은포맷코드를넣을수있다. --pretty=format은 2장에서배웠다. 예를들어 LAST_COMMIT이라는파일을만들고 git archive 명령을실행할때자동으로이파일에마지막커밋날짜가삽입되게하려면아래와같이해야한다 : $ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT $ echo "LAST_COMMIT export-subst" >>.gitattributes $ git add LAST_COMMIT.gitattributes $ git commit -am 'adding LAST_COMMIT file for archives' git archive 명령으로아카이브를만들고나서이파일을열어보면아래와같이보인다 : $ cat LAST_COMMIT Last commit date: $Format:Tue Apr 21 08:38:48 2009-0700$ 7.2.4 Merge 전략파일마다다른 Merge 전략을사용하도록설정할수있다. Merge할때충돌이날것같은파일이있다고하자. Git Attrbute로이파일만항상타인의코드말고내코드를사용하도록설정할수있다. 이설정은다양한환경에서운영하려고만든환경브랜치를 Merge할때좋다. 이때는환경설정과관련된파일은 Merge하지않고무시하는게편리하다. 브랜치에 database.xml이라는데이터베이스설정파일이있는데이파일은브랜치마다다르다. Database 설정파일은 Merge하면안된다. Attribute 를아래와같이설정하면이파일은그냥두고 Merge한다. database.xml merge=ours 이제 Merge 해도 database.xml 파일은충돌하지않는다 : $ git merge topic Auto-merging database.xml Merge made by recursive. 187

7 장 Git 맞춤 Scott Chacon Pro Git Merge 했지만 database.xml 은원래가지고있던파일그대로다. 7.3 Git 훅 Git도다른버전관리시스템처럼어떤이벤트가생겼을때자동으로특정스크립트를실행하도록할수있다. 이훅은클라이언트훅과서버훅으로나눌수있다. 클라이언트훅은커밋이나 Merge할때실행되고서버훅은 Push할때서버에서실행된다. 이절에서는어떤훅이있고어떻게사용하는지배운다. 7.3.1 훅설치하기훅은 Git 디렉토리밑에 hooks라는디렉토리에저장한다. 기본훅디렉토리는.git/hooks이다. 이디렉토리에가보면 Git이자동으로넣어준매우유용한스크립트예제가몇개있다. 그리고스크립트가입력받는값이어떤값인지파일안에자세히설명돼있다. 모든예제는쉘과 Perl 스크립트로작성돼있지만실행할수만있으면되고 Ruby나 Python같은다른스크립트언어로만들어도된다. 예제스크립트의파일이름에는.sample이라는확장자가붙어있다. 그래서이름만바꿔주면그훅을사용할수있다. 실행할수있는스크립트파일을저장소의 hooks 디렉토리에넣으면훅스크립트가켜진다. 이스크립트는앞으로계속호출된다. 중요한훅은여기서모두설명한다. 7.3.2 클라이언트훅 클라이언트훅은매우다양하다. 이절에서는클라이언트훅을커밋 Workflow 훅, E-mail Workflow 훅, 그리고나머지로분류해서설명한다. 커밋 Workflow 훅먼저커밋과관련된훅을살펴보자. 커밋과관련된훅은모두네가지다. pre-commit 훅은커밋할때가장먼저호출되는훅으로커밋메시지를작성하기전에호출된다. 이훅에서커밋하는 Snapshot을점검한다. 빠트린것은없는지, 테스트는확실히했는지등을검사한다. 커밋할때꼭확인해야할게있으면이훅으로확인한다. 그리고이훅의 Exit 코드가 0이아니면커밋은취소된다. 물론 git commit -- no-verify라고실행하면이훅을일시적으로생략할수있다. lint 같은프로그램으로코드스타일을검사하거나, 줄끝의공백문자를검사하거나 ( 예제로들어있는 pre-commit 훅이하는게이일이다 ), 코드에주석을달았는지검사하는일은이훅으로하는것이좋다. prepare-commit-msg 훅은 Git이커밋메시지를생성하고나서편집기를실행하기전에실행된다. 이훅은사람이커밋메시지를수정하기전에먼저프로그램으로손보고싶을때사용한다. 이훅은커밋메시지가들어있는파일의경로, 커밋의종류를아규먼트로받는다. 그리고최근커밋을수정할때에는 (Amending 커밋 ) SHA-1 값을추가아규먼트로더받는다. 사실이훅은일반커밋에는별로필요없고커밋메시지를자동으로생성하는커밋에좋다. 커밋메시지에템플릿을적용하거나, Merge 커밋, Squash 커밋, Amend 커밋일때유용하다. 이스크립트로커밋메시지템플릿에정보를삽입할수있다. commit-msg 훅은커밋메시지가들어있는임시파일의경로를아규먼트로받는다. 그리고이스크립트가 0이아닌값을반환하면커밋되지않는다. 이훅에서최종적으로커밋이완료되기전에프로젝트상태나커밋메시지를검증한다. 이장의마지막절에서이훅을사용하는예제를보여준다. 커밋메시지가정책에맞는지검사하는스크립트를만들어보자. 커밋이완료되면 post-commit 훅이실행된다. 이훅은넘겨받는아규먼트가하나도없지만 git log -1 HEAD 명령으로정보를쉽게가져올수있다. 일반적으로이스크립트는커밋된것을누군가에게알릴때사용한다. 188

Scott Chacon Pro Git 7.3 절 Git 훅 이커밋 Workflow 스크립트는어떤 Workflow에나사용할수있다. 특히정책을강제할때유용하다. 클라이언트훅은개발자가클라이언트에서사용하는훅이다. 모든개발자에게유용한훅이지만 Clone 할때복사되지않는다. 그래서직접설치하고관리해야한다. 물론정책을서버훅으로만들고정책을잘지키는지 Push할때검사해도된다. E-mail Workflow 훅 E-mail Workflow에해당하는클라이언트훅은세가지이다. 이훅은모두 git am 명령으로실행된다. 이명령어를사용할일이없으면이절은읽지않아도된다. 하지만, 언젠가는 git format-patch 명령으로만든 Patch를 E-mail로받는날이올지도모른다. 제일먼저실행하는훅은 applypatch-msg이다. 이훅의아규먼트는 Author가보내온커밋메시지파일의이름이다. 이스크립트가종료할때 0이아닌값을반환하면 Git은 Patch하지않는다. 커밋메시지가규칙에맞는지확인하거나자동으로메시지를수정할때이훅을사용한다. git am으로 Patch할때두번째로실행되는훅이 pre-applypatch이다. 이훅은아규먼트가없고단순히 Patch를적용하고나서실행된다. 그래서커밋할스냅샷을검사하는데사용한다. 이스크립트로테스트를수행하고파일을검사할수있다. 테스트에실패하거나뭔가부족하면 0이아닌값을반환시켜서 git am 명령을취소시킬수있다. git am 명령에서마지막으로실행되는훅은 post-applypatch다. 이스크립트를이용하면자동으로 Patch를보낸사람이나그룹에게알림메시지를보낼수있다. 이스크립트로는 Patch를중단시킬수없다. 기타훅 pre-rebase 훅은 Rebase하기전에실행된다. 이훅이 0이아닌값을반환하면 Rebase가취소된다. 이훅으로이미 Push한커밋을 Rebase하지못하게할수있다. Git이자동으로넣어주는 pre-rebase 예제가바로그예제다. 이예제에는기준브랜치가 next라고돼있다. 실제로적용할브랜치이름으로사용하면된다. 그리고 git checkout 명령이끝나면 post-checkout 훅이실행된다. 이훅은 Checkout할때마다작업하는디렉토리에서뭔가할일이있을때사용한다. 그러니까용량이크거나 Git이관리하지않는파일을옮기거나, 문서를자동으로생성하는데쓴다. 마지막으로, post-merge 훅은 Merge가끝나고나서실행된다. 이훅은파일권한같이 Git이추적하지않는정보를관리하는데사용한다. Merge로 Working Tree가변경될때 Git이관리하지않는파일이원하는대로잘배치됐는지검사할때도좋다. 7.3.3 서버훅클라이언트훅으로도어떤정책을강제할수있지만, 시스템관리자에게는서버훅이더중요하다. 서버훅은모두 Push 전후에실행된다. Push 전에실행되는훅이 0이아닌값을반환하면해당 Push는거절되고클라이언트는에러메시지를출력한다. 이훅으로아주복잡한 Push 정책도가능하다. pre-receive와 post-receive Push하면가장처음실행되는훅은 pre-receive 훅이다. 이스크립트는표준입력 (STDIN) 으로 Push하는레퍼런스의목록을입력받는다. 0이아닌값을반환하면해당레퍼런스가전부거절된다. Fast-forward Push가아니면거절하거나, 브랜치 Push 권한을제어하려면이훅에서하는것이좋다. 관리자만브랜치를새로 Push하고삭제할수있고일반개발자는수정사항만 Push할수있게할수있다. 189

7 장 Git 맞춤 Scott Chacon Pro Git post-receive 훅은 Push한후에실행된다. 이훅으로사용자나서비스에알림메시지를보낼수있다. 그리고 pre-receive 훅처럼표준입력 (STDIN) 으로레퍼런스목록이넘어간다. 이훅으로메일링리스트에메일을보내거나, CI(Continuous Integration) 서버나 Ticket-tracking 시스템의정보를수정할수있다. 심지어커밋메시지도파싱할수있기때문에이훅으로 Ticket을만들고, 수정하고, 닫을수있다. 이스크립트가완전히종료할때까지클라이언트와의연결은유지되고 Push를중단시킬수없다. 그래서이스크립트로시간이오래걸릴만한일을할때는조심해야한다. update update 스크립트는각브랜치마다한번씩실행된다는것을제외하면 pre-receive 스크립트와거의같다. 한번에브랜치를여러개 Push하면 pre-receive는딱한번만실행되지만, update는브랜치마다실행된다. 이스크립트는표준입력으로데이터를입력받는것이아니라아규먼트로브랜치이름, 원래가리키던 SHA-1 값, 사용자가 Push하는 SHA-1 값을입력받는다. update 스크립트가 0이아닌값을반환하면해당레퍼런스만거절되고나머지다른레퍼런스는상관없다. 7.4 정책구현하기 지금까지배운것을한번적용해보자. 커밋메시지규칙검사하고, Fast-forward Push만허용하고, 디렉토리마다사용자의수정권한을제어하는 Workflow를만든다. 실질적으로정책을강제하려면서버훅으로만들어야한다. 하지만, 개발자들이 Push할수없는커밋은아예만들지않도록클라이언트훅도만든다. 필자가제일좋아하는 Ruby로만든다. 필자는독자가슈도코드를읽듯이 Ruby 코드를읽을수있다고생각한다. Ruby를모르더라도충분히개념을이해할수있을것이다. 하지만, Git은언어를가리지않는다. Git이자동으로생성해주는예제는모두 Perl과 Bash로작성돼있다. 그래서예제를열어보면 Perl과 Bash로작성된예제를참고할수있다. 7.4.1 서버훅서버정책은전부 update 훅으로만든다. 이스크립트는브랜치가 Push될때마다한번실행되고해당브랜치의이름, 원래브랜치가가리키던레퍼런스, 새레퍼런스를아규먼트로받는다. 그리고 SSH를통해서 Push하는것이라면누가 Push하는지도알수있다. SSH로접근하긴하지만개발자모두계정하나로 ( git 같은 ) Push하고있다면실제로 Push하는사람이누구인지판별해주는쉘 Wrapper가필요하다. 이스크립트에서는 $USER 환경변수에현재접속한사용자정보가있다고가정한다. update 스크립트는필요한정보를수집하는것으로시작한다 : #!/usr/bin/env ruby $refname = ARGV[0] $oldrev = ARGV[1] $newrev = ARGV[2] $user = ENV['USER'] puts "Enforcing Policies... \n(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})" 쉽게설명하기위해전역변수를사용했다. 비판하지말기바란다. 190

Scott Chacon Pro Git 7.4 절정책구현하기 커밋메시지규칙만들기커밋메시지규칙부터해보자. 일단목표가있어야하니까커밋메시지에 ref: 1234 같은스트링이포함돼있어야한다고가정하자. 보통커밋은이슈트래커에있는이슈와관련돼있으니그이슈가뭔지커밋메시지에적어놓으면좋다. Push할때마다커밋메시지에해당스트링이포함돼있는지확인한다. 만약커밋메시지에해당스트링이없는커밋이면 0이아닌값을반환해서 Push를거절한다. $newrev, $oldrev 변수와 git rev-list라는 Plumbing 명령어를이용해서 Push하는커밋의모든 SHA-1 값을알수있다. 이것은 git log와근본적으로같은명령이고옵션을하나도주지않으면다른정보없이 SHA-1 값만보여준다. 이명령으로 Push하는커밋을모두알수있다 : $ git rev-list 538c33..d14fc7 d14fc7c847ab946ec39590d87783c69b031bdfb7 9f585da4401b0a3999e84113824d15245c13f0be 234071a1be950e2a8d078e6141f5cd20c1e61ad3 dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475 이 SHA-1 값으로각커밋의메시지도가져온다. 커밋메시지를가져와서정규표현식으로해당패턴이있는지검사한다. 커밋메시지를얻는방법을알아보자. 커밋의 raw 데이터는 git cat-file이라는 Plumbing 명령어로얻을수있다. 9장에서 Plumbing 명령어에대해자세히다루니까지금은커밋메시지얻는것에집중하자 : $ git cat-file commit ca82a6 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon <schacon@gmail.com> 1205815931-0700 committer Scott Chacon <schacon@gmail.com> 1240030591-0700 changed the version number 이명령이출력하는메시지에서커밋메시지만잘라내야한다. 첫번째빈줄다음부터가커밋메시지 니까유닉스명령어 sed 로첫빈줄이후를잘라낸다. $ git cat-file commit ca82a6 sed '1,/^$/d' changed the version number 이제커밋메시지에서찾는패턴과일치하는문자열이있는지검사해서있으면통과시키고없으면거절한다. 스크립트가종료할때 0이아닌값을반환하면 Push가거절된다. 이일을하는코드는아래와같다 : 191

7 장 Git 맞춤 Scott Chacon Pro Git $regex = /\[ref: (\d+)\]/ # enforced custom commit message format def check_message_format missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") missed_revs.each do rev message = `git cat-file commit #{rev} sed '1,/^$/d'` if!$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end end end check_message_format 이코드를 update 스크립트에넣으면규칙을어긴커밋은 Push 할수없다. ACL로사용자마다다른규칙적용하기진행하는프로젝트에모듈이여러개있는데, 모듈마다속한사용자들만 Push할수있게설정해야한다고가정하자. 모든권한을다가진사람들도있고특정디렉토리나파일만 Push할수있는사람도있다. 이런일을강제하려면먼저서버의 Bare 저장소에 acl이라는파일을만들고거기에규칙을기술한다. 그리고 update 훅에서 Push하는파일이무엇인지확인하고 ACL과비교해서 Push할수있는지없는지결정한다. 우선 ACL부터작성한다. CVS에서사용하는것과비슷한 ACL을만든다. 규칙은한줄에하나씩기술한다. 각줄의첫번째필드는 avail이나 unavail이고두번째필드는규칙을적용할사용자들의목록을 CSV(Comma-Separated Values) 형식으로적는다. 마지막필드엔규칙을적용할경로를적는다. 만약마지막필드가비워져있으면모든경로를의미한다. 이필드는파이프 ( ) 문자로구분한다. 관리자도여러명이고, doc 디렉토리에서문서를만드는사람도여러명이다. 하지만 lib과 tests 디렉토리에접근하는사람은한명이다. 이런상황을 ACL로만들면아래와같다 : avail nickh,pjhyett,defunkt,tpw avail usinclair,cdickens,ebronte doc avail schacon lib avail schacon tests 이 ACL 정보는스크립트에서읽어사용한다. 설명을쉽게하고자여기서는 avail 만처리한다. 다음메 소드는 Associative Array 를반환하는데, 키는사용자이름이고값은사용자가 Push 할수있는경로 의목록이다 : def get_acl_access_data(acl_file) 192

Scott Chacon Pro Git 7.4 절정책구현하기 # read in ACL data acl_file = File.read(acl_file).split("\n").reject { line line == '' } access = {} acl_file.each do line avail, users, path = line.split(' ') next unless avail == 'avail' users.split(',').each do user access[user] = [] access[user] << path end end access end 이함수가 ACL 파일을처리하고나서반환하는결과는아래와같다 : {"defunkt"=>[nil], "tpw"=>[nil], "nickh"=>[nil], "pjhyett"=>[nil], "schacon"=>["lib", "tests"], "cdickens"=>["doc"], "usinclair"=>["doc"], "ebronte"=>["doc"]} 바로사용할수있는권한정보를만들었다. 이제 Push하는파일을그사용자가 Push할수있는지없는지알아내야한다. git log 명령에 --name-only 옵션을주면해당커밋에서수정된파일이뭔지알려준다. git log 명령은 2장에서다루었다 : $ git log -1 --name-only --pretty=format:'' 9f585d README lib/test.rb get_acl_access_data 메소드를호출해서 ACL 정보를구하고, 각커밋에들어있는파일목록도얻은다 음에, 사용자가모든커밋을 Push 할수있는지판단한다 : # only allows certain users to modify certain subdirectories in a project def check_directory_perms 193

7 장 Git 맞춤 Scott Chacon Pro Git access = get_acl_access_data('acl') # see if anyone is trying to push something they can't new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") new_commits.each do rev files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n") files_modified.each do path next if path.size == 0 has_file_access = false access[$user].each do access_path if!access_path # user has access to everything (path.index(access_path) == 0) # access to this path has_file_access = true end end if!has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end end check_directory_perms 어렵지않다. 먼저 git rev-list 명령으로서버에 Push하려는커밋이무엇인지알아낸다. 그리고각커밋에서수정한파일이어떤것들이있는지찾고, 해당사용자가모든파일에대한권한이있는지확인한다. Rubyism 철학에따르면 path.index(access_path) == 0이란표현은불명확하다. 이표현은해당파일의경로가 access_path로시작할때참이라는뜻이다. 그러니까 access_path가단순히허용된파일하나를의미하는것이아니라 access_path로시작하는모든파일을의미한다. 이제사용자는메시지규칙을어겼거나권한이없는파일이포함된커밋은어떤것도 Push하지못한다. Fast-Forward Push만허용하기이제 Fast-forward Push가아니면거절되게해보자. receive.denydeletes와 receive.denynonfastforwards 설정으로간단하게거절할수있다. 하지만, 그이전버전에는꼭훅으로구현해야했다. 게다가특정사용자만제한하거나허용하려면훅으로구현해야한다. 기존에있던커밋이 Push하는브랜치에없으면 Fast-forward Push가아니라고판단한다. 커밋하나라도없으면거절하고모두있으면 Fast-forward Push이므로그대로둔다 : # enforces fast-forward only pushes def check_fast_forward 194

Scott Chacon Pro Git 7.4 절정책구현하기 missed_refs = `git rev-list #{$newrev}..#{$oldrev}` missed_ref_count = missed_refs.split("\n").size if missed_ref_count > 0 puts "[POLICY] Cannot push a non fast-forward reference" exit 1 end end check_fast_forward 이정책을다구현해서 update 스크립트에넣고 chmod u+x.git/hooks/update 명령으로실행권한을 준다. 그리고나서 -f 옵션을주고강제로 Push 하면아래와같이실패한다 : $ git push -f origin master Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 323 bytes, done. Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. Enforcing Policies... (refs/heads/master) (8338c5) (c5b616) [POLICY] Cannot push a non-fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master To git@gitserver:project.git! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git' 정책과관련해하나씩살펴보자. 먼저훅이실행될때마다다음메시지가출력된다. Enforcing Policies... (refs/heads/master) (fb8c72) (c56860) 이것은 update 스크립트맨윗부분에서표준출력 (STDOUT) 에출력한내용이다. 스크립트에서표준 출력으로출력하면클라이언트로전송된다. 이점을꼭기억하자. 그리고아래의에러메시지를보자 : [POLICY] Cannot push a non fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master 195

7 장 Git 맞춤 Scott Chacon Pro Git 첫번째줄은스크립트에서직접출력한것이고나머지두줄은 Git 이출력해주는것이다. 이메시지는 update 스크립트에서 0 이아닌값을반환해서 Push 할수없다는메시지다. 그리고마지막메시지를 보자 : To git@gitserver:project.git! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git' 이메시지는훅에서거절된것이라고말해주는것이고브랜치가거부될때마다하나씩출력된다. 게다가 Push 하는커밋에커밋메시지규칙을지키지않은것이하나라도있으면아래와같은에러메 시지를보여준다 : [POLICY] Your message is not formatted correctly 그리고누군가권한이없는파일을수정해서 Push 해도에러메시지를출력한다. 예를들어문서담당 자가 lib 디렉토리에있는파일을수정해서커밋하면아래와같은메시지가출력된다 : [POLICY] You do not have access to push to lib/test.rb 이제서버훅은다만들었다. 앞으로는 update 스크립트가항상실행될것이기때문에저장소를되돌 릴수없고, 커밋메시지도규칙대로작성해야하고, 권한이있는파일만 Push 할수있다. 7.4.2 클라이언트훅서버훅의단점은 Push할때까지 Push할수있는지없는지알수없다는데있다. 기껏공들여정성껏구현했는데막상 Push할수없으면곤혹스럽다. 히스토리를제대로고치는일은정신건강에매우해롭다. 이문제는클라이언트훅으로해결한다. 클라이언트훅으로서버가거부할지말지검사한다. 사람들은커밋하기전에, 그러니까시간이지나고치기어려워지기전에문제를해결할수있다. Clone할때이훅은전송되지않기때문에다른방법으로동료에게배포해야한다. 그훅을가져다.git/hooks 디렉토리에복사하고실행할수있게만든다. 이훅파일을프로젝트에넣어서배포해도되고 Git 훅프로젝트를만들어서배포해도된다. 하지만, 자동으로설치하는방법은없다. 커밋메시지부터검사해보자. 이훅이있으면커밋메시지가구리다고서버가뒤늦게거절하지않는다. 이것은 commit-msg 훅으로구현한다. 이훅은커밋메시지가저장된파일을첫번째아규먼트로입력받는다. 그파일을읽어패턴을검사한다. 필요한패턴이없으면커밋을중단시킨다 : #!/usr/bin/env ruby message_file = ARGV[0] message = File.read(message_file) 196

Scott Chacon Pro Git 7.4 절정책구현하기 $regex = /\[ref: (\d+)\]/ if!$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end 이스크립트를.git/hooks/commit-msg 라는파일로만들고실행권한을준다. 커밋이메시지규칙을어 기면아래와같은메시지를보여준다 : $ git commit -am 'test' [POLICY] Your message is not formatted correctly 커밋하지못했다. 하지만, 커밋메지시를바르게작성하면커밋할수있다 : $ git commit -am 'test [ref: 132]' [master e05c914] test [ref: 132] 1 files changed, 1 insertions(+), 0 deletions(-) 그리고아예권한이없는파일을수정못하게할때는 pre-commit 훅을이용한다. 사전에.git 디렉토리 안에 ACL 파일을가져다놓고아래와같이작성한다 : #!/usr/bin/env ruby $user = ENV['USER'] # [ insert acl_access_data method from above ] # only allows certain users to modify certain subdirectories in a project def check_directory_perms access = get_acl_access_data('.git/acl') files_modified = `git diff-index --cached --name-only HEAD`.split("\n") files_modified.each do path next if path.size == 0 has_file_access = false access[$user].each do access_path if!access_path (path.index(access_path) == 0) has_file_access = true end 197

7 장 Git 맞춤 Scott Chacon Pro Git if!has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end check_directory_perms 내용은서버훅과똑같지만두가지가다르다. 첫째, 클라이언트훅은 Git 디렉토리가아니라워킹디렉 토리에서실행하기때문에 ACL 파일위치가다르다. 그래서 ACL 파일경로를수정해야한다 : access = get_acl_access_data('acl') 이부분을아래와같이바꾼다 : access = get_acl_access_data('.git/acl') 두번째차이점은파일목록을얻는방법이다. 서버훅에서는커밋에있는파일을모두찾았지만여기 서는아직커밋하지도않았다. 그래서 Staging Area 의파일목록을이용한다 : files_modified = `git log -1 --name-only --pretty=format:'' #{ref}` 이부분을아래와같이바꾼다 : files_modified = `git diff-index --cached --name-only HEAD` 이두가지점만다르고나머지는똑같다. 보통은리모트저장소의계정과로컬의계정도같다. 다른계정을사용하려면 $user 환경변수에누군지알려야한다. Fast-forward Push인지확인하는일이남았다. 보통은 Fast-forward가아닌 Push는좀드물다. Fast-forward가아닌 Push를하려면 Rebase로이미 Push한커밋을바꿔버렸거나전혀다른로컬브랜치를 Push하는경우다. 어쨌든이서버는 Fast-forward Push만허용하기때문에이미 Push한커밋을수정했다면그건아마실수로그랬을것이다. 이실수를막는훅을살펴보자. 아래는이미 Push한커밋을 Rebase하지못하게하는 pre-rebase 스크립트다. 이스크립트는먼저 Rebase할커밋목록을구하고커밋이리모트레퍼런스 / 브랜치에들어있는지확인한다. 커밋이한개라도리모트레퍼런스 / 브랜치에들어있으면 Rebase할수없다 : 198

Scott Chacon Pro Git 7.5 절요약 #!/usr/bin/env ruby base_branch = ARGV[0] if ARGV[1] topic_branch = ARGV[1] else topic_branch = "HEAD" end target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n") remote_refs = `git branch -r`.split("\n").map { r r.strip } target_shas.each do sha remote_refs.each do remote_ref shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}` if shas_pushed.split("\n").include?(sha) puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}" exit 1 end end end 이스크립트는 6 장 리비전조회하기 절에서설명하지않은표현을사용했다. 아래의표현은이미 Push 한커밋목록을얻어오는부분이다 : git rev-list ^#{sha}^@ refs/remotes/#{remote_ref} SHA @ 은해당커밋의모든부모를가리킨다. 그러니까이명령은지금 Push하려는커밋에서리모트저장소의커밋에도달할수있는지확인하는명령이다. 즉, Fast-forward인지확인하는것이다. 이방법은매우느리고보통은필요없다. 어차피 Fast-forward가아닌 Push은 -f 옵션을주어야 Push할수있다. 문제가될만한 Rebase를방지할수있다는것을보여주려고이예제를설명했다. 7.5 요약 Git을프로젝트에맞추는방법을배웠다. 주요한서버 / 클라이언트설정방법, 파일단위로설정하는 Git Attributes, 이벤트훅, 정책을강제하는방법을배웠다. 이제필요한 Workflow를만들고 Git을거기에맞게설정할수있을것이다. 199

8 장 Git 으로이전하기 Git은완벽하지않다. 프로젝트를전부 Git으로옮기기는어렵다. 프로젝트가특정 VCS 시스템에매우의존적으로개발됐을수도있다. 보통은 Subversion에의존적이다. 이번장은 git svn이라는 Git 과 Subversion을양방향으로이어주는도구를알아보며시작한다. 언젠가이미존재하는프로젝트환경을 Git으로변경하고싶게될것이다. 이장의나머지부분에서프로젝트를 Git으로변경하는방법에대해다룰것이다. 먼저 Subversion에서프로젝트를옮겨오는방법을설명하고그다음에는 Perforce, 그리고스크립트를직접만들어서잘쓰지않는 VCS에서도프로젝트를옮기는방법을다룰것이다. 8.1 Git 과 Subversion 현재도많은오픈소스프로젝트와수많은기업프로젝트는 Subversion으로소스코드를관리한다. 10여년간 Subversion이가장인기있는오픈소스 VCS 도구였다. Subversion은그이전시대에서가장많이사용하던 CVS와많이닮았다. Git이자랑하는또하나의기능은 git svn이라는양방향 Subversion 지원도구이다. Git을 Subversion 클라이언트로사용할수있기때문에로컬에서는 Git의기능을활용하고 Push 할때는 Subversion 서버에 Push한다. 로컬브랜치와 Merge, Staging Area, Rebase, Cherry-pick 등의 Git 기능을충분히사용할수있다. 같이일하는동료는빛한줄기없는선사시대동굴에서일하겠지만말이다. git svn은기업에서 git을사용할수있도록돕는출발점이다. 우리가 Git을도입하기위해기업내에서노력하는동안동료가효율적으로환경을바꿀수있도록도움을줄수잇다. Subversion 지원도구는우리를 DVCS 세상으로인도하는붉은알약과같은것이다. 8.1.1 git svn Git과 Subversion을이어주는명령은 git svn 으로시작한다. 이명령뒤에추가하는명령이몇가지더있으며간단한예제를보여주고설명한다. git svn 명령을사용할때는절름발이인 Subversion을사용하고있다는점을염두하자. 우리가로컬브랜치와 Merge를맘대로쓸수있다고하더라도최대한일직선으로히스토리를유지하는것이좋다. Git 저장소처럼사용하지않는다. 히스토리를재작성해서 Push하지말아야한다. Git을사용하는동료들끼기따로 Git 저장소에 Push 하지도말아야한다. Subversion은단순하게일직선히스토리만가능하다. 팀원중일부는 SVN을사용하고일부는 Git을사용하는팀이라면 SVN Server를사용해서협업하는것이좋다. 그래야삶이편해진다. 201

8 장 Git 으로이전하기 Scott Chacon Pro Git 8.1.2 설정하기 git svn을사용하려면 SVN 저장소가하나필요하다. 저장소에쓰기권한이있어야한다. 필자의 test 저장소를복사한다. Subversion(1.4 이상 ) 에포함된 svnsync라는도구를사용하여 SVN 저장소를복사한다. 테스트용저장소가필요해서 Google Code에새로 Subversion 저장소를하나만들었다. protobuf 라는프로젝트의일부코드를복사했다. protobuf는네트워크전송에필요한구조화된데이터 ( 프로토콜같은것들 ) 의인코딩을도와주는도구이다. 로컬 Subversion 저장소를하나만든다 : $ mkdir /tmp/test-svn $ svnadmin create /tmp/test-svn 그리고모든사용자가 revprops 속성을변경할수있도록항상 0 을반환하는 pre-revprop-change 스크립트를준비한다 ( 역주 : 파일이없거나, 다른이름으로되어있을수있다. 이경우아래내용으로새 로파일을만들고실행권한을준다 ): $ cat /tmp/test-svn/hooks/pre-revprop-change #!/bin/sh exit 0; $ chmod +x /tmp/test-svn/hooks/pre-revprop-change 이제 svnsync init 명령으로다른 Subversion 저장소를로컬로복사할수있도록지정한다 : $ svnsync init file:///tmp/test-svn http://progit-example.googlecode.com/svn/ 이렇게다른저장소의주소를설정하면복사할준비가된다. 아래명령으로저장소를실제로복사한다 : $ svnsync sync file:///tmp/test-svn Committed revision 1. Copied properties for revision 1. Committed revision 2. Copied properties for revision 2. Committed revision 3.... 이명령은몇분걸리지않는다. 저장하는위치가로컬이아니라리모트서버라면오래걸린다. 커밋이 100개이하라고해도오래걸린다. Subversion은한번에커밋을하나씩받아서 Push하기때문에엄청나게비효율적이다. 하지만, 저장소를복사하는다른방법은없다. 202

Scott Chacon Pro Git 8.1 절 Git 과 Subversion 8.1.3 시작하기이제갖고놀 Subversion 저장소를하나준비했다. git svn clone 명령으로 Subversion 저장소전체를 Git 저장소로가져온다. 만약 Subversion 저장소가로컬에있는것이아니라리모트서버에있으면 file:///tmp/test-svn 부분에서버저장소의 URL을적어준다. $ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags Initialized empty Git repository in /Users/schacon/projects/testsvnsync/svn/.git/ r1 = b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk) A m4/acx_pthread.m4 A m4/stl_hash.m4... r75 = d1957f3b307922124eec6314e15bcda59e3d9610 (trunk) Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn /branches/my-calc-branch, 75 Found branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610 Following parent with do_switch Successfully followed parent r76 = 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch) Checked out HEAD: file:///tmp/test-svn/branches/my-calc-branch r76 이명령은사실 SVN 저장소주소를주고 git svn init과 git svn fetch 명령을순서대로실행한것과같다. 이명령은시간이좀걸린다. 테스트용프로젝트는커밋이 75개정도밖에안되서시간이많이걸리지않는다. Git은커밋을한번에하나씩일일이기록해야한다. 커밋이수천개인프로젝트라면몇시간혹은몇일이걸릴수도있다. -T trunk -b branches -t tags 부분은 Subversion이어떤브랜치구조를가지고있는지 Git에게알려주는부분이다. Subversion 표준형식과다르면이옵션부분에서알맞은이름을지정해준다. 표준형식을사용한다면간단하게 -s 옵션을사용한다. 즉아래의명령도같은의미이다. $ git svn clone file:///tmp/test-svn -s Git 에서브랜치와태그정보가제대로보이는것을확인한다 : $ git branch -a * master my-calc-branch tags/2.0.2 tags/release-2.0.1 tags/release-2.0.2 tags/release-2.0.2rc1 trunk 203

8 장 Git 으로이전하기 Scott Chacon Pro Git git svn 도구가리모트브랜치의이름을어떻게짓는지알아야한다. Git 저장소를 Clone할때는보통 origin/[branch] 처럼리모트저장소이름이들어간브랜치이름으로만들어진다. git svn은우리가리모트저장소를딱하나만사용한다고가정한다. 그래서리모트저장소의이름을붙여서브랜치를관리하지않는다. Plumbing 명령어인 show-ref 명령으로리모트브랜치의정확한이름을확인할수있다. $ git show-ref 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/heads/master aee1ecc26318164f355a883f5d99cff0c852d3c4 refs/remotes/my-calc-branch 03d09b0e2aad427e34a6d50ff147128e76c0e0f5 refs/remotes/tags/2.0.2 50d02cc0adc9da4319eeba0900430ba219b9c376 refs/remotes/tags/release-2.0.1 4caaa711a50c77879a91b8b90380060f672745cb refs/remotes/tags/release-2.0.2 1c4cb508144c513ff1214c3488abe66dcb92916f refs/remotes/tags/release-2.0.2rc1 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/remotes/trunk 일반적인 Git 저장소라면아래와비슷하다 : $ git show-ref 83e38c7a0af325a9722f2fdc56b10188806d83a1 refs/heads/master 3e15e38c198baac84223acfc6224bb8b99ff2281 refs/remotes/gitserver/master 0a30dd3b0c795b80212ae723640d4e5d48cabdff refs/remotes/origin/master 25812380387fdd55f916652be4881c6f11600d6f refs/remotes/origin/testing 이결과를보면리모트저장소가두개있다. gitserver라는리모트저장소에 master 브랜치가있고 origin이라는리모트저장소에 master, testing 브랜치가있다. git svn으로저장소를가져오면 Subversion 태그는 Git 태그가아니라리모트브랜치로등록되는점을잘기억하자. git svn은 Subversion 태그를 tags라는리모트서버에있는브랜치처럼만든다. 8.1.4 Subversion 서버에커밋하기자작업할 Git 저장소는준비했다. 무엇인가수정하고서버로고친내용을 Push해야할때가왔다. Git 을 Subversion의클라이언트로사용해서수정한내용을전송한다. 어떤파일을수정하고커밋을하면그수정한내용은 Git의로컬저장소에저장된다. Subversion 서버에는아직반영되지않는다. $ git commit -am 'Adding git-svn instructions to the README' [master 97031e5] Adding git-svn instructions to the README 1 files changed, 1 insertions(+), 1 deletions(-) 이제수정한내용을서버로전송한다. Git 저장소에여러개의커밋을쌓아놓고한번에 Subversion 서 버로보낸다는점을잘살펴보자. git svn dcommit 명령으로서버에 Push 한다. 204

Scott Chacon Pro Git 8.1 절 Git 과 Subversion $ git svn dcommit Committing to file:///tmp/test-svn/trunk... M README.txt Committed r79 M README.txt r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk 이명령은새로추가한커밋을모두 Subversion 에커밋하고로컬 Git 커밋을다시만든다. 커밋을다시 만들기때문에이미저장된커밋의 SHA-1 체크섬이바뀐다. 그래서리모트 Git 저장소와 Subversion 저장소를함께사용하면안된다. 새로만들어진커밋을살펴보면아래와같이 git-svn-id 가추가된다 : $ git log -1 commit 938b1a547c2cc92033b74d32030e86468294a5c8 Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sat May 2 22:06:44 2009 +0000 Adding git-svn instructions to the README git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029 원래 97031e5 로시작하는 SHA 체크섬이지금은 938b1a5 로시작한다. 만약 Git 서버와 Subversion 서 버에함께 Push 하고싶으면우선 Subversion 서버에 dcommit 으로 Push 를하고그다음에 Git 서버에 Push 해야한다. 8.1.5 새로운변경사항받아오기 다른개발자와함께일하는과정에서다른개발자가 Push 한상태에서 Push 를하면충돌이날수있다. 충돌을해결하지않으면서버로 Push 할수없다. 충돌이나면 git svn 명령은아래와같이보여준다 : $ git svn dcommit Committing to file:///tmp/test-svn/trunk... Merge conflict during commit: Your file or directory 'README.txt' is probably \ out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-\ core/git-svn line 482 이런상황에서는 git svn rebase 명령으로이문제를해결한다. 이명령은변경사항을서버에서내려 받고그다음에로컬의변경사항을그위에적용한다 : 205

8 장 Git 으로이전하기 Scott Chacon Pro Git $ git svn rebase M README.txt r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk) First, rewinding head to replay your work on top of it... Applying: first user change 그러면서버코드위에변경사항을적용하기때문에성공적으로 dcommit 명령을마칠수있다 : $ git svn dcommit Committing to file:///tmp/test-svn/trunk... M README.txt Committed r81 M README.txt r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk Push 하기전에서버의내용을 Merge 하는 Git 과달리 git svn 은충돌이날때에만서버에업데이트할 것이있다고알려준다. 이점을꼭기억해야한다. 만약다른사람이한파일을수정하고내가그사람과 다른파일을수정한다면 dcommit 은성공적으로수행된다 : $ git svn dcommit Committing to file:///tmp/test-svn/trunk... M configure.ac Committed r84 M autogen.sh r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk) M configure.ac r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk) W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ, \ using rebase: :100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \ 015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh First, rewinding head to replay your work on top of it... Nothing to do. Push하고나면프로젝트상태가달라진다는점을기억해야한다. 충돌이없으면변경사항이바램대로적용되지않아도알려주지않는다. 이부분이 Git과다른점이다. Git에서는서버로보내기전에프로젝트상태를전부테스트할수있다. SVN은서버로커밋하기전과후의상태가동일하다는것이보장되지않는다. 206

Scott Chacon Pro Git 8.1 절 Git 과 Subversion git svn rebase 명령으로도 Subversion 서버의변경사항을가져올수있다. 커밋을보낼준비가안됐 어도괞찮다. git svn fetch 명령을사용해도되지만 git svn rebase 명령은변경사항을가져오고적용 까지한번에해준다. $ git svn rebase M generate_descriptor_proto.sh r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk) First, rewinding head to replay your work on top of it... Fast-forwarded master to refs/remotes/trunk. 수시로 git svn rebase 명령을사용하면로컬코드를항상최신버전으로유지할수있다. 이명령을사용하기전에워킹디렉토리를깨끗하게만드는것이좋다. 깨끗하지못하면 Stash를하거나임시로커밋하고나서 git svn rebase 명령을실행하는것이좋다. 깨끗하지않으면충돌이나서 Rebase가중지될수있다. 8.1.6 Git 브랜치문제 Git에익숙한사람이면일을할때먼저토픽브랜치를만들고, 일을끝낸다음에, Merge하는방식을쓰려고할것이다. 하지만, git svn으로 Subversion 서버에 Push할때에는브랜치를 Merge하지않고 Rebase해야한다. Subversion은일직선히스토리밖에모르고 Git의 Merge도알지못한다. 그래서 git svn은첫번째부모정보만사용해서 Git 커밋을 Subversion 커밋으로변경한다. 예제를하나살펴보자. experiment 브랜치를하나만들고 2개의변경사항을커밋한다. 그리고 master 브랜치로 Merge하고나서 dcommit 명령을수행하면아래와같은모양이된다 : $ git svn dcommit Committing to file:///tmp/test-svn/trunk... M CHANGES.txt Committed r85 M CHANGES.txt r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk COPYING.txt: locally modified INSTALL.txt: locally modified M COPYING.txt M INSTALL.txt Committed r86 M INSTALL.txt M COPYING.txt r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk 207

8 장 Git 으로이전하기 Scott Chacon Pro Git Merge 커밋이들어있는히스토리에서 dcommit 명령을실행한다. 그리고나서 Git 히스토리를살펴보면 experiment 브랜치의커밋은재작성되지않았다. 대신 Merge 커밋만 SVN 서버로전송됐을뿐이다. 누군가이것을내려받으면결과가합쳐진 Merge 커밋하나만볼수있다. 다른사람은언제어디서커밋한것인지알수없다. 8.1.7 Subversion 의브랜치 Subversion 의브랜치는 Git 의브랜치와달라서가능한사용을하지않는것이좋다. 하지만 git svn 으 로도 Subversion 브랜치를관리할수있다. SVN 브랜치만들기 Subversion 브랜치를만들려면 git svn branch [branchname] 명령을사용한다 : $ git svn branch opera Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera... Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn/branches/opera, 87 Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f Following parent with do_switch Successfully followed parent r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera) 이명령은 Subversion 의 svn copy trunk branches/opera 명령과동일하다. 이명령은브랜치를 Checkout 해주지않는다는것을주의해야한다. 여기서커밋하면 opera 브랜치가아니라 trunk 브랜치 에커밋된다. 8.1.8 Subversion 브랜치넘나들기 dcommit 명령은어떻게커밋할브랜치를결정할까? Git은히스토리에있는커밋중에서가장마지막으로기록된 Subversion 브랜치를찾는다. 즉, 현브랜치히스토리의커밋메시지에있는 git-svn-id 항목을읽는것이기때문에오직한브랜치에만전송할수있다. 동시에여러브랜치에서작업하려면 Subversion 브랜치에 dcommit할수있는로컬브랜치가필요하다. 이브랜치는 Subversion 커밋에서시작하는브랜치다. 아래와같이 opera 브랜치를만들면독립적으로일할수있다 : $ git branch opera remotes/opera git merge 명령으로 opera 브랜치를 trunk 브랜치 (master 브랜치역할 ) 에 Merge한다. 하지만 -m 옵션을주고적절한커밋메시지를작성하지않으면아무짝에쓸모없는 Merge branch opera 같은메시지가커밋된다. git merge 명령으로 Merge한다는것에주목하자. Git은자동으로공통커밋을찾아서 Merge에참고하기때문에 Subversion에서하는것보다 Merge가더잘된다. 여기서생성되는 Merge 커밋은일반적인 Merge 커밋과다르다. 이커밋을 Subversion 서버에 Push해야하지만 Subversion에서는부 208

Scott Chacon Pro Git 8.1 절 Git 과 Subversion 모가 2개인커밋이있을수없다. 그래서 Push하면브랜치에서만들었던커밋여러개가하나로합쳐진 (squash된) 것처럼 Push된다. 그래서일단 Merge하면취소하거나해당브랜치에서계속작업하기어렵다. dcommit 명령을수행하면 Merge한브랜치의정보를어쩔수없이잃어버리게된다. Merge Base도찾을수없게된다. dcommit 명령은 Merge한것을 git merge --squash로 Merge한것과똑같이만들어버린다. Branch를 Merge한정보는저장되지않기때문에이문제를해결할방법이없다. 문제를최소화하려면 trunk에 Merge하자마자해당브랜치를 ( 여기서는 opera) 삭제하는것이좋다. 8.1.9 Subversion 명령 git svn 명령은 Git 으로전향하기쉽도록 Subversion 에있는것과비슷한명령어를지원한다. 아마 여기서설명하는명령은익숙할것이다. SVN 형식의히스토리 Subversion 에익숙한사람은 Git 히스토리를 SVN 형식으로보고싶을수도있다. git svn log 명령 은 SVN 형식으로히스토리를보여준다 : $ git svn log ------------------------------------------------------------------------ r87 schacon 2009-05-02 16:07:37-0700 (Sat, 02 May 2009) 2 lines autogen change ------------------------------------------------------------------------ r86 schacon 2009-05-02 16:00:21-0700 (Sat, 02 May 2009) 2 lines Merge branch 'experiment' ------------------------------------------------------------------------ r85 schacon 2009-05-02 16:00:09-0700 (Sat, 02 May 2009) 2 lines updated the changelog git svn log명령에서기억해야할것은두가지다. 우선오프라인에서동작한다는점이다. SVN의 svn log 명령어는히스토리데이터를조회할때서버가필요하다. 둘째로이미서버로전송한커밋만출력해준다. 아직 dcommit 명령으로서버에전송하지않은로컬 Git 커밋은보여주지않는다. Subversion 서버에는있지만아직내려받지않은변경사항도보여주지않는다. 즉, 현재알고있는 Subversion 서버의상태만보여준다. SVN 어노테이션 git svn log 명령이 svn log 명령을흉내내는것처럼 git svn blame [FILE] 명령으로 svn annotate 명령을흉내낼수있다. 실행한결과는아래와같다 : 209

8 장 Git 으로이전하기 Scott Chacon Pro Git $ git svn blame README.txt 2 temporal Protocol Buffers - Google's data interchange format 2 temporal Copyright 2008 Google Inc. 2 temporal http://code.google.com/apis/protocolbuffers/ 2 temporal 22 temporal C++ Installation - Unix 22 temporal ======================= 2 temporal 79 schacon Committing in git-svn. 78 schacon 2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol 2 temporal Buffer compiler (protoc) execute the following: 2 temporal 다시한번말하지만이명령도아직서버로전송하지않은커밋은보여주지않는다. SVN 서버정보 svn info 명령은 git svn info 명령으로대신할수있다 : $ git svn info Path:. URL: https://schacon-test.googlecode.com/svn/trunk Repository Root: https://schacon-test.googlecode.com/svn Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029 Revision: 87 Node Kind: directory Schedule: normal Last Changed Author: schacon Last Changed Rev: 87 Last Changed Date: 2009-05-02 16:07:37-0700 (Sat, 02 May 2009) blame 이나 log 명령이오프라인으로동작하듯이이명령도오프라인으로동작한다. 서버에서가장최근 에내려받은정보를출력한다. Subversion에서무시하는것무시하기 Subversion 저장소를클론하면쓸데없는파일을커밋하지않도록 svn:ignore 속성을.gitignore 파일로만들고싶을것이다. git svn에는이문제와관련된명령이두가지있다. 하나는 git svn createignore 명령이다. 해당위치에커밋할수있는.gitignore 파일을생성해준다. 두번째방법은 git svn show-ignore 명령이다..gitignore에추가할목록을출력해준다. 프로젝트의 exclude 파일로결과를리다이렉트할수있다 : 210

Scott Chacon Pro Git 8.2 절 Git 으로옮기기 $ git svn show-ignore >.git/info/exclude 이렇게하면.gitignore 파일로프로젝트를더럽히지않아도된다. 혼자서만 Git 을사용하는거라면다 른팀원들은프로젝트에.gitignore 파일이있는것을싫어할수있다. 8.1.10 Git-Svn 요약 git svn 도구는여러가지이유로 Subversion 서버를사용해야만하는상황에서빛을발한다. 하지만 Git의모든장점을이용할수는없다. Git과 Subversion은다르기때문에혼란이빚어질수도있다. 이런문제에빠지지않기위해서다음가이드라인을지켜야한다 : Git 히스토리를일직선으로유지하라. git merge로 Merge 커밋이생기지않도록하라. Merge 말고 Rebase로변경사항을 Master 브랜치에적용하라. 따로 Git 저장소서버를두지말라. 클론을빨리하기위해서잠깐하나만들어쓰는것은무방하나절대로 Git 서버에 Push하지는말아야한다. pre-receive 훅에서 git-svn-id가들어있는커밋메시지는거절하는방법도괜찮다. 이러한가이드라인을잘지키면 Subversion 서버도쓸만하다. 그래도 Git 서버를사용할수있으면 Git 서버를사용하는것이훨씬좋다. 8.2 Git 으로옮기기 다른 VCS 를사용하는프로젝트를 Git 으로옮기고싶다면우선프로젝트를 Git 으로이전 (Migrate) 해 야한다. 이번절에서는 Git 에들어있는 Importer 를살펴보고직접 Importer 를만드는방법을알아본 다. 8.2.1 가져오기 많이사용하는 Subversion 과 Perforce 프로젝트를이전하는방법을살펴보자. 이두 VCS 에서 Git 으 로이전하고자하는사람이많고 Importer 도이미 Git 에들어있다. 8.2.2 Subversion git svn을설명하는절을읽었으면쉽게 git svn clone 명령으로저장소를가져올수있다. 가져오고나서 Subversion 서버는중지하고 Git 서버를만들고사용하면된다. 만약히스토리정보가필요하면 ( 느린 ) Subversion 서버없이로컬에서조회할수있다. 우선가져오기에시간이많이드니까일단가져오기를시작하자. 이가져오기기능에문제가좀있다. 첫번째문제는 Author 정보이다. Subversion에서는커밋하려면해당시스템계정이있어야한다. blame이나 git svn log 같은명령에서 schacon이라는이름을봤을것이다. 이정보를 Git 형식의정보로변경하려면 Subversion 사용자와 Git Author를연결시켜줘야한다. Subversion 사용자이름과 Git Author 간에연결을해줘서이 Author 정보를 Git 스타일의 Author 정보로변경한다. users.txt라는파일을아래와같이만든다 : 211

8 장 Git 으로이전하기 Scott Chacon Pro Git schacon = Scott Chacon <schacon@geemail.com> selse = Someo Nelse <selse@geemail.com> SVN 에기록된 Author 이름을아래명령으로조회한다 : $ svn log ^/ --xml grep -P "^<author" sort -u \ perl -pe 's/<author>(.*?)<\/author>/$1 = /' > users.txt 우선 XML 형식으로 SVN 로그를출력하고, 거기서 Author 정보만찾고, 중복된것을제거하고, XML 태그는버린다. 물론 grep, sort, perl 명령이동작하는시스템에서만이명령을사용할수있다. 이결과에 Git Author 정보를더해서 users.txt를만든다. 이파일을 git svn 명령에전달하면보다정확한 Author 정보를 Git 저장소에남길수있다. 그리고 git svn의 clone이나 init 명령에 --no-metadata 옵션을주면 Subversion의메타데이터를저장하지않는다. 해당명령은아래와같다 : $ git svn clone http://my-project.googlecode.com/svn/ \ --authors-file=users.txt --no-metadata -s my_project my_project 디렉토리에진짜 Git 저장소가생성된다. 결과는아래와같지않고 : commit 37efa680e8473b615de980fa935944215428a35a Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11debe05-5f7a86268029 아래와같다 : commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 Author: Scott Chacon <schacon@geemail.com> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk Author 정보가훨씬 Git 답고 git-svn-id 항목도기록되지않았다. 212

Scott Chacon Pro Git 8.2 절 Git 으로옮기기 이제뒷정리를해야한다. git svn 이만들어준이상한브랜치나태그를제거한다. 우선이상한리모트 태그를모두진짜 Git 태그로옮긴다. 그리고리모트브랜치도로컬브랜치로옮긴다. 아래와같이태그를진정한 Git 태그로만든다 : $ git for- each- ref refs/ remotes/ tags cut - d / - f 4- grep - v @ while read tagname; do git tag "$tagname" "tags/$tagname"; git branch -r -d "tags/ $tagname"; done tags/ 로시작하는리모트브랜치를가져다 (Lightweight) 태그로만들었다. refs/remotes 밑에있는레퍼런스는전부로컬브랜치로만든다 : $ git for- each- ref refs/ remotes cut - d / - f 3- grep - v @ while read branchname; do git branch "$branchname" "refs/remotes/$branchname"; git branch - r -d "$branchname"; done 이제모든태그와브랜치는진짜 Git 태그와브랜치가됐다. Git 서버를새로추가를하고지금까지작 업한것을 Push 하는일이남았다. 아래처럼리모트서버를추가한다 : $ git remote add origin git@my-git-server:myrepository.git 분명모든브랜치와태그를 Push 하고싶을것이다 : $ git push origin --all 모든브랜치와태그를 Git 서버로깔끔하게잘옮겼다. 8.2.3 Perforce 이제 Perforce 차례다. Preforce Importer도 Git에들어있다. Git 1.7.11 이전버전을사용한다면소스코드의 contrib 에포함되어있다. 이런경우라면 Perforce Importer를사용하기위해우선 git.kernel.org에서 Git 소스코드를가져와야한다 : $ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/contrib/fast-import git-p4 라는 Python 스크립트는 fast-import 디렉토리에있다. 그리고 Python과 p4가설치돼있어야이스크립트가동작한다. Perforce Public Depot에있는 Jam 프로젝트를 Git으로옮겨보자. 우선 Perfoce Depot의주소를 P4PORT 환경변수에설정한다 : 213

8 장 Git 으로이전하기 Scott Chacon Pro Git $ export P4PORT=public.perforce.com:1666 git-p4 clone 명령으로 Perforce 서버에서 Jam 프로젝트를가져온다. 이명령에 Depot, 프로젝트경 로, 프로젝트를가져올경로를주면된다 : $ git-p4 clone //public/jam/src@all /opt/p4import Importing from //public/jam/src@all into /opt/p4import Reinitialized existing Git repository in /opt/p4import/.git/ Import destination: refs/remotes/p4/master Importing revision 4409 (100%) /opt/p4import 디렉토리로이동해서 git log 명령을실행하면프로젝트정보를볼수있다 : $ git log -2 commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 Author: Perforce staff <support@perforce.com> Date: Thu Aug 19 10:18:45 2004-0800 Drop 'rc3' moniker of jam-2.5. the main part of the document. Folded rc2 and rc3 RELNOTES into Built new tar/zip balls. Only 16 months later. [git-p4: depot-paths = "//public/jam/src/": change = 4409] commit ca8870db541a23ed867f38847eda65bf4363371d Author: Richard Geiger <rmg@perforce.com> Date: Tue Apr 22 20:51:34 2003-0800 Update derived jamgram.c [git-p4: depot-paths = "//public/jam/src/": change = 3108] 커밋마다 git-p4 라는 ID 항목이들어가있다. 나중에 Perforce Change Number 가필요해질수도 있으니커밋에그대로유지하는편이좋다. 하지만 ID 를지우고자한다면지금하는것이가장좋다. git filter-branch 명령으로한방에삭제한다 : $ git filter-branch --msg-filter ' sed -e "/^\[git-p4:/d" 214

Scott Chacon Pro Git 8.2 절 Git 으로옮기기 ' Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) Ref 'refs/heads/master' was rewritten git log 명령을실행하면모든 SHA-1 체크섬이변경됐고커밋메시지에서 git-p4 항목도삭제된것을 확인할수있다. $ git log -2 commit 10a16d60cffca14d454a15c6164378f4082bc5b0 Author: Perforce staff <support@perforce.com> Date: Thu Aug 19 10:18:45 2004-0800 Drop 'rc3' moniker of jam-2.5. the main part of the document. Folded rc2 and rc3 RELNOTES into Built new tar/zip balls. Only 16 months later. commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 Author: Richard Geiger <rmg@perforce.com> Date: Tue Apr 22 20:51:34 2003-0800 Update derived jamgram.c 이제새 Git 서버에 Push 하면된다. 8.2.4 직접 Importer 만들기사용하는 VCS가 Subversion이나 Perforce가아니면인터넷에서적당한 Importer를찾아봐야한다. CVS, Clear Case, Visual Source Safe 같은시스템용 Importer가좋은게많다. 심지어단순히디렉토리아카이브용 Importer에도좋은게있다. 사람들이잘안쓰는시스템을사용하고있는데적당한 Importer를못찾았거나부족해서좀더고쳐야한다면 git fast-import를사용한다. 이명령은표준입력으로데이터를입력받는데, 9장에서배우는저수준명령어와내부객체를직접다루는것보다훨씬쉽다. 먼저사용하는 VCS에서필요한정보를수집해서표준출력으로출력하는스크립트를만든다. 그리고그결과를 git fast-import의표준입력으로보낸다 (pipe). 간단한 Importer를작성해보자. back_yyyy_mm_dd 라는디렉토리에백업하면서프로젝트를진행하는예제를보자. Importer를만들때디렉토리상태는아래와같다 : $ ls /opt/import_from back_2009_01_02 back_2009_01_04 back_2009_01_14 back_2009_02_03 215

8 장 Git 으로이전하기 Scott Chacon Pro Git current Importer를만들기전에우선 Git이어떻게데이터를저장하는지알아야한다. 이미알고있듯이 Git 은기본적으로스냅샷을가리키는커밋개체가연결된리스트이다. 스냅샷이뭐고, 그걸가리키는커밋은또뭐고, 그커밋의순서가어떻게되는지 fast-import에알려줘야한다. 이것이해야할일의전부다. 그러면디렉토리마다스냅샷을만들고, 그스냅샷을가리키는커밋개체를만들고, 이전커밋과연결시킨다. 7장의 정책구현하기 절에서했던것처럼 Ruby로스크립트를작성한다. 필자는 Ruby를많이사용하기도하고 Ruby가읽기도쉽다. 하지만자신에게익숙한것을사용해서표준출력으로적절한정보만출력할수있으면된다. 그리고윈도에서는줄바꿈문자에 CR(Carriage Return) 문자가들어가지않도록주의해야한다. git fast-import 명령은윈도에서도줄바꿈문자로 CRLF 문자가아니라 LF(Line Feed) 문자만허용한다. 우선해당디렉토리로이동해서어떤디렉토리가있는지살펴본다. 하위디렉토리마다스냅샷하나가되고커밋하나가된다. 하위디렉토리를이동하면서필요한정보를출력한다. 기본적인로직은아래와같다 : last_mark = nil # loop through the directories Dir.chdir(ARGV[0]) do Dir.glob("*").each do dir next if File.file?(dir) # move into the target directory Dir.chdir(dir) do last_mark = print_export(dir, last_mark) end end end 각디렉토리에서 print_export를호출하는데이함수는아규먼트로디렉토리와이전스냅샷 Mark를전달받고현스냅샷 Mark를반환한다. 그래서적절히연결시킬수있다. fast-import에서 Mark 는커밋의식별자를말한다. 커밋을하나만들면 Mark도같이만들어이 Mark로다른커밋과연결시킨다. 그래서 print_export에서우선해야하는일은각디렉토리이름으로 Mark를생성하는것이다 : mark = convert_dir_to_mark(dir) Mark 는정수값을사용해야하기때문에디렉토리를배열에담고그인덱스를 Mark 로사용한다. 아 래와같이작성한다 : 216

Scott Chacon Pro Git 8.2 절 Git 으로옮기기 $marks = [] def convert_dir_to_mark(dir) if!$marks.include?(dir) $marks << dir end ($marks.index(dir) + 1).to_s end 각커밋을가리키는정수 Mark 를만들었고다음은커밋메타데이터에넣을날짜정보가필요하다. 이 날짜는디렉토리이름에있는것을가져다사용한다. print_export 의두번째줄은아래와같다 : date = convert_dir_to_date(dir) convert_dir_to_date 는아래와같이정의한다 : def convert_dir_to_date(dir) if dir == 'current' return Time.now().to_i else dir = dir.gsub('back_', '') (year, month, day) = dir.split('_') return Time.local(year, month, day).to_i end end 시간는정수형태로반환한다. 마지막으로메타정보에필요한것은 Author 인데이것은전역변수하 나로설정해서사용한다 : $author = 'Scott Chacon <schacon@example.com>' 이제 Importer 에서출력할커밋데이터는다준비했다. 이제출력해보자. 사용할브랜치, 해당커밋과 관련된 Mark, 커미터정보, 커밋메시지, 이전커밋를출력한다. 코드로만들면아래와같다 : # print the import information puts 'commit refs/heads/master' puts 'mark :' + mark puts "committer #{$author} #{date} -0700" export_data('imported from ' + dir) puts 'from :' + last_mark if last_mark 217

8 장 Git 으로이전하기 Scott Chacon Pro Git 우선시간대 (-0700) 정보는편의상하드코딩으로처리했다. 각자의시간대에맞는오프셋을설정해 야한다. 커밋메시지는아래와같은형식을따라야한다 : data (size)\n(contents) 이형식은 data 라는단어, 읽을데이터의크기, 줄바꿈문자, 실데이터로구성된다. 이형식을여러 곳에서사용해야하므로 export_data 라는메소드로만들어놓는게좋다 : def export_data(string) end print "data #{string.size}\n#{string}" 이제남은것은스냅샷에파일내용를포함시키는것뿐이다. 디렉토리로구분돼있기때문에어렵지않 다. 우선 deleteall 이라는명령을출력하고그뒤에모든파일의내용을출력한다. 그런면 Git 은스냅샷 을잘저장한다 : puts 'deleteall' Dir.glob("**/*").each do file next if!file.file?(file) inline_data(file) end 중요 : 대부분의 VCS는리비전을커밋간의변화로생각하기때문에 fast-import에추가 / 삭제 / 변경된부분만입력할수도있다. 스냅샷사이의차이를구해서 fast-import에넘길수도있지만훨씬복잡하다. 줄수있는데이터는전부 Git에줘서 Git이계산하게해야한다. 꼭이렇게해야한다면어떻게데이터를전달해야하는지 fast-import의 ManPage를참고하라. 파일정보와내용은아래와같은형식으로출력한다 : M 644 inline path/to/file data (size) (file contents) 644 는파일의모드를나타낸다 ( 실행파일이라면 755 로지정해줘야한다 ). inline 은다음줄부터는 파일내용이라는말하는것이다. inline_data 메소드는아래와같다 : def inline_data(file, code = 'M', mode = '644') content = File.read(file) puts "#{code} #{mode} inline #{file}" export_data(content) end 218

Scott Chacon Pro Git 8.2 절 Git 으로옮기기 파일내용은커밋메시지랑같은방법을사용하기때문에앞서만들어놓은 export_data 메소드를다시 이용한다. 마지막으로다음커밋에사용할현 Mark 값을반환한다 : return mark 중요 : 윈도에서실행할때는추가작업이하나더필요하다. 앞에서얘기했지만윈도는 CRLF 를사용하 지만 git fast-import 는 LF 를사용한다. 이문제를해결하려면 Ruby 가 CRLF 대신 LF 를사용하도록 알려줘야한다 : $stdout.binmode 모든게끝났다. 스크립트를실행하면아래와같이출력된다 : $ ruby import.rb /opt/import_from commit refs/heads/master mark :1 committer Scott Chacon <schacon@geemail.com> 1230883200-0700 data 29 imported from back_2009_01_02deleteall M 644 inline file.rb data 12 version two commit refs/heads/master mark :2 committer Scott Chacon <schacon@geemail.com> 1231056000-0700 data 29 imported from back_2009_01_04from :1 deleteall M 644 inline file.rb data 14 version three M 644 inline new.rb data 16 new version one (...) 디렉토리를하나만들고 git init 명령을실행해서옮길 Git 프로젝트를만든다. 그리고그프로젝트 디렉토리로이동해서이명령의표준출력을 git fast-import 명령의표준입력으로연결한다 (pipe). 219

8 장 Git 으로이전하기 Scott Chacon Pro Git $ git init Initialized empty Git repository in /opt/import_to/.git/ $ ruby import.rb /opt/import_from git fast-import git-fast-import statistics: --------------------------------------------------------------------- Alloc'd objects: 5000 Total objects: 18 ( 1 duplicates ) blobs : 7 ( 1 duplicates 0 deltas) trees : 6 ( 0 duplicates 1 deltas) commits: 5 ( 0 duplicates 0 deltas) tags : 0 ( 0 duplicates 0 deltas) Total branches: 1 ( 1 loads ) marks: 1024 ( 5 unique ) atoms: 3 Memory total: 2255 KiB pools: 2098 KiB objects: 156 KiB --------------------------------------------------------------------- pack_report: getpagesize() = 4096 pack_report: core.packedgitwindowsize = 33554432 pack_report: core.packedgitlimit = 268435456 pack_report: pack_used_ctr = 9 pack_report: pack_mmap_calls = 5 pack_report: pack_open_windows = 1 / 1 pack_report: pack_mapped = 1356 / 1356 --------------------------------------------------------------------- 성공적으로끝나면여기서보여주는것처럼어떻게됐는지통계를보여준다. 이경우엔브랜치 1 개와 커밋 5 개그리고개체 18 개가임포트됐다. git log 명령으로히스토리조회가가능하다 : $ git log -2 commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 Author: Scott Chacon <schacon@example.com> Date: Sun May 3 12:57:39 2009-0700 imported from current commit 7e519590de754d079dd73b44d695a42c9d2df452 Author: Scott Chacon <schacon@example.com> Date: Tue Feb 3 01:00:00 2009-0700 imported from back_2009_02_03 220

Scott Chacon Pro Git 8.3 절요약 이시점에서는아무것도 Checkout 하지않았기때문에워킹디렉토리에아직아무파일도없다. master 브랜치로 Reset 해서파일을 Checkout 한다 : $ ls $ git reset --hard master HEAD is now at 10bfe7d imported from current $ ls file.rb lib fast-import 명령으로많은일을할수있다. 모드를설정하고, 바이너리데이터를다루고, 브랜치를여러개다루고, Merge하고, 태그를달고, 진행상황을보여주고, 등등무수히많은일을할수있다. Git 소스의 contrib/fast-import 디렉토리에복잡한상황을다루는예제가많다. 그중여기서설명한 gitp4 스크립트가좋은예제이다. 8.3 요약 Subversion 프로젝트에서 Git을사용하거나, 다른 VCS 저장소를 Git 저장소로손실없이옮기는방법에대해알아봤다. 다음장에서는 Git 내부를까본다. 필요하다면바이트하나하나다루는것도가능하다. 221

9 장 Git 의내부 여기까지다읽고왔든바로 9장부터보기시작했든지간에이제마지막장이다. 9장은 Git이어떻게구현돼있고내부적으로어떻게동작하는지설명한다. Git이얼마나유용하고강력한지이해하려면 9 장의내용을꼭알아야한다. 9장은초보자에게너무혼란스럽고불필요한내용이라고이야기하는사람들도있다. 그래서필자는본내용을책의가장마지막에두었고독자가스스로먼저볼지나중에볼지선택할수있도록했다. 자이제본격적으로살펴보자. 우선 Git은기본적으로 Content-addressable 파일시스템이고그위에 VCS 사용자인터페이스가있는구조다. 뭔가깔끔한정의는아니지만, 이말이무슨의미인지는차차알게된다. Git 초기에는 (1.5 이전버전 ) 사용자인터페이스가훨씬복잡했었다. VCS가아니라파일시스템을강조했기때문이었다. 최근몇년간 Git은다른 VCS처럼쉽고간결하게사용자인터페이스를다듬어왔다. 하지만, 여전히복잡하고배우기어렵다는선입견이있다. 우선 Content-addressable 파일시스템은정말대단한것이므로먼저다룬다. 그리고나서데이터전송원리를배우고마지막에는저장소를관리하는법까지배운다. 9.1 Plumbing 명령과 Porcelain 명령 이책에서는 checkout, branch, remote 같은 30여가지의 Git 명령을사용했다. Git은사실사용자친화적인 VCS이기보다는 VCS로도사용할수있는툴킷이다. 저수준명령어가매우많아서저수준의일도쉽게처리할수있다. 명령어여러개를 Unix 스타일로함께엮어서실행하거나스크립트에서호출할수있도록설계됐다. 이러한저수준의명령어는 Plumbing 명령어라고부르고좀더사용자에게친숙한사용자용명령어는 Porcelain 명령어라고부른다. 이책의앞여덟장은 Porcelain 명령어만사용했다. 하지만, 이장에서는저수준명령인 Plumbing 명령어을주로사용한다. 이명령으로 Git의내부구조에접근할수있고실제로왜, 그렇게작동하는지도살펴볼수있다. Plumbing 명령어은직접커맨드라인에서실행하기보다새로운도구를만들거나각자필요한스크립트를작성할때사용한다. 새로만든디렉토리나이미파일이있는디렉토리에서 git init 명령을실행하면 Git은데이터를저장하고관리하는.git 디렉토리를만든다. 이디렉토리를복사하기만해도저장소가백업된다. 이장은기본적으로이디렉토리에대한내용을설명한다. 디렉토리구조는아래와같다 : $ ls HEAD 223

9 장 Git 의내부 Scott Chacon Pro Git branches/ config description hooks/ index info/ objects/ refs/ 이외에다른파일들이더있지만이상태가 git init을한직후에보이는새저장소의모습이다. branches 디렉토리는 Git의예전버전에서만사용하고 description 파일은기본적으로 GitWeb 프로그램에서만사용하기때문에이둘은무시해도된다. config 파일에는해당프로젝트에만적용되는설정옵션이들어있다. info 디렉토리는.gitignore 파일처럼무시할파일의패턴을적어두는곳이다. 하지만.gitignore 파일과는달리 Git으로관리되지않는다. hook 디렉토리에는클라이언트훅이나서버훅을넣는다. 관련내용은 7장에서설명했다. 이제남은네가지항목은모두중요한항목이다. HEAD 파일, index 파일, objects 디렉토리, refs 디렉토리가남았다. 이네항목이 Git의핵심이다. objects 디렉토리는모든컨텐트를저장하는데이터베이스이고 refs 디렉토리에는커밋개체의포인터를저장한다. HEAD 파일은현재 Checkout한브랜치를가리키고 index 파일은 Staging Area의정보를저장한다. 각절마다주제를나눠서 Git이어떻게동작하는지자세히설명한다. 9.2 Git 개체 Git은 Content-addressable 파일시스템이다. 이게무슨말이냐하면 Git이단순한 Key-Value 데이터저장소라는것이다. 어떤형식의데이터라도집어넣을수있고해당 Key로언제든지데이터를다시가져올수있다. Plumbing 명령어 hash-object에데이터를주면.git 디렉토리에저장하고그 key 를알려준다. 우선 Git 저장소를새로만들고 objects 디렉토리에뭐가들어있는지확인한다 : $ mkdir test $ cd test $ git init Initialized empty Git repository in /tmp/test/.git/ $ find.git/objects.git/objects.git/objects/info.git/objects/pack $ find.git/objects -type f $ 아무것도없다. Git 은 objects 디렉토리를만들고그밑에 pack 과 info 디렉토리도만든다. 아직빈디 렉토리일뿐파일은아무것도없다. Git 데이터베이스에텍스트파일을저장해보자 : 224

Scott Chacon Pro Git 9.2 절 Git 개체 $ echo 'test content' git hash-object -w --stdin d670460b4b4aece5915caf5c68d12f560a9fe3e4 이명령은표준입력으로들어오는데이터를저장할수있다. -w 옵션을줘야실제로저장한다. -w가없으면저장하지않고 key만보여준다. 그리고 --stdin 옵션을주면표준입력으로입력되는데이터를읽는다. 이옵션이없으면파일경로를알려줘야한다. hash-object 명령이출력하는것은 40자길이의체크섬해시다. 이해시는헤더정보와데이터모두에대한 SHA-1 해시이다. 헤더정보는차차자세히살펴볼것이다. Git이저장한데이터를알아보자 : $ find.git/objects -type f.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 objects 디렉토리에파일이하나새로생겼다. Git은데이터를저장할때데이터와헤더로생성한 SHA-1 체크섬으로파일이름을짓는다. 해시의처음두글자를따서디렉토리이름에사용하고나머지 38글자를파일이름에사용한다. 데이터는새로만든파일에저장한다. cat-file 명령으로저장한데이터를불러올수있다. 이명령은 Git 개체를살펴보고싶을때맥가이버칼처럼사용할수있다. cat-file 명령에 -p 옵션을주면파일내용이출력된다 : $ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content 다시한번데이터를 Git 저장소에추가하고불러와보자. Git 이파일버전을관리하는방식을이해할 수있도록가상의상황을만들어살펴본다. 우선새파일을하나만들고 Git 저장소에저장한다 : $ echo 'version 1' > test.txt $ git hash-object -w test.txt 83baae61804e65cc73a7201a7252750c76066a30 그리고그파일을수정하고다시저장한다 : $ echo 'version 2' > test.txt $ git hash-object -w test.txt 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a 이제데이터베이스에는데이터가두가지버전으로저장돼있다 : 225

9 장 Git 의내부 Scott Chacon Pro Git $ find.git/objects -type f.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a.git/objects/83/baae61804e65cc73a7201a7252750c76066a30.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 파일의내용을첫번째버전으로되돌리려본다 : $ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt $ cat test.txt version 1 다시두번째버전을적용한다 : $ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt $ cat test.txt version 2 파일의 SHA-1 키를외워서사용하는것은너무어렵다. 게다가원래파일의이름은저장하지도않았 다. 단지파일내용만저장했을뿐이다. 이런종류의개체를 Blob 개체라고부른다. cat-file -t 명령으 로해당개체가무슨개채인지확인할수있다 : $ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 9.2.1 Tree 개체다음은 Tree 개체를살펴보자. 이 Tree 개체에파일이름을저장한다. 파일여러개를한꺼번에저장할수도있다. Git은유닉스파일시스템과비슷한방법으로저장하지만좀더단순하다. 모든것을 Tree와 Blob 개체로저장한다. Tree는유닉스의디렉토리에대응되고 Blob은 Inode나일반파일에대응된다. Tree 개체하나는항목을여러개가질수있다. 그리고그항목에는 Blob 개체나하위 Tree 개체를가리키는 SHA-1 포인터, 파일모드, 개체타입, 파일이름이들어있다. simplegit 프로젝트의마지막 Tree 개체를살펴보자 : $ git cat-file -p master^{tree} 100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README 100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib 226

Scott Chacon Pro Git 9.2 절 Git 개체 master {tree} 구문은 master 브랜치가가리키는 Tree 개체를말한다. lib 은디렉토리인데 Blob 개체 가아니고다른 Tree 개체다 : $ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb Git 이저장하는데이터는대강그림 9-1 과같다. 그림 9.1: 단순화한 Git 데이터모델 직접 Tree 개체를만들어보자. Git은일반적으로 Staging Area(Index) 의상태대로 Tree 개체를만들고기록한다. 그래서 Tree 개체를만들려면우선 Staging Area에파일을추가해서 Index를만들어야한다. 우선 Plumbing 명령어 update-index로 test.txt 파일만들어있는 Index를만든다. 이명령어는파일을인위적으로 Staging Area에추가하는명령다. 아직 Staging Area에없는파일이기때문에 --add 옵션을꼭줘야한다 ( 사실아직 Staging Area도설정하지않았다 ). 그리고디렉토리에있는파일이아니라데이터베이스에있는파일을추가하는것이기때문에 --cacheinfo 옵션이필요하다. 파일모드, SHA-1 해시, 파일이름정보도입력한다 : $ git update-index --add --cacheinfo 100644 \ 83baae61804e65cc73a7201a7252750c76066a30 test.txt 여기서파일모드는보통의파일을나타내는 100644로지정했다. 실행파일이라면 100755로지정하고심볼릭링크라면 120000으로지정한다. 이런파일모드는유닉스에서가져오긴했지만, 유닉스모드를전부사용하지는않는다. Blob 파일에는이세가지모드만사용한다. 디렉토리나서브모듈에는다른모드를사용한다. Staging Area를 Tree 개체로저장할때는 write-tree 명령을사용한다. write-tree 명령은 Tree 개체가없으면자동으로생성하므로 -w 옵션이필요없다 : $ git write-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt 227

9 장 Git 의내부 Scott Chacon Pro Git 아래명령으로이개체가 Tree 개체라는것을확인한다 : $ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 파일을새로하나추가하고 test.txt 파일도두번째버전을만든다. 그리고나서 Tree 개체를만든다 : $ echo 'new file' > new.txt $ git update-index test.txt $ git update-index --add new.txt 새파일인 new.txt 와새로운버전의 test.txt 파일까지 Staging Area 에추가했다. 현재상태의 Staging Area 를새로운 Tree 개체로기록하면어떻게보이는지살펴보자 : $ git write-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 $ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt 이 Tree 개체에는파일이두개있고 test.txt 파일의 SHA 값도두번째버전인 1f7a7a1 이다. 재미난걸 해보자. 처음에만든 Tree 개체를하위디렉토리로만들수있다. read-tree 명령으로 Tree 개체를읽 어 Staging Area 에추가한다. --prefix 옵션을주면 Tree 개체를하위디렉토리로추가할수있다. $ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git write-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 $ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt 이 Tree 개체로워킹디렉토리를만들면파일두개와 bak 이라는하위디렉토리가생긴다. 그리고 bak 디렉토리안에는 test.txt 파일의처음버전이들어있다. 그림 9-2 와같은구조로데이터가저장된다. 9.2.2 커밋개체각기다른스냅샷을나타내는 Tree 개체를세개만들었다. 하지만, 여전히이스냅샷을불러오려면 SHA-1 값을기억하고있어야한다. 스냅샷을누가, 언제, 왜저장했는지에대한정보는아예없다. 이런정보는커밋개체에저장된다 : 228

Scott Chacon Pro Git 9.2 절 Git 개체 그림 9.2: 현재 Git 데이터구조 커밋개체는 commit-tree 명령으로만든다. 이명령에커밋개체에대한설명과 Tree 개체의 SHA-1 값 한개를넘긴다. 앞서저장한첫번째 Tree 를가지고아래와같이만들어본다 : $ echo 'first commit' git commit-tree d8329f fdf4fc3344e67ab068f836878b6c4951e3b15f3d 새로생긴커밋개체를 cat-file 명령으로확인해보자 : $ git cat-file -p fdf4fc3 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon <schacon@gmail.com> 1243040974-0700 committer Scott Chacon <schacon@gmail.com> 1243040974-0700 first commit 커밋개체의형식은간단하다. 해당스냅샷에서최상단 Tree를 ( 역주 - 루트디렉터리같은 ) 하나가리킨다. 그리고 user.name과 user.email 설정에서가져온 Author/Committer 정보, 시간정보, 그리고한줄띄운다음커밋메시지가들어간다. 이제커밋개체를두개더만들어보자. 각커밋개체는이전개체를가리키도록한다 : $ echo 'second commit' git commit-tree 0155eb -p fdf4fc3 cac0cab538b970a37ea1e769cbbde608743bc96d $ echo 'third commit' git commit-tree 3c4e9c -p cac0cab 1a410efbd13591db07496601ebc7a059dd55cfe9 세커밋개체는각각해당스냅샷을나타내는 Tree 개체를하나씩가리키고있다. 이상해보이겠지만우리는진짜 Git 히스토리를만들었다. 마지막커밋개체의 SHA-1 값을주고 git log 명령을실행하면아래와같이출력한다 : 229

9 장 Git 의내부 Scott Chacon Pro Git $ git log --stat 1a410e commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Author: Scott Chacon <schacon@gmail.com> Date: Fri May 22 18:15:24 2009-0700 third commit bak/test.txt 1 + 1 files changed, 1 insertions(+), 0 deletions(-) commit cac0cab538b970a37ea1e769cbbde608743bc96d Author: Scott Chacon <schacon@gmail.com> Date: Fri May 22 18:14:29 2009-0700 second commit new.txt 1 + test.txt 2 +- 2 files changed, 2 insertions(+), 1 deletions(-) commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d Author: Scott Chacon <schacon@gmail.com> Date: Fri May 22 18:09:34 2009-0700 first commit test.txt 1 + 1 files changed, 1 insertions(+), 0 deletions(-) 놀랍지않은가! 방금우리는고수준명령어없이저수준의명령으로만 Git 히스토리를만들었다. 지금한일이 git add와 git commit 명령을실행했을때 Git 내부에서일어나는일이다. Git은변경된파일을 Blob 개체로저장하고현 Index에따라서 Tree 개체를만든다. 그리고이전커밋개체와최상위 Tree 개체를참고해서커밋개체를만든다. 즉 Blob, Tree, 커밋개체가 Git의주요개체이고이개체는전부.git/objects 디렉토리에저장된다. 이예제에서생성한개체는아래와같다 : $ find.git/objects -type f.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content' 230

Scott Chacon Pro Git 9.2 절 Git 개체.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 내부의포인터를따라가면그림 9-3 과같은그래프가그려진다. 그림 9.3: Git 저장소내의모든개체 9.2.3 개체저장소 내용과함께헤더도저장한다고얘기했다. 잠시 Git 이개체를어떻게저장하는지부터살펴보자. what is up, doc? 이라는문자열을가지고대화형 Ruby 쉘 irb 명령어로흉내내보자 : $ irb >> content = "what is up, doc?" => "what is up, doc?" Git 은개체의타입을시작으로헤더를만든다. 그다음에공백문자하나, 내용의크기, 마지막에널문 자를추가한다 : >> header = "blob #{content.length}\0" => "blob 16\000" Git 은헤더와원래내용을합쳐서 SHA-1 체크섬을계산한다. Ruby 에서도 require 로 SHA1 라이브 러리를가져다가흉내낼수있다. require 로라이브러리를포함하고나서 Digest::SHA1.hexdigest() 를 호출한다 : 231

9 장 Git 의내부 Scott Chacon Pro Git >> store = header + content => "blob 16\000what is up, doc?" >> require 'digest/sha1' => true >> sha1 = Digest::SHA1.hexdigest(store) => "bd9dbf5aae1a3862dd1526723246b20206e5fc37" Git 은또 zlib 으로내용을압축한다. Ruby 에도 zlib 라이브러리가있으니 Ruby 에서도할수있다. 라 이브러리를포함하고 Zlib::Deflate.deflate() 를호출한다 : >> require 'zlib' => true >> zlib_content = Zlib::Deflate.deflate(store) => "x\234k\312\311or04c(\317h,q\310,v(-\320qh\311o\266\a\000_\034\a\235" 마지막으로 zlib으로압축한내용을개체로저장한다. SHA-1 값중에서맨앞에있는두자를가져다하위디렉토리이름으로사용하고나머지 38자를그디렉토리안에있는파일이름으로사용한다. Ruby에서는 FileUtils.mkdir_p() 로하위디렉토리의존재를보장하고나서 File.open() 으로파일을연다. 그리고그파일에 zlib으로압축한내용을 write() 함수로저장한다. >> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38] => ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37" >> require 'fileutils' => true >> FileUtils.mkdir_p(File.dirname(path)) => ".git/objects/bd" >> File.open(path, 'w') { f f.write zlib_content } => 32 다됐다. 이제 Git Blob 개체를손으로만들었다. Git 개체는모두이방식으로저장하며단지종류만 다르다. 헤더가 blob 이아니라그냥 commit 이나 tree 로시작하게되는것뿐이다. Blob 개체는여기서보 여준것과거의같지만커밋이개체나 Tree 개체는각기다른형식을사용한다. 9.3 Git 레퍼런스 git log 1a410e 라고실행하면전체히스토리를볼수있지만, 여전히 1a410e를기억해야한다. 이커밋은마지막커밋이기때문에히스토리를따라모든개체를조회할수있다. SHA-1 값을날로사용하기보다쉬운이름으로된포인터가있으면그걸사용하는게더좋다. 외우기쉬운이름으로된파일에 SHA-1 값을저장한다. Git에서는이런것을 레퍼런스 또는 refs 라고부른다. SHA-1 값이든파일은.git/refs 디렉토리에있다. 이프로젝트에는아직레퍼런스가하나도없다. 이디렉토리의구조는매우단순하다 : 232

Scott Chacon Pro Git 9.3 절 Git 레퍼런스 $ find.git/refs.git/refs.git/refs/heads.git/refs/tags $ find.git/refs -type f $ 레퍼런스가있으면마지막커밋이무엇인지기억하기쉽다. 사실내부는아래처럼단순하다 : $ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" >.git/refs/heads/master SHA-1 값대신에지금만든레퍼런스를사용할수있다 : $ git log --pretty=oneline master 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 레퍼런스파일을직접고치는것이좀못마땅하다. Git 에는좀더안전하게바꿀수있는 update-ref 명 령이있다 : $ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9 Git 브랜치의역할이바로이거다. 브랜치는어떤작업들중마지막작업을가리키는포인터또는레퍼 런스이다. 간단히두번째커밋을가리키는브랜치를만들어보자 : $ git update-ref refs/heads/test cac0ca 브랜치는직접가리키는커밋과그커밋으로따라갈수있는모든커밋을포함한다 : $ git log --pretty=oneline test cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 이제 Git 데이터베이스는그림 9-4처럼보인다. git branch (branchname) 명령을실행하면 Git은내부적으로 update-ref 명령을실행한다. 입력받은브랜치이름과현브랜치의마지막커밋의 SHA-1 값을가져다 update-ref 명령을실행한다. 233

9 장 Git 의내부 Scott Chacon Pro Git 그림 9.4: 브랜치레퍼런스가추가된 Git 데이터베이스 9.3.1 HEAD git branch (branchname) 명령을실행할때 Git은어떻게마지막커밋의 SHA-1 값을아는걸까? HEAD 파일은현브랜치를가리키는간접 (symbolic) 레퍼런스다. 간접레퍼런스이기때문에다른레퍼런스와다르다. 이레퍼런스은다른레퍼런스를가리키는것이라서 SHA-1 값이없다. 파일을열어보면아래와같이생겼다 : $ cat.git/head ref: refs/heads/master git checkout test 를실행하면 Git 은 HEAD 파일을아래와같이바꾼다 : $ cat.git/head ref: refs/heads/test git commit을실행하면커밋개체가만들어지는데, 지금 HEAD가가리키고있던커밋의 SHA-1 값이그커밋개체의부모로사용된다. 이파일도손으로직접편집할수있지만 symbolic-ref라는명령어가있어서좀더안전하게사용할수있다. 이명령으로 HEAD의값을읽을수있다 : $ git symbolic-ref HEAD refs/heads/master HEAD 의값을변경할수도있다 : $ git symbolic-ref HEAD refs/heads/test $ cat.git/head ref: refs/heads/test refs 형식에맞지않으면수정할수없다 : 234