縁あって Eclipse Collections にコントリビュートすることができた。
Git は個人的な趣味プログラミングでしか使っておらず、 Pull Request を考慮した Git 操作というものについては全く知識も経験もなかった。
今回 Eclipse Collections に Pull Request を出すにあたって、様々な初めての Git 操作を経験することができ大変勉強になったので、忘れないようにメモする。
Eclipse Collections にコントリビュートするときのルール
適当に Pull Request を送れば良いわけではなく、いくつかルールがある。
具体的な話は eclipse-collections/CONTRIBUTING.md at master · eclipse/eclipse-collections に書いてある(英語)。
ざっと、以下のような感じ。
(2016年7月現在のものなので、今後変更される可能性があるかもしれません。実際にコントリビュートする際は、本家の説明を必ず読んでください)
- Eclipse コミュニティのアカウント作成
- ソースの修正
- 修正は、 GitHub 上でEclipse Collections のリポジトリ を fork して行う。
- コーディングスタイルのチェックは、 CheckStyle と FindBugs で行うので、 Pull Request を送る前に実行して問題が無いか確認する。
- Travis CI と連携しているので、それを利用するのも OK。
- というか、 Windows で動かそうとすると改行コードの設定に問題があるのか CheckStyle でエラーになるので、途中からは Travis CI でチェックするようにしていた。
- 単体テストは Windows 上でも動くので、 IntelliJ 上で動かしていた。
- コミット
- コミットメッセージは命令形にする(
Fixed bug
よりFix bug
)。 - GitHub の issue と関連するコミットの場合は、 issue へのメンションを入れる(
#18
とか) - Eclipse コミュニティに登録したものと同じメールアドレスで署名(
--signoff
)する。
- コミットメッセージは命令形にする(
- リリースドラフトの修正
- 自身が修正した内容を RELEASE_NOTE_DRAFT.md に追記する。
- Pull Request のコミット
- コミットは1つにまとめ、本家
master
の最新の上にrebase
してから送る。
- コミットは1つにまとめ、本家
Git のコミットログの矢印が持つ意味
よく Git のコミットログを説明するために上のような図が描かれることがある。
最初、この図を見た時「なぜ矢印の向きがコミットの順序(時間軸)と逆向きなのだろう?」と疑問に思っていた。
矢印といったら時間順序を表しているイメージがあり、それに逆行していることに違和感があった。
しかし、 Git のコミットが持つ特徴を知ることで、その意味が理解できるようになった。
Git のコミットには更新者やファイルのスナップショットの情報と共に、「1つ前のコミット」の情報が保持されている。
「1つ前のコミット」は、そのコミットを識別する属性の1つであり、これが異なるコミットは Git 上では別のコミットとして認識される。
矢印は、この「1つ前のコミット」を表している。
矢印で「1つ前のコミット」を表現することで、変更内容が同じでも「1つ前のコミット」が異なればそれらのコミットは別物であることが意識しやすくなる。
また、もし rebase
で「1つ前のコミット」を変更した場合も「コミットが別物に変化した」ということを意識しやすくなる。
参考
今思えば(振り返り)
以下に記載している操作は実際に試行錯誤しながら実施した Git 操作なので、今思えば他の方法があったのではないかなぁと思っている。
例えば、 Pull Request は master
から出すのではなく、 topic
ブランチから出しておけばもう少し楽だったかもしれない。
master
は常に本家の状態を追従するようにしておけば、 reset --hard などの作業 も不要だったのかなぁと思っている。
Git 操作
以下、実際にやった Git 操作。
本家リポジトリを fork する
- これは簡単。
- GitHub 上で「Fork」ボタンを押して自分のアカウントを fork 先に指定すればいい。
fork したリモートリポジトリをローカルに clone する
$ git clone <url>
- これも簡単な話。
ローカルで開発する
# 作業用のブランチを作成し、そちらへ移動
$ git checkout -b topic
# 署名付きでコミット
$ git commit --signoff -m "<コミットメッセージ>"
- ローカルに
clone
したリポジトリで開発作業を行う。 -
master
から作業用のブランチ(トピックブランチ)を作成、そちらでコミットを行う。 - コミットには署名を付けなければならないルールなので、
--signoff
オプションを付けてコミットする。-
git config --list
で表示される設定の中のuser.email
で設定されているメールアドレスが署名に使用される。 - このメールアドレスは、 Eclipse コミュニティに登録しておいたメールアドレスと同じものにしておくこと。
-
- ローカルで開発している間も、本家の master ブランチにはどんどんコミットが追加されていっている。
コミットを1つにまとめる
# 過去4件のログを確認
$ git log --oneline --decorate --graph -4
* 12ebd5e (HEAD, topic) c
* 1ba2503 b
* 50a8717 a
* 280720d (master) 1
# rebase -i でコミットをまとめる
$ git rebase -i HEAD~3
# rebase 後のログを確認
$ git log --oneline --decorate --graph -2
* 8a1a17c (HEAD, topic) a + b + c
* 280720d (master) 1
- Pull Request 時にはコミットを1つにまとめるルールなので、ローカルで行ったコミットを1つにまとめる。
-
git rebase -i
を使用すると、複数のコミットを1つにまとめることができる。 -
HEAD~3
の部分は、HEAD
(現在の作業ブランチの最新)から 3 つ前のコミットを対象にする、という意味になる。
rebase -i 中に立ち上がるエディタ
pick 50a8717 a
pick 1ba2503 b
pick 12ebd5e c
(以下略)
-
rebase -i
を実行すると、指定した範囲のコミットが古いもの順に表示されたエディタが開く。 - 先頭の
pick
の部分を変更することで、そのコミットをどう変更するかを指定できる。 -
s
またはsquash
と記述すると、そのコミットは直上のコミットに統合される。 - 今回はコミット a に全てのコミットを統合したいので、以下のように書き換える。
pick 50a8717 a
s 1ba2503 b
s 12ebd5e c
(以下略)
- 保存してからエディタを終了させると、次にコミットコメントを修正するエディタが開く。
# This is a combination of 3 commits.
# The first commit's message is:
a
# This is the 2nd commit message:
b
# This is the 3rd commit message:
c
(以下略)
- 統合対象のコミットのコミットメッセージが表示されるので、統合後のコミットメッセージに整形する。
-
#
で始まる行はコメントとなり無視される。 - また、空行も無視される。
# This is a combination of 3 commits.
a + b + c
(以下略)
- 修正が完了したら、保存してからエディタを終了させる。
本家 master の最新を取り込む
# 本家のリポジトリをリモートに追加する
$ git remote add eclipse git@github.com:eclipse/eclipse-collections.git
# リモートが追加されたことを確認する
$ git remote -v
eclipse git@github.com:eclipse/eclipse-collections.git (fetch)
eclipse git@github.com:eclipse/eclipse-collections.git (push)
origin git@github.com:opengl-8080/eclipse-collections.git (fetch)
origin git@github.com:opengl-8080/eclipse-collections.git (push)
# リモート追跡ブランチに最新の情報を取り込む
$ git fetch eclipse master
# 情報が取り込まれたことを確認する
$ git branch -av
master 280720d [ahead 2] 1
* topic 8a1a17c a + b + c
remotes/eclipse/master 1f699b5 4
(略)
# ローカルの master に eclipse/master を merge して一致させる
$ git checkout master
$ git merge eclipse/master
# マージ後のログを確認
$ git log --oneline --decorate --graph --branches -5
* 1f699b5 (HEAD, eclipse/master, master) 4
* b958a4b 3
* 707e739 2
| * 8a1a17c (topic) a + b + c
|/
* 280720d 1
- 本家の
master
を取得するため、eclipse
という名前でリモートを追加する。 -
fetch
コマンドでリモートの情報をローカルのリモート追跡ブランチ (eclipse/master
)に落としてくる。 -
git checkout
で一旦master
ブランチに移動してから、merge
コマンドでeclipse/master
ブランチの内容をマージする。 - これで、ローカルの
master
ブランチは本家のmaster
と同じ状態になる。
rebase でマージする
# rebase 前のログを確認
$ git log --oneline --decorate --graph --branches -5
* 1f699b5 (HEAD, eclipse/master, master) 4
* b958a4b 3
* 707e739 2
| * 8a1a17c (topic) a + b + c
|/
* 280720d 1
# rebase で topic の変更を master の先頭に持ってくる
$ git checkout topic
$ git rebase master
# rebase 後のログを確認
$ git log --oneline --decorate --graph --branches -5
* 62cce6a (HEAD, topic) a + b + c
* 1f699b5 (eclipse/master, master) 4
* b958a4b 3
* 707e739 2
* 280720d 1
# merge コマンドで master に topic をマージする
$ git checkout master
$ git merge topic
# マージ後のログを確認
$ git log --oneline --decorate --graph --branches -5
* 62cce6a (HEAD, topic, master) a + b + c
* 1f699b5 (eclipse/master) 4
* b958a4b 3
* 707e739 2
* 280720d 1
- 最新化した
master
ブランチにローカルでの変更をマージする。 - Eclipse Collections では、
rebase
によるマージがルールとなっている。 -
rebase
とは、あるコミットのベース(1つ前のコミット)を別のコミットに切り替える処理のことで、今回はtopic
のコミットをmaster
の最新の後ろに切り替える処理を行う。
rebase 時にコンフリクトした場合
-
rebase
時にコンフリクトする可能性は十分ある。 -
rebase
時にコンフリクトが発生すると、 git はrebase
処理を中断してコンフリクトの解消を促してくる。
$ git rebase master
First, rewinding head to replay your work on top of it...
(略)
CONFLICT (content): Merge conflict in text.md
(略)
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
- コンフリクトしたファイルを修正したら、そのファイルを
git add
でステージングに上げてからgit rebase --continue
を実行する。 - これで、 git は中断していた
rebase
処理を再開する。
rebase しないで merge した場合
-
rebase
せずにmaster
にtopic
ブランチをマージした場合、コミットは上図のようになる。 - マージしたことを表すコミット(マージコミット)が作成され、ログ上はブランチが分岐したという情報が残ることになる。
- ただし Fast-Forward Merge が可能な場合はマージコミットは作成されない。
-
rebase
した場合は Fast-Forward Merge になるので、マージコミットは作成されなくなる。
rebase して Fast-Forward Merge した場合のメリット・デメリット
- メリット
- 履歴が一直線になるため、コミットログが見やすくなる。
- デメリット
- ブランチを切ったときの情報は失われる。
merge で Non Fast-Forward Merge した場合のメリット・デメリット
- メリット
- コマンドが単純。
- ブランチを切ったときの情報は残る。
- デメリット
- たくさんブランチを切っている場合は、履歴が複雑になる可能性がある。
リモートに push してから Pull Request を送る
git push origin master
- ローカルでのマージが完了したら、それを GitHub の自分のリポジトリに
push
する。 -
push
が完了したら、画面上で「New pull request」ボタンを押して Pull Request を作成する。 - あとは、中の人にレビューして頂くのを座して待つ。
Pull Request の結果さらなる修正が必要になった
- 残念ながら?初回の Pull Request は一発 OK を貰えなかった。
- 指摘を受けた問題をローカルで修正するため、
topic
ブランチで修正作業を再開させた。
ローカルで新規に作成したコミットを Pull Request 用のコミットに統合する
# rebase 前のログ確認
$ git log --oneline --decorate --graph --branches -7
* d446040 (HEAD, topic) e
* 991821f d
* 62cce6a (master) a + b + c
* 1f699b5 (eclipse/master) 4
* b958a4b 3
* 707e739 2
* 280720d 1
# rebase -i でコミットを統合
$ git rebase -i HEAD~3
# rebase 後のログ確認
$ git log --oneline --decorate --graph --branches -6
* cb077a7 (HEAD, topic) a + b + c + d + e
| * 62cce6a (master) a + b + c
|/
* 1f699b5 (eclipse/master) 4
* b958a4b 3
* 707e739 2
* 280720d 1
- Pull Request のコミットは1つにまとめるので、ローカルで追加したコミット
d
とe
を、a + b + c
のコミットに統合する。 - コミットの統合は、前述 した
rebase -i
を使用する。
再び本家 master の最新を取り込む
# 本家 master の情報を取得する
$ git fetch eclipse master
# 本家の最新が取得できたことを確認
$ git branch -av
master 62cce6a [ahead 6] a + b + c
* topic cb077a7 a + b + c + d + e
remotes/eclipse/master cacd681 6
# ローカルの master を eclipse/master で上書きする
$ git checkout master
$ git reset --hard eclipse/master
# 上書き後のログを確認
$ git log --oneline --decorate --graph --branches -7
* cacd681 (HEAD, eclipse/master, master) 6
* 5082497 5
| * cb077a7 (topic) a + b + c + d + e
|/
* 1f699b5 4
* b958a4b 3
* 707e739 2
* 280720d 1
- Pull Request を修正している間も、本家の
master
には次々とコミットが追加されている。 - これらを再びローカルに取り込んでマージしなければならない。
- 本家からの情報取得は
fetch
コマンドで実行する。 - ローカルの
master
を本家master
と同じ状態にするには、reset --hard
を使用する。- ※このときローカルの
master
の情報は消えてしまうので、消えたら困るコミットがある場合は他の方法を検討する必要がある。
- ※このときローカルの
再び rebase でマージする
# rebase 前のログ確認
$ git log --oneline --decorate --graph --branches -7
* cacd681 (HEAD, eclipse/master, master) 6
* 5082497 5
| * cb077a7 (topic) a + b + c + d + e
|/
* 1f699b5 4
* b958a4b 3
* 707e739 2
* 280720d 1
# rebase で topic を master の先頭に持ってくる
$ git checkout topic
$ git rebase master
# merge で master の先頭に topic の内容をマージ
$ git checkout master
$ git merge topic
# マージ後のログを確認
$ git log --oneline --decorate --graph --branches -7
* d6243de (HEAD, topic, master) a + b + c + d + e
* cacd681 (eclipse/master) 6
* 5082497 5
* 1f699b5 4
* b958a4b 3
* 707e739 2
* 280720d 1
- ここでの作業内容はrebase でマージしたときのものと同じになる。
修正内容をリモートに push する
$ git checkout master
$ git push -f origin master
- 修正+本家最新のマージが完了したので、修正したコミットをリモートに
push
する。 - しかし、そのまま
push
しようとしてもエラーになってしまう。- ローカルの
master
は本家の最新をもとに作りなおしているため、リモートのmaster
とは全然違う履歴になってしまっているため。
- ローカルの
- リモートに存在する前回の Pull Request のコミットはもう不要なので、今回
push
し直す情報で上書きしても問題ない。 - なので、
push
コマンドに-f
オプションをつけることでリモートの情報をpush
した情報で強制的に上書きしている。 - GitHub 上の Pull Request の表示も、
-f
付きでpush
することで新しいコミットの内容で更新されるようになっている。 - もし Pull Request のスレッド上でコードにコメントを付けていたとしても、そのコメントが失われることはない。
おわりに
伊藤博志さん
今回 Eclipse Collections にコントリビュートするという素晴らしい機会をくださり、ありがとうございました。
GS 社の方々との英語でのやりとりをサポート頂いたり、開発方法についての様々なご質問にも丁寧にご回答頂き、とても助かりました。
Craig P. Motlin さん、 Donald Raab さん
私の拙い英語にも親切に対応頂き、ありがとうございました。
私の書いた英語がネイティブの方に通じていると感じた時は、とても感動しました。