-
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 サードパーティホスティングの選択肢
- 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の内側
-
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 配管コマンド
7.7 Gitツール - Resetを解明する
Resetを解明する
より専門的なツールに進む前に、Gitのreset
コマンドとcheckout
コマンドについて説明しましょう。これらのコマンドは、Gitを初めて使う人にとって最も混乱しやすい部分の2つです。あまりにも多くのことを行うため、それらを実際に理解し、適切に使いこなすことは絶望的に思えます。そのため、私たちは簡単な比喩をお勧めします。
3つのツリー
reset
とcheckout
について考えるより簡単な方法は、Gitが3つの異なるツリーのコンテンツマネージャーであるという精神的な枠組みを通すことです。ここで「ツリー」とは、データ構造そのものではなく、「ファイルの集合」を意味しています。インデックスが厳密にはツリーのように動作しないケースもいくつかありますが、当面はどのように考える方が簡単です。
Gitはシステムとして、通常の操作において3つのツリーを管理し、操作します
ツリー | 役割 |
---|---|
HEAD |
最後のコミットスナップショット、次の親 |
インデックス |
提案された次のコミットスナップショット |
ワーキングディレクトリ |
サンドボックス |
HEAD
HEADは現在のブランチ参照へのポインタであり、そのブランチで行われた最後のコミットへのポインタでもあります。つまり、HEADは次に作成されるコミットの親となります。一般的には、HEADはそのブランチでの最後のコミットのスナップショットと考えるのが最も簡単です。
実際、そのスナップショットがどのようなものかを見るのは非常に簡単です。以下は、HEADスナップショット内の各ファイルの実際のディレクトリリストとSHA-1チェックサムを取得する例です
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Gitのcat-file
コマンドとls-tree
コマンドは、低レベルな用途に使われる「配管(plumbing)」コマンドであり、日常的な作業ではあまり使われませんが、ここで何が起こっているかを見るのに役立ちます。
インデックス
インデックスはあなたの提案された次のコミットです。私たちはこの概念をGitの「ステージングエリア」とも呼んでいますが、git commit
を実行したときにGitが参照するのはこれだからです。
Gitは、あなたのワーキングディレクトリに最後にチェックアウトされたすべてのファイルの内容と、それらが最初にチェックアウトされたときの状態のリストでこのインデックスを埋めます。その後、それらのファイルの一部を新しいバージョンに置き換え、git commit
がそれを新しいコミットのツリーに変換します。
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
ここでもまた、git ls-files
を使用しています。これは、舞台裏でインデックスが現在どのような状態になっているかを示すコマンドです。
インデックスは厳密にはツリー構造ではありません。実際にはフラットなマニフェストとして実装されていますが、私たちの目的にはこれで十分です。
ワーキングディレクトリ
最後に、ワーキングディレクトリ(一般的には「ワーキングツリー」とも呼ばれます)があります。他の2つのツリーは、.git
フォルダ内に効率的ですが不便な方法でコンテンツを格納しています。ワーキングディレクトリはそれらを実際のファイルとして展開するため、編集がはるかに容易になります。ワーキングディレクトリはサンドボックスとして考えてください。そこで変更をステージングエリア(インデックス)に、そして履歴にコミットする前に試すことができます。
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
ワークフロー
Gitの典型的なワークフローは、これら3つのツリーを操作することで、プロジェクトのスナップショットをより良い状態へと順次記録していくことです。

このプロセスを視覚化してみましょう。単一のファイルがある新しいディレクトリに入るとします。このファイルをv1と呼び、青で示します。次にgit init
を実行すると、未誕生のmaster
ブランチを指すHEAD参照を持つGitリポジトリが作成されます。

この時点では、ワーキングディレクトリのツリーのみがコンテンツを持っています。
次に、このファイルをコミットしたいので、git add
を使ってワーキングディレクトリのコンテンツをインデックスにコピーします。

