Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 374 – Python 프로젝트를 위한 분산 VCS 선택

Author:
Brett Cannon <brett at python.org>, Stephen J. Turnbull <stephen at xemacs.org>, Alexandre Vassalotti <alexandre at peadrop.com>, Barry Warsaw <barry at python.org>, Dirkjan Ochtman <dirkjan at ochtman.nl>
Status:
Final
Type:
Process
Created:
07-Nov-2008
Post-History:
07-Nov-2008, 22-Jan-2009

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

근거

Python은 수년 동안 중앙 집중식 버전 관리 시스템(VCS; 처음에는 CVS, 현재는 Subversion)을 사용해 왔으며 큰 효과를 거두었습니다. Python 공식 버전의 마스터 사본이 있으면 사람들은 공식 Python 소스 코드를 언제나 한곳에서 얻을 수 있습니다. 또한 이를 통해 언어의 이력을 저장할 수 있었는데, 주로 개발에 도움을 주기 위해서였지만 후대를 위한 것이기도 했습니다. 그리고 물론 VCS의 V(버전)는 개발할 때 매우 유용합니다.

하지만 중앙 집중식 버전 관리 시스템에는 단점이 있습니다. 무엇보다도, Python에서 버전 관리의 이점을 매끄럽게 누리려면 “코어 개발자”(즉, Python 마스터 사본에 대한 커밋 권한을 가진 사람)여야 합니다. 코어 개발자는 아니지만 Python의 리비전 트리로 작업하려는 사람들, 예를 들어 Python용 패치를 작성하거나 커스텀 버전을 만드는 사람은 리비전에 대한 직접적인 도구 지원을 받지 못합니다. 이러한 비코어 개발자는 변경 사항을 이전에 저장한 상태로 되돌리기, 브랜치 만들기, 전체 리비전 이력과 함께 자신의 변경 사항 공개하기 등의 기본적인 작업을 쉽게 할 수 없으므로 이는 상당한 제약이 될 수 있습니다. 비코어 개발자에게 마지막으로 안전한 트리 상태는 Python 개발자들이 그때그때 정해 둔 상태일 뿐이며, 이로 인해 안전한 개발이 어려워집니다. 이러한 이등 시민 취급은 어느 정도 복잡한 패치로 Python에 기여하려 하고, 개발을 더 수월하게 하기 위해 진행 상황을 점진적으로 저장할 방법을 원하는 사람들에게 방해가 됩니다.

작업을 커밋하려면 온라인 상태여야 한다는 문제도 있습니다. 중앙 집중식 VCS는 모든 리비전을 저장하는 중앙 사본을 유지하므로, 리비전을 저장하려면 인터넷에 접속할 수 있어야 합니다. 즉, 인터넷이 없으면 커밋도 없습니다. 여행 중이라 인터넷을 전혀 쓸 수 없다면 이는 불편할 수 있습니다. 또한 Python에 기여하고 싶지만 인터넷 연결 상태가 나빠서 커밋에 시간과 비용이 많이 들고, 차라리 한 번에 커밋하는 편이 나을 수 있는 상황도 있습니다.

중앙 집중식 VCS의 또 다른 단점은 개발자가 리뷰 의견에 따라 패치를 수정하는 흔한 사용 사례와 관련이 있습니다. 중간 작업을 담아 둘 곳이 없기 때문에 중앙 집중식 모델에서는 이 작업이 더 어렵습니다. 전부 체크인되거나 아무것도 체크인되지 않거나 둘 중 하나입니다. 중앙 집중식 VCS에서는 기능 브랜치나 버그 수정 브랜치에서 작업하는 동안 트렁크에 커밋되는 변경 사항을 추적하기도 매우 어렵습니다. 이로 인해 그러한 브랜치가 낡고 뒤처지거나, 트렁크에 병합할 때 쉽게 해결할 수 없을 만큼 많은 충돌이 발생할 위험이 커집니다.

마지막으로, Python 유지 보수 문제가 있습니다. 어느 시점에든 개발 중인 Python 메이저 버전이 적어도 하나는 있습니다(이 글을 쓰는 시점에는 두 개입니다). 개발 중인 Python 메이저 버전마다 적어도 직전 마이너 버전의 유지 보수 버전과 개발 중인 마이너 버전이 있습니다(예를 들어 2.6이 막 릴리스된 지금은 2.6과 2.7 모두 작업이 진행되고 있다는 뜻입니다). 릴리스가 완료되면 코드 베이스 사이에 브랜치가 생기며, 한 버전의 변경 사항이 다른 버전에는 속하지 않는(속할 수도 있는) 상황이 됩니다. 현재로서는 중앙 집중식 VCS에서 이러한 시간상의 브랜치를 자연스럽게 지원하지 않으므로, 브랜치를 흉내 내는 도구를 사용해야 합니다. 리비전을 네 개의 활성 브랜치(예: 2.6 유지 보수, 3.0 유지 보수, 2.7 개발, 3.1 개발) 사이에서 병합해야 하는 경우가 많기 때문에, 병합을 추적하는 일도 개발자에게 마찬가지로 고통스럽습니다. 이 경우 Subversion 같은 VCS는 난해한 서드파티 도구를 통해서만 이를 처리할 수 있습니다.

분산 VCS(DVCS)는 이 모든 문제를 해결합니다. 리비전 트리의 마스터 사본을 유지할 수 있는 한편, 누구나 자유롭게 그 트리를 복사해 자신의 용도로 사용할 수 있습니다. 이를 통해 누구나 온라인이든 오프라인이든 자신의 사본에 변경 사항을 커밋할 수 있습니다. 또한 유지 보수와 Python에 들어갈 새 기능 개발을 위해 리비전 트리의 이력에서 브랜치를 만든다는 개념과도 더 자연스럽게 맞물립니다. DVCS는 중앙 집중식 VCS가 제공하지 않거나 제공할 수 없는 수많은 추가 기능도 제공합니다.

이 PEP는 위에서 설명한 이점을 얻기 위해, Python이 사용하는 Subversion을 현재 널리 쓰이는 DVCS 중 하나로 바꾸는 방안을 검토합니다. 이 PEP가 마무리된다고 해서 반드시 DVCS로 전환한다는 보장은 없습니다. 확실히 나은 시스템을 찾지 못해 svn을 계속 사용하게 될 가능성도 충분히 있습니다. 그렇게 되면 DVCS가 발전하는 상황에 맞춰 이 PEP를 앞으로 다시 검토하고 수정할 것입니다.

용어

공통 용어를 정하는 일은 생각보다 어렵습니다. 무엇보다 VCS마다 같은 용어로 미묘하게 다른 작업, 객체, 개념을 가리키기 때문입니다. 가능한 한 각 개념을 일반적으로 정의하려고 하지만, 자세한 내용은 각 시스템의 용어집을 참고하십시오. 다음은 각 VCS의 대표적인 웹 참고 자료에서 가져온 기본 용어 참고 자료입니다. 각 DVCS의 용어집도 참고할 수 있습니다:

브랜치(branch)
하나의 개발 흐름이며, 시간순으로 정렬된 리비전의 모음입니다.
체크아웃(checkout)/작업 사본(working copy)/작업 트리(working tree)
개발자가 편집할 수 있고 브랜치에 연결된 코드 트리입니다.
인덱스(index)
리비전을 만드는 “스테이징 영역”입니다(git에만 있습니다).
저장소(repository)
브랜치로 구성된 리비전의 모음입니다.
클론(clone)
브랜치나 저장소의 완전한 사본입니다.
커밋(commit)
리비전을 저장소에 기록하는 것입니다.
병합(merge)
한 브랜치/저장소의 모든 변경 사항과 이력을 다른 브랜치/저장소에 적용하는 것입니다.
풀(pull)
원래의 브랜치/저장소에서 체크아웃/클론을 업데이트하는 것입니다. 원래의 브랜치/저장소는 원격일 수도 있고 로컬일 수도 있습니다.
푸시(push)/게시(publish)
리비전 하나와 그 리비전이 의존하는 모든 리비전을 한 저장소에서 다른 저장소로 복사하는 것입니다.
체리픽(cherry-pick)
한 브랜치에서 다른 브랜치로 하나 이상의 특정 리비전을 병합하는 것입니다. 대상 브랜치는 다른 저장소에 있을 수도 있으며, 해당 리비전이 의존하는 리비전 없이 병합할 수도 있습니다.
rebase
브랜치를 “분리”하여 새로운 분기 지점으로 옮기는 것입니다. 즉, 커밋을 실제로 이루어진 시점이 아니라 브랜치의 시작 부분으로 옮깁니다.

