Gitの内部データ構造をGraphvizで描画してみた 第2回 ブランチとマージ


解決すべき問題

一年前のこと、Subversionを長く使ってきた開発チームにGitを教えようとした。わたしはGitのブランチをちゃんと説明したかった。というのもSubversionにもブランチという用語があるが、GitのブランチはSubversionとは似て非なるものだからだ。"Git"と"Subversion"と"ブランチ"をキーにネットを検索すると解説記事がたくさんみつかった。だがわたしが同僚諸君に説明するのに使えるようなひと目で理解できる記事が見当たらなかった。

いま自分の手元にあるプロジェクトの.gitディレクトリのなかにあるGitレポジトリの実物を読み出して図にしてくれる、そういうツールがほしい。

解決方法

Pythonでツール kazurayam/visualize_git_repository.py を開発した。これを使えばいま自分の手元にあるプロジェクトの .git ディレクトリのなかにあるオブジェクト群の実物を読み出し、Graphvizでグラフを生成してPNG画像ファイルを出力することができる。

説明

デモ用にディレクトリを作りGitレポジトリを作ろう。masterブランチとdevelopブランチでファイルを追加・変更などして、developをmasterにマージしよう。Gitレポジトリのなかがどんなふうに形を変化させていくか、図を示しつつ、説明しよう。

ステップ1 最初のコミット

新しいGitレポジトリを作ろう。いくつかファイルを作ってコミットしよう。

  • 作業用ディレクトリを作った

  • git initした

  • ファイルを3つ作った。README.md.gitignoresrc/greeting.plを。

  • git add .した

  • git commit -m "initial commit'した

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
まだ無い

このグラフから次のことが読みとれる。

  1. git initするとmasterという参照名が自動的に作られる。

  2. Gitにおいてブランチとはひとつのcommitオブジェクトを指す別名にすぎない。上記の図で参照名 masterinitial commit というコメントを含むひとつのcommitオブジェクトを指す別名にすぎない。

  3. Gitには HEAD という名前の参照名が組み込みで定義されている。HEADもやはりひとつのcommitオブジェクトを指す。上記の図でHEADは参照名masterを指しているが、masterがcommitオブジェクトを指すので、けっきょくHEADmasterを介して間接的にひとつのcommitオブジェクトを指すと解釈することができる。

  4. HEADは特殊な参照名だ。さまざまのgit xxxコマンドを実行した副作用としてHEADが指すモノが切り替わる。

  5. 参照名HEADが参照名masterを指しているという状態を「カレント・ブランチがmasterである」と表現することもある。この言い方のほうが日本語としてすなおなので多用するだろう。以下で「カレント・ブランチが...」という文がでてきたら、参照名HEADから伸びた矢印 → が何を指しているかを思い浮かべてほしい。

ステップ2 developブランチを作る

新しいブランチ develop を追加しよう。

  • カレント・ブランチがmasterである状態で git branch developコマンドを実行した

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
前と同じ

このグラフから次のことが読みとれる。

  1. 新しい参照名 develop が作られた。

  2. 参照名 developinitial commitというコメントを含む
    commitオブジェクト すなわち master が指していたのと同じcommitオブジェクトを指している。

  3. 参照名HEADが参照名developを指すかたちに変更された。別の言い方をすると カレント・ブランチがmasterからdevelopに切り替えられた。

  4. カレント・ブランチがdevelopに切り替えられた後でも参照名masterはGitレポジトリのなかに存続している。上の図ではmasterの図を省略したがそれは図をシンプルにして見やすくするためであって、消えてなくなったわけではない。

ステップ3 developでコミットする

developブランチで新しいファイルを追加してコミットしよう。

  • ワーキングツリーで新しいファイル doc/TODO.txt を追加した

  • カレント・ブランチがdevelopであることをたしかめて

  • git add .を実行した

  • git commit -m "add doc/TXT.txt"を実行した

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
前と同じ

このグラフから次のことが読みとれる。

  1. 新しいcommitオブジェクトができた。新しいcommitオブジェクトが親commitにリンクする形になり、commitオブジェクトの鎖がひとつ伸びた。新しいcommitオブジェクトはルートディレクトリ / を指すtreeオブジェクトにリンクしている。treeのなかには doc/TODO.txt ファイルのblobオブジェクトがある。つまり追加したファイルがちゃんと認識されている。

  2. 参照名developすなわちdevelopブランチがどのcommitオブジェクトを指しているか?を見ると自動的に更新されたのがわかる。ブランチ名はcommitオブジェクトの鎖の端っこ(ゴーヤの蔓の成長点のようなところ)を指すようにgitによって自動的に更新されるのだ。

