Chienomi

Git/Mercurial/分散バージョン管理システムの基礎

Gitに関する話をするとき、「ん???」となることがまぁまぁある。

で、多くの場合よく考えれば「GitHubの概念に引きずられている」ものが多いように感じる。

今回は、Git、そしてMercurialを含めて分散バージョン管理システムに関する概念と用語を、簡潔・明瞭に説明したいと思う。 なお、Bazzrその他に関しては私は使ったことがないので、分散バージョン管理システムの説明といいながら、GitとMercurialだけで進めさせていただきたい。

概念に関するもの

リポジトリ

恐らく、用語としてはこれが最も難しい。

「リポジトリとは歴史である」などといったりするのだが、どうも各々の定義にぶれがある。

であるから、Gitの場合は、.gitディレクトリ、あるいは*.gitディレクトリ(ベアリポジトリ)のことを指していると思えば良い。 また、.gitがあるディレクトリは「ワーキングツリー」である。

Mercurialの場合は.hgディレクトリを指す。

これは単にファイルであるだけではなく、ファイルの変更などを管理するための情報をもち、実際に管理することができる。

ローカルリポジトリ

ローカルリポジトリは、ワーキングツリーから見て、そのワーキングツリーが所属するリポジトリ(つまりは、ワーキングツリー先頭の.gitあるいは.hg)を指す。

ローカルリポジトリという語が出てくるのはリモートリポジトリに対する対比である。 なぜならば、リポジトリの操作はローカルリポジトリ(ワーキングツリー)上で行うため、「手元側」を意味することになるからだ。

リモートリポジトリ

リモートリポジトリは、ワーキングツリー、あるいはリポジトリから見て、自身ではないリポジトリを指す。

リモートリポジトリは一般的にはローカルリポジトリに対して何らかの関係性を持つ。ただし、持たない場合もある。 何らかの関係性とは、ローカルリポジトリがリモートリポジトリのことを登録しているか、リモートリポジトリがローカルリポジトリのことを登録しているかを指す。

「リモート」といっても、あくまでも「このリポジトリの外」の意味であり、ネットワーク越しであることを意味するわけではない。 むしろ、最も基本的なGitやMercurialの運用においてはリモートリポジトリはファイルシステム上にあるほうが普通であり、ネットワークにおけるリモートを指してはいない。

また、場合によってはそもそもローカルリポジトリ上で登録されたリポジトリのことだけを指してリモートリポジトリと呼ぶ場合もある。

コミット

リポジトリによって管理されるファイルのある状態の記録である。 GitやMercurialの変更は連続的に記録されるわけではなく、コミットした瞬間ごとが記録される。

コミットは本質的にリポジトリへの書き込みである。 このことから、最終的な修正が反映される権威リポジトリが存在する場合、その権威リポジトリに対するpush、あるいは権威リポジトリ上でのコミットを「コミット」と呼ぶ場合がある。 この場合、「そのリポジトリを更新する行為」を指すのであり、その行為をしうる者を「コミッタ」と呼ぶ。

ステージング

Mercurialにはない、「コミット候補」。

基本的にはGitの場合、ステージされたものはステージされた状態で保たれ、コミットされる。 ステージされてからコミットされるまでに変更は加えられないので、「コミットする前に考える」段階があると考えて良い。

実際のところ、ほとんどの場合ステージングは省略されている。

HEADはGit独自の概念である。

HEADは コミットを指している訳ではない 。 HEADはあくまで位置である。

リポジトリがリモートリポジトリと同期される関係にある場合、リモートリポジトリと同期した位置というものが記録されている。 HEADは全体で一番最後にコミットされた位置である。

コミットを取り込む場合、取り込む側(つまり、それによって変更される側)のほうがHEADの位置が前にあってはいけない。

フォーク

フォークは分散バージョン管理システムにおける用語ではない。

フォークは(由来は置いておくとして)ソフトウェアを複製し、複製元とは異なる未来を歩むことを意味する。