일반적인 워크플로

현재 Python 코어 개발자의 일반적인 워크플로는 다음과 같습니다:

  • 커밋/푸시할 수 있을 만큼 안정될 때까지 체크아웃에서 코드를 편집합니다.
  • 마스터 저장소에 커밋합니다.

꽤 단순한 워크플로이지만 단점이 있습니다. 우선, 저장소와 관련된 모든 작업은 네트워크 때문에 시간이 걸리므로, 커밋/푸시가 꼭 최대한 원자적으로 이루어지지는 않는 경향이 있습니다. 또한 체크아웃 디렉터리를 재귀적으로 복사하는 것 외에는 새 체크아웃을 저렴하게 만들 방법이 딱히 없다는 단점도 있습니다.

DVCS를 사용하면 워크플로가 다음과 비슷해집니다:

  • 마스터 저장소의 로컬 클론에서 브랜치를 만듭니다.
  • 코드를 편집하면서 원자적인 단위로 커밋합니다.
  • 브랜치를 메인라인에 병합하고,
  • 모든 커밋을 마스터 저장소에 푸시합니다.

가능한 단계는 더 많지만, 이 워크플로는 현재 가능한 것보다 마스터 저장소에 훨씬 덜 의존합니다. 디스크 속도로 로컬에서 커밋할 수 있으므로, 코어 개발자는 원자적 커밋을 훨씬 더 자주 할 수 있고, 코드에 여러 가지 일을 한꺼번에 하는 커밋을 최소화할 수 있습니다. 또한 브랜치를 사용하면 (원하는 경우) 변경 사항을 다른 개발자가 만드는 다른 변경 사항으로부터 격리할 수 있습니다. 브랜치는 비용이 저렴하므로, 버그 하나나 새 기능 하나처럼 특정한 문제 하나를 다루는 작은 브랜치를 많이 만들고 유지하기가 쉽습니다. DVCS의 더 정교한 기능을 사용하면, 공식 메인라인이 진행되는 동안 장기간 이어지는 개발 브랜치를 개발자가 더 쉽게 추적할 수 있습니다.

후보

이름 약칭 버전 2.x 트렁크 미러 3.x 트렁크 미러
Bazaar bzr 1.12 http://code.python.org/python/trunk http://code.python.org/python/3.0
Mercurial hg 1.2.0 http://code.python.org/hg/trunk/ http://code.python.org/hg/branches/py3k/
git N/A 1.6.1 git://code.python.org/python/trunk git://code.python.org/python/branches/py3k

이 PEP에서는 darcs, arch, monotone을 다루지 않습니다. 이 DVCS들의 주된 문제는 다른 DVCS들처럼 아주 매력적인 기능을 제공하지도 않으면서 굳이 지원할 만큼 널리 쓰이지도 않는다는 점입니다. 또한 Arch와 darcs에는 심각한 성능 문제가 있는데, 가까운 시일 안에 해결될 가능성은 낮아 보입니다.

상호 운용성

어떤 DVCS를 쓸지 이미 정했고 로컬 미러를 직접 유지 관리할 의향이 있는 사람들을 위해 말하자면, 세 DVCS 모두 git “fast-import” 변경 집합 형식으로 서로 데이터를 주고받을 수 있습니다. git은 당연히 이를 기본으로 지원합니다. Bazaar의 기본 지원은 활발히 개발 중이며, 2009년 2월 중순 현재 초기 평가도 좋습니다. Mercurial은 hg convert 명령으로 가져오기를 지원하는데 그 방식이 독특합니다. 내보내기에는 third-party fast-import support를 사용할 수 있습니다. 또한 Tailor 도구를 사용하면, 후보 형식 중 어떤 형식으로 된 공식 저장소든 이를 기반으로 원하는 형식의 로컬 미러를 자동으로 유지 관리할 수 있습니다.

사용 시나리오

Subversion을 DVCS로 바꿀지, 바꾼다면 어떤 DVCS로 바꿀지 결정하는 데 가장 좋은 방법은 아마도 개발자(코어 개발자와 그 밖의 개발자)가 실제로 다루어야 하는 몇 가지 사용 시나리오를 수행하려면 무엇이 필요한지 살펴보는 것입니다. 각 사용 시나리오에서는 해당 시나리오가 무엇인지 설명하고, 기본 단계를 글머리 기호 목록으로 보여 주며(단계는 VCS마다 조금씩 다를 수 있습니다), 여러 VCS(Subversion 포함)에서 그 시나리오를 수행하는 방법을 제시합니다.

별도로 밝히지 않은 경우, 각 VCS마다 작성자 한 명이 모든 시나리오의 구현 작성을 맡았습니다.

이름 VCS
Brett svn
Barry bzr
Alexandre hg
Stephen git

초기 설정

일부 DVCS는 초기 설정을 미리 해 두면 몇 가지 이점을 얻을 수 있습니다. 이 절에서는 도구를 더 잘 활용하기 위해 사용 시나리오를 실행하기 전에 할 수 있는 작업을 다룹니다.

모든 DVCS는 프로젝트 식별 정보 설정을 지원합니다. 중앙 집중식 시스템과 달리, DVCS는 이메일 주소로 커밋을 식별합니다. (접근 제어는 일반적으로 ssh나 콘솔 로그인 같은 DVCS 외부 메커니즘으로 이루어집니다.) 이 식별 정보에는 전체 이름을 연결할 수 있습니다.

모든 DVCS는 시스템에 질의하여 이 정보의 근삿값을 얻지만, 그 값이 원하는 값이 아닐 수도 있습니다. 또한 이 정보를 사용자별로, 그리고 프로젝트별로 설정하는 기능도 지원합니다. 이러한 속성을 설정하는 편의 명령은 도구마다 다르지만, 모든 도구에서 설정 파일을 직접 편집할 수 있습니다.

일부 VCS는 체크아웃/체크인 시 줄 끝(EOL) 변환을 지원합니다.

svn

필수 설정은 없지만, 개발 FAQ의 지침을 따르는 것을 권장합니다.

bzr

설정이 필요하지는 않지만, 훨씬 빠르고 공간 효율적으로 로컬 브랜치를 만들려면 모든 Python 브랜치를 담을 공유 저장소를 만들어야 합니다. 공유 저장소는 사실 .bzr 디렉터리를 포함하는 상위 디렉터리일 뿐입니다. bzr는 리비전을 커밋할 때 로컬 디렉터리에서 시작하여 파일 시스템을 거슬러 올라가며 리비전을 저장할 .bzr 디렉터리를 찾습니다. 여러 브랜치가 리비전을 공유하므로 사용하는 디스크 공간이 줄어듭니다. 다음과 같이 하십시오:

cd ~/projects
bzr init-repo python
cd python

이제 모든 Python 브랜치는 ~/projects/python 안에 만들어야 합니다.

또한 ~/.bzr/bazaar.conf 및 ~/.bzr/locations.conf 파일에 넣어 Python 코드 작업의 기본값을 정할 수 있는 설정도 몇 가지 있습니다. 어느 것도 필수는 아니지만, 일부는 권장됩니다. 예를 들어 저는 모든 커밋에 gpg 서명을 하도록 제안하고 싶지만, 개발자에게는 너무 높은 장벽일 수도 있습니다. 또한 기본적으로 브랜치를 푸시하려는 위치에 따라 기본 푸시 위치를 설정할 수 있습니다. 마스터 브랜치에 쓰기 권한이 있다면 그 푸시 위치는 code.python.org가 될 수 있습니다. 그렇지 않다면 Launchpad 같은 무료 Bazaar 코드 호스팅 서비스가 될 수 있습니다. Bazaar가 선택된다면, 어떤 정책과 권장 사항을 둘지 결정해야 합니다.

