distribute git

1. contribute

1.1 project contribute

git의 컨트리뷰트 방식은 매우 다양하다. git이 무척 유연하게 설계되었기 때문에 여러 방식으로 사용할 수 있다. 또한 프로젝트 환경에 따라 컨트리뷰트 방식을 다르게 가져갈 수 있기 때문에 이를 간단하게 설명하기에 어려움이 따를 수 있다. 컨트리뷰트 방식에 영향을 미치는 변수를 확인하자면 활발히 컨트리뷰트 하는 개발자의 수, 선택한 워크플로가 무엇인지, 개발자에게 접근 권한을 어떻게 부여하는지, 외부에서 기여할 수 있는지 등이 변수이다.

  • 활발히 활동하는 개발자의 수: 얼마나 자주 코드를 생성하는지에 대한 것이 활발한 개발자의 기준이다. 개발자가 많으면 많을수록 코드를 깔끔하게 적용하거나 merge 하기 어려워진다. 어떻해야 코드를 최신으로 유지하면서 원하는 수정을 할 수 있는지 고민하여야 한다.
  • 프로젝트에서 선택한 워크플로: 개발자 모두가 메인 저장소에 쓰기 권한을 갖는 중앙집중형, 프로젝트의 모든 patch 를 검사하고 통합하는 관리자가 따로 존재하는 형, 모든 수정사항을 개발자끼리 검토하고 승인하는 형 등
  • 접근 권한: 쓰기 권한 및 읽기 권한에 따라 컨트리뷰트 방식이 달라짐 (쓰기 권한이 없다면 수정사항을 어떻게 반영할 수 있는지, 수정사항을 적용하는 정책이 프로젝트에 존재하는지 등)

1.2 commit guide-line

git 프로젝트의 Documentation/SubmittingPatches 문서를 참고하는 것이 좋다.

최우선적으로 공백 문자를 정리하고 commit 하는 것이 좋다. git 은 공백문자를 검사해볼 수 있는 간단한 명령을 제공한다. commit 하기전에 git diff --check 명령으로 공백문자에 대한 오류를 확인할 수 있다.

lib/simplegit.rb:5: trailing whitespace.
+  @git_dir = File.expand_path(git_dir)##red maker##
lib/simplegit.rb:7: trailing whitespace.
+##red maker##
lib/simplegit.rb:20: trailing whitespace.
+end##red maker##
(END)

commit 을 하기 전에 공백문자에 대해 검사를 하면 공백으로 불필요하게 commit 되는 것을 막고 불필요하게 다른 개발자들이 신경 쓰는 일을 방지할 수 있다.

commit 은 논리적으로 구분되는 changeset 이다. 최대한 수정사항을 한 주제로 요약할 수 있어야 하고 여러 이슈에 대한 수정사항을 하나의 commit 에 담지 않아야 한다. 여러 이슈를 한꺼번에 수정했다고 하더라도 Staging Area 를 이용하여 한 commit 에 이슈 하나만 담기도록 한다. 작업 내용을 분할하고, 각 commit 마다 적절한 메시지를 작성한다. 같은 파일의 다른 부분을 수정하는 경우에는 git add -patch 명령을 써서 한 부분씩 나누어 Staging Area에 저장해야 한다. 결과적으로 최종 프로젝트의 모습은 한 번에 commit 을 하든 다섯 번에 나누어 commit 을 하든 똑같다.

여러 번 나누어 commit 하는 것이 다른 동료가 수정한 부분을 확인할 때나 각 commit 의 시점으로 복원해서 검토할 때 이해하기 훨씬 쉽다.

마지막으로 commit 메시지를 잘 작성하여야 한다. 메시지의 첫 라인에 50자가 넘지 않는 아주 간략한 메시지를 적어 해당 commit 을 요약한다. 다음 한 라인은 비우고 그다음 라인부터 커밋을 자세히 설명한다. git 개발 프로젝트에서는 개발 동기와 구현 상황의 제약 조건이나 상황 등을 자세하게 요구한다. 이런 점은 따를 만한 좋은 가이드라인이다. 그리고 현재형 표현을 사용하는 것이 좋다. 명령문으로 시작하는 것도 좋은 방법이다. 예를 들어 “I added tests for (테스트를 추가함)” 보다는 “Add tests for (테스트 추가)” 와 같은 메시지를 작성한다.

