A Way of Code

興味の赴くままに書き綴っていきます。

実践Git&GitHub - homebrewをフォークするためのGit&GitHub入門 後編(1/2)

前回:Gitの仕組み - homebrewをフォークするためのGit&GitHub入門 中編

今回はFormulaを実際に修正しながら、GitとGitHubの使い方を学んでいきます。
ghostscriptのFormulaを修正して、ghostscriptのバージョンを9.04から9.05に上げる修正を行います。

なお、ここではhomebrewのオリジナルを"本家"と呼称します。
#その他の用語は、Gitの用語Gitの仕組みを参照してください。

はじめに - ブランチの方針

ブランチを利用する流れは以下のようにします。
右側のラインが本家のmasterブランチです。左側がフォークしたリポジトリのブランチです(簡略化のためにリモートとローカルを同一視しています)。
f:id:toggtc:20120309215855p:plain

homebrewのFormulaに関しては、以下の方針とします。

  • 作業用(トピック)ブランチ"ghostscript-spike"を作って、そこで修正作業を行う
  • Pull Request用のブランチ"ghostscript-updated"を作成し、作業用ブランチのコミットをひとまとめにしたものを取り込む
  • Pull Request用のブランチでPull Requestを行う

こうすることで、本家側では1つのコミットログを見るだけで済みます

なぜmasterブランチで編集をしないのか

例えばmasterブランチを使って、ghostscriptの修正を行い、Pull Reqestしたとします。その後、masterブランチのままImageMagickの修正をしてしまった場合、タイミングが悪いと本家の方で、ghostscriptとImageMagickの両方を取り込んでしまうことになります。こうなってしまうと、本家の方でImageMagickの修正を除外して、本家のmasterブランチに変更を取り込む、といった面倒な作業を強いることなってしまいます。
また、修正対象のFormula用にブランチを別途作成したとしても、そのブランチに大量のコミットログがある場合、本家の方でコミットログを1つにまとめる、という作業をさせてしまうことになります。
そこで、別途Pull Request用のブランチを作成してコミットログを1つにまとめる、というわけです。
なお、masterブランチは本家の更新を取得するためだけに使います。masterブランチが本家の内容と異なると、masterブランチから派生させる全てのブランチが本家と異なることになります。そうすると、またしてもPull Requestするときに余計な混乱を招いてしまいますから。

1. Fork - リポジトリを自分のGitHubにコピーする

(1) homebrewのページを開きます。
https://github.com/mxcl/homebrew

(2) 次に、勇気を振り絞って右上にある"Fork"をクリックします。
フォークすると、作者にフォークしたよ、と通知されます。

f:id:toggtc:20120309215916j:image


(3) ちょっとだけ待つと、自動的にページが切り替わります。
f:id:toggtc:20120309215930j:image


(4) 自分のgithubにhomebrewのリポジトリがコピーされました。
URLはgit@github.com:toggtc/homebrew.gitになりました。
f:id:toggtc:20120309215943j:image

これでフォークが完了し、あなたのhomebewリポジトリが作成されました!

2. リポジトリの準備

この作業は初回の1度のみ行います。

2.1 clone

GitHub上に作成したリポジトリをローカルにコピーします。
git cloneコマンドのパラメータに、homebrewをフォークしたときに作られたあなたのリポジトリのURLを指定します。

$ git clone git@github.com:ユーザ名/homebrew.git
Cloning into 'homebrew'...
...
Resolving deltas: 100% (36367/36367), done.

# なお、このとき前々回設定したSSHパスフレーズが求められることがあります。

以降の作業は、cloneしたときに作成されたhomebrewディレクトリにて行います。

$ cd ./homebrew

2.2 remote add

この時点では、まだリモートリポジトリとしてはorigin(=自分のGitHubにあるhomebrewリポジトリ)のみです。
本家側の更新を取り込めるようにするためには、予め本家のリモートリポジトリを追加しておく必要があります。

$ git remote add upstream https://github.com/mxcl/homebrew.git

upstreamの部分は任意の名前で構いませんが、Fork元のリモートリポジトリをupstreamと呼ぶのが通例です。
リモートリポジトリをさらに追加する場合は、taroやhanako等、リポジトリを所有している人の名前(アカウント名)にすると分かりやすいでしょう。