최소한 이메일 주소는 설정해 두기를 권합니다:

bzr whoami "Firstname Lastname <email.address@example.com>"

아래의 hg 및 git과 마찬가지로, 이메일 주소(사실상 거의 모든 매개변수)를 저장소별로 설정하는 방법이 있습니다. $HOME/.bazaar/locations.conf 파일의 설정으로 이를 할 수 있으며, 이 파일은 다른 DVCS와 마찬가지로 ini 스타일 형식을 사용합니다. 자세한 내용은 Bazaar 문서를 참조하십시오. 다만 대부분은 이 논의와 관련이 없습니다.

hg

최소한 사용자 이름은 설정해야 합니다. 그러려면 홈 디렉터리에 .hgrc 파일을 만들고 다음 내용을 추가하십시오:

[ui]
username = Firstname Lastname <email.address@example.com>

Windows를 사용하고 있고 사용하는 도구가 Unix 스타일 줄바꿈을 지원하지 않는다면, 설정에 다음을 추가하여 자동 줄바꿈 변환을 활성화할 수 있습니다:

[extensions]
win32text =

이 옵션들은 ~/.hgrc 대신 <repo>/.hg/hgrc를 수정하여 특정 저장소에만 로컬로 설정할 수도 있습니다.

git

필요한 설정은 없습니다. 하지만 git은 약간만 준비하면 작업을 더 수월하게 해 주는 여러 기능을 지원합니다. git은 작업 공간, 사용자, 시스템 수준에서 기본값을 설정하는 기능을 지원합니다. 시스템 수준은 이 PEP의 범위를 벗어납니다. 사용자 설정 파일은 Unix 계열 시스템에서 $HOME/.gitconfig이고, 작업 공간 설정 파일은 $REPOSITORY/.git/config입니다.

git-config 도구를 사용하여 user.name과 user.email 설정을 전역적으로(시스템 로그인 계정에 대해) 또는 지역적으로(특정 git 작업 사본에 대해) 지정할 수 있으며, 설정 파일(위의 Mercurial 절에서 보인 것과 같은 형식입니다)을 직접 편집할 수도 있습니다.:

# my full name doesn't change
# note "--global" flag means per user
# (system-wide configuration is set with "--system")
git config --global user.name 'Firstname Lastname'
# but use my Pythonic email address
cd /path/to/python/repository
git config user.email email.address@python.example.com

Windows를 사용하고 있다면 git-config를 사용하여 core.autocrlf와 core.safecrlf 설정을 true로 지정하는 것이 좋습니다.:

# check out files with CRLF line endings rather than Unix-style LF only
git config --global core.autocrlf true
# scream if a transformation would be ambiguous
# (eg, a working file contains both naked LF and CRLF)
# and check them back in with the reverse transformation
git config --global core.safecrlf true

저장소에는 보통 VCS에 등록할 일이 거의 또는 전혀 없는 파일 이름을 지정한 .gitignore 파일이 들어 있지만, 따로 지정하고 싶은 개인적인 관례(예: 로그 메시지를 항상 “.msg”라는 임시 파일에서 편집하는 것)가 있을 수 있습니다.:

# tell git where my personal ignores are
git config --global core.excludesfile ~/.gitignore
# I use .msg for my long commit logs, and Emacs makes backups in
# files ending with ~
# these are globs, not regular expressions
echo '*~' >> ~/.gitignore
echo '.msg' >> ~/.gitignore

여러 브랜치를 사용한다면, 다른 VCS와 마찬가지로 모든 객체를 공통 객체 저장소에 두어 많은 공간을 절약할 수 있습니다. 브랜치의 출처가 서로 다른 저장소였다면 다운로드 시간도 절약할 수 있습니다. 객체가 업스트림 저장소에 없었더라도 여러분의 저장소 안에서는 브랜치 간에 공유되기 때문입니다. git은 공간과 시간 면에서 매우 효율적이고 여러 최적화를 자동으로 적용하므로 이 구성은 선택 사항입니다. (예시는 생략합니다.)

일회성 체크아웃

코어 개발자가 아닌 사람으로서, 저는 버그를 수정하는 일회성 패치를 만들어 공개하여 코어 개발자가 메인라인에 포함할지 검토할 수 있게 하고 싶습니다.

  • trunk를 체크아웃/브랜치/클론합니다.
  • 코드를 편집합니다.
  • 패치를 생성합니다(VCS가 가장 잘 지원하는 방식, 예를 들어 브랜치 이력을 기반으로 합니다).
  • 리뷰어의 의견을 받고 지적된 문제를 해결합니다.
  • 코어 개발자가 커밋할 두 번째 패치를 생성합니다.

svn

svn checkout http://svn.python.org/projects/python/trunk
cd trunk
# Edit some code.
echo "The cake is a lie!" > README
# Since svn lacks support for local commits, we fake it with patches.
svn diff >> commit-1.diff
svn diff >> patch-1.diff
# Upload the patch-1 to bugs.python.org.
# Receive reviewer comments.
# Edit some code.
echo "The cake is real!" > README
# Since svn lacks support for local commits, we fake it with patches.
svn diff >> commit-2.diff
svn diff >> patch-2.diff
# Upload patch-2 to bugs.python.org

bzr

bzr branch http://code.python.org/python/trunk
cd trunk
# Edit some code.
bzr commit -m 'Stuff I did'
bzr send -o bundle
# Upload bundle to bugs.python.org
# Receive reviewer comments
# Edit some code
bzr commit -m 'Respond to reviewer comments'
bzr send -o bundle
# Upload updated bundle to bugs.python.org

bundle 파일은 일종의 슈퍼 패치입니다. patch(1)로 읽을 수 있지만, 추가 메타데이터가 들어 있어서 bzr merge에 넘기면 이력까지 모두 갖춘, 완전히 사용 가능한 브랜치를 만들 수 있습니다. 아래의 Patch Review 절을 참조하십시오.

hg

hg clone http://code.python.org/hg/trunk
cd trunk
# Edit some code.
hg commit -m "Stuff I did"
hg outgoing -p > fixes.patch
# Upload patch to bugs.python.org
# Receive reviewer comments
# Edit some code
hg commit -m "Address reviewer comments."
hg outgoing -p > additional-fixes.patch
# Upload patch to bugs.python.org

hg outgoing에는 해당 플래그가 없지만, 대부분의 Mercurial 명령은 --git 명령을 통해 git의 확장 패치 형식을 지원합니다. 이것을 .hgrc 파일에 설정해 두면 패치를 생성하는 모든 명령이 확장 형식을 사용합니다.

git

패치는 git diff master > stuff-i-did.patch로도 만들 수 있지만, git format-patch | git am은 일반 patch가 처리하지 못하는 몇 가지 요령(빈 파일, 이름 변경 등)을 알고 있습니다. git은 커밋 메시지에서 “Stuff I did”를 가져와 0001-Stuff-I-did.patch라는 파일 이름을 만듭니다. git-format-patch 형식에 대한 설명은 아래의 패치 리뷰를 참고하십시오.

# Get the mainline code.
git clone git://code.python.org/python/trunk
cd trunk
# Edit some code.
git commit -a -m 'Stuff I did.'
# Create patch for my changes (i.e, relative to master).
git format-patch master
git tag stuff-v1
# Upload 0001-Stuff-I-did.patch to bugs.python.org.
# Time passes ... receive reviewer comments.
# Edit more code.
git commit -a -m 'Address reviewer comments.'
# Make an add-on patch to apply on top of the original.
git format-patch stuff-v1
# Upload 0001-Address-reviewer-comments.patch to bugs.python.org.