영문 50글자 이하의 간략한 수정 요약

자세한 설명. 영문 72글자 이상이 되면
라인 바꿈을 하고 이어지는 내용을 작성한다.
특정 상황에서는 첫 번째 라인이 이메일
메시지의 제목이 되고 나머지는 메일
내용이 된다. 빈 라인은 본문과 요약을
구별해주기에 중요하다(본문 전체를 생략하지 않는 한).

이어지는 내용도 한 라인 띄우고 쓴다.

  - 목록 표시도 사용할 수 있다.

  - 보통 '-' 나 '*' 표시를 사용해서 목록을 표현하고
    표시 앞에 공백 하나, 각 목록 사이에는 빈 라인
    하나를 넣는데, 이건 상황에 따라 다르다.

1.3 비공개 소규모 팀

두세명으로 이루어진 비공개 프로젝트가 가장 간단한 프로젝트 형태이다. “비공개” 라고 함은 소스 코드가 공개되지 않는 것을 말하는 것이지 외부에서 접근할 수 없는 것을 의미하는 것이 아니다.

이런 환경은 보통 Subversion 같은 중앙집중형 버전 관리 시스템에서 사용하던 방식을 사용한다. 물론 git 이 가진 오프라인 commit 기능이나 브랜치 merge 기능을 이용하긴 하지만 크게 다르지 않다. 가장 큰 차이점은 서버가 아닌 클라이언트 쪽에서 merge 한다는 점이다. 예를들어 두 개발자가 저장소를 공유하는 시나리오를 살펴보면 다음과 같다. 개발자인 John 은 저장소를 clone 하고 파일을 수정하고 나서 로컬에 commit 한다.

# John's Machine
$ git clone john@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim lib/simplegit.rb
$ git commit -am 'remove invalid default value'
[master 738ee87] remove invalid default value
 1 files changed, 1 insertions(+), 1 deletions(-)

개발자인 Jessica 도 저장소를 clone 하고 파일을 하나 추가하고 commit 한다.

# Jessica's Machine
$git clone jessica@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim TODO
$ git commit -am 'add reset task'
[master fbff5bc] add reset task
 1 files changed, 1 insertions(+), 0 deletions(-)

Jessica 는 서버에 commitpush 한다.

# Jessica's Machine
$ git push origin master
...
To jessica@githost:simplegit.git
   1edee6b..fbff5bc  master -> master

push 명령을 실행하고 난 결과 중 가장 마지막 줄은 유용한 정보를 보여주고 있다. 마지막 줄의 기본적인 형태는 <oldref>..<newref> fromref -> toref 이다. oldref 는 이전 레퍼런스를, newref는 새 레퍼런스를, fromrefpush 명령에서 사용한 로컬 레퍼런스의 이름을, torefpush 로 업데이트한 리모트 레퍼런스를 나타낸다. 이어지는 내용에서 지금과 비슷한 push 명령 출력 결과는 여러번 등장한다. 이 출력 메시지의 내용을 이해하고 있으면 다양한 상태에서 정확하게 어떤일이 벌어지는가를 좀 더 쉽게 이해할 수 있다. 자세한 내용을 좀 더 살펴보려면 Git 문서 git-push 를 참고한다.

이제 John 도 내용을 변경하고 commit 을 만든 후 서버로 commitpush 하려고 한다.

# 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 의 commit 은 서버에서 거절된다. Subversion에서는 서로 다른 파일을 수정하는 이런 merge 작업은 자동으로 서버가 처리한다. 하지만 git 은 로컬에서 먼저 merge 해야 한다. 다시 말해 John 은 push 하기 전에 Jessica 가 수정한 commitfetch 하고 merge 해야 한다는 말이다.

이를 위해 우선 John 은 Jessica 의 작업 내용을 아래와 같이 fetch 한다. (아래 명령은 Jessica 의 작업 내용을 내려받긴 하지만 merge 까지 하지는 않는 작업이다)