git add
でファイルがインデックスにコピーされる次にgit commit
を実行します。これはインデックスの内容を取得して永続的なスナップショットとして保存し、そのスナップショットを指すコミットオブジェクトを作成し、master
をそのコミットを指すように更新します。

git commit
のステップgit status
を実行すると、3つのツリーすべてが同じであるため、変更は表示されません。
次に、そのファイルに変更を加えてコミットしたいとします。同じプロセスを実行します。まず、ワーキングディレクトリでファイルを変更します。このファイルをv2と呼び、赤で示します。

この時点でgit status
を実行すると、インデックスとワーキングディレクトリの間でエントリが異なるため、ファイルが赤で「Changes not staged for commit(コミットのためにステージされていない変更)」として表示されます。次に、git add
を実行してインデックスにステージングします。

この時点でgit status
を実行すると、インデックスとHEADが異なるため、ファイルが緑で「Changes to be committed(コミットされる変更)」の下に表示されます。つまり、提案された次のコミットは、最後のコミットとは異なります。最後に、git commit
を実行してコミットを完了します。

git commit
ステップこれで、3つのツリーすべてが再び同じになったため、git status
は何も出力しません。
ブランチの切り替えやクローンも同様のプロセスを経ます。ブランチをチェックアウトすると、HEADが新しいブランチ参照を指すように変更され、インデックスがそのコミットのスナップショットで埋められ、次にインデックスの内容があなたのワーキングディレクトリにコピーされます。
Resetの役割
この文脈で見ると、reset
コマンドはより理解できます。
これらの例のために、file.txt
を再度変更し、3回目にコミットしたとしましょう。すると、私たちの履歴は次のようになります

次に、reset
が呼び出されたときに何をするのかを正確に見ていきましょう。それはこれら3つのツリーを単純かつ予測可能な方法で直接操作します。最大で3つの基本的な操作を行います。
ステップ1: HEADの移動
reset
が最初に行うのは、HEADが指しているものを移動することです。これはHEAD自体を変更すること(checkout
が行うこと)とは異なります。reset
はHEADが指しているブランチを移動させます。つまり、HEADがmaster
ブランチに設定されている場合(つまり、現在master
ブランチにいる場合)、git reset 9e5e6a4
を実行すると、まずmaster
が9e5e6a4
を指すように変更されます。

コミットを指定してreset
のどの形式を呼び出すにしても、これが常に最初に行われることです。reset --soft
の場合、そこで停止します。
ここで少し時間を取ってこの図を見て、何が起こったのかを理解してください。それは実質的に最後のgit commit
コマンドを取り消しました。git commit
を実行すると、Gitは新しいコミットを作成し、HEADが指すブランチをそのコミットまで移動させます。HEAD~
(HEADの親)までreset
すると、インデックスやワーキングディレクトリを変更せずに、ブランチを元の場所に戻すことになります。これでインデックスを更新し、再びgit commit
を実行することで、git commit --amend
が達成したであろうことを実現できます(詳細については最後のコミットの変更を参照してください)。
ステップ2: インデックスの更新 (--mixed
)
ここでgit status
を実行すると、インデックスと新しいHEADとの違いが緑色で表示されることに注意してください。
reset
が次に行うのは、HEADが現在指しているスナップショットの内容でインデックスを更新することです。

--mixed
オプションを指定すると、reset
はこの時点で停止します。これはデフォルトでもありますので、オプションを全く指定しない場合(この場合はgit reset HEAD~
のみ)、コマンドはここで停止します。
ここでまた少し時間を取ってこの図を見て、何が起こったのかを理解してください。それは最後のcommit
を取り消しただけでなく、すべてをアンステージしました。すべてのgit add
とgit commit
コマンドを実行する前の状態に戻ったのです。
ステップ3: ワーキングディレクトリの更新 (--hard
)
reset
が3番目に行うのは、ワーキングディレクトリをインデックスと同じ状態にすることです。--hard
オプションを使用すると、この段階まで続行されます。