변경 사항 되돌리기

코어 개발자로서, 저는 아직 메인라인에 포함될 준비가 되지 않은 변경을 되돌리고 싶습니다.

  • 원치 않는 변경 사항을 되돌립니다.
  • 패치를 서버에 푸시합니다.

svn

# Assume the change to revert is in revision 40
svn merge -c -40 .
# Resolve conflicts, if any.
svn commit -m "Reverted revision 40"

bzr

# Assume the change to revert is in revision 40
bzr merge -r 40..39
# Resolve conflicts, if any.
bzr commit -m "Reverted revision 40"

되돌리려는 변경 사항이 가장 마지막에 이루어진 것이라면 bzr uncommit만 사용하면 된다는 점에 유의하십시오.

hg

# Assume the change to revert is in revision 9150dd9c6d30
hg backout --merge -r 9150dd9c6d30
# Resolve conflicts, if any.
hg commit -m "Reverted changeset 9150dd9c6d30"
hg push

참고로, 로컬 저장소에 커밋했지만 아직 다른 저장소로 푸시하지 않은 변경 사항은 “hg rollback”과 “hg strip”을 사용하여 되돌릴 수 있습니다.

git

# Assume the change to revert is the grandfather of a revision tagged "newhotness".
git revert newhotness~2
# Resolve conflicts if any.  If there are no conflicts, the commit
# will be done automatically by "git revert", which prompts for a log.
git commit -m "Reverted changeset 9150dd9c6d30."
git push

패치 검토

코어 개발자로서, 승인된 변경 사항만 Python에 추가되도록 하기 위해 다른 사람들이 제출한 패치를 검토하고 싶습니다.

코어 개발자는 다른 사람들이 제출한 그대로의 패치를 검토해야 합니다. 이를 위해서는 패치를 적용하고, 테스트한 다음, 변경 사항을 버려야 합니다. 코어 개발자가 이미 trunk의 체크아웃/브랜치/클론을 가지고 있다고 가정할 수 있습니다.

  • trunk에서 브랜치를 만듭니다.
  • 패치 제출자가 생성한 패치를 아무런 코멘트 없이 그대로 적용합니다.
  • 패치를 서버에 푸시합니다.
  • 이제 쓸모없어진 브랜치를 삭제합니다.

svn

Subversion에는 이 PEP에서 정의한 것과 같은 “브랜치”라는 개념이 없기 때문에 이러한 개발 방식에 그다지 잘 들어맞지 않습니다. 대신 개발자는 패치를 테스트하기 위해 또 다른 체크아웃을 만들거나 서버에 브랜치를 만들어야 합니다. 지금까지 코어 개발자들은 개별 패치를 다룰 때 “서버에 브랜치를 만드는” 방식을 택하지 않았습니다. 이 시나리오에서는 개발자가 작업용으로 trunk의 로컬 체크아웃을 만든다고 가정합니다.:

cp -r trunk issue0000
cd issue0000
patch -p0 < __patch__
# Review patch.
svn commit -m "Some patch."
cd ..
rm -r issue0000

또 다른 방법은 한 번에 하나의 체크아웃만 유지하면서, svn diff와 svn revert -R을 함께 사용하여 여러분이 만들었을 수 있는 독립적인 변경 사항을 따로 보관해 두는 것입니다.

bzr

bzr branch trunk issueNNNN
# Download `patch` bundle from Roundup
bzr merge patch
# Review patch
bzr commit -m'Patch NNN by So N. So' --fixes python:NNNN
bzr push bzr+ssh://me@code.python.org/trunk
rm -rf ../issueNNNN

또는, 어차피 이 변경 사항을 trunk에 커밋하게 될 것이므로 그냥 체크아웃을 할 수도 있습니다. 그러면 브랜치(즉, 모든 리비전)는 계속 서버에 남아 있으면서 로컬 작업 트리를 얻을 수 있습니다. 이는 svn 모델과 비슷하며, 패치를 더 빠르게 검토할 수 있게 해 줄 수도 있습니다. 이 경우에는 푸시할 필요가 없습니다.:

bzr checkout trunk issueNNNN
# Download `patch` bundle from Roundup
bzr merge patch
# Review patch
bzr commit -m'Patch NNNN by So N. So' --fixes python:NNNN
rm -rf ../issueNNNN

hg

hg clone trunk issue0000
cd issue0000
# If the patch was generated using hg export, the user name of the
# submitter is automatically recorded. Otherwise,
# use hg import --no-commit submitted.diff and commit with
# hg commit -u "Firstname Lastname <email.address@example.com>"
hg import submitted.diff
# Review patch.
hg push ssh://alexandre@code.python.org/hg/trunk/

git

git-format-patch로 만든 패치를 가정합니다. 이 파일은 하나 이상의 패치를 담은 Unix mbox 파일이며, 각 패치는 RFC 2822 메시지 형식으로 되어 있습니다. git-am은 다음과 같이 각 메시지를 하나의 커밋으로 해석합니다. 패치 작성자는 From: 헤더에서, 날짜는 Date 헤더에서 가져옵니다. 커밋 로그는 제목 줄의 내용, 빈 줄, 그리고 패치가 시작되기 전까지의 메시지 본문을 이어 붙여 만듭니다.:

cd trunk
# Create a branch in case we don't like the patch.
# This checkout takes zero time, since the workspace is left in
# the same state as the master branch.
git checkout -b patch-review
# Download patch from bugs.python.org to submitted.patch.
git am < submitted.patch
# Review and approve patch.
# Merge into master and push.
git checkout master
git merge patch-review
git push

백포트

코어 개발자로서 저는 세 버전 모두에서 문제를 고칠 수 있도록 2.6, 2.7, 3.0, 3.1에 패치를 적용하고 싶습니다.

항상 최첨단 버전과 최신 릴리스 버전을 함께 개발하고 있기 때문에, 현재 Python에서는 네 개의 브랜치에서 동시에 작업이 진행되고 있습니다. 따라서 변경 사항이 여러 브랜치로 쉽게 전파되는 것이 중요합니다.

svn

Python은 svnmerge를 사용하므로, 변경 사항은 트렁크(2.7)에서 시작한 다음 2.6 릴리스 버전으로 병합됩니다. 변경 사항을 3.x 계열에 반영하려면 먼저 3.1에 병합하고 손본 다음 3.0에 병합합니다(2.7 -> 2.6; 2.7 -> 3.1 -> 3.0).

이는 패치를 먼저 2.6에 추가한 다음 더 새로운 버전으로 끌어올리는 포워드 포트 전략(2.6 -> 2.7 -> 3.0 -> 3.1)과 대조됩니다.

# Assume patch applied to 2.7 in revision 0000.
cd release26-maint
svnmerge merge -r 0000
# Resolve merge conflicts and make sure patch works.
svn commit -F svnmerge-commit-message.txt  # revision 0001.
cd ../py3k
svnmerge merge -r 0000
# Same as for 2.6, except Misc/NEWS changes are reverted.
svn revert Misc/NEWS
svn commit -F svnmerge-commit-message.txt  # revision 0002.
cd ../release30-maint
svnmerge merge -r 0002
svn commit -F svnmerge-commit-message.txt  # revision 0003.

bzr

Bazaar는 리비전을 수동으로 체리픽하는 기능을 지원하므로 이 경우 꽤 간단합니다. 아래 예제에서는 리비전 번호 대신 리비전 ID를 지정할 수도 있었지만, 보통은 그럴 필요가 없습니다. Martin Pool은 “일반적으로 지원되는 가장 오래된 브랜치에서 먼저 수정한 다음, 이후 릴리스로 포워드 병합하는 것을 권장합니다.”라고 제안합니다.:

