hg and git

hg と git のコマンド相違点

似てるようで違う hg と git の違いのメモ。

基本
  • working directory : バージョン管理対象のファイルを置くディレクトリ。バージョン管理対象にしないオブジェクトファイル等を一緒に置いても良い。
  • repository : working directory の一番上にある、.hg (hg の場合) または .git (git の場合) ディレクトリの中身。バージョン管理に関する情報、履歴等が置かれる。
あるところにあるリポジトリを追いかけるだけの使い方

たとえば www.kernel.org の Linus のリポジトリを追いかけるとか、そんな使い方の場合。一番シンプルな例。

  • 最初の取得 (リポジトリを取得し作業ディレクトリに最新の内容を展開する)
    hg clone url [dir]
    git clone url [dir]
  • 最新リポジトリの取得 (A)
    hg pull
    git fetch
  • 作業ディレクトリをリポジトリの最新内容に更新 (B)
    hg update (up, checkout, co 等でも良い)
    git merge origin/master
  • 上の (A) と (B) を一回で行う
    hg pull -u
    git pull
一人で履歴を取るだけのシンプルな使い方
  • リポジトリの新規作成
    hg init
    git init
  • ファイル追加予約
    hg add filename
    git add filename
  • ファイル削除予約 (実際に削除もされる)
    hg rm filename (remove でも良い)
    git rm filename
  • ファイル名変更予約 (実際に変更もされる)
    hg mv old-filename new-filename (rename でも良い)
    git mv old-filename new-filename
  • ファイル複製予約 (実際に複製が作られ add した状態となる。履歴は引き継がれる)
    hg cp old-filename new-filename (copy でも良い)
    git ?
  • コミット (リポジトリに反映)
    hg commit [filename1 filename2 ...] (ci でも良い)
    git commit [filename1 filename2 ...]
    • hg の場合は、引数を省略すると、add 等で予約した内容と、変更のあったファイルの内容がコミットされる。ファイル名を指定することにより、一部のファイルのみをコミットすることができる。
    • git の場合は、変更のあったファイルが自動的にはコミットされない。ファイル名を指定するか、(すでにリポジトリにファイルはあるのでファイルの追加ではないが) add をしておかないとコミットされない。
    • hg も git も、エディタでコミットメッセージの入力を促される。エディタでメッセージを入れずに終了することでコミットを中止できる。エディタを使いたくなければ、-m オプションによりメッセージを指定しておくこともできる。
  • 差分の確認
    hg diff (d でも良い)
    git diff
    作業ディレクトリでの作業内容確認の他、引数をつければ古いリビジョンとの比較も行える。
  • 行ごとにリビジョンを確認する
    hg annotate filename (blame, anno 等でも良い)
    git annotate filename
    こんな怪しい行を誰がいつ追加したんだ、ってのを確認するのに便利な機能。
過去の履歴を取り出す

ちょっと前のバージョンを取り出してコンパイルしたいとか、古いバージョン用のパッチが見つかったのでいったん戻して作業したいとか、そういうの。

  • ログを参照
    hg log (ログ全体が見られる)
    git log (現在の作業ディレクトリ以前のログが見られる)
  • 過去のリビジョン rev を作業ディレクトリに展開する
    hg update rev
    git checkout rev
  • 最新のリビジョンに戻る
    hg update [-C] (ブランチなどの状態によっては -C をつける必要がある)
    git checkout master

分散バージョン管理システムでは、このようにして取り出した古いリビジョンに対してコミットをすることもできる。hg の場合

  • 古いリビジョンに対してコミットをすると、その時点で枝分かれが起きるので、head が増える。head を確認する
    hg heads
  • そのまま push するのは通常は推奨されないため -f オプションが必要になる
    hg push -f
  • 他の head とマージする
    hg merge [-r rev]
    2 個の heads の場合は自動的に 2 つが merge される。3 個以上あるなら -r rev でどの head とマージしたいのかを指定する。

git の場合、git checkout rev で取り出したものは「名無しブランチ」の扱いとなり、コミットもできるが作業が面倒になるため、コミットするなら何かローカルブランチの名前をつけておくようにする。

  • ブランチを確認する
    git branch
    (no branch) となっていたら「名無しブランチ」
  • 古いリビジョンを取り出す時にローカルブランチを作る
    git checkout -b branchname rev
  • 現在のリビジョンにローカルブランチの名前をつける
    git branch branchname
    ただしそのブランチがチェックアウトされるわけではない。
  • ブランチをチェックアウトする
    git checkout branchname
  • 他のブランチとマージする
    git merge branchname
ブランチの考え方

上の作業例がこんなに違うのは、主に hg と git のブランチの考え方の違いから来てると言える。

hg にも名前をつけたブランチを作る機能はあるが、それを使わなくても基本的な作業はすべてこなせる。名前付きブランチは、削除されることが想定されておらず、stable と development といった、恒久的に使用するものが想定されているようである。

  • ブランチを作る予約
    hg branch branchname
    コミットするまで実際にブランチは作られない。ブランチはコミットに対して「これはこういう名前のブランチにあるコミットですよ」という名前付けの意味を持っているため。ファイルの変更がなくても、ブランチの作成だけでコミットは可能である。
  • ブランチ一覧を見る
    hg branches
  • 他のブランチに切り替える
    hg update -C branchname
  • 他のブランチとマージする
    hg merge branchname