では、何が起こったのか考えてみましょう。最後のコミット、git add
コマンドとgit commit
コマンド、そしてワーキングディレクトリで行ったすべての作業を取り消したのです。
このフラグ(--hard
)がreset
コマンドを危険にする唯一の方法であり、Gitが実際にデータを破壊する非常に数少ないケースの1つであることに注意することが重要です。reset
の他のどの呼び出しもかなり簡単に元に戻せますが、--hard
オプションはワーキングディレクトリ内のファイルを強制的に上書きするため、元に戻すことができません。この特定のケースでは、ファイルのv3バージョンはまだGit DBのコミットに残っており、reflog
を見れば取り戻すことができますが、もしコミットしていなかった場合、Gitはファイルを上書きしてしまい、復元不可能になっていたでしょう。
要約
reset
コマンドはこれら3つのツリーを特定の順序で上書きし、指示された時点で停止します
-
HEADが指すブランチを移動する (
--soft
の場合、ここで停止)。 -
インデックスをHEADと同じにする (
--hard
でない限り、ここで停止)。 -
ワーキングディレクトリをインデックスと同じにする。
パスを指定したReset
これはreset
の基本的な形式での動作を説明していますが、操作対象となるパスを指定することもできます。パスを指定すると、reset
はステップ1をスキップし、残りのアクションを特定のファイルまたはファイルセットに限定します。これは実際にはある程度理にかなっています。HEADは単なるポインタであり、あるコミットの一部と別のコミットの一部を指すことはできません。しかし、インデックスとワーキングディレクトリは部分的に更新できるため、resetはステップ2と3に進みます。
では、git reset file.txt
を実行したと仮定しましょう。この形式(コミットのSHA-1やブランチを指定せず、--soft
や--hard
も指定しなかったため)は、git reset --mixed HEAD file.txt
の省略形であり、次のように動作します。
-
HEADが指すブランチを移動する (スキップ)。
-
インデックスをHEADと同じにする (ここで停止)。
したがって、実質的にはfile.txt
をHEADからインデックスにコピーするだけです。

これは実質的にファイルをアンステージする効果があります。このコマンドの図を見て、git add
が何をするかを考えると、それらは正確に逆の動作をします。

これが、git status
コマンドの出力がファイルをアンステージするためにこれを実行することを提案する理由です(これについてはステージされたファイルのアンステージを参照してください)。
Gitに「HEADからデータを取得する」と仮定させないで、そのファイルバージョンを取得する特定のコミットを指定することも同様に簡単です。その場合、git reset eb43bf file.txt
のようなものを実行します。

これは実質的に、ワーキングディレクトリ内のファイルのコンテンツをv1に戻し、それにgit add
を実行し、その後再びv3に戻したのと同じことです(実際にそれらすべてのステップを経ることなく)。もし今git commit
を実行すると、そのファイルをv1に戻す変更が記録されますが、実際にそのファイルがワーキングディレクトリに再度あったわけではありません。
また、git add
と同様に、reset
コマンドは--patch
オプションを受け入れ、ハンク単位でコンテンツをアンステージすることができます。したがって、コンテンツを選択的にアンステージまたは元に戻すことができます。
スカッシュ
この新たに得た能力を使って面白いことをする方法を見てみましょう。それはコミットのスカッシュです。
「おっと。」、「WIP」、「このファイルを忘れた」のようなメッセージを持つ一連のコミットがあるとしましょう。reset
を使うと、それらを迅速かつ簡単に一つのコミットにスカッシュして、非常に賢く見せることができます。コミットのスカッシュでは別の方法も紹介されていますが、この例ではreset
を使う方が簡単です。
あるプロジェクトで、最初のコミットにファイルが1つあり、2番目のコミットで新しいファイルが追加され、最初のファイルが変更され、3番目のコミットで最初のファイルが再び変更されたとします。2番目のコミットは作業途中のものであり、それをスカッシュしたいとします。

