みなさん、Git使ってますか?僕はまだメインのVCSがSubversionなのもあって、なかなか慣れません。せっかくGitを使っているのに、ちょっと不便なSubversionくらいの位置づけです。でも、同じような理解度の人って多いんじゃないでしょうか。
一方で、最近はGitHub管理のオープンソースプロジェクトが増えてきました。バグレポートを送るにしてもpull request*1が前提のような空気があり、Git初心者には少し敷居が高い印象があります。
そんな僕も先日初pull requestをしてみたんですが、色々な失敗の積み重ねで残念なpull requestになってしまいました。その反省を元に、本稿ではpull requestする際のベストプラクティスを紹介します。これは「Git Workflow」をベースにコマンド例などを加筆したものです。
pull requestする際は、次の流れのようにすれば万全です。
$ git clone git@github.com:hnw/Spoon-Knife.git
$ cd Spoon-Knife
$ git push origin myFeature
ここで大事な点を強調しておきますが、絶対にmasterブランチで作業してはいけません。また、masterブランチからpull requestを送るのもいけません。必ずpull requestのための別ブランチから送るようにしましょう。これにはいくつか理由があるのですが、順を追って説明していきます。
上記の手順4について説明します。手順7でpull request用のローカルブランチをpushしていますが、いきなりpull request用のブランチで作業するのではなく、作業用にさらに別のブランチを作ることをお勧めします。簡単な修正をするつもりでも、試行錯誤して何度もcommitしたり、最初からやり直したりするかもしれません。そのための作業用ブランチを別に切っておくというわけです。
作業用ブランチには、spike(XP用語、お試し実装といった意味だと理解しています)とかprototypeとかいう単語を入れておくと、GitHubを見ている他の人にもブランチの意図がわかりやすくて良いでしょう。
$ git checkout -b myFeatureSpike Switched to a new branch 'myFeatureSpike' $
では、ファイルを編集して次のようにローカルリポジトリにcommitを行ったとします。
$ vi README $ git commit -a -m '感想を追記' [myFeatureSpike 12ca2a5] 感想を追記 1 files changed, 3 insertions(+), 1 deletions(-) $ vi README $ git commit -a -m 'コナミコマンドについてREADMEに追記(ネタバレ)' [myFeatureSpike bd66e17] コナミコマンドについてREADMEに追記(ネタバレ) 1 files changed, 2 insertions(+), 0 deletions(-) $
一通りローカルでの作業が済んだらpushしましょう。ローカルのmyFeatureSpikeブランチの内容を同名のリモートブランチとしてpushします。
$ git push origin myFeatureSpike Counting objects: 8, done. Delta compression using up to 2 threads. Compressing objects: 100% (6/6), done. Writing objects: 100% (6/6), 789 bytes, done. Total 6 (delta 1), reused 0 (delta 0) To git@github.com:hnw/Spoon-Knife.git * [new branch] myFeatureSpike -> myFeatureSpike $
作業中にfork元のリポジトリが更新されることがあるかもしれません。これに追随するための手順は次のようになります。
$ git remote add upstream git://github.com/octocat/Spoon-Knife.git $
$ git stash Saved working directory and index state WIP on myFeatureSpike: bd66e17 コナミコマンドについてREADMEに追記(ネタバレ) HEAD is now at bd66e17 コナミコマンドについてREADMEに追記(ネタバレ) $
$ git checkout master Switched to branch 'master' $
$ git pull upstream master From git://github.com/octocat/Spoon-Knife * branch master -> FETCH_HEAD (略) $
$ git checkout myFeatureSpike Switched to branch 'myFeatureSpike' $
$ git rebase master myFeatureSpike First, rewinding head to replay your work on top of it... Applying: 感想を追記 Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... Auto-merging README Applying: コナミコマンドについてREADMEに追記(ネタバレ) Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... Auto-merging README $
$ git push origin master (略) $ git push -f origin myFeatureSpike (略) $
$ git checkout myFeatureSpike Switched to branch 'myFeatureSpike' $ git stash pop (略) $
上記フローでは、masterブランチをfork元への追随のために利用しています。これがmasterブランチ上で作業をしない最大の理由だと言えるでしょう。
この作業をしなくてもpull requestすることはできますが、万一最新版とコンフリクトするような場合は対処を中の人に任せることになってしまいます。中の人は忙しいことも多いでしょうから、できるだけ最新版までrebaseしてからpull requestすべきです。
さて、ついに作業ブランチ上に満足のいくファイルをcommitできたとしましょう。fork元の更新にも追随できています。早速pull requestだ!と言いたいところですが、その前に今までの作業を1commitにまとめたブランチを作り直しましょう。
というのも、作業ブランチ上では試行錯誤したりfork元に追随したりした跡が複数commitに分かれているはずです。これらをそのまま本家プロジェクトでpullしてしてしまうと、複数commit全てが本家の履歴として残ってしまいます。後で差分をチェックする人のためにも、あらかじめ整理してからpull requestすべきでしょう。これは次のような作業になります。
$ git checkout myFeatureSpike Switched to branch 'myFeatureSpike' $
$ git checkout -b myFeature Switched to branch 'myFeature' $
$ git rebase -i master
git rebase -iで全差分を1 commitにまとめるには、2個目以降のcommitを全部squash指定します。たとえば次のような内容がエディタに表示されたとしましょう。
pick 12ca2a5 感想を追記 pick bd66e17 コナミコマンドについてREADMEに追記(ネタバレ) # Rebase bdd3996..bd66e17 onto bdd3996 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. #
このような場合、次のように修正してセーブ・終了します。
pick 12ca2a5 感想を追記 squash bd66e17 コナミコマンドについてREADMEに追記(ネタバレ) # Rebase bdd3996..bd66e17 onto bdd3996 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. #
すると次に2個分を1個にまとめたcommitのログを書くよう求められますので、適当に書いてセーブしましょう。これでmyFeatureSpike上の複数commitがmyFeatureブランチ上では1 commitにまとまった状態になりました。
fork元の最新状態に追随できていていますか?1 commitにまとめましたか?では、いよいよpull requestしましょう。
$ git push origin myFeature (略) $
pull requestはGitHubのissue tracking systemと同期しており、pull requestと同時にチケットが起票されます。ふだんのバグレポートと同様に、問題点とパッチの概要などを簡単に書けばよいでしょう。
1点だけ、pull request後の注意点があります。pull requestに使ったブランチで別の作業をしてはいけません。万一無関係なcommitをpushしてしまうと、pull requestにも反映されてしまいます。
masterブランチからのpull requestが許されるのは小学生までです。僕の失敗談を紹介すると、あるプロジェクトに2つの修正をそれぞれpull requestするつもりだったのが、2個ともmasterで作業していたため、2つの修正が混ざったカオスなpull requestを投げてしまいました。ホントに迷惑すぎですね。
今回の手順に従うと、かなりGitっぽい作業ができるはずです。というより、上記はGitで協調作業するときの流れをpull requestに合わせて説明したものだと言えます(originとupstreamとでremoteが2個あるため、通常のパターンよりも複雑ですが)。
面倒すぎるだろ!と感じた人がいるかもしれません。特にブランチ周りはGit初級者がつまずきやすい場所だという印象がありますが、Subversionとの違いがわかればじきに慣れると思います。チンプンカンプンな方はPro Gitの「ブランチとは」あたりを読み進めてみてください。
僕は必要にならないと覚えない性質なので、今回pull requestしてみてGitの理解が随分進んだ気がします。皆さんもpull requestのついでにGitの知識を深めてみませんか?
*1:自分のGitHubへのcommitをそのまま本家に取り込んでよ、というリクエスト。BTSへの登録も同時に行われる。GitHub用語。
*2:これはforkのデモ用リポジトリで、GitHubのドキュメント「Fork A Repo」の中で紹介されています。GitHubのfork数ランキング第3位(2011/5/28日現在)という大人気プロジェクトです。
pull request際には参考にさせていただきます。
pull requestする際には