名前をつけなくても、古いリビジョンにコミットしていけば、構造としては同じように枝分かれができるが、名前をつけていないと、それがどの目的で枝分かれされたものなのか、分からなくなる。名前をつけずに運用する方法としては、全体を clone で複製する方法があるが、作業ディレクトリが完全に分離してしまい、hg update で気楽に切り替えられないのが欠点。(ちょっと違うだけのブランチであっても、コンパイルしたら全体やり直しになるとかそういった意味で。)

git はリモートブランチとローカルブランチというのがある。コミットにつけるというよりは、ブランチに作業ディレクトリとコミットがぶら下がっているという雰囲気?

  • ローカルブランチをつくる
    git branch branchname
    コミットとは関係なく、ブランチができる。チェックアウトしないと現在のブランチは切り替わらない。
  • ブランチ一覧を見る
    git branch [-a]
    -a をつけるとリモートブランチも見られる。
  • ブランチを切り替える
    git checkout branchname
  • 他のブランチとマージする
    git merge branchname

ローカルブランチはローカルの作業用のブランチであり、push 等で伝搬するものではない。checkout すると必ずブランチが切り替わる。hg のブランチは意図的に切り替えないと切り替わらないが、git の checkout は常にブランチ切り替えの意味を持つ。

最新のコミットにたどり着けるように、最新のコミットには何かブランチ名をつけておくべきである。

clone を使った場合にはリモートブランチという概念が出てくる。リモートの○○ブランチがリポジトリに取り込まれていると、git checkout origin/○○ といったコマンドで取り出せる。ただしこういう風に取り出すと、実際にはローカルブランチは「名無しブランチ」状態になるので、作業する場合は何かローカル名をつけるかマージするなどする。ちょっと分かりにくい。

絵にしてみた

絵があったほうが少しはわかりやすいかな。

ここでは、○がリビジョン、→がリビジョン間の結びつきを表す。□は mq のパッチスタックを表す。

hg:

  • ○の下に head と示してあるものが head (子リビジョンのないリビジョン) を表し、work と示してあるものが作業領域に展開してあるリビジョンを指す。
  • hg のブランチ名は使用していない。

git:

  • ○の下にブランチ名を示してある。
  • git のブランチ名は特定のリビジョンを指し、コミットの際にはチェックアウトされているローカルブランチが新しいリビジョンを指すように更新される。
hg

hg clone でリモートの複製を作る。

変更を加える。

pull でリモートの変更を手元に持ってくる。head が増える。この時点で log には新しい変更も含めてすべてが表示される。すなわちリモートもローカルもすべて対等であり、pull で取り込んだ後はリモートとローカルの区別はなくすべてが同じように見える。heads で head の情報を確認することができる。

ここで head を 1 つにまとめる方法は複数ある。

(a) merge でマージする。

(b) rebase 拡張を使って自分の変更を最後に移動して一本化する。

(c) mq 拡張を使って自分の変更をパッチにして一本化する。

まず qimport でパッチ化する。

qpop -a でパッチをすべて pop する。これで head がひとつになる。

update で最新リビジョンに更新する。

qpush -a でパッチをすべて push する。

qfinish -a でパッチをすべて commit に戻す。

git

git clone でリモートの複製を作る。

変更を加える。

fetch でリモートの変更を手元に持ってくる。fetch 後も、log は何も指定しなければ自分の master ブランチの変更のみが表示される。log にリモートブランチの名前 origin/master を指定すれば、リモートの変更が見られる。

ブランチ名によってローカルとリモートが区別されており、fetch が更新するのは手元にあるリモート側のブランチの情報、commit が更新するのはローカルのブランチの情報である。

merge origin/master でマージする。リモートブランチを track しているなら、pull で fetch と merge が行われる。

rebase origin/master を使って自分の変更を最後に移動して一本化できる。merge や pull した場合、マージしたものも残ってはいるが (図の点線部)、ブランチは rebase 後のリビジョンを指すように変更されるため、マージした時のリビジョンを直接指定しない限りそれらが見えることはないし、clone や push/pull での複製もされない。

guilt で mq と同じようなパッチ化も可能。

ブランチ名が複数ある場合、clone ですべてのブランチが複製され、ひとつのローカルブランチが自動的にリモートブランチのひとつを track している状態に設定される。他のリモートブランチをチェックアウトしたい時は、以下のようなコマンドを実行して、リモートブランチを track するローカルブランチを作る。

git branch local-branch-name remote-branch-name

リモートブランチはリモートのブランチを表していて、ローカルで更新することはできない。リモートブランチや特定のコミットを直接指定してチェックアウトすると、ブランチ名がない状態となる。そのままコミットはできるが最新リビジョンを指すブランチ名がなくなるため、他のブランチをチェックアウトするとそのコミットは行方不明となるし、fetch, merge などもできなくなるため通常はブランチ名を与えて使用する。

リモートブランチの track 情報は .git/config ファイルに書き込まれる。必要があればテキストエディターで編集しても良い。