分散バージョン管理システムにおいては、行為としてはcloneすることがまさにforkすることを指す。 ただし、cloneした後に異なる未来を歩み、それを元のリポジトリに反映する場合や、恒常的に元のリポジトリの変更を反映する場合はforkしたとは言えない。

先割れしたフォークの先端が交わることはない。forkは決別なのである。

ブランチ

ブランチの概念はソフトウェアによって随分違う。

Gitの場合はあくまで歴史の分岐である。 ブランチを作ることでブランチ作成の起点になるコミットから、他のブランチに影響されることなくコミットを作っていける。

Mercurialの場合は、ブランチは位置情報になっている。 枝分かれしているというよりは、同じように時間が流れる平行世界みたいな状態である。

両者の大きな違いとして、Gitはブランチを作ったらそのまま完全に違う未来を歩んでもいいので、最初のブランチであるmasterブランチにそこまで特別な意味がない。 対してMercurialの場合は一種のコミットのような扱いになり、ブランチは最終的には取り込まれるか、クローズして捨てられるかすることを想定している。 だから、Mercurialの場合はdefaultブランチが本命である。

なお、Gitのmasterブランチは基本的に進んだHEADを持っているので、masterブランチをリリースブランチにするのはちょっとまずい。 リリースブランチは別に切るべきだ。 対して、Mercurialは一番進んだコミットを持つdefaultに合流するようになっており、あんまりリリースのことは考えてない感じになっている。

また、ブランチの大きな違いとして、Gitはブランチは削除できるが、Mercurialは閉鎖できるだけで削除はできない。 どうしても削除したい場合は方法がなくもないが、それはそれでMercurialでは本来禁止されている歴史操作を使ってそのブランチの世界線にあるコミットを全て消滅させるというすごいことをすることになる。

さらにもうひとつ大きな違いとして、Gitの場合ブランチは個々のリポジトリに属している。明示して送りつけない限りはpushあるいはpullするのはブランチ単位である。 対してMercurialは全てのブランチが共有される。だから、Mercurialでのpushあるいはpullするのはリポジトリ全体である。

競合 (conflict)

バージョン管理システムにおいて最も重要なのは、「同じファイルを同時に変更することに対して保護する」である。 古代のバージョン管理システムであるRCSでは、「変更可能な状態で持ち出せるのは1ユーザーだけ」という方法で管理していた。

GitもMercurialも、基本的には同一ファイルに対する変更を競合とみなす。

ただし、Gitの場合は変更点が重複していなければ競合にはならない。 Mercurialの場合は変更点が重複していなくても同一ファイルに対して変更していれば競合になる。

ただし、Mercurialの競合はそもそも歴史が割り込まれた時点で発生するため、こっちも向こうもそれぞれにコミットしたんだよね、という状態になったら確実に競合が発生する。 この変更が統合可能なのであれば、mergingという扱いにはなるものの、実際にはmergeは必要なく、単に「歴史を統合したコミット」を作れば良いようになっている。

ここらへんはGitのほうがきっちりしていて、Mercurialの場合はそれぞれが無軌道に変更を加えているとえらいことになる。 Gitではそもそもpush可能なのはベアリポジトリだけなのに対し、Mercurialではベアリポジトリという概念がなく、リポジトリは須らくワーキングツリーを持っているという考え方になっている。 でも、複数人で作業するような場合はワーキングツリーに対する変更を加えない、つまり自分でコミットを作成しないリポジトリを作ってそこにpushするようにしておかないと混乱を招くことになる。

pull request

pull request (通称プルリク)は、GitでもMercurialでもなく、 GitHubの機能である。 ちなみに、GitLabでは “Merge Request”という名前で同種の機能がある。

リポジトリに対してpushするためには、当該リポジトリに対する書き込み権限が必要である。 読み取り権限があればcloneできるため、cloneされるリポジトリは所有者が異なる可能性があり、元のリポジトリに対する書き込み権限がないことも少なくはない。

