-
1. Gitを始めるにあたって
- 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 スマートHTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティのホスティングオプション
- 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 Resetの解明
- 7.8 高度なマージ
- 7.9 Rerere
- 7.10 Gitを使ったデバッグ
- 7.11 サブモジュール
- 7.12 バンドル
- 7.13 置換
- 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 Visual Studio CodeでのGit
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMineでのGit
- A1.5 Sublime TextでのGit
- A1.6 BashでのGit
- A1.7 ZshでのGit
- A1.8 PowerShellでのGit
- A1.9 まとめ
-
A2. 付録B: アプリケーションへのGitの組み込み
- A2.1 コマンドラインGit
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
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 Plumbingコマンド
10.7 Git内部 - メンテナンスとデータ復旧
メンテナンスとデータ復旧
時々、リポジトリをよりコンパクトにしたり、インポートしたリポジトリをクリーンアップしたり、失われた作業を回復したりするために、クリーンアップが必要になる場合があります。このセクションでは、これらのシナリオのいくつかについて説明します。
メンテナンス
Gitは時々、「auto gc」というコマンドを自動的に実行します。ほとんどの場合、このコマンドは何もしません。しかし、ルーズオブジェクト(パックファイルに含まれていないオブジェクト)が多すぎる場合や、パックファイルが多すぎる場合は、Gitは本格的なgit gc
コマンドを起動します。「gc」はガベージコレクトの略で、このコマンドはいくつかのことを行います。すべてのルーズオブジェクトを収集してパックファイルに配置し、パックファイルを1つの大きなパックファイルに統合し、どのコミットからも到達できず、数ヶ月経過したオブジェクトを削除します。
auto gc
は次のように手動で実行できます。
$ git gc --auto
繰り返しになりますが、これは通常何も行いません。Gitが実際に gc
コマンドを起動するには、約7,000個以上のルーズオブジェクトまたは50個以上のパックファイルが必要です。これらの制限は、それぞれ gc.auto
と gc.autopacklimit
の設定で変更できます。
gc
が実行するもう1つのことは、参照を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の旅のどこかで、誤ってコミットを失ってしまうことがあるかもしれません。一般的に、これは作業中のブランチを強制的に削除してしまい、結局そのブランチが必要だったと判明した場合、またはブランチをハードリセットして、そこから何かが必要だったコミットを放棄した場合に発生します。このようなことが起こったとして、どうすればコミットを取り戻せるでしょうか?
これは、テストリポジトリのmaster
ブランチを古いコミットにハードリセットし、失われたコミットを回復する例です。まず、この時点でのリポジトリの状態を確認しましょう。
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create 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
というツールを使うことです。作業中、GitはHEADが変更されるたびに、そのHEADの情報を密かに記録しています。コミットしたり、ブランチを変更したりするたびに、reflogが更新されます。reflogは git update-ref
コマンドによっても更新されます。これは、Gitリファレンスで説明したように、リファレンスファイルにSHA-1値を直接書き込むのではなく、このコマンドを使用するもう1つの理由です。git reflog
を実行すると、いつでも自分がどこにいたかを確認できます。
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create 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
Modify repo.rb a bit
失ったのは一番下のコミットのようです。そのコミットで新しいブランチを作成することで回復できます。たとえば、そのコミット(ab1afef)でrecover-branch
という名前のブランチを開始できます。
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create 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はありません。この時点で、そのコミットをどのように回復できますか?1つの方法は、データベースの整合性をチェックする 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には素晴らしい点がたくさんありますが、問題を引き起こす可能性のある機能の1つは、`git clone`がプロジェクトの全履歴、つまりすべてのファイルのすべてのバージョンをダウンロードすることです。プロジェクト全体がソースコードであれば問題ありません。Gitはデータを効率的に圧縮するために高度に最適化されているからです。しかし、プロジェクトの履歴のどこかで誰かが単一の巨大なファイルを追加した場合、たとえそのファイルが次のコミットですぐにプロジェクトから削除されたとしても、その後すべてのクローンはその大きなファイルをダウンロードしなければなりません。履歴から到達可能であるため、常にそこに存在します。
これは、SubversionやPerforceのリポジトリをGitに変換する場合に大きな問題となることがあります。これらのシステムでは完全な履歴をダウンロードしないため、この種の追加による影響はほとんどありません。別のシステムからインポートした場合や、リポジトリが本来よりもはるかに大きいことが判明した場合、大きなオブジェクトを見つけて削除する方法を説明します。
警告: この方法はコミット履歴を破壊します。 大きなファイル参照を削除するために変更する必要がある最も古いツリー以降のすべてのコミットオブジェクトを書き換えます。インポート直後で、誰もそのコミットに基づいて作業を開始する前にこれを行う場合は問題ありません。そうでなければ、すべての貢献者に、新しいコミットに作業をリベースする必要があることを通知する必要があります。
実演のために、テストリポジトリに大きなファイルを追加し、次のコミットでそれを削除し、それを見つけて、リポジトリから完全に削除します。まず、大きなオブジェクトを履歴に追加します。
$ curl -L 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
おっと、プロジェクトに巨大なtarballを追加するつもりはありませんでしたね。さっさと削除しましょう。
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove 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
エントリはパックファイルのサイズをキロバイトで表すので、ほぼ5MBを使用しています。最後のコミットの前は2K程度だったので、前のコミットからファイルを削除しても、履歴から削除されたわけではないことが明らかです。誰かがこのリポジトリをクローンするたびに、この小さなプロジェクトを取得するためだけに、誤って大きなファイルを追加したため、5MBすべてをクローンしなければなりません。削除しましょう。
まず、それを見つけなければなりません。この場合、どのファイルか既に知っています。しかし、知らなかったとしたらどうでしょう?どのファイルがそんなに多くの容量を占めているかをどうやって特定するでしょうか?git gc
を実行すると、すべてのオブジェクトはパックファイルにあります。大きなオブジェクトは、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
コマンドを使用します。rev-list
に--objects
を渡すと、すべてのコミットのSHA-1と、それらに関連付けられたファイルパスを持つBLOBのSHA-1がリストされます。これを使ってBLOBの名前を見つけることができます。
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
次に、過去のすべてのツリーからこのファイルを削除する必要があります。どのコミットがこのファイルを変更したかは簡単に確認できます。
$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove 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
コミットからのみ履歴を書き換えるように要求します。なぜなら、問題が始まったのがそこだと知っているからです。そうしないと、最初から始まり、不必要に時間がかかります。
あなたの履歴には、そのファイルへの参照はもう含まれていません。しかし、reflogと、filter-branch
を実行したときにGitが.git/refs/original
の下に追加した新しいrefsセットにはまだ含まれています。そのため、それらを削除してからデータベースを再パックする必要があります。再パックする前に、それらの古いコミットへのポインタを持つものをすべて削除する必要があります。
$ 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
パックされたリポジトリのサイズは8Kに減少しました。これは5MBよりもはるかに優れています。size
の値から、大きなオブジェクトはまだルーズオブジェクトに残っていることがわかります。つまり、まだ消えていません。しかし、プッシュやその後のクローン時には転送されないため、それが重要です。本当に必要であれば、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