今どきこんな記事の需要があるのかわからないけど、まだ「プログラマだけどsvn以前の版管理しか使ったことがない、gitに興味ある」という人もきっと残っているので、プログラマ視点で理解しやすいgitの考え方の紹介を。
svnの履歴は配列、gitはリスト
データを並べた構造の表現の仕方として配列とリストってのまず習いますよね。
連続したメモリ空間にただ並べるのが配列、各エントリごとにメモリを確保して「隣のノード」へのポインタを各ノードが持つようにするのがリスト。
svnとgitの最大の違いはこれです。svnは履歴を配列として持つから、履歴にリビジョン番号が付きます。gitは各リビジョンが「前のリビジョン」へのポインタを持ったノードなので、番号が付きません。代わりにリビジョンの内容のハッシュ値をIDとして持ちます。
svnの(仮想的な)ブランチは配列、gitのブランチはポインタ
ブランチのことは、svn,gitそれぞれで理解しにくさがあります。
svnには仕組みとしてブランチという概念が存在せず、運用で仮想的にブランチが作れたかのように扱うのが一般的。
で、その仮想的なブランチというのはどういうものを想定しているかというと、分岐したところから新しい配列が生えてきたというもの。配列の途中から新しい配列が生えてくる、まさにブランチ(枝)です。運用解決ではなく仕組みとしてそうなっていればもっと良かった。
gitのブランチは、枝を意味しません。むしろ葉を意味します。前項の通りgitの履歴構造はリスト構造(ただし線形ではありません、分岐、合流があります。正確な情報科学用語としては、「DAG」。合流があるから「ツリー」ではない)なのですが、そのノードのどれかを指すポインタのことをブランチまたはタグと呼んでいます。動かないポインタがタグ。動くポインタがブランチ。
ブランチが動くポインタだ、というのは、そのブランチ名義で新しいリビジョンをコミットすることでその新しいリビジョンを指してくれるということ。
過去の履歴を改変できるできないとは
svnが過去のリビジョンの内容を書き換えたり最新のリビジョンを取り消したりできないのは、リビジョン番号の意味するリビジョン内容が変わってしまうと多分クライアント動作に不整合を起こしてしまうから。リビジョン番号を振った上にハッシュ値をプロパティとして持つなどすれば不整合は回避しえたんだろうけど、そこまでするかって話。よってsvnでは過去の履歴は修正できません。
gitの場合も、内容が違えばハッシュ値も変わるのでそういう意味での書き換え(リビジョンIDを保ったままの書き換え)は無理です。
が、なにしろ配列でなく分岐・合流ありのリストなので、新しい履歴を生やしてからブランチというポインタをそっちに貼り替えてしまえば履歴の見た目はすぐに書き換えられます。ブランチまたはタグからたどれないノードは「ないもの」と扱われるので、元の履歴は消えます(一定期間後にGCされるまではポインタを貼り直すことでサルベージ可能)。
分散型であるとは
gitが複数マシンにリポジトリを分散すると言っても、基本的にそれぞれのリポジトリは同じ履歴構造を保持しているのが前提となる。
ただし複数人いる開発者がそれぞれ自分のマシンでの改変内容を自分のマシンのリポジトリにコミットするので、そこだけ違いが生じる。違いというのは、自分のところにだけあるリビジョンノードがあると言うこと。ノードを他のマシンに渡す操作が「プッシュ」、他のマシンから引き出す操作が「フェッチ」。
履歴構造はリスト、ブランチ・タグはポインタ、というgitの履歴構造はノード単位で受け渡しすることを可能にして分散化を実現している。ただしこのままでは開発者の数だけどんどん履歴が分流していくばかりでどれが本流だって話になってしまう。そこで、合流させる「マージ」がある。
ただ、また用語的にややこしいことに、マージはマージでも合流させないマージ操作がある。合流するまでもなく「何歩か進めば追いつく」ことができる場合、合流ノードを作るのでなく単にブランチというポインタを進めるだけで済んでしまうわけだけど、そういう操作も「マージ」の一種だということになっている。本来の合流操作と区別したいときは「fast-forward(早送り)マージ」と呼ぶ。
普通はmasterという名前のブランチを本流扱いし、チーム内の責任者が各ブランチを内容確認してmasterにマージしていく。でもこういうチーム内ワークフローの話をし出すと本が一冊書けるのでそれ以上は触れない。
でもワークフロー上これだけは基本ルール
書かないと言ったけどこれだけはというルール。
分散型だから各人のマシン上で同じ名前のブランチが別のノードを指している場合が当然ありえるんだけど、 「同名のブランチが分岐している状態にすべからず」 、これはまず普遍的なルール。
同名のブランチがマシンによって別のノードを指しているとしても、単に遅れているか進んでいるかだけの違い、前項で説明した「fast-forward」で追いつけるだけの違いに留めないといけないというもの。ブランチが「枝」であることを保証する最低限の取り決めです。
必然的に、「masterにコミットするな」となります。各人がローカルでmasterにコミットするとその時点で分岐が生じますから。
コマンドラインは難しい
gitの難しさは、概念を理解するときの壁と、コマンドラインオプションが複雑だというのの2段階があります。
概念の理解はこの記事の内容で十分。十分ですとも。
コマンドラインオプションはねえ⋯ あれはgitコマンドの仕様が悪い。GUI環境で開発してるなら無理してコマンドライン覚えずにGUIツールでいいんじゃないですかね。
SourceTreeが人気だけど、私としておすすめするのはそれよりもSmartGitHg。上記のgitの概念をより自然にGUI化していて、より脳になじみます。ドイツ製で、日本語化(というか英語以外の他言語対応)をする予定がまったくないという硬派ですけど。
まとめ
- DAGだ
- 「ブランチ」は、名前に反して生えている枝のことではなく葉を指すポインタである
- 「マージ」は、名前に反して何もマージすることなく単に前へ進むだけの操作も含んでいる
- masterにコミットするな
- SmartGitHgはいいぞ
- 逆に言うと、非プログラマにはgitの概念を理解するところの壁が厚いんだよな