念のため設定されているのかどうか、確認してみましょう。

$ cat ./.git/config
[core]
	repositoryformatversion = 0
...
[remote "upstream"]
	url = https://github.com/mxcl/homebrew.git
	fetch = +refs/heads/*:refs/remotes/upstream/*

ちゃんとupstreamが設定されていますね。

3. Pull Requestのチェック

修正あるいは追加するFormulaを決めたら、まずは本家にあがっているPull Requetsを確認しておきましょう。
以下のページから、Pull Requestsを閲覧することができます。
https://github.com/mxcl/homebrew/pulls

普段から200前後のPull Requestsが溜まっています。中の人も大変そうです。
同じ内容のPull Requestを重複させてしまうと本家の方々にご迷惑なので、重複していないか確認しておきます。#私もいろいろとご迷惑を掛けた一人です。
また、既に同じような内容のFormulaが投稿されているけど、少しだけ自分の変更したい内容と異なる場合は、そのPull Requestを開いて、コメントを書きこんでみましょう。要望が通るかもしれません。

また、homebrewやFormulaに何か問題があったり、Formulaの作成を依頼する場合はissuesに投稿するといいかもしれません
https://github.com/mxcl/homebrew/issues

4. 作業用ブランチの作成

Pull Requestが重複していないことを確認したら、修正作業のための一時的なブランチを作成します。このブランチはmasterブランチを元に作成します。

念のため、元となるmasterブランチを最新化してから、ブランチを作成してきます。

4.1 checkout master

まず、現在使用しているブランチを確認しておきます。--allオプションを指定することで、リモートブランチも表示されます

$ git branch --all
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/gh-pages
  remotes/origin/master

アスタリスクが付いてるものが現在チェックアウトされているブランチです。masterとなっていますね。
masterになっていなければ、git checkout masterをしてください。

$ git checkout master

ところで、上記には、upstreamが表示されていません。remote addしただけでは、単にリモートリポジトリへの設定を追加しただけなので、upstreamリモートブランチはまだ作成されていないのです。
#なお、gh-pagesというブランチはGitHub上のWebページを構成するためのブランチです。今回使わないため無視してください。

4.2 pull

では、masterブランチを最新化していきます。
ここまでで、リモートリポジトリは2つ設定されていますね(originとupstreamです)。この2つのリモートリポジトリからそれぞれ、最新の更新を取得していきます。

(1) まずはoriginから。

$ git pull  # もしくは git pull originでもよい
Already up-to-date.

(2) 次にupstreamの内容を取得・反映します。

$ git pull upstream
From https://github.com/mxcl/homebrew
 * [new branch]      gh-pages   -> upstream/gh-pages
 * [new branch]      master     -> upstream/master
You asked to pull from the remote 'upstream', but did not specify
a branch. Because this is not the default configured remote
for your current branch, you must specify a branch on the command line.

これで、本家の最新がローカルリポジトリに取り込まれました。

(3) ブランチの一覧を見てみましょう。

$ git branch -a
* master
  ...
  remotes/upstream/gh-pages
  remotes/upstream/master

upstreamが追加されていますね。

4.3 push master

この段階では、ローカルリポジトリに最新が取り込まれただけ、です。フォークした自分のリモートリポジトリ(origin)には反映されていません。
pushしてリモートリポジトリ側にも最新を反映させましょう。

$ git push origin master

4.4 branch spike

masterブランチが最新化されたので、masterブランチから作業用ブランチを派生させます。
ghostscriptのFormulaに対する修正作業用のブランチなので、ブランチ名はghostscript-spikeとしておきます。

$ git branch ghostscript-spike

5. 修正作業

ブランチが作成できたので、実際にFormulaを修正していきます。

5.1 checkout spike

作業用にghostscript-spikeというブランチを作成しました。ブランチを作成しただけでは、作業ツリーは変更されていません。ローカルリポジトリに新しくブランチが追加されただけです。
作業ツリーとは、ブランチ内から作業ディレクトリに展開されたファイル・ディレクトリ郡のことです。チェックアウトをすることで、ブランチの内容を作業ツリーに反映できるというわけです。

$ git checkout ghostscript-spike
Switched to branch 'ghostscript-spike'

5.2 Edit

作業ツリーにghostscript-spikeブランチの内容が展開されたので、ghostscript.rbを直接編集します。
今回は、ghostscriptのバージョンを9.04から9.05にアップデートするための変更を入れます。

■urlの変更
url 'http://downloads.ghostscript.com/public/ghostscript-9.04.tar.bz2'

url 'http://downloads.ghostscript.com/public/ghostscript-9.05.tar.gz'

md5の変更
md5 '9f6899e821ab6d78ab2c856f10fa3023'

md5 'f7c6f0431ca8d44ee132a55d583212c1'

■他、9.04用のパッチ処理を削除。

5.3 add

Gitの醍醐味、ステージングの時間がやってまいりました。

(1) ghostscript.rbの編集が終わった後の、現在の状態を確認してみましょう。git statusで確認できます。

$ git status
# On branch ghostscript-spike
# 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:   ghostscript.rb
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	ghostscript.rb~
no changes added to commit (use "git add" and/or "git commit -a")

># Changes not staged for commit:
># modified: ghostscript.rb
と表示されていることから、ghostscript.rbがステージされていないことが分かります。

また、トラックされていないファイルがありました。
># Untracked files:
># ghostscript.rb~
viで編集したときに~ファイルが作成されてしまったようです。rm ghostscript.rb~で消しておきました。

(2) さて、ghostscript.rbがステージされていないことが分かったので、git addコマンドでghostscript.rbをステージします。
ここでは、-iオプションを付けて、対話モードでステージしていきます。

$ git add -i
           staged     unstaged path
  1:    unchanged       +2/-24 Library/Formula/ghostscript.rb

*** Commands ***
  1: status	  2: update	  3: revert	  4: add untracked
  5: patch	  6: diff	  7: quit	  8: help
What now> # 入力を求められる

編集したファイルはghostscript.rbのみなので、ステージの候補にあがっているのは1ファイルだけですね。
(3) くどいですが、1を押してステータスを確認します。

What now> 1[Enter]
           staged     unstaged path
  1:    unchanged       +2/-24 Library/Formula/ghostscript.rb
...

ここで、unstagedの欄にある" +2/-24"は、差分において追加が2行、削除が-24行という意味です。
stagedの欄は"unchanged"となっているので、この時点ではまだステージされてい無いことが分かります。

5.3.1 updateコマンドを使った場合のステージ

(1) 変更したファイルをステージに追加するには、2:updateを実行します。

What now> 2[Enter]
           staged     unstaged path
  1:    unchanged       +2/-24 Library/Formula/ghostscript.rb
Update>> 

現在ステージに追加可能なファイルがリストアップされ、ここでまた入力が求められます。
(2) ここでは、Library/Formura/ghostscript.rbの1つしかなく、番号は1が振られているので、1を入力します。

Update>> 1[Enter]
           staged     unstaged path
* 1:    unchanged       +2/-24 Library/Formula/ghostscript.rb
Update>> 

(3) 今度は*が付きました。この状態でもう一度Enterを押すと確定されます。

Update>>  [Enter]
updated one path
…

updated one pathと表示されました。

(4) ここで、ステータスを確認しましょう。コマンドは1:statusですね。

What now> 1[Enter]
           staged     unstaged path
  1:       +2/-24      nothing Library/Formula/ghostscript.rb
…

今度はghostscript.rbがstagedになりました。この段階でステージは完了しました。
#なお、ステージから外すには3:revertのメニューを実行します。

(5) 更新対象のファイルがステージされたので、念のため差分をチェックしましょう。6:diffコマンドで確認できます。

What now> 6       # 6:diff
           staged     unstaged path
  1:       +2/-24      nothing Library/Formula/ghostscript.rb
Review diff>> 1    # 1を押して、ghostscript.rbを選択する
diff --git a/Library/Formula/ghostscript.rb b/Library/Formula/ghostscript.rb
index c2863fa..97c58d2 100644
--- a/Library/Formula/ghostscript.rb
+++ b/Library/Formula/ghostscript.rb
@@ -7,21 +7,15 @@ class GhostscriptFonts < Formula
 end
 
 class Ghostscript < Formula
-  url 'http://downloads.ghostscript.com/public/ghostscript-9.04.tar.bz2'
+  url 'http://downloads.ghostscript.com/public/ghostscript-9.05.tar.gz'
   head 'git://git.ghostscript.com/ghostpdl.git'
   homepage 'http://www.ghostscript.com/'
-  md5 '9f6899e821ab6d78ab2c856f10fa3023'
+  md5 'f7c6f0431ca8d44ee132a55d583212c1'
(以下省略) 

差分が確認できました。

(6) 7:quitでインタラクティブモードを終了します。

...
What now> 7
Bye.

Bye!

5.3.2 patchコマンドを使った場合のステージ

さきほどはupdateコマンドを使ってghostscript.rbの変更をステージしました。
今度は、patchコマンドを使ってステージしてみましょう。
まずはさきほどのステージを取り消します。

(1) もう一度、インタラクティブモードを起動します。
そして、3:revertを選択します。

$ git add -i
           staged     unstaged path
  1:       +2/-24      nothing Library/Formula/ghostscript.rb

*** Commands ***
  1: status	  2: update	  3: revert	  4: add untracked
  5: patch	  6: diff	  7: quit	  8: help
What now> 3   # 3: revertを実行

(2) これまで同様、ファイルの候補が表示されるので、1を選択し、Enterで確定します。

           staged     unstaged path
  1:       +2/-24      nothing Library/Formula/ghostscript.rb
Revert>> 1    # ghostscript.rbを選択
           staged     unstaged path
* 1:       +2/-24      nothing Library/Formula/ghostscript.rb
Revert>> [Enter]
reverted one path

reverted on pathと表示されました。
(3) 7:quitでインタラクティブモードを終了します。これで、さきほどのステージはキャンセルされました。


(4) では、実際にpatchコマンドを使ってみます。add に-pオプションをつけるとpatchコマンドになります。
(インタラクティブモードからでも5:patchでpatchが使えます)

$ git add -p
diff --git a/Library/Formula/ghostscript.rb b/Library/Formula/ghostscript.rb
...
-  url 'http://downloads.ghostscript.com/public/ghostscript-9.04.tar.bz2'
+  url 'http://downloads.ghostscript.com/public/ghostscript-9.05.tar.gz'
...
-  md5 '9f6899e821ab6d78ab2c856f10fa3023'
+  md5 'f7c6f0431ca8d44ee132a55d583212c1'
...
Stage this hunk [y,n,q,a,d,/,j,J,g,s,e,?]? 

差分が表示され、「このhunkをステージしますか?」と聞かれました。
patchではhunkと呼ばれる変更内容のかたまりごとにステージングできます。
updateコマンドではファイル単位でステージしたのに対し、patchではhunk単位でステージできます。変更差分を細かくチェックしながら、コミット対象を選別できるのでとても便利!

(4) >Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]?
さて、入力を求められましたが、それぞれどんな意味でしょう?
?を押すと確認できます。(日本語訳を追記しています)

Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? ?[Enter]
y - stage this hunk
        このhunkをステージする
n - do not stage this hunk
        このhunkはステージしない
q - quit; do not stage this hunk nor any of the remaining ones
        終了; このhunkをステージせず、残りのhunkについてもステージしない
a - stage this hunk and all later hunks in the file
        このhunkおよび以降のhunkを全てステージする 
d - do not stage this hunk nor any of the later hunks in the file
        このhunkをステージせず、以降のhunkについても全てステージしない
g - select a hunk to go to
        hunkを選択する
/ - search for a hunk matching the given regex
        正規表現にマッチするhunkを検索する
j - leave this hunk undecided, see next undecided hunk
        このhunkを決めないでおいて、次の未決のhunkをみる
J - leave this hunk undecided, see next hunk
        このhunkを決めないでおいて、次のhunkをみる
k - leave this hunk undecided, see previous undecided hunk
        このhunkを決めないでおいて、前の未決のhunkをみる
K - leave this hunk undecided, see previous hunk
        このhunkを決めないでおいて、前のhunkをみる
s - split the current hunk into smaller hunks
        現在のhunkをより小さいhunkに分割する
e - manually edit the current hunk
        現在のhunkを手動で編集する
? - print help
        ヘルプを表示する

hunkを直接編集できたりするんですねぇ、便利。
それはさておき、さきほどのhunkは問題ないので、yを押して確定しましょう。
(5) すると、次のhunkが表示され、同じように変更を取り込むかどうかを聞かれます。

Stage this hunk [y,n,q,a,d,/,j,J,g,s,e,?]? y
...
-
-__END__
-diff --git a/base/Makefile.in b/base/Makefile.in
-index 5b7847d..85e1a32 100644
---- a/base/Makefile.in
-+++ b/base/Makefile.in
...
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y

同様にyを押して確定させます。
次のhunkが無ければ、patchは自動的に終了します。

5.4 commit

(1) コミットの前にstatusを確認しておきます。

$ git status
# On branch ghostscript-updated
# Your branch is behind 'origin/ghostscript-updated' by 2 commits, and can be fast-forwarded.
#
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	modified:   Library/Formula/ghostscript.rb
#

># Changes to be committed:
to be committedだから、コミットされるのを待っている状態ということですね。

commitの直前でファイルを編集したらどうなるか

ではコミットを、
っとその前に、ステージングが終わった後で修正を入れたらどうなるのでしょうか?
(2) 試しに先頭行に"# this is comment."というコメント行を追加しました。
(3) コメント行を追加後、statusを見てみると、

           staged     unstaged path
  1:       +2/-24        +1/-0 Library/Formula/ghostscript.rb
...

unstagedの欄に、+1/-0が表示されています。コメント行が追加されたことが分かります。
(4) 5:patchを実行してみると、先ほど追加した1行分の差分しか出ていないことが分かります。

Patch update>> 
diff --git a/Library/Formula/ghostscript.rb b/Library/Formula/ghostscript.rb
…
+# this is comment.
…
Stage this hunk [y,n,q,a,d,/,e,?]? y

(5) yを押して反映しました。作業用のブランチですから、後で削除すればいいのです。

いざcommit

(6) ではコミットします。作業用のブランチですから気軽にポチッとな。

$ git commit

(7) ここでエディタが起動するので、コメントを記述します。

Gitでのコメントの書き方

Gitでは以下のようにコメントを書きます。

一行の要約
[空行]
変更の理由や内容

今回は単純に9.05にアップデートしただけなので、一行で記述しちゃいます。
"Ghostscript: update to 9.05"


記述した内容を保存してエディタを終了すれば、コミット処理が完了します。

(8) ついうっかり、(2)のところで"#this is comment"という行を追加して、しかもコミットまでしてしまいました。こんな行が入っていたら、本家の人から"What's this??"と言われてしまいますね。
さきほど追加した"#this is comment"の行を削除し、もう一度ステージングとコミットを行います。作業用ブランチなので気兼ねなく、ね。

面倒なので、今回はファイルを直接指定してステージしました。

$ git add Library/Formula/ghostscript.rb
$ git commit -m "Remove comment."
[ghostscript-spike d3d1b15] Remove comment.
 1 file changed, 1 deletion(-)

(9) ここまでで、2度コミットしました。コミットログはgit logコマンドで見ることができます。

$ git log --pretty=oneline
d3d1b15c323a0e8702c6d9a69bc3997be1d42b2e Remove comment.
1dd2ed5417b7c0a4dc35ca6bf679ffa343ea42ad Ghostscript: update to 9.05.
...

5.5 push

コミットが終わったら、サーバ側に反映しておきます。

ghostscript-spikeは作業用ブランチですから、サーバ側に置かずに作業することもできますが、サーバ側に置くといろいろと楽です。
例えば、開発用のMacでFormulaを修正して、それをサーバにアップさせた後、検証用のMacでFormulaが正しく機能するかを試しながらconfigureのパラメータを調整する、なんてことが出来ます。ビルドに時間の掛かるFormulaの検証をマシンパワーのある別のMacで行うときや、開発機とは別にクリーンな環境でビルド検証したい場合に便利です。

サーバ側のリポジトリに反映するにはpushを使います。初回はpush コマンドに-uオプションを忘れてはいけません。-uオプションをつけることで、push先のブランチをトラック出来るようになります。なお、このときconfigファイルに以下のような設定が追加されます。

[branch "ghostscript-spike"]
	remote = origin
	merge = refs/heads/ghostscript-spike

pushコマンドの形式には省略形がいろいろありますが、きっちり書くならこの形式。

$ git push -u <リモートリポジトリ> <ローカルブランチ>:<リモートブランチ>
$ git push -u origin ghostscript-spike:ghostscript-spike

リモートリポジトリにはghostscript-spikeというブランチはありませんが、上記の指定によりpushした時点でブランチが作成されます。
(1) ではpushします。

$ git push -u origin ghostscript-spike:ghostscript-spike
Counting objects: 14, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (10/10), 800 bytes, done.
Total 10 (delta 2), reused 0 (delta 0)
To git@github.com:toggtc/homebrew.git
   cb4768a..d3d1b15  ghostscript-spike -> ghostscript-spike
Branch ghostscript-spike set up to track remote branch ghostscript-spike from origin.


(2) GitHubに反映されたかどうかを見てみます。
f:id:toggtc:20120309220054p:image

反映されていますね!

6. 検証

実際に更新したFormulaを動かしたい場合は、GitHubのページからRawファイルのURLを取得してbrew installのパラメータに渡します。

(1) RawファイルのURLは、対象のFormulaのページを開いて「Raw」ボタンのリンクアドレスをコピーします。
f:id:toggtc:20120309220108p:image

f:id:toggtc:20120309220119p:image

(2) brew install のパラメータにRawファイルのURLを指定すればインストールが実行されます。

$ brew install https://raw.github.com … Library/Formula/ghostscript.rb

(3) 検証が済んだら、brew uninstall ghostscriptで削除できます。

$ brew uninstall ghostscript

なお、既に同一名のパッケージがインストールされているとインストール出来ませんので、事前にbrew uninstallしておきます。
#/usr/localとは別に、検証用のhomebrew環境を別ディレクトリに構築しておくといいかもしれないですね

おまけ:
なお、/usr/local側のhomebrewリポジトリをおかしくしてしまったら、以下のようにgit resetすると作業ツリーの変更等を破棄してリポジトリの最新の状態に置き換えられます。

git fetch https://github.com/mxcl/homebrew.git
git reset --hard FETCH_HEAD

それでもダメなら、homebrewを再構築してみるとうまくいくかもしれません。(無保証です)
http://toggtc.hatenablog.com/entry/2012/03/01/222352

7. 最新のマージ

Pull Request用のブランチを作る前に、本家の最新をマージしておきます。

7.1 checkout master

本家の最新を取り込むために、masterブランチに切り替えます。

$ git checkout master

後述のpullは、fetchとmergeを同時に行うのでしたね。(中編参照:http://toggtc.hatenablog.com/entry/2012/03/05/023137
mergeは現在の作業ツリーに対してマージ処理をしてしまうため、マージさせたいブランチに事前に切り替える必要があるのでした。

7.2 pull

一応、originの最新も取得しておきます。

$ git pull origin
$ git pull upstream

この時点でmasterブランチが最新化されていることを覚えておいてください。

7.3 push master

さきほどはghostscript-spikeをpushしましたが、今度は本家の最新を取り込んだmasterブランチをpushします。

$ git push -u origin master:master

7.4 checkout spike

後述のリベース作業を行うため、ghostscript-spikeブランチに切り替えます。

$ git checkout ghostscript-spike

7.5 rebase

masterが最新化できたので、リベースします。
リベースは簡単に言ってしまうと、ghotscript-spikeで行ったコミットを一度無かったことにして、最新の更新差分を反映し、その後でghostscript-spikeで行ったコミットを適用する処理です。
なぜ、ここでリベースをするかというと、Pull Requestするブランチの内容を、可能な限り本家のものと同じ状態にしたいからです。
rebaseのオプションには、どのブランチの更新をどのブランチに取り込ませるかを指定します。ここではmasterの更新をghostscript-spikeに適用しています。

$ git rebase master ghostscript-spike
Current branch ghostscript-spike is up to date.

今回は更新差分が無かったので、up to dateを表示されています。

rebaseとmergeの違い

rebaseとmergeの違いをオブジェクトグラフで見てみましょう。
以下の例では、本家のghostscriptが9.02だった頃にリポジトリをフォークを行い、その後、それぞれがコミットを加えたと仮定しています。
※オブジェクトグラフは簡略化しています。treeオブジェクトが無い、とか突っ込まないように。
f:id:toggtc:20120310004018p:image


この状態でmergeを行うと以下のようになります。
f:id:toggtc:20120310004029p:image


上図では、C2とC3の親コミットがC1であり、同一ファイルを参照しているため、マージする際にC1の子が2つになります。
ここで更新内容が競合するので手修正を行い、9.05に寄せます。ここでC4コミットが作成されます。
フォーク以降の赤枠部分の差分が、本家側はC3のみに対し、フォーク先はC3,C2,C4となっていることが分かります。
さらに、厳密にはC1にも差分が発生しています。本家のC1は子が1つなのに対し、フォーク先ではC3,C2の2つの子を持っています。

一方、rebaseを行うと以下のようなオブジェクトグラフが得られます。
f:id:toggtc:20120310004041p:image

フォーク先で変更したC2(9.05)を一旦無かったことにして、本家と同じようにC3(9.04)を加えた後、C3(9.04)の子としてC2’(9.05)を加えています。これなら差分はC2’(9.05)が増えるだけ、となり、フォーク元で更新を取り込むときに分かりやすいのです。

ただし、全てのケースでrebaseが有効であるとは限りません。
上記の例にあるC2’はC2とは似て非なるコミットです。なぜなら、rebase前と後ではオブジェクトグラフが異なるからです。前回、GitオブジェクトはImmutable(変更不可)であると述べました。ですから、既存のC2のparentにC3を設定したくてもできないのです。そこで、C3を親とする新たなコミットオブジェクトC2'を作成する、というわけです。

このことがどう影響するのでしょうか。
rebaseは差分コミットオブジェクトを無かったことにしてから再構築します。このため、rebaseする前のフォーク先のC2コミット(9.05)を、ある人がclone&checkoutして何らかの作業をしたとします。その状態であなたがrebaseをすると、C2コミットは無くなりますから、そのある人はマージ作業を強いられることになります。
特にGitHubの無料ユーザは、公開リポジトリしか作れません。もしかすると恥ずかしがり屋の誰かがあなたのリポジトリを(フォークする前に)こっそりcloneして、ローカルで作業をして、着々とPull Requestの準備を進めているかもしれません。そしてあなたの(私の)身勝手なrebaseで今夜も泣く泣くマージしているかもしれませんよ。なんてね。

7.6 push (ghostscript-spike)

rebaseした結果、何らかの差分を取り込んだ場合はリモート側に反映しておきましょう。

# git push -u <リモートリポジトリ> <ローカルブランチ>:<リモートブランチ>
$ git push -u origin ghostscript-spike:ghostscript-spike

8. Pull Request用ブランチの作成

8.1 checkout spike

Pull Reqest用ブランチを作成するため、元となるghostscript-spikeブランチに切り替えます。

$ git checkout ghostscript-spike

8.2 git checkout -b updated

checkoutコマンドに-bオプションをつけるとブランチを作成しつつ、チェックアウトを同時に行なってくれます。

$ git checkout -b ghostscript-updated
Switched to a new branch 'ghostscript-updated'

8.3 git rebase (squash)

圧縮コミットを行い、コミットログを1つにまとめます。

$ git rebase -i master

"-i"はインタラクティブモードの指定です。
masterブランチを指定しているのは、rebase対象のコミットを「フォークした時点以降」のコミットにするためです。
さきほどrebaseしたときの図でいうと、C3の位置が現在のmasterです。ここでは、C2'とC4以下にあるコミットを1まとめにしたいので、このような指定になるのです。
f:id:toggtc:20120310005159p:image

git rebase -i masterをすると、以下のような内容でエディタが起動します。

  1 pick f208bee GhostScript: update to 9.05.
  2 pick 20998bd Remove comment.
  3 
  4 # Rebase 4b41393..20998bd onto 4b41393
  5 #
  6 # Commands:
  7 #  p, pick = use commit
  8 #  r, reword = use commit, but edit the commit message
  9 #  e, edit = use commit, but stop for amending
 10 #  s, squash = use commit, but meld into previous commit
 11 #  f, fixup = like "squash", but discard this commit's log message
 12 #  x, exec = run command (the rest of the line) using shell
 13 #
 14 # If you remove a line here THAT COMMIT WILL BE LOST.
 15 # However, if you remove everything, the rebase will be aborted.
 16 #

先頭2行に表示されているのが、Fork以降に行った2つのコミットです。"Ghostscript: update to 9.05"と"Remove comment"を1つのコミットにします。
1行目はpickのままにしておきます。1行目にあるコミットを、そのままコミット対象にする、という意味です。
2行目については、pickからsquashに変更します。squashを指定すると、当該コミットとその直前のコミット(つまり1行目)を1つにまとめます。

  1 pick f208bee Ghostscript: update to 9.05.
  2 squash 20998bd Remove comment.

すると、またエディタが起動し、それぞれのコミットコメントを1つにまとめるように要求されます。

  1 # This is a combination of 3 commits.
  2 # The first commit's message is:
  3 Ghostscript: update to 9.05.
  4 
  5 # This is the 2nd commit message:$
  6 Remove comment.

ここでは2つ目のコミットコメントを削除して、"Ghostscript: update to 9.05"のみにしました。
編集を保存してエディタを終了すると、以下のようなメッセージが表示されます。

[detached HEAD e1c4135] Ghostscript: update to 9.05
 1 file changed, 62 insertions(+)
 create mode 100644 Library/Formula/ghostscript.rb
Successfully rebased and updated refs/heads/ghostscript-updated.

これで圧縮コミットの完了です。

8.4 push origin updated

圧縮コミットによってブランチを更新したのでpushして反映しましょう。
"-u"オプションを忘れずに。

$ git push -u origin ghostscript-updated:ghostscript-updated

9. Pull Request

Pull Request用のブランチがリモートに反映されたら、いよいよPull Requestを行います。

(1) まず、自分のhomebrewのGitHubページを開きます。
https://github.com/アカウント名/homebrew

(2) 右上に表示されている"Pull Request"ボタンを押します。
f:id:toggtc:20120310004250p:image

(3) "Change Commits"ボタンを押します。
#事前にpullしてもらうブランチを選択してからPull Requestを押してもOK
f:id:toggtc:20120310004304p:image

(4) ブランチ名をmasterからghostscript-updatedに変更して、"Update Commit Range"ボタンを押して確定します。
f:id:toggtc:20120310004321p:image

(5) 最後に、Pull Requestするときの件名とメッセージを入力して、"Send pull request"ボタンを押します。
f:id:toggtc:20120310004339p:image

なお、"Files Changed"タブを開くと、masterに対するファイルの差分が表示されます。
f:id:toggtc:20120310004355p:image

Pull Requestすると、本家に通知され、URLが払い出されます。
今回Pull Requestしたページは以下です。
https://github.com/mxcl/homebrew/pull/10379
#恥ずかしながら、パッチ処理のコードを一部削除しておらずadamv氏に突っ込まれていますが、なんとか本家に取り込んで貰えました。

今回はPull Request用のブランチを作成しましたが、もちろん、作業用ブランチに対して直接squashして、pull requestすることもできます。
しかし、requestしてみたら、いろいろと議論が発展して修正作業が必要になる場合も考えられなくはありません。そのため、ここでは作業用ブランチにあるコミットログはそのまま残しておいて、リリースブランチを別にしました。

10. 別のFormulaを更新する

上記の作業をひと通り終えると、既にhomebrewリポジトリをFork&cloneしている状態になるので、次からは「3. Pull Requestのチェック」から「9. Pull Request - 更新の取得を依頼する」の手順を行います。
#もちろん、この手順どおりでなけれなならない、ということはありません。Gitは多機能で恐ろしいほど柔軟性がありますので、好みの使い方ができます。


続く! 
実践Git&GitHub - homebrewをフォークするためのGit&GitHub入門 後編(2/2)
(続きの内容)
A.ブランチの削除
B. Forkしたリポジトリの削除
C. コマンドのまとめ
Gitを使った感想
参考