$ git fetch origin
...
From john@githost:simplegit
 + 049d078...fbff5bc master     -> origin/master

fetch 하고 나면 John 의 로컬 저장소는 다음과 같이 된다.

                 +------+
                 |master|
                 +---|--+
+-----+  +-----+  +--V--+
|4b078<--|1edee<--|738ee|
+-----+  +--^--+  +-----+
            |     +-----+
            +-----|fbff5|
                  +--^--+
              +------|------+
              |origin/master|
              +-------------+

이제 John은 Fetch하여 가져 온 Jessica의 작업 내용을 merge 할 수 있다.

$ git merge origin/master
Merge made by the 'recursive' strategy.
 TODO |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

merge 가 잘 이루어지면 John 의 저장소는 아래와 같이 된다.

                          +------+
                          |master|
                          +---|--+
+-----+  +-----+  +-----+  +--V--+
|4b078<--|1edee<--|738ee<--|72bbc|
+-----+  +--^--+  +-----+  +-|-^-+
            |              +-+ |
            |     +-----+  |  +-------------+
            +-----|fbff5<--+  |origin/master|
                  +-----+     +-------------+

동시에 Jessica 는 토픽 브랜치를 하나 만든다. issue54 브랜치를 만들고 세 번에 걸쳐서 commit 한다. 아직 John의 commitfetch 하지 않은 상황이기 때문에 아래와 같은 상황이 된다.

                 +------+                  +-------+
                 |master|                  |issue54|
                 +---|--+                  +----|--+
+-----+  +-----+  +--V--+  +-----+  +-----+  +--V--+
|4b078<--|1edee<--|fbff5<--|8149a<--|23ac6<--|4af42|
+-----+  +-----+  +--^--+  +-----+  +-----+  +-----+
              +------|------+
              |origin/master|
              +-------------+

Jessica 는 John 이 새로 push 했다는 것을 알게 되어 하던 작업을 멈추고 John 의 작업 내용을 살펴보려고 한다. 하지만 아직 Jessica 는 John 의 변경사항을 가지고 있지 않은 상태이다.

# Jessica's Machine
$ git fetch origin
...
From jessica@githost:simplegit
   fbff5bc..72bbc59  master     -> origin/master

위 명령으로 John 이 pushcommit 을 모두 내려받는다. 그러면 Jessica 의 저장소는 아래와 같은 상태가 된다.

                 +------+                  +-------+
                 |master|                  |issue54|
                 +---|--+                  +----|--+
+-----+  +-----+  +--V--+  +-----+  +-----+  +--V--+
|4b078<--|1edee<--|fbff5<--|8149a<--|23ac6<--|4af42|
+-----+  +--^--+  +--^--+  +-----+  +-----+  +-----+
            |        |
            |        +--------+
            |     +-----+  +--|--+
            +-----|738ee<--|72bbc|
                  +-----+  +--^--+
                       +------|------+
                       |origin/master|
                       +-------------+

이제 orgin/mastermerge 한다. Jessica 는 토픽 브랜치에서의 작업을 마치고 어떤 내용이 merge 되는지 git log 명령으로 확인한다.

$ git log --no-merges issue54..origin/master
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 16:01:27 2009 -0700

   remove invalid default value

issue54..origin/master 문법은 히스토리를 검색할 때 뒤의 브랜치(origin/master)에 속한 commit 중 앞의 브랜치(issue54)에 속하지 않은 commit 을 검색하는 것이다.

앞의 명령에 따라 히스토리를 검색한 결과 John 이 생성하고 Jessica 가 merge 하지 않은 commit 을 하나 찾았다. origin/master 브랜치를 merge 하게 되면 검색된 commit 하나가 로컬 작업에 merge 될 것이다.

merge 할 내용을 확인한 Jessica 는 자신이 작업한 내용과 John 이 push 한 작업(origin/master)을 master 브랜치에 merge 하고 push 한다.

issue54 토픽 브랜치에 쌓은 모든 내용을 합치려면, 우선 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 모두 Upstream 브랜치이기 때문에 둘 중에 무엇을 먼저 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 이기 때문에 명령 실행 결과는 별 문제가 없다. origin/master 에 쌓여있던 John 의 작업 내용을 다음과 같이 실행하여 Jessica는 merge 작업을 완료할 수 있다.