# Assume patch applied to 2.7 in revision 0000
cd release26-maint
bzr merge ../trunk -c 0000
# Resolve conflicts and make sure patch works
bzr commit -m 'Back port patch NNNN'
bzr push bzr+ssh://me@code.python.org/trunk
cd ../py3k
bzr merge ../trunk -r 0000
# Same as for 2.6 except Misc/NEWS changes are reverted
bzr revert Misc/NEWS
bzr commit -m 'Forward port patch NNNN'
bzr push bzr+ssh://me@code.python.org/py3k

hg

Mercurial은 다른 DVCS와 마찬가지로 Python 코어 개발자들이 패치를 백포트할 때 사용하는 현재 워크플로를 잘 지원하지 않습니다. 현재는 버그 수정을 먼저 개발 메인라인(즉, 트렁크)에 적용한 다음, 유지보수 브랜치로 백포트하고, 필요에 따라 py3k 브랜치로 포워드 포트합니다. 이 워크플로에는 개별 변경 사항을 체리픽하는 기능이 필요합니다. Mercurial의 transplant 확장이 이 기능을 제공합니다. 다음은 이 워크플로를 사용하는 시나리오의 예입니다:

cd release26-maint
# Assume patch applied to 2.7 in revision 0000
hg transplant -s ../trunk 0000
# Resolve conflicts, if any.
cd ../py3k
hg pull ../trunk
hg merge
hg revert Misc/NEWS
hg commit -m "Merged trunk"
hg push

위 예제에서 transplant는 현재의 svnmerge 명령과 매우 비슷하게 동작합니다. 리비전 없이 transplant를 호출하면, 이 명령은 여러 변경 사항을 이식할 때 유용한 대화형 루프를 실행합니다. 또 다른 유용한 기능은 변경 집합을 프로그래밍 방식으로 수정하는 데 사용할 수 있는 –filter 옵션입니다(예를 들어 Misc/NEWS에 대한 변경 사항을 자동으로 제거하는 데 사용할 수 있습니다).

기존 워크플로 대신, 버그 수정을 지원되는 가장 오래된 릴리스에 커밋한 다음 이 수정 사항을 더 최신 브랜치로 상향 병합하여 변경 집합 이식을 피할 수도 있습니다.

cd release25-maint
hg import fix_some_bug.diff
# Review patch and run test suite. Revert if failure.
hg push
cd ../release26-maint
hg pull ../release25-maint
hg merge
# Resolve conflicts, if any. Then, review patch and run test suite.
hg commit -m "Merged patches from release25-maint."
hg push
cd ../trunk
hg pull ../release26-maint
hg merge
# Resolve conflicts, if any, then review.
hg commit -m "Merged patches from release26-maint."
hg push

이 방식은 히스토리를 비선형으로 만들어 따라가기가 조금 더 어려워지지만, 지원되는 모든 릴리스에서 버그를 수정하도록 장려합니다. 게다가 병합할 특정 리비전 ID를 찾을 필요가 없으므로, 백포트할 변경 사항이 많을 때 더 잘 확장됩니다.

git

git에서는 관련된 마스터 저장소 브랜치를 모두 담은 작업 공간을 두게 됩니다. git cherry-pick은 저장소 간에는 동작하지 않으므로, 브랜치들이 같은 저장소에 있어야 합니다.

# Assume patch applied to 2.7 in revision release27~3 (4th patch back from tip).
cd integration
git checkout release26
git cherry-pick release27~3
# If there are conflicts, resolve them, and commit those changes.
# git commit -a -m "Resolve conflicts."
# Run test suite. If fixes are necessary, record as a separate commit.
# git commit -a -m "Fix code causing test failures."
git checkout master
git cherry-pick release27~3
# Do any conflict resolution and test failure fixups.
# Revert Misc/NEWS changes.
git checkout HEAD^ -- Misc/NEWS
git commit -m 'Revert cherry-picked Misc/NEWS changes.' Misc/NEWS
# Push both ports.
git push release26 master

특정 브랜치에서 (체리픽이 아니라) 정기적으로 병합하고 있다면, 특정 커밋을 병합한 다음 되돌려서 그 커밋이 나중에 실수로 병합되지 않도록 막을 수 있습니다. 이 방법으로는 체리픽으로 원하지 않는 패치를 가져오는 것까지 막을 수는 없으며, 이 기법을 쓰려면 병합되지 않기를 원하는 모든 것을 이런 식으로 막아야 합니다. 이 점에서 svn과 다른지는 잘 모르겠습니다.

cd trunk
# Merge in the alpha tested code.
git merge experimental-branch
# We don't want the 3rd-to-last commit from the experimental-branch,
# and we don't want it to ever be merged.
# The notation "^N" means Nth parent of the current commit. Thus HEAD^2^1^1
# means the first parent of the first parent of the second parent of HEAD.
git revert HEAD^2^1^1
# Propagate the merge and the prohibition to the public repository.
git push

새 기능의 공동 개발

때때로 코어 개발자는 여러 개발자와 함께 주요 기능을 작업하게 됩니다. 코어 개발자로서 저는 다른 개발자와 협업할 수 있도록 기능 브랜치를 공용 공개 위치에 게시할 수 있기를 원합니다.

이를 위해서는 다른 개발자가 접근할 수 있는 서버에 브랜치를 만들어야 합니다. 모든 DVCS는 저장소 호스트를 적절히 설정하면 개발자가 이미 커밋할 수 있는 호스트에 새 저장소를 만드는 것을 지원합니다. 이는 개념적으로 svn의 기존 샌드박스와 비슷하지만, 저장소 초기화의 세부 사항은 다를 수 있습니다.

코어 개발자가 아닌 사람을 위해서는 어느 정도 공개적으로 접근할 수 있는 다양한 저장소 호스팅 서비스가 있습니다. Bazaar에는 Launchpad가 있고, Mercurial에는 bitbucket.org가 있으며, git에는 GitHub가 있습니다. 또한 모두 자체 서버를 운영하는 개발자를 위한 사용하기 쉬운 CGI 인터페이스를 갖추고 있습니다.

  • 트렁크에서 브랜치를 만드십시오.
  • 서버의 브랜치에서 풀하십시오.
  • 트렁크에서 풀하십시오.
  • 병합한 결과를 트렁크에 푸시하십시오.

svn

# Create branch.
svn copy svn+ssh://pythondev@svn.python.org/python/trunk svn+ssh://pythondev@svn.python.org/python/branches/NewHotness
svn checkout svn+ssh://pythondev@svn.python.org/python/branches/NewHotness
cd NewHotness
svnmerge init
svn commit -m "Initialize svnmerge."
# Pull in changes from other developers.
svn update
# Pull in trunk and merge to the branch.
svnmerge merge
svn commit -F svnmerge-commit-message.txt

어떤 DVCS를 채택할지가 작업이 완료되기 전에 결정되었기 때문에 이 시나리오는 미완성입니다.

이슈 의존성의 분리

때때로 이슈를 작업하다 보면, 작업 중인 문제가 실제로는 여러 작은 이슈로 이루어진 복합 이슈라는 것이 드러납니다. 현재 작업을 그대로 두고 별도의 이슈 작업을 시작할 수 있으면, 이슈들을 하나의 큰 단위로 뭉치는 대신 개별 작업 단위로 분리하는 데 매우 도움이 됩니다.

  • 브랜치 A를 만드십시오(예: urllib에 버그가 있는 경우).
  • 코드를 편집하십시오.
  • 브랜치 A가 의존하는 새 브랜치 B를 만듭니다(예: urllib 버그로 인해 소켓 버그가 드러나는 경우).
  • 브랜치 B에서 코드를 편집하십시오.
  • 브랜치 B를 커밋하십시오.
  • 브랜치 A에서 코드를 편집하십시오.
  • 브랜치 A를 커밋하십시오.
  • 정리하십시오.

svn

svn은 비용이 적게 드는 브랜치 기능이 없는 점을 보완하기 위해, 파일을 하나의 changelist에 연결하는 changelist 옵션을 제공합니다. 이는 커밋 수준에서 연결할 수 있는 것만큼 강력하지는 않습니다. 또한 changelist 간의 의존성을 표현할 방법도 없습니다.

