-
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 スマート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 リセット徹底解説
- 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 内部コマンドと外部コマンド
- 10.2 Gitオブジェクト
- 10.3 Git参照
- 10.4 パックファイル
- 10.5 リファレンス指定
- 10.6 転送プロトコル
- 10.7 メンテナンスとデータ復旧
- 10.8 環境変数
- 10.9 まとめ
-
付録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 まとめ
-
付録B. アプリケーションへのGitの埋め込み
- A2.1 コマンドラインGit
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
付録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ツール - リセット徹底解説
リセット徹底解説
より専門的なツールに移る前に、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`コマンドは、「内部コマンド」であり、低レベルの処理に使用され、日々の作業ではほとんど使用されませんが、ここで何が起こっているのかを理解するのに役立ちます。
インデックス
インデックスは、**次のコミット候補**です。これは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つのツリーを操作することにより、プロジェクトのスナップショットを徐々に改善された状態で記録することです。

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

この時点では、作業ディレクトリツリーのみにコンテンツがあります。
次に、このファイルをコミットするために、git add
を使用して作業ディレクトリのコンテンツをインデックスにコピーします。

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

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

現在git status
を実行すると、インデックスと作業ディレクトリの間でエントリが異なるため、ファイルが赤で「コミットのためにステージングされていない変更」として表示されます。次に、git add
を実行してインデックスにステージングします。

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

git commit
ステップこれでgit status
を実行しても出力されなくなります。3つのツリーすべてが再び同じになったためです。
ブランチの切り替えやクローン作成も同様のプロセスで行われます。ブランチをチェックアウトすると、HEADが新しいブランチ参照を指すように変更され、そのコミットのスナップショットでインデックスが設定され、インデックスの内容が作業ディレクトリにコピーされます。
リセットの役割
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
オプションは、作業ディレクトリのファイルを強制的に上書きするため、元に戻すことができません。この特定のケースでは、Git DBのコミットにファイルのv3バージョンはまだあり、reflog
を確認することで復元できますが、コミットしていなかった場合は、Gitはまだファイルを上書きし、復元できなくなります。
要約
reset
コマンドは、指示された時点で停止する特定の順序でこれら3つのツリーを上書きします。
-
HEADが指しているブランチを移動する(
--soft
の場合はここで停止)。 -
インデックスをHEADと同じにする(
--hard
でない限りここで停止)。 -
作業ディレクトリをインデックスと同じにする。
パスを使用したリセット
これは、reset
の基本的な動作を網羅していますが、作用させるパスも指定できます。パスを指定すると、reset
はステップ1をスキップし、残りの操作を特定のファイルまたはファイルのセットに制限します。これは実際には理にかなっています。HEADは単なるポインタであり、あるコミットの一部と別のコミットの一部を指すことはできません。しかし、インデックスと作業ディレクトリは部分的に更新できるため、resetはステップ2と3に進みます。
そこで、git reset file.txt
を実行するとします。この形式(コミットSHA-1またはブランチを指定せず、--soft
または--hard
を指定していないため)はgit reset --mixed HEAD file.txt
の略記であり、次のようになります。
-
HEADが指しているブランチを移動する(スキップ)。
-
インデックスをHEADと同じにする(ここで停止)。
基本的に、HEADからインデックスにfile.txt
をコピーするだけです。

これは、ファイルをステージング解除するという実際的な効果があります。そのコマンドの図を見て、git add
が実行することを考えると、それらは正反対です。

そのため、git status
コマンドの出力が、ファイルをステージング解除するためにこれを実行することを示唆しています(詳細については、ステージングされたファイルのステージング解除を参照)。
特定のコミットからそのファイルのバージョンを取得する必要があるとGitが推測するのを防ぐために、特定のコミットを指定することもできます。git reset eb43bf file.txt
を実行するだけです。

これは、作業ディレクトリでファイルのコンテンツをv1に戻し、git add
を実行して、v3に戻した(実際にはそれらのすべてのステップを実行していない)場合と事実上同じです。ここでgit commit
を実行すると、ファイルがv1に戻る変更が記録されますが、作業ディレクトリに再びファイルがあったわけではありません。
git add
と同様に、reset
コマンドは--patch
オプションを受け入れ、変更単位でコンテンツのステージングを解除することも興味深い点です。そのため、コンテンツを選択的にステージング解除したり、元に戻したりできます。
圧縮
この新しく得られた能力で何か面白いことをしてみましょう。コミットの圧縮です。
「oops」、「WIP」、「このファイルは忘れていました」のようなメッセージを含む一連のコミットがあるとします。reset
を使用して、それらを簡単に1つのコミットに圧縮し、非常にスマートに見せることができます。コミットの圧縮では別の方法を示していますが、この例ではreset
を使用する方が簡単です。
最初のコミットに1つのファイルがあり、2番目のコミットで新しいファイルが追加され、最初のファイルが変更され、3番目のコミットで最初のファイルが再び変更されたプロジェクトがあるとします。2番目のコミットは進行中の作業であり、圧縮したいと考えています。

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

そして、git commit
を再度実行するだけです。

これで、到達可能な履歴、つまりプッシュする履歴は、file-a.txt
のv1を含む1つのコミットがあり、次にfile-a.txt
をv3に変更し、file-b.txt
を追加した2番目のコミットがあるように見えます。ファイルのv2バージョンのコミットは履歴に存在しなくなりました。
確認してみましょう
最後に、checkout
とreset
の違いについて疑問に思われるかもしれません。reset
と同様に、checkout
は3つのツリーを操作しますが、コマンドにファイルパスを指定するかどうかによって少し異なります。
パスなしの場合
git checkout [ブランチ]
を実行することは、git reset --hard [ブランチ]
を実行することと非常によく似ており、[ブランチ]
のように見えるように3つのツリーすべてを更新しますが、2つの重要な違いがあります。
まず、reset --hard
とは異なり、checkout
は作業ディレクトリを安全に保ちます。変更されたファイルが失われないように確認します。実際には、それよりも少し賢く、作業ディレクトリで簡単なマージを試みるため、変更していないファイルはすべて更新されます。一方、reset --hard
は、確認せずにすべてを全面的に置き換えます。
2つ目の重要な違いは、checkout
がHEADを更新する方法です。reset
はHEADが指すブランチを移動するのに対し、checkout
はHEAD自体を別のブランチを指すように移動します。
例えば、異なるコミットを指すmaster
ブランチとdevelop
ブランチがあり、現在develop
ブランチにいる(つまりHEADがそれを指している)とします。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 [ブランチ] ファイル
と同じですが、作業ディレクトリのファイルも上書きします。これはgit reset --hard [ブランチ] ファイル
(もしreset
がそれを実行できたとしたら)と全く同じです。作業ディレクトリは安全ではなく、HEADは移動しません。
また、git reset
やgit add
と同様に、checkout
は--patch
オプションを受け入れ、変更単位でファイルの内容を選択的に元に戻すことができます。
概要
これでreset
コマンドを理解し、より使いやすくなったと思いますが、checkout
とどのように異なるのか、そしてさまざまな呼び出しのすべてのルールを覚えることは不可能であるため、まだ少し混乱しているかもしれません。
どのコマンドがどのツリーに影響を与えるかのチートシートを次に示します。「HEAD」列には、そのコマンドがHEADが指す参照(ブランチ)を移動する場合は「参照」、HEAD自体を移動する場合は「HEAD」と表示されます。'作業ディレクトリ安全?'列に特に注意してください。**いいえ**と表示されている場合は、そのコマンドを実行する前に少し考えてください。
HEAD | インデックス | 作業ディレクトリ | 作業ディレクトリ安全? | |
---|---|---|---|---|
コミットレベル |
||||
|
参照 |
いいえ |
いいえ |
はい |
|
参照 |
はい |
いいえ |
はい |
|
参照 |
はい |
はい |
いいえ |
|
HEAD |
はい |
はい |
はい |
ファイルレベル |
||||
|
いいえ |
はい |
いいえ |
はい |
|
いいえ |
はい |
はい |
いいえ |