$ git merge origin/master
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

위와 같이 merge 가 잘 된다면 아래의 상태가 된다.

                                            +-------+ +------+
                                            |issue54| |master|
                                            +---|---+ +----|-+
+-----+  +-----+  +-----+  +-----+  +-----+  +--V--+       |
|4b078<--|1edee<--|fbff5<--|8149a<--|23ac6<--|4af42<---+   |
+-----+  +---^-+  +---^-+  +-----+  +-----+  +-----+   |   |
             |        |                                |   |
             +-----+  +-----+                          |   |
                  +|----+  +|----+                    +|---V+
                  |738ee<--|72bbc|<-------------------|8059c|
                  +-----+  +--^--+                    +-----+
                       +------|------+
                       |origin/master|
                       +-------------+

origin/master 브랜치가 Jessica 의 master 브랜치로 나아갈 (reachable) 수 있기 때문에 push 는 성공한다. (물론 John 이 그 사이에 push 하지 않았다면)

$ git push origin master
...
To jessica@githost:simplegit.git
   72bbc59..8059c15  master -> master

두 개발자의 commit 을 성공적으로 merge 하고 나면 결과는 아래와 같다.

                                            +-------+ +------+
                                            |issue54| |master|
                                            +---|---+ +----|-+
+-----+  +-----+  +-----+  +-----+  +-----+  +--V--+       |
|4b078<--|1edee<--|fbff5<--|8149a<--|23ac6<--|4af42<---+   |
+-----+  +---^-+  +---^-+  +-----+  +-----+  +-----+   |   |
             |        |                                |   |
             +-----+  +-----+                          |   |
                  +|----+  +|----+                    +|---V+
                  |738ee<--|72bbc|<-------------------|8059c|
                  +-----+  +-----+                    +--^--+
                                                  +------|------+
                                                  |origin/master|
                                                  +-------------+

토픽 브랜치에서 수정하고 로컬의 master 브랜치에 merge 한다. 작업한 내용을 프로젝트의 공유 저장소에 push 하고자 할 때는 우선 origin/master 브랜치를 fetch 하고 merge 한다. 그리고 나서 merge 한 결과를 다시 서버로 push 한다. 이런 워크플로가 일반적이며 아래와 같이 나타낼 수 있다.

        +-------+     +------+     +----+
        |Jessica|     |Server|     |John|
        +-------+     +------+     +----+
            |            |            |
            |            | git clone  |
            |            |----------->|
            |            |            |----+
            |            |            |    |
            |            |            |  git commit
            |            |            |    |
            | git clone  |            |<---+
            |<-----------|            |
       +----|            |            |
       |    |            |            |
git commit  |            |            |
       |    |            |            |
       +--->|            |            |
            | git push   |            |
            |----------->| git fetch  |
            |            |----------->|
            |            |            |----+
            |            |            |    |
            |            |            |  git merge
            |            |            |    |
            |            |            |<---+
            |            | git push   |
            | git fetch  |<-----------|
            |<-----------|            |
       +----|            |            |
       |    |            |            |
git merge   |            |            |
       |    |            |            |
       +--->| git push   |            |
            |----------->| git fetch  |
            |            |----------->|
            |            |            |
        +-------+     +------+     +----+
        |Jessica|     |Server|     |John|
        +-------+     +------+     +----+

1.4 비공개 대규모 팀

비공개 대규모 팀에서는 각각의 작은 팀이 서로 어떻게 merge 하는지가 핵심이다.

John과 Jessica는 “featureA” 기능을 함께 작업하게 됐다. Jessica는 Josie와 함께 “featureB” 기능도 작업하고 있다. 이런 상황이라면 회사는 Integration-manager 워크플로를 선택하는 게 좋다. 작은 팀이 수행한 결과물은 Integration-Manager가 merge 하고 공유 저장소의 master 브랜치를 업데이트한다. 팀마다 브랜치를 하나씩 만들고 Integration-Manager는 그 브랜치를 pull 해서 merge 한다.