cp -r trunk issue0000
cd issue0000
# Edit some code.
echo "The cake is a lie!" > README
svn changelist A README
# Edit some other code.
echo "I own Python!" > LICENSE
svn changelist B LICENSE
svn ci -m "Tell it how it is." --changelist B
# Edit changelist A some more.
svn ci -m "Speak the truth." --changelist A
cd ..
rm -rf issue0000

bzr

다음은 소켓 버그를 수정하러 잠시 우회하는 동안 bzr shelf(현재 bzr의 표준 기능)를 사용하여 일부 변경 사항을 임시로 보관해 두는 방법입니다.

bzr branch trunk bug-0000
cd bug-0000
# Edit some code. Dang, we need to fix the socket module.
bzr shelve --all
# Edit some code.
bzr commit -m "Socket module fixes"
# Detour over, now resume fixing urllib
bzr unshelve
# Edit some code

또 다른 방법은 loom 플러그인을 사용하는 것입니다. loom은 쌓여 있는 의존성을 자동으로 처리해 주므로, 서로 의존하는 브랜치에서 작업하는 일을 크게 단순화할 수 있습니다. loom을 서로 의존하는 브랜치(loom 용어로는 “스레드(thread)”라고 합니다)의 스택이라고 생각하십시오. 이 스택에서는 스레드 사이를 위아래로 이동하고, 변경 사항을 스택 위쪽의 후손 스레드로 병합하고, 스레드 간 diff를 만드는 등의 작업을 쉽게 할 수 있습니다. 때로는 리뷰나 커밋을 위해 loom 스레드를 별도의 브랜치로 내보내야 하거나 내보내고 싶을 수도 있습니다. 상위 스레드는 하위 스레드의 모든 변경 사항을 자동으로 포함합니다.

bzr branch trunk bug-0000
cd bug-0000
bzr loomify --base trunk
bzr create-thread fix-urllib
# Edit some code. Dang, we need to fix the socket module first.
bzr commit -m "Checkpointing my work so far"
bzr down-thread
bzr create-thread fix-socket
# Edit some code
bzr commit -m "Socket module fixes"
bzr up-thread
# Manually resolve conflicts if necessary
bzr commit -m 'Merge in socket fixes'
# Edit me some more code
bzr commit -m "Now that socket is fixed, complete the urllib fixes"
bzr record done

보너스로, 다른 누군가가 방금 여러분이 한 것과 정확히 같은 방식으로 소켓 모듈을 수정했다고 가정해 봅시다. 어쩌면 이 사람은 여러분의 fix-socket 스레드(소켓 수정 스레드)를 가져와 그것만 트렁크에 적용했을 수도 있습니다. 여러분은 그 사람의 변경 사항을 자신의 loom에 병합하고, 이제 불필요해진 fix-socket 스레드(소켓 수정 스레드)를 삭제하고 싶을 것입니다.

bzr down-thread trunk
# Get all new revisions to the trunk. If you've done things
# correctly, this will succeed without conflict.
bzr pull
bzr up-thread
# See? The fix-socket thread is now identical to the trunk
bzr commit -m 'Merge in trunk changes'
bzr diff -r thread: | wc -l # returns 0
bzr combine-thread
bzr up-thread
# Resolve any conflicts
bzr commit -m 'Merge trunk'
# Now our top-thread has an up-to-date trunk and just the urllib fix.

hg

한 가지 방법은 shelve 확장을 사용하는 것입니다. 이 확장은 Mercurial에 포함되어 있지 않지만 설치하기 쉽습니다. shelve를 사용하면 임시로 치워 둘 변경 사항을 선택할 수 있습니다.

hg clone trunk issue0000
cd issue0000
# Edit some code (e.g. urllib).
hg shelve
# Select changes to put aside
# Edit some other code (e.g. socket).
hg commit
hg unshelve
# Complete initial fix.
hg commit
cd ../trunk
hg pull ../issue0000
hg merge
hg commit
rm -rf ../issue0000

Mercurial로 이 시나리오에 접근하는 다른 방법도 여러 가지 있습니다. Alexander Solovyov는 Mercurial 메일링 리스트에서 몇 가지 alternative approaches를 제시했습니다.

git

cd trunk
# Edit some code in urllib.
# Discover a bug in socket, want to fix that first.
# So save away our current work.
git stash
# Edit some code, commit some changes.
git commit -a -m "Completed fix of socket."
# Restore the in-progress work on urllib.
git stash apply
# Edit me some more code, commit some more fixes.
git commit -a -m "Complete urllib fixes."
# And push both patches to the public repository.
git push

보너스: 여러분이 작업을 천천히 하는 사이에 다른 누군가가 방금 여러분이 한 것과 같은 방식으로 소켓을 수정하여 트렁크에 반영했다고 가정해 봅시다. 이 경우 여러분의 브랜치가 최신 상태가 아니므로 push가 실패합니다. 수정이 한 줄짜리였다면, 글자 하나하나까지 정확히 같을 가능성이 매우 높습니다. git은 이를 알아차리므로 여러분은 더 할 일이 없습니다. git이 조용히 병합해 줍니다.

운이 그렇게 좋지 않다고 가정해 봅시다:

# Update your branch.
git pull git://code.python.org/public/trunk master

# git has fetched all the necessary data, but reports that the
# merge failed.  We discover the nearly-duplicated patch.
# Neither our version of the master branch nor the workspace has
# been touched.  Revert our socket patch and pull again:
git revert HEAD^
git pull git://code.python.org/public/trunk master

Bazaar 및 Mercurial과 마찬가지로 git에도 패치 스택을 관리하는 확장이 있습니다. Andrew Morton이 만든 원조 Quilt를 사용할 수도 있고, Mercurial Queues(Mercurial 큐)나 Bazaar loom과 비슷한 방식으로 대규모 패치 집합에 대한 패치 추적 기능을 VCS에 통합한 StGit(“stacked git”)도 있습니다.

Python 릴리스 진행하기

DVCS를 사용하면 PEP 101은 어떻게 달라집니까?

bzr

바뀌기는 하겠지만 크게 바뀌지는 않을 것입니다. 유지보수 브랜치를 만들 때는 svn cp를 하는 대신 새 위치로 push하기만 하면 됩니다. 태그는 완전히 다릅니다. svn에서 태그는 디렉터리 복사본이지만, bzr(그리고 제 추측으로는 hg)에서 태그는 특정 브랜치의 리비전에 붙인 기호 이름일 뿐입니다. release.py 스크립트는 bzr 명령을 사용하도록 바뀌어야 할 것입니다. DVCS(특히 bzr)는 체리 피킹과 병합을 충분히 잘 처리하므로, 유지보수 브랜치를 더 일찍 만들 수 있을지도 모릅니다. bzr/hg 미러에서 릴리스를 시도해 보는 것은 유용한 연습이 될 것입니다.

hg

분명히 PEP 101과 릴리스 스크립트에 있는 Subversion 관련 세부 사항은 갱신해야 할 것입니다. 특히 릴리스에 태그를 붙이는 과정과 유지보수 브랜치를 만드는 과정은 Mercurial의 기능을 사용하도록 수정해야 합니다. 이렇게 하면 릴리스 과정의 몇몇 부분이 더 단순하고 효율적으로 바뀝니다. 예를 들어 Mercurial에서 태그는 특정 리비전에 붙인 기호 이름일 뿐이므로, 릴리스에 태그를 붙이거나 다시 붙이는 일은 간단한 작업이 됩니다.

git

