Gitリポジトリ運用の最適解

続編のような物(2012/11/21追加)

掲題の件について最近調べたことをまとめることにします。

はじめに

タイトルだいぶ誇張していますが、結論から言うとマージコミットを発生させないGitの使い方です。


マージコミットが悪という言葉を最近よく目にしていたのですが下記リンク先の説明でしっくり来ました。

そのためマージコミットが発生しないような使い方を目指します。


また、A successful Git branching modelを実現するためのgit-flowというもののありますが今回は触れません。
(ちなみにA successful Git branching modelではマージコミットは必ず残すことを推奨しています)
Non-Fast-Forwordマージでの利点はブランチでの作業履歴が残ることと認識しています。
Fast-Forwordマージのみを使用する場合作業履歴はすべて一直線になりどのブランチで作業していたかどうかはわかりません。

マージコミットとは


このコミットのような物です。Mergeの欄に親となるコミットのハッシュが2つ存在しています。

どのような時に発生するのか

マージコミットが発生するマージは次の条件のいずれかです。

  • Non-Fast-Forwordマージを行う
  • no-ffオプションを使ってマージを行う
Fast-Forwordマージとは

Non-Fast-Forwordマージと述べていますがこれはFast-Forwordマージでない時です。ではFast-Forwordマージとはどのような状態でしょうか。

ブランチdevにmasterブランチでの変更の履歴はすべて含まれている為devブランチをmasterにマージするとmasterのHEADはマージされるブランチのHEADと同じになるのでコミットDになります。
Fast-Forwordマージでは新たなコミットは発生しません。HEADがコミットDに移動するだけです。


逆にNon-Fast-Forwordな状態とはどういうものでしょうか

ブランチdevをmasterブランチにマージをしようとしますが、devブランチには含まれていないコミットDがmasterブランチに存在します。その為GitはコミットCとコミットDを自動的にマージしてマージコミットEを作成します。
ちなみにno-ffオプションというのはどんな状況でもNon-Fast-Forwordなマージを行い、マージコミットを作成するということです。

どうすればいいのか

git-rebaseを使います。上記のNon-Fast-Forwordの状態の時にdevブランチでgit rebase masterを実行するとこのようになります。

コミットBの時点で作ったdevブランチですが、現在はコミットDから分岐しています。masterブランチの最新の内容を取り込んだ後にコミットCを適応します。
この際にコミットCのハッシュ値は変更されるためコミットC'としています。

mergeではなくrebaseをする場面

git-pull

git-pullはリモートリポジトリの変更内容をローカルブランチにマージします。内部的にはgit-fetch&&git-mergeをやっているようです。
git-pushでリモートリポジトリに作業内容を反映させる際にFast-Forwordでないとpushができません。
その為複数人の開発時などには必然的にpushするまえにpullをすることになるでしょう。


その際にリモートリモートリポジトリに新しい内容があった場合当然Non-Fast-Forwordのマージが発生しマージコミットが作られてしまいます。
そのためにあるのが--rebaseオプションです。git-pullを行う際にrebaseを行いリモートリポジトリの内容を取り込んだあと作業内容を反映します。

$ git pull --rebase

また、git 1.7.9からはgit-pullのデフォルトをrebaseするようにするオプションが新設されました。下記コマンドを実行すると~/.gitconfigに書き込まれます

$ git config --global pull.rebase true
ローカルブランチのマージ

開発時にmasterブランチではなく必ず作業用のローカルブランチ(今回のdev等)を開発して作業を行い、
作業が完了or一段落したらmasterにmergeをしてリモートリポジトリにpushするというやり方をしてます。


次のようなやり方をするとマージコミットが発生しなくてよさそうです。

$ git checkout -b dev
$ git commit -m "開発"
$ git checkout master
$ git pull --rebase # リモートの更新をとりこんだ
$ git rebase master dev # git checkout dev; git rebase masterをやったのと同じ
$ git checkout master
$ git merge dev
$ git push


このようにするとマージコミットが発生しない綺麗な履歴を保つことができそうです。

その他Tips

関係ないちょっとしたTipsのコーナー()