두 팀에 모두 속한 Jessica 의 작업 순서를 살펴보자. 우선 Jessica 는 저장소를 clone 하고 featureA작업을 먼저 한다. featureA 브랜치를 만들고 수정하고 commit 한다.

# 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 -u origin featureA
...
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 브랜치에 commit 한다.

$ 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 의 저장소는 그림 아래와 같다.

        +------+  +--------+
        |master|  |featureA|
        +---|--+  +--|-----+
+-----+  +--V--+  +--V--+
|4b078<--|1edee<--|33009|
+-----+  +--^--+  +-----+
         +--|--+  +-----+
         |e5b0f<--|85127|
         +-----+  +--^--+
                  +--|-----+
                  |featureB|
                  +--------+

작업을 마치고 push 하려고 하는데 Jesie 가 이미 “featureB” 작업을 하고 서버에 featureBee 브랜치로 push 했다는 이메일을 보내왔다. Jessica는 Jesie 의 작업을 먼저 merge 해야만 push 할 수 있다. merge 하기 위해서 우선 git fetchfetch 한다.

$ git fetch origin
...
From jessica@githost:simplegit
 * [new branch]      featureBee -> origin/featureBee

Jessica 가 앞서 checkoutfeatureB 브랜치에서 작업중일 때, fetch 해 온 브랜치를 git merge 명령으로 merge 한다.

$ git merge origin/featureBee
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
 lib/simplegit.rb |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)

이 시점에서 Jessica 는 merge 한 “featureB” 작업을 서버로 push 할 때 서버의 featureB 브랜치로 push 하지 않고자 한다. 이미 Josie 가 생성한 featureBee로 작업 내용을 push 하러면 아래와 같이 실행한다.

$ git push -u origin featureB:featureBee
...
To jessica@githost:simplegit.git
   fba9af8..cd685d1  featureB -> featureBee

명령에서 사용한 -u옵션은 --set-upstream 옵션의 짧은 표현인데 브랜치를 추적하도록 설정해서 이후 Push 나 Pull 할 때 좀 더 편하게 사용할 수 있다.

John 이 몇 가지 작업을 하고 나서 featureApush 했고 확인해 달라는 내용의 이메일을 보내왔다. Jessica 는 John 의 작업 내용을 확인하기 위해 다시 한 번 git fetchpush 된 작업을 fetch 한다.

$ git fetch origin
...
From jessica@githost:simplegit
   3300904..aad881d  featureA   -> origin/featureA

Jessica 의 로컬 featureA 브랜치와 fetch 해 온 John 의 작업내용이 같은 featureA 브랜치 상에서 어떤 것이 업데이트됐는지 git log 명령으로 확인한다.

$ git log featureA..origin/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

확인을 마치면 로컬의 featureA 브랜치로 John 의 작업 내용을 다음과 같이 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 는 일부 수정하고, featureA 브랜치에 commit 하고, 수정한 내용을 다시 서버로 push 한다.

$ git commit -am 'small tweak'
[featureA 774b3ed] small tweak
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push
...
To jessica@githost:simplegit.git
   3300904..774b3ed  featureA -> featureA

위와 같은 작업을 마치고 나면 Jessica 의 저장소는 아래와 같은 모습이 된다.

                          +---------------+
                          |origin/featureA|
                          +------------|--+
        +------+                 +-----V--+
        |master|                 |featureA|
        +---|--+                 +-----|--+
+-----+  +--V--+  +-----+  +-----+  +--V--+
|4b078<--|1edee<--|33009<--|aad88<--|774b3|
+-----+  +-^-^-+  +-----+  +-----+  +-----+
           | |    +-----+  +-----+  +-----+
           | +----|e5b0f<--|85127<--|cd685|
           |      +-----+  +-----+  +-|-^-+
           |          +-----+         | |
           +----------|fba9a<---------+ |
                      +-----+    +------|-+
                                 |featureB|
                                 +------|-+
                        +---------------|-+
                        |origin/featureBee|
                        +-----------------+      