바뀌기는 하겠지만 크게 바뀌지는 않을 것입니다. 유지보수 브랜치를 만들 때는 svn cp를 하는 대신 새 위치로 git push하기만 하면 됩니다. 태그는 완전히 다릅니다. svn에서 태그는 디렉터리 복사본이지만, git에서 태그는 브랜치와 마찬가지로 리비전에 붙인 기호 이름일 뿐입니다. (태그와 브랜치의 차이는 태그가 특정 커밋을 가리키고, git tag -f로 강제로 옮기지 않는 한 절대 바뀌지 않는다는 점입니다. 반면 체크아웃된 브랜치는 git commit(커밋)에 의해 자동으로 갱신됩니다.) release.py 스크립트는 대신 git 명령을 사용하도록 변경해야 합니다. git을 쓴다면 저는 릴리스 엔지니어가 정해지는 즉시 (로컬) 유지보수 브랜치를 만들겠습니다. 그런 다음 마음에 들지 않는 패치가 나올 때까지 “git pull”을 하고, 그런 패치가 나오면 “git pull; git revert ugly-patch”를 하겠습니다. 그러다가 갈라져 나오는 편이 합리적이라고 판단되면, 좋은 패치에 대해 “git cherry-pick”을 하기 시작하겠습니다.

플랫폼/도구 지원

운영 체제

DVCS Windows OS X UNIX
bzr 예 (설치 프로그램), tortoise 포함 예 (설치 프로그램, fink 또는 MacPorts) 예 (다양한 패키지 형식)
hg 예 (서드파티 설치 프로그램), tortoise 포함 예 (서드파티 설치 프로그램, fink 또는 MacPorts) 예 (다양한 패키지 형식)
git 예(서드파티 설치 프로그램) 예(서드파티 설치 프로그램, fink 또는 MacPorts) 예(.deb 또는 .rpm)

위 표에서 알 수 있듯이, 세 DVCS 모두 세 가지 주요 OS 플랫폼에서 사용할 수 있습니다. 하지만 이 표는 Bazaar가 바이너리 설치 프로그램으로 Windows를 직접 지원하는 유일한 DVCS인 반면, Mercurial과 git은 바이너리를 서드파티에 의존해야 한다는 점도 보여 줍니다. bzr과 hg에는 모두 tortoise 버전이 있지만 git에는 없습니다.

Bazaar와 Mercurial은 순수 Python으로 사용할 수 있으면서도 성능을 위한 선택적 확장 기능을 제공한다는 장점도 있습니다.

CRLF -> LF 지원

bzr
제가 알기로는 이 글을 쓰고 있는 지금도 이에 대한 지원 작업이 진행 중이며, 곧 나올 버전에 포함될 예정입니다. 자세한 내용을 찾아보도록 하겠습니다.
hg
win32text 확장 기능을 통해 지원됩니다.
git
직접 경험해 본 것은 아니지만, core.autocrlf 및 core.safecrlf 설정 속성을 통해 꽤 잘 지원되는 것으로 보입니다.

대소문자를 구분하지 않는 파일 시스템 지원

bzr
문제없을 것입니다. 저는 늘 Linux와 OS X 사이에서 브랜치를 공유합니다. 대소문자 변경(예: bzr mv Mailman mailman)을 해 본 적이 있는데, (당연히) Linux에서 작업하기만 하면 OS X에서 변경 사항을 가져왔을 때 모든 것이 아무 문제 없었습니다.
hg
Mercurial은 대소문자에 안전한 저장소 메커니즘을 사용하며, 대소문자 폴딩 충돌을 감지합니다.
git
OS X는 대소문자를 보존하므로 그곳에서도 대소문자를 변경할 수 있습니다. git은 어느 방향으로 이름을 변경하든 문제가 없습니다. 하지만 대소문자를 구분하지 않는 파일 시스템 지원이란 보통 대소문자를 구분하는 파일 시스템에서의 충돌에 대해 경고하는 것을 의미합니다. git은 그렇게 하지 않습니다.

도구

Review Board와 Rietveld 같은 코드 리뷰 도구의 경우, 전자는 세 가지 모두를 지원하는 반면 후자는 hg와 git은 지원하지만 bzr은 지원하지 않습니다. Bazaar에는 아직 온라인 리뷰 보드가 없지만, 이메일 기반 리뷰와 트렁크 병합을 관리하는 여러 방법이 있습니다. Bundle Buggy, Patch Queue Manager (PQM), 그리고 Launchpad’s code reviews가 있습니다.

세 가지 모두 저장소를 온라인에 올리려는 사람들을 위해 기본적인 호스팅 지원을 제공하는 웹 사이트가 있습니다. Bazaar에는 Launchpad가 있고, Mercurial에는 bitbucket.org가 있으며, git에는 GitHub가 있습니다. Google Code에도 이 서비스에서 git을 사용하는 방법, 즉 저장소를 보관하는 방법과 읽기 전용 미러로 사용하는 방법에 대한 안내가 있습니다.

세 가지 모두 Buildbot에서도 지원되는 것으로 보입니다.

Subversion 위에서의 사용

DVCS svn 지원
bzr bzr-svn (서드파티)
hg 여러 서드파티
git git-svn

세 DVCS 모두 svn을 지원하지만, 별도 설치 없이 기본으로 이를 지원하는 것은 git뿐입니다.

서버 지원

DVCS 웹 페이지 인터페이스
bzr loggerhead
hg hgweb
git gitweb

세 DVCS 모두 클라이언트 측과 서버 측에서 커밋 전/후 검증 등을 위한 다양한 훅을 지원합니다.

개발

세 프로젝트 모두 활발히 개발되고 있습니다. Git은 월간 릴리스 일정을 따르는 것으로 보입니다. Bazaar는 시간 기반의 월간 릴리스 일정을 따릅니다. Mercurial은 4개월 주기의 시간 기반 릴리스 일정을 따릅니다.

특별한 기능

bzr

Martin Pool은 다음과 같이 덧붙입니다: “bzr에는 공개 인터페이스와 비공개 인터페이스가 구분되고 변경되는 API에 대한 폐지 유예 기간이 있는 안정적인 Python 스크립팅 인터페이스가 있습니다. 일부 플러그인은 다음 주소에 나열되어 있습니다: https://edge.launchpad.net/bazaar 및 http://bazaar-vcs.org/Documentation”.

hg

Alexander Solovyov는 다음과 같이 말합니다:

Mercurial은 주요 이벤트에 대한 훅과 명령 확장 기능을 갖춘, 사용하기 쉽고 광범위한 API를 제공합니다. 또한 Mercurial과 함께 배포되는 mq(mercurial queues, Mercurial 큐) 확장이 있어 패치 작업을 단순화해 줍니다.

git

git에는 cvsserver 모드가 있습니다. 즉, CVS를 사용하여 git에서 트리를 체크아웃할 수 있습니다. 트리에 커밋까지 할 수 있지만 병합 같은 기능은 없으며, 브랜치는 CVS 모듈로 처리되므로 숙련된 CVS 사용자라면 충격을 받을 가능성이 큽니다.

테스트/소감

어떤 DVCS를 채택할지, 혹은 채택할지 여부에 대한 최종 결정을 내리는 일이 공동 저자들이 아니라 저(Brett Cannon)에게 맡겨졌으므로, 가능한 한 투명하게 하기 위해 여러 도구를 평가하면서 제가 수행한 테스트와 소감을 기록하는 것이 공정하다고 생각했습니다.

진입 장벽

Python 저장소를 체크아웃하는 데 드는 시간과 노력의 양은 매우 중요합니다. 난이도가 너무 높거나 시간이 너무 오래 걸리면 Python에 기여하려는 사람이 포기할 가능성이 큽니다. 그런 일이 일어나도록 둘 수는 없습니다.

저는 코어 개발자가 아닌 사람의 입장에서 2.x 트렁크를 체크아웃하는 과정을 측정했습니다. 시간은 zsh의 time 명령을 사용하여 측정했으며, 공간은 du -c -h로 계산했습니다.

DVCS 샌프란시스코 밴쿠버 공간
svn 1:04 2:59 139 M
bzr 10:45 16:04 276 M
hg 2:30 5:24 171 M
git 2:54 5:28 134 M

