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 ファイルに書き込まれる。必要があればテキストエディターで編集しても良い。
GitとMercurial
init
$ git init |$ hg init
add
$ echo 1 > file |$ echo 1 > file
$ git add file |$ hg add file
gitはstagingにコピー
hgは管理対象に追加予約
$ echo 2 > file |$ echo 2 > file
gitはadd後に変更してもstagingは変わらない
hgはadd後の変更がcommit対象になる
commit内容の確認
$ git diff --cached |$ hg diff --git
diff --git a/file b/file |diff --git a/file b/file
new file mode 100644 |new file mode 100644
index 0000000..d00491f |--- /dev/null
--- /dev/null |+++ b/file
+++ b/file |@@ -0,0 +1,1 @@
@@ -0,0 +1 @@ |+2
+1 |
gitはstagingの内容を確認する
hgは管理対象に対する差分を確認する
commit
$ git commit -ma |$ hg commit -ma
commitの確認
$ git show |$ hg export --git
commit c179ea9d08bc0914199325386b6e55d$|# HG changeset patch
Author: name |# User name
Date: Sat Jul 5 22:14:24 2025 +0900 |# Date 1751721266 -32400
|# Sat Jul 05 22:14:26 2025 +0900
a |# Node ID b9ab6ce3077c8b8903dbe6ab96623$
|# Parent 00000000000000000000000000000$
diff --git a/file b/file |a
new file mode 100644 |
index 0000000..d00491f |diff --git a/file b/file
--- /dev/null |new file mode 100644
+++ b/file |--- /dev/null
@@ -0,0 +1 @@ |+++ b/file
+1 |@@ -0,0 +1,1 @@
|+2
clone
$ git clone ./ ../gg |$ hg clone ./ ../hh
cloneした後に変更してcommit
$ echo 3 > file |$ echo 3 > file
$ git diff |$ hg diff --git
diff --git a/file b/file |diff --git a/file b/file
index d00491f..00750ed 100644 |--- a/file
--- a/file |+++ b/file
+++ b/file |@@ -1,1 +1,1 @@
@@ -1 +1 @@ |-2
-1 |+3
+3 |$ hg commit -mb
$ git add file |
$ git commit -mb |
commit結果をさっきのcloneに取り込む
$ cd ../gg |$ cd ../hh
$ git fetch origin master |$ hg paths
From /tmp/g/. |default = /tmp/h
* branch master -> FET$|$ hg pull default
c179ea9..7d7c34f master -> ori$|/tmp/h から取り込み中
$ git merge origin/master |変更点を探索中
Updating c179ea9..7d7c34f |リビジョンを追加中
Fast-forward |マニフェストを追加中
file | 2 +- |ファイルの変更を追加中
1 file changed, 1 insertion(+), 1 del$|1 個のリビジョン(1 の変更を 1 ファイル$$
|新規リビジョン範囲 5aa3c1f2d9ac
|(作業領域の更新は 'hg update')
|$ hg update
|ファイルの更新数 1、 マージ数 0、 削除$$
git pullはgit fetchとgit mergeを行う
逆にpushを使う場合
$ cd ../g |$ cd ../h
$ echo 4 > file |$ echo 4 > file
$ git add file |$ hg commit -mc
$ git commit -mc |$ hg push ../hh
[master 7231669] c |../hh への反映中
1 file changed, 1 insertion(+), 1 del$|変更点を探索中
$ git branch work |リビジョンを追加中
$ git push ../gg work |マニフェストを追加中
Enumerating objects: 5, done. |ファイルの変更を追加中
Counting objects: 100% (5/5), done. |1 個のリビジョン(1 の変更を 1 ファイル$$
Writing objects: 100% (3/3), 246 bytes$|
Total 3 (delta 0), reused 0 (delta 0),$|
To ../gg |
* [new branch] work -> work |
gitはチェックアウトしているブランチへ外からpushすることはできないため、別のブランチを作成してpushする
hgはローカルのリビジョン番号が増えていくだけなのでどんなchangesetでも外からpushできる
cloneのほうに別のcommitを作ってみる
$ cd ../gg |$ cd ../hh
$ cat file |$ cat file
3 |3
$ echo a > file |$ echo a > file
$ git add file |$ hg commit -m0
$ git commit -m0 |新規ヘッドが増えました
[master e7cd308] 0 |
1 file changed, 1 insertion(+), 1 del$|
gitはブランチが最新コミットを指していてそれが枝を区別しているので特にメッセージは出ない
hgは、ヘッド (子がいないchangeset) 以外にコミットすることによってヘッドが増えたというメッセージになる
戻ってリモートの状態を知る
$ cd ../g |$ cd ../h
$ git remote add gg ../gg |$ hg incoming ../hh
$ git fetch gg |../hh と比較中
remote: Enumerating objects: 5, done. $|変更点を探索中
remote: Counting objects: 100% (5/5), $|リビジョン: 3:350fcbec1a0e
remote: Total 3 (delta 0), reused 0 (d$|タグ: tip
Unpacking objects: 100% (3/3), 224 byt$|親リビジョン: 1:5aa3c1f2d9ac
From ../gg |ユーザ: name
* [new branch] master -> gg/$|日付: Sat Jul 05 22:31:55 2025 $
* [new branch] work -> gg/$|要約: 0
$ git show gg/master |
commit e7cd308adb46aeb7b46a7ac628cd75a$|
Author: name |
Date: Sat Jul 5 22:31:48 2025 +0900 |
|
0 |
|
diff --git a/file b/file |
index 00750ed..7898192 100644 |
--- a/file |
+++ b/file |
@@ -1 +1 @@ |
-3 |
+a |
gitはremoteを追加してfetchすることによってリモートブランチとそのコミットが取り込まれリモートの状態に合わせて更新される
hgはincomingによりローカルにchangesetを取り込むことなくリモートに追加されたchangesetを確認でき、pullして取り込まれた後はローカルのそれまでのchangesetと一体となる
部分的なcommit
$ seq 1 3 >> file |$ seq 1 3 >> file
$ git add -p |$ hg commit -mi -i
diff --git a/file b/file |diff --git a/file b/file
index b8626c4..7ddda17 100644 |1 個の差分、 3 行の変更
--- a/file |'file' の変更点を調べますか?
+++ b/file |(enter ? for help) [Ynesfdaq?] y
@@ -1 +1,4 @@ |
4 |@@ -1,1 +1,4 @@
+1 | 4
+2 |+1
+3 |+2
(1/1) Stage this hunk [y,n,q,a,d,e,?]?$|+3
hint: Waiting for your editor to close$|この変更を 'file' に記録しますか?
488 |(enter ? for help) [Ynesfdaq?] e
1,8p |
# Manual hunk edit mode -- see bottom $|675
@@ -1 +1,4 @@ |1,8p
4 |diff --git a/file b/file
+1 |--- a/file
+2 |+++ b/file
+3 |@@ -1,1 +1,4 @@
# --- | 4
# To remove '-' lines, make them ' ' l$|+1
5,6d |+2
w |+3
482 |7,8d
q |w
|669
$ git commit -mi |q
いずれも1, 2, 3の3行のうち1だけをコミットする
gitはadd -pによりstagingに一部だけをコピーする
hgはcommit -iにより一部を選択してコミットする (以前はrecord extensionが必要だった)
きれいなパッチセットを完成させるためのコミット編集
gitはrebase -iでほとんどこなせる。git rebase -i commitidでcommitidを親とするコミットから今のHEADまでのコミットがpickとして並んだファイルがテキストエディターで開くので、編集して保存すると処理が始まる。
pickはcherry-pick相当で、何も変更せずに保存すればもとと同じ並びでコミットが取り込まれる。
editにするとそのコミットをpickしたところでシェルが起動して、編集してgit commit --amendなどができる。
break行を追加することによりそこでシェルが起動して、そこで編集することもできる。
squashで前のコミットとまとめることができる。両方をまとめたコミットメッセージの入力になる。
順番を入れ替えてもいいし、消してもいいし、ないものを追加してもいい。
--ontoで別のコミットの上に移すこともできるが、breakして自分でcheckoutしてもいいんじゃないかな。
rebaseの最中はブランチ名がないHEADで作業を行う。git rebase --continueで継続、git rebase --abortで中止してもとのブランチに戻る。pickの際にconflictになった場合もシェルが起動して、やはりgit rebase --continueやgit rebase --abortを使う。何をやっていたかわからなくなったらgit statusを確認。
hgはmq extensionでほとんどこなせる。他は知らない。
ミスるとこわいので時々バックアップをとることをおすすめ。バックアップ用のrepositoryを作って何でもかんでもそこにhg push -fしておく。一番最初のchangesetが違ってもpush -fはできるので、無関係なrepositoryでも何でもよい。mqのぶんもqfinishやっていなくてもそのままpushできる。大量のheadができるが、いざとなったらそこから発掘できるのは大きい。phaseに注意。