그럼 featureAfeatureBee 브랜치가 프로젝트의 메인 브랜치로 merge 할 준비가 되었다고 Integration-Manager에게 알려준다. Integration-Manager가 두 브랜치를 모두 merge 하고 난 후에 메인 브랜치를 fetch 하면 아래와 같은 모양이 된다.

                          +---------------+
                          |origin/featureA|
                          +------------|--+
        +------+                 +-----V--+  +-------------+
        |master|                 |featureA|  |origin/master|
        +---|--+                 +-----|--+  +--|----------+
+-----+  +--V--+  +-----+  +-----+  +--V--+  +--V--+
|4b078<--|1edee<--|33009<--|aad88<--|774b3<--|5399e|
+-----+  +-^-^-+  +-----+  +-----+  +-----+  +--|--+
           | |    +-----+  +-----+  +-----+     |
           | +----|e5b0f<--|85127<--|cd685<-----+
           |      +-----+  +-----+  +-|-^-+
           |          +-----+         | |
           +----------|fba9a<---------+ |
                      +-----+    +------|-+
                                 |featureB|
                                 +------|-+
                        +---------------|-+
                        |origin/featureBee|
                        +-----------------+      

수많은 팀의 작업을 동시에 진행하고 나중에 merge 하는 기능을 사용하려고 다른 버전 관리 시스템에서 git 으로 바꾸는 조직들이 많아지고 있다. 팀은 자신의 브랜치로 작업하지만, 메인 브랜치에 영향을 끼치지 않는다는 점이 git 의 장점이다. 아래는 이런 워크플로를 나타내고 있다.

        +-------+     +-----+     +----+     +--------+     +----------+
        |Jessica|     |Josie|     |John|     |server: |     |server:   |
        |       |     |     |     |    |     |featureA|     |featureBee|
        +---|---+     +--|--+     +--|-+     +---|----+     +-----|----+
            |            |           |           |                |
        +---|            |           |           |                |
   (A)  |   |            |           |           |                |
git commit  |            |           |           |                |
        |   |            |           |           |                |
        +-->|        git push origin featureA    |                |
            |----------------------------------->|                |
            |        +---|           |           |                |
            |    (A) |   |           |           |                |
            | git commit |           |           |                |
            |        |   |           |           |                |
            |        +-->|   git fetch origin    |                |
            |            |<----------------------|                |
            |        +---|           |           |                |
            |    (A) |   |           |           |                |
            | git merge  |           |           |                |
            |        |   |           |           |                |
            |        +-->| git push origin featureA               |
            |            |---------------------->|                |
            |            |       +---|           |                |
            |            |   (B) |   |           |                |
            |            | git commit|           |                |
            |            |       |   |           |                |
            |            |       +-->| git push origin featureBee |
            |            |           |--------------------------->|
        +---|            |           |           |                |
   (B)  |   |            |           |           |                |
git commit  |            |           |           |                |
        |   |            |           |           |                |
        +-->|                  git fetch origin                   |
            |<----------------------------------------------------|
        +---|            |           |           |                |
   (B)  |   |            |           |           |                |
git merge   |            |           |           |                |
        |   |            |           |           |                |
        +-->|         git push origin featureB:featureBee         |
            |---------------------------------------------------->|
            |         git fetch origin           |                |
            |<-----------------------------------|                |
        +---|            |           |           |                |
   (A)  |   |            |           |           |                |
git marge   |            |           |           |                |
        |   |            |           |           |                |
        +-->|            |           |           |                |
        +---|            |           |           |                |
   (A)  |   |            |           |           |                |
git commit  |            |           |           |                |
        |   |            |           |           |                |
        +-->|      git push origin featureA      |                |
            |----------------------------------->|                |
            |            |           |           |                |
        +---|---+     +--|--+     +--|-+     +---|----+     +-----|----+
        |Jessica|     |Josie|     |John|     |server: |     |server:   |
        |       |     |     |     |    |     |featureA|     |featureBee|
        +-------+     +-----+     +----+     +--------+     +----------+

1.5 공개 프로젝트 Fork

공개 팀을 운영할 때는 모든 개발자가 프로젝트의 공유 저장소에 직접적으로 쓰기 권한을 가지지는 않는다. git 호스팅에서 fork 를 통해 프로젝트에 contribute 한다.