git reset --soft HEAD~2
を実行すると、HEADブランチを古いコミット(残しておきたい最も新しいコミット)に戻すことができます。

そして、単に再度git commit
を実行します

これで、到達可能な履歴、つまりプッシュする履歴は、file-a.txt
のv1を含む最初のコミットがあり、次にfile-a.txt
をv3に修正し、file-b.txt
を追加した2番目のコミットがあったように見えます。ファイルのv2バージョンを含むコミットは履歴にはもうありません。
チェックアウト
最後に、checkout
とreset
の違いについて疑問に思うかもしれません。reset
と同様に、checkout
も3つのツリーを操作しますが、コマンドにファイルパスを指定するかどうかで少し異なります。
パスなしの場合
git checkout [branch]
の実行は、git reset --hard [branch]
の実行とかなり似ており、3つのツリーすべてを[branch]
のように更新しますが、2つの重要な違いがあります。
まず、reset --hard
とは異なり、checkout
はワーキングディレクトリに対して安全です。変更のあるファイルを吹き飛ばさないように確認します。実際にはそれよりも少し賢く、ワーキングディレクトリ内で簡単なマージを試みるため、変更していないすべてのファイルが更新されます。一方、reset --hard
は、確認なしにすべてを全面的に置き換えます。
2つ目の重要な違いは、checkout
がHEADを更新する方法です。reset
がHEADが指すブランチを移動させるのに対し、checkout
はHEAD自体を別のブランチを指すように移動させます。
例えば、異なるコミットを指すmaster
とdevelop
ブランチがあり、現在develop
にいるとします(そのためHEADがdevelop
を指しています)。git reset master
を実行すると、develop
自体がmaster
が指すのと同じコミットを指すようになります。代わりにgit checkout master
を実行すると、develop
は移動せず、HEAD自体が移動します。HEADはmaster
を指すようになります。
したがって、どちらの場合もHEADをコミットAを指すように移動していますが、その方法は大きく異なります。reset
はHEADが指すブランチを移動させ、checkout
はHEAD自体を移動させます。

git checkout
とgit reset
パスありの場合
checkout
を実行するもう一つの方法はファイルパスを指定する方法で、これはreset
と同様にHEADを移動させません。特定のコミットのファイルでインデックスを更新するという点でgit reset [branch] file
と似ていますが、ワーキングディレクトリのファイルも上書きします。これはまさにgit reset --hard [branch] file
(もしreset
がそれを実行させてくれるなら)と同じです。ワーキングディレクトリに対して安全ではなく、HEADも移動させません。
また、git reset
やgit add
と同様に、checkout
は--patch
オプションを受け入れ、ハンク単位でファイルコンテンツを選択的に元に戻すことができます。
まとめ
これで、reset
コマンドについて理解し、より慣れてきたことと思いますが、checkout
との正確な違いについてはまだ少し混乱しているかもしれませんし、異なる呼び出しのすべてのルールを覚えることは不可能かもしれません。
どのコマンドがどのツリーに影響を与えるかのチートシートを次に示します。「HEAD」列は、そのコマンドがHEADが指す参照(ブランチ)を移動させる場合は「参照」、HEAD自体を移動させる場合は「HEAD」と表示されます。「WD安全?」列には特に注意してください。いいえと表示されている場合は、そのコマンドを実行する前に一秒立ち止まって考えてください。
HEAD | インデックス | ワーキングディレクトリ | WD安全? | |
---|---|---|---|---|
コミットレベル |
||||
|
参照 |
いいえ |
いいえ |
はい |
|
参照 |
はい |
いいえ |
はい |
|
参照 |
はい |
はい |
いいえ |
|
HEAD |
はい |
はい |
はい |
ファイルレベル |
||||
|
いいえ |
はい |
いいえ |
はい |
|
いいえ |
はい |
はい |
いいえ |