이 수치를 svn과 비교할 때는 이것이 1:1 비교가 아니라는 점을 알아 두는 것이 중요합니다. svn은 모든 DVCS와 달리 전체 리비전 이력을 내려받지 않습니다. 즉, svn은 네트워크로 내려받을 정보가 더 적다는 사실만으로도 DVCS보다 훨씬 빠르게 최초 체크아웃을 수행할 수 있습니다.

기본 정보 기능의 성능

이력 조회가 필요한 명령을 각 도구가 얼마나 잘 수행하는지 확인하기 위해 README 파일의 로그를 조회하는 시간을 측정했습니다.

DVCS 시간
bzr 4.5초
hg 1.1초
git 1.5초

이 테스트에서 주목할 점은 git의 경우 페이저를 사용하지 않고 로그를 얻는 방법을 알아내는 데 다른 세 도구보다 더 오랜 시간이 걸렸다는 것입니다. 페이저를 사용하는 것은 대체로 좋은 배려이지만, 페이저가 자동으로 켜지지 않게 하는 방법을 찾는 데 시간이 좀 걸렸습니다(알고 보니 주 git 명령에 페이저 사용을 끄는 --no-pager 플래그가 있습니다).

내장 도움말에서 사용할 명령 알아내기

저장소를 어떤 URL에서 복제했는지 확인하는 명령이 무엇인지 알아내려고 해 보았습니다. 이를 위해 도구 자체가 제공하는 도움말이나 man 페이지 외에는 아무것도 사용하지 않았습니다.

Bzr이 가장 쉬웠습니다: bzr info. bzr help를 실행하면 원하는 내용이 나오지 않았지만, bzr help commands가 언급되어 있었습니다. 그 목록에 납득할 만한 설명이 달린 해당 명령이 있었습니다.

두 번째로 쉬운 것은 Git이었습니다. git help 명령은 보여 주는 내용이 많지 않았고 모든 명령을 나열하는 방법도 없었습니다. 그래서 man 페이지를 보았습니다. 여러 명령을 읽어 보다가 git remote를 발견했습니다. 이 명령 자체는 origin 외에는 아무것도 출력하지 않았습니다. git remote origin을 시도하자 오류라고 하면서 명령 사용법을 출력했습니다. 그때 git remote show를 알게 되었습니다. git remote show origin을 실행하자 원하던 정보가 나왔습니다.

hg의 경우에는 원하는 정보를 끝내 혼자 힘으로 찾지 못했습니다. 알고 보니 필요한 것은 hg paths였지만, hg help가 출력하는 “show definition of symbolic path names”라는 설명만으로는 이를 알기 어려웠습니다(이 PEP에서 이 문제를 보고한 덕분에 Mercurial 개발자들이 hg paths 명령의 용도가 더 분명해지도록 문구를 고쳤다는 점을 밝혀 둡니다).

체크아웃 업데이트

오래된 저장소를 업데이트하는 데 시간이 얼마나 걸리는지 알아보기 위해, 700 커밋 뒤처진 저장소와 50 커밋 뒤처진 저장소(각각 3주, 1주 지난 상태)의 업데이트 시간을 측정했습니다.

DVCS 700 커밋 50 커밋
bzr 39초 7초
hg 17초 3초
git 해당 없음 4초

Note

Git은 특정 리비전의 저장소를 체크아웃하는 기능을 제공하지 않는 것으로 보여 700 커밋 시나리오의 값이 없습니다.

Git은 git pull의 출력 때문에 특별히 언급할 만합니다. 각 파일의 변경 내역 정보를 나열할 뿐만 아니라 그 정보를 색상으로 구분해 보여 줍니다.

결정

PyCon 2009에서 Mercurial을 채택하기로 결정했습니다.

Subversion 대신 Mercurial을 선택한 이유

svn은 그동안 개발 팀에 큰 도움이 되어 왔지만, 커미터가 아닌 사람들의 필요는 DVCS만큼 충족하지 못한다는 점을 인정해야 합니다. svn은 버전 관리나 브랜치 같은 기능을 저장소 커밋 권한이 있는 사람에게만 제공하므로 커밋 권한이 없는 사람에게는 걸림돌이 될 수 있습니다. 하지만 DVCS에는 그런 제약이 없습니다. 누구나 svn 저장소 전체를 복제하는 부담 없이 Python의 로컬 브랜치를 만들고 로컬에서 직접 커밋할 수 있기 때문입니다. 누구나 코어 개발자와 같은 워크플로를 가질 수 있게 하는 것이 svn에서 hg로 전환한 핵심 이유였습니다.

누구나 자신의 브랜치에 로컬로 쉽게 커밋할 수 있다는 이점과는 별개로, 오프라인에서도 빠르게 작업할 수 있다는 이점이 있습니다. hg는 모든 데이터를 로컬에 저장하므로 원격 서버에 요청을 보낼 필요 없이 로컬 디스크에서 작업합니다. 이 덕분에 응답 시간이 크게 줄어듭니다. 또한 인터넷에 연결되어 있지 않을 때도 오프라인으로 사용할 수 있습니다. 하지만 이 이점은 사소한 것이며, Subversion을 떠나게 한 주된 요인이라기보다 부수적인 이점으로 여겨집니다.

다른 DVCS 대신 Mercurial을 선택한 이유

Git은 세 가지 핵심 이유로 선택되지 않았습니다(Brett Cannon이 바로 이 이유들을 나열한 PyCon 2009 라이트닝 토크를 참고하십시오. 발표는 3:45에 시작합니다). 첫째, 검토 중인 세 DVCS 가운데 git의 Windows 지원이 가장 약합니다. Python은 자신이 실행되는 모든 플랫폼에서 개발을 지원해야 하므로 이는 받아들일 수 없습니다. Python은 Windows에서 실행되고 실제로 그 플랫폼에서 개발하는 사람들도 있으므로 탄탄한 지원이 필요합니다. 또한 git의 지원이 개선되고 있기는 하지만, 현재로서는 문제로 간주해야 할 만큼 큰 격차로 가장 약합니다.

둘째로, 첫 번째 문제만큼이나 중요한 것은 Python 코어 개발자들이 세 가지 DVCS 선택지 가운데 git을 큰 차이로 가장 덜 좋아했다는 점입니다. 다음 표를 보면 코어 개발자들을 대상으로 한 설문 조사 결과와 함께, git이 큰 차이로 가장 선호도가 낮은 버전 관리 시스템이라는 것을 알 수 있습니다.

DVCS ++ 동등 – 잘 모름
git 5 1 8 13
bzr 10 3 2 12
hg 15 1 1 10

마지막으로, 다른 모든 조건이 같다면(앞의 두 문제에서 보았듯이 실제로는 같지 않지만), C와 셸로 작성된 도구보다는 Python으로 작성된 도구를 사용하고 지원하는 편이 바람직합니다. 저희는 단지 Python으로 작성되었다는 이유만으로 도구를 선택하지 않을 만큼 실용적이지만, 이번 경우처럼 그렇게 하는 것이 합리적일 때에는 Python을 사용하는 도구를 장려하는 것이 유용하다고 봅니다.

Bazaar 대신 Mercurial이 선택된 이유는 결국 인기 때문이었습니다. 코어 개발자 설문 조사에서 보듯이 hg가 bzr보다 선호되었습니다. 그러나 git을 검토 대상에서 제외한다는 발표가 있은 후 PyCon에서 드러났듯이, 커뮤니티 역시 hg를 선호하는 것으로 보입니다. 많은 사람들이 Brett에게 찾아와 여러 방식으로 hg가 선택되기를 원한다고 말했습니다. bzr이 선택되지 않기를 원한다고 말한 사람은 아무도 없었지만, 선택되기를 원한다고 말한 사람도 없었습니다.

이 모든 정보를 바탕으로 Guido와 Brett은 Mercurial을 Python의 차기 버전 관리 시스템으로 결정했습니다.

전환 계획

PEP 385는 svn에서 hg로의 전환 과정을 개괄합니다.