読者です 読者をやめる 読者になる 読者になる

ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

【下】レビューを中心に据えた開発に於ける、 git rebase の活用例【書】

タイトル適当。

まともに書いてブロッグにアップしたかっt

必要なもの

  • gitに対する愛
  • 開発プロセスに対する愛(わぁいレビュー あかりレビュー大好き)

前提状況

申し訳ないけど「gerrit の機能が一通り使える」ことが前提のフロー。githubは読み替えて。gerrit 自体の使い方も、正直よい日本語の記事が無いので時間があれば書くかもしれない。

で、以下のような機能を作る

  • 1) Foo 機能の処理モジュール(Service)
  • 2) Foo 機能のHTTPサーバ側エンドポイント(Controller)
  • 3) Foo 機能のブラウザ側の表示(View)
  • 4) Foo 機能のブラウザ側からのサーバ側への連携(Client-side Cooperation)

2 は 1 に、 4 は 1-2 と 3 に依存している。とりあえず上から作っていく。この時点では 2 までできたよ、とする。後、トピックごとに(タグだと後述するように不都合なんで)ブランチを切り直し(features/N)、開発のてっぺんは topic-head とでもしておこう。

---(*)---(1)---(2)...(3)...(4)  [topic-head]

git rebase -i

まず、(1)のみレビューが帰ってくる。(1)に含まれる3つのコミットのうち「1つめ」にコメントがついたので直したい。

git checkout features/1
git rebase -i HEAD~3
pick abcd12 Implement Foo model
pick ef3456 Implement Foo service
pick 7890ab Add some validations

....

上記の p(ick) の個所を変更する。

e abcd12 Implement Foo model
pick ef3456 Implement FooService
pick 7890ab Add some validations

#...
# 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.
#

e は edit 。他にもよく使うのは、 s(quash) と f(ixup) か。まあ書いてあるね。なお、コミットの行を消せばそのコミットは無かったことになる(取り扱い注意、このスタイルで開発していると git reflog が溜まりすぎてうっかり消すと探せない)し、並び替えたいときはそのままカットペーストする。一種のコミット操作DSLか。

で、リベースが始まり、「e」のコミットの直後の辺で止まる。

# On branch ~~~(実際の文言はなんかもっと違う)
nothing to commit (working directory clean)

git diff --stat HEAD~1..HEAD ってコマンドを発行すると、その直前のコミットで何がどう変わったかが分かるので、ぼくはエイリアスに登録している。で、直したいファイルをまずは直し、 commit --amend で例の直前のコミットに溶かし込む。

git add .
git commit --amend

メッセージは変えても変えなくても良い。 # gerrit であれば、このとき「Change-ID」を変えてはならない。

そうしてリベースを続ける。

git rebase --continue

最終的に「過去が変更された」 (1) を実装した新しいブランチ (1') が生まれるので、 gerrit なり何なりでさっきと同じトピックにプッシュする。

  • rebase するので、各コミットのハッシュ値が変わる(tagとかは、消える)
  • ハッシュ値が変わるので、既存の先々のコミット(すでにできてる2)につながらない「新しいブランチ」が生えている。

そして、無事コミットがレビューを通ってマージされました。良かった。承認されてうれしい!

git checkout master && git pull --rebase

---(*)---(1') [master]
    |
    +----(1)---(2)---(3)...(4) [topic-head]

あっ!大変だ!

  • 今開発のブランチ topic-head と全然関係ないところに master が伸びていった。 (2) 以降の今後やいかに?
  • さらに (3) までできてしまった!

git rebase master

慌てず騒がず、 (2) の位置に立ち戻り、

git checkout topick-head
git rebase master

---(*)---(1') [master]
    |     |
    |     +----(2)---(3) [topic-head]
    |
    +----(1)---(_2)--(_3)... [abandon]

これで、無事 (2) は (1') の続きから開発されていることにできる。実際、(1')の変更も無事反映された状態になる。

git rebase master 、日本語に訳すと「現在のブランチの根っこを master のてっぺんにする」ぐらいの意味である。

時々 merge のようにコンフリクトするかもしれない。そのときは、 git status とか git diff とかでどうなってるか確認して修正できる。特に git diff はコンフリクト専用の表示になるのでえらく便利。

git add .
git rebase --continue

原則、「コンフリクトは、コミットしてはいけない」。そのままコンティニューだ。

ちなみに、

---(*)---(1)
    |
    +----(2)
    |
    +----(3)---(6)
    |
    +----(4)
    |
    +----(5)

みたいな開発をしている際でも、 git rebase master で(forkやmergeが)多い日も安心です。

git rebase --onto

ところで、レビューに出したものの、「来週レビューすっから」と言われているが炎上しているので仕方なく、週末に家で(3)どころか(4)まで作ってしまったとしよう(事実とは無関係なたとえです)。

翌週無事マージされました。

---(*)---(1') [master]
    |
    +----(1)---(2)---(3)---(4) [topic-head]

色々あって、以下のようになりました。

---(*)---(1')--(2')--(3') [master]
    |     |     |
    |     |     +----(3w)
    |     |
    |     +----(2x)--(3x)--(4x)
    |     |           |
    |     +----(2y)  (3y)--(4y) [topic-head ...?]
    |
    +----(_1)--(_2)--(_3)--(_4)

色々あったんです。「仕様変更!そういうのもあるのか!」みたいな。

この状態で、素直に git rebase master topic-head するのは無限コンフリクトでダルい。というか、(4y)のコミットだけ奇麗に欲しいんです。どうすればいいの。

(4x)にあたるコミットを一つ一つ cherry-pick するのも手ではあるが、 SHA1 をコピペ間違えたりしそうで危ない。あなたと、 rebase --onto

git rebase --onto master features/3y features/4y

git rebase --onto master features/3y features/4y とは、日本語にすると「features/4y(省略するとカレントブランチ)なんですけど、今 features/3y の根っこのところから、 master のてっぺんに挿し木しましょう」ぐらいの意味なんじゃないかな。

で、こうすると、若干のコンフリクト修正の後(多くとも (4y) 相当のコミット回数分しかないですから)、

---(*)---(1')--(2')--(3')--(4y') [master]
    |     |     |
... どうでも良いブランチ

本質的な「(4)をどうするか?」に集中できる。この状態なら、一回ぐらいレビューさし戻っても心が折れないはず!

とはいえ、 git は頭が良いのでたいていは rebase master で事足りるはずだし、さらに言えばなるべく --onto 使わないように無理に仕事を進めまくるべきではない。ちゃんとレビューでフィードバックをもらいましょう。

まとめ

  • git rebase を効率的に使えば、レビューを回しながら時間を有効活用して開発が進められる。コミットを無駄にしない!
  • とはいえ、やり過ぎ注意。

言っていないこと

  • 前提として、「1つ1つのコミットを意味のある、奇麗なものに」を徹底していないと戦えない
  • git diff とか活用すればコンフリクト修正のある程度の助けになるが、割とセンスや慣れ、あと人のコミットを読む力が要る
  • reflog ちょう溜まる。 git gc しましょう。