alias

上記の例ではくそ真面目に全部うっていましたがこんなの売ってられないのでgitconfigにaliasを書いています

[alias]
    st = status
    ci = commit
    br = branch
    co = checkout
    df = diff
シンプルなstatus

通常の表示だと下記のような感じになりますが毎回この説明は要らない気がしますね。

$ git st
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	modified:   homura
#	modified:   madoka
#
# 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:   kyouko
#	modified:   mami
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	sayaka
  • sをつけると簡易表示になります
$ git st -s
M  homura
 M kyouko
M  madoka
 M mami
?? sayaka

また設定すれば色がついてindex済みのものが緑、その他が赤(デフォルトの色)になりますのでもっと見やすいかと思います。こんなのを足すといいかもです

[color]
    branch = auto
    diff = auto
    status = auto   
作業履歴を視覚的にみたい
$ git log --graph --pretty='format:%C(yellow)%h%Creset %s %Cgreen(%an)%Creset %Cred%d%Creset'

という風にすると作業履歴がみれます。今回取り扱ったマージコミットでの表示をみてみましょう。
毎回叩いてられないのでaliasに設定しておいてください適当に


マージコミット

*   dc89d6e commit e (arcright)  (HEAD, master)
|\  
| * 43dad78 commit c (arcright)  (dev)
* | 322ab63 commit d (arcright) 
|/  
* 18725fd commit b (arcright) 
* 2a914fc commit a (arcright) 

こんな感じ。

マージコミットを発生させないようにコミットCの時点でrebaseを行なってからFast-Forwordマージをします。
一応手順

$ git reset --hard ORIG_HEAD # マージをするまえに戻す
$ git rebase master dev
$ git co master
$ git merge dev

こうなります。(わかりやすいようにコミットコメントをcからc'に変更しました)

* 99de6e9 commit c' (arcright)  (HEAD, master)
* 322ab63 commit d (arcright) 
* 18725fd commit b (arcright) 
* 2a914fc commit a (arcright) 
git-rebaseで困ったときに

rebaseを行った際にもコンクリフトが発生する可能性があります。コンクリフトの修正方法はmergeした時と同じようにエディタ等で採用する変更を残してgit-addをします。
このあとgit-mergeした時にはgit-commitをするのですがrebaseのばあいは違います

$ git rebase --continue

rebase作業中にコンクリフトをすると(no branch)に移動され解決を待ちます。解決後にはコミット反映が残っているのでcontinueして反映してやります。


コンクリフト大量に発生して、あーrebaseするんじゃなかったって時にはgit rebase --abortをするといいです。rebaseする前に戻れます

$ git rebase --abort
git-reflog最強説
$ git reflog

どうしてもわけわからなくなった時git-reflogは頼りになります。すべてのHEADが移動した履歴が残っているので戻りたいものを探して

$ git reset --hard HEAD@{13}

上記の例だとcommit aした時点に戻っているのですが下記reflogを参照してください。commit c'したHEADも当然のこっています。

2a914fc HEAD@{0}: HEAD@{13}: updating HEAD
99de6e9 HEAD@{1}: commit (amend): commit c'
0c444c0 HEAD@{2}: merge dev: Fast-forward
322ab63 HEAD@{3}: checkout: moving from dev to master
0c444c0 HEAD@{4}: rebase: commit c
322ab63 HEAD@{5}: checkout: moving from dev to 322ab63fa49fb29a6b536e82def1784b4d271a22^0
43dad78 HEAD@{6}: checkout: moving from master to dev
322ab63 HEAD@{7}: ORIG_HEAD: updating HEAD
dc89d6e HEAD@{8}: merge dev: Merge made by recursive.
322ab63 HEAD@{9}: commit: commit d
18725fd HEAD@{10}: checkout: moving from dev to master
43dad78 HEAD@{11}: commit: commit c
18725fd HEAD@{12}: checkout: moving from master to dev
18725fd HEAD@{13}: commit: commit b
2a914fc HEAD@{14}: commit (initial): commit a

つまり過去にも未来にもいけてやりたい放題ですね。やったあ