우선 처음 할 일은 메인 저장소를 clone 하는 것이다. 그리고 나서 토픽 브랜치를 만들고 일정 부분 기여한다. 그 순서는 아래와 같다.

$ git clone <url>
$ cd project
$ git checkout -b featureA
  ... work ...
$ git commit
  ... work ...
$ git commit

일단 프로젝트의 웹사이트로 가서 Fork 버튼을 누르면 원래 프로젝트 저장소에서 갈라져 나온, 쓰기 권한이 있는 저장소가 하나 만들어진다. 그러면 로컬에서 수정한 commit 을 원래 저장소에 push 할 수 있다. 그 저장소를 로컬 저장소의 리모트 저장소로 등록한다. 예를 들어 myfork로 등록한다.

$ git remote add myfork <url>

등록한 리모트 저장소에 push 한다. 작업하던 것을 로컬 저장소의 master 브랜치에 merge 한 후 push 하는 것보다 리모트 브랜치에 바로 push 하는 방식이 훨씬 간단하다. 이렇게 하는 이유는 관리자가 토픽 브랜치를 프로젝트에 포함시키고 싶지 않을 때 토픽 브랜치를 merge 하기 이전 상태로 master 브랜치를 되돌릴 필요가 없기 때문이다. 관리자가 토픽 브랜치를 merge 하든 rebase 하든 Cherry-Pick 하든지 간에 결국 다시 관리자의 저장소를 pull 할 때는 토픽 브랜치의 내용이 들어 있을 것이다.

어떤 경우라도 다음과 같이 작업 내용을 push 할 수 있다.

$ git push -u myfork featureA

fork 한 저장소에 push 하고 나면 프로젝트 관리자에게 이 내용을 알려야 한다. 이것을 Pull Request 라고 한다. git 호스팅 사이트에서 관리자에게 보낼 메시지를 생성하거나 git request-pull명령으로 이메일을 수동으로 만들 수 있다.

git request-pull 명령은 아규먼트를 두 개 입력받는다. 첫 번째 아규먼트는 작업한 토픽 브랜치의 Base 브랜치이다. 두 번째는 토픽 브랜치가 위치한 저장소 URL인데 위에서 등록한 리모트 저장소 이름을 적을 수 있다. 이 명령은 토픽 브랜치 수정사항을 요약한 내용을 결과로 보여준다. 예를 들어 Jessica 가 John 에게 pull 요청을 보내는 상황을 살펴보자. Jessica 는 토픽 브랜치에 두 번 commit 을 하고 fork 한 저장소에 push 했다. 그리고 아래와 같이 실행한다.

$ git request-pull origin/master myfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
Jessica Smith (1):
        added a new function

are available in the git repository at:

  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(-)

관리자에게 이 내용을 보낸다. 이 내용에는 토픽 브랜치가 어느 시점에 갈라져 나온 것인지, 어떤 commit 이 있는지, pull 하려면 어떤 저장소에 접근해야 하는지에 대한 내용이 들어 있다.

프로젝트 관리자가 아니라고 해도 보통 origin/master 를 추적하는 master 브랜치는 가지고 있다. 그래도 토픽 브랜치를 만들고 일을 하면 관리자가 수정 내용을 거부할 때 쉽게 버릴 수 있다. 토픽 브랜치를 만들어서 주제별로 독립적으로 일을 하는 동안에도 주 저장소의 master 브랜치는 계속 수정된다. 하지만 주 저장소의 브랜치의 최근 commit 이후로 rebase 하면 깨끗하게 merge 할 수 있다. 그리고 다른 주제의 일을 하려고 할 때는 앞서 push 한 토픽 브랜치에서 시작하지 말고 주 저장소의 master 브랜치로부터 만들어야 한다.

$ git checkout -b featureB origin/master
  ... work ...
$ git commit
$ git push myfork featureB
$ git request-pull origin/master myfork
  ... email generated request pull to maintainer ...
$ git fetch origin

