-
1. 使い始める
- 1.1 バージョン管理に関して
- 1.2 Git略史
- 1.3 Gitの基本
- 1.4 コマンドライン
- 1.5 Gitのインストール
- 1.6 最初のGitの構成
- 1.7 ヘルプを見る
- 1.8 まとめ
-
2. Git の基本
- 2.1 Git リポジトリの取得
- 2.2 変更内容のリポジトリへの記録
- 2.3 コミット履歴の閲覧
- 2.4 作業のやり直し
- 2.5 リモートでの作業
- 2.6 タグ
- 2.7 Git エイリアス
- 2.8 まとめ
-
3. Git のブランチ機能
- 3.1 ブランチとは
- 3.2 ブランチとマージの基本
- 3.3 ブランチの管理
- 3.4 ブランチでの作業の流れ
- 3.5 リモートブランチ
- 3.6 リベース
- 3.7 まとめ
-
4. Gitサーバー
- 4.1 プロトコル
- 4.2 サーバー用の Git の取得
- 4.3 SSH 公開鍵の作成
- 4.4 サーバーのセットアップ
- 4.5 Git デーモン
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティによる Git ホスティング
- 4.10 まとめ
-
5. Git での分散作業
- 5.1 分散作業の流れ
- 5.2 プロジェクトへの貢献
- 5.3 プロジェクトの運営
- 5.4 まとめ
-
6. GitHub
- 6.1 アカウントの準備と設定
- 6.2 プロジェクトへの貢献
- 6.3 プロジェクトのメンテナンス
- 6.4 組織の管理
- 6.5 スクリプトによる GitHub の操作
- 6.6 まとめ
-
7. Git のさまざまなツール
- 7.1 リビジョンの選択
- 7.2 対話的なステージング
- 7.3 作業の隠しかたと消しかた
- 7.4 作業内容への署名
- 7.5 検索
- 7.6 歴史の書き換え
- 7.7 リセットコマンド詳説
- 7.8 高度なマージ手法
- 7.9 Rerere
- 7.10 Git によるデバッグ
- 7.11 サブモジュール
- 7.12 バンドルファイルの作成
- 7.13 Git オブジェクトの置き換え
- 7.14 認証情報の保存
- 7.15 まとめ
-
8. Git のカスタマイズ
- 8.1 Git の設定
- 8.2 Git の属性
- 8.3 Git フック
- 8.4 Git ポリシーの実施例
- 8.5 まとめ
-
9. Gitとその他のシステムの連携
- 9.1 Git をクライアントとして使用する
- 9.2 Git へ移行する
- 9.3 まとめ
-
10. Gitの内側
- 10.1 配管(Plumbing)と磁器(Porcelain)
- 10.2 Gitオブジェクト
- 10.3 Gitの参照
- 10.4 Packfile
- 10.5 Refspec
- 10.6 転送プロトコル
- 10.7 メンテナンスとデータリカバリ
- 10.8 環境変数
- 10.9 まとめ
-
A1. 付録 A: その他の環境でのGit
- A1.1 グラフィカルインタフェース
- A1.2 Visual StudioでGitを使う
- A1.3 EclipseでGitを使う
- A1.4 BashでGitを使う
- A1.5 ZshでGitを使う
- A1.6 PowershellでGitを使う
- A1.7 まとめ
-
A2. 付録 B: Gitをあなたのアプリケーションに組み込む
- A2.1 Gitのコマンドラインツールを使う方法
- A2.2 Libgit2を使う方法
- A2.3 JGit
-
A3. 付録 C: Gitのコマンド
- A3.1 セットアップと設定
- A3.2 プロジェクトの取得と作成
- A3.3 基本的なスナップショット
- A3.4 ブランチとマージ
- A3.5 プロジェクトの共有とアップデート
- A3.6 検査と比較
- A3.7 デバッグ
- A3.8 パッチの適用
- A3.9 メール
- A3.10 外部システム
- A3.11 システム管理
- A3.12 配管コマンド
10.7 Gitの内側 - メンテナンスとデータリカバリ
メンテナンスとデータリカバリ
たまには、ちょっとしたお掃除 – リポジトリを圧縮したり、インポートしたリポジトリをクリーンアップしたり、失われた成果物をもとに戻したり – が必要になるかもしれません。 このセクションではこれらのシナリオのいくつかについて取り上げます。
メンテナンス
Gitは時々 “auto gc” と呼ばれるコマンドを自動的に実行します。
大抵の場合、このコマンドは何もしません。
ですが、緩いオブジェクト(packfileの中に入っていないオブジェクト)やpackfileがあまりに多い場合は、Gitは完全な(full-fledged)git gc
コマンドを起動します。
“gc” はガベージコレクト(garbage collect)を意味します。このコマンドは幾つものことを行います。すべての緩いオブジェクトを集めてpackfileに入れ、複数のpackfileをひとつの大きなpackfileに統合し、さらにどのコミットからも到達が不可能かつ数ヶ月間更新がないオブジェクトを削除します。
次のように手動でauto gcを実行することもできます。
$ git gc --auto
繰り返しますが、これは通常は何も行いません。
約7,000個もの緩いオブジェクトがあるか、または50以上のpackfileがある場合でないと、Gitは実際にgcコマンドを開始しません。
これらのリミットはそれぞれ設定ファイルの gc.auto
と gc.autopacklimit
で変更できます。
その他に gc
が行うこととしては、複数の参照を1つのファイルにパックすることが挙げられます。
リポジトリに、次のようなブランチとタグが含まれているとしましょう。
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
git gc
を実行すると、これらのファイルは refs
ディレクトリからなくなります。
効率化のため、Gitはそれらのファイルの内容を、以下のような .git/packed-refs
という名前のファイルに移します。
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
ただ、ここで参照を更新しても、Gitはこのファイルを編集せず、その代わりに refs/heads
に新しいファイルを書き込みます。
とある参照に対する適切なSHA-1ハッシュを得るために、Gitは refs
ディレクトリ内でその参照をチェックした上で、見つからなかった場合の代替として packed-refs
ファイルをチェックします。
一方、 refs
ディレクトリ内で参照が見つけられない場合は、それはおそらく packed-refs
ファイル内にあります。
ファイルの最後の行に注意してください。 ^
という文字で始まっています。
これは、この行のすぐ上にあるタグは注釈付き版のタグであり、この行はそのタグが指しているコミットであるということを意味しています。
データリカバリ
Gitを使っていく過程のある時点で、誤ってコミットを失ってしまうことがあるかもしれません。 このようなことが起こりがちなのは、成果物が入っていたブランチをforce-deleteしたけれど、その後結局そのブランチが必要になったときか、あるいはブランチをhard-resetしたために、何か必要なものが入っているコミットがそのブランチから切り離されてしまったときです。 このようなことが起きたとして、どうやったらコミットを取り戻せるでしょうか?
以下に示す例では、testリポジトリ内のmasterブランチを古いコミットにhard-resetして、それから失ったコミットを復元します。 まず、今の時点でリポジトリがどのような状況にあるのか調べてみましょう。
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
ここで、master
ブランチを真ん中のコミットの時点まで戻します。
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
これで、一番上にあった2つのコミットは、事実上失われたことになります。これらのコミットに辿り着けるブランチがないためです。 そのため、最後のコミットのSHA-1ハッシュを調べた上で、そこを指すブランチを追加する必要があります。 ここでポイントとなるのは、最後のコミットのSHA-1ハッシュを見つける方法です。ハッシュ値を記憶してます、なんてことはないですよね?
大抵の場合、最も手っ取り早いのは、git reflog
というツールを使う方法です。
あなたが作業をしている間、HEADを変更する度に、HEADがどこを指しているかをGitは裏で記録しています。
コミットをしたり、ブランチを変更したりする度に、reflogは更新されます。
また、reflogは git update-ref
コマンドによっても更新されます。refファイルに書かれたSHA-1ハッシュ値を直に編集せずに、このコマンドを使って編集すべき理由の1つがこれです(詳しくは Gitの参照 で取り上げました)。
git reflog
を実行することで、ある時点で自分がどこにいたのかを知ることができます。
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
このとおり、チェックアウトした2つのコミットが見つかりました。ですが、それ以上の情報は表示されていません。
同じ情報をもっと有用な形式で表示するには git log -g
を実行します。これはreflogを通常のログ出力と同じ形式で出力してくれます。
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
modified repo.rb a bit
一番下にあるコミットが、失われたコミットのようです。そこから新しいブランチを作成すれば、失ったコミットを取り戻せます。
例えば、そのコミット(ab1afef)を起点に recover-branch
という名前のブランチを作成できます。
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
やった! – master
ブランチがかつて存在した場所に、 recover-branch
という名前のブランチが作られて、最初の2つのコミットは再び到達可能になりました。
さて次は、失われたコミットが何らかの理由でreflogの中にもなかった場合を考えましょう – recover-branch
を取り除き、reflogを削除することによって、擬似的にその状況を作り出すことができます。
これで、最初の2つのコミットは、今どこからも到達不能になりました。
$ git branch -D recover-branch
$ rm -Rf .git/logs/
reflogのデータは .git/logs/
ディレクトリに保存されるため、これでreflogは事実上なくなりました。
この時点で、どうしたら失われたコミットを復元できるでしょうか?
ひとつの方法として、 git fsck
ユーティリティーを使用してデータベースの完全性をチェックする方法があります。
--full
オプションを付けて実行すると、他のどのオブジェクトからも指されていないオブジェクトをすべて表示します。
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
このケースでは、 “dangling commit” という文字列の後に失われたコミットが表示されています。 前と同様にこのSHA-1ハッシュを指すブランチを作成すれば、失われたコミットを取り戻せます。
オブジェクトの削除
Gitには素晴らしい点がたくさんありますが、問題となり得る特徴がひとつあります。それは、 git clone
がすべてのファイルのすべてのバージョンを含んだプロジェクトの歴史全体をダウンロードしてしまうということです。
保存されているのがソースコードだけなら、特に問題はありません。なぜなら、Gitはそのようなデータを効率良く圧縮することに高度に最適化されているからです。
しかし、もし誰かがある時点でプロジェクトの歴史に非常に大きなファイルを1つ加えると、以降のクローンではすべて、その大きなファイルのダウンロードを強いられることになります。これは、直後のコミットでそのファイルをプロジェクトから削除したとしても変わりません。
なぜなら、そのファイルは履歴から到達可能であり、常にそこに存在し続けるためです。
SubversionやPerforceのリポジトリをGitに変換するときに、これは大きな問題になり得ます。 なぜなら、それらのシステムではすべての履歴をダウンロードする必要がないため、非常に大きなファイルを追加してもほとんど悪影響がないからです。 別のシステムからリポジトリをインポートした場合や、リポジトリがあるべき状態よりもずっと大きくなっている場合に、大きなオブジェクトを見つけて取り除く方法を以下に示します。
注意: この操作はコミット履歴を破壊的に変更します。 この操作では、大きなファイルへの参照を取り除くため、修正が必要な一番古いツリーから、以降すべてのコミットオブジェクトを再書き込みします。 インポートの直後、そのコミットをベースとして誰かが作業を始める前にこの操作を行った場合は問題ありません。そうでない場合は、作業中の内容を新しいコミットにリベースしなければならないことを、すべての関係者に知らせる必要があります。
実演のため、testリポジトリに大きなファイルを追加して、次のコミットでそれを取り除いた上で、リポジトリからそのファイルを探し出し、そしてリポジトリからそれを完全に削除します。 まず、あなたの歴史に大きなオブジェクトを追加します。
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
おっと、誤ってプロジェクトに非常に大きなtarボールを追加してしまいました。取り除いたほうがいいでしょう。
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
次に、データベースに対して gc
を実行します。その後、どれくらいのスペースを使用しているのかを見てみましょう。
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
count-objects
コマンドを実行すると、どれくらいのスペースを使用しているのかをすぐに見ることができます。
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
size-pack
エントリにはpackfileのサイズがキロバイト単位で表示されていて、約5MB使用していることがわかります。
大きなファイルを追加するコミットの前に使用していたのは、2KB程度でした – 明らかに、直近のコミットで行ったファイルの削除では、歴史からファイルが削除されていません。
誤って大きなファイルを追加してしまったがために、誰かがこのリポジトリをクローンするたび、この小さなプロジェクトを取得するだけのために5MBすべてをクローンしなければならなくなってしまいました。
この大きなファイルを削除しましょう。
最初に、その大きなファイルを見つけなければなりません。
この例では、どのファイルがそれかは既に分かっています。
しかし、それが分からない場合、どうやって多くのスペースを占めているファイルを特定するのでしょうか?
git gc
を実行すると、すべてのオブジェクトがpackfileに格納されます。
そのため、別の配管コマンド git verify-pack
を実行し、その出力を3つ目のフィールド(ファイルサイズ)でソートすれば、大きなオブジェクトを特定できます。
関心の対象になるのは最も大きなファイル数個だけなので、その出力をパイプで tail
コマンドに通してもよいでしょう。
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
探していた大きなオブジェクトは、一番下の5MBのものです。
そのオブジェクトが何のファイルなのかを知るには 特定のコミットメッセージ書式の強制 で少し使用した rev-list
コマンドを使用します。
--objects
を rev-list
に渡すと、すべてのコミットのSHA-1ハッシュに加えて、すべてのブロブのSHA-1ハッシュと、そのブロブに関連付けられたファイルのパスを一覧表示します。
これは、ブロブの名前を特定するのに使えます。
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
次に、過去のすべてのツリーからこのファイルを削除する必要があります。 このファイルを変更したのがどのコミットかは簡単に分かります。
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
Gitリポジトリからこのファイルを完全に削除するには、 7b30847
の下流にあるすべてのコミットを修正しなければなりません。
そのためには、 歴史の書き換え で使用した filter-branch
を使用します。
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
--index-filter
オプションは 歴史の書き換え で使用した --tree-filter
オプションに似ていますが、ディスク上のチェックアウトされたファイルを変更するコマンドを渡すのではなく、コミット毎にステージングエリアまたはインデックスを変更する点が異なります。
ここでは、あるファイルを rm file
で削除するのではなく、 git rm --cached
で削除する必要があります。つまり、ディスクではなくインデックスからファイルを削除しなければなりません。
このようにする理由はスピードです。この場合、Gitがフィルタを実行する前に各リビジョンをディスク上へチェックアウトする必要がないので、プロセスをもっともっと速くすることができます。
お望みなら、同様のタスクは --tree-filter
でも行えます。
git rm
に渡している --ignore-unmatch
オプションは、削除しようとするパターンに合うファイルがない場合に、エラーを出力しないようにします。
最後に、filter-branch
に、コミット 7b30847
以降の履歴のみを修正するように伝えています。なぜなら、問題が発生した場所がここだと分かっているからです。
そうでない場合は、歴史の先頭から処理を開始することになり、不必要に長い時間がかかるでしょう。
これで、歴史から大きなファイルへの参照がなくなりました。
しかし、 .git/refs/original
の下で filter-branch
を行ったときにGitが新しく追加したrefsには、まだ参照が含まれています。reflogについても同様です。それらを削除した上で、データベースを再パックしなければなりません。
再パックの前に、それら古いコミットへのポインタを持つものをすべて削除する必要があります。
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
どれくらいのスペースが節約されたかを見てみましょう。
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
パックされたリポジトリのサイズは8KBに下がり、当初の5MBよりもずっとよくなりました。
サイズの値を見ると、緩いオブジェクトの中には大きなオブジェクトが残っており、無くなったわけではないことが分かります。ですが、プッシュや以降のクローンで転送されることはもうありません。ここが重要な点です。
お望みなら、 git prune
に --expire
オプションを指定すれば、オブジェクトを完全に削除することもできます。
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0