もちろん、書き込み権限があるのであれば当該リポジトリに対してpushすれば良いのだが、ない場合は当該リポジトリの書き込み権限を持つ者にpullしてもらうことになる。 しかし、その場合「pullしてほしい」と伝えなくてはならない。これを、「pullして欲しいと伝えて、変更点を明確にして、ついでにボタン一発でpullできるようにしたもの」がPull Requestである。

これに関しては誤解が深く、GitHubでGitを触り始めた人がだいたい混乱している。

アクション

init

リポジトリを作成すること、だが、どちらかといえば「今いるこの場所をリポジトリにする」のほうが実態を指している。

ただし、Gitにおいてはgit init --bareがあるためそうとも限らない。 この場合はベアリポジトリを単純に作成する。

このアクションはローカルリポジトリが存在しない状態で行う。

clone

リモートリポジトリの複製を作成する。

このアクションはローカルリポジトリが存在しない状態で行う。

push

ローカルリポジトリのコミットをリモートリポジトリに書き込む。 リモートリポジトリの書き込み権限が必要である。

pull

リモートリポジトリのコミットをローカルリポジトリに書き込む。 リモートリポジトリの読み取り権限が必要である。

add

基本的にはワーキングツリー以下のファイルをリポジトリの管理下に加えるアクション。

Gitの場合はステージングの際にも使用する。

Mercurialの場合、ワーキングツリー以下で明に除外されていないのに管理外にファイルがあることは望ましい状態ではないと考えるため、addの手順はまぁまぁ省略される。 Gitでは省略はできない。

merge

Gitにおいては異なるブランチを取り込むこと。

Mercurialにおいては、割り込みの発生した歴史を一本にまとめたコミットを作ること。

reset / rollback

resetはGitにおけるアクションで、ステージされたファイル、あるいは最新のコミットを取り消す。

Mercurialでは最新のコミットを取り消すrollbackがあり、Mercurialではコミットの歴史を操作するアクションはこれが唯一。 ステージして慎重にコミットするGitと違い、Mercurialは一発でコミットをキメてしまうため、rollbackは結構よく使うし、実際に簡単に使えるようになっている。

revert / backout

revertはGitとMercurialで全く意味が違う。

Gitにおいてはコミットを取り消す。この場合、そのコミットにおいて行われた変更そのものを元に戻す。 Mercurialは歴史を変更することはできないので、あるコミットで行われた変更を元の状態に戻す変更を加えたというコミットを作成する。それ用にbackoutというアクションがある。

Mercurialのrevertはワーキングツリーのファイルをコミットの状態に戻すことを指す。 これはGitであればgit checkout <commit> <file>に相当する操作である。 Gitのcheckoutはこれとは全く異なる「ブランチの切り替え」という機能も兼ねており、少々わかりにくい。

Gitには他にも歴史操作に関するアクションがあり、特にrebaseはまさに歴史修正主義者のためのコマンドである。

あんまり知られていないが、Gitにはblameという大変便利な歴史チェックコマンドがあったりする。 そして、実はMercurialにも似た感じのことができるannotateというコマンドがあり、hg annotate --user --numberとやればblame相当になる。

「revertするぞ」と言われたら、「お前のコミットは問題があるからなかったことにする」という意味になる。 例え実際にはMercurialを使っている場合でも「backoutするぞ」じゃなく「revertするぞ」と言う場合が多い。

diff

何か(コミット, タグ, ブックマーク, e.t.c.)の間でファイルの変更を比較するアクションである。

実はGitのdiffはGitリポジトリ外でも使うことができる。

diff -uと同じだろ?何が嬉しいんだよ」と思うかもしれないが、実はGit diffはインラインで変更を表示することができるのだ。 これがすごく便利。

stash

ワーキングツリーに対する変更を保留にするGitのアクション。Mercurialには全く存在しない。 ほとんどの場合、「作業すべきブランチを間違えた」という場合に別のブランチに変更を持っていくために行う。

すごく便利である。 そもそも、ブランチに関する操作はMercurialよりGitのほうがずっとやりやすい。

Wrote on: 2019-08-18T00:00:00+09:00