각 토픽은 일종의 실험실이라고 할 수 있다. 각 토픽은 서로 방해하지 않고 독립적으로 수정하고 rebase 할 수 있다.

        +------+ +-------------+
        |master| |origin/master|
        +---|--+ +---|---------+
            V        V
+-----+  +-----+  +-----+
|4b078<--|1edee<--|33009|
+-----+  +-^-^-+  +-----+
           | |    +-----+  +-----+  +--------+
           | +----|a2de3<--|0d708<--|featureA|
           |      +-----+  +-----+  +--------+
           |      +-----+  +--------+
           +------|e5b0f<--|featureB|
                  +-----+  +--------+

프로젝트 관리자가 사람들의 수정 사항을 merge 하고 나서 Jessica 의 브랜치를 merge 하려고 할 때 충돌이 날 수도 있다. 그러면 Jessica 가 자신의 브랜치를 origin/masterrebase 해서 충돌을 해결하고 다시 pull request 를 보낸다.

$ git checkout featureA
$ git rebase origin/master
$ git push -f myfork featureA

위 명령들을 실행하고 나면 히스토리는 아래와 같아진다.

        +------+ +-------------+
        |master| |origin/master|
        +---|--+ +---|---------+
            V        V
+-----+  +-----+  +-----+
|4b078<--|1edee<--|33009<-----+
+-----+  +--^--+  +-----+  +--|--+  +-----+  +--------+
            |              |dee6b<--|5399e<--|featureA|
            |              +-----+  +-----+  +--------+
            |     +-----+  +--------+
            +-----|e5b0f<--|featureB|
                  +-----+  +--------+

브랜치를 rebase 해 버렸기 때문에 push 할 때 -f 옵션을 주고 강제로 기존 서버에 있던 featureA브랜치의 내용을 덮어 써야 한다. 아니면 새로운 브랜치를(예를 들어 featureAv2) 서버에 push 해도 된다.

또 다른 시나리오를 하나 더 살펴보자. 프로젝트 관리자는 featureB 브랜치의 내용은 좋지만, 상세 구현은 다르게 하고 싶다. 관리자는 featureB 담당자에게 상세 구현을 다르게 해달라고 요청한다.featureB 담당자는 하는 김에 featureB 브랜치를 프로젝트의 최신 master 브랜치 기반으로 옮긴다. 먼저 origin/master 브랜치에서 featureBv2 브랜치를 새로 하나 만들고, featureBcommit 들을 모두 squash 해서 merge 하고, 만약 충돌이 나면 해결하고, 상세 구현을 수정하고, 새 브랜치를 push 한다.

$ git checkout -b featureBv2 origin/master
$ git merge --squash featureB
  ... change implementation ...
$ git commit
$ git push myfork featureBv2

--squash 옵션은 현재 브랜치에 merge 할 때 해당 브랜치의 commit 을 모두 commit 하나로 합쳐서 merge 한다. 이 때 merge commit 은 만들지 않는다. 다른 브랜치에서 수정한 사항을 전부 가져오는 것은 똑같다. 하지만 새로 만들어지는 commit 은 부모가 하나이고 commit 을 기록하기 전에 좀 더 수정할 기회도 있다. 다른 브랜치에서 수정한 사항을 전부 가져오면서 그전에 추가적으로 수정할 게 있으면 수정하고 merge 할 수 있다. 게다가 새로 만들어지는 commit 은 부모가 하나다. --no-commit 옵션을 추가하면 commit 을 합쳐 놓고 자동으로 commit 하지 않는다.

수정을 마치면 관리자에게 featureBv2 브랜치를 확인해 보라고 메시지를 보낸다.

        +------+ +-------------+
        |master| |origin/master|
        +---|--+ +--|----------+
            |       |      +-----+  +----------+
            V       V      |17f4d<--|featureBv2|
+-----+  +-----+  +-----+  +-|---+  +----------+
|4b078<--|1edee<--|33009<----+
+-----+  +--^--+  +--^--+
            |        |     +-----+  +-----+  +--------+
            |        +-----|dee6b<--|5399e<--|featureA|
            |              +-----+  +-----+  +--------+
            |     +-----+  +--------+
            +-----|e5b0f<--|featureB|
                  +-----+  +--------+