- SCMBC #2 Mercurial 入門セッション資料(のたたき台)です。
- TODO たとえ話募集中
TODO 基調講演がここらへんのネタなのでおまけとして
TODO Subversionその他集中型との差異について
TODO 思いの丈をぶつけます
全ての変更点、履歴をまとめて格納するデータベースの事です。 Subversionなどの集中型のバージョン管理システムでは基本的にはリポジトリはリモートに一つだけ存在します。 作業を行う場合、リポジトリをチェックアウトして最新のコピーを取得し作業を行います。 分散バージョン管理システム(Bazaar、Git、Mercuiralなど)では、チェックアウトして開発を行うのではなく、 リポジトリ自体のコピーを作成して開発を行います。
Subversionでは最新のコピーを取得しているだけです。 履歴を閲覧する場合はリモートのリポジトリから履歴を得ます。コミットする場合はリモートのリポジトリにコミットを行います。
リポジトリ 作業領域 |----------| チェックアウト |--------| | | ----------------> | | | | | | | | | | |----------| |--------| リポジトリ 作業領域 |----------| |--------| | | | xxx | なにか作業する | | | | | | | | |----------| |--------| リポジトリ 作業領域 |----------| 履歴下さい |--------| svn log | | <---------------- | xxx | <---------- | | | | | | 履歴 | | 履歴 |----------| ----------------> |--------| ----------> リポジトリ 作業領域 |----------| svn commit |--------| svn commit | xxx | <---------------- | xxx | <---------- | | | | | | 結果 | | 結果 |----------| ----------------> |--------| -----------> 図 svn 時代
Mercurialではリポジトリ自体のコピーを作成しています。 履歴を閲覧する場合は手元のリポジトリの履歴を、コミットする場合は手元のリポジトリにコミットを行います。
リポジトリ リポジトリ(兼作業領域) |----------| クローン |--------| | | ----------------> | | | | | | | | | | |----------| |--------| リポジトリ リポジトリ(兼作業領域) |----------| |--------| | | | xxx | なにか作業する | | | | | | | | |----------| |--------| リポジトリ リポジトリ(兼作業領域) |----------| |--------| hg log | | | xxx | <--------- | | | | | | | | 履歴 |----------| |--------| ---------> リポジトリ リポジトリ(兼作業領域) |----------| |--------| hg commit | xxx | | xxx | <------------ | | | | | | | | 結果 |----------| |--------| ------------> 図 分散時代
リポジトリ間の変更点の同期にはhg pushを利用します。
リポジトリ リポジトリ(兼作業領域) |----------| 変更点 |--------| hg push | xxx | <---------------- | xxx | <---------- | | | | | | 結果 | | 結果 |----------| ----------------> |--------| ----------> 図 分散時代のリポジトリ間の同期
TODO 概要っぽい図じゃない。。。
TODO 複数人での作業をどうする、、
DVCSではリポジトリは複製(クローン、clone)して利用する事が前提となっています。 実際にプログラミングなどの作業を行うために複製したリポジトリを作業領域と呼びます。
具体的にはリポジトリの.hgディレクトリ以外を作業領域として利用します。 これは、Mercurialでは履歴などのメタデータを.hgディレクトリに格納するためです。
作業領域でファイルを編集し、コミットすると、複製元には存在しない新しいリビジョンを作成する事が出来ます。
一度のコミットに含まれる変更点のまとまりの事です。チェンジセットやコミットとも呼ばれます。
具体的に、リビジョンには次の情報が記録されています。
- 作成したしたユーザ名
- 作成した日付
- ファイルに関する情報(変更、追加、削除など)
- チェンジセットに対するコメント
- その他情報(ブランチ名などを含む)
次の情報は直接リビジョンの情報として管理するのではなく、コミットしたファイルから取得します。
- タグ
Mercurialでは次の方法でリビジョンを一意に指定出来ます。
- ハッシュ値 リビジョン固有の40桁の16進数
- リビジョン番号 一つのリポジトリ内の連番
リビジョンの指定にタグ名やブランチ名を利用した場合は、次のルールで該当するリビジョンを決定します。
- タグ名(タグ名が付けられたリビジョン)
- ブランチ名(ブランチ名が付けられたリビジョンのうち、最新のもの)
一つのリビジョンではなく、リビジョンの集合を指定したい場合は、集合の条件を簡単に記述する為のDSLが用意されています。
- DSLの例
# "指定したブランチの先祖を新しい順で、ただしマージコミットは除く"リビジョンのログを出力 hg log -r "reverse(ancestors('BRANCH_NAME')) and not merge()"
コミットを積み重ねるとリポジトリの中では次の様なグラフが作成されます。 「o」一つ一つがリビジョンです。コミットすると出来ます。「@」は今の作業領域のリビジョンです。
TODO 図 リビジョングラフ
リビジョングラフの中で子リビジョンを持たないリビジョンの事をヘッド(HEAD)と呼びます。 次のグラフではリビジョン2がヘッドです。
@ 2:default(ヘッド) | | o 1:default | | o 0:default 図 ヘッド
例外として、親リビジョンと子リビジョンのブランチ名が異なる場合は、親リビジョンはヘッド扱いされます。 次のグラフでは親リビジョン1と子リビジョン2のブランチ名が異なるので、リビジョン1はヘッドです。
@ 2:default(ヘッド) |\ | | | o 1:feature/hogehoge(ヘッド) |/ | o 0:default 図 ブランチが異なる場合のヘッド
Mercurialではコミットすると必ずリビジョンが出来ます。 では、次の様に親リビジョンに戻った後に新しいリビジョンを作ったらどうなるでしょうか。
o 1:default | | @ 0:default 図 親リビジョンに戻った状態でコミットするとどうなる?
Mercurialではコミットすると必ずリビジョンが出来ます。大事なので2回言いました。 答えはつぎの様なグラフになります。
@ 2:default(ヘッド) | | | o 1:default(ヘッド) |/ | o 0:default 図 マルチプルヘッド
このグラフでは、defaultという同一ブランチ内に二つ以上のヘッドが存在します。 このような状態の事を(狭義の)マルチプルヘッドと呼びます。
毎回(狭義の)マルチプルヘッドと呼ぶのは手間なのでこれ以降は単にマルチプルヘッドと呼びます。
Note
(広義の)マルチプルヘッド
再度、ヘッドの項で例外として紹介したブランチの存在するグラフを見てみましょう。
@ 2:default(ヘッド) |\ | | | o 1:feature/hogehoge(ヘッド) |/ | o 0:default 図 ブランチが異なる場合のヘッド
このグラフでは、同一ブランチ内に二つ以上のヘッドは存在しません。 しかし、リポジトリ全体としてはリビジョン1とリビジョン2の二つのヘッドが存在します。 このように、リポジトリ全体でヘッドが二つ以上存在する状態の事を(広義の)マルチプルヘッドと呼びます。
このことから、ブランチを1つでも作成するとそれ以降、そのリポジトリは(広義の)マルチプルヘッドとなります。
先ほどのマルチプルヘッドの例では無理矢理マルチプルヘッドを作成していました。 これは一人で作業している場合はマルチプルヘッドが作成されにくい為です。
マルチプルヘッドは複数人で開発する場合に発生する問題でも有ります。
例として、次のリポジトリを2人(Alice, Bob)で作業した場合を考えます。
@ 1:f8148c10efce:default | README 更新 | o 0:ec60efd545e6:default initial import 中央リポジトリ 図 リポジトリ
まず作業するためにAliceとBobがリポジトリを複製(hg clone)します。
@ 1:f8148c10efce:default @ 1:f8148c10efce:default @ 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 クローン
Aliceが先に編集してコミットしました。
@ 2:501185e47b97:default | index.htmlにメニューを追加 | @ 1:f8148c10efce:default o 1:f8148c10efce:default @ 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 Aliceは手が早い
Bobも編集を加えてコミットしました。
@ 2:501185e47b97:default @ 2:bd6a69bda4c6:default | index.htmlにメニューを追加 | 画像ファイルサイズ圧縮 | | @ 1:f8148c10efce:default o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 Bobの作業は手がかかる
Aliceは作業が完了したのでみんなに見てもらうように中央リポジトリに変更を反映します。(hg push)
@ 2:501185e47b97:default @ 2:501185e47b97:default @ 2:bd6a69bda4c6:default | index.htmlにメニューを追加 | index.htmlにメニューを追加 | 画像ファイルサイズ圧縮 | | | o 1:f8148c10efce:default o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 Aliceの変更を中央リポジトリに反映
Bobも作業が完了したのでみんなに見てもらうように中央リポジトリに変更を反映します。(hg push) が、実はここで変更を反映する事が出来ません(演習で体験する事になると思います)。 理由は、中央リポジトリにマルチプルヘッドが作成されてしまうからです。
@ 2:501185e47b97:default @ 2:501185e47b97:default @ 2:bd6a69bda4c6:default | index.htmlにメニューを追加 | index.htmlにメニューを追加 | 画像ファイルサイズ圧縮 | | | o 1:f8148c10efce:default o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 この状態でBobの変更を中央リポジトリに反映する事は出来ない
Bobは中央リポジトリに自分の変更を反映する事は出来ませんが、中央リポジトリの変更を取り込むことが出来ます。(hg pull) 試して見ましょう。
o 3:501185e47b97:default | index.htmlにメニューを追加 | @ 2:501185e47b97:default @ 2:501185e47b97:default | @ 2:bd6a69bda4c6:default | index.htmlにメニューを追加 | index.htmlにメニューを追加 |/ 画像ファイルサイズ圧縮 | | | o 1:f8148c10efce:default o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 Bobリポジトリに中央リポジトリの変更を取り込む
マルチプルヘッドが出来てしまいました。
グラフを統合する作業をもう少し詳しく見てみます。
Bobのリポジトリと中央リポジトリの「リビジョン1:f8148c10efce」は共通で、Bobのリポジトリには「リビジョン2:501185e47b97」は存在しません。
「リビジョン2:501185e47b97」をBobのリポジトリに取り込む場合も「リビジョン1:f8148c10efce」の子として取り込まれる必要が有ります。 (子として取り込まないと、中央リポジトリと親子関係が異なってしまいます)
その結果、次の様なグラフが作成されます。
o 3:501185e47b97:default | index.htmlにメニューを追加 | @ 2:501185e47b97:default @ 2:bd6a69bda4c6:default | @ 2:bd6a69bda4c6:default | index.htmlにメニューを追加 | 画像ファイルサイズ圧縮 |/ 画像ファイルサイズ圧縮 | | | ---- | o 1:f8148c10efce:default --+-- o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | | README 更新 ---- | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Bobリポジトリ 新Bobリポジトリ 図 中央リポジトリ + Bobリポジトリ = 新Bobリポジトリ
マルチプルヘッドの様に、二つの分離してしまった履歴を統合する作業をマージと呼びます。
先ほどの新Bobリポジトリでマージを行うと次の様になります。
@ 3:501185e47b97:default | index.htmlにメニューを追加 | | @ 2:bd6a69bda4c6:default |/ 画像ファイルサイズ圧縮 | o 1:f8148c10efce:default | README 更新 | o 0:ec60efd545e6:default initial import 図 新Bobリポジトリでマージしてみる
「@」が増えました、これは現在の作業領域がリビジョン2:bd6a69bda4c6とリビジョン3:501185e47b97の内容を含んでいるという事を表しています。 この状態でコミットすると、マージ完了です。
@ 4:d7553b7b7e59:default |\ マージ | | o | 3:501185e47b97:default | | index.htmlにメニューを追加 | | | o 2:bd6a69bda4c6:default |/ 画像ファイルサイズ圧縮 | o 1:f8148c10efce:default | README 更新 | o 0:ec60efd545e6:default initial import 図 新Bobリポジトリでマージをコミット
この状態で初めて中央リポジトリに変更を反映できます。
@ 4:d7553b7b7e59:default @ 4:d7553b7b7e59:default |\ マージ |\ マージ | | | | | o 3:bd6a69bda4c6:default o | 3:501185e47b97:default | | 画像ファイルサイズ圧縮 | | index.htmlにメニューを追加 | | | | o | 2:501185e47b97:default @ 2:501185e47b97:default | o 2:bd6a69bda4c6:default |/ index.htmlにメニューを追加 | index.htmlにメニューを追加 |/ 画像ファイルサイズ圧縮 | | | o 1:f8148c10efce:default o 1:f8148c10efce:default o 1:f8148c10efce:default | README 更新 | README 更新 | README 更新 | | | o 0:ec60efd545e6:default o 0:ec60efd545e6:default o 0:ec60efd545e6:default initial import initial import initial import 中央リポジトリ Aliceリポジトリ Bobリポジトリ 図 Bobリポジトリのマージの成果を中央リポジトリの変更を取り込む
Mercurialは自動的に複数人の作業のマージを行わないので、Subversionから比べると一見不便に見えます。
しかしこれはとても安全な方法なのです。
AliceとBobの操作をSubversionで行った場合は次のようなグラフになるでしょう。 Subversionの場合は「Subversionが自動的にマージした変更」がそのままコミットされます。
o 3:bd6a69bda4c6:default <-- 実は「Subversionが自動的にマージした変更」 | 画像ファイルサイズ圧縮 | o 2:501185e47b97:default | index.htmlにメニューを追加 | o 1:f8148c10efce:default | README 更新 | o 0:ec60efd545e6:default initial import 中央リポジトリ 図 Subversionの場合の中央リポジトリ
これは次の点で不便です。
- 「Subversionが自動的にマージした変更」の動作、表示確認ができないまま中央リポジトリに反映されてしまう
- コンフリクト(変更内容の衝突)が発生した場合、作業領域に自分の編集と相手の編集(コンフリクトマーカーを含む)が混ざってしまう
- コミット時、更新時にコンフリクトが発生するという不安が常に付きまとう
Mercurialの場合を再度確認してみましょう。
@ 4:d7553b7b7e59:default |\ マージ | | o | 3:501185e47b97:default | | index.htmlにメニューを追加 | | | o 2:bd6a69bda4c6:default |/ 画像ファイルサイズ圧縮 | o 1:f8148c10efce:default | README 更新 | o 0:ec60efd545e6:default initial import Bobのリポジトリ 図 Mercurialの場合のBobリポジトリ(中央リポジトリに反映前)
Mercurialではマルチプルヘッドをマージしています。この方法を「コミット済み成果ベースのマージ」と呼びます。
「コミット済み成果ベースのマージ」にはSubversionの方法と比べ次の利点があります。
- マージ後、手元で十分確認(ユニットテストの実行、表示確認)してから、中央リポジトリに反映できる
- マージ対象はコミット済みのため、マージ時のコンフリクトの解決を何度でもやり直せる
- コミットしても変更を取り込んでもマルチプルヘッドができるだけでコンフリクトが発生しないため、心の平穏が保てる
これらのことからMercurialのマージはとても安全な方法といえます。
Mercurialではチェンジセットに付けられた属性の事です。
一連の作業のまとまりを区別する為にブランチを利用します。 作業の目的でブランチを切り(ブランチ名の変更のみを持つコミットをすること)作業を行います。 ブランチでの作業が完了したら、元のブランチにマージをしてブランチの変更を取り込みます。
TODO 利点、運用の変化について
大きく分けて次の二つの種類のブランチが存在する。
- 息の長いブランチ(例:XX日リリースの為の開発ブランチ)
- 短いブランチ(例:○○のバグ修正)
息の長いブランチ用にリポジトリを用意することもできる
分散バージョン管理システムをより効果的に使いこなすためのお作法として、一度のコミットに含める変更の粒度があります。
Subversionではコミットすると全員の開発者に影響するため、次のような運用を行っていることが多いと思います。
- コンパイルエラーやほかの開発者への影響が出ない程度に空気を読んでコミットする
- 新しい機能を作成する場合は全部できてからコミットする
- 1チケットの修正は1コミットにする
- あとで戻したい単位でコミットする
- 一度に一つの変更をコミットする
Mercurialに限らず分散バージョン管理システムのコミットは自分の作業領域のリポジトリにのみ反映されるので、1コミットに詰め込む必要はありません。 次の粒度でコミットし、任意のタイミングで中央リポジトリに反映させましょう。
- あとで戻したい単位でコミットする
- 一度に一つの変更をコミットする
最初は「あとで戻したい単位でコミットする」を意識するとよいと思います。慣れてきたところで「一度に一つの変更をコミットする」に移行しましょう。
「一度に一つの変更をコミットする」ためは具体的には次のようなことを意識しましょう。
- 機能追加とバグフィックスを同時に行わない
- 機能追加と直接関係ないインデント修正は行わない
- 機能追加とリファクタリングを同時に行わない
- 大きな機能追加を一度のコミットで行わない
- (「あとで戻したい単位でコミットする」と相反します)
前向きに考えると次になります。
- 機能追加だけのコミットをしましょう
- バグフィックスだけのコミットをしましょう
- インデント修正だけのコミットをしましょう
- リファクタリングだけのコミットをしましょう
- 大きい機能追加の場合はいくつかのステップに分割してコミットしましょう
- (「あとで戻したい単位でコミットする」と相反します)
また、このドキュメントのコミットの粒度も参考にしてください。
MercurialのWikiにそのものずばりの 優れたチェンジセットコメントの書き方 という記事が存在します。
具体的には次の点を気を付けてコメントを書きましょう。(Gitのお作法と同じです)
- 1行目に変更内容を簡潔に記述する
- 2行目は空行
- 3行目以降に変更内容の詳細を記述する
このお作法をベースにプロジェクト毎に追加ルールを設定すると効果的です。
- 1行目の先頭にチケット番号をいれる
- マージコミットのコメントの書式を固定化する
- ブランチのマージは「merge with BRANCH_NAME」にする
- マルチプルヘッドのマージは単に「merge」にする
英語のドキュメントですが、 Mercurialのコメントの作法 も存在します。
このドキュメントのコミットコメントは「1行目に変更内容を簡潔に記述する」しか守っていないのであまり参考にならないと思います。。。
Mercurialはプラグイン機構を持っているので、プラグイン作成の為のインターフェースが整っています。
- changectx
- filectx
内部の保存形式
- revlog
- changelog
- manifest
- filelog
localrepositoryを渡してchangectxになる。 cahngectxはlocalrepositoryのchangelogのラッパークラス。 主な情報はchangectx経由でchangelogから取得する リビジョンの変更ファイルはflectxから取得 チェンジセット=オブジェクトって話とか。 タグはチェンジセットとは別に保存されるとか。 ブックマークなどは変更管理されないとか。
TODO webサービスみたいな本番が一つのやつの場合の話です
TODO 実際にはまった点を書く
TODO 余力があったら書く
初心者の方がすぐに利用できるhgrcを用意しました。
[ui] # Mercurialが利用するユーザ名を設定してください。 #username = Your Name <[email protected]> [extensions] bookmarks = color = graphlog = mq = pager = progress = rebase = record = transplant = [alias] _status = status st = ! $HG _status $($HG root) $HG_ARGS # branch b = branch #bs = branches -a # resolve mark = resolve -m unmark = resolve -u conflicts = resolve -l uselocal = resolve --tool internal:local useother = resolve --tool internal:other # push nudge = push --rev . #push = ! echo -e "\033[31m(use 'hg nudge' to push changesets)\033[m" [pager] attend = annotate, cat, diff, export, glog, log, qdiff, help [diff] git = True
このhgrcは次の点を考慮しています。
- 普段の操作が便利になる拡張を有効にする
- nudgeやstなど便利な別名を追加する
- 詳しい人に助けを求める際に詳しい人が作業しやすいよう、歴史改変系の拡張を有効にする
この設定を足掛かりに自分にあったhgrcを模索していきましょう。
Mercurialのリポジトリは次の様な構成を取っています。 .hgディレクトリの直下は、Mercurial拡張が利用するデータ保存場所としても利用されています。 .hg/storeディレクトリが実際にリビジョンを格納しているディレクトリです。
主な物の解説を行います。
/ リポジトリルート(兼 作業領域となるディレクトリ) | |-- .hg/ メタデータ格納ディレクトリ | | | |-- store/ リビジョンの格納されているディレクトリ | | 壊したらやばい | | | |-- requires リポジトリフォーマットが書かれたファイル | | 破壊されるとstore以下が読み取れなくなる | | | |-- hgrc リポジトリ固有のMercurialの設定ファイル | | 主にクローン元URL、ユーザ名、認証情報、フック、などを設定する | | hg init で初期化した場合は作成されない為、手で作成する | | よく編集するファイル | | | |-- branch 次コミットする時のリビジョンのブランチ名の入ったファイル | | 通常は現在のブランチ名。hg brach BRANCH_NAME を行うとBRANCH_NAMEが設定される | | | |-- 00changelog.i 下位互換の為のダミーファイル | |-- bookmarks Mercurialの内部情報のファイルキャッシュ。消しても復活する | |-- bookmarks.current Mercurialの内部情報のファイルキャッシュ。消しても復活する | |-- dirstate Mercurialの内部情報のファイルキャッシュ。消しても復活する | | | |-- patches/ MQ(Mercurial Queue)拡張用ディレクトリ | | | series、statusという管理ファイル以外に、パッチファイルが保存される | | | | | |-- series MQのパッチファイルの適応順番が記録されているファイル | | |-- status MQのパッチファイルの適応状態が記録されているファイル | | | |-- strip-backup/ MQ拡張で追加されるstripコマンド用ディレクトリ | | stripで削除したリビジョンのバックアップファイル置き場 | | | |-- shelve/ Shelve拡張用ディレクトリ | | | |-- translpant/ Transplant拡張用ディレクトリ | |-- .hgignore Mercurialで管理しないファイルのルールを記述する。作成は任意 |-- .hgtags hg tag で付けたタグの情報を保存する。初回のhg tag実行時に作成される。作成は任意 | (注) .hgignoreと.hgtagsは作業領域の中に作成するファイルですが特別な意味を持ちます。 | |-- 作業領域 図 リポジトリの構成
- Mercurial Wiki
- 「入門Mercurial Linux/Windows対応」 (藤原克則 著/ 秀和システム 刊)
- 「Mercurial: The Definitive Guide」日本語版