ステップ4 masterブランチに戻る

ファイル README.md を修正してコミットする前に、masterブランチに戻ろう。

  • git checkout master を実行した

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
前と同じ

このグラフから次のことが読みとれる。

  1. 参照名 HEAD が参照名 master を指すように更新された。すなわちカレント・ブランチがmasterに切り替わった。

  2. masterブランチの中身は1回目のcommitをした時のままで変わっていない。

  3. カレント・ブランチがmasterに切り替えられた後でも参照名developはGitレポジトリのなかに存続している。上の図ではdevelopの図を省略したがそれは図をシンプルにして見やすくするためであって、消えてなくなったわけではない。

ステップ5 masterでコミットする

masterブランチで既存のREADME.mdファイルを変更してコミットしよう。

  • git branch --show-currentとやってカレント・ブランチがmasterであることを確かめた

  • echo "#Read me carefully" > README.mdとやってファイルを更新した

  • git add .した

  • git commit -m "modified README.md"とやった

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
前と同じ

このグラフから次のことが読みとれる。

  • 新しいcommitオブジェクトができた。新しいcommitオブジェクトが指すtreeのなかにあるREADME.mdファイルのblobをみるとhash値が変わっている。つまり変更されたREADME.mdファイルのために新しいblobオブジェクトがひとつ生成されて、そのhash値をcommitが参照している。これこそが「変更後のREADME.mdファイルがレポジトリにコミットされた」という表現の意味するところだ。
  • 参照名 master が新しいcommitオブジェクトを指すように更新された。

ステップ6 developをmasterにマージする

developブランチをmasterブランチにマージしよう。すなわちdevelopブランチで行ったファイルの追加・変更・削除をmasterブランチにとり込もう。

  • git branch --show-currentとやってカレント・ブランチがmasterであることを確かめた

  • git merge developとやった

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

master develop
前と同じ

このグラフから次のことが読みとれる。

  1. マージする前にmasterブランチにはcommitオブジェクトが2個あった。マージした後にmasterブランチにcommitオブジェクトが4個ある。2個増えた。

  2. 増えた2個のcommitオブジェクトうちひとつはdevelopブランチで作られたものだ。add doc/TODO.txtというコミットメッセージをもっている。developブランチだけで見えていたcommitオブジェクトがgit merge developコマンドによってmasterブランチでも見えるようになった。

  3. 増えた2個のcommitオブジェクトのうち残るひとつは新しくmasterブランチに追加されたものだ。このcommitオブジェクトは親(parent)コミットを2つ持っている点が特徴的だ。これをマージコミットともいう。

  4. マージコミットの親コミット2つとはカレント・ブランチの先端にあったcommitオブジェクトと、マージされたブランチの先端にあったcommitオブジェクトである。マージコミットによってコミットの履歴2本が合流する。マージコミットより古い履歴は2本のまま残されて、マージコミットからあとは1本の履歴が始まる。

  5. 参照名 master はマージコミットを指すように自動的に更新された。

  6. マージコミットは普通のcommitオブジェクトと同様にルートディレクトリ / に対応するtreeオブジェクトへの参照をもっている。そのtreeオブジェクトを起点としてすべてのファイルのblobオブジェクトを参照することができる。それらblobはmasterブランチとdevelopブランチにおいてこれまでに追加・変更・削除されたファイルの最新状態が反映されている。

  7. 上の図ではマージコミットが指しているtreeオブジェクトだけを描き、時系列的に古いcommitオブジェクトが指しているtreeオブジェクトを省略した。これは図を簡素化するためだ。treeオブジェクトがGitレポジトリから消えて無くなったわけではない。

ツールについて

本稿で示したPNG画像は自作のツール visualize_git_repository で描画した。このツールはPython言語で開発した。ソースコードは下記のGitHubレポジトリにある。

このツールは下記2つのライブラリを利用している。

PNG画像を生成するにはコマンドラインで下記の操作をする。

$ cd $visualize_git_repository
$ pytest -s kazurayam/visualize_git_repository.py::test_2_branch_and_mearge

上記の例を作るのにどういうgitコマンドを実行したのかを知りたいならプログラムのソースコードを読み解いてください。下記を入り口として解読してください。

まとめ

一年前の同僚諸君に本稿を読んでもらいたいなあ。これならGitのブランチマージが理解できるだろうと思うのだが、どうだろうか?

  • author: kazurayam
  • date